Files
HostKeeper/pkg/tui/tabs.go
T

206 lines
4.0 KiB
Go

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