feat(file): gum file to pick files

This commit is contained in:
Maas Lalani 2022-09-30 18:40:10 -04:00
parent 430ab459d7
commit 2bea4dc030
10 changed files with 408 additions and 14 deletions

58
file/command.go Normal file
View file

@ -0,0 +1,58 @@
package file
import (
"fmt"
"os"
"path/filepath"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/stack"
)
// Run is the interface to picking a file.
func (o Options) Run() error {
if o.Path == "" {
o.Path = "."
}
path, err := filepath.Abs(o.Path)
if err != nil {
return err
}
m := model{
path: path,
cursor: o.Cursor,
selected: 0,
showHidden: o.All,
autoHeight: o.Height == 0,
height: o.Height,
max: 0,
min: 0,
selectedStack: stack.NewStack(),
minStack: stack.NewStack(),
maxStack: stack.NewStack(),
cursorStyle: o.CursorStyle.ToLipgloss(),
symlinkStyle: o.SymlinkStyle.ToLipgloss(),
directoryStyle: o.DirectoryStyle.ToLipgloss(),
fileStyle: o.FileStyle.ToLipgloss(),
permissionStyle: o.PermissionsStyle.ToLipgloss(),
selectedStyle: o.SelectedStyle.ToLipgloss(),
fileSizeStyle: o.FileSizeStyle.ToLipgloss(),
}
tm, err := tea.NewProgram(&m, tea.WithOutput(os.Stderr)).StartReturningModel()
if err != nil {
return err
}
m = tm.(model)
if m.path == "" {
os.Exit(1)
}
fmt.Println(m.path)
return nil
}

226
file/file.go Normal file
View file

@ -0,0 +1,226 @@
// Package file provides an interface to pick a file from a folder (tree).
// The user is provided a file manager-like interface to navigate, to
// select a file.
//
// Let's pick a file from the current directory:
//
// $ gum file
// $ gum file .
//
// Let's pick a file from the home directory:
//
// $ gum file $HOME
package file
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/stack"
"github.com/charmbracelet/lipgloss"
"github.com/dustin/go-humanize"
)
const marginBottom = 5
type model struct {
quitting bool
path string
files []os.DirEntry
showHidden bool
selected int
selectedStack stack.Stack
min int
max int
maxStack stack.Stack
minStack stack.Stack
height int
autoHeight bool
cursor string
cursorStyle lipgloss.Style
symlinkStyle lipgloss.Style
directoryStyle lipgloss.Style
fileStyle lipgloss.Style
permissionStyle lipgloss.Style
selectedStyle lipgloss.Style
fileSizeStyle lipgloss.Style
}
type readDirMsg []os.DirEntry
func readDir(path string, showHidden bool) tea.Cmd {
return func() tea.Msg {
dirEntries, err := os.ReadDir(path)
if err != nil {
return tea.Quit
}
sort.Slice(dirEntries, func(i, j int) bool {
if dirEntries[i].IsDir() == dirEntries[j].IsDir() {
return dirEntries[i].Name() < dirEntries[j].Name()
}
return dirEntries[i].IsDir()
})
if showHidden {
return readDirMsg(dirEntries)
}
var sanitizedDirEntries []fs.DirEntry
for _, dirEntry := range dirEntries {
isHidden, _ := IsHidden(dirEntry.Name())
if isHidden {
continue
}
sanitizedDirEntries = append(sanitizedDirEntries, dirEntry)
}
return readDirMsg(sanitizedDirEntries)
}
}
func (m model) Init() tea.Cmd {
return readDir(m.path, m.showHidden)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case readDirMsg:
m.files = msg
case tea.WindowSizeMsg:
if m.autoHeight {
m.height = msg.Height - marginBottom
}
m.max = m.height
case tea.KeyMsg:
switch msg.String() {
case "g":
m.selected = 0
m.min = 0
m.max = m.height - 1
case "G":
m.selected = len(m.files) - 1
m.min = len(m.files) - m.height
m.max = len(m.files) - 1
case "j", "down":
m.selected++
if m.selected >= len(m.files) {
m.selected = len(m.files) - 1
}
if m.selected > m.max {
m.min++
m.max++
}
case "k", "up":
m.selected--
if m.selected < 0 {
m.selected = 0
}
if m.selected < m.min {
m.min--
m.max--
}
case "ctrl+c", "q":
m.path = ""
m.quitting = true
return m, tea.Quit
case "backspace", "h", "left":
m.path = filepath.Dir(m.path)
if m.selectedStack.Length() > 0 {
m.selected, m.min, m.max = m.popView()
} else {
m.selected = 0
m.min = 0
m.max = m.height - 1
}
return m, readDir(m.path, m.showHidden)
case "l", "right", "enter":
if len(m.files) == 0 {
break
}
if !m.files[m.selected].IsDir() {
if msg.String() == "enter" {
m.path = filepath.Join(m.path, m.files[m.selected].Name())
m.quitting = true
return m, tea.Quit
}
break
}
m.path = filepath.Join(m.path, m.files[m.selected].Name())
m.pushView()
m.selected = 0
m.min = 0
m.max = m.height - 1
return m, readDir(m.path, m.showHidden)
}
}
return m, nil
}
func (m model) pushView() {
m.minStack.Push(m.min)
m.maxStack.Push(m.max)
m.selectedStack.Push(m.selected)
}
func (m model) popView() (int, int, int) {
return m.selectedStack.Pop(), m.minStack.Pop(), m.maxStack.Pop()
}
func (m model) View() string {
if m.quitting {
return ""
}
if len(m.files) == 0 {
return "Bummer. No files found."
}
var s strings.Builder
for i, f := range m.files {
if i < m.min {
continue
}
if i > m.max {
break
}
var symlinkPath string
info, _ := f.Info()
isSymlink := info.Mode()&fs.ModeSymlink != 0
size := humanize.Bytes(uint64(info.Size()))
name := f.Name()
if isSymlink {
symlinkPath, _ = filepath.EvalSymlinks(filepath.Join(m.path, name))
}
if m.selected == i {
selected := fmt.Sprintf(" %s %"+fmt.Sprint(m.fileSizeStyle.GetWidth())+"s %s", info.Mode().String(), size, name)
if isSymlink {
selected = fmt.Sprintf("%s → %s", selected, symlinkPath)
}
s.WriteString(m.cursorStyle.Render(m.cursor) + m.selectedStyle.Render(selected))
} else {
var style = m.fileStyle
if f.IsDir() {
style = m.directoryStyle
} else if isSymlink {
style = m.symlinkStyle
}
fileName := style.Render(name)
if isSymlink {
fileName = fmt.Sprintf("%s → %s", fileName, symlinkPath)
}
s.WriteString(fmt.Sprintf(" %s %s %s", m.permissionStyle.Render(info.Mode().String()), m.fileSizeStyle.Render(size), fileName))
}
s.WriteString("\n")
}
return s.String()
}

