diff options
| author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-06-12 17:16:19 +0200 |
|---|---|---|
| committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-06-12 17:16:19 +0200 |
| commit | 2ca7dd57b12861e8c9bbc9238cda612e0ff22ff3 (patch) | |
| tree | 302a644f6a50d60cc8304c4498fe6bbb72ddaaa9 | |
| parent | feat(server): Really make users stateless (with tests) (diff) | |
| download | atuin-2ca7dd57b12861e8c9bbc9238cda612e0ff22ff3.zip | |
chore(treewide): Cleanup themes
42 files changed, 413 insertions, 2257 deletions
@@ -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"); |
