diff options
Diffstat (limited to '')
36 files changed, 485 insertions, 219 deletions
@@ -143,37 +143,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] -name = "atuin" -version = "18.16.1" -dependencies = [ - "clap", - "clap_complete", - "clap_complete_nushell", - "colored", - "config", - "crossterm", - "eyre", - "fs-err", - "interim", - "log", - "regex", - "rustix", - "serde", - "serde_json", - "serde_regex", - "serde_with", - "shellexpand", - "time", - "tokio", - "toml_edit", - "tracing", - "tracing-subscriber", - "turtle", - "turtle-common", - "unicode-segmentation", -] - -[[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3747,6 +3716,37 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" name = "turtle" version = "18.16.1" dependencies = [ + "clap", + "clap_complete", + "clap_complete_nushell", + "colored", + "config", + "crossterm", + "eyre", + "fs-err", + "interim", + "log", + "regex", + "rustix", + "serde", + "serde_json", + "serde_regex", + "serde_with", + "shellexpand", + "time", + "tokio", + "toml_edit", + "tracing", + "tracing-subscriber", + "turtle-api", + "turtle-common", + "unicode-segmentation", +] + +[[package]] +name = "turtle-api" +version = "18.16.1" +dependencies = [ "eyre", "hyper-util", "prost", @@ -3761,6 +3761,7 @@ dependencies = [ "tower", "turtle-common", "typed-builder", + "uuid", ] [[package]] @@ -3813,7 +3814,7 @@ dependencies = [ "tonic", "tracing", "tracing-subscriber", - "turtle", + "turtle-api", "turtle-common", "uuid", ] @@ -18,7 +18,7 @@ repository = "https://github.com/atuinsh/atuin" readme = "README.md" [workspace.dependencies] -turtle = { path = "crates/turtle" } +turtle-api = { path = "crates/turtle" } turtle-common = { path = "crates/common" } turtle-daemon = { path = "crates/daemon" } diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 93599cd6..10ed1001 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "atuin" +name = "turtle" edition = "2024" description = "atuin - magical shell history" readme = "./README.md" @@ -34,7 +34,7 @@ tokio = { workspace = true } toml_edit = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } -turtle = { workspace = true } +turtle-api = { workspace = true } turtle-common = { workspace = true } unicode-segmentation = { workspace = true } diff --git a/crates/client/src/atuin_client/settings/mod.rs b/crates/client/src/atuin_client/settings/mod.rs index 5a3a1525..21ac3778 100644 --- a/crates/client/src/atuin_client/settings/mod.rs +++ b/crates/client/src/atuin_client/settings/mod.rs @@ -187,9 +187,8 @@ impl Settings { let record_store_path = data_dir.join("records.db"); let kv_path = data_dir.join("kv.db"); let scripts_path = data_dir.join("scripts.db"); - let socket_path = utils::runtime_dir().join("atuin.sock"); + let socket_path = utils::daemon_socket_path(); let pidfile_path = data_dir.join("atuin-daemon.pid"); - let logs_dir = utils::logs_dir(); let key_path = data_dir.join("key"); @@ -254,7 +253,6 @@ impl Settings { .set_default("daemon.systemd_socket", false)? .set_default("daemon.tcp_port", 8889)? .set_default("logs.enabled", true)? - .set_default("logs.dir", logs_dir.to_str())? .set_default("logs.level", "info")? .set_default("logs.search.file", "search.log")? .set_default("logs.daemon.file", "daemon.log")? @@ -587,13 +585,7 @@ mod tests { scripts_db_path, custom_dir.join("scripts.db").to_str().unwrap() ); - assert_eq!( - daemon_socket_path, - turtle_common::utils::runtime_dir() - .join("atuin.sock") - .to_str() - .unwrap() - ); + assert_eq!( daemon_pidfile_path, custom_dir.join("atuin-daemon.pid").to_str().unwrap() diff --git a/crates/client/src/atuin_history/stats.rs b/crates/client/src/atuin_history/stats.rs index cf6671c5..40be14f9 100644 --- a/crates/client/src/atuin_history/stats.rs +++ b/crates/client/src/atuin_history/stats.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet}; use crossterm::style::{Color, ResetColor, SetAttribute, SetForegroundColor}; use serde::{Deserialize, Serialize}; -use turtle::history::History; +use turtle_api::history::History; use unicode_segmentation::UnicodeSegmentation; use crate::atuin_client::settings::Settings; diff --git a/crates/client/src/command/client/daemon.rs b/crates/client/src/command/client/daemon.rs index 08ce7a96..e9b0ea4c 100644 --- a/crates/client/src/command/client/daemon.rs +++ b/crates/client/src/command/client/daemon.rs @@ -1,7 +1,7 @@ use clap::Subcommand; use eyre::Result; -use turtle::client::{Probe, probe}; +use turtle_api::client::{Probe, probe}; use crate::atuin_client::settings::Settings; diff --git a/crates/client/src/command/client/history/end.rs b/crates/client/src/command/client/history/end.rs index 0e1e6b91..290f7697 100644 --- a/crates/client/src/command/client/history/end.rs +++ b/crates/client/src/command/client/history/end.rs @@ -1,28 +1,34 @@ +use std::time::Duration; + use crate::atuin_client::settings::Settings; use eyre::{Result, eyre}; -use turtle::client::{HistoryClient, daemon_matches_expected, daemon_mismatch_message}; +use turtle_api::client::{HistoryClient, daemon_matches_expected, daemon_mismatch_message}; pub(super) async fn handle( settings: &Settings, id: &str, exit: i64, - duration: Option<u64>, + duration: Option<Duration>, ) -> Result<()> { - end_history(settings, id.to_string(), duration.unwrap_or(0), exit).await?; + end_history( + settings, + id.to_string(), + duration.unwrap_or(Duration::ZERO), + exit, + ) + .await?; Ok(()) } -async fn end_history(settings: &Settings, id: String, duration: u64, exit: i64) -> Result<()> { - match async { - HistoryClient::new(settings.daemon.socket_path.clone()) - .await? - .end_history(id.clone(), duration, exit) - .await - } - .await - { +async fn end_history(settings: &Settings, id: String, duration: Duration, exit: i64) -> Result<()> { + let response = HistoryClient::new(settings.daemon.socket_path.clone()) + .await? + .end_history(id.clone(), duration, exit) + .await; + + match response { Ok(resp) => { if daemon_matches_expected(resp.protocol) { return Ok(()); diff --git a/crates/client/src/command/client/history/list.rs b/crates/client/src/command/client/history/list.rs new file mode 100644 index 00000000..0fc59f6a --- /dev/null +++ b/crates/client/src/command/client/history/list.rs @@ -0,0 +1,39 @@ +use std::{fmt::Display, time::Duration}; + +use crate::{atuin_client::settings::Settings, command::client::history::format_duration_into}; + +use eyre::Result; +use turtle_api::client::HistoryClient; + +pub(super) async fn handle(settings: &Settings) -> Result<()> { + const CSI: &str = "\x1b["; + const CSE: &str = "m"; + fn col(v: impl Display, num: u32) -> String { + format!("{CSI}{num}{CSE}{v}{CSI}0{CSE}") + } + + struct F(Duration); + impl Display for F { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + format_duration_into(self.0, f) + } + } + + let mut client = HistoryClient::new(settings.daemon.socket_path.clone()).await?; + + let hists = client.history(None).await?; + + for hist in hists { + println!( + "{}@{}: {} at {} for {} [{}]", + hist.author, + hist.hostname, + col(hist.command, 36), + col(hist.cwd, 32), + F(hist.duration), + col(hist.exit, 31) + ); + } + + Ok(()) +} diff --git a/crates/client/src/command/client/history/mod.rs b/crates/client/src/command/client/history/mod.rs index b6bfe6a1..f32ad83d 100644 --- a/crates/client/src/command/client/history/mod.rs +++ b/crates/client/src/command/client/history/mod.rs @@ -7,11 +7,12 @@ use std::{ use clap::Subcommand; use eyre::Result; use time::macros::format_description; -use turtle::history::History; +use turtle_api::history::History; use crate::atuin_client::settings::Settings; mod end; +mod list; mod start; mod tail; @@ -43,12 +44,16 @@ pub(crate) enum Cmd { #[arg(long, short)] exit: i64, + /// The duration this command ran, specified as nano seconds. #[arg(long, short)] duration: Option<u64>, }, /// Stream history events from the daemon as they are received Tail, + + /// Very basic listing of tracked history. + List, } impl Cmd { @@ -74,10 +79,11 @@ impl Cmd { Ok(()) } - Self::End { id, exit, duration } => end::handle(settings, &id, exit, duration).await, - Self::Tail => { - return tail::handle(settings).await; + Self::End { id, exit, duration } => { + end::handle(settings, &id, exit, duration.map(Duration::from_nanos)).await } + Self::Tail => tail::handle(settings).await, + Self::List => list::handle(settings).await, } } } diff --git a/crates/client/src/command/client/history/start.rs b/crates/client/src/command/client/history/start.rs index cb8f1b0c..c462755e 100644 --- a/crates/client/src/command/client/history/start.rs +++ b/crates/client/src/command/client/history/start.rs @@ -9,7 +9,7 @@ use crate::{ use eyre::{Result, eyre}; use time::OffsetDateTime; use tracing::debug; -use turtle::{ +use turtle_api::{ client::{HistoryClient, daemon_matches_expected, daemon_mismatch_message}, history::{History, SettingsFilter}, }; @@ -51,7 +51,7 @@ pub(super) async fn handle( Ok(id) => id, Err(e) => { debug!("failed to start history via daemon: {e}"); - h.id.0.clone() + h.id.to_string() } }; @@ -59,14 +59,12 @@ pub(super) async fn handle( } async fn start_history(settings: &Settings, history: History) -> Result<String> { - match async { - HistoryClient::new(settings.daemon.socket_path.clone()) - .await? - .start_history(history.clone()) - .await - } - .await - { + let response = HistoryClient::new(settings.daemon.socket_path.clone()) + .await? + .start_history(history.clone()) + .await; + + match response { Ok(resp) => { if daemon_matches_expected(resp.protocol) { return Ok(resp.id); diff --git a/crates/client/src/command/client/history/tail.rs b/crates/client/src/command/client/history/tail.rs index 212d7fcd..2cad5dd6 100644 --- a/crates/client/src/command/client/history/tail.rs +++ b/crates/client/src/command/client/history/tail.rs @@ -7,7 +7,7 @@ use colored::Colorize; use eyre::{Context, Result, bail}; use serde::Serialize; use time::OffsetDateTime; -use turtle::{ +use turtle_api::{ client::{ HistoryClient, HistoryEventKind, Probe, TailHistoryReply, history_entry_to_history, probe, }, @@ -56,9 +56,7 @@ struct TailJsonHistory<'a> { #[serde(skip_serializing_if = "Option::is_none")] exit: Option<i64>, #[serde(skip_serializing_if = "Option::is_none")] - duration_ns: Option<i64>, - #[serde(skip_serializing_if = "Option::is_none")] - duration: Option<String>, + duration: Option<Duration>, #[serde(skip_serializing_if = "Option::is_none")] success: Option<bool>, #[serde(skip_serializing_if = "Option::is_none")] @@ -98,7 +96,7 @@ impl TailEvent { let payload = TailJsonEvent { event: self.kind.as_str(), history: TailJsonHistory { - id: &self.history.id.0, + id: &self.history.id.to_string(), timestamp: format_history_time(self.history.timestamp, tz)?, timestamp_unix_ns: u64::try_from(self.history.timestamp.unix_timestamp_nanos()) .context("history timestamp predates unix epoch")?, @@ -111,8 +109,7 @@ impl TailEvent { author: &self.history.author, intent: self.history.intent.as_deref(), exit: self.exit_value(), - duration_ns: self.duration_value(), - duration: self.duration_value().map(format_duration_ns), + duration: self.duration_value(), success: self.success_value(), finished_at: self .finished_at() @@ -158,7 +155,7 @@ impl TailEvent { &format_history_time(self.history.timestamp, tz) .unwrap_or_else(|_| "invalid".to_owned()), ); - push_pretty_field(&mut out, "history", &self.history.id.0); + push_pretty_field(&mut out, "history", &self.history.id.to_string()); push_pretty_field(&mut out, "session", &self.history.session); push_pretty_field(&mut out, "exit", &self.exit_display()); push_pretty_field(&mut out, "duration", &self.duration_display()); @@ -204,7 +201,7 @@ impl TailEvent { matches!(self.kind, TailKind::Ended).then_some(self.history.exit) } - fn duration_value(&self) -> Option<i64> { + fn duration_value(&self) -> Option<Duration> { matches!(self.kind, TailKind::Ended).then_some(self.history.duration) } @@ -214,8 +211,12 @@ impl TailEvent { fn finished_at(&self) -> Option<OffsetDateTime> { self.duration_value() - .filter(|duration| *duration >= 0) - .map(time::Duration::nanoseconds) + .filter(|duration| *duration >= Duration::ZERO) + .map(|d| { + time::Duration::nanoseconds_i128( + i128::try_from(d.as_nanos()).expect("to be small enough"), + ) + }) .and_then(|duration| self.history.timestamp.checked_add(duration)) } @@ -229,7 +230,7 @@ impl TailEvent { fn duration_display(&self) -> String { match self.duration_value() { - Some(duration) if duration >= 0 => format_duration_ns(duration), + Some(duration) if duration >= Duration::ZERO => format_duration_ns(duration), Some(_) => "unknown".bright_yellow().to_string(), None => "running".bright_yellow().to_string(), } @@ -274,7 +275,7 @@ fn push_pretty_field(out: &mut String, label: &str, value: &str) { } } -fn format_duration_ns(duration_ns: i64) -> String { +fn format_duration_ns(duration: Duration) -> String { struct F(Duration); impl Display for F { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -282,7 +283,7 @@ fn format_duration_ns(duration_ns: i64) -> String { } } - F(Duration::from_nanos(duration_ns.max(0).cast_unsigned())).to_string() + F(duration).to_string() } fn format_history_time(timestamp: OffsetDateTime, tz: Timezone) -> Result<String> { diff --git a/crates/client/src/command/client/info.rs b/crates/client/src/command/client/info.rs index c4839c38..1af8ee39 100644 --- a/crates/client/src/command/client/info.rs +++ b/crates/client/src/command/client/info.rs @@ -2,7 +2,7 @@ use crate::atuin_client::settings::Settings; use crate::{SHA, VERSION};
use eyre::Result;
-use turtle::client::ControlClient;
+use turtle_api::client::ControlClient;
pub(crate) async fn run(settings: &Settings) -> Result<()> {
let config = turtle_common::utils::config_dir();
diff --git a/crates/client/src/command/client/stats.rs b/crates/client/src/command/client/stats.rs index 88734bb6..9f74ecc3 100644 --- a/crates/client/src/command/client/stats.rs +++ b/crates/client/src/command/client/stats.rs @@ -2,12 +2,11 @@ use clap::Parser; use eyre::Result; use interim::{Dialect, parse_date_string}; use time::{Duration, OffsetDateTime, Time}; -use turtle::client::{HistoryClient, Range}; +use turtle_api::client::{HistoryClient, Range}; use crate::atuin_client::settings::Settings; use crate::atuin_history::stats::{compute, pretty_print}; -use crate::command::current_session; fn parse_ngram_size(s: &str) -> Result<usize, String> { let value = s @@ -38,7 +37,6 @@ pub(crate) struct Cmd { impl Cmd { pub(crate) async fn run(&self, settings: &Settings) -> Result<()> { - let session = current_session()?; let mut client = HistoryClient::new(settings.daemon.socket_path.clone()).await?; let words = if self.period.is_empty() { @@ -50,30 +48,32 @@ impl Cmd { let now = OffsetDateTime::now_utc().to_offset(settings.timezone.0); let last_night = now.replace_time(Time::MIDNIGHT); - let history = if words.as_str() == "all" { - client.history(session, None).await? + let range = if words.as_str() == "all" { + None } else if words.trim() == "today" { let start = last_night; let end = start + Duration::days(1); - client.history(session, Some(Range { start, end })).await? + Some(Range { start, end }) } else if words.trim() == "month" { let end = last_night; let start = end - Duration::days(31); - client.history(session, Some(Range { start, end })).await? + Some(Range { start, end }) } else if words.trim() == "week" { let end = last_night; let start = end - Duration::days(7); - client.history(session, Some(Range { start, end })).await? + Some(Range { start, end }) } else if words.trim() == "year" { let end = last_night; let start = end - Duration::days(365); - client.history(session, Some(Range { start, end })).await? + Some(Range { start, end }) } else { let start = parse_date_string(&words, now, Dialect::Uk)?; let end = start + Duration::days(1); - client.history(session, Some(Range { start, end })).await? + Some(Range { start, end }) }; + let history = client.history(range).await?; + let stats = compute(settings, &history, self.count, self.ngram_size); if let Some(stats) = stats { diff --git a/crates/client/src/command/client/sync.rs b/crates/client/src/command/client/sync.rs index 77ae97ba..7297c659 100644 --- a/crates/client/src/command/client/sync.rs +++ b/crates/client/src/command/client/sync.rs @@ -1,7 +1,8 @@ use clap::Subcommand; use eyre::{Result, bail}; -use turtle::client::{Probe, probe}; +use tracing::info; +use turtle_api::client::{Probe, probe}; use crate::atuin_client::settings::Settings; @@ -74,8 +75,18 @@ async fn perform_cmd(settings: &Settings) -> Result<()> { match probe(settings.daemon.socket_path.clone()).await { Probe::Ready(mut control_client) => { let reply = control_client.force_sync().await?; - if !reply.accepted { - bail!("Daemon refused to accept sync request"); + + match reply.error { + Some(err) => { + bail!("Daemon failed to sync: {err}"); + } + None => { + info!( + down = reply.downloaded, + up = reply.uploaded, + "Sync completed." + ); + } } } Probe::NeedsRestart(msg) => { diff --git a/crates/client/src/command/client/wrapped.rs b/crates/client/src/command/client/wrapped.rs index 8c15a5d3..a47b81a1 100644 --- a/crates/client/src/command/client/wrapped.rs +++ b/crates/client/src/command/client/wrapped.rs @@ -2,7 +2,7 @@ use crossterm::style::{ResetColor, SetAttribute}; use eyre::Result; use std::collections::{HashMap, HashSet}; use time::{Date, Duration, Month, OffsetDateTime, Time}; -use turtle::{ +use turtle_api::{ client::{HistoryClient, Range}, history::History, }; @@ -274,7 +274,6 @@ fn print_fun_facts(wrapped_stats: &WrappedStats, stats: &Stats, year: i32) { } pub(crate) async fn run(year: Option<i32>, settings: &Settings) -> Result<()> { - let session = current_session()?; let mut client = HistoryClient::new(settings.daemon.socket_path.clone()).await?; let now = OffsetDateTime::now_utc().to_offset(settings.timezone.0); @@ -300,7 +299,7 @@ pub(crate) async fn run(year: Option<i32>, settings: &Settings) -> Result<()> { now.offset(), ); - let history = client.history(session, Some(Range { start, end })).await?; + let history = client.history(Some(Range { start, end })).await?; if history.is_empty() { println!( "Your history for {year} is empty!\nMaybe 'atuin import' could help you import your previous history 🪄" diff --git a/crates/client/src/main.rs b/crates/client/src/main.rs index 05f328e2..4ec97cef 100644 --- a/crates/client/src/main.rs +++ b/crates/client/src/main.rs @@ -11,6 +11,7 @@ use clap::builder::styling::{AnsiColor, Effects}; use eyre::Result; use command::AtuinCmd; +use tracing_subscriber::EnvFilter; use tracing_subscriber::util::SubscriberInitExt; mod command; @@ -60,7 +61,18 @@ impl Atuin { } fn main() -> Result<()> { - if let Err(e) = tracing_subscriber::registry().try_init() { + if let Err(e) = tracing_subscriber::fmt() + .with_file(true) + .with_line_number(true) + .with_level(true) + .without_time() + .with_env_filter( + EnvFilter::builder() + .from_env_lossy() + .add_directive("turtle=debug".parse().unwrap()), + ) + .try_init() + { eprintln!("failed to initialize logging: {e}"); } diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs index 7f46f223..b50328a9 100644 --- a/crates/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -91,33 +91,36 @@ pub fn in_git_repo(path: &str) -> Option<PathBuf> { // I don't want to use ProjectDirs, it puts config in awkward places on // mac. Data too. Seems to be more intended for GUI apps. +#[must_use] pub fn home_dir() -> PathBuf { directories::BaseDirs::new() .map(|d| d.home_dir().to_path_buf()) .expect("could not determine home directory") } +#[must_use] pub fn config_dir() -> PathBuf { let config_dir = env::var("XDG_CONFIG_HOME").map_or_else(|_| home_dir().join(".config"), PathBuf::from); - config_dir.join("atuin") + config_dir.join("turtle") } +#[must_use] pub fn data_dir() -> PathBuf { let data_dir = env::var("XDG_DATA_HOME") .map_or_else(|_| home_dir().join(".local").join("share"), PathBuf::from); - data_dir.join("atuin") + data_dir.join("turtle") } #[must_use] -pub fn runtime_dir() -> PathBuf { - env::var("XDG_RUNTIME_DIR").map_or_else(|_| data_dir(), PathBuf::from) +pub fn daemon_socket_path() -> PathBuf { + runtime_dir().join("turtle.sock") } #[must_use] -pub fn logs_dir() -> PathBuf { - home_dir().join(".atuin").join("logs") +pub fn runtime_dir() -> PathBuf { + env::var("XDG_RUNTIME_DIR").map_or_else(|_| data_dir(), PathBuf::from) } #[must_use] diff --git a/crates/daemon/Cargo.toml b/crates/daemon/Cargo.toml index cbc4c223..e514ee03 100644 --- a/crates/daemon/Cargo.toml +++ b/crates/daemon/Cargo.toml @@ -43,7 +43,7 @@ tokio-stream = { workspace = true } tonic = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } -turtle = { workspace = true } +turtle-api = { workspace = true } turtle-common = { workspace = true } uuid = { workspace = true } diff --git a/crates/daemon/src/aclient/database/mod.rs b/crates/daemon/src/aclient/database/mod.rs index b112b076..f24eb777 100644 --- a/crates/daemon/src/aclient/database/mod.rs +++ b/crates/daemon/src/aclient/database/mod.rs @@ -1,4 +1,4 @@ -use std::{path::Path, str::FromStr}; +use std::{path::Path, str::FromStr, time::Duration}; use fs_err::{self as fs}; use sql_builder::{SqlBuilder, SqlName}; @@ -8,7 +8,7 @@ use sqlx::{ }; use time::OffsetDateTime; use tracing::debug; -use turtle::history::{History, HistoryId}; +use turtle_api::history::{History, HistoryId}; use turtle_common::utils; use crate::aclient::utils::setup_db; @@ -59,9 +59,9 @@ impl ClientSqlite { "insert or ignore into history(id, timestamp, duration, exit, command, cwd, session, hostname, author, intent, deleted_at) values(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", ) - .bind(h.id.0.as_str()) + .bind(h.id.to_string().as_str()) .bind(h.timestamp.unix_timestamp_nanos() as i64) - .bind(h.duration) + .bind(h.duration.as_nanos() as i64) .bind(h.exit) .bind(h.command.as_str()) .bind(h.cwd.as_str()) @@ -81,7 +81,7 @@ impl ClientSqlite { id: HistoryId, ) -> Result<()> { sqlx::query("delete from history where id = ?1") - .bind(id.0.as_str()) + .bind(id.to_string().as_str()) .execute(&mut **tx) .await?; @@ -107,7 +107,9 @@ impl ClientSqlite { )) .unwrap(), ) - .duration(row.get("duration")) + .duration(Duration::from_nanos( + u64::try_from(row.get::<i64, _>("duration")).expect("to be small enough"), + )) .exit(row.get("exit")) .command(row.get("command")) .cwd(row.get("cwd")) diff --git a/crates/daemon/src/aclient/history/mod.rs b/crates/daemon/src/aclient/history/mod.rs index ae7654a6..35abc89d 100644 --- a/crates/daemon/src/aclient/history/mod.rs +++ b/crates/daemon/src/aclient/history/mod.rs @@ -1,7 +1,9 @@ +use std::time::Duration; + use rmp::decode::DecodeStringError; use rmp::decode::ValueReadError; use rmp::{Marker, decode::Bytes}; -use turtle::history::History; +use turtle_api::history::History; use turtle_common::record::DecryptedData; @@ -40,9 +42,12 @@ impl HistoryExt for History { let include_intent = self.intent.is_some(); encode::write_array_len(&mut output, 10 + u32::from(include_intent))?; - encode::write_str(&mut output, &self.id.0)?; + encode::write_str(&mut output, &self.id.to_string())?; encode::write_u64(&mut output, self.timestamp.unix_timestamp_nanos() as u64)?; - encode::write_sint(&mut output, self.duration)?; + encode::write_sint( + &mut output, + i64::try_from(self.duration.as_nanos()).expect("should be small enough"), + )?; encode::write_sint(&mut output, self.exit)?; encode::write_str(&mut output, &self.command)?; encode::write_str(&mut output, &self.cwd)?; @@ -107,7 +112,11 @@ impl HistoryExt for History { let mut bytes = Bytes::new(bytes); let timestamp = decode::read_u64(&mut bytes).map_err(error_report)?; - let duration = decode::read_int(&mut bytes).map_err(error_report)?; + let duration = decode::read_int(&mut bytes) + .map(|int: i64| { + Duration::from_nanos(u64::try_from(int).expect("should be small enough")) + }) + .map_err(error_report)?; let exit = decode::read_int(&mut bytes).map_err(error_report)?; let bytes = bytes.remaining_slice(); @@ -171,7 +180,9 @@ impl HistoryExt for History { let mut bytes = Bytes::new(bytes); let timestamp = decode::read_u64(&mut bytes).map_err(error_report)?; - let duration = decode::read_int(&mut bytes).map_err(error_report)?; + let duration = decode::read_int(&mut bytes) + .map(|int: i64| Duration::from_nanos(u64::try_from(int).expect("to be small enough"))) + .map_err(error_report)?; let exit = decode::read_int(&mut bytes).map_err(error_report)?; let bytes = bytes.remaining_slice(); @@ -228,6 +239,8 @@ impl HistoryExt for History { #[cfg(test)] mod tests { + use std::time::Duration; + use time::macros::datetime; use crate::aclient::history::{HISTORY_VERSION, HistoryExt}; @@ -239,7 +252,7 @@ mod tests { let history = History { id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned().into(), timestamp: datetime!(2023-05-28 18:35:40.633872 +00:00), - duration: 49_206_000, + duration: Duration::from_nanos(49_206_000), exit: 0, command: "git status".to_owned(), cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(), @@ -267,7 +280,7 @@ mod tests { let history = History { id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned().into(), timestamp: datetime!(2023-05-28 18:35:40.633872 +00:00), - duration: 49_206_000, + duration: Duration::from_nanos(49_206_000), exit: 0, command: "git status".to_owned(), cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(), @@ -291,7 +304,7 @@ mod tests { let history = History { id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned().into(), timestamp: datetime!(2023-05-28 18:35:40.633872 +00:00), - duration: 49_206_000, + duration: Duration::from_nanos(49_206_000), exit: 0, command: "git status".to_owned(), cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(), @@ -333,7 +346,7 @@ mod tests { let current = History { id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned().into(), timestamp: datetime!(2023-05-28 18:35:40.633872 +00:00), - duration: 49_206_000, + duration: Duration::from_nanos(49_206_000), exit: 0, command: "git status".to_owned(), cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(), diff --git a/crates/daemon/src/aclient/history/store.rs b/crates/daemon/src/aclient/history/store.rs index 952f2070..a6d6a627 100644 --- a/crates/daemon/src/aclient/history/store.rs +++ b/crates/daemon/src/aclient/history/store.rs @@ -1,6 +1,6 @@ use eyre::{Result, bail, eyre}; use rmp::decode::Bytes; -use turtle::history::{History, HistoryId}; +use turtle_api::history::{History, HistoryId}; use crate::aclient::{ database::ClientSqlite, @@ -56,7 +56,7 @@ impl HistoryRecord { Self::Delete(id) => { // 1 -> a history delete encode::write_u8(&mut output, 1)?; - encode::write_str(&mut output, id.0.as_str())?; + encode::write_str(&mut output, id.to_string().as_str())?; } } @@ -221,6 +221,8 @@ impl HistoryStore { #[cfg(test)] mod tests { + use std::time::Duration; + use time::macros::datetime; use turtle_common::record::DecryptedData; @@ -244,7 +246,7 @@ mod tests { let history = History { id: "018cd4fe81757cd2aee65cd7861f9c81".to_owned().into(), timestamp: datetime!(2024-01-04 00:00:00.000000 +00:00), - duration: 100, + duration: Duration::from_nanos(100), exit: 0, command: "ls".to_owned(), cwd: "/Users/ellie/src/github.com/atuinsh/atuin".to_owned(), diff --git a/crates/daemon/src/aclient/settings/mod.rs b/crates/daemon/src/aclient/settings/mod.rs index ef3f1dd0..379ee563 100644 --- a/crates/daemon/src/aclient/settings/mod.rs +++ b/crates/daemon/src/aclient/settings/mod.rs @@ -252,9 +252,8 @@ impl Settings { let kv_path = data_dir.join("kv.db"); let scripts_path = data_dir.join("scripts.db"); let ai_sessions_path = data_dir.join("ai_sessions.db"); - let socket_path = utils::runtime_dir().join("atuin.sock"); + let socket_path = utils::daemon_socket_path(); let pidfile_path = data_dir.join("atuin-daemon.pid"); - let logs_dir = utils::logs_dir(); let key_path = data_dir.join("key"); let meta_path = data_dir.join("meta.db"); @@ -320,7 +319,6 @@ impl Settings { .set_default("daemon.systemd_socket", false)? .set_default("daemon.tcp_port", 8889)? .set_default("logs.enabled", true)? - .set_default("logs.dir", logs_dir.to_str())? .set_default("logs.level", "info")? .set_default("logs.search.file", "search.log")? .set_default("logs.daemon.file", "daemon.log")? @@ -583,13 +581,6 @@ mod tests { ); assert_eq!(meta_db_path, custom_dir.join("meta.db").to_str().unwrap()); assert_eq!( - daemon_socket_path, - turtle_common::utils::runtime_dir() - .join("atuin.sock") - .to_str() - .unwrap() - ); - assert_eq!( daemon_pidfile_path, custom_dir.join("atuin-daemon.pid").to_str().unwrap() ); diff --git a/crates/daemon/src/api/control.rs b/crates/daemon/src/api/control.rs index ff19f593..8277f434 100644 --- a/crates/daemon/src/api/control.rs +++ b/crates/daemon/src/api/control.rs @@ -6,7 +6,7 @@ use tokio::time::{self, MissedTickBehavior}; use tonic::{Request, Response, Status}; use tracing::{Level, instrument}; -use turtle::generated::{ +use turtle_api::generated::{ DAEMON_PROTOCOL_VERSION, control::{ ForceSyncReply, ForceSyncRequest, PathsReply, PathsRequest, StatusReply, StatusRequest, @@ -91,9 +91,36 @@ impl Control for ControlService { &self, _request: Request<ForceSyncRequest>, ) -> Result<Response<ForceSyncReply>, Status> { - let reply = ForceSyncReply { accepted: true }; - self.handle.emit(DaemonEvent::ForceSync); + let event = self + .handle + .wait_for(|e| { + matches!( + e, + DaemonEvent::SyncFailed { .. } | DaemonEvent::SyncCompleted { .. } + ) + }) + .await + .map_err(|e| { + Status::internal(format!("failed to wait for sync response event: {e:?}")) + })?; + + let reply = match event { + DaemonEvent::SyncCompleted { + uploaded, + downloaded, + } => ForceSyncReply { + error: None, + uploaded: uploaded as u32, + downloaded: downloaded as u32, + }, + DaemonEvent::SyncFailed { error } => ForceSyncReply { + error: Some(error), + uploaded: 0, + downloaded: 0, + }, + _ => unreachable!(), + }; Ok(Response::new(reply)) } diff --git a/crates/daemon/src/api/history.rs b/crates/daemon/src/api/history.rs index 6165464d..0b373604 100644 --- a/crates/daemon/src/api/history.rs +++ b/crates/daemon/src/api/history.rs @@ -1,10 +1,10 @@ -use std::pin::Pin; +use std::{pin::Pin, time::Duration}; use dashmap::DashMap; use eyre::Result; use time::OffsetDateTime; use tokio_stream::Stream; -use tonic::{Request, Response, Status}; +use tonic::{IntoRequest, Request, Response, Status}; use tracing::{Level, instrument}; use crate::{ @@ -12,12 +12,16 @@ use crate::{ daemon::DaemonHandle, events::DaemonEvent, }; -use turtle::{ +use turtle_api::{ + client::{ + proto_duration_to_std, proto_timestamp_to_time, std_to_proto_duration, + time_to_proto_timestamp, + }, generated::{ DAEMON_PROTOCOL_VERSION, history::{ - EndHistoryReply, EndHistoryRequest, HistoryEntry, HistoryEventKind, HistoryReply, - HistoryRequest, StartHistoryReply, StartHistoryRequest, TailHistoryReply, + AddHistoryRequest, EndHistoryReply, EndHistoryRequest, HistoryEntry, HistoryEventKind, + HistoryReply, HistoryRequest, StartHistoryReply, StartHistoryRequest, TailHistoryReply, TailHistoryRequest, history_server::{History as HistorySvc, HistoryServer}, }, @@ -60,8 +64,8 @@ impl HistoryService { fn history_to_reply(history: History) -> HistoryEntry { HistoryEntry { - timestamp: history.timestamp.unix_timestamp_nanos() as u64, - id: history.id.0, + timestamp: time_to_proto_timestamp(history.timestamp), + id: history.id.to_string(), command: history.command, cwd: history.cwd, session: history.session, @@ -69,7 +73,7 @@ fn history_to_reply(history: History) -> HistoryEntry { author: history.author, intent: history.intent.unwrap_or_default(), exit: history.exit, - duration: history.duration, + duration: std_to_proto_duration(history.duration), } } @@ -85,8 +89,8 @@ impl HistorySvc for HistoryService { let req = request.into_inner(); let entries = if let Some(range) = req.range { - let from = OffsetDateTime::from_unix_timestamp(range.start as i64).unwrap(); - let to = OffsetDateTime::from_unix_timestamp(range.end as i64).unwrap(); + let from = proto_timestamp_to_time(range.start); + let to = proto_timestamp_to_time(range.end); self.handle.history_db().range(from, to).await } else { @@ -101,18 +105,41 @@ impl HistorySvc for HistoryService { } #[instrument(skip_all, level = Level::INFO)] + async fn add_history( + &self, + request: Request<AddHistoryRequest>, + ) -> Result<Response<EndHistoryReply>, Status> { + let req = request.into_inner(); + let start_req = req.start.expect("is some"); + + let start_response = self + .start_history(start_req.into_request()) + .await? + .into_inner(); + let end_responnse = self + .end_history( + EndHistoryRequest { + id: start_response.id, + exit: req.exit, + duration: req.duration, + } + .into_request(), + ) + .await?; + + Ok(end_responnse) + } + + #[instrument(skip_all, level = Level::INFO)] async fn start_history( &self, request: Request<StartHistoryRequest>, ) -> Result<Response<StartHistoryReply>, Status> { + tokio::time::sleep(Duration::from_secs(5)).await; + let req = request.into_inner(); - let timestamp = OffsetDateTime::from_unix_timestamp_nanos(i128::from(req.timestamp)) - .map_err(|_| { - Status::invalid_argument( - "failed to parse timestamp as unix time (expected nanos since epoch)", - ) - })?; + let timestamp = proto_timestamp_to_time(req.timestamp); let h: History = History::daemon() .timestamp(timestamp) @@ -121,7 +148,7 @@ impl HistorySvc for HistoryService { .session(req.session) .hostname(req.hostname) .author(req.author) - .intent(req.intent) + .intent(req.intent.unwrap_or_default()) .build() .into(); @@ -146,16 +173,15 @@ impl HistorySvc for HistoryService { request: Request<EndHistoryRequest>, ) -> Result<Response<EndHistoryReply>, Status> { let req = request.into_inner(); - let id = HistoryId(req.id); + let id = HistoryId::from(req.id); if let Some((_, mut history)) = self.running.remove(&id) { history.exit = req.exit; - history.duration = match req.duration { - 0 => i64::try_from( - (OffsetDateTime::now_utc() - history.timestamp).whole_nanoseconds(), - ) - .expect("failed to convert calculated duration to i64"), - value => i64::try_from(value).expect("failed to get i64 duration"), + history.duration = match proto_duration_to_std(req.duration) { + Duration::ZERO => Duration::from_nanos_u128( + (OffsetDateTime::now_utc() - history.timestamp).whole_nanoseconds() as u128, + ), + value => value, }; self.handle @@ -164,7 +190,11 @@ impl HistorySvc for HistoryService { .await .map_err(|e| Status::internal(format!("failed to write to db: {e:?}")))?; - tracing::info!(id = id.0, duration = history.duration, "end history"); + tracing::info!( + id = id.to_string(), + duration = history.duration.as_nanos(), + "end history" + ); let (record_id, idx) = self .history_store diff --git a/crates/daemon/src/daemon.rs b/crates/daemon/src/daemon.rs index 1c3afcde..70e65c1e 100644 --- a/crates/daemon/src/daemon.rs +++ b/crates/daemon/src/daemon.rs @@ -87,6 +87,20 @@ impl DaemonHandle { tracing::warn!("failed to emit event (no receivers?): {e}"); } } + pub(crate) async fn wait_for(&self, matches: fn(&DaemonEvent) -> bool) -> Result<DaemonEvent> { + let mut rx = self.subscribe(); + loop { + match rx.recv().await { + Ok(e) if matches(&e) => { + return Ok(e); + } + Err(err) => { + return Err(err).context("while waiting for events"); + } + Ok(_) => (), + } + } + } /// Subscribe to the event bus. /// diff --git a/crates/daemon/src/events.rs b/crates/daemon/src/events.rs index 3b6fa5d8..654e56cb 100644 --- a/crates/daemon/src/events.rs +++ b/crates/daemon/src/events.rs @@ -7,15 +7,14 @@ //! External processes (like CLI commands) can also inject events via the //! Control gRPC service. -use turtle::history::History; +use turtle_api::history::History; /// Events that flow through the daemon's event bus. /// /// Events are broadcast to all components. Each component decides which /// events it cares about in its `handle_event` implementation. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum DaemonEvent { - // ---- History lifecycle ---- /// A command has started running. HistoryStarted(History), @@ -25,11 +24,9 @@ pub(crate) enum DaemonEvent { /// Sync completed successfully. SyncCompleted { /// Number of records uploaded. - #[expect(unused)] uploaded: usize, /// Number of records downloaded. - #[expect(unused)] downloaded: usize, }, @@ -42,7 +39,6 @@ pub(crate) enum DaemonEvent { /// Request an immediate sync (external trigger). ForceSync, - // ---- Lifecycle ---- /// Request graceful shutdown of the daemon. ShutdownRequested, } diff --git a/crates/daemon/src/main.rs b/crates/daemon/src/main.rs index 9d5a5333..8fd3c119 100644 --- a/crates/daemon/src/main.rs +++ b/crates/daemon/src/main.rs @@ -14,7 +14,7 @@ use clap::Parser; use eyre::WrapErr; use eyre::{Result, bail}; use fs4::fs_std::FileExt; -use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::EnvFilter; use crate::{ aclient::{database::ClientSqlite, record::sqlite_store::SqliteStore, settings::Settings}, @@ -43,7 +43,18 @@ enum Cmd { #[tokio::main] async fn main() -> Result<()> { - if let Err(e) = tracing_subscriber::registry().try_init() { + if let Err(e) = tracing_subscriber::fmt() + .with_file(true) + .with_line_number(true) + .with_level(true) + .without_time() + .with_env_filter( + EnvFilter::builder() + .from_env_lossy() + .add_directive("turtle_daemon=debug".parse().unwrap()), + ) + .try_init() + { eprintln!("failed to initialize logging: {e}"); } diff --git a/crates/daemon/src/server.rs b/crates/daemon/src/server.rs index 6747f276..d3427769 100644 --- a/crates/daemon/src/server.rs +++ b/crates/daemon/src/server.rs @@ -2,7 +2,7 @@ use std::{os::unix::net::SocketAddr, path::PathBuf}; use eyre::Result; use eyre::{OptionExt, WrapErr}; -use turtle::generated::{ +use turtle_api::generated::{ control::control_server::ControlServer, history::history_server::HistoryServer, }; @@ -57,7 +57,12 @@ pub(crate) fn run_grpc_server( (UnixListener::from_std(listener)?, false) } else { tracing::info!("listening on unix socket {socket_path:?}"); - (UnixListener::bind(socket_path.clone())?, true) + ( + UnixListener::bind(socket_path.clone()).with_context(|| { + format!("Failed to bind to unix socket at: {socket_path}") + })?, + true, + ) }; let uds_stream = UnixListenerStream::new(uds); diff --git a/crates/turtle/Cargo.toml b/crates/turtle/Cargo.toml index d5397f4e..3ad7340f 100644 --- a/crates/turtle/Cargo.toml +++ b/crates/turtle/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "turtle" +name = "turtle-api" edition = "2024" description = "turtle - library for sqlite shell history" readme = "./README.md" @@ -12,6 +12,7 @@ homepage = { workspace = true } repository = { workspace = true } [dependencies] +uuid = { workspace = true } eyre = { workspace = true } hyper-util = { workspace = true } prost = { workspace = true } diff --git a/crates/turtle/build.rs b/crates/turtle/build.rs index 62612968..2946438c 100644 --- a/crates/turtle/build.rs +++ b/crates/turtle/build.rs @@ -28,5 +28,8 @@ fn main() -> Result<(), std::io::Error> { .skip_protoc_run() .compile_protos(&proto_paths, &proto_include_dirs)?; + println!("cargo::rerun-if-changed=proto/control.proto"); + println!("cargo::rerun-if-changed=proto/history.proto"); + Ok(()) } diff --git a/crates/turtle/proto/control.proto b/crates/turtle/proto/control.proto index a8026cb8..f1656d73 100644 --- a/crates/turtle/proto/control.proto +++ b/crates/turtle/proto/control.proto @@ -14,7 +14,9 @@ service Control { message ForceSyncRequest {} message ForceSyncReply { - bool accepted = 1; + optional string error = 1; + uint32 uploaded = 2; + uint32 downloaded = 3; } message StatusRequest {} diff --git a/crates/turtle/proto/history.proto b/crates/turtle/proto/history.proto index 850b16b9..90fcf55f 100644 --- a/crates/turtle/proto/history.proto +++ b/crates/turtle/proto/history.proto @@ -4,6 +4,7 @@ package history; service History { rpc StartHistory(StartHistoryRequest) returns (StartHistoryReply); rpc EndHistory(EndHistoryRequest) returns (EndHistoryReply); + rpc AddHistory(AddHistoryRequest) returns (EndHistoryReply); rpc TailHistory(TailHistoryRequest) returns (stream TailHistoryReply); @@ -11,14 +12,20 @@ service History { rpc History(HistoryRequest) returns (HistoryReply); } +message AddHistoryRequest { + StartHistoryRequest start = 1; + int64 exit = 2; + Duration duration = 3; +} + message StartHistoryRequest { - uint64 timestamp = 1; // nanosecond unix epoch + Timestamp timestamp = 1; string command = 2; string cwd = 3; string session = 4; string hostname = 5; string author = 6; - string intent = 7; + optional string intent = 7; } message StartHistoryReply { string id = 1; @@ -28,8 +35,9 @@ message StartHistoryReply { message EndHistoryRequest { string id = 1; + int64 exit = 2; - uint64 duration = 3; + Duration duration = 3; } message EndHistoryReply { string id = 1; @@ -52,7 +60,7 @@ enum HistoryEventKind { } message HistoryEntry { - uint64 timestamp = 1; // nanosecond unix epoch + Timestamp timestamp = 1; string id = 2; string command = 3; string cwd = 4; @@ -61,17 +69,25 @@ message HistoryEntry { string author = 7; string intent = 8; int64 exit = 9; - int64 duration = 10; + Duration duration = 10; } message Range { - uint64 start = 1; - uint64 end = 2; + Timestamp start = 1; + Timestamp end = 2; +} + +message Timestamp { + // Encoded as nanoseconds since the UNIX epoch. + uint64 value = 1; +} +message Duration { + // Encoded as nanoseconds. + uint64 value = 1; } message HistoryRequest { - string session = 1; - optional Range range = 2; + Range range = 2; } message HistoryReply { repeated HistoryEntry entries = 1; diff --git a/crates/turtle/src/client/mod.rs b/crates/turtle/src/client/mod.rs index ec97c994..a2322fdd 100644 --- a/crates/turtle/src/client/mod.rs +++ b/crates/turtle/src/client/mod.rs @@ -1,14 +1,13 @@ use eyre::{Context as EyreContext, Result}; -use time::OffsetDateTime; use tonic::Code; use tonic::transport::{Channel, Endpoint, Uri}; use tower::service_fn; use hyper_util::rt::TokioIo; -#[cfg(unix)] use tokio::net::UnixStream; +use crate::generated::history::AddHistoryRequest; use crate::generated::{ self, DAEMON_PROTOCOL_VERSION, control::{ @@ -34,14 +33,49 @@ fn normalize_optional_field(value: &str) -> Option<String> { } } +/// The protobuf compile (for some reason) supports not actually sending a request with all fields +/// (so every field is either fetched from the wire or set to a default). +/// For custom messages, there are no defaults and thus they get generated as `Option`s. +/// Our code will (obviously) never leave out a required (!) field in a message, and thus we can +/// just unwrap all the pointless options. +fn unwrap_proto_option<T>(field: Option<T>) -> T { + field.expect("should be some (see comment above)") +} + +#[must_use] +pub fn proto_duration_to_std(proto: Option<generated::history::Duration>) -> std::time::Duration { + std::time::Duration::from_nanos(unwrap_proto_option(proto).value) +} + +#[must_use] +pub fn proto_timestamp_to_time(proto: Option<generated::history::Timestamp>) -> OffsetDateTime { + OffsetDateTime::from_unix_timestamp_nanos(i128::from(unwrap_proto_option(proto).value)) + .expect("Daemon history timestamp should always be valid") +} + +#[must_use] +pub fn time_to_proto_timestamp(t: OffsetDateTime) -> Option<generated::history::Timestamp> { + Some(generated::history::Timestamp { + value: t.unix_timestamp_nanos() as u64, + }) +} + +#[must_use] +pub fn std_to_proto_duration(s: std::time::Duration) -> Option<generated::history::Duration> { + Some(generated::history::Duration { + value: s.as_nanos() as u64, + }) +} + +#[must_use] pub fn history_entry_to_history(entry: HistoryEntry) -> History { - let timestamp = OffsetDateTime::from_unix_timestamp_nanos(i128::from(entry.timestamp)) - .expect("Daemon history timestamp should always be valid"); + let timestamp = proto_timestamp_to_time(entry.timestamp); + let duration = proto_duration_to_std(entry.duration); History { id: entry.id.into(), timestamp, - duration: entry.duration, + duration, exit: entry.exit, command: entry.command, cwd: entry.cwd, @@ -135,12 +169,14 @@ pub struct Range { pub end: OffsetDateTime, } +pub use time::Duration; +pub use time::OffsetDateTime; + // Wrap the grpc client impl HistoryClient { #[cfg(unix)] pub async fn new(path: String) -> Result<Self> { use eyre::Context; - let log_path = path.clone(); let channel = Endpoint::try_from("http://atuin_local_daemon:0")? .connect_with_connector(service_fn(move |_: Uri| { @@ -169,20 +205,57 @@ impl HistoryClient { cwd: h.cwd, hostname: h.hostname, session: h.session, - timestamp: h.timestamp.unix_timestamp_nanos() as u64, + timestamp: time_to_proto_timestamp(h.timestamp), author: h.author, - intent: h.intent.unwrap_or_default(), + intent: h.intent, }; Ok(self.client.start_history(req).await?.into_inner()) } - pub async fn history(&mut self, session: String, range: Option<Range>) -> Result<Vec<History>> { + pub async fn end_history( + &mut self, + id: String, + duration: std::time::Duration, + exit: i64, + ) -> Result<EndHistoryReply> { + let req = EndHistoryRequest { + id, + exit, + duration: std_to_proto_duration(duration), + }; + + Ok(self.client.end_history(req).await?.into_inner()) + } + + pub async fn add_history( + &mut self, + h: History, + duration: std::time::Duration, + exit: i64, + ) -> Result<EndHistoryReply> { + let req = AddHistoryRequest { + start: Some(StartHistoryRequest { + timestamp: time_to_proto_timestamp(h.timestamp), + command: h.command, + cwd: h.cwd, + session: h.session, + hostname: h.hostname, + author: h.author, + intent: h.intent, + }), + exit, + duration: std_to_proto_duration(duration), + }; + + Ok(self.client.add_history(req).await?.into_inner()) + } + + pub async fn history(&mut self, range: Option<Range>) -> Result<Vec<History>> { let req = HistoryRequest { - session, range: range.map(|r| generated::history::Range { - start: r.start.unix_timestamp() as u64, - end: r.end.unix_timestamp() as u64, + start: time_to_proto_timestamp(r.start), + end: time_to_proto_timestamp(r.end), }), }; @@ -195,17 +268,6 @@ impl HistoryClient { .collect()) } - pub async fn end_history( - &mut self, - id: String, - duration: u64, - exit: i64, - ) -> Result<EndHistoryReply> { - let req = EndHistoryRequest { id, exit, duration }; - - Ok(self.client.end_history(req).await?.into_inner()) - } - pub async fn tail_history(&mut self) -> Result<tonic::Streaming<TailHistoryReply>> { Ok(self .client diff --git a/crates/turtle/src/history/builder.rs b/crates/turtle/src/history/builder.rs index 08d26f7f..57970301 100644 --- a/crates/turtle/src/history/builder.rs +++ b/crates/turtle/src/history/builder.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use typed_builder::TypedBuilder; use super::History; @@ -12,7 +14,7 @@ pub struct HistoryFromDb { command: String, cwd: String, exit: i64, - duration: i64, + duration: Duration, session: String, hostname: String, author: String, @@ -48,14 +50,19 @@ pub struct HistoryDaemonCapture { timestamp: time::OffsetDateTime, #[builder(setter(into))] command: String, + #[builder(setter(into))] cwd: String, + #[builder(setter(into))] session: String, + #[builder(setter(into))] hostname: String, + #[builder(default, setter(strip_option, into))] author: Option<String>, + #[builder(default, setter(strip_option, into))] intent: Option<String>, } @@ -67,7 +74,7 @@ impl From<HistoryDaemonCapture> for History { captured.command, captured.cwd, -1, - -1, + Duration::from_nanos(0), captured.session, captured.hostname, captured.author, diff --git a/crates/turtle/src/history/mod.rs b/crates/turtle/src/history/mod.rs index 27755bbe..3617bc54 100644 --- a/crates/turtle/src/history/mod.rs +++ b/crates/turtle/src/history/mod.rs @@ -2,6 +2,8 @@ use core::fmt::Formatter; use regex::RegexSet; use std::env; use std::fmt::Display; +use std::time::Duration; +use uuid::Uuid; use turtle_common::utils::uuid_v7; @@ -15,8 +17,8 @@ mod secrets; const HISTORY_AUTHOR_ENV: &str = "ATUIN_HISTORY_AUTHOR"; const HISTORY_INTENT_ENV: &str = "ATUIN_HISTORY_INTENT"; -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub struct HistoryId(pub String); +#[derive(Clone, Debug, Eq, PartialEq, Hash, Copy)] +pub struct HistoryId(Uuid); impl Display for HistoryId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { @@ -26,7 +28,12 @@ impl Display for HistoryId { impl From<String> for HistoryId { fn from(s: String) -> Self { - Self(s) + Self(Uuid::parse_str(&s).expect("should be a valid uuid")) + } +} +impl From<&str> for HistoryId { + fn from(s: &str) -> Self { + Self(Uuid::parse_str(s).expect("should be a valid uuid")) } } @@ -54,7 +61,7 @@ pub struct History { pub timestamp: OffsetDateTime, /// How long the command took to run. - pub duration: i64, + pub duration: Duration, /// The exit code of the command. pub exit: i64, @@ -106,7 +113,7 @@ impl History { command: String, cwd: String, exit: i64, - duration: i64, + duration: Duration, session: String, hostname: String, author: Option<String>, diff --git a/crates/turtle/src/lib.rs b/crates/turtle/src/lib.rs index fbee6761..31702837 100644 --- a/crates/turtle/src/lib.rs +++ b/crates/turtle/src/lib.rs @@ -1,3 +1,12 @@ +#![expect( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::missing_errors_doc, + clippy::missing_panics_doc +)] + pub mod client; pub mod generated; pub mod history; + +pub use uuid::Uuid; |
