major architecture refactor
This commit is contained in:
parent
4266bfbfc2
commit
f82d06dbf0
11 changed files with 636 additions and 272 deletions
192
internal/ui/pages/login/model.go
Normal file
192
internal/ui/pages/login/model.go
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
package login
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.juancwu.dev/juancwu/porkbacon/internal/config"
|
||||
"git.juancwu.dev/juancwu/porkbacon/internal/porkbun"
|
||||
"git.juancwu.dev/juancwu/porkbacon/internal/ui/messages"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type Mode int
|
||||
|
||||
const (
|
||||
ModeSetup Mode = iota
|
||||
ModeUnlock
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
cfg *config.Config
|
||||
inputs []textinput.Model
|
||||
focusIndex int
|
||||
mode Mode
|
||||
err error
|
||||
}
|
||||
|
||||
func New(cfg *config.Config) *Model {
|
||||
m := Model{
|
||||
cfg: cfg,
|
||||
}
|
||||
|
||||
if cfg.HasCredentials() {
|
||||
m.mode = ModeUnlock
|
||||
m.inputs = make([]textinput.Model, 1)
|
||||
m.inputs[0] = textinput.New()
|
||||
m.inputs[0].Placeholder = "Master Password"
|
||||
m.inputs[0].EchoMode = textinput.EchoPassword
|
||||
m.inputs[0].Width = 50
|
||||
m.inputs[0].Focus()
|
||||
} else {
|
||||
m.mode = ModeSetup
|
||||
m.inputs = make([]textinput.Model, 3)
|
||||
|
||||
m.inputs[0] = textinput.New()
|
||||
m.inputs[0].Placeholder = "API Key"
|
||||
m.inputs[0].EchoMode = textinput.EchoPassword
|
||||
m.inputs[0].Width = 50
|
||||
m.inputs[0].Focus()
|
||||
|
||||
m.inputs[1] = textinput.New()
|
||||
m.inputs[1].Placeholder = "Secret API Key"
|
||||
m.inputs[1].EchoMode = textinput.EchoPassword
|
||||
m.inputs[1].Width = 50
|
||||
|
||||
m.inputs[2] = textinput.New()
|
||||
m.inputs[2].Placeholder = "Master Password (to encrypt keys)"
|
||||
m.inputs[2].EchoMode = textinput.EchoPassword
|
||||
m.inputs[2].Width = 50
|
||||
}
|
||||
|
||||
return &m
|
||||
}
|
||||
|
||||
func (m *Model) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "tab", "shift+tab", "enter", "up", "down":
|
||||
s := msg.String()
|
||||
|
||||
if s == "enter" && m.focusIndex == len(m.inputs)-1 {
|
||||
return m.submit()
|
||||
}
|
||||
|
||||
if s == "up" || s == "shift+tab" {
|
||||
m.focusIndex--
|
||||
} else {
|
||||
m.focusIndex++
|
||||
}
|
||||
|
||||
if m.focusIndex > len(m.inputs)-1 {
|
||||
m.focusIndex = 0
|
||||
} else if m.focusIndex < 0 {
|
||||
m.focusIndex = len(m.inputs) - 1
|
||||
}
|
||||
|
||||
cmds := make([]tea.Cmd, len(m.inputs))
|
||||
for i := 0; i <= len(m.inputs)-1; i++ {
|
||||
if i == m.focusIndex {
|
||||
cmds[i] = m.inputs[i].Focus()
|
||||
continue
|
||||
}
|
||||
m.inputs[i].Blur()
|
||||
}
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle character input and blinking
|
||||
cmd := m.updateInputs(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *Model) updateInputs(msg tea.Msg) tea.Cmd {
|
||||
cmds := make([]tea.Cmd, len(m.inputs))
|
||||
for i := range m.inputs {
|
||||
m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
|
||||
}
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *Model) View() string {
|
||||
var b strings.Builder
|
||||
|
||||
if m.mode == ModeSetup {
|
||||
b.WriteString("Welcome to Porkbacon! Let's set up your credentials.\n\n")
|
||||
} else {
|
||||
b.WriteString("Welcome back! Please unlock your vault.\n\n")
|
||||
}
|
||||
|
||||
for i := range m.inputs {
|
||||
b.WriteString(m.inputs[i].View())
|
||||
if i < len(m.inputs)-1 {
|
||||
b.WriteRune('\n')
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n\n(Press Enter to confirm, Ctrl+C to quit)\n")
|
||||
|
||||
if m.err != nil {
|
||||
b.WriteString(fmt.Sprintf("\nError: %v\n", m.err))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m *Model) submit() (tea.Model, tea.Cmd) {
|
||||
var client *porkbun.Client
|
||||
|
||||
if m.mode == ModeSetup {
|
||||
apiKey := m.inputs[0].Value()
|
||||
secretKey := m.inputs[1].Value()
|
||||
password := m.inputs[2].Value()
|
||||
|
||||
if apiKey == "" || secretKey == "" || password == "" {
|
||||
m.err = fmt.Errorf("all fields are required")
|
||||
return m, nil
|
||||
}
|
||||
|
||||
client = porkbun.New(apiKey, secretKey)
|
||||
_, err := client.Ping()
|
||||
if err != nil {
|
||||
m.err = err
|
||||
return m, nil
|
||||
}
|
||||
|
||||
err = m.cfg.SetCredentials(apiKey, secretKey, password)
|
||||
if err != nil {
|
||||
m.err = err
|
||||
return m, nil
|
||||
}
|
||||
|
||||
err = m.cfg.Save()
|
||||
if err != nil {
|
||||
m.err = err
|
||||
return m, nil
|
||||
}
|
||||
} else {
|
||||
password := m.inputs[0].Value()
|
||||
apiKey, secretKey, err := m.cfg.GetCredentials(password)
|
||||
if err != nil {
|
||||
m.err = err
|
||||
// Clear password field on error
|
||||
m.inputs[0].SetValue("")
|
||||
return m, nil
|
||||
}
|
||||
|
||||
client = porkbun.New(apiKey, secretKey)
|
||||
}
|
||||
|
||||
return m, func() tea.Msg {
|
||||
return messages.SessionReadyMsg{Client: client}
|
||||
}
|
||||
}
|
||||
124
internal/ui/pages/menu/model.go
Normal file
124
internal/ui/pages/menu/model.go
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
package menu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.juancwu.dev/juancwu/porkbacon/internal/porkbun"
|
||||
"git.juancwu.dev/juancwu/porkbacon/internal/ui/messages"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type pingResultMsg struct {
|
||||
IP string
|
||||
}
|
||||
|
||||
type item struct {
|
||||
id uint8
|
||||
title, desc string
|
||||
}
|
||||
|
||||
func (i item) ID() uint8 { return i.id }
|
||||
func (i item) Title() string { return i.title }
|
||||
func (i item) Description() string { return i.desc }
|
||||
func (i item) FilterValue() string { return i.title }
|
||||
|
||||
const (
|
||||
domainListAll uint8 = iota
|
||||
domainGetDetails
|
||||
dnsRetrieveRecords
|
||||
dnsCreateRecord
|
||||
dnsEditRecord
|
||||
dnsDeleteRecord
|
||||
utilPing
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
list list.Model
|
||||
client *porkbun.Client
|
||||
err error
|
||||
output string
|
||||
}
|
||||
|
||||
func New(client *porkbun.Client) *Model {
|
||||
items := []list.Item{
|
||||
item{id: domainListAll, title: "Domain: List All", desc: "List all domains in your account"},
|
||||
item{id: domainGetDetails, title: "Domain: Get Details", desc: "Get details for a specific domain"},
|
||||
item{id: dnsRetrieveRecords, title: "DNS: Retrieve Records", desc: "Retrieve DNS records for a domain"},
|
||||
item{id: dnsCreateRecord, title: "DNS: Create Record", desc: "Create a new DNS record"},
|
||||
item{id: dnsEditRecord, title: "DNS: Edit Record", desc: "Edit an existing DNS record"},
|
||||
item{id: dnsDeleteRecord, title: "DNS: Delete Record", desc: "Delete a DNS record"},
|
||||
item{id: utilPing, title: "Util: Ping", desc: "Ping Porkbun"},
|
||||
}
|
||||
|
||||
l := list.New(items, list.NewDefaultDelegate(), 0, 0)
|
||||
l.Title = "Porkbun Actions"
|
||||
|
||||
return &Model{
|
||||
list: l,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.list.SetWidth(msg.Width)
|
||||
m.list.SetHeight(msg.Height)
|
||||
return m, nil
|
||||
|
||||
case tea.KeyMsg:
|
||||
if m.output != "" {
|
||||
if msg.String() == "esc" {
|
||||
m.output = ""
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if msg.String() == "enter" {
|
||||
i, ok := m.list.SelectedItem().(item)
|
||||
if ok {
|
||||
return m.handleSelection(i)
|
||||
}
|
||||
}
|
||||
|
||||
case pingResultMsg:
|
||||
m.output = fmt.Sprintf("Ping successful!\nYour IP: %s", msg.IP)
|
||||
return m, nil
|
||||
|
||||
case messages.ErrorMsg:
|
||||
m.output = fmt.Sprintf("Error: %v", msg)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.list, cmd = m.list.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *Model) View() string {
|
||||
if m.output != "" {
|
||||
return fmt.Sprintf("%s\n\n(Press Esc to go back)", m.output)
|
||||
}
|
||||
return m.list.View()
|
||||
}
|
||||
|
||||
func (m *Model) handleSelection(i item) (tea.Model, tea.Cmd) {
|
||||
// TODO: Implement other actions. For now, just Ping.
|
||||
switch i.id {
|
||||
case utilPing:
|
||||
return m, func() tea.Msg {
|
||||
resp, err := m.client.Ping()
|
||||
if err != nil {
|
||||
return messages.ErrorMsg(err)
|
||||
}
|
||||
return pingResultMsg{IP: resp.YourIP}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue