diff --git a/examples/bubbletea/main.go b/examples/bubbletea/main.go new file mode 100644 index 00000000..fce8e3c3 --- /dev/null +++ b/examples/bubbletea/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "fmt" + "os" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" +) + +var highlight = lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Render +var help = lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render + +type Model struct { + class string + level string + + form *huh.Form +} + +func NewModel() *Model { + var m Model + m.class = "Warrior" + m.level = "1" + f := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Options(huh.NewOptions("Warrior", "Mage", "Rogue")...). + Title("Choose your class"). + Description("This will determine your department"). + Value(&m.class), + huh.NewSelect[string](). + Options(huh.NewOptions("1", "20", "9999")...). + Title("Choose your level"). + Description("This will determine your benefits package"). + Value(&m.level), + ), + ) + + m.form = f + return &m +} + +func (m Model) Init() tea.Cmd { + return m.form.Init() +} + +func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "esc", "ctrl+c", "q": + return m, tea.Quit + } + } + + form, cmd := m.form.Update(msg) + if f, ok := form.(*huh.Form); ok { + m.form = f + } + + return m, cmd +} + +func (m Model) View() string { + v := "Charm Employment Application\n\n" + m.form.View() + if m.form.State == huh.StateCompleted { + v += highlight(fmt.Sprintf("You selected: Level %s, %s\n", m.level, m.class)) + v += help("\nctrl+c to quit\n") + } + return lipgloss.NewStyle().Margin(1, 2).Render(v) +} + +func main() { + _, err := tea.NewProgram(NewModel()).Run() + if err != nil { + fmt.Println("Oh no:", err) + os.Exit(1) + } +} diff --git a/form.go b/form.go index c1079267..a9ced2cb 100644 --- a/form.go +++ b/form.go @@ -8,8 +8,23 @@ import ( tea "github.com/charmbracelet/bubbletea" ) +// FormState represents the current state of the form. +type FormState int + +const ( + // StateNormal is when the user is completing the form. + StateNormal FormState = iota + + // StateCompleted is when the user has completed the form. + StateCompleted + + // StateAborted is when the user has aborted the form. + StateAborted +) + +// ErrUserAborted is the error returned when a user exits the form before +// submitting. var ( - // ErrUserAborted is the error returned when a user exits the form before submitting. ErrUserAborted = errors.New("user aborted") ) @@ -24,6 +39,12 @@ type Form struct { // navigation paginator paginator.Model + // callbacks + submitCmd tea.Cmd + cancelCmd tea.Cmd + + State FormState + // whether or not to use bubble tea rendering for accessibility // purposes, if true, the form will render with basic prompting primitives // to be more accessible to screen readers. @@ -47,7 +68,7 @@ func NewForm(groups ...*Group) *Form { p := paginator.New() p.SetTotalPages(len(groups)) - f := Form{ + f := &Form{ groups: groups, paginator: p, theme: NewCharmTheme(), @@ -61,7 +82,7 @@ func NewForm(groups ...*Group) *Form { f.WithKeyMap(f.keymap) f.WithWidth(f.width) - return &f + return f } // Field is a primitive of a form. @@ -206,7 +227,8 @@ func (f *Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case key.Matches(msg, f.keymap.Quit): f.aborted = true f.quitting = true - return f, tea.Quit + f.State = StateAborted + return f, f.cancelCmd } case nextGroupMsg: @@ -216,7 +238,8 @@ func (f *Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if f.paginator.OnLastPage() { f.quitting = true - return f, tea.Quit + f.State = StateCompleted + return f, f.submitCmd } f.paginator.NextPage() @@ -244,6 +267,9 @@ func (f *Form) View() string { // Run runs the form. func (f *Form) Run() error { + f.submitCmd = tea.Quit + f.cancelCmd = tea.Quit + if len(f.groups) == 0 { return nil }