Compare commits

...

48 commits

Author SHA1 Message Date
dependabot[bot] 4222e59c25
feat(deps): bump github.com/charmbracelet/bubbletea (#560) 2024-05-03 01:14:56 -04:00
Zimo Li ed0b62f7e9
feat(pager): use home/end to go to top/bottom (#548) 2024-04-30 12:40:26 -04:00
dependabot[bot] 7ad8d1b37b
chore(deps): bump golangci/golangci-lint-action from 4 to 5 (#546)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 4 to 5.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-26 17:35:37 -04:00
camcui a0f96abea4
Replace custom strings with constants from the standard library (#537) 2024-04-26 11:40:18 -04:00
bashbunni a4f52465e7
fix(docs): include winget install 2024-04-22 12:44:33 -07:00
Code_Zealot 4bdcb2bc0c
Update example in comment to latest pager syntax (#542) 2024-04-18 07:23:40 -04:00
Maas Lalani 1a0111eaff
docs: update readme 2024-04-05 04:20:49 -04:00
Maas Lalani f75dfa668f
docs: add new gifs (#533)
* docs: add new gifs

* Update README.md

* docs: spin.gif

* docs: add spin.gif to readme

* fix: lint

* don't commit filter.tape
2024-04-05 04:16:25 -04:00
Maas Lalani 2a35019323
docs: rework examples section 2024-04-05 02:38:26 -04:00
Maas Lalani 9ab722ca4f
chore: new gifs (JetBrains Mono) 2024-04-05 02:32:16 -04:00
Maas Lalani 42f59ed330
Update README.md (#531) 2024-04-04 02:26:40 -04:00
Maas Lalani 1705593eb9
Clean up README.md 2024-04-04 02:17:07 -04:00
Maas Lalani 4d5d53169e
feat: huh gum write (#525) 2024-03-29 07:19:03 -04:00
Maas Lalani 2f0ea96504
fix(input): width 2024-03-28 16:38:24 -04:00
Maas Lalani 589be38936
fix: textinput stdin read 2024-03-28 16:36:14 -04:00
Maas Lalani 4a560b1953
feat: huh for gum input (#524) 2024-03-28 16:29:08 -04:00
Maas Lalani 3a717104a9
feat: huh file picker (#523)
Use `huh` for `gum file`.
2024-03-28 16:19:06 -04:00
Maas Lalani f7572e387e
Use Huh for Gum Confirm (#522)
* feat: gum confirm with huh

Use `huh` for `gum confirm`.

* fix: lint
2024-03-28 14:41:06 -04:00
Maas Lalani 44906e23b9
Use Huh for Gum Choose (#521)
* feat: use huh for gum choose (select + multiselect)

Uses Select for gum choose and MultiSelect for gum choose --no-limit.

* chore: bump to 1.21
2024-03-28 14:22:03 -04:00
Maas Lalani 598ee57330
fix: lint 2024-03-28 13:21:51 -04:00
dependabot[bot] 4cc4611a34
feat(deps): bump github.com/charmbracelet/glamour (#517)
Bumps [github.com/charmbracelet/glamour](https://github.com/charmbracelet/glamour) from 0.6.1-0.20230531150759-6d5b52861a9d to 0.7.0.
- [Release notes](https://github.com/charmbracelet/glamour/releases)
- [Commits](https://github.com/charmbracelet/glamour/commits/v0.7.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/glamour
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-28 13:12:31 -04:00
Kevin de9f6b0397
docs(readme): add missing documentation (#513)
add documentation surrounding how to select multiple items in a list and returning them
2024-03-28 13:12:19 -04:00
Jelle Besseling f4d198396f
feat(spin): Add support for --show-error for the spinner. (rebase #440) (#518)
* feat(spin): Add support for `--show-error` for the spinner.

This makes it so that if the `--show-error` flag is provided then the
full output of the command will be printed if the command fails. This
kind of works in conjuncture with `--show-output` in that if the command
succeeds only STDOUT is pushed. If the command fails both `STDOUT` and
`STDERR` are pushed.

This builds off of https://github.com/charmbracelet/gum/pull/371

Resolves #55

* chore: Fix formatting

---------

Co-authored-by: Elliot Courant <me@elliotcourant.dev>
2024-03-28 13:11:07 -04:00
dependabot[bot] 2f2fa3bf00
feat(deps): bump github.com/charmbracelet/lipgloss from 0.9.1 to 0.10.0 (#508)
Bumps [github.com/charmbracelet/lipgloss](https://github.com/charmbracelet/lipgloss) from 0.9.1 to 0.10.0.
- [Release notes](https://github.com/charmbracelet/lipgloss/releases)
- [Commits](https://github.com/charmbracelet/lipgloss/compare/v0.9.1...v0.10.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/lipgloss
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-06 11:19:47 -05:00
Carlos Alexandro Becker 396ddf86df
build: rename scoop to charm-gum (#504)
gum conflicts with another tool, even if it is in our own bucket, this
still makes it difficult to install.

the official scoop for gum is also called charm-gum, so I think this
should work better.

refs https://github.com/charmbracelet/scoop-bucket/pull/7
2024-03-01 17:36:15 -03:00
Carlos Alexandro Becker 5951e0612f
fix: lint issues, linter config (#505) 2024-03-01 08:32:02 -05:00
dependabot[bot] 491042b25f
feat(deps): bump github.com/sahilm/fuzzy (#503)
Bumps [github.com/sahilm/fuzzy](https://github.com/sahilm/fuzzy) from 0.1.1-0.20230530133925-c48e322e2a8f to 0.1.1.
- [Release notes](https://github.com/sahilm/fuzzy/releases)
- [Commits](https://github.com/sahilm/fuzzy/commits/v0.1.1)

---
updated-dependencies:
- dependency-name: github.com/sahilm/fuzzy
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-01 10:12:13 -03:00
dependabot[bot] 7ccd488d42
feat(deps): bump github.com/charmbracelet/bubbles from 0.17.1 to 0.18.0 (#489)
Bumps [github.com/charmbracelet/bubbles](https://github.com/charmbracelet/bubbles) from 0.17.1 to 0.18.0.
- [Release notes](https://github.com/charmbracelet/bubbles/releases)
- [Commits](https://github.com/charmbracelet/bubbles/compare/v0.17.1...v0.18.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbles
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-28 21:28:38 -05:00
dependabot[bot] 6255eaeb02
chore(deps): bump golangci/golangci-lint-action from 3 to 4 (#496)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3 to 4.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-28 21:28:17 -05:00
Maas Lalani e4c4002496
Create CODEOWNERS 2024-02-28 20:58:01 -05:00
Maas Lalani 7caf7d44ff fix(format): force ansi256 for template formatting 2024-01-13 23:12:03 -05:00
Maas Lalani 2d896f777e feat(filter): allow customizing placeholder 2024-01-13 15:33:58 -05:00
Maas Lalani 7e5b494ae4 feat(input): allow placeholder style customization 2024-01-13 15:33:58 -05:00
Kevin Ernst cd115c44e9
Add example and help for gum log --time option to README (#472)
* docs(log): add help for `--time` option

The `gum log --help` output for `--time` option says

```
-t, --time=""             The time format to use (kitchen, layout, ansic, rfc822, etc...)
```

with no indication of what `etc...` means. This is probably inferred for proficient Go programmers, but not for everyone else.

This commit makes it clearer which options are supported by `--time` by linking to the docs for the `time` library,

* Update README.md

* Update README.md

---------

Co-authored-by: Maas Lalani <maas@lalani.dev>
2024-01-12 14:31:18 -05:00
dependabot[bot] 3a37defc82 feat(deps): bump github.com/charmbracelet/bubbles from 0.16.1 to 0.17.1
Bumps [github.com/charmbracelet/bubbles](https://github.com/charmbracelet/bubbles) from 0.16.1 to 0.17.1.
- [Release notes](https://github.com/charmbracelet/bubbles/releases)
- [Commits](https://github.com/charmbracelet/bubbles/compare/v0.16.1...v0.17.1)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbles
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-12 14:08:46 -05:00
Maas Lalani 6a275b423f
feat(spin): stdout streaming (#467) 2023-12-21 15:09:00 -05:00
Rose Thatcher 4a00db207a
Spin output can still be piped if timeout occurs (#461) 2023-12-13 13:54:14 -05:00
dependabot[bot] 7b16e873c7
feat(deps): bump github.com/charmbracelet/bubbletea (#465)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 0.24.2 to 0.25.0.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v0.24.2...v0.25.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbletea
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-13 13:53:14 -05:00
Maas Lalani 4d75f110a7
fix(spinner): hide spinner when done 2023-12-13 12:26:10 -05:00
snan a11d1ff648
fix: Make --select-if-one print to stdout (#463)
For some reason it wasn't printing to stdout (and I could repro that
bug even on versions before I added the newline). It was only showing
up on other streams in the shell (error stream probably), not getting
sent into pipes.

I changed it to fmt.Println.

As for the ansi-stripping that was in `filter`, LMK if that's what you
prefer and I'll add it to `choose` too. I just wanted them to match.
2023-12-10 13:52:11 -05:00
snan d1145b4163
Add newline printing to --select-if-one (#459)
* Add newline printing to --select-if-one

This matches how choose works normally when there are more than
one option.

* Add newline printing to filter --select-if-one

To match how it works without --select-if-one.
2023-12-07 10:29:40 -05:00
dependabot[bot] c9afacc74b
chore(deps): bump actions/setup-go from 4 to 5 (#460)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-07 08:54:33 -03:00
Rose Thatcher 5c65944c66
(fix): ShowOutput flag displays in realtime (#405) 2023-11-29 16:54:57 -05:00
Maas Lalani 32c9d20692
fix(lint): groupName is unused 2023-11-29 11:38:27 -05:00
Maas Lalani 76582446ec
fix: add error printing back 2023-11-29 11:00:43 -05:00
Maas Lalani 01a66511a1
Hide Style Flags consistently (#457)
* refactor: hide style flags on error to not clutter usage

* docs(style): add comment regarding dynamically hiding flags
2023-11-28 14:17:57 -05:00
Kenny Parnell fb6849ca16
Add --select-if-one flag to choose/filter. (#398)
* Add `--select-if-one` flag to `choose`/`filter`.

* Remove accidental commit of other changes.

* fix: use o.Options

---------

Co-authored-by: Maas Lalani <maas@lalani.dev>
2023-11-28 12:34:50 -05:00
dependabot[bot] c5aa973625
feat(deps): bump github.com/charmbracelet/log from 0.3.0 to 0.3.1 (#456)
Bumps [github.com/charmbracelet/log](https://github.com/charmbracelet/log) from 0.3.0 to 0.3.1.
- [Release notes](https://github.com/charmbracelet/log/releases)
- [Commits](https://github.com/charmbracelet/log/compare/v0.3.0...v0.3.1)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/log
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-28 09:28:30 -03:00
58 changed files with 856 additions and 1237 deletions

1
.github/CODEOWNERS vendored Normal file
View file

@ -0,0 +1 @@
* @maaslalani

View file

@ -12,9 +12,9 @@ jobs:
GO111MODULE: "on"
steps:
- name: Install Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: ~1.19
go-version: ~1.21
- name: Checkout code
uses: actions/checkout@v4

View file

@ -14,13 +14,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: ^1
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v5
with:
# Optional: golangci-lint command line arguments.
args: --config .golangci-soft.yml --issues-exit-code=0

View file

@ -14,13 +14,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: ^1
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v5
with:
# Optional: golangci-lint command line arguments.
#args:

View file

@ -23,7 +23,7 @@ linters:
- gomnd
- gomoddirectives
- goprintffuncname
- ifshort
# - ifshort
# - lll
- misspell
- nakedret
@ -43,5 +43,4 @@ linters:
- staticcheck
- structcheck
- typecheck
- unused
- varcheck

View file

@ -4,6 +4,7 @@ includes:
variables:
main: "."
scoop_name: charm-gum
description: "A tool for glamorous shell scripts"
github_url: "https://github.com/charmbracelet/gum"
maintainer: "Maas Lalani <maas@charm.sh>"

283
README.md
View file

@ -14,7 +14,7 @@ A tool for glamorous shell scripts. Leverage the power of
Gloss](https://github.com/charmbracelet/lipgloss) in your scripts and aliases
without writing any Go code!
<img alt="Shell running the ./demo.sh script" width="600" src="https://stuff.charm.sh/gum/demo.gif">
<img alt="Shell running the ./demo.sh script" width="600" src="https://vhs.charm.sh/vhs-1qY57RrQlXCuydsEgDp68G.gif">
The above example is running from a single shell script ([source](./examples/demo.sh)).
@ -22,70 +22,36 @@ The above example is running from a single shell script ([source](./examples/dem
Gum provides highly configurable, ready-to-use utilities to help you write
useful shell scripts and dotfiles aliases with just a few lines of code.
Let's build a simple script to help you write [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary)
for your dotfiles.
Let's build a simple script to help you write [Conventional
Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) for your
dotfiles.
Start with a `#!/bin/sh`.
```bash
#!/bin/sh
```
Ask for the commit type with `gum choose`:
Ask for the commit type with gum choose:
```bash
gum choose "fix" "feat" "docs" "style" "refactor" "test" "chore" "revert"
```
> Tip: this command itself will print to `stdout` which is not all that useful.
To make use of the command later on you can save the stdout to a `$VARIABLE` or
`file.txt`.
Prompt for an (optional) scope for the commit:
> [!NOTE]
> This command itself will print to stdout which is not all that useful. To make use of the command later on you can save the stdout to a `$VARIABLE` or `file.txt`.
Prompt for the scope of these changes:
```bash
gum input --placeholder "scope"
```
Prompt for a commit message:
Prompt for the summary and description of changes:
```bash
gum input --placeholder "Summary of this change"
gum input --value "$TYPE$SCOPE: " --placeholder "Summary of this change"
gum write --placeholder "Details of this change"
```
Prompt for a detailed (multi-line) explanation of the changes:
```bash
gum write --placeholder "Details of this change (CTRL+D to finish)"
```
Prompt for a confirmation before committing:
> `gum confirm` exits with status `0` if confirmed and status `1` if cancelled.
Confirm before committing:
```bash
gum confirm "Commit changes?" && git commit -m "$SUMMARY" -m "$DESCRIPTION"
```
Putting it all together...
Check out the [complete example](https://github.com/charmbracelet/gum/blob/main/examples/commit.sh) for combining these commands in a single script.
```bash
#!/bin/sh
TYPE=$(gum choose "fix" "feat" "docs" "style" "refactor" "test" "chore" "revert")
SCOPE=$(gum input --placeholder "scope")
# Since the scope is optional, wrap it in parentheses if it has a value.
test -n "$SCOPE" && SCOPE="($SCOPE)"
# Pre-populate the input with the type(scope): so that the user may change it
SUMMARY=$(gum input --value "$TYPE$SCOPE: " --placeholder "Summary of this change")
DESCRIPTION=$(gum write --placeholder "Details of this change (CTRL+D to finish)")
# Commit these changes
gum confirm "Commit changes?" && git commit -m "$SUMMARY" -m "$DESCRIPTION"
```
<img alt="Running the ./examples/commit.sh script to commit to git" width="600" src="https://stuff.charm.sh/gum/commit_2.gif">
<img alt="Running the ./examples/commit.sh script to commit to git" width="600" src="https://vhs.charm.sh/vhs-7rRq3LsEuJVwhwr0xf6Er7.gif">
## Installation
@ -100,29 +66,6 @@ pacman -S gum
# Nix
nix-env -iA nixpkgs.gum
# Or, with flakes
nix run "github:charmbracelet/gum" -- --help
# Debian/Ubuntu
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg
echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list
sudo apt update && sudo apt install gum
# Fedora/RHEL
echo '[charm]
name=Charm
baseurl=https://repo.charm.sh/yum/
enabled=1
gpgcheck=1
gpgkey=https://repo.charm.sh/yum/gpg.key' | sudo tee /etc/yum.repos.d/charm.repo
sudo yum install gum
# Alpine
apk add gum
# Android (via termux)
pkg install gum
# Windows (via WinGet or Scoop)
winget install charmbracelet.gum
@ -142,25 +85,40 @@ go install github.com/charmbracelet/gum@latest
[releases]: https://github.com/charmbracelet/gum/releases
## Commands
* [`choose`](#choose): Choose an option from a list of choices
* [`confirm`](#confirm): Ask a user to confirm an action
* [`file`](#file): Pick a file from a folder
* [`filter`](#filter): Filter items from a list
* [`format`](#format): Format a string using a template
* [`input`](#input): Prompt for some input
* [`join`](#join): Join text vertically or horizontally
* [`pager`](#pager): Scroll through a file
* [`spin`](#spin): Display spinner while running a command
* [`style`](#style): Apply coloring, borders, spacing to text
* [`table`](#table): Render a table of data
* [`write`](#write): Prompt for long-form text
* [`log`](#log): Log messages to output
## Customization
`gum` is designed to be embedded in scripts and supports all sorts of use
cases. Components are configurable and customizable to fit your theme and
use case.
You can customize with `--flags`. See `gum <command> --help` for a full view of
each command's customization and configuration options.
For example, let's use an `input` and change the cursor color, prompt color,
prompt indicator, placeholder text, width, and pre-populate the value:
You can customize `gum` options and styles with `--flags` and `$ENVIRONMENT_VARIABLES`.
See `gum <command> --help` for a full view of each command's customization and configuration options.
Customize with `--flags`:
```bash
gum input --cursor.foreground "#FF0" --prompt.foreground "#0FF" --prompt "* " \
--placeholder "What's up?" --width 80 --value "Not much, hby?"
gum input --cursor.foreground "#FF0" \
--prompt.foreground "#0FF" \
--placeholder "What's up?" \
--prompt "* " \
--width 80 \
--value "Not much, hby?"
```
You can also use `ENVIRONMENT_VARIABLES` to customize `gum` by default, this is
useful to keep a consistent theme for all your `gum` commands.
Customize with `ENVIRONMENT_VARIABLES`:
```bash
export GUM_INPUT_CURSOR_FOREGROUND="#FF0"
@ -169,70 +127,54 @@ export GUM_INPUT_PLACEHOLDER="What's up?"
export GUM_INPUT_PROMPT="* "
export GUM_INPUT_WIDTH=80
# Uses values configured through environment variables above but can still be
# overridden with flags.
# --flags can override values set with environment
gum input
```
<img alt="Gum input displaying most customization options" width="600" src="https://stuff.charm.sh/gum/customization.gif">
<img alt="Gum input displaying most customization options" width="600" src="https://vhs.charm.sh/vhs-5zb9DlQYA70aL9ZpYLTwKv.gif">
## Interaction
#### Input
## Input
Prompt for input with a simple command.
```bash
gum input > answer.txt
```
Prompt for sensitive input with the `--password` flag.
```bash
gum input --password > password.txt
```
<img src="https://stuff.charm.sh/gum/input_1.gif" width="600" alt="Shell running gum input typing Not much, you?" />
<img src="https://vhs.charm.sh/vhs-1nScrStFI3BMlCp5yrLtyg.gif" width="600" alt="Shell running gum input typing Not much, you?" />
#### Write
## Write
Prompt for some multi-line text.
Note: `CTRL+D` is used to complete text entry. `CTRL+C` and `esc` will cancel.
Prompt for some multi-line text (`ctrl+d` to complete text entry).
```bash
gum write > story.txt
```
<img src="https://stuff.charm.sh/gum/write.gif" width="600" alt="Shell running gum write typing a story" />
<img src="https://vhs.charm.sh/vhs-7abdKKrUEukgx9aJj8O5GX.gif" width="600" alt="Shell running gum write typing a story" />
#### Filter
## Filter
Use fuzzy matching to filter a list of values:
Filter a list of values with fuzzy matching:
```bash
echo Strawberry >> flavors.txt
echo Banana >> flavors.txt
echo Cherry >> flavors.txt
cat flavors.txt | gum filter > selection.txt
gum filter < flavors.txt > selection.txt
```
<img src="https://stuff.charm.sh/gum/filter.gif" width="600" alt="Shell running gum filter on different bubble gum flavors" />
<img src="https://vhs.charm.sh/vhs-61euOQtKPtQVD7nDpHQhzr.gif" width="600" alt="Shell running gum filter on different bubble gum flavors" />
You can also select multiple items with the `--limit` flag, which determines
the maximum number of items that can be chosen.
Select multiple options with the `--limit` flag or `--no-limit` flag. Use `tab` or `ctrl+space` to select, `enter` to confirm.
```bash
cat flavors.txt | gum filter --limit 2
```
Or, allow any number of selections with the `--no-limit` flag.
```bash
cat flavors.txt | gum filter --no-limit
```
#### Choose
## Choose
Choose an option from a list of choices.
@ -242,24 +184,17 @@ CARD=$(gum choose --height 15 {{A,K,Q,J},{10..2}}" "{♠,♥,♣,♦})
echo "Was your card the $CARD?"
```
You can also select multiple items with the `--limit` flag, which determines
You can also select multiple items with the `--limit` or `--no-limit` flag, which determines
the maximum of items that can be chosen.
```bash
echo "Pick your top 5 songs."
cat songs.txt | gum choose --limit 5
cat foods.txt | gum choose --no-limit --header "Grocery Shopping"
```
Or, allow any number of selections with the `--no-limit` flag.
<img src="https://vhs.charm.sh/vhs-3zV1LvofA6Cbn5vBu1NHHl.gif" width="600" alt="Shell running gum choose with numbers and gum flavors" />
```bash
echo "What do you need from the grocery store?"
cat foods.txt | gum choose --no-limit
```
<img src="https://stuff.charm.sh/gum/choose.gif" width="600" alt="Shell running gum choose with numbers and gum flavors" />
#### Confirm
## Confirm
Confirm whether to perform an action. Exits with code `0` (affirmative) or `1`
(negative) depending on selection.
@ -268,9 +203,9 @@ Confirm whether to perform an action. Exits with code `0` (affirmative) or `1`
gum confirm && rm file.txt || echo "File not removed"
```
<img src="https://stuff.charm.sh/gum/confirm_2.gif" width="600" alt="Shell running gum confirm" />
<img src="https://vhs.charm.sh/vhs-3xRFvbeQ4lqGerbHY7y3q2.gif" width="600" alt="Shell running gum confirm" />
#### File
## File
Prompt the user to select a file from the file tree.
@ -278,9 +213,9 @@ Prompt the user to select a file from the file tree.
EDITOR $(gum file $HOME)
```
<img src="https://stuff.charm.sh/gum/file.gif" width="600" alt="Shell running gum file" />
<img src="https://vhs.charm.sh/vhs-2RMRqmnOPneneIgVJJ3mI1.gif" width="600" alt="Shell running gum file" />
#### Pager
## Pager
Scroll through a long document with line numbers and a fully customizable viewport.
@ -288,9 +223,9 @@ Scroll through a long document with line numbers and a fully customizable viewpo
gum pager < README.md
```
<img src="https://stuff.charm.sh/gum/pager.gif" width="600" alt="Shell running gum pager" />
<img src="https://vhs.charm.sh/vhs-3iMDpgOLmbYr0jrYEGbk7p.gif" width="600" alt="Shell running gum pager" />
#### Spin
## Spin
Display a spinner while running a script or command. The spinner will
automatically stop after the given command exits.
@ -301,11 +236,11 @@ To view or pipe the command's output, use the `--show-output` flag.
gum spin --spinner dot --title "Buying Bubble Gum..." -- sleep 5
```
<img src="https://stuff.charm.sh/gum/spin.gif" width="600" alt="Shell running gum spin while sleeping for 5 seconds" />
<img src="https://vhs.charm.sh/vhs-3YFswCmoY4o3Q7MyzWl6sS.gif" width="600" alt="Shell running gum spin while sleeping for 5 seconds" />
Available spinner types include: `line`, `dot`, `minidot`, `jump`, `pulse`, `points`, `globe`, `moon`, `monkey`, `meter`, `hamburger`.
#### Table
## Table
Select a row from some tabular data.
@ -313,11 +248,9 @@ Select a row from some tabular data.
gum table < flavors.csv | cut -d ',' -f 1
```
<img src="https://stuff.charm.sh/gum/table.gif" width="600" alt="Shell running gum table" />
<!-- <img src="https://stuff.charm.sh/gum/table.gif" width="600" alt="Shell running gum table" /> -->
## Styling
#### Style
## Style
Pretty print any string with any layout with one command.
@ -328,11 +261,9 @@ gum style \
'Bubble Gum (1¢)' 'So sweet and so fresh!'
```
<img src="https://stuff.charm.sh/gum/style.gif" width="600" alt="Bubble Gum, So sweet and so fresh!" />
<img src="https://github.com/charmbracelet/gum/assets/42545625/67468acf-b3e0-4e78-bd89-360739eb44fa" width="600" alt="Bubble Gum, So sweet and so fresh!" />
## Layout
#### Join
## Join
Combine text vertically or horizontally. Use this command with `gum style` to
build layouts and pretty output.
@ -351,7 +282,7 @@ BUBBLE_GUM=$(gum join "$BUBBLE" "$GUM")
gum join --align center --vertical "$I_LOVE" "$BUBBLE_GUM"
```
<img src="https://stuff.charm.sh/gum/join.gif" width="600" alt="I LOVE Bubble Gum written out in four boxes with double borders around them." />
<img src="https://github.com/charmbracelet/gum/assets/42545625/68f7a25d-b495-48dd-982a-cee0c8ea5786" width="600" alt="I LOVE Bubble Gum written out in four boxes with double borders around them." />
## Format
@ -378,7 +309,7 @@ For more information on template helpers, see the [Termenv
docs](https://github.com/muesli/termenv#template-helpers). For a full list of
named emojis see the [GitHub API](https://api.github.com/emojis).
<img src="https://stuff.charm.sh/gum/format.gif" width="600" alt="Running gum format for different types of formats" />
<img src="https://github.com/charmbracelet/gum/assets/42545625/5cfbb0c8-0022-460d-841b-fec37527ca66" width="300" alt="Running gum format for different types of formats" />
## Log
@ -393,116 +324,80 @@ gum log --structured --level debug "Creating file..." name file.txt
# Log some error.
gum log --structured --level error "Unable to create file." name file.txt
# ERROR Unable to create file. name=temp.txt
# Include a timestamp.
gum log --time rfc822 --level error "Unable to create file."
```
See the Go [`time` package](https://pkg.go.dev/time#pkg-constants) for acceptable `--time` formats.
See [`charmbracelet/log`](https://github.com/charmbracelet/log) for more usage.
<img src="https://vhs.charm.sh/vhs-6jupuFM0s2fXiUrBE0I1vU.gif" width="600" alt="Running gum log with debug and error levels" />
## Examples
See the [examples](./examples/) directory for more real world use cases.
How to use `gum` in your daily workflows:
#### Write a commit message
See the [examples](./examples/) directory for more real world use cases.
Prompt for input to write git commit messages with a short summary and
longer details with `gum input` and `gum write`.
Bonus points: use `gum filter` with the [Conventional Commits
Specification](https://www.conventionalcommits.org/en/v1.0.0/#summary) as a
prefix for your commit message.
* Write a commit message:
```bash
git commit -m "$(gum input --width 50 --placeholder "Summary of changes")" \
-m "$(gum write --width 80 --placeholder "Details of changes (CTRL+D to finish)")"
-m "$(gum write --width 80 --placeholder "Details of changes")"
```
#### Open files in your `$EDITOR`
By default, `gum filter` will display a list of all files (searched
recursively) through your current directory, with some sensible ignore settings
(`.git`, `node_modules`). You can use this command to easily to pick a file and
open it in your `$EDITOR`.
* Open files in your `$EDITOR`
```bash
$EDITOR $(gum filter)
```
#### Connect to a TMUX session
Pick from a running `tmux` session and attach to it. Or, if you're already in a
`tmux` session, switch sessions.
* Connect to a `tmux` session
```bash
SESSION=$(tmux list-sessions -F \#S | gum filter --placeholder "Pick session...")
tmux switch-client -t $SESSION || tmux attach -t $SESSION
```
<img src="https://stuff.charm.sh/gum/pick-tmux-session.gif" width="600" alt="Picking a tmux session with gum filter" />
#### Pick commit hash from your Git history
Filter through your git history searching for commit messages, copying the
commit hash of the commit you select.
* Pick a commit hash from `git` history
```bash
git log --oneline | gum filter | cut -d' ' -f1 # | copy
```
<img src="https://stuff.charm.sh/gum/pick-commit.gif" width="600" alt="Picking a commit with gum filter" />
#### Skate Passwords
Build a simple (encrypted) password selector with [Skate](https://github.com/charmbracelet/skate).
Save all your passwords to [Skate](https://github.com/charmbracelet/skate) with `skate set github@pass.db PASSWORD`, etc...
* Simple [`skate`](https://github.com/charmbracelet/skate) password selector.
```
skate list -k | gum filter | xargs skate get
```
<img src="https://stuff.charm.sh/gum/skate-pass.gif" width="600" alt="Selecting a skate value with gum" />
#### Choose packages to uninstall
List all packages installed by your package manager (we'll use `brew`) and
choose which packages to uninstall.
* Uninstall packages
```bash
brew list | gum choose --no-limit | xargs brew uninstall
```
#### Choose branches to delete
List all branches and choose which branches to delete.
* Clean up `git` branches
```bash
git branch | cut -c 3- | gum choose --no-limit | xargs git branch -D
```
#### Choose pull request to checkout
List all PRs for the current GitHub repository and checkout the chosen PR (using [`gh`](https://cli.github.com/)).
* Checkout GitHub pull requests with [`gh`](https://cli.github.com/)
```bash
gh pr list | cut -f1,2 | gum choose | cut -f1 | xargs gh pr checkout
```
#### Pick command from shell history
Pick a previously executed command from your shell history to execute, copy,
edit, etc...
* Copy command from shell history
```bash
gum filter < $HISTFILE --height 20
```
#### Sudo password input
See visual feedback when entering password with masked characters with `gum
input --password`.
* `sudo` replacement
```bash
alias please="gum input --password | sudo -nS"

View file

@ -1,220 +0,0 @@
// Package choose provides an interface to choose one option from a given list
// of options. The options can be provided as (new-line separated) stdin or a
// list of arguments.
//
// It is different from the filter command as it does not provide a fuzzy
// finding input, so it is best used for smaller lists of options.
//
// Let's pick from a list of gum flavors:
//
// $ gum choose "Strawberry" "Banana" "Cherry"
package choose
import (
"strings"
"time"
"github.com/charmbracelet/gum/timeout"
"github.com/charmbracelet/bubbles/paginator"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type model struct {
height int
cursor string
selectedPrefix string
unselectedPrefix string
cursorPrefix string
header string
items []item
quitting bool
index int
limit int
numSelected int
currentOrder int
paginator paginator.Model
aborted bool
// styles
cursorStyle lipgloss.Style
headerStyle lipgloss.Style
itemStyle lipgloss.Style
selectedItemStyle lipgloss.Style
hasTimeout bool
timeout time.Duration
}
type item struct {
text string
selected bool
order int
}
func (m model) Init() tea.Cmd {
return timeout.Init(m.timeout, nil)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
return m, nil
case timeout.TickTimeoutMsg:
if msg.TimeoutValue <= 0 {
m.quitting = true
// If the user hasn't selected any items in a multi-select.
// Then we select the item that they have pressed enter on. If they
// have selected items, then we simply return them.
if m.numSelected < 1 {
m.items[m.index].selected = true
}
return m, tea.Quit
}
m.timeout = msg.TimeoutValue
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
case tea.KeyMsg:
start, end := m.paginator.GetSliceBounds(len(m.items))
switch keypress := msg.String(); keypress {
case "down", "j", "ctrl+j", "ctrl+n":
m.index++
if m.index >= len(m.items) {
m.index = 0
m.paginator.Page = 0
}
if m.index >= end {
m.paginator.NextPage()
}
case "up", "k", "ctrl+k", "ctrl+p":
m.index--
if m.index < 0 {
m.index = len(m.items) - 1
m.paginator.Page = m.paginator.TotalPages - 1
}
if m.index < start {
m.paginator.PrevPage()
}
case "right", "l", "ctrl+f":
m.index = clamp(m.index+m.height, 0, len(m.items)-1)
m.paginator.NextPage()
case "left", "h", "ctrl+b":
m.index = clamp(m.index-m.height, 0, len(m.items)-1)
m.paginator.PrevPage()
case "G", "end":
m.index = len(m.items) - 1
m.paginator.Page = m.paginator.TotalPages - 1
case "g", "home":
m.index = 0
m.paginator.Page = 0
case "a":
if m.limit <= 1 {
break
}
for i := range m.items {
if m.numSelected >= m.limit {
break // do not exceed given limit
}
if m.items[i].selected {
continue
}
m.items[i].selected = true
m.items[i].order = m.currentOrder
m.numSelected++
m.currentOrder++
}
case "A":
if m.limit <= 1 {
break
}
for i := range m.items {
m.items[i].selected = false
m.items[i].order = 0
}
m.numSelected = 0
m.currentOrder = 0
case "ctrl+c", "esc":
m.aborted = true
m.quitting = true
return m, tea.Quit
case " ", "tab", "x", "ctrl+@":
if m.limit == 1 {
break // no op
}
if m.items[m.index].selected {
m.items[m.index].selected = false
m.numSelected--
} else if m.numSelected < m.limit {
m.items[m.index].selected = true
m.items[m.index].order = m.currentOrder
m.numSelected++
m.currentOrder++
}
case "enter":
m.quitting = true
if m.limit <= 1 && m.numSelected < 1 {
m.items[m.index].selected = true
}
return m, tea.Quit
}
}
var cmd tea.Cmd
m.paginator, cmd = m.paginator.Update(msg)
return m, cmd
}
func (m model) View() string {
if m.quitting {
return ""
}
var s strings.Builder
var timeoutStr string
start, end := m.paginator.GetSliceBounds(len(m.items))
for i, item := range m.items[start:end] {
if i == m.index%m.height {
s.WriteString(m.cursorStyle.Render(m.cursor))
} else {
s.WriteString(strings.Repeat(" ", lipgloss.Width(m.cursor)))
}
if item.selected {
if m.hasTimeout {
timeoutStr = timeout.Str(m.timeout)
}
s.WriteString(m.selectedItemStyle.Render(m.selectedPrefix + item.text + timeoutStr))
} else if i == m.index%m.height {
s.WriteString(m.cursorStyle.Render(m.cursorPrefix + item.text))
} else {
s.WriteString(m.itemStyle.Render(m.unselectedPrefix + item.text))
}
if i != m.height {
s.WriteRune('\n')
}
}
if m.paginator.TotalPages > 1 {
s.WriteString(strings.Repeat("\n", m.height-m.paginator.ItemsOnPage(len(m.items))+1))
s.WriteString(" " + m.paginator.View())
}
if m.header != "" {
header := m.headerStyle.Render(m.header)
return lipgloss.JoinVertical(lipgloss.Left, header, s.String())
}
return s.String()
}
//nolint:unparam
func clamp(x, min, max int) int {
if x < min {
return min
}
if x > max {
return max
}
return x
}

View file

@ -4,30 +4,20 @@ import (
"errors"
"fmt"
"os"
"sort"
"strings"
"github.com/alecthomas/kong"
"github.com/charmbracelet/bubbles/paginator"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"github.com/mattn/go-isatty"
"github.com/charmbracelet/gum/ansi"
"github.com/charmbracelet/gum/internal/exit"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/style"
)
var (
subduedStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#847A85", Dark: "#979797"})
verySubduedStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"})
)
// Run provides a shell script interface for choosing between different through
// options.
func (o Options) Run() error {
if len(o.Options) == 0 {
if len(o.Options) <= 0 {
input, _ := stdin.Read()
if input == "" {
return errors.New("no options provided, see `gum choose --help`")
@ -35,133 +25,106 @@ func (o Options) Run() error {
o.Options = strings.Split(strings.TrimSuffix(input, "\n"), "\n")
}
// We don't need to display prefixes if we are only picking one option.
// Simply displaying the cursor is enough.
if o.Limit == 1 && !o.NoLimit {
o.SelectedPrefix = ""
o.UnselectedPrefix = ""
o.CursorPrefix = ""
if o.SelectIfOne && len(o.Options) == 1 {
fmt.Println(o.Options[0])
return nil
}
// If we've set no limit then we can simply select as many options as there
// are so let's set the limit to the number of options.
if o.NoLimit {
o.Limit = len(o.Options) + 1
}
theme := huh.ThemeCharm()
options := huh.NewOptions(o.Options...)
if len(o.Selected) > o.Limit {
return errors.New("number of selected options cannot be greater than the limit")
}
theme.Focused.Base.Border(lipgloss.Border{})
theme.Focused.Title = o.HeaderStyle.ToLipgloss()
theme.Focused.SelectSelector = o.CursorStyle.ToLipgloss().SetString(o.Cursor)
theme.Focused.MultiSelectSelector = o.CursorStyle.ToLipgloss().SetString(o.Cursor)
theme.Focused.SelectedOption = o.SelectedItemStyle.ToLipgloss()
theme.Focused.UnselectedOption = o.ItemStyle.ToLipgloss()
theme.Focused.SelectedPrefix = o.SelectedItemStyle.ToLipgloss().SetString(o.SelectedPrefix)
theme.Focused.UnselectedPrefix = o.ItemStyle.ToLipgloss().SetString(o.UnselectedPrefix)
// Keep track of the selected items.
currentSelected := 0
// Check if selected items should be used.
hasSelectedItems := len(o.Selected) > 0
startingIndex := 0
currentOrder := 0
items := make([]item, len(o.Options))
for i, option := range o.Options {
var order int
// Check if the option should be selected.
isSelected := hasSelectedItems && currentSelected < o.Limit && arrayContains(o.Selected, option)
// If the option is selected then increment the current selected count.
if isSelected {
if o.Limit == 1 {
// When the user can choose only one option don't select the option but
// start with the cursor hovering over it.
startingIndex = i
isSelected = false
} else {
currentSelected++
order = currentOrder
currentOrder++
for _, s := range o.Selected {
for i, opt := range options {
if s == opt.Key || s == opt.Value {
options[i] = opt.Selected(true)
}
}
items[i] = item{text: option, selected: isSelected, order: order}
}
// Use the pagination model to display the current and total number of
// pages.
pager := paginator.New()
pager.SetTotalPages((len(items) + o.Height - 1) / o.Height)
pager.PerPage = o.Height
pager.Type = paginator.Dots
pager.ActiveDot = subduedStyle.Render("•")
pager.InactiveDot = verySubduedStyle.Render("•")
pager.KeyMap = paginator.KeyMap{}
pager.Page = startingIndex / o.Height
if o.NoLimit {
o.Limit = len(o.Options)
}
// Disable Keybindings since we will control it ourselves.
tm, err := tea.NewProgram(model{
index: startingIndex,
currentOrder: currentOrder,
height: o.Height,
cursor: o.Cursor,
header: o.Header,
selectedPrefix: o.SelectedPrefix,
unselectedPrefix: o.UnselectedPrefix,
cursorPrefix: o.CursorPrefix,
items: items,
limit: o.Limit,
paginator: pager,
cursorStyle: o.CursorStyle.ToLipgloss(),
headerStyle: o.HeaderStyle.ToLipgloss(),
itemStyle: o.ItemStyle.ToLipgloss(),
selectedItemStyle: o.SelectedItemStyle.ToLipgloss(),
numSelected: currentSelected,
hasTimeout: o.Timeout > 0,
timeout: o.Timeout,
}, tea.WithOutput(os.Stderr)).Run()
width := max(widest(o.Options)+
max(lipgloss.Width(o.SelectedPrefix), lipgloss.Width(o.UnselectedPrefix))+
lipgloss.Width(o.Cursor)+1, lipgloss.Width(o.Header)+1)
if o.Limit > 1 {
var choices []string
err := huh.NewForm(
huh.NewGroup(
huh.NewMultiSelect[string]().
Options(options...).
Title(o.Header).
Filterable(false).
Height(o.Height).
Limit(o.Limit).
Value(&choices),
),
).
WithWidth(width).
WithShowHelp(false).
WithTheme(theme).
Run()
if err != nil {
return err
}
if len(choices) > 0 {
s := strings.Join(choices, "\n")
if isatty.IsTerminal(os.Stdout.Fd()) {
fmt.Println(s)
} else {
fmt.Print(ansi.Strip(s))
}
}
return nil
}
var choice string
err := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Options(options...).
Title(o.Header).
Height(o.Height).
Value(&choice),
),
).
WithWidth(width).
WithTheme(theme).
WithShowHelp(false).
Run()
if err != nil {
return fmt.Errorf("failed to start tea program: %w", err)
}
m := tm.(model)
if m.aborted {
return exit.ErrAborted
}
if o.Ordered && o.Limit > 1 {
sort.Slice(m.items, func(i, j int) bool {
return m.items[i].order < m.items[j].order
})
}
var s strings.Builder
for _, item := range m.items {
if item.selected {
s.WriteString(item.text)
s.WriteRune('\n')
}
return err
}
if isatty.IsTerminal(os.Stdout.Fd()) {
fmt.Print(s.String())
fmt.Println(choice)
} else {
fmt.Print(ansi.Strip(s.String()))
fmt.Print(ansi.Strip(choice))
}
return nil
}
// BeforeReset hook. Used to unclutter style flags.
func (o Options) BeforeReset(ctx *kong.Context) error {
style.HideFlags(ctx)
return nil
}
// Check if an array contains a value.
func arrayContains(strArray []string, value string) bool {
for _, str := range strArray {
if str == value {
return true
func widest(options []string) int {
var max int
for _, o := range options {
w := lipgloss.Width(o)
if w > max {
max = w
}
}
return false
return max
}

View file

@ -15,12 +15,13 @@ type Options struct {
Height int `help:"Height of the list" default:"10" env:"GUM_CHOOSE_HEIGHT"`
Cursor string `help:"Prefix to show on item that corresponds to the cursor position" default:"> " env:"GUM_CHOOSE_CURSOR"`
Header string `help:"Header value" default:"" env:"GUM_CHOOSE_HEADER"`
CursorPrefix string `help:"Prefix to show on the cursor item (hidden if limit is 1)" default:" " env:"GUM_CHOOSE_CURSOR_PREFIX"`
SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:" " env:"GUM_CHOOSE_SELECTED_PREFIX"`
UnselectedPrefix string `help:"Prefix to show on unselected items (hidden if limit is 1)" default:" " env:"GUM_CHOOSE_UNSELECTED_PREFIX"`
CursorPrefix string `help:"Prefix to show on the cursor item (hidden if limit is 1)" default:" " env:"GUM_CHOOSE_CURSOR_PREFIX"`
SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:" " env:"GUM_CHOOSE_SELECTED_PREFIX"`
UnselectedPrefix string `help:"Prefix to show on unselected items (hidden if limit is 1)" default:" " env:"GUM_CHOOSE_UNSELECTED_PREFIX"`
Selected []string `help:"Options that should start as selected" default:"" env:"GUM_CHOOSE_SELECTED"`
SelectIfOne bool `help:"Select the given option if there is only one" group:"Selection"`
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_CURSOR_"`
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_CHOOSE_HEADER_"`
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=99" envprefix:"GUM_CHOOSE_HEADER_"`
ItemStyle style.Styles `embed:"" prefix:"item." hidden:"" envprefix:"GUM_CHOOSE_ITEM_"`
SelectedItemStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_SELECTED_"`
Timeout time.Duration `help:"Timeout until choose returns selected element" default:"0" env:"GUM_CCHOOSE_TIMEOUT"` // including timeout command options [Timeout,...]

View file

@ -4,47 +4,41 @@ import (
"fmt"
"os"
"github.com/charmbracelet/gum/internal/exit"
"github.com/alecthomas/kong"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/style"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
)
// Run provides a shell script interface for prompting a user to confirm an
// action with an affirmative or negative answer.
func (o Options) Run() error {
m, err := tea.NewProgram(model{
affirmative: o.Affirmative,
negative: o.Negative,
confirmation: o.Default,
defaultSelection: o.Default,
timeout: o.Timeout,
hasTimeout: o.Timeout > 0,
prompt: o.Prompt,
selectedStyle: o.SelectedStyle.ToLipgloss(),
unselectedStyle: o.UnselectedStyle.ToLipgloss(),
promptStyle: o.PromptStyle.ToLipgloss(),
}, tea.WithOutput(os.Stderr)).Run()
theme := huh.ThemeCharm()
theme.Focused.Base = lipgloss.NewStyle().Margin(0, 1)
theme.Focused.Title = o.PromptStyle.ToLipgloss()
theme.Focused.FocusedButton = o.SelectedStyle.ToLipgloss()
theme.Focused.BlurredButton = o.UnselectedStyle.ToLipgloss()
choice := o.Default
err := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Affirmative(o.Affirmative).
Negative(o.Negative).
Title(o.Prompt).
Value(&choice),
),
).
WithTheme(theme).
WithShowHelp(false).
Run()
if err != nil {
return fmt.Errorf("unable to run confirm: %w", err)
}
if m.(model).aborted {
os.Exit(exit.StatusAborted)
} else if m.(model).confirmation {
os.Exit(0)
if !choice {
os.Exit(1)
}
os.Exit(1)
return nil
}
// BeforeReset hook. Used to unclutter style flags.
func (o Options) BeforeReset(ctx *kong.Context) error {
style.HideFlags(ctx)
return nil
}

View file

@ -1,121 +0,0 @@
// Package confirm provides an interface to ask a user to confirm an action.
// The user is provided with an interface to choose an affirmative or negative
// answer, which is then reflected in the exit code for use in scripting.
//
// If the user selects the affirmative answer, the program exits with 0. If the
// user selects the negative answer, the program exits with 1.
//
// I.e. confirm if the user wants to delete a file
//
// $ gum confirm "Are you sure?" && rm file.txt
package confirm
import (
"time"
"github.com/charmbracelet/gum/timeout"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type model struct {
prompt string
affirmative string
negative string
quitting bool
aborted bool
hasTimeout bool
timeout time.Duration
confirmation bool
defaultSelection bool
// styles
promptStyle lipgloss.Style
selectedStyle lipgloss.Style
unselectedStyle lipgloss.Style
}
func (m model) Init() tea.Cmd {
return timeout.Init(m.timeout, m.defaultSelection)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
return m, nil
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
m.confirmation = false
m.aborted = true
fallthrough
case "esc":
m.confirmation = false
m.quitting = true
return m, tea.Quit
case "q", "n", "N":
m.confirmation = false
m.quitting = true
return m, tea.Quit
case "left", "h", "ctrl+p", "tab",
"right", "l", "ctrl+n", "shift+tab":
if m.negative == "" {
break
}
m.confirmation = !m.confirmation
case "enter":
m.quitting = true
return m, tea.Quit
case "y", "Y":
m.quitting = true
m.confirmation = true
return m, tea.Quit
}
case timeout.TickTimeoutMsg:
if msg.TimeoutValue <= 0 {
m.quitting = true
m.confirmation = m.defaultSelection
return m, tea.Quit
}
m.timeout = msg.TimeoutValue
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
}
return m, nil
}
func (m model) View() string {
if m.quitting {
return ""
}
var aff, neg, timeoutStrYes, timeoutStrNo string
timeoutStrNo = ""
timeoutStrYes = ""
if m.hasTimeout {
if m.defaultSelection {
timeoutStrYes = timeout.Str(m.timeout)
} else {
timeoutStrNo = timeout.Str(m.timeout)
}
}
if m.confirmation {
aff = m.selectedStyle.Render(m.affirmative + timeoutStrYes)
neg = m.unselectedStyle.Render(m.negative + timeoutStrNo)
} else {
aff = m.unselectedStyle.Render(m.affirmative + timeoutStrYes)
neg = m.selectedStyle.Render(m.negative + timeoutStrNo)
}
// If the option is intentionally empty, do not show it.
if m.negative == "" {
neg = ""
}
return lipgloss.JoinVertical(lipgloss.Center, m.promptStyle.Render(m.prompt), lipgloss.JoinHorizontal(lipgloss.Left, aff, neg))
}

View file

@ -12,10 +12,10 @@ type Options struct {
Affirmative string `help:"The title of the affirmative action" default:"Yes"`
Negative string `help:"The title of the negative action" default:"No"`
Prompt string `arg:"" help:"Prompt to display." default:"Are you sure?"`
PromptStyle style.Styles `embed:"" prefix:"prompt." help:"The style of the prompt" set:"defaultMargin=1 0 0 0" envprefix:"GUM_CONFIRM_PROMPT_"`
PromptStyle style.Styles `embed:"" prefix:"prompt." help:"The style of the prompt" set:"defaultMargin=1 0 0 1" envprefix:"GUM_CONFIRM_PROMPT_"`
//nolint:staticcheck
SelectedStyle style.Styles `embed:"" prefix:"selected." help:"The style of the selected action" set:"defaultBackground=212" set:"defaultForeground=230" set:"defaultPadding=0 3" set:"defaultMargin=1 1" envprefix:"GUM_CONFIRM_SELECTED_"`
SelectedStyle style.Styles `embed:"" prefix:"selected." help:"The style of the selected action" set:"defaultBackground=212" set:"defaultForeground=230" set:"defaultPadding=0 3" set:"defaultMargin=0 1" envprefix:"GUM_CONFIRM_SELECTED_"`
//nolint:staticcheck
UnselectedStyle style.Styles `embed:"" prefix:"unselected." help:"The style of the unselected action" set:"defaultBackground=235" set:"defaultForeground=254" set:"defaultPadding=0 3" set:"defaultMargin=1 1" envprefix:"GUM_CONFIRM_UNSELECTED_"`
UnselectedStyle style.Styles `embed:"" prefix:"unselected." help:"The style of the unselected action" set:"defaultBackground=235" set:"defaultForeground=254" set:"defaultPadding=0 3" set:"defaultMargin=0 1" envprefix:"GUM_CONFIRM_UNSELECTED_"`
Timeout time.Duration `help:"Timeout until confirm returns selected value or default if provided" default:"0" env:"GUM_CONFIRM_TIMEOUT"`
}

2
examples/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.gif
*.png

36
examples/README.md Normal file
View 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
View 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

View file

@ -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
View 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
View 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
View 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

View file

@ -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
View 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
View file

@ -0,0 +1 @@
Banana

15
examples/file.tape Normal file
View 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
View file

@ -0,0 +1,4 @@
Banana
Cherry
Orange
Strawberry

12
examples/format.ansi Normal file
View file

@ -0,0 +1,12 @@
> gum format -t code < main.go
   package main
   
   import "fmt"
   
   func main() {
    fmt.Println("Charm_™ Gum")
   }
   


16
examples/input.tape Normal file
View file

@ -0,0 +1,16 @@
Output input.gif
Set Width 800
Set Height 250
Set Shell bash
Sleep 1s
Type `gum input --placeholder "What's up?"`
Sleep 1s
Enter
Sleep 1s
Type "Not much, you?"
Sleep 1s
Enter
Sleep 1s

7
examples/main.go Normal file
View file

@ -0,0 +1,7 @@
package main
import "fmt"
func main() {
fmt.Println("Charm_™ Gum")
}

15
examples/pager.tape Normal file
View 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
View 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
View file

@ -0,0 +1,2 @@
Once upon a time
In a land far, far away....

21
examples/write.tape Normal file
View 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

View file

@ -3,15 +3,10 @@ package file
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/charmbracelet/gum/internal/exit"
"github.com/alecthomas/kong"
"github.com/charmbracelet/bubbles/filepicker"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/style"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
)
// Run is the interface to picking a file.
@ -29,52 +24,36 @@ func (o Options) Run() error {
return fmt.Errorf("file not found: %w", err)
}
fp := filepicker.New()
fp.CurrentDirectory = path
fp.Path = path
fp.Height = o.Height
fp.AutoHeight = o.Height == 0
fp.Cursor = o.Cursor
fp.DirAllowed = o.Directory
fp.FileAllowed = o.File
fp.ShowHidden = o.All
fp.Styles = filepicker.DefaultStyles()
fp.Styles.Cursor = o.CursorStyle.ToLipgloss()
fp.Styles.Symlink = o.SymlinkStyle.ToLipgloss()
fp.Styles.Directory = o.DirectoryStyle.ToLipgloss()
fp.Styles.File = o.FileStyle.ToLipgloss()
fp.Styles.Permission = o.PermissionsStyle.ToLipgloss()
fp.Styles.Selected = o.SelectedStyle.ToLipgloss()
fp.Styles.FileSize = o.FileSizeStyle.ToLipgloss()
theme := huh.ThemeCharm()
theme.Focused.Base = lipgloss.NewStyle()
theme.Focused.File = o.FileStyle.ToLipgloss()
theme.Focused.Directory = o.DirectoryStyle.ToLipgloss()
theme.Focused.SelectedOption = o.SelectedStyle.ToLipgloss()
m := model{
filepicker: fp,
timeout: o.Timeout,
hasTimeout: o.Timeout > 0,
aborted: false,
}
// XXX: These should be file selected specific.
theme.Focused.TextInput.Placeholder = o.PermissionsStyle.ToLipgloss()
theme.Focused.TextInput.Prompt = o.CursorStyle.ToLipgloss()
err = huh.NewForm(
huh.NewGroup(
huh.NewFilePicker().
Picking(true).
CurrentDirectory(path).
DirAllowed(o.Directory).
FileAllowed(o.File).
Height(o.Height).
ShowHidden(o.All).
Value(&path),
),
).
WithShowHelp(false).
WithTheme(theme).
Run()
tm, err := tea.NewProgram(&m, tea.WithOutput(os.Stderr)).Run()
if err != nil {
return fmt.Errorf("unable to pick selection: %w", err)
return err
}
m = tm.(model)
if m.aborted {
return exit.ErrAborted
}
if m.selectedPath == "" {
os.Exit(1)
}
fmt.Println(m.selectedPath)
return nil
}
// BeforeReset hook. Used to unclutter style flags.
func (o Options) BeforeReset(ctx *kong.Context) error {
style.HideFlags(ctx)
fmt.Println(path)
return nil
}

View file

@ -1,72 +0,0 @@
// Package file provides an interface to pick a file from a folder (tree).
// The user is provided a file manager-like interface to navigate, to
// select a file.
//
// Let's pick a file from the current directory:
//
// $ gum file
// $ gum file .
//
// Let's pick a file from the home directory:
//
// $ gum file $HOME
package file
import (
"time"
"github.com/charmbracelet/bubbles/filepicker"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/timeout"
)
type model struct {
filepicker filepicker.Model
selectedPath string
aborted bool
quitting bool
timeout time.Duration
hasTimeout bool
}
func (m model) Init() tea.Cmd {
return tea.Batch(
timeout.Init(m.timeout, nil),
m.filepicker.Init(),
)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q", "esc":
m.aborted = true
m.quitting = true
return m, tea.Quit
}
case timeout.TickTimeoutMsg:
if msg.TimeoutValue <= 0 {
m.quitting = true
m.aborted = true
return m, tea.Quit
}
m.timeout = msg.TimeoutValue
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
}
var cmd tea.Cmd
m.filepicker, cmd = m.filepicker.Update(msg)
if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect {
m.selectedPath = path
m.quitting = true
return m, tea.Quit
}
return m, cmd
}
func (m model) View() string {
if m.quitting {
return ""
}
return m.filepicker.View()
}

View file

@ -16,7 +16,7 @@ type Options struct {
File bool `help:"Allow files selection" default:"true" env:"GUM_FILE_FILE"`
Directory bool `help:"Allow directories selection" default:"false" env:"GUM_FILE_DIRECTORY"`
Height int `help:"Maximum number of files to display" default:"0" env:"GUM_FILE_HEIGHT"`
Height int `help:"Maximum number of files to display" default:"10" env:"GUM_FILE_HEIGHT"`
CursorStyle style.Styles `embed:"" prefix:"cursor." help:"The cursor style" set:"defaultForeground=212" envprefix:"GUM_FILE_CURSOR_"`
SymlinkStyle style.Styles `embed:"" prefix:"symlink." help:"The style to use for symlinks" set:"defaultForeground=36" envprefix:"GUM_FILE_SYMLINK_"`
DirectoryStyle style.Styles `embed:"" prefix:"directory." help:"The style to use for directories" set:"defaultForeground=99" envprefix:"GUM_FILE_DIRECTORY_"`

View file

@ -6,7 +6,6 @@ import (
"os"
"strings"
"github.com/alecthomas/kong"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
@ -17,7 +16,6 @@ import (
"github.com/charmbracelet/gum/internal/exit"
"github.com/charmbracelet/gum/internal/files"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/style"
)
// Run provides a shell script interface for filtering through options, powered
@ -28,6 +26,7 @@ func (o Options) Run() error {
i.Prompt = o.Prompt
i.PromptStyle = o.PromptStyle.ToLipgloss()
i.PlaceholderStyle = o.PlaceholderStyle.ToLipgloss()
i.Placeholder = o.Placeholder
i.Width = o.Width
@ -45,6 +44,11 @@ func (o Options) Run() error {
return errors.New("no options provided, see `gum filter --help`")
}
if o.SelectIfOne && len(o.Options) == 1 {
fmt.Println(o.Options[0])
return nil
}
options := []tea.ProgramOption{tea.WithOutput(os.Stderr)}
if o.Height == 0 {
options = append(options, tea.WithAltScreen())
@ -132,9 +136,3 @@ func (o Options) checkSelected(m model, isTTY bool) {
}
}
}
// BeforeReset hook. Used to unclutter style flags.
func (o Options) BeforeReset(ctx *kong.Context) error {
style.HideFlags(ctx)
return nil
}

View file

@ -14,6 +14,7 @@ type Options struct {
IndicatorStyle style.Styles `embed:"" prefix:"indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_INDICATOR_"`
Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"`
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
SelectIfOne bool `help:"Select the given option if there is only one" group:"Selection"`
Strict bool `help:"Only returns if anything matched. Otherwise return Filter" negatable:"true" default:"true" group:"Selection"`
SelectedPrefix string `help:"Character to indicate selected items (hidden if limit is 1)" default:" ◉ " env:"GUM_FILTER_SELECTED_PREFIX"`
SelectedPrefixStyle style.Styles `embed:"" prefix:"selected-indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_SELECTED_PREFIX_"`
@ -27,6 +28,7 @@ type Options struct {
Placeholder string `help:"Placeholder value" default:"Filter..." env:"GUM_FILTER_PLACEHOLDER"`
Prompt string `help:"Prompt to display" default:"> " env:"GUM_FILTER_PROMPT"`
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=240" envprefix:"GUM_FILTER_PROMPT_"`
PlaceholderStyle style.Styles `embed:"" prefix:"placeholder." set:"defaultForeground=240" envprefix:"GUM_FILTER_PLACEHOLDER_"`
Width int `help:"Input width" default:"20" env:"GUM_FILTER_WIDTH"`
Height int `help:"Input height" default:"0" env:"GUM_FILTER_HEIGHT"`
Value string `help:"Initial filter value" default:"" env:"GUM_FILTER_VALUE"`

View file

@ -54,7 +54,7 @@ func markdown(input string, theme string) (string, error) {
}
func template(input string) (string, error) {
f := termenv.TemplateFuncs(termenv.EnvColorProfile())
f := termenv.TemplateFuncs(termenv.ANSI256)
t, err := tpl.New("tpl").Funcs(f).Parse(input)
if err != nil {
return "", fmt.Errorf("unable to parse template: %w", err)

47
go.mod
View file

@ -1,47 +1,50 @@
module github.com/charmbracelet/gum
go 1.19
go 1.21
require (
github.com/alecthomas/kong v0.8.1
github.com/alecthomas/kong v0.9.0
github.com/alecthomas/mango-kong v0.1.0
github.com/charmbracelet/bubbles v0.16.1
github.com/charmbracelet/bubbletea v0.24.2
github.com/charmbracelet/glamour v0.6.1-0.20230531150759-6d5b52861a9d
github.com/charmbracelet/lipgloss v0.9.1
github.com/charmbracelet/log v0.3.0
github.com/charmbracelet/bubbles v0.18.0
github.com/charmbracelet/bubbletea v0.26.1
github.com/charmbracelet/glamour v0.7.0
github.com/charmbracelet/huh v0.3.1-0.20240328185852-590ecabc34b9
github.com/charmbracelet/lipgloss v0.10.0
github.com/charmbracelet/log v0.4.0
github.com/mattn/go-isatty v0.0.20
github.com/muesli/reflow v0.3.0
github.com/muesli/roff v0.1.0
github.com/muesli/termenv v0.15.2
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f
github.com/sahilm/fuzzy v0.1.1
)
require (
github.com/alecthomas/chroma/v2 v2.7.0 // indirect
github.com/alecthomas/chroma/v2 v2.13.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/dlclark/regexp2 v1.8.1 // indirect
github.com/catppuccin/go v0.2.0 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240328150354-ab9afc214dfd // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/microcosm-cc/bluemonday v1.0.23 // indirect
github.com/microcosm-cc/bluemonday v1.0.26 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/mango v0.2.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/yuin/goldmark v1.5.4 // indirect
github.com/yuin/goldmark-emoji v1.0.1 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/yuin/goldmark v1.7.0 // indirect
github.com/yuin/goldmark-emoji v1.0.2 // indirect
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/term v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
)

112
go.sum
View file

@ -1,40 +1,55 @@
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
github.com/alecthomas/chroma/v2 v2.7.0 h1:hm1rY6c/Ob4eGclpQ7X/A3yhqBOZNUTk9q+yhyLIViI=
github.com/alecthomas/chroma/v2 v2.7.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw=
github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY=
github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI=
github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk=
github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA=
github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os=
github.com/alecthomas/mango-kong v0.1.0 h1:iFVfP1k1K4qpml3JUQmD5I8MCQYfIvsD9mRdrw7jJC4=
github.com/alecthomas/mango-kong v0.1.0/go.mod h1:t+TYVdsONUolf/BwVcm+15eqcdAj15h4Qe9MMFAwwT4=
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY=
github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc=
github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY=
github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg=
github.com/charmbracelet/glamour v0.6.1-0.20230531150759-6d5b52861a9d h1:S4Ejl/M2VrryIgDrDbiuvkwMUDa67/t/H3Wz3i2/vUw=
github.com/charmbracelet/glamour v0.6.1-0.20230531150759-6d5b52861a9d/go.mod h1:swCB3CXFsh22H1ESDYdY1tirLiNqCziulDyJ1B6Nt7Q=
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
github.com/charmbracelet/log v0.3.0 h1:u5aB2KJDgNZo4WOfOC8C+KvGIkJ2rCFNlPWDu6xhnqI=
github.com/charmbracelet/log v0.3.0/go.mod h1:OR4E1hutLsax3ZKpXbgUqPtTjQfrh1pG3zwHGWuuq8g=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
github.com/charmbracelet/bubbletea v0.26.1 h1:xujcQeF73rh4jwu3+zhfQsvV18x+7zIjlw7/CYbzGJ0=
github.com/charmbracelet/bubbletea v0.26.1/go.mod h1:FzKr7sKoO8iFVcdIBM9J0sJOcQv5nDQaYwsee3kpbgo=
github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng=
github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps=
github.com/charmbracelet/huh v0.3.1-0.20240328185852-590ecabc34b9 h1:Izr4MC+shs9PpR4MWz/OFA4+ywbKutvPv0eSHJwfn60=
github.com/charmbracelet/huh v0.3.1-0.20240328185852-590ecabc34b9/go.mod h1:x0rYoA1kpsaefXhRJZuxLM+qP4CYyEFE67T3ZGl7zPU=
github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/charmbracelet/x/exp/strings v0.0.0-20240328150354-ab9afc214dfd h1:yTFoT3v/wDWzeoRXt9mIKlslAKfVNr0XdVCOVwRK8ck=
github.com/charmbracelet/x/exp/strings v0.0.0-20240328150354-ab9afc214dfd/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/exp/term v0.0.0-20240321133156-7faadd06c281 h1:ZYwrF0GAd859tU6oF63T2pIkZVQ4z9BosDVD7jYu93A=
github.com/charmbracelet/x/exp/term v0.0.0-20240321133156-7faadd06c281/go.mod h1:madZtB2OVDOG+ZnLruGITVZceYy047W+BLQ1MNQzbWg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@ -45,8 +60,8 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY=
github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
@ -62,30 +77,33 @@ github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1n
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -4,65 +4,59 @@ import (
"fmt"
"os"
"github.com/alecthomas/kong"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/gum/cursor"
"github.com/charmbracelet/gum/internal/exit"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/style"
)
// Run provides a shell script interface for the text input bubble.
// https://github.com/charmbracelet/bubbles/textinput
func (o Options) Run() error {
i := textinput.New()
var value string
if o.Value != "" {
i.SetValue(o.Value)
value = o.Value
} else if in, _ := stdin.Read(); in != "" {
i.SetValue(in)
value = in
}
i.Focus()
i.Prompt = o.Prompt
i.Placeholder = o.Placeholder
i.Width = o.Width
i.PromptStyle = o.PromptStyle.ToLipgloss()
i.Cursor.Style = o.CursorStyle.ToLipgloss()
i.Cursor.SetMode(cursor.Modes[o.CursorMode])
i.CharLimit = o.CharLimit
theme := huh.ThemeCharm()
theme.Focused.Base = lipgloss.NewStyle()
// theme.Focused.TextInput.Cursor = o.CursorStyle.ToLipgloss()
theme.Focused.TextInput.Placeholder = o.PlaceholderStyle.ToLipgloss()
theme.Focused.TextInput.Prompt = o.PromptStyle.ToLipgloss()
theme.Focused.Title = o.HeaderStyle.ToLipgloss()
var echoMode huh.EchoMode
if o.Password {
i.EchoMode = textinput.EchoPassword
i.EchoCharacter = '•'
echoMode = huh.EchoModePassword
} else {
echoMode = huh.EchoModeNormal
}
p := tea.NewProgram(model{
textinput: i,
aborted: false,
header: o.Header,
headerStyle: o.HeaderStyle.ToLipgloss(),
timeout: o.Timeout,
hasTimeout: o.Timeout > 0,
autoWidth: o.Width < 1,
}, tea.WithOutput(os.Stderr))
tm, err := p.Run()
err := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Prompt(o.Prompt).
Placeholder(o.Placeholder).
CharLimit(o.CharLimit).
EchoMode(echoMode).
Title(o.Header).
Value(&value),
),
).
WithShowHelp(false).
WithWidth(o.Width).
WithTheme(theme).
WithProgramOptions(tea.WithOutput(os.Stderr)).
Run()
if err != nil {
return fmt.Errorf("failed to run input: %w", err)
}
m := tm.(model)
if m.aborted {
return exit.ErrAborted
return err
}
fmt.Println(m.textinput.Value())
return nil
}
// BeforeReset hook. Used to unclutter style flags.
func (o Options) BeforeReset(ctx *kong.Context) error {
style.HideFlags(ctx)
fmt.Println(value)
return nil
}

View file

@ -1,77 +0,0 @@
// Package input provides a shell script interface for the text input bubble.
// https://github.com/charmbracelet/bubbles/tree/master/textinput
//
// It can be used to prompt the user for some input. The text the user entered
// will be sent to stdout.
//
// $ gum input --placeholder "What's your favorite gum?" > answer.text
package input
import (
"time"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/timeout"
"github.com/charmbracelet/lipgloss"
)
type model struct {
autoWidth bool
header string
headerStyle lipgloss.Style
textinput textinput.Model
quitting bool
aborted bool
timeout time.Duration
hasTimeout bool
}
func (m model) Init() tea.Cmd {
return tea.Batch(
textinput.Blink,
timeout.Init(m.timeout, nil),
)
}
func (m model) View() string {
if m.quitting {
return ""
}
if m.header != "" {
header := m.headerStyle.Render(m.header)
return lipgloss.JoinVertical(lipgloss.Left, header, m.textinput.View())
}
return m.textinput.View()
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case timeout.TickTimeoutMsg:
if msg.TimeoutValue <= 0 {
m.quitting = true
m.aborted = true
return m, tea.Quit
}
m.timeout = msg.TimeoutValue
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
case tea.WindowSizeMsg:
if m.autoWidth {
m.textinput.Width = msg.Width - lipgloss.Width(m.textinput.Prompt) - 1
}
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc":
m.quitting = true
m.aborted = true
return m, tea.Quit
case "enter":
m.quitting = true
return m, tea.Quit
}
}
var cmd tea.Cmd
m.textinput, cmd = m.textinput.Update(msg)
return m, cmd
}

View file

@ -8,16 +8,17 @@ import (
// Options are the customization options for the input.
type Options struct {
Placeholder string `help:"Placeholder value" default:"Type something..." env:"GUM_INPUT_PLACEHOLDER"`
Prompt string `help:"Prompt to display" default:"> " env:"GUM_INPUT_PROMPT"`
PromptStyle style.Styles `embed:"" prefix:"prompt." envprefix:"GUM_INPUT_PROMPT_"`
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_INPUT_CURSOR_"`
CursorMode string `prefix:"cursor." name:"mode" help:"Cursor mode" default:"blink" enum:"blink,hide,static" env:"GUM_INPUT_CURSOR_MODE"`
Value string `help:"Initial value (can also be passed via stdin)" default:""`
CharLimit int `help:"Maximum value length (0 for no limit)" default:"400"`
Width int `help:"Input width (0 for terminal width)" default:"40" env:"GUM_INPUT_WIDTH"`
Password bool `help:"Mask input characters" default:"false"`
Header string `help:"Header value" default:"" env:"GUM_INPUT_HEADER"`
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_INPUT_HEADER_"`
Timeout time.Duration `help:"Timeout until input aborts" default:"0" env:"GUM_INPUT_TIMEOUT"`
Placeholder string `help:"Placeholder value" default:"Type something..." env:"GUM_INPUT_PLACEHOLDER"`
Prompt string `help:"Prompt to display" default:"> " env:"GUM_INPUT_PROMPT"`
PromptStyle style.Styles `embed:"" prefix:"prompt." envprefix:"GUM_INPUT_PROMPT_"`
PlaceholderStyle style.Styles `embed:"" prefix:"placeholder." set:"defaultForeground=240" envprefix:"GUM_INPUT_PLACEHOLDER_"`
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_INPUT_CURSOR_"`
CursorMode string `prefix:"cursor." name:"mode" help:"Cursor mode" default:"blink" enum:"blink,hide,static" env:"GUM_INPUT_CURSOR_MODE"`
Value string `help:"Initial value (can also be passed via stdin)" default:""`
CharLimit int `help:"Maximum value length (0 for no limit)" default:"400"`
Width int `help:"Input width (0 for terminal width)" default:"40" env:"GUM_INPUT_WIDTH"`
Password bool `help:"Mask input characters" default:"false"`
Header string `help:"Header value" default:"" env:"GUM_INPUT_HEADER"`
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_INPUT_HEADER_"`
Timeout time.Duration `help:"Timeout until input aborts" default:"0" env:"GUM_INPUT_TIMEOUT"`
}

View file

@ -46,9 +46,9 @@ func (o Options) Run() error {
"stampmilli": time.StampMilli,
"stampmicro": time.StampMicro,
"stampnano": time.StampNano,
"datetime": "2006-01-02 15:04:05",
"dateonly": "2006-01-02",
"timeonly": "15:04:05",
"datetime": time.DateTime,
"dateonly": time.DateOnly,
"timeonly": time.TimeOnly,
}
tf, ok := timeFormats[strings.ToLower(o.Time)]

View file

@ -1,7 +1,6 @@
package log
import (
"github.com/alecthomas/kong"
"github.com/charmbracelet/gum/style"
)
@ -25,9 +24,3 @@ type Options struct {
ValueStyle style.Styles `embed:"" prefix:"value." help:"The style of the value" envprefix:"GUM_LOG_VALUE_"`
SeparatorStyle style.Styles `embed:"" prefix:"separator." help:"The style of the separator" set:"defaultFaint=true" envprefix:"GUM_LOG_SEPARATOR_"`
}
// BeforeReset hook. Used to unclutter style flags.
func (o Options) BeforeReset(ctx *kong.Context) error {
style.HideFlags(ctx)
return nil
}

View file

@ -28,7 +28,7 @@ var (
var bubbleGumPink = lipgloss.NewStyle().Foreground(lipgloss.Color("212"))
func main() {
lipgloss.SetColorProfile(termenv.NewOutput(os.Stderr).EnvColorProfile())
lipgloss.SetColorProfile(termenv.NewOutput(os.Stderr).Profile)
if Version == "" {
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" {
@ -48,8 +48,9 @@ func main() {
kong.Description(fmt.Sprintf("A tool for %s shell scripts.", bubbleGumPink.Render("glamorous"))),
kong.UsageOnError(),
kong.ConfigureHelp(kong.HelpOptions{
Compact: true,
Summary: false,
Compact: true,
Summary: false,
NoExpandSubcommands: true,
}),
kong.Vars{
"version": version,

View file

@ -4,11 +4,9 @@ import (
"fmt"
"regexp"
"github.com/alecthomas/kong"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/style"
)
// Run provides a shell script interface for the viewport bubble.
@ -50,9 +48,3 @@ func (o Options) Run() error {
}
return nil
}
// BeforeReset hook. Used to unclutter style flags.
func (o Options) BeforeReset(ctx *kong.Context) error {
style.HideFlags(ctx)
return nil
}

View file

@ -1,6 +1,6 @@
// Package pager provides a pager (similar to less) for the terminal.
//
// $ cat file.txt | gum page
// $ cat file.txt | gum pager
package pager
import (
@ -120,9 +120,9 @@ func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) {
}
} else {
switch key.String() {
case "g":
case "g", "home":
m.viewport.GotoTop()
case "G":
case "G", "end":
m.viewport.GotoBottom()
case "/":
m.search.Begin()

View file

@ -4,13 +4,11 @@ import (
"fmt"
"os"
"github.com/alecthomas/kong"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/mattn/go-isatty"
"github.com/charmbracelet/gum/internal/exit"
"github.com/charmbracelet/gum/style"
)
// Run provides a shell script interface for the spinner bubble.
@ -26,6 +24,8 @@ func (o Options) Run() error {
title: o.TitleStyle.ToLipgloss().Render(o.Title),
command: o.Command,
align: o.Align,
showOutput: o.ShowOutput && isTTY,
showError: o.ShowError,
timeout: o.Timeout,
hasTimeout: o.Timeout > 0,
}
@ -41,25 +41,28 @@ func (o Options) Run() error {
return exit.ErrAborted
}
if err != nil {
return fmt.Errorf("failed to access stdout: %w", err)
}
if o.ShowOutput {
if isTTY {
_, err := os.Stdout.WriteString(m.stdout)
if err != nil {
return fmt.Errorf("failed to write to stdout: %w", err)
// If the command succeeds, and we are printing output and we are in a TTY then push the STDOUT we got to the actual
// STDOUT for piping or other things.
if m.status == 0 {
if o.ShowOutput {
// BubbleTea writes the View() to stderr.
// If the program is being piped then put the accumulated output in stdout.
if !isTTY {
_, err := os.Stdout.WriteString(m.stdout)
if err != nil {
return fmt.Errorf("failed to write to stdout: %w", err)
}
}
}
} else if o.ShowError {
// Otherwise if we are showing errors and the command did not exit with a 0 status code then push all of the command
// output to the terminal. This way failed commands can be debugged.
_, err := os.Stdout.WriteString(m.output)
if err != nil {
return fmt.Errorf("failed to write to stdout: %w", err)
}
}
os.Exit(m.status)
return nil
}
// BeforeReset hook. Used to unclutter style flags.
func (o Options) BeforeReset(ctx *kong.Context) error {
style.HideFlags(ctx)
return nil
}

View file

@ -11,6 +11,7 @@ type Options struct {
Command []string `arg:"" help:"Command to run"`
ShowOutput bool `help:"Show or pipe output of command during execution" default:"false" env:"GUM_SPIN_SHOW_OUTPUT"`
ShowError bool `help:"Show output of command only if the command fails" default:"false" env:"GUM_SPIN_SHOW_ERROR"`
Spinner string `help:"Spinner type" short:"s" type:"spinner" enum:"line,dot,minidot,jump,pulse,points,globe,moon,monkey,meter,hamburger" default:"dot" env:"GUM_SPIN_SPINNER"`
SpinnerStyle style.Styles `embed:"" prefix:"spinner." set:"defaultForeground=212" envprefix:"GUM_SPIN_SPINNER_"`
Title string `help:"Text to display to user while spinning" default:"Loading..." env:"GUM_SPIN_TITLE"`

View file

@ -15,12 +15,15 @@
package spin
import (
"io"
"os"
"os/exec"
"strings"
"time"
"github.com/charmbracelet/gum/internal/exit"
"github.com/charmbracelet/gum/timeout"
"github.com/mattn/go-isatty"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
@ -31,19 +34,26 @@ type model struct {
title string
align string
command []string
quitting bool
aborted bool
status int
stdout string
stderr string
output string
showOutput bool
showError bool
timeout time.Duration
hasTimeout bool
}
var bothbuf strings.Builder
var outbuf strings.Builder
var errbuf strings.Builder
type finishCommandMsg struct {
stdout string
stderr string
output string
status int
}
@ -55,8 +65,15 @@ func commandStart(command []string) tea.Cmd {
}
cmd := exec.Command(command[0], args...) //nolint:gosec
cmd.Stdout = &outbuf
cmd.Stderr = &errbuf
if isatty.IsTerminal(os.Stdout.Fd()) {
stdout := io.MultiWriter(&bothbuf, &errbuf)
stderr := io.MultiWriter(&bothbuf, &outbuf)
cmd.Stdout = stdout
cmd.Stderr = stderr
} else {
cmd.Stdout = os.Stdout
}
_ = cmd.Run()
@ -68,6 +85,8 @@ func commandStart(command []string) tea.Cmd {
return finishCommandMsg{
stdout: outbuf.String(),
stderr: errbuf.String(),
output: bothbuf.String(),
status: status,
}
}
@ -81,6 +100,10 @@ func (m model) Init() tea.Cmd {
)
}
func (m model) View() string {
if m.quitting && m.showOutput {
return strings.TrimPrefix(errbuf.String()+"\n"+outbuf.String(), "\n")
}
var str string
if m.hasTimeout {
str = timeout.Str(m.timeout)
@ -102,6 +125,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case timeout.TickTimeoutMsg:
if msg.TimeoutValue <= 0 {
// grab current output before closing for piped instances
m.stdout = outbuf.String()
m.status = exit.StatusAborted
return m, tea.Quit
}
@ -109,7 +135,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
case finishCommandMsg:
m.stdout = msg.stdout
m.stderr = msg.stderr
m.output = msg.output
m.status = msg.status
m.quitting = true
return m, tea.Quit
case tea.KeyMsg:
switch msg.String() {

View file

@ -10,7 +10,6 @@ import (
"fmt"
"strings"
"github.com/alecthomas/kong"
"github.com/charmbracelet/gum/internal/stdin"
)
@ -30,19 +29,3 @@ func (o Options) Run() error {
fmt.Println(o.Style.ToLipgloss().Render(text))
return nil
}
// HideFlags hides the flags from the usage output. This is used in conjunction
// with BeforeReset hook.
func HideFlags(ctx *kong.Context) {
n := ctx.Selected()
if n == nil {
return
}
for _, f := range n.Flags {
if g := f.Group; g != nil && g.Key == groupName {
if !strings.HasSuffix(f.Name, ".foreground") {
f.Hidden = true
}
}
}
}

View file

@ -26,3 +26,24 @@ func (s Styles) ToLipgloss() lipgloss.Style {
Strikethrough(s.Strikethrough).
Underline(s.Underline)
}
// ToLipgloss takes a Styles flag set and returns the corresponding
// lipgloss.Style.
func (s StylesNotHidden) ToLipgloss() lipgloss.Style {
return lipgloss.NewStyle().
Background(lipgloss.Color(s.Background)).
Foreground(lipgloss.Color(s.Foreground)).
BorderBackground(lipgloss.Color(s.BorderBackground)).
BorderForeground(lipgloss.Color(s.BorderForeground)).
Align(decode.Align[s.Align]).
Border(Border[s.Border]).
Height(s.Height).
Width(s.Width).
Margin(parseMargin(s.Margin)).
Padding(parsePadding(s.Padding)).
Bold(s.Bold).
Faint(s.Faint).
Italic(s.Italic).
Strikethrough(s.Strikethrough).
Underline(s.Underline)
}

View file

@ -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"`

View file

@ -5,7 +5,6 @@ import (
"fmt"
"os"
"github.com/alecthomas/kong"
"github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
@ -57,7 +56,7 @@ func (o Options) Run() error {
if err != nil {
return fmt.Errorf("invalid data provided")
}
var columns = make([]table.Column, 0, len(columnNames))
columns := make([]table.Column, 0, len(columnNames))
for i, title := range columnNames {
width := lipgloss.Width(title)
@ -78,7 +77,7 @@ func (o Options) Run() error {
Selected: o.SelectedStyle.ToLipgloss(),
}
var rows = make([]table.Row, 0, len(data))
rows := make([]table.Row, 0, len(data))
for _, row := range data {
if len(row) > len(columns) {
return fmt.Errorf("invalid number of columns")
@ -92,7 +91,7 @@ func (o Options) Run() error {
Rows(data...).
BorderStyle(o.BorderStyle.ToLipgloss()).
Border(style.Border[o.Border]).
StyleFunc(func(row, col int) lipgloss.Style {
StyleFunc(func(row, _ int) lipgloss.Style {
if row == 0 {
return styles.Header
}
@ -112,7 +111,6 @@ func (o Options) Run() error {
)
tm, err := tea.NewProgram(model{table: table}, tea.WithOutput(os.Stderr)).Run()
if err != nil {
return fmt.Errorf("failed to start tea program: %w", err)
}
@ -131,9 +129,3 @@ func (o Options) Run() error {
return nil
}
// BeforeReset hook. Used to unclutter style flags.
func (o Options) BeforeReset(ctx *kong.Context) error {
style.HideFlags(ctx)
return nil
}

View file

@ -2,17 +2,10 @@ package write
import (
"fmt"
"os"
"strings"
"github.com/alecthomas/kong"
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/cursor"
"github.com/charmbracelet/gum/internal/exit"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/style"
"github.com/charmbracelet/huh"
)
// Run provides a shell script interface for the text area bubble.
@ -23,54 +16,33 @@ func (o Options) Run() error {
o.Value = strings.ReplaceAll(in, "\r", "")
}
a := textarea.New()
a.Focus()
var value = o.Value
a.Prompt = o.Prompt
a.Placeholder = o.Placeholder
a.ShowLineNumbers = o.ShowLineNumbers
a.CharLimit = o.CharLimit
theme := huh.ThemeCharm()
theme.Focused.Base = o.BaseStyle.ToLipgloss()
theme.Focused.TextInput.Cursor = o.CursorStyle.ToLipgloss()
theme.Focused.Title = o.HeaderStyle.ToLipgloss()
theme.Focused.TextInput.Placeholder = o.PlaceholderStyle.ToLipgloss()
theme.Focused.TextInput.Prompt = o.PromptStyle.ToLipgloss()
style := textarea.Style{
Base: o.BaseStyle.ToLipgloss(),
Placeholder: o.PlaceholderStyle.ToLipgloss(),
CursorLine: o.CursorLineStyle.ToLipgloss(),
CursorLineNumber: o.CursorLineNumberStyle.ToLipgloss(),
EndOfBuffer: o.EndOfBufferStyle.ToLipgloss(),
LineNumber: o.LineNumberStyle.ToLipgloss(),
Prompt: o.PromptStyle.ToLipgloss(),
}
err := huh.NewForm(
huh.NewGroup(
huh.NewText().
Title(o.Header).
Placeholder(o.Placeholder).
CharLimit(o.CharLimit).
ShowLineNumbers(o.ShowLineNumbers).
Value(&value),
),
).
WithWidth(o.Width).
WithHeight(o.Height).
WithShowHelp(false).Run()
a.BlurredStyle = style
a.FocusedStyle = style
a.Cursor.Style = o.CursorStyle.ToLipgloss()
a.Cursor.SetMode(cursor.Modes[o.CursorMode])
a.SetWidth(o.Width)
a.SetHeight(o.Height)
a.SetValue(o.Value)
p := tea.NewProgram(model{
textarea: a,
header: o.Header,
headerStyle: o.HeaderStyle.ToLipgloss(),
autoWidth: o.Width < 1,
}, tea.WithOutput(os.Stderr))
tm, err := p.Run()
if err != nil {
return fmt.Errorf("failed to run write: %w", err)
}
m := tm.(model)
if m.aborted {
return exit.ErrAborted
return err
}
fmt.Println(m.textarea.Value())
return nil
}
// BeforeReset hook. Used to unclutter style flags.
func (o Options) BeforeReset(ctx *kong.Context) error {
style.HideFlags(ctx)
fmt.Println(value)
return nil
}

View file

@ -15,13 +15,14 @@ type Options struct {
CharLimit int `help:"Maximum value length (0 for no limit)" default:"400"`
CursorMode string `prefix:"cursor." name:"mode" help:"Cursor mode" default:"blink" enum:"blink,hide,static" env:"GUM_WRITE_CURSOR_MODE"`
BaseStyle style.Styles `embed:"" prefix:"base." envprefix:"GUM_WRITE_BASE_"`
CursorLineNumberStyle style.Styles `embed:"" prefix:"cursor-line-number." set:"defaultForeground=7" envprefix:"GUM_WRITE_CURSOR_LINE_NUMBER_"`
CursorLineStyle style.Styles `embed:"" prefix:"cursor-line." envprefix:"GUM_WRITE_CURSOR_LINE_"`
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_WRITE_CURSOR_"`
BaseStyle style.Styles `embed:"" prefix:"base." envprefix:"GUM_WRITE_BASE_"`
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_WRITE_CURSOR_"`
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_WRITE_HEADER_"`
PlaceholderStyle style.Styles `embed:"" prefix:"placeholder." set:"defaultForeground=240" envprefix:"GUM_WRITE_PLACEHOLDER_"`
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=7" envprefix:"GUM_WRITE_PROMPT_"`
EndOfBufferStyle style.Styles `embed:"" prefix:"end-of-buffer." set:"defaultForeground=0" envprefix:"GUM_WRITE_END_OF_BUFFER_"`
LineNumberStyle style.Styles `embed:"" prefix:"line-number." set:"defaultForeground=7" envprefix:"GUM_WRITE_LINE_NUMBER_"`
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_WRITE_HEADER_"`
PlaceholderStyle style.Styles `embed:"" prefix:"placeholder." set:"defaultForeground=240" envprefix:"GUM_WRITE_PLACEHOLDER_"`
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=7" envprefix:"GUM_WRITE_PROMPT_"`
CursorLineNumberStyle style.Styles `embed:"" prefix:"cursor-line-number." set:"defaultForeground=7" envprefix:"GUM_WRITE_CURSOR_LINE_NUMBER_"`
CursorLineStyle style.Styles `embed:"" prefix:"cursor-line." envprefix:"GUM_WRITE_CURSOR_LINE_"`
}

View file

@ -1,62 +0,0 @@
// Package write provides a shell script interface for the text area bubble.
// https://github.com/charmbracelet/bubbles/tree/master/textarea
//
// It can be used to ask the user to write some long form of text (multi-line)
// input. The text the user entered will be sent to stdout.
// Text entry is completed with CTRL+D and aborted with CTRL+C or Escape.
//
// $ gum write > output.text
package write
import (
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type model struct {
autoWidth bool
aborted bool
header string
headerStyle lipgloss.Style
quitting bool
textarea textarea.Model
}
func (m model) Init() tea.Cmd { return textarea.Blink }
func (m model) View() string {
if m.quitting {
return ""
}
// Display the header above the text area if it is not empty.
if m.header != "" {
header := m.headerStyle.Render(m.header)
return lipgloss.JoinVertical(lipgloss.Left, header, m.textarea.View())
}
return m.textarea.View()
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
if m.autoWidth {
m.textarea.SetWidth(msg.Width)
}
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
m.aborted = true
m.quitting = true
return m, tea.Quit
case "ctrl+d", "esc":
m.quitting = true
return m, tea.Quit
}
}
var cmd tea.Cmd
m.textarea, cmd = m.textarea.Update(msg)
return m, cmd
}