package tui import ( "fmt" "strings" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "git.tukangketik.id/swanadiva/hostkeeper/internal/models" ) type snippetFormMode int const ( snippetFormAdd snippetFormMode = iota snippetFormEdit ) type snippetFieldID int const ( snippetFieldName snippetFieldID = iota snippetFieldCommand snippetFieldDescription snippetFieldTags snippetFieldCount ) var snippetFieldLabels = map[snippetFieldID]string{ snippetFieldName: "Name", snippetFieldCommand: "Command", snippetFieldDescription: "Description", snippetFieldTags: "Tags (comma-separated)", } // SnippetFormTab is a tab for adding/editing command snippets type SnippetFormTab struct { mode snippetFormMode editing *models.Snippet dataDir string inputs []textinput.Model focus snippetFieldID width int height int err error saved bool } func NewAddSnippetFormTab(dataDir string) *SnippetFormTab { return newSnippetFormTab(snippetFormAdd, nil, dataDir) } func NewEditSnippetFormTab(snippet *models.Snippet, dataDir string) *SnippetFormTab { return newSnippetFormTab(snippetFormEdit, snippet, dataDir) } func newSnippetFormTab(mode snippetFormMode, sn *models.Snippet, dataDir string) *SnippetFormTab { inputs := make([]textinput.Model, snippetFieldCount) for i := range inputs { inputs[i] = textinput.New() inputs[i].Prompt = "" } inputs[snippetFieldName].Placeholder = "Check logs" inputs[snippetFieldCommand].Placeholder = "journalctl -u nginx --no-pager -n 100" inputs[snippetFieldDescription].Placeholder = "View last 100 nginx log entries" inputs[snippetFieldTags].Placeholder = "nginx,logs,troubleshooting" if mode == snippetFormEdit && sn != nil { inputs[snippetFieldName].SetValue(sn.Name) inputs[snippetFieldCommand].SetValue(sn.Command) inputs[snippetFieldDescription].SetValue(sn.Description) inputs[snippetFieldTags].SetValue(strings.Join(sn.Tags, ",")) } inputs[snippetFieldName].Focus() inputs[snippetFieldName].Prompt = "> " return &SnippetFormTab{ mode: mode, editing: sn, dataDir: dataDir, inputs: inputs, } } func (t *SnippetFormTab) Name() string { if t.mode == snippetFormEdit { return "Edit Snippet" } return "Add Snippet" } func (t *SnippetFormTab) Init() tea.Cmd { return textinput.Blink } func (t *SnippetFormTab) Update(msg tea.Msg) (Tab, tea.Cmd) { if t.saved { return t, nil } switch msg := msg.(type) { case tea.WindowSizeMsg: t.width = msg.Width t.height = msg.Height case tea.KeyMsg: switch msg.String() { case "esc": return t, func() tea.Msg { return closeFormMsg{} } case "enter": if t.focus == snippetFieldCount-1 { return t.submit() } t.nextField() case "tab", "down": t.nextField() case "shift+tab", "up": t.prevField() case "ctrl+s": return t.submit() default: var cmd tea.Cmd t.inputs[t.focus], cmd = t.inputs[t.focus].Update(msg) return t, cmd } } return t, nil } func (t *SnippetFormTab) nextField() { t.inputs[t.focus].Blur() t.inputs[t.focus].Prompt = "" t.focus++ if t.focus >= snippetFieldCount { t.focus = snippetFieldCount - 1 } t.inputs[t.focus].Focus() t.inputs[t.focus].Prompt = "> " } func (t *SnippetFormTab) prevField() { t.inputs[t.focus].Blur() t.inputs[t.focus].Prompt = "" t.focus-- if t.focus < 0 { t.focus = 0 } t.inputs[t.focus].Focus() t.inputs[t.focus].Prompt = "> " } func (t *SnippetFormTab) submit() (Tab, tea.Cmd) { name := t.inputs[snippetFieldName].Value() command := t.inputs[snippetFieldCommand].Value() if name == "" || command == "" { t.err = fmt.Errorf("name and command are required") return t, nil } var tags []string if tagStr := t.inputs[snippetFieldTags].Value(); tagStr != "" { for _, tag := range strings.Split(tagStr, ",") { if trimmed := strings.TrimSpace(tag); trimmed != "" { tags = append(tags, trimmed) } } } var sn *models.Snippet if t.mode == snippetFormEdit && t.editing != nil { sn = t.editing sn.Name = name sn.Command = command sn.Description = t.inputs[snippetFieldDescription].Value() sn.Tags = tags } else { sn = &models.Snippet{ Name: name, Command: command, Description: t.inputs[snippetFieldDescription].Value(), Tags: tags, } } t.saved = true return t, saveSnippetCmd(sn, t.dataDir) } func (t *SnippetFormTab) View() string { contentW := t.width - 12 if contentW < 50 { contentW = 50 } if contentW > 70 { contentW = 70 } var inner strings.Builder title := "Add Snippet" if t.mode == snippetFormEdit { title = "Edit Snippet" } inner.WriteString(lipgloss.PlaceHorizontal(contentW, lipgloss.Center, lipgloss.NewStyle().Bold(true).Foreground(gbYellow).Render(title))) inner.WriteString("\n\n") if t.err != nil { inner.WriteString(lipgloss.PlaceHorizontal(contentW, lipgloss.Center, ErrorStyle.Render(fmt.Sprintf("Error: %v", t.err)))) inner.WriteString("\n\n") } for i := snippetFieldID(0); i < snippetFieldCount; i++ { input := t.inputs[i] label := snippetFieldLabels[i] style := SubtitleStyle if i == t.focus { style = HighlightStyle } inner.WriteString(style.Render(label + ":")) inner.WriteString("\n ") inner.WriteString(input.View()) inner.WriteString("\n\n") } inner.WriteString("\n") inner.WriteString(lipgloss.PlaceHorizontal(contentW, lipgloss.Center, SubtitleStyle.Render("Ctrl+Tab:switch Ctrl+Q:close Tab:next Enter:next Ctrl+S:save Esc:cancel"))) box := BorderStyle.Render(inner.String()) var b strings.Builder b.WriteString(lipgloss.PlaceHorizontal(t.width, lipgloss.Center, box)) return b.String() } func (t *SnippetFormTab) Close() {}