major architecture refactor

This commit is contained in:
juancwu 2026-01-23 03:10:53 +00:00
commit f82d06dbf0
11 changed files with 636 additions and 272 deletions

View file

@ -0,0 +1,20 @@
package messages
import "git.juancwu.dev/juancwu/porkbacon/internal/porkbun"
type Page int
const (
PageLogin Page = iota
PageMenu
)
type SwitchPageMsg struct {
Page Page
}
type SessionReadyMsg struct {
Client *porkbun.Client
}
type ErrorMsg error

86
internal/ui/model.go Normal file
View file

@ -0,0 +1,86 @@
package ui
import (
"fmt"
"os"
"git.juancwu.dev/juancwu/porkbacon/internal/config"
"git.juancwu.dev/juancwu/porkbacon/internal/ui/messages"
"git.juancwu.dev/juancwu/porkbacon/internal/ui/pages/login"
"git.juancwu.dev/juancwu/porkbacon/internal/ui/pages/menu"
tea "github.com/charmbracelet/bubbletea"
)
type MainModel struct {
currentPage messages.Page
login *login.Model
menu *menu.Model
isMenuInit bool
width int
height int
}
func New() MainModel {
cfg, err := config.Load()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
os.Exit(1)
}
return MainModel{
currentPage: messages.PageLogin,
login: login.New(cfg),
}
}
func (m MainModel) Init() tea.Cmd {
return m.login.Init()
}
func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
case messages.SwitchPageMsg:
m.currentPage = msg.Page
return m, nil
case messages.SessionReadyMsg:
m.menu = menu.New(msg.Client)
if m.width > 0 && m.height > 0 {
newMenu, _ := m.menu.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
m.menu = newMenu.(*menu.Model)
}
m.currentPage = messages.PageMenu
m.isMenuInit = true
return m, m.menu.Init()
}
switch m.currentPage {
case messages.PageLogin:
var newLogin tea.Model
newLogin, cmd = m.login.Update(msg)
m.login = newLogin.(*login.Model)
case messages.PageMenu:
var newMenu tea.Model
newMenu, cmd = m.menu.Update(msg)
m.menu = newMenu.(*menu.Model)
}
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (m MainModel) View() string {
switch m.currentPage {
case messages.PageLogin:
return m.login.View()
case messages.PageMenu:
return m.menu.View()
default:
return "Unknown Page"
}
}

View 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}
}
}

View 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
}