mirror of
https://github.com/charmbracelet/gum
synced 2024-06-16 12:35:06 +02:00
Compare commits
213 commits
Author | SHA1 | Date | |
---|---|---|---|
851a48aee2 | |||
b1b02e4ecb | |||
fa2f67d1d6 | |||
baf36ae662 | |||
657c76eb9b | |||
c977aaf4ae | |||
efb70a1302 | |||
a8712df2a7 | |||
00767d209a | |||
2fe0291e02 | |||
26178f8894 | |||
68400ae7b3 | |||
2b0a4c033e | |||
4222e59c25 | |||
ed0b62f7e9 | |||
7ad8d1b37b | |||
a0f96abea4 | |||
a4f52465e7 | |||
4bdcb2bc0c | |||
1a0111eaff | |||
f75dfa668f | |||
2a35019323 | |||
9ab722ca4f | |||
42f59ed330 | |||
1705593eb9 | |||
4d5d53169e | |||
2f0ea96504 | |||
589be38936 | |||
4a560b1953 | |||
3a717104a9 | |||
f7572e387e | |||
44906e23b9 | |||
598ee57330 | |||
4cc4611a34 | |||
de9f6b0397 | |||
f4d198396f | |||
2f2fa3bf00 | |||
396ddf86df | |||
5951e0612f | |||
491042b25f | |||
7ccd488d42 | |||
6255eaeb02 | |||
e4c4002496 | |||
7caf7d44ff | |||
2d896f777e | |||
7e5b494ae4 | |||
cd115c44e9 | |||
3a37defc82 | |||
6a275b423f | |||
4a00db207a | |||
7b16e873c7 | |||
4d75f110a7 | |||
a11d1ff648 | |||
d1145b4163 | |||
c9afacc74b | |||
5c65944c66 | |||
32c9d20692 | |||
76582446ec | |||
01a66511a1 | |||
fb6849ca16 | |||
c5aa973625 | |||
dd557baf6a | |||
eb0e8afeba | |||
504a2060b2 | |||
7bae4c8fcb | |||
bf3864e231 | |||
3839b8d6e1 | |||
055aa0d791 | |||
12ef4d3085 | |||
86dbd9c70f | |||
7d51fd8b73 | |||
46328de806 | |||
8e959e4cdc | |||
1cedd4f20b | |||
89e2a0fbd5 | |||
971b6cf16f | |||
77aa8640f2 | |||
a63ea30136 | |||
a61f3bdc3f | |||
6763de12e8 | |||
4b998515fd | |||
ed52291b33 | |||
f5b09a434a | |||
d1ad453ce6 | |||
f73341a56c | |||
eef6431d7c | |||
6bf79aa899 | |||
f8caeef195 | |||
0c1cc8e669 | |||
7e71c4d664 | |||
abae6fd80c | |||
b6f739d7d1 | |||
ae1da5d329 | |||
93ffc250e7 | |||
99f1348a45 | |||
6aac40560f | |||
f048bd8d87 | |||
f1b99f0aa4 | |||
fd11b787e0 | |||
0010018d61 | |||
8f17aa3f9a | |||
3609fe1da8 | |||
3f7db714ff | |||
5a4b12c8ca | |||
a892c39289 | |||
accce59ed1 | |||
25ff33e710 | |||
4c3cc1773f | |||
6e802805cf | |||
b23ebce896 | |||
c8710071ad | |||
23c56854d3 | |||
c668e153e6 | |||
92c890e717 | |||
ece25c7789 | |||
9d2741c5f9 | |||
7f54b3b289 | |||
11584b5982 | |||
fff07286b7 | |||
39346ed015 | |||
99a1fa9d8e | |||
8da9620bfd | |||
5887a10fa0 | |||
6dbadf30b4 | |||
11f23830c5 | |||
066c79fa14 | |||
0f0f8e9189 | |||
e0bcab8608 | |||
95ddbdb416 | |||
b6daeece02 | |||
bb103c0a03 | |||
38521ff870 | |||
d9a3dc5324 | |||
97feb1b4d0 | |||
ec2b8d0fee | |||
ccc5d9cfea | |||
b0c9b56e0e | |||
1e4012ffde | |||
f46060ac43 | |||
f8adcd649b | |||
65f5a7e44e | |||
708a653eae | |||
2bda001480 | |||
1267b7a78e | |||
b5444d5f0b | |||
99e6625a39 | |||
e5cb9877cf | |||
5431540431 | |||
83db83296a | |||
46dee843db | |||
e6de7749b1 | |||
440a3dd81c | |||
877c475aa6 | |||
488138e4b5 | |||
f7d8ef5871 | |||
78bb3b5f06 | |||
80f3598efd | |||
36ef76185c | |||
08ed3e2519 | |||
832c4fc917 | |||
c8e6b4a9d5 | |||
7756c809d1 | |||
b4d7ebf2cb | |||
240e163f01 | |||
2d54d5394e | |||
e108bc4668 | |||
b8dbcc3e82 | |||
7169c0c490 | |||
eb3c5c1037 | |||
614f0e8028 | |||
b0aba2261d | |||
e38cfdaa10 | |||
7bb92dec2f | |||
e20d3a97f0 | |||
5ed1f2b1b8 | |||
f0a8011b95 | |||
b87d77554c | |||
d88f9aec55 | |||
1dec524b9a | |||
75c41866b2 | |||
57c8c90bfd | |||
7a32dd579b | |||
a1e2b3d3c3 | |||
fd58eb07fc | |||
2e4ddce3f7 | |||
5723977c68 | |||
0b500f6ec9 | |||
992cac834e | |||
844727f185 | |||
2c66222fd8 | |||
1353b97272 | |||
08c34cfa2f | |||
8dec822e75 | |||
d45b728b4d | |||
d74f126d41 | |||
98ff7656d1 | |||
b92c9ec858 | |||
dfa412fa1a | |||
b4c07eb3b9 | |||
1426c2fed9 | |||
86fa35d672 | |||
db75c218de | |||
995bd04e38 | |||
2bea4dc030 | |||
430ab459d7 | |||
bdd86d5fbc | |||
a82d5af1e8 | |||
0bd02434a3 | |||
45e930c54e | |||
f13b5b6b82 | |||
f69fc23242 | |||
a4d4793829 | |||
fecb5ffc7d |
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
* @maaslalani
|
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
|
@ -4,7 +4,6 @@ updates:
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
time: "08:00"
|
|
||||||
labels:
|
labels:
|
||||||
- "dependencies"
|
- "dependencies"
|
||||||
commit-message:
|
commit-message:
|
||||||
|
@ -14,7 +13,6 @@ updates:
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
time: "08:00"
|
|
||||||
labels:
|
labels:
|
||||||
- "dependencies"
|
- "dependencies"
|
||||||
commit-message:
|
commit-message:
|
||||||
|
|
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
|
@ -12,12 +12,12 @@ jobs:
|
||||||
GO111MODULE: "on"
|
GO111MODULE: "on"
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.17
|
go-version: ~1.21
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download Go modules
|
- name: Download Go modules
|
||||||
run: go mod download
|
run: go mod download
|
||||||
|
@ -31,4 +31,4 @@ jobs:
|
||||||
snapshot:
|
snapshot:
|
||||||
uses: charmbracelet/meta/.github/workflows/snapshot.yml@main
|
uses: charmbracelet/meta/.github/workflows/snapshot.yml@main
|
||||||
secrets:
|
secrets:
|
||||||
goreleaser_key: ${{ secrets.GORELEASER_KEY }}
|
goreleaser_key: ${{ secrets.GORELEASER_KEY }}
|
||||||
|
|
6
.github/workflows/lint-soft.yml
vendored
6
.github/workflows/lint-soft.yml
vendored
|
@ -14,13 +14,13 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1
|
go-version: ^1
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v3
|
uses: golangci/golangci-lint-action@v6
|
||||||
with:
|
with:
|
||||||
# Optional: golangci-lint command line arguments.
|
# Optional: golangci-lint command line arguments.
|
||||||
args: --config .golangci-soft.yml --issues-exit-code=0
|
args: --config .golangci-soft.yml --issues-exit-code=0
|
||||||
|
|
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
|
@ -14,13 +14,13 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1
|
go-version: ^1
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v3
|
uses: golangci/golangci-lint-action@v6
|
||||||
with:
|
with:
|
||||||
# Optional: golangci-lint command line arguments.
|
# Optional: golangci-lint command line arguments.
|
||||||
#args:
|
#args:
|
||||||
|
|
12
.github/workflows/soft-serve.yml
vendored
12
.github/workflows/soft-serve.yml
vendored
|
@ -1,12 +0,0 @@
|
||||||
name: soft-serve
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
soft-serve:
|
|
||||||
uses: charmbracelet/meta/.github/workflows/soft-serve.yml@main
|
|
||||||
secrets:
|
|
||||||
ssh-key: "${{ secrets.CHARM_SOFT_SERVE_KEY }}"
|
|
|
@ -23,7 +23,7 @@ linters:
|
||||||
- gomnd
|
- gomnd
|
||||||
- gomoddirectives
|
- gomoddirectives
|
||||||
- goprintffuncname
|
- goprintffuncname
|
||||||
- ifshort
|
# - ifshort
|
||||||
# - lll
|
# - lll
|
||||||
- misspell
|
- misspell
|
||||||
- nakedret
|
- nakedret
|
||||||
|
@ -31,10 +31,10 @@ linters:
|
||||||
- noctx
|
- noctx
|
||||||
- nolintlint
|
- nolintlint
|
||||||
- prealloc
|
- prealloc
|
||||||
- wrapcheck
|
|
||||||
|
|
||||||
# disable default linters, they are already enabled in .golangci.yml
|
# disable default linters, they are already enabled in .golangci.yml
|
||||||
disable:
|
disable:
|
||||||
|
- wrapcheck
|
||||||
- deadcode
|
- deadcode
|
||||||
- errcheck
|
- errcheck
|
||||||
- gosimple
|
- gosimple
|
||||||
|
@ -43,5 +43,4 @@ linters:
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- structcheck
|
- structcheck
|
||||||
- typecheck
|
- typecheck
|
||||||
- unused
|
|
||||||
- varcheck
|
- varcheck
|
||||||
|
|
|
@ -4,6 +4,7 @@ includes:
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
main: "."
|
main: "."
|
||||||
|
scoop_name: charm-gum
|
||||||
description: "A tool for glamorous shell scripts"
|
description: "A tool for glamorous shell scripts"
|
||||||
github_url: "https://github.com/charmbracelet/gum"
|
github_url: "https://github.com/charmbracelet/gum"
|
||||||
maintainer: "Maas Lalani <maas@charm.sh>"
|
maintainer: "Maas Lalani <maas@charm.sh>"
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2021 Charmbracelet, Inc
|
Copyright (c) 2022-2024 Charmbracelet, Inc
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
414
README.md
414
README.md
|
@ -14,11 +14,7 @@ A tool for glamorous shell scripts. Leverage the power of
|
||||||
Gloss](https://github.com/charmbracelet/lipgloss) in your scripts and aliases
|
Gloss](https://github.com/charmbracelet/lipgloss) in your scripts and aliases
|
||||||
without writing any Go code!
|
without writing any Go code!
|
||||||
|
|
||||||
<picture>
|
<img alt="Shell running the ./demo.sh script" width="600" src="https://vhs.charm.sh/vhs-1qY57RrQlXCuydsEgDp68G.gif">
|
||||||
<source media="(max-width: 600px)" srcset="https://stuff.charm.sh/gum/demo.gif">
|
|
||||||
<source media="(min-width: 600px)" width="600" srcset="https://stuff.charm.sh/gum/demo.gif">
|
|
||||||
<img alt="Shell running the ./demo.sh script" src="https://stuff.charm.sh/gum/demo.gif">
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
The above example is running from a single shell script ([source](./examples/demo.sh)).
|
The above example is running from a single shell script ([source](./examples/demo.sh)).
|
||||||
|
|
||||||
|
@ -26,74 +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
|
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.
|
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
|
Ask for the commit type with gum choose:
|
||||||
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`:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gum choose "fix" "feat" "docs" "style" "refactor" "test" "chore" "revert"
|
gum choose "fix" "feat" "docs" "style" "refactor" "test" "chore" "revert"
|
||||||
```
|
```
|
||||||
|
|
||||||
> Tip: this command itself will print to `stdout` which is not all that useful.
|
> [!NOTE]
|
||||||
To make use of the command later on you can save the stdout to a `$VARIABLE` or
|
> 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`.
|
||||||
`file.txt`.
|
|
||||||
|
|
||||||
Prompt for an (optional) scope for the commit:
|
|
||||||
|
|
||||||
|
Prompt for the scope of these changes:
|
||||||
```bash
|
```bash
|
||||||
gum input --placeholder "scope"
|
gum input --placeholder "scope"
|
||||||
```
|
```
|
||||||
|
|
||||||
Prompt for a commit message:
|
Prompt for the summary and description of changes:
|
||||||
|
|
||||||
```bash
|
```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:
|
Confirm before committing:
|
||||||
|
|
||||||
```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.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gum confirm "Commit changes?" && git commit -m "$SUMMARY" -m "$DESCRIPTION"
|
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
|
<img alt="Running the ./examples/commit.sh script to commit to git" width="600" src="https://vhs.charm.sh/vhs-7rRq3LsEuJVwhwr0xf6Er7.gif">
|
||||||
#!/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"
|
|
||||||
```
|
|
||||||
|
|
||||||
<picture>
|
|
||||||
<source media="(max-width: 600px)" srcset="https://stuff.charm.sh/gum/commit_2.gif">
|
|
||||||
<source media="(min-width: 600px)" width="600" srcset="https://stuff.charm.sh/gum/commit_2.gif">
|
|
||||||
<img alt="Running the ./examples/commit.sh script to commit to git" src="https://stuff.charm.sh/gum/commit_2.gif">
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -109,23 +67,40 @@ pacman -S gum
|
||||||
# Nix
|
# Nix
|
||||||
nix-env -iA nixpkgs.gum
|
nix-env -iA nixpkgs.gum
|
||||||
|
|
||||||
# Debian/Ubuntu
|
# Windows (via WinGet or Scoop)
|
||||||
echo 'deb [trusted=yes] https://repo.charm.sh/apt/ /' | sudo tee /etc/apt/sources.list.d/charm.list
|
winget install charmbracelet.gum
|
||||||
sudo apt update && sudo apt install gum
|
scoop install charm-gum
|
||||||
|
```
|
||||||
|
|
||||||
# Fedora
|
<details>
|
||||||
|
<summary>Debian/Ubuntu</summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /etc/apt/keyrings
|
||||||
|
curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg
|
||||||
|
echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list
|
||||||
|
sudo apt update && sudo apt install gum
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Fedora/RHEL</summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
echo '[charm]
|
echo '[charm]
|
||||||
name=Charm
|
name=Charm
|
||||||
baseurl=https://repo.charm.sh/yum/
|
baseurl=https://repo.charm.sh/yum/
|
||||||
enabled=1
|
enabled=1
|
||||||
gpgcheck=0' | sudo tee /etc/yum.repos.d/charm.repo
|
gpgcheck=1
|
||||||
|
gpgkey=https://repo.charm.sh/yum/gpg.key' | sudo tee /etc/yum.repos.d/charm.repo
|
||||||
sudo yum install gum
|
sudo yum install gum
|
||||||
```
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
Or download it:
|
Or download it:
|
||||||
|
|
||||||
* [Packages][releases] are available in Debian and RPM formats
|
* [Packages][releases] are available in Debian, RPM, and Alpine formats
|
||||||
* [Binaries][releases] are available for Linux, macOS, and Windows
|
* [Binaries][releases] are available for Linux, macOS, Windows, FreeBSD, OpenBSD, and NetBSD
|
||||||
|
|
||||||
Or just install it with `go`:
|
Or just install it with `go`:
|
||||||
|
|
||||||
|
@ -135,25 +110,40 @@ go install github.com/charmbracelet/gum@latest
|
||||||
|
|
||||||
[releases]: https://github.com/charmbracelet/gum/releases
|
[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
|
## Customization
|
||||||
|
|
||||||
`gum` is designed to be embedded in scripts and supports all sorts of use
|
You can customize `gum` options and styles with `--flags` and `$ENVIRONMENT_VARIABLES`.
|
||||||
cases. Components are configurable and customizable to fit your theme and
|
See `gum <command> --help` for a full view of each command's customization and configuration options.
|
||||||
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:
|
|
||||||
|
|
||||||
|
Customize with `--flags`:
|
||||||
```bash
|
```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
|
Customize with `ENVIRONMENT_VARIABLES`:
|
||||||
useful to keep a consistent theme for all your `gum` commands.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export GUM_INPUT_CURSOR_FOREGROUND="#FF0"
|
export GUM_INPUT_CURSOR_FOREGROUND="#FF0"
|
||||||
|
@ -162,86 +152,54 @@ export GUM_INPUT_PLACEHOLDER="What's up?"
|
||||||
export GUM_INPUT_PROMPT="* "
|
export GUM_INPUT_PROMPT="* "
|
||||||
export GUM_INPUT_WIDTH=80
|
export GUM_INPUT_WIDTH=80
|
||||||
|
|
||||||
# Uses values configured through environment variables above but can still be
|
# --flags can override values set with environment
|
||||||
# overridden with flags.
|
|
||||||
gum input
|
gum input
|
||||||
```
|
```
|
||||||
|
|
||||||
<picture>
|
<img alt="Gum input displaying most customization options" width="600" src="https://vhs.charm.sh/vhs-5zb9DlQYA70aL9ZpYLTwKv.gif">
|
||||||
<source media="(max-width: 600px)" srcset="https://stuff.charm.sh/gum/customization.gif">
|
|
||||||
<source media="(min-width: 600px)" width="600" srcset="https://stuff.charm.sh/gum/customization.gif">
|
|
||||||
<img alt="Gum input displaying most customization options" src="https://stuff.charm.sh/gum/customization.gif">
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
## Interaction
|
## Input
|
||||||
|
|
||||||
#### Input
|
|
||||||
|
|
||||||
Prompt for input with a simple command.
|
Prompt for input with a simple command.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gum input > answer.text
|
gum input > answer.txt
|
||||||
|
gum input --password > password.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Prompt for sensitive input with the `--password` flag.
|
<img src="https://vhs.charm.sh/vhs-1nScrStFI3BMlCp5yrLtyg.gif" width="600" alt="Shell running gum input typing Not much, you?" />
|
||||||
|
|
||||||
|
## Write
|
||||||
|
|
||||||
|
Prompt for some multi-line text (`ctrl+d` to complete text entry).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gum input --password > password.text
|
gum write > story.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
<picture>
|
<img src="https://vhs.charm.sh/vhs-7abdKKrUEukgx9aJj8O5GX.gif" width="600" alt="Shell running gum write typing a story" />
|
||||||
<source media="(max-width: 600px)" srcset="https://stuff.charm.sh/gum/input_1.gif">
|
|
||||||
<source media="(min-width: 600px)" width="600" srcset="https://stuff.charm.sh/gum/input_1.gif">
|
|
||||||
<img src="https://stuff.charm.sh/gum/input_1.gif" alt="Shell running gum input typing Not much, you?" />
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
#### Write
|
## Filter
|
||||||
|
|
||||||
Prompt for some multi-line text.
|
Filter a list of values with fuzzy matching:
|
||||||
|
|
||||||
Note: `CTRL+D` and `esc` are used to complete text entry. `CTRL+C` will cancel.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gum write > story.text
|
echo Strawberry >> flavors.txt
|
||||||
|
echo Banana >> flavors.txt
|
||||||
|
echo Cherry >> flavors.txt
|
||||||
|
gum filter < flavors.txt > selection.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
<picture>
|
<img src="https://vhs.charm.sh/vhs-61euOQtKPtQVD7nDpHQhzr.gif" width="600" alt="Shell running gum filter on different bubble gum flavors" />
|
||||||
<source media="(max-width: 600px)" srcset="https://stuff.charm.sh/gum/write.gif">
|
|
||||||
<source media="(min-width: 600px)" width="600" srcset="https://stuff.charm.sh/gum/write.gif">
|
|
||||||
<img src="https://stuff.charm.sh/gum/write.gif" alt="Shell running gum write typing a story" />
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
#### Filter
|
Select multiple options with the `--limit` flag or `--no-limit` flag. Use `tab` or `ctrl+space` to select, `enter` to confirm.
|
||||||
|
|
||||||
Use fuzzy matching to filter a list of values:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
echo Strawberry >> flavors.text
|
cat flavors.txt | gum filter --limit 2
|
||||||
echo Banana >> flavors.text
|
cat flavors.txt | gum filter --no-limit
|
||||||
echo Cherry >> flavors.text
|
|
||||||
cat flavors.text | gum filter > selection.text
|
|
||||||
```
|
```
|
||||||
|
|
||||||
<picture>
|
## Choose
|
||||||
<source media="(max-width: 600px)" srcset="https://stuff.charm.sh/gum/filter.gif">
|
|
||||||
<source media="(min-width: 600px)" width="600" srcset="https://stuff.charm.sh/gum/filter.gif">
|
|
||||||
<img src="https://stuff.charm.sh/gum/filter.gif" alt="Shell running gum filter on different bubble gum flavors" />
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
You can also select multiple items with the `--limit` flag, which determines
|
|
||||||
the maximum number of items that can be chosen.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cat flavors.text | gum filter --limit 2
|
|
||||||
```
|
|
||||||
|
|
||||||
Or, allow any number of selections with the `--no-limit` flag.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cat flavors.text | gum filter --no-limit
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Choose
|
|
||||||
|
|
||||||
Choose an option from a list of choices.
|
Choose an option from a list of choices.
|
||||||
|
|
||||||
|
@ -251,28 +209,17 @@ CARD=$(gum choose --height 15 {{A,K,Q,J},{10..2}}" "{♠,♥,♣,♦})
|
||||||
echo "Was your card the $CARD?"
|
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.
|
the maximum of items that can be chosen.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
echo "Pick your top 5 songs."
|
|
||||||
cat songs.txt | gum choose --limit 5
|
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
|
## Confirm
|
||||||
echo "What do you need from the grocery store?"
|
|
||||||
cat foods.txt | gum choose --no-limit
|
|
||||||
```
|
|
||||||
|
|
||||||
<picture>
|
|
||||||
<source media="(max-width: 600px)" srcset="https://stuff.charm.sh/gum/choose.gif">
|
|
||||||
<source media="(min-width: 600px)" width="600" srcset="https://stuff.charm.sh/gum/choose.gif">
|
|
||||||
<img src="https://stuff.charm.sh/gum/choose.gif" alt="Shell running gum choose with numbers and gum flavors" />
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
#### Confirm
|
|
||||||
|
|
||||||
Confirm whether to perform an action. Exits with code `0` (affirmative) or `1`
|
Confirm whether to perform an action. Exits with code `0` (affirmative) or `1`
|
||||||
(negative) depending on selection.
|
(negative) depending on selection.
|
||||||
|
@ -281,32 +228,54 @@ Confirm whether to perform an action. Exits with code `0` (affirmative) or `1`
|
||||||
gum confirm && rm file.txt || echo "File not removed"
|
gum confirm && rm file.txt || echo "File not removed"
|
||||||
```
|
```
|
||||||
|
|
||||||
<picture>
|
<img src="https://vhs.charm.sh/vhs-3xRFvbeQ4lqGerbHY7y3q2.gif" width="600" alt="Shell running gum confirm" />
|
||||||
<source media="(max-width: 600px)" srcset="https://stuff.charm.sh/gum/confirm_2.gif">
|
|
||||||
<source media="(min-width: 600px)" width="600" srcset="https://stuff.charm.sh/gum/confirm_2.gif">
|
|
||||||
<img src="https://stuff.charm.sh/gum/confirm_2.gif" alt="Shell running gum confirm" />
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
#### Spin
|
## File
|
||||||
|
|
||||||
|
Prompt the user to select a file from the file tree.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
EDITOR $(gum file $HOME)
|
||||||
|
```
|
||||||
|
|
||||||
|
<img src="https://vhs.charm.sh/vhs-2RMRqmnOPneneIgVJJ3mI1.gif" width="600" alt="Shell running gum file" />
|
||||||
|
|
||||||
|
## Pager
|
||||||
|
|
||||||
|
Scroll through a long document with line numbers and a fully customizable viewport.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gum pager < README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
<img src="https://vhs.charm.sh/vhs-3iMDpgOLmbYr0jrYEGbk7p.gif" width="600" alt="Shell running gum pager" />
|
||||||
|
|
||||||
|
## Spin
|
||||||
|
|
||||||
Display a spinner while running a script or command. The spinner will
|
Display a spinner while running a script or command. The spinner will
|
||||||
automatically stop after the given command exits.
|
automatically stop after the given command exits.
|
||||||
|
|
||||||
|
To view or pipe the command's output, use the `--show-output` flag.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gum spin --spinner dot --title "Buying Bubble Gum..." -- sleep 5
|
gum spin --spinner dot --title "Buying Bubble Gum..." -- sleep 5
|
||||||
```
|
```
|
||||||
|
|
||||||
<picture>
|
<img src="https://vhs.charm.sh/vhs-3YFswCmoY4o3Q7MyzWl6sS.gif" width="600" alt="Shell running gum spin while sleeping for 5 seconds" />
|
||||||
<source media="(max-width: 600px)" srcset="https://stuff.charm.sh/gum/spin.gif">
|
|
||||||
<source media="(min-width: 600px)" width="600" srcset="https://stuff.charm.sh/gum/spin.gif">
|
|
||||||
<img src="https://stuff.charm.sh/gum/spin.gif" alt="Shell running gum spin while sleeping for 5 seconds" />
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
Available spinner types include: `line`, `dot`, `minidot`, `jump`, `pulse`, `points`, `globe`, `moon`, `monkey`, `meter`, `hamburger`.
|
Available spinner types include: `line`, `dot`, `minidot`, `jump`, `pulse`, `points`, `globe`, `moon`, `monkey`, `meter`, `hamburger`.
|
||||||
|
|
||||||
## Styling
|
## Table
|
||||||
|
|
||||||
#### Style
|
Select a row from some tabular data.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gum table < flavors.csv | cut -d ',' -f 1
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- <img src="https://stuff.charm.sh/gum/table.gif" width="600" alt="Shell running gum table" /> -->
|
||||||
|
|
||||||
|
## Style
|
||||||
|
|
||||||
Pretty print any string with any layout with one command.
|
Pretty print any string with any layout with one command.
|
||||||
|
|
||||||
|
@ -317,15 +286,9 @@ gum style \
|
||||||
'Bubble Gum (1¢)' 'So sweet and so fresh!'
|
'Bubble Gum (1¢)' 'So sweet and so fresh!'
|
||||||
```
|
```
|
||||||
|
|
||||||
<picture>
|
<img src="https://github.com/charmbracelet/gum/assets/42545625/67468acf-b3e0-4e78-bd89-360739eb44fa" width="600" alt="Bubble Gum, So sweet and so fresh!" />
|
||||||
<source media="(max-width: 600px)" srcset="https://stuff.charm.sh/gum/style.gif">
|
|
||||||
<source media="(min-width: 600px)" width="600" srcset="https://stuff.charm.sh/gum/style.gif">
|
|
||||||
<img src="https://stuff.charm.sh/gum/style.gif" alt="Bubble Gum, So sweet and so fresh!" />
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
## Layout
|
## Join
|
||||||
|
|
||||||
#### Join
|
|
||||||
|
|
||||||
Combine text vertically or horizontally. Use this command with `gum style` to
|
Combine text vertically or horizontally. Use this command with `gum style` to
|
||||||
build layouts and pretty output.
|
build layouts and pretty output.
|
||||||
|
@ -344,11 +307,7 @@ BUBBLE_GUM=$(gum join "$BUBBLE" "$GUM")
|
||||||
gum join --align center --vertical "$I_LOVE" "$BUBBLE_GUM"
|
gum join --align center --vertical "$I_LOVE" "$BUBBLE_GUM"
|
||||||
```
|
```
|
||||||
|
|
||||||
<picture>
|
<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." />
|
||||||
<source media="(max-width: 600px)" srcset="https://stuff.charm.sh/gum/join.gif">
|
|
||||||
<source media="(min-width: 600px)" width="600" srcset="https://stuff.charm.sh/gum/join.gif">
|
|
||||||
<img src="https://stuff.charm.sh/gum/join.gif" alt="I LOVE Bubble Gum written out in four boxes with double borders around them." />
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
## Format
|
## Format
|
||||||
|
|
||||||
|
@ -375,128 +334,95 @@ For more information on template helpers, see the [Termenv
|
||||||
docs](https://github.com/muesli/termenv#template-helpers). For a full list of
|
docs](https://github.com/muesli/termenv#template-helpers). For a full list of
|
||||||
named emojis see the [GitHub API](https://api.github.com/emojis).
|
named emojis see the [GitHub API](https://api.github.com/emojis).
|
||||||
|
|
||||||
<picture>
|
<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" />
|
||||||
<source media="(max-width: 600px)" srcset="https://stuff.charm.sh/gum/format.gif">
|
|
||||||
<source media="(min-width: 600px)" width="600" srcset="https://stuff.charm.sh/gum/format.gif">
|
## Log
|
||||||
<img src="https://stuff.charm.sh/gum/format.gif" alt="Running gum format for different types of formats" />
|
|
||||||
</picture>
|
`log` logs messages to the terminal at using different levels and styling using
|
||||||
|
the [`charmbracelet/log`](https://github.com/charmbracelet/log) library.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Log some debug information.
|
||||||
|
gum log --structured --level debug "Creating file..." name file.txt
|
||||||
|
# DEBUG Unable to create file. name=temp.txt
|
||||||
|
|
||||||
|
# Log some error.
|
||||||
|
gum log --structured --level error "Unable to create file." name file.txt
|
||||||
|
# ERROR Unable to create file. name=temp.txt
|
||||||
|
|
||||||
|
# Include a timestamp.
|
||||||
|
gum log --time rfc822 --level error "Unable to create file."
|
||||||
|
```
|
||||||
|
|
||||||
|
See the Go [`time` package](https://pkg.go.dev/time#pkg-constants) for acceptable `--time` formats.
|
||||||
|
|
||||||
|
See [`charmbracelet/log`](https://github.com/charmbracelet/log) for more usage.
|
||||||
|
|
||||||
|
<img src="https://vhs.charm.sh/vhs-6jupuFM0s2fXiUrBE0I1vU.gif" width="600" alt="Running gum log with debug and error levels" />
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
See the [examples](./examples/) directory for more real world use cases.
|
|
||||||
|
|
||||||
How to use `gum` in your daily workflows:
|
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
|
* Write a commit message:
|
||||||
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.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git commit -m "$(gum input --width 50 --placeholder "Summary of changes")" \
|
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`
|
* 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`.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$EDITOR $(gum filter)
|
$EDITOR $(gum filter)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Connect to a TMUX session
|
* 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.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
SESSION=$(tmux list-sessions -F \#S | gum filter --placeholder "Pick session...")
|
SESSION=$(tmux list-sessions -F \#S | gum filter --placeholder "Pick session...")
|
||||||
tmux switch-client -t $SESSION || tmux attach -t $SESSION
|
tmux switch-client -t $SESSION || tmux attach -t $SESSION
|
||||||
```
|
```
|
||||||
|
|
||||||
<picture>
|
* Pick a commit hash from `git` history
|
||||||
<source media="(max-width: 600px)" srcset="https://stuff.charm.sh/gum/pick-tmux-session.gif">
|
|
||||||
<source media="(min-width: 600px)" width="600" srcset="https://stuff.charm.sh/gum/pick-tmux-session.gif">
|
|
||||||
<img src="https://stuff.charm.sh/gum/pick-tmux-session.gif" alt="Picking a tmux session with gum filter" />
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
#### 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.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git log --oneline | gum filter | cut -d' ' -f1 # | copy
|
git log --oneline | gum filter | cut -d' ' -f1 # | copy
|
||||||
```
|
```
|
||||||
|
|
||||||
<picture>
|
* Simple [`skate`](https://github.com/charmbracelet/skate) password selector.
|
||||||
<source media="(max-width: 600px)" srcset="https://stuff.charm.sh/gum/pick-commit.gif">
|
|
||||||
<source media="(min-width: 600px)" width="600" srcset="https://stuff.charm.sh/gum/pick-commit.gif">
|
|
||||||
<img src="https://stuff.charm.sh/gum/pick-commit.gif" alt="Picking a commit with gum filter" />
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
#### 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...
|
|
||||||
|
|
||||||
```
|
```
|
||||||
skate list -k | gum filter | xargs skate get
|
skate list -k | gum filter | xargs skate get
|
||||||
```
|
```
|
||||||
|
|
||||||
<picture>
|
* Uninstall packages
|
||||||
<source media="(max-width: 600px)" srcset="https://stuff.charm.sh/gum/skate-pass.gif">
|
|
||||||
<source media="(min-width: 600px)" width="600" srcset="https://stuff.charm.sh/gum/skate-pass.gif">
|
|
||||||
<img src="https://stuff.charm.sh/gum/skate-pass.gif" alt="Selecting a skate value with gum" />
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
#### Choose packages to uninstall
|
|
||||||
|
|
||||||
List all packages installed by your package manager (we'll use `brew`) and
|
|
||||||
choose which packages to uninstall.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew list | gum choose --no-limit | xargs brew uninstall
|
brew list | gum choose --no-limit | xargs brew uninstall
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Choose branches to delete
|
* Clean up `git` branches
|
||||||
|
|
||||||
List all branches and choose which branches to delete.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git branch | cut -c 3- | gum choose --no-limit | xargs git branch -D
|
git branch | cut -c 3- | gum choose --no-limit | xargs git branch -D
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Choose pull request to checkout
|
* Checkout GitHub pull requests with [`gh`](https://cli.github.com/)
|
||||||
|
|
||||||
List all PRs for the current GitHub repository and checkout the chosen PR (using [`gh`](https://cli.github.com/)).
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gh pr list | cut -f1,2 | gum choose | cut -f1 | xargs gh pr checkout
|
gh pr list | cut -f1,2 | gum choose | cut -f1 | xargs gh pr checkout
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Pick command from shell history
|
* Copy command from shell history
|
||||||
|
|
||||||
Pick a previously executed command from your shell history to execute, copy,
|
|
||||||
edit, etc...
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gum filter < $HISTFILE --height 20
|
gum filter < $HISTFILE --height 20
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Sudo password input
|
* `sudo` replacement
|
||||||
|
|
||||||
See visual feedback when entering password with masked characters with `gum
|
|
||||||
input --password`.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
alias please="gum input --password | sudo -nS"
|
alias please="gum input --password | sudo -nS"
|
||||||
|
@ -507,14 +433,14 @@ alias please="gum input --password | sudo -nS"
|
||||||
We’d love to hear your thoughts on this project. Feel free to drop us a note!
|
We’d love to hear your thoughts on this project. Feel free to drop us a note!
|
||||||
|
|
||||||
* [Twitter](https://twitter.com/charmcli)
|
* [Twitter](https://twitter.com/charmcli)
|
||||||
* [The Fediverse](https://mastodon.technology/@charm)
|
* [The Fediverse](https://mastodon.social/@charmcli)
|
||||||
* [Slack](https://charm.sh/slack)
|
* [Discord](https://charm.sh/chat)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[MIT](https://github.com/charmbracelet/gum/raw/main/LICENSE)
|
[MIT](https://github.com/charmbracelet/gum/raw/main/LICENSE)
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
Part of [Charm](https://charm.sh).
|
Part of [Charm](https://charm.sh).
|
||||||
|
|
||||||
|
|
10
ansi/ansi.go
Normal file
10
ansi/ansi.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package ansi
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
var ansiEscape = regexp.MustCompile(`\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])`)
|
||||||
|
|
||||||
|
// Strip strips a string of any of it's ansi sequences.
|
||||||
|
func Strip(text string) string {
|
||||||
|
return ansiEscape.ReplaceAllString(text, "")
|
||||||
|
}
|
188
choose/choose.go
188
choose/choose.go
|
@ -1,188 +0,0 @@
|
||||||
// Package choose provides an interface to choose one option from a given list
|
|
||||||
// of options. The options can be provided as (new-line separated) stdin or a
|
|
||||||
// list of arguments.
|
|
||||||
//
|
|
||||||
// It is different from the filter command as it does not provide a fuzzy
|
|
||||||
// finding input, so it is best used for smaller lists of options.
|
|
||||||
//
|
|
||||||
// Let's pick from a list of gum flavors:
|
|
||||||
//
|
|
||||||
// $ gum choose "Strawberry" "Banana" "Cherry"
|
|
||||||
package choose
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/paginator"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
"github.com/mattn/go-runewidth"
|
|
||||||
)
|
|
||||||
|
|
||||||
type model struct {
|
|
||||||
height int
|
|
||||||
cursor string
|
|
||||||
selectedPrefix string
|
|
||||||
unselectedPrefix string
|
|
||||||
cursorPrefix string
|
|
||||||
items []item
|
|
||||||
quitting bool
|
|
||||||
index int
|
|
||||||
limit int
|
|
||||||
numSelected int
|
|
||||||
paginator paginator.Model
|
|
||||||
aborted bool
|
|
||||||
|
|
||||||
// styles
|
|
||||||
cursorStyle lipgloss.Style
|
|
||||||
itemStyle lipgloss.Style
|
|
||||||
selectedItemStyle lipgloss.Style
|
|
||||||
}
|
|
||||||
|
|
||||||
type item struct {
|
|
||||||
text string
|
|
||||||
selected bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m model) Init() tea.Cmd { return nil }
|
|
||||||
|
|
||||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case tea.WindowSizeMsg:
|
|
||||||
return m, nil
|
|
||||||
|
|
||||||
case tea.KeyMsg:
|
|
||||||
start, end := m.paginator.GetSliceBounds(len(m.items))
|
|
||||||
switch keypress := msg.String(); keypress {
|
|
||||||
case "down", "j", "ctrl+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+p":
|
|
||||||
m.index--
|
|
||||||
if m.index < 0 {
|
|
||||||
m.index = len(m.items) - 1
|
|
||||||
m.paginator.Page = m.paginator.TotalPages - 1
|
|
||||||
}
|
|
||||||
if m.index < start {
|
|
||||||
m.paginator.PrevPage()
|
|
||||||
}
|
|
||||||
case "right", "l", "ctrl+f":
|
|
||||||
m.index = clamp(m.index+m.height, 0, len(m.items)-1)
|
|
||||||
m.paginator.NextPage()
|
|
||||||
case "left", "h", "ctrl+b":
|
|
||||||
m.index = clamp(m.index-m.height, 0, len(m.items)-1)
|
|
||||||
m.paginator.PrevPage()
|
|
||||||
case "G":
|
|
||||||
m.index = len(m.items) - 1
|
|
||||||
m.paginator.Page = m.paginator.TotalPages - 1
|
|
||||||
case "g":
|
|
||||||
m.index = 0
|
|
||||||
m.paginator.Page = 0
|
|
||||||
case "a":
|
|
||||||
if m.limit <= 1 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for i := range m.items {
|
|
||||||
if m.numSelected >= m.limit {
|
|
||||||
break // do not exceed given limit
|
|
||||||
}
|
|
||||||
if m.items[i].selected {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
m.items[i].selected = true
|
|
||||||
m.numSelected++
|
|
||||||
}
|
|
||||||
case "A":
|
|
||||||
if m.limit <= 1 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for i := range m.items {
|
|
||||||
m.items[i].selected = false
|
|
||||||
}
|
|
||||||
m.numSelected = 0
|
|
||||||
case "ctrl+c", "esc":
|
|
||||||
m.aborted = true
|
|
||||||
m.quitting = true
|
|
||||||
return m, tea.Quit
|
|
||||||
case " ", "x":
|
|
||||||
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.numSelected++
|
|
||||||
}
|
|
||||||
case "enter":
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var cmd tea.Cmd
|
|
||||||
m.paginator, cmd = m.paginator.Update(msg)
|
|
||||||
return m, cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m model) View() string {
|
|
||||||
if m.quitting {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var s strings.Builder
|
|
||||||
|
|
||||||
start, end := m.paginator.GetSliceBounds(len(m.items))
|
|
||||||
for i, item := range m.items[start:end] {
|
|
||||||
if i == m.index%m.height {
|
|
||||||
s.WriteString(m.cursorStyle.Render(m.cursor))
|
|
||||||
} else {
|
|
||||||
s.WriteString(strings.Repeat(" ", runewidth.StringWidth(m.cursor)))
|
|
||||||
}
|
|
||||||
|
|
||||||
if item.selected {
|
|
||||||
s.WriteString(m.selectedItemStyle.Render(m.selectedPrefix + item.text))
|
|
||||||
} else if i == m.index%m.height {
|
|
||||||
s.WriteString(m.cursorStyle.Render(m.cursorPrefix + item.text))
|
|
||||||
} else {
|
|
||||||
s.WriteString(m.itemStyle.Render(m.unselectedPrefix + item.text))
|
|
||||||
}
|
|
||||||
if i != m.height {
|
|
||||||
s.WriteRune('\n')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.paginator.TotalPages <= 1 {
|
|
||||||
return s.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
s.WriteString(strings.Repeat("\n", m.height-m.paginator.ItemsOnPage(len(m.items))+1))
|
|
||||||
s.WriteString(" " + m.paginator.View())
|
|
||||||
|
|
||||||
return s.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:unparam
|
|
||||||
func clamp(x, min, max int) int {
|
|
||||||
if x < min {
|
|
||||||
return min
|
|
||||||
}
|
|
||||||
if x > max {
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
return x
|
|
||||||
}
|
|
|
@ -6,135 +6,132 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
"github.com/charmbracelet/huh"
|
||||||
"github.com/charmbracelet/bubbles/paginator"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
|
|
||||||
"github.com/charmbracelet/gum/internal/exit"
|
"github.com/charmbracelet/gum/ansi"
|
||||||
"github.com/charmbracelet/gum/internal/stdin"
|
"github.com/charmbracelet/gum/internal/stdin"
|
||||||
"github.com/charmbracelet/gum/style"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
const widthBuffer = 2
|
||||||
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
|
// Run provides a shell script interface for choosing between different through
|
||||||
// options.
|
// options.
|
||||||
func (o Options) Run() error {
|
func (o Options) Run() error {
|
||||||
if len(o.Options) == 0 {
|
if len(o.Options) <= 0 {
|
||||||
input, _ := stdin.Read()
|
input, _ := stdin.Read()
|
||||||
if input == "" {
|
if input == "" {
|
||||||
return errors.New("no options provided, see `gum choose --help`")
|
return errors.New("no options provided, see `gum choose --help`")
|
||||||
}
|
}
|
||||||
o.Options = strings.Split(strings.TrimSpace(input), "\n")
|
o.Options = strings.Split(input, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't need to display prefixes if we are only picking one option.
|
if o.SelectIfOne && len(o.Options) == 1 {
|
||||||
// Simply displaying the cursor is enough.
|
fmt.Println(o.Options[0])
|
||||||
if o.Limit == 1 && !o.NoLimit {
|
return nil
|
||||||
o.SelectedPrefix = ""
|
}
|
||||||
o.UnselectedPrefix = ""
|
|
||||||
o.CursorPrefix = ""
|
theme := huh.ThemeCharm()
|
||||||
|
options := huh.NewOptions(o.Options...)
|
||||||
|
|
||||||
|
theme.Focused.Base = lipgloss.NewStyle()
|
||||||
|
theme.Focused.Title = o.HeaderStyle.ToLipgloss()
|
||||||
|
theme.Focused.SelectSelector = o.CursorStyle.ToLipgloss().SetString(o.Cursor)
|
||||||
|
theme.Focused.MultiSelectSelector = o.CursorStyle.ToLipgloss().SetString(o.Cursor)
|
||||||
|
theme.Focused.SelectedOption = o.SelectedItemStyle.ToLipgloss()
|
||||||
|
theme.Focused.UnselectedOption = o.ItemStyle.ToLipgloss()
|
||||||
|
theme.Focused.SelectedPrefix = o.SelectedItemStyle.ToLipgloss().SetString(o.SelectedPrefix)
|
||||||
|
theme.Focused.UnselectedPrefix = o.ItemStyle.ToLipgloss().SetString(o.UnselectedPrefix)
|
||||||
|
|
||||||
|
for _, s := range o.Selected {
|
||||||
|
for i, opt := range options {
|
||||||
|
if s == opt.Key || s == opt.Value {
|
||||||
|
options[i] = opt.Selected(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've set no limit then we can simply select as many options as there
|
|
||||||
// are so let's set the limit to the number of options.
|
|
||||||
if o.NoLimit {
|
if o.NoLimit {
|
||||||
o.Limit = len(o.Options)
|
o.Limit = len(o.Options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep track of the selected items.
|
width := max(widest(o.Options)+
|
||||||
currentSelected := 0
|
max(lipgloss.Width(o.SelectedPrefix)+lipgloss.Width(o.UnselectedPrefix))+
|
||||||
// Check if selected items should be used.
|
lipgloss.Width(o.Cursor)+1, lipgloss.Width(o.Header)+widthBuffer)
|
||||||
hasSelectedItems := len(o.Selected) > 0
|
|
||||||
|
|
||||||
startingIndex := 0
|
if o.Limit > 1 {
|
||||||
|
var choices []string
|
||||||
|
|
||||||
var items = make([]item, len(o.Options))
|
field := huh.NewMultiSelect[string]().
|
||||||
|
Options(options...).
|
||||||
|
Title(o.Header).
|
||||||
|
Height(o.Height).
|
||||||
|
Limit(o.Limit).
|
||||||
|
Value(&choices)
|
||||||
|
|
||||||
for i, option := range o.Options {
|
form := huh.NewForm(huh.NewGroup(field))
|
||||||
// Check if the option should be selected.
|
|
||||||
isSelected := hasSelectedItems && currentSelected < o.Limit && arrayContains(o.Selected, option)
|
err := form.
|
||||||
// If the option is selected then increment the current selected count.
|
WithWidth(width).
|
||||||
if isSelected {
|
WithShowHelp(o.ShowHelp).
|
||||||
currentSelected++
|
WithTheme(theme).
|
||||||
if o.Limit == 1 {
|
Run()
|
||||||
startingIndex = i
|
|
||||||
}
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
if len(choices) > 0 {
|
||||||
items[i] = item{text: option, selected: isSelected}
|
s := strings.Join(choices, "\n")
|
||||||
|
ansiprint(s)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the pagination model to display the current and total number of
|
var choice string
|
||||||
// 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("•")
|
|
||||||
|
|
||||||
// Disable Keybindings since we will control it ourselves.
|
err := huh.NewForm(
|
||||||
pager.UseHLKeys = false
|
huh.NewGroup(
|
||||||
pager.UseLeftRightKeys = false
|
huh.NewSelect[string]().
|
||||||
pager.UseJKKeys = false
|
Options(options...).
|
||||||
pager.UsePgUpPgDownKeys = false
|
Title(o.Header).
|
||||||
|
Height(o.Height).
|
||||||
tm, err := tea.NewProgram(model{
|
Value(&choice),
|
||||||
index: startingIndex,
|
),
|
||||||
height: o.Height,
|
).
|
||||||
cursor: o.Cursor,
|
WithWidth(width).
|
||||||
selectedPrefix: o.SelectedPrefix,
|
WithTheme(theme).
|
||||||
unselectedPrefix: o.UnselectedPrefix,
|
WithShowHelp(o.ShowHelp).
|
||||||
cursorPrefix: o.CursorPrefix,
|
Run()
|
||||||
items: items,
|
|
||||||
limit: o.Limit,
|
|
||||||
paginator: pager,
|
|
||||||
cursorStyle: o.CursorStyle.ToLipgloss(),
|
|
||||||
itemStyle: o.ItemStyle.ToLipgloss(),
|
|
||||||
selectedItemStyle: o.SelectedItemStyle.ToLipgloss(),
|
|
||||||
numSelected: currentSelected,
|
|
||||||
}, tea.WithOutput(os.Stderr)).StartReturningModel()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to start tea program: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
m := tm.(model)
|
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||||
if m.aborted {
|
fmt.Println(choice)
|
||||||
return exit.ErrAborted
|
} else {
|
||||||
|
fmt.Print(ansi.Strip(choice))
|
||||||
}
|
}
|
||||||
|
|
||||||
var s strings.Builder
|
|
||||||
|
|
||||||
for _, item := range m.items {
|
|
||||||
if item.selected {
|
|
||||||
s.WriteString(item.text)
|
|
||||||
s.WriteRune('\n')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Print(s.String())
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeReset hook. Used to unclutter style flags.
|
func widest(options []string) int {
|
||||||
func (o Options) BeforeReset(ctx *kong.Context) error {
|
var max int
|
||||||
style.HideFlags(ctx)
|
for _, o := range options {
|
||||||
return nil
|
w := lipgloss.Width(o)
|
||||||
}
|
if w > max {
|
||||||
|
max = w
|
||||||
// Check if an array contains a value.
|
|
||||||
func arrayContains(strArray []string, value string) bool {
|
|
||||||
for _, str := range strArray {
|
|
||||||
if str == value {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
func ansiprint(s string) {
|
||||||
|
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||||
|
fmt.Println(s)
|
||||||
|
} else {
|
||||||
|
fmt.Print(ansi.Strip(s))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,29 @@
|
||||||
package choose
|
package choose
|
||||||
|
|
||||||
import "github.com/charmbracelet/gum/style"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/gum/style"
|
||||||
|
)
|
||||||
|
|
||||||
// Options is the customization options for the choose command.
|
// Options is the customization options for the choose command.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Options []string `arg:"" optional:"" help:"Options to choose from."`
|
Options []string `arg:"" optional:"" help:"Options to choose from."`
|
||||||
|
Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"`
|
||||||
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"`
|
||||||
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
|
Ordered bool `help:"Maintain the order of the selected options" env:"GUM_CHOOSE_ORDERED"`
|
||||||
Height int `help:"Height of the list" default:"10" env:"GUM_CHOOSE_HEIGHT"`
|
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"`
|
Cursor string `help:"Prefix to show on item that corresponds to the cursor position" default:"> " env:"GUM_CHOOSE_CURSOR"`
|
||||||
CursorPrefix string `help:"Prefix to show on the cursor item (hidden if limit is 1)" default:"○ " env:"GUM_CHOOSE_CURSOR_PREFIX"`
|
ShowHelp bool `help:"Show help keybinds" default:"true" negatable:"true" env:"GUM_CHOOSE_SHOW_HELP"`
|
||||||
SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:"◉ " env:"GUM_CHOOSE_SELECTED_PREFIX"`
|
Header string `help:"Header value" default:"Choose:" env:"GUM_CHOOSE_HEADER"`
|
||||||
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"`
|
||||||
Selected []string `help:"Options that should start as selected" default:"" env:"GUM_CHOOSE_SELECTED"`
|
SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:"✓ " env:"GUM_CHOOSE_SELECTED_PREFIX"`
|
||||||
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_CURSOR_"`
|
UnselectedPrefix string `help:"Prefix to show on unselected items (hidden if limit is 1)" default:"• " env:"GUM_CHOOSE_UNSELECTED_PREFIX"`
|
||||||
ItemStyle style.Styles `embed:"" prefix:"item." hidden:"" envprefix:"GUM_CHOOSE_ITEM_"`
|
Selected []string `help:"Options that should start as selected" default:"" env:"GUM_CHOOSE_SELECTED"`
|
||||||
SelectedItemStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_SELECTED_"`
|
SelectIfOne bool `help:"Select the given option if there is only one" group:"Selection"`
|
||||||
|
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_CURSOR_"`
|
||||||
|
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=99" envprefix:"GUM_CHOOSE_HEADER_"`
|
||||||
|
ItemStyle style.Styles `embed:"" prefix:"item." hidden:"" envprefix:"GUM_CHOOSE_ITEM_"`
|
||||||
|
SelectedItemStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_SELECTED_"`
|
||||||
|
Timeout time.Duration `help:"Timeout until choose returns selected element" default:"0" env:"GUM_CCHOOSE_TIMEOUT"` // including timeout command options [Timeout,...]
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,42 +4,39 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
"github.com/charmbracelet/huh"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/gum/style"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run provides a shell script interface for prompting a user to confirm an
|
// Run provides a shell script interface for prompting a user to confirm an
|
||||||
// action with an affirmative or negative answer.
|
// action with an affirmative or negative answer.
|
||||||
func (o Options) Run() error {
|
func (o Options) Run() error {
|
||||||
m, err := tea.NewProgram(model{
|
theme := huh.ThemeCharm()
|
||||||
affirmative: o.Affirmative,
|
theme.Focused.Title = o.PromptStyle.ToLipgloss()
|
||||||
negative: o.Negative,
|
theme.Focused.FocusedButton = o.SelectedStyle.ToLipgloss()
|
||||||
confirmation: o.Default,
|
theme.Focused.BlurredButton = o.UnselectedStyle.ToLipgloss()
|
||||||
timeout: o.Timeout,
|
|
||||||
hasTimeout: o.Timeout > 0,
|
choice := o.Default
|
||||||
prompt: o.Prompt,
|
|
||||||
selectedStyle: o.SelectedStyle.ToLipgloss(),
|
err := huh.NewForm(
|
||||||
unselectedStyle: o.UnselectedStyle.ToLipgloss(),
|
huh.NewGroup(
|
||||||
promptStyle: o.PromptStyle.ToLipgloss(),
|
huh.NewConfirm().
|
||||||
}, tea.WithOutput(os.Stderr)).StartReturningModel()
|
Affirmative(o.Affirmative).
|
||||||
|
Negative(o.Negative).
|
||||||
|
Title(o.Prompt).
|
||||||
|
Value(&choice),
|
||||||
|
),
|
||||||
|
).
|
||||||
|
WithTheme(theme).
|
||||||
|
WithShowHelp(o.ShowHelp).
|
||||||
|
Run()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to run confirm: %w", err)
|
return fmt.Errorf("unable to run confirm: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.(model).confirmation {
|
if !choice {
|
||||||
os.Exit(0)
|
|
||||||
} else {
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeReset hook. Used to unclutter style flags.
|
|
||||||
func (o Options) BeforeReset(ctx *kong.Context) error {
|
|
||||||
style.HideFlags(ctx)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,114 +0,0 @@
|
||||||
// Package confirm provides an interface to ask a user to confirm an action.
|
|
||||||
// The user is provided with an interface to choose an affirmative or negative
|
|
||||||
// answer, which is then reflected in the exit code for use in scripting.
|
|
||||||
//
|
|
||||||
// If the user selects the affirmative answer, the program exits with 0. If the
|
|
||||||
// user selects the negative answer, the program exits with 1.
|
|
||||||
//
|
|
||||||
// I.e. confirm if the user wants to delete a file
|
|
||||||
//
|
|
||||||
// $ gum confirm "Are you sure?" && rm file.txt
|
|
||||||
package confirm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
)
|
|
||||||
|
|
||||||
type model struct {
|
|
||||||
prompt string
|
|
||||||
affirmative string
|
|
||||||
negative string
|
|
||||||
quitting bool
|
|
||||||
hasTimeout bool
|
|
||||||
timeout time.Duration
|
|
||||||
|
|
||||||
confirmation bool
|
|
||||||
|
|
||||||
// styles
|
|
||||||
promptStyle lipgloss.Style
|
|
||||||
selectedStyle lipgloss.Style
|
|
||||||
unselectedStyle lipgloss.Style
|
|
||||||
}
|
|
||||||
|
|
||||||
const tickInterval = time.Second
|
|
||||||
|
|
||||||
type tickMsg struct{}
|
|
||||||
|
|
||||||
func tick() tea.Cmd {
|
|
||||||
return tea.Tick(tickInterval, func(time.Time) tea.Msg {
|
|
||||||
return tickMsg{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m model) Init() tea.Cmd {
|
|
||||||
if m.timeout > 0 {
|
|
||||||
return tick()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case tea.WindowSizeMsg:
|
|
||||||
return m, nil
|
|
||||||
case tea.KeyMsg:
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c", "esc", "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":
|
|
||||||
m.confirmation = !m.confirmation
|
|
||||||
case "enter":
|
|
||||||
m.quitting = true
|
|
||||||
return m, tea.Quit
|
|
||||||
case "y", "Y":
|
|
||||||
m.quitting = true
|
|
||||||
m.confirmation = true
|
|
||||||
return m, tea.Quit
|
|
||||||
}
|
|
||||||
case tickMsg:
|
|
||||||
if m.timeout <= 0 {
|
|
||||||
m.quitting = true
|
|
||||||
m.confirmation = false
|
|
||||||
return m, tea.Quit
|
|
||||||
}
|
|
||||||
m.timeout -= tickInterval
|
|
||||||
return m, tick()
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m model) View() string {
|
|
||||||
if m.quitting {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var aff, neg, timeout string
|
|
||||||
|
|
||||||
if m.hasTimeout {
|
|
||||||
timeout = fmt.Sprintf(" (%d)", max(0, int(m.timeout.Seconds())))
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.confirmation {
|
|
||||||
aff = m.selectedStyle.Render(m.affirmative)
|
|
||||||
neg = m.unselectedStyle.Render(m.negative + timeout)
|
|
||||||
} else {
|
|
||||||
aff = m.unselectedStyle.Render(m.affirmative)
|
|
||||||
neg = m.selectedStyle.Render(m.negative + timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
return lipgloss.JoinVertical(lipgloss.Center, m.promptStyle.Render(m.prompt), lipgloss.JoinHorizontal(lipgloss.Left, aff, neg))
|
|
||||||
}
|
|
||||||
|
|
||||||
func max(a, b int) int {
|
|
||||||
if a > b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
|
@ -8,14 +8,16 @@ import (
|
||||||
|
|
||||||
// Options is the customization options for the confirm command.
|
// Options is the customization options for the confirm command.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Affirmative string `help:"The title of the affirmative action" default:"Yes"`
|
Default bool `help:"Default confirmation action" default:"true"`
|
||||||
Negative string `help:"The title of the negative action" default:"No"`
|
Affirmative string `help:"The title of the affirmative action" default:"Yes"`
|
||||||
Default bool `help:"Default confirmation action" default:"true"`
|
Negative string `help:"The title of the negative action" default:"No"`
|
||||||
Timeout time.Duration `help:"Timeout for confirmation" default:"0" env:"GUM_CONFIRM_TIMEOUT"`
|
Prompt string `arg:"" help:"Prompt to display." default:"Are you sure?"`
|
||||||
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_"`
|
|
||||||
//nolint:staticcheck
|
//nolint:staticcheck
|
||||||
SelectedStyle style.Styles `embed:"" prefix:"selected." help:"The style of the selected action" set:"defaultBackground=212" set:"defaultForeground=230" set:"defaultPadding=0 3" set:"defaultMargin=1 1" envprefix:"GUM_CONFIRM_SELECTED_"`
|
PromptStyle style.Styles `embed:"" prefix:"prompt." help:"The style of the prompt" set:"defaultMargin=0 0 0 1" set:"defaultForeground=#7571F9" set:"defaultBold=true" envprefix:"GUM_CONFIRM_PROMPT_"`
|
||||||
//nolint:staticcheck
|
//nolint:staticcheck
|
||||||
UnselectedStyle style.Styles `embed:"" prefix:"unselected." help:"The style of the unselected action" set:"defaultBackground=235" set:"defaultForeground=254" set:"defaultPadding=0 3" set:"defaultMargin=1 1" envprefix:"GUM_CONFIRM_UNSELECTED_"`
|
SelectedStyle style.Styles `embed:"" prefix:"selected." help:"The style of the selected action" set:"defaultBackground=212" set:"defaultForeground=230" set:"defaultPadding=0 3" set:"defaultMargin=0 1" envprefix:"GUM_CONFIRM_SELECTED_"`
|
||||||
|
//nolint:staticcheck
|
||||||
|
UnselectedStyle style.Styles `embed:"" prefix:"unselected." help:"The style of the unselected action" set:"defaultBackground=235" set:"defaultForeground=254" set:"defaultPadding=0 3" set:"defaultMargin=0 1" envprefix:"GUM_CONFIRM_UNSELECTED_"`
|
||||||
|
ShowHelp bool `help:"Show help key binds" negatable:"" default:"true" env:"GUM_CONFIRM_SHOW_HELP"`
|
||||||
|
Timeout time.Duration `help:"Timeout until confirm returns selected value or default if provided" default:"0" env:"GUM_CONFIRM_TIMEOUT"`
|
||||||
}
|
}
|
||||||
|
|
12
cursor/cursor.go
Normal file
12
cursor/cursor.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package cursor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charmbracelet/bubbles/cursor"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Modes maps strings to cursor modes.
|
||||||
|
var Modes = map[string]cursor.Mode{
|
||||||
|
"blink": cursor.CursorBlink,
|
||||||
|
"hide": cursor.CursorHide,
|
||||||
|
"static": cursor.CursorStatic,
|
||||||
|
}
|
11
default.nix
11
default.nix
|
@ -1,7 +1,12 @@
|
||||||
{ pkgs }:
|
{ pkgs }:
|
||||||
|
|
||||||
pkgs.buildGoModule {
|
pkgs.buildGoModule rec {
|
||||||
name = "gum";
|
pname = "gum";
|
||||||
|
version = "0.14.0";
|
||||||
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
vendorSha256="rMqhYZMa0+5F3X4WDm4jE6IwlzOugqm65SAP38bdQx8=";
|
|
||||||
|
vendorHash = "sha256-gDDaKrwlrJyyDzgyGf9iP/XPnOAwpkvIyzCXobXrlF4=";
|
||||||
|
|
||||||
|
ldflags = [ "-s" "-w" "-X=main.Version=${version}" ];
|
||||||
}
|
}
|
||||||
|
|
2
examples/.gitignore
vendored
Normal file
2
examples/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*.gif
|
||||||
|
*.png
|
36
examples/README.md
Normal file
36
examples/README.md
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# Glamour
|
||||||
|
|
||||||
|
A casual introduction. 你好世界!
|
||||||
|
|
||||||
|
## Let's talk about artichokes
|
||||||
|
|
||||||
|
The artichoke is mentioned as a garden
|
||||||
|
plant in the 8th century BC by Homer
|
||||||
|
and Hesiod. The naturally occurring
|
||||||
|
variant of the artichoke, the cardoon,
|
||||||
|
which is native to the Mediterranean
|
||||||
|
area, also has records of use as a
|
||||||
|
food among the ancient Greeks and
|
||||||
|
Romans. Pliny the Elder mentioned
|
||||||
|
growing of 'carduus' in Carthage
|
||||||
|
and Cordoba.
|
||||||
|
|
||||||
|
He holds him with his skinny hand,
|
||||||
|
There was ship,' quoth he.
|
||||||
|
'Hold off! unhand me, grey-beard loon!'
|
||||||
|
An artichoke dropt he.
|
||||||
|
|
||||||
|
## Other foods worth mentioning
|
||||||
|
|
||||||
|
1. Carrots
|
||||||
|
2. Celery
|
||||||
|
3. Tacos
|
||||||
|
• Soft
|
||||||
|
• Hard
|
||||||
|
4. Cucumber
|
||||||
|
|
||||||
|
## Things to eat today
|
||||||
|
|
||||||
|
* Carrots
|
||||||
|
* Ramen
|
||||||
|
* Currywurst
|
26
examples/choose.tape
Normal file
26
examples/choose.tape
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
Output choose.gif
|
||||||
|
|
||||||
|
Set Width 1000
|
||||||
|
Set Height 430
|
||||||
|
Set Shell bash
|
||||||
|
|
||||||
|
Type "gum choose {1..5}"
|
||||||
|
Sleep 500ms
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
Down@250ms 3
|
||||||
|
Sleep 500ms
|
||||||
|
Up@250ms 2
|
||||||
|
Enter
|
||||||
|
Sleep 1.5s
|
||||||
|
Ctrl+L
|
||||||
|
Sleep 500ms
|
||||||
|
Type "gum choose --limit 2 Banana Cherry Orange"
|
||||||
|
Sleep 500ms
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
Type@250ms "jxjxk"
|
||||||
|
Sleep 1s
|
||||||
|
Enter
|
||||||
|
Sleep 2s
|
||||||
|
|
|
@ -10,6 +10,10 @@
|
||||||
#
|
#
|
||||||
# alias gcm='git commit -m "$(gum input)" -m "$(gum write)"'
|
# alias gcm='git commit -m "$(gum input)" -m "$(gum write)"'
|
||||||
|
|
||||||
|
# if [ -z "$(git status -s -uno | grep -v '^ ' | awk '{print $2}')" ]; then
|
||||||
|
# gum confirm "Stage all?" && git add .
|
||||||
|
# fi
|
||||||
|
|
||||||
TYPE=$(gum choose "fix" "feat" "docs" "style" "refactor" "test" "chore" "revert")
|
TYPE=$(gum choose "fix" "feat" "docs" "style" "refactor" "test" "chore" "revert")
|
||||||
SCOPE=$(gum input --placeholder "scope")
|
SCOPE=$(gum input --placeholder "scope")
|
||||||
|
|
||||||
|
|
45
examples/commit.tape
Normal file
45
examples/commit.tape
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
Output commit.gif
|
||||||
|
|
||||||
|
Set Shell "bash"
|
||||||
|
Set FontSize 32
|
||||||
|
Set Width 1200
|
||||||
|
Set Height 600
|
||||||
|
|
||||||
|
Type "./commit.sh" Sleep 500ms Enter
|
||||||
|
|
||||||
|
Sleep 1s
|
||||||
|
Down@250ms 2
|
||||||
|
Sleep 500ms
|
||||||
|
Enter
|
||||||
|
|
||||||
|
Sleep 500ms
|
||||||
|
|
||||||
|
Type "gum"
|
||||||
|
|
||||||
|
Sleep 500ms
|
||||||
|
Enter
|
||||||
|
|
||||||
|
Sleep 1s
|
||||||
|
|
||||||
|
Type "Gum is sooo tasty"
|
||||||
|
Sleep 500ms
|
||||||
|
|
||||||
|
Enter
|
||||||
|
|
||||||
|
Sleep 1s
|
||||||
|
|
||||||
|
Type@65ms "I love bubble gum."
|
||||||
|
Sleep 500ms
|
||||||
|
Alt+Enter
|
||||||
|
Sleep 500ms
|
||||||
|
Alt+Enter
|
||||||
|
Sleep 500ms
|
||||||
|
Type "This commit shows how much I love chewing bubble gum!!!"
|
||||||
|
Sleep 500ms
|
||||||
|
Enter
|
||||||
|
|
||||||
|
Sleep 1s
|
||||||
|
|
||||||
|
Left@400ms 3
|
||||||
|
|
||||||
|
Sleep 1s
|
26
examples/confirm.tape
Normal file
26
examples/confirm.tape
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
Output confirm.gif
|
||||||
|
|
||||||
|
Set Width 1000
|
||||||
|
Set Height 350
|
||||||
|
Set Shell bash
|
||||||
|
|
||||||
|
Sleep 500ms
|
||||||
|
Type "gum confirm && echo 'Me too!' || echo 'Me neither.'"
|
||||||
|
Sleep 1s
|
||||||
|
Enter
|
||||||
|
Sleep 1s
|
||||||
|
Right
|
||||||
|
Sleep 500ms
|
||||||
|
Left
|
||||||
|
Sleep 500ms
|
||||||
|
Enter
|
||||||
|
Sleep 1.5s
|
||||||
|
Ctrl+L
|
||||||
|
Type "gum confirm && echo 'Me too!' || echo 'Me neither.'"
|
||||||
|
Sleep 500ms
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
Right
|
||||||
|
Sleep 500ms
|
||||||
|
Enter
|
||||||
|
Sleep 1s
|
19
examples/customize.tape
Normal file
19
examples/customize.tape
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
Output customize.gif
|
||||||
|
|
||||||
|
Set Width 1000
|
||||||
|
Set Height 350
|
||||||
|
Set Shell bash
|
||||||
|
|
||||||
|
Sleep 1s
|
||||||
|
Type `gum input --cursor.foreground "#F4AC45" \` Enter
|
||||||
|
Type `--prompt.foreground "#04B575" --prompt "What's up? " \` Enter
|
||||||
|
Type `--placeholder "Not much, you?" --value "Not much, you?" \` Enter
|
||||||
|
Type `--width 80` Enter
|
||||||
|
Sleep 1s
|
||||||
|
Ctrl+A
|
||||||
|
Sleep 1s
|
||||||
|
Ctrl+E
|
||||||
|
Sleep 1s
|
||||||
|
Ctrl+U
|
||||||
|
Sleep 1s
|
||||||
|
|
|
@ -5,7 +5,7 @@ NAME=$(gum input --placeholder "What is your name?")
|
||||||
|
|
||||||
echo -e "Well, it is nice to meet you, $(gum style --foreground 212 "$NAME")."
|
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"
|
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
|
clear; echo "What should I do with this information?"; sleep 1
|
||||||
|
|
||||||
READ="Read"; THINK="Think"; DISCARD="Discard"
|
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."
|
clear; echo "One moment, please."
|
||||||
|
|
||||||
|
@ -24,8 +24,7 @@ grep -q "$DISCARD" <<< "$ACTIONS" && gum spin -s monkey --title " Discarding you
|
||||||
|
|
||||||
sleep 1; clear
|
sleep 1; clear
|
||||||
|
|
||||||
echo "What's your favorite $(gum style --foreground 212 "Gum") flavor?"
|
GUM=$(echo -e "Cherry\nGrape\nLime\nOrange" | gum filter --placeholder "Favorite flavor?")
|
||||||
GUM=$(echo -e "Cherry\nGrape\nLime\nOrange" | gum filter)
|
|
||||||
echo "I'll keep that in mind!"
|
echo "I'll keep that in mind!"
|
||||||
|
|
||||||
sleep 1; clear
|
sleep 1; clear
|
||||||
|
@ -39,10 +38,10 @@ CHOICE=$(gum choose --item.foreground 250 "Yes" "No" "It's complicated")
|
||||||
|
|
||||||
sleep 1
|
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
|
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!")
|
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 25 --padding '1 3' --border double --border-foreground 212 "Don't forget to chew some $(gum style --foreground "#04B575" "$GUM") bubble gum.")
|
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"
|
gum join --horizontal "$NICE_MEETING_YOU" "$CHEW_BUBBLE_GUM"
|
||||||
|
|
49
examples/demo.tape
Normal file
49
examples/demo.tape
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
Output ./demo.gif
|
||||||
|
|
||||||
|
Set Shell bash
|
||||||
|
|
||||||
|
Set FontSize 22
|
||||||
|
Set Width 800
|
||||||
|
Set Height 450
|
||||||
|
|
||||||
|
Type "./demo.sh"
|
||||||
|
Enter
|
||||||
|
Sleep 1s
|
||||||
|
Type "Walter"
|
||||||
|
Sleep 500ms
|
||||||
|
Enter
|
||||||
|
|
||||||
|
Sleep 2s
|
||||||
|
|
||||||
|
Type "Nope, sorry!"
|
||||||
|
Sleep 500ms
|
||||||
|
Alt+Enter
|
||||||
|
Sleep 200ms
|
||||||
|
Alt+Enter
|
||||||
|
Sleep 500ms
|
||||||
|
Type "I don't trust you."
|
||||||
|
Sleep 1s
|
||||||
|
Enter
|
||||||
|
|
||||||
|
Sleep 2s
|
||||||
|
|
||||||
|
Type "x" Sleep 250ms Type "j" Sleep 250ms
|
||||||
|
Type "x" Sleep 250ms Type "j" Sleep 250ms
|
||||||
|
Type "x" Sleep 1s
|
||||||
|
|
||||||
|
Enter
|
||||||
|
|
||||||
|
Sleep 6s
|
||||||
|
|
||||||
|
Type "li"
|
||||||
|
Sleep 1s
|
||||||
|
Enter
|
||||||
|
|
||||||
|
Sleep 3s
|
||||||
|
Down@500ms 2
|
||||||
|
Up@500ms 2
|
||||||
|
Sleep 1s
|
||||||
|
Enter
|
||||||
|
|
||||||
|
|
||||||
|
Sleep 6s
|
1
examples/fav.txt
Normal file
1
examples/fav.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Banana
|
15
examples/file.tape
Normal file
15
examples/file.tape
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
Output file.gif
|
||||||
|
Set Width 800
|
||||||
|
Set Height 525
|
||||||
|
Set Shell bash
|
||||||
|
|
||||||
|
Type "gum file .."
|
||||||
|
Enter
|
||||||
|
Sleep 1s
|
||||||
|
Down@150ms 6
|
||||||
|
Sleep 1s
|
||||||
|
Enter
|
||||||
|
Sleep 1s
|
||||||
|
Type "j"
|
||||||
|
Sleep 1s
|
||||||
|
|
4
examples/flavors.txt
Normal file
4
examples/flavors.txt
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
Banana
|
||||||
|
Cherry
|
||||||
|
Orange
|
||||||
|
Strawberry
|
12
examples/format.ansi
Normal file
12
examples/format.ansi
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[38;2;90;86;224m> [0mgum format -t code < main.go
|
||||||
|
|
||||||
|
|
||||||
|
[38;5;204m[0m[38;5;252m[0m [38;5;252m [0m[38;5;252m [0m[38;5;204mpackage[0m[38;5;251m [0m[38;5;251mmain[0m[38;5;251m[0m
|
||||||
|
[0m[38;5;252m[0m [38;5;252m [0m[38;5;252m [0m[38;5;251m[0m
|
||||||
|
[0m[38;5;204m[0m[38;5;252m[0m [38;5;252m [0m[38;5;252m [0m[38;5;204mimport[0m[38;5;251m [0m[38;5;173m"fmt"[0m[38;5;251m[0m
|
||||||
|
[0m[38;5;252m[0m [38;5;252m [0m[38;5;252m [0m[38;5;251m[0m
|
||||||
|
[0m[38;5;39m[0m[38;5;252m[0m [38;5;252m [0m[38;5;252m [0m[38;5;39mfunc[0m[38;5;251m [0m[38;5;42mmain[0m[38;5;187m()[0m[38;5;251m [0m[38;5;187m{[0m[38;5;251m[0m
|
||||||
|
[0m[38;5;252m[0m [38;5;252m [0m[38;5;252m [0m[38;5;251m [0m[38;5;251mfmt[0m[38;5;187m.[0m[38;5;42mPrintln[0m[38;5;187m([0m[38;5;173m"Charm_™ Gum"[0m[38;5;187m)[0m[38;5;251m[0m
|
||||||
|
[0m[38;5;187m[0m[38;5;252m[0m [38;5;252m [0m[38;5;252m [0m[38;5;187m}[0m[38;5;251m[0m
|
||||||
|
[0m[38;5;252m[0m [38;5;252m [0m[38;5;252m [0m[38;5;251m[0m
|
||||||
|
[0m
|
16
examples/input.tape
Normal file
16
examples/input.tape
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
Output input.gif
|
||||||
|
|
||||||
|
Set Width 800
|
||||||
|
Set Height 250
|
||||||
|
Set Shell bash
|
||||||
|
|
||||||
|
Sleep 1s
|
||||||
|
Type `gum input --placeholder "What's up?"`
|
||||||
|
Sleep 1s
|
||||||
|
Enter
|
||||||
|
Sleep 1s
|
||||||
|
Type "Not much, you?"
|
||||||
|
Sleep 1s
|
||||||
|
Enter
|
||||||
|
Sleep 1s
|
||||||
|
|
47
examples/kaomoji.sh
Normal file
47
examples/kaomoji.sh
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# If the user passes '-h', '--help', or 'help' print out a little bit of help.
|
||||||
|
# text.
|
||||||
|
case "$1" in
|
||||||
|
"-h" | "--help" | "help")
|
||||||
|
printf 'Generate kaomojis on request.\n\n'
|
||||||
|
printf 'Usage: %s [kind]\n' "$(basename "$0")"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# The user can pass an argument like "bear" or "angry" to specify the general
|
||||||
|
# kind of Kaomoji produced.
|
||||||
|
sentiment=""
|
||||||
|
if [[ $1 != "" ]]; then
|
||||||
|
sentiment=" $1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ask mods to generate Kaomojis. Save the output in a variable.
|
||||||
|
kaomoji="$(mods "generate 10${sentiment} kaomojis. number them and put each one on its own line.")"
|
||||||
|
if [[ $kaomoji == "" ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pipe mods output to gum so the user can choose the perfect kaomoji. Save that
|
||||||
|
# choice in a variable. Also note that we're using cut to drop the item number
|
||||||
|
# in front of the Kaomoji.
|
||||||
|
choice="$(echo "$kaomoji" | gum choose | cut -d ' ' -f 2)"
|
||||||
|
if [[ $choice == "" ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If xsel (X11) or pbcopy (macOS) exists, copy to the clipboard. If not, just
|
||||||
|
# print the Kaomoji.
|
||||||
|
if command -v xsel &> /dev/null; then
|
||||||
|
printf '%s' "$choice" | xclip -sel clip # X11
|
||||||
|
elif command -v pbcopy &> /dev/null; then
|
||||||
|
printf '%s' "$choice" | pbcopy # macOS
|
||||||
|
else
|
||||||
|
# We can't copy, so just print it out.
|
||||||
|
printf 'Here you go: %s\n' "$choice"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# We're done!
|
||||||
|
printf 'Copied %s to the clipboard\n' "$choice"
|
7
examples/main.go
Normal file
7
examples/main.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("Charm_™ Gum")
|
||||||
|
}
|
15
examples/pager.tape
Normal file
15
examples/pager.tape
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
Output pager.gif
|
||||||
|
|
||||||
|
Set Shell bash
|
||||||
|
Set Width 900
|
||||||
|
Set Height 750
|
||||||
|
|
||||||
|
Sleep 1s
|
||||||
|
Type "gum pager < README.md"
|
||||||
|
Enter
|
||||||
|
Sleep 1.5s
|
||||||
|
Down@100ms 25
|
||||||
|
Sleep 1s
|
||||||
|
Up@100ms 25
|
||||||
|
Sleep 3s
|
||||||
|
|
13
examples/spin.tape
Normal file
13
examples/spin.tape
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
Output spin.gif
|
||||||
|
|
||||||
|
Set Shell bash
|
||||||
|
Set Width 1200
|
||||||
|
Set Height 300
|
||||||
|
Set FontSize 36
|
||||||
|
|
||||||
|
Sleep 500ms
|
||||||
|
Type `gum spin --title "Buying Gum..." -- sleep 5`
|
||||||
|
Sleep 1s
|
||||||
|
Enter
|
||||||
|
Sleep 4s
|
||||||
|
|
2
examples/story.txt
Normal file
2
examples/story.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Once upon a time
|
||||||
|
In a land far, far away....
|
|
@ -40,4 +40,13 @@ gum style --foreground 99 --border double --border-foreground 99 --padding "1 2"
|
||||||
|
|
||||||
# Write
|
# Write
|
||||||
gum write
|
gum write
|
||||||
gum write --width 40 --height 3 --placeholder "Type whatever you want" --prompt "| " --show-cursor-line --show-line-numbers --value "Something..." --base.padding 1 --cursor.foreground 99 --prompt.foreground 99
|
gum write --width 40 --height 6 --placeholder "Type whatever you want" --prompt "| " --show-cursor-line --show-line-numbers --value "Something..." --base.padding 1 --cursor.foreground 99 --prompt.foreground 99
|
||||||
|
|
||||||
|
# Table
|
||||||
|
gum table < table/example.csv
|
||||||
|
|
||||||
|
# Pager
|
||||||
|
gum pager < README.md
|
||||||
|
|
||||||
|
# File
|
||||||
|
gum file
|
||||||
|
|
21
examples/write.tape
Normal file
21
examples/write.tape
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
Output write.gif
|
||||||
|
|
||||||
|
Set Width 800
|
||||||
|
Set Height 350
|
||||||
|
Set Shell bash
|
||||||
|
|
||||||
|
Sleep 500ms
|
||||||
|
Type "gum write > story.txt"
|
||||||
|
Enter
|
||||||
|
Sleep 1s
|
||||||
|
Type "Once upon a time"
|
||||||
|
Sleep 1s
|
||||||
|
Alt+Enter
|
||||||
|
Type "In a land far, far away...."
|
||||||
|
Sleep 500ms
|
||||||
|
Enter
|
||||||
|
Sleep 1s
|
||||||
|
Type "cat story.txt"
|
||||||
|
Enter
|
||||||
|
Sleep 2s
|
||||||
|
|
63
file/command.go
Normal file
63
file/command.go
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run is the interface to picking a file.
|
||||||
|
func (o Options) Run() error {
|
||||||
|
if !o.File && !o.Directory {
|
||||||
|
return errors.New("at least one between --file and --directory must be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.Path == "" {
|
||||||
|
o.Path = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := filepath.Abs(o.Path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("file not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
keymap := huh.NewDefaultKeyMap()
|
||||||
|
keymap.FilePicker.Open.SetEnabled(false)
|
||||||
|
|
||||||
|
// XXX: These should be file selected specific.
|
||||||
|
theme.Focused.TextInput.Placeholder = o.PermissionsStyle.ToLipgloss()
|
||||||
|
theme.Focused.TextInput.Prompt = o.CursorStyle.ToLipgloss()
|
||||||
|
|
||||||
|
err = huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewFilePicker().
|
||||||
|
Picking(true).
|
||||||
|
CurrentDirectory(path).
|
||||||
|
DirAllowed(o.Directory).
|
||||||
|
FileAllowed(o.File).
|
||||||
|
Height(o.Height).
|
||||||
|
ShowHidden(o.All).
|
||||||
|
Value(&path),
|
||||||
|
),
|
||||||
|
).
|
||||||
|
WithShowHelp(o.ShowHelp).
|
||||||
|
WithKeyMap(keymap).
|
||||||
|
WithTheme(theme).
|
||||||
|
Run()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(path)
|
||||||
|
return nil
|
||||||
|
}
|
31
file/options.go
Normal file
31
file/options.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/gum/style"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options are the options for the file command.
|
||||||
|
type Options struct {
|
||||||
|
// Path is the path to the folder / directory to begin traversing.
|
||||||
|
Path string `arg:"" optional:"" name:"path" help:"The path to the folder to begin traversing" env:"GUM_FILE_PATH"`
|
||||||
|
// Cursor is the character to display in front of the current selected items.
|
||||||
|
Cursor string `short:"c" help:"The cursor character" default:">" env:"GUM_FILE_CURSOR"`
|
||||||
|
All bool `short:"a" help:"Show hidden and 'dot' files" default:"false" env:"GUM_FILE_ALL"`
|
||||||
|
File bool `help:"Allow files selection" default:"true" env:"GUM_FILE_FILE"`
|
||||||
|
Directory bool `help:"Allow directories selection" default:"false" env:"GUM_FILE_DIRECTORY"`
|
||||||
|
ShowHelp bool `help:"Show help key binds" negatable:"" default:"true" env:"GUM_FILE_SHOW_HELP"`
|
||||||
|
|
||||||
|
Height int `help:"Maximum number of files to display" default:"10" env:"GUM_FILE_HEIGHT"`
|
||||||
|
CursorStyle style.Styles `embed:"" prefix:"cursor." help:"The cursor style" set:"defaultForeground=212" envprefix:"GUM_FILE_CURSOR_"`
|
||||||
|
SymlinkStyle style.Styles `embed:"" prefix:"symlink." help:"The style to use for symlinks" set:"defaultForeground=36" envprefix:"GUM_FILE_SYMLINK_"`
|
||||||
|
DirectoryStyle style.Styles `embed:"" prefix:"directory." help:"The style to use for directories" set:"defaultForeground=99" envprefix:"GUM_FILE_DIRECTORY_"`
|
||||||
|
FileStyle style.Styles `embed:"" prefix:"file." help:"The style to use for files" envprefix:"GUM_FILE_FILE_"`
|
||||||
|
PermissionsStyle style.Styles `embed:"" prefix:"permissions." help:"The style to use for permissions" set:"defaultForeground=244" envprefix:"GUM_FILE_PERMISSIONS_"`
|
||||||
|
//nolint:staticcheck
|
||||||
|
SelectedStyle style.Styles `embed:"" prefix:"selected." help:"The style to use for the selected item" set:"defaultBold=true" set:"defaultForeground=212" envprefix:"GUM_FILE_SELECTED_"`
|
||||||
|
//nolint:staticcheck
|
||||||
|
FileSizeStyle style.Styles `embed:"" prefix:"file-size." help:"The style to use for file sizes" set:"defaultWidth=8" set:"defaultAlign=right" set:"defaultForeground=240" envprefix:"GUM_FILE_FILE_SIZE_"`
|
||||||
|
Timeout time.Duration `help:"Timeout until command aborts without a selection" default:"0" env:"GUM_FILE_TIMEOUT"`
|
||||||
|
}
|
|
@ -6,16 +6,16 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
"github.com/charmbracelet/bubbles/viewport"
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
"github.com/sahilm/fuzzy"
|
"github.com/sahilm/fuzzy"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/gum/ansi"
|
||||||
"github.com/charmbracelet/gum/internal/exit"
|
"github.com/charmbracelet/gum/internal/exit"
|
||||||
"github.com/charmbracelet/gum/internal/files"
|
"github.com/charmbracelet/gum/internal/files"
|
||||||
"github.com/charmbracelet/gum/internal/stdin"
|
"github.com/charmbracelet/gum/internal/stdin"
|
||||||
"github.com/charmbracelet/gum/style"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run provides a shell script interface for filtering through options, powered
|
// Run provides a shell script interface for filtering through options, powered
|
||||||
|
@ -26,25 +26,29 @@ func (o Options) Run() error {
|
||||||
|
|
||||||
i.Prompt = o.Prompt
|
i.Prompt = o.Prompt
|
||||||
i.PromptStyle = o.PromptStyle.ToLipgloss()
|
i.PromptStyle = o.PromptStyle.ToLipgloss()
|
||||||
|
i.PlaceholderStyle = o.PlaceholderStyle.ToLipgloss()
|
||||||
i.Placeholder = o.Placeholder
|
i.Placeholder = o.Placeholder
|
||||||
i.Width = o.Width
|
i.Width = o.Width
|
||||||
|
|
||||||
v := viewport.New(o.Width, o.Height)
|
v := viewport.New(o.Width, o.Height)
|
||||||
|
|
||||||
var choices []string
|
if len(o.Options) == 0 {
|
||||||
if input, _ := stdin.Read(); input != "" {
|
if input, _ := stdin.Read(); input != "" {
|
||||||
input = strings.TrimSpace(input)
|
o.Options = strings.Split(input, "\n")
|
||||||
if input != "" {
|
} else {
|
||||||
choices = strings.Split(input, "\n")
|
o.Options = files.List()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
choices = files.List()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(choices) == 0 {
|
if len(o.Options) == 0 {
|
||||||
return errors.New("no options provided, see `gum filter --help`")
|
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)}
|
options := []tea.ProgramOption{tea.WithOutput(os.Stderr)}
|
||||||
if o.Height == 0 {
|
if o.Height == 0 {
|
||||||
options = append(options, tea.WithAltScreen())
|
options = append(options, tea.WithAltScreen())
|
||||||
|
@ -53,19 +57,25 @@ func (o Options) Run() error {
|
||||||
var matches []fuzzy.Match
|
var matches []fuzzy.Match
|
||||||
if o.Value != "" {
|
if o.Value != "" {
|
||||||
i.SetValue(o.Value)
|
i.SetValue(o.Value)
|
||||||
matches = fuzzy.Find(o.Value, choices)
|
}
|
||||||
} else {
|
switch {
|
||||||
matches = matchAll(choices)
|
case o.Value != "" && o.Fuzzy:
|
||||||
|
matches = fuzzy.Find(o.Value, o.Options)
|
||||||
|
case o.Value != "" && !o.Fuzzy:
|
||||||
|
matches = exactMatches(o.Value, o.Options)
|
||||||
|
default:
|
||||||
|
matches = matchAll(o.Options)
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.NoLimit {
|
if o.NoLimit {
|
||||||
o.Limit = len(choices)
|
o.Limit = len(o.Options)
|
||||||
}
|
}
|
||||||
|
|
||||||
p := tea.NewProgram(model{
|
p := tea.NewProgram(model{
|
||||||
choices: choices,
|
choices: o.Options,
|
||||||
indicator: o.Indicator,
|
indicator: o.Indicator,
|
||||||
matches: matches,
|
matches: matches,
|
||||||
|
header: o.Header,
|
||||||
textinput: i,
|
textinput: i,
|
||||||
viewport: &v,
|
viewport: &v,
|
||||||
indicatorStyle: o.IndicatorStyle.ToLipgloss(),
|
indicatorStyle: o.IndicatorStyle.ToLipgloss(),
|
||||||
|
@ -74,38 +84,55 @@ func (o Options) Run() error {
|
||||||
unselectedPrefixStyle: o.UnselectedPrefixStyle.ToLipgloss(),
|
unselectedPrefixStyle: o.UnselectedPrefixStyle.ToLipgloss(),
|
||||||
unselectedPrefix: o.UnselectedPrefix,
|
unselectedPrefix: o.UnselectedPrefix,
|
||||||
matchStyle: o.MatchStyle.ToLipgloss(),
|
matchStyle: o.MatchStyle.ToLipgloss(),
|
||||||
|
headerStyle: o.HeaderStyle.ToLipgloss(),
|
||||||
textStyle: o.TextStyle.ToLipgloss(),
|
textStyle: o.TextStyle.ToLipgloss(),
|
||||||
|
cursorTextStyle: o.CursorTextStyle.ToLipgloss(),
|
||||||
height: o.Height,
|
height: o.Height,
|
||||||
selected: make(map[string]struct{}),
|
selected: make(map[string]struct{}),
|
||||||
limit: o.Limit,
|
limit: o.Limit,
|
||||||
|
reverse: o.Reverse,
|
||||||
|
fuzzy: o.Fuzzy,
|
||||||
|
timeout: o.Timeout,
|
||||||
|
hasTimeout: o.Timeout > 0,
|
||||||
|
sort: o.Sort,
|
||||||
}, options...)
|
}, options...)
|
||||||
|
|
||||||
tm, err := p.StartReturningModel()
|
tm, err := p.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to run filter: %w", err)
|
return fmt.Errorf("unable to run filter: %w", err)
|
||||||
}
|
}
|
||||||
m := tm.(model)
|
m := tm.(model)
|
||||||
|
|
||||||
if m.aborted {
|
if m.aborted {
|
||||||
return exit.ErrAborted
|
return exit.ErrAborted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isTTY := isatty.IsTerminal(os.Stdout.Fd())
|
||||||
|
|
||||||
// allSelections contains values only if limit is greater
|
// allSelections contains values only if limit is greater
|
||||||
// than 1 or if flag --no-limit is passed, hence there is
|
// than 1 or if flag --no-limit is passed, hence there is
|
||||||
// no need to further checks
|
// no need to further checks
|
||||||
if len(m.selected) > 0 {
|
if len(m.selected) > 0 {
|
||||||
for k := range m.selected {
|
o.checkSelected(m, isTTY)
|
||||||
fmt.Println(k)
|
|
||||||
}
|
|
||||||
} else if len(m.matches) > m.cursor && m.cursor >= 0 {
|
} else if len(m.matches) > m.cursor && m.cursor >= 0 {
|
||||||
fmt.Println(m.matches[m.cursor].Str)
|
if isTTY {
|
||||||
|
fmt.Println(m.matches[m.cursor].Str)
|
||||||
|
} else {
|
||||||
|
fmt.Println(ansi.Strip(m.matches[m.cursor].Str))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !o.Strict && len(m.textinput.Value()) != 0 && len(m.matches) == 0 {
|
||||||
|
fmt.Println(m.textinput.Value())
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeReset hook. Used to unclutter style flags.
|
func (o Options) checkSelected(m model, isTTY bool) {
|
||||||
func (o Options) BeforeReset(ctx *kong.Context) error {
|
for k := range m.selected {
|
||||||
style.HideFlags(ctx)
|
if isTTY {
|
||||||
return nil
|
fmt.Println(k)
|
||||||
|
} else {
|
||||||
|
fmt.Println(ansi.Strip(k))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
180
filter/filter.go
180
filter/filter.go
|
@ -12,12 +12,14 @@ package filter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/gum/timeout"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
"github.com/charmbracelet/bubbles/viewport"
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/mattn/go-runewidth"
|
|
||||||
"github.com/sahilm/fuzzy"
|
"github.com/sahilm/fuzzy"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,6 +29,7 @@ type model struct {
|
||||||
choices []string
|
choices []string
|
||||||
matches []fuzzy.Match
|
matches []fuzzy.Match
|
||||||
cursor int
|
cursor int
|
||||||
|
header string
|
||||||
selected map[string]struct{}
|
selected map[string]struct{}
|
||||||
limit int
|
limit int
|
||||||
numSelected int
|
numSelected int
|
||||||
|
@ -36,30 +39,58 @@ type model struct {
|
||||||
height int
|
height int
|
||||||
aborted bool
|
aborted bool
|
||||||
quitting bool
|
quitting bool
|
||||||
|
headerStyle lipgloss.Style
|
||||||
matchStyle lipgloss.Style
|
matchStyle lipgloss.Style
|
||||||
textStyle lipgloss.Style
|
textStyle lipgloss.Style
|
||||||
|
cursorTextStyle lipgloss.Style
|
||||||
indicatorStyle lipgloss.Style
|
indicatorStyle lipgloss.Style
|
||||||
selectedPrefixStyle lipgloss.Style
|
selectedPrefixStyle lipgloss.Style
|
||||||
unselectedPrefixStyle lipgloss.Style
|
unselectedPrefixStyle lipgloss.Style
|
||||||
|
reverse bool
|
||||||
|
fuzzy bool
|
||||||
|
sort bool
|
||||||
|
timeout time.Duration
|
||||||
|
hasTimeout bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) Init() tea.Cmd { return nil }
|
func (m model) Init() tea.Cmd {
|
||||||
|
return timeout.Init(m.timeout, nil)
|
||||||
|
}
|
||||||
func (m model) View() string {
|
func (m model) View() string {
|
||||||
if m.quitting {
|
if m.quitting {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
|
var lineTextStyle lipgloss.Style
|
||||||
|
|
||||||
|
// For reverse layout, if the number of matches is less than the viewport
|
||||||
|
// height, we need to offset the matches so that the first match is at the
|
||||||
|
// bottom edge of the viewport instead of in the middle.
|
||||||
|
if m.reverse && len(m.matches) < m.viewport.Height {
|
||||||
|
s.WriteString(strings.Repeat("\n", m.viewport.Height-len(m.matches)))
|
||||||
|
}
|
||||||
|
|
||||||
// Since there are matches, display them so that the user can see, in real
|
// Since there are matches, display them so that the user can see, in real
|
||||||
// time, what they are searching for.
|
// time, what they are searching for.
|
||||||
for i, match := range m.matches {
|
last := len(m.matches) - 1
|
||||||
|
for i := range m.matches {
|
||||||
|
// For reverse layout, the matches are displayed in reverse order.
|
||||||
|
if m.reverse {
|
||||||
|
i = last - i
|
||||||
|
}
|
||||||
|
match := m.matches[i]
|
||||||
|
|
||||||
// If this is the current selected index, we add a small indicator to
|
// If this is the current selected index, we add a small indicator to
|
||||||
// represent it. Otherwise, simply pad the string.
|
// represent it. Otherwise, simply pad the string.
|
||||||
|
// The line's text style is set depending on whether or not the cursor
|
||||||
|
// points to this line.
|
||||||
if i == m.cursor {
|
if i == m.cursor {
|
||||||
s.WriteString(m.indicatorStyle.Render(m.indicator))
|
s.WriteString(m.indicatorStyle.Render(m.indicator))
|
||||||
|
lineTextStyle = m.cursorTextStyle
|
||||||
} else {
|
} else {
|
||||||
s.WriteString(strings.Repeat(" ", runewidth.StringWidth(m.indicator)))
|
s.WriteString(strings.Repeat(" ", lipgloss.Width(m.indicator)))
|
||||||
|
lineTextStyle = m.textStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are multiple selections mark them, otherwise leave an empty space
|
// If there are multiple selections mark them, otherwise leave an empty space
|
||||||
|
@ -74,20 +105,27 @@ func (m model) View() string {
|
||||||
// For this match, there are a certain number of characters that have
|
// For this match, there are a certain number of characters that have
|
||||||
// caused the match. i.e. fuzzy matching.
|
// caused the match. i.e. fuzzy matching.
|
||||||
// We should indicate to the users which characters are being matched.
|
// We should indicate to the users which characters are being matched.
|
||||||
var mi = 0
|
mi := 0
|
||||||
|
var buf strings.Builder
|
||||||
for ci, c := range match.Str {
|
for ci, c := range match.Str {
|
||||||
// Check if the current character index matches the current matched
|
// Check if the current character index matches the current matched
|
||||||
// index. If so, color the character to indicate a match.
|
// index. If so, color the character to indicate a match.
|
||||||
if mi < len(match.MatchedIndexes) && ci == match.MatchedIndexes[mi] {
|
if mi < len(match.MatchedIndexes) && ci == match.MatchedIndexes[mi] {
|
||||||
|
// Flush text buffer.
|
||||||
|
s.WriteString(lineTextStyle.Render(buf.String()))
|
||||||
|
buf.Reset()
|
||||||
|
|
||||||
s.WriteString(m.matchStyle.Render(string(c)))
|
s.WriteString(m.matchStyle.Render(string(c)))
|
||||||
// We have matched this character, so we never have to check it
|
// We have matched this character, so we never have to check it
|
||||||
// again. Move on to the next match.
|
// again. Move on to the next match.
|
||||||
mi++
|
mi++
|
||||||
} else {
|
} else {
|
||||||
// Not a match, simply show the character, unstyled.
|
// Not a match, buffer a regular character.
|
||||||
s.WriteString(m.textStyle.Render(string(c)))
|
buf.WriteRune(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Flush text buffer.
|
||||||
|
s.WriteString(lineTextStyle.Render(buf.String()))
|
||||||
|
|
||||||
// We have finished displaying the match with all of it's matched
|
// We have finished displaying the match with all of it's matched
|
||||||
// characters highlighted and the rest filled in.
|
// characters highlighted and the rest filled in.
|
||||||
|
@ -98,17 +136,48 @@ func (m model) View() string {
|
||||||
m.viewport.SetContent(s.String())
|
m.viewport.SetContent(s.String())
|
||||||
|
|
||||||
// View the input and the filtered choices
|
// View the input and the filtered choices
|
||||||
return m.textinput.View() + "\n" + m.viewport.View()
|
header := m.headerStyle.Render(m.header)
|
||||||
|
if m.reverse {
|
||||||
|
view := m.viewport.View() + "\n" + m.textinput.View()
|
||||||
|
if m.header != "" {
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, view, header)
|
||||||
|
}
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
view := m.textinput.View() + "\n" + m.viewport.View()
|
||||||
|
if m.header != "" {
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, header, view)
|
||||||
|
}
|
||||||
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
switch msg := msg.(type) {
|
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:
|
case tea.WindowSizeMsg:
|
||||||
if m.height == 0 || m.height > msg.Height {
|
if m.height == 0 || m.height > msg.Height {
|
||||||
m.viewport.Height = msg.Height - lipgloss.Height(m.textinput.View())
|
m.viewport.Height = msg.Height - lipgloss.Height(m.textinput.View())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make place in the view port if header is set
|
||||||
|
if m.header != "" {
|
||||||
|
m.viewport.Height = m.viewport.Height - lipgloss.Height(m.headerStyle.Render(m.header))
|
||||||
|
}
|
||||||
m.viewport.Width = msg.Width
|
m.viewport.Width = msg.Width
|
||||||
|
if m.reverse {
|
||||||
|
m.viewport.YOffset = clamp(0, len(m.matches), len(m.matches)-m.viewport.Height)
|
||||||
|
}
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c", "esc":
|
case "ctrl+c", "esc":
|
||||||
|
@ -134,20 +203,47 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
m.ToggleSelection()
|
m.ToggleSelection()
|
||||||
m.CursorUp()
|
m.CursorUp()
|
||||||
|
case "ctrl+@":
|
||||||
|
if m.limit == 1 {
|
||||||
|
break // no op
|
||||||
|
}
|
||||||
|
m.ToggleSelection()
|
||||||
default:
|
default:
|
||||||
m.textinput, cmd = m.textinput.Update(msg)
|
m.textinput, cmd = m.textinput.Update(msg)
|
||||||
|
|
||||||
// A character was entered, this likely means that the text input
|
// yOffsetFromBottom is the number of lines from the bottom of the
|
||||||
// has changed. This suggests that the matches are outdated, so
|
// list to the top of the viewport. This is used to keep the viewport
|
||||||
// update them, with a fuzzy finding algorithm provided by
|
// at a constant position when the number of matches are reduced
|
||||||
// https://github.com/sahilm/fuzzy
|
// in the reverse layout.
|
||||||
m.matches = fuzzy.Find(m.textinput.Value(), m.choices)
|
var yOffsetFromBottom int
|
||||||
|
if m.reverse {
|
||||||
|
yOffsetFromBottom = max(0, len(m.matches)-m.viewport.YOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A character was entered, this likely means that the text input has
|
||||||
|
// changed. This suggests that the matches are outdated, so update them.
|
||||||
|
if m.fuzzy {
|
||||||
|
if m.sort {
|
||||||
|
m.matches = fuzzy.Find(m.textinput.Value(), m.choices)
|
||||||
|
} else {
|
||||||
|
m.matches = fuzzy.FindNoSort(m.textinput.Value(), m.choices)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m.matches = exactMatches(m.textinput.Value(), m.choices)
|
||||||
|
}
|
||||||
|
|
||||||
// If the search field is empty, let's not display the matches
|
// If the search field is empty, let's not display the matches
|
||||||
// (none), but rather display all possible choices.
|
// (none), but rather display all possible choices.
|
||||||
if m.textinput.Value() == "" {
|
if m.textinput.Value() == "" {
|
||||||
m.matches = matchAll(m.choices)
|
m.matches = matchAll(m.choices)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For reverse layout, we need to offset the viewport so that the
|
||||||
|
// it remains at a constant position relative to the cursor.
|
||||||
|
if m.reverse {
|
||||||
|
maxYOffset := max(0, len(m.matches)-m.viewport.Height)
|
||||||
|
m.viewport.YOffset = clamp(0, maxYOffset, len(m.matches)-yOffsetFromBottom)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,16 +254,30 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *model) CursorUp() {
|
func (m *model) CursorUp() {
|
||||||
m.cursor = clamp(0, len(m.matches)-1, m.cursor-1)
|
if m.reverse {
|
||||||
if m.cursor < m.viewport.YOffset {
|
m.cursor = clamp(0, len(m.matches)-1, m.cursor+1)
|
||||||
m.viewport.SetYOffset(m.cursor)
|
if len(m.matches)-m.cursor <= m.viewport.YOffset {
|
||||||
|
m.viewport.SetYOffset(len(m.matches) - m.cursor - 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m.cursor = clamp(0, len(m.matches)-1, m.cursor-1)
|
||||||
|
if m.cursor < m.viewport.YOffset {
|
||||||
|
m.viewport.SetYOffset(m.cursor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *model) CursorDown() {
|
func (m *model) CursorDown() {
|
||||||
m.cursor = clamp(0, len(m.matches)-1, m.cursor+1)
|
if m.reverse {
|
||||||
if m.cursor >= m.viewport.YOffset+m.viewport.Height {
|
m.cursor = clamp(0, len(m.matches)-1, m.cursor-1)
|
||||||
m.viewport.LineDown(1)
|
if len(m.matches)-m.cursor > m.viewport.Height+m.viewport.YOffset {
|
||||||
|
m.viewport.LineDown(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m.cursor = clamp(0, len(m.matches)-1, m.cursor+1)
|
||||||
|
if m.cursor >= m.viewport.YOffset+m.viewport.Height {
|
||||||
|
m.viewport.LineDown(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,13 +292,36 @@ func (m *model) ToggleSelection() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchAll(options []string) []fuzzy.Match {
|
func matchAll(options []string) []fuzzy.Match {
|
||||||
var matches = make([]fuzzy.Match, len(options))
|
matches := make([]fuzzy.Match, len(options))
|
||||||
for i, option := range options {
|
for i, option := range options {
|
||||||
matches[i] = fuzzy.Match{Str: option}
|
matches[i] = fuzzy.Match{Str: option}
|
||||||
}
|
}
|
||||||
return matches
|
return matches
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func exactMatches(search string, choices []string) []fuzzy.Match {
|
||||||
|
matches := fuzzy.Matches{}
|
||||||
|
for i, choice := range choices {
|
||||||
|
search = strings.ToLower(search)
|
||||||
|
matchedString := strings.ToLower(choice)
|
||||||
|
|
||||||
|
index := strings.Index(matchedString, search)
|
||||||
|
if index >= 0 {
|
||||||
|
matchedIndexes := []int{}
|
||||||
|
for s := range search {
|
||||||
|
matchedIndexes = append(matchedIndexes, index+s)
|
||||||
|
}
|
||||||
|
matches = append(matches, fuzzy.Match{
|
||||||
|
Str: choice,
|
||||||
|
Index: i,
|
||||||
|
MatchedIndexes: matchedIndexes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
//nolint:unparam
|
//nolint:unparam
|
||||||
func clamp(min, max, val int) int {
|
func clamp(min, max, val int) int {
|
||||||
if val < min {
|
if val < min {
|
||||||
|
@ -199,3 +332,10 @@ func clamp(min, max, val int) int {
|
||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func max(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
|
@ -1,23 +1,39 @@
|
||||||
package filter
|
package filter
|
||||||
|
|
||||||
import "github.com/charmbracelet/gum/style"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/gum/style"
|
||||||
|
)
|
||||||
|
|
||||||
// Options is the customization options for the filter command.
|
// Options is the customization options for the filter command.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Indicator string `help:"Character for selection" default:"•" env:"GUM_FILTER_INDICATOR"`
|
Options []string `arg:"" optional:"" help:"Options to filter."`
|
||||||
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"`
|
Indicator string `help:"Character for selection" default:"•" env:"GUM_FILTER_INDICATOR"`
|
||||||
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
|
IndicatorStyle style.Styles `embed:"" prefix:"indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_INDICATOR_"`
|
||||||
SelectedPrefix string `help:"Character to indicate selected items (hidden if limit is 1)" default:" ◉ " env:"GUM_FILTER_SELECTED_PREFIX"`
|
Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"`
|
||||||
SelectedPrefixStyle style.Styles `embed:"" prefix:"selected-indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_SELECTED_PREFIX_"`
|
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
|
||||||
UnselectedPrefix string `help:"Character to indicate unselected items (hidden if limit is 1)" default:" ○ " env:"GUM_FILTER_UNSELECTED_PREFIX"`
|
SelectIfOne bool `help:"Select the given option if there is only one" group:"Selection"`
|
||||||
UnselectedPrefixStyle style.Styles `embed:"" prefix:"unselected-prefix." set:"defaultForeground=240" envprefix:"GUM_FILTER_UNSELECTED_PREFIX_"`
|
Strict bool `help:"Only returns if anything matched. Otherwise return Filter" negatable:"true" default:"true" group:"Selection"`
|
||||||
TextStyle style.Styles `embed:"" prefix:"text." envprefix:"GUM_FILTER_TEXT_"`
|
SelectedPrefix string `help:"Character to indicate selected items (hidden if limit is 1)" default:" ◉ " env:"GUM_FILTER_SELECTED_PREFIX"`
|
||||||
MatchStyle style.Styles `embed:"" prefix:"match." set:"defaultForeground=212" envprefix:"GUM_FILTER_MATCH_"`
|
SelectedPrefixStyle style.Styles `embed:"" prefix:"selected-indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_SELECTED_PREFIX_"`
|
||||||
Placeholder string `help:"Placeholder value" default:"Filter..." env:"GUM_FILTER_PLACEHOLDER"`
|
UnselectedPrefix string `help:"Character to indicate unselected items (hidden if limit is 1)" default:" ○ " env:"GUM_FILTER_UNSELECTED_PREFIX"`
|
||||||
Prompt string `help:"Prompt to display" default:"> " env:"GUM_FILTER_PROMPT"`
|
UnselectedPrefixStyle style.Styles `embed:"" prefix:"unselected-prefix." set:"defaultForeground=240" envprefix:"GUM_FILTER_UNSELECTED_PREFIX_"`
|
||||||
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=240" envprefix:"GUM_FILTER_PROMPT_"`
|
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_FILTER_HEADER_"`
|
||||||
Width int `help:"Input width" default:"20" env:"GUM_FILTER_WIDTH"`
|
Header string `help:"Header value" default:"" env:"GUM_FILTER_HEADER"`
|
||||||
Height int `help:"Input height" default:"0" env:"GUM_FILTER_HEIGHT"`
|
TextStyle style.Styles `embed:"" prefix:"text." envprefix:"GUM_FILTER_TEXT_"`
|
||||||
Value string `help:"Initial filter value" default:"" env:"GUM_FILTER_VALUE"`
|
CursorTextStyle style.Styles `embed:"" prefix:"cursor-text." envprefix:"GUM_FILTER_CURSOR_TEXT_"`
|
||||||
|
MatchStyle style.Styles `embed:"" prefix:"match." set:"defaultForeground=212" envprefix:"GUM_FILTER_MATCH_"`
|
||||||
|
Placeholder string `help:"Placeholder value" default:"Filter..." env:"GUM_FILTER_PLACEHOLDER"`
|
||||||
|
Prompt string `help:"Prompt to display" default:"> " env:"GUM_FILTER_PROMPT"`
|
||||||
|
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=240" envprefix:"GUM_FILTER_PROMPT_"`
|
||||||
|
PlaceholderStyle style.Styles `embed:"" prefix:"placeholder." set:"defaultForeground=240" envprefix:"GUM_FILTER_PLACEHOLDER_"`
|
||||||
|
Width int `help:"Input width" default:"20" env:"GUM_FILTER_WIDTH"`
|
||||||
|
Height int `help:"Input height" default:"0" env:"GUM_FILTER_HEIGHT"`
|
||||||
|
Value string `help:"Initial filter value" default:"" env:"GUM_FILTER_VALUE"`
|
||||||
|
Reverse bool `help:"Display from the bottom of the screen" env:"GUM_FILTER_REVERSE"`
|
||||||
|
Fuzzy bool `help:"Enable fuzzy matching" default:"true" env:"GUM_FILTER_FUZZY" negatable:""`
|
||||||
|
Sort bool `help:"Sort the results" default:"true" env:"GUM_FILTER_SORT" negatable:""`
|
||||||
|
Timeout time.Duration `help:"Timeout until filter command aborts" default:"0" env:"GUM_FILTER_TIMEOUT"`
|
||||||
}
|
}
|
||||||
|
|
32
flake.lock
32
flake.lock
|
@ -1,12 +1,15 @@
|
||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-utils": {
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1659877975,
|
"lastModified": 1710146030,
|
||||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -17,16 +20,16 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1664107978,
|
"lastModified": 1715447595,
|
||||||
"narHash": "sha256-31I9XnIjXkUa62BM1Zr/ylKMf9eVO5PtoX2mGpmB7/U=",
|
"narHash": "sha256-VsVAUQOj/cS1LCOmMjAGeRksXIAdPnFIjCQ0XLkCsT0=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "72783a2d0dbbf030bff1537873dd5b85b3fb332f",
|
"rev": "062ca2a9370a27a35c524dc82d540e6e9824b652",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"ref": "nixos-22.05",
|
"ref": "nixos-unstable",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
@ -36,6 +39,21 @@
|
||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
description = "A tool for glamorous shell scripts";
|
description = "A tool for glamorous shell scripts";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = github:nixos/nixpkgs/nixos-22.05;
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
flake-utils.url = github:numtide/flake-utils;
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils }:
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
|
|
@ -17,30 +17,30 @@ import (
|
||||||
"github.com/charmbracelet/gum/internal/stdin"
|
"github.com/charmbracelet/gum/internal/stdin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Func is a function that formats some text.
|
|
||||||
type Func func(string) (string, error)
|
|
||||||
|
|
||||||
var formatType = map[string]Func{
|
|
||||||
"code": code,
|
|
||||||
"emoji": emoji,
|
|
||||||
"markdown": markdown,
|
|
||||||
"template": template,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run runs the format command.
|
// Run runs the format command.
|
||||||
func (o Options) Run() error {
|
func (o Options) Run() error {
|
||||||
var input string
|
var input, output string
|
||||||
|
var err error
|
||||||
if len(o.Template) > 0 {
|
if len(o.Template) > 0 {
|
||||||
input = strings.Join(o.Template, "\n")
|
input = strings.Join(o.Template, "\n")
|
||||||
} else {
|
} else {
|
||||||
input, _ = stdin.Read()
|
input, _ = stdin.Read()
|
||||||
}
|
}
|
||||||
|
|
||||||
v, err := formatType[o.Type](input)
|
switch o.Type {
|
||||||
|
case "code":
|
||||||
|
output, err = code(input, o.Language)
|
||||||
|
case "emoji":
|
||||||
|
output, err = emoji(input)
|
||||||
|
case "template":
|
||||||
|
output, err = template(input)
|
||||||
|
default:
|
||||||
|
output, err = markdown(input, o.Theme)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Print(v)
|
fmt.Print(output)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"github.com/muesli/termenv"
|
"github.com/muesli/termenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var code Func = func(input string) (string, error) {
|
func code(input, language string) (string, error) {
|
||||||
renderer, err := glamour.NewTermRenderer(
|
renderer, err := glamour.NewTermRenderer(
|
||||||
glamour.WithAutoStyle(),
|
glamour.WithAutoStyle(),
|
||||||
glamour.WithWordWrap(0),
|
glamour.WithWordWrap(0),
|
||||||
|
@ -17,14 +17,14 @@ var code Func = func(input string) (string, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("unable to create renderer: %w", err)
|
return "", fmt.Errorf("unable to create renderer: %w", err)
|
||||||
}
|
}
|
||||||
output, err := renderer.Render(fmt.Sprintf("```\n%s\n```", input))
|
output, err := renderer.Render(fmt.Sprintf("```%s\n%s\n```", language, input))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("unable to render: %w", err)
|
return "", fmt.Errorf("unable to render: %w", err)
|
||||||
}
|
}
|
||||||
return output, nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var emoji Func = func(input string) (string, error) {
|
func emoji(input string) (string, error) {
|
||||||
renderer, err := glamour.NewTermRenderer(
|
renderer, err := glamour.NewTermRenderer(
|
||||||
glamour.WithEmoji(),
|
glamour.WithEmoji(),
|
||||||
)
|
)
|
||||||
|
@ -38,13 +38,13 @@ var emoji Func = func(input string) (string, error) {
|
||||||
return output, nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var markdown Func = func(input string) (string, error) {
|
func markdown(input string, theme string) (string, error) {
|
||||||
renderer, err := glamour.NewTermRenderer(
|
renderer, err := glamour.NewTermRenderer(
|
||||||
glamour.WithStandardStyle("pink"),
|
glamour.WithStylePath(theme),
|
||||||
glamour.WithWordWrap(0),
|
glamour.WithWordWrap(0),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("unable to create renderer: %w", err)
|
return "", fmt.Errorf("unable to render: %w", err)
|
||||||
}
|
}
|
||||||
output, err := renderer.Render(input)
|
output, err := renderer.Render(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -53,8 +53,8 @@ var markdown Func = func(input string) (string, error) {
|
||||||
return output, nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var template Func = func(input string) (string, error) {
|
func template(input string) (string, error) {
|
||||||
f := termenv.TemplateFuncs(termenv.ColorProfile())
|
f := termenv.TemplateFuncs(termenv.ANSI256)
|
||||||
t, err := tpl.New("tpl").Funcs(f).Parse(input)
|
t, err := tpl.New("tpl").Funcs(f).Parse(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("unable to parse template: %w", err)
|
return "", fmt.Errorf("unable to parse template: %w", err)
|
||||||
|
|
|
@ -3,6 +3,8 @@ package format
|
||||||
// Options is customization options for the format command.
|
// Options is customization options for the format command.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Template []string `arg:"" optional:"" help:"Template string to format (can also be provided via stdin)"`
|
Template []string `arg:"" optional:"" help:"Template string to format (can also be provided via stdin)"`
|
||||||
|
Theme string `help:"Glamour theme to use for markdown formatting" default:"pink" env:"GUM_FORMAT_THEME"`
|
||||||
|
Language string `help:"Programming language to parse code" short:"l" default:"" env:"GUM_FORMAT_LANGUAGE"`
|
||||||
|
|
||||||
Type string `help:"Format to use (markdown,template,code,emoji)" enum:"markdown,template,code,emoji" short:"t" default:"markdown"`
|
Type string `help:"Format to use (markdown,template,code,emoji)" enum:"markdown,template,code,emoji" short:"t" default:"markdown" env:"GUM_FORMAT_TYPE"`
|
||||||
}
|
}
|
||||||
|
|
56
go.mod
56
go.mod
|
@ -1,16 +1,54 @@
|
||||||
module github.com/charmbracelet/gum
|
module github.com/charmbracelet/gum
|
||||||
|
|
||||||
go 1.16
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alecthomas/kong v0.6.1
|
github.com/alecthomas/kong v0.9.0
|
||||||
github.com/alecthomas/mango-kong v0.1.0
|
github.com/alecthomas/mango-kong v0.1.0
|
||||||
github.com/charmbracelet/bubbles v0.14.1-0.20220926062606-e857875f2a75
|
github.com/charmbracelet/bubbles v0.18.0
|
||||||
github.com/charmbracelet/bubbletea v0.22.1
|
github.com/charmbracelet/bubbletea v0.26.3
|
||||||
github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da
|
github.com/charmbracelet/glamour v0.7.0
|
||||||
github.com/charmbracelet/lipgloss v0.6.0
|
github.com/charmbracelet/huh v0.4.2
|
||||||
github.com/mattn/go-runewidth v0.0.14
|
github.com/charmbracelet/lipgloss v0.11.0
|
||||||
|
github.com/charmbracelet/log v0.4.0
|
||||||
|
github.com/mattn/go-isatty v0.0.20
|
||||||
|
github.com/muesli/reflow v0.3.0
|
||||||
github.com/muesli/roff v0.1.0
|
github.com/muesli/roff v0.1.0
|
||||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739
|
github.com/muesli/termenv v0.15.2
|
||||||
github.com/sahilm/fuzzy v0.1.0
|
github.com/sahilm/fuzzy v0.1.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
|
github.com/catppuccin/go v0.2.0 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.1.1 // indirect
|
||||||
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240525152034-77596eb8760e // indirect
|
||||||
|
github.com/charmbracelet/x/input v0.1.1 // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.1.1 // indirect
|
||||||
|
github.com/charmbracelet/x/windows v0.1.2 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||||
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.26 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/mango v0.2.0 // indirect
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
github.com/yuin/goldmark v1.7.1 // indirect
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.2 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect
|
||||||
|
golang.org/x/net v0.25.0 // indirect
|
||||||
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
|
golang.org/x/text v0.15.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
165
go.sum
165
go.sum
|
@ -1,110 +1,115 @@
|
||||||
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
||||||
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/alecthomas/kong v0.4.0/go.mod h1:uzxf/HUh0tj43x1AyJROl3JT7SgsZ5m+icOv1csRhc0=
|
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
||||||
github.com/alecthomas/kong v0.6.1 h1:1kNhcFepkR+HmasQpbiKDLylIL8yh5B5y1zPp5bJimA=
|
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
||||||
github.com/alecthomas/kong v0.6.1/go.mod h1:JfHWDzLmbh/puW6I3V7uWenoh56YNVONW+w8eKeUr9I=
|
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 h1:iFVfP1k1K4qpml3JUQmD5I8MCQYfIvsD9mRdrw7jJC4=
|
||||||
github.com/alecthomas/mango-kong v0.1.0/go.mod h1:t+TYVdsONUolf/BwVcm+15eqcdAj15h4Qe9MMFAwwT4=
|
github.com/alecthomas/mango-kong v0.1.0/go.mod h1:t+TYVdsONUolf/BwVcm+15eqcdAj15h4Qe9MMFAwwT4=
|
||||||
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48=
|
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||||
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
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 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
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 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/charmbracelet/bubbles v0.14.0 h1:DJfCwnARfWjZLvMglhSQzo76UZ2gucuHPy9jLWX45Og=
|
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
|
||||||
github.com/charmbracelet/bubbles v0.14.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=
|
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||||
github.com/charmbracelet/bubbles v0.14.1-0.20220926062606-e857875f2a75 h1:rjgidQdLMPMe7wQOs+ki2cD69keSKd8nZFPGIDxv4ZI=
|
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
|
||||||
github.com/charmbracelet/bubbles v0.14.1-0.20220926062606-e857875f2a75/go.mod h1:5rZgJTHmgWISQnxnzzIJtQt3GC1bfJfNmr4SEtRDtTQ=
|
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
|
||||||
github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4=
|
github.com/charmbracelet/bubbletea v0.26.3 h1:iXyGvI+FfOWqkB2V07m1DF3xxQijxjY2j8PqiXYqasg=
|
||||||
github.com/charmbracelet/bubbletea v0.22.1 h1:z66q0LWdJNOWEH9zadiAIXp2GN1AWrwNXU8obVY9X24=
|
github.com/charmbracelet/bubbletea v0.26.3/go.mod h1:bpZHfDHTYJC5g+FBK+ptJRCQotRC+Dhh3AoMxa/2+3Q=
|
||||||
github.com/charmbracelet/bubbletea v0.22.1/go.mod h1:8/7hVvbPN6ZZPkczLiB8YpLkLJ0n7DMho5Wvfd2X1C0=
|
github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng=
|
||||||
github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da h1:FGz53GWQRiKQ/5xUsoCCkewSQIC7u81Scaxx2nUy3nM=
|
github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps=
|
||||||
github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da/go.mod h1:HXz79SMFnF9arKxqeoHWxmo1BhplAH7wehlRhKQIL94=
|
github.com/charmbracelet/huh v0.4.2 h1:5wLkwrA58XDAfEZsJzNQlfJ+K8N9+wYwvR5FOM7jXFM=
|
||||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
github.com/charmbracelet/huh v0.4.2/go.mod h1:g9OXBgtY3zRV4ahnVih9bZE+1yGYN+y2C9Q6L2P+WM0=
|
||||||
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
|
github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g=
|
||||||
github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY=
|
github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8=
|
||||||
github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
|
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
|
||||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
|
||||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||||
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240525152034-77596eb8760e h1:DhvN6ye3nHLhRtNHtlrQ0Zk+vmeN7YtEnyIRfcl7e0E=
|
||||||
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240525152034-77596eb8760e/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||||
|
github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a h1:k/s6UoOSVynWiw7PlclyGO2VdVs5ZLbMIHiGp4shFZE=
|
||||||
|
github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a/go.mod h1:YBotIGhfoWhHDlnUpJMkjebGV2pdGRCn1Y4/Nk/vVcU=
|
||||||
|
github.com/charmbracelet/x/input v0.1.1 h1:YDOJaTUKCqtGnq9PHzx3pkkl4pXDOANUHmhH3DqMtM4=
|
||||||
|
github.com/charmbracelet/x/input v0.1.1/go.mod h1:jvdTVUnNWj/RD6hjC4FsoB0SeZCJ2ZBkiuFP9zXvZI0=
|
||||||
|
github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
|
||||||
|
github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw=
|
||||||
|
github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg=
|
||||||
|
github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||||
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
|
||||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.19 h1:OI7hoF5FY4pFz2VA//RN8TfM0YJ2dJcl4P4APrCWy6c=
|
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.19/go.mod h1:QNzV2UbLK2/53oIIwTOyLUSABMkjZ4tqiyC1g/DyqxE=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
|
||||||
github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
|
||||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/muesli/mango v0.1.1-0.20220205060214-77e2058169ab h1:m7QFONkzLK0fVXCjwX5tANcnj1yXxTnYQtnfJiY3tcA=
|
github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
|
||||||
github.com/muesli/mango v0.1.1-0.20220205060214-77e2058169ab/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
|
github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
|
||||||
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
|
|
||||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||||
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
|
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
|
||||||
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
|
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
|
||||||
github.com/muesli/termenv v0.11.0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||||
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
|
|
||||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
|
||||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
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/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/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.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||||
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
|
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
|
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
|
||||||
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
|
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
|
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
|
||||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||||
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
63
gum.go
63
gum.go
|
@ -6,13 +6,17 @@ import (
|
||||||
"github.com/charmbracelet/gum/choose"
|
"github.com/charmbracelet/gum/choose"
|
||||||
"github.com/charmbracelet/gum/completion"
|
"github.com/charmbracelet/gum/completion"
|
||||||
"github.com/charmbracelet/gum/confirm"
|
"github.com/charmbracelet/gum/confirm"
|
||||||
|
"github.com/charmbracelet/gum/file"
|
||||||
"github.com/charmbracelet/gum/filter"
|
"github.com/charmbracelet/gum/filter"
|
||||||
"github.com/charmbracelet/gum/format"
|
"github.com/charmbracelet/gum/format"
|
||||||
"github.com/charmbracelet/gum/input"
|
"github.com/charmbracelet/gum/input"
|
||||||
"github.com/charmbracelet/gum/join"
|
"github.com/charmbracelet/gum/join"
|
||||||
|
"github.com/charmbracelet/gum/log"
|
||||||
"github.com/charmbracelet/gum/man"
|
"github.com/charmbracelet/gum/man"
|
||||||
|
"github.com/charmbracelet/gum/pager"
|
||||||
"github.com/charmbracelet/gum/spin"
|
"github.com/charmbracelet/gum/spin"
|
||||||
"github.com/charmbracelet/gum/style"
|
"github.com/charmbracelet/gum/style"
|
||||||
|
"github.com/charmbracelet/gum/table"
|
||||||
"github.com/charmbracelet/gum/write"
|
"github.com/charmbracelet/gum/write"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -54,6 +58,20 @@ type Gum struct {
|
||||||
//
|
//
|
||||||
Confirm confirm.Options `cmd:"" help:"Ask a user to confirm an action"`
|
Confirm confirm.Options `cmd:"" help:"Ask a user to confirm an action"`
|
||||||
|
|
||||||
|
// File provides an interface to pick a file from a folder (tree).
|
||||||
|
// The user is provided a file manager-like interface to navigate, to
|
||||||
|
// select a file.
|
||||||
|
//
|
||||||
|
// Let's pick a file from the current directory:
|
||||||
|
//
|
||||||
|
// $ gum file
|
||||||
|
// $ gum file .
|
||||||
|
//
|
||||||
|
// Let's pick a file from the home directory:
|
||||||
|
//
|
||||||
|
// $ gum file $HOME
|
||||||
|
File file.Options `cmd:"" help:"Pick a file from a folder"`
|
||||||
|
|
||||||
// Filter provides a fuzzy searching text input to allow filtering a list of
|
// Filter provides a fuzzy searching text input to allow filtering a list of
|
||||||
// options to select one option.
|
// options to select one option.
|
||||||
//
|
//
|
||||||
|
@ -100,6 +118,25 @@ type Gum struct {
|
||||||
//
|
//
|
||||||
Join join.Options `cmd:"" help:"Join text vertically or horizontally"`
|
Join join.Options `cmd:"" help:"Join text vertically or horizontally"`
|
||||||
|
|
||||||
|
// Pager provides a shell script interface for the viewport bubble.
|
||||||
|
// https://github.com/charmbracelet/bubbles/tree/master/viewport
|
||||||
|
//
|
||||||
|
// It allows the user to scroll through content like a pager.
|
||||||
|
//
|
||||||
|
// ╭────────────────────────────────────────────────╮
|
||||||
|
// │ 1 │ Gum Pager │
|
||||||
|
// │ 2 │ ========= │
|
||||||
|
// │ 3 │ │
|
||||||
|
// │ 4 │ ``` │
|
||||||
|
// │ 5 │ gum pager --height 10 --width 25 < text │
|
||||||
|
// │ 6 │ ``` │
|
||||||
|
// │ 7 │ │
|
||||||
|
// │ 8 │ │
|
||||||
|
// ╰────────────────────────────────────────────────╯
|
||||||
|
// ↑/↓: Navigate • q: Quit
|
||||||
|
//
|
||||||
|
Pager pager.Options `cmd:"" help:"Scroll through a file"`
|
||||||
|
|
||||||
// Spin provides a shell script interface for the spinner bubble.
|
// Spin provides a shell script interface for the spinner bubble.
|
||||||
// https://github.com/charmbracelet/bubbles/tree/master/spinner
|
// https://github.com/charmbracelet/bubbles/tree/master/spinner
|
||||||
//
|
//
|
||||||
|
@ -142,6 +179,23 @@ type Gum struct {
|
||||||
//
|
//
|
||||||
Style style.Options `cmd:"" help:"Apply coloring, borders, spacing to text"`
|
Style style.Options `cmd:"" help:"Apply coloring, borders, spacing to text"`
|
||||||
|
|
||||||
|
// Table provides a shell script interface for the table bubble.
|
||||||
|
// https://github.com/charmbracelet/bubbles/tree/master/table
|
||||||
|
//
|
||||||
|
// It is useful to render tabular (CSV) data in a terminal and allows
|
||||||
|
// the user to select a row from the table.
|
||||||
|
//
|
||||||
|
// Let's render a table of gum flavors:
|
||||||
|
//
|
||||||
|
// $ gum table <<< "Flavor,Price\nStrawberry,$0.50\nBanana,$0.99\nCherry,$0.75"
|
||||||
|
//
|
||||||
|
// Flavor Price
|
||||||
|
// Strawberry $0.50
|
||||||
|
// Banana $0.99
|
||||||
|
// Cherry $0.75
|
||||||
|
//
|
||||||
|
Table table.Options `cmd:"" help:"Render a table of data"`
|
||||||
|
|
||||||
// Write provides a shell script interface for the text area bubble.
|
// Write provides a shell script interface for the text area bubble.
|
||||||
// https://github.com/charmbracelet/bubbles/tree/master/textarea
|
// https://github.com/charmbracelet/bubbles/tree/master/textarea
|
||||||
//
|
//
|
||||||
|
@ -151,4 +205,13 @@ type Gum struct {
|
||||||
// $ gum write > output.text
|
// $ gum write > output.text
|
||||||
//
|
//
|
||||||
Write write.Options `cmd:"" help:"Prompt for long-form text"`
|
Write write.Options `cmd:"" help:"Prompt for long-form text"`
|
||||||
|
|
||||||
|
// Log provides a shell script interface for logging using Log.
|
||||||
|
// https://github.com/charmbracelet/log
|
||||||
|
//
|
||||||
|
// It can be used to log messages to output.
|
||||||
|
//
|
||||||
|
// $ gum log --level info "Hello, world!"
|
||||||
|
//
|
||||||
|
Log log.Options `cmd:"" help:"Log messages to output"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,60 +2,69 @@ package input
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
"github.com/charmbracelet/bubbles/key"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
|
||||||
"github.com/charmbracelet/gum/internal/exit"
|
|
||||||
"github.com/charmbracelet/gum/internal/stdin"
|
"github.com/charmbracelet/gum/internal/stdin"
|
||||||
"github.com/charmbracelet/gum/style"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run provides a shell script interface for the text input bubble.
|
// Run provides a shell script interface for the text input bubble.
|
||||||
// https://github.com/charmbracelet/bubbles/textinput
|
// https://github.com/charmbracelet/bubbles/textinput
|
||||||
func (o Options) Run() error {
|
func (o Options) Run() error {
|
||||||
i := textinput.New()
|
var value string
|
||||||
if in, _ := stdin.Read(); in != "" && o.Value == "" {
|
if o.Value != "" {
|
||||||
i.SetValue(in)
|
value = o.Value
|
||||||
} else {
|
} else if in, _ := stdin.Read(); in != "" {
|
||||||
i.SetValue(o.Value)
|
value = in
|
||||||
}
|
}
|
||||||
|
|
||||||
i.Focus()
|
theme := huh.ThemeCharm()
|
||||||
i.Prompt = o.Prompt
|
theme.Focused.Base = lipgloss.NewStyle()
|
||||||
i.Placeholder = o.Placeholder
|
theme.Focused.TextInput.Cursor = o.CursorStyle.ToLipgloss()
|
||||||
i.Width = o.Width
|
theme.Focused.TextInput.Placeholder = o.PlaceholderStyle.ToLipgloss()
|
||||||
i.PromptStyle = o.PromptStyle.ToLipgloss()
|
theme.Focused.TextInput.Prompt = o.PromptStyle.ToLipgloss()
|
||||||
i.CursorStyle = o.CursorStyle.ToLipgloss()
|
theme.Focused.Title = o.HeaderStyle.ToLipgloss()
|
||||||
i.CharLimit = o.CharLimit
|
|
||||||
|
// Keep input keymap backwards compatible
|
||||||
|
keymap := huh.NewDefaultKeyMap()
|
||||||
|
keymap.Quit = key.NewBinding(key.WithKeys("ctrl+c", "esc"))
|
||||||
|
|
||||||
|
var echoMode huh.EchoMode
|
||||||
|
|
||||||
if o.Password {
|
if o.Password {
|
||||||
i.EchoMode = textinput.EchoPassword
|
echoMode = huh.EchoModePassword
|
||||||
i.EchoCharacter = '•'
|
} else {
|
||||||
|
echoMode = huh.EchoModeNormal
|
||||||
}
|
}
|
||||||
|
|
||||||
p := tea.NewProgram(model{
|
err := huh.NewForm(
|
||||||
textinput: i,
|
huh.NewGroup(
|
||||||
aborted: false,
|
huh.NewInput().
|
||||||
}, tea.WithOutput(os.Stderr))
|
Prompt(o.Prompt).
|
||||||
tm, err := p.StartReturningModel()
|
Placeholder(o.Placeholder).
|
||||||
|
CharLimit(o.CharLimit).
|
||||||
|
EchoMode(echoMode).
|
||||||
|
Title(o.Header).
|
||||||
|
Value(&value),
|
||||||
|
),
|
||||||
|
).
|
||||||
|
WithShowHelp(false).
|
||||||
|
WithWidth(o.Width).
|
||||||
|
WithTheme(theme).
|
||||||
|
WithKeyMap(keymap).
|
||||||
|
WithShowHelp(o.ShowHelp).
|
||||||
|
WithProgramOptions(tea.WithOutput(os.Stderr)).
|
||||||
|
Run()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to run input: %w", err)
|
return err
|
||||||
}
|
|
||||||
m := tm.(model)
|
|
||||||
|
|
||||||
if m.aborted {
|
|
||||||
return exit.ErrAborted
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(m.textinput.Value())
|
fmt.Println(value)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeforeReset hook. Used to unclutter style flags.
|
|
||||||
func (o Options) BeforeReset(ctx *kong.Context) error {
|
|
||||||
style.HideFlags(ctx)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
// Package input provides a shell script interface for the text input bubble.
|
|
||||||
// https://github.com/charmbracelet/bubbles/tree/master/textinput
|
|
||||||
//
|
|
||||||
// It can be used to prompt the user for some input. The text the user entered
|
|
||||||
// will be sent to stdout.
|
|
||||||
//
|
|
||||||
// $ gum input --placeholder "What's your favorite gum?" > answer.text
|
|
||||||
package input
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
)
|
|
||||||
|
|
||||||
type model struct {
|
|
||||||
textinput textinput.Model
|
|
||||||
aborted bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m model) Init() tea.Cmd { return textinput.Blink }
|
|
||||||
func (m model) View() string { return m.textinput.View() }
|
|
||||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case tea.KeyMsg:
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c", "esc":
|
|
||||||
m.aborted = true
|
|
||||||
return m, tea.Quit
|
|
||||||
case "enter":
|
|
||||||
return m, tea.Quit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var cmd tea.Cmd
|
|
||||||
m.textinput, cmd = m.textinput.Update(msg)
|
|
||||||
return m, cmd
|
|
||||||
}
|
|
|
@ -1,15 +1,25 @@
|
||||||
package input
|
package input
|
||||||
|
|
||||||
import "github.com/charmbracelet/gum/style"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/gum/style"
|
||||||
|
)
|
||||||
|
|
||||||
// Options are the customization options for the input.
|
// Options are the customization options for the input.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Placeholder string `help:"Placeholder value" default:"Type something..." env:"GUM_INPUT_PLACEHOLDER"`
|
Placeholder string `help:"Placeholder value" default:"Type something..." env:"GUM_INPUT_PLACEHOLDER"`
|
||||||
Prompt string `help:"Prompt to display" default:"> " env:"GUM_INPUT_PROMPT"`
|
Prompt string `help:"Prompt to display" default:"> " env:"GUM_INPUT_PROMPT"`
|
||||||
PromptStyle style.Styles `embed:"" prefix:"prompt." envprefix:"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_"`
|
PlaceholderStyle style.Styles `embed:"" prefix:"placeholder." set:"defaultForeground=240" envprefix:"GUM_INPUT_PLACEHOLDER_"`
|
||||||
Value string `help:"Initial value (can also be passed via stdin)" default:""`
|
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_INPUT_CURSOR_"`
|
||||||
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_INPUT_CURSOR_MODE"`
|
||||||
Width int `help:"Input width" default:"40" env:"GUM_INPUT_WIDTH"`
|
Value string `help:"Initial value (can also be passed via stdin)" default:""`
|
||||||
Password bool `help:"Mask input characters" default:"false"`
|
CharLimit int `help:"Maximum value length (0 for no limit)" default:"400"`
|
||||||
|
Width int `help:"Input width (0 for terminal width)" default:"40" env:"GUM_INPUT_WIDTH"`
|
||||||
|
Password bool `help:"Mask input characters" default:"false"`
|
||||||
|
ShowHelp bool `help:"Show help keybinds" default:"true" negatable:"true" env:"GUM_INPUT_SHOW_HELP"`
|
||||||
|
Header string `help:"Header value" default:"" env:"GUM_INPUT_HEADER"`
|
||||||
|
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_INPUT_HEADER_"`
|
||||||
|
Timeout time.Duration `help:"Timeout until input aborts" default:"0" env:"GUM_INPUT_TIMEOUT"`
|
||||||
}
|
}
|
||||||
|
|
26
internal/stack/stack.go
Normal file
26
internal/stack/stack.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package stack
|
||||||
|
|
||||||
|
// Stack is a stack interface for integers.
|
||||||
|
type Stack struct {
|
||||||
|
Push func(int)
|
||||||
|
Pop func() int
|
||||||
|
Length func() int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStack returns a new stack of integers.
|
||||||
|
func NewStack() Stack {
|
||||||
|
slice := make([]int, 0)
|
||||||
|
return Stack{
|
||||||
|
Push: func(i int) {
|
||||||
|
slice = append(slice, i)
|
||||||
|
},
|
||||||
|
Pop: func() int {
|
||||||
|
res := slice[len(slice)-1]
|
||||||
|
slice = slice[:len(slice)-1]
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
Length: func() int {
|
||||||
|
return len(slice)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,13 +10,8 @@ import (
|
||||||
|
|
||||||
// Read reads input from an stdin pipe.
|
// Read reads input from an stdin pipe.
|
||||||
func Read() (string, error) {
|
func Read() (string, error) {
|
||||||
stat, err := os.Stdin.Stat()
|
if IsEmpty() {
|
||||||
if err != nil {
|
return "", fmt.Errorf("stdin is empty")
|
||||||
return "", fmt.Errorf("failed to stat stdin: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if stat.Mode()&os.ModeNamedPipe == 0 && stat.Size() == 0 {
|
|
||||||
return "", nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
@ -33,5 +28,19 @@ func Read() (string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.String(), nil
|
return strings.TrimSuffix(b.String(), "\n"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns whether stdin is empty.
|
||||||
|
func IsEmpty() bool {
|
||||||
|
stat, err := os.Stdin.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat.Mode()&os.ModeNamedPipe == 0 && stat.Size() == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
15
internal/utils/utils.go
Normal file
15
internal/utils/utils.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LipglossPadding calculates how much padding a string is given by a style.
|
||||||
|
func LipglossPadding(style lipgloss.Style) (int, int) {
|
||||||
|
render := style.Render(" ")
|
||||||
|
before := strings.Index(render, " ")
|
||||||
|
after := len(render) - len(" ") - before
|
||||||
|
return before, after
|
||||||
|
}
|
141
log/command.go
Normal file
141
log/command.go
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run is the command-line interface for logging text.
|
||||||
|
func (o Options) Run() error {
|
||||||
|
l := log.New(os.Stderr)
|
||||||
|
|
||||||
|
if o.File != "" {
|
||||||
|
f, err := os.OpenFile(o.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error opening file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close() //nolint:errcheck
|
||||||
|
l.SetOutput(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.SetPrefix(o.Prefix)
|
||||||
|
l.SetLevel(-math.MaxInt32) // log all levels
|
||||||
|
l.SetReportTimestamp(o.Time != "")
|
||||||
|
|
||||||
|
timeFormats := map[string]string{
|
||||||
|
"layout": time.Layout,
|
||||||
|
"ansic": time.ANSIC,
|
||||||
|
"unixdate": time.UnixDate,
|
||||||
|
"rubydate": time.RubyDate,
|
||||||
|
"rfc822": time.RFC822,
|
||||||
|
"rfc822z": time.RFC822Z,
|
||||||
|
"rfc850": time.RFC850,
|
||||||
|
"rfc1123": time.RFC1123,
|
||||||
|
"rfc1123z": time.RFC1123Z,
|
||||||
|
"rfc3339": time.RFC3339,
|
||||||
|
"rfc3339nano": time.RFC3339Nano,
|
||||||
|
"kitchen": time.Kitchen,
|
||||||
|
"stamp": time.Stamp,
|
||||||
|
"stampmilli": time.StampMilli,
|
||||||
|
"stampmicro": time.StampMicro,
|
||||||
|
"stampnano": time.StampNano,
|
||||||
|
"datetime": time.DateTime,
|
||||||
|
"dateonly": time.DateOnly,
|
||||||
|
"timeonly": time.TimeOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
tf, ok := timeFormats[strings.ToLower(o.Time)]
|
||||||
|
if ok {
|
||||||
|
l.SetTimeFormat(tf)
|
||||||
|
} else {
|
||||||
|
l.SetTimeFormat(o.Time)
|
||||||
|
}
|
||||||
|
|
||||||
|
st := log.DefaultStyles()
|
||||||
|
lvl := levelToLog[o.Level]
|
||||||
|
lvlStyle := o.LevelStyle.ToLipgloss()
|
||||||
|
if lvlStyle.GetForeground() == lipgloss.Color("") {
|
||||||
|
lvlStyle = lvlStyle.Foreground(st.Levels[lvl].GetForeground())
|
||||||
|
}
|
||||||
|
|
||||||
|
st.Levels[lvl] = lvlStyle.
|
||||||
|
SetString(strings.ToUpper(lvl.String())).
|
||||||
|
Inline(true)
|
||||||
|
|
||||||
|
st.Timestamp = o.TimeStyle.ToLipgloss().
|
||||||
|
Inline(true)
|
||||||
|
st.Prefix = o.PrefixStyle.ToLipgloss().
|
||||||
|
Inline(true)
|
||||||
|
st.Message = o.MessageStyle.ToLipgloss().
|
||||||
|
Inline(true)
|
||||||
|
st.Key = o.KeyStyle.ToLipgloss().
|
||||||
|
Inline(true)
|
||||||
|
st.Value = o.ValueStyle.ToLipgloss().
|
||||||
|
Inline(true)
|
||||||
|
st.Separator = o.SeparatorStyle.ToLipgloss().
|
||||||
|
Inline(true)
|
||||||
|
|
||||||
|
l.SetStyles(st)
|
||||||
|
|
||||||
|
switch o.Formatter {
|
||||||
|
case "json":
|
||||||
|
l.SetFormatter(log.JSONFormatter)
|
||||||
|
case "logfmt":
|
||||||
|
l.SetFormatter(log.LogfmtFormatter)
|
||||||
|
case "text":
|
||||||
|
l.SetFormatter(log.TextFormatter)
|
||||||
|
}
|
||||||
|
|
||||||
|
var arg0 string
|
||||||
|
var args []interface{}
|
||||||
|
if len(o.Text) > 0 {
|
||||||
|
arg0 = o.Text[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(o.Text) > 1 {
|
||||||
|
args = make([]interface{}, len(o.Text[1:]))
|
||||||
|
for i, arg := range o.Text[1:] {
|
||||||
|
args[i] = arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := map[string]logger{
|
||||||
|
"none": {printf: l.Printf, print: l.Print},
|
||||||
|
"debug": {printf: l.Debugf, print: l.Debug},
|
||||||
|
"info": {printf: l.Infof, print: l.Info},
|
||||||
|
"warn": {printf: l.Warnf, print: l.Warn},
|
||||||
|
"error": {printf: l.Errorf, print: l.Error},
|
||||||
|
"fatal": {printf: l.Fatalf, print: l.Fatal},
|
||||||
|
}[o.Level]
|
||||||
|
|
||||||
|
if o.Format {
|
||||||
|
logger.printf(arg0, args...)
|
||||||
|
} else if o.Structured {
|
||||||
|
logger.print(arg0, args...)
|
||||||
|
} else {
|
||||||
|
logger.print(strings.Join(o.Text, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type logger struct {
|
||||||
|
printf func(string, ...interface{})
|
||||||
|
print func(interface{}, ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
var levelToLog = map[string]log.Level{
|
||||||
|
"none": log.Level(math.MaxInt32),
|
||||||
|
"debug": log.DebugLevel,
|
||||||
|
"info": log.InfoLevel,
|
||||||
|
"warn": log.WarnLevel,
|
||||||
|
"error": log.ErrorLevel,
|
||||||
|
"fatal": log.FatalLevel,
|
||||||
|
}
|
26
log/options.go
Normal file
26
log/options.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charmbracelet/gum/style"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options is the set of options that can configure a join.
|
||||||
|
type Options struct {
|
||||||
|
Text []string `arg:"" help:"Text to log"`
|
||||||
|
|
||||||
|
File string `short:"o" help:"Log to file"`
|
||||||
|
Format bool `short:"f" help:"Format message using printf" xor:"format,structured"`
|
||||||
|
Formatter string `help:"The log formatter to use" enum:"json,logfmt,text" default:"text"`
|
||||||
|
Level string `short:"l" help:"The log level to use" enum:"none,debug,info,warn,error,fatal" default:"none"`
|
||||||
|
Prefix string `help:"Prefix to print before the message"`
|
||||||
|
Structured bool `short:"s" help:"Use structured logging" xor:"format,structured"`
|
||||||
|
Time string `short:"t" help:"The time format to use (kitchen, layout, ansic, rfc822, etc...)" default:""`
|
||||||
|
|
||||||
|
LevelStyle style.Styles `embed:"" prefix:"level." help:"The style of the level being used" set:"defaultBold=true" envprefix:"GUM_LOG_LEVEL_"` //nolint:staticcheck
|
||||||
|
TimeStyle style.Styles `embed:"" prefix:"time." help:"The style of the time" envprefix:"GUM_LOG_TIME_"`
|
||||||
|
PrefixStyle style.Styles `embed:"" prefix:"prefix." help:"The style of the prefix" set:"defaultBold=true" set:"defaultFaint=true" envprefix:"GUM_LOG_PREFIX_"` //nolint:staticcheck
|
||||||
|
MessageStyle style.Styles `embed:"" prefix:"message." help:"The style of the message" envprefix:"GUM_LOG_MESSAGE_"`
|
||||||
|
KeyStyle style.Styles `embed:"" prefix:"key." help:"The style of the key" set:"defaultFaint=true" envprefix:"GUM_LOG_KEY_"`
|
||||||
|
ValueStyle style.Styles `embed:"" prefix:"value." help:"The style of the value" envprefix:"GUM_LOG_VALUE_"`
|
||||||
|
SeparatorStyle style.Styles `embed:"" prefix:"separator." help:"The style of the separator" set:"defaultFaint=true" envprefix:"GUM_LOG_SEPARATOR_"`
|
||||||
|
}
|
32
main.go
32
main.go
|
@ -7,6 +7,7 @@ import (
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
"github.com/alecthomas/kong"
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/muesli/termenv"
|
"github.com/muesli/termenv"
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@ var (
|
||||||
var bubbleGumPink = lipgloss.NewStyle().Foreground(lipgloss.Color("212"))
|
var bubbleGumPink = lipgloss.NewStyle().Foreground(lipgloss.Color("212"))
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
lipgloss.SetColorProfile(termenv.ANSI256)
|
lipgloss.SetColorProfile(termenv.NewOutput(os.Stderr).Profile)
|
||||||
|
|
||||||
if Version == "" {
|
if Version == "" {
|
||||||
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" {
|
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" {
|
||||||
|
@ -48,20 +49,31 @@ func main() {
|
||||||
kong.Description(fmt.Sprintf("A tool for %s shell scripts.", bubbleGumPink.Render("glamorous"))),
|
kong.Description(fmt.Sprintf("A tool for %s shell scripts.", bubbleGumPink.Render("glamorous"))),
|
||||||
kong.UsageOnError(),
|
kong.UsageOnError(),
|
||||||
kong.ConfigureHelp(kong.HelpOptions{
|
kong.ConfigureHelp(kong.HelpOptions{
|
||||||
Compact: true,
|
Compact: true,
|
||||||
Summary: false,
|
Summary: false,
|
||||||
|
NoExpandSubcommands: true,
|
||||||
}),
|
}),
|
||||||
kong.Vars{
|
kong.Vars{
|
||||||
"version": version,
|
"version": version,
|
||||||
"defaultBackground": "",
|
"defaultHeight": "0",
|
||||||
"defaultForeground": "",
|
"defaultWidth": "0",
|
||||||
"defaultMargin": "0 0",
|
"defaultAlign": "left",
|
||||||
"defaultPadding": "0 0",
|
"defaultBorder": "none",
|
||||||
"defaultUnderline": "false",
|
"defaultBorderForeground": "",
|
||||||
|
"defaultBorderBackground": "",
|
||||||
|
"defaultBackground": "",
|
||||||
|
"defaultForeground": "",
|
||||||
|
"defaultMargin": "0 0",
|
||||||
|
"defaultPadding": "0 0",
|
||||||
|
"defaultUnderline": "false",
|
||||||
|
"defaultBold": "false",
|
||||||
|
"defaultFaint": "false",
|
||||||
|
"defaultItalic": "false",
|
||||||
|
"defaultStrikethrough": "false",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err := ctx.Run(); err != nil {
|
if err := ctx.Run(); err != nil {
|
||||||
if errors.Is(err, exit.ErrAborted) {
|
if errors.Is(err, exit.ErrAborted) || errors.Is(err, huh.ErrUserAborted) {
|
||||||
os.Exit(exit.StatusAborted)
|
os.Exit(exit.StatusAborted)
|
||||||
}
|
}
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|
|
@ -16,9 +16,9 @@ func (m Man) BeforeApply(ctx *kong.Context) error {
|
||||||
// Set the correct man pages description without color escape sequences.
|
// Set the correct man pages description without color escape sequences.
|
||||||
ctx.Model.Help = "A tool for glamorous shell scripts."
|
ctx.Model.Help = "A tool for glamorous shell scripts."
|
||||||
man := mangokong.NewManPage(1, ctx.Model)
|
man := mangokong.NewManPage(1, ctx.Model)
|
||||||
man = man.WithSection("Copyright", "(C) 2021-2022 Charmbracelet, Inc.\n"+
|
man = man.WithSection("Copyright", "(c) 2022-2024 Charmbracelet, Inc.\n"+
|
||||||
"Released under MIT license.")
|
"Released under MIT license.")
|
||||||
fmt.Fprint(ctx.Stdout, man.Build(roff.NewDocument()))
|
_, _ = fmt.Fprint(ctx.Stdout, man.Build(roff.NewDocument()))
|
||||||
ctx.Exit(0)
|
ctx.Exit(0)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
50
pager/command.go
Normal file
50
pager/command.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package pager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/gum/internal/stdin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run provides a shell script interface for the viewport bubble.
|
||||||
|
// https://github.com/charmbracelet/bubbles/viewport
|
||||||
|
func (o Options) Run() error {
|
||||||
|
vp := viewport.New(o.Style.Width, o.Style.Height)
|
||||||
|
vp.Style = o.Style.ToLipgloss()
|
||||||
|
|
||||||
|
if o.Content == "" {
|
||||||
|
stdin, err := stdin.Read()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to read stdin")
|
||||||
|
}
|
||||||
|
if stdin != "" {
|
||||||
|
// Sanitize the input from stdin by removing backspace sequences.
|
||||||
|
backspace := regexp.MustCompile(".\x08")
|
||||||
|
o.Content = backspace.ReplaceAllString(stdin, "")
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("provide some content to display")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
model := model{
|
||||||
|
viewport: vp,
|
||||||
|
helpStyle: o.HelpStyle.ToLipgloss(),
|
||||||
|
content: o.Content,
|
||||||
|
origContent: o.Content,
|
||||||
|
showLineNumbers: o.ShowLineNumbers,
|
||||||
|
lineNumberStyle: o.LineNumberStyle.ToLipgloss(),
|
||||||
|
softWrap: o.SoftWrap,
|
||||||
|
matchStyle: o.MatchStyle.ToLipgloss(),
|
||||||
|
matchHighlightStyle: o.MatchHighlightStyle.ToLipgloss(),
|
||||||
|
timeout: o.Timeout,
|
||||||
|
hasTimeout: o.Timeout > 0,
|
||||||
|
}
|
||||||
|
_, err := tea.NewProgram(model, tea.WithAltScreen()).Run()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to start program: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
21
pager/options.go
Normal file
21
pager/options.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package pager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/gum/style"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options are the options for the pager.
|
||||||
|
type Options struct {
|
||||||
|
//nolint:staticcheck
|
||||||
|
Style style.Styles `embed:"" help:"Style the pager" set:"defaultBorder=rounded" set:"defaultPadding=0 1" set:"defaultBorderForeground=212" envprefix:"GUM_PAGER_"`
|
||||||
|
HelpStyle style.Styles `embed:"" prefix:"help." help:"Style the help text" set:"defaultForeground=241" envprefix:"GUM_PAGER_HELP_"`
|
||||||
|
Content string `arg:"" optional:"" help:"Display content to scroll"`
|
||||||
|
ShowLineNumbers bool `help:"Show line numbers" default:"true"`
|
||||||
|
LineNumberStyle style.Styles `embed:"" prefix:"line-number." help:"Style the line numbers" set:"defaultForeground=237" envprefix:"GUM_PAGER_LINE_NUMBER_"`
|
||||||
|
SoftWrap bool `help:"Soft wrap lines" default:"false"`
|
||||||
|
MatchStyle style.Styles `embed:"" prefix:"match." help:"Style the matched text" set:"defaultForeground=212" set:"defaultBold=true" envprefix:"GUM_PAGER_MATCH_"` //nolint:staticcheck
|
||||||
|
MatchHighlightStyle style.Styles `embed:"" prefix:"match-highlight." help:"Style the matched highlight text" set:"defaultForeground=235" set:"defaultBackground=225" set:"defaultBold=true" envprefix:"GUM_PAGER_MATCH_HIGH_"` //nolint:staticcheck
|
||||||
|
Timeout time.Duration `help:"Timeout until command exits" default:"0" env:"GUM_PAGER_TIMEOUT"`
|
||||||
|
}
|
159
pager/pager.go
Normal file
159
pager/pager.go
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
// Package pager provides a pager (similar to less) for the terminal.
|
||||||
|
//
|
||||||
|
// $ cat file.txt | gum pager
|
||||||
|
package pager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/gum/timeout"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/muesli/reflow/truncate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type model struct {
|
||||||
|
content string
|
||||||
|
origContent string
|
||||||
|
viewport viewport.Model
|
||||||
|
helpStyle lipgloss.Style
|
||||||
|
showLineNumbers bool
|
||||||
|
lineNumberStyle lipgloss.Style
|
||||||
|
softWrap bool
|
||||||
|
search search
|
||||||
|
matchStyle lipgloss.Style
|
||||||
|
matchHighlightStyle lipgloss.Style
|
||||||
|
maxWidth int
|
||||||
|
timeout time.Duration
|
||||||
|
hasTimeout bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Init() tea.Cmd {
|
||||||
|
return timeout.Init(m.timeout, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case timeout.TickTimeoutMsg:
|
||||||
|
if msg.TimeoutValue <= 0 {
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
m.timeout = msg.TimeoutValue
|
||||||
|
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
|
||||||
|
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.ProcessText(msg)
|
||||||
|
case tea.KeyMsg:
|
||||||
|
return m.KeyHandler(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) ProcessText(msg tea.WindowSizeMsg) {
|
||||||
|
m.viewport.Height = msg.Height - lipgloss.Height(m.helpStyle.Render("?")) - 1
|
||||||
|
m.viewport.Width = msg.Width
|
||||||
|
textStyle := lipgloss.NewStyle().Width(m.viewport.Width)
|
||||||
|
var text strings.Builder
|
||||||
|
|
||||||
|
// Determine max width of a line.
|
||||||
|
m.maxWidth = m.viewport.Width
|
||||||
|
if m.softWrap {
|
||||||
|
vpStyle := m.viewport.Style
|
||||||
|
m.maxWidth -= vpStyle.GetHorizontalBorderSize() + vpStyle.GetHorizontalMargins() + vpStyle.GetHorizontalPadding()
|
||||||
|
if m.showLineNumbers {
|
||||||
|
m.maxWidth -= lipgloss.Width(" │ ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, line := range strings.Split(m.content, "\n") {
|
||||||
|
line = strings.ReplaceAll(line, "\t", " ")
|
||||||
|
if m.showLineNumbers {
|
||||||
|
text.WriteString(m.lineNumberStyle.Render(fmt.Sprintf("%4d │ ", i+1)))
|
||||||
|
}
|
||||||
|
for m.softWrap && lipgloss.Width(line) > m.maxWidth {
|
||||||
|
truncatedLine := truncate.String(line, uint(m.maxWidth))
|
||||||
|
text.WriteString(textStyle.Render(truncatedLine))
|
||||||
|
text.WriteString("\n")
|
||||||
|
if m.showLineNumbers {
|
||||||
|
text.WriteString(m.lineNumberStyle.Render(" │ "))
|
||||||
|
}
|
||||||
|
line = strings.Replace(line, truncatedLine, "", 1)
|
||||||
|
}
|
||||||
|
text.WriteString(textStyle.Render(truncate.String(line, uint(m.maxWidth))))
|
||||||
|
text.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
diffHeight := m.viewport.Height - lipgloss.Height(text.String())
|
||||||
|
if diffHeight > 0 && m.showLineNumbers {
|
||||||
|
remainingLines := " ~ │ " + strings.Repeat("\n ~ │ ", diffHeight-1)
|
||||||
|
text.WriteString(m.lineNumberStyle.Render(remainingLines))
|
||||||
|
}
|
||||||
|
m.viewport.SetContent(text.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
const heightOffset = 2
|
||||||
|
|
||||||
|
func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) {
|
||||||
|
var cmd tea.Cmd
|
||||||
|
if m.search.active {
|
||||||
|
switch key.String() {
|
||||||
|
case "enter":
|
||||||
|
if m.search.input.Value() != "" {
|
||||||
|
m.content = m.origContent
|
||||||
|
m.search.Execute(&m)
|
||||||
|
|
||||||
|
// Trigger a view update to highlight the found matches.
|
||||||
|
m.search.NextMatch(&m)
|
||||||
|
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
|
||||||
|
} else {
|
||||||
|
m.search.Done()
|
||||||
|
}
|
||||||
|
case "ctrl+d", "ctrl+c", "esc":
|
||||||
|
m.search.Done()
|
||||||
|
default:
|
||||||
|
m.search.input, cmd = m.search.input.Update(key)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch key.String() {
|
||||||
|
case "g", "home":
|
||||||
|
m.viewport.GotoTop()
|
||||||
|
case "G", "end":
|
||||||
|
m.viewport.GotoBottom()
|
||||||
|
case "/":
|
||||||
|
m.search.Begin()
|
||||||
|
case "p", "N":
|
||||||
|
m.search.PrevMatch(&m)
|
||||||
|
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
|
||||||
|
case "n":
|
||||||
|
m.search.NextMatch(&m)
|
||||||
|
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
|
||||||
|
case "q", "ctrl+c", "esc":
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
m.viewport, cmd = m.viewport.Update(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) View() string {
|
||||||
|
var timeoutStr string
|
||||||
|
if m.hasTimeout {
|
||||||
|
timeoutStr = timeout.Str(m.timeout) + " "
|
||||||
|
}
|
||||||
|
helpMsg := "\n" + timeoutStr + " ↑/↓: Navigate • q: Quit • /: Search "
|
||||||
|
if m.search.query != nil {
|
||||||
|
helpMsg += "• n: Next Match "
|
||||||
|
helpMsg += "• N: Prev Match "
|
||||||
|
}
|
||||||
|
if m.search.active {
|
||||||
|
return m.viewport.View() + "\n" + timeoutStr + " " + m.search.input.View()
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.viewport.View() + m.helpStyle.Render(helpMsg)
|
||||||
|
}
|
164
pager/search.go
Normal file
164
pager/search.go
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
package pager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
|
"github.com/charmbracelet/gum/internal/utils"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/muesli/reflow/truncate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type search struct {
|
||||||
|
active bool
|
||||||
|
input textinput.Model
|
||||||
|
query *regexp.Regexp
|
||||||
|
matchIndex int
|
||||||
|
matchLipglossStr string
|
||||||
|
matchString string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *search) new() {
|
||||||
|
input := textinput.New()
|
||||||
|
input.Placeholder = "search"
|
||||||
|
input.Prompt = "/"
|
||||||
|
input.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
||||||
|
s.input = input
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *search) Begin() {
|
||||||
|
s.new()
|
||||||
|
s.active = true
|
||||||
|
s.input.Focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute find all lines in the model with a match.
|
||||||
|
func (s *search) Execute(m *model) {
|
||||||
|
defer s.Done()
|
||||||
|
if s.input.Value() == "" {
|
||||||
|
s.query = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
s.query, err = regexp.Compile(s.input.Value())
|
||||||
|
if err != nil {
|
||||||
|
s.query = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query := regexp.MustCompile(fmt.Sprintf("(%s)", s.query.String()))
|
||||||
|
m.content = query.ReplaceAllString(m.content, m.matchStyle.Render("$1"))
|
||||||
|
|
||||||
|
// Recompile the regex to match the an replace the highlights.
|
||||||
|
leftPad, _ := utils.LipglossPadding(m.matchStyle)
|
||||||
|
matchingString := regexp.QuoteMeta(m.matchStyle.Render()[:leftPad]) + s.query.String() + regexp.QuoteMeta(m.matchStyle.Render()[leftPad:])
|
||||||
|
s.query, err = regexp.Compile(matchingString)
|
||||||
|
if err != nil {
|
||||||
|
s.query = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *search) Done() {
|
||||||
|
s.active = false
|
||||||
|
|
||||||
|
// To account for the first match is always executed.
|
||||||
|
s.matchIndex = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *search) NextMatch(m *model) {
|
||||||
|
// Check that we are within bounds.
|
||||||
|
if s.query == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove previous highlight.
|
||||||
|
m.content = strings.Replace(m.content, s.matchLipglossStr, s.matchString, 1)
|
||||||
|
|
||||||
|
// Highlight the next match.
|
||||||
|
allMatches := s.query.FindAllStringIndex(m.content, -1)
|
||||||
|
if len(allMatches) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
leftPad, rightPad := utils.LipglossPadding(m.matchStyle)
|
||||||
|
s.matchIndex = (s.matchIndex + 1) % len(allMatches)
|
||||||
|
match := allMatches[s.matchIndex]
|
||||||
|
lhs := m.content[:match[0]]
|
||||||
|
rhs := m.content[match[0]:]
|
||||||
|
s.matchString = m.content[match[0]:match[1]]
|
||||||
|
s.matchLipglossStr = m.matchHighlightStyle.Render(s.matchString[leftPad : len(s.matchString)-rightPad])
|
||||||
|
m.content = lhs + strings.Replace(rhs, m.content[match[0]:match[1]], s.matchLipglossStr, 1)
|
||||||
|
|
||||||
|
// Update the viewport position.
|
||||||
|
var line int
|
||||||
|
formatStr := softWrapEm(m.content, m.maxWidth, m.softWrap)
|
||||||
|
index := strings.Index(formatStr, s.matchLipglossStr)
|
||||||
|
if index != -1 {
|
||||||
|
line = strings.Count(formatStr[:index], "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update if the match is not within the viewport.
|
||||||
|
if index != -1 && (line > m.viewport.YOffset-1+m.viewport.VisibleLineCount()-1 || line < m.viewport.YOffset) {
|
||||||
|
m.viewport.SetYOffset(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *search) PrevMatch(m *model) {
|
||||||
|
// Check that we are within bounds.
|
||||||
|
if s.query == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove previous highlight.
|
||||||
|
m.content = strings.Replace(m.content, s.matchLipglossStr, s.matchString, 1)
|
||||||
|
|
||||||
|
// Highlight the previous match.
|
||||||
|
allMatches := s.query.FindAllStringIndex(m.content, -1)
|
||||||
|
if len(allMatches) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.matchIndex = (s.matchIndex - 1) % len(allMatches)
|
||||||
|
if s.matchIndex < 0 {
|
||||||
|
s.matchIndex = len(allMatches) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
leftPad, rightPad := utils.LipglossPadding(m.matchStyle)
|
||||||
|
match := allMatches[s.matchIndex]
|
||||||
|
lhs := m.content[:match[0]]
|
||||||
|
rhs := m.content[match[0]:]
|
||||||
|
s.matchString = m.content[match[0]:match[1]]
|
||||||
|
s.matchLipglossStr = m.matchHighlightStyle.Render(s.matchString[leftPad : len(s.matchString)-rightPad])
|
||||||
|
m.content = lhs + strings.Replace(rhs, m.content[match[0]:match[1]], s.matchLipglossStr, 1)
|
||||||
|
|
||||||
|
// Update the viewport position.
|
||||||
|
var line int
|
||||||
|
formatStr := softWrapEm(m.content, m.maxWidth, m.softWrap)
|
||||||
|
index := strings.Index(formatStr, s.matchLipglossStr)
|
||||||
|
if index != -1 {
|
||||||
|
line = strings.Count(formatStr[:index], "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update if the match is not within the viewport.
|
||||||
|
if index != -1 && (line > m.viewport.YOffset-1+m.viewport.VisibleLineCount()-1 || line < m.viewport.YOffset) {
|
||||||
|
m.viewport.SetYOffset(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func softWrapEm(str string, maxWidth int, softWrap bool) string {
|
||||||
|
var text strings.Builder
|
||||||
|
for _, line := range strings.Split(str, "\n") {
|
||||||
|
for softWrap && lipgloss.Width(line) > maxWidth {
|
||||||
|
truncatedLine := truncate.String(line, uint(maxWidth))
|
||||||
|
text.WriteString(truncatedLine)
|
||||||
|
text.WriteString("\n")
|
||||||
|
line = strings.Replace(line, truncatedLine, "", 1)
|
||||||
|
}
|
||||||
|
text.WriteString(truncate.String(line, uint(maxWidth)))
|
||||||
|
text.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.String()
|
||||||
|
}
|
|
@ -4,49 +4,66 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
|
||||||
"github.com/charmbracelet/bubbles/spinner"
|
"github.com/charmbracelet/bubbles/spinner"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
|
|
||||||
"github.com/charmbracelet/gum/internal/exit"
|
"github.com/charmbracelet/gum/internal/exit"
|
||||||
"github.com/charmbracelet/gum/style"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run provides a shell script interface for the spinner bubble.
|
// Run provides a shell script interface for the spinner bubble.
|
||||||
// https://github.com/charmbracelet/bubbles/spinner
|
// https://github.com/charmbracelet/bubbles/spinner
|
||||||
func (o Options) Run() error {
|
func (o Options) Run() error {
|
||||||
|
isTTY := isatty.IsTerminal(os.Stdout.Fd())
|
||||||
|
|
||||||
s := spinner.New()
|
s := spinner.New()
|
||||||
s.Style = o.SpinnerStyle.ToLipgloss()
|
s.Style = o.SpinnerStyle.ToLipgloss()
|
||||||
s.Spinner = spinnerMap[o.Spinner]
|
s.Spinner = spinnerMap[o.Spinner]
|
||||||
m := model{
|
m := model{
|
||||||
spinner: s,
|
spinner: s,
|
||||||
title: o.TitleStyle.ToLipgloss().Render(o.Title),
|
title: o.TitleStyle.ToLipgloss().Render(o.Title),
|
||||||
command: o.Command,
|
command: o.Command,
|
||||||
align: o.Align,
|
align: o.Align,
|
||||||
|
showOutput: o.ShowOutput && isTTY,
|
||||||
|
showError: o.ShowError,
|
||||||
|
timeout: o.Timeout,
|
||||||
|
hasTimeout: o.Timeout > 0,
|
||||||
}
|
}
|
||||||
p := tea.NewProgram(m, tea.WithOutput(os.Stderr))
|
p := tea.NewProgram(m, tea.WithOutput(os.Stderr))
|
||||||
mm, err := p.StartReturningModel()
|
mm, err := p.Run()
|
||||||
m = mm.(model)
|
m = mm.(model)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to run spin: %w", err)
|
return fmt.Errorf("failed to run spin: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.ShowOutput {
|
|
||||||
fmt.Fprint(os.Stdout, m.stdout)
|
|
||||||
fmt.Fprint(os.Stderr, m.stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.aborted {
|
if m.aborted {
|
||||||
return exit.ErrAborted
|
return exit.ErrAborted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the command succeeds, and we are printing output and we are in a TTY then push the STDOUT we got to the actual
|
||||||
|
// STDOUT for piping or other things.
|
||||||
|
//nolint:nestif
|
||||||
|
if m.status == 0 {
|
||||||
|
if o.ShowOutput {
|
||||||
|
// BubbleTea writes the View() to stderr.
|
||||||
|
// If the program is being piped then put the accumulated output in stdout.
|
||||||
|
if !isTTY {
|
||||||
|
_, err := os.Stdout.WriteString(m.stdout)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write to stdout: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if o.ShowError {
|
||||||
|
// Otherwise if we are showing errors and the command did not exit with a 0 status code then push all of the command
|
||||||
|
// output to the terminal. This way failed commands can be debugged.
|
||||||
|
_, err := os.Stdout.WriteString(m.output)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write to stdout: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
os.Exit(m.status)
|
os.Exit(m.status)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeReset hook. Used to unclutter style flags.
|
|
||||||
func (o Options) BeforeReset(ctx *kong.Context) error {
|
|
||||||
style.HideFlags(ctx)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
package spin
|
package spin
|
||||||
|
|
||||||
import "github.com/charmbracelet/gum/style"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/gum/style"
|
||||||
|
)
|
||||||
|
|
||||||
// Options is the customization options for the spin command.
|
// Options is the customization options for the spin command.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Command []string `arg:"" help:"Command to run"`
|
Command []string `arg:"" help:"Command to run"`
|
||||||
|
|
||||||
ShowOutput bool `help:"Show output of command" default:"false" env:"GUM_SPIN_SHOW_OUTPUT"`
|
ShowOutput bool `help:"Show or pipe output of command during execution" default:"false" env:"GUM_SPIN_SHOW_OUTPUT"`
|
||||||
Spinner string `help:"Spinner type" short:"s" type:"spinner" enum:"line,dot,minidot,jump,pulse,points,globe,moon,monkey,meter,hamburger" default:"dot" env:"GUM_SPIN_SPINNER"`
|
ShowError bool `help:"Show output of command only if the command fails" default:"false" env:"GUM_SPIN_SHOW_ERROR"`
|
||||||
SpinnerStyle style.Styles `embed:"" prefix:"spinner." set:"defaultForeground=212" envprefix:"GUM_SPIN_SPINNER_"`
|
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"`
|
||||||
Title string `help:"Text to display to user while spinning" default:"Loading..." env:"GUM_SPIN_TITLE"`
|
SpinnerStyle style.Styles `embed:"" prefix:"spinner." set:"defaultForeground=212" envprefix:"GUM_SPIN_SPINNER_"`
|
||||||
TitleStyle style.Styles `embed:"" prefix:"title." envprefix:"GUM_SPIN_TITLE_"`
|
Title string `help:"Text to display to user while spinning" default:"Loading..." env:"GUM_SPIN_TITLE"`
|
||||||
Align string `help:"Alignment of spinner with regard to the title" short:"a" type:"align" enum:"left,right" default:"left" env:"GUM_SPIN_ALIGN"`
|
TitleStyle style.Styles `embed:"" prefix:"title." envprefix:"GUM_SPIN_TITLE_"`
|
||||||
|
Align string `help:"Alignment of spinner with regard to the title" short:"a" type:"align" enum:"left,right" default:"left" env:"GUM_SPIN_ALIGN"`
|
||||||
|
Timeout time.Duration `help:"Timeout until spin command aborts" default:"0" env:"GUM_SPIN_TIMEOUT"`
|
||||||
}
|
}
|
||||||
|
|
80
spin/spin.go
80
spin/spin.go
|
@ -15,28 +15,45 @@
|
||||||
package spin
|
package spin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/gum/internal/exit"
|
||||||
|
"github.com/charmbracelet/gum/timeout"
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/spinner"
|
"github.com/charmbracelet/bubbles/spinner"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
type model struct {
|
type model struct {
|
||||||
spinner spinner.Model
|
spinner spinner.Model
|
||||||
title string
|
title string
|
||||||
align string
|
align string
|
||||||
command []string
|
command []string
|
||||||
aborted bool
|
quitting bool
|
||||||
|
aborted bool
|
||||||
status int
|
status int
|
||||||
stdout string
|
stdout string
|
||||||
stderr 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 {
|
type finishCommandMsg struct {
|
||||||
stdout string
|
stdout string
|
||||||
stderr string
|
stderr string
|
||||||
|
output string
|
||||||
status int
|
status int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,9 +65,15 @@ func commandStart(command []string) tea.Cmd {
|
||||||
}
|
}
|
||||||
cmd := exec.Command(command[0], args...) //nolint:gosec
|
cmd := exec.Command(command[0], args...) //nolint:gosec
|
||||||
|
|
||||||
var outbuf, errbuf strings.Builder
|
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||||
cmd.Stdout = &outbuf
|
stdout := io.MultiWriter(&bothbuf, &errbuf)
|
||||||
cmd.Stderr = &errbuf
|
stderr := io.MultiWriter(&bothbuf, &outbuf)
|
||||||
|
|
||||||
|
cmd.Stdout = stdout
|
||||||
|
cmd.Stderr = stderr
|
||||||
|
} else {
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
_ = cmd.Run()
|
_ = cmd.Run()
|
||||||
|
|
||||||
|
@ -63,6 +86,7 @@ func commandStart(command []string) tea.Cmd {
|
||||||
return finishCommandMsg{
|
return finishCommandMsg{
|
||||||
stdout: outbuf.String(),
|
stdout: outbuf.String(),
|
||||||
stderr: errbuf.String(),
|
stderr: errbuf.String(),
|
||||||
|
output: bothbuf.String(),
|
||||||
status: status,
|
status: status,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,23 +96,49 @@ func (m model) Init() tea.Cmd {
|
||||||
return tea.Batch(
|
return tea.Batch(
|
||||||
m.spinner.Tick,
|
m.spinner.Tick,
|
||||||
commandStart(m.command),
|
commandStart(m.command),
|
||||||
|
timeout.Init(m.timeout, nil),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
func (m model) View() string {
|
func (m model) View() string {
|
||||||
if m.align == "left" {
|
if m.quitting && m.showOutput {
|
||||||
return m.spinner.View() + " " + m.title
|
return strings.TrimPrefix(errbuf.String()+"\n"+outbuf.String(), "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.title + " " + m.spinner.View()
|
var str string
|
||||||
|
if m.hasTimeout {
|
||||||
|
str = timeout.Str(m.timeout)
|
||||||
|
}
|
||||||
|
var header string
|
||||||
|
if m.align == "left" {
|
||||||
|
header = m.spinner.View() + str + " " + m.title
|
||||||
|
} else {
|
||||||
|
header = str + " " + m.title + " " + m.spinner.View()
|
||||||
|
}
|
||||||
|
if !m.showOutput {
|
||||||
|
return header
|
||||||
|
}
|
||||||
|
return header + errbuf.String() + "\n" + outbuf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
case timeout.TickTimeoutMsg:
|
||||||
|
if msg.TimeoutValue <= 0 {
|
||||||
|
// grab current output before closing for piped instances
|
||||||
|
m.stdout = outbuf.String()
|
||||||
|
|
||||||
|
m.status = exit.StatusAborted
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
m.timeout = msg.TimeoutValue
|
||||||
|
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
|
||||||
case finishCommandMsg:
|
case finishCommandMsg:
|
||||||
m.stdout = msg.stdout
|
m.stdout = msg.stdout
|
||||||
m.stderr = msg.stderr
|
m.stderr = msg.stderr
|
||||||
|
m.output = msg.output
|
||||||
m.status = msg.status
|
m.status = msg.status
|
||||||
|
m.quitting = true
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
|
|
|
@ -2,8 +2,8 @@ package style
|
||||||
|
|
||||||
import "github.com/charmbracelet/lipgloss"
|
import "github.com/charmbracelet/lipgloss"
|
||||||
|
|
||||||
// border maps strings to `lipgloss.Border`s.
|
// Border maps strings to `lipgloss.Border`s.
|
||||||
var border map[string]lipgloss.Border = map[string]lipgloss.Border{
|
var Border map[string]lipgloss.Border = map[string]lipgloss.Border{
|
||||||
"double": lipgloss.DoubleBorder(),
|
"double": lipgloss.DoubleBorder(),
|
||||||
"hidden": lipgloss.HiddenBorder(),
|
"hidden": lipgloss.HiddenBorder(),
|
||||||
"none": {},
|
"none": {},
|
||||||
|
|
|
@ -6,32 +6,25 @@
|
||||||
package style
|
package style
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
"github.com/charmbracelet/gum/internal/stdin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run provides a shell script interface for the Lip Gloss styling.
|
// Run provides a shell script interface for the Lip Gloss styling.
|
||||||
// https://github.com/charmbracelet/lipgloss
|
// https://github.com/charmbracelet/lipgloss
|
||||||
func (o Options) Run() error {
|
func (o Options) Run() error {
|
||||||
text := strings.Join(o.Text, "\n")
|
var text string
|
||||||
|
if len(o.Text) > 0 {
|
||||||
|
text = strings.Join(o.Text, "\n")
|
||||||
|
} else {
|
||||||
|
text, _ = stdin.Read()
|
||||||
|
if text == "" {
|
||||||
|
return errors.New("no input provided, see `gum style --help`")
|
||||||
|
}
|
||||||
|
}
|
||||||
fmt.Println(o.Style.ToLipgloss().Render(text))
|
fmt.Println(o.Style.ToLipgloss().Render(text))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HideFlags hides the flags from the usage output. This is used in conjunction
|
|
||||||
// with BeforeReset hook.
|
|
||||||
func HideFlags(ctx *kong.Context) {
|
|
||||||
n := ctx.Selected()
|
|
||||||
if n == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, f := range n.Flags {
|
|
||||||
if g := f.Group; g != nil && g.Key == groupName {
|
|
||||||
if !strings.HasSuffix(f.Name, ".foreground") {
|
|
||||||
f.Hidden = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -15,7 +15,28 @@ func (s Styles) ToLipgloss() lipgloss.Style {
|
||||||
BorderBackground(lipgloss.Color(s.BorderBackground)).
|
BorderBackground(lipgloss.Color(s.BorderBackground)).
|
||||||
BorderForeground(lipgloss.Color(s.BorderForeground)).
|
BorderForeground(lipgloss.Color(s.BorderForeground)).
|
||||||
Align(decode.Align[s.Align]).
|
Align(decode.Align[s.Align]).
|
||||||
Border(border[s.Border]).
|
Border(Border[s.Border]).
|
||||||
|
Height(s.Height).
|
||||||
|
Width(s.Width).
|
||||||
|
Margin(parseMargin(s.Margin)).
|
||||||
|
Padding(parsePadding(s.Padding)).
|
||||||
|
Bold(s.Bold).
|
||||||
|
Faint(s.Faint).
|
||||||
|
Italic(s.Italic).
|
||||||
|
Strikethrough(s.Strikethrough).
|
||||||
|
Underline(s.Underline)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToLipgloss takes a Styles flag set and returns the corresponding
|
||||||
|
// lipgloss.Style.
|
||||||
|
func (s StylesNotHidden) ToLipgloss() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color(s.Background)).
|
||||||
|
Foreground(lipgloss.Color(s.Foreground)).
|
||||||
|
BorderBackground(lipgloss.Color(s.BorderBackground)).
|
||||||
|
BorderForeground(lipgloss.Color(s.BorderForeground)).
|
||||||
|
Align(decode.Align[s.Align]).
|
||||||
|
Border(Border[s.Border]).
|
||||||
Height(s.Height).
|
Height(s.Height).
|
||||||
Width(s.Width).
|
Width(s.Width).
|
||||||
Margin(parseMargin(s.Margin)).
|
Margin(parseMargin(s.Margin)).
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
package style
|
package style
|
||||||
|
|
||||||
const (
|
|
||||||
groupName = "Style Flags"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Options is the customization options for the style command.
|
// Options is the customization options for the style command.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Text []string `arg:"" optional:"" help:"Text to which to apply the style"`
|
Text []string `arg:"" optional:"" help:"Text to which to apply the style"`
|
||||||
Style Styles `embed:""`
|
Style StylesNotHidden `embed:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Styles is a flag set of possible styles.
|
// Styles is a flag set of possible styles.
|
||||||
|
@ -18,25 +14,55 @@ type Options struct {
|
||||||
// components, through embedding and prefixing.
|
// components, through embedding and prefixing.
|
||||||
type Styles struct {
|
type Styles struct {
|
||||||
// Colors
|
// 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"`
|
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
|
||||||
Border string `help:"Border Style" enum:"none,hidden,normal,rounded,thick,double" default:"none" group:"Style Flags" env:"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" env:"BORDER_BACKGROUND"`
|
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" env:"BORDER_FOREGROUND"`
|
BorderForeground string `help:"Border Foreground Color" group:"Style Flags" default:"${defaultBorderForeground}" env:"BORDER_FOREGROUND" hidden:"true"`
|
||||||
|
|
||||||
// Layout
|
// Layout
|
||||||
Align string `help:"Text Alignment" enum:"left,center,right,bottom,middle,top" default:"left" group:"Style Flags" env:"ALIGN"`
|
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" group:"Style Flags" env:"HEIGHT"`
|
Height int `help:"Text height" default:"${defaultHeight}" group:"Style Flags" env:"HEIGHT" hidden:"true"`
|
||||||
Width int `help:"Text width" group:"Style Flags" env:"WIDTH"`
|
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"`
|
||||||
|
BorderBackground string `help:"Border Background Color" group:"Style Flags" default:"${defaultBorderBackground}" env:"BORDER_BACKGROUND"`
|
||||||
|
BorderForeground string `help:"Border Foreground Color" group:"Style Flags" default:"${defaultBorderForeground}" env:"BORDER_FOREGROUND"`
|
||||||
|
|
||||||
|
// Layout
|
||||||
|
Align string `help:"Text Alignment" enum:"left,center,right,bottom,middle,top" default:"${defaultAlign}" group:"Style Flags" env:"ALIGN"`
|
||||||
|
Height int `help:"Text height" default:"${defaultHeight}" group:"Style Flags" env:"HEIGHT"`
|
||||||
|
Width int `help:"Text width" default:"${defaultWidth}" group:"Style Flags" env:"WIDTH"`
|
||||||
Margin string `help:"Text margin" default:"${defaultMargin}" group:"Style Flags" env:"MARGIN"`
|
Margin string `help:"Text margin" default:"${defaultMargin}" group:"Style Flags" env:"MARGIN"`
|
||||||
Padding string `help:"Text padding" default:"${defaultPadding}" group:"Style Flags" env:"PADDING"`
|
Padding string `help:"Text padding" default:"${defaultPadding}" group:"Style Flags" env:"PADDING"`
|
||||||
|
|
||||||
// Format
|
// Format
|
||||||
Bold bool `help:"Bold text" group:"Style Flags" env:"BOLD"`
|
Bold bool `help:"Bold text" default:"${defaultBold}" group:"Style Flags" env:"BOLD"`
|
||||||
Faint bool `help:"Faint text" group:"Style Flags" env:"FAINT"`
|
Faint bool `help:"Faint text" default:"${defaultFaint}" group:"Style Flags" env:"FAINT"`
|
||||||
Italic bool `help:"Italicize text" group:"Style Flags" env:"ITALIC"`
|
Italic bool `help:"Italicize text" default:"${defaultItalic}" group:"Style Flags" env:"ITALIC"`
|
||||||
Strikethrough bool `help:"Strikethrough text" group:"Style Flags" env:"STRIKETHROUGH"`
|
Strikethrough bool `help:"Strikethrough text" default:"${defaultStrikethrough}" group:"Style Flags" env:"STRIKETHROUGH"`
|
||||||
Underline bool `help:"Underline text" default:"${defaultUnderline}" group:"Style Flags" env:"UNDERLINE"`
|
Underline bool `help:"Underline text" default:"${defaultUnderline}" group:"Style Flags" env:"UNDERLINE"`
|
||||||
}
|
}
|
||||||
|
|
5
table/comma.csv
Normal file
5
table/comma.csv
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
Bubble Gum,Price,Ingredients
|
||||||
|
Strawberry,$0.88,"Water,Sugar"
|
||||||
|
Guava,$1.00,"Guava Flavoring,Food Coloring,Xanthan Gum"
|
||||||
|
Orange,$0.99,"Sugar,Dextrose,Glucose"
|
||||||
|
Cinnamon,$0.50,"Cin""na""mon"
|
|
131
table/command.go
Normal file
131
table/command.go
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/table"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
ltable "github.com/charmbracelet/lipgloss/table"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/gum/internal/stdin"
|
||||||
|
"github.com/charmbracelet/gum/style"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run provides a shell script interface for rendering tabular data (CSV).
|
||||||
|
func (o Options) Run() error {
|
||||||
|
var reader *csv.Reader
|
||||||
|
if o.File != "" {
|
||||||
|
file, err := os.Open(o.File)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not find file at path %s", o.File)
|
||||||
|
}
|
||||||
|
reader = csv.NewReader(file)
|
||||||
|
} else {
|
||||||
|
if stdin.IsEmpty() {
|
||||||
|
return fmt.Errorf("no data provided")
|
||||||
|
}
|
||||||
|
reader = csv.NewReader(os.Stdin)
|
||||||
|
}
|
||||||
|
|
||||||
|
separatorRunes := []rune(o.Separator)
|
||||||
|
if len(separatorRunes) != 1 {
|
||||||
|
return fmt.Errorf("separator must be single character")
|
||||||
|
}
|
||||||
|
reader.Comma = separatorRunes[0]
|
||||||
|
|
||||||
|
writer := csv.NewWriter(os.Stdout)
|
||||||
|
writer.Comma = separatorRunes[0]
|
||||||
|
|
||||||
|
var columnNames []string
|
||||||
|
var err error
|
||||||
|
// If no columns are provided we'll use the first row of the CSV as the
|
||||||
|
// column names.
|
||||||
|
if len(o.Columns) <= 0 {
|
||||||
|
columnNames, err = reader.Read()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to parse columns")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
columnNames = o.Columns
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := reader.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid data provided")
|
||||||
|
}
|
||||||
|
columns := make([]table.Column, 0, len(columnNames))
|
||||||
|
|
||||||
|
for i, title := range columnNames {
|
||||||
|
width := lipgloss.Width(title)
|
||||||
|
if len(o.Widths) > i {
|
||||||
|
width = o.Widths[i]
|
||||||
|
}
|
||||||
|
columns = append(columns, table.Column{
|
||||||
|
Title: title,
|
||||||
|
Width: width,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultStyles := table.DefaultStyles()
|
||||||
|
|
||||||
|
styles := table.Styles{
|
||||||
|
Cell: defaultStyles.Cell.Inherit(o.CellStyle.ToLipgloss()),
|
||||||
|
Header: defaultStyles.Header.Inherit(o.HeaderStyle.ToLipgloss()),
|
||||||
|
Selected: o.SelectedStyle.ToLipgloss(),
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]table.Row, 0, len(data))
|
||||||
|
for _, row := range data {
|
||||||
|
if len(row) > len(columns) {
|
||||||
|
return fmt.Errorf("invalid number of columns")
|
||||||
|
}
|
||||||
|
rows = append(rows, table.Row(row))
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.Print {
|
||||||
|
table := ltable.New().
|
||||||
|
Headers(columnNames...).
|
||||||
|
Rows(data...).
|
||||||
|
BorderStyle(o.BorderStyle.ToLipgloss()).
|
||||||
|
Border(style.Border[o.Border]).
|
||||||
|
StyleFunc(func(row, _ int) lipgloss.Style {
|
||||||
|
if row == 0 {
|
||||||
|
return styles.Header
|
||||||
|
}
|
||||||
|
return styles.Cell
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Println(table.Render())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
table := table.New(
|
||||||
|
table.WithColumns(columns),
|
||||||
|
table.WithFocused(true),
|
||||||
|
table.WithHeight(o.Height),
|
||||||
|
table.WithRows(rows),
|
||||||
|
table.WithStyles(styles),
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tm == nil {
|
||||||
|
return fmt.Errorf("failed to get selection")
|
||||||
|
}
|
||||||
|
|
||||||
|
m := tm.(model)
|
||||||
|
|
||||||
|
if err = writer.Write([]string(m.selected)); err != nil {
|
||||||
|
return fmt.Errorf("failed to write selected row: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Flush()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
19
table/example.csv
Normal file
19
table/example.csv
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
Bubble Gum Flavor,Price
|
||||||
|
Strawberry,$0.99
|
||||||
|
Cherry,$0.50
|
||||||
|
Banana,$0.75
|
||||||
|
Orange,$0.25
|
||||||
|
Lemon,$0.50
|
||||||
|
Lime,$0.50
|
||||||
|
Grape,$0.50
|
||||||
|
Watermelon,$0.50
|
||||||
|
Pineapple,$0.50
|
||||||
|
Blueberry,$0.50
|
||||||
|
Raspberry,$0.50
|
||||||
|
Cranberry,$0.50
|
||||||
|
Peach,$0.50
|
||||||
|
Apple,$0.50
|
||||||
|
Mango,$0.50
|
||||||
|
Pomegranate,$0.50
|
||||||
|
Coconut,$0.50
|
||||||
|
Cinnamon,$0.50
|
|
19
table/invalid.csv
Normal file
19
table/invalid.csv
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
Bubble Gum Flavor
|
||||||
|
Strawberry,$0.99
|
||||||
|
Cherry,$0.50
|
||||||
|
Banana,$0.75
|
||||||
|
Orange
|
||||||
|
Lemon,$0.50
|
||||||
|
Lime,$0.50
|
||||||
|
Grape,$0.50
|
||||||
|
Watermelon,$0.50
|
||||||
|
Pineapple,$0.50
|
||||||
|
Blueberry,$0.50
|
||||||
|
Raspberry,$0.50
|
||||||
|
Cranberry,$0.50
|
||||||
|
Peach,$0.50
|
||||||
|
Apple,$0.50
|
||||||
|
Mango,$0.50
|
||||||
|
Pomegranate,$0.50
|
||||||
|
Coconut,$0.50
|
||||||
|
Cinnamon,$0.50
|
|
19
table/options.go
Normal file
19
table/options.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package table
|
||||||
|
|
||||||
|
import "github.com/charmbracelet/gum/style"
|
||||||
|
|
||||||
|
// Options is the customization options for the table command.
|
||||||
|
type Options struct {
|
||||||
|
Separator string `short:"s" help:"Row separator" default:","`
|
||||||
|
Columns []string `short:"c" help:"Column names"`
|
||||||
|
Widths []int `short:"w" help:"Column widths"`
|
||||||
|
Height int `help:"Table height" default:"10"`
|
||||||
|
Print bool `short:"p" help:"static print" default:"false"`
|
||||||
|
File string `short:"f" help:"file path" default:""`
|
||||||
|
Border string `short:"b" help:"border style" default:"rounded" enum:"rounded,thick,normal,hidden,double,none"`
|
||||||
|
|
||||||
|
BorderStyle style.Styles `embed:"" prefix:"border." envprefix:"GUM_TABLE_BORDER_"`
|
||||||
|
CellStyle style.Styles `embed:"" prefix:"cell." envprefix:"GUM_TABLE_CELL_"`
|
||||||
|
HeaderStyle style.Styles `embed:"" prefix:"header." envprefix:"GUM_TABLE_HEADER_"`
|
||||||
|
SelectedStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" envprefix:"GUM_TABLE_SELECTED_"`
|
||||||
|
}
|
57
table/table.go
Normal file
57
table/table.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// Package table provides a shell script interface for the table bubble.
|
||||||
|
// https://github.com/charmbracelet/bubbles/tree/master/table
|
||||||
|
//
|
||||||
|
// It is useful to render tabular (CSV) data in a terminal and allows
|
||||||
|
// the user to select a row from the table.
|
||||||
|
//
|
||||||
|
// Let's render a table of gum flavors:
|
||||||
|
//
|
||||||
|
// $ gum table <<< "Flavor,Price\nStrawberry,$0.50\nBanana,$0.99\nCherry,$0.75"
|
||||||
|
//
|
||||||
|
// Flavor Price
|
||||||
|
// Strawberry $0.50
|
||||||
|
// Banana $0.99
|
||||||
|
// Cherry $0.75
|
||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charmbracelet/bubbles/table"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type model struct {
|
||||||
|
table table.Model
|
||||||
|
selected table.Row
|
||||||
|
quitting bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cmd tea.Cmd
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "enter":
|
||||||
|
m.selected = m.table.SelectedRow()
|
||||||
|
m.quitting = true
|
||||||
|
return m, tea.Quit
|
||||||
|
case "ctrl+c", "q", "esc":
|
||||||
|
m.quitting = true
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.table, cmd = m.table.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) View() string {
|
||||||
|
if m.quitting {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return m.table.View()
|
||||||
|
}
|
55
timeout/options.go
Normal file
55
timeout/options.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package timeout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tick interval.
|
||||||
|
const tickInterval = time.Second
|
||||||
|
|
||||||
|
// TickTimeoutMsg will be dispatched for every tick.
|
||||||
|
// Containing current timeout value
|
||||||
|
// and optional parameter to be used when handling the timeout msg.
|
||||||
|
type TickTimeoutMsg struct {
|
||||||
|
TimeoutValue time.Duration
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init Start Timeout ticker using with timeout in seconds and optional data.
|
||||||
|
func Init(timeout time.Duration, data interface{}) tea.Cmd {
|
||||||
|
if timeout > 0 {
|
||||||
|
return Tick(timeout, data)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start ticker.
|
||||||
|
func Tick(timeoutValue time.Duration, data interface{}) tea.Cmd {
|
||||||
|
return tea.Tick(tickInterval, func(time.Time) tea.Msg {
|
||||||
|
// every tick checks if the timeout needs to be decremented
|
||||||
|
// and send as message
|
||||||
|
if timeoutValue >= 0 {
|
||||||
|
timeoutValue -= tickInterval
|
||||||
|
return TickTimeoutMsg{
|
||||||
|
TimeoutValue: timeoutValue,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Str produce Timeout String to be rendered.
|
||||||
|
func Str(timeout time.Duration) string {
|
||||||
|
return fmt.Sprintf(" (%d)", max(0, int(timeout.Seconds())))
|
||||||
|
}
|
||||||
|
|
||||||
|
func max(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
|
@ -2,15 +2,10 @@ package write
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"strings"
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
|
||||||
"github.com/charmbracelet/bubbles/textarea"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/gum/internal/exit"
|
|
||||||
"github.com/charmbracelet/gum/internal/stdin"
|
"github.com/charmbracelet/gum/internal/stdin"
|
||||||
"github.com/charmbracelet/gum/style"
|
"github.com/charmbracelet/huh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run provides a shell script interface for the text area bubble.
|
// Run provides a shell script interface for the text area bubble.
|
||||||
|
@ -18,51 +13,42 @@ import (
|
||||||
func (o Options) Run() error {
|
func (o Options) Run() error {
|
||||||
in, _ := stdin.Read()
|
in, _ := stdin.Read()
|
||||||
if in != "" && o.Value == "" {
|
if in != "" && o.Value == "" {
|
||||||
o.Value = in
|
o.Value = strings.ReplaceAll(in, "\r", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
a := textarea.New()
|
var value = o.Value
|
||||||
a.Focus()
|
|
||||||
|
|
||||||
a.Prompt = o.Prompt
|
theme := huh.ThemeCharm()
|
||||||
a.Placeholder = o.Placeholder
|
theme.Focused.Base = o.BaseStyle.ToLipgloss()
|
||||||
a.ShowLineNumbers = o.ShowLineNumbers
|
theme.Focused.TextInput.Cursor = o.CursorStyle.ToLipgloss()
|
||||||
a.CharLimit = o.CharLimit
|
theme.Focused.Title = o.HeaderStyle.ToLipgloss()
|
||||||
|
theme.Focused.TextInput.Placeholder = o.PlaceholderStyle.ToLipgloss()
|
||||||
|
theme.Focused.TextInput.Prompt = o.PromptStyle.ToLipgloss()
|
||||||
|
|
||||||
style := textarea.Style{
|
keymap := huh.NewDefaultKeyMap()
|
||||||
Base: o.BaseStyle.ToLipgloss(),
|
keymap.Text.NewLine.SetHelp("ctrl+j", "new line")
|
||||||
Placeholder: o.PlaceholderStyle.ToLipgloss(),
|
|
||||||
CursorLine: o.CursorLineStyle.ToLipgloss(),
|
|
||||||
CursorLineNumber: o.CursorLineNumberStyle.ToLipgloss(),
|
|
||||||
EndOfBuffer: o.EndOfBufferStyle.ToLipgloss(),
|
|
||||||
LineNumber: o.LineNumberStyle.ToLipgloss(),
|
|
||||||
Prompt: o.PromptStyle.ToLipgloss(),
|
|
||||||
}
|
|
||||||
|
|
||||||
a.BlurredStyle = style
|
err := huh.NewForm(
|
||||||
a.FocusedStyle = style
|
huh.NewGroup(
|
||||||
a.Cursor.Style = o.CursorStyle.ToLipgloss()
|
huh.NewText().
|
||||||
|
Title(o.Header).
|
||||||
|
Placeholder(o.Placeholder).
|
||||||
|
CharLimit(o.CharLimit).
|
||||||
|
ShowLineNumbers(o.ShowLineNumbers).
|
||||||
|
Value(&value),
|
||||||
|
),
|
||||||
|
).
|
||||||
|
WithWidth(o.Width).
|
||||||
|
WithHeight(o.Height).
|
||||||
|
WithTheme(theme).
|
||||||
|
WithKeyMap(keymap).
|
||||||
|
WithShowHelp(o.ShowHelp).
|
||||||
|
Run()
|
||||||
|
|
||||||
a.SetWidth(o.Width)
|
|
||||||
a.SetHeight(o.Height)
|
|
||||||
a.SetValue(o.Value)
|
|
||||||
|
|
||||||
p := tea.NewProgram(model{textarea: a}, tea.WithOutput(os.Stderr))
|
|
||||||
tm, err := p.StartReturningModel()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to run write: %w", err)
|
return err
|
||||||
}
|
|
||||||
m := tm.(model)
|
|
||||||
if m.aborted {
|
|
||||||
return exit.ErrAborted
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(m.textarea.Value())
|
fmt.Println(value)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeforeReset hook. Used to unclutter style flags.
|
|
||||||
func (o Options) BeforeReset(ctx *kong.Context) error {
|
|
||||||
style.HideFlags(ctx)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,21 +4,26 @@ import "github.com/charmbracelet/gum/style"
|
||||||
|
|
||||||
// Options are the customization options for the textarea.
|
// Options are the customization options for the textarea.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Width int `help:"Text area width" default:"50" env:"GUM_WRITE_WIDTH"`
|
Width int `help:"Text area width (0 for terminal width)" default:"50" env:"GUM_WRITE_WIDTH"`
|
||||||
Height int `help:"Text area height" default:"5" env:"GUM_WRITE_HEIGHT"`
|
Height int `help:"Text area height" default:"5" env:"GUM_WRITE_HEIGHT"`
|
||||||
|
Header string `help:"Header value" default:"" env:"GUM_WRITE_HEADER"`
|
||||||
Placeholder string `help:"Placeholder value" default:"Write something..." env:"GUM_WRITE_PLACEHOLDER"`
|
Placeholder string `help:"Placeholder value" default:"Write something..." env:"GUM_WRITE_PLACEHOLDER"`
|
||||||
Prompt string `help:"Prompt to display" default:"┃ " env:"GUM_WRITE_PROMPT"`
|
Prompt string `help:"Prompt to display" default:"┃ " env:"GUM_WRITE_PROMPT"`
|
||||||
ShowCursorLine bool `help:"Show cursor line" default:"false" env:"GUM_WRITE_SHOW_CURSOR_LINE"`
|
ShowCursorLine bool `help:"Show cursor line" default:"false" env:"GUM_WRITE_SHOW_CURSOR_LINE"`
|
||||||
ShowLineNumbers bool `help:"Show line numbers" default:"false" env:"GUM_WRITE_SHOW_LINE_NUMBERS"`
|
ShowLineNumbers bool `help:"Show line numbers" default:"false" env:"GUM_WRITE_SHOW_LINE_NUMBERS"`
|
||||||
Value string `help:"Initial value (can be passed via stdin)" default:"" env:"GUM_WRITE_VALUE"`
|
Value string `help:"Initial value (can be passed via stdin)" default:"" env:"GUM_WRITE_VALUE"`
|
||||||
CharLimit int `help:"Maximum value length (0 for no limit)" default:"400"`
|
CharLimit int `help:"Maximum value length (0 for no limit)" default:"400"`
|
||||||
|
ShowHelp bool `help:"Show help key binds" negatable:"" default:"true" env:"GUM_WRITE_SHOW_HELP"`
|
||||||
|
CursorMode string `prefix:"cursor." name:"mode" help:"Cursor mode" default:"blink" enum:"blink,hide,static" env:"GUM_WRITE_CURSOR_MODE"`
|
||||||
|
|
||||||
|
BaseStyle style.Styles `embed:"" prefix:"base." envprefix:"GUM_WRITE_BASE_"`
|
||||||
|
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_WRITE_CURSOR_"`
|
||||||
|
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_WRITE_HEADER_"`
|
||||||
|
PlaceholderStyle style.Styles `embed:"" prefix:"placeholder." set:"defaultForeground=240" envprefix:"GUM_WRITE_PLACEHOLDER_"`
|
||||||
|
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=7" envprefix:"GUM_WRITE_PROMPT_"`
|
||||||
|
|
||||||
BaseStyle style.Styles `embed:"" prefix:"base." envprefix:"GUM_WRITE_BASE_"`
|
|
||||||
CursorLineNumberStyle style.Styles `embed:"" prefix:"cursor-line-number." set:"defaultForeground=7" envprefix:"GUM_WRITE_CURSOR_LINE_NUMBER_"`
|
|
||||||
CursorLineStyle style.Styles `embed:"" prefix:"cursor-line." envprefix:"GUM_WRITE_CURSOR_LINE_"`
|
|
||||||
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_WRITE_CURSOR_"`
|
|
||||||
EndOfBufferStyle style.Styles `embed:"" prefix:"end-of-buffer." set:"defaultForeground=0" envprefix:"GUM_WRITE_END_OF_BUFFER_"`
|
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_"`
|
LineNumberStyle style.Styles `embed:"" prefix:"line-number." set:"defaultForeground=7" envprefix:"GUM_WRITE_LINE_NUMBER_"`
|
||||||
PlaceholderStyle style.Styles `embed:"" prefix:"placeholder." set:"defaultForeground=240" envprefix:"GUM_WRITE_PLACEHOLDER_"`
|
CursorLineNumberStyle style.Styles `embed:"" prefix:"cursor-line-number." set:"defaultForeground=7" envprefix:"GUM_WRITE_CURSOR_LINE_NUMBER_"`
|
||||||
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=7" envprefix:"GUM_WRITE_PROMPT_"`
|
CursorLineStyle style.Styles `embed:"" prefix:"cursor-line." envprefix:"GUM_WRITE_CURSOR_LINE_"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,46 +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"
|
|
||||||
)
|
|
||||||
|
|
||||||
type model struct {
|
|
||||||
aborted bool
|
|
||||||
quitting bool
|
|
||||||
textarea textarea.Model
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m model) Init() tea.Cmd { return textarea.Blink }
|
|
||||||
func (m model) View() string {
|
|
||||||
if m.quitting {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return m.textarea.View()
|
|
||||||
}
|
|
||||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case tea.KeyMsg:
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c":
|
|
||||||
m.aborted = true
|
|
||||||
m.quitting = true
|
|
||||||
return m, tea.Quit
|
|
||||||
case "esc", "ctrl+d":
|
|
||||||
m.quitting = true
|
|
||||||
return m, tea.Quit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var cmd tea.Cmd
|
|
||||||
m.textarea, cmd = m.textarea.Update(msg)
|
|
||||||
return m, cmd
|
|
||||||
}
|
|
Loading…
Reference in a new issue