Compare commits

...

199 commits

Author SHA1 Message Date
rohan436
4409974788 docs: fix README wording 2026-03-14 11:09:23 -04:00
dependabot[bot]
06d72ec646
chore(deps): bump the all group with 3 updates (#1016)
Bumps the all group with 3 updates: [github.com/charmbracelet/bubbles](https://github.com/charmbracelet/bubbles), [github.com/charmbracelet/x/ansi](https://github.com/charmbracelet/x) and [golang.org/x/text](https://github.com/golang/text).


Updates `github.com/charmbracelet/bubbles` from 0.21.1 to 1.0.0
- [Release notes](https://github.com/charmbracelet/bubbles/releases)
- [Commits](https://github.com/charmbracelet/bubbles/compare/v0.21.1...v1.0.0)

Updates `github.com/charmbracelet/x/ansi` from 0.11.5 to 0.11.6
- [Commits](https://github.com/charmbracelet/x/compare/ansi/v0.11.5...ansi/v0.11.6)

Updates `golang.org/x/text` from 0.33.0 to 0.34.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.33.0...v0.34.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbles
  dependency-version: 1.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all
- dependency-name: github.com/charmbracelet/x/ansi
  dependency-version: 0.11.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: golang.org/x/text
  dependency-version: 0.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 11:00:03 +00:00
dependabot[bot]
bff0c8584e
chore(deps): bump the all group with 3 updates (#1015)
Bumps the all group with 3 updates: [github.com/alecthomas/kong](https://github.com/alecthomas/kong), [github.com/charmbracelet/bubbles](https://github.com/charmbracelet/bubbles) and [github.com/charmbracelet/x/ansi](https://github.com/charmbracelet/x).


Updates `github.com/alecthomas/kong` from 1.13.0 to 1.14.0
- [Commits](https://github.com/alecthomas/kong/compare/v1.13.0...v1.14.0)

Updates `github.com/charmbracelet/bubbles` from 0.21.0 to 0.21.1
- [Release notes](https://github.com/charmbracelet/bubbles/releases)
- [Commits](https://github.com/charmbracelet/bubbles/compare/v0.21.0...v0.21.1)

Updates `github.com/charmbracelet/x/ansi` from 0.11.4 to 0.11.5
- [Commits](https://github.com/charmbracelet/x/compare/ansi/v0.11.4...ansi/v0.11.5)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/kong
  dependency-version: 1.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: github.com/charmbracelet/bubbles
  dependency-version: 0.21.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: github.com/charmbracelet/x/ansi
  dependency-version: 0.11.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-09 11:15:27 +00:00
Andrey Nering
057099caf0 fix: write was not compiling 2026-01-21 12:01:27 -03:00
dependabot[bot]
dfe61991ce chore(deps): bump the all group with 3 updates
Bumps the all group with 3 updates: [github.com/alecthomas/kong](https://github.com/alecthomas/kong), [github.com/charmbracelet/x/editor](https://github.com/charmbracelet/x) and [golang.org/x/text](https://github.com/golang/text).


Updates `github.com/alecthomas/kong` from 1.12.1 to 1.13.0
- [Commits](https://github.com/alecthomas/kong/compare/v1.12.1...v1.13.0)

Updates `github.com/charmbracelet/x/editor` from 0.1.0 to 0.2.0
- [Commits](https://github.com/charmbracelet/x/compare/v0.1.0...ansi/v0.2.0)

Updates `golang.org/x/text` from 0.29.0 to 0.33.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.29.0...v0.33.0)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/kong
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: github.com/charmbracelet/x/editor
  dependency-version: 0.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: golang.org/x/text
  dependency-version: 0.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-21 12:01:27 -03:00
Ayman Bagabas
7fafddf384 chore: bump dependencies
Fixes: https://github.com/charmbracelet/gum/pull/1004
2026-01-21 09:40:34 -05:00
Andrey Nering
8502bbd808 chore: remove issue templates, inherit from .github repo
The base ones should be used from now on:
https://github.com/charmbracelet/.github/tree/main/.github/ISSUE_TEMPLATE
2026-01-21 11:25:02 -03:00
Andrey Nering
f3a3f53026 ci: fix build action 2026-01-21 11:25:02 -03:00
Jens Petersen
7871625c9d
readme: gum is packaged in Fedora for some time (#1007) 2026-01-21 10:52:02 -03:00
github-actions[bot]
0ad76ee88c
ci: sync golangci-lint config (#995)
Co-authored-by: caarlos0 <245435+caarlos0@users.noreply.github.com>
2025-12-06 21:17:45 -03:00
dependabot[bot]
4a5553eb21
chore(deps): bump the all group across 1 directory with 3 updates (#974)
Bumps the all group with 3 updates in the / directory: [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea), [github.com/charmbracelet/x/ansi](https://github.com/charmbracelet/x) and [github.com/charmbracelet/x/xpty](https://github.com/charmbracelet/x).


Updates `github.com/charmbracelet/bubbletea` from 1.3.7 to 1.3.10
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.3.7...v1.3.10)

Updates `github.com/charmbracelet/x/ansi` from 0.10.1 to 0.10.2
- [Commits](https://github.com/charmbracelet/x/compare/ansi/v0.10.1...ansi/v0.10.2)

Updates `github.com/charmbracelet/x/xpty` from 0.1.2 to 0.1.3
- [Commits](https://github.com/charmbracelet/x/compare/ansi/v0.1.2...ansi/v0.1.3)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbletea
  dependency-version: 1.3.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: github.com/charmbracelet/x/ansi
  dependency-version: 0.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: github.com/charmbracelet/x/xpty
  dependency-version: 0.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-05 17:41:02 +02:00
Charm
845f6b2ec8
ci: sync dependabot config (#973) 2025-10-05 17:30:53 +02:00
dependabot[bot]
0e98776744 chore(deps): bump actions/setup-go from 5 to 6 in the all group
Bumps the all group with 1 update: [actions/setup-go](https://github.com/actions/setup-go).


Updates `actions/setup-go` from 5 to 6
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-09 10:14:22 -04:00
dependabot[bot]
e7ded305bc
chore(deps): bump the all group with 2 updates (#962)
Bumps the all group with 2 updates: [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) and [golang.org/x/text](https://github.com/golang/text).


Updates `github.com/charmbracelet/bubbletea` from 1.3.6 to 1.3.7
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.3.6...v1.3.7)

Updates `golang.org/x/text` from 0.28.0 to 0.29.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbletea
  dependency-version: 1.3.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: golang.org/x/text
  dependency-version: 0.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 09:18:25 +00:00
Carlos Alexandro Becker
6045525ab9
feat: adding --padding to most commands (#960)
* feat(filter,choose): allow UI to be padded

* feat: --padding everywhere

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: unrelated lint issue

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: filter

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: use ordered.Clamp

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
Co-authored-by: Christian Rocha <christian@rocha.is>
2025-09-05 13:55:10 -03:00
Charm
09940da8c0
ci: sync dependabot config (#956) 2025-08-26 13:03:49 -03:00
dependabot[bot]
50fff7815a
chore(deps): bump actions/checkout from 4 to 5 (#949)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  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>
2025-08-18 13:00:28 +00:00
dependabot[bot]
9d610efaf9
chore(deps): bump golang.org/x/text from 0.27.0 to 0.28.0 (#948)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.27.0 to 0.28.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.27.0...v0.28.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.28.0
  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>
2025-08-11 13:28:39 +00:00
dependabot[bot]
886f5132a5
chore(deps): bump github.com/charmbracelet/x/ansi from 0.9.3 to 0.10.1 (#947)
Bumps [github.com/charmbracelet/x/ansi](https://github.com/charmbracelet/x) from 0.9.3 to 0.10.1.
- [Commits](https://github.com/charmbracelet/x/compare/ansi/v0.9.3...ansi/v0.10.1)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/x/ansi
  dependency-version: 0.10.1
  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>
2025-08-11 13:27:29 +00:00
github-actions[bot]
8c12c2a6a2
ci: sync golangci-lint config (#946)
Co-authored-by: caarlos0 <245435+caarlos0@users.noreply.github.com>
2025-08-11 14:21:37 +02:00
dependabot[bot]
7e0ca9c335
chore(deps): bump github.com/alecthomas/kong from 1.12.0 to 1.12.1 (#940)
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 1.12.0 to 1.12.1.
- [Commits](https://github.com/alecthomas/kong/compare/v1.12.0...v1.12.1)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/kong
  dependency-version: 1.12.1
  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>
2025-08-04 12:38:56 +00:00
dependabot[bot]
8537aa9de4
chore(deps): bump github.com/charmbracelet/bubbletea from 1.3.5 to 1.3.6 (#932)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 1.3.5 to 1.3.6.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.3.5...v1.3.6)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbletea
  dependency-version: 1.3.6
  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>
2025-07-14 12:27:30 +00:00
dependabot[bot]
269da20026
chore(deps): bump golang.org/x/text from 0.26.0 to 0.27.0 (#931)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.26.0 to 0.27.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.26.0...v0.27.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.27.0
  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>
2025-07-14 11:58:16 +00:00
dependabot[bot]
99397479f1
chore(deps): bump github.com/alecthomas/kong from 1.11.0 to 1.12.0 (#928)
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 1.11.0 to 1.12.0.
- [Commits](https://github.com/alecthomas/kong/compare/v1.11.0...v1.12.0)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/kong
  dependency-version: 1.12.0
  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>
2025-06-30 11:44:58 +00:00
dependabot[bot]
7a076dfed1
chore(deps): bump github.com/Masterminds/semver/v3 from 3.3.1 to 3.4.0 (#927)
Bumps [github.com/Masterminds/semver/v3](https://github.com/Masterminds/semver) from 3.3.1 to 3.4.0.
- [Release notes](https://github.com/Masterminds/semver/releases)
- [Changelog](https://github.com/Masterminds/semver/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Masterminds/semver/compare/v3.3.1...v3.4.0)

---
updated-dependencies:
- dependency-name: github.com/Masterminds/semver/v3
  dependency-version: 3.4.0
  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>
2025-06-30 11:41:27 +00:00
dependabot[bot]
a50a8033fa
chore(deps): bump github.com/charmbracelet/x/ansi from 0.9.2 to 0.9.3 (#925)
Bumps [github.com/charmbracelet/x/ansi](https://github.com/charmbracelet/x) from 0.9.2 to 0.9.3.
- [Commits](https://github.com/charmbracelet/x/compare/ansi/v0.9.2...ansi/v0.9.3)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/x/ansi
  dependency-version: 0.9.3
  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>
2025-06-23 11:28:43 +00:00
Sebastian Adamczyk
501402cbba
fix(choose): fix typo in environment variable GUM_CCHOOSE_TIMEOUT (#922)
Co-authored-by: sadamczyk <13919759+sadamczyk@users.noreply.github.com>
2025-06-14 10:38:25 -03:00
bashbunni
3c972b0873
docs: add contributing guidelines (#920) 2025-06-11 08:14:11 -03:00
Carlos Alexandro Becker
0107dffd27
fix(filter): text input width is too small (#919)
close #913
2025-06-10 15:42:57 -03:00
arithmeticmean
8081f74c4a
fix: logic to handle interrupt before timeout in error checking (#918)
- Reorder conditions to check ErrInterrupted before ErrProgramKilled
- Prevents Ctrl+C from being incorrectly treated as timeout
2025-06-10 14:26:28 -03:00
dependabot[bot]
181be44694
chore(deps): bump golang.org/x/text from 0.25.0 to 0.26.0 (#915)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.25.0 to 0.26.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.26.0
  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>
2025-06-09 09:50:07 +00:00
github-actions[bot]
f1e274c05f
ci: sync golangci-lint config (#911)
Co-authored-by: caarlos0 <245435+caarlos0@users.noreply.github.com>
2025-06-02 08:52:04 -03:00
Carlos Alexandro Becker
817c4bd446
fix: lint issues (#909)
* fix: some of the lint issues

* fix: staticcheck
2025-05-30 10:34:31 -03:00
Carlos Alexandro Becker
a539127432
ci: update lint jobs and settings 2025-05-30 10:04:39 -03:00
Carlos Alexandro Becker
3bc854e268
chore(deps): update x/net, x/sys, x/term
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2025-05-30 09:57:27 -03:00
dependabot[bot]
6f5dfd8117
chore(deps): bump github.com/charmbracelet/log from 0.4.1 to 0.4.2 (#906)
Bumps [github.com/charmbracelet/log](https://github.com/charmbracelet/log) from 0.4.1 to 0.4.2.
- [Release notes](https://github.com/charmbracelet/log/releases)
- [Commits](https://github.com/charmbracelet/log/compare/v0.4.1...v0.4.2)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/log
  dependency-version: 0.4.2
  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>
2025-05-19 09:58:35 +00:00
dependabot[bot]
bb3aaf35e7
chore(deps): bump github.com/alecthomas/kong from 1.10.0 to 1.11.0 (#905)
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 1.10.0 to 1.11.0.
- [Commits](https://github.com/alecthomas/kong/compare/v1.10.0...v1.11.0)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/kong
  dependency-version: 1.11.0
  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>
2025-05-19 09:57:48 +00:00
dependabot[bot]
dd906d9363
chore(deps): bump golang.org/x/text from 0.24.0 to 0.25.0 (#904)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.24.0 to 0.25.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.25.0
  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>
2025-05-12 10:14:41 +00:00
dependabot[bot]
02bc801b9f
chore(deps): bump golangci/golangci-lint-action from 7 to 8 (#902)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 7 to 8.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v7...v8)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: '8'
  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>
2025-05-05 10:12:15 +00:00
dependabot[bot]
78f18cb5da
chore(deps): bump github.com/charmbracelet/bubbletea from 1.3.4 to 1.3.5 (#901)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 1.3.4 to 1.3.5.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.3.4...v1.3.5)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbletea
  dependency-version: 1.3.5
  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>
2025-05-05 10:06:43 +00:00
dependabot[bot]
9904967fb0
chore(deps): bump github.com/charmbracelet/x/ansi from 0.8.0 to 0.9.2 (#900)
Bumps [github.com/charmbracelet/x/ansi](https://github.com/charmbracelet/x) from 0.8.0 to 0.9.2.
- [Commits](https://github.com/charmbracelet/x/compare/ansi/v0.8.0...ansi/v0.9.2)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/x/ansi
  dependency-version: 0.9.2
  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>
2025-05-05 10:02:08 +00:00
haya14busa
39290a03b4
docs: use $EDITOR instead of EDITOR (#897) 2025-04-30 08:43:13 -03:00
Charm
6682e2079b
ci: sync dependabot config (#896) 2025-04-28 08:44:21 -03:00
dependabot[bot]
1baa8286e9
chore(deps): bump github.com/charmbracelet/glamour from 0.9.1 to 0.10.0 (#893)
Bumps [github.com/charmbracelet/glamour](https://github.com/charmbracelet/glamour) from 0.9.1 to 0.10.0.
- [Release notes](https://github.com/charmbracelet/glamour/releases)
- [Changelog](https://github.com/charmbracelet/glamour/blob/master/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/glamour/compare/v0.9.1...v0.10.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/glamour
  dependency-version: 0.10.0
  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>
2025-04-21 10:56:43 +00:00
dependabot[bot]
b7df657549
chore(deps): bump github.com/charmbracelet/bubbles from 0.20.0 to 0.21.0 (#890)
Bumps [github.com/charmbracelet/bubbles](https://github.com/charmbracelet/bubbles) from 0.20.0 to 0.21.0.
- [Release notes](https://github.com/charmbracelet/bubbles/releases)
- [Changelog](https://github.com/charmbracelet/bubbles/blob/master/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbles/compare/v0.20.0...v0.21.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbles
  dependency-version: 0.21.0
  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>
2025-04-14 09:36:19 +00:00
Miguel Ibars
e292bbf049
fix: detect timeout error and apply default option (#888) 2025-04-11 10:46:49 -03:00
dependabot[bot]
e43b277240
chore(deps): bump golang.org/x/text from 0.23.0 to 0.24.0 (#884)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.23.0 to 0.24.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.23.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.24.0
  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>
2025-04-07 09:45:36 +00:00
dependabot[bot]
cae32e76b1
chore(deps): bump github.com/alecthomas/kong from 1.9.0 to 1.10.0 (#885)
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 1.9.0 to 1.10.0.
- [Commits](https://github.com/alecthomas/kong/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/kong
  dependency-version: 1.10.0
  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>
2025-04-07 09:45:12 +00:00
dependabot[bot]
33b4e03e66
chore(deps): bump golangci/golangci-lint-action from 6 to 7 (#882)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6 to 7.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v6...v7)

---
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>
2025-03-31 09:49:29 +00:00
Carlos Alexandro Becker
4fea9a037a
fix: make empty line before help consistent 2025-03-25 14:00:20 -03:00
dependabot[bot]
699ac86e9a
chore(deps): bump github.com/charmbracelet/glamour from 0.9.0 to 0.9.1 (#878)
Bumps [github.com/charmbracelet/glamour](https://github.com/charmbracelet/glamour) from 0.9.0 to 0.9.1.
- [Release notes](https://github.com/charmbracelet/glamour/releases)
- [Changelog](https://github.com/charmbracelet/glamour/blob/master/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/glamour/compare/v0.9.0...v0.9.1)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/glamour
  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>
2025-03-24 10:16:06 +00:00
dependabot[bot]
93c6a4bf1f
chore(deps): bump github.com/charmbracelet/log from 0.4.0 to 0.4.1 (#874)
Bumps [github.com/charmbracelet/log](https://github.com/charmbracelet/log) from 0.4.0 to 0.4.1.
- [Release notes](https://github.com/charmbracelet/log/releases)
- [Commits](https://github.com/charmbracelet/log/compare/v0.4.0...v0.4.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>
2025-03-17 10:19:46 +00:00
dependabot[bot]
7c833f1b75
chore(deps): bump github.com/charmbracelet/glamour from 0.8.0 to 0.9.0 (#873)
Bumps [github.com/charmbracelet/glamour](https://github.com/charmbracelet/glamour) from 0.8.0 to 0.9.0.
- [Release notes](https://github.com/charmbracelet/glamour/releases)
- [Changelog](https://github.com/charmbracelet/glamour/blob/master/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/glamour/compare/v0.8.0...v0.9.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>
2025-03-17 10:15:37 +00:00
dependabot[bot]
a8b94c1c83
chore(deps): bump github.com/alecthomas/kong from 1.8.1 to 1.9.0 (#872)
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 1.8.1 to 1.9.0.
- [Release notes](https://github.com/alecthomas/kong/releases)
- [Commits](https://github.com/alecthomas/kong/compare/v1.8.1...v1.9.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-17 10:14:18 +00:00
dependabot[bot]
0d116b8068
chore(deps): bump golang.org/x/text from 0.22.0 to 0.23.0 (#868)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.22.0 to 0.23.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  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>
2025-03-10 09:19:20 +00:00
Carlos Alexandro Becker
204d21940e
fix(choose): order when using --label-delimiter (#867)
closes #829
2025-03-05 23:57:08 -03:00
dependabot[bot]
dbac6a83f3
chore(deps): bump github.com/charmbracelet/bubbletea from 1.3.3 to 1.3.4 (#866)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 1.3.3 to 1.3.4.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.3.3...v1.3.4)

---
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 10:29:58 +00:00
dependabot[bot]
db86a909bb
chore(deps): bump github.com/muesli/termenv (#865)
Bumps [github.com/muesli/termenv](https://github.com/muesli/termenv) from 0.15.3-0.20241211131612-0d230cb6eb15 to 0.16.0.
- [Release notes](https://github.com/muesli/termenv/releases)
- [Commits](https://github.com/muesli/termenv/commits/v0.16.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 11:05:37 +00:00
Abel Chalier
85a29801d8
fix: wildcard escaping issue (#862) 2025-02-21 14:04:30 -03:00
dependabot[bot]
9f503f5335
chore(deps): bump github.com/alecthomas/kong from 1.8.0 to 1.8.1 (#861)
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/alecthomas/kong/releases)
- [Commits](https://github.com/alecthomas/kong/compare/v1.8.0...v1.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>
2025-02-17 11:21:22 +00:00
ctn-malone
308d86be7d
chore(nix): update version and vendor hash (#859) 2025-02-14 21:08:37 -03:00
Charm
a3975b78ee
ci: sync dependabot config (#856) 2025-02-13 11:31:46 -03:00
Charm
25c40f5eca
ci: sync dependabot config (#855) 2025-02-13 11:07:04 -03:00
dependabot[bot]
24fa527d08
chore(deps): bump github.com/charmbracelet/bubbletea from 1.3.2 to 1.3.3 (#854)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 1.3.2 to 1.3.3.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.3.2...v1.3.3)

---
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-12 04:44:46 +00:00
Andrey Nering
2b72e80297
fix(confirm): ensure --show-output show the right answer (#853) 2025-02-11 18:00:03 -03:00
Andrey Nering
93f6857e3d
fix(spin): preserve color output when --show-output is given (#850) 2025-02-11 16:51:38 -03:00
dependabot[bot]
a30c8bdf31
chore(deps): bump github.com/charmbracelet/bubbletea from 1.3.0 to 1.3.2 (#851)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 1.3.0 to 1.3.2.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.3.0...v1.3.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-11 04:22:17 +00:00
dependabot[bot]
7b9d51d462
chore(deps): bump github.com/alecthomas/kong from 1.7.0 to 1.8.0 (#849)
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/alecthomas/kong/releases)
- [Commits](https://github.com/alecthomas/kong/compare/v1.7.0...v1.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 04:43:23 +00:00
dependabot[bot]
9a387e4079
chore(deps): bump golang.org/x/text from 0.21.0 to 0.22.0 (#842)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.21.0 to 0.22.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.21.0...v0.22.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  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>
2025-02-05 04:40:26 +00:00
Carlos Alexandro Becker
d795b8ab2f
fix(table): padding on item indicator (#841) 2025-02-04 16:38:09 -03:00
Carlos Alexandro Becker
ce6bb49ce0
chore(deps): use latest stable bubbletea
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2025-02-04 16:10:46 -03:00
Raphael Amorim
5660060c40
rename to count instead (#840) 2025-02-04 12:35:00 -03:00
Raphael Amorim
984c84fbd0
feat: show indicator on help keybindings (opt-in) (#839)
* feat: show indicator on help keybindings (opt-in)

* format code
2025-02-04 11:54:06 -03:00
Raphael Amorim
bb098b2662
fix: generated completion invalid for fish shell (choose/options) (#838) 2025-02-03 14:55:05 -03:00
Raphael Amorim
9705aa3384
fix: generated completion invalid for fish shell (#837) 2025-02-03 14:43:46 -03:00
BitBoss
bb9fee7331
docs: update FreeBSD installation instructions. (#824)
Co-authored-by: Dave Turner <dave@clearpathdigital.com>
2025-01-31 14:37:22 -03:00
dependabot[bot]
928ba9ace0
chore(deps): bump github.com/alecthomas/kong from 1.6.1 to 1.7.0 (#832)
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 1.6.1 to 1.7.0.
- [Release notes](https://github.com/alecthomas/kong/releases)
- [Commits](https://github.com/alecthomas/kong/compare/v1.6.1...v1.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-30 04:51:54 +00:00
Carlos Alexandro Becker
d1fc05155c
fix(pager): memory/cpu usage when using soft-wrap (#827)
easily reproducible, especially with something like

```sh
PAGER="gum pager" man ls
```

Problems here were:
- reflow's truncate is not very efficient
- useless calls to truncate
- bad strings.Replace calls (not necessary, and wouldn't work if text has ansi sequences)
2025-01-28 11:50:36 -03:00
dependabot[bot]
e7c916cff6
chore(deps): bump github.com/charmbracelet/x/ansi from 0.7.0 to 0.8.0 (#826)
Bumps [github.com/charmbracelet/x/ansi](https://github.com/charmbracelet/x) from 0.7.0 to 0.8.0.
- [Release notes](https://github.com/charmbracelet/x/releases)
- [Commits](https://github.com/charmbracelet/x/compare/ansi/v0.7.0...ansi/v0.8.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/x/ansi
  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>
2025-01-28 04:28:08 +00:00
Carlos Alexandro Becker
37456557c4
fix(filter): wrong highlight when option has grapheme clusters (#799) 2025-01-22 12:51:18 -03:00
Carlos Alexandro Becker
c11af42c1b
fix(write): ctrl+j not making new line (#819) 2025-01-22 12:51:04 -03:00
Carlos Alexandro Becker
30bc180679
fix(confirm): do not print 'not confirmed' on exit 1 (#814)
closes #809
2025-01-22 11:19:10 -03:00
Carlos Alexandro Becker
2846d19b70
Revert "feat(table): set --print if stdout is not a terminal (#762)" (#811)
This reverts commit 05614c8196.
2025-01-22 11:18:53 -03:00
Carlos Alexandro Becker
2da952756f
fix(spin): clear title after finished (#815)
closes #802

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2025-01-22 11:17:56 -03:00
Carlos Alexandro Becker
05c4bb9868
fix: spin when not a tty (#813)
* fix: spin when not a tty

* fix: typo
2025-01-21 19:37:50 -03:00
Carlos Alexandro Becker
7e3216e2c8
fix(viewport): remove extra line in viewport help (#816) 2025-01-21 19:33:52 -03:00
ctn-malone
65516a664c
chore(nix): update src hash and nix version (#810) 2025-01-21 09:34:38 -03:00
dependabot[bot]
3dcc8b3f37
chore(deps): bump github.com/charmbracelet/x/ansi (#801)
Bumps [github.com/charmbracelet/x/ansi](https://github.com/charmbracelet/x) from 0.6.1-0.20250107110353-48b574af22a5 to 0.7.0.
- [Release notes](https://github.com/charmbracelet/x/releases)
- [Commits](https://github.com/charmbracelet/x/commits/ansi/v0.7.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/x/ansi
  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>
2025-01-15 05:06:10 +00:00
dependabot[bot]
c93da09e13
chore(deps): bump golang.org/x/net from 0.27.0 to 0.33.0 (#798)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.27.0 to 0.33.0.
- [Commits](https://github.com/golang/net/compare/v0.27.0...v0.33.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>
2025-01-13 19:31:05 +00:00
Christian Rocha
89d495292b chore(file): remove extra newline above help 2025-01-13 13:58:37 -05:00
Carlos Alexandro Becker
bb34c45fe1
fix(filter): take pre-filtering into account 2025-01-12 17:42:52 -03:00
Carlos Alexandro Becker
b0ec3a7915
fix(filter): select all
refs https://github.com/charmbracelet/gum/pull/777#issuecomment-2585836624

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2025-01-12 17:40:44 -03:00
Carlos Alexandro Becker
da325ae345
fix(filter): wide chars
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2025-01-07 11:28:20 -03:00
Carlos Alexandro Becker
d3d20efc70
fix(filter): properly handle options with ansi styles (#789)
* fix(filter): handle styles option matches

* perf: use ranges

* fix: cut

* fix: ansi update
2025-01-07 11:08:24 -03:00
dependabot[bot]
cd151b51bf
chore(deps): bump github.com/alecthomas/kong from 1.6.0 to 1.6.1 (#790)
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/alecthomas/kong/releases)
- [Commits](https://github.com/alecthomas/kong/compare/v1.6.0...v1.6.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>
2025-01-07 05:00:57 +00:00
Carlos Alexandro Becker
0f8f67f96e
feat(choose): select from stdin (#773)
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-12-17 14:43:24 -03:00
Carlos Alexandro Becker
0b89ff82d4
feat: yes|gum confirm (#772)
* feat: yes|gum confirm

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: rebase on main

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-12-17 14:17:43 -03:00
Carlos Alexandro Becker
4cedf9fca0
feat: --no-strip-ansi (#784)
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
Co-authored-by: Sridaran Thoniyil <sri7thon@gmail.com>
2024-12-17 13:56:19 -03:00
Carlos Alexandro Becker
2e321f57e2
feat(choose): label delimiters (#783)
I'm not sure if I like this impl, but it does work.

closes #406
2024-12-17 09:47:31 -03:00
Carlos Alexandro Becker
6d405c49b1
feat(choose,filter): --input-delimiter --output-delimiter (#779)
* feat(choose,filter): --input-delimiter --output-delimiter

allows to change how content from stdin is used, and how results are
printed.

one could get around it piping into and from `tr`, but results aren't
quite right, especially when `tr '\n' ','` for example, as it'll add an
extra `,` in the end of the string.

This makes things a bit cleaner, hopefully.

closes #274

* fix: use new tty pkg
2024-12-13 17:03:42 -03:00
Carlos Alexandro Becker
0e501ea47f
feat(filter): --select-if-one returns if single match (#778)
closes #311

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-12-13 16:59:59 -03:00
Carlos Alexandro Becker
966237b378
feat(filter): allow to pre-select items with --selected (#777)
closes #593
2024-12-13 16:59:44 -03:00
Carlos Alexandro Becker
f230a3d5fc
feat(filter): allow to focus out of filter (#776)
- esc focus out of the filter
- esc with filter blurred quits
- g/G/j/k navigation when filter is blurred

closes #201
2024-12-13 16:59:32 -03:00
Carlos Alexandro Becker
64d69eb59b
feat(version): adds command to check current gum version (#775)
* feat(version): adds command to check current gum version

closes #352

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* Update version/command.go

Co-authored-by: Gareth Jones <Jones258@Gmail.com>

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
Co-authored-by: Gareth Jones <Jones258@Gmail.com>
2024-12-13 16:59:14 -03:00
Carlos Alexandro Becker
32786f7764
feat(spin): --show-stdout --show-stderr (#774)
closes #362
2024-12-13 14:34:10 -03:00
vahnrr
d1bfd569ca
feat(confirm): add --show-output (#427)
* feat(confirm): add `--show-output`

* feat(confirm): add `--show-output` model

---------

Co-authored-by: vahnrr <vahnrr@pm.me>
2024-12-12 16:02:19 -03:00
Carlos Alexandro Becker
74c1079c9d
feat(filter): ctrl+a to toggle select all (#770)
closes #388
2024-12-12 10:12:28 -03:00
Carlos Alexandro Becker
f921ebd07f
feat(file): add --header (#768)
* feat(file): add --header

closes #497

* fix: show help
2024-12-11 23:24:25 -03:00
Carlos Alexandro Becker
55120aead6
feat(choose): --selected="*" to select all (#769)
close #390

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-12-11 23:23:58 -03:00
Carlos Alexandro Becker
774667a943
feat(choose,confirm,file,filter,input,table,write): esc exit 1, ctrl+c exit 130, help arrow order (#771)
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-12-11 23:23:33 -03:00
Carlos Alexandro Becker
05614c8196
feat(table): set --print if stdout is not a terminal (#762)
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-12-11 14:22:26 -03:00
Carlos Alexandro Becker
cf2da6406c
fix(spin): if not a tty, only print title, do not open tty for stdin (#763)
closes #328
2024-12-11 14:17:03 -03:00
Carlos Alexandro Becker
2e53efc0ec
feat(style): trim line spaces (#767)
Lipgloss applies aligning on the string as given to it, and ascii art
usually contain left whitespaces to align things.

This adds an option to trim space on all lines of the input, before
giving it to lipgloss, so aligning use only non-whitespace content.
2024-12-11 14:00:55 -03:00
Carlos Alexandro Becker
6a37a14819
fix: update termenv to detect xterm, rio
closes #391

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-12-11 10:19:25 -03:00
dependabot[bot]
b45ae0e049
chore(deps): bump golang.org/x/text from 0.18.0 to 0.21.0 (#764)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.18.0 to 0.21.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.18.0...v0.21.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  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-12-11 04:44:50 +00:00
dependabot[bot]
6f5b0d2d67
chore(deps): bump github.com/charmbracelet/x/ansi from 0.5.2 to 0.6.0 (#765)
Bumps [github.com/charmbracelet/x/ansi](https://github.com/charmbracelet/x) from 0.5.2 to 0.6.0.
- [Release notes](https://github.com/charmbracelet/x/releases)
- [Commits](https://github.com/charmbracelet/x/compare/ansi/v0.5.2...ansi/v0.6.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/x/ansi
  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-12-11 04:44:38 +00:00
Carlos Alexandro Becker
b0c9c58302
fix(stdin): trim space instead of \n (#761)
we were trimming an ending \n, but it would then break a \r\n sequence, causing misrenders.

this fixes it

closes #682

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-12-10 21:27:13 -03:00
Carlos Alexandro Becker
8d611cb7df
fix(table): grow table rows based on --columns (#760)
* fix(table): grow table rows based on --columns

closes #411

* fix: merge
2024-12-10 21:26:49 -03:00
Carlos Alexandro Becker
a8cce1cad9
feat(table): --lazy-quotes and --fields-per-record (#759)
As per [csv.Reader].

closes #345
2024-12-10 21:18:13 -03:00
Carlos Alexandro Becker
bf06fce1c9
fix(table): set widths (#758)
I'm not sure about this, as it might be slow in big tables...

This will go through all rows, calculate their widths, and set it as the
column width if none was provided with `-w`.

```console
$ echo -e "a,b\naaaaaaaaaaaaaaaaaaaaaaa,bbbbbbbbbbbbbbbb" | gum table
 a  b
 …  …

$ echo -e "a,b\naaaaaaaaaaaaaaaaaaaaaaa,bbbbbbbbbbbbbbbb" | gum table
 a                        b
 aaaaaaaaaaaaaaaaaaaaaaa  bbbbbbbbbbbbbbbb

$ echo -e "a,b\naaaaaaaaaaaaaaaaaaaaaaa,bbbbbbbbbbbbbbbb" | gum table -w 5,5
 a      b
 aaaa…  bbbb…

```

closes #285
2024-12-10 21:17:53 -03:00
Carlos Alexandro Becker
2b090e8cb5
feat(table): add help (#756)
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-12-10 14:45:32 -03:00
Carlos Alexandro Becker
cc71f600f2
fix(table): ignore BOM (#757)
* fix(table): ignore BOM

closes #520

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: lint issue

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: simplify

* fix: better error

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-12-10 14:45:18 -03:00
Carlos Alexandro Becker
b58aad189a
docs(format): add table example
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-12-10 12:10:42 -03:00
Carlos Alexandro Becker
afb6111258
fix(spinner): set stdin and stderr
fixes #266

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-12-10 11:48:47 -03:00
Carlos Alexandro Becker
c25be3c8c3
feat(pager): make --soft-wrap the default
closes #744
2024-12-09 22:16:58 -03:00
Carlos Alexandro Becker
01892a027f
fix(write): max height, max chars (#753) 2024-12-09 20:53:54 -03:00
Carlos Alexandro Becker
2939e516cc
fix(pager): do not strip ansi sequences (#754) 2024-12-09 17:54:17 -03:00
Carlos Alexandro Becker
2e2b020541
fix(pager): use help bubble (#748) 2024-12-09 15:05:12 -03:00
Carlos Alexandro Becker
fb543c3294
fix: strip ansi sequences from stdin (#739)
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-12-09 14:44:43 -03:00
Carlos Alexandro Becker
71d7e6539c
feat: handle focus/blur events (#749)
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-12-09 14:41:29 -03:00
Carlos Alexandro Becker
4f469522d5
feat: handle interrupts and timeouts (#747) 2024-12-09 14:30:35 -03:00
Carlos Alexandro Becker
e30fc5ecdf
refactor: removing huh as a dep (#742)
* Revert "feat: huh gum write (#525)"

This reverts commit 4d5d53169e.

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* Revert "Use Huh for Gum Confirm (#522)"

This reverts commit f7572e387e.

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* revert: Use Huh for Gum Choose (#521)

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* revert: feat: huh for gum input (#524)

* revert: feat: huh file picker (#523)

* feat: remove huh

* fix: timeouts

* fix: lint issues

* fix(choose): quit on ctrl+q

ported over 63a3e8c8ce

* fix: ctrl+a to reverse selection

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: better handle spin exit codes

* fix(file): bind --[no-]permissions and --[no-]size

* feat(confirm): show help

* fix(confirm): fix help style

* fix(file): help

* fix(input): --no-show-help doesn't work

* fix(input): help

* fix(file): keymap improvement

* fix(write): focus

* feat(write): ctrl+e, keymaps, help

* feat(choose): help

* feat(filter): help

* refactor: keymaps

* fix(choose): only show 'toggle all' if there's no limit

* fix(choose): don't show toggle if the choices are limited to 1

* fix(filter): match choose header color

* fix(filter): add space above help

* fix(filter): factor help into the height setting

* chore(choose,filter): use verb for navigation label in help

* fix(filter): hide toggle help if limit is 1

* fix(file): factor help into height setting (#746)

* fix: lint issues

* fix(file): handle ctrl+c

* fix: remove full help

* fix: lint

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
Co-authored-by: Christian Rocha <christian@rocha.is>
2024-12-09 13:18:35 -03:00
dependabot[bot]
d74e9ea531
chore(deps): bump github.com/alecthomas/kong from 1.5.1 to 1.6.0 (#750)
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 1.5.1 to 1.6.0.
- [Commits](https://github.com/alecthomas/kong/compare/v1.5.1...v1.6.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 04:16:12 +00:00
Olaf Alders
a5fb6b1798
docs: quote tmux session name in code sample (#745)
A tmux session name could contain a space. Quote the variable to avoid
the session name being split on spaces.
2024-12-06 15:13:48 -03:00
Carlos Alexandro Becker
29250f8feb
fix(filter): --no-strict not working, also weird behavior (#737)
- `--no-strict` was erroring as non-existent option
- when `--no-strict`, we should actually create a "fake" option with
  whatever the user is typing

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-12-04 10:34:22 -03:00
Carlos Alexandro Becker
71af32ce16
fix(spin): properly redirect output
closes #690
2024-12-04 10:32:45 -03:00
Carlos Alexandro Becker
c422e76fe3
fix: clarify filter --sort flag (#738)
* fix: clarify filter --sort flag

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: if sort, sort options alphabetically first

* Revert "fix: if sort, sort options alphabetically first"

This reverts commit 86e8fc0a5b.

* fix: filter

* Update filter/options.go

Co-authored-by: Christian Rocha <christian@rocha.is>

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
Co-authored-by: Christian Rocha <christian@rocha.is>
2024-12-04 09:02:06 -03:00
Carlos Alexandro Becker
63a3e8c8ce
fix(filter): abort on ctrl+q (#721)
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-12-03 11:40:24 -03:00
ctn-malone
cb61fe6c84
chore(nix): update src hash (#733) 2024-12-03 11:39:51 -03:00
dependabot[bot]
90919986f2
chore(deps): bump github.com/alecthomas/kong from 1.5.0 to 1.5.1 (#736)
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 1.5.0 to 1.5.1.
- [Commits](https://github.com/alecthomas/kong/compare/v1.5.0...v1.5.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>
2024-12-03 04:28:34 +00:00
dependabot[bot]
fef37dae1f
chore(deps): bump github.com/alecthomas/kong from 1.4.0 to 1.5.0 (#735)
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 1.4.0 to 1.5.0.
- [Commits](https://github.com/alecthomas/kong/compare/v1.4.0...v1.5.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-02 04:16:50 +00:00
Carlos Alexandro Becker
c3e836f0dd
fix(spin): interrupt child process on ctrl+c (#732)
* fix(spin): interrupt child process on ctrl+c

This will send a SIGINT to the child process when ctrl+c is pressed.

closes #730

* fix: lint
2024-11-27 14:58:21 -03:00
Carlos Alexandro Becker
01f36b58a4
chore(deps): use huh main
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-11-26 12:43:44 -03:00
Carlos Alexandro Becker
fb11344ea4
fix(choose): --ordered (#722)
closes #687

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-11-26 12:42:54 -03:00
Pranav RK
620e645845
feat: allow cursor option in file (#667) 2024-11-26 12:42:27 -03:00
dependabot[bot]
e3ba400d6d
chore(deps): bump github.com/charmbracelet/bubbletea from 1.2.3 to 1.2.4 (#731)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.2.3...v1.2.4)

---
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-26 04:38:37 +00:00
dependabot[bot]
1156c33ad5
chore(deps): bump github.com/charmbracelet/x/ansi from 0.5.0 to 0.5.2 (#729)
Bumps [github.com/charmbracelet/x/ansi](https://github.com/charmbracelet/x) from 0.5.0 to 0.5.2.
- [Release notes](https://github.com/charmbracelet/x/releases)
- [Commits](https://github.com/charmbracelet/x/compare/ansi/v0.5.0...ansi/v0.5.2)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/x/ansi
  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-11-22 04:19:11 +00:00
dependabot[bot]
8ed2ca5e0d
chore(deps): bump github.com/charmbracelet/x/ansi from 0.4.5 to 0.5.0 (#728)
Bumps [github.com/charmbracelet/x/ansi](https://github.com/charmbracelet/x) from 0.4.5 to 0.5.0.
- [Release notes](https://github.com/charmbracelet/x/releases)
- [Commits](https://github.com/charmbracelet/x/compare/ansi/v0.4.5...ansi/v0.5.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/x/ansi
  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-11-21 04:56:03 +00:00
dependabot[bot]
06bdaba08f
chore(deps): bump github.com/charmbracelet/bubbletea from 1.2.2 to 1.2.3 (#726)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 1.2.2 to 1.2.3.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.2.2...v1.2.3)

---
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-20 04:37:21 +00:00
Carlos Alexandro Becker
098d09a270
fix(choose,confirm,file,filter,input,pager,spin): timeout default unit (#724)
Change it `0s` instead of `0`.

closes #402
2024-11-18 17:10:06 -03:00
Carlos Alexandro Becker
1ffe8b7e70
feat(log): support setting minimum log level with GUM_LOG_LEVEL (#723)
closes #490
2024-11-18 14:02:55 -03:00
Carlos Alexandro Becker
c868aa1c6c
fix(confirm,choose,file,input): timeout handling (#718)
* fix(confirm,choose,file,input): timeout handling

- some fields were not actually using the `--timeout` value
- some fields had different behavior when a timeout did occur. On this
  matter, it seems to me the best way forward is to specifically say it
  timed out, and after how long
- added exit status 124 (copied from `timeout` from coreutils) (fixes #684)

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* Update main.go

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

* Update internal/exit/exit.go

Co-authored-by: ccoVeille <3875889+ccoVeille@users.noreply.github.com>

* fix: improve

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: stderr

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
Co-authored-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Co-authored-by: ccoVeille <3875889+ccoVeille@users.noreply.github.com>
2024-11-18 10:49:15 -03:00
Carlos Alexandro Becker
3cec9b7b9a
fix(filter): panic if no matches
closes #715

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-11-18 10:00:10 -03:00
Carlos Alexandro Becker
20a381a10a
Merge remote-tracking branch 'origin/main' 2024-11-18 09:09:46 -03:00
Carlos Alexandro Becker
60a4b3bf93
feat(filepicker): show permissions and size (#717)
closes #495
2024-11-18 09:07:48 -03:00
Carlos Alexandro Becker
6ad8882990
fix(table): only set height if > 0 (#716)
closes #685
closes #660
2024-11-18 09:07:19 -03:00
Ayman Bagabas
7bdd189616
fix(spin): indenting lines when command is piped (#636)
We don't need to check for isatty, always store the output to the
designated buffer.

Fixes: https://github.com/charmbracelet/gum/issues/607
2024-11-15 15:53:27 -03:00
Carlos Alexandro Becker
f8c466bf3b
fix(confirm): improve timeout handling
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-11-15 15:37:24 -03:00
Ayman Bagabas
6a05246f53
fix: catch huh timeout error (#632)
Huh returns a `ErrTimeout` on timeout error and we need to catch that.

Fixes: https://github.com/charmbracelet/gum/pull/618
2024-11-15 15:37:16 -03:00
Daniel Pritchett ⚡
19e79b11e4
fix(confirm): --timeout was being ignored (#697)
* fix(confirm) Options.Timeout was ignored, now works as documented

* Streamlines command.go per PR feedback
2024-11-15 14:57:27 -03:00
Dieter Eickstaedt
93da22d656
feat(table): adds --return-column (#415)
* feat: Adding Return Column to table command

Consider a table of
ID Name Description
1  Task1 Task description

It would be good to select the row but retrieve the ID for example for further processing

like 'task 1 delete' when first row was selected

* feat: Return Column boundary check fixed

* Update table/options.go

---------

Co-authored-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-11-15 13:50:37 -03:00
dependabot[bot]
c3ce2f97bf
chore(deps): bump github.com/charmbracelet/bubbletea from 1.2.1 to 1.2.2 (#712)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 1.2.1 to 1.2.2.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.2.1...v1.2.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-13 04:44:49 +00:00
dependabot[bot]
03d3bcf178
chore(deps): bump github.com/alecthomas/kong from 1.2.1 to 1.4.0 (#705)
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 1.2.1 to 1.4.0.
- [Commits](https://github.com/alecthomas/kong/compare/v1.2.1...v1.4.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 04:57:18 +00:00
dependabot[bot]
8a007012be
chore(deps): bump github.com/charmbracelet/x/term from 0.2.0 to 0.2.1 (#711)
Bumps [github.com/charmbracelet/x/term](https://github.com/charmbracelet/x) from 0.2.0 to 0.2.1.
- [Release notes](https://github.com/charmbracelet/x/releases)
- [Commits](https://github.com/charmbracelet/x/compare/ansi/v0.2.0...ansi/v0.2.1)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/x/term
  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-11-11 04:55:41 +00:00
dependabot[bot]
790be1247a
chore(deps): bump github.com/charmbracelet/bubbletea from 1.1.2 to 1.2.1 (#710)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 1.1.2 to 1.2.1.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.1.2...v1.2.1)

---
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>
2024-11-11 04:53:45 +00:00
Carlos Alexandro Becker
b8f99b6053
ci: update 2024-11-07 13:28:31 -03:00
Carlos Alexandro Becker
ac3f44a9db
fix(deps): update goldmark-emoji (#686)
refs #331

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-11-07 13:19:56 -03:00
dependabot[bot]
1023911ea2 chore(deps): bump github.com/charmbracelet/x/ansi from 0.4.0 to 0.4.2
Bumps [github.com/charmbracelet/x/ansi](https://github.com/charmbracelet/x) from 0.4.0 to 0.4.2.
- [Release notes](https://github.com/charmbracelet/x/releases)
- [Commits](https://github.com/charmbracelet/x/compare/ansi/v0.4.0...ansi/v0.4.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-30 11:22:05 -04:00
dependabot[bot]
4323b2e750
chore(deps): bump github.com/charmbracelet/lipgloss (#692) 2024-10-28 12:08:17 +00:00
dependabot[bot]
b5b20c5031
chore(deps): bump github.com/charmbracelet/bubbletea from 1.1.1 to 1.1.2 (#694) 2024-10-28 11:57:16 +00:00
Carlos Alexandro Becker
8c7abe7335
fix: lint issues
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-09-17 09:40:12 -03:00
dependabot[bot]
e06d2e69a6
chore(deps): bump github.com/charmbracelet/x/ansi from 0.3.0 to 0.3.2 (#679)
Bumps [github.com/charmbracelet/x/ansi](https://github.com/charmbracelet/x) from 0.3.0 to 0.3.2.
- [Release notes](https://github.com/charmbracelet/x/releases)
- [Commits](https://github.com/charmbracelet/x/compare/ansi/v0.3.0...ansi/v0.3.2)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/x/ansi
  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-09-17 09:38:35 -03:00
dependabot[bot]
c00c2e416a
chore(deps): bump github.com/charmbracelet/x/ansi from 0.2.3 to 0.3.0 (#673)
Bumps [github.com/charmbracelet/x/ansi](https://github.com/charmbracelet/x) from 0.2.3 to 0.3.0.
- [Release notes](https://github.com/charmbracelet/x/releases)
- [Commits](https://github.com/charmbracelet/x/compare/ansi/v0.2.3...ansi/v0.3.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/x/ansi
  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-09-12 12:06:57 -03:00
dependabot[bot]
73a7be24fc
chore(deps): bump github.com/charmbracelet/bubbletea from 1.1.0 to 1.1.1 (#672)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.1.0...v1.1.1)

---
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-12 12:06:45 -03:00
dependabot[bot]
40889fecd9
chore(deps): bump github.com/alecthomas/kong from 0.9.0 to 1.2.1 (#674)
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 0.9.0 to 1.2.1.
- [Commits](https://github.com/alecthomas/kong/compare/v0.9.0...v1.2.1)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/kong
  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-09-12 12:06:27 -03:00
Carlos Alexandro Becker
1917023901
ci: fix dependabot config
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-09-06 13:39:31 -03:00
Carlos Alexandro Becker
f0bd6f4b45
chore(deps): update
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-09-06 13:39:11 -03:00
Carlos Alexandro Becker
b9611e1d83
fix: lint issues (#663) 2024-09-06 12:06:27 -03:00
dependabot[bot]
8ab6253ca1
feat(deps): bump github.com/charmbracelet/bubbletea from 1.0.0 to 1.1.0 (#665)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 1.0.0 to 1.1.0.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.0.0...v1.1.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>
2024-09-06 12:06:13 -03:00
dependabot[bot]
65e46d6e84
feat(deps): bump github.com/charmbracelet/x/ansi from 0.2.2 to 0.2.3 (#656) 2024-09-06 15:02:03 +00:00
Carlos Alexandro Becker
a30dda54eb
build: fix goreleaser version
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-08-29 08:47:15 -03:00
dependabot[bot]
6837ed2d45
feat(deps): bump github.com/charmbracelet/bubbletea from 0.27.0 to 1.0.0 (#661)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 0.27.0 to 1.0.0.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v0.27.0...v1.0.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbletea
  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-08-29 08:46:53 -03:00
Carlos Alexandro Becker
9c1128985e
chore(deps): huh@latest
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-08-23 10:44:10 -03:00
Carlos Alexandro Becker
926c18afed
chore(deps): update huh
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-08-21 16:36:05 -03:00
Carlos Alexandro Becker
7195cf66dc
chore: update codeowners
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-08-21 16:35:52 -03:00
dependabot[bot]
2ee90c8893
feat(deps): bump github.com/charmbracelet/lipgloss (#655) 2024-08-21 18:48:10 +00:00
dependabot[bot]
dd5aa97c4a
feat(deps): bump github.com/charmbracelet/bubbles (#654)
Bumps [github.com/charmbracelet/bubbles](https://github.com/charmbracelet/bubbles) from 0.18.1-0.20240815190156-5428d6ddecae to 0.19.0.
- [Release notes](https://github.com/charmbracelet/bubbles/releases)
- [Changelog](https://github.com/charmbracelet/bubbles/blob/master/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbles/commits/v0.19.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-08-21 15:39:44 -03:00
dependabot[bot]
1a91d335fa
feat(deps): bump github.com/charmbracelet/glamour from 0.7.0 to 0.8.0 (#646)
Bumps [github.com/charmbracelet/glamour](https://github.com/charmbracelet/glamour) from 0.7.0 to 0.8.0.
- [Release notes](https://github.com/charmbracelet/glamour/releases)
- [Commits](https://github.com/charmbracelet/glamour/compare/v0.7.0...v0.8.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-08-19 15:00:32 -03:00
Carlos Alexandro Becker
c9a4e4fa7b
chore(deps): remove replace
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-08-19 14:55:27 -03:00
Carlos Alexandro Becker
19a93b08b9
refactor(input): simplify echoMode
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-08-19 12:00:02 -03:00
Carlos Alexandro Becker
e095a9142b
fix(input): wrong height when using borders in the header
closes #582

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-08-19 11:59:01 -03:00
Carlos Alexandro Becker
f55c314558
fix(deps): update huh 2024-08-14 14:01:26 -03:00
Carlos Alexandro Becker
96448e08e5
fix: show background style help (#641)
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-07-25 16:56:03 -04:00
Carlos Alexandro Becker
7e56d57478
docs: update install instructions
closes #474
2024-07-25 15:58:29 -04:00
Carlos Alexandro Becker
d722a2f1b8
fix: height 0 by default (#640) 2024-07-25 15:50:22 -04:00
Carlos Alexandro Becker
9db5c7fbba
fix: select all keybinding (#639)
* fix: select all keybinding

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* chore(deps): update gum

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-07-25 15:45:13 -04:00
Carlos Alexandro Becker
046a4d361e
fix: use 0 as default width (#634)
* fix: use 0 as default width

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: filter width

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-07-25 13:12:30 -04:00
Piero Lescano
8422c49018
feat(filter): Add cyclic navigation (#483) 2024-07-25 10:02:56 -04:00
Mikael Fangel
b0f4413188
chore: remove explicitly defined max functions (#613)
* filter: remove max function

* timeout: remove max function
2024-07-25 09:57:35 -04:00
76 changed files with 2835 additions and 1091 deletions

2
.github/CODEOWNERS vendored
View file

@ -1 +1 @@
* @maaslalani
* @charmbracelet/everyone

View file

@ -1,38 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View file

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -1,20 +1,57 @@
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"
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"
ignore:
- dependency-name: github.com/charmbracelet/bubbletea/v2
versions:
- v2.0.0-beta1
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"

View file

@ -1,34 +1,13 @@
name: build
on: [push, pull_request]
on:
push:
branches:
- main
pull_request:
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
env:
GO111MODULE: "on"
steps:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ~1.21
- name: Checkout code
uses: actions/checkout@v4
- name: Download Go modules
run: go mod download
- name: Build
run: go build -v ./...
- name: Test
run: go test -v -cover -timeout=30s ./...
snapshot:
uses: charmbracelet/meta/.github/workflows/snapshot.yml@main
uses: charmbracelet/meta/.github/workflows/build.yml@main
secrets:
goreleaser_key: ${{ secrets.GORELEASER_KEY }}
gh_pat: ${{ secrets.PERSONAL_ACCESS_TOKEN }}

17
.github/workflows/dependabot-sync.yml vendored Normal file
View file

@ -0,0 +1,17 @@
name: dependabot-sync
on:
schedule:
- cron: "0 0 * * 0" # every Sunday at midnight
workflow_dispatch: # allows manual triggering
permissions:
contents: write
pull-requests: write
jobs:
dependabot-sync:
uses: charmbracelet/meta/.github/workflows/dependabot-sync.yml@main
with:
repo_name: ${{ github.event.repository.name }}
secrets:
gh_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}

View file

@ -1,28 +0,0 @@
name: lint-soft
on:
push:
pull_request:
permissions:
contents: read
# Optional: allow read access to pull request. Use with `only-new-issues` option.
pull-requests: read
jobs:
golangci:
name: lint-soft
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ^1
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
# Optional: golangci-lint command line arguments.
args: --config .golangci-soft.yml --issues-exit-code=0
# Optional: show only new issues if it's a pull request. The default value is `false`.
only-new-issues: true

14
.github/workflows/lint-sync.yml vendored Normal file
View file

@ -0,0 +1,14 @@
name: lint-sync
on:
schedule:
# every Sunday at midnight
- cron: "0 0 * * 0"
workflow_dispatch: # allows manual triggering
permissions:
contents: write
pull-requests: write
jobs:
lint:
uses: charmbracelet/meta/.github/workflows/lint-sync.yml@main

View file

@ -3,26 +3,6 @@ on:
push:
pull_request:
permissions:
contents: read
# Optional: allow read access to pull request. Use with `only-new-issues` option.
pull-requests: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ^1
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
# Optional: golangci-lint command line arguments.
#args:
# Optional: show only new issues if it's a pull request. The default value is `false`.
only-new-issues: true
lint:
uses: charmbracelet/meta/.github/workflows/lint.yml@main

View file

@ -1,46 +0,0 @@
run:
tests: false
issues:
include:
- EXC0001
- EXC0005
- EXC0011
- EXC0012
- EXC0013
max-issues-per-linter: 0
max-same-issues: 0
linters:
enable:
# - dupl
- exhaustive
# - exhaustivestruct
- goconst
- godot
- godox
- gomnd
- gomoddirectives
- goprintffuncname
# - ifshort
# - lll
- misspell
- nakedret
- nestif
- noctx
- nolintlint
- prealloc
# disable default linters, they are already enabled in .golangci.yml
disable:
- wrapcheck
- deadcode
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- structcheck
- typecheck
- varcheck

View file

@ -1,25 +1,22 @@
version: "2"
run:
tests: false
issues:
include:
- EXC0001
- EXC0005
- EXC0011
- EXC0012
- EXC0013
max-issues-per-linter: 0
max-same-issues: 0
linters:
enable:
- bodyclose
- exportloopref
- goimports
- exhaustive
- goconst
- godot
- gomoddirectives
- goprintffuncname
- gosec
- misspell
- nakedret
- nestif
- nilerr
- predeclared
- noctx
- nolintlint
- prealloc
- revive
- rowserrcheck
- sqlclosecheck
@ -27,3 +24,24 @@ linters:
- unconvert
- unparam
- whitespace
- wrapcheck
exclusions:
rules:
- text: '(slog|log)\.\w+'
linters:
- noctx
generated: lax
presets:
- common-false-positives
settings:
exhaustive:
default-signifies-exhaustive: true
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofumpt
- goimports
exclusions:
generated: lax

View file

@ -1,5 +1,7 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
version: 2
includes:
- from_url:
url: charmbracelet/meta/main/goreleaser-full.yaml

109
README.md
View file

@ -1,5 +1,4 @@
Gum
===
# Gum
<p>
<a href="https://stuff.charm.sh/gum/nutritional-information.png" target="_blank"><img src="https://stuff.charm.sh/gum/gum.png" alt="Gum Image" width="450" /></a>
@ -21,11 +20,13 @@ The above example is running from a single shell script ([source](./examples/dem
## Tutorial
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)
useful shell scripts and dotfile 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.
Ask for the commit type with gum choose:
```bash
gum choose "fix" "feat" "docs" "style" "refactor" "test" "chore" "revert"
```
@ -34,17 +35,20 @@ gum choose "fix" "feat" "docs" "style" "refactor" "test" "chore" "revert"
> 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 the summary and description of changes:
```bash
gum input --value "$TYPE$SCOPE: " --placeholder "Summary of this change"
gum write --placeholder "Details of this change"
```
Confirm before committing:
```bash
gum confirm "Commit changes?" && git commit -m "$SUMMARY" -m "$DESCRIPTION"
```
@ -64,6 +68,9 @@ brew install gum
# Arch Linux (btw)
pacman -S gum
# Fedora or EPEL 10
dnf install gum
# Nix
nix-env -iA nixpkgs.gum
@ -84,10 +91,11 @@ curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/ke
echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list
sudo apt update && sudo apt install gum
```
</details>
<details>
<summary>Fedora/RHEL</summary>
<summary>Fedora/RHEL/OpenSuse</summary>
```bash
echo '[charm]
@ -96,14 +104,35 @@ baseurl=https://repo.charm.sh/yum/
enabled=1
gpgcheck=1
gpgkey=https://repo.charm.sh/yum/gpg.key' | sudo tee /etc/yum.repos.d/charm.repo
sudo rpm --import https://repo.charm.sh/yum/gpg.key
# yum
sudo yum install gum
# zypper
sudo zypper refresh
sudo zypper install gum
```
</details>
<details>
<summary>FreeBSD</summary>
```bash
# packages
sudo pkg install gum
# ports
cd /usr/ports/devel/gum && sudo make install clean
```
</details>
Or download it:
* [Packages][releases] are available in Debian, RPM, and Alpine formats
* [Binaries][releases] are available for Linux, macOS, Windows, FreeBSD, OpenBSD, and NetBSD
- [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`:
@ -115,20 +144,19 @@ go install github.com/charmbracelet/gum@latest
## 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
- [`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
@ -136,6 +164,7 @@ You can customize `gum` options and styles with `--flags` and `$ENVIRONMENT_VARI
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" \
@ -238,7 +267,7 @@ gum confirm && rm file.txt || echo "File not removed"
Prompt the user to select a file from the file tree.
```bash
EDITOR $(gum file $HOME)
$EDITOR $(gum file $HOME)
```
<img src="https://vhs.charm.sh/vhs-2RMRqmnOPneneIgVJJ3mI1.gif" width="600" alt="Shell running gum file" />
@ -369,81 +398,87 @@ How to use `gum` in your daily workflows:
See the [examples](./examples/) directory for more real world use cases.
* Write a 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")"
```
* Open files in your `$EDITOR`
- Open files in your `$EDITOR`
```bash
$EDITOR $(gum filter)
```
* Connect to a `tmux` session
- 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
tmux switch-client -t "$SESSION" || tmux attach -t "$SESSION"
```
* Pick a commit hash from `git` history
- Pick a commit hash from `git` history
```bash
git log --oneline | gum filter | cut -d' ' -f1 # | copy
```
* Simple [`skate`](https://github.com/charmbracelet/skate) password selector.
- Simple [`skate`](https://github.com/charmbracelet/skate) password selector.
```
skate list -k | gum filter | xargs skate get
```
* Uninstall packages
- Uninstall packages
```bash
brew list | gum choose --no-limit | xargs brew uninstall
```
* Clean up `git` branches
- Clean up `git` branches
```bash
git branch | cut -c 3- | gum choose --no-limit | xargs git branch -D
```
* Checkout GitHub pull requests with [`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
```
* Copy command from shell history
- Copy command from shell history
```bash
gum filter < $HISTFILE --height 20
```
* `sudo` replacement
- `sudo` replacement
```bash
alias please="gum input --password | sudo -nS"
```
## Contributing
See [contributing][contribute].
[contribute]: https://github.com/charmbracelet/gum/contribute
## Feedback
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.social/@charmcli)
* [Discord](https://charm.sh/chat)
- [Twitter](https://twitter.com/charmcli)
- [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).

289
choose/choose.go Normal file
View file

@ -0,0 +1,289 @@
// 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/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/paginator"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/exp/ordered"
)
func defaultKeymap() keymap {
return keymap{
Down: key.NewBinding(
key.WithKeys("down", "j", "ctrl+j", "ctrl+n"),
),
Up: key.NewBinding(
key.WithKeys("up", "k", "ctrl+k", "ctrl+p"),
),
Right: key.NewBinding(
key.WithKeys("right", "l", "ctrl+f"),
),
Left: key.NewBinding(
key.WithKeys("left", "h", "ctrl+b"),
),
Home: key.NewBinding(
key.WithKeys("g", "home"),
),
End: key.NewBinding(
key.WithKeys("G", "end"),
),
ToggleAll: key.NewBinding(
key.WithKeys("a", "A", "ctrl+a"),
key.WithHelp("ctrl+a", "select all"),
key.WithDisabled(),
),
Toggle: key.NewBinding(
key.WithKeys(" ", "tab", "x", "ctrl+@"),
key.WithHelp("x", "toggle"),
key.WithDisabled(),
),
Abort: key.NewBinding(
key.WithKeys("ctrl+c"),
key.WithHelp("ctrl+c", "abort"),
),
Quit: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "quit"),
),
Submit: key.NewBinding(
key.WithKeys("enter", "ctrl+q"),
key.WithHelp("enter", "submit"),
),
}
}
type keymap struct {
Down,
Up,
Right,
Left,
Home,
End,
ToggleAll,
Toggle,
Abort,
Quit,
Submit key.Binding
}
// FullHelp implements help.KeyMap.
func (k keymap) FullHelp() [][]key.Binding { return nil }
// ShortHelp implements help.KeyMap.
func (k keymap) ShortHelp() []key.Binding {
return []key.Binding{
k.Toggle,
key.NewBinding(
key.WithKeys("up", "down", "right", "left"),
key.WithHelp("←↓↑→", "navigate"),
),
k.Submit,
k.ToggleAll,
}
}
type model struct {
height int
padding []int
cursor string
selectedPrefix string
unselectedPrefix string
cursorPrefix string
header string
items []item
quitting bool
submitted bool
index int
limit int
numSelected int
currentOrder int
paginator paginator.Model
showHelp bool
help help.Model
keymap keymap
// styles
cursorStyle lipgloss.Style
headerStyle lipgloss.Style
itemStyle lipgloss.Style
selectedItemStyle lipgloss.Style
}
type item struct {
text string
selected bool
order int
}
func (m model) Init() tea.Cmd { return nil }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
return m, nil
case tea.KeyMsg:
start, end := m.paginator.GetSliceBounds(len(m.items))
km := m.keymap
switch {
case key.Matches(msg, km.Down):
m.index++
if m.index >= len(m.items) {
m.index = 0
m.paginator.Page = 0
}
if m.index >= end {
m.paginator.NextPage()
}
case key.Matches(msg, km.Up):
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 key.Matches(msg, km.Right):
m.index = ordered.Clamp(m.index+m.height, 0, len(m.items)-1)
m.paginator.NextPage()
case key.Matches(msg, km.Left):
m.index = ordered.Clamp(m.index-m.height, 0, len(m.items)-1)
m.paginator.PrevPage()
case key.Matches(msg, km.End):
m.index = len(m.items) - 1
m.paginator.Page = m.paginator.TotalPages - 1
case key.Matches(msg, km.Home):
m.index = 0
m.paginator.Page = 0
case key.Matches(msg, km.ToggleAll):
if m.limit <= 1 {
break
}
if m.numSelected < len(m.items) && m.numSelected < m.limit {
m = m.selectAll()
} else {
m = m.deselectAll()
}
case key.Matches(msg, km.Quit):
m.quitting = true
return m, tea.Quit
case key.Matches(msg, km.Abort):
m.quitting = true
return m, tea.Interrupt
case key.Matches(msg, km.Toggle):
if m.limit == 1 {
break // no op
}
if m.items[m.index].selected {
m.items[m.index].selected = false
m.numSelected--
} else if m.numSelected < m.limit {
m.items[m.index].selected = true
m.items[m.index].order = m.currentOrder
m.numSelected++
m.currentOrder++
}
case key.Matches(msg, km.Submit):
m.quitting = true
if m.limit <= 1 && m.numSelected < 1 {
m.items[m.index].selected = true
}
m.submitted = true
return m, tea.Quit
}
}
var cmd tea.Cmd
m.paginator, cmd = m.paginator.Update(msg)
return m, cmd
}
func (m model) selectAll() model {
for i := range m.items {
if m.numSelected >= m.limit {
break // do not exceed given limit
}
if m.items[i].selected {
continue
}
m.items[i].selected = true
m.items[i].order = m.currentOrder
m.numSelected++
m.currentOrder++
}
return m
}
func (m model) deselectAll() model {
for i := range m.items {
m.items[i].selected = false
m.items[i].order = 0
}
m.numSelected = 0
m.currentOrder = 0
return m
}
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(" ", lipgloss.Width(m.cursor)))
}
if item.selected {
s.WriteString(m.selectedItemStyle.Render(m.selectedPrefix + item.text))
} else if i == m.index%m.height {
s.WriteString(m.cursorStyle.Render(m.cursorPrefix + item.text))
} else {
s.WriteString(m.itemStyle.Render(m.unselectedPrefix + item.text))
}
if i != m.height {
s.WriteRune('\n')
}
}
if m.paginator.TotalPages > 1 {
s.WriteString(strings.Repeat("\n", m.height-m.paginator.ItemsOnPage(len(m.items))+1))
s.WriteString(" " + m.paginator.View())
}
var parts []string
if m.header != "" {
parts = append(parts, m.headerStyle.Render(m.header))
}
parts = append(parts, s.String())
if m.showHelp {
parts = append(parts, "", m.help.View(m.keymap))
}
view := lipgloss.JoinVertical(lipgloss.Left, parts...)
return lipgloss.NewStyle().
Padding(m.padding...).
Render(view)
}

View file

@ -4,132 +4,179 @@ import (
"errors"
"fmt"
"os"
"slices"
"sort"
"strings"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/term"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/paginator"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/internal/timeout"
"github.com/charmbracelet/gum/internal/tty"
"github.com/charmbracelet/gum/style"
"github.com/charmbracelet/lipgloss"
)
const widthBuffer = 2
// Run provides a shell script interface for choosing between different through
// options.
func (o Options) Run() error {
if len(o.Options) <= 0 {
input, _ := stdin.Read()
var (
subduedStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#847A85", Dark: "#979797"})
verySubduedStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"})
)
input, _ := stdin.Read(stdin.StripANSI(o.StripANSI))
if len(o.Options) > 0 && len(o.Selected) == 0 {
o.Selected = strings.Split(input, o.InputDelimiter)
} else if len(o.Options) == 0 {
if input == "" {
return errors.New("no options provided, see `gum choose --help`")
}
o.Options = strings.Split(input, "\n")
o.Options = strings.Split(input, o.InputDelimiter)
}
// normalize options into a map
options := map[string]string{}
// keep the labels in the user-provided order
var labels []string //nolint:prealloc
for _, opt := range o.Options {
if o.LabelDelimiter == "" {
options[opt] = opt
continue
}
label, value, ok := strings.Cut(opt, o.LabelDelimiter)
if !ok {
return fmt.Errorf("invalid option format: %q", opt)
}
labels = append(labels, label)
options[label] = value
}
if o.LabelDelimiter != "" {
o.Options = labels
}
if o.SelectIfOne && len(o.Options) == 1 {
fmt.Println(o.Options[0])
fmt.Println(options[o.Options[0]])
return nil
}
theme := huh.ThemeCharm()
options := huh.NewOptions(o.Options...)
theme.Focused.Base = lipgloss.NewStyle()
theme.Focused.Title = o.HeaderStyle.ToLipgloss()
theme.Focused.SelectSelector = o.CursorStyle.ToLipgloss().SetString(o.Cursor)
theme.Focused.MultiSelectSelector = o.CursorStyle.ToLipgloss().SetString(o.Cursor)
theme.Focused.SelectedOption = o.SelectedItemStyle.ToLipgloss()
theme.Focused.UnselectedOption = o.ItemStyle.ToLipgloss()
theme.Focused.SelectedPrefix = o.SelectedItemStyle.ToLipgloss().SetString(o.SelectedPrefix)
theme.Focused.UnselectedPrefix = o.ItemStyle.ToLipgloss().SetString(o.UnselectedPrefix)
for _, s := range o.Selected {
for i, opt := range options {
if s == opt.Key || s == opt.Value {
options[i] = opt.Selected(true)
}
}
// We don't need to display prefixes if we are only picking one option.
// Simply displaying the cursor is enough.
if o.Limit == 1 && !o.NoLimit {
o.SelectedPrefix = ""
o.UnselectedPrefix = ""
o.CursorPrefix = ""
}
if o.NoLimit {
o.Limit = len(o.Options)
o.Limit = len(o.Options) + 1
}
width := max(widest(o.Options)+
max(lipgloss.Width(o.SelectedPrefix)+lipgloss.Width(o.UnselectedPrefix))+
lipgloss.Width(o.Cursor)+1, lipgloss.Width(o.Header)+widthBuffer)
if o.Limit > 1 {
var choices []string
field := huh.NewMultiSelect[string]().
Options(options...).
Title(o.Header).
Height(o.Height).
Limit(o.Limit).
Value(&choices)
form := huh.NewForm(huh.NewGroup(field))
err := form.
WithWidth(width).
WithShowHelp(o.ShowHelp).
WithTheme(theme).
Run()
if err != nil {
return err
}
if len(choices) > 0 {
s := strings.Join(choices, "\n")
ansiprint(s)
}
return nil
if o.Ordered {
slices.SortFunc(o.Options, strings.Compare)
}
var choice string
isSelectAll := len(o.Selected) == 1 && o.Selected[0] == "*"
err := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Options(options...).
Title(o.Header).
Height(o.Height).
Value(&choice),
),
).
WithWidth(width).
WithTheme(theme).
WithShowHelp(o.ShowHelp).
Run()
// Keep track of the selected items.
currentSelected := 0
// Check if selected items should be used.
hasSelectedItems := len(o.Selected) > 0
startingIndex := 0
currentOrder := 0
items := make([]item, len(o.Options))
for i, option := range o.Options {
var order int
// Check if the option should be selected.
isSelected := hasSelectedItems && currentSelected < o.Limit && (isSelectAll || slices.Contains(o.Selected, option))
// If the option is selected then increment the current selected count.
if isSelected {
if o.Limit == 1 {
// When the user can choose only one option don't select the option but
// start with the cursor hovering over it.
startingIndex = i
isSelected = false
} else {
currentSelected++
order = currentOrder
currentOrder++
}
}
items[i] = item{text: option, selected: isSelected, order: order}
}
// Use the pagination model to display the current and total number of
// pages.
top, right, bottom, left := style.ParsePadding(o.Padding)
pager := paginator.New()
pager.SetTotalPages((len(items) + o.Height - 1) / o.Height)
pager.PerPage = o.Height
pager.Type = paginator.Dots
pager.ActiveDot = subduedStyle.Render("•")
pager.InactiveDot = verySubduedStyle.Render("•")
pager.KeyMap = paginator.KeyMap{}
pager.Page = startingIndex / o.Height
km := defaultKeymap()
if o.NoLimit || o.Limit > 1 {
km.Toggle.SetEnabled(true)
}
if o.NoLimit {
km.ToggleAll.SetEnabled(true)
}
m := model{
index: startingIndex,
currentOrder: currentOrder,
height: o.Height,
padding: []int{top, right, bottom, left},
cursor: o.Cursor,
header: o.Header,
selectedPrefix: o.SelectedPrefix,
unselectedPrefix: o.UnselectedPrefix,
cursorPrefix: o.CursorPrefix,
items: items,
limit: o.Limit,
paginator: pager,
cursorStyle: o.CursorStyle.ToLipgloss(),
headerStyle: o.HeaderStyle.ToLipgloss(),
itemStyle: o.ItemStyle.ToLipgloss(),
selectedItemStyle: o.SelectedItemStyle.ToLipgloss(),
numSelected: currentSelected,
showHelp: o.ShowHelp,
help: help.New(),
keymap: km,
}
ctx, cancel := timeout.Context(o.Timeout)
defer cancel()
// Disable Keybindings since we will control it ourselves.
tm, err := tea.NewProgram(
m,
tea.WithOutput(os.Stderr),
tea.WithContext(ctx),
).Run()
if err != nil {
return err
return fmt.Errorf("unable to pick selection: %w", err)
}
m = tm.(model)
if !m.submitted {
return errors.New("nothing selected")
}
if o.Ordered && o.Limit > 1 {
sort.Slice(m.items, func(i, j int) bool {
return m.items[i].order < m.items[j].order
})
}
if term.IsTerminal(os.Stdout.Fd()) {
fmt.Println(choice)
} else {
fmt.Print(ansi.Strip(choice))
var out []string
for _, item := range m.items {
if item.selected {
out = append(out, options[item.text])
}
}
tty.Println(strings.Join(out, o.OutputDelimiter))
return nil
}
func widest(options []string) int {
var max int
for _, o := range options {
w := lipgloss.Width(o)
if w > max {
max = w
}
}
return max
}
func ansiprint(s string) {
if term.IsTerminal(os.Stdout.Fd()) {
fmt.Println(s)
} else {
fmt.Print(ansi.Strip(s))
}
}

View file

@ -8,22 +8,28 @@ import (
// Options is the customization options for the choose command.
type Options struct {
Options []string `arg:"" optional:"" help:"Options to choose from."`
Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"`
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
Ordered bool `help:"Maintain the order of the selected options" env:"GUM_CHOOSE_ORDERED"`
Height int `help:"Height of the list" default:"10" env:"GUM_CHOOSE_HEIGHT"`
Cursor string `help:"Prefix to show on item that corresponds to the cursor position" default:"> " env:"GUM_CHOOSE_CURSOR"`
ShowHelp bool `help:"Show help keybinds" default:"true" negatable:"true" env:"GUM_CHOOSE_SHOW_HELP"`
Header string `help:"Header value" default:"Choose:" env:"GUM_CHOOSE_HEADER"`
CursorPrefix string `help:"Prefix to show on the cursor item (hidden if limit is 1)" default:"• " env:"GUM_CHOOSE_CURSOR_PREFIX"`
SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:"✓ " env:"GUM_CHOOSE_SELECTED_PREFIX"`
UnselectedPrefix string `help:"Prefix to show on unselected items (hidden if limit is 1)" default:"• " env:"GUM_CHOOSE_UNSELECTED_PREFIX"`
Selected []string `help:"Options that should start as selected" default:"" env:"GUM_CHOOSE_SELECTED"`
SelectIfOne bool `help:"Select the given option if there is only one" group:"Selection"`
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_CURSOR_"`
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=99" envprefix:"GUM_CHOOSE_HEADER_"`
ItemStyle style.Styles `embed:"" prefix:"item." hidden:"" envprefix:"GUM_CHOOSE_ITEM_"`
SelectedItemStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_SELECTED_"`
Timeout time.Duration `help:"Timeout until choose returns selected element" default:"0" env:"GUM_CCHOOSE_TIMEOUT"` // including timeout command options [Timeout,...]
Options []string `arg:"" optional:"" help:"Options to choose from."`
Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"`
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
Ordered bool `help:"Maintain the order of the selected options" env:"GUM_CHOOSE_ORDERED"`
Height int `help:"Height of the list" default:"10" env:"GUM_CHOOSE_HEIGHT"`
Cursor string `help:"Prefix to show on item that corresponds to the cursor position" default:"> " env:"GUM_CHOOSE_CURSOR"`
ShowHelp bool `help:"Show help keybinds" default:"true" negatable:"" env:"GUM_CHOOSE_SHOW_HELP"`
Timeout time.Duration `help:"Timeout until choose returns selected element" default:"0s" env:"GUM_CHOOSE_TIMEOUT"` // including timeout command options [Timeout,...]
Header string `help:"Header value" default:"Choose:" env:"GUM_CHOOSE_HEADER"`
CursorPrefix string `help:"Prefix to show on the cursor item (hidden if limit is 1)" default:"• " env:"GUM_CHOOSE_CURSOR_PREFIX"`
SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:"✓ " env:"GUM_CHOOSE_SELECTED_PREFIX"`
UnselectedPrefix string `help:"Prefix to show on unselected items (hidden if limit is 1)" default:"• " env:"GUM_CHOOSE_UNSELECTED_PREFIX"`
Selected []string `help:"Options that should start as selected (selects all if given *)" default:"" env:"GUM_CHOOSE_SELECTED"`
SelectIfOne bool `help:"Select the given option if there is only one" group:"Selection"`
InputDelimiter string `help:"Option delimiter when reading from STDIN" default:"\n" env:"GUM_CHOOSE_INPUT_DELIMITER"`
OutputDelimiter string `help:"Option delimiter when writing to STDOUT" default:"\n" env:"GUM_CHOOSE_OUTPUT_DELIMITER"`
LabelDelimiter string `help:"Allows to set a delimiter, so options can be set as label:value" default:"" env:"GUM_CHOOSE_LABEL_DELIMITER"`
StripANSI bool `help:"Strip ANSI sequences when reading from STDIN" default:"true" negatable:"" env:"GUM_CHOOSE_STRIP_ANSI"`
Padding string `help:"Padding" default:"${defaultPadding}" group:"Style Flags" env:"GUM_CHOOSE_PADDING"`
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_"`
}

View file

@ -1,3 +1,5 @@
// Package completion provides a bash completion generator for Kong
// applications.
package completion
import (
@ -628,6 +630,7 @@ func writeCmdAliases(buf io.StringWriter, cmd *kong.Node) {
writeString(buf, ` fi`)
writeString(buf, "\n")
}
func writeArgAliases(buf io.StringWriter, cmd *kong.Node) {
writeString(buf, " noun_aliases=()\n")
sort.Strings(cmd.Aliases)

View file

@ -79,7 +79,7 @@ func (f Fish) gen(buf io.StringWriter, cmd *kong.Node) {
_, _ = buf.WriteString(fmt.Sprintf(" -s %c", f.Short))
}
_, _ = buf.WriteString(fmt.Sprintf(" -l %s", f.Name))
_, _ = buf.WriteString(fmt.Sprintf(" -d '%s'", f.Help))
_, _ = buf.WriteString(fmt.Sprintf(" -d \"%s\"", f.Help))
_, _ = buf.WriteString("\n")
}
_, _ = buf.WriteString("\n")

View file

@ -1,42 +1,71 @@
package confirm
import (
"context"
"fmt"
"os"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/bubbles/help"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/exit"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/internal/timeout"
"github.com/charmbracelet/gum/style"
)
// 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 {
theme := huh.ThemeCharm()
theme.Focused.Title = o.PromptStyle.ToLipgloss()
theme.Focused.FocusedButton = o.SelectedStyle.ToLipgloss()
theme.Focused.BlurredButton = o.UnselectedStyle.ToLipgloss()
choice := o.Default
err := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Affirmative(o.Affirmative).
Negative(o.Negative).
Title(o.Prompt).
Value(&choice),
),
).
WithTheme(theme).
WithShowHelp(o.ShowHelp).
Run()
if err != nil {
return fmt.Errorf("unable to run confirm: %w", err)
line, err := stdin.Read(stdin.SingleLine(true))
if err == nil {
switch line {
case "yes", "y":
return nil
default:
return exit.ErrExit(1)
}
}
if !choice {
os.Exit(1)
ctx, cancel := timeout.Context(o.Timeout)
defer cancel()
top, right, bottom, left := style.ParsePadding(o.Padding)
m := model{
affirmative: o.Affirmative,
negative: o.Negative,
showOutput: o.ShowOutput,
confirmation: o.Default,
defaultSelection: o.Default,
keys: defaultKeymap(o.Affirmative, o.Negative),
help: help.New(),
showHelp: o.ShowHelp,
prompt: o.Prompt,
selectedStyle: o.SelectedStyle.ToLipgloss(),
unselectedStyle: o.UnselectedStyle.ToLipgloss(),
promptStyle: o.PromptStyle.ToLipgloss(),
padding: []int{top, right, bottom, left},
}
tm, err := tea.NewProgram(
m,
tea.WithOutput(os.Stderr),
tea.WithContext(ctx),
).Run()
if err != nil && ctx.Err() != context.DeadlineExceeded {
return fmt.Errorf("unable to confirm: %w", err)
}
m = tm.(model)
if o.ShowOutput {
confirmationText := m.negative
if m.confirmation {
confirmationText = m.affirmative
}
fmt.Println(m.prompt, confirmationText)
}
return nil
if m.confirmation {
return nil
}
return exit.ErrExit(1)
}

168
confirm/confirm.go Normal file
View file

@ -0,0 +1,168 @@
// 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 (
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
func defaultKeymap(affirmative, negative string) keymap {
return keymap{
Abort: key.NewBinding(
key.WithKeys("ctrl+c"),
key.WithHelp("ctrl+c", "cancel"),
),
Quit: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "quit"),
),
Negative: key.NewBinding(
key.WithKeys("n", "N", "q"),
key.WithHelp("n", negative),
),
Affirmative: key.NewBinding(
key.WithKeys("y", "Y"),
key.WithHelp("y", affirmative),
),
Toggle: key.NewBinding(
key.WithKeys(
"left",
"h",
"ctrl+n",
"shift+tab",
"right",
"l",
"ctrl+p",
"tab",
),
key.WithHelp("←→", "toggle"),
),
Submit: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "submit"),
),
}
}
type keymap struct {
Abort key.Binding
Quit key.Binding
Negative key.Binding
Affirmative key.Binding
Toggle key.Binding
Submit key.Binding
}
// FullHelp implements help.KeyMap.
func (k keymap) FullHelp() [][]key.Binding { return nil }
// ShortHelp implements help.KeyMap.
func (k keymap) ShortHelp() []key.Binding {
return []key.Binding{k.Toggle, k.Submit, k.Affirmative, k.Negative}
}
type model struct {
prompt string
affirmative string
negative string
quitting bool
showHelp bool
help help.Model
keys keymap
showOutput bool
confirmation bool
defaultSelection bool
// styles
promptStyle lipgloss.Style
selectedStyle lipgloss.Style
unselectedStyle lipgloss.Style
padding []int
}
func (m model) Init() tea.Cmd { return nil }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
return m, nil
case tea.KeyMsg:
switch {
case key.Matches(msg, m.keys.Abort):
m.confirmation = false
return m, tea.Interrupt
case key.Matches(msg, m.keys.Quit):
m.confirmation = false
m.quitting = true
return m, tea.Quit
case key.Matches(msg, m.keys.Negative):
m.confirmation = false
m.quitting = true
return m, tea.Quit
case key.Matches(msg, m.keys.Toggle):
if m.negative == "" {
break
}
m.confirmation = !m.confirmation
case key.Matches(msg, m.keys.Submit):
m.quitting = true
return m, tea.Quit
case key.Matches(msg, m.keys.Affirmative):
m.quitting = true
m.confirmation = true
return m, tea.Quit
}
}
return m, nil
}
func (m model) View() string {
if m.quitting {
return ""
}
var aff, neg string
if m.confirmation {
aff = m.selectedStyle.Render(m.affirmative)
neg = m.unselectedStyle.Render(m.negative)
} else {
aff = m.unselectedStyle.Render(m.affirmative)
neg = m.selectedStyle.Render(m.negative)
}
// If the option is intentionally empty, do not show it.
if m.negative == "" {
neg = ""
}
parts := []string{
m.promptStyle.Render(m.prompt) + "\n",
lipgloss.JoinHorizontal(lipgloss.Left, aff, neg),
}
if m.showHelp {
parts = append(parts, "", m.help.View(m.keys))
}
return lipgloss.NewStyle().
Padding(m.padding...).
Render(lipgloss.JoinVertical(
lipgloss.Left,
parts...,
))
}

View file

@ -9,6 +9,7 @@ import (
// Options is the customization options for the confirm command.
type Options struct {
Default bool `help:"Default confirmation action" default:"true"`
ShowOutput bool `help:"Print prompt and chosen action to output" default:"false"`
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?"`
@ -19,5 +20,6 @@ type Options struct {
//nolint:staticcheck
UnselectedStyle style.Styles `embed:"" prefix:"unselected." help:"The style of the unselected action" set:"defaultBackground=235" set:"defaultForeground=254" set:"defaultPadding=0 3" set:"defaultMargin=0 1" envprefix:"GUM_CONFIRM_UNSELECTED_"`
ShowHelp bool `help:"Show help key binds" negatable:"" default:"true" env:"GUM_CONFIRM_SHOW_HELP"`
Timeout time.Duration `help:"Timeout until confirm returns selected value or default if provided" default:"0" env:"GUM_CONFIRM_TIMEOUT"`
Timeout time.Duration `help:"Timeout until confirm returns selected value or default if provided" default:"0s" env:"GUM_CONFIRM_TIMEOUT"`
Padding string `help:"Padding" default:"${defaultPadding}" group:"Style Flags" env:"GUM_CONFIRM_PADDING"`
}

View file

@ -1,3 +1,4 @@
// Package cursor provides cursor modes.
package cursor
import (

View file

@ -2,11 +2,11 @@
pkgs.buildGoModule rec {
pname = "gum";
version = "0.14.0";
version = "0.15.2";
src = ./.;
vendorHash = "sha256-gDDaKrwlrJyyDzgyGf9iP/XPnOAwpkvIyzCXobXrlF4=";
vendorHash = "sha256-TK2Fc4bTkiSpyYrg4dJOzamEnii03P7kyHZdah9izqY=";
ldflags = [ "-s" "-w" "-X=main.Version=${version}" ];
}

View file

@ -3,10 +3,14 @@ package file
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/bubbles/filepicker"
"github.com/charmbracelet/bubbles/help"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/timeout"
"github.com/charmbracelet/gum/style"
)
// Run is the interface to picking a file.
@ -24,40 +28,52 @@ func (o Options) Run() error {
return fmt.Errorf("file not found: %w", err)
}
theme := huh.ThemeCharm()
theme.Focused.Base = lipgloss.NewStyle()
theme.Focused.File = o.FileStyle.ToLipgloss()
theme.Focused.Directory = o.DirectoryStyle.ToLipgloss()
theme.Focused.SelectedOption = o.SelectedStyle.ToLipgloss()
keymap := huh.NewDefaultKeyMap()
keymap.FilePicker.Open.SetEnabled(false)
// XXX: These should be file selected specific.
theme.Focused.TextInput.Placeholder = o.PermissionsStyle.ToLipgloss()
theme.Focused.TextInput.Prompt = o.CursorStyle.ToLipgloss()
err = huh.NewForm(
huh.NewGroup(
huh.NewFilePicker().
Picking(true).
CurrentDirectory(path).
DirAllowed(o.Directory).
FileAllowed(o.File).
Height(o.Height).
ShowHidden(o.All).
Value(&path),
),
).
WithShowHelp(o.ShowHelp).
WithKeyMap(keymap).
WithTheme(theme).
Run()
if err != nil {
return err
fp := filepicker.New()
fp.CurrentDirectory = path
fp.Path = path
fp.SetHeight(o.Height)
fp.AutoHeight = o.Height == 0
fp.Cursor = o.Cursor
fp.DirAllowed = o.Directory
fp.FileAllowed = o.File
fp.ShowPermissions = o.Permissions
fp.ShowSize = o.Size
fp.ShowHidden = o.All
fp.Styles = filepicker.DefaultStyles()
fp.Styles.Cursor = o.CursorStyle.ToLipgloss()
fp.Styles.Symlink = o.SymlinkStyle.ToLipgloss()
fp.Styles.Directory = o.DirectoryStyle.ToLipgloss()
fp.Styles.File = o.FileStyle.ToLipgloss()
fp.Styles.Permission = o.PermissionsStyle.ToLipgloss()
fp.Styles.Selected = o.SelectedStyle.ToLipgloss()
fp.Styles.FileSize = o.FileSizeStyle.ToLipgloss()
top, right, bottom, left := style.ParsePadding(o.Padding)
m := model{
filepicker: fp,
padding: []int{top, right, bottom, left},
showHelp: o.ShowHelp,
help: help.New(),
keymap: defaultKeymap(),
headerStyle: o.HeaderStyle.ToLipgloss(),
header: o.Header,
}
fmt.Println(path)
ctx, cancel := timeout.Context(o.Timeout)
defer cancel()
tm, err := tea.NewProgram(
&m,
tea.WithOutput(os.Stderr),
tea.WithContext(ctx),
).Run()
if err != nil {
return fmt.Errorf("unable to pick selection: %w", err)
}
m = tm.(model)
if m.selectedPath == "" {
return errors.New("no file selected")
}
fmt.Println(m.selectedPath)
return nil
}

119
file/file.go Normal file
View file

@ -0,0 +1,119 @@
// Package file provides an interface to pick a file from a folder (tree).
// The user is provided a file manager-like interface to navigate, to
// select a file.
//
// Let's pick a file from the current directory:
//
// $ gum file
// $ gum file .
//
// Let's pick a file from the home directory:
//
// $ gum file $HOME
package file
import (
"github.com/charmbracelet/bubbles/filepicker"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type keymap filepicker.KeyMap
var keyQuit = key.NewBinding(
key.WithKeys("esc", "q"),
key.WithHelp("esc", "close"),
)
var keyAbort = key.NewBinding(
key.WithKeys("ctrl+c"),
key.WithHelp("ctrl+c", "abort"),
)
func defaultKeymap() keymap {
km := filepicker.DefaultKeyMap()
return keymap(km)
}
// FullHelp implements help.KeyMap.
func (k keymap) FullHelp() [][]key.Binding { return nil }
// ShortHelp implements help.KeyMap.
func (k keymap) ShortHelp() []key.Binding {
return []key.Binding{
key.NewBinding(
key.WithKeys("up", "down"),
key.WithHelp("↓↑", "navigate"),
),
keyQuit,
k.Select,
}
}
type model struct {
header string
headerStyle lipgloss.Style
filepicker filepicker.Model
selectedPath string
quitting bool
showHelp bool
padding []int
help help.Model
keymap keymap
}
func (m model) Init() tea.Cmd { return m.filepicker.Init() }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
height := msg.Height - m.padding[0] - m.padding[2]
if m.showHelp {
height -= lipgloss.Height(m.helpView())
}
m.filepicker.SetHeight(height)
case tea.KeyMsg:
switch {
case key.Matches(msg, keyAbort):
m.quitting = true
return m, tea.Interrupt
case key.Matches(msg, keyQuit):
m.quitting = true
return m, tea.Quit
}
}
var cmd tea.Cmd
m.filepicker, cmd = m.filepicker.Update(msg)
if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect {
m.selectedPath = path
m.quitting = true
return m, tea.Quit
}
return m, cmd
}
func (m model) View() string {
if m.quitting {
return ""
}
var parts []string
if m.header != "" {
parts = append(parts, m.headerStyle.Render(m.header))
}
parts = append(parts, m.filepicker.View())
if m.showHelp {
parts = append(parts, m.helpView())
}
return lipgloss.NewStyle().
Padding(m.padding...).
Render(lipgloss.JoinVertical(
lipgloss.Left,
parts...,
))
}
func (m model) helpView() string {
return m.help.View(m.keymap)
}

View file

@ -11,21 +11,24 @@ type Options struct {
// Path is the path to the folder / directory to begin traversing.
Path string `arg:"" optional:"" name:"path" help:"The path to the folder to begin traversing" env:"GUM_FILE_PATH"`
// Cursor is the character to display in front of the current selected items.
Cursor string `short:"c" help:"The cursor character" default:">" env:"GUM_FILE_CURSOR"`
All bool `short:"a" help:"Show hidden and 'dot' files" default:"false" env:"GUM_FILE_ALL"`
File bool `help:"Allow files selection" default:"true" env:"GUM_FILE_FILE"`
Directory bool `help:"Allow directories selection" default:"false" env:"GUM_FILE_DIRECTORY"`
ShowHelp bool `help:"Show help key binds" negatable:"" default:"true" env:"GUM_FILE_SHOW_HELP"`
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"`
Permissions bool `short:"p" help:"Show file permissions" default:"true" negatable:"" env:"GUM_FILE_PERMISSION"`
Size bool `short:"s" help:"Show file size" default:"true" negatable:"" env:"GUM_FILE_SIZE"`
File bool `help:"Allow files selection" default:"true" env:"GUM_FILE_FILE"`
Directory bool `help:"Allow directories selection" default:"false" env:"GUM_FILE_DIRECTORY"`
ShowHelp bool `help:"Show help key binds" negatable:"" default:"true" env:"GUM_FILE_SHOW_HELP"`
Timeout time.Duration `help:"Timeout until command aborts without a selection" default:"0s" env:"GUM_FILE_TIMEOUT"`
Header string `help:"Header value" default:"" env:"GUM_FILE_HEADER"`
Height int `help:"Maximum number of files to display" default:"10" env:"GUM_FILE_HEIGHT"`
Height int `help:"Maximum number of files to display" default:"10" env:"GUM_FILE_HEIGHT"`
CursorStyle style.Styles `embed:"" prefix:"cursor." help:"The cursor style" set:"defaultForeground=212" envprefix:"GUM_FILE_CURSOR_"`
SymlinkStyle style.Styles `embed:"" prefix:"symlink." help:"The style to use for symlinks" set:"defaultForeground=36" envprefix:"GUM_FILE_SYMLINK_"`
DirectoryStyle style.Styles `embed:"" prefix:"directory." help:"The style to use for directories" set:"defaultForeground=99" envprefix:"GUM_FILE_DIRECTORY_"`
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"`
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_"` //nolint:staticcheck
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=99" envprefix:"GUM_FILE_HEADER_"`
Padding string `help:"Padding" default:"${defaultPadding}" group:"Style Flags" env:"GUM_FILE_PADDING"`
}

View file

@ -4,18 +4,20 @@ import (
"errors"
"fmt"
"os"
"slices"
"strings"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/term"
"github.com/sahilm/fuzzy"
"github.com/charmbracelet/gum/internal/exit"
"github.com/charmbracelet/gum/internal/files"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/internal/timeout"
"github.com/charmbracelet/gum/internal/tty"
"github.com/charmbracelet/gum/style"
"github.com/charmbracelet/x/ansi"
"github.com/sahilm/fuzzy"
)
// Run provides a shell script interface for filtering through options, powered
@ -33,8 +35,8 @@ func (o Options) Run() error {
v := viewport.New(o.Width, o.Height)
if len(o.Options) == 0 {
if input, _ := stdin.Read(); input != "" {
o.Options = strings.Split(input, "\n")
if input, _ := stdin.Read(stdin.StripANSI(o.StripANSI)); input != "" {
o.Options = strings.Split(input, o.InputDelimiter)
} else {
o.Options = files.List()
}
@ -44,12 +46,14 @@ func (o Options) Run() error {
return errors.New("no options provided, see `gum filter --help`")
}
if o.SelectIfOne && len(o.Options) == 1 {
fmt.Println(o.Options[0])
return nil
}
ctx, cancel := timeout.Context(o.Timeout)
defer cancel()
options := []tea.ProgramOption{tea.WithOutput(os.Stderr)}
options := []tea.ProgramOption{
tea.WithOutput(os.Stderr),
tea.WithReportFocus(),
tea.WithContext(ctx),
}
if o.Height == 0 {
options = append(options, tea.WithAltScreen())
}
@ -58,21 +62,43 @@ func (o Options) Run() error {
if o.Value != "" {
i.SetValue(o.Value)
}
choices := map[string]string{}
filteringChoices := []string{}
for _, opt := range o.Options {
s := ansi.Strip(opt)
choices[s] = opt
filteringChoices = append(filteringChoices, s)
}
switch {
case o.Value != "" && o.Fuzzy:
matches = fuzzy.Find(o.Value, o.Options)
matches = fuzzy.Find(o.Value, filteringChoices)
case o.Value != "" && !o.Fuzzy:
matches = exactMatches(o.Value, o.Options)
matches = exactMatches(o.Value, filteringChoices)
default:
matches = matchAll(o.Options)
matches = matchAll(filteringChoices)
}
if o.NoLimit {
o.Limit = len(o.Options)
}
p := tea.NewProgram(model{
choices: o.Options,
if o.SelectIfOne && len(matches) == 1 {
tty.Println(matches[0].Str)
return nil
}
km := defaultKeymap()
if o.NoLimit || o.Limit > 1 {
km.Toggle.SetEnabled(true)
km.ToggleAndPrevious.SetEnabled(true)
km.ToggleAndNext.SetEnabled(true)
km.ToggleAll.SetEnabled(true)
}
top, right, bottom, left := style.ParsePadding(o.Padding)
m := model{
choices: choices,
filteringChoices: filteringChoices,
indicator: o.Indicator,
matches: matches,
header: o.Header,
@ -88,51 +114,61 @@ func (o Options) Run() error {
textStyle: o.TextStyle.ToLipgloss(),
cursorTextStyle: o.CursorTextStyle.ToLipgloss(),
height: o.Height,
padding: []int{top, right, bottom, left},
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...)
sort: o.Sort && o.FuzzySort,
strict: o.Strict,
showHelp: o.ShowHelp,
keymap: km,
help: help.New(),
}
tm, err := p.Run()
isSelectAll := len(o.Selected) == 1 && o.Selected[0] == "*"
currentSelected := 0
if len(o.Selected) > 0 {
for i, option := range matches {
if currentSelected >= o.Limit || (!isSelectAll && !slices.Contains(o.Selected, option.Str)) {
continue
}
if o.Limit == 1 {
m.cursor = i
m.selected[option.Str] = struct{}{}
} else {
currentSelected++
m.selected[option.Str] = struct{}{}
}
}
}
tm, err := tea.NewProgram(m, options...).Run()
if err != nil {
return fmt.Errorf("unable to run filter: %w", err)
}
m := tm.(model)
if m.aborted {
return exit.ErrAborted
}
isTTY := term.IsTerminal(os.Stdout.Fd())
m = tm.(model)
if !m.submitted {
return errors.New("nothing selected")
}
// 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 {
o.checkSelected(m, isTTY)
o.checkSelected(m)
} else if len(m.matches) > m.cursor && m.cursor >= 0 {
if isTTY {
fmt.Println(m.matches[m.cursor].Str)
} else {
fmt.Println(ansi.Strip(m.matches[m.cursor].Str))
}
tty.Println(m.matches[m.cursor].Str)
}
if !o.Strict && len(m.textinput.Value()) != 0 && len(m.matches) == 0 {
fmt.Println(m.textinput.Value())
}
return nil
}
func (o Options) checkSelected(m model, isTTY bool) {
func (o Options) checkSelected(m model) {
out := []string{}
for k := range m.selected {
if isTTY {
fmt.Println(k)
} else {
fmt.Println(ansi.Strip(k))
}
out = append(out, k)
}
tty.Println(strings.Join(out, o.OutputDelimiter))
}

View file

@ -12,21 +12,122 @@ package filter
import (
"strings"
"time"
"github.com/charmbracelet/gum/timeout"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/exp/ordered"
"github.com/rivo/uniseg"
"github.com/sahilm/fuzzy"
)
func defaultKeymap() keymap {
return keymap{
Down: key.NewBinding(
key.WithKeys("down", "ctrl+j", "ctrl+n"),
),
Up: key.NewBinding(
key.WithKeys("up", "ctrl+k", "ctrl+p"),
),
NDown: key.NewBinding(
key.WithKeys("j"),
),
NUp: key.NewBinding(
key.WithKeys("k"),
),
Home: key.NewBinding(
key.WithKeys("g", "home"),
),
End: key.NewBinding(
key.WithKeys("G", "end"),
),
ToggleAndNext: key.NewBinding(
key.WithKeys("tab"),
key.WithHelp("tab", "toggle"),
key.WithDisabled(),
),
ToggleAndPrevious: key.NewBinding(
key.WithKeys("shift+tab"),
key.WithHelp("shift+tab", "toggle"),
key.WithDisabled(),
),
Toggle: key.NewBinding(
key.WithKeys("ctrl+@"),
key.WithHelp("ctrl+@", "toggle"),
key.WithDisabled(),
),
ToggleAll: key.NewBinding(
key.WithKeys("ctrl+a"),
key.WithHelp("ctrl+a", "select all"),
key.WithDisabled(),
),
FocusInSearch: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("/", "search"),
),
FocusOutSearch: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "blur search"),
),
Quit: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "quit"),
),
Abort: key.NewBinding(
key.WithKeys("ctrl+c"),
key.WithHelp("ctrl+c", "abort"),
),
Submit: key.NewBinding(
key.WithKeys("enter", "ctrl+q"),
key.WithHelp("enter", "submit"),
),
}
}
type keymap struct {
FocusInSearch,
FocusOutSearch,
Down,
Up,
NDown,
NUp,
Home,
End,
ToggleAndNext,
ToggleAndPrevious,
ToggleAll,
Toggle,
Abort,
Quit,
Submit key.Binding
}
// FullHelp implements help.KeyMap.
func (k keymap) FullHelp() [][]key.Binding { return nil }
// ShortHelp implements help.KeyMap.
func (k keymap) ShortHelp() []key.Binding {
return []key.Binding{
key.NewBinding(
key.WithKeys("up", "down"),
key.WithHelp("↓↑", "navigate"),
),
k.FocusInSearch,
k.FocusOutSearch,
k.ToggleAndNext,
k.ToggleAll,
k.Submit,
}
}
type model struct {
textinput textinput.Model
viewport *viewport.Model
choices []string
choices map[string]string
filteringChoices []string
matches []fuzzy.Match
cursor int
header string
@ -37,7 +138,7 @@ type model struct {
selectedPrefix string
unselectedPrefix string
height int
aborted bool
padding []int
quitting bool
headerStyle lipgloss.Style
matchStyle lipgloss.Style
@ -49,13 +150,15 @@ type model struct {
reverse bool
fuzzy bool
sort bool
timeout time.Duration
hasTimeout bool
showHelp bool
keymap keymap
help help.Model
strict bool
submitted bool
}
func (m model) Init() tea.Cmd {
return timeout.Init(m.timeout, nil)
}
func (m model) Init() tea.Cmd { return textinput.Blink }
func (m model) View() string {
if m.quitting {
return ""
@ -102,30 +205,24 @@ func (m model) View() string {
s.WriteString(" ")
}
// 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.
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, buffer a regular character.
buf.WriteRune(c)
}
styledOption := m.choices[match.Str]
if len(match.MatchedIndexes) == 0 {
// No matches, just render the text.
s.WriteString(lineTextStyle.Render(styledOption))
s.WriteRune('\n')
continue
}
// Flush text buffer.
s.WriteString(lineTextStyle.Render(buf.String()))
var ranges []lipgloss.Range
for _, rng := range matchedRanges(match.MatchedIndexes) {
// ansi.Cut is grapheme and ansi sequence aware, we match against a ansi.Stripped string, but we might still have graphemes.
// all that to say that rng is byte positions, but we need to pass it down to ansi.Cut as char positions.
// so we need to adjust it here:
start, stop := bytePosToVisibleCharPos(match.Str, rng)
ranges = append(ranges, lipgloss.NewRange(start, stop+1, m.matchStyle))
}
s.WriteString(lineTextStyle.Render(lipgloss.StyleRanges(styledOption, ranges...)))
// We have finished displaying the match with all of it's matched
// characters highlighted and the rest filled in.
@ -138,79 +235,114 @@ func (m model) View() string {
// View the input and the filtered choices
header := m.headerStyle.Render(m.header)
if m.reverse {
view := m.viewport.View() + "\n" + m.textinput.View()
view := m.viewport.View()
if m.header != "" {
return lipgloss.JoinVertical(lipgloss.Left, view, header)
view += "\n" + header
}
return view
view += "\n" + m.textinput.View()
if m.showHelp {
view += m.helpView()
}
return lipgloss.NewStyle().
Padding(m.padding...).
Render(view)
}
view := m.textinput.View() + "\n" + m.viewport.View()
if m.header != "" {
return lipgloss.JoinVertical(lipgloss.Left, header, view)
if m.showHelp {
view += m.helpView()
}
return view
if m.header != "" {
return lipgloss.NewStyle().
Padding(m.padding...).
Render(header + "\n" + view)
}
return lipgloss.NewStyle().
Padding(m.padding...).
Render(view)
}
func (m model) helpView() string {
return "\n\n" + m.help.View(m.keymap)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
var cmd, icmd tea.Cmd
m.textinput, icmd = m.textinput.Update(msg)
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
// Include the header in the height calculation.
if m.header != "" {
m.viewport.Height = m.viewport.Height - lipgloss.Height(m.headerStyle.Render(m.header))
}
m.viewport.Width = msg.Width
// Include the help in the total height calculation.
if m.showHelp {
m.viewport.Height = m.viewport.Height - lipgloss.Height(m.helpView())
}
m.viewport.Height = m.viewport.Height - m.padding[0] - m.padding[2]
m.viewport.Width = msg.Width - m.padding[1] - m.padding[3]
m.textinput.Width = msg.Width - m.padding[1] - m.padding[3]
if m.reverse {
m.viewport.YOffset = clamp(0, len(m.matches), len(m.matches)-m.viewport.Height)
m.viewport.YOffset = ordered.Clamp(len(m.matches)-m.viewport.Height, 0, len(m.matches))
}
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc":
m.aborted = true
km := m.keymap
switch {
case key.Matches(msg, km.FocusInSearch):
m.textinput.Focus()
case key.Matches(msg, km.FocusOutSearch):
m.textinput.Blur()
case key.Matches(msg, km.Quit):
m.quitting = true
return m, tea.Quit
case "enter":
case key.Matches(msg, km.Abort):
m.quitting = true
return m, tea.Interrupt
case key.Matches(msg, km.Submit):
m.quitting = true
m.submitted = true
return m, tea.Quit
case "ctrl+n", "ctrl+j", "down":
case key.Matches(msg, km.Down, km.NDown):
m.CursorDown()
case "ctrl+p", "ctrl+k", "up":
case key.Matches(msg, km.Up, km.NUp):
m.CursorUp()
case "tab":
case key.Matches(msg, km.Home):
m.cursor = 0
m.viewport.GotoTop()
case key.Matches(msg, km.End):
m.cursor = len(m.choices) - 1
m.viewport.GotoBottom()
case key.Matches(msg, km.ToggleAndNext):
if m.limit == 1 {
break // no op
}
m.ToggleSelection()
m.CursorDown()
case "shift+tab":
case key.Matches(msg, km.ToggleAndPrevious):
if m.limit == 1 {
break // no op
}
m.ToggleSelection()
m.CursorUp()
case "ctrl+@":
case key.Matches(msg, km.Toggle):
if m.limit == 1 {
break // no op
}
m.ToggleSelection()
case key.Matches(msg, km.ToggleAll):
if m.limit <= 1 {
break
}
if m.numSelected < len(m.matches) && m.numSelected < m.limit {
m = m.selectAll()
} else {
m = m.deselectAll()
}
default:
m.textinput, cmd = m.textinput.Update(msg)
// 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
@ -222,61 +354,91 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// A character was entered, this likely means that the text input has
// changed. This suggests that the matches are outdated, so update them.
var choices []string
if !m.strict {
choices = append(choices, m.textinput.Value())
}
choices = append(choices, m.filteringChoices...)
if m.fuzzy {
if m.sort {
m.matches = fuzzy.Find(m.textinput.Value(), m.choices)
m.matches = fuzzy.Find(m.textinput.Value(), choices)
} else {
m.matches = fuzzy.FindNoSort(m.textinput.Value(), m.choices)
m.matches = fuzzy.FindNoSort(m.textinput.Value(), choices)
}
} else {
m.matches = exactMatches(m.textinput.Value(), m.choices)
m.matches = exactMatches(m.textinput.Value(), 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)
m.matches = matchAll(m.filteringChoices)
}
// 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)
m.viewport.YOffset = ordered.Clamp(len(m.matches)-yOffsetFromBottom, 0, maxYOffset)
}
}
}
m.keymap.FocusInSearch.SetEnabled(!m.textinput.Focused())
m.keymap.FocusOutSearch.SetEnabled(m.textinput.Focused())
m.keymap.NUp.SetEnabled(!m.textinput.Focused())
m.keymap.NDown.SetEnabled(!m.textinput.Focused())
m.keymap.Home.SetEnabled(!m.textinput.Focused())
m.keymap.End.SetEnabled(!m.textinput.Focused())
// It's possible that filtering items have caused fewer matches. So, ensure
// that the selected index is within the bounds of the number of matches.
m.cursor = clamp(0, len(m.matches)-1, m.cursor)
return m, cmd
m.cursor = ordered.Clamp(m.cursor, 0, len(m.matches)-1)
return m, tea.Batch(cmd, icmd)
}
func (m *model) CursorUp() {
if m.reverse {
m.cursor = clamp(0, len(m.matches)-1, m.cursor+1)
if len(m.matches) == 0 {
return
}
if m.reverse { //nolint:nestif
m.cursor = (m.cursor + 1) % len(m.matches)
if len(m.matches)-m.cursor <= m.viewport.YOffset {
m.viewport.SetYOffset(len(m.matches) - m.cursor - 1)
m.viewport.ScrollUp(1)
}
if len(m.matches)-m.cursor > m.viewport.Height+m.viewport.YOffset {
m.viewport.SetYOffset(len(m.matches) - m.viewport.Height)
}
} else {
m.cursor = clamp(0, len(m.matches)-1, m.cursor-1)
m.cursor = (m.cursor - 1 + len(m.matches)) % len(m.matches)
if m.cursor < m.viewport.YOffset {
m.viewport.SetYOffset(m.cursor)
m.viewport.ScrollUp(1)
}
if m.cursor >= m.viewport.YOffset+m.viewport.Height {
m.viewport.SetYOffset(len(m.matches) - m.viewport.Height)
}
}
}
func (m *model) CursorDown() {
if m.reverse {
m.cursor = clamp(0, len(m.matches)-1, m.cursor-1)
if len(m.matches) == 0 {
return
}
if m.reverse { //nolint:nestif
m.cursor = (m.cursor - 1 + len(m.matches)) % len(m.matches)
if len(m.matches)-m.cursor > m.viewport.Height+m.viewport.YOffset {
m.viewport.LineDown(1)
m.viewport.ScrollDown(1)
}
if len(m.matches)-m.cursor <= m.viewport.YOffset {
m.viewport.GotoTop()
}
} else {
m.cursor = clamp(0, len(m.matches)-1, m.cursor+1)
m.cursor = (m.cursor + 1) % len(m.matches)
if m.cursor >= m.viewport.YOffset+m.viewport.Height {
m.viewport.LineDown(1)
m.viewport.ScrollDown(1)
}
if m.cursor < m.viewport.YOffset {
m.viewport.GotoTop()
}
}
}
@ -291,6 +453,26 @@ func (m *model) ToggleSelection() {
}
}
func (m model) selectAll() model {
for i := range m.matches {
if m.numSelected >= m.limit {
break // do not exceed given limit
}
if _, ok := m.selected[m.matches[i].Str]; ok {
continue
}
m.selected[m.matches[i].Str] = struct{}{}
m.numSelected++
}
return m
}
func (m model) deselectAll() model {
m.selected = make(map[string]struct{})
m.numSelected = 0
return m
}
func matchAll(options []string) []fuzzy.Match {
matches := make([]fuzzy.Match, len(options))
for i, option := range options {
@ -322,20 +504,46 @@ func exactMatches(search string, choices []string) []fuzzy.Match {
return matches
}
//nolint:unparam
func clamp(min, max, val int) int {
if val < min {
return min
func matchedRanges(in []int) [][2]int {
if len(in) == 0 {
return [][2]int{}
}
if val > max {
return max
current := [2]int{in[0], in[0]}
if len(in) == 1 {
return [][2]int{current}
}
return val
var out [][2]int
for i := 1; i < len(in); i++ {
if in[i] == current[1]+1 {
current[1] = in[i]
} else {
out = append(out, current)
current = [2]int{in[i], in[i]}
}
}
out = append(out, current)
return out
}
func max(a, b int) int {
if a > b {
return a
func bytePosToVisibleCharPos(str string, rng [2]int) (int, int) {
bytePos, byteStart, byteStop := 0, rng[0], rng[1]
pos, start, stop := 0, 0, 0
gr := uniseg.NewGraphemes(str)
for byteStart > bytePos {
if !gr.Next() {
break
}
bytePos += len(gr.Str())
pos += max(1, gr.Width())
}
return b
start = pos
for byteStop > bytePos {
if !gr.Next() {
break
}
bytePos += len(gr.Str())
pos += max(1, gr.Width())
}
stop = pos
return start, stop
}

59
filter/filter_test.go Normal file
View file

@ -0,0 +1,59 @@
package filter
import (
"reflect"
"testing"
"github.com/charmbracelet/x/ansi"
)
func TestMatchedRanges(t *testing.T) {
for name, tt := range map[string]struct {
in []int
out [][2]int
}{
"empty": {
in: []int{},
out: [][2]int{},
},
"one char": {
in: []int{1},
out: [][2]int{{1, 1}},
},
"2 char range": {
in: []int{1, 2},
out: [][2]int{{1, 2}},
},
"multiple char range": {
in: []int{1, 2, 3, 4, 5, 6},
out: [][2]int{{1, 6}},
},
"multiple char ranges": {
in: []int{1, 2, 3, 5, 6, 10, 11, 12, 13, 23, 24, 40, 42, 43, 45, 52},
out: [][2]int{{1, 3}, {5, 6}, {10, 13}, {23, 24}, {40, 40}, {42, 43}, {45, 45}, {52, 52}},
},
} {
t.Run(name, func(t *testing.T) {
match := matchedRanges(tt.in)
if !reflect.DeepEqual(match, tt.out) {
t.Errorf("expected %v, got %v", tt.out, match)
}
})
}
}
func TestByteToChar(t *testing.T) {
stStr := "\x1b[90m\ue615\x1b[39m \x1b[3m\x1b[32mDow\x1b[0m\x1b[90m\x1b[39m\x1b[3wnloads"
str := " Downloads"
rng := [2]int{4, 7}
expect := "Dow"
if got := str[rng[0]:rng[1]]; got != expect {
t.Errorf("expected %q, got %q", expect, got)
}
start, stop := bytePosToVisibleCharPos(str, rng)
if got := ansi.Strip(ansi.Cut(stStr, start, stop)); got != expect {
t.Errorf("expected %+q, got %+q", expect, got)
}
}

View file

@ -15,12 +15,14 @@ type Options struct {
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"`
Selected []string `help:"Options that should start as selected (selects all if given *)" default:"" env:"GUM_FILTER_SELECTED"`
ShowHelp bool `help:"Show help keybinds" default:"true" negatable:"" env:"GUM_FILTER_SHOW_HELP"`
Strict bool `help:"Only returns if anything matched. Otherwise return Filter" negatable:"" 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_"`
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=99" 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_"`
@ -29,11 +31,18 @@ type Options struct {
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"`
Width int `help:"Input width" default:"0" 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"`
Fuzzy bool `help:"Enable fuzzy matching; otherwise match from start of word" default:"true" env:"GUM_FILTER_FUZZY" negatable:""`
FuzzySort bool `help:"Sort fuzzy results by their scores" default:"true" env:"GUM_FILTER_FUZZY_SORT" negatable:""`
Timeout time.Duration `help:"Timeout until filter command aborts" default:"0s" env:"GUM_FILTER_TIMEOUT"`
InputDelimiter string `help:"Option delimiter when reading from STDIN" default:"\n" env:"GUM_FILTER_INPUT_DELIMITER"`
OutputDelimiter string `help:"Option delimiter when writing to STDOUT" default:"\n" env:"GUM_FILTER_OUTPUT_DELIMITER"`
StripANSI bool `help:"Strip ANSI sequences when reading from STDIN" default:"true" negatable:"" env:"GUM_FILTER_STRIP_ANSI"`
Padding string `help:"Padding" default:"${defaultPadding}" group:"Style Flags" env:"GUM_FILTER_PADDING"`
// Deprecated: use [FuzzySort]. This will be removed at some point.
Sort bool `help:"Sort fuzzy results by their scores" default:"true" env:"GUM_FILTER_FUZZY_SORT" negatable:"" hidden:""`
}

12
flake.lock generated
View file

@ -5,11 +5,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1715447595,
"narHash": "sha256-VsVAUQOj/cS1LCOmMjAGeRksXIAdPnFIjCQ0XLkCsT0=",
"lastModified": 1737062831,
"narHash": "sha256-Tbk1MZbtV2s5aG+iM99U8FqwxU/YNArMcWAv6clcsBc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "062ca2a9370a27a35c524dc82d540e6e9824b652",
"rev": "5df43628fdf08d642be8ba5b3625a6c70731c19c",
"type": "github"
},
"original": {

View file

@ -7,7 +7,7 @@ Four different parse-able formats exist:
1. [Markdown](#markdown)
2. [Code](#code)
3. [Template](#template)
3. [Emoji](#emoji)
4. [Emoji](#emoji)
## Markdown
@ -43,7 +43,7 @@ Render styled input from a string template. Templating is handled by
```bash
gum format --type template '{{ Bold "Tasty" }} {{ Italic "Bubble" }} {{ Color "99" "0" " Gum " }}'
# Or, via stdin
echo '{{ Bold "Tasty" }} {{ Italic "Bubble" }} {{ Color "99" "0" " Gum " }}' | gum format --type template
echo '{{ Bold "Tasty" }} {{ Italic "Bubble" }} {{ Color "99" "0" " Gum " }}' | gum format --type template
```
## Emoji
@ -55,5 +55,30 @@ Emoji](https://github.com/yuin/goldmark-emoji)
```bash
gum format --type emoji 'I :heart: Bubble Gum :candy:'
# You know the drill, also via stdin
echo 'I :heart: Bubble Gum :candy:' | gum format --type emoji
echo 'I :heart: Bubble Gum :candy:' | gum format --type emoji
```
## Tables
Tables are rendered using [Glamour](https://github.com/charmbracelet/glamour).
| 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 |

View file

@ -24,7 +24,7 @@ func (o Options) Run() error {
if len(o.Template) > 0 {
input = strings.Join(o.Template, "\n")
} else {
input, _ = stdin.Read()
input, _ = stdin.Read(stdin.StripANSI(o.StripANSI))
}
switch o.Type {

View file

@ -6,5 +6,7 @@ type Options struct {
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"`
StripANSI bool `help:"Strip ANSI sequences when reading from STDIN" default:"true" negatable:"" env:"GUM_FORMAT_STRIP_ANSI"`
Type string `help:"Format to use (markdown,template,code,emoji)" enum:"markdown,template,code,emoji" short:"t" default:"markdown" env:"GUM_FORMAT_TYPE"`
}

63
go.mod
View file

@ -1,22 +1,26 @@
module github.com/charmbracelet/gum
go 1.21
go 1.24.2
require (
github.com/alecthomas/kong v0.9.0
github.com/Masterminds/semver/v3 v3.4.0
github.com/alecthomas/kong v1.14.0
github.com/alecthomas/mango-kong v0.1.0
github.com/charmbracelet/bubbles v0.18.0
github.com/charmbracelet/bubbletea v0.26.7-0.20240716165615-7d708384a105
github.com/charmbracelet/glamour v0.7.0
github.com/charmbracelet/huh v0.5.2
github.com/charmbracelet/lipgloss v0.12.1
github.com/charmbracelet/log v0.4.0
github.com/charmbracelet/x/ansi v0.1.4
github.com/charmbracelet/x/term v0.1.1
github.com/muesli/reflow v0.3.0
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/charmbracelet/log v0.4.2
github.com/charmbracelet/x/ansi v0.11.6
github.com/charmbracelet/x/editor v0.2.0
github.com/charmbracelet/x/exp/ordered v0.1.0
github.com/charmbracelet/x/term v0.2.2
github.com/charmbracelet/x/xpty v0.1.3
github.com/muesli/roff v0.1.0
github.com/muesli/termenv v0.15.2
github.com/muesli/termenv v0.16.0
github.com/rivo/uniseg v0.4.7
github.com/sahilm/fuzzy v0.1.1
golang.org/x/text v0.34.0
)
require (
@ -24,32 +28,35 @@ require (
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-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/input v0.1.3 // indirect
github.com/charmbracelet/x/windows v0.1.2 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/conpty v0.1.1 // indirect
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/creack/pty v1.1.24 // 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/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/microcosm-cc/bluemonday v1.0.26 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // 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/muesli/reflow v0.3.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.2 // indirect
github.com/yuin/goldmark-emoji v1.0.2 // indirect
github.com/yuin/goldmark v1.7.8 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.32.0 // indirect
)

137
go.sum
View file

@ -1,45 +1,67 @@
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA=
github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os=
github.com/alecthomas/kong v1.14.0 h1:gFgEUZWu2ZmZ+UhyZ1bDhuutbKN1nTtJTwh19Wsn21s=
github.com/alecthomas/kong v1.14.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
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.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/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/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
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/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.7-0.20240716165615-7d708384a105 h1:ye4X1GMrzY6ebvZeUB9bgyxreb5xxa5o9Kd/Y/auxFs=
github.com/charmbracelet/bubbletea v0.26.7-0.20240716165615-7d708384a105/go.mod h1:gw7FxN8J9u7IAlwc1ab1GnbfOMGExC9iI0e1t2SHs6I=
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.5.2 h1:ofeNkJ4iaFnzv46Njhx896DzLUe/j0L2QAf8znwzX4c=
github.com/charmbracelet/huh v0.5.2/go.mod h1:Sf7dY0oAn6N/e3sXJFtFX9hdQLrUdO3z7AYollG9bAM=
github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs=
github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM=
github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a h1:k/s6UoOSVynWiw7PlclyGO2VdVs5ZLbMIHiGp4shFZE=
github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a/go.mod h1:YBotIGhfoWhHDlnUpJMkjebGV2pdGRCn1Y4/Nk/vVcU=
github.com/charmbracelet/x/input v0.1.3 h1:oy4TMhyGQsYs/WWJwu1ELUMFnjiUAXwtDf048fHbCkg=
github.com/charmbracelet/x/input v0.1.3/go.mod h1:1gaCOyw1KI9e2j00j/BBZ4ErzRZqa05w0Ghn83yIhKU=
github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw=
github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg=
github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
github.com/charmbracelet/x/editor v0.2.0 h1:7XLUKtaRaB8jN7bWU2p2UChiySyaAuIfYiIRg8gGWwk=
github.com/charmbracelet/x/editor v0.2.0/go.mod h1:p3oQ28TSL3YPd+GKJ1fHWcp+7bVGpedHpXmo0D6t1dY=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
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.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
@ -56,20 +78,17 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq
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/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/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/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
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=
@ -80,10 +99,8 @@ 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.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/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
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=
@ -92,26 +109,26 @@ 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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.7.2 h1:NjGd7lO7zrUn/A7eKwn5PEOt4ONYGqpxSEeZuduvgxc=
github.com/yuin/goldmark v1.7.2/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=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

13
gum.go
View file

@ -17,6 +17,7 @@ import (
"github.com/charmbracelet/gum/spin"
"github.com/charmbracelet/gum/style"
"github.com/charmbracelet/gum/table"
"github.com/charmbracelet/gum/version"
"github.com/charmbracelet/gum/write"
)
@ -133,7 +134,7 @@ type Gum struct {
// │ 7 │ │
// │ 8 │ │
// ╰────────────────────────────────────────────────╯
// ↑/↓: Navigate • q: Quit
// ↓↑: navigate • q: quit
//
Pager pager.Options `cmd:"" help:"Scroll through a file"`
@ -214,4 +215,14 @@ type Gum struct {
// $ gum log --level info "Hello, world!"
//
Log log.Options `cmd:"" help:"Log messages to output"`
// VersionCheck provides a command that checks if the current gum version
// matches a given semantic version constraint.
//
// It can be used to check that a minimum gum version is installed in a
// script.
//
// $ gum version-check '~> 0.15'
//
VersionCheck version.Options `cmd:"" help:"Semver check current gum version"`
}

View file

@ -1,70 +1,79 @@
package input
import (
"errors"
"fmt"
"os"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/gum/cursor"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/internal/timeout"
"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 {
var value string
if o.Value != "" {
value = o.Value
} else if in, _ := stdin.Read(); in != "" {
value = in
if o.Value == "" {
if in, _ := stdin.Read(stdin.StripANSI(o.StripANSI)); in != "" {
o.Value = in
}
}
theme := huh.ThemeCharm()
theme.Focused.Base = lipgloss.NewStyle()
theme.Focused.TextInput.Cursor = o.CursorStyle.ToLipgloss()
theme.Focused.TextInput.Placeholder = o.PlaceholderStyle.ToLipgloss()
theme.Focused.TextInput.Prompt = o.PromptStyle.ToLipgloss()
theme.Focused.Title = o.HeaderStyle.ToLipgloss()
// Keep input keymap backwards compatible
keymap := huh.NewDefaultKeyMap()
keymap.Quit = key.NewBinding(key.WithKeys("ctrl+c", "esc"))
var echoMode huh.EchoMode
i := textinput.New()
if o.Value != "" {
i.SetValue(o.Value)
} else if in, _ := stdin.Read(stdin.StripANSI(o.StripANSI)); in != "" {
i.SetValue(in)
}
i.Focus()
i.Prompt = o.Prompt
i.Placeholder = o.Placeholder
i.Width = o.Width
i.PromptStyle = o.PromptStyle.ToLipgloss()
i.PlaceholderStyle = o.PlaceholderStyle.ToLipgloss()
i.Cursor.Style = o.CursorStyle.ToLipgloss()
i.Cursor.SetMode(cursor.Modes[o.CursorMode])
i.CharLimit = o.CharLimit
if o.Password {
echoMode = huh.EchoModePassword
} else {
echoMode = huh.EchoModeNormal
i.EchoMode = textinput.EchoPassword
i.EchoCharacter = '•'
}
err := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Prompt(o.Prompt).
Placeholder(o.Placeholder).
CharLimit(o.CharLimit).
EchoMode(echoMode).
Title(o.Header).
Value(&value),
),
).
WithShowHelp(false).
WithWidth(o.Width).
WithTheme(theme).
WithKeyMap(keymap).
WithShowHelp(o.ShowHelp).
WithProgramOptions(tea.WithOutput(os.Stderr)).
Run()
top, right, bottom, left := style.ParsePadding(o.Padding)
m := model{
textinput: i,
header: o.Header,
headerStyle: o.HeaderStyle.ToLipgloss(),
padding: []int{top, right, bottom, left},
autoWidth: o.Width < 1,
showHelp: o.ShowHelp,
help: help.New(),
keymap: defaultKeymap(),
}
ctx, cancel := timeout.Context(o.Timeout)
defer cancel()
p := tea.NewProgram(
m,
tea.WithOutput(os.Stderr),
tea.WithReportFocus(),
tea.WithContext(ctx),
)
tm, err := p.Run()
if err != nil {
return err
return fmt.Errorf("failed to run input: %w", err)
}
fmt.Println(value)
m = tm.(model)
if !m.submitted {
return errors.New("not submitted")
}
fmt.Println(m.textinput.Value())
return nil
}

100
input/input.go Normal file
View file

@ -0,0 +1,100 @@
// 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/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type keymap textinput.KeyMap
func defaultKeymap() keymap {
k := textinput.DefaultKeyMap
return keymap(k)
}
// FullHelp implements help.KeyMap.
func (k keymap) FullHelp() [][]key.Binding { return nil }
// ShortHelp implements help.KeyMap.
func (k keymap) ShortHelp() []key.Binding {
return []key.Binding{
key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "submit"),
),
}
}
type model struct {
autoWidth bool
header string
padding []int
headerStyle lipgloss.Style
textinput textinput.Model
quitting bool
submitted bool
showHelp bool
help help.Model
keymap keymap
}
func (m model) Init() tea.Cmd { return textinput.Blink }
func (m model) View() string {
if m.quitting {
return ""
}
var parts []string
if m.header != "" {
parts = append(parts, m.headerStyle.Render(m.header))
}
parts = append(parts, m.textinput.View())
if m.showHelp {
parts = append(parts, "", m.help.View(m.keymap))
}
return lipgloss.NewStyle().
Padding(m.padding...).
Render(lipgloss.JoinVertical(
lipgloss.Top,
parts...,
))
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
if m.autoWidth {
m.textinput.Width = msg.Width - 1 -
lipgloss.Width(m.textinput.Prompt) -
m.padding[1] - m.padding[3]
}
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
m.quitting = true
return m, tea.Interrupt
case "esc":
m.quitting = true
return m, tea.Quit
case "enter":
m.quitting = true
m.submitted = true
return m, tea.Quit
}
}
var cmd tea.Cmd
m.textinput, cmd = m.textinput.Update(msg)
return m, cmd
}

View file

@ -16,10 +16,12 @@ type Options struct {
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"`
Width int `help:"Input width (0 for terminal width)" default:"0" env:"GUM_INPUT_WIDTH"`
Password bool `help:"Mask input characters" default:"false"`
ShowHelp bool `help:"Show help keybinds" default:"true" negatable:"true" env:"GUM_INPUT_SHOW_HELP"`
ShowHelp bool `help:"Show help keybinds" default:"true" negatable:"" env:"GUM_INPUT_SHOW_HELP"`
Header string `help:"Header value" default:"" env:"GUM_INPUT_HEADER"`
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_INPUT_HEADER_"`
Timeout time.Duration `help:"Timeout until input aborts" default:"0" env:"GUM_INPUT_TIMEOUT"`
Timeout time.Duration `help:"Timeout until input aborts" default:"0s" env:"GUM_INPUT_TIMEOUT"`
StripANSI bool `help:"Strip ANSI sequences when reading from STDIN" default:"true" negatable:"" env:"GUM_INPUT_STRIP_ANSI"`
Padding string `help:"Padding" default:"${defaultPadding}" group:"Style Flags" env:"GUM_INPUT_PADDING"`
}

View file

@ -1,3 +1,4 @@
// Package decode position strings to lipgloss.
package decode
import "github.com/charmbracelet/lipgloss"

View file

@ -1,9 +1,16 @@
// Package exit code implementation.
package exit
import "fmt"
import "strconv"
// StatusTimeout is the exit code for timed out commands.
const StatusTimeout = 124
// StatusAborted is the exit code for aborted commands.
const StatusAborted = 130
// ErrAborted is the error to return when a gum command is aborted by Ctrl + C.
var ErrAborted = fmt.Errorf("aborted")
// ErrExit is a custom exit error.
type ErrExit int
// Error implements error.
func (e ErrExit) Error() string { return "exit " + strconv.Itoa(int(e)) }

View file

@ -1,3 +1,4 @@
// Package files handles files.
package files
import (
@ -18,7 +19,6 @@ func List() []string {
files = append(files, path)
return nil
})
if err != nil {
return []string{}
}

View file

@ -1,8 +0,0 @@
package log
import "fmt"
// Error prints an error message to the user.
func Error(message string) {
fmt.Println("Error:", message)
}

View file

@ -1,26 +0,0 @@
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

@ -1,3 +1,4 @@
// Package stdin handles processing input from stdin.
package stdin
import (
@ -6,18 +7,58 @@ import (
"io"
"os"
"strings"
"github.com/charmbracelet/x/ansi"
)
type options struct {
ansiStrip bool
singleLine bool
}
// Option is a read option.
type Option func(*options)
// StripANSI optionally strips ansi sequences.
func StripANSI(b bool) Option {
return func(o *options) {
o.ansiStrip = b
}
}
// SingleLine reads a single line.
func SingleLine(b bool) Option {
return func(o *options) {
o.singleLine = b
}
}
// Read reads input from an stdin pipe.
func Read() (string, error) {
func Read(opts ...Option) (string, error) {
if IsEmpty() {
return "", fmt.Errorf("stdin is empty")
}
options := options{}
for _, opt := range opts {
opt(&options)
}
reader := bufio.NewReader(os.Stdin)
var b strings.Builder
for {
if options.singleLine {
line, _, err := reader.ReadLine()
if err != nil {
return "", fmt.Errorf("failed to read line: %w", err)
}
_, err = b.Write(line)
if err != nil {
return "", fmt.Errorf("failed to write: %w", err)
}
}
for !options.singleLine {
r, _, err := reader.ReadRune()
if err != nil && err == io.EOF {
break
@ -28,7 +69,11 @@ func Read() (string, error) {
}
}
return strings.TrimSuffix(b.String(), "\n"), nil
s := strings.TrimSpace(b.String())
if options.ansiStrip {
return ansi.Strip(s), nil
}
return s, nil
}
// IsEmpty returns whether stdin is empty.

View file

@ -0,0 +1,16 @@
// Package timeout handles context timeouts.
package timeout
import (
"context"
"time"
)
// Context setup a new context that times out if the given timeout is > 0.
func Context(timeout time.Duration) (context.Context, context.CancelFunc) {
ctx := context.Background()
if timeout == 0 {
return ctx, func() {}
}
return context.WithTimeout(ctx, timeout)
}

24
internal/tty/tty.go Normal file
View file

@ -0,0 +1,24 @@
// Package tty provides tty-aware printing.
package tty
import (
"fmt"
"os"
"sync"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/term"
)
var isTTY = sync.OnceValue(func() bool {
return term.IsTerminal(os.Stdout.Fd())
})
// Println handles println, striping ansi sequences if stdout is not a tty.
func Println(s string) {
if isTTY() {
fmt.Println(s)
return
}
fmt.Println(ansi.Strip(s))
}

View file

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

View file

@ -1,3 +1,4 @@
// Package log the log command.
package log
import (
@ -16,7 +17,7 @@ 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)
f, err := os.OpenFile(o.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.ModePerm) //nolint:gosec
if err != nil {
return fmt.Errorf("error opening file: %w", err)
}
@ -28,6 +29,13 @@ func (o Options) Run() error {
l.SetPrefix(o.Prefix)
l.SetLevel(-math.MaxInt32) // log all levels
l.SetReportTimestamp(o.Time != "")
if o.MinLevel != "" {
lvl, err := log.ParseLevel(o.MinLevel)
if err != nil {
return err //nolint:wrapcheck
}
l.SetLevel(lvl)
}
timeFormats := map[string]string{
"layout": time.Layout,

View file

@ -16,7 +16,9 @@ type Options struct {
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
MinLevel string `help:"Minimal level to show" default:"" env:"GUM_LOG_LEVEL"`
LevelStyle style.Styles `embed:"" prefix:"level." help:"The style of the level being used" set:"defaultBold=true" envprefix:"GUM_LOG_LEVEL_"`
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_"`

19
main.go
View file

@ -1,3 +1,4 @@
// Package main is Gum: a tool for glamorous shell scripts.
package main
import (
@ -7,11 +8,10 @@ import (
"runtime/debug"
"github.com/alecthomas/kong"
"github.com/charmbracelet/huh"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/exit"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"github.com/charmbracelet/gum/internal/exit"
)
const shaLen = 7
@ -55,6 +55,7 @@ func main() {
}),
kong.Vars{
"version": version,
"versionNumber": Version,
"defaultHeight": "0",
"defaultWidth": "0",
"defaultAlign": "left",
@ -73,10 +74,18 @@ func main() {
},
)
if err := ctx.Run(); err != nil {
if errors.Is(err, exit.ErrAborted) || errors.Is(err, huh.ErrUserAborted) {
var ex exit.ErrExit
if errors.As(err, &ex) {
os.Exit(int(ex))
}
if errors.Is(err, tea.ErrInterrupted) {
os.Exit(exit.StatusAborted)
}
fmt.Println(err)
if errors.Is(err, tea.ErrProgramKilled) {
fmt.Fprintln(os.Stderr, "timed out")
os.Exit(exit.StatusTimeout)
}
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

View file

@ -1,3 +1,4 @@
// Package man the man command.
package man
import (

View file

@ -4,9 +4,11 @@ import (
"fmt"
"regexp"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/internal/timeout"
)
// Run provides a shell script interface for the viewport bubble.
@ -29,9 +31,9 @@ func (o Options) Run() error {
}
}
model := model{
m := model{
viewport: vp,
helpStyle: o.HelpStyle.ToLipgloss(),
help: help.New(),
content: o.Content,
origContent: o.Content,
showLineNumbers: o.ShowLineNumbers,
@ -39,12 +41,21 @@ func (o Options) Run() error {
softWrap: o.SoftWrap,
matchStyle: o.MatchStyle.ToLipgloss(),
matchHighlightStyle: o.MatchHighlightStyle.ToLipgloss(),
timeout: o.Timeout,
hasTimeout: o.Timeout > 0,
keymap: defaultKeymap(),
}
_, err := tea.NewProgram(model, tea.WithAltScreen()).Run()
ctx, cancel := timeout.Context(o.Timeout)
defer cancel()
_, err := tea.NewProgram(
m,
tea.WithAltScreen(),
tea.WithReportFocus(),
tea.WithContext(ctx),
).Run()
if err != nil {
return fmt.Errorf("unable to start program: %w", err)
}
return nil
}

View file

@ -10,12 +10,14 @@ import (
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"`
SoftWrap bool `help:"Soft wrap lines" default:"true" negatable:""`
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"`
Timeout time.Duration `help:"Timeout until command exits" default:"0s" env:"GUM_PAGER_TIMEOUT"`
// Deprecated: this has no effect anymore.
HelpStyle style.Styles `embed:"" prefix:"help." help:"Style the help text" set:"defaultForeground=241" envprefix:"GUM_PAGER_HELP_" hidden:""`
}

View file

@ -6,21 +6,93 @@ package pager
import (
"fmt"
"strings"
"time"
"github.com/charmbracelet/gum/timeout"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/truncate"
"github.com/charmbracelet/x/ansi"
)
type keymap struct {
Home,
End,
Search,
NextMatch,
PrevMatch,
Abort,
Quit,
ConfirmSearch,
CancelSearch key.Binding
}
// FullHelp implements help.KeyMap.
func (k keymap) FullHelp() [][]key.Binding {
return nil
}
// ShortHelp implements help.KeyMap.
func (k keymap) ShortHelp() []key.Binding {
return []key.Binding{
key.NewBinding(
key.WithKeys("up", "down"),
key.WithHelp("↓↑", "navigate"),
),
k.Quit,
k.Search,
k.NextMatch,
k.PrevMatch,
}
}
func defaultKeymap() keymap {
return keymap{
Home: key.NewBinding(
key.WithKeys("g", "home"),
key.WithHelp("h", "home"),
),
End: key.NewBinding(
key.WithKeys("G", "end"),
key.WithHelp("G", "end"),
),
Search: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("/", "search"),
),
PrevMatch: key.NewBinding(
key.WithKeys("p", "N"),
key.WithHelp("N", "previous match"),
),
NextMatch: key.NewBinding(
key.WithKeys("n"),
key.WithHelp("n", "next match"),
),
Abort: key.NewBinding(
key.WithKeys("ctrl+c"),
key.WithHelp("ctrl+c", "abort"),
),
Quit: key.NewBinding(
key.WithKeys("q", "esc"),
key.WithHelp("esc", "quit"),
),
ConfirmSearch: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "confirm"),
),
CancelSearch: key.NewBinding(
key.WithKeys("ctrl+c", "ctrl+d", "esc"),
key.WithHelp("ctrl+c", "cancel"),
),
}
}
type model struct {
content string
origContent string
viewport viewport.Model
helpStyle lipgloss.Style
help help.Model
showLineNumbers bool
lineNumberStyle lipgloss.Style
softWrap bool
@ -28,34 +100,33 @@ type model struct {
matchStyle lipgloss.Style
matchHighlightStyle lipgloss.Style
maxWidth int
timeout time.Duration
hasTimeout bool
keymap keymap
}
func (m model) Init() tea.Cmd {
return timeout.Init(m.timeout, nil)
}
func (m model) Init() tea.Cmd { return 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)
m.processText(msg)
case tea.KeyMsg:
return m.KeyHandler(msg)
return m.keyHandler(msg)
}
return m, nil
m.keymap.PrevMatch.SetEnabled(m.search.query != nil)
m.keymap.NextMatch.SetEnabled(m.search.query != nil)
var cmd tea.Cmd
m.search.input, cmd = m.search.input.Update(msg)
return m, cmd
}
func (m *model) ProcessText(msg tea.WindowSizeMsg) {
m.viewport.Height = msg.Height - lipgloss.Height(m.helpStyle.Render("?")) - 1
func (m *model) helpView() string {
return m.help.View(m.keymap)
}
func (m *model) processText(msg tea.WindowSizeMsg) {
m.viewport.Height = msg.Height - lipgloss.Height(m.helpView())
m.viewport.Width = msg.Width
textStyle := lipgloss.NewStyle().Width(m.viewport.Width)
var text strings.Builder
@ -75,17 +146,21 @@ func (m *model) ProcessText(msg tea.WindowSizeMsg) {
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(" │ "))
idx := 0
if w := ansi.StringWidth(line); m.softWrap && w > m.maxWidth {
for w > idx {
if m.showLineNumbers && idx != 0 {
text.WriteString(m.lineNumberStyle.Render(" │ "))
}
truncatedLine := ansi.Cut(line, idx, m.maxWidth+idx)
idx += m.maxWidth
text.WriteString(textStyle.Render(truncatedLine))
text.WriteString("\n")
}
line = strings.Replace(line, truncatedLine, "", 1)
} else {
text.WriteString(textStyle.Render(line))
text.WriteString("\n")
}
text.WriteString(textStyle.Render(truncate.String(line, uint(m.maxWidth))))
text.WriteString("\n")
}
diffHeight := m.viewport.Height - lipgloss.Height(text.String())
@ -98,62 +173,57 @@ func (m *model) ProcessText(msg tea.WindowSizeMsg) {
const heightOffset = 2
func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) {
func (m model) keyHandler(msg tea.KeyMsg) (model, tea.Cmd) {
km := m.keymap
var cmd tea.Cmd
if m.search.active {
switch key.String() {
case "enter":
switch {
case key.Matches(msg, km.ConfirmSearch):
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})
m.processText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
} else {
m.search.Done()
}
case "ctrl+d", "ctrl+c", "esc":
case key.Matches(msg, km.CancelSearch):
m.search.Done()
default:
m.search.input, cmd = m.search.input.Update(key)
m.search.input, cmd = m.search.input.Update(msg)
}
} else {
switch key.String() {
case "g", "home":
switch {
case key.Matches(msg, km.Home):
m.viewport.GotoTop()
case "G", "end":
case key.Matches(msg, km.End):
m.viewport.GotoBottom()
case "/":
case key.Matches(msg, km.Search):
m.search.Begin()
case "p", "N":
return m, textinput.Blink
case key.Matches(msg, km.PrevMatch):
m.search.PrevMatch(&m)
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
case "n":
m.processText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
case key.Matches(msg, km.NextMatch):
m.search.NextMatch(&m)
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
case "q", "ctrl+c", "esc":
m.processText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
case key.Matches(msg, km.Quit):
return m, tea.Quit
case key.Matches(msg, km.Abort):
return m, tea.Interrupt
}
m.viewport, cmd = m.viewport.Update(key)
m.viewport, cmd = m.viewport.Update(msg)
}
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() + "\n " + m.search.input.View()
}
return m.viewport.View() + m.helpStyle.Render(helpMsg)
return m.viewport.View() + "\n" + m.helpView()
}

View file

@ -6,9 +6,8 @@ import (
"strings"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/gum/internal/utils"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/truncate"
"github.com/charmbracelet/x/ansi"
)
type search struct {
@ -52,7 +51,7 @@ func (s *search) Execute(m *model) {
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)
leftPad, _ := 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 {
@ -82,7 +81,7 @@ func (s *search) NextMatch(m *model) {
return
}
leftPad, rightPad := utils.LipglossPadding(m.matchStyle)
leftPad, rightPad := lipglossPadding(m.matchStyle)
s.matchIndex = (s.matchIndex + 1) % len(allMatches)
match := allMatches[s.matchIndex]
lhs := m.content[:match[0]]
@ -125,7 +124,7 @@ func (s *search) PrevMatch(m *model) {
s.matchIndex = len(allMatches) - 1
}
leftPad, rightPad := utils.LipglossPadding(m.matchStyle)
leftPad, rightPad := lipglossPadding(m.matchStyle)
match := allMatches[s.matchIndex]
lhs := m.content[:match[0]]
rhs := m.content[match[0]:]
@ -150,15 +149,27 @@ func (s *search) PrevMatch(m *model) {
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)
idx := 0
if w := ansi.StringWidth(line); softWrap && w > maxWidth {
for w > idx {
truncatedLine := ansi.Cut(line, idx, maxWidth+idx)
idx += maxWidth
text.WriteString(truncatedLine)
text.WriteString("\n")
}
} else {
text.WriteString(line)
text.WriteString("\n")
line = strings.Replace(line, truncatedLine, "", 1)
}
text.WriteString(truncate.String(line, uint(maxWidth)))
text.WriteString("\n")
}
return text.String()
}
// 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
}

View file

@ -6,64 +6,77 @@ import (
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/x/term"
"github.com/charmbracelet/gum/internal/exit"
"github.com/charmbracelet/gum/internal/timeout"
"github.com/charmbracelet/gum/style"
"github.com/charmbracelet/x/term"
)
// Run provides a shell script interface for the spinner bubble.
// https://github.com/charmbracelet/bubbles/spinner
func (o Options) Run() error {
isTTY := term.IsTerminal(os.Stdout.Fd())
isOutTTY := term.IsTerminal(os.Stdout.Fd())
isErrTTY := term.IsTerminal(os.Stderr.Fd())
s := spinner.New()
s.Style = o.SpinnerStyle.ToLipgloss()
s.Spinner = spinnerMap[o.Spinner]
top, right, bottom, left := style.ParsePadding(o.Padding)
m := model{
spinner: s,
title: o.TitleStyle.ToLipgloss().Render(o.Title),
command: o.Command,
align: o.Align,
showOutput: o.ShowOutput && isTTY,
showStdout: (o.ShowOutput || o.ShowStdout) && isOutTTY,
showStderr: (o.ShowOutput || o.ShowStderr) && isErrTTY,
showError: o.ShowError,
timeout: o.Timeout,
hasTimeout: o.Timeout > 0,
isTTY: isErrTTY,
padding: []int{top, right, bottom, left},
}
p := tea.NewProgram(m, tea.WithOutput(os.Stderr))
mm, err := p.Run()
m = mm.(model)
ctx, cancel := timeout.Context(o.Timeout)
defer cancel()
tm, err := tea.NewProgram(
m,
tea.WithOutput(os.Stderr),
tea.WithContext(ctx),
tea.WithInput(nil),
).Run()
if err != nil {
return fmt.Errorf("failed to run spin: %w", err)
}
if m.aborted {
return exit.ErrAborted
return fmt.Errorf("unable to run action: %w", err)
}
m = tm.(model)
// If the command succeeds, and we are printing output and we are in a TTY then push the STDOUT we got to the actual
// STDOUT for piping or other things.
//nolint:nestif
if m.status == 0 {
if o.ShowOutput {
// BubbleTea writes the View() to stderr.
// If the program is being piped then put the accumulated output in stdout.
if !isTTY {
_, err := os.Stdout.WriteString(m.stdout)
if err != nil {
return fmt.Errorf("failed to write to stdout: %w", err)
}
if m.err != nil {
if _, err := fmt.Fprintf(os.Stderr, "%s\n", m.err.Error()); err != nil {
return fmt.Errorf("failed to write to stdout: %w", err)
}
return exit.ErrExit(1)
} else if m.status == 0 {
var output string
if o.ShowOutput || (o.ShowStdout && o.ShowStderr) {
output = m.output
} else if o.ShowStdout {
output = m.stdout
} else if o.ShowStderr {
output = m.stderr
}
if output != "" {
if _, err := os.Stdout.WriteString(output); 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 {
if _, err := os.Stdout.WriteString(m.output); err != nil {
return fmt.Errorf("failed to write to stdout: %w", err)
}
}
os.Exit(m.status)
return nil
return exit.ErrExit(m.status)
}

View file

@ -10,12 +10,15 @@ import (
type Options struct {
Command []string `arg:"" help:"Command to run"`
ShowOutput bool `help:"Show or pipe output of command during execution" default:"false" env:"GUM_SPIN_SHOW_OUTPUT"`
ShowOutput bool `help:"Show or pipe output of command during execution (shows both STDOUT and STDERR)" 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"`
ShowStdout bool `help:"Show STDOUT output" default:"false" env:"GUM_SPIN_SHOW_STDOUT"`
ShowStderr bool `help:"Show STDERR errput" default:"false" env:"GUM_SPIN_SHOW_STDERR"`
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"`
Timeout time.Duration `help:"Timeout until spin command aborts" default:"0s" env:"GUM_SPIN_TIMEOUT"`
Padding string `help:"Padding" default:"${defaultPadding}" group:"Style Flags" env:"GUM_SPIN_PADDING"`
}

22
spin/pty.go Normal file
View file

@ -0,0 +1,22 @@
package spin
import (
"os"
"github.com/charmbracelet/x/term"
"github.com/charmbracelet/x/xpty"
)
func openPty(f *os.File) (pty xpty.Pty, err error) {
width, height, err := term.GetSize(f.Fd())
if err != nil {
return nil, err //nolint:wrapcheck
}
pty, err = xpty.NewPty(width, height)
if err != nil {
return nil, err //nolint:wrapcheck
}
return pty, nil
}

View file

@ -15,43 +15,49 @@
package spin
import (
"bytes"
"context"
"io"
"os"
"os/exec"
"strings"
"time"
"github.com/charmbracelet/gum/internal/exit"
"github.com/charmbracelet/gum/timeout"
"github.com/charmbracelet/x/term"
"runtime"
"syscall"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/term"
"github.com/charmbracelet/x/xpty"
)
type model struct {
spinner spinner.Model
title string
padding []int
align string
command []string
quitting bool
aborted bool
isTTY bool
status int
stdout string
stderr string
output string
showOutput bool
showStdout bool
showStderr bool
showError bool
timeout time.Duration
hasTimeout bool
err error
}
var (
bothbuf strings.Builder
outbuf strings.Builder
errbuf strings.Builder
bothbuf bytes.Buffer
outbuf bytes.Buffer
errbuf bytes.Buffer
executing *exec.Cmd
)
type errorMsg error
type finishCommandMsg struct {
stdout string
stderr string
@ -65,22 +71,54 @@ func commandStart(command []string) tea.Cmd {
if len(command) > 1 {
args = command[1:]
}
cmd := exec.Command(command[0], args...) //nolint:gosec
if term.IsTerminal(os.Stdout.Fd()) {
stdout := io.MultiWriter(&bothbuf, &errbuf)
stderr := io.MultiWriter(&bothbuf, &outbuf)
executing = exec.CommandContext(context.Background(), command[0], args...) //nolint:gosec
executing.Stdin = os.Stdin
cmd.Stdout = stdout
cmd.Stderr = stderr
isTerminal := term.IsTerminal(os.Stdout.Fd())
// NOTE(@andreynering): We had issues with Git Bash on Windows
// when it comes to handling PTYs, so we're falling back to
// to redirecting stdout/stderr as usual to avoid issues.
//nolint:nestif
if isTerminal && runtime.GOOS == "windows" {
executing.Stdout = io.MultiWriter(&bothbuf, &outbuf)
executing.Stderr = io.MultiWriter(&bothbuf, &errbuf)
_ = executing.Run()
} else if isTerminal {
stdoutPty, err := openPty(os.Stdout)
if err != nil {
return errorMsg(err)
}
defer stdoutPty.Close() //nolint:errcheck
stderrPty, err := openPty(os.Stderr)
if err != nil {
return errorMsg(err)
}
defer stderrPty.Close() //nolint:errcheck
if outUnixPty, isOutUnixPty := stdoutPty.(*xpty.UnixPty); isOutUnixPty {
executing.Stdout = outUnixPty.Slave()
}
if errUnixPty, isErrUnixPty := stderrPty.(*xpty.UnixPty); isErrUnixPty {
executing.Stderr = errUnixPty.Slave()
}
go io.Copy(io.MultiWriter(&bothbuf, &outbuf), stdoutPty) //nolint:errcheck
go io.Copy(io.MultiWriter(&bothbuf, &errbuf), stderrPty) //nolint:errcheck
if err = stdoutPty.Start(executing); err != nil {
return errorMsg(err)
}
_ = xpty.WaitProcess(context.Background(), executing)
} else {
cmd.Stdout = os.Stdout
executing.Stdout = os.Stdout
executing.Stderr = os.Stderr
_ = executing.Run()
}
_ = cmd.Run()
status := cmd.ProcessState.ExitCode()
status := executing.ProcessState.ExitCode()
if status == -1 {
status = 1
}
@ -94,48 +132,50 @@ func commandStart(command []string) tea.Cmd {
}
}
func commandAbort() tea.Msg {
if executing != nil && executing.Process != nil {
_ = executing.Process.Signal(syscall.SIGINT)
}
return tea.InterruptMsg{}
}
func (m model) Init() tea.Cmd {
return tea.Batch(
m.spinner.Tick,
commandStart(m.command),
timeout.Init(m.timeout, nil),
)
}
func (m model) View() string {
if m.quitting && m.showOutput {
return strings.TrimPrefix(errbuf.String()+"\n"+outbuf.String(), "\n")
if m.quitting {
return ""
}
var str string
if m.hasTimeout {
str = timeout.Str(m.timeout)
var out string
if m.showStderr {
out += errbuf.String()
}
if m.showStdout {
out += outbuf.String()
}
if !m.isTTY {
return m.title
}
var header string
if m.align == "left" {
header = m.spinner.View() + str + " " + m.title
header = m.spinner.View() + " " + m.title
} else {
header = str + " " + m.title + " " + m.spinner.View()
header = m.title + " " + m.spinner.View()
}
if !m.showOutput {
return header
}
return header + errbuf.String() + "\n" + outbuf.String()
return lipgloss.NewStyle().
Padding(m.padding...).
Render(header, "", out)
}
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
@ -146,11 +186,15 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
m.aborted = true
return m, tea.Quit
return m, commandAbort
}
case errorMsg:
m.err = msg
m.quitting = true
return m, tea.Quit
}
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
}

7
style/ascii_a.txt Normal file
View file

@ -0,0 +1,7 @@
#
# #
# #
# #
#######
# #
# #

View file

@ -20,11 +20,18 @@ func (o Options) Run() error {
if len(o.Text) > 0 {
text = strings.Join(o.Text, "\n")
} else {
text, _ = stdin.Read()
text, _ = stdin.Read(stdin.StripANSI(o.StripANSI))
if text == "" {
return errors.New("no input provided, see `gum style --help`")
}
}
if o.Trim {
var lines []string
for _, line := range strings.Split(text, "\n") {
lines = append(lines, strings.TrimSpace(line))
}
text = strings.Join(lines, "\n")
}
fmt.Println(o.Style.ToLipgloss().Render(text))
return nil
}

View file

@ -19,7 +19,7 @@ func (s Styles) ToLipgloss() lipgloss.Style {
Height(s.Height).
Width(s.Width).
Margin(parseMargin(s.Margin)).
Padding(parsePadding(s.Padding)).
Padding(ParsePadding(s.Padding)).
Bold(s.Bold).
Faint(s.Faint).
Italic(s.Italic).
@ -40,7 +40,7 @@ func (s StylesNotHidden) ToLipgloss() lipgloss.Style {
Height(s.Height).
Width(s.Width).
Margin(parseMargin(s.Margin)).
Padding(parsePadding(s.Padding)).
Padding(ParsePadding(s.Padding)).
Bold(s.Bold).
Faint(s.Faint).
Italic(s.Italic).

View file

@ -2,8 +2,10 @@ package style
// 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 StylesNotHidden `embed:""`
Text []string `arg:"" optional:"" help:"Text to which to apply the style"`
Trim bool `help:"Trim whitespaces on every input line" default:"false"`
StripANSI bool `help:"Strip ANSI sequences when reading from STDIN" default:"true" negatable:"" env:"GUM_STYLE_STRIP_ANSI"`
Style StylesNotHidden `embed:""`
}
// Styles is a flag set of possible styles.
@ -15,7 +17,7 @@ type Options struct {
type Styles 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" hidden:"true"`
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" hidden:"true"`

View file

@ -5,13 +5,15 @@ import (
"strings"
)
const minTokens = 1
const halfTokens = 2
const maxTokens = 4
const (
minTokens = 1
halfTokens = 2
maxTokens = 4
)
// parsePadding parses 1 - 4 integers from a string and returns them in a top,
// ParsePadding parses 1 - 4 integers from a string and returns them in a top,
// right, bottom, left order for use in the lipgloss.Padding() method.
func parsePadding(s string) (int, int, int, int) {
func ParsePadding(s string) (int, int, int, int) {
var ints [maxTokens]int
tokens := strings.Split(s, " ")
@ -46,4 +48,4 @@ func parsePadding(s string) (int, int, int, int) {
// parseMargin is an alias for parsePadding since they involve the same logic
// to parse integers to the same format.
var parseMargin = parsePadding
var parseMargin = ParsePadding

4
table/bom.csv Normal file
View file

@ -0,0 +1,4 @@
"first_name","last_name","username"
"Rob","Pike",rob
Ken,Thompson,ken
"Robert","Griesemer","gri"
1 first_name last_name username
2 Rob Pike rob
3 Ken Thompson ken
4 Robert Griesemer gri

View file

@ -5,31 +5,40 @@ import (
"fmt"
"os"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/internal/timeout"
"github.com/charmbracelet/gum/style"
"github.com/charmbracelet/lipgloss"
ltable "github.com/charmbracelet/lipgloss/table"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/style"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
)
// Run provides a shell script interface for rendering tabular data (CSV).
func (o Options) Run() error {
var reader *csv.Reader
var input *os.File
if o.File != "" {
file, err := os.Open(o.File)
var err error
input, err = os.Open(o.File)
if err != nil {
return fmt.Errorf("could not find file at path %s", o.File)
return fmt.Errorf("could not render file: %w", err)
}
reader = csv.NewReader(file)
} else {
if stdin.IsEmpty() {
return fmt.Errorf("no data provided")
}
reader = csv.NewReader(os.Stdin)
input = os.Stdin
}
defer input.Close() //nolint: errcheck
transformer := unicode.BOMOverride(encoding.Nop.NewDecoder())
reader := csv.NewReader(transform.NewReader(input, transformer))
reader.LazyQuotes = o.LazyQuotes
reader.FieldsPerRecord = o.FieldsPerRecord
separatorRunes := []rune(o.Separator)
if len(separatorRunes) != 1 {
return fmt.Errorf("separator must be single character")
@ -70,6 +79,7 @@ func (o Options) Run() error {
}
defaultStyles := table.DefaultStyles()
top, right, bottom, left := style.ParsePadding(o.Padding)
styles := table.Styles{
Cell: defaultStyles.Cell.Inherit(o.CellStyle.ToLipgloss()),
@ -78,11 +88,26 @@ func (o Options) Run() error {
}
rows := make([]table.Row, 0, len(data))
for _, row := range data {
if len(row) > len(columns) {
for row := range data {
if len(data[row]) > len(columns) {
return fmt.Errorf("invalid number of columns")
}
rows = append(rows, table.Row(row))
// fixes the data in case we have more columns than rows:
for len(data[row]) < len(columns) {
data[row] = append(data[row], "")
}
for i, col := range data[row] {
if len(o.Widths) == 0 {
width := lipgloss.Width(col)
if width > columns[i].Width {
columns[i].Width = width
}
}
}
rows = append(rows, table.Row(data[row]))
}
if o.Print {
@ -102,15 +127,34 @@ func (o Options) Run() error {
return nil
}
table := table.New(
opts := []table.Option{
table.WithColumns(columns),
table.WithFocused(true),
table.WithHeight(o.Height),
table.WithRows(rows),
table.WithStyles(styles),
)
}
if o.Height > 0 {
opts = append(opts, table.WithHeight(o.Height-top-bottom))
}
tm, err := tea.NewProgram(model{table: table}, tea.WithOutput(os.Stderr)).Run()
table := table.New(opts...)
ctx, cancel := timeout.Context(o.Timeout)
defer cancel()
m := model{
table: table,
showHelp: o.ShowHelp,
hideCount: o.HideCount,
help: help.New(),
keymap: defaultKeymap(),
padding: []int{top, right, bottom, left},
}
tm, err := tea.NewProgram(
m,
tea.WithOutput(os.Stderr),
tea.WithContext(ctx),
).Run()
if err != nil {
return fmt.Errorf("failed to start tea program: %w", err)
}
@ -119,10 +163,15 @@ func (o Options) Run() error {
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)
m = tm.(model)
if o.ReturnColumn > 0 && o.ReturnColumn <= len(m.selected) {
if err = writer.Write([]string{m.selected[o.ReturnColumn-1]}); err != nil {
return fmt.Errorf("failed to write col %d of selected row: %w", o.ReturnColumn, err)
}
} else {
if err = writer.Write([]string(m.selected)); err != nil {
return fmt.Errorf("failed to write selected row: %w", err)
}
}
writer.Flush()

View file

@ -1,19 +1,30 @@
package table
import "github.com/charmbracelet/gum/style"
import (
"time"
"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"`
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:"0"`
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"`
ShowHelp bool `help:"Show help keybinds" default:"true" negatable:"" env:"GUM_TABLE_SHOW_HELP"`
HideCount bool `help:"Hide item count on help keybinds" default:"false" negatable:"" env:"GUM_TABLE_HIDE_COUNT"`
LazyQuotes bool `help:"If LazyQuotes is true, a quote may appear in an unquoted field and a non-doubled quote may appear in a quoted field" default:"false" env:"GUM_TABLE_LAZY_QUOTES"`
FieldsPerRecord int `help:"Sets the number of expected fields per record" default:"0" env:"GUM_TABLE_FIELDS_PER_RECORD"`
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_"`
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_"`
ReturnColumn int `short:"r" help:"Which column number should be returned instead of whole row as string. Default=0 returns whole Row" default:"0"`
Timeout time.Duration `help:"Timeout until choose returns selected element" default:"0s" env:"GUM_TABLE_TIMEOUT"`
Padding string `help:"Padding" default:"${defaultPadding}" group:"Style Flags" env:"GUM_TABLE_PADDING"`
}

View file

@ -15,18 +15,81 @@
package table
import (
"fmt"
"strconv"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type model struct {
table table.Model
selected table.Row
quitting bool
type keymap struct {
Navigate,
Select,
Quit,
Abort key.Binding
}
func (m model) Init() tea.Cmd {
return nil
// FullHelp implements help.KeyMap.
func (k keymap) FullHelp() [][]key.Binding { return nil }
// ShortHelp implements help.KeyMap.
func (k keymap) ShortHelp() []key.Binding {
return []key.Binding{
k.Navigate,
k.Select,
k.Quit,
}
}
func defaultKeymap() keymap {
return keymap{
Navigate: key.NewBinding(
key.WithKeys("up", "down"),
key.WithHelp("↓↑", "navigate"),
),
Select: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "select"),
),
Quit: key.NewBinding(
key.WithKeys("esc", "ctrl+q", "q"),
key.WithHelp("esc", "quit"),
),
Abort: key.NewBinding(
key.WithKeys("ctrl+c"),
key.WithHelp("ctrl+c", "abort"),
),
}
}
type model struct {
table table.Model
selected table.Row
quitting bool
showHelp bool
hideCount bool
help help.Model
keymap keymap
padding []int
}
func (m model) Init() tea.Cmd { return nil }
func (m model) countView() string {
if m.hideCount {
return ""
}
padding := strconv.Itoa(numLen(len(m.table.Rows())))
return m.help.Styles.FullDesc.Render(fmt.Sprintf(
"%"+padding+"d/%d%s",
m.table.Cursor()+1,
len(m.table.Rows()),
m.help.ShortSeparator,
))
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@ -34,14 +97,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "enter":
km := m.keymap
switch {
case key.Matches(msg, km.Select):
m.selected = m.table.SelectedRow()
m.quitting = true
return m, tea.Quit
case "ctrl+c", "q", "esc":
case key.Matches(msg, km.Quit):
m.quitting = true
return m, tea.Quit
case key.Matches(msg, km.Abort):
m.quitting = true
return m, tea.Interrupt
}
}
@ -53,5 +120,23 @@ func (m model) View() string {
if m.quitting {
return ""
}
return m.table.View()
s := m.table.View()
if m.showHelp {
s += "\n" + m.countView() + m.help.View(m.keymap)
}
return lipgloss.NewStyle().
Padding(m.padding...).
Render(s)
}
func numLen(i int) int {
if i == 0 {
return 1
}
count := 0
for i != 0 {
i /= 10
count++
}
return count
}

View file

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

26
version/command.go Normal file
View file

@ -0,0 +1,26 @@
// Package version the version command.
package version
import (
"fmt"
"github.com/Masterminds/semver/v3"
"github.com/alecthomas/kong"
)
// Run check that a given version matches a semantic version constraint.
func (o Options) Run(ctx *kong.Context) error {
c, err := semver.NewConstraint(o.Constraint)
if err != nil {
return fmt.Errorf("could not parse range %s: %w", o.Constraint, err)
}
current := ctx.Model.Vars()["versionNumber"]
v, err := semver.NewVersion(current)
if err != nil {
return fmt.Errorf("could not parse version %s: %w", current, err)
}
if !c.Check(v) {
return fmt.Errorf("gum version %q is not within given range %q", current, o.Constraint)
}
return nil
}

6
version/options.go Normal file
View file

@ -0,0 +1,6 @@
package version
// Options is the set of options that can be used with version.
type Options struct {
Constraint string `arg:"" help:"Semantic version constraint"`
}

View file

@ -1,54 +1,87 @@
package write
import (
"errors"
"fmt"
"os"
"strings"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/cursor"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/gum/internal/timeout"
"github.com/charmbracelet/gum/style"
)
// Run provides a shell script interface for the text area bubble.
// https://github.com/charmbracelet/bubbles/textarea
func (o Options) Run() error {
in, _ := stdin.Read()
in, _ := stdin.Read(stdin.StripANSI(o.StripANSI))
if in != "" && o.Value == "" {
o.Value = strings.ReplaceAll(in, "\r", "")
}
var value = o.Value
a := textarea.New()
a.Focus()
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()
a.Prompt = o.Prompt
a.Placeholder = o.Placeholder
a.ShowLineNumbers = o.ShowLineNumbers
a.CharLimit = o.CharLimit
a.MaxHeight = o.MaxLines
top, right, bottom, left := style.ParsePadding(o.Padding)
keymap := huh.NewDefaultKeyMap()
keymap.Text.NewLine.SetHelp("ctrl+j", "new line")
err := huh.NewForm(
huh.NewGroup(
huh.NewText().
Title(o.Header).
Placeholder(o.Placeholder).
CharLimit(o.CharLimit).
ShowLineNumbers(o.ShowLineNumbers).
Value(&value),
),
).
WithWidth(o.Width).
WithHeight(o.Height).
WithTheme(theme).
WithKeyMap(keymap).
WithShowHelp(o.ShowHelp).
Run()
if err != nil {
return err
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(),
}
fmt.Println(value)
a.BlurredStyle = style
a.FocusedStyle = style
a.Cursor.Style = o.CursorStyle.ToLipgloss()
a.Cursor.SetMode(cursor.Modes[o.CursorMode])
a.SetWidth(max(0, o.Width-left-right))
a.SetHeight(max(0, o.Height-top-bottom))
a.SetValue(o.Value)
m := model{
textarea: a,
header: o.Header,
headerStyle: o.HeaderStyle.ToLipgloss(),
autoWidth: o.Width < 1,
help: help.New(),
showHelp: o.ShowHelp,
keymap: defaultKeymap(),
padding: []int{top, right, bottom, left},
}
m.textarea.KeyMap.InsertNewline = m.keymap.InsertNewline
ctx, cancel := timeout.Context(o.Timeout)
defer cancel()
p := tea.NewProgram(
m,
tea.WithOutput(os.Stderr),
tea.WithReportFocus(),
tea.WithContext(ctx),
)
tm, err := p.Run()
if err != nil {
return fmt.Errorf("failed to run write: %w", err)
}
m = tm.(model)
if !m.submitted {
return errors.New("not submitted")
}
fmt.Println(m.textarea.Value())
return nil
}

View file

@ -1,29 +1,36 @@
package write
import "github.com/charmbracelet/gum/style"
import (
"time"
"github.com/charmbracelet/gum/style"
)
// Options are the customization options for the textarea.
type Options struct {
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"`
ShowHelp bool `help:"Show help key binds" negatable:"" default:"true" env:"GUM_WRITE_SHOW_HELP"`
CursorMode string `prefix:"cursor." name:"mode" help:"Cursor mode" default:"blink" enum:"blink,hide,static" env:"GUM_WRITE_CURSOR_MODE"`
Width int `help:"Text area width (0 for terminal width)" default:"0" 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:"0"`
MaxLines int `help:"Maximum number of lines (0 for no limit)" default:"0"`
ShowHelp bool `help:"Show help key binds" negatable:"" default:"true" env:"GUM_WRITE_SHOW_HELP"`
CursorMode string `prefix:"cursor." name:"mode" help:"Cursor mode" default:"blink" enum:"blink,hide,static" env:"GUM_WRITE_CURSOR_MODE"`
Timeout time.Duration `help:"Timeout until choose returns selected element" default:"0s" env:"GUM_WRITE_TIMEOUT"`
StripANSI bool `help:"Strip ANSI sequences when reading from STDIN" default:"true" negatable:"" env:"GUM_WRITE_STRIP_ANSI"`
BaseStyle style.Styles `embed:"" prefix:"base." envprefix:"GUM_WRITE_BASE_"`
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_WRITE_CURSOR_"`
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_WRITE_HEADER_"`
PlaceholderStyle style.Styles `embed:"" prefix:"placeholder." set:"defaultForeground=240" envprefix:"GUM_WRITE_PLACEHOLDER_"`
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=7" envprefix:"GUM_WRITE_PROMPT_"`
EndOfBufferStyle style.Styles `embed:"" prefix:"end-of-buffer." set:"defaultForeground=0" envprefix:"GUM_WRITE_END_OF_BUFFER_"`
LineNumberStyle style.Styles `embed:"" prefix:"line-number." set:"defaultForeground=7" envprefix:"GUM_WRITE_LINE_NUMBER_"`
BaseStyle style.Styles `embed:"" prefix:"base." envprefix:"GUM_WRITE_BASE_"`
CursorLineNumberStyle style.Styles `embed:"" prefix:"cursor-line-number." set:"defaultForeground=7" envprefix:"GUM_WRITE_CURSOR_LINE_NUMBER_"`
CursorLineStyle style.Styles `embed:"" prefix:"cursor-line." envprefix:"GUM_WRITE_CURSOR_LINE_"`
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_WRITE_CURSOR_"`
EndOfBufferStyle style.Styles `embed:"" prefix:"end-of-buffer." set:"defaultForeground=0" envprefix:"GUM_WRITE_END_OF_BUFFER_"`
LineNumberStyle style.Styles `embed:"" prefix:"line-number." set:"defaultForeground=7" envprefix:"GUM_WRITE_LINE_NUMBER_"`
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_WRITE_HEADER_"`
PlaceholderStyle style.Styles `embed:"" prefix:"placeholder." set:"defaultForeground=240" envprefix:"GUM_WRITE_PLACEHOLDER_"`
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=7" envprefix:"GUM_WRITE_PROMPT_"`
Padding string `help:"Padding" default:"${defaultPadding}" group:"Style Flags" env:"GUM_WRITE_PADDING"`
}

202
write/write.go Normal file
View file

@ -0,0 +1,202 @@
// 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 (
"io"
"os"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/editor"
)
type keymap struct {
textarea.KeyMap
Submit key.Binding
Quit key.Binding
Abort key.Binding
OpenInEditor key.Binding
}
// FullHelp implements help.KeyMap.
func (k keymap) FullHelp() [][]key.Binding { return nil }
// ShortHelp implements help.KeyMap.
func (k keymap) ShortHelp() []key.Binding {
return []key.Binding{
k.InsertNewline,
k.OpenInEditor,
k.Submit,
}
}
func defaultKeymap() keymap {
km := textarea.DefaultKeyMap
km.InsertNewline = key.NewBinding(
key.WithKeys("ctrl+j"),
key.WithHelp("ctrl+j", "insert newline"),
)
return keymap{
KeyMap: km,
Quit: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "quit"),
),
Abort: key.NewBinding(
key.WithKeys("ctrl+c"),
key.WithHelp("ctrl+c", "cancel"),
),
OpenInEditor: key.NewBinding(
key.WithKeys("ctrl+e"),
key.WithHelp("ctrl+e", "open editor"),
),
Submit: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "submit"),
),
}
}
type model struct {
autoWidth bool
header string
headerStyle lipgloss.Style
quitting bool
submitted bool
textarea textarea.Model
showHelp bool
help help.Model
keymap keymap
padding []int
}
func (m model) Init() tea.Cmd { return textarea.Blink }
func (m model) View() string {
if m.quitting {
return ""
}
var parts []string
// Display the header above the text area if it is not empty.
if m.header != "" {
parts = append(parts, m.headerStyle.Render(m.header))
}
parts = append(parts, m.textarea.View())
if m.showHelp {
parts = append(parts, "", m.help.View(m.keymap))
}
return lipgloss.NewStyle().
Padding(m.padding...).
Render(lipgloss.JoinVertical(
lipgloss.Left,
parts...,
))
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
if m.autoWidth {
m.textarea.SetWidth(msg.Width - m.padding[1] - m.padding[3])
}
case tea.FocusMsg, tea.BlurMsg:
var cmd tea.Cmd
m.textarea, cmd = m.textarea.Update(msg)
return m, cmd
case startEditorMsg:
return m, openEditor(msg.path, msg.lineno)
case editorFinishedMsg:
if msg.err != nil {
m.quitting = true
return m, tea.Interrupt
}
m.textarea.SetValue(msg.content)
case tea.KeyMsg:
km := m.keymap
switch {
case key.Matches(msg, km.Abort):
m.quitting = true
return m, tea.Interrupt
case key.Matches(msg, km.Quit):
m.quitting = true
return m, tea.Quit
case key.Matches(msg, km.Submit):
m.quitting = true
m.submitted = true
return m, tea.Quit
case key.Matches(msg, km.OpenInEditor):
return m, createTempFile(m.textarea.Value(), m.textarea.Line()+1)
}
}
var cmd tea.Cmd
m.textarea, cmd = m.textarea.Update(msg)
return m, cmd
}
type startEditorMsg struct {
path string
lineno int
}
type editorFinishedMsg struct {
content string
err error
}
func createTempFile(content string, lineno int) tea.Cmd {
return func() tea.Msg {
f, err := os.CreateTemp("", "gum.*.md")
if err != nil {
return editorFinishedMsg{err: err}
}
_, err = io.WriteString(f, content)
if err != nil {
return editorFinishedMsg{err: err}
}
_ = f.Close()
return startEditorMsg{
path: f.Name(),
lineno: lineno,
}
}
}
func openEditor(path string, lineno int) tea.Cmd {
cb := func(err error) tea.Msg {
if err != nil {
return editorFinishedMsg{
err: err,
}
}
bts, err := os.ReadFile(path)
if err != nil {
return editorFinishedMsg{err: err}
}
return editorFinishedMsg{
content: string(bts),
}
}
cmd, err := editor.Cmd(
"Gum",
path,
editor.LineNumber(lineno),
editor.EndOfLine(),
)
if err != nil {
return func() tea.Msg { return cb(err) }
}
return tea.ExecProcess(cmd, cb)
}