huh icon indicating copy to clipboard operation
huh copied to clipboard

Focus not given to first field when redirected to a huh.Form from a tea.Model

Open keyneston opened this issue 1 year ago • 2 comments

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

  1. Create some kind of navigation model that directs to a huh model. (Alternatively run the reproduction code below)
  2. Put a validation on the first field of the huh model in order to soft lock it.
  3. Run the code, proceeding through the navigation model to the huh model.
  4. Try and type into the Input
  5. Try and tab/shift-tab off the Input.
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.

CleanShot 2024-09-20 at 12 17 25

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
)

keyneston avatar Sep 20 '24 10:09 keyneston

I was able to work around this by calling:

	m.form.NextField()
	m.form.PrevField()

After initialising my struct.

keyneston avatar Sep 20 '24 10:09 keyneston

Seconding This issue, thanks @keyneston for a workaround :black_heart:

cloverLynn avatar Sep 23 '24 02:09 cloverLynn

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()
}

marcusprice avatar Oct 17 '24 01:10 marcusprice

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.

keyneston avatar Oct 17 '24 11:10 keyneston

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

marcusprice avatar Oct 18 '24 04:10 marcusprice

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 :)

bashbunni avatar Nov 26 '24 22:11 bashbunni

@cloverLynn @marcusprice if you could provide me with a minimum reproducible version of your code, I would be happy to take a look :)

bashbunni avatar Nov 26 '24 22:11 bashbunni

@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.

mdthompson-helium avatar Jan 08 '25 08:01 mdthompson-helium

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.

bashbunni avatar Mar 19 '25 18:03 bashbunni