From 0139718120ab1ab3619223b536ca4d11d74b218f Mon Sep 17 00:00:00 2001 From: Sem Van Broekhoven <144097969+dotsem@users.noreply.github.com> Date: Sun, 14 Jun 2026 00:27:21 +0200 Subject: [PATCH 1/2] add alt+enter & ctrl+s shortcut to save a form directly --- internal/tui/commands.go | 3 +- internal/tui/components/form.go | 75 ++++++++++++++++++++++++++-- internal/tui/components/form_test.go | 59 ++++++++++++++++++++++ internal/tui/forms.go | 11 +++- internal/tui/keybinds.go | 6 ++- 5 files changed, 146 insertions(+), 8 deletions(-) diff --git a/internal/tui/commands.go b/internal/tui/commands.go index 2efa3d1..fe5e9ca 100644 --- a/internal/tui/commands.go +++ b/internal/tui/commands.go @@ -75,7 +75,8 @@ func (c *cmdContext) OpenHelp() { func (c *cmdContext) OpenForm(action string) { c.model.FormAction = action c.model.ActiveComponent = &components.Form{ - Form: c.model.BuildHostForm(c.model.ActiveTab), + Form: c.model.BuildHostForm(c.model.ActiveTab), + Validate: c.model.ValidateForm, OnSubmit: func() { c.model.executeFormSubmit() }, diff --git a/internal/tui/components/form.go b/internal/tui/components/form.go index 960cf52..9772a61 100644 --- a/internal/tui/components/form.go +++ b/internal/tui/components/form.go @@ -1,6 +1,10 @@ package components import ( + "reflect" + "strings" + + "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" ) @@ -9,6 +13,7 @@ import ( type Form struct { Form *huh.Form OnSubmit func() + Validate func() error } // Init initializes the huh form. @@ -18,8 +23,31 @@ func (f *Form) Init() tea.Cmd { // Update delegates key inputs to Huh and triggers submission. func (f *Form) Update(msg tea.Msg) (tea.Cmd, bool) { - if keyMsg, ok := msg.(tea.KeyMsg); ok && keyMsg.String() == keyEsc { - return nil, true + if keyMsg, ok := msg.(tea.KeyMsg); ok { + switch keyMsg.String() { + case keyEsc: + return nil, true + + case "alt+enter", "ctrl+s": + if focused := f.Form.GetFocusedField(); focused != nil { + _ = focused.Blur() + } + + if f.Validate != nil { + if err := f.Validate(); err != nil { + if focused := f.Form.GetFocusedField(); focused != nil { + _ = focused.Focus() + } + return nil, false + } + } + + f.Form.State = huh.StateCompleted + if f.OnSubmit != nil { + f.OnSubmit() + } + return nil, true + } } newForm, cmd := f.Form.Update(msg) @@ -38,7 +66,46 @@ func (f *Form) Update(msg tea.Msg) (tea.Cmd, bool) { return cmd, false } -// View renders the huh form. +// View renders the huh form with a custom help footer. func (f *Form) View(_ int) string { - return f.Form.View() + formView := f.Form.View() + if f.Form.State != huh.StateNormal { + return formView + } + + var bindings []key.Binding + + bindings = append(bindings, key.NewBinding( + key.WithKeys("ctrl+s", "alt+enter"), + key.WithHelp("ctrl+s/alt+enter", "save"), + )) + + if focused := f.Form.GetFocusedField(); focused != nil { + focusedType := reflect.TypeOf(focused).String() + if strings.Contains(focusedType, "Select") { + bindings = append(bindings, key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "select"), + )) + } else { + bindings = append(bindings, key.NewBinding( + key.WithKeys("enter", "tab"), + key.WithHelp("enter", "next"), + )) + } + } + + bindings = append(bindings, key.NewBinding( + key.WithKeys("shift+tab"), + key.WithHelp("shift+tab", "back"), + )) + + bindings = append(bindings, key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc", "exit"), + )) + + helpView := f.Form.Help().ShortHelpView(bindings) + + return formView + "\n\n" + helpView } diff --git a/internal/tui/components/form_test.go b/internal/tui/components/form_test.go index 91c4934..b650ab8 100644 --- a/internal/tui/components/form_test.go +++ b/internal/tui/components/form_test.go @@ -1,6 +1,7 @@ package components_test import ( + "errors" "testing" "tusshi/internal/tui/components" @@ -60,3 +61,61 @@ func TestFormComponent(t *testing.T) { t.Error("expected aborted form state to return done = true") } } + +func TestFormValidationAndSubmission(t *testing.T) { + var val string + huhForm := huh.NewForm( + huh.NewGroup( + huh.NewInput().Value(&val), + ), + ) + + submitted := false + validated := false + var validationErr error + + f := &components.Form{ + Form: huhForm, + OnSubmit: func() { + submitted = true + }, + Validate: func() error { + validated = true + return validationErr + }, + } + + // Case 1: Validation fails on ctrl+j + f.Form.State = huh.StateNormal + validationErr = errors.New("invalid field") + _, done := f.Update(tea.KeyMsg{Type: tea.KeyCtrlJ}) + if done { + t.Error("expected form to not finalize when validation fails") + } + if !validated { + t.Error("expected validation function to be called") + } + if submitted { + t.Error("expected form not to submit when validation fails") + } + + // Reset flags + validated = false + submitted = false + + // Case 2: Validation succeeds on ctrl+j + validationErr = nil + _, done = f.Update(tea.KeyMsg{Type: tea.KeyCtrlJ}) + if !done { + t.Error("expected form to finalize when validation succeeds") + } + if !validated { + t.Error("expected validation function to be called") + } + if !submitted { + t.Error("expected form to submit when validation succeeds") + } + if f.Form.State != huh.StateCompleted { + t.Errorf("expected form state to be Completed, got %v", f.Form.State) + } +} diff --git a/internal/tui/forms.go b/internal/tui/forms.go index bc73c4e..f146d84 100644 --- a/internal/tui/forms.go +++ b/internal/tui/forms.go @@ -108,7 +108,16 @@ func (m *Model) BuildHostForm(defaultFile string) *huh.Form { // Construct the final beautiful form form := huh.NewForm(groups...). WithTheme(huh.ThemeCharm()). - WithWidth(60) + WithWidth(60). + WithShowHelp(false) return form } + +// ValidateForm validates the current fields in the host form. +func (m *Model) ValidateForm() error { + if m.FormHost == nil { + return nil + } + return validation.ValidateAlias(m.FormHost.Alias) +} diff --git a/internal/tui/keybinds.go b/internal/tui/keybinds.go index a2f3471..8d049ea 100644 --- a/internal/tui/keybinds.go +++ b/internal/tui/keybinds.go @@ -75,7 +75,8 @@ func (m *Model) handleNormalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { case "a": m.FormAction = actionAdd m.ActiveComponent = &components.Form{ - Form: m.BuildHostForm(m.ActiveTab), + Form: m.BuildHostForm(m.ActiveTab), + Validate: m.ValidateForm, OnSubmit: func() { m.executeFormSubmit() }, @@ -86,7 +87,8 @@ func (m *Model) handleNormalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { if len(m.Filtered) > 0 { m.FormAction = actionEdit m.ActiveComponent = &components.Form{ - Form: m.BuildHostForm(m.ActiveTab), + Form: m.BuildHostForm(m.ActiveTab), + Validate: m.ValidateForm, OnSubmit: func() { m.executeFormSubmit() }, From ed42bde8fbab5eea4f01d63ab45957c3dd532083 Mon Sep 17 00:00:00 2001 From: Sem Van Broekhoven <144097969+dotsem@users.noreply.github.com> Date: Sun, 14 Jun 2026 00:34:10 +0200 Subject: [PATCH 2/2] fix wrong key in tests --- internal/tui/components/form_test.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/internal/tui/components/form_test.go b/internal/tui/components/form_test.go index b650ab8..baf6d11 100644 --- a/internal/tui/components/form_test.go +++ b/internal/tui/components/form_test.go @@ -85,10 +85,10 @@ func TestFormValidationAndSubmission(t *testing.T) { }, } - // Case 1: Validation fails on ctrl+j + // Part 1: Validation fails f.Form.State = huh.StateNormal validationErr = errors.New("invalid field") - _, done := f.Update(tea.KeyMsg{Type: tea.KeyCtrlJ}) + _, done := f.Update(tea.KeyMsg{Type: tea.KeyCtrlS}) if done { t.Error("expected form to not finalize when validation fails") } @@ -99,13 +99,12 @@ func TestFormValidationAndSubmission(t *testing.T) { t.Error("expected form not to submit when validation fails") } - // Reset flags + // Part 2: validation succeeds validated = false submitted = false - // Case 2: Validation succeeds on ctrl+j validationErr = nil - _, done = f.Update(tea.KeyMsg{Type: tea.KeyCtrlJ}) + _, done = f.Update(tea.KeyMsg{Type: tea.KeyCtrlS}) if !done { t.Error("expected form to finalize when validation succeeds") }