mirror of
https://github.com/charmbracelet/gum
synced 2026-03-14 13:45:45 +01:00
feat: handle interrupts and timeouts (#747)
This commit is contained in:
parent
e30fc5ecdf
commit
4f469522d5
26 changed files with 213 additions and 391 deletions
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -123,8 +119,6 @@ type model struct {
|
|||
headerStyle lipgloss.Style
|
||||
itemStyle lipgloss.Style
|
||||
selectedItemStyle lipgloss.Style
|
||||
hasTimeout bool
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
type item struct {
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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] {
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
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("failed to start tea program: %w", err)
|
||||
}
|
||||
m := tm.(model)
|
||||
if m.aborted {
|
||||
return exit.ErrAborted
|
||||
}
|
||||
if m.timedOut {
|
||||
return exit.ErrTimeout
|
||||
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
|
||||
|
|
|
|||
|
|
@ -2,24 +2,25 @@ 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,
|
||||
|
|
@ -27,18 +28,17 @@ func (o Options) Run() error {
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
25
file/file.go
25
file/file.go
|
|
@ -13,13 +13,10 @@
|
|||
package file
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/filepicker"
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
|
|
@ -58,22 +55,13 @@ func (k keymap) ShortHelp() []key.Binding {
|
|||
type model struct {
|
||||
filepicker filepicker.Model
|
||||
selectedPath string
|
||||
aborted bool
|
||||
timedOut bool
|
||||
quitting bool
|
||||
timeout time.Duration
|
||||
hasTimeout bool
|
||||
showHelp bool
|
||||
help help.Model
|
||||
keymap keymap
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
timeout.Init(m.timeout, nil),
|
||||
m.filepicker.Init(),
|
||||
)
|
||||
}
|
||||
func (m model) Init() tea.Cmd { return m.filepicker.Init() }
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
|
|
@ -84,21 +72,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, keyAbort):
|
||||
m.aborted = true
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
return m, tea.Interrupt
|
||||
case key.Matches(msg, keyQuit):
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
case timeout.TickTimeoutMsg:
|
||||
if msg.TimeoutValue <= 0 {
|
||||
m.quitting = true
|
||||
m.timedOut = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
m.timeout = msg.TimeoutValue
|
||||
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.filepicker, cmd = m.filepicker.Update(msg)
|
||||
|
|
|
|||
|
|
@ -10,13 +10,12 @@ import (
|
|||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/gum/internal/files"
|
||||
"github.com/charmbracelet/gum/internal/stdin"
|
||||
"github.com/charmbracelet/gum/internal/timeout"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/charmbracelet/x/term"
|
||||
"github.com/sahilm/fuzzy"
|
||||
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
"github.com/charmbracelet/gum/internal/files"
|
||||
"github.com/charmbracelet/gum/internal/stdin"
|
||||
)
|
||||
|
||||
// Run provides a shell script interface for filtering through options, powered
|
||||
|
|
@ -50,7 +49,13 @@ func (o Options) Run() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
options := []tea.ProgramOption{tea.WithOutput(os.Stderr)}
|
||||
ctx, cancel := timeout.Context(o.Timeout)
|
||||
defer cancel()
|
||||
|
||||
options := []tea.ProgramOption{
|
||||
tea.WithOutput(os.Stderr),
|
||||
tea.WithContext(ctx),
|
||||
}
|
||||
if o.Height == 0 {
|
||||
options = append(options, tea.WithAltScreen())
|
||||
}
|
||||
|
|
@ -100,8 +105,6 @@ func (o Options) Run() error {
|
|||
limit: o.Limit,
|
||||
reverse: o.Reverse,
|
||||
fuzzy: o.Fuzzy,
|
||||
timeout: o.Timeout,
|
||||
hasTimeout: o.Timeout > 0,
|
||||
sort: o.Sort && o.FuzzySort,
|
||||
strict: o.Strict,
|
||||
showHelp: o.ShowHelp,
|
||||
|
|
@ -113,14 +116,8 @@ func (o Options) Run() error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("unable to run filter: %w", err)
|
||||
}
|
||||
m := tm.(model)
|
||||
if m.aborted {
|
||||
return exit.ErrAborted
|
||||
}
|
||||
if m.timedOut {
|
||||
return exit.ErrTimeout
|
||||
}
|
||||
|
||||
m := tm.(model)
|
||||
isTTY := term.IsTerminal(os.Stdout.Fd())
|
||||
|
||||
// allSelections contains values only if limit is greater
|
||||
|
|
|
|||
|
|
@ -12,9 +12,6 @@ package filter
|
|||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
|
|
@ -100,8 +97,6 @@ type model struct {
|
|||
selectedPrefix string
|
||||
unselectedPrefix string
|
||||
height int
|
||||
aborted bool
|
||||
timedOut bool
|
||||
quitting bool
|
||||
headerStyle lipgloss.Style
|
||||
matchStyle lipgloss.Style
|
||||
|
|
@ -116,14 +111,10 @@ type model struct {
|
|||
showHelp bool
|
||||
keymap keymap
|
||||
help help.Model
|
||||
timeout time.Duration
|
||||
hasTimeout bool
|
||||
strict bool
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return timeout.Init(m.timeout, nil)
|
||||
}
|
||||
func (m model) Init() tea.Cmd { return nil }
|
||||
|
||||
func (m model) View() string {
|
||||
if m.quitting {
|
||||
|
|
@ -240,15 +231,6 @@ func (m model) helpView() string {
|
|||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
switch msg := msg.(type) {
|
||||
case timeout.TickTimeoutMsg:
|
||||
if msg.TimeoutValue <= 0 {
|
||||
m.quitting = true
|
||||
m.timedOut = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
m.timeout = msg.TimeoutValue
|
||||
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
if m.height == 0 || m.height > msg.Height {
|
||||
m.viewport.Height = msg.Height - lipgloss.Height(m.textinput.View())
|
||||
|
|
@ -269,9 +251,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
km := m.keymap
|
||||
switch {
|
||||
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.Submit):
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
|
|
|
|||
6
go.mod
6
go.mod
|
|
@ -6,7 +6,7 @@ require (
|
|||
github.com/alecthomas/kong v1.6.0
|
||||
github.com/alecthomas/mango-kong v0.1.0
|
||||
github.com/charmbracelet/bubbles v0.20.0
|
||||
github.com/charmbracelet/bubbletea v1.2.4
|
||||
github.com/charmbracelet/bubbletea v1.2.5-0.20241207142916-e0515bc22ad1
|
||||
github.com/charmbracelet/glamour v0.8.0
|
||||
github.com/charmbracelet/lipgloss v1.0.0
|
||||
github.com/charmbracelet/log v0.4.0
|
||||
|
|
@ -42,8 +42,8 @@ require (
|
|||
github.com/yuin/goldmark-emoji v1.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sync v0.9.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.22.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
)
|
||||
|
|
|
|||
12
go.sum
12
go.sum
|
|
@ -20,8 +20,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP
|
|||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
|
||||
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
|
||||
github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE=
|
||||
github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM=
|
||||
github.com/charmbracelet/bubbletea v1.2.5-0.20241207142916-e0515bc22ad1 h1:osd3dk14DEriOrqJBWzeDE9eN2Yd00BkKzFAiLXxkS8=
|
||||
github.com/charmbracelet/bubbletea v1.2.5-0.20241207142916-e0515bc22ad1/go.mod h1:Hbk5+oE4a7cDyjfdPi4sHZ42aGTMYcmHnVDhsRswn7A=
|
||||
github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs=
|
||||
github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw=
|
||||
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
|
||||
|
|
@ -94,12 +94,12 @@ golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0J
|
|||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@ import (
|
|||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"github.com/charmbracelet/gum/cursor"
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
"github.com/charmbracelet/gum/internal/stdin"
|
||||
"github.com/charmbracelet/gum/internal/timeout"
|
||||
)
|
||||
|
||||
// Run provides a shell script interface for the text input bubble.
|
||||
|
|
@ -43,31 +42,30 @@ func (o Options) Run() error {
|
|||
i.EchoCharacter = '•'
|
||||
}
|
||||
|
||||
p := tea.NewProgram(model{
|
||||
m := model{
|
||||
textinput: i,
|
||||
aborted: false,
|
||||
header: o.Header,
|
||||
headerStyle: o.HeaderStyle.ToLipgloss(),
|
||||
timeout: o.Timeout,
|
||||
hasTimeout: o.Timeout > 0,
|
||||
autoWidth: o.Width < 1,
|
||||
showHelp: o.ShowHelp,
|
||||
help: help.New(),
|
||||
keymap: defaultKeymap(),
|
||||
}, tea.WithOutput(os.Stderr))
|
||||
}
|
||||
|
||||
ctx, cancel := timeout.Context(o.Timeout)
|
||||
defer cancel()
|
||||
|
||||
p := tea.NewProgram(
|
||||
m,
|
||||
tea.WithOutput(os.Stderr),
|
||||
tea.WithContext(ctx),
|
||||
)
|
||||
tm, err := p.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run input: %w", err)
|
||||
}
|
||||
|
||||
m := tm.(model)
|
||||
if m.aborted {
|
||||
return exit.ErrAborted
|
||||
}
|
||||
if m.timedOut {
|
||||
return exit.ErrTimeout
|
||||
}
|
||||
|
||||
m = tm.(model)
|
||||
fmt.Println(m.textinput.Value())
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,10 @@
|
|||
package input
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
|
|
@ -44,21 +41,12 @@ type model struct {
|
|||
headerStyle lipgloss.Style
|
||||
textinput textinput.Model
|
||||
quitting bool
|
||||
timedOut bool
|
||||
aborted bool
|
||||
timeout time.Duration
|
||||
hasTimeout bool
|
||||
showHelp bool
|
||||
help help.Model
|
||||
keymap keymap
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
textinput.Blink,
|
||||
timeout.Init(m.timeout, nil),
|
||||
)
|
||||
}
|
||||
func (m model) Init() tea.Cmd { return textinput.Blink }
|
||||
|
||||
func (m model) View() string {
|
||||
if m.quitting {
|
||||
|
|
@ -82,25 +70,16 @@ func (m model) View() string {
|
|||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case timeout.TickTimeoutMsg:
|
||||
if msg.TimeoutValue <= 0 {
|
||||
m.quitting = true
|
||||
m.timedOut = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
m.timeout = msg.TimeoutValue
|
||||
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
|
||||
case tea.WindowSizeMsg:
|
||||
if m.autoWidth {
|
||||
m.textinput.Width = msg.Width - lipgloss.Width(m.textinput.Prompt) - 1
|
||||
}
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "esc":
|
||||
case "ctrl+c":
|
||||
m.quitting = true
|
||||
m.aborted = true
|
||||
return m, tea.Quit
|
||||
case "enter":
|
||||
return m, tea.Interrupt
|
||||
case "esc", "enter":
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package exit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
|
|
@ -11,12 +10,6 @@ const StatusTimeout = 124
|
|||
// StatusAborted is the exit code for aborted commands.
|
||||
const StatusAborted = 130
|
||||
|
||||
// ErrAborted is the error to return when a gum command is aborted by Ctrl+C.
|
||||
var ErrAborted = errors.New("user aborted")
|
||||
|
||||
// ErrTimeout is the error returned when the timeout is reached.
|
||||
var ErrTimeout = errors.New("timeout")
|
||||
|
||||
// ErrExit is a custom exit error.
|
||||
type ErrExit int
|
||||
|
||||
|
|
|
|||
15
internal/timeout/context.go
Normal file
15
internal/timeout/context.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package timeout
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Context setup a new context that times out if the given timeout is > 0.
|
||||
func Context(timeout time.Duration) (context.Context, context.CancelFunc) {
|
||||
ctx := context.Background()
|
||||
if timeout == 0 {
|
||||
return ctx, func() {}
|
||||
}
|
||||
return context.WithTimeout(ctx, timeout)
|
||||
}
|
||||
10
main.go
10
main.go
|
|
@ -7,10 +7,10 @@ import (
|
|||
"runtime/debug"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/muesli/termenv"
|
||||
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
)
|
||||
|
||||
const shaLen = 7
|
||||
|
|
@ -76,11 +76,11 @@ func main() {
|
|||
if errors.As(err, &ex) {
|
||||
os.Exit(int(ex))
|
||||
}
|
||||
if errors.Is(err, exit.ErrTimeout) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
if errors.Is(err, tea.ErrProgramKilled) {
|
||||
fmt.Fprintln(os.Stderr, "timed out")
|
||||
os.Exit(exit.StatusTimeout)
|
||||
}
|
||||
if errors.Is(err, exit.ErrAborted) {
|
||||
if errors.Is(err, tea.ErrInterrupted) {
|
||||
os.Exit(exit.StatusAborted)
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import (
|
|||
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
"github.com/charmbracelet/gum/internal/stdin"
|
||||
"github.com/charmbracelet/gum/internal/timeout"
|
||||
)
|
||||
|
||||
// Run provides a shell script interface for the viewport bubble.
|
||||
|
|
@ -30,7 +30,7 @@ func (o Options) Run() error {
|
|||
}
|
||||
}
|
||||
|
||||
tm, err := tea.NewProgram(model{
|
||||
m := model{
|
||||
viewport: vp,
|
||||
helpStyle: o.HelpStyle.ToLipgloss(),
|
||||
content: o.Content,
|
||||
|
|
@ -40,17 +40,19 @@ func (o Options) Run() error {
|
|||
softWrap: o.SoftWrap,
|
||||
matchStyle: o.MatchStyle.ToLipgloss(),
|
||||
matchHighlightStyle: o.MatchHighlightStyle.ToLipgloss(),
|
||||
timeout: o.Timeout,
|
||||
hasTimeout: o.Timeout > 0,
|
||||
}, tea.WithAltScreen()).Run()
|
||||
}
|
||||
|
||||
ctx, cancel := timeout.Context(o.Timeout)
|
||||
defer cancel()
|
||||
|
||||
_, err := tea.NewProgram(
|
||||
m,
|
||||
tea.WithAltScreen(),
|
||||
tea.WithContext(ctx),
|
||||
).Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to start program: %w", err)
|
||||
}
|
||||
|
||||
m := tm.(model)
|
||||
if m.timedOut {
|
||||
return exit.ErrTimeout
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,6 @@ package pager
|
|||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
|
@ -28,25 +25,12 @@ type model struct {
|
|||
matchStyle lipgloss.Style
|
||||
matchHighlightStyle lipgloss.Style
|
||||
maxWidth int
|
||||
timeout time.Duration
|
||||
hasTimeout bool
|
||||
timedOut bool
|
||||
}
|
||||
|
||||
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 timeout.TickTimeoutMsg:
|
||||
if msg.TimeoutValue <= 0 {
|
||||
m.timedOut = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
m.timeout = msg.TimeoutValue
|
||||
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
m.ProcessText(msg)
|
||||
case tea.KeyMsg:
|
||||
|
|
@ -134,8 +118,10 @@ func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) {
|
|||
case "n":
|
||||
m.search.NextMatch(&m)
|
||||
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
|
||||
case "q", "ctrl+c", "esc":
|
||||
case "q", "esc":
|
||||
return m, tea.Quit
|
||||
case "ctrl+c":
|
||||
return m, tea.Interrupt
|
||||
}
|
||||
m.viewport, cmd = m.viewport.Update(key)
|
||||
}
|
||||
|
|
@ -144,17 +130,14 @@ func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) {
|
|||
}
|
||||
|
||||
func (m model) View() string {
|
||||
var timeoutStr string
|
||||
if m.hasTimeout {
|
||||
timeoutStr = timeout.Str(m.timeout) + " "
|
||||
}
|
||||
helpMsg := "\n" + timeoutStr + " ↑/↓: Navigate • q: Quit • /: Search "
|
||||
// TODO: use help bubble here
|
||||
helpMsg := "\n ↑/↓: Navigate • q: Quit • /: Search "
|
||||
if m.search.query != nil {
|
||||
helpMsg += "• n: Next Match "
|
||||
helpMsg += "• N: Prev Match "
|
||||
}
|
||||
if m.search.active {
|
||||
return m.viewport.View() + "\n" + timeoutStr + " " + m.search.input.View()
|
||||
return m.viewport.View() + "\n " + m.search.input.View()
|
||||
}
|
||||
|
||||
return m.viewport.View() + m.helpStyle.Render(helpMsg)
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import (
|
|||
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/x/term"
|
||||
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
"github.com/charmbracelet/gum/internal/timeout"
|
||||
"github.com/charmbracelet/x/term"
|
||||
)
|
||||
|
||||
// Run provides a shell script interface for the spinner bubble.
|
||||
|
|
@ -19,28 +19,28 @@ func (o Options) Run() error {
|
|||
s := spinner.New()
|
||||
s.Style = o.SpinnerStyle.ToLipgloss()
|
||||
s.Spinner = spinnerMap[o.Spinner]
|
||||
tm, err := tea.NewProgram(model{
|
||||
m := model{
|
||||
spinner: s,
|
||||
title: o.TitleStyle.ToLipgloss().Render(o.Title),
|
||||
command: o.Command,
|
||||
align: o.Align,
|
||||
showOutput: o.ShowOutput && isTTY,
|
||||
showError: o.ShowError,
|
||||
timeout: o.Timeout,
|
||||
hasTimeout: o.Timeout > 0,
|
||||
}, 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("failed to run spin: %w", err)
|
||||
}
|
||||
|
||||
m := tm.(model)
|
||||
if m.aborted {
|
||||
return exit.ErrAborted
|
||||
}
|
||||
if m.timedOut {
|
||||
return exit.ErrTimeout
|
||||
return fmt.Errorf("unable to run action: %w", err)
|
||||
}
|
||||
|
||||
m = tm.(model)
|
||||
// If the command succeeds, and we are printing output and we are in a TTY then push the STDOUT we got to the actual
|
||||
// STDOUT for piping or other things.
|
||||
//nolint:nestif
|
||||
|
|
|
|||
30
spin/spin.go
30
spin/spin.go
|
|
@ -20,13 +20,10 @@ import (
|
|||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
"github.com/charmbracelet/x/term"
|
||||
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/x/term"
|
||||
)
|
||||
|
||||
type model struct {
|
||||
|
|
@ -35,16 +32,12 @@ type model struct {
|
|||
align string
|
||||
command []string
|
||||
quitting bool
|
||||
aborted bool
|
||||
timedOut bool
|
||||
status int
|
||||
stdout string
|
||||
stderr string
|
||||
output string
|
||||
showOutput bool
|
||||
showError bool
|
||||
timeout time.Duration
|
||||
hasTimeout bool
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -95,14 +88,13 @@ func commandAbort() tea.Msg {
|
|||
if executing != nil && executing.Process != nil {
|
||||
_ = executing.Process.Signal(syscall.SIGINT)
|
||||
}
|
||||
return nil
|
||||
return tea.InterruptMsg{}
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
m.spinner.Tick,
|
||||
commandStart(m.command),
|
||||
timeout.Init(m.timeout, nil),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -111,15 +103,11 @@ func (m model) View() string {
|
|||
return strings.TrimPrefix(errbuf.String()+"\n"+outbuf.String(), "\n")
|
||||
}
|
||||
|
||||
var str string
|
||||
if m.hasTimeout {
|
||||
str = timeout.Str(m.timeout)
|
||||
}
|
||||
var header string
|
||||
if m.align == "left" {
|
||||
header = m.spinner.View() + str + " " + m.title
|
||||
header = m.spinner.View() + " " + m.title
|
||||
} else {
|
||||
header = str + " " + m.title + " " + m.spinner.View()
|
||||
header = m.title + " " + m.spinner.View()
|
||||
}
|
||||
if !m.showOutput {
|
||||
return header
|
||||
|
|
@ -130,15 +118,6 @@ func (m model) View() string {
|
|||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
switch msg := msg.(type) {
|
||||
case timeout.TickTimeoutMsg:
|
||||
if msg.TimeoutValue <= 0 {
|
||||
// grab current output before closing for piped instances
|
||||
m.stdout = outbuf.String()
|
||||
m.timedOut = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
m.timeout = msg.TimeoutValue
|
||||
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
|
||||
case finishCommandMsg:
|
||||
m.stdout = msg.stdout
|
||||
m.stderr = msg.stderr
|
||||
|
|
@ -149,7 +128,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
m.aborted = true
|
||||
return m, commandAbort
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import (
|
|||
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/gum/internal/stdin"
|
||||
"github.com/charmbracelet/gum/internal/timeout"
|
||||
"github.com/charmbracelet/gum/style"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
ltable "github.com/charmbracelet/lipgloss/table"
|
||||
|
||||
"github.com/charmbracelet/gum/internal/stdin"
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Run provides a shell script interface for rendering tabular data (CSV).
|
||||
|
|
@ -111,9 +111,17 @@ func (o Options) Run() error {
|
|||
if o.Height > 0 {
|
||||
opts = append(opts, table.WithHeight(o.Height))
|
||||
}
|
||||
|
||||
table := table.New(opts...)
|
||||
|
||||
tm, err := tea.NewProgram(model{table: table}, tea.WithOutput(os.Stderr)).Run()
|
||||
ctx, cancel := timeout.Context(o.Timeout)
|
||||
defer cancel()
|
||||
|
||||
tm, err := tea.NewProgram(
|
||||
model{table: table},
|
||||
tea.WithOutput(os.Stderr),
|
||||
tea.WithContext(ctx),
|
||||
).Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start tea program: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
package table
|
||||
|
||||
import "github.com/charmbracelet/gum/style"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Options is the customization options for the table command.
|
||||
type Options struct {
|
||||
|
|
@ -12,9 +16,10 @@ type Options struct {
|
|||
File string `short:"f" help:"file path" default:""`
|
||||
Border string `short:"b" help:"border style" default:"rounded" enum:"rounded,thick,normal,hidden,double,none"`
|
||||
|
||||
BorderStyle style.Styles `embed:"" prefix:"border." envprefix:"GUM_TABLE_BORDER_"`
|
||||
CellStyle style.Styles `embed:"" prefix:"cell." envprefix:"GUM_TABLE_CELL_"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." envprefix:"GUM_TABLE_HEADER_"`
|
||||
SelectedStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" envprefix:"GUM_TABLE_SELECTED_"`
|
||||
ReturnColumn int `short:"r" help:"Which column number should be returned instead of whole row as string. Default=0 returns whole Row" default:"0"`
|
||||
BorderStyle style.Styles `embed:"" prefix:"border." envprefix:"GUM_TABLE_BORDER_"`
|
||||
CellStyle style.Styles `embed:"" prefix:"cell." envprefix:"GUM_TABLE_CELL_"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." envprefix:"GUM_TABLE_HEADER_"`
|
||||
SelectedStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" envprefix:"GUM_TABLE_SELECTED_"`
|
||||
ReturnColumn int `short:"r" help:"Which column number should be returned instead of whole row as string. Default=0 returns whole Row" default:"0"`
|
||||
Timeout time.Duration `help:"Timeout until choose returns selected element" default:"0s" env:"GUM_TABLE_TIMEOUT"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,7 @@ type model struct {
|
|||
quitting bool
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
func (m model) Init() tea.Cmd { return nil }
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
|
|
@ -39,9 +37,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.selected = m.table.SelectedRow()
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
case "ctrl+c", "q", "esc":
|
||||
case "q", "esc":
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
case "ctrl+c":
|
||||
m.quitting = true
|
||||
return m, tea.Interrupt
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
package timeout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// Tick interval.
|
||||
const tickInterval = time.Second
|
||||
|
||||
// TickTimeoutMsg will be dispatched for every tick.
|
||||
// Containing current timeout value
|
||||
// and optional parameter to be used when handling the timeout msg.
|
||||
type TickTimeoutMsg struct {
|
||||
TimeoutValue time.Duration
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
// Init Start Timeout ticker using with timeout in seconds and optional data.
|
||||
func Init(timeout time.Duration, data interface{}) tea.Cmd {
|
||||
if timeout > 0 {
|
||||
return Tick(timeout, data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start ticker.
|
||||
func Tick(timeoutValue time.Duration, data interface{}) tea.Cmd {
|
||||
return tea.Tick(tickInterval, func(time.Time) tea.Msg {
|
||||
// every tick checks if the timeout needs to be decremented
|
||||
// and send as message
|
||||
if timeoutValue >= 0 {
|
||||
timeoutValue -= tickInterval
|
||||
return TickTimeoutMsg{
|
||||
TimeoutValue: timeoutValue,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Str produce Timeout String to be rendered.
|
||||
func Str(timeout time.Duration) string {
|
||||
return fmt.Sprintf(" (%d)", max(0, int(timeout.Seconds())))
|
||||
}
|
||||
|
|
@ -8,10 +8,9 @@ import (
|
|||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/textarea"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"github.com/charmbracelet/gum/cursor"
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
"github.com/charmbracelet/gum/internal/stdin"
|
||||
"github.com/charmbracelet/gum/internal/timeout"
|
||||
)
|
||||
|
||||
// Run provides a shell script interface for the text area bubble.
|
||||
|
|
@ -49,7 +48,7 @@ func (o Options) Run() error {
|
|||
a.SetHeight(o.Height)
|
||||
a.SetValue(o.Value)
|
||||
|
||||
p := tea.NewProgram(model{
|
||||
m := model{
|
||||
textarea: a,
|
||||
header: o.Header,
|
||||
headerStyle: o.HeaderStyle.ToLipgloss(),
|
||||
|
|
@ -57,16 +56,22 @@ func (o Options) Run() error {
|
|||
help: help.New(),
|
||||
showHelp: o.ShowHelp,
|
||||
keymap: defaultKeymap(),
|
||||
}, tea.WithOutput(os.Stderr), tea.WithReportFocus())
|
||||
}
|
||||
|
||||
ctx, cancel := timeout.Context(o.Timeout)
|
||||
defer cancel()
|
||||
|
||||
p := tea.NewProgram(
|
||||
m,
|
||||
tea.WithOutput(os.Stderr),
|
||||
tea.WithReportFocus(),
|
||||
tea.WithContext(ctx),
|
||||
)
|
||||
tm, err := p.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run write: %w", err)
|
||||
}
|
||||
m := tm.(model)
|
||||
if m.aborted {
|
||||
return exit.ErrAborted
|
||||
}
|
||||
|
||||
m = tm.(model)
|
||||
fmt.Println(m.textarea.Value())
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,25 @@
|
|||
package write
|
||||
|
||||
import "github.com/charmbracelet/gum/style"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Options are the customization options for the textarea.
|
||||
type Options struct {
|
||||
Width int `help:"Text area width (0 for terminal width)" default:"0" env:"GUM_WRITE_WIDTH"`
|
||||
Height int `help:"Text area height" default:"5" env:"GUM_WRITE_HEIGHT"`
|
||||
Header string `help:"Header value" default:"" env:"GUM_WRITE_HEADER"`
|
||||
Placeholder string `help:"Placeholder value" default:"Write something..." env:"GUM_WRITE_PLACEHOLDER"`
|
||||
Prompt string `help:"Prompt to display" default:"┃ " env:"GUM_WRITE_PROMPT"`
|
||||
ShowCursorLine bool `help:"Show cursor line" default:"false" env:"GUM_WRITE_SHOW_CURSOR_LINE"`
|
||||
ShowLineNumbers bool `help:"Show line numbers" default:"false" env:"GUM_WRITE_SHOW_LINE_NUMBERS"`
|
||||
Value string `help:"Initial value (can be passed via stdin)" default:"" env:"GUM_WRITE_VALUE"`
|
||||
CharLimit int `help:"Maximum value length (0 for no limit)" default:"400"`
|
||||
ShowHelp bool `help:"Show help key binds" negatable:"" default:"true" env:"GUM_WRITE_SHOW_HELP"`
|
||||
CursorMode string `prefix:"cursor." name:"mode" help:"Cursor mode" default:"blink" enum:"blink,hide,static" env:"GUM_WRITE_CURSOR_MODE"`
|
||||
Width int `help:"Text area width (0 for terminal width)" default:"0" env:"GUM_WRITE_WIDTH"`
|
||||
Height int `help:"Text area height" default:"5" env:"GUM_WRITE_HEIGHT"`
|
||||
Header string `help:"Header value" default:"" env:"GUM_WRITE_HEADER"`
|
||||
Placeholder string `help:"Placeholder value" default:"Write something..." env:"GUM_WRITE_PLACEHOLDER"`
|
||||
Prompt string `help:"Prompt to display" default:"┃ " env:"GUM_WRITE_PROMPT"`
|
||||
ShowCursorLine bool `help:"Show cursor line" default:"false" env:"GUM_WRITE_SHOW_CURSOR_LINE"`
|
||||
ShowLineNumbers bool `help:"Show line numbers" default:"false" env:"GUM_WRITE_SHOW_LINE_NUMBERS"`
|
||||
Value string `help:"Initial value (can be passed via stdin)" default:"" env:"GUM_WRITE_VALUE"`
|
||||
CharLimit int `help:"Maximum value length (0 for no limit)" default:"400"`
|
||||
ShowHelp bool `help:"Show help key binds" negatable:"" default:"true" env:"GUM_WRITE_SHOW_HELP"`
|
||||
CursorMode string `prefix:"cursor." name:"mode" help:"Cursor mode" default:"blink" enum:"blink,hide,static" env:"GUM_WRITE_CURSOR_MODE"`
|
||||
Timeout time.Duration `help:"Timeout until choose returns selected element" default:"0s" env:"GUM_WRITE_TIMEOUT"`
|
||||
|
||||
BaseStyle style.Styles `embed:"" prefix:"base." envprefix:"GUM_WRITE_BASE_"`
|
||||
CursorLineNumberStyle style.Styles `embed:"" prefix:"cursor-line-number." set:"defaultForeground=7" envprefix:"GUM_WRITE_CURSOR_LINE_NUMBER_"`
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import (
|
|||
type keymap struct {
|
||||
textarea.KeyMap
|
||||
Submit key.Binding
|
||||
Quit key.Binding
|
||||
Abort key.Binding
|
||||
OpenInEditor key.Binding
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ func defaultKeymap() keymap {
|
|||
)
|
||||
return keymap{
|
||||
KeyMap: km,
|
||||
Quit: key.NewBinding(
|
||||
Abort: key.NewBinding(
|
||||
key.WithKeys("ctrl+c"),
|
||||
key.WithHelp("ctrl+c", "cancel"),
|
||||
),
|
||||
|
|
@ -64,7 +64,6 @@ func defaultKeymap() keymap {
|
|||
|
||||
type model struct {
|
||||
autoWidth bool
|
||||
aborted bool
|
||||
header string
|
||||
headerStyle lipgloss.Style
|
||||
quitting bool
|
||||
|
|
@ -75,6 +74,7 @@ type model struct {
|
|||
}
|
||||
|
||||
func (m model) Init() tea.Cmd { return textarea.Blink }
|
||||
|
||||
func (m model) View() string {
|
||||
if m.quitting {
|
||||
return ""
|
||||
|
|
@ -107,18 +107,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m, openEditor(msg.path, msg.lineno)
|
||||
case editorFinishedMsg:
|
||||
if msg.err != nil {
|
||||
m.aborted = true
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
return m, tea.Interrupt
|
||||
}
|
||||
m.textarea.SetValue(msg.content)
|
||||
case tea.KeyMsg:
|
||||
km := m.keymap
|
||||
switch {
|
||||
case key.Matches(msg, km.Quit):
|
||||
m.aborted = true
|
||||
case key.Matches(msg, km.Abort):
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
return m, tea.Interrupt
|
||||
case key.Matches(msg, km.Submit):
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue