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 /crates/turtle/src/command/client/history.rs | |
| parent | feat(server): Really make users stateless (with tests) (diff) | |
| download | atuin-2ca7dd57b12861e8c9bbc9238cda612e0ff22ff3.zip | |
chore(treewide): Cleanup themes
Diffstat (limited to 'crates/turtle/src/command/client/history.rs')
| -rw-r--r-- | crates/turtle/src/command/client/history.rs | 224 |
1 files changed, 8 insertions, 216 deletions
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) |
