Showing preview only (243K chars total). Download the full file or copy to clipboard to get everything.
Repository: charmbracelet/gum
Branch: main
Commit: 4409974788c6
Files: 115
Total size: 220.4 KB
Directory structure:
gitextract_697t_p9p/
├── .github/
│ ├── CODEOWNERS
│ ├── dependabot.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── build.yml
│ ├── dependabot-sync.yml
│ ├── goreleaser.yml
│ ├── lint-sync.yml
│ ├── lint.yml
│ └── nightly.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── Dockerfile
├── LICENSE
├── README.md
├── choose/
│ ├── choose.go
│ ├── command.go
│ └── options.go
├── completion/
│ ├── bash.go
│ ├── command.go
│ ├── fish.go
│ └── zsh.go
├── confirm/
│ ├── command.go
│ ├── confirm.go
│ └── options.go
├── cursor/
│ └── cursor.go
├── default.nix
├── examples/
│ ├── .gitignore
│ ├── README.md
│ ├── choose.tape
│ ├── commit.sh
│ ├── commit.tape
│ ├── confirm.tape
│ ├── convert-to-gif.sh
│ ├── customize.tape
│ ├── demo.sh
│ ├── demo.tape
│ ├── diyfetch
│ ├── fav.txt
│ ├── file.tape
│ ├── filter-key-value.sh
│ ├── flavors.txt
│ ├── format.ansi
│ ├── git-branch-manager.sh
│ ├── git-stage.sh
│ ├── gum.js
│ ├── gum.py
│ ├── gum.rb
│ ├── input.tape
│ ├── kaomoji.sh
│ ├── magic.sh
│ ├── pager.tape
│ ├── posix.sh
│ ├── skate.sh
│ ├── spin.tape
│ ├── story.txt
│ ├── test.sh
│ └── write.tape
├── file/
│ ├── command.go
│ ├── file.go
│ └── options.go
├── filter/
│ ├── command.go
│ ├── filter.go
│ ├── filter_test.go
│ └── options.go
├── flake.nix
├── format/
│ ├── README.md
│ ├── command.go
│ ├── formats.go
│ └── options.go
├── go.mod
├── go.sum
├── gum.go
├── input/
│ ├── command.go
│ ├── input.go
│ └── options.go
├── internal/
│ ├── decode/
│ │ └── align.go
│ ├── exit/
│ │ └── exit.go
│ ├── files/
│ │ └── files.go
│ ├── stdin/
│ │ └── stdin.go
│ ├── timeout/
│ │ └── context.go
│ └── tty/
│ └── tty.go
├── join/
│ ├── command.go
│ └── options.go
├── log/
│ ├── command.go
│ └── options.go
├── main.go
├── man/
│ └── command.go
├── pager/
│ ├── command.go
│ ├── options.go
│ ├── pager.go
│ └── search.go
├── spin/
│ ├── command.go
│ ├── options.go
│ ├── pty.go
│ ├── spin.go
│ └── spinners.go
├── style/
│ ├── ascii_a.txt
│ ├── borders.go
│ ├── command.go
│ ├── lipgloss.go
│ ├── options.go
│ └── spacing.go
├── table/
│ ├── bom.csv
│ ├── comma.csv
│ ├── command.go
│ ├── example.csv
│ ├── invalid.csv
│ ├── options.go
│ └── table.go
├── version/
│ ├── command.go
│ └── options.go
└── write/
├── command.go
├── options.go
└── write.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/CODEOWNERS
================================================
* @charmbracelet/everyone
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"
ignore:
- dependency-name: github.com/charmbracelet/bubbletea/v2
versions:
- v2.0.0-beta1
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"
================================================
FILE: .github/pull_request_template.md
================================================
Fixes #...
### Changes
-
-
-
================================================
FILE: .github/workflows/build.yml
================================================
name: build
on:
push:
branches:
- main
pull_request:
jobs:
build:
uses: charmbracelet/meta/.github/workflows/build.yml@main
secrets:
gh_pat: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
================================================
FILE: .github/workflows/dependabot-sync.yml
================================================
name: dependabot-sync
on:
schedule:
- cron: "0 0 * * 0" # every Sunday at midnight
workflow_dispatch: # allows manual triggering
permissions:
contents: write
pull-requests: write
jobs:
dependabot-sync:
uses: charmbracelet/meta/.github/workflows/dependabot-sync.yml@main
with:
repo_name: ${{ github.event.repository.name }}
secrets:
gh_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
================================================
FILE: .github/workflows/goreleaser.yml
================================================
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: goreleaser
on:
push:
tags:
- v*.*.*
concurrency:
group: goreleaser
cancel-in-progress: true
jobs:
goreleaser:
uses: charmbracelet/meta/.github/workflows/goreleaser.yml@main
secrets:
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
gh_pat: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
goreleaser_key: ${{ secrets.GORELEASER_KEY }}
fury_token: ${{ secrets.FURY_TOKEN }}
nfpm_gpg_key: ${{ secrets.NFPM_GPG_KEY }}
nfpm_passphrase: ${{ secrets.NFPM_PASSPHRASE }}
macos_sign_p12: ${{ secrets.MACOS_SIGN_P12 }}
macos_sign_password: ${{ secrets.MACOS_SIGN_PASSWORD }}
macos_notary_issuer_id: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
macos_notary_key_id: ${{ secrets.MACOS_NOTARY_KEY_ID }}
macos_notary_key: ${{ secrets.MACOS_NOTARY_KEY }}
================================================
FILE: .github/workflows/lint-sync.yml
================================================
name: lint-sync
on:
schedule:
# every Sunday at midnight
- cron: "0 0 * * 0"
workflow_dispatch: # allows manual triggering
permissions:
contents: write
pull-requests: write
jobs:
lint:
uses: charmbracelet/meta/.github/workflows/lint-sync.yml@main
================================================
FILE: .github/workflows/lint.yml
================================================
name: lint
on:
push:
pull_request:
jobs:
lint:
uses: charmbracelet/meta/.github/workflows/lint.yml@main
================================================
FILE: .github/workflows/nightly.yml
================================================
name: nightly
on:
push:
branches:
- main
jobs:
nightly:
uses: charmbracelet/meta/.github/workflows/nightly.yml@main
secrets:
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
goreleaser_key: ${{ secrets.GORELEASER_KEY }}
macos_sign_p12: ${{ secrets.MACOS_SIGN_P12 }}
macos_sign_password: ${{ secrets.MACOS_SIGN_PASSWORD }}
macos_notary_issuer_id: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
macos_notary_key_id: ${{ secrets.MACOS_NOTARY_KEY_ID }}
macos_notary_key: ${{ secrets.MACOS_NOTARY_KEY }}
================================================
FILE: .gitignore
================================================
# Files
test
.DS_Store
# Binaries
gum
dist
testdata
# Folders
completions/
manpages/
# nix
result
================================================
FILE: .golangci.yml
================================================
version: "2"
run:
tests: false
linters:
enable:
- bodyclose
- exhaustive
- goconst
- godot
- gomoddirectives
- goprintffuncname
- gosec
- misspell
- nakedret
- nestif
- nilerr
- noctx
- nolintlint
- prealloc
- revive
- rowserrcheck
- sqlclosecheck
- tparallel
- unconvert
- unparam
- whitespace
- wrapcheck
exclusions:
rules:
- text: '(slog|log)\.\w+'
linters:
- noctx
generated: lax
presets:
- common-false-positives
settings:
exhaustive:
default-signifies-exhaustive: true
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofumpt
- goimports
exclusions:
generated: lax
================================================
FILE: .goreleaser.yml
================================================
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
version: 2
includes:
- from_url:
url: charmbracelet/meta/main/goreleaser-full.yaml
variables:
main: "."
scoop_name: charm-gum
description: "A tool for glamorous shell scripts"
github_url: "https://github.com/charmbracelet/gum"
maintainer: "Maas Lalani <maas@charm.sh>"
brew_commit_author_name: "Maas Lalani"
brew_commit_author_email: "maas@charm.sh"
milestones:
- close: true
================================================
FILE: Dockerfile
================================================
FROM gcr.io/distroless/static
COPY gum /usr/local/bin/gum
ENTRYPOINT [ "/usr/local/bin/gum" ]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2022-2024 Charmbracelet, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Gum
<p>
<a href="https://stuff.charm.sh/gum/nutritional-information.png" target="_blank"><img src="https://stuff.charm.sh/gum/gum.png" alt="Gum Image" width="450" /></a>
<br><br>
<a href="https://github.com/charmbracelet/gum/releases"><img src="https://img.shields.io/github/release/charmbracelet/gum.svg" alt="Latest Release"></a>
<a href="https://pkg.go.dev/github.com/charmbracelet/gum?tab=doc"><img src="https://godoc.org/github.com/golang/gddo?status.svg" alt="Go Docs"></a>
<a href="https://github.com/charmbracelet/gum/actions"><img src="https://github.com/charmbracelet/gum/workflows/build/badge.svg" alt="Build Status"></a>
</p>
A tool for glamorous shell scripts. Leverage the power of
[Bubbles](https://github.com/charmbracelet/bubbles) and [Lip
Gloss](https://github.com/charmbracelet/lipgloss) in your scripts and aliases
without writing any Go code!
<img alt="Shell running the ./demo.sh script" width="600" src="https://vhs.charm.sh/vhs-1qY57RrQlXCuydsEgDp68G.gif">
The above example is running from a single shell script ([source](./examples/demo.sh)).
## Tutorial
Gum provides highly configurable, ready-to-use utilities to help you write
useful shell scripts and dotfile aliases with just a few lines of code.
Let's build a simple script to help you write
[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary)
for your dotfiles.
Ask for the commit type with gum choose:
```bash
gum choose "fix" "feat" "docs" "style" "refactor" "test" "chore" "revert"
```
> [!NOTE]
> This command itself will print to stdout which is not all that useful. To make use of the command later on you can save the stdout to a `$VARIABLE` or `file.txt`.
Prompt for the scope of these changes:
```bash
gum input --placeholder "scope"
```
Prompt for the summary and description of changes:
```bash
gum input --value "$TYPE$SCOPE: " --placeholder "Summary of this change"
gum write --placeholder "Details of this change"
```
Confirm before committing:
```bash
gum confirm "Commit changes?" && git commit -m "$SUMMARY" -m "$DESCRIPTION"
```
Check out the [complete example](https://github.com/charmbracelet/gum/blob/main/examples/commit.sh) for combining these commands in a single script.
<img alt="Running the ./examples/commit.sh script to commit to git" width="600" src="https://vhs.charm.sh/vhs-7rRq3LsEuJVwhwr0xf6Er7.gif">
## Installation
Use a package manager:
```bash
# macOS or Linux
brew install gum
# Arch Linux (btw)
pacman -S gum
# Fedora or EPEL 10
dnf install gum
# Nix
nix-env -iA nixpkgs.gum
# Flox
flox install gum
# Windows (via WinGet or Scoop)
winget install charmbracelet.gum
scoop install charm-gum
```
<details>
<summary>Debian/Ubuntu</summary>
```bash
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg
echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list
sudo apt update && sudo apt install gum
```
</details>
<details>
<summary>Fedora/RHEL/OpenSuse</summary>
```bash
echo '[charm]
name=Charm
baseurl=https://repo.charm.sh/yum/
enabled=1
gpgcheck=1
gpgkey=https://repo.charm.sh/yum/gpg.key' | sudo tee /etc/yum.repos.d/charm.repo
sudo rpm --import https://repo.charm.sh/yum/gpg.key
# yum
sudo yum install gum
# zypper
sudo zypper refresh
sudo zypper install gum
```
</details>
<details>
<summary>FreeBSD</summary>
```bash
# packages
sudo pkg install gum
# ports
cd /usr/ports/devel/gum && sudo make install clean
```
</details>
Or download it:
- [Packages][releases] are available in Debian, RPM, and Alpine formats
- [Binaries][releases] are available for Linux, macOS, Windows, FreeBSD, OpenBSD, and NetBSD
Or just install it with `go`:
```bash
go install github.com/charmbracelet/gum@latest
```
[releases]: https://github.com/charmbracelet/gum/releases
## Commands
- [`choose`](#choose): Choose an option from a list of choices
- [`confirm`](#confirm): Ask a user to confirm an action
- [`file`](#file): Pick a file from a folder
- [`filter`](#filter): Filter items from a list
- [`format`](#format): Format a string using a template
- [`input`](#input): Prompt for some input
- [`join`](#join): Join text vertically or horizontally
- [`pager`](#pager): Scroll through a file
- [`spin`](#spin): Display spinner while running a command
- [`style`](#style): Apply coloring, borders, spacing to text
- [`table`](#table): Render a table of data
- [`write`](#write): Prompt for long-form text
- [`log`](#log): Log messages to output
## Customization
You can customize `gum` options and styles with `--flags` and `$ENVIRONMENT_VARIABLES`.
See `gum <command> --help` for a full view of each command's customization and configuration options.
Customize with `--flags`:
```bash
gum input --cursor.foreground "#FF0" \
--prompt.foreground "#0FF" \
--placeholder "What's up?" \
--prompt "* " \
--width 80 \
--value "Not much, hby?"
```
Customize with `ENVIRONMENT_VARIABLES`:
```bash
export GUM_INPUT_CURSOR_FOREGROUND="#FF0"
export GUM_INPUT_PROMPT_FOREGROUND="#0FF"
export GUM_INPUT_PLACEHOLDER="What's up?"
export GUM_INPUT_PROMPT="* "
export GUM_INPUT_WIDTH=80
# --flags can override values set with environment
gum input
```
<img alt="Gum input displaying most customization options" width="600" src="https://vhs.charm.sh/vhs-5zb9DlQYA70aL9ZpYLTwKv.gif">
## Input
Prompt for input with a simple command.
```bash
gum input > answer.txt
gum input --password > password.txt
```
<img src="https://vhs.charm.sh/vhs-1nScrStFI3BMlCp5yrLtyg.gif" width="600" alt="Shell running gum input typing Not much, you?" />
## Write
Prompt for some multi-line text (`ctrl+d` to complete text entry).
```bash
gum write > story.txt
```
<img src="https://vhs.charm.sh/vhs-7abdKKrUEukgx9aJj8O5GX.gif" width="600" alt="Shell running gum write typing a story" />
## Filter
Filter a list of values with fuzzy matching:
```bash
echo Strawberry >> flavors.txt
echo Banana >> flavors.txt
echo Cherry >> flavors.txt
gum filter < flavors.txt > selection.txt
```
<img src="https://vhs.charm.sh/vhs-61euOQtKPtQVD7nDpHQhzr.gif" width="600" alt="Shell running gum filter on different bubble gum flavors" />
Select multiple options with the `--limit` flag or `--no-limit` flag. Use `tab` or `ctrl+space` to select, `enter` to confirm.
```bash
cat flavors.txt | gum filter --limit 2
cat flavors.txt | gum filter --no-limit
```
## Choose
Choose an option from a list of choices.
```bash
echo "Pick a card, any card..."
CARD=$(gum choose --height 15 {{A,K,Q,J},{10..2}}" "{♠,♥,♣,♦})
echo "Was your card the $CARD?"
```
You can also select multiple items with the `--limit` or `--no-limit` flag, which determines
the maximum of items that can be chosen.
```bash
cat songs.txt | gum choose --limit 5
cat foods.txt | gum choose --no-limit --header "Grocery Shopping"
```
<img src="https://vhs.charm.sh/vhs-3zV1LvofA6Cbn5vBu1NHHl.gif" width="600" alt="Shell running gum choose with numbers and gum flavors" />
## Confirm
Confirm whether to perform an action. Exits with code `0` (affirmative) or `1`
(negative) depending on selection.
```bash
gum confirm && rm file.txt || echo "File not removed"
```
<img src="https://vhs.charm.sh/vhs-3xRFvbeQ4lqGerbHY7y3q2.gif" width="600" alt="Shell running gum confirm" />
## File
Prompt the user to select a file from the file tree.
```bash
$EDITOR $(gum file $HOME)
```
<img src="https://vhs.charm.sh/vhs-2RMRqmnOPneneIgVJJ3mI1.gif" width="600" alt="Shell running gum file" />
## Pager
Scroll through a long document with line numbers and a fully customizable viewport.
```bash
gum pager < README.md
```
<img src="https://vhs.charm.sh/vhs-3iMDpgOLmbYr0jrYEGbk7p.gif" width="600" alt="Shell running gum pager" />
## Spin
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
```
<img src="https://vhs.charm.sh/vhs-3YFswCmoY4o3Q7MyzWl6sS.gif" width="600" alt="Shell running gum spin while sleeping for 5 seconds" />
Available spinner types include: `line`, `dot`, `minidot`, `jump`, `pulse`, `points`, `globe`, `moon`, `monkey`, `meter`, `hamburger`.
## Table
Select a row from some tabular data.
```bash
gum table < flavors.csv | cut -d ',' -f 1
```
<!-- <img src="https://stuff.charm.sh/gum/table.gif" width="600" alt="Shell running gum table" /> -->
## Style
Pretty print any string with any layout with one command.
```bash
gum style \
--foreground 212 --border-foreground 212 --border double \
--align center --width 50 --margin "1 2" --padding "2 4" \
'Bubble Gum (1¢)' 'So sweet and so fresh!'
```
<img src="https://github.com/charmbracelet/gum/assets/42545625/67468acf-b3e0-4e78-bd89-360739eb44fa" width="600" alt="Bubble Gum, So sweet and so fresh!" />
## Join
Combine text vertically or horizontally. Use this command with `gum style` to
build layouts and pretty output.
Tip: Always wrap the output of `gum style` in quotes to preserve newlines
(`\n`) when using it as an argument in the `join` command.
```bash
I=$(gum style --padding "1 5" --border double --border-foreground 212 "I")
LOVE=$(gum style --padding "1 4" --border double --border-foreground 57 "LOVE")
BUBBLE=$(gum style --padding "1 8" --border double --border-foreground 255 "Bubble")
GUM=$(gum style --padding "1 5" --border double --border-foreground 240 "Gum")
I_LOVE=$(gum join "$I" "$LOVE")
BUBBLE_GUM=$(gum join "$BUBBLE" "$GUM")
gum join --align center --vertical "$I_LOVE" "$BUBBLE_GUM"
```
<img src="https://github.com/charmbracelet/gum/assets/42545625/68f7a25d-b495-48dd-982a-cee0c8ea5786" width="600" alt="I LOVE Bubble Gum written out in four boxes with double borders around them." />
## Format
`format` processes and formats bodies of text. `gum format` can parse markdown,
template strings, and named emojis.
```bash
# Format some markdown
gum format -- "# Gum Formats" "- Markdown" "- Code" "- Template" "- Emoji"
echo "# Gum Formats\n- Markdown\n- Code\n- Template\n- Emoji" | gum format
# Syntax highlight some code
cat main.go | gum format -t code
# Render text any way you want with templates
echo '{{ Bold "Tasty" }} {{ Italic "Bubble" }} {{ Color "99" "0" " Gum " }}' \
| gum format -t template
# Display your favorite emojis!
echo 'I :heart: Bubble Gum :candy:' | gum format -t emoji
```
For more information on template helpers, see the [Termenv
docs](https://github.com/muesli/termenv#template-helpers). For a full list of
named emojis see the [GitHub API](https://api.github.com/emojis).
<img src="https://github.com/charmbracelet/gum/assets/42545625/5cfbb0c8-0022-460d-841b-fec37527ca66" width="300" alt="Running gum format for different types of formats" />
## Log
`log` logs messages to the terminal at using different levels and styling using
the [`charmbracelet/log`](https://github.com/charmbracelet/log) library.
```bash
# Log some debug information.
gum log --structured --level debug "Creating file..." name file.txt
# DEBUG Unable to create file. name=temp.txt
# Log some error.
gum log --structured --level error "Unable to create file." name file.txt
# ERROR Unable to create file. name=temp.txt
# Include a timestamp.
gum log --time rfc822 --level error "Unable to create file."
```
See the Go [`time` package](https://pkg.go.dev/time#pkg-constants) for acceptable `--time` formats.
See [`charmbracelet/log`](https://github.com/charmbracelet/log) for more usage.
<img src="https://vhs.charm.sh/vhs-6jupuFM0s2fXiUrBE0I1vU.gif" width="600" alt="Running gum log with debug and error levels" />
## Examples
How to use `gum` in your daily workflows:
See the [examples](./examples/) directory for more real world use cases.
- Write a commit message:
```bash
git commit -m "$(gum input --width 50 --placeholder "Summary of changes")" \
-m "$(gum write --width 80 --placeholder "Details of changes")"
```
- Open files in your `$EDITOR`
```bash
$EDITOR $(gum filter)
```
- Connect to a `tmux` session
```bash
SESSION=$(tmux list-sessions -F \#S | gum filter --placeholder "Pick session...")
tmux switch-client -t "$SESSION" || tmux attach -t "$SESSION"
```
- Pick a commit hash from `git` history
```bash
git log --oneline | gum filter | cut -d' ' -f1 # | copy
```
- Simple [`skate`](https://github.com/charmbracelet/skate) password selector.
```
skate list -k | gum filter | xargs skate get
```
- Uninstall packages
```bash
brew list | gum choose --no-limit | xargs brew uninstall
```
- Clean up `git` branches
```bash
git branch | cut -c 3- | gum choose --no-limit | xargs git branch -D
```
- Checkout GitHub pull requests with [`gh`](https://cli.github.com/)
```bash
gh pr list | cut -f1,2 | gum choose | cut -f1 | xargs gh pr checkout
```
- Copy command from shell history
```bash
gum filter < $HISTFILE --height 20
```
- `sudo` replacement
```bash
alias please="gum input --password | sudo -nS"
```
## Contributing
See [contributing][contribute].
[contribute]: https://github.com/charmbracelet/gum/contribute
## Feedback
We’d love to hear your thoughts on this project. Feel free to drop us a note!
- [Twitter](https://twitter.com/charmcli)
- [The Fediverse](https://mastodon.social/@charmcli)
- [Discord](https://charm.sh/chat)
## License
[MIT](https://github.com/charmbracelet/gum/raw/main/LICENSE)
---
Part of [Charm](https://charm.sh).
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400" /></a>
Charm热爱开源 • Charm loves open source
================================================
FILE: choose/choose.go
================================================
// Package choose provides an interface to choose one option from a given list
// of options. The options can be provided as (new-line separated) stdin or a
// list of arguments.
//
// It is different from the filter command as it does not provide a fuzzy
// finding input, so it is best used for smaller lists of options.
//
// Let's pick from a list of gum flavors:
//
// $ gum choose "Strawberry" "Banana" "Cherry"
package choose
import (
"strings"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/paginator"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/exp/ordered"
)
func defaultKeymap() keymap {
return keymap{
Down: key.NewBinding(
key.WithKeys("down", "j", "ctrl+j", "ctrl+n"),
),
Up: key.NewBinding(
key.WithKeys("up", "k", "ctrl+k", "ctrl+p"),
),
Right: key.NewBinding(
key.WithKeys("right", "l", "ctrl+f"),
),
Left: key.NewBinding(
key.WithKeys("left", "h", "ctrl+b"),
),
Home: key.NewBinding(
key.WithKeys("g", "home"),
),
End: key.NewBinding(
key.WithKeys("G", "end"),
),
ToggleAll: key.NewBinding(
key.WithKeys("a", "A", "ctrl+a"),
key.WithHelp("ctrl+a", "select all"),
key.WithDisabled(),
),
Toggle: key.NewBinding(
key.WithKeys(" ", "tab", "x", "ctrl+@"),
key.WithHelp("x", "toggle"),
key.WithDisabled(),
),
Abort: key.NewBinding(
key.WithKeys("ctrl+c"),
key.WithHelp("ctrl+c", "abort"),
),
Quit: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "quit"),
),
Submit: key.NewBinding(
key.WithKeys("enter", "ctrl+q"),
key.WithHelp("enter", "submit"),
),
}
}
type keymap struct {
Down,
Up,
Right,
Left,
Home,
End,
ToggleAll,
Toggle,
Abort,
Quit,
Submit key.Binding
}
// FullHelp implements help.KeyMap.
func (k keymap) FullHelp() [][]key.Binding { return nil }
// ShortHelp implements help.KeyMap.
func (k keymap) ShortHelp() []key.Binding {
return []key.Binding{
k.Toggle,
key.NewBinding(
key.WithKeys("up", "down", "right", "left"),
key.WithHelp("←↓↑→", "navigate"),
),
k.Submit,
k.ToggleAll,
}
}
type model struct {
height int
padding []int
cursor string
selectedPrefix string
unselectedPrefix string
cursorPrefix string
header string
items []item
quitting bool
submitted bool
index int
limit int
numSelected int
currentOrder int
paginator paginator.Model
showHelp bool
help help.Model
keymap keymap
// styles
cursorStyle lipgloss.Style
headerStyle lipgloss.Style
itemStyle lipgloss.Style
selectedItemStyle lipgloss.Style
}
type item struct {
text string
selected bool
order int
}
func (m model) Init() tea.Cmd { return nil }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
return m, nil
case tea.KeyMsg:
start, end := m.paginator.GetSliceBounds(len(m.items))
km := m.keymap
switch {
case key.Matches(msg, km.Down):
m.index++
if m.index >= len(m.items) {
m.index = 0
m.paginator.Page = 0
}
if m.index >= end {
m.paginator.NextPage()
}
case key.Matches(msg, km.Up):
m.index--
if m.index < 0 {
m.index = len(m.items) - 1
m.paginator.Page = m.paginator.TotalPages - 1
}
if m.index < start {
m.paginator.PrevPage()
}
case key.Matches(msg, km.Right):
m.index = ordered.Clamp(m.index+m.height, 0, len(m.items)-1)
m.paginator.NextPage()
case key.Matches(msg, km.Left):
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
m.paginator.Page = m.paginator.TotalPages - 1
case key.Matches(msg, km.Home):
m.index = 0
m.paginator.Page = 0
case key.Matches(msg, km.ToggleAll):
if m.limit <= 1 {
break
}
if m.numSelected < len(m.items) && m.numSelected < m.limit {
m = m.selectAll()
} else {
m = m.deselectAll()
}
case key.Matches(msg, km.Quit):
m.quitting = true
return m, tea.Quit
case key.Matches(msg, km.Abort):
m.quitting = true
return m, tea.Interrupt
case key.Matches(msg, km.Toggle):
if m.limit == 1 {
break // no op
}
if m.items[m.index].selected {
m.items[m.index].selected = false
m.numSelected--
} else if m.numSelected < m.limit {
m.items[m.index].selected = true
m.items[m.index].order = m.currentOrder
m.numSelected++
m.currentOrder++
}
case key.Matches(msg, km.Submit):
m.quitting = true
if m.limit <= 1 && m.numSelected < 1 {
m.items[m.index].selected = true
}
m.submitted = true
return m, tea.Quit
}
}
var cmd tea.Cmd
m.paginator, cmd = m.paginator.Update(msg)
return m, cmd
}
func (m model) selectAll() model {
for i := range m.items {
if m.numSelected >= m.limit {
break // do not exceed given limit
}
if m.items[i].selected {
continue
}
m.items[i].selected = true
m.items[i].order = m.currentOrder
m.numSelected++
m.currentOrder++
}
return m
}
func (m model) deselectAll() model {
for i := range m.items {
m.items[i].selected = false
m.items[i].order = 0
}
m.numSelected = 0
m.currentOrder = 0
return m
}
func (m model) View() string {
if m.quitting {
return ""
}
var s strings.Builder
start, end := m.paginator.GetSliceBounds(len(m.items))
for i, item := range m.items[start:end] {
if i == m.index%m.height {
s.WriteString(m.cursorStyle.Render(m.cursor))
} else {
s.WriteString(strings.Repeat(" ", lipgloss.Width(m.cursor)))
}
if item.selected {
s.WriteString(m.selectedItemStyle.Render(m.selectedPrefix + item.text))
} else if i == m.index%m.height {
s.WriteString(m.cursorStyle.Render(m.cursorPrefix + item.text))
} else {
s.WriteString(m.itemStyle.Render(m.unselectedPrefix + item.text))
}
if i != m.height {
s.WriteRune('\n')
}
}
if m.paginator.TotalPages > 1 {
s.WriteString(strings.Repeat("\n", m.height-m.paginator.ItemsOnPage(len(m.items))+1))
s.WriteString(" " + m.paginator.View())
}
var parts []string
if m.header != "" {
parts = append(parts, m.headerStyle.Render(m.header))
}
parts = append(parts, s.String())
if m.showHelp {
parts = append(parts, "", m.help.View(m.keymap))
}
view := lipgloss.JoinVertical(lipgloss.Left, parts...)
return lipgloss.NewStyle().
Padding(m.padding...).
Render(view)
}
================================================
FILE: choose/command.go
================================================
package choose
import (
"errors"
"fmt"
"os"
"slices"
"sort"
"strings"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/paginator"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/internal/timeout"
"github.com/charmbracelet/gum/internal/tty"
"github.com/charmbracelet/gum/style"
"github.com/charmbracelet/lipgloss"
)
// Run provides a shell script interface for choosing between different through
// options.
func (o Options) Run() error {
var (
subduedStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#847A85", Dark: "#979797"})
verySubduedStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"})
)
input, _ := stdin.Read(stdin.StripANSI(o.StripANSI))
if len(o.Options) > 0 && len(o.Selected) == 0 {
o.Selected = strings.Split(input, o.InputDelimiter)
} else if len(o.Options) == 0 {
if input == "" {
return errors.New("no options provided, see `gum choose --help`")
}
o.Options = strings.Split(input, o.InputDelimiter)
}
// normalize options into a map
options := map[string]string{}
// keep the labels in the user-provided order
var labels []string //nolint:prealloc
for _, opt := range o.Options {
if o.LabelDelimiter == "" {
options[opt] = opt
continue
}
label, value, ok := strings.Cut(opt, o.LabelDelimiter)
if !ok {
return fmt.Errorf("invalid option format: %q", opt)
}
labels = append(labels, label)
options[label] = value
}
if o.LabelDelimiter != "" {
o.Options = labels
}
if o.SelectIfOne && len(o.Options) == 1 {
fmt.Println(options[o.Options[0]])
return nil
}
// We don't need to display prefixes if we are only picking one option.
// Simply displaying the cursor is enough.
if o.Limit == 1 && !o.NoLimit {
o.SelectedPrefix = ""
o.UnselectedPrefix = ""
o.CursorPrefix = ""
}
if o.NoLimit {
o.Limit = len(o.Options) + 1
}
if o.Ordered {
slices.SortFunc(o.Options, strings.Compare)
}
isSelectAll := len(o.Selected) == 1 && o.Selected[0] == "*"
// Keep track of the selected items.
currentSelected := 0
// Check if selected items should be used.
hasSelectedItems := len(o.Selected) > 0
startingIndex := 0
currentOrder := 0
items := make([]item, len(o.Options))
for i, option := range o.Options {
var order int
// Check if the option should be selected.
isSelected := hasSelectedItems && currentSelected < o.Limit && (isSelectAll || slices.Contains(o.Selected, option))
// If the option is selected then increment the current selected count.
if isSelected {
if o.Limit == 1 {
// When the user can choose only one option don't select the option but
// start with the cursor hovering over it.
startingIndex = i
isSelected = false
} else {
currentSelected++
order = currentOrder
currentOrder++
}
}
items[i] = item{text: option, selected: isSelected, order: order}
}
// 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
pager.Type = paginator.Dots
pager.ActiveDot = subduedStyle.Render("•")
pager.InactiveDot = verySubduedStyle.Render("•")
pager.KeyMap = paginator.KeyMap{}
pager.Page = startingIndex / o.Height
km := defaultKeymap()
if o.NoLimit || o.Limit > 1 {
km.Toggle.SetEnabled(true)
}
if o.NoLimit {
km.ToggleAll.SetEnabled(true)
}
m := model{
index: startingIndex,
currentOrder: currentOrder,
height: o.Height,
padding: []int{top, right, bottom, left},
cursor: o.Cursor,
header: o.Header,
selectedPrefix: o.SelectedPrefix,
unselectedPrefix: o.UnselectedPrefix,
cursorPrefix: o.CursorPrefix,
items: items,
limit: o.Limit,
paginator: pager,
cursorStyle: o.CursorStyle.ToLipgloss(),
headerStyle: o.HeaderStyle.ToLipgloss(),
itemStyle: o.ItemStyle.ToLipgloss(),
selectedItemStyle: o.SelectedItemStyle.ToLipgloss(),
numSelected: currentSelected,
showHelp: o.ShowHelp,
help: help.New(),
keymap: km,
}
ctx, cancel := timeout.Context(o.Timeout)
defer cancel()
// Disable Keybindings since we will control it ourselves.
tm, err := tea.NewProgram(
m,
tea.WithOutput(os.Stderr),
tea.WithContext(ctx),
).Run()
if err != nil {
return fmt.Errorf("unable to pick selection: %w", err)
}
m = tm.(model)
if !m.submitted {
return errors.New("nothing selected")
}
if o.Ordered && o.Limit > 1 {
sort.Slice(m.items, func(i, j int) bool {
return m.items[i].order < m.items[j].order
})
}
var out []string
for _, item := range m.items {
if item.selected {
out = append(out, options[item.text])
}
}
tty.Println(strings.Join(out, o.OutputDelimiter))
return nil
}
================================================
FILE: choose/options.go
================================================
package choose
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"`
ShowHelp bool `help:"Show help keybinds" default:"true" negatable:"" env:"GUM_CHOOSE_SHOW_HELP"`
Timeout time.Duration `help:"Timeout until choose returns selected element" default:"0s" env:"GUM_CHOOSE_TIMEOUT"` // including timeout command options [Timeout,...]
Header string `help:"Header value" default:"Choose:" 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 (selects all if given *)" default:"" env:"GUM_CHOOSE_SELECTED"`
SelectIfOne bool `help:"Select the given option if there is only one" group:"Selection"`
InputDelimiter string `help:"Option delimiter when reading from STDIN" default:"\n" env:"GUM_CHOOSE_INPUT_DELIMITER"`
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_"`
ItemStyle style.Styles `embed:"" prefix:"item." hidden:"" envprefix:"GUM_CHOOSE_ITEM_"`
SelectedItemStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_SELECTED_"`
}
================================================
FILE: completion/bash.go
================================================
// Package completion provides a bash completion generator for Kong
// applications.
package completion
import (
"bytes"
"fmt"
"io"
"sort"
"strings"
"github.com/alecthomas/kong"
)
// Bash is a bash completion generator.
type Bash struct{}
// Run generates bash completion script.
func (b Bash) Run(ctx *kong.Context) error {
buf := new(bytes.Buffer)
writePreamble(buf, ctx.Model.Name)
b.gen(buf, ctx.Model.Node)
writePostscript(buf, ctx.Model.Name)
_, err := fmt.Fprint(ctx.Stdout, buf.String())
if err != nil {
return fmt.Errorf("unable to generate bash completion: %v", err)
}
return nil
}
// ShellCompDirective is a bit map representing the different behaviors the shell
// can be instructed to have once completions have been provided.
type ShellCompDirective int
const (
// ShellCompDirectiveError indicates an error occurred and completions should be ignored.
ShellCompDirectiveError ShellCompDirective = 1 << iota
// ShellCompDirectiveNoSpace indicates that the shell should not add a space
// after the completion even if there is a single completion provided.
ShellCompDirectiveNoSpace
// ShellCompDirectiveNoFileComp indicates that the shell should not provide
// file completion even when no completion is provided.
ShellCompDirectiveNoFileComp
// ShellCompDirectiveFilterFileExt indicates that the provided completions
// should be used as file extension filters.
// For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename()
// is a shortcut to using this directive explicitly. The BashCompFilenameExt
// annotation can also be used to obtain the same behavior for flags.
ShellCompDirectiveFilterFileExt
// ShellCompDirectiveFilterDirs indicates that only directory names should
// be provided in file completion. To request directory names within another
// directory, the returned completions should specify the directory within
// which to search. The BashCompSubdirsInDir annotation can be used to
// obtain the same behavior but only for flags.
ShellCompDirectiveFilterDirs
// ===========================================================================
//
// All directives using iota should be above this one.
// For internal use.
shellCompDirectiveMaxValue //nolint:deadcode,unused,varcheck
// ShellCompDirectiveDefault indicates to let the shell perform its default
// behavior after completions have been provided.
// This one must be last to avoid messing up the iota count.
ShellCompDirectiveDefault ShellCompDirective = 0
)
// Annotations for Bash completion.
const (
// ShellCompNoDescRequestCmd is the name of the hidden command that is used to request
// completion results without their description. It is used by the shell completion scripts.
ShellCompNoDescRequestCmd = "completion completeNoDesc"
BashCompFilenameExt = "kong_annotation_bash_completion_filename_extensions"
BashCompCustom = "kong_annotation_bash_completion_custom"
BashCompOneRequiredFlag = "kong_annotation_bash_completion_one_required_flag"
BashCompSubdirsInDir = "kong_annotation_bash_completion_subdirs_in_dir"
activeHelpEnvVarSuffix = "_ACTIVE_HELP"
)
// activeHelpEnvVar returns the name of the program-specific ActiveHelp environment
// variable. It has the format <PROGRAM>_ACTIVE_HELP where <PROGRAM> is the name of the
// root command in upper case, with all - replaced by _.
func activeHelpEnvVar(name string) string {
// This format should not be changed: users will be using it explicitly.
activeHelpEnvVar := strings.ToUpper(fmt.Sprintf("%s%s", name, activeHelpEnvVarSuffix))
return strings.ReplaceAll(activeHelpEnvVar, "-", "_")
}
func writePreamble(buf io.StringWriter, name string) {
writeString(buf, fmt.Sprintf("# bash completion for %-36s -*- shell-script -*-\n", name))
writeString(buf, fmt.Sprintf(`
__%[1]s_debug()
{
if [[ -n ${BASH_COMP_DEBUG_FILE:-} ]]; then
echo "$*" >> "${BASH_COMP_DEBUG_FILE}"
fi
}
# Homebrew on Macs have version 1.3 of bash-completion which doesn't include
# _init_completion. This is a very minimal version of that function.
__%[1]s_init_completion()
{
COMPREPLY=()
_get_comp_words_by_ref "$@" cur prev words cword
}
__%[1]s_index_of_word()
{
local w word=$1
shift
index=0
for w in "$@"; do
[[ $w = "$word" ]] && return
index=$((index+1))
done
index=-1
}
__%[1]s_contains_word()
{
local w word=$1; shift
for w in "$@"; do
[[ $w = "$word" ]] && return
done
return 1
}
__%[1]s_handle_go_custom_completion()
{
__%[1]s_debug "${FUNCNAME[0]}: cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}"
local shellCompDirectiveError=%[3]d
local shellCompDirectiveNoSpace=%[4]d
local shellCompDirectiveNoFileComp=%[5]d
local shellCompDirectiveFilterFileExt=%[6]d
local shellCompDirectiveFilterDirs=%[7]d
local out requestComp lastParam lastChar comp directive args
# Prepare the command to request completions for the program.
# Calling ${words[0]} instead of directly %[1]s allows to handle aliases
args=("${words[@]:1}")
# Disable ActiveHelp which is not supported for bash completion v1
requestComp="%[8]s=0 ${words[0]} %[2]s ${args[*]}"
lastParam=${words[$((${#words[@]}-1))]}
lastChar=${lastParam:$((${#lastParam}-1)):1}
__%[1]s_debug "${FUNCNAME[0]}: lastParam ${lastParam}, lastChar ${lastChar}"
if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then
# If the last parameter is complete (there is a space following it)
# We add an extra empty parameter so we can indicate this to the go method.
__%[1]s_debug "${FUNCNAME[0]}: Adding extra empty parameter"
requestComp="${requestComp} \"\""
fi
__%[1]s_debug "${FUNCNAME[0]}: calling ${requestComp}"
# Use eval to handle any environment variables and such
out=$(eval "${requestComp}" 2>/dev/null)
# Extract the directive integer at the very end of the output following a colon (:)
directive=${out##*:}
# Remove the directive
out=${out%%:*}
if [ "${directive}" = "${out}" ]; then
# There is not directive specified
directive=0
fi
__%[1]s_debug "${FUNCNAME[0]}: the completion directive is: ${directive}"
__%[1]s_debug "${FUNCNAME[0]}: the completions are: ${out}"
if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then
# Error code. No completion.
__%[1]s_debug "${FUNCNAME[0]}: received error from custom completion go code"
return
else
if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then
if [[ $(type -t compopt) = "builtin" ]]; then
__%[1]s_debug "${FUNCNAME[0]}: activating no space"
compopt -o nospace
fi
fi
if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then
if [[ $(type -t compopt) = "builtin" ]]; then
__%[1]s_debug "${FUNCNAME[0]}: activating no file completion"
compopt +o default
fi
fi
fi
if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
# File extension filtering
local fullFilter filter filteringCmd
# Do not use quotes around the $out variable or else newline
# characters will be kept.
for filter in ${out}; do
fullFilter+="$filter|"
done
filteringCmd="_filedir $fullFilter"
__%[1]s_debug "File filtering command: $filteringCmd"
$filteringCmd
elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then
# File completion for directories only
local subdir
# Use printf to strip any trailing newline
subdir=$(printf "%%s" "${out}")
if [ -n "$subdir" ]; then
__%[1]s_debug "Listing directories in $subdir"
__%[1]s_handle_subdirs_in_dir_flag "$subdir"
else
__%[1]s_debug "Listing directories in ."
_filedir -d
fi
else
while IFS='' read -r comp; do
COMPREPLY+=("$comp")
done < <(compgen -W "${out}" -- "$cur")
fi
}
__%[1]s_handle_reply()
{
__%[1]s_debug "${FUNCNAME[0]}"
local comp
case $cur in
-*)
if [[ $(type -t compopt) = "builtin" ]]; then
compopt -o nospace
fi
local allflags
if [ ${#must_have_one_flag[@]} -ne 0 ]; then
allflags=("${must_have_one_flag[@]}")
else
allflags=("${flags[*]} ${two_word_flags[*]}")
fi
while IFS='' read -r comp; do
COMPREPLY+=("$comp")
done < <(compgen -W "${allflags[*]}" -- "$cur")
if [[ $(type -t compopt) = "builtin" ]]; then
[[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace
fi
# complete after --flag=abc
if [[ $cur == *=* ]]; then
if [[ $(type -t compopt) = "builtin" ]]; then
compopt +o nospace
fi
local index flag
flag="${cur%%=*}"
__%[1]s_index_of_word "${flag}" "${flags_with_completion[@]}"
COMPREPLY=()
if [[ ${index} -ge 0 ]]; then
PREFIX=""
cur="${cur#*=}"
${flags_completion[${index}]}
if [ -n "${ZSH_VERSION:-}" ]; then
# zsh completion needs --flag= prefix
eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )"
fi
fi
fi
if [[ -z "${flag_parsing_disabled}" ]]; then
# If flag parsing is enabled, we have completed the flags and can return.
# If flag parsing is disabled, we may not know all (or any) of the flags, so we fallthrough
# to possibly call handle_go_custom_completion.
return 0;
fi
;;
esac
# check if we are handling a flag with special work handling
local index
__%[1]s_index_of_word "${prev}" "${flags_with_completion[@]}"
if [[ ${index} -ge 0 ]]; then
${flags_completion[${index}]}
return
fi
# we are parsing a flag and don't have a special handler, no completion
if [[ ${cur} != "${words[cword]}" ]]; then
return
fi
local completions
completions=("${commands[@]}")
if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then
completions+=("${must_have_one_noun[@]}")
elif [[ -n "${has_completion_function}" ]]; then
# if a go completion function is provided, defer to that function
__%[1]s_handle_go_custom_completion
fi
if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then
completions+=("${must_have_one_flag[@]}")
fi
while IFS='' read -r comp; do
COMPREPLY+=("$comp")
done < <(compgen -W "${completions[*]}" -- "$cur")
if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then
while IFS='' read -r comp; do
COMPREPLY+=("$comp")
done < <(compgen -W "${noun_aliases[*]}" -- "$cur")
fi
if [[ ${#COMPREPLY[@]} -eq 0 ]]; then
if declare -F __%[1]s_custom_func >/dev/null; then
# try command name qualified custom func
__%[1]s_custom_func
else
# otherwise fall back to unqualified for compatibility
declare -F __custom_func >/dev/null && __custom_func
fi
fi
# available in bash-completion >= 2, not always present on macOS
if declare -F __ltrim_colon_completions >/dev/null; then
__ltrim_colon_completions "$cur"
fi
# If there is only 1 completion and it is a flag with an = it will be completed
# but we don't want a space after the =
if [[ "${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "${COMPREPLY[0]}" == --*= ]]; then
compopt -o nospace
fi
}
# The arguments should be in the form "ext1|ext2|extn"
__%[1]s_handle_filename_extension_flag()
{
local ext="$1"
_filedir "@(${ext})"
}
__%[1]s_handle_subdirs_in_dir_flag()
{
local dir="$1"
pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return
}
__%[1]s_handle_flag()
{
__%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
# if a command required a flag, and we found it, unset must_have_one_flag()
local flagname=${words[c]}
local flagvalue=""
# if the word contained an =
if [[ ${words[c]} == *"="* ]]; then
flagvalue=${flagname#*=} # take in as flagvalue after the =
flagname=${flagname%%=*} # strip everything after the =
flagname="${flagname}=" # but put the = back
fi
__%[1]s_debug "${FUNCNAME[0]}: looking for ${flagname}"
if __%[1]s_contains_word "${flagname}" "${must_have_one_flag[@]}"; then
must_have_one_flag=()
fi
# if you set a flag which only applies to this command, don't show subcommands
if __%[1]s_contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then
commands=()
fi
# keep flag value with flagname as flaghash
# flaghash variable is an associative array which is only supported in bash > 3.
if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then
if [ -n "${flagvalue}" ] ; then
flaghash[${flagname}]=${flagvalue}
elif [ -n "${words[ $((c+1)) ]}" ] ; then
flaghash[${flagname}]=${words[ $((c+1)) ]}
else
flaghash[${flagname}]="true" # pad "true" for bool flag
fi
fi
# skip the argument to a two word flag
if [[ ${words[c]} != *"="* ]] && __%[1]s_contains_word "${words[c]}" "${two_word_flags[@]}"; then
__%[1]s_debug "${FUNCNAME[0]}: found a flag ${words[c]}, skip the next argument"
c=$((c+1))
# if we are looking for a flags value, don't show commands
if [[ $c -eq $cword ]]; then
commands=()
fi
fi
c=$((c+1))
}
__%[1]s_handle_noun()
{
__%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
if __%[1]s_contains_word "${words[c]}" "${must_have_one_noun[@]}"; then
must_have_one_noun=()
elif __%[1]s_contains_word "${words[c]}" "${noun_aliases[@]}"; then
must_have_one_noun=()
fi
nouns+=("${words[c]}")
c=$((c+1))
}
__%[1]s_handle_command()
{
__%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
local next_command
if [[ -n ${last_command} ]]; then
next_command="_${last_command}_${words[c]//:/__}"
else
if [[ $c -eq 0 ]]; then
next_command="_%[1]s_root_command"
else
next_command="_${words[c]//:/__}"
fi
fi
c=$((c+1))
__%[1]s_debug "${FUNCNAME[0]}: looking for ${next_command}"
declare -F "$next_command" >/dev/null && $next_command
}
__%[1]s_handle_word()
{
if [[ $c -ge $cword ]]; then
__%[1]s_handle_reply
return
fi
__%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
if [[ "${words[c]}" == -* ]]; then
__%[1]s_handle_flag
elif __%[1]s_contains_word "${words[c]}" "${commands[@]}"; then
__%[1]s_handle_command
elif [[ $c -eq 0 ]]; then
__%[1]s_handle_command
elif __%[1]s_contains_word "${words[c]}" "${command_aliases[@]}"; then
# aliashash variable is an associative array which is only supported in bash > 3.
if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then
words[c]=${aliashash[${words[c]}]}
__%[1]s_handle_command
else
__%[1]s_handle_noun
fi
else
__%[1]s_handle_noun
fi
__%[1]s_handle_word
}
`, name, ShellCompNoDescRequestCmd,
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, activeHelpEnvVar(name)))
}
func writePostscript(buf io.StringWriter, name string) {
name = strings.ReplaceAll(name, ":", "__")
writeString(buf, fmt.Sprintf("__start_%s()\n", name))
writeString(buf, fmt.Sprintf(`{
local cur prev words cword split
declare -A flaghash 2>/dev/null || :
declare -A aliashash 2>/dev/null || :
if declare -F _init_completion >/dev/null 2>&1; then
_init_completion -s || return
else
__%[1]s_init_completion -n "=" || return
fi
local c=0
local flag_parsing_disabled=
local flags=()
local two_word_flags=()
local local_nonpersistent_flags=()
local flags_with_completion=()
local flags_completion=()
local commands=("%[1]s")
local command_aliases=()
local must_have_one_flag=()
local must_have_one_noun=()
local has_completion_function=""
local last_command=""
local nouns=()
local noun_aliases=()
__%[1]s_handle_word
}
`, name))
writeString(buf, fmt.Sprintf(`if [[ $(type -t compopt) = "builtin" ]]; then
complete -o default -F __start_%s %s
else
complete -o default -o nospace -F __start_%s %s
fi
`, name, name, name, name))
writeString(buf, "# ex: ts=4 sw=4 et filetype=sh\n")
}
func writeCommands(buf io.StringWriter, cmd *kong.Node) {
writeString(buf, " commands=()\n")
for _, c := range cmd.Children {
if c == nil || c.Hidden {
continue
}
writeString(buf, fmt.Sprintf(" commands+=(%q)\n", c.Name))
writeCmdAliases(buf, c)
}
writeString(buf, "\n")
}
func writeFlagHandler(buf io.StringWriter, name string, annotations map[string][]string, cmd *kong.Node) {
for key, value := range annotations {
switch key {
case BashCompFilenameExt:
writeString(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name))
var ext string
if len(value) > 0 {
ext = fmt.Sprintf("__%s_handle_filename_extension_flag ", cmd.Parent.Name) + strings.Join(value, "|")
} else {
ext = "_filedir"
}
writeString(buf, fmt.Sprintf(" flags_completion+=(%q)\n", ext))
case BashCompCustom:
writeString(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name))
if len(value) > 0 {
handlers := strings.Join(value, "; ")
writeString(buf, fmt.Sprintf(" flags_completion+=(%q)\n", handlers))
} else {
writeString(buf, " flags_completion+=(:)\n")
}
case BashCompSubdirsInDir:
writeString(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name))
var ext string
if len(value) == 1 {
ext = fmt.Sprintf("__%s_handle_subdirs_in_dir_flag ", cmd.Parent.Name) + value[0]
} else {
ext = "_filedir -d"
}
writeString(buf, fmt.Sprintf(" flags_completion+=(%q)\n", ext))
}
}
}
const cbn = "\")\n"
func writeShortFlag(buf io.StringWriter, flag *kong.Flag, cmd *kong.Node) {
name := fmt.Sprintf("%c", flag.Short)
format := " "
if len(flag.DefaultValue.String()) == 0 {
format += "two_word_"
}
format += "flags+=(\"-%s" + cbn
writeString(buf, fmt.Sprintf(format, name))
writeFlagHandler(buf, "-"+name, map[string][]string{}, cmd)
}
func writeFlag(buf io.StringWriter, flag *kong.Flag, cmd *kong.Node) {
name := flag.Name
format := " flags+=(\"--%s"
if len(flag.DefaultValue.String()) == 0 {
format += "="
}
format += cbn
writeString(buf, fmt.Sprintf(format, name))
if len(flag.DefaultValue.String()) == 0 {
format = " two_word_flags+=(\"--%s" + cbn
writeString(buf, fmt.Sprintf(format, name))
}
writeFlagHandler(buf, "--"+name, map[string][]string{}, cmd)
}
//nolint:deadcode,unused
func writeLocalNonPersistentFlag(buf io.StringWriter, flag *kong.Flag) {
name := flag.Name
format := " local_nonpersistent_flags+=(\"--%[1]s" + cbn
if len(flag.DefaultValue.String()) == 0 {
format += " local_nonpersistent_flags+=(\"--%[1]s=" + cbn
}
writeString(buf, fmt.Sprintf(format, name))
if flag.Short > 0 {
writeString(buf, fmt.Sprintf(" local_nonpersistent_flags+=(\"-%c\")\n", flag.Short))
}
}
func writeFlags(buf io.StringWriter, cmd *kong.Node) {
writeString(buf, ` flags=()
two_word_flags=()
local_nonpersistent_flags=()
flags_with_completion=()
flags_completion=()
`)
for _, flag := range cmd.Flags {
if nonCompletableFlag(flag) {
continue
}
writeFlag(buf, flag, cmd)
if flag.Short != 0 {
writeShortFlag(buf, flag, cmd)
}
}
writeString(buf, "\n")
}
func writeCmdAliases(buf io.StringWriter, cmd *kong.Node) {
if len(cmd.Aliases) == 0 {
return
}
sort.Strings(cmd.Aliases)
writeString(buf, fmt.Sprint(` if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then`, "\n"))
for _, value := range cmd.Aliases {
writeString(buf, fmt.Sprintf(" command_aliases+=(%q)\n", value))
writeString(buf, fmt.Sprintf(" aliashash[%q]=%q\n", value, cmd.Name))
}
writeString(buf, ` fi`)
writeString(buf, "\n")
}
func writeArgAliases(buf io.StringWriter, cmd *kong.Node) {
writeString(buf, " noun_aliases=()\n")
sort.Strings(cmd.Aliases)
for _, value := range cmd.Aliases {
writeString(buf, fmt.Sprintf(" noun_aliases+=(%q)\n", value))
}
}
func (b Bash) gen(buf io.StringWriter, cmd *kong.Node) {
for _, c := range cmd.Children {
if c == nil || c.Hidden {
continue
}
b.gen(buf, c)
}
commandName := cmd.FullPath()
commandName = strings.ReplaceAll(commandName, " ", "_")
commandName = strings.ReplaceAll(commandName, ":", "__")
if cmd.Parent == nil {
writeString(buf, fmt.Sprintf("_%s_root_command()\n{\n", commandName))
} else {
writeString(buf, fmt.Sprintf("_%s()\n{\n", commandName))
}
writeString(buf, fmt.Sprintf(" last_command=%q\n", commandName))
writeString(buf, "\n")
writeString(buf, " command_aliases=()\n")
writeString(buf, "\n")
writeCommands(buf, cmd)
writeFlags(buf, cmd)
writeArgAliases(buf, cmd)
writeString(buf, "}\n\n")
}
================================================
FILE: completion/command.go
================================================
package completion
import (
"fmt"
"io"
"os"
"strings"
"github.com/alecthomas/kong"
)
// Completion command.
type Completion struct {
Bash Bash `cmd:"" help:"Generate the autocompletion script for bash"`
Zsh Zsh `cmd:"" help:"Generate the autocompletion script for zsh"`
Fish Fish `cmd:"" help:"Generate the autocompletion script for fish"`
}
func commandName(cmd *kong.Node) string {
commandName := cmd.FullPath()
commandName = strings.ReplaceAll(commandName, " ", "_")
commandName = strings.ReplaceAll(commandName, ":", "__")
return commandName
}
func hasCommands(cmd *kong.Node) bool {
for _, c := range cmd.Children {
if !c.Hidden {
return true
}
}
return false
}
//nolint:deadcode,unused
func isArgument(cmd *kong.Node) bool {
return cmd.Type == kong.ArgumentNode
}
// writeString writes a string into a buffer, and checks if the error is not nil.
func writeString(b io.StringWriter, s string) {
if _, err := b.WriteString(s); err != nil {
fmt.Fprintln(os.Stderr, "Error:", err)
os.Exit(1)
}
}
func nonCompletableFlag(flag *kong.Flag) bool {
return flag.Hidden
}
func flagPossibleValues(flag *kong.Flag) []string {
values := make([]string, 0)
for _, enum := range flag.EnumSlice() {
if strings.TrimSpace(enum) != "" {
values = append(values, enum)
}
}
return values
}
================================================
FILE: completion/fish.go
================================================
package completion
import (
"fmt"
"io"
"strings"
"github.com/alecthomas/kong"
)
// Fish is a fish shell completion generator.
type Fish struct{}
// Run generates fish completion script.
func (f Fish) Run(ctx *kong.Context) error {
var buf strings.Builder
buf.WriteString(`# Fish shell completion for gum
# Generated by gum completion
# disable file completion unless explicitly enabled
complete -c gum -f
`)
node := ctx.Model.Node
f.gen(&buf, node)
_, err := fmt.Fprint(ctx.Stdout, buf.String())
if err != nil {
return fmt.Errorf("unable to generate fish completion: %w", err)
}
return nil
}
func (f Fish) gen(buf io.StringWriter, cmd *kong.Node) {
root := cmd
for root.Parent != nil {
root = root.Parent
}
rootName := root.Name
if cmd.Parent == nil {
_, _ = buf.WriteString(fmt.Sprintf("# %s\n", rootName))
} else {
_, _ = buf.WriteString(fmt.Sprintf("# %s\n", cmd.Path()))
_, _ = buf.WriteString(
fmt.Sprintf("complete -c %s -f -n '__fish_use_subcommand' -a %s -d '%s'\n",
rootName,
cmd.Name,
cmd.Help,
),
)
}
for _, f := range cmd.Flags {
if f.Hidden {
continue
}
if cmd.Parent == nil {
_, _ = buf.WriteString(
fmt.Sprintf("complete -c %s -f",
rootName,
),
)
} else {
_, _ = buf.WriteString(
fmt.Sprintf("complete -c %s -f -n '__fish_seen_subcommand_from %s'",
rootName,
cmd.Name,
),
)
}
if !f.IsBool() {
enums := flagPossibleValues(f)
if len(enums) > 0 {
_, _ = buf.WriteString(fmt.Sprintf(" -xa '%s'", strings.Join(enums, " ")))
} else {
_, _ = buf.WriteString(" -x")
}
}
if f.Short != 0 {
_, _ = buf.WriteString(fmt.Sprintf(" -s %c", f.Short))
}
_, _ = buf.WriteString(fmt.Sprintf(" -l %s", f.Name))
_, _ = buf.WriteString(fmt.Sprintf(" -d \"%s\"", f.Help))
_, _ = buf.WriteString("\n")
}
_, _ = buf.WriteString("\n")
for _, c := range cmd.Children {
if c == nil || c.Hidden {
continue
}
f.gen(buf, c)
}
}
================================================
FILE: completion/zsh.go
================================================
package completion
import (
"fmt"
"io"
"strings"
"github.com/alecthomas/kong"
)
// Zsh is zsh completion generator.
type Zsh struct{}
// Run generates zsh completion script.
func (z Zsh) Run(ctx *kong.Context) error {
var out strings.Builder
format := `#compdef %[1]s
# zsh completion for %[1]s
# generated by gum completion
`
fmt.Fprintf(&out, format, ctx.Model.Name)
z.gen(&out, ctx.Model.Node)
_, err := fmt.Fprint(ctx.Stdout, out.String())
if err != nil {
return fmt.Errorf("unable to generate zsh completion: %w", err)
}
return nil
}
func (z Zsh) writeFlag(buf io.StringWriter, f *kong.Flag) {
var str strings.Builder
str.WriteString(" ")
if f.Short != 0 {
str.WriteString("'(")
str.WriteString(fmt.Sprintf("-%c --%s", f.Short, f.Name))
if !f.IsBool() {
str.WriteString("=")
}
str.WriteString(")'")
str.WriteString("{")
str.WriteString(fmt.Sprintf("-%c,--%s", f.Short, f.Name))
if !f.IsBool() {
str.WriteString("=")
}
str.WriteString("}")
str.WriteString("\"")
} else {
str.WriteString("\"")
str.WriteString(fmt.Sprintf("--%s", f.Name))
if !f.IsBool() {
str.WriteString("=")
}
}
str.WriteString(fmt.Sprintf("[%s]", f.Help))
if !f.IsBool() {
str.WriteString(":")
str.WriteString(strings.ToLower(f.Help))
str.WriteString(":")
}
values := flagPossibleValues(f)
if len(values) > 0 {
str.WriteString("(")
for i, v := range f.EnumSlice() {
str.WriteString(v)
if i < len(values)-1 {
str.WriteString(" ")
}
}
str.WriteString(")")
}
str.WriteString("\"")
writeString(buf, str.String())
}
func (z Zsh) writeFlags(buf io.StringWriter, cmd *kong.Node) {
for i, f := range cmd.Flags {
if f.Hidden {
continue
}
z.writeFlag(buf, f)
if i < len(cmd.Flags)-1 {
writeString(buf, " \\\n")
}
}
}
func (z Zsh) writeCommand(buf io.StringWriter, c *kong.Node) {
writeString(buf, fmt.Sprintf(" \"%s[%s]\"", c.Name, c.Help))
}
func (z Zsh) writeCommands(buf io.StringWriter, cmd *kong.Node) {
for i, c := range cmd.Children {
if c == nil || c.Hidden {
continue
}
z.writeCommand(buf, c)
if i < len(cmd.Children)-1 {
_, _ = buf.WriteString(" \\")
}
writeString(buf, "\n")
}
}
func (z Zsh) gen(buf io.StringWriter, cmd *kong.Node) {
for _, c := range cmd.Children {
if c == nil || c.Hidden {
continue
}
z.gen(buf, c)
}
cmdName := commandName(cmd)
writeString(buf, fmt.Sprintf("_%s() {\n", cmdName))
if hasCommands(cmd) {
writeString(buf, " local line state\n")
}
writeString(buf, " _arguments -C \\\n")
z.writeFlags(buf, cmd)
if hasCommands(cmd) {
writeString(buf, " \\\n")
writeString(buf, " \"1: :->cmds\" \\\n")
writeString(buf, " \"*::arg:->args\"\n")
writeString(buf, " case \"$state\" in\n")
writeString(buf, " cmds)\n")
writeString(buf, fmt.Sprintf(" _values \"%s command\" \\\n", cmdName))
z.writeCommands(buf, cmd)
writeString(buf, " ;;\n")
writeString(buf, " args)\n")
writeString(buf, " case \"$line[1]\" in\n")
for _, c := range cmd.Children {
if c == nil || c.Hidden {
continue
}
writeString(buf, fmt.Sprintf(" %s)\n", c.Name))
writeString(buf, fmt.Sprintf(" _%s\n", commandName(c)))
writeString(buf, " ;;\n")
}
writeString(buf, " esac\n")
writeString(buf, " ;;\n")
writeString(buf, " esac\n")
}
// writeArgAliases(buf, cmd)
writeString(buf, "\n")
writeString(buf, "}\n\n")
}
================================================
FILE: confirm/command.go
================================================
package confirm
import (
"context"
"fmt"
"os"
"github.com/charmbracelet/bubbles/help"
tea "github.com/charmbracelet/bubbletea"
"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
// action with an affirmative or negative answer.
func (o Options) Run() error {
line, err := stdin.Read(stdin.SingleLine(true))
if err == nil {
switch line {
case "yes", "y":
return nil
default:
return exit.ErrExit(1)
}
}
ctx, cancel := timeout.Context(o.Timeout)
defer cancel()
top, right, bottom, left := style.ParsePadding(o.Padding)
m := model{
affirmative: o.Affirmative,
negative: o.Negative,
showOutput: o.ShowOutput,
confirmation: o.Default,
defaultSelection: o.Default,
keys: defaultKeymap(o.Affirmative, o.Negative),
help: help.New(),
showHelp: o.ShowHelp,
prompt: o.Prompt,
selectedStyle: o.SelectedStyle.ToLipgloss(),
unselectedStyle: o.UnselectedStyle.ToLipgloss(),
promptStyle: o.PromptStyle.ToLipgloss(),
padding: []int{top, right, bottom, left},
}
tm, err := tea.NewProgram(
m,
tea.WithOutput(os.Stderr),
tea.WithContext(ctx),
).Run()
if err != nil && ctx.Err() != context.DeadlineExceeded {
return fmt.Errorf("unable to confirm: %w", err)
}
m = tm.(model)
if o.ShowOutput {
confirmationText := m.negative
if m.confirmation {
confirmationText = m.affirmative
}
fmt.Println(m.prompt, confirmationText)
}
if m.confirmation {
return nil
}
return exit.ErrExit(1)
}
================================================
FILE: confirm/confirm.go
================================================
// Package confirm provides an interface to ask a user to confirm an action.
// The user is provided with an interface to choose an affirmative or negative
// answer, which is then reflected in the exit code for use in scripting.
//
// If the user selects the affirmative answer, the program exits with 0. If the
// user selects the negative answer, the program exits with 1.
//
// I.e. confirm if the user wants to delete a file
//
// $ gum confirm "Are you sure?" && rm file.txt
package confirm
import (
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
func defaultKeymap(affirmative, negative string) keymap {
return keymap{
Abort: key.NewBinding(
key.WithKeys("ctrl+c"),
key.WithHelp("ctrl+c", "cancel"),
),
Quit: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "quit"),
),
Negative: key.NewBinding(
key.WithKeys("n", "N", "q"),
key.WithHelp("n", negative),
),
Affirmative: key.NewBinding(
key.WithKeys("y", "Y"),
key.WithHelp("y", affirmative),
),
Toggle: key.NewBinding(
key.WithKeys(
"left",
"h",
"ctrl+n",
"shift+tab",
"right",
"l",
"ctrl+p",
"tab",
),
key.WithHelp("←→", "toggle"),
),
Submit: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "submit"),
),
}
}
type keymap struct {
Abort key.Binding
Quit key.Binding
Negative key.Binding
Affirmative key.Binding
Toggle key.Binding
Submit key.Binding
}
// FullHelp implements help.KeyMap.
func (k keymap) FullHelp() [][]key.Binding { return nil }
// ShortHelp implements help.KeyMap.
func (k keymap) ShortHelp() []key.Binding {
return []key.Binding{k.Toggle, k.Submit, k.Affirmative, k.Negative}
}
type model struct {
prompt string
affirmative string
negative string
quitting bool
showHelp bool
help help.Model
keys keymap
showOutput bool
confirmation bool
defaultSelection bool
// styles
promptStyle lipgloss.Style
selectedStyle lipgloss.Style
unselectedStyle lipgloss.Style
padding []int
}
func (m model) Init() tea.Cmd { return nil }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
return m, nil
case tea.KeyMsg:
switch {
case key.Matches(msg, m.keys.Abort):
m.confirmation = false
return m, tea.Interrupt
case key.Matches(msg, m.keys.Quit):
m.confirmation = false
m.quitting = true
return m, tea.Quit
case key.Matches(msg, m.keys.Negative):
m.confirmation = false
m.quitting = true
return m, tea.Quit
case key.Matches(msg, m.keys.Toggle):
if m.negative == "" {
break
}
m.confirmation = !m.confirmation
case key.Matches(msg, m.keys.Submit):
m.quitting = true
return m, tea.Quit
case key.Matches(msg, m.keys.Affirmative):
m.quitting = true
m.confirmation = true
return m, tea.Quit
}
}
return m, nil
}
func (m model) View() string {
if m.quitting {
return ""
}
var aff, neg string
if m.confirmation {
aff = m.selectedStyle.Render(m.affirmative)
neg = m.unselectedStyle.Render(m.negative)
} else {
aff = m.unselectedStyle.Render(m.affirmative)
neg = m.selectedStyle.Render(m.negative)
}
// If the option is intentionally empty, do not show it.
if m.negative == "" {
neg = ""
}
parts := []string{
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...,
))
}
================================================
FILE: confirm/options.go
================================================
package confirm
import (
"time"
"github.com/charmbracelet/gum/style"
)
// Options is the customization options for the confirm command.
type Options struct {
Default bool `help:"Default confirmation action" default:"true"`
ShowOutput bool `help:"Print prompt and chosen action to output" default:"false"`
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?"`
//nolint:staticcheck
PromptStyle style.Styles `embed:"" prefix:"prompt." help:"The style of the prompt" set:"defaultMargin=0 0 0 1" set:"defaultForeground=#7571F9" set:"defaultBold=true" 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=0 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=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"`
}
================================================
FILE: cursor/cursor.go
================================================
// Package cursor provides cursor modes.
package cursor
import (
"github.com/charmbracelet/bubbles/cursor"
)
// Modes maps strings to cursor modes.
var Modes = map[string]cursor.Mode{
"blink": cursor.CursorBlink,
"hide": cursor.CursorHide,
"static": cursor.CursorStatic,
}
================================================
FILE: default.nix
================================================
{ pkgs }:
pkgs.buildGoModule rec {
pname = "gum";
version = "0.15.2";
src = ./.;
vendorHash = "sha256-TK2Fc4bTkiSpyYrg4dJOzamEnii03P7kyHZdah9izqY=";
ldflags = [ "-s" "-w" "-X=main.Version=${version}" ];
}
================================================
FILE: examples/.gitignore
================================================
*.gif
*.png
================================================
FILE: examples/README.md
================================================
# Glamour
A casual introduction. 你好世界!
## Let's talk about artichokes
The artichoke is mentioned as a garden
plant in the 8th century BC by Homer
and Hesiod. The naturally occurring
variant of the artichoke, the cardoon,
which is native to the Mediterranean
area, also has records of use as a
food among the ancient Greeks and
Romans. Pliny the Elder mentioned
growing of 'carduus' in Carthage
and Cordoba.
He holds him with his skinny hand,
There was ship,' quoth he.
'Hold off! unhand me, grey-beard loon!'
An artichoke dropt he.
## Other foods worth mentioning
1. Carrots
2. Celery
3. Tacos
• Soft
• Hard
4. Cucumber
## Things to eat today
* Carrots
* Ramen
* Currywurst
================================================
FILE: examples/choose.tape
================================================
Output choose.gif
Set Width 1000
Set Height 430
Set Shell bash
Type "gum choose {1..5}"
Sleep 500ms
Enter
Sleep 500ms
Down@250ms 3
Sleep 500ms
Up@250ms 2
Enter
Sleep 1.5s
Ctrl+L
Sleep 500ms
Type "gum choose --limit 2 Banana Cherry Orange"
Sleep 500ms
Enter
Sleep 500ms
Type@250ms "jxjxk"
Sleep 1s
Enter
Sleep 2s
================================================
FILE: examples/commit.sh
================================================
#!/bin/sh
# This script is used to write a conventional commit message.
# It prompts the user to choose the type of commit as specified in the
# conventional commit spec. And then prompts for the summary and detailed
# description of the message and uses the values provided. as the summary and
# details of the message.
#
# If you want to add a simpler version of this script to your dotfiles, use:
#
# alias gcm='git commit -m "$(gum input)" -m "$(gum write)"'
# if [ -z "$(git status -s -uno | grep -v '^ ' | awk '{print $2}')" ]; then
# gum confirm "Stage all?" && git add .
# fi
TYPE=$(gum choose "fix" "feat" "docs" "style" "refactor" "test" "chore" "revert")
SCOPE=$(gum input --placeholder "scope")
# Since the scope is optional, wrap it in parentheses if it has a value.
test -n "$SCOPE" && SCOPE="($SCOPE)"
# Pre-populate the input with the type(scope): so that the user may change it
SUMMARY=$(gum input --value "$TYPE$SCOPE: " --placeholder "Summary of this change")
DESCRIPTION=$(gum write --placeholder "Details of this change")
# Commit these changes if user confirms
gum confirm "Commit changes?" && git commit -m "$SUMMARY" -m "$DESCRIPTION"
================================================
FILE: examples/commit.tape
================================================
Output commit.gif
Set Shell "bash"
Set FontSize 32
Set Width 1200
Set Height 600
Type "./commit.sh" Sleep 500ms Enter
Sleep 1s
Down@250ms 2
Sleep 500ms
Enter
Sleep 500ms
Type "gum"
Sleep 500ms
Enter
Sleep 1s
Type "Gum is sooo tasty"
Sleep 500ms
Enter
Sleep 1s
Type@65ms "I love bubble gum."
Sleep 500ms
Alt+Enter
Sleep 500ms
Alt+Enter
Sleep 500ms
Type "This commit shows how much I love chewing bubble gum!!!"
Sleep 500ms
Enter
Sleep 1s
Left@400ms 3
Sleep 1s
================================================
FILE: examples/confirm.tape
================================================
Output confirm.gif
Set Width 1000
Set Height 350
Set Shell bash
Sleep 500ms
Type "gum confirm && echo 'Me too!' || echo 'Me neither.'"
Sleep 1s
Enter
Sleep 1s
Right
Sleep 500ms
Left
Sleep 500ms
Enter
Sleep 1.5s
Ctrl+L
Type "gum confirm && echo 'Me too!' || echo 'Me neither.'"
Sleep 500ms
Enter
Sleep 500ms
Right
Sleep 500ms
Enter
Sleep 1s
================================================
FILE: examples/convert-to-gif.sh
================================================
#!/bin/bash
# This script converts some video to a GIF. It prompts the user to select an
# video file with `gum filter` Set the frame rate, desired width, and max
# colors to use Then, converts the video to a GIF.
INPUT=$(gum filter --placeholder "Input file")
FRAMERATE=$(gum input --prompt "Frame rate: " --placeholder "Frame Rate" --prompt.foreground 240 --value "50")
WIDTH=$(gum input --prompt "Width: " --placeholder "Width" --prompt.foreground 240 --value "1200")
MAXCOLORS=$(gum input --prompt "Max Colors: " --placeholder "Max Colors" --prompt.foreground 240 --value "256")
BASENAME=$(basename "$INPUT")
BASENAME="${BASENAME%%.*}"
gum spin --title "Converting to GIF" -- ffmpeg -i "$INPUT" -vf "fps=$FRAMERATE,scale=$WIDTH:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=$MAXCOLORS[p];[s1][p]paletteuse" "$BASENAME.gif"
================================================
FILE: examples/customize.tape
================================================
Output customize.gif
Set Width 1000
Set Height 350
Set Shell bash
Sleep 1s
Type `gum input --cursor.foreground "#F4AC45" \` Enter
Type `--prompt.foreground "#04B575" --prompt "What's up? " \` Enter
Type `--placeholder "Not much, you?" --value "Not much, you?" \` Enter
Type `--width 80` Enter
Sleep 1s
Ctrl+A
Sleep 1s
Ctrl+E
Sleep 1s
Ctrl+U
Sleep 1s
================================================
FILE: examples/demo.sh
================================================
#!/bin/bash
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "Hello, there! Welcome to $(gum style --foreground 212 'Gum')."
NAME=$(gum input --placeholder "What is your name?")
echo -e "Well, it is nice to meet you, $(gum style --foreground 212 "$NAME")."
sleep 1; clear
echo -e "Can you tell me a $(gum style --italic --foreground 99 'secret')?\n"
gum write --placeholder "I'll keep it to myself, I promise!" > /dev/null # we keep the secret to ourselves
clear; echo "What should I do with this information?"; sleep 1
READ="Read"; THINK="Think"; DISCARD="Discard"
ACTIONS=$(gum choose --no-limit "$READ" "$THINK" "$DISCARD")
clear; echo "One moment, please."
grep -q "$READ" <<< "$ACTIONS" && gum spin -s line --title "Reading the secret..." -- sleep 1
grep -q "$THINK" <<< "$ACTIONS" && gum spin -s pulse --title "Thinking about your secret..." -- sleep 1
grep -q "$DISCARD" <<< "$ACTIONS" && gum spin -s monkey --title " Discarding your secret..." -- sleep 2
sleep 1; clear
GUM=$(echo -e "Cherry\nGrape\nLime\nOrange" | gum filter --placeholder "Favorite flavor?")
echo "I'll keep that in mind!"
sleep 1; clear
echo "Do you like $(gum style --foreground "#04B575" "Bubble Gum?")"
sleep 1
CHOICE=$(gum choose --item.foreground 250 "Yes" "No" "It's complicated")
[[ "$CHOICE" == "Yes" ]] && echo "I thought so, $(gum style --bold "Bubble Gum") is the best." || echo "I'm sorry to hear that."
sleep 1
gum spin --title "Chewing some $(gum style --foreground "#04B575" "$GUM") bubble gum..." -- sleep 2.5
clear
NICE_MEETING_YOU=$(gum style --height 5 --width 20 --padding '1 3' --border double --border-foreground 57 "Nice meeting you, $(gum style --foreground 212 "$NAME"). See you soon!")
CHEW_BUBBLE_GUM=$(gum style --width 17 --padding '1 3' --border double --border-foreground 212 "Go chew some $(gum style --foreground "#04B575" "$GUM") bubble gum.")
gum join --horizontal "$NICE_MEETING_YOU" "$CHEW_BUBBLE_GUM"
================================================
FILE: examples/demo.tape
================================================
Output ./demo.gif
Set Shell bash
Set FontSize 22
Set Width 800
Set Height 450
Type "./demo.sh"
Enter
Sleep 1s
Type "Walter"
Sleep 500ms
Enter
Sleep 2s
Type "Nope, sorry!"
Sleep 500ms
Alt+Enter
Sleep 200ms
Alt+Enter
Sleep 500ms
Type "I don't trust you."
Sleep 1s
Enter
Sleep 2s
Type "x" Sleep 250ms Type "j" Sleep 250ms
Type "x" Sleep 250ms Type "j" Sleep 250ms
Type "x" Sleep 1s
Enter
Sleep 6s
Type "li"
Sleep 1s
Enter
Sleep 3s
Down@500ms 2
Up@500ms 2
Sleep 1s
Enter
Sleep 6s
================================================
FILE: examples/diyfetch
================================================
#!/bin/sh
# ____ _____ ____ _ _
# | _ \_ _\ \ / / _| ___| |_ ___| |__
# | | | | | \ V / |_ / _ \ __/ __| '_ \
# | |_| | | | || _| __/ || (__| | | |
# |____/___| |_||_| \___|\__\___|_| |_|
#
# About:
# DIYfetch it the shell script template for writing fetch tool
# utilizing `gum join` command (https://github.com/charmbracelet/gum#join).
#
# This script is written in POSIX-shell for portability
# feel free to switch it to any scripting language that you prefer.
#
# Note:
# When copy ANSI string from random script make sure to replace "\033" and "\e" to ""
# or wrap it in `$(printf '%b' "<ansi_string>")`.
#
# URL: https://github.com/info-mono/diyfetch
# Prepare ------------------------------------------------------------------------------------------
# You can lookup the color codes on https://wikipedia.org/wiki/ANSI_escape_code#8-bit
main_color=4
# You can add some eye candy icons with Emoji of use Nerd Fonts (https://www.nerdfonts.com).
info=$(gum style "[1;38;5;${main_color}m${USER}[0m@[1;38;5;${main_color}m$(hostname)[0m
----------------
[1;38;5;${main_color}mOS: [0m<your_os>
[1;38;5;${main_color}mKERNEL: [0m$(uname -sr)
[1;38;5;${main_color}mUPTIME: [0m$(uptime -p | cut -c 4-)
[1;38;5;${main_color}mSHELL: [0m$(basename "${SHELL}")
[1;38;5;${main_color}mEDITOR: [0m$(basename "${EDITOR:-<your_editor>}")
[1;38;5;${main_color}mDE: [0m<your_de>
[1;38;5;${main_color}mWM: [0m<your_wm>
[1;38;5;${main_color}mTERMINAL: [0m<your_terminal>")
# You can get OS arts on https://github.com/info-mono/os-ansi
# copy the raw data of the .ansi file then paste it down below.
art=$(gum style ' [34m___[0m
[34m([0m.. [34m|[0m
[34m([33m<> [34m|[0m
[34m/ [0m__ [34m\[0m
[34m( [0m/ \[34m/ |[0m
[33m_[34m/\ [0m__)[34m/[33m_[34m)[0m
[33m\/[34m-____[33m\/[0m')
# You can generate colorstrip using https://github.com/info-mono/colorstrip
color=$(gum style '[0;30m███[0;31m███[0;32m███[0;33m███[0;34m███[0;35m███[0;36m███[0;37m███[0m
[0;1;90m███[0;1;91m███[0;1;92m███[0;1;93m███[0;1;94m███[0;1;95m███[0;1;96m███[0;1;97m███[0m')
# Display ------------------------------------------------------------------------------------------
# The code in this section is to display the fetch adaptively to the terminal's size.
# If you just want a static fetch display, you can just use something like this:
#
# group_info_color=$(gum join --vertical "${info}" '' "${color}")
# gum join --horizontal --align center ' ' "${art}" ' ' "${group_info_color}"
terminal_size=$(stty size)
terminal_height=${terminal_size% *}
terminal_width=${terminal_size#* }
# Acknowledge of how high the shell prompt is so the prompt don't push the fetch out.
prompt_height=${PROMPT_HEIGHT:-1}
print_test() {
no_color=$(printf '%b' "${1}" | sed -e 's/\x1B\[[0-9;]*[JKmsu]//g')
[ "$(printf '%s' "${no_color}" | wc --lines)" -gt $(( terminal_height - prompt_height )) ] && return 1
[ "$(printf '%s' "${no_color}" | wc --max-line-length)" -gt "${terminal_width}" ] && return 1
gum style --align center --width="${terminal_width}" "${1}" ''
printf '%b' "\033[A"
exit 0
}
# Paper layout
print_test "$(gum join --vertical --align center "${art}" '' "${info}" '' "${color}")"
# Classic layout
group_info_color=$(gum join --vertical "${info}" '' "${color}")
print_test "$(gum join --horizontal --align center "${art}" ' ' "${group_info_color}")"
# Hybrid layout
group_art_info=$(gum join --horizontal --align center "${art}" ' ' "${info}")
print_test "$(gum join --vertical --align center "${group_art_info}" '' "${color}")"
# Other layout
print_test "$(gum join --vertical --align center "${art}" '' "${info}")"
print_test "${group_art_info}"
print_test "${group_info_color}"
print_test "${info}"
exit 1
================================================
FILE: examples/fav.txt
================================================
Banana
================================================
FILE: examples/file.tape
================================================
Output file.gif
Set Width 800
Set Height 525
Set Shell bash
Type "gum file .."
Enter
Sleep 1s
Down@150ms 6
Sleep 1s
Enter
Sleep 1s
Type "j"
Sleep 1s
================================================
FILE: examples/filter-key-value.sh
================================================
#!/bin/bash
export LIST=$(cat <<END
Cow:Moo
Cat:Meow
Dog:Woof
END
)
ANIMAL=$(echo "$LIST" | cut -d':' -f1 | gum filter)
SOUND=$(echo "$LIST" | grep $ANIMAL | cut -d':' -f2)
echo "The $ANIMAL goes $SOUND"
================================================
FILE: examples/flavors.txt
================================================
Banana
Cherry
Orange
Strawberry
================================================
FILE: examples/format.ansi
================================================
[38;2;90;86;224m> [0mgum format -t code < main.go
[38;5;204m[0m[38;5;252m[0m [38;5;252m [0m[38;5;252m [0m[38;5;204mpackage[0m[38;5;251m [0m[38;5;251mmain[0m[38;5;251m[0m
[0m[38;5;252m[0m [38;5;252m [0m[38;5;252m [0m[38;5;251m[0m
[0m[38;5;204m[0m[38;5;252m[0m [38;5;252m [0m[38;5;252m [0m[38;5;204mimport[0m[38;5;251m [0m[38;5;173m"fmt"[0m[38;5;251m[0m
[0m[38;5;252m[0m [38;5;252m [0m[38;5;252m [0m[38;5;251m[0m
[0m[38;5;39m[0m[38;5;252m[0m [38;5;252m [0m[38;5;252m [0m[38;5;39mfunc[0m[38;5;251m [0m[38;5;42mmain[0m[38;5;187m()[0m[38;5;251m [0m[38;5;187m{[0m[38;5;251m[0m
[0m[38;5;252m[0m [38;5;252m [0m[38;5;252m [0m[38;5;251m [0m[38;5;251mfmt[0m[38;5;187m.[0m[38;5;42mPrintln[0m[38;5;187m([0m[38;5;173m"Charm_™ Gum"[0m[38;5;187m)[0m[38;5;251m[0m
[0m[38;5;187m[0m[38;5;252m[0m [38;5;252m [0m[38;5;252m [0m[38;5;187m}[0m[38;5;251m[0m
[0m[38;5;252m[0m [38;5;252m [0m[38;5;252m [0m[38;5;251m[0m
[0m
================================================
FILE: examples/git-branch-manager.sh
================================================
#! /bin/sh
# This script is used to manage git branches such as delete, update, and rebase
# them. It prompts the user to choose the branches and the action they want to
# perform.
#
# For an explanation on the script and tutorial on how to create it, watch:
# https://www.youtube.com/watch?v=tnikefEuArQ
GIT_COLOR="#f14e32"
git_color_text () {
gum style --foreground "$GIT_COLOR" "$1"
}
get_branches () {
if [ ${1+x} ]; then
gum choose --selected.foreground="$GIT_COLOR" --limit="$1" $(git branch --format="%(refname:short)")
else
gum choose --selected.foreground="$GIT_COLOR" --no-limit $(git branch --format="%(refname:short)")
fi
}
git rev-parse --git-dir > /dev/null 2>&1
if [ $? -ne 0 ];
then
echo "$(git_color_text "!!") Must be run in a $(git_color_text "git") repo"
exit 1
fi
gum style \
--border normal \
--margin "1" \
--padding "1" \
--border-foreground "$GIT_COLOR" \
"$(git_color_text ' Git') Branch Manager"
echo "Choose $(git_color_text 'branches') to operate on:"
branches=$(get_branches)
echo ""
echo "Choose a $(git_color_text "command"):"
command=$(gum choose --cursor.foreground="$GIT_COLOR" rebase delete update)
echo ""
echo $branches | tr " " "\n" | while read -r branch
do
case $command in
rebase)
base_branch=$(get_branches 1)
git fetch origin
git checkout "$branch"
git rebase "origin/$base_branch"
;;
delete)
git branch -D "$branch"
;;
update)
git checkout "$branch"
git pull --ff-only
;;
esac
done
================================================
FILE: examples/git-stage.sh
================================================
#!/bin/bash
ADD="Add"
RESET="Reset"
ACTION=$(gum choose "$ADD" "$RESET")
if [ "$ACTION" == "$ADD" ]; then
git status --short | cut -c 4- | gum choose --no-limit | xargs git add
else
git status --short | cut -c 4- | gum choose --no-limit | xargs git restore
fi
================================================
FILE: examples/gum.js
================================================
const { spawn } = require("child_process");
const activities = ["walking", "running", "cycling", "driving", "transport"];
console.log("What's your favorite activity?")
const gum = spawn("gum", ["choose", ...activities]);
gum.stderr.pipe(process.stderr);
gum.stdout.on("data", data => {
const activity = data.toString().trim();
console.log(`I like ${activity} too!`);
});
================================================
FILE: examples/gum.py
================================================
import subprocess
print("What's your favorite language?")
result = subprocess.run(["gum", "choose", "Go", "Python"], stdout=subprocess.PIPE, text=True)
print(f"I like {result.stdout.strip()}, too!")
================================================
FILE: examples/gum.rb
================================================
puts 'What is your name?'
name = `gum input --placeholder "Your name"`.chomp
puts "Hello #{name}!"
puts 'Pick your 2 favorite colors'
COLORS = {
'Red' => '#FF0000',
'Blue' => '#0000FF',
'Green' => '#00FF00',
'Yellow' => '#FFFF00',
'Orange' => '#FFA500',
'Purple' => '#800080',
'Pink' => '#FF00FF'
}.freeze
colors = `gum choose #{COLORS.keys.join(' ')} --limit 2`.chomp.split("\n")
if colors.length == 2
first = `gum style --foreground '#{COLORS[colors[0]]}' '#{colors[0]}'`.chomp
second = `gum style --foreground '#{COLORS[colors[1]]}' '#{colors[1]}'`.chomp
puts "You chose #{first} and #{second}."
elsif colors.length == 1
first = `gum style --foreground '#{COLORS[colors[0]]}' '#{colors[0]}'`.chomp
puts "You chose #{first}."
else
puts "You didn't pick any colors!"
end
================================================
FILE: examples/input.tape
================================================
Output input.gif
Set Width 800
Set Height 250
Set Shell bash
Sleep 1s
Type `gum input --placeholder "What's up?"`
Sleep 1s
Enter
Sleep 1s
Type "Not much, you?"
Sleep 1s
Enter
Sleep 1s
================================================
FILE: examples/kaomoji.sh
================================================
#!/usr/bin/env bash
# If the user passes '-h', '--help', or 'help' print out a little bit of help.
# text.
case "$1" in
"-h" | "--help" | "help")
printf 'Generate kaomojis on request.\n\n'
printf 'Usage: %s [kind]\n' "$(basename "$0")"
exit 1
;;
esac
# The user can pass an argument like "bear" or "angry" to specify the general
# kind of Kaomoji produced.
sentiment=""
if [[ $1 != "" ]]; then
sentiment=" $1"
fi
# Ask mods to generate Kaomojis. Save the output in a variable.
kaomoji="$(mods "generate 10${sentiment} kaomojis. number them and put each one on its own line.")"
if [[ $kaomoji == "" ]]; then
exit 1
fi
# Pipe mods output to gum so the user can choose the perfect kaomoji. Save that
# choice in a variable. Also note that we're using cut to drop the item number
# in front of the Kaomoji.
choice="$(echo "$kaomoji" | gum choose | cut -d ' ' -f 2)"
if [[ $choice == "" ]]; then
exit 1
fi
# If xsel (X11) or pbcopy (macOS) exists, copy to the clipboard. If not, just
# print the Kaomoji.
if command -v xsel &> /dev/null; then
printf '%s' "$choice" | xclip -sel clip # X11
elif command -v pbcopy &> /dev/null; then
printf '%s' "$choice" | pbcopy # macOS
else
# We can't copy, so just print it out.
printf 'Here you go: %s\n' "$choice"
exit 0
fi
# We're done!
printf 'Copied %s to the clipboard\n' "$choice"
================================================
FILE: examples/magic.sh
================================================
#!/bin/bash
# Always ask for permission!
echo "Do you want to see a magic trick?"
YES="Yes, please!"
NO="No, thank you!"
CHOICE=$(gum choose "$YES" "$NO")
if [ "$CHOICE" != "$YES" ]; then
echo "Alright, then. Have a nice day!"
exit 1
fi
# Let the magic begin.
echo "Alright, then. Let's begin!"
gum style --foreground 212 "Pick a card, any card..."
CARD=$(gum choose "Ace (A)" "Two (2)" "Three (3)" "Four (4)" "Five (5)" "Six (6)" "Seven (7)" "Eight (8)" "Nine (9)" "Ten (10)" "Jack (J)" "Queen (Q)" "King (K)")
SUIT=$(gum choose "Hearts (♥)" "Diamonds (♦)" "Clubs (♣)" "Spades (♠)")
gum style --foreground 212 "You picked the $CARD of $SUIT."
SHORT_CARD=$(echo $CARD | cut -d' ' -f2 | tr -d '()')
SHORT_SUIT=$(echo $SUIT | cut -d' ' -f2 | tr -d '()')
TOP_LEFT=$(gum join --vertical "$SHORT_CARD" "$SHORT_SUIT")
BOTTOM_RIGHT=$(gum join --vertical "$SHORT_SUIT" "$SHORT_CARD")
TOP_LEFT=$(gum style --width 10 --height 5 --align left "$TOP_LEFT")
BOTTOM_RIGHT=$(gum style --width 10 --align right "$BOTTOM_RIGHT")
if [[ "$SHORT_SUIT" == "♥" || "$SHORT_SUIT" == "♦" ]]; then
CARD_COLOR="1" # Red
else
CARD_COLOR="7" # Black
fi
gum style --border rounded --padding "0 1" --margin 2 --border-foreground "$CARD_COLOR" --foreground "$CARD_COLOR" "$(gum join --vertical "$TOP_LEFT" "$BOTTOM_RIGHT")"
echo "Is this your card?"
gum choose "Omg, yes!" "Nope, sorry!"
================================================
FILE: examples/pager.tape
================================================
Output pager.gif
Set Shell bash
Set Width 900
Set Height 750
Sleep 1s
Type "gum pager < README.md"
Enter
Sleep 1.5s
Down@100ms 25
Sleep 1s
Up@100ms 25
Sleep 3s
================================================
FILE: examples/posix.sh
================================================
#!/bin/sh
echo "What's your favorite shell?"
gum choose "Posix" "Bash" "Zsh" "Fish" "Elvish"
================================================
FILE: examples/skate.sh
================================================
#!/bin/sh
# Building a simple `skate` TUI with gum to allow you to select a database and
# pick a value from skate.
DATABASE=$(skate list-dbs | gum choose)
skate list --keys-only "$DATABASE" | gum filter | xargs -I {} skate get {}"$DATABASE"
================================================
FILE: examples/spin.tape
================================================
Output spin.gif
Set Shell bash
Set Width 1200
Set Height 300
Set FontSize 36
Sleep 500ms
Type `gum spin --title "Buying Gum..." -- sleep 5`
Sleep 1s
Enter
Sleep 4s
================================================
FILE: examples/story.txt
================================================
Once upon a time
In a land far, far away....
================================================
FILE: examples/test.sh
================================================
#!/bin/sh
# Choose
gum choose Foo Bar Baz
gum choose Choose One Item --cursor "* " --cursor.foreground 99 --selected.foreground 99
gum choose Pick Two Items Maximum --limit 2 --cursor "* " --cursor-prefix "(•) " --selected-prefix "(x) " --unselected-prefix "( ) " --cursor.foreground 99 --selected.foreground 99
gum choose Unlimited Choice Of Items --no-limit --cursor "* " --cursor-prefix "(•) " --selected-prefix "(x) " --unselected-prefix "( ) " --cursor.foreground 99 --selected.foreground 99
# Confirm
gum confirm "Testing?"
gum confirm "No?" --default=false --affirmative "Okay." --negative "Cancel."
# Filter
gum filter
echo {1..500} | sed 's/ /\n/g' | gum filter
echo {1..500} | sed 's/ /\n/g' | gum filter --indicator ">" --placeholder "Pick a number..." --indicator.foreground 1 --text.foreground 2 --match.foreground 3 --prompt.foreground 4 --height 5
# Format
echo "# Header\nBody" | gum format
echo 'package main\n\nimport "fmt"\n\nfunc main() {\n\tfmt.Println("Hello, Gum!")\n}\n' | gum format -t code
echo ":candy:" | gum format -t emoji
echo '{{ Bold "Bold" }}' | gum format -t template
# Input
gum input
gum input --prompt "Email: " --placeholder "john@doe.com" --prompt.foreground 99 --cursor.foreground 99 --width 50
gum input --password --prompt "Password: " --placeholder "hunter2" --prompt.foreground 99 --cursor.foreground 99 --width 50
# Join
gum join "Horizontal" "Join"
gum join --vertical "Vertical" "Join"
# Spin
gum spin -- sleep 1
gum spin --spinner minidot --title "Loading..." --title.foreground 99 -- sleep 1
gum spin --show-output --spinner monkey --title "Loading..." --title.foreground 99 -- sh -c 'sleep 1; echo "Hello, Gum!"'
# Style
gum style --foreground 99 --border double --border-foreground 99 --padding "1 2" --margin 1 "Hello, Gum."
# Write
gum write
gum write --width 40 --height 6 --placeholder "Type whatever you want" --prompt "| " --show-cursor-line --show-line-numbers --value "Something..." --base.padding 1 --cursor.foreground 99 --prompt.foreground 99
# Table
gum table < table/example.csv
# Pager
gum pager < README.md
# File
gum file
================================================
FILE: examples/write.tape
================================================
Output write.gif
Set Width 800
Set Height 350
Set Shell bash
Sleep 500ms
Type "gum write > story.txt"
Enter
Sleep 1s
Type "Once upon a time"
Sleep 1s
Alt+Enter
Type "In a land far, far away...."
Sleep 500ms
Enter
Sleep 1s
Type "cat story.txt"
Enter
Sleep 2s
================================================
FILE: file/command.go
================================================
package file
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/charmbracelet/bubbles/filepicker"
"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.
func (o Options) Run() error {
if !o.File && !o.Directory {
return errors.New("at least one between --file and --directory must be set")
}
if o.Path == "" {
o.Path = "."
}
path, err := filepath.Abs(o.Path)
if err != nil {
return fmt.Errorf("file not found: %w", err)
}
fp := filepicker.New()
fp.CurrentDirectory = path
fp.Path = path
fp.SetHeight(o.Height)
fp.AutoHeight = o.Height == 0
fp.Cursor = o.Cursor
fp.DirAllowed = o.Directory
fp.FileAllowed = o.File
fp.ShowPermissions = o.Permissions
fp.ShowSize = o.Size
fp.ShowHidden = o.All
fp.Styles = filepicker.DefaultStyles()
fp.Styles.Cursor = o.CursorStyle.ToLipgloss()
fp.Styles.Symlink = o.SymlinkStyle.ToLipgloss()
fp.Styles.Directory = o.DirectoryStyle.ToLipgloss()
fp.Styles.File = o.FileStyle.ToLipgloss()
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(),
headerStyle: o.HeaderStyle.ToLipgloss(),
header: o.Header,
}
ctx, cancel := timeout.Context(o.Timeout)
defer cancel()
tm, err := tea.NewProgram(
&m,
tea.WithOutput(os.Stderr),
tea.WithContext(ctx),
).Run()
if err != nil {
return fmt.Errorf("unable to pick selection: %w", err)
}
m = tm.(model)
if m.selectedPath == "" {
return errors.New("no file selected")
}
fmt.Println(m.selectedPath)
return nil
}
================================================
FILE: file/file.go
================================================
// 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 (
"github.com/charmbracelet/bubbles/filepicker"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type keymap filepicker.KeyMap
var keyQuit = key.NewBinding(
key.WithKeys("esc", "q"),
key.WithHelp("esc", "close"),
)
var keyAbort = key.NewBinding(
key.WithKeys("ctrl+c"),
key.WithHelp("ctrl+c", "abort"),
)
func defaultKeymap() keymap {
km := filepicker.DefaultKeyMap()
return keymap(km)
}
// FullHelp implements help.KeyMap.
func (k keymap) FullHelp() [][]key.Binding { return nil }
// ShortHelp implements help.KeyMap.
func (k keymap) ShortHelp() []key.Binding {
return []key.Binding{
key.NewBinding(
key.WithKeys("up", "down"),
key.WithHelp("↓↑", "navigate"),
),
keyQuit,
k.Select,
}
}
type model struct {
header string
headerStyle lipgloss.Style
filepicker filepicker.Model
selectedPath string
quitting bool
showHelp bool
padding []int
help help.Model
keymap keymap
}
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 {
height -= lipgloss.Height(m.helpView())
}
m.filepicker.SetHeight(height)
case tea.KeyMsg:
switch {
case key.Matches(msg, keyAbort):
m.quitting = true
return m, tea.Interrupt
case key.Matches(msg, keyQuit):
m.quitting = true
return m, tea.Quit
}
}
var cmd tea.Cmd
m.filepicker, cmd = m.filepicker.Update(msg)
if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect {
m.selectedPath = path
m.quitting = true
return m, tea.Quit
}
return m, cmd
}
func (m model) View() string {
if m.quitting {
return ""
}
var parts []string
if m.header != "" {
parts = append(parts, m.headerStyle.Render(m.header))
}
parts = append(parts, m.filepicker.View())
if m.showHelp {
parts = append(parts, m.helpView())
}
return lipgloss.NewStyle().
Padding(m.padding...).
Render(lipgloss.JoinVertical(
lipgloss.Left,
parts...,
))
}
func (m model) helpView() string {
return m.help.View(m.keymap)
}
================================================
FILE: file/options.go
================================================
package file
import (
"time"
"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" env:"GUM_FILE_PATH"`
// Cursor is the character to display in front of the current selected items.
Cursor string `short:"c" help:"The cursor character" default:">" env:"GUM_FILE_CURSOR"`
All bool `short:"a" help:"Show hidden and 'dot' files" default:"false" env:"GUM_FILE_ALL"`
Permissions bool `short:"p" help:"Show file permissions" default:"true" negatable:"" env:"GUM_FILE_PERMISSION"`
Size bool `short:"s" help:"Show file size" default:"true" negatable:"" env:"GUM_FILE_SIZE"`
File bool `help:"Allow files selection" default:"true" env:"GUM_FILE_FILE"`
Directory bool `help:"Allow directories selection" default:"false" env:"GUM_FILE_DIRECTORY"`
ShowHelp bool `help:"Show help key binds" negatable:"" default:"true" env:"GUM_FILE_SHOW_HELP"`
Timeout time.Duration `help:"Timeout until command aborts without a selection" default:"0s" env:"GUM_FILE_TIMEOUT"`
Header string `help:"Header value" default:"" env:"GUM_FILE_HEADER"`
Height int `help:"Maximum number of files to display" default:"10" env:"GUM_FILE_HEIGHT"`
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_"` //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"`
}
================================================
FILE: filter/command.go
================================================
package filter
import (
"errors"
"fmt"
"os"
"slices"
"strings"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/files"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/internal/timeout"
"github.com/charmbracelet/gum/internal/tty"
"github.com/charmbracelet/gum/style"
"github.com/charmbracelet/x/ansi"
"github.com/sahilm/fuzzy"
)
// Run provides a shell script interface for filtering through options, powered
// by the textinput bubble.
func (o Options) Run() error {
i := textinput.New()
i.Focus()
i.Prompt = o.Prompt
i.PromptStyle = o.PromptStyle.ToLipgloss()
i.PlaceholderStyle = o.PlaceholderStyle.ToLipgloss()
i.Placeholder = o.Placeholder
i.Width = o.Width
v := viewport.New(o.Width, o.Height)
if len(o.Options) == 0 {
if input, _ := stdin.Read(stdin.StripANSI(o.StripANSI)); input != "" {
o.Options = strings.Split(input, o.InputDelimiter)
} else {
o.Options = files.List()
}
}
if len(o.Options) == 0 {
return errors.New("no options provided, see `gum filter --help`")
}
ctx, cancel := timeout.Context(o.Timeout)
defer cancel()
options := []tea.ProgramOption{
tea.WithOutput(os.Stderr),
tea.WithReportFocus(),
tea.WithContext(ctx),
}
if o.Height == 0 {
options = append(options, tea.WithAltScreen())
}
var matches []fuzzy.Match
if o.Value != "" {
i.SetValue(o.Value)
}
choices := map[string]string{}
filteringChoices := []string{}
for _, opt := range o.Options {
s := ansi.Strip(opt)
choices[s] = opt
filteringChoices = append(filteringChoices, s)
}
switch {
case o.Value != "" && o.Fuzzy:
matches = fuzzy.Find(o.Value, filteringChoices)
case o.Value != "" && !o.Fuzzy:
matches = exactMatches(o.Value, filteringChoices)
default:
matches = matchAll(filteringChoices)
}
if o.NoLimit {
o.Limit = len(o.Options)
}
if o.SelectIfOne && len(matches) == 1 {
tty.Println(matches[0].Str)
return nil
}
km := defaultKeymap()
if o.NoLimit || o.Limit > 1 {
km.Toggle.SetEnabled(true)
km.ToggleAndPrevious.SetEnabled(true)
km.ToggleAndNext.SetEnabled(true)
km.ToggleAll.SetEnabled(true)
}
top, right, bottom, left := style.ParsePadding(o.Padding)
m := model{
choices: choices,
filteringChoices: filteringChoices,
indicator: o.Indicator,
matches: matches,
header: o.Header,
textinput: i,
viewport: &v,
indicatorStyle: o.IndicatorStyle.ToLipgloss(),
selectedPrefixStyle: o.SelectedPrefixStyle.ToLipgloss(),
selectedPrefix: o.SelectedPrefix,
unselectedPrefixStyle: o.UnselectedPrefixStyle.ToLipgloss(),
unselectedPrefix: o.UnselectedPrefix,
matchStyle: o.MatchStyle.ToLipgloss(),
headerStyle: o.HeaderStyle.ToLipgloss(),
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,
fuzzy: o.Fuzzy,
sort: o.Sort && o.FuzzySort,
strict: o.Strict,
showHelp: o.ShowHelp,
keymap: km,
help: help.New(),
}
isSelectAll := len(o.Selected) == 1 && o.Selected[0] == "*"
currentSelected := 0
if len(o.Selected) > 0 {
for i, option := range matches {
if currentSelected >= o.Limit || (!isSelectAll && !slices.Contains(o.Selected, option.Str)) {
continue
}
if o.Limit == 1 {
m.cursor = i
m.selected[option.Str] = struct{}{}
} else {
currentSelected++
m.selected[option.Str] = struct{}{}
}
}
}
tm, err := tea.NewProgram(m, options...).Run()
if err != nil {
return fmt.Errorf("unable to run filter: %w", err)
}
m = tm.(model)
if !m.submitted {
return errors.New("nothing selected")
}
// allSelections contains values only if limit is greater
// than 1 or if flag --no-limit is passed, hence there is
// no need to further checks
if len(m.selected) > 0 {
o.checkSelected(m)
} else if len(m.matches) > m.cursor && m.cursor >= 0 {
tty.Println(m.matches[m.cursor].Str)
}
return nil
}
func (o Options) checkSelected(m model) {
out := []string{}
for k := range m.selected {
out = append(out, k)
}
tty.Println(strings.Join(out, o.OutputDelimiter))
}
================================================
FILE: filter/filter.go
================================================
// Package filter provides a fuzzy searching text input to allow filtering a
// list of options to select one option.
//
// By default it will list all the files (recursively) in the current directory
// for the user to choose one, but the script (or user) can provide different
// new-line separated options to choose from.
//
// I.e. let's pick from a list of gum flavors:
//
// $ cat flavors.text | gum filter
package filter
import (
"strings"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
"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"
)
func defaultKeymap() keymap {
return keymap{
Down: key.NewBinding(
key.WithKeys("down", "ctrl+j", "ctrl+n"),
),
Up: key.NewBinding(
key.WithKeys("up", "ctrl+k", "ctrl+p"),
),
NDown: key.NewBinding(
key.WithKeys("j"),
),
NUp: key.NewBinding(
key.WithKeys("k"),
),
Home: key.NewBinding(
key.WithKeys("g", "home"),
),
End: key.NewBinding(
key.WithKeys("G", "end"),
),
ToggleAndNext: key.NewBinding(
key.WithKeys("tab"),
key.WithHelp("tab", "toggle"),
key.WithDisabled(),
),
ToggleAndPrevious: key.NewBinding(
key.WithKeys("shift+tab"),
key.WithHelp("shift+tab", "toggle"),
key.WithDisabled(),
),
Toggle: key.NewBinding(
key.WithKeys("ctrl+@"),
key.WithHelp("ctrl+@", "toggle"),
key.WithDisabled(),
),
ToggleAll: key.NewBinding(
key.WithKeys("ctrl+a"),
key.WithHelp("ctrl+a", "select all"),
key.WithDisabled(),
),
FocusInSearch: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("/", "search"),
),
FocusOutSearch: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "blur search"),
),
Quit: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "quit"),
),
Abort: key.NewBinding(
key.WithKeys("ctrl+c"),
key.WithHelp("ctrl+c", "abort"),
),
Submit: key.NewBinding(
key.WithKeys("enter", "ctrl+q"),
key.WithHelp("enter", "submit"),
),
}
}
type keymap struct {
FocusInSearch,
FocusOutSearch,
Down,
Up,
NDown,
NUp,
Home,
End,
ToggleAndNext,
ToggleAndPrevious,
ToggleAll,
Toggle,
Abort,
Quit,
Submit key.Binding
}
// FullHelp implements help.KeyMap.
func (k keymap) FullHelp() [][]key.Binding { return nil }
// ShortHelp implements help.KeyMap.
func (k keymap) ShortHelp() []key.Binding {
return []key.Binding{
key.NewBinding(
key.WithKeys("up", "down"),
key.WithHelp("↓↑", "navigate"),
),
k.FocusInSearch,
k.FocusOutSearch,
k.ToggleAndNext,
k.ToggleAll,
k.Submit,
}
}
type model struct {
textinput textinput.Model
viewport *viewport.Model
choices map[string]string
filteringChoices []string
matches []fuzzy.Match
cursor int
header string
selected map[string]struct{}
limit int
numSelected int
indicator string
selectedPrefix string
unselectedPrefix string
height int
padding []int
quitting bool
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
showHelp bool
keymap keymap
help help.Model
strict bool
submitted bool
}
func (m model) Init() tea.Cmd { return textinput.Blink }
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
// bottom edge of the viewport instead of in the middle.
if m.reverse && len(m.matches) < m.viewport.Height {
s.WriteString(strings.Repeat("\n", m.viewport.Height-len(m.matches)))
}
// Since there are matches, display them so that the user can see, in real
// time, what they are searching for.
last := len(m.matches) - 1
for i := range m.matches {
// For reverse layout, the matches are displayed in reverse order.
if m.reverse {
i = last - i
}
match := m.matches[i]
// 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
if _, ok := m.selected[match.Str]; ok {
s.WriteString(m.selectedPrefixStyle.Render(m.selectedPrefix))
} else if m.limit > 1 {
s.WriteString(m.unselectedPrefixStyle.Render(m.unselectedPrefix))
} else {
s.WriteString(" ")
}
styledOption := m.choices[match.Str]
if len(match.MatchedIndexes) == 0 {
// No matches, just render the text.
s.WriteString(lineTextStyle.Render(styledOption))
s.WriteRune('\n')
continue
}
var ranges []lipgloss.Range
for _, rng := range matchedRanges(match.MatchedIndexes) {
// ansi.Cut is grapheme and ansi sequence aware, we match against a ansi.Stripped string, but we might still have graphemes.
// all that to say that rng is byte positions, but we need to pass it down to ansi.Cut as char positions.
// so we need to adjust it here:
start, stop := bytePosToVisibleCharPos(match.Str, rng)
ranges = append(ranges, lipgloss.NewRange(start, stop+1, m.matchStyle))
}
s.WriteString(lineTextStyle.Render(lipgloss.StyleRanges(styledOption, ranges...)))
// We have finished displaying the match with all of it's matched
// characters highlighted and the rest filled in.
// Move on to the next match.
s.WriteRune('\n')
}
m.viewport.SetContent(s.String())
// View the input and the filtered choices
header := m.headerStyle.Render(m.header)
if m.reverse {
view := m.viewport.View()
if m.header != "" {
view += "\n" + header
}
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 += m.helpView()
}
if m.header != "" {
return lipgloss.NewStyle().
Padding(m.padding...).
Render(header + "\n" + view)
}
return lipgloss.NewStyle().
Padding(m.padding...).
Render(view)
}
func (m model) helpView() string {
return "\n\n" + m.help.View(m.keymap)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd, icmd tea.Cmd
m.textinput, icmd = m.textinput.Update(msg)
switch msg := msg.(type) {
case tea.WindowSizeMsg:
if m.height == 0 || m.height > msg.Height {
m.viewport.Height = msg.Height - lipgloss.Height(m.textinput.View())
}
// Include the header in the height calculation.
if m.header != "" {
m.viewport.Height = m.viewport.Height - lipgloss.Height(m.headerStyle.Render(m.header))
}
// Include the help in the total height calculation.
if m.showHelp {
m.viewport.Height = m.viewport.Height - lipgloss.Height(m.helpView())
}
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 = ordered.Clamp(len(m.matches)-m.viewport.Height, 0, len(m.matches))
}
case tea.KeyMsg:
km := m.keymap
switch {
case key.Matches(msg, km.FocusInSearch):
m.textinput.Focus()
case key.Matches(msg, km.FocusOutSearch):
m.textinput.Blur()
case key.Matches(msg, km.Quit):
m.quitting = true
return m, tea.Quit
case key.Matches(msg, km.Abort):
m.quitting = true
return m, tea.Interrupt
case key.Matches(msg, km.Submit):
m.quitting = true
m.submitted = true
return m, tea.Quit
case key.Matches(msg, km.Down, km.NDown):
m.CursorDown()
case key.Matches(msg, km.Up, km.NUp):
m.CursorUp()
case key.Matches(msg, km.Home):
m.cursor = 0
m.viewport.GotoTop()
case key.Matches(msg, km.End):
m.cursor = len(m.choices) - 1
m.viewport.GotoBottom()
case key.Matches(msg, km.ToggleAndNext):
if m.limit == 1 {
break // no op
}
m.ToggleSelection()
m.CursorDown()
case key.Matches(msg, km.ToggleAndPrevious):
if m.limit == 1 {
break // no op
}
m.ToggleSelection()
m.CursorUp()
case key.Matches(msg, km.Toggle):
if m.limit == 1 {
break // no op
}
m.ToggleSelection()
case key.Matches(msg, km.ToggleAll):
if m.limit <= 1 {
break
}
if m.numSelected < len(m.matches) && m.numSelected < m.limit {
m = m.selectAll()
} else {
m = m.deselectAll()
}
default:
// yOffsetFromBottom is the number of lines from the bottom of the
// list to the top of the viewport. This is used to keep the viewport
// at a constant position when the number of matches are reduced
// in the reverse layout.
var yOffsetFromBottom int
if m.reverse {
yOffsetFromBottom = max(0, len(m.matches)-m.viewport.YOffset)
}
// A character was entered, this likely means that the text input has
// changed. This suggests that the matches are outdated, so update them.
var choices []string
if !m.strict {
choices = append(choices, m.textinput.Value())
}
choices = append(choices, m.filteringChoices...)
if m.fuzzy {
if m.sort {
m.matches = fuzzy.Find(m.textinput.Value(), choices)
} else {
m.matches = fuzzy.FindNoSort(m.textinput.Value(), choices)
}
} else {
m.matches = exactMatches(m.textinput.Value(), choices)
}
// If the search field is empty, let's not display the matches
// (none), but rather display all possible choices.
if m.textinput.Value() == "" {
m.matches = matchAll(m.filteringChoices)
}
// For reverse layout, we need to offset the viewport so that the
// 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 = ordered.Clamp(len(m.matches)-yOffsetFromBottom, 0, maxYOffset)
}
}
}
m.keymap.FocusInSearch.SetEnabled(!m.textinput.Focused())
m.keymap.FocusOutSearch.SetEnabled(m.textinput.Focused())
m.keymap.NUp.SetEnabled(!m.textinput.Focused())
m.keymap.NDown.SetEnabled(!m.textinput.Focused())
m.keymap.Home.SetEnabled(!m.textinput.Focused())
m.keymap.End.SetEnabled(!m.textinput.Focused())
// 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 = ordered.Clamp(m.cursor, 0, len(m.matches)-1)
return m, tea.Batch(cmd, icmd)
}
func (m *model) CursorUp() {
if len(m.matches) == 0 {
return
}
if m.reverse { //nolint:nestif
m.cursor = (m.cursor + 1) % len(m.matches)
if len(m.matches)-m.cursor <= m.viewport.YOffset {
m.viewport.ScrollUp(1)
}
if len(m.matches)-m.cursor > m.viewport.Height+m.viewport.YOffset {
m.viewport.SetYOffset(len(m.matches) - m.viewport.Height)
}
} else {
m.cursor = (m.cursor - 1 + len(m.matches)) % len(m.matches)
if m.cursor < m.viewport.YOffset {
m.viewport.ScrollUp(1)
}
if m.cursor >= m.viewport.YOffset+m.viewport.Height {
m.viewport.SetYOffset(len(m.matches) - m.viewport.Height)
}
}
}
func (m *model) CursorDown() {
if len(m.matches) == 0 {
return
}
if m.reverse { //nolint:nestif
m.cursor = (m.cursor - 1 + len(m.matches)) % len(m.matches)
if len(m.matches)-m.cursor > m.viewport.Height+m.viewport.YOffset {
m.viewport.ScrollDown(1)
}
if len(m.matches)-m.cursor <= m.viewport.YOffset {
m.viewport.GotoTop()
}
} else {
m.cursor = (m.cursor + 1) % len(m.matches)
if m.cursor >= m.viewport.YOffset+m.viewport.Height {
m.viewport.ScrollDown(1)
}
if m.cursor < m.viewport.YOffset {
m.viewport.GotoTop()
}
}
}
func (m *model) ToggleSelection() {
if _, ok := m.selected[m.matches[m.cursor].Str]; ok {
delete(m.selected, m.matches[m.cursor].Str)
m.numSelected--
} else if m.numSelected < m.limit {
m.selected[m.matches[m.cursor].Str] = struct{}{}
m.numSelected++
}
}
func (m model) selectAll() model {
for i := range m.matches {
if m.numSelected >= m.limit {
break // do not exceed given limit
}
if _, ok := m.selected[m.matches[i].Str]; ok {
continue
}
m.selected[m.matches[i].Str] = struct{}{}
m.numSelected++
}
return m
}
func (m model) deselectAll() model {
m.selected = make(map[string]struct{})
m.numSelected = 0
return m
}
func matchAll(options []string) []fuzzy.Match {
matches := make([]fuzzy.Match, len(options))
for i, option := range options {
matches[i] = fuzzy.Match{Str: option}
}
return matches
}
func exactMatches(search string, choices []string) []fuzzy.Match {
matches := fuzzy.Matches{}
for i, choice := range choices {
search = strings.ToLower(search)
matchedString := strings.ToLower(choice)
index := strings.Index(matchedString, search)
if index >= 0 {
matchedIndexes := []int{}
for s := range search {
matchedIndexes = append(matchedIndexes, index+s)
}
matches = append(matches, fuzzy.Match{
Str: choice,
Index: i,
MatchedIndexes: matchedIndexes,
})
}
}
return matches
}
func matchedRanges(in []int) [][2]int {
if len(in) == 0 {
return [][2]int{}
}
current := [2]int{in[0], in[0]}
if len(in) == 1 {
return [][2]int{current}
}
var out [][2]int
for i := 1; i < len(in); i++ {
if in[i] == current[1]+1 {
current[1] = in[i]
} else {
out = append(out, current)
current = [2]int{in[i], in[i]}
}
}
out = append(out, current)
return out
}
func bytePosToVisibleCharPos(str string, rng [2]int) (int, int) {
bytePos, byteStart, byteStop := 0, rng[0], rng[1]
pos, start, stop := 0, 0, 0
gr := uniseg.NewGraphemes(str)
for byteStart > bytePos {
if !gr.Next() {
break
}
bytePos += len(gr.Str())
pos += max(1, gr.Width())
}
start = pos
for byteStop > bytePos {
if !gr.Next() {
break
}
bytePos += len(gr.Str())
pos += max(1, gr.Width())
}
stop = pos
return start, stop
}
================================================
FILE: filter/filter_test.go
================================================
package filter
import (
"reflect"
"testing"
"github.com/charmbracelet/x/ansi"
)
func TestMatchedRanges(t *testing.T) {
for name, tt := range map[string]struct {
in []int
out [][2]int
}{
"empty": {
in: []int{},
out: [][2]int{},
},
"one char": {
in: []int{1},
out: [][2]int{{1, 1}},
},
"2 char range": {
in: []int{1, 2},
out: [][2]int{{1, 2}},
},
"multiple char range": {
in: []int{1, 2, 3, 4, 5, 6},
out: [][2]int{{1, 6}},
},
"multiple char ranges": {
in: []int{1, 2, 3, 5, 6, 10, 11, 12, 13, 23, 24, 40, 42, 43, 45, 52},
out: [][2]int{{1, 3}, {5, 6}, {10, 13}, {23, 24}, {40, 40}, {42, 43}, {45, 45}, {52, 52}},
},
} {
t.Run(name, func(t *testing.T) {
match := matchedRanges(tt.in)
if !reflect.DeepEqual(match, tt.out) {
t.Errorf("expected %v, got %v", tt.out, match)
}
})
}
}
func TestByteToChar(t *testing.T) {
stStr := "\x1b[90m\ue615\x1b[39m \x1b[3m\x1b[32mDow\x1b[0m\x1b[90m\x1b[39m\x1b[3wnloads"
str := " Downloads"
rng := [2]int{4, 7}
expect := "Dow"
if got := str[rng[0]:rng[1]]; got != expect {
t.Errorf("expected %q, got %q", expect, got)
}
start, stop := bytePosToVisibleCharPos(str, rng)
if got := ansi.Strip(ansi.Cut(stStr, start, stop)); got != expect {
t.Errorf("expected %+q, got %+q", expect, got)
}
}
================================================
FILE: filter/options.go
================================================
package filter
import (
"time"
"github.com/charmbracelet/gum/style"
)
// Options is the customization options for the filter command.
type Options struct {
Options []string `arg:"" optional:"" help:"Options to filter."`
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"`
SelectIfOne bool `help:"Select the given option if there is only one" group:"Selection"`
Selected []string `help:"Options that should start as selected (selects all if given *)" default:"" env:"GUM_FILTER_SELECTED"`
ShowHelp bool `help:"Show help keybinds" default:"true" negatable:"" env:"GUM_FILTER_SHOW_HELP"`
Strict bool `help:"Only returns if anything matched. Otherwise return Filter" negatable:"" 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=99" 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_"`
PlaceholderStyle style.Styles `embed:"" prefix:"placeholder." set:"defaultForeground=240" envprefix:"GUM_FILTER_PLACEHOLDER_"`
Width int `help:"Input width" default:"0" 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; otherwise match from start of word" default:"true" env:"GUM_FILTER_FUZZY" negatable:""`
FuzzySort bool `help:"Sort fuzzy results by their scores" default:"true" env:"GUM_FILTER_FUZZY_SORT" negatable:""`
Timeout time.Duration `help:"Timeout until filter command aborts" default:"0s" env:"GUM_FILTER_TIMEOUT"`
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:""`
}
================================================
FILE: flake.nix
================================================
{
description = "A tool for glamorous shell scripts";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let pkgs = import nixpkgs { inherit system; }; in
rec {
packages.default = import ./default.nix { inherit pkgs; };
}) // {
overlays.default = final: prev: {
gum = import ./default.nix { pkgs = final; };
};
};
}
================================================
FILE: format/README.md
================================================
# Gum Format
Gum format allows you to format different text into human readable output.
Four different parse-able formats exist:
1. [Markdown](#markdown)
2. [Code](#code)
3. [Template](#template)
4. [Emoji](#emoji)
## Markdown
Render any input as markdown text. This uses
[Glamour](https://github.com/charmbracelet/glamour) behind the scenes.
You can pass input as lines directly as arguments to the command invocation or
pass markdown over `stdin`.
```bash
gum format --type markdown < README.md
# Or, directly as arguments (useful for quick lists)
gum format --type markdown -- "# Gum Formats" "- Markdown" "- Code" "- Template" "- Emoji"
```
## Code
Render any code snippet with syntax highlighting.
[Glamour](https://github.com/charmbracelet/glamour), which uses
[Chroma](https://github.com/alecthomas/chroma) under the hood, handles styling.
Similarly to the `markdown` format, `code` can take input over `stdin`.
```bash
cat options.go | gum format --type code
```
## Template
Render styled input from a string template. Templating is handled by
[Termenv](https://github.com/muesli/termenv).
```bash
gum format --type template '{{ Bold "Tasty" }} {{ Italic "Bubble" }} {{ Color "99" "0" " Gum " }}'
# Or, via stdin
echo '{{ Bold "Tasty" }} {{ Italic "Bubble" }} {{ Color "99" "0" " Gum " }}' | gum format --type template
```
## Emoji
Parse and render emojis from their matching `:name:`s. Powered by
[Glamour](https://github.com/charmbracelet/glamour) and [Goldmark
Emoji](https://github.com/yuin/goldmark-emoji)
```bash
gum format --type emoji 'I :heart: Bubble Gum :candy:'
# You know the drill, also via stdin
echo 'I :heart: Bubble Gum :candy:' | gum format --type emoji
```
## Tables
Tables are rendered using [Glamour](https://github.com/charmbracelet/glamour).
| Bubble Gum Flavor | Price |
| ----------------- | ----- |
| Strawberry | $0.99 |
| Cherry | $0.50 |
| Banana | $0.75 |
| Orange | $0.25 |
| Lemon | $0.50 |
| Lime | $0.50 |
| Grape | $0.50 |
| Watermelon | $0.50 |
| Pineapple | $0.50 |
| Blueberry | $0.50 |
| Raspberry | $0.50 |
| Cranberry | $0.50 |
| Peach | $0.50 |
| Apple | $0.50 |
| Mango | $0.50 |
| Pomegranate | $0.50 |
| Coconut | $0.50 |
| Cinnamon | $0.50 |
================================================
FILE: format/command.go
================================================
// Package format allows you to render formatted text from the command line.
//
// It supports the following types:
//
// 1. Markdown
// 2. Code
// 3. Emoji
// 4. Template
//
// For more information, see the format/README.md file.
package format
import (
"fmt"
"strings"
"github.com/charmbracelet/gum/internal/stdin"
)
// Run runs the format command.
func (o Options) Run() error {
var input, output string
var err error
if len(o.Template) > 0 {
input = strings.Join(o.Template, "\n")
} else {
input, _ = stdin.Read(stdin.StripANSI(o.StripANSI))
}
switch o.Type {
case "code":
output, err = code(input, o.Language)
case "emoji":
output, err = emoji(input)
case "template":
output, err = template(input)
default:
output, err = markdown(input, o.Theme)
}
if err != nil {
return err
}
fmt.Print(output)
return nil
}
================================================
FILE: format/formats.go
================================================
package format
import (
"bytes"
"fmt"
tpl "text/template"
"github.com/charmbracelet/glamour"
"github.com/muesli/termenv"
)
func code(input, language string) (string, error) {
renderer, err := glamour.NewTermRenderer(
glamour.WithAutoStyle(),
glamour.WithWordWrap(0),
)
if err != nil {
return "", fmt.Errorf("unable to create renderer: %w", err)
}
output, err := renderer.Render(fmt.Sprintf("```%s\n%s\n```", language, input))
if err != nil {
return "", fmt.Errorf("unable to render: %w", err)
}
return output, nil
}
func emoji(input string) (string, error) {
renderer, err := glamour.NewTermRenderer(
glamour.WithEmoji(),
)
if err != nil {
return "", fmt.Errorf("unable to create renderer: %w", err)
}
output, err := renderer.Render(input)
if err != nil {
return "", fmt.Errorf("unable to render: %w", err)
}
return output, nil
}
func markdown(input string, theme string) (string, error) {
renderer, err := glamour.NewTermRenderer(
glamour.WithStylePath(theme),
glamour.WithWordWrap(0),
)
if err != nil {
return "", fmt.Errorf("unable to render: %w", err)
}
output, err := renderer.Render(input)
if err != nil {
return "", fmt.Errorf("unable to render: %w", err)
}
return output, nil
}
func template(input string) (string, error) {
f := termenv.TemplateFuncs(termenv.ANSI256)
t, err := tpl.New("tpl").Funcs(f).Parse(input)
if err != nil {
return "", fmt.Errorf("unable to parse template: %w", err)
}
var buf bytes.Buffer
err = t.Execute(&buf, nil)
return buf.String(), err
}
================================================
FILE: format/options.go
================================================
package format
// Options is customization options for the format command.
type Options struct {
Template []string `arg:"" optional:"" help:"Template string to format (can also be provided via stdin)"`
Theme string `help:"Glamour theme to use for markdown formatting" default:"pink" env:"GUM_FORMAT_THEME"`
Language string `help:"Programming language to parse code" short:"l" default:"" env:"GUM_FORMAT_LANGUAGE"`
StripANSI bool `help:"Strip ANSI sequences when reading from STDIN" default:"true" negatable:"" env:"GUM_FORMAT_STRIP_ANSI"`
Type string `help:"Format to use (markdown,template,code,emoji)" enum:"markdown,template,code,emoji" short:"t" default:"markdown" env:"GUM_FORMAT_TYPE"`
}
================================================
FILE: go.mod
================================================
module github.com/charmbracelet/gum
go 1.24.2
require (
github.com/Masterminds/semver/v3 v3.4.0
github.com/alecthomas/kong v1.14.0
github.com/alecthomas/mango-kong v0.1.0
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/charmbracelet/log v0.4.2
github.com/charmbracelet/x/ansi v0.11.6
github.com/charmbracelet/x/editor v0.2.0
github.com/charmbracelet/x/exp/ordered v0.1.0
github.com/charmbracelet/x/term v0.2.2
github.com/charmbracelet/x/xpty v0.1.3
github.com/muesli/roff v0.1.0
github.com/muesli/termenv v0.16.0
github.com/rivo/uniseg v0.4.7
github.com/sahilm/fuzzy v0.1.1
golang.org/x/text v0.34.0
)
require (
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/conpty v0.1.1 // indirect
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/creack/pty v1.1.24 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/mango v0.2.0 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.8 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.32.0 // indirect
)
================================================
FILE: go.sum
================================================
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/kong v1.14.0 h1:gFgEUZWu2ZmZ+UhyZ1bDhuutbKN1nTtJTwh19Wsn21s=
github.com/alecthomas/kong v1.14.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
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.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
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 v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
github.com/charmbracelet/x/editor v0.2.0 h1:7XLUKtaRaB8jN7bWU2p2UChiySyaAuIfYiIRg8gGWwk=
github.com/charmbracelet/x/editor v0.2.0/go.mod h1:p3oQ28TSL3YPd+GKJ1fHWcp+7bVGpedHpXmo0D6t1dY=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
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.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
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.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
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.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
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.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: gum.go
================================================
package main
import (
"github.com/alecthomas/kong"
"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"
"github.com/charmbracelet/gum/join"
"github.com/charmbracelet/gum/log"
"github.com/charmbracelet/gum/man"
"github.com/charmbracelet/gum/pager"
"github.com/charmbracelet/gum/spin"
"github.com/charmbracelet/gum/style"
"github.com/charmbracelet/gum/table"
"github.com/charmbracelet/gum/version"
"github.com/charmbracelet/gum/write"
)
// Gum is the command-line interface for Gum.
type Gum struct {
// Version is a flag that can be used to display the version number.
Version kong.VersionFlag `short:"v" help:"Print the version number"`
// Completion generates Gum shell completion scripts.
Completion completion.Completion `cmd:"" hidden:"" help:"Request shell completion"`
// Man is a hidden command that generates Gum man pages.
Man man.Man `cmd:"" hidden:"" help:"Generate man pages"`
// Choose provides an interface to choose one option from a given list of
// options. The options can be provided as (new-line separated) stdin or a
// list of arguments.
//
// It is different from the filter command as it does not provide a fuzzy
// finding input, so it is best used for smaller lists of options.
//
// Let's pick from a list of gum flavors:
//
// $ gum choose "Strawberry" "Banana" "Cherry"
//
Choose choose.Options `cmd:"" help:"Choose an option from a list of choices"`
// Confirm provides an interface to ask a user to confirm an action.
// The user is provided with an interface to choose an affirmative or
// negative answer, which is then reflected in the exit code for use in
// scripting.
//
// If the user selects the affirmative answer, the program exits with 0.
// If the user selects the negative answer, the program exits with 1.
//
// I.e. confirm if the user wants to delete a file
//
// $ gum confirm "Are you sure?" && rm file.txt
//
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.
//
// By default it will list all the files (recursively) in the current directory
// for the user to choose one, but the script (or user) can provide different
// new-line separated options to choose from.
//
// I.e. let's pick from a list of gum flavors:
//
// $ cat flavors.text | gum filter
//
Filter filter.Options `cmd:"" help:"Filter items from a list"`
// Format allows you to render styled text from `markdown`, `code`,
// `template` strings, or embedded `emoji` strings.
// For more information see the format/README.md file.
Format format.Options `cmd:"" help:"Format a string using a template"`
// Input provides a shell script interface for the text input bubble.
// https://github.com/charmbracelet/bubbles/tree/master/textinput
//
// It can be used to prompt the user for some input. The text the user
// entered will be sent to stdout.
//
// $ gum input --placeholder "What's your favorite gum?" > answer.text
//
Input input.Options `cmd:"" help:"Prompt for some input"`
// Join provides a shell script interface for the lipgloss JoinHorizontal
// and JoinVertical commands. It allows you to join multi-line text to
// build different layouts.
//
// For example, you can place two bordered boxes next to each other:
// Note: We wrap the variable in quotes to ensure the new lines are part of a
// single argument. Otherwise, the command won't work as expected.
//
// $ gum join --horizontal "$BUBBLE_BOX" "$GUM_BOX"
//
// ╔══════════════════════╗╔═════════════╗
// ║ ║║ ║
// ║ Bubble ║║ Gum ║
// ║ ║║ ║
// ╚══════════════════════╝╚═════════════╝
//
Join join.Options `cmd:"" help:"Join text vertically or horizontally"`
// Pager provides a shell script interface for the viewport bubble.
// 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.
// https://github.com/charmbracelet/bubbles/tree/master/spinner
//
// It is useful for displaying that some task is running in the background
// while consuming it's output so that it is not shown to the user.
//
// For example, let's do a long running task: $ sleep 5
//
// We can simply prepend a spinner to this task to show it to the user,
// while performing the task / command in the background.
//
// $ gum spin -t "Taking a nap..." -- sleep 5
//
// The spinner will automatically exit when the task is complete.
//
Spin spin.Options `cmd:"" help:"Display spinner while running a command"`
// Style provides a shell script interface for Lip Gloss.
// https://github.com/charmbracelet/lipgloss
//
// It allows you to use Lip Gloss to style text without needing to use Go.
// All of the styling options are available as flags.
//
// Let's make some text glamorous using bash:
//
// $ gum style \
// --foreground 212 --border double --align center \
// --width 50 --margin 2 --padding "2 4" \
// "Bubble Gum (1¢)" "So sweet and so fresh\!"
//
//
// ╔══════════════════════════════════════════════════╗
// ║ ║
// ║ ║
// ║ Bubble Gum (1¢) ║
// ║ So sweet and so fresh! ║
// ║ ║
// ║ ║
// ╚══════════════════════════════════════════════════╝
//
Style style.Options `cmd:"" help:"Apply coloring, borders, spacing to text"`
// Table provides a shell script interface for the table bubble.
// https://github.com/charmbracelet/bubbles/tree/master/table
//
// It is useful to render tabular (CSV) data in a terminal and allows
// the user to select a row from the table.
//
// Let's render a table of gum flavors:
//
// $ gum table <<< "Flavor,Price\nStrawberry,$0.50\nBanana,$0.99\nCherry,$0.75"
//
// Flavor Price
// Strawberry $0.50
// Banana $0.99
// Cherry $0.75
//
Table table.Options `cmd:"" help:"Render a table of data"`
// Write provides a shell script interface for the text area bubble.
// https://github.com/charmbracelet/bubbles/tree/master/textarea
//
// It can be used to ask the user to write some long form of text
// (multi-line) input. The text the user entered will be sent to stdout.
//
// $ gum write > output.text
//
Write write.Options `cmd:"" help:"Prompt for long-form text"`
// Log provides a shell script interface for logging using Log.
// https://github.com/charmbracelet/log
//
// It can be used to log messages to output.
//
// $ gum log --level info "Hello, world!"
//
Log log.Options `cmd:"" help:"Log messages to output"`
// VersionCheck provides a command that checks if the current gum version
// matches a given semantic version constraint.
//
// It can be used to check that a minimum gum version is installed in a
// script.
//
// $ gum version-check '~> 0.15'
//
VersionCheck version.Options `cmd:"" help:"Semver check current gum version"`
}
================================================
FILE: input/command.go
================================================
package input
import (
"errors"
"fmt"
"os"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/cursor"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/internal/timeout"
"github.com/charmbracelet/gum/style"
)
// Run provides a shell script interface for the text input bubble.
// https://github.com/charmbracelet/bubbles/textinput
func (o Options) Run() error {
if o.Value == "" {
if in, _ := stdin.Read(stdin.StripANSI(o.StripANSI)); in != "" {
o.Value = in
}
}
i := textinput.New()
if o.Value != "" {
i.SetValue(o.Value)
} else if in, _ := stdin.Read(stdin.StripANSI(o.StripANSI)); in != "" {
i.SetValue(in)
}
i.Focus()
i.Prompt = o.Prompt
i.Placeholder = o.Placeholder
i.Width = o.Width
i.PromptStyle = o.PromptStyle.ToLipgloss()
i.PlaceholderStyle = o.PlaceholderStyle.ToLipgloss()
i.Cursor.Style = o.CursorStyle.ToLipgloss()
i.Cursor.SetMode(cursor.Modes[o.CursorMode])
i.CharLimit = o.CharLimit
if o.Password {
i.EchoMode = textinput.EchoPassword
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(),
keymap: defaultKeymap(),
}
ctx, cancel := timeout.Context(o.Timeout)
defer cancel()
p := tea.NewProgram(
m,
tea.WithOutput(os.Stderr),
tea.WithReportFocus(),
tea.WithContext(ctx),
)
tm, err := p.Run()
if err != nil {
return fmt.Errorf("failed to run input: %w", err)
}
m = tm.(model)
if !m.submitted {
return errors.New("not submitted")
}
fmt.Println(m.textinput.Value())
return nil
}
================================================
FILE: input/input.go
================================================
// Package input provides a shell script interface for the text input bubble.
// https://github.com/charmbracelet/bubbles/tree/master/textinput
//
// It can be used to prompt the user for some input. The text the user entered
// will be sent to stdout.
//
// $ gum input --placeholder "What's your favorite gum?" > answer.text
package input
import (
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type keymap textinput.KeyMap
func defaultKeymap() keymap {
k := textinput.DefaultKeyMap
return keymap(k)
}
// FullHelp implements help.KeyMap.
func (k keymap) FullHelp() [][]key.Binding { return nil }
// ShortHelp implements help.KeyMap.
func (k keymap) ShortHelp() []key.Binding {
return []key.Binding{
key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "submit"),
),
}
}
type model struct {
autoWidth bool
header string
padding []int
headerStyle lipgloss.Style
textinput textinput.Model
quitting bool
submitted bool
showHelp bool
help help.Model
keymap keymap
}
func (m model) Init() tea.Cmd { return textinput.Blink }
func (m model) View() string {
if m.quitting {
return ""
}
var parts []string
if m.header != "" {
parts = append(parts, m.headerStyle.Render(m.header))
}
parts = append(parts, m.textinput.View())
if m.showHelp {
parts = append(parts, "", 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 - 1 -
lipgloss.Width(m.textinput.Prompt) -
m.padding[1] - m.padding[3]
}
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
m.quitting = true
return m, tea.Interrupt
case "esc":
m.quitting = true
return m, tea.Quit
case "enter":
m.quitting = true
m.submitted = true
return m, tea.Quit
}
}
var cmd tea.Cmd
m.textinput, cmd = m.textinput.Update(msg)
return m, cmd
}
================================================
FILE: input/options.go
================================================
package input
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_"`
PlaceholderStyle style.Styles `embed:"" prefix:"placeholder." set:"defaultForeground=240" envprefix:"GUM_INPUT_PLACEHOLDER_"`
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:"0" env:"GUM_INPUT_WIDTH"`
Password bool `help:"Mask input characters" default:"false"`
ShowHelp bool `help:"Show help keybinds" default:"true" negatable:"" env:"GUM_INPUT_SHOW_HELP"`
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:"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"`
}
================================================
FILE: internal/decode/align.go
================================================
// Package decode position strings to lipgloss.
package decode
import "github.com/charmbracelet/lipgloss"
// Align maps strings to `lipgloss.Position`s.
var Align = map[string]lipgloss.Position{
"center": lipgloss.Center,
"left": lipgloss.Left,
"top": lipgloss.Top,
"bottom": lipgloss.Bottom,
"right": lipgloss.Right,
}
================================================
FILE: internal/exit/exit.go
================================================
// Package exit code implementation.
package exit
import "strconv"
// StatusTimeout is the exit code for timed out commands.
const StatusTimeout = 124
// StatusAborted is the exit code for aborted commands.
const StatusAborted = 130
// ErrExit is a custom exit error.
type ErrExit int
// Error implements error.
func (e ErrExit) Error() string { return "exit " + strconv.Itoa(int(e)) }
================================================
FILE: internal/files/files.go
================================================
// Package files handles files.
package files
import (
"os"
"path/filepath"
"strings"
)
// List returns a list of all files in the current directory.
// It ignores the .git directory.
func List() []string {
var files []string
err := filepath.Walk(".",
func(path string, info os.FileInfo, err error) error {
if shouldIgnore(path) || info.IsDir() || err != nil {
return nil //nolint:nilerr
}
files = append(files, path)
return nil
})
if err != nil {
return []string{}
}
return files
}
var defaultIgnorePatterns = []string{"node_modules", ".git", "."}
func shouldIgnore(path string) bool {
for _, prefix := range defaultIgnorePatterns {
if strings.HasPrefix(path, prefix) {
return true
}
}
return false
}
================================================
FILE: internal/stdin/stdin.go
================================================
// Package stdin handles processing input from stdin.
package stdin
import (
"bufio"
"fmt"
"io"
"os"
"strings"
"github.com/charmbracelet/x/ansi"
)
type options struct {
ansiStrip bool
singleLine bool
}
// Option is a read option.
type Option func(*options)
// StripANSI optionally strips ansi sequences.
func StripANSI(b bool) Option {
return func(o *options) {
o.ansiStrip = b
}
}
// SingleLine reads a single line.
func SingleLine(b bool) Option {
return func(o *options) {
o.singleLine = b
}
}
// Read reads input from an stdin pipe.
func Read(opts ...Option) (string, error) {
if IsEmpty() {
return "", fmt.Errorf("stdin is empty")
}
options := options{}
for _, opt := range opts {
opt(&options)
}
reader := bufio.NewReader(os.Stdin)
var b strings.Builder
if options.singleLine {
line, _, err := reader.ReadLine()
if err != nil {
return "", fmt.Errorf("failed to read line: %w", err)
}
_, err = b.Write(line)
if err != nil {
return "", fmt.Errorf("failed to write: %w", err)
}
}
for !options.singleLine {
r, _, err := reader.ReadRune()
if err != nil && err == io.EOF {
break
}
_, err = b.WriteRune(r)
if err != nil {
return "", fmt.Errorf("failed to write rune: %w", err)
}
}
s := strings.TrimSpace(b.String())
if options.ansiStrip {
return ansi.Strip(s), nil
}
return s, nil
}
// IsEmpty returns whether stdin is empty.
func IsEmpty() bool {
stat, err := os.Stdin.Stat()
if err != nil {
return true
}
if stat.Mode()&os.ModeNamedPipe == 0 && stat.Size() == 0 {
return true
}
return false
}
================================================
FILE: internal/timeout/context.go
================================================
// Package timeout handles context timeouts.
package timeout
import (
"context"
"time"
)
// Context setup a new context that times out if the given timeout is > 0.
func Context(timeout time.Duration) (context.Context, context.CancelFunc) {
ctx := context.Background()
if timeout == 0 {
return ctx, func() {}
}
return context.WithTimeout(ctx, timeout)
}
================================================
FILE: internal/tty/tty.go
================================================
// Package tty provides tty-aware printing.
package tty
import (
"fmt"
"os"
"sync"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/term"
)
var isTTY = sync.OnceValue(func() bool {
return term.IsTerminal(os.Stdout.Fd())
})
// Println handles println, striping ansi sequences if stdout is not a tty.
func Println(s string) {
if isTTY() {
fmt.Println(s)
return
}
fmt.Println(ansi.Strip(s))
}
================================================
FILE: join/command.go
================================================
// Package join provides a shell script interface for the lipgloss
// JoinHorizontal and JoinVertical commands. It allows you to join multi-line
// text to build different layouts.
//
// For example, you can place two bordered boxes next to each other: Note: We
// wrap the variable in quotes to ensure the new lines are part of a single
// argument. Otherwise, the command won't work as expected.
//
// $ gum join --horizontal "$BUBBLE_BOX" "$GUM_BOX"
//
// ╔══════════════════════╗╔═════════════╗
// ║ ║║ ║
// ║ Bubble ║║ Gum ║
// ║ ║║ ║
// ╚══════════════════════╝╚═════════════╝
package join
import (
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/gum/internal/decode"
)
// Run is the command-line interface for the joining strings through lipgloss.
func (o Options) Run() error {
join := lipgloss.JoinHorizontal
if o.Vertical {
join = lipgloss.JoinVertical
}
fmt.Println(join(decode.Align[o.Align], o.Text...))
return nil
}
================================================
FILE: join/options.go
================================================
package join
// Options is the set of options that can configure a join.
type Options struct {
Text []string `arg:"" help:"Text to join."`
Align string `help:"Text alignment" enum:"left,center,right,bottom,middle,top" default:"left"`
Horizontal bool `help:"Join (potentially multi-line) strings horizontally"`
Vertical bool `help:"Join (potentially multi-line) strings vertically"`
}
================================================
FILE: log/command.go
================================================
// Package log the log command.
package log
import (
"fmt"
"math"
"os"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/log"
)
// Run is the command-line interface for logging text.
func (o Options) Run() error {
l := log.New(os.Stderr)
if o.File != "" {
f, err := os.OpenFile(o.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.ModePerm) //nolint:gosec
if err != nil {
return fmt.Errorf("error opening file: %w", err)
}
defer f.Close() //nolint:errcheck
l.SetOutput(f)
}
l.SetPrefix(o.Prefix)
l.SetLevel(-math.MaxInt32) // log all levels
l.SetReportTimestamp(o.Time != "")
if o.MinLevel != "" {
lvl, err := log.ParseLevel(o.MinLevel)
if err != nil {
return err //nolint:wrapcheck
}
l.SetLevel(lvl)
}
timeFormats := map[string]string{
"layout": time.Layout,
"ansic": time.ANSIC,
"unixdate": time.UnixDate,
"rubydate": time.RubyDate,
"rfc822": time.RFC822,
"rfc822z": time.RFC822Z,
"rfc850": time.RFC850,
"rfc1123": time.RFC1123,
"rfc1123z": time.RFC1123Z,
"rfc3339": time.RFC3339,
"rfc3339nano": time.RFC3339Nano,
"kitchen": time.Kitchen,
"stamp": time.Stamp,
"stampmilli": time.StampMilli,
"stampmicro": time.StampMicro,
"stampnano": time.StampNano,
"datetime": time.DateTime,
"dateonly": time.DateOnly,
"timeonly": time.TimeOnly,
}
tf, ok := timeFormats[strings.ToLower(o.Time)]
if ok {
l.SetTimeFormat(tf)
} else {
l.SetTimeFormat(o.Time)
}
st := log.DefaultStyles()
lvl := levelToLog[o.Level]
lvlStyle := o.LevelStyle.ToLipgloss()
if lvlStyle.GetForeground() == lipgloss.Color("") {
lvlStyle = lvlStyle.Foreground(st.Levels[lvl].GetForeground())
}
st.Levels[lvl] = lvlStyle.
SetString(strings.ToUpper(lvl.String())).
Inline(true)
st.Timestamp = o.TimeStyle.ToLipgloss().
Inline(true)
st.Prefix = o.PrefixStyle.ToLipgloss().
Inline(true)
st.Message = o.MessageStyle.ToLipgloss().
Inline(true)
st.Key = o.KeyStyle.ToLipgloss().
Inline(true)
st.Value = o.ValueStyle.ToLipgloss().
Inline(true)
st.Separator = o.SeparatorStyle.ToLipgloss().
Inline(true)
l.SetStyles(st)
switch o.Formatter {
case "json":
l.SetFormatter(log.JSONFormatter)
case "logfmt":
l.SetFormatter(log.LogfmtFormatter)
case "text":
l.SetFormatter(log.TextFormatter)
}
var arg0 string
var args []interface{}
if len(o.Text) > 0 {
arg0 = o.Text[0]
}
if len(o.Text) > 1 {
args = make([]interface{}, len(o.Text[1:]))
for i, arg := range o.Text[1:] {
args[i] = arg
}
}
logger := map[string]logger{
"none": {printf: l.Printf, print: l.Print},
"debug": {printf: l.Debugf, print: l.Debug},
"info": {printf: l.Infof, print: l.Info},
"warn": {printf: l.Warnf, print: l.Warn},
"error": {printf: l.Errorf, print: l.Error},
"fatal": {printf: l.Fatalf, print: l.Fatal},
}[o.Level]
if o.Format {
logger.printf(arg0, args...)
} else if o.Structured {
logger.print(arg0, args...)
} else {
logger.print(strings.Join(o.Text, " "))
}
return nil
}
type logger struct {
printf func(string, ...interface{})
print func(interface{}, ...interface{})
}
var levelToLog = map[string]log.Level{
"none": log.Level(math.MaxInt32),
"debug": log.DebugLevel,
"info": log.InfoLevel,
"warn": log.WarnLevel,
"error": log.ErrorLevel,
"fatal": log.FatalLevel,
}
================================================
FILE: log/options.go
================================================
package log
import (
"github.com/charmbracelet/gum/style"
)
// Options is the set of options that can configure a join.
type Options struct {
Text []string `arg:"" help:"Text to log"`
File string `short:"o" help:"Log to file"`
Format bool `short:"f" help:"Format message using printf" xor:"format,structured"`
Formatter string `help:"The log formatter to use" enum:"json,logfmt,text" default:"text"`
Level string `short:"l" help:"The log level to use" enum:"none,debug,info,warn,error,fatal" default:"none"`
Prefix string `help:"Prefix to print before the message"`
Structured bool `short:"s" help:"Use structured logging" xor:"format,structured"`
Time string `short:"t" help:"The time format to use (kitchen, layout, ansic, rfc822, etc...)" default:""`
MinLevel string `help:"Minimal level to show" default:"" env:"GUM_LOG_LEVEL"`
LevelStyle style.Styles `embed:"" prefix:"level." help:"The style of the level being used" set:"defaultBold=true" envprefix:"GUM_LOG_LEVEL_"`
TimeStyle style.Styles `embed:"" prefix:"time." help:"The style of the time" envprefix:"GUM_LOG_TIME_"`
PrefixStyle style.Styles `embed:"" prefix:"prefix." help:"The style of the prefix" set:"defaultBold=true" set:"defaultFaint=true" envprefix:"GUM_LOG_PREFIX_"` //nolint:staticcheck
MessageStyle style.Styles `embed:"" prefix:"message." help:"The style of the message" envprefix:"GUM_LOG_MESSAGE_"`
KeyStyle style.Styles `embed:"" prefix:"key." help:"The style of the key" set:"defaultFaint=true" envprefix:"GUM_LOG_KEY_"`
ValueStyle style.Styles `embed:"" prefix:"value." help:"The style of the value" envprefix:"GUM_LOG_VALUE_"`
SeparatorStyle style.Styles `embed:"" prefix:"separator." help:"The style of the separator" set:"defaultFaint=true" envprefix:"GUM_LOG_SEPARATOR_"`
}
================================================
FILE: main.go
================================================
// Package main is Gum: a tool for glamorous shell scripts.
package main
import (
"errors"
"fmt"
"os"
"runtime/debug"
"github.com/alecthomas/kong"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/exit"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
)
const shaLen = 7
var (
// Version contains the application version number. It's set via ldflags
// when building.
Version = ""
// CommitSHA contains the SHA of the commit that this application was built
// against. It's set via ldflags when building.
CommitSHA = ""
)
var bubbleGumPink = lipgloss.NewStyle().Foreground(lipgloss.Color("212"))
func main() {
lipgloss.SetColorProfile(termenv.NewOutput(os.Stderr).Profile)
if Version == "" {
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" {
Version = info.Main.Version
} else {
Version = "unknown (built from source)"
}
}
version := fmt.Sprintf("gum version %s", Version)
if len(CommitSHA) >= shaLen {
version += " (" + CommitSHA[:shaLen] + ")"
}
gum := &Gum{}
ctx := kong.Parse(
gum,
kong.Description(fmt.Sprintf("A tool for %s shell scripts.", bubbleGumPink.Render("glamorous"))),
kong.UsageOnError(),
kong.ConfigureHelp(kong.HelpOptions{
Compact: true,
Summary: false,
NoExpandSubcommands: true,
}),
kong.Vars{
"version": version,
"versionNumber": 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 {
var ex exit.ErrExit
if errors.As(err, &ex) {
os.Exit(int(ex))
}
if errors.Is(err, tea.ErrInterrupted) {
os.Exit(exit.StatusAborted)
}
if errors.Is(err, tea.ErrProgramKilled) {
fmt.Fprintln(os.Stderr, "timed out")
os.Exit(exit.StatusTimeout)
}
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
================================================
FILE: man/command.go
================================================
// Package man the man command.
package man
import (
"fmt"
"github.com/alecthomas/kong"
mangokong "github.com/alecthomas/mango-kong"
"github.com/muesli/roff"
)
// Man is a gum sub-command that generates man pages.
type Man struct{}
// BeforeApply implements Kong BeforeApply hook.
func (m Man) BeforeApply(ctx *kong.Context) error {
// Set the correct man pages description without color escape sequences.
ctx.Model.Help = "A tool for glamorous shell scripts."
man := mangokong.NewManPage(1, ctx.Model)
man = man.WithSection("Copyright", "(c) 2022-2024 Charmbracelet, Inc.\n"+
"Released under MIT license.")
_, _ = fmt.Fprint(ctx.Stdout, man.Build(roff.NewDocument()))
ctx.Exit(0)
return nil
}
================================================
FILE: pager/command.go
================================================
package pager
import (
"fmt"
"regexp"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/internal/timeout"
)
// Run provides a shell script interface for the viewport bubble.
// https://github.com/charmbracelet/bubbles/viewport
func (o Options) Run() error {
vp := viewport.New(o.Style.Width, o.Style.Height)
vp.Style = o.Style.ToLipgloss()
if o.Content == "" {
stdin, err := stdin.Read()
if err != nil {
return fmt.Errorf("unable to read stdin")
}
if stdin != "" {
// Sanitize the input from stdin by removing backspace sequences.
backspace := regexp.MustCompile(".\x08")
o.Content = backspace.ReplaceAllString(stdin, "")
} else {
return fmt.Errorf("provide some content to display")
}
}
m := model{
viewport: vp,
help: help.New(),
content: o.Content,
origContent: o.Content,
showLineNumbers: o.ShowLineNumbers,
lineNumberStyle: o.LineNumberStyle.ToLipgloss(),
softWrap: o.SoftWrap,
matchStyle: o.MatchStyle.ToLipgloss(),
matchHighlightStyle: o.MatchHighlightStyle.ToLipgloss(),
keymap: defaultKeymap(),
}
ctx, cancel := timeout.Context(o.Timeout)
defer cancel()
_, err := tea.NewProgram(
m,
tea.WithAltScreen(),
tea.WithReportFocus(),
tea.WithContext(ctx),
).Run()
if err != nil {
return fmt.Errorf("unable to start program: %w", err)
}
return nil
}
================================================
FILE: pager/options.go
================================================
package pager
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_"`
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:"true" negatable:""`
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:"0s" env:"GUM_PAGER_TIMEOUT"`
// Deprecated: this has no effect anymore.
HelpStyle style.Styles `embed:"" prefix:"help." help:"Style the help text" set:"defaultForeground=241" envprefix:"GUM_PAGER_HELP_" hidden:""`
}
================================================
FILE: pager/pager.go
================================================
// Package pager provides a pager (similar to less) for the terminal.
//
// $ cat file.txt | gum pager
package pager
import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
)
type keymap struct {
Home,
End,
Search,
NextMatch,
PrevMatch,
Abort,
Quit,
ConfirmSearch,
CancelSearch key.Binding
}
// FullHelp implements help.KeyMap.
func (k keymap) FullHelp() [][]key.Binding {
return nil
}
// ShortHelp implements help.KeyMap.
func (k keymap) ShortHelp() []key.Binding {
return []key.Binding{
key.NewBinding(
key.WithKeys("up", "down"),
key.WithHelp("↓↑", "navigate"),
),
k.Quit,
k.Search,
k.NextMatch,
k.PrevMatch,
}
}
func defaultKeymap() keymap {
return keymap{
Home: key.NewBinding(
key.WithKeys("g", "home"),
key.WithHelp("h", "home"),
),
End: key.NewBinding(
key.WithKeys("G", "end"),
key.WithHelp("G", "end"),
),
Search: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("/", "search"),
),
PrevMatch: key.NewBinding(
key.WithKeys("p", "N"),
key.WithHelp("N", "previous match"),
),
NextMatch: key.NewBinding(
key.WithKeys("n"),
key.WithHelp("n", "next match"),
),
Abort: key.NewBinding(
key.WithKeys("ctrl+c"),
key.WithHelp("ctrl+c", "abort"),
),
Quit: key.NewBinding(
key.WithKeys("q", "esc"),
key.WithHelp("esc", "quit"),
),
ConfirmSearch: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "confirm"),
),
CancelSearch: key.NewBinding(
key.WithKeys("ctrl+c", "ctrl+d", "esc"),
key.WithHelp("ctrl+c", "cancel"),
),
}
}
type model struct {
content string
origContent string
viewport viewport.Model
help help.Model
showLineNumbers bool
lineNumberStyle lipgloss.Style
softWrap bool
search search
matchStyle lipgloss.Style
matchHighlightStyle lipgloss.Style
maxWidth int
keymap keymap
}
func (m model) Init() tea.Cmd { return nil }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.processText(msg)
case tea.KeyMsg:
return m.keyHandler(msg)
}
m.keymap.PrevMatch.SetEnabled(m.search.query != nil)
m.keymap.NextMatch.SetEnabled(m.search.query != nil)
var cmd tea.Cmd
m.search.input, cmd = m.search.input.Update(msg)
return m, cmd
}
func (m *model) helpView() string {
return m.help.View(m.keymap)
}
func (m *model) processText(msg tea.WindowSizeMsg) {
m.viewport.Height = msg.Height - lipgloss.Height(m.helpView())
m.viewport.Width = msg.Width
textStyle := lipgloss.NewStyle().Width(m.viewport.Width)
var text strings.Builder
// Determine max width of a line.
m.maxWidth = m.viewport.Width
if m.softWrap {
vpStyle := m.viewport.Style
m.maxWidth -= vpStyle.GetHorizontalBorderSize() + vpStyle.GetHorizontalMargins() + vpStyle.GetHorizontalPadding()
if m.showLineNumbers {
m.maxWidth -= lipgloss.Width(" │ ")
}
}
for i, line := range strings.Split(m.content, "\n") {
line = strings.ReplaceAll(line, "\t", " ")
if m.showLineNumbers {
text.WriteString(m.lineNumberStyle.Render(fmt.Sprintf("%4d │ ", i+1)))
}
idx := 0
if w := ansi.StringWidth(line); m.softWrap && w > m.maxWidth {
for w > idx {
if m.showLineNumbers && idx != 0 {
text.WriteString(m.lineNumberStyle.Render(" │ "))
}
truncatedLine := ansi.Cut(line, idx, m.maxWidth+idx)
idx += m.maxWidth
text.WriteString(textStyle.Render(truncatedLine))
text.WriteString("\n")
}
} else {
text.WriteString(textStyle.Render(line))
text.WriteString("\n")
}
}
diffHeight := m.viewport.Height - lipgloss.Height(text.String())
if diffHeight > 0 && m.showLineNumbers {
remainingLines := " ~ │ " + strings.Repeat("\n ~ │ ", diffHeight-1)
text.WriteString(m.lineNumberStyle.Render(remainingLines))
}
m.viewport.SetContent(text.String())
}
const heightOffset = 2
func (m model) keyHandler(msg tea.KeyMsg) (model, tea.Cmd) {
km := m.keymap
var cmd tea.Cmd
if m.search.active {
switch {
case key.Matches(msg, km.ConfirmSearch):
if m.search.input.Value() != "" {
m.content = m.origContent
m.search.Execute(&m)
// Trigger a view update to highlight the found matches.
m.search.NextMatch(&m)
m.processText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
} else {
m.search.Done()
}
case key.Matches(msg, km.CancelSearch):
m.search.Done()
default:
m.search.input, cmd = m.search.input.Update(msg)
}
} else {
switch {
case key.Matches(msg, km.Home):
m.viewport.GotoTop()
case key.Matches(msg, km.End):
m.viewport.GotoBottom()
case key.Matches(msg, km.Search):
m.search.Begin()
return m, textinput.Blink
case key.Matches(msg, km.PrevMatch):
m.search.PrevMatch(&m)
m.processText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
case key.Matches(msg, km.NextMatch):
m.search.NextMatch(&m)
m.processText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
case key.Matches(msg, km.Quit):
return m, tea.Quit
case key.Matches(msg, km.Abort):
return m, tea.Interrupt
}
m.viewport, cmd = m.viewport.Update(msg)
}
return m, cmd
}
func (m model) View() string {
if m.search.active {
return m.viewport.View() + "\n " + m.search.input.View()
}
return m.viewport.View() + "\n" + m.helpView()
}
================================================
FILE: pager/search.go
================================================
package pager
import (
"fmt"
"regexp"
"strings"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
)
type search struct {
active bool
input textinput.Model
query *regexp.Regexp
matchIndex int
matchLipglossStr string
matchString string
}
func (s *search) new() {
input := textinput.New()
input.Placeholder = "search"
input.Prompt = "/"
input.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
s.input = input
}
func (s *search) Begin() {
s.new()
s.active = true
s.input.Focus()
}
// Execute find all lines in the model with a match.
func (s *search) Execute(m *model) {
defer s.Done()
if s.input.Value() == "" {
s.query = nil
return
}
var err error
s.query, err = regexp.Compile(s.input.Value())
if err != nil {
s.query = nil
return
}
query := regexp.MustCompile(fmt.Sprintf("(%s)", s.query.String()))
m.content = query.ReplaceAllString(m.content, m.matchStyle.Render("$1"))
// Recompile the regex to match the an replace the highlights.
leftPad, _ := lipglossPadding(m.matchStyle)
matchingString := regexp.QuoteMeta(m.matchStyle.Render()[:leftPad]) + s.query.String() + regexp.QuoteMeta(m.matchStyle.Render()[leftPad:])
s.query, err = regexp.Compile(matchingString)
if err != nil {
s.query = nil
}
}
func (s *search) Done() {
s.active = false
// To account for the first match is always executed.
s.matchIndex = -1
}
func (s *search) NextMatch(m *model) {
// Check that we are within bounds.
if s.query == nil {
return
}
// Remove previous highlight.
m.content = strings.Replace(m.content, s.matchLipglossStr, s.matchString, 1)
// Highlight the next match.
allMatches := s.query.FindAllStringIndex(m.content, -1)
if len(allMatches) == 0 {
return
}
leftPad, rightPad := lipglossPadding(m.matchStyle)
s.matchIndex = (s.matchIndex + 1) % len(allMatches)
match := allMatches[s.matchIndex]
lhs := m.content[:match[0]]
rhs := m.content[match[0]:]
s.matchString = m.content[match[0]:match[1]]
s.matchLipglossStr = m.matchHighlightStyle.Render(s.matchString[leftPad : len(s.matchString)-rightPad])
m.content = lhs + strings.Replace(rhs, m.content[match[0]:match[1]], s.matchLipglossStr, 1)
// Update the viewport position.
var line int
formatStr := softWrapEm(m.content, m.maxWidth, m.softWrap)
index := strings.Index(formatStr, s.matchLipglossStr)
if index != -1 {
line = strings.Count(formatStr[:index], "\n")
}
// Only update if the match is not within the viewport.
if index != -1 && (line > m.viewport.YOffset-1+m.viewport.VisibleLineCount()-1 || line < m.viewport.YOffset) {
m.viewport.SetYOffset(line)
}
}
func (s *search) PrevMatch(m *model) {
// Check that we are within bounds.
if s.query == nil {
return
}
// Remove previous highlight.
m.content = strings.Replace(m.content, s.matchLipglossStr, s.matchString, 1)
// Highlight the previous match.
allMatches := s.query.FindAllStringIndex(m.content, -1)
if len(allMatches) == 0 {
return
}
s.matchIndex = (s.matchIndex - 1) % len(allMatches)
if s.matchIndex < 0 {
s.matchIndex = len(allMatches) - 1
}
leftPad, rightPad := lipglossPadding(m.matchStyle)
match := allMatches[s.matchIndex]
lhs := m.content[:match[0]]
rhs := m.content[match[0]:]
s.matchString = m.content[match[0]:match[1]]
s.matchLipglossStr = m.matchHighlightStyle.Render(s.matchString[leftPad : len(s.matchString)-rightPad])
m.content = lhs + strings.Replace(rhs, m.content[match[0]:match[1]], s.matchLipglossStr, 1)
// Update the viewport position.
var line int
formatStr := softWrapEm(m.content, m.maxWidth, m.softWrap)
index := strings.Index(formatStr, s.matchLipglossStr)
if index != -1 {
line = strings.Count(formatStr[:index], "\n")
}
// Only update if the match is not within the viewport.
if index != -1 && (line > m.viewport.YOffset-1+m.viewport.VisibleLineCount()-1 || line < m.viewport.YOffset) {
m.viewport.SetYOffset(line)
}
}
func softWrapEm(str string, maxWidth int, softWrap bool) string {
var text strings.Builder
for _, line := range strings.Split(str, "\n") {
idx := 0
if w := ansi.StringWidth(line); softWrap && w > maxWidth {
for w > idx {
truncatedLine := ansi.Cut(line, idx, maxWidth+idx)
idx += maxWidth
text.WriteString(truncatedLine)
text.WriteString("\n")
}
} else {
text.WriteString(line)
text.WriteString("\n")
}
}
return text.String()
}
// lipglossPadding calculates how much padding a string is given by a style.
func lipglossPadding(style lipgloss.Style) (int, int) {
render := style.Render(" ")
before := strings.Index(render, " ")
after := len(render) - len(" ") - before
return before, after
}
================================================
FILE: spin/command.go
================================================
package spin
import (
"fmt"
"os"
"github.com/charmbracelet/bubbles/spinner"
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"
)
// Run provides a shell script interface for the spinner bubble.
// https://github.com/charmbracelet/bubbles/spinner
func (o Options) Run() error {
isOutTTY := term.IsTerminal(os.Stdout.Fd())
isErrTTY := term.IsTerminal(os.Stderr.Fd())
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),
command: o.Command,
align: o.Align,
showStdout: (o.ShowOutput || o.ShowStdout) && isOutTTY,
showStderr: (o.ShowOutput || o.ShowStderr) && isEr
gitextract_697t_p9p/
├── .github/
│ ├── CODEOWNERS
│ ├── dependabot.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── build.yml
│ ├── dependabot-sync.yml
│ ├── goreleaser.yml
│ ├── lint-sync.yml
│ ├── lint.yml
│ └── nightly.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── Dockerfile
├── LICENSE
├── README.md
├── choose/
│ ├── choose.go
│ ├── command.go
│ └── options.go
├── completion/
│ ├── bash.go
│ ├── command.go
│ ├── fish.go
│ └── zsh.go
├── confirm/
│ ├── command.go
│ ├── confirm.go
│ └── options.go
├── cursor/
│ └── cursor.go
├── default.nix
├── examples/
│ ├── .gitignore
│ ├── README.md
│ ├── choose.tape
│ ├── commit.sh
│ ├── commit.tape
│ ├── confirm.tape
│ ├── convert-to-gif.sh
│ ├── customize.tape
│ ├── demo.sh
│ ├── demo.tape
│ ├── diyfetch
│ ├── fav.txt
│ ├── file.tape
│ ├── filter-key-value.sh
│ ├── flavors.txt
│ ├── format.ansi
│ ├── git-branch-manager.sh
│ ├── git-stage.sh
│ ├── gum.js
│ ├── gum.py
│ ├── gum.rb
│ ├── input.tape
│ ├── kaomoji.sh
│ ├── magic.sh
│ ├── pager.tape
│ ├── posix.sh
│ ├── skate.sh
│ ├── spin.tape
│ ├── story.txt
│ ├── test.sh
│ └── write.tape
├── file/
│ ├── command.go
│ ├── file.go
│ └── options.go
├── filter/
│ ├── command.go
│ ├── filter.go
│ ├── filter_test.go
│ └── options.go
├── flake.nix
├── format/
│ ├── README.md
│ ├── command.go
│ ├── formats.go
│ └── options.go
├── go.mod
├── go.sum
├── gum.go
├── input/
│ ├── command.go
│ ├── input.go
│ └── options.go
├── internal/
│ ├── decode/
│ │ └── align.go
│ ├── exit/
│ │ └── exit.go
│ ├── files/
│ │ └── files.go
│ ├── stdin/
│ │ └── stdin.go
│ ├── timeout/
│ │ └── context.go
│ └── tty/
│ └── tty.go
├── join/
│ ├── command.go
│ └── options.go
├── log/
│ ├── command.go
│ └── options.go
├── main.go
├── man/
│ └── command.go
├── pager/
│ ├── command.go
│ ├── options.go
│ ├── pager.go
│ └── search.go
├── spin/
│ ├── command.go
│ ├── options.go
│ ├── pty.go
│ ├── spin.go
│ └── spinners.go
├── style/
│ ├── ascii_a.txt
│ ├── borders.go
│ ├── command.go
│ ├── lipgloss.go
│ ├── options.go
│ └── spacing.go
├── table/
│ ├── bom.csv
│ ├── comma.csv
│ ├── command.go
│ ├── example.csv
│ ├── invalid.csv
│ ├── options.go
│ └── table.go
├── version/
│ ├── command.go
│ └── options.go
└── write/
├── command.go
├── options.go
└── write.go
SYMBOL INDEX (215 symbols across 55 files)
FILE: choose/choose.go
function defaultKeymap (line 24) | func defaultKeymap() keymap {
type keymap (line 69) | type keymap struct
method FullHelp (line 84) | func (k keymap) FullHelp() [][]key.Binding { return nil }
method ShortHelp (line 87) | func (k keymap) ShortHelp() []key.Binding {
type model (line 99) | type model struct
method Init (line 132) | func (m model) Init() tea.Cmd { return nil }
method Update (line 134) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method selectAll (line 217) | func (m model) selectAll() model {
method deselectAll (line 233) | func (m model) deselectAll() model {
method View (line 243) | func (m model) View() string {
type item (line 126) | type item struct
FILE: choose/command.go
method Run (line 23) | func (o Options) Run() error {
FILE: choose/options.go
type Options (line 10) | type Options struct
FILE: completion/bash.go
type Bash (line 16) | type Bash struct
method Run (line 19) | func (b Bash) Run(ctx *kong.Context) error {
method gen (line 642) | func (b Bash) gen(buf io.StringWriter, cmd *kong.Node) {
type ShellCompDirective (line 34) | type ShellCompDirective
constant ShellCompDirectiveError (line 38) | ShellCompDirectiveError ShellCompDirective = 1 << iota
constant ShellCompDirectiveNoSpace (line 42) | ShellCompDirectiveNoSpace
constant ShellCompDirectiveNoFileComp (line 46) | ShellCompDirectiveNoFileComp
constant ShellCompDirectiveFilterFileExt (line 53) | ShellCompDirectiveFilterFileExt
constant ShellCompDirectiveFilterDirs (line 60) | ShellCompDirectiveFilterDirs
constant shellCompDirectiveMaxValue (line 66) | shellCompDirectiveMaxValue
constant ShellCompDirectiveDefault (line 71) | ShellCompDirectiveDefault ShellCompDirective = 0
constant ShellCompNoDescRequestCmd (line 78) | ShellCompNoDescRequestCmd = "completion completeNoDesc"
constant BashCompFilenameExt (line 79) | BashCompFilenameExt = "kong_annotation_bash_completion_filename_ex...
constant BashCompCustom (line 80) | BashCompCustom = "kong_annotation_bash_completion_custom"
constant BashCompOneRequiredFlag (line 81) | BashCompOneRequiredFlag = "kong_annotation_bash_completion_one_require...
constant BashCompSubdirsInDir (line 82) | BashCompSubdirsInDir = "kong_annotation_bash_completion_subdirs_in_...
constant activeHelpEnvVarSuffix (line 84) | activeHelpEnvVarSuffix = "_ACTIVE_HELP"
function activeHelpEnvVar (line 90) | func activeHelpEnvVar(name string) string {
function writePreamble (line 96) | func writePreamble(buf io.StringWriter, name string) {
function writePostscript (line 464) | func writePostscript(buf io.StringWriter, name string) {
function writeCommands (line 507) | func writeCommands(buf io.StringWriter, cmd *kong.Node) {
function writeFlagHandler (line 519) | func writeFlagHandler(buf io.StringWriter, name string, annotations map[...
constant cbn (line 555) | cbn = "\")\n"
function writeShortFlag (line 557) | func writeShortFlag(buf io.StringWriter, flag *kong.Flag, cmd *kong.Node) {
function writeFlag (line 568) | func writeFlag(buf io.StringWriter, flag *kong.Flag, cmd *kong.Node) {
function writeLocalNonPersistentFlag (line 584) | func writeLocalNonPersistentFlag(buf io.StringWriter, flag *kong.Flag) {
function writeFlags (line 596) | func writeFlags(buf io.StringWriter, cmd *kong.Node) {
function writeCmdAliases (line 618) | func writeCmdAliases(buf io.StringWriter, cmd *kong.Node) {
function writeArgAliases (line 634) | func writeArgAliases(buf io.StringWriter, cmd *kong.Node) {
FILE: completion/command.go
type Completion (line 13) | type Completion struct
function commandName (line 19) | func commandName(cmd *kong.Node) string {
function hasCommands (line 26) | func hasCommands(cmd *kong.Node) bool {
function isArgument (line 36) | func isArgument(cmd *kong.Node) bool {
function writeString (line 41) | func writeString(b io.StringWriter, s string) {
function nonCompletableFlag (line 48) | func nonCompletableFlag(flag *kong.Flag) bool {
function flagPossibleValues (line 52) | func flagPossibleValues(flag *kong.Flag) []string {
FILE: completion/fish.go
type Fish (line 12) | type Fish struct
method Run (line 15) | func (f Fish) Run(ctx *kong.Context) error {
method gen (line 33) | func (f Fish) gen(buf io.StringWriter, cmd *kong.Node) {
FILE: completion/zsh.go
type Zsh (line 12) | type Zsh struct
method Run (line 15) | func (z Zsh) Run(ctx *kong.Context) error {
method writeFlag (line 31) | func (z Zsh) writeFlag(buf io.StringWriter, f *kong.Flag) {
method writeFlags (line 76) | func (z Zsh) writeFlags(buf io.StringWriter, cmd *kong.Node) {
method writeCommand (line 88) | func (z Zsh) writeCommand(buf io.StringWriter, c *kong.Node) {
method writeCommands (line 92) | func (z Zsh) writeCommands(buf io.StringWriter, cmd *kong.Node) {
method gen (line 105) | func (z Zsh) gen(buf io.StringWriter, cmd *kong.Node) {
FILE: confirm/command.go
method Run (line 18) | func (o Options) Run() error {
FILE: confirm/confirm.go
function defaultKeymap (line 21) | func defaultKeymap(affirmative, negative string) keymap {
type keymap (line 59) | type keymap struct
method FullHelp (line 69) | func (k keymap) FullHelp() [][]key.Binding { return nil }
method ShortHelp (line 72) | func (k keymap) ShortHelp() []key.Binding {
type model (line 76) | type model struct
method Init (line 97) | func (m model) Init() tea.Cmd { return nil }
method Update (line 99) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 133) | func (m model) View() string {
FILE: confirm/options.go
type Options (line 10) | type Options struct
FILE: file/command.go
method Run (line 17) | func (o Options) Run() error {
FILE: file/file.go
type keymap (line 23) | type keymap
method FullHelp (line 41) | func (k keymap) FullHelp() [][]key.Binding { return nil }
method ShortHelp (line 44) | func (k keymap) ShortHelp() []key.Binding {
function defaultKeymap (line 35) | func defaultKeymap() keymap {
type model (line 55) | type model struct
method Init (line 67) | func (m model) Init() tea.Cmd { return m.filepicker.Init() }
method Update (line 69) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 97) | func (m model) View() string {
method helpView (line 117) | func (m model) helpView() string {
FILE: file/options.go
type Options (line 10) | type Options struct
FILE: filter/command.go
method Run (line 25) | func (o Options) Run() error {
method checkSelected (line 168) | func (o Options) checkSelected(m model) {
FILE: filter/filter.go
function defaultKeymap (line 27) | func defaultKeymap() keymap {
type keymap (line 90) | type keymap struct
method FullHelp (line 109) | func (k keymap) FullHelp() [][]key.Binding { return nil }
method ShortHelp (line 112) | func (k keymap) ShortHelp() []key.Binding {
type model (line 126) | type model struct
method Init (line 160) | func (m model) Init() tea.Cmd { return textinput.Blink }
method View (line 162) | func (m model) View() string {
method helpView (line 266) | func (m model) helpView() string {
method Update (line 270) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method CursorUp (line 400) | func (m *model) CursorUp() {
method CursorDown (line 423) | func (m *model) CursorDown() {
method ToggleSelection (line 446) | func (m *model) ToggleSelection() {
method selectAll (line 456) | func (m model) selectAll() model {
method deselectAll (line 470) | func (m model) deselectAll() model {
function matchAll (line 476) | func matchAll(options []string) []fuzzy.Match {
function exactMatches (line 484) | func exactMatches(search string, choices []string) []fuzzy.Match {
function matchedRanges (line 507) | func matchedRanges(in []int) [][2]int {
function bytePosToVisibleCharPos (line 528) | func bytePosToVisibleCharPos(str string, rng [2]int) (int, int) {
FILE: filter/filter_test.go
function TestMatchedRanges (line 10) | func TestMatchedRanges(t *testing.T) {
function TestByteToChar (line 45) | func TestByteToChar(t *testing.T) {
FILE: filter/options.go
type Options (line 10) | type Options struct
FILE: format/command.go
method Run (line 21) | func (o Options) Run() error {
FILE: format/formats.go
function code (line 12) | func code(input, language string) (string, error) {
function emoji (line 27) | func emoji(input string) (string, error) {
function markdown (line 41) | func markdown(input string, theme string) (string, error) {
function template (line 56) | func template(input string) (string, error) {
FILE: format/options.go
type Options (line 4) | type Options struct
FILE: gum.go
type Gum (line 25) | type Gum struct
FILE: input/command.go
method Run (line 19) | func (o Options) Run() error {
FILE: input/input.go
type keymap (line 18) | type keymap
method FullHelp (line 26) | func (k keymap) FullHelp() [][]key.Binding { return nil }
method ShortHelp (line 29) | func (k keymap) ShortHelp() []key.Binding {
function defaultKeymap (line 20) | func defaultKeymap() keymap {
type model (line 38) | type model struct
method Init (line 51) | func (m model) Init() tea.Cmd { return textinput.Blink }
method View (line 53) | func (m model) View() string {
method Update (line 74) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
FILE: input/options.go
type Options (line 10) | type Options struct
FILE: internal/exit/exit.go
constant StatusTimeout (line 7) | StatusTimeout = 124
constant StatusAborted (line 10) | StatusAborted = 130
type ErrExit (line 13) | type ErrExit
method Error (line 16) | func (e ErrExit) Error() string { return "exit " + strconv.Itoa(int(e)) }
FILE: internal/files/files.go
function List (line 12) | func List() []string {
function shouldIgnore (line 30) | func shouldIgnore(path string) bool {
FILE: internal/stdin/stdin.go
type options (line 14) | type options struct
type Option (line 20) | type Option
function StripANSI (line 23) | func StripANSI(b bool) Option {
function SingleLine (line 30) | func SingleLine(b bool) Option {
function Read (line 37) | func Read(opts ...Option) (string, error) {
function IsEmpty (line 80) | func IsEmpty() bool {
FILE: internal/timeout/context.go
function Context (line 10) | func Context(timeout time.Duration) (context.Context, context.CancelFunc) {
FILE: internal/tty/tty.go
function Println (line 18) | func Println(s string) {
FILE: join/command.go
method Run (line 27) | func (o Options) Run() error {
FILE: join/options.go
type Options (line 4) | type Options struct
FILE: log/command.go
method Run (line 16) | func (o Options) Run() error {
type logger (line 137) | type logger struct
FILE: log/options.go
type Options (line 8) | type Options struct
FILE: main.go
constant shaLen (line 17) | shaLen = 7
function main (line 31) | func main() {
FILE: man/command.go
type Man (line 13) | type Man struct
method BeforeApply (line 16) | func (m Man) BeforeApply(ctx *kong.Context) error {
FILE: pager/command.go
method Run (line 16) | func (o Options) Run() error {
FILE: pager/options.go
type Options (line 10) | type Options struct
FILE: pager/pager.go
type keymap (line 19) | type keymap struct
method FullHelp (line 32) | func (k keymap) FullHelp() [][]key.Binding {
method ShortHelp (line 37) | func (k keymap) ShortHelp() []key.Binding {
function defaultKeymap (line 50) | func defaultKeymap() keymap {
type model (line 91) | type model struct
method Init (line 106) | func (m model) Init() tea.Cmd { return nil }
method Update (line 108) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method helpView (line 124) | func (m *model) helpView() string {
method processText (line 128) | func (m *model) processText(msg tea.WindowSizeMsg) {
method keyHandler (line 176) | func (m model) keyHandler(msg tea.KeyMsg) (model, tea.Cmd) {
method View (line 223) | func (m model) View() string {
constant heightOffset (line 174) | heightOffset = 2
FILE: pager/search.go
type search (line 13) | type search struct
method new (line 22) | func (s *search) new() {
method Begin (line 30) | func (s *search) Begin() {
method Execute (line 37) | func (s *search) Execute(m *model) {
method Done (line 62) | func (s *search) Done() {
method NextMatch (line 69) | func (s *search) NextMatch(m *model) {
method PrevMatch (line 107) | func (s *search) PrevMatch(m *model) {
function softWrapEm (line 149) | func softWrapEm(str string, maxWidth int, softWrap bool) string {
function lipglossPadding (line 170) | func lipglossPadding(style lipgloss.Style) (int, int) {
FILE: spin/command.go
method Run (line 17) | func (o Options) Run() error {
FILE: spin/options.go
type Options (line 10) | type Options struct
FILE: spin/pty.go
function openPty (line 10) | func openPty(f *os.File) (pty xpty.Pty, err error) {
FILE: spin/spin.go
type model (line 33) | type model struct
method Init (line 142) | func (m model) Init() tea.Cmd {
method View (line 149) | func (m model) View() string {
method Update (line 177) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
type errorMsg (line 59) | type errorMsg
type finishCommandMsg (line 61) | type finishCommandMsg struct
function commandStart (line 68) | func commandStart(command []string) tea.Cmd {
function commandAbort (line 135) | func commandAbort() tea.Msg {
FILE: style/command.go
method Run (line 18) | func (o Options) Run() error {
FILE: style/lipgloss.go
method ToLipgloss (line 11) | func (s Styles) ToLipgloss() lipgloss.Style {
method ToLipgloss (line 32) | func (s StylesNotHidden) ToLipgloss() lipgloss.Style {
FILE: style/options.go
type Options (line 4) | type Options struct
type Styles (line 17) | type Styles struct
type StylesNotHidden (line 47) | type StylesNotHidden struct
FILE: style/spacing.go
constant minTokens (line 9) | minTokens = 1
constant halfTokens (line 10) | halfTokens = 2
constant maxTokens (line 11) | maxTokens = 4
function ParsePadding (line 16) | func ParsePadding(s string) (int, int, int, int) {
FILE: table/command.go
method Run (line 22) | func (o Options) Run() error {
FILE: table/options.go
type Options (line 10) | type Options struct
FILE: table/table.go
type keymap (line 28) | type keymap struct
method FullHelp (line 36) | func (k keymap) FullHelp() [][]key.Binding { return nil }
method ShortHelp (line 39) | func (k keymap) ShortHelp() []key.Binding {
function defaultKeymap (line 47) | func defaultKeymap() keymap {
type model (line 68) | type model struct
method Init (line 79) | func (m model) Init() tea.Cmd { return nil }
method countView (line 81) | func (m model) countView() string {
method Update (line 95) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 119) | func (m model) View() string {
function numLen (line 132) | func numLen(i int) int {
FILE: version/command.go
method Run (line 12) | func (o Options) Run(ctx *kong.Context) error {
FILE: version/options.go
type Options (line 4) | type Options struct
FILE: write/command.go
method Run (line 20) | func (o Options) Run() error {
FILE: write/options.go
type Options (line 10) | type Options struct
FILE: write/write.go
type keymap (line 23) | type keymap struct
method FullHelp (line 32) | func (k keymap) FullHelp() [][]key.Binding { return nil }
method ShortHelp (line 35) | func (k keymap) ShortHelp() []key.Binding {
function defaultKeymap (line 43) | func defaultKeymap() keymap {
type model (line 70) | type model struct
method Init (line 83) | func (m model) Init() tea.Cmd { return textarea.Blink }
method View (line 85) | func (m model) View() string {
method Update (line 108) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
type startEditorMsg (line 149) | type startEditorMsg struct
type editorFinishedMsg (line 154) | type editorFinishedMsg struct
function createTempFile (line 159) | func createTempFile(content string, lineno int) tea.Cmd {
function openEditor (line 177) | func openEditor(path string, lineno int) tea.Cmd {
Condensed preview — 115 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (251K chars).
[
{
"path": ".github/CODEOWNERS",
"chars": 27,
"preview": "* @charmbracelet/everyone\n"
},
{
"path": ".github/dependabot.yml",
"chars": 1114,
"preview": "version: 2\n\nupdates:\n - package-ecosystem: \"gomod\"\n directory: \"/\"\n schedule:\n interval: \"weekly\"\n day:"
},
{
"path": ".github/pull_request_template.md",
"chars": 32,
"preview": "Fixes #...\n\n### Changes\n- \n- \n-\n"
},
{
"path": ".github/workflows/build.yml",
"chars": 210,
"preview": "name: build\n\non:\n push:\n branches:\n - main\n pull_request:\n\njobs:\n build:\n uses: charmbracelet/meta/.github"
},
{
"path": ".github/workflows/dependabot-sync.yml",
"chars": 419,
"preview": "name: dependabot-sync\non:\n schedule:\n - cron: \"0 0 * * 0\" # every Sunday at midnight\n workflow_dispatch: # allows m"
},
{
"path": ".github/workflows/goreleaser.yml",
"chars": 957,
"preview": "# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json\n\nname: goreleaser\n\non:\n push:\n tag"
},
{
"path": ".github/workflows/lint-sync.yml",
"chars": 271,
"preview": "name: lint-sync\non:\n schedule:\n # every Sunday at midnight\n - cron: \"0 0 * * 0\"\n workflow_dispatch: # allows man"
},
{
"path": ".github/workflows/lint.yml",
"chars": 115,
"preview": "name: lint\non:\n push:\n pull_request:\n\njobs:\n lint:\n uses: charmbracelet/meta/.github/workflows/lint.yml@main\n"
},
{
"path": ".github/workflows/nightly.yml",
"chars": 609,
"preview": "name: nightly\n\non:\n push:\n branches:\n - main\n\njobs:\n nightly:\n uses: charmbracelet/meta/.github/workflows/n"
},
{
"path": ".gitignore",
"chars": 101,
"preview": "# Files\ntest\n.DS_Store\n\n# Binaries\ngum\ndist\ntestdata\n\n# Folders\ncompletions/\nmanpages/\n\n# nix\nresult\n"
},
{
"path": ".golangci.yml",
"chars": 764,
"preview": "version: \"2\"\nrun:\n tests: false\nlinters:\n enable:\n - bodyclose\n - exhaustive\n - goconst\n - godot\n - gom"
},
{
"path": ".goreleaser.yml",
"chars": 482,
"preview": "# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json\n\nversion: 2\n\nincludes:\n - from_url:\n "
},
{
"path": "Dockerfile",
"chars": 94,
"preview": "FROM gcr.io/distroless/static\nCOPY gum /usr/local/bin/gum\nENTRYPOINT [ \"/usr/local/bin/gum\" ]\n"
},
{
"path": "LICENSE",
"chars": 1080,
"preview": "MIT License\n\nCopyright (c) 2022-2024 Charmbracelet, Inc\n\nPermission is hereby granted, free of charge, to any person obt"
},
{
"path": "README.md",
"chars": 13745,
"preview": "# Gum\n\n<p>\n <a href=\"https://stuff.charm.sh/gum/nutritional-information.png\" target=\"_blank\"><img src=\"https://stuff."
},
{
"path": "choose/choose.go",
"chars": 6586,
"preview": "// Package choose provides an interface to choose one option from a given list\n// of options. The options can be provide"
},
{
"path": "choose/command.go",
"chars": 5037,
"preview": "package choose\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/bubbles/help\"\n\t"
},
{
"path": "choose/options.go",
"chars": 2998,
"preview": "package choose\n\nimport (\n\t\"time\"\n\n\t\"github.com/charmbracelet/gum/style\"\n)\n\n// Options is the customization options for t"
},
{
"path": "completion/bash.go",
"chars": 21919,
"preview": "// Package completion provides a bash completion generator for Kong\n// applications.\npackage completion\n\nimport (\n\t\"byte"
},
{
"path": "completion/command.go",
"chars": 1324,
"preview": "package completion\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/alecthomas/kong\"\n)\n\n// Completion command.\ntype"
},
{
"path": "completion/fish.go",
"chars": 1977,
"preview": "package completion\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/alecthomas/kong\"\n)\n\n// Fish is a fish shell completio"
},
{
"path": "completion/zsh.go",
"chars": 3551,
"preview": "package completion\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/alecthomas/kong\"\n)\n\n// Zsh is zsh completion generato"
},
{
"path": "confirm/command.go",
"chars": 1738,
"preview": "package confirm\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/charmbracelet/bubbles/help\"\n\ttea \"github.com/charmbracel"
},
{
"path": "confirm/confirm.go",
"chars": 3742,
"preview": "// Package confirm provides an interface to ask a user to confirm an action.\n// The user is provided with an interface t"
},
{
"path": "confirm/options.go",
"chars": 1690,
"preview": "package confirm\n\nimport (\n\t\"time\"\n\n\t\"github.com/charmbracelet/gum/style\"\n)\n\n// Options is the customization options for "
},
{
"path": "cursor/cursor.go",
"chars": 282,
"preview": "// Package cursor provides cursor modes.\npackage cursor\n\nimport (\n\t\"github.com/charmbracelet/bubbles/cursor\"\n)\n\n// Modes"
},
{
"path": "default.nix",
"chars": 219,
"preview": "{ pkgs }:\n\npkgs.buildGoModule rec {\n pname = \"gum\";\n version = \"0.15.2\";\n\n src = ./.;\n\n vendorHash = \"sha256-TK2Fc4b"
},
{
"path": "examples/.gitignore",
"chars": 12,
"preview": "*.gif\n*.png\n"
},
{
"path": "examples/README.md",
"chars": 682,
"preview": "# Glamour\n\nA casual introduction. 你好世界!\n\n## Let's talk about artichokes\n\nThe artichoke is mentioned as a garden\nplant in"
},
{
"path": "examples/choose.tape",
"chars": 315,
"preview": "Output choose.gif\n\nSet Width 1000\nSet Height 430\nSet Shell bash\n\nType \"gum choose {1..5}\"\nSleep 500ms\nEnter\nSleep 500ms\n"
},
{
"path": "examples/commit.sh",
"chars": 1169,
"preview": "#!/bin/sh\n\n# This script is used to write a conventional commit message.\n# It prompts the user to choose the type of com"
},
{
"path": "examples/commit.tape",
"chars": 474,
"preview": "Output commit.gif\n\nSet Shell \"bash\"\nSet FontSize 32\nSet Width 1200\nSet Height 600\n\nType \"./commit.sh\" Sleep 500ms Enter"
},
{
"path": "examples/confirm.tape",
"chars": 342,
"preview": "Output confirm.gif\n\nSet Width 1000\nSet Height 350\nSet Shell bash\n\nSleep 500ms\nType \"gum confirm && echo 'Me too!' || ech"
},
{
"path": "examples/convert-to-gif.sh",
"chars": 842,
"preview": "#!/bin/bash\n\n# This script converts some video to a GIF. It prompts the user to select an\n# video file with `gum filter`"
},
{
"path": "examples/customize.tape",
"chars": 353,
"preview": "Output customize.gif\n\nSet Width 1000\nSet Height 350\nSet Shell bash\n\nSleep 1s\nType `gum input --cursor.foreground \"#F4AC4"
},
{
"path": "examples/demo.sh",
"chars": 1966,
"preview": "#!/bin/bash\n\ngum style --border normal --margin \"1\" --padding \"1 2\" --border-foreground 212 \"Hello, there! Welcome to $("
},
{
"path": "examples/demo.tape",
"chars": 489,
"preview": "Output ./demo.gif\n\nSet Shell bash\n\nSet FontSize 22\nSet Width 800\nSet Height 450\n\nType \"./demo.sh\"\nEnter\nSleep 1s\nType \"W"
},
{
"path": "examples/diyfetch",
"chars": 3843,
"preview": "#!/bin/sh\n\n# ____ _____ ____ _ _\n# | _ \\_ _\\ \\ / / _| ___| |_ ___| |__\n# | | | | | \\ V / |_ / _ \\ __/ __|"
},
{
"path": "examples/fav.txt",
"chars": 7,
"preview": "Banana\n"
},
{
"path": "examples/file.tape",
"chars": 151,
"preview": "Output file.gif\nSet Width 800\nSet Height 525\nSet Shell bash\n\nType \"gum file ..\"\nEnter\nSleep 1s\nDown@150ms 6\nSleep 1s\nEnt"
},
{
"path": "examples/filter-key-value.sh",
"chars": 207,
"preview": "#!/bin/bash\n\nexport LIST=$(cat <<END\nCow:Moo\nCat:Meow\nDog:Woof\nEND\n)\n\nANIMAL=$(echo \"$LIST\" | cut -d':' -f1 | gum filter"
},
{
"path": "examples/flavors.txt",
"chars": 32,
"preview": "Banana\nCherry\nOrange\nStrawberry\n"
},
{
"path": "examples/format.ansi",
"chars": 1033,
"preview": "\u001b[38;2;90;86;224m> \u001b[0mgum format -t code < main.go\n\n \n\u001b[38;5;204m\u001b[0m\u001b[38;5;252m\u001b[0m \u001b[38;5;252m \u001b[0m\u001b[38;5;252m \u001b[0m"
},
{
"path": "examples/git-branch-manager.sh",
"chars": 1545,
"preview": "#! /bin/sh\n\n# This script is used to manage git branches such as delete, update, and rebase\n# them. It prompts the user "
},
{
"path": "examples/git-stage.sh",
"chars": 271,
"preview": "#!/bin/bash\n\nADD=\"Add\"\nRESET=\"Reset\"\n\nACTION=$(gum choose \"$ADD\" \"$RESET\")\n\nif [ \"$ACTION\" == \"$ADD\" ]; then\n git sta"
},
{
"path": "examples/gum.js",
"chars": 383,
"preview": "const { spawn } = require(\"child_process\");\n\nconst activities = [\"walking\", \"running\", \"cycling\", \"driving\", \"transport\""
},
{
"path": "examples/gum.py",
"chars": 202,
"preview": "import subprocess\n\nprint(\"What's your favorite language?\")\n\nresult = subprocess.run([\"gum\", \"choose\", \"Go\", \"Python\"], s"
},
{
"path": "examples/gum.rb",
"chars": 803,
"preview": "puts 'What is your name?'\nname = `gum input --placeholder \"Your name\"`.chomp\n\nputs \"Hello #{name}!\"\n\nputs 'Pick your 2 f"
},
{
"path": "examples/input.tape",
"chars": 187,
"preview": "Output input.gif\n\nSet Width 800\nSet Height 250\nSet Shell bash\n\nSleep 1s\nType `gum input --placeholder \"What's up?\"`\nSlee"
},
{
"path": "examples/kaomoji.sh",
"chars": 1365,
"preview": "#!/usr/bin/env bash\n\n# If the user passes '-h', '--help', or 'help' print out a little bit of help.\n# text.\ncase \"$1\" in"
},
{
"path": "examples/magic.sh",
"chars": 1387,
"preview": "#!/bin/bash\n\n# Always ask for permission!\necho \"Do you want to see a magic trick?\"\n\nYES=\"Yes, please!\"\nNO=\"No, thank you"
},
{
"path": "examples/pager.tape",
"chars": 163,
"preview": "Output pager.gif\n\nSet Shell bash\nSet Width 900\nSet Height 750\n\nSleep 1s\nType \"gum pager < README.md\"\nEnter\nSleep 1.5s\nDo"
},
{
"path": "examples/posix.sh",
"chars": 95,
"preview": "#!/bin/sh\n\necho \"What's your favorite shell?\"\n\ngum choose \"Posix\" \"Bash\" \"Zsh\" \"Fish\" \"Elvish\"\n"
},
{
"path": "examples/skate.sh",
"chars": 244,
"preview": "#!/bin/sh\n\n# Building a simple `skate` TUI with gum to allow you to select a database and\n# pick a value from skate.\n\nDA"
},
{
"path": "examples/spin.tape",
"chars": 167,
"preview": "Output spin.gif\n\nSet Shell bash\nSet Width 1200\nSet Height 300\nSet FontSize 36\n\nSleep 500ms\nType `gum spin --title \"Buyin"
},
{
"path": "examples/story.txt",
"chars": 45,
"preview": "Once upon a time\nIn a land far, far away....\n"
},
{
"path": "examples/test.sh",
"chars": 2103,
"preview": "#!/bin/sh\n\n# Choose\ngum choose Foo Bar Baz\ngum choose Choose One Item --cursor \"* \" --cursor.foreground 99 --selected.fo"
},
{
"path": "examples/write.tape",
"chars": 261,
"preview": "Output write.gif\n\nSet Width 800\nSet Height 350\nSet Shell bash\n\nSleep 500ms\nType \"gum write > story.txt\"\nEnter\nSleep 1s\nT"
},
{
"path": "file/command.go",
"chars": 1942,
"preview": "package file\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/charmbracelet/bubbles/filepicker\"\n\t\"github."
},
{
"path": "file/file.go",
"chars": 2594,
"preview": "// Package file provides an interface to pick a file from a folder (tree).\n// The user is provided a file manager-like i"
},
{
"path": "file/options.go",
"chars": 2898,
"preview": "package file\n\nimport (\n\t\"time\"\n\n\t\"github.com/charmbracelet/gum/style\"\n)\n\n// Options are the options for the file command"
},
{
"path": "filter/command.go",
"chars": 4621,
"preview": "package filter\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/bubbles/help\"\n\t\"github."
},
{
"path": "filter/filter.go",
"chars": 14731,
"preview": "// Package filter provides a fuzzy searching text input to allow filtering a\n// list of options to select one option.\n//"
},
{
"path": "filter/filter_test.go",
"chars": 1323,
"preview": "package filter\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/charmbracelet/x/ansi\"\n)\n\nfunc TestMatchedRanges(t *testing."
},
{
"path": "filter/options.go",
"chars": 4367,
"preview": "package filter\n\nimport (\n\t\"time\"\n\n\t\"github.com/charmbracelet/gum/style\"\n)\n\n// Options is the customization options for t"
},
{
"path": "flake.nix",
"chars": 546,
"preview": "{\n description = \"A tool for glamorous shell scripts\";\n\n inputs = {\n nixpkgs.url = \"github:nixos/nixpkgs/nixos-unst"
},
{
"path": "format/README.md",
"chars": 2394,
"preview": "# Gum Format\n\nGum format allows you to format different text into human readable output.\n\nFour different parse-able form"
},
{
"path": "format/command.go",
"chars": 850,
"preview": "// Package format allows you to render formatted text from the command line.\n//\n// It supports the following types:\n//\n/"
},
{
"path": "format/formats.go",
"chars": 1542,
"preview": "package format\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\ttpl \"text/template\"\n\n\t\"github.com/charmbracelet/glamour\"\n\t\"github.com/muesli/t"
},
{
"path": "format/options.go",
"chars": 709,
"preview": "package format\n\n// Options is customization options for the format command.\ntype Options struct {\n\tTemplate []string `ar"
},
{
"path": "go.mod",
"chars": 2684,
"preview": "module github.com/charmbracelet/gum\n\ngo 1.24.2\n\nrequire (\n\tgithub.com/Masterminds/semver/v3 v3.4.0\n\tgithub.com/alecthoma"
},
{
"path": "go.sum",
"chars": 12262,
"preview": "github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=\ngithub.com/MakeNowJust/heredoc v1."
},
{
"path": "gum.go",
"chars": 8565,
"preview": "package main\n\nimport (\n\t\"github.com/alecthomas/kong\"\n\n\t\"github.com/charmbracelet/gum/choose\"\n\t\"github.com/charmbracelet/"
},
{
"path": "input/command.go",
"chars": 1864,
"preview": "package input\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/charmbracelet/bubbles/help\"\n\t\"github.com/charmbracelet/bubb"
},
{
"path": "input/input.go",
"chars": 2233,
"preview": "// Package input provides a shell script interface for the text input bubble.\n// https://github.com/charmbracelet/bubble"
},
{
"path": "input/options.go",
"chars": 1917,
"preview": "package input\n\nimport (\n\t\"time\"\n\n\t\"github.com/charmbracelet/gum/style\"\n)\n\n// Options are the customization options for t"
},
{
"path": "internal/decode/align.go",
"chars": 333,
"preview": "// Package decode position strings to lipgloss.\npackage decode\n\nimport \"github.com/charmbracelet/lipgloss\"\n\n// Align map"
},
{
"path": "internal/exit/exit.go",
"chars": 391,
"preview": "// Package exit code implementation.\npackage exit\n\nimport \"strconv\"\n\n// StatusTimeout is the exit code for timed out com"
},
{
"path": "internal/files/files.go",
"chars": 746,
"preview": "// Package files handles files.\npackage files\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n// List returns a list of a"
},
{
"path": "internal/stdin/stdin.go",
"chars": 1593,
"preview": "// Package stdin handles processing input from stdin.\npackage stdin\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\t\""
},
{
"path": "internal/timeout/context.go",
"chars": 363,
"preview": "// Package timeout handles context timeouts.\npackage timeout\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n// Context setup a new cont"
},
{
"path": "internal/tty/tty.go",
"chars": 420,
"preview": "// Package tty provides tty-aware printing.\npackage tty\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/charmbracelet/x/ans"
},
{
"path": "join/command.go",
"chars": 1052,
"preview": "// Package join provides a shell script interface for the lipgloss\n// JoinHorizontal and JoinVertical commands. It allow"
},
{
"path": "join/options.go",
"chars": 401,
"preview": "package join\n\n// Options is the set of options that can configure a join.\ntype Options struct {\n\tText []string `arg:\"\" h"
},
{
"path": "log/command.go",
"chars": 3387,
"preview": "// Package log the log command.\npackage log\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/charmbracele"
},
{
"path": "log/options.go",
"chars": 1832,
"preview": "package log\n\nimport (\n\t\"github.com/charmbracelet/gum/style\"\n)\n\n// Options is the set of options that can configure a joi"
},
{
"path": "main.go",
"chars": 2357,
"preview": "// Package main is Gum: a tool for glamorous shell scripts.\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"runtime/debu"
},
{
"path": "man/command.go",
"chars": 711,
"preview": "// Package man the man command.\npackage man\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/alecthomas/kong\"\n\tmangokong \"github.com/alect"
},
{
"path": "pager/command.go",
"chars": 1574,
"preview": "package pager\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\n\t\"github.com/charmbracelet/bubbles/help\"\n\t\"github.com/charmbracelet/bubbles/vi"
},
{
"path": "pager/options.go",
"chars": 1545,
"preview": "package pager\n\nimport (\n\t\"time\"\n\n\t\"github.com/charmbracelet/gum/style\"\n)\n\n// Options are the options for the pager.\ntype"
},
{
"path": "pager/pager.go",
"chars": 5732,
"preview": "// Package pager provides a pager (similar to less) for the terminal.\n//\n// $ cat file.txt | gum pager\npackage pager\n\nim"
},
{
"path": "pager/search.go",
"chars": 4759,
"preview": "package pager\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\t\"github.com/charmbra"
},
{
"path": "spin/command.go",
"chars": 2409,
"preview": "package spin\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/charmbracelet/bubbles/spinner\"\n\ttea \"github.com/charmbracelet/bubblete"
},
{
"path": "spin/options.go",
"chars": 1581,
"preview": "package spin\n\nimport (\n\t\"time\"\n\n\t\"github.com/charmbracelet/gum/style\"\n)\n\n// Options is the customization options for the"
},
{
"path": "spin/pty.go",
"chars": 374,
"preview": "package spin\n\nimport (\n\t\"os\"\n\n\t\"github.com/charmbracelet/x/term\"\n\t\"github.com/charmbracelet/x/xpty\"\n)\n\nfunc openPty(f *o"
},
{
"path": "spin/spin.go",
"chars": 4444,
"preview": "// Package spin provides a shell script interface for the spinner bubble.\n// https://github.com/charmbracelet/bubbles/tr"
},
{
"path": "spin/spinners.go",
"chars": 434,
"preview": "package spin\n\nimport \"github.com/charmbracelet/bubbles/spinner\"\n\nvar spinnerMap = map[string]spinner.Spinner{\n\t\"line\": "
},
{
"path": "style/ascii_a.txt",
"chars": 50,
"preview": " #\n # #\n # #\n# #\n#######\n# #\n# #\n"
},
{
"path": "style/borders.go",
"chars": 376,
"preview": "package style\n\nimport \"github.com/charmbracelet/lipgloss\"\n\n// Border maps strings to `lipgloss.Border`s.\nvar Border map["
},
{
"path": "style/command.go",
"chars": 958,
"preview": "// Package style provides a shell script interface for Lip Gloss.\n// https://github.com/charmbracelet/lipgloss\n//\n// It "
},
{
"path": "style/lipgloss.go",
"chars": 1411,
"preview": "package style\n\nimport (\n\t\"github.com/charmbracelet/lipgloss\"\n\n\t\"github.com/charmbracelet/gum/internal/decode\"\n)\n\n// ToLi"
},
{
"path": "style/options.go",
"chars": 4786,
"preview": "package style\n\n// Options is the customization options for the style command.\ntype Options struct {\n\tText []string "
},
{
"path": "style/spacing.go",
"chars": 1002,
"preview": "package style\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst (\n\tminTokens = 1\n\thalfTokens = 2\n\tmaxTokens = 4\n)\n\n// ParsePadd"
},
{
"path": "table/bom.csv",
"chars": 98,
"preview": "\"first_name\",\"last_name\",\"username\"\n\"Rob\",\"Pike\",rob\nKen,Thompson,ken\n\"Robert\",\"Griesemer\",\"gri\"\n"
},
{
"path": "table/comma.csv",
"chars": 187,
"preview": "Bubble Gum,Price,Ingredients\r\nStrawberry,$0.88,\"Water,Sugar\"\r\nGuava,$1.00,\"Guava Flavoring,Food Coloring,Xanthan Gum\"\r\nO"
},
{
"path": "table/command.go",
"chars": 4343,
"preview": "package table\n\nimport (\n\t\"encoding/csv\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/charmbracelet/bubbles/help\"\n\t\"github.com/charmbracele"
},
{
"path": "table/example.csv",
"chars": 279,
"preview": "Bubble Gum Flavor,Price\nStrawberry,$0.99\nCherry,$0.50\nBanana,$0.75\nOrange,$0.25\nLemon,$0.50\nLime,$0.50\nGrape,$0.50\nWater"
},
{
"path": "table/invalid.csv",
"chars": 267,
"preview": "Bubble Gum Flavor\nStrawberry,$0.99\nCherry,$0.50\nBanana,$0.75\nOrange\nLemon,$0.50\nLime,$0.50\nGrape,$0.50\nWatermelon,$0.50\n"
},
{
"path": "table/options.go",
"chars": 2001,
"preview": "package table\n\nimport (\n\t\"time\"\n\n\t\"github.com/charmbracelet/gum/style\"\n)\n\n// Options is the customization options for th"
},
{
"path": "table/table.go",
"chars": 2786,
"preview": "// Package table provides a shell script interface for the table bubble.\n// https://github.com/charmbracelet/bubbles/tre"
},
{
"path": "version/command.go",
"chars": 698,
"preview": "// Package version the version command.\npackage version\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/Masterminds/semver/v3\"\n\t\"github.c"
},
{
"path": "version/options.go",
"chars": 168,
"preview": "package version\n\n// Options is the set of options that can be used with version.\ntype Options struct {\n\tConstraint strin"
},
{
"path": "write/command.go",
"chars": 2225,
"preview": "package write\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/bubbles/help\"\n\t\"github.com/charmbr"
},
{
"path": "write/options.go",
"chars": 2919,
"preview": "package write\n\nimport (\n\t\"time\"\n\n\t\"github.com/charmbracelet/gum/style\"\n)\n\n// Options are the customization options for t"
},
{
"path": "write/write.go",
"chars": 4422,
"preview": "// Package write provides a shell script interface for the text area bubble.\n// https://github.com/charmbracelet/bubbles"
}
]
About this extraction
This page contains the full source code of the charmbracelet/gum GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 115 files (220.4 KB), approximately 69.9k tokens, and a symbol index with 215 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.