Skip to content

Commit 46fee13

Browse files
authored
add alt+enter & ctrl+s shortcut to save a form directly (#6)
* add alt+enter & ctrl+s shortcut to save a form directly * fix wrong key in tests
1 parent 8d7d05b commit 46fee13

5 files changed

Lines changed: 145 additions & 8 deletions

File tree

internal/tui/commands.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ func (c *cmdContext) OpenHelp() {
7575
func (c *cmdContext) OpenForm(action string) {
7676
c.model.FormAction = action
7777
c.model.ActiveComponent = &components.Form{
78-
Form: c.model.BuildHostForm(c.model.ActiveTab),
78+
Form: c.model.BuildHostForm(c.model.ActiveTab),
79+
Validate: c.model.ValidateForm,
7980
OnSubmit: func() {
8081
c.model.executeFormSubmit()
8182
},

internal/tui/components/form.go

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package components
22

33
import (
4+
"reflect"
5+
"strings"
6+
7+
"github.com/charmbracelet/bubbles/key"
48
tea "github.com/charmbracelet/bubbletea"
59
"github.com/charmbracelet/huh"
610
)
@@ -9,6 +13,7 @@ import (
913
type Form struct {
1014
Form *huh.Form
1115
OnSubmit func()
16+
Validate func() error
1217
}
1318

1419
// Init initializes the huh form.
@@ -18,8 +23,31 @@ func (f *Form) Init() tea.Cmd {
1823

1924
// Update delegates key inputs to Huh and triggers submission.
2025
func (f *Form) Update(msg tea.Msg) (tea.Cmd, bool) {
21-
if keyMsg, ok := msg.(tea.KeyMsg); ok && keyMsg.String() == keyEsc {
22-
return nil, true
26+
if keyMsg, ok := msg.(tea.KeyMsg); ok {
27+
switch keyMsg.String() {
28+
case keyEsc:
29+
return nil, true
30+
31+
case "alt+enter", "ctrl+s":
32+
if focused := f.Form.GetFocusedField(); focused != nil {
33+
_ = focused.Blur()
34+
}
35+
36+
if f.Validate != nil {
37+
if err := f.Validate(); err != nil {
38+
if focused := f.Form.GetFocusedField(); focused != nil {
39+
_ = focused.Focus()
40+
}
41+
return nil, false
42+
}
43+
}
44+
45+
f.Form.State = huh.StateCompleted
46+
if f.OnSubmit != nil {
47+
f.OnSubmit()
48+
}
49+
return nil, true
50+
}
2351
}
2452

2553
newForm, cmd := f.Form.Update(msg)
@@ -38,7 +66,46 @@ func (f *Form) Update(msg tea.Msg) (tea.Cmd, bool) {
3866
return cmd, false
3967
}
4068

41-
// View renders the huh form.
69+
// View renders the huh form with a custom help footer.
4270
func (f *Form) View(_ int) string {
43-
return f.Form.View()
71+
formView := f.Form.View()
72+
if f.Form.State != huh.StateNormal {
73+
return formView
74+
}
75+
76+
var bindings []key.Binding
77+
78+
bindings = append(bindings, key.NewBinding(
79+
key.WithKeys("ctrl+s", "alt+enter"),
80+
key.WithHelp("ctrl+s/alt+enter", "save"),
81+
))
82+
83+
if focused := f.Form.GetFocusedField(); focused != nil {
84+
focusedType := reflect.TypeOf(focused).String()
85+
if strings.Contains(focusedType, "Select") {
86+
bindings = append(bindings, key.NewBinding(
87+
key.WithKeys("enter"),
88+
key.WithHelp("enter", "select"),
89+
))
90+
} else {
91+
bindings = append(bindings, key.NewBinding(
92+
key.WithKeys("enter", "tab"),
93+
key.WithHelp("enter", "next"),
94+
))
95+
}
96+
}
97+
98+
bindings = append(bindings, key.NewBinding(
99+
key.WithKeys("shift+tab"),
100+
key.WithHelp("shift+tab", "back"),
101+
))
102+
103+
bindings = append(bindings, key.NewBinding(
104+
key.WithKeys("esc"),
105+
key.WithHelp("esc", "exit"),
106+
))
107+
108+
helpView := f.Form.Help().ShortHelpView(bindings)
109+
110+
return formView + "\n\n" + helpView
44111
}

internal/tui/components/form_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package components_test
22

33
import (
4+
"errors"
45
"testing"
56

67
"tusshi/internal/tui/components"
@@ -60,3 +61,60 @@ func TestFormComponent(t *testing.T) {
6061
t.Error("expected aborted form state to return done = true")
6162
}
6263
}
64+
65+
func TestFormValidationAndSubmission(t *testing.T) {
66+
var val string
67+
huhForm := huh.NewForm(
68+
huh.NewGroup(
69+
huh.NewInput().Value(&val),
70+
),
71+
)
72+
73+
submitted := false
74+
validated := false
75+
var validationErr error
76+
77+
f := &components.Form{
78+
Form: huhForm,
79+
OnSubmit: func() {
80+
submitted = true
81+
},
82+
Validate: func() error {
83+
validated = true
84+
return validationErr
85+
},
86+
}
87+
88+
// Part 1: Validation fails
89+
f.Form.State = huh.StateNormal
90+
validationErr = errors.New("invalid field")
91+
_, done := f.Update(tea.KeyMsg{Type: tea.KeyCtrlS})
92+
if done {
93+
t.Error("expected form to not finalize when validation fails")
94+
}
95+
if !validated {
96+
t.Error("expected validation function to be called")
97+
}
98+
if submitted {
99+
t.Error("expected form not to submit when validation fails")
100+
}
101+
102+
// Part 2: validation succeeds
103+
validated = false
104+
submitted = false
105+
106+
validationErr = nil
107+
_, done = f.Update(tea.KeyMsg{Type: tea.KeyCtrlS})
108+
if !done {
109+
t.Error("expected form to finalize when validation succeeds")
110+
}
111+
if !validated {
112+
t.Error("expected validation function to be called")
113+
}
114+
if !submitted {
115+
t.Error("expected form to submit when validation succeeds")
116+
}
117+
if f.Form.State != huh.StateCompleted {
118+
t.Errorf("expected form state to be Completed, got %v", f.Form.State)
119+
}
120+
}

internal/tui/forms.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,16 @@ func (m *Model) BuildHostForm(defaultFile string) *huh.Form {
108108
// Construct the final beautiful form
109109
form := huh.NewForm(groups...).
110110
WithTheme(huh.ThemeCharm()).
111-
WithWidth(60)
111+
WithWidth(60).
112+
WithShowHelp(false)
112113

113114
return form
114115
}
116+
117+
// ValidateForm validates the current fields in the host form.
118+
func (m *Model) ValidateForm() error {
119+
if m.FormHost == nil {
120+
return nil
121+
}
122+
return validation.ValidateAlias(m.FormHost.Alias)
123+
}

internal/tui/keybinds.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ func (m *Model) handleNormalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
7575
case "a":
7676
m.FormAction = actionAdd
7777
m.ActiveComponent = &components.Form{
78-
Form: m.BuildHostForm(m.ActiveTab),
78+
Form: m.BuildHostForm(m.ActiveTab),
79+
Validate: m.ValidateForm,
7980
OnSubmit: func() {
8081
m.executeFormSubmit()
8182
},
@@ -86,7 +87,8 @@ func (m *Model) handleNormalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
8687
if len(m.Filtered) > 0 {
8788
m.FormAction = actionEdit
8889
m.ActiveComponent = &components.Form{
89-
Form: m.BuildHostForm(m.ActiveTab),
90+
Form: m.BuildHostForm(m.ActiveTab),
91+
Validate: m.ValidateForm,
9092
OnSubmit: func() {
9193
m.executeFormSubmit()
9294
},

0 commit comments

Comments
 (0)