mirror of
https://github.com/RWejlgaard/org.git
synced 2026-09-02 01:15:35 +00:00
218 lines
5.3 KiB
Go
218 lines
5.3 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/bubbles/textinput"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
// stateForm backs the add/edit state form (modeSettingsStateForm).
|
|
// field: 0 = name, 1 = color, 2 = done.
|
|
type stateForm struct {
|
|
editing bool
|
|
index int // index into config.States.States when editing
|
|
name textinput.Model
|
|
color textinput.Model
|
|
done bool
|
|
field int
|
|
}
|
|
|
|
// startStateForm initializes the form and switches into the form mode.
|
|
// editing=false adds a new state; editing=true edits config.States.States[index].
|
|
func (m *uiModel) startStateForm(editing bool, index int) {
|
|
name := textinput.New()
|
|
name.Placeholder = "STATE NAME"
|
|
name.CharLimit = 32
|
|
name.Width = 30
|
|
|
|
color := textinput.New()
|
|
color.Placeholder = "0-255"
|
|
color.CharLimit = 3
|
|
color.Width = 12
|
|
|
|
sf := stateForm{editing: editing, index: index, name: name, color: color}
|
|
if editing {
|
|
st := m.config.States.States[index]
|
|
sf.name.SetValue(st.Name)
|
|
sf.color.SetValue(st.Color)
|
|
sf.done = st.Done
|
|
sf.field = 1 // name is read-only when editing
|
|
} else {
|
|
sf.color.SetValue("99")
|
|
sf.field = 0
|
|
}
|
|
|
|
m.stateForm = sf
|
|
m.focusStateFormField()
|
|
m.mode = modeSettingsStateForm
|
|
}
|
|
|
|
// stateFormMinField is the lowest navigable field: name (0) when adding,
|
|
// color (1) when editing (name is read-only).
|
|
func (m *uiModel) stateFormMinField() int {
|
|
if m.stateForm.editing {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// moveStateFormField shifts the focused field by delta, clamped to [min, 2].
|
|
func (m *uiModel) moveStateFormField(delta int) {
|
|
f := m.stateForm.field + delta
|
|
if min := m.stateFormMinField(); f < min {
|
|
f = min
|
|
}
|
|
if f > 2 {
|
|
f = 2
|
|
}
|
|
m.stateForm.field = f
|
|
m.focusStateFormField()
|
|
}
|
|
|
|
// focusStateFormField focuses the active text field (none for the Done field).
|
|
func (m *uiModel) focusStateFormField() {
|
|
m.stateForm.name.Blur()
|
|
m.stateForm.color.Blur()
|
|
switch m.stateForm.field {
|
|
case 0:
|
|
m.stateForm.name.Focus()
|
|
case 1:
|
|
m.stateForm.color.Focus()
|
|
}
|
|
}
|
|
|
|
// applyStateForm commits the form to config. It mutates config only (no Save).
|
|
// Returns false (without mutating) if a new state's name is empty.
|
|
func (m *uiModel) applyStateForm() bool {
|
|
sf := &m.stateForm
|
|
color := strings.TrimSpace(sf.color.Value())
|
|
|
|
if sf.editing {
|
|
st := &m.config.States.States[sf.index]
|
|
if color != "" {
|
|
st.Color = color
|
|
}
|
|
st.Done = sf.done
|
|
m.setStatus(fmt.Sprintf("Updated state '%s' (saved)", st.Name))
|
|
return true
|
|
}
|
|
|
|
name := strings.ToUpper(strings.TrimSpace(sf.name.Value()))
|
|
if name == "" {
|
|
m.setStatus("State name required")
|
|
return false
|
|
}
|
|
if color == "" {
|
|
color = "99"
|
|
}
|
|
if sf.done {
|
|
m.config.AddDoneState(name, color)
|
|
} else {
|
|
m.config.AddState(name, color)
|
|
}
|
|
m.setStatus(fmt.Sprintf("Added state '%s' (saved)", name))
|
|
return true
|
|
}
|
|
|
|
// updateSettingsStateForm handles key input while the state form is open.
|
|
func (m *uiModel) updateSettingsStateForm(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
keyMsg, ok := msg.(tea.KeyMsg)
|
|
if !ok {
|
|
return m, nil
|
|
}
|
|
|
|
switch keyMsg.Type {
|
|
case tea.KeyEsc:
|
|
m.mode = modeSettings
|
|
return m, nil
|
|
|
|
case tea.KeyEnter:
|
|
if m.applyStateForm() {
|
|
if err := m.config.Save(); err != nil {
|
|
m.setStatus(fmt.Sprintf("Error auto-saving config: %v", err))
|
|
} else {
|
|
m.keys = newKeyMapFromConfig(m.config)
|
|
m.styles = newStyleMapFromConfig(m.config)
|
|
}
|
|
m.mode = modeSettings
|
|
}
|
|
return m, nil
|
|
|
|
case tea.KeyTab, tea.KeyDown:
|
|
m.moveStateFormField(1)
|
|
return m, nil
|
|
|
|
case tea.KeyShiftTab, tea.KeyUp:
|
|
m.moveStateFormField(-1)
|
|
return m, nil
|
|
|
|
case tea.KeySpace:
|
|
if m.stateForm.field == 2 {
|
|
m.stateForm.done = !m.stateForm.done
|
|
return m, nil
|
|
}
|
|
}
|
|
|
|
// Route printable input to the focused text field.
|
|
var cmd tea.Cmd
|
|
switch m.stateForm.field {
|
|
case 0:
|
|
m.stateForm.name, cmd = m.stateForm.name.Update(msg)
|
|
case 1:
|
|
m.stateForm.color, cmd = m.stateForm.color.Update(msg)
|
|
}
|
|
return m, cmd
|
|
}
|
|
|
|
// viewSettingsStateForm renders the add/edit state form.
|
|
func (m *uiModel) viewSettingsStateForm() string {
|
|
var b strings.Builder
|
|
|
|
title := "Add New State"
|
|
if m.stateForm.editing {
|
|
title = "Edit State: " + m.config.States.States[m.stateForm.index].Name
|
|
}
|
|
b.WriteString(m.styles.titleStyle.Render(title) + "\n\n")
|
|
|
|
cursor := func(field int) string {
|
|
if m.stateForm.field == field {
|
|
return "▶ "
|
|
}
|
|
return " "
|
|
}
|
|
|
|
// Name row: editable input when adding, read-only label when editing.
|
|
if m.stateForm.editing {
|
|
b.WriteString(" Name " + m.stateForm.name.Value() + m.styles.statusStyle.Render(" (fixed)") + "\n")
|
|
} else {
|
|
b.WriteString(cursor(0) + "Name " + m.stateForm.name.View() + "\n")
|
|
}
|
|
|
|
// Color row with a live swatch.
|
|
colorVal := strings.TrimSpace(m.stateForm.color.Value())
|
|
swatch := ""
|
|
if colorVal != "" {
|
|
label := m.stateForm.name.Value()
|
|
if label == "" {
|
|
label = "sample"
|
|
}
|
|
sw := lipgloss.NewStyle().Foreground(lipgloss.Color(colorVal))
|
|
swatch = " " + sw.Render("●"+label)
|
|
}
|
|
b.WriteString(cursor(1) + "Color " + m.stateForm.color.View() + swatch + "\n")
|
|
|
|
// Done row: a checkbox.
|
|
box := "[ ] no"
|
|
if m.stateForm.done {
|
|
box = "[✓] yes"
|
|
}
|
|
b.WriteString(cursor(2) + "Done " + box + "\n\n")
|
|
|
|
b.WriteString(m.styles.statusStyle.Render(
|
|
"tab/↑↓: move field • space: toggle done • enter: save • esc: cancel") + "\n")
|
|
|
|
return b.String()
|
|
}
|