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
194 lines
4.0 KiB
Go
194 lines
4.0 KiB
Go
package tui
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
|
"git.tukangketik.id/swanadiva/hostkeeper/internal/models"
|
|
"git.tukangketik.id/swanadiva/hostkeeper/pkg/storage"
|
|
)
|
|
|
|
type snippetListState int
|
|
|
|
const (
|
|
snippetListLoading snippetListState = iota
|
|
snippetListReady
|
|
snippetListError
|
|
)
|
|
|
|
// SnippetListTab displays and manages command snippets
|
|
type SnippetListTab struct {
|
|
dataDir string
|
|
snippets []*models.Snippet
|
|
selected int
|
|
state snippetListState
|
|
err error
|
|
width int
|
|
height int
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func NewSnippetListTab(dataDir string) *SnippetListTab {
|
|
return &SnippetListTab{
|
|
dataDir: dataDir,
|
|
state: snippetListLoading,
|
|
}
|
|
}
|
|
|
|
func (t *SnippetListTab) Name() string { return "Snippets" }
|
|
|
|
func (t *SnippetListTab) Init() tea.Cmd {
|
|
return func() tea.Msg {
|
|
store, err := storage.NewJSONStorage(t.dataDir)
|
|
if err != nil {
|
|
return snippetListLoadedMsg{err: err}
|
|
}
|
|
snippets, err := store.ListSnippets(context.Background())
|
|
if err != nil {
|
|
return snippetListLoadedMsg{err: err}
|
|
}
|
|
return snippetListLoadedMsg{snippets: snippets}
|
|
}
|
|
}
|
|
|
|
func (t *SnippetListTab) Update(msg tea.Msg) (Tab, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case tea.WindowSizeMsg:
|
|
t.mu.Lock()
|
|
t.width = msg.Width
|
|
t.height = msg.Height
|
|
t.mu.Unlock()
|
|
|
|
case snippetListLoadedMsg:
|
|
t.mu.Lock()
|
|
if msg.err != nil {
|
|
t.state = snippetListError
|
|
t.err = msg.err
|
|
} else {
|
|
t.state = snippetListReady
|
|
t.snippets = msg.snippets
|
|
}
|
|
t.mu.Unlock()
|
|
|
|
case saveSnippetResultMsg:
|
|
return t, t.Init()
|
|
|
|
case deleteSnippetResultMsg:
|
|
if msg.err != nil {
|
|
t.mu.Lock()
|
|
t.err = msg.err
|
|
t.mu.Unlock()
|
|
}
|
|
return t, t.Init()
|
|
|
|
case tea.KeyMsg:
|
|
switch msg.String() {
|
|
case "up", "k":
|
|
t.mu.Lock()
|
|
if t.selected > 0 {
|
|
t.selected--
|
|
}
|
|
t.mu.Unlock()
|
|
|
|
case "down", "j":
|
|
t.mu.Lock()
|
|
if t.selected < len(t.snippets)-1 {
|
|
t.selected++
|
|
}
|
|
t.mu.Unlock()
|
|
|
|
case "ctrl+n":
|
|
return t, func() tea.Msg { return openSnippetFormMsg{} }
|
|
|
|
case "ctrl+e":
|
|
t.mu.Lock()
|
|
snippets := t.snippets
|
|
idx := t.selected
|
|
t.mu.Unlock()
|
|
if len(snippets) > 0 && idx >= 0 && idx < len(snippets) {
|
|
return t, func() tea.Msg { return openSnippetFormMsg{editing: snippets[idx]} }
|
|
}
|
|
|
|
case "delete", "d":
|
|
t.mu.Lock()
|
|
snippets := t.snippets
|
|
idx := t.selected
|
|
t.mu.Unlock()
|
|
if len(snippets) > 0 && idx >= 0 && idx < len(snippets) {
|
|
return t, deleteSnippetCmd(snippets[idx].ID, t.dataDir)
|
|
}
|
|
|
|
case "esc":
|
|
return t, func() tea.Msg { return closeFormMsg{} }
|
|
}
|
|
}
|
|
|
|
return t, nil
|
|
}
|
|
|
|
func (t *SnippetListTab) View() string {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
var b strings.Builder
|
|
|
|
b.WriteString(AppTitleStyle.Render("Command Snippets"))
|
|
b.WriteString("\n\n")
|
|
|
|
switch t.state {
|
|
case snippetListLoading:
|
|
b.WriteString(SubtitleStyle.Render("Loading snippets..."))
|
|
return b.String()
|
|
case snippetListError:
|
|
b.WriteString(ErrorStyle.Render(fmt.Sprintf("Error: %v", t.err)))
|
|
b.WriteString("\n\n")
|
|
b.WriteString(SubtitleStyle.Render("Press Esc to go back"))
|
|
return b.String()
|
|
}
|
|
|
|
if t.err != nil {
|
|
b.WriteString(ErrorStyle.Render(fmt.Sprintf("Error: %v", t.err)))
|
|
b.WriteString("\n\n")
|
|
}
|
|
|
|
if len(t.snippets) == 0 {
|
|
b.WriteString(SubtitleStyle.Render("No snippets stored."))
|
|
b.WriteString("\n")
|
|
b.WriteString(SubtitleStyle.Render("Press Ctrl+N to add a new snippet."))
|
|
} else {
|
|
for i, sn := range t.snippets {
|
|
var line string
|
|
desc := sn.Description
|
|
if desc != "" {
|
|
line = fmt.Sprintf(" %s — %s", sn.Name, desc)
|
|
} else {
|
|
line = fmt.Sprintf(" %s", sn.Name)
|
|
}
|
|
|
|
if i == t.selected {
|
|
b.WriteString(SelectedStyle.Render(line))
|
|
} else {
|
|
b.WriteString(HostNameStyle.Render(line))
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
b.WriteString("\n")
|
|
b.WriteString(SubtitleStyle.Render("Ctrl+N:add Ctrl+E:edit D:delete Esc:back"))
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func (t *SnippetListTab) Close() {}
|
|
|
|
// snippetListLoadedMsg carries the loaded snippet list
|
|
type snippetListLoadedMsg struct {
|
|
snippets []*models.Snippet
|
|
err error
|
|
}
|