10
file/hidden_unix.go Normal file
View file

@ -0,0 +1,10 @@
//go:build !windows
package file
import "strings"
// IsHidden reports whether a file is hidden or not
func IsHidden(file string) (bool, error) {
return strings.HasPrefix(file, "."), nil
}

21
file/options.go Normal file
View file

@ -0,0 +1,21 @@
package file
import "github.com/charmbracelet/gum/style"
// Options are the options for the file command.
type Options struct {
// Path is the path to the folder / directory to begin traversing.
Path string `arg:"" optional:"" name:"path" help:"The path to the folder to begin traversing"`
// Cursor is the character to display in front of the current selected items.
Cursor string `short:"c" help:"The cursor character" default:">"`
All bool `short:"a" help:"Show hidden and 'dot' files" default:"true"`
Height int `help:"Maximum number of files to display" default:"0"`
CursorStyle style.Styles `embed:"" prefix:"cursor." help:"The cursor style" set:"defaultForeground=212" envprefix:"GUM_FILE_CURSOR_"`
SymlinkStyle style.Styles `embed:"" prefix:"symlink." help:"The style to use for symlinks" set:"defaultForeground=36" envprefix:"GUM_FILE_SYMLINK_"`
DirectoryStyle style.Styles `embed:"" prefix:"directory." help:"The style to use for directories" set:"defaultForeground=99" envprefix:"GUM_FILE_DIRECTORY_"`
FileStyle style.Styles `embed:"" prefix:"file." help:"The style to use for files" envprefix:"GUM_FILE_FILE_"`
PermissionsStyle style.Styles `embed:"" prefix:"permissions." help:"The style to use for permissions" set:"defaultForeground=244" envprefix:"GUM_FILE_PERMISSIONS_"`
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_"`
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_"`
}

3
go.mod
View file

