Focus not given to first field when redirected to a huh.Form from a tea.Model
Describe the bug
When directed to a huh model from another bubbletea model, the initial input does not have focus. This is exacerbated by, but does not require, the use of a field validation. If a field validation is used it soft-locks the program. If a field validation is not used on the first field then one can simply tab and shift-tab back.
To Reproduce
- Create some kind of navigation model that directs to a
huhmodel. (Alternatively run the reproduction code below) - Put a validation on the first field of the huh model in order to soft lock it.
- Run the code, proceeding through the navigation model to the huh model.
- Try and type into the
Input - Try and
tab/shift-taboff theInput.
package main
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
)
func main() {
tea.NewProgram(NavModel{}).Run()
}
type NavModel struct{}
func (n NavModel) Init() tea.Cmd {
return nil
}
func (n NavModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if msg, ok := msg.(tea.KeyMsg); ok {
if msg.String() == "ctrl+c" {
return n, tea.Quit
}
if msg.String() == "enter" {
// Hardcoding 80,80 because I'm being lazy in this reproduction code and not saving them after we receive them.
// Sending this windowSizeMsg is necessary to get it to properly render the NewModel at all.
return NewModel(), func() tea.Msg { return tea.WindowSizeMsg{Height: 80, Width: 80} }
}
}
return n, nil
}
func (n NavModel) View() string {
return "Press enter to proceed."
}
type Model struct {
form *huh.Form // huh.Form is just a tea.Model
}
func NewModel() Model {
return Model{
form: huh.NewForm(
huh.NewGroup(
huh.NewInput().
Validate(func(in string) error {
if in == "" {
return fmt.Errorf("Class must be set.")
}
return nil
}).
Key("class").
Title("Choose your class"),
huh.NewSelect[int]().
Key("level").
Options(huh.NewOptions(1, 20, 9999)...).
Title("Choose your level"),
),
),
}
}
func (m Model) Init() tea.Cmd {
return m.form.Init()
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if msg, ok := msg.(tea.KeyMsg); ok {
if msg.String() == "ctrl+c" {
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 {
if m.form.State == huh.StateCompleted {
class := m.form.GetString("class")
level := m.form.GetString("level")
return fmt.Sprintf("You selected: %s, Lvl. %d", class, level)
}
return m.form.View()
}
Expected behavior
When the huh model is rendered the initial input field should have focus and you should be able to type into it.
Screenshots
What isn't visible is I'm trying to type into the Input field.
Desktop:
- Mac OS Sonoma 14.6.1
- iTerm2 Build 3.5.4
go.mod
module github.com/keyneston/bubbletea-huh-test
go 1.22.4
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/catppuccin/go v0.2.0 // indirect
github.com/charmbracelet/bubbles v0.20.0 // indirect
github.com/charmbracelet/bubbletea v1.1.1 // indirect
github.com/charmbracelet/huh v0.6.0 // indirect
github.com/charmbracelet/lipgloss v0.13.0 // indirect
github.com/charmbracelet/x/ansi v0.2.3 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
)
I was able to work around this by calling:
m.form.NextField()
m.form.PrevField()
After initialising my struct.
Seconding This issue, thanks @keyneston for a workaround :black_heart:
I think I might be running into this issue as well? Though I'm brand new to this library/charm.sh in general so I could also be skill issuing this too.
If I set an input as the first field in the form, it doesn't focus take any input. However if I have the first form element be huh.NewSelect it does focus and I can interact with it. I see @keyneston's fix, but I couldn't seem to get it to work for me? Am I just called the Next/PrevField's in the wrong place? Any help would be greatly appreciated.
package client
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
)
type ServerLanding struct {
server *string
form *huh.Form
}
func NewServerLanding(text string) *ServerLanding {
server := "placeholder text"
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Value(&server),
),
)
return &ServerLanding{form: form, server: &server}
}
func (s ServerLanding) Init() tea.Cmd {
var cmds []tea.Cmd
cmds = append(
cmds,
s.form.Init(),
s.form.NextField(),
s.form.PrevField(),
)
return tea.Batch(cmds...)
}
func (s ServerLanding) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Process the form
var cmds []tea.Cmd
form, cmd := s.form.Update(msg)
if f, ok := form.(*huh.Form); ok {
s.form = f
cmds = append(cmds, cmd)
}
return s, tea.Batch(cmds...)
}
func (s ServerLanding) View() string {
return s.form.View()
}
Hey @marcusprice try changing it like:
func NewServerLanding(text string) *ServerLanding {
server := "placeholder text"
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Value(&server),
),
)
form.NextField()
form.PrevField()
return &ServerLanding{form: form, server: &server}
}
This is instead of doing it in the Init function.
Thank you for the response, but it doesn't work for me :(
Was able to get an input/forms working in a non-nested model, but doesn't seem to work otherwise. Prob going to see if I can get away without using huh at least for now. I've been stuck trying to figure this out for days now
Hey @keyneston, in this specific instance, it looks like your problem is in the main function. You're running tea.NewProgram(NavModel{}).Run() where it should be tea.NewProgram(NewModel()).Run() as that function is where you initialize your form. I changed that one line and was able to type in the input field without issue.
Please let me know if that fix works on your end too :)
@cloverLynn @marcusprice if you could provide me with a minimum reproducible version of your code, I would be happy to take a look :)
@keyneston This is not due to a bug in huh but rather a problem with Init() from Model not being called. I don't think this would be considered best practice but see the alteration to the code you originally posted that solves the problem.
func (n NavModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if msg, ok := msg.(tea.KeyMsg); ok {
if msg.String() == "ctrl+c" {
return n, tea.Quit
}
if msg.String() == "enter" {
model := NewModel()
model.Init()
return model, func() tea.Msg { return tea.WindowSizeMsg{Height: 80, Width: 80} }
}
}
return n, nil
}
m.form.Init() was never getting called and thats why it wasn't able to focus on the first field.
See this discussion in the bubbletea repo for some extra information about nested components. Particularly that you need to initialize submodules from the parent.
Hey @keyneston I'm not sure if you're still having this problem. There are a couple of suggestions here that you can try. I'm going to convert this to a discussion as it seems it might be related to your implementation rather than a bug in Huh. If this ends up being something we need to address in Huh's source code, we'll convert this back to an issue!
Please let us know if you're still having an issue and if not, what solved it for you.