Compare commits

...

21 Commits

Author SHA1 Message Date
45af67d22d chore: make deps 2025-11-11 14:18:57 +01:00
db7c4042d0 chore: go mod vendor 2025-11-09 11:52:00 +01:00
ed1a66dc5f chore: publish next tag 0.12.0-beta
Some checks failed
continuous-integration/drone/tag Build is failing
continuous-integration/drone/push Build is passing
2025-11-09 11:46:38 +01:00
bb93e4266a fix: show domain with https (clickable)
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #643
2025-11-09 10:56:21 +01:00
a2cc70b2f5 test: reinstate debug
Some checks failed
continuous-integration/drone/push Build is failing
2025-11-09 10:43:09 +01:00
ce1aa3d870 test: ensure recipe sync is robust
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-09 10:41:32 +01:00
d75700c8a9 test: ensure env vars updated
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #723
2025-11-09 09:32:52 +01:00
0ccc4aae72 chore: remove --debug 2025-11-09 09:32:42 +01:00
ec22d5d51d test: remove old tests
All checks were successful
continuous-integration/drone/push Build is passing
See #716
2025-11-05 09:52:28 +01:00
ab42584d05 docs: update var name 2025-11-05 09:52:28 +01:00
f
40eb6e9a18 fix: shorter hyphen
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-11-04 11:49:44 -03:00
35eb9d4a89 chore: update translation files
All checks were successful
continuous-integration/drone/push Build is passing
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-11-04 14:34:39 +00:00
08cc63d523 chore: make i18n
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-04 15:34:27 +01:00
797b8d899b chore: update translation files
All checks were successful
continuous-integration/drone/push Build is passing
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-11-04 13:53:26 +00:00
fb786306b5 chore: make i18n
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-04 14:53:15 +01:00
c3a2048eba fix: throw away unknown version
Some checks failed
continuous-integration/drone/push Build is failing
See #715
2025-11-04 13:52:49 +00:00
1bdc11ba62 fix no-input app deployment when no tty is present
Some checks failed
continuous-integration/drone/push Build is failing
2025-11-04 13:52:28 +00:00
cc8703310c chore: update translation files
All checks were successful
continuous-integration/drone/push Build is passing
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-11-04 07:35:14 +00:00
fcd5bd863d chore: make i18n
Some checks failed
continuous-integration/drone/push Build is failing
2025-11-04 08:35:04 +01:00
e6af2da9dd refactor: named note, merge if clause 2025-11-04 08:34:49 +01:00
4b688825e0 feat: create docker context when server folder does exist
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-11-03 17:29:04 +01:00
647 changed files with 23186 additions and 22179 deletions

View File

@ -260,6 +260,7 @@ checkout as-is. Recipe commit hashes are also supported as values for
app.Name,
app.Server,
internal.DontWaitConverge,
internal.NoInput,
f,
); err != nil {
log.Fatal(err)

View File

@ -128,6 +128,7 @@ Pass "--all-services/-a" to restart all services.`),
AppName: app.Name,
ServerName: app.Server,
Filters: f,
NoInput: internal.NoInput,
NoLog: true,
Quiet: true,
}

View File

@ -246,6 +246,7 @@ beforehand. See "abra app backup" for more.`),
stackName,
app.Server,
internal.DontWaitConverge,
internal.NoInput,
f,
); err != nil {
log.Fatal(err)

View File

@ -282,6 +282,7 @@ beforehand. See "abra app backup" for more.`),
stackName,
app.Server,
internal.DontWaitConverge,
internal.NoInput,
f,
); err != nil {
log.Fatal(err)

View File

@ -64,7 +64,7 @@ func DeployOverview(
server = "local"
}
domain := app.Domain
domain := fmt.Sprintf("https://%s", app.Domain)
if domain == "" {
domain = config.MISSING_DEFAULT
}

69
go.mod
View File

@ -1,30 +1,26 @@
module coopcloud.tech/abra
go 1.24.0
toolchain go1.24.1
go 1.24.2
require (
coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v0.4.2
github.com/distribution/reference v0.6.0
github.com/docker/cli v28.4.0+incompatible
github.com/docker/docker v28.4.0+incompatible
github.com/docker/cli v29.0.0+incompatible
github.com/docker/docker v28.5.2+incompatible
github.com/docker/go-units v0.5.0
github.com/evertras/bubble-table v0.19.2
github.com/go-git/go-git/v5 v5.16.2
github.com/go-git/go-git/v5 v5.16.3
github.com/google/go-cmp v0.7.0
github.com/leonelquinteros/gotext v1.7.2
github.com/moby/sys/signal v0.7.1
github.com/moby/term v0.5.2
github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.18.0
golang.org/x/term v0.35.0
golang.org/x/term v0.36.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.2
)
@ -35,24 +31,26 @@ require (
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.3.2 // indirect
github.com/charmbracelet/x/ansi v0.10.2 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/charmbracelet/colorprofile v0.3.3 // indirect
github.com/charmbracelet/x/ansi v0.11.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.5.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/cyphar/filepath-securejoin v0.5.0 // indirect
github.com/cyphar/filepath-securejoin v0.6.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
@ -64,19 +62,19 @@ require (
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
@ -88,13 +86,14 @@ require (
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.1.0 // indirect
github.com/moby/moby/api v1.52.0 // indirect
github.com/moby/moby/client v0.1.0 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
@ -103,12 +102,12 @@ require (
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/prometheus/common v0.67.2 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
@ -125,17 +124,17 @@ require (
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/time v0.13.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.9 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251110190251-83f479183930 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251110190251-83f479183930 // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
@ -144,7 +143,7 @@ require (
github.com/containers/image v3.0.2+incompatible
github.com/containers/storage v1.38.2 // indirect
github.com/decentral1se/passgen v1.0.1
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/docker/docker-credential-helpers v0.9.4 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.1 // indirect
@ -158,5 +157,5 @@ require (
github.com/stretchr/testify v1.11.1
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
golang.org/x/sys v0.36.0
golang.org/x/sys v0.38.0
)

80
go.sum
View File

@ -99,8 +99,6 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
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=
@ -131,28 +129,35 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
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.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
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.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw=
github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8=
github.com/charmbracelet/x/ansi v0.11.0 h1:uuIVK7GIplwX6UBIz8S2TF8nkr7xRlygSsBRjSJqIvA=
github.com/charmbracelet/x/ansi v0.11.0/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
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/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
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/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
@ -168,8 +173,14 @@ github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJ
github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/displaywidth v0.5.0 h1:AIG5vQaSL2EKqzt0M9JMnvNxOCRTKUc4vUnLWGgP89I=
github.com/clipperhouse/displaywidth v0.5.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
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.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
@ -304,6 +315,8 @@ github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cyphar/filepath-securejoin v0.5.0 h1:hIAhkRBMQ8nIeuVwcAoymp7MY4oherZdAxD+m0u9zaw=
github.com/cyphar/filepath-securejoin v0.5.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
@ -324,6 +337,8 @@ github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyG
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v28.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY=
github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.0.0+incompatible h1:KgsN2RUFMNM8wChxryicn4p46BdQWpXOA1XLGBGPGAw=
github.com/docker/cli v29.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@ -332,9 +347,13 @@ github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4Kfc
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk=
github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/docker/docker-credential-helpers v0.9.4 h1:76ItO69/AP/V4yT9V4uuuItG0B1N8hvt0T0c0NN/DzI=
github.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
@ -373,8 +392,6 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evertras/bubble-table v0.19.2 h1:u77oiM6JlRR+CvS5FZc3Hz+J6iEsvEDcR5kO8OFb1Yw=
github.com/evertras/bubble-table v0.19.2/go.mod h1:ifHujS1YxwnYSOgcR2+m3GnJ84f7CVU/4kUOxUCjEbQ=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
@ -401,6 +418,8 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-git/go-git/v5 v5.16.3 h1:Z8BtvxZ09bYm/yYNgPKCzgWtaRqDTgIKRgIRHBfU6Z8=
github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -411,6 +430,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@ -535,9 +556,12 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -597,6 +621,8 @@ github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdY
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
@ -640,7 +666,6 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
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.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
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/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
@ -669,6 +694,10 @@ github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6U
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.1.0 h1:nt+hn6O9cyJQqq5UWnFGqsZRTS/JirUqzPjEl0Bdc/8=
github.com/moby/moby/client v0.1.0/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
@ -702,8 +731,6 @@ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
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/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
@ -804,6 +831,8 @@ github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
@ -817,9 +846,9 @@ github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
@ -827,6 +856,8 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@ -852,6 +883,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
@ -975,6 +1008,8 @@ go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42s
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@ -1003,6 +1038,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -1015,6 +1052,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -1080,6 +1119,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1177,11 +1218,15 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1193,6 +1238,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -1201,6 +1248,8 @@ golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -1298,8 +1347,12 @@ google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7Fc
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
google.golang.org/genproto/googleapis/api v0.0.0-20251110190251-83f479183930 h1:8BWFtrvJRbplrKV5VHlIm4YM726eeBPPAL2QDNWhRrU=
google.golang.org/genproto/googleapis/api v0.0.0-20251110190251-83f479183930/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251110190251-83f479183930 h1:tK4fkUnnRhig9TsTp4otV1FxwBFYgbKUq1RY0V6KZ4U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251110190251-83f479183930/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@ -1321,6 +1374,8 @@ google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -1336,6 +1391,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU=
@ -1373,6 +1430,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=

View File

@ -633,6 +633,11 @@ func (a App) WipeRecipeVersion() error {
// WriteRecipeVersion writes the recipe version to the app .env file.
func (a App) WriteRecipeVersion(version string, dryRun bool) error {
if version == config.UNKNOWN_DEFAULT {
log.Debug(i18n.G("version is unknown, skipping env write"))
return nil
}
file, err := os.Open(a.Path)
if err != nil {
return err

View File

@ -224,3 +224,16 @@ func TestWriteRecipeVersionOverwrite(t *testing.T) {
assert.Equal(t, "foo", app.Recipe.EnvVersion)
}
func TestWriteRecipeVersionUnknown(t *testing.T) {
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
if err != nil {
t.Fatal(err)
}
if err := app.WriteRecipeVersion(config.UNKNOWN_DEFAULT, false); err != nil {
t.Fatal(err)
}
assert.NotEqual(t, config.UNKNOWN_DEFAULT, app.Recipe.EnvVersion)
}

View File

@ -37,18 +37,27 @@ func WithTimeout(timeout int) Opt {
// New initiates a new Docker client. New client connections are validated so
// that we ensure connections via SSH to the daemon can succeed. It takes into
// account that you may only want the local client and not communicate via SSH.
// For this use-case, please pass "default" as the contextName.
// For this use-case, please pass "default" as the serverName.
func New(serverName string, opts ...Opt) (*client.Client, error) {
var clientOpts []client.Opt
ctx, err := GetContext(serverName)
if err != nil {
serverDir := path.Join(config.SERVERS_DIR, serverName)
if _, err := os.Stat(serverDir); err == nil {
return nil, errors.New(i18n.G("server missing context, run \"abra server add %s\"?", serverName))
if _, err := os.Stat(serverDir); err != nil {
return nil, errors.New(i18n.G("server missing, run \"abra server add %s\"?", serverName))
}
return nil, errors.New(i18n.G("unknown server, run \"abra server add %s\"?", serverName))
// NOTE(p4u1): when the docker context does not exist but the server folder
// is there, let's create a new docker context.
if err = CreateContext(serverName); err != nil {
return nil, errors.New(i18n.G("server missing context, context creation failed: %s", err))
}
ctx, err = GetContext(serverName)
if err != nil {
return nil, errors.New(i18n.G("server missing context, run \"abra server add %s\"?", serverName))
}
}
ctxEndpoint, err := contextPkg.GetContextEndpoint(ctx)

View File

@ -7,7 +7,7 @@
msgid ""
msgstr "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-11-02 11:41+0100\n"
"POT-Creation-Date: 2025-11-04 15:34+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -274,12 +274,12 @@ msgstr ""
msgid "%s has been detected as not deployed"
msgstr ""
#: ./cli/app/restart.go:139
#: ./cli/app/restart.go:140
#, c-format
msgid "%s has been scaled to 0"
msgstr ""
#: ./cli/app/restart.go:150
#: ./cli/app/restart.go:151
#, c-format
msgid "%s has been scaled to 1"
msgstr ""
@ -339,17 +339,17 @@ msgstr ""
msgid "%s is missing the TYPE env var?"
msgstr ""
#: ./cli/app/rollback.go:308 ./cli/app/rollback.go:312
#: ./cli/app/rollback.go:309 ./cli/app/rollback.go:313
#, c-format
msgid "%s is not a downgrade for %s?"
msgstr ""
#: ./cli/app/upgrade.go:428 ./cli/app/upgrade.go:432
#: ./cli/app/upgrade.go:429 ./cli/app/upgrade.go:433
#, c-format
msgid "%s is not an upgrade for %s?"
msgstr ""
#: ./cli/app/env.go:146 ./cli/app/logs.go:65 ./cli/app/ps.go:62 ./cli/app/restart.go:100 ./cli/app/services.go:55 ./cli/app/undeploy.go:66 ./cli/app/upgrade.go:449
#: ./cli/app/env.go:146 ./cli/app/logs.go:65 ./cli/app/ps.go:62 ./cli/app/restart.go:100 ./cli/app/services.go:55 ./cli/app/undeploy.go:66 ./cli/app/upgrade.go:450
#, c-format
msgid "%s is not deployed?"
msgstr ""
@ -424,7 +424,7 @@ msgstr ""
msgid "%s service is missing image tag?"
msgstr ""
#: ./cli/app/restart.go:151
#: ./cli/app/restart.go:152
#, c-format
msgid "%s service successfully restarted"
msgstr ""
@ -459,7 +459,7 @@ msgstr ""
msgid "%s/example.git"
msgstr ""
#: ./pkg/upstream/stack/stack.go:602
#: ./pkg/upstream/stack/stack.go:613
#, c-format
msgid "%s: %s"
msgstr ""
@ -549,12 +549,12 @@ msgstr ""
msgid "%s: waiting %d seconds before next retry"
msgstr ""
#: ./cli/app/upgrade.go:423
#: ./cli/app/upgrade.go:424
#, c-format
msgid "'%s' is not a known version"
msgstr ""
#: ./cli/app/rollback.go:303 ./cli/app/upgrade.go:418
#: ./cli/app/rollback.go:304 ./cli/app/upgrade.go:419
#, c-format
msgid "'%s' is not a known version for %s"
msgstr ""
@ -621,7 +621,7 @@ msgstr ""
msgid "Both local recipe and live deployment labels are shown."
msgstr ""
#: ./cli/app/backup.go:319 ./cli/app/backup.go:335 ./cli/app/check.go:95 ./cli/app/cmd.go:285 ./cli/app/cp.go:385 ./cli/app/deploy.go:395 ./cli/app/labels.go:143 ./cli/app/new.go:397 ./cli/app/ps.go:213 ./cli/app/restart.go:162 ./cli/app/restore.go:138 ./cli/app/secret.go:569 ./cli/app/secret.go:609 ./cli/app/secret.go:633 ./cli/app/secret.go:641 ./cli/catalogue/catalogue.go:318 ./cli/recipe/lint.go:137
#: ./cli/app/backup.go:319 ./cli/app/backup.go:335 ./cli/app/check.go:95 ./cli/app/cmd.go:285 ./cli/app/cp.go:385 ./cli/app/deploy.go:396 ./cli/app/labels.go:143 ./cli/app/new.go:397 ./cli/app/ps.go:213 ./cli/app/restart.go:163 ./cli/app/restore.go:138 ./cli/app/secret.go:569 ./cli/app/secret.go:609 ./cli/app/secret.go:633 ./cli/app/secret.go:641 ./cli/catalogue/catalogue.go:318 ./cli/recipe/lint.go:137
msgid "C"
msgstr ""
@ -762,7 +762,7 @@ msgid "Creates a new app from a default recipe.\n"
"on your $PATH."
msgstr ""
#: ./cli/app/deploy.go:411 ./cli/app/new.go:373 ./cli/app/rollback.go:360 ./cli/app/upgrade.go:469
#: ./cli/app/deploy.go:412 ./cli/app/new.go:373 ./cli/app/rollback.go:361 ./cli/app/upgrade.go:470
msgid "D"
msgstr ""
@ -1469,7 +1469,7 @@ msgid "To load completions:\n"
" # and source this file from your PowerShell profile."
msgstr ""
#: ./cli/app/deploy.go:435 ./cli/app/rollback.go:376 ./cli/app/upgrade.go:493
#: ./cli/app/deploy.go:436 ./cli/app/rollback.go:377 ./cli/app/upgrade.go:494
msgid "U"
msgstr ""
@ -1676,7 +1676,7 @@ msgctxt "app backup list"
msgid "a"
msgstr ""
#: ./cli/app/restart.go:169
#: ./cli/app/restart.go:170
msgctxt "app restart"
msgid "a"
msgstr ""
@ -1785,7 +1785,7 @@ msgstr ""
msgid "all tasks reached terminal state"
msgstr ""
#: ./cli/app/restart.go:168
#: ./cli/app/restart.go:169
msgid "all-services"
msgstr ""
@ -1844,7 +1844,7 @@ msgstr ""
msgid "attempting to run %s"
msgstr ""
#: ./cli/app/deploy.go:272 ./cli/app/upgrade.go:295
#: ./cli/app/deploy.go:273 ./cli/app/upgrade.go:296
#, c-format
msgid "attempting to run post deploy commands, saw: %s"
msgstr ""
@ -1854,7 +1854,7 @@ msgstr ""
msgid "attempting to scale %s to 0"
msgstr ""
#: ./cli/app/restart.go:140
#: ./cli/app/restart.go:141
#, c-format
msgid "attempting to scale %s to 1"
msgstr ""
@ -1924,7 +1924,7 @@ msgstr ""
#. no spaces in between
#. translators: `abra app cp` aliases. use a comma separated list of aliases with
#. no spaces in between
#: ./cli/app/backup.go:148 ./cli/app/cp.go:30 ./cli/app/deploy.go:419 ./cli/app/rollback.go:368 ./cli/app/upgrade.go:477
#: ./cli/app/backup.go:148 ./cli/app/cp.go:30 ./cli/app/deploy.go:420 ./cli/app/rollback.go:369 ./cli/app/upgrade.go:478
msgid "c"
msgstr ""
@ -1965,7 +1965,7 @@ msgstr ""
msgid "cannot find app with name %s"
msgstr ""
#: ./pkg/upstream/stack/stack.go:657
#: ./pkg/upstream/stack/stack.go:668
#, c-format
msgid "cannot get label %s for %s"
msgstr ""
@ -1980,7 +1980,7 @@ msgstr ""
msgid "cannot redeploy previous chaos version (%s), did you mean to use \"--chaos\"?"
msgstr ""
#: ./cli/app/deploy.go:369
#: ./cli/app/deploy.go:370
#, c-format
msgid "cannot redeploy previous chaos version (%s), did you mean to use \"--chaos\"?\n"
" to return to a regular release, specify a release tag, commit SHA or use \"--latest\""
@ -1999,7 +1999,7 @@ msgstr ""
msgid "cannot use '[secret] [version]' and '--all' together"
msgstr ""
#: ./cli/app/deploy.go:311
#: ./cli/app/deploy.go:312
msgid "cannot use --chaos and --latest together"
msgstr ""
@ -2023,11 +2023,11 @@ msgstr ""
msgid "cannot use [service] and --all-services/-a together"
msgstr ""
#: ./cli/app/deploy.go:303 ./cli/app/new.go:76
#: ./cli/app/deploy.go:304 ./cli/app/new.go:76
msgid "cannot use [version] and --chaos together"
msgstr ""
#: ./cli/app/deploy.go:307
#: ./cli/app/deploy.go:308
msgid "cannot use [version] and --latest together"
msgstr ""
@ -2059,7 +2059,7 @@ msgstr ""
msgid "cfg"
msgstr ""
#: ./cli/app/backup.go:318 ./cli/app/backup.go:334 ./cli/app/check.go:94 ./cli/app/cmd.go:284 ./cli/app/cp.go:384 ./cli/app/deploy.go:394 ./cli/app/labels.go:142 ./cli/app/new.go:396 ./cli/app/ps.go:212 ./cli/app/restart.go:161 ./cli/app/restore.go:137 ./cli/app/secret.go:568 ./cli/app/secret.go:608 ./cli/app/secret.go:632 ./cli/app/secret.go:640 ./cli/catalogue/catalogue.go:317 ./cli/recipe/lint.go:136
#: ./cli/app/backup.go:318 ./cli/app/backup.go:334 ./cli/app/check.go:94 ./cli/app/cmd.go:284 ./cli/app/cp.go:384 ./cli/app/deploy.go:395 ./cli/app/labels.go:142 ./cli/app/new.go:396 ./cli/app/ps.go:212 ./cli/app/restart.go:162 ./cli/app/restore.go:137 ./cli/app/secret.go:568 ./cli/app/secret.go:608 ./cli/app/secret.go:632 ./cli/app/secret.go:640 ./cli/catalogue/catalogue.go:317 ./cli/recipe/lint.go:136
msgid "chaos"
msgstr ""
@ -2068,7 +2068,7 @@ msgstr ""
msgid "check <domain> [flags]"
msgstr ""
#: ./cli/app/deploy.go:94 ./cli/app/undeploy.go:58 ./cli/app/upgrade.go:441
#: ./cli/app/deploy.go:94 ./cli/app/undeploy.go:58 ./cli/app/upgrade.go:442
#, c-format
msgid "checking whether %s is already deployed"
msgstr ""
@ -2291,7 +2291,7 @@ msgstr ""
msgid "create remote directory: %s"
msgstr ""
#: ./pkg/client/client.go:102
#: ./pkg/client/client.go:111
#, c-format
msgid "created client for %s"
msgstr ""
@ -2311,7 +2311,7 @@ msgstr ""
msgid "created the %s context"
msgstr ""
#: ./pkg/upstream/stack/stack.go:520
#: ./pkg/upstream/stack/stack.go:524
#, c-format
msgid "creating %s"
msgstr ""
@ -2326,12 +2326,12 @@ msgstr ""
msgid "creating context with domain %s"
msgstr ""
#: ./pkg/upstream/stack/stack.go:422
#: ./pkg/upstream/stack/stack.go:426
#, c-format
msgid "creating network %s"
msgstr ""
#: ./pkg/upstream/stack/stack.go:369
#: ./pkg/upstream/stack/stack.go:373
#, c-format
msgid "creating secret %s"
msgstr ""
@ -2350,7 +2350,7 @@ msgstr ""
msgid "critical errors present in %s config"
msgstr ""
#: ./cli/app/rollback.go:298
#: ./cli/app/rollback.go:299
#, c-format
msgid "current deployment '%s' is not a known version for %s"
msgstr ""
@ -2390,11 +2390,11 @@ msgstr ""
msgid "deploy <domain> [version] [flags]"
msgstr ""
#: ./pkg/upstream/stack/stack.go:593
#: ./pkg/upstream/stack/stack.go:604
msgid "deploy failed 🛑"
msgstr ""
#: ./pkg/upstream/stack/stack.go:597
#: ./pkg/upstream/stack/stack.go:608
msgid "deploy in progress 🟠"
msgstr ""
@ -2402,15 +2402,15 @@ msgstr ""
msgid "deploy labels stanza present"
msgstr ""
#: ./cli/app/deploy.go:429
#: ./cli/app/deploy.go:430
msgid "deploy latest recipe version"
msgstr ""
#: ./pkg/upstream/stack/stack.go:637
#: ./pkg/upstream/stack/stack.go:648
msgid "deploy succeeded 🟢"
msgstr ""
#: ./pkg/upstream/stack/stack.go:595
#: ./pkg/upstream/stack/stack.go:606
msgid "deploy timed out 🟠"
msgstr ""
@ -2504,11 +2504,11 @@ msgstr ""
msgid "dirty: %v, "
msgstr ""
#: ./cli/app/deploy.go:421 ./cli/app/rollback.go:370 ./cli/app/upgrade.go:479
#: ./cli/app/deploy.go:422 ./cli/app/rollback.go:371 ./cli/app/upgrade.go:480
msgid "disable converge logic checks"
msgstr ""
#: ./cli/app/deploy.go:413 ./cli/app/rollback.go:362 ./cli/app/upgrade.go:471
#: ./cli/app/deploy.go:414 ./cli/app/rollback.go:363 ./cli/app/upgrade.go:472
msgid "disable public DNS checks"
msgstr ""
@ -2726,7 +2726,7 @@ msgstr ""
#. translators: `abra recipe fetch` aliases. use a comma separated list of aliases
#. with no spaces in between
#: ./cli/app/deploy.go:403 ./cli/app/env.go:325 ./cli/app/remove.go:163 ./cli/app/rollback.go:352 ./cli/app/secret.go:593 ./cli/app/upgrade.go:461 ./cli/app/volume.go:217 ./cli/recipe/fetch.go:20 ./cli/recipe/fetch.go:138
#: ./cli/app/deploy.go:404 ./cli/app/env.go:325 ./cli/app/remove.go:163 ./cli/app/rollback.go:353 ./cli/app/secret.go:593 ./cli/app/upgrade.go:462 ./cli/app/volume.go:217 ./cli/recipe/fetch.go:20 ./cli/recipe/fetch.go:138
msgid "f"
msgstr ""
@ -2760,22 +2760,22 @@ msgstr ""
msgid "failed to copy %s from local machine to %s: output:%s err:%s"
msgstr ""
#: ./pkg/upstream/stack/stack.go:531
#: ./pkg/upstream/stack/stack.go:535
#, c-format
msgid "failed to create %s"
msgstr ""
#: ./pkg/upstream/stack/stack.go:393
#: ./pkg/upstream/stack/stack.go:397
#, c-format
msgid "failed to create config %s"
msgstr ""
#: ./pkg/upstream/stack/stack.go:424
#: ./pkg/upstream/stack/stack.go:428
#, c-format
msgid "failed to create network %s"
msgstr ""
#: ./pkg/upstream/stack/stack.go:371
#: ./pkg/upstream/stack/stack.go:375
#, c-format
msgid "failed to create secret %s"
msgstr ""
@ -2872,7 +2872,7 @@ msgstr ""
msgid "failed to retrieve latest commit for %s: %s"
msgstr ""
#: ./pkg/upstream/stack/stack.go:468
#: ./pkg/upstream/stack/stack.go:472
#, c-format
msgid "failed to retrieve registry auth for image %s: %s"
msgstr ""
@ -2892,17 +2892,17 @@ msgstr ""
msgid "failed to tag release: %s"
msgstr ""
#: ./pkg/upstream/stack/stack.go:508
#: ./pkg/upstream/stack/stack.go:512
#, c-format
msgid "failed to update %s"
msgstr ""
#: ./pkg/upstream/stack/stack.go:387
#: ./pkg/upstream/stack/stack.go:391
#, c-format
msgid "failed to update config %s"
msgstr ""
#: ./pkg/upstream/stack/stack.go:365
#: ./pkg/upstream/stack/stack.go:369
#, c-format
msgid "failed to update secret %s"
msgstr ""
@ -2957,7 +2957,7 @@ msgstr ""
msgid "final merged env values for %s are: %s"
msgstr ""
#: ./cli/app/deploy.go:402 ./cli/app/env.go:324 ./cli/app/remove.go:162 ./cli/app/rollback.go:351 ./cli/app/upgrade.go:460 ./cli/app/volume.go:216 ./cli/recipe/fetch.go:137
#: ./cli/app/deploy.go:403 ./cli/app/env.go:324 ./cli/app/remove.go:162 ./cli/app/rollback.go:352 ./cli/app/upgrade.go:461 ./cli/app/volume.go:216 ./cli/recipe/fetch.go:137
msgid "force"
msgstr ""
@ -3171,7 +3171,7 @@ msgstr ""
msgid "id: %s, "
msgstr ""
#: ./cli/app/backup.go:321 ./cli/app/backup.go:337 ./cli/app/check.go:97 ./cli/app/cmd.go:287 ./cli/app/cp.go:387 ./cli/app/deploy.go:397 ./cli/app/labels.go:145 ./cli/app/new.go:399 ./cli/app/ps.go:215 ./cli/app/restart.go:164 ./cli/app/restore.go:140 ./cli/app/secret.go:571 ./cli/app/secret.go:611 ./cli/app/secret.go:635 ./cli/app/secret.go:643 ./cli/catalogue/catalogue.go:320 ./cli/recipe/lint.go:139
#: ./cli/app/backup.go:321 ./cli/app/backup.go:337 ./cli/app/check.go:97 ./cli/app/cmd.go:287 ./cli/app/cp.go:387 ./cli/app/deploy.go:398 ./cli/app/labels.go:145 ./cli/app/new.go:399 ./cli/app/ps.go:215 ./cli/app/restart.go:165 ./cli/app/restore.go:140 ./cli/app/secret.go:571 ./cli/app/secret.go:611 ./cli/app/secret.go:635 ./cli/app/secret.go:643 ./cli/catalogue/catalogue.go:320 ./cli/recipe/lint.go:139
msgid "ignore uncommitted recipes changes"
msgstr ""
@ -3282,7 +3282,7 @@ msgstr ""
msgid "initialised new git repo in %s"
msgstr ""
#: ./pkg/upstream/stack/stack.go:206
#: ./pkg/upstream/stack/stack.go:207
msgid "initialising deployment"
msgstr ""
@ -3346,7 +3346,7 @@ msgstr ""
msgid "invalid npipe source, source cannot be empty"
msgstr ""
#: ./pkg/upstream/stack/stack.go:239
#: ./pkg/upstream/stack/stack.go:241
#, c-format
msgid "invalid option %s for flag --resolve-image"
msgstr ""
@ -3369,7 +3369,7 @@ msgstr ""
#. no spaces in between
#. translators: `abra recipe lint` aliases. use a comma separated list of
#. aliases with no spaces in between
#: ./cli/app/cmd.go:261 ./cli/app/deploy.go:427 ./cli/app/logs.go:20 ./cli/recipe/lint.go:17 ./cli/server/add.go:207
#: ./cli/app/cmd.go:261 ./cli/app/deploy.go:428 ./cli/app/logs.go:20 ./cli/recipe/lint.go:17 ./cli/server/add.go:207
msgid "l"
msgstr ""
@ -3384,7 +3384,7 @@ msgstr ""
msgid "labels <domain> [flags]"
msgstr ""
#: ./cli/app/deploy.go:426 ./cli/app/list.go:182
#: ./cli/app/deploy.go:427 ./cli/app/list.go:182
msgid "latest"
msgstr ""
@ -3473,12 +3473,12 @@ msgstr ""
msgid "logs <domain> [service] [flags]"
msgstr ""
#: ./pkg/upstream/stack/stack.go:628
#: ./pkg/upstream/stack/stack.go:639
#, c-format
msgid "logs: %s"
msgstr ""
#: ./pkg/upstream/stack/stack.go:630
#: ./pkg/upstream/stack/stack.go:641
msgid "logs: no log output received from deployment"
msgstr ""
@ -3614,12 +3614,12 @@ msgstr ""
msgid "need 3 or 4 arguments"
msgstr ""
#: ./pkg/upstream/stack/stack.go:348
#: ./pkg/upstream/stack/stack.go:352
#, c-format
msgid "network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed, which you can do by running this on the server: docker network create -d overlay proxy"
msgstr ""
#: ./pkg/upstream/stack/stack.go:352
#: ./pkg/upstream/stack/stack.go:356
#, c-format
msgid "network %q is declared as external, but it is not in the right scope: %q instead of \"swarm\""
msgstr ""
@ -3842,11 +3842,11 @@ msgstr ""
msgid "no volumes to remove"
msgstr ""
#: ./cli/app/deploy.go:418 ./cli/app/rollback.go:367 ./cli/app/upgrade.go:476
#: ./cli/app/deploy.go:419 ./cli/app/rollback.go:368 ./cli/app/upgrade.go:477
msgid "no-converge-checks"
msgstr ""
#: ./cli/app/deploy.go:410 ./cli/app/rollback.go:359 ./cli/app/upgrade.go:468
#: ./cli/app/deploy.go:411 ./cli/app/rollback.go:360 ./cli/app/upgrade.go:469
msgid "no-domain-checks"
msgstr ""
@ -3902,7 +3902,7 @@ msgstr ""
msgid "only show errors"
msgstr ""
#: ./cli/app/upgrade.go:487
#: ./cli/app/upgrade.go:488
msgid "only show release notes"
msgstr ""
@ -3933,22 +3933,22 @@ msgstr ""
msgid "parsed following command arguments: %s"
msgstr ""
#: ./cli/app/upgrade.go:344
#: ./cli/app/upgrade.go:345
#, c-format
msgid "parsing chosen upgrade version failed: %s"
msgstr ""
#: ./cli/app/upgrade.go:388
#: ./cli/app/upgrade.go:389
#, c-format
msgid "parsing deployed version failed: %s"
msgstr ""
#: ./cli/app/upgrade.go:349
#: ./cli/app/upgrade.go:350
#, c-format
msgid "parsing deployment version failed: %s"
msgstr ""
#: ./cli/app/upgrade.go:355 ./cli/app/upgrade.go:394
#: ./cli/app/upgrade.go:356 ./cli/app/upgrade.go:395
#, c-format
msgid "parsing recipe version failed: %s"
msgstr ""
@ -3973,7 +3973,7 @@ msgstr ""
msgid "pattern"
msgstr ""
#: ./cli/app/deploy.go:405 ./cli/app/env.go:327 ./cli/app/remove.go:165 ./cli/app/rollback.go:354 ./cli/app/upgrade.go:463 ./cli/app/volume.go:219
#: ./cli/app/deploy.go:406 ./cli/app/env.go:327 ./cli/app/remove.go:165 ./cli/app/rollback.go:355 ./cli/app/upgrade.go:464 ./cli/app/volume.go:219
msgid "perform action without further prompt"
msgstr ""
@ -3988,27 +3988,27 @@ msgstr ""
msgid "please fix your synced label for %s and re-run this command"
msgstr ""
#: ./cli/app/rollback.go:266
#: ./cli/app/rollback.go:267
#, c-format
msgid "please select a downgrade (version: %s):"
msgstr ""
#: ./cli/app/rollback.go:271
#: ./cli/app/rollback.go:272
#, c-format
msgid "please select a downgrade (version: %s, chaos: %s):"
msgstr ""
#: ./cli/app/upgrade.go:311
#: ./cli/app/upgrade.go:312
#, c-format
msgid "please select an upgrade (version: %s):"
msgstr ""
#: ./cli/app/upgrade.go:316
#: ./cli/app/upgrade.go:317
#, c-format
msgid "please select an upgrade (version: %s, chaos: %s):"
msgstr ""
#: ./pkg/upstream/stack/stack.go:576
#: ./pkg/upstream/stack/stack.go:587
msgid "polling deployment status"
msgstr ""
@ -4094,7 +4094,7 @@ msgstr ""
#. with no spaces in between
#. translators: `abra recipe` aliases. use a comma separated list of aliases
#. with no spaces in between
#: ./cli/app/backup.go:327 ./cli/app/list.go:300 ./cli/app/move.go:350 ./cli/app/run.go:23 ./cli/app/upgrade.go:485 ./cli/catalogue/catalogue.go:302 ./cli/recipe/recipe.go:12 ./cli/recipe/release.go:649 ./cli/recipe/sync.go:272
#: ./cli/app/backup.go:327 ./cli/app/list.go:300 ./cli/app/move.go:350 ./cli/app/run.go:23 ./cli/app/upgrade.go:486 ./cli/catalogue/catalogue.go:302 ./cli/recipe/recipe.go:12 ./cli/recipe/release.go:649 ./cli/recipe/sync.go:272
msgid "r"
msgstr ""
@ -4210,7 +4210,7 @@ msgstr ""
msgid "release <recipe> [version] [flags]"
msgstr ""
#: ./cli/app/upgrade.go:484
#: ./cli/app/upgrade.go:485
msgid "releasenotes"
msgstr ""
@ -4376,7 +4376,7 @@ msgstr ""
msgid "restart <domain> [[service] | --all-services] [flags]"
msgstr ""
#: ./cli/app/restart.go:171
#: ./cli/app/restart.go:172
msgid "restart all services"
msgstr ""
@ -4441,7 +4441,7 @@ msgstr ""
msgid "retrieved versions from local recipe repository"
msgstr ""
#: ./pkg/upstream/stack/stack.go:464
#: ./pkg/upstream/stack/stack.go:468
#, c-format
msgid "retrieving docker auth token: failed create docker cli: %s"
msgstr ""
@ -4514,7 +4514,7 @@ msgstr ""
msgid "run command locally"
msgstr ""
#: ./cli/app/deploy.go:270 ./cli/app/upgrade.go:292
#: ./cli/app/deploy.go:271 ./cli/app/upgrade.go:293
#, c-format
msgid "run the following post-deploy commands: %s"
msgstr ""
@ -4599,12 +4599,12 @@ msgstr ""
msgid "secret not found: %s"
msgstr ""
#: ./cli/app/deploy.go:339
#: ./cli/app/deploy.go:340
#, c-format
msgid "secret not generated: %s"
msgstr ""
#: ./cli/app/deploy.go:337
#: ./cli/app/deploy.go:338
#, c-format
msgid "secret not inserted (#generate=false): %s"
msgstr ""
@ -4651,11 +4651,21 @@ msgstr ""
msgid "server doesn't exist?"
msgstr ""
#: ./pkg/client/client.go:48
#: ./pkg/client/client.go:54
#, c-format
msgid "server missing context, context creation failed: %s"
msgstr ""
#: ./pkg/client/client.go:59
#, c-format
msgid "server missing context, run \"abra server add %s\"?"
msgstr ""
#: ./pkg/client/client.go:48
#, c-format
msgid "server missing, run \"abra server add %s\"?"
msgstr ""
#: ./cli/server/add.go:148
#, c-format
msgid "serverAdd: cleanUp: %s is not empty, aborting cleanup"
@ -4728,7 +4738,7 @@ msgstr ""
msgid "severity"
msgstr ""
#: ./cli/app/deploy.go:437 ./cli/app/rollback.go:378 ./cli/app/upgrade.go:495
#: ./cli/app/deploy.go:438 ./cli/app/rollback.go:379 ./cli/app/upgrade.go:496
msgid "show all configs & images, including unchanged ones"
msgstr ""
@ -4752,7 +4762,7 @@ msgstr ""
msgid "show debug messages"
msgstr ""
#: ./cli/app/deploy.go:434 ./cli/app/rollback.go:375 ./cli/app/upgrade.go:492
#: ./cli/app/deploy.go:435 ./cli/app/rollback.go:376 ./cli/app/upgrade.go:493
msgid "show-unchanged"
msgstr ""
@ -4796,7 +4806,7 @@ msgstr ""
msgid "skipping as requested, undeploy still in progress 🟠"
msgstr ""
#: ./pkg/upstream/stack/stack.go:306
#: ./pkg/upstream/stack/stack.go:309
msgid "skipping converge logic checks"
msgstr ""
@ -4818,12 +4828,12 @@ msgstr ""
msgid "skipping secret (because it already exists) on %s: %s"
msgstr ""
#: ./pkg/app/app.go:692
#: ./pkg/app/app.go:697
#, c-format
msgid "skipping version %s write as already exists in %s.env"
msgstr ""
#: ./pkg/app/app.go:686
#: ./pkg/app/app.go:691
#, c-format
msgid "skipping writing version %s because dry run"
msgstr ""
@ -4931,12 +4941,12 @@ msgstr ""
msgid "successfully created %s"
msgstr ""
#: ./pkg/client/client.go:111
#: ./pkg/client/client.go:120
#, c-format
msgid "swarm mode not enabled on %s?"
msgstr ""
#: ./pkg/client/client.go:114
#: ./pkg/client/client.go:123
msgid "swarm mode not enabled on local server?"
msgstr ""
@ -5012,7 +5022,7 @@ msgstr ""
msgid "timeout label: %s"
msgstr ""
#: ./pkg/upstream/stack/remove.go:29 ./pkg/upstream/stack/stack.go:209
#: ./pkg/upstream/stack/remove.go:29 ./pkg/upstream/stack/stack.go:210
#, c-format
msgid "timeout: set to %d second(s)"
msgstr ""
@ -5435,11 +5445,6 @@ msgstr ""
msgid "unknown server %s, run \"abra server add %s\"?"
msgstr ""
#: ./pkg/client/client.go:51
#, c-format
msgid "unknown server, run \"abra server add %s\"?"
msgstr ""
#: ./cli/app/cp.go:259
#, c-format
msgid "untar: %s"
@ -5451,7 +5456,7 @@ msgstr ""
msgid "up"
msgstr ""
#: ./pkg/upstream/stack/stack.go:473
#: ./pkg/upstream/stack/stack.go:477
#, c-format
msgid "updating %s"
msgstr ""
@ -5563,7 +5568,7 @@ msgstr ""
msgid "version"
msgstr ""
#: ./pkg/app/app.go:690
#: ./pkg/app/app.go:695
#, c-format
msgid "version %s saved to %s.env"
msgstr ""
@ -5582,6 +5587,10 @@ msgstr ""
msgid "version for abra"
msgstr ""
#: ./pkg/app/app.go:637
msgid "version is unknown, skipping env write"
msgstr ""
#: ./pkg/recipe/recipe.go:130
#, c-format
msgid "version seems invalid: %s"
@ -5592,27 +5601,27 @@ msgstr ""
msgid "version wiped from %s.env"
msgstr ""
#: ./cli/app/deploy.go:353
#: ./cli/app/deploy.go:354
#, c-format
msgid "version: taking chaos version: %s"
msgstr ""
#: ./cli/app/deploy.go:379
#: ./cli/app/deploy.go:380
#, c-format
msgid "version: taking deployed version: %s"
msgstr ""
#: ./cli/app/deploy.go:384
#: ./cli/app/deploy.go:385
#, c-format
msgid "version: taking new recipe version: %s"
msgstr ""
#: ./cli/app/deploy.go:373
#: ./cli/app/deploy.go:374
#, c-format
msgid "version: taking version from .env file: %s"
msgstr ""
#: ./cli/app/deploy.go:359
#: ./cli/app/deploy.go:360
#, c-format
msgid "version: taking version from cli arg: %s"
msgstr ""
@ -5672,22 +5681,22 @@ msgstr ""
msgid "volumes pruned: %d; space reclaimed: %s"
msgstr ""
#: ./pkg/upstream/stack/stack.go:614
#: ./pkg/upstream/stack/stack.go:625
#, c-format
msgid "waitOnServices: error creating log dir: %s"
msgstr ""
#: ./pkg/upstream/stack/stack.go:619
#: ./pkg/upstream/stack/stack.go:630
#, c-format
msgid "waitOnServices: error opening file: %s"
msgstr ""
#: ./pkg/upstream/stack/stack.go:585
#: ./pkg/upstream/stack/stack.go:596
#, c-format
msgid "waitOnServices: error running TUI: %s"
msgstr ""
#: ./pkg/upstream/stack/stack.go:625
#: ./pkg/upstream/stack/stack.go:636
#, c-format
msgid "waitOnServices: writeFile: %s"
msgstr ""
@ -5735,7 +5744,7 @@ msgstr ""
msgid "writer: %v, "
msgstr ""
#: ./cli/app/deploy.go:277 ./cli/app/new.go:241 ./cli/app/rollback.go:255 ./cli/app/undeploy.go:120 ./cli/app/upgrade.go:300
#: ./cli/app/deploy.go:278 ./cli/app/new.go:241 ./cli/app/rollback.go:256 ./cli/app/undeploy.go:120 ./cli/app/upgrade.go:301
#, c-format
msgid "writing recipe version failed: %s"
msgstr ""

View File

@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-11-02 11:41+0100\n"
"POT-Creation-Date: 2025-11-04 15:34+0100\n"
"PO-Revision-Date: 2025-09-04 08:14+0000\n"
"Last-Translator: chasqui <chasqui@cryptolab.net>\n"
"Language-Team: Spanish <https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/>\n"
@ -285,12 +285,12 @@ msgstr ""
msgid "%s has been detected as not deployed"
msgstr ""
#: cli/app/restart.go:139
#: cli/app/restart.go:140
#, c-format
msgid "%s has been scaled to 0"
msgstr ""
#: cli/app/restart.go:150
#: cli/app/restart.go:151
#, c-format
msgid "%s has been scaled to 1"
msgstr ""
@ -350,19 +350,19 @@ msgstr ""
msgid "%s is missing the TYPE env var?"
msgstr ""
#: cli/app/rollback.go:308 cli/app/rollback.go:312
#: cli/app/rollback.go:309 cli/app/rollback.go:313
#, c-format
msgid "%s is not a downgrade for %s?"
msgstr ""
#: cli/app/upgrade.go:428 cli/app/upgrade.go:432
#: cli/app/upgrade.go:429 cli/app/upgrade.go:433
#, c-format
msgid "%s is not an upgrade for %s?"
msgstr ""
#: cli/app/env.go:146 cli/app/logs.go:65 cli/app/ps.go:62
#: cli/app/restart.go:100 cli/app/services.go:55 cli/app/undeploy.go:66
#: cli/app/upgrade.go:449
#: cli/app/upgrade.go:450
#, c-format
msgid "%s is not deployed?"
msgstr ""
@ -437,7 +437,7 @@ msgstr ""
msgid "%s service is missing image tag?"
msgstr ""
#: cli/app/restart.go:151
#: cli/app/restart.go:152
#, c-format
msgid "%s service successfully restarted"
msgstr ""
@ -472,7 +472,7 @@ msgstr ""
msgid "%s/example.git"
msgstr ""
#: pkg/upstream/stack/stack.go:602
#: pkg/upstream/stack/stack.go:613
#, c-format
msgid "%s: %s"
msgstr ""
@ -562,12 +562,12 @@ msgstr ""
msgid "%s: waiting %d seconds before next retry"
msgstr ""
#: cli/app/upgrade.go:423
#: cli/app/upgrade.go:424
#, c-format
msgid "'%s' is not a known version"
msgstr ""
#: cli/app/rollback.go:303 cli/app/upgrade.go:418
#: cli/app/rollback.go:304 cli/app/upgrade.go:419
#, c-format
msgid "'%s' is not a known version for %s"
msgstr ""
@ -637,9 +637,9 @@ msgid "Both local recipe and live deployment labels are shown."
msgstr ""
#: cli/app/backup.go:319 cli/app/backup.go:335 cli/app/check.go:95
#: cli/app/cmd.go:285 cli/app/cp.go:385 cli/app/deploy.go:395
#: cli/app/cmd.go:285 cli/app/cp.go:385 cli/app/deploy.go:396
#: cli/app/labels.go:143 cli/app/new.go:397 cli/app/ps.go:213
#: cli/app/restart.go:162 cli/app/restore.go:138 cli/app/secret.go:569
#: cli/app/restart.go:163 cli/app/restore.go:138 cli/app/secret.go:569
#: cli/app/secret.go:609 cli/app/secret.go:633 cli/app/secret.go:641
#: cli/catalogue/catalogue.go:318 cli/recipe/lint.go:137
msgid "C"
@ -785,8 +785,8 @@ msgid ""
"on your $PATH."
msgstr ""
#: cli/app/deploy.go:411 cli/app/new.go:373 cli/app/rollback.go:360
#: cli/app/upgrade.go:469
#: cli/app/deploy.go:412 cli/app/new.go:373 cli/app/rollback.go:361
#: cli/app/upgrade.go:470
msgid "D"
msgstr ""
@ -1518,7 +1518,7 @@ msgid ""
" # and source this file from your PowerShell profile."
msgstr ""
#: cli/app/deploy.go:435 cli/app/rollback.go:376 cli/app/upgrade.go:493
#: cli/app/deploy.go:436 cli/app/rollback.go:377 cli/app/upgrade.go:494
msgid "U"
msgstr ""
@ -1734,7 +1734,7 @@ msgctxt "app backup list"
msgid "a"
msgstr ""
#: cli/app/restart.go:169
#: cli/app/restart.go:170
msgctxt "app restart"
msgid "a"
msgstr ""
@ -1844,7 +1844,7 @@ msgstr ""
msgid "all tasks reached terminal state"
msgstr ""
#: cli/app/restart.go:168
#: cli/app/restart.go:169
msgid "all-services"
msgstr ""
@ -1903,7 +1903,7 @@ msgstr ""
msgid "attempting to run %s"
msgstr ""
#: cli/app/deploy.go:272 cli/app/upgrade.go:295
#: cli/app/deploy.go:273 cli/app/upgrade.go:296
#, c-format
msgid "attempting to run post deploy commands, saw: %s"
msgstr ""
@ -1913,7 +1913,7 @@ msgstr ""
msgid "attempting to scale %s to 0"
msgstr ""
#: cli/app/restart.go:140
#: cli/app/restart.go:141
#, c-format
msgid "attempting to scale %s to 1"
msgstr ""
@ -1989,8 +1989,8 @@ msgstr ""
#. no spaces in between
#. translators: `abra app cp` aliases. use a comma separated list of aliases with
#. no spaces in between
#: cli/app/backup.go:148 cli/app/cp.go:30 cli/app/deploy.go:419
#: cli/app/rollback.go:368 cli/app/upgrade.go:477
#: cli/app/backup.go:148 cli/app/cp.go:30 cli/app/deploy.go:420
#: cli/app/rollback.go:369 cli/app/upgrade.go:478
msgid "c"
msgstr ""
@ -2031,7 +2031,7 @@ msgstr ""
msgid "cannot find app with name %s"
msgstr ""
#: pkg/upstream/stack/stack.go:657
#: pkg/upstream/stack/stack.go:668
#, c-format
msgid "cannot get label %s for %s"
msgstr ""
@ -2046,7 +2046,7 @@ msgstr ""
msgid "cannot redeploy previous chaos version (%s), did you mean to use \"--chaos\"?"
msgstr ""
#: cli/app/deploy.go:369
#: cli/app/deploy.go:370
#, c-format
msgid ""
"cannot redeploy previous chaos version (%s), did you mean to use \"--chaos\"?\n"
@ -2066,7 +2066,7 @@ msgstr ""
msgid "cannot use '[secret] [version]' and '--all' together"
msgstr ""
#: cli/app/deploy.go:311
#: cli/app/deploy.go:312
msgid "cannot use --chaos and --latest together"
msgstr ""
@ -2090,11 +2090,11 @@ msgstr ""
msgid "cannot use [service] and --all-services/-a together"
msgstr ""
#: cli/app/deploy.go:303 cli/app/new.go:76
#: cli/app/deploy.go:304 cli/app/new.go:76
msgid "cannot use [version] and --chaos together"
msgstr ""
#: cli/app/deploy.go:307
#: cli/app/deploy.go:308
msgid "cannot use [version] and --latest together"
msgstr ""
@ -2127,9 +2127,9 @@ msgid "cfg"
msgstr ""
#: cli/app/backup.go:318 cli/app/backup.go:334 cli/app/check.go:94
#: cli/app/cmd.go:284 cli/app/cp.go:384 cli/app/deploy.go:394
#: cli/app/cmd.go:284 cli/app/cp.go:384 cli/app/deploy.go:395
#: cli/app/labels.go:142 cli/app/new.go:396 cli/app/ps.go:212
#: cli/app/restart.go:161 cli/app/restore.go:137 cli/app/secret.go:568
#: cli/app/restart.go:162 cli/app/restore.go:137 cli/app/secret.go:568
#: cli/app/secret.go:608 cli/app/secret.go:632 cli/app/secret.go:640
#: cli/catalogue/catalogue.go:317 cli/recipe/lint.go:136
msgid "chaos"
@ -2140,7 +2140,7 @@ msgstr ""
msgid "check <domain> [flags]"
msgstr "verificar <domain> [flags]"
#: cli/app/deploy.go:94 cli/app/undeploy.go:58 cli/app/upgrade.go:441
#: cli/app/deploy.go:94 cli/app/undeploy.go:58 cli/app/upgrade.go:442
#, c-format
msgid "checking whether %s is already deployed"
msgstr ""
@ -2363,7 +2363,7 @@ msgstr ""
msgid "create remote directory: %s"
msgstr ""
#: pkg/client/client.go:102
#: pkg/client/client.go:111
#, c-format
msgid "created client for %s"
msgstr ""
@ -2383,7 +2383,7 @@ msgstr ""
msgid "created the %s context"
msgstr ""
#: pkg/upstream/stack/stack.go:520
#: pkg/upstream/stack/stack.go:524
#, c-format
msgid "creating %s"
msgstr ""
@ -2398,12 +2398,12 @@ msgstr ""
msgid "creating context with domain %s"
msgstr ""
#: pkg/upstream/stack/stack.go:422
#: pkg/upstream/stack/stack.go:426
#, c-format
msgid "creating network %s"
msgstr ""
#: pkg/upstream/stack/stack.go:369
#: pkg/upstream/stack/stack.go:373
#, c-format
msgid "creating secret %s"
msgstr ""
@ -2422,7 +2422,7 @@ msgstr ""
msgid "critical errors present in %s config"
msgstr ""
#: cli/app/rollback.go:298
#: cli/app/rollback.go:299
#, c-format
msgid "current deployment '%s' is not a known version for %s"
msgstr ""
@ -2462,11 +2462,11 @@ msgstr ""
msgid "deploy <domain> [version] [flags]"
msgstr "desplegar <domain> [version] [flags]"
#: pkg/upstream/stack/stack.go:593
#: pkg/upstream/stack/stack.go:604
msgid "deploy failed 🛑"
msgstr ""
#: pkg/upstream/stack/stack.go:597
#: pkg/upstream/stack/stack.go:608
msgid "deploy in progress 🟠"
msgstr ""
@ -2474,16 +2474,16 @@ msgstr ""
msgid "deploy labels stanza present"
msgstr ""
#: cli/app/deploy.go:429
#: cli/app/deploy.go:430
#, fuzzy
msgid "deploy latest recipe version"
msgstr "Publicar una nueva versión de una receta"
#: pkg/upstream/stack/stack.go:637
#: pkg/upstream/stack/stack.go:648
msgid "deploy succeeded 🟢"
msgstr ""
#: pkg/upstream/stack/stack.go:595
#: pkg/upstream/stack/stack.go:606
msgid "deploy timed out 🟠"
msgstr ""
@ -2577,11 +2577,11 @@ msgstr ""
msgid "dirty: %v, "
msgstr ""
#: cli/app/deploy.go:421 cli/app/rollback.go:370 cli/app/upgrade.go:479
#: cli/app/deploy.go:422 cli/app/rollback.go:371 cli/app/upgrade.go:480
msgid "disable converge logic checks"
msgstr ""
#: cli/app/deploy.go:413 cli/app/rollback.go:362 cli/app/upgrade.go:471
#: cli/app/deploy.go:414 cli/app/rollback.go:363 cli/app/upgrade.go:472
msgid "disable public DNS checks"
msgstr ""
@ -2803,8 +2803,8 @@ msgstr ""
#. translators: `abra recipe fetch` aliases. use a comma separated list of aliases
#. with no spaces in between
#: cli/app/deploy.go:403 cli/app/env.go:325 cli/app/remove.go:163
#: cli/app/rollback.go:352 cli/app/secret.go:593 cli/app/upgrade.go:461
#: cli/app/deploy.go:404 cli/app/env.go:325 cli/app/remove.go:163
#: cli/app/rollback.go:353 cli/app/secret.go:593 cli/app/upgrade.go:462
#: cli/app/volume.go:217 cli/recipe/fetch.go:20 cli/recipe/fetch.go:138
msgid "f"
msgstr ""
@ -2839,22 +2839,22 @@ msgstr ""
msgid "failed to copy %s from local machine to %s: output:%s err:%s"
msgstr ""
#: pkg/upstream/stack/stack.go:531
#: pkg/upstream/stack/stack.go:535
#, c-format
msgid "failed to create %s"
msgstr ""
#: pkg/upstream/stack/stack.go:393
#: pkg/upstream/stack/stack.go:397
#, c-format
msgid "failed to create config %s"
msgstr ""
#: pkg/upstream/stack/stack.go:424
#: pkg/upstream/stack/stack.go:428
#, c-format
msgid "failed to create network %s"
msgstr ""
#: pkg/upstream/stack/stack.go:371
#: pkg/upstream/stack/stack.go:375
#, c-format
msgid "failed to create secret %s"
msgstr ""
@ -2951,7 +2951,7 @@ msgstr ""
msgid "failed to retrieve latest commit for %s: %s"
msgstr ""
#: pkg/upstream/stack/stack.go:468
#: pkg/upstream/stack/stack.go:472
#, c-format
msgid "failed to retrieve registry auth for image %s: %s"
msgstr ""
@ -2971,17 +2971,17 @@ msgstr "🥷 Genera secretos (contraseñas) automáticamente 🤖"
msgid "failed to tag release: %s"
msgstr ""
#: pkg/upstream/stack/stack.go:508
#: pkg/upstream/stack/stack.go:512
#, c-format
msgid "failed to update %s"
msgstr ""
#: pkg/upstream/stack/stack.go:387
#: pkg/upstream/stack/stack.go:391
#, c-format
msgid "failed to update config %s"
msgstr ""
#: pkg/upstream/stack/stack.go:365
#: pkg/upstream/stack/stack.go:369
#, c-format
msgid "failed to update secret %s"
msgstr ""
@ -3036,8 +3036,8 @@ msgstr ""
msgid "final merged env values for %s are: %s"
msgstr ""
#: cli/app/deploy.go:402 cli/app/env.go:324 cli/app/remove.go:162
#: cli/app/rollback.go:351 cli/app/upgrade.go:460 cli/app/volume.go:216
#: cli/app/deploy.go:403 cli/app/env.go:324 cli/app/remove.go:162
#: cli/app/rollback.go:352 cli/app/upgrade.go:461 cli/app/volume.go:216
#: cli/recipe/fetch.go:137
msgid "force"
msgstr ""
@ -3254,9 +3254,9 @@ msgid "id: %s, "
msgstr ""
#: cli/app/backup.go:321 cli/app/backup.go:337 cli/app/check.go:97
#: cli/app/cmd.go:287 cli/app/cp.go:387 cli/app/deploy.go:397
#: cli/app/cmd.go:287 cli/app/cp.go:387 cli/app/deploy.go:398
#: cli/app/labels.go:145 cli/app/new.go:399 cli/app/ps.go:215
#: cli/app/restart.go:164 cli/app/restore.go:140 cli/app/secret.go:571
#: cli/app/restart.go:165 cli/app/restore.go:140 cli/app/secret.go:571
#: cli/app/secret.go:611 cli/app/secret.go:635 cli/app/secret.go:643
#: cli/catalogue/catalogue.go:320 cli/recipe/lint.go:139
msgid "ignore uncommitted recipes changes"
@ -3369,7 +3369,7 @@ msgstr ""
msgid "initialised new git repo in %s"
msgstr ""
#: pkg/upstream/stack/stack.go:206
#: pkg/upstream/stack/stack.go:207
msgid "initialising deployment"
msgstr ""
@ -3433,7 +3433,7 @@ msgstr ""
msgid "invalid npipe source, source cannot be empty"
msgstr ""
#: pkg/upstream/stack/stack.go:239
#: pkg/upstream/stack/stack.go:241
#, c-format
msgid "invalid option %s for flag --resolve-image"
msgstr ""
@ -3456,7 +3456,7 @@ msgstr ""
#. no spaces in between
#. translators: `abra recipe lint` aliases. use a comma separated list of
#. aliases with no spaces in between
#: cli/app/cmd.go:261 cli/app/deploy.go:427 cli/app/logs.go:20
#: cli/app/cmd.go:261 cli/app/deploy.go:428 cli/app/logs.go:20
#: cli/recipe/lint.go:17 cli/server/add.go:207
msgid "l"
msgstr ""
@ -3472,7 +3472,7 @@ msgstr ""
msgid "labels <domain> [flags]"
msgstr "etiquetas <domain> [flags]"
#: cli/app/deploy.go:426 cli/app/list.go:182
#: cli/app/deploy.go:427 cli/app/list.go:182
msgid "latest"
msgstr ""
@ -3562,12 +3562,12 @@ msgstr ""
msgid "logs <domain> [service] [flags]"
msgstr ""
#: pkg/upstream/stack/stack.go:628
#: pkg/upstream/stack/stack.go:639
#, c-format
msgid "logs: %s"
msgstr ""
#: pkg/upstream/stack/stack.go:630
#: pkg/upstream/stack/stack.go:641
msgid "logs: no log output received from deployment"
msgstr ""
@ -3714,12 +3714,12 @@ msgstr ""
msgid "need 3 or 4 arguments"
msgstr ""
#: pkg/upstream/stack/stack.go:348
#: pkg/upstream/stack/stack.go:352
#, c-format
msgid "network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed, which you can do by running this on the server: docker network create -d overlay proxy"
msgstr ""
#: pkg/upstream/stack/stack.go:352
#: pkg/upstream/stack/stack.go:356
#, c-format
msgid "network %q is declared as external, but it is not in the right scope: %q instead of \"swarm\""
msgstr ""
@ -3942,11 +3942,11 @@ msgstr ""
msgid "no volumes to remove"
msgstr ""
#: cli/app/deploy.go:418 cli/app/rollback.go:367 cli/app/upgrade.go:476
#: cli/app/deploy.go:419 cli/app/rollback.go:368 cli/app/upgrade.go:477
msgid "no-converge-checks"
msgstr ""
#: cli/app/deploy.go:410 cli/app/rollback.go:359 cli/app/upgrade.go:468
#: cli/app/deploy.go:411 cli/app/rollback.go:360 cli/app/upgrade.go:469
msgid "no-domain-checks"
msgstr ""
@ -4002,7 +4002,7 @@ msgstr ""
msgid "only show errors"
msgstr ""
#: cli/app/upgrade.go:487
#: cli/app/upgrade.go:488
msgid "only show release notes"
msgstr ""
@ -4036,22 +4036,22 @@ msgstr ""
msgid "parsed following command arguments: %s"
msgstr ""
#: cli/app/upgrade.go:344
#: cli/app/upgrade.go:345
#, c-format
msgid "parsing chosen upgrade version failed: %s"
msgstr ""
#: cli/app/upgrade.go:388
#: cli/app/upgrade.go:389
#, c-format
msgid "parsing deployed version failed: %s"
msgstr ""
#: cli/app/upgrade.go:349
#: cli/app/upgrade.go:350
#, c-format
msgid "parsing deployment version failed: %s"
msgstr ""
#: cli/app/upgrade.go:355 cli/app/upgrade.go:394
#: cli/app/upgrade.go:356 cli/app/upgrade.go:395
#, c-format
msgid "parsing recipe version failed: %s"
msgstr ""
@ -4079,8 +4079,8 @@ msgstr ""
msgid "pattern"
msgstr ""
#: cli/app/deploy.go:405 cli/app/env.go:327 cli/app/remove.go:165
#: cli/app/rollback.go:354 cli/app/upgrade.go:463 cli/app/volume.go:219
#: cli/app/deploy.go:406 cli/app/env.go:327 cli/app/remove.go:165
#: cli/app/rollback.go:355 cli/app/upgrade.go:464 cli/app/volume.go:219
msgid "perform action without further prompt"
msgstr ""
@ -4095,27 +4095,27 @@ msgstr ""
msgid "please fix your synced label for %s and re-run this command"
msgstr ""
#: cli/app/rollback.go:266
#: cli/app/rollback.go:267
#, c-format
msgid "please select a downgrade (version: %s):"
msgstr ""
#: cli/app/rollback.go:271
#: cli/app/rollback.go:272
#, c-format
msgid "please select a downgrade (version: %s, chaos: %s):"
msgstr ""
#: cli/app/upgrade.go:311
#: cli/app/upgrade.go:312
#, c-format
msgid "please select an upgrade (version: %s):"
msgstr ""
#: cli/app/upgrade.go:316
#: cli/app/upgrade.go:317
#, c-format
msgid "please select an upgrade (version: %s, chaos: %s):"
msgstr ""
#: pkg/upstream/stack/stack.go:576
#: pkg/upstream/stack/stack.go:587
msgid "polling deployment status"
msgstr ""
@ -4206,7 +4206,7 @@ msgstr ""
#. translators: `abra recipe` aliases. use a comma separated list of aliases
#. with no spaces in between
#: cli/app/backup.go:327 cli/app/list.go:300 cli/app/move.go:350
#: cli/app/run.go:23 cli/app/upgrade.go:485 cli/catalogue/catalogue.go:302
#: cli/app/run.go:23 cli/app/upgrade.go:486 cli/catalogue/catalogue.go:302
#: cli/recipe/recipe.go:12 cli/recipe/release.go:649 cli/recipe/sync.go:272
msgid "r"
msgstr ""
@ -4325,7 +4325,7 @@ msgstr ""
msgid "release <recipe> [version] [flags]"
msgstr "publicar <recipe> [version] [flags]"
#: cli/app/upgrade.go:484
#: cli/app/upgrade.go:485
msgid "releasenotes"
msgstr ""
@ -4492,7 +4492,7 @@ msgstr ""
msgid "restart <domain> [[service] | --all-services] [flags]"
msgstr "reiniciar <domain> [[service] | --all-services] [flags]"
#: cli/app/restart.go:171
#: cli/app/restart.go:172
msgid "restart all services"
msgstr ""
@ -4557,7 +4557,7 @@ msgstr ""
msgid "retrieved versions from local recipe repository"
msgstr ""
#: pkg/upstream/stack/stack.go:464
#: pkg/upstream/stack/stack.go:468
#, c-format
msgid "retrieving docker auth token: failed create docker cli: %s"
msgstr ""
@ -4631,7 +4631,7 @@ msgstr ""
msgid "run command locally"
msgstr ""
#: cli/app/deploy.go:270 cli/app/upgrade.go:292
#: cli/app/deploy.go:271 cli/app/upgrade.go:293
#, c-format
msgid "run the following post-deploy commands: %s"
msgstr ""
@ -4720,12 +4720,12 @@ msgstr ""
msgid "secret not found: %s"
msgstr ""
#: cli/app/deploy.go:339
#: cli/app/deploy.go:340
#, c-format
msgid "secret not generated: %s"
msgstr ""
#: cli/app/deploy.go:337
#: cli/app/deploy.go:338
#, c-format
msgid "secret not inserted (#generate=false): %s"
msgstr ""
@ -4774,11 +4774,21 @@ msgstr ""
msgid "server doesn't exist?"
msgstr ""
#: pkg/client/client.go:48
#: pkg/client/client.go:54
#, c-format
msgid "server missing context, context creation failed: %s"
msgstr ""
#: pkg/client/client.go:59
#, c-format
msgid "server missing context, run \"abra server add %s\"?"
msgstr ""
#: pkg/client/client.go:48
#, c-format
msgid "server missing, run \"abra server add %s\"?"
msgstr ""
#: cli/server/add.go:148
#, c-format
msgid "serverAdd: cleanUp: %s is not empty, aborting cleanup"
@ -4851,7 +4861,7 @@ msgstr ""
msgid "severity"
msgstr ""
#: cli/app/deploy.go:437 cli/app/rollback.go:378 cli/app/upgrade.go:495
#: cli/app/deploy.go:438 cli/app/rollback.go:379 cli/app/upgrade.go:496
msgid "show all configs & images, including unchanged ones"
msgstr ""
@ -4875,7 +4885,7 @@ msgstr ""
msgid "show debug messages"
msgstr ""
#: cli/app/deploy.go:434 cli/app/rollback.go:375 cli/app/upgrade.go:492
#: cli/app/deploy.go:435 cli/app/rollback.go:376 cli/app/upgrade.go:493
msgid "show-unchanged"
msgstr ""
@ -4919,7 +4929,7 @@ msgstr ""
msgid "skipping as requested, undeploy still in progress 🟠"
msgstr ""
#: pkg/upstream/stack/stack.go:306
#: pkg/upstream/stack/stack.go:309
msgid "skipping converge logic checks"
msgstr ""
@ -4941,12 +4951,12 @@ msgstr ""
msgid "skipping secret (because it already exists) on %s: %s"
msgstr ""
#: pkg/app/app.go:692
#: pkg/app/app.go:697
#, c-format
msgid "skipping version %s write as already exists in %s.env"
msgstr ""
#: pkg/app/app.go:686
#: pkg/app/app.go:691
#, c-format
msgid "skipping writing version %s because dry run"
msgstr ""
@ -5054,12 +5064,12 @@ msgstr ""
msgid "successfully created %s"
msgstr ""
#: pkg/client/client.go:111
#: pkg/client/client.go:120
#, c-format
msgid "swarm mode not enabled on %s?"
msgstr ""
#: pkg/client/client.go:114
#: pkg/client/client.go:123
msgid "swarm mode not enabled on local server?"
msgstr ""
@ -5136,7 +5146,7 @@ msgstr ""
msgid "timeout label: %s"
msgstr ""
#: pkg/upstream/stack/remove.go:29 pkg/upstream/stack/stack.go:209
#: pkg/upstream/stack/remove.go:29 pkg/upstream/stack/stack.go:210
#, c-format
msgid "timeout: set to %d second(s)"
msgstr ""
@ -5565,11 +5575,6 @@ msgstr ""
msgid "unknown server %s, run \"abra server add %s\"?"
msgstr ""
#: pkg/client/client.go:51
#, c-format
msgid "unknown server, run \"abra server add %s\"?"
msgstr ""
#: cli/app/cp.go:259
#, c-format
msgid "untar: %s"
@ -5581,7 +5586,7 @@ msgstr ""
msgid "up"
msgstr ""
#: pkg/upstream/stack/stack.go:473
#: pkg/upstream/stack/stack.go:477
#, c-format
msgid "updating %s"
msgstr ""
@ -5694,7 +5699,7 @@ msgstr ""
msgid "version"
msgstr ""
#: pkg/app/app.go:690
#: pkg/app/app.go:695
#, c-format
msgid "version %s saved to %s.env"
msgstr ""
@ -5713,6 +5718,10 @@ msgstr ""
msgid "version for abra"
msgstr ""
#: pkg/app/app.go:637
msgid "version is unknown, skipping env write"
msgstr ""
#: pkg/recipe/recipe.go:130
#, c-format
msgid "version seems invalid: %s"
@ -5723,27 +5732,27 @@ msgstr ""
msgid "version wiped from %s.env"
msgstr ""
#: cli/app/deploy.go:353
#: cli/app/deploy.go:354
#, c-format
msgid "version: taking chaos version: %s"
msgstr ""
#: cli/app/deploy.go:379
#: cli/app/deploy.go:380
#, c-format
msgid "version: taking deployed version: %s"
msgstr ""
#: cli/app/deploy.go:384
#: cli/app/deploy.go:385
#, c-format
msgid "version: taking new recipe version: %s"
msgstr ""
#: cli/app/deploy.go:373
#: cli/app/deploy.go:374
#, c-format
msgid "version: taking version from .env file: %s"
msgstr ""
#: cli/app/deploy.go:359
#: cli/app/deploy.go:360
#, c-format
msgid "version: taking version from cli arg: %s"
msgstr ""
@ -5803,22 +5812,22 @@ msgstr ""
msgid "volumes pruned: %d; space reclaimed: %s"
msgstr ""
#: pkg/upstream/stack/stack.go:614
#: pkg/upstream/stack/stack.go:625
#, c-format
msgid "waitOnServices: error creating log dir: %s"
msgstr ""
#: pkg/upstream/stack/stack.go:619
#: pkg/upstream/stack/stack.go:630
#, c-format
msgid "waitOnServices: error opening file: %s"
msgstr ""
#: pkg/upstream/stack/stack.go:585
#: pkg/upstream/stack/stack.go:596
#, c-format
msgid "waitOnServices: error running TUI: %s"
msgstr ""
#: pkg/upstream/stack/stack.go:625
#: pkg/upstream/stack/stack.go:636
#, c-format
msgid "waitOnServices: writeFile: %s"
msgstr ""
@ -5869,8 +5878,8 @@ msgstr ""
msgid "writer: %v, "
msgstr ""
#: cli/app/deploy.go:277 cli/app/new.go:241 cli/app/rollback.go:255
#: cli/app/undeploy.go:120 cli/app/upgrade.go:300
#: cli/app/deploy.go:278 cli/app/new.go:241 cli/app/rollback.go:256
#: cli/app/undeploy.go:120 cli/app/upgrade.go:301
#, c-format
msgid "writing recipe version failed: %s"
msgstr ""

View File

@ -19,7 +19,7 @@ import (
"coopcloud.tech/abra/pkg/ui"
"coopcloud.tech/abra/pkg/upstream/convert"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/stack/formatter"
"github.com/docker/cli/cli/command/formatter"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
@ -201,6 +201,7 @@ func RunDeploy(
appName string,
serverName string,
dontWait bool,
noInput bool,
filters filters.Args,
) error {
log.Info(i18n.G("initialising deployment"))
@ -226,6 +227,7 @@ func RunDeploy(
appName,
serverName,
dontWait,
noInput,
filters,
)
}
@ -248,6 +250,7 @@ func deployCompose(
appName string,
serverName string,
dontWait bool,
noInput bool,
filters filters.Args,
) error {
namespace := convert.NewNamespace(opts.Namespace)
@ -311,6 +314,7 @@ func deployCompose(
Services: serviceIDs,
AppName: appName,
ServerName: serverName,
NoInput: noInput,
Filters: filters,
}
@ -561,6 +565,7 @@ func timestamp() string {
type WaitOpts struct {
AppName string
Filters filters.Args
NoInput bool
NoLog bool
Quiet bool
ServerName string
@ -570,7 +575,13 @@ type WaitOpts struct {
func WaitOnServices(ctx context.Context, cl *dockerClient.Client, opts WaitOpts) error {
timeout := time.Duration(WaitTimeout) * time.Second
model := ui.DeployInitialModel(ctx, cl, opts.Services, opts.AppName, timeout, opts.Filters)
tui := tea.NewProgram(model)
var tui *tea.Program
if opts.NoInput {
tui = tea.NewProgram(model, tea.WithoutRenderer(), tea.WithInput(nil))
} else {
tui = tea.NewProgram(model)
}
if !opts.Quiet {
log.Info(i18n.G("polling deployment status"))

View File

@ -1,8 +1,8 @@
#!/usr/bin/env bash
ABRA_VERSION="0.11.0-beta"
ABRA_VERSION="0.12.0-beta"
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$ABRA_VERSION"
RC_VERSION="0.11.0-beta"
RC_VERSION="0.12.0-beta"
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$RC_VERSION"
for arg in "$@"; do
@ -14,15 +14,15 @@ done
function show_banner {
echo ""
echo " ____ ____ _ _ "
echo " / ___|___ ___ _ __ / ___| | ___ _ _ __| |"
echo " | | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |"
echo " | |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |"
echo " \____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|"
echo " |_|"
echo " ____ ____ _ _ "
echo " / ___|___ ___ _ __ / ___| | ___ _ _ __| |"
echo " | | / _ \ ___ / _ \| '_ \ | | | |/ _ \| | | |/ _' |"
echo " | |__| (_) |___| (_) | |_) | | |___| | (_) | |_| | (_| |"
echo " \____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|"
echo " |_|"
echo ""
echo ""
echo " === Public interest infrastructure === "
echo " === Public interest infrastructure === "
echo ""
echo ""
}

View File

@ -543,7 +543,7 @@ teardown(){
# bats test_tags=slow
@test "ignore timeout when not present in env" {
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks --debug
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
refute_output --partial "timeout: set to"
}
@ -554,6 +554,7 @@ teardown(){
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
# NOTE(d1}: --debug required
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks --debug
assert_success
assert_output --partial "timeout: set to 120"
@ -579,16 +580,25 @@ teardown(){
}
# bats test_tags=slow
@test "manually created server without context bails gracefully" {
run mkdir -p "$ABRA_DIR/servers/default2"
@test "re-deploy updates existing env vars" {
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
assert_success
assert_exists "$ABRA_DIR/servers/default2"
run cp "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/default2/$TEST_APP_DOMAIN_2.env"
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
assert_success
assert_exists "$ABRA_DIR/servers/default2/$TEST_APP_DOMAIN_2.env"
assert_output --partial "WITH_COMMENT=foo"
run $ABRA app deploy "$TEST_APP_DOMAIN_2" --no-input --no-converge-checks
assert_failure
assert_output --partial "server missing context"
run sed -i 's/WITH_COMMENT=foo/WITH_COMMENT=bar/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --force
assert_success
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
assert_success
refute_output --partial "WITH_COMMENT=foo"
assert_output --partial "WITH_COMMENT=bar"
}

View File

@ -68,6 +68,13 @@ teardown(){
assert_success
}
@test "domain shown with https" {
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks
assert_success
assert_output --partial "https://$TEST_DOMAIN"
}
# bats test_tags=slow
@test "show changed config version on re-deploy" {
run $ABRA app deploy "$TEST_APP_DOMAIN" \

View File

@ -160,23 +160,6 @@ teardown(){
assert_not_exists "$ABRA_DIR/servers/foo.com"
}
@test "list with status skips unknown servers" {
if [[ ! -d "$ABRA_DIR/servers/foo" ]]; then
run mkdir -p "$ABRA_DIR/servers/foo"
assert_success
assert_exists "$ABRA_DIR/servers/foo"
run cp "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" \
"$ABRA_DIR/servers/foo/$TEST_APP_DOMAIN.env"
assert_success
assert_exists "$ABRA_DIR/servers/foo/$TEST_APP_DOMAIN.env"
fi
run $ABRA app ls --status
assert_success
assert_output --partial "server missing context"
}
# bats test_tags=slow
@test "list does not fail if missing .env" {
_deploy_app

View File

@ -19,6 +19,10 @@ setup(){
teardown(){
_reset_recipe
_reset_tags
if [[ -d "$ABRA_DIR/recipes/foobar" ]]; then
run rm -rf "$ABRA_DIR/recipes/foobar"
assert_success
fi
}
@test "validate recipe argument" {
@ -126,3 +130,71 @@ teardown(){
assert_line --index 0 --partial 'synced label'
refute_line --index 1 --partial 'synced label'
}
@test "sync with no tags or previous release" {
_remove_tags
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
assert_success
assert_output --partial 'image: nginx:1.21.6'
# NOTE(d1): ensure the latest tag is the one we expect
_remove_tags
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
-a "0.3.0+1.21.0" -m "fake: 0.3.0+1.21.0"
assert_success
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
assert_success
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.3\.1\+1\.2.*'
}
@test "sync recipe without input fails with prompt" {
run $ABRA recipe new foobar
assert_success
assert_exists "$ABRA_DIR/recipes/foobar"
run $ABRA recipe sync foobar --no-input --patch
assert_failure
assert_output --partial "input required for initial version"
}
@test "sync new recipe: development release" {
run $ABRA recipe new foobar
assert_success
assert_exists "$ABRA_DIR/recipes/foobar"
run bash -c "echo 0.1.0 | $ABRA recipe sync foobar --patch"
assert_success
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.1\.0\+1\.2.*'
}
@test "sync new recipe: public release" {
run $ABRA recipe new foobar
assert_success
assert_exists "$ABRA_DIR/recipes/foobar"
run bash -c "echo 1.0.0 | $ABRA recipe sync foobar --patch"
assert_success
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=1\.0\.0\+1\.2.*'
}
@test "sync newly created recipe with no version label" {
run $ABRA recipe new foobar
assert_success
assert_exists "$ABRA_DIR/recipes/foobar"
run sed -i 's/- "coop-cloud.${STACK_NAME}.version="/#- "coop-cloud.${STACK_NAME}.version="/g' \
"$ABRA_DIR/recipes/foobar/compose.yml"
assert_success
run bash -c "echo 0.1.0 | $ABRA recipe sync foobar --patch"
assert_failure
assert_output --partial "automagic insertion not supported yet"
}

View File

@ -1,22 +0,0 @@
language: go
os:
- linux
- osx
- windows
go:
- go1.13.x
- go1.x
services:
- xvfb
before_install:
- export DISPLAY=:99.0
script:
- if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get install xsel; fi
- go test -v .
- if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get install xclip; fi
- go test -v .

View File

@ -1,27 +0,0 @@
Copyright (c) 2013 Ato Araki. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of @atotto. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,48 +0,0 @@
[![Build Status](https://travis-ci.org/atotto/clipboard.svg?branch=master)](https://travis-ci.org/atotto/clipboard)
[![GoDoc](https://godoc.org/github.com/atotto/clipboard?status.svg)](http://godoc.org/github.com/atotto/clipboard)
# Clipboard for Go
Provide copying and pasting to the Clipboard for Go.
Build:
$ go get github.com/atotto/clipboard
Platforms:
* OSX
* Windows 7 (probably work on other Windows)
* Linux, Unix (requires 'xclip' or 'xsel' command to be installed)
Document:
* http://godoc.org/github.com/atotto/clipboard
Notes:
* Text string only
* UTF-8 text encoding only (no conversion)
TODO:
* Clipboard watcher(?)
## Commands:
paste shell command:
$ go get github.com/atotto/clipboard/cmd/gopaste
$ # example:
$ gopaste > document.txt
copy shell command:
$ go get github.com/atotto/clipboard/cmd/gocopy
$ # example:
$ cat document.txt | gocopy

View File

@ -1,20 +0,0 @@
// Copyright 2013 @atotto. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package clipboard read/write on clipboard
package clipboard
// ReadAll read string from clipboard
func ReadAll() (string, error) {
return readAll()
}
// WriteAll write string to clipboard
func WriteAll(text string) error {
return writeAll(text)
}
// Unsupported might be set true during clipboard init, to help callers decide
// whether or not to offer clipboard options.
var Unsupported bool

View File

@ -1,52 +0,0 @@
// Copyright 2013 @atotto. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin
package clipboard
import (
"os/exec"
)
var (
pasteCmdArgs = "pbpaste"
copyCmdArgs = "pbcopy"
)
func getPasteCommand() *exec.Cmd {
return exec.Command(pasteCmdArgs)
}
func getCopyCommand() *exec.Cmd {
return exec.Command(copyCmdArgs)
}
func readAll() (string, error) {
pasteCmd := getPasteCommand()
out, err := pasteCmd.Output()
if err != nil {
return "", err
}
return string(out), nil
}
func writeAll(text string) error {
copyCmd := getCopyCommand()
in, err := copyCmd.StdinPipe()
if err != nil {
return err
}
if err := copyCmd.Start(); err != nil {
return err
}
if _, err := in.Write([]byte(text)); err != nil {
return err
}
if err := in.Close(); err != nil {
return err
}
return copyCmd.Wait()
}

View File

@ -1,42 +0,0 @@
// Copyright 2013 @atotto. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build plan9
package clipboard
import (
"os"
"io/ioutil"
)
func readAll() (string, error) {
f, err := os.Open("/dev/snarf")
if err != nil {
return "", err
}
defer f.Close()
str, err := ioutil.ReadAll(f)
if err != nil {
return "", err
}
return string(str), nil
}
func writeAll(text string) error {
f, err := os.OpenFile("/dev/snarf", os.O_WRONLY, 0666)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write([]byte(text))
if err != nil {
return err
}
return nil
}

View File

@ -1,149 +0,0 @@
// Copyright 2013 @atotto. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build freebsd linux netbsd openbsd solaris dragonfly
package clipboard
import (
"errors"
"os"
"os/exec"
)
const (
xsel = "xsel"
xclip = "xclip"
powershellExe = "powershell.exe"
clipExe = "clip.exe"
wlcopy = "wl-copy"
wlpaste = "wl-paste"
termuxClipboardGet = "termux-clipboard-get"
termuxClipboardSet = "termux-clipboard-set"
)
var (
Primary bool
trimDos bool
pasteCmdArgs []string
copyCmdArgs []string
xselPasteArgs = []string{xsel, "--output", "--clipboard"}
xselCopyArgs = []string{xsel, "--input", "--clipboard"}
xclipPasteArgs = []string{xclip, "-out", "-selection", "clipboard"}
xclipCopyArgs = []string{xclip, "-in", "-selection", "clipboard"}
powershellExePasteArgs = []string{powershellExe, "Get-Clipboard"}
clipExeCopyArgs = []string{clipExe}
wlpasteArgs = []string{wlpaste, "--no-newline"}
wlcopyArgs = []string{wlcopy}
termuxPasteArgs = []string{termuxClipboardGet}
termuxCopyArgs = []string{termuxClipboardSet}
missingCommands = errors.New("No clipboard utilities available. Please install xsel, xclip, wl-clipboard or Termux:API add-on for termux-clipboard-get/set.")
)
func init() {
if os.Getenv("WAYLAND_DISPLAY") != "" {
pasteCmdArgs = wlpasteArgs
copyCmdArgs = wlcopyArgs
if _, err := exec.LookPath(wlcopy); err == nil {
if _, err := exec.LookPath(wlpaste); err == nil {
return
}
}
}
pasteCmdArgs = xclipPasteArgs
copyCmdArgs = xclipCopyArgs
if _, err := exec.LookPath(xclip); err == nil {
return
}
pasteCmdArgs = xselPasteArgs
copyCmdArgs = xselCopyArgs
if _, err := exec.LookPath(xsel); err == nil {
return
}
pasteCmdArgs = termuxPasteArgs
copyCmdArgs = termuxCopyArgs
if _, err := exec.LookPath(termuxClipboardSet); err == nil {
if _, err := exec.LookPath(termuxClipboardGet); err == nil {
return
}
}
pasteCmdArgs = powershellExePasteArgs
copyCmdArgs = clipExeCopyArgs
trimDos = true
if _, err := exec.LookPath(clipExe); err == nil {
if _, err := exec.LookPath(powershellExe); err == nil {
return
}
}
Unsupported = true
}
func getPasteCommand() *exec.Cmd {
if Primary {
pasteCmdArgs = pasteCmdArgs[:1]
}
return exec.Command(pasteCmdArgs[0], pasteCmdArgs[1:]...)
}
func getCopyCommand() *exec.Cmd {
if Primary {
copyCmdArgs = copyCmdArgs[:1]
}
return exec.Command(copyCmdArgs[0], copyCmdArgs[1:]...)
}
func readAll() (string, error) {
if Unsupported {
return "", missingCommands
}
pasteCmd := getPasteCommand()
out, err := pasteCmd.Output()
if err != nil {
return "", err
}
result := string(out)
if trimDos && len(result) > 1 {
result = result[:len(result)-2]
}
return result, nil
}
func writeAll(text string) error {
if Unsupported {
return missingCommands
}
copyCmd := getCopyCommand()
in, err := copyCmd.StdinPipe()
if err != nil {
return err
}
if err := copyCmd.Start(); err != nil {
return err
}
if _, err := in.Write([]byte(text)); err != nil {
return err
}
if err := in.Close(); err != nil {
return err
}
return copyCmd.Wait()
}

View File

@ -1,157 +0,0 @@
// Copyright 2013 @atotto. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build windows
package clipboard
import (
"runtime"
"syscall"
"time"
"unsafe"
)
const (
cfUnicodetext = 13
gmemMoveable = 0x0002
)
var (
user32 = syscall.MustLoadDLL("user32")
isClipboardFormatAvailable = user32.MustFindProc("IsClipboardFormatAvailable")
openClipboard = user32.MustFindProc("OpenClipboard")
closeClipboard = user32.MustFindProc("CloseClipboard")
emptyClipboard = user32.MustFindProc("EmptyClipboard")
getClipboardData = user32.MustFindProc("GetClipboardData")
setClipboardData = user32.MustFindProc("SetClipboardData")
kernel32 = syscall.NewLazyDLL("kernel32")
globalAlloc = kernel32.NewProc("GlobalAlloc")
globalFree = kernel32.NewProc("GlobalFree")
globalLock = kernel32.NewProc("GlobalLock")
globalUnlock = kernel32.NewProc("GlobalUnlock")
lstrcpy = kernel32.NewProc("lstrcpyW")
)
// waitOpenClipboard opens the clipboard, waiting for up to a second to do so.
func waitOpenClipboard() error {
started := time.Now()
limit := started.Add(time.Second)
var r uintptr
var err error
for time.Now().Before(limit) {
r, _, err = openClipboard.Call(0)
if r != 0 {
return nil
}
time.Sleep(time.Millisecond)
}
return err
}
func readAll() (string, error) {
// LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution).
// Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if formatAvailable, _, err := isClipboardFormatAvailable.Call(cfUnicodetext); formatAvailable == 0 {
return "", err
}
err := waitOpenClipboard()
if err != nil {
return "", err
}
h, _, err := getClipboardData.Call(cfUnicodetext)
if h == 0 {
_, _, _ = closeClipboard.Call()
return "", err
}
l, _, err := globalLock.Call(h)
if l == 0 {
_, _, _ = closeClipboard.Call()
return "", err
}
text := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(l))[:])
r, _, err := globalUnlock.Call(h)
if r == 0 {
_, _, _ = closeClipboard.Call()
return "", err
}
closed, _, err := closeClipboard.Call()
if closed == 0 {
return "", err
}
return text, nil
}
func writeAll(text string) error {
// LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution).
// Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
err := waitOpenClipboard()
if err != nil {
return err
}
r, _, err := emptyClipboard.Call(0)
if r == 0 {
_, _, _ = closeClipboard.Call()
return err
}
data := syscall.StringToUTF16(text)
// "If the hMem parameter identifies a memory object, the object must have
// been allocated using the function with the GMEM_MOVEABLE flag."
h, _, err := globalAlloc.Call(gmemMoveable, uintptr(len(data)*int(unsafe.Sizeof(data[0]))))
if h == 0 {
_, _, _ = closeClipboard.Call()
return err
}
defer func() {
if h != 0 {
globalFree.Call(h)
}
}()
l, _, err := globalLock.Call(h)
if l == 0 {
_, _, _ = closeClipboard.Call()
return err
}
r, _, err = lstrcpy.Call(l, uintptr(unsafe.Pointer(&data[0])))
if r == 0 {
_, _, _ = closeClipboard.Call()
return err
}
r, _, err = globalUnlock.Call(h)
if r == 0 {
if err.(syscall.Errno) != 0 {
_, _, _ = closeClipboard.Call()
return err
}
}
r, _, err = setClipboardData.Call(cfUnicodetext, h)
if r == 0 {
_, _, _ = closeClipboard.Call()
return err
}
h = 0 // suppress deferred cleanup
closed, _, err := closeClipboard.Call()
if closed == 0 {
return err
}
return nil
}

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2020-2023 Charmbracelet, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,219 +0,0 @@
// Package cursor provides cursor functionality for Bubble Tea applications.
package cursor
import (
"context"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
const defaultBlinkSpeed = time.Millisecond * 530
// initialBlinkMsg initializes cursor blinking.
type initialBlinkMsg struct{}
// BlinkMsg signals that the cursor should blink. It contains metadata that
// allows us to tell if the blink message is the one we're expecting.
type BlinkMsg struct {
id int
tag int
}
// blinkCanceled is sent when a blink operation is canceled.
type blinkCanceled struct{}
// blinkCtx manages cursor blinking.
type blinkCtx struct {
ctx context.Context
cancel context.CancelFunc
}
// Mode describes the behavior of the cursor.
type Mode int
// Available cursor modes.
const (
CursorBlink Mode = iota
CursorStatic
CursorHide
)
// String returns the cursor mode in a human-readable format. This method is
// provisional and for informational purposes only.
func (c Mode) String() string {
return [...]string{
"blink",
"static",
"hidden",
}[c]
}
// Model is the Bubble Tea model for this cursor element.
type Model struct {
BlinkSpeed time.Duration
// Style for styling the cursor block.
Style lipgloss.Style
// TextStyle is the style used for the cursor when it is hidden (when blinking).
// I.e. displaying normal text.
TextStyle lipgloss.Style
// char is the character under the cursor
char string
// The ID of this Model as it relates to other cursors
id int
// focus indicates whether the containing input is focused
focus bool
// Cursor Blink state.
Blink bool
// Used to manage cursor blink
blinkCtx *blinkCtx
// The ID of the blink message we're expecting to receive.
blinkTag int
// mode determines the behavior of the cursor
mode Mode
}
// New creates a new model with default settings.
func New() Model {
return Model{
BlinkSpeed: defaultBlinkSpeed,
Blink: true,
mode: CursorBlink,
blinkCtx: &blinkCtx{
ctx: context.Background(),
},
}
}
// Update updates the cursor.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case initialBlinkMsg:
// We accept all initialBlinkMsgs generated by the Blink command.
if m.mode != CursorBlink || !m.focus {
return m, nil
}
cmd := m.BlinkCmd()
return m, cmd
case tea.FocusMsg:
return m, m.Focus()
case tea.BlurMsg:
m.Blur()
return m, nil
case BlinkMsg:
// We're choosy about whether to accept blinkMsgs so that our cursor
// only exactly when it should.
// Is this model blink-able?
if m.mode != CursorBlink || !m.focus {
return m, nil
}
// Were we expecting this blink message?
if msg.id != m.id || msg.tag != m.blinkTag {
return m, nil
}
var cmd tea.Cmd
if m.mode == CursorBlink {
m.Blink = !m.Blink
cmd = m.BlinkCmd()
}
return m, cmd
case blinkCanceled: // no-op
return m, nil
}
return m, nil
}
// Mode returns the model's cursor mode. For available cursor modes, see
// type Mode.
func (m Model) Mode() Mode {
return m.mode
}
// SetMode sets the model's cursor mode. This method returns a command.
//
// For available cursor modes, see type CursorMode.
func (m *Model) SetMode(mode Mode) tea.Cmd {
// Adjust the mode value if it's value is out of range
if mode < CursorBlink || mode > CursorHide {
return nil
}
m.mode = mode
m.Blink = m.mode == CursorHide || !m.focus
if mode == CursorBlink {
return Blink
}
return nil
}
// BlinkCmd is a command used to manage cursor blinking.
func (m *Model) BlinkCmd() tea.Cmd {
if m.mode != CursorBlink {
return nil
}
if m.blinkCtx != nil && m.blinkCtx.cancel != nil {
m.blinkCtx.cancel()
}
ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed)
m.blinkCtx.cancel = cancel
m.blinkTag++
return func() tea.Msg {
defer cancel()
<-ctx.Done()
if ctx.Err() == context.DeadlineExceeded {
return BlinkMsg{id: m.id, tag: m.blinkTag}
}
return blinkCanceled{}
}
}
// Blink is a command used to initialize cursor blinking.
func Blink() tea.Msg {
return initialBlinkMsg{}
}
// Focus focuses the cursor to allow it to blink if desired.
func (m *Model) Focus() tea.Cmd {
m.focus = true
m.Blink = m.mode == CursorHide // show the cursor unless we've explicitly hidden it
if m.mode == CursorBlink && m.focus {
return m.BlinkCmd()
}
return nil
}
// Blur blurs the cursor.
func (m *Model) Blur() {
m.focus = false
m.Blink = true
}
// SetChar sets the character under the cursor.
func (m *Model) SetChar(char string) {
m.char = char
}
// View displays the cursor.
func (m Model) View() string {
if m.Blink {
return m.TextStyle.Inline(true).Render(m.char)
}
return m.Style.Inline(true).Reverse(true).Render(m.char)
}

View File

@ -1,140 +0,0 @@
// Package key provides some types and functions for generating user-definable
// keymappings useful in Bubble Tea components. There are a few different ways
// you can define a keymapping with this package. Here's one example:
//
// type KeyMap struct {
// Up key.Binding
// Down key.Binding
// }
//
// var DefaultKeyMap = KeyMap{
// Up: key.NewBinding(
// key.WithKeys("k", "up"), // actual keybindings
// key.WithHelp("↑/k", "move up"), // corresponding help text
// ),
// Down: key.NewBinding(
// key.WithKeys("j", "down"),
// key.WithHelp("↓/j", "move down"),
// ),
// }
//
// func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// switch msg := msg.(type) {
// case tea.KeyMsg:
// switch {
// case key.Matches(msg, DefaultKeyMap.Up):
// // The user pressed up
// case key.Matches(msg, DefaultKeyMap.Down):
// // The user pressed down
// }
// }
//
// // ...
// }
//
// The help information, which is not used in the example above, can be used
// to render help text for keystrokes in your views.
package key
import "fmt"
// Binding describes a set of keybindings and, optionally, their associated
// help text.
type Binding struct {
keys []string
help Help
disabled bool
}
// BindingOpt is an initialization option for a keybinding. It's used as an
// argument to NewBinding.
type BindingOpt func(*Binding)
// NewBinding returns a new keybinding from a set of BindingOpt options.
func NewBinding(opts ...BindingOpt) Binding {
b := &Binding{}
for _, opt := range opts {
opt(b)
}
return *b
}
// WithKeys initializes a keybinding with the given keystrokes.
func WithKeys(keys ...string) BindingOpt {
return func(b *Binding) {
b.keys = keys
}
}
// WithHelp initializes a keybinding with the given help text.
func WithHelp(key, desc string) BindingOpt {
return func(b *Binding) {
b.help = Help{Key: key, Desc: desc}
}
}
// WithDisabled initializes a disabled keybinding.
func WithDisabled() BindingOpt {
return func(b *Binding) {
b.disabled = true
}
}
// SetKeys sets the keys for the keybinding.
func (b *Binding) SetKeys(keys ...string) {
b.keys = keys
}
// Keys returns the keys for the keybinding.
func (b Binding) Keys() []string {
return b.keys
}
// SetHelp sets the help text for the keybinding.
func (b *Binding) SetHelp(key, desc string) {
b.help = Help{Key: key, Desc: desc}
}
// Help returns the Help information for the keybinding.
func (b Binding) Help() Help {
return b.help
}
// Enabled returns whether or not the keybinding is enabled. Disabled
// keybindings won't be activated and won't show up in help. Keybindings are
// enabled by default.
func (b Binding) Enabled() bool {
return !b.disabled && b.keys != nil
}
// SetEnabled enables or disables the keybinding.
func (b *Binding) SetEnabled(v bool) {
b.disabled = !v
}
// Unbind removes the keys and help from this binding, effectively nullifying
// it. This is a step beyond disabling it, since applications can enable
// or disable key bindings based on application state.
func (b *Binding) Unbind() {
b.keys = nil
b.help = Help{}
}
// Help is help information for a given keybinding.
type Help struct {
Key string
Desc string
}
// Matches checks if the given key matches the given bindings.
func Matches[Key fmt.Stringer](k Key, b ...Binding) bool {
keys := k.String()
for _, binding := range b {
for _, v := range binding.keys {
if keys == v && binding.Enabled() {
return true
}
}
}
return false
}

View File

@ -1,102 +0,0 @@
// Package runeutil provides a utility function for use in Bubbles
// that can process Key messages containing runes.
package runeutil
import (
"unicode"
"unicode/utf8"
)
// Sanitizer is a helper for bubble widgets that want to process
// Runes from input key messages.
type Sanitizer interface {
// Sanitize removes control characters from runes in a KeyRunes
// message, and optionally replaces newline/carriage return/tabs by a
// specified character.
//
// The rune array is modified in-place if possible. In that case, the
// returned slice is the original slice shortened after the control
// characters have been removed/translated.
Sanitize(runes []rune) []rune
}
// NewSanitizer constructs a rune sanitizer.
func NewSanitizer(opts ...Option) Sanitizer {
s := sanitizer{
replaceNewLine: []rune("\n"),
replaceTab: []rune(" "),
}
for _, o := range opts {
s = o(s)
}
return &s
}
// Option is the type of option that can be passed to Sanitize().
type Option func(sanitizer) sanitizer
// ReplaceTabs replaces tabs by the specified string.
func ReplaceTabs(tabRepl string) Option {
return func(s sanitizer) sanitizer {
s.replaceTab = []rune(tabRepl)
return s
}
}
// ReplaceNewlines replaces newline characters by the specified string.
func ReplaceNewlines(nlRepl string) Option {
return func(s sanitizer) sanitizer {
s.replaceNewLine = []rune(nlRepl)
return s
}
}
func (s *sanitizer) Sanitize(runes []rune) []rune {
// dstrunes are where we are storing the result.
dstrunes := runes[:0:len(runes)]
// copied indicates whether dstrunes is an alias of runes
// or a copy. We need a copy when dst moves past src.
// We use this as an optimization to avoid allocating
// a new rune slice in the common case where the output
// is smaller or equal to the input.
copied := false
for src := 0; src < len(runes); src++ {
r := runes[src]
switch {
case r == utf8.RuneError:
// skip
case r == '\r' || r == '\n':
if len(dstrunes)+len(s.replaceNewLine) > src && !copied {
dst := len(dstrunes)
dstrunes = make([]rune, dst, len(runes)+len(s.replaceNewLine))
copy(dstrunes, runes[:dst])
copied = true
}
dstrunes = append(dstrunes, s.replaceNewLine...)
case r == '\t':
if len(dstrunes)+len(s.replaceTab) > src && !copied {
dst := len(dstrunes)
dstrunes = make([]rune, dst, len(runes)+len(s.replaceTab))
copy(dstrunes, runes[:dst])
copied = true
}
dstrunes = append(dstrunes, s.replaceTab...)
case unicode.IsControl(r):
// Other control characters: skip.
default:
// Keep the character.
dstrunes = append(dstrunes, runes[src])
}
}
return dstrunes
}
type sanitizer struct {
replaceNewLine []rune
replaceTab []rune
}

View File

@ -1,224 +0,0 @@
// Package spinner provides a spinner component for Bubble Tea applications.
package spinner
import (
"sync/atomic"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// Internal ID management. Used during animating to ensure that frame messages
// are received only by spinner components that sent them.
var lastID int64
func nextID() int {
return int(atomic.AddInt64(&lastID, 1))
}
// Spinner is a set of frames used in animating the spinner.
type Spinner struct {
Frames []string
FPS time.Duration
}
// Some spinners to choose from. You could also make your own.
var (
Line = Spinner{
Frames: []string{"|", "/", "-", "\\"},
FPS: time.Second / 10, //nolint:mnd
}
Dot = Spinner{
Frames: []string{"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "},
FPS: time.Second / 10, //nolint:mnd
}
MiniDot = Spinner{
Frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
FPS: time.Second / 12, //nolint:mnd
}
Jump = Spinner{
Frames: []string{"⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"},
FPS: time.Second / 10, //nolint:mnd
}
Pulse = Spinner{
Frames: []string{"█", "▓", "▒", "░"},
FPS: time.Second / 8, //nolint:mnd
}
Points = Spinner{
Frames: []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●"},
FPS: time.Second / 7, //nolint:mnd
}
Globe = Spinner{
Frames: []string{"🌍", "🌎", "🌏"},
FPS: time.Second / 4, //nolint:mnd
}
Moon = Spinner{
Frames: []string{"🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"},
FPS: time.Second / 8, //nolint:mnd
}
Monkey = Spinner{
Frames: []string{"🙈", "🙉", "🙊"},
FPS: time.Second / 3, //nolint:mnd
}
Meter = Spinner{
Frames: []string{
"▱▱▱",
"▰▱▱",
"▰▰▱",
"▰▰▰",
"▰▰▱",
"▰▱▱",
"▱▱▱",
},
FPS: time.Second / 7, //nolint:mnd
}
Hamburger = Spinner{
Frames: []string{"☱", "☲", "☴", "☲"},
FPS: time.Second / 3, //nolint:mnd
}
Ellipsis = Spinner{
Frames: []string{"", ".", "..", "..."},
FPS: time.Second / 3, //nolint:mnd
}
)
// Model contains the state for the spinner. Use New to create new models
// rather than using Model as a struct literal.
type Model struct {
// Spinner settings to use. See type Spinner.
Spinner Spinner
// Style sets the styling for the spinner. Most of the time you'll just
// want foreground and background coloring, and potentially some padding.
//
// For an introduction to styling with Lip Gloss see:
// https://github.com/charmbracelet/lipgloss
Style lipgloss.Style
frame int
id int
tag int
}
// ID returns the spinner's unique ID.
func (m Model) ID() int {
return m.id
}
// New returns a model with default values.
func New(opts ...Option) Model {
m := Model{
Spinner: Line,
id: nextID(),
}
for _, opt := range opts {
opt(&m)
}
return m
}
// NewModel returns a model with default values.
//
// Deprecated: use [New] instead.
var NewModel = New
// TickMsg indicates that the timer has ticked and we should render a frame.
type TickMsg struct {
Time time.Time
tag int
ID int
}
// Update is the Tea update function.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case TickMsg:
// If an ID is set, and the ID doesn't belong to this spinner, reject
// the message.
if msg.ID > 0 && msg.ID != m.id {
return m, nil
}
// If a tag is set, and it's not the one we expect, reject the message.
// This prevents the spinner from receiving too many messages and
// thus spinning too fast.
if msg.tag > 0 && msg.tag != m.tag {
return m, nil
}
m.frame++
if m.frame >= len(m.Spinner.Frames) {
m.frame = 0
}
m.tag++
return m, m.tick(m.id, m.tag)
default:
return m, nil
}
}
// View renders the model's view.
func (m Model) View() string {
if m.frame >= len(m.Spinner.Frames) {
return "(error)"
}
return m.Style.Render(m.Spinner.Frames[m.frame])
}
// Tick is the command used to advance the spinner one frame. Use this command
// to effectively start the spinner.
func (m Model) Tick() tea.Msg {
return TickMsg{
// The time at which the tick occurred.
Time: time.Now(),
// The ID of the spinner that this message belongs to. This can be
// helpful when routing messages, however bear in mind that spinners
// will ignore messages that don't contain ID by default.
ID: m.id,
tag: m.tag,
}
}
func (m Model) tick(id, tag int) tea.Cmd {
return tea.Tick(m.Spinner.FPS, func(t time.Time) tea.Msg {
return TickMsg{
Time: t,
ID: id,
tag: tag,
}
})
}
// Tick is the command used to advance the spinner one frame. Use this command
// to effectively start the spinner.
//
// Deprecated: Use [Model.Tick] instead.
func Tick() tea.Msg {
return TickMsg{Time: time.Now()}
}
// Option is used to set options in New. For example:
//
// spinner := New(WithSpinner(Dot))
type Option func(*Model)
// WithSpinner is an option to set the spinner.
func WithSpinner(spinner Spinner) Option {
return func(m *Model) {
m.Spinner = spinner
}
}
// WithStyle is an option to set the spinner style.
func WithStyle(style lipgloss.Style) Option {
return func(m *Model) {
m.Style = style
}
}

View File

@ -1,898 +0,0 @@
// Package textinput provides a text input component for Bubble Tea
// applications.
package textinput
import (
"reflect"
"strings"
"time"
"unicode"
"github.com/atotto/clipboard"
"github.com/charmbracelet/bubbles/cursor"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/runeutil"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
rw "github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
)
// Internal messages for clipboard operations.
type (
pasteMsg string
pasteErrMsg struct{ error }
)
// EchoMode sets the input behavior of the text input field.
type EchoMode int
const (
// EchoNormal displays text as is. This is the default behavior.
EchoNormal EchoMode = iota
// EchoPassword displays the EchoCharacter mask instead of actual
// characters. This is commonly used for password fields.
EchoPassword
// EchoNone displays nothing as characters are entered. This is commonly
// seen for password fields on the command line.
EchoNone
)
// ValidateFunc is a function that returns an error if the input is invalid.
type ValidateFunc func(string) error
// KeyMap is the key bindings for different actions within the textinput.
type KeyMap struct {
CharacterForward key.Binding
CharacterBackward key.Binding
WordForward key.Binding
WordBackward key.Binding
DeleteWordBackward key.Binding
DeleteWordForward key.Binding
DeleteAfterCursor key.Binding
DeleteBeforeCursor key.Binding
DeleteCharacterBackward key.Binding
DeleteCharacterForward key.Binding
LineStart key.Binding
LineEnd key.Binding
Paste key.Binding
AcceptSuggestion key.Binding
NextSuggestion key.Binding
PrevSuggestion key.Binding
}
// DefaultKeyMap is the default set of key bindings for navigating and acting
// upon the textinput.
var DefaultKeyMap = KeyMap{
CharacterForward: key.NewBinding(key.WithKeys("right", "ctrl+f")),
CharacterBackward: key.NewBinding(key.WithKeys("left", "ctrl+b")),
WordForward: key.NewBinding(key.WithKeys("alt+right", "ctrl+right", "alt+f")),
WordBackward: key.NewBinding(key.WithKeys("alt+left", "ctrl+left", "alt+b")),
DeleteWordBackward: key.NewBinding(key.WithKeys("alt+backspace", "ctrl+w")),
DeleteWordForward: key.NewBinding(key.WithKeys("alt+delete", "alt+d")),
DeleteAfterCursor: key.NewBinding(key.WithKeys("ctrl+k")),
DeleteBeforeCursor: key.NewBinding(key.WithKeys("ctrl+u")),
DeleteCharacterBackward: key.NewBinding(key.WithKeys("backspace", "ctrl+h")),
DeleteCharacterForward: key.NewBinding(key.WithKeys("delete", "ctrl+d")),
LineStart: key.NewBinding(key.WithKeys("home", "ctrl+a")),
LineEnd: key.NewBinding(key.WithKeys("end", "ctrl+e")),
Paste: key.NewBinding(key.WithKeys("ctrl+v")),
AcceptSuggestion: key.NewBinding(key.WithKeys("tab")),
NextSuggestion: key.NewBinding(key.WithKeys("down", "ctrl+n")),
PrevSuggestion: key.NewBinding(key.WithKeys("up", "ctrl+p")),
}
// Model is the Bubble Tea model for this text input element.
type Model struct {
Err error
// General settings.
Prompt string
Placeholder string
EchoMode EchoMode
EchoCharacter rune
Cursor cursor.Model
// Deprecated: use [cursor.BlinkSpeed] instead.
BlinkSpeed time.Duration
// Styles. These will be applied as inline styles.
//
// For an introduction to styling with Lip Gloss see:
// https://github.com/charmbracelet/lipgloss
PromptStyle lipgloss.Style
TextStyle lipgloss.Style
PlaceholderStyle lipgloss.Style
CompletionStyle lipgloss.Style
// Deprecated: use Cursor.Style instead.
CursorStyle lipgloss.Style
// CharLimit is the maximum amount of characters this input element will
// accept. If 0 or less, there's no limit.
CharLimit int
// Width is the maximum number of characters that can be displayed at once.
// It essentially treats the text field like a horizontally scrolling
// viewport. If 0 or less this setting is ignored.
Width int
// KeyMap encodes the keybindings recognized by the widget.
KeyMap KeyMap
// Underlying text value.
value []rune
// focus indicates whether user input focus should be on this input
// component. When false, ignore keyboard input and hide the cursor.
focus bool
// Cursor position.
pos int
// Used to emulate a viewport when width is set and the content is
// overflowing.
offset int
offsetRight int
// Validate is a function that checks whether or not the text within the
// input is valid. If it is not valid, the `Err` field will be set to the
// error returned by the function. If the function is not defined, all
// input is considered valid.
Validate ValidateFunc
// rune sanitizer for input.
rsan runeutil.Sanitizer
// Should the input suggest to complete
ShowSuggestions bool
// suggestions is a list of suggestions that may be used to complete the
// input.
suggestions [][]rune
matchedSuggestions [][]rune
currentSuggestionIndex int
}
// New creates a new model with default settings.
func New() Model {
return Model{
Prompt: "> ",
EchoCharacter: '*',
CharLimit: 0,
PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
ShowSuggestions: false,
CompletionStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
Cursor: cursor.New(),
KeyMap: DefaultKeyMap,
suggestions: [][]rune{},
value: nil,
focus: false,
pos: 0,
}
}
// NewModel creates a new model with default settings.
//
// Deprecated: Use [New] instead.
var NewModel = New
// SetValue sets the value of the text input.
func (m *Model) SetValue(s string) {
// Clean up any special characters in the input provided by the
// caller. This avoids bugs due to e.g. tab characters and whatnot.
runes := m.san().Sanitize([]rune(s))
err := m.validate(runes)
m.setValueInternal(runes, err)
}
func (m *Model) setValueInternal(runes []rune, err error) {
m.Err = err
empty := len(m.value) == 0
if m.CharLimit > 0 && len(runes) > m.CharLimit {
m.value = runes[:m.CharLimit]
} else {
m.value = runes
}
if (m.pos == 0 && empty) || m.pos > len(m.value) {
m.SetCursor(len(m.value))
}
m.handleOverflow()
}
// Value returns the value of the text input.
func (m Model) Value() string {
return string(m.value)
}
// Position returns the cursor position.
func (m Model) Position() int {
return m.pos
}
// SetCursor moves the cursor to the given position. If the position is
// out of bounds the cursor will be moved to the start or end accordingly.
func (m *Model) SetCursor(pos int) {
m.pos = clamp(pos, 0, len(m.value))
m.handleOverflow()
}
// CursorStart moves the cursor to the start of the input field.
func (m *Model) CursorStart() {
m.SetCursor(0)
}
// CursorEnd moves the cursor to the end of the input field.
func (m *Model) CursorEnd() {
m.SetCursor(len(m.value))
}
// Focused returns the focus state on the model.
func (m Model) Focused() bool {
return m.focus
}
// Focus sets the focus state on the model. When the model is in focus it can
// receive keyboard input and the cursor will be shown.
func (m *Model) Focus() tea.Cmd {
m.focus = true
return m.Cursor.Focus()
}
// Blur removes the focus state on the model. When the model is blurred it can
// not receive keyboard input and the cursor will be hidden.
func (m *Model) Blur() {
m.focus = false
m.Cursor.Blur()
}
// Reset sets the input to its default state with no input.
func (m *Model) Reset() {
m.value = nil
m.SetCursor(0)
}
// SetSuggestions sets the suggestions for the input.
func (m *Model) SetSuggestions(suggestions []string) {
m.suggestions = make([][]rune, len(suggestions))
for i, s := range suggestions {
m.suggestions[i] = []rune(s)
}
m.updateSuggestions()
}
// rsan initializes or retrieves the rune sanitizer.
func (m *Model) san() runeutil.Sanitizer {
if m.rsan == nil {
// Textinput has all its input on a single line so collapse
// newlines/tabs to single spaces.
m.rsan = runeutil.NewSanitizer(
runeutil.ReplaceTabs(" "), runeutil.ReplaceNewlines(" "))
}
return m.rsan
}
func (m *Model) insertRunesFromUserInput(v []rune) {
// Clean up any special characters in the input provided by the
// clipboard. This avoids bugs due to e.g. tab characters and
// whatnot.
paste := m.san().Sanitize(v)
var availSpace int
if m.CharLimit > 0 {
availSpace = m.CharLimit - len(m.value)
// If the char limit's been reached, cancel.
if availSpace <= 0 {
return
}
// If there's not enough space to paste the whole thing cut the pasted
// runes down so they'll fit.
if availSpace < len(paste) {
paste = paste[:availSpace]
}
}
// Stuff before and after the cursor
head := m.value[:m.pos]
tailSrc := m.value[m.pos:]
tail := make([]rune, len(tailSrc))
copy(tail, tailSrc)
// Insert pasted runes
for _, r := range paste {
head = append(head, r)
m.pos++
if m.CharLimit > 0 {
availSpace--
if availSpace <= 0 {
break
}
}
}
// Put it all back together
value := append(head, tail...)
inputErr := m.validate(value)
m.setValueInternal(value, inputErr)
}
// If a max width is defined, perform some logic to treat the visible area
// as a horizontally scrolling viewport.
func (m *Model) handleOverflow() {
if m.Width <= 0 || uniseg.StringWidth(string(m.value)) <= m.Width {
m.offset = 0
m.offsetRight = len(m.value)
return
}
// Correct right offset if we've deleted characters
m.offsetRight = min(m.offsetRight, len(m.value))
if m.pos < m.offset {
m.offset = m.pos
w := 0
i := 0
runes := m.value[m.offset:]
for i < len(runes) && w <= m.Width {
w += rw.RuneWidth(runes[i])
if w <= m.Width+1 {
i++
}
}
m.offsetRight = m.offset + i
} else if m.pos >= m.offsetRight {
m.offsetRight = m.pos
w := 0
runes := m.value[:m.offsetRight]
i := len(runes) - 1
for i > 0 && w < m.Width {
w += rw.RuneWidth(runes[i])
if w <= m.Width {
i--
}
}
m.offset = m.offsetRight - (len(runes) - 1 - i)
}
}
// deleteBeforeCursor deletes all text before the cursor.
func (m *Model) deleteBeforeCursor() {
m.value = m.value[m.pos:]
m.Err = m.validate(m.value)
m.offset = 0
m.SetCursor(0)
}
// deleteAfterCursor deletes all text after the cursor. If input is masked
// delete everything after the cursor so as not to reveal word breaks in the
// masked input.
func (m *Model) deleteAfterCursor() {
m.value = m.value[:m.pos]
m.Err = m.validate(m.value)
m.SetCursor(len(m.value))
}
// deleteWordBackward deletes the word left to the cursor.
func (m *Model) deleteWordBackward() {
if m.pos == 0 || len(m.value) == 0 {
return
}
if m.EchoMode != EchoNormal {
m.deleteBeforeCursor()
return
}
// Linter note: it's critical that we acquire the initial cursor position
// here prior to altering it via SetCursor() below. As such, moving this
// call into the corresponding if clause does not apply here.
oldPos := m.pos //nolint:ifshort
m.SetCursor(m.pos - 1)
for unicode.IsSpace(m.value[m.pos]) {
if m.pos <= 0 {
break
}
// ignore series of whitespace before cursor
m.SetCursor(m.pos - 1)
}
for m.pos > 0 {
if !unicode.IsSpace(m.value[m.pos]) {
m.SetCursor(m.pos - 1)
} else {
if m.pos > 0 {
// keep the previous space
m.SetCursor(m.pos + 1)
}
break
}
}
if oldPos > len(m.value) {
m.value = m.value[:m.pos]
} else {
m.value = append(m.value[:m.pos], m.value[oldPos:]...)
}
m.Err = m.validate(m.value)
}
// deleteWordForward deletes the word right to the cursor. If input is masked
// delete everything after the cursor so as not to reveal word breaks in the
// masked input.
func (m *Model) deleteWordForward() {
if m.pos >= len(m.value) || len(m.value) == 0 {
return
}
if m.EchoMode != EchoNormal {
m.deleteAfterCursor()
return
}
oldPos := m.pos
m.SetCursor(m.pos + 1)
for unicode.IsSpace(m.value[m.pos]) {
// ignore series of whitespace after cursor
m.SetCursor(m.pos + 1)
if m.pos >= len(m.value) {
break
}
}
for m.pos < len(m.value) {
if !unicode.IsSpace(m.value[m.pos]) {
m.SetCursor(m.pos + 1)
} else {
break
}
}
if m.pos > len(m.value) {
m.value = m.value[:oldPos]
} else {
m.value = append(m.value[:oldPos], m.value[m.pos:]...)
}
m.Err = m.validate(m.value)
m.SetCursor(oldPos)
}
// wordBackward moves the cursor one word to the left. If input is masked, move
// input to the start so as not to reveal word breaks in the masked input.
func (m *Model) wordBackward() {
if m.pos == 0 || len(m.value) == 0 {
return
}
if m.EchoMode != EchoNormal {
m.CursorStart()
return
}
i := m.pos - 1
for i >= 0 {
if unicode.IsSpace(m.value[i]) {
m.SetCursor(m.pos - 1)
i--
} else {
break
}
}
for i >= 0 {
if !unicode.IsSpace(m.value[i]) {
m.SetCursor(m.pos - 1)
i--
} else {
break
}
}
}
// wordForward moves the cursor one word to the right. If the input is masked,
// move input to the end so as not to reveal word breaks in the masked input.
func (m *Model) wordForward() {
if m.pos >= len(m.value) || len(m.value) == 0 {
return
}
if m.EchoMode != EchoNormal {
m.CursorEnd()
return
}
i := m.pos
for i < len(m.value) {
if unicode.IsSpace(m.value[i]) {
m.SetCursor(m.pos + 1)
i++
} else {
break
}
}
for i < len(m.value) {
if !unicode.IsSpace(m.value[i]) {
m.SetCursor(m.pos + 1)
i++
} else {
break
}
}
}
func (m Model) echoTransform(v string) string {
switch m.EchoMode {
case EchoPassword:
return strings.Repeat(string(m.EchoCharacter), uniseg.StringWidth(v))
case EchoNone:
return ""
case EchoNormal:
return v
default:
return v
}
}
// Update is the Bubble Tea update loop.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
if !m.focus {
return m, nil
}
// Need to check for completion before, because key is configurable and might be double assigned
keyMsg, ok := msg.(tea.KeyMsg)
if ok && key.Matches(keyMsg, m.KeyMap.AcceptSuggestion) {
if m.canAcceptSuggestion() {
m.value = append(m.value, m.matchedSuggestions[m.currentSuggestionIndex][len(m.value):]...)
m.CursorEnd()
}
}
// Let's remember where the position of the cursor currently is so that if
// the cursor position changes, we can reset the blink.
oldPos := m.pos
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, m.KeyMap.DeleteWordBackward):
m.deleteWordBackward()
case key.Matches(msg, m.KeyMap.DeleteCharacterBackward):
m.Err = nil
if len(m.value) > 0 {
m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)
m.Err = m.validate(m.value)
if m.pos > 0 {
m.SetCursor(m.pos - 1)
}
}
case key.Matches(msg, m.KeyMap.WordBackward):
m.wordBackward()
case key.Matches(msg, m.KeyMap.CharacterBackward):
if m.pos > 0 {
m.SetCursor(m.pos - 1)
}
case key.Matches(msg, m.KeyMap.WordForward):
m.wordForward()
case key.Matches(msg, m.KeyMap.CharacterForward):
if m.pos < len(m.value) {
m.SetCursor(m.pos + 1)
}
case key.Matches(msg, m.KeyMap.LineStart):
m.CursorStart()
case key.Matches(msg, m.KeyMap.DeleteCharacterForward):
if len(m.value) > 0 && m.pos < len(m.value) {
m.value = append(m.value[:m.pos], m.value[m.pos+1:]...)
m.Err = m.validate(m.value)
}
case key.Matches(msg, m.KeyMap.LineEnd):
m.CursorEnd()
case key.Matches(msg, m.KeyMap.DeleteAfterCursor):
m.deleteAfterCursor()
case key.Matches(msg, m.KeyMap.DeleteBeforeCursor):
m.deleteBeforeCursor()
case key.Matches(msg, m.KeyMap.Paste):
return m, Paste
case key.Matches(msg, m.KeyMap.DeleteWordForward):
m.deleteWordForward()
case key.Matches(msg, m.KeyMap.NextSuggestion):
m.nextSuggestion()
case key.Matches(msg, m.KeyMap.PrevSuggestion):
m.previousSuggestion()
default:
// Input one or more regular characters.
m.insertRunesFromUserInput(msg.Runes)
}
// Check again if can be completed
// because value might be something that does not match the completion prefix
m.updateSuggestions()
case pasteMsg:
m.insertRunesFromUserInput([]rune(msg))
case pasteErrMsg:
m.Err = msg
}
var cmds []tea.Cmd
var cmd tea.Cmd
m.Cursor, cmd = m.Cursor.Update(msg)
cmds = append(cmds, cmd)
if oldPos != m.pos && m.Cursor.Mode() == cursor.CursorBlink {
m.Cursor.Blink = false
cmds = append(cmds, m.Cursor.BlinkCmd())
}
m.handleOverflow()
return m, tea.Batch(cmds...)
}
// View renders the textinput in its current state.
func (m Model) View() string {
// Placeholder text
if len(m.value) == 0 && m.Placeholder != "" {
return m.placeholderView()
}
styleText := m.TextStyle.Inline(true).Render
value := m.value[m.offset:m.offsetRight]
pos := max(0, m.pos-m.offset)
v := styleText(m.echoTransform(string(value[:pos])))
if pos < len(value) { //nolint:nestif
char := m.echoTransform(string(value[pos]))
m.Cursor.SetChar(char)
v += m.Cursor.View() // cursor and text under it
v += styleText(m.echoTransform(string(value[pos+1:]))) // text after cursor
v += m.completionView(0) // suggested completion
} else {
if m.focus && m.canAcceptSuggestion() {
suggestion := m.matchedSuggestions[m.currentSuggestionIndex]
if len(value) < len(suggestion) {
m.Cursor.TextStyle = m.CompletionStyle
m.Cursor.SetChar(m.echoTransform(string(suggestion[pos])))
v += m.Cursor.View()
v += m.completionView(1)
} else {
m.Cursor.SetChar(" ")
v += m.Cursor.View()
}
} else {
m.Cursor.SetChar(" ")
v += m.Cursor.View()
}
}
// If a max width and background color were set fill the empty spaces with
// the background color.
valWidth := uniseg.StringWidth(string(value))
if m.Width > 0 && valWidth <= m.Width {
padding := max(0, m.Width-valWidth)
if valWidth+padding <= m.Width && pos < len(value) {
padding++
}
v += styleText(strings.Repeat(" ", padding))
}
return m.PromptStyle.Render(m.Prompt) + v
}
// placeholderView returns the prompt and placeholder view, if any.
func (m Model) placeholderView() string {
var (
v string
style = m.PlaceholderStyle.Inline(true).Render
)
p := make([]rune, m.Width+1)
copy(p, []rune(m.Placeholder))
m.Cursor.TextStyle = m.PlaceholderStyle
m.Cursor.SetChar(string(p[:1]))
v += m.Cursor.View()
// If the entire placeholder is already set and no padding is needed, finish
if m.Width < 1 && len(p) <= 1 {
return m.PromptStyle.Render(m.Prompt) + v
}
// If Width is set then size placeholder accordingly
if m.Width > 0 {
// available width is width - len + cursor offset of 1
minWidth := lipgloss.Width(m.Placeholder)
availWidth := m.Width - minWidth + 1
// if width < len, 'subtract'(add) number to len and dont add padding
if availWidth < 0 {
minWidth += availWidth
availWidth = 0
}
// append placeholder[len] - cursor, append padding
v += style(string(p[1:minWidth]))
v += style(strings.Repeat(" ", availWidth))
} else {
// if there is no width, the placeholder can be any length
v += style(string(p[1:]))
}
return m.PromptStyle.Render(m.Prompt) + v
}
// Blink is a command used to initialize cursor blinking.
func Blink() tea.Msg {
return cursor.Blink()
}
// Paste is a command for pasting from the clipboard into the text input.
func Paste() tea.Msg {
str, err := clipboard.ReadAll()
if err != nil {
return pasteErrMsg{err}
}
return pasteMsg(str)
}
func clamp(v, low, high int) int {
if high < low {
low, high = high, low
}
return min(high, max(low, v))
}
// Deprecated.
// Deprecated: use [cursor.Mode].
//
//nolint:revive
type CursorMode int
//nolint:revive
const (
// Deprecated: use [cursor.CursorBlink].
CursorBlink = CursorMode(cursor.CursorBlink)
// Deprecated: use [cursor.CursorStatic].
CursorStatic = CursorMode(cursor.CursorStatic)
// Deprecated: use [cursor.CursorHide].
CursorHide = CursorMode(cursor.CursorHide)
)
func (c CursorMode) String() string {
return cursor.Mode(c).String()
}
// Deprecated: use [cursor.Mode].
//
//nolint:revive
func (m Model) CursorMode() CursorMode {
return CursorMode(m.Cursor.Mode())
}
// Deprecated: use cursor.SetMode().
//
//nolint:revive
func (m *Model) SetCursorMode(mode CursorMode) tea.Cmd {
return m.Cursor.SetMode(cursor.Mode(mode))
}
func (m Model) completionView(offset int) string {
var (
value = m.value
style = m.PlaceholderStyle.Inline(true).Render
)
if m.canAcceptSuggestion() {
suggestion := m.matchedSuggestions[m.currentSuggestionIndex]
if len(value) < len(suggestion) {
return style(string(suggestion[len(value)+offset:]))
}
}
return ""
}
func (m *Model) getSuggestions(sugs [][]rune) []string {
suggestions := make([]string, len(sugs))
for i, s := range sugs {
suggestions[i] = string(s)
}
return suggestions
}
// AvailableSuggestions returns the list of available suggestions.
func (m *Model) AvailableSuggestions() []string {
return m.getSuggestions(m.suggestions)
}
// MatchedSuggestions returns the list of matched suggestions.
func (m *Model) MatchedSuggestions() []string {
return m.getSuggestions(m.matchedSuggestions)
}
// CurrentSuggestionIndex returns the currently selected suggestion index.
func (m *Model) CurrentSuggestionIndex() int {
return m.currentSuggestionIndex
}
// CurrentSuggestion returns the currently selected suggestion.
func (m *Model) CurrentSuggestion() string {
if m.currentSuggestionIndex >= len(m.matchedSuggestions) {
return ""
}
return string(m.matchedSuggestions[m.currentSuggestionIndex])
}
// canAcceptSuggestion returns whether there is an acceptable suggestion to
// autocomplete the current value.
func (m *Model) canAcceptSuggestion() bool {
return len(m.matchedSuggestions) > 0
}
// updateSuggestions refreshes the list of matching suggestions.
func (m *Model) updateSuggestions() {
if !m.ShowSuggestions {
return
}
if len(m.value) <= 0 || len(m.suggestions) <= 0 {
m.matchedSuggestions = [][]rune{}
return
}
matches := [][]rune{}
for _, s := range m.suggestions {
suggestion := string(s)
if strings.HasPrefix(strings.ToLower(suggestion), strings.ToLower(string(m.value))) {
matches = append(matches, []rune(suggestion))
}
}
if !reflect.DeepEqual(matches, m.matchedSuggestions) {
m.currentSuggestionIndex = 0
}
m.matchedSuggestions = matches
}
// nextSuggestion selects the next suggestion.
func (m *Model) nextSuggestion() {
m.currentSuggestionIndex = (m.currentSuggestionIndex + 1)
if m.currentSuggestionIndex >= len(m.matchedSuggestions) {
m.currentSuggestionIndex = 0
}
}
// previousSuggestion selects the previous suggestion.
func (m *Model) previousSuggestion() {
m.currentSuggestionIndex = (m.currentSuggestionIndex - 1)
if m.currentSuggestionIndex < 0 {
m.currentSuggestionIndex = len(m.matchedSuggestions) - 1
}
}
func (m Model) validate(v []rune) error {
if m.Validate != nil {
return m.Validate(string(v))
}
return nil
}

View File

@ -153,29 +153,24 @@ func envColorProfile(env environ) (p Profile) {
p = ANSI
}
parts := strings.Split(term, "-")
switch parts[0] {
case "alacritty",
"contour",
"foot",
"ghostty",
"kitty",
"rio",
"st",
"wezterm":
switch {
case strings.Contains(term, "alacritty"),
strings.Contains(term, "contour"),
strings.Contains(term, "foot"),
strings.Contains(term, "ghostty"),
strings.Contains(term, "kitty"),
strings.Contains(term, "rio"),
strings.Contains(term, "st"),
strings.Contains(term, "wezterm"):
return TrueColor
case "xterm":
if len(parts) > 1 {
switch parts[1] {
case "ghostty", "kitty":
// These terminals can be defined as xterm-TERMNAME
return TrueColor
}
}
case "tmux", "screen":
case strings.HasPrefix(term, "tmux"), strings.HasPrefix(term, "screen"):
if p < ANSI256 {
p = ANSI256
}
case strings.HasPrefix(term, "xterm"):
if p < ANSI {
p = ANSI
}
}
if isCloudShell, _ := strconv.ParseBool(env.get("GOOGLE_CLOUD_SHELL")); isCloudShell {

View File

@ -108,7 +108,7 @@ func DECRST(modes ...Mode) string {
func setMode(reset bool, modes ...Mode) (s string) {
if len(modes) == 0 {
return //nolint:nakedret
return s
}
cmd := "h"
@ -142,7 +142,7 @@ func setMode(reset bool, modes ...Mode) (s string) {
if len(dec) > 0 {
s += seq + "?" + strings.Join(dec, ";") + cmd
}
return //nolint:nakedret
return s
}
// RequestMode (DECRQM) returns a sequence to request a mode from the terminal.
@ -228,12 +228,12 @@ func (m DECMode) Mode() int {
//
// See: https://vt100.net/docs/vt510-rm/KAM.html
const (
KeyboardActionMode = ANSIMode(2)
KAM = KeyboardActionMode
ModeKeyboardAction = ANSIMode(2)
KAM = ModeKeyboardAction
SetKeyboardActionMode = "\x1b[2h"
ResetKeyboardActionMode = "\x1b[2l"
RequestKeyboardActionMode = "\x1b[2$p"
SetModeKeyboardAction = "\x1b[2h"
ResetModeKeyboardAction = "\x1b[2l"
RequestModeKeyboardAction = "\x1b[2$p"
)
// Insert/Replace Mode (IRM) is a mode that determines whether characters are
@ -245,12 +245,12 @@ const (
//
// See: https://vt100.net/docs/vt510-rm/IRM.html
const (
InsertReplaceMode = ANSIMode(4)
IRM = InsertReplaceMode
ModeInsertReplace = ANSIMode(4)
IRM = ModeInsertReplace
SetInsertReplaceMode = "\x1b[4h"
ResetInsertReplaceMode = "\x1b[4l"
RequestInsertReplaceMode = "\x1b[4$p"
SetModeInsertReplace = "\x1b[4h"
ResetModeInsertReplace = "\x1b[4l"
RequestModeInsertReplace = "\x1b[4$p"
)
// BiDirectional Support Mode (BDSM) is a mode that determines whether the
@ -260,12 +260,12 @@ const (
//
// See ECMA-48 7.2.1.
const (
BiDirectionalSupportMode = ANSIMode(8)
BDSM = BiDirectionalSupportMode
ModeBiDirectionalSupport = ANSIMode(8)
BDSM = ModeBiDirectionalSupport
SetBiDirectionalSupportMode = "\x1b[8h"
ResetBiDirectionalSupportMode = "\x1b[8l"
RequestBiDirectionalSupportMode = "\x1b[8$p"
SetModeBiDirectionalSupport = "\x1b[8h"
ResetModeBiDirectionalSupport = "\x1b[8l"
RequestModeBiDirectionalSupport = "\x1b[8$p"
)
// Send Receive Mode (SRM) or Local Echo Mode is a mode that determines whether
@ -274,17 +274,17 @@ const (
//
// See: https://vt100.net/docs/vt510-rm/SRM.html
const (
SendReceiveMode = ANSIMode(12)
LocalEchoMode = SendReceiveMode
SRM = SendReceiveMode
ModeSendReceive = ANSIMode(12)
ModeLocalEcho = ModeSendReceive
SRM = ModeSendReceive
SetSendReceiveMode = "\x1b[12h"
ResetSendReceiveMode = "\x1b[12l"
RequestSendReceiveMode = "\x1b[12$p"
SetModeSendReceive = "\x1b[12h"
ResetModeSendReceive = "\x1b[12l"
RequestModeSendReceive = "\x1b[12$p"
SetLocalEchoMode = "\x1b[12h"
ResetLocalEchoMode = "\x1b[12l"
RequestLocalEchoMode = "\x1b[12$p"
SetModeLocalEcho = "\x1b[12h"
ResetModeLocalEcho = "\x1b[12l"
RequestModeLocalEcho = "\x1b[12$p"
)
// Line Feed/New Line Mode (LNM) is a mode that determines whether the terminal
@ -299,12 +299,12 @@ const (
//
// See: https://vt100.net/docs/vt510-rm/LNM.html
const (
LineFeedNewLineMode = ANSIMode(20)
LNM = LineFeedNewLineMode
ModeLineFeedNewLine = ANSIMode(20)
LNM = ModeLineFeedNewLine
SetLineFeedNewLineMode = "\x1b[20h"
ResetLineFeedNewLineMode = "\x1b[20l"
RequestLineFeedNewLineMode = "\x1b[20$p"
SetModeLineFeedNewLine = "\x1b[20h"
ResetModeLineFeedNewLine = "\x1b[20l"
RequestModeLineFeedNewLine = "\x1b[20$p"
)
// Cursor Keys Mode (DECCKM) is a mode that determines whether the cursor keys
@ -312,18 +312,12 @@ const (
//
// See: https://vt100.net/docs/vt510-rm/DECCKM.html
const (
CursorKeysMode = DECMode(1)
DECCKM = CursorKeysMode
ModeCursorKeys = DECMode(1)
DECCKM = ModeCursorKeys
SetCursorKeysMode = "\x1b[?1h"
ResetCursorKeysMode = "\x1b[?1l"
RequestCursorKeysMode = "\x1b[?1$p"
)
// Deprecated: use [SetCursorKeysMode] and [ResetCursorKeysMode] instead.
const (
EnableCursorKeys = "\x1b[?1h" //nolint:revive // grouped constants
DisableCursorKeys = "\x1b[?1l"
SetModeCursorKeys = "\x1b[?1h"
ResetModeCursorKeys = "\x1b[?1l"
RequestModeCursorKeys = "\x1b[?1$p"
)
// Origin Mode (DECOM) is a mode that determines whether the cursor moves to the
@ -331,12 +325,12 @@ const (
//
// See: https://vt100.net/docs/vt510-rm/DECOM.html
const (
OriginMode = DECMode(6)
DECOM = OriginMode
ModeOrigin = DECMode(6)
DECOM = ModeOrigin
SetOriginMode = "\x1b[?6h"
ResetOriginMode = "\x1b[?6l"
RequestOriginMode = "\x1b[?6$p"
SetModeOrigin = "\x1b[?6h"
ResetModeOrigin = "\x1b[?6l"
RequestModeOrigin = "\x1b[?6$p"
)
// Auto Wrap Mode (DECAWM) is a mode that determines whether the cursor wraps
@ -344,12 +338,12 @@ const (
//
// See: https://vt100.net/docs/vt510-rm/DECAWM.html
const (
AutoWrapMode = DECMode(7)
DECAWM = AutoWrapMode
ModeAutoWrap = DECMode(7)
DECAWM = ModeAutoWrap
SetAutoWrapMode = "\x1b[?7h"
ResetAutoWrapMode = "\x1b[?7l"
RequestAutoWrapMode = "\x1b[?7$p"
SetModeAutoWrap = "\x1b[?7h"
ResetModeAutoWrap = "\x1b[?7l"
RequestModeAutoWrap = "\x1b[?7$p"
)
// X10 Mouse Mode is a mode that determines whether the mouse reports on button
@ -364,39 +358,29 @@ const (
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
const (
X10MouseMode = DECMode(9)
ModeMouseX10 = DECMode(9)
SetX10MouseMode = "\x1b[?9h"
ResetX10MouseMode = "\x1b[?9l"
RequestX10MouseMode = "\x1b[?9$p"
SetModeMouseX10 = "\x1b[?9h"
ResetModeMouseX10 = "\x1b[?9l"
RequestModeMouseX10 = "\x1b[?9$p"
)
// Text Cursor Enable Mode (DECTCEM) is a mode that shows/hides the cursor.
//
// See: https://vt100.net/docs/vt510-rm/DECTCEM.html
const (
TextCursorEnableMode = DECMode(25)
DECTCEM = TextCursorEnableMode
ModeTextCursorEnable = DECMode(25)
DECTCEM = ModeTextCursorEnable
SetTextCursorEnableMode = "\x1b[?25h"
ResetTextCursorEnableMode = "\x1b[?25l"
RequestTextCursorEnableMode = "\x1b[?25$p"
SetModeTextCursorEnable = "\x1b[?25h"
ResetModeTextCursorEnable = "\x1b[?25l"
RequestModeTextCursorEnable = "\x1b[?25$p"
)
// These are aliases for [SetTextCursorEnableMode] and [ResetTextCursorEnableMode].
// These are aliases for [SetModeTextCursorEnable] and [ResetModeTextCursorEnable].
const (
ShowCursor = SetTextCursorEnableMode
HideCursor = ResetTextCursorEnableMode
)
// Text Cursor Enable Mode (DECTCEM) is a mode that shows/hides the cursor.
//
// See: https://vt100.net/docs/vt510-rm/DECTCEM.html
//
// Deprecated: use [SetTextCursorEnableMode] and [ResetTextCursorEnableMode] instead.
const (
CursorEnableMode = DECMode(25)
RequestCursorVisibility = "\x1b[?25$p"
ShowCursor = SetModeTextCursorEnable
HideCursor = ResetModeTextCursorEnable
)
// Numeric Keypad Mode (DECNKM) is a mode that determines whether the keypad
@ -406,12 +390,12 @@ const (
//
// See: https://vt100.net/docs/vt510-rm/DECNKM.html
const (
NumericKeypadMode = DECMode(66)
DECNKM = NumericKeypadMode
ModeNumericKeypad = DECMode(66)
DECNKM = ModeNumericKeypad
SetNumericKeypadMode = "\x1b[?66h"
ResetNumericKeypadMode = "\x1b[?66l"
RequestNumericKeypadMode = "\x1b[?66$p"
SetModeNumericKeypad = "\x1b[?66h"
ResetModeNumericKeypad = "\x1b[?66l"
RequestModeNumericKeypad = "\x1b[?66$p"
)
// Backarrow Key Mode (DECBKM) is a mode that determines whether the backspace
@ -419,12 +403,12 @@ const (
//
// See: https://vt100.net/docs/vt510-rm/DECBKM.html
const (
BackarrowKeyMode = DECMode(67)
DECBKM = BackarrowKeyMode
ModeBackarrowKey = DECMode(67)
DECBKM = ModeBackarrowKey
SetBackarrowKeyMode = "\x1b[?67h"
ResetBackarrowKeyMode = "\x1b[?67l"
RequestBackarrowKeyMode = "\x1b[?67$p"
SetModeBackarrowKey = "\x1b[?67h"
ResetModeBackarrowKey = "\x1b[?67l"
RequestModeBackarrowKey = "\x1b[?67$p"
)
// Left Right Margin Mode (DECLRMM) is a mode that determines whether the left
@ -432,47 +416,33 @@ const (
//
// See: https://vt100.net/docs/vt510-rm/DECLRMM.html
const (
LeftRightMarginMode = DECMode(69)
DECLRMM = LeftRightMarginMode
ModeLeftRightMargin = DECMode(69)
DECLRMM = ModeLeftRightMargin
SetLeftRightMarginMode = "\x1b[?69h"
ResetLeftRightMarginMode = "\x1b[?69l"
RequestLeftRightMarginMode = "\x1b[?69$p"
SetModeLeftRightMargin = "\x1b[?69h"
ResetModeLeftRightMargin = "\x1b[?69l"
RequestModeLeftRightMargin = "\x1b[?69$p"
)
// Normal Mouse Mode is a mode that determines whether the mouse reports on
// button presses and releases. It will also report modifier keys, wheel
// events, and extra buttons.
//
// It uses the same encoding as [X10MouseMode] with a few differences:
// It uses the same encoding as [ModeMouseX10] with a few differences:
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
const (
NormalMouseMode = DECMode(1000)
ModeMouseNormal = DECMode(1000)
SetNormalMouseMode = "\x1b[?1000h"
ResetNormalMouseMode = "\x1b[?1000l"
RequestNormalMouseMode = "\x1b[?1000$p"
)
// VT Mouse Tracking is a mode that determines whether the mouse reports on
// button press and release.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
//
// Deprecated: use [NormalMouseMode] instead.
const (
MouseMode = DECMode(1000)
EnableMouse = "\x1b[?1000h"
DisableMouse = "\x1b[?1000l"
RequestMouse = "\x1b[?1000$p"
SetModeMouseNormal = "\x1b[?1000h"
ResetModeMouseNormal = "\x1b[?1000l"
RequestModeMouseNormal = "\x1b[?1000$p"
)
// Highlight Mouse Tracking is a mode that determines whether the mouse reports
// on button presses, releases, and highlighted cells.
//
// It uses the same encoding as [NormalMouseMode] with a few differences:
// It uses the same encoding as [ModeMouseNormal] with a few differences:
//
// On highlight events, the terminal responds with the following encoding:
//
@ -481,11 +451,11 @@ const (
//
// Where the parameters are startx, starty, endx, endy, mousex, and mousey.
const (
HighlightMouseMode = DECMode(1001)
ModeMouseHighlight = DECMode(1001)
SetHighlightMouseMode = "\x1b[?1001h"
ResetHighlightMouseMode = "\x1b[?1001l"
RequestHighlightMouseMode = "\x1b[?1001$p"
SetModeMouseHighlight = "\x1b[?1001h"
ResetModeMouseHighlight = "\x1b[?1001l"
RequestModeMouseHighlight = "\x1b[?1001$p"
)
// VT Hilite Mouse Tracking is a mode that determines whether the mouse reports on
@ -493,65 +463,29 @@ const (
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
//
// Deprecated: use [HighlightMouseMode] instead.
const (
MouseHiliteMode = DECMode(1001)
EnableMouseHilite = "\x1b[?1001h"
DisableMouseHilite = "\x1b[?1001l"
RequestMouseHilite = "\x1b[?1001$p"
)
// Button Event Mouse Tracking is essentially the same as [NormalMouseMode],
// Button Event Mouse Tracking is essentially the same as [ModeMouseNormal],
// but it also reports button-motion events when a button is pressed.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
const (
ButtonEventMouseMode = DECMode(1002)
ModeMouseButtonEvent = DECMode(1002)
SetButtonEventMouseMode = "\x1b[?1002h"
ResetButtonEventMouseMode = "\x1b[?1002l"
RequestButtonEventMouseMode = "\x1b[?1002$p"
SetModeMouseButtonEvent = "\x1b[?1002h"
ResetModeMouseButtonEvent = "\x1b[?1002l"
RequestModeMouseButtonEvent = "\x1b[?1002$p"
)
// Cell Motion Mouse Tracking is a mode that determines whether the mouse
// reports on button press, release, and motion events.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
//
// Deprecated: use [ButtonEventMouseMode] instead.
const (
MouseCellMotionMode = DECMode(1002)
EnableMouseCellMotion = "\x1b[?1002h"
DisableMouseCellMotion = "\x1b[?1002l"
RequestMouseCellMotion = "\x1b[?1002$p"
)
// Any Event Mouse Tracking is the same as [ButtonEventMouseMode], except that
// Any Event Mouse Tracking is the same as [ModeMouseButtonEvent], except that
// all motion events are reported even if no mouse buttons are pressed.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
const (
AnyEventMouseMode = DECMode(1003)
ModeMouseAnyEvent = DECMode(1003)
SetAnyEventMouseMode = "\x1b[?1003h"
ResetAnyEventMouseMode = "\x1b[?1003l"
RequestAnyEventMouseMode = "\x1b[?1003$p"
)
// All Mouse Tracking is a mode that determines whether the mouse reports on
// button press, release, motion, and highlight events.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
//
// Deprecated: use [AnyEventMouseMode] instead.
const (
MouseAllMotionMode = DECMode(1003)
EnableMouseAllMotion = "\x1b[?1003h"
DisableMouseAllMotion = "\x1b[?1003l"
RequestMouseAllMotion = "\x1b[?1003$p"
SetModeMouseAnyEvent = "\x1b[?1003h"
ResetModeMouseAnyEvent = "\x1b[?1003l"
RequestModeMouseAnyEvent = "\x1b[?1003$p"
)
// Focus Event Mode is a mode that determines whether the terminal reports focus
@ -564,22 +498,11 @@ const (
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Focus-Tracking
const (
FocusEventMode = DECMode(1004)
ModeFocusEvent = DECMode(1004)
SetFocusEventMode = "\x1b[?1004h"
ResetFocusEventMode = "\x1b[?1004l"
RequestFocusEventMode = "\x1b[?1004$p"
)
// Deprecated: use [SetFocusEventMode], [ResetFocusEventMode], and
// [RequestFocusEventMode] instead.
// Focus reporting mode constants.
const (
ReportFocusMode = DECMode(1004) //nolint:revive // grouped constants
EnableReportFocus = "\x1b[?1004h"
DisableReportFocus = "\x1b[?1004l"
RequestReportFocus = "\x1b[?1004$p"
SetModeFocusEvent = "\x1b[?1004h"
ResetModeFocusEvent = "\x1b[?1004l"
RequestModeFocusEvent = "\x1b[?1004$p"
)
// SGR Extended Mouse Mode is a mode that changes the mouse tracking encoding
@ -589,24 +512,15 @@ const (
//
// CSI < Cb ; Cx ; Cy M
//
// Where Cb is the same as [NormalMouseMode], and Cx and Cy are the x and y.
// Where Cb is the same as [ModeMouseNormal], and Cx and Cy are the x and y.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
const (
SgrExtMouseMode = DECMode(1006)
ModeMouseExtSgr = DECMode(1006)
SetSgrExtMouseMode = "\x1b[?1006h"
ResetSgrExtMouseMode = "\x1b[?1006l"
RequestSgrExtMouseMode = "\x1b[?1006$p"
)
// Deprecated: use [SgrExtMouseMode] [SetSgrExtMouseMode],
// [ResetSgrExtMouseMode], and [RequestSgrExtMouseMode] instead.
const (
MouseSgrExtMode = DECMode(1006) //nolint:revive // grouped constants
EnableMouseSgrExt = "\x1b[?1006h"
DisableMouseSgrExt = "\x1b[?1006l"
RequestMouseSgrExt = "\x1b[?1006$p"
SetModeMouseExtSgr = "\x1b[?1006h"
ResetModeMouseExtSgr = "\x1b[?1006l"
RequestModeMouseExtSgr = "\x1b[?1006$p"
)
// UTF-8 Extended Mouse Mode is a mode that changes the mouse tracking encoding
@ -614,11 +528,11 @@ const (
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
const (
Utf8ExtMouseMode = DECMode(1005)
ModeMouseExtUtf8 = DECMode(1005)
SetUtf8ExtMouseMode = "\x1b[?1005h"
ResetUtf8ExtMouseMode = "\x1b[?1005l"
RequestUtf8ExtMouseMode = "\x1b[?1005$p"
SetModeMouseExtUtf8 = "\x1b[?1005h"
ResetModeMouseExtUtf8 = "\x1b[?1005l"
RequestModeMouseExtUtf8 = "\x1b[?1005$p"
)
// URXVT Extended Mouse Mode is a mode that changes the mouse tracking encoding
@ -626,25 +540,25 @@ const (
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
const (
UrxvtExtMouseMode = DECMode(1015)
ModeMouseExtUrxvt = DECMode(1015)
SetUrxvtExtMouseMode = "\x1b[?1015h"
ResetUrxvtExtMouseMode = "\x1b[?1015l"
RequestUrxvtExtMouseMode = "\x1b[?1015$p"
SetModeMouseExtUrxvt = "\x1b[?1015h"
ResetModeMouseExtUrxvt = "\x1b[?1015l"
RequestModeMouseExtUrxvt = "\x1b[?1015$p"
)
// SGR Pixel Extended Mouse Mode is a mode that changes the mouse tracking
// encoding to use SGR parameters with pixel coordinates.
//
// This is similar to [SgrExtMouseMode], but also reports pixel coordinates.
// This is similar to [ModeMouseExtSgr], but also reports pixel coordinates.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
const (
SgrPixelExtMouseMode = DECMode(1016)
ModeMouseExtSgrPixel = DECMode(1016)
SetSgrPixelExtMouseMode = "\x1b[?1016h"
ResetSgrPixelExtMouseMode = "\x1b[?1016l"
RequestSgrPixelExtMouseMode = "\x1b[?1016$p"
SetModeMouseExtSgrPixel = "\x1b[?1016h"
ResetModeMouseExtSgrPixel = "\x1b[?1016l"
RequestModeMouseExtSgrPixel = "\x1b[?1016$p"
)
// Alternate Screen Mode is a mode that determines whether the alternate screen
@ -653,11 +567,11 @@ const (
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
const (
AltScreenMode = DECMode(1047)
ModeAltScreen = DECMode(1047)
SetAltScreenMode = "\x1b[?1047h"
ResetAltScreenMode = "\x1b[?1047l"
RequestAltScreenMode = "\x1b[?1047$p"
SetModeAltScreen = "\x1b[?1047h"
ResetModeAltScreen = "\x1b[?1047l"
RequestModeAltScreen = "\x1b[?1047$p"
)
// Save Cursor Mode is a mode that saves the cursor position.
@ -665,42 +579,24 @@ const (
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
const (
SaveCursorMode = DECMode(1048)
ModeSaveCursor = DECMode(1048)
SetSaveCursorMode = "\x1b[?1048h"
ResetSaveCursorMode = "\x1b[?1048l"
RequestSaveCursorMode = "\x1b[?1048$p"
SetModeSaveCursor = "\x1b[?1048h"
ResetModeSaveCursor = "\x1b[?1048l"
RequestModeSaveCursor = "\x1b[?1048$p"
)
// Alternate Screen Save Cursor Mode is a mode that saves the cursor position as in
// [SaveCursorMode], switches to the alternate screen buffer as in [AltScreenMode],
// [ModeSaveCursor], switches to the alternate screen buffer as in [ModeAltScreen],
// and clears the screen on switch.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
const (
AltScreenSaveCursorMode = DECMode(1049)
ModeAltScreenSaveCursor = DECMode(1049)
SetAltScreenSaveCursorMode = "\x1b[?1049h"
ResetAltScreenSaveCursorMode = "\x1b[?1049l"
RequestAltScreenSaveCursorMode = "\x1b[?1049$p"
)
// Alternate Screen Buffer is a mode that determines whether the alternate screen
// buffer is active.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
//
// Deprecated: use [AltScreenSaveCursorMode] instead.
const (
AltScreenBufferMode = DECMode(1049)
SetAltScreenBufferMode = "\x1b[?1049h"
ResetAltScreenBufferMode = "\x1b[?1049l"
RequestAltScreenBufferMode = "\x1b[?1049$p"
EnableAltScreenBuffer = "\x1b[?1049h"
DisableAltScreenBuffer = "\x1b[?1049l"
RequestAltScreenBuffer = "\x1b[?1049$p"
SetModeAltScreenSaveCursor = "\x1b[?1049h"
ResetModeAltScreenSaveCursor = "\x1b[?1049l"
RequestModeAltScreenSaveCursor = "\x1b[?1049$p"
)
// Bracketed Paste Mode is a mode that determines whether pasted text is
@ -709,19 +605,11 @@ const (
// See: https://cirw.in/blog/bracketed-paste
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode
const (
BracketedPasteMode = DECMode(2004)
ModeBracketedPaste = DECMode(2004)
SetBracketedPasteMode = "\x1b[?2004h"
ResetBracketedPasteMode = "\x1b[?2004l"
RequestBracketedPasteMode = "\x1b[?2004$p"
)
// Deprecated: use [SetBracketedPasteMode], [ResetBracketedPasteMode], and
// [RequestBracketedPasteMode] instead.
const (
EnableBracketedPaste = "\x1b[?2004h" //nolint:revive // grouped constants
DisableBracketedPaste = "\x1b[?2004l"
RequestBracketedPaste = "\x1b[?2004$p"
SetModeBracketedPaste = "\x1b[?2004h"
ResetModeBracketedPaste = "\x1b[?2004l"
RequestModeBracketedPaste = "\x1b[?2004$p"
)
// Synchronized Output Mode is a mode that determines whether output is
@ -729,23 +617,11 @@ const (
//
// See: https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036
const (
SynchronizedOutputMode = DECMode(2026)
ModeSynchronizedOutput = DECMode(2026)
SetSynchronizedOutputMode = "\x1b[?2026h"
ResetSynchronizedOutputMode = "\x1b[?2026l"
RequestSynchronizedOutputMode = "\x1b[?2026$p"
)
// Synchronized Output Mode. See [SynchronizedOutputMode].
//
// Deprecated: use [SynchronizedOutputMode], [SetSynchronizedOutputMode], and
// [ResetSynchronizedOutputMode], and [RequestSynchronizedOutputMode] instead.
const (
SyncdOutputMode = DECMode(2026)
EnableSyncdOutput = "\x1b[?2026h"
DisableSyncdOutput = "\x1b[?2026l"
RequestSyncdOutput = "\x1b[?2026$p"
SetModeSynchronizedOutput = "\x1b[?2026h"
ResetModeSynchronizedOutput = "\x1b[?2026l"
RequestModeSynchronizedOutput = "\x1b[?2026$p"
)
// Unicode Core Mode is a mode that determines whether the terminal should use
@ -754,41 +630,16 @@ const (
//
// See: https://github.com/contour-terminal/terminal-unicode-core
const (
UnicodeCoreMode = DECMode(2027)
ModeUnicodeCore = DECMode(2027)
SetUnicodeCoreMode = "\x1b[?2027h"
ResetUnicodeCoreMode = "\x1b[?2027l"
RequestUnicodeCoreMode = "\x1b[?2027$p"
SetModeUnicodeCore = "\x1b[?2027h"
ResetModeUnicodeCore = "\x1b[?2027l"
RequestModeUnicodeCore = "\x1b[?2027$p"
)
// Grapheme Clustering Mode is a mode that determines whether the terminal
// should look for grapheme clusters instead of single runes in the rendered
// text. This makes the terminal properly render combining characters such as
// emojis.
//
// See: https://github.com/contour-terminal/terminal-unicode-core
//
// Deprecated: use [GraphemeClusteringMode], [SetUnicodeCoreMode],
// [ResetUnicodeCoreMode], and [RequestUnicodeCoreMode] instead.
const (
GraphemeClusteringMode = DECMode(2027)
SetGraphemeClusteringMode = "\x1b[?2027h"
ResetGraphemeClusteringMode = "\x1b[?2027l"
RequestGraphemeClusteringMode = "\x1b[?2027$p"
)
// Grapheme Clustering Mode. See [GraphemeClusteringMode].
//
// Deprecated: use [SetUnicodeCoreMode], [ResetUnicodeCoreMode], and
// [RequestUnicodeCoreMode] instead.
const (
EnableGraphemeClustering = "\x1b[?2027h"
DisableGraphemeClustering = "\x1b[?2027l"
RequestGraphemeClustering = "\x1b[?2027$p"
)
// LightDarkMode is a mode that enables reporting the operating system's color
// ModeLightDark is a mode that enables reporting the operating system's color
// scheme (light or dark) preference. It reports the color scheme as a [DSR]
// and [LightDarkReport] escape sequences encoded as follows:
//
@ -802,14 +653,14 @@ const (
//
// See: https://contour-terminal.org/vt-extensions/color-palette-update-notifications/
const (
LightDarkMode = DECMode(2031)
ModeLightDark = DECMode(2031)
SetLightDarkMode = "\x1b[?2031h"
ResetLightDarkMode = "\x1b[?2031l"
RequestLightDarkMode = "\x1b[?2031$p"
SetModeLightDark = "\x1b[?2031h"
ResetModeLightDark = "\x1b[?2031l"
RequestModeLightDark = "\x1b[?2031$p"
)
// InBandResizeMode is a mode that reports terminal resize events as escape
// ModeInBandResize is a mode that reports terminal resize events as escape
// sequences. This is useful for systems that do not support [SIGWINCH] like
// Windows.
//
@ -819,11 +670,11 @@ const (
//
// See: https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83
const (
InBandResizeMode = DECMode(2048)
ModeInBandResize = DECMode(2048)
SetInBandResizeMode = "\x1b[?2048h"
ResetInBandResizeMode = "\x1b[?2048l"
RequestInBandResizeMode = "\x1b[?2048$p"
SetModeInBandResize = "\x1b[?2048h"
ResetModeInBandResize = "\x1b[?2048l"
RequestModeInBandResize = "\x1b[?2048$p"
)
// Win32Input is a mode that determines whether input is processed by the
@ -831,17 +682,9 @@ const (
//
// See: https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md
const (
Win32InputMode = DECMode(9001)
ModeWin32Input = DECMode(9001)
SetWin32InputMode = "\x1b[?9001h"
ResetWin32InputMode = "\x1b[?9001l"
RequestWin32InputMode = "\x1b[?9001$p"
)
// Deprecated: use [SetWin32InputMode], [ResetWin32InputMode], and
// [RequestWin32InputMode] instead.
const (
EnableWin32Input = "\x1b[?9001h" //nolint:revive // grouped constants
DisableWin32Input = "\x1b[?9001l"
RequestWin32Input = "\x1b[?9001$p"
SetModeWin32Input = "\x1b[?9001h"
ResetModeWin32Input = "\x1b[?9001l"
RequestModeWin32Input = "\x1b[?9001$p"
)

View File

@ -0,0 +1,495 @@
package ansi
// Keyboard Action Mode (KAM) controls locking of the keyboard.
//
// Deprecated: use [ModeKeyboardAction] instead.
const (
KeyboardActionMode = ANSIMode(2)
SetKeyboardActionMode = "\x1b[2h"
ResetKeyboardActionMode = "\x1b[2l"
RequestKeyboardActionMode = "\x1b[2$p"
)
// Insert/Replace Mode (IRM) determines whether characters are inserted or replaced.
//
// Deprecated: use [ModeInsertReplace] instead.
const (
InsertReplaceMode = ANSIMode(4)
SetInsertReplaceMode = "\x1b[4h"
ResetInsertReplaceMode = "\x1b[4l"
RequestInsertReplaceMode = "\x1b[4$p"
)
// BiDirectional Support Mode (BDSM) determines whether the terminal supports bidirectional text.
//
// Deprecated: use [ModeBiDirectionalSupport] instead.
const (
BiDirectionalSupportMode = ANSIMode(8)
SetBiDirectionalSupportMode = "\x1b[8h"
ResetBiDirectionalSupportMode = "\x1b[8l"
RequestBiDirectionalSupportMode = "\x1b[8$p"
)
// Send Receive Mode (SRM) or Local Echo Mode determines whether the terminal echoes characters.
//
// Deprecated: use [ModeSendReceive] instead.
const (
SendReceiveMode = ANSIMode(12)
LocalEchoMode = SendReceiveMode
SetSendReceiveMode = "\x1b[12h"
ResetSendReceiveMode = "\x1b[12l"
RequestSendReceiveMode = "\x1b[12$p"
SetLocalEchoMode = "\x1b[12h"
ResetLocalEchoMode = "\x1b[12l"
RequestLocalEchoMode = "\x1b[12$p"
)
// Line Feed/New Line Mode (LNM) determines whether the terminal interprets line feed as new line.
//
// Deprecated: use [ModeLineFeedNewLine] instead.
const (
LineFeedNewLineMode = ANSIMode(20)
SetLineFeedNewLineMode = "\x1b[20h"
ResetLineFeedNewLineMode = "\x1b[20l"
RequestLineFeedNewLineMode = "\x1b[20$p"
)
// Cursor Keys Mode (DECCKM) determines whether cursor keys send ANSI or application sequences.
//
// Deprecated: use [ModeCursorKeys] instead.
const (
CursorKeysMode = DECMode(1)
SetCursorKeysMode = "\x1b[?1h"
ResetCursorKeysMode = "\x1b[?1l"
RequestCursorKeysMode = "\x1b[?1$p"
)
// Cursor Keys mode.
//
// Deprecated: use [SetModeCursorKeys] and [ResetModeCursorKeys] instead.
const (
EnableCursorKeys = "\x1b[?1h"
DisableCursorKeys = "\x1b[?1l"
)
// Origin Mode (DECOM) determines whether the cursor moves to home or margin position.
//
// Deprecated: use [ModeOrigin] instead.
const (
OriginMode = DECMode(6)
SetOriginMode = "\x1b[?6h"
ResetOriginMode = "\x1b[?6l"
RequestOriginMode = "\x1b[?6$p"
)
// Auto Wrap Mode (DECAWM) determines whether the cursor wraps to the next line.
//
// Deprecated: use [ModeAutoWrap] instead.
const (
AutoWrapMode = DECMode(7)
SetAutoWrapMode = "\x1b[?7h"
ResetAutoWrapMode = "\x1b[?7l"
RequestAutoWrapMode = "\x1b[?7$p"
)
// X10 Mouse Mode determines whether the mouse reports on button presses.
//
// Deprecated: use [ModeMouseX10] instead.
const (
X10MouseMode = DECMode(9)
SetX10MouseMode = "\x1b[?9h"
ResetX10MouseMode = "\x1b[?9l"
RequestX10MouseMode = "\x1b[?9$p"
)
// Text Cursor Enable Mode (DECTCEM) shows/hides the cursor.
//
// Deprecated: use [ModeTextCursorEnable] instead.
const (
TextCursorEnableMode = DECMode(25)
SetTextCursorEnableMode = "\x1b[?25h"
ResetTextCursorEnableMode = "\x1b[?25l"
RequestTextCursorEnableMode = "\x1b[?25$p"
)
// Text Cursor Enable mode.
//
// Deprecated: use [SetModeTextCursorEnable] and [ResetModeTextCursorEnable] instead.
const (
CursorEnableMode = DECMode(25)
RequestCursorVisibility = "\x1b[?25$p"
)
// Numeric Keypad Mode (DECNKM) determines whether the keypad sends application or numeric sequences.
//
// Deprecated: use [ModeNumericKeypad] instead.
const (
NumericKeypadMode = DECMode(66)
SetNumericKeypadMode = "\x1b[?66h"
ResetNumericKeypadMode = "\x1b[?66l"
RequestNumericKeypadMode = "\x1b[?66$p"
)
// Backarrow Key Mode (DECBKM) determines whether the backspace key sends backspace or delete.
//
// Deprecated: use [ModeBackarrowKey] instead.
const (
BackarrowKeyMode = DECMode(67)
SetBackarrowKeyMode = "\x1b[?67h"
ResetBackarrowKeyMode = "\x1b[?67l"
RequestBackarrowKeyMode = "\x1b[?67$p"
)
// Left Right Margin Mode (DECLRMM) determines whether left and right margins can be set.
//
// Deprecated: use [ModeLeftRightMargin] instead.
const (
LeftRightMarginMode = DECMode(69)
SetLeftRightMarginMode = "\x1b[?69h"
ResetLeftRightMarginMode = "\x1b[?69l"
RequestLeftRightMarginMode = "\x1b[?69$p"
)
// Normal Mouse Mode determines whether the mouse reports on button presses and releases.
//
// Deprecated: use [ModeMouseNormal] instead.
const (
NormalMouseMode = DECMode(1000)
SetNormalMouseMode = "\x1b[?1000h"
ResetNormalMouseMode = "\x1b[?1000l"
RequestNormalMouseMode = "\x1b[?1000$p"
)
// VT Mouse Tracking mode.
//
// Deprecated: use [ModeMouseNormal] instead.
const (
MouseMode = DECMode(1000)
EnableMouse = "\x1b[?1000h"
DisableMouse = "\x1b[?1000l"
RequestMouse = "\x1b[?1000$p"
)
// Highlight Mouse Tracking determines whether the mouse reports on button presses and highlighted cells.
//
// Deprecated: use [ModeMouseHighlight] instead.
const (
HighlightMouseMode = DECMode(1001)
SetHighlightMouseMode = "\x1b[?1001h"
ResetHighlightMouseMode = "\x1b[?1001l"
RequestHighlightMouseMode = "\x1b[?1001$p"
)
// VT Hilite Mouse Tracking mode.
//
// Deprecated: use [ModeMouseHighlight] instead.
const (
MouseHiliteMode = DECMode(1001)
EnableMouseHilite = "\x1b[?1001h"
DisableMouseHilite = "\x1b[?1001l"
RequestMouseHilite = "\x1b[?1001$p"
)
// Button Event Mouse Tracking reports button-motion events when a button is pressed.
//
// Deprecated: use [ModeMouseButtonEvent] instead.
const (
ButtonEventMouseMode = DECMode(1002)
SetButtonEventMouseMode = "\x1b[?1002h"
ResetButtonEventMouseMode = "\x1b[?1002l"
RequestButtonEventMouseMode = "\x1b[?1002$p"
)
// Cell Motion Mouse Tracking mode.
//
// Deprecated: use [ModeMouseButtonEvent] instead.
const (
MouseCellMotionMode = DECMode(1002)
EnableMouseCellMotion = "\x1b[?1002h"
DisableMouseCellMotion = "\x1b[?1002l"
RequestMouseCellMotion = "\x1b[?1002$p"
)
// Any Event Mouse Tracking reports all motion events.
//
// Deprecated: use [ModeMouseAnyEvent] instead.
const (
AnyEventMouseMode = DECMode(1003)
SetAnyEventMouseMode = "\x1b[?1003h"
ResetAnyEventMouseMode = "\x1b[?1003l"
RequestAnyEventMouseMode = "\x1b[?1003$p"
)
// All Mouse Tracking mode.
//
// Deprecated: use [ModeMouseAnyEvent] instead.
const (
MouseAllMotionMode = DECMode(1003)
EnableMouseAllMotion = "\x1b[?1003h"
DisableMouseAllMotion = "\x1b[?1003l"
RequestMouseAllMotion = "\x1b[?1003$p"
)
// Focus Event Mode determines whether the terminal reports focus and blur events.
//
// Deprecated: use [ModeFocusEvent] instead.
const (
FocusEventMode = DECMode(1004)
SetFocusEventMode = "\x1b[?1004h"
ResetFocusEventMode = "\x1b[?1004l"
RequestFocusEventMode = "\x1b[?1004$p"
)
// Focus reporting mode.
//
// Deprecated: use [SetModeFocusEvent], [ResetModeFocusEvent], and
// [RequestModeFocusEvent] instead.
const (
ReportFocusMode = DECMode(1004)
EnableReportFocus = "\x1b[?1004h"
DisableReportFocus = "\x1b[?1004l"
RequestReportFocus = "\x1b[?1004$p"
)
// UTF-8 Extended Mouse Mode changes the mouse tracking encoding to use UTF-8 parameters.
//
// Deprecated: use [ModeMouseExtUtf8] instead.
const (
Utf8ExtMouseMode = DECMode(1005)
SetUtf8ExtMouseMode = "\x1b[?1005h"
ResetUtf8ExtMouseMode = "\x1b[?1005l"
RequestUtf8ExtMouseMode = "\x1b[?1005$p"
)
// SGR Extended Mouse Mode changes the mouse tracking encoding to use SGR parameters.
//
// Deprecated: use [ModeMouseExtSgr] instead.
const (
SgrExtMouseMode = DECMode(1006)
SetSgrExtMouseMode = "\x1b[?1006h"
ResetSgrExtMouseMode = "\x1b[?1006l"
RequestSgrExtMouseMode = "\x1b[?1006$p"
)
// Mouse SGR Extended mode.
//
// Deprecated: use [ModeMouseExtSgr], [SetModeMouseExtSgr],
// [ResetModeMouseExtSgr], and [RequestModeMouseExtSgr] instead.
const (
MouseSgrExtMode = DECMode(1006)
EnableMouseSgrExt = "\x1b[?1006h"
DisableMouseSgrExt = "\x1b[?1006l"
RequestMouseSgrExt = "\x1b[?1006$p"
)
// URXVT Extended Mouse Mode changes the mouse tracking encoding to use an alternate encoding.
//
// Deprecated: use [ModeMouseUrxvtExt] instead.
const (
UrxvtExtMouseMode = DECMode(1015)
SetUrxvtExtMouseMode = "\x1b[?1015h"
ResetUrxvtExtMouseMode = "\x1b[?1015l"
RequestUrxvtExtMouseMode = "\x1b[?1015$p"
)
// SGR Pixel Extended Mouse Mode changes the mouse tracking encoding to use SGR parameters with pixel coordinates.
//
// Deprecated: use [ModeMouseExtSgrPixel] instead.
const (
SgrPixelExtMouseMode = DECMode(1016)
SetSgrPixelExtMouseMode = "\x1b[?1016h"
ResetSgrPixelExtMouseMode = "\x1b[?1016l"
RequestSgrPixelExtMouseMode = "\x1b[?1016$p"
)
// Alternate Screen Mode determines whether the alternate screen buffer is active.
//
// Deprecated: use [ModeAltScreen] instead.
const (
AltScreenMode = DECMode(1047)
SetAltScreenMode = "\x1b[?1047h"
ResetAltScreenMode = "\x1b[?1047l"
RequestAltScreenMode = "\x1b[?1047$p"
)
// Save Cursor Mode saves the cursor position.
//
// Deprecated: use [ModeSaveCursor] instead.
const (
SaveCursorMode = DECMode(1048)
SetSaveCursorMode = "\x1b[?1048h"
ResetSaveCursorMode = "\x1b[?1048l"
RequestSaveCursorMode = "\x1b[?1048$p"
)
// Alternate Screen Save Cursor Mode saves the cursor position and switches to alternate screen.
//
// Deprecated: use [ModeAltScreenSaveCursor] instead.
const (
AltScreenSaveCursorMode = DECMode(1049)
SetAltScreenSaveCursorMode = "\x1b[?1049h"
ResetAltScreenSaveCursorMode = "\x1b[?1049l"
RequestAltScreenSaveCursorMode = "\x1b[?1049$p"
)
// Alternate Screen Buffer mode.
//
// Deprecated: use [ModeAltScreenSaveCursor] instead.
const (
AltScreenBufferMode = DECMode(1049)
SetAltScreenBufferMode = "\x1b[?1049h"
ResetAltScreenBufferMode = "\x1b[?1049l"
RequestAltScreenBufferMode = "\x1b[?1049$p"
EnableAltScreenBuffer = "\x1b[?1049h"
DisableAltScreenBuffer = "\x1b[?1049l"
RequestAltScreenBuffer = "\x1b[?1049$p"
)
// Bracketed Paste Mode determines whether pasted text is bracketed with escape sequences.
//
// Deprecated: use [ModeBracketedPaste] instead.
const (
BracketedPasteMode = DECMode(2004)
SetBracketedPasteMode = "\x1b[?2004h"
ResetBracketedPasteMode = "\x1b[?2004l"
RequestBracketedPasteMode = "\x1b[?2004$p"
)
// Deprecated: use [SetModeBracketedPaste], [ResetModeBracketedPaste], and
// [RequestModeBracketedPaste] instead.
const (
EnableBracketedPaste = "\x1b[?2004h" //nolint:revive
DisableBracketedPaste = "\x1b[?2004l"
RequestBracketedPaste = "\x1b[?2004$p"
)
// Synchronized Output Mode determines whether output is synchronized with the terminal.
//
// Deprecated: use [ModeSynchronizedOutput] instead.
const (
SynchronizedOutputMode = DECMode(2026)
SetSynchronizedOutputMode = "\x1b[?2026h"
ResetSynchronizedOutputMode = "\x1b[?2026l"
RequestSynchronizedOutputMode = "\x1b[?2026$p"
)
// Synchronized output mode.
//
// Deprecated: use [ModeSynchronizedOutput], [SetModeSynchronizedOutput],
// [ResetModeSynchronizedOutput], and [RequestModeSynchronizedOutput] instead.
const (
SyncdOutputMode = DECMode(2026)
EnableSyncdOutput = "\x1b[?2026h"
DisableSyncdOutput = "\x1b[?2026l"
RequestSyncdOutput = "\x1b[?2026$p"
)
// Unicode Core Mode determines whether the terminal uses Unicode grapheme clustering.
//
// Deprecated: use [ModeUnicodeCore] instead.
const (
UnicodeCoreMode = DECMode(2027)
SetUnicodeCoreMode = "\x1b[?2027h"
ResetUnicodeCoreMode = "\x1b[?2027l"
RequestUnicodeCoreMode = "\x1b[?2027$p"
)
// Grapheme Clustering Mode determines whether the terminal looks for grapheme clusters.
//
// Deprecated: use [ModeUnicodeCore], [SetModeUnicodeCore],
// [ResetModeUnicodeCore], and [RequestModeUnicodeCore] instead.
const (
GraphemeClusteringMode = DECMode(2027)
SetGraphemeClusteringMode = "\x1b[?2027h"
ResetGraphemeClusteringMode = "\x1b[?2027l"
RequestGraphemeClusteringMode = "\x1b[?2027$p"
)
// Unicode Core mode.
//
// Deprecated: use [SetModeUnicodeCore], [ResetModeUnicodeCore], and
// [RequestModeUnicodeCore] instead.
const (
EnableGraphemeClustering = "\x1b[?2027h"
DisableGraphemeClustering = "\x1b[?2027l"
RequestGraphemeClustering = "\x1b[?2027$p"
)
// Light Dark Mode enables reporting the operating system's color scheme preference.
//
// Deprecated: use [ModeLightDark] instead.
const (
LightDarkMode = DECMode(2031)
SetLightDarkMode = "\x1b[?2031h"
ResetLightDarkMode = "\x1b[?2031l"
RequestLightDarkMode = "\x1b[?2031$p"
)
// In Band Resize Mode reports terminal resize events as escape sequences.
//
// Deprecated: use [ModeInBandResize] instead.
const (
InBandResizeMode = DECMode(2048)
SetInBandResizeMode = "\x1b[?2048h"
ResetInBandResizeMode = "\x1b[?2048l"
RequestInBandResizeMode = "\x1b[?2048$p"
)
// Win32Input determines whether input is processed by the Win32 console and Conpty.
//
// Deprecated: use [ModeWin32Input] instead.
const (
Win32InputMode = DECMode(9001)
SetWin32InputMode = "\x1b[?9001h"
ResetWin32InputMode = "\x1b[?9001l"
RequestWin32InputMode = "\x1b[?9001$p"
)
// Deprecated: use [SetModeWin32Input], [ResetModeWin32Input], and
// [RequestModeWin32Input] instead.
const (
EnableWin32Input = "\x1b[?9001h" //nolint:revive
DisableWin32Input = "\x1b[?9001l"
RequestWin32Input = "\x1b[?9001$p"
)

View File

@ -134,7 +134,7 @@ func EncodeMouseButton(b MouseButton, motion, shift, alt, ctrl bool) (m byte) {
m |= bitMotion
}
return //nolint:nakedret
return m
}
// x10Offset is the offset for X10 mouse events.

View File

@ -1,5 +1,10 @@
package ansi
import (
"fmt"
"strings"
)
// Notify sends a desktop notification using iTerm's OSC 9.
//
// OSC 9 ; Mc ST
@ -11,3 +16,17 @@ package ansi
func Notify(s string) string {
return "\x1b]9;" + s + "\x07"
}
// DesktopNotification sends a desktop notification based on the extensible OSC
// 99 escape code.
//
// OSC 99 ; <metadata> ; <payload> ST
// OSC 99 ; <metadata> ; <payload> BEL
//
// Where <metadata> is a colon-separated list of key-value pairs, and
// <payload> is the notification body.
//
// See: https://sw.kovidgoyal.net/kitty/desktop-notifications/
func DesktopNotification(payload string, metadata ...string) string {
return fmt.Sprintf("\x1b]99;%s;%s\x07", strings.Join(metadata, ":"), payload)
}

View File

@ -4,8 +4,9 @@ import (
"unicode/utf8"
"github.com/charmbracelet/x/ansi/parser"
"github.com/clipperhouse/displaywidth"
"github.com/clipperhouse/uax29/v2/graphemes"
"github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
)
// State represents the state of the ANSI escape sequence parser used by
@ -176,10 +177,7 @@ func decodeSequence[T string | []byte](m Method, b T, state State, p *Parser) (s
}
if utf8.RuneStart(c) {
seq, _, width, _ = FirstGraphemeCluster(b, -1)
if m == WcWidth {
width = runewidth.StringWidth(string(seq))
}
seq, width = FirstGraphemeCluster(b, m)
i += len(seq)
return b[:i], width, i, NormalState
}
@ -434,17 +432,22 @@ func HasEscPrefix[T string | []byte](b T) bool {
return len(b) > 0 && b[0] == ESC
}
// FirstGraphemeCluster returns the first grapheme cluster in the given string or byte slice.
// This is a syntactic sugar function that wraps
// uniseg.FirstGraphemeClusterInString and uniseg.FirstGraphemeCluster.
func FirstGraphemeCluster[T string | []byte](b T, state int) (T, T, int, int) {
// FirstGraphemeCluster returns the first grapheme cluster in the given string
// or byte slice, and its monospace display width.
func FirstGraphemeCluster[T string | []byte](b T, m Method) (T, int) {
switch b := any(b).(type) {
case string:
cluster, rest, width, newState := uniseg.FirstGraphemeClusterInString(b, state)
return T(cluster), T(rest), width, newState
cluster := graphemes.FromString(b).First()
if m == WcWidth {
return T(cluster), runewidth.StringWidth(cluster)
}
return T(cluster), displaywidth.String(cluster)
case []byte:
cluster, rest, width, newState := uniseg.FirstGraphemeCluster(b, state)
return T(cluster), T(rest), width, newState
cluster := graphemes.FromBytes(b).First()
if m == WcWidth {
return T(cluster), runewidth.StringWidth(string(cluster))
}
return T(cluster), displaywidth.Bytes(cluster)
}
panic("unreachable")
}
@ -490,7 +493,7 @@ func Command(prefix, inter, final byte) (c int) {
c = int(final)
c |= int(prefix) << parser.PrefixShift
c |= int(inter) << parser.IntermedShift
return
return c
}
// Param represents a sequence parameter. Sequence parameters with
@ -520,5 +523,5 @@ func Parameter(p int, hasMore bool) (s int) {
if hasMore {
s |= parser.HasMoreFlag
}
return
return s
}

View File

@ -21,59 +21,59 @@ func SGR(ps ...Attr) string {
}
var attrStrings = map[int]string{
ResetAttr: resetAttr,
BoldAttr: boldAttr,
FaintAttr: faintAttr,
ItalicAttr: italicAttr,
UnderlineAttr: underlineAttr,
SlowBlinkAttr: slowBlinkAttr,
RapidBlinkAttr: rapidBlinkAttr,
ReverseAttr: reverseAttr,
ConcealAttr: concealAttr,
StrikethroughAttr: strikethroughAttr,
NormalIntensityAttr: normalIntensityAttr,
NoItalicAttr: noItalicAttr,
NoUnderlineAttr: noUnderlineAttr,
NoBlinkAttr: noBlinkAttr,
NoReverseAttr: noReverseAttr,
NoConcealAttr: noConcealAttr,
NoStrikethroughAttr: noStrikethroughAttr,
BlackForegroundColorAttr: blackForegroundColorAttr,
RedForegroundColorAttr: redForegroundColorAttr,
GreenForegroundColorAttr: greenForegroundColorAttr,
YellowForegroundColorAttr: yellowForegroundColorAttr,
BlueForegroundColorAttr: blueForegroundColorAttr,
MagentaForegroundColorAttr: magentaForegroundColorAttr,
CyanForegroundColorAttr: cyanForegroundColorAttr,
WhiteForegroundColorAttr: whiteForegroundColorAttr,
ExtendedForegroundColorAttr: extendedForegroundColorAttr,
DefaultForegroundColorAttr: defaultForegroundColorAttr,
BlackBackgroundColorAttr: blackBackgroundColorAttr,
RedBackgroundColorAttr: redBackgroundColorAttr,
GreenBackgroundColorAttr: greenBackgroundColorAttr,
YellowBackgroundColorAttr: yellowBackgroundColorAttr,
BlueBackgroundColorAttr: blueBackgroundColorAttr,
MagentaBackgroundColorAttr: magentaBackgroundColorAttr,
CyanBackgroundColorAttr: cyanBackgroundColorAttr,
WhiteBackgroundColorAttr: whiteBackgroundColorAttr,
ExtendedBackgroundColorAttr: extendedBackgroundColorAttr,
DefaultBackgroundColorAttr: defaultBackgroundColorAttr,
ExtendedUnderlineColorAttr: extendedUnderlineColorAttr,
DefaultUnderlineColorAttr: defaultUnderlineColorAttr,
BrightBlackForegroundColorAttr: brightBlackForegroundColorAttr,
BrightRedForegroundColorAttr: brightRedForegroundColorAttr,
BrightGreenForegroundColorAttr: brightGreenForegroundColorAttr,
BrightYellowForegroundColorAttr: brightYellowForegroundColorAttr,
BrightBlueForegroundColorAttr: brightBlueForegroundColorAttr,
BrightMagentaForegroundColorAttr: brightMagentaForegroundColorAttr,
BrightCyanForegroundColorAttr: brightCyanForegroundColorAttr,
BrightWhiteForegroundColorAttr: brightWhiteForegroundColorAttr,
BrightBlackBackgroundColorAttr: brightBlackBackgroundColorAttr,
BrightRedBackgroundColorAttr: brightRedBackgroundColorAttr,
BrightGreenBackgroundColorAttr: brightGreenBackgroundColorAttr,
BrightYellowBackgroundColorAttr: brightYellowBackgroundColorAttr,
BrightBlueBackgroundColorAttr: brightBlueBackgroundColorAttr,
BrightMagentaBackgroundColorAttr: brightMagentaBackgroundColorAttr,
BrightCyanBackgroundColorAttr: brightCyanBackgroundColorAttr,
BrightWhiteBackgroundColorAttr: brightWhiteBackgroundColorAttr,
AttrReset: attrReset,
AttrBold: attrBold,
AttrFaint: attrFaint,
AttrItalic: attrItalic,
AttrUnderline: attrUnderline,
AttrBlink: attrBlink,
AttrRapidBlink: attrRapidBlink,
AttrReverse: attrReverse,
AttrConceal: attrConceal,
AttrStrikethrough: attrStrikethrough,
AttrNormalIntensity: attrNormalIntensity,
AttrNoItalic: attrNoItalic,
AttrNoUnderline: attrNoUnderline,
AttrNoBlink: attrNoBlink,
AttrNoReverse: attrNoReverse,
AttrNoConceal: attrNoConceal,
AttrNoStrikethrough: attrNoStrikethrough,
AttrBlackForegroundColor: attrBlackForegroundColor,
AttrRedForegroundColor: attrRedForegroundColor,
AttrGreenForegroundColor: attrGreenForegroundColor,
AttrYellowForegroundColor: attrYellowForegroundColor,
AttrBlueForegroundColor: attrBlueForegroundColor,
AttrMagentaForegroundColor: attrMagentaForegroundColor,
AttrCyanForegroundColor: attrCyanForegroundColor,
AttrWhiteForegroundColor: attrWhiteForegroundColor,
AttrExtendedForegroundColor: attrExtendedForegroundColor,
AttrDefaultForegroundColor: attrDefaultForegroundColor,
AttrBlackBackgroundColor: attrBlackBackgroundColor,
AttrRedBackgroundColor: attrRedBackgroundColor,
AttrGreenBackgroundColor: attrGreenBackgroundColor,
AttrYellowBackgroundColor: attrYellowBackgroundColor,
AttrBlueBackgroundColor: attrBlueBackgroundColor,
AttrMagentaBackgroundColor: attrMagentaBackgroundColor,
AttrCyanBackgroundColor: attrCyanBackgroundColor,
AttrWhiteBackgroundColor: attrWhiteBackgroundColor,
AttrExtendedBackgroundColor: attrExtendedBackgroundColor,
AttrDefaultBackgroundColor: attrDefaultBackgroundColor,
AttrExtendedUnderlineColor: attrExtendedUnderlineColor,
AttrDefaultUnderlineColor: attrDefaultUnderlineColor,
AttrBrightBlackForegroundColor: attrBrightBlackForegroundColor,
AttrBrightRedForegroundColor: attrBrightRedForegroundColor,
AttrBrightGreenForegroundColor: attrBrightGreenForegroundColor,
AttrBrightYellowForegroundColor: attrBrightYellowForegroundColor,
AttrBrightBlueForegroundColor: attrBrightBlueForegroundColor,
AttrBrightMagentaForegroundColor: attrBrightMagentaForegroundColor,
AttrBrightCyanForegroundColor: attrBrightCyanForegroundColor,
AttrBrightWhiteForegroundColor: attrBrightWhiteForegroundColor,
AttrBrightBlackBackgroundColor: attrBrightBlackBackgroundColor,
AttrBrightRedBackgroundColor: attrBrightRedBackgroundColor,
AttrBrightGreenBackgroundColor: attrBrightGreenBackgroundColor,
AttrBrightYellowBackgroundColor: attrBrightYellowBackgroundColor,
AttrBrightBlueBackgroundColor: attrBrightBlueBackgroundColor,
AttrBrightMagentaBackgroundColor: attrBrightMagentaBackgroundColor,
AttrBrightCyanBackgroundColor: attrBrightCyanBackgroundColor,
AttrBrightWhiteBackgroundColor: attrBrightWhiteBackgroundColor,
}

View File

@ -17,7 +17,9 @@ type Attr = int
// Style represents an ANSI SGR (Select Graphic Rendition) style.
type Style []string
// NewStyle returns a new style with the given attributes.
// NewStyle returns a new style with the given attributes. Attributes are SGR
// (Select Graphic Rendition) codes that control text formatting like bold,
// italic, colors, etc.
func NewStyle(attrs ...Attr) Style {
if len(attrs) == 0 {
return Style{}
@ -46,7 +48,8 @@ func (s Style) String() string {
return "\x1b[" + strings.Join(s, ";") + "m"
}
// Styled returns a styled string with the given style applied.
// Styled returns a styled string with the given style applied. The style is
// applied at the beginning and reset at the end of the string.
func (s Style) Styled(str string) string {
if len(s) == 0 {
return str
@ -54,161 +57,211 @@ func (s Style) Styled(str string) string {
return s.String() + str + ResetStyle
}
// Reset appends the reset style attribute to the style.
// Reset appends the reset style attribute to the style. This resets all
// formatting attributes to their defaults.
func (s Style) Reset() Style {
return append(s, resetAttr)
return append(s, attrReset)
}
// Bold appends the bold style attribute to the style.
// Bold appends the bold or normal intensity style attribute to the style.
// You can use [Style.Normal] to reset to normal intensity.
func (s Style) Bold() Style {
return append(s, boldAttr)
return append(s, attrBold)
}
// Faint appends the faint style attribute to the style.
// Faint appends the faint or normal intensity style attribute to the style.
// You can use [Style.Normal] to reset to normal intensity.
func (s Style) Faint() Style {
return append(s, faintAttr)
return append(s, attrFaint)
}
// Italic appends the italic style attribute to the style.
func (s Style) Italic() Style {
return append(s, italicAttr)
// Italic appends the italic or no italic style attribute to the style.
// When v is true, text is rendered in italic. When false, italic is disabled.
func (s Style) Italic(v bool) Style {
if v {
return append(s, attrItalic)
}
return append(s, attrNoItalic)
}
// Underline appends the underline style attribute to the style.
func (s Style) Underline() Style {
return append(s, underlineAttr)
// Underline appends the underline or no underline style attribute to the style.
// When v is true, text is underlined. When false, underline is disabled.
func (s Style) Underline(v bool) Style {
if v {
return append(s, attrUnderline)
}
return append(s, attrNoUnderline)
}
// UnderlineStyle appends the underline style attribute to the style.
// Supports various underline styles including single, double, curly, dotted,
// and dashed.
func (s Style) UnderlineStyle(u UnderlineStyle) Style {
switch u {
case NoUnderlineStyle:
return s.NoUnderline()
case SingleUnderlineStyle:
return s.Underline()
case DoubleUnderlineStyle:
return append(s, doubleUnderlineStyle)
case CurlyUnderlineStyle:
return append(s, curlyUnderlineStyle)
case DottedUnderlineStyle:
return append(s, dottedUnderlineStyle)
case DashedUnderlineStyle:
return append(s, dashedUnderlineStyle)
case UnderlineStyleNone:
return s.Underline(false)
case UnderlineStyleSingle:
return s.Underline(true)
case UnderlineStyleDouble:
return append(s, underlineStyleDouble)
case UnderlineStyleCurly:
return append(s, underlineStyleCurly)
case UnderlineStyleDotted:
return append(s, underlineStyleDotted)
case UnderlineStyleDashed:
return append(s, underlineStyleDashed)
}
return s
}
// DoubleUnderline appends the double underline style attribute to the style.
// This is a convenience method for UnderlineStyle(DoubleUnderlineStyle).
func (s Style) DoubleUnderline() Style {
return s.UnderlineStyle(DoubleUnderlineStyle)
// Blink appends the slow blink or no blink style attribute to the style.
// When v is true, text blinks slowly (less than 150 per minute). When false,
// blinking is disabled.
func (s Style) Blink(v bool) Style {
if v {
return append(s, attrBlink)
}
return append(s, attrNoBlink)
}
// CurlyUnderline appends the curly underline style attribute to the style.
// This is a convenience method for UnderlineStyle(CurlyUnderlineStyle).
func (s Style) CurlyUnderline() Style {
return s.UnderlineStyle(CurlyUnderlineStyle)
// RapidBlink appends the rapid blink or no blink style attribute to the style.
// When v is true, text blinks rapidly (150+ per minute). When false, blinking
// is disabled.
//
// Note that this is not widely supported in terminal emulators.
func (s Style) RapidBlink(v bool) Style {
if v {
return append(s, attrRapidBlink)
}
return append(s, attrNoBlink)
}
// DottedUnderline appends the dotted underline style attribute to the style.
// This is a convenience method for UnderlineStyle(DottedUnderlineStyle).
func (s Style) DottedUnderline() Style {
return s.UnderlineStyle(DottedUnderlineStyle)
// Reverse appends the reverse or no reverse style attribute to the style.
// When v is true, foreground and background colors are swapped. When false,
// reverse video is disabled.
func (s Style) Reverse(v bool) Style {
if v {
return append(s, attrReverse)
}
return append(s, attrNoReverse)
}
// DashedUnderline appends the dashed underline style attribute to the style.
// This is a convenience method for UnderlineStyle(DashedUnderlineStyle).
func (s Style) DashedUnderline() Style {
return s.UnderlineStyle(DashedUnderlineStyle)
// Conceal appends the conceal or no conceal style attribute to the style.
// When v is true, text is hidden/concealed. When false, concealment is
// disabled.
func (s Style) Conceal(v bool) Style {
if v {
return append(s, attrConceal)
}
return append(s, attrNoConceal)
}
// SlowBlink appends the slow blink style attribute to the style.
func (s Style) SlowBlink() Style {
return append(s, slowBlinkAttr)
// Strikethrough appends the strikethrough or no strikethrough style attribute
// to the style. When v is true, text is rendered with a horizontal line through
// it. When false, strikethrough is disabled.
func (s Style) Strikethrough(v bool) Style {
if v {
return append(s, attrStrikethrough)
}
return append(s, attrNoStrikethrough)
}
// RapidBlink appends the rapid blink style attribute to the style.
func (s Style) RapidBlink() Style {
return append(s, rapidBlinkAttr)
}
// Reverse appends the reverse style attribute to the style.
func (s Style) Reverse() Style {
return append(s, reverseAttr)
}
// Conceal appends the conceal style attribute to the style.
func (s Style) Conceal() Style {
return append(s, concealAttr)
}
// Strikethrough appends the strikethrough style attribute to the style.
func (s Style) Strikethrough() Style {
return append(s, strikethroughAttr)
}
// NormalIntensity appends the normal intensity style attribute to the style.
func (s Style) NormalIntensity() Style {
return append(s, normalIntensityAttr)
// Normal appends the normal intensity style attribute to the style. This
// resets [Style.Bold] and [Style.Faint] attributes.
func (s Style) Normal() Style {
return append(s, attrNormalIntensity)
}
// NoItalic appends the no italic style attribute to the style.
//
// Deprecated: use [Style.Italic](false) instead.
func (s Style) NoItalic() Style {
return append(s, noItalicAttr)
return append(s, attrNoItalic)
}
// NoUnderline appends the no underline style attribute to the style.
//
// Deprecated: use [Style.Underline](false) instead.
func (s Style) NoUnderline() Style {
return append(s, noUnderlineAttr)
return append(s, attrNoUnderline)
}
// NoBlink appends the no blink style attribute to the style.
//
// Deprecated: use [Style.Blink](false) or [Style.RapidBlink](false) instead.
func (s Style) NoBlink() Style {
return append(s, noBlinkAttr)
return append(s, attrNoBlink)
}
// NoReverse appends the no reverse style attribute to the style.
//
// Deprecated: use [Style.Reverse](false) instead.
func (s Style) NoReverse() Style {
return append(s, noReverseAttr)
return append(s, attrNoReverse)
}
// NoConceal appends the no conceal style attribute to the style.
//
// Deprecated: use [Style.Conceal](false) instead.
func (s Style) NoConceal() Style {
return append(s, noConcealAttr)
return append(s, attrNoConceal)
}
// NoStrikethrough appends the no strikethrough style attribute to the style.
//
// Deprecated: use [Style.Strikethrough](false) instead.
func (s Style) NoStrikethrough() Style {
return append(s, noStrikethroughAttr)
return append(s, attrNoStrikethrough)
}
// DefaultForegroundColor appends the default foreground color style attribute to the style.
//
// Deprecated: use [Style.ForegroundColor](nil) instead.
func (s Style) DefaultForegroundColor() Style {
return append(s, defaultForegroundColorAttr)
return append(s, attrDefaultForegroundColor)
}
// DefaultBackgroundColor appends the default background color style attribute to the style.
//
// Deprecated: use [Style.BackgroundColor](nil) instead.
func (s Style) DefaultBackgroundColor() Style {
return append(s, defaultBackgroundColorAttr)
return append(s, attrDefaultBackgroundColor)
}
// DefaultUnderlineColor appends the default underline color style attribute to the style.
//
// Deprecated: use [Style.UnderlineColor](nil) instead.
func (s Style) DefaultUnderlineColor() Style {
return append(s, defaultUnderlineColorAttr)
return append(s, attrDefaultUnderlineColor)
}
// ForegroundColor appends the foreground color style attribute to the style.
// If c is nil, the default foreground color is used. Supports [BasicColor],
// [IndexedColor] (256-color), and [color.Color] (24-bit RGB).
func (s Style) ForegroundColor(c Color) Style {
if c == nil {
return append(s, attrDefaultForegroundColor)
}
return append(s, foregroundColorString(c))
}
// BackgroundColor appends the background color style attribute to the style.
// If c is nil, the default background color is used. Supports [BasicColor],
// [IndexedColor] (256-color), and [color.Color] (24-bit RGB).
func (s Style) BackgroundColor(c Color) Style {
if c == nil {
return append(s, attrDefaultBackgroundColor)
}
return append(s, backgroundColorString(c))
}
// UnderlineColor appends the underline color style attribute to the style.
// If c is nil, the default underline color is used. Supports [BasicColor],
// [IndexedColor] (256-color), and [color.Color] (24-bit RGB).
func (s Style) UnderlineColor(c Color) Style {
if c == nil {
return append(s, attrDefaultUnderlineColor)
}
return append(s, underlineColorString(c))
}
@ -217,146 +270,216 @@ func (s Style) UnderlineColor(c Color) Style {
type UnderlineStyle = byte
const (
doubleUnderlineStyle = "4:2"
curlyUnderlineStyle = "4:3"
dottedUnderlineStyle = "4:4"
dashedUnderlineStyle = "4:5"
underlineStyleDouble = "4:2"
underlineStyleCurly = "4:3"
underlineStyleDotted = "4:4"
underlineStyleDashed = "4:5"
)
// Underline styles constants.
const (
UnderlineStyleNone UnderlineStyle = iota
UnderlineStyleSingle
UnderlineStyleDouble
UnderlineStyleCurly
UnderlineStyleDotted
UnderlineStyleDashed
)
// Underline styles constants.
//
// Deprecated: use [UnderlineStyleNone], [UnderlineStyleSingle], etc. instead.
const (
// NoUnderlineStyle is the default underline style.
NoUnderlineStyle UnderlineStyle = iota
// SingleUnderlineStyle is a single underline style.
SingleUnderlineStyle
// DoubleUnderlineStyle is a double underline style.
DoubleUnderlineStyle
// CurlyUnderlineStyle is a curly underline style.
CurlyUnderlineStyle
// DottedUnderlineStyle is a dotted underline style.
DottedUnderlineStyle
// DashedUnderlineStyle is a dashed underline style.
DashedUnderlineStyle
)
// SGR (Select Graphic Rendition) style attributes.
// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
const (
ResetAttr Attr = 0
BoldAttr Attr = 1
FaintAttr Attr = 2
ItalicAttr Attr = 3
UnderlineAttr Attr = 4
SlowBlinkAttr Attr = 5
RapidBlinkAttr Attr = 6
ReverseAttr Attr = 7
ConcealAttr Attr = 8
StrikethroughAttr Attr = 9
NormalIntensityAttr Attr = 22
NoItalicAttr Attr = 23
NoUnderlineAttr Attr = 24
NoBlinkAttr Attr = 25
NoReverseAttr Attr = 27
NoConcealAttr Attr = 28
NoStrikethroughAttr Attr = 29
BlackForegroundColorAttr Attr = 30
RedForegroundColorAttr Attr = 31
GreenForegroundColorAttr Attr = 32
YellowForegroundColorAttr Attr = 33
BlueForegroundColorAttr Attr = 34
MagentaForegroundColorAttr Attr = 35
CyanForegroundColorAttr Attr = 36
WhiteForegroundColorAttr Attr = 37
ExtendedForegroundColorAttr Attr = 38
DefaultForegroundColorAttr Attr = 39
BlackBackgroundColorAttr Attr = 40
RedBackgroundColorAttr Attr = 41
GreenBackgroundColorAttr Attr = 42
YellowBackgroundColorAttr Attr = 43
BlueBackgroundColorAttr Attr = 44
MagentaBackgroundColorAttr Attr = 45
CyanBackgroundColorAttr Attr = 46
WhiteBackgroundColorAttr Attr = 47
ExtendedBackgroundColorAttr Attr = 48
DefaultBackgroundColorAttr Attr = 49
ExtendedUnderlineColorAttr Attr = 58
DefaultUnderlineColorAttr Attr = 59
BrightBlackForegroundColorAttr Attr = 90
BrightRedForegroundColorAttr Attr = 91
BrightGreenForegroundColorAttr Attr = 92
BrightYellowForegroundColorAttr Attr = 93
BrightBlueForegroundColorAttr Attr = 94
BrightMagentaForegroundColorAttr Attr = 95
BrightCyanForegroundColorAttr Attr = 96
BrightWhiteForegroundColorAttr Attr = 97
BrightBlackBackgroundColorAttr Attr = 100
BrightRedBackgroundColorAttr Attr = 101
BrightGreenBackgroundColorAttr Attr = 102
BrightYellowBackgroundColorAttr Attr = 103
BrightBlueBackgroundColorAttr Attr = 104
BrightMagentaBackgroundColorAttr Attr = 105
BrightCyanBackgroundColorAttr Attr = 106
BrightWhiteBackgroundColorAttr Attr = 107
AttrReset Attr = 0
AttrBold Attr = 1
AttrFaint Attr = 2
AttrItalic Attr = 3
AttrUnderline Attr = 4
AttrBlink Attr = 5
AttrRapidBlink Attr = 6
AttrReverse Attr = 7
AttrConceal Attr = 8
AttrStrikethrough Attr = 9
AttrNormalIntensity Attr = 22
AttrNoItalic Attr = 23
AttrNoUnderline Attr = 24
AttrNoBlink Attr = 25
AttrNoReverse Attr = 27
AttrNoConceal Attr = 28
AttrNoStrikethrough Attr = 29
AttrBlackForegroundColor Attr = 30
AttrRedForegroundColor Attr = 31
AttrGreenForegroundColor Attr = 32
AttrYellowForegroundColor Attr = 33
AttrBlueForegroundColor Attr = 34
AttrMagentaForegroundColor Attr = 35
AttrCyanForegroundColor Attr = 36
AttrWhiteForegroundColor Attr = 37
AttrExtendedForegroundColor Attr = 38
AttrDefaultForegroundColor Attr = 39
AttrBlackBackgroundColor Attr = 40
AttrRedBackgroundColor Attr = 41
AttrGreenBackgroundColor Attr = 42
AttrYellowBackgroundColor Attr = 43
AttrBlueBackgroundColor Attr = 44
AttrMagentaBackgroundColor Attr = 45
AttrCyanBackgroundColor Attr = 46
AttrWhiteBackgroundColor Attr = 47
AttrExtendedBackgroundColor Attr = 48
AttrDefaultBackgroundColor Attr = 49
AttrExtendedUnderlineColor Attr = 58
AttrDefaultUnderlineColor Attr = 59
AttrBrightBlackForegroundColor Attr = 90
AttrBrightRedForegroundColor Attr = 91
AttrBrightGreenForegroundColor Attr = 92
AttrBrightYellowForegroundColor Attr = 93
AttrBrightBlueForegroundColor Attr = 94
AttrBrightMagentaForegroundColor Attr = 95
AttrBrightCyanForegroundColor Attr = 96
AttrBrightWhiteForegroundColor Attr = 97
AttrBrightBlackBackgroundColor Attr = 100
AttrBrightRedBackgroundColor Attr = 101
AttrBrightGreenBackgroundColor Attr = 102
AttrBrightYellowBackgroundColor Attr = 103
AttrBrightBlueBackgroundColor Attr = 104
AttrBrightMagentaBackgroundColor Attr = 105
AttrBrightCyanBackgroundColor Attr = 106
AttrBrightWhiteBackgroundColor Attr = 107
RGBColorIntroducerAttr Attr = 2
ExtendedColorIntroducerAttr Attr = 5
AttrRGBColorIntroducer Attr = 2
AttrExtendedColorIntroducer Attr = 5
)
// SGR (Select Graphic Rendition) style attributes.
//
// Deprecated: use Attr* constants instead.
const (
ResetAttr = AttrReset
BoldAttr = AttrBold
FaintAttr = AttrFaint
ItalicAttr = AttrItalic
UnderlineAttr = AttrUnderline
SlowBlinkAttr = AttrBlink
RapidBlinkAttr = AttrRapidBlink
ReverseAttr = AttrReverse
ConcealAttr = AttrConceal
StrikethroughAttr = AttrStrikethrough
NormalIntensityAttr = AttrNormalIntensity
NoItalicAttr = AttrNoItalic
NoUnderlineAttr = AttrNoUnderline
NoBlinkAttr = AttrNoBlink
NoReverseAttr = AttrNoReverse
NoConcealAttr = AttrNoConceal
NoStrikethroughAttr = AttrNoStrikethrough
BlackForegroundColorAttr = AttrBlackForegroundColor
RedForegroundColorAttr = AttrRedForegroundColor
GreenForegroundColorAttr = AttrGreenForegroundColor
YellowForegroundColorAttr = AttrYellowForegroundColor
BlueForegroundColorAttr = AttrBlueForegroundColor
MagentaForegroundColorAttr = AttrMagentaForegroundColor
CyanForegroundColorAttr = AttrCyanForegroundColor
WhiteForegroundColorAttr = AttrWhiteForegroundColor
ExtendedForegroundColorAttr = AttrExtendedForegroundColor
DefaultForegroundColorAttr = AttrDefaultForegroundColor
BlackBackgroundColorAttr = AttrBlackBackgroundColor
RedBackgroundColorAttr = AttrRedBackgroundColor
GreenBackgroundColorAttr = AttrGreenBackgroundColor
YellowBackgroundColorAttr = AttrYellowBackgroundColor
BlueBackgroundColorAttr = AttrBlueBackgroundColor
MagentaBackgroundColorAttr = AttrMagentaBackgroundColor
CyanBackgroundColorAttr = AttrCyanBackgroundColor
WhiteBackgroundColorAttr = AttrWhiteBackgroundColor
ExtendedBackgroundColorAttr = AttrExtendedBackgroundColor
DefaultBackgroundColorAttr = AttrDefaultBackgroundColor
ExtendedUnderlineColorAttr = AttrExtendedUnderlineColor
DefaultUnderlineColorAttr = AttrDefaultUnderlineColor
BrightBlackForegroundColorAttr = AttrBrightBlackForegroundColor
BrightRedForegroundColorAttr = AttrBrightRedForegroundColor
BrightGreenForegroundColorAttr = AttrBrightGreenForegroundColor
BrightYellowForegroundColorAttr = AttrBrightYellowForegroundColor
BrightBlueForegroundColorAttr = AttrBrightBlueForegroundColor
BrightMagentaForegroundColorAttr = AttrBrightMagentaForegroundColor
BrightCyanForegroundColorAttr = AttrBrightCyanForegroundColor
BrightWhiteForegroundColorAttr = AttrBrightWhiteForegroundColor
BrightBlackBackgroundColorAttr = AttrBrightBlackBackgroundColor
BrightRedBackgroundColorAttr = AttrBrightRedBackgroundColor
BrightGreenBackgroundColorAttr = AttrBrightGreenBackgroundColor
BrightYellowBackgroundColorAttr = AttrBrightYellowBackgroundColor
BrightBlueBackgroundColorAttr = AttrBrightBlueBackgroundColor
BrightMagentaBackgroundColorAttr = AttrBrightMagentaBackgroundColor
BrightCyanBackgroundColorAttr = AttrBrightCyanBackgroundColor
BrightWhiteBackgroundColorAttr = AttrBrightWhiteBackgroundColor
RGBColorIntroducerAttr = AttrRGBColorIntroducer
ExtendedColorIntroducerAttr = AttrExtendedColorIntroducer
)
const (
resetAttr = "0"
boldAttr = "1"
faintAttr = "2"
italicAttr = "3"
underlineAttr = "4"
slowBlinkAttr = "5"
rapidBlinkAttr = "6"
reverseAttr = "7"
concealAttr = "8"
strikethroughAttr = "9"
normalIntensityAttr = "22"
noItalicAttr = "23"
noUnderlineAttr = "24"
noBlinkAttr = "25"
noReverseAttr = "27"
noConcealAttr = "28"
noStrikethroughAttr = "29"
blackForegroundColorAttr = "30"
redForegroundColorAttr = "31"
greenForegroundColorAttr = "32"
yellowForegroundColorAttr = "33"
blueForegroundColorAttr = "34"
magentaForegroundColorAttr = "35"
cyanForegroundColorAttr = "36"
whiteForegroundColorAttr = "37"
extendedForegroundColorAttr = "38"
defaultForegroundColorAttr = "39"
blackBackgroundColorAttr = "40"
redBackgroundColorAttr = "41"
greenBackgroundColorAttr = "42"
yellowBackgroundColorAttr = "43"
blueBackgroundColorAttr = "44"
magentaBackgroundColorAttr = "45"
cyanBackgroundColorAttr = "46"
whiteBackgroundColorAttr = "47"
extendedBackgroundColorAttr = "48"
defaultBackgroundColorAttr = "49"
extendedUnderlineColorAttr = "58"
defaultUnderlineColorAttr = "59"
brightBlackForegroundColorAttr = "90"
brightRedForegroundColorAttr = "91"
brightGreenForegroundColorAttr = "92"
brightYellowForegroundColorAttr = "93"
brightBlueForegroundColorAttr = "94"
brightMagentaForegroundColorAttr = "95"
brightCyanForegroundColorAttr = "96"
brightWhiteForegroundColorAttr = "97"
brightBlackBackgroundColorAttr = "100"
brightRedBackgroundColorAttr = "101"
brightGreenBackgroundColorAttr = "102"
brightYellowBackgroundColorAttr = "103"
brightBlueBackgroundColorAttr = "104"
brightMagentaBackgroundColorAttr = "105"
brightCyanBackgroundColorAttr = "106"
brightWhiteBackgroundColorAttr = "107"
attrReset = "0"
attrBold = "1"
attrFaint = "2"
attrItalic = "3"
attrUnderline = "4"
attrBlink = "5"
attrRapidBlink = "6"
attrReverse = "7"
attrConceal = "8"
attrStrikethrough = "9"
attrNormalIntensity = "22"
attrNoItalic = "23"
attrNoUnderline = "24"
attrNoBlink = "25"
attrNoReverse = "27"
attrNoConceal = "28"
attrNoStrikethrough = "29"
attrBlackForegroundColor = "30"
attrRedForegroundColor = "31"
attrGreenForegroundColor = "32"
attrYellowForegroundColor = "33"
attrBlueForegroundColor = "34"
attrMagentaForegroundColor = "35"
attrCyanForegroundColor = "36"
attrWhiteForegroundColor = "37"
attrExtendedForegroundColor = "38"
attrDefaultForegroundColor = "39"
attrBlackBackgroundColor = "40"
attrRedBackgroundColor = "41"
attrGreenBackgroundColor = "42"
attrYellowBackgroundColor = "43"
attrBlueBackgroundColor = "44"
attrMagentaBackgroundColor = "45"
attrCyanBackgroundColor = "46"
attrWhiteBackgroundColor = "47"
attrExtendedBackgroundColor = "48"
attrDefaultBackgroundColor = "49"
attrExtendedUnderlineColor = "58"
attrDefaultUnderlineColor = "59"
attrBrightBlackForegroundColor = "90"
attrBrightRedForegroundColor = "91"
attrBrightGreenForegroundColor = "92"
attrBrightYellowForegroundColor = "93"
attrBrightBlueForegroundColor = "94"
attrBrightMagentaForegroundColor = "95"
attrBrightCyanForegroundColor = "96"
attrBrightWhiteForegroundColor = "97"
attrBrightBlackBackgroundColor = "100"
attrBrightRedBackgroundColor = "101"
attrBrightGreenBackgroundColor = "102"
attrBrightYellowBackgroundColor = "103"
attrBrightBlueBackgroundColor = "104"
attrBrightMagentaBackgroundColor = "105"
attrBrightCyanBackgroundColor = "106"
attrBrightWhiteBackgroundColor = "107"
)
// foregroundColorString returns the style SGR attribute for the given
@ -369,37 +492,37 @@ func foregroundColorString(c Color) string {
// "3<n>" or "9<n>" where n is the color number from 0 to 7
switch c {
case Black:
return blackForegroundColorAttr
return attrBlackForegroundColor
case Red:
return redForegroundColorAttr
return attrRedForegroundColor
case Green:
return greenForegroundColorAttr
return attrGreenForegroundColor
case Yellow:
return yellowForegroundColorAttr
return attrYellowForegroundColor
case Blue:
return blueForegroundColorAttr
return attrBlueForegroundColor
case Magenta:
return magentaForegroundColorAttr
return attrMagentaForegroundColor
case Cyan:
return cyanForegroundColorAttr
return attrCyanForegroundColor
case White:
return whiteForegroundColorAttr
return attrWhiteForegroundColor
case BrightBlack:
return brightBlackForegroundColorAttr
return attrBrightBlackForegroundColor
case BrightRed:
return brightRedForegroundColorAttr
return attrBrightRedForegroundColor
case BrightGreen:
return brightGreenForegroundColorAttr
return attrBrightGreenForegroundColor
case BrightYellow:
return brightYellowForegroundColorAttr
return attrBrightYellowForegroundColor
case BrightBlue:
return brightBlueForegroundColorAttr
return attrBrightBlueForegroundColor
case BrightMagenta:
return brightMagentaForegroundColorAttr
return attrBrightMagentaForegroundColor
case BrightCyan:
return brightCyanForegroundColorAttr
return attrBrightCyanForegroundColor
case BrightWhite:
return brightWhiteForegroundColorAttr
return attrBrightWhiteForegroundColor
}
case ExtendedColor:
// 256-color ANSI foreground
@ -414,7 +537,7 @@ func foregroundColorString(c Color) string {
strconv.FormatUint(uint64(shift(g)), 10) + ";" +
strconv.FormatUint(uint64(shift(b)), 10)
}
return defaultForegroundColorAttr
return attrDefaultForegroundColor
}
// backgroundColorString returns the style SGR attribute for the given
@ -427,37 +550,37 @@ func backgroundColorString(c Color) string {
// "4<n>" or "10<n>" where n is the color number from 0 to 7
switch c {
case Black:
return blackBackgroundColorAttr
return attrBlackBackgroundColor
case Red:
return redBackgroundColorAttr
return attrRedBackgroundColor
case Green:
return greenBackgroundColorAttr
return attrGreenBackgroundColor
case Yellow:
return yellowBackgroundColorAttr
return attrYellowBackgroundColor
case Blue:
return blueBackgroundColorAttr
return attrBlueBackgroundColor
case Magenta:
return magentaBackgroundColorAttr
return attrMagentaBackgroundColor
case Cyan:
return cyanBackgroundColorAttr
return attrCyanBackgroundColor
case White:
return whiteBackgroundColorAttr
return attrWhiteBackgroundColor
case BrightBlack:
return brightBlackBackgroundColorAttr
return attrBrightBlackBackgroundColor
case BrightRed:
return brightRedBackgroundColorAttr
return attrBrightRedBackgroundColor
case BrightGreen:
return brightGreenBackgroundColorAttr
return attrBrightGreenBackgroundColor
case BrightYellow:
return brightYellowBackgroundColorAttr
return attrBrightYellowBackgroundColor
case BrightBlue:
return brightBlueBackgroundColorAttr
return attrBrightBlueBackgroundColor
case BrightMagenta:
return brightMagentaBackgroundColorAttr
return attrBrightMagentaBackgroundColor
case BrightCyan:
return brightCyanBackgroundColorAttr
return attrBrightCyanBackgroundColor
case BrightWhite:
return brightWhiteBackgroundColorAttr
return attrBrightWhiteBackgroundColor
}
case ExtendedColor:
// 256-color ANSI foreground
@ -472,7 +595,7 @@ func backgroundColorString(c Color) string {
strconv.FormatUint(uint64(shift(g)), 10) + ";" +
strconv.FormatUint(uint64(shift(b)), 10)
}
return defaultBackgroundColorAttr
return attrDefaultBackgroundColor
}
// underlineColorString returns the style SGR attribute for the given underline
@ -498,7 +621,7 @@ func underlineColorString(c Color) string {
strconv.FormatUint(uint64(shift(g)), 10) + ";" +
strconv.FormatUint(uint64(shift(b)), 10)
}
return defaultUnderlineColorAttr
return attrDefaultUnderlineColor
}
// ReadStyleColor decodes a color from a slice of parameters. It returns the
@ -526,7 +649,7 @@ func underlineColorString(c Color) string {
// 2. Support ignoring and omitting the color space id (second parameter) with respect to RGB colors
// 3. Support ignoring and omitting the 6th parameter with respect to RGB and CMY colors
// 4. Support reading RGBA colors
func ReadStyleColor(params Params, co *color.Color) (n int) {
func ReadStyleColor(params Params, co *color.Color) int {
if len(params) < 2 { // Need at least SGR type and color type
return 0
}
@ -535,7 +658,7 @@ func ReadStyleColor(params Params, co *color.Color) (n int) {
s := params[0]
p := params[1]
colorType := p.Param(0)
n = 2
n := 2
paramsfn := func() (p1, p2, p3, p4 int) {
// Where should we start reading the color?
@ -594,7 +717,7 @@ func ReadStyleColor(params Params, co *color.Color) (n int) {
B: uint8(b), //nolint:gosec
A: 0xff,
}
return //nolint:nakedret
return n
case 3: // CMY direct color
if len(params) < 5 {
@ -612,7 +735,7 @@ func ReadStyleColor(params Params, co *color.Color) (n int) {
Y: uint8(y), //nolint:gosec
K: 0,
}
return //nolint:nakedret
return n
case 4: // CMYK direct color
if len(params) < 6 {
@ -630,7 +753,7 @@ func ReadStyleColor(params Params, co *color.Color) (n int) {
Y: uint8(y), //nolint:gosec
K: uint8(k), //nolint:gosec
}
return //nolint:nakedret
return n
case 5: // indexed color
if len(params) < 3 {
@ -665,7 +788,7 @@ func ReadStyleColor(params Params, co *color.Color) (n int) {
B: uint8(b), //nolint:gosec
A: uint8(a), //nolint:gosec
}
return //nolint:nakedret
return n
default:
return 0

View File

@ -1,11 +1,11 @@
package ansi
import (
"bytes"
"strings"
"github.com/charmbracelet/x/ansi/parser"
"github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
"github.com/clipperhouse/displaywidth"
"github.com/clipperhouse/uax29/v2/graphemes"
)
// Cut the string, without adding any prefix or tail strings. This function is
@ -74,12 +74,11 @@ func truncate(m Method, s string, length int, tail string) string {
return ""
}
var cluster []byte
var buf bytes.Buffer
var cluster string
var buf strings.Builder
curWidth := 0
ignoring := false
pstate := parser.GroundState // initial state
b := []byte(s)
i := 0
// Here we iterate over the bytes of the string and collect printable
@ -88,16 +87,12 @@ func truncate(m Method, s string, length int, tail string) string {
//
// Once we reach the given length, we start ignoring characters and only
// collect ANSI escape codes until we reach the end of string.
for i < len(b) {
state, action := parser.Table.Transition(pstate, b[i])
for i < len(s) {
state, action := parser.Table.Transition(pstate, s[i])
if state == parser.Utf8State {
// This action happens when we transition to the Utf8State.
var width int
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
if m == WcWidth {
width = runewidth.StringWidth(string(cluster))
}
cluster, width = FirstGraphemeCluster(s[i:], m)
// increment the index by the length of the cluster
i += len(cluster)
curWidth += width
@ -118,7 +113,7 @@ func truncate(m Method, s string, length int, tail string) string {
continue
}
buf.Write(cluster)
buf.WriteString(cluster)
// Done collecting, now we're back in the ground state.
pstate = parser.GroundState
@ -152,7 +147,7 @@ func truncate(m Method, s string, length int, tail string) string {
}
fallthrough
default:
buf.WriteByte(b[i])
buf.WriteByte(s[i])
i++
}
@ -193,27 +188,23 @@ func truncateLeft(m Method, s string, n int, prefix string) string {
return s
}
var cluster []byte
var buf bytes.Buffer
var cluster string
var buf strings.Builder
curWidth := 0
ignoring := true
pstate := parser.GroundState
b := []byte(s)
i := 0
for i < len(b) {
for i < len(s) {
if !ignoring {
buf.Write(b[i:])
buf.WriteString(s[i:])
break
}
state, action := parser.Table.Transition(pstate, b[i])
state, action := parser.Table.Transition(pstate, s[i])
if state == parser.Utf8State {
var width int
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
if m == WcWidth {
width = runewidth.StringWidth(string(cluster))
}
cluster, width = FirstGraphemeCluster(s[i:], m)
i += len(cluster)
curWidth += width
@ -224,7 +215,7 @@ func truncateLeft(m Method, s string, n int, prefix string) string {
}
if curWidth > n {
buf.Write(cluster)
buf.WriteString(cluster)
}
if ignoring {
@ -259,7 +250,7 @@ func truncateLeft(m Method, s string, n int, prefix string) string {
}
fallthrough
default:
buf.WriteByte(b[i])
buf.WriteByte(s[i])
i++
}
@ -278,22 +269,22 @@ func truncateLeft(m Method, s string, n int, prefix string) string {
// You can use this with [Truncate], [TruncateLeft], and [Cut].
func ByteToGraphemeRange(str string, byteStart, byteStop int) (charStart, charStop int) {
bytePos, charPos := 0, 0
gr := uniseg.NewGraphemes(str)
gr := graphemes.FromString(str)
for byteStart > bytePos {
if !gr.Next() {
break
}
bytePos += len(gr.Str())
charPos += max(1, gr.Width())
bytePos += len(gr.Value())
charPos += max(1, displaywidth.String(gr.Value()))
}
charStart = charPos
for byteStop > bytePos {
if !gr.Next() {
break
}
bytePos += len(gr.Str())
charPos += max(1, gr.Width())
bytePos += len(gr.Value())
charPos += max(1, displaywidth.String(gr.Value()))
}
charStop = charPos
return
return charStart, charStop
}

17
vendor/github.com/charmbracelet/x/ansi/urxvt.go generated vendored Normal file
View File

@ -0,0 +1,17 @@
package ansi
import (
"fmt"
"strings"
)
// URxvtExt returns an escape sequence for calling a URxvt perl extension with
// the given name and parameters.
//
// OSC 777 ; extension_name ; param1 ; param2 ; ... ST
// OSC 777 ; extension_name ; param1 ; param2 ; ... BEL
//
// See: https://man.archlinux.org/man/extra/rxvt-unicode/urxvt.7.en#XTerm_Operating_System_Commands
func URxvtExt(extension string, params ...string) string {
return fmt.Sprintf("\x1b]777;%s;%s\x07", extension, strings.Join(params, ";"))
}

View File

@ -4,8 +4,6 @@ import (
"bytes"
"github.com/charmbracelet/x/ansi/parser"
"github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
)
// Strip removes ANSI escape codes from a string.
@ -83,20 +81,16 @@ func stringWidth(m Method, s string) int {
}
var (
pstate = parser.GroundState // initial state
cluster string
width int
pstate = parser.GroundState // initial state
width int
)
for i := 0; i < len(s); i++ {
state, action := parser.Table.Transition(pstate, s[i])
if state == parser.Utf8State {
var w int
cluster, _, w, _ = uniseg.FirstGraphemeClusterInString(s[i:], -1)
if m == WcWidth {
w = runewidth.StringWidth(cluster)
}
cluster, w := FirstGraphemeCluster(s[i:], m)
width += w
i += len(cluster) - 1
pstate = parser.GroundState
continue

View File

@ -2,12 +2,11 @@ package ansi
import (
"bytes"
"strings"
"unicode"
"unicode/utf8"
"github.com/charmbracelet/x/ansi/parser"
"github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
)
// nbsp is a non-breaking space.
@ -55,12 +54,9 @@ func hardwrap(m Method, s string, limit int, preserveSpace bool) string {
i := 0
for i < len(b) {
state, action := parser.Table.Transition(pstate, b[i])
if state == parser.Utf8State { //nolint:nestif
if state == parser.Utf8State {
var width int
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
if m == WcWidth {
width = runewidth.StringWidth(string(cluster))
}
cluster, width = FirstGraphemeCluster(b[i:], m)
i += len(cluster)
if curWidth+width > limit {
@ -192,10 +188,7 @@ func wordwrap(m Method, s string, limit int, breakpoints string) string {
state, action := parser.Table.Transition(pstate, b[i])
if state == parser.Utf8State { //nolint:nestif
var width int
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
if m == WcWidth {
width = runewidth.StringWidth(string(cluster))
}
cluster, width = FirstGraphemeCluster(b[i:], m)
i += len(cluster)
r, _ := utf8.DecodeRune(cluster)
@ -303,7 +296,7 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
}
var (
cluster []byte
cluster string
buf bytes.Buffer
word bytes.Buffer
space bytes.Buffer
@ -311,10 +304,12 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
curWidth int // written width of the line
wordLen int // word buffer len without ANSI escape codes
pstate = parser.GroundState // initial state
b = []byte(s)
)
addSpace := func() {
if spaceWidth == 0 && space.Len() == 0 {
return
}
curWidth += spaceWidth
buf.Write(space.Bytes())
space.Reset()
@ -341,30 +336,27 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
}
i := 0
for i < len(b) {
state, action := parser.Table.Transition(pstate, b[i])
for i < len(s) {
state, action := parser.Table.Transition(pstate, s[i])
if state == parser.Utf8State { //nolint:nestif
var width int
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
if m == WcWidth {
width = runewidth.StringWidth(string(cluster))
}
cluster, width = FirstGraphemeCluster(s[i:], m)
i += len(cluster)
r, _ := utf8.DecodeRune(cluster)
r, _ := utf8.DecodeRuneInString(cluster)
switch {
case r != utf8.RuneError && unicode.IsSpace(r) && r != nbsp: // nbsp is a non-breaking space
addWord()
space.WriteRune(r)
spaceWidth += width
case bytes.ContainsAny(cluster, breakpoints):
case strings.ContainsAny(cluster, breakpoints):
addSpace()
if curWidth+wordLen+width > limit {
word.Write(cluster)
word.WriteString(cluster)
wordLen += width
} else {
addWord()
buf.Write(cluster)
buf.WriteString(cluster)
curWidth += width
}
default:
@ -373,12 +365,17 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
addWord()
}
word.Write(cluster)
word.WriteString(cluster)
wordLen += width
if curWidth+wordLen+spaceWidth > limit {
addNewline()
}
if wordLen == limit {
// Hardwrap the word if it's too long
addWord()
}
}
pstate = parser.GroundState
@ -387,7 +384,7 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
switch action {
case parser.PrintAction, parser.ExecuteAction:
switch r := rune(b[i]); {
switch r := rune(s[i]); {
case r == '\n':
if wordLen == 0 {
if curWidth+spaceWidth > limit {
@ -424,6 +421,7 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
if curWidth == limit {
addNewline()
}
word.WriteRune(r)
wordLen++
@ -438,7 +436,7 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
}
default:
word.WriteByte(b[i])
word.WriteByte(s[i])
}
// We manage the UTF8 state separately manually above.

View File

@ -1,3 +1,4 @@
// Package cellbuf provides terminal cell buffer functionality.
package cellbuf
import (
@ -24,7 +25,7 @@ func NewCell(r rune, comb ...rune) (c *Cell) {
}
c.Comb = comb
c.Width = runewidth.StringWidth(string(append([]rune{r}, comb...)))
return
return c
}
// NewCellString returns a new cell with the given string content. This is a
@ -46,7 +47,7 @@ func NewCellString(s string) (c *Cell) {
c.Comb = append(c.Comb, r)
}
}
return
return c
}
// NewGraphemeCell returns a new cell. This is a convenience function that
@ -71,7 +72,7 @@ func newGraphemeCell(s string, w int) (c *Cell) {
c.Comb = append(c.Comb, r)
}
}
return
return c
}
// Line represents a line in the terminal.
@ -104,7 +105,7 @@ func (l Line) String() (s string) {
}
}
s = strings.TrimRight(s, " ")
return
return s
}
// At returns the cell at the given x position.
@ -150,7 +151,7 @@ func (l Line) set(x int, c *Cell, clone bool) bool {
for j := 1; j < maxCellWidth && x-j >= 0; j++ {
wide := l.At(x - j)
if wide != nil && wide.Width > 1 && j < wide.Width {
for k := 0; k < wide.Width; k++ {
for k := range wide.Width {
l[x-j+k] = wide.Clone().Blank()
}
break
@ -206,7 +207,7 @@ func (b *Buffer) String() (s string) {
s += "\r\n"
}
}
return
return s
}
// Line returns a pointer to the line at the given y position.
@ -296,7 +297,7 @@ func (b *Buffer) FillRect(c *Cell, rect Rectangle) {
}
for y := rect.Min.Y; y < rect.Max.Y; y++ {
for x := rect.Min.X; x < rect.Max.X; x += cellWidth {
b.setCell(x, y, c, false) //nolint:errcheck
b.setCell(x, y, c, false)
}
}
}

View File

@ -96,7 +96,7 @@ func (c *Cell) Clear() bool {
func (c *Cell) Clone() (n *Cell) {
n = new(Cell)
*n = *c
return
return n
}
// Blank makes the cell a blank cell by setting the rune to a space, comb to
@ -164,12 +164,12 @@ type UnderlineStyle = ansi.UnderlineStyle
// These are the available underline styles.
const (
NoUnderline = ansi.NoUnderlineStyle
SingleUnderline = ansi.SingleUnderlineStyle
DoubleUnderline = ansi.DoubleUnderlineStyle
CurlyUnderline = ansi.CurlyUnderlineStyle
DottedUnderline = ansi.DottedUnderlineStyle
DashedUnderline = ansi.DashedUnderlineStyle
NoUnderline = ansi.UnderlineStyleNone
SingleUnderline = ansi.UnderlineStyleSingle
DoubleUnderline = ansi.UnderlineStyleDouble
CurlyUnderline = ansi.UnderlineStyleCurly
DottedUnderline = ansi.UnderlineStyleDotted
DashedUnderline = ansi.UnderlineStyleDashed
)
// Style represents the Style of a cell.
@ -189,7 +189,7 @@ func (s Style) Sequence() string {
var b ansi.Style
if s.Attrs != 0 {
if s.Attrs != 0 { //nolint:nestif
if s.Attrs&BoldAttr != 0 {
b = b.Bold()
}
@ -197,36 +197,31 @@ func (s Style) Sequence() string {
b = b.Faint()
}
if s.Attrs&ItalicAttr != 0 {
b = b.Italic()
b = b.Italic(true)
}
if s.Attrs&SlowBlinkAttr != 0 {
b = b.SlowBlink()
b = b.Blink(true)
}
if s.Attrs&RapidBlinkAttr != 0 {
b = b.RapidBlink()
b = b.RapidBlink(true)
}
if s.Attrs&ReverseAttr != 0 {
b = b.Reverse()
b = b.Reverse(true)
}
if s.Attrs&ConcealAttr != 0 {
b = b.Conceal()
b = b.Conceal(true)
}
if s.Attrs&StrikethroughAttr != 0 {
b = b.Strikethrough()
b = b.Strikethrough(true)
}
}
if s.UlStyle != NoUnderline {
switch s.UlStyle {
case SingleUnderline:
b = b.Underline()
case DoubleUnderline:
b = b.DoubleUnderline()
case CurlyUnderline:
b = b.CurlyUnderline()
case DottedUnderline:
b = b.DottedUnderline()
case DashedUnderline:
b = b.DashedUnderline()
switch u := s.UlStyle; u {
case NoUnderline:
b = b.Underline(false)
default:
b = b.Underline(true)
b = b.UnderlineStyle(u)
}
}
if s.Fg != nil {
@ -268,64 +263,48 @@ func (s Style) DiffSequence(o Style) string {
isNormal bool
)
if s.Attrs != o.Attrs {
if s.Attrs != o.Attrs { //nolint:nestif
if s.Attrs&BoldAttr != o.Attrs&BoldAttr {
if s.Attrs&BoldAttr != 0 {
b = b.Bold()
} else if !isNormal {
isNormal = true
b = b.NormalIntensity()
b = b.Normal()
}
}
if s.Attrs&FaintAttr != o.Attrs&FaintAttr {
if s.Attrs&FaintAttr != 0 {
b = b.Faint()
} else if !isNormal {
b = b.NormalIntensity()
b = b.Normal()
}
}
if s.Attrs&ItalicAttr != o.Attrs&ItalicAttr {
if s.Attrs&ItalicAttr != 0 {
b = b.Italic()
} else {
b = b.NoItalic()
}
b = b.Italic(s.Attrs&ItalicAttr != 0)
}
if s.Attrs&SlowBlinkAttr != o.Attrs&SlowBlinkAttr {
if s.Attrs&SlowBlinkAttr != 0 {
b = b.SlowBlink()
b = b.Blink(true)
} else if !noBlink {
noBlink = true
b = b.NoBlink()
b = b.Blink(false)
}
}
if s.Attrs&RapidBlinkAttr != o.Attrs&RapidBlinkAttr {
if s.Attrs&RapidBlinkAttr != 0 {
b = b.RapidBlink()
b = b.RapidBlink(true)
} else if !noBlink {
b = b.NoBlink()
b = b.Blink(false)
}
}
if s.Attrs&ReverseAttr != o.Attrs&ReverseAttr {
if s.Attrs&ReverseAttr != 0 {
b = b.Reverse()
} else {
b = b.NoReverse()
}
b = b.Reverse(s.Attrs&ReverseAttr != 0)
}
if s.Attrs&ConcealAttr != o.Attrs&ConcealAttr {
if s.Attrs&ConcealAttr != 0 {
b = b.Conceal()
} else {
b = b.NoConceal()
}
b = b.Conceal(s.Attrs&ConcealAttr != 0)
}
if s.Attrs&StrikethroughAttr != o.Attrs&StrikethroughAttr {
if s.Attrs&StrikethroughAttr != 0 {
b = b.Strikethrough()
} else {
b = b.NoStrikethrough()
}
b = b.Strikethrough(s.Attrs&StrikethroughAttr != 0)
}
}

View File

@ -12,7 +12,7 @@ func Pos(x, y int) Position {
return image.Pt(x, y)
}
// Rectange represents a rectangle.
// Rectangle represents a rectangle.
type Rectangle = image.Rectangle
// Rect is a shorthand for Rectangle.

View File

@ -75,7 +75,7 @@ func (s *Screen) scrolln(n, top, bot, maxY int) (v bool) { //nolint:unparam
)
blank := s.clearBlank()
if n > 0 {
if n > 0 { //nolint:nestif
// Scroll up (forward)
v = s.scrollUp(n, top, bot, 0, maxY, blank)
if !v {
@ -99,7 +99,7 @@ func (s *Screen) scrolln(n, top, bot, maxY int) (v bool) { //nolint:unparam
s.move(0, bot-n+1)
s.clearToBottom(nil)
} else {
for i := 0; i < n; i++ {
for i := range n {
s.move(0, bot-i)
s.clearToEnd(nil, false)
}
@ -124,7 +124,7 @@ func (s *Screen) scrolln(n, top, bot, maxY int) (v bool) { //nolint:unparam
// Clear newly shifted-in lines.
if v &&
(nonDestScrollRegion || (memoryBelow && top == 0)) {
for i := 0; i < -n; i++ {
for i := range -n {
s.move(0, top+i)
s.clearToEnd(nil, false)
}
@ -133,7 +133,7 @@ func (s *Screen) scrolln(n, top, bot, maxY int) (v bool) { //nolint:unparam
}
if !v {
return
return v
}
s.scrollBuffer(s.curbuf, n, top, bot, blank)
@ -193,7 +193,7 @@ func (s *Screen) touchLine(width, height, y, n int, changed bool) {
// scrollUp scrolls the screen up by n lines.
func (s *Screen) scrollUp(n, top, bot, minY, maxY int, blank *Cell) bool {
if n == 1 && top == minY && bot == maxY {
if n == 1 && top == minY && bot == maxY { //nolint:nestif
s.move(0, bot)
s.updatePen(blank)
s.buf.WriteByte('\n')
@ -202,13 +202,14 @@ func (s *Screen) scrollUp(n, top, bot, minY, maxY int, blank *Cell) bool {
s.updatePen(blank)
s.buf.WriteString(ansi.DeleteLine(1))
} else if top == minY && bot == maxY {
if s.xtermLike {
supportsSU := s.caps.Contains(capSU)
if supportsSU {
s.move(0, bot)
} else {
s.move(0, top)
}
s.updatePen(blank)
if s.xtermLike {
if supportsSU {
s.buf.WriteString(ansi.ScrollUp(n))
} else {
s.buf.WriteString(strings.Repeat("\n", n))
@ -225,7 +226,7 @@ func (s *Screen) scrollUp(n, top, bot, minY, maxY int, blank *Cell) bool {
// scrollDown scrolls the screen down by n lines.
func (s *Screen) scrollDown(n, top, bot, minY, maxY int, blank *Cell) bool {
if n == 1 && top == minY && bot == maxY {
if n == 1 && top == minY && bot == maxY { //nolint:nestif
s.move(0, top)
s.updatePen(blank)
s.buf.WriteString(ansi.ReverseIndex)
@ -236,7 +237,7 @@ func (s *Screen) scrollDown(n, top, bot, minY, maxY int, blank *Cell) bool {
} else if top == minY && bot == maxY {
s.move(0, top)
s.updatePen(blank)
if s.xtermLike {
if s.caps.Contains(capSD) {
s.buf.WriteString(ansi.ScrollDown(n))
} else {
s.buf.WriteString(strings.Repeat(ansi.ReverseIndex, n))

View File

@ -15,7 +15,7 @@ func hash(l Line) (h uint64) {
}
h += (h << 5) + uint64(r)
}
return
return h
}
// hashmap represents a single [Line] hash.
@ -33,7 +33,7 @@ func (s *Screen) updateHashmap() {
height := s.newbuf.Height()
if len(s.oldhash) >= height && len(s.newhash) >= height {
// rehash changed lines
for i := 0; i < height; i++ {
for i := range height {
_, ok := s.touch[i]
if ok {
s.oldhash[i] = hash(s.curbuf.Line(i))
@ -48,14 +48,14 @@ func (s *Screen) updateHashmap() {
if len(s.newhash) != height {
s.newhash = make([]uint64, height)
}
for i := 0; i < height; i++ {
for i := range height {
s.oldhash[i] = hash(s.curbuf.Line(i))
s.newhash[i] = hash(s.newbuf.Line(i))
}
}
s.hashtab = make([]hashmap, height*2)
for i := 0; i < height; i++ {
for i := range height {
hashval := s.oldhash[i]
// Find matching hash or empty slot
@ -71,7 +71,7 @@ func (s *Screen) updateHashmap() {
s.hashtab[idx].oldcount++
s.hashtab[idx].oldindex = i
}
for i := 0; i < height; i++ {
for i := range height {
hashval := s.newhash[i]
// Find matching hash or empty slot
@ -130,7 +130,7 @@ func (s *Screen) updateHashmap() {
s.growHunks()
}
// scrollOldhash
// scrollOldhash.
func (s *Screen) scrollOldhash(n, top, bot int) {
if len(s.oldhash) == 0 {
return
@ -287,7 +287,7 @@ func (s *Screen) updateCost(from, to Line) (cost int) {
cost++
}
}
return
return cost
}
func (s *Screen) updateCostBlank(to Line) (cost int) {
@ -297,5 +297,5 @@ func (s *Screen) updateCostBlank(to Line) (cost int) {
cost++
}
}
return
return cost
}

View File

@ -4,7 +4,7 @@ import (
"github.com/charmbracelet/colorprofile"
)
// Convert converts a hyperlink to respect the given color profile.
// ConvertLink converts a hyperlink to respect the given color profile.
func ConvertLink(h Link, p colorprofile.Profile) Link {
if p == colorprofile.NoTTY {
return Link{}

92
vendor/github.com/charmbracelet/x/cellbuf/pen.go generated vendored Normal file
View File

@ -0,0 +1,92 @@
package cellbuf
import (
"io"
"github.com/charmbracelet/x/ansi"
)
// PenWriter is a writer that writes to a buffer and keeps track of the current
// pen style and link state for the purpose of wrapping with newlines.
type PenWriter struct {
w io.Writer
p *ansi.Parser
style Style
link Link
}
// NewPenWriter returns a new PenWriter.
func NewPenWriter(w io.Writer) *PenWriter {
pw := &PenWriter{w: w}
pw.p = ansi.GetParser()
handleCsi := func(cmd ansi.Cmd, params ansi.Params) {
if cmd == 'm' {
ReadStyle(params, &pw.style)
}
}
handleOsc := func(cmd int, data []byte) {
if cmd == 8 {
ReadLink(data, &pw.link)
}
}
pw.p.SetHandler(ansi.Handler{
HandleCsi: handleCsi,
HandleOsc: handleOsc,
})
return pw
}
// Style returns the current pen style.
func (w *PenWriter) Style() Style {
return w.style
}
// Link returns the current pen link.
func (w *PenWriter) Link() Link {
return w.link
}
// Write writes to the buffer.
func (w *PenWriter) Write(p []byte) (int, error) {
for i := range p {
b := p[i]
w.p.Advance(b)
if b == '\n' {
if !w.style.Empty() {
_, _ = w.w.Write([]byte(ansi.ResetStyle))
}
if !w.link.Empty() {
_, _ = w.w.Write([]byte(ansi.ResetHyperlink()))
}
}
_, _ = w.w.Write([]byte{b})
if b == '\n' {
if !w.link.Empty() {
_, _ = w.w.Write([]byte(ansi.SetHyperlink(w.link.URL, w.link.Params)))
}
if !w.style.Empty() {
_, _ = w.w.Write([]byte(w.style.Sequence()))
}
}
}
return len(p), nil
}
// Close closes the writer, resets the style and link if necessary, and releases
// its parser. Calling it is performance critical, but forgetting it does not
// cause safety issues or leaks.
func (w *PenWriter) Close() error {
if !w.style.Empty() {
_, _ = w.w.Write([]byte(ansi.ResetStyle))
}
if !w.link.Empty() {
_, _ = w.w.Write([]byte(ansi.ResetHyperlink()))
}
if w.p != nil {
ansi.PutParser(w.p)
w.p = nil
}
return nil
}

View File

@ -39,9 +39,9 @@ func relativeCursorMove(s *Screen, fx, fy, tx, ty int, overwrite, useTabs, useBa
var seq strings.Builder
width, height := s.newbuf.Width(), s.newbuf.Height()
if ty != fy {
if ty != fy { //nolint:nestif
var yseq string
if s.xtermLike && !s.opts.RelativeCursor {
if s.caps.Contains(capVPA) && !s.opts.RelativeCursor {
yseq = ansi.VerticalPositionAbsolute(ty + 1)
}
@ -54,9 +54,13 @@ func relativeCursorMove(s *Screen, fx, fy, tx, ty int, overwrite, useTabs, useBa
}
shouldScroll := !s.opts.AltScreen && fy+n >= s.scrollHeight
if lf := strings.Repeat("\n", n); shouldScroll || (fy+n < height && len(lf) < len(yseq)) {
//nolint:godox
// TODO: Ensure we're not unintentionally scrolling the screen down.
yseq = lf
s.scrollHeight = max(s.scrollHeight, fy+n)
if s.opts.MapNL {
fx = 0
}
}
} else if ty < fy {
n := fy - ty
@ -64,6 +68,7 @@ func relativeCursorMove(s *Screen, fx, fy, tx, ty int, overwrite, useTabs, useBa
yseq = cuu
}
if n == 1 && fy-1 > 0 {
//nolint:godox
// TODO: Ensure we're not unintentionally scrolling the screen up.
yseq = ansi.ReverseIndex
}
@ -72,9 +77,9 @@ func relativeCursorMove(s *Screen, fx, fy, tx, ty int, overwrite, useTabs, useBa
seq.WriteString(yseq)
}
if tx != fx {
if tx != fx { //nolint:nestif
var xseq string
if s.xtermLike && !s.opts.RelativeCursor {
if s.caps.Contains(capHPA) && !s.opts.RelativeCursor {
xseq = ansi.HorizontalPositionAbsolute(tx + 1)
}
@ -93,7 +98,8 @@ func relativeCursorMove(s *Screen, fx, fy, tx, ty int, overwrite, useTabs, useBa
if tabs > 0 {
cht := ansi.CursorHorizontalForwardTab(tabs)
tab := strings.Repeat("\t", tabs)
if false && s.xtermLike && len(cht) < len(tab) {
if false && s.caps.Contains(capCHT) && len(cht) < len(tab) {
//nolint:godox
// TODO: The linux console and some terminals such as
// Alacritty don't support [ansi.CHT]. Enable this when
// we have a way to detect this, or after 5 years when
@ -144,7 +150,7 @@ func relativeCursorMove(s *Screen, fx, fy, tx, ty int, overwrite, useTabs, useBa
}
} else if tx < fx {
n := fx - tx
if useTabs && s.xtermLike {
if useTabs && s.caps.Contains(capCBT) {
// VT100 does not support backward tabs [ansi.CBT].
col := fx
@ -190,7 +196,7 @@ func moveCursor(s *Screen, x, y int, overwrite bool) (seq string) {
// Method #0: Use [ansi.CUP] if the distance is long.
seq = ansi.CursorPosition(x+1, y+1)
if fx == -1 || fy == -1 || notLocal(s.newbuf.Width(), fx, fy, x, y) {
return
return seq
}
}
@ -234,7 +240,7 @@ func moveCursor(s *Screen, x, y int, overwrite bool) (seq string) {
}
}
return
return seq
}
// moveCursor moves the cursor to the specified position.
@ -242,10 +248,10 @@ func (s *Screen) moveCursor(x, y int, overwrite bool) {
if !s.opts.AltScreen && s.cur.X == -1 && s.cur.Y == -1 {
// First cursor movement in inline mode, move the cursor to the first
// column before moving to the target position.
s.buf.WriteByte('\r') //nolint:errcheck
s.buf.WriteByte('\r')
s.cur.X, s.cur.Y = 0, 0
}
s.buf.WriteString(moveCursor(s, x, y, overwrite)) //nolint:errcheck
s.buf.WriteString(moveCursor(s, x, y, overwrite))
s.cur.X, s.cur.Y = x, y
}
@ -274,10 +280,11 @@ func (s *Screen) move(x, y int) {
// Reset wrap around (phantom cursor) state
if s.atPhantom {
s.cur.X = 0
s.buf.WriteByte('\r') //nolint:errcheck
s.atPhantom = false // reset phantom cell state
s.buf.WriteByte('\r')
s.atPhantom = false // reset phantom cell state
}
//nolint:godox
// TODO: Investigate if we need to handle this case and/or if we need the
// following code.
//
@ -291,7 +298,7 @@ func (s *Screen) move(x, y int) {
//
// if l > 0 {
// s.cur.X = 0
// s.buf.WriteString("\r" + strings.Repeat("\n", l)) //nolint:errcheck
// s.buf.WriteString("\r" + strings.Repeat("\n", l))
// }
// }
@ -339,6 +346,10 @@ type ScreenOptions struct {
HardTabs bool
// Backspace is whether to use backspace characters to move the cursor.
Backspace bool
// MapNL whether we have ONLCR mapping enabled. When we set the terminal to
// raw mode, the ONLCR mode gets disabled. ONLCR maps any newline/linefeed
// (`\n`) character to carriage return + line feed (`\r\n`).
MapNL bool
}
// lineData represents the metadata for a line.
@ -365,13 +376,13 @@ type Screen struct {
opts ScreenOptions
mu sync.Mutex
method ansi.Method
scrollHeight int // keeps track of how many lines we've scrolled down (inline mode)
altScreenMode bool // whether alternate screen mode is enabled
cursorHidden bool // whether text cursor mode is enabled
clear bool // whether to force clear the screen
xtermLike bool // whether to use xterm-like optimizations, otherwise, it uses vt100 only
queuedText bool // whether we have queued non-zero width text queued up
atPhantom bool // whether the cursor is out of bounds and at a phantom cell
scrollHeight int // keeps track of how many lines we've scrolled down (inline mode)
altScreenMode bool // whether alternate screen mode is enabled
cursorHidden bool // whether text cursor mode is enabled
clear bool // whether to force clear the screen
caps capabilities // terminal control sequence capabilities
queuedText bool // whether we have queued non-zero width text queued up
atPhantom bool // whether the cursor is out of bounds and at a phantom cell
}
// SetMethod sets the method used to calculate the width of cells.
@ -491,36 +502,77 @@ func (s *Screen) FillRect(cell *Cell, r Rectangle) bool {
return true
}
// isXtermLike returns whether the terminal is xterm-like. This means that the
// capabilities represents a mask of supported ANSI escape sequences.
type capabilities uint
const (
// Vertical Position Absolute [ansi.VPA].
capVPA capabilities = 1 << iota
// Horizontal Position Absolute [ansi.HPA].
capHPA
// Cursor Horizontal Tab [ansi.CHT].
capCHT
// Cursor Backward Tab [ansi.CBT].
capCBT
// Repeat Previous Character [ansi.REP].
capREP
// Erase Character [ansi.ECH].
capECH
// Insert Character [ansi.ICH].
capICH
// Scroll Down [ansi.SD].
capSD
// Scroll Up [ansi.SU].
capSU
noCaps capabilities = 0
allCaps = capVPA | capHPA | capCHT | capCBT | capREP | capECH | capICH |
capSD | capSU
)
// Contains returns whether the capabilities contains the given capability.
func (v capabilities) Contains(c capabilities) bool {
return v&c == c
}
// xtermCaps returns whether the terminal is xterm-like. This means that the
// terminal supports ECMA-48 and ANSI X3.64 escape sequences.
// TODO: Should this be a lookup table into each $TERM terminfo database? Like
// we could keep a map of ANSI escape sequence to terminfo capability name and
// check if the database supports the escape sequence. Instead of keeping a
// list of terminal names here.
func isXtermLike(termtype string) (v bool) {
// xtermCaps returns a list of control sequence capabilities for the given
// terminal type. This only supports a subset of sequences that can
// be different among terminals.
// NOTE: A hybrid approach would be to support Terminfo databases for a full
// set of capabilities.
func xtermCaps(termtype string) (v capabilities) {
parts := strings.Split(termtype, "-")
if len(parts) == 0 {
return
return v
}
switch parts[0] {
case
"alacritty",
"contour",
"foot",
"ghostty",
"kitty",
"linux",
"rio",
"screen",
"st",
"tmux",
"wezterm",
"xterm":
v = true
v = allCaps
case "alacritty":
v = allCaps
v &^= capCHT // NOTE: alacritty added support for [ansi.CHT] in 2024-12-28 #62d5b13.
case "screen":
// See https://www.gnu.org/software/screen/manual/screen.html#Control-Sequences-1
v = allCaps
v &^= capREP
case "linux":
// See https://man7.org/linux/man-pages/man4/console_codes.4.html
v = capVPA | capHPA | capECH | capICH
}
return
return v
}
// NewScreen creates a new Screen.
@ -548,14 +600,14 @@ func NewScreen(w io.Writer, width, height int, opts *ScreenOptions) (s *Screen)
}
s.buf = new(bytes.Buffer)
s.xtermLike = isXtermLike(s.opts.Term)
s.caps = xtermCaps(s.opts.Term)
s.curbuf = NewBuffer(width, height)
s.newbuf = NewBuffer(width, height)
s.cur = Cursor{Position: Pos(-1, -1)} // start at -1 to force a move
s.saved = s.cur
s.reset()
return
return s
}
// Width returns the width of the screen.
@ -595,7 +647,7 @@ func (s *Screen) putCell(cell *Cell) {
// wrapCursor wraps the cursor to the next line.
//
//nolint:unused
func (s *Screen) wrapCursor() {
const autoRightMargin = true
if autoRightMargin {
@ -628,9 +680,9 @@ func (s *Screen) putAttrCell(cell *Cell) {
}
s.updatePen(cell)
s.buf.WriteRune(cell.Rune) //nolint:errcheck
s.buf.WriteRune(cell.Rune)
for _, c := range cell.Comb {
s.buf.WriteRune(c) //nolint:errcheck
s.buf.WriteRune(c)
}
s.cur.X += cell.Width
@ -649,12 +701,12 @@ func (s *Screen) putCellLR(cell *Cell) {
// Optimize for the lower right corner cell.
curX := s.cur.X
if cell == nil || !cell.Empty() {
s.buf.WriteString(ansi.ResetAutoWrapMode) //nolint:errcheck
s.buf.WriteString(ansi.ResetModeAutoWrap)
s.putAttrCell(cell)
// Writing to lower-right corner cell should not wrap.
s.atPhantom = false
s.cur.X = curX
s.buf.WriteString(ansi.SetAutoWrapMode) //nolint:errcheck
s.buf.WriteString(ansi.SetModeAutoWrap)
}
}
@ -675,11 +727,11 @@ func (s *Screen) updatePen(cell *Cell) {
if cell.Style.Empty() && len(seq) > len(ansi.ResetStyle) {
seq = ansi.ResetStyle
}
s.buf.WriteString(seq) //nolint:errcheck
s.buf.WriteString(seq)
s.cur.Style = cell.Style
}
if !cell.Link.Equal(&s.cur.Link) {
s.buf.WriteString(ansi.SetHyperlink(cell.Link.URL, cell.Link.Params)) //nolint:errcheck
s.buf.WriteString(ansi.SetHyperlink(cell.Link.URL, cell.Link.Params))
s.cur.Link = cell.Link
}
}
@ -712,9 +764,9 @@ func (s *Screen) emitRange(line Line, n int) (eoi bool) {
ech := ansi.EraseCharacter(count)
cup := ansi.CursorPosition(s.cur.X+count, s.cur.Y)
rep := ansi.RepeatPreviousCharacter(count)
if s.xtermLike && count > len(ech)+len(cup) && cell0 != nil && cell0.Clear() {
if s.caps.Contains(capECH) && count > len(ech)+len(cup) && cell0 != nil && cell0.Clear() { //nolint:nestif
s.updatePen(cell0)
s.buf.WriteString(ech) //nolint:errcheck
s.buf.WriteString(ech)
// If this is the last cell, we don't need to move the cursor.
if count < n {
@ -722,7 +774,7 @@ func (s *Screen) emitRange(line Line, n int) (eoi bool) {
} else {
return true // cursor in the middle
}
} else if s.xtermLike && count > len(rep) &&
} else if s.caps.Contains(capREP) && count > len(rep) &&
(cell0 == nil || (len(cell0.Comb) == 0 && cell0.Rune < 256)) {
// We only support ASCII characters. Most terminals will handle
// non-ASCII characters correctly, but some might not, ahem xterm.
@ -740,13 +792,13 @@ func (s *Screen) emitRange(line Line, n int) (eoi bool) {
s.putCell(cell0)
repCount-- // cell0 is a single width cell ASCII character
s.buf.WriteString(ansi.RepeatPreviousCharacter(repCount)) //nolint:errcheck
s.buf.WriteString(ansi.RepeatPreviousCharacter(repCount))
s.cur.X += repCount
if wrapPossible {
s.putCell(cell0)
}
} else {
for i := 0; i < count; i++ {
for i := range count {
s.putCell(line.At(i))
}
}
@ -755,7 +807,7 @@ func (s *Screen) emitRange(line Line, n int) (eoi bool) {
n -= count
}
return
return eoi
}
// putRange puts a range of cells from the old line to the new line.
@ -765,7 +817,7 @@ func (s *Screen) putRange(oldLine, newLine Line, y, start, end int) (eoi bool) {
inline := min(len(ansi.CursorPosition(start+1, y+1)),
min(len(ansi.HorizontalPositionAbsolute(start+1)),
len(ansi.CursorForward(start+1))))
if (end - start + 1) > inline {
if (end - start + 1) > inline { //nolint:nestif
var j, same int
for j, same = start, 0; j <= end; j++ {
oldCell, newCell := oldLine.At(j), newLine.At(j)
@ -817,9 +869,9 @@ func (s *Screen) clearToEnd(blank *Cell, force bool) { //nolint:unparam
s.updatePen(blank)
count := s.newbuf.Width() - s.cur.X
if s.el0Cost() <= count {
s.buf.WriteString(ansi.EraseLineRight) //nolint:errcheck
s.buf.WriteString(ansi.EraseLineRight)
} else {
for i := 0; i < count; i++ {
for range count {
s.putCell(blank)
}
}
@ -839,12 +891,13 @@ func (s *Screen) clearBlank() *Cell {
// insertCells inserts the count cells pointed by the given line at the current
// cursor position.
func (s *Screen) insertCells(line Line, count int) {
if s.xtermLike {
supportsICH := s.caps.Contains(capICH)
if supportsICH {
// Use [ansi.ICH] as an optimization.
s.buf.WriteString(ansi.InsertCharacter(count)) //nolint:errcheck
s.buf.WriteString(ansi.InsertCharacter(count))
} else {
// Otherwise, use [ansi.IRM] mode.
s.buf.WriteString(ansi.SetInsertReplaceMode) //nolint:errcheck
s.buf.WriteString(ansi.SetModeInsertReplace)
}
for i := 0; count > 0; i++ {
@ -852,8 +905,8 @@ func (s *Screen) insertCells(line Line, count int) {
count--
}
if !s.xtermLike {
s.buf.WriteString(ansi.ResetInsertReplaceMode) //nolint:errcheck
if !supportsICH {
s.buf.WriteString(ansi.ResetModeInsertReplace)
}
}
@ -862,7 +915,7 @@ func (s *Screen) insertCells(line Line, count int) {
// [ansi.EL] 0 i.e. [ansi.EraseLineRight] to clear
// trailing spaces.
func (s *Screen) el0Cost() int {
if s.xtermLike {
if s.caps != noCaps {
return 0
}
return len(ansi.EraseLineRight)
@ -878,7 +931,7 @@ func (s *Screen) transformLine(y int) {
// Find the first changed cell in the line
var lineChanged bool
for i := 0; i < s.newbuf.Width(); i++ {
for i := range s.newbuf.Width() {
if !cellEqual(newLine.At(i), oldLine.At(i)) {
lineChanged = true
break
@ -886,7 +939,7 @@ func (s *Screen) transformLine(y int) {
}
const ceolStandoutGlitch = false
if ceolStandoutGlitch && lineChanged {
if ceolStandoutGlitch && lineChanged { //nolint:nestif
s.move(0, y)
s.clearToEnd(nil, false)
s.putRange(oldLine, newLine, y, 0, s.newbuf.Width()-1)
@ -897,12 +950,12 @@ func (s *Screen) transformLine(y int) {
// [ansi.EraseLineLeft].
if blank == nil || blank.Clear() {
var oFirstCell, nFirstCell int
for oFirstCell = 0; oFirstCell < s.curbuf.Width(); oFirstCell++ {
for oFirstCell = range s.curbuf.Width() {
if !cellEqual(oldLine.At(oFirstCell), blank) {
break
}
}
for nFirstCell = 0; nFirstCell < s.newbuf.Width(); nFirstCell++ {
for nFirstCell = range s.newbuf.Width() {
if !cellEqual(newLine.At(nFirstCell), blank) {
break
}
@ -925,11 +978,11 @@ func (s *Screen) transformLine(y int) {
if nFirstCell >= s.newbuf.Width() {
s.move(0, y)
s.updatePen(blank)
s.buf.WriteString(ansi.EraseLineRight) //nolint:errcheck
s.buf.WriteString(ansi.EraseLineRight)
} else {
s.move(nFirstCell-1, y)
s.updatePen(blank)
s.buf.WriteString(ansi.EraseLineLeft) //nolint:errcheck
s.buf.WriteString(ansi.EraseLineLeft)
}
for firstCell < nFirstCell {
@ -1045,7 +1098,7 @@ func (s *Screen) transformLine(y int) {
s.move(n+1, y)
ichCost := 3 + nLastCell - oLastCell
if s.xtermLike && (nLastCell < nLastNonBlank || ichCost > (m-n)) {
if s.caps.Contains(capICH) && (nLastCell < nLastNonBlank || ichCost > (m-n)) {
s.putRange(oldLine, newLine, y, n+1, m)
} else {
s.insertCells(newLine[n+1:], nLastCell-oLastCell)
@ -1079,7 +1132,7 @@ func (s *Screen) transformLine(y int) {
func (s *Screen) deleteCells(count int) {
// [ansi.DCH] will shift in cells from the right margin so we need to
// ensure that they are the right style.
s.buf.WriteString(ansi.DeleteCharacter(count)) //nolint:errcheck
s.buf.WriteString(ansi.DeleteCharacter(count))
}
// clearToBottom clears the screen from the current cursor position to the end
@ -1091,7 +1144,7 @@ func (s *Screen) clearToBottom(blank *Cell) {
}
s.updatePen(blank)
s.buf.WriteString(ansi.EraseScreenBelow) //nolint:errcheck
s.buf.WriteString(ansi.EraseScreenBelow)
// Clear the rest of the current line
s.curbuf.ClearRect(Rect(col, row, s.curbuf.Width()-col, 1))
// Clear everything below the current line
@ -1104,7 +1157,7 @@ func (s *Screen) clearToBottom(blank *Cell) {
// It returns the top line.
func (s *Screen) clearBottom(total int) (top int) {
if total <= 0 {
return
return top
}
top = total
@ -1112,7 +1165,7 @@ func (s *Screen) clearBottom(total int) (top int) {
blank := s.clearBlank()
canClearWithBlank := blank == nil || blank.Clear()
if canClearWithBlank {
if canClearWithBlank { //nolint:nestif
var row int
for row = total - 1; row >= 0; row-- {
oldLine := s.curbuf.Line(row)
@ -1147,14 +1200,14 @@ func (s *Screen) clearBottom(total int) (top int) {
}
}
return
return top
}
// clearScreen clears the screen and put cursor at home.
func (s *Screen) clearScreen(blank *Cell) {
s.updatePen(blank)
s.buf.WriteString(ansi.CursorHomePosition) //nolint:errcheck
s.buf.WriteString(ansi.EraseEntireScreen) //nolint:errcheck
s.buf.WriteString(ansi.CursorHomePosition)
s.buf.WriteString(ansi.EraseEntireScreen)
s.cur.X, s.cur.Y = 0, 0
s.curbuf.Fill(blank)
}
@ -1179,7 +1232,7 @@ func (s *Screen) clearUpdate() {
s.clearBelow(blank, 0)
}
nonEmpty = s.clearBottom(nonEmpty)
for i := 0; i < nonEmpty; i++ {
for i := range nonEmpty {
s.transformLine(i)
}
}
@ -1194,13 +1247,13 @@ func (s *Screen) Flush() (err error) {
func (s *Screen) flush() (err error) {
// Write the buffer
if s.buf.Len() > 0 {
_, err = s.w.Write(s.buf.Bytes()) //nolint:errcheck
_, err = s.w.Write(s.buf.Bytes())
if err == nil {
s.buf.Reset()
}
}
return
return err //nolint:wrapcheck
}
// Render renders changes of the screen to the internal buffer. Call
@ -1221,6 +1274,7 @@ func (s *Screen) render() {
return
}
//nolint:godox
// TODO: Investigate whether this is necessary. Theoretically, terminals
// can add/remove tab stops and we should be able to handle that. We could
// use [ansi.DECTABSR] to read the tab stops, but that's not implemented in
@ -1235,9 +1289,9 @@ func (s *Screen) render() {
// Do we need alt-screen mode?
if s.opts.AltScreen != s.altScreenMode {
if s.opts.AltScreen {
s.buf.WriteString(ansi.SetAltScreenSaveCursorMode)
s.buf.WriteString(ansi.SetModeAltScreenSaveCursor)
} else {
s.buf.WriteString(ansi.ResetAltScreenSaveCursorMode)
s.buf.WriteString(ansi.ResetModeAltScreenSaveCursor)
}
s.altScreenMode = s.opts.AltScreen
}
@ -1252,7 +1306,9 @@ func (s *Screen) render() {
// Do we have queued strings to write above the screen?
if len(s.queueAbove) > 0 {
//nolint:godox
// TODO: Use scrolling region if available.
//nolint:godox
// TODO: Use [Screen.Write] [io.Writer] interface.
// We need to scroll the screen up by the number of lines in the queue.
@ -1290,12 +1346,13 @@ func (s *Screen) render() {
s.clearBelow(nil, s.newbuf.Height()-1)
}
if s.clear {
if s.clear { //nolint:nestif
s.clearUpdate()
s.clear = false
} else if len(s.touch) > 0 {
if s.opts.AltScreen {
// Optimize scrolling for the alternate screen buffer.
//nolint:godox
// TODO: Should we optimize for inline mode as well? If so, we need
// to know the actual cursor position to use [ansi.DECSTBM].
s.scrollOptimize()
@ -1311,7 +1368,7 @@ func (s *Screen) render() {
}
nonEmpty = s.clearBottom(nonEmpty)
for i = 0; i < nonEmpty; i++ {
for i = range nonEmpty {
_, ok := s.touch[i]
if ok {
s.transformLine(i)
@ -1359,7 +1416,7 @@ func (s *Screen) Close() (err error) {
s.move(0, s.newbuf.Height()-1)
if s.altScreenMode {
s.buf.WriteString(ansi.ResetAltScreenSaveCursorMode)
s.buf.WriteString(ansi.ResetModeAltScreenSaveCursor)
s.altScreenMode = false
}
@ -1371,11 +1428,11 @@ func (s *Screen) Close() (err error) {
// Write the buffer
err = s.flush()
if err != nil {
return
return err
}
s.reset()
return
return err
}
// reset resets the screen to its initial state.
@ -1420,9 +1477,9 @@ func (s *Screen) Resize(width, height int) bool {
}
if height > oldh {
s.ClearRect(Rect(0, max(oldh-1, 0), width, height-oldh))
s.ClearRect(Rect(0, max(oldh, 0), width, height-oldh))
} else if height < oldh {
s.ClearRect(Rect(0, max(height-1, 0), width, oldh-height))
s.ClearRect(Rect(0, max(height, 0), width, oldh-height))
}
s.mu.Lock()

View File

@ -4,9 +4,9 @@ import (
"github.com/charmbracelet/colorprofile"
)
// Convert converts a style to respect the given color profile.
// ConvertStyle converts a style to respect the given color profile.
func ConvertStyle(s Style, p colorprofile.Profile) Style {
switch p {
switch p { //nolint:exhaustive
case colorprofile.TrueColor:
return s
case colorprofile.Ascii:

View File

@ -9,20 +9,6 @@ func Height(s string) int {
return strings.Count(s, "\n") + 1
}
func min(a, b int) int { //nolint:predeclared
if a > b {
return b
}
return a
}
func max(a, b int) int { //nolint:predeclared
if a > b {
return a
}
return b
}
func clamp(v, low, high int) int {
if high < low {
low, high = high, low

View File

@ -2,6 +2,7 @@ package cellbuf
import (
"bytes"
"slices"
"unicode"
"unicode/utf8"
@ -20,6 +21,16 @@ const nbsp = '\u00a0'
//
// Note: breakpoints must be a string of 1-cell wide rune characters.
func Wrap(s string, limit int, breakpoints string) string {
//nolint:godox
// TODO: Use [PenWriter] once we get
// https://github.com/charmbracelet/lipgloss/pull/489 out the door and
// released.
// The problem is that [ansi.Wrap] doesn't keep track of style and link
// state, so combining both breaks styled space cells. To fix this, we use
// non-breaking space cells for padding and styled blank cells. And since
// both wrapping methods respect non-breaking spaces, we can use them to
// preserve styled spaces in the output.
if len(s) == 0 {
return ""
}
@ -90,7 +101,7 @@ func Wrap(s string, limit int, breakpoints string) string {
seq, width, n, newState := ansi.DecodeSequence(s, state, p)
switch width {
case 0:
if ansi.Equal(seq, "\t") {
if ansi.Equal(seq, "\t") { //nolint:nestif
addWord()
space.WriteString(seq)
break
@ -176,10 +187,5 @@ func Wrap(s string, limit int, breakpoints string) string {
}
func runeContainsAny[T string | []rune](r rune, s T) bool {
for _, c := range []rune(s) {
if c == r {
return true
}
}
return false
return slices.Contains([]rune(s), r)
}

View File

@ -25,7 +25,7 @@ type CellBuffer interface {
func FillRect(s CellBuffer, c *Cell, rect Rectangle) {
for y := rect.Min.Y; y < rect.Max.Y; y++ {
for x := rect.Min.X; x < rect.Max.X; x++ {
s.SetCell(x, y, c) //nolint:errcheck
s.SetCell(x, y, c)
}
}
}
@ -68,7 +68,7 @@ func SetContent(s CellBuffer, str string) {
func Render(d CellBuffer) string {
var buf bytes.Buffer
height := d.Bounds().Dy()
for y := 0; y < height; y++ {
for y := range height {
_, line := RenderLine(d, y)
buf.WriteString(line)
if y < height-1 {
@ -98,32 +98,32 @@ func RenderLine(d CellBuffer, n int) (w int, line string) {
pendingLine = ""
}
for x := 0; x < d.Bounds().Dx(); x++ {
if cell := d.Cell(x, n); cell != nil && cell.Width > 0 {
for x := range d.Bounds().Dx() {
if cell := d.Cell(x, n); cell != nil && cell.Width > 0 { //nolint:nestif
// Convert the cell's style and link to the given color profile.
cellStyle := cell.Style
cellLink := cell.Link
if cellStyle.Empty() && !pen.Empty() {
writePending()
buf.WriteString(ansi.ResetStyle) //nolint:errcheck
buf.WriteString(ansi.ResetStyle)
pen.Reset()
}
if !cellStyle.Equal(&pen) {
writePending()
seq := cellStyle.DiffSequence(pen)
buf.WriteString(seq) // nolint:errcheck
buf.WriteString(seq)
pen = cellStyle
}
// Write the URL escape sequence
if cellLink != link && link.URL != "" {
writePending()
buf.WriteString(ansi.ResetHyperlink()) //nolint:errcheck
buf.WriteString(ansi.ResetHyperlink())
link.Reset()
}
if cellLink != link {
writePending()
buf.WriteString(ansi.SetHyperlink(cellLink.URL, cellLink.Params)) //nolint:errcheck
buf.WriteString(ansi.SetHyperlink(cellLink.URL, cellLink.Params))
link = cellLink
}
@ -140,10 +140,10 @@ func RenderLine(d CellBuffer, n int) (w int, line string) {
}
}
if link.URL != "" {
buf.WriteString(ansi.ResetHyperlink()) //nolint:errcheck
buf.WriteString(ansi.ResetHyperlink())
}
if !pen.Empty() {
buf.WriteString(ansi.ResetStyle) //nolint:errcheck
buf.WriteString(ansi.ResetStyle)
}
return w, strings.TrimRight(buf.String(), " ") // Trim trailing spaces
}
@ -201,7 +201,7 @@ func (s *ScreenWriter) SetContentRect(str string, rect Rectangle) {
// string to the width of the screen if it exceeds the width of the screen.
// This will recognize ANSI [ansi.SGR] style and [ansi.SetHyperlink] escape
// sequences.
func (s *ScreenWriter) Print(str string, v ...interface{}) {
func (s *ScreenWriter) Print(str string, v ...any) {
if len(v) > 0 {
str = fmt.Sprintf(str, v...)
}
@ -214,7 +214,7 @@ func (s *ScreenWriter) Print(str string, v ...interface{}) {
// the width of the screen if it exceeds the width of the screen.
// This will recognize ANSI [ansi.SGR] style and [ansi.SetHyperlink] escape
// sequences.
func (s *ScreenWriter) PrintAt(x, y int, str string, v ...interface{}) {
func (s *ScreenWriter) PrintAt(x, y int, str string, v ...any) {
if len(v) > 0 {
str = fmt.Sprintf(str, v...)
}
@ -299,7 +299,7 @@ func printString[T []byte | string](
// Print the cell to the screen
cell.Style = style
cell.Link = link
s.SetCell(x, y, &cell) //nolint:errcheck
s.SetCell(x, y, &cell)
x += width
}
}
@ -309,6 +309,7 @@ func printString[T []byte | string](
cell.Reset()
default:
// Valid sequences always have a non-zero Cmd.
//nolint:godox
// TODO: Handle cursor movement and other sequences
switch {
case ansi.HasCsiPrefix(seq) && p.Command() == 'm':
@ -333,7 +334,7 @@ func printString[T []byte | string](
// Make sure to set the last cell if it's not empty.
if !cell.Empty() {
s.SetCell(x, y, &cell) //nolint:errcheck
s.SetCell(x, y, &cell)
cell.Reset()
}
}

View File

@ -1,3 +1,5 @@
// Package term provides a platform-independent interfaces for interacting with
// Terminal and TTY devices.
package term
// State contains platform-specific state of a terminal.

118
vendor/github.com/charmbracelet/x/term/term_plan9.go generated vendored Normal file
View File

@ -0,0 +1,118 @@
package term
import (
"fmt"
"os"
"path/filepath"
"strings"
)
type state struct {
termName string
raw bool
ctl *os.File
}
// termName returns the name of the terminal or os.ErrNotExist if there is no terminal.
func termName(fd uintptr) (string, error) {
ctl, err := os.ReadFile(filepath.Join("/fd", fmt.Sprintf("%dctl", fd)))
if err != nil {
return "", err
}
f := strings.Fields(string(ctl))
if len(f) == 0 {
return "", os.ErrNotExist
}
return f[len(f)-1], nil
}
func isTerminal(fd uintptr) bool {
ctl, err := os.ReadFile(filepath.Join("/fd", fmt.Sprintf("%dctl", fd)))
if err != nil {
return false
}
if strings.Contains(string(ctl), "/dev/cons") {
return true
}
return false
}
func makeRaw(fd uintptr) (*State, error) {
t, err := termName(fd)
if err != nil {
return nil, err
}
ctl, err := os.OpenFile(t, os.O_RDWR, 0)
if err != nil {
return nil, err
}
if _, err := ctl.Write([]byte("rawon")); err != nil {
return nil, err
}
return &State{state: state{termName: t, raw: true, ctl: ctl}}, nil
}
func getState(fd uintptr) (*State, error) {
t, err := termName(fd)
if err != nil {
return nil, err
}
ctl, err := os.OpenFile(t, os.O_RDWR, 0)
if err != nil {
return nil, err
}
return &State{state: state{termName: t, raw: false, ctl: ctl}}, nil
}
func restore(_ uintptr, state *State) error {
if _, err := state.ctl.Write([]byte("rawoff")); err != nil {
return err
}
return nil
}
// getSize returns the size. This will only work if you are running
// under a window manager in Plan 9. Else, the only option
// is to return a reasonable default.
func getSize(fd uintptr) (int, int, error) {
w, h := 80, 40
b, err := os.ReadFile("/dev/wctl")
if err != nil {
return w, h, err
}
f := strings.Fields(string(b))
if len(f) != 4 {
return w, h, fmt.Errorf("%q only has %d of 4 needed fields:%w", f, len(f), os.ErrInvalid)
}
// The contents of wctl, as defined in the driver, are
// 4 12-char fields: upper left x, y; and lower-right x, y
var ulx, uly, lrx, lry int
if n, err := fmt.Sscanf(string(b[:48]), "%d%d%d%d", &ulx, &uly, &lrx, &lry); n != 4 || err != nil {
return w, h, fmt.Errorf("scanning %q:%d of 4 items scanned:%w", string(b[:48]), n, err)
}
w, h = lrx-lrx, lry-uly
return w, h, nil
}
func setState(_ uintptr, state *State) error {
raw := "rawoff"
if state.raw {
raw = "rawon"
}
if _, err := state.ctl.Write([]byte(raw)); err != nil {
return err
}
return nil
}
func readPassword(fd uintptr) ([]byte, error) {
f := os.NewFile(fd, "cons")
var b [128]byte
n, err := f.Read(b[:])
if err != nil {
return nil, err
}
return b[:n], nil
}

View File

@ -19,7 +19,7 @@ func isTerminal(fd uintptr) bool {
func makeRaw(fd uintptr) (*State, error) {
termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios)
if err != nil {
return nil, err
return nil, err //nolint:wrapcheck
}
oldState := State{state{Termios: *termios}}
@ -34,7 +34,7 @@ func makeRaw(fd uintptr) (*State, error) {
termios.Cc[unix.VMIN] = 1
termios.Cc[unix.VTIME] = 0
if err := unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios); err != nil {
return nil, err
return nil, err //nolint:wrapcheck
}
return &oldState, nil
@ -45,26 +45,26 @@ func setState(fd uintptr, state *State) error {
if state != nil {
termios = &state.Termios
}
return unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios)
return unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios) //nolint:wrapcheck
}
func getState(fd uintptr) (*State, error) {
termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios)
if err != nil {
return nil, err
return nil, err //nolint:wrapcheck
}
return &State{state{Termios: *termios}}, nil
}
func restore(fd uintptr, state *State) error {
return unix.IoctlSetTermios(int(fd), ioctlWriteTermios, &state.Termios)
return unix.IoctlSetTermios(int(fd), ioctlWriteTermios, &state.Termios) //nolint:wrapcheck
}
func getSize(fd uintptr) (width, height int, err error) {
ws, err := unix.IoctlGetWinsize(int(fd), unix.TIOCGWINSZ)
if err != nil {
return 0, 0, err
return 0, 0, err //nolint:wrapcheck
}
return int(ws.Col), int(ws.Row), nil
}
@ -73,13 +73,13 @@ func getSize(fd uintptr) (width, height int, err error) {
type passwordReader int
func (r passwordReader) Read(buf []byte) (int, error) {
return unix.Read(int(r), buf)
return unix.Read(int(r), buf) //nolint:wrapcheck
}
func readPassword(fd uintptr) ([]byte, error) {
termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios)
if err != nil {
return nil, err
return nil, err //nolint:wrapcheck
}
newState := *termios
@ -87,10 +87,10 @@ func readPassword(fd uintptr) ([]byte, error) {
newState.Lflag |= unix.ICANON | unix.ISIG
newState.Iflag |= unix.ICRNL
if err := unix.IoctlSetTermios(int(fd), ioctlWriteTermios, &newState); err != nil {
return nil, err
return nil, err //nolint:wrapcheck
}
defer unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios)
defer unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios) //nolint:errcheck
return readPasswordLine(passwordReader(fd))
}

View File

@ -24,7 +24,7 @@ func makeRaw(fd uintptr) (*State, error) {
if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil {
return nil, err
}
raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT)
raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT)
raw |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT
if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil {
return nil, err

View File

@ -41,7 +41,7 @@ func readPasswordLine(reader io.Reader) ([]byte, error) {
if err == io.EOF && len(ret) > 0 {
return ret, nil
}
return ret, err
return ret, err //nolint:wrapcheck
}
}
}

View File

@ -0,0 +1 @@
.DS_Store

37
vendor/github.com/clipperhouse/displaywidth/AGENTS.md generated vendored Normal file
View File

@ -0,0 +1,37 @@
The goals and overview of this package can be found in the README.md file,
start by reading that.
The goal of this package is to determine the display (column) width of a
string, UTF-8 bytes, or runes, as would happen in a monospace font, especially
in a terminal.
When troubleshooting, write Go unit tests instead of executing debug scripts.
The tests can return whatever logs or output you need. If those tests are
only for temporary troubleshooting, clean up the tests after the debugging is
done.
(Separate executable debugging scripts are messy, tend to have conflicting
dependencies and are hard to cleanup.)
If you make changes to the trie generation in internal/gen, it can be invoked
by running `go generate` from the top package directory.
## Pull Requests and branches
For PRs (pull requests), you can use the gh CLI tool to retrieve details,
or post comments. Then, compare the current branch with main. Reviewing a PR
and reviewing a branch are about the same, but the PR may add context.
Look for bugs. Think like GitHub Copilot or Cursor BugBot.
Offer to post a brief summary of the review to the PR, via the gh CLI tool.
## Comparisons to go-runewidth
We originally attempted to make this package compatible with go-runewidth.
However, we found that there were too many differences in the handling of
certain characters and properties.
We believe, preliminarily, that our choices are more correct and complete,
by using more complete categories such as Unicode Cf (format) for zero-width
and Mn (Nonspacing_Mark) for combining marks.

View File

@ -0,0 +1,49 @@
# Changelog
## [0.5.0]
[Compare](https://github.com/clipperhouse/displaywidth/compare/v0.4.1...v0.5.0)
### Added
- Unicode 16 support
- Improved emoji presentation handling per Unicode TR51
### Changed
- Corrected VS15 (U+FE0E) handling: now preserves base character width (no-op) per Unicode TR51
- Performance optimizations: reduced property lookups
### Fixed
- VS15 variation selector now correctly preserves base character width instead of forcing width 1
## [0.4.1]
[Compare](https://github.com/clipperhouse/displaywidth/compare/v0.4.0...v0.4.1)
### Changed
- Updated uax29 dependency
- Improved flag handling
## [0.4.0]
[Compare](https://github.com/clipperhouse/displaywidth/compare/v0.3.1...v0.4.0)
### Added
- Support for variation selectors (VS15, VS16) and regional indicator pairs (flags)
## [0.3.1]
[Compare](https://github.com/clipperhouse/displaywidth/compare/v0.3.0...v0.3.1)
### Added
- Fuzz testing support
### Changed
- Updated stringish dependency
## [0.3.0]
[Compare](https://github.com/clipperhouse/displaywidth/compare/v0.2.0...v0.3.0)
### Changed
- Dropped compatibility with go-runewidth
- Trie implementation cleanup

View File

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

123
vendor/github.com/clipperhouse/displaywidth/README.md generated vendored Normal file
View File

@ -0,0 +1,123 @@
# displaywidth
A high-performance Go package for measuring the monospace display width of strings, UTF-8 bytes, and runes.
[![Documentation](https://pkg.go.dev/badge/github.com/clipperhouse/displaywidth.svg)](https://pkg.go.dev/github.com/clipperhouse/displaywidth)
[![Test](https://github.com/clipperhouse/displaywidth/actions/workflows/gotest.yml/badge.svg)](https://github.com/clipperhouse/displaywidth/actions/workflows/gotest.yml)
[![Fuzz](https://github.com/clipperhouse/displaywidth/actions/workflows/gofuzz.yml/badge.svg)](https://github.com/clipperhouse/displaywidth/actions/workflows/gofuzz.yml)
## Install
```bash
go get github.com/clipperhouse/displaywidth
```
## Usage
```go
package main
import (
"fmt"
"github.com/clipperhouse/displaywidth"
)
func main() {
width := displaywidth.String("Hello, 世界!")
fmt.Println(width)
width = displaywidth.Bytes([]byte("🌍"))
fmt.Println(width)
width = displaywidth.Rune('🌍')
fmt.Println(width)
}
```
For most purposes, you should use the `String` or `Bytes` methods.
### Options
You can specify East Asian Width settings. When false (default),
[East Asian Ambiguous characters](https://www.unicode.org/reports/tr11/#Ambiguous)
are treated as width 1. When true, East Asian Ambiguous characters are treated
as width 2.
```go
myOptions := displaywidth.Options{
EastAsianWidth: true,
}
width := myOptions.String("Hello, 世界!")
fmt.Println(width)
```
## Technical details
This package implements the Unicode East Asian Width standard
([UAX #11](https://www.unicode.org/reports/tr11/)), and handles
[version selectors](https://en.wikipedia.org/wiki/Variation_Selectors_(Unicode_block)),
and [regional indicator pairs](https://en.wikipedia.org/wiki/Regional_indicator_symbol)
(flags). We implement [Unicode TR51](https://unicode.org/reports/tr51/).
`clipperhouse/displaywidth`, `mattn/go-runewidth`, and `rivo/uniseg` will
give the same outputs for most real-world text. See extensive details in the
[compatibility analysis](comparison/COMPATIBILITY_ANALYSIS.md).
If you wish to investigate the core logic, see the `lookupProperties` and `width`
functions in [width.go](width.go#L135). The essential trie generation logic is in
`buildPropertyBitmap` in [unicode.go](internal/gen/unicode.go#L317).
I (@clipperhouse) am keeping an eye on [emerging standards and test suites](https://www.jeffquast.com/post/state-of-terminal-emulation-2025/).
## Prior Art
[mattn/go-runewidth](https://github.com/mattn/go-runewidth)
[rivo/uniseg](https://github.com/rivo/uniseg)
[x/text/width](https://pkg.go.dev/golang.org/x/text/width)
[x/text/internal/triegen](https://pkg.go.dev/golang.org/x/text/internal/triegen)
## Benchmarks
```bash
cd comparison
go test -bench=. -benchmem
```
```
goos: darwin
goarch: arm64
pkg: github.com/clipperhouse/displaywidth/comparison
cpu: Apple M2
BenchmarkString_Mixed/clipperhouse/displaywidth-8 10929 ns/op 154.36 MB/s 0 B/op 0 allocs/op
BenchmarkString_Mixed/mattn/go-runewidth-8 14540 ns/op 116.02 MB/s 0 B/op 0 allocs/op
BenchmarkString_Mixed/rivo/uniseg-8 19751 ns/op 85.41 MB/s 0 B/op 0 allocs/op
BenchmarkString_EastAsian/clipperhouse/displaywidth-8 10885 ns/op 154.98 MB/s 0 B/op 0 allocs/op
BenchmarkString_EastAsian/mattn/go-runewidth-8 23969 ns/op 70.38 MB/s 0 B/op 0 allocs/op
BenchmarkString_EastAsian/rivo/uniseg-8 19852 ns/op 84.98 MB/s 0 B/op 0 allocs/op
BenchmarkString_ASCII/clipperhouse/displaywidth-8 1103 ns/op 116.01 MB/s 0 B/op 0 allocs/op
BenchmarkString_ASCII/mattn/go-runewidth-8 1166 ns/op 109.79 MB/s 0 B/op 0 allocs/op
BenchmarkString_ASCII/rivo/uniseg-8 1584 ns/op 80.83 MB/s 0 B/op 0 allocs/op
BenchmarkString_Emoji/clipperhouse/displaywidth-8 3108 ns/op 232.93 MB/s 0 B/op 0 allocs/op
BenchmarkString_Emoji/mattn/go-runewidth-8 4802 ns/op 150.76 MB/s 0 B/op 0 allocs/op
BenchmarkString_Emoji/rivo/uniseg-8 6607 ns/op 109.58 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Mixed/clipperhouse/displaywidth-8 3456 ns/op 488.20 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Mixed/mattn/go-runewidth-8 5400 ns/op 312.39 MB/s 0 B/op 0 allocs/op
BenchmarkRune_EastAsian/clipperhouse/displaywidth-8 3475 ns/op 485.41 MB/s 0 B/op 0 allocs/op
BenchmarkRune_EastAsian/mattn/go-runewidth-8 15701 ns/op 107.44 MB/s 0 B/op 0 allocs/op
BenchmarkRune_ASCII/clipperhouse/displaywidth-8 257.0 ns/op 498.13 MB/s 0 B/op 0 allocs/op
BenchmarkRune_ASCII/mattn/go-runewidth-8 266.4 ns/op 480.50 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Emoji/clipperhouse/displaywidth-8 1384 ns/op 523.02 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Emoji/mattn/go-runewidth-8 2273 ns/op 318.45 MB/s 0 B/op 0 allocs/op
```

3
vendor/github.com/clipperhouse/displaywidth/gen.go generated vendored Normal file
View File

@ -0,0 +1,3 @@
package displaywidth
//go:generate go run -C internal/gen .

1716
vendor/github.com/clipperhouse/displaywidth/trie.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

210
vendor/github.com/clipperhouse/displaywidth/width.go generated vendored Normal file
View File

@ -0,0 +1,210 @@
package displaywidth
import (
"unicode/utf8"
"github.com/clipperhouse/stringish"
"github.com/clipperhouse/uax29/v2/graphemes"
)
// String calculates the display width of a string,
// by iterating over grapheme clusters in the string
// and summing their widths.
func String(s string) int {
return DefaultOptions.String(s)
}
// Bytes calculates the display width of a []byte,
// by iterating over grapheme clusters in the byte slice
// and summing their widths.
func Bytes(s []byte) int {
return DefaultOptions.Bytes(s)
}
// Rune calculates the display width of a rune. You
// should almost certainly use [String] or [Bytes] for
// most purposes.
//
// The smallest unit of display width is a grapheme
// cluster, not a rune. Iterating over runes to measure
// width is incorrect in most cases.
func Rune(r rune) int {
return DefaultOptions.Rune(r)
}
// Options allows you to specify the treatment of ambiguous East Asian
// characters. When EastAsianWidth is false (default), ambiguous East Asian
// characters are treated as width 1. When EastAsianWidth is true, ambiguous
// East Asian characters are treated as width 2.
type Options struct {
EastAsianWidth bool
}
// DefaultOptions is the default options for the display width
// calculation, which is EastAsianWidth: false.
var DefaultOptions = Options{EastAsianWidth: false}
// String calculates the display width of a string,
// for the given options, by iterating over grapheme clusters
// and summing their widths.
func (options Options) String(s string) int {
if len(s) == 0 {
return 0
}
total := 0
g := graphemes.FromString(s)
for g.Next() {
props := lookupProperties(g.Value())
total += props.width(options)
}
return total
}
// Bytes calculates the display width of a []byte,
// for the given options, by iterating over grapheme
// clusters in the byte slice and summing their widths.
func (options Options) Bytes(s []byte) int {
if len(s) == 0 {
return 0
}
total := 0
g := graphemes.FromBytes(s)
for g.Next() {
props := lookupProperties(g.Value())
total += props.width(options)
}
return total
}
// Rune calculates the display width of a rune,
// for the given options.
//
// The smallest unit of display width is a grapheme
// cluster, not a rune. Iterating over runes to measure
// width is incorrect in most cases.
func (options Options) Rune(r rune) int {
// Fast path for ASCII
if r < utf8.RuneSelf {
if isASCIIControl(byte(r)) {
// Control (0x00-0x1F) and DEL (0x7F)
return 0
}
// ASCII printable (0x20-0x7E)
return 1
}
// Surrogates (U+D800-U+DFFF) are invalid UTF-8 and have zero width
// Other packages might turn them into the replacement character (U+FFFD)
// in which case, we won't see it.
if r >= 0xD800 && r <= 0xDFFF {
return 0
}
// Stack-allocated to avoid heap allocation
var buf [4]byte // UTF-8 is at most 4 bytes
n := utf8.EncodeRune(buf[:], r)
// Skip the grapheme iterator and directly lookup properties
props := lookupProperties(buf[:n])
return props.width(options)
}
func isASCIIControl(b byte) bool {
return b < 0x20 || b == 0x7F
}
// isRIPrefix checks if the slice matches the Regional Indicator prefix
// (F0 9F 87). It assumes len(s) >= 3.
func isRIPrefix[T stringish.Interface](s T) bool {
return s[0] == 0xF0 && s[1] == 0x9F && s[2] == 0x87
}
// isVS16 checks if the slice matches VS16 (U+FE0F) UTF-8 encoding
// (EF B8 8F). It assumes len(s) >= 3.
func isVS16[T stringish.Interface](s T) bool {
return s[0] == 0xEF && s[1] == 0xB8 && s[2] == 0x8F
}
// lookupProperties returns the properties for the first character in a string
func lookupProperties[T stringish.Interface](s T) property {
l := len(s)
if l == 0 {
return 0
}
b := s[0]
if isASCIIControl(b) {
return _Zero_Width
}
if b < utf8.RuneSelf {
// Check for variation selector after ASCII (e.g., keycap sequences like 1⃣)
if l >= 4 {
// Subslice may help eliminate bounds checks
vs := s[1:4]
if isVS16(vs) {
// VS16 requests emoji presentation (width 2)
return _Emoji
}
// VS15 (0x8E) requests text presentation but does not affect width,
// in my reading of Unicode TR51. Falls through to _Default.
}
return _Default
}
// Regional indicator pair (flag)
if l >= 8 {
// Subslice may help eliminate bounds checks
ri := s[:8]
if isRIPrefix(ri[0:3]) {
b3 := ri[3]
if b3 >= 0xA6 && b3 <= 0xBF && isRIPrefix(ri[4:7]) {
b7 := ri[7]
if b7 >= 0xA6 && b7 <= 0xBF {
return _Emoji
}
}
}
}
props, size := lookup(s)
p := property(props)
// Variation Selectors
if size > 0 && l >= size+3 {
// Subslice may help eliminate bounds checks
vs := s[size : size+3]
if isVS16(vs) {
// VS16 requests emoji presentation (width 2)
return _Emoji
}
// VS15 (0x8E) requests text presentation but does not affect width,
// in my reading of Unicode TR51. Falls through to return the base
// character's property (p).
}
return p
}
const _Default property = 0
// a jump table of sorts, instead of a switch
var widthTable = [5]int{
_Default: 1,
_Zero_Width: 0,
_East_Asian_Wide: 2,
_East_Asian_Ambiguous: 1,
_Emoji: 2,
}
// width determines the display width of a character based on its properties
// and configuration options
func (p property) width(options Options) int {
if options.EastAsianWidth && p == _East_Asian_Ambiguous {
return 2
}
return widthTable[p]
}

2
vendor/github.com/clipperhouse/stringish/.gitignore generated vendored Normal file
View File

@ -0,0 +1,2 @@
.DS_Store
*.test

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 Christian Muehlhaeuser
Copyright (c) 2025 Matt Sherman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

64
vendor/github.com/clipperhouse/stringish/README.md generated vendored Normal file
View File

@ -0,0 +1,64 @@
# stringish
A small Go module that provides a generic type constraint for “string-like”
data, and a utf8 package that works with both strings and byte slices
without conversions.
```go
type Interface interface {
~[]byte | ~string
}
```
[![Go Reference](https://pkg.go.dev/badge/github.com/clipperhouse/stringish/utf8.svg)](https://pkg.go.dev/github.com/clipperhouse/stringish/utf8)
[![Test Status](https://github.com/clipperhouse/stringish/actions/workflows/gotest.yml/badge.svg)](https://github.com/clipperhouse/stringish/actions/workflows/gotest.yml)
## Install
```
go get github.com/clipperhouse/stringish
```
## Examples
```go
import (
"github.com/clipperhouse/stringish"
"github.com/clipperhouse/stringish/utf8"
)
s := "Hello, 世界"
r, size := utf8.DecodeRune(s) // not DecodeRuneInString 🎉
b := []byte("Hello, 世界")
r, size = utf8.DecodeRune(b) // same API!
func MyFoo[T stringish.Interface](s T) T {
// pass a string or a []byte
// iterate, slice, transform, whatever
}
```
## Motivation
Sometimes we want APIs to accept `string` or `[]byte` without having to convert
between those types. That conversion usually allocates!
By implementing with `stringish.Interface`, we can have a single API, and
single implementation for both types: one `Foo` instead of `Foo` and
`FooString`.
We have converted the
[`unicode/utf8` package](https://github.com/clipperhouse/stringish/blob/main/utf8/utf8.go)
as an example -- note the absence of`*InString` funcs. We might look at `x/text`
next.
## Used by
- clipperhouse/uax29: [stringish trie](https://github.com/clipperhouse/uax29/blob/master/graphemes/trie.go#L27), [stringish iterator](https://github.com/clipperhouse/uax29/blob/master/internal/iterators/iterator.go#L9), [stringish SplitFunc](https://github.com/clipperhouse/uax29/blob/master/graphemes/splitfunc.go#L21)
- [clipperhouse/displaywidth](https://github.com/clipperhouse/displaywidth)
## Prior discussion
- [Consideration of similar by the Go team](https://github.com/golang/go/issues/48643)

View File

@ -0,0 +1,5 @@
package stringish
type Interface interface {
~[]byte | ~string
}

View File

@ -1,5 +1,9 @@
An implementation of grapheme cluster boundaries from [Unicode text segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) (UAX 29), for Unicode version 15.0.0.
[![Documentation](https://pkg.go.dev/badge/github.com/clipperhouse/uax29/v2/graphemes.svg)](https://pkg.go.dev/github.com/clipperhouse/uax29/v2/graphemes)
![Tests](https://github.com/clipperhouse/uax29/actions/workflows/gotest.yml/badge.svg)
![Fuzz](https://github.com/clipperhouse/uax29/actions/workflows/gofuzz.yml/badge.svg)
## Quick start
```
@ -18,15 +22,14 @@ for tokens.Next() { // Next() returns true until end of data
}
```
[![Documentation](https://pkg.go.dev/badge/github.com/clipperhouse/uax29/v2/graphemes.svg)](https://pkg.go.dev/github.com/clipperhouse/uax29/v2/graphemes)
_A grapheme is a “single visible character”, which might be a simple as a single letter, or a complex emoji that consists of several Unicode code points._
## Conformance
We use the Unicode [test suite](https://unicode.org/reports/tr41/tr41-26.html#Tests29). Status:
We use the Unicode [test suite](https://unicode.org/reports/tr41/tr41-26.html#Tests29).
![Go](https://github.com/clipperhouse/uax29/actions/workflows/gotest.yml/badge.svg)
![Tests](https://github.com/clipperhouse/uax29/actions/workflows/gotest.yml/badge.svg)
![Fuzz](https://github.com/clipperhouse/uax29/actions/workflows/gofuzz.yml/badge.svg)
## APIs
@ -71,9 +74,18 @@ for tokens.Next() { // Next() returns true until end of data
}
```
### Performance
### Benchmarks
On a Mac M2 laptop, we see around 200MB/s, or around 100 million graphemes per second. You should see ~constant memory, and no allocations.
On a Mac M2 laptop, we see around 200MB/s, or around 100 million graphemes per second, and no allocations.
```
goos: darwin
goarch: arm64
pkg: github.com/clipperhouse/uax29/graphemes/comparative
cpu: Apple M2
BenchmarkGraphemes/clipperhouse/uax29-8 173805 ns/op 201.16 MB/s 0 B/op 0 allocs/op
BenchmarkGraphemes/rivo/uniseg-8 2045128 ns/op 17.10 MB/s 0 B/op 0 allocs/op
```
### Invalid inputs

View File

@ -1,8 +1,11 @@
package graphemes
import "github.com/clipperhouse/uax29/v2/internal/iterators"
import (
"github.com/clipperhouse/stringish"
"github.com/clipperhouse/uax29/v2/internal/iterators"
)
type Iterator[T iterators.Stringish] struct {
type Iterator[T stringish.Interface] struct {
*iterators.Iterator[T]
}

View File

@ -3,7 +3,7 @@ package graphemes
import (
"bufio"
"github.com/clipperhouse/uax29/v2/internal/iterators"
"github.com/clipperhouse/stringish"
)
// is determines if lookup intersects propert(ies)
@ -18,7 +18,7 @@ const _Ignore = _Extend
// See https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries.
var SplitFunc bufio.SplitFunc = splitFunc[[]byte]
func splitFunc[T iterators.Stringish](data T, atEOF bool) (advance int, token T, err error) {
func splitFunc[T stringish.Interface](data T, atEOF bool) (advance int, token T, err error) {
var empty T
if len(data) == 0 {
return 0, empty, nil

View File

@ -1,10 +1,10 @@
package graphemes
import "github.com/clipperhouse/stringish"
// generated by github.com/clipperhouse/uax29/v2
// from https://www.unicode.org/Public/15.0.0/ucd/auxiliary/GraphemeBreakProperty.txt
import "github.com/clipperhouse/uax29/v2/internal/iterators"
type property uint16
const (
@ -27,7 +27,7 @@ const (
// lookup returns the trie value for the first UTF-8 encoding in s and
// the width in bytes of this encoding. The size will be 0 if s does not
// hold enough bytes to complete the encoding. len(s) must be greater than 0.
func lookup[T iterators.Stringish](s T) (v property, sz int) {
func lookup[T stringish.Interface](s T) (v property, sz int) {
c0 := s[0]
switch {
case c0 < 0x80: // is ASCII

View File

@ -1,14 +1,12 @@
package iterators
type Stringish interface {
[]byte | string
}
import "github.com/clipperhouse/stringish"
type SplitFunc[T Stringish] func(T, bool) (int, T, error)
type SplitFunc[T stringish.Interface] func(T, bool) (int, T, error)
// Iterator is a generic iterator for words that are either []byte or string.
// Iterate while Next() is true, and access the word via Value().
type Iterator[T Stringish] struct {
type Iterator[T stringish.Interface] struct {
split SplitFunc[T]
data T
start int
@ -16,7 +14,7 @@ type Iterator[T Stringish] struct {
}
// New creates a new Iterator for the given data and SplitFunc.
func New[T Stringish](split SplitFunc[T], data T) *Iterator[T] {
func New[T stringish.Interface](split SplitFunc[T], data T) *Iterator[T] {
return &Iterator[T]{
split: split,
data: data,
@ -83,3 +81,20 @@ func (iter *Iterator[T]) Reset() {
iter.start = 0
iter.pos = 0
}
func (iter *Iterator[T]) First() T {
if len(iter.data) == 0 {
return iter.data
}
advance, _, err := iter.split(iter.data, true)
if err != nil {
panic(err)
}
if advance <= 0 {
panic("SplitFunc returned a zero or negative advance")
}
if advance > len(iter.data) {
panic("SplitFunc advanced beyond the end of the data")
}
return iter.data[:advance]
}

View File

@ -9,6 +9,10 @@
version: "2"
run:
build-tags:
- libpathrs
linters:
enable:
- asasalint

View File

@ -6,6 +6,92 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased] ##
## [0.6.0] - 2025-11-03 ##
> By the Power of Greyskull!
While quite small code-wise, this release marks a very key point in the
development of filepath-securejoin.
filepath-securejoin was originally intended (back in 2017) to simply be a
single-purpose library that would take some common code used in container
runtimes (specifically, Docker's `FollowSymlinksInScope`) and make it more
general-purpose (with the eventual goals of it ending up in the Go stdlib).
Of course, I quickly discovered that this problem was actually far more
complicated to solve when dealing with racing attackers, which lead to me
developing `openat2(2)` and [libpathrs][]. I had originally planned for
libpathrs to completely replace filepath-securejoin "once it was ready" but in
the interim we needed to fix several race attacks in runc as part of security
advisories. Obviously we couldn't require the usage of a pre-0.1 Rust library
in runc so it was necessary to port bits of libpathrs into filepath-securejoin.
(Ironically the first prototypes of libpathrs were originally written in Go and
then rewritten to Rust, so the code in filepath-securejoin is actually Go code
that was rewritten to Rust then re-rewritten to Go.)
It then became clear that pure-Go libraries will likely not be willing to
require CGo for all of their builds, so it was necessary to accept that
filepath-securejoin will need to stay. As such, in v0.5.0 we provided more
pure-Go implementations of features from libpathrs but moved them into
`pathrs-lite` subpackage to clarify what purpose these helpers serve.
This release finally closes the loop and makes it so that pathrs-lite can
transparently use libpathrs (via a `libpathrs` build-tag). This means that
upstream libraries can use the pure Go version if they prefer, but downstreams
(either downstream library users or even downstream distributions) are able to
migrate to libpathrs for all usages of pathrs-lite in an entire Go binary.
I should make it clear that I do not plan to port the rest of libpathrs to Go,
as I do not wish to maintain two copies of the same codebase. pathrs-lite
already provides the core essentials necessary to operate on paths safely for
most modern systems. Users who want additional hardening or more ergonomic APIs
are free to use [`cyphar.com/go-pathrs`][go-pathrs] (libpathrs's Go bindings).
[libpathrs]: https://github.com/cyphar/libpathrs
[go-pathrs]: https://cyphar.com/go-pathrs
### Breaking ###
- The deprecated `MkdirAll`, `MkdirAllHandle`, `OpenInRoot`, `OpenatInRoot` and
`Reopen` wrappers have been removed. Please switch to using `pathrs-lite`
directly.
### Added ###
- `pathrs-lite` now has support for using [libpathrs][libpathrs] as a backend.
This is opt-in and can be enabled at build time with the `libpathrs` build
tag. The intention is to allow for downstream libraries and other projects to
make use of the pure-Go `github.com/cyphar/filepath-securejoin/pathrs-lite`
package and distributors can then opt-in to using `libpathrs` for the entire
binary if they wish.
## [0.5.1] - 2025-10-31 ##
> Spooky scary skeletons send shivers down your spine!
### Changed ###
- `openat2` can return `-EAGAIN` if it detects a possible attack in certain
scenarios (namely if there was a rename or mount while walking a path with a
`..` component). While this is necessary to avoid a denial-of-service in the
kernel, it does require retry loops in userspace.
In previous versions, `pathrs-lite` would retry `openat2` 32 times before
returning an error, but we've received user reports that this limit can be
hit on systems with very heavy load. In some synthetic benchmarks (testing
the worst-case of an attacker doing renames in a tight loop on every core of
a 16-core machine) we managed to get a ~3% failure rate in runc. We have
improved this situation in two ways:
* We have now increased this limit to 128, which should be good enough for
most use-cases without becoming a denial-of-service vector (the number of
syscalls called by the `O_PATH` resolver in a typical case is within the
same ballpark). The same benchmarks show a failure rate of ~0.12% which
(while not zero) is probably sufficient for most users.
* In addition, we now return a `unix.EAGAIN` error that is bubbled up and can
be detected by callers. This means that callers with stricter requirements
to avoid spurious errors can choose to do their own infinite `EAGAIN` retry
loop (though we would strongly recommend users use time-based deadlines in
such retry loops to avoid potentially unbounded denials-of-service).
## [0.5.0] - 2025-09-26 ##
> Let the past die. Kill it if you have to.
@ -354,7 +440,9 @@ This is our first release of `github.com/cyphar/filepath-securejoin`,
containing a full implementation with a coverage of 93.5% (the only missing
cases are the error cases, which are hard to mocktest at the moment).
[Unreleased]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.0...HEAD
[Unreleased]: https://github.com/cyphar/filepath-securejoin/compare/v0.6.0...HEAD
[0.6.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.1...v0.6.0
[0.5.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.0...v0.5.1
[0.5.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.1...v0.5.0
[0.4.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.0...v0.4.1
[0.4.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.6...v0.4.0

View File

@ -1 +1 @@
0.5.0
0.6.0

View File

@ -1,48 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//go:build linux
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
package securejoin
import (
"github.com/cyphar/filepath-securejoin/pathrs-lite"
)
var (
// MkdirAll is a wrapper around [pathrs.MkdirAll].
//
// Deprecated: You should use [pathrs.MkdirAll] directly instead. This
// wrapper will be removed in filepath-securejoin v0.6.
MkdirAll = pathrs.MkdirAll
// MkdirAllHandle is a wrapper around [pathrs.MkdirAllHandle].
//
// Deprecated: You should use [pathrs.MkdirAllHandle] directly instead.
// This wrapper will be removed in filepath-securejoin v0.6.
MkdirAllHandle = pathrs.MkdirAllHandle
// OpenInRoot is a wrapper around [pathrs.OpenInRoot].
//
// Deprecated: You should use [pathrs.OpenInRoot] directly instead. This
// wrapper will be removed in filepath-securejoin v0.6.
OpenInRoot = pathrs.OpenInRoot
// OpenatInRoot is a wrapper around [pathrs.OpenatInRoot].
//
// Deprecated: You should use [pathrs.OpenatInRoot] directly instead. This
// wrapper will be removed in filepath-securejoin v0.6.
OpenatInRoot = pathrs.OpenatInRoot
// Reopen is a wrapper around [pathrs.Reopen].
//
// Deprecated: You should use [pathrs.Reopen] directly instead. This
// wrapper will be removed in filepath-securejoin v0.6.
Reopen = pathrs.Reopen
)

View File

@ -1,33 +0,0 @@
## `pathrs-lite` ##
`github.com/cyphar/filepath-securejoin/pathrs-lite` provides a minimal **pure
Go** implementation of the core bits of [libpathrs][]. This is not intended to
be a complete replacement for libpathrs, instead it is mainly intended to be
useful as a transition tool for existing Go projects.
The long-term plan for `pathrs-lite` is to provide a build tag that will cause
all `pathrs-lite` operations to call into libpathrs directly, thus removing
code duplication for projects that wish to make use of libpathrs (and providing
the ability for software packagers to opt-in to libpathrs support without
needing to patch upstream).
[libpathrs]: https://github.com/cyphar/libpathrs
### License ###
Most of this subpackage is licensed under the Mozilla Public License (version
2.0). For more information, see the top-level [COPYING.md][] and
[LICENSE.MPL-2.0][] files, as well as the individual license headers for each
file.
```
Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
Copyright (C) 2024-2025 SUSE LLC
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
```
[COPYING.md]: ../COPYING.md
[LICENSE.MPL-2.0]: ../LICENSE.MPL-2.0

View File

@ -1,14 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//go:build linux
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Package pathrs (pathrs-lite) is a less complete pure Go implementation of
// some of the APIs provided by [libpathrs].
package pathrs

View File

@ -1,30 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (C) 2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Package assert provides some basic assertion helpers for Go.
package assert
import (
"fmt"
)
// Assert panics if the predicate is false with the provided argument.
func Assert(predicate bool, msg any) {
if !predicate {
panic(msg)
}
}
// Assertf panics if the predicate is false and formats the message using the
// same formatting as [fmt.Printf].
//
// [fmt.Printf]: https://pkg.go.dev/fmt#Printf
func Assertf(predicate bool, fmtMsg string, args ...any) {
Assert(predicate, fmt.Sprintf(fmtMsg, args...))
}

View File

@ -1,30 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Package internal contains unexported common code for filepath-securejoin.
package internal
import (
"errors"
)
var (
// ErrPossibleAttack indicates that some attack was detected.
ErrPossibleAttack = errors.New("possible attack detected")
// ErrPossibleBreakout indicates that during an operation we ended up in a
// state that could be a breakout but we detected it.
ErrPossibleBreakout = errors.New("possible breakout detected")
// ErrInvalidDirectory indicates an unlinked directory.
ErrInvalidDirectory = errors.New("wandered into deleted directory")
// ErrDeletedInode indicates an unlinked file (non-directory).
ErrDeletedInode = errors.New("cannot verify path of deleted inode")
)

View File

@ -1,148 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//go:build linux
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
package fd
import (
"fmt"
"os"
"path/filepath"
"runtime"
"golang.org/x/sys/unix"
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
)
// prepareAtWith returns -EBADF (an invalid fd) if dir is nil, otherwise using
// the dir.Fd(). We use -EBADF because in filepath-securejoin we generally
// don't want to allow relative-to-cwd paths. The returned path is an
// *informational* string that describes a reasonable pathname for the given
// *at(2) arguments. You must not use the full path for any actual filesystem
// operations.
func prepareAt(dir Fd, path string) (dirFd int, unsafeUnmaskedPath string) {
dirFd, dirPath := -int(unix.EBADF), "."
if dir != nil {
dirFd, dirPath = int(dir.Fd()), dir.Name()
}
if !filepath.IsAbs(path) {
// only prepend the dirfd path for relative paths
path = dirPath + "/" + path
}
// NOTE: If path is "." or "", the returned path won't be filepath.Clean,
// but that's okay since this path is either used for errors (in which case
// a trailing "/" or "/." is important information) or will be
// filepath.Clean'd later (in the case of fd.Openat).
return dirFd, path
}
// Openat is an [Fd]-based wrapper around unix.Openat.
func Openat(dir Fd, path string, flags int, mode int) (*os.File, error) { //nolint:unparam // wrapper func
dirFd, fullPath := prepareAt(dir, path)
// Make sure we always set O_CLOEXEC.
flags |= unix.O_CLOEXEC
fd, err := unix.Openat(dirFd, path, flags, uint32(mode))
if err != nil {
return nil, &os.PathError{Op: "openat", Path: fullPath, Err: err}
}
runtime.KeepAlive(dir)
// openat is only used with lexically-safe paths so we can use
// filepath.Clean here, and also the path itself is not going to be used
// for actual path operations.
fullPath = filepath.Clean(fullPath)
return os.NewFile(uintptr(fd), fullPath), nil
}
// Fstatat is an [Fd]-based wrapper around unix.Fstatat.
func Fstatat(dir Fd, path string, flags int) (unix.Stat_t, error) {
dirFd, fullPath := prepareAt(dir, path)
var stat unix.Stat_t
if err := unix.Fstatat(dirFd, path, &stat, flags); err != nil {
return stat, &os.PathError{Op: "fstatat", Path: fullPath, Err: err}
}
runtime.KeepAlive(dir)
return stat, nil
}
// Faccessat is an [Fd]-based wrapper around unix.Faccessat.
func Faccessat(dir Fd, path string, mode uint32, flags int) error {
dirFd, fullPath := prepareAt(dir, path)
err := unix.Faccessat(dirFd, path, mode, flags)
if err != nil {
err = &os.PathError{Op: "faccessat", Path: fullPath, Err: err}
}
runtime.KeepAlive(dir)
return err
}
// Readlinkat is an [Fd]-based wrapper around unix.Readlinkat.
func Readlinkat(dir Fd, path string) (string, error) {
dirFd, fullPath := prepareAt(dir, path)
size := 4096
for {
linkBuf := make([]byte, size)
n, err := unix.Readlinkat(dirFd, path, linkBuf)
if err != nil {
return "", &os.PathError{Op: "readlinkat", Path: fullPath, Err: err}
}
runtime.KeepAlive(dir)
if n != size {
return string(linkBuf[:n]), nil
}
// Possible truncation, resize the buffer.
size *= 2
}
}
const (
// STATX_MNT_ID_UNIQUE is provided in golang.org/x/sys@v0.20.0, but in order to
// avoid bumping the requirement for a single constant we can just define it
// ourselves.
_STATX_MNT_ID_UNIQUE = 0x4000 //nolint:revive // unix.* name
// We don't care which mount ID we get. The kernel will give us the unique
// one if it is supported. If the kernel doesn't support
// STATX_MNT_ID_UNIQUE, the bit is ignored and the returned request mask
// will only contain STATX_MNT_ID (if supported).
wantStatxMntMask = _STATX_MNT_ID_UNIQUE | unix.STATX_MNT_ID
)
var hasStatxMountID = gocompat.SyncOnceValue(func() bool {
var stx unix.Statx_t
err := unix.Statx(-int(unix.EBADF), "/", 0, wantStatxMntMask, &stx)
return err == nil && stx.Mask&wantStatxMntMask != 0
})
// GetMountID gets the mount identifier associated with the fd and path
// combination. It is effectively a wrapper around fetching
// STATX_MNT_ID{,_UNIQUE} with unix.Statx, but with a fallback to 0 if the
// kernel doesn't support the feature.
func GetMountID(dir Fd, path string) (uint64, error) {
// If we don't have statx(STATX_MNT_ID*) support, we can't do anything.
if !hasStatxMountID() {
return 0, nil
}
dirFd, fullPath := prepareAt(dir, path)
var stx unix.Statx_t
err := unix.Statx(dirFd, path, unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW, wantStatxMntMask, &stx)
if stx.Mask&wantStatxMntMask == 0 {
// It's not a kernel limitation, for some reason we couldn't get a
// mount ID. Assume it's some kind of attack.
err = fmt.Errorf("could not get mount id: %w", err)
}
if err != nil {
return 0, &os.PathError{Op: "statx(STATX_MNT_ID_...)", Path: fullPath, Err: err}
}
runtime.KeepAlive(dir)
return stx.Mnt_id, nil
}

View File

@ -1,55 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (C) 2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Package fd provides a drop-in interface-based replacement of [*os.File] that
// allows for things like noop-Close wrappers to be used.
//
// [*os.File]: https://pkg.go.dev/os#File
package fd
import (
"io"
"os"
)
// Fd is an interface that mirrors most of the API of [*os.File], allowing you
// to create wrappers that can be used in place of [*os.File].
//
// [*os.File]: https://pkg.go.dev/os#File
type Fd interface {
io.Closer
Name() string
Fd() uintptr
}
// Compile-time interface checks.
var (
_ Fd = (*os.File)(nil)
_ Fd = noClose{}
)
type noClose struct{ inner Fd }
func (f noClose) Name() string { return f.inner.Name() }
func (f noClose) Fd() uintptr { return f.inner.Fd() }
func (f noClose) Close() error { return nil }
// NopCloser returns an [*os.File]-like object where the [Close] method is now
// a no-op.
//
// Note that for [*os.File] and similar objects, the Go garbage collector will
// still call [Close] on the underlying file unless you use
// [runtime.SetFinalizer] to disable this behaviour. This is up to the caller
// to do (if necessary).
//
// [*os.File]: https://pkg.go.dev/os#File
// [Close]: https://pkg.go.dev/io#Closer
// [runtime.SetFinalizer]: https://pkg.go.dev/runtime#SetFinalizer
func NopCloser(f Fd) Fd { return noClose{inner: f} }

View File

@ -1,78 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//go:build linux
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
package fd
import (
"fmt"
"os"
"runtime"
"golang.org/x/sys/unix"
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal"
)
// DupWithName creates a new file descriptor referencing the same underlying
// file, but with the provided name instead of fd.Name().
func DupWithName(fd Fd, name string) (*os.File, error) {
fd2, err := unix.FcntlInt(fd.Fd(), unix.F_DUPFD_CLOEXEC, 0)
if err != nil {
return nil, os.NewSyscallError("fcntl(F_DUPFD_CLOEXEC)", err)
}
runtime.KeepAlive(fd)
return os.NewFile(uintptr(fd2), name), nil
}
// Dup creates a new file description referencing the same underlying file.
func Dup(fd Fd) (*os.File, error) {
return DupWithName(fd, fd.Name())
}
// Fstat is an [Fd]-based wrapper around unix.Fstat.
func Fstat(fd Fd) (unix.Stat_t, error) {
var stat unix.Stat_t
if err := unix.Fstat(int(fd.Fd()), &stat); err != nil {
return stat, &os.PathError{Op: "fstat", Path: fd.Name(), Err: err}
}
runtime.KeepAlive(fd)
return stat, nil
}
// Fstatfs is an [Fd]-based wrapper around unix.Fstatfs.
func Fstatfs(fd Fd) (unix.Statfs_t, error) {
var statfs unix.Statfs_t
if err := unix.Fstatfs(int(fd.Fd()), &statfs); err != nil {
return statfs, &os.PathError{Op: "fstatfs", Path: fd.Name(), Err: err}
}
runtime.KeepAlive(fd)
return statfs, nil
}
// IsDeadInode detects whether the file has been unlinked from a filesystem and
// is thus a "dead inode" from the kernel's perspective.
func IsDeadInode(file Fd) error {
// If the nlink of a file drops to 0, there is an attacker deleting
// directories during our walk, which could result in weird /proc values.
// It's better to error out in this case.
stat, err := Fstat(file)
if err != nil {
return fmt.Errorf("check for dead inode: %w", err)
}
if stat.Nlink == 0 {
err := internal.ErrDeletedInode
if stat.Mode&unix.S_IFMT == unix.S_IFDIR {
err = internal.ErrInvalidDirectory
}
return fmt.Errorf("%w %q", err, file.Name())
}
return nil
}

View File

@ -1,54 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//go:build linux
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
package fd
import (
"os"
"runtime"
"golang.org/x/sys/unix"
)
// Fsopen is an [Fd]-based wrapper around unix.Fsopen.
func Fsopen(fsName string, flags int) (*os.File, error) {
// Make sure we always set O_CLOEXEC.
flags |= unix.FSOPEN_CLOEXEC
fd, err := unix.Fsopen(fsName, flags)
if err != nil {
return nil, os.NewSyscallError("fsopen "+fsName, err)
}
return os.NewFile(uintptr(fd), "fscontext:"+fsName), nil
}
// Fsmount is an [Fd]-based wrapper around unix.Fsmount.
func Fsmount(ctx Fd, flags, mountAttrs int) (*os.File, error) {
// Make sure we always set O_CLOEXEC.
flags |= unix.FSMOUNT_CLOEXEC
fd, err := unix.Fsmount(int(ctx.Fd()), flags, mountAttrs)
if err != nil {
return nil, os.NewSyscallError("fsmount "+ctx.Name(), err)
}
return os.NewFile(uintptr(fd), "fsmount:"+ctx.Name()), nil
}
// OpenTree is an [Fd]-based wrapper around unix.OpenTree.
func OpenTree(dir Fd, path string, flags uint) (*os.File, error) {
dirFd, fullPath := prepareAt(dir, path)
// Make sure we always set O_CLOEXEC.
flags |= unix.OPEN_TREE_CLOEXEC
fd, err := unix.OpenTree(dirFd, path, flags)
if err != nil {
return nil, &os.PathError{Op: "open_tree", Path: fullPath, Err: err}
}
runtime.KeepAlive(dir)
return os.NewFile(uintptr(fd), fullPath), nil
}

View File

@ -1,62 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//go:build linux
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
package fd
import (
"errors"
"os"
"runtime"
"golang.org/x/sys/unix"
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal"
)
func scopedLookupShouldRetry(how *unix.OpenHow, err error) bool {
// RESOLVE_IN_ROOT (and RESOLVE_BENEATH) can return -EAGAIN if we resolve
// ".." while a mount or rename occurs anywhere on the system. This could
// happen spuriously, or as the result of an attacker trying to mess with
// us during lookup.
//
// In addition, scoped lookups have a "safety check" at the end of
// complete_walk which will return -EXDEV if the final path is not in the
// root.
return how.Resolve&(unix.RESOLVE_IN_ROOT|unix.RESOLVE_BENEATH) != 0 &&
(errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EXDEV))
}
const scopedLookupMaxRetries = 32
// Openat2 is an [Fd]-based wrapper around unix.Openat2, but with some retry
// logic in case of EAGAIN errors.
func Openat2(dir Fd, path string, how *unix.OpenHow) (*os.File, error) {
dirFd, fullPath := prepareAt(dir, path)
// Make sure we always set O_CLOEXEC.
how.Flags |= unix.O_CLOEXEC
var tries int
for tries < scopedLookupMaxRetries {
fd, err := unix.Openat2(dirFd, path, how)
if err != nil {
if scopedLookupShouldRetry(how, err) {
// We retry a couple of times to avoid the spurious errors, and
// if we are being attacked then returning -EAGAIN is the best
// we can do.
tries++
continue
}
return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: err}
}
runtime.KeepAlive(dir)
return os.NewFile(uintptr(fd), fullPath), nil
}
return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: internal.ErrPossibleAttack}
}

View File

@ -1,10 +0,0 @@
## gocompat ##
This directory contains backports of stdlib functions from later Go versions so
the filepath-securejoin can continue to be used by projects that are stuck with
Go 1.18 support. Note that often filepath-securejoin is added in security
patches for old releases, so avoiding the need to bump Go compiler requirements
is a huge plus to downstreams.
The source code is licensed under the same license as the Go stdlib. See the
source files for the precise license information.

View File

@ -1,13 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && go1.20
// Copyright (C) 2025 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package gocompat includes compatibility shims (backported from future Go
// stdlib versions) to permit filepath-securejoin to be used with older Go
// versions (often filepath-securejoin is added in security patches for old
// releases, so avoiding the need to bump Go compiler requirements is a huge
// plus to downstreams).
package gocompat

View File

@ -1,19 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && go1.20
// Copyright (C) 2024 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gocompat
import (
"fmt"
)
// WrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except
// that on pre-1.20 Go versions only errors.Is() works properly (errors.Unwrap)
// is only guaranteed to give you baseErr.
func WrapBaseError(baseErr, extraErr error) error {
return fmt.Errorf("%w: %w", extraErr, baseErr)
}

View File

@ -1,40 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !go1.20
// Copyright (C) 2024 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gocompat
import (
"fmt"
)
type wrappedError struct {
inner error
isError error
}
func (err wrappedError) Is(target error) bool {
return err.isError == target
}
func (err wrappedError) Unwrap() error {
return err.inner
}
func (err wrappedError) Error() string {
return fmt.Sprintf("%v: %v", err.isError, err.inner)
}
// WrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except
// that on pre-1.20 Go versions only errors.Is() works properly (errors.Unwrap)
// is only guaranteed to give you baseErr.
func WrapBaseError(baseErr, extraErr error) error {
return wrappedError{
inner: baseErr,
isError: extraErr,
}
}

View File

@ -1,53 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && go1.21
// Copyright (C) 2024-2025 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gocompat
import (
"cmp"
"slices"
"sync"
)
// SlicesDeleteFunc is equivalent to Go 1.21's slices.DeleteFunc.
func SlicesDeleteFunc[S ~[]E, E any](slice S, delFn func(E) bool) S {
return slices.DeleteFunc(slice, delFn)
}
// SlicesContains is equivalent to Go 1.21's slices.Contains.
func SlicesContains[S ~[]E, E comparable](slice S, val E) bool {
return slices.Contains(slice, val)
}
// SlicesClone is equivalent to Go 1.21's slices.Clone.
func SlicesClone[S ~[]E, E any](slice S) S {
return slices.Clone(slice)
}
// SyncOnceValue is equivalent to Go 1.21's sync.OnceValue.
func SyncOnceValue[T any](f func() T) func() T {
return sync.OnceValue(f)
}
// SyncOnceValues is equivalent to Go 1.21's sync.OnceValues.
func SyncOnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
return sync.OnceValues(f)
}
// CmpOrdered is equivalent to Go 1.21's cmp.Ordered generic type definition.
type CmpOrdered = cmp.Ordered
// CmpCompare is equivalent to Go 1.21's cmp.Compare.
func CmpCompare[T CmpOrdered](x, y T) int {
return cmp.Compare(x, y)
}
// Max2 is equivalent to Go 1.21's max builtin (but only for two parameters).
func Max2[T CmpOrdered](x, y T) T {
return max(x, y)
}

View File

@ -1,187 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !go1.21
// Copyright (C) 2021, 2022 The Go Authors. All rights reserved.
// Copyright (C) 2024-2025 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE.BSD file.
package gocompat
import (
"sync"
)
// These are very minimal implementations of functions that appear in Go 1.21's
// stdlib, included so that we can build on older Go versions. Most are
// borrowed directly from the stdlib, and a few are modified to be "obviously
// correct" without needing to copy too many other helpers.
// clearSlice is equivalent to Go 1.21's builtin clear.
// Copied from the Go 1.24 stdlib implementation.
func clearSlice[S ~[]E, E any](slice S) {
var zero E
for i := range slice {
slice[i] = zero
}
}
// slicesIndexFunc is equivalent to Go 1.21's slices.IndexFunc.
// Copied from the Go 1.24 stdlib implementation.
func slicesIndexFunc[S ~[]E, E any](s S, f func(E) bool) int {
for i := range s {
if f(s[i]) {
return i
}
}
return -1
}
// SlicesDeleteFunc is equivalent to Go 1.21's slices.DeleteFunc.
// Copied from the Go 1.24 stdlib implementation.
func SlicesDeleteFunc[S ~[]E, E any](s S, del func(E) bool) S {
i := slicesIndexFunc(s, del)
if i == -1 {
return s
}
// Don't start copying elements until we find one to delete.
for j := i + 1; j < len(s); j++ {
if v := s[j]; !del(v) {
s[i] = v
i++
}
}
clearSlice(s[i:]) // zero/nil out the obsolete elements, for GC
return s[:i]
}
// SlicesContains is equivalent to Go 1.21's slices.Contains.
// Similar to the stdlib slices.Contains, except that we don't have
// slices.Index so we need to use slices.IndexFunc for this non-Func helper.
func SlicesContains[S ~[]E, E comparable](s S, v E) bool {
return slicesIndexFunc(s, func(e E) bool { return e == v }) >= 0
}
// SlicesClone is equivalent to Go 1.21's slices.Clone.
// Copied from the Go 1.24 stdlib implementation.
func SlicesClone[S ~[]E, E any](s S) S {
// Preserve nil in case it matters.
if s == nil {
return nil
}
return append(S([]E{}), s...)
}
// SyncOnceValue is equivalent to Go 1.21's sync.OnceValue.
// Copied from the Go 1.25 stdlib implementation.
func SyncOnceValue[T any](f func() T) func() T {
// Use a struct so that there's a single heap allocation.
d := struct {
f func() T
once sync.Once
valid bool
p any
result T
}{
f: f,
}
return func() T {
d.once.Do(func() {
defer func() {
d.f = nil
d.p = recover()
if !d.valid {
panic(d.p)
}
}()
d.result = d.f()
d.valid = true
})
if !d.valid {
panic(d.p)
}
return d.result
}
}
// SyncOnceValues is equivalent to Go 1.21's sync.OnceValues.
// Copied from the Go 1.25 stdlib implementation.
func SyncOnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
// Use a struct so that there's a single heap allocation.
d := struct {
f func() (T1, T2)
once sync.Once
valid bool
p any
r1 T1
r2 T2
}{
f: f,
}
return func() (T1, T2) {
d.once.Do(func() {
defer func() {
d.f = nil
d.p = recover()
if !d.valid {
panic(d.p)
}
}()
d.r1, d.r2 = d.f()
d.valid = true
})
if !d.valid {
panic(d.p)
}
return d.r1, d.r2
}
}
// CmpOrdered is equivalent to Go 1.21's cmp.Ordered generic type definition.
// Copied from the Go 1.25 stdlib implementation.
type CmpOrdered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}
// isNaN reports whether x is a NaN without requiring the math package.
// This will always return false if T is not floating-point.
// Copied from the Go 1.25 stdlib implementation.
func isNaN[T CmpOrdered](x T) bool {
return x != x
}
// CmpCompare is equivalent to Go 1.21's cmp.Compare.
// Copied from the Go 1.25 stdlib implementation.
func CmpCompare[T CmpOrdered](x, y T) int {
xNaN := isNaN(x)
yNaN := isNaN(y)
if xNaN {
if yNaN {
return 0
}
return -1
}
if yNaN {
return +1
}
if x < y {
return -1
}
if x > y {
return +1
}
return 0
}
// Max2 is equivalent to Go 1.21's max builtin for two parameters.
func Max2[T CmpOrdered](x, y T) T {
m := x
if y > m {
m = y
}
return m
}

View File

@ -1,123 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (C) 2022 The Go Authors. All rights reserved.
// Copyright (C) 2025 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE.BSD file.
// The parsing logic is very loosely based on the Go stdlib's
// src/internal/syscall/unix/kernel_version_linux.go but with an API that looks
// a bit like runc's libcontainer/system/kernelversion.
//
// TODO(cyphar): This API has been copied around to a lot of different projects
// (Docker, containerd, runc, and now filepath-securejoin) -- maybe we should
// put it in a separate project?
// Package kernelversion provides a simple mechanism for checking whether the
// running kernel is at least as new as some baseline kernel version. This is
// often useful when checking for features that would be too complicated to
// test support for (or in cases where we know that some kernel features in
// backport-heavy kernels are broken and need to be avoided).
package kernelversion
import (
"bytes"
"errors"
"fmt"
"strconv"
"strings"
"golang.org/x/sys/unix"
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
)
// KernelVersion is a numeric representation of the key numerical elements of a
// kernel version (for instance, "4.1.2-default-1" would be represented as
// KernelVersion{4, 1, 2}).
type KernelVersion []uint64
func (kver KernelVersion) String() string {
var str strings.Builder
for idx, elem := range kver {
if idx != 0 {
_, _ = str.WriteRune('.')
}
_, _ = str.WriteString(strconv.FormatUint(elem, 10))
}
return str.String()
}
var errInvalidKernelVersion = errors.New("invalid kernel version")
// parseKernelVersion parses a string and creates a KernelVersion based on it.
func parseKernelVersion(kverStr string) (KernelVersion, error) {
kver := make(KernelVersion, 1, 3)
for idx, ch := range kverStr {
if '0' <= ch && ch <= '9' {
v := &kver[len(kver)-1]
*v = (*v * 10) + uint64(ch-'0')
} else {
if idx == 0 || kverStr[idx-1] < '0' || '9' < kverStr[idx-1] {
// "." must be preceded by a digit while in version section
return nil, fmt.Errorf("%w %q: kernel version has dot(s) followed by non-digit in version section", errInvalidKernelVersion, kverStr)
}
if ch != '.' {
break
}
kver = append(kver, 0)
}
}
if len(kver) < 2 {
return nil, fmt.Errorf("%w %q: kernel versions must contain at least two components", errInvalidKernelVersion, kverStr)
}
return kver, nil
}
// getKernelVersion gets the current kernel version.
var getKernelVersion = gocompat.SyncOnceValues(func() (KernelVersion, error) {
var uts unix.Utsname
if err := unix.Uname(&uts); err != nil {
return nil, err
}
// Remove the \x00 from the release.
release := uts.Release[:]
return parseKernelVersion(string(release[:bytes.IndexByte(release, 0)]))
})
// GreaterEqualThan returns true if the the host kernel version is greater than
// or equal to the provided [KernelVersion]. When doing this comparison, any
// non-numerical suffixes of the host kernel version are ignored.
//
// If the number of components provided is not equal to the number of numerical
// components of the host kernel version, any missing components are treated as
// 0. This means that GreaterEqualThan(KernelVersion{4}) will be treated the
// same as GreaterEqualThan(KernelVersion{4, 0, 0, ..., 0, 0}), and that if the
// host kernel version is "4" then GreaterEqualThan(KernelVersion{4, 1}) will
// return false (because the host version will be treated as "4.0").
func GreaterEqualThan(wantKver KernelVersion) (bool, error) {
hostKver, err := getKernelVersion()
if err != nil {
return false, err
}
// Pad out the kernel version lengths to match one another.
cmpLen := gocompat.Max2(len(hostKver), len(wantKver))
hostKver = append(hostKver, make(KernelVersion, cmpLen-len(hostKver))...)
wantKver = append(wantKver, make(KernelVersion, cmpLen-len(wantKver))...)
for i := 0; i < cmpLen; i++ {
switch gocompat.CmpCompare(hostKver[i], wantKver[i]) {
case -1:
// host < want
return false, nil
case +1:
// host > want
return true, nil
case 0:
continue
}
}
// equal version values
return true, nil
}

View File

@ -1,12 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Package linux returns information about what features are supported on the
// running kernel.
package linux

View File

@ -1,47 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//go:build linux
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
// Copyright (C) 2024-2025 SUSE LLC
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
package linux
import (
"golang.org/x/sys/unix"
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/kernelversion"
)
// HasNewMountAPI returns whether the new fsopen(2) mount API is supported on
// the running kernel.
var HasNewMountAPI = gocompat.SyncOnceValue(func() bool {
// All of the pieces of the new mount API we use (fsopen, fsconfig,
// fsmount, open_tree) were added together in Linux 5.2[1,2], so we can
// just check for one of the syscalls and the others should also be
// available.
//
// Just try to use open_tree(2) to open a file without OPEN_TREE_CLONE.
// This is equivalent to openat(2), but tells us if open_tree is
// available (and thus all of the other basic new mount API syscalls).
// open_tree(2) is most light-weight syscall to test here.
//
// [1]: merge commit 400913252d09
// [2]: <https://lore.kernel.org/lkml/153754740781.17872.7869536526927736855.stgit@warthog.procyon.org.uk/>
fd, err := unix.OpenTree(-int(unix.EBADF), "/", unix.OPEN_TREE_CLOEXEC)
if err != nil {
return false
}
_ = unix.Close(fd)
// RHEL 8 has a backport of fsopen(2) that appears to have some very
// difficult to debug performance pathology. As such, it seems prudent to
// simply reject pre-5.2 kernels.
isNotBackport, _ := kernelversion.GreaterEqualThan(kernelversion.KernelVersion{5, 2})
return isNotBackport
})

Some files were not shown because too many files have changed in this diff Show More