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:
@@ -1,84 +0,0 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"git.tukangketik.id/swanadiva/hostkeeper/internal/models"
|
||||
)
|
||||
|
||||
var (
|
||||
HostNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("226")).Bold(true)
|
||||
HostDetailStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
|
||||
SelectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Background(lipgloss.Color("235")).Padding(0, 1)
|
||||
TagStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("86"))
|
||||
)
|
||||
|
||||
// renderHostList renders the host list screen
|
||||
func renderHostList(m *Model) string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(TitleStyle.Render("HOSTKEEPER - SSH Manager"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if len(m.Hosts) == 0 {
|
||||
b.WriteString(SubtitleStyle.Render("No hosts found. Add your first host with: hostkeeper add"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(InfoStyle.Render("Press 'q' to quit"))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
for i, host := range m.Hosts {
|
||||
if i == m.SelectedIndex {
|
||||
b.WriteString(renderSelectedHost(host))
|
||||
} else {
|
||||
b.WriteString(renderHost(host))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(SubtitleStyle.Render("\u2191\u2193: Navigate | Enter: Connect | q: Quit"))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// renderHost renders a single host
|
||||
func renderHost(host *models.Host) string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(HostNameStyle.Render(host.Name))
|
||||
b.WriteString("\n")
|
||||
|
||||
details := fmt.Sprintf(" %s@%s:%d", host.Username, host.Hostname, host.Port)
|
||||
b.WriteString(HostDetailStyle.Render(details))
|
||||
|
||||
if len(host.Tags) > 0 {
|
||||
tags := formatTagsForTUI(host.Tags)
|
||||
b.WriteString(" " + TagStyle.Render(tags))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// renderSelectedHost renders the selected host with highlight
|
||||
func renderSelectedHost(host *models.Host) string {
|
||||
hostText := renderHost(host)
|
||||
return SelectedStyle.Render(hostText)
|
||||
}
|
||||
|
||||
// formatTagsForTUI formats tags for TUI display
|
||||
func formatTagsForTUI(tags []string) string {
|
||||
if len(tags) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var formatted []string
|
||||
for _, tag := range tags {
|
||||
formatted = append(formatted, "["+tag+"]")
|
||||
}
|
||||
|
||||
return strings.Join(formatted, " ")
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"git.tukangketik.id/swanadiva/hostkeeper/internal/models"
|
||||
)
|
||||
|
||||
// HostListTab is the host list tab
|
||||
type HostListTab struct {
|
||||
hosts []*models.Host
|
||||
selectedIndex int
|
||||
err error
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
// NewHostListTab creates a new host list tab
|
||||
func NewHostListTab() *HostListTab {
|
||||
return &HostListTab{
|
||||
selectedIndex: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes the tab
|
||||
func (t *HostListTab) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the tab name
|
||||
func (t *HostListTab) Name() string {
|
||||
return "Hosts"
|
||||
}
|
||||
|
||||
// Update handles messages for the host list
|
||||
func (t *HostListTab) Update(msg tea.Msg) (Tab, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
t.width = msg.Width
|
||||
t.height = msg.Height
|
||||
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "up", "k":
|
||||
if t.selectedIndex > 0 {
|
||||
t.selectedIndex--
|
||||
}
|
||||
|
||||
case "down", "j":
|
||||
if t.selectedIndex < len(t.hosts)-1 {
|
||||
t.selectedIndex++
|
||||
}
|
||||
|
||||
case "enter", " ":
|
||||
if len(t.hosts) > 0 {
|
||||
return t, tea.Quit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// View renders the host list
|
||||
func (t *HostListTab) View() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(AppTitleStyle.Render("HOSTKEEPER - SSH Manager"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if len(t.hosts) == 0 {
|
||||
b.WriteString(SubtitleStyle.Render("No hosts found. Add your first host with: hostkeeper add"))
|
||||
b.WriteString("\n\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
for i, host := range t.hosts {
|
||||
if i == t.selectedIndex {
|
||||
b.WriteString(renderSelectedHost(host))
|
||||
} else {
|
||||
b.WriteString(renderHost(host))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// SetHosts sets the host list
|
||||
func (t *HostListTab) SetHosts(hosts []*models.Host) {
|
||||
t.hosts = hosts
|
||||
if len(hosts) > 0 && t.selectedIndex >= len(hosts) {
|
||||
t.selectedIndex = len(hosts) - 1
|
||||
}
|
||||
}
|
||||
|
||||
// Hosts returns the host list
|
||||
func (t *HostListTab) Hosts() []*models.Host {
|
||||
return t.hosts
|
||||
}
|
||||
|
||||
// SelectedIndex returns the selected index
|
||||
func (t *HostListTab) SelectedIndex() int {
|
||||
return t.selectedIndex
|
||||
}
|
||||
|
||||
// FindHostListTab finds the first HostListTab in a list of tabs
|
||||
func FindHostListTab(tabs []Tab) *HostListTab {
|
||||
for _, tab := range tabs {
|
||||
if ht, ok := tab.(*HostListTab); ok {
|
||||
return ht
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderHost renders a single host
|
||||
func renderHost(host *models.Host) string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(HostNameStyle.Render(host.Name))
|
||||
b.WriteString("\n")
|
||||
|
||||
details := fmt.Sprintf(" %s@%s:%d", host.Username, host.Hostname, host.Port)
|
||||
b.WriteString(HostDetailStyle.Render(details))
|
||||
|
||||
if len(host.Tags) > 0 {
|
||||
tags := formatTagsForTUI(host.Tags)
|
||||
b.WriteString(" " + TagStyle.Render(tags))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// renderSelectedHost renders the selected host with highlight
|
||||
func renderSelectedHost(host *models.Host) string {
|
||||
hostText := renderHost(host)
|
||||
return SelectedStyle.Render(hostText)
|
||||
}
|
||||
|
||||
// formatTagsForTUI formats tags for TUI display
|
||||
func formatTagsForTUI(tags []string) string {
|
||||
if len(tags) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var formatted []string
|
||||
for _, tag := range tags {
|
||||
formatted = append(formatted, "["+tag+"]")
|
||||
}
|
||||
|
||||
return strings.Join(formatted, " ")
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package tui
|
||||
|
||||
import "github.com/charmbracelet/lipgloss"
|
||||
|
||||
// Orange theme palette
|
||||
var (
|
||||
OrangePrimary = lipgloss.Color("#FF6B00")
|
||||
OrangeSecondary = lipgloss.Color("#FF9F43")
|
||||
OrangeLight = lipgloss.Color("#FFB800")
|
||||
DarkBg = lipgloss.Color("#1A1A1A")
|
||||
LightBg = lipgloss.Color("#2D2D2D")
|
||||
TextPrimary = lipgloss.Color("#FFFFFF")
|
||||
TextSecondary = lipgloss.Color("#AAAAAA")
|
||||
GreenSuccess = lipgloss.Color("#00FF88")
|
||||
RedError = lipgloss.Color("#FF4444")
|
||||
BlueInfo = lipgloss.Color("#44AAFF")
|
||||
)
|
||||
|
||||
// Component styles
|
||||
var (
|
||||
TabActiveStyle = lipgloss.NewStyle().Background(OrangePrimary).Foreground(DarkBg).Bold(true).Padding(0, 2)
|
||||
TabInactiveStyle = lipgloss.NewStyle().Background(LightBg).Foreground(TextSecondary).Padding(0, 2)
|
||||
TabBarStyle = lipgloss.NewStyle().Background(DarkBg)
|
||||
StatusBarStyle = lipgloss.NewStyle().Background(OrangePrimary).Foreground(DarkBg).Padding(0, 1)
|
||||
AppTitleStyle = lipgloss.NewStyle().Foreground(OrangeSecondary).Bold(true)
|
||||
HighlightStyle = lipgloss.NewStyle().Foreground(OrangePrimary).Bold(true)
|
||||
SelectedStyle = lipgloss.NewStyle().Foreground(DarkBg).Background(OrangePrimary).Padding(0, 1)
|
||||
ErrorStyle = lipgloss.NewStyle().Foreground(RedError).Bold(true)
|
||||
SuccessStyle = lipgloss.NewStyle().Foreground(GreenSuccess).Bold(true)
|
||||
InfoStyle = lipgloss.NewStyle().Foreground(BlueInfo)
|
||||
SubtitleStyle = lipgloss.NewStyle().Foreground(TextSecondary)
|
||||
HostNameStyle = lipgloss.NewStyle().Foreground(OrangeLight).Bold(true)
|
||||
HostDetailStyle = lipgloss.NewStyle().Foreground(TextSecondary)
|
||||
TagStyle = lipgloss.NewStyle().Foreground(GreenSuccess)
|
||||
TitleStyle = AppTitleStyle
|
||||
)
|
||||
|
||||
// TabWidth returns the width of the tab bar content
|
||||
func TabBarWidth(totalWidth int) int {
|
||||
if totalWidth < 10 {
|
||||
return totalWidth
|
||||
}
|
||||
return totalWidth - 2
|
||||
}
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// Tab represents a single tab in the TUI
|
||||
type Tab interface {
|
||||
Init() tea.Cmd
|
||||
Update(tea.Msg) (Tab, tea.Cmd)
|
||||
View() string
|
||||
Name() string
|
||||
}
|
||||
|
||||
// TabManager manages multiple tabs
|
||||
type TabManager struct {
|
||||
tabs []Tab
|
||||
active int
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
// NewTabManager creates a new TabManager with an initial tab
|
||||
func NewTabManager(initial Tab) *TabManager {
|
||||
return &TabManager{
|
||||
tabs: []Tab{initial},
|
||||
active: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Active returns the currently active tab
|
||||
func (tm *TabManager) Active() Tab {
|
||||
if len(tm.tabs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return tm.tabs[tm.active]
|
||||
}
|
||||
|
||||
// Add adds a new tab and switches to it
|
||||
func (tm *TabManager) Add(tab Tab) {
|
||||
tm.tabs = append(tm.tabs, tab)
|
||||
tm.active = len(tm.tabs) - 1
|
||||
}
|
||||
|
||||
// Close removes the tab at index and returns the active tab
|
||||
func (tm *TabManager) Close(index int) Tab {
|
||||
if index < 0 || index >= len(tm.tabs) {
|
||||
return nil
|
||||
}
|
||||
|
||||
tm.tabs = append(tm.tabs[:index], tm.tabs[index+1:]...)
|
||||
|
||||
if len(tm.tabs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if tm.active >= len(tm.tabs) {
|
||||
tm.active = len(tm.tabs) - 1
|
||||
}
|
||||
return tm.tabs[tm.active]
|
||||
}
|
||||
|
||||
// CloseActive closes the active tab
|
||||
func (tm *TabManager) CloseActive() Tab {
|
||||
if len(tm.tabs) <= 1 {
|
||||
return nil
|
||||
}
|
||||
return tm.Close(tm.active)
|
||||
}
|
||||
|
||||
// Next switches to the next tab
|
||||
func (tm *TabManager) Next() {
|
||||
if len(tm.tabs) <= 1 {
|
||||
return
|
||||
}
|
||||
tm.active = (tm.active + 1) % len(tm.tabs)
|
||||
}
|
||||
|
||||
// Prev switches to the previous tab
|
||||
func (tm *TabManager) Prev() {
|
||||
if len(tm.tabs) <= 1 {
|
||||
return
|
||||
}
|
||||
tm.active--
|
||||
if tm.active < 0 {
|
||||
tm.active = len(tm.tabs) - 1
|
||||
}
|
||||
}
|
||||
|
||||
// Len returns the number of tabs
|
||||
func (tm *TabManager) Len() int {
|
||||
return len(tm.tabs)
|
||||
}
|
||||
|
||||
// SetSize updates the terminal size for the tab manager
|
||||
func (tm *TabManager) SetSize(width, height int) {
|
||||
tm.width = width
|
||||
tm.height = height
|
||||
}
|
||||
|
||||
// Init initializes all tabs
|
||||
func (tm *TabManager) Init() tea.Cmd {
|
||||
var cmds []tea.Cmd
|
||||
for _, t := range tm.tabs {
|
||||
if cmd := t.Init(); cmd != nil {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
}
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
// Update sends a message to the active tab
|
||||
func (tm *TabManager) Update(msg tea.Msg) (tea.Cmd, error) {
|
||||
if len(tm.tabs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Handle tab-level keys
|
||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||
switch keyMsg.String() {
|
||||
case "ctrl+tab":
|
||||
tm.Next()
|
||||
return nil, nil
|
||||
case "shift+tab":
|
||||
tm.Prev()
|
||||
return nil, nil
|
||||
case "ctrl+q":
|
||||
if closed := tm.CloseActive(); closed != nil {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle window resize
|
||||
if wsMsg, ok := msg.(tea.WindowSizeMsg); ok {
|
||||
tm.SetSize(wsMsg.Width, wsMsg.Height)
|
||||
}
|
||||
|
||||
updated, cmd := tm.tabs[tm.active].Update(msg)
|
||||
tm.tabs[tm.active] = updated
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// View renders the tab bar and active tab content
|
||||
func (tm *TabManager) View() string {
|
||||
if len(tm.tabs) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
|
||||
// Render tab bar
|
||||
b.WriteString(renderTabBar(tm))
|
||||
|
||||
// Render active tab content
|
||||
content := tm.tabs[tm.active].View()
|
||||
if content != "" {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(content)
|
||||
}
|
||||
|
||||
// Render status bar
|
||||
b.WriteString("\n")
|
||||
b.WriteString(renderStatusBar(tm))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// renderTabBar renders the top tab bar
|
||||
func renderTabBar(tm *TabManager) string {
|
||||
if len(tm.tabs) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var cells []string
|
||||
for i, tab := range tm.tabs {
|
||||
name := tab.Name()
|
||||
if i == tm.active {
|
||||
cells = append(cells, TabActiveStyle.Render(name))
|
||||
} else {
|
||||
cells = append(cells, TabInactiveStyle.Render(name))
|
||||
}
|
||||
}
|
||||
|
||||
bar := strings.Join(cells, "")
|
||||
return TabBarStyle.Render(bar)
|
||||
}
|
||||
|
||||
// renderStatusBar renders the bottom status bar
|
||||
func renderStatusBar(tm *TabManager) string {
|
||||
hints := "Ctrl+Tab:switch Ctrl+Q:close ↑↓:nav Enter:select q:quit"
|
||||
return StatusBarStyle.Render(hints)
|
||||
}
|
||||
+39
-40
@@ -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
|
||||
}
|
||||
|
||||
+89
-2
@@ -10,13 +10,23 @@ func TestTUIInitialization(t *testing.T) {
|
||||
t.Fatal("Failed to initialize TUI")
|
||||
}
|
||||
|
||||
if ui.CurrentScreen != ScreenHostList {
|
||||
t.Errorf("expected CurrentScreen ScreenHostList, got %d", ui.CurrentScreen)
|
||||
if ui.tabs == nil {
|
||||
t.Fatal("expected tabs manager to be initialized")
|
||||
}
|
||||
|
||||
if ui.tabs.Len() != 1 {
|
||||
t.Errorf("expected 1 tab, got %d", ui.tabs.Len())
|
||||
}
|
||||
|
||||
if ui.Quit {
|
||||
t.Error("expected Quit to be false")
|
||||
}
|
||||
|
||||
// Should have a HostListTab by default
|
||||
ht := FindHostListTab(ui.tabs.tabs)
|
||||
if ht == nil {
|
||||
t.Error("expected HostListTab to be the initial tab")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTUILoadHosts(t *testing.T) {
|
||||
@@ -29,4 +39,81 @@ func TestTUILoadHosts(t *testing.T) {
|
||||
if ui.Hosts != nil {
|
||||
t.Error("expected Hosts to be nil")
|
||||
}
|
||||
|
||||
// Should still have a valid tab manager
|
||||
if ui.tabs == nil {
|
||||
t.Fatal("expected tabs manager to be valid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTabManagerBasic(t *testing.T) {
|
||||
tm := NewTabManager(NewHostListTab())
|
||||
if tm.Len() != 1 {
|
||||
t.Errorf("expected 1 tab, got %d", tm.Len())
|
||||
}
|
||||
|
||||
if tm.Active() == nil {
|
||||
t.Fatal("expected active tab")
|
||||
}
|
||||
|
||||
if tm.Active().Name() != "Hosts" {
|
||||
t.Errorf("expected 'Hosts', got '%s'", tm.Active().Name())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTabManagerNavigation(t *testing.T) {
|
||||
tm := NewTabManager(NewHostListTab())
|
||||
|
||||
// Add a second tab
|
||||
second := NewHostListTab()
|
||||
tm.Add(second)
|
||||
if tm.Len() != 2 {
|
||||
t.Errorf("expected 2 tabs, got %d", tm.Len())
|
||||
}
|
||||
|
||||
// Active should now be the last added tab
|
||||
if tm.active != 1 {
|
||||
t.Errorf("expected active index 1, got %d", tm.active)
|
||||
}
|
||||
|
||||
// Previous
|
||||
tm.Prev()
|
||||
if tm.active != 0 {
|
||||
t.Errorf("expected active index 0 after Prev, got %d", tm.active)
|
||||
}
|
||||
|
||||
// Next
|
||||
tm.Next()
|
||||
if tm.active != 1 {
|
||||
t.Errorf("expected active index 1 after Next, got %d", tm.active)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTabManagerClose(t *testing.T) {
|
||||
tm := NewTabManager(NewHostListTab())
|
||||
second := NewHostListTab()
|
||||
tm.Add(second)
|
||||
tm.Add(NewHostListTab())
|
||||
|
||||
// Close active (last tab)
|
||||
closed := tm.CloseActive()
|
||||
if closed == nil {
|
||||
t.Error("expected closed tab to be returned")
|
||||
}
|
||||
|
||||
if tm.Len() != 2 {
|
||||
t.Errorf("expected 2 tabs after close, got %d", tm.Len())
|
||||
}
|
||||
|
||||
// Close all tabs except last
|
||||
tm.Close(0)
|
||||
if tm.Len() != 1 {
|
||||
t.Errorf("expected 1 tab after close, got %d", tm.Len())
|
||||
}
|
||||
|
||||
// Should not close the last tab via CloseActive (returns nil)
|
||||
result := tm.CloseActive()
|
||||
if result != nil {
|
||||
t.Error("expected nil when trying to close the last tab")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user