From 6c458ea642bd714d363c3ffe14133444706b7489 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Thu, 5 Dec 2024 09:32:39 -0300 Subject: [PATCH] Revert "Use Huh for Gum Confirm (#522)" This reverts commit f7572e387ebc7e9c52cd576b244535a89496860e. Signed-off-by: Carlos Alexandro Becker --- confirm/command.go | 43 ++++++++-------- confirm/confirm.go | 121 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 23 deletions(-) create mode 100644 confirm/confirm.go diff --git a/confirm/command.go b/confirm/command.go index 3653502d3..f02d7e173 100644 --- a/confirm/command.go +++ b/confirm/command.go @@ -4,39 +4,36 @@ import ( "os" "github.com/charmbracelet/gum/internal/exit" - "github.com/charmbracelet/huh" + + tea "github.com/charmbracelet/bubbletea" ) // Run provides a shell script interface for prompting a user to confirm an // action with an affirmative or negative answer. func (o Options) Run() error { - theme := huh.ThemeCharm() - theme.Focused.Title = o.PromptStyle.ToLipgloss() - theme.Focused.FocusedButton = o.SelectedStyle.ToLipgloss() - theme.Focused.BlurredButton = o.UnselectedStyle.ToLipgloss() - - choice := o.Default - - err := huh.NewForm( - huh.NewGroup( - huh.NewConfirm(). - Affirmative(o.Affirmative). - Negative(o.Negative). - Title(o.Prompt). - Value(&choice), - ), - ). - WithTimeout(o.Timeout). - WithTheme(theme). - WithShowHelp(o.ShowHelp). - Run() + m, err := tea.NewProgram(model{ + affirmative: o.Affirmative, + negative: o.Negative, + confirmation: o.Default, + defaultSelection: o.Default, + timeout: o.Timeout, + hasTimeout: o.Timeout > 0, + prompt: o.Prompt, + selectedStyle: o.SelectedStyle.ToLipgloss(), + unselectedStyle: o.UnselectedStyle.ToLipgloss(), + promptStyle: o.PromptStyle.ToLipgloss(), + }, tea.WithOutput(os.Stderr)).Run() if err != nil { return exit.Handle(err, o.Timeout) } - if !choice { - os.Exit(1) + if m.(model).aborted { + os.Exit(exit.StatusAborted) + } else if m.(model).confirmation { + os.Exit(0) } + os.Exit(1) + return nil } diff --git a/confirm/confirm.go b/confirm/confirm.go new file mode 100644 index 000000000..c148da3a0 --- /dev/null +++ b/confirm/confirm.go @@ -0,0 +1,121 @@ +// Package confirm provides an interface to ask a user to confirm an action. +// The user is provided with an interface to choose an affirmative or negative +// answer, which is then reflected in the exit code for use in scripting. +// +// If the user selects the affirmative answer, the program exits with 0. If the +// user selects the negative answer, the program exits with 1. +// +// I.e. confirm if the user wants to delete a file +// +// $ gum confirm "Are you sure?" && rm file.txt +package confirm + +import ( + "time" + + "github.com/charmbracelet/gum/timeout" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type model struct { + prompt string + affirmative string + negative string + quitting bool + aborted bool + hasTimeout bool + timeout time.Duration + + confirmation bool + + defaultSelection bool + + // styles + promptStyle lipgloss.Style + selectedStyle lipgloss.Style + unselectedStyle lipgloss.Style +} + +func (m model) Init() tea.Cmd { + return timeout.Init(m.timeout, m.defaultSelection) +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + return m, nil + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c": + m.confirmation = false + m.aborted = true + fallthrough + case "esc": + m.confirmation = false + m.quitting = true + return m, tea.Quit + case "q", "n", "N": + m.confirmation = false + m.quitting = true + return m, tea.Quit + case "left", "h", "ctrl+p", "tab", + "right", "l", "ctrl+n", "shift+tab": + if m.negative == "" { + break + } + m.confirmation = !m.confirmation + case "enter": + m.quitting = true + return m, tea.Quit + case "y", "Y": + m.quitting = true + m.confirmation = true + return m, tea.Quit + } + case timeout.TickTimeoutMsg: + + if msg.TimeoutValue <= 0 { + m.quitting = true + m.confirmation = m.defaultSelection + return m, tea.Quit + } + + m.timeout = msg.TimeoutValue + return m, timeout.Tick(msg.TimeoutValue, msg.Data) + } + return m, nil +} + +func (m model) View() string { + if m.quitting { + return "" + } + + var aff, neg, timeoutStrYes, timeoutStrNo string + timeoutStrNo = "" + timeoutStrYes = "" + if m.hasTimeout { + if m.defaultSelection { + timeoutStrYes = timeout.Str(m.timeout) + } else { + timeoutStrNo = timeout.Str(m.timeout) + } + } + + if m.confirmation { + aff = m.selectedStyle.Render(m.affirmative + timeoutStrYes) + neg = m.unselectedStyle.Render(m.negative + timeoutStrNo) + } else { + aff = m.unselectedStyle.Render(m.affirmative + timeoutStrYes) + neg = m.selectedStyle.Render(m.negative + timeoutStrNo) + } + + // If the option is intentionally empty, do not show it. + if m.negative == "" { + neg = "" + } + + return lipgloss.JoinVertical(lipgloss.Center, m.promptStyle.Render(m.prompt), lipgloss.JoinHorizontal(lipgloss.Left, aff, neg)) +}