Skip to content

feat(form): expose global keybinds at form level #617

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: v2-exp-2
Choose a base branch
from

Conversation

Kapparina
Copy link

@Kapparina Kapparina commented Apr 5, 2025

Description

This pull request refactors the keymap instantiation process for the Form component:

  • Introduces a defaultKeyMap to prevent redundant keymap creation.
  • Updates Form to reuse the defaultKeyMap for efficiency.
  • Adjusts KeyBinds, exposing what is currently the only "global" keybinding (the Quit binding) at the Form level.
  • Does not change anything about how keybindings are displayed via standard (read: "bubbletea-less") usage; exposing the Quit binding to the ShortHelp() method is only done where a Form's Quit binding or help text differs from the default and one wishes to do so (see the attached example).

The primary use-case for this update is to cater for scenarios where the Quit binding differs from the default and must therefore be exposed to the user (a common example of when one might do this is when using a huh.Form in a bubbletea.Model as in the huh/bubbletea example).

Prerequisites

This PR relies upon charmbracelet/bubbles#773

Working Example

The following sample demonstrates this change, displaying both the Form's native help (Group/Field level) and that which is exposed at the Form level:

package main

import (
	"github.com/charmbracelet/bubbles/v2/key"
	tea "github.com/charmbracelet/bubbletea/v2"
	"github.com/charmbracelet/huh/v2"
	"github.com/charmbracelet/lipgloss/v2"
	"github.com/charmbracelet/lipgloss/v2/compat"
)

var (
	red    = compat.AdaptiveColor{Light: lipgloss.Color("#FE5F86"), Dark: lipgloss.Color("#FE5F86")}
	indigo = compat.AdaptiveColor{Light: lipgloss.Color("#5A56E0"), Dark: lipgloss.Color("#7571F9")}
	green  = compat.AdaptiveColor{Light: lipgloss.Color("#02BA84"), Dark: lipgloss.Color("#02BF87")}
)

type Styles struct {
	Base,
	HeaderText,
	Status,
	StatusHeader,
	Highlight,
	ErrorHeaderText,
	Help lipgloss.Style
}

func NewStyles() *Styles {
	s := Styles{}
	s.Base = lipgloss.NewStyle().
		Padding(1, 4, 0, 1)
	s.HeaderText = lipgloss.NewStyle().
		Foreground(indigo).
		Bold(true).
		Padding(0, 1, 0, 2)
	s.Status = lipgloss.NewStyle().
		Border(lipgloss.RoundedBorder()).
		BorderForeground(indigo).
		PaddingLeft(1).
		MarginTop(1)
	s.StatusHeader = lipgloss.NewStyle().
		Foreground(green).
		Bold(true)
	s.Highlight = lipgloss.NewStyle().
		Foreground(lipgloss.Color("212"))
	s.ErrorHeaderText = s.HeaderText.
		Foreground(red)
	s.Help = lipgloss.NewStyle().
		Foreground(lipgloss.Color("240"))
	return &s
}

func NewModel(form *huh.Form) tea.Model {
	var m TuiModel
	m.styles = NewStyles()
	m.form = form
	return m
}

type TuiModel struct {
	styles *Styles
	form   *huh.Form
}

func (t TuiModel) Init() tea.Cmd {
	k := huh.NewDefaultKeyMap()
	t.form = t.form.WithKeyMap(
		&huh.KeyMap{
			Quit: key.NewBinding(
				key.WithKeys("ctrl+c", "esc"),
				key.WithHelp("ctrl+c | esc", "Quit the program"),
			),
			Confirm:     k.Confirm,
			FilePicker:  k.FilePicker,
			Input:       k.Input,
			MultiSelect: k.MultiSelect,
			Note:        k.Note,
			Select:      k.Select,
			Text:        k.Text,
		},
	).WithWidth(100)
	return t.form.Init()
}

func (t TuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "esc":
			return t, tea.Quit
		}
	}
	var cmds []tea.Cmd
	form, cmd := t.form.Update(msg)
	if f, ok := form.(*huh.Form); ok {
		t.form = f
		cmds = append(cmds, cmd)
	}
	if t.form.State == huh.StateCompleted {
		cmds = append(cmds, tea.Quit)
	}
	return t, tea.Batch(cmds...)
}

func (t TuiModel) View() string {
	s := t.styles
	form := lipgloss.NewStyle().Render(t.form.View())
	body := lipgloss.JoinHorizontal(lipgloss.Top, form)
	footer := t.form.Help().ShortHelpView(t.form.KeyBinds())
	return s.Base.Render(body + "\n\n" + footer)
}

func main() {
	form := huh.NewForm(
		huh.NewGroup(
			huh.NewSelect[string]().
				Title("Select something").
				Options(
					huh.NewOptions[string]("one", "two")...,
				),
		),
	).WithShowHelp(true)

	model := NewModel(form)
	_, err := tea.NewProgram(model).Run()
	if err != nil {
		panic(err)
	}
}

Below is what the above sample looks like at runtime (note again that both help texts are shown for the sake of contrast):
image

andreynering and others added 2 commits April 4, 2025 19:22
Removed this stuff to use the main from the `.github` repo:

* `CONTRIBUTING.md`: pretty basic, the main one is complete
* `SECURITY.md`: the same
* `.github/ISSUE_TEMPLATE`: an almost exact copy of what we have on
  `.github`

Also, updated `README.md` to include a link to the `/contribute` page.
- Introduce defaultKeyMap to avoid redundant keymap creation.
- Update Form to reuse defaultKeyMap.
- Adjust KeyBinds to account for custom Quit bindings.
@Kapparina Kapparina marked this pull request as ready for review April 5, 2025 12:57
@Kapparina Kapparina requested a review from a team as a code owner April 5, 2025 12:57
@Kapparina Kapparina requested review from bashbunni and removed request for a team April 5, 2025 12:57
@Kapparina Kapparina changed the title refactor(form): optimize keymap instantiation refactor(form): expose global keybinds at form level Apr 5, 2025
@Kapparina Kapparina changed the title refactor(form): expose global keybinds at form level feat(form): expose global keybinds at form level Apr 5, 2025
- Add charmbracelet/bubbles v0.20.1 transient dependencies.
@Kapparina Kapparina changed the base branch from main to v2-exp-2 April 6, 2025 01:58
@Kapparina Kapparina marked this pull request as draft April 6, 2025 02:21
@Kapparina Kapparina marked this pull request as ready for review April 6, 2025 02:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants