mirror of
https://github.com/charmbracelet/gum
synced 2024-06-09 01:02:17 +02:00
Merge branch 'main' into output-on-error
This commit is contained in:
commit
13a37dfb64
|
@ -294,6 +294,8 @@ gum pager < README.md
|
|||
Display a spinner while running a script or command. The spinner will
|
||||
automatically stop after the given command exits.
|
||||
|
||||
To view or pipe the command's output, use the `--show-output` flag.
|
||||
|
||||
```bash
|
||||
gum spin --spinner dot --title "Buying Bubble Gum..." -- sleep 5
|
||||
```
|
||||
|
|
|
@ -12,6 +12,9 @@ package choose
|
|||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
|
||||
"github.com/charmbracelet/bubbles/paginator"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
@ -39,6 +42,8 @@ type model struct {
|
|||
headerStyle lipgloss.Style
|
||||
itemStyle lipgloss.Style
|
||||
selectedItemStyle lipgloss.Style
|
||||
hasTimeout bool
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
type item struct {
|
||||
|
@ -47,13 +52,27 @@ type item struct {
|
|||
order int
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd { return nil }
|
||||
func (m model) Init() tea.Cmd {
|
||||
return timeout.Init(m.timeout, 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
|
||||
// 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))
|
||||
switch keypress := msg.String(); keypress {
|
||||
|
@ -151,6 +170,7 @@ 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] {
|
||||
|
@ -161,7 +181,10 @@ func (m model) View() string {
|
|||
}
|
||||
|
||||
if item.selected {
|
||||
s.WriteString(m.selectedItemStyle.Render(m.selectedPrefix + item.text))
|
||||
if m.hasTimeout {
|
||||
timeoutStr = timeout.Str(m.timeout)
|
||||
}
|
||||
s.WriteString(m.selectedItemStyle.Render(m.selectedPrefix + item.text + timeoutStr))
|
||||
} else if i == m.index%m.height {
|
||||
s.WriteString(m.cursorStyle.Render(m.cursorPrefix + item.text))
|
||||
} else {
|
||||
|
|
|
@ -112,6 +112,8 @@ func (o Options) Run() error {
|
|||
itemStyle: o.ItemStyle.ToLipgloss(),
|
||||
selectedItemStyle: o.SelectedItemStyle.ToLipgloss(),
|
||||
numSelected: currentSelected,
|
||||
hasTimeout: o.Timeout > 0,
|
||||
timeout: o.Timeout,
|
||||
}, tea.WithOutput(os.Stderr)).Run()
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -1,23 +1,27 @@
|
|||
package choose
|
||||
|
||||
import "github.com/charmbracelet/gum/style"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Options is the customization options for the choose command.
|
||||
type Options struct {
|
||||
Options []string `arg:"" optional:"" help:"Options to choose from."`
|
||||
|
||||
Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"`
|
||||
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
|
||||
Ordered bool `help:"Maintain the order of the selected options" env:"GUM_CHOOSE_ORDERED"`
|
||||
Height int `help:"Height of the list" default:"10" env:"GUM_CHOOSE_HEIGHT"`
|
||||
Cursor string `help:"Prefix to show on item that corresponds to the cursor position" default:"> " env:"GUM_CHOOSE_CURSOR"`
|
||||
Header string `help:"Header value" default:"" env:"GUM_CHOOSE_HEADER"`
|
||||
CursorPrefix string `help:"Prefix to show on the cursor item (hidden if limit is 1)" default:"○ " env:"GUM_CHOOSE_CURSOR_PREFIX"`
|
||||
SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:"◉ " env:"GUM_CHOOSE_SELECTED_PREFIX"`
|
||||
UnselectedPrefix string `help:"Prefix to show on unselected items (hidden if limit is 1)" default:"○ " env:"GUM_CHOOSE_UNSELECTED_PREFIX"`
|
||||
Selected []string `help:"Options that should start as selected" default:"" env:"GUM_CHOOSE_SELECTED"`
|
||||
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_CURSOR_"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_CHOOSE_HEADER_"`
|
||||
ItemStyle style.Styles `embed:"" prefix:"item." hidden:"" envprefix:"GUM_CHOOSE_ITEM_"`
|
||||
SelectedItemStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_SELECTED_"`
|
||||
Options []string `arg:"" optional:"" help:"Options to choose from."`
|
||||
Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"`
|
||||
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
|
||||
Ordered bool `help:"Maintain the order of the selected options" env:"GUM_CHOOSE_ORDERED"`
|
||||
Height int `help:"Height of the list" default:"10" env:"GUM_CHOOSE_HEIGHT"`
|
||||
Cursor string `help:"Prefix to show on item that corresponds to the cursor position" default:"> " env:"GUM_CHOOSE_CURSOR"`
|
||||
Header string `help:"Header value" default:"" env:"GUM_CHOOSE_HEADER"`
|
||||
CursorPrefix string `help:"Prefix to show on the cursor item (hidden if limit is 1)" default:"○ " env:"GUM_CHOOSE_CURSOR_PREFIX"`
|
||||
SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:"◉ " env:"GUM_CHOOSE_SELECTED_PREFIX"`
|
||||
UnselectedPrefix string `help:"Prefix to show on unselected items (hidden if limit is 1)" default:"○ " env:"GUM_CHOOSE_UNSELECTED_PREFIX"`
|
||||
Selected []string `help:"Options that should start as selected" default:"" env:"GUM_CHOOSE_SELECTED"`
|
||||
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_CURSOR_"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_CHOOSE_HEADER_"`
|
||||
ItemStyle style.Styles `embed:"" prefix:"item." hidden:"" envprefix:"GUM_CHOOSE_ITEM_"`
|
||||
SelectedItemStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_SELECTED_"`
|
||||
Timeout time.Duration `help:"Timeout until choose returns selected element" default:"0" env:"GUM_CCHOOSE_TIMEOUT"` // including timeout command options [Timeout,...]
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
|
@ -31,7 +33,7 @@ func (o Options) Run() error {
|
|||
}
|
||||
|
||||
if m.(model).aborted {
|
||||
os.Exit(130)
|
||||
os.Exit(exit.StatusAborted)
|
||||
} else if m.(model).confirmation {
|
||||
os.Exit(0)
|
||||
} else {
|
||||
|
|
|
@ -11,9 +11,10 @@
|
|||
package confirm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
@ -37,21 +38,8 @@ type model struct {
|
|||
unselectedStyle lipgloss.Style
|
||||
}
|
||||
|
||||
const tickInterval = time.Second
|
||||
|
||||
type tickMsg struct{}
|
||||
|
||||
func tick() tea.Cmd {
|
||||
return tea.Tick(tickInterval, func(time.Time) tea.Msg {
|
||||
return tickMsg{}
|
||||
})
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
if m.timeout > 0 {
|
||||
return tick()
|
||||
}
|
||||
return nil
|
||||
return timeout.Init(m.timeout, m.defaultSelection)
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
@ -61,6 +49,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
m.confirmation = false
|
||||
m.aborted = true
|
||||
fallthrough
|
||||
case "esc":
|
||||
|
@ -85,14 +74,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.confirmation = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
case tickMsg:
|
||||
if m.timeout <= 0 {
|
||||
case timeout.TickTimeoutMsg:
|
||||
|
||||
if msg.TimeoutValue <= 0 {
|
||||
m.quitting = true
|
||||
m.confirmation = m.defaultSelection
|
||||
return m, tea.Quit
|
||||
}
|
||||
m.timeout -= tickInterval
|
||||
return m, tick()
|
||||
|
||||
m.timeout = msg.TimeoutValue
|
||||
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
@ -102,27 +93,23 @@ func (m model) View() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
var aff, neg, timeout, affirmativeTimeout, negativeTimeout string
|
||||
|
||||
var aff, neg, timeoutStrYes, timeoutStrNo string
|
||||
timeoutStrNo = ""
|
||||
timeoutStrYes = ""
|
||||
if m.hasTimeout {
|
||||
timeout = fmt.Sprintf(" (%d)", max(0, int(m.timeout.Seconds())))
|
||||
}
|
||||
|
||||
// set timer based on defaultSelection
|
||||
if m.defaultSelection {
|
||||
affirmativeTimeout = m.affirmative + timeout
|
||||
negativeTimeout = m.negative
|
||||
} else {
|
||||
affirmativeTimeout = m.affirmative
|
||||
negativeTimeout = m.negative + timeout
|
||||
if m.defaultSelection {
|
||||
timeoutStrYes = timeout.Str(m.timeout)
|
||||
} else {
|
||||
timeoutStrNo = timeout.Str(m.timeout)
|
||||
}
|
||||
}
|
||||
|
||||
if m.confirmation {
|
||||
aff = m.selectedStyle.Render(affirmativeTimeout)
|
||||
neg = m.unselectedStyle.Render(negativeTimeout)
|
||||
aff = m.selectedStyle.Render(m.affirmative + timeoutStrYes)
|
||||
neg = m.unselectedStyle.Render(m.negative + timeoutStrNo)
|
||||
} else {
|
||||
aff = m.unselectedStyle.Render(affirmativeTimeout)
|
||||
neg = m.selectedStyle.Render(negativeTimeout)
|
||||
aff = m.unselectedStyle.Render(m.affirmative + timeoutStrYes)
|
||||
neg = m.selectedStyle.Render(m.negative + timeoutStrNo)
|
||||
}
|
||||
|
||||
// If the option is intentionally empty, do not show it.
|
||||
|
@ -132,10 +119,3 @@ func (m model) View() string {
|
|||
|
||||
return lipgloss.JoinVertical(lipgloss.Center, m.promptStyle.Render(m.prompt), lipgloss.JoinHorizontal(lipgloss.Left, aff, neg))
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
|
|
@ -8,14 +8,14 @@ import (
|
|||
|
||||
// Options is the customization options for the confirm command.
|
||||
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"`
|
||||
Default bool `help:"Default confirmation action" default:"true"`
|
||||
Timeout time.Duration `help:"Timeout for confirmation" default:"0" env:"GUM_CONFIRM_TIMEOUT"`
|
||||
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_"`
|
||||
Default bool `help:"Default confirmation action" default:"true"`
|
||||
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_"`
|
||||
//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_"`
|
||||
//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=1 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"`
|
||||
}
|
||||
|
|
|
@ -6,10 +6,11 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/charmbracelet/bubbles/filepicker"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
|
@ -46,7 +47,12 @@ func (o Options) Run() error {
|
|||
fp.Styles.Selected = o.SelectedStyle.ToLipgloss()
|
||||
fp.Styles.FileSize = o.FileSizeStyle.ToLipgloss()
|
||||
|
||||
m := model{filepicker: fp}
|
||||
m := model{
|
||||
filepicker: fp,
|
||||
timeout: o.Timeout,
|
||||
hasTimeout: o.Timeout > 0,
|
||||
aborted: false,
|
||||
}
|
||||
|
||||
tm, err := tea.NewProgram(&m, tea.WithOutput(os.Stderr)).Run()
|
||||
if err != nil {
|
||||
|
|
18
file/file.go
18
file/file.go
|
@ -13,8 +13,11 @@
|
|||
package file
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/filepicker"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
)
|
||||
|
||||
type model struct {
|
||||
|
@ -22,10 +25,15 @@ type model struct {
|
|||
selectedPath string
|
||||
aborted bool
|
||||
quitting bool
|
||||
timeout time.Duration
|
||||
hasTimeout bool
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return m.filepicker.Init()
|
||||
return tea.Batch(
|
||||
timeout.Init(m.timeout, nil),
|
||||
m.filepicker.Init(),
|
||||
)
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
@ -37,6 +45,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
case timeout.TickTimeoutMsg:
|
||||
if msg.TimeoutValue <= 0 {
|
||||
m.quitting = true
|
||||
m.aborted = 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)
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package file
|
||||
|
||||
import "github.com/charmbracelet/gum/style"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Options are the options for the file command.
|
||||
type Options struct {
|
||||
|
@ -21,5 +25,6 @@ type Options struct {
|
|||
//nolint:staticcheck
|
||||
SelectedStyle style.Styles `embed:"" prefix:"selected." help:"The style to use for the selected item" set:"defaultBold=true" set:"defaultForeground=212" envprefix:"GUM_FILE_SELECTED_"`
|
||||
//nolint:staticcheck
|
||||
FileSizeStyle style.Styles `embed:"" prefix:"file-size." help:"The style to use for file sizes" set:"defaultWidth=8" set:"defaultAlign=right" set:"defaultForeground=240" envprefix:"GUM_FILE_FILE_SIZE_"`
|
||||
FileSizeStyle style.Styles `embed:"" prefix:"file-size." help:"The style to use for file sizes" set:"defaultWidth=8" set:"defaultAlign=right" set:"defaultForeground=240" envprefix:"GUM_FILE_FILE_SIZE_"`
|
||||
Timeout time.Duration `help:"Timeout until command aborts without a selection" default:"0" env:"GUM_FILE_TIMEOUT"`
|
||||
}
|
||||
|
|
|
@ -84,11 +84,14 @@ func (o Options) Run() error {
|
|||
matchStyle: o.MatchStyle.ToLipgloss(),
|
||||
headerStyle: o.HeaderStyle.ToLipgloss(),
|
||||
textStyle: o.TextStyle.ToLipgloss(),
|
||||
cursorTextStyle: o.CursorTextStyle.ToLipgloss(),
|
||||
height: o.Height,
|
||||
selected: make(map[string]struct{}),
|
||||
limit: o.Limit,
|
||||
reverse: o.Reverse,
|
||||
fuzzy: o.Fuzzy,
|
||||
timeout: o.Timeout,
|
||||
hasTimeout: o.Timeout > 0,
|
||||
sort: o.Sort,
|
||||
}, options...)
|
||||
|
||||
|
@ -107,13 +110,7 @@ func (o Options) Run() error {
|
|||
// than 1 or if flag --no-limit is passed, hence there is
|
||||
// no need to further checks
|
||||
if len(m.selected) > 0 {
|
||||
for k := range m.selected {
|
||||
if isTTY {
|
||||
fmt.Println(k)
|
||||
} else {
|
||||
fmt.Println(ansi.Strip(k))
|
||||
}
|
||||
}
|
||||
o.checkSelected(m, isTTY)
|
||||
} else if len(m.matches) > m.cursor && m.cursor >= 0 {
|
||||
if isTTY {
|
||||
fmt.Println(m.matches[m.cursor].Str)
|
||||
|
@ -128,6 +125,16 @@ func (o Options) Run() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (o Options) checkSelected(m model, isTTY bool) {
|
||||
for k := range m.selected {
|
||||
if isTTY {
|
||||
fmt.Println(k)
|
||||
} else {
|
||||
fmt.Println(ansi.Strip(k))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BeforeReset hook. Used to unclutter style flags.
|
||||
func (o Options) BeforeReset(ctx *kong.Context) error {
|
||||
style.HideFlags(ctx)
|
||||
|
|
|
@ -12,6 +12,9 @@ package filter
|
|||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
|
@ -39,21 +42,27 @@ type model struct {
|
|||
headerStyle lipgloss.Style
|
||||
matchStyle lipgloss.Style
|
||||
textStyle lipgloss.Style
|
||||
cursorTextStyle lipgloss.Style
|
||||
indicatorStyle lipgloss.Style
|
||||
selectedPrefixStyle lipgloss.Style
|
||||
unselectedPrefixStyle lipgloss.Style
|
||||
reverse bool
|
||||
fuzzy bool
|
||||
sort bool
|
||||
timeout time.Duration
|
||||
hasTimeout bool
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd { return nil }
|
||||
func (m model) Init() tea.Cmd {
|
||||
return timeout.Init(m.timeout, nil)
|
||||
}
|
||||
func (m model) View() string {
|
||||
if m.quitting {
|
||||
return ""
|
||||
}
|
||||
|
||||
var s strings.Builder
|
||||
var lineTextStyle lipgloss.Style
|
||||
|
||||
// For reverse layout, if the number of matches is less than the viewport
|
||||
// height, we need to offset the matches so that the first match is at the
|
||||
|
@ -74,10 +83,14 @@ func (m model) View() string {
|
|||
|
||||
// If this is the current selected index, we add a small indicator to
|
||||
// represent it. Otherwise, simply pad the string.
|
||||
// The line's text style is set depending on whether or not the cursor
|
||||
// points to this line.
|
||||
if i == m.cursor {
|
||||
s.WriteString(m.indicatorStyle.Render(m.indicator))
|
||||
lineTextStyle = m.cursorTextStyle
|
||||
} else {
|
||||
s.WriteString(strings.Repeat(" ", lipgloss.Width(m.indicator)))
|
||||
lineTextStyle = m.textStyle
|
||||
}
|
||||
|
||||
// If there are multiple selections mark them, otherwise leave an empty space
|
||||
|
@ -99,7 +112,7 @@ func (m model) View() string {
|
|||
// index. If so, color the character to indicate a match.
|
||||
if mi < len(match.MatchedIndexes) && ci == match.MatchedIndexes[mi] {
|
||||
// Flush text buffer.
|
||||
s.WriteString(m.textStyle.Render(buf.String()))
|
||||
s.WriteString(lineTextStyle.Render(buf.String()))
|
||||
buf.Reset()
|
||||
|
||||
s.WriteString(m.matchStyle.Render(string(c)))
|
||||
|
@ -112,7 +125,7 @@ func (m model) View() string {
|
|||
}
|
||||
}
|
||||
// Flush text buffer.
|
||||
s.WriteString(m.textStyle.Render(buf.String()))
|
||||
s.WriteString(lineTextStyle.Render(buf.String()))
|
||||
|
||||
// We have finished displaying the match with all of it's matched
|
||||
// characters highlighted and the rest filled in.
|
||||
|
@ -143,6 +156,15 @@ 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 {
|
||||
m.quitting = true
|
||||
m.aborted = 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())
|
||||
|
|
|
@ -1,29 +1,35 @@
|
|||
package filter
|
||||
|
||||
import "github.com/charmbracelet/gum/style"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Options is the customization options for the filter command.
|
||||
type Options struct {
|
||||
Indicator string `help:"Character for selection" default:"•" env:"GUM_FILTER_INDICATOR"`
|
||||
IndicatorStyle style.Styles `embed:"" prefix:"indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_INDICATOR_"`
|
||||
Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"`
|
||||
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
|
||||
Strict bool `help:"Only returns if anything matched. Otherwise return Filter" negatable:"true" default:"true" group:"Selection"`
|
||||
SelectedPrefix string `help:"Character to indicate selected items (hidden if limit is 1)" default:" ◉ " env:"GUM_FILTER_SELECTED_PREFIX"`
|
||||
SelectedPrefixStyle style.Styles `embed:"" prefix:"selected-indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_SELECTED_PREFIX_"`
|
||||
UnselectedPrefix string `help:"Character to indicate unselected items (hidden if limit is 1)" default:" ○ " env:"GUM_FILTER_UNSELECTED_PREFIX"`
|
||||
UnselectedPrefixStyle style.Styles `embed:"" prefix:"unselected-prefix." set:"defaultForeground=240" envprefix:"GUM_FILTER_UNSELECTED_PREFIX_"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_FILTER_HEADER_"`
|
||||
Header string `help:"Header value" default:"" env:"GUM_FILTER_HEADER"`
|
||||
TextStyle style.Styles `embed:"" prefix:"text." envprefix:"GUM_FILTER_TEXT_"`
|
||||
MatchStyle style.Styles `embed:"" prefix:"match." set:"defaultForeground=212" envprefix:"GUM_FILTER_MATCH_"`
|
||||
Placeholder string `help:"Placeholder value" default:"Filter..." env:"GUM_FILTER_PLACEHOLDER"`
|
||||
Prompt string `help:"Prompt to display" default:"> " env:"GUM_FILTER_PROMPT"`
|
||||
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=240" envprefix:"GUM_FILTER_PROMPT_"`
|
||||
Width int `help:"Input width" default:"20" env:"GUM_FILTER_WIDTH"`
|
||||
Height int `help:"Input height" default:"0" env:"GUM_FILTER_HEIGHT"`
|
||||
Value string `help:"Initial filter value" default:"" env:"GUM_FILTER_VALUE"`
|
||||
Reverse bool `help:"Display from the bottom of the screen" env:"GUM_FILTER_REVERSE"`
|
||||
Fuzzy bool `help:"Enable fuzzy matching" default:"true" env:"GUM_FILTER_FUZZY" negatable:""`
|
||||
Sort bool `help:"Sort the results" default:"true" env:"GUM_FILTER_SORT" negatable:""`
|
||||
Indicator string `help:"Character for selection" default:"•" env:"GUM_FILTER_INDICATOR"`
|
||||
IndicatorStyle style.Styles `embed:"" prefix:"indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_INDICATOR_"`
|
||||
Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"`
|
||||
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
|
||||
Strict bool `help:"Only returns if anything matched. Otherwise return Filter" negatable:"true" default:"true" group:"Selection"`
|
||||
SelectedPrefix string `help:"Character to indicate selected items (hidden if limit is 1)" default:" ◉ " env:"GUM_FILTER_SELECTED_PREFIX"`
|
||||
SelectedPrefixStyle style.Styles `embed:"" prefix:"selected-indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_SELECTED_PREFIX_"`
|
||||
UnselectedPrefix string `help:"Character to indicate unselected items (hidden if limit is 1)" default:" ○ " env:"GUM_FILTER_UNSELECTED_PREFIX"`
|
||||
UnselectedPrefixStyle style.Styles `embed:"" prefix:"unselected-prefix." set:"defaultForeground=240" envprefix:"GUM_FILTER_UNSELECTED_PREFIX_"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_FILTER_HEADER_"`
|
||||
Header string `help:"Header value" default:"" env:"GUM_FILTER_HEADER"`
|
||||
TextStyle style.Styles `embed:"" prefix:"text." envprefix:"GUM_FILTER_TEXT_"`
|
||||
CursorTextStyle style.Styles `embed:"" prefix:"cursor-text." envprefix:"GUM_FILTER_CURSOR_TEXT_"`
|
||||
MatchStyle style.Styles `embed:"" prefix:"match." set:"defaultForeground=212" envprefix:"GUM_FILTER_MATCH_"`
|
||||
Placeholder string `help:"Placeholder value" default:"Filter..." env:"GUM_FILTER_PLACEHOLDER"`
|
||||
Prompt string `help:"Prompt to display" default:"> " env:"GUM_FILTER_PROMPT"`
|
||||
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=240" envprefix:"GUM_FILTER_PROMPT_"`
|
||||
Width int `help:"Input width" default:"20" env:"GUM_FILTER_WIDTH"`
|
||||
Height int `help:"Input height" default:"0" env:"GUM_FILTER_HEIGHT"`
|
||||
Value string `help:"Initial filter value" default:"" env:"GUM_FILTER_VALUE"`
|
||||
Reverse bool `help:"Display from the bottom of the screen" env:"GUM_FILTER_REVERSE"`
|
||||
Fuzzy bool `help:"Enable fuzzy matching" default:"true" env:"GUM_FILTER_FUZZY" negatable:""`
|
||||
Sort bool `help:"Sort the results" default:"true" env:"GUM_FILTER_SORT" negatable:""`
|
||||
Timeout time.Duration `help:"Timeout until filter command aborts" default:"0" env:"GUM_FILTER_TIMEOUT"`
|
||||
}
|
||||
|
|
8
go.mod
8
go.mod
|
@ -3,16 +3,16 @@ module github.com/charmbracelet/gum
|
|||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/alecthomas/kong v0.7.1
|
||||
github.com/alecthomas/kong v0.8.0
|
||||
github.com/alecthomas/mango-kong v0.1.0
|
||||
github.com/charmbracelet/bubbles v0.16.1
|
||||
github.com/charmbracelet/bubbletea v0.24.2
|
||||
github.com/charmbracelet/glamour v0.6.1-0.20230531150759-6d5b52861a9d
|
||||
github.com/charmbracelet/lipgloss v0.7.2-0.20230316100548-06dd20ee5707
|
||||
github.com/mattn/go-isatty v0.0.18
|
||||
github.com/mattn/go-isatty v0.0.19
|
||||
github.com/muesli/reflow v0.3.0
|
||||
github.com/muesli/roff v0.1.0
|
||||
github.com/muesli/termenv v0.15.2-0.20230323153104-73a40463ff25
|
||||
github.com/muesli/termenv v0.15.2
|
||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f
|
||||
)
|
||||
|
||||
|
@ -38,7 +38,7 @@ require (
|
|||
github.com/yuin/goldmark-emoji v1.0.1 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
)
|
||||
|
|
19
go.sum
19
go.sum
|
@ -1,8 +1,8 @@
|
|||
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
|
||||
github.com/alecthomas/chroma/v2 v2.7.0 h1:hm1rY6c/Ob4eGclpQ7X/A3yhqBOZNUTk9q+yhyLIViI=
|
||||
github.com/alecthomas/chroma/v2 v2.7.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw=
|
||||
github.com/alecthomas/kong v0.7.1 h1:azoTh0IOfwlAX3qN9sHWTxACE2oV8Bg2gAwBsMwDQY4=
|
||||
github.com/alecthomas/kong v0.7.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
|
||||
github.com/alecthomas/kong v0.8.0 h1:ryDCzutfIqJPnNn0omnrgHLbAggDQM2VWHikE1xqK7s=
|
||||
github.com/alecthomas/kong v0.8.0/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
|
||||
github.com/alecthomas/mango-kong v0.1.0 h1:iFVfP1k1K4qpml3JUQmD5I8MCQYfIvsD9mRdrw7jJC4=
|
||||
github.com/alecthomas/mango-kong v0.1.0/go.mod h1:t+TYVdsONUolf/BwVcm+15eqcdAj15h4Qe9MMFAwwT4=
|
||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||
|
@ -18,6 +18,7 @@ github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06
|
|||
github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg=
|
||||
github.com/charmbracelet/glamour v0.6.1-0.20230531150759-6d5b52861a9d h1:S4Ejl/M2VrryIgDrDbiuvkwMUDa67/t/H3Wz3i2/vUw=
|
||||
github.com/charmbracelet/glamour v0.6.1-0.20230531150759-6d5b52861a9d/go.mod h1:swCB3CXFsh22H1ESDYdY1tirLiNqCziulDyJ1B6Nt7Q=
|
||||
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||
github.com/charmbracelet/lipgloss v0.7.2-0.20230316100548-06dd20ee5707 h1:dXv2HjaDlJZj7wLpTjg1P4B68bdvoXfx7+VXF2/RelY=
|
||||
github.com/charmbracelet/lipgloss v0.7.2-0.20230316100548-06dd20ee5707/go.mod h1:BDceYFEeE5FBoGZeuApZ+V4wSgi8AOIHoryyjYbCTHM=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
|
||||
|
@ -32,8 +33,8 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq
|
|||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
|
@ -52,10 +53,11 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
|||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
|
||||
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
|
||||
github.com/muesli/termenv v0.15.2-0.20230323153104-73a40463ff25 h1:bgCNxFKF+mM5GxpNvkGleUFt12xOzLOzmMOytttpeK4=
|
||||
github.com/muesli/termenv v0.15.2-0.20230323153104-73a40463ff25/go.mod h1:puu7Fg2fBjAuOzC9hb6zDO/s86uLSYrBlPkIplp2EiA=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
|
@ -67,14 +69,17 @@ github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
|
|||
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
|
||||
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
|
|
|
@ -43,6 +43,8 @@ func (o Options) Run() error {
|
|||
aborted: false,
|
||||
header: o.Header,
|
||||
headerStyle: o.HeaderStyle.ToLipgloss(),
|
||||
timeout: o.Timeout,
|
||||
hasTimeout: o.Timeout > 0,
|
||||
autoWidth: o.Width < 1,
|
||||
}, tea.WithOutput(os.Stderr))
|
||||
tm, err := p.Run()
|
||||
|
|
|
@ -8,8 +8,11 @@
|
|||
package input
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
|
@ -20,14 +23,20 @@ type model struct {
|
|||
textinput textinput.Model
|
||||
quitting bool
|
||||
aborted bool
|
||||
timeout time.Duration
|
||||
hasTimeout bool
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd { return textinput.Blink }
|
||||
func (m model) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
textinput.Blink,
|
||||
timeout.Init(m.timeout, nil),
|
||||
)
|
||||
}
|
||||
func (m model) View() string {
|
||||
if m.quitting {
|
||||
return ""
|
||||
}
|
||||
|
||||
if m.header != "" {
|
||||
header := m.headerStyle.Render(m.header)
|
||||
return lipgloss.JoinVertical(lipgloss.Left, header, m.textinput.View())
|
||||
|
@ -38,6 +47,14 @@ 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.aborted = 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
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
package input
|
||||
|
||||
import "github.com/charmbracelet/gum/style"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Options are the customization options for the input.
|
||||
type Options struct {
|
||||
Placeholder string `help:"Placeholder value" default:"Type something..." env:"GUM_INPUT_PLACEHOLDER"`
|
||||
Prompt string `help:"Prompt to display" default:"> " env:"GUM_INPUT_PROMPT"`
|
||||
PromptStyle style.Styles `embed:"" prefix:"prompt." envprefix:"GUM_INPUT_PROMPT_"`
|
||||
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_INPUT_CURSOR_"`
|
||||
CursorMode string `prefix:"cursor." name:"mode" help:"Cursor mode" default:"blink" enum:"blink,hide,static" env:"GUM_INPUT_CURSOR_MODE"`
|
||||
Value string `help:"Initial value (can also be passed via stdin)" default:""`
|
||||
CharLimit int `help:"Maximum value length (0 for no limit)" default:"400"`
|
||||
Width int `help:"Input width (0 for terminal width)" default:"40" env:"GUM_INPUT_WIDTH"`
|
||||
Password bool `help:"Mask input characters" default:"false"`
|
||||
Header string `help:"Header value" default:"" env:"GUM_INPUT_HEADER"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_INPUT_HEADER_"`
|
||||
Placeholder string `help:"Placeholder value" default:"Type something..." env:"GUM_INPUT_PLACEHOLDER"`
|
||||
Prompt string `help:"Prompt to display" default:"> " env:"GUM_INPUT_PROMPT"`
|
||||
PromptStyle style.Styles `embed:"" prefix:"prompt." envprefix:"GUM_INPUT_PROMPT_"`
|
||||
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_INPUT_CURSOR_"`
|
||||
CursorMode string `prefix:"cursor." name:"mode" help:"Cursor mode" default:"blink" enum:"blink,hide,static" env:"GUM_INPUT_CURSOR_MODE"`
|
||||
Value string `help:"Initial value (can also be passed via stdin)" default:""`
|
||||
CharLimit int `help:"Maximum value length (0 for no limit)" default:"400"`
|
||||
Width int `help:"Input width (0 for terminal width)" default:"40" env:"GUM_INPUT_WIDTH"`
|
||||
Password bool `help:"Mask input characters" default:"false"`
|
||||
Header string `help:"Header value" default:"" env:"GUM_INPUT_HEADER"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_INPUT_HEADER_"`
|
||||
Timeout time.Duration `help:"Timeout until input aborts" default:"0" env:"GUM_INPUT_TIMEOUT"`
|
||||
}
|
||||
|
|
|
@ -41,6 +41,8 @@ func (o Options) Run() error {
|
|||
softWrap: o.SoftWrap,
|
||||
matchStyle: o.MatchStyle.ToLipgloss(),
|
||||
matchHighlightStyle: o.MatchHighlightStyle.ToLipgloss(),
|
||||
timeout: o.Timeout,
|
||||
hasTimeout: o.Timeout > 0,
|
||||
}
|
||||
_, err := tea.NewProgram(model, tea.WithAltScreen()).Run()
|
||||
if err != nil {
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
package pager
|
||||
|
||||
import "github.com/charmbracelet/gum/style"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Options are the options for the pager.
|
||||
type Options struct {
|
||||
//nolint:staticcheck
|
||||
Style style.Styles `embed:"" help:"Style the pager" set:"defaultBorder=rounded" set:"defaultPadding=0 1" set:"defaultBorderForeground=212" envprefix:"GUM_PAGER_"`
|
||||
HelpStyle style.Styles `embed:"" prefix:"help." help:"Style the help text" set:"defaultForeground=241" envprefix:"GUM_PAGER_HELP_"`
|
||||
Content string `arg:"" optional:"" help:"Display content to scroll"`
|
||||
ShowLineNumbers bool `help:"Show line numbers" default:"true"`
|
||||
LineNumberStyle style.Styles `embed:"" prefix:"line-number." help:"Style the line numbers" set:"defaultForeground=237" envprefix:"GUM_PAGER_LINE_NUMBER_"`
|
||||
SoftWrap bool `help:"Soft wrap lines" default:"false"`
|
||||
MatchStyle style.Styles `embed:"" prefix:"match." help:"Style the matched text" set:"defaultForeground=212" set:"defaultBold=true" envprefix:"GUM_PAGER_MATCH_"` //nolint:staticcheck
|
||||
MatchHighlightStyle style.Styles `embed:"" prefix:"match-highlight." help:"Style the matched highlight text" set:"defaultForeground=235" set:"defaultBackground=225" set:"defaultBold=true" envprefix:"GUM_PAGER_MATCH_HIGH_"` //nolint:staticcheck
|
||||
Style style.Styles `embed:"" help:"Style the pager" set:"defaultBorder=rounded" set:"defaultPadding=0 1" set:"defaultBorderForeground=212" envprefix:"GUM_PAGER_"`
|
||||
HelpStyle style.Styles `embed:"" prefix:"help." help:"Style the help text" set:"defaultForeground=241" envprefix:"GUM_PAGER_HELP_"`
|
||||
Content string `arg:"" optional:"" help:"Display content to scroll"`
|
||||
ShowLineNumbers bool `help:"Show line numbers" default:"true"`
|
||||
LineNumberStyle style.Styles `embed:"" prefix:"line-number." help:"Style the line numbers" set:"defaultForeground=237" envprefix:"GUM_PAGER_LINE_NUMBER_"`
|
||||
SoftWrap bool `help:"Soft wrap lines" default:"false"`
|
||||
MatchStyle style.Styles `embed:"" prefix:"match." help:"Style the matched text" set:"defaultForeground=212" set:"defaultBold=true" envprefix:"GUM_PAGER_MATCH_"` //nolint:staticcheck
|
||||
MatchHighlightStyle style.Styles `embed:"" prefix:"match-highlight." help:"Style the matched highlight text" set:"defaultForeground=235" set:"defaultBackground=225" set:"defaultBold=true" envprefix:"GUM_PAGER_MATCH_HIGH_"` //nolint:staticcheck
|
||||
Timeout time.Duration `help:"Timeout until command exits" default:"0" env:"GUM_PAGER_TIMEOUT"`
|
||||
}
|
||||
|
|
|
@ -6,6 +6,9 @@ package pager
|
|||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
@ -25,14 +28,23 @@ type model struct {
|
|||
matchStyle lipgloss.Style
|
||||
matchHighlightStyle lipgloss.Style
|
||||
maxWidth int
|
||||
timeout time.Duration
|
||||
hasTimeout bool
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return nil
|
||||
return timeout.Init(m.timeout, nil)
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case timeout.TickTimeoutMsg:
|
||||
if msg.TimeoutValue <= 0 {
|
||||
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:
|
||||
|
@ -84,6 +96,8 @@ func (m *model) ProcessText(msg tea.WindowSizeMsg) {
|
|||
m.viewport.SetContent(text.String())
|
||||
}
|
||||
|
||||
const heightOffset = 2
|
||||
|
||||
func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) {
|
||||
var cmd tea.Cmd
|
||||
if m.search.active {
|
||||
|
@ -95,7 +109,7 @@ func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) {
|
|||
|
||||
// Trigger a view update to highlight the found matches.
|
||||
m.search.NextMatch(&m)
|
||||
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + 2, Width: m.viewport.Width})
|
||||
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
|
||||
} else {
|
||||
m.search.Done()
|
||||
}
|
||||
|
@ -114,10 +128,10 @@ func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) {
|
|||
m.search.Begin()
|
||||
case "p", "N":
|
||||
m.search.PrevMatch(&m)
|
||||
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + 2, Width: m.viewport.Width})
|
||||
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
|
||||
case "n":
|
||||
m.search.NextMatch(&m)
|
||||
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + 2, Width: m.viewport.Width})
|
||||
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
|
||||
case "q", "ctrl+c", "esc":
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
@ -128,13 +142,17 @@ func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) {
|
|||
}
|
||||
|
||||
func (m model) View() string {
|
||||
helpMsg := "\n ↑/↓: Navigate • q: Quit • /: Search "
|
||||
var timeoutStr string
|
||||
if m.hasTimeout {
|
||||
timeoutStr = timeout.Str(m.timeout) + " "
|
||||
}
|
||||
helpMsg := "\n" + timeoutStr + " ↑/↓: 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 " + m.search.input.View()
|
||||
return m.viewport.View() + "\n" + timeoutStr + " " + m.search.input.View()
|
||||
}
|
||||
|
||||
return m.viewport.View() + m.helpStyle.Render(helpMsg)
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/alecthomas/kong"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/mattn/go-isatty"
|
||||
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
"github.com/charmbracelet/gum/style"
|
||||
|
@ -15,14 +16,18 @@ import (
|
|||
// Run provides a shell script interface for the spinner bubble.
|
||||
// https://github.com/charmbracelet/bubbles/spinner
|
||||
func (o Options) Run() error {
|
||||
isTTY := isatty.IsTerminal(os.Stdout.Fd())
|
||||
|
||||
s := spinner.New()
|
||||
s.Style = o.SpinnerStyle.ToLipgloss()
|
||||
s.Spinner = spinnerMap[o.Spinner]
|
||||
m := model{
|
||||
spinner: s,
|
||||
title: o.TitleStyle.ToLipgloss().Render(o.Title),
|
||||
command: o.Command,
|
||||
align: o.Align,
|
||||
spinner: s,
|
||||
title: o.TitleStyle.ToLipgloss().Render(o.Title),
|
||||
command: o.Command,
|
||||
align: o.Align,
|
||||
timeout: o.Timeout,
|
||||
hasTimeout: o.Timeout > 0,
|
||||
}
|
||||
p := tea.NewProgram(m, tea.WithOutput(os.Stderr))
|
||||
mm, err := p.Run()
|
||||
|
@ -46,6 +51,19 @@ func (o Options) Run() error {
|
|||
return exit.ErrAborted
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to access stdout: %w", err)
|
||||
}
|
||||
|
||||
if o.ShowOutput {
|
||||
if !isTTY {
|
||||
_, err := os.Stdout.WriteString(m.stdout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to stdout: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(m.status)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package spin
|
||||
|
||||
import "github.com/charmbracelet/gum/style"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Options is the customization options for the spin command.
|
||||
type Options struct {
|
||||
|
@ -13,4 +17,5 @@ type Options struct {
|
|||
Title string `help:"Text to display to user while spinning" default:"Loading..." env:"GUM_SPIN_TITLE"`
|
||||
TitleStyle style.Styles `embed:"" prefix:"title." envprefix:"GUM_SPIN_TITLE_"`
|
||||
Align string `help:"Alignment of spinner with regard to the title" short:"a" type:"align" enum:"left,right" default:"left" env:"GUM_SPIN_ALIGN"`
|
||||
Timeout time.Duration `help:"Timeout until spin command aborts" default:"0" env:"GUM_SPIN_TIMEOUT"`
|
||||
}
|
||||
|
|
55
spin/spin.go
55
spin/spin.go
|
@ -17,26 +17,33 @@ package spin
|
|||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type model struct {
|
||||
spinner spinner.Model
|
||||
title string
|
||||
align string
|
||||
command []string
|
||||
aborted bool
|
||||
|
||||
status int
|
||||
stdout string
|
||||
stderr string
|
||||
spinner spinner.Model
|
||||
title string
|
||||
align string
|
||||
command []string
|
||||
aborted bool
|
||||
status int
|
||||
stdout string
|
||||
showOutput bool
|
||||
timeout time.Duration
|
||||
hasTimeout bool
|
||||
}
|
||||
|
||||
var outbuf strings.Builder
|
||||
var errbuf strings.Builder
|
||||
|
||||
type finishCommandMsg struct {
|
||||
stdout string
|
||||
stderr string
|
||||
status int
|
||||
}
|
||||
|
||||
|
@ -48,7 +55,6 @@ func commandStart(command []string) tea.Cmd {
|
|||
}
|
||||
cmd := exec.Command(command[0], args...) //nolint:gosec
|
||||
|
||||
var outbuf, errbuf strings.Builder
|
||||
cmd.Stdout = &outbuf
|
||||
cmd.Stderr = &errbuf
|
||||
|
||||
|
@ -62,7 +68,6 @@ func commandStart(command []string) tea.Cmd {
|
|||
|
||||
return finishCommandMsg{
|
||||
stdout: outbuf.String(),
|
||||
stderr: errbuf.String(),
|
||||
status: status,
|
||||
}
|
||||
}
|
||||
|
@ -72,22 +77,38 @@ func (m model) Init() tea.Cmd {
|
|||
return tea.Batch(
|
||||
m.spinner.Tick,
|
||||
commandStart(m.command),
|
||||
timeout.Init(m.timeout, nil),
|
||||
)
|
||||
}
|
||||
func (m model) View() string {
|
||||
if m.align == "left" {
|
||||
return m.spinner.View() + " " + m.title
|
||||
var str string
|
||||
if m.hasTimeout {
|
||||
str = timeout.Str(m.timeout)
|
||||
}
|
||||
|
||||
return m.title + " " + m.spinner.View()
|
||||
var header string
|
||||
if m.align == "left" {
|
||||
header = m.spinner.View() + str + " " + m.title
|
||||
} else {
|
||||
header = str + " " + m.title + " " + m.spinner.View()
|
||||
}
|
||||
if !m.showOutput {
|
||||
return header
|
||||
}
|
||||
return header + errbuf.String() + "\n" + outbuf.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.status = exit.StatusAborted
|
||||
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
|
||||
m.status = msg.status
|
||||
return m, tea.Quit
|
||||
case tea.KeyMsg:
|
||||
|
|
55
timeout/options.go
Normal file
55
timeout/options.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
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())))
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
Loading…
Reference in a new issue