@ -8,7 +8,8 @@ require (
github.com/charmbracelet/bubbles v0.14.1-0.20221006154229-d1775121146a
github.com/charmbracelet/bubbletea v0.22.2-0.20221007181357-2696b2f3399f
github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da
github.com/charmbracelet/lipgloss v0.6.0
github.com/charmbracelet/lipgloss v0.6.1-0.20220930064401-ae7c84f7b158
github.com/dustin/go-humanize v1.0.0
github.com/mattn/go-runewidth v0.0.14
github.com/muesli/roff v0.1.0
github.com/muesli/termenv v0.13.0

26
go.sum
View file

@ -13,16 +13,25 @@ github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZs
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/charmbracelet/bubbles v0.14.1-0.20221006154229-d1775121146a h1:/prXWlDbR4CWT1FaTvkU8WhLfpZv3eOrN9PtL8oDdDU=
github.com/charmbracelet/bubbles v0.14.1-0.20221006154229-d1775121146a/go.mod h1:5rZgJTHmgWISQnxnzzIJtQt3GC1bfJfNmr4SEtRDtTQ=
github.com/charmbracelet/bubbles v0.14.0 h1:DJfCwnARfWjZLvMglhSQzo76UZ2gucuHPy9jLWX45Og=
github.com/charmbracelet/bubbles v0.14.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=
github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4=
github.com/charmbracelet/bubbletea v0.22.1 h1:z66q0LWdJNOWEH9zadiAIXp2GN1AWrwNXU8obVY9X24=
github.com/charmbracelet/bubbletea v0.22.1/go.mod h1:8/7hVvbPN6ZZPkczLiB8YpLkLJ0n7DMho5Wvfd2X1C0=
<<<<<<< HEAD
github.com/charmbracelet/bubbletea v0.22.2-0.20221007181357-2696b2f3399f h1:mbSd0Sdm2wXUWtXJ81o86G9V+9IhddX0qQcGK6bMbKo=
github.com/charmbracelet/bubbletea v0.22.2-0.20221007181357-2696b2f3399f/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=
||||||| parent of c5917c0 (feat(file): gum file to pick files)
github.com/charmbracelet/bubbletea v0.22.2-0.20221006105051-f406999cba69 h1:GpZktjqyEQjuvFtFb0UlMlbZCOwOhk/bpKb6+quLz+E=
github.com/charmbracelet/bubbletea v0.22.2-0.20221006105051-f406999cba69/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=
=======
>>>>>>> c5917c0 (feat(file): gum file to pick files)
github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da h1:FGz53GWQRiKQ/5xUsoCCkewSQIC7u81Scaxx2nUy3nM=
github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da/go.mod h1:HXz79SMFnF9arKxqeoHWxmo1BhplAH7wehlRhKQIL94=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY=
github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
github.com/charmbracelet/lipgloss v0.6.1-0.20220930064401-ae7c84f7b158 h1:uuY3ti70rfEiLw3rHKSRiJ+cWfq4KWScgjxhoVRf0eE=
github.com/charmbracelet/lipgloss v0.6.1-0.20220930064401-ae7c84f7b158/go.mod h1:5EY1dcRQX7kPSA5ssoYjq2qEDhpS4cdtmdYY1OlAdMs=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -30,6 +39,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
@ -51,6 +62,7 @@ github.com/microcosm-cc/bluemonday v1.0.19 h1:OI7hoF5FY4pFz2VA//RN8TfM0YJ2dJcl4P
github.com/microcosm-cc/bluemonday v1.0.19/go.mod h1:QNzV2UbLK2/53oIIwTOyLUSABMkjZ4tqiyC1g/DyqxE=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/mango v0.1.1-0.20220205060214-77e2058169ab h1:m7QFONkzLK0fVXCjwX5tANcnj1yXxTnYQtnfJiY3tcA=
@ -76,9 +88,12 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs=
github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
@ -92,6 +107,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

28
gum.go
View file

@ -6,6 +6,7 @@ import (
"github.com/charmbracelet/gum/choose"
"github.com/charmbracelet/gum/completion"
"github.com/charmbracelet/gum/confirm"
"github.com/charmbracelet/gum/file"
"github.com/charmbracelet/gum/filter"
"github.com/charmbracelet/gum/format"
"github.com/charmbracelet/gum/input"
@ -56,6 +57,20 @@ type Gum struct {
//
Confirm confirm.Options `cmd:"" help:"Ask a user to confirm an action"`
// File provides an interface to pick a file from a folder (tree).
// The user is provided a file manager-like interface to navigate, to
// select a file.
//
// Let's pick a file from the current directory:
//
// $ gum file
// $ gum file .
//
// Let's pick a file from the home directory:
//
// $ gum file $HOME
File file.Options `cmd:"" help:"Pick a file from a folder"`
// Filter provides a fuzzy searching text input to allow filtering a list of
// options to select one option.
//
@ -106,6 +121,19 @@ type Gum struct {
// https://github.com/charmbracelet/bubbles/tree/master/viewport
//
// It allows the user to scroll through content like a pager.
//
// ╭────────────────────────────────────────────────╮
// │ 1 │ Gum Pager │
// │ 2 │ ========= │
// │ 3 │ │
// │ 4 │ ``` │
// │ 5 │ gum pager --height 10 --width 25 < text │
// │ 6 │ ``` │
// │ 7 │ │
// │ 8 │ │
// ╰────────────────────────────────────────────────╯
// ↑/↓: Navigate • q: Quit
//
Pager pager.Options `cmd:"" help:"Scroll through a file"`
// Spin provides a shell script interface for the spinner bubble.

26
internal/stack/stack.go Normal file
View file

@ -0,0 +1,26 @@
package stack
// Stack is a stack interface for integers.
type Stack struct {
Push func(int)
Pop func() int
Length func() int
}
// NewStack returns a new stack of integers
func NewStack() Stack {
slice := make([]int, 0)
return Stack{
Push: func(i int) {
slice = append(slice, i)
},
Pop: func() int {
res := slice[len(slice)-1]
slice = slice[:len(slice)-1]
return res
},
Length: func() int {
return len(slice)
},
}
}

View file

@ -53,13 +53,21 @@ func main() {
}),
kong.Vars{
"version": version,
"defaultHeight": "0",
"defaultWidth": "0",
"defaultAlign": "left",
"defaultBorder": "none",
"defaultBorderForeground": "",
"defaultBorderBackground": "",
"defaultBackground": "",
"defaultForeground": "",
"defaultMargin": "0 0",
"defaultPadding": "0 0",
"defaultUnderline": "false",
"defaultBold": "false",
"defaultFaint": "false",
"defaultItalic": "false",
"defaultStrikethrough": "false",
},
)
if err := ctx.Run(); err != nil {

View file

@ -23,20 +23,20 @@ type Styles struct {
// Border
Border string `help:"Border Style" enum:"none,hidden,normal,rounded,thick,double" default:"${defaultBorder}" group:"Style Flags" env:"BORDER"`
BorderBackground string `help:"Border Background Color" group:"Style Flags" env:"BORDER_BACKGROUND"`
BorderBackground string `help:"Border Background Color" group:"Style Flags" default:"${defaultBorderBackground}" env:"BORDER_BACKGROUND"`
BorderForeground string `help:"Border Foreground Color" group:"Style Flags" default:"${defaultBorderForeground}" env:"BORDER_FOREGROUND"`
// Layout
Align string `help:"Text Alignment" enum:"left,center,right,bottom,middle,top" default:"left" group:"Style Flags" env:"ALIGN"`
Height int `help:"Text height" group:"Style Flags" env:"HEIGHT"`
Width int `help:"Text width" group:"Style Flags" env:"WIDTH"`
Align string `help:"Text Alignment" enum:"left,center,right,bottom,middle,top" default:"${defaultAlign}" group:"Style Flags" env:"ALIGN"`
Height int `help:"Text height" default:"${defaultHeight}" group:"Style Flags" env:"HEIGHT"`
Width int `help:"Text width" default:"${defaultWidth}" group:"Style Flags" env:"WIDTH"`
Margin string `help:"Text margin" default:"${defaultMargin}" group:"Style Flags" env:"MARGIN"`
Padding string `help:"Text padding" default:"${defaultPadding}" group:"Style Flags" env:"PADDING"`
// Format
Bold bool `help:"Bold text" group:"Style Flags" env:"BOLD"`
Faint bool `help:"Faint text" group:"Style Flags" env:"FAINT"`
Italic bool `help:"Italicize text" group:"Style Flags" env:"ITALIC"`
Strikethrough bool `help:"Strikethrough text" group:"Style Flags" env:"STRIKETHROUGH"`
Bold bool `help:"Bold text" default:"${defaultBold}" group:"Style Flags" env:"BOLD"`
Faint bool `help:"Faint text" default:"${defaultFaint}" group:"Style Flags" env:"FAINT"`
Italic bool `help:"Italicize text" default:"${defaultItalic}" group:"Style Flags" env:"ITALIC"`
Strikethrough bool `help:"Strikethrough text" default:"${defaultStrikethrough}" group:"Style Flags" env:"STRIKETHROUGH"`
Underline bool `help:"Underline text" default:"${defaultUnderline}" group:"Style Flags" env:"UNDERLINE"`
}