Skip to content

Commit

Permalink
feat: handle interrupts and timeouts (#747)
Browse files Browse the repository at this point in the history
  • Loading branch information
caarlos0 authored Dec 9, 2024
1 parent e30fc5e commit 4f46952
Show file tree
Hide file tree
Showing 26 changed files with 213 additions and 391 deletions.
34 changes: 4 additions & 30 deletions choose/choose.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@ package choose

import (
"strings"
"time"

"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/paginator"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/timeout"
"github.com/charmbracelet/lipgloss"
)

Expand Down Expand Up @@ -112,8 +110,6 @@ type model struct {
numSelected int
currentOrder int
paginator paginator.Model
aborted bool
timedOut bool
showHelp bool
help help.Model
keymap keymap
Expand All @@ -123,8 +119,6 @@ type model struct {
headerStyle lipgloss.Style
itemStyle lipgloss.Style
selectedItemStyle lipgloss.Style
hasTimeout bool
timeout time.Duration
}

type item struct {
Expand All @@ -133,28 +127,13 @@ type item struct {
order int
}

func (m model) Init() tea.Cmd {
return timeout.Init(m.timeout, nil)
}
func (m model) Init() tea.Cmd { return nil }

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
return m, nil
case timeout.TickTimeoutMsg:
if msg.TimeoutValue <= 0 {
m.quitting = true
m.timedOut = true
// If the user hasn't selected any items in a multi-select.
// Then we select the item that they have pressed enter on. If they
// have selected items, then we simply return them.
if m.numSelected < 1 {
m.items[m.index].selected = true
}
return m, tea.Quit
}
m.timeout = msg.TimeoutValue
return m, timeout.Tick(msg.TimeoutValue, msg.Data)

case tea.KeyMsg:
start, end := m.paginator.GetSliceBounds(len(m.items))
km := m.keymap
Expand Down Expand Up @@ -199,9 +178,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m = m.deselectAll()
}
case key.Matches(msg, km.Abort):
m.aborted = true
m.quitting = true
return m, tea.Quit
return m, tea.Interrupt
case key.Matches(msg, km.Toggle):
if m.limit == 1 {
break // no op
Expand Down Expand Up @@ -262,7 +240,6 @@ func (m model) View() string {
}

var s strings.Builder
var timeoutStr string

start, end := m.paginator.GetSliceBounds(len(m.items))
for i, item := range m.items[start:end] {
Expand All @@ -273,10 +250,7 @@ func (m model) View() string {
}

if item.selected {
if m.hasTimeout {
timeoutStr = timeout.Str(m.timeout)
}
s.WriteString(m.selectedItemStyle.Render(m.selectedPrefix + item.text + timeoutStr))
s.WriteString(m.selectedItemStyle.Render(m.selectedPrefix + item.text))
} else if i == m.index%m.height {
s.WriteString(m.cursorStyle.Render(m.cursorPrefix + item.text))
} else {
Expand Down
32 changes: 16 additions & 16 deletions choose/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@ import (
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/paginator"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/internal/timeout"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/term"

"github.com/charmbracelet/gum/internal/exit"
"github.com/charmbracelet/gum/internal/stdin"
)

// Run provides a shell script interface for choosing between different through
Expand Down Expand Up @@ -102,8 +101,7 @@ func (o Options) Run() error {
km.ToggleAll.SetEnabled(true)
}

// Disable Keybindings since we will control it ourselves.
tm, err := tea.NewProgram(model{
m := model{
index: startingIndex,
currentOrder: currentOrder,
height: o.Height,
Expand All @@ -120,22 +118,24 @@ func (o Options) Run() error {
itemStyle: o.ItemStyle.ToLipgloss(),
selectedItemStyle: o.SelectedItemStyle.ToLipgloss(),
numSelected: currentSelected,
hasTimeout: o.Timeout > 0,
timeout: o.Timeout,
showHelp: o.ShowHelp,
help: help.New(),
keymap: km,
}, tea.WithOutput(os.Stderr)).Run()
if err != nil {
return fmt.Errorf("failed to start tea program: %w", err)
}
m := tm.(model)
if m.aborted {
return exit.ErrAborted
}
if m.timedOut {
return exit.ErrTimeout

ctx, cancel := timeout.Context(o.Timeout)
defer cancel()

// Disable Keybindings since we will control it ourselves.
tm, err := tea.NewProgram(
m,
tea.WithOutput(os.Stderr),
tea.WithContext(ctx),
).Run()
if err != nil {
return fmt.Errorf("unable to pick selection: %w", err)
}
m = tm.(model)
if o.Ordered && o.Limit > 1 {
sort.Slice(m.items, func(i, j int) bool {
return m.items[i].order < m.items[j].order
Expand Down
28 changes: 14 additions & 14 deletions confirm/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,43 @@ package confirm

import (
"errors"
"fmt"
"os"

"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/gum/internal/exit"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/timeout"
)

// 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 {
tm, err := tea.NewProgram(model{
ctx, cancel := timeout.Context(o.Timeout)
defer cancel()

m := model{
affirmative: o.Affirmative,
negative: o.Negative,
confirmation: o.Default,
defaultSelection: o.Default,
timeout: o.Timeout,
hasTimeout: o.Timeout > 0,
keys: defaultKeymap(o.Affirmative, o.Negative),
help: help.New(),
showHelp: o.ShowHelp,
prompt: o.Prompt,
selectedStyle: o.SelectedStyle.ToLipgloss(),
unselectedStyle: o.UnselectedStyle.ToLipgloss(),
promptStyle: o.PromptStyle.ToLipgloss(),
}, tea.WithOutput(os.Stderr)).Run()
}
tm, err := tea.NewProgram(
m,
tea.WithOutput(os.Stderr),
tea.WithContext(ctx),
).Run()
if err != nil {
return err
return fmt.Errorf("unable to confirm: %w", err)
}

m := tm.(model)
if m.timedOut {
return exit.ErrTimeout
}
if m.aborted {
return exit.ErrAborted
}
m = tm.(model)
if m.confirmation {
return nil
}
Expand Down
43 changes: 7 additions & 36 deletions confirm/confirm.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,8 @@
package confirm

import (
"time"

"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/gum/timeout"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
Expand Down Expand Up @@ -81,15 +78,11 @@ type model struct {
affirmative string
negative string
quitting bool
aborted bool
hasTimeout bool
showHelp bool
help help.Model
keys keymap
timeout time.Duration

confirmation bool
timedOut bool

defaultSelection bool

Expand All @@ -99,9 +92,7 @@ type model struct {
unselectedStyle lipgloss.Style
}

func (m model) Init() tea.Cmd {
return timeout.Init(m.timeout, m.defaultSelection)
}
func (m model) Init() tea.Cmd { return nil }

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
Expand All @@ -111,8 +102,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch {
case key.Matches(msg, m.keys.Abort):
m.confirmation = false
m.aborted = true
fallthrough
return m, tea.Interrupt
case key.Matches(msg, m.keys.Quit):
m.confirmation = false
m.quitting = true
Expand All @@ -134,16 +124,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.confirmation = true
return m, tea.Quit
}
case timeout.TickTimeoutMsg:
if msg.TimeoutValue <= 0 {
m.quitting = true
m.confirmation = m.defaultSelection
m.timedOut = true
return m, tea.Quit
}

m.timeout = msg.TimeoutValue
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
}
return m, nil
}
Expand All @@ -153,23 +133,14 @@ func (m model) View() string {
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)
}
}
var aff, neg string

if m.confirmation {
aff = m.selectedStyle.Render(m.affirmative + timeoutStrYes)
neg = m.unselectedStyle.Render(m.negative + timeoutStrNo)
aff = m.selectedStyle.Render(m.affirmative)
neg = m.unselectedStyle.Render(m.negative)
} else {
aff = m.unselectedStyle.Render(m.affirmative + timeoutStrYes)
neg = m.selectedStyle.Render(m.negative + timeoutStrNo)
aff = m.unselectedStyle.Render(m.affirmative)
neg = m.selectedStyle.Render(m.negative)
}

// If the option is intentionally empty, do not show it.
Expand Down
20 changes: 9 additions & 11 deletions file/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/charmbracelet/bubbles/filepicker"
"github.com/charmbracelet/bubbles/help"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/exit"
"github.com/charmbracelet/gum/internal/timeout"
)

// Run is the interface to picking a file.
Expand Down Expand Up @@ -48,25 +48,23 @@ func (o Options) Run() error {
fp.Styles.FileSize = o.FileSizeStyle.ToLipgloss()
m := model{
filepicker: fp,
timeout: o.Timeout,
hasTimeout: o.Timeout > 0,
aborted: false,
showHelp: o.ShowHelp,
help: help.New(),
keymap: defaultKeymap(),
}

tm, err := tea.NewProgram(&m, tea.WithOutput(os.Stderr)).Run()
ctx, cancel := timeout.Context(o.Timeout)
defer cancel()

tm, err := tea.NewProgram(
&m,
tea.WithOutput(os.Stderr),
tea.WithContext(ctx),
).Run()
if err != nil {
return fmt.Errorf("unable to pick selection: %w", err)
}
m = tm.(model)
if m.aborted {
return exit.ErrAborted
}
if m.timedOut {
return exit.ErrTimeout
}
if m.selectedPath == "" {
return errors.New("no file selected")
}
Expand Down
Loading

0 comments on commit 4f46952

Please sign in to comment.