Use Huh for Gum Confirm (#522)

* feat: gum confirm with huh

Use `huh` for `gum confirm`.

* fix: lint
This commit is contained in:
Maas Lalani 2024-03-28 14:41:06 -04:00 committed by GitHub
parent 44906e23b9
commit f7572e387e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 27 additions and 145 deletions

View file

@ -4,38 +4,41 @@ import (
"fmt"
"os"
"github.com/charmbracelet/gum/internal/exit"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
)
// 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 {
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()
theme := huh.ThemeCharm()
theme.Focused.Base = lipgloss.NewStyle().Margin(0, 1)
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),
),
).
WithTheme(theme).
WithShowHelp(false).
Run()
if err != nil {
return fmt.Errorf("unable to run confirm: %w", err)
}
if m.(model).aborted {
os.Exit(exit.StatusAborted)
} else if m.(model).confirmation {
os.Exit(0)
if !choice {
os.Exit(1)
}
os.Exit(1)
return nil
}

View file

@ -1,121 +0,0 @@
// 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))
}

View file

@ -12,10 +12,10 @@ type Options struct {
Affirmative string `help:"The title of the affirmative action" default:"Yes"`
Negative string `help:"The title of the negative action" default:"No"`
Prompt string `arg:"" help:"Prompt to display." default:"Are you sure?"`
PromptStyle style.Styles `embed:"" prefix:"prompt." help:"The style of the prompt" set:"defaultMargin=1 0 0 0" envprefix:"GUM_CONFIRM_PROMPT_"`
PromptStyle style.Styles `embed:"" prefix:"prompt." help:"The style of the prompt" set:"defaultMargin=1 0 0 1" envprefix:"GUM_CONFIRM_PROMPT_"`
//nolint:staticcheck
SelectedStyle style.Styles `embed:"" prefix:"selected." help:"The style of the selected action" set:"defaultBackground=212" set:"defaultForeground=230" set:"defaultPadding=0 3" set:"defaultMargin=1 1" envprefix:"GUM_CONFIRM_SELECTED_"`
SelectedStyle style.Styles `embed:"" prefix:"selected." help:"The style of the selected action" set:"defaultBackground=212" set:"defaultForeground=230" set:"defaultPadding=0 3" set:"defaultMargin=0 1" envprefix:"GUM_CONFIRM_SELECTED_"`
//nolint:staticcheck
UnselectedStyle style.Styles `embed:"" prefix:"unselected." help:"The style of the unselected action" set:"defaultBackground=235" set:"defaultForeground=254" set:"defaultPadding=0 3" set:"defaultMargin=1 1" envprefix:"GUM_CONFIRM_UNSELECTED_"`
UnselectedStyle style.Styles `embed:"" prefix:"unselected." help:"The style of the unselected action" set:"defaultBackground=235" set:"defaultForeground=254" set:"defaultPadding=0 3" set:"defaultMargin=0 1" envprefix:"GUM_CONFIRM_UNSELECTED_"`
Timeout time.Duration `help:"Timeout until confirm returns selected value or default if provided" default:"0" env:"GUM_CONFIRM_TIMEOUT"`
}