aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-06-12 17:16:19 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-06-12 17:16:19 +0200
commit2ca7dd57b12861e8c9bbc9238cda612e0ff22ff3 (patch)
tree302a644f6a50d60cc8304c4498fe6bbb72ddaaa9
parentfeat(server): Really make users stateless (with tests) (diff)
downloadatuin-2ca7dd57b12861e8c9bbc9238cda612e0ff22ff3.zip
chore(treewide): Cleanup themes
-rw-r--r--Cargo.toml54
-rw-r--r--crates/atuin-nucleo/Cargo.lock319
-rw-r--r--crates/atuin-nucleo/matcher/src/pattern.rs2
-rw-r--r--crates/turtle/Cargo.toml6
-rw-r--r--crates/turtle/src/atuin_client/database.rs99
-rw-r--r--crates/turtle/src/atuin_client/history.rs61
-rw-r--r--crates/turtle/src/atuin_client/history/store.rs2
-rw-r--r--crates/turtle/src/atuin_client/mod.rs2
-rw-r--r--crates/turtle/src/atuin_client/record/mod.rs3
-rw-r--r--crates/turtle/src/atuin_client/record/sqlite_store.rs94
-rw-r--r--crates/turtle/src/atuin_client/record/store.rs60
-rw-r--r--crates/turtle/src/atuin_client/record/sync.rs28
-rw-r--r--crates/turtle/src/atuin_client/settings.rs91
-rw-r--r--crates/turtle/src/atuin_client/settings/watcher.rs70
-rw-r--r--crates/turtle/src/atuin_client/theme.rs846
-rw-r--r--crates/turtle/src/atuin_client/utils.rs1
-rw-r--r--crates/turtle/src/atuin_common/record.rs114
-rw-r--r--crates/turtle/src/atuin_daemon/client.rs83
-rw-r--r--crates/turtle/src/atuin_daemon/daemon.rs28
-rw-r--r--crates/turtle/src/atuin_daemon/mod.rs3
-rw-r--r--crates/turtle/src/atuin_daemon/search/index.rs29
-rw-r--r--crates/turtle/src/atuin_history/stats.rs33
-rw-r--r--crates/turtle/src/command/client.rs41
-rw-r--r--crates/turtle/src/command/client/daemon.rs36
-rw-r--r--crates/turtle/src/command/client/history.rs224
-rw-r--r--crates/turtle/src/command/client/search.rs4
-rw-r--r--crates/turtle/src/command/client/search/engines.rs8
-rw-r--r--crates/turtle/src/command/client/search/history_list.rs44
-rw-r--r--crates/turtle/src/command/client/search/inspector.rs52
-rw-r--r--crates/turtle/src/command/client/search/interactive.rs140
-rw-r--r--crates/turtle/src/command/client/stats.rs11
-rw-r--r--crates/turtle/src/command/client/store.rs14
-rw-r--r--crates/turtle/src/command/client/store/pull.rs12
-rw-r--r--crates/turtle/src/command/client/store/purge.rs4
-rw-r--r--crates/turtle/src/command/client/store/push.rs4
-rw-r--r--crates/turtle/src/command/client/store/rebuild.rs2
-rw-r--r--crates/turtle/src/command/client/store/rekey.rs1
-rw-r--r--crates/turtle/src/command/client/store/verify.rs4
-rw-r--r--crates/turtle/src/command/client/sync.rs6
-rw-r--r--crates/turtle/src/command/client/wrapped.rs18
-rw-r--r--crates/turtle/src/command/mod.rs15
-rw-r--r--crates/turtle/src/main.rs2
42 files changed, 413 insertions, 2257 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 4c87b914..78d8dd44 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,3 +20,57 @@ readme = "README.md"
[workspace.dependencies]
atuin-nucleo = { path = "crates/atuin-nucleo", version = "0.6.0" }
atuin-nucleo-matcher = { path = "crates/atuin-nucleo/matcher", version = "0.3.1" }
+
+[workspace.lints.rust]
+# rustc lint groups https://doc.rust-lang.org/rustc/lints/groups.html
+warnings = "warn"
+future_incompatible = { level = "warn", priority = -1 }
+let_underscore = { level = "warn", priority = -1 }
+nonstandard_style = { level = "warn", priority = -1 }
+rust_2018_compatibility = { level = "warn", priority = -1 }
+rust_2018_idioms = { level = "warn", priority = -1 }
+rust_2021_compatibility = { level = "warn", priority = -1 }
+unused = { level = "warn", priority = -1 }
+
+# rustc allowed-by-default lints https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html
+# missing_docs = "warn"
+macro_use_extern_crate = "warn"
+meta_variable_misuse = "warn"
+missing_abi = "warn"
+missing_copy_implementations = "warn"
+missing_debug_implementations = "warn"
+non_ascii_idents = "warn"
+noop_method_call = "warn"
+single_use_lifetimes = "warn"
+trivial_casts = "warn"
+trivial_numeric_casts = "warn"
+unreachable_pub = "warn"
+unsafe_op_in_unsafe_fn = "warn"
+unused_crate_dependencies = "warn"
+unused_import_braces = "warn"
+unused_lifetimes = "warn"
+unused_qualifications = "warn"
+variant_size_differences = "warn"
+
+[workspace.lints.rustdoc]
+# rustdoc lints https://doc.rust-lang.org/rustdoc/lints.html
+broken_intra_doc_links = "warn"
+private_intra_doc_links = "warn"
+missing_crate_level_docs = "warn"
+private_doc_tests = "warn"
+invalid_codeblock_attributes = "warn"
+invalid_rust_codeblocks = "warn"
+bare_urls = "warn"
+
+[workspace.lints.clippy]
+# clippy allowed by default
+dbg_macro = "warn"
+
+# clippy categories https://doc.rust-lang.org/clippy/
+all = { level = "warn", priority = -1 }
+correctness = { level = "warn", priority = -1 }
+suspicious = { level = "warn", priority = -1 }
+style = { level = "warn", priority = -1 }
+complexity = { level = "warn", priority = -1 }
+perf = { level = "warn", priority = -1 }
+pedantic = { level = "warn", priority = -1 }
diff --git a/crates/atuin-nucleo/Cargo.lock b/crates/atuin-nucleo/Cargo.lock
deleted file mode 100644
index d31c11c0..00000000
--- a/crates/atuin-nucleo/Cargo.lock
+++ /dev/null
@@ -1,319 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 3
-
-[[package]]
-name = "autocfg"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
-
-[[package]]
-name = "benches"
-version = "0.1.0"
-dependencies = [
- "brunch",
- "fuzzy-matcher",
- "nucleo",
- "walkdir",
-]
-
-[[package]]
-name = "bitflags"
-version = "2.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
-
-[[package]]
-name = "brunch"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3932d710d985d35c7b08e7e439a6ac8607aa8f619d373eb1f808578cd3cd56e5"
-dependencies = [
- "dactyl",
- "unicode-width",
-]
-
-[[package]]
-name = "cfg-if"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
-
-[[package]]
-name = "crossbeam-deque"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
-dependencies = [
- "crossbeam-epoch",
- "crossbeam-utils",
-]
-
-[[package]]
-name = "crossbeam-epoch"
-version = "0.9.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
-dependencies = [
- "crossbeam-utils",
-]
-
-[[package]]
-name = "crossbeam-utils"
-version = "0.8.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
-
-[[package]]
-name = "dactyl"
-version = "0.7.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ecad1ab94b1336be6cff409436ad9ceedb0afd52a85d54132189c2c3babb049"
-
-[[package]]
-name = "either"
-version = "1.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
-
-[[package]]
-name = "fuzzy-matcher"
-version = "0.3.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
-dependencies = [
- "thread_local",
-]
-
-[[package]]
-name = "libc"
-version = "0.2.167"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc"
-
-[[package]]
-name = "lock_api"
-version = "0.4.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
-dependencies = [
- "autocfg",
- "scopeguard",
-]
-
-[[package]]
-name = "memchr"
-version = "2.7.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
-
-[[package]]
-name = "nucleo"
-version = "0.5.0"
-dependencies = [
- "nucleo-matcher",
- "parking_lot",
- "rayon",
-]
-
-[[package]]
-name = "nucleo-matcher"
-version = "0.3.1"
-dependencies = [
- "memchr",
- "unicode-segmentation",
-]
-
-[[package]]
-name = "once_cell"
-version = "1.20.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
-
-[[package]]
-name = "parking_lot"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
-dependencies = [
- "lock_api",
- "parking_lot_core",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.9.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
-dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall",
- "smallvec",
- "windows-targets",
-]
-
-[[package]]
-name = "rayon"
-version = "1.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
-dependencies = [
- "either",
- "rayon-core",
-]
-
-[[package]]
-name = "rayon-core"
-version = "1.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
-dependencies = [
- "crossbeam-deque",
- "crossbeam-utils",
-]
-
-[[package]]
-name = "redox_syscall"
-version = "0.5.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
-dependencies = [
- "bitflags",
-]
-
-[[package]]
-name = "same-file"
-version = "1.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
-dependencies = [
- "winapi-util",
-]
-
-[[package]]
-name = "scopeguard"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
-
-[[package]]
-name = "smallvec"
-version = "1.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
-
-[[package]]
-name = "thread_local"
-version = "1.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
-dependencies = [
- "cfg-if",
- "once_cell",
-]
-
-[[package]]
-name = "unicode-segmentation"
-version = "1.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
-
-[[package]]
-name = "unicode-width"
-version = "0.1.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
-
-[[package]]
-name = "walkdir"
-version = "2.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
-dependencies = [
- "same-file",
- "winapi-util",
-]
-
-[[package]]
-name = "winapi-util"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
-dependencies = [
- "windows-sys",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.59.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
-dependencies = [
- "windows-targets",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
-dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
-]
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
-
-[[package]]
-name = "windows_i686_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
diff --git a/crates/atuin-nucleo/matcher/src/pattern.rs b/crates/atuin-nucleo/matcher/src/pattern.rs
index 495feede..89affdde 100644
--- a/crates/atuin-nucleo/matcher/src/pattern.rs
+++ b/crates/atuin-nucleo/matcher/src/pattern.rs
@@ -16,9 +16,11 @@ pub enum CaseMatching {
/// Characters never match their case folded version (`a != A`).
#[cfg_attr(not(feature = "unicode-casefold"), default)]
Respect,
+
/// Characters always match their case folded version (`a == A`).
#[cfg(feature = "unicode-casefold")]
Ignore,
+
/// Acts like [`Ignore`](CaseMatching::Ignore) if all characters in a pattern atom are
/// lowercase and like [`Respect`](CaseMatching::Respect) otherwise.
#[default]
diff --git a/crates/turtle/Cargo.toml b/crates/turtle/Cargo.toml
index 87557905..df98aa8f 100644
--- a/crates/turtle/Cargo.toml
+++ b/crates/turtle/Cargo.toml
@@ -140,3 +140,9 @@ testing_logger = "0.1.1"
protox = "0.9"
tonic-build = "0.14"
tonic-prost-build = "0.14"
+
+[package.metadata.docs.rs]
+all-features = true
+
+[lints]
+workspace = true
diff --git a/crates/turtle/src/atuin_client/database.rs b/crates/turtle/src/atuin_client/database.rs
index f8b73809..6a2d5887 100644
--- a/crates/turtle/src/atuin_client/database.rs
+++ b/crates/turtle/src/atuin_client/database.rs
@@ -8,7 +8,6 @@ use std::{
use crate::atuin_common::utils;
use fs_err as fs;
use itertools::Itertools;
-use rand::{Rng, distributions::Alphanumeric};
use sql_builder::{SqlBuilder, SqlName, bind::Bind, esc, quote};
use sqlx::{
Result, Row,
@@ -192,8 +191,10 @@ impl ClientSqlite {
History::from_db()
.id(row.get("id"))
.timestamp(
- OffsetDateTime::from_unix_timestamp_nanos(row.get::<i64, _>("timestamp") as i128)
- .unwrap(),
+ OffsetDateTime::from_unix_timestamp_nanos(i128::from(
+ row.get::<i64, _>("timestamp"),
+ ))
+ .unwrap(),
)
.duration(row.get("duration"))
.exit(row.get("exit"))
@@ -247,31 +248,6 @@ impl ClientSqlite {
Ok(res)
}
- pub(crate) async fn update(&self, h: &History) -> Result<()> {
- debug!("updating sqlite history");
-
- sqlx::query(
- "update history
- set timestamp = ?2, duration = ?3, exit = ?4, command = ?5, cwd = ?6, session = ?7, hostname = ?8, author = ?9, intent = ?10, deleted_at = ?11
- where id = ?1",
- )
- .bind(h.id.0.as_str())
- .bind(h.timestamp.unix_timestamp_nanos() as i64)
- .bind(h.duration)
- .bind(h.exit)
- .bind(h.command.as_str())
- .bind(h.cwd.as_str())
- .bind(h.session.as_str())
- .bind(h.hostname.as_str())
- .bind(h.author.as_str())
- .bind(h.intent.as_deref())
- .bind(h.deleted_at.map(|t|t.unix_timestamp_nanos() as i64))
- .execute(&self.pool)
- .await?;
-
- Ok(())
- }
-
// make a unique list, that only shows the *newest* version of things
pub(crate) async fn list(
&self,
@@ -452,9 +428,9 @@ impl ClientSqlite {
if !is_or {
is_or = true;
continue;
- } else {
- format!("{glob}|{glob}")
}
+
+ format!("{glob}|{glob}")
}
QueryToken::MatchStart(term, _) => {
format!("{term}{glob}")
@@ -584,22 +560,6 @@ impl ClientSqlite {
Paged::new(self.clone(), page_size, include_deleted, unique)
}
- // deleted_at doesn't mean the actual time that the user deleted it,
- // but the time that the system marks it as deleted
- pub(crate) async fn delete(&self, mut h: History) -> Result<()> {
- let now = OffsetDateTime::now_utc();
- h.command = rand::thread_rng()
- .sample_iter(&Alphanumeric)
- .take(32)
- .map(char::from)
- .collect(); // overwrite with random string
- h.deleted_at = Some(now); // delete it
-
- self.update(&h).await?; // save it
-
- Ok(())
- }
-
pub(crate) async fn delete_rows(&self, ids: &[HistoryId]) -> Result<()> {
let mut tx = self.pool.begin().await?;
@@ -1233,53 +1193,6 @@ mod test {
}
#[tokio::test(flavor = "multi_thread")]
- async fn test_paged_include_deleted() {
- let mut db = ClientSqlite::new("sqlite::memory:", test_local_timeout())
- .await
- .unwrap();
-
- // Add items
- new_history_item(&mut db, "keep1").await.unwrap();
- new_history_item(&mut db, "keep2").await.unwrap();
- new_history_item(&mut db, "delete_me").await.unwrap();
-
- // Delete one item
- let all = db
- .list(
- &[],
- &Context {
- hostname: "".to_string(),
- session: "".to_string(),
- cwd: "".to_string(),
- host_id: "".to_string(),
- git_root: None,
- },
- None,
- false,
- false,
- )
- .await
- .unwrap();
-
- let to_delete = all
- .iter()
- .find(|h| h.command == "delete_me")
- .unwrap()
- .clone();
- db.delete(to_delete).await.unwrap();
-
- // Without include_deleted - should get 2
- let mut paged = db.all_paged(10, false, false);
- let page = paged.next().await.unwrap().unwrap();
- assert_eq!(page.len(), 2);
-
- // With include_deleted - should get 3
- let mut paged_deleted = db.all_paged(10, true, false);
- let page_deleted = paged_deleted.next().await.unwrap().unwrap();
- assert_eq!(page_deleted.len(), 3);
- }
-
- #[tokio::test(flavor = "multi_thread")]
async fn test_search_bench_dupes() {
let context = Context {
hostname: "test:host".to_string(),
diff --git a/crates/turtle/src/atuin_client/history.rs b/crates/turtle/src/atuin_client/history.rs
index 5e2f89f2..1f89cd71 100644
--- a/crates/turtle/src/atuin_client/history.rs
+++ b/crates/turtle/src/atuin_client/history.rs
@@ -61,24 +61,34 @@ pub(crate) struct History {
///
/// Stored as `client_id` in the database.
pub(crate) id: HistoryId,
+
/// When the command was run.
pub(crate) timestamp: OffsetDateTime,
+
/// How long the command took to run.
pub(crate) duration: i64,
+
/// The exit code of the command.
pub(crate) exit: i64,
+
/// The command that was run.
pub(crate) command: String,
+
/// The current working directory when the command was run.
pub(crate) cwd: String,
+
/// The session ID, associated with a terminal session.
pub(crate) session: String,
+
/// The hostname of the machine the command was run on.
pub(crate) hostname: String,
+
/// Who wrote this command (human user or automation/agent identity).
pub(crate) author: String,
+
/// Optional rationale for why the command was executed.
pub(crate) intent: Option<String>,
+
/// Timestamp, which is set when the entry is deleted, allowing a soft delete.
pub(crate) deleted_at: Option<OffsetDateTime>,
}
@@ -87,7 +97,7 @@ pub(crate) struct History {
pub(crate) struct HistoryStats {
/// The command that was ran after this one in the session
pub(crate) next: Option<History>,
- ///
+
/// The command that was ran before this one in the session
pub(crate) previous: Option<History>,
@@ -333,7 +343,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(),
@@ -343,7 +353,7 @@ impl History {
author: author.unwrap_or_else(|| Self::author_from_hostname(hostname)),
intent,
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()?,
})
}
@@ -357,51 +367,6 @@ impl History {
}
}
- /// Builder for a history entry that is imported from shell history.
- ///
- /// The only two required fields are `timestamp` and `command`.
- ///
- /// ## Examples
- /// ```
- /// use crate::atuin_client::history::History;
- ///
- /// let history: History = History::import()
- /// .timestamp(time::OffsetDateTime::now_utc())
- /// .command("ls -la")
- /// .build()
- /// .into();
- /// ```
- ///
- /// If shell history contains more information, it can be added to the builder:
- /// ```
- /// use crate::atuin_client::history::History;
- ///
- /// let history: History = History::import()
- /// .timestamp(time::OffsetDateTime::now_utc())
- /// .command("ls -la")
- /// .cwd("/home/user")
- /// .exit(0)
- /// .duration(100)
- /// .build()
- /// .into();
- /// ```
- ///
- /// Unknown command or command without timestamp cannot be imported, which
- /// is forced at compile time:
- ///
- /// ```compile_fail
- /// use crate::atuin_client::history::History;
- ///
- /// // this will not compile because timestamp is missing
- /// let history: History = History::import()
- /// .command("ls -la")
- /// .build()
- /// .into();
- /// ```
- pub(crate) fn import() -> builder::HistoryImportedBuilder {
- builder::HistoryImported::builder()
- }
-
/// Builder for a history entry that is captured via hook.
///
/// This builder is used only at the `start` step of the hook,
diff --git a/crates/turtle/src/atuin_client/history/store.rs b/crates/turtle/src/atuin_client/history/store.rs
index a8162e21..c6e079f3 100644
--- a/crates/turtle/src/atuin_client/history/store.rs
+++ b/crates/turtle/src/atuin_client/history/store.rs
@@ -7,7 +7,7 @@ use tracing::debug;
use crate::atuin_client::{
database::{ClientSqlite, current_context},
- record::{encryption::PASETO_V4, sqlite_store::SqliteStore, store::Store},
+ record::{encryption::PASETO_V4, sqlite_store::SqliteStore},
};
use crate::atuin_common::record::{DecryptedData, Host, HostId, Record, RecordId, RecordIdx};
diff --git a/crates/turtle/src/atuin_client/mod.rs b/crates/turtle/src/atuin_client/mod.rs
index 530b7d81..851dfbdb 100644
--- a/crates/turtle/src/atuin_client/mod.rs
+++ b/crates/turtle/src/atuin_client/mod.rs
@@ -1,6 +1,4 @@
-#[cfg(feature = "sync")]
pub(crate) mod api_client;
-
pub(crate) mod database;
pub(crate) mod encryption;
pub(crate) mod history;
diff --git a/crates/turtle/src/atuin_client/record/mod.rs b/crates/turtle/src/atuin_client/record/mod.rs
index 175c7a9d..4e5774ea 100644
--- a/crates/turtle/src/atuin_client/record/mod.rs
+++ b/crates/turtle/src/atuin_client/record/mod.rs
@@ -1,6 +1,3 @@
pub(crate) mod encryption;
pub(crate) mod sqlite_store;
-pub(crate) mod store;
-
-#[cfg(feature = "sync")]
pub(crate) mod sync;
diff --git a/crates/turtle/src/atuin_client/record/sqlite_store.rs b/crates/turtle/src/atuin_client/record/sqlite_store.rs
index f8eab076..24188443 100644
--- a/crates/turtle/src/atuin_client/record/sqlite_store.rs
+++ b/crates/turtle/src/atuin_client/record/sqlite_store.rs
@@ -5,7 +5,6 @@
use std::str::FromStr;
use std::{path::Path, time::Duration};
-use async_trait::async_trait;
use eyre::{Result, eyre};
use fs_err as fs;
@@ -22,7 +21,6 @@ use crate::atuin_common::utils;
use uuid::Uuid;
use super::encryption::PASETO_V4;
-use super::store::Store;
#[derive(Debug, Clone)]
pub(crate) struct SqliteStore {
@@ -37,7 +35,8 @@ impl SqliteStore {
if utils::broken_symlink(path) {
eprintln!(
- "Atuin: Sqlite db path ({path:?}) is a broken symlink. Unable to read or create replacement."
+ "Atuin: Sqlite db path ({}) is a broken symlink. Unable to read or create replacement.",
+ path.display()
);
std::process::exit(1);
}
@@ -128,9 +127,18 @@ impl SqliteStore {
}
}
-#[async_trait]
-impl Store for SqliteStore {
- async fn push_batch(
+/// A record store stores records
+/// In more detail - we tend to need to process this into _another_ format to actually query it.
+/// As is, the record store is intended as the source of truth for arbitrary data, which could
+/// be shell history, kvs, etc.
+impl SqliteStore {
+ /// Push a record
+ pub(crate) async fn push(&self, record: &Record<EncryptedData>) -> Result<()> {
+ self.push_batch(std::iter::once(record)).await
+ }
+
+ /// Push a batch of records, all in one transaction
+ pub(crate) async fn push_batch(
&self,
records: impl Iterator<Item = &Record<EncryptedData>> + Send + Sync,
) -> Result<()> {
@@ -145,7 +153,7 @@ impl Store for SqliteStore {
Ok(())
}
- async fn get(&self, id: RecordId) -> Result<Record<EncryptedData>> {
+ pub(crate) async fn get(&self, id: RecordId) -> Result<Record<EncryptedData>> {
let res = sqlx::query("select * from store where store.id = ?1")
.bind(id.0.as_hyphenated().to_string())
.map(Self::query_row)
@@ -155,7 +163,7 @@ impl Store for SqliteStore {
Ok(res)
}
- async fn delete(&self, id: RecordId) -> Result<()> {
+ pub(crate) async fn delete(&self, id: RecordId) -> Result<()> {
sqlx::query("delete from store where id = ?1")
.bind(id.0.as_hyphenated().to_string())
.execute(&self.pool)
@@ -164,13 +172,17 @@ impl Store for SqliteStore {
Ok(())
}
- async fn delete_all(&self) -> Result<()> {
+ pub(crate) async fn delete_all(&self) -> Result<()> {
sqlx::query("delete from store").execute(&self.pool).await?;
Ok(())
}
- async fn last(&self, host: HostId, tag: &str) -> Result<Option<Record<EncryptedData>>> {
+ pub(crate) async fn last(
+ &self,
+ host: HostId,
+ tag: &str,
+ ) -> Result<Option<Record<EncryptedData>>> {
let res =
sqlx::query("select * from store where host=?1 and tag=?2 order by idx desc limit 1")
.bind(host.0.as_hyphenated().to_string())
@@ -186,21 +198,15 @@ impl Store for SqliteStore {
}
}
- async fn first(&self, host: HostId, tag: &str) -> Result<Option<Record<EncryptedData>>> {
+ pub(crate) async fn first(
+ &self,
+ host: HostId,
+ tag: &str,
+ ) -> Result<Option<Record<EncryptedData>>> {
self.idx(host, tag, 0).await
}
- async fn len_all(&self) -> Result<u64> {
- let res: Result<(i64,), sqlx::Error> = sqlx::query_as("select count(*) from store")
- .fetch_one(&self.pool)
- .await;
- match res {
- Err(e) => Err(eyre!("failed to fetch local store len: {}", e)),
- Ok(v) => Ok(v.0 as u64),
- }
- }
-
- async fn len_tag(&self, tag: &str) -> Result<u64> {
+ pub(crate) async fn len_tag(&self, tag: &str) -> Result<u64> {
let res: Result<(i64,), sqlx::Error> =
sqlx::query_as("select count(*) from store where tag=?1")
.bind(tag)
@@ -212,17 +218,8 @@ impl Store for SqliteStore {
}
}
- async fn len(&self, host: HostId, tag: &str) -> Result<u64> {
- let last = self.last(host, tag).await?;
-
- if let Some(last) = last {
- return Ok(last.idx + 1);
- }
-
- return Ok(0);
- }
-
- async fn next(
+ /// Get the next `limit` records, after and including the given index
+ pub(crate) async fn next(
&self,
host: HostId,
tag: &str,
@@ -243,7 +240,8 @@ impl Store for SqliteStore {
Ok(res)
}
- async fn idx(
+ /// Get the first record for a given host and tag
+ pub(crate) async fn idx(
&self,
host: HostId,
tag: &str,
@@ -264,7 +262,7 @@ impl Store for SqliteStore {
}
}
- async fn status(&self) -> Result<RecordStatus> {
+ pub(crate) async fn status(&self) -> Result<RecordStatus> {
let mut status = RecordStatus::new();
let res: Result<Vec<(String, String, i64)>, sqlx::Error> =
@@ -288,7 +286,8 @@ impl Store for SqliteStore {
Ok(status)
}
- async fn all_tagged(&self, tag: &str) -> Result<Vec<Record<EncryptedData>>> {
+ /// Get all records for a given tag
+ pub(crate) async fn all_tagged(&self, tag: &str) -> Result<Vec<Record<EncryptedData>>> {
let res = sqlx::query("select * from store where tag = ?1 order by timestamp asc")
.bind(tag)
.map(Self::query_row)
@@ -300,7 +299,7 @@ impl Store for SqliteStore {
/// Reencrypt every single item in this store with a new key
/// Be careful - this may mess with sync.
- async fn re_encrypt(&self, old_key: &[u8; 32], new_key: &[u8; 32]) -> Result<()> {
+ pub(crate) async fn re_encrypt(&self, old_key: &[u8; 32], new_key: &[u8; 32]) -> Result<()> {
// Load all the records
// In memory like some of the other code here
// This will never be called in a hot loop, and only under the following circumstances
@@ -339,7 +338,7 @@ impl Store for SqliteStore {
/// Verify that every record in this store can be decrypted with the current key
/// Someday maybe also check each tag/record can be deserialized, but not for now.
- async fn verify(&self, key: &[u8; 32]) -> Result<()> {
+ pub(crate) async fn verify(&self, key: &[u8; 32]) -> Result<()> {
let all = self.load_all().await?;
all.into_iter()
@@ -351,7 +350,7 @@ impl Store for SqliteStore {
/// Verify that every record in this store can be decrypted with the current key
/// Someday maybe also check each tag/record can be deserialized, but not for now.
- async fn purge(&self, key: &[u8; 32]) -> Result<()> {
+ pub(crate) async fn purge(&self, key: &[u8; 32]) -> Result<()> {
let all = self.load_all().await?;
for record in all.iter() {
@@ -374,15 +373,16 @@ impl Store for SqliteStore {
#[cfg(test)]
mod tests {
- use crate::atuin_common::{
- record::{DecryptedData, EncryptedData, Host, HostId, Record},
- utils::uuid_v7,
- };
-
use crate::{
- encryption::generate_encoded_key,
- record::{encryption::PASETO_V4, store::Store},
- settings::test_local_timeout,
+ atuin_client::{
+ encryption::generate_encoded_key, record::encryption::PASETO_V4,
+ settings::test_local_timeout,
+ },
+ atuin_common::{
+ self,
+ record::{DecryptedData, EncryptedData, Host, HostId, Record},
+ utils::uuid_v7,
+ },
};
use super::SqliteStore;
diff --git a/crates/turtle/src/atuin_client/record/store.rs b/crates/turtle/src/atuin_client/record/store.rs
deleted file mode 100644
index db832a0d..00000000
--- a/crates/turtle/src/atuin_client/record/store.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-use async_trait::async_trait;
-use eyre::Result;
-
-use crate::atuin_common::record::{EncryptedData, HostId, Record, RecordId, RecordIdx, RecordStatus};
-
-/// A record store stores records
-/// In more detail - we tend to need to process this into _another_ format to actually query it.
-/// As is, the record store is intended as the source of truth for arbitrary data, which could
-/// be shell history, kvs, etc.
-#[async_trait]
-pub(crate) trait Store {
- // Push a record
- async fn push(&self, record: &Record<EncryptedData>) -> Result<()> {
- self.push_batch(std::iter::once(record)).await
- }
-
- // Push a batch of records, all in one transaction
- async fn push_batch(
- &self,
- records: impl Iterator<Item = &Record<EncryptedData>> + Send + Sync,
- ) -> Result<()>;
-
- async fn get(&self, id: RecordId) -> Result<Record<EncryptedData>>;
-
- async fn delete(&self, id: RecordId) -> Result<()>;
- async fn delete_all(&self) -> Result<()>;
-
- async fn len_all(&self) -> Result<u64>;
- async fn len(&self, host: HostId, tag: &str) -> Result<u64>;
- async fn len_tag(&self, tag: &str) -> Result<u64>;
-
- async fn last(&self, host: HostId, tag: &str) -> Result<Option<Record<EncryptedData>>>;
- async fn first(&self, host: HostId, tag: &str) -> Result<Option<Record<EncryptedData>>>;
-
- async fn re_encrypt(&self, old_key: &[u8; 32], new_key: &[u8; 32]) -> Result<()>;
- async fn verify(&self, key: &[u8; 32]) -> Result<()>;
- async fn purge(&self, key: &[u8; 32]) -> Result<()>;
-
- /// Get the next `limit` records, after and including the given index
- async fn next(
- &self,
- host: HostId,
- tag: &str,
- idx: RecordIdx,
- limit: u64,
- ) -> Result<Vec<Record<EncryptedData>>>;
-
- /// Get the first record for a given host and tag
- async fn idx(
- &self,
- host: HostId,
- tag: &str,
- idx: RecordIdx,
- ) -> Result<Option<Record<EncryptedData>>>;
-
- async fn status(&self) -> Result<RecordStatus>;
-
- /// Get all records for a given tag
- async fn all_tagged(&self, tag: &str) -> Result<Vec<Record<EncryptedData>>>;
-}
diff --git a/crates/turtle/src/atuin_client/record/sync.rs b/crates/turtle/src/atuin_client/record/sync.rs
index 9a7abfba..a86fc7a9 100644
--- a/crates/turtle/src/atuin_client/record/sync.rs
+++ b/crates/turtle/src/atuin_client/record/sync.rs
@@ -5,7 +5,8 @@ use eyre::{OptionExt, Result};
use thiserror::Error;
use tracing::error;
-use super::{encryption::PASETO_V4, store::Store};
+use super::encryption::PASETO_V4;
+use crate::atuin_client::record::sqlite_store::SqliteStore;
use crate::atuin_client::{api_client::Client, settings::Settings};
use crate::atuin_common::record::{Diff, HostId, RecordId, RecordIdx, RecordStatus};
@@ -58,7 +59,7 @@ pub(crate) enum Operation {
},
}
-pub(crate) async fn build_client(settings: &Settings) -> Result<Client<'_>, SyncError> {
+pub(crate) fn build_client(settings: &Settings) -> Result<Client<'_>, SyncError> {
Client::new(
&settings.sync.address,
settings.network_connect_timeout,
@@ -75,7 +76,7 @@ pub(crate) async fn build_client(settings: &Settings) -> Result<Client<'_>, Sync
pub(crate) async fn diff(
client: &Client<'_>,
- store: &impl Store,
+ store: &SqliteStore,
) -> Result<(Vec<Diff>, RecordStatus), SyncError> {
let local_index = store
.status()
@@ -96,9 +97,9 @@ pub(crate) async fn diff(
// With the store as context, we can determine if a tail exists locally or not and therefore if it needs uploading or download.
// In theory this could be done as a part of the diffing stage, but it's easier to reason
// about and test this way
-pub(crate) async fn operations(
+pub(crate) fn operations(
diffs: Vec<Diff>,
- _store: &impl Store,
+ _store: &SqliteStore,
) -> Result<Vec<Operation>, SyncError> {
let mut operations = Vec::with_capacity(diffs.len());
@@ -170,7 +171,7 @@ pub(crate) async fn operations(
}
async fn sync_upload(
- store: &impl Store,
+ store: &SqliteStore,
client: &Client<'_>,
host: HostId,
tag: String,
@@ -229,7 +230,7 @@ async fn sync_upload(
}
async fn sync_download(
- store: &impl Store,
+ store: &SqliteStore,
client: &Client<'_>,
host: HostId,
tag: String,
@@ -288,7 +289,7 @@ async fn sync_download(
pub(crate) async fn sync_remote(
client: &Client<'_>,
operations: Vec<Operation>,
- local_store: &impl Store,
+ local_store: &SqliteStore,
page_size: u64,
) -> Result<(i64, Vec<RecordId>), SyncError> {
let mut uploaded = 0;
@@ -304,7 +305,7 @@ pub(crate) async fn sync_remote(
remote,
} => {
uploaded +=
- sync_upload(local_store, client, host, tag, local, remote, page_size).await?
+ sync_upload(local_store, client, host, tag, local, remote, page_size).await?;
}
Operation::Download {
@@ -315,7 +316,7 @@ pub(crate) async fn sync_remote(
} => {
let mut d =
sync_download(local_store, client, host, tag, local, remote, page_size).await?;
- downloaded.append(&mut d)
+ downloaded.append(&mut d);
}
Operation::Noop { .. } => continue,
@@ -358,16 +359,16 @@ pub(crate) async fn check_encryption_key(
pub(crate) async fn sync(
settings: &Settings,
- store: &impl Store,
+ store: &SqliteStore,
encryption_key: &[u8; 32],
) -> Result<(i64, Vec<RecordId>), SyncError> {
- let client = build_client(settings).await?;
+ let client = build_client(settings)?;
let (diff, remote_index) = diff(&client, store).await?;
// Bail before mutating either side if the local key can't read the remote.
check_encryption_key(&client, &remote_index, encryption_key).await?;
- let operations = operations(diff, store).await?;
+ let operations = operations(diff, store)?;
let (uploaded, downloaded) = sync_remote(&client, operations, store, 100).await?;
Ok((uploaded, downloaded))
@@ -382,7 +383,6 @@ mod tests {
record::{
encryption::PASETO_V4,
sqlite_store::SqliteStore,
- store::Store,
sync::{self, Operation},
},
settings::test_local_timeout,
diff --git a/crates/turtle/src/atuin_client/settings.rs b/crates/turtle/src/atuin_client/settings.rs
index d84e2eb0..c966ba67 100644
--- a/crates/turtle/src/atuin_client/settings.rs
+++ b/crates/turtle/src/atuin_client/settings.rs
@@ -14,7 +14,6 @@ use config::{
};
use eyre::{Context, Error, Result, bail, eyre};
use fs_err::{File, create_dir_all};
-use humantime::parse_duration;
use regex::RegexSet;
use serde::{Deserialize, Serialize};
use serde_with::DeserializeFromStr;
@@ -222,7 +221,6 @@ pub(crate) enum KeymapMode {
Auto,
}
-
// We want to translate the config to crossterm::cursor::SetCursorStyle, but
// the original type does not implement trait serde::Deserialize unfortunately.
// It seems impossible to implement Deserialize for external types when it is
@@ -252,7 +250,6 @@ pub(crate) enum CursorStyle {
SteadyBar,
}
-
#[derive(Clone, Debug, Deserialize, Serialize)]
pub(crate) struct Stats {
#[serde(default = "Stats::common_prefix_default")]
@@ -336,17 +333,6 @@ impl Keys {
prefix: "a".to_string(),
}
}
-
- /// Returns true if any value differs from the standard defaults.
- pub(crate) fn has_non_default_values(&self) -> bool {
- let d = Self::standard_defaults();
- self.scroll_exits != d.scroll_exits
- || self.exit_past_line_start != d.exit_past_line_start
- || self.accept_past_line_end != d.accept_past_line_end
- || self.accept_past_line_start != d.accept_past_line_start
- || self.accept_with_backspace != d.accept_with_backspace
- || self.prefix != d.prefix
- }
}
/// A single rule within a conditional keybinding config.
@@ -403,24 +389,7 @@ pub(crate) struct Preview {
}
#[derive(Clone, Debug, Deserialize, Serialize)]
-pub(crate) struct Theme {
- /// Name of desired theme ("default" for base)
- pub(crate) name: String,
-
- /// Whether any available additional theme debug should be shown
- pub(crate) debug: Option<bool>,
-
- /// How many levels of parenthood will be traversed if needed
- pub(crate) max_depth: Option<u8>,
-}
-
-#[derive(Clone, Debug, Deserialize, Serialize)]
pub(crate) struct Daemon {
- /// Use the daemon to sync
- /// If enabled, history hooks are routed through the daemon.
- #[serde(alias = "enable")]
- pub(crate) enabled: bool,
-
/// Automatically start and manage a local daemon when needed.
pub(crate) autostart: bool,
@@ -534,24 +503,13 @@ impl Default for Preview {
}
}
-impl Default for Theme {
- fn default() -> Self {
- Self {
- name: "".to_string(),
- debug: None::<bool>,
- max_depth: Some(10),
- }
- }
-}
-
impl Default for Daemon {
fn default() -> Self {
Self {
- enabled: false,
autostart: false,
sync_frequency: 300,
- socket_path: "".to_string(),
- pidfile_path: "".to_string(),
+ socket_path: String::new(),
+ pidfile_path: String::new(),
systemd_socket: false,
tcp_port: 8889,
}
@@ -562,7 +520,7 @@ impl Default for Logs {
fn default() -> Self {
Self {
enabled: true,
- dir: "".to_string(),
+ dir: String::new(),
level: LogLevel::default(),
retention: Self::default_retention(),
search: LogConfig {
@@ -621,18 +579,6 @@ impl Logs {
pub(crate) fn daemon_retention(&self) -> u64 {
self.daemon.retention.unwrap_or(self.retention)
}
-
- /// Returns the full path for the search log file.
- pub(crate) fn search_path(&self) -> PathBuf {
- let path = PathBuf::from(&self.search.file);
- PathBuf::from(&self.dir).join(path)
- }
-
- /// Returns the full path for the daemon log file.
- pub(crate) fn daemon_path(&self) -> PathBuf {
- let path = PathBuf::from(&self.daemon.file);
- PathBuf::from(&self.dir).join(path)
- }
}
impl Default for Search {
@@ -902,24 +848,6 @@ impl Sync {
.map(decode_key)
.transpose()
}
-
- pub(crate) async fn should_sync(&self) -> Result<bool> {
- if !self.auto || !self.have_sync_user()? {
- return Ok(false);
- }
-
- if self.frequency == "0" || self.frequency.is_empty() {
- return Ok(true);
- }
-
- match parse_duration(self.frequency.as_str()) {
- Ok(d) => {
- let d = time::Duration::try_from(d)?;
- Ok(OffsetDateTime::now_utc() - Settings::last_sync().await? >= d)
- }
- Err(e) => Err(eyre!("failed to check sync: {}", e)),
- }
- }
}
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -998,9 +926,6 @@ pub(crate) struct Settings {
pub(crate) search: Search,
#[serde(default)]
- pub(crate) theme: Theme,
-
- #[serde(default)]
pub(crate) ui: Ui,
#[serde(default)]
@@ -1126,7 +1051,6 @@ impl Settings {
.set_default("command_chaining", false)?
.set_default("store_failed", true)?
.set_default("daemon.sync_frequency", 300)?
- .set_default("daemon.enabled", false)?
.set_default("daemon.autostart", false)?
.set_default("daemon.socket_path", socket_path.to_str())?
.set_default("daemon.pidfile_path", pidfile_path.to_str())?
@@ -1553,15 +1477,6 @@ mod tests {
}
#[test]
- fn effective_data_dir_returns_default_when_not_set() {
- let effective = super::Settings::effective_data_dir();
- let default = crate::atuin_common::utils::data_dir();
-
- assert!(effective.to_str().is_some());
- assert!(effective.ends_with("atuin") || effective == default);
- }
-
- #[test]
fn keymap_config_deserializes_simple_binding() {
let json = r#"{"emacs": {"ctrl-c": "exit"}}"#;
let config: super::KeymapConfig = serde_json::from_str(json).unwrap();
diff --git a/crates/turtle/src/atuin_client/settings/watcher.rs b/crates/turtle/src/atuin_client/settings/watcher.rs
index 20082639..e280480c 100644
--- a/crates/turtle/src/atuin_client/settings/watcher.rs
+++ b/crates/turtle/src/atuin_client/settings/watcher.rs
@@ -22,7 +22,7 @@
//! ```
use std::{
- path::PathBuf,
+ path::{Path, PathBuf},
sync::{Arc, OnceLock},
time::Duration,
};
@@ -74,9 +74,9 @@ impl SettingsWatcher {
let (tx, rx) = watch::channel(initial_settings);
let config_path = Self::config_path();
- info!("starting config file watcher: {:?}", config_path);
+ info!("starting config file watcher: {}", config_path.display());
- let watcher = Self::create_watcher(tx, config_path)?;
+ let watcher = Self::create_watcher(tx, &config_path)?;
Ok(Self {
rx,
@@ -93,37 +93,29 @@ impl SettingsWatcher {
self.rx.clone()
}
- /// Get the current settings without subscribing to updates.
- pub(crate) fn current(&self) -> Arc<Settings> {
- self.rx.borrow().clone()
- }
-
/// Get the config file path.
fn config_path() -> PathBuf {
- let config_dir = if let Ok(p) = std::env::var("ATUIN_CONFIG_DIR") {
- PathBuf::from(p)
- } else {
- crate::atuin_common::utils::config_dir()
- };
+ let config_dir = std::env::var("ATUIN_CONFIG_DIR")
+ .map_or_else(|_| crate::atuin_common::utils::config_dir(), PathBuf::from);
config_dir.join("config.toml")
}
/// Create the file watcher with debouncing.
fn create_watcher(
tx: watch::Sender<Arc<Settings>>,
- config_path: PathBuf,
+ config_path: &Path,
) -> Result<RecommendedWatcher> {
// Channel for debouncing file events
let (debounce_tx, debounce_rx) = std::sync::mpsc::channel::<()>();
// Spawn debounce thread
- let config_path_clone = config_path.clone();
+ let config_path_clone = config_path.to_owned();
std::thread::spawn(move || {
- Self::debounce_loop(debounce_rx, tx, config_path_clone);
+ Self::debounce_loop(&debounce_rx, &tx, &config_path_clone);
});
// Clone config_path for use in the watcher callback
- let config_path_for_watcher = config_path.clone();
+ let config_path_for_watcher = config_path.to_owned();
// Canonicalize config path for reliable comparison on macOS
// (handles symlinks like /var -> /private/var)
@@ -169,13 +161,13 @@ impl SettingsWatcher {
EventKind::Modify(ModifyKind::Data(_) | ModifyKind::Any)
| EventKind::Create(_)
) {
- debug!("config file event detected: {:?}", event);
+ debug!("config file event detected: {event:?}");
// Send to debounce channel (ignore send errors - receiver might be gone)
let _ = debounce_tx.send(());
}
}
Err(e) => {
- error!("file watcher error: {}", e);
+ error!("file watcher error: {e}");
}
}
},
@@ -184,31 +176,40 @@ impl SettingsWatcher {
.wrap_err("failed to create file watcher")?;
// Watch the config file's parent directory (some editors create new files)
- let watch_path = config_path.parent().unwrap_or(&config_path);
+ let watch_path = config_path.parent().unwrap_or(config_path);
// Defensive: ensure watch path exists before trying to watch
if !watch_path.exists() {
warn!(
- "config directory does not exist, creating it: {:?}",
- watch_path
+ "config directory does not exist, creating it: {}",
+ watch_path.display()
);
- std::fs::create_dir_all(watch_path)
- .wrap_err_with(|| format!("failed to create config directory: {:?}", watch_path))?;
+ std::fs::create_dir_all(watch_path).wrap_err_with(|| {
+ format!(
+ "failed to create config directory: {}",
+ watch_path.display()
+ )
+ })?;
}
watcher
.watch(watch_path, RecursiveMode::NonRecursive)
- .wrap_err_with(|| format!("failed to watch config directory: {:?}", watch_path))?;
+ .wrap_err_with(|| {
+ format!("failed to watch config directory: {}", watch_path.display())
+ })?;
- info!("config file watcher initialized for: {:?}", watch_path);
+ info!(
+ "config file watcher initialized for: {}",
+ watch_path.display()
+ );
Ok(watcher)
}
/// Debounce loop that batches file events and reloads settings.
fn debounce_loop(
- rx: std::sync::mpsc::Receiver<()>,
- tx: watch::Sender<Arc<Settings>>,
- config_path: PathBuf,
+ rx: &std::sync::mpsc::Receiver<()>,
+ tx: &watch::Sender<Arc<Settings>>,
+ config_path: &Path,
) {
const DEBOUNCE_DURATION: Duration = Duration::from_millis(500);
@@ -229,14 +230,17 @@ impl SettingsWatcher {
// (handles case where file was deleted - we'll get notified when it's recreated)
if !config_path.exists() {
debug!(
- "config file does not exist, skipping reload: {:?}",
- config_path
+ "config file does not exist, skipping reload: {}",
+ config_path.display()
);
continue;
}
// Now reload settings
- info!("config file changed, reloading settings: {:?}", config_path);
+ info!(
+ "config file changed, reloading settings: {}",
+ config_path.display()
+ );
match Settings::new() {
Ok(settings) => {
if tx.send(Arc::new(settings)).is_err() {
@@ -247,7 +251,7 @@ impl SettingsWatcher {
info!("settings reloaded successfully");
}
Err(e) => {
- warn!("failed to reload settings: {}", e);
+ warn!("failed to reload settings: {e}");
// Keep the old settings, don't broadcast the error
}
}
diff --git a/crates/turtle/src/atuin_client/theme.rs b/crates/turtle/src/atuin_client/theme.rs
index 21bbe07c..ec0538e9 100644
--- a/crates/turtle/src/atuin_client/theme.rs
+++ b/crates/turtle/src/atuin_client/theme.rs
@@ -1,831 +1,41 @@
-use config::{Config, File as ConfigFile, FileFormat};
-use log;
-use palette::named;
-use serde::{Deserialize, Serialize};
-use serde_json;
-use std::collections::HashMap;
-use std::error;
-use std::io::{Error, ErrorKind};
-use std::path::PathBuf;
-use std::sync::LazyLock;
-use strum_macros;
-
-static DEFAULT_MAX_DEPTH: u8 = 10;
-
-// Collection of settable "meanings" that can have colors set.
-// NOTE: You can add a new meaning here without breaking backwards compatibility but please:
-// - update the atuin/docs repository, which has a list of available meanings
-// - add a fallback in the MEANING_FALLBACKS below, so that themes which do not have it
-// get a sensible fallback (see Title as an example)
-#[derive(
- Serialize, Deserialize, Copy, Clone, Hash, Debug, Eq, PartialEq, strum_macros::Display,
-)]
-#[strum(serialize_all = "camel_case")]
-pub(crate) enum Meaning {
- AlertInfo,
- AlertWarn,
- AlertError,
- Annotation,
- Base,
- Guidance,
- Important,
- Title,
- Muted,
-}
-
-#[derive(Clone, Debug, Deserialize, Serialize)]
-pub(crate) struct ThemeConfig {
- // Definition of the theme
- pub(crate) theme: ThemeDefinitionConfigBlock,
-
- // Colors
- pub(crate) colors: HashMap<Meaning, String>,
-}
-
-#[derive(Clone, Debug, Deserialize, Serialize)]
-pub(crate) struct ThemeDefinitionConfigBlock {
- /// Name of theme ("default" for base)
- pub(crate) name: String,
-
- /// Whether any theme should be treated as a parent _if available_
- pub(crate) parent: Option<String>,
-}
-
use crossterm::style::{Attribute, Attributes, Color, ContentStyle};
-
-// For now, a theme is loaded as a mapping of meanings to colors, but it may be desirable to
-// expand that in the future to general styles, so we populate a Meaning->ContentStyle hashmap.
-pub(crate) struct Theme {
- pub(crate) name: String,
- pub(crate) parent: Option<String>,
- pub(crate) styles: HashMap<Meaning, ContentStyle>,
+pub(crate) fn style_base() -> ContentStyle {
+ ContentStyle::default()
}
-
-// Themes have a number of convenience functions for the most commonly used meanings.
-// The general purpose `as_style` routine gives back a style, but for ease-of-use and to keep
-// theme-related boilerplate minimal, the convenience functions give a color.
-impl Theme {
- // This is the base "default" color, for general text
- pub(crate) fn get_base(&self) -> ContentStyle {
- self.styles[&Meaning::Base]
- }
-
- pub(crate) fn get_info(&self) -> ContentStyle {
- self.get_alert(log::Level::Info)
- }
-
- pub(crate) fn get_warning(&self) -> ContentStyle {
- self.get_alert(log::Level::Warn)
- }
-
- pub(crate) fn get_error(&self) -> ContentStyle {
- self.get_alert(log::Level::Error)
- }
-
- // The alert meanings may be chosen by the Level enum, rather than the methods above
- // or the full Meaning enum, to simplify programmatic selection of a log-level.
- pub(crate) fn get_alert(&self, severity: log::Level) -> ContentStyle {
- self.styles[ALERT_TYPES.get(&severity).unwrap()]
- }
-
- pub(crate) fn new(
- name: String,
- parent: Option<String>,
- styles: HashMap<Meaning, ContentStyle>,
- ) -> Theme {
- Theme {
- name,
- parent,
- styles,
- }
- }
-
- pub(crate) fn closest_meaning<'a>(&self, meaning: &'a Meaning) -> &'a Meaning {
- if self.styles.contains_key(meaning) {
- meaning
- } else if MEANING_FALLBACKS.contains_key(meaning) {
- self.closest_meaning(&MEANING_FALLBACKS[meaning])
- } else {
- &Meaning::Base
- }
- }
-
- // General access - if you have a meaning, this will give you a (crossterm) style
- pub(crate) fn as_style(&self, meaning: Meaning) -> ContentStyle {
- self.styles[self.closest_meaning(&meaning)]
- }
-
- // Turns a map of meanings to colornames into a theme
- // If theme-debug is on, then we will print any colornames that we cannot load,
- // but we do not have this on in general, as it could print unfiltered text to the terminal
- // from a theme TOML file. However, it will always return a theme, falling back to
- // defaults on error, so that a TOML file does not break loading
- pub(crate) fn from_foreground_colors(
- name: String,
- parent: Option<&Theme>,
- foreground_colors: HashMap<Meaning, String>,
- debug: bool,
- ) -> Theme {
- let styles: HashMap<Meaning, ContentStyle> = foreground_colors
- .iter()
- .map(|(name, color)| {
- (
- *name,
- StyleFactory::from_fg_string(color).unwrap_or_else(|err| {
- if debug {
- log::warn!("Tried to load string as a color unsuccessfully: ({name}={color}) {err}");
- }
- ContentStyle::default()
- }),
- )
- })
- .collect();
- Theme::from_map(name, parent, &styles)
- }
-
- // Boil down a meaning-color hashmap into a theme, by taking the defaults
- // for any unknown colors
- fn from_map(
- name: String,
- parent: Option<&Theme>,
- overrides: &HashMap<Meaning, ContentStyle>,
- ) -> Theme {
- let styles = match parent {
- Some(theme) => Box::new(theme.styles.clone()),
- None => Box::new(DEFAULT_THEME.styles.clone()),
- }
- .iter()
- .map(|(name, color)| match overrides.get(name) {
- Some(value) => (*name, *value),
- None => (*name, *color),
- })
- .collect();
- Theme::new(name, parent.map(|p| p.name.clone()), styles)
+pub(crate) fn style_annotation() -> ContentStyle {
+ ContentStyle {
+ foreground_color: Some(Color::DarkGrey),
+ ..ContentStyle::default()
}
}
-
-// Use palette to get a color from a string name, if possible
-fn from_string(name: &str) -> Result<Color, String> {
- if name.is_empty() {
- return Err("Empty string".into());
- }
- let first_char = name.chars().next().unwrap();
- match first_char {
- '#' => {
- let hexcode = &name[1..];
- let vec: Vec<u8> = hexcode
- .chars()
- .collect::<Vec<char>>()
- .chunks(2)
- .map(|pair| u8::from_str_radix(pair.iter().collect::<String>().as_str(), 16))
- .filter_map(|n| n.ok())
- .collect();
- if vec.len() != 3 {
- return Err("Could not parse 3 hex values from string".into());
- }
- Ok(Color::Rgb {
- r: vec[0],
- g: vec[1],
- b: vec[2],
- })
- }
- '@' => {
- // For full flexibility, we need to use serde_json, given
- // crossterm's approach.
- serde_json::from_str::<Color>(format!("\"{}\"", &name[1..]).as_str())
- .map_err(|_| format!("Could not convert color name {name} to Crossterm color"))
- }
- _ => {
- let srgb = named::from_str(name).ok_or("No such color in palette")?;
- Ok(Color::Rgb {
- r: srgb.red,
- g: srgb.green,
- b: srgb.blue,
- })
- }
+pub(crate) fn style_important() -> ContentStyle {
+ ContentStyle {
+ foreground_color: Some(Color::White),
+ attributes: Attributes::from(Attribute::Bold),
+ ..ContentStyle::default()
}
}
-
-pub(crate) struct StyleFactory {}
-
-impl StyleFactory {
- fn from_fg_string(name: &str) -> Result<ContentStyle, String> {
- match from_string(name) {
- Ok(color) => Ok(Self::from_fg_color(color)),
- Err(err) => Err(err),
- }
- }
-
- // For succinctness, if we are confident that the name will be known,
- // this routine is available to keep the code readable
- fn known_fg_string(name: &str) -> ContentStyle {
- Self::from_fg_string(name).unwrap()
- }
-
- fn from_fg_color(color: Color) -> ContentStyle {
- ContentStyle {
- foreground_color: Some(color),
- ..ContentStyle::default()
- }
- }
-
- fn from_fg_color_and_attributes(color: Color, attributes: Attributes) -> ContentStyle {
- ContentStyle {
- foreground_color: Some(color),
- attributes,
- ..ContentStyle::default()
- }
+pub(crate) fn style_guidance() -> ContentStyle {
+ ContentStyle {
+ foreground_color: Some(Color::DarkBlue),
+ ..ContentStyle::default()
}
}
-
-// Built-in themes. Rather than having extra files added before any theming
-// is available, this gives a couple of basic options, demonstrating the use
-// of themes: autumn and marine
-static ALERT_TYPES: LazyLock<HashMap<log::Level, Meaning>> = LazyLock::new(|| {
- HashMap::from([
- (log::Level::Info, Meaning::AlertInfo),
- (log::Level::Warn, Meaning::AlertWarn),
- (log::Level::Error, Meaning::AlertError),
- ])
-});
-
-static MEANING_FALLBACKS: LazyLock<HashMap<Meaning, Meaning>> = LazyLock::new(|| {
- HashMap::from([
- (Meaning::Guidance, Meaning::AlertInfo),
- (Meaning::Annotation, Meaning::AlertInfo),
- (Meaning::Title, Meaning::Important),
- ])
-});
-
-static DEFAULT_THEME: LazyLock<Theme> = LazyLock::new(|| {
- Theme::new(
- "default".to_string(),
- None,
- HashMap::from([
- (
- Meaning::AlertError,
- StyleFactory::from_fg_color(Color::DarkRed),
- ),
- (
- Meaning::AlertWarn,
- StyleFactory::from_fg_color(Color::DarkYellow),
- ),
- (
- Meaning::AlertInfo,
- StyleFactory::from_fg_color(Color::DarkGreen),
- ),
- (
- Meaning::Annotation,
- StyleFactory::from_fg_color(Color::DarkGrey),
- ),
- (
- Meaning::Guidance,
- StyleFactory::from_fg_color(Color::DarkBlue),
- ),
- (
- Meaning::Important,
- StyleFactory::from_fg_color_and_attributes(
- Color::White,
- Attributes::from(Attribute::Bold),
- ),
- ),
- (Meaning::Muted, StyleFactory::from_fg_color(Color::Grey)),
- (Meaning::Base, ContentStyle::default()),
- ]),
- )
-});
-
-static BUILTIN_THEMES: LazyLock<HashMap<&'static str, Theme>> = LazyLock::new(|| {
- HashMap::from([
- ("default", HashMap::new()),
- (
- "(none)",
- HashMap::from([
- (Meaning::AlertError, ContentStyle::default()),
- (Meaning::AlertWarn, ContentStyle::default()),
- (Meaning::AlertInfo, ContentStyle::default()),
- (Meaning::Annotation, ContentStyle::default()),
- (Meaning::Guidance, ContentStyle::default()),
- (Meaning::Important, ContentStyle::default()),
- (Meaning::Muted, ContentStyle::default()),
- (Meaning::Base, ContentStyle::default()),
- ]),
- ),
- (
- "autumn",
- HashMap::from([
- (
- Meaning::AlertError,
- StyleFactory::known_fg_string("saddlebrown"),
- ),
- (
- Meaning::AlertWarn,
- StyleFactory::known_fg_string("darkorange"),
- ),
- (Meaning::AlertInfo, StyleFactory::known_fg_string("gold")),
- (
- Meaning::Annotation,
- StyleFactory::from_fg_color(Color::DarkGrey),
- ),
- (Meaning::Guidance, StyleFactory::known_fg_string("brown")),
- ]),
- ),
- (
- "marine",
- HashMap::from([
- (
- Meaning::AlertError,
- StyleFactory::known_fg_string("yellowgreen"),
- ),
- (Meaning::AlertWarn, StyleFactory::known_fg_string("cyan")),
- (
- Meaning::AlertInfo,
- StyleFactory::known_fg_string("turquoise"),
- ),
- (
- Meaning::Annotation,
- StyleFactory::known_fg_string("steelblue"),
- ),
- (
- Meaning::Base,
- StyleFactory::known_fg_string("lightsteelblue"),
- ),
- (Meaning::Guidance, StyleFactory::known_fg_string("teal")),
- ]),
- ),
- ])
- .iter()
- .map(|(name, theme)| (*name, Theme::from_map(name.to_string(), None, theme)))
- .collect()
-});
-
-// To avoid themes being repeatedly loaded, we store them in a theme manager
-pub(crate) struct ThemeManager {
- loaded_themes: HashMap<String, Theme>,
- debug: bool,
- override_theme_dir: Option<String>,
-}
-
-// Theme-loading logic
-impl ThemeManager {
- pub(crate) fn new(debug: Option<bool>, theme_dir: Option<String>) -> Self {
- Self {
- loaded_themes: HashMap::new(),
- debug: debug.unwrap_or(false),
- override_theme_dir: match theme_dir {
- Some(theme_dir) => Some(theme_dir),
- None => std::env::var("ATUIN_THEME_DIR").ok(),
- },
- }
- }
-
- // Try to load a theme from a `{name}.toml` file in the theme directory. If an override is set
- // for the theme dir (via ATUIN_THEME_DIR env) we should load the theme from there
- pub(crate) fn load_theme_from_file(
- &mut self,
- name: &str,
- max_depth: u8,
- ) -> Result<&Theme, Box<dyn error::Error>> {
- let mut theme_file = if let Some(p) = &self.override_theme_dir {
- if p.is_empty() {
- return Err(Box::new(Error::new(
- ErrorKind::NotFound,
- "Empty theme directory override and could not find theme elsewhere",
- )));
- }
- PathBuf::from(p)
- } else {
- let config_dir = crate::atuin_common::utils::config_dir();
- let mut theme_file = if let Ok(p) = std::env::var("ATUIN_CONFIG_DIR") {
- PathBuf::from(p)
- } else {
- let mut theme_file = PathBuf::new();
- theme_file.push(config_dir);
- theme_file
- };
- theme_file.push("themes");
- theme_file
- };
-
- let theme_toml = format!("{name}.toml");
- theme_file.push(theme_toml);
-
- let mut config_builder = Config::builder();
-
- config_builder = config_builder.add_source(ConfigFile::new(
- theme_file.to_str().unwrap(),
- FileFormat::Toml,
- ));
-
- let config = config_builder.build()?;
- self.load_theme_from_config(name, config, max_depth)
- }
-
- pub(crate) fn load_theme_from_config(
- &mut self,
- name: &str,
- config: Config,
- max_depth: u8,
- ) -> Result<&Theme, Box<dyn error::Error>> {
- let debug = self.debug;
- let theme_config: ThemeConfig = match config.try_deserialize() {
- Ok(tc) => tc,
- Err(e) => {
- return Err(Box::new(Error::new(
- ErrorKind::InvalidInput,
- format!(
- "Failed to deserialize theme: {}",
- if debug {
- e.to_string()
- } else {
- "set theme debug on for more info".to_string()
- }
- ),
- )));
- }
- };
- let colors: HashMap<Meaning, String> = theme_config.colors;
- let parent: Option<&Theme> = match theme_config.theme.parent {
- Some(parent_name) => {
- if max_depth == 0 {
- return Err(Box::new(Error::new(
- ErrorKind::InvalidInput,
- "Parent requested but we hit the recursion limit",
- )));
- }
- Some(self.load_theme(parent_name.as_str(), Some(max_depth - 1)))
- }
- None => Some(self.load_theme("default", Some(max_depth - 1))),
- };
-
- if debug && name != theme_config.theme.name {
- log::warn!(
- "Your theme config name is not the name of your loaded theme {} != {}",
- name,
- theme_config.theme.name
- );
- }
-
- let theme = Theme::from_foreground_colors(theme_config.theme.name, parent, colors, debug);
- let name = name.to_string();
- self.loaded_themes.insert(name.clone(), theme);
- let theme = self.loaded_themes.get(&name).unwrap();
- Ok(theme)
- }
-
- // Check if the requested theme is loaded and, if not, then attempt to get it
- // from the builtins or, if not there, from file
- pub(crate) fn load_theme(&mut self, name: &str, max_depth: Option<u8>) -> &Theme {
- if self.loaded_themes.contains_key(name) {
- return self.loaded_themes.get(name).unwrap();
- }
- let built_ins = &BUILTIN_THEMES;
- match built_ins.get(name) {
- Some(theme) => theme,
- None => match self.load_theme_from_file(name, max_depth.unwrap_or(DEFAULT_MAX_DEPTH)) {
- Ok(theme) => theme,
- Err(err) => {
- log::warn!("Could not load theme {name}: {err}");
- built_ins.get("(none)").unwrap()
- }
- },
- }
+pub(crate) fn style_alerterror() -> ContentStyle {
+ ContentStyle {
+ foreground_color: Some(Color::DarkRed),
+ ..ContentStyle::default()
}
}
-
-#[cfg(test)]
-mod theme_tests {
- use super::*;
-
- #[test]
- fn test_can_load_builtin_theme() {
- let mut manager = ThemeManager::new(Some(false), Some("".to_string()));
- let theme = manager.load_theme("autumn", None);
- assert_eq!(
- theme.as_style(Meaning::Guidance).foreground_color,
- from_string("brown").ok()
- );
- }
-
- #[test]
- fn test_can_create_theme() {
- let mut manager = ThemeManager::new(Some(false), Some("".to_string()));
- let mytheme = Theme::new(
- "mytheme".to_string(),
- None,
- HashMap::from([(
- Meaning::AlertError,
- StyleFactory::known_fg_string("yellowgreen"),
- )]),
- );
- manager.loaded_themes.insert("mytheme".to_string(), mytheme);
- let theme = manager.load_theme("mytheme", None);
- assert_eq!(
- theme.as_style(Meaning::AlertError).foreground_color,
- from_string("yellowgreen").ok()
- );
+pub(crate) fn style_alertinfo() -> ContentStyle {
+ ContentStyle {
+ foreground_color: Some(Color::DarkGreen),
+ ..ContentStyle::default()
}
-
- #[test]
- fn test_can_fallback_when_meaning_missing() {
- let mut manager = ThemeManager::new(Some(false), Some("".to_string()));
-
- // We use title as an example of a meaning that is not defined
- // even in the base theme.
- assert!(!DEFAULT_THEME.styles.contains_key(&Meaning::Title));
-
- let config = Config::builder()
- .add_source(ConfigFile::from_str(
- "
- [theme]
- name = \"title_theme\"
-
- [colors]
- Guidance = \"white\"
- AlertInfo = \"zomp\"
- ",
- FileFormat::Toml,
- ))
- .build()
- .unwrap();
- let theme = manager
- .load_theme_from_config("config_theme", config, 1)
- .unwrap();
-
- // Correctly picks overridden color.
- assert_eq!(
- theme.as_style(Meaning::Guidance).foreground_color,
- from_string("white").ok()
- );
-
- // Does not fall back to any color.
- assert_eq!(theme.as_style(Meaning::AlertInfo).foreground_color, None);
-
- // Even for the base.
- assert_eq!(theme.as_style(Meaning::Base).foreground_color, None);
-
- // Falls back to red as meaning missing from theme, so picks base default.
- assert_eq!(
- theme.as_style(Meaning::AlertError).foreground_color,
- Some(Color::DarkRed)
- );
-
- // Falls back to Important as Title not available.
- assert_eq!(
- theme.as_style(Meaning::Title).foreground_color,
- theme.as_style(Meaning::Important).foreground_color,
- );
-
- let title_config = Config::builder()
- .add_source(ConfigFile::from_str(
- "
- [theme]
- name = \"title_theme\"
-
- [colors]
- Title = \"white\"
- AlertInfo = \"zomp\"
- ",
- FileFormat::Toml,
- ))
- .build()
- .unwrap();
- let title_theme = manager
- .load_theme_from_config("title_theme", title_config, 1)
- .unwrap();
-
- assert_eq!(
- title_theme.as_style(Meaning::Title).foreground_color,
- Some(Color::White)
- );
- }
-
- #[test]
- fn test_no_fallbacks_are_circular() {
- let mytheme = Theme::new("mytheme".to_string(), None, HashMap::from([]));
- MEANING_FALLBACKS
- .iter()
- .for_each(|pair| assert_eq!(mytheme.closest_meaning(pair.0), &Meaning::Base))
- }
-
- #[test]
- fn test_can_get_colors_via_convenience_functions() {
- let mut manager = ThemeManager::new(Some(true), Some("".to_string()));
- let theme = manager.load_theme("default", None);
- assert_eq!(theme.get_error().foreground_color.unwrap(), Color::DarkRed);
- assert_eq!(
- theme.get_warning().foreground_color.unwrap(),
- Color::DarkYellow
- );
- assert_eq!(theme.get_info().foreground_color.unwrap(), Color::DarkGreen);
- assert_eq!(theme.get_base().foreground_color, None);
- assert_eq!(
- theme.get_alert(log::Level::Error).foreground_color.unwrap(),
- Color::DarkRed
- )
- }
-
- #[test]
- fn test_can_use_parent_theme_for_fallbacks() {
- testing_logger::setup();
-
- let mut manager = ThemeManager::new(Some(false), Some("".to_string()));
-
- // First, we introduce a base theme
- let solarized = Config::builder()
- .add_source(ConfigFile::from_str(
- "
- [theme]
- name = \"solarized\"
-
- [colors]
- Guidance = \"white\"
- AlertInfo = \"pink\"
- ",
- FileFormat::Toml,
- ))
- .build()
- .unwrap();
- let solarized_theme = manager
- .load_theme_from_config("solarized", solarized, 1)
- .unwrap();
-
- assert_eq!(
- solarized_theme
- .as_style(Meaning::AlertInfo)
- .foreground_color,
- from_string("pink").ok()
- );
-
- // Then we introduce a derived theme
- let unsolarized = Config::builder()
- .add_source(ConfigFile::from_str(
- "
- [theme]
- name = \"unsolarized\"
- parent = \"solarized\"
-
- [colors]
- AlertInfo = \"red\"
- ",
- FileFormat::Toml,
- ))
- .build()
- .unwrap();
- let unsolarized_theme = manager
- .load_theme_from_config("unsolarized", unsolarized, 1)
- .unwrap();
-
- // It will take its own values
- assert_eq!(
- unsolarized_theme
- .as_style(Meaning::AlertInfo)
- .foreground_color,
- from_string("red").ok()
- );
-
- // ...or fall back to the parent
- assert_eq!(
- unsolarized_theme
- .as_style(Meaning::Guidance)
- .foreground_color,
- from_string("white").ok()
- );
-
- testing_logger::validate(|captured_logs| assert_eq!(captured_logs.len(), 0));
-
- // If the parent is not found, we end up with the no theme colors or styling
- // as this is considered a (soft) error state.
- let nunsolarized = Config::builder()
- .add_source(ConfigFile::from_str(
- "
- [theme]
- name = \"nunsolarized\"
- parent = \"nonsolarized\"
-
- [colors]
- AlertInfo = \"red\"
- ",
- FileFormat::Toml,
- ))
- .build()
- .unwrap();
- let nunsolarized_theme = manager
- .load_theme_from_config("nunsolarized", nunsolarized, 1)
- .unwrap();
-
- assert_eq!(
- nunsolarized_theme
- .as_style(Meaning::Guidance)
- .foreground_color,
- None
- );
-
- testing_logger::validate(|captured_logs| {
- assert_eq!(captured_logs.len(), 1);
- assert_eq!(
- captured_logs[0].body,
- "Could not load theme nonsolarized: Empty theme directory override and could not find theme elsewhere"
- );
- assert_eq!(captured_logs[0].level, log::Level::Warn)
- });
- }
-
- #[test]
- fn test_can_debug_theme() {
- testing_logger::setup();
- [true, false].iter().for_each(|debug| {
- let mut manager = ThemeManager::new(Some(*debug), Some("".to_string()));
- let config = Config::builder()
- .add_source(ConfigFile::from_str(
- "
- [theme]
- name = \"mytheme\"
-
- [colors]
- Guidance = \"white\"
- AlertInfo = \"xinetic\"
- ",
- FileFormat::Toml,
- ))
- .build()
- .unwrap();
- manager
- .load_theme_from_config("config_theme", config, 1)
- .unwrap();
- testing_logger::validate(|captured_logs| {
- if *debug {
- assert_eq!(captured_logs.len(), 2);
- assert_eq!(
- captured_logs[0].body,
- "Your theme config name is not the name of your loaded theme config_theme != mytheme"
- );
- assert_eq!(captured_logs[0].level, log::Level::Warn);
- assert_eq!(
- captured_logs[1].body,
- "Tried to load string as a color unsuccessfully: (AlertInfo=xinetic) No such color in palette"
- );
- assert_eq!(captured_logs[1].level, log::Level::Warn)
- } else {
- assert_eq!(captured_logs.len(), 0)
- }
- })
- })
- }
-
- #[test]
- fn test_can_parse_color_strings_correctly() {
- assert_eq!(
- from_string("brown").unwrap(),
- Color::Rgb {
- r: 165,
- g: 42,
- b: 42
- }
- );
-
- assert_eq!(from_string(""), Err("Empty string".into()));
-
- ["manatee", "caput mortuum", "123456"]
- .iter()
- .for_each(|inp| {
- assert_eq!(from_string(inp), Err("No such color in palette".into()));
- });
-
- assert_eq!(
- from_string("#ff1122").unwrap(),
- Color::Rgb {
- r: 255,
- g: 17,
- b: 34
- }
- );
- ["#1122", "#ffaa112", "#brown"].iter().for_each(|inp| {
- assert_eq!(
- from_string(inp),
- Err("Could not parse 3 hex values from string".into())
- );
- });
-
- assert_eq!(from_string("@dark_grey").unwrap(), Color::DarkGrey);
- assert_eq!(
- from_string("@rgb_(255,255,255)").unwrap(),
- Color::Rgb {
- r: 255,
- g: 255,
- b: 255
- }
- );
- assert_eq!(from_string("@ansi_(255)").unwrap(), Color::AnsiValue(255));
- ["@", "@DarkGray", "@Dark 4ay", "@ansi(256)"]
- .iter()
- .for_each(|inp| {
- assert_eq!(
- from_string(inp),
- Err(format!(
- "Could not convert color name {inp} to Crossterm color"
- ))
- );
- });
+}
+pub(crate) fn style_alertwarn() -> ContentStyle {
+ ContentStyle {
+ foreground_color: Some(Color::DarkYellow),
+ ..ContentStyle::default()
}
}
diff --git a/crates/turtle/src/atuin_client/utils.rs b/crates/turtle/src/atuin_client/utils.rs
index 35d7db26..6d178b77 100644
--- a/crates/turtle/src/atuin_client/utils.rs
+++ b/crates/turtle/src/atuin_client/utils.rs
@@ -1,3 +1,4 @@
+
pub(crate) fn get_hostname() -> String {
std::env::var("ATUIN_HOST_NAME")
.unwrap_or_else(|_| whoami::hostname().unwrap_or_else(|_| "unknown-host".to_string()))
diff --git a/crates/turtle/src/atuin_common/record.rs b/crates/turtle/src/atuin_common/record.rs
index 9db9cf98..8a10804c 100644
--- a/crates/turtle/src/atuin_common/record.rs
+++ b/crates/turtle/src/atuin_common/record.rs
@@ -83,18 +83,6 @@ pub(crate) struct AdditionalData<'a> {
pub(crate) host: &'a HostId,
}
-impl<Data> Record<Data> {
- pub(crate) fn append(&self, data: Vec<u8>) -> Record<DecryptedData> {
- Record::builder()
- .host(self.host.clone())
- .version(self.version.clone())
- .idx(self.idx + 1)
- .tag(self.tag.clone())
- .data(DecryptedData(data))
- .build()
- }
-}
-
/// An index representing the current state of the record stores
/// This can be both remote, or local, and compared in either direction
#[derive(Debug, Serialize, Deserialize)]
@@ -125,16 +113,12 @@ impl RecordStatus {
}
/// Insert a new tail record into the store
- pub(crate) fn set(&mut self, tail: Record<DecryptedData>) {
- self.set_raw(tail.host.id, tail.tag, tail.idx)
- }
-
pub(crate) fn set_raw(&mut self, host: HostId, tag: String, tail_id: RecordIdx) {
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)).cloned()
+ self.hosts.get(&host).and_then(|v| v.get(&tag)).copied()
}
/// Diff this index with another, likely remote index.
@@ -148,11 +132,11 @@ impl RecordStatus {
let mut ret = Vec::new();
// First, we check if other has everything that self has
- for (host, tag_map) in self.hosts.iter() {
- for (tag, idx) in tag_map.iter() {
+ for (host, tag_map) in &self.hosts {
+ for (tag, idx) in tag_map {
match other.get(*host, tag.clone()) {
// The other store is all up to date! No diff.
- Some(t) if t.eq(idx) => continue,
+ Some(t) if t.eq(idx) => (),
// The other store does exist, and it is either ahead or behind us. A diff regardless
Some(t) => ret.push(Diff {
@@ -169,7 +153,7 @@ impl RecordStatus {
local: Some(*idx),
remote: None,
}),
- };
+ }
}
}
@@ -177,11 +161,11 @@ impl RecordStatus {
// If the other store knows of a tag that we are not yet aware of, then the diff will be missed
// account for that!
- for (host, tag_map) in other.hosts.iter() {
- for (tag, idx) in tag_map.iter() {
+ for (host, tag_map) in &other.hosts {
+ for (tag, idx) in tag_map {
match self.get(*host, tag.clone()) {
// If we have this host/tag combo, the comparison and diff will have already happened above
- Some(_) => continue,
+ Some(_) => (),
None => ret.push(Diff {
host: *host,
@@ -189,7 +173,7 @@ impl RecordStatus {
remote: Some(*idx),
local: None,
}),
- };
+ }
}
}
@@ -282,7 +266,7 @@ impl Record<EncryptedData> {
mod tests {
use crate::atuin_common::record::{Host, HostId};
- use super::{DecryptedData, Diff, Record, RecordStatus};
+ use super::{DecryptedData, Record, RecordStatus};
use pretty_assertions::assert_eq;
fn test_record() -> Record<DecryptedData> {
@@ -296,84 +280,6 @@ mod tests {
}
#[test]
- fn record_index() {
- let mut index = RecordStatus::new();
- let record = test_record();
-
- index.set(record.clone());
-
- let tail = index.get(record.host.id, record.tag);
-
- assert_eq!(
- record.idx,
- tail.expect("tail not in store"),
- "tail in store did not match"
- );
- }
-
- #[test]
- fn record_index_overwrite() {
- let mut index = RecordStatus::new();
- let record = test_record();
- let child = record.append(vec![1, 2, 3]);
-
- index.set(record.clone());
- index.set(child.clone());
-
- let tail = index.get(record.host.id, record.tag);
-
- assert_eq!(
- child.idx,
- tail.expect("tail not in store"),
- "tail in store did not match"
- );
- }
-
- #[test]
- fn record_index_no_diff() {
- // Here, they both have the same version and should have no diff
-
- let mut index1 = RecordStatus::new();
- let mut index2 = RecordStatus::new();
-
- let record1 = test_record();
-
- index1.set(record1.clone());
- index2.set(record1);
-
- let diff = index1.diff(&index2);
-
- assert_eq!(0, diff.len(), "expected empty diff");
- }
-
- #[test]
- fn record_index_single_diff() {
- // Here, they both have the same stores, but one is ahead by a single record
-
- let mut index1 = RecordStatus::new();
- let mut index2 = RecordStatus::new();
-
- let record1 = test_record();
- let record2 = record1.append(vec![1, 2, 3]);
-
- index1.set(record1);
- index2.set(record2.clone());
-
- let diff = index1.diff(&index2);
-
- assert_eq!(1, diff.len(), "expected single diff");
- assert_eq!(
- diff[0],
- Diff {
- host: record2.host.id,
- tag: record2.tag,
- remote: Some(1),
- local: Some(0)
- }
- );
- }
-
- #[test]
fn record_index_multi_diff() {
// A much more complex case, with a bunch more checks
let mut index1 = RecordStatus::new();
diff --git a/crates/turtle/src/atuin_daemon/client.rs b/crates/turtle/src/atuin_daemon/client.rs
index a0a27741..325b21b8 100644
--- a/crates/turtle/src/atuin_daemon/client.rs
+++ b/crates/turtle/src/atuin_daemon/client.rs
@@ -29,8 +29,7 @@ use crate::atuin_daemon::search::{
search_client::SearchClient as SearchServiceClient,
};
use crate::atuin_daemon::semantic::{
- CommandCapture, CommandOutputReply, CommandOutputRequest, OutputRange, RecordCommandsReply,
- semantic_client::SemanticClient as SemanticServiceClient,
+ CommandCapture, RecordCommandsReply, semantic_client::SemanticClient as SemanticServiceClient,
};
pub(crate) struct HistoryClient {
@@ -112,7 +111,7 @@ impl HistoryClient {
duration: u64,
exit: i64,
) -> Result<EndHistoryReply> {
- let req = EndHistoryRequest { id, duration, exit };
+ let req = EndHistoryRequest { id, exit, duration };
Ok(self.client.end_history(req).await?.into_inner())
}
@@ -255,22 +254,6 @@ impl SemanticClient {
let stream = tokio_stream::iter(captures);
Ok(self.client.record_commands(stream).await?.into_inner())
}
-
- pub(crate) async fn command_output(
- &mut self,
- history_id: String,
- ranges: Vec<(i64, i64)>,
- ) -> Result<CommandOutputReply> {
- let request = CommandOutputRequest {
- history_id,
- ranges: ranges
- .into_iter()
- .map(|(start, end)| OutputRange { start, end })
- .collect(),
- };
-
- Ok(self.client.command_output(request).await?.into_inner())
- }
}
// ============================================================================
@@ -354,65 +337,3 @@ fn daemon_event_to_proto(
}
}
}
-
-// ============================================================================
-// Convenience Functions
-// ============================================================================
-
-/// Emit an event to the daemon.
-///
-/// This is a fire-and-forget helper for sending events to the daemon from
-/// external processes like CLI commands. If the daemon isn't running, this
-/// will silently succeed (returns Ok).
-///
-/// # Example
-///
-/// ```ignore
-/// // After pruning history
-/// emit_event(DaemonEvent::HistoryPruned).await?;
-///
-/// // After deleting specific history items
-/// emit_event(DaemonEvent::HistoryDeleted { ids: vec![...] }).await?;
-///
-/// // Request immediate sync
-/// emit_event(DaemonEvent::ForceSync).await?;
-/// ```
-pub(crate) async fn emit_event(event: DaemonEvent) -> Result<()> {
- emit_event_with_settings(event, None).await
-}
-
-/// Emit an event to the daemon with explicit settings.
-///
-/// If settings are not provided, they will be loaded from the default location.
-/// If the daemon isn't running, this will silently succeed.
-pub(crate) async fn emit_event_with_settings(
- event: DaemonEvent,
- settings: Option<&Settings>,
-) -> Result<()> {
- // Load settings if not provided
- let owned_settings;
- let settings = match settings {
- Some(s) => s,
- None => {
- owned_settings = Settings::new()?;
- &owned_settings
- }
- };
-
- // Try to connect - if daemon isn't running, that's fine
- let mut client = match ControlClient::from_settings(settings).await {
- Ok(c) => c,
- Err(e) => {
- tracing::debug!(?e, "daemon not running, skipping event emission");
- return Ok(());
- }
- };
-
- // Send the event
- if let Err(e) = client.send_event(event).await {
- tracing::debug!(?e, "failed to send event to daemon");
- // Don't fail - this is fire-and-forget
- }
-
- Ok(())
-}
diff --git a/crates/turtle/src/atuin_daemon/daemon.rs b/crates/turtle/src/atuin_daemon/daemon.rs
index 7583c197..80aaeef8 100644
--- a/crates/turtle/src/atuin_daemon/daemon.rs
+++ b/crates/turtle/src/atuin_daemon/daemon.rs
@@ -113,17 +113,7 @@ impl DaemonHandle {
self.state.settings.read().await
}
- /// Reload settings from disk and emit a SettingsReloaded event.
- ///
- /// Components listening for `SettingsReloaded` can then re-read settings
- /// via `handle.settings()` to pick up the changes.
- pub(crate) async fn reload_settings(&self) -> Result<()> {
- let new_settings = Settings::new()?;
- self.apply_settings(new_settings).await;
- Ok(())
- }
-
- /// Apply already-loaded settings and emit a SettingsReloaded event.
+ /// Apply already-loaded settings and emit a [`SettingsReloaded`] event.
///
/// Use this when settings have already been loaded (e.g., from a file watcher)
/// to avoid parsing the config file twice.
@@ -292,7 +282,7 @@ impl Daemon {
/// Run the daemon event loop.
///
- /// This processes events until a ShutdownRequested event is received.
+ /// This processes events until a [`ShutdownRequested`] event is received.
/// Components must be started first via `start_components()`.
pub(crate) async fn run_event_loop(&mut self) -> Result<()> {
let mut event_rx = self.handle.subscribe();
@@ -338,18 +328,6 @@ impl Daemon {
tracing::info!("all components stopped");
}
- /// Run the daemon.
- ///
- /// This is a convenience method that starts components, runs the event loop,
- /// and handles shutdown. It does not return until the daemon is shut down.
- pub(crate) async fn run(mut self) -> Result<()> {
- self.start_components().await?;
- self.run_event_loop().await?;
- self.stop_components().await;
- tracing::info!("daemon stopped");
- Ok(())
- }
-
async fn dispatch_event(&mut self, event: &DaemonEvent) {
for component in &mut self.components {
if let Err(e) = component.handle_event(event).await {
@@ -424,7 +402,7 @@ impl DaemonBuilder {
/// Build the daemon.
///
/// This loads the encryption key and creates the daemon state.
- pub(crate) async fn build(self) -> Result<Daemon> {
+ pub(crate) fn build(self) -> Result<Daemon> {
let store = self.store.ok_or_else(|| eyre::eyre!("store is required"))?;
let history_db = self
.history_db
diff --git a/crates/turtle/src/atuin_daemon/mod.rs b/crates/turtle/src/atuin_daemon/mod.rs
index 6037b5a8..b161b0cc 100644
--- a/crates/turtle/src/atuin_daemon/mod.rs
+++ b/crates/turtle/src/atuin_daemon/mod.rs
@@ -52,8 +52,7 @@ pub(crate) async fn boot(
.component(search_component)
.component(semantic_component)
.component(sync_component)
- .build()
- .await?;
+ .build()?;
// Get a handle for the control service and gRPC server shutdown
let handle = daemon.handle();
diff --git a/crates/turtle/src/atuin_daemon/search/index.rs b/crates/turtle/src/atuin_daemon/search/index.rs
index a23b3133..197a8c1b 100644
--- a/crates/turtle/src/atuin_daemon/search/index.rs
+++ b/crates/turtle/src/atuin_daemon/search/index.rs
@@ -90,7 +90,7 @@ impl FrecencyData {
};
// Frequency boost: more uses = higher score (with diminishing returns)
- let frequency_score = ((self.count as f64).ln() * 20.0).min(100.0);
+ 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
@@ -117,7 +117,7 @@ pub(crate) struct CommandData {
}
impl CommandData {
- /// Create a new CommandData from a history entry.
+ /// 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)?;
@@ -237,9 +237,13 @@ pub(crate) enum IndexFilterMode {
/// 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>,
}
@@ -325,21 +329,21 @@ impl SearchIndex {
self.commands.len()
}
- /// Get the number of items in Nucleo (should match command_count).
- pub(crate) async fn nucleo_item_count(&self) -> u32 {
- self.nucleo.read().await.snapshot().item_count()
- }
-
/// 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,
- _context: &QueryContext,
+ // 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;
@@ -480,15 +484,6 @@ mod tests {
use super::*;
use time::macros::datetime;
- fn make_history(command: &str, cwd: &str, timestamp: OffsetDateTime) -> History {
- History::import()
- .timestamp(timestamp)
- .command(command)
- .cwd(cwd)
- .build()
- .into()
- }
-
#[test]
fn frecency_data_compute() {
let now = 1_000_000i64;
diff --git a/crates/turtle/src/atuin_history/stats.rs b/crates/turtle/src/atuin_history/stats.rs
index 12d1ffc5..2328eeca 100644
--- a/crates/turtle/src/atuin_history/stats.rs
+++ b/crates/turtle/src/atuin_history/stats.rs
@@ -4,7 +4,7 @@ use crossterm::style::{Color, ResetColor, SetAttribute, SetForegroundColor};
use serde::{Deserialize, Serialize};
use unicode_segmentation::UnicodeSegmentation;
-use crate::atuin_client::{history::History, settings::Settings, theme::Meaning, theme::Theme};
+use crate::atuin_client::{history::History, settings::Settings};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Stats {
@@ -162,7 +162,7 @@ fn strip_leading_env_vars(command: &str) -> &str {
command[token_start_pos..].trim()
}
-pub(crate) fn pretty_print(stats: Stats, ngram_size: usize, theme: &Theme) {
+pub(crate) fn pretty_print(stats: Stats, ngram_size: usize) {
let max = stats.top.iter().map(|x| x.1).max().unwrap();
let num_pad = max.ilog10() as usize + 1;
@@ -179,42 +179,21 @@ pub(crate) fn pretty_print(stats: Stats, ngram_size: usize, theme: &Theme) {
});
for (command, count) in stats.top {
- let gray = SetForegroundColor(match theme.as_style(Meaning::Muted).foreground_color {
- Some(color) => color,
- None => Color::Grey,
- });
+ let gray = SetForegroundColor(Color::Grey);
let bold = SetAttribute(crossterm::style::Attribute::Bold);
let in_ten = 10 * count / max;
print!("[");
- print!(
- "{}",
- SetForegroundColor(match theme.get_error().foreground_color {
- Some(color) => color,
- None => Color::Red,
- })
- );
+ print!("{}", SetForegroundColor(Color::Red));
for i in 0..in_ten {
if i == 2 {
- print!(
- "{}",
- SetForegroundColor(match theme.get_warning().foreground_color {
- Some(color) => color,
- None => Color::Yellow,
- })
- );
+ print!("{}", SetForegroundColor(Color::Yellow));
}
if i == 5 {
- print!(
- "{}",
- SetForegroundColor(match theme.get_info().foreground_color {
- Some(color) => color,
- None => Color::Green,
- })
- );
+ print!("{}", SetForegroundColor(Color::Green));
}
print!("▮");
diff --git a/crates/turtle/src/command/client.rs b/crates/turtle/src/command/client.rs
index 9ab28e15..0c97f945 100644
--- a/crates/turtle/src/command/client.rs
+++ b/crates/turtle/src/command/client.rs
@@ -5,7 +5,7 @@ use clap::Subcommand;
use eyre::{Result, WrapErr};
use crate::atuin_client::{
- database::ClientSqlite, record::sqlite_store::SqliteStore, settings::Settings, theme,
+ database::ClientSqlite, record::sqlite_store::SqliteStore, settings::Settings,
};
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::{
@@ -40,13 +40,8 @@ fn cleanup_old_logs(log_dir: &Path, prefix: &str, retention_days: u64) {
}
}
-#[cfg(feature = "sync")]
-mod sync;
-
-#[cfg(feature = "daemon")]
-mod daemon;
-
mod config;
+mod daemon;
mod default_config;
mod history;
mod info;
@@ -55,6 +50,7 @@ mod search;
mod server;
mod stats;
mod store;
+mod sync;
mod wrapped;
#[derive(Subcommand, Debug)]
@@ -67,7 +63,6 @@ pub(crate) enum Cmd {
/// Interactive history search
Search(search::Cmd),
- #[cfg(feature = "sync")]
#[command(subcommand)]
/// Request a sync or view sync status
Sync(sync::Cmd),
@@ -96,7 +91,6 @@ pub(crate) enum Cmd {
Wrapped { year: Option<i32> },
/// *Experimental* Manage the background daemon
- #[cfg(feature = "daemon")]
#[command()]
Daemon(daemon::Cmd),
@@ -134,9 +128,8 @@ impl Cmd {
// doing anything else. History commands are performance-sensitive and run before and after
// every shell command, so we want to skip any unnecessary initialization for them.
let settings = Settings::new().wrap_err("could not load client settings")?;
- let theme_manager = theme::ThemeManager::new(settings.theme.debug, None);
- runtime.block_on(self.run_inner(settings, theme_manager))
+ runtime.block_on(self.run_inner(settings))
};
runtime.shutdown_timeout(std::time::Duration::from_millis(50));
@@ -145,11 +138,7 @@ impl Cmd {
}
#[expect(clippy::too_many_lines)]
- async fn run_inner(
- self,
- mut settings: Settings,
- mut theme_manager: theme::ThemeManager,
- ) -> Result<()> {
+ async fn run_inner(self, mut settings: Settings) -> Result<()> {
// ATUIN_LOG env var overrides config file level settings
let env_log_set = std::env::var("ATUIN_LOG").is_ok();
@@ -162,19 +151,11 @@ impl Cmd {
let use_search_logging = is_interactive_search && settings.logs.search_enabled();
// Use file-based logging for daemon
- #[cfg(feature = "daemon")]
let use_daemon_logging = matches!(&self, Self::Daemon(_)) && settings.logs.daemon_enabled();
- #[cfg(not(feature = "daemon"))]
- let use_daemon_logging = false;
-
// Check if daemon should also log to console
- #[cfg(feature = "daemon")]
let daemon_show_logs = matches!(&self, Self::Daemon(cmd) if cmd.show_logs());
- #[cfg(not(feature = "daemon"))]
- let daemon_show_logs = false;
-
// Set up span timing JSON logs if ATUIN_SPAN is set
let span_path = std::env::var("ATUIN_SPAN").ok().map(|p| {
if p.is_empty() {
@@ -317,14 +298,11 @@ impl Cmd {
let db = ClientSqlite::new(db_path, settings.local_timeout).await?;
let sqlite_store = SqliteStore::new(record_store_path, settings.local_timeout).await?;
- let theme_name = settings.theme.name.clone();
- let theme = theme_manager.load_theme(theme_name.as_str(), settings.theme.max_depth);
-
match self {
- Self::Stats(stats) => stats.run(&db, &settings, theme).await,
- Self::Search(search) => search.run(db, &mut settings, sqlite_store, theme).await,
+ Self::Stats(stats) => stats.run(&db, &settings).await,
+ Self::Search(search) => search.run(db, &mut settings, sqlite_store).await,
+ Self::Wrapped { year } => wrapped::run(year, &db, &settings).await,
- #[cfg(feature = "sync")]
Self::Sync(sync) => sync.run(settings, &db, sqlite_store).await,
Self::Store(store) => store.run(&settings, &db, sqlite_store).await,
@@ -336,9 +314,6 @@ impl Cmd {
Ok(())
}
- Self::Wrapped { year } => wrapped::run(year, &db, &settings, theme).await,
-
- #[cfg(feature = "daemon")]
Self::Daemon(cmd) => cmd.run(settings, sqlite_store, db).await,
Self::History(_) | Self::Init(_) | Self::Config(_) | Self::Server(_) => {
diff --git a/crates/turtle/src/command/client/daemon.rs b/crates/turtle/src/command/client/daemon.rs
index cb5dd118..41cb04fe 100644
--- a/crates/turtle/src/command/client/daemon.rs
+++ b/crates/turtle/src/command/client/daemon.rs
@@ -22,16 +22,8 @@ use tokio::time::sleep;
#[derive(clap::Args, Debug)]
pub(crate) struct Cmd {
- /// Internal flag for daemonization
- #[arg(long, hide = true)]
- daemonize: bool,
-
- /// Also write daemon logs to the console (useful for debugging)
- #[arg(long)]
- show_logs: bool,
-
#[command(subcommand)]
- subcmd: Option<SubCmd>,
+ subcmd: SubCmd,
}
#[derive(Subcommand, Debug)]
@@ -67,8 +59,7 @@ impl Cmd {
#[cfg(unix)]
pub(crate) fn should_daemonize(&self) -> bool {
match &self.subcmd {
- Some(SubCmd::Start { daemonize, .. }) => *daemonize,
- None => self.daemonize,
+ SubCmd::Start { daemonize, .. } => *daemonize,
_ => false,
}
}
@@ -76,8 +67,7 @@ impl Cmd {
/// Returns `true` when logs should also be written to the console.
pub(crate) fn show_logs(&self) -> bool {
match &self.subcmd {
- Some(SubCmd::Start { show_logs, .. }) => *show_logs,
- None => self.show_logs,
+ SubCmd::Start { show_logs, .. } => *show_logs,
_ => false,
}
}
@@ -89,14 +79,10 @@ impl Cmd {
history_db: ClientSqlite,
) -> Result<()> {
match self.subcmd {
- None => {
- eprintln!("Warning: `atuin daemon` is deprecated, use `atuin daemon start`");
- run(settings, store, history_db, false).await
- }
- Some(SubCmd::Start { force, .. }) => run(settings, store, history_db, force).await,
- Some(SubCmd::Status) => status_cmd(&settings).await,
- Some(SubCmd::Stop) => stop_cmd(&settings).await,
- Some(SubCmd::Restart) => restart_cmd(&settings).await,
+ SubCmd::Start { force, .. } => run(settings, store, history_db, force).await,
+ SubCmd::Status => status_cmd(&settings).await,
+ SubCmd::Stop => stop_cmd(&settings).await,
+ SubCmd::Restart => restart_cmd(&settings).await,
}
}
}
@@ -331,7 +317,6 @@ async fn wait_until_ready(settings: &Settings, timeout: Duration) -> Result<Hist
}
}
-#[expect(clippy::unnecessary_wraps)]
fn ensure_autostart_supported(settings: &Settings) -> Result<()> {
#[cfg(unix)]
if settings.daemon.systemd_socket {
@@ -438,7 +423,12 @@ pub(crate) async fn start_history(settings: &Settings, history: History) -> Resu
Ok(resp.id)
}
-pub(crate) async fn end_history(settings: &Settings, id: String, duration: u64, exit: i64) -> Result<()> {
+pub(crate) async fn end_history(
+ settings: &Settings,
+ id: String,
+ duration: u64,
+ exit: i64,
+) -> Result<()> {
match async {
connect_client(settings)
.await?
diff --git a/crates/turtle/src/command/client/history.rs b/crates/turtle/src/command/client/history.rs
index 693098c0..2ddcb3a6 100644
--- a/crates/turtle/src/command/client/history.rs
+++ b/crates/turtle/src/command/client/history.rs
@@ -10,14 +10,10 @@ use clap::Subcommand;
use eyre::{Context, Result, bail};
use runtime_format::{FormatKey, FormatKeyError, ParseSegment, ParsedFmt};
-#[cfg(feature = "daemon")]
use super::daemon as daemon_cmd;
-#[cfg(feature = "daemon")]
use colored::Colorize;
-#[cfg(feature = "daemon")]
use serde::Serialize;
-#[cfg(feature = "daemon")]
use crate::atuin_daemon::history::{HistoryEventKind, TailHistoryReply};
use crate::atuin_client::{
@@ -31,13 +27,9 @@ use crate::atuin_client::{
},
};
-#[cfg(feature = "sync")]
-use crate::atuin_client::record;
-
-use log::{debug, warn};
+use log::debug;
use time::{OffsetDateTime, macros::format_description};
-#[cfg(feature = "daemon")]
use super::daemon;
use super::search::format_duration_into;
@@ -65,8 +57,10 @@ pub(crate) enum Cmd {
/// Finishes a new command in the history (adds time, exit code)
End {
id: String,
+
#[arg(long, short)]
exit: i64,
+
#[arg(long, short)]
duration: Option<u64>,
},
@@ -181,7 +175,6 @@ impl ListMode {
}
}
-#[expect(clippy::cast_sign_loss)]
pub(crate) fn print_list(
h: &[History],
list_mode: ListMode,
@@ -410,42 +403,6 @@ fn normalize_command_for_storage<'a>(command: &'a str, settings: &Settings) -> &
}
}
-async fn handle_start(
- db: &ClientSqlite,
- settings: &Settings,
- command: &str,
- author: Option<&str>,
- intent: Option<&str>,
-) -> Result<Option<String>> {
- // It's better for atuin to silently fail here and attempt to
- // store whatever is ran, than to throw an error to the terminal
- let cwd = utils::get_current_dir();
- let command = normalize_command_for_storage(command, settings);
-
- let mut h: History = History::capture()
- .timestamp(OffsetDateTime::now_utc())
- .command(command)
- .cwd(cwd)
- .build()
- .into();
- apply_start_metadata(&mut h, author, intent);
-
- if !h.should_save(settings) {
- return Ok(None);
- }
-
- let id = h.id.0.clone();
-
- // Silently ignore database errors to avoid breaking the shell
- // This is important when disk is full or database is locked
- if let Err(e) = db.save(&h).await {
- debug!("failed to save history: {e}");
- }
-
- Ok(Some(id))
-}
-
-#[cfg(feature = "daemon")]
async fn handle_daemon_start(
settings: &Settings,
command: &str,
@@ -482,65 +439,6 @@ async fn handle_daemon_start(
Ok(Some(resp))
}
-#[expect(unused_variables)]
-async fn handle_end(
- db: &ClientSqlite,
- store: SqliteStore,
- history_store: HistoryStore,
- settings: &Settings,
- id: &str,
- exit: i64,
- duration: Option<u64>,
-) -> Result<()> {
- if id.trim() == "" {
- return Ok(());
- }
-
- let Some(mut h) = db.load(id).await? else {
- warn!("history entry is missing");
- return Ok(());
- };
-
- if h.duration > 0 {
- debug!("cannot end history - already has duration");
-
- // returning OK as this can occur if someone Ctrl-c a prompt
- return Ok(());
- }
-
- if !settings.store_failed && exit > 0 {
- debug!("history has non-zero exit code, and store_failed is false");
-
- // the history has already been inserted half complete. remove it
- db.delete(h).await?;
-
- return Ok(());
- }
-
- h.exit = exit;
- h.duration = match duration {
- Some(value) => i64::try_from(value).context("command took over 292 years")?,
- None => i64::try_from((OffsetDateTime::now_utc() - h.timestamp).whole_nanoseconds())
- .context("command took over 292 years")?,
- };
-
- db.update(&h).await?;
- history_store.push(h).await?;
-
- if settings.sync.should_sync().await? {
- let (_, downloaded) =
- record::sync::sync(settings, &store, &history_store.encryption_key).await?;
- Settings::save_sync_time().await?;
-
- crate::sync::build(settings, &store, db, Some(&downloaded)).await?;
- } else {
- debug!("sync disabled! not syncing");
- }
-
- Ok(())
-}
-
-#[cfg(feature = "daemon")]
async fn handle_daemon_end(
settings: &Settings,
id: &str,
@@ -552,70 +450,24 @@ async fn handle_daemon_end(
Ok(())
}
-pub(super) async fn start_history_entry(
- settings: &Settings,
- command: &str,
- author: Option<&str>,
- intent: Option<&str>,
-) -> Result<Option<String>> {
- #[cfg(feature = "daemon")]
- if settings.daemon.enabled {
- return handle_daemon_start(settings, command, author, intent).await;
- }
-
- let db_path = PathBuf::from(settings.db_path.as_str());
- let db = ClientSqlite::new(db_path, settings.local_timeout).await?;
- handle_start(&db, settings, command, author, intent).await
-}
-
-pub(super) async fn end_history_entry(
- settings: &Settings,
- id: &str,
- exit: i64,
- duration: Option<u64>,
-) -> Result<()> {
- #[cfg(feature = "daemon")]
- if settings.daemon.enabled {
- return handle_daemon_end(settings, id, exit, duration).await;
- }
-
- let db_path = PathBuf::from(settings.db_path.as_str());
- let record_store_path = PathBuf::from(settings.record_store_path.as_str());
-
- let db = ClientSqlite::new(db_path, settings.local_timeout).await?;
- let store = SqliteStore::new(record_store_path, settings.local_timeout).await?;
-
- let encryption_key: [u8; 32] = encryption::load_key(settings)
- .context("could not load encryption key")?
- .into();
- let host_id = Settings::host_id().await?;
- let history_store = HistoryStore::new(store.clone(), host_id, encryption_key);
-
- handle_end(&db, store, history_store, settings, id, exit, duration).await
-}
-
-#[cfg(feature = "daemon")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum TailKind {
Started,
Ended,
}
-#[cfg(feature = "daemon")]
#[derive(Clone, Debug, Eq, PartialEq)]
struct TailEvent {
kind: TailKind,
history: History,
}
-#[cfg(feature = "daemon")]
#[derive(Serialize)]
struct TailJsonEvent<'a> {
event: &'static str,
history: TailJsonHistory<'a>,
}
-#[cfg(feature = "daemon")]
#[derive(Serialize)]
struct TailJsonHistory<'a> {
id: &'a str,
@@ -642,7 +494,6 @@ struct TailJsonHistory<'a> {
finished_at: Option<String>,
}
-#[cfg(feature = "daemon")]
impl TailEvent {
fn from_proto(reply: TailHistoryReply) -> Result<Self> {
let history = reply
@@ -828,7 +679,6 @@ impl TailEvent {
}
}
-#[cfg(feature = "daemon")]
impl TailKind {
const fn as_str(self) -> &'static str {
match self {
@@ -846,12 +696,10 @@ impl TailKind {
}
}
-#[cfg(feature = "daemon")]
fn format_history_time(timestamp: OffsetDateTime, tz: Timezone) -> Result<String> {
Ok(timestamp.to_offset(tz.0).format(TIME_FMT)?)
}
-#[cfg(feature = "daemon")]
fn format_duration_ns(duration_ns: i64) -> String {
struct F(Duration);
impl Display for F {
@@ -863,7 +711,6 @@ fn format_duration_ns(duration_ns: i64) -> String {
F(Duration::from_nanos(duration_ns.max(0).cast_unsigned())).to_string()
}
-#[cfg(feature = "daemon")]
fn push_pretty_field(out: &mut String, label: &str, value: &str) {
out.push_str(" ");
let label = format!("{label}:");
@@ -885,7 +732,6 @@ fn push_pretty_field(out: &mut String, label: &str, value: &str) {
}
}
-#[cfg(feature = "daemon")]
fn normalize_optional_field(value: &str) -> Option<String> {
let trimmed = value.trim();
if trimmed.is_empty() {
@@ -896,7 +742,6 @@ fn normalize_optional_field(value: &str) -> Option<String> {
}
impl Cmd {
- #[cfg(feature = "daemon")]
async fn handle_tail(settings: &Settings) -> Result<()> {
let tty = std::io::stdout().is_terminal();
let mut client = daemon::tail_client(settings).await?;
@@ -918,7 +763,6 @@ impl Cmd {
Ok(())
}
- #[expect(clippy::too_many_lines, clippy::cast_possible_truncation)]
#[expect(clippy::too_many_arguments)]
#[expect(clippy::fn_params_excessive_bools)]
async fn handle_list(
@@ -1010,7 +854,6 @@ impl Cmd {
history_store.incremental_build(db, &[id]).await?;
}
- #[cfg(feature = "daemon")]
daemon_cmd::emit_event(settings, crate::atuin_daemon::DaemonEvent::HistoryPruned).await;
}
Ok(())
@@ -1058,7 +901,6 @@ impl Cmd {
let host_id = Settings::host_id().await?;
let history_store = HistoryStore::new(store.clone(), host_id, encryption_key);
- #[cfg(feature = "daemon")]
let ids = matches.iter().map(|h| h.id.clone()).collect::<Vec<_>>();
for entry in matches {
@@ -1067,7 +909,6 @@ impl Cmd {
history_store.incremental_build(db, &[id]).await?;
}
- #[cfg(feature = "daemon")]
daemon_cmd::emit_event(
settings,
crate::atuin_daemon::DaemonEvent::HistoryDeleted { ids },
@@ -1093,7 +934,7 @@ impl Cmd {
};
if let Some(id) =
- start_history_entry(settings, &command, author.as_deref(), intent.as_deref())
+ handle_daemon_start(settings, &command, author.as_deref(), intent.as_deref())
.await?
{
println!("{id}");
@@ -1102,16 +943,10 @@ impl Cmd {
Ok(())
}
Self::End { id, exit, duration } => {
- end_history_entry(settings, &id, exit, duration).await
+ handle_daemon_end(settings, &id, exit, duration).await
}
Self::Tail => {
- #[cfg(feature = "daemon")]
- {
- return Self::handle_tail(settings).await;
- }
-
- #[cfg(not(feature = "daemon"))]
- bail!("`atuin history tail` requires Atuin to be built with the `daemon` feature");
+ return Self::handle_tail(settings).await;
}
cmd => {
let context = current_context().await?;
@@ -1204,14 +1039,13 @@ impl Cmd {
#[cfg(test)]
mod tests {
- #[cfg(feature = "daemon")]
use time::macros::datetime;
use super::*;
#[test]
fn normalize_command_strips_trailing_spaces_and_tabs() {
- let settings = Settings::utc();
+ let settings = Settings::new().unwrap();
assert!(settings.strip_trailing_whitespace);
assert_eq!(normalize_command_for_storage("ls \t", &settings), "ls");
@@ -1219,7 +1053,7 @@ mod tests {
#[test]
fn normalize_command_preserves_escaped_trailing_space() {
- let settings = Settings::utc();
+ let settings = Settings::new().unwrap();
assert_eq!(
normalize_command_for_storage("printf foo\\ ", &settings),
@@ -1231,45 +1065,6 @@ mod tests {
);
}
- #[tokio::test]
- async fn handle_start_saves_trimmed_command() {
- let db = ClientSqlite::new("sqlite::memory:", 2.0).await.unwrap();
- let settings = Settings::utc();
-
- handle_start(&db, &settings, "ls \t", None, None)
- .await
- .unwrap();
-
- let history = db
- .before(OffsetDateTime::now_utc() + time::Duration::SECOND, 1)
- .await
- .unwrap()
- .pop()
- .unwrap();
- assert_eq!(history.command, "ls");
- }
-
- #[tokio::test]
- async fn handle_start_can_keep_trailing_whitespace() {
- let db = ClientSqlite::new("sqlite::memory:", 2.0).await.unwrap();
- let settings = Settings {
- strip_trailing_whitespace: false,
- ..Settings::utc()
- };
-
- handle_start(&db, &settings, "ls \t", None, None)
- .await
- .unwrap();
-
- let history = db
- .before(OffsetDateTime::now_utc() + time::Duration::SECOND, 1)
- .await
- .unwrap()
- .pop()
- .unwrap();
- assert_eq!(history.command, "ls \t");
- }
-
#[test]
fn test_format_string_no_panic() {
// Don't panic but provide helpful output (issue #2776)
@@ -1286,7 +1081,6 @@ mod tests {
assert!(std::panic::catch_unwind(|| parse_fmt("{time} - {command}")).is_ok());
}
- #[cfg(feature = "daemon")]
fn sample_tail_event(kind: TailKind) -> TailEvent {
TailEvent {
kind,
@@ -1306,7 +1100,6 @@ mod tests {
}
}
- #[cfg(feature = "daemon")]
#[test]
fn test_tail_json_output_contains_history_fields() {
let json = sample_tail_event(TailKind::Ended)
@@ -1321,7 +1114,6 @@ mod tests {
assert!(value.get("record").is_none());
}
- #[cfg(feature = "daemon")]
#[test]
fn test_tail_pretty_output_shows_pending_fields_for_started_events() {
let rendered = sample_tail_event(TailKind::Started)
diff --git a/crates/turtle/src/command/client/search.rs b/crates/turtle/src/command/client/search.rs
index 962e6b1e..bba48b8a 100644
--- a/crates/turtle/src/command/client/search.rs
+++ b/crates/turtle/src/command/client/search.rs
@@ -12,7 +12,6 @@ use crate::atuin_client::{
history::{History, store::HistoryStore},
record::sqlite_store::SqliteStore,
settings::{FilterMode, KeymapMode, SearchMode, Settings, Timezone},
- theme::Theme,
};
use super::history::ListMode;
@@ -160,7 +159,6 @@ impl Cmd {
db: ClientSqlite,
settings: &mut Settings,
store: SqliteStore,
- theme: &Theme,
) -> Result<()> {
let query = self.query.unwrap_or_else(|| {
std::env::var("ATUIN_QUERY").map_or_else(
@@ -226,7 +224,7 @@ impl Cmd {
let history_store = HistoryStore::new(store.clone(), host_id, encryption_key);
if self.interactive {
- let item = interactive::history(&query, settings, db, &history_store, theme).await?;
+ let item = interactive::history(&query, settings, db, &history_store).await?;
if let Some(result_file) = self.result_file {
let mut file = File::create(result_file)?;
diff --git a/crates/turtle/src/command/client/search/engines.rs b/crates/turtle/src/command/client/search/engines.rs
index a84c4798..94834221 100644
--- a/crates/turtle/src/command/client/search/engines.rs
+++ b/crates/turtle/src/command/client/search/engines.rs
@@ -8,22 +8,14 @@ use eyre::Result;
use super::cursor::Cursor;
-#[cfg(feature = "daemon")]
pub(crate) mod daemon;
pub(crate) mod db;
pub(crate) mod skim;
-#[expect(unused)] // settings is only used if daemon feature is enabled
pub(crate) fn engine(search_mode: SearchMode, settings: &Settings) -> Box<dyn SearchEngine> {
match search_mode {
SearchMode::Skim => Box::new(skim::Search::new()) as Box<_>,
- #[cfg(feature = "daemon")]
SearchMode::DaemonFuzzy => Box::new(daemon::Search::new(settings)) as Box<_>,
- #[cfg(not(feature = "daemon"))]
- SearchMode::DaemonFuzzy => {
- // Fall back to fuzzy mode if daemon feature is not enabled
- Box::new(db::Search(SearchMode::Fuzzy)) as Box<_>
- }
mode => Box::new(db::Search(mode)) as Box<_>,
}
}
diff --git a/crates/turtle/src/command/client/search/history_list.rs b/crates/turtle/src/command/client/search/history_list.rs
index 9d3a60e0..e46f37b7 100644
--- a/crates/turtle/src/command/client/search/history_list.rs
+++ b/crates/turtle/src/command/client/search/history_list.rs
@@ -5,7 +5,10 @@ use super::engines::SearchEngine;
use crate::atuin_client::{
history::History,
settings::{UiColumn, UiColumnType},
- theme::{Meaning, Theme},
+ theme::{
+ style_alerterror, style_alertinfo, style_alertwarn, style_annotation, style_base,
+ style_guidance,
+ },
};
use crate::atuin_common::utils::Escapable as _;
use itertools::Itertools;
@@ -39,7 +42,7 @@ pub(crate) struct HistoryList<'a> {
alternate_highlight: bool,
now: &'a dyn Fn() -> OffsetDateTime,
indicator: &'a str,
- theme: &'a Theme,
+
history_highlighter: HistoryHighlighter<'a>,
show_numeric_shortcuts: bool,
/// Columns to display (in order, after the indicator)
@@ -100,7 +103,7 @@ impl StatefulWidget for HistoryList<'_> {
alternate_highlight: self.alternate_highlight,
now: &self.now,
indicator: self.indicator,
- theme: self.theme,
+
history_highlighter: self.history_highlighter,
show_numeric_shortcuts: self.show_numeric_shortcuts,
columns: self.columns,
@@ -124,7 +127,7 @@ impl<'a> HistoryList<'a> {
alternate_highlight: bool,
now: &'a dyn Fn() -> OffsetDateTime,
indicator: &'a str,
- theme: &'a Theme,
+
history_highlighter: HistoryHighlighter<'a>,
show_numeric_shortcuts: bool,
columns: &'a [UiColumn],
@@ -136,7 +139,6 @@ impl<'a> HistoryList<'a> {
alternate_highlight,
now,
indicator,
- theme,
history_highlighter,
show_numeric_shortcuts,
columns,
@@ -173,7 +175,7 @@ struct DrawState<'a> {
alternate_highlight: bool,
now: &'a dyn Fn() -> OffsetDateTime,
indicator: &'a str,
- theme: &'a Theme,
+
history_highlighter: HistoryHighlighter<'a>,
show_numeric_shortcuts: bool,
columns: &'a [UiColumn],
@@ -203,7 +205,7 @@ impl DrawState<'_> {
.width
.saturating_sub(indicator_width + fixed_width);
- let style = self.theme.as_style(Meaning::Base);
+ let style = style_base();
// Render each configured column
for (idx, column) in self.columns.iter().enumerate() {
if idx != 0 {
@@ -251,11 +253,11 @@ impl DrawState<'_> {
}
fn duration(&mut self, h: &History, width: u16) {
- let style = self.theme.as_style(if h.success() {
- Meaning::AlertInfo
+ let style = if h.success() {
+ style_alertinfo()
} else {
- Meaning::AlertError
- });
+ style_alerterror()
+ };
let duration = Duration::from_nanos(u64::try_from(h.duration).unwrap_or(0));
let formatted = format_duration(duration);
let w = width as usize;
@@ -265,7 +267,7 @@ impl DrawState<'_> {
}
fn time(&mut self, h: &History, width: u16) {
- let style = self.theme.as_style(Meaning::Guidance);
+ let style = style_guidance();
// Account for the chance that h.timestamp is "in the future"
// This would mean that "since" is negative, and the unwrap here
@@ -284,13 +286,13 @@ impl DrawState<'_> {
}
fn command(&mut self, h: &History) {
- let mut style = self.theme.as_style(Meaning::Base);
+ let mut style = style_base();
let mut row_highlighted = false;
if !self.alternate_highlight && (self.y as usize + self.state.offset == self.state.selected)
{
row_highlighted = true;
// if not applying alternative highlighting to the whole row, color the command
- style = self.theme.as_style(Meaning::AlertError);
+ style = style_alerterror();
style.attributes.set(style::Attribute::Bold);
}
@@ -318,7 +320,7 @@ impl DrawState<'_> {
if row_highlighted {
// if the row is highlighted bold is not enough as the whole row is bold
// change the color too
- style = self.theme.as_style(Meaning::AlertWarn);
+ style = style_alertwarn();
}
style.attributes.set(style::Attribute::Bold);
}
@@ -332,7 +334,7 @@ impl DrawState<'_> {
/// Render the absolute datetime column (e.g., "2025-01-22 14:35")
fn datetime(&mut self, h: &History, width: u16) {
- let style = self.theme.as_style(Meaning::Annotation);
+ let style = style_annotation();
// Format: YYYY-MM-DD HH:MM
let formatted = h
.timestamp
@@ -348,7 +350,7 @@ impl DrawState<'_> {
/// Render the directory column (working directory, truncated)
fn directory(&mut self, h: &History, width: u16) {
- let style = self.theme.as_style(Meaning::Annotation);
+ let style = style_annotation();
let w = width as usize;
let cwd = &h.cwd;
let char_count = cwd.chars().count();
@@ -365,7 +367,7 @@ impl DrawState<'_> {
/// Render the host column (just the hostname)
fn host(&mut self, h: &History, width: u16) {
- let style = self.theme.as_style(Meaning::Annotation);
+ let style = style_annotation();
let w = width as usize;
// Database stores hostname as "hostname:username"
let host = h.hostname.split(':').next().unwrap_or(&h.hostname);
@@ -382,7 +384,7 @@ impl DrawState<'_> {
/// Render the user column
fn user(&mut self, h: &History, width: u16) {
- let style = self.theme.as_style(Meaning::Annotation);
+ let style = style_annotation();
let w = width as usize;
// Database stores hostname as "hostname:username"
let user = h.hostname.split(':').nth(1).unwrap_or("");
@@ -400,9 +402,9 @@ impl DrawState<'_> {
/// Render the exit code column
fn exit_code(&mut self, h: &History, width: u16) {
let style = if h.success() {
- self.theme.as_style(Meaning::AlertInfo)
+ style_alertinfo()
} else {
- self.theme.as_style(Meaning::AlertError)
+ style_alerterror()
};
let w = width as usize;
let display = format!("{:>w$}", h.exit);
diff --git a/crates/turtle/src/command/client/search/inspector.rs b/crates/turtle/src/command/client/search/inspector.rs
index a1bf803f..f7b40a26 100644
--- a/crates/turtle/src/command/client/search/inspector.rs
+++ b/crates/turtle/src/command/client/search/inspector.rs
@@ -4,6 +4,7 @@ use time::macros::format_description;
use crate::atuin_client::{
history::{History, HistoryStats},
settings::{Settings, Timezone},
+ theme::{style_annotation, style_base, style_important},
};
use ratatui::{
Frame,
@@ -17,7 +18,6 @@ use ratatui::{
use super::duration::format_duration;
-use super::super::theme::{Meaning, Theme};
use super::interactive::{Compactness, to_compactness};
#[expect(clippy::cast_sign_loss)]
@@ -31,7 +31,6 @@ pub(crate) fn draw_commands(
history: &History,
stats: &HistoryStats,
compact: bool,
- theme: &Theme,
) {
let commands = Layout::default()
.direction(if compact {
@@ -56,16 +55,16 @@ pub(crate) fn draw_commands(
let command = Paragraph::new(Text::from(Span::styled(
history.command.clone(),
- Style::from_crossterm(theme.as_style(Meaning::Important)),
+ Style::from_crossterm(style_important()),
)))
.block(if compact {
Block::new()
.borders(Borders::NONE)
- .style(Style::from_crossterm(theme.as_style(Meaning::Base)))
+ .style(Style::from_crossterm(style_base()))
} else {
Block::new()
.borders(Borders::ALL)
- .style(Style::from_crossterm(theme.as_style(Meaning::Base)))
+ .style(Style::from_crossterm(style_base()))
.title("Command")
.padding(Padding::horizontal(1))
});
@@ -79,11 +78,11 @@ pub(crate) fn draw_commands(
.block(if compact {
Block::new()
.borders(Borders::NONE)
- .style(Style::from_crossterm(theme.as_style(Meaning::Annotation)))
+ .style(Style::from_crossterm(style_annotation()))
} else {
Block::new()
.borders(Borders::ALL)
- .style(Style::from_crossterm(theme.as_style(Meaning::Annotation)))
+ .style(Style::from_crossterm(style_annotation()))
.title("Previous command")
.padding(Padding::horizontal(1))
});
@@ -99,13 +98,13 @@ pub(crate) fn draw_commands(
.block(if compact {
Block::new()
.borders(Borders::NONE)
- .style(Style::from_crossterm(theme.as_style(Meaning::Annotation)))
+ .style(Style::from_crossterm(style_annotation()))
} else {
Block::new()
.borders(Borders::ALL)
.title("Next command")
.padding(Padding::horizontal(1))
- .style(Style::from_crossterm(theme.as_style(Meaning::Annotation)))
+ .style(Style::from_crossterm(style_annotation()))
});
f.render_widget(previous, commands[0]);
@@ -119,7 +118,6 @@ pub(crate) fn draw_stats_table(
history: &History,
tz: Timezone,
stats: &HistoryStats,
- theme: &Theme,
) {
let duration = Duration::from_nanos(u64_or_zero(history.duration));
let avg_duration = Duration::from_nanos(stats.average_duration);
@@ -149,7 +147,7 @@ pub(crate) fn draw_stats_table(
Block::default()
.title("Command stats")
.borders(Borders::ALL)
- .style(Style::from_crossterm(theme.as_style(Meaning::Base)))
+ .style(Style::from_crossterm(style_base()))
.padding(Padding::vertical(1)),
);
@@ -196,7 +194,7 @@ fn sort_duration_over_time(durations: &[(String, i64)]) -> Vec<(String, i64)> {
.collect()
}
-fn draw_stats_charts(f: &mut Frame<'_>, parent: Rect, stats: &HistoryStats, theme: &Theme) {
+fn draw_stats_charts(f: &mut Frame<'_>, parent: Rect, stats: &HistoryStats) {
let exits: Vec<Bar> = stats
.exits
.iter()
@@ -211,7 +209,7 @@ fn draw_stats_charts(f: &mut Frame<'_>, parent: Rect, stats: &HistoryStats, them
.block(
Block::default()
.title("Exit code distribution")
- .style(Style::from_crossterm(theme.as_style(Meaning::Base)))
+ .style(Style::from_crossterm(style_base()))
.borders(Borders::ALL),
)
.bar_width(3)
@@ -235,7 +233,7 @@ fn draw_stats_charts(f: &mut Frame<'_>, parent: Rect, stats: &HistoryStats, them
.block(
Block::default()
.title("Runs per day")
- .style(Style::from_crossterm(theme.as_style(Meaning::Base)))
+ .style(Style::from_crossterm(style_base()))
.borders(Borders::ALL),
)
.bar_width(3)
@@ -261,7 +259,7 @@ fn draw_stats_charts(f: &mut Frame<'_>, parent: Rect, stats: &HistoryStats, them
.block(
Block::default()
.title("Duration over time")
- .style(Style::from_crossterm(theme.as_style(Meaning::Base)))
+ .style(Style::from_crossterm(style_base()))
.borders(Borders::ALL),
)
.bar_width(5)
@@ -291,14 +289,13 @@ pub(crate) fn draw(
history: &History,
stats: &HistoryStats,
settings: &Settings,
- theme: &Theme,
tz: Timezone,
) {
let compactness = to_compactness(f, settings);
match compactness {
- Compactness::Ultracompact => draw_ultracompact(f, chunk, history, stats, theme),
- _ => draw_full(f, chunk, history, stats, theme, tz),
+ Compactness::Ultracompact => draw_ultracompact(f, chunk, history, stats),
+ _ => draw_full(f, chunk, history, stats, tz),
}
}
@@ -307,9 +304,8 @@ pub(crate) fn draw_ultracompact(
chunk: Rect,
history: &History,
stats: &HistoryStats,
- theme: &Theme,
) {
- draw_commands(f, chunk, history, stats, true, theme);
+ draw_commands(f, chunk, history, stats, true);
}
pub(crate) fn draw_full(
@@ -317,7 +313,6 @@ pub(crate) fn draw_full(
chunk: Rect,
history: &History,
stats: &HistoryStats,
- theme: &Theme,
tz: Timezone,
) {
let vert_layout = Layout::default()
@@ -330,18 +325,15 @@ pub(crate) fn draw_full(
.constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)])
.split(vert_layout[1]);
- draw_commands(f, vert_layout[0], history, stats, false, theme);
- draw_stats_table(f, stats_layout[0], history, tz, stats, theme);
- draw_stats_charts(f, stats_layout[1], stats, theme);
+ draw_commands(f, vert_layout[0], history, stats, false);
+ draw_stats_table(f, stats_layout[0], history, tz, stats);
+ draw_stats_charts(f, stats_layout[1], stats);
}
#[cfg(test)]
mod tests {
use super::draw_ultracompact;
- use crate::atuin_client::{
- history::{History, HistoryId, HistoryStats},
- theme::ThemeManager,
- };
+ use crate::atuin_client::history::{History, HistoryId, HistoryStats};
use ratatui::{backend::TestBackend, prelude::*};
use time::OffsetDateTime;
@@ -406,9 +398,7 @@ mod tests {
let prev = stats.previous.clone().unwrap();
let next = stats.next.clone().unwrap();
- let mut manager = ThemeManager::new(Some(true), Some("".to_string()));
- let theme = manager.load_theme("(none)", None);
- let _ = terminal.draw(|f| draw_ultracompact(f, chunk, &history, &stats, &theme));
+ let _ = 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 1d067e50..2c6af8cf 100644
--- a/crates/turtle/src/command/client/search/interactive.rs
+++ b/crates/turtle/src/command/client/search/interactive.rs
@@ -7,7 +7,10 @@ use std::{
use std::io::Read as _;
use crate::{
- atuin_client::database::ClientSqlite,
+ atuin_client::{
+ database::ClientSqlite,
+ theme::{style_annotation, style_base, style_important},
+ },
atuin_common::{shell::Shell, utils::Escapable as _},
};
use eyre::Result;
@@ -30,7 +33,6 @@ use crate::atuin_client::{
use crate::command::client::search::history_list::HistoryHighlighter;
use crate::command::client::search::keybindings::KeymapSet;
-use crate::command::client::theme::{Meaning, Theme};
use crate::{VERSION, command::client::search::engines};
use ratatui::{
@@ -42,7 +44,7 @@ use ratatui::{
execute, queue, terminal,
},
layout::{Alignment, Constraint, Direction, Layout},
- prelude::*,
+ prelude::Rect,
style::{Modifier, Style},
text::{Line, Span, Text},
widgets::{Block, BorderType, Borders, Clear, Padding, Paragraph, Tabs},
@@ -800,9 +802,6 @@ impl State {
}
}
- #[expect(clippy::bool_to_int_with_if)]
- #[expect(clippy::too_many_lines)]
- #[expect(clippy::too_many_arguments)]
fn draw(
&mut self,
f: &mut Frame,
@@ -810,17 +809,16 @@ impl State {
stats: Option<HistoryStats>,
inspecting: Option<&History>,
settings: &Settings,
- theme: &Theme,
+
popup_mode: bool,
) {
let area = f.area();
if popup_mode {
f.render_widget(Clear, area);
}
- self.draw_inner(f, area, results, stats, inspecting, settings, theme);
+ self.draw_inner(f, area, results, stats, inspecting, settings);
}
- #[expect(clippy::too_many_arguments)]
#[expect(clippy::too_many_lines)]
#[expect(clippy::bool_to_int_with_if)]
fn draw_inner(
@@ -831,7 +829,6 @@ impl State {
stats: Option<HistoryStats>,
inspecting: Option<&History>,
settings: &Settings,
- theme: &Theme,
) {
let compactness = to_compactness(f, settings);
let invert = settings.invert;
@@ -905,7 +902,7 @@ impl State {
.block(Block::default().borders(Borders::NONE))
.select(self.tab_index)
.style(Style::default())
- .highlight_style(Style::from_crossterm(theme.as_style(Meaning::Important)));
+ .highlight_style(Style::from_crossterm(style_important()));
f.render_widget(tabs, tabs_chunk);
}
@@ -928,13 +925,13 @@ impl State {
)
.split(header_chunk);
- let title = Self::build_title(theme);
+ let title = Self::build_title();
f.render_widget(title, header_chunks[0]);
- let help = self.build_help(settings, theme);
+ let help = self.build_help(settings);
f.render_widget(help, header_chunks[1]);
- let stats_tab = self.build_stats(theme);
+ let stats_tab = self.build_stats();
f.render_widget(stats_tab, header_chunks[2]);
let indicator: String = match compactness {
@@ -968,7 +965,6 @@ impl State {
self.keymap_mode,
&self.now,
indicator.as_str(),
- theme,
history_highlighter,
settings.show_numeric_shortcuts,
&settings.ui.columns,
@@ -999,7 +995,6 @@ impl State {
inspecting,
&stats.expect("Drawing inspector, but no stats"),
settings,
- theme,
settings.timezone,
);
}
@@ -1029,7 +1024,6 @@ impl State {
compactness,
preview_width,
preview_chunk.width.into(),
- theme,
);
#[expect(clippy::cast_possible_truncation)]
let prefix_width = settings
@@ -1083,9 +1077,9 @@ impl State {
));
}
- fn build_title(theme: &Theme) -> Paragraph<'_> {
+ fn build_title<'a>() -> Paragraph<'a> {
let title = {
- let style: Style = Style::from_crossterm(theme.as_style(Meaning::Base));
+ let style: Style = Style::from_crossterm(style_base());
Paragraph::new(Text::from(Span::styled(
format!("Atuin v{VERSION}"),
style.add_modifier(Modifier::BOLD),
@@ -1095,7 +1089,7 @@ impl State {
}
#[expect(clippy::unused_self)]
- fn build_help(&self, settings: &Settings, theme: &Theme) -> Paragraph<'_> {
+ fn build_help(&self, settings: &Settings) -> Paragraph<'_> {
match self.tab_index {
// search
0 => Paragraph::new(Text::from(Line::from(vec![
@@ -1129,16 +1123,16 @@ impl State {
_ => unreachable!("invalid tab index"),
}
- .style(Style::from_crossterm(theme.as_style(Meaning::Annotation)))
+ .style(Style::from_crossterm(style_annotation()))
.alignment(Alignment::Center)
}
- fn build_stats(&self, theme: &Theme) -> Paragraph<'_> {
+ fn build_stats(&self) -> Paragraph<'_> {
Paragraph::new(Text::from(Span::raw(format!(
"history count: {}",
self.history_count,
))))
- .style(Style::from_crossterm(theme.as_style(Meaning::Annotation)))
+ .style(Style::from_crossterm(style_annotation()))
.alignment(Alignment::Right)
}
@@ -1149,7 +1143,7 @@ impl State {
keymap_mode: KeymapMode,
now: &'a dyn Fn() -> OffsetDateTime,
indicator: &'a str,
- theme: &'a Theme,
+
history_highlighter: HistoryHighlighter<'a>,
show_numeric_shortcuts: bool,
columns: &'a [UiColumn],
@@ -1160,7 +1154,6 @@ impl State {
keymap_mode == KeymapMode::VimNormal,
now,
indicator,
- theme,
history_highlighter,
show_numeric_shortcuts,
columns,
@@ -1228,7 +1221,6 @@ impl State {
compactness: Compactness,
preview_width: u16,
chunk_width: usize,
- theme: &Theme,
) -> Paragraph<'_> {
let selected = self.results_state.selected();
let command = if results.is_empty() {
@@ -1264,8 +1256,7 @@ impl State {
.border_type(BorderType::Rounded)
.title(format!("{:─>width$}", "", width = chunk_width - 2)),
),
- _ => Paragraph::new(command)
- .style(Style::from_crossterm(theme.as_style(Meaning::Annotation))),
+ _ => Paragraph::new(command).style(Style::from_crossterm(style_annotation())),
}
}
}
@@ -1320,9 +1311,7 @@ impl Write for TerminalWriter {
/// Screen state captured from atuin pty-proxy's screen server.
#[cfg(unix)]
struct SavedScreen {
- #[expect(dead_code)]
rows: u16,
- #[expect(dead_code)]
cols: u16,
cursor_row: u16,
cursor_col: u16,
@@ -1555,7 +1544,6 @@ pub(crate) async fn history(
settings: &Settings,
mut db: ClientSqlite,
history_store: &HistoryStore,
- theme: &Theme,
) -> Result<String> {
let inline_height = if settings.shell_up_key_binding {
settings
@@ -1752,7 +1740,6 @@ pub(crate) async fn history(
stats.clone(),
inspecting.as_ref(),
settings,
- theme,
popup_mode,
);
})?;
@@ -1832,7 +1819,7 @@ pub(crate) async fn history(
terminal.clear()?;
}
terminal.draw(|f| {
- app.draw(f, &results, stats.clone(), inspecting.as_ref(), settings, theme, popup_mode);
+ app.draw(f, &results, stats.clone(), inspecting.as_ref(), settings, popup_mode);
})?;
},
r => {
@@ -1979,10 +1966,7 @@ pub(crate) async fn history(
// cli-clipboard only works on Windows, Mac, and Linux.
-#[cfg(all(
- feature = "clipboard",
- any(target_os = "windows", target_os = "macos", target_os = "linux")
-))]
+#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
fn set_clipboard(s: String) {
let mut ctx = arboard::Clipboard::new().unwrap();
ctx.set_text(s).unwrap();
@@ -1990,12 +1974,6 @@ fn set_clipboard(s: String) {
ctx.get_text().unwrap();
}
-#[cfg(not(all(
- feature = "clipboard",
- any(target_os = "windows", target_os = "macos", target_os = "linux")
-)))]
-fn set_clipboard(_s: String) {}
-
#[cfg(test)]
mod tests {
use crate::atuin_client::database::Context;
@@ -2018,7 +1996,7 @@ mod tests {
strategy: PreviewStrategy::Auto,
},
show_preview: true,
- ..Settings::utc()
+ ..Settings::now()
};
let settings_preview_auto_h2 = Settings {
@@ -2027,7 +2005,7 @@ mod tests {
},
show_preview: true,
max_preview_height: 2,
- ..Settings::utc()
+ ..Settings::now()
};
let settings_preview_h4 = Settings {
@@ -2036,7 +2014,7 @@ mod tests {
},
show_preview: true,
max_preview_height: 4,
- ..Settings::utc()
+ ..Settings::now()
};
let settings_preview_fixed = Settings {
@@ -2045,7 +2023,7 @@ mod tests {
},
show_preview: true,
max_preview_height: 15,
- ..Settings::utc()
+ ..Settings::now()
};
let cmd_60: History = History::capture()
@@ -2167,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::utc();
+ let settings = Settings::now();
let mut state = State {
history_count: 0,
results_state: ListState::default(),
@@ -2212,7 +2190,7 @@ mod tests {
use crate::atuin_client::settings::Keys;
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
- let mut settings = Settings::utc();
+ let mut settings = Settings::now();
settings.keys = Keys {
scroll_exits: true,
exit_past_line_start: false,
@@ -2338,7 +2316,7 @@ mod tests {
fn test_vim_gg_multikey_sequence() {
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
- let settings = Settings::utc();
+ let settings = Settings::now();
let mut state = State {
history_count: 100,
@@ -2396,7 +2374,7 @@ mod tests {
fn test_vim_g_key_clears_on_other_input() {
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
- let settings = Settings::utc();
+ let settings = Settings::now();
let mut state = State {
history_count: 100,
@@ -2450,7 +2428,7 @@ mod tests {
fn test_vim_big_g_jump_to_bottom() {
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
- let settings = Settings::utc();
+ let settings = Settings::now();
let mut state = State {
history_count: 100,
@@ -2500,7 +2478,7 @@ mod tests {
fn test_vim_ctrl_u_d_half_page_scroll() {
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
- let settings = Settings::utc();
+ let settings = Settings::now();
let mut state = State {
history_count: 100,
@@ -2559,7 +2537,7 @@ mod tests {
fn test_vim_ctrl_f_b_full_page_scroll() {
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
- let settings = Settings::utc();
+ let settings = Settings::now();
let mut state = State {
history_count: 100,
@@ -2620,7 +2598,7 @@ mod tests {
/// Helper to build a State for executor tests.
fn make_executor_state(results_len: usize, selected: usize) -> State {
- let settings = Settings::utc();
+ let settings = Settings::now();
let mut state = State {
history_count: results_len as i64,
results_state: ListState::default(),
@@ -2664,7 +2642,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 50);
- let settings = Settings::utc();
+ let settings = Settings::now();
let result = state.execute_action(&Action::SelectNext, &settings);
assert!(matches!(result, super::InputAction::Continue));
// Non-inverted: SelectNext = scroll_down = selected - 1
@@ -2676,7 +2654,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 50);
- let mut settings = Settings::utc();
+ let mut settings = Settings::now();
settings.invert = true;
let result = state.execute_action(&Action::SelectNext, &settings);
assert!(matches!(result, super::InputAction::Continue));
@@ -2689,7 +2667,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 50);
- let settings = Settings::utc();
+ let settings = Settings::now();
let result = state.execute_action(&Action::SelectPrevious, &settings);
assert!(matches!(result, super::InputAction::Continue));
// Non-inverted: SelectPrevious = scroll_up = selected + 1
@@ -2701,7 +2679,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 0);
- let settings = Settings::utc();
+ let settings = Settings::now();
let result = state.execute_action(&Action::VimEnterNormal, &settings);
assert!(matches!(result, super::InputAction::Continue));
assert_eq!(state.keymap_mode, KeymapMode::VimNormal);
@@ -2713,7 +2691,7 @@ mod tests {
let mut state = make_executor_state(100, 0);
state.keymap_mode = KeymapMode::VimNormal;
- let settings = Settings::utc();
+ let settings = Settings::now();
let result = state.execute_action(&Action::VimEnterInsert, &settings);
assert!(matches!(result, super::InputAction::Continue));
assert_eq!(state.keymap_mode, KeymapMode::VimInsert);
@@ -2724,7 +2702,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 5);
- let mut settings = Settings::utc();
+ let mut settings = Settings::now();
settings.enter_accept = true;
let result = state.execute_action(&Action::Accept, &settings);
assert!(matches!(result, super::InputAction::Accept(5)));
@@ -2736,7 +2714,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 5);
- let settings = Settings::utc();
+ let settings = Settings::now();
let result = state.execute_action(&Action::ReturnSelection, &settings);
assert!(matches!(result, super::InputAction::Accept(5)));
assert!(!state.accept);
@@ -2747,7 +2725,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 5);
- let settings = Settings::utc();
+ let settings = Settings::now();
let result = state.execute_action(&Action::AcceptNth(3), &settings);
assert!(matches!(result, super::InputAction::Accept(8)));
}
@@ -2757,7 +2735,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 50);
- let settings = Settings::utc();
+ let settings = Settings::now();
let result = state.execute_action(&Action::ScrollToTop, &settings);
assert!(matches!(result, super::InputAction::Continue));
// Non-inverted: visual top = highest index
@@ -2769,7 +2747,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 50);
- let mut settings = Settings::utc();
+ let mut settings = Settings::now();
settings.invert = true;
let result = state.execute_action(&Action::ScrollToTop, &settings);
assert!(matches!(result, super::InputAction::Continue));
@@ -2782,7 +2760,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 50);
- let settings = Settings::utc();
+ let settings = Settings::now();
let result = state.execute_action(&Action::ScrollToBottom, &settings);
assert!(matches!(result, super::InputAction::Continue));
// Non-inverted: visual bottom = index 0
@@ -2794,7 +2772,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 0);
- let settings = Settings::utc();
+ let settings = Settings::now();
assert_eq!(state.tab_index, 0);
state.execute_action(&Action::ToggleTab, &settings);
assert_eq!(state.tab_index, 1);
@@ -2807,7 +2785,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 0);
- let settings = Settings::utc();
+ let settings = Settings::now();
assert!(!state.prefix);
state.execute_action(&Action::EnterPrefixMode, &settings);
assert!(state.prefix);
@@ -2819,7 +2797,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 0);
- let mut settings = Settings::utc();
+ let mut settings = Settings::now();
settings.exit_mode = ExitMode::ReturnOriginal;
let result = state.execute_action(&Action::Exit, &settings);
@@ -2835,7 +2813,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 0);
- let settings = Settings::utc();
+ let settings = Settings::now();
let result = state.execute_action(&Action::ReturnOriginal, &settings);
assert!(matches!(result, super::InputAction::ReturnOriginal));
}
@@ -2845,7 +2823,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 7);
- let settings = Settings::utc();
+ let settings = Settings::now();
let result = state.execute_action(&Action::Copy, &settings);
assert!(matches!(result, super::InputAction::Copy(7)));
}
@@ -2855,7 +2833,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 7);
- let settings = Settings::utc();
+ let settings = Settings::now();
let result = state.execute_action(&Action::Delete, &settings);
assert!(matches!(result, super::InputAction::Delete(7)));
}
@@ -2865,7 +2843,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 7);
- let settings = Settings::utc();
+ let settings = Settings::now();
let result = state.execute_action(&Action::SwitchContext, &settings);
assert!(matches!(result, super::InputAction::SwitchContext(Some(7))));
}
@@ -2875,7 +2853,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 7);
- let settings = Settings::utc();
+ let settings = Settings::now();
let result = state.execute_action(&Action::ClearContext, &settings);
assert!(matches!(result, super::InputAction::SwitchContext(None)));
}
@@ -2885,7 +2863,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 50);
- let settings = Settings::utc();
+ let settings = Settings::now();
let result = state.execute_action(&Action::Noop, &settings);
assert!(matches!(result, super::InputAction::Continue));
assert_eq!(state.results_state.selected(), 50);
@@ -2897,7 +2875,7 @@ mod tests {
let mut state = make_executor_state(100, 5);
state.tab_index = 1;
- let settings = Settings::utc();
+ let settings = Settings::now();
let result = state.execute_action(&Action::Accept, &settings);
assert!(matches!(result, super::InputAction::AcceptInspecting));
}
@@ -2907,7 +2885,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 0);
- let settings = Settings::utc();
+ let settings = Settings::now();
let original_mode = state.search_mode;
let result = state.execute_action(&Action::CycleSearchMode, &settings);
assert!(matches!(result, super::InputAction::Continue));
@@ -2923,7 +2901,7 @@ mod tests {
state.search.input.insert('h');
state.search.input.insert('i');
state.keymap_mode = KeymapMode::VimNormal;
- let settings = Settings::utc();
+ let settings = Settings::now();
let result = state.execute_action(&Action::VimSearchInsert, &settings);
assert!(matches!(result, super::InputAction::Continue));
// Should clear input and switch to insert mode
@@ -2936,7 +2914,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 0);
- let settings = Settings::utc();
+ let settings = Settings::now();
// Insert some text
state.search.input.insert('h');
@@ -2968,7 +2946,7 @@ mod tests {
use crate::command::client::search::keybindings::Action;
let mut state = make_executor_state(100, 0);
- let settings = Settings::utc();
+ let settings = Settings::now();
// Insert "hello"
state.search.input.insert('h');
@@ -2992,7 +2970,7 @@ mod tests {
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::collections::HashMap;
- let mut settings = Settings::utc();
+ let mut settings = Settings::now();
// Configure tab to return-query
settings.keymap.emacs = HashMap::from([(
"tab".to_string(),
diff --git a/crates/turtle/src/command/client/stats.rs b/crates/turtle/src/command/client/stats.rs
index 17432bb2..9ea5e283 100644
--- a/crates/turtle/src/command/client/stats.rs
+++ b/crates/turtle/src/command/client/stats.rs
@@ -4,7 +4,7 @@ use interim::parse_date_string;
use time::{Duration, OffsetDateTime, Time};
use crate::atuin_client::database::ClientSqlite;
-use crate::atuin_client::{database::current_context, settings::Settings, theme::Theme};
+use crate::atuin_client::{database::current_context, settings::Settings};
use crate::atuin_history::stats::{compute, pretty_print};
@@ -36,12 +36,7 @@ pub(crate) struct Cmd {
}
impl Cmd {
- pub(crate) async fn run(
- &self,
- db: &ClientSqlite,
- settings: &Settings,
- theme: &Theme,
- ) -> Result<()> {
+ pub(crate) async fn run(&self, db: &ClientSqlite, settings: &Settings) -> Result<()> {
let context = current_context().await?;
let words = if self.period.is_empty() {
String::from("all")
@@ -79,7 +74,7 @@ impl Cmd {
let stats = compute(settings, &history, self.count, self.ngram_size);
if let Some(stats) = stats {
- pretty_print(stats, self.ngram_size, theme);
+ pretty_print(stats, self.ngram_size);
}
Ok(())
diff --git a/crates/turtle/src/command/client/store.rs b/crates/turtle/src/command/client/store.rs
index 347c4bee..bc57488d 100644
--- a/crates/turtle/src/command/client/store.rs
+++ b/crates/turtle/src/command/client/store.rs
@@ -2,18 +2,14 @@ use clap::Subcommand;
use eyre::Result;
use crate::atuin_client::{
- database::ClientSqlite, record::{sqlite_store::SqliteStore, store::Store}, settings::Settings
+ database::ClientSqlite, record::sqlite_store::SqliteStore, settings::Settings,
};
use itertools::Itertools;
use time::{OffsetDateTime, UtcOffset};
-#[cfg(feature = "sync")]
-mod push;
-
-#[cfg(feature = "sync")]
mod pull;
-
mod purge;
+mod push;
mod rebuild;
mod rekey;
mod verify;
@@ -37,11 +33,9 @@ pub(crate) enum Cmd {
Verify(verify::Verify),
/// Push all records to the remote sync server (one way sync)
- #[cfg(feature = "sync")]
Push(push::Push),
/// Pull records from the remote sync server (one way sync)
- #[cfg(feature = "sync")]
Pull(pull::Pull),
}
@@ -58,11 +52,7 @@ impl Cmd {
Self::Rekey(rekey) => rekey.run(settings, store).await,
Self::Verify(verify) => verify.run(settings, store).await,
Self::Purge(purge) => purge.run(settings, store).await,
-
- #[cfg(feature = "sync")]
Self::Push(push) => push.run(settings, store).await,
-
- #[cfg(feature = "sync")]
Self::Pull(pull) => pull.run(settings, store, database).await,
}
}
diff --git a/crates/turtle/src/command/client/store/pull.rs b/crates/turtle/src/command/client/store/pull.rs
index f2e628d6..3a0865be 100644
--- a/crates/turtle/src/command/client/store/pull.rs
+++ b/crates/turtle/src/command/client/store/pull.rs
@@ -2,7 +2,13 @@ use clap::Args;
use eyre::Result;
use crate::atuin_client::{
- database::ClientSqlite, encryption::load_key, record::{sqlite_store::SqliteStore, store::Store, sync::{self, Operation}}, settings::Settings
+ database::ClientSqlite,
+ encryption::load_key,
+ record::{
+ sqlite_store::SqliteStore,
+ sync::{self, Operation},
+ },
+ settings::Settings,
};
#[derive(Args, Debug)]
@@ -42,7 +48,7 @@ impl Pull {
// 3. Filter operations by
// a) are they a download op?
// b) are they for the host/tag we are pushing here?
- let client = sync::build_client(settings).await?;
+ let client = sync::build_client(settings)?;
let (diff, remote_index) = sync::diff(&client, &store).await?;
// Skip on --force: local was already wiped above, mismatch is the user's call.
@@ -53,7 +59,7 @@ impl Pull {
.map_err(crate::print_error::format_sync_error)?;
}
- let operations = sync::operations(diff, &store).await?;
+ let operations = sync::operations(diff, &store)?;
let operations = operations
.into_iter()
diff --git a/crates/turtle/src/command/client/store/purge.rs b/crates/turtle/src/command/client/store/purge.rs
index 3ed55787..a23f1886 100644
--- a/crates/turtle/src/command/client/store/purge.rs
+++ b/crates/turtle/src/command/client/store/purge.rs
@@ -2,9 +2,7 @@ use clap::Args;
use eyre::Result;
use crate::atuin_client::{
- encryption::load_key,
- record::{sqlite_store::SqliteStore, store::Store},
- settings::Settings,
+ encryption::load_key, record::sqlite_store::SqliteStore, settings::Settings,
};
#[derive(Args, Debug)]
diff --git a/crates/turtle/src/command/client/store/push.rs b/crates/turtle/src/command/client/store/push.rs
index beec613c..9d66b5b2 100644
--- a/crates/turtle/src/command/client/store/push.rs
+++ b/crates/turtle/src/command/client/store/push.rs
@@ -60,7 +60,7 @@ impl Push {
// 3. Filter operations by
// a) are they an upload op?
// b) are they for the host/tag we are pushing here?
- let client = sync::build_client(settings).await?;
+ let client = sync::build_client(settings)?;
let (diff, remote_index) = sync::diff(&client, &store).await?;
// Skip on --force: that path intentionally replaces remote with local.
@@ -71,7 +71,7 @@ impl Push {
.map_err(crate::print_error::format_sync_error)?;
}
- let operations = sync::operations(diff, &store).await?;
+ let operations = sync::operations(diff, &store)?;
let operations = operations
.into_iter()
diff --git a/crates/turtle/src/command/client/store/rebuild.rs b/crates/turtle/src/command/client/store/rebuild.rs
index bee1aa05..6be67cd0 100644
--- a/crates/turtle/src/command/client/store/rebuild.rs
+++ b/crates/turtle/src/command/client/store/rebuild.rs
@@ -1,7 +1,6 @@
use clap::Args;
use eyre::{Result, bail};
-#[cfg(feature = "daemon")]
use crate::command::client::daemon as daemon_cmd;
use crate::atuin_client::{
@@ -50,7 +49,6 @@ impl Rebuild {
history_store.build(database).await?;
- #[cfg(feature = "daemon")]
daemon_cmd::emit_event(settings, crate::atuin_daemon::DaemonEvent::HistoryRebuilt).await;
Ok(())
diff --git a/crates/turtle/src/command/client/store/rekey.rs b/crates/turtle/src/command/client/store/rekey.rs
index b99fb16a..e89d83c2 100644
--- a/crates/turtle/src/command/client/store/rekey.rs
+++ b/crates/turtle/src/command/client/store/rekey.rs
@@ -5,7 +5,6 @@ use tokio::{fs::File, io::AsyncWriteExt};
use crate::atuin_client::{
encryption::{decode_key, generate_encoded_key, load_key},
record::sqlite_store::SqliteStore,
- record::store::Store,
settings::Settings,
};
diff --git a/crates/turtle/src/command/client/store/verify.rs b/crates/turtle/src/command/client/store/verify.rs
index e91addcf..a39227f9 100644
--- a/crates/turtle/src/command/client/store/verify.rs
+++ b/crates/turtle/src/command/client/store/verify.rs
@@ -2,9 +2,7 @@ use clap::Args;
use eyre::Result;
use crate::atuin_client::{
- encryption::load_key,
- record::{sqlite_store::SqliteStore, store::Store},
- settings::Settings,
+ encryption::load_key, record::sqlite_store::SqliteStore, settings::Settings,
};
#[derive(Args, Debug)]
diff --git a/crates/turtle/src/command/client/sync.rs b/crates/turtle/src/command/client/sync.rs
index 84b74cc1..c29a82fc 100644
--- a/crates/turtle/src/command/client/sync.rs
+++ b/crates/turtle/src/command/client/sync.rs
@@ -4,7 +4,11 @@ use serde_json::json;
use crate::{
atuin_client::{
- database::ClientSqlite, encryption, history::store::HistoryStore, record::{sqlite_store::SqliteStore, store::Store, sync}, settings::Settings
+ database::ClientSqlite,
+ encryption,
+ history::store::HistoryStore,
+ record::{sqlite_store::SqliteStore, sync},
+ settings::Settings,
},
atuin_common::utils,
};
diff --git a/crates/turtle/src/command/client/wrapped.rs b/crates/turtle/src/command/client/wrapped.rs
index d502d3ec..2ce19bf7 100644
--- a/crates/turtle/src/command/client/wrapped.rs
+++ b/crates/turtle/src/command/client/wrapped.rs
@@ -4,7 +4,7 @@ use std::collections::{HashMap, HashSet};
use time::{Date, Duration, Month, OffsetDateTime, Time};
use crate::atuin_client::database::ClientSqlite;
-use crate::atuin_client::{settings::Settings, theme::Theme};
+use crate::atuin_client::settings::Settings;
use crate::atuin_history::stats::{Stats, compute};
@@ -31,7 +31,12 @@ impl WrappedStats {
.iter()
.filter(|(cmd, _)| {
let cmd = &cmd[0];
- cmd == "cd" || cmd == "ls" || cmd == "pwd" || cmd == "pushd" || cmd == "popd"
+ cmd == "cd"
+ || cmd == "ls"
+ || cmd == "ll"
+ || cmd == "pwd"
+ || cmd == "pushd"
+ || cmd == "popd"
})
.map(|(_, count)| count)
.sum();
@@ -267,12 +272,7 @@ fn print_fun_facts(wrapped_stats: &WrappedStats, stats: &Stats, year: i32) {
println!();
}
-pub(crate) async fn run(
- year: Option<i32>,
- db: &ClientSqlite,
- settings: &Settings,
- theme: &Theme,
-) -> Result<()> {
+pub(crate) async fn run(year: Option<i32>, db: &ClientSqlite, settings: &Settings) -> Result<()> {
let now = OffsetDateTime::now_utc().to_offset(settings.timezone.0);
let month = now.month();
@@ -318,7 +318,7 @@ pub(crate) async fn run(
);
println!("Your Top Commands:");
- crate::atuin_history::stats::pretty_print(stats.clone(), 1, theme);
+ crate::atuin_history::stats::pretty_print(stats.clone(), 1);
println!();
print_fun_facts(&wrapped_stats, &stats, year);
diff --git a/crates/turtle/src/command/mod.rs b/crates/turtle/src/command/mod.rs
index 308e1970..78de2d03 100644
--- a/crates/turtle/src/command/mod.rs
+++ b/crates/turtle/src/command/mod.rs
@@ -4,23 +4,18 @@ use eyre::Result;
#[cfg(not(windows))]
use rustix::{fs::Mode, process::umask};
-#[cfg(feature = "client")]
mod client;
-
mod contributors;
-
mod gen_completions;
#[derive(Subcommand)]
#[command(infer_subcommands = true)]
#[expect(clippy::large_enum_variant)]
pub(crate) enum AtuinCmd {
- #[cfg(feature = "client")]
#[command(flatten)]
Client(client::Cmd),
/// PTY proxy for atuin
- #[cfg(feature = "pty-proxy")]
#[command(alias = "hex")]
PtyProxy(crate::atuin_pty_proxy::PtyProxy),
@@ -44,10 +39,8 @@ impl AtuinCmd {
}
match self {
- #[cfg(feature = "client")]
Self::Client(client) => client.run(),
- #[cfg(feature = "pty-proxy")]
Self::PtyProxy(proxy) => {
run_pty_proxy(proxy);
Ok(())
@@ -66,16 +59,12 @@ impl AtuinCmd {
}
}
-#[cfg(all(feature = "pty-proxy", unix))]
+#[cfg(unix)]
fn run_pty_proxy(proxy: crate::atuin_pty_proxy::PtyProxy) {
- #[cfg(feature = "daemon")]
proxy.run(semantic_command_capture_sink());
-
- #[cfg(not(feature = "daemon"))]
- proxy.run(None);
}
-#[cfg(all(feature = "daemon", feature = "pty-proxy", unix))]
+#[cfg(unix)]
fn semantic_command_capture_sink() -> Option<crate::atuin_pty_proxy::CommandCaptureSink> {
use std::sync::mpsc;
use std::time::Duration;
diff --git a/crates/turtle/src/main.rs b/crates/turtle/src/main.rs
index fb3405be..664cf3a6 100644
--- a/crates/turtle/src/main.rs
+++ b/crates/turtle/src/main.rs
@@ -20,9 +20,7 @@ pub(crate) mod atuin_history;
pub(crate) mod atuin_pty_proxy;
pub(crate) mod atuin_server;
-#[cfg(feature = "sync")]
mod print_error;
-#[cfg(feature = "sync")]
mod sync;
const VERSION: &str = env!("CARGO_PKG_VERSION");