Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion internal/tui/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
},
Expand Down
75 changes: 71 additions & 4 deletions internal/tui/components/form.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package components

import (
"reflect"
"strings"

"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
)
Expand All @@ -9,6 +13,7 @@ import (
type Form struct {
Form *huh.Form
OnSubmit func()
Validate func() error
}

// Init initializes the huh form.
Expand All @@ -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)
Expand All @@ -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
}
58 changes: 58 additions & 0 deletions internal/tui/components/form_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package components_test

import (
"errors"
"testing"

"tusshi/internal/tui/components"
Expand Down Expand Up @@ -60,3 +61,60 @@ 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
},
}

// Part 1: Validation fails
f.Form.State = huh.StateNormal
validationErr = errors.New("invalid field")
_, done := f.Update(tea.KeyMsg{Type: tea.KeyCtrlS})
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")
}

// Part 2: validation succeeds
validated = false
submitted = false

validationErr = nil
_, done = f.Update(tea.KeyMsg{Type: tea.KeyCtrlS})
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)
}
}
11 changes: 10 additions & 1 deletion internal/tui/forms.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
6 changes: 4 additions & 2 deletions internal/tui/keybinds.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
},
Expand All @@ -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()
},
Expand Down
Loading