diff options
| author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-07-24 17:56:36 +0200 |
|---|---|---|
| committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-07-24 17:56:36 +0200 |
| commit | 9cdc575666faad4c162f4b4cf519bfdd1fd528dc (patch) | |
| tree | 9287963ad11e237ca74edbd54161c03f3309927b /crates/turtle | |
| parent | chore: Last big refactoring (diff) | |
| download | atuin-9cdc575666faad4c162f4b4cf519bfdd1fd528dc.zip | |
feat: Finalize design for fish-shell integration
Diffstat (limited to '')
| -rw-r--r-- | crates/turtle/Cargo.toml | 3 | ||||
| -rw-r--r-- | crates/turtle/build.rs | 3 | ||||
| -rw-r--r-- | crates/turtle/proto/control.proto | 4 | ||||
| -rw-r--r-- | crates/turtle/proto/history.proto | 34 | ||||
| -rw-r--r-- | crates/turtle/src/client/mod.rs | 108 | ||||
| -rw-r--r-- | crates/turtle/src/history/builder.rs | 11 | ||||
| -rw-r--r-- | crates/turtle/src/history/mod.rs | 17 | ||||
| -rw-r--r-- | crates/turtle/src/lib.rs | 9 |
8 files changed, 148 insertions, 41 deletions
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; |
