43858dda87
- KeyListTab: view/add/edit/delete SSH keys (Ctrl+K from host list) - KeyFormTab: add/edit key form with name, type, private key, passphrase - SnippetListTab: view/add/edit/delete snippets (Ctrl+P from host list) - SnippetFormTab: add/edit snippet form with name, command, description, tags - Updated status bar with new hints - Save/delete commands for both key pairs and snippets
202 lines
4.0 KiB
Go
202 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
|
|
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
|
|
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 Ctrl+N:add Ctrl+F:SFTP Ctrl+K:keys Ctrl+P:snippets q:quit"
|
|
return StatusBarStyle.Render(hints)
|
|
}
|