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 }