mirror of
https://github.com/charmbracelet/gum
synced 2024-06-16 12:35:06 +02:00
Compare commits
126 commits
Author | SHA1 | Date | |
---|---|---|---|
851a48aee2 | |||
b1b02e4ecb | |||
fa2f67d1d6 | |||
baf36ae662 | |||
657c76eb9b | |||
c977aaf4ae | |||
efb70a1302 | |||
a8712df2a7 | |||
00767d209a | |||
2fe0291e02 | |||
26178f8894 | |||
68400ae7b3 | |||
2b0a4c033e | |||
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 | |||
dd557baf6a | |||
eb0e8afeba | |||
504a2060b2 | |||
7bae4c8fcb | |||
bf3864e231 | |||
3839b8d6e1 | |||
055aa0d791 | |||
12ef4d3085 | |||
86dbd9c70f | |||
7d51fd8b73 | |||
46328de806 | |||
8e959e4cdc | |||
1cedd4f20b | |||
89e2a0fbd5 | |||
971b6cf16f | |||
77aa8640f2 | |||
a63ea30136 | |||
a61f3bdc3f | |||
6763de12e8 | |||
4b998515fd | |||
ed52291b33 | |||
f5b09a434a | |||
d1ad453ce6 | |||
f73341a56c | |||
eef6431d7c | |||
6bf79aa899 | |||
f8caeef195 | |||
0c1cc8e669 | |||
7e71c4d664 | |||
abae6fd80c | |||
b6f739d7d1 | |||
ae1da5d329 | |||
93ffc250e7 | |||
99f1348a45 | |||
6aac40560f | |||
f048bd8d87 | |||
f1b99f0aa4 | |||
fd11b787e0 | |||
0010018d61 | |||
8f17aa3f9a | |||
3609fe1da8 | |||
3f7db714ff | |||
5a4b12c8ca | |||
a892c39289 | |||
accce59ed1 | |||
25ff33e710 | |||
4c3cc1773f | |||
6e802805cf | |||
b23ebce896 | |||
c8710071ad | |||
23c56854d3 | |||
c668e153e6 | |||
92c890e717 | |||
ece25c7789 | |||
9d2741c5f9 | |||
7f54b3b289 | |||
11584b5982 | |||
fff07286b7 | |||
39346ed015 | |||
99a1fa9d8e | |||
8da9620bfd | |||
5887a10fa0 | |||
6dbadf30b4 | |||
11f23830c5 | |||
066c79fa14 |
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* @maaslalani
|
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
|
@ -12,12 +12,12 @@ jobs:
|
|||
GO111MODULE: "on"
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.18
|
||||
go-version: ~1.21
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Go modules
|
||||
run: go mod download
|
||||
|
@ -31,4 +31,4 @@ jobs:
|
|||
snapshot:
|
||||
uses: charmbracelet/meta/.github/workflows/snapshot.yml@main
|
||||
secrets:
|
||||
goreleaser_key: ${{ secrets.GORELEASER_KEY }}
|
||||
goreleaser_key: ${{ secrets.GORELEASER_KEY }}
|
||||
|
|
6
.github/workflows/lint-soft.yml
vendored
6
.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@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
# Optional: golangci-lint command line arguments.
|
||||
args: --config .golangci-soft.yml --issues-exit-code=0
|
||||
|
|
6
.github/workflows/lint.yml
vendored
6
.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@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
# Optional: golangci-lint command line arguments.
|
||||
#args:
|
||||
|
|
12
.github/workflows/soft-serve.yml
vendored
12
.github/workflows/soft-serve.yml
vendored
|
@ -1,12 +0,0 @@
|
|||
name: soft-serve
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
soft-serve:
|
||||
uses: charmbracelet/meta/.github/workflows/soft-serve.yml@main
|
||||
secrets:
|
||||
ssh-key: "${{ secrets.CHARM_SOFT_SERVE_KEY }}"
|
|
@ -23,7 +23,7 @@ linters:
|
|||
- gomnd
|
||||
- gomoddirectives
|
||||
- goprintffuncname
|
||||
- ifshort
|
||||
# - ifshort
|
||||
# - lll
|
||||
- misspell
|
||||
- nakedret
|
||||
|
@ -31,10 +31,10 @@ linters:
|
|||
- noctx
|
||||
- nolintlint
|
||||
- prealloc
|
||||
- wrapcheck
|
||||
|
||||
# disable default linters, they are already enabled in .golangci.yml
|
||||
disable:
|
||||
- wrapcheck
|
||||
- deadcode
|
||||
- errcheck
|
||||
- gosimple
|
||||
|
@ -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>"
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Charmbracelet, Inc
|
||||
Copyright (c) 2022-2024 Charmbracelet, Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
310
README.md
310
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,16 +66,27 @@ pacman -S gum
|
|||
|
||||
# Nix
|
||||
nix-env -iA nixpkgs.gum
|
||||
# Or, with flakes
|
||||
nix run "github:charmbracelet/gum" -- --help
|
||||
|
||||
# Debian/Ubuntu
|
||||
# Windows (via WinGet or Scoop)
|
||||
winget install charmbracelet.gum
|
||||
scoop install charm-gum
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Debian/Ubuntu</summary>
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg
|
||||
echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list
|
||||
sudo apt update && sudo apt install gum
|
||||
```
|
||||
</details>
|
||||
|
||||
# Fedora/RHEL
|
||||
<details>
|
||||
<summary>Fedora/RHEL</summary>
|
||||
|
||||
```bash
|
||||
echo '[charm]
|
||||
name=Charm
|
||||
baseurl=https://repo.charm.sh/yum/
|
||||
|
@ -117,16 +94,8 @@ 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 Scoop)
|
||||
scoop install charm-gum
|
||||
```
|
||||
</details>
|
||||
|
||||
Or download it:
|
||||
|
||||
|
@ -141,25 +110,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"
|
||||
|
@ -168,70 +152,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.
|
||||
|
||||
|
@ -241,24 +209,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.
|
||||
|
@ -267,9 +228,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.
|
||||
|
||||
|
@ -277,9 +238,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.
|
||||
|
||||
|
@ -287,22 +248,24 @@ 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.
|
||||
|
||||
To view or pipe the command's output, use the `--show-output` flag.
|
||||
|
||||
```bash
|
||||
gum spin --spinner dot --title "Buying Bubble Gum..." -- sleep 5
|
||||
```
|
||||
|
||||
<img src="https://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.
|
||||
|
||||
|
@ -310,11 +273,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.
|
||||
|
||||
|
@ -325,11 +286,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.
|
||||
|
@ -348,7 +307,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
|
||||
|
||||
|
@ -375,112 +334,95 @@ 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
|
||||
|
||||
`log` logs messages to the terminal at using different levels and styling using
|
||||
the [`charmbracelet/log`](https://github.com/charmbracelet/log) library.
|
||||
|
||||
```bash
|
||||
# Log some debug information.
|
||||
gum log --structured --level debug "Creating file..." name file.txt
|
||||
# DEBUG Unable to create file. name=temp.txt
|
||||
|
||||
# Log some error.
|
||||
gum log --structured --level error "Unable to create file." name file.txt
|
||||
# ERROR Unable to create file. name=temp.txt
|
||||
|
||||
# Include a timestamp.
|
||||
gum log --time rfc822 --level error "Unable to create file."
|
||||
```
|
||||
|
||||
See the Go [`time` package](https://pkg.go.dev/time#pkg-constants) for acceptable `--time` formats.
|
||||
|
||||
See [`charmbracelet/log`](https://github.com/charmbracelet/log) for more usage.
|
||||
|
||||
<img src="https://vhs.charm.sh/vhs-6jupuFM0s2fXiUrBE0I1vU.gif" width="600" alt="Running gum log with debug and error levels" />
|
||||
|
||||
## Examples
|
||||
|
||||
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"
|
||||
|
|
10
ansi/ansi.go
Normal file
10
ansi/ansi.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package ansi
|
||||
|
||||
import "regexp"
|
||||
|
||||
var ansiEscape = regexp.MustCompile(`\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])`)
|
||||
|
||||
// Strip strips a string of any of it's ansi sequences.
|
||||
func Strip(text string) string {
|
||||
return ansiEscape.ReplaceAllString(text, "")
|
||||
}
|
198
choose/choose.go
198
choose/choose.go
|
@ -1,198 +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"
|
||||
|
||||
"github.com/charmbracelet/bubbles/paginator"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type item struct {
|
||||
text string
|
||||
selected bool
|
||||
order int
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd { return nil }
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
return m, nil
|
||||
|
||||
case tea.KeyMsg:
|
||||
start, end := m.paginator.GetSliceBounds(len(m.items))
|
||||
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":
|
||||
m.index = len(m.items) - 1
|
||||
m.paginator.Page = m.paginator.TotalPages - 1
|
||||
case "g":
|
||||
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
|
||||
|
||||
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(" ", runewidth.StringWidth(m.cursor)))
|
||||
}
|
||||
|
||||
if item.selected {
|
||||
s.WriteString(m.selectedItemStyle.Render(m.selectedPrefix + item.text))
|
||||
} else if i == m.index%m.height {
|
||||
s.WriteString(m.cursorStyle.Render(m.cursorPrefix + item.text))
|
||||
} else {
|
||||
s.WriteString(m.itemStyle.Render(m.unselectedPrefix + item.text))
|
||||
}
|
||||
if i != m.height {
|
||||
s.WriteRune('\n')
|
||||
}
|
||||
}
|
||||
|
||||
if m.paginator.TotalPages > 1 {
|
||||
s.WriteString(strings.Repeat("\n", m.height-m.paginator.ItemsOnPage(len(m.items))+1))
|
||||
s.WriteString(" " + m.paginator.View())
|
||||
}
|
||||
|
||||
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,155 +4,134 @@ 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/internal/exit"
|
||||
"github.com/charmbracelet/gum/ansi"
|
||||
"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"})
|
||||
)
|
||||
const widthBuffer = 2
|
||||
|
||||
// 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`")
|
||||
}
|
||||
o.Options = strings.Split(strings.TrimSuffix(input, "\n"), "\n")
|
||||
o.Options = strings.Split(input, "\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
|
||||
}
|
||||
|
||||
theme := huh.ThemeCharm()
|
||||
options := huh.NewOptions(o.Options...)
|
||||
|
||||
theme.Focused.Base = lipgloss.NewStyle()
|
||||
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)
|
||||
|
||||
for _, s := range o.Selected {
|
||||
for i, opt := range options {
|
||||
if s == opt.Key || s == opt.Value {
|
||||
options[i] = opt.Selected(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
if len(o.Selected) > o.Limit {
|
||||
return errors.New("number of selected options cannot be greater than the limit")
|
||||
}
|
||||
width := max(widest(o.Options)+
|
||||
max(lipgloss.Width(o.SelectedPrefix)+lipgloss.Width(o.UnselectedPrefix))+
|
||||
lipgloss.Width(o.Cursor)+1, lipgloss.Width(o.Header)+widthBuffer)
|
||||
|
||||
// Keep track of the selected items.
|
||||
currentSelected := 0
|
||||
// Check if selected items should be used.
|
||||
hasSelectedItems := len(o.Selected) > 0
|
||||
if o.Limit > 1 {
|
||||
var choices []string
|
||||
|
||||
startingIndex := 0
|
||||
currentOrder := 0
|
||||
field := huh.NewMultiSelect[string]().
|
||||
Options(options...).
|
||||
Title(o.Header).
|
||||
Height(o.Height).
|
||||
Limit(o.Limit).
|
||||
Value(&choices)
|
||||
|
||||
items := make([]item, len(o.Options))
|
||||
form := huh.NewForm(huh.NewGroup(field))
|
||||
|
||||
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++
|
||||
}
|
||||
err := form.
|
||||
WithWidth(width).
|
||||
WithShowHelp(o.ShowHelp).
|
||||
WithTheme(theme).
|
||||
Run()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
items[i] = item{text: option, selected: isSelected, order: order}
|
||||
if len(choices) > 0 {
|
||||
s := strings.Join(choices, "\n")
|
||||
ansiprint(s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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{}
|
||||
var choice string
|
||||
|
||||
// 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,
|
||||
}, tea.WithOutput(os.Stderr)).Run()
|
||||
err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().
|
||||
Options(options...).
|
||||
Title(o.Header).
|
||||
Height(o.Height).
|
||||
Value(&choice),
|
||||
),
|
||||
).
|
||||
WithWidth(width).
|
||||
WithTheme(theme).
|
||||
WithShowHelp(o.ShowHelp).
|
||||
Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start tea program: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
m := tm.(model)
|
||||
if m.aborted {
|
||||
return exit.ErrAborted
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
fmt.Println(choice)
|
||||
} else {
|
||||
fmt.Print(ansi.Strip(choice))
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Print(s.String())
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func ansiprint(s string) {
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
fmt.Println(s)
|
||||
} else {
|
||||
fmt.Print(ansi.Strip(s))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,29 @@
|
|||
package choose
|
||||
|
||||
import "github.com/charmbracelet/gum/style"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Options is the customization options for the choose command.
|
||||
type Options struct {
|
||||
Options []string `arg:"" optional:"" help:"Options to choose from."`
|
||||
|
||||
Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"`
|
||||
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
|
||||
Ordered bool `help:"Maintain the order of the selected options" env:"GUM_CHOOSE_ORDERED"`
|
||||
Height int `help:"Height of the list" default:"10" env:"GUM_CHOOSE_HEIGHT"`
|
||||
Cursor string `help:"Prefix to show on item that corresponds to the cursor position" default:"> " env:"GUM_CHOOSE_CURSOR"`
|
||||
Header string `help:"Header value" default:"" env:"GUM_CHOOSE_HEADER"`
|
||||
CursorPrefix string `help:"Prefix to show on the cursor item (hidden if limit is 1)" default:"○ " env:"GUM_CHOOSE_CURSOR_PREFIX"`
|
||||
SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:"◉ " env:"GUM_CHOOSE_SELECTED_PREFIX"`
|
||||
UnselectedPrefix string `help:"Prefix to show on unselected items (hidden if limit is 1)" default:"○ " env:"GUM_CHOOSE_UNSELECTED_PREFIX"`
|
||||
Selected []string `help:"Options that should start as selected" default:"" env:"GUM_CHOOSE_SELECTED"`
|
||||
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_CURSOR_"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_CHOOSE_HEADER_"`
|
||||
ItemStyle style.Styles `embed:"" prefix:"item." hidden:"" envprefix:"GUM_CHOOSE_ITEM_"`
|
||||
SelectedItemStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_SELECTED_"`
|
||||
Options []string `arg:"" optional:"" help:"Options to choose from."`
|
||||
Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"`
|
||||
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
|
||||
Ordered bool `help:"Maintain the order of the selected options" env:"GUM_CHOOSE_ORDERED"`
|
||||
Height int `help:"Height of the list" default:"10" env:"GUM_CHOOSE_HEIGHT"`
|
||||
Cursor string `help:"Prefix to show on item that corresponds to the cursor position" default:"> " env:"GUM_CHOOSE_CURSOR"`
|
||||
ShowHelp bool `help:"Show help keybinds" default:"true" negatable:"true" env:"GUM_CHOOSE_SHOW_HELP"`
|
||||
Header string `help:"Header value" default:"Choose:" env:"GUM_CHOOSE_HEADER"`
|
||||
CursorPrefix string `help:"Prefix to show on the cursor item (hidden if limit is 1)" default:"• " env:"GUM_CHOOSE_CURSOR_PREFIX"`
|
||||
SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:"✓ " env:"GUM_CHOOSE_SELECTED_PREFIX"`
|
||||
UnselectedPrefix string `help:"Prefix to show on unselected items (hidden if limit is 1)" default:"• " env:"GUM_CHOOSE_UNSELECTED_PREFIX"`
|
||||
Selected []string `help:"Options that should start as selected" 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=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,45 +4,39 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"github.com/charmbracelet/gum/style"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
// 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.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(o.ShowHelp).
|
||||
Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to run confirm: %w", err)
|
||||
}
|
||||
|
||||
if m.(model).aborted {
|
||||
os.Exit(130)
|
||||
} else if m.(model).confirmation {
|
||||
os.Exit(0)
|
||||
} else {
|
||||
if !choice {
|
||||
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,141 +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 (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const tickInterval = time.Second
|
||||
|
||||
type tickMsg struct{}
|
||||
|
||||
func tick() tea.Cmd {
|
||||
return tea.Tick(tickInterval, func(time.Time) tea.Msg {
|
||||
return tickMsg{}
|
||||
})
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
if m.timeout > 0 {
|
||||
return tick()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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.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 tickMsg:
|
||||
if m.timeout <= 0 {
|
||||
m.quitting = true
|
||||
m.confirmation = m.defaultSelection
|
||||
return m, tea.Quit
|
||||
}
|
||||
m.timeout -= tickInterval
|
||||
return m, tick()
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
if m.quitting {
|
||||
return ""
|
||||
}
|
||||
|
||||
var aff, neg, timeout, affirmativeTimeout, negativeTimeout string
|
||||
|
||||
if m.hasTimeout {
|
||||
timeout = fmt.Sprintf(" (%d)", max(0, int(m.timeout.Seconds())))
|
||||
}
|
||||
|
||||
// set timer based on defaultSelection
|
||||
if m.defaultSelection {
|
||||
affirmativeTimeout = m.affirmative + timeout
|
||||
negativeTimeout = m.negative
|
||||
} else {
|
||||
affirmativeTimeout = m.affirmative
|
||||
negativeTimeout = m.negative + timeout
|
||||
}
|
||||
|
||||
if m.confirmation {
|
||||
aff = m.selectedStyle.Render(affirmativeTimeout)
|
||||
neg = m.unselectedStyle.Render(negativeTimeout)
|
||||
} else {
|
||||
aff = m.unselectedStyle.Render(affirmativeTimeout)
|
||||
neg = m.selectedStyle.Render(negativeTimeout)
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
|
@ -8,14 +8,16 @@ import (
|
|||
|
||||
// Options is the customization options for the confirm command.
|
||||
type Options struct {
|
||||
Affirmative string `help:"The title of the affirmative action" default:"Yes"`
|
||||
Negative string `help:"The title of the negative action" default:"No"`
|
||||
Default bool `help:"Default confirmation action" default:"true"`
|
||||
Timeout time.Duration `help:"Timeout for confirmation" default:"0" env:"GUM_CONFIRM_TIMEOUT"`
|
||||
Prompt string `arg:"" help:"Prompt to display." default:"Are you sure?"`
|
||||
PromptStyle style.Styles `embed:"" prefix:"prompt." help:"The style of the prompt" set:"defaultMargin=1 0 0 0" envprefix:"GUM_CONFIRM_PROMPT_"`
|
||||
Default bool `help:"Default confirmation action" default:"true"`
|
||||
Affirmative string `help:"The title of the affirmative action" default:"Yes"`
|
||||
Negative string `help:"The title of the negative action" default:"No"`
|
||||
Prompt string `arg:"" help:"Prompt to display." default:"Are you sure?"`
|
||||
//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_"`
|
||||
PromptStyle style.Styles `embed:"" prefix:"prompt." help:"The style of the prompt" set:"defaultMargin=0 0 0 1" set:"defaultForeground=#7571F9" set:"defaultBold=true" envprefix:"GUM_CONFIRM_PROMPT_"`
|
||||
//nolint:staticcheck
|
||||
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_"`
|
||||
SelectedStyle style.Styles `embed:"" prefix:"selected." help:"The style of the selected action" set:"defaultBackground=212" set:"defaultForeground=230" set:"defaultPadding=0 3" set:"defaultMargin=0 1" envprefix:"GUM_CONFIRM_SELECTED_"`
|
||||
//nolint:staticcheck
|
||||
UnselectedStyle style.Styles `embed:"" prefix:"unselected." help:"The style of the unselected action" set:"defaultBackground=235" set:"defaultForeground=254" set:"defaultPadding=0 3" set:"defaultMargin=0 1" envprefix:"GUM_CONFIRM_UNSELECTED_"`
|
||||
ShowHelp bool `help:"Show help key binds" negatable:"" default:"true" env:"GUM_CONFIRM_SHOW_HELP"`
|
||||
Timeout time.Duration `help:"Timeout until confirm returns selected value or default if provided" default:"0" env:"GUM_CONFIRM_TIMEOUT"`
|
||||
}
|
||||
|
|
12
cursor/cursor.go
Normal file
12
cursor/cursor.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package cursor
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/cursor"
|
||||
)
|
||||
|
||||
// Modes maps strings to cursor modes.
|
||||
var Modes = map[string]cursor.Mode{
|
||||
"blink": cursor.CursorBlink,
|
||||
"hide": cursor.CursorHide,
|
||||
"static": cursor.CursorStatic,
|
||||
}
|
11
default.nix
11
default.nix
|
@ -1,7 +1,12 @@
|
|||
{ pkgs }:
|
||||
|
||||
pkgs.buildGoModule {
|
||||
name = "gum";
|
||||
pkgs.buildGoModule rec {
|
||||
pname = "gum";
|
||||
version = "0.14.0";
|
||||
|
||||
src = ./.;
|
||||
vendorSha256 = "sha256-rOBwhPXo4sTSI3j3rn3c5qWGnGFgkpeFUKgtzKBltbg=";
|
||||
|
||||
vendorHash = "sha256-gDDaKrwlrJyyDzgyGf9iP/XPnOAwpkvIyzCXobXrlF4=";
|
||||
|
||||
ldflags = [ "-s" "-w" "-X=main.Version=${version}" ];
|
||||
}
|
||||
|
|
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
|
||||
|
47
examples/kaomoji.sh
Normal file
47
examples/kaomoji.sh
Normal file
|
@ -0,0 +1,47 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# If the user passes '-h', '--help', or 'help' print out a little bit of help.
|
||||
# text.
|
||||
case "$1" in
|
||||
"-h" | "--help" | "help")
|
||||
printf 'Generate kaomojis on request.\n\n'
|
||||
printf 'Usage: %s [kind]\n' "$(basename "$0")"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# The user can pass an argument like "bear" or "angry" to specify the general
|
||||
# kind of Kaomoji produced.
|
||||
sentiment=""
|
||||
if [[ $1 != "" ]]; then
|
||||
sentiment=" $1"
|
||||
fi
|
||||
|
||||
# Ask mods to generate Kaomojis. Save the output in a variable.
|
||||
kaomoji="$(mods "generate 10${sentiment} kaomojis. number them and put each one on its own line.")"
|
||||
if [[ $kaomoji == "" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Pipe mods output to gum so the user can choose the perfect kaomoji. Save that
|
||||
# choice in a variable. Also note that we're using cut to drop the item number
|
||||
# in front of the Kaomoji.
|
||||
choice="$(echo "$kaomoji" | gum choose | cut -d ' ' -f 2)"
|
||||
if [[ $choice == "" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If xsel (X11) or pbcopy (macOS) exists, copy to the clipboard. If not, just
|
||||
# print the Kaomoji.
|
||||
if command -v xsel &> /dev/null; then
|
||||
printf '%s' "$choice" | xclip -sel clip # X11
|
||||
elif command -v pbcopy &> /dev/null; then
|
||||
printf '%s' "$choice" | pbcopy # macOS
|
||||
else
|
||||
# We can't copy, so just print it out.
|
||||
printf 'Here you go: %s\n' "$choice"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# We're done!
|
||||
printf 'Copied %s to the clipboard\n' "$choice"
|
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....
|
|
@ -40,7 +40,7 @@ gum style --foreground 99 --border double --border-foreground 99 --padding "1 2"
|
|||
|
||||
# Write
|
||||
gum write
|
||||
gum write --width 40 --height 3 --placeholder "Type whatever you want" --prompt "| " --show-cursor-line --show-line-numbers --value "Something..." --base.padding 1 --cursor.foreground 99 --prompt.foreground 99
|
||||
gum write --width 40 --height 6 --placeholder "Type whatever you want" --prompt "| " --show-cursor-line --show-line-numbers --value "Something..." --base.padding 1 --cursor.foreground 99 --prompt.foreground 99
|
||||
|
||||
# Table
|
||||
gum table < table/example.csv
|
||||
|
|
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,14 +3,10 @@ package file
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/charmbracelet/bubbles/filepicker"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/gum/internal/exit"
|
||||
"github.com/charmbracelet/gum/style"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// Run is the interface to picking a file.
|
||||
|
@ -28,47 +24,40 @@ func (o Options) Run() error {
|
|||
return fmt.Errorf("file not found: %w", err)
|
||||
}
|
||||
|
||||
fp := filepicker.New()
|
||||
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.Styles{
|
||||
Cursor: o.CursorStyle.ToLipgloss(),
|
||||
Symlink: o.SymlinkStyle.ToLipgloss(),
|
||||
Directory: o.DirectoryStyle.ToLipgloss(),
|
||||
File: o.FileStyle.ToLipgloss(),
|
||||
Permission: o.PermissionsStyle.ToLipgloss(),
|
||||
Selected: o.SelectedStyle.ToLipgloss(),
|
||||
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}
|
||||
keymap := huh.NewDefaultKeyMap()
|
||||
keymap.FilePicker.Open.SetEnabled(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(o.ShowHelp).
|
||||
WithKeyMap(keymap).
|
||||
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
|
||||
}
|
||||
|
|
56
file/file.go
56
file/file.go
|
@ -1,56 +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 (
|
||||
"github.com/charmbracelet/bubbles/filepicker"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type model struct {
|
||||
filepicker filepicker.Model
|
||||
selectedPath string
|
||||
aborted bool
|
||||
quitting bool
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return m.filepicker.Init()
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q", "esc":
|
||||
m.aborted = true
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.filepicker, cmd = m.filepicker.Update(msg)
|
||||
if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect {
|
||||
m.selectedPath = path
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
if m.quitting {
|
||||
return ""
|
||||
}
|
||||
return m.filepicker.View()
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
package file
|
||||
|
||||
import "github.com/charmbracelet/gum/style"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Options are the options for the file command.
|
||||
type Options struct {
|
||||
|
@ -11,8 +15,9 @@ type Options struct {
|
|||
All bool `short:"a" help:"Show hidden and 'dot' files" default:"false" env:"GUM_FILE_ALL"`
|
||||
File bool `help:"Allow files selection" default:"true" env:"GUM_FILE_FILE"`
|
||||
Directory bool `help:"Allow directories selection" default:"false" env:"GUM_FILE_DIRECTORY"`
|
||||
ShowHelp bool `help:"Show help key binds" negatable:"" default:"true" env:"GUM_FILE_SHOW_HELP"`
|
||||
|
||||
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_"`
|
||||
|
@ -21,5 +26,6 @@ type Options struct {
|
|||
//nolint:staticcheck
|
||||
SelectedStyle style.Styles `embed:"" prefix:"selected." help:"The style to use for the selected item" set:"defaultBold=true" set:"defaultForeground=212" envprefix:"GUM_FILE_SELECTED_"`
|
||||
//nolint:staticcheck
|
||||
FileSizeStyle style.Styles `embed:"" prefix:"file-size." help:"The style to use for file sizes" set:"defaultWidth=8" set:"defaultAlign=right" set:"defaultForeground=240" envprefix:"GUM_FILE_FILE_SIZE_"`
|
||||
FileSizeStyle style.Styles `embed:"" prefix:"file-size." help:"The style to use for file sizes" set:"defaultWidth=8" set:"defaultAlign=right" set:"defaultForeground=240" envprefix:"GUM_FILE_FILE_SIZE_"`
|
||||
Timeout time.Duration `help:"Timeout until command aborts without a selection" default:"0" env:"GUM_FILE_TIMEOUT"`
|
||||
}
|
||||
|
|
|
@ -6,16 +6,16 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/sahilm/fuzzy"
|
||||
|
||||
"github.com/charmbracelet/gum/ansi"
|
||||
"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
|
||||
|
@ -26,25 +26,29 @@ 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
|
||||
|
||||
v := viewport.New(o.Width, o.Height)
|
||||
|
||||
var choices []string
|
||||
if input, _ := stdin.Read(); input != "" {
|
||||
input = strings.TrimSuffix(input, "\n")
|
||||
if input != "" {
|
||||
choices = strings.Split(input, "\n")
|
||||
if len(o.Options) == 0 {
|
||||
if input, _ := stdin.Read(); input != "" {
|
||||
o.Options = strings.Split(input, "\n")
|
||||
} else {
|
||||
o.Options = files.List()
|
||||
}
|
||||
} else {
|
||||
choices = files.List()
|
||||
}
|
||||
|
||||
if len(choices) == 0 {
|
||||
if len(o.Options) == 0 {
|
||||
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())
|
||||
|
@ -56,19 +60,19 @@ func (o Options) Run() error {
|
|||
}
|
||||
switch {
|
||||
case o.Value != "" && o.Fuzzy:
|
||||
matches = fuzzy.Find(o.Value, choices)
|
||||
matches = fuzzy.Find(o.Value, o.Options)
|
||||
case o.Value != "" && !o.Fuzzy:
|
||||
matches = exactMatches(o.Value, choices)
|
||||
matches = exactMatches(o.Value, o.Options)
|
||||
default:
|
||||
matches = matchAll(choices)
|
||||
matches = matchAll(o.Options)
|
||||
}
|
||||
|
||||
if o.NoLimit {
|
||||
o.Limit = len(choices)
|
||||
o.Limit = len(o.Options)
|
||||
}
|
||||
|
||||
p := tea.NewProgram(model{
|
||||
choices: choices,
|
||||
choices: o.Options,
|
||||
indicator: o.Indicator,
|
||||
matches: matches,
|
||||
header: o.Header,
|
||||
|
@ -82,11 +86,15 @@ func (o Options) Run() error {
|
|||
matchStyle: o.MatchStyle.ToLipgloss(),
|
||||
headerStyle: o.HeaderStyle.ToLipgloss(),
|
||||
textStyle: o.TextStyle.ToLipgloss(),
|
||||
cursorTextStyle: o.CursorTextStyle.ToLipgloss(),
|
||||
height: o.Height,
|
||||
selected: make(map[string]struct{}),
|
||||
limit: o.Limit,
|
||||
reverse: o.Reverse,
|
||||
fuzzy: o.Fuzzy,
|
||||
timeout: o.Timeout,
|
||||
hasTimeout: o.Timeout > 0,
|
||||
sort: o.Sort,
|
||||
}, options...)
|
||||
|
||||
tm, err := p.Run()
|
||||
|
@ -98,15 +106,19 @@ func (o Options) Run() error {
|
|||
return exit.ErrAborted
|
||||
}
|
||||
|
||||
isTTY := isatty.IsTerminal(os.Stdout.Fd())
|
||||
|
||||
// allSelections contains values only if limit is greater
|
||||
// than 1 or if flag --no-limit is passed, hence there is
|
||||
// no need to further checks
|
||||
if len(m.selected) > 0 {
|
||||
for k := range m.selected {
|
||||
fmt.Println(k)
|
||||
}
|
||||
o.checkSelected(m, isTTY)
|
||||
} else if len(m.matches) > m.cursor && m.cursor >= 0 {
|
||||
fmt.Println(m.matches[m.cursor].Str)
|
||||
if isTTY {
|
||||
fmt.Println(m.matches[m.cursor].Str)
|
||||
} else {
|
||||
fmt.Println(ansi.Strip(m.matches[m.cursor].Str))
|
||||
}
|
||||
}
|
||||
|
||||
if !o.Strict && len(m.textinput.Value()) != 0 && len(m.matches) == 0 {
|
||||
|
@ -115,8 +127,12 @@ 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
|
||||
func (o Options) checkSelected(m model, isTTY bool) {
|
||||
for k := range m.selected {
|
||||
if isTTY {
|
||||
fmt.Println(k)
|
||||
} else {
|
||||
fmt.Println(ansi.Strip(k))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,12 +12,14 @@ package filter
|
|||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/sahilm/fuzzy"
|
||||
)
|
||||
|
||||
|
@ -40,20 +42,27 @@ type model struct {
|
|||
headerStyle lipgloss.Style
|
||||
matchStyle lipgloss.Style
|
||||
textStyle lipgloss.Style
|
||||
cursorTextStyle lipgloss.Style
|
||||
indicatorStyle lipgloss.Style
|
||||
selectedPrefixStyle lipgloss.Style
|
||||
unselectedPrefixStyle lipgloss.Style
|
||||
reverse bool
|
||||
fuzzy bool
|
||||
sort bool
|
||||
timeout time.Duration
|
||||
hasTimeout bool
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd { return nil }
|
||||
func (m model) Init() tea.Cmd {
|
||||
return timeout.Init(m.timeout, nil)
|
||||
}
|
||||
func (m model) View() string {
|
||||
if m.quitting {
|
||||
return ""
|
||||
}
|
||||
|
||||
var s strings.Builder
|
||||
var lineTextStyle lipgloss.Style
|
||||
|
||||
// For reverse layout, if the number of matches is less than the viewport
|
||||
// height, we need to offset the matches so that the first match is at the
|
||||
|
@ -74,10 +83,14 @@ func (m model) View() string {
|
|||
|
||||
// If this is the current selected index, we add a small indicator to
|
||||
// represent it. Otherwise, simply pad the string.
|
||||
// The line's text style is set depending on whether or not the cursor
|
||||
// points to this line.
|
||||
if i == m.cursor {
|
||||
s.WriteString(m.indicatorStyle.Render(m.indicator))
|
||||
lineTextStyle = m.cursorTextStyle
|
||||
} else {
|
||||
s.WriteString(strings.Repeat(" ", runewidth.StringWidth(m.indicator)))
|
||||
s.WriteString(strings.Repeat(" ", lipgloss.Width(m.indicator)))
|
||||
lineTextStyle = m.textStyle
|
||||
}
|
||||
|
||||
// If there are multiple selections mark them, otherwise leave an empty space
|
||||
|
@ -99,7 +112,7 @@ func (m model) View() string {
|
|||
// index. If so, color the character to indicate a match.
|
||||
if mi < len(match.MatchedIndexes) && ci == match.MatchedIndexes[mi] {
|
||||
// Flush text buffer.
|
||||
s.WriteString(m.textStyle.Render(buf.String()))
|
||||
s.WriteString(lineTextStyle.Render(buf.String()))
|
||||
buf.Reset()
|
||||
|
||||
s.WriteString(m.matchStyle.Render(string(c)))
|
||||
|
@ -112,7 +125,7 @@ func (m model) View() string {
|
|||
}
|
||||
}
|
||||
// Flush text buffer.
|
||||
s.WriteString(m.textStyle.Render(buf.String()))
|
||||
s.WriteString(lineTextStyle.Render(buf.String()))
|
||||
|
||||
// We have finished displaying the match with all of it's matched
|
||||
// characters highlighted and the rest filled in.
|
||||
|
@ -143,6 +156,15 @@ func (m model) View() string {
|
|||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
switch msg := msg.(type) {
|
||||
case timeout.TickTimeoutMsg:
|
||||
if msg.TimeoutValue <= 0 {
|
||||
m.quitting = true
|
||||
m.aborted = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
m.timeout = msg.TimeoutValue
|
||||
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
if m.height == 0 || m.height > msg.Height {
|
||||
m.viewport.Height = msg.Height - lipgloss.Height(m.textinput.View())
|
||||
|
@ -201,7 +223,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
// A character was entered, this likely means that the text input has
|
||||
// changed. This suggests that the matches are outdated, so update them.
|
||||
if m.fuzzy {
|
||||
m.matches = fuzzy.Find(m.textinput.Value(), m.choices)
|
||||
if m.sort {
|
||||
m.matches = fuzzy.Find(m.textinput.Value(), m.choices)
|
||||
} else {
|
||||
m.matches = fuzzy.FindNoSort(m.textinput.Value(), m.choices)
|
||||
}
|
||||
} else {
|
||||
m.matches = exactMatches(m.textinput.Value(), m.choices)
|
||||
}
|
||||
|
|
|
@ -1,28 +1,39 @@
|
|||
package filter
|
||||
|
||||
import "github.com/charmbracelet/gum/style"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Options is the customization options for the filter command.
|
||||
type Options struct {
|
||||
Indicator string `help:"Character for selection" default:"•" env:"GUM_FILTER_INDICATOR"`
|
||||
IndicatorStyle style.Styles `embed:"" prefix:"indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_INDICATOR_"`
|
||||
Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"`
|
||||
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
|
||||
Strict bool `help:"Only returns if anything matched. Otherwise return Filter" negatable:"true" default:"true" group:"Selection"`
|
||||
SelectedPrefix string `help:"Character to indicate selected items (hidden if limit is 1)" default:" ◉ " env:"GUM_FILTER_SELECTED_PREFIX"`
|
||||
SelectedPrefixStyle style.Styles `embed:"" prefix:"selected-indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_SELECTED_PREFIX_"`
|
||||
UnselectedPrefix string `help:"Character to indicate unselected items (hidden if limit is 1)" default:" ○ " env:"GUM_FILTER_UNSELECTED_PREFIX"`
|
||||
UnselectedPrefixStyle style.Styles `embed:"" prefix:"unselected-prefix." set:"defaultForeground=240" envprefix:"GUM_FILTER_UNSELECTED_PREFIX_"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_FILTER_HEADER_"`
|
||||
Header string `help:"Header value" default:"" env:"GUM_FILTER_HEADER"`
|
||||
TextStyle style.Styles `embed:"" prefix:"text." envprefix:"GUM_FILTER_TEXT_"`
|
||||
MatchStyle style.Styles `embed:"" prefix:"match." set:"defaultForeground=212" envprefix:"GUM_FILTER_MATCH_"`
|
||||
Placeholder string `help:"Placeholder value" default:"Filter..." env:"GUM_FILTER_PLACEHOLDER"`
|
||||
Prompt string `help:"Prompt to display" default:"> " env:"GUM_FILTER_PROMPT"`
|
||||
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=240" envprefix:"GUM_FILTER_PROMPT_"`
|
||||
Width int `help:"Input width" default:"20" env:"GUM_FILTER_WIDTH"`
|
||||
Height int `help:"Input height" default:"0" env:"GUM_FILTER_HEIGHT"`
|
||||
Value string `help:"Initial filter value" default:"" env:"GUM_FILTER_VALUE"`
|
||||
Reverse bool `help:"Display from the bottom of the screen" env:"GUM_FILTER_REVERSE"`
|
||||
Fuzzy bool `help:"Enable fuzzy matching" default:"true" env:"GUM_FILTER_FUZZY" negatable:""`
|
||||
Options []string `arg:"" optional:"" help:"Options to filter."`
|
||||
|
||||
Indicator string `help:"Character for selection" default:"•" env:"GUM_FILTER_INDICATOR"`
|
||||
IndicatorStyle style.Styles `embed:"" prefix:"indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_INDICATOR_"`
|
||||
Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"`
|
||||
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
|
||||
SelectIfOne bool `help:"Select the given option if there is only one" group:"Selection"`
|
||||
Strict bool `help:"Only returns if anything matched. Otherwise return Filter" negatable:"true" default:"true" group:"Selection"`
|
||||
SelectedPrefix string `help:"Character to indicate selected items (hidden if limit is 1)" default:" ◉ " env:"GUM_FILTER_SELECTED_PREFIX"`
|
||||
SelectedPrefixStyle style.Styles `embed:"" prefix:"selected-indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_SELECTED_PREFIX_"`
|
||||
UnselectedPrefix string `help:"Character to indicate unselected items (hidden if limit is 1)" default:" ○ " env:"GUM_FILTER_UNSELECTED_PREFIX"`
|
||||
UnselectedPrefixStyle style.Styles `embed:"" prefix:"unselected-prefix." set:"defaultForeground=240" envprefix:"GUM_FILTER_UNSELECTED_PREFIX_"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_FILTER_HEADER_"`
|
||||
Header string `help:"Header value" default:"" env:"GUM_FILTER_HEADER"`
|
||||
TextStyle style.Styles `embed:"" prefix:"text." envprefix:"GUM_FILTER_TEXT_"`
|
||||
CursorTextStyle style.Styles `embed:"" prefix:"cursor-text." envprefix:"GUM_FILTER_CURSOR_TEXT_"`
|
||||
MatchStyle style.Styles `embed:"" prefix:"match." set:"defaultForeground=212" envprefix:"GUM_FILTER_MATCH_"`
|
||||
Placeholder string `help:"Placeholder value" default:"Filter..." env:"GUM_FILTER_PLACEHOLDER"`
|
||||
Prompt string `help:"Prompt to display" default:"> " env:"GUM_FILTER_PROMPT"`
|
||||
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=240" envprefix:"GUM_FILTER_PROMPT_"`
|
||||
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"`
|
||||
Reverse bool `help:"Display from the bottom of the screen" env:"GUM_FILTER_REVERSE"`
|
||||
Fuzzy bool `help:"Enable fuzzy matching" default:"true" env:"GUM_FILTER_FUZZY" negatable:""`
|
||||
Sort bool `help:"Sort the results" default:"true" env:"GUM_FILTER_SORT" negatable:""`
|
||||
Timeout time.Duration `help:"Timeout until filter command aborts" default:"0" env:"GUM_FILTER_TIMEOUT"`
|
||||
}
|
||||
|
|
32
flake.lock
32
flake.lock
|
@ -1,12 +1,15 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1659877975,
|
||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -17,16 +20,16 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1664107978,
|
||||
"narHash": "sha256-31I9XnIjXkUa62BM1Zr/ylKMf9eVO5PtoX2mGpmB7/U=",
|
||||
"lastModified": 1715447595,
|
||||
"narHash": "sha256-VsVAUQOj/cS1LCOmMjAGeRksXIAdPnFIjCQ0XLkCsT0=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "72783a2d0dbbf030bff1537873dd5b85b3fb332f",
|
||||
"rev": "062ca2a9370a27a35c524dc82d540e6e9824b652",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-22.05",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
|
@ -36,6 +39,21 @@
|
|||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
description = "A tool for glamorous shell scripts";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = github:nixos/nixpkgs/nixos-22.05;
|
||||
flake-utils.url = github:numtide/flake-utils;
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
|
|
|
@ -54,7 +54,7 @@ func markdown(input string, theme string) (string, error) {
|
|||
}
|
||||
|
||||
func template(input string) (string, error) {
|
||||
f := termenv.TemplateFuncs(termenv.ColorProfile())
|
||||
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)
|
||||
|
|
|
@ -3,8 +3,8 @@ package format
|
|||
// Options is customization options for the format command.
|
||||
type Options struct {
|
||||
Template []string `arg:"" optional:"" help:"Template string to format (can also be provided via stdin)"`
|
||||
Theme string `help:"Glamour theme to use for markdown formatting" default:"pink"`
|
||||
Language string `help:"Programming language to parse code" short:"l" default:""`
|
||||
Theme string `help:"Glamour theme to use for markdown formatting" default:"pink" env:"GUM_FORMAT_THEME"`
|
||||
Language string `help:"Programming language to parse code" short:"l" default:"" env:"GUM_FORMAT_LANGUAGE"`
|
||||
|
||||
Type string `help:"Format to use (markdown,template,code,emoji)" enum:"markdown,template,code,emoji" short:"t" default:"markdown"`
|
||||
Type string `help:"Format to use (markdown,template,code,emoji)" enum:"markdown,template,code,emoji" short:"t" default:"markdown" env:"GUM_FORMAT_TYPE"`
|
||||
}
|
||||
|
|
58
go.mod
58
go.mod
|
@ -1,44 +1,54 @@
|
|||
module github.com/charmbracelet/gum
|
||||
|
||||
go 1.18
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/alecthomas/kong v0.7.1
|
||||
github.com/alecthomas/kong v0.9.0
|
||||
github.com/alecthomas/mango-kong v0.1.0
|
||||
github.com/charmbracelet/bubbles v0.15.1-0.20230324185713-1de5816ab4f7
|
||||
github.com/charmbracelet/bubbletea v0.23.3-0.20230316100943-248eb83001a7
|
||||
github.com/charmbracelet/glamour v0.6.0
|
||||
github.com/charmbracelet/lipgloss v0.7.2-0.20230316100548-06dd20ee5707
|
||||
github.com/mattn/go-runewidth v0.0.14
|
||||
github.com/charmbracelet/bubbles v0.18.0
|
||||
github.com/charmbracelet/bubbletea v0.26.3
|
||||
github.com/charmbracelet/glamour v0.7.0
|
||||
github.com/charmbracelet/huh v0.4.2
|
||||
github.com/charmbracelet/lipgloss v0.11.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-0.20230323153104-73a40463ff25
|
||||
github.com/sahilm/fuzzy v0.1.0
|
||||
github.com/muesli/termenv v0.15.2
|
||||
github.com/sahilm/fuzzy v0.1.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/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/ansi v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240525152034-77596eb8760e // indirect
|
||||
github.com/charmbracelet/x/input v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/term v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.1.2 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.23 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // 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/muesli/reflow v0.3.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/net v0.8.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yuin/goldmark v1.7.1 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.2 // indirect
|
||||
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
)
|
||||
|
|
184
go.sum
184
go.sum
|
@ -1,145 +1,115 @@
|
|||
github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0=
|
||||
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/alecthomas/kong v0.7.1 h1:azoTh0IOfwlAX3qN9sHWTxACE2oV8Bg2gAwBsMwDQY4=
|
||||
github.com/alecthomas/kong v0.7.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
|
||||
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
||||
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
||||
github.com/alecthomas/kong 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.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
|
||||
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 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
|
||||
github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
|
||||
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.15.1-0.20230306155959-3372cf1aea2b h1:K9dWJ2spDhDhIrqnchjG867djPxWWe3mwdk6RdLMfhg=
|
||||
github.com/charmbracelet/bubbles v0.15.1-0.20230306155959-3372cf1aea2b/go.mod h1:39HL8bnL0foloiENA/KvD+3mNg5SqWQV2Qh3eY/4ey4=
|
||||
github.com/charmbracelet/bubbles v0.15.1-0.20230311144731-7ba044773bfe h1:j4zeR1cZdlf+VmGvpU4NIANsobFfD3u6+fFn9CInaUE=
|
||||
github.com/charmbracelet/bubbles v0.15.1-0.20230311144731-7ba044773bfe/go.mod h1:39HL8bnL0foloiENA/KvD+3mNg5SqWQV2Qh3eY/4ey4=
|
||||
github.com/charmbracelet/bubbles v0.15.1-0.20230324185713-1de5816ab4f7 h1:16QEZgcSr80fcXZBlqSZG5F+1j5CATd/ZLcNjV8DBdU=
|
||||
github.com/charmbracelet/bubbles v0.15.1-0.20230324185713-1de5816ab4f7/go.mod h1:39HL8bnL0foloiENA/KvD+3mNg5SqWQV2Qh3eY/4ey4=
|
||||
github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=
|
||||
github.com/charmbracelet/bubbletea v0.23.2 h1:vuUJ9HJ7b/COy4I30e8xDVQ+VRDUEFykIjryPfgsdps=
|
||||
github.com/charmbracelet/bubbletea v0.23.2/go.mod h1:FaP3WUivcTM0xOKNmhciz60M6I+weYLF76mr1JyI7sM=
|
||||
github.com/charmbracelet/bubbletea v0.23.3-0.20230316100943-248eb83001a7 h1:QWRijV4kfijpili/tRA9FAxS1Pw1B2n4+RF2UVOmBbw=
|
||||
github.com/charmbracelet/bubbletea v0.23.3-0.20230316100943-248eb83001a7/go.mod h1:/9VgEZjOAyBTwlaj5ILlkrpeuulz+b2ITKJNKo/5vK0=
|
||||
github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=
|
||||
github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
|
||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
|
||||
github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
|
||||
github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
|
||||
github.com/charmbracelet/lipgloss v0.7.2-0.20230316100548-06dd20ee5707 h1:dXv2HjaDlJZj7wLpTjg1P4B68bdvoXfx7+VXF2/RelY=
|
||||
github.com/charmbracelet/lipgloss v0.7.2-0.20230316100548-06dd20ee5707/go.mod h1:BDceYFEeE5FBoGZeuApZ+V4wSgi8AOIHoryyjYbCTHM=
|
||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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.3 h1:iXyGvI+FfOWqkB2V07m1DF3xxQijxjY2j8PqiXYqasg=
|
||||
github.com/charmbracelet/bubbletea v0.26.3/go.mod h1:bpZHfDHTYJC5g+FBK+ptJRCQotRC+Dhh3AoMxa/2+3Q=
|
||||
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.4.2 h1:5wLkwrA58XDAfEZsJzNQlfJ+K8N9+wYwvR5FOM7jXFM=
|
||||
github.com/charmbracelet/huh v0.4.2/go.mod h1:g9OXBgtY3zRV4ahnVih9bZE+1yGYN+y2C9Q6L2P+WM0=
|
||||
github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g=
|
||||
github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8=
|
||||
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/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk=
|
||||
github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240525152034-77596eb8760e h1:DhvN6ye3nHLhRtNHtlrQ0Zk+vmeN7YtEnyIRfcl7e0E=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240525152034-77596eb8760e/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||
github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a h1:k/s6UoOSVynWiw7PlclyGO2VdVs5ZLbMIHiGp4shFZE=
|
||||
github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a/go.mod h1:YBotIGhfoWhHDlnUpJMkjebGV2pdGRCn1Y4/Nk/vVcU=
|
||||
github.com/charmbracelet/x/input v0.1.1 h1:YDOJaTUKCqtGnq9PHzx3pkkl4pXDOANUHmhH3DqMtM4=
|
||||
github.com/charmbracelet/x/input v0.1.1/go.mod h1:jvdTVUnNWj/RD6hjC4FsoB0SeZCJ2ZBkiuFP9zXvZI0=
|
||||
github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
|
||||
github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw=
|
||||
github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg=
|
||||
github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
|
||||
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
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/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lucasb-eyer/go-colorful v1.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.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
|
||||
github.com/microcosm-cc/bluemonday v1.0.22 h1:p2tT7RNzRdCi0qmwxG+HbqD6ILkmwter1ZwVZn1oTxA=
|
||||
github.com/microcosm-cc/bluemonday v1.0.22/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
|
||||
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/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a h1:jlDOeO5TU0pYlbc/y6PFguab5IjANI0Knrpg3u/ton4=
|
||||
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/ansi v0.0.0-20230307104941-78d3738a59f2 h1:95KWtCWqE5TaDGV9kDxSSvuXGGHIZ0FQsr9jHLllfzM=
|
||||
github.com/muesli/ansi v0.0.0-20230307104941-78d3738a59f2/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
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.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=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
|
||||
github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
|
||||
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
|
||||
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
|
||||
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
|
||||
github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8=
|
||||
github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
|
||||
github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
|
||||
github.com/muesli/termenv v0.15.2-0.20230323153104-73a40463ff25 h1:bgCNxFKF+mM5GxpNvkGleUFt12xOzLOzmMOytttpeK4=
|
||||
github.com/muesli/termenv v0.15.2-0.20230323153104-73a40463ff25/go.mod h1:puu7Fg2fBjAuOzC9hb6zDO/s86uLSYrBlPkIplp2EiA=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/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.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
|
||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
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/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
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/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
|
||||
github.com/yuin/goldmark v1.7.1/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-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4=
|
||||
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
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/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.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=
|
||||
|
|
10
gum.go
10
gum.go
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/charmbracelet/gum/format"
|
||||
"github.com/charmbracelet/gum/input"
|
||||
"github.com/charmbracelet/gum/join"
|
||||
"github.com/charmbracelet/gum/log"
|
||||
"github.com/charmbracelet/gum/man"
|
||||
"github.com/charmbracelet/gum/pager"
|
||||
"github.com/charmbracelet/gum/spin"
|
||||
|
@ -204,4 +205,13 @@ type Gum struct {
|
|||
// $ gum write > output.text
|
||||
//
|
||||
Write write.Options `cmd:"" help:"Prompt for long-form text"`
|
||||
|
||||
// Log provides a shell script interface for logging using Log.
|
||||
// https://github.com/charmbracelet/log
|
||||
//
|
||||
// It can be used to log messages to output.
|
||||
//
|
||||
// $ gum log --level info "Hello, world!"
|
||||
//
|
||||
Log log.Options `cmd:"" help:"Log messages to output"`
|
||||
}
|
||||
|
|
|
@ -2,62 +2,69 @@ package input
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"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()
|
||||
if in, _ := stdin.Read(); in != "" && o.Value == "" {
|
||||
i.SetValue(in)
|
||||
} else {
|
||||
i.SetValue(o.Value)
|
||||
var value string
|
||||
if o.Value != "" {
|
||||
value = o.Value
|
||||
} else if in, _ := stdin.Read(); in != "" {
|
||||
value = in
|
||||
}
|
||||
|
||||
i.Focus()
|
||||
i.Prompt = o.Prompt
|
||||
i.Placeholder = o.Placeholder
|
||||
i.Width = o.Width
|
||||
i.PromptStyle = o.PromptStyle.ToLipgloss()
|
||||
i.CursorStyle = o.CursorStyle.ToLipgloss()
|
||||
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()
|
||||
|
||||
// Keep input keymap backwards compatible
|
||||
keymap := huh.NewDefaultKeyMap()
|
||||
keymap.Quit = key.NewBinding(key.WithKeys("ctrl+c", "esc"))
|
||||
|
||||
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(),
|
||||
}, 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).
|
||||
WithKeyMap(keymap).
|
||||
WithShowHelp(o.ShowHelp).
|
||||
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,55 +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 (
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type model struct {
|
||||
header string
|
||||
headerStyle lipgloss.Style
|
||||
textinput textinput.Model
|
||||
quitting bool
|
||||
aborted bool
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd { return textinput.Blink }
|
||||
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 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
|
||||
}
|
|
@ -1,17 +1,25 @@
|
|||
package input
|
||||
|
||||
import "github.com/charmbracelet/gum/style"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Options are the customization options for the input.
|
||||
type Options struct {
|
||||
Placeholder string `help:"Placeholder value" default:"Type something..." env:"GUM_INPUT_PLACEHOLDER"`
|
||||
Prompt string `help:"Prompt to display" default:"> " env:"GUM_INPUT_PROMPT"`
|
||||
PromptStyle style.Styles `embed:"" prefix:"prompt." envprefix:"GUM_INPUT_PROMPT_"`
|
||||
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_INPUT_CURSOR_"`
|
||||
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" default:"40" env:"GUM_INPUT_WIDTH"`
|
||||
Password bool `help:"Mask input characters" default:"false"`
|
||||
Header string `help:"Header value" default:"" env:"GUM_INPUT_HEADER"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_INPUT_HEADER_"`
|
||||
Placeholder string `help:"Placeholder value" default:"Type something..." env:"GUM_INPUT_PLACEHOLDER"`
|
||||
Prompt string `help:"Prompt to display" default:"> " env:"GUM_INPUT_PROMPT"`
|
||||
PromptStyle style.Styles `embed:"" prefix:"prompt." envprefix:"GUM_INPUT_PROMPT_"`
|
||||
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"`
|
||||
ShowHelp bool `help:"Show help keybinds" default:"true" negatable:"true" env:"GUM_INPUT_SHOW_HELP"`
|
||||
Header string `help:"Header value" default:"" env:"GUM_INPUT_HEADER"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_INPUT_HEADER_"`
|
||||
Timeout time.Duration `help:"Timeout until input aborts" default:"0" env:"GUM_INPUT_TIMEOUT"`
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ func Read() (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
return b.String(), nil
|
||||
return strings.TrimSuffix(b.String(), "\n"), nil
|
||||
}
|
||||
|
||||
// IsEmpty returns whether stdin is empty.
|
||||
|
|
15
internal/utils/utils.go
Normal file
15
internal/utils/utils.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// LipglossPadding calculates how much padding a string is given by a style.
|
||||
func LipglossPadding(style lipgloss.Style) (int, int) {
|
||||
render := style.Render(" ")
|
||||
before := strings.Index(render, " ")
|
||||
after := len(render) - len(" ") - before
|
||||
return before, after
|
||||
}
|
141
log/command.go
Normal file
141
log/command.go
Normal file
|
@ -0,0 +1,141 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
// Run is the command-line interface for logging text.
|
||||
func (o Options) Run() error {
|
||||
l := log.New(os.Stderr)
|
||||
|
||||
if o.File != "" {
|
||||
f, err := os.OpenFile(o.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening file: %w", err)
|
||||
}
|
||||
|
||||
defer f.Close() //nolint:errcheck
|
||||
l.SetOutput(f)
|
||||
}
|
||||
|
||||
l.SetPrefix(o.Prefix)
|
||||
l.SetLevel(-math.MaxInt32) // log all levels
|
||||
l.SetReportTimestamp(o.Time != "")
|
||||
|
||||
timeFormats := map[string]string{
|
||||
"layout": time.Layout,
|
||||
"ansic": time.ANSIC,
|
||||
"unixdate": time.UnixDate,
|
||||
"rubydate": time.RubyDate,
|
||||
"rfc822": time.RFC822,
|
||||
"rfc822z": time.RFC822Z,
|
||||
"rfc850": time.RFC850,
|
||||
"rfc1123": time.RFC1123,
|
||||
"rfc1123z": time.RFC1123Z,
|
||||
"rfc3339": time.RFC3339,
|
||||
"rfc3339nano": time.RFC3339Nano,
|
||||
"kitchen": time.Kitchen,
|
||||
"stamp": time.Stamp,
|
||||
"stampmilli": time.StampMilli,
|
||||
"stampmicro": time.StampMicro,
|
||||
"stampnano": time.StampNano,
|
||||
"datetime": time.DateTime,
|
||||
"dateonly": time.DateOnly,
|
||||
"timeonly": time.TimeOnly,
|
||||
}
|
||||
|
||||
tf, ok := timeFormats[strings.ToLower(o.Time)]
|
||||
if ok {
|
||||
l.SetTimeFormat(tf)
|
||||
} else {
|
||||
l.SetTimeFormat(o.Time)
|
||||
}
|
||||
|
||||
st := log.DefaultStyles()
|
||||
lvl := levelToLog[o.Level]
|
||||
lvlStyle := o.LevelStyle.ToLipgloss()
|
||||
if lvlStyle.GetForeground() == lipgloss.Color("") {
|
||||
lvlStyle = lvlStyle.Foreground(st.Levels[lvl].GetForeground())
|
||||
}
|
||||
|
||||
st.Levels[lvl] = lvlStyle.
|
||||
SetString(strings.ToUpper(lvl.String())).
|
||||
Inline(true)
|
||||
|
||||
st.Timestamp = o.TimeStyle.ToLipgloss().
|
||||
Inline(true)
|
||||
st.Prefix = o.PrefixStyle.ToLipgloss().
|
||||
Inline(true)
|
||||
st.Message = o.MessageStyle.ToLipgloss().
|
||||
Inline(true)
|
||||
st.Key = o.KeyStyle.ToLipgloss().
|
||||
Inline(true)
|
||||
st.Value = o.ValueStyle.ToLipgloss().
|
||||
Inline(true)
|
||||
st.Separator = o.SeparatorStyle.ToLipgloss().
|
||||
Inline(true)
|
||||
|
||||
l.SetStyles(st)
|
||||
|
||||
switch o.Formatter {
|
||||
case "json":
|
||||
l.SetFormatter(log.JSONFormatter)
|
||||
case "logfmt":
|
||||
l.SetFormatter(log.LogfmtFormatter)
|
||||
case "text":
|
||||
l.SetFormatter(log.TextFormatter)
|
||||
}
|
||||
|
||||
var arg0 string
|
||||
var args []interface{}
|
||||
if len(o.Text) > 0 {
|
||||
arg0 = o.Text[0]
|
||||
}
|
||||
|
||||
if len(o.Text) > 1 {
|
||||
args = make([]interface{}, len(o.Text[1:]))
|
||||
for i, arg := range o.Text[1:] {
|
||||
args[i] = arg
|
||||
}
|
||||
}
|
||||
|
||||
logger := map[string]logger{
|
||||
"none": {printf: l.Printf, print: l.Print},
|
||||
"debug": {printf: l.Debugf, print: l.Debug},
|
||||
"info": {printf: l.Infof, print: l.Info},
|
||||
"warn": {printf: l.Warnf, print: l.Warn},
|
||||
"error": {printf: l.Errorf, print: l.Error},
|
||||
"fatal": {printf: l.Fatalf, print: l.Fatal},
|
||||
}[o.Level]
|
||||
|
||||
if o.Format {
|
||||
logger.printf(arg0, args...)
|
||||
} else if o.Structured {
|
||||
logger.print(arg0, args...)
|
||||
} else {
|
||||
logger.print(strings.Join(o.Text, " "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
printf func(string, ...interface{})
|
||||
print func(interface{}, ...interface{})
|
||||
}
|
||||
|
||||
var levelToLog = map[string]log.Level{
|
||||
"none": log.Level(math.MaxInt32),
|
||||
"debug": log.DebugLevel,
|
||||
"info": log.InfoLevel,
|
||||
"warn": log.WarnLevel,
|
||||
"error": log.ErrorLevel,
|
||||
"fatal": log.FatalLevel,
|
||||
}
|
26
log/options.go
Normal file
26
log/options.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Options is the set of options that can configure a join.
|
||||
type Options struct {
|
||||
Text []string `arg:"" help:"Text to log"`
|
||||
|
||||
File string `short:"o" help:"Log to file"`
|
||||
Format bool `short:"f" help:"Format message using printf" xor:"format,structured"`
|
||||
Formatter string `help:"The log formatter to use" enum:"json,logfmt,text" default:"text"`
|
||||
Level string `short:"l" help:"The log level to use" enum:"none,debug,info,warn,error,fatal" default:"none"`
|
||||
Prefix string `help:"Prefix to print before the message"`
|
||||
Structured bool `short:"s" help:"Use structured logging" xor:"format,structured"`
|
||||
Time string `short:"t" help:"The time format to use (kitchen, layout, ansic, rfc822, etc...)" default:""`
|
||||
|
||||
LevelStyle style.Styles `embed:"" prefix:"level." help:"The style of the level being used" set:"defaultBold=true" envprefix:"GUM_LOG_LEVEL_"` //nolint:staticcheck
|
||||
TimeStyle style.Styles `embed:"" prefix:"time." help:"The style of the time" envprefix:"GUM_LOG_TIME_"`
|
||||
PrefixStyle style.Styles `embed:"" prefix:"prefix." help:"The style of the prefix" set:"defaultBold=true" set:"defaultFaint=true" envprefix:"GUM_LOG_PREFIX_"` //nolint:staticcheck
|
||||
MessageStyle style.Styles `embed:"" prefix:"message." help:"The style of the message" envprefix:"GUM_LOG_MESSAGE_"`
|
||||
KeyStyle style.Styles `embed:"" prefix:"key." help:"The style of the key" set:"defaultFaint=true" envprefix:"GUM_LOG_KEY_"`
|
||||
ValueStyle style.Styles `embed:"" prefix:"value." help:"The style of the value" envprefix:"GUM_LOG_VALUE_"`
|
||||
SeparatorStyle style.Styles `embed:"" prefix:"separator." help:"The style of the separator" set:"defaultFaint=true" envprefix:"GUM_LOG_SEPARATOR_"`
|
||||
}
|
8
main.go
8
main.go
|
@ -7,6 +7,7 @@ import (
|
|||
"runtime/debug"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/muesli/termenv"
|
||||
|
||||
|
@ -48,8 +49,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,
|
||||
|
@ -71,7 +73,7 @@ func main() {
|
|||
},
|
||||
)
|
||||
if err := ctx.Run(); err != nil {
|
||||
if errors.Is(err, exit.ErrAborted) {
|
||||
if errors.Is(err, exit.ErrAborted) || errors.Is(err, huh.ErrUserAborted) {
|
||||
os.Exit(exit.StatusAborted)
|
||||
}
|
||||
fmt.Println(err)
|
||||
|
|
|
@ -16,9 +16,9 @@ func (m Man) BeforeApply(ctx *kong.Context) error {
|
|||
// Set the correct man pages description without color escape sequences.
|
||||
ctx.Model.Help = "A tool for glamorous shell scripts."
|
||||
man := mangokong.NewManPage(1, ctx.Model)
|
||||
man = man.WithSection("Copyright", "(C) 2021-2022 Charmbracelet, Inc.\n"+
|
||||
man = man.WithSection("Copyright", "(c) 2022-2024 Charmbracelet, Inc.\n"+
|
||||
"Released under MIT license.")
|
||||
fmt.Fprint(ctx.Stdout, man.Build(roff.NewDocument()))
|
||||
_, _ = fmt.Fprint(ctx.Stdout, man.Build(roff.NewDocument()))
|
||||
ctx.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
@ -32,12 +30,17 @@ func (o Options) Run() error {
|
|||
}
|
||||
|
||||
model := model{
|
||||
viewport: vp,
|
||||
helpStyle: o.HelpStyle.ToLipgloss(),
|
||||
content: o.Content,
|
||||
showLineNumbers: o.ShowLineNumbers,
|
||||
lineNumberStyle: o.LineNumberStyle.ToLipgloss(),
|
||||
softWrap: o.SoftWrap,
|
||||
viewport: vp,
|
||||
helpStyle: o.HelpStyle.ToLipgloss(),
|
||||
content: o.Content,
|
||||
origContent: o.Content,
|
||||
showLineNumbers: o.ShowLineNumbers,
|
||||
lineNumberStyle: o.LineNumberStyle.ToLipgloss(),
|
||||
softWrap: o.SoftWrap,
|
||||
matchStyle: o.MatchStyle.ToLipgloss(),
|
||||
matchHighlightStyle: o.MatchHighlightStyle.ToLipgloss(),
|
||||
timeout: o.Timeout,
|
||||
hasTimeout: o.Timeout > 0,
|
||||
}
|
||||
_, err := tea.NewProgram(model, tea.WithAltScreen()).Run()
|
||||
if err != nil {
|
||||
|
@ -45,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,14 +1,21 @@
|
|||
package pager
|
||||
|
||||
import "github.com/charmbracelet/gum/style"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Options are the options for the pager.
|
||||
type Options struct {
|
||||
//nolint:staticcheck
|
||||
Style style.Styles `embed:"" help:"Style the pager" set:"defaultBorder=rounded" set:"defaultPadding=0 1" set:"defaultBorderForeground=212" envprefix:"GUM_PAGER_"`
|
||||
HelpStyle style.Styles `embed:"" prefix:"help." help:"Style the help text" set:"defaultForeground=241" envprefix:"GUM_PAGER_HELP_"`
|
||||
Content string `arg:"" optional:"" help:"Display content to scroll"`
|
||||
ShowLineNumbers bool `help:"Show line numbers" default:"true"`
|
||||
LineNumberStyle style.Styles `embed:"" prefix:"line-number." help:"Style the line numbers" set:"defaultForeground=237" envprefix:"GUM_PAGER_LINE_NUMBER_"`
|
||||
SoftWrap bool `help:"Soft wrap lines" default:"false"`
|
||||
Style style.Styles `embed:"" help:"Style the pager" set:"defaultBorder=rounded" set:"defaultPadding=0 1" set:"defaultBorderForeground=212" envprefix:"GUM_PAGER_"`
|
||||
HelpStyle style.Styles `embed:"" prefix:"help." help:"Style the help text" set:"defaultForeground=241" envprefix:"GUM_PAGER_HELP_"`
|
||||
Content string `arg:"" optional:"" help:"Display content to scroll"`
|
||||
ShowLineNumbers bool `help:"Show line numbers" default:"true"`
|
||||
LineNumberStyle style.Styles `embed:"" prefix:"line-number." help:"Style the line numbers" set:"defaultForeground=237" envprefix:"GUM_PAGER_LINE_NUMBER_"`
|
||||
SoftWrap bool `help:"Soft wrap lines" default:"false"`
|
||||
MatchStyle style.Styles `embed:"" prefix:"match." help:"Style the matched text" set:"defaultForeground=212" set:"defaultBold=true" envprefix:"GUM_PAGER_MATCH_"` //nolint:staticcheck
|
||||
MatchHighlightStyle style.Styles `embed:"" prefix:"match-highlight." help:"Style the matched highlight text" set:"defaultForeground=235" set:"defaultBackground=225" set:"defaultBold=true" envprefix:"GUM_PAGER_MATCH_HIGH_"` //nolint:staticcheck
|
||||
Timeout time.Duration `help:"Timeout until command exits" default:"0" env:"GUM_PAGER_TIMEOUT"`
|
||||
}
|
||||
|
|
179
pager/pager.go
179
pager/pager.go
|
@ -1,88 +1,159 @@
|
|||
// Package pager provides a pager (similar to less) for the terminal.
|
||||
//
|
||||
// $ cat file.txt | gum page
|
||||
// $ cat file.txt | gum pager
|
||||
package pager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/timeout"
|
||||
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/muesli/reflow/truncate"
|
||||
)
|
||||
|
||||
type model struct {
|
||||
content string
|
||||
viewport viewport.Model
|
||||
helpStyle lipgloss.Style
|
||||
showLineNumbers bool
|
||||
lineNumberStyle lipgloss.Style
|
||||
softWrap bool
|
||||
content string
|
||||
origContent string
|
||||
viewport viewport.Model
|
||||
helpStyle lipgloss.Style
|
||||
showLineNumbers bool
|
||||
lineNumberStyle lipgloss.Style
|
||||
softWrap bool
|
||||
search search
|
||||
matchStyle lipgloss.Style
|
||||
matchHighlightStyle lipgloss.Style
|
||||
maxWidth int
|
||||
timeout time.Duration
|
||||
hasTimeout bool
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return nil
|
||||
return timeout.Init(m.timeout, nil)
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case timeout.TickTimeoutMsg:
|
||||
if msg.TimeoutValue <= 0 {
|
||||
return m, tea.Quit
|
||||
}
|
||||
m.timeout = msg.TimeoutValue
|
||||
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
m.viewport.Height = msg.Height - lipgloss.Height(m.helpStyle.Render("?")) - 1
|
||||
m.viewport.Width = msg.Width
|
||||
textStyle := lipgloss.NewStyle().Width(m.viewport.Width)
|
||||
var text strings.Builder
|
||||
|
||||
// Determine max width of a line
|
||||
maxLineWidth := m.viewport.Width
|
||||
if m.softWrap {
|
||||
vpStyle := m.viewport.Style
|
||||
maxLineWidth -= vpStyle.GetHorizontalBorderSize() + vpStyle.GetHorizontalMargins() + vpStyle.GetHorizontalPadding()
|
||||
if m.showLineNumbers {
|
||||
maxLineWidth -= len(" │ ")
|
||||
}
|
||||
}
|
||||
|
||||
for i, line := range strings.Split(m.content, "\n") {
|
||||
line = strings.ReplaceAll(line, "\t", " ")
|
||||
if m.showLineNumbers {
|
||||
text.WriteString(m.lineNumberStyle.Render(fmt.Sprintf("%4d │ ", i+1)))
|
||||
}
|
||||
for m.softWrap && len(line) > maxLineWidth {
|
||||
truncatedLine := runewidth.Truncate(line, maxLineWidth, "")
|
||||
text.WriteString(textStyle.Render(truncatedLine))
|
||||
text.WriteString("\n")
|
||||
if m.showLineNumbers {
|
||||
text.WriteString(m.lineNumberStyle.Render(" │ "))
|
||||
}
|
||||
line = strings.Replace(line, truncatedLine, "", 1)
|
||||
}
|
||||
text.WriteString(textStyle.Render(runewidth.Truncate(line, maxLineWidth, "")))
|
||||
text.WriteString("\n")
|
||||
}
|
||||
|
||||
diffHeight := m.viewport.Height - lipgloss.Height(text.String())
|
||||
if diffHeight > 0 && m.showLineNumbers {
|
||||
remainingLines := " ~ │ " + strings.Repeat("\n ~ │ ", diffHeight-1)
|
||||
text.WriteString(m.lineNumberStyle.Render(remainingLines))
|
||||
}
|
||||
m.viewport.SetContent(text.String())
|
||||
m.ProcessText(msg)
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "g":
|
||||
return m.KeyHandler(msg)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *model) ProcessText(msg tea.WindowSizeMsg) {
|
||||
m.viewport.Height = msg.Height - lipgloss.Height(m.helpStyle.Render("?")) - 1
|
||||
m.viewport.Width = msg.Width
|
||||
textStyle := lipgloss.NewStyle().Width(m.viewport.Width)
|
||||
var text strings.Builder
|
||||
|
||||
// Determine max width of a line.
|
||||
m.maxWidth = m.viewport.Width
|
||||
if m.softWrap {
|
||||
vpStyle := m.viewport.Style
|
||||
m.maxWidth -= vpStyle.GetHorizontalBorderSize() + vpStyle.GetHorizontalMargins() + vpStyle.GetHorizontalPadding()
|
||||
if m.showLineNumbers {
|
||||
m.maxWidth -= lipgloss.Width(" │ ")
|
||||
}
|
||||
}
|
||||
|
||||
for i, line := range strings.Split(m.content, "\n") {
|
||||
line = strings.ReplaceAll(line, "\t", " ")
|
||||
if m.showLineNumbers {
|
||||
text.WriteString(m.lineNumberStyle.Render(fmt.Sprintf("%4d │ ", i+1)))
|
||||
}
|
||||
for m.softWrap && lipgloss.Width(line) > m.maxWidth {
|
||||
truncatedLine := truncate.String(line, uint(m.maxWidth))
|
||||
text.WriteString(textStyle.Render(truncatedLine))
|
||||
text.WriteString("\n")
|
||||
if m.showLineNumbers {
|
||||
text.WriteString(m.lineNumberStyle.Render(" │ "))
|
||||
}
|
||||
line = strings.Replace(line, truncatedLine, "", 1)
|
||||
}
|
||||
text.WriteString(textStyle.Render(truncate.String(line, uint(m.maxWidth))))
|
||||
text.WriteString("\n")
|
||||
}
|
||||
|
||||
diffHeight := m.viewport.Height - lipgloss.Height(text.String())
|
||||
if diffHeight > 0 && m.showLineNumbers {
|
||||
remainingLines := " ~ │ " + strings.Repeat("\n ~ │ ", diffHeight-1)
|
||||
text.WriteString(m.lineNumberStyle.Render(remainingLines))
|
||||
}
|
||||
m.viewport.SetContent(text.String())
|
||||
}
|
||||
|
||||
const heightOffset = 2
|
||||
|
||||
func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) {
|
||||
var cmd tea.Cmd
|
||||
if m.search.active {
|
||||
switch key.String() {
|
||||
case "enter":
|
||||
if m.search.input.Value() != "" {
|
||||
m.content = m.origContent
|
||||
m.search.Execute(&m)
|
||||
|
||||
// Trigger a view update to highlight the found matches.
|
||||
m.search.NextMatch(&m)
|
||||
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
|
||||
} else {
|
||||
m.search.Done()
|
||||
}
|
||||
case "ctrl+d", "ctrl+c", "esc":
|
||||
m.search.Done()
|
||||
default:
|
||||
m.search.input, cmd = m.search.input.Update(key)
|
||||
}
|
||||
} else {
|
||||
switch key.String() {
|
||||
case "g", "home":
|
||||
m.viewport.GotoTop()
|
||||
case "G":
|
||||
case "G", "end":
|
||||
m.viewport.GotoBottom()
|
||||
case "/":
|
||||
m.search.Begin()
|
||||
case "p", "N":
|
||||
m.search.PrevMatch(&m)
|
||||
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
|
||||
case "n":
|
||||
m.search.NextMatch(&m)
|
||||
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
|
||||
case "q", "ctrl+c", "esc":
|
||||
return m, tea.Quit
|
||||
}
|
||||
m.viewport, cmd = m.viewport.Update(key)
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.viewport, cmd = m.viewport.Update(msg)
|
||||
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
return m.viewport.View() + m.helpStyle.Render("\n ↑/↓: Navigate • q: Quit")
|
||||
var timeoutStr string
|
||||
if m.hasTimeout {
|
||||
timeoutStr = timeout.Str(m.timeout) + " "
|
||||
}
|
||||
helpMsg := "\n" + timeoutStr + " ↑/↓: Navigate • q: Quit • /: Search "
|
||||
if m.search.query != nil {
|
||||
helpMsg += "• n: Next Match "
|
||||
helpMsg += "• N: Prev Match "
|
||||
}
|
||||
if m.search.active {
|
||||
return m.viewport.View() + "\n" + timeoutStr + " " + m.search.input.View()
|
||||
}
|
||||
|
||||
return m.viewport.View() + m.helpStyle.Render(helpMsg)
|
||||
}
|
||||
|
|
164
pager/search.go
Normal file
164
pager/search.go
Normal file
|
@ -0,0 +1,164 @@
|
|||
package pager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"github.com/charmbracelet/gum/internal/utils"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/muesli/reflow/truncate"
|
||||
)
|
||||
|
||||
type search struct {
|
||||
active bool
|
||||
input textinput.Model
|
||||
query *regexp.Regexp
|
||||
matchIndex int
|
||||
matchLipglossStr string
|
||||
matchString string
|
||||
}
|
||||
|
||||
func (s *search) new() {
|
||||
input := textinput.New()
|
||||
input.Placeholder = "search"
|
||||
input.Prompt = "/"
|
||||
input.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
||||
s.input = input
|
||||
}
|
||||
|
||||
func (s *search) Begin() {
|
||||
s.new()
|
||||
s.active = true
|
||||
s.input.Focus()
|
||||
}
|
||||
|
||||
// Execute find all lines in the model with a match.
|
||||
func (s *search) Execute(m *model) {
|
||||
defer s.Done()
|
||||
if s.input.Value() == "" {
|
||||
s.query = nil
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
s.query, err = regexp.Compile(s.input.Value())
|
||||
if err != nil {
|
||||
s.query = nil
|
||||
return
|
||||
}
|
||||
query := regexp.MustCompile(fmt.Sprintf("(%s)", s.query.String()))
|
||||
m.content = query.ReplaceAllString(m.content, m.matchStyle.Render("$1"))
|
||||
|
||||
// Recompile the regex to match the an replace the highlights.
|
||||
leftPad, _ := utils.LipglossPadding(m.matchStyle)
|
||||
matchingString := regexp.QuoteMeta(m.matchStyle.Render()[:leftPad]) + s.query.String() + regexp.QuoteMeta(m.matchStyle.Render()[leftPad:])
|
||||
s.query, err = regexp.Compile(matchingString)
|
||||
if err != nil {
|
||||
s.query = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *search) Done() {
|
||||
s.active = false
|
||||
|
||||
// To account for the first match is always executed.
|
||||
s.matchIndex = -1
|
||||
}
|
||||
|
||||
func (s *search) NextMatch(m *model) {
|
||||
// Check that we are within bounds.
|
||||
if s.query == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Remove previous highlight.
|
||||
m.content = strings.Replace(m.content, s.matchLipglossStr, s.matchString, 1)
|
||||
|
||||
// Highlight the next match.
|
||||
allMatches := s.query.FindAllStringIndex(m.content, -1)
|
||||
if len(allMatches) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
leftPad, rightPad := utils.LipglossPadding(m.matchStyle)
|
||||
s.matchIndex = (s.matchIndex + 1) % len(allMatches)
|
||||
match := allMatches[s.matchIndex]
|
||||
lhs := m.content[:match[0]]
|
||||
rhs := m.content[match[0]:]
|
||||
s.matchString = m.content[match[0]:match[1]]
|
||||
s.matchLipglossStr = m.matchHighlightStyle.Render(s.matchString[leftPad : len(s.matchString)-rightPad])
|
||||
m.content = lhs + strings.Replace(rhs, m.content[match[0]:match[1]], s.matchLipglossStr, 1)
|
||||
|
||||
// Update the viewport position.
|
||||
var line int
|
||||
formatStr := softWrapEm(m.content, m.maxWidth, m.softWrap)
|
||||
index := strings.Index(formatStr, s.matchLipglossStr)
|
||||
if index != -1 {
|
||||
line = strings.Count(formatStr[:index], "\n")
|
||||
}
|
||||
|
||||
// Only update if the match is not within the viewport.
|
||||
if index != -1 && (line > m.viewport.YOffset-1+m.viewport.VisibleLineCount()-1 || line < m.viewport.YOffset) {
|
||||
m.viewport.SetYOffset(line)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *search) PrevMatch(m *model) {
|
||||
// Check that we are within bounds.
|
||||
if s.query == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Remove previous highlight.
|
||||
m.content = strings.Replace(m.content, s.matchLipglossStr, s.matchString, 1)
|
||||
|
||||
// Highlight the previous match.
|
||||
allMatches := s.query.FindAllStringIndex(m.content, -1)
|
||||
if len(allMatches) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
s.matchIndex = (s.matchIndex - 1) % len(allMatches)
|
||||
if s.matchIndex < 0 {
|
||||
s.matchIndex = len(allMatches) - 1
|
||||
}
|
||||
|
||||
leftPad, rightPad := utils.LipglossPadding(m.matchStyle)
|
||||
match := allMatches[s.matchIndex]
|
||||
lhs := m.content[:match[0]]
|
||||
rhs := m.content[match[0]:]
|
||||
s.matchString = m.content[match[0]:match[1]]
|
||||
s.matchLipglossStr = m.matchHighlightStyle.Render(s.matchString[leftPad : len(s.matchString)-rightPad])
|
||||
m.content = lhs + strings.Replace(rhs, m.content[match[0]:match[1]], s.matchLipglossStr, 1)
|
||||
|
||||
// Update the viewport position.
|
||||
var line int
|
||||
formatStr := softWrapEm(m.content, m.maxWidth, m.softWrap)
|
||||
index := strings.Index(formatStr, s.matchLipglossStr)
|
||||
if index != -1 {
|
||||
line = strings.Count(formatStr[:index], "\n")
|
||||
}
|
||||
|
||||
// Only update if the match is not within the viewport.
|
||||
if index != -1 && (line > m.viewport.YOffset-1+m.viewport.VisibleLineCount()-1 || line < m.viewport.YOffset) {
|
||||
m.viewport.SetYOffset(line)
|
||||
}
|
||||
}
|
||||
|
||||
func softWrapEm(str string, maxWidth int, softWrap bool) string {
|
||||
var text strings.Builder
|
||||
for _, line := range strings.Split(str, "\n") {
|
||||
for softWrap && lipgloss.Width(line) > maxWidth {
|
||||
truncatedLine := truncate.String(line, uint(maxWidth))
|
||||
text.WriteString(truncatedLine)
|
||||
text.WriteString("\n")
|
||||
line = strings.Replace(line, truncatedLine, "", 1)
|
||||
}
|
||||
text.WriteString(truncate.String(line, uint(maxWidth)))
|
||||
text.WriteString("\n")
|
||||
}
|
||||
|
||||
return text.String()
|
||||
}
|
|
@ -4,25 +4,30 @@ 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.
|
||||
// https://github.com/charmbracelet/bubbles/spinner
|
||||
func (o Options) Run() error {
|
||||
isTTY := isatty.IsTerminal(os.Stdout.Fd())
|
||||
|
||||
s := spinner.New()
|
||||
s.Style = o.SpinnerStyle.ToLipgloss()
|
||||
s.Spinner = spinnerMap[o.Spinner]
|
||||
m := model{
|
||||
spinner: s,
|
||||
title: o.TitleStyle.ToLipgloss().Render(o.Title),
|
||||
command: o.Command,
|
||||
align: o.Align,
|
||||
spinner: s,
|
||||
title: o.TitleStyle.ToLipgloss().Render(o.Title),
|
||||
command: o.Command,
|
||||
align: o.Align,
|
||||
showOutput: o.ShowOutput && isTTY,
|
||||
showError: o.ShowError,
|
||||
timeout: o.Timeout,
|
||||
hasTimeout: o.Timeout > 0,
|
||||
}
|
||||
p := tea.NewProgram(m, tea.WithOutput(os.Stderr))
|
||||
mm, err := p.Run()
|
||||
|
@ -32,21 +37,33 @@ func (o Options) Run() error {
|
|||
return fmt.Errorf("failed to run spin: %w", err)
|
||||
}
|
||||
|
||||
if o.ShowOutput {
|
||||
fmt.Fprint(os.Stdout, m.stdout)
|
||||
fmt.Fprint(os.Stderr, m.stderr)
|
||||
}
|
||||
|
||||
if m.aborted {
|
||||
return exit.ErrAborted
|
||||
}
|
||||
|
||||
// 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.
|
||||
//nolint:nestif
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
package spin
|
||||
|
||||
import "github.com/charmbracelet/gum/style"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/gum/style"
|
||||
)
|
||||
|
||||
// Options is the customization options for the spin command.
|
||||
type Options struct {
|
||||
Command []string `arg:"" help:"Command to run"`
|
||||
|
||||
ShowOutput bool `help:"Show output of command" default:"false" env:"GUM_SPIN_SHOW_OUTPUT"`
|
||||
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"`
|
||||
TitleStyle style.Styles `embed:"" prefix:"title." envprefix:"GUM_SPIN_TITLE_"`
|
||||
Align string `help:"Alignment of spinner with regard to the title" short:"a" type:"align" enum:"left,right" default:"left" env:"GUM_SPIN_ALIGN"`
|
||||
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"`
|
||||
TitleStyle style.Styles `embed:"" prefix:"title." envprefix:"GUM_SPIN_TITLE_"`
|
||||
Align string `help:"Alignment of spinner with regard to the title" short:"a" type:"align" enum:"left,right" default:"left" env:"GUM_SPIN_ALIGN"`
|
||||
Timeout time.Duration `help:"Timeout until spin command aborts" default:"0" env:"GUM_SPIN_TIMEOUT"`
|
||||
}
|
||||
|
|
80
spin/spin.go
80
spin/spin.go
|
@ -15,28 +15,45 @@
|
|||
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"
|
||||
)
|
||||
|
||||
type model struct {
|
||||
spinner spinner.Model
|
||||
title string
|
||||
align string
|
||||
command []string
|
||||
aborted bool
|
||||
|
||||
status int
|
||||
stdout string
|
||||
stderr string
|
||||
spinner spinner.Model
|
||||
title string
|
||||
align string
|
||||
command []string
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -48,9 +65,15 @@ func commandStart(command []string) tea.Cmd {
|
|||
}
|
||||
cmd := exec.Command(command[0], args...) //nolint:gosec
|
||||
|
||||
var outbuf, errbuf strings.Builder
|
||||
cmd.Stdout = &outbuf
|
||||
cmd.Stderr = &errbuf
|
||||
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()
|
||||
|
||||
|
@ -63,6 +86,7 @@ func commandStart(command []string) tea.Cmd {
|
|||
return finishCommandMsg{
|
||||
stdout: outbuf.String(),
|
||||
stderr: errbuf.String(),
|
||||
output: bothbuf.String(),
|
||||
status: status,
|
||||
}
|
||||
}
|
||||
|
@ -72,23 +96,49 @@ func (m model) Init() tea.Cmd {
|
|||
return tea.Batch(
|
||||
m.spinner.Tick,
|
||||
commandStart(m.command),
|
||||
timeout.Init(m.timeout, nil),
|
||||
)
|
||||
}
|
||||
func (m model) View() string {
|
||||
if m.align == "left" {
|
||||
return m.spinner.View() + " " + m.title
|
||||
if m.quitting && m.showOutput {
|
||||
return strings.TrimPrefix(errbuf.String()+"\n"+outbuf.String(), "\n")
|
||||
}
|
||||
|
||||
return m.title + " " + m.spinner.View()
|
||||
var str string
|
||||
if m.hasTimeout {
|
||||
str = timeout.Str(m.timeout)
|
||||
}
|
||||
var header string
|
||||
if m.align == "left" {
|
||||
header = m.spinner.View() + str + " " + m.title
|
||||
} else {
|
||||
header = str + " " + m.title + " " + m.spinner.View()
|
||||
}
|
||||
if !m.showOutput {
|
||||
return header
|
||||
}
|
||||
return header + errbuf.String() + "\n" + outbuf.String()
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
switch msg := msg.(type) {
|
||||
case timeout.TickTimeoutMsg:
|
||||
if msg.TimeoutValue <= 0 {
|
||||
// grab current output before closing for piped instances
|
||||
m.stdout = outbuf.String()
|
||||
|
||||
m.status = exit.StatusAborted
|
||||
return m, tea.Quit
|
||||
}
|
||||
m.timeout = msg.TimeoutValue
|
||||
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
|
||||
case finishCommandMsg:
|
||||
m.stdout = msg.stdout
|
||||
m.stderr = msg.stderr
|
||||
m.output = msg.output
|
||||
m.status = msg.status
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
|
|
|
@ -2,8 +2,8 @@ package style
|
|||
|
||||
import "github.com/charmbracelet/lipgloss"
|
||||
|
||||
// border maps strings to `lipgloss.Border`s.
|
||||
var border map[string]lipgloss.Border = map[string]lipgloss.Border{
|
||||
// Border maps strings to `lipgloss.Border`s.
|
||||
var Border map[string]lipgloss.Border = map[string]lipgloss.Border{
|
||||
"double": lipgloss.DoubleBorder(),
|
||||
"hidden": lipgloss.HiddenBorder(),
|
||||
"none": {},
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/charmbracelet/gum/internal/stdin"
|
||||
)
|
||||
|
||||
|
@ -25,24 +24,7 @@ func (o Options) Run() error {
|
|||
if text == "" {
|
||||
return errors.New("no input provided, see `gum style --help`")
|
||||
}
|
||||
text = strings.TrimSuffix(text, "\n")
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,28 @@ func (s Styles) ToLipgloss() lipgloss.Style {
|
|||
BorderBackground(lipgloss.Color(s.BorderBackground)).
|
||||
BorderForeground(lipgloss.Color(s.BorderForeground)).
|
||||
Align(decode.Align[s.Align]).
|
||||
Border(border[s.Border]).
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)).
|
||||
|
|
|
@ -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,10 +5,10 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
ltable "github.com/charmbracelet/lipgloss/table"
|
||||
|
||||
"github.com/charmbracelet/gum/internal/stdin"
|
||||
"github.com/charmbracelet/gum/style"
|
||||
|
@ -56,10 +56,10 @@ 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 := runewidth.StringWidth(title)
|
||||
width := lipgloss.Width(title)
|
||||
if len(o.Widths) > i {
|
||||
width = o.Widths[i]
|
||||
}
|
||||
|
@ -77,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")
|
||||
|
@ -85,6 +85,23 @@ func (o Options) Run() error {
|
|||
rows = append(rows, table.Row(row))
|
||||
}
|
||||
|
||||
if o.Print {
|
||||
table := ltable.New().
|
||||
Headers(columnNames...).
|
||||
Rows(data...).
|
||||
BorderStyle(o.BorderStyle.ToLipgloss()).
|
||||
Border(style.Border[o.Border]).
|
||||
StyleFunc(func(row, _ int) lipgloss.Style {
|
||||
if row == 0 {
|
||||
return styles.Header
|
||||
}
|
||||
return styles.Cell
|
||||
})
|
||||
|
||||
fmt.Println(table.Render())
|
||||
return nil
|
||||
}
|
||||
|
||||
table := table.New(
|
||||
table.WithColumns(columns),
|
||||
table.WithFocused(true),
|
||||
|
@ -94,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)
|
||||
}
|
||||
|
@ -113,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
|
||||
}
|
||||
|
|
|
@ -4,12 +4,16 @@ import "github.com/charmbracelet/gum/style"
|
|||
|
||||
// Options is the customization options for the table command.
|
||||
type Options struct {
|
||||
Separator string `short:"s" help:"Row separator" default:","`
|
||||
Columns []string `short:"c" help:"Column names"`
|
||||
Widths []int `short:"w" help:"Column widths"`
|
||||
Height int `help:"Table height" default:"10"`
|
||||
Separator string `short:"s" help:"Row separator" default:","`
|
||||
Columns []string `short:"c" help:"Column names"`
|
||||
Widths []int `short:"w" help:"Column widths"`
|
||||
Height int `help:"Table height" default:"10"`
|
||||
Print bool `short:"p" help:"static print" default:"false"`
|
||||
File string `short:"f" help:"file path" default:""`
|
||||
Border string `short:"b" help:"border style" default:"rounded" enum:"rounded,thick,normal,hidden,double,none"`
|
||||
|
||||
BorderStyle style.Styles `embed:"" prefix:"border." envprefix:"GUM_TABLE_BORDER_"`
|
||||
CellStyle style.Styles `embed:"" prefix:"cell." envprefix:"GUM_TABLE_CELL_"`
|
||||
HeaderStyle style.Styles `embed:"" prefix:"header." envprefix:"GUM_TABLE_HEADER_"`
|
||||
SelectedStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" envprefix:"GUM_TABLE_SELECTED_"`
|
||||
File string `short:"f" help:"file path" default:""`
|
||||
}
|
||||
|
|
55
timeout/options.go
Normal file
55
timeout/options.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package timeout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// Tick interval.
|
||||
const tickInterval = time.Second
|
||||
|
||||
// TickTimeoutMsg will be dispatched for every tick.
|
||||
// Containing current timeout value
|
||||
// and optional parameter to be used when handling the timeout msg.
|
||||
type TickTimeoutMsg struct {
|
||||
TimeoutValue time.Duration
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
// Init Start Timeout ticker using with timeout in seconds and optional data.
|
||||
func Init(timeout time.Duration, data interface{}) tea.Cmd {
|
||||
if timeout > 0 {
|
||||
return Tick(timeout, data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start ticker.
|
||||
func Tick(timeoutValue time.Duration, data interface{}) tea.Cmd {
|
||||
return tea.Tick(tickInterval, func(time.Time) tea.Msg {
|
||||
// every tick checks if the timeout needs to be decremented
|
||||
// and send as message
|
||||
if timeoutValue >= 0 {
|
||||
timeoutValue -= tickInterval
|
||||
return TickTimeoutMsg{
|
||||
TimeoutValue: timeoutValue,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Str produce Timeout String to be rendered.
|
||||
func Str(timeout time.Duration) string {
|
||||
return fmt.Sprintf(" (%d)", max(0, int(timeout.Seconds())))
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
|
@ -2,16 +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/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.
|
||||
|
@ -22,52 +16,39 @@ 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(),
|
||||
}
|
||||
keymap := huh.NewDefaultKeyMap()
|
||||
keymap.Text.NewLine.SetHelp("ctrl+j", "new line")
|
||||
|
||||
a.BlurredStyle = style
|
||||
a.FocusedStyle = style
|
||||
a.Cursor.Style = o.CursorStyle.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).
|
||||
WithTheme(theme).
|
||||
WithKeyMap(keymap).
|
||||
WithShowHelp(o.ShowHelp).
|
||||
Run()
|
||||
|
||||
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(),
|
||||
}, 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
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import "github.com/charmbracelet/gum/style"
|
|||
|
||||
// Options are the customization options for the textarea.
|
||||
type Options struct {
|
||||
Width int `help:"Text area width" default:"50" env:"GUM_WRITE_WIDTH"`
|
||||
Width int `help:"Text area width (0 for terminal width)" default:"50" env:"GUM_WRITE_WIDTH"`
|
||||
Height int `help:"Text area height" default:"5" env:"GUM_WRITE_HEIGHT"`
|
||||
Header string `help:"Header value" default:"" env:"GUM_WRITE_HEADER"`
|
||||
Placeholder string `help:"Placeholder value" default:"Write something..." env:"GUM_WRITE_PLACEHOLDER"`
|
||||
|
@ -13,14 +13,17 @@ type Options struct {
|
|||
ShowLineNumbers bool `help:"Show line numbers" default:"false" env:"GUM_WRITE_SHOW_LINE_NUMBERS"`
|
||||
Value string `help:"Initial value (can be passed via stdin)" default:"" env:"GUM_WRITE_VALUE"`
|
||||
CharLimit int `help:"Maximum value length (0 for no limit)" default:"400"`
|
||||
ShowHelp bool `help:"Show help key binds" negatable:"" default:"true" env:"GUM_WRITE_SHOW_HELP"`
|
||||
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_"`
|
||||
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_"`
|
||||
|
||||
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_"`
|
||||
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,57 +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 {
|
||||
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.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "esc":
|
||||
m.aborted = true
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
case "ctrl+d":
|
||||
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