preset.go

  1package screens
  2
  3import (
  4	"charm.land/bubbles/v2/key"
  5	tea "charm.land/bubbletea/v2"
  6
  7	"charm.land/huh/v2"
  8
  9	"git.secluded.site/keld/internal/theme"
 10	"git.secluded.site/keld/internal/ui"
 11)
 12
 13// globalDefaultLabel is the breadcrumb display text when the user
 14// selects "(global defaults only)". This avoids the ambiguity of
 15// Selection() returning "" for both "not yet selected" and "selected
 16// global defaults".
 17const globalDefaultLabel = "(global defaults)"
 18
 19// Preset is a Screen adapter that wraps a huh Select form for
 20// choosing a configuration preset. It intercepts Esc for back
 21// navigation when the filter is not active, and delegates all other
 22// input to the embedded form.
 23type Preset struct {
 24	presets   []string
 25	styles    *theme.Styles
 26	form      *huh.Form
 27	selectFld *huh.Select[string]
 28	selected  string
 29	selection string
 30}
 31
 32// NewPreset creates a preset selector screen. The styles pointer
 33// should come from Session.Styles() so theme updates propagate
 34// automatically.
 35//
 36// The huh form is not built until Init is called, so the form
 37// picks up whatever theme the session has settled on by then
 38// (after background colour detection).
 39func NewPreset(presets []string, styles *theme.Styles) *Preset {
 40	return &Preset{
 41		presets: presets,
 42		styles:  styles,
 43	}
 44}
 45
 46// buildForm constructs the huh form and select field. Called from
 47// the constructor and from Init when the form needs resetting
 48// (e.g. after back navigation).
 49func (p *Preset) buildForm() {
 50	opts := make([]huh.Option[string], 0, len(p.presets)+1)
 51	opts = append(opts, huh.NewOption("(global defaults only)", ""))
 52	for _, name := range p.presets {
 53		opts = append(opts, huh.NewOption(name, name))
 54	}
 55
 56	// Preserve any previous selection so back-navigation shows
 57	// the user's earlier choice. Reset the breadcrumb label since
 58	// the screen is being re-activated (no longer "completed").
 59	p.selection = ""
 60
 61	sel := huh.NewSelect[string]().
 62		Options(opts...).
 63		Value(&p.selected)
 64
 65	p.selectFld = sel
 66
 67	p.form = huh.NewForm(
 68		huh.NewGroup(sel),
 69	).WithTheme(p.styles.Huh).WithShowHelp(false)
 70}
 71
 72// Init initialises the embedded form. On first call or after
 73// completion, the form is (re)built so it picks up the current
 74// theme and can accept input again.
 75func (p *Preset) Init() tea.Cmd {
 76	if p.form == nil || p.form.State != huh.StateNormal {
 77		p.buildForm()
 78	}
 79	return p.form.Init()
 80}
 81
 82// Update handles messages. Esc is intercepted for back navigation
 83// when the select field is not in filter mode. All other messages
 84// are forwarded to the huh form.
 85func (p *Preset) Update(msg tea.Msg) (ui.Screen, tea.Cmd) {
 86	if p.form == nil {
 87		return p, nil
 88	}
 89
 90	switch msg.(type) {
 91	case tea.BackgroundColorMsg:
 92		// The session has already updated *styles. Rebuild the
 93		// form so it picks up the new huh theme.
 94		p.buildForm()
 95		return p, p.form.Init()
 96	}
 97
 98	// Intercept Esc for back navigation, but only when the select
 99	// field is not filtering. When filtering, Esc should close the
100	// filter (handled by huh internally).
101	if kp, ok := msg.(tea.KeyPressMsg); ok {
102		if kp.Code == tea.KeyEscape && !p.selectFld.GetFiltering() {
103			return p, ui.BackCmd
104		}
105	}
106
107	// Forward to huh.
108	model, cmd := p.form.Update(msg)
109	if f, ok := model.(*huh.Form); ok {
110		p.form = f
111	}
112
113	if p.form.State == huh.StateCompleted {
114		if p.selected == "" {
115			p.selection = globalDefaultLabel
116		} else {
117			p.selection = p.selected
118		}
119		return p, ui.DoneCmd
120	}
121
122	return p, cmd
123}
124
125// View renders the form.
126func (p *Preset) View() string {
127	if p.form == nil {
128		return ""
129	}
130	return p.form.View()
131}
132
133// Title returns the screen's display title.
134func (p *Preset) Title() string { return "Select a preset" }
135
136// KeyBindings returns bindings for the help bar. The huh form
137// handles its own key bindings internally; we expose the most
138// relevant ones for display.
139func (p *Preset) KeyBindings() []key.Binding {
140	return []key.Binding{
141		key.NewBinding(key.WithKeys("↑/↓"), key.WithHelp("↑/↓", "navigate")),
142		key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "select")),
143	}
144}
145
146// Selection returns the chosen preset name for breadcrumb display,
147// or "" if nothing has been selected yet. For global defaults this
148// returns a display label like "(global defaults)", not the empty
149// string — use [Value] for the actual config preset name.
150func (p *Preset) Selection() string { return p.selection }
151
152// Value returns the resolved preset name for use in config resolution.
153// Returns "" when the user selected global defaults.
154func (p *Preset) Value() string { return p.selected }