package tui import ( "context" tea "github.com/charmbracelet/bubbletea" "git.tukangketik.id/swanadiva/hostkeeper/internal/models" "git.tukangketik.id/swanadiva/hostkeeper/pkg/storage" ) // Screen represents different TUI screens (deprecated, use tabs) type Screen int const ( ScreenHostList Screen = iota ScreenConnection ScreenSettings ) // Model represents the main TUI model type Model struct { tabs *TabManager CurrentScreen Screen // deprecated, kept for backward compat Hosts []*models.Host // deprecated SelectedIndex int // deprecated Error error Quit bool dataDir string } // New creates a new TUI model func New() *Model { hostList := NewHostListTab() tm := NewTabManager(hostList) return &Model{ tabs: tm, CurrentScreen: ScreenHostList, SelectedIndex: 0, Quit: false, } } // Init initializes the TUI func (m *Model) Init() tea.Cmd { return m.tabs.Init() } // Update handles messages and updates the model func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: // Only hard quit on ctrl+c (let tabs handle q) if msg.String() == "ctrl+c" { m.Quit = true return m, tea.Quit } case quitMsg: m.Quit = true return m, tea.Quit case sshConnectToMsg: return m, tea.Batch(tea.ClearScreen, sshConnectCmd(msg.host, m.dataDir)) case sshExitMsg: if msg.err != nil { m.Error = msg.err } return m, tea.ClearScreen case openHostFormMsg: var tab Tab if msg.editing != nil { tab = NewEditHostFormTab(msg.editing, m.dataDir) } else { tab = NewAddHostFormTab(m.dataDir) } cmd := m.tabs.Add(tab) return m, cmd case openSFTPMsg: tab := NewSFTPBrowserTab(msg.host, m.dataDir) cmd := m.tabs.Add(tab) return m, cmd case openKeyListMsg: tab := NewKeyListTab(m.dataDir) cmd := m.tabs.Add(tab) return m, cmd case openSnippetListMsg: tab := NewSnippetListTab(m.dataDir) cmd := m.tabs.Add(tab) return m, cmd case openKeyFormMsg: var tab Tab if msg.editing != nil { tab = NewEditKeyFormTab(msg.editing, m.dataDir) } else { tab = NewAddKeyFormTab(m.dataDir) } cmd := m.tabs.Add(tab) return m, cmd case openSnippetFormMsg: var tab Tab if msg.editing != nil { tab = NewEditSnippetFormTab(msg.editing, m.dataDir) } else { tab = NewAddSnippetFormTab(m.dataDir) } cmd := m.tabs.Add(tab) return m, cmd case closeFormMsg: if m.tabs.Len() > 1 { m.tabs.CloseActive() } return m, nil case saveHostResultMsg: if msg.err != nil { m.Error = msg.err return m, nil } // Close the form tab, switch back to host list if m.tabs.Len() > 1 { m.tabs.CloseActive() } // Reload hosts return m, func() tea.Msg { store, err := storage.NewJSONStorage(m.dataDir) if err != nil { return nil } hosts, err := store.ListHosts(context.Background()) if err != nil { return nil } return loadedHostsMsg{hosts: hosts} } case loadedHostsMsg: m.LoadHosts(msg.hosts) case saveKeyResultMsg: if msg.err != nil { m.Error = msg.err return m, nil } // Close form tab and return to list (which auto-refreshes via Init) if m.tabs.Len() > 1 { m.tabs.CloseActive() } return m, nil case deleteKeyResultMsg: if msg.err != nil { m.Error = msg.err } return m, nil case saveSnippetResultMsg: if msg.err != nil { m.Error = msg.err return m, nil } if m.tabs.Len() > 1 { m.tabs.CloseActive() } return m, nil case deleteSnippetResultMsg: if msg.err != nil { m.Error = msg.err } return m, nil } cmd, err := m.tabs.Update(msg) if err != nil { m.Error = err } // Sync deprecated fields if ht := FindHostListTab(m.tabs.tabs); ht != nil { m.Hosts = ht.Hosts() m.SelectedIndex = ht.SelectedIndex() } return m, cmd } // View renders the TUI func (m *Model) View() string { if m.Quit { m.tabs = nil return "Thanks for using hostkeeper!\n" } if m.tabs == nil || m.tabs.Len() == 0 { return "No tabs open. Press 'q' to quit.\n" } // Pass error to host list tab for display if m.Error != nil { if ht := FindHostListTab(m.tabs.tabs); ht != nil { ht.err = m.Error } m.Error = nil } return m.tabs.View() } // LoadHosts loads hosts into the TUI model func (m *Model) LoadHosts(hosts []*models.Host) { if m.tabs == nil { return } if ht := FindHostListTab(m.tabs.tabs); ht != nil { ht.SetHosts(hosts) m.Hosts = hosts } } // SetDataDir sets the data directory for SSH connections func (m *Model) SetDataDir(dir string) { m.dataDir = dir } // TabManager returns the underlying tab manager func (m *Model) TabManager() *TabManager { return m.tabs }