Files
HostKeeper/v1/pkg/tui/snippet_form_tab.go
T
swanadiva 847989df75 refactor: move V1 code into v1/ subdirectory
- git mv cmd/ internal/ pkg/ test/ go.mod go.sum Makefile build.sh docs/ v1/
- Create v1/README.md with V1 documentation
- Update root README for V1 + V2 structure
- V1 still builds (cd v1 && go build ./cmd/hostkeeper) and 105 tests pass
- Root is now clean for V2 development
2026-07-07 11:56:27 +07:00

260 lines
5.9 KiB
Go

package tui
import (
"fmt"
"strings"
"time"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/google/uuid"
"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{
ID: uuid.New().String(),
Name: name,
Command: command,
Description: t.inputs[snippetFieldDescription].Value(),
Tags: tags,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}
t.saved = true
return t, saveSnippetCmd(sn, t.dataDir)
}
func (t *SnippetFormTab) View() string {
contentW := t.width - 12
if contentW < 30 {
contentW = 30
}
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")
footerText := "Ctrl+Tab:switch Ctrl+Q:close Tab:next Shift+Tab:prev ↑↓:nav Enter:next Ctrl+S:save Esc:cancel"
footerWrapped := wrapFooter(footerText, contentW)
for _, line := range strings.Split(footerWrapped, "\n") {
inner.WriteString(lipgloss.PlaceHorizontal(contentW, lipgloss.Center, SubtitleStyle.Render(line)))
inner.WriteString("\n")
}
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() {}