mirror of
https://github.com/atuinsh/atuin.git
synced 2026-10-12 02:11:09 +00:00
feat(i18n): localize user-facing text with Fluent (#4233)
This commit is contained in:
+1
-1
@@ -3,5 +3,5 @@
|
||||
skip = .git*,*.lock,.codespellrc,CODE_OF_CONDUCT.md,CONTRIBUTORS,./vendor/axoasset
|
||||
check-hidden = true
|
||||
# ignore-regex =
|
||||
ignore-words-list = crate,ratatui,inbetween,iterm,fo,brunch,ser
|
||||
ignore-words-list = crate,ratatui,inbetween,iterm,fo,brunch,ser,writet
|
||||
|
||||
|
||||
Generated
+277
-4
@@ -149,6 +149,15 @@ dependencies = [
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.5.3"
|
||||
@@ -285,6 +294,8 @@ dependencies = [
|
||||
"eyre",
|
||||
"frizbee",
|
||||
"futures-util",
|
||||
"i18n-embed",
|
||||
"i18n-embed-fl",
|
||||
"indicatif",
|
||||
"interim",
|
||||
"itertools 0.15.0",
|
||||
@@ -302,6 +313,7 @@ dependencies = [
|
||||
"rpassword",
|
||||
"rstest",
|
||||
"runtime-format",
|
||||
"rust-embed",
|
||||
"rustix",
|
||||
"semver",
|
||||
"serde",
|
||||
@@ -374,7 +386,7 @@ dependencies = [
|
||||
"thiserror 2.0.19",
|
||||
"time",
|
||||
"tokio",
|
||||
"toml",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
"toml_edit",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
@@ -441,7 +453,7 @@ dependencies = [
|
||||
"thiserror 2.0.19",
|
||||
"time",
|
||||
"tokio",
|
||||
"toml",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
"tracing",
|
||||
"tracing-futures",
|
||||
"typed-builder",
|
||||
@@ -876,6 +888,15 @@ version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "basic-toml"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "beef"
|
||||
version = "0.5.2"
|
||||
@@ -1262,7 +1283,7 @@ checksum = "b85f248a4de22d204ceabc6299d89d2c70fbd7f09fea53c06c852369652d8139"
|
||||
dependencies = [
|
||||
"pathdiff",
|
||||
"serde_core",
|
||||
"toml",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
@@ -1284,6 +1305,12 @@ version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.10.0"
|
||||
@@ -1598,7 +1625,7 @@ version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"const-oid 0.9.6",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -1658,6 +1685,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||
dependencies = [
|
||||
"block-buffer 0.12.1",
|
||||
"const-oid 0.10.2",
|
||||
"crypto-common 0.2.2",
|
||||
"ctutils",
|
||||
]
|
||||
@@ -1972,6 +2000,15 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-crate"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2"
|
||||
dependencies = [
|
||||
"toml 0.5.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -2023,6 +2060,51 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluent"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477"
|
||||
dependencies = [
|
||||
"fluent-bundle",
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluent-bundle"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4"
|
||||
dependencies = [
|
||||
"fluent-langneg",
|
||||
"fluent-syntax",
|
||||
"intl-memoizer",
|
||||
"intl_pluralrules",
|
||||
"rustc-hash 2.1.3",
|
||||
"self_cell",
|
||||
"smallvec",
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluent-langneg"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0"
|
||||
dependencies = [
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluent-syntax"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"thiserror 2.0.19",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.12.0"
|
||||
@@ -2551,6 +2633,72 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "i18n-config"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef"
|
||||
dependencies = [
|
||||
"basic-toml",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"thiserror 1.0.69",
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "i18n-embed"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a217bbb075dcaefb292efa78897fc0678245ca67f265d12c351e42268fcb0305"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"fluent",
|
||||
"fluent-langneg",
|
||||
"fluent-syntax",
|
||||
"i18n-embed-impl",
|
||||
"intl-memoizer",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"rust-embed",
|
||||
"thiserror 1.0.69",
|
||||
"unic-langid",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "i18n-embed-fl"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "602e6bd30c3db2749e13e38b363a3d98d9d41de1d8de7a79c31bb69e45b47cda"
|
||||
dependencies = [
|
||||
"find-crate",
|
||||
"fluent",
|
||||
"fluent-syntax",
|
||||
"i18n-config",
|
||||
"i18n-embed",
|
||||
"proc-macro-error3",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.119",
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "i18n-embed-impl"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2"
|
||||
dependencies = [
|
||||
"find-crate",
|
||||
"i18n-config",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
@@ -2818,6 +2966,25 @@ dependencies = [
|
||||
"compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "intl-memoizer"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f"
|
||||
dependencies = [
|
||||
"type-map",
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "intl_pluralrules"
|
||||
version = "7.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972"
|
||||
dependencies = [
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.0"
|
||||
@@ -3232,6 +3399,16 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minijinja"
|
||||
version = "2.18.0"
|
||||
@@ -4066,6 +4243,28 @@ dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr3"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e564d14133360e1ae169ffde5da25881b5fa47261665b8e5713c212c27799da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error3"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f0d4471b3436c22106b21913b1dda531558918ae9b7ec55d58aa84b43552233"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr3",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
@@ -4710,6 +4909,41 @@ dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097"
|
||||
dependencies = [
|
||||
"mime_guess",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn 2.0.119",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0"
|
||||
dependencies = [
|
||||
"sha2 0.11.0",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
@@ -5803,6 +6037,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"serde_core",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
@@ -5885,6 +6120,15 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
@@ -6276,6 +6520,15 @@ version = "2.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5283634e518fe9e82c7b20520bb4bc209009fd16c82077c802f8111ecbb0117a"
|
||||
|
||||
[[package]]
|
||||
name = "type-map"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90"
|
||||
dependencies = [
|
||||
"rustc-hash 2.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed-builder"
|
||||
version = "0.23.2"
|
||||
@@ -6314,6 +6567,25 @@ version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
|
||||
|
||||
[[package]]
|
||||
name = "unic-langid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05"
|
||||
dependencies = [
|
||||
"unic-langid-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-langid-impl"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"tinystr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
@@ -7402,6 +7674,7 @@ version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
|
||||
@@ -85,6 +85,8 @@ glob-match = "0.2"
|
||||
http = "1"
|
||||
humantime = "2"
|
||||
hyper-util = "0.1"
|
||||
i18n-embed = { version = "0.16", features = ["fluent-system"] }
|
||||
i18n-embed-fl = "0.10"
|
||||
imara-diff = "0.2"
|
||||
indicatif = "0.18"
|
||||
interim = { version = "0.2", features = ["time_0_3"] }
|
||||
@@ -131,6 +133,7 @@ rmp = "0.8"
|
||||
rpassword = "7"
|
||||
rstest = "0.27"
|
||||
runtime-format = "0.1"
|
||||
rust-embed = "8"
|
||||
rustix = { version = "1", features = ["process", "fs", "pty", "termios", "stdio", "event"] }
|
||||
schemars = "1"
|
||||
rusty_paserk = { version = "0.5", default-features = false, features = ["v4", "serde"] }
|
||||
|
||||
@@ -75,12 +75,9 @@ profiling-traced = [
|
||||
[dependencies]
|
||||
atuin-ai = { workspace = true, optional = true }
|
||||
# TODO(taylordotfish): in Rust 1.99 we can change this to `workspace = true`
|
||||
atuin-client = {
|
||||
path = "../atuin-client",
|
||||
version = "18.23.0",
|
||||
optional = true,
|
||||
default-features = false,
|
||||
}
|
||||
# Keep inline tables on one line: i18n-embed-fl's `fl!` parses this manifest
|
||||
# with a TOML 1.0 parser (find-crate -> toml 0.5).
|
||||
atuin-client = { path = "../atuin-client", version = "18.23.0", optional = true, default-features = false }
|
||||
atuin-common = { workspace = true, features = ["unicode", "os", "ai"] }
|
||||
atuin-daemon = { workspace = true, optional = true }
|
||||
atuin-domain = { workspace = true }
|
||||
@@ -106,6 +103,8 @@ enum_dispatch = { workspace = true }
|
||||
eyre = { workspace = true }
|
||||
frizbee = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
i18n-embed = { workspace = true }
|
||||
i18n-embed-fl = { workspace = true }
|
||||
indicatif = { workspace = true }
|
||||
interim = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
@@ -118,6 +117,7 @@ ratatui = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rpassword = { workspace = true }
|
||||
runtime-format = { workspace = true }
|
||||
rust-embed = { workspace = true }
|
||||
rustix = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
fallback_language = "en-US"
|
||||
|
||||
[fluent]
|
||||
assets_dir = "i18n"
|
||||
@@ -0,0 +1,364 @@
|
||||
unrecognized-subcommand =
|
||||
unrecognized subcommand '{ $subcommand }' and no executable named '{ $bin }' found in your PATH
|
||||
cmd-contributors = List the people who have contributed to Atuin
|
||||
arg-gen-completions-shell = Set the shell for generating completions
|
||||
arg-gen-completions-out-dir = Set the output directory
|
||||
output-search-disabled = output capture is disabled. enable [output] in your config to search command output.
|
||||
output-search-empty-query = blank query provided. please run 'atuin output search --help'
|
||||
daemon-remove-socket-failed = failed to remove daemon socket { $path }: { $source }
|
||||
hook-malformed-json = hook payload is not valid JSON at line { $line }, column { $column }
|
||||
output-search-connect-failed = could not connect to the daemon
|
||||
output-search-daemon-failed = the daemon failed to search command output
|
||||
output-search-load-history-failed = could not load history from the local database
|
||||
output-search-write-failed = could not write search results
|
||||
log-otel-collector-failed = failed to build the otel collector: { $error }
|
||||
log-subscriber-init-failed = failed to initialize subscriber: { $error }
|
||||
log-file-name-not-utf8 = log file name must be utf-8
|
||||
otel-not-compiled = this build of atuin has no OpenTelemetry support: rebuild with the `profiling-traced` feature (e.g. `cargo build-traced`) to export `ATUIN_OTEL` traces
|
||||
otel-url-not-utf8 = the given ATUIN_OTEL URL does not appear to be a utf-8 string
|
||||
otel-url-invalid = the given ATUIN_OTEL failed to parse as URL: { $error }
|
||||
otel-url-not-http = the given ATUIN_OTEL URL does not appear to be an HTTP(s) URL
|
||||
otel-exporter-build-failed = failed to construct the exporter: { $error }
|
||||
account-run-logout = Run 'atuin logout' to log out.
|
||||
account-not-migrated-note = Note: Your account has not been fully migrated to Atuin Hub.
|
||||
account-not-logged-in = You are not logged in
|
||||
account-provide-password = please provide a password
|
||||
prompt-username = Please enter username
|
||||
prompt-email = Please enter email
|
||||
prompt-password = Please enter password
|
||||
prompt-current-password = Please enter the current password
|
||||
prompt-new-password = Please enter the new password
|
||||
prompt-two-factor-code = Please enter two-factor code
|
||||
prompt-key-or-existing = Please enter encryption key [blank to use existing key file]
|
||||
prompt-key-or-logout = Please enter encryption key [blank to log out and cancel]
|
||||
account-hub-authenticated = You are already authenticated with Atuin Hub.
|
||||
login-legacy-logged-in = You are logged in to your sync server.
|
||||
login-upgrading-legacy = You have a legacy sync session. Continuing login to upgrade to full Hub authentication.
|
||||
account-not-migrated-hint = Sync will continue to work, but you can visit hub.atuin.sh to create a Hub account and link it to your existing CLI account.
|
||||
login-key-generate-failed = could not load or generate encryption key
|
||||
login-success = Successfully authenticated.
|
||||
login-legacy-unexpected-2fa = unexpected two-factor requirement from legacy server
|
||||
login-legacy-success = Logged in!
|
||||
account-hub-open-url = Open this URL to authenticate with Atuin Hub:
|
||||
login-key-important = IMPORTANT
|
||||
login-key-same-everywhere = If you are already logged in on another machine, you must ensure that the key you use here is the same as the key you used there.
|
||||
login-key-find = You can find your key by running 'atuin key' on the other machine.
|
||||
login-key-secret = Do not share this key with anyone.
|
||||
login-key-read-more = Read more here: { $url }
|
||||
login-no-key-provided = No encryption key provided
|
||||
login-no-key-found = No key provided and no existing key file found. Please use 'atuin key' on your other machine, or recover your key from a backup
|
||||
login-key-file-invalid = The key in existing key file at '{ $path }' is invalid
|
||||
login-key-try-again = { $error }. Please try again.
|
||||
login-reencrypting = Re-encrypting local store with new key
|
||||
login-writing-key = Writing new key
|
||||
login-key-load-failed = could not load encryption key for verification
|
||||
login-key-mismatch = The encryption key on this machine does not match the data on the server.
|
||||
login-key-find-correct = You can find the correct key by running 'atuin key' on a machine that already syncs successfully.
|
||||
login-wrong-key-title = Wrong encryption key
|
||||
login-wrong-key-body =
|
||||
The encryption key on this machine does not match the data on the server. You have been logged out.
|
||||
|
||||
To fix this, find your existing key by running `atuin key` on a machine that already syncs successfully, then run `atuin login` again here with that key.
|
||||
register-legacy-already = You are already logged in.
|
||||
register-has-legacy-session = You already have a sync session. Run 'atuin login' to upgrade to full Hub authentication.
|
||||
register-logout-first = Run 'atuin logout' first if you want to register a new account.
|
||||
register-headless-incomplete = Username, password, and email are all required for headless registration. Continuing with interactive registration.
|
||||
register-unexpected-2fa = unexpected two-factor requirement during registration
|
||||
register-success-key = Registration successful! Please make a note of your key (run 'atuin key') and keep it safe.
|
||||
register-key-warning = You will need it to log in on other devices, and we cannot help recover it if you lose it.
|
||||
register-legacy-start = Registering for an Atuin Sync account
|
||||
link-no-cli-session = No CLI session found. Please log in first with 'atuin login'.
|
||||
link-both-sessions = Found both Hub and CLI sessions. Linking accounts...
|
||||
link-hub-login-first = Found CLI session but no Hub session. Logging in to Hub first...
|
||||
link-hub-complete = Hub authentication complete.
|
||||
link-success = Successfully linked CLI account to Hub.
|
||||
delete-provide-password = please provide your password
|
||||
delete-success = Your account is deleted
|
||||
change-password-provide-current = please provide the current password
|
||||
change-password-provide-new = please provide a new password
|
||||
change-password-success = Account password successfully changed!
|
||||
sync-key-load-failed = could not load encryption key
|
||||
sync-key-invalid = invalid key
|
||||
sync-up-down = { $uploaded }/{ $downloaded } up/down to record store
|
||||
sync-history-mismatch = { $index } in history index, but { $store } in history store
|
||||
sync-store-init = Running automatic history store init...
|
||||
sync-rerun = Re-running sync due to new records locally
|
||||
sync-complete = Sync complete! { $count } items in history database, force: { $force }
|
||||
sync-status-not-logged-in = You are not logged in to a sync server - cannot show sync status
|
||||
sync-status-version = Atuin v{ $version } - Build rev { $sha }
|
||||
sync-status-local = [Local]
|
||||
sync-status-frequency = Sync frequency: { $frequency }
|
||||
sync-status-last-sync = Last sync: { $time }
|
||||
sync-status-remote = [Remote]
|
||||
sync-status-address = Address: { $address }
|
||||
sync-status-username = Username: { $username }
|
||||
cmd-search = Interactive history search
|
||||
cmd-stats = Calculate statistics for your history
|
||||
cmd-account = Manage your sync account
|
||||
cmd-setup = Setup Atuin features
|
||||
cmd-init = Print Atuin's shell init script
|
||||
cmd-doctor = Run the doctor to check for common issues
|
||||
cmd-update = Update atuin to the latest version on your release channel
|
||||
cmd-hook = Manage AI-agent shell hooks
|
||||
cmd-mcp = Start an MCP server exposing history search to AI tools (stdio)
|
||||
cmd-wrapped = Show a fun, year-in-review recap of your shell history
|
||||
arg-wrapped-year = Year to recap (defaults to last year)
|
||||
cmd-default-config = Print the default atuin configuration (config.toml)
|
||||
cmd-info = Information about Atuin data locations and ENV vars
|
||||
cmd-daemon = {"*"}Experimental* Manage the background daemon
|
||||
cmd-__internal_ = We want to exclude the `__internal` subcommand from Clap's `infer_subcommands`; otherwise, a user could access it simply by typing `atuin _`. However, Clap has no way to disable `infer_subcommands` for a single command. As a workaround, we define a dummy command with the same name but with an extra understore, which forces `__internal` to be typed out in entirety, since any prefix of the name would be ambiguous
|
||||
cmd-account-register = Register a new account
|
||||
cmd-account-delete = Delete your account, and all synced data
|
||||
cmd-account-change-password = Change your password
|
||||
cmd-account-link = Link your CLI sync account to your Hub account
|
||||
arg-account-login-key = The encryption key for your account. Falls back to `ATUIN_ENCRYPTION_KEY`, then a prompt
|
||||
arg-totp-code = The two-factor authentication code for your account, if any
|
||||
cmd-config-get = Get a configuration value from your config.toml file or after defaults and overrides are applied
|
||||
cmd-config-set = Set a configuration value in your config.toml file
|
||||
cmd-config-enable = Enable a feature, along with everything it depends on
|
||||
cmd-config-print = Print all configuration values from your config.toml file in TOML format
|
||||
.long =
|
||||
Print all configuration values from your config.toml file in TOML format
|
||||
|
||||
If a key is provided, only print the value of that key and all its children
|
||||
arg-config-get-key = The configuration key to get
|
||||
arg-config-get-resolved = Print the value after defaults and overrides are applied
|
||||
arg-config-get-verbose = Print both the config file value and the resolved value
|
||||
arg-config-set-key = The configuration key to set
|
||||
arg-config-set-value = The value to set
|
||||
arg-config-set-the-type = Store value as an explicit type
|
||||
value-config-set-the-type-auto = Automatically determine the type of the value
|
||||
value-config-set-the-type-string = Store value as a string
|
||||
value-config-set-the-type-boolean = Store value as a boolean
|
||||
value-config-set-the-type-integer = Store value as an integer
|
||||
value-config-set-the-type-float = Store the value as a float
|
||||
arg-config-enable-feature = The feature to enable
|
||||
value-config-enable-feature-daemon = Run the daemon, autostart it, and use it for search
|
||||
value-config-enable-feature-output-capture = Capture command output, via the daemon and the pty proxy
|
||||
arg-config-print-key = Print the value of a specific key and all its children
|
||||
arg-daemon-daemonize = Internal flag for daemonization
|
||||
arg-daemon-show-logs = Also write daemon logs to the console (useful for debugging)
|
||||
cmd-daemon-start = Start the daemon server
|
||||
arg-daemon-start-force = Force start: kill existing daemon process and reset the socket
|
||||
cmd-daemon-status = Show the daemon's current status
|
||||
cmd-daemon-stop = Stop the daemon gracefully
|
||||
cmd-daemon-restart = Restart the daemon (stop, then start in background)
|
||||
value-dotfiles-alias-list-sort-by-name = Sort by alias name
|
||||
value-dotfiles-alias-list-sort-by-value = Sort by alias value
|
||||
cmd-dotfiles-alias-list = List all aliases
|
||||
arg-dotfiles-list-sort-by = Sort results by field
|
||||
arg-dotfiles-list-reverse = Sort in reverse (descending) order
|
||||
arg-dotfiles-alias-list-name = Filter aliases by name (substring match)
|
||||
arg-dotfiles-alias-list-value = Filter aliases by value (substring match)
|
||||
value-dotfiles-var-list-sort-by-name = Sort by variable name
|
||||
value-dotfiles-var-list-sort-by-value = Sort by variable value
|
||||
cmd-dotfiles-var-list = List all variables
|
||||
arg-dotfiles-var-list-name = Filter variables by name (substring match)
|
||||
arg-dotfiles-var-list-value = Filter variables by value (substring match)
|
||||
arg-dotfiles-var-list-exports-only = Show only exported variables
|
||||
arg-dotfiles-var-list-shell-only = Show only non-exported (shell) variables
|
||||
cmd-history-start = Begins a new command in the history
|
||||
arg-history-start-cmd-env = Collects the command from the `ATUIN_COMMAND_LINE` environment variable, which does not need escaping and is more compatible between OS and shells
|
||||
arg-history-start-author = Author of this command, eg `ellie`, `claude`, or `copilot`
|
||||
arg-history-start-author-kind = Whether a human or an AI agent ran this command
|
||||
.long =
|
||||
Whether a human or an AI agent ran this command
|
||||
|
||||
{"["}`Option::None`] will cause us to perform a best-guess effort.
|
||||
arg-history-start-intent = Optional intent/rationale for running this command
|
||||
arg-history-hook = Passed by shell hooks; this flag disables logging to avoid corrupting the terminal and to minimize the amount of time the command takes to run
|
||||
cmd-history-end = Finishes a new command in the history (adds time, exit code)
|
||||
cmd-history-tail = Stream history events from the daemon as they are received
|
||||
cmd-history-list = List all items in history
|
||||
arg-print0 = Terminate the output with a null, for better multiline support
|
||||
arg-history-timezone = Display the command time in another timezone other than the configured default.
|
||||
.long =
|
||||
Display the command time in another timezone other than the configured default.
|
||||
|
||||
This option takes one of the following kinds of values:
|
||||
|
||||
- the special value "local" (or "l") which refers to the system time zone
|
||||
- an offset from UTC (e.g. "+9", "-2:30")
|
||||
arg-history-list-format = Available variables: {"{"}command{"}"}, {"{"}directory{"}"}, {"{"}duration{"}"}, {"{"}user{"}"}, {"{"}host{"}"}, {"{"}author{"}"}, {"{"}intent{"}"}, {"{"}exit{"}"}, {"{"}time{"}"}, {"{"}session{"}"}, and {"{"}uuid{"}"}
|
||||
.long =
|
||||
Available variables: {"{"}command{"}"}, {"{"}directory{"}"}, {"{"}duration{"}"}, {"{"}user{"}"}, {"{"}host{"}"}, {"{"}author{"}"}, {"{"}intent{"}"}, {"{"}exit{"}"}, {"{"}time{"}"}, {"{"}session{"}"}, and {"{"}uuid{"}"}
|
||||
|
||||
Example: --format "{"{"}time{"}"} - [{"{"}duration{"}"}] - {"{"}directory{"}"}$\t{"{"}command{"}"}"
|
||||
cmd-history-last = Get the last command that was run
|
||||
arg-history-last-format = Available variables: {"{"}command{"}"}, {"{"}directory{"}"}, {"{"}duration{"}"}, {"{"}user{"}"}, {"{"}host{"}"}, {"{"}author{"}"}, {"{"}intent{"}"}, {"{"}time{"}"}, {"{"}session{"}"}, {"{"}uuid{"}"} and {"{"}relativetime{"}"}
|
||||
.long =
|
||||
Available variables: {"{"}command{"}"}, {"{"}directory{"}"}, {"{"}duration{"}"}, {"{"}user{"}"}, {"{"}host{"}"}, {"{"}author{"}"}, {"{"}intent{"}"}, {"{"}time{"}"}, {"{"}session{"}"}, {"{"}uuid{"}"} and {"{"}relativetime{"}"}.
|
||||
|
||||
Example: --format "{"{"}time{"}"} - [{"{"}duration{"}"}] - {"{"}directory{"}"}$\t{"{"}command{"}"}"
|
||||
cmd-history-prune = Delete history entries matching the configured exclusion filters
|
||||
arg-history-dry-run = List matching history lines without performing the actual deletion
|
||||
cmd-history-dedup = Delete duplicate history entries (that have the same command, cwd and hostname)
|
||||
arg-history-dedup-before = Only delete results added before this date, read in the configured timezone unless it carries an explicit offset
|
||||
arg-history-dedup-dupkeep = How many recent duplicates to keep
|
||||
cmd-hook-install = Install hooks for an AI agent to capture commands in atuin history
|
||||
arg-hook-install-agent = Agent to install hooks for (e.g., "claude-code")
|
||||
arg-hook-agent = Which agent's hook format to parse (e.g., "claude-code")
|
||||
cmd-import-auto = Import history for the current shell
|
||||
cmd-import-shell = Import history from the { $shell } history file
|
||||
cmd-import-zsh-hist-db = Import history from the zsh-histdb database
|
||||
cmd-import-nu-hist-db = Import history from the nu history database
|
||||
cmd-import-xonsh = Import history from xonsh json files
|
||||
cmd-import-xonsh-sqlite = Import history from xonsh sqlite db
|
||||
arg-init-disable-ctrl-r = Disable the binding of CTRL-R to atuin
|
||||
arg-init-disable-up-arrow = Disable the binding of the Up Arrow key to atuin
|
||||
arg-init-disable-ai = Disable the binding of ? to Atuin AI
|
||||
value-init-shell-zsh = Zsh setup
|
||||
value-init-shell-bash = Bash setup
|
||||
value-init-shell-fish = Fish setup
|
||||
value-init-shell-nu = Nu setup
|
||||
value-init-shell-xonsh = Xonsh setup
|
||||
value-init-shell-powershell = PowerShell setup
|
||||
cmd-__internal-pty-proxy-active = Check whether the current terminal belongs to a live PTY proxy
|
||||
.long =
|
||||
Check whether the current terminal belongs to a live PTY proxy.
|
||||
|
||||
Prints `0` or `1` to stdout. This command is used by shell hooks to determine whether the PTY proxy is in use.
|
||||
cmd-kv-set = Set a key-value pair
|
||||
arg-kv-set-key = Key to set
|
||||
arg-kv-set-value = Value to store (reads from stdin if not provided)
|
||||
arg-kv-namespace = Namespace for the key-value pair
|
||||
cmd-kv-delete = Delete one or more key-value pairs
|
||||
arg-kv-delete-keys = Keys to delete
|
||||
cmd-kv-get = Retrieve a saved value
|
||||
arg-kv-get-key = Key to retrieve
|
||||
cmd-kv-list = List all keys in a namespace, or in all namespaces
|
||||
arg-kv-list-namespace = Namespace to list keys from
|
||||
arg-kv-list-all-namespaces = List all keys in all namespaces
|
||||
cmd-kv-rebuild = Rebuild the KV store
|
||||
cmd-output-search = Full-text search over captured command output
|
||||
value-output-search-style-json = A single JSON array of match objects
|
||||
value-output-search-style-ndjson = Newline-delimited JSON: one match object per line
|
||||
arg-output-search-query = Words to search for; all must appear. Use `--` before a query that starts with `-`
|
||||
arg-output-search-limit = Maximum number of matches to return
|
||||
arg-output-search-context = Show only the matching lines, with this many lines of context on either side; without it, each match's whole output
|
||||
arg-output-search-style = How matches are rendered
|
||||
arg-scripts-new-last = Use the last command as the script content
|
||||
.long =
|
||||
Use the last command as the script content
|
||||
|
||||
Optionally specify a number to use the last N commands
|
||||
arg-scripts-new-no-edit = Skip opening editor when using --last
|
||||
arg-scripts-run-var = Specify template variables in the format KEY=VALUE
|
||||
.long =
|
||||
Specify template variables in the format KEY=VALUE
|
||||
|
||||
Example: -v name=John -v greeting="Hello there"
|
||||
arg-scripts-get-script = Display only the executable script with shebang
|
||||
arg-scripts-edit-tags = Replace all existing tags with these new tags
|
||||
arg-scripts-edit-no-tags = Remove all tags from the script
|
||||
arg-scripts-edit-rename = Rename the script
|
||||
arg-scripts-edit-no-edit = Skip opening editor
|
||||
arg-search-cwd = Filter search result by directory
|
||||
arg-search-exclude-cwd = Exclude directory from results
|
||||
arg-search-exit = Filter by exit code; repeat to include any of the given codes
|
||||
arg-search-exclude-exit = Exclude results with this exit code; repeat to exclude multiple codes
|
||||
arg-search-before = Only include results added before this date
|
||||
.long =
|
||||
Only include results added before this date.
|
||||
|
||||
Read in the timezone from `--timezone` (or the configured one) unless it carries an explicit offset; relative phrases like "yesterday 3pm" are anchored there too.
|
||||
arg-search-after = Only include results after this date; see `--before` for how it is interpreted
|
||||
arg-search-limit = How many entries to return at most
|
||||
arg-search-offset = Offset from the start of the results
|
||||
arg-search-interactive = Open interactive search UI
|
||||
arg-search-filter-mode = Allow overriding filter mode over config
|
||||
arg-search-search-mode = Allow overriding search mode over config
|
||||
.long =
|
||||
Allow overriding search mode over config
|
||||
|
||||
Note: for non-interactive searches, "daemon-fuzzy" behaves like "fuzzy". "skim" used to behave like "fuzzy" in non-interactive searches too; it has since been removed but is still accepted here as an alias of "fuzzy".
|
||||
arg-search-shell-up-key-binding = Marker argument used to inform atuin that it was invoked from a shell up-key binding (hidden from help to avoid confusion)
|
||||
arg-search-keymap-mode = Notify the keymap at the shell's side
|
||||
arg-search-human = Use human-readable formatting for time
|
||||
arg-cmd-only = Show only the text of the command
|
||||
arg-search-delete = Delete anything matching this query. Will not print out the match
|
||||
arg-search-delete-it-all = Delete EVERYTHING!
|
||||
arg-search-reverse = Reverse the order of results, oldest first
|
||||
arg-search-timezone =
|
||||
Timezone to display command times in and to interpret `--before`/`--after` in, instead
|
||||
of the configured default.
|
||||
.long =
|
||||
Timezone to display command times in and to interpret `--before`/`--after` in, instead
|
||||
of the configured default.
|
||||
|
||||
This option takes one of the following kinds of values:
|
||||
|
||||
- the special value "local" (or "l") which refers to the system time zone
|
||||
- an offset from UTC (e.g. "+9", "-2:30")
|
||||
arg-search-format = Available variables: {"{"}command{"}"}, {"{"}directory{"}"}, {"{"}duration{"}"}, {"{"}user{"}"}, {"{"}host{"}"}, {"{"}time{"}"}, {"{"}exit{"}"} and {"{"}relativetime{"}"}
|
||||
.long =
|
||||
Available variables: {"{"}command{"}"}, {"{"}directory{"}"}, {"{"}duration{"}"}, {"{"}user{"}"}, {"{"}host{"}"}, {"{"}time{"}"}, {"{"}exit{"}"} and {"{"}relativetime{"}"}.
|
||||
|
||||
Example: --format "{"{"}time{"}"} - [{"{"}duration{"}"}] - {"{"}directory{"}"}$\t{"{"}command{"}"}"
|
||||
arg-search-inline-height = Set the maximum number of lines Atuin's interface should take up
|
||||
arg-search-author = Filter by author. Supports $all-user (non-agents), $all-agent, or literal names
|
||||
.long =
|
||||
Filter by author. Supports $all-user (non-agents), $all-agent, or literal names.
|
||||
|
||||
Can be specified multiple times.
|
||||
arg-search-include-duplicates = Include duplicate commands in the output (non-interactive only)
|
||||
arg-search-result-file = File name to write the result to (hidden from help as this is meant to be used from a script)
|
||||
arg-search-shell = Filter by the shell that was used to run the command
|
||||
.long =
|
||||
Filter by the shell that was used to run the command
|
||||
|
||||
If passed multiple times, commands from any of the shells will be shown.
|
||||
|
||||
`--shell ""` will include commands for which the shell is unknown.
|
||||
arg-stats-period = Compute statistics for the specified period, leave blank for statistics since the beginning
|
||||
.long = Compute statistics for the specified period, leave blank for statistics since the beginning. See [this]({ $url }) for more details.
|
||||
arg-stats-count = How many top commands to list
|
||||
arg-stats-ngram-size = The number of consecutive commands to consider
|
||||
arg-stats-filter-mode = Filter commands by scope [global, host, session, directory, workspace]
|
||||
cmd-store-status = Print the current status of the record store
|
||||
cmd-store-rebuild = Rebuild a store (eg atuin store rebuild history)
|
||||
cmd-store-rekey = Re-encrypt the store with a new key (potential for data loss!)
|
||||
cmd-store-purge = Delete all records in the store that cannot be decrypted with the current key
|
||||
cmd-store-verify = Verify that all records in the store can be decrypted with the current key
|
||||
cmd-store-push = Push all records to the remote sync server (one way sync)
|
||||
cmd-store-pull = Pull records from the remote sync server (one way sync)
|
||||
arg-store-pull-force = Force push records
|
||||
.long =
|
||||
Force push records
|
||||
|
||||
This will first wipe the local store, and then download all records from the remote
|
||||
arg-store-pull-page = Page Size
|
||||
.long =
|
||||
Page Size
|
||||
|
||||
How many records to download at once. Defaults to 100
|
||||
arg-store-tag = The tag to push (eg, 'history'). Defaults to all tags
|
||||
arg-store-push-host = The host to push, in the form of a UUID host ID. Defaults to the current host
|
||||
arg-store-push-force = Force push records
|
||||
.long =
|
||||
Force push records
|
||||
|
||||
This will override both host and tag, to be all hosts and all tags. First clear the remote store, then upload all of the local store
|
||||
arg-store-push-page = Page Size
|
||||
.long =
|
||||
Page Size
|
||||
|
||||
How many records to upload at once. Defaults to 100
|
||||
arg-store-rekey-key = The new key to use for encryption. Omit for a randomly-generated key
|
||||
cmd-sync = Sync with the configured server
|
||||
arg-sync-force = Force re-download everything
|
||||
cmd-login = Login to the configured server
|
||||
cmd-logout = Log out
|
||||
cmd-register = Register with the configured server
|
||||
cmd-key = Print the encryption key for transfer to another machine
|
||||
arg-key-base64 = Switch to base64 output of the key
|
||||
cmd-status = Display the sync status
|
||||
arg-update-check = Check whether an update is available without installing it
|
||||
arg-update-version = Update (or roll back) to a specific version, e.g. "18.9.0" or "18.9.0-nightly.1", instead of the channel's latest release
|
||||
cmd-pty-proxy = PTY proxy for atuin
|
||||
cmd-uuid = Generate a UUID
|
||||
cmd-gen-completions = Generate shell completions
|
||||
cmd-atuin = Magical shell history
|
||||
arg-password = Your password, or `-` to read it from stdin. Falls back to `ATUIN_PASSWORD`, then a prompt
|
||||
login-totp-required = A two-factor code is required. Pass it with --totp-code
|
||||
account-password-stdin-failed = failed to read password from stdin
|
||||
@@ -7,6 +7,7 @@ use atuin_common::logs::{self, LogConfig};
|
||||
use clap::Subcommand;
|
||||
use eyre::{Result, WrapErr};
|
||||
|
||||
use crate::i18n::fl;
|
||||
use crate::logs::LogCtx;
|
||||
|
||||
#[cfg(feature = "sync")]
|
||||
@@ -45,7 +46,7 @@ mod wrapped;
|
||||
pub enum Cmd {
|
||||
// Variant order sets the `--help` command list order, so keep the commands users reach for
|
||||
// most (search, sync, stats) at the top and the plumbing/config commands lower down.
|
||||
/// Interactive history search
|
||||
#[command(about = fl!("cmd-search"))]
|
||||
Search(search::Cmd),
|
||||
|
||||
/// Work with captured command output
|
||||
@@ -53,14 +54,14 @@ pub enum Cmd {
|
||||
#[command(subcommand)]
|
||||
Output(output::Cmd),
|
||||
|
||||
/// Calculate statistics for your history
|
||||
#[command(about = fl!("cmd-stats"))]
|
||||
Stats(stats::Cmd),
|
||||
|
||||
#[cfg(feature = "sync")]
|
||||
#[command(flatten)]
|
||||
Sync(sync::Cmd),
|
||||
|
||||
/// Manage your sync account
|
||||
#[command(about = fl!("cmd-account"))]
|
||||
#[cfg(feature = "sync")]
|
||||
Account(account::Cmd),
|
||||
|
||||
@@ -68,25 +69,21 @@ pub enum Cmd {
|
||||
#[command(subcommand)]
|
||||
History(history::Cmd),
|
||||
|
||||
/// Setup Atuin features
|
||||
#[command()]
|
||||
#[command(about = fl!("cmd-setup"))]
|
||||
Setup,
|
||||
|
||||
/// Print Atuin's shell init script
|
||||
#[command()]
|
||||
#[command(about = fl!("cmd-init"))]
|
||||
Init(init::Cmd),
|
||||
|
||||
/// Import shell history from file
|
||||
#[command(subcommand)]
|
||||
Import(import::Cmd),
|
||||
|
||||
/// Run the doctor to check for common issues
|
||||
#[command()]
|
||||
#[command(about = fl!("cmd-doctor"))]
|
||||
Doctor,
|
||||
|
||||
/// Update atuin to the latest version on your release channel
|
||||
#[cfg(feature = "self-update")]
|
||||
#[command()]
|
||||
#[command(about = fl!("cmd-update"))]
|
||||
Update(update::Cmd),
|
||||
|
||||
/// Get or set small key-value pairs
|
||||
@@ -105,7 +102,7 @@ pub enum Cmd {
|
||||
#[command(subcommand)]
|
||||
Scripts(scripts::Cmd),
|
||||
|
||||
/// Manage AI-agent shell hooks
|
||||
#[command(about = fl!("cmd-hook"))]
|
||||
Hook(hook::Cmd),
|
||||
|
||||
/// Run the AI assistant
|
||||
@@ -113,33 +110,28 @@ pub enum Cmd {
|
||||
#[command(subcommand)]
|
||||
Ai(atuin_ai::commands::Command),
|
||||
|
||||
/// Start an MCP server exposing history search to AI tools (stdio)
|
||||
#[cfg(feature = "ai")]
|
||||
#[command()]
|
||||
#[command(about = fl!("cmd-mcp"))]
|
||||
Mcp,
|
||||
|
||||
/// Show a fun, year-in-review recap of your shell history
|
||||
#[command()]
|
||||
#[command(about = fl!("cmd-wrapped"))]
|
||||
Wrapped {
|
||||
/// Year to recap (defaults to last year)
|
||||
#[arg(help = fl!("arg-wrapped-year"))]
|
||||
year: Option<i32>,
|
||||
},
|
||||
|
||||
/// Print the default atuin configuration (config.toml)
|
||||
#[command()]
|
||||
#[command(about = fl!("cmd-default-config"))]
|
||||
DefaultConfig,
|
||||
|
||||
/// Get, set, or print values in your atuin config file
|
||||
#[command(subcommand)]
|
||||
Config(config::Cmd),
|
||||
|
||||
/// Information about Atuin data locations and ENV vars
|
||||
#[command()]
|
||||
#[command(about = fl!("cmd-info"))]
|
||||
Info,
|
||||
|
||||
/// *Experimental* Manage the background daemon
|
||||
#[cfg(feature = "daemon")]
|
||||
#[command()]
|
||||
#[command(about = fl!("cmd-daemon"))]
|
||||
Daemon(daemon::Cmd),
|
||||
|
||||
/// Internal subcommands, not for direct use by users.
|
||||
@@ -153,16 +145,12 @@ pub enum Cmd {
|
||||
)]
|
||||
Internal(internal::Cmd),
|
||||
|
||||
/// We want to exclude the `__internal` subcommand from Clap's `infer_subcommands`; otherwise,
|
||||
/// a user could access it simply by typing `atuin _`. However, Clap has no way to disable
|
||||
/// `infer_subcommands` for a single command. As a workaround, we define a dummy command with
|
||||
/// the same name but with an extra understore, which forces `__internal` to be typed out in
|
||||
/// entirety, since any prefix of the name would be ambiguous.
|
||||
#[command(
|
||||
hide = true,
|
||||
name = "__internal_",
|
||||
disable_help_flag = true,
|
||||
disable_help_subcommand = true
|
||||
disable_help_subcommand = true,
|
||||
about = fl!("cmd-__internal_")
|
||||
)]
|
||||
InternalDecoy,
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ use clap::{Args, Subcommand};
|
||||
use eyre::{Context, Result};
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
pub mod change_password;
|
||||
pub mod delete;
|
||||
pub mod link;
|
||||
@@ -49,7 +51,7 @@ impl PasswordArg {
|
||||
Self::Value(password) => Ok(Some(password.clone())),
|
||||
Self::Stdin => {
|
||||
let mut buf = String::new();
|
||||
stdin.read_to_string(&mut buf).context("failed to read password from stdin")?;
|
||||
stdin.read_to_string(&mut buf).context(fl!("account-password-stdin-failed"))?;
|
||||
Ok(Some(buf.trim_end_matches(['\r', '\n']).to_owned()))
|
||||
}
|
||||
}
|
||||
@@ -64,22 +66,22 @@ pub struct Cmd {
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum Commands {
|
||||
/// Login to the configured server
|
||||
#[command(about = fl!("cmd-login"))]
|
||||
Login(login::Cmd),
|
||||
|
||||
/// Register a new account
|
||||
#[command(about = fl!("cmd-account-register"))]
|
||||
Register(register::Cmd),
|
||||
|
||||
/// Log out
|
||||
#[command(about = fl!("cmd-logout"))]
|
||||
Logout,
|
||||
|
||||
/// Delete your account, and all synced data
|
||||
#[command(about = fl!("cmd-account-delete"))]
|
||||
Delete(delete::Cmd),
|
||||
|
||||
/// Change your password
|
||||
#[command(about = fl!("cmd-account-change-password"))]
|
||||
ChangePassword(change_password::Cmd),
|
||||
|
||||
/// Link your CLI sync account to your Hub account
|
||||
#[command(about = fl!("cmd-account-link"))]
|
||||
Link,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ use clap::Parser;
|
||||
use eyre::{Result, bail};
|
||||
use rpassword::prompt_password;
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct Cmd {
|
||||
#[clap(long, short)]
|
||||
@@ -12,34 +14,34 @@ pub struct Cmd {
|
||||
#[clap(long, short)]
|
||||
pub new_password: Option<String>,
|
||||
|
||||
/// The two-factor authentication code for your account, if any
|
||||
#[clap(long, short)]
|
||||
#[clap(long, short, help = fl!("arg-totp-code"))]
|
||||
pub totp_code: Option<String>,
|
||||
}
|
||||
|
||||
impl Cmd {
|
||||
pub async fn run(&self, settings: &Settings) -> Result<()> {
|
||||
if !settings.logged_in().await? {
|
||||
bail!("You are not logged in");
|
||||
bail!(fl!("account-not-logged-in"));
|
||||
}
|
||||
|
||||
let client = auth::auth_client(settings).await;
|
||||
|
||||
let current_password = self.current_password.clone().unwrap_or_else(|| {
|
||||
prompt_password("Please enter the current password: ")
|
||||
prompt_password(format!("{}: ", fl!("prompt-current-password")))
|
||||
.expect("Failed to read from input")
|
||||
});
|
||||
|
||||
if current_password.is_empty() {
|
||||
bail!("please provide the current password");
|
||||
bail!(fl!("change-password-provide-current"));
|
||||
}
|
||||
|
||||
let new_password = self.new_password.clone().unwrap_or_else(|| {
|
||||
prompt_password("Please enter the new password: ").expect("Failed to read from input")
|
||||
prompt_password(format!("{}: ", fl!("prompt-new-password")))
|
||||
.expect("Failed to read from input")
|
||||
});
|
||||
|
||||
if new_password.is_empty() {
|
||||
bail!("please provide a new password");
|
||||
bail!(fl!("change-password-provide-new"));
|
||||
}
|
||||
|
||||
let mut totp_code = self.totp_code.clone();
|
||||
@@ -52,12 +54,13 @@ impl Cmd {
|
||||
match response {
|
||||
MutateResponse::Success => break,
|
||||
MutateResponse::TwoFactorRequired => {
|
||||
totp_code = Some(super::login::or_user_input(None, "two-factor code"));
|
||||
totp_code =
|
||||
Some(super::login::or_user_input(None, &fl!("prompt-two-factor-code")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Account password successfully changed!");
|
||||
println!("{}", fl!("change-password-success"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,21 +4,21 @@ use clap::Parser;
|
||||
use eyre::{Result, bail};
|
||||
|
||||
use super::login::{or_user_input, read_user_password};
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct Cmd {
|
||||
#[clap(long, short)]
|
||||
pub password: Option<String>,
|
||||
|
||||
/// The two-factor authentication code for your account, if any
|
||||
#[clap(long, short)]
|
||||
#[clap(long, short, help = fl!("arg-totp-code"))]
|
||||
pub totp_code: Option<String>,
|
||||
}
|
||||
|
||||
impl Cmd {
|
||||
pub async fn run(&self, settings: &Settings) -> Result<()> {
|
||||
if !settings.logged_in().await? {
|
||||
bail!("You are not logged in");
|
||||
bail!(fl!("account-not-logged-in"));
|
||||
}
|
||||
|
||||
let client = auth::auth_client(settings).await;
|
||||
@@ -26,7 +26,7 @@ impl Cmd {
|
||||
let password = self.password.clone().unwrap_or_else(read_user_password);
|
||||
|
||||
if password.is_empty() {
|
||||
bail!("please provide your password");
|
||||
bail!(fl!("delete-provide-password"));
|
||||
}
|
||||
|
||||
let mut totp_code = self.totp_code.clone();
|
||||
@@ -37,7 +37,7 @@ impl Cmd {
|
||||
match response {
|
||||
MutateResponse::Success => break,
|
||||
MutateResponse::TwoFactorRequired => {
|
||||
totp_code = Some(or_user_input(None, "two-factor code"));
|
||||
totp_code = Some(or_user_input(None, &fl!("prompt-two-factor-code")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ impl Cmd {
|
||||
meta.delete_session().await?;
|
||||
meta.delete_hub_session().await?;
|
||||
|
||||
println!("Your account is deleted");
|
||||
println!("{}", fl!("delete-success"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use atuin_client::settings::Settings;
|
||||
use eyre::{Result, bail};
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
pub async fn run(settings: &Settings) -> Result<()> {
|
||||
let meta = Settings::meta_store().await?;
|
||||
|
||||
@@ -8,18 +10,18 @@ pub async fn run(settings: &Settings) -> Result<()> {
|
||||
let hub_token = meta.hub_session_token().await?;
|
||||
|
||||
let Some(cli_token) = cli_token else {
|
||||
bail!("No CLI session found. Please log in first with 'atuin login'.");
|
||||
bail!(fl!("link-no-cli-session"));
|
||||
};
|
||||
|
||||
let hub_address = settings.hub_endpoint();
|
||||
|
||||
if hub_token.is_some() {
|
||||
println!("Found both Hub and CLI sessions. Linking accounts...");
|
||||
println!("{}", fl!("link-both-sessions"));
|
||||
} else {
|
||||
println!("Found CLI session but no Hub session. Logging in to Hub first...");
|
||||
println!("{}", fl!("link-hub-login-first"));
|
||||
|
||||
let session = atuin_client::hub::HubAuthSession::start(&hub_address).await?;
|
||||
println!("Open this URL to authenticate with Atuin Hub:");
|
||||
println!("{}", fl!("account-hub-open-url"));
|
||||
println!("{}", session.auth_url);
|
||||
|
||||
let token = session
|
||||
@@ -30,11 +32,11 @@ pub async fn run(settings: &Settings) -> Result<()> {
|
||||
.await?;
|
||||
|
||||
atuin_client::hub::save_session(&token).await?;
|
||||
println!("Hub authentication complete.");
|
||||
println!("{}", fl!("link-hub-complete"));
|
||||
}
|
||||
|
||||
atuin_client::hub::link_account(&hub_address, &cli_token).await?;
|
||||
println!("Successfully linked CLI account to Hub.");
|
||||
println!("{}", fl!("link-success"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use eyre::{Context, Result, bail};
|
||||
use rpassword::prompt_password;
|
||||
|
||||
use super::PasswordArg;
|
||||
use crate::i18n::fl;
|
||||
|
||||
const KEY_ENV: &str = "ATUIN_ENCRYPTION_KEY";
|
||||
|
||||
@@ -19,16 +20,13 @@ pub struct Cmd {
|
||||
#[clap(long, short)]
|
||||
pub username: Option<String>,
|
||||
|
||||
/// Your password, or `-` to read it from stdin. Falls back to `ATUIN_PASSWORD`, then a prompt
|
||||
#[clap(long, short)]
|
||||
#[clap(long, short, help = fl!("arg-password"))]
|
||||
pub password: Option<PasswordArg>,
|
||||
|
||||
/// The encryption key for your account. Falls back to `ATUIN_ENCRYPTION_KEY`, then a prompt
|
||||
#[clap(long, short)]
|
||||
#[clap(long, short, help = fl!("arg-account-login-key"))]
|
||||
pub key: Option<String>,
|
||||
|
||||
/// The two-factor authentication code for your account, if any
|
||||
#[clap(long, short)]
|
||||
#[clap(long, short, help = fl!("arg-totp-code"))]
|
||||
pub totp_code: Option<String>,
|
||||
|
||||
#[clap(long, hide = true)]
|
||||
@@ -50,20 +48,17 @@ impl Cmd {
|
||||
pub async fn run(&self, settings: &Settings, store: &SqliteStore) -> Result<()> {
|
||||
match settings.resolve_sync_auth().await {
|
||||
SyncAuth::Hub { .. } => {
|
||||
println!("You are authenticated with Atuin Hub.");
|
||||
println!("Run 'atuin logout' to log out.");
|
||||
println!("{}", fl!("account-hub-authenticated"));
|
||||
println!("{}", fl!("account-run-logout"));
|
||||
return Ok(());
|
||||
}
|
||||
SyncAuth::Legacy { .. } => {
|
||||
println!("You are logged in to your sync server.");
|
||||
println!("Run 'atuin logout' to log out.");
|
||||
println!("{}", fl!("login-legacy-logged-in"));
|
||||
println!("{}", fl!("account-run-logout"));
|
||||
return Ok(());
|
||||
}
|
||||
SyncAuth::HubViaCli { .. } => {
|
||||
println!(
|
||||
"You have a legacy sync session. Continuing login to upgrade to full Hub \
|
||||
authentication."
|
||||
);
|
||||
println!("{}", fl!("login-upgrading-legacy"));
|
||||
}
|
||||
SyncAuth::NotLoggedIn { .. } => {}
|
||||
}
|
||||
@@ -107,8 +102,8 @@ impl Cmd {
|
||||
match response {
|
||||
AuthResponse::Success { session, auth_type } => break (session, auth_type),
|
||||
AuthResponse::TwoFactorRequired => {
|
||||
let Some(code) = read_user_input("two-factor code") else {
|
||||
bail!("A two-factor code is required. Pass it with --totp-code");
|
||||
let Some(code) = read_user_input(&fl!("prompt-two-factor-code")) else {
|
||||
bail!(fl!("login-totp-required"));
|
||||
};
|
||||
totp_code = Some(code);
|
||||
}
|
||||
@@ -122,17 +117,14 @@ impl Cmd {
|
||||
meta.save_hub_session(&session).await?;
|
||||
} else {
|
||||
meta.save_session(&session).await?;
|
||||
println!("\nNote: Your account has not been fully migrated to Atuin Hub.");
|
||||
println!(
|
||||
"Sync will continue to work, but you can visit hub.atuin.sh to create an \
|
||||
account and link it to your existing CLI account."
|
||||
);
|
||||
println!("\n{}", fl!("account-not-migrated-note"));
|
||||
println!("{}", fl!("account-not-migrated-hint"));
|
||||
}
|
||||
} else {
|
||||
// Interactive login via browser OAuth flow.
|
||||
if self.from_registration {
|
||||
paseto_v4::Key::try_load_or_generate(&settings.key_path)
|
||||
.context("could not load or generate encryption key")?;
|
||||
.context(fl!("login-key-generate-failed"))?;
|
||||
} else {
|
||||
self.prompt_and_store_key(settings, store).await?;
|
||||
}
|
||||
@@ -147,14 +139,14 @@ impl Cmd {
|
||||
tracing::debug!("Could not link CLI account to Hub: {}", e);
|
||||
}
|
||||
|
||||
println!("Successfully authenticated.");
|
||||
println!("{}", fl!("login-success"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Legacy login: always prompt for username/password interactively
|
||||
/// (or accept them via flags).
|
||||
async fn run_legacy_login(&self, settings: &Settings, store: &SqliteStore) -> Result<()> {
|
||||
let username = or_user_input(self.username.clone(), "username");
|
||||
let username = or_user_input(self.username.clone(), &fl!("prompt-username"));
|
||||
let password = PasswordArg::resolve(self.password.as_ref(), io::stdin().lock())?
|
||||
.unwrap_or_else(read_user_password);
|
||||
|
||||
@@ -169,11 +161,11 @@ impl Cmd {
|
||||
}
|
||||
AuthResponse::TwoFactorRequired => {
|
||||
// Legacy server doesn't support 2FA, so this shouldn't happen.
|
||||
bail!("unexpected two-factor requirement from legacy server");
|
||||
bail!(fl!("login-legacy-unexpected-2fa"));
|
||||
}
|
||||
}
|
||||
|
||||
println!("Logged in!");
|
||||
println!("{}", fl!("login-legacy-success"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -181,7 +173,7 @@ impl Cmd {
|
||||
tracing::info!("Authenticating with Atuin Hub...");
|
||||
|
||||
let session = atuin_client::hub::HubAuthSession::start(hub_address).await?;
|
||||
println!("Open this URL to continue authenticating with Atuin Hub:");
|
||||
println!("{}", fl!("account-hub-open-url"));
|
||||
println!("{}", session.auth_url);
|
||||
|
||||
let token = session
|
||||
@@ -201,14 +193,14 @@ impl Cmd {
|
||||
async fn prompt_and_store_key(&self, settings: &Settings, store: &SqliteStore) -> Result<()> {
|
||||
let key_path = &settings.key_path;
|
||||
|
||||
println!("IMPORTANT");
|
||||
println!("{}", fl!("login-key-important"));
|
||||
println!("{}", fl!("login-key-same-everywhere"));
|
||||
println!("{}", fl!("login-key-find"));
|
||||
println!("{}", fl!("login-key-secret"));
|
||||
println!(
|
||||
"If you are already logged in on another machine, you must ensure that the key you \
|
||||
use here is the same as the key you used there."
|
||||
"\n{} \n",
|
||||
fl!("login-key-read-more", url = atuin_common::docs::url("guide/sync/#login"))
|
||||
);
|
||||
println!("You can find your key by running 'atuin key' on the other machine.");
|
||||
println!("Do not share this key with anyone.");
|
||||
println!("\nRead more here: {} \n", atuin_common::docs::url("guide/sync/#login"));
|
||||
|
||||
let interactive = self.interactive();
|
||||
let mut flag_key = self.scripted_key();
|
||||
@@ -216,17 +208,16 @@ impl Cmd {
|
||||
loop {
|
||||
let key = match flag_key.take() {
|
||||
Some(key) => key,
|
||||
None => match read_user_input("encryption key [blank to use existing key file]") {
|
||||
None => match read_user_input(&fl!("prompt-key-or-existing")) {
|
||||
Some(key) => key,
|
||||
// Stdin is exhausted, so re-prompting would spin forever.
|
||||
None => bail!("No encryption key provided"),
|
||||
None => bail!(fl!("login-no-key-provided")),
|
||||
},
|
||||
};
|
||||
|
||||
if key.is_empty() {
|
||||
if !key_path.exists() {
|
||||
let msg = "No key provided and no existing key file found. Please use 'atuin \
|
||||
key' on your other machine, or recover your key from a backup";
|
||||
let msg = fl!("login-no-key-found");
|
||||
if !interactive {
|
||||
bail!(msg);
|
||||
}
|
||||
@@ -234,9 +225,9 @@ impl Cmd {
|
||||
continue;
|
||||
}
|
||||
|
||||
paseto_v4::Key::try_load_from_path(key_path).context(format!(
|
||||
"The key in existing key file at '{}' is invalid",
|
||||
key_path.to_string_lossy()
|
||||
paseto_v4::Key::try_load_from_path(key_path).context(fl!(
|
||||
"login-key-file-invalid",
|
||||
path = key_path.to_string_lossy().into_owned()
|
||||
))?;
|
||||
|
||||
return Ok(());
|
||||
@@ -245,7 +236,9 @@ impl Cmd {
|
||||
// The key may be EITHER base64 or a bip39 mnemonic.
|
||||
match paseto_v4::Key::try_from_mnemonic(&key) {
|
||||
Ok(key) => return store_key(settings, store, &key).await,
|
||||
Err(err) if interactive => println!("\n{err}. Please try again.\n"),
|
||||
Err(err) if interactive => {
|
||||
println!("\n{}\n", fl!("login-key-try-again", error = err.to_string()));
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
}
|
||||
@@ -267,10 +260,10 @@ async fn store_key(settings: &Settings, store: &SqliteStore, key: &paseto_v4::Ke
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("\nRe-encrypting local store with new key");
|
||||
println!("\n{}", fl!("login-reencrypting"));
|
||||
store.re_encrypt(¤t_key, key).await?;
|
||||
|
||||
println!("Writing new key");
|
||||
println!("{}", fl!("login-writing-key"));
|
||||
key.overwrite_path(key_path)?;
|
||||
|
||||
Ok(())
|
||||
@@ -282,7 +275,7 @@ async fn verify_key_against_remote(
|
||||
interactive: bool,
|
||||
) -> Result<()> {
|
||||
let mut key = paseto_v4::Key::try_load_from_path(&settings.key_path)
|
||||
.context("could not load encryption key for verification")?;
|
||||
.context(fl!("login-key-load-failed"))?;
|
||||
|
||||
// Build the session once (this hits the network). The key can change between retries below, so
|
||||
// each iteration re-keys the shared session rather than reconnecting.
|
||||
@@ -306,20 +299,20 @@ async fn verify_key_against_remote(
|
||||
logout_wrong_key().await;
|
||||
}
|
||||
|
||||
println!(
|
||||
"\nThe encryption key on this machine does not match the data on the server."
|
||||
);
|
||||
println!(
|
||||
"You can find the correct key by running 'atuin key' on a machine that \
|
||||
already syncs successfully."
|
||||
);
|
||||
println!("\n{}", fl!("login-key-mismatch"));
|
||||
println!("{}", fl!("login-key-find-correct"));
|
||||
|
||||
let input = read_user_input("encryption key [blank to log out and cancel]");
|
||||
let input = read_user_input(&fl!("prompt-key-or-logout"));
|
||||
match input {
|
||||
Some(input) if !input.is_empty() => {
|
||||
match paseto_v4::Key::try_from_mnemonic(&input) {
|
||||
Ok(candidate) => key = candidate,
|
||||
Err(err) => println!("\n{err}. Please try again."),
|
||||
Err(err) => {
|
||||
println!(
|
||||
"\n{}",
|
||||
fl!("login-key-try-again", error = err.to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// A blank line or exhausted stdin both mean "give up".
|
||||
@@ -344,29 +337,24 @@ async fn logout_wrong_key() -> ! {
|
||||
let _ = meta.delete_session().await;
|
||||
let _ = meta.delete_hub_session().await;
|
||||
}
|
||||
crate::print_error::print_error(
|
||||
"Wrong encryption key",
|
||||
"The encryption key on this machine does not match the data on the server. You have been \
|
||||
logged out.\n\nTo fix this, find your existing key by running `atuin key` on a machine \
|
||||
that already syncs successfully, then run `atuin login` again here with that key.",
|
||||
);
|
||||
crate::print_error::print_error(&fl!("login-wrong-key-title"), &fl!("login-wrong-key-body"));
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(super) fn or_user_input(value: Option<String>, name: &'static str) -> String {
|
||||
value.unwrap_or_else(|| read_user_input(name).unwrap_or_default())
|
||||
pub(super) fn or_user_input(value: Option<String>, prompt: &str) -> String {
|
||||
value.unwrap_or_else(|| read_user_input(prompt).unwrap_or_default())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(super) fn read_user_password() -> String {
|
||||
let password = prompt_password("Please enter password: ");
|
||||
let password = prompt_password(format!("{}: ", fl!("prompt-password")));
|
||||
password.expect("Failed to read from input")
|
||||
}
|
||||
|
||||
/// Returns `None` if stdin reached end of input before a line was read.
|
||||
fn read_user_input(name: &'static str) -> Option<String> {
|
||||
eprint!("Please enter {name}: ");
|
||||
fn read_user_input(prompt: &str) -> Option<String> {
|
||||
eprint!("{prompt}: ");
|
||||
get_input().expect("Failed to read from input")
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@ use eyre::{Result, bail};
|
||||
|
||||
use super::PasswordArg;
|
||||
use super::login::or_user_input;
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct Cmd {
|
||||
#[clap(long, short)]
|
||||
pub username: Option<String>,
|
||||
|
||||
/// Your password, or `-` to read it from stdin. Falls back to `ATUIN_PASSWORD`, then a prompt
|
||||
#[clap(long, short)]
|
||||
#[clap(long, short, help = fl!("arg-password"))]
|
||||
pub password: Option<PasswordArg>,
|
||||
|
||||
#[clap(long, short)]
|
||||
@@ -26,21 +26,18 @@ impl Cmd {
|
||||
pub async fn run(&self, settings: &Settings, store: &SqliteStore) -> Result<()> {
|
||||
match settings.resolve_sync_auth().await {
|
||||
SyncAuth::Hub { .. } => {
|
||||
println!("You are already authenticated with Atuin Hub.");
|
||||
println!("Run 'atuin logout' to log out.");
|
||||
println!("{}", fl!("account-hub-authenticated"));
|
||||
println!("{}", fl!("account-run-logout"));
|
||||
return Ok(());
|
||||
}
|
||||
SyncAuth::Legacy { .. } => {
|
||||
println!("You are already logged in.");
|
||||
println!("Run 'atuin logout' to log out.");
|
||||
println!("{}", fl!("register-legacy-already"));
|
||||
println!("{}", fl!("account-run-logout"));
|
||||
return Ok(());
|
||||
}
|
||||
SyncAuth::HubViaCli { .. } => {
|
||||
println!(
|
||||
"You already have a sync session. Run 'atuin login' to upgrade to full Hub \
|
||||
authentication."
|
||||
);
|
||||
println!("Run 'atuin logout' first if you want to register a new account.");
|
||||
println!("{}", fl!("register-has-legacy-session"));
|
||||
println!("{}", fl!("register-logout-first"));
|
||||
return Ok(());
|
||||
}
|
||||
SyncAuth::NotLoggedIn { .. } => {}
|
||||
@@ -63,7 +60,7 @@ impl Cmd {
|
||||
let client = auth::auth_client(settings).await;
|
||||
|
||||
if password.is_empty() {
|
||||
bail!("please provide a password");
|
||||
bail!(fl!("account-provide-password"));
|
||||
}
|
||||
|
||||
let response = client.register(username, email, password).await?;
|
||||
@@ -78,36 +75,21 @@ impl Cmd {
|
||||
meta.save_hub_session(&session).await?;
|
||||
} else {
|
||||
meta.save_session(&session).await?;
|
||||
println!(
|
||||
"\nNote: Your account has not been fully migrated to Atuin Hub."
|
||||
);
|
||||
println!(
|
||||
"Sync will continue to work, but you can visit hub.atuin.sh to \
|
||||
create a new Hub account and link it to your existing CLI \
|
||||
account."
|
||||
);
|
||||
println!("\n{}", fl!("account-not-migrated-note"));
|
||||
println!("{}", fl!("account-not-migrated-hint"));
|
||||
}
|
||||
}
|
||||
AuthResponse::TwoFactorRequired => {
|
||||
bail!("unexpected two-factor requirement during registration");
|
||||
bail!(fl!("register-unexpected-2fa"));
|
||||
}
|
||||
}
|
||||
|
||||
let _key = paseto_v4::Key::try_load_or_generate(&settings.key_path)?;
|
||||
|
||||
println!(
|
||||
"Registration successful! Please make a note of your key (run 'atuin key') \
|
||||
and keep it safe."
|
||||
);
|
||||
println!(
|
||||
"You will need it to log in on other devices, and we cannot help recover it \
|
||||
if you lose it."
|
||||
);
|
||||
println!("{}", fl!("register-success-key"));
|
||||
println!("{}", fl!("register-key-warning"));
|
||||
} else {
|
||||
println!(
|
||||
"Username, password, and email are all required for headless registration. \
|
||||
Continuing with interactive registration.\n"
|
||||
);
|
||||
println!("{}\n", fl!("register-headless-incomplete"));
|
||||
|
||||
// Interactive registration: delegate to the browser OAuth flow.
|
||||
// Registration on Hub happens on the website; the CLI just needs
|
||||
@@ -124,15 +106,15 @@ impl Cmd {
|
||||
}
|
||||
} else {
|
||||
// Legacy registration flow
|
||||
println!("Registering for an Atuin Sync account");
|
||||
println!("{}", fl!("register-legacy-start"));
|
||||
|
||||
let username = or_user_input(self.username.clone(), "username");
|
||||
let email = or_user_input(self.email.clone(), "email");
|
||||
let username = or_user_input(self.username.clone(), &fl!("prompt-username"));
|
||||
let email = or_user_input(self.email.clone(), &fl!("prompt-email"));
|
||||
let password = PasswordArg::resolve(self.password.as_ref(), std::io::stdin().lock())?
|
||||
.unwrap_or_else(super::login::read_user_password);
|
||||
|
||||
if password.is_empty() {
|
||||
bail!("please provide a password");
|
||||
bail!(fl!("account-provide-password"));
|
||||
}
|
||||
|
||||
let session = atuin_client::api_client::register(
|
||||
@@ -149,14 +131,8 @@ impl Cmd {
|
||||
|
||||
let _key = paseto_v4::Key::try_load_or_generate(&settings.key_path)?;
|
||||
|
||||
println!(
|
||||
"Registration successful! Please make a note of your key (run 'atuin key') and \
|
||||
keep it safe."
|
||||
);
|
||||
println!(
|
||||
"You will need it to log in on other devices, and we cannot help recover it if \
|
||||
you lose it."
|
||||
);
|
||||
println!("{}", fl!("register-success-key"));
|
||||
println!("{}", fl!("register-key-warning"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -6,28 +6,21 @@ use tracing::instrument;
|
||||
|
||||
#[cfg(feature = "daemon")]
|
||||
use crate::command::client::daemon;
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
#[command(infer_subcommands = true)]
|
||||
pub enum Cmd {
|
||||
/// Get a configuration value from your config.toml file
|
||||
/// or after defaults and overrides are applied
|
||||
#[command()]
|
||||
#[command(about = fl!("cmd-config-get"))]
|
||||
Get(GetCmd),
|
||||
|
||||
/// Set a configuration value in your config.toml file
|
||||
#[command()]
|
||||
#[command(about = fl!("cmd-config-set"))]
|
||||
Set(SetCmd),
|
||||
|
||||
/// Enable a feature, along with everything it depends on
|
||||
#[command()]
|
||||
#[command(about = fl!("cmd-config-enable"))]
|
||||
Enable(EnableCmd),
|
||||
|
||||
/// Print all configuration values from your config.toml file
|
||||
/// in TOML format
|
||||
///
|
||||
/// If a key is provided, only print the value of that key and all its children
|
||||
#[command()]
|
||||
#[command(about = fl!("cmd-config-print"), long_about = fl!("cmd-config-print", "long"))]
|
||||
Print(PrintCmd),
|
||||
}
|
||||
|
||||
@@ -47,15 +40,13 @@ impl Cmd {
|
||||
/// or optionally the effective value after defaults and overrides are applied.
|
||||
#[derive(Args, Debug)]
|
||||
pub struct GetCmd {
|
||||
/// The configuration key to get
|
||||
#[arg(help = fl!("arg-config-get-key"))]
|
||||
pub key: String,
|
||||
|
||||
/// Print the value after defaults and overrides are applied
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-config-get-resolved"))]
|
||||
pub resolved: bool,
|
||||
|
||||
/// Print both the config file value and the resolved value
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-config-get-verbose"))]
|
||||
pub verbose: bool,
|
||||
}
|
||||
|
||||
@@ -126,28 +117,34 @@ impl GetCmd {
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct SetCmd {
|
||||
/// The configuration key to set
|
||||
#[arg(help = fl!("arg-config-set-key"))]
|
||||
pub key: String,
|
||||
|
||||
/// The value to set
|
||||
#[arg(help = fl!("arg-config-set-value"))]
|
||||
pub value: String,
|
||||
|
||||
/// Store value as an explicit type
|
||||
#[arg(long = "type", short, value_enum, default_value_t = ValueType::Auto, value_name = "TYPE")]
|
||||
#[arg(
|
||||
long = "type",
|
||||
short,
|
||||
value_enum,
|
||||
default_value_t = ValueType::Auto,
|
||||
value_name = "TYPE",
|
||||
help = fl!("arg-config-set-the-type")
|
||||
)]
|
||||
pub the_type: ValueType,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ValueType {
|
||||
/// Automatically determine the type of the value
|
||||
#[value(help = fl!("value-config-set-the-type-auto"))]
|
||||
Auto,
|
||||
/// Store value as a string
|
||||
#[value(help = fl!("value-config-set-the-type-string"))]
|
||||
String,
|
||||
/// Store value as a boolean
|
||||
#[value(help = fl!("value-config-set-the-type-boolean"))]
|
||||
Boolean,
|
||||
/// Store value as an integer
|
||||
#[value(help = fl!("value-config-set-the-type-integer"))]
|
||||
Integer,
|
||||
/// Store the value as a float
|
||||
#[value(help = fl!("value-config-set-the-type-float"))]
|
||||
Float,
|
||||
}
|
||||
|
||||
@@ -232,16 +229,15 @@ impl SetCmd {
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct EnableCmd {
|
||||
/// The feature to enable
|
||||
#[arg(value_enum)]
|
||||
#[arg(value_enum, help = fl!("arg-config-enable-feature"))]
|
||||
pub feature: Feature,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Feature {
|
||||
/// Run the daemon, autostart it, and use it for search
|
||||
#[value(help = fl!("value-config-enable-feature-daemon"))]
|
||||
Daemon,
|
||||
/// Capture command output, via the daemon and the pty proxy
|
||||
#[value(help = fl!("value-config-enable-feature-output-capture"))]
|
||||
OutputCapture,
|
||||
}
|
||||
|
||||
@@ -301,7 +297,7 @@ impl EnableCmd {
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct PrintCmd {
|
||||
/// Print the value of a specific key and all its children
|
||||
#[arg(help = fl!("arg-config-print-key"))]
|
||||
pub key: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -18,14 +18,14 @@ use clap::Subcommand;
|
||||
use daemonix::Daemonize;
|
||||
use eyre::{Result, WrapErr, bail, eyre};
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(clap::Args, Debug)]
|
||||
pub struct Cmd {
|
||||
/// Internal flag for daemonization
|
||||
#[arg(long, hide = true)]
|
||||
#[arg(long, hide = true, help = fl!("arg-daemon-daemonize"))]
|
||||
daemonize: bool,
|
||||
|
||||
/// Also write daemon logs to the console (useful for debugging)
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-daemon-show-logs"))]
|
||||
show_logs: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
@@ -35,27 +35,25 @@ pub struct Cmd {
|
||||
#[derive(Subcommand, Debug)]
|
||||
#[command(infer_subcommands = true)]
|
||||
pub enum SubCmd {
|
||||
/// Start the daemon server
|
||||
#[command(about = fl!("cmd-daemon-start"))]
|
||||
Start {
|
||||
#[arg(long, hide = true)]
|
||||
daemonize: bool,
|
||||
|
||||
/// Also write daemon logs to the console (useful for debugging)
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-daemon-show-logs"))]
|
||||
show_logs: bool,
|
||||
|
||||
/// Force start: kill existing daemon process and reset the socket
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-daemon-start-force"))]
|
||||
force: bool,
|
||||
},
|
||||
|
||||
/// Show the daemon's current status
|
||||
#[command(about = fl!("cmd-daemon-status"))]
|
||||
Status,
|
||||
|
||||
/// Stop the daemon gracefully
|
||||
#[command(about = fl!("cmd-daemon-stop"))]
|
||||
Stop,
|
||||
|
||||
/// Restart the daemon (stop, then start in background)
|
||||
#[command(about = fl!("cmd-daemon-restart"))]
|
||||
Restart,
|
||||
}
|
||||
|
||||
@@ -276,7 +274,14 @@ fn startup_timeout(settings: &Settings) -> Duration {
|
||||
/// An error that occurred while trying to remove a socket.
|
||||
#[cfg(unix)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("failed to remove daemon socket {}: {source}", .path.display())]
|
||||
#[error(
|
||||
"{}",
|
||||
fl!(
|
||||
"daemon-remove-socket-failed",
|
||||
path = .path.display().to_string(),
|
||||
source = .source.to_string()
|
||||
)
|
||||
)]
|
||||
struct RemoveSocketError {
|
||||
path: PathBuf,
|
||||
source: std::io::Error,
|
||||
@@ -681,6 +686,19 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[cfg(unix)]
|
||||
#[rstest]
|
||||
fn remove_socket_error_names_the_path_and_cause() {
|
||||
let err = RemoveSocketError {
|
||||
path: PathBuf::from("/run/atuin.sock"),
|
||||
source: std::io::Error::from(std::io::ErrorKind::PermissionDenied),
|
||||
};
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"failed to remove daemon socket /run/atuin.sock: permission denied"
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::matches(DAEMON_VERSION, DAEMON_PROTOCOL_VERSION, true)]
|
||||
#[case::wrong_version("0.0.0", DAEMON_PROTOCOL_VERSION, false)]
|
||||
|
||||
@@ -5,34 +5,37 @@ use atuin_dotfiles::store::AliasStore;
|
||||
use clap::{Subcommand, ValueEnum};
|
||||
use eyre::{Context, Result};
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, ValueEnum)]
|
||||
pub enum SortBy {
|
||||
/// Sort by alias name
|
||||
#[value(help = fl!("value-dotfiles-alias-list-sort-by-name"))]
|
||||
#[default]
|
||||
Name,
|
||||
/// Sort by alias value
|
||||
#[value(help = fl!("value-dotfiles-alias-list-sort-by-value"))]
|
||||
Value,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
#[command(infer_subcommands = true)]
|
||||
pub enum Cmd {
|
||||
/// List all aliases
|
||||
#[command(about = fl!("cmd-dotfiles-alias-list"))]
|
||||
List {
|
||||
/// Sort results by field
|
||||
#[arg(long, value_enum, default_value_t = SortBy::Name)]
|
||||
#[arg(
|
||||
long,
|
||||
value_enum,
|
||||
default_value_t = SortBy::Name,
|
||||
help = fl!("arg-dotfiles-list-sort-by")
|
||||
)]
|
||||
sort_by: SortBy,
|
||||
|
||||
/// Sort in reverse (descending) order
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-dotfiles-list-reverse"))]
|
||||
reverse: bool,
|
||||
|
||||
/// Filter aliases by name (substring match)
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-dotfiles-alias-list-name"))]
|
||||
name: Option<String>,
|
||||
|
||||
/// Filter aliases by value (substring match)
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-dotfiles-alias-list-value"))]
|
||||
value: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,42 +5,51 @@ use atuin_dotfiles::store::var::VarStore;
|
||||
use clap::{Subcommand, ValueEnum};
|
||||
use eyre::{Context, Result};
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, ValueEnum)]
|
||||
pub enum SortBy {
|
||||
/// Sort by variable name
|
||||
#[value(help = fl!("value-dotfiles-var-list-sort-by-name"))]
|
||||
#[default]
|
||||
Name,
|
||||
/// Sort by variable value
|
||||
#[value(help = fl!("value-dotfiles-var-list-sort-by-value"))]
|
||||
Value,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
#[command(infer_subcommands = true)]
|
||||
pub enum Cmd {
|
||||
/// List all variables
|
||||
#[command(about = fl!("cmd-dotfiles-var-list"))]
|
||||
List {
|
||||
/// Sort results by field
|
||||
#[arg(long, value_enum, default_value_t = SortBy::Name)]
|
||||
#[arg(
|
||||
long,
|
||||
value_enum,
|
||||
default_value_t = SortBy::Name,
|
||||
help = fl!("arg-dotfiles-list-sort-by")
|
||||
)]
|
||||
sort_by: SortBy,
|
||||
|
||||
/// Sort in reverse (descending) order
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-dotfiles-list-reverse"))]
|
||||
reverse: bool,
|
||||
|
||||
/// Filter variables by name (substring match)
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-dotfiles-var-list-name"))]
|
||||
name: Option<String>,
|
||||
|
||||
/// Filter variables by value (substring match)
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-dotfiles-var-list-value"))]
|
||||
value: Option<String>,
|
||||
|
||||
/// Show only exported variables
|
||||
#[arg(long, conflicts_with = "shell_only")]
|
||||
#[arg(
|
||||
long,
|
||||
conflicts_with = "shell_only",
|
||||
help = fl!("arg-dotfiles-var-list-exports-only")
|
||||
)]
|
||||
exports_only: bool,
|
||||
|
||||
/// Show only non-exported (shell) variables
|
||||
#[arg(long, conflicts_with = "exports_only")]
|
||||
#[arg(
|
||||
long,
|
||||
conflicts_with = "exports_only",
|
||||
help = fl!("arg-dotfiles-var-list-shell-only")
|
||||
)]
|
||||
shell_only: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -33,40 +33,37 @@ use tracing::{debug, instrument, warn};
|
||||
|
||||
#[cfg(feature = "daemon")]
|
||||
use super::daemon;
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
#[command(infer_subcommands = true)]
|
||||
pub enum Cmd {
|
||||
/// Begins a new command in the history
|
||||
#[command(about = fl!("cmd-history-start"))]
|
||||
Start {
|
||||
/// Collects the command from the `ATUIN_COMMAND_LINE` environment variable,
|
||||
/// which does not need escaping and is more compatible between OS and shells
|
||||
#[arg(long = "command-from-env", hide = true)]
|
||||
#[arg(long = "command-from-env", hide = true, help = fl!("arg-history-start-cmd-env"))]
|
||||
cmd_env: bool,
|
||||
|
||||
/// Author of this command, eg `ellie`, `claude`, or `copilot`
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-history-start-author"))]
|
||||
author: Option<String>,
|
||||
|
||||
/// Whether a human or an AI agent ran this command
|
||||
///
|
||||
/// [`Option::None`] will cause us to perform a best-guess effort.
|
||||
#[arg(long, value_enum)]
|
||||
#[arg(
|
||||
long,
|
||||
value_enum,
|
||||
help = fl!("arg-history-start-author-kind"),
|
||||
long_help = fl!("arg-history-start-author-kind", "long")
|
||||
)]
|
||||
author_kind: Option<AuthorKind>,
|
||||
|
||||
/// Optional intent/rationale for running this command
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-history-start-intent"))]
|
||||
intent: Option<String>,
|
||||
|
||||
command: Vec<String>,
|
||||
|
||||
/// Passed by shell hooks; this flag disables logging to avoid corrupting the terminal and
|
||||
/// to minimize the amount of time the command takes to run.
|
||||
#[arg(long, hide = true)]
|
||||
#[arg(long, hide = true, help = fl!("arg-history-hook"))]
|
||||
hook: bool,
|
||||
},
|
||||
|
||||
/// Finishes a new command in the history (adds time, exit code)
|
||||
#[command(about = fl!("cmd-history-end"))]
|
||||
End {
|
||||
id: HistoryId,
|
||||
#[arg(long, short)]
|
||||
@@ -74,16 +71,14 @@ pub enum Cmd {
|
||||
#[arg(long, short)]
|
||||
duration: Option<u64>,
|
||||
|
||||
/// Passed by shell hooks; this flag disables logging to avoid corrupting the terminal and
|
||||
/// to minimize the amount of time the command takes to run.
|
||||
#[arg(long, hide = true)]
|
||||
#[arg(long, hide = true, help = fl!("arg-history-hook"))]
|
||||
hook: bool,
|
||||
},
|
||||
|
||||
/// Stream history events from the daemon as they are received
|
||||
#[command(about = fl!("cmd-history-tail"))]
|
||||
Tail,
|
||||
|
||||
/// List all items in history
|
||||
#[command(about = fl!("cmd-history-list"))]
|
||||
List {
|
||||
#[arg(long, short)]
|
||||
cwd: bool,
|
||||
@@ -94,12 +89,10 @@ pub enum Cmd {
|
||||
#[arg(long)]
|
||||
human: bool,
|
||||
|
||||
/// Show only the text of the command
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-cmd-only"))]
|
||||
cmd_only: bool,
|
||||
|
||||
/// Terminate the output with a null, for better multiline support
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-print0"))]
|
||||
print0: bool,
|
||||
|
||||
#[arg(long, short, default_value = "true")]
|
||||
@@ -109,69 +102,65 @@ pub enum Cmd {
|
||||
#[arg(action = clap::ArgAction::Set)]
|
||||
reverse: bool,
|
||||
|
||||
/// Display the command time in another timezone other than the configured default.
|
||||
///
|
||||
/// This option takes one of the following kinds of values:
|
||||
///
|
||||
/// - the special value "local" (or "l") which refers to the system time zone
|
||||
/// - an offset from UTC (e.g. "+9", "-2:30")
|
||||
#[arg(long, visible_alias = "tz", verbatim_doc_comment)]
|
||||
#[arg(
|
||||
long,
|
||||
visible_alias = "tz",
|
||||
help = fl!("arg-history-timezone"),
|
||||
long_help = fl!("arg-history-timezone", "long")
|
||||
)]
|
||||
timezone: Option<UtcOffsetSpec>,
|
||||
|
||||
/// Available variables: {command}, {directory}, {duration}, {user}, {host}, {author}, {intent}, {exit}, {time}, {session}, and {uuid}
|
||||
///
|
||||
/// Example: --format "{time} - [{duration}] - {directory}$\t{command}"
|
||||
#[arg(long, short)]
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
help = fl!("arg-history-list-format"),
|
||||
long_help = fl!("arg-history-list-format", "long")
|
||||
)]
|
||||
format: Option<String>,
|
||||
},
|
||||
|
||||
/// Get the last command that was run
|
||||
#[command(about = fl!("cmd-history-last"))]
|
||||
Last {
|
||||
#[arg(long)]
|
||||
human: bool,
|
||||
|
||||
/// Show only the text of the command
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-cmd-only"))]
|
||||
cmd_only: bool,
|
||||
|
||||
/// Display the command time in another timezone other than the configured default.
|
||||
///
|
||||
/// This option takes one of the following kinds of values:
|
||||
///
|
||||
/// - the special value "local" (or "l") which refers to the system time zone
|
||||
/// - an offset from UTC (e.g. "+9", "-2:30")
|
||||
#[arg(long, visible_alias = "tz", verbatim_doc_comment)]
|
||||
#[arg(
|
||||
long,
|
||||
visible_alias = "tz",
|
||||
help = fl!("arg-history-timezone"),
|
||||
long_help = fl!("arg-history-timezone", "long")
|
||||
)]
|
||||
timezone: Option<UtcOffsetSpec>,
|
||||
|
||||
/// Available variables: {command}, {directory}, {duration}, {user}, {host}, {author}, {intent}, {time}, {session}, {uuid} and {relativetime}.
|
||||
///
|
||||
/// Example: --format "{time} - [{duration}] - {directory}$\t{command}"
|
||||
#[arg(long, short)]
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
help = fl!("arg-history-last-format"),
|
||||
long_help = fl!("arg-history-last-format", "long")
|
||||
)]
|
||||
format: Option<String>,
|
||||
},
|
||||
|
||||
InitStore,
|
||||
|
||||
/// Delete history entries matching the configured exclusion filters
|
||||
#[command(about = fl!("cmd-history-prune"))]
|
||||
Prune {
|
||||
/// List matching history lines without performing the actual deletion.
|
||||
#[arg(short = 'n', long)]
|
||||
#[arg(short = 'n', long, help = fl!("arg-history-dry-run"))]
|
||||
dry_run: bool,
|
||||
},
|
||||
|
||||
/// Delete duplicate history entries (that have the same command, cwd and hostname)
|
||||
#[command(about = fl!("cmd-history-dedup"))]
|
||||
Dedup {
|
||||
/// List matching history lines without performing the actual deletion.
|
||||
#[arg(short = 'n', long)]
|
||||
#[arg(short = 'n', long, help = fl!("arg-history-dry-run"))]
|
||||
dry_run: bool,
|
||||
|
||||
/// Only delete results added before this date, read in the configured timezone unless it
|
||||
/// carries an explicit offset
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-history-dedup-before"))]
|
||||
before: String,
|
||||
|
||||
/// How many recent duplicates to keep
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-history-dedup-dupkeep"))]
|
||||
dupkeep: u32,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,12 +16,13 @@ mod wire;
|
||||
|
||||
use event::HookEvent;
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Action {
|
||||
/// Install hooks for an AI agent to capture commands in atuin history
|
||||
#[command(about = fl!("cmd-hook-install"))]
|
||||
Install {
|
||||
/// Agent to install hooks for (e.g., "claude-code")
|
||||
#[arg(value_name = "AGENT")]
|
||||
#[arg(value_name = "AGENT", help = fl!("arg-hook-install-agent"))]
|
||||
agent: String,
|
||||
},
|
||||
}
|
||||
@@ -32,8 +33,7 @@ pub struct Cmd {
|
||||
#[command(subcommand)]
|
||||
action: Option<Action>,
|
||||
|
||||
/// Which agent's hook format to parse (e.g., "claude-code")
|
||||
#[arg(value_name = "AGENT", hide = true)]
|
||||
#[arg(value_name = "AGENT", hide = true, help = fl!("arg-hook-agent"))]
|
||||
agent: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use atuin_common::string::NonNulStr;
|
||||
use serde_json::error::Category;
|
||||
|
||||
use super::wire::{HookEventName, WireHookEvent, WireToolName};
|
||||
use crate::i18n::fl;
|
||||
|
||||
/// Why a hook payload could not be parsed.
|
||||
///
|
||||
@@ -19,7 +20,7 @@ pub enum ParseError {
|
||||
/// The payload on stdin was not valid JSON — a syntax error or truncated
|
||||
/// input. Agents always emit syntactically valid JSON, so this signals a
|
||||
/// real fault rather than an event to skip.
|
||||
#[error("hook payload is not valid JSON at line {line}, column {column}")]
|
||||
#[error("{}", fl!("hook-malformed-json", line = *.line, column = *.column))]
|
||||
MalformedJson {
|
||||
line: usize,
|
||||
column: usize,
|
||||
@@ -330,10 +331,14 @@ mod tests {
|
||||
#[case::not_json("not json")]
|
||||
#[case::truncated(r#"{"tool_name":"#)]
|
||||
fn malformed_json_is_an_error(#[case] input: &str) {
|
||||
let ParseError::MalformedJson { line, column, .. } =
|
||||
HookEvent::from_json_str(input).unwrap_err();
|
||||
let err = HookEvent::from_json_str(input).unwrap_err();
|
||||
let ParseError::MalformedJson { line, column, .. } = err;
|
||||
|
||||
assert!(line >= 1 && column >= 1, "position should be 1-based, got {line}:{column}");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
format!("hook payload is not valid JSON at line {line}, column {column}")
|
||||
);
|
||||
}
|
||||
|
||||
proptest! {
|
||||
|
||||
@@ -21,33 +21,35 @@ use eyre::Result;
|
||||
use indicatif::ProgressBar;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(infer_subcommands = true)]
|
||||
pub enum Cmd {
|
||||
/// Import history for the current shell
|
||||
#[command(about = fl!("cmd-import-auto"))]
|
||||
Auto,
|
||||
|
||||
/// Import history from the zsh history file
|
||||
#[command(about = fl!("cmd-import-shell", shell = "zsh"))]
|
||||
Zsh,
|
||||
/// Import history from the zsh history file
|
||||
#[command(about = fl!("cmd-import-zsh-hist-db"))]
|
||||
ZshHistDb,
|
||||
/// Import history from the bash history file
|
||||
#[command(about = fl!("cmd-import-shell", shell = "bash"))]
|
||||
Bash,
|
||||
/// Import history from the replxx history file
|
||||
#[command(about = fl!("cmd-import-shell", shell = "replxx"))]
|
||||
Replxx,
|
||||
/// Import history from the resh history file
|
||||
#[command(about = fl!("cmd-import-shell", shell = "resh"))]
|
||||
Resh,
|
||||
/// Import history from the fish history file
|
||||
#[command(about = fl!("cmd-import-shell", shell = "fish"))]
|
||||
Fish,
|
||||
/// Import history from the nu history file
|
||||
#[command(about = fl!("cmd-import-shell", shell = "nu"))]
|
||||
Nu,
|
||||
/// Import history from the nu history file
|
||||
#[command(about = fl!("cmd-import-nu-hist-db"))]
|
||||
NuHistDb,
|
||||
/// Import history from xonsh json files
|
||||
#[command(about = fl!("cmd-import-xonsh"))]
|
||||
Xonsh,
|
||||
/// Import history from xonsh sqlite db
|
||||
#[command(about = fl!("cmd-import-xonsh-sqlite"))]
|
||||
XonshSqlite,
|
||||
/// Import history from the powershell history file
|
||||
#[command(about = fl!("cmd-import-shell", shell = "powershell"))]
|
||||
Powershell,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ use clap::{Parser, ValueEnum};
|
||||
use eyre::Result;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
mod bash;
|
||||
mod fish;
|
||||
mod nu;
|
||||
@@ -14,16 +16,13 @@ mod zsh;
|
||||
pub struct Cmd {
|
||||
shell: Shell,
|
||||
|
||||
/// Disable the binding of CTRL-R to atuin
|
||||
#[clap(long)]
|
||||
#[clap(long, help = fl!("arg-init-disable-ctrl-r"))]
|
||||
disable_ctrl_r: bool,
|
||||
|
||||
/// Disable the binding of the Up Arrow key to atuin
|
||||
#[clap(long)]
|
||||
#[clap(long, help = fl!("arg-init-disable-up-arrow"))]
|
||||
disable_up_arrow: bool,
|
||||
|
||||
/// Disable the binding of ? to Atuin AI
|
||||
#[clap(long)]
|
||||
#[clap(long, help = fl!("arg-init-disable-ai"))]
|
||||
disable_ai: bool,
|
||||
}
|
||||
|
||||
@@ -31,17 +30,17 @@ pub struct Cmd {
|
||||
#[value(rename_all = "lower")]
|
||||
#[allow(clippy::enum_variant_names, clippy::doc_markdown)]
|
||||
pub enum Shell {
|
||||
/// Zsh setup
|
||||
#[value(help = fl!("value-init-shell-zsh"))]
|
||||
Zsh,
|
||||
/// Bash setup
|
||||
#[value(help = fl!("value-init-shell-bash"))]
|
||||
Bash,
|
||||
/// Fish setup
|
||||
#[value(help = fl!("value-init-shell-fish"))]
|
||||
Fish,
|
||||
/// Nu setup
|
||||
#[value(help = fl!("value-init-shell-nu"))]
|
||||
Nu,
|
||||
/// Xonsh setup
|
||||
#[value(help = fl!("value-init-shell-xonsh"))]
|
||||
Xonsh,
|
||||
/// PowerShell setup
|
||||
#[value(help = fl!("value-init-shell-powershell"))]
|
||||
PowerShell,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
use atuin_client::settings::Settings;
|
||||
use atuin_common::logs::LogConfig;
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
pub enum Cmd {
|
||||
PrepareSearchIndex,
|
||||
/// Check whether the current terminal belongs to a live PTY proxy.
|
||||
///
|
||||
/// Prints `0` or `1` to stdout. This command is used by shell hooks to determine whether the
|
||||
/// PTY proxy is in use.
|
||||
#[command(
|
||||
about = fl!("cmd-__internal-pty-proxy-active"),
|
||||
long_about = fl!("cmd-__internal-pty-proxy-active", "long")
|
||||
)]
|
||||
PtyProxyActive,
|
||||
}
|
||||
|
||||
|
||||
@@ -8,58 +8,51 @@ use clap::Subcommand;
|
||||
use eyre::{Context, Result, eyre};
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
#[command(infer_subcommands = true)]
|
||||
pub enum Cmd {
|
||||
/// Set a key-value pair
|
||||
#[command(about = fl!("cmd-kv-set"))]
|
||||
Set {
|
||||
/// Key to set
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-kv-set-key"))]
|
||||
key: String,
|
||||
|
||||
/// Value to store (reads from stdin if not provided)
|
||||
#[arg(help = fl!("arg-kv-set-value"))]
|
||||
value: Option<String>,
|
||||
|
||||
/// Namespace for the key-value pair
|
||||
#[arg(long, short, default_value = "default")]
|
||||
#[arg(long, short, default_value = "default", help = fl!("arg-kv-namespace"))]
|
||||
namespace: String,
|
||||
},
|
||||
|
||||
/// Delete one or more key-value pairs
|
||||
#[command(alias = "rm")]
|
||||
#[command(alias = "rm", about = fl!("cmd-kv-delete"))]
|
||||
Delete {
|
||||
/// Keys to delete
|
||||
#[arg(required = true)]
|
||||
#[arg(required = true, help = fl!("arg-kv-delete-keys"))]
|
||||
keys: Vec<String>,
|
||||
|
||||
/// Namespace for the key-value pair
|
||||
#[arg(long, short, default_value = "default")]
|
||||
#[arg(long, short, default_value = "default", help = fl!("arg-kv-namespace"))]
|
||||
namespace: String,
|
||||
},
|
||||
|
||||
/// Retrieve a saved value
|
||||
#[command(about = fl!("cmd-kv-get"))]
|
||||
Get {
|
||||
/// Key to retrieve
|
||||
#[arg(help = fl!("arg-kv-get-key"))]
|
||||
key: String,
|
||||
|
||||
/// Namespace for the key-value pair
|
||||
#[arg(long, short, default_value = "default")]
|
||||
#[arg(long, short, default_value = "default", help = fl!("arg-kv-namespace"))]
|
||||
namespace: String,
|
||||
},
|
||||
|
||||
/// List all keys in a namespace, or in all namespaces
|
||||
#[command(alias = "ls")]
|
||||
#[command(alias = "ls", about = fl!("cmd-kv-list"))]
|
||||
List {
|
||||
/// Namespace to list keys from
|
||||
#[arg(long, short, default_value = "default")]
|
||||
#[arg(long, short, default_value = "default", help = fl!("arg-kv-list-namespace"))]
|
||||
namespace: String,
|
||||
|
||||
/// List all keys in all namespaces
|
||||
#[arg(long, short, alias = "all")]
|
||||
#[arg(long, short, alias = "all", help = fl!("arg-kv-list-all-namespaces"))]
|
||||
all_namespaces: bool,
|
||||
},
|
||||
|
||||
/// Rebuild the KV store
|
||||
#[command(about = fl!("cmd-kv-rebuild"))]
|
||||
Rebuild,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ use atuin_client::theme::Theme;
|
||||
use clap::Subcommand;
|
||||
use eyre::Result;
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
mod search;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum Cmd {
|
||||
/// Full-text search over captured command output
|
||||
#[command(about = fl!("cmd-output-search"))]
|
||||
Search(search::Cmd),
|
||||
}
|
||||
|
||||
|
||||
@@ -21,25 +21,26 @@ mod writers;
|
||||
use writers::{HistoryMatch, JsonWriter, PlainWriter, PrettyWriter, RenderCtx, Writer};
|
||||
|
||||
use crate::command::client::daemon;
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RunError {
|
||||
#[error("output capture is disabled. enable [output] in your config to search command output.")]
|
||||
#[error("{}", fl!("output-search-disabled"))]
|
||||
Disabled,
|
||||
|
||||
#[error("blank query provided. please run 'atuin output search --help'")]
|
||||
#[error("{}", fl!("output-search-empty-query"))]
|
||||
EmptyQuery,
|
||||
|
||||
#[error("could not connect to the daemon")]
|
||||
#[error("{}", fl!("output-search-connect-failed"))]
|
||||
Connect(#[source] eyre::Report),
|
||||
|
||||
#[error("the daemon failed to search command output")]
|
||||
#[error("{}", fl!("output-search-daemon-failed"))]
|
||||
Search(#[source] eyre::Report),
|
||||
|
||||
#[error("could not load history from the local database")]
|
||||
#[error("{}", fl!("output-search-load-history-failed"))]
|
||||
LoadHistory(#[source] eyre::Report),
|
||||
|
||||
#[error("could not write search results")]
|
||||
#[error("{}", fl!("output-search-write-failed"))]
|
||||
Write(#[from] io::Error),
|
||||
}
|
||||
|
||||
@@ -48,30 +49,25 @@ enum Style {
|
||||
Auto,
|
||||
Plain,
|
||||
Pretty,
|
||||
/// A single JSON array of match objects.
|
||||
#[value(help = fl!("value-output-search-style-json"))]
|
||||
Json,
|
||||
/// Newline-delimited JSON: one match object per line.
|
||||
#[value(help = fl!("value-output-search-style-ndjson"))]
|
||||
Ndjson,
|
||||
}
|
||||
|
||||
/// Full-text search over captured command output.
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct Cmd {
|
||||
/// Words to search for; all must appear. Use `--` before a query that starts with `-`.
|
||||
#[arg(required = true)]
|
||||
#[arg(required = true, help = fl!("arg-output-search-query"))]
|
||||
query: Vec<String>,
|
||||
|
||||
/// Maximum number of matches to return.
|
||||
#[arg(long, default_value_t = 5)]
|
||||
#[arg(long, default_value_t = 5, help = fl!("arg-output-search-limit"))]
|
||||
limit: u32,
|
||||
|
||||
/// Show only the matching lines, with this many lines of context on either side; without it,
|
||||
/// each match's whole output.
|
||||
#[arg(short = 'C', long)]
|
||||
#[arg(short = 'C', long, help = fl!("arg-output-search-context"))]
|
||||
context: Option<u32>,
|
||||
|
||||
/// How matches are rendered.
|
||||
#[arg(long, value_enum, default_value_t = Style::Auto)]
|
||||
#[arg(long, value_enum, default_value_t = Style::Auto, help = fl!("arg-output-search-style"))]
|
||||
style: Style,
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ use eyre::{Context as _, OptionExt, Result, bail};
|
||||
use tempfile::NamedTempFile;
|
||||
use tracing::{debug, instrument};
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct NewScript {
|
||||
pub name: String,
|
||||
@@ -34,13 +36,11 @@ pub struct NewScript {
|
||||
|
||||
#[allow(clippy::option_option)]
|
||||
#[arg(long)]
|
||||
/// Use the last command as the script content
|
||||
///
|
||||
/// Optionally specify a number to use the last N commands
|
||||
#[arg(help = fl!("arg-scripts-new-last"), long_help = fl!("arg-scripts-new-last", "long"))]
|
||||
pub last: Option<Option<usize>>,
|
||||
|
||||
#[arg(long)]
|
||||
/// Skip opening editor when using --last
|
||||
#[arg(help = fl!("arg-scripts-new-no-edit"))]
|
||||
pub no_edit: bool,
|
||||
}
|
||||
|
||||
@@ -48,10 +48,12 @@ pub struct NewScript {
|
||||
pub struct Run {
|
||||
pub name: String,
|
||||
|
||||
/// Specify template variables in the format KEY=VALUE
|
||||
///
|
||||
/// Example: -v name=John -v greeting="Hello there"
|
||||
#[arg(short, long = "var")]
|
||||
#[arg(
|
||||
short,
|
||||
long = "var",
|
||||
help = fl!("arg-scripts-run-var"),
|
||||
long_help = fl!("arg-scripts-run-var", "long")
|
||||
)]
|
||||
pub var: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -63,7 +65,7 @@ pub struct Get {
|
||||
pub name: String,
|
||||
|
||||
#[arg(short, long)]
|
||||
/// Display only the executable script with shebang
|
||||
#[arg(help = fl!("arg-scripts-get-script"))]
|
||||
pub script: bool,
|
||||
}
|
||||
|
||||
@@ -74,16 +76,13 @@ pub struct Edit {
|
||||
#[arg(short, long)]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Replace all existing tags with these new tags
|
||||
#[arg(short, long)]
|
||||
#[arg(short, long, help = fl!("arg-scripts-edit-tags"))]
|
||||
pub tags: Vec<String>,
|
||||
|
||||
/// Remove all tags from the script
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-scripts-edit-no-tags"))]
|
||||
pub no_tags: bool,
|
||||
|
||||
/// Rename the script
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-scripts-edit-rename"))]
|
||||
pub rename: Option<String>,
|
||||
|
||||
#[arg(short, long)]
|
||||
@@ -93,8 +92,7 @@ pub struct Edit {
|
||||
pub script: Option<PathBuf>,
|
||||
|
||||
#[allow(clippy::struct_field_names)]
|
||||
/// Skip opening editor
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-scripts-edit-no-edit"))]
|
||||
pub no_edit: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -27,103 +27,86 @@ mod syntax;
|
||||
|
||||
use atuin_common::time::UtcOffsetSpec;
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[allow(clippy::struct_excessive_bools, clippy::struct_field_names)]
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct Cmd {
|
||||
/// Filter search result by directory
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-search-cwd"))]
|
||||
cwd: Option<String>,
|
||||
|
||||
/// Exclude directory from results
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-search-exclude-cwd"))]
|
||||
exclude_cwd: Option<String>,
|
||||
|
||||
/// Filter by exit code; repeat to include any of the given codes
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-search-exit"))]
|
||||
exit: Vec<i64>,
|
||||
|
||||
/// Exclude results with this exit code; repeat to exclude multiple codes
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-search-exclude-exit"))]
|
||||
exclude_exit: Vec<i64>,
|
||||
|
||||
/// Only include results added before this date.
|
||||
///
|
||||
/// Read in the timezone from `--timezone` (or the configured one) unless it carries an
|
||||
/// explicit offset; relative phrases like "yesterday 3pm" are anchored there too.
|
||||
#[arg(long, short)]
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
help = fl!("arg-search-before"),
|
||||
long_help = fl!("arg-search-before", "long")
|
||||
)]
|
||||
before: Option<String>,
|
||||
|
||||
/// Only include results after this date; see `--before` for how it is interpreted.
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-search-after"))]
|
||||
after: Option<String>,
|
||||
|
||||
/// How many entries to return at most
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-search-limit"))]
|
||||
limit: Option<i64>,
|
||||
|
||||
/// Offset from the start of the results
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-search-offset"))]
|
||||
offset: Option<i64>,
|
||||
|
||||
/// Open interactive search UI
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-search-interactive"))]
|
||||
interactive: bool,
|
||||
|
||||
/// Allow overriding filter mode over config
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-search-filter-mode"))]
|
||||
filter_mode: Option<FilterMode>,
|
||||
|
||||
/// Allow overriding search mode over config
|
||||
///
|
||||
/// Note: for non-interactive searches, "daemon-fuzzy" behaves like "fuzzy". "skim" used to
|
||||
/// behave like "fuzzy" in non-interactive searches too; it has since been removed but is still
|
||||
/// accepted here as an alias of "fuzzy".
|
||||
#[arg(long)]
|
||||
#[arg(
|
||||
long,
|
||||
help = fl!("arg-search-search-mode"),
|
||||
long_help = fl!("arg-search-search-mode", "long")
|
||||
)]
|
||||
search_mode: Option<RequestedSearchMode>,
|
||||
|
||||
/// Marker argument used to inform atuin that it was invoked from a shell up-key binding (hidden from help to avoid confusion)
|
||||
#[arg(long, hide = true)]
|
||||
#[arg(long, hide = true, help = fl!("arg-search-shell-up-key-binding"))]
|
||||
shell_up_key_binding: bool,
|
||||
|
||||
/// Notify the keymap at the shell's side
|
||||
#[arg(long, default_value = "auto")]
|
||||
#[arg(long, default_value = "auto", help = fl!("arg-search-keymap-mode"))]
|
||||
keymap_mode: KeymapMode,
|
||||
|
||||
/// Use human-readable formatting for time
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-search-human"))]
|
||||
human: bool,
|
||||
|
||||
#[arg(allow_hyphen_values = true)]
|
||||
query: Vec<String>,
|
||||
|
||||
/// Show only the text of the command
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-cmd-only"))]
|
||||
cmd_only: bool,
|
||||
|
||||
/// Terminate the output with a null, for better multiline handling
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-print0"))]
|
||||
print0: bool,
|
||||
|
||||
/// Delete anything matching this query. Will not print out the match
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-search-delete"))]
|
||||
delete: bool,
|
||||
|
||||
/// Delete EVERYTHING!
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-search-delete-it-all"))]
|
||||
delete_it_all: bool,
|
||||
|
||||
/// Reverse the order of results, oldest first
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-search-reverse"))]
|
||||
reverse: bool,
|
||||
|
||||
/// Timezone to display command times in and to interpret `--before`/`--after` in, instead
|
||||
/// of the configured default.
|
||||
///
|
||||
/// This option takes one of the following kinds of values:
|
||||
///
|
||||
/// - the special value "local" (or "l") which refers to the system time zone
|
||||
/// - an offset from UTC (e.g. "+9", "-2:30")
|
||||
#[arg(long, visible_alias = "tz", verbatim_doc_comment)]
|
||||
#[arg(
|
||||
long,
|
||||
visible_alias = "tz",
|
||||
help = fl!("arg-search-timezone"),
|
||||
long_help = fl!("arg-search-timezone", "long")
|
||||
)]
|
||||
// `num_args = 0..=1` allows a user to run `atuin search --tz` with no argument to `--tz`. This
|
||||
// does the same thing as not providing the flag, but we previously allowed it (via an
|
||||
// `Option<Option<T>>` field type), so let's keep supporting it to avoid breaking existing
|
||||
@@ -131,37 +114,27 @@ pub struct Cmd {
|
||||
#[arg(allow_hyphen_values = true, num_args = 0..=1)]
|
||||
timezone: Option<UtcOffsetSpec>,
|
||||
|
||||
/// Available variables: {command}, {directory}, {duration}, {user}, {host}, {time}, {exit} and
|
||||
/// {relativetime}.
|
||||
///
|
||||
/// Example: --format "{time} - [{duration}] - {directory}$\t{command}"
|
||||
#[arg(long, short)]
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
help = fl!("arg-search-format"),
|
||||
long_help = fl!("arg-search-format", "long")
|
||||
)]
|
||||
format: Option<String>,
|
||||
|
||||
/// Set the maximum number of lines Atuin's interface should take up.
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-search-inline-height"))]
|
||||
inline_height: Option<u16>,
|
||||
|
||||
/// Filter by author. Supports $all-user (non-agents), $all-agent, or literal names.
|
||||
///
|
||||
/// Can be specified multiple times.
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-search-author"), long_help = fl!("arg-search-author", "long"))]
|
||||
author: Vec<AuthorPattern>,
|
||||
|
||||
/// Include duplicate commands in the output (non-interactive only)
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-search-include-duplicates"))]
|
||||
include_duplicates: bool,
|
||||
|
||||
/// File name to write the result to (hidden from help as this is meant to be used from a script)
|
||||
#[arg(long, hide = true)]
|
||||
#[arg(long, hide = true, help = fl!("arg-search-result-file"))]
|
||||
result_file: Option<String>,
|
||||
|
||||
/// Filter by the shell that was used to run the command
|
||||
///
|
||||
/// If passed multiple times, commands from any of the shells will be shown.
|
||||
///
|
||||
/// `--shell ""` will include commands for which the shell is unknown.
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-search-shell"), long_help = fl!("arg-search-shell", "long"))]
|
||||
shell: Vec<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ use interim::parse_date_string;
|
||||
use time::{Duration, OffsetDateTime, Time};
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
fn parse_ngram_size(s: &str) -> Result<usize, String> {
|
||||
let value = s.parse::<usize>().map_err(|_| format!("'{s}' is not a valid window size"))?;
|
||||
|
||||
@@ -18,31 +20,28 @@ fn parse_ngram_size(s: &str) -> Result<usize, String> {
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn period_long_help() -> String {
|
||||
format!(
|
||||
"Compute statistics for the specified period, leave blank for statistics since the \
|
||||
beginning. See [this]({}) for more details.",
|
||||
atuin_common::docs::url("reference/stats/")
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(infer_subcommands = true)]
|
||||
pub struct Cmd {
|
||||
/// Compute statistics for the specified period, leave blank for statistics since the beginning
|
||||
#[arg(long_help = period_long_help())]
|
||||
#[arg(
|
||||
help = fl!("arg-stats-period"),
|
||||
long_help = fl!("arg-stats-period", "long", url = atuin_common::docs::url("reference/stats/"))
|
||||
)]
|
||||
period: Vec<String>,
|
||||
|
||||
/// How many top commands to list
|
||||
#[arg(long, short, default_value = "10")]
|
||||
#[arg(long, short, default_value = "10", help = fl!("arg-stats-count"))]
|
||||
count: usize,
|
||||
|
||||
/// The number of consecutive commands to consider
|
||||
#[arg(long, short, default_value = "1", value_parser = parse_ngram_size)]
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
default_value = "1",
|
||||
value_parser = parse_ngram_size,
|
||||
help = fl!("arg-stats-ngram-size")
|
||||
)]
|
||||
ngram_size: usize,
|
||||
|
||||
/// Filter commands by scope [global, host, session, directory, workspace]
|
||||
#[arg(long = "filter-mode")]
|
||||
#[arg(long = "filter-mode", help = fl!("arg-stats-filter-mode"))]
|
||||
filter_mode: Option<FilterMode>,
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ use itertools::Itertools;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[cfg(feature = "sync")]
|
||||
mod push;
|
||||
|
||||
@@ -23,26 +25,26 @@ mod verify;
|
||||
#[derive(Subcommand, Debug)]
|
||||
#[command(infer_subcommands = true)]
|
||||
pub enum Cmd {
|
||||
/// Print the current status of the record store
|
||||
#[command(about = fl!("cmd-store-status"))]
|
||||
Status,
|
||||
|
||||
/// Rebuild a store (eg atuin store rebuild history)
|
||||
#[command(about = fl!("cmd-store-rebuild"))]
|
||||
Rebuild(rebuild::Rebuild),
|
||||
|
||||
/// Re-encrypt the store with a new key (potential for data loss!)
|
||||
#[command(about = fl!("cmd-store-rekey"))]
|
||||
Rekey(rekey::Rekey),
|
||||
|
||||
/// Delete all records in the store that cannot be decrypted with the current key
|
||||
#[command(about = fl!("cmd-store-purge"))]
|
||||
Purge(purge::Purge),
|
||||
|
||||
/// Verify that all records in the store can be decrypted with the current key
|
||||
#[command(about = fl!("cmd-store-verify"))]
|
||||
Verify(verify::Verify),
|
||||
|
||||
/// Push all records to the remote sync server (one way sync)
|
||||
#[command(about = fl!("cmd-store-push"))]
|
||||
#[cfg(feature = "sync")]
|
||||
Push(push::Push),
|
||||
|
||||
/// Pull records from the remote sync server (one way sync)
|
||||
#[command(about = fl!("cmd-store-pull"))]
|
||||
#[cfg(feature = "sync")]
|
||||
Pull(pull::Pull),
|
||||
}
|
||||
|
||||
@@ -9,22 +9,27 @@ use atuin_domain::record::RecordTag;
|
||||
use clap::Args;
|
||||
use eyre::{Context as _, Result};
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct Pull {
|
||||
/// The tag to push (eg, 'history'). Defaults to all tags
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-store-tag"))]
|
||||
pub tag: Option<RecordTag>,
|
||||
|
||||
/// Force push records
|
||||
///
|
||||
/// This will first wipe the local store, and then download all records from the remote
|
||||
#[arg(long, default_value = "false")]
|
||||
#[arg(
|
||||
long,
|
||||
default_value = "false",
|
||||
help = fl!("arg-store-pull-force"),
|
||||
long_help = fl!("arg-store-pull-force", "long")
|
||||
)]
|
||||
pub force: bool,
|
||||
|
||||
/// Page Size
|
||||
///
|
||||
/// How many records to download at once. Defaults to 100
|
||||
#[arg(long, default_value = "100")]
|
||||
#[arg(
|
||||
long,
|
||||
default_value = "100",
|
||||
help = fl!("arg-store-pull-page"),
|
||||
long_help = fl!("arg-store-pull-page", "long")
|
||||
)]
|
||||
pub page: NonZeroU64,
|
||||
}
|
||||
|
||||
|
||||
@@ -10,27 +10,30 @@ use clap::Args;
|
||||
use eyre::{Context as _, Result};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct Push {
|
||||
/// The tag to push (eg, 'history'). Defaults to all tags
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-store-tag"))]
|
||||
pub tag: Option<RecordTag>,
|
||||
|
||||
/// The host to push, in the form of a UUID host ID. Defaults to the current host.
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-store-push-host"))]
|
||||
pub host: Option<Uuid>,
|
||||
|
||||
/// Force push records
|
||||
///
|
||||
/// This will override both host and tag, to be all hosts and all tags. First clear the remote store, then upload all of the
|
||||
/// local store
|
||||
#[arg(long, default_value = "false")]
|
||||
#[arg(
|
||||
long,
|
||||
default_value = "false",
|
||||
help = fl!("arg-store-push-force"),
|
||||
long_help = fl!("arg-store-push-force", "long")
|
||||
)]
|
||||
pub force: bool,
|
||||
|
||||
/// Page Size
|
||||
///
|
||||
/// How many records to upload at once. Defaults to 100
|
||||
#[arg(long, default_value = "100")]
|
||||
#[arg(
|
||||
long,
|
||||
default_value = "100",
|
||||
help = fl!("arg-store-push-page"),
|
||||
long_help = fl!("arg-store-push-page", "long")
|
||||
)]
|
||||
pub page: NonZeroU64,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@ use atuin_common::encryption::paseto_v4;
|
||||
use clap::Args;
|
||||
use eyre::{Context as _, Result};
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct Rekey {
|
||||
/// The new key to use for encryption. Omit for a randomly-generated key
|
||||
#[arg(help = fl!("arg-store-rekey-key"))]
|
||||
key: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -13,34 +13,33 @@ use tracing::instrument;
|
||||
mod status;
|
||||
|
||||
use crate::command::client::account;
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
#[command(infer_subcommands = true)]
|
||||
pub enum Cmd {
|
||||
/// Sync with the configured server
|
||||
#[command(about = fl!("cmd-sync"))]
|
||||
Sync {
|
||||
/// Force re-download everything
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-sync-force"))]
|
||||
force: bool,
|
||||
},
|
||||
|
||||
/// Login to the configured server
|
||||
#[command(about = fl!("cmd-login"))]
|
||||
Login(account::login::Cmd),
|
||||
|
||||
/// Log out
|
||||
#[command(about = fl!("cmd-logout"))]
|
||||
Logout,
|
||||
|
||||
/// Register with the configured server
|
||||
#[command(about = fl!("cmd-register"))]
|
||||
Register(account::register::Cmd),
|
||||
|
||||
/// Print the encryption key for transfer to another machine
|
||||
#[command(about = fl!("cmd-key"))]
|
||||
Key {
|
||||
/// Switch to base64 output of the key
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-key-base64"))]
|
||||
base64: bool,
|
||||
},
|
||||
|
||||
/// Display the sync status
|
||||
#[command(about = fl!("cmd-status"))]
|
||||
Status,
|
||||
}
|
||||
|
||||
@@ -55,12 +54,12 @@ impl Cmd {
|
||||
Self::Status => status::run(&settings).await,
|
||||
Self::Key { base64 } => {
|
||||
let key = paseto_v4::Key::try_load_from_path(&settings.key_path)
|
||||
.wrap_err("could not load encryption key")?;
|
||||
.wrap_err(fl!("sync-key-load-failed"))?;
|
||||
|
||||
if base64 {
|
||||
println!("{}", key.encode().dangerously_leak_secret());
|
||||
} else {
|
||||
println!("{}", key.try_mnemonic().context("invalid key")?);
|
||||
println!("{}", key.try_mnemonic().context(fl!("sync-key-invalid"))?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -97,20 +96,23 @@ async fn run(settings: &Settings, force: bool, db: &Sqlite, store: SqliteStore)
|
||||
|
||||
crate::sync::build(settings, &store, db, Some(&downloaded)).await?;
|
||||
|
||||
println!("{uploaded}/{} up/down to record store", downloaded.len());
|
||||
println!("{}", fl!("sync-up-down", uploaded = uploaded, downloaded = downloaded.len()));
|
||||
|
||||
let history_length = db.history_count(true).await?;
|
||||
let store_history_length = store.len_tag(&RecordTag::History).await?;
|
||||
|
||||
if u64::conv(history_length) > store_history_length {
|
||||
println!("{history_length} in history index, but {store_history_length} in history store");
|
||||
println!("Running automatic history store init...");
|
||||
println!(
|
||||
"{}",
|
||||
fl!("sync-history-mismatch", index = history_length, store = store_history_length)
|
||||
);
|
||||
println!("{}", fl!("sync-store-init"));
|
||||
|
||||
// Internally we use the global filter mode, so this context is ignored.
|
||||
// don't recurse or loop here.
|
||||
history_store.init_store(db).await?;
|
||||
|
||||
println!("Re-running sync due to new records locally");
|
||||
println!("{}", fl!("sync-rerun"));
|
||||
|
||||
// we'll want to run sync once more, as there will now be stuff to upload -- re-key the same
|
||||
// session rather than reconnecting.
|
||||
@@ -122,13 +124,12 @@ async fn run(settings: &Settings, force: bool, db: &Sqlite, store: SqliteStore)
|
||||
|
||||
crate::sync::build(settings, &store, db, Some(&downloaded)).await?;
|
||||
|
||||
println!("{uploaded}/{} up/down to record store", downloaded.len());
|
||||
println!("{}", fl!("sync-up-down", uploaded = uploaded, downloaded = downloaded.len()));
|
||||
}
|
||||
|
||||
println!(
|
||||
"Sync complete! {} items in history database, force: {}",
|
||||
db.history_count(true).await?,
|
||||
force
|
||||
"{}",
|
||||
fl!("sync-complete", count = db.history_count(true).await?, force = force.to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -4,11 +4,12 @@ use atuin_common::time::DurationExt;
|
||||
use colored::Colorize;
|
||||
use eyre::{Result, bail};
|
||||
|
||||
use crate::i18n::fl;
|
||||
use crate::{SHA, VERSION};
|
||||
|
||||
pub async fn run(settings: &Settings) -> Result<()> {
|
||||
if !settings.logged_in().await? {
|
||||
bail!("You are not logged in to a sync server - cannot show sync status");
|
||||
bail!(fl!("sync-status-not-logged-in"));
|
||||
}
|
||||
|
||||
let caps = api_client::caps_client(settings)?;
|
||||
@@ -24,19 +25,31 @@ pub async fn run(settings: &Settings) -> Result<()> {
|
||||
let me = client.me().await?;
|
||||
let last_sync = Settings::last_sync().await?;
|
||||
|
||||
println!("Atuin v{VERSION} - Build rev {SHA}\n");
|
||||
println!("{}\n", fl!("sync-status-version", version = VERSION, sha = SHA));
|
||||
|
||||
println!("{}", "[Local]".green());
|
||||
println!("{}", fl!("sync-status-local").green());
|
||||
|
||||
if settings.auto_sync {
|
||||
println!("Sync frequency: {}", settings.sync_frequency.display().largest_unit());
|
||||
println!("Last sync: {}", last_sync.to_offset(settings.timezone.0));
|
||||
println!(
|
||||
"{}",
|
||||
fl!(
|
||||
"sync-status-frequency",
|
||||
frequency = settings.sync_frequency.display().largest_unit().to_string()
|
||||
)
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
fl!(
|
||||
"sync-status-last-sync",
|
||||
time = last_sync.to_offset(settings.timezone.0).to_string()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if settings.auto_sync {
|
||||
println!("{}", "[Remote]".green());
|
||||
println!("Address: {}", settings.sync_address);
|
||||
println!("Username: {}", me.username);
|
||||
println!("{}", fl!("sync-status-remote").green());
|
||||
println!("{}", fl!("sync-status-address", address = settings.sync_address.to_string()));
|
||||
println!("{}", fl!("sync-status-username", username = me.username));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -4,15 +4,14 @@ use clap::Parser;
|
||||
use eyre::{Result, bail, eyre};
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct Cmd {
|
||||
/// Check whether an update is available without installing it
|
||||
#[arg(long)]
|
||||
#[arg(long, help = fl!("arg-update-check"))]
|
||||
check: bool,
|
||||
|
||||
/// Update (or roll back) to a specific version, e.g. "18.9.0" or
|
||||
/// "18.9.0-nightly.1", instead of the channel's latest release
|
||||
#[arg(long, conflicts_with = "check")]
|
||||
#[arg(long, conflicts_with = "check", help = fl!("arg-update-version"))]
|
||||
version: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use clap::builder::{StyledStr, Styles};
|
||||
use eyre::Result;
|
||||
|
||||
use crate::Atuin;
|
||||
use crate::i18n::writet;
|
||||
|
||||
pub fn run(args: &[String]) -> Result<()> {
|
||||
let subcommand = &args[0];
|
||||
@@ -83,9 +84,12 @@ fn render_not_found(subcommand: &str, bin: &str) -> StyledStr {
|
||||
let usage = atuin_cmd.render_usage();
|
||||
|
||||
let _ = write!(output, "{error}error:{error:#} ");
|
||||
let _ = write!(output, "unrecognized subcommand '{invalid}{subcommand}{invalid:#}' ");
|
||||
let _ =
|
||||
write!(output, "and no executable named '{invalid}{bin}{invalid:#}' found in your PATH");
|
||||
let _ = writet!(
|
||||
output,
|
||||
"unrecognized-subcommand",
|
||||
subcommand = format!("{invalid}{subcommand}{invalid:#}"),
|
||||
bin = format!("{invalid}{bin}{invalid:#}"),
|
||||
);
|
||||
let _ = write!(output, "\n\n");
|
||||
let _ = write!(output, "{usage}");
|
||||
let _ = write!(output, "\n\n");
|
||||
@@ -93,3 +97,22 @@ fn render_not_found(subcommand: &str, bin: &str) -> StyledStr {
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::render_not_found;
|
||||
|
||||
#[rstest]
|
||||
fn not_found_names_the_subcommand_and_its_binary() {
|
||||
let rendered = render_not_found("frobnicate", "atuin-frobnicate").to_string();
|
||||
assert!(
|
||||
rendered.starts_with(
|
||||
"error: unrecognized subcommand 'frobnicate' and no executable named \
|
||||
'atuin-frobnicate' found in your PATH\n\n"
|
||||
),
|
||||
"{rendered:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ use clap_complete::{Generator, Shell, generate, generate_to};
|
||||
use clap_complete_nushell::Nushell;
|
||||
use eyre::Result;
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
// clap put nushell completions into a separate package due to the maintainers
|
||||
// being a little less committed to support them.
|
||||
// This means we have to do a tiny bit of legwork to combine these completions
|
||||
@@ -50,12 +52,10 @@ impl Generator for GenShell {
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Cmd {
|
||||
/// Set the shell for generating completions
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-gen-completions-shell"))]
|
||||
shell: GenShell,
|
||||
|
||||
/// Set the output directory
|
||||
#[arg(long, short)]
|
||||
#[arg(long, short, help = fl!("arg-gen-completions-out-dir"))]
|
||||
out_dir: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ use eyre::Result;
|
||||
#[cfg(not(windows))]
|
||||
use rustix::{fs::Mode, process::umask};
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
mod client;
|
||||
|
||||
@@ -21,20 +23,18 @@ pub enum AtuinCmd {
|
||||
#[command(flatten)]
|
||||
Client(client::Cmd),
|
||||
|
||||
/// PTY proxy for atuin
|
||||
#[cfg(feature = "pty-proxy")]
|
||||
#[command(alias = "hex")]
|
||||
#[command(alias = "hex", about = fl!("cmd-pty-proxy"))]
|
||||
PtyProxy(atuin_pty_proxy::PtyProxy),
|
||||
|
||||
/// Generate a UUID
|
||||
// Plumbing: every shell init calls `atuin uuid` to seed ATUIN_SESSION; not for interactive use.
|
||||
#[command(hide = true)]
|
||||
#[command(hide = true, about = fl!("cmd-uuid"))]
|
||||
Uuid,
|
||||
|
||||
/// List the people who have contributed to Atuin
|
||||
#[command(about = fl!("cmd-contributors"))]
|
||||
Contributors,
|
||||
|
||||
/// Generate shell completions
|
||||
#[command(about = fl!("cmd-gen-completions"))]
|
||||
GenCompletions(gen_completions::Cmd),
|
||||
|
||||
#[command(external_subcommand)]
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
//! Localization of user-facing text.
|
||||
//!
|
||||
//! Messages are Fluent strings in `i18n/<language>/atuin.ftl`, looked up by id with [`fl!`]. `fl!`
|
||||
//! fails the build if an id is missing from the `en-US` catalogue or its arguments don't match the
|
||||
//! message's variables.
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use i18n_embed::LanguageLoader;
|
||||
use i18n_embed::fluent::{FluentLanguageLoader, fluent_language_loader};
|
||||
use i18n_embed::unic_langid::LanguageIdentifier;
|
||||
|
||||
#[allow(
|
||||
clippy::same_name_method,
|
||||
reason = "RustEmbed derives inherent `get`/`iter` next to its trait methods, as sibling impls"
|
||||
)]
|
||||
mod embed {
|
||||
#[derive(rust_embed::RustEmbed)]
|
||||
#[folder = "i18n"]
|
||||
pub struct Localizations;
|
||||
}
|
||||
use embed::Localizations;
|
||||
|
||||
/// The message catalogue for the user's language, negotiated on first use.
|
||||
pub static LOADER: LazyLock<FluentLanguageLoader> = LazyLock::new(|| {
|
||||
/// The language named by the first non-empty of `LC_ALL`, `LC_MESSAGES` and `LANG`, if any.
|
||||
///
|
||||
/// Do **not** swap this for `DesktopLanguageRequester`: on macOS it goes through CoreFoundation,
|
||||
/// and a daemon that forks after CoreFoundation is initialized crashes on its next use of it.
|
||||
fn requested_language() -> Option<LanguageIdentifier> {
|
||||
let locale = ["LC_ALL", "LC_MESSAGES", "LANG"]
|
||||
.into_iter()
|
||||
.find_map(|var| std::env::var(var).ok().filter(|locale| !locale.is_empty()))?;
|
||||
language_from_locale(&locale)
|
||||
}
|
||||
|
||||
/// Parse a POSIX locale such as `en_US.UTF-8` into a language, if it names one (`C` does not).
|
||||
fn language_from_locale(locale: &str) -> Option<LanguageIdentifier> {
|
||||
let name = locale.split(['.', '@']).next()?;
|
||||
if matches!(name, "C" | "POSIX") {
|
||||
return None;
|
||||
}
|
||||
name.replace('_', "-").parse().ok()
|
||||
}
|
||||
|
||||
let loader: FluentLanguageLoader = fluent_language_loader!();
|
||||
|
||||
let requested: Vec<_> = requested_language().into_iter().collect();
|
||||
if let Err(err) = i18n_embed::select(&loader, &Localizations, &requested) {
|
||||
tracing::warn!(?err, "failed to load translations; falling back to en-US");
|
||||
loader
|
||||
.load_fallback_language(&Localizations)
|
||||
.expect("fl! checks the en-US catalogue at compile time");
|
||||
}
|
||||
|
||||
// Fluent wraps arguments in U+2068/U+2069 for bidi isolation; in CLI output those invisible
|
||||
// characters end up in copied text and in scripts matching on it. This only applies to
|
||||
// bundles that are already loaded, so it MUST come after loading.
|
||||
loader.set_use_isolating(false);
|
||||
loader
|
||||
});
|
||||
|
||||
/// Look up a message in the user's language: `fl!("message-id", name = value)`, or one of its
|
||||
/// attributes: `fl!("message-id", "long")`.
|
||||
macro_rules! fl {
|
||||
($message_id:literal) => {
|
||||
i18n_embed_fl::fl!($crate::i18n::LOADER, $message_id)
|
||||
};
|
||||
($message_id:literal, $attribute_id:literal) => {
|
||||
i18n_embed_fl::fl!($crate::i18n::LOADER, $message_id, $attribute_id)
|
||||
};
|
||||
// Each value is parenthesized: i18n-embed-fl expands it as `$value.into()`, so `n = *x` would
|
||||
// otherwise become `*(x.into())`.
|
||||
($message_id:literal, $attribute_id:literal, $($name:ident = $value:expr),+ $(,)?) => {
|
||||
i18n_embed_fl::fl!($crate::i18n::LOADER, $message_id, $attribute_id, $($name = ($value)),+)
|
||||
};
|
||||
($message_id:literal, $($name:ident = $value:expr),* $(,)?) => {
|
||||
i18n_embed_fl::fl!($crate::i18n::LOADER, $message_id, $($name = ($value)),*)
|
||||
};
|
||||
}
|
||||
|
||||
/// [`write!`](std::write) a message looked up with [`fl!`].
|
||||
macro_rules! writet {
|
||||
($dst:expr, $($args:tt)+) => {
|
||||
::std::write!($dst, "{}", $crate::i18n::fl!($($args)+))
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use fl;
|
||||
pub(crate) use writet;
|
||||
@@ -12,11 +12,13 @@ mod otel;
|
||||
use otel::OtelCtx;
|
||||
pub use otel::OtelCtxEnableError;
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum LogCtxEnableError {
|
||||
#[error("failed to build the otel collector: {0}")]
|
||||
#[error("{}", fl!("log-otel-collector-failed", error = .0.to_string()))]
|
||||
OtelCtx(#[from] OtelCtxEnableError),
|
||||
#[error("failed to initialize subscriber: {0}")]
|
||||
#[error("{}", fl!("log-subscriber-init-failed", error = .0.to_string()))]
|
||||
Subscriber(#[from] TryInitError),
|
||||
}
|
||||
|
||||
@@ -165,7 +167,7 @@ fn clean_up_old_logs(config: &FileConfig) {
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum FileWriterError {
|
||||
#[error("log file name must be utf-8")]
|
||||
#[error("{}", fl!("log-file-name-not-utf8"))]
|
||||
NonUtf8Filename,
|
||||
#[error("{0}")]
|
||||
RollingFileAppender(#[from] rolling::InitError),
|
||||
|
||||
@@ -5,10 +5,7 @@ use tracing_subscriber::registry::LookupSpan;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum OtelCtxEnableError {
|
||||
#[error(
|
||||
"this build of atuin has no OpenTelemetry support: rebuild with the `profiling-traced` \
|
||||
feature (e.g. `cargo build-traced`) to export `ATUIN_OTEL` traces"
|
||||
)]
|
||||
#[error("{}", crate::i18n::fl!("otel-not-compiled"))]
|
||||
NotCompiled,
|
||||
}
|
||||
|
||||
|
||||
@@ -11,15 +11,17 @@ use tracing_subscriber::filter::LevelFilter;
|
||||
use tracing_subscriber::registry::LookupSpan;
|
||||
use url::Url;
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum OtelCtxEnableError {
|
||||
#[error("the given ATUIN_OTEL URL does not appear to be a utf-8 string")]
|
||||
#[error("{}", fl!("otel-url-not-utf8"))]
|
||||
NonUtf8EnvVar(OsString),
|
||||
#[error("the given ATUIN_OTEL failed to parse as URL: {0}")]
|
||||
#[error("{}", fl!("otel-url-invalid", error = .0.to_string()))]
|
||||
InvalidUrl(#[from] url::ParseError),
|
||||
#[error("the given ATUIN_OTEL URL does not appear to be an HTTP(s) URL")]
|
||||
#[error("{}", fl!("otel-url-not-http"))]
|
||||
NonHttpUrl,
|
||||
#[error("failed to construct the exporter: {0}")]
|
||||
#[error("{}", fl!("otel-exporter-build-failed", error = .0.to_string()))]
|
||||
ExporterBuild(#[from] ExporterBuildError),
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@ use clap::builder::styling::{AnsiColor, Effects};
|
||||
use command::AtuinCmd;
|
||||
use eyre::Result;
|
||||
|
||||
use crate::i18n::fl;
|
||||
|
||||
mod command;
|
||||
mod i18n;
|
||||
pub(crate) mod logs;
|
||||
#[cfg(feature = "client")]
|
||||
pub(crate) mod shell;
|
||||
@@ -38,7 +41,6 @@ const STYLES: Styles = Styles::styled()
|
||||
.literal(AnsiColor::Green.on_default().effects(Effects::BOLD))
|
||||
.placeholder(AnsiColor::Green.on_default());
|
||||
|
||||
/// Magical shell history
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
author = "Ellie Huxtable <[email protected]>",
|
||||
@@ -46,6 +48,7 @@ const STYLES: Styles = Styles::styled()
|
||||
long_version = LONG_VERSION,
|
||||
help_template(HELP_TEMPLATE),
|
||||
styles = STYLES,
|
||||
about = fl!("cmd-atuin"),
|
||||
)]
|
||||
struct Atuin {
|
||||
#[command(subcommand)]
|
||||
|
||||
Reference in New Issue
Block a user