aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-06-13 00:50:54 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-06-13 00:50:54 +0200
commit6723829a3398b3c9dd6dc6ae79124f46000606ee (patch)
treea1ec535eddd711a4557e4bcc5b94382c3623504c
parentchore(treewide): Cleanup themes (diff)
downloadatuin-6723829a3398b3c9dd6dc6ae79124f46000606ee.zip
chore(treewide): Remove `cargo` warnings to 0
There are still the `clippy` warnings, but they are for a future date.
-rw-r--r--Cargo.lock340
-rw-r--r--crates/turtle/Cargo.toml49
-rw-r--r--crates/turtle/build.rs15
-rw-r--r--crates/turtle/src/atuin_client/api_client.rs16
-rw-r--r--crates/turtle/src/atuin_client/encryption.rs4
-rw-r--r--crates/turtle/src/atuin_client/history.rs6
-rw-r--r--crates/turtle/src/atuin_client/history/store.rs78
-rw-r--r--crates/turtle/src/atuin_client/record/encryption.rs22
-rw-r--r--crates/turtle/src/atuin_client/record/sqlite_store.rs100
-rw-r--r--crates/turtle/src/atuin_client/record/sync.rs219
-rw-r--r--crates/turtle/src/atuin_client/secrets.rs2
-rw-r--r--crates/turtle/src/atuin_client/settings.rs68
-rw-r--r--crates/turtle/src/atuin_client/settings/watcher.rs4
-rw-r--r--crates/turtle/src/atuin_common/record.rs74
-rw-r--r--crates/turtle/src/atuin_common/utils.rs10
-rw-r--r--crates/turtle/src/atuin_daemon/client.rs57
-rw-r--r--crates/turtle/src/atuin_daemon/components/history.rs21
-rw-r--r--crates/turtle/src/atuin_daemon/components/search.rs13
-rw-r--r--crates/turtle/src/atuin_daemon/components/semantic.rs41
-rw-r--r--crates/turtle/src/atuin_daemon/components/sync.rs37
-rw-r--r--crates/turtle/src/atuin_daemon/control/mod.rs80
-rw-r--r--crates/turtle/src/atuin_daemon/control/service.rs71
-rw-r--r--crates/turtle/src/atuin_daemon/events.rs3
-rw-r--r--crates/turtle/src/atuin_daemon/generated.rs38
-rw-r--r--crates/turtle/src/atuin_daemon/history/mod.rs6
-rw-r--r--crates/turtle/src/atuin_daemon/mod.rs13
-rw-r--r--crates/turtle/src/atuin_daemon/search/index.rs678
-rw-r--r--crates/turtle/src/atuin_daemon/search/mod.rs681
-rw-r--r--crates/turtle/src/atuin_daemon/semantic/mod.rs3
-rw-r--r--crates/turtle/src/atuin_daemon/server.rs38
-rw-r--r--crates/turtle/src/atuin_history/stats.rs16
-rw-r--r--crates/turtle/src/atuin_pty_proxy/osc133.rs5
-rw-r--r--crates/turtle/src/atuin_pty_proxy/runtime.rs26
-rw-r--r--crates/turtle/src/atuin_pty_proxy/screen.rs6
-rw-r--r--crates/turtle/src/atuin_server/database/db/mod.rs2
-rw-r--r--crates/turtle/src/atuin_server/database/db/wrappers.rs2
-rw-r--r--crates/turtle/src/atuin_server/database/mod.rs24
-rw-r--r--crates/turtle/src/command/client.rs4
-rw-r--r--crates/turtle/src/command/client/daemon.rs51
-rw-r--r--crates/turtle/src/command/client/history.rs15
-rw-r--r--crates/turtle/src/command/client/search.rs2
-rw-r--r--crates/turtle/src/command/client/search/engines.rs6
-rw-r--r--crates/turtle/src/command/client/search/engines/db.rs2
-rw-r--r--crates/turtle/src/command/client/search/engines/skim.rs2
-rw-r--r--crates/turtle/src/command/client/search/inspector.rs8
-rw-r--r--crates/turtle/src/command/client/search/interactive.rs158
-rw-r--r--crates/turtle/src/command/client/search/keybindings/defaults.rs32
-rw-r--r--crates/turtle/src/command/client/search/keybindings/key.rs24
-rw-r--r--crates/turtle/src/command/mod.rs12
-rw-r--r--crates/turtle/src/main.rs15
50 files changed, 1254 insertions, 1945 deletions
diff --git a/Cargo.lock b/Cargo.lock
index cfe35268..3e1354f0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -105,15 +105,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
-name = "approx"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
-dependencies = [
- "num-traits",
-]
-
-[[package]]
name = "arboard"
version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -189,7 +180,6 @@ name = "atuin"
version = "18.16.1"
dependencies = [
"arboard",
- "argon2",
"async-trait",
"atuin-nucleo",
"atuin-nucleo-matcher",
@@ -205,46 +195,30 @@ dependencies = [
"daemonize",
"dashmap",
"directories",
- "divan",
"eyre",
"fs-err",
"fs4",
- "futures",
- "futures-util",
"fuzzy-matcher",
- "generic-array",
- "getrandom 0.2.17",
- "glob-match",
- "hex",
- "humantime",
"hyper-util",
- "imara-diff",
"indicatif",
"interim",
"itertools",
"lasso",
"listenfd",
"log",
- "memchr",
"metrics",
"metrics-exporter-prometheus",
- "minijinja",
"minspan",
"norm",
"notify",
- "open",
- "palette",
"portable-pty",
- "pretty_assertions",
"prost",
- "prost-types",
"protox",
"rand 0.8.5",
"ratatui",
"regex",
"reqwest",
"rmp",
- "rpassword",
"runtime-format",
"rustix",
"rustls",
@@ -255,42 +229,30 @@ dependencies = [
"serde_json",
"serde_regex",
"serde_with",
- "sha2",
"shellexpand",
- "shlex",
"signal-hook",
"sql-builder",
"sqlx",
- "strum",
- "strum_macros",
- "sysinfo",
- "tempfile",
- "testing_logger",
"thiserror 2.0.18",
"time",
"tokio",
"tokio-stream",
"toml_edit",
"tonic",
- "tonic-build",
"tonic-prost",
"tonic-prost-build",
- "tonic-types",
"tower",
"tower-http",
"tracing",
"tracing-appender",
"tracing-subscriber",
- "tracing-tree",
"typed-builder",
"unicode-segmentation",
"unicode-width 0.2.2",
"url",
- "urlencoding",
"uuid",
"vt100",
"whoami 2.1.1",
- "xxhash-rust",
]
[[package]]
@@ -461,12 +423,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
-name = "by_address"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
-
-[[package]]
name = "bytemuck"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -581,7 +537,6 @@ dependencies = [
"anstyle",
"clap_lex",
"strsim",
- "terminal_size",
]
[[package]]
@@ -680,12 +635,6 @@ dependencies = [
]
[[package]]
-name = "condtype"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af"
-
-[[package]]
name = "config"
version = "0.15.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1012,12 +961,6 @@ dependencies = [
]
[[package]]
-name = "diff"
-version = "0.1.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
-
-[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1081,31 +1024,6 @@ dependencies = [
]
[[package]]
-name = "divan"
-version = "0.1.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a405457ec78b8fe08b0e32b4a3570ab5dff6dd16eb9e76a5ee0a9d9cbd898933"
-dependencies = [
- "cfg-if",
- "clap",
- "condtype",
- "divan-macros",
- "libc",
- "regex-lite",
-]
-
-[[package]]
-name = "divan-macros"
-version = "0.1.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9556bc800956545d6420a640173e5ba7dfa82f38d3ea5a167eb555bc69ac3323"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.117",
-]
-
-[[package]]
name = "document-features"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1245,12 +1163,6 @@ dependencies = [
]
[[package]]
-name = "fast-srgb8"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
-
-[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1375,21 +1287,6 @@ dependencies = [
]
[[package]]
-name = "futures"
-version = "0.3.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-executor",
- "futures-io",
- "futures-sink",
- "futures-task",
- "futures-util",
-]
-
-[[package]]
name = "futures-channel"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1462,7 +1359,6 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
- "futures-channel",
"futures-core",
"futures-io",
"futures-macro",
@@ -1488,7 +1384,6 @@ version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
- "serde",
"typenum",
"version_check",
"zeroize",
@@ -1541,12 +1436,6 @@ dependencies = [
]
[[package]]
-name = "glob-match"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d"
-
-[[package]]
name = "h2"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1697,12 +1586,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
-name = "humantime"
-version = "2.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
-
-[[package]]
name = "hyper"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1789,7 +1672,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
- "windows-core 0.62.2",
+ "windows-core",
]
[[package]]
@@ -1916,16 +1799,6 @@ dependencies = [
]
[[package]]
-name = "imara-diff"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f01d462f766df78ab820dd06f5eb700233c51f0f4c2e846520eaf4ba6aa5c5c"
-dependencies = [
- "hashbrown 0.15.5",
- "memchr",
-]
-
-[[package]]
name = "indenter"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2054,25 +1927,6 @@ dependencies = [
]
[[package]]
-name = "is-docker"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
-dependencies = [
- "once_cell",
-]
-
-[[package]]
-name = "is-wsl"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
-dependencies = [
- "is-docker",
- "once_cell",
-]
-
-[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2375,12 +2229,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15"
[[package]]
-name = "memo-map"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b"
-
-[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2458,16 +2306,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
-name = "minijinja"
-version = "2.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2929e494b2280e1e18959bb2e121da03347ae896896fdfaceaab43c88a02803f"
-dependencies = [
- "memo-map",
- "serde",
-]
-
-[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2579,15 +2417,6 @@ dependencies = [
]
[[package]]
-name = "ntapi"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
-dependencies = [
- "winapi",
-]
-
-[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2769,17 +2598,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
-name = "open"
-version = "5.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc"
-dependencies = [
- "is-wsl",
- "libc",
- "pathdiff",
-]
-
-[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2811,31 +2629,6 @@ dependencies = [
]
[[package]]
-name = "palette"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6"
-dependencies = [
- "approx",
- "fast-srgb8",
- "palette_derive",
- "phf",
- "serde",
-]
-
-[[package]]
-name = "palette_derive"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30"
-dependencies = [
- "by_address",
- "proc-macro2",
- "quote",
- "syn 2.0.117",
-]
-
-[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3130,16 +2923,6 @@ dependencies = [
]
[[package]]
-name = "pretty_assertions"
-version = "1.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
-dependencies = [
- "diff",
- "yansi",
-]
-
-[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3570,12 +3353,6 @@ dependencies = [
]
[[package]]
-name = "regex-lite"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
-
-[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3644,17 +3421,6 @@ dependencies = [
]
[[package]]
-name = "rpassword"
-version = "7.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39"
-dependencies = [
- "libc",
- "rtoolbox",
- "windows-sys 0.59.0",
-]
-
-[[package]]
name = "rsa"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3675,16 +3441,6 @@ dependencies = [
]
[[package]]
-name = "rtoolbox"
-version = "0.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f"
-dependencies = [
- "libc",
- "windows-sys 0.52.0",
-]
-
-[[package]]
name = "runtime-format"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4524,21 +4280,6 @@ dependencies = [
]
[[package]]
-name = "sysinfo"
-version = "0.30.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3"
-dependencies = [
- "cfg-if",
- "core-foundation-sys",
- "libc",
- "ntapi",
- "once_cell",
- "rayon",
- "windows",
-]
-
-[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4552,16 +4293,6 @@ dependencies = [
]
[[package]]
-name = "terminal_size"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
-dependencies = [
- "rustix",
- "windows-sys 0.60.2",
-]
-
-[[package]]
name = "terminfo"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4625,15 +4356,6 @@ dependencies = [
]
[[package]]
-name = "testing_logger"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d92b727cb45d33ae956f7f46b966b25f1bc712092aeef9dba5ac798fc89f720"
-dependencies = [
- "log",
-]
-
-[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4921,17 +4643,6 @@ dependencies = [
]
[[package]]
-name = "tonic-types"
-version = "0.14.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a875a902255423d34c1f20838ab374126db8eb41625b7947a1d54113b0b7399"
-dependencies = [
- "prost",
- "prost-types",
- "tonic",
-]
-
-[[package]]
name = "tower"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5069,18 +4780,6 @@ dependencies = [
]
[[package]]
-name = "tracing-tree"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac87aa03b6a4d5a7e4810d1a80c19601dbe0f8a837e9177f23af721c7ba7beec"
-dependencies = [
- "nu-ansi-term",
- "tracing-core",
- "tracing-log",
- "tracing-subscriber",
-]
-
-[[package]]
name = "tree_magic_mini"
version = "3.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5232,12 +4931,6 @@ dependencies = [
]
[[package]]
-name = "urlencoding"
-version = "2.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
-
-[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5727,25 +5420,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
-name = "windows"
-version = "0.52.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
-dependencies = [
- "windows-core 0.52.0",
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows-core"
-version = "0.52.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
-dependencies = [
- "windows-targets 0.52.6",
-]
-
-[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6255,18 +5929,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
-name = "xxhash-rust"
-version = "0.8.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
-
-[[package]]
-name = "yansi"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
-
-[[package]]
name = "yoke"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/crates/turtle/Cargo.toml b/crates/turtle/Cargo.toml
index df98aa8f..26ee6771 100644
--- a/crates/turtle/Cargo.toml
+++ b/crates/turtle/Cargo.toml
@@ -11,24 +11,7 @@ license = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }
-[features]
-default = [
- "clipboard",
- "daemon",
- "hex",
- "sync",
- "client",
-]
-
-clipboard = ["arboard"]
-daemon = ["pty-proxy"]
-pty-proxy = []
-client = []
-hex = ["dep:hex"]
-sync = ["urlencoding", "reqwest", "sha2", "hex"]
-
[dependencies]
-argon2 = "0.5"
async-trait = "0.1.58"
atuin-nucleo-matcher = { workspace = true }
atuin-nucleo = { workspace = true }
@@ -46,39 +29,24 @@ directories = "6.0.0"
eyre = "0.6"
fs-err = "3.1"
fs4 = "0.13.1"
-futures = "0.3"
-futures-util = "0.3"
fuzzy-matcher = "0.3.7"
-generic-array = { version = "0.14", features = ["serde"] }
-getrandom = "0.2"
-glob-match = "0.2.1"
-hex = { version = "0.4", optional = true }
-humantime = "2.1.0"
hyper-util = "0.1"
-imara-diff = "0.2"
indicatif = "0.18.0"
interim = { version = "0.2.0", features = ["time_0_3"] }
itertools = "0.14.0"
lasso = { version = "0.7", features = ["multi-threaded"] }
log = "0.4"
-memchr = "2.7"
metrics = "0.24"
metrics-exporter-prometheus = { version = "0.18", default-features = false }
-minijinja = "2.9.0"
minspan = "0.1.5"
norm = { version = "0.1.1", features = ["fzf-v2"] }
notify = "7"
-open = "5"
-palette = { version = "0.7.5", features = ["serializing"] }
-pretty_assertions = "1.3.0"
prost = "0.14"
-prost-types = "0.14"
rand = { version = "0.8.5", features = ["std"] }
ratatui = "0.30.0"
regex = "1.10.5"
-reqwest = { version = "0.13", optional = true, features = ["json", "rustls-no-provider", "stream"], default-features = false }
+reqwest = { version = "0.13", features = ["json", "rustls-no-provider", "stream"], default-features = false }
rmp = { version = "0.8.14" }
-rpassword = "7.0"
runtime-format = "0.1.3"
rustix = { version = "1.1.4", features = ["process", "fs"] }
rustls = { version = "0.23", default-features = false, features = [ "ring", "std", "tls12", ] }
@@ -89,15 +57,9 @@ serde = { version = "1.0.202", features = ["derive"] }
serde_json = "1.0.119"
serde_regex = "1.1.0"
serde_with = "3.8.1"
-sha2 = { version = "0.10", optional = true }
shellexpand = "3"
-shlex = "1.3.0"
sql-builder = "3"
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "time", "postgres", "uuid", "sqlite", "regexp"] }
-strum = { version = "0.27", features = ["strum_macros"] }
-strum_macros = "0.27"
-sysinfo = "0.30.7"
-tempfile = { version = "3.19" }
thiserror = "2"
time = { version = "0.3.47", features = [ "serde-human-readable", "macros", "local-offset", "macros", "formatting", "parsing"] }
tokio = { version = "1", features = ["full"] }
@@ -105,7 +67,6 @@ tokio-stream = { version = "0.1.14", features = ["net"] }
toml_edit = "0.25.4"
tonic = "0.14"
tonic-prost = "0.14"
-tonic-types = "0.14"
tower = "0.5"
tower-http = { version = "0.6", features = ["trace"] }
tracing = "0.1"
@@ -115,14 +76,12 @@ typed-builder = "0.18.2"
unicode-segmentation = "1.11.0"
unicode-width = "0.2"
url = "2.5.2"
-urlencoding = { version = "2.1.0", optional = true }
uuid = { version = "1.9", features = ["v4", "v7", "serde"] }
vt100 = "0.16"
whoami = "2.1.0"
-xxhash-rust = { version = "0.8", features = ["xxh3"] }
[target.'cfg(target_os = "linux")'.dependencies]
-arboard = { version = "3.4", optional = true, default-features = false, features = [ "wayland-data-control", ] }
+arboard = { version = "3.4", default-features = false, features = [ "wayland-data-control", ] }
listenfd = "1.0.1"
[target.'cfg(unix)'.dependencies]
@@ -131,14 +90,10 @@ portable-pty = "0.9"
signal-hook = "0.3"
[dev-dependencies]
-tracing-tree = "0.4"
-divan = "0.1.14"
tokio = { version = "1", features = ["full"] }
-testing_logger = "0.1.1"
[build-dependencies]
protox = "0.9"
-tonic-build = "0.14"
tonic-prost-build = "0.14"
[package.metadata.docs.rs]
diff --git a/crates/turtle/build.rs b/crates/turtle/build.rs
index 5f26e26c..ad4bc3c8 100644
--- a/crates/turtle/build.rs
+++ b/crates/turtle/build.rs
@@ -1,6 +1,7 @@
use std::process::Command;
use std::{env, fs, path::PathBuf};
+use protox::Compiler;
use protox::prost::Message;
fn main() -> Result<(), std::io::Error> {
@@ -24,16 +25,24 @@ fn main() -> Result<(), std::io::Error> {
];
let proto_include_dirs = ["proto"];
- let file_descriptors = protox::compile(proto_paths, proto_include_dirs).unwrap();
+ let file_descriptor_set = Compiler::new(proto_include_dirs)
+ .map_err(std::io::Error::other)?
+ .include_source_info(true)
+ .include_imports(true)
+ .open_files(proto_paths)
+ .map_err(std::io::Error::other)?
+ .file_descriptor_set();
let file_descriptor_path = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR not set"))
.join("file_descriptor_set.bin");
- fs::write(&file_descriptor_path, file_descriptors.encode_to_vec()).unwrap();
+ fs::write(&file_descriptor_path, file_descriptor_set.encode_to_vec()).unwrap();
tonic_prost_build::configure()
.build_server(true)
.file_descriptor_set_path(&file_descriptor_path)
.skip_protoc_run()
- .compile_protos(&proto_paths, &proto_include_dirs)
+ .compile_protos(&proto_paths, &proto_include_dirs)?;
}
+
+ Ok(())
}
diff --git a/crates/turtle/src/atuin_client/api_client.rs b/crates/turtle/src/atuin_client/api_client.rs
index 15d96d93..bd5bf59e 100644
--- a/crates/turtle/src/atuin_client/api_client.rs
+++ b/crates/turtle/src/atuin_client/api_client.rs
@@ -19,8 +19,8 @@ static APP_USER_AGENT: &str = concat!("atuin/", env!("CARGO_PKG_VERSION"),);
pub(crate) struct Client<'a> {
sync_addr: &'a str,
- client: reqwest::Client,
user_id: Uuid,
+ inner: reqwest::Client,
}
fn make_url(address: &str, path: &str, user_id: Uuid) -> Result<String> {
@@ -28,7 +28,7 @@ fn make_url(address: &str, path: &str, user_id: Uuid) -> Result<String> {
// `join()` expects a trailing `/` in order to join paths
// e.g. it treats `http://host:port/subdir` as a file called `subdir`
- let address = &format!("{address}/api/v0/{}/", user_id.to_string());
+ let address = &format!("{address}/api/v0/{user_id}/");
// passing a path with a leading `/` will cause `join()` to replace the entire URL path
let path = path.strip_prefix("/").unwrap_or(path);
@@ -81,7 +81,7 @@ async fn handle_resp_error(resp: Response) -> Result<Response> {
}
if !status.is_success() {
- if let Ok(error) = resp.json::<ErrorResponse>().await {
+ if let Ok(error) = resp.json::<ErrorResponse<'_>>().await {
let reason = error.reason;
if status.is_client_error() {
@@ -117,7 +117,7 @@ impl<'a> Client<'a> {
Ok(Client {
user_id,
sync_addr,
- client: reqwest::Client::builder()
+ inner: reqwest::Client::builder()
.user_agent(APP_USER_AGENT)
.default_headers(headers)
.connect_timeout(Duration::new(connect_timeout, 0))
@@ -130,7 +130,7 @@ impl<'a> Client<'a> {
let url = make_url(self.sync_addr, "/store", self.user_id)?;
let url = Url::parse(url.as_str())?;
- let resp = self.client.delete(url).send().await?;
+ let resp = self.inner.delete(url).send().await?;
handle_resp_error(resp).await?;
@@ -143,7 +143,7 @@ impl<'a> Client<'a> {
debug!("uploading {} records to {url}", records.len());
- let resp = self.client.post(url).json(records).send().await?;
+ let resp = self.inner.post(url).json(records).send().await?;
handle_resp_error(resp).await?;
Ok(())
@@ -169,7 +169,7 @@ impl<'a> Client<'a> {
let url = Url::parse(url.as_str())?;
- let resp = self.client.get(url).send().await?;
+ let resp = self.inner.get(url).send().await?;
let resp = handle_resp_error(resp).await?;
let records = resp.json::<Vec<Record<EncryptedData>>>().await?;
@@ -181,7 +181,7 @@ impl<'a> Client<'a> {
let url = make_url(self.sync_addr, "/record", self.user_id)?;
let url = Url::parse(url.as_str())?;
- let resp = self.client.get(url).send().await?;
+ let resp = self.inner.get(url).send().await?;
let resp = handle_resp_error(resp).await?;
if !ensure_version(&resp)? {
diff --git a/crates/turtle/src/atuin_client/encryption.rs b/crates/turtle/src/atuin_client/encryption.rs
index e9c8d7e9..661a6669 100644
--- a/crates/turtle/src/atuin_client/encryption.rs
+++ b/crates/turtle/src/atuin_client/encryption.rs
@@ -75,7 +75,7 @@ pub(crate) fn decode_key(key: String) -> Result<Key> {
if let Ok(key) = <[u8; 32]>::try_from(&*buf) {
Ok(key.into())
} else {
- let mut bytes = rmp::decode::Bytes::new(&buf);
+ let mut bytes = decode::Bytes::new(&buf);
match Marker::from_u8(buf[0]) {
Marker::Bin8 => {
@@ -91,7 +91,7 @@ pub(crate) fn decode_key(key: String) -> Result<Key> {
let mut key = Key::default();
for i in &mut key {
- *i = rmp::decode::read_int(&mut bytes).map_err(|err| eyre!("{err:?}"))?;
+ *i = decode::read_int(&mut bytes).map_err(|err| eyre!("{err:?}"))?;
}
Ok(key)
}
diff --git a/crates/turtle/src/atuin_client/history.rs b/crates/turtle/src/atuin_client/history.rs
index 1f89cd71..11d60548 100644
--- a/crates/turtle/src/atuin_client/history.rs
+++ b/crates/turtle/src/atuin_client/history.rs
@@ -272,7 +272,7 @@ impl History {
Ok(History {
id: id.to_owned().into(),
- timestamp: OffsetDateTime::from_unix_timestamp_nanos(timestamp as i128)?,
+ timestamp: OffsetDateTime::from_unix_timestamp_nanos(i128::from(timestamp))?,
duration,
exit,
command: command.to_owned(),
@@ -282,7 +282,7 @@ impl History {
author: Self::author_from_hostname(hostname),
intent: None,
deleted_at: deleted_at
- .map(|t| OffsetDateTime::from_unix_timestamp_nanos(t as i128))
+ .map(|t| OffsetDateTime::from_unix_timestamp_nanos(i128::from(t)))
.transpose()?,
})
}
@@ -553,7 +553,7 @@ mod tests {
fn disable_secrets() {
let settings = Settings {
secrets_filter: false,
- ..Settings::utc()
+ ..Settings::new().unwrap()
};
let stripe_key: History = History::capture()
diff --git a/crates/turtle/src/atuin_client/history/store.rs b/crates/turtle/src/atuin_client/history/store.rs
index c6e079f3..9c7771cc 100644
--- a/crates/turtle/src/atuin_client/history/store.rs
+++ b/crates/turtle/src/atuin_client/history/store.rs
@@ -27,12 +27,12 @@ pub(crate) enum HistoryRecord {
}
impl HistoryRecord {
- /// Serialize a history record, returning DecryptedData
+ /// Serialize a history record, returning `DecryptedData`
/// The record will be of a certain type
/// We map those like so:
///
- /// HistoryRecord::Create -> 0
- /// HistoryRecord::Delete-> 1
+ /// `HistoryRecord::Create` -> 0
+ /// `HistoryRecord::Delete`-> 1
///
/// This numeric identifier is then written as the first byte to the buffer. For history, we
/// append the serialized history right afterwards, to avoid having to handle serialization
@@ -47,7 +47,7 @@ impl HistoryRecord {
let mut output = vec![];
match self {
- HistoryRecord::Create(history) => {
+ Self::Create(history) => {
// 0 -> a history create
encode::write_u8(&mut output, 0)?;
@@ -55,12 +55,12 @@ impl HistoryRecord {
encode::write_bin(&mut output, &bytes.0)?;
}
- HistoryRecord::Delete(id) => {
+ Self::Delete(id) => {
// 1 -> a history delete
encode::write_u8(&mut output, 1)?;
encode::write_str(&mut output, id.0.as_str())?;
}
- };
+ }
Ok(DecryptedData(output))
}
@@ -81,11 +81,11 @@ impl HistoryRecord {
0 => {
// not super useful to us atm, but perhaps in the future
// written by write_bin above
- let _ = decode::read_bin_len(&mut bytes).map_err(error_report)?;
+ decode::read_bin_len(&mut bytes).map_err(error_report)?;
let record = History::deserialize(bytes.remaining_slice(), version)?;
- Ok(HistoryRecord::Create(record))
+ Ok(Self::Create(record))
}
// 1 -> HistoryRecord::Delete
@@ -99,7 +99,7 @@ impl HistoryRecord {
);
}
- Ok(HistoryRecord::Delete(id.to_string().into()))
+ Ok(Self::Delete(id.to_string().into()))
}
n => {
@@ -111,7 +111,7 @@ impl HistoryRecord {
impl HistoryStore {
pub(crate) fn new(store: SqliteStore, host_id: HostId, encryption_key: [u8; 32]) -> Self {
- HistoryStore {
+ Self {
store,
host_id,
encryption_key,
@@ -182,7 +182,7 @@ impl HistoryStore {
}
/// Delete a batch of history entries via the record store.
- /// Returns the record IDs so the caller can run incremental_build when ready.
+ /// Returns the record IDs so the caller can run `incremental_build` when ready.
pub(crate) async fn delete_entries(
&self,
entries: impl IntoIterator<Item = History>,
@@ -209,7 +209,7 @@ impl HistoryStore {
let records = self.store.all_tagged(HISTORY_TAG).await?;
let mut ret = Vec::with_capacity(records.len());
- for record in records.into_iter() {
+ for record in records {
let hist = match record.version.as_str() {
HISTORY_VERSION_V0 | HISTORY_VERSION => {
let version = record.version.clone();
@@ -266,33 +266,28 @@ impl HistoryStore {
for id in ids {
let record = self.store.get(*id).await;
- let record = match record {
- Ok(record) => record,
- _ => {
+ if let Ok(record) = record {
+ if record.tag != HISTORY_TAG {
continue;
}
- };
- if record.tag != HISTORY_TAG {
- continue;
- }
+ let version = record.version.clone();
+ let decrypted = record.decrypt::<PASETO_V4>(&self.encryption_key)?;
+ let record = match version.as_str() {
+ HISTORY_VERSION_V0 | HISTORY_VERSION => {
+ HistoryRecord::deserialize(&decrypted.data, version.as_str())?
+ }
+ version => bail!("unknown history version {version:?}"),
+ };
- let version = record.version.clone();
- let decrypted = record.decrypt::<PASETO_V4>(&self.encryption_key)?;
- let record = match version.as_str() {
- HISTORY_VERSION_V0 | HISTORY_VERSION => {
- HistoryRecord::deserialize(&decrypted.data, version.as_str())?
- }
- version => bail!("unknown history version {version:?}"),
- };
-
- match record {
- HistoryRecord::Create(h) => {
- // TODO: benchmark CPU time/memory tradeoff of batch commit vs one at a time
- database.save(&h).await?;
- }
- HistoryRecord::Delete(id) => {
- database.delete_rows(&[id]).await?;
+ match record {
+ HistoryRecord::Create(h) => {
+ // TODO: benchmark CPU time/memory tradeoff of batch commit vs one at a time
+ database.save(&h).await?;
+ }
+ HistoryRecord::Delete(id) => {
+ database.delete_rows(&[id]).await?;
+ }
}
}
}
@@ -306,10 +301,13 @@ impl HistoryStore {
pub(crate) async fn history_ids(&self) -> Result<HashSet<HistoryId>> {
let history = self.history().await?;
- let ret = HashSet::from_iter(history.iter().map(|h| match h {
- HistoryRecord::Create(h) => h.id.clone(),
- HistoryRecord::Delete(id) => id.clone(),
- }));
+ let ret = history
+ .iter()
+ .map(|h| match h {
+ HistoryRecord::Create(h) => h.id.clone(),
+ HistoryRecord::Delete(id) => id.clone(),
+ })
+ .collect::<HashSet<_>>();
Ok(ret)
}
@@ -320,7 +318,7 @@ impl HistoryStore {
ProgressStyle::with_template("{spinner:.blue} {msg}")
.unwrap()
.with_key("eta", |state: &ProgressState, w: &mut dyn Write| {
- write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()
+ write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap();
})
.progress_chars("#>-"),
);
diff --git a/crates/turtle/src/atuin_client/record/encryption.rs b/crates/turtle/src/atuin_client/record/encryption.rs
index 70723bb7..afac5b02 100644
--- a/crates/turtle/src/atuin_client/record/encryption.rs
+++ b/crates/turtle/src/atuin_client/record/encryption.rs
@@ -54,16 +54,16 @@ that happens in the background can make the network calls to the HSM
impl Encryption for PASETO_V4 {
fn re_encrypt(
mut data: EncryptedData,
- _ad: AdditionalData,
+ _ad: AdditionalData<'_>,
old_key: &[u8; 32],
new_key: &[u8; 32],
) -> Result<EncryptedData> {
- let cek = Self::decrypt_cek(data.content_encryption_key, old_key)?;
+ let cek = Self::decrypt_cek(&data.content_encryption_key, old_key)?;
data.content_encryption_key = Self::encrypt_cek(cek, new_key);
Ok(data)
}
- fn encrypt(data: DecryptedData, ad: AdditionalData, key: &[u8; 32]) -> EncryptedData {
+ fn encrypt(data: DecryptedData, ad: AdditionalData<'_>, key: &[u8; 32]) -> EncryptedData {
// generate a random key for this entry
// aka content-encryption-key (CEK)
let random_key = Key::<V4, Local>::new_os_random();
@@ -91,9 +91,13 @@ impl Encryption for PASETO_V4 {
}
}
- fn decrypt(data: EncryptedData, ad: AdditionalData, key: &[u8; 32]) -> Result<DecryptedData> {
+ fn decrypt(
+ data: EncryptedData,
+ ad: AdditionalData<'_>,
+ key: &[u8; 32],
+ ) -> Result<DecryptedData> {
let token = data.data;
- let cek = Self::decrypt_cek(data.content_encryption_key, key)?;
+ let cek = Self::decrypt_cek(&data.content_encryption_key, key)?;
// encode the implicit assertions
let assertions = Assertions::from(ad).encode();
@@ -114,12 +118,12 @@ impl Encryption for PASETO_V4 {
}
impl PASETO_V4 {
- fn decrypt_cek(wrapped_cek: String, key: &[u8; 32]) -> Result<Key<V4, Local>> {
+ fn decrypt_cek(wrapped_cek: &str, key: &[u8; 32]) -> Result<Key<V4, Local>> {
let wrapping_key = Key::<V4, Local>::from_bytes(*key);
// let wrapping_key = PasetoSymmetricKey::from(Key::from(key));
- let AtuinFooter { kid, wpk } = serde_json::from_str(&wrapped_cek)
+ let AtuinFooter { kid, wpk } = serde_json::from_str(wrapped_cek)
.context("wrapped cek did not contain the correct contents")?;
// check that the wrapping key matches the required key to decrypt.
@@ -262,7 +266,7 @@ mod tests {
let data = DecryptedData(vec![1, 2, 3, 4]);
let encrypted = PASETO_V4::encrypt(data, ad, &key.to_bytes());
- let _ = PASETO_V4::decrypt(encrypted, ad, &fake_key.to_bytes()).unwrap_err();
+ drop(PASETO_V4::decrypt(encrypted, ad, &fake_key.to_bytes()).unwrap_err());
}
#[test]
@@ -285,7 +289,7 @@ mod tests {
id: &RecordId(uuid_v7()),
..ad
};
- let _ = PASETO_V4::decrypt(encrypted, ad, &key.to_bytes()).unwrap_err();
+ drop(PASETO_V4::decrypt(encrypted, ad, &key.to_bytes()).unwrap_err());
}
#[test]
diff --git a/crates/turtle/src/atuin_client/record/sqlite_store.rs b/crates/turtle/src/atuin_client/record/sqlite_store.rs
index 24188443..12f9fc4e 100644
--- a/crates/turtle/src/atuin_client/record/sqlite_store.rs
+++ b/crates/turtle/src/atuin_client/record/sqlite_store.rs
@@ -353,18 +353,17 @@ impl SqliteStore {
pub(crate) async fn purge(&self, key: &[u8; 32]) -> Result<()> {
let all = self.load_all().await?;
- for record in all.iter() {
- match record.clone().decrypt::<PASETO_V4>(key) {
- Ok(_) => continue,
- Err(_) => {
- println!(
- "Failed to decrypt {}, deleting",
- record.id.0.as_hyphenated()
- );
-
- self.delete(record.id).await?;
- }
+ for record in &all {
+ if record.clone().decrypt::<PASETO_V4>(key).is_ok() {
+ continue;
}
+
+ println!(
+ "Failed to decrypt {}, deleting",
+ record.id.0.as_hyphenated()
+ );
+
+ self.delete(record.id).await?;
}
Ok(())
@@ -379,7 +378,6 @@ mod tests {
settings::test_local_timeout,
},
atuin_common::{
- self,
record::{DecryptedData, EncryptedData, Host, HostId, Record},
utils::uuid_v7,
},
@@ -389,9 +387,9 @@ mod tests {
fn test_record() -> Record<EncryptedData> {
Record::builder()
- .host(Host::new(HostId(atuin_common::utils::uuid_v7())))
+ .host(Host::new(HostId(uuid_v7())))
.version("v1".into())
- .tag(atuin_common::utils::uuid_v7().simple().to_string())
+ .tag(uuid_v7().simple().to_string())
.data(EncryptedData {
data: "1234".into(),
content_encryption_key: "1234".into(),
@@ -507,80 +505,6 @@ mod tests {
}
#[tokio::test]
- async fn len_different_tags() {
- let db = SqliteStore::new(":memory:", test_local_timeout())
- .await
- .unwrap();
-
- // these have different tags, so the len should be the same
- // we model multiple stores within one database
- // new store = new tag = independent length
- let first = test_record();
- let second = test_record();
-
- db.push(&first).await.unwrap();
- db.push(&second).await.unwrap();
-
- let first_len = db.len(first.host.id, first.tag.as_str()).await.unwrap();
- let second_len = db.len(second.host.id, second.tag.as_str()).await.unwrap();
-
- assert_eq!(first_len, 1, "expected length of 1 after insert");
- assert_eq!(second_len, 1, "expected length of 1 after insert");
- }
-
- #[tokio::test]
- async fn append_a_bunch() {
- let db = SqliteStore::new(":memory:", test_local_timeout())
- .await
- .unwrap();
-
- let mut tail = test_record();
- db.push(&tail).await.expect("failed to push record");
-
- for _ in 1..100 {
- tail = tail.append(vec![1, 2, 3, 4]).encrypt::<PASETO_V4>(&[0; 32]);
- db.push(&tail).await.unwrap();
- }
-
- assert_eq!(
- db.len(tail.host.id, tail.tag.as_str()).await.unwrap(),
- 100,
- "failed to insert 100 records"
- );
-
- assert_eq!(
- db.len_tag(tail.tag.as_str()).await.unwrap(),
- 100,
- "failed to insert 100 records"
- );
- }
-
- #[tokio::test]
- async fn append_a_big_bunch() {
- let db = SqliteStore::new(":memory:", test_local_timeout())
- .await
- .unwrap();
-
- let mut records: Vec<Record<EncryptedData>> = Vec::with_capacity(10000);
-
- let mut tail = test_record();
- records.push(tail.clone());
-
- for _ in 1..10000 {
- tail = tail.append(vec![1, 2, 3]).encrypt::<PASETO_V4>(&[0; 32]);
- records.push(tail.clone());
- }
-
- db.push_batch(records.iter()).await.unwrap();
-
- assert_eq!(
- db.len(tail.host.id, tail.tag.as_str()).await.unwrap(),
- 10000,
- "failed to insert 10k records"
- );
- }
-
- #[tokio::test]
async fn re_encrypt() {
let store = SqliteStore::new(":memory:", test_local_timeout())
.await
diff --git a/crates/turtle/src/atuin_client/record/sync.rs b/crates/turtle/src/atuin_client/record/sync.rs
index a86fc7a9..3057bb10 100644
--- a/crates/turtle/src/atuin_client/record/sync.rs
+++ b/crates/turtle/src/atuin_client/record/sync.rs
@@ -14,9 +14,6 @@ use indicatif::{ProgressBar, ProgressState, ProgressStyle};
#[derive(Error, Debug)]
pub(crate) enum SyncError {
- #[error("the local store is ahead of the remote, but for another host. has remote lost data?")]
- LocalAheadOtherHost,
-
#[error("an issue with the local database occurred: {msg:?}")]
LocalStoreError { msg: String },
@@ -319,7 +316,7 @@ pub(crate) async fn sync_remote(
downloaded.append(&mut d);
}
- Operation::Noop { .. } => continue,
+ Operation::Noop { .. } => (),
}
}
@@ -376,14 +373,13 @@ pub(crate) async fn sync(
#[cfg(test)]
mod tests {
+ use crate::atuin_client::record::sync::Operation;
use crate::atuin_common::record::{Diff, EncryptedData, HostId, Record};
- use pretty_assertions::assert_eq;
use crate::atuin_client::{
record::{
- encryption::PASETO_V4,
sqlite_store::SqliteStore,
- sync::{self, Operation},
+ sync::{self},
},
settings::test_local_timeout,
};
@@ -442,7 +438,7 @@ mod tests {
assert_eq!(diff.len(), 1);
- let operations = sync::operations(diff, &store).await.unwrap();
+ let operations = sync::operations(diff, &store).unwrap();
assert_eq!(operations.len(), 1);
@@ -456,211 +452,4 @@ mod tests {
}
);
}
-
- #[tokio::test]
- async fn build_two_way_diff() {
- // a diff where local is ahead of remote for one, and remote for
- // another. One upload, one download
-
- let shared_record = test_record();
- let remote_ahead = test_record();
-
- let local_ahead = shared_record
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
-
- assert_eq!(local_ahead.idx, 1);
-
- let local = vec![shared_record.clone(), local_ahead.clone()]; // local knows about the already synced, and something newer in the same store
- let remote = vec![shared_record.clone(), remote_ahead.clone()]; // remote knows about the already-synced, and one new record in a new store
-
- let (store, diff) = build_test_diff(local, remote).await;
- let operations = sync::operations(diff, &store).await.unwrap();
-
- assert_eq!(operations.len(), 2);
-
- assert_eq!(
- operations,
- vec![
- // Or in otherwords, local is ahead by one
- Operation::Upload {
- host: local_ahead.host.id,
- tag: local_ahead.tag,
- local: 1,
- remote: Some(0),
- },
- // Or in other words, remote knows of a record in an entirely new store (tag)
- Operation::Download {
- host: remote_ahead.host.id,
- tag: remote_ahead.tag,
- local: None,
- remote: 0,
- },
- ]
- );
- }
-
- #[tokio::test]
- async fn build_complex_diff() {
- // One shared, ahead but known only by remote
- // One known only by local
- // One known only by remote
-
- let shared_record = test_record();
- let local_only = test_record();
-
- let local_only_20 = test_record();
- let local_only_21 = local_only_20
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
- let local_only_22 = local_only_21
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
- let local_only_23 = local_only_22
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
-
- let remote_only = test_record();
-
- let remote_only_20 = test_record();
- let remote_only_21 = remote_only_20
- .append(vec![2, 3, 2])
- .encrypt::<PASETO_V4>(&[0; 32]);
- let remote_only_22 = remote_only_21
- .append(vec![2, 3, 2])
- .encrypt::<PASETO_V4>(&[0; 32]);
- let remote_only_23 = remote_only_22
- .append(vec![2, 3, 2])
- .encrypt::<PASETO_V4>(&[0; 32]);
- let remote_only_24 = remote_only_23
- .append(vec![2, 3, 2])
- .encrypt::<PASETO_V4>(&[0; 32]);
-
- let second_shared = test_record();
- let second_shared_remote_ahead = second_shared
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
- let second_shared_remote_ahead2 = second_shared_remote_ahead
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
-
- let third_shared = test_record();
- let third_shared_local_ahead = third_shared
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
- let third_shared_local_ahead2 = third_shared_local_ahead
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
-
- let fourth_shared = test_record();
- let fourth_shared_remote_ahead = fourth_shared
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
- let fourth_shared_remote_ahead2 = fourth_shared_remote_ahead
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
-
- let local = vec![
- shared_record.clone(),
- second_shared.clone(),
- third_shared.clone(),
- fourth_shared.clone(),
- fourth_shared_remote_ahead.clone(),
- // single store, only local has it
- local_only.clone(),
- // bigger store, also only known by local
- local_only_20.clone(),
- local_only_21.clone(),
- local_only_22.clone(),
- local_only_23.clone(),
- // another shared store, but local is ahead on this one
- third_shared_local_ahead.clone(),
- third_shared_local_ahead2.clone(),
- ];
-
- let remote = vec![
- remote_only.clone(),
- remote_only_20.clone(),
- remote_only_21.clone(),
- remote_only_22.clone(),
- remote_only_23.clone(),
- remote_only_24.clone(),
- shared_record.clone(),
- second_shared.clone(),
- third_shared.clone(),
- second_shared_remote_ahead.clone(),
- second_shared_remote_ahead2.clone(),
- fourth_shared.clone(),
- fourth_shared_remote_ahead.clone(),
- fourth_shared_remote_ahead2.clone(),
- ]; // remote knows about the already-synced, and one new record in a new store
-
- let (store, diff) = build_test_diff(local, remote).await;
- let operations = sync::operations(diff, &store).await.unwrap();
-
- assert_eq!(operations.len(), 7);
-
- let mut result_ops = vec![
- // We started with a shared record, but the remote knows of two newer records in the
- // same store
- Operation::Download {
- local: Some(0),
- remote: 2,
- host: second_shared_remote_ahead.host.id,
- tag: second_shared_remote_ahead.tag,
- },
- // We have a shared record, local knows of the first two but not the last
- Operation::Download {
- local: Some(1),
- remote: 2,
- host: fourth_shared_remote_ahead2.host.id,
- tag: fourth_shared_remote_ahead2.tag,
- },
- // Remote knows of a store with a single record that local does not have
- Operation::Download {
- local: None,
- remote: 0,
- host: remote_only.host.id,
- tag: remote_only.tag,
- },
- // Remote knows of a store with a bunch of records that local does not have
- Operation::Download {
- local: None,
- remote: 4,
- host: remote_only_20.host.id,
- tag: remote_only_20.tag,
- },
- // Local knows of a record in a store that remote does not have
- Operation::Upload {
- local: 0,
- remote: None,
- host: local_only.host.id,
- tag: local_only.tag,
- },
- // Local knows of 4 records in a store that remote does not have
- Operation::Upload {
- local: 3,
- remote: None,
- host: local_only_20.host.id,
- tag: local_only_20.tag,
- },
- // Local knows of 2 more records in a shared store that remote only has one of
- Operation::Upload {
- local: 2,
- remote: Some(0),
- host: third_shared.host.id,
- tag: third_shared.tag,
- },
- ];
-
- result_ops.sort_by_key(|op| match op {
- Operation::Noop { host, tag } => (0, *host, tag.clone()),
-
- Operation::Upload { host, tag, .. } => (1, *host, tag.clone()),
-
- Operation::Download { host, tag, .. } => (2, *host, tag.clone()),
- });
-
- assert_eq!(result_ops, operations);
- }
}
diff --git a/crates/turtle/src/atuin_client/secrets.rs b/crates/turtle/src/atuin_client/secrets.rs
index 30723890..74d47ea6 100644
--- a/crates/turtle/src/atuin_client/secrets.rs
+++ b/crates/turtle/src/atuin_client/secrets.rs
@@ -16,7 +16,7 @@ type SpType<'a> = &'a [(&'a str, &'a str, TestValue<'a>)];
type SpType<'a> = &'a [(&'a str, &'a str)];
/// A list of `(name, regex, test)`, where `test` should match against `regex`.
-pub(crate) static SECRET_PATTERNS: SpType = &[
+pub(crate) static SECRET_PATTERNS: SpType<'_> = &[
(
"AWS Access Key ID",
"A[KS]IA[0-9A-Z]{16}",
diff --git a/crates/turtle/src/atuin_client/settings.rs b/crates/turtle/src/atuin_client/settings.rs
index c966ba67..074b2634 100644
--- a/crates/turtle/src/atuin_client/settings.rs
+++ b/crates/turtle/src/atuin_client/settings.rs
@@ -47,7 +47,7 @@ pub(crate) enum SearchMode {
}
impl SearchMode {
- pub(crate) fn as_str(&self) -> &'static str {
+ pub(crate) fn as_str(self) -> &'static str {
match self {
SearchMode::Prefix => "PREFIX",
SearchMode::FullText => "FULLTXT",
@@ -56,7 +56,7 @@ impl SearchMode {
SearchMode::DaemonFuzzy => "DAEMON",
}
}
- pub(crate) fn next(&self, settings: &Settings) -> Self {
+ pub(crate) fn next(self, settings: &Settings) -> Self {
match self {
SearchMode::Prefix => SearchMode::FullText,
// if the user is using skim, we go to skim
@@ -94,7 +94,7 @@ pub(crate) enum FilterMode {
}
impl FilterMode {
- pub(crate) fn as_str(&self) -> &'static str {
+ pub(crate) fn as_str(self) -> &'static str {
match self {
FilterMode::Global => "GLOBAL",
FilterMode::Host => "HOST",
@@ -427,7 +427,7 @@ pub(crate) struct Search {
pub(crate) frecency_score_multiplier: f64,
}
-/// Log level for file logging. Maps to tracing's LevelFilter.
+/// Log level for file logging. Maps to tracing's [`LevelFilter`].
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum LogLevel {
@@ -440,8 +440,8 @@ pub(crate) enum LogLevel {
}
impl LogLevel {
- /// Convert to a tracing directive string for use with EnvFilter.
- pub(crate) fn as_directive(&self) -> &'static str {
+ /// Convert to a tracing directive string for use with [`EnvFilter`].
+ pub(crate) fn as_directive(self) -> &'static str {
match self {
LogLevel::Trace => "trace",
LogLevel::Debug => "debug",
@@ -478,7 +478,7 @@ pub(crate) struct Logs {
pub(crate) dir: String,
/// Default log level for file logging. Defaults to "info".
- /// Note: ATUIN_LOG environment variable overrides this.
+ /// Note: [`ATUIN_LOG`] environment variable overrides this.
#[serde(default)]
pub(crate) level: LogLevel,
@@ -641,7 +641,7 @@ pub(crate) enum UiColumnType {
impl UiColumnType {
/// Returns the default width for this column type (in characters).
/// The Command column returns 0 as it expands to fill remaining space.
- pub(crate) fn default_width(&self) -> u16 {
+ pub(crate) fn default_width(self) -> u16 {
match self {
UiColumnType::Duration => 5, // "814ms"
UiColumnType::Time => 9, // "459ms ago"
@@ -683,7 +683,7 @@ impl UiColumn {
// Custom deserialize to handle both string and object formats:
// "duration" or { type = "duration", width = 8, expand = true }
-impl<'de> serde::Deserialize<'de> for UiColumn {
+impl<'de> Deserialize<'de> for UiColumn {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
@@ -695,7 +695,7 @@ impl<'de> serde::Deserialize<'de> for UiColumn {
impl<'de> Visitor<'de> for UiColumnVisitor {
type Value = UiColumn;
- fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+ fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(
"a column type string or an object with 'type' and optional 'width'/'expand'",
)
@@ -706,7 +706,7 @@ impl<'de> serde::Deserialize<'de> for UiColumn {
E: de::Error,
{
let column_type: UiColumnType =
- serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(value))?;
+ Deserialize::deserialize(de::value::StrDeserializer::new(value))?;
Ok(UiColumn::new(column_type))
}
@@ -730,7 +730,7 @@ impl<'de> serde::Deserialize<'de> for UiColumn {
expand = Some(map.next_value()?);
}
_ => {
- let _: serde::de::IgnoredAny = map.next_value()?;
+ let _: de::IgnoredAny = map.next_value()?;
}
}
}
@@ -979,18 +979,19 @@ impl Settings {
}
pub(crate) fn builder() -> Result<ConfigBuilder<DefaultState>> {
- Self::builder_with_data_dir(&crate::atuin_common::utils::data_dir())
+ Self::builder_with_data_dir(&utils::data_dir())
}
+ #[expect(clippy::too_many_lines)]
fn builder_with_data_dir(data_dir: &std::path::Path) -> Result<ConfigBuilder<DefaultState>> {
let db_path = data_dir.join("history.db");
let record_store_path = data_dir.join("records.db");
let kv_path = data_dir.join("kv.db");
let scripts_path = data_dir.join("scripts.db");
let ai_sessions_path = data_dir.join("ai_sessions.db");
- let socket_path = crate::atuin_common::utils::runtime_dir().join("atuin.sock");
+ let socket_path = utils::runtime_dir().join("atuin.sock");
let pidfile_path = data_dir.join("atuin-daemon.pid");
- let logs_dir = crate::atuin_common::utils::logs_dir();
+ let logs_dir = utils::logs_dir();
let key_path = data_dir.join("key");
let meta_path = data_dir.join("meta.db");
@@ -1091,10 +1092,10 @@ impl Settings {
.set_default("tmux.height", "60%")?
.set_default(
"prefers_reduced_motion",
- std::env::var("NO_MOTION")
- .ok()
- .map(|_| config::Value::new(None, config::ValueKind::Boolean(true)))
- .unwrap_or_else(|| config::Value::new(None, config::ValueKind::Boolean(false))),
+ std::env::var("NO_MOTION").ok().map_or_else(
+ || config::Value::new(None, config::ValueKind::Boolean(false)),
+ |_| config::Value::new(None, config::ValueKind::Boolean(true)),
+ ),
)?
.set_default("no_mouse", false)?
.add_source(
@@ -1105,18 +1106,19 @@ impl Settings {
}
pub(crate) fn get_config_path() -> Result<PathBuf> {
- let config_dir = crate::atuin_common::utils::config_dir();
+ let config_dir = utils::config_dir();
create_dir_all(&config_dir)
.wrap_err_with(|| format!("could not create dir {}", config_dir.display()))?;
- let mut config_file = if let Ok(p) = std::env::var("ATUIN_CONFIG_DIR") {
- PathBuf::from(p)
- } else {
- let mut config_file = PathBuf::new();
- config_file.push(config_dir);
- config_file
- };
+ let mut config_file = std::env::var("ATUIN_CONFIG_DIR").map_or_else(
+ |_| {
+ let mut config_file = PathBuf::new();
+ config_file.push(config_dir);
+ config_file
+ },
+ PathBuf::from,
+ );
config_file.push("config.toml");
@@ -1162,16 +1164,16 @@ impl Settings {
.map_err(|e| eyre!("failed to expand data_dir path: {}", e))?;
PathBuf::from(expanded.as_ref())
}
- None => crate::atuin_common::utils::data_dir(),
+ None => utils::data_dir(),
}
} else {
- crate::atuin_common::utils::data_dir()
+ utils::data_dir()
};
DATA_DIR.set(effective_data_dir.clone()).ok();
create_dir_all(&effective_data_dir)
- .wrap_err_with(|| format!("could not create dir {effective_data_dir:?}"))?;
+ .wrap_err_with(|| format!("could not create dir {}", effective_data_dir.display()))?;
let mut config_builder = Self::builder_with_data_dir(&effective_data_dir)?;
@@ -1205,7 +1207,7 @@ impl Settings {
]
.iter()
.map(|key| (key, built.get_string(key).unwrap_or_default()))
- .filter_map(|(key, value)| match Self::expand_path(value) {
+ .filter_map(|(key, value)| match Self::expand_path(&value) {
Ok(expanded) => Some((key, expanded)),
Err(e) => {
log::warn!("failed to expand path for {key}: {e}");
@@ -1263,7 +1265,7 @@ impl Settings {
let full_key = if prefix.is_empty() {
k.clone()
} else {
- format!("{}.{}", prefix, k)
+ format!("{prefix}.{k}")
};
match &v.kind {
@@ -1302,7 +1304,7 @@ impl Settings {
Ok(settings)
}
- fn expand_path(path: String) -> Result<String> {
+ fn expand_path(path: &str) -> Result<String> {
shellexpand::full(&path)
.map(|p| p.to_string())
.map_err(|e| eyre!("failed to expand path: {}", e))
diff --git a/crates/turtle/src/atuin_client/settings/watcher.rs b/crates/turtle/src/atuin_client/settings/watcher.rs
index e280480c..5eec3692 100644
--- a/crates/turtle/src/atuin_client/settings/watcher.rs
+++ b/crates/turtle/src/atuin_client/settings/watcher.rs
@@ -134,7 +134,7 @@ impl SettingsWatcher {
warn!(
"config watcher: event has no paths, triggering reload to be safe"
);
- let _ = debounce_tx.send(());
+ debounce_tx.send(()).expect("should still be active");
return;
}
@@ -163,7 +163,7 @@ impl SettingsWatcher {
) {
debug!("config file event detected: {event:?}");
// Send to debounce channel (ignore send errors - receiver might be gone)
- let _ = debounce_tx.send(());
+ debounce_tx.send(()).ok();
}
}
Err(e) => {
diff --git a/crates/turtle/src/atuin_common/record.rs b/crates/turtle/src/atuin_common/record.rs
index 8a10804c..7b2a1a10 100644
--- a/crates/turtle/src/atuin_common/record.rs
+++ b/crates/turtle/src/atuin_common/record.rs
@@ -30,7 +30,7 @@ pub(crate) struct Host {
impl Host {
pub(crate) fn new(id: HostId) -> Self {
- Host {
+ Self {
id,
name: String::new(),
}
@@ -117,8 +117,8 @@ impl RecordStatus {
self.hosts.entry(host).or_default().insert(tag, tail_id);
}
- pub(crate) fn get(&self, host: HostId, tag: String) -> Option<RecordIdx> {
- self.hosts.get(&host).and_then(|v| v.get(&tag)).copied()
+ pub(crate) fn get(&self, host: HostId, tag: &str) -> Option<RecordIdx> {
+ self.hosts.get(&host).and_then(|v| v.get(tag)).copied()
}
/// Diff this index with another, likely remote index.
@@ -134,7 +134,7 @@ impl RecordStatus {
// First, we check if other has everything that self has
for (host, tag_map) in &self.hosts {
for (tag, idx) in tag_map {
- match other.get(*host, tag.clone()) {
+ match other.get(*host, tag) {
// The other store is all up to date! No diff.
Some(t) if t.eq(idx) => (),
@@ -163,7 +163,7 @@ impl RecordStatus {
// account for that!
for (host, tag_map) in &other.hosts {
for (tag, idx) in tag_map {
- match self.get(*host, tag.clone()) {
+ match self.get(*host, tag) {
// If we have this host/tag combo, the comparison and diff will have already happened above
Some(_) => (),
@@ -186,15 +186,19 @@ impl RecordStatus {
pub(crate) trait Encryption {
fn re_encrypt(
data: EncryptedData,
- ad: AdditionalData,
+ ad: AdditionalData<'_>,
old_key: &[u8; 32],
new_key: &[u8; 32],
) -> Result<EncryptedData> {
let data = Self::decrypt(data, ad, old_key)?;
Ok(Self::encrypt(data, ad, new_key))
}
- fn encrypt(data: DecryptedData, ad: AdditionalData, key: &[u8; 32]) -> EncryptedData;
- fn decrypt(data: EncryptedData, ad: AdditionalData, key: &[u8; 32]) -> Result<DecryptedData>;
+ fn encrypt(data: DecryptedData, ad: AdditionalData<'_>, key: &[u8; 32]) -> EncryptedData;
+ fn decrypt(
+ data: EncryptedData,
+ ad: AdditionalData<'_>,
+ key: &[u8; 32],
+ ) -> Result<DecryptedData>;
}
impl Record<DecryptedData> {
@@ -266,8 +270,7 @@ impl Record<EncryptedData> {
mod tests {
use crate::atuin_common::record::{Host, HostId};
- use super::{DecryptedData, Record, RecordStatus};
- use pretty_assertions::assert_eq;
+ use super::{DecryptedData, Record};
fn test_record() -> Record<DecryptedData> {
Record::builder()
@@ -278,55 +281,4 @@ mod tests {
.idx(0)
.build()
}
-
- #[test]
- fn record_index_multi_diff() {
- // A much more complex case, with a bunch more checks
- let mut index1 = RecordStatus::new();
- let mut index2 = RecordStatus::new();
-
- let store1record1 = test_record();
- let store1record2 = store1record1.append(vec![1, 2, 3]);
-
- let store2record1 = test_record();
- let store2record2 = store2record1.append(vec![1, 2, 3]);
-
- let store3record1 = test_record();
-
- let store4record1 = test_record();
-
- // index1 only knows about the first two entries of the first two stores
- index1.set(store1record1);
- index1.set(store2record1);
-
- // index2 is fully up to date with the first two stores, and knows of a third
- index2.set(store1record2);
- index2.set(store2record2);
- index2.set(store3record1);
-
- // index1 knows of a 4th store
- index1.set(store4record1);
-
- let diff1 = index1.diff(&index2);
- let diff2 = index2.diff(&index1);
-
- // both diffs the same length
- assert_eq!(4, diff1.len());
- assert_eq!(4, diff2.len());
-
- dbg!(&diff1, &diff2);
-
- // both diffs should be ALMOST the same. They will agree on which hosts and tags
- // require updating, but the "other" value will not be the same.
- let smol_diff_1: Vec<(HostId, String)> =
- diff1.iter().map(|v| (v.host, v.tag.clone())).collect();
- let smol_diff_2: Vec<(HostId, String)> =
- diff1.iter().map(|v| (v.host, v.tag.clone())).collect();
-
- assert_eq!(smol_diff_1, smol_diff_2);
-
- // diffing with yourself = no diff
- assert_eq!(index1.diff(&index1).len(), 0);
- assert_eq!(index2.diff(&index2).len(), 0);
- }
}
diff --git a/crates/turtle/src/atuin_common/utils.rs b/crates/turtle/src/atuin_common/utils.rs
index ba0c8eb7..c8c2776e 100644
--- a/crates/turtle/src/atuin_common/utils.rs
+++ b/crates/turtle/src/atuin_common/utils.rs
@@ -81,19 +81,19 @@ pub(crate) fn home_dir() -> PathBuf {
pub(crate) fn config_dir() -> PathBuf {
let config_dir =
- std::env::var("XDG_CONFIG_HOME").map_or_else(|_| home_dir().join(".config"), PathBuf::from);
+ env::var("XDG_CONFIG_HOME").map_or_else(|_| home_dir().join(".config"), PathBuf::from);
config_dir.join("atuin")
}
pub(crate) fn data_dir() -> PathBuf {
- let data_dir = std::env::var("XDG_DATA_HOME")
+ let data_dir = env::var("XDG_DATA_HOME")
.map_or_else(|_| home_dir().join(".local").join("share"), PathBuf::from);
data_dir.join("atuin")
}
pub(crate) fn runtime_dir() -> PathBuf {
- std::env::var("XDG_RUNTIME_DIR").map_or_else(|_| data_dir(), PathBuf::from)
+ env::var("XDG_RUNTIME_DIR").map_or_else(|_| data_dir(), PathBuf::from)
}
pub(crate) fn logs_dir() -> PathBuf {
@@ -217,7 +217,7 @@ mod tests {
fn in_git_repo_regular() {
// regular git repo should resolve to the directory containing .git
let tmp = std::env::temp_dir().join("atuin-test-regular-git");
- let _ = std::fs::remove_dir_all(&tmp);
+ drop(std::fs::remove_dir_all(&tmp));
let subdir = tmp.join("src").join("deep");
std::fs::create_dir_all(&subdir).unwrap();
std::fs::create_dir_all(tmp.join(".git")).unwrap();
@@ -234,7 +234,7 @@ mod tests {
// worktree .git is a file pointing back to the main repo —
// in_git_repo should follow it so all worktrees share a workspace
let tmp = std::env::temp_dir().join("atuin-test-worktree-git");
- let _ = std::fs::remove_dir_all(&tmp);
+ drop(std::fs::remove_dir_all(&tmp));
// main repo at tmp/main with a real .git directory
let main_repo = tmp.join("main");
diff --git a/crates/turtle/src/atuin_daemon/client.rs b/crates/turtle/src/atuin_daemon/client.rs
index 325b21b8..4ec1a60b 100644
--- a/crates/turtle/src/atuin_daemon/client.rs
+++ b/crates/turtle/src/atuin_daemon/client.rs
@@ -1,5 +1,3 @@
-use crate::atuin_client::database::Context;
-use crate::atuin_client::settings::{FilterMode, Settings};
use eyre::{Context as EyreContext, Result};
use tonic::Code;
use tonic::transport::{Channel, Endpoint, Uri};
@@ -9,27 +7,38 @@ use hyper_util::rt::TokioIo;
#[cfg(unix)]
use tokio::net::UnixStream;
-
-use crate::atuin_client::history::History;
use tracing::{Level, instrument, span};
-use crate::atuin_daemon::control::HistoryRebuiltEvent;
-use crate::atuin_daemon::control::{
- ForceSyncEvent, HistoryDeletedEvent, HistoryPrunedEvent, SendEventRequest,
- SettingsReloadedEvent, ShutdownEvent, control_client::ControlClient as ControlServiceClient,
-};
-use crate::atuin_daemon::events::DaemonEvent;
-use crate::atuin_daemon::history::{
- EndHistoryReply, EndHistoryRequest, ShutdownRequest, StartHistoryReply, StartHistoryRequest,
- StatusReply, StatusRequest, TailHistoryReply, TailHistoryRequest,
- history_client::HistoryClient as HistoryServiceClient,
-};
-use crate::atuin_daemon::search::{
- FilterMode as RpcFilterMode, SearchContext as RpcSearchContext, SearchRequest, SearchResponse,
- search_client::SearchClient as SearchServiceClient,
-};
-use crate::atuin_daemon::semantic::{
- CommandCapture, RecordCommandsReply, semantic_client::SemanticClient as SemanticServiceClient,
+use crate::atuin_daemon::generated;
+use crate::{
+ atuin_client::{
+ database::Context,
+ history::History,
+ settings::{FilterMode, Settings},
+ },
+ atuin_daemon::{
+ events::DaemonEvent,
+ generated::{
+ control::{
+ ForceSyncEvent, HistoryDeletedEvent, HistoryPrunedEvent, HistoryRebuiltEvent,
+ SendEventRequest, SettingsReloadedEvent, ShutdownEvent,
+ control_client::ControlClient as ControlServiceClient,
+ },
+ history::{
+ EndHistoryReply, EndHistoryRequest, ShutdownRequest, StartHistoryReply,
+ StartHistoryRequest, StatusReply, StatusRequest, TailHistoryReply,
+ TailHistoryRequest, history_client::HistoryClient as HistoryServiceClient,
+ },
+ search::{
+ FilterMode as RpcFilterMode, SearchContext as RpcSearchContext, SearchRequest,
+ SearchResponse, search_client::SearchClient as SearchServiceClient,
+ },
+ semantic::{
+ CommandCapture, RecordCommandsReply,
+ semantic_client::SemanticClient as SemanticServiceClient,
+ },
+ },
+ },
};
pub(crate) struct HistoryClient {
@@ -311,10 +320,8 @@ impl ControlClient {
}
/// Convert a daemon event to its proto representation.
-fn daemon_event_to_proto(
- event: DaemonEvent,
-) -> crate::atuin_daemon::control::send_event_request::Event {
- use crate::atuin_daemon::control::send_event_request::Event;
+fn daemon_event_to_proto(event: DaemonEvent) -> generated::control::send_event_request::Event {
+ use generated::control::send_event_request::Event;
match event {
DaemonEvent::HistoryPruned => Event::HistoryPruned(HistoryPrunedEvent {}),
diff --git a/crates/turtle/src/atuin_daemon/components/history.rs b/crates/turtle/src/atuin_daemon/components/history.rs
index b71543c1..b4f91b06 100644
--- a/crates/turtle/src/atuin_daemon/components/history.rs
+++ b/crates/turtle/src/atuin_daemon/components/history.rs
@@ -18,7 +18,7 @@ use tracing::{Level, instrument};
use crate::atuin_daemon::{
daemon::{Component, DaemonHandle},
events::DaemonEvent,
- history::{
+ generated::history::{
EndHistoryReply, EndHistoryRequest, HistoryEntry, HistoryEventKind, ShutdownReply,
ShutdownRequest, StartHistoryReply, StartHistoryRequest, StatusReply, StatusRequest,
TailHistoryReply, TailHistoryRequest,
@@ -144,8 +144,8 @@ impl HistorySvc for HistoryGrpcService {
) -> Result<Response<StartHistoryReply>, Status> {
let req = request.into_inner();
- let timestamp =
- OffsetDateTime::from_unix_timestamp_nanos(req.timestamp as i128).map_err(|_| {
+ let timestamp = OffsetDateTime::from_unix_timestamp_nanos(i128::from(req.timestamp))
+ .map_err(|_| {
Status::invalid_argument(
"failed to parse timestamp as unix time (expected nanos since epoch)",
)
@@ -181,6 +181,7 @@ impl HistorySvc for HistoryGrpcService {
}
#[instrument(skip_all, level = Level::INFO)]
+ #[expect(clippy::significant_drop_tightening, reason = "Would be a logic-bug")]
async fn end_history(
&self,
request: Request<EndHistoryRequest>,
@@ -216,11 +217,7 @@ impl HistorySvc for HistoryGrpcService {
.await
.map_err(|e| Status::internal(format!("failed to write to db: {e:?}")))?;
- tracing::info!(
- id = id.0.to_string(),
- duration = history.duration,
- "end history"
- );
+ tracing::info!(id = id.0, duration = history.duration, "end history");
// Push to record store
let (record_id, idx) = history_store
@@ -247,6 +244,7 @@ impl HistorySvc for HistoryGrpcService {
}
#[instrument(skip_all, level = Level::INFO)]
+ #[expect(clippy::significant_drop_tightening, reason = "Would be a logic-bug")]
async fn tail_history(
&self,
_request: Request<TailHistoryRequest>,
@@ -265,11 +263,12 @@ impl HistorySvc for HistoryGrpcService {
let event = match rx.recv().await {
Ok(event) => event,
Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
- let _ = tx
- .send(Err(Status::resource_exhausted(format!(
+ drop(
+ tx.send(Err(Status::resource_exhausted(format!(
"tail stream lagged behind and dropped {skipped} events"
))))
- .await;
+ .await,
+ );
break;
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
diff --git a/crates/turtle/src/atuin_daemon/components/search.rs b/crates/turtle/src/atuin_daemon/components/search.rs
index 832d05d8..39df758b 100644
--- a/crates/turtle/src/atuin_daemon/components/search.rs
+++ b/crates/turtle/src/atuin_daemon/components/search.rs
@@ -15,10 +15,11 @@ use uuid::Uuid;
use crate::atuin_daemon::{
daemon::{Component, DaemonHandle},
events::DaemonEvent,
- search::{
- FilterMode, IndexFilterMode, QueryContext, SearchIndex, SearchRequest, SearchResponse,
+ generated::search::{
+ self, FilterMode, SearchRequest, SearchResponse,
search_server::{Search as SearchSvc, SearchServer},
},
+ search::{IndexFilterMode, QueryContext, SearchIndex},
};
const PAGE_SIZE: usize = 5000;
@@ -35,7 +36,7 @@ const FRECENCY_REFRESH_INTERVAL_SECS: u64 = 60;
/// - Provides the Search gRPC service
pub(crate) struct SearchComponent {
index: Arc<RwLock<SearchIndex>>,
- handle: tokio::sync::RwLock<Option<DaemonHandle>>,
+ handle: RwLock<Option<DaemonHandle>>,
loader_handle: Option<tokio::task::JoinHandle<()>>,
frecency_handle: Option<tokio::task::JoinHandle<()>>,
}
@@ -45,7 +46,7 @@ impl SearchComponent {
pub(crate) fn new() -> Self {
Self {
index: Arc::new(RwLock::new(SearchIndex::new())),
- handle: tokio::sync::RwLock::new(None),
+ handle: RwLock::new(None),
loader_handle: None,
frecency_handle: None,
}
@@ -351,7 +352,7 @@ impl SearchSvc for SearchGrpcService {
}
}
Err(e) => {
- let _ = tx.send(Err(e)).await;
+ drop(tx.send(Err(e)).await);
break;
}
}
@@ -367,7 +368,7 @@ impl SearchSvc for SearchGrpcService {
/// Convert proto FilterMode and context to IndexFilterMode.
fn convert_filter_mode(
mode: FilterMode,
- context: &Option<crate::atuin_daemon::search::SearchContext>,
+ context: &Option<search::SearchContext>,
) -> IndexFilterMode {
match (mode, context) {
(FilterMode::Global, _) => IndexFilterMode::Global,
diff --git a/crates/turtle/src/atuin_daemon/components/semantic.rs b/crates/turtle/src/atuin_daemon/components/semantic.rs
index 052c2d73..69ffc134 100644
--- a/crates/turtle/src/atuin_daemon/components/semantic.rs
+++ b/crates/turtle/src/atuin_daemon/components/semantic.rs
@@ -9,6 +9,7 @@ use std::fmt::{Display, Formatter};
use std::sync::Arc;
use crate::atuin_client::history::{History, HistoryId};
+use crate::atuin_daemon::generated::semantic;
use eyre::Result;
use tokio::sync::Mutex;
use tonic::{Request, Response, Status, Streaming};
@@ -17,7 +18,7 @@ use tracing::{Level, instrument};
use crate::atuin_daemon::{
daemon::{Component, DaemonHandle},
events::DaemonEvent,
- semantic::{
+ generated::semantic::{
CommandCapture, CommandOutputReply, CommandOutputRequest, OutputLine, RecordCommandsReply,
semantic_server::{Semantic as SemanticSvc, SemanticServer},
},
@@ -244,7 +245,7 @@ impl SemanticState {
fn command_output_for_ref(
&self,
capture_ref: &CaptureRef,
- ranges: &[crate::atuin_daemon::semantic::OutputRange],
+ ranges: &[semantic::OutputRange],
) -> Option<CommandOutputReply> {
let stored = self
.sessions
@@ -534,17 +535,14 @@ fn command_output_not_found() -> CommandOutputReply {
}
}
-fn select_output_ranges(
- output: &str,
- ranges: &[crate::atuin_daemon::semantic::OutputRange],
-) -> Vec<OutputLine> {
+fn select_output_ranges(output: &str, ranges: &[semantic::OutputRange]) -> Vec<OutputLine> {
let lines: Vec<&str> = output.lines().collect();
if lines.is_empty() {
return Vec::new();
}
let ranges = if ranges.is_empty() {
- vec![crate::atuin_daemon::semantic::OutputRange { start: 0, end: 999 }]
+ vec![semantic::OutputRange { start: 0, end: 999 }]
} else {
ranges.to_vec()
};
@@ -627,9 +625,20 @@ fn log_record(record: &SemanticCommandRecord, message: &'static str) {
#[cfg(test)]
mod tests {
- use super::*;
use time::OffsetDateTime;
+ use crate::{
+ atuin_client::history::{History, HistoryId},
+ atuin_daemon::{
+ components::semantic::{
+ MAX_COMMANDS_PER_SESSION, MAX_SESSIONS, SemanticCommandRecord, SemanticState,
+ SessionCaptures, SessionId, select_output_ranges,
+ },
+ generated::semantic::{self, CommandOutputReply, CommandOutputRequest, OutputLine},
+ },
+ atuin_pty_proxy::CommandCapture,
+ };
+
fn history(id: &str, session: &str, command: &str) -> History {
History {
id: HistoryId(id.to_string()),
@@ -819,8 +828,8 @@ mod tests {
fn output_ranges_are_line_based_inclusive_and_support_negative_offsets() {
let output = "zero\none\ntwo\nthree\nfour";
let ranges = vec![
- crate::atuin_daemon::semantic::OutputRange { start: 1, end: 2 },
- crate::atuin_daemon::semantic::OutputRange { start: -2, end: -1 },
+ semantic::OutputRange { start: 1, end: 2 },
+ semantic::OutputRange { start: -2, end: -1 },
];
assert_eq!(
@@ -841,8 +850,8 @@ mod tests {
.collect::<Vec<_>>()
.join("\n");
let ranges = vec![
- crate::atuin_daemon::semantic::OutputRange { start: 0, end: 100 },
- crate::atuin_daemon::semantic::OutputRange {
+ semantic::OutputRange { start: 0, end: 100 },
+ semantic::OutputRange {
start: -100,
end: -1,
},
@@ -859,8 +868,8 @@ mod tests {
fn output_ranges_can_leave_gaps_for_client_formatting() {
let output = "zero\none\ntwo\nthree\nfour";
let ranges = vec![
- crate::atuin_daemon::semantic::OutputRange { start: 0, end: 1 },
- crate::atuin_daemon::semantic::OutputRange { start: 4, end: 4 },
+ semantic::OutputRange { start: 0, end: 1 },
+ semantic::OutputRange { start: 4, end: 4 },
];
assert_eq!(
@@ -891,8 +900,8 @@ mod tests {
fn output_ranges_skip_ranges_fully_outside_output() {
let output = "zero\none\ntwo";
let ranges = vec![
- crate::atuin_daemon::semantic::OutputRange { start: 10, end: 20 },
- crate::atuin_daemon::semantic::OutputRange {
+ semantic::OutputRange { start: 10, end: 20 },
+ semantic::OutputRange {
start: -20,
end: -10,
},
diff --git a/crates/turtle/src/atuin_daemon/components/sync.rs b/crates/turtle/src/atuin_daemon/components/sync.rs
index fbfbbd67..8b5b621d 100644
--- a/crates/turtle/src/atuin_daemon/components/sync.rs
+++ b/crates/turtle/src/atuin_daemon/components/sync.rs
@@ -27,9 +27,9 @@ enum SyncCommand {
/// Sync state - tracks whether we're in normal operation or retrying after failure.
#[derive(Clone, Copy, PartialEq, Eq)]
enum SyncState {
- /// Normal operation. Periodic syncs only run if auto_sync is enabled.
+ /// Normal operation. Periodic syncs only run if [`auto_sync`] is enabled.
Idle,
- /// Retrying after a sync failure. Retries continue regardless of auto_sync
+ /// Retrying after a sync failure. Retries continue regardless of [`auto_sync`]
/// until the sync succeeds.
Retrying,
}
@@ -39,7 +39,7 @@ enum SyncState {
/// This component:
/// - Runs a background sync loop on a configurable interval
/// - Implements exponential backoff on sync failures
-/// - Responds to ForceSync events for immediate sync
+/// - Responds to [`ForceSync`] events for immediate sync
/// - Emits SyncCompleted/SyncFailed events
pub(crate) struct SyncComponent {
task_handle: Option<tokio::task::JoinHandle<()>>,
@@ -80,22 +80,28 @@ impl Component for SyncComponent {
}
async fn handle_event(&mut self, event: &DaemonEvent) -> Result<()> {
- if let DaemonEvent::ForceSync = event {
- tracing::info!("force sync requested");
- if let Some(tx) = &self.command_tx {
- let _ = tx.send(SyncCommand::ForceSync).await;
+ match event {
+ DaemonEvent::ForceSync => {
+ tracing::info!("force sync requested");
+ if let Some(tx) = &self.command_tx {
+ drop(tx.send(SyncCommand::ForceSync).await);
+ }
+ }
+ DaemonEvent::SyncFailed { error } => {
+ tracing::error!(?error, "Sync failed.");
}
+ _ => (),
}
Ok(())
}
async fn stop(&mut self) -> Result<()> {
if let Some(tx) = &self.command_tx {
- let _ = tx.send(SyncCommand::Stop).await;
+ drop(tx.send(SyncCommand::Stop).await);
}
if let Some(handle) = self.task_handle.take() {
// Give the task a moment to shut down gracefully
- let _ = tokio::time::timeout(std::time::Duration::from_secs(5), handle).await;
+ drop(time::timeout(Duration::from_secs(5), handle).await);
}
tracing::info!("sync component stopped");
Ok(())
@@ -126,7 +132,7 @@ async fn sync_loop(handle: DaemonHandle, mut cmd_rx: mpsc::Receiver<SyncCommand>
// Don't backoff by more than 30 mins (with a random jitter of up to 1 min)
let max_interval: f64 = 60.0 * 30.0 + rand::thread_rng().gen_range(0.0..60.0);
- let mut ticker = time::interval(time::Duration::from_secs(settings.daemon.sync_frequency));
+ let mut ticker = time::interval(Duration::from_secs(settings.daemon.sync_frequency));
// IMPORTANT: without this, if we miss ticks because a sync takes ages or is otherwise delayed,
// we may end up running a lot of syncs in a hot loop.
@@ -224,10 +230,10 @@ async fn do_sync_tick(
}
*ticker = time::interval_at(
- tokio::time::Instant::now() + Duration::from_secs(new_interval as u64),
- time::Duration::from_secs(new_interval as u64),
+ time::Instant::now() + Duration::from_secs(new_interval as u64),
+ Duration::from_secs(new_interval as u64),
);
- ticker.reset_after(time::Duration::from_secs(new_interval as u64));
+ ticker.reset_after(Duration::from_secs(new_interval as u64));
ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
tracing::error!("backing off, next sync tick in {new_interval}");
@@ -261,9 +267,8 @@ async fn do_sync_tick(
// Reset backoff on success
if ticker.period().as_secs() != settings.daemon.sync_frequency {
*ticker = time::interval_at(
- tokio::time::Instant::now()
- + Duration::from_secs(settings.daemon.sync_frequency),
- time::Duration::from_secs(settings.daemon.sync_frequency),
+ time::Instant::now() + Duration::from_secs(settings.daemon.sync_frequency),
+ Duration::from_secs(settings.daemon.sync_frequency),
);
ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
}
diff --git a/crates/turtle/src/atuin_daemon/control/mod.rs b/crates/turtle/src/atuin_daemon/control/mod.rs
index 23068519..7015db5b 100644
--- a/crates/turtle/src/atuin_daemon/control/mod.rs
+++ b/crates/turtle/src/atuin_daemon/control/mod.rs
@@ -1,12 +1,76 @@
-//! Control module for external event injection.
+//! Control service implementation.
//!
-//! This module provides the gRPC service that allows external processes
-//! (like CLI commands) to inject events into the daemon's event bus.
+//! This gRPC service allows external processes (like CLI commands) to inject
+//! events into the daemon's event bus.
-mod service;
+use tonic::{Request, Response, Status};
+use tracing::{Level, info, instrument};
-// Include the generated proto code
-tonic::include_proto!("control");
+use crate::{
+ atuin_client::history::HistoryId,
+ atuin_daemon::{
+ daemon::DaemonHandle,
+ events::DaemonEvent,
+ generated::control::{
+ SendEventRequest, SendEventResponse,
+ control_server::{Control, ControlServer},
+ send_event_request::Event,
+ },
+ },
+};
-// Re-export the service
-pub(crate) use service::ControlService;
+/// The Control gRPC service.
+///
+/// This service is used by external processes to inject events into the daemon.
+/// It's not a component - it's part of the daemon's core infrastructure.
+pub(crate) struct ControlService {
+ handle: DaemonHandle,
+}
+
+impl ControlService {
+ /// Create a new control service with the given daemon handle.
+ pub(crate) fn new(handle: DaemonHandle) -> Self {
+ Self { handle }
+ }
+
+ /// Get a tonic server for this service.
+ pub(crate) fn into_server(self) -> ControlServer<Self> {
+ ControlServer::new(self)
+ }
+}
+
+#[tonic::async_trait]
+impl Control for ControlService {
+ #[instrument(skip_all, level = Level::INFO, name = "control_send_event")]
+ async fn send_event(
+ &self,
+ request: Request<SendEventRequest>,
+ ) -> Result<Response<SendEventResponse>, Status> {
+ let req = request.into_inner();
+
+ let event = req
+ .event
+ .ok_or_else(|| Status::invalid_argument("event is required"))?;
+
+ let daemon_event = proto_event_to_daemon_event(event)?;
+
+ info!(?daemon_event, "received control event");
+ self.handle.emit(daemon_event);
+
+ Ok(Response::new(SendEventResponse {}))
+ }
+}
+
+/// Convert a proto event to a daemon event.
+fn proto_event_to_daemon_event(event: Event) -> Result<DaemonEvent, Status> {
+ match event {
+ Event::HistoryPruned(_) => Ok(DaemonEvent::HistoryPruned),
+ Event::HistoryRebuilt(_) => Ok(DaemonEvent::HistoryRebuilt),
+ Event::HistoryDeleted(e) => Ok(DaemonEvent::HistoryDeleted {
+ ids: e.ids.into_iter().map(HistoryId).collect(),
+ }),
+ Event::ForceSync(_) => Ok(DaemonEvent::ForceSync),
+ Event::SettingsReloaded(_) => Ok(DaemonEvent::SettingsReloaded),
+ Event::Shutdown(_) => Ok(DaemonEvent::ShutdownRequested),
+ }
+}
diff --git a/crates/turtle/src/atuin_daemon/control/service.rs b/crates/turtle/src/atuin_daemon/control/service.rs
deleted file mode 100644
index 8061a3c2..00000000
--- a/crates/turtle/src/atuin_daemon/control/service.rs
+++ /dev/null
@@ -1,71 +0,0 @@
-//! Control service implementation.
-//!
-//! This gRPC service allows external processes (like CLI commands) to inject
-//! events into the daemon's event bus.
-
-use crate::atuin_client::history::HistoryId;
-use tonic::{Request, Response, Status};
-use tracing::{Level, info, instrument};
-
-use super::{
- SendEventRequest, SendEventResponse,
- control_server::{Control, ControlServer},
- send_event_request::Event,
-};
-use crate::atuin_daemon::{daemon::DaemonHandle, events::DaemonEvent};
-
-/// The Control gRPC service.
-///
-/// This service is used by external processes to inject events into the daemon.
-/// It's not a component - it's part of the daemon's core infrastructure.
-pub(crate) struct ControlService {
- handle: DaemonHandle,
-}
-
-impl ControlService {
- /// Create a new control service with the given daemon handle.
- pub(crate) fn new(handle: DaemonHandle) -> Self {
- Self { handle }
- }
-
- /// Get a tonic server for this service.
- pub(crate) fn into_server(self) -> ControlServer<Self> {
- ControlServer::new(self)
- }
-}
-
-#[tonic::async_trait]
-impl Control for ControlService {
- #[instrument(skip_all, level = Level::INFO, name = "control_send_event")]
- async fn send_event(
- &self,
- request: Request<SendEventRequest>,
- ) -> Result<Response<SendEventResponse>, Status> {
- let req = request.into_inner();
-
- let event = req
- .event
- .ok_or_else(|| Status::invalid_argument("event is required"))?;
-
- let daemon_event = proto_event_to_daemon_event(event)?;
-
- info!(?daemon_event, "received control event");
- self.handle.emit(daemon_event);
-
- Ok(Response::new(SendEventResponse {}))
- }
-}
-
-/// Convert a proto event to a daemon event.
-fn proto_event_to_daemon_event(event: Event) -> Result<DaemonEvent, Status> {
- match event {
- Event::HistoryPruned(_) => Ok(DaemonEvent::HistoryPruned),
- Event::HistoryRebuilt(_) => Ok(DaemonEvent::HistoryRebuilt),
- Event::HistoryDeleted(e) => Ok(DaemonEvent::HistoryDeleted {
- ids: e.ids.into_iter().map(HistoryId).collect(),
- }),
- Event::ForceSync(_) => Ok(DaemonEvent::ForceSync),
- Event::SettingsReloaded(_) => Ok(DaemonEvent::SettingsReloaded),
- Event::Shutdown(_) => Ok(DaemonEvent::ShutdownRequested),
- }
-}
diff --git a/crates/turtle/src/atuin_daemon/events.rs b/crates/turtle/src/atuin_daemon/events.rs
index 09369512..d379277d 100644
--- a/crates/turtle/src/atuin_daemon/events.rs
+++ b/crates/turtle/src/atuin_daemon/events.rs
@@ -32,8 +32,11 @@ pub(crate) enum DaemonEvent {
/// Sync completed successfully.
SyncCompleted {
/// Number of records uploaded.
+ #[expect(unused)]
uploaded: usize,
+
/// Number of records downloaded.
+ #[expect(unused)]
downloaded: usize,
},
diff --git a/crates/turtle/src/atuin_daemon/generated.rs b/crates/turtle/src/atuin_daemon/generated.rs
new file mode 100644
index 00000000..e43f7523
--- /dev/null
+++ b/crates/turtle/src/atuin_daemon/generated.rs
@@ -0,0 +1,38 @@
+#![allow(
+ unreachable_pub,
+ unused_qualifications,
+ reason = "All of these lints are triggered by the generated code"
+)]
+
+/// Semantic command capture gRPC service types.
+pub(crate) mod semantic {
+ tonic::include_proto!("semantic");
+}
+
+/// Search module for the daemon gRPC search service.
+///
+/// This module provides fuzzy search over command history using Nucleo.
+pub(crate) mod search {
+ // Include the generated proto code
+ tonic::include_proto!("search");
+}
+
+/// History module for the daemon gRPC history service.
+///
+/// This module contains the proto-generated types for the history gRPC service.
+pub(crate) mod history {
+ // Include the generated proto code
+ tonic::include_proto!("history");
+}
+
+/// Control module for external event injection.
+///
+/// This module provides the gRPC service that allows external processes
+/// (like CLI commands) to inject events into the daemon's event bus.
+pub(crate) mod control {
+ // Include the generated proto code
+ tonic::include_proto!("control");
+
+ // Re-export the service
+ pub(crate) use crate::atuin_daemon::control::ControlService;
+}
diff --git a/crates/turtle/src/atuin_daemon/history/mod.rs b/crates/turtle/src/atuin_daemon/history/mod.rs
deleted file mode 100644
index b71853df..00000000
--- a/crates/turtle/src/atuin_daemon/history/mod.rs
+++ /dev/null
@@ -1,6 +0,0 @@
-//! History module for the daemon gRPC history service.
-//!
-//! This module contains the proto-generated types for the history gRPC service.
-
-// Include the generated proto code
-tonic::include_proto!("history");
diff --git a/crates/turtle/src/atuin_daemon/mod.rs b/crates/turtle/src/atuin_daemon/mod.rs
index b161b0cc..5f0f489e 100644
--- a/crates/turtle/src/atuin_daemon/mod.rs
+++ b/crates/turtle/src/atuin_daemon/mod.rs
@@ -8,11 +8,11 @@ pub(crate) mod components;
pub(crate) mod control;
pub(crate) mod daemon;
pub(crate) mod events;
-pub(crate) mod history;
pub(crate) mod search;
-pub(crate) mod semantic;
pub(crate) mod server;
+pub(crate) mod generated;
+
// Re-export core daemon types for convenience
pub(crate) use daemon::Daemon;
pub(crate) use events::DaemonEvent;
@@ -91,21 +91,18 @@ pub(crate) async fn boot(
signal_handle.shutdown();
});
- // Start the gRPC server in the background
server::run_grpc_server(
- settings,
+ &settings,
history_service,
search_service,
semantic_service,
control_service.into_server(),
handle,
- )
- .await?;
+ )?;
- // Run the daemon event loop
daemon.run_event_loop().await?;
- // Stop all components on shutdown
+ // After the event loop exited, we shut-down the components.
daemon.stop_components().await;
tracing::info!("daemon shut down complete");
diff --git a/crates/turtle/src/atuin_daemon/search/index.rs b/crates/turtle/src/atuin_daemon/search/index.rs
deleted file mode 100644
index 197a8c1b..00000000
--- a/crates/turtle/src/atuin_daemon/search/index.rs
+++ /dev/null
@@ -1,678 +0,0 @@
-//! Search index with frecency-based ranking.
-//!
-//! This module provides a deduplicated search index where each unique command
-//! is stored once, with metadata about all its invocations. This enables:
-//!
-//! - Efficient fuzzy matching (fewer items to match)
-//! - Frecency-based ranking (frequency + recency)
-//! - Dynamic filtering by directory, host, session, etc.
-
-use std::{
- collections::{HashMap, HashSet},
- sync::Arc,
-};
-
-use crate::atuin_client::settings::Search;
-use crate::{
- atuin_client::history::History, atuin_daemon::components::search::with_trailing_slash,
-};
-use atuin_nucleo::{Injector, Nucleo, pattern};
-use dashmap::DashMap;
-use lasso::{Spur, ThreadedRodeo};
-use time::OffsetDateTime;
-use tokio::sync::RwLock;
-use tracing::{Level, instrument};
-use uuid::Uuid;
-
-/// Parse a UUID string into a 16-byte array.
-/// Returns None if the string is not a valid UUID.
-fn parse_uuid_bytes(s: &str) -> Option<[u8; 16]> {
- Uuid::parse_str(s).ok().map(|u| *u.as_bytes())
-}
-
-/// Format a 16-byte array as a UUID string.
-fn format_uuid_bytes(bytes: &[u8; 16]) -> String {
- Uuid::from_bytes(*bytes).to_string()
-}
-
-/// Pre-computed frecency data for O(1) lookup.
-#[derive(Debug, Clone, Default)]
-pub(crate) struct FrecencyData {
- /// Total number of times this command was used.
- pub(crate) count: u32,
- /// Most recent usage timestamp (unix seconds).
- pub(crate) last_used: i64,
-}
-
-impl FrecencyData {
- /// Record a new usage of this command.
- pub(crate) fn record_use(&mut self, timestamp: i64) {
- self.count += 1;
- if timestamp > self.last_used {
- self.last_used = timestamp;
- }
- }
-
- /// Compute frecency score based on count and recency.
- ///
- /// Uses a decay function where more recent commands score higher.
- /// The formula balances frequency (how often) with recency (how recent).
- ///
- /// Multipliers allow tuning the relative weights:
- /// - `recency_mul`: Multiplier for recency score (default: 1.0)
- /// - `frequency_mul`: Multiplier for frequency score (default: 1.0)
- ///
- /// A multiplier of 0.0 disables that component, 1.0 is unchanged, 2.0 doubles weight.
- /// Values like 0.5 reduce weight by half, 1.5 increases by 50%, etc.
- #[instrument(level = tracing::Level::TRACE, name = "index_frecency_compute")]
- pub(crate) fn compute(&self, now: i64, recency_mul: f64, frequency_mul: f64) -> u32 {
- if self.count == 0 {
- return 0;
- }
-
- // Time-based decay: score decreases as time passes
- let age_seconds = (now - self.last_used).max(0) as u64;
- let age_hours = age_seconds / 3600;
-
- // Decay factor: recent commands get higher scores
- // - Last hour: multiplier ~1.0
- // - Last day: multiplier ~0.5
- // - Last week: multiplier ~0.1
- // - Older: multiplier approaches 0
- let recency_score: f64 = match age_hours {
- 0 => 100.0,
- 1..=6 => 90.0,
- 7..=24 => 70.0,
- 25..=72 => 50.0,
- 73..=168 => 30.0,
- 169..=720 => 15.0,
- _ => 5.0,
- };
-
- // Frequency boost: more uses = higher score (with diminishing returns)
- let frequency_score = (f64::from(self.count).ln() * 20.0).min(100.0);
-
- // Apply multipliers and combine scores, then round to u32
- ((recency_score * recency_mul) + (frequency_score * frequency_mul)).round() as u32
- }
-}
-
-/// Data for a unique command.
-pub(crate) struct CommandData {
- /// History ID of the most recent invocation (16-byte UUID).
- most_recent_id: [u8; 16],
- /// Timestamp of the most recent invocation.
- most_recent_timestamp: i64,
- /// Pre-computed global frecency.
- pub(crate) global_frecency: FrecencyData,
-
- // Pre-computed indexes for O(1) filter lookups
- // Using HashSet instead of DashSet since CommandData lives inside DashMap (already synchronized)
- /// All directories where this command has been run (interned keys).
- directories: HashSet<Spur>,
- /// All hostnames where this command has been run (interned keys).
- hosts: HashSet<Spur>,
- /// All sessions where this command has been run (as 16-byte UUIDs).
- sessions: HashSet<[u8; 16]>,
-}
-
-impl CommandData {
- /// Create a new [`CommandData`] from a history entry.
- /// Returns None if the history entry has invalid UUIDs.
- pub(crate) fn new(history: &History, interner: &ThreadedRodeo) -> Option<Self> {
- let history_id = parse_uuid_bytes(&history.id.0)?;
- let session = parse_uuid_bytes(&history.session)?;
- let timestamp = history.timestamp.unix_timestamp();
-
- let dir_key = interner.get_or_intern(with_trailing_slash(&history.cwd));
- let host_key = interner.get_or_intern(&history.hostname);
-
- let mut directories = HashSet::new();
- directories.insert(dir_key);
-
- let mut hosts = HashSet::new();
- hosts.insert(host_key);
-
- let mut sessions = HashSet::new();
- sessions.insert(session);
-
- let mut global_frecency = FrecencyData::default();
- global_frecency.record_use(timestamp);
-
- Some(Self {
- most_recent_id: history_id,
- most_recent_timestamp: timestamp,
- global_frecency,
- directories,
- hosts,
- sessions,
- })
- }
-
- /// Add an invocation from a history entry.
- /// Returns false if the history entry has invalid UUIDs.
- pub(crate) fn add_invocation(&mut self, history: &History, interner: &ThreadedRodeo) -> bool {
- let Some(history_id) = parse_uuid_bytes(&history.id.0) else {
- return false;
- };
- let Some(session) = parse_uuid_bytes(&history.session) else {
- return false;
- };
-
- let timestamp = history.timestamp.unix_timestamp();
-
- // Update global frecency
- self.global_frecency.record_use(timestamp);
-
- // Update pre-computed indexes for O(1) filter lookups
- let dir_key = interner.get_or_intern(with_trailing_slash(&history.cwd));
- self.directories.insert(dir_key);
- self.hosts.insert(interner.get_or_intern(&history.hostname));
- self.sessions.insert(session);
-
- // Update most recent if this invocation is newer
- if timestamp > self.most_recent_timestamp {
- self.most_recent_id = history_id;
- self.most_recent_timestamp = timestamp;
- }
-
- true
- }
-
- /// Get the most recent history ID for this command.
- pub(crate) fn most_recent_id(&self) -> String {
- format_uuid_bytes(&self.most_recent_id)
- }
-
- /// Check if any invocation matches a directory filter (exact match).
- /// O(1) lookup using pre-computed index.
- pub(crate) fn has_invocation_in_dir(&self, dir: &str, interner: &ThreadedRodeo) -> bool {
- interner
- .get(dir)
- .is_some_and(|spur| self.directories.contains(&spur))
- }
-
- /// Check if any invocation matches a directory prefix (workspace/git root).
- /// O(n) where n = number of unique directories for this command.
- pub(crate) fn has_invocation_in_workspace(
- &self,
- prefix: &str,
- interner: &ThreadedRodeo,
- ) -> bool {
- self.directories
- .iter()
- .any(|&spur| interner.resolve(&spur).starts_with(prefix))
- }
-
- /// Check if any invocation matches a hostname.
- /// O(1) lookup using pre-computed index.
- pub(crate) fn has_invocation_on_host(&self, hostname: &str, interner: &ThreadedRodeo) -> bool {
- interner
- .get(hostname)
- .is_some_and(|spur| self.hosts.contains(&spur))
- }
-
- /// Check if any invocation matches a session.
- /// O(1) lookup using pre-computed index.
- pub(crate) fn has_invocation_in_session(&self, session: &str) -> bool {
- parse_uuid_bytes(session).is_some_and(|bytes| self.sessions.contains(&bytes))
- }
-}
-
-/// Filter mode for search queries.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub(crate) enum IndexFilterMode {
- /// No filtering - search all commands.
- Global,
- /// Filter to commands run in a specific directory.
- Directory(String),
- /// Filter to commands run in a workspace (directory prefix).
- Workspace(String),
- /// Filter to commands run on a specific host.
- Host(String),
- /// Filter to commands run in a specific session.
- Session(String),
-}
-
-/// Context for search queries.
-#[derive(Debug, Clone, Default)]
-pub(crate) struct QueryContext {
- #[expect(dead_code)]
- pub(crate) cwd: Option<String>,
- #[expect(dead_code)]
- pub(crate) git_root: Option<String>,
- #[expect(dead_code)]
- pub(crate) hostname: Option<String>,
- #[expect(dead_code)]
- pub(crate) session_id: Option<String>,
-}
-
-/// Shareable frecency map: command -> frecency score.
-/// Wrapped in Arc for zero-copy sharing with scorer callbacks.
-type FrecencyMap = Arc<HashMap<Arc<str>, u32>>;
-
-/// A deduplicated search index with frecency-based ranking.
-///
-/// Commands are stored by their text, with metadata about all invocations.
-/// Nucleo handles fuzzy matching, while frecency is computed via scorer callback.
-///
-/// Global frecency is precomputed by a background task and used for scoring.
-/// If frecency data is not available, search still works but without frecency ranking;
-/// although this should never happen due to precomputing the frecency map.
-pub(crate) struct SearchIndex {
- /// Map from command text to command data.
- /// Using DashMap for concurrent read/write access, wrapped in Arc for sharing with scorer.
- /// Keys are Arc<str> to enable zero-copy sharing with frecency_map.
- commands: Arc<DashMap<Arc<str>, CommandData>>,
- /// Nucleo fuzzy matcher - items are command strings.
- nucleo: RwLock<Nucleo<String>>,
- /// Injector for adding new commands to Nucleo.
- injector: Injector<String>,
- /// Precomputed global frecency map. Updated by background task.
- frecency_map: RwLock<Option<FrecencyMap>>,
- /// String interner for deduplicating cwd, hostname, and directory paths.
- interner: Arc<ThreadedRodeo>,
-}
-
-impl SearchIndex {
- /// Create a new empty search index.
- pub(crate) fn new() -> Self {
- let nucleo_config = atuin_nucleo::Config::DEFAULT;
- // Single column for command text
- let nucleo = Nucleo::<String>::new(nucleo_config, Arc::new(|| {}), None, 1);
- let injector = nucleo.injector();
-
- Self {
- commands: Arc::new(DashMap::new()),
- nucleo: RwLock::new(nucleo),
- injector,
- frecency_map: RwLock::new(None),
- interner: Arc::new(ThreadedRodeo::new()),
- }
- }
-
- /// Add a history entry to the index.
- ///
- /// If the command already exists, updates its invocation data.
- /// If it's a new command, adds it to both the map and Nucleo.
- pub(crate) fn add_history(&self, history: &History) {
- let command = history.command.as_str();
-
- // DashMap with Arc<str> keys can be looked up with &str via Borrow trait
- if let Some(mut entry) = self.commands.get_mut(command) {
- // Existing command - just update invocations
- entry.add_invocation(history, &self.interner);
- } else {
- // New command - create Arc<str> once and share it
- let Some(data) = CommandData::new(history, &self.interner) else {
- return; // Invalid UUIDs, skip this entry
- };
- let command_arc: Arc<str> = command.into();
- self.commands.insert(Arc::clone(&command_arc), data);
- // Nucleo still needs String (unavoidable copy for fuzzy matching)
- self.injector.push(command_arc.to_string(), |cmd, cols| {
- cols[0] = cmd.clone().into();
- });
- }
- // Note: frecency_map is rebuilt by background task, not invalidated here
- }
-
- /// Add multiple history entries to the index.
- pub(crate) fn add_histories(&self, histories: &[History]) {
- for history in histories {
- self.add_history(history);
- }
- }
-
- /// Get the number of unique commands in the index.
- pub(crate) fn command_count(&self) -> usize {
- self.commands.len()
- }
-
- /// Search for commands matching a query.
- ///
- /// Returns a list of history IDs (most recent invocation per command).
- /// Uses precomputed global frecency for scoring if available.
- #[instrument(skip_all, level = tracing::Level::TRACE, name = "index_search", fields(query = %query))]
- #[expect(
- clippy::significant_drop_tightening,
- reason = "The nucleo early drop is a false-positive"
- )]
- pub(crate) async fn search(
- &self,
- query: &str,
- filter_mode: IndexFilterMode,
- // TODO(@bpeetz): Use the query context here <2026-06-12>
- #[expect(unused)] context: &QueryContext,
- limit: u32,
- ) -> Vec<String> {
- let mut nucleo = self.nucleo.write().await;
-
- // Get precomputed frecency map (may be None if not yet computed)
- let frecency_map = self.frecency_map.read().await.clone();
-
- // Build filter based on mode
- let filter = self.build_filter(&filter_mode);
- nucleo.set_filter(filter);
-
- // Build scorer from precomputed frecency (or None if not available)
- let scorer = Self::build_scorer(frecency_map);
- nucleo.set_scorer(scorer);
-
- // Update pattern
- nucleo.pattern.reparse(
- 0,
- query,
- pattern::CaseMatching::Smart,
- pattern::Normalization::Smart,
- false,
- );
-
- tracing::span!(Level::TRACE, "index_search_tick").in_scope(|| {
- // Tick until complete
- while nucleo.tick(10).running {}
- });
-
- // Collect results
- let snapshot = nucleo.snapshot();
- let matched_count = snapshot.matched_item_count().min(limit);
-
- tracing::span!(Level::TRACE, "index_search_results").in_scope(|| {
- snapshot
- .matched_items(..matched_count)
- .filter_map(|item| {
- let cmd = item.data;
- // DashMap<Arc<str>, _>::get accepts &str via Borrow trait
- self.commands
- .get(cmd.as_str())
- .map(|data| data.most_recent_id())
- })
- .collect()
- })
- }
-
- /// Rebuild the global frecency map.
- ///
- /// This should be called by a background task periodically.
- /// The map is used for scoring search results.
- ///
- /// Uses multipliers from search settings:
- /// - `recency_score_multiplier`: Weight for recency component
- /// - `frequency_score_multiplier`: Weight for frequency component
- /// - `frecency_score_multiplier`: Overall multiplier for final score
- #[instrument(skip_all, level = tracing::Level::DEBUG, name = "rebuild_frecency")]
- pub(crate) async fn rebuild_frecency(&self, search_settings: &Search) {
- let now = OffsetDateTime::now_utc().unix_timestamp();
- let mut frecency_map: HashMap<Arc<str>, u32> = HashMap::new();
-
- // Clamp multipliers to non-negative values to prevent broken frecency ranking
- // (negative values would produce unexpected results when cast to u32)
- let recency_mul = search_settings.recency_score_multiplier.max(0.0);
- let frequency_mul = search_settings.frequency_score_multiplier.max(0.0);
- let frecency_mul = search_settings.frecency_score_multiplier.max(0.0);
-
- for entry in self.commands.iter() {
- let frecency = entry
- .global_frecency
- .compute(now, recency_mul, frequency_mul);
- // Apply overall frecency multiplier and round to u32
- let frecency = (frecency as f64 * frecency_mul).round() as u32;
- // Arc::clone is cheap - just increments reference count
- frecency_map.insert(Arc::clone(entry.key()), frecency);
- }
-
- *self.frecency_map.write().await = Some(Arc::new(frecency_map));
- }
-
- /// Build filter predicate for the given mode.
- fn build_filter(&self, mode: &IndexFilterMode) -> Option<atuin_nucleo::Filter<String>> {
- // For Global mode, no filter needed
- if matches!(mode, IndexFilterMode::Global) {
- return None;
- }
-
- // Pre-compute which commands pass the filter
- // Use HashSet<String> for the short-lived filter (simpler than Arc lookup)
- let passing_commands: Arc<HashSet<String>> = {
- let mut set = HashSet::new();
- for entry in self.commands.iter() {
- let passes = match mode {
- IndexFilterMode::Global => unreachable!(),
- IndexFilterMode::Directory(dir) => {
- entry.has_invocation_in_dir(dir, &self.interner)
- }
- IndexFilterMode::Workspace(prefix) => {
- entry.has_invocation_in_workspace(prefix, &self.interner)
- }
- IndexFilterMode::Host(hostname) => {
- entry.has_invocation_on_host(hostname, &self.interner)
- }
- IndexFilterMode::Session(session) => entry.has_invocation_in_session(session),
- };
- if passes {
- // Convert Arc<str> to String for filter lookup
- set.insert(entry.key().to_string());
- }
- }
- Arc::new(set)
- };
-
- Some(Arc::new(move |cmd: &String| passing_commands.contains(cmd)))
- }
-
- /// Build scorer from precomputed frecency map.
- ///
- /// Returns None if frecency map is not available (search still works, just without frecency ranking).
- fn build_scorer(frecency_map: Option<FrecencyMap>) -> Option<atuin_nucleo::Scorer<String>> {
- let map = frecency_map?;
- Some(Arc::new(move |cmd: &String, fuzzy_score: u32| {
- // HashMap<Arc<str>, _>::get accepts &str via Borrow trait
- let frecency = map.get(cmd.as_str()).copied().unwrap_or(0);
- fuzzy_score + frecency
- }))
- }
-}
-
-impl Default for SearchIndex {
- fn default() -> Self {
- Self::new()
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use time::macros::datetime;
-
- #[test]
- fn frecency_data_compute() {
- let now = 1_000_000i64;
-
- // Recent command (with default multipliers of 1.0)
- let recent = FrecencyData {
- count: 5,
- last_used: now - 60, // 1 minute ago
- };
- assert!(recent.compute(now, 1.0, 1.0) > 100); // High score
-
- // Old command
- let old = FrecencyData {
- count: 5,
- last_used: now - 86400 * 30, // 30 days ago
- };
- assert!(old.compute(now, 1.0, 1.0) < recent.compute(now, 1.0, 1.0));
-
- // Frequently used old command
- let frequent_old = FrecencyData {
- count: 100,
- last_used: now - 86400 * 7, // 1 week ago
- };
- // Should still have decent score due to frequency
- assert!(frequent_old.compute(now, 1.0, 1.0) > 50);
- }
-
- #[test]
- fn frecency_data_compute_with_multipliers() {
- let now = 1_000_000_i64;
-
- let data = FrecencyData {
- count: 5,
- last_used: now - 60, // 1 minute ago (recency_score = 100)
- };
-
- // Default multipliers (1.0, 1.0)
- let default_score = data.compute(now, 1.0, 1.0);
-
- // Double recency weight
- let double_recency = data.compute(now, 2.0, 1.0);
- assert!(double_recency > default_score);
-
- // Double frequency weight
- let double_frequency = data.compute(now, 1.0, 2.0);
- assert!(double_frequency > default_score);
-
- // Zero out recency (only frequency counts)
- let no_recency = data.compute(now, 0.0, 1.0);
- assert!(no_recency < default_score);
-
- // Zero out frequency (only recency counts)
- let no_frequency = data.compute(now, 1.0, 0.0);
- assert!(no_frequency < default_score);
-
- // Zero both (should be zero)
- let no_score = data.compute(now, 0.0, 0.0);
- assert_eq!(no_score, 0);
-
- // Fractional multipliers
- let half_recency = data.compute(now, 0.5, 1.0);
- assert!(half_recency < default_score);
- assert!(half_recency > no_recency);
-
- // 1.5x multiplier
- let boost_recency = data.compute(now, 1.5, 1.0);
- assert!(boost_recency > default_score);
- assert!(boost_recency < double_recency);
- }
-
- #[test]
- fn command_data_add_invocation() {
- let interner = ThreadedRodeo::new();
-
- let (dir1, dir2) = if cfg!(windows) {
- ("C:\\Users\\User\\project", "C:\\Users\\User\\other")
- } else {
- ("/home/user/project", "/home/user/other")
- };
-
- let history1 = make_history("git status", dir1, datetime!(2024-01-01 10:00 UTC));
- let history2 = make_history("git status", dir2, datetime!(2024-01-01 12:00 UTC));
-
- let mut data = CommandData::new(&history1, &interner).unwrap();
- assert_eq!(data.global_frecency.count, 1);
- let id1 = data.most_recent_id();
-
- data.add_invocation(&history2, &interner);
- assert_eq!(data.global_frecency.count, 2);
-
- // Most recent ID should update to history2 (newer timestamp)
- let id2 = data.most_recent_id();
- assert_ne!(id1, id2);
- }
-
- #[test]
- fn command_data_filters() {
- let interner = ThreadedRodeo::new();
-
- let (dir1, dir2) = if cfg!(windows) {
- ("C:\\Users\\User\\project", "C:\\Users\\User\\other")
- } else {
- ("/home/user/project", "/home/user/other")
- };
-
- let h1 = make_history("git status", dir1, datetime!(2024-01-01 10:00 UTC));
- let h2 = make_history("git status", dir2, datetime!(2024-01-01 12:00 UTC));
-
- let mut data = CommandData::new(&h1, &interner).unwrap();
- data.add_invocation(&h2, &interner);
-
- let (check1, check2, check3) = if cfg!(windows) {
- (
- with_trailing_slash("C:\\Users\\User\\project"),
- with_trailing_slash("C:\\Users\\User\\other"),
- with_trailing_slash("C:\\Users\\User\\missing"),
- )
- } else {
- (
- with_trailing_slash("/home/user/project"),
- with_trailing_slash("/home/user/other"),
- with_trailing_slash("/home/user/missing"),
- )
- };
-
- assert!(data.has_invocation_in_dir(&check1, &interner));
- assert!(data.has_invocation_in_dir(&check2, &interner));
- assert!(!data.has_invocation_in_dir(&check3, &interner));
-
- let (check1, check2, check3) = if cfg!(windows) {
- (
- with_trailing_slash("C:\\Users\\User"),
- with_trailing_slash("C:\\Users"),
- with_trailing_slash("C:\\Users\\User\\var"),
- )
- } else {
- (
- with_trailing_slash("/home/user"),
- with_trailing_slash("/home"),
- with_trailing_slash("/var"),
- )
- };
-
- assert!(data.has_invocation_in_workspace(&check1, &interner));
- assert!(data.has_invocation_in_workspace(&check2, &interner));
- assert!(!data.has_invocation_in_workspace(&check3, &interner));
- }
-
- #[tokio::test]
- async fn search_index_add_and_search() {
- let index = SearchIndex::new();
-
- let h1 = make_history(
- "git status",
- "/home/user/project",
- datetime!(2024-01-01 10:00 UTC),
- );
- let h2 = make_history(
- "git commit -m 'test'",
- "/home/user/project",
- datetime!(2024-01-01 10:05 UTC),
- );
- let h3 = make_history(
- "ls -la",
- "/home/user/other",
- datetime!(2024-01-01 10:10 UTC),
- );
-
- index.add_history(&h1);
- index.add_history(&h2);
- index.add_history(&h3);
-
- assert_eq!(index.command_count(), 3);
-
- // Search for "git" - should match 2 commands
- let results = index
- .search("git", IndexFilterMode::Global, &QueryContext::default(), 10)
- .await;
- assert_eq!(results.len(), 2);
-
- // Search with directory filter
- let results = index
- .search(
- "",
- IndexFilterMode::Directory(with_trailing_slash("/home/user/project")),
- &QueryContext::default(),
- 10,
- )
- .await;
- assert_eq!(results.len(), 2); // git status and git commit
- }
-}
diff --git a/crates/turtle/src/atuin_daemon/search/mod.rs b/crates/turtle/src/atuin_daemon/search/mod.rs
index 51b6c6cc..60d923cd 100644
--- a/crates/turtle/src/atuin_daemon/search/mod.rs
+++ b/crates/turtle/src/atuin_daemon/search/mod.rs
@@ -1,11 +1,678 @@
-//! Search module for the daemon gRPC search service.
+//! Search index with frecency-based ranking.
//!
-//! This module provides fuzzy search over command history using Nucleo.
+//! This module provides a deduplicated search index where each unique command
+//! is stored once, with metadata about all its invocations. This enables:
+//!
+//! - Efficient fuzzy matching (fewer items to match)
+//! - Frecency-based ranking (frequency + recency)
+//! - Dynamic filtering by directory, host, session, etc.
+
+use std::{
+ collections::{HashMap, HashSet},
+ sync::Arc,
+};
+
+use crate::atuin_client::settings::Search;
+use crate::{
+ atuin_client::history::History, atuin_daemon::components::search::with_trailing_slash,
+};
+use atuin_nucleo::{Injector, Nucleo, pattern};
+use dashmap::DashMap;
+use lasso::{Spur, ThreadedRodeo};
+use time::OffsetDateTime;
+use tokio::sync::RwLock;
+use tracing::{Level, instrument};
+use uuid::Uuid;
+
+/// Parse a UUID string into a 16-byte array.
+/// Returns None if the string is not a valid UUID.
+fn parse_uuid_bytes(s: &str) -> Option<[u8; 16]> {
+ Uuid::parse_str(s).ok().map(|u| *u.as_bytes())
+}
+
+/// Format a 16-byte array as a UUID string.
+fn format_uuid_bytes(bytes: &[u8; 16]) -> String {
+ Uuid::from_bytes(*bytes).to_string()
+}
+
+/// Pre-computed frecency data for O(1) lookup.
+#[derive(Debug, Clone, Default)]
+pub(crate) struct FrecencyData {
+ /// Total number of times this command was used.
+ pub(crate) count: u32,
+ /// Most recent usage timestamp (unix seconds).
+ pub(crate) last_used: i64,
+}
+
+impl FrecencyData {
+ /// Record a new usage of this command.
+ pub(crate) fn record_use(&mut self, timestamp: i64) {
+ self.count += 1;
+ if timestamp > self.last_used {
+ self.last_used = timestamp;
+ }
+ }
+
+ /// Compute frecency score based on count and recency.
+ ///
+ /// Uses a decay function where more recent commands score higher.
+ /// The formula balances frequency (how often) with recency (how recent).
+ ///
+ /// Multipliers allow tuning the relative weights:
+ /// - `recency_mul`: Multiplier for recency score (default: 1.0)
+ /// - `frequency_mul`: Multiplier for frequency score (default: 1.0)
+ ///
+ /// A multiplier of 0.0 disables that component, 1.0 is unchanged, 2.0 doubles weight.
+ /// Values like 0.5 reduce weight by half, 1.5 increases by 50%, etc.
+ #[instrument(level = Level::TRACE, name = "index_frecency_compute")]
+ pub(crate) fn compute(&self, now: i64, recency_mul: f64, frequency_mul: f64) -> u32 {
+ if self.count == 0 {
+ return 0;
+ }
+
+ // Time-based decay: score decreases as time passes
+ let age_seconds = (now - self.last_used).max(0) as u64;
+ let age_hours = age_seconds / 3600;
+
+ // Decay factor: recent commands get higher scores
+ // - Last hour: multiplier ~1.0
+ // - Last day: multiplier ~0.5
+ // - Last week: multiplier ~0.1
+ // - Older: multiplier approaches 0
+ let recency_score: f64 = match age_hours {
+ 0 => 100.0,
+ 1..=6 => 90.0,
+ 7..=24 => 70.0,
+ 25..=72 => 50.0,
+ 73..=168 => 30.0,
+ 169..=720 => 15.0,
+ _ => 5.0,
+ };
+
+ // Frequency boost: more uses = higher score (with diminishing returns)
+ let frequency_score = (f64::from(self.count).ln() * 20.0).min(100.0);
+
+ // Apply multipliers and combine scores, then round to u32
+ ((recency_score * recency_mul) + (frequency_score * frequency_mul)).round() as u32
+ }
+}
+
+/// Data for a unique command.
+pub(crate) struct CommandData {
+ /// History ID of the most recent invocation (16-byte UUID).
+ most_recent_id: [u8; 16],
+ /// Timestamp of the most recent invocation.
+ most_recent_timestamp: i64,
+ /// Pre-computed global frecency.
+ pub(crate) global_frecency: FrecencyData,
+
+ // Pre-computed indexes for O(1) filter lookups
+ // Using HashSet instead of DashSet since CommandData lives inside DashMap (already synchronized)
+ /// All directories where this command has been run (interned keys).
+ directories: HashSet<Spur>,
+ /// All hostnames where this command has been run (interned keys).
+ hosts: HashSet<Spur>,
+ /// All sessions where this command has been run (as 16-byte UUIDs).
+ sessions: HashSet<[u8; 16]>,
+}
+
+impl CommandData {
+ /// Create a new [`CommandData`] from a history entry.
+ /// Returns None if the history entry has invalid UUIDs.
+ pub(crate) fn new(history: &History, interner: &ThreadedRodeo) -> Option<Self> {
+ let history_id = parse_uuid_bytes(&history.id.0)?;
+ let session = parse_uuid_bytes(&history.session)?;
+ let timestamp = history.timestamp.unix_timestamp();
+
+ let dir_key = interner.get_or_intern(with_trailing_slash(&history.cwd));
+ let host_key = interner.get_or_intern(&history.hostname);
+
+ let mut directories = HashSet::new();
+ directories.insert(dir_key);
+
+ let mut hosts = HashSet::new();
+ hosts.insert(host_key);
+
+ let mut sessions = HashSet::new();
+ sessions.insert(session);
+
+ let mut global_frecency = FrecencyData::default();
+ global_frecency.record_use(timestamp);
+
+ Some(Self {
+ most_recent_id: history_id,
+ most_recent_timestamp: timestamp,
+ global_frecency,
+ directories,
+ hosts,
+ sessions,
+ })
+ }
+
+ /// Add an invocation from a history entry.
+ /// Returns false if the history entry has invalid UUIDs.
+ pub(crate) fn add_invocation(&mut self, history: &History, interner: &ThreadedRodeo) -> bool {
+ let Some(history_id) = parse_uuid_bytes(&history.id.0) else {
+ return false;
+ };
+ let Some(session) = parse_uuid_bytes(&history.session) else {
+ return false;
+ };
+
+ let timestamp = history.timestamp.unix_timestamp();
+
+ // Update global frecency
+ self.global_frecency.record_use(timestamp);
+
+ // Update pre-computed indexes for O(1) filter lookups
+ let dir_key = interner.get_or_intern(with_trailing_slash(&history.cwd));
+ self.directories.insert(dir_key);
+ self.hosts.insert(interner.get_or_intern(&history.hostname));
+ self.sessions.insert(session);
+
+ // Update most recent if this invocation is newer
+ if timestamp > self.most_recent_timestamp {
+ self.most_recent_id = history_id;
+ self.most_recent_timestamp = timestamp;
+ }
+
+ true
+ }
+
+ /// Get the most recent history ID for this command.
+ pub(crate) fn most_recent_id(&self) -> String {
+ format_uuid_bytes(&self.most_recent_id)
+ }
+
+ /// Check if any invocation matches a directory filter (exact match).
+ /// O(1) lookup using pre-computed index.
+ pub(crate) fn has_invocation_in_dir(&self, dir: &str, interner: &ThreadedRodeo) -> bool {
+ interner
+ .get(dir)
+ .is_some_and(|spur| self.directories.contains(&spur))
+ }
+
+ /// Check if any invocation matches a directory prefix (workspace/git root).
+ /// O(n) where n = number of unique directories for this command.
+ pub(crate) fn has_invocation_in_workspace(
+ &self,
+ prefix: &str,
+ interner: &ThreadedRodeo,
+ ) -> bool {
+ self.directories
+ .iter()
+ .any(|&spur| interner.resolve(&spur).starts_with(prefix))
+ }
+
+ /// Check if any invocation matches a hostname.
+ /// O(1) lookup using pre-computed index.
+ pub(crate) fn has_invocation_on_host(&self, hostname: &str, interner: &ThreadedRodeo) -> bool {
+ interner
+ .get(hostname)
+ .is_some_and(|spur| self.hosts.contains(&spur))
+ }
+
+ /// Check if any invocation matches a session.
+ /// O(1) lookup using pre-computed index.
+ pub(crate) fn has_invocation_in_session(&self, session: &str) -> bool {
+ parse_uuid_bytes(session).is_some_and(|bytes| self.sessions.contains(&bytes))
+ }
+}
+
+/// Filter mode for search queries.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) enum IndexFilterMode {
+ /// No filtering - search all commands.
+ Global,
+ /// Filter to commands run in a specific directory.
+ Directory(String),
+ /// Filter to commands run in a workspace (directory prefix).
+ Workspace(String),
+ /// Filter to commands run on a specific host.
+ Host(String),
+ /// Filter to commands run in a specific session.
+ Session(String),
+}
+
+/// Context for search queries.
+#[derive(Debug, Clone, Default)]
+pub(crate) struct QueryContext {
+ #[expect(dead_code)]
+ pub(crate) cwd: Option<String>,
+ #[expect(dead_code)]
+ pub(crate) git_root: Option<String>,
+ #[expect(dead_code)]
+ pub(crate) hostname: Option<String>,
+ #[expect(dead_code)]
+ pub(crate) session_id: Option<String>,
+}
+
+/// Shareable frecency map: command -> frecency score.
+/// Wrapped in Arc for zero-copy sharing with scorer callbacks.
+type FrecencyMap = Arc<HashMap<Arc<str>, u32>>;
+
+/// A deduplicated search index with frecency-based ranking.
+///
+/// Commands are stored by their text, with metadata about all invocations.
+/// Nucleo handles fuzzy matching, while frecency is computed via scorer callback.
+///
+/// Global frecency is precomputed by a background task and used for scoring.
+/// If frecency data is not available, search still works but without frecency ranking;
+/// although this should never happen due to precomputing the frecency map.
+pub(crate) struct SearchIndex {
+ /// Map from command text to command data.
+ /// Using DashMap for concurrent read/write access, wrapped in Arc for sharing with scorer.
+ /// Keys are Arc<str> to enable zero-copy sharing with frecency_map.
+ commands: Arc<DashMap<Arc<str>, CommandData>>,
+ /// Nucleo fuzzy matcher - items are command strings.
+ nucleo: RwLock<Nucleo<String>>,
+ /// Injector for adding new commands to Nucleo.
+ injector: Injector<String>,
+ /// Precomputed global frecency map. Updated by background task.
+ frecency_map: RwLock<Option<FrecencyMap>>,
+ /// String interner for deduplicating cwd, hostname, and directory paths.
+ interner: Arc<ThreadedRodeo>,
+}
+
+impl SearchIndex {
+ /// Create a new empty search index.
+ pub(crate) fn new() -> Self {
+ let nucleo_config = atuin_nucleo::Config::DEFAULT;
+ // Single column for command text
+ let nucleo = Nucleo::<String>::new(nucleo_config, Arc::new(|| {}), None, 1);
+ let injector = nucleo.injector();
+
+ Self {
+ commands: Arc::new(DashMap::new()),
+ nucleo: RwLock::new(nucleo),
+ injector,
+ frecency_map: RwLock::new(None),
+ interner: Arc::new(ThreadedRodeo::new()),
+ }
+ }
+
+ /// Add a history entry to the index.
+ ///
+ /// If the command already exists, updates its invocation data.
+ /// If it's a new command, adds it to both the map and Nucleo.
+ pub(crate) fn add_history(&self, history: &History) {
+ let command = history.command.as_str();
+
+ // DashMap with Arc<str> keys can be looked up with &str via Borrow trait
+ if let Some(mut entry) = self.commands.get_mut(command) {
+ // Existing command - just update invocations
+ entry.add_invocation(history, &self.interner);
+ } else {
+ // New command - create Arc<str> once and share it
+ let Some(data) = CommandData::new(history, &self.interner) else {
+ return; // Invalid UUIDs, skip this entry
+ };
+ let command_arc: Arc<str> = command.into();
+ self.commands.insert(Arc::clone(&command_arc), data);
+ // Nucleo still needs String (unavoidable copy for fuzzy matching)
+ self.injector.push(command_arc.to_string(), |cmd, cols| {
+ cols[0] = cmd.clone().into();
+ });
+ }
+ // Note: frecency_map is rebuilt by background task, not invalidated here
+ }
+
+ /// Add multiple history entries to the index.
+ pub(crate) fn add_histories(&self, histories: &[History]) {
+ for history in histories {
+ self.add_history(history);
+ }
+ }
+
+ /// Get the number of unique commands in the index.
+ pub(crate) fn command_count(&self) -> usize {
+ self.commands.len()
+ }
+
+ /// Search for commands matching a query.
+ ///
+ /// Returns a list of history IDs (most recent invocation per command).
+ /// Uses precomputed global frecency for scoring if available.
+ #[instrument(skip_all, level = Level::TRACE, name = "index_search", fields(query = %query))]
+ #[expect(
+ clippy::significant_drop_tightening,
+ reason = "The nucleo early drop is a false-positive"
+ )]
+ pub(crate) async fn search(
+ &self,
+ query: &str,
+ filter_mode: IndexFilterMode,
+ // TODO(@bpeetz): Use the query context here <2026-06-12>
+ #[expect(unused)] context: &QueryContext,
+ limit: u32,
+ ) -> Vec<String> {
+ let mut nucleo = self.nucleo.write().await;
+
+ // Get precomputed frecency map (may be None if not yet computed)
+ let frecency_map = self.frecency_map.read().await.clone();
+
+ // Build filter based on mode
+ let filter = self.build_filter(&filter_mode);
+ nucleo.set_filter(filter);
+
+ // Build scorer from precomputed frecency (or None if not available)
+ let scorer = Self::build_scorer(frecency_map);
+ nucleo.set_scorer(scorer);
+
+ // Update pattern
+ nucleo.pattern.reparse(
+ 0,
+ query,
+ pattern::CaseMatching::Smart,
+ pattern::Normalization::Smart,
+ false,
+ );
+
+ tracing::span!(Level::TRACE, "index_search_tick").in_scope(|| {
+ // Tick until complete
+ while nucleo.tick(10).running {}
+ });
+
+ // Collect results
+ let snapshot = nucleo.snapshot();
+ let matched_count = snapshot.matched_item_count().min(limit);
+
+ tracing::span!(Level::TRACE, "index_search_results").in_scope(|| {
+ snapshot
+ .matched_items(..matched_count)
+ .filter_map(|item| {
+ let cmd = item.data;
+ // DashMap<Arc<str>, _>::get accepts &str via Borrow trait
+ self.commands
+ .get(cmd.as_str())
+ .map(|data| data.most_recent_id())
+ })
+ .collect()
+ })
+ }
+
+ /// Rebuild the global frecency map.
+ ///
+ /// This should be called by a background task periodically.
+ /// The map is used for scoring search results.
+ ///
+ /// Uses multipliers from search settings:
+ /// - `recency_score_multiplier`: Weight for recency component
+ /// - `frequency_score_multiplier`: Weight for frequency component
+ /// - `frecency_score_multiplier`: Overall multiplier for final score
+ #[instrument(skip_all, level = Level::DEBUG, name = "rebuild_frecency")]
+ pub(crate) async fn rebuild_frecency(&self, search_settings: &Search) {
+ let now = OffsetDateTime::now_utc().unix_timestamp();
+ let mut frecency_map: HashMap<Arc<str>, u32> = HashMap::new();
+
+ // Clamp multipliers to non-negative values to prevent broken frecency ranking
+ // (negative values would produce unexpected results when cast to u32)
+ let recency_mul = search_settings.recency_score_multiplier.max(0.0);
+ let frequency_mul = search_settings.frequency_score_multiplier.max(0.0);
+ let frecency_mul = search_settings.frecency_score_multiplier.max(0.0);
+
+ for entry in self.commands.iter() {
+ let frecency = entry
+ .global_frecency
+ .compute(now, recency_mul, frequency_mul);
+ // Apply overall frecency multiplier and round to u32
+ let frecency = (frecency as f64 * frecency_mul).round() as u32;
+ // Arc::clone is cheap - just increments reference count
+ frecency_map.insert(Arc::clone(entry.key()), frecency);
+ }
+
+ *self.frecency_map.write().await = Some(Arc::new(frecency_map));
+ }
+
+ /// Build filter predicate for the given mode.
+ fn build_filter(&self, mode: &IndexFilterMode) -> Option<atuin_nucleo::Filter<String>> {
+ // For Global mode, no filter needed
+ if matches!(mode, IndexFilterMode::Global) {
+ return None;
+ }
+
+ // Pre-compute which commands pass the filter
+ // Use HashSet<String> for the short-lived filter (simpler than Arc lookup)
+ let passing_commands: Arc<HashSet<String>> = {
+ let mut set = HashSet::new();
+ for entry in self.commands.iter() {
+ let passes = match mode {
+ IndexFilterMode::Global => unreachable!(),
+ IndexFilterMode::Directory(dir) => {
+ entry.has_invocation_in_dir(dir, &self.interner)
+ }
+ IndexFilterMode::Workspace(prefix) => {
+ entry.has_invocation_in_workspace(prefix, &self.interner)
+ }
+ IndexFilterMode::Host(hostname) => {
+ entry.has_invocation_on_host(hostname, &self.interner)
+ }
+ IndexFilterMode::Session(session) => entry.has_invocation_in_session(session),
+ };
+ if passes {
+ // Convert Arc<str> to String for filter lookup
+ set.insert(entry.key().to_string());
+ }
+ }
+ Arc::new(set)
+ };
+
+ Some(Arc::new(move |cmd: &String| passing_commands.contains(cmd)))
+ }
+
+ /// Build scorer from precomputed frecency map.
+ ///
+ /// Returns None if frecency map is not available (search still works, just without frecency ranking).
+ fn build_scorer(frecency_map: Option<FrecencyMap>) -> Option<atuin_nucleo::Scorer<String>> {
+ let map = frecency_map?;
+ Some(Arc::new(move |cmd: &String, fuzzy_score: u32| {
+ // HashMap<Arc<str>, _>::get accepts &str via Borrow trait
+ let frecency = map.get(cmd.as_str()).copied().unwrap_or(0);
+ fuzzy_score + frecency
+ }))
+ }
+}
+
+impl Default for SearchIndex {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use time::macros::datetime;
+
+ #[test]
+ fn frecency_data_compute() {
+ let now = 1_000_000i64;
+
+ // Recent command (with default multipliers of 1.0)
+ let recent = FrecencyData {
+ count: 5,
+ last_used: now - 60, // 1 minute ago
+ };
+ assert!(recent.compute(now, 1.0, 1.0) > 100); // High score
+
+ // Old command
+ let old = FrecencyData {
+ count: 5,
+ last_used: now - 86400 * 30, // 30 days ago
+ };
+ assert!(old.compute(now, 1.0, 1.0) < recent.compute(now, 1.0, 1.0));
+
+ // Frequently used old command
+ let frequent_old = FrecencyData {
+ count: 100,
+ last_used: now - 86400 * 7, // 1 week ago
+ };
+ // Should still have decent score due to frequency
+ assert!(frequent_old.compute(now, 1.0, 1.0) > 50);
+ }
+
+ #[test]
+ fn frecency_data_compute_with_multipliers() {
+ let now = 1_000_000_i64;
+
+ let data = FrecencyData {
+ count: 5,
+ last_used: now - 60, // 1 minute ago (recency_score = 100)
+ };
+
+ // Default multipliers (1.0, 1.0)
+ let default_score = data.compute(now, 1.0, 1.0);
+
+ // Double recency weight
+ let double_recency = data.compute(now, 2.0, 1.0);
+ assert!(double_recency > default_score);
+
+ // Double frequency weight
+ let double_frequency = data.compute(now, 1.0, 2.0);
+ assert!(double_frequency > default_score);
+
+ // Zero out recency (only frequency counts)
+ let no_recency = data.compute(now, 0.0, 1.0);
+ assert!(no_recency < default_score);
+
+ // Zero out frequency (only recency counts)
+ let no_frequency = data.compute(now, 1.0, 0.0);
+ assert!(no_frequency < default_score);
+
+ // Zero both (should be zero)
+ let no_score = data.compute(now, 0.0, 0.0);
+ assert_eq!(no_score, 0);
+
+ // Fractional multipliers
+ let half_recency = data.compute(now, 0.5, 1.0);
+ assert!(half_recency < default_score);
+ assert!(half_recency > no_recency);
+
+ // 1.5x multiplier
+ let boost_recency = data.compute(now, 1.5, 1.0);
+ assert!(boost_recency > default_score);
+ assert!(boost_recency < double_recency);
+ }
+
+ #[test]
+ fn command_data_add_invocation() {
+ let interner = ThreadedRodeo::new();
+
+ let (dir1, dir2) = if cfg!(windows) {
+ ("C:\\Users\\User\\project", "C:\\Users\\User\\other")
+ } else {
+ ("/home/user/project", "/home/user/other")
+ };
+
+ let history1 = make_history("git status", dir1, datetime!(2024-01-01 10:00 UTC));
+ let history2 = make_history("git status", dir2, datetime!(2024-01-01 12:00 UTC));
+
+ let mut data = CommandData::new(&history1, &interner).unwrap();
+ assert_eq!(data.global_frecency.count, 1);
+ let id1 = data.most_recent_id();
+
+ data.add_invocation(&history2, &interner);
+ assert_eq!(data.global_frecency.count, 2);
+
+ // Most recent ID should update to history2 (newer timestamp)
+ let id2 = data.most_recent_id();
+ assert_ne!(id1, id2);
+ }
+
+ #[test]
+ fn command_data_filters() {
+ let interner = ThreadedRodeo::new();
+
+ let (dir1, dir2) = if cfg!(windows) {
+ ("C:\\Users\\User\\project", "C:\\Users\\User\\other")
+ } else {
+ ("/home/user/project", "/home/user/other")
+ };
+
+ let h1 = make_history("git status", dir1, datetime!(2024-01-01 10:00 UTC));
+ let h2 = make_history("git status", dir2, datetime!(2024-01-01 12:00 UTC));
+
+ let mut data = CommandData::new(&h1, &interner).unwrap();
+ data.add_invocation(&h2, &interner);
+
+ let (check1, check2, check3) = if cfg!(windows) {
+ (
+ with_trailing_slash("C:\\Users\\User\\project"),
+ with_trailing_slash("C:\\Users\\User\\other"),
+ with_trailing_slash("C:\\Users\\User\\missing"),
+ )
+ } else {
+ (
+ with_trailing_slash("/home/user/project"),
+ with_trailing_slash("/home/user/other"),
+ with_trailing_slash("/home/user/missing"),
+ )
+ };
+
+ assert!(data.has_invocation_in_dir(&check1, &interner));
+ assert!(data.has_invocation_in_dir(&check2, &interner));
+ assert!(!data.has_invocation_in_dir(&check3, &interner));
+
+ let (check1, check2, check3) = if cfg!(windows) {
+ (
+ with_trailing_slash("C:\\Users\\User"),
+ with_trailing_slash("C:\\Users"),
+ with_trailing_slash("C:\\Users\\User\\var"),
+ )
+ } else {
+ (
+ with_trailing_slash("/home/user"),
+ with_trailing_slash("/home"),
+ with_trailing_slash("/var"),
+ )
+ };
+
+ assert!(data.has_invocation_in_workspace(&check1, &interner));
+ assert!(data.has_invocation_in_workspace(&check2, &interner));
+ assert!(!data.has_invocation_in_workspace(&check3, &interner));
+ }
+
+ #[tokio::test]
+ async fn search_index_add_and_search() {
+ let index = SearchIndex::new();
+
+ let h1 = make_history(
+ "git status",
+ "/home/user/project",
+ datetime!(2024-01-01 10:00 UTC),
+ );
+ let h2 = make_history(
+ "git commit -m 'test'",
+ "/home/user/project",
+ datetime!(2024-01-01 10:05 UTC),
+ );
+ let h3 = make_history(
+ "ls -la",
+ "/home/user/other",
+ datetime!(2024-01-01 10:10 UTC),
+ );
+
+ index.add_history(&h1);
+ index.add_history(&h2);
+ index.add_history(&h3);
-mod index;
+ assert_eq!(index.command_count(), 3);
-// Include the generated proto code
-tonic::include_proto!("search");
+ // Search for "git" - should match 2 commands
+ let results = index
+ .search("git", IndexFilterMode::Global, &QueryContext::default(), 10)
+ .await;
+ assert_eq!(results.len(), 2);
-// Re-export the service and index
-pub(crate) use index::{IndexFilterMode, QueryContext, SearchIndex};
+ // Search with directory filter
+ let results = index
+ .search(
+ "",
+ IndexFilterMode::Directory(with_trailing_slash("/home/user/project")),
+ &QueryContext::default(),
+ 10,
+ )
+ .await;
+ assert_eq!(results.len(), 2); // git status and git commit
+ }
+}
diff --git a/crates/turtle/src/atuin_daemon/semantic/mod.rs b/crates/turtle/src/atuin_daemon/semantic/mod.rs
deleted file mode 100644
index c3511676..00000000
--- a/crates/turtle/src/atuin_daemon/semantic/mod.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-//! Semantic command capture gRPC service types.
-
-tonic::include_proto!("semantic");
diff --git a/crates/turtle/src/atuin_daemon/server.rs b/crates/turtle/src/atuin_daemon/server.rs
index 65272d2a..36954cca 100644
--- a/crates/turtle/src/atuin_daemon/server.rs
+++ b/crates/turtle/src/atuin_daemon/server.rs
@@ -1,23 +1,28 @@
use eyre::Result;
-use crate::atuin_daemon::components::history::HistoryGrpcService;
-use crate::atuin_daemon::components::search::SearchGrpcService;
-use crate::atuin_daemon::components::semantic::SemanticGrpcService;
-use crate::atuin_daemon::control::{ControlService, control_server::ControlServer};
-use crate::atuin_daemon::daemon::DaemonHandle;
-use crate::atuin_daemon::history::history_server::HistoryServer;
-use crate::atuin_daemon::search::search_server::SearchServer;
-use crate::atuin_daemon::semantic::semantic_server::SemanticServer;
-
-use crate::atuin_client::settings::Settings;
+use crate::{
+ atuin_client::settings::Settings,
+ atuin_daemon::{
+ components::{
+ history::HistoryGrpcService, search::SearchGrpcService, semantic::SemanticGrpcService,
+ },
+ daemon::DaemonHandle,
+ generated::{
+ control::{ControlService, control_server::ControlServer},
+ history::history_server::HistoryServer,
+ search::search_server::SearchServer,
+ semantic::semantic_server::SemanticServer,
+ },
+ },
+};
/// Run the gRPC server with the given services.
///
/// This starts the gRPC server in the background and returns immediately.
-/// The server will shut down when a ShutdownRequested event is received.
+/// The server will shut down when a [`ShutdownRequested`] event is received.
#[cfg(unix)]
-pub(crate) async fn run_grpc_server(
- settings: Settings,
+pub(crate) fn run_grpc_server(
+ settings: &Settings,
history_service: HistoryServer<HistoryGrpcService>,
search_service: SearchServer<SearchGrpcService>,
semantic_service: SemanticServer<SemanticGrpcService>,
@@ -75,15 +80,16 @@ pub(crate) async fn run_grpc_server(
// Create shutdown signal from daemon handle
let shutdown_signal = async move {
let mut rx = handle.subscribe();
+
loop {
use crate::atuin_daemon::DaemonEvent;
match rx.recv().await {
- Ok(DaemonEvent::ShutdownRequested) => break,
- Ok(_) => continue,
- Err(_) => break, // Channel closed
+ Err(_) | Ok(DaemonEvent::ShutdownRequested) => break,
+ Ok(_) => (),
}
}
+
if cleanup {
eprintln!("Removing socket...");
if let Err(e) = std::fs::remove_file(&socket_path)
diff --git a/crates/turtle/src/atuin_history/stats.rs b/crates/turtle/src/atuin_history/stats.rs
index 2328eeca..a19d2d0e 100644
--- a/crates/turtle/src/atuin_history/stats.rs
+++ b/crates/turtle/src/atuin_history/stats.rs
@@ -170,7 +170,7 @@ pub(crate) fn pretty_print(stats: Stats, ngram_size: usize) {
let column_widths = stats
.top
.iter()
- .map(|(commands, _)| commands.iter().map(|c| c.len()).collect::<Vec<usize>>())
+ .map(|(commands, _)| commands.iter().map(String::len).collect::<Vec<usize>>())
.fold(vec![0; ngram_size], |acc, item| {
acc.iter()
.zip(item.iter())
@@ -287,10 +287,10 @@ mod tests {
#[test]
fn ignored_env_vars() {
- let settings = Settings::utc();
+ let settings = Settings::new().unwrap();
let history: History = History::capture()
- .timestamp(time::OffsetDateTime::now_utc())
+ .timestamp(OffsetDateTime::now_utc())
.command("FOO='BAR=🚀' echo foo")
.cwd("/")
.build()
@@ -302,7 +302,7 @@ mod tests {
#[test]
fn ignored_commands() {
- let mut settings = Settings::utc();
+ let mut settings = Settings::new().unwrap();
settings.stats.ignored_commands.push("cd".to_string());
let history = [
@@ -325,7 +325,7 @@ mod tests {
#[test]
fn interesting_commands() {
- let settings = Settings::utc();
+ let settings = Settings::new().unwrap();
assert_eq!(interesting_command(&settings, "cargo"), "cargo");
assert_eq!(
@@ -342,7 +342,7 @@ mod tests {
// Test with spaces in the common_prefix
#[test]
fn interesting_commands_spaces() {
- let mut settings = Settings::utc();
+ let mut settings = Settings::new().unwrap();
settings.stats.common_prefix.push("sudo test".to_string());
assert_eq!(interesting_command(&settings, "sudo test"), "sudo test");
@@ -367,7 +367,7 @@ mod tests {
// Test with spaces in the common_subcommand
#[test]
fn interesting_commands_spaces_subcommand() {
- let mut settings = Settings::utc();
+ let mut settings = Settings::new().unwrap();
settings
.stats
.common_subcommands
@@ -397,7 +397,7 @@ mod tests {
// Test with spaces in the common_prefix and common_subcommand
#[test]
fn interesting_commands_spaces_both() {
- let mut settings = Settings::utc();
+ let mut settings = Settings::new().unwrap();
settings.stats.common_prefix.push("sudo test".to_string());
settings
.stats
diff --git a/crates/turtle/src/atuin_pty_proxy/osc133.rs b/crates/turtle/src/atuin_pty_proxy/osc133.rs
index d79166a6..b0cf0f0a 100644
--- a/crates/turtle/src/atuin_pty_proxy/osc133.rs
+++ b/crates/turtle/src/atuin_pty_proxy/osc133.rs
@@ -1,4 +1,4 @@
-//! Streaming parser for OSC 133 (FinalTerm semantic prompt) escape sequences.
+//! Streaming parser for OSC 133 ([`FinalTerm`] semantic prompt) escape sequences.
//!
//! OSC 133 marks four regions of a shell interaction:
//!
@@ -24,7 +24,7 @@
//! can ride alongside standard OSC 133 markers.
/// Events emitted when an OSC 133 marker is detected.
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub(crate) enum Event {
/// `ESC ] 133 ; A ST` — the shell is about to display its prompt.
PromptStart,
@@ -98,7 +98,6 @@ pub(crate) struct LocatedEvent {
/// The current semantic zone as determined by the most recent OSC 133 marker.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
-#[expect(dead_code)]
pub(crate) enum Zone {
/// No marker seen yet, or after a `D` marker (between commands).
#[default]
diff --git a/crates/turtle/src/atuin_pty_proxy/runtime.rs b/crates/turtle/src/atuin_pty_proxy/runtime.rs
index 37c77eef..69b3a075 100644
--- a/crates/turtle/src/atuin_pty_proxy/runtime.rs
+++ b/crates/turtle/src/atuin_pty_proxy/runtime.rs
@@ -13,7 +13,7 @@ use crate::atuin_pty_proxy::screen::{self, Msg};
pub(crate) fn main(options: RuntimeOptions) {
if let Err(e) = run(options) {
- let _ = terminal::disable_raw_mode();
+ drop(terminal::disable_raw_mode());
eprintln!("atuin pty-proxy: {e:#}");
std::process::exit(1);
}
@@ -33,7 +33,7 @@ fn run(options: RuntimeOptions) -> eyre::Result<()> {
.map_err(|e| eyre::eyre!("{e:#}"))?;
let sock_path = screen::socket_path();
- let _ = std::fs::remove_file(&sock_path);
+ drop(std::fs::remove_file(&sock_path));
let mut cmd = CommandBuilder::new_default_prog();
cmd.cwd(std::env::current_dir()?);
@@ -87,26 +87,26 @@ fn run(options: RuntimeOptions) -> eyre::Result<()> {
if let Some(highlighter) = highlighter.as_mut() {
let rendered = highlighter.render(&buf[..n]);
- let _ = msg_tx.try_send(Msg::Data(rendered.clone()));
+ drop(msg_tx.try_send(Msg::Data(rendered.clone())));
if stdout.write_all(&rendered).is_err() {
break;
}
} else {
- let _ = msg_tx.try_send(Msg::Data(buf[..n].to_vec()));
+ drop(msg_tx.try_send(Msg::Data(buf[..n].to_vec())));
if stdout.write_all(&buf[..n]).is_err() {
break;
}
}
- let _ = stdout.flush();
+ drop(stdout.flush());
}
}
}
if highlighter.is_some() {
- let _ = stdout.write_all(RESET);
- let _ = stdout.flush();
+ drop(stdout.write_all(RESET));
+ drop(stdout.flush());
}
});
@@ -126,10 +126,10 @@ fn run(options: RuntimeOptions) -> eyre::Result<()> {
});
let status = child.wait()?;
- let _ = stdout_thread.join();
+ drop(stdout_thread.join());
- let _ = terminal::disable_raw_mode();
- let _ = std::fs::remove_file(&sock_path);
+ drop(terminal::disable_raw_mode());
+ drop(std::fs::remove_file(&sock_path));
std::process::exit(process_exit_code(status.exit_code()));
}
@@ -148,13 +148,13 @@ fn spawn_resize_handler(
for _ in signals.forever() {
if let Ok((cols, rows)) = terminal::size() {
current_cols.store(cols.max(1), Ordering::Relaxed);
- let _ = master.resize(PtySize {
+ drop(master.resize(PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
- });
- let _ = resize_tx.try_send(Msg::Resize { rows, cols });
+ }));
+ drop(resize_tx.try_send(Msg::Resize { rows, cols }));
}
}
});
diff --git a/crates/turtle/src/atuin_pty_proxy/screen.rs b/crates/turtle/src/atuin_pty_proxy/screen.rs
index 5b892e21..c51a0c7d 100644
--- a/crates/turtle/src/atuin_pty_proxy/screen.rs
+++ b/crates/turtle/src/atuin_pty_proxy/screen.rs
@@ -54,8 +54,8 @@ pub(crate) fn spawn_socket_server(sock_path: PathBuf, screen_tx: SyncSender<Msg>
break;
}
if let Ok(data) = reply_rx.recv() {
- let _ = stream.write_all(&data);
- let _ = stream.flush();
+ drop(stream.write_all(&data));
+ drop(stream.flush());
}
}
});
@@ -98,7 +98,7 @@ fn handle_parser_msg(parser: &mut vt100::Parser, msg: Msg) {
Msg::Data(data) => parser.process(&data),
Msg::Resize { rows, cols } => parser.screen_mut().set_size(rows, cols),
Msg::ScreenRequest(reply_tx) => {
- let _ = reply_tx.send(encode_screen(parser));
+ drop(reply_tx.send(encode_screen(parser)));
}
}
}
diff --git a/crates/turtle/src/atuin_server/database/db/mod.rs b/crates/turtle/src/atuin_server/database/db/mod.rs
index 5b3c169b..47dbd6d1 100644
--- a/crates/turtle/src/atuin_server/database/db/mod.rs
+++ b/crates/turtle/src/atuin_server/database/db/mod.rs
@@ -17,7 +17,7 @@ mod wrappers;
const MIN_PG_VERSION: u32 = 14;
#[derive(Clone)]
-pub struct ServerPostgres {
+pub(crate) struct ServerPostgres {
pool: sqlx::Pool<sqlx::postgres::Postgres>,
/// Optional read replica pool for read-only queries
read_pool: Option<sqlx::Pool<sqlx::postgres::Postgres>>,
diff --git a/crates/turtle/src/atuin_server/database/db/wrappers.rs b/crates/turtle/src/atuin_server/database/db/wrappers.rs
index 8a52d56e..0a7b6ff3 100644
--- a/crates/turtle/src/atuin_server/database/db/wrappers.rs
+++ b/crates/turtle/src/atuin_server/database/db/wrappers.rs
@@ -1,7 +1,7 @@
use crate::atuin_common::record::{EncryptedData, Host, Record};
use sqlx::{Row, postgres::PgRow};
-pub struct DbRecord(pub Record<EncryptedData>);
+pub(crate) struct DbRecord(pub Record<EncryptedData>);
impl<'a> ::sqlx::FromRow<'a, PgRow> for DbRecord {
fn from_row(row: &'a PgRow) -> ::sqlx::Result<Self> {
diff --git a/crates/turtle/src/atuin_server/database/mod.rs b/crates/turtle/src/atuin_server/database/mod.rs
index bb64767a..43fe5c3b 100644
--- a/crates/turtle/src/atuin_server/database/mod.rs
+++ b/crates/turtle/src/atuin_server/database/mod.rs
@@ -14,29 +14,29 @@ pub(crate) enum DbError {
impl Display for DbError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
- DbError::NotFound => write!(f, "Not found"),
- DbError::Other(report) => write!(f, "Other: {report}"),
+ Self::NotFound => write!(f, "Not found"),
+ Self::Other(report) => write!(f, "Other: {report}"),
}
}
}
impl From<time::error::ComponentRange> for DbError {
fn from(error: time::error::ComponentRange) -> Self {
- DbError::Other(error.into())
+ Self::Other(error.into())
}
}
impl From<time::error::Error> for DbError {
fn from(error: time::error::Error) -> Self {
- DbError::Other(error.into())
+ Self::Other(error.into())
}
}
impl From<sqlx::Error> for DbError {
fn from(error: sqlx::Error) -> Self {
match error {
- sqlx::Error::RowNotFound => DbError::NotFound,
- error => DbError::Other(error.into()),
+ sqlx::Error::RowNotFound => Self::NotFound,
+ error => Self::Other(error.into()),
}
}
}
@@ -54,6 +54,7 @@ pub(crate) enum DbType {
#[derive(Clone, Deserialize, Serialize)]
pub(crate) struct DbSettings {
pub(crate) db_uri: String,
+
/// Optional URI for read replicas. If set, read-only queries will use this connection.
pub(crate) read_db_uri: Option<String>,
}
@@ -69,12 +70,13 @@ impl DbSettings {
}
fn redact_db_uri(uri: &str) -> String {
- url::Url::parse(uri)
- .map(|mut url| {
- let _ = url.set_password(Some("****"));
+ url::Url::parse(uri).map_or_else(
+ |_| uri.to_string(),
+ |mut url| {
+ url.set_password(Some("****")).expect("should be possible");
url.to_string()
- })
- .unwrap_or_else(|_| uri.to_string())
+ },
+ )
}
// Do our best to redact passwords so they're not logged in the event of an error.
diff --git a/crates/turtle/src/command/client.rs b/crates/turtle/src/command/client.rs
index 0c97f945..2ec5abe7 100644
--- a/crates/turtle/src/command/client.rs
+++ b/crates/turtle/src/command/client.rs
@@ -35,7 +35,7 @@ fn cleanup_old_logs(log_dir: &Path, prefix: &str, retention_days: u64) {
&& let Ok(modified) = metadata.modified()
&& modified < cutoff
{
- let _ = fs::remove_file(&path);
+ drop(fs::remove_file(&path));
}
}
}
@@ -107,7 +107,7 @@ impl Cmd {
pub(crate) fn run(self) -> Result<()> {
// Daemonize before creating the async runtime – fork() inside a live
// tokio runtime corrupts its internal state.
- #[cfg(all(unix, feature = "daemon"))]
+ #[cfg(unix)]
if let Self::Daemon(ref cmd) = self
&& cmd.should_daemonize()
{
diff --git a/crates/turtle/src/command/client/daemon.rs b/crates/turtle/src/command/client/daemon.rs
index 41cb04fe..4960dacd 100644
--- a/crates/turtle/src/command/client/daemon.rs
+++ b/crates/turtle/src/command/client/daemon.rs
@@ -120,7 +120,7 @@ impl PidfileGuard {
impl Drop for PidfileGuard {
fn drop(&mut self) {
- let _ = self.file.unlock();
+ drop(self.file.unlock());
}
}
@@ -232,7 +232,7 @@ async fn probe(settings: &Settings) -> Probe {
async fn request_shutdown(settings: &Settings) {
if let Ok(mut client) = connect_client(settings).await {
- let _ = client.shutdown().await;
+ drop(client.shutdown().await);
}
}
@@ -365,7 +365,7 @@ pub(crate) async fn ensure_daemon_running(settings: &Settings) -> Result<()> {
remove_stale_socket_if_present(settings)?;
spawn_daemon_process()?;
- let _ = wait_until_ready(settings, timeout).await?;
+ drop(wait_until_ready(settings, timeout).await?);
drop(startup_lock);
Ok(())
@@ -451,7 +451,7 @@ pub(crate) async fn end_history(
// End succeeded on the running daemon, so avoid replaying it.
// We only restart to make subsequent hook calls target the expected version.
- let _ = restart_daemon(settings).await;
+ drop(restart_daemon(settings).await);
return Ok(());
}
Err(err) if !settings.daemon.autostart => return Err(err),
@@ -679,11 +679,13 @@ fn force_cleanup(settings: &Settings) {
#[cfg(unix)]
fn kill_process(pid: u32) {
// Use kill command to send SIGTERM for graceful shutdown
- let _ = Command::new("kill")
- .args(["-TERM", &pid.to_string()])
- .stdout(Stdio::null())
- .stderr(Stdio::null())
- .status();
+ drop(
+ Command::new("kill")
+ .args(["-TERM", &pid.to_string()])
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .status(),
+ );
}
#[cfg(test)]
@@ -725,35 +727,4 @@ mod tests {
let lock = daemon_startup_lock_path(pidfile);
assert_eq!(lock, PathBuf::from("/tmp/atuin-daemon.pid.startup.lock"));
}
-
- #[test]
- fn test_pidfile_guard_acquire_and_drop() {
- let tmp = tempfile::tempdir().unwrap();
- let pidfile = tmp.path().join("daemon.pid");
-
- {
- let _guard = PidfileGuard::acquire(&pidfile).unwrap();
- // Guard holds an exclusive lock — on Windows other handles cannot
- // read the file, so we verify contents after the guard is dropped.
- }
-
- let contents = std::fs::read_to_string(&pidfile).unwrap();
- let lines: Vec<&str> = contents.lines().collect();
- assert_eq!(lines.len(), 2);
- assert_eq!(lines[0], std::process::id().to_string());
- assert_eq!(lines[1], DAEMON_VERSION);
-
- // After guard is dropped, lock should be released — acquiring again must succeed.
- let _guard2 = PidfileGuard::acquire(&pidfile).unwrap();
- }
-
- #[test]
- fn test_pidfile_guard_prevents_double_acquire() {
- let tmp = tempfile::tempdir().unwrap();
- let pidfile = tmp.path().join("daemon.pid");
-
- let _guard = PidfileGuard::acquire(&pidfile).unwrap();
- let result = PidfileGuard::acquire(&pidfile);
- assert!(result.is_err());
- }
}
diff --git a/crates/turtle/src/command/client/history.rs b/crates/turtle/src/command/client/history.rs
index 2ddcb3a6..e574a2e9 100644
--- a/crates/turtle/src/command/client/history.rs
+++ b/crates/turtle/src/command/client/history.rs
@@ -14,7 +14,7 @@ use super::daemon as daemon_cmd;
use colored::Colorize;
use serde::Serialize;
-use crate::atuin_daemon::history::{HistoryEventKind, TailHistoryReply};
+use crate::atuin_daemon::generated::history::{HistoryEventKind, TailHistoryReply};
use crate::atuin_client::{
database::{ClientSqlite, current_context},
@@ -166,11 +166,11 @@ pub(crate) enum ListMode {
impl ListMode {
pub(crate) const fn from_flags(human: bool, cmd_only: bool) -> Self {
if human {
- ListMode::Human
+ Self::Human
} else if cmd_only {
- ListMode::CmdOnly
+ Self::CmdOnly
} else {
- ListMode::Regular
+ Self::Regular
}
}
}
@@ -183,7 +183,7 @@ pub(crate) fn print_list(
reverse: bool,
tz: Timezone,
) {
- let w = std::io::stdout();
+ let w = io::stdout();
let mut w = w.lock();
let fmt_str = match list_mode {
@@ -202,6 +202,7 @@ pub(crate) fn print_list(
ListMode::CmdOnly => std::iter::once(ParseSegment::Key("command")).collect(),
};
+ #[allow(trivial_casts)]
let iterator = if reverse {
Box::new(h.iter().rev()) as Box<dyn Iterator<Item = &History>>
} else {
@@ -743,10 +744,10 @@ fn normalize_optional_field(value: &str) -> Option<String> {
impl Cmd {
async fn handle_tail(settings: &Settings) -> Result<()> {
- let tty = std::io::stdout().is_terminal();
+ let tty = io::stdout().is_terminal();
let mut client = daemon::tail_client(settings).await?;
let mut stream = client.tail_history().await?;
- let stdout = std::io::stdout();
+ let stdout = io::stdout();
while let Some(reply) = stream.message().await? {
let event = TailEvent::from_proto(reply)?;
diff --git a/crates/turtle/src/command/client/search.rs b/crates/turtle/src/command/client/search.rs
index bba48b8a..5c51caea 100644
--- a/crates/turtle/src/command/client/search.rs
+++ b/crates/turtle/src/command/client/search.rs
@@ -166,7 +166,7 @@ impl Cmd {
|query| {
query
.split(' ')
- .map(std::string::ToString::to_string)
+ .map(ToString::to_string)
.collect()
},
)
diff --git a/crates/turtle/src/command/client/search/engines.rs b/crates/turtle/src/command/client/search/engines.rs
index 94834221..9fbff278 100644
--- a/crates/turtle/src/command/client/search/engines.rs
+++ b/crates/turtle/src/command/client/search/engines.rs
@@ -14,9 +14,9 @@ pub(crate) mod skim;
pub(crate) fn engine(search_mode: SearchMode, settings: &Settings) -> Box<dyn SearchEngine> {
match search_mode {
- SearchMode::Skim => Box::new(skim::Search::new()) as Box<_>,
- SearchMode::DaemonFuzzy => Box::new(daemon::Search::new(settings)) as Box<_>,
- mode => Box::new(db::Search(mode)) as Box<_>,
+ SearchMode::Skim => Box::new(skim::Search::new()),
+ SearchMode::DaemonFuzzy => Box::new(daemon::Search::new(settings)),
+ mode => Box::new(db::Search(mode)),
}
}
diff --git a/crates/turtle/src/command/client/search/engines/db.rs b/crates/turtle/src/command/client/search/engines/db.rs
index e6657b17..0eb86878 100644
--- a/crates/turtle/src/command/client/search/engines/db.rs
+++ b/crates/turtle/src/command/client/search/engines/db.rs
@@ -49,7 +49,7 @@ impl SearchEngine for Search {
let mut parser = FzfParser::new();
let query = parser.parse(search_input);
let mut ranges: Vec<Range<usize>> = Vec::new();
- let _ = fzf.distance_and_ranges(query, command, &mut ranges);
+ fzf.distance_and_ranges(query, command, &mut ranges);
// convert ranges to all indices
ranges.into_iter().flatten().collect()
diff --git a/crates/turtle/src/command/client/search/engines/skim.rs b/crates/turtle/src/command/client/search/engines/skim.rs
index a6a77573..739e790b 100644
--- a/crates/turtle/src/command/client/search/engines/skim.rs
+++ b/crates/turtle/src/command/client/search/engines/skim.rs
@@ -117,7 +117,7 @@ async fn fuzzy_search(
continue;
};
let (seconds, nanos) = timestamp.to_unix();
- let Ok(session_start) = time::OffsetDateTime::from_unix_timestamp_nanos(
+ let Ok(session_start) = OffsetDateTime::from_unix_timestamp_nanos(
i128::from(seconds) * 1_000_000_000 + i128::from(nanos),
) else {
warn!(
diff --git a/crates/turtle/src/command/client/search/inspector.rs b/crates/turtle/src/command/client/search/inspector.rs
index f7b40a26..6c2c59fe 100644
--- a/crates/turtle/src/command/client/search/inspector.rs
+++ b/crates/turtle/src/command/client/search/inspector.rs
@@ -195,7 +195,7 @@ fn sort_duration_over_time(durations: &[(String, i64)]) -> Vec<(String, i64)> {
}
fn draw_stats_charts(f: &mut Frame<'_>, parent: Rect, stats: &HistoryStats) {
- let exits: Vec<Bar> = stats
+ let exits: Vec<Bar<'_>> = stats
.exits
.iter()
.map(|(exit, count)| {
@@ -219,7 +219,7 @@ fn draw_stats_charts(f: &mut Frame<'_>, parent: Rect, stats: &HistoryStats) {
.label_style(Style::default())
.data(BarGroup::default().bars(&exits));
- let day_of_week: Vec<Bar> = stats
+ let day_of_week: Vec<Bar<'_>> = stats
.day_of_week
.iter()
.map(|(day, count)| {
@@ -244,7 +244,7 @@ fn draw_stats_charts(f: &mut Frame<'_>, parent: Rect, stats: &HistoryStats) {
.data(BarGroup::default().bars(&day_of_week));
let duration_over_time = sort_duration_over_time(&stats.duration_over_time);
- let duration_over_time: Vec<Bar> = duration_over_time
+ let duration_over_time: Vec<Bar<'_>> = duration_over_time
.iter()
.map(|(date, duration)| {
let d = Duration::from_nanos(u64_or_zero(*duration));
@@ -398,7 +398,7 @@ mod tests {
let prev = stats.previous.clone().unwrap();
let next = stats.next.clone().unwrap();
- let _ = terminal.draw(|f| draw_ultracompact(f, chunk, &history, &stats));
+ drop(terminal.draw(|f| draw_ultracompact(f, chunk, &history, &stats)));
let mut lines = [" "; 5].map(|l| Line::from(l));
for (n, entry) in [prev, history, next].iter().enumerate() {
let mut l = lines[n].to_string();
diff --git a/crates/turtle/src/command/client/search/interactive.rs b/crates/turtle/src/command/client/search/interactive.rs
index 2c6af8cf..49d483b0 100644
--- a/crates/turtle/src/command/client/search/interactive.rs
+++ b/crates/turtle/src/command/client/search/interactive.rs
@@ -97,7 +97,7 @@ impl InspectingState {
}
}
-pub(crate) fn to_compactness(f: &Frame, settings: &Settings) -> Compactness {
+pub(crate) fn to_compactness(f: &Frame<'_>, settings: &Settings) -> Compactness {
if match settings.style {
crate::atuin_client::settings::Style::Auto => f.area().height < 14,
crate::atuin_client::settings::Style::Compact => true,
@@ -232,7 +232,7 @@ impl State {
&& let Some(style) = cursor_style
{
self.current_cursor = cursor_style;
- let _ = execute!(stdout(), Self::cast_cursor_style(style));
+ drop(execute!(stdout(), Self::cast_cursor_style(style)));
}
}
@@ -804,7 +804,7 @@ impl State {
fn draw(
&mut self,
- f: &mut Frame,
+ f: &mut Frame<'_>,
results: &[History],
stats: Option<HistoryStats>,
inspecting: Option<&History>,
@@ -823,7 +823,7 @@ impl State {
#[expect(clippy::bool_to_int_with_if)]
fn draw_inner(
&mut self,
- f: &mut Frame,
+ f: &mut Frame<'_>,
area: Rect,
results: &[History],
stats: Option<HistoryStats>,
@@ -1051,12 +1051,12 @@ impl State {
#[expect(clippy::cast_possible_truncation, clippy::too_many_arguments)]
fn draw_preview(
&self,
- f: &mut Frame,
+ f: &mut Frame<'_>,
style: StyleState,
input_chunk: Rect,
compactness: Compactness,
preview_chunk: Rect,
- preview: Paragraph,
+ preview: Paragraph<'_>,
prefix_width: u16,
) {
let input = self.build_input(style, prefix_width);
@@ -1088,7 +1088,6 @@ impl State {
title.alignment(Alignment::Left)
}
- #[expect(clippy::unused_self)]
fn build_help(&self, settings: &Settings) -> Paragraph<'_> {
match self.tab_index {
// search
@@ -1272,7 +1271,7 @@ impl TerminalWriter {
fn new() -> std::io::Result<Self> {
let stdout = stdout();
if stdout.is_terminal() {
- return Ok(TerminalWriter::Stdout(stdout));
+ return Ok(Self::Stdout(stdout));
}
// If stdout is not a terminal (e.g., captured by command substitution),
@@ -1280,7 +1279,7 @@ impl TerminalWriter {
// This allows usage like: VAR=$(atuin search -i)
#[cfg(unix)]
{
- Ok(TerminalWriter::Tty(
+ Ok(Self::Tty(
std::fs::File::options()
.read(true)
.write(true)
@@ -1293,17 +1292,17 @@ impl TerminalWriter {
impl Write for TerminalWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match self {
- TerminalWriter::Stdout(stdout) => stdout.write(buf),
+ Self::Stdout(stdout) => stdout.write(buf),
#[cfg(unix)]
- TerminalWriter::Tty(file) => file.write(buf),
+ Self::Tty(file) => file.write(buf),
}
}
fn flush(&mut self) -> std::io::Result<()> {
match self {
- TerminalWriter::Stdout(stdout) => stdout.flush(),
+ Self::Stdout(stdout) => stdout.flush(),
#[cfg(unix)]
- TerminalWriter::Tty(file) => file.flush(),
+ Self::Tty(file) => file.flush(),
}
}
}
@@ -1395,27 +1394,32 @@ fn restore_popup_area(saved: &SavedScreen, popup_rect: Rect, scroll_offset: u16)
// beforehand. We write `popup_rect.width` spaces instead of
// ClearType::CurrentLine so that only the popup area is cleared, not
// the entire terminal line.
- let _ = execute!(
+ drop(execute!(
stdout,
MoveTo(popup_rect.x, target_row),
- ratatui::crossterm::style::SetAttribute(ratatui::crossterm::style::Attribute::Reset),
- );
- let _ = write!(stdout, "{:width$}", "", width = popup_rect.width as usize);
- let _ = execute!(stdout, MoveTo(popup_rect.x, target_row));
+ crossterm::style::SetAttribute(crossterm::style::Attribute::Reset),
+ ));
+ drop(write!(
+ stdout,
+ "{:width$}",
+ "",
+ width = popup_rect.width as usize
+ ));
+ drop(execute!(stdout, MoveTo(popup_rect.x, target_row)));
if let Some(row_bytes) = saved.rows_data.get(source_row) {
- let _ = stdout.write_all(row_bytes);
+ drop(stdout.write_all(row_bytes));
}
}
- let _ = execute!(
+ drop(execute!(
stdout,
MoveTo(
saved.cursor_col,
saved.cursor_row.saturating_sub(scroll_offset)
)
- );
- let _ = stdout.flush();
+ ));
+ drop(stdout.flush());
}
struct Stdout {
@@ -1534,11 +1538,7 @@ fn compute_popup_placement(
// for now, it works. But it'd be great if it were more easily readable, and
// modular. I'd like to add some more stats and stuff at some point
-#[expect(
- clippy::cast_possible_truncation,
- clippy::too_many_lines,
- clippy::cognitive_complexity
-)]
+#[expect(clippy::too_many_lines, clippy::cognitive_complexity)]
pub(crate) async fn history(
query: &[String],
settings: &Settings,
@@ -1589,11 +1589,11 @@ pub(crate) async fn history(
if scroll > 0 {
use ratatui::crossterm::cursor::MoveTo;
let mut stdout = stdout();
- let _ = execute!(stdout, MoveTo(0, term_rows - 1));
+ drop(execute!(stdout, MoveTo(0, term_rows - 1)));
for _ in 0..scroll {
- let _ = writeln!(stdout);
+ drop(writeln!(stdout));
}
- let _ = stdout.flush();
+ drop(stdout.flush());
}
(saved, popup_rect, scroll)
@@ -1619,20 +1619,20 @@ pub(crate) async fn history(
let mut raw_stdout = std::io::stdout();
// Queue all commands without flushing so the terminal receives them
// as a single write — no intermediate cursor positions are visible.
- let _ = queue!(
+ drop(queue!(
raw_stdout,
- ratatui::crossterm::style::SetAttribute(ratatui::crossterm::style::Attribute::Reset)
- );
+ crossterm::style::SetAttribute(crossterm::style::Attribute::Reset)
+ ));
for row in popup_rect.y..popup_rect.y.saturating_add(popup_rect.height) {
- let _ = queue!(raw_stdout, MoveTo(popup_rect.x, row));
- let _ = write!(
+ drop(queue!(raw_stdout, MoveTo(popup_rect.x, row)));
+ drop(write!(
raw_stdout,
"{:width$}",
"",
width = popup_rect.width as usize
- );
+ ));
}
- let _ = raw_stdout.flush();
+ drop(raw_stdout.flush());
}
let backend = CrosstermBackend::new(stdout);
@@ -1996,7 +1996,7 @@ mod tests {
strategy: PreviewStrategy::Auto,
},
show_preview: true,
- ..Settings::now()
+ ..Settings::new().unwrap()
};
let settings_preview_auto_h2 = Settings {
@@ -2005,7 +2005,7 @@ mod tests {
},
show_preview: true,
max_preview_height: 2,
- ..Settings::now()
+ ..Settings::new().unwrap()
};
let settings_preview_h4 = Settings {
@@ -2014,7 +2014,7 @@ mod tests {
},
show_preview: true,
max_preview_height: 4,
- ..Settings::now()
+ ..Settings::new().unwrap()
};
let settings_preview_fixed = Settings {
@@ -2023,25 +2023,25 @@ mod tests {
},
show_preview: true,
max_preview_height: 15,
- ..Settings::now()
+ ..Settings::new().unwrap()
};
let cmd_60: History = History::capture()
- .timestamp(time::OffsetDateTime::now_utc())
+ .timestamp(OffsetDateTime::now_utc())
.command("for i in $(seq -w 10); do echo \"item number $i - abcd\"; done")
.cwd("/")
.build()
.into();
let cmd_124: History = History::capture()
- .timestamp(time::OffsetDateTime::now_utc())
+ .timestamp(OffsetDateTime::now_utc())
.command("echo 'Aurea prima sata est aetas, quae vindice nullo, sponte sua, sine lege fidem rectumque colebat. Poena metusque aberant'")
.cwd("/")
.build()
.into();
let cmd_200: History = History::capture()
- .timestamp(time::OffsetDateTime::now_utc())
+ .timestamp(OffsetDateTime::now_utc())
.command("CREATE USER atuin WITH ENCRYPTED PASSWORD 'supersecretpassword'; CREATE DATABASE atuin WITH OWNER = atuin; \\c atuin; REVOKE ALL PRIVILEGES ON SCHEMA public FROM PUBLIC; echo 'All done. 200 characters'")
.cwd("/")
.build()
@@ -2145,7 +2145,7 @@ mod tests {
// Test when there's no results, scrolling up or down doesn't underflow
#[test]
fn state_scroll_up_underflow() {
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let mut state = State {
history_count: 0,
results_state: ListState::default(),
@@ -2190,7 +2190,7 @@ mod tests {
use crate::atuin_client::settings::Keys;
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
- let mut settings = Settings::now();
+ let mut settings = Settings::new().unwrap();
settings.keys = Keys {
scroll_exits: true,
exit_past_line_start: false,
@@ -2316,7 +2316,7 @@ mod tests {
fn test_vim_gg_multikey_sequence() {
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let mut state = State {
history_count: 100,
@@ -2374,7 +2374,7 @@ mod tests {
fn test_vim_g_key_clears_on_other_input() {
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let mut state = State {
history_count: 100,
@@ -2428,7 +2428,7 @@ mod tests {
fn test_vim_big_g_jump_to_bottom() {
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let mut state = State {
history_count: 100,
@@ -2475,10 +2475,11 @@ mod tests {
}
#[test]
+ #[allow(clippy::similar_names)]
fn test_vim_ctrl_u_d_half_page_scroll() {
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let mut state = State {
history_count: 100,
@@ -2534,10 +2535,11 @@ mod tests {
}
#[test]
+ #[allow(clippy::similar_names)]
fn test_vim_ctrl_f_b_full_page_scroll() {
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let mut state = State {
history_count: 100,
@@ -2598,7 +2600,7 @@ mod tests {
/// Helper to build a State for executor tests.
fn make_executor_state(results_len: usize, selected: usize) -> State {
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let mut state = State {
history_count: results_len as i64,
results_state: ListState::default(),
@@ -2642,7 +2644,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 50);
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let result = state.execute_action(&Action::SelectNext, &settings);
assert!(matches!(result, super::InputAction::Continue));
// Non-inverted: SelectNext = scroll_down = selected - 1
@@ -2654,7 +2656,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 50);
- let mut settings = Settings::now();
+ let mut settings = Settings::new().unwrap();
settings.invert = true;
let result = state.execute_action(&Action::SelectNext, &settings);
assert!(matches!(result, super::InputAction::Continue));
@@ -2667,7 +2669,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 50);
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let result = state.execute_action(&Action::SelectPrevious, &settings);
assert!(matches!(result, super::InputAction::Continue));
// Non-inverted: SelectPrevious = scroll_up = selected + 1
@@ -2679,7 +2681,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 0);
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let result = state.execute_action(&Action::VimEnterNormal, &settings);
assert!(matches!(result, super::InputAction::Continue));
assert_eq!(state.keymap_mode, KeymapMode::VimNormal);
@@ -2691,7 +2693,7 @@ mod tests {
let mut state = make_executor_state(100, 0);
state.keymap_mode = KeymapMode::VimNormal;
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let result = state.execute_action(&Action::VimEnterInsert, &settings);
assert!(matches!(result, super::InputAction::Continue));
assert_eq!(state.keymap_mode, KeymapMode::VimInsert);
@@ -2702,7 +2704,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 5);
- let mut settings = Settings::now();
+ let mut settings = Settings::new().unwrap();
settings.enter_accept = true;
let result = state.execute_action(&Action::Accept, &settings);
assert!(matches!(result, super::InputAction::Accept(5)));
@@ -2714,7 +2716,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 5);
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let result = state.execute_action(&Action::ReturnSelection, &settings);
assert!(matches!(result, super::InputAction::Accept(5)));
assert!(!state.accept);
@@ -2725,7 +2727,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 5);
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let result = state.execute_action(&Action::AcceptNth(3), &settings);
assert!(matches!(result, super::InputAction::Accept(8)));
}
@@ -2735,7 +2737,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 50);
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let result = state.execute_action(&Action::ScrollToTop, &settings);
assert!(matches!(result, super::InputAction::Continue));
// Non-inverted: visual top = highest index
@@ -2747,7 +2749,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 50);
- let mut settings = Settings::now();
+ let mut settings = Settings::new().unwrap();
settings.invert = true;
let result = state.execute_action(&Action::ScrollToTop, &settings);
assert!(matches!(result, super::InputAction::Continue));
@@ -2760,7 +2762,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 50);
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let result = state.execute_action(&Action::ScrollToBottom, &settings);
assert!(matches!(result, super::InputAction::Continue));
// Non-inverted: visual bottom = index 0
@@ -2772,7 +2774,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 0);
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
assert_eq!(state.tab_index, 0);
state.execute_action(&Action::ToggleTab, &settings);
assert_eq!(state.tab_index, 1);
@@ -2785,7 +2787,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 0);
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
assert!(!state.prefix);
state.execute_action(&Action::EnterPrefixMode, &settings);
assert!(state.prefix);
@@ -2797,7 +2799,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 0);
- let mut settings = Settings::now();
+ let mut settings = Settings::new().unwrap();
settings.exit_mode = ExitMode::ReturnOriginal;
let result = state.execute_action(&Action::Exit, &settings);
@@ -2813,7 +2815,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 0);
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let result = state.execute_action(&Action::ReturnOriginal, &settings);
assert!(matches!(result, super::InputAction::ReturnOriginal));
}
@@ -2823,7 +2825,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 7);
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let result = state.execute_action(&Action::Copy, &settings);
assert!(matches!(result, super::InputAction::Copy(7)));
}
@@ -2833,7 +2835,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 7);
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let result = state.execute_action(&Action::Delete, &settings);
assert!(matches!(result, super::InputAction::Delete(7)));
}
@@ -2843,7 +2845,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 7);
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let result = state.execute_action(&Action::SwitchContext, &settings);
assert!(matches!(result, super::InputAction::SwitchContext(Some(7))));
}
@@ -2853,7 +2855,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 7);
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let result = state.execute_action(&Action::ClearContext, &settings);
assert!(matches!(result, super::InputAction::SwitchContext(None)));
}
@@ -2863,7 +2865,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 50);
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let result = state.execute_action(&Action::Noop, &settings);
assert!(matches!(result, super::InputAction::Continue));
assert_eq!(state.results_state.selected(), 50);
@@ -2875,7 +2877,7 @@ mod tests {
let mut state = make_executor_state(100, 5);
state.tab_index = 1;
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let result = state.execute_action(&Action::Accept, &settings);
assert!(matches!(result, super::InputAction::AcceptInspecting));
}
@@ -2885,7 +2887,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 0);
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let original_mode = state.search_mode;
let result = state.execute_action(&Action::CycleSearchMode, &settings);
assert!(matches!(result, super::InputAction::Continue));
@@ -2901,7 +2903,7 @@ mod tests {
state.search.input.insert('h');
state.search.input.insert('i');
state.keymap_mode = KeymapMode::VimNormal;
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
let result = state.execute_action(&Action::VimSearchInsert, &settings);
assert!(matches!(result, super::InputAction::Continue));
// Should clear input and switch to insert mode
@@ -2914,7 +2916,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 0);
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
// Insert some text
state.search.input.insert('h');
@@ -2946,7 +2948,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 0);
- let settings = Settings::now();
+ let settings = Settings::new().unwrap();
// Insert "hello"
state.search.input.insert('h');
@@ -2970,7 +2972,7 @@ mod tests {
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::collections::HashMap;
- let mut settings = Settings::now();
+ let mut settings = Settings::new().unwrap();
// Configure tab to return-query
settings.keymap.emacs = HashMap::from([(
"tab".to_string(),
diff --git a/crates/turtle/src/command/client/search/keybindings/defaults.rs b/crates/turtle/src/command/client/search/keybindings/defaults.rs
index 6627c84d..82219b33 100644
--- a/crates/turtle/src/command/client/search/keybindings/defaults.rs
+++ b/crates/turtle/src/command/client/search/keybindings/defaults.rs
@@ -527,7 +527,11 @@ impl KeymapSet {
#[cfg(test)]
mod tests {
- use super::*;
+ use super::{
+ Action, HashMap, KeymapSet, Settings, default_emacs_keymap, default_inspector_keymap,
+ default_prefix_keymap, default_vim_insert_keymap, default_vim_normal_keymap, key,
+ parse_binding_config,
+ };
use crate::command::client::search::keybindings::conditions::EvalContext;
fn make_ctx(cursor: usize, width: usize, selected: usize, len: usize) -> EvalContext {
@@ -543,7 +547,7 @@ mod tests {
}
fn default_settings() -> Settings {
- Settings::utc()
+ Settings::new().unwrap()
}
// -- Emacs keymap tests --
@@ -977,7 +981,7 @@ mod tests {
fn parse_simple_binding_config() {
use crate::atuin_client::settings::KeyBindingConfig;
let cfg = KeyBindingConfig::Simple("accept".to_string());
- let binding = super::parse_binding_config(&cfg).unwrap();
+ let binding = parse_binding_config(&cfg).unwrap();
assert_eq!(binding.rules.len(), 1);
assert!(binding.rules[0].condition.is_none());
assert_eq!(binding.rules[0].action, Action::Accept);
@@ -996,7 +1000,7 @@ mod tests {
action: "cursor-left".to_string(),
},
]);
- let binding = super::parse_binding_config(&cfg).unwrap();
+ let binding = parse_binding_config(&cfg).unwrap();
assert_eq!(binding.rules.len(), 2);
assert!(binding.rules[0].condition.is_some());
assert_eq!(binding.rules[0].action, Action::Exit);
@@ -1008,7 +1012,7 @@ mod tests {
fn parse_binding_config_invalid_action() {
use crate::atuin_client::settings::KeyBindingConfig;
let cfg = KeyBindingConfig::Simple("not-a-real-action".to_string());
- assert!(super::parse_binding_config(&cfg).is_err());
+ assert!(parse_binding_config(&cfg).is_err());
}
#[test]
@@ -1018,7 +1022,7 @@ mod tests {
when: Some("not-a-real-condition".to_string()),
action: "exit".to_string(),
}]);
- assert!(super::parse_binding_config(&cfg).is_err());
+ assert!(parse_binding_config(&cfg).is_err());
}
#[test]
@@ -1213,22 +1217,6 @@ mod tests {
}
#[test]
- fn keys_has_non_default_values_detection() {
- use crate::atuin_client::settings::Keys;
-
- let standard = Keys::standard_defaults();
- assert!(!standard.has_non_default_values());
-
- let mut modified = Keys::standard_defaults();
- modified.scroll_exits = false;
- assert!(modified.has_non_default_values());
-
- let mut modified = Keys::standard_defaults();
- modified.prefix = "x".to_string();
- assert!(modified.has_non_default_values());
- }
-
- #[test]
fn original_input_empty_condition_in_config() {
use crate::atuin_client::settings::{KeyBindingConfig, KeyRuleConfig};
use std::collections::HashMap;
diff --git a/crates/turtle/src/command/client/search/keybindings/key.rs b/crates/turtle/src/command/client/search/keybindings/key.rs
index 35107a24..841656d2 100644
--- a/crates/turtle/src/command/client/search/keybindings/key.rs
+++ b/crates/turtle/src/command/client/search/keybindings/key.rs
@@ -16,6 +16,10 @@ pub(crate) struct SingleKey {
/// The key code portion of a key press.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+#[expect(
+ variant_size_differences,
+ reason = "It's not that much. So should be ok?"
+)]
pub(crate) enum KeyCodeValue {
Char(char),
Enter,
@@ -59,7 +63,7 @@ impl SingleKey {
// we store the uppercase char directly and clear the shift flag
// since the case already encodes it.
if shift && !ctrl && !alt && !super_key && c.is_ascii_uppercase() {
- return Some(SingleKey {
+ return Some(Self {
code: KeyCodeValue::Char(c),
ctrl: false,
alt: false,
@@ -74,7 +78,7 @@ impl SingleKey {
KeyCode::Tab => KeyCodeValue::Tab,
// BackTab is sent by many terminals for Shift+Tab
KeyCode::BackTab => {
- return Some(SingleKey {
+ return Some(Self {
code: KeyCodeValue::Tab,
ctrl,
alt,
@@ -98,7 +102,7 @@ impl SingleKey {
_ => return None,
};
- Some(SingleKey {
+ Some(Self {
code,
ctrl,
alt,
@@ -185,7 +189,7 @@ impl SingleKey {
let c = chars[0];
// An uppercase letter implies shift (unless shift already specified)
if c.is_ascii_uppercase() && !ctrl && !alt && !super_key {
- return Ok(SingleKey {
+ return Ok(Self {
code: KeyCodeValue::Char(c),
ctrl: false,
alt: false,
@@ -200,7 +204,7 @@ impl SingleKey {
}
};
- Ok(SingleKey {
+ Ok(Self {
code,
ctrl,
alt,
@@ -272,9 +276,9 @@ impl KeyInput {
if parts.len() > 1 {
let keys: Result<Vec<SingleKey>, String> =
parts.iter().map(|p| SingleKey::parse(p)).collect();
- Ok(KeyInput::Sequence(keys?))
+ Ok(Self::Sequence(keys?))
} else {
- Ok(KeyInput::Single(SingleKey::parse(s)?))
+ Ok(Self::Single(SingleKey::parse(s)?))
}
}
}
@@ -282,8 +286,8 @@ impl KeyInput {
impl fmt::Display for KeyInput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
- KeyInput::Single(k) => write!(f, "{k}"),
- KeyInput::Sequence(keys) => {
+ Self::Single(k) => write!(f, "{k}"),
+ Self::Sequence(keys) => {
for (i, k) in keys.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
@@ -305,7 +309,7 @@ impl Serialize for KeyInput {
impl<'de> Deserialize<'de> for KeyInput {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
- KeyInput::parse(&s).map_err(serde::de::Error::custom)
+ Self::parse(&s).map_err(serde::de::Error::custom)
}
}
diff --git a/crates/turtle/src/command/mod.rs b/crates/turtle/src/command/mod.rs
index 78de2d03..9a648254 100644
--- a/crates/turtle/src/command/mod.rs
+++ b/crates/turtle/src/command/mod.rs
@@ -101,11 +101,11 @@ fn semantic_command_capture_sink() -> Option<crate::atuin_pty_proxy::CommandCapt
});
Some(Box::new(move |capture| {
- let _ = tx.try_send(capture);
+ drop(tx.try_send(capture));
}))
}
-#[cfg(all(feature = "daemon", feature = "pty-proxy", unix))]
+#[cfg(unix)]
#[inline]
fn is_truthy_env(name: &str) -> bool {
std::env::var(name)
@@ -114,14 +114,16 @@ fn is_truthy_env(name: &str) -> bool {
.is_some_and(|value| !value.trim().is_empty() && value.trim() != "false")
}
-#[cfg(all(feature = "daemon", feature = "pty-proxy", unix))]
+#[cfg(unix)]
async fn send_semantic_command_captures(
settings: &crate::atuin_client::settings::Settings,
batch: Vec<crate::atuin_pty_proxy::CommandCapture>,
) {
+ use crate::atuin_daemon::generated;
+
let captures = batch
.into_iter()
- .map(|capture| crate::atuin_daemon::semantic::CommandCapture {
+ .map(|capture| generated::semantic::CommandCapture {
prompt: capture.prompt,
command: capture.command,
output: capture.output,
@@ -134,6 +136,6 @@ async fn send_semantic_command_captures(
.collect();
if let Ok(mut client) = crate::atuin_daemon::SemanticClient::from_settings(settings).await {
- let _ = client.record_commands(captures).await;
+ drop(client.record_commands(captures).await);
}
}
diff --git a/crates/turtle/src/main.rs b/crates/turtle/src/main.rs
index 664cf3a6..7ea08a9b 100644
--- a/crates/turtle/src/main.rs
+++ b/crates/turtle/src/main.rs
@@ -1,8 +1,15 @@
-#![warn(clippy::pedantic, clippy::nursery)]
-#![allow(clippy::use_self, clippy::missing_const_for_fn)] // not 100% reliable
-// #![deny(unsafe_code)]
#![forbid(unsafe_code)]
-#![expect(clippy::redundant_pub_crate)]
+#![warn(clippy::pedantic, clippy::nursery)]
+#![expect(
+ clippy::missing_const_for_fn, // not 100% reliable
+ clippy::redundant_pub_crate,
+)]
+#![expect(
+ clippy::cast_possible_wrap,
+ clippy::cast_sign_loss,
+ clippy::cast_possible_truncation,
+ reason = "We should remove all of these. But it's just a lot of work in this code-base"
+)]
use clap::Parser;
use clap::builder::Styles;