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 }