mirror of
https://github.com/charmbracelet/gum
synced 2024-05-10 10:16:36 +02:00
Compare commits
48 commits
Author | SHA1 | Date | |
---|---|---|---|
4222e59c25 | |||
ed0b62f7e9 | |||
7ad8d1b37b | |||
a0f96abea4 | |||
a4f52465e7 | |||
4bdcb2bc0c | |||
1a0111eaff | |||
f75dfa668f | |||
2a35019323 | |||
9ab722ca4f | |||
42f59ed330 | |||
1705593eb9 | |||
4d5d53169e | |||
2f0ea96504 | |||
589be38936 | |||
4a560b1953 | |||
3a717104a9 | |||
f7572e387e | |||
44906e23b9 | |||
598ee57330 | |||
4cc4611a34 | |||
de9f6b0397 | |||
f4d198396f | |||
2f2fa3bf00 | |||
396ddf86df | |||
5951e0612f | |||
491042b25f | |||
7ccd488d42 | |||
6255eaeb02 | |||
e4c4002496 | |||
7caf7d44ff | |||
2d896f777e | |||
7e5b494ae4 | |||
cd115c44e9 | |||
3a37defc82 | |||
6a275b423f | |||
4a00db207a | |||
7b16e873c7 | |||
4d75f110a7 | |||
a11d1ff648 | |||
d1145b4163 | |||
c9afacc74b | |||
5c65944c66 | |||
32c9d20692 | |||
76582446ec | |||
01a66511a1 | |||
fb6849ca16 | |||
c5aa973625 |
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* @maaslalani
|
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
|
@ -12,9 +12,9 @@ jobs:
|
|||
GO111MODULE: "on"
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.19
|
||||
go-version: ~1.21
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
|
4
.github/workflows/lint-soft.yml
vendored
4
.github/workflows/lint-soft.yml
vendored
|
@ -14,13 +14,13 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
uses: golangci/golangci-lint-action@v5
|
||||
with:
|
||||
# Optional: golangci-lint command line arguments.
|
||||
args: --config .golangci-soft.yml --issues-exit-code=0
|
||||
|
|
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
|
@ -14,13 +14,13 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
uses: golangci/golangci-lint-action@v5
|
||||
with:
|
||||
# Optional: golangci-lint command line arguments.
|
||||
#args:
|
||||
|
|
|
@ -23,7 +23,7 @@ linters:
|
|||
- gomnd
|
||||
- gomoddirectives
|
||||
- goprintffuncname
|
||||
- ifshort
|
||||
# - ifshort
|
||||
# - lll
|
||||
- misspell
|
||||
- nakedret
|
||||
|
@ -43,5 +43,4 @@ linters:
|
|||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unused
|
||||
- varcheck
|
||||
|
|
|
@ -4,6 +4,7 @@ includes:
|
|||
|
||||
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>"
|
||||
|
|
283
README.md
283
README.md
|
@ -14,7 +14,7 @@ A tool for glamorous shell scripts. Leverage the power of
|
|||
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://stuff.charm.sh/gum/demo.gif">
|
||||
<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)).
|
||||
|
||||
|
@ -22,70 +22,36 @@ The above example is running from a single shell script ([source](./examples/dem
|
|||
|
||||
Gum provides highly configurable, ready-to-use utilities to help you write
|
||||
useful shell scripts and dotfiles 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.
|
||||
|
||||
Let's build a simple script to help you write [Conventional
|
||||
Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) for your
|
||||
dotfiles.
|
||||
|
||||
Start with a `#!/bin/sh`.
|
||||
```bash
|
||||
#!/bin/sh
|
||||
```
|
||||
|
||||
Ask for the commit type with `gum choose`:
|
||||
|
||||
Ask for the commit type with gum choose:
|
||||
```bash
|
||||
gum choose "fix" "feat" "docs" "style" "refactor" "test" "chore" "revert"
|
||||
```
|
||||
|
||||
> Tip: 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 an (optional) scope for the commit:
|
||||
> [!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 a commit message:
|
||||
|
||||
Prompt for the summary and description of changes:
|
||||
```bash
|
||||
gum input --placeholder "Summary of this change"
|
||||
gum input --value "$TYPE$SCOPE: " --placeholder "Summary of this change"
|
||||
gum write --placeholder "Details of this change"
|
||||
```
|
||||
|
||||
Prompt for a detailed (multi-line) explanation of the changes:
|
||||
|
||||
```bash
|
||||
gum write --placeholder "Details of this change (CTRL+D to finish)"
|
||||
```
|
||||
|
||||
Prompt for a confirmation before committing:
|
||||
> `gum confirm` exits with status `0` if confirmed and status `1` if cancelled.
|
||||
|
||||
Confirm before committing:
|
||||
```bash
|
||||
gum confirm "Commit changes?" && git commit -m "$SUMMARY" -m "$DESCRIPTION"
|
||||
```
|
||||
|
||||
Putting it all together...
|
||||
Check out the [complete example](https://github.com/charmbracelet/gum/blob/main/examples/commit.sh) for combining these commands in a single script.
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
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 (CTRL+D to finish)")
|
||||
|
||||
# Commit these changes
|
||||
gum confirm "Commit changes?" && git commit -m "$SUMMARY" -m "$DESCRIPTION"
|
||||
```
|
||||
|
||||
<img alt="Running the ./examples/commit.sh script to commit to git" width="600" src="https://stuff.charm.sh/gum/commit_2.gif">
|
||||
<img alt="Running the ./examples/commit.sh script to commit to git" width="600" src="https://vhs.charm.sh/vhs-7rRq3LsEuJVwhwr0xf6Er7.gif">
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -100,29 +66,6 @@ pacman -S gum
|
|||
|
||||
# Nix
|
||||
nix-env -iA nixpkgs.gum
|
||||
# Or, with flakes
|
||||
nix run "github:charmbracelet/gum" -- --help
|
||||
|
||||
# Debian/Ubuntu
|
||||
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
|
||||
|
||||
# Fedora/RHEL
|
||||
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 yum install gum
|
||||
|
||||
# Alpine
|
||||
apk add gum
|
||||
|
||||
# Android (via termux)
|
||||
pkg install gum
|
||||
|
||||
# Windows (via WinGet or Scoop)
|
||||
winget install charmbracelet.gum
|
||||
|
@ -142,25 +85,40 @@ 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
|
||||
|
||||
`gum` is designed to be embedded in scripts and supports all sorts of use
|
||||
cases. Components are configurable and customizable to fit your theme and
|
||||
use case.
|
||||
|
||||
You can customize with `--flags`. See `gum <command> --help` for a full view of
|
||||
each command's customization and configuration options.
|
||||
|
||||
For example, let's use an `input` and change the cursor color, prompt color,
|
||||
prompt indicator, placeholder text, width, and pre-populate the value:
|
||||
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" --prompt "* " \
|
||||
--placeholder "What's up?" --width 80 --value "Not much, hby?"
|
||||
|
||||
gum input --cursor.foreground "#FF0" \
|
||||
--prompt.foreground "#0FF" \
|
||||
--placeholder "What's up?" \
|
||||
--prompt "* " \
|
||||
--width 80 \
|
||||
--value "Not much, hby?"
|
||||
```
|
||||
|
||||
You can also use `ENVIRONMENT_VARIABLES` to customize `gum` by default, this is
|
||||
useful to keep a consistent theme for all your `gum` commands.
|
||||
Customize with `ENVIRONMENT_VARIABLES`:
|
||||
|
||||
```bash
|
||||
export GUM_INPUT_CURSOR_FOREGROUND="#FF0"
|
||||
|
@ -169,70 +127,54 @@ export GUM_INPUT_PLACEHOLDER="What's up?"
|
|||
export GUM_INPUT_PROMPT="* "
|
||||
export GUM_INPUT_WIDTH=80
|
||||
|
||||
# Uses values configured through environment variables above but can still be
|
||||
# overridden with flags.
|
||||
# --flags can override values set with environment
|
||||
gum input
|
||||
```
|
||||
|
||||
<img alt="Gum input displaying most customization options" width="600" src="https://stuff.charm.sh/gum/customization.gif">
|
||||
<img alt="Gum input displaying most customization options" width="600" src="https://vhs.charm.sh/vhs-5zb9DlQYA70aL9ZpYLTwKv.gif">
|
||||
|
||||
## Interaction
|
||||
|
||||
#### Input
|
||||
## Input
|
||||
|
||||
Prompt for input with a simple command.
|
||||
|
||||
```bash
|
||||
gum input > answer.txt
|
||||
```
|
||||
|
||||
Prompt for sensitive input with the `--password` flag.
|
||||
|
||||
```bash
|
||||
gum input --password > password.txt
|
||||
```
|
||||
|
||||
<img src="https://stuff.charm.sh/gum/input_1.gif" width="600" alt="Shell running gum input typing Not much, you?" />
|
||||
<img src="https://vhs.charm.sh/vhs-1nScrStFI3BMlCp5yrLtyg.gif" width="600" alt="Shell running gum input typing Not much, you?" />
|
||||
|
||||
#### Write
|
||||
## Write
|
||||
|
||||
Prompt for some multi-line text.
|
||||
|
||||
Note: `CTRL+D` is used to complete text entry. `CTRL+C` and `esc` will cancel.
|
||||
Prompt for some multi-line text (`ctrl+d` to complete text entry).
|
||||
|
||||
```bash
|
||||
gum write > story.txt
|
||||
```
|
||||
|
||||
<img src="https://stuff.charm.sh/gum/write.gif" width="600" alt="Shell running gum write typing a story" />
|
||||
<img src="https://vhs.charm.sh/vhs-7abdKKrUEukgx9aJj8O5GX.gif" width="600" alt="Shell running gum write typing a story" />
|
||||
|
||||
#### Filter
|
||||
## Filter
|
||||
|
||||
Use fuzzy matching to filter a list of values:
|
||||
Filter a list of values with fuzzy matching:
|
||||
|
||||
```bash
|
||||
echo Strawberry >> flavors.txt
|
||||
echo Banana >> flavors.txt
|
||||
echo Cherry >> flavors.txt
|
||||
cat flavors.txt | gum filter > selection.txt
|
||||
gum filter < flavors.txt > selection.txt
|
||||
```
|
||||
|
||||
<img src="https://stuff.charm.sh/gum/filter.gif" width="600" alt="Shell running gum filter on different bubble gum flavors" />
|
||||
<img src="https://vhs.charm.sh/vhs-61euOQtKPtQVD7nDpHQhzr.gif" width="600" alt="Shell running gum filter on different bubble gum flavors" />
|
||||
|
||||
You can also select multiple items with the `--limit` flag, which determines
|
||||
the maximum number of items that can be chosen.
|
||||
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
|
||||
```
|
||||
|
||||
Or, allow any number of selections with the `--no-limit` flag.
|
||||
|
||||
```bash
|
||||
cat flavors.txt | gum filter --no-limit
|
||||
```
|
||||
|
||||
#### Choose
|
||||
## Choose
|
||||
|
||||
Choose an option from a list of choices.
|
||||
|
||||
|
@ -242,24 +184,17 @@ 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` flag, which determines
|
||||
You can also select multiple items with the `--limit` or `--no-limit` flag, which determines
|
||||
the maximum of items that can be chosen.
|
||||
|
||||
```bash
|
||||
echo "Pick your top 5 songs."
|
||||
cat songs.txt | gum choose --limit 5
|
||||
cat foods.txt | gum choose --no-limit --header "Grocery Shopping"
|
||||
```
|
||||
|
||||
Or, allow any number of selections with the `--no-limit` flag.
|
||||
<img src="https://vhs.charm.sh/vhs-3zV1LvofA6Cbn5vBu1NHHl.gif" width="600" alt="Shell running gum choose with numbers and gum flavors" />
|
||||
|
||||
```bash
|
||||
echo "What do you need from the grocery store?"
|
||||
cat foods.txt | gum choose --no-limit
|
||||
```
|
||||
|
||||
<img src="https://stuff.charm.sh/gum/choose.gif" width="600" alt="Shell running gum choose with numbers and gum flavors" />
|
||||
|
||||
#### Confirm
|
||||
## Confirm
|
||||
|
||||
Confirm whether to perform an action. Exits with code `0` (affirmative) or `1`
|
||||
(negative) depending on selection.
|
||||
|
@ -268,9 +203,9 @@ Confirm whether to perform an action. Exits with code `0` (affirmative) or `1`
|
|||
gum confirm && rm file.txt || echo "File not removed"
|
||||
```
|
||||
|
||||
<img src="https://stuff.charm.sh/gum/confirm_2.gif" width="600" alt="Shell running gum confirm" />
|
||||
<img src="https://vhs.charm.sh/vhs-3xRFvbeQ4lqGerbHY7y3q2.gif" width="600" alt="Shell running gum confirm" />
|
||||
|
||||
#### File
|
||||
## File
|
||||
|
||||
Prompt the user to select a file from the file tree.
|
||||
|
||||
|
@ -278,9 +213,9 @@ Prompt the user to select a file from the file tree.
|
|||
EDITOR $(gum file $HOME)
|
||||
```
|
||||
|
||||
<img src="https://stuff.charm.sh/gum/file.gif" width="600" alt="Shell running gum file" />
|
||||
<img src="https://vhs.charm.sh/vhs-2RMRqmnOPneneIgVJJ3mI1.gif" width="600" alt="Shell running gum file" />
|
||||
|
||||
#### Pager
|
||||
## Pager
|
||||
|
||||
Scroll through a long document with line numbers and a fully customizable viewport.
|
||||
|
||||
|
@ -288,9 +223,9 @@ Scroll through a long document with line numbers and a fully customizable viewpo
|
|||
gum pager < README.md
|
||||
```
|
||||
|
||||
<img src="https://stuff.charm.sh/gum/pager.gif" width="600" alt="Shell running gum pager" />
|
||||
<img src="https://vhs.charm.sh/vhs-3iMDpgOLmbYr0jrYEGbk7p.gif" width="600" alt="Shell running gum pager" />
|
||||
|
||||
#### Spin
|
||||
## Spin
|
||||
|
||||
Display a spinner while running a script or command. The spinner will
|
||||
automatically stop after the given command exits.
|
||||
|
@ -301,11 +236,11 @@ To view or pipe the command's output, use the `--show-output` flag.
|
|||
gum spin --spinner dot --title "Buying Bubble Gum..." -- sleep 5
|
||||
```
|
||||
|
||||
<img src="https://stuff.charm.sh/gum/spin.gif" width="600" alt="Shell running gum spin while sleeping for 5 seconds" />
|
||||
<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
|
||||
## Table
|
||||
|
||||
Select a row from some tabular data.
|
||||
|
||||
|
@ -313,11 +248,9 @@ Select a row from some tabular data.
|
|||
gum table < flavors.csv | cut -d ',' -f 1
|
||||
```
|
||||
|
||||
<img src="https://stuff.charm.sh/gum/table.gif" width="600" alt="Shell running gum table" />
|
||||
<!-- <img src="https://stuff.charm.sh/gum/table.gif" width="600" alt="Shell running gum table" /> -->
|
||||
|
||||
## Styling
|
||||
|
||||
#### Style
|
||||
## Style
|
||||
|
||||
Pretty print any string with any layout with one command.
|
||||
|
||||
|
@ -328,11 +261,9 @@ gum style \
|
|||
'Bubble Gum (1¢)' 'So sweet and so fresh!'
|
||||
```
|
||||
|
||||
<img src="https://stuff.charm.sh/gum/style.gif" width="600" alt="Bubble Gum, 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!" />
|
||||
|
||||
## Layout
|
||||
|
||||
#### Join
|
||||
## Join
|
||||
|
||||
Combine text vertically or horizontally. Use this command with `gum style` to
|
||||
build layouts and pretty output.
|
||||
|
@ -351,7 +282,7 @@ BUBBLE_GUM=$(gum join "$BUBBLE" "$GUM")
|
|||
gum join --align center --vertical "$I_LOVE" "$BUBBLE_GUM"
|
||||
```
|
||||
|
||||
<img src="https://stuff.charm.sh/gum/join.gif" width="600" alt="I LOVE Bubble Gum written out in four boxes with double borders around them." />
|
||||
<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
|
||||
|
||||
|
@ -378,7 +309,7 @@ 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://stuff.charm.sh/gum/format.gif" width="600" alt="Running gum format for different types of formats" />
|
||||
<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
|
||||
|
||||
|
@ -393,116 +324,80 @@ gum log --structured --level debug "Creating file..." name file.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
|
||||
|
||||
See the [examples](./examples/) directory for more real world use cases.
|
||||
|
||||
How to use `gum` in your daily workflows:
|
||||
|
||||
#### Write a commit message
|
||||
See the [examples](./examples/) directory for more real world use cases.
|
||||
|
||||
Prompt for input to write git commit messages with a short summary and
|
||||
longer details with `gum input` and `gum write`.
|
||||
|
||||
Bonus points: use `gum filter` with the [Conventional Commits
|
||||
Specification](https://www.conventionalcommits.org/en/v1.0.0/#summary) as a
|
||||
prefix for your commit message.
|
||||
* 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 (CTRL+D to finish)")"
|
||||
-m "$(gum write --width 80 --placeholder "Details of changes")"
|
||||
```
|
||||
|
||||
#### Open files in your `$EDITOR`
|
||||
|
||||
By default, `gum filter` will display a list of all files (searched
|
||||
recursively) through your current directory, with some sensible ignore settings
|
||||
(`.git`, `node_modules`). You can use this command to easily to pick a file and
|
||||
open it in your `$EDITOR`.
|
||||
* Open files in your `$EDITOR`
|
||||
|
||||
```bash
|
||||
$EDITOR $(gum filter)
|
||||
```
|
||||
|
||||
#### Connect to a TMUX session
|
||||
|
||||
Pick from a running `tmux` session and attach to it. Or, if you're already in a
|
||||
`tmux` session, switch sessions.
|
||||
* 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
|
||||
```
|
||||
|
||||
<img src="https://stuff.charm.sh/gum/pick-tmux-session.gif" width="600" alt="Picking a tmux session with gum filter" />
|
||||
|
||||
#### Pick commit hash from your Git history
|
||||
|
||||
Filter through your git history searching for commit messages, copying the
|
||||
commit hash of the commit you select.
|
||||
* Pick a commit hash from `git` history
|
||||
|
||||
```bash
|
||||
git log --oneline | gum filter | cut -d' ' -f1 # | copy
|
||||
```
|
||||
|
||||
<img src="https://stuff.charm.sh/gum/pick-commit.gif" width="600" alt="Picking a commit with gum filter" />
|
||||
|
||||
#### Skate Passwords
|
||||
|
||||
Build a simple (encrypted) password selector with [Skate](https://github.com/charmbracelet/skate).
|
||||
|
||||
Save all your passwords to [Skate](https://github.com/charmbracelet/skate) with `skate set github@pass.db PASSWORD`, etc...
|
||||
* Simple [`skate`](https://github.com/charmbracelet/skate) password selector.
|
||||
|
||||
```
|
||||
skate list -k | gum filter | xargs skate get
|
||||
```
|
||||
|
||||
<img src="https://stuff.charm.sh/gum/skate-pass.gif" width="600" alt="Selecting a skate value with gum" />
|
||||
|
||||
#### Choose packages to uninstall
|
||||
|
||||
List all packages installed by your package manager (we'll use `brew`) and
|
||||
choose which packages to uninstall.
|
||||
* Uninstall packages
|
||||
|
||||
```bash
|
||||
brew list | gum choose --no-limit | xargs brew uninstall
|
||||
```
|
||||
|
||||
#### Choose branches to delete
|
||||
|
||||
List all branches and choose which branches to delete.
|
||||
* Clean up `git` branches
|
||||
|
||||
```bash
|
||||
git branch | cut -c 3- | gum choose --no-limit | xargs git branch -D
|
||||
```
|
||||
|
||||
#### Choose pull request to checkout
|
||||
|
||||
List all PRs for the current GitHub repository and checkout the chosen PR (using [`gh`](https://cli.github.com/)).
|
||||
* 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
|
||||
```
|
||||
|
||||
#### Pick command from shell history
|
||||
|
||||
Pick a previously executed command from your shell history to execute, copy,
|
||||
edit, etc...
|
||||
* Copy command from shell history
|
||||
|
||||
```bash
|
||||
gum filter < $HISTFILE --height 20
|
||||
```
|
||||
|
||||
#### Sudo password input
|
||||
|
||||
See visual feedback when entering password with masked characters with `gum
|
||||
input --password`.
|
||||
* `sudo` replacement
|
||||
|
||||
```bash
|
||||
alias please="gum input --password | sudo -nS"
|
||||
|
|
220
choose/choose.go
220
choose/choose.go
|
@ -1,220 +0,0 @@
|
|||
// 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"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
|
||||
"github.com/charmbracelet/bubbles/paginator"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type model struct {
|
||||
height int
|
||||
cursor string
|
||||
selectedPrefix string
|
||||
unselectedPrefix string
|
||||
cursorPrefix string
|
||||
header string
|
||||
items []item
|
||||
quitting bool
|
||||
index int
|
||||
limit int
|
||||
numSelected int
|
||||
currentOrder int
|
||||
paginator paginator.Model
|
||||
aborted bool
|
||||
|
||||
// styles
|
||||
cursorStyle lipgloss.Style
|
||||
headerStyle lipgloss.Style
|
||||
itemStyle lipgloss.Style
|
||||
selectedItemStyle lipgloss.Style
|
||||
hasTimeout bool
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
type item struct {
|
||||
text string
|
||||
selected bool
|
||||
order int
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return timeout.Init(m.timeout, nil)
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
return m, nil
|
||||
case timeout.TickTimeoutMsg:
|
||||
if msg.TimeoutValue <= 0 {
|
||||
m.quitting = true
|
||||
// If the user hasn't selected any items in a multi-select.
|
||||
// Then we select the item that they have pressed enter on. If they
|
||||
// have selected items, then we simply return them.
|
||||
if m.numSelected < 1 {
|
||||
m.items[m.index].selected = true
|
||||
}
|
||||
return m, tea.Quit
|
||||
}
|
||||
m.timeout = msg.TimeoutValue
|
||||
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
|
||||
case tea.KeyMsg:
|
||||
start, end := m.paginator.GetSliceBounds(len(m.items))
|
||||
switch keypress := msg.String(); keypress {
|
||||
case "down", "j", "ctrl+j", "ctrl+n":
|
||||
m.index++
|
||||
if m.index >= len(m.items) {
|
||||
m.index = 0
|
||||
m.paginator.Page = 0
|
||||
}
|
||||
if m.index >= end {
|
||||
m.paginator.NextPage()
|
||||
}
|
||||
case "up", "k", "ctrl+k", "ctrl+p":
|
||||
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 "right", "l", "ctrl+f":
|
||||
m.index = clamp(m.index+m.height, 0, len(m.items)-1)
|
||||
m.paginator.NextPage()
|
||||
case "left", "h", "ctrl+b":
|
||||
m.index = clamp(m.index-m.height, 0, len(m.items)-1)
|
||||
m.paginator.PrevPage()
|
||||
case "G", "end":
|
||||
m.index = len(m.items) - 1
|
||||
m.paginator.Page = m.paginator.TotalPages - 1
|
||||
case "g", "home":
|
||||
m.index = 0
|
||||
m.paginator.Page = 0
|
||||
case "a":
|
||||
if m.limit <= 1 {
|
||||
break
|
||||
}
|
||||
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++
|
||||
}
|
||||
case "A":
|
||||
if m.limit <= 1 {
|
||||
break
|
||||
}
|
||||
for i := range m.items {
|
||||
m.items[i].selected = false
|
||||
m.items[i].order = 0
|
||||
}
|
||||
m.numSelected = 0
|
||||
m.currentOrder = 0
|
||||
case "ctrl+c", "esc":
|
||||
m.aborted = true
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
case " ", "tab", "x", "ctrl+@":
|
||||
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 "enter":
|
||||
m.quitting = true
|
||||
if m.limit <= 1 && m.numSelected < 1 {
|
||||
m.items[m.index].selected = true
|
||||
}
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.paginator, cmd = m.paginator.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
if m.quitting {
|
||||
return ""
|
||||
}
|
||||
|
||||
var s strings.Builder
|
||||
var timeoutStr string
|
||||
|
||||
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 {
|
||||
if m.hasTimeout {
|
||||
timeoutStr = timeout.Str(m.timeout)
|
||||
}
|
||||
s.WriteString(m.selectedItemStyle.Render(m.selectedPrefix + item.text + timeoutStr))
|
||||
} else if i == m.index%m.height {
|
||||
s.WriteString(m.cursorStyle.Render(m.cursorPrefix + item.text))
|
||||
} else {
|
||||
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())
|
||||
}
|
||||
|
||||
if m.header != "" {
|
||||
header := m.headerStyle.Render(m.header)
|
||||
return lipgloss.JoinVertical(lipgloss.Left, header, s.String())
|
||||
}
|
||||
|
||||
return s.String()
|
||||
}
|
||||
|
||||
//nolint:unparam
|
||||
func clamp(x, min, max int) int {
|
||||
if x < min {
|
||||
return min
|
||||
}
|
||||
if x > max {
|
||||
return max
|
||||
}
|
||||
return x
|
||||
}
|
|
@ -4,30 +4,20 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/charmbracelet/bubbles/paginator"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/mattn/go-isatty"
|
||||
|
||||
"github.com/charmbracelet/gum/ansi"
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
"github.com/charmbracelet/gum/internal/stdin"
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
var (
|
||||
subduedStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#847A85", Dark: "#979797"})
|
||||
verySubduedStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"})
|
||||
)
|
||||
|
||||
// Run provides a shell script interface for choosing between different through
|
||||
// options.
|
||||
func (o Options) Run() error {
|
||||
if len(o.Options) == 0 {
|
||||
if len(o.Options) <= 0 {
|
||||
input, _ := stdin.Read()
|
||||
if input == "" {
|
||||
return errors.New("no options provided, see `gum choose --help`")
|
||||
|
@ -35,133 +25,106 @@ func (o Options) Run() error {
|
|||
o.Options = strings.Split(strings.TrimSuffix(input, "\n"), "\n")
|
||||
}
|
||||
|
||||
// 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.SelectIfOne && len(o.Options) == 1 {
|
||||
fmt.Println(o.Options[0])
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we've set no limit then we can simply select as many options as there
|
||||
// are so let's set the limit to the number of options.
|
||||
if o.NoLimit {
|
||||
o.Limit = len(o.Options) + 1
|
||||
}
|
||||
theme := huh.ThemeCharm()
|
||||
options := huh.NewOptions(o.Options...)
|
||||
|
||||
if len(o.Selected) > o.Limit {
|
||||
return errors.New("number of selected options cannot be greater than the limit")
|
||||
}
|
||||
theme.Focused.Base.Border(lipgloss.Border{})
|
||||
theme.Focused.Title = o.HeaderStyle.ToLipgloss()
|
||||
theme.Focused.SelectSelector = o.CursorStyle.ToLipgloss().SetString(o.Cursor)
|
||||
theme.Focused.MultiSelectSelector = o.CursorStyle.ToLipgloss().SetString(o.Cursor)
|
||||
theme.Focused.SelectedOption = o.SelectedItemStyle.ToLipgloss()
|
||||
theme.Focused.UnselectedOption = o.ItemStyle.ToLipgloss()
|
||||
theme.Focused.SelectedPrefix = o.SelectedItemStyle.ToLipgloss().SetString(o.SelectedPrefix)
|
||||
theme.Focused.UnselectedPrefix = o.ItemStyle.ToLipgloss().SetString(o.UnselectedPrefix)
|
||||
|
||||
// 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 && arrayContains(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++
|
||||
for _, s := range o.Selected {
|
||||
for i, opt := range options {
|
||||
if s == opt.Key || s == opt.Value {
|
||||
options[i] = opt.Selected(true)
|
||||
}
|
||||
}
|
||||
|
||||
items[i] = item{text: option, selected: isSelected, order: order}
|
||||
}
|
||||
|
||||
// Use the pagination model to display the current and total number of
|
||||
// pages.
|
||||
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
|
||||
if o.NoLimit {
|
||||
o.Limit = len(o.Options)
|
||||
}
|
||||
|
||||
// Disable Keybindings since we will control it ourselves.
|
||||
tm, err := tea.NewProgram(model{
|
||||
index: startingIndex,
|
||||
currentOrder: currentOrder,
|
||||
height: o.Height,
|
||||
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,
|
||||
hasTimeout: o.Timeout > 0,
|
||||
timeout: o.Timeout,
|
||||
}, tea.WithOutput(os.Stderr)).Run()
|
||||
width := max(widest(o.Options)+
|
||||
max(lipgloss.Width(o.SelectedPrefix), lipgloss.Width(o.UnselectedPrefix))+
|
||||
lipgloss.Width(o.Cursor)+1, lipgloss.Width(o.Header)+1)
|
||||
|
||||
if o.Limit > 1 {
|
||||
var choices []string
|
||||
err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewMultiSelect[string]().
|
||||
Options(options...).
|
||||
Title(o.Header).
|
||||
Filterable(false).
|
||||
Height(o.Height).
|
||||
Limit(o.Limit).
|
||||
Value(&choices),
|
||||
),
|
||||
).
|
||||
WithWidth(width).
|
||||
WithShowHelp(false).
|
||||
WithTheme(theme).
|
||||
Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(choices) > 0 {
|
||||
s := strings.Join(choices, "\n")
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
fmt.Println(s)
|
||||
} else {
|
||||
fmt.Print(ansi.Strip(s))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var choice string
|
||||
|
||||
err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().
|
||||
Options(options...).
|
||||
Title(o.Header).
|
||||
Height(o.Height).
|
||||
Value(&choice),
|
||||
),
|
||||
).
|
||||
WithWidth(width).
|
||||
WithTheme(theme).
|
||||
WithShowHelp(false).
|
||||
Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start tea program: %w", err)
|
||||
}
|
||||
|
||||
m := tm.(model)
|
||||
if m.aborted {
|
||||
return exit.ErrAborted
|
||||
}
|
||||
|
||||
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 s strings.Builder
|
||||
|
||||
for _, item := range m.items {
|
||||
if item.selected {
|
||||
s.WriteString(item.text)
|
||||
s.WriteRune('\n')
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
fmt.Print(s.String())
|
||||
fmt.Println(choice)
|
||||
} else {
|
||||
fmt.Print(ansi.Strip(s.String()))
|
||||
fmt.Print(ansi.Strip(choice))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeReset hook. Used to unclutter style flags.
|
||||
func (o Options) BeforeReset(ctx *kong.Context) error {
|
||||
style.HideFlags(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if an array contains a value.
|
||||
func arrayContains(strArray []string, value string) bool {
|
||||
for _, str := range strArray {
|
||||
if str == value {
|
||||
return true
|
||||
func widest(options []string) int {
|
||||
var max int
|
||||
for _, o := range options {
|
||||
w := lipgloss.Width(o)
|
||||
if w > max {
|
||||
max = w
|
||||
}
|
||||
}
|
||||
return false
|
||||
return max
|
||||
}
|
||||
|
|
|
@ -15,12 +15,13 @@ type Options struct {
|
|||
Height int `help:"Height of the list" default:"10" env:"GUM_CHOOSE_HEIGHT"`
|
||||
Cursor string `help:"Prefix to show on item that corresponds to the cursor position" default:"> " env:"GUM_CHOOSE_CURSOR"`
|
||||
Header string `help:"Header value" default:"" env:"GUM_CHOOSE_HEADER"`
|
||||
CursorPrefix string `help:"Prefix to show on the cursor item (hidden if limit is 1)" default:"○ " env:"GUM_CHOOSE_CURSOR_PREFIX"`
|
||||
SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:"◉ " env:"GUM_CHOOSE_SELECTED_PREFIX"`
|
||||
UnselectedPrefix string `help:"Prefix to show on unselected items (hidden if limit is 1)" default:"○ " env:"GUM_CHOOSE_UNSELECTED_PREFIX"`
|
||||
CursorPrefix string `help:"Prefix to show on the cursor item (hidden if limit is 1)" default:"• " env:"GUM_CHOOSE_CURSOR_PREFIX"`
|
||||
SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:"✓ " env:"GUM_CHOOSE_SELECTED_PREFIX"`
|
||||
UnselectedPrefix string `help:"Prefix to show on unselected items (hidden if limit is 1)" default:"• " env:"GUM_CHOOSE_UNSELECTED_PREFIX"`
|
||||
Selected []string `help:"Options that should start as selected" default:"" env:"GUM_CHOOSE_SELECTED"`
|
||||
SelectIfOne bool `help:"Select the given option if there is only one" group:"Selection"`
|
||||
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_CURSOR_"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_CHOOSE_HEADER_"`
|
||||
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_"`
|
||||
Timeout time.Duration `help:"Timeout until choose returns selected element" default:"0" env:"GUM_CCHOOSE_TIMEOUT"` // including timeout command options [Timeout,...]
|
||||
|
|
|
@ -4,47 +4,41 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"github.com/charmbracelet/gum/style"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
m, err := tea.NewProgram(model{
|
||||
affirmative: o.Affirmative,
|
||||
negative: o.Negative,
|
||||
confirmation: o.Default,
|
||||
defaultSelection: o.Default,
|
||||
timeout: o.Timeout,
|
||||
hasTimeout: o.Timeout > 0,
|
||||
prompt: o.Prompt,
|
||||
selectedStyle: o.SelectedStyle.ToLipgloss(),
|
||||
unselectedStyle: o.UnselectedStyle.ToLipgloss(),
|
||||
promptStyle: o.PromptStyle.ToLipgloss(),
|
||||
}, tea.WithOutput(os.Stderr)).Run()
|
||||
theme := huh.ThemeCharm()
|
||||
theme.Focused.Base = lipgloss.NewStyle().Margin(0, 1)
|
||||
theme.Focused.Title = o.PromptStyle.ToLipgloss()
|
||||
theme.Focused.FocusedButton = o.SelectedStyle.ToLipgloss()
|
||||
theme.Focused.BlurredButton = o.UnselectedStyle.ToLipgloss()
|
||||
|
||||
choice := o.Default
|
||||
|
||||
err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewConfirm().
|
||||
Affirmative(o.Affirmative).
|
||||
Negative(o.Negative).
|
||||
Title(o.Prompt).
|
||||
Value(&choice),
|
||||
),
|
||||
).
|
||||
WithTheme(theme).
|
||||
WithShowHelp(false).
|
||||
Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to run confirm: %w", err)
|
||||
}
|
||||
|
||||
if m.(model).aborted {
|
||||
os.Exit(exit.StatusAborted)
|
||||
} else if m.(model).confirmation {
|
||||
os.Exit(0)
|
||||
if !choice {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeReset hook. Used to unclutter style flags.
|
||||
func (o Options) BeforeReset(ctx *kong.Context) error {
|
||||
style.HideFlags(ctx)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,121 +0,0 @@
|
|||
// 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 (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type model struct {
|
||||
prompt string
|
||||
affirmative string
|
||||
negative string
|
||||
quitting bool
|
||||
aborted bool
|
||||
hasTimeout bool
|
||||
timeout time.Duration
|
||||
|
||||
confirmation bool
|
||||
|
||||
defaultSelection bool
|
||||
|
||||
// styles
|
||||
promptStyle lipgloss.Style
|
||||
selectedStyle lipgloss.Style
|
||||
unselectedStyle lipgloss.Style
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return timeout.Init(m.timeout, m.defaultSelection)
|
||||
}
|
||||
|
||||
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 msg.String() {
|
||||
case "ctrl+c":
|
||||
m.confirmation = false
|
||||
m.aborted = true
|
||||
fallthrough
|
||||
case "esc":
|
||||
m.confirmation = false
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
case "q", "n", "N":
|
||||
m.confirmation = false
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
case "left", "h", "ctrl+p", "tab",
|
||||
"right", "l", "ctrl+n", "shift+tab":
|
||||
if m.negative == "" {
|
||||
break
|
||||
}
|
||||
m.confirmation = !m.confirmation
|
||||
case "enter":
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
case "y", "Y":
|
||||
m.quitting = true
|
||||
m.confirmation = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
case timeout.TickTimeoutMsg:
|
||||
|
||||
if msg.TimeoutValue <= 0 {
|
||||
m.quitting = true
|
||||
m.confirmation = m.defaultSelection
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
m.timeout = msg.TimeoutValue
|
||||
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
if m.quitting {
|
||||
return ""
|
||||
}
|
||||
|
||||
var aff, neg, timeoutStrYes, timeoutStrNo string
|
||||
timeoutStrNo = ""
|
||||
timeoutStrYes = ""
|
||||
if m.hasTimeout {
|
||||
if m.defaultSelection {
|
||||
timeoutStrYes = timeout.Str(m.timeout)
|
||||
} else {
|
||||
timeoutStrNo = timeout.Str(m.timeout)
|
||||
}
|
||||
}
|
||||
|
||||
if m.confirmation {
|
||||
aff = m.selectedStyle.Render(m.affirmative + timeoutStrYes)
|
||||
neg = m.unselectedStyle.Render(m.negative + timeoutStrNo)
|
||||
} else {
|
||||
aff = m.unselectedStyle.Render(m.affirmative + timeoutStrYes)
|
||||
neg = m.selectedStyle.Render(m.negative + timeoutStrNo)
|
||||
}
|
||||
|
||||
// If the option is intentionally empty, do not show it.
|
||||
if m.negative == "" {
|
||||
neg = ""
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Center, m.promptStyle.Render(m.prompt), lipgloss.JoinHorizontal(lipgloss.Left, aff, neg))
|
||||
}
|
|
@ -12,10 +12,10 @@ type Options struct {
|
|||
Affirmative string `help:"The title of the affirmative action" default:"Yes"`
|
||||
Negative string `help:"The title of the negative action" default:"No"`
|
||||
Prompt string `arg:"" help:"Prompt to display." default:"Are you sure?"`
|
||||
PromptStyle style.Styles `embed:"" prefix:"prompt." help:"The style of the prompt" set:"defaultMargin=1 0 0 0" envprefix:"GUM_CONFIRM_PROMPT_"`
|
||||
PromptStyle style.Styles `embed:"" prefix:"prompt." help:"The style of the prompt" set:"defaultMargin=1 0 0 1" envprefix:"GUM_CONFIRM_PROMPT_"`
|
||||
//nolint:staticcheck
|
||||
SelectedStyle style.Styles `embed:"" prefix:"selected." help:"The style of the selected action" set:"defaultBackground=212" set:"defaultForeground=230" set:"defaultPadding=0 3" set:"defaultMargin=1 1" envprefix:"GUM_CONFIRM_SELECTED_"`
|
||||
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=1 1" envprefix:"GUM_CONFIRM_UNSELECTED_"`
|
||||
UnselectedStyle style.Styles `embed:"" prefix:"unselected." help:"The style of the unselected action" set:"defaultBackground=235" set:"defaultForeground=254" set:"defaultPadding=0 3" set:"defaultMargin=0 1" envprefix:"GUM_CONFIRM_UNSELECTED_"`
|
||||
Timeout time.Duration `help:"Timeout until confirm returns selected value or default if provided" default:"0" env:"GUM_CONFIRM_TIMEOUT"`
|
||||
}
|
||||
|
|
2
examples/.gitignore
vendored
Normal file
2
examples/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*.gif
|
||||
*.png
|
36
examples/README.md
Normal file
36
examples/README.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
# 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
|
26
examples/choose.tape
Normal file
26
examples/choose.tape
Normal file
|
@ -0,0 +1,26 @@
|
|||
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
|
||||
|
|
@ -10,9 +10,9 @@
|
|||
#
|
||||
# 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
|
||||
# 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")
|
||||
|
|
45
examples/commit.tape
Normal file
45
examples/commit.tape
Normal file
|
@ -0,0 +1,45 @@
|
|||
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
|
26
examples/confirm.tape
Normal file
26
examples/confirm.tape
Normal file
|
@ -0,0 +1,26 @@
|
|||
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
|
19
examples/customize.tape
Normal file
19
examples/customize.tape
Normal file
|
@ -0,0 +1,19 @@
|
|||
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
|
||||
|
|
@ -5,7 +5,7 @@ NAME=$(gum input --placeholder "What is your name?")
|
|||
|
||||
echo -e "Well, it is nice to meet you, $(gum style --foreground 212 "$NAME")."
|
||||
|
||||
sleep 2; clear
|
||||
sleep 1; clear
|
||||
|
||||
echo -e "Can you tell me a $(gum style --italic --foreground 99 'secret')?\n"
|
||||
|
||||
|
@ -14,7 +14,7 @@ gum write --placeholder "I'll keep it to myself, I promise!" > /dev/null # we ke
|
|||
clear; echo "What should I do with this information?"; sleep 1
|
||||
|
||||
READ="Read"; THINK="Think"; DISCARD="Discard"
|
||||
ACTIONS=$(gum choose --cursor-prefix "[ ] " --selected-prefix "[✓] " --no-limit "$READ" "$THINK" "$DISCARD")
|
||||
ACTIONS=$(gum choose --no-limit "$READ" "$THINK" "$DISCARD")
|
||||
|
||||
clear; echo "One moment, please."
|
||||
|
||||
|
@ -24,8 +24,7 @@ grep -q "$DISCARD" <<< "$ACTIONS" && gum spin -s monkey --title " Discarding you
|
|||
|
||||
sleep 1; clear
|
||||
|
||||
echo "What's your favorite $(gum style --foreground 212 "Gum") flavor?"
|
||||
GUM=$(echo -e "Cherry\nGrape\nLime\nOrange" | gum filter)
|
||||
GUM=$(echo -e "Cherry\nGrape\nLime\nOrange" | gum filter --placeholder "Favorite flavor?")
|
||||
echo "I'll keep that in mind!"
|
||||
|
||||
sleep 1; clear
|
||||
|
@ -39,10 +38,10 @@ CHOICE=$(gum choose --item.foreground 250 "Yes" "No" "It's complicated")
|
|||
|
||||
sleep 1
|
||||
|
||||
gum spin --title "Chewing some $(gum style --foreground "#04B575" "$GUM") bubble gum..." -- sleep 5
|
||||
gum spin --title "Chewing some $(gum style --foreground "#04B575" "$GUM") bubble gum..." -- sleep 2.5
|
||||
|
||||
clear
|
||||
|
||||
NICE_MEETING_YOU=$(gum style --height 5 --width 25 --padding '1 3' --border double --border-foreground 57 "Well, it was nice meeting you, $(gum style --foreground 212 "$NAME"). Hope to see you soon!")
|
||||
CHEW_BUBBLE_GUM=$(gum style --width 25 --padding '1 3' --border double --border-foreground 212 "Don't forget to chew some $(gum style --foreground "#04B575" "$GUM") bubble gum.")
|
||||
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"
|
||||
|
|
49
examples/demo.tape
Normal file
49
examples/demo.tape
Normal file
|
@ -0,0 +1,49 @@
|
|||
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
|
1
examples/fav.txt
Normal file
1
examples/fav.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Banana
|
15
examples/file.tape
Normal file
15
examples/file.tape
Normal file
|
@ -0,0 +1,15 @@
|
|||
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
|
||||
|
4
examples/flavors.txt
Normal file
4
examples/flavors.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
Banana
|
||||
Cherry
|
||||
Orange
|
||||
Strawberry
|
12
examples/format.ansi
Normal file
12
examples/format.ansi
Normal file
|
@ -0,0 +1,12 @@
|
|||
[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
|
16
examples/input.tape
Normal file
16
examples/input.tape
Normal file
|
@ -0,0 +1,16 @@
|
|||
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
|
||||
|
7
examples/main.go
Normal file
7
examples/main.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("Charm_™ Gum")
|
||||
}
|
15
examples/pager.tape
Normal file
15
examples/pager.tape
Normal file
|
@ -0,0 +1,15 @@
|
|||
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
|
||||
|
13
examples/spin.tape
Normal file
13
examples/spin.tape
Normal file
|
@ -0,0 +1,13 @@
|
|||
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
|
||||
|
2
examples/story.txt
Normal file
2
examples/story.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Once upon a time
|
||||
In a land far, far away....
|
21
examples/write.tape
Normal file
21
examples/write.tape
Normal file
|
@ -0,0 +1,21 @@
|
|||
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
|
||||
|
|
@ -3,15 +3,10 @@ package file
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/charmbracelet/bubbles/filepicker"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/gum/style"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// Run is the interface to picking a file.
|
||||
|
@ -29,52 +24,36 @@ func (o Options) Run() error {
|
|||
return fmt.Errorf("file not found: %w", err)
|
||||
}
|
||||
|
||||
fp := filepicker.New()
|
||||
fp.CurrentDirectory = path
|
||||
fp.Path = path
|
||||
fp.Height = o.Height
|
||||
fp.AutoHeight = o.Height == 0
|
||||
fp.Cursor = o.Cursor
|
||||
fp.DirAllowed = o.Directory
|
||||
fp.FileAllowed = o.File
|
||||
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()
|
||||
theme := huh.ThemeCharm()
|
||||
theme.Focused.Base = lipgloss.NewStyle()
|
||||
theme.Focused.File = o.FileStyle.ToLipgloss()
|
||||
theme.Focused.Directory = o.DirectoryStyle.ToLipgloss()
|
||||
theme.Focused.SelectedOption = o.SelectedStyle.ToLipgloss()
|
||||
|
||||
m := model{
|
||||
filepicker: fp,
|
||||
timeout: o.Timeout,
|
||||
hasTimeout: o.Timeout > 0,
|
||||
aborted: false,
|
||||
}
|
||||
// XXX: These should be file selected specific.
|
||||
theme.Focused.TextInput.Placeholder = o.PermissionsStyle.ToLipgloss()
|
||||
theme.Focused.TextInput.Prompt = o.CursorStyle.ToLipgloss()
|
||||
|
||||
err = huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewFilePicker().
|
||||
Picking(true).
|
||||
CurrentDirectory(path).
|
||||
DirAllowed(o.Directory).
|
||||
FileAllowed(o.File).
|
||||
Height(o.Height).
|
||||
ShowHidden(o.All).
|
||||
Value(&path),
|
||||
),
|
||||
).
|
||||
WithShowHelp(false).
|
||||
WithTheme(theme).
|
||||
Run()
|
||||
|
||||
tm, err := tea.NewProgram(&m, tea.WithOutput(os.Stderr)).Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to pick selection: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
m = tm.(model)
|
||||
if m.aborted {
|
||||
return exit.ErrAborted
|
||||
}
|
||||
|
||||
if m.selectedPath == "" {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println(m.selectedPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeReset hook. Used to unclutter style flags.
|
||||
func (o Options) BeforeReset(ctx *kong.Context) error {
|
||||
style.HideFlags(ctx)
|
||||
fmt.Println(path)
|
||||
return nil
|
||||
}
|
||||
|
|
72
file/file.go
72
file/file.go
|
@ -1,72 +0,0 @@
|
|||
// 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 (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/filepicker"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
)
|
||||
|
||||
type model struct {
|
||||
filepicker filepicker.Model
|
||||
selectedPath string
|
||||
aborted bool
|
||||
quitting bool
|
||||
timeout time.Duration
|
||||
hasTimeout bool
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
timeout.Init(m.timeout, nil),
|
||||
m.filepicker.Init(),
|
||||
)
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q", "esc":
|
||||
m.aborted = true
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
case timeout.TickTimeoutMsg:
|
||||
if msg.TimeoutValue <= 0 {
|
||||
m.quitting = true
|
||||
m.aborted = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
m.timeout = msg.TimeoutValue
|
||||
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.filepicker, cmd = m.filepicker.Update(msg)
|
||||
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 ""
|
||||
}
|
||||
return m.filepicker.View()
|
||||
}
|
|
@ -16,7 +16,7 @@ type Options struct {
|
|||
File bool `help:"Allow files selection" default:"true" env:"GUM_FILE_FILE"`
|
||||
Directory bool `help:"Allow directories selection" default:"false" env:"GUM_FILE_DIRECTORY"`
|
||||
|
||||
Height int `help:"Maximum number of files to display" default:"0" env:"GUM_FILE_HEIGHT"`
|
||||
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_"`
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
@ -17,7 +16,6 @@ import (
|
|||
"github.com/charmbracelet/gum/internal/exit"
|
||||
"github.com/charmbracelet/gum/internal/files"
|
||||
"github.com/charmbracelet/gum/internal/stdin"
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Run provides a shell script interface for filtering through options, powered
|
||||
|
@ -28,6 +26,7 @@ func (o Options) Run() error {
|
|||
|
||||
i.Prompt = o.Prompt
|
||||
i.PromptStyle = o.PromptStyle.ToLipgloss()
|
||||
i.PlaceholderStyle = o.PlaceholderStyle.ToLipgloss()
|
||||
i.Placeholder = o.Placeholder
|
||||
i.Width = o.Width
|
||||
|
||||
|
@ -45,6 +44,11 @@ func (o Options) Run() error {
|
|||
return errors.New("no options provided, see `gum filter --help`")
|
||||
}
|
||||
|
||||
if o.SelectIfOne && len(o.Options) == 1 {
|
||||
fmt.Println(o.Options[0])
|
||||
return nil
|
||||
}
|
||||
|
||||
options := []tea.ProgramOption{tea.WithOutput(os.Stderr)}
|
||||
if o.Height == 0 {
|
||||
options = append(options, tea.WithAltScreen())
|
||||
|
@ -132,9 +136,3 @@ func (o Options) checkSelected(m model, isTTY bool) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BeforeReset hook. Used to unclutter style flags.
|
||||
func (o Options) BeforeReset(ctx *kong.Context) error {
|
||||
style.HideFlags(ctx)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ type Options struct {
|
|||
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"`
|
||||
Strict bool `help:"Only returns if anything matched. Otherwise return Filter" negatable:"true" default:"true" group:"Selection"`
|
||||
SelectedPrefix string `help:"Character to indicate selected items (hidden if limit is 1)" default:" ◉ " env:"GUM_FILTER_SELECTED_PREFIX"`
|
||||
SelectedPrefixStyle style.Styles `embed:"" prefix:"selected-indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_SELECTED_PREFIX_"`
|
||||
|
@ -27,6 +28,7 @@ type Options struct {
|
|||
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:"20" env:"GUM_FILTER_WIDTH"`
|
||||
Height int `help:"Input height" default:"0" env:"GUM_FILTER_HEIGHT"`
|
||||
Value string `help:"Initial filter value" default:"" env:"GUM_FILTER_VALUE"`
|
||||
|
|
|
@ -54,7 +54,7 @@ func markdown(input string, theme string) (string, error) {
|
|||
}
|
||||
|
||||
func template(input string) (string, error) {
|
||||
f := termenv.TemplateFuncs(termenv.EnvColorProfile())
|
||||
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)
|
||||
|
|
47
go.mod
47
go.mod
|
@ -1,47 +1,50 @@
|
|||
module github.com/charmbracelet/gum
|
||||
|
||||
go 1.19
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/alecthomas/kong v0.8.1
|
||||
github.com/alecthomas/kong v0.9.0
|
||||
github.com/alecthomas/mango-kong v0.1.0
|
||||
github.com/charmbracelet/bubbles v0.16.1
|
||||
github.com/charmbracelet/bubbletea v0.24.2
|
||||
github.com/charmbracelet/glamour v0.6.1-0.20230531150759-6d5b52861a9d
|
||||
github.com/charmbracelet/lipgloss v0.9.1
|
||||
github.com/charmbracelet/log v0.3.0
|
||||
github.com/charmbracelet/bubbles v0.18.0
|
||||
github.com/charmbracelet/bubbletea v0.26.1
|
||||
github.com/charmbracelet/glamour v0.7.0
|
||||
github.com/charmbracelet/huh v0.3.1-0.20240328185852-590ecabc34b9
|
||||
github.com/charmbracelet/lipgloss v0.10.0
|
||||
github.com/charmbracelet/log v0.4.0
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/muesli/reflow v0.3.0
|
||||
github.com/muesli/roff v0.1.0
|
||||
github.com/muesli/termenv v0.15.2
|
||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f
|
||||
github.com/sahilm/fuzzy v0.1.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma/v2 v2.7.0 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.13.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/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||
github.com/dlclark/regexp2 v1.8.1 // indirect
|
||||
github.com/catppuccin/go v0.2.0 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240328150354-ab9afc214dfd // 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.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.23 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 // 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/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/yuin/goldmark v1.5.4 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/term v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/yuin/goldmark v1.7.0 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.2 // indirect
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
|
||||
golang.org/x/net v0.22.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
golang.org/x/term v0.19.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
)
|
||||
|
|
112
go.sum
112
go.sum
|
@ -1,40 +1,55 @@
|
|||
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
|
||||
github.com/alecthomas/chroma/v2 v2.7.0 h1:hm1rY6c/Ob4eGclpQ7X/A3yhqBOZNUTk9q+yhyLIViI=
|
||||
github.com/alecthomas/chroma/v2 v2.7.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw=
|
||||
github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY=
|
||||
github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
|
||||
github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
|
||||
github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI=
|
||||
github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk=
|
||||
github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA=
|
||||
github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os=
|
||||
github.com/alecthomas/mango-kong v0.1.0 h1:iFVfP1k1K4qpml3JUQmD5I8MCQYfIvsD9mRdrw7jJC4=
|
||||
github.com/alecthomas/mango-kong v0.1.0/go.mod h1:t+TYVdsONUolf/BwVcm+15eqcdAj15h4Qe9MMFAwwT4=
|
||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/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/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY=
|
||||
github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc=
|
||||
github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY=
|
||||
github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg=
|
||||
github.com/charmbracelet/glamour v0.6.1-0.20230531150759-6d5b52861a9d h1:S4Ejl/M2VrryIgDrDbiuvkwMUDa67/t/H3Wz3i2/vUw=
|
||||
github.com/charmbracelet/glamour v0.6.1-0.20230531150759-6d5b52861a9d/go.mod h1:swCB3CXFsh22H1ESDYdY1tirLiNqCziulDyJ1B6Nt7Q=
|
||||
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
|
||||
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
|
||||
github.com/charmbracelet/log v0.3.0 h1:u5aB2KJDgNZo4WOfOC8C+KvGIkJ2rCFNlPWDu6xhnqI=
|
||||
github.com/charmbracelet/log v0.3.0/go.mod h1:OR4E1hutLsax3ZKpXbgUqPtTjQfrh1pG3zwHGWuuq8g=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
|
||||
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
|
||||
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
|
||||
github.com/charmbracelet/bubbletea v0.26.1 h1:xujcQeF73rh4jwu3+zhfQsvV18x+7zIjlw7/CYbzGJ0=
|
||||
github.com/charmbracelet/bubbletea v0.26.1/go.mod h1:FzKr7sKoO8iFVcdIBM9J0sJOcQv5nDQaYwsee3kpbgo=
|
||||
github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng=
|
||||
github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps=
|
||||
github.com/charmbracelet/huh v0.3.1-0.20240328185852-590ecabc34b9 h1:Izr4MC+shs9PpR4MWz/OFA4+ywbKutvPv0eSHJwfn60=
|
||||
github.com/charmbracelet/huh v0.3.1-0.20240328185852-590ecabc34b9/go.mod h1:x0rYoA1kpsaefXhRJZuxLM+qP4CYyEFE67T3ZGl7zPU=
|
||||
github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
|
||||
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
|
||||
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
|
||||
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240328150354-ab9afc214dfd h1:yTFoT3v/wDWzeoRXt9mIKlslAKfVNr0XdVCOVwRK8ck=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240328150354-ab9afc214dfd/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||
github.com/charmbracelet/x/exp/term v0.0.0-20240321133156-7faadd06c281 h1:ZYwrF0GAd859tU6oF63T2pIkZVQ4z9BosDVD7jYu93A=
|
||||
github.com/charmbracelet/x/exp/term v0.0.0-20240321133156-7faadd06c281/go.mod h1:madZtB2OVDOG+ZnLruGITVZceYy047W+BLQ1MNQzbWg=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
|
||||
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
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.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
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.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
|
@ -45,8 +60,8 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
|
|||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY=
|
||||
github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
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=
|
||||
|
@ -62,30 +77,33 @@ github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1n
|
|||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/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.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
|
||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
|
||||
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
|
||||
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
|
||||
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
|
||||
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
|
||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
@ -4,65 +4,59 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"github.com/charmbracelet/gum/cursor"
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
"github.com/charmbracelet/gum/internal/stdin"
|
||||
"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 {
|
||||
i := textinput.New()
|
||||
var value string
|
||||
if o.Value != "" {
|
||||
i.SetValue(o.Value)
|
||||
value = o.Value
|
||||
} else if in, _ := stdin.Read(); in != "" {
|
||||
i.SetValue(in)
|
||||
value = in
|
||||
}
|
||||
|
||||
i.Focus()
|
||||
i.Prompt = o.Prompt
|
||||
i.Placeholder = o.Placeholder
|
||||
i.Width = o.Width
|
||||
i.PromptStyle = o.PromptStyle.ToLipgloss()
|
||||
i.Cursor.Style = o.CursorStyle.ToLipgloss()
|
||||
i.Cursor.SetMode(cursor.Modes[o.CursorMode])
|
||||
i.CharLimit = o.CharLimit
|
||||
theme := huh.ThemeCharm()
|
||||
theme.Focused.Base = lipgloss.NewStyle()
|
||||
// theme.Focused.TextInput.Cursor = o.CursorStyle.ToLipgloss()
|
||||
theme.Focused.TextInput.Placeholder = o.PlaceholderStyle.ToLipgloss()
|
||||
theme.Focused.TextInput.Prompt = o.PromptStyle.ToLipgloss()
|
||||
theme.Focused.Title = o.HeaderStyle.ToLipgloss()
|
||||
|
||||
var echoMode huh.EchoMode
|
||||
|
||||
if o.Password {
|
||||
i.EchoMode = textinput.EchoPassword
|
||||
i.EchoCharacter = '•'
|
||||
echoMode = huh.EchoModePassword
|
||||
} else {
|
||||
echoMode = huh.EchoModeNormal
|
||||
}
|
||||
|
||||
p := tea.NewProgram(model{
|
||||
textinput: i,
|
||||
aborted: false,
|
||||
header: o.Header,
|
||||
headerStyle: o.HeaderStyle.ToLipgloss(),
|
||||
timeout: o.Timeout,
|
||||
hasTimeout: o.Timeout > 0,
|
||||
autoWidth: o.Width < 1,
|
||||
}, tea.WithOutput(os.Stderr))
|
||||
tm, err := p.Run()
|
||||
err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Prompt(o.Prompt).
|
||||
Placeholder(o.Placeholder).
|
||||
CharLimit(o.CharLimit).
|
||||
EchoMode(echoMode).
|
||||
Title(o.Header).
|
||||
Value(&value),
|
||||
),
|
||||
).
|
||||
WithShowHelp(false).
|
||||
WithWidth(o.Width).
|
||||
WithTheme(theme).
|
||||
WithProgramOptions(tea.WithOutput(os.Stderr)).
|
||||
Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run input: %w", err)
|
||||
}
|
||||
m := tm.(model)
|
||||
|
||||
if m.aborted {
|
||||
return exit.ErrAborted
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(m.textinput.Value())
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeReset hook. Used to unclutter style flags.
|
||||
func (o Options) BeforeReset(ctx *kong.Context) error {
|
||||
style.HideFlags(ctx)
|
||||
fmt.Println(value)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
// 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 (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type model struct {
|
||||
autoWidth bool
|
||||
header string
|
||||
headerStyle lipgloss.Style
|
||||
textinput textinput.Model
|
||||
quitting bool
|
||||
aborted bool
|
||||
timeout time.Duration
|
||||
hasTimeout bool
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
textinput.Blink,
|
||||
timeout.Init(m.timeout, nil),
|
||||
)
|
||||
}
|
||||
func (m model) View() string {
|
||||
if m.quitting {
|
||||
return ""
|
||||
}
|
||||
if m.header != "" {
|
||||
header := m.headerStyle.Render(m.header)
|
||||
return lipgloss.JoinVertical(lipgloss.Left, header, m.textinput.View())
|
||||
}
|
||||
|
||||
return m.textinput.View()
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case timeout.TickTimeoutMsg:
|
||||
if msg.TimeoutValue <= 0 {
|
||||
m.quitting = true
|
||||
m.aborted = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
m.timeout = msg.TimeoutValue
|
||||
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
|
||||
case tea.WindowSizeMsg:
|
||||
if m.autoWidth {
|
||||
m.textinput.Width = msg.Width - lipgloss.Width(m.textinput.Prompt) - 1
|
||||
}
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "esc":
|
||||
m.quitting = true
|
||||
m.aborted = true
|
||||
return m, tea.Quit
|
||||
case "enter":
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.textinput, cmd = m.textinput.Update(msg)
|
||||
return m, cmd
|
||||
}
|
|
@ -8,16 +8,17 @@ import (
|
|||
|
||||
// Options are the customization options for the input.
|
||||
type Options struct {
|
||||
Placeholder string `help:"Placeholder value" default:"Type something..." env:"GUM_INPUT_PLACEHOLDER"`
|
||||
Prompt string `help:"Prompt to display" default:"> " env:"GUM_INPUT_PROMPT"`
|
||||
PromptStyle style.Styles `embed:"" prefix:"prompt." envprefix:"GUM_INPUT_PROMPT_"`
|
||||
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_INPUT_CURSOR_"`
|
||||
CursorMode string `prefix:"cursor." name:"mode" help:"Cursor mode" default:"blink" enum:"blink,hide,static" env:"GUM_INPUT_CURSOR_MODE"`
|
||||
Value string `help:"Initial value (can also be passed via stdin)" default:""`
|
||||
CharLimit int `help:"Maximum value length (0 for no limit)" default:"400"`
|
||||
Width int `help:"Input width (0 for terminal width)" default:"40" env:"GUM_INPUT_WIDTH"`
|
||||
Password bool `help:"Mask input characters" default:"false"`
|
||||
Header string `help:"Header value" default:"" env:"GUM_INPUT_HEADER"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_INPUT_HEADER_"`
|
||||
Timeout time.Duration `help:"Timeout until input aborts" default:"0" env:"GUM_INPUT_TIMEOUT"`
|
||||
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:"40" env:"GUM_INPUT_WIDTH"`
|
||||
Password bool `help:"Mask input characters" default:"false"`
|
||||
Header string `help:"Header value" default:"" env:"GUM_INPUT_HEADER"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_INPUT_HEADER_"`
|
||||
Timeout time.Duration `help:"Timeout until input aborts" default:"0" env:"GUM_INPUT_TIMEOUT"`
|
||||
}
|
||||
|
|
|
@ -46,9 +46,9 @@ func (o Options) Run() error {
|
|||
"stampmilli": time.StampMilli,
|
||||
"stampmicro": time.StampMicro,
|
||||
"stampnano": time.StampNano,
|
||||
"datetime": "2006-01-02 15:04:05",
|
||||
"dateonly": "2006-01-02",
|
||||
"timeonly": "15:04:05",
|
||||
"datetime": time.DateTime,
|
||||
"dateonly": time.DateOnly,
|
||||
"timeonly": time.TimeOnly,
|
||||
}
|
||||
|
||||
tf, ok := timeFormats[strings.ToLower(o.Time)]
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
|
@ -25,9 +24,3 @@ type Options struct {
|
|||
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_"`
|
||||
}
|
||||
|
||||
// BeforeReset hook. Used to unclutter style flags.
|
||||
func (o Options) BeforeReset(ctx *kong.Context) error {
|
||||
style.HideFlags(ctx)
|
||||
return nil
|
||||
}
|
||||
|
|
7
main.go
7
main.go
|
@ -28,7 +28,7 @@ var (
|
|||
var bubbleGumPink = lipgloss.NewStyle().Foreground(lipgloss.Color("212"))
|
||||
|
||||
func main() {
|
||||
lipgloss.SetColorProfile(termenv.NewOutput(os.Stderr).EnvColorProfile())
|
||||
lipgloss.SetColorProfile(termenv.NewOutput(os.Stderr).Profile)
|
||||
|
||||
if Version == "" {
|
||||
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" {
|
||||
|
@ -48,8 +48,9 @@ func main() {
|
|||
kong.Description(fmt.Sprintf("A tool for %s shell scripts.", bubbleGumPink.Render("glamorous"))),
|
||||
kong.UsageOnError(),
|
||||
kong.ConfigureHelp(kong.HelpOptions{
|
||||
Compact: true,
|
||||
Summary: false,
|
||||
Compact: true,
|
||||
Summary: false,
|
||||
NoExpandSubcommands: true,
|
||||
}),
|
||||
kong.Vars{
|
||||
"version": version,
|
||||
|
|
|
@ -4,11 +4,9 @@ import (
|
|||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/gum/internal/stdin"
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Run provides a shell script interface for the viewport bubble.
|
||||
|
@ -50,9 +48,3 @@ func (o Options) Run() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeReset hook. Used to unclutter style flags.
|
||||
func (o Options) BeforeReset(ctx *kong.Context) error {
|
||||
style.HideFlags(ctx)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Package pager provides a pager (similar to less) for the terminal.
|
||||
//
|
||||
// $ cat file.txt | gum page
|
||||
// $ cat file.txt | gum pager
|
||||
package pager
|
||||
|
||||
import (
|
||||
|
@ -120,9 +120,9 @@ func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) {
|
|||
}
|
||||
} else {
|
||||
switch key.String() {
|
||||
case "g":
|
||||
case "g", "home":
|
||||
m.viewport.GotoTop()
|
||||
case "G":
|
||||
case "G", "end":
|
||||
m.viewport.GotoBottom()
|
||||
case "/":
|
||||
m.search.Begin()
|
||||
|
|
|
@ -4,13 +4,11 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/mattn/go-isatty"
|
||||
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Run provides a shell script interface for the spinner bubble.
|
||||
|
@ -26,6 +24,8 @@ func (o Options) Run() error {
|
|||
title: o.TitleStyle.ToLipgloss().Render(o.Title),
|
||||
command: o.Command,
|
||||
align: o.Align,
|
||||
showOutput: o.ShowOutput && isTTY,
|
||||
showError: o.ShowError,
|
||||
timeout: o.Timeout,
|
||||
hasTimeout: o.Timeout > 0,
|
||||
}
|
||||
|
@ -41,25 +41,28 @@ func (o Options) Run() error {
|
|||
return exit.ErrAborted
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to access stdout: %w", err)
|
||||
}
|
||||
|
||||
if o.ShowOutput {
|
||||
if isTTY {
|
||||
_, err := os.Stdout.WriteString(m.stdout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to stdout: %w", err)
|
||||
// If the command succeeds, and we are printing output and we are in a TTY then push the STDOUT we got to the actual
|
||||
// STDOUT for piping or other things.
|
||||
if m.status == 0 {
|
||||
if o.ShowOutput {
|
||||
// BubbleTea writes the View() to stderr.
|
||||
// If the program is being piped then put the accumulated output in stdout.
|
||||
if !isTTY {
|
||||
_, err := os.Stdout.WriteString(m.stdout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to stdout: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if o.ShowError {
|
||||
// Otherwise if we are showing errors and the command did not exit with a 0 status code then push all of the command
|
||||
// output to the terminal. This way failed commands can be debugged.
|
||||
_, err := os.Stdout.WriteString(m.output)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to stdout: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(m.status)
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeReset hook. Used to unclutter style flags.
|
||||
func (o Options) BeforeReset(ctx *kong.Context) error {
|
||||
style.HideFlags(ctx)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ type Options struct {
|
|||
Command []string `arg:"" help:"Command to run"`
|
||||
|
||||
ShowOutput bool `help:"Show or pipe output of command during execution" default:"false" env:"GUM_SPIN_SHOW_OUTPUT"`
|
||||
ShowError bool `help:"Show output of command only if the command fails" default:"false" env:"GUM_SPIN_SHOW_ERROR"`
|
||||
Spinner string `help:"Spinner type" short:"s" type:"spinner" enum:"line,dot,minidot,jump,pulse,points,globe,moon,monkey,meter,hamburger" default:"dot" env:"GUM_SPIN_SPINNER"`
|
||||
SpinnerStyle style.Styles `embed:"" prefix:"spinner." set:"defaultForeground=212" envprefix:"GUM_SPIN_SPINNER_"`
|
||||
Title string `help:"Text to display to user while spinning" default:"Loading..." env:"GUM_SPIN_TITLE"`
|
||||
|
|
33
spin/spin.go
33
spin/spin.go
|
@ -15,12 +15,15 @@
|
|||
package spin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
"github.com/mattn/go-isatty"
|
||||
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
@ -31,19 +34,26 @@ type model struct {
|
|||
title string
|
||||
align string
|
||||
command []string
|
||||
quitting bool
|
||||
aborted bool
|
||||
status int
|
||||
stdout string
|
||||
stderr string
|
||||
output string
|
||||
showOutput bool
|
||||
showError bool
|
||||
timeout time.Duration
|
||||
hasTimeout bool
|
||||
}
|
||||
|
||||
var bothbuf strings.Builder
|
||||
var outbuf strings.Builder
|
||||
var errbuf strings.Builder
|
||||
|
||||
type finishCommandMsg struct {
|
||||
stdout string
|
||||
stderr string
|
||||
output string
|
||||
status int
|
||||
}
|
||||
|
||||
|
@ -55,8 +65,15 @@ func commandStart(command []string) tea.Cmd {
|
|||
}
|
||||
cmd := exec.Command(command[0], args...) //nolint:gosec
|
||||
|
||||
cmd.Stdout = &outbuf
|
||||
cmd.Stderr = &errbuf
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
stdout := io.MultiWriter(&bothbuf, &errbuf)
|
||||
stderr := io.MultiWriter(&bothbuf, &outbuf)
|
||||
|
||||
cmd.Stdout = stdout
|
||||
cmd.Stderr = stderr
|
||||
} else {
|
||||
cmd.Stdout = os.Stdout
|
||||
}
|
||||
|
||||
_ = cmd.Run()
|
||||
|
||||
|
@ -68,6 +85,8 @@ func commandStart(command []string) tea.Cmd {
|
|||
|
||||
return finishCommandMsg{
|
||||
stdout: outbuf.String(),
|
||||
stderr: errbuf.String(),
|
||||
output: bothbuf.String(),
|
||||
status: status,
|
||||
}
|
||||
}
|
||||
|
@ -81,6 +100,10 @@ func (m model) Init() tea.Cmd {
|
|||
)
|
||||
}
|
||||
func (m model) View() string {
|
||||
if m.quitting && m.showOutput {
|
||||
return strings.TrimPrefix(errbuf.String()+"\n"+outbuf.String(), "\n")
|
||||
}
|
||||
|
||||
var str string
|
||||
if m.hasTimeout {
|
||||
str = timeout.Str(m.timeout)
|
||||
|
@ -102,6 +125,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
switch msg := msg.(type) {
|
||||
case timeout.TickTimeoutMsg:
|
||||
if msg.TimeoutValue <= 0 {
|
||||
// grab current output before closing for piped instances
|
||||
m.stdout = outbuf.String()
|
||||
|
||||
m.status = exit.StatusAborted
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
@ -109,7 +135,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
|
||||
case finishCommandMsg:
|
||||
m.stdout = msg.stdout
|
||||
m.stderr = msg.stderr
|
||||
m.output = msg.output
|
||||
m.status = msg.status
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/charmbracelet/gum/internal/stdin"
|
||||
)
|
||||
|
||||
|
@ -30,19 +29,3 @@ func (o Options) Run() error {
|
|||
fmt.Println(o.Style.ToLipgloss().Render(text))
|
||||
return nil
|
||||
}
|
||||
|
||||
// HideFlags hides the flags from the usage output. This is used in conjunction
|
||||
// with BeforeReset hook.
|
||||
func HideFlags(ctx *kong.Context) {
|
||||
n := ctx.Selected()
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
for _, f := range n.Flags {
|
||||
if g := f.Group; g != nil && g.Key == groupName {
|
||||
if !strings.HasSuffix(f.Name, ".foreground") {
|
||||
f.Hidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,3 +26,24 @@ func (s Styles) ToLipgloss() lipgloss.Style {
|
|||
Strikethrough(s.Strikethrough).
|
||||
Underline(s.Underline)
|
||||
}
|
||||
|
||||
// ToLipgloss takes a Styles flag set and returns the corresponding
|
||||
// lipgloss.Style.
|
||||
func (s StylesNotHidden) ToLipgloss() lipgloss.Style {
|
||||
return lipgloss.NewStyle().
|
||||
Background(lipgloss.Color(s.Background)).
|
||||
Foreground(lipgloss.Color(s.Foreground)).
|
||||
BorderBackground(lipgloss.Color(s.BorderBackground)).
|
||||
BorderForeground(lipgloss.Color(s.BorderForeground)).
|
||||
Align(decode.Align[s.Align]).
|
||||
Border(Border[s.Border]).
|
||||
Height(s.Height).
|
||||
Width(s.Width).
|
||||
Margin(parseMargin(s.Margin)).
|
||||
Padding(parsePadding(s.Padding)).
|
||||
Bold(s.Bold).
|
||||
Faint(s.Faint).
|
||||
Italic(s.Italic).
|
||||
Strikethrough(s.Strikethrough).
|
||||
Underline(s.Underline)
|
||||
}
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
package style
|
||||
|
||||
const (
|
||||
groupName = "Style Flags"
|
||||
)
|
||||
|
||||
// Options is the customization options for the style command.
|
||||
type Options struct {
|
||||
Text []string `arg:"" optional:"" help:"Text to which to apply the style"`
|
||||
Style Styles `embed:""`
|
||||
Text []string `arg:"" optional:"" help:"Text to which to apply the style"`
|
||||
Style StylesNotHidden `embed:""`
|
||||
}
|
||||
|
||||
// Styles is a flag set of possible styles.
|
||||
|
@ -18,8 +14,38 @@ type Options struct {
|
|||
// components, through embedding and prefixing.
|
||||
type Styles struct {
|
||||
// Colors
|
||||
Background string `help:"Background Color" default:"${defaultBackground}" group:"Style Flags" env:"BACKGROUND"`
|
||||
Foreground string `help:"Foreground Color" default:"${defaultForeground}" group:"Style Flags" env:"FOREGROUND"`
|
||||
Background string `help:"Background Color" default:"${defaultBackground}" group:"Style Flags" env:"BACKGROUND" hidden:"true"`
|
||||
|
||||
// Border
|
||||
Border string `help:"Border Style" enum:"none,hidden,normal,rounded,thick,double" default:"${defaultBorder}" group:"Style Flags" env:"BORDER" hidden:"true"`
|
||||
BorderBackground string `help:"Border Background Color" group:"Style Flags" default:"${defaultBorderBackground}" env:"BORDER_BACKGROUND" hidden:"true"`
|
||||
BorderForeground string `help:"Border Foreground Color" group:"Style Flags" default:"${defaultBorderForeground}" env:"BORDER_FOREGROUND" hidden:"true"`
|
||||
|
||||
// Layout
|
||||
Align string `help:"Text Alignment" enum:"left,center,right,bottom,middle,top" default:"${defaultAlign}" group:"Style Flags" env:"ALIGN" hidden:"true"`
|
||||
Height int `help:"Text height" default:"${defaultHeight}" group:"Style Flags" env:"HEIGHT" hidden:"true"`
|
||||
Width int `help:"Text width" default:"${defaultWidth}" group:"Style Flags" env:"WIDTH" hidden:"true"`
|
||||
Margin string `help:"Text margin" default:"${defaultMargin}" group:"Style Flags" env:"MARGIN" hidden:"true"`
|
||||
Padding string `help:"Text padding" default:"${defaultPadding}" group:"Style Flags" env:"PADDING" hidden:"true"`
|
||||
|
||||
// Format
|
||||
Bold bool `help:"Bold text" default:"${defaultBold}" group:"Style Flags" env:"BOLD" hidden:"true"`
|
||||
Faint bool `help:"Faint text" default:"${defaultFaint}" group:"Style Flags" env:"FAINT" hidden:"true"`
|
||||
Italic bool `help:"Italicize text" default:"${defaultItalic}" group:"Style Flags" env:"ITALIC" hidden:"true"`
|
||||
Strikethrough bool `help:"Strikethrough text" default:"${defaultStrikethrough}" group:"Style Flags" env:"STRIKETHROUGH" hidden:"true"`
|
||||
Underline bool `help:"Underline text" default:"${defaultUnderline}" group:"Style Flags" env:"UNDERLINE" hidden:"true"`
|
||||
}
|
||||
|
||||
// StylesNotHidden allows the style struct to display full help when not-embedded.
|
||||
//
|
||||
// NB: We must duplicate this struct to ensure that `gum style` does not hide
|
||||
// flags when an error pops up. Ideally, we can dynamically hide or show flags
|
||||
// based on the command run: https://github.com/alecthomas/kong/issues/316
|
||||
type StylesNotHidden struct {
|
||||
// Colors
|
||||
Foreground string `help:"Foreground Color" default:"${defaultForeground}" group:"Style Flags" env:"FOREGROUND"`
|
||||
Background string `help:"Background Color" default:"${defaultBackground}" group:"Style Flags" env:"BACKGROUND"`
|
||||
|
||||
// Border
|
||||
Border string `help:"Border Style" enum:"none,hidden,normal,rounded,thick,double" default:"${defaultBorder}" group:"Style Flags" env:"BORDER"`
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
@ -57,7 +56,7 @@ func (o Options) Run() error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("invalid data provided")
|
||||
}
|
||||
var columns = make([]table.Column, 0, len(columnNames))
|
||||
columns := make([]table.Column, 0, len(columnNames))
|
||||
|
||||
for i, title := range columnNames {
|
||||
width := lipgloss.Width(title)
|
||||
|
@ -78,7 +77,7 @@ func (o Options) Run() error {
|
|||
Selected: o.SelectedStyle.ToLipgloss(),
|
||||
}
|
||||
|
||||
var rows = make([]table.Row, 0, len(data))
|
||||
rows := make([]table.Row, 0, len(data))
|
||||
for _, row := range data {
|
||||
if len(row) > len(columns) {
|
||||
return fmt.Errorf("invalid number of columns")
|
||||
|
@ -92,7 +91,7 @@ func (o Options) Run() error {
|
|||
Rows(data...).
|
||||
BorderStyle(o.BorderStyle.ToLipgloss()).
|
||||
Border(style.Border[o.Border]).
|
||||
StyleFunc(func(row, col int) lipgloss.Style {
|
||||
StyleFunc(func(row, _ int) lipgloss.Style {
|
||||
if row == 0 {
|
||||
return styles.Header
|
||||
}
|
||||
|
@ -112,7 +111,6 @@ func (o Options) Run() error {
|
|||
)
|
||||
|
||||
tm, err := tea.NewProgram(model{table: table}, tea.WithOutput(os.Stderr)).Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start tea program: %w", err)
|
||||
}
|
||||
|
@ -131,9 +129,3 @@ func (o Options) Run() error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeReset hook. Used to unclutter style flags.
|
||||
func (o Options) BeforeReset(ctx *kong.Context) error {
|
||||
style.HideFlags(ctx)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,17 +2,10 @@ package write
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/charmbracelet/bubbles/textarea"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"github.com/charmbracelet/gum/cursor"
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
"github.com/charmbracelet/gum/internal/stdin"
|
||||
"github.com/charmbracelet/gum/style"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
// Run provides a shell script interface for the text area bubble.
|
||||
|
@ -23,54 +16,33 @@ func (o Options) Run() error {
|
|||
o.Value = strings.ReplaceAll(in, "\r", "")
|
||||
}
|
||||
|
||||
a := textarea.New()
|
||||
a.Focus()
|
||||
var value = o.Value
|
||||
|
||||
a.Prompt = o.Prompt
|
||||
a.Placeholder = o.Placeholder
|
||||
a.ShowLineNumbers = o.ShowLineNumbers
|
||||
a.CharLimit = o.CharLimit
|
||||
theme := huh.ThemeCharm()
|
||||
theme.Focused.Base = o.BaseStyle.ToLipgloss()
|
||||
theme.Focused.TextInput.Cursor = o.CursorStyle.ToLipgloss()
|
||||
theme.Focused.Title = o.HeaderStyle.ToLipgloss()
|
||||
theme.Focused.TextInput.Placeholder = o.PlaceholderStyle.ToLipgloss()
|
||||
theme.Focused.TextInput.Prompt = o.PromptStyle.ToLipgloss()
|
||||
|
||||
style := textarea.Style{
|
||||
Base: o.BaseStyle.ToLipgloss(),
|
||||
Placeholder: o.PlaceholderStyle.ToLipgloss(),
|
||||
CursorLine: o.CursorLineStyle.ToLipgloss(),
|
||||
CursorLineNumber: o.CursorLineNumberStyle.ToLipgloss(),
|
||||
EndOfBuffer: o.EndOfBufferStyle.ToLipgloss(),
|
||||
LineNumber: o.LineNumberStyle.ToLipgloss(),
|
||||
Prompt: o.PromptStyle.ToLipgloss(),
|
||||
}
|
||||
err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewText().
|
||||
Title(o.Header).
|
||||
Placeholder(o.Placeholder).
|
||||
CharLimit(o.CharLimit).
|
||||
ShowLineNumbers(o.ShowLineNumbers).
|
||||
Value(&value),
|
||||
),
|
||||
).
|
||||
WithWidth(o.Width).
|
||||
WithHeight(o.Height).
|
||||
WithShowHelp(false).Run()
|
||||
|
||||
a.BlurredStyle = style
|
||||
a.FocusedStyle = style
|
||||
a.Cursor.Style = o.CursorStyle.ToLipgloss()
|
||||
a.Cursor.SetMode(cursor.Modes[o.CursorMode])
|
||||
|
||||
a.SetWidth(o.Width)
|
||||
a.SetHeight(o.Height)
|
||||
a.SetValue(o.Value)
|
||||
|
||||
p := tea.NewProgram(model{
|
||||
textarea: a,
|
||||
header: o.Header,
|
||||
headerStyle: o.HeaderStyle.ToLipgloss(),
|
||||
autoWidth: o.Width < 1,
|
||||
}, tea.WithOutput(os.Stderr))
|
||||
tm, err := p.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run write: %w", err)
|
||||
}
|
||||
m := tm.(model)
|
||||
if m.aborted {
|
||||
return exit.ErrAborted
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(m.textarea.Value())
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeReset hook. Used to unclutter style flags.
|
||||
func (o Options) BeforeReset(ctx *kong.Context) error {
|
||||
style.HideFlags(ctx)
|
||||
fmt.Println(value)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -15,13 +15,14 @@ type Options struct {
|
|||
CharLimit int `help:"Maximum value length (0 for no limit)" default:"400"`
|
||||
CursorMode string `prefix:"cursor." name:"mode" help:"Cursor mode" default:"blink" enum:"blink,hide,static" env:"GUM_WRITE_CURSOR_MODE"`
|
||||
|
||||
BaseStyle style.Styles `embed:"" prefix:"base." envprefix:"GUM_WRITE_BASE_"`
|
||||
CursorLineNumberStyle style.Styles `embed:"" prefix:"cursor-line-number." set:"defaultForeground=7" envprefix:"GUM_WRITE_CURSOR_LINE_NUMBER_"`
|
||||
CursorLineStyle style.Styles `embed:"" prefix:"cursor-line." envprefix:"GUM_WRITE_CURSOR_LINE_"`
|
||||
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_WRITE_CURSOR_"`
|
||||
BaseStyle style.Styles `embed:"" prefix:"base." envprefix:"GUM_WRITE_BASE_"`
|
||||
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_WRITE_CURSOR_"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_WRITE_HEADER_"`
|
||||
PlaceholderStyle style.Styles `embed:"" prefix:"placeholder." set:"defaultForeground=240" envprefix:"GUM_WRITE_PLACEHOLDER_"`
|
||||
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=7" envprefix:"GUM_WRITE_PROMPT_"`
|
||||
|
||||
EndOfBufferStyle style.Styles `embed:"" prefix:"end-of-buffer." set:"defaultForeground=0" envprefix:"GUM_WRITE_END_OF_BUFFER_"`
|
||||
LineNumberStyle style.Styles `embed:"" prefix:"line-number." set:"defaultForeground=7" envprefix:"GUM_WRITE_LINE_NUMBER_"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_WRITE_HEADER_"`
|
||||
PlaceholderStyle style.Styles `embed:"" prefix:"placeholder." set:"defaultForeground=240" envprefix:"GUM_WRITE_PLACEHOLDER_"`
|
||||
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=7" envprefix:"GUM_WRITE_PROMPT_"`
|
||||
CursorLineNumberStyle style.Styles `embed:"" prefix:"cursor-line-number." set:"defaultForeground=7" envprefix:"GUM_WRITE_CURSOR_LINE_NUMBER_"`
|
||||
CursorLineStyle style.Styles `embed:"" prefix:"cursor-line." envprefix:"GUM_WRITE_CURSOR_LINE_"`
|
||||
}
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
// Package 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.
|
||||
// Text entry is completed with CTRL+D and aborted with CTRL+C or Escape.
|
||||
//
|
||||
// $ gum write > output.text
|
||||
package write
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/textarea"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type model struct {
|
||||
autoWidth bool
|
||||
aborted bool
|
||||
header string
|
||||
headerStyle lipgloss.Style
|
||||
quitting bool
|
||||
textarea textarea.Model
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd { return textarea.Blink }
|
||||
func (m model) View() string {
|
||||
if m.quitting {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Display the header above the text area if it is not empty.
|
||||
if m.header != "" {
|
||||
header := m.headerStyle.Render(m.header)
|
||||
return lipgloss.JoinVertical(lipgloss.Left, header, m.textarea.View())
|
||||
}
|
||||
|
||||
return m.textarea.View()
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
if m.autoWidth {
|
||||
m.textarea.SetWidth(msg.Width)
|
||||
}
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
m.aborted = true
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
case "ctrl+d", "esc":
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.textarea, cmd = m.textarea.Update(msg)
|
||||
return m, cmd
|
||||
}
|
Loading…
Reference in a new issue