Skip to content

chore: replace AlecAivazis/survey with charmbracelet/bubbletea #14475

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

Merged
merged 28 commits into from
Sep 4, 2024

Conversation

DanielleMaywood
Copy link
Contributor

@DanielleMaywood DanielleMaywood commented Aug 29, 2024

fixes #12720

This PR replaces the unmaintained https://github.com/AlecAivazis/survey library with https://github.com/charmbracelet/bubbletea.

The existing tests for the components Select, RichSelect and MultiSelect do not test anything meaningful as the components short-circuit to return the first option when ran in a test and the tests just confirm this short-circuiting behaviour. Future work will be to remove this short-circuiting behaviour and fix the tests.

I've manually tested the behaviour of the components with:

@DanielleMaywood DanielleMaywood force-pushed the dm-replace-survey-with-bubbletea branch 3 times, most recently from 88d286f to 2b063b0 Compare September 3, 2024 09:25
@DanielleMaywood DanielleMaywood marked this pull request as ready for review September 3, 2024 09:25
@mtojek mtojek requested review from mafredri and johnstcn September 3, 2024 09:28
func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd

if msg, ok := msg.(tea.KeyMsg); ok {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: invert and early return

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've slightly changed the logic here. I just noticed it is slightly different to how the func (m selectModel) Update function works. m.search.Update can handle more than just a tea.KeyMsg. With the updated logic does this still make sense?

Copy link
Member

@mafredri mafredri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, and thanks for the asciinemas! Left a few comments on changes I'd like to see in all functions/models, but otherwise looks good.

}

func (selectModel) Init() tea.Cmd {
return textinput.Blink
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found the blinking fairly distracting in the aciinemas, that behavior seemed different from the original. Is this what controls it and should we actually blink?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is what controls it https://pkg.go.dev/github.com/charmbracelet/bubbles@v0.19.0/textinput#Blink.

I'm happy to back it out

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing some more research, it appears that the textinput component also triggers the blinking behaviour

func (m selectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd

if msg, ok := msg.(tea.KeyMsg); ok {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if the program receives a kill -INT $PID while tea is active? Do we handle it nicely?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I'm doing something wrong, it appears nothing happens?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, did you try with a coder command or cliui test binary? With coder I would expect the program to exit with a cancelled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the cliui binary with go run ./cmd/cliui select. I shall try against the coder binary

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked on main and this behaviour appears to be the status quo.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tested and it does not! Need to handle that then, err is nil

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So... Bubbletea listens for SIGINT and SIGTERM signals and emits a QuitMsg command
https://github.com/charmbracelet/bubbletea/blob/e58efab3713e8f924e90a416e66326b23253f7ab/tea.go#L238-L272

The issue is, the event loop finishes on a QuitMsg command without notifying our Update function.
https://github.com/charmbracelet/bubbletea/blob/e58efab3713e8f924e90a416e66326b23253f7ab/tea.go#L352-L355

We could potentially install our own signal handlers by disabling the builtin one
https://pkg.go.dev/github.com/charmbracelet/bubbletea#WithoutSignalHandler
https://github.com/charmbracelet/bubbletea/blob/e58efab3713e8f924e90a416e66326b23253f7ab/tea.go#L508-L511

Copy link
Contributor Author

@DanielleMaywood DanielleMaywood Sep 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can confirm by disabling the builtin signal handler it is possible to get err to contain the Canceled error.

I wrote a small signal handler

type terminateMsg struct{}

func installSignalHandler(p *tea.Program) chan struct{} {
	ch := make(chan struct{})

	go func() {
		sig := make(chan os.Signal, 1)
		signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

		defer func() {
			signal.Stop(sig)
			close(ch)
		}()

		for {
			select {
			case <-ch:
				return

			case <-sig:
				p.Send(terminateMsg{})
			}
		}
	}()

	return ch
}

Then install it on a *tea.Program

	ch := installSignalHandler(p)
	defer func() {
		ch <- struct{}{}
	}()

And handle it in the Update methods

	switch msg := msg.(type) {
	case terminateMsg:
		m.canceled = true
		return m, tea.Quit

If this seems okay I can push this change? @mafredri

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might I suggest returning a func instead of the channel for close/deregistering the handler? I feel the usage will be a bit more idiomatic/easy to understand.

On the other hand, perhaps we don't need to handle signals at all and leave it to be the responsibility of the coder command that needs it? I guess it's possible the terminal is left in a bad state unless the signal is handled though (not sure if it enters raw mode or not).

PS. You might want to only handle signal.Interrupt for the Windows compatibility.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, definitely a good idea. Have made that change 👍

Killing the process with kill -TERM $PID leaves the cursor in a bad state if we don't explicitly handle it (syscall.SIGTERM).

Will push these changes up, can always revert if unhappy.

Copy link
Member

@johnstcn johnstcn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work and thorough testing!

Copy link
Member

@mafredri mafredri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM after the conflicts are resolved, nice work!

Copy link
Member

@mtojek mtojek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍 Feel free to merge once you update flake.nix go.mod and make CI happy. Nice contribution!

@DanielleMaywood DanielleMaywood force-pushed the dm-replace-survey-with-bubbletea branch 3 times, most recently from 7f32e05 to 6d87a98 Compare September 4, 2024 09:30
@DanielleMaywood DanielleMaywood force-pushed the dm-replace-survey-with-bubbletea branch from 6d87a98 to 966f772 Compare September 4, 2024 10:19
@DanielleMaywood DanielleMaywood merged commit 1958436 into main Sep 4, 2024
28 checks passed
@DanielleMaywood DanielleMaywood deleted the dm-replace-survey-with-bubbletea branch September 4, 2024 10:38
@github-actions github-actions bot locked and limited conversation to collaborators Sep 4, 2024
@Emyrk
Copy link
Member

Emyrk commented Sep 4, 2024

Late to the party, but one thing that bothered me with the survey package was that the prompt question remained after it was answered. I wonder if we can fix that now.

@DanielleMaywood
Copy link
Contributor Author

Late to the party, but one thing that bothered me with the survey package was that the prompt question remained after it was answered. I wonder if we can fix that now.

This absolutely can be changed now! Personally I prefer the question (with the answer) remaining, so I'd be curious if there is any way of finding out if that would be a positive/negative change amongst our customers.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

AlecAivazis/survey is no longer maintained
5 participants