256 lines
5.8 KiB
Go
256 lines
5.8 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 < 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() {}
|