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 // Close is called when the tab is removed; implement for cleanup (e.g. disconnect SSH) Close() } // 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, switches to it, and returns its init command func (tm *TabManager) Add(tab Tab) tea.Cmd { tm.tabs = append(tm.tabs, tab) tm.active = len(tm.tabs) - 1 // Forward current terminal size so new tabs know their dimensions if tm.width > 0 && tm.height > 0 { updated, _ := tab.Update(tea.WindowSizeMsg{Width: tm.width, Height: tm.height}) tm.tabs[tm.active] = updated } return tab.Init() } // 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 } // Call Close for cleanup (e.g. disconnect SSH) tm.tabs[index].Close() 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 — forward to ALL tabs if wsMsg, ok := msg.(tea.WindowSizeMsg); ok { tm.SetSize(wsMsg.Width, wsMsg.Height) for i, t := range tm.tabs { updated, _ := t.Update(msg) tm.tabs[i] = updated } return nil, nil } 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) } 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) }