diff --git a/choose/choose.go b/choose/choose.go index 48f8857..c6be614 100644 --- a/choose/choose.go +++ b/choose/choose.go @@ -18,6 +18,7 @@ import ( "github.com/charmbracelet/bubbles/paginator" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/x/exp/ordered" ) func defaultKeymap() keymap { @@ -97,6 +98,7 @@ func (k keymap) ShortHelp() []key.Binding { type model struct { height int + padding []int cursor string selectedPrefix string unselectedPrefix string @@ -157,10 +159,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.paginator.PrevPage() } case key.Matches(msg, km.Right): - m.index = clamp(m.index+m.height, 0, len(m.items)-1) + m.index = ordered.Clamp(m.index+m.height, 0, len(m.items)-1) m.paginator.NextPage() case key.Matches(msg, km.Left): - m.index = clamp(m.index-m.height, 0, len(m.items)-1) + m.index = ordered.Clamp(m.index-m.height, 0, len(m.items)-1) m.paginator.PrevPage() case key.Matches(msg, km.End): m.index = len(m.items) - 1 @@ -280,15 +282,8 @@ func (m model) View() string { parts = append(parts, "", m.help.View(m.keymap)) } - return lipgloss.JoinVertical(lipgloss.Left, parts...) -} - -func clamp(x, low, high int) int { - if x < low { - return low - } - if x > high { - return high - } - return x + view := lipgloss.JoinVertical(lipgloss.Left, parts...) + return lipgloss.NewStyle(). + Padding(m.padding...). + Render(view) } diff --git a/choose/command.go b/choose/command.go index 8821258..70b8a9f 100644 --- a/choose/command.go +++ b/choose/command.go @@ -14,6 +14,7 @@ import ( "github.com/charmbracelet/gum/internal/stdin" "github.com/charmbracelet/gum/internal/timeout" "github.com/charmbracelet/gum/internal/tty" + "github.com/charmbracelet/gum/style" "github.com/charmbracelet/lipgloss" ) @@ -107,6 +108,7 @@ func (o Options) Run() error { // Use the pagination model to display the current and total number of // pages. + top, right, bottom, left := style.ParsePadding(o.Padding) pager := paginator.New() pager.SetTotalPages((len(items) + o.Height - 1) / o.Height) pager.PerPage = o.Height @@ -128,6 +130,7 @@ func (o Options) Run() error { index: startingIndex, currentOrder: currentOrder, height: o.Height, + padding: []int{top, right, bottom, left}, cursor: o.Cursor, header: o.Header, selectedPrefix: o.SelectedPrefix, diff --git a/choose/options.go b/choose/options.go index 4af1744..abfca22 100644 --- a/choose/options.go +++ b/choose/options.go @@ -26,6 +26,7 @@ type Options struct { OutputDelimiter string `help:"Option delimiter when writing to STDOUT" default:"\n" env:"GUM_CHOOSE_OUTPUT_DELIMITER"` LabelDelimiter string `help:"Allows to set a delimiter, so options can be set as label:value" default:"" env:"GUM_CHOOSE_LABEL_DELIMITER"` StripANSI bool `help:"Strip ANSI sequences when reading from STDIN" default:"true" negatable:"" env:"GUM_CHOOSE_STRIP_ANSI"` + Padding string `help:"Padding" default:"${defaultPadding}" group:"Style Flags" env:"GUM_CHOOSE_PADDING"` CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_CURSOR_"` HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=99" envprefix:"GUM_CHOOSE_HEADER_"` diff --git a/confirm/command.go b/confirm/command.go index e1f30bc..fc0a02b 100644 --- a/confirm/command.go +++ b/confirm/command.go @@ -10,6 +10,7 @@ import ( "github.com/charmbracelet/gum/internal/exit" "github.com/charmbracelet/gum/internal/stdin" "github.com/charmbracelet/gum/internal/timeout" + "github.com/charmbracelet/gum/style" ) // Run provides a shell script interface for prompting a user to confirm an @@ -28,6 +29,7 @@ func (o Options) Run() error { ctx, cancel := timeout.Context(o.Timeout) defer cancel() + top, right, bottom, left := style.ParsePadding(o.Padding) m := model{ affirmative: o.Affirmative, negative: o.Negative, @@ -41,6 +43,7 @@ func (o Options) Run() error { selectedStyle: o.SelectedStyle.ToLipgloss(), unselectedStyle: o.UnselectedStyle.ToLipgloss(), promptStyle: o.PromptStyle.ToLipgloss(), + padding: []int{top, right, bottom, left}, } tm, err := tea.NewProgram( m, diff --git a/confirm/confirm.go b/confirm/confirm.go index c32ade9..ac35c39 100644 --- a/confirm/confirm.go +++ b/confirm/confirm.go @@ -91,6 +91,7 @@ type model struct { promptStyle lipgloss.Style selectedStyle lipgloss.Style unselectedStyle lipgloss.Style + padding []int } func (m model) Init() tea.Cmd { return nil } @@ -149,18 +150,19 @@ func (m model) View() string { neg = "" } - if m.showHelp { - return lipgloss.JoinVertical( - lipgloss.Left, - m.promptStyle.Render(m.prompt)+"\n", - lipgloss.JoinHorizontal(lipgloss.Left, aff, neg), - "\n"+m.help.View(m.keys), - ) + parts := []string{ + m.promptStyle.Render(m.prompt) + "\n", + lipgloss.JoinHorizontal(lipgloss.Left, aff, neg), } - return lipgloss.JoinVertical( - lipgloss.Left, - m.promptStyle.Render(m.prompt)+"\n", - lipgloss.JoinHorizontal(lipgloss.Left, aff, neg), - ) + if m.showHelp { + parts = append(parts, "", m.help.View(m.keys)) + } + + return lipgloss.NewStyle(). + Padding(m.padding...). + Render(lipgloss.JoinVertical( + lipgloss.Left, + parts..., + )) } diff --git a/confirm/options.go b/confirm/options.go index b38d7e0..9740885 100644 --- a/confirm/options.go +++ b/confirm/options.go @@ -21,4 +21,5 @@ type Options struct { UnselectedStyle style.Styles `embed:"" prefix:"unselected." help:"The style of the unselected action" set:"defaultBackground=235" set:"defaultForeground=254" set:"defaultPadding=0 3" set:"defaultMargin=0 1" envprefix:"GUM_CONFIRM_UNSELECTED_"` ShowHelp bool `help:"Show help key binds" negatable:"" default:"true" env:"GUM_CONFIRM_SHOW_HELP"` Timeout time.Duration `help:"Timeout until confirm returns selected value or default if provided" default:"0s" env:"GUM_CONFIRM_TIMEOUT"` + Padding string `help:"Padding" default:"${defaultPadding}" group:"Style Flags" env:"GUM_CONFIRM_PADDING"` } diff --git a/file/command.go b/file/command.go index 09b53c1..b7cc546 100644 --- a/file/command.go +++ b/file/command.go @@ -10,6 +10,7 @@ import ( "github.com/charmbracelet/bubbles/help" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/gum/internal/timeout" + "github.com/charmbracelet/gum/style" ) // Run is the interface to picking a file. @@ -46,8 +47,10 @@ func (o Options) Run() error { fp.Styles.Permission = o.PermissionsStyle.ToLipgloss() fp.Styles.Selected = o.SelectedStyle.ToLipgloss() fp.Styles.FileSize = o.FileSizeStyle.ToLipgloss() + top, right, bottom, left := style.ParsePadding(o.Padding) m := model{ filepicker: fp, + padding: []int{top, right, bottom, left}, showHelp: o.ShowHelp, help: help.New(), keymap: defaultKeymap(), diff --git a/file/file.go b/file/file.go index f7b142b..33c8233 100644 --- a/file/file.go +++ b/file/file.go @@ -59,6 +59,7 @@ type model struct { selectedPath string quitting bool showHelp bool + padding []int help help.Model keymap keymap } @@ -68,9 +69,11 @@ 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) { case tea.WindowSizeMsg: + height := msg.Height - m.padding[0] - m.padding[2] if m.showHelp { - m.filepicker.Height -= lipgloss.Height(m.helpView()) //nolint:staticcheck + height -= lipgloss.Height(m.helpView()) } + m.filepicker.SetHeight(height) case tea.KeyMsg: switch { case key.Matches(msg, keyAbort): @@ -103,7 +106,12 @@ func (m model) View() string { if m.showHelp { parts = append(parts, m.helpView()) } - return lipgloss.JoinVertical(lipgloss.Left, parts...) + return lipgloss.NewStyle(). + Padding(m.padding...). + Render(lipgloss.JoinVertical( + lipgloss.Left, + parts..., + )) } func (m model) helpView() string { diff --git a/file/options.go b/file/options.go index 72bfb8f..61c9ced 100644 --- a/file/options.go +++ b/file/options.go @@ -30,4 +30,5 @@ type Options struct { 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_"` //nolint:staticcheck HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=99" envprefix:"GUM_FILE_HEADER_"` + Padding string `help:"Padding" default:"${defaultPadding}" group:"Style Flags" env:"GUM_FILE_PADDING"` } diff --git a/filter/command.go b/filter/command.go index 4185100..6c9b2a3 100644 --- a/filter/command.go +++ b/filter/command.go @@ -15,6 +15,7 @@ import ( "github.com/charmbracelet/gum/internal/stdin" "github.com/charmbracelet/gum/internal/timeout" "github.com/charmbracelet/gum/internal/tty" + "github.com/charmbracelet/gum/style" "github.com/charmbracelet/x/ansi" "github.com/sahilm/fuzzy" ) @@ -94,7 +95,7 @@ func (o Options) Run() error { km.ToggleAndNext.SetEnabled(true) km.ToggleAll.SetEnabled(true) } - + top, right, bottom, left := style.ParsePadding(o.Padding) m := model{ choices: choices, filteringChoices: filteringChoices, @@ -113,6 +114,7 @@ func (o Options) Run() error { textStyle: o.TextStyle.ToLipgloss(), cursorTextStyle: o.CursorTextStyle.ToLipgloss(), height: o.Height, + padding: []int{top, right, bottom, left}, selected: make(map[string]struct{}), limit: o.Limit, reverse: o.Reverse, diff --git a/filter/filter.go b/filter/filter.go index 176659a..5e433cd 100644 --- a/filter/filter.go +++ b/filter/filter.go @@ -19,6 +19,7 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/x/exp/ordered" "github.com/rivo/uniseg" "github.com/sahilm/fuzzy" ) @@ -137,6 +138,7 @@ type model struct { selectedPrefix string unselectedPrefix string height int + padding []int quitting bool headerStyle lipgloss.Style matchStyle lipgloss.Style @@ -230,33 +232,35 @@ func (m model) View() string { m.viewport.SetContent(s.String()) - help := "" - if m.showHelp { - help = m.helpView() - } - // View the input and the filtered choices header := m.headerStyle.Render(m.header) if m.reverse { - view := m.viewport.View() + "\n" + m.textinput.View() - if m.showHelp { - view += help - } + view := m.viewport.View() if m.header != "" { - return lipgloss.JoinVertical(lipgloss.Left, view, header) + view += "\n" + header } - - return view + view += "\n" + m.textinput.View() + if m.showHelp { + view += m.helpView() + } + return lipgloss.NewStyle(). + Padding(m.padding...). + Render(view) } view := m.textinput.View() + "\n" + m.viewport.View() if m.showHelp { - view += help + view += m.helpView() } if m.header != "" { - return lipgloss.JoinVertical(lipgloss.Left, header, view) + return lipgloss.NewStyle(). + Padding(m.padding...). + Render(header + "\n" + view) } - return view + + return lipgloss.NewStyle(). + Padding(m.padding...). + Render(view) } func (m model) helpView() string { @@ -279,10 +283,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.showHelp { m.viewport.Height = m.viewport.Height - lipgloss.Height(m.helpView()) } - m.viewport.Width = msg.Width - m.textinput.Width = msg.Width + m.viewport.Height = m.viewport.Height - m.padding[0] - m.padding[2] + m.viewport.Width = msg.Width - m.padding[1] - m.padding[3] + m.textinput.Width = msg.Width - m.padding[1] - m.padding[3] if m.reverse { - m.viewport.YOffset = clamp(0, len(m.matches), len(m.matches)-m.viewport.Height) + m.viewport.YOffset = ordered.Clamp(len(m.matches)-m.viewport.Height, 0, len(m.matches)) } case tea.KeyMsg: km := m.keymap @@ -374,7 +379,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // it remains at a constant position relative to the cursor. if m.reverse { maxYOffset := max(0, len(m.matches)-m.viewport.Height) - m.viewport.YOffset = clamp(0, maxYOffset, len(m.matches)-yOffsetFromBottom) + m.viewport.YOffset = ordered.Clamp(len(m.matches)-yOffsetFromBottom, 0, maxYOffset) } } } @@ -388,7 +393,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // It's possible that filtering items have caused fewer matches. So, ensure // that the selected index is within the bounds of the number of matches. - m.cursor = clamp(0, len(m.matches)-1, m.cursor) + m.cursor = ordered.Clamp(m.cursor, 0, len(m.matches)-1) return m, tea.Batch(cmd, icmd) } @@ -499,16 +504,6 @@ func exactMatches(search string, choices []string) []fuzzy.Match { return matches } -func clamp(low, high, val int) int { - if val < low { - return low - } - if val > high { - return high - } - return val -} - func matchedRanges(in []int) [][2]int { if len(in) == 0 { return [][2]int{} diff --git a/filter/options.go b/filter/options.go index e63644b..26eb3ea 100644 --- a/filter/options.go +++ b/filter/options.go @@ -41,6 +41,7 @@ type Options struct { InputDelimiter string `help:"Option delimiter when reading from STDIN" default:"\n" env:"GUM_FILTER_INPUT_DELIMITER"` OutputDelimiter string `help:"Option delimiter when writing to STDOUT" default:"\n" env:"GUM_FILTER_OUTPUT_DELIMITER"` StripANSI bool `help:"Strip ANSI sequences when reading from STDIN" default:"true" negatable:"" env:"GUM_FILTER_STRIP_ANSI"` + Padding string `help:"Padding" default:"${defaultPadding}" group:"Style Flags" env:"GUM_FILTER_PADDING"` // Deprecated: use [FuzzySort]. This will be removed at some point. Sort bool `help:"Sort fuzzy results by their scores" default:"true" env:"GUM_FILTER_FUZZY_SORT" negatable:"" hidden:""` diff --git a/go.mod b/go.mod index a7530c0..397fb65 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/charmbracelet/log v0.4.2 github.com/charmbracelet/x/ansi v0.10.1 github.com/charmbracelet/x/editor v0.1.0 + github.com/charmbracelet/x/exp/ordered v0.1.0 github.com/charmbracelet/x/term v0.2.1 github.com/charmbracelet/x/xpty v0.1.2 github.com/muesli/roff v0.1.0 diff --git a/go.sum b/go.sum index 70a52a1..d2db102 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,8 @@ github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9 github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE= +github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8= github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= diff --git a/input/command.go b/input/command.go index 7d96e72..0900d8d 100644 --- a/input/command.go +++ b/input/command.go @@ -11,6 +11,7 @@ import ( "github.com/charmbracelet/gum/cursor" "github.com/charmbracelet/gum/internal/stdin" "github.com/charmbracelet/gum/internal/timeout" + "github.com/charmbracelet/gum/style" ) // Run provides a shell script interface for the text input bubble. @@ -43,10 +44,12 @@ func (o Options) Run() error { i.EchoCharacter = '•' } + top, right, bottom, left := style.ParsePadding(o.Padding) m := model{ textinput: i, header: o.Header, headerStyle: o.HeaderStyle.ToLipgloss(), + padding: []int{top, right, bottom, left}, autoWidth: o.Width < 1, showHelp: o.ShowHelp, help: help.New(), diff --git a/input/input.go b/input/input.go index 505b052..f63ac6a 100644 --- a/input/input.go +++ b/input/input.go @@ -38,6 +38,7 @@ func (k keymap) ShortHelp() []key.Binding { type model struct { autoWidth bool header string + padding []int headerStyle lipgloss.Style textinput textinput.Model quitting bool @@ -53,27 +54,30 @@ func (m model) View() string { if m.quitting { return "" } + var parts []string if m.header != "" { - header := m.headerStyle.Render(m.header) - return lipgloss.JoinVertical(lipgloss.Left, header, m.textinput.View()) + parts = append(parts, m.headerStyle.Render(m.header)) } - if !m.showHelp { - return m.textinput.View() + parts = append(parts, m.textinput.View()) + if m.showHelp { + parts = append(parts, "", m.help.View(m.keymap)) } - return lipgloss.JoinVertical( - lipgloss.Top, - m.textinput.View(), - "", - m.help.View(m.keymap), - ) + return lipgloss.NewStyle(). + Padding(m.padding...). + Render(lipgloss.JoinVertical( + lipgloss.Top, + parts..., + )) } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: if m.autoWidth { - m.textinput.Width = msg.Width - lipgloss.Width(m.textinput.Prompt) - 1 + m.textinput.Width = msg.Width - 1 - + lipgloss.Width(m.textinput.Prompt) - + m.padding[1] - m.padding[3] } case tea.KeyMsg: switch msg.String() { diff --git a/input/options.go b/input/options.go index 7463adb..57cbf53 100644 --- a/input/options.go +++ b/input/options.go @@ -23,4 +23,5 @@ type Options struct { HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_INPUT_HEADER_"` Timeout time.Duration `help:"Timeout until input aborts" default:"0s" env:"GUM_INPUT_TIMEOUT"` StripANSI bool `help:"Strip ANSI sequences when reading from STDIN" default:"true" negatable:"" env:"GUM_INPUT_STRIP_ANSI"` + Padding string `help:"Padding" default:"${defaultPadding}" group:"Style Flags" env:"GUM_INPUT_PADDING"` } diff --git a/spin/command.go b/spin/command.go index 08cb3bd..cff2797 100644 --- a/spin/command.go +++ b/spin/command.go @@ -8,6 +8,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/gum/internal/exit" "github.com/charmbracelet/gum/internal/timeout" + "github.com/charmbracelet/gum/style" "github.com/charmbracelet/x/term" ) @@ -20,6 +21,7 @@ func (o Options) Run() error { s := spinner.New() s.Style = o.SpinnerStyle.ToLipgloss() s.Spinner = spinnerMap[o.Spinner] + top, right, bottom, left := style.ParsePadding(o.Padding) m := model{ spinner: s, title: o.TitleStyle.ToLipgloss().Render(o.Title), @@ -29,6 +31,7 @@ func (o Options) Run() error { showStderr: (o.ShowOutput || o.ShowStderr) && isErrTTY, showError: o.ShowError, isTTY: isErrTTY, + padding: []int{top, right, bottom, left}, } ctx, cancel := timeout.Context(o.Timeout) diff --git a/spin/options.go b/spin/options.go index 5243eab..702cc2a 100644 --- a/spin/options.go +++ b/spin/options.go @@ -20,4 +20,5 @@ type Options struct { 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:"0s" env:"GUM_SPIN_TIMEOUT"` + Padding string `help:"Padding" default:"${defaultPadding}" group:"Style Flags" env:"GUM_SPIN_PADDING"` } diff --git a/spin/spin.go b/spin/spin.go index ee7ea7d..6e41690 100644 --- a/spin/spin.go +++ b/spin/spin.go @@ -25,6 +25,7 @@ import ( "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/term" "github.com/charmbracelet/x/xpty" ) @@ -32,6 +33,7 @@ import ( type model struct { spinner spinner.Model title string + padding []int align string command []string quitting bool @@ -70,7 +72,7 @@ func commandStart(command []string) tea.Cmd { args = command[1:] } - executing = exec.Command(command[0], args...) //nolint:gosec + executing = exec.CommandContext(context.Background(), command[0], args...) //nolint:gosec executing.Stdin = os.Stdin isTerminal := term.IsTerminal(os.Stdout.Fd()) @@ -167,7 +169,9 @@ func (m model) View() string { } else { header = m.title + " " + m.spinner.View() } - return header + "\n" + out + return lipgloss.NewStyle(). + Padding(m.padding...). + Render(header, "", out) } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { diff --git a/style/lipgloss.go b/style/lipgloss.go index 9ba52a2..07ad0c8 100644 --- a/style/lipgloss.go +++ b/style/lipgloss.go @@ -19,7 +19,7 @@ func (s Styles) ToLipgloss() lipgloss.Style { Height(s.Height). Width(s.Width). Margin(parseMargin(s.Margin)). - Padding(parsePadding(s.Padding)). + Padding(ParsePadding(s.Padding)). Bold(s.Bold). Faint(s.Faint). Italic(s.Italic). @@ -40,7 +40,7 @@ func (s StylesNotHidden) ToLipgloss() lipgloss.Style { Height(s.Height). Width(s.Width). Margin(parseMargin(s.Margin)). - Padding(parsePadding(s.Padding)). + Padding(ParsePadding(s.Padding)). Bold(s.Bold). Faint(s.Faint). Italic(s.Italic). diff --git a/style/spacing.go b/style/spacing.go index d7f1238..57ea7db 100644 --- a/style/spacing.go +++ b/style/spacing.go @@ -11,9 +11,9 @@ const ( maxTokens = 4 ) -// parsePadding parses 1 - 4 integers from a string and returns them in a top, +// ParsePadding parses 1 - 4 integers from a string and returns them in a top, // right, bottom, left order for use in the lipgloss.Padding() method. -func parsePadding(s string) (int, int, int, int) { +func ParsePadding(s string) (int, int, int, int) { var ints [maxTokens]int tokens := strings.Split(s, " ") @@ -48,4 +48,4 @@ func parsePadding(s string) (int, int, int, int) { // parseMargin is an alias for parsePadding since they involve the same logic // to parse integers to the same format. -var parseMargin = parsePadding +var parseMargin = ParsePadding diff --git a/table/command.go b/table/command.go index 4ee0fb9..479cd93 100644 --- a/table/command.go +++ b/table/command.go @@ -79,6 +79,7 @@ func (o Options) Run() error { } defaultStyles := table.DefaultStyles() + top, right, bottom, left := style.ParsePadding(o.Padding) styles := table.Styles{ Cell: defaultStyles.Cell.Inherit(o.CellStyle.ToLipgloss()), @@ -133,7 +134,7 @@ func (o Options) Run() error { table.WithStyles(styles), } if o.Height > 0 { - opts = append(opts, table.WithHeight(o.Height)) + opts = append(opts, table.WithHeight(o.Height-top-bottom)) } table := table.New(opts...) @@ -147,6 +148,7 @@ func (o Options) Run() error { hideCount: o.HideCount, help: help.New(), keymap: defaultKeymap(), + padding: []int{top, right, bottom, left}, } tm, err := tea.NewProgram( m, diff --git a/table/options.go b/table/options.go index 58efaa4..d7a241f 100644 --- a/table/options.go +++ b/table/options.go @@ -26,4 +26,5 @@ type Options struct { 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"` + Padding string `help:"Padding" default:"${defaultPadding}" group:"Style Flags" env:"GUM_TABLE_PADDING"` } diff --git a/table/table.go b/table/table.go index 9c6ed72..c0d389f 100644 --- a/table/table.go +++ b/table/table.go @@ -22,6 +22,7 @@ import ( "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) type keymap struct { @@ -72,6 +73,7 @@ type model struct { hideCount bool help help.Model keymap keymap + padding []int } func (m model) Init() tea.Cmd { return nil } @@ -122,7 +124,9 @@ func (m model) View() string { if m.showHelp { s += "\n" + m.countView() + m.help.View(m.keymap) } - return s + return lipgloss.NewStyle(). + Padding(m.padding...). + Render(s) } func numLen(i int) int { diff --git a/write/command.go b/write/command.go index adc3c69..6a745fb 100644 --- a/write/command.go +++ b/write/command.go @@ -12,6 +12,7 @@ import ( "github.com/charmbracelet/gum/cursor" "github.com/charmbracelet/gum/internal/stdin" "github.com/charmbracelet/gum/internal/timeout" + "github.com/charmbracelet/gum/style" ) // Run provides a shell script interface for the text area bubble. @@ -30,6 +31,7 @@ func (o Options) Run() error { a.ShowLineNumbers = o.ShowLineNumbers a.CharLimit = o.CharLimit a.MaxHeight = o.MaxLines + top, right, bottom, left := style.ParsePadding(o.Padding) style := textarea.Style{ Base: o.BaseStyle.ToLipgloss(), @@ -46,8 +48,8 @@ func (o Options) Run() error { a.Cursor.Style = o.CursorStyle.ToLipgloss() a.Cursor.SetMode(cursor.Modes[o.CursorMode]) - a.SetWidth(o.Width) - a.SetHeight(o.Height) + a.SetWidth(max(0, o.Width-left-right)) + a.SetHeight(max(0, o.Height-top-bottom)) a.SetValue(o.Value) m := model{ @@ -58,6 +60,7 @@ func (o Options) Run() error { help: help.New(), showHelp: o.ShowHelp, keymap: defaultKeymap(), + padding: []int{top, right, bottom, left}, } m.textarea.KeyMap.InsertNewline = m.keymap.InsertNewline diff --git a/write/options.go b/write/options.go index 16653eb..63c7b0c 100644 --- a/write/options.go +++ b/write/options.go @@ -32,4 +32,5 @@ type Options struct { HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_WRITE_HEADER_"` PlaceholderStyle style.Styles `embed:"" prefix:"placeholder." set:"defaultForeground=240" envprefix:"GUM_WRITE_PLACEHOLDER_"` PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=7" envprefix:"GUM_WRITE_PROMPT_"` + Padding string `help:"Padding" default:"${defaultPadding}" group:"Style Flags" env:"GUM_WRITE_PADDING"` } diff --git a/write/write.go b/write/write.go index 323dc91..614ecaf 100644 --- a/write/write.go +++ b/write/write.go @@ -77,6 +77,7 @@ type model struct { showHelp bool help help.Model keymap keymap + padding []int } func (m model) Init() tea.Cmd { return textarea.Blink } @@ -96,14 +97,19 @@ func (m model) View() string { if m.showHelp { parts = append(parts, "", m.help.View(m.keymap)) } - return lipgloss.JoinVertical(lipgloss.Left, parts...) + return lipgloss.NewStyle(). + Padding(m.padding...). + Render(lipgloss.JoinVertical( + lipgloss.Left, + parts..., + )) } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: if m.autoWidth { - m.textarea.SetWidth(msg.Width) + m.textarea.SetWidth(msg.Width - m.padding[1] - m.padding[3]) } case tea.FocusMsg, tea.BlurMsg: var cmd tea.Cmd