Skip to content

Commit db2e277

Browse files
reworked form mapping and implemented popup based form input
1 parent b98fc02 commit db2e277

12 files changed

Lines changed: 246 additions & 217 deletions

File tree

ui/tui/helpers/form/element/internal_value.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,3 @@ func (t *InternalValue) Update(msg tea.Msg) (tea.Cmd, form.Action) { return nil,
3737
func (t *InternalValue) View(width int, eager bool) string { return "" }
3838

3939
func (t *InternalValue) Focusable() bool { return false }
40-
41-
var _ form.FormElement = (*InternalValue)(nil)

ui/tui/helpers/form/element/label.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,9 @@ func (l *Label) Focusable() bool {
3636

3737
// not needed
3838
func (l *Label) Get() any { return nil }
39-
func (l *Label) Init() (tea.Cmd, keys.KeyBindingList) { return nil, nil }
39+
func (l *Label) Init() (tea.Cmd, keys.KeyBindingList) { return nil, nil }
4040
func (l *Label) Update(msg tea.Msg) (tea.Cmd, form.Action) { return nil, form.ActionNone }
4141
func (l *Label) Reset() {}
4242
func (l *Label) Set(any) {}
4343
func (l *Label) Focus(parentKeyMap help.KeyMap) tea.Cmd { return nil }
4444
func (l *Label) Blur() {}
45-
46-
var _ form.FormElement = (*Label)(nil)
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright (c) 2026 Keymaster Team
2+
// Keymaster - SSH key management system
3+
// This source code is licensed under the MIT license found in the LICENSE file.
4+
package formelement
5+
6+
import (
7+
"github.com/charmbracelet/bubbles/help"
8+
"github.com/charmbracelet/bubbles/key"
9+
tea "github.com/charmbracelet/bubbletea"
10+
"github.com/charmbracelet/lipgloss"
11+
"github.com/charmbracelet/x/ansi"
12+
"github.com/toeirei/keymaster/ui/tui/helpers/form"
13+
"github.com/toeirei/keymaster/ui/tui/util"
14+
"github.com/toeirei/keymaster/ui/tui/util/keys"
15+
)
16+
17+
// *[Popup] implements [form.FormElement]
18+
var _ form.FormElement = (*Popup[any])(nil)
19+
20+
type popupReturnValueMsg[T any] struct{ value T }
21+
22+
type Popup[T any] struct {
23+
Label string
24+
fnOpenPopup func(returnValue func(value T) tea.Cmd) tea.Cmd
25+
fnToString func(value T) string
26+
27+
DisabledStyle lipgloss.Style
28+
BlurredStyle lipgloss.Style
29+
FocusedStyle lipgloss.Style
30+
31+
value T
32+
focused bool
33+
Disabled bool
34+
}
35+
36+
func NewPopup[T any](
37+
label string,
38+
fnOpenPopup func(returnValue func(value T) tea.Cmd) tea.Cmd,
39+
fnToString func(value T) string,
40+
) form.FormElement {
41+
return &Popup[T]{
42+
Label: label,
43+
fnOpenPopup: fnOpenPopup,
44+
fnToString: fnToString,
45+
46+
DisabledStyle: lipgloss.NewStyle().
47+
Foreground(lipgloss.Color("240")),
48+
BlurredStyle: lipgloss.NewStyle().
49+
Foreground(lipgloss.Color("240")),
50+
FocusedStyle: lipgloss.NewStyle().
51+
Foreground(lipgloss.Color("205")).
52+
Bold(true),
53+
}
54+
}
55+
56+
func (p *Popup[T]) Focus(parentKeyMap help.KeyMap) tea.Cmd {
57+
p.focused = true
58+
return util.AnnounceKeyMapCmd(parentKeyMap, keys.KeyBindingList{keys.Open()})
59+
}
60+
61+
func (p *Popup[T]) Blur() { p.focused = false }
62+
63+
func (p *Popup[T]) Get() any { return p.value }
64+
65+
func (p *Popup[T]) Init() (tea.Cmd, keys.KeyBindingList) { return nil, nil }
66+
67+
func (p *Popup[T]) Reset() { p.value = util.NewZero[T]() }
68+
69+
func (p *Popup[T]) Set(value any) {
70+
if value, ok := value.(T); ok {
71+
p.value = value
72+
}
73+
}
74+
75+
func (p *Popup[T]) Update(msg tea.Msg) (tea.Cmd, form.Action) {
76+
switch msg := msg.(type) {
77+
case popupReturnValueMsg[T]:
78+
p.value = msg.value
79+
case tea.KeyMsg:
80+
if p.Disabled {
81+
break
82+
}
83+
84+
switch {
85+
case key.Matches(msg, keys.Open()):
86+
return p.fnOpenPopup(
87+
func(value T) tea.Cmd { return util.TeaMsgToCmd(popupReturnValueMsg[T]{value}) },
88+
), form.ActionNone
89+
case key.Matches(msg, keys.DownArrow(), keys.RightArrow()):
90+
return nil, form.ActionNext
91+
case key.Matches(msg, keys.UpArrow(), keys.LeftArrow()):
92+
return nil, form.ActionPrev
93+
}
94+
}
95+
96+
return nil, form.ActionNone
97+
}
98+
99+
func (p *Popup[T]) View(width int, eager bool) string {
100+
var style lipgloss.Style
101+
if p.Disabled {
102+
style = p.DisabledStyle
103+
} else if p.focused {
104+
style = p.FocusedStyle
105+
} else {
106+
style = p.BlurredStyle
107+
}
108+
109+
label := ansi.Truncate(p.Label, width, "…")
110+
content := ansi.Truncate(p.fnToString(p.value), width-4, "…")
111+
112+
return lipgloss.JoinVertical(lipgloss.Left, style.Render(label), "[ "+content+" ]")
113+
}
114+
115+
func (p *Popup[T]) Focusable() bool { return !p.Disabled }

ui/tui/helpers/form/element/select.go.backup

Lines changed: 0 additions & 123 deletions
This file was deleted.

ui/tui/helpers/form/element/spacer.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,9 @@ func (s *Spacer) Focusable() bool { return false }
3737

3838
// not needed
3939
func (s *Spacer) Get() any { return nil }
40-
func (s *Spacer) Init() (tea.Cmd, keys.KeyBindingList) { return nil, nil }
40+
func (s *Spacer) Init() (tea.Cmd, keys.KeyBindingList) { return nil, nil }
4141
func (s *Spacer) Update(msg tea.Msg) (tea.Cmd, form.Action) { return nil, form.ActionNone }
4242
func (s *Spacer) Reset() {}
4343
func (s *Spacer) Set(any) {}
4444
func (s *Spacer) Focus(parentKeyMap help.KeyMap) tea.Cmd { return nil }
4545
func (s *Spacer) Blur() {}
46-
47-
var _ form.FormElement = (*Spacer)(nil)

ui/tui/helpers/form/element/text.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,5 +125,3 @@ func (t *Text) View(width int, eager bool) string {
125125
func (t *Text) Focusable() bool {
126126
return !t.Disabled
127127
}
128-
129-
var _ form.FormElement = (*Text)(nil)

ui/tui/helpers/form/element/textarea.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,5 +116,3 @@ func (t *TextArea) View(width int, eager bool) string {
116116
func (t *TextArea) Focusable() bool {
117117
return !t.Disabled
118118
}
119-
120-
var _ form.FormElement = (*TextArea)(nil)

ui/tui/helpers/form/form.go

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"github.com/charmbracelet/bubbles/key"
1313
tea "github.com/charmbracelet/bubbletea"
1414
"github.com/charmbracelet/lipgloss"
15-
"github.com/go-viper/mapstructure/v2"
1615
"github.com/toeirei/keymaster/ui/tui/util"
1716
"github.com/toeirei/keymaster/ui/tui/util/keys"
1817
"github.com/toeirei/keymaster/util/slicest"
@@ -383,20 +382,20 @@ func (f *Form[T]) changeActiveIndex(index_delta int) tea.Cmd {
383382
}
384383

385384
func (f *Form[T]) Get() (T, error) {
386-
var data T
387385
values := make(map[string]any, len(f.items))
388386

389387
for _, item := range f.items {
390388
values[item.Id] = item.Element.Get()
391389
}
392390

393-
err := decode(values, &data)
391+
var data T
392+
err := mapToStruct(values, &data)
394393
return data, err
395394
}
396395

397396
func (f *Form[T]) Set(data T) error {
398-
values := make(map[string]any, len(f.items))
399-
if err := decode(data, &values); err != nil {
397+
values, err := mapFromStruct(data)
398+
if err != nil {
400399
return err
401400
}
402401

@@ -421,17 +420,19 @@ func (f *Form[T]) SetItem(id string, value any) error {
421420

422421
func (f *Form[T]) SetInitialData(data T) { f.InitialData = data }
423422

424-
func decode(input any, output any) error {
425-
config := &mapstructure.DecoderConfig{
426-
Metadata: nil,
427-
Result: output,
428-
TagName: "mapstructure,form", // TODO deprecate "mapstructure"
429-
}
430-
431-
decoder, err := mapstructure.NewDecoder(config)
432-
if err != nil {
433-
return err
434-
}
435-
436-
return decoder.Decode(input)
437-
}
423+
// func decode(input any, output any) error {
424+
// config := &mapstructure.DecoderConfig{
425+
// Metadata: nil,
426+
// Result: output,
427+
// Deep: false,
428+
// Squash: ,
429+
// TagName: "mapstructure,form", // TODO deprecate "mapstructure"
430+
// }
431+
432+
// decoder, err := mapstructure.NewDecoder(config)
433+
// if err != nil {
434+
// return err
435+
// }
436+
437+
// return decoder.Decode(input)
438+
// }

0 commit comments

Comments
 (0)