aboutsummaryrefslogtreecommitdiffstats
path: root/crates/client/src
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-24 17:56:36 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-24 17:56:36 +0200
commit9cdc575666faad4c162f4b4cf519bfdd1fd528dc (patch)
tree9287963ad11e237ca74edbd54161c03f3309927b /crates/client/src
parentchore: Last big refactoring (diff)
downloadatuin-9cdc575666faad4c162f4b4cf519bfdd1fd528dc.zip
feat: Finalize design for fish-shell integration
Diffstat (limited to 'crates/client/src')
-rw-r--r--crates/client/src/atuin_client/settings/mod.rs12
-rw-r--r--crates/client/src/atuin_history/stats.rs2
-rw-r--r--crates/client/src/command/client/daemon.rs2
-rw-r--r--crates/client/src/command/client/history/end.rs30
-rw-r--r--crates/client/src/command/client/history/list.rs39
-rw-r--r--crates/client/src/command/client/history/mod.rs14
-rw-r--r--crates/client/src/command/client/history/start.rs18
-rw-r--r--crates/client/src/command/client/history/tail.rs29
-rw-r--r--crates/client/src/command/client/info.rs2
-rw-r--r--crates/client/src/command/client/stats.rs20
-rw-r--r--crates/client/src/command/client/sync.rs17
-rw-r--r--crates/client/src/command/client/wrapped.rs5
-rw-r--r--crates/client/src/main.rs14
13 files changed, 134 insertions, 70 deletions
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}");
}