aboutsummaryrefslogtreecommitdiffstats
path: root/crates/turtle/src/command/client/history.rs
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-06-12 17:16:19 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-06-12 17:16:19 +0200
commit2ca7dd57b12861e8c9bbc9238cda612e0ff22ff3 (patch)
tree302a644f6a50d60cc8304c4498fe6bbb72ddaaa9 /crates/turtle/src/command/client/history.rs
parentfeat(server): Really make users stateless (with tests) (diff)
downloadatuin-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.rs224
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)