Compare commits

...

219 commits
v0.6.0 ... main

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

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

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

* Update README.md

* docs: spin.gif

* docs: add spin.gif to readme

* fix: lint

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

Use `huh` for `gum confirm`.

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

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

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

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

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

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

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

Resolves #55

* chore: Fix formatting

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Update README.md

* Update README.md

---------

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

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

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

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

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

I changed it to fmt.Println.

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

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

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

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

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

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

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

* Remove accidental commit of other changes.

* fix: use o.Options

---------

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-28 09:28:30 -03:00
Piero Lescano dd557baf6a
fix(input): Avoid reading from stdin if --value is being used (#448) 2023-11-14 16:54:45 -05:00
dependabot[bot] eb0e8afeba
feat(deps): bump golang.org/x/net from 0.8.0 to 0.17.0 (#451)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.8.0 to 0.17.0.
- [Commits](https://github.com/golang/net/compare/v0.8.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-14 16:44:23 -05:00
Ayman Bagabas 504a2060b2
fix(log): use log default colors 2023-11-08 12:12:06 -05:00
Ayman Bagabas 7bae4c8fcb
fix(log): default info level color
https://github.com/charmbracelet/log/blob/main/styles.go#L67
2023-11-08 12:09:50 -05:00
Maas Lalani bf3864e231
Log predefined time formats (#452)
* feat: allow predefined time formats

* refactor(log): use time format constants

* feat(log): time format flag -> time flag

* docs: add formats to help
2023-11-08 12:03:49 -05:00
Maas Lalani 3839b8d6e1
docs(log): add gif 2023-11-08 11:11:50 -05:00
Maas Lalani 055aa0d791
feat(log): add short flags 2023-11-08 11:10:58 -05:00
Maas Lalani 12ef4d3085
docs(log): add log to the readme 2023-11-08 10:59:16 -05:00
Ayman Bagabas 86dbd9c70f
fix: add missing import 2023-11-08 10:44:36 -05:00
Ayman Bagabas 7d51fd8b73
feat: add log command (#449)
* feat: add log command

* fix: lint

* refactor: remove switch

* fix: use one level style embed

* fix(log): lint

* Update log/command.go

Co-authored-by: Ayman Bagabas <ayman.bagabas@gmail.com>

---------

Co-authored-by: Maas Lalani <maas@lalani.dev>
2023-11-08 10:43:50 -05:00
Christian Höltje 46328de806
feat(write): ESC should be successful (#433)
Change ESC from aborting to successful quitting.

'vi' users press ESC as an uncontrollable tick, making using 'gum write'
painful when all their work is lost.
2023-10-19 12:38:23 -04:00
dependabot[bot] 8e959e4cdc
feat(deps): bump github.com/alecthomas/kong from 0.8.0 to 0.8.1 (#438)
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 0.8.0 to 0.8.1.
- [Commits](https://github.com/alecthomas/kong/compare/v0.8.0...v0.8.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 12:33:51 -04:00
dependabot[bot] 1cedd4f20b
feat(deps): bump github.com/mattn/go-isatty from 0.0.19 to 0.0.20 (#441)
Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.19 to 0.0.20.
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.19...v0.0.20)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-isatty
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 12:33:18 -04:00
Maas Lalani 89e2a0fbd5
feat: static --print table (#436) 2023-10-16 10:51:31 -04:00
Mikael Fangel 971b6cf16f
fix(filter): made filter work with lists as choose (#424)
* made filter work with lists as choose

* lint fix

* response to code review
2023-10-03 16:33:57 -04:00
dependabot[bot] 77aa8640f2
chore(deps): bump actions/checkout from 3 to 4 (#418) 2023-09-05 12:39:55 +00:00
Greg Kroah-Hartman a63ea30136
spin: fix isTTY check (#404)
The isTTY check is inverted in the --show-output option for spin, so no
output is shown anymore.  Fix this by correctly checking if we are a tty
or not.
2023-07-28 10:40:18 -04:00
Maas Lalani a61f3bdc3f
fix(choose): --no-limit when 1 option available should still allow selection 2023-07-25 12:38:55 -04:00
Maas Lalani 6763de12e8
fix(choose): set initial page for selected 2023-07-25 12:34:00 -04:00
Maas Lalani 4b998515fd
feat(choose): use home/end to go to first/last option 2023-07-25 12:28:02 -04:00
Chris Long ed52291b33
docs: Add WinGet installation details (#401) 2023-07-25 12:23:48 -04:00
Kenny Parnell f5b09a434a
Switch to termenv.EnvColorProfile() (#387)
* Switch to termenv.EnvColorProfile()

This will allow users to set `NO_COLOR` and `CLICOLOR_FORCE` when they need to override the detected values.

* Update main.go
2023-07-20 14:14:31 -04:00
Dieter Eickstaedt d1ad453ce6
feat: Timeout for Choose Command (#384) 2023-06-30 09:28:46 -04:00
Dieter Eickstaedt f73341a56c
feat: Timeout for File Command (#386) 2023-06-30 09:22:45 -04:00
Dieter Eickstaedt eef6431d7c
feat: Timeout for Confirm Command (#383)
* feat: Timeout for Confirm Command

* fix: comment

---------

Co-authored-by: Maas Lalani <maas@lalani.dev>
2023-06-30 09:20:33 -04:00
Dieter Eickstaedt 6bf79aa899
feat: Timeout for Filter Command (#382) 2023-06-30 09:18:02 -04:00
Dieter Eickstaedt f8caeef195
feat: Timeout for Spin Command (#385)
* feat: Timeout for Spin Command

* fix: spin timeout

---------

Co-authored-by: Maas Lalani <maas@lalani.dev>
2023-06-30 09:17:09 -04:00
Maas Lalani 0c1cc8e669
fix(pager): lint 2023-06-29 17:43:58 -04:00
Dieter Eickstaedt 7e71c4d664
feat: Adding timeout option to Filter command (#380) 2023-06-29 17:29:46 -04:00
Dieter Eickstaedt abae6fd80c
feat: Adding timeout option to Pager command (#381) 2023-06-29 17:27:25 -04:00
Maas Lalani b6f739d7d1
refactor: use exit.StatusAborted, unexport heightOffset 2023-06-29 16:36:47 -04:00
Dieter Eickstaedt ae1da5d329
Feature/218/adding timeout option (#379)
* feat: Adding timeout option in preparation for coming timeout features in all commands

* feat: Adding timeout option in preparation for coming timeout features in all commands

* feat: Adding timeout option in preparation for coming timeout features in all commands

* chore: Linter issues
2023-06-29 16:35:03 -04:00
dependabot[bot] 93ffc250e7 feat(deps): bump github.com/muesli/termenv
Bumps [github.com/muesli/termenv](https://github.com/muesli/termenv) from 0.15.2-0.20230323153104-73a40463ff25 to 0.15.2.
- [Release notes](https://github.com/muesli/termenv/releases)
- [Commits](https://github.com/muesli/termenv/commits/v0.15.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-29 14:35:22 -04:00
ROMAIN GUISSET 99f1348a45 feat(filter): add cursor text line styling 2023-06-27 10:37:13 -04:00
Maas Lalani 6aac40560f
fix: isTTY & no new line 2023-06-27 10:36:33 -04:00
Rose Thatcher f048bd8d87
feat(Spin): Option to show live output (#303)
* Added live output buffer and option flag

* Update Spin on README.md

* Returned output formatting to previous version.

* Separated the showOutput and liveOutput flags.
Both flags can now be used at once.

* Removed liveOutout flag
showOutput flag is now realtime

* (spin) Consolodated stderr and stdout

* (spin) Consolodated stdout and stderr

* (spin) If being piped, writes to stdout

* Added error check and did some housekeeping

* No longer outputs to tea.View if piped

* Cleaned up the combining of stderr and stdout

* Fixed spinner alignment.  Updated Readme
2023-06-27 10:31:54 -04:00
dependabot[bot] f1b99f0aa4 feat(deps): bump github.com/mattn/go-isatty from 0.0.18 to 0.0.19
Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.18 to 0.0.19.
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.18...v0.0.19)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-isatty
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-27 10:18:34 -04:00
dependabot[bot] fd11b787e0 feat(deps): bump github.com/alecthomas/kong from 0.7.1 to 0.8.0
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 0.7.1 to 0.8.0.
- [Commits](https://github.com/alecthomas/kong/compare/v0.7.1...v0.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-27 10:18:26 -04:00
dependabot[bot] 0010018d61 feat(deps): bump github.com/charmbracelet/bubbletea
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 0.24.1 to 0.24.2.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v0.24.1...v0.24.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-07 11:33:26 -04:00
Maas Lalani 8f17aa3f9a feat(write,input): add --cursor.mode=hide,blink,static 2023-06-05 16:08:40 -04:00
Maas Lalani 3609fe1da8
feat: use file picker v0.16.1 2023-05-31 14:07:00 -04:00
Maas Lalani 3f7db714ff
fix(format): indentation issues 2023-05-31 11:09:15 -04:00
Carlos Alexandro Becker 5a4b12c8ca feat: gum filter --no-sort
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2023-05-30 09:42:41 -05:00
Carlos Alexandro Becker a892c39289
fix: format pager/options.go
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2023-05-30 14:29:41 +00:00
Maas Lalani accce59ed1 fix: bump to v0.24.1 2023-05-24 13:17:02 -04:00
dependabot[bot] 25ff33e710 feat(deps): bump github.com/charmbracelet/bubbletea
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 0.23.3-0.20230316100943-248eb83001a7 to 0.24.0.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Commits](https://github.com/charmbracelet/bubbletea/commits/v0.24.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-24 13:17:02 -04:00
Maas Lalani 4c3cc1773f
feat: add kaomoji example 2023-05-22 15:00:53 -04:00
Christian Rocha 6e802805cf fix(docs): correct LipglossPadding doc comment 2023-05-18 12:26:33 -04:00
Christian Rocha b23ebce896 fix: measure runes instead of bytes when truncating
There are a couple of gotchas in LipGlossTruncate:

* len() returns the number of bytes in a string
* slicing a string slices it on a byte level

The fix would normally be to convert the string to a slice of runes and
operate on that new slice:

r := []rune(str)

However, reflow already contains an ansi-aware truncate feature that we
can use instead as a drop-in replacement for the LipGlossTruncate
function.
2023-05-18 11:45:14 -04:00
Mikael Fangel c8710071ad
feature(pager): add search functionality (#321)
* Added initial search functionality

 * Added a handler for the key presses
 * Added a searchbar at the bottom of the screen
 * Made search results cycleable by pressing n

* correct start pos and ignore keys while searching

* fix out of bound error when pressing n

* made the matching pattern relative to the current pos

* added p for searching for previous match

* added highlighting to search matches

* dynamically replaced all matches

* fixed string highlight issue

* fixed truncation issue

* small simplifaction in ypos logic

* made prev and next behave the same atBottom

* simplified logic and fixed linter errors

* updated help text

* style changes

* added comments

* fixed truncation issue

* fixes infinte loop on very long lines

* added simple lipgloss truncate function

* updated colors for better contrast

* lint fix

* initial commit for soft-wrap functionality

* linter corrections and added for pager with new model

* added generic functions to a utility package

* fix soft lint errors

* made N match previous as well as p

* replaced help text when search is active

* ran gofmt -w

* reimplemented search and next to enabled support for dynamic highlights

* made the highlight move as you progress through the search

* simplified highlighter

* improvements to the clean up of the highlight function

* semi working reverse search

* reverse search without highlight

* added semi working highlight for reverser search

* working version of previous match

* fixed issue with single letter matches in next

* added support for softwrapping

* respond to soft lint warnings

* removed unused function

* lint

* simplified matchers and fixed duplicate highlights

* optimisations and change in matching pattern

* fixed bug in lipglosspadding and allowed matching 1 etc.

* make highlight respect user settings

* fixed logic error in slice

* made prev match wrap around

* fix: show next/prev match help when active

* updated how view port line is set

* avoid crashes when regex doesn't compile

* fix: spelling previous

---------

Co-authored-by: Maas Lalani <maas@lalani.dev>
2023-05-14 23:19:07 -04:00
Ayman Bagabas 23c56854d3
fix(ci): remove soft-serve workflow 2023-05-12 14:10:10 -04:00
Maas Lalani c668e153e6 feat(format): add GUM_FORMAT_THEME / GUM_FORMAT_LANGUAGE environment variable configuration 2023-05-11 23:45:29 -04:00
Maas Lalani 92c890e717
fix: use lipgloss.Width instead of runewidth.StringWidth 2023-05-11 22:21:09 -04:00
Christian Rocha ece25c7789
fix: correct copyright in man 2023-05-11 21:14:22 -04:00
Ayman Bagabas 9d2741c5f9
fix: update copyrights 2023-05-11 20:56:39 -04:00
Maas Lalani 7f54b3b289 feat(write): width < 1 uses terminal width 2023-05-11 15:13:01 -04:00
Christian Rocha 11584b5982
chore(docs): correct release year in license 2023-05-11 11:55:06 -04:00
Carlos Alexandro Becker fff07286b7
docs: update license
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2023-05-11 12:39:14 +00:00
Carlos Alexandro Becker 39346ed015 fix: missing filepicker styles
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2023-05-01 10:01:18 -04:00
Maas Lalani 99a1fa9d8e
chore(deps): update bubbles 2023-05-01 09:59:13 -04:00
Maas Lalani 8da9620bfd
chore(lint): add function comment 2023-04-12 09:57:36 -04:00
Maas Lalani 5887a10fa0 fix: strip ansi from gum filter when output is not a tty 2023-04-06 17:36:29 -04:00
Maas Lalani 6dbadf30b4 fix: strip ansi when stdout is not a tty 2023-04-06 17:36:29 -04:00
Maas Lalani 11f23830c5
fix: use Cursor.Style instead of CursorStyle 2023-03-31 11:42:09 -04:00
Maas Lalani 066c79fa14 fix(file): respect file path specified in arguments 2023-03-30 11:30:05 -04:00
Maas Lalani 0f0f8e9189 fix: detect stderr color profile 2023-03-27 12:00:24 -04:00
Maas Lalani e0bcab8608 fix: remove truecolor 2023-03-27 12:00:24 -04:00
Maas Lalani 95ddbdb416 feat: allow passing language to parse code 2023-03-24 13:40:37 -04:00
dependabot[bot] b6daeece02 chore(deps): bump actions/setup-go from 3 to 4
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-22 11:23:35 -04:00
Maas Lalani bb103c0a03
fix: show header only if not empty 2023-03-22 11:17:06 -04:00
Mikael Fangel 38521ff870 fix: correct wrong key combination for gum write in README.md
Fixes #312

### Changes:
 - Moves the `esc` over to the cancel option together with CTRL+c, so it reflects the current behaviour of the program.
2023-03-20 17:47:23 -04:00
mikael d9a3dc5324 Added esc to keys and changed error code to 130 2023-03-20 15:17:16 -04:00
Mikael Fangel 97feb1b4d0
feat: adds headers for choose and filter (#307)
* added header to choose

* corrected mistake in envvar

* added header for filter

* simplified return logic for filter

* Update choose/options.go

* render the header before calculating the height

---------

Co-authored-by: Maas Lalani <maas@lalani.dev>
2023-03-14 15:58:48 -04:00
Mikael Fangel ec2b8d0fee
removed bksp from stdin (#306)
* removed bksp from stdin

---------

Co-authored-by: Maas Lalani <maas@lalani.dev>
2023-03-13 16:35:29 -04:00
Maas Lalani ccc5d9cfea
chore: bump deps 2023-03-13 15:21:39 -04:00
dependabot[bot] b0c9b56e0e feat(deps): bump github.com/charmbracelet/lipgloss
Bumps [github.com/charmbracelet/lipgloss](https://github.com/charmbracelet/lipgloss) from 0.6.1-0.20230222162833-a74950e6da16 to 0.7.1.
- [Release notes](https://github.com/charmbracelet/lipgloss/releases)
- [Commits](https://github.com/charmbracelet/lipgloss/commits/v0.7.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-13 15:09:53 -04:00
dependabot[bot] 1e4012ffde feat(deps): bump github.com/muesli/termenv from 0.15.0 to 0.15.1
Bumps [github.com/muesli/termenv](https://github.com/muesli/termenv) from 0.15.0 to 0.15.1.
- [Release notes](https://github.com/muesli/termenv/releases)
- [Commits](https://github.com/muesli/termenv/compare/v0.15.0...v0.15.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-13 15:09:44 -04:00
Mikael Fangel f46060ac43 Update write/command.go
Co-authored-by: Maas Lalani <maas@lalani.dev>
2023-03-09 12:59:53 -05:00
mikael f8adcd649b fixes errors with CR in write --value 2023-03-09 12:59:53 -05:00
dependabot[bot] 65f5a7e44e feat(deps): bump github.com/muesli/termenv from 0.14.0 to 0.15.0
Bumps [github.com/muesli/termenv](https://github.com/muesli/termenv) from 0.14.0 to 0.15.0.
- [Release notes](https://github.com/muesli/termenv/releases)
- [Commits](https://github.com/muesli/termenv/compare/v0.14.0...v0.15.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-09 12:43:56 -05:00
mikael 708a653eae Fix for using --value in combination w/ --no-fuzzy
This add a check if the no fuzzy flag is set together with the
--no-fuzzy flag.
2023-03-09 12:42:55 -05:00
Mikael Fangel 2bda001480
fix(table): removed inheritance for selected foreground (#300) 2023-03-09 12:35:06 -05:00
Mikael Fangel 1267b7a78e
fix: added esc-key to quit table (#290) 2023-03-06 14:17:47 -05:00
Maas Lalani b5444d5f0b
feat: use filepicker bubble (#289)
* feat: use filepicker bubble

* fix: use new API

* chore: bump bubbles
2023-03-06 11:54:02 -05:00
Maas Lalani 99e6625a39
fix: don't use deprecated pager functions 2023-03-03 13:15:50 -05:00
Maas Lalani e5cb9877cf
chore: bump dependencies 2023-02-27 19:27:06 -05:00
Leon Stam 5431540431 feat(file): Add page up/down key bindings 2023-02-27 19:22:56 -05:00
Mathew Payne 83db83296a
Add staged file check for commit example (#269)
* Add staged file check for commit example

* fix: stage all confirmation

---------

Co-authored-by: Maas Lalani <maas@lalani.dev>
2023-02-27 19:19:23 -05:00
ChrHan 46dee843db
fix(prompt): fix default selection of prompt, timer and exit code (#148)
* fix(prompt): fix default selection of prompt, timer and exit code

* chore(comment): remove unused comment

* fix(confirm): ensure timer is located at fixed selection

* chore(confirm): remove inefficient assignment

* fix: default timeout selection

---------

Co-authored-by: Maas Lalani <maas@lalani.dev>
2023-02-27 19:16:08 -05:00
Maas Lalani e6de7749b1
fix: require choice for multiselect 2023-02-27 19:05:50 -05:00
Maas Lalani 440a3dd81c
fix: go mod tidy 2023-02-27 19:01:08 -05:00
MuXiu1997 877c475aa6
feat: add ctrl+@(ctrl+space) keybinding for choose and filter (#276)
* feat(filter): add ctrl+@ keybinding

* feat(choose): add ctrl+@ keybinding
2023-02-27 18:59:40 -05:00
dependabot[bot] 488138e4b5 feat(deps): bump github.com/charmbracelet/bubbletea
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 0.23.1 to 0.23.2.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v0.23.1...v0.23.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-27 18:58:59 -05:00
Nicolas M f7d8ef5871 build: update go version to 1.17 2023-02-27 18:58:43 -05:00
Kris Schneider 78bb3b5f06 Added Scoop installation instructions for gum 2023-01-19 11:41:03 -05:00
dependabot[bot] 80f3598efd feat(deps): bump github.com/dustin/go-humanize from 1.0.0 to 1.0.1
Bumps [github.com/dustin/go-humanize](https://github.com/dustin/go-humanize) from 1.0.0 to 1.0.1.
- [Release notes](https://github.com/dustin/go-humanize/releases)
- [Commits](https://github.com/dustin/go-humanize/compare/v1.0.0...v1.0.1)

---
updated-dependencies:
- dependency-name: github.com/dustin/go-humanize
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-19 11:39:59 -05:00
Maas Lalani 36ef76185c
fix(confirm): esc should not exit with 130 2023-01-13 12:08:06 -05:00
Dhruv Manilawala 08ed3e2519
feat(choose): keep order of selected items (#182)
* feat(choose): keep order of selected items

* fix: KeepOrder -> Ordered

Co-authored-by: Maas Lalani <maas@lalani.dev>
2023-01-12 13:36:11 -05:00
Maas Lalani 832c4fc917 feat: ctrl+c / esc aborts action 2022-12-13 15:47:32 -05:00
Maas Lalani c8e6b4a9d5
fix: trim only trailing newline, space may be intentional 2022-12-13 15:41:01 -05:00
Maas Lalani 7756c809d1
fix(file): all default false (to match ls), GUM_FILE_ environment 2022-12-13 15:36:58 -05:00
Maas Lalani b4d7ebf2cb
fix(write): exit with status 130 on escape 2022-12-13 15:11:37 -05:00
Maas Lalani 240e163f01 feat(input): header values 2022-12-13 15:05:56 -05:00
Maas Lalani 2d54d5394e feat(choose): add ctrl+j / ctrl+k keybindings 2022-12-13 14:41:29 -05:00
Leon Stam e108bc4668 feat(pager): add --soft-wrap option to gum pager 2022-12-13 14:34:58 -05:00
dependabot[bot] b8dbcc3e82 feat(deps): bump github.com/alecthomas/kong from 0.7.0 to 0.7.1
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 0.7.0 to 0.7.1.
- [Release notes](https://github.com/alecthomas/kong/releases)
- [Commits](https://github.com/alecthomas/kong/compare/v0.7.0...v0.7.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-06 20:08:26 -05:00
Mel Massadian 7169c0c490
fix: update bubbletea dependency (#248) 2022-12-06 20:08:00 -05:00
Christian Muehlhaeuser eb3c5c1037 chore: disable dependabot timer 2022-11-18 02:11:34 +01:00
Maas Lalani 614f0e8028
feat(style): allow passing input over stdin 2022-11-09 23:13:21 -05:00
Dieter Eickstaedt b0aba2261d
feature(choose): Adding option to use non-matching filter as result (#233) 2022-11-09 13:54:47 -05:00
Lee Marlow e38cfdaa10 Collapse err assignment and check 2022-11-07 10:09:56 -05:00
Lee Marlow 7bb92dec2f Set writer.Comma in same manner as reader.Comma
Fix linting issue with ignoring an error
2022-11-07 10:09:56 -05:00
Lee Marlow e20d3a97f0 Use encoding/csv to write proper CSV for the selected row 2022-11-07 10:09:56 -05:00
fedeztk 5ed1f2b1b8 feat(file): allow distinctly file/dirs selections 2022-11-01 21:41:29 -04:00
dependabot[bot] f0a8011b95 feat(deps): bump github.com/alecthomas/kong from 0.6.1 to 0.7.0
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 0.6.1 to 0.7.0.
- [Release notes](https://github.com/alecthomas/kong/releases)
- [Commits](https://github.com/alecthomas/kong/compare/v0.6.1...v0.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-30 22:38:14 -04:00
Christian Muehlhaeuser b87d77554c docs: picture tag is causing display issues 2022-10-24 21:23:52 -04:00
Christian Muehlhaeuser d88f9aec55 docs: new readme footer 2022-10-24 02:46:16 -04:00
Christian Muehlhaeuser 1dec524b9a docs: mention BSD releases 2022-10-22 23:38:10 -04:00
Christian Muehlhaeuser 75c41866b2 docs: add Alpine & termux install instructions 2022-10-22 22:47:40 -04:00
Kevin Pham 57c8c90bfd feat(choose): use tab to toggle selection
Resolves #200
2022-10-18 16:31:21 -04:00
Maas Lalani 7a32dd579b feat: allow customization of glamour theme 2022-10-18 15:21:35 -04:00
Maas Lalani a1e2b3d3c3
fix: downgrade uniseg 2022-10-17 23:01:55 -04:00
Maas Lalani fd58eb07fc
fix: use program.Run() in pager 2022-10-17 20:26:26 -04:00
Maas Lalani 2e4ddce3f7
fix: use program.Start 2022-10-17 20:23:59 -04:00
Maas Lalani 5723977c68
chore: bump bubbletea@master 2022-10-17 20:07:27 -04:00
Ayman Bagabas 0b500f6ec9 docs: apt-key deprecation
Use the recommended keyrings path /etc/apt/keyrings

Per: https://manpages.debian.org/unstable/apt/apt-key.8.en.html#DEPRECATION
Fixes: https://github.com/charmbracelet/gum/issues/208
Fixes: https://github.com/charmbracelet/gum/pull/209
2022-10-17 16:45:44 -04:00
Maas Lalani 992cac834e fix: symlink directory follows directory 2022-10-13 11:48:23 -04:00
Maas Lalani 844727f185
fix(table): hide extra style flags + fix envprefix 2022-10-13 11:02:08 -04:00
Dhruv Manilawala 2c66222fd8 fix(choose): error if selected options > limit
Rationale: If the number of selected options in `--selected` flag is
greater than the limit, the last `n` options are selected where `n` is
the limit.
2022-10-13 11:01:21 -04:00
David Lewis 1353b97272
added nix flake directions (#195)
* added nix flake directions

Co-authored-by: lew2mz <david.lewis@cchmc.org>
Co-authored-by: Maas Lalani <maas@lalani.dev>
2022-10-13 11:00:04 -04:00
Maas Lalani 08c34cfa2f chore: use --[no-]fuzzy as flag 2022-10-13 10:58:57 -04:00
Enrico Candino 8dec822e75 avoid Fuzzy search 2022-10-13 10:58:57 -04:00
Enrico Candino d45b728b4d fix "shuffling" 2022-10-13 10:58:57 -04:00
Enrico Candino d74f126d41 added exact match to filter 2022-10-13 10:58:57 -04:00
Maas Lalani 98ff7656d1 fix: inline file styles 2022-10-12 16:59:56 -04:00
lew2mz b92c9ec858 updated sha256 2022-10-12 11:38:54 -04:00
Maas Lalani dfa412fa1a
test: add table, pager, file to test 2022-10-11 17:47:14 -04:00
Maas Lalani b4c07eb3b9
fix(table): invalid column number 2022-10-11 14:40:25 -04:00
Caeden 1426c2fed9 fix(README): Change .text to .txt 2022-10-08 17:01:32 -04:00
Christian Muehlhaeuser 86fa35d672 perf: speed up filter rendering
We don't need to style each character individually, buffer them
and flush before highlights.
2022-10-07 23:01:38 -04:00
Maas Lalani db75c218de
fix(choose): selected bug when limit == 1 2022-10-07 18:57:26 -04:00
Maas Lalani 995bd04e38 docs: document gum table, file, pager
This commit documents `gum table`, `gum file`, and `gum pager` with
demonstration GIFs and sample scripts.
2022-10-07 15:38:48 -04:00
Maas Lalani 2bea4dc030 feat(file): gum file to pick files 2022-10-07 15:38:48 -04:00
Maas Lalani 430ab459d7 feat(pager): gum pager for scrolling through long documents 2022-10-07 15:38:48 -04:00
Maas Lalani bdd86d5fbc feat(table): gum table for tabular data 2022-10-07 15:38:48 -04:00
Dhruv Manilawala a82d5af1e8
feat(filter): add reverse layout (#177)
* feat: add reverse layout for filter command

* fix: linter warnings

* fix: keep viewport yoffset constant in reverse layout
2022-10-07 15:23:28 -04:00
Ayman Bagabas 0bd02434a3
docs: update apt/yum installation instructions 2022-10-06 17:30:43 -04:00
Maas Lalani 45e930c54e
chore: bump bubbletea@master 2022-10-06 14:55:03 -04:00
Kevin Pham f13b5b6b82 feat(write): use --header to display a label above the textarea 2022-10-06 13:35:43 -04:00
Maas Lalani f69fc23242 fix: don't allow selecting negative value if it is an empty string 2022-10-06 13:30:05 -04:00
Kevin Pham a4d4793829 fix(confirm): an empty --negative will not be displayed
This change will hide the normally "No" button when the text is empty. In most cases, this will serve as an acknowledgement and the affirmative message would likely change.

This seems to be better than displaying an empty button.

Resolves #145
2022-10-06 13:30:05 -04:00
Maas Lalani fecb5ffc7d
chore: bump bubbles@master 2022-10-06 11:45:07 -04:00
Greg Kroah-Hartman 6677920319 feat: add alignment for spinner title
Sometimes it is nice to spin to the right side of the title, not the
left, so add a --align option to pick the left or right side.  The
default remains "left".

New option is:
	--align="left|right"
and the environment variable GUM_SPIN_ALIGN can also be used.
2022-10-02 14:42:19 -04:00
Matt Farstad fa533691c4
feat(choose): Preselect the Selected Option When Limit is One (#166)
* Add startingIndex variable to choose model

* refactor: simplify code branches

Co-authored-by: Matthew Farstad <matthewwilliamfarstad@gmail.com>
Co-authored-by: Maas Lalani <maas@lalani.dev>
2022-10-02 12:51:35 -04:00
Dhruv Manilawala 5c98432070
feat: use shift+tab to toggle selection and move up (#167)
* feat: use `shift+tab` to toggle selection and move up

* refactor: extract CursorUp, CursorDown, ToggleSelection methods

Co-authored-by: Maas Lalani <maas@lalani.dev>
2022-10-02 12:47:41 -04:00
Maas Lalani 9b0f5f015b
docs(filter): document --no-limit / --limit flag on gum filter 2022-09-30 13:34:21 -04:00
NNB 25b5ebff5e Added DIYfetch to examples/ 2022-09-28 11:59:11 -04:00
Maas Lalani a0f2a0b096
chore: bump bubbles 2022-09-27 11:09:17 -04:00
solomon 9cfd2ff75e Updates flake hash and adds overlay to flake 2022-09-27 10:54:37 -04:00
dependabot[bot] a48b71e580 feat(deps): bump github.com/mattn/go-runewidth from 0.0.13 to 0.0.14
Bumps [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) from 0.0.13 to 0.0.14.
- [Release notes](https://github.com/mattn/go-runewidth/releases)
- [Commits](https://github.com/mattn/go-runewidth/compare/v0.0.13...v0.0.14)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-runewidth
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-26 08:50:58 +02:00
Christian Muehlhaeuser 85704a54dc chore: add dependabot config 2022-09-26 08:21:34 +02:00
Maas Lalani ea4eb64bee
chore: bump bubbletea to v0.22.1 2022-09-16 15:14:27 -04:00
fedeztk adc82b2fce fix(format): do not append additional new line 2022-09-09 11:43:36 -04:00
Maas Lalani aa7683f5e0
fix: full width on gum filter 2022-09-08 12:36:37 -04:00
Maas Lalani c71c918c86
fix(choose): fix spacing for limit == 1 gum choose 2022-09-07 21:01:42 -04:00
Maas Lalani 21332c833a
feat(choose): Change default icons to match gum filter 2022-09-07 15:43:15 -04:00
eetann 92cd4b0107 docs: fix write key description 2022-09-07 13:06:25 -04:00
lew2mz 5993a7b490 added nix flake 2022-09-06 16:14:18 -04:00
Vaniel 76fc2f3d91 feat(choose): improving selected option parse 2022-09-06 16:13:53 -04:00
Vaniel 6abc94c87a feat(choose): add --selected option to choose command 2022-09-06 16:13:53 -04:00
Maas Lalani b403b0ae58
chore: bump bubbles@v0.14.0 + lipgloss@v0.6.0 2022-09-06 15:29:32 -04:00
86 changed files with 2699 additions and 1136 deletions

1
.github/CODEOWNERS vendored Normal file
View file

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

20
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,20 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
labels:
- "dependencies"
commit-message:
prefix: "feat"
include: "scope"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"

View file

@ -12,12 +12,12 @@ jobs:
GO111MODULE: "on"
steps:
- name: Install Go
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: ~1.17
go-version: ~1.21
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Download Go modules
run: go mod download
@ -31,4 +31,4 @@ jobs:
snapshot:
uses: charmbracelet/meta/.github/workflows/snapshot.yml@main
secrets:
goreleaser_key: ${{ secrets.GORELEASER_KEY }}
goreleaser_key: ${{ secrets.GORELEASER_KEY }}

View file

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

View file

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

View file

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

3
.gitignore vendored
View file

@ -10,3 +10,6 @@ testdata
# Folders
completions/
manpages/
# nix
result

View file

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

View file

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

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 Charmbracelet, Inc
Copyright (c) 2022-2023 Charmbracelet, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

388
README.md
View file

@ -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
without writing any Go code!
<picture>
<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>
<img alt="Shell running the ./demo.sh script" width="600" src="https://vhs.charm.sh/vhs-1qY57RrQlXCuydsEgDp68G.gif">
The above example is running from a single shell script ([source](./examples/demo.sh)).
@ -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
useful shell scripts and dotfiles aliases with just a few lines of code.
Let's build a simple script to help you write [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary)
for your dotfiles.
Let's build a simple script to help you write [Conventional
Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) for your
dotfiles.
Start with a `#!/bin/sh`.
```bash
#!/bin/sh
```
Ask for the commit type with `gum choose`:
Ask for the commit type with gum choose:
```bash
gum choose "fix" "feat" "docs" "style" "refactor" "test" "chore" "revert"
```
> Tip: this command itself will print to `stdout` which is not all that useful.
To make use of the command later on you can save the stdout to a `$VARIABLE` or
`file.txt`.
Prompt for an (optional) scope for the commit:
> [!NOTE]
> This command itself will print to stdout which is not all that useful. To make use of the command later on you can save the stdout to a `$VARIABLE` or `file.txt`.
Prompt for the scope of these changes:
```bash
gum input --placeholder "scope"
```
Prompt for a commit message:
Prompt for the summary and description of changes:
```bash
gum input --placeholder "Summary of this change"
gum input --value "$TYPE$SCOPE: " --placeholder "Summary of this change"
gum write --placeholder "Details of this change"
```
Prompt for a detailed (multi-line) explanation of the changes:
```bash
gum write --placeholder "Details of this change (CTRL+D to finish)"
```
Prompt for a confirmation before committing:
> `gum confirm` exits with status `0` if confirmed and status `1` if cancelled.
Confirm before committing:
```bash
gum confirm "Commit changes?" && git commit -m "$SUMMARY" -m "$DESCRIPTION"
```
Putting it all together...
Check out the [complete example](https://github.com/charmbracelet/gum/blob/main/examples/commit.sh) for combining these commands in a single script.
```bash
#!/bin/sh
TYPE=$(gum choose "fix" "feat" "docs" "style" "refactor" "test" "chore" "revert")
SCOPE=$(gum input --placeholder "scope")
# Since the scope is optional, wrap it in parentheses if it has a value.
test -n "$SCOPE" && SCOPE="($SCOPE)"
# Pre-populate the input with the type(scope): so that the user may change it
SUMMARY=$(gum input --value "$TYPE$SCOPE: " --placeholder "Summary of this change")
DESCRIPTION=$(gum write --placeholder "Details of this change (CTRL+D to finish)")
# Commit these changes
gum confirm "Commit changes?" && git commit -m "$SUMMARY" -m "$DESCRIPTION"
```
<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>
<img alt="Running the ./examples/commit.sh script to commit to git" width="600" src="https://vhs.charm.sh/vhs-7rRq3LsEuJVwhwr0xf6Er7.gif">
## Installation
@ -109,23 +67,15 @@ pacman -S gum
# Nix
nix-env -iA nixpkgs.gum
# Debian/Ubuntu
echo 'deb [trusted=yes] https://repo.charm.sh/apt/ /' | sudo tee /etc/apt/sources.list.d/charm.list
sudo apt update && sudo apt install gum
# Fedora
echo '[charm]
name=Charm
baseurl=https://repo.charm.sh/yum/
enabled=1
gpgcheck=0' | sudo tee /etc/yum.repos.d/charm.repo
sudo yum install gum
# Windows (via WinGet or Scoop)
winget install charmbracelet.gum
scoop install charm-gum
```
Or download it:
* [Packages][releases] are available in Debian and RPM formats
* [Binaries][releases] are available for Linux, macOS, and Windows
* [Packages][releases] are available in Debian, RPM, and Alpine formats
* [Binaries][releases] are available for Linux, macOS, Windows, FreeBSD, OpenBSD, and NetBSD
Or just install it with `go`:
@ -135,25 +85,40 @@ go install github.com/charmbracelet/gum@latest
[releases]: https://github.com/charmbracelet/gum/releases
## Commands
* [`choose`](#choose): Choose an option from a list of choices
* [`confirm`](#confirm): Ask a user to confirm an action
* [`file`](#file): Pick a file from a folder
* [`filter`](#filter): Filter items from a list
* [`format`](#format): Format a string using a template
* [`input`](#input): Prompt for some input
* [`join`](#join): Join text vertically or horizontally
* [`pager`](#pager): Scroll through a file
* [`spin`](#spin): Display spinner while running a command
* [`style`](#style): Apply coloring, borders, spacing to text
* [`table`](#table): Render a table of data
* [`write`](#write): Prompt for long-form text
* [`log`](#log): Log messages to output
## Customization
`gum` is designed to be embedded in scripts and supports all sorts of use
cases. Components are configurable and customizable to fit your theme and
use case.
You can customize with `--flags`. See `gum <command> --help` for a full view of
each command's customization and configuration options.
For example, let's use an `input` and change the cursor color, prompt color,
prompt indicator, placeholder text, width, and pre-populate the value:
You can customize `gum` options and styles with `--flags` and `$ENVIRONMENT_VARIABLES`.
See `gum <command> --help` for a full view of each command's customization and configuration options.
Customize with `--flags`:
```bash
gum input --cursor.foreground "#FF0" --prompt.foreground "#0FF" --prompt "* " \
--placeholder "What's up?" --width 80 --value "Not much, hby?"
gum input --cursor.foreground "#FF0" \
--prompt.foreground "#0FF" \
--placeholder "What's up?" \
--prompt "* " \
--width 80 \
--value "Not much, hby?"
```
You can also use `ENVIRONMENT_VARIABLES` to customize `gum` by default, this is
useful to keep a consistent theme for all your `gum` commands.
Customize with `ENVIRONMENT_VARIABLES`:
```bash
export GUM_INPUT_CURSOR_FOREGROUND="#FF0"
@ -162,73 +127,54 @@ export GUM_INPUT_PLACEHOLDER="What's up?"
export GUM_INPUT_PROMPT="* "
export GUM_INPUT_WIDTH=80
# Uses values configured through environment variables above but can still be
# overridden with flags.
# --flags can override values set with environment
gum input
```
<picture>
<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>
<img alt="Gum input displaying most customization options" width="600" src="https://vhs.charm.sh/vhs-5zb9DlQYA70aL9ZpYLTwKv.gif">
## Interaction
#### Input
## Input
Prompt for input with a simple command.
```bash
gum input > answer.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
gum input --password > password.text
gum write > story.txt
```
<picture>
<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>
<img src="https://vhs.charm.sh/vhs-7abdKKrUEukgx9aJj8O5GX.gif" width="600" alt="Shell running gum write typing a story" />
#### Write
## Filter
Prompt for some multi-line text.
Note: `CTRL+D` is used to complete text entry. `CTRL+C` and `esc` will cancel.
Filter a list of values with fuzzy matching:
```bash
gum write > story.text
echo Strawberry >> flavors.txt
echo Banana >> flavors.txt
echo Cherry >> flavors.txt
gum filter < flavors.txt > selection.txt
```
<picture>
<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>
<img src="https://vhs.charm.sh/vhs-61euOQtKPtQVD7nDpHQhzr.gif" width="600" alt="Shell running gum filter on different bubble gum flavors" />
#### Filter
Use fuzzy matching to filter a list of values:
Select multiple options with the `--limit` flag or `--no-limit` flag. Use `tab` or `ctrl+space` to select, `enter` to confirm.
```bash
echo Strawberry >> flavors.text
echo Banana >> flavors.text
echo Cherry >> flavors.text
cat flavors.text | gum filter > selection.text
cat flavors.txt | gum filter --limit 2
cat flavors.txt | gum filter --no-limit
```
<picture>
<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>
#### Choose
## Choose
Choose an option from a list of choices.
@ -238,28 +184,17 @@ CARD=$(gum choose --height 15 {{A,K,Q,J},{10..2}}" "{♠,♥,♣,♦})
echo "Was your card the $CARD?"
```
You can also select multiple items with the `--limit` flag, which determines
You can also select multiple items with the `--limit` or `--no-limit` flag, which determines
the maximum of items that can be chosen.
```bash
echo "Pick your top 5 songs."
cat songs.txt | gum choose --limit 5
cat foods.txt | gum choose --no-limit --header "Grocery Shopping"
```
Or, allow any number of selections with the `--no-limit` flag.
<img src="https://vhs.charm.sh/vhs-3zV1LvofA6Cbn5vBu1NHHl.gif" width="600" alt="Shell running gum choose with numbers and gum flavors" />
```bash
echo "What do you need from the grocery store?"
cat foods.txt | gum choose --no-limit
```
<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
Confirm whether to perform an action. Exits with code `0` (affirmative) or `1`
(negative) depending on selection.
@ -268,32 +203,54 @@ Confirm whether to perform an action. Exits with code `0` (affirmative) or `1`
gum confirm && rm file.txt || echo "File not removed"
```
<picture>
<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>
<img src="https://vhs.charm.sh/vhs-3xRFvbeQ4lqGerbHY7y3q2.gif" width="600" alt="Shell running gum confirm" />
#### 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
automatically stop after the given command exits.
To view or pipe the command's output, use the `--show-output` flag.
```bash
gum spin --spinner dot --title "Buying Bubble Gum..." -- sleep 5
```
<picture>
<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>
<img src="https://vhs.charm.sh/vhs-3YFswCmoY4o3Q7MyzWl6sS.gif" width="600" alt="Shell running gum spin while sleeping for 5 seconds" />
Available spinner types include: `line`, `dot`, `minidot`, `jump`, `pulse`, `points`, `globe`, `moon`, `monkey`, `meter`, `hamburger`.
## 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.
@ -304,15 +261,9 @@ gum style \
'Bubble Gum (1¢)' 'So sweet and so fresh!'
```
<picture>
<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>
<img src="https://github.com/charmbracelet/gum/assets/42545625/67468acf-b3e0-4e78-bd89-360739eb44fa" width="600" alt="Bubble Gum, So sweet and so fresh!" />
## Layout
#### Join
## Join
Combine text vertically or horizontally. Use this command with `gum style` to
build layouts and pretty output.
@ -331,11 +282,7 @@ BUBBLE_GUM=$(gum join "$BUBBLE" "$GUM")
gum join --align center --vertical "$I_LOVE" "$BUBBLE_GUM"
```
<picture>
<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>
<img src="https://github.com/charmbracelet/gum/assets/42545625/68f7a25d-b495-48dd-982a-cee0c8ea5786" width="600" alt="I LOVE Bubble Gum written out in four boxes with double borders around them." />
## Format
@ -362,128 +309,95 @@ For more information on template helpers, see the [Termenv
docs](https://github.com/muesli/termenv#template-helpers). For a full list of
named emojis see the [GitHub API](https://api.github.com/emojis).
<picture>
<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">
<img src="https://stuff.charm.sh/gum/format.gif" alt="Running gum format for different types of formats" />
</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" />
## Log
`log` logs messages to the terminal at using different levels and styling using
the [`charmbracelet/log`](https://github.com/charmbracelet/log) library.
```bash
# Log some debug information.
gum log --structured --level debug "Creating file..." name file.txt
# DEBUG Unable to create file. name=temp.txt
# Log some error.
gum log --structured --level error "Unable to create file." name file.txt
# ERROR Unable to create file. name=temp.txt
# Include a timestamp.
gum log --time rfc822 --level error "Unable to create file."
```
See the Go [`time` package](https://pkg.go.dev/time#pkg-constants) for acceptable `--time` formats.
See [`charmbracelet/log`](https://github.com/charmbracelet/log) for more usage.
<img src="https://vhs.charm.sh/vhs-6jupuFM0s2fXiUrBE0I1vU.gif" width="600" alt="Running gum log with debug and error levels" />
## Examples
See the [examples](./examples/) directory for more real world use cases.
How to use `gum` in your daily workflows:
#### Write a commit message
See the [examples](./examples/) directory for more real world use cases.
Prompt for input to write git commit messages with a short summary and
longer details with `gum input` and `gum write`.
Bonus points: use `gum filter` with the [Conventional Commits
Specification](https://www.conventionalcommits.org/en/v1.0.0/#summary) as a
prefix for your commit message.
* Write a commit message:
```bash
git commit -m "$(gum input --width 50 --placeholder "Summary of changes")" \
-m "$(gum write --width 80 --placeholder "Details of changes (CTRL+D to finish)")"
-m "$(gum write --width 80 --placeholder "Details of changes")"
```
#### Open files in your `$EDITOR`
By default, `gum filter` will display a list of all files (searched
recursively) through your current directory, with some sensible ignore settings
(`.git`, `node_modules`). You can use this command to easily to pick a file and
open it in your `$EDITOR`.
* Open files in your `$EDITOR`
```bash
$EDITOR $(gum filter)
```
#### Connect to a TMUX session
Pick from a running `tmux` session and attach to it. Or, if you're already in a
`tmux` session, switch sessions.
* Connect to a `tmux` session
```bash
SESSION=$(tmux list-sessions -F \#S | gum filter --placeholder "Pick session...")
tmux switch-client -t $SESSION || tmux attach -t $SESSION
```
<picture>
<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.
* Pick a commit hash from `git` history
```bash
git log --oneline | gum filter | cut -d' ' -f1 # | copy
```
<picture>
<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...
* Simple [`skate`](https://github.com/charmbracelet/skate) password selector.
```
skate list -k | gum filter | xargs skate get
```
<picture>
<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.
* Uninstall packages
```bash
brew list | gum choose --no-limit | xargs brew uninstall
```
#### Choose branches to delete
List all branches and choose which branches to delete.
* Clean up `git` branches
```bash
git branch | cut -c 3- | gum choose --no-limit | xargs git branch -D
```
#### Choose pull request to checkout
List all PRs for the current GitHub repository and checkout the chosen PR (using [`gh`](https://cli.github.com/)).
* Checkout GitHub pull requests with [`gh`](https://cli.github.com/)
```bash
gh pr list | cut -f1,2 | gum choose | cut -f1 | xargs gh pr checkout
```
#### Pick command from shell history
Pick a previously executed command from your shell history to execute, copy,
edit, etc...
* Copy command from shell history
```bash
gum filter < $HISTFILE --height 20
```
#### Sudo password input
See visual feedback when entering password with masked characters with `gum
input --password`.
* `sudo` replacement
```bash
alias please="gum input --password | sudo -nS"
@ -494,14 +408,14 @@ alias please="gum input --password | sudo -nS"
Wed love to hear your thoughts on this project. Feel free to drop us a note!
* [Twitter](https://twitter.com/charmcli)
* [The Fediverse](https://mastodon.technology/@charm)
* [Slack](https://charm.sh/slack)
* [The Fediverse](https://mastodon.social/@charmcli)
* [Discord](https://charm.sh/chat)
## License
[MIT](https://github.com/charmbracelet/gum/raw/main/LICENSE)
---
***
Part of [Charm](https://charm.sh).

10
ansi/ansi.go Normal file
View 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, "")
}

View file

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

View file

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

View file

@ -1,19 +1,28 @@
package choose
import "github.com/charmbracelet/gum/style"
import (
"time"
"github.com/charmbracelet/gum/style"
)
// Options is the customization options for the choose command.
type Options struct {
Options []string `arg:"" optional:"" help:"Options to choose from."`
Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"`
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
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"`
CursorPrefix string `help:"Prefix to show on the cursor item (hidden if limit is 1)" default:"[•] " env:"GUM_CHOOSE_CURSOR_PREFIX"`
SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:"[✕] " env:"GUM_CHOOSE_SELECTED_PREFIX"`
UnselectedPrefix string `help:"Prefix to show on unselected items (hidden if limit is 1)" default:"[ ] " env:"GUM_CHOOSE_UNSELECTED_PREFIX"`
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_CURSOR_"`
ItemStyle style.Styles `embed:"" prefix:"item." hidden:"" envprefix:"GUM_CHOOSE_ITEM_"`
SelectedItemStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_SELECTED_"`
Options []string `arg:"" optional:"" help:"Options to choose from."`
Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"`
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
Ordered bool `help:"Maintain the order of the selected options" env:"GUM_CHOOSE_ORDERED"`
Height int `help:"Height of the list" default:"10" env:"GUM_CHOOSE_HEIGHT"`
Cursor string `help:"Prefix to show on item that corresponds to the cursor position" default:"> " env:"GUM_CHOOSE_CURSOR"`
Header string `help:"Header value" default:"" env:"GUM_CHOOSE_HEADER"`
CursorPrefix string `help:"Prefix to show on the cursor item (hidden if limit is 1)" default:"• " env:"GUM_CHOOSE_CURSOR_PREFIX"`
SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:"✓ " env:"GUM_CHOOSE_SELECTED_PREFIX"`
UnselectedPrefix string `help:"Prefix to show on unselected items (hidden if limit is 1)" default:"• " env:"GUM_CHOOSE_UNSELECTED_PREFIX"`
Selected []string `help:"Options that should start as selected" default:"" env:"GUM_CHOOSE_SELECTED"`
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,...]
}

View file

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

View file

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

View file

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

12
cursor/cursor.go Normal file
View 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,
}

7
default.nix Normal file
View file

@ -0,0 +1,7 @@
{ pkgs }:
pkgs.buildGoModule {
name = "gum";
src = ./.;
vendorSha256 = "sha256-rOBwhPXo4sTSI3j3rn3c5qWGnGFgkpeFUKgtzKBltbg=";
}

2
examples/.gitignore vendored Normal file
View file

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

36
examples/README.md Normal file
View file

@ -0,0 +1,36 @@
# Glamour
A casual introduction. 你好世界!
## Let's talk about artichokes
The artichoke is mentioned as a garden
plant in the 8th century BC by Homer
and Hesiod. The naturally occurring
variant of the artichoke, the cardoon,
which is native to the Mediterranean
area, also has records of use as a
food among the ancient Greeks and
Romans. Pliny the Elder mentioned
growing of 'carduus' in Carthage
and Cordoba.
He holds him with his skinny hand,
There was ship,' quoth he.
'Hold off! unhand me, grey-beard loon!'
An artichoke dropt he.
## Other foods worth mentioning
1. Carrots
2. Celery
3. Tacos
• Soft
• Hard
4. Cucumber
## Things to eat today
* Carrots
* Ramen
* Currywurst

26
examples/choose.tape Normal file
View file

@ -0,0 +1,26 @@
Output choose.gif
Set Width 1000
Set Height 430
Set Shell bash
Type "gum choose {1..5}"
Sleep 500ms
Enter
Sleep 500ms
Down@250ms 3
Sleep 500ms
Up@250ms 2
Enter
Sleep 1.5s
Ctrl+L
Sleep 500ms
Type "gum choose --limit 2 Banana Cherry Orange"
Sleep 500ms
Enter
Sleep 500ms
Type@250ms "jxjxk"
Sleep 1s
Enter
Sleep 2s

View file

@ -10,6 +10,10 @@
#
# alias gcm='git commit -m "$(gum input)" -m "$(gum write)"'
# if [ -z "$(git status -s -uno | grep -v '^ ' | awk '{print $2}')" ]; then
# gum confirm "Stage all?" && git add .
# fi
TYPE=$(gum choose "fix" "feat" "docs" "style" "refactor" "test" "chore" "revert")
SCOPE=$(gum input --placeholder "scope")

45
examples/commit.tape Normal file
View file

@ -0,0 +1,45 @@
Output commit.gif
Set Shell "bash"
Set FontSize 32
Set Width 1200
Set Height 600
Type "./commit.sh" Sleep 500ms Enter
Sleep 1s
Down@250ms 2
Sleep 500ms
Enter
Sleep 500ms
Type "gum"
Sleep 500ms
Enter
Sleep 1s
Type "Gum is sooo tasty"
Sleep 500ms
Enter
Sleep 1s
Type@65ms "I love bubble gum."
Sleep 500ms
Alt+Enter
Sleep 500ms
Alt+Enter
Sleep 500ms
Type "This commit shows how much I love chewing bubble gum!!!"
Sleep 500ms
Enter
Sleep 1s
Left@400ms 3
Sleep 1s

26
examples/confirm.tape Normal file
View file

@ -0,0 +1,26 @@
Output confirm.gif
Set Width 1000
Set Height 350
Set Shell bash
Sleep 500ms
Type "gum confirm && echo 'Me too!' || echo 'Me neither.'"
Sleep 1s
Enter
Sleep 1s
Right
Sleep 500ms
Left
Sleep 500ms
Enter
Sleep 1.5s
Ctrl+L
Type "gum confirm && echo 'Me too!' || echo 'Me neither.'"
Sleep 500ms
Enter
Sleep 500ms
Right
Sleep 500ms
Enter
Sleep 1s

19
examples/customize.tape Normal file
View file

@ -0,0 +1,19 @@
Output customize.gif
Set Width 1000
Set Height 350
Set Shell bash
Sleep 1s
Type `gum input --cursor.foreground "#F4AC45" \` Enter
Type `--prompt.foreground "#04B575" --prompt "What's up? " \` Enter
Type `--placeholder "Not much, you?" --value "Not much, you?" \` Enter
Type `--width 80` Enter
Sleep 1s
Ctrl+A
Sleep 1s
Ctrl+E
Sleep 1s
Ctrl+U
Sleep 1s

View file

@ -5,7 +5,7 @@ NAME=$(gum input --placeholder "What is your name?")
echo -e "Well, it is nice to meet you, $(gum style --foreground 212 "$NAME")."
sleep 2; clear
sleep 1; clear
echo -e "Can you tell me a $(gum style --italic --foreground 99 'secret')?\n"
@ -14,7 +14,7 @@ gum write --placeholder "I'll keep it to myself, I promise!" > /dev/null # we ke
clear; echo "What should I do with this information?"; sleep 1
READ="Read"; THINK="Think"; DISCARD="Discard"
ACTIONS=$(gum choose --cursor-prefix "[ ] " --selected-prefix "[✓] " --no-limit "$READ" "$THINK" "$DISCARD")
ACTIONS=$(gum choose --no-limit "$READ" "$THINK" "$DISCARD")
clear; echo "One moment, please."
@ -24,8 +24,7 @@ grep -q "$DISCARD" <<< "$ACTIONS" && gum spin -s monkey --title " Discarding you
sleep 1; clear
echo "What's your favorite $(gum style --foreground 212 "Gum") flavor?"
GUM=$(echo -e "Cherry\nGrape\nLime\nOrange" | gum filter)
GUM=$(echo -e "Cherry\nGrape\nLime\nOrange" | gum filter --placeholder "Favorite flavor?")
echo "I'll keep that in mind!"
sleep 1; clear
@ -39,10 +38,10 @@ CHOICE=$(gum choose --item.foreground 250 "Yes" "No" "It's complicated")
sleep 1
gum spin --title "Chewing some $(gum style --foreground "#04B575" "$GUM") bubble gum..." -- sleep 5
gum spin --title "Chewing some $(gum style --foreground "#04B575" "$GUM") bubble gum..." -- sleep 2.5
clear
NICE_MEETING_YOU=$(gum style --height 5 --width 25 --padding '1 3' --border double --border-foreground 57 "Well, it was nice meeting you, $(gum style --foreground 212 "$NAME"). Hope to see you soon!")
CHEW_BUBBLE_GUM=$(gum style --width 25 --padding '1 3' --border double --border-foreground 212 "Don't forget to chew some $(gum style --foreground "#04B575" "$GUM") bubble gum.")
NICE_MEETING_YOU=$(gum style --height 5 --width 20 --padding '1 3' --border double --border-foreground 57 "Nice meeting you, $(gum style --foreground 212 "$NAME"). See you soon!")
CHEW_BUBBLE_GUM=$(gum style --width 17 --padding '1 3' --border double --border-foreground 212 "Go chew some $(gum style --foreground "#04B575" "$GUM") bubble gum.")
gum join --horizontal "$NICE_MEETING_YOU" "$CHEW_BUBBLE_GUM"

49
examples/demo.tape Normal file
View file

@ -0,0 +1,49 @@
Output ./demo.gif
Set Shell bash
Set FontSize 22
Set Width 800
Set Height 450
Type "./demo.sh"
Enter
Sleep 1s
Type "Walter"
Sleep 500ms
Enter
Sleep 2s
Type "Nope, sorry!"
Sleep 500ms
Alt+Enter
Sleep 200ms
Alt+Enter
Sleep 500ms
Type "I don't trust you."
Sleep 1s
Enter
Sleep 2s
Type "x" Sleep 250ms Type "j" Sleep 250ms
Type "x" Sleep 250ms Type "j" Sleep 250ms
Type "x" Sleep 1s
Enter
Sleep 6s
Type "li"
Sleep 1s
Enter
Sleep 3s
Down@500ms 2
Up@500ms 2
Sleep 1s
Enter
Sleep 6s

100
examples/diyfetch Executable file
View file

@ -0,0 +1,100 @@
#!/bin/sh
# ____ _____ ____ _ _
# | _ \_ _\ \ / / _| ___| |_ ___| |__
# | | | | | \ V / |_ / _ \ __/ __| '_ \
# | |_| | | | || _| __/ || (__| | | |
# |____/___| |_||_| \___|\__\___|_| |_|
#
# About:
# DIYfetch it the shell script template for writing fetch tool
# utilizing `gum join` command (https://github.com/charmbracelet/gum#join).
#
# This script is written in POSIX-shell for portability
# feel free to switch it to any scripting language that you prefer.
#
# Note:
# When copy ANSI string from random script make sure to replace "\033" and "\e" to ""
# or wrap it in `$(printf '%b' "<ansi_string>")`.
#
# URL: https://github.com/info-mono/diyfetch
# Prepare ------------------------------------------------------------------------------------------
# You can lookup the color codes on https://wikipedia.org/wiki/ANSI_escape_code#8-bit
main_color=4
# You can add some eye candy icons with Emoji of use Nerd Fonts (https://www.nerdfonts.com).
info=$(gum style "[1;38;5;${main_color}m${USER}@[1;38;5;${main_color}m$(hostname)
----------------
[1;38;5;${main_color}mOS: <your_os>
[1;38;5;${main_color}mKERNEL: $(uname -sr)
[1;38;5;${main_color}mUPTIME: $(uptime -p | cut -c 4-)
[1;38;5;${main_color}mSHELL: $(basename "${SHELL}")
[1;38;5;${main_color}mEDITOR: $(basename "${EDITOR:-<your_editor>}")
[1;38;5;${main_color}mDE: <your_de>
[1;38;5;${main_color}mWM: <your_wm>
[1;38;5;${main_color}mTERMINAL: <your_terminal>")
# You can get OS arts on https://github.com/info-mono/os-ansi
# copy the raw data of the .ansi file then paste it down below.
art=$(gum style ' ___
(.. |
(<> |
/ __ \
( / \/ |
_/\ __)/_)
\/-____\/')
# You can generate colorstrip using https://github.com/info-mono/colorstrip
color=$(gum style '████████████████████████
████████████████████████')
# Display ------------------------------------------------------------------------------------------
# The code in this section is to display the fetch adaptively to the terminal's size.
# If you just want a static fetch display, you can just use something like this:
#
# group_info_color=$(gum join --vertical "${info}" '' "${color}")
# gum join --horizontal --align center ' ' "${art}" ' ' "${group_info_color}"
terminal_size=$(stty size)
terminal_height=${terminal_size% *}
terminal_width=${terminal_size#* }
# Acknowledge of how high the shell prompt is so the prompt don't push the fetch out.
prompt_height=${PROMPT_HEIGHT:-1}
print_test() {
no_color=$(printf '%b' "${1}" | sed -e 's/\x1B\[[0-9;]*[JKmsu]//g')
[ "$(printf '%s' "${no_color}" | wc --lines)" -gt $(( terminal_height - prompt_height )) ] && return 1
[ "$(printf '%s' "${no_color}" | wc --max-line-length)" -gt "${terminal_width}" ] && return 1
gum style --align center --width="${terminal_width}" "${1}" ''
printf '%b' "\033[A"
exit 0
}
# Paper layout
print_test "$(gum join --vertical --align center "${art}" '' "${info}" '' "${color}")"
# Classic layout
group_info_color=$(gum join --vertical "${info}" '' "${color}")
print_test "$(gum join --horizontal --align center "${art}" ' ' "${group_info_color}")"
# Hybrid layout
group_art_info=$(gum join --horizontal --align center "${art}" ' ' "${info}")
print_test "$(gum join --vertical --align center "${group_art_info}" '' "${color}")"
# Other layout
print_test "$(gum join --vertical --align center "${art}" '' "${info}")"
print_test "${group_art_info}"
print_test "${group_info_color}"
print_test "${info}"
exit 1

1
examples/fav.txt Normal file
View file

@ -0,0 +1 @@
Banana

15
examples/file.tape Normal file
View file

@ -0,0 +1,15 @@
Output file.gif
Set Width 800
Set Height 525
Set Shell bash
Type "gum file .."
Enter
Sleep 1s
Down@150ms 6
Sleep 1s
Enter
Sleep 1s
Type "j"
Sleep 1s

4
examples/flavors.txt Normal file
View file

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

12
examples/format.ansi Normal file
View file

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


16
examples/input.tape Normal file
View file

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

47
examples/kaomoji.sh Normal file
View 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
View file

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

15
examples/pager.tape Normal file
View file

@ -0,0 +1,15 @@
Output pager.gif
Set Shell bash
Set Width 900
Set Height 750
Sleep 1s
Type "gum pager < README.md"
Enter
Sleep 1.5s
Down@100ms 25
Sleep 1s
Up@100ms 25
Sleep 3s

13
examples/spin.tape Normal file
View file

@ -0,0 +1,13 @@
Output spin.gif
Set Shell bash
Set Width 1200
Set Height 300
Set FontSize 36
Sleep 500ms
Type `gum spin --title "Buying Gum..." -- sleep 5`
Sleep 1s
Enter
Sleep 4s

2
examples/story.txt Normal file
View file

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

View file

@ -41,3 +41,12 @@ gum style --foreground 99 --border double --border-foreground 99 --padding "1 2"
# Write
gum write
gum write --width 40 --height 3 --placeholder "Type whatever you want" --prompt "| " --show-cursor-line --show-line-numbers --value "Something..." --base.padding 1 --cursor.foreground 99 --prompt.foreground 99
# Table
gum table < table/example.csv
# Pager
gum pager < README.md
# File
gum file

21
examples/write.tape Normal file
View file

@ -0,0 +1,21 @@
Output write.gif
Set Width 800
Set Height 350
Set Shell bash
Sleep 500ms
Type "gum write > story.txt"
Enter
Sleep 1s
Type "Once upon a time"
Sleep 1s
Alt+Enter
Type "In a land far, far away...."
Sleep 500ms
Enter
Sleep 1s
Type "cat story.txt"
Enter
Sleep 2s

59
file/command.go Normal file
View file

@ -0,0 +1,59 @@
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()
// XXX: These should be file selected specific.
theme.Focused.TextInput.Placeholder = o.PermissionsStyle.ToLipgloss()
theme.Focused.TextInput.Prompt = o.CursorStyle.ToLipgloss()
err = huh.NewForm(
huh.NewGroup(
huh.NewFilePicker().
Picking(true).
CurrentDirectory(path).
DirAllowed(o.Directory).
FileAllowed(o.File).
Height(o.Height).
ShowHidden(o.All).
Value(&path),
),
).
WithShowHelp(false).
WithTheme(theme).
Run()
if err != nil {
return err
}
fmt.Println(path)
return nil
}

30
file/options.go Normal file
View file

@ -0,0 +1,30 @@
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"`
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"`
}

View file

@ -6,16 +6,16 @@ import (
"os"
"strings"
"github.com/alecthomas/kong"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/mattn/go-isatty"
"github.com/sahilm/fuzzy"
"github.com/charmbracelet/gum/ansi"
"github.com/charmbracelet/gum/internal/exit"
"github.com/charmbracelet/gum/internal/files"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/style"
)
// Run provides a shell script interface for filtering through options, powered
@ -26,25 +26,29 @@ func (o Options) Run() error {
i.Prompt = o.Prompt
i.PromptStyle = o.PromptStyle.ToLipgloss()
i.PlaceholderStyle = o.PlaceholderStyle.ToLipgloss()
i.Placeholder = o.Placeholder
i.Width = o.Width
v := viewport.New(o.Width, o.Height)
var choices []string
if input, _ := stdin.Read(); input != "" {
input = strings.TrimSpace(input)
if input != "" {
choices = strings.Split(input, "\n")
if len(o.Options) == 0 {
if input, _ := stdin.Read(); input != "" {
o.Options = strings.Split(strings.TrimSuffix(input, "\n"), "\n")
} else {
o.Options = files.List()
}
} else {
choices = files.List()
}
if len(choices) == 0 {
if len(o.Options) == 0 {
return errors.New("no options provided, see `gum filter --help`")
}
if o.SelectIfOne && len(o.Options) == 1 {
fmt.Println(o.Options[0])
return nil
}
options := []tea.ProgramOption{tea.WithOutput(os.Stderr)}
if o.Height == 0 {
options = append(options, tea.WithAltScreen())
@ -53,19 +57,25 @@ func (o Options) Run() error {
var matches []fuzzy.Match
if o.Value != "" {
i.SetValue(o.Value)
matches = fuzzy.Find(o.Value, choices)
} else {
matches = matchAll(choices)
}
switch {
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 {
o.Limit = len(choices)
o.Limit = len(o.Options)
}
p := tea.NewProgram(model{
choices: choices,
choices: o.Options,
indicator: o.Indicator,
matches: matches,
header: o.Header,
textinput: i,
viewport: &v,
indicatorStyle: o.IndicatorStyle.ToLipgloss(),
@ -74,38 +84,55 @@ func (o Options) Run() error {
unselectedPrefixStyle: o.UnselectedPrefixStyle.ToLipgloss(),
unselectedPrefix: o.UnselectedPrefix,
matchStyle: o.MatchStyle.ToLipgloss(),
headerStyle: o.HeaderStyle.ToLipgloss(),
textStyle: o.TextStyle.ToLipgloss(),
cursorTextStyle: o.CursorTextStyle.ToLipgloss(),
height: o.Height,
selected: make(map[string]struct{}),
limit: o.Limit,
reverse: o.Reverse,
fuzzy: o.Fuzzy,
timeout: o.Timeout,
hasTimeout: o.Timeout > 0,
sort: o.Sort,
}, options...)
tm, err := p.StartReturningModel()
tm, err := p.Run()
if err != nil {
return fmt.Errorf("unable to run filter: %w", err)
}
m := tm.(model)
if m.aborted {
return exit.ErrAborted
}
isTTY := isatty.IsTerminal(os.Stdout.Fd())
// allSelections contains values only if limit is greater
// than 1 or if flag --no-limit is passed, hence there is
// no need to further checks
if len(m.selected) > 0 {
for k := range m.selected {
fmt.Println(k)
}
o.checkSelected(m, isTTY)
} else if len(m.matches) > m.cursor && m.cursor >= 0 {
fmt.Println(m.matches[m.cursor].Str)
if isTTY {
fmt.Println(m.matches[m.cursor].Str)
} else {
fmt.Println(ansi.Strip(m.matches[m.cursor].Str))
}
}
if !o.Strict && len(m.textinput.Value()) != 0 && len(m.matches) == 0 {
fmt.Println(m.textinput.Value())
}
return nil
}
// BeforeReset hook. Used to unclutter style flags.
func (o Options) BeforeReset(ctx *kong.Context) error {
style.HideFlags(ctx)
return nil
func (o Options) checkSelected(m model, isTTY bool) {
for k := range m.selected {
if isTTY {
fmt.Println(k)
} else {
fmt.Println(ansi.Strip(k))
}
}
}

View file

@ -12,12 +12,14 @@ package filter
import (
"strings"
"time"
"github.com/charmbracelet/gum/timeout"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/mattn/go-runewidth"
"github.com/sahilm/fuzzy"
)
@ -27,6 +29,7 @@ type model struct {
choices []string
matches []fuzzy.Match
cursor int
header string
selected map[string]struct{}
limit int
numSelected int
@ -36,30 +39,58 @@ type model struct {
height int
aborted bool
quitting bool
headerStyle lipgloss.Style
matchStyle lipgloss.Style
textStyle lipgloss.Style
cursorTextStyle lipgloss.Style
indicatorStyle lipgloss.Style
selectedPrefixStyle lipgloss.Style
unselectedPrefixStyle lipgloss.Style
reverse bool
fuzzy bool
sort bool
timeout time.Duration
hasTimeout bool
}
func (m model) Init() tea.Cmd { return nil }
func (m model) Init() tea.Cmd {
return timeout.Init(m.timeout, nil)
}
func (m model) View() string {
if m.quitting {
return ""
}
var s strings.Builder
var lineTextStyle lipgloss.Style
// For reverse layout, if the number of matches is less than the viewport
// height, we need to offset the matches so that the first match is at the
// bottom edge of the viewport instead of in the middle.
if m.reverse && len(m.matches) < m.viewport.Height {
s.WriteString(strings.Repeat("\n", m.viewport.Height-len(m.matches)))
}
// Since there are matches, display them so that the user can see, in real
// time, what they are searching for.
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
// represent it. Otherwise, simply pad the string.
// The line's text style is set depending on whether or not the cursor
// points to this line.
if i == m.cursor {
s.WriteString(m.indicatorStyle.Render(m.indicator))
lineTextStyle = m.cursorTextStyle
} else {
s.WriteString(strings.Repeat(" ", runewidth.StringWidth(m.indicator)))
s.WriteString(strings.Repeat(" ", lipgloss.Width(m.indicator)))
lineTextStyle = m.textStyle
}
// If there are multiple selections mark them, otherwise leave an empty space
@ -74,20 +105,27 @@ func (m model) View() string {
// For this match, there are a certain number of characters that have
// caused the match. i.e. fuzzy matching.
// 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 {
// Check if the current character index matches the current matched
// index. If so, color the character to indicate a match.
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)))
// We have matched this character, so we never have to check it
// again. Move on to the next match.
mi++
} else {
// Not a match, simply show the character, unstyled.
s.WriteString(m.textStyle.Render(string(c)))
// Not a match, buffer a regular character.
buf.WriteRune(c)
}
}
// Flush text buffer.
s.WriteString(lineTextStyle.Render(buf.String()))
// We have finished displaying the match with all of it's matched
// characters highlighted and the rest filled in.
@ -98,16 +136,48 @@ func (m model) View() string {
m.viewport.SetContent(s.String())
// 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) {
var cmd tea.Cmd
switch msg := msg.(type) {
case timeout.TickTimeoutMsg:
if msg.TimeoutValue <= 0 {
m.quitting = true
m.aborted = true
return m, tea.Quit
}
m.timeout = msg.TimeoutValue
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
case tea.WindowSizeMsg:
if m.height == 0 || m.height > msg.Height {
m.viewport.Height = msg.Height - lipgloss.Height(m.textinput.View())
}
// 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
if m.reverse {
m.viewport.YOffset = clamp(0, len(m.matches), len(m.matches)-m.viewport.Height)
}
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc":
@ -118,48 +188,62 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.quitting = true
return m, tea.Quit
case "ctrl+n", "ctrl+j", "down":
m.cursor = clamp(0, len(m.matches)-1, m.cursor+1)
if m.cursor >= m.viewport.YOffset+m.viewport.Height {
m.viewport.LineDown(1)
}
m.CursorDown()
case "ctrl+p", "ctrl+k", "up":
m.cursor = clamp(0, len(m.matches)-1, m.cursor-1)
if m.cursor < m.viewport.YOffset {
m.viewport.SetYOffset(m.cursor)
}
m.CursorUp()
case "tab":
if m.limit == 1 {
break // no op
}
// Tab is used to toggle selection of current item in the list
if _, ok := m.selected[m.matches[m.cursor].Str]; ok {
delete(m.selected, m.matches[m.cursor].Str)
m.numSelected--
} else if m.numSelected < m.limit {
m.selected[m.matches[m.cursor].Str] = struct{}{}
m.numSelected++
m.ToggleSelection()
m.CursorDown()
case "shift+tab":
if m.limit == 1 {
break // no op
}
// Go down by one line
m.cursor = clamp(0, len(m.matches)-1, m.cursor+1)
if m.cursor >= m.viewport.YOffset+m.viewport.Height {
m.viewport.LineDown(1)
m.ToggleSelection()
m.CursorUp()
case "ctrl+@":
if m.limit == 1 {
break // no op
}
m.ToggleSelection()
default:
m.textinput, cmd = m.textinput.Update(msg)
// A character was entered, this likely means that the text input
// has changed. This suggests that the matches are outdated, so
// update them, with a fuzzy finding algorithm provided by
// https://github.com/sahilm/fuzzy
m.matches = fuzzy.Find(m.textinput.Value(), m.choices)
// yOffsetFromBottom is the number of lines from the bottom of the
// list to the top of the viewport. This is used to keep the viewport
// at a constant position when the number of matches are reduced
// in the reverse layout.
var yOffsetFromBottom int
if m.reverse {
yOffsetFromBottom = max(0, len(m.matches)-m.viewport.YOffset)
}
// A character was entered, this likely means that the text input has
// changed. This suggests that the matches are outdated, so update them.
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
// (none), but rather display all possible choices.
if m.textinput.Value() == "" {
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)
}
}
}
@ -169,14 +253,75 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, cmd
}
func (m *model) CursorUp() {
if m.reverse {
m.cursor = clamp(0, len(m.matches)-1, m.cursor+1)
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() {
if m.reverse {
m.cursor = clamp(0, len(m.matches)-1, m.cursor-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)
}
}
}
func (m *model) ToggleSelection() {
if _, ok := m.selected[m.matches[m.cursor].Str]; ok {
delete(m.selected, m.matches[m.cursor].Str)
m.numSelected--
} else if m.numSelected < m.limit {
m.selected[m.matches[m.cursor].Str] = struct{}{}
m.numSelected++
}
}
func matchAll(options []string) []fuzzy.Match {
var matches = make([]fuzzy.Match, len(options))
matches := make([]fuzzy.Match, len(options))
for i, option := range options {
matches[i] = fuzzy.Match{Str: option}
}
return matches
}
func exactMatches(search string, choices []string) []fuzzy.Match {
matches := fuzzy.Matches{}
for i, choice := range choices {
search = strings.ToLower(search)
matchedString := strings.ToLower(choice)
index := strings.Index(matchedString, search)
if index >= 0 {
matchedIndexes := []int{}
for s := range search {
matchedIndexes = append(matchedIndexes, index+s)
}
matches = append(matches, fuzzy.Match{
Str: choice,
Index: i,
MatchedIndexes: matchedIndexes,
})
}
}
return matches
}
//nolint:unparam
func clamp(min, max, val int) int {
if val < min {
@ -187,3 +332,10 @@ func clamp(min, max, val int) int {
}
return val
}
func max(a, b int) int {
if a > b {
return a
}
return b
}

View file

@ -1,23 +1,39 @@
package filter
import "github.com/charmbracelet/gum/style"
import (
"time"
"github.com/charmbracelet/gum/style"
)
// Options is the customization options for the filter command.
type Options struct {
Indicator string `help:"Character for selection" default:"•" env:"GUM_FILTER_INDICATOR"`
IndicatorStyle style.Styles `embed:"" prefix:"indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_INDICATOR_"`
Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"`
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
SelectedPrefix string `help:"Character to indicate selected items (hidden if limit is 1)" default:" ◉ " env:"GUM_FILTER_SELECTED_PREFIX"`
SelectedPrefixStyle style.Styles `embed:"" prefix:"selected-indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_SELECTED_PREFIX_"`
UnselectedPrefix string `help:"Character to indicate unselected items (hidden if limit is 1)" default:" ○ " env:"GUM_FILTER_UNSELECTED_PREFIX"`
UnselectedPrefixStyle style.Styles `embed:"" prefix:"unselected-prefix." set:"defaultForeground=240" envprefix:"GUM_FILTER_UNSELECTED_PREFIX_"`
TextStyle style.Styles `embed:"" prefix:"text." envprefix:"GUM_FILTER_TEXT_"`
MatchStyle style.Styles `embed:"" prefix:"match." set:"defaultForeground=212" envprefix:"GUM_FILTER_MATCH_"`
Placeholder string `help:"Placeholder value" default:"Filter..." env:"GUM_FILTER_PLACEHOLDER"`
Prompt string `help:"Prompt to display" default:"> " env:"GUM_FILTER_PROMPT"`
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=240" envprefix:"GUM_FILTER_PROMPT_"`
Width int `help:"Input width" default:"20" env:"GUM_FILTER_WIDTH"`
Height int `help:"Input height" default:"0" env:"GUM_FILTER_HEIGHT"`
Value string `help:"Initial filter value" default:"" env:"GUM_FILTER_VALUE"`
Options []string `arg:"" optional:"" help:"Options to filter."`
Indicator string `help:"Character for selection" default:"•" env:"GUM_FILTER_INDICATOR"`
IndicatorStyle style.Styles `embed:"" prefix:"indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_INDICATOR_"`
Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"`
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
SelectIfOne bool `help:"Select the given option if there is only one" group:"Selection"`
Strict bool `help:"Only returns if anything matched. Otherwise return Filter" negatable:"true" default:"true" group:"Selection"`
SelectedPrefix string `help:"Character to indicate selected items (hidden if limit is 1)" default:" ◉ " env:"GUM_FILTER_SELECTED_PREFIX"`
SelectedPrefixStyle style.Styles `embed:"" prefix:"selected-indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_SELECTED_PREFIX_"`
UnselectedPrefix string `help:"Character to indicate unselected items (hidden if limit is 1)" default:" ○ " env:"GUM_FILTER_UNSELECTED_PREFIX"`
UnselectedPrefixStyle style.Styles `embed:"" prefix:"unselected-prefix." set:"defaultForeground=240" envprefix:"GUM_FILTER_UNSELECTED_PREFIX_"`
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_FILTER_HEADER_"`
Header string `help:"Header value" default:"" env:"GUM_FILTER_HEADER"`
TextStyle style.Styles `embed:"" prefix:"text." envprefix:"GUM_FILTER_TEXT_"`
CursorTextStyle style.Styles `embed:"" prefix:"cursor-text." envprefix:"GUM_FILTER_CURSOR_TEXT_"`
MatchStyle style.Styles `embed:"" prefix:"match." set:"defaultForeground=212" envprefix:"GUM_FILTER_MATCH_"`
Placeholder string `help:"Placeholder value" default:"Filter..." env:"GUM_FILTER_PLACEHOLDER"`
Prompt string `help:"Prompt to display" default:"> " env:"GUM_FILTER_PROMPT"`
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=240" envprefix:"GUM_FILTER_PROMPT_"`
PlaceholderStyle style.Styles `embed:"" prefix:"placeholder." set:"defaultForeground=240" envprefix:"GUM_FILTER_PLACEHOLDER_"`
Width int `help:"Input width" default:"20" env:"GUM_FILTER_WIDTH"`
Height int `help:"Input height" default:"0" env:"GUM_FILTER_HEIGHT"`
Value string `help:"Initial filter value" default:"" env:"GUM_FILTER_VALUE"`
Reverse bool `help:"Display from the bottom of the screen" env:"GUM_FILTER_REVERSE"`
Fuzzy bool `help:"Enable fuzzy matching" default:"true" env:"GUM_FILTER_FUZZY" negatable:""`
Sort bool `help:"Sort the results" default:"true" env:"GUM_FILTER_SORT" negatable:""`
Timeout time.Duration `help:"Timeout until filter command aborts" default:"0" env:"GUM_FILTER_TIMEOUT"`
}

43
flake.lock Normal file
View file

@ -0,0 +1,43 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1664107978,
"narHash": "sha256-31I9XnIjXkUa62BM1Zr/ylKMf9eVO5PtoX2mGpmB7/U=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "72783a2d0dbbf030bff1537873dd5b85b3fb332f",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-22.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

19
flake.nix Normal file
View file

@ -0,0 +1,19 @@
{
description = "A tool for glamorous shell scripts";
inputs = {
nixpkgs.url = github:nixos/nixpkgs/nixos-22.05;
flake-utils.url = github:numtide/flake-utils;
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let pkgs = import nixpkgs { inherit system; }; in
rec {
packages.default = import ./default.nix { inherit pkgs; };
}) // {
overlays.default = final: prev: {
gum = import ./default.nix { pkgs = final; };
};
};
}

View file

@ -17,30 +17,30 @@ import (
"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.
func (o Options) Run() error {
var input string
var input, output string
var err error
if len(o.Template) > 0 {
input = strings.Join(o.Template, "\n")
} else {
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 {
return err
}
fmt.Println(v)
fmt.Print(output)
return nil
}

View file

@ -9,7 +9,7 @@ import (
"github.com/muesli/termenv"
)
var code Func = func(input string) (string, error) {
func code(input, language string) (string, error) {
renderer, err := glamour.NewTermRenderer(
glamour.WithAutoStyle(),
glamour.WithWordWrap(0),
@ -17,14 +17,14 @@ var code Func = func(input string) (string, error) {
if err != nil {
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 {
return "", fmt.Errorf("unable to render: %w", err)
}
return output, nil
}
var emoji Func = func(input string) (string, error) {
func emoji(input string) (string, error) {
renderer, err := glamour.NewTermRenderer(
glamour.WithEmoji(),
)
@ -38,13 +38,13 @@ var emoji Func = func(input string) (string, error) {
return output, nil
}
var markdown Func = func(input string) (string, error) {
func markdown(input string, theme string) (string, error) {
renderer, err := glamour.NewTermRenderer(
glamour.WithStandardStyle("pink"),
glamour.WithStylePath(theme),
glamour.WithWordWrap(0),
)
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)
if err != nil {
@ -53,8 +53,8 @@ var markdown Func = func(input string) (string, error) {
return output, nil
}
var template Func = func(input string) (string, error) {
f := termenv.TemplateFuncs(termenv.ColorProfile())
func template(input string) (string, error) {
f := termenv.TemplateFuncs(termenv.ANSI256)
t, err := tpl.New("tpl").Funcs(f).Parse(input)
if err != nil {
return "", fmt.Errorf("unable to parse template: %w", err)

View file

@ -3,6 +3,8 @@ package format
// Options is customization options for the format command.
type Options struct {
Template []string `arg:"" optional:"" help:"Template string to format (can also be provided via stdin)"`
Theme string `help:"Glamour theme to use for markdown formatting" default:"pink" 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"`
}

52
go.mod
View file

@ -1,16 +1,50 @@
module github.com/charmbracelet/gum
go 1.16
go 1.21
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/charmbracelet/bubbles v0.13.1-0.20220804185250-84eacf535a81
github.com/charmbracelet/bubbletea v0.22.0
github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da
github.com/charmbracelet/lipgloss v0.5.0
github.com/mattn/go-runewidth v0.0.13
github.com/charmbracelet/bubbles v0.18.0
github.com/charmbracelet/bubbletea v0.26.1
github.com/charmbracelet/glamour v0.7.0
github.com/charmbracelet/huh v0.3.1-0.20240328185852-590ecabc34b9
github.com/charmbracelet/lipgloss v0.10.0
github.com/charmbracelet/log v0.4.0
github.com/mattn/go-isatty v0.0.20
github.com/muesli/reflow v0.3.0
github.com/muesli/roff v0.1.0
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739
github.com/sahilm/fuzzy v0.1.0
github.com/muesli/termenv v0.15.2
github.com/sahilm/fuzzy v0.1.1
)
require (
github.com/alecthomas/chroma/v2 v2.13.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/catppuccin/go v0.2.0 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240328150354-ab9afc214dfd // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/gorilla/css v1.0.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/yuin/goldmark v1.7.0 // indirect
github.com/yuin/goldmark-emoji v1.0.2 // indirect
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/term v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
)

157
go.sum
View file

@ -1,100 +1,109 @@
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/alecthomas/kong v0.4.0/go.mod h1:uzxf/HUh0tj43x1AyJROl3JT7SgsZ5m+icOv1csRhc0=
github.com/alecthomas/kong v0.6.1 h1:1kNhcFepkR+HmasQpbiKDLylIL8yh5B5y1zPp5bJimA=
github.com/alecthomas/kong v0.6.1/go.mod h1:JfHWDzLmbh/puW6I3V7uWenoh56YNVONW+w8eKeUr9I=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI=
github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk=
github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA=
github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os=
github.com/alecthomas/mango-kong v0.1.0 h1:iFVfP1k1K4qpml3JUQmD5I8MCQYfIvsD9mRdrw7jJC4=
github.com/alecthomas/mango-kong v0.1.0/go.mod h1:t+TYVdsONUolf/BwVcm+15eqcdAj15h4Qe9MMFAwwT4=
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48=
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/charmbracelet/bubbles v0.13.1-0.20220804185250-84eacf535a81 h1:iQZnv9OkOx/jhBSVNn5bIhH3PdJgVk82dVCIVJg9jik=
github.com/charmbracelet/bubbles v0.13.1-0.20220804185250-84eacf535a81/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=
github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4=
github.com/charmbracelet/bubbletea v0.22.0 h1:E1BTNSE3iIrq0G0X6TjGAmrQ32cGCbFDPcIuImikrUc=
github.com/charmbracelet/bubbletea v0.22.0/go.mod h1:aoVIwlNlr5wbCB26KhxfrqAn0bMp4YpJcoOelbxApjs=
github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da h1:FGz53GWQRiKQ/5xUsoCCkewSQIC7u81Scaxx2nUy3nM=
github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da/go.mod h1:HXz79SMFnF9arKxqeoHWxmo1BhplAH7wehlRhKQIL94=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
github.com/charmbracelet/bubbletea v0.26.1 h1:xujcQeF73rh4jwu3+zhfQsvV18x+7zIjlw7/CYbzGJ0=
github.com/charmbracelet/bubbletea v0.26.1/go.mod h1:FzKr7sKoO8iFVcdIBM9J0sJOcQv5nDQaYwsee3kpbgo=
github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng=
github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps=
github.com/charmbracelet/huh v0.3.1-0.20240328185852-590ecabc34b9 h1:Izr4MC+shs9PpR4MWz/OFA4+ywbKutvPv0eSHJwfn60=
github.com/charmbracelet/huh v0.3.1-0.20240328185852-590ecabc34b9/go.mod h1:x0rYoA1kpsaefXhRJZuxLM+qP4CYyEFE67T3ZGl7zPU=
github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/charmbracelet/x/exp/strings v0.0.0-20240328150354-ab9afc214dfd h1:yTFoT3v/wDWzeoRXt9mIKlslAKfVNr0XdVCOVwRK8ck=
github.com/charmbracelet/x/exp/strings v0.0.0-20240328150354-ab9afc214dfd/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/exp/term v0.0.0-20240321133156-7faadd06c281 h1:ZYwrF0GAd859tU6oF63T2pIkZVQ4z9BosDVD7jYu93A=
github.com/charmbracelet/x/exp/term v0.0.0-20240321133156-7faadd06c281/go.mod h1:madZtB2OVDOG+ZnLruGITVZceYy047W+BLQ1MNQzbWg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/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.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
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.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/microcosm-cc/bluemonday v1.0.19 h1:OI7hoF5FY4pFz2VA//RN8TfM0YJ2dJcl4P4APrCWy6c=
github.com/microcosm-cc/bluemonday v1.0.19/go.mod h1:QNzV2UbLK2/53oIIwTOyLUSABMkjZ4tqiyC1g/DyqxE=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
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.1 h1:Xzd1B4U5bWQOuSKuN398MyynIGTNT89dxzpEDsalXZs=
github.com/muesli/cancelreader v0.2.1/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/mango v0.1.1-0.20220205060214-77e2058169ab h1:m7QFONkzLK0fVXCjwX5tANcnj1yXxTnYQtnfJiY3tcA=
github.com/muesli/mango v0.1.1-0.20220205060214-77e2058169ab/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
github.com/muesli/termenv v0.11.0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
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/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs=
github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

63
gum.go
View file

@ -6,13 +6,17 @@ import (
"github.com/charmbracelet/gum/choose"
"github.com/charmbracelet/gum/completion"
"github.com/charmbracelet/gum/confirm"
"github.com/charmbracelet/gum/file"
"github.com/charmbracelet/gum/filter"
"github.com/charmbracelet/gum/format"
"github.com/charmbracelet/gum/input"
"github.com/charmbracelet/gum/join"
"github.com/charmbracelet/gum/log"
"github.com/charmbracelet/gum/man"
"github.com/charmbracelet/gum/pager"
"github.com/charmbracelet/gum/spin"
"github.com/charmbracelet/gum/style"
"github.com/charmbracelet/gum/table"
"github.com/charmbracelet/gum/write"
)
@ -54,6 +58,20 @@ type Gum struct {
//
Confirm confirm.Options `cmd:"" help:"Ask a user to confirm an action"`
// File provides an interface to pick a file from a folder (tree).
// The user is provided a file manager-like interface to navigate, to
// select a file.
//
// Let's pick a file from the current directory:
//
// $ gum file
// $ gum file .
//
// Let's pick a file from the home directory:
//
// $ gum file $HOME
File file.Options `cmd:"" help:"Pick a file from a folder"`
// Filter provides a fuzzy searching text input to allow filtering a list of
// options to select one option.
//
@ -100,6 +118,25 @@ type Gum struct {
//
Join join.Options `cmd:"" help:"Join text vertically or horizontally"`
// Pager provides a shell script interface for the viewport bubble.
// https://github.com/charmbracelet/bubbles/tree/master/viewport
//
// It allows the user to scroll through content like a pager.
//
// ╭────────────────────────────────────────────────╮
// │ 1 │ Gum Pager │
// │ 2 │ ========= │
// │ 3 │ │
// │ 4 │ ``` │
// │ 5 │ gum pager --height 10 --width 25 < text │
// │ 6 │ ``` │
// │ 7 │ │
// │ 8 │ │
// ╰────────────────────────────────────────────────╯
// ↑/↓: Navigate • q: Quit
//
Pager pager.Options `cmd:"" help:"Scroll through a file"`
// Spin provides a shell script interface for the spinner bubble.
// https://github.com/charmbracelet/bubbles/tree/master/spinner
//
@ -142,6 +179,23 @@ type Gum struct {
//
Style style.Options `cmd:"" help:"Apply coloring, borders, spacing to text"`
// Table provides a shell script interface for the table bubble.
// https://github.com/charmbracelet/bubbles/tree/master/table
//
// It is useful to render tabular (CSV) data in a terminal and allows
// the user to select a row from the table.
//
// Let's render a table of gum flavors:
//
// $ gum table <<< "Flavor,Price\nStrawberry,$0.50\nBanana,$0.99\nCherry,$0.75"
//
// Flavor Price
// Strawberry $0.50
// Banana $0.99
// Cherry $0.75
//
Table table.Options `cmd:"" help:"Render a table of data"`
// Write provides a shell script interface for the text area bubble.
// https://github.com/charmbracelet/bubbles/tree/master/textarea
//
@ -151,4 +205,13 @@ type Gum struct {
// $ gum write > output.text
//
Write write.Options `cmd:"" help:"Prompt for long-form text"`
// Log provides a shell script interface for logging using Log.
// https://github.com/charmbracelet/log
//
// It can be used to log messages to output.
//
// $ gum log --level info "Hello, world!"
//
Log log.Options `cmd:"" help:"Log messages to output"`
}

View file

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

View file

@ -1,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
}

View file

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

26
internal/stack/stack.go Normal file
View 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)
},
}
}

View file

@ -10,13 +10,8 @@ import (
// Read reads input from an stdin pipe.
func Read() (string, error) {
stat, err := os.Stdin.Stat()
if err != nil {
return "", fmt.Errorf("failed to stat stdin: %w", err)
}
if stat.Mode()&os.ModeNamedPipe == 0 && stat.Size() == 0 {
return "", nil
if IsEmpty() {
return "", fmt.Errorf("stdin is empty")
}
reader := bufio.NewReader(os.Stdin)
@ -35,3 +30,17 @@ func Read() (string, error) {
return b.String(), 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
View 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
View 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
View 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_"`
}

29
main.go
View file

@ -28,7 +28,7 @@ var (
var bubbleGumPink = lipgloss.NewStyle().Foreground(lipgloss.Color("212"))
func main() {
lipgloss.SetColorProfile(termenv.ANSI256)
lipgloss.SetColorProfile(termenv.NewOutput(os.Stderr).Profile)
if Version == "" {
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" {
@ -48,16 +48,27 @@ func main() {
kong.Description(fmt.Sprintf("A tool for %s shell scripts.", bubbleGumPink.Render("glamorous"))),
kong.UsageOnError(),
kong.ConfigureHelp(kong.HelpOptions{
Compact: true,
Summary: false,
Compact: true,
Summary: false,
NoExpandSubcommands: true,
}),
kong.Vars{
"version": version,
"defaultBackground": "",
"defaultForeground": "",
"defaultMargin": "0 0",
"defaultPadding": "0 0",
"defaultUnderline": "false",
"version": version,
"defaultHeight": "0",
"defaultWidth": "0",
"defaultAlign": "left",
"defaultBorder": "none",
"defaultBorderForeground": "",
"defaultBorderBackground": "",
"defaultBackground": "",
"defaultForeground": "",
"defaultMargin": "0 0",
"defaultPadding": "0 0",
"defaultUnderline": "false",
"defaultBold": "false",
"defaultFaint": "false",
"defaultItalic": "false",
"defaultStrikethrough": "false",
},
)
if err := ctx.Run(); err != nil {

View file

@ -16,7 +16,7 @@ func (m Man) BeforeApply(ctx *kong.Context) error {
// Set the correct man pages description without color escape sequences.
ctx.Model.Help = "A tool for glamorous shell scripts."
man := mangokong.NewManPage(1, ctx.Model)
man = man.WithSection("Copyright", "(C) 2021-2022 Charmbracelet, Inc.\n"+
man = man.WithSection("Copyright", "(C) 2022-2023 Charmbracelet, Inc.\n"+
"Released under MIT license.")
fmt.Fprint(ctx.Stdout, man.Build(roff.NewDocument()))
ctx.Exit(0)

50
pager/command.go Normal file
View 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
View 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
View 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
View 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()
}

View file

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

View file

@ -1,14 +1,21 @@
package spin
import "github.com/charmbracelet/gum/style"
import (
"time"
"github.com/charmbracelet/gum/style"
)
// Options is the customization options for the spin command.
type Options struct {
Command []string `arg:"" help:"Command to run"`
ShowOutput bool `help:"Show output of command" default:"false" env:"GUM_SPIN_SHOW_OUTPUT"`
Spinner string `help:"Spinner type" short:"s" type:"spinner" enum:"line,dot,minidot,jump,pulse,points,globe,moon,monkey,meter,hamburger" default:"dot" env:"GUM_SPIN_SPINNER"`
SpinnerStyle style.Styles `embed:"" prefix:"spinner." set:"defaultForeground=212" envprefix:"GUM_SPIN_SPINNER_"`
Title string `help:"Text to display to user while spinning" default:"Loading..." env:"GUM_SPIN_TITLE"`
TitleStyle style.Styles `embed:"" prefix:"title." envprefix:"GUM_SPIN_TITLE_"`
ShowOutput bool `help:"Show or pipe output of command during execution" default:"false" env:"GUM_SPIN_SHOW_OUTPUT"`
ShowError bool `help:"Show output of command only if the command fails" default:"false" env:"GUM_SPIN_SHOW_ERROR"`
Spinner string `help:"Spinner type" short:"s" type:"spinner" enum:"line,dot,minidot,jump,pulse,points,globe,moon,monkey,meter,hamburger" default:"dot" env:"GUM_SPIN_SPINNER"`
SpinnerStyle style.Styles `embed:"" prefix:"spinner." set:"defaultForeground=212" envprefix:"GUM_SPIN_SPINNER_"`
Title string `help:"Text to display to user while spinning" default:"Loading..." env:"GUM_SPIN_TITLE"`
TitleStyle style.Styles `embed:"" prefix:"title." envprefix:"GUM_SPIN_TITLE_"`
Align string `help:"Alignment of spinner with regard to the title" short:"a" type:"align" enum:"left,right" default:"left" env:"GUM_SPIN_ALIGN"`
Timeout time.Duration `help:"Timeout until spin command aborts" default:"0" env:"GUM_SPIN_TIMEOUT"`
}

View file

@ -15,27 +15,45 @@
package spin
import (
"io"
"os"
"os/exec"
"strings"
"time"
"github.com/charmbracelet/gum/internal/exit"
"github.com/charmbracelet/gum/timeout"
"github.com/mattn/go-isatty"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
)
type model struct {
spinner spinner.Model
title string
command []string
aborted bool
status int
stdout string
stderr string
spinner spinner.Model
title string
align string
command []string
quitting bool
aborted bool
status int
stdout string
stderr string
output string
showOutput bool
showError bool
timeout time.Duration
hasTimeout bool
}
var bothbuf strings.Builder
var outbuf strings.Builder
var errbuf strings.Builder
type finishCommandMsg struct {
stdout string
stderr string
output string
status int
}
@ -47,9 +65,15 @@ func commandStart(command []string) tea.Cmd {
}
cmd := exec.Command(command[0], args...) //nolint:gosec
var outbuf, errbuf strings.Builder
cmd.Stdout = &outbuf
cmd.Stderr = &errbuf
if isatty.IsTerminal(os.Stdout.Fd()) {
stdout := io.MultiWriter(&bothbuf, &errbuf)
stderr := io.MultiWriter(&bothbuf, &outbuf)
cmd.Stdout = stdout
cmd.Stderr = stderr
} else {
cmd.Stdout = os.Stdout
}
_ = cmd.Run()
@ -62,6 +86,7 @@ func commandStart(command []string) tea.Cmd {
return finishCommandMsg{
stdout: outbuf.String(),
stderr: errbuf.String(),
output: bothbuf.String(),
status: status,
}
}
@ -71,17 +96,49 @@ func (m model) Init() tea.Cmd {
return tea.Batch(
m.spinner.Tick,
commandStart(m.command),
timeout.Init(m.timeout, nil),
)
}
func (m model) View() string { return m.spinner.View() + " " + m.title }
func (m model) View() string {
if m.quitting && m.showOutput {
return strings.TrimPrefix(errbuf.String()+"\n"+outbuf.String(), "\n")
}
var str string
if m.hasTimeout {
str = timeout.Str(m.timeout)
}
var header string
if m.align == "left" {
header = m.spinner.View() + str + " " + m.title
} else {
header = str + " " + m.title + " " + m.spinner.View()
}
if !m.showOutput {
return header
}
return header + errbuf.String() + "\n" + outbuf.String()
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case timeout.TickTimeoutMsg:
if msg.TimeoutValue <= 0 {
// grab current output before closing for piped instances
m.stdout = outbuf.String()
m.status = exit.StatusAborted
return m, tea.Quit
}
m.timeout = msg.TimeoutValue
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
case finishCommandMsg:
m.stdout = msg.stdout
m.stderr = msg.stderr
m.output = msg.output
m.status = msg.status
m.quitting = true
return m, tea.Quit
case tea.KeyMsg:
switch msg.String() {

View file

@ -2,8 +2,8 @@ package style
import "github.com/charmbracelet/lipgloss"
// border maps strings to `lipgloss.Border`s.
var border map[string]lipgloss.Border = map[string]lipgloss.Border{
// Border maps strings to `lipgloss.Border`s.
var Border map[string]lipgloss.Border = map[string]lipgloss.Border{
"double": lipgloss.DoubleBorder(),
"hidden": lipgloss.HiddenBorder(),
"none": {},

View file

@ -6,32 +6,26 @@
package style
import (
"errors"
"fmt"
"strings"
"github.com/alecthomas/kong"
"github.com/charmbracelet/gum/internal/stdin"
)
// Run provides a shell script interface for the Lip Gloss styling.
// https://github.com/charmbracelet/lipgloss
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`")
}
text = strings.TrimSuffix(text, "\n")
}
fmt.Println(o.Style.ToLipgloss().Render(text))
return nil
}
// HideFlags hides the flags from the usage output. This is used in conjunction
// with BeforeReset hook.
func HideFlags(ctx *kong.Context) {
n := ctx.Selected()
if n == nil {
return
}
for _, f := range n.Flags {
if g := f.Group; g != nil && g.Key == groupName {
if !strings.HasSuffix(f.Name, ".foreground") {
f.Hidden = true
}
}
}
}

View file

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

View file

@ -1,13 +1,9 @@
package style
const (
groupName = "Style Flags"
)
// Options is the customization options for the style command.
type Options struct {
Text []string `arg:"" optional:"" help:"Text to which to apply the style"`
Style Styles `embed:""`
Text []string `arg:"" optional:"" help:"Text to which to apply the style"`
Style StylesNotHidden `embed:""`
}
// Styles is a flag set of possible styles.
@ -18,25 +14,55 @@ type Options struct {
// components, through embedding and prefixing.
type Styles struct {
// Colors
Background string `help:"Background Color" default:"${defaultBackground}" group:"Style Flags" env:"BACKGROUND"`
Foreground string `help:"Foreground Color" default:"${defaultForeground}" group:"Style Flags" env:"FOREGROUND"`
Background string `help:"Background Color" default:"${defaultBackground}" group:"Style Flags" env:"BACKGROUND" hidden:"true"`
// Border
Border string `help:"Border Style" enum:"none,hidden,normal,rounded,thick,double" default:"none" group:"Style Flags" env:"BORDER"`
BorderBackground string `help:"Border Background Color" group:"Style Flags" env:"BORDER_BACKGROUND"`
BorderForeground string `help:"Border Foreground Color" group:"Style Flags" env:"BORDER_FOREGROUND"`
Border string `help:"Border Style" enum:"none,hidden,normal,rounded,thick,double" default:"${defaultBorder}" group:"Style Flags" env:"BORDER" hidden:"true"`
BorderBackground string `help:"Border Background Color" group:"Style Flags" default:"${defaultBorderBackground}" env:"BORDER_BACKGROUND" hidden:"true"`
BorderForeground string `help:"Border Foreground Color" group:"Style Flags" default:"${defaultBorderForeground}" env:"BORDER_FOREGROUND" hidden:"true"`
// Layout
Align string `help:"Text Alignment" enum:"left,center,right,bottom,middle,top" default:"left" group:"Style Flags" env:"ALIGN"`
Height int `help:"Text height" group:"Style Flags" env:"HEIGHT"`
Width int `help:"Text width" group:"Style Flags" env:"WIDTH"`
Align string `help:"Text Alignment" enum:"left,center,right,bottom,middle,top" default:"${defaultAlign}" group:"Style Flags" env:"ALIGN" hidden:"true"`
Height int `help:"Text height" default:"${defaultHeight}" group:"Style Flags" env:"HEIGHT" hidden:"true"`
Width int `help:"Text width" default:"${defaultWidth}" group:"Style Flags" env:"WIDTH" hidden:"true"`
Margin string `help:"Text margin" default:"${defaultMargin}" group:"Style Flags" env:"MARGIN" hidden:"true"`
Padding string `help:"Text padding" default:"${defaultPadding}" group:"Style Flags" env:"PADDING" hidden:"true"`
// Format
Bold bool `help:"Bold text" default:"${defaultBold}" group:"Style Flags" env:"BOLD" hidden:"true"`
Faint bool `help:"Faint text" default:"${defaultFaint}" group:"Style Flags" env:"FAINT" hidden:"true"`
Italic bool `help:"Italicize text" default:"${defaultItalic}" group:"Style Flags" env:"ITALIC" hidden:"true"`
Strikethrough bool `help:"Strikethrough text" default:"${defaultStrikethrough}" group:"Style Flags" env:"STRIKETHROUGH" hidden:"true"`
Underline bool `help:"Underline text" default:"${defaultUnderline}" group:"Style Flags" env:"UNDERLINE" hidden:"true"`
}
// StylesNotHidden allows the style struct to display full help when not-embedded.
//
// NB: We must duplicate this struct to ensure that `gum style` does not hide
// flags when an error pops up. Ideally, we can dynamically hide or show flags
// based on the command run: https://github.com/alecthomas/kong/issues/316
type StylesNotHidden struct {
// Colors
Foreground string `help:"Foreground Color" default:"${defaultForeground}" group:"Style Flags" env:"FOREGROUND"`
Background string `help:"Background Color" default:"${defaultBackground}" group:"Style Flags" env:"BACKGROUND"`
// Border
Border string `help:"Border Style" enum:"none,hidden,normal,rounded,thick,double" default:"${defaultBorder}" group:"Style Flags" env:"BORDER"`
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"`
Padding string `help:"Text padding" default:"${defaultPadding}" group:"Style Flags" env:"PADDING"`
// Format
Bold bool `help:"Bold text" group:"Style Flags" env:"BOLD"`
Faint bool `help:"Faint text" group:"Style Flags" env:"FAINT"`
Italic bool `help:"Italicize text" group:"Style Flags" env:"ITALIC"`
Strikethrough bool `help:"Strikethrough text" group:"Style Flags" env:"STRIKETHROUGH"`
Bold bool `help:"Bold text" default:"${defaultBold}" group:"Style Flags" env:"BOLD"`
Faint bool `help:"Faint text" default:"${defaultFaint}" group:"Style Flags" env:"FAINT"`
Italic bool `help:"Italicize text" default:"${defaultItalic}" group:"Style Flags" env:"ITALIC"`
Strikethrough bool `help:"Strikethrough text" default:"${defaultStrikethrough}" group:"Style Flags" env:"STRIKETHROUGH"`
Underline bool `help:"Underline text" default:"${defaultUnderline}" group:"Style Flags" env:"UNDERLINE"`
}

5
table/comma.csv Normal file
View 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"
1 Bubble Gum Price Ingredients
2 Strawberry $0.88 Water,Sugar
3 Guava $1.00 Guava Flavoring,Food Coloring,Xanthan Gum
4 Orange $0.99 Sugar,Dextrose,Glucose
5 Cinnamon $0.50 Cin"na"mon

131
table/command.go Normal file
View 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
View 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
1 Bubble Gum Flavor Price
2 Strawberry $0.99
3 Cherry $0.50
4 Banana $0.75
5 Orange $0.25
6 Lemon $0.50
7 Lime $0.50
8 Grape $0.50
9 Watermelon $0.50
10 Pineapple $0.50
11 Blueberry $0.50
12 Raspberry $0.50
13 Cranberry $0.50
14 Peach $0.50
15 Apple $0.50
16 Mango $0.50
17 Pomegranate $0.50
18 Coconut $0.50
19 Cinnamon $0.50

19
table/invalid.csv Normal file
View 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
1 Bubble Gum Flavor
2 Strawberry,$0.99
3 Cherry,$0.50
4 Banana,$0.75
5 Orange
6 Lemon,$0.50
7 Lime,$0.50
8 Grape,$0.50
9 Watermelon,$0.50
10 Pineapple,$0.50
11 Blueberry,$0.50
12 Raspberry,$0.50
13 Cranberry,$0.50
14 Peach,$0.50
15 Apple,$0.50
16 Mango,$0.50
17 Pomegranate,$0.50
18 Coconut,$0.50
19 Cinnamon,$0.50

19
table/options.go Normal file
View 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
View 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
View 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
}

View file

@ -2,15 +2,10 @@ package write
import (
"fmt"
"os"
"strings"
"github.com/alecthomas/kong"
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/exit"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/style"
"github.com/charmbracelet/huh"
)
// Run provides a shell script interface for the text area bubble.
@ -18,51 +13,36 @@ import (
func (o Options) Run() error {
in, _ := stdin.Read()
if in != "" && o.Value == "" {
o.Value = in
o.Value = strings.ReplaceAll(in, "\r", "")
}
a := textarea.New()
a.Focus()
var value = o.Value
a.Prompt = o.Prompt
a.Placeholder = o.Placeholder
a.ShowLineNumbers = o.ShowLineNumbers
a.CharLimit = o.CharLimit
theme := huh.ThemeCharm()
theme.Focused.Base = o.BaseStyle.ToLipgloss()
theme.Focused.TextInput.Cursor = o.CursorStyle.ToLipgloss()
theme.Focused.Title = o.HeaderStyle.ToLipgloss()
theme.Focused.TextInput.Placeholder = o.PlaceholderStyle.ToLipgloss()
theme.Focused.TextInput.Prompt = o.PromptStyle.ToLipgloss()
style := textarea.Style{
Base: o.BaseStyle.ToLipgloss(),
Placeholder: o.PlaceholderStyle.ToLipgloss(),
CursorLine: o.CursorLineStyle.ToLipgloss(),
CursorLineNumber: o.CursorLineNumberStyle.ToLipgloss(),
EndOfBuffer: o.EndOfBufferStyle.ToLipgloss(),
LineNumber: o.LineNumberStyle.ToLipgloss(),
Prompt: o.PromptStyle.ToLipgloss(),
}
err := huh.NewForm(
huh.NewGroup(
huh.NewText().
Title(o.Header).
Placeholder(o.Placeholder).
CharLimit(o.CharLimit).
ShowLineNumbers(o.ShowLineNumbers).
Value(&value),
),
).
WithWidth(o.Width).
WithHeight(o.Height).
WithShowHelp(false).Run()
a.BlurredStyle = style
a.FocusedStyle = style
a.Cursor.Style = o.CursorStyle.ToLipgloss()
a.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 {
return fmt.Errorf("failed to run write: %w", err)
}
m := tm.(model)
if m.aborted {
return exit.ErrAborted
return err
}
fmt.Println(m.textarea.Value())
return nil
}
// BeforeReset hook. Used to unclutter style flags.
func (o Options) BeforeReset(ctx *kong.Context) error {
style.HideFlags(ctx)
fmt.Println(value)
return nil
}

View file

@ -4,21 +4,25 @@ import "github.com/charmbracelet/gum/style"
// Options are the customization options for the textarea.
type Options struct {
Width int `help:"Text area width" default:"50" env:"GUM_WRITE_WIDTH"`
Width int `help:"Text area width (0 for terminal width)" default:"50" env:"GUM_WRITE_WIDTH"`
Height int `help:"Text area height" default:"5" env:"GUM_WRITE_HEIGHT"`
Header string `help:"Header value" default:"" env:"GUM_WRITE_HEADER"`
Placeholder string `help:"Placeholder value" default:"Write something..." env:"GUM_WRITE_PLACEHOLDER"`
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"`
ShowLineNumbers bool `help:"Show line numbers" default:"false" env:"GUM_WRITE_SHOW_LINE_NUMBERS"`
Value string `help:"Initial value (can be passed via stdin)" default:"" env:"GUM_WRITE_VALUE"`
CharLimit int `help:"Maximum value length (0 for no limit)" default:"400"`
CursorMode string `prefix:"cursor." name:"mode" help:"Cursor mode" default:"blink" enum:"blink,hide,static" env:"GUM_WRITE_CURSOR_MODE"`
BaseStyle style.Styles `embed:"" prefix:"base." envprefix:"GUM_WRITE_BASE_"`
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_WRITE_CURSOR_"`
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_WRITE_HEADER_"`
PlaceholderStyle style.Styles `embed:"" prefix:"placeholder." set:"defaultForeground=240" envprefix:"GUM_WRITE_PLACEHOLDER_"`
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=7" envprefix:"GUM_WRITE_PROMPT_"`
BaseStyle style.Styles `embed:"" prefix:"base." envprefix:"GUM_WRITE_BASE_"`
CursorLineNumberStyle style.Styles `embed:"" prefix:"cursor-line-number." set:"defaultForeground=7" envprefix:"GUM_WRITE_CURSOR_LINE_NUMBER_"`
CursorLineStyle style.Styles `embed:"" prefix:"cursor-line." envprefix:"GUM_WRITE_CURSOR_LINE_"`
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_WRITE_CURSOR_"`
EndOfBufferStyle style.Styles `embed:"" prefix:"end-of-buffer." set:"defaultForeground=0" envprefix:"GUM_WRITE_END_OF_BUFFER_"`
LineNumberStyle style.Styles `embed:"" prefix:"line-number." set:"defaultForeground=7" envprefix:"GUM_WRITE_LINE_NUMBER_"`
PlaceholderStyle style.Styles `embed:"" prefix:"placeholder." set:"defaultForeground=240" envprefix:"GUM_WRITE_PLACEHOLDER_"`
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=7" envprefix:"GUM_WRITE_PROMPT_"`
CursorLineNumberStyle style.Styles `embed:"" prefix:"cursor-line-number." set:"defaultForeground=7" envprefix:"GUM_WRITE_CURSOR_LINE_NUMBER_"`
CursorLineStyle style.Styles `embed:"" prefix:"cursor-line." envprefix:"GUM_WRITE_CURSOR_LINE_"`
}

View file

@ -1,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
}