feat: Task 15 — TUI Tab Framework + Orange Theme

- pkg/tui/styles.go — orange theme palette and component styles
- pkg/tui/tabs.go — Tab interface + TabManager with add/close/next/prev
- pkg/tui/host_list_tab.go — HostListTab implementing Tab interface
- pkg/tui/tui.go — refactored to use TabManager (backward compatible)
- pkg/tui/host_list.go — removed (logic moved to host_list_tab.go)
- pkg/tui/tui_test.go — adapted + added TabManager tests
This commit is contained in:
swanadiva
2026-06-23 14:51:13 +07:00
parent c5cc3bd711
commit 5d9e38f8db
6 changed files with 523 additions and 126 deletions
+39 -40
View File
@@ -2,22 +2,11 @@ package tui
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"git.tukangketik.id/swanadiva/hostkeeper/internal/models"
)
// Styles for TUI components
var (
TitleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("86")).Bold(true)
SubtitleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
HighlightStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Bold(true)
ErrorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Bold(true)
SuccessStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("76")).Bold(true)
InfoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("117"))
)
// Screen represents different TUI screens
// Screen represents different TUI screens (deprecated, use tabs)
type Screen int
const (
@@ -28,16 +17,21 @@ const (
// Model represents the main TUI model
type Model struct {
CurrentScreen Screen
Hosts []*models.Host
SelectedIndex int
tabs *TabManager
CurrentScreen Screen // deprecated, kept for backward compat
Hosts []*models.Host // deprecated
SelectedIndex int // deprecated
Error error
Quit bool
}
// New creates a new TUI model
func New() *Model {
hostList := NewHostListTab()
tm := NewTabManager(hostList)
return &Model{
tabs: tm,
CurrentScreen: ScreenHostList,
SelectedIndex: 0,
Quit: false,
@@ -46,7 +40,7 @@ func New() *Model {
// Init initializes the TUI
func (m *Model) Init() tea.Cmd {
return nil
return m.tabs.Init()
}
// Update handles messages and updates the model
@@ -57,45 +51,50 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "ctrl+c", "q":
m.Quit = true
return m, tea.Quit
case "up", "k":
if m.SelectedIndex > 0 {
m.SelectedIndex--
}
case "down", "j":
if m.SelectedIndex < len(m.Hosts)-1 {
m.SelectedIndex++
}
case "enter", " ":
if len(m.Hosts) > 0 {
return m, tea.Quit
}
}
}
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"
}
switch m.CurrentScreen {
case ScreenHostList:
return renderHostList(m)
default:
return "Screen not implemented yet"
if m.tabs == nil || m.tabs.Len() == 0 {
return "No tabs open. Press 'q' to quit.\n"
}
return m.tabs.View()
}
// LoadHosts loads hosts into the TUI model
func (m *Model) LoadHosts(hosts []*models.Host) {
m.Hosts = hosts
if len(hosts) > 0 && m.SelectedIndex >= len(hosts) {
m.SelectedIndex = len(hosts) - 1
if m.tabs == nil {
return
}
if ht := FindHostListTab(m.tabs.tabs); ht != nil {
ht.SetHosts(hosts)
m.Hosts = hosts
}
}
// TabManager returns the underlying tab manager
func (m *Model) TabManager() *TabManager {
return m.tabs
}