diff options
| author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-07-20 23:00:06 +0200 |
|---|---|---|
| committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-07-20 23:00:06 +0200 |
| commit | 2f671f196e245ac1a791c001286e401a2fab9d3a (patch) | |
| tree | be90d8f16d5c8b65c6b77ebe1359d22e9738bbf6 | |
| parent | chore: Commit (diff) | |
| download | atuin-2f671f196e245ac1a791c001286e401a2fab9d3a.zip | |
chore: Commit
Diffstat (limited to '')
48 files changed, 396 insertions, 1894 deletions
@@ -189,9 +189,7 @@ dependencies = [ "fs-err", "interim", "log", - "protox", "regex", - "runtime-format", "rustix", "serde", "serde_json", @@ -201,12 +199,10 @@ dependencies = [ "time", "tokio", "toml_edit", - "tonic-prost-build", "tracing", "tracing-subscriber", "turtle", "turtle-common", - "turtle-daemon", "unicode-segmentation", ] @@ -4682,79 +4678,20 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" name = "turtle" version = "18.16.1" dependencies = [ - "arboard", - "async-trait", - "axum", - "base64", - "clap", - "clap_complete", - "clap_complete_nushell", - "colored", - "config", - "crossterm", - "crypto_secretbox", - "daemonize", - "dashmap", - "directories", "eyre", - "fs-err", - "fs4", - "fuzzy-matcher", "hyper-util", - "indicatif", - "interim", - "itertools", - "lasso", - "listenfd", - "log", - "metrics", - "metrics-exporter-prometheus", - "minspan", - "norm", - "notify", - "portable-pty", "prost", "protox", - "rand 0.8.5", - "ratatui", "regex", - "reqwest", - "rmp", - "runtime-format", - "rustix", - "rustls", - "rusty_paserk", - "rusty_paseto", - "semver", - "serde", - "serde_json", - "serde_regex", - "serde_with", - "shellexpand", - "signal-hook", - "sql-builder", "sqlx", - "thiserror 2.0.18", "time", "tokio", - "tokio-stream", - "toml_edit", "tonic", "tonic-prost", "tonic-prost-build", "tower", - "tower-http", - "tracing", - "tracing-appender", - "tracing-subscriber", "turtle-common", "typed-builder", - "unicode-segmentation", - "unicode-width 0.2.2", - "url", - "uuid", - "vt100", - "whoami 2.1.1", ] [[package]] @@ -4769,6 +4706,7 @@ dependencies = [ "time", "typed-builder", "uuid", + "whoami 2.1.1", ] [[package]] @@ -4848,7 +4786,6 @@ dependencies = [ "url", "uuid", "vt100", - "whoami 2.1.1", ] [[package]] @@ -4870,6 +4807,7 @@ dependencies = [ "tower", "tower-http", "tracing", + "tracing-subscriber", "turtle-common", "url", "uuid", diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 058f2f3a..4387a1c1 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -12,9 +12,6 @@ homepage = { workspace = true } repository = { workspace = true } [dependencies] -turtle-daemon = {workspace = true} -turtle-common = {workspace = true} -turtle = { workspace = true } clap = { version = "4.5.7", features = ["derive"] } clap_complete = "4.5.8" clap_complete_nushell = "4.5.4" @@ -26,7 +23,6 @@ fs-err = "3.1" interim = { version = "0.2.0", features = ["time_0_3"] } log = "0.4" regex = "1.10.5" -runtime-format = "0.1.3" rustix = { version = "1.1.4", features = ["process", "fs"] } serde = { version = "1.0.202", features = ["derive"] } serde_json = "1.0.119" @@ -38,14 +34,13 @@ tokio = { version = "1", features = ["full"] } toml_edit = "0.25.4" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["ansi", "fmt", "registry", "env-filter", "json"] } +turtle = { workspace = true } +turtle-common = {workspace = true} unicode-segmentation = "1.11.0" [dev-dependencies] -tokio = { version = "1", features = ["full"] } [build-dependencies] -protox = "0.9" -tonic-prost-build = "0.14" [package.metadata.docs.rs] all-features = true diff --git a/crates/client/build.rs b/crates/client/build.rs index 17a532cc..939d6405 100644 --- a/crates/client/build.rs +++ b/crates/client/build.rs @@ -1,20 +1,12 @@ use std::process::Command; -use std::{env, fs, path::PathBuf}; -use protox::Compiler; -use protox::prost::Message; +fn main() { + let output = Command::new("git").args(["rev-parse", "HEAD"]).output(); -fn main() -> Result<(), std::io::Error> { - { - let output = Command::new("git").args(["rev-parse", "HEAD"]).output(); + let sha = match output { + Ok(sha) => String::from_utf8(sha.stdout).unwrap(), + Err(_) => String::from("NO_GIT"), + }; - let sha = match output { - Ok(sha) => String::from_utf8(sha.stdout).unwrap(), - Err(_) => String::from("NO_GIT"), - }; - - println!("cargo:rustc-env=GIT_HASH={sha}"); - } - - Ok(()) + println!("cargo:rustc-env=GIT_HASH={sha}"); } diff --git a/crates/client/src/atuin_client/mod.rs b/crates/client/src/atuin_client/mod.rs index fa40d8dd..3f36722f 100644 --- a/crates/client/src/atuin_client/mod.rs +++ b/crates/client/src/atuin_client/mod.rs @@ -1,2 +1,16 @@ pub(crate) mod settings; -// pub(crate) mod theme; + +pub async fn current_context(session: String) -> eyre::Result<Context> { + let hostname = get_host_user(); + let cwd = utils::get_current_dir(); + let host_id = Settings::host_id().await?; + let git_root = utils::in_git_repo(cwd.as_str()); + + Ok(Context { + session, + hostname, + cwd, + git_root, + host_id: host_id.0.as_simple().to_string(), + }) +} diff --git a/crates/client/src/atuin_client/settings/mod.rs b/crates/client/src/atuin_client/settings/mod.rs index bcec25db..5a3a1525 100644 --- a/crates/client/src/atuin_client/settings/mod.rs +++ b/crates/client/src/atuin_client/settings/mod.rs @@ -15,35 +15,6 @@ use turtle_common::utils; static DATA_DIR: OnceLock<PathBuf> = OnceLock::new(); -#[derive(Clone, Debug, Deserialize, Copy, Serialize)] -pub(crate) enum ExitMode { - #[serde(rename = "return-original")] - ReturnOriginal, - - #[serde(rename = "return-query")] - ReturnQuery, -} - -// FIXME: Can use upstream Dialect enum if https://github.com/stevedonovan/chrono-english/pull/16 is merged -// FIXME: Above PR was merged, but dependency was changed to interim (fork of chrono-english) in the ... interim -#[derive(Clone, Debug, Deserialize, Copy, Serialize)] -pub(crate) enum Dialect { - #[serde(rename = "us")] - Us, - - #[serde(rename = "uk")] - Uk, -} - -impl From<Dialect> for interim::Dialect { - fn from(d: Dialect) -> Self { - match d { - Dialect::Uk => Self::Uk, - Dialect::Us => Self::Us, - } - } -} - /// Type wrapper around `time::UtcOffset` to support a wider variety of timezone formats. /// /// Note that the parsing of this struct needs to be done before starting any @@ -94,79 +65,19 @@ impl FromStr for Timezone { } } -#[derive(Clone, Debug, Deserialize, Copy, Serialize)] -pub(crate) enum Style { - #[serde(rename = "auto")] - Auto, - - #[serde(rename = "full")] - Full, - - #[serde(rename = "compact")] - Compact, -} - -#[derive(Clone, Debug, Deserialize, Copy, Serialize)] -pub(crate) enum WordJumpMode { - #[serde(rename = "emacs")] - Emacs, - - #[serde(rename = "subl")] - Subl, -} - -#[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum, Serialize)] -pub(crate) enum KeymapMode { - #[serde(rename = "emacs")] - Emacs, - - #[serde(rename = "vim-normal")] - VimNormal, - - #[serde(rename = "vim-insert")] - VimInsert, - - #[serde(rename = "auto")] - Auto, -} - -// We want to translate the config to crossterm::cursor::SetCursorStyle, but -// the original type does not implement trait serde::Deserialize unfortunately. -// It seems impossible to implement Deserialize for external types when it is -// used in HashMap (https://stackoverflow.com/questions/67142663). We instead -// define an adapter type. -#[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum, Serialize)] -pub(crate) enum CursorStyle { - #[serde(rename = "default")] - DefaultUserShape, - - #[serde(rename = "blink-block")] - BlinkingBlock, - - #[serde(rename = "steady-block")] - SteadyBlock, - - #[serde(rename = "blink-underline")] - BlinkingUnderScore, - - #[serde(rename = "steady-underline")] - SteadyUnderScore, - - #[serde(rename = "blink-bar")] - BlinkingBar, - - #[serde(rename = "steady-bar")] - SteadyBar, -} - #[derive(Clone, Debug, Deserialize, Serialize)] pub(crate) struct Stats { + /// sudo, etc. commands we want to strip off #[serde(default = "Stats::common_prefix_default")] - pub(crate) common_prefix: Vec<String>, // sudo, etc. commands we want to strip off + pub(crate) common_prefix: Vec<String>, + + /// kubectl, commands we should consider subcommands for #[serde(default = "Stats::common_subcommands_default")] - pub(crate) common_subcommands: Vec<String>, // kubectl, commands we should consider subcommands for + pub(crate) common_subcommands: Vec<String>, + + /// cd, ls, etc. commands we want to completely hide from stats #[serde(default = "Stats::ignored_commands_default")] - pub(crate) ignored_commands: Vec<String>, // cd, ls, etc. commands we want to completely hide from stats + pub(crate) ignored_commands: Vec<String>, } impl Stats { @@ -218,86 +129,7 @@ impl Default for Stats { } } -#[derive(Clone, Debug, Deserialize, Default, Serialize)] -#[expect(clippy::struct_excessive_bools)] -pub(crate) struct Keys { - pub(crate) scroll_exits: bool, - pub(crate) exit_past_line_start: bool, - pub(crate) accept_past_line_end: bool, - pub(crate) accept_past_line_start: bool, - pub(crate) accept_with_backspace: bool, - pub(crate) prefix: String, -} - -impl Keys { - /// The standard default values for all `[keys]` options. - /// These match the config defaults set in `builder_with_data_dir()`. - pub(crate) fn standard_defaults() -> Self { - Self { - scroll_exits: true, - exit_past_line_start: true, - accept_past_line_end: true, - accept_past_line_start: false, - accept_with_backspace: false, - prefix: "a".to_string(), - } - } -} - -/// A single rule within a conditional keybinding config. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub(crate) struct KeyRuleConfig { - /// Optional condition expression (e.g. "cursor-at-start", "input-empty && no-results"). - /// If absent, the rule always matches. - #[serde(default)] - pub(crate) when: Option<String>, - /// The action to perform (e.g. "exit", "cursor-left", "accept"). - pub(crate) action: String, -} - -/// A keybinding config value: either a simple action string or an ordered list of conditional rules. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(untagged)] -pub(crate) enum KeyBindingConfig { - /// Simple unconditional binding: `"ctrl-c" = "return-original"` - Simple(String), - /// Conditional binding: `"left" = [{ when = "cursor-at-start", action = "exit" }, { action = "cursor-left" }]` - Rules(Vec<KeyRuleConfig>), -} - -/// User-facing keymap configuration. Each mode maps key strings to bindings. -/// Keys present here override the defaults for that key; unmentioned keys keep defaults. #[derive(Clone, Debug, Deserialize, Serialize, Default)] -pub(crate) struct KeymapConfig { - #[serde(default)] - pub(crate) emacs: HashMap<String, KeyBindingConfig>, - #[serde(default, rename = "vim-normal")] - pub(crate) vim_normal: HashMap<String, KeyBindingConfig>, - #[serde(default, rename = "vim-insert")] - pub(crate) vim_insert: HashMap<String, KeyBindingConfig>, - #[serde(default)] - pub(crate) inspector: HashMap<String, KeyBindingConfig>, - #[serde(default)] - pub(crate) prefix: HashMap<String, KeyBindingConfig>, -} - -impl KeymapConfig { - /// Returns true if no keybinding overrides are configured in any mode. - pub(crate) fn is_empty(&self) -> bool { - self.emacs.is_empty() - && self.vim_normal.is_empty() - && self.vim_insert.is_empty() - && self.inspector.is_empty() - && self.prefix.is_empty() - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub(crate) struct Preview { - pub(crate) strategy: PreviewStrategy, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] pub(crate) struct Daemon { /// The path to the unix socket used by the daemon pub(crate) socket_path: String, @@ -306,104 +138,6 @@ pub(crate) struct Daemon { pub(crate) systemd_socket: bool, } -/// Log level for file logging. Maps to tracing's [`LevelFilter`]. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "lowercase")] -pub(crate) enum LogLevel { - Trace, - Debug, - #[default] - Info, - Warn, - Error, -} - -impl LogLevel { - /// Convert to a tracing directive string for use with [`EnvFilter`]. - pub(crate) fn as_directive(self) -> &'static str { - match self { - Self::Trace => "trace", - Self::Debug => "debug", - Self::Info => "info", - Self::Warn => "warn", - Self::Error => "error", - } - } -} - -/// Configuration for a specific log type (search or daemon). -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -pub(crate) struct LogConfig { - /// Log file name (relative to dir) or absolute path. - pub(crate) file: String, - - /// Override global enabled setting for this log type. - pub(crate) enabled: Option<bool>, - - /// Override global level setting for this log type. - pub(crate) level: Option<LogLevel>, - - /// Override global retention days setting for this log type. - pub(crate) retention: Option<u64>, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub(crate) struct Logs { - /// Enable file logging globally. Defaults to true. - #[serde(default = "Logs::default_enabled")] - pub(crate) enabled: bool, - - /// Directory for log files. Defaults to ~/.atuin/logs - pub(crate) dir: String, - - /// Default log level for file logging. Defaults to "info". - /// Note: [`ATUIN_LOG`] environment variable overrides this. - #[serde(default)] - pub(crate) level: LogLevel, - - /// Default retention days for log files. Defaults to 4. - #[serde(default = "Logs::default_retention")] - pub(crate) retention: u64, -} - -impl Default for Preview { - fn default() -> Self { - Self { - strategy: PreviewStrategy::Auto, - } - } -} - -impl Default for Daemon { - fn default() -> Self { - Self { - socket_path: String::new(), - systemd_socket: false, - } - } -} - -impl Default for Logs { - fn default() -> Self { - Self { - enabled: true, - dir: String::new(), - level: LogLevel::default(), - retention: Self::default_retention(), - } - } -} - -impl Logs { - fn default_enabled() -> bool { - true - } - - fn default_retention() -> u64 { - 4 - } -} - // The preview height strategy also takes max_preview_height into account. #[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum, Serialize)] pub(crate) enum PreviewStrategy { @@ -421,33 +155,11 @@ pub(crate) enum PreviewStrategy { } #[derive(Clone, Debug, Deserialize, Serialize)] -#[expect(clippy::struct_excessive_bools)] pub(crate) struct Settings { - pub(crate) data_dir: Option<String>, - pub(crate) dialect: Dialect, + data_dir: Option<String>, pub(crate) timezone: Timezone, - pub(crate) style: Style, - pub(crate) shell_up_key_binding: bool, - pub(crate) invert: bool, - pub(crate) show_preview: bool, - pub(crate) max_preview_height: u16, - pub(crate) show_help: bool, - pub(crate) show_tabs: bool, - pub(crate) show_numeric_shortcuts: bool, - pub(crate) auto_hide_height: u16, - pub(crate) exit_mode: ExitMode, - pub(crate) keymap_mode: KeymapMode, - pub(crate) keymap_mode_shell: KeymapMode, - pub(crate) keymap_cursor: HashMap<String, CursorStyle>, - pub(crate) word_jump_mode: WordJumpMode, - pub(crate) word_chars: String, - pub(crate) scroll_context_lines: usize, - pub(crate) history_format: String, pub(crate) strip_trailing_whitespace: bool, - pub(crate) prefers_reduced_motion: bool, - pub(crate) store_failed: bool, - pub(crate) no_mouse: bool, #[serde(with = "serde_regex", default = "RegexSet::empty", skip_serializing)] pub(crate) history_filter: RegexSet, @@ -456,37 +168,16 @@ pub(crate) struct Settings { pub(crate) cwd_filter: RegexSet, pub(crate) secrets_filter: bool, - pub(crate) workspaces: bool, - pub(crate) ctrl_n_shortcuts: bool, - - pub(crate) network_connect_timeout: u64, - pub(crate) network_timeout: u64, - pub(crate) local_timeout: f64, - pub(crate) enter_accept: bool, - pub(crate) smart_sort: bool, - pub(crate) command_chaining: bool, #[serde(default)] pub(crate) stats: Stats, #[serde(default)] - pub(crate) keys: Keys, - - #[serde(default)] - pub(crate) keymap: KeymapConfig, - - #[serde(default)] - pub(crate) preview: Preview, - - #[serde(default)] pub(crate) daemon: Daemon, - - #[serde(default)] - pub(crate) logs: Logs, } impl Settings { - pub(crate) fn builder() -> Result<ConfigBuilder<DefaultState>> { + fn builder() -> Result<ConfigBuilder<DefaultState>> { Self::builder_with_data_dir(&utils::data_dir()) } @@ -910,68 +601,4 @@ mod tests { Ok(()) } - - #[test] - fn keymap_config_deserializes_simple_binding() { - let json = r#"{"emacs": {"ctrl-c": "exit"}}"#; - let config: super::KeymapConfig = serde_json::from_str(json).unwrap(); - assert_eq!(config.emacs.len(), 1); - match &config.emacs["ctrl-c"] { - super::KeyBindingConfig::Simple(s) => assert_eq!(s, "exit"), - _ => panic!("expected Simple variant"), - } - } - - #[test] - fn keymap_config_deserializes_conditional_binding() { - let json = r#"{ - "emacs": { - "left": [ - {"when": "cursor-at-start", "action": "exit"}, - {"action": "cursor-left"} - ] - } - }"#; - let config: super::KeymapConfig = serde_json::from_str(json).unwrap(); - match &config.emacs["left"] { - super::KeyBindingConfig::Rules(rules) => { - assert_eq!(rules.len(), 2); - assert_eq!(rules[0].when.as_deref(), Some("cursor-at-start")); - assert_eq!(rules[0].action, "exit"); - assert!(rules[1].when.is_none()); - assert_eq!(rules[1].action, "cursor-left"); - } - _ => panic!("expected Rules variant"), - } - } - - #[test] - fn keymap_config_deserializes_vim_normal() { - let json = r#"{"vim-normal": {"j": "select-next", "k": "select-previous"}}"#; - let config: super::KeymapConfig = serde_json::from_str(json).unwrap(); - assert_eq!(config.vim_normal.len(), 2); - assert!(config.emacs.is_empty()); - } - - #[test] - fn keymap_config_is_empty_when_default() { - let config = super::KeymapConfig::default(); - assert!(config.is_empty()); - } - - #[test] - fn keymap_config_mixed_modes() { - let json = r#"{ - "emacs": {"ctrl-c": "exit"}, - "vim-normal": {"q": "exit"}, - "inspector": {"d": "delete"} - }"#; - let config: super::KeymapConfig = serde_json::from_str(json).unwrap(); - assert!(!config.is_empty()); - assert_eq!(config.emacs.len(), 1); - assert_eq!(config.vim_normal.len(), 1); - assert_eq!(config.inspector.len(), 1); - assert!(config.vim_insert.is_empty()); - assert!(config.prefix.is_empty()); - } } diff --git a/crates/client/src/atuin_client/theme.rs b/crates/client/src/atuin_client/theme.rs deleted file mode 100644 index ec0538e9..00000000 --- a/crates/client/src/atuin_client/theme.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crossterm::style::{Attribute, Attributes, Color, ContentStyle}; -pub(crate) fn style_base() -> ContentStyle { - ContentStyle::default() -} -pub(crate) fn style_annotation() -> ContentStyle { - ContentStyle { - foreground_color: Some(Color::DarkGrey), - ..ContentStyle::default() - } -} -pub(crate) fn style_important() -> ContentStyle { - ContentStyle { - foreground_color: Some(Color::White), - attributes: Attributes::from(Attribute::Bold), - ..ContentStyle::default() - } -} -pub(crate) fn style_guidance() -> ContentStyle { - ContentStyle { - foreground_color: Some(Color::DarkBlue), - ..ContentStyle::default() - } -} -pub(crate) fn style_alerterror() -> ContentStyle { - ContentStyle { - foreground_color: Some(Color::DarkRed), - ..ContentStyle::default() - } -} -pub(crate) fn style_alertinfo() -> ContentStyle { - ContentStyle { - foreground_color: Some(Color::DarkGreen), - ..ContentStyle::default() - } -} -pub(crate) fn style_alertwarn() -> ContentStyle { - ContentStyle { - foreground_color: Some(Color::DarkYellow), - ..ContentStyle::default() - } -} diff --git a/crates/client/src/atuin_history/stats.rs b/crates/client/src/atuin_history/stats.rs index 462fe077..cf6671c5 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_daemon::aclient::history::History; +use turtle::history::History; use unicode_segmentation::UnicodeSegmentation; use crate::atuin_client::settings::Settings; diff --git a/crates/client/src/command/client.rs b/crates/client/src/command/client.rs index 32686c23..60329500 100644 --- a/crates/client/src/command/client.rs +++ b/crates/client/src/command/client.rs @@ -1,6 +1,3 @@ -use std::fs::{self}; -use std::path::Path; - use clap::Subcommand; use eyre::{Result, WrapErr}; @@ -8,34 +5,6 @@ use tracing_subscriber::filter::EnvFilter; use crate::atuin_client::settings::Settings; -fn cleanup_old_logs(log_dir: &Path, prefix: &str, retention_days: u64) { - let cutoff = std::time::SystemTime::now() - - std::time::Duration::from_secs(retention_days * 24 * 60 * 60); - - let Ok(entries) = fs::read_dir(log_dir) else { - return; - }; - - for entry in entries.flatten() { - let path = entry.path(); - let Some(name) = path.file_name().and_then(|n| n.to_str()) else { - continue; - }; - - // Match files like "search.log.2024-02-23" or "daemon.log.2024-02-23" - if !name.starts_with(prefix) || name == prefix { - continue; - } - - if let Ok(metadata) = entry.metadata() - && let Ok(modified) = metadata.modified() - && modified < cutoff - { - drop(fs::remove_file(&path)); - } - } -} - mod config; mod daemon; mod default_config; diff --git a/crates/client/src/command/client/daemon.rs b/crates/client/src/command/client/daemon.rs index 83877cfd..08ce7a96 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_daemon::api::client::{Probe, probe}; +use turtle::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 bfec0aab..0e1e6b91 100644 --- a/crates/client/src/command/client/history/end.rs +++ b/crates/client/src/command/client/history/end.rs @@ -1,7 +1,7 @@ use crate::atuin_client::settings::Settings; use eyre::{Result, eyre}; -use turtle_daemon::api::client::{HistoryClient, daemon_matches_expected, daemon_mismatch_message}; +use turtle::client::{HistoryClient, daemon_matches_expected, daemon_mismatch_message}; pub(super) async fn handle( settings: &Settings, @@ -24,13 +24,13 @@ async fn end_history(settings: &Settings, id: String, duration: u64, exit: i64) .await { Ok(resp) => { - if daemon_matches_expected(&resp.version, resp.protocol) { + if daemon_matches_expected(resp.protocol) { return Ok(()); } Err(eyre!( "{}. Restart the daemon manually", - daemon_mismatch_message(&resp.version, resp.protocol) + daemon_mismatch_message(resp.protocol) )) } Err(err) => Err(err), diff --git a/crates/client/src/command/client/history/mod.rs b/crates/client/src/command/client/history/mod.rs index d71c653d..b6bfe6a1 100644 --- a/crates/client/src/command/client/history/mod.rs +++ b/crates/client/src/command/client/history/mod.rs @@ -1,18 +1,15 @@ use std::{ - fmt::{self, Display}, - io::{self, IsTerminal, Write}, + fmt::{self}, ops::ControlFlow, time::Duration, }; use clap::Subcommand; use eyre::Result; -use runtime_format::{FormatKey, FormatKeyError, ParseSegment, ParsedFmt}; -use time::{OffsetDateTime, macros::format_description}; -use turtle_common::utils::Escapable; -use turtle_daemon::aclient::history::History; +use time::macros::format_description; +use turtle::history::History; -use crate::atuin_client::settings::{Settings, Timezone}; +use crate::atuin_client::settings::Settings; mod end; mod start; @@ -52,250 +49,9 @@ pub(crate) enum Cmd { /// Stream history events from the daemon as they are received Tail, - - /// List all items in history - List { - #[arg(long, short)] - cwd: bool, - - #[arg(long, short)] - session: bool, - - #[arg(long)] - human: bool, - - /// Show only the text of the command - #[arg(long)] - cmd_only: bool, - - /// Terminate the output with a null, for better multiline support - #[arg(long)] - print0: bool, - - #[arg(long, short, default_value = "true")] - // accept no value - #[arg(num_args(0..=1), default_missing_value("true"))] - // accept a value - #[arg(action = clap::ArgAction::Set)] - reverse: bool, - - /// Display the command time in another timezone other than the configured default. - /// - /// This option takes one of the following kinds of values: - /// - the special value "local" (or "l") which refers to the system time zone - /// - an offset from UTC (e.g. "+9", "-2:30") - #[arg(long, visible_alias = "tz")] - timezone: Option<Timezone>, - - /// Available variables: {command}, {directory}, {duration}, {user}, {host}, {author}, {intent}, {exit}, {time}, {session}, and {uuid} - /// Example: --format "{time} - [{duration}] - {directory}$\t{command}" - #[arg(long, short)] - format: Option<String>, - }, - - /// Get the last command ran - Last { - #[arg(long)] - human: bool, - - /// Show only the text of the command - #[arg(long)] - cmd_only: bool, - - /// Display the command time in another timezone other than the configured default. - /// - /// This option takes one of the following kinds of values: - /// - the special value "local" (or "l") which refers to the system time zone - /// - an offset from UTC (e.g. "+9", "-2:30") - #[arg(long, visible_alias = "tz")] - timezone: Option<Timezone>, - - /// Available variables: {command}, {directory}, {duration}, {user}, {host}, {author}, {intent}, {time}, {session}, {uuid} and {relativetime}. - /// Example: --format "{time} - [{duration}] - {directory}$\t{command}" - #[arg(long, short)] - format: Option<String>, - }, - - InitStore, - - /// Delete history entries matching the configured exclusion filters - Prune { - /// List matching history lines without performing the actual deletion. - #[arg(short = 'n', long)] - dry_run: bool, - }, - - /// Delete duplicate history entries (that have the same command, cwd and hostname) - Dedup { - /// List matching history lines without performing the actual deletion. - #[arg(short = 'n', long)] - dry_run: bool, - - /// Only delete results added before this date - #[arg(long, short)] - before: String, - - /// How many recent duplicates to keep - #[arg(long)] - dupkeep: u32, - }, } impl Cmd { - // #[expect(clippy::too_many_arguments)] - // #[expect(clippy::fn_params_excessive_bools)] - // async fn handle_list( - // settings: &Settings, - // session: bool, - // cwd: bool, - // mode: ListMode, - // format: Option<String>, - // include_deleted: bool, - // print0: bool, - // reverse: bool, - // tz: Timezone, - // ) -> Result<()> { - // let filters = match (session, cwd) { - // (true, true) => [Session, Directory], - // (true, false) => [Session, Global], - // (false, true) => [Global, Directory], - // (false, false) => [ - // settings.default_filter_mode(context.git_root.is_some()), - // Global, - // ], - // }; - // - // let history = db - // .list(&filters, &context, None, false, include_deleted) - // .await?; - // - // print_list( - // &history, - // mode, - // match format { - // None => Some(settings.history_format.as_str()), - // _ => format.as_deref(), - // }, - // print0, - // reverse, - // tz, - // ); - // - // Ok(()) - // } - - // async fn handle_prune( - // db: &ClientSqlite, - // settings: &Settings, - // store: SqliteStore, - // context: crate::atuin_client::database::Context, - // dry_run: bool, - // ) -> Result<()> { - // // Grab all executed commands and filter them using History::should_save. - // // We could iterate or paginate here if memory usage becomes an issue. - // let matches: Vec<History> = db - // .list(&[Global], &context, None, false, false) - // .await? - // .into_iter() - // .filter(|h| !h.should_save(settings)) - // .collect(); - // - // match matches.len() { - // 0 => { - // println!("No entries to prune."); - // return Ok(()); - // } - // 1 => println!("Found 1 entry to prune."), - // n => println!("Found {n} entries to prune."), - // } - // - // if dry_run { - // print_list( - // &matches, - // ListMode::Human, - // Some(settings.history_format.as_str()), - // false, - // false, - // settings.timezone, - // ); - // } else { - // 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); - // - // for entry in matches { - // eprintln!("deleting {}", entry.id); - // let (id, _) = history_store.delete(entry.id.clone()).await?; - // history_store.incremental_build(db, &[id]).await?; - // } - // - // daemon::emit_event(settings, crate::atuin_daemon::DaemonEvent::HistoryPruned).await; - // } - // Ok(()) - // } - // - // async fn handle_dedup( - // db: &ClientSqlite, - // settings: &Settings, - // store: SqliteStore, - // before: i64, - // dupkeep: u32, - // dry_run: bool, - // ) -> Result<()> { - // if dupkeep == 0 { - // eprintln!( - // "\"--dupkeep 0\" would keep 0 copies of duplicate commands and thus delete all of them! Use \"atuin search --delete ...\" if you really want that." - // ); - // std::process::exit(1); - // } - // - // let matches: Vec<History> = db.get_dups(before, dupkeep).await?; - // - // match matches.len() { - // 0 => { - // println!("No duplicates to delete."); - // return Ok(()); - // } - // 1 => println!("Found 1 duplicate to delete."), - // n => println!("Found {n} duplicates to delete."), - // } - // - // if dry_run { - // print_list( - // &matches, - // ListMode::Human, - // Some(settings.history_format.as_str()), - // false, - // false, - // settings.timezone, - // ); - // } else { - // 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); - // - // let ids = matches.iter().map(|h| h.id.clone()).collect::<Vec<_>>(); - // - // for entry in matches { - // eprintln!("deleting {}", entry.id); - // let (id, _) = history_store.delete(entry.id).await?; - // history_store.incremental_build(db, &[id]).await?; - // } - // - // daemon::emit_event( - // settings, - // crate::atuin_daemon::DaemonEvent::HistoryDeleted { ids }, - // ) - // .await; - // } - // Ok(()) - // } - - #[expect(clippy::too_many_lines)] pub(crate) async fn run(self, settings: &Settings) -> Result<()> { match self { Self::Start { @@ -322,267 +78,10 @@ impl Cmd { Self::Tail => { return tail::handle(settings).await; } - cmd => { - todo!() - // let context = current_context().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); - // - // match cmd { - // Self::List { - // session, - // cwd, - // human, - // cmd_only, - // print0, - // reverse, - // timezone, - // format, - // } => { - // let mode = ListMode::from_flags(human, cmd_only); - // let tz = timezone.unwrap_or(settings.timezone); - // Self::handle_list( - // &db, settings, context, session, cwd, mode, format, false, print0, - // reverse, tz, - // ) - // .await - // } - // - // Self::Last { - // human, - // cmd_only, - // timezone, - // format, - // } => { - // let last = db.last().await?; - // let last = last.as_slice(); - // let tz = timezone.unwrap_or(settings.timezone); - // print_list( - // last, - // ListMode::from_flags(human, cmd_only), - // match format { - // None => Some(settings.history_format.as_str()), - // _ => format.as_deref(), - // }, - // false, - // true, - // tz, - // ); - // - // Ok(()) - // } - // - // Self::InitStore => history_store.init_store(&db).await, - // - // Self::Prune { dry_run } => { - // Self::handle_prune(&db, settings, store, context, dry_run).await - // } - // - // Self::Dedup { - // dry_run, - // before, - // dupkeep, - // } => { - // let before = i64::try_from( - // interim::parse_date_string( - // before.as_str(), - // OffsetDateTime::now_utc(), - // interim::Dialect::Uk, - // )? - // .unix_timestamp_nanos(), - // )?; - // Self::handle_dedup(&db, settings, store, before, dupkeep, dry_run).await - // } - // - // Self::Start { .. } | Self::End { .. } | Self::Tail => unreachable!(), - // } - } - } - } -} - -#[derive(Clone, Copy, Debug)] -pub(crate) enum ListMode { - Human, - CmdOnly, - Regular, -} - -impl ListMode { - pub(crate) const fn from_flags(human: bool, cmd_only: bool) -> Self { - if human { - Self::Human - } else if cmd_only { - Self::CmdOnly - } else { - Self::Regular - } - } -} - -pub(crate) fn print_list( - h: &[History], - list_mode: ListMode, - format: Option<&str>, - print0: bool, - reverse: bool, - tz: Timezone, -) { - let w = io::stdout(); - let mut w = w.lock(); - - let fmt_str = match list_mode { - ListMode::Human => format - .unwrap_or("{time} · {duration}\t{command}") - .replace("\\t", "\t"), - ListMode::Regular => format - .unwrap_or("{time}\t{command}\t{duration}") - .replace("\\t", "\t"), - // not used - ListMode::CmdOnly => String::new(), - }; - - let parsed_fmt = match list_mode { - ListMode::Human | ListMode::Regular => parse_fmt(&fmt_str), - ListMode::CmdOnly => std::iter::once(ParseSegment::Key("command")).collect(), - }; - - #[expect(trivial_casts)] - let iterator = if reverse { - Box::new(h.iter().rev()) as Box<dyn Iterator<Item = &History>> - } else { - Box::new(h.iter()) as Box<dyn Iterator<Item = &History>> - }; - - let entry_terminator = if print0 { "\0" } else { "\n" }; - let flush_each_line = print0; - - for history in iterator { - let fh = FmtHistory { - history, - cmd_format: CmdFormat::for_output(&w), - tz: &tz, - }; - let args = parsed_fmt.with_args(&fh); - - // Check for formatting errors before attempting to write - if let Err(err) = args.status() { - eprintln!("ERROR: history output failed with: {err}"); - std::process::exit(1); - } - - let write_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - write!(w, "{args}{entry_terminator}") - })); - - match write_result { - Ok(Ok(())) => { - // Write succeeded - } - Ok(Err(err)) => { - if err.kind() != io::ErrorKind::BrokenPipe { - eprintln!("ERROR: Failed to write history output: {err}"); - std::process::exit(1); - } - } - Err(_) => { - eprintln!("ERROR: Format string caused a formatting error."); - eprintln!( - "This may be due to an unsupported format string containing special characters." - ); - eprintln!( - "Please check your format string syntax and ensure literal braces are properly escaped." - ); - std::process::exit(1); - } - } - if flush_each_line { - check_for_write_errors(w.flush()); - } - } - - if !flush_each_line { - check_for_write_errors(w.flush()); - } -} - -fn check_for_write_errors(write: Result<(), io::Error>) { - if let Err(err) = write { - // Ignore broken pipe (issue #626) - if err.kind() != io::ErrorKind::BrokenPipe { - eprintln!("ERROR: History output failed with the following error: {err}"); - std::process::exit(1); } } } -/// Type wrapper around `History` with formatting settings. -#[derive(Clone, Copy, Debug)] -struct FmtHistory<'a> { - history: &'a History, - cmd_format: CmdFormat, - tz: &'a Timezone, -} -/// defines how to format the history -impl FormatKey for FmtHistory<'_> { - #[expect(clippy::cast_sign_loss)] - fn fmt(&self, key: &str, f: &mut fmt::Formatter<'_>) -> Result<(), FormatKeyError> { - match key { - "command" => match self.cmd_format { - CmdFormat::Literal => f.write_str(self.history.command.trim()), - CmdFormat::Escaped => f.write_str(&self.history.command.trim().escape_control()), - }?, - "directory" => f.write_str(self.history.cwd.trim())?, - "exit" => f.write_str(&self.history.exit.to_string())?, - "duration" => { - let dur = Duration::from_nanos(std::cmp::max(self.history.duration, 0) as u64); - format_duration_into(dur, f)?; - } - "time" => { - self.history - .timestamp - .to_offset(self.tz.0) - .format(TIME_FMT) - .map_err(|_| fmt::Error)? - .fmt(f)?; - } - "relativetime" => { - let since = OffsetDateTime::now_utc() - self.history.timestamp; - let d = Duration::try_from(since).unwrap_or_default(); - format_duration_into(d, f)?; - } - "host" => f.write_str( - self.history - .hostname - .split_once(':') - .map_or(&self.history.hostname, |(host, _)| host), - )?, - "author" => f.write_str(&self.history.author)?, - "intent" => f.write_str(self.history.intent.as_deref().unwrap_or_default())?, - "user" => f.write_str( - self.history - .hostname - .split_once(':') - .map_or("", |(_, user)| user), - )?, - "session" => f.write_str(&self.history.session)?, - "uuid" => f.write_str(&self.history.id.0)?, - _ => return Err(FormatKeyError::UnknownKey), - } - Ok(()) - } -} fn format_duration_into(dur: Duration, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn item(unit: &'static str, value: u64) -> ControlFlow<(&'static str, u64)> { if value > 0 { @@ -634,45 +133,9 @@ fn format_duration_into(dur: Duration, f: &mut fmt::Formatter<'_>) -> fmt::Resul } } -#[derive(Clone, Copy, Debug)] -enum CmdFormat { - Literal, - Escaped, -} -impl CmdFormat { - fn for_output<O: IsTerminal>(out: &O) -> Self { - if out.is_terminal() { - Self::Escaped - } else { - Self::Literal - } - } -} - static TIME_FMT: &[time::format_description::FormatItem<'static>] = format_description!("[year]-[month]-[day] [hour repr:24]:[minute]:[second]"); -fn parse_fmt(format: &str) -> ParsedFmt<'_> { - match ParsedFmt::new(format) { - Ok(fmt) => fmt, - Err(err) => { - eprintln!("ERROR: History formatting failed with the following error: {err}"); - - if format.contains('"') && (format.contains(":{") || format.contains(",{")) { - eprintln!("It looks like you're trying to create JSON output."); - eprintln!("For JSON, you need to escape literal braces by doubling them:"); - eprintln!("Example: '{{\"command\":\"{{command}}\",\"time\":\"{{time}}\"}}'"); - } else { - eprintln!( - "If your formatting string contains literal curly braces, you need to escape them by doubling:" - ); - eprintln!("Use {{{{ for literal {{ and }}}} for literal }}"); - } - std::process::exit(1) - } - } -} - fn apply_start_metadata(history: &mut History, author: Option<&str>, intent: Option<&str>) { if let Some(author) = author.map(str::trim).filter(|author| !author.is_empty()) { author.clone_into(&mut history.author); @@ -711,7 +174,7 @@ fn normalize_command_for_storage<'a>(command: &'a str, settings: &Settings) -> & #[cfg(test)] mod tests { - use super::{Settings, normalize_command_for_storage, parse_fmt}; + use super::{Settings, normalize_command_for_storage}; #[test] fn normalize_command_strips_trailing_spaces_and_tabs() { @@ -734,20 +197,4 @@ mod tests { "printf foo\\\\" ); } - - #[test] - fn test_format_string_no_panic() { - // Don't panic but provide helpful output (issue #2776) - let malformed_json = r#"{"command":"{command}","key":"value"}"#; - - let result = std::panic::catch_unwind(|| parse_fmt(malformed_json)); - - assert!(result.is_ok()); - } - - #[test] - fn test_valid_formats_still_work() { - assert!(std::panic::catch_unwind(|| parse_fmt("{command}")).is_ok()); - assert!(std::panic::catch_unwind(|| parse_fmt("{time} - {command}")).is_ok()); - } } diff --git a/crates/client/src/command/client/history/start.rs b/crates/client/src/command/client/history/start.rs index defd46a3..cb8f1b0c 100644 --- a/crates/client/src/command/client/history/start.rs +++ b/crates/client/src/command/client/history/start.rs @@ -1,17 +1,19 @@ use crate::{ atuin_client::settings::Settings, - command::client::history::{apply_start_metadata, normalize_command_for_storage}, + command::{ + client::history::{apply_start_metadata, normalize_command_for_storage}, + current_session, + }, }; use eyre::{Result, eyre}; use time::OffsetDateTime; use tracing::debug; -use turtle::History; -use turtle_common::utils; -use turtle_daemon::{ - aclient::history::SettingsFilter, - api::client::{HistoryClient, daemon_matches_expected, daemon_mismatch_message}, +use turtle::{ + client::{HistoryClient, daemon_matches_expected, daemon_mismatch_message}, + history::{History, SettingsFilter}, }; +use turtle_common::utils::{self, get_hostname, get_username}; pub(super) async fn handle( settings: &Settings, @@ -28,6 +30,9 @@ pub(super) async fn handle( .timestamp(OffsetDateTime::now_utc()) .command(command) .cwd(cwd) + .session(current_session()?) + .hostname(get_hostname()) + .author(get_username()) .build() .into(); apply_start_metadata(&mut h, author, intent); @@ -63,13 +68,13 @@ async fn start_history(settings: &Settings, history: History) -> Result<String> .await { Ok(resp) => { - if daemon_matches_expected(&resp.version, resp.protocol) { + if daemon_matches_expected(resp.protocol) { return Ok(resp.id); } Err(eyre!( "{}. Restart the daemon manually", - daemon_mismatch_message(&resp.version, resp.protocol) + daemon_mismatch_message(resp.protocol) )) } Err(err) => Err(err), diff --git a/crates/client/src/command/client/history/tail.rs b/crates/client/src/command/client/history/tail.rs index 595fe3a0..212d7fcd 100644 --- a/crates/client/src/command/client/history/tail.rs +++ b/crates/client/src/command/client/history/tail.rs @@ -7,13 +7,13 @@ use colored::Colorize; use eyre::{Context, Result, bail}; use serde::Serialize; use time::OffsetDateTime; -use turtle_common::utils::Escapable; -use turtle_daemon::{ - aclient::history::History, - api::client::{ +use turtle::{ + client::{ HistoryClient, HistoryEventKind, Probe, TailHistoryReply, history_entry_to_history, probe, }, + history::History, }; +use turtle_common::utils::Escapable; use std::{ fmt::{self, Display}, @@ -301,7 +301,7 @@ async fn tail_client(settings: &Settings) -> Result<HistoryClient> { pub(super) async fn handle(settings: &Settings) -> Result<()> { let tty = io::stdout().is_terminal(); - let mut client = HistoryClient::new(settings.daemon.socket_path.clone()).await?; + let mut client = tail_client(settings).await?; let mut stream = client.tail_history().await?; let stdout = io::stdout(); diff --git a/crates/client/src/command/client/info.rs b/crates/client/src/command/client/info.rs index 77c7064c..c4839c38 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_daemon::api::client::ControlClient;
+use turtle::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 ec235f36..88734bb6 100644 --- a/crates/client/src/command/client/stats.rs +++ b/crates/client/src/command/client/stats.rs @@ -1,8 +1,8 @@ use clap::Parser; use eyre::Result; -use interim::parse_date_string; +use interim::{Dialect, parse_date_string}; use time::{Duration, OffsetDateTime, Time}; -use turtle_daemon::api::client::{HistoryClient, Range}; +use turtle::client::{HistoryClient, Range}; use crate::atuin_client::settings::Settings; @@ -69,7 +69,7 @@ impl Cmd { let start = end - Duration::days(365); client.history(session, Some(Range { start, end })).await? } else { - let start = parse_date_string(&words, now, settings.dialect.into())?; + let start = parse_date_string(&words, now, Dialect::Uk)?; let end = start + Duration::days(1); client.history(session, Some(Range { start, end })).await? }; diff --git a/crates/client/src/command/client/sync.rs b/crates/client/src/command/client/sync.rs index ac0d0afe..77ae97ba 100644 --- a/crates/client/src/command/client/sync.rs +++ b/crates/client/src/command/client/sync.rs @@ -1,7 +1,7 @@ use clap::Subcommand; use eyre::{Result, bail}; -use turtle_daemon::api::client::{Probe, probe}; +use turtle::client::{Probe, probe}; use crate::atuin_client::settings::Settings; diff --git a/crates/client/src/command/client/wrapped.rs b/crates/client/src/command/client/wrapped.rs index 64e5b718..8c15a5d3 100644 --- a/crates/client/src/command/client/wrapped.rs +++ b/crates/client/src/command/client/wrapped.rs @@ -2,13 +2,16 @@ use crossterm::style::{ResetColor, SetAttribute}; use eyre::Result; use std::collections::{HashMap, HashSet}; use time::{Date, Duration, Month, OffsetDateTime, Time}; -use turtle_daemon::aclient::history::History; -use turtle_daemon::api::client::{HistoryClient, Range}; +use turtle::{ + client::{HistoryClient, Range}, + history::History, +}; -use crate::atuin_client::settings::Settings; - -use crate::atuin_history::stats::{Stats, compute}; -use crate::command::current_session; +use crate::{ + atuin_client::settings::Settings, + atuin_history::stats::{Stats, compute}, + command::current_session, +}; #[derive(Debug)] struct WrappedStats { diff --git a/crates/client/src/command/mod.rs b/crates/client/src/command/mod.rs index 2e51a1e2..a2e75034 100644 --- a/crates/client/src/command/mod.rs +++ b/crates/client/src/command/mod.rs @@ -10,7 +10,6 @@ mod gen_completions; #[derive(Subcommand)] #[command(infer_subcommands = true)] -#[expect(clippy::large_enum_variant)] pub(crate) enum AtuinCmd { #[command(flatten)] Client(client::Cmd), @@ -50,14 +49,6 @@ impl AtuinCmd { } } -#[cfg(unix)] -#[inline] -fn is_truthy_env(name: &str) -> bool { - std::env::var(name) - .ok() - .as_ref() - .is_some_and(|value| !value.trim().is_empty() && value.trim() != "false") -} pub(crate) fn current_session() -> Result<String> { std::env::var("ATUIN_SESSION").map_err(|_| { eyre::eyre!("Failed to find $ATUIN_SESSION in the environment. Check that you have correctly set up your shell.") diff --git a/crates/client/src/main.rs b/crates/client/src/main.rs index 51480a7b..05f328e2 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::util::SubscriberInitExt; mod command; @@ -59,5 +60,9 @@ impl Atuin { } fn main() -> Result<()> { + if let Err(e) = tracing_subscriber::registry().try_init() { + eprintln!("failed to initialize logging: {e}"); + } + Atuin::parse().run() } diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index df978c03..b79932ff 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -20,6 +20,7 @@ sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "time", "postgres" time = { version = "0.3.47", features = [ "serde-human-readable", "macros", "local-offset", "macros", "formatting", "parsing"] } typed-builder = "0.18.2" uuid = { version = "1.9", features = ["v4", "v7", "serde"] } +whoami = "2.1.0" [dev-dependencies] diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs index d6077ee7..4451140b 100644 --- a/crates/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -4,6 +4,24 @@ use std::path::{Path, PathBuf}; use uuid::Uuid; +#[must_use] +pub fn get_hostname() -> String { + env::var("ATUIN_HOST_NAME") + .unwrap_or_else(|_| whoami::hostname().unwrap_or_else(|_| "unknown-host".to_string())) +} + +#[must_use] +pub fn get_username() -> String { + env::var("ATUIN_HOST_USER") + .unwrap_or_else(|_| whoami::username().unwrap_or_else(|_| "unknown-user".to_string())) +} + +/// Returns a pair of the hostname and username, separated by a colon. +#[must_use] +pub fn get_host_user() -> String { + format!("{}:{}", get_hostname(), get_username()) +} + pub fn uuid_v7() -> Uuid { Uuid::now_v7() } diff --git a/crates/daemon/Cargo.toml b/crates/daemon/Cargo.toml index 830dbd12..52a501fc 100644 --- a/crates/daemon/Cargo.toml +++ b/crates/daemon/Cargo.toml @@ -78,7 +78,6 @@ unicode-width = "0.2" url = "2.5.2" uuid = { version = "1.9", features = ["v4", "v7", "serde"] } vt100 = "0.16" -whoami = "2.1.0" [target.'cfg(target_os = "linux")'.dependencies] arboard = { version = "3.4", default-features = false, features = [ "wayland-data-control", ] } diff --git a/crates/daemon/src/aclient/api_client.rs b/crates/daemon/src/aclient/api_client.rs index c0688cf9..954426b4 100644 --- a/crates/daemon/src/aclient/api_client.rs +++ b/crates/daemon/src/aclient/api_client.rs @@ -39,7 +39,7 @@ fn make_url(address: &str, path: &str, user_id: Uuid) -> Result<String> { Ok(url.to_string()) } -pub(crate) fn ensure_version(response: &Response) -> Result<bool> { +fn ensure_version(response: &Response) -> Result<bool> { let version = response.headers().get(ATUIN_HEADER_VERSION); let version = if let Some(version) = version { @@ -141,7 +141,7 @@ impl<'a> Client<'a> { }) } - pub(crate) async fn delete_store(&self) -> Result<()> { + async fn delete_store(&self) -> Result<()> { let url = make_url(self.sync_addr, "/store", self.user_id)?; let url = Url::parse(url.as_str())?; diff --git a/crates/daemon/src/aclient/database/mod.rs b/crates/daemon/src/aclient/database/mod.rs index cdf71065..9da943ca 100644 --- a/crates/daemon/src/aclient/database/mod.rs +++ b/crates/daemon/src/aclient/database/mod.rs @@ -12,8 +12,8 @@ use sqlx::{ }; use time::OffsetDateTime; use tracing::debug; -use turtle::history::{History, HistoryId, get_host_user}; -use turtle_common::utils; +use turtle::history::{History, HistoryId}; +use turtle_common::utils::{self, get_host_user}; use uuid::Uuid; use crate::aclient::{ @@ -25,46 +25,29 @@ use crate::aclient::{settings::Settings, utils::setup_db}; #[derive(Clone)] pub(crate) struct Context { - pub(crate) session: String, - pub(crate) cwd: String, - pub(crate) hostname: String, - pub(crate) host_id: String, - pub(crate) git_root: Option<PathBuf>, + session: String, + cwd: String, + hostname: String, + host_id: String, + git_root: Option<PathBuf>, } #[derive(Default, Clone)] -pub(crate) struct OptFilters { - pub(crate) exit: Option<i64>, - pub(crate) exclude_exit: Option<i64>, - pub(crate) cwd: Option<String>, - pub(crate) exclude_cwd: Option<String>, - pub(crate) before: Option<String>, - pub(crate) after: Option<String>, - pub(crate) limit: Option<i64>, - pub(crate) offset: Option<i64>, - pub(crate) reverse: bool, - pub(crate) include_duplicates: bool, -} - -pub(crate) async fn current_context(session: String) -> eyre::Result<Context> { - // TODO(@bpeetz): More of this needs to be moved to the client <2026-07-20> - - let hostname = get_host_user(); - let cwd = utils::get_current_dir(); - let host_id = Settings::host_id().await?; - let git_root = utils::in_git_repo(cwd.as_str()); - - Ok(Context { - session, - hostname, - cwd, - git_root, - host_id: host_id.0.as_simple().to_string(), - }) +struct OptFilters { + exit: Option<i64>, + exclude_exit: Option<i64>, + cwd: Option<String>, + exclude_cwd: Option<String>, + before: Option<String>, + after: Option<String>, + limit: Option<i64>, + offset: Option<i64>, + reverse: bool, + include_duplicates: bool, } impl Context { - pub(crate) fn from_history(entry: &History) -> Self { + fn from_history(entry: &History) -> Self { Self { session: entry.session.clone(), cwd: entry.cwd.clone(), @@ -88,12 +71,12 @@ fn get_session_start_time(session_id: &str) -> Option<i64> { // Intended for use on a developer machine and not a sync server. // TODO: implement IntoIterator #[derive(Debug, Clone)] -pub struct ClientSqlite { - pub(crate) pool: SqlitePool, +pub(crate) struct ClientSqlite { + pool: SqlitePool, } impl ClientSqlite { - pub async fn new(path: impl AsRef<Path>, timeout: f64) -> Result<Self> { + pub(crate) async fn new(path: impl AsRef<Path>, timeout: f64) -> Result<Self> { fn mk_opts(path: &str) -> Result<SqliteConnectOptions> { let opts = SqliteConnectOptions::from_str(path)? .journal_mode(SqliteJournalMode::Wal) @@ -220,7 +203,7 @@ impl ClientSqlite { Ok(()) } - pub(crate) async fn load(&self, id: &str) -> Result<Option<History>> { + async fn load(&self, id: &str) -> Result<Option<History>> { debug!("loading history item {}", id); let res = sqlx::query("select * from history where id = ?1") @@ -232,11 +215,10 @@ impl ClientSqlite { Ok(res) } - // make a unique list, that only shows the *newest* version of things + /// make a unique list, that only shows the *newest* version of things pub(crate) async fn list( &self, - filters: &[FilterMode], - context: &Context, + filters: Option<(&Context, &[FilterMode])>, max: Option<usize>, unique: bool, include_deleted: bool, @@ -249,28 +231,30 @@ impl ClientSqlite { query.and_where_is_null("deleted_at"); } - let git_root = context.git_root.clone().map_or_else( - || context.cwd.clone(), - |git_root| git_root.to_str().unwrap_or("/").to_string(), - ); + if let Some((context, filters)) = filters { + let git_root = context.git_root.clone().map_or_else( + || context.cwd.clone(), + |git_root| git_root.to_str().unwrap_or("/").to_string(), + ); - let session_start = get_session_start_time(&context.session); + let session_start = get_session_start_time(&context.session); - for filter in filters { - match filter { - FilterMode::Global => &mut query, - FilterMode::Host => query.and_where_eq("hostname", quote(&context.hostname)), - FilterMode::Session => query.and_where_eq("session", quote(&context.session)), - FilterMode::SessionPreload => { - query.and_where_eq("session", quote(&context.session)); - if let Some(session_start) = session_start { - query.or_where_lt("timestamp", session_start); + for filter in filters { + match filter { + FilterMode::Global => &mut query, + FilterMode::Host => query.and_where_eq("hostname", quote(&context.hostname)), + FilterMode::Session => query.and_where_eq("session", quote(&context.session)), + FilterMode::SessionPreload => { + query.and_where_eq("session", quote(&context.session)); + if let Some(session_start) = session_start { + query.or_where_lt("timestamp", session_start); + } + &mut query } - &mut query - } - FilterMode::Directory => query.and_where_eq("cwd", quote(&context.cwd)), - FilterMode::Workspace => query.and_where_like_left("cwd", &git_root), - }; + FilterMode::Directory => query.and_where_eq("cwd", quote(&context.cwd)), + FilterMode::Workspace => query.and_where_like_left("cwd", &git_root), + }; + } } if unique { @@ -310,7 +294,7 @@ impl ClientSqlite { Ok(res) } - pub(crate) async fn last(&self) -> Result<Option<History>> { + async fn last(&self) -> Result<Option<History>> { let res = sqlx::query( "select * from history where duration >= 0 order by timestamp desc limit 1", ) @@ -321,7 +305,7 @@ impl ClientSqlite { Ok(res) } - pub(crate) async fn history_count(&self, include_deleted: bool) -> Result<i64> { + async fn history_count(&self, include_deleted: bool) -> Result<i64> { let query = if include_deleted { "select count(1) from history" } else { @@ -336,7 +320,7 @@ impl ClientSqlite { // Could maybe break it down to a searchparams struct or smth but that feels a little... pointless. // Been debating maybe a DSL for search? eg "before:time limit:1 the query" #[expect(clippy::too_many_lines)] - pub(crate) async fn search( + async fn search( &self, search_mode: SearchMode, filter: FilterMode, @@ -492,7 +476,7 @@ impl ClientSqlite { Ok(ordering::reorder_fuzzy(search_mode, orig_query, res)) } - pub(crate) async fn query_history(&self, query: &str) -> Result<Vec<History>> { + async fn query_history(&self, query: &str) -> Result<Vec<History>> { let res = sqlx::query(query) .map(Self::query_history_inner) .fetch_all(&self.pool) @@ -501,7 +485,7 @@ impl ClientSqlite { Ok(res) } - pub(crate) async fn all_with_count(&self) -> Result<Vec<(History, i32)>> { + async fn all_with_count(&self) -> Result<Vec<(History, i32)>> { debug!("listing history"); let mut query = SqlBuilder::select_from(SqlName::new("history").alias("h").baquoted()); @@ -539,7 +523,7 @@ impl ClientSqlite { Ok(res) } - pub(crate) fn all_paged(&self, page_size: usize, include_deleted: bool, unique: bool) -> Paged { + fn all_paged(&self, page_size: usize, include_deleted: bool, unique: bool) -> Paged { Paged::new(self.clone(), page_size, include_deleted, unique) } @@ -555,7 +539,7 @@ impl ClientSqlite { Ok(()) } - pub(crate) async fn stats(&self, h: &History) -> Result<HistoryStats> { + async fn stats(&self, h: &History) -> Result<HistoryStats> { // We select the previous in the session by time let mut prev = SqlBuilder::select_from("history"); prev.field("*") @@ -672,7 +656,7 @@ impl ClientSqlite { }) } - pub(crate) async fn get_dups(&self, before: i64, dupkeep: u32) -> Result<Vec<History>> { + async fn get_dups(&self, before: i64, dupkeep: u32) -> Result<Vec<History>> { let res = sqlx::query( "SELECT * FROM ( SELECT *, ROW_NUMBER() @@ -693,7 +677,7 @@ impl ClientSqlite { } } -pub(crate) struct Paged { +struct Paged { database: ClientSqlite, page_size: usize, last_id: Option<String>, @@ -702,12 +686,7 @@ pub(crate) struct Paged { } impl Paged { - pub(crate) fn new( - database: ClientSqlite, - page_size: usize, - include_deleted: bool, - unique: bool, - ) -> Self { + fn new(database: ClientSqlite, page_size: usize, include_deleted: bool, unique: bool) -> Self { Self { database, page_size, @@ -717,7 +696,7 @@ impl Paged { } } - pub(crate) async fn next(&mut self) -> Result<Option<Vec<History>>> { + async fn next(&mut self) -> Result<Option<Vec<History>>> { let mut query = SqlBuilder::select_from(SqlName::new("history").alias("h").baquoted()); query.field("*").order_desc("id"); @@ -1220,12 +1199,12 @@ mod test { } } -pub(crate) struct QueryTokenizer<'a> { +struct QueryTokenizer<'a> { query: &'a str, last_pos: usize, } -pub(crate) enum QueryToken<'a> { +enum QueryToken<'a> { Match(&'a str, bool), MatchStart(&'a str, bool), MatchEnd(&'a str, bool), @@ -1235,7 +1214,7 @@ pub(crate) enum QueryToken<'a> { } impl QueryToken<'_> { - pub(crate) fn has_uppercase(&self) -> bool { + fn has_uppercase(&self) -> bool { match self { Self::Match(term, _) | Self::MatchStart(term, _) @@ -1245,7 +1224,7 @@ impl QueryToken<'_> { } } - pub(crate) fn is_inverse(&self) -> bool { + fn is_inverse(&self) -> bool { match self { Self::Match(_, inv) | Self::MatchStart(_, inv) @@ -1257,7 +1236,7 @@ impl QueryToken<'_> { } impl<'a> QueryTokenizer<'a> { - pub(crate) fn new(query: &'a str) -> Self { + fn new(query: &'a str) -> Self { Self { query, last_pos: 0 } } } diff --git a/crates/daemon/src/aclient/encryption.rs b/crates/daemon/src/aclient/encryption.rs index 220ac74e..45e82ab3 100644 --- a/crates/daemon/src/aclient/encryption.rs +++ b/crates/daemon/src/aclient/encryption.rs @@ -11,7 +11,7 @@ use std::io::prelude::Write; use base64::prelude::{BASE64_STANDARD, Engine}; -pub(crate) use crypto_secretbox::Key; +use crypto_secretbox::Key; use crypto_secretbox::{KeyInit, XSalsa20Poly1305, aead::OsRng}; use eyre::{Context, Result, bail, ensure, eyre}; use fs_err as fs; @@ -19,14 +19,14 @@ use rmp::Marker; use crate::aclient::settings::Settings; -pub(crate) fn generate_encoded_key() -> Result<(Key, String)> { +fn generate_encoded_key() -> Result<(Key, String)> { let key = XSalsa20Poly1305::generate_key(&mut OsRng); let encoded = encode_key(&key)?; Ok((key, encoded)) } -pub(crate) fn new_key(settings: &Settings) -> Result<Key> { +fn new_key(settings: &Settings) -> Result<Key> { if settings.sync.encryption_key()?.is_some() { bail!("key already exists! cannot overwrite"); } else if let Some(path) = settings.sync.encryption_key_path.as_ref() { @@ -50,7 +50,7 @@ pub(crate) fn load_key(settings: &Settings) -> Result<Key> { } } -pub(crate) fn encode_key(key: &Key) -> Result<String> { +fn encode_key(key: &Key) -> Result<String> { let mut buf = vec![]; rmp::encode::write_array_len(&mut buf, key.len() as u32) .wrap_err("could not encode key to message pack")?; diff --git a/crates/daemon/src/aclient/history/mod.rs b/crates/daemon/src/aclient/history/mod.rs index ea71dcf4..f9afa5e9 100644 --- a/crates/daemon/src/aclient/history/mod.rs +++ b/crates/daemon/src/aclient/history/mod.rs @@ -1,14 +1,10 @@ -use core::fmt::Formatter; use regex::RegexSet; use rmp::decode::DecodeStringError; use rmp::decode::ValueReadError; use rmp::{Marker, decode::Bytes}; -use std::env; -use std::fmt::Display; use turtle::history::History; use turtle_common::record::DecryptedData; -use turtle_common::utils::uuid_v7; use eyre::{Result, bail, eyre}; @@ -16,12 +12,12 @@ use time::OffsetDateTime; pub(crate) mod store; -pub(crate) const HISTORY_VERSION_V0: &str = "v0"; -pub(crate) const HISTORY_VERSION_V1: &str = "v1"; +const HISTORY_VERSION_V0: &str = "v0"; +const HISTORY_VERSION_V1: &str = "v1"; const HISTORY_RECORD_VERSION_V0: u16 = 0; const HISTORY_RECORD_VERSION_V1: u16 = 1; -pub(crate) const HISTORY_VERSION: &str = HISTORY_VERSION_V1; -pub(crate) const HISTORY_TAG: &str = "history"; +const HISTORY_VERSION: &str = HISTORY_VERSION_V1; +const HISTORY_TAG: &str = "history"; const HISTORY_AUTHOR_ENV: &str = "ATUIN_HISTORY_AUTHOR"; const HISTORY_INTENT_ENV: &str = "ATUIN_HISTORY_INTENT"; @@ -45,7 +41,7 @@ pub(crate) struct HistoryStats { pub(crate) duration_over_time: Vec<(String, i64)>, } -pub(crate) trait HistoryExt: Sized { +trait HistoryExt: Sized { fn serialize(&self) -> Result<DecryptedData>; fn read_optional_string(bytes: &[u8]) -> Result<(Option<String>, &[u8])>; fn deserialize_v0(bytes: &[u8]) -> Result<Self>; @@ -260,7 +256,7 @@ impl HistoryExt for History { } #[derive(Debug, Copy, Clone)] -pub struct SettingsFilter<'a> { +struct SettingsFilter<'a> { pub history: &'a RegexSet, pub cwd: &'a RegexSet, pub secrets: bool, diff --git a/crates/daemon/src/aclient/history/store.rs b/crates/daemon/src/aclient/history/store.rs index c62f9068..244725eb 100644 --- a/crates/daemon/src/aclient/history/store.rs +++ b/crates/daemon/src/aclient/history/store.rs @@ -15,13 +15,13 @@ use super::{HISTORY_TAG, HISTORY_VERSION, HISTORY_VERSION_V0}; #[derive(Debug, Clone)] pub(crate) struct HistoryStore { - pub(crate) store: SqliteStore, - pub(crate) host_id: HostId, - pub(crate) encryption_key: [u8; 32], + store: SqliteStore, + host_id: HostId, + encryption_key: [u8; 32], } #[derive(Debug, Eq, PartialEq, Clone)] -pub(crate) enum HistoryRecord { +enum HistoryRecord { Create(History), // Create a history record Delete(HistoryId), // Delete a history record, identified by ID } @@ -39,7 +39,7 @@ impl HistoryRecord { /// twice. /// /// Deletion simply refers to the history by ID - pub(crate) fn serialize(&self) -> Result<DecryptedData> { + fn serialize(&self) -> Result<DecryptedData> { // probably don't actually need to use rmp here, but if we ever need to extend it, it's a // nice wrapper around raw byte stuff use rmp::encode; @@ -65,7 +65,7 @@ impl HistoryRecord { Ok(DecryptedData(output)) } - pub(crate) fn deserialize(bytes: &DecryptedData, version: &str) -> Result<Self> { + fn deserialize(bytes: &DecryptedData, version: &str) -> Result<Self> { use rmp::decode; fn error_report<E: std::fmt::Debug>(err: E) -> eyre::Report { @@ -175,7 +175,7 @@ impl HistoryStore { Ok(()) } - pub(crate) async fn delete(&self, id: HistoryId) -> Result<(RecordId, RecordIdx)> { + async fn delete(&self, id: HistoryId) -> Result<(RecordId, RecordIdx)> { let record = HistoryRecord::Delete(id); self.push_record(record).await @@ -183,7 +183,7 @@ impl HistoryStore { /// Delete a batch of history entries via the record store. /// Returns the record IDs so the caller can run `incremental_build` when ready. - pub(crate) async fn delete_entries( + async fn delete_entries( &self, entries: impl IntoIterator<Item = History>, ) -> Result<Vec<RecordId>> { @@ -203,7 +203,7 @@ impl HistoryStore { self.push_record(record).await } - pub(crate) async fn history(&self) -> Result<Vec<HistoryRecord>> { + async fn history(&self) -> Result<Vec<HistoryRecord>> { // Atm this loads all history into memory // Not ideal as that is potentially quite a lot, although history will be small. let records = self.store.all_tagged(HISTORY_TAG).await?; @@ -226,7 +226,7 @@ impl HistoryStore { Ok(ret) } - pub(crate) async fn build(&self, database: &ClientSqlite) -> Result<()> { + async fn build(&self, database: &ClientSqlite) -> Result<()> { // I'd like to change how we rebuild and not couple this with the database, but need to // consider the structure more deeply. This will be easy to change. @@ -298,7 +298,7 @@ impl HistoryStore { /// Get a list of history IDs that exist in the store /// Note: This currently involves loading all history into memory. This is not going to be a /// large amount in absolute terms, but do not all it in a hot loop. - pub(crate) async fn history_ids(&self) -> Result<HashSet<HistoryId>> { + async fn history_ids(&self) -> Result<HashSet<HistoryId>> { let history = self.history().await?; let ret = history @@ -312,7 +312,7 @@ impl HistoryStore { Ok(ret) } - pub(crate) async fn init_store(&self, db: &ClientSqlite) -> Result<()> { + async fn init_store(&self, db: &ClientSqlite) -> Result<()> { todo!(); // let pb = ProgressBar::new_spinner(); diff --git a/crates/daemon/src/aclient/meta.rs b/crates/daemon/src/aclient/meta.rs index 00dabcce..ea660745 100644 --- a/crates/daemon/src/aclient/meta.rs +++ b/crates/daemon/src/aclient/meta.rs @@ -2,12 +2,12 @@ use std::path::Path; use std::str::FromStr; use std::time::Duration; -use turtle_common::record::HostId; use eyre::{Result, eyre}; use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions}; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use tokio::sync::OnceCell; use tracing::debug; +use turtle_common::record::HostId; use uuid::Uuid; const KEY_HOST_ID: &str = "host_id"; @@ -71,7 +71,7 @@ impl MetaStore { // Generic key-value operations - pub(crate) async fn get(&self, key: &str) -> Result<Option<String>> { + async fn get(&self, key: &str) -> Result<Option<String>> { let row: Option<(String,)> = sqlx::query_as("SELECT value FROM meta WHERE key = ?1") .bind(key) .fetch_optional(&self.pool) @@ -80,7 +80,7 @@ impl MetaStore { Ok(row.map(|r| r.0)) } - pub(crate) async fn set(&self, key: &str, value: &str) -> Result<()> { + async fn set(&self, key: &str, value: &str) -> Result<()> { sqlx::query( " INSERT INTO meta (key, value, updated_at) @@ -154,8 +154,8 @@ mod tests { store.set("foo", "baz").await.unwrap(); assert_eq!(store.get("foo").await.unwrap(), Some("baz".to_string())); - store.delete("foo").await.unwrap(); - assert_eq!(store.get("foo").await.unwrap(), None); + // store.delete("foo").await.unwrap(); + // assert_eq!(store.get("foo").await.unwrap(), None); } #[tokio::test] diff --git a/crates/daemon/src/aclient/mod.rs b/crates/daemon/src/aclient/mod.rs index f2d14e01..fdadb81b 100644 --- a/crates/daemon/src/aclient/mod.rs +++ b/crates/daemon/src/aclient/mod.rs @@ -1,10 +1,10 @@ pub(crate) mod database; -pub mod history; +pub(crate) mod encryption; +pub(crate) mod history; pub(crate) mod record; pub(crate) mod settings; -pub(crate) mod api_client; -pub(crate) mod encryption; -pub(crate) mod meta; -pub(crate) mod ordering; -pub(crate) mod utils; +mod api_client; +mod meta; +mod ordering; +mod utils; diff --git a/crates/daemon/src/aclient/record/encryption.rs b/crates/daemon/src/aclient/record/encryption.rs index 6851a99f..67f191e9 100644 --- a/crates/daemon/src/aclient/record/encryption.rs +++ b/crates/daemon/src/aclient/record/encryption.rs @@ -1,6 +1,3 @@ -use turtle_common::record::{ - AdditionalData, DecryptedData, EncryptedData, Encryption, HostId, RecordId, RecordIdx, -}; use base64::{Engine, engine::general_purpose}; use eyre::{Context, Result, ensure}; use rusty_paserk::{Key, KeyId, Local, PieWrappedKey}; @@ -8,6 +5,9 @@ use rusty_paseto::core::{ ImplicitAssertion, Key as DataKey, Local as LocalPurpose, Paseto, PasetoNonce, Payload, V4, }; use serde::{Deserialize, Serialize}; +use turtle_common::record::{ + AdditionalData, DecryptedData, EncryptedData, Encryption, HostId, RecordId, RecordIdx, +}; /// Use PASETO V4 Local encryption using the additional data as an implicit assertion. #[expect(non_camel_case_types)] diff --git a/crates/daemon/src/aclient/record/mod.rs b/crates/daemon/src/aclient/record/mod.rs index 2ace26f5..4e5774ea 100644 --- a/crates/daemon/src/aclient/record/mod.rs +++ b/crates/daemon/src/aclient/record/mod.rs @@ -1,4 +1,3 @@ pub(crate) mod encryption; +pub(crate) mod sqlite_store; pub(crate) mod sync; - -pub mod sqlite_store; diff --git a/crates/daemon/src/aclient/record/sqlite_store.rs b/crates/daemon/src/aclient/record/sqlite_store.rs index f2fc9d84..2186da54 100644 --- a/crates/daemon/src/aclient/record/sqlite_store.rs +++ b/crates/daemon/src/aclient/record/sqlite_store.rs @@ -24,12 +24,12 @@ use uuid::Uuid; use super::encryption::PASETO_V4; #[derive(Debug, Clone)] -pub struct SqliteStore { +pub(crate) struct SqliteStore { pool: SqlitePool, } impl SqliteStore { - pub async fn new(path: impl AsRef<Path>, timeout: f64) -> Result<Self> { + pub(crate) async fn new(path: impl AsRef<Path>, timeout: f64) -> Result<Self> { fn mk_opts(path: &str) -> sqlx::Result<SqliteConnectOptions> { let opts = SqliteConnectOptions::from_str(path)? .journal_mode(SqliteJournalMode::Wal) @@ -157,7 +157,7 @@ impl SqliteStore { Ok(res) } - pub(crate) async fn delete(&self, id: RecordId) -> Result<()> { + async fn delete(&self, id: RecordId) -> Result<()> { sqlx::query("delete from store where id = ?1") .bind(id.0.as_hyphenated().to_string()) .execute(&self.pool) @@ -166,7 +166,7 @@ impl SqliteStore { Ok(()) } - pub(crate) async fn delete_all(&self) -> Result<()> { + async fn delete_all(&self) -> Result<()> { sqlx::query("delete from store").execute(&self.pool).await?; Ok(()) @@ -192,15 +192,11 @@ impl SqliteStore { } } - pub(crate) async fn first( - &self, - host: HostId, - tag: &str, - ) -> Result<Option<Record<EncryptedData>>> { + async fn first(&self, host: HostId, tag: &str) -> Result<Option<Record<EncryptedData>>> { self.idx(host, tag, 0).await } - pub(crate) async fn len_tag(&self, tag: &str) -> Result<u64> { + async fn len_tag(&self, tag: &str) -> Result<u64> { let res: Result<(i64,), sqlx::Error> = sqlx::query_as("select count(*) from store where tag=?1") .bind(tag) @@ -235,7 +231,7 @@ impl SqliteStore { } /// Get the first record for a given host and tag - pub(crate) async fn idx( + async fn idx( &self, host: HostId, tag: &str, @@ -293,7 +289,7 @@ impl SqliteStore { /// Reencrypt every single item in this store with a new key /// Be careful - this may mess with sync. - pub(crate) async fn re_encrypt(&self, old_key: &[u8; 32], new_key: &[u8; 32]) -> Result<()> { + async fn re_encrypt(&self, old_key: &[u8; 32], new_key: &[u8; 32]) -> Result<()> { // Load all the records // In memory like some of the other code here // This will never be called in a hot loop, and only under the following circumstances @@ -332,7 +328,7 @@ impl SqliteStore { /// Verify that every record in this store can be decrypted with the current key /// Someday maybe also check each tag/record can be deserialized, but not for now. - pub(crate) async fn verify(&self, key: &[u8; 32]) -> Result<()> { + async fn verify(&self, key: &[u8; 32]) -> Result<()> { let all = self.load_all().await?; all.into_iter() @@ -344,7 +340,7 @@ impl SqliteStore { /// Verify that every record in this store can be decrypted with the current key /// Someday maybe also check each tag/record can be deserialized, but not for now. - pub(crate) async fn purge(&self, key: &[u8; 32]) -> Result<()> { + async fn purge(&self, key: &[u8; 32]) -> Result<()> { let all = self.load_all().await?; for record in &all { diff --git a/crates/daemon/src/aclient/record/sync.rs b/crates/daemon/src/aclient/record/sync.rs index 94764f67..79239b99 100644 --- a/crates/daemon/src/aclient/record/sync.rs +++ b/crates/daemon/src/aclient/record/sync.rs @@ -9,8 +9,8 @@ use super::encryption::PASETO_V4; use crate::aclient::record::sqlite_store::SqliteStore; use crate::aclient::{api_client::Client, settings::Settings}; -use turtle_common::record::{Diff, HostId, RecordId, RecordIdx, RecordStatus}; use indicatif::{ProgressBar, ProgressState, ProgressStyle}; +use turtle_common::record::{Diff, HostId, RecordId, RecordIdx, RecordStatus}; #[derive(Error, Debug)] pub(crate) enum SyncError { @@ -36,7 +36,7 @@ pub(crate) enum SyncError { } #[derive(Debug, Eq, PartialEq)] -pub(crate) enum Operation { +enum Operation { // Either upload or download until the states matches the below Upload { local: RecordIdx, @@ -56,7 +56,7 @@ pub(crate) enum Operation { }, } -pub(crate) fn build_client(settings: &Settings) -> Result<Client<'_>, SyncError> { +fn build_client(settings: &Settings) -> Result<Client<'_>, SyncError> { Client::new( &settings.sync.address, settings.network_connect_timeout, @@ -71,7 +71,7 @@ pub(crate) fn build_client(settings: &Settings) -> Result<Client<'_>, SyncError> .map_err(|e| SyncError::OperationalError { msg: e.to_string() }) } -pub(crate) async fn diff( +async fn diff( client: &Client<'_>, store: &SqliteStore, ) -> Result<(Vec<Diff>, RecordStatus), SyncError> { @@ -94,10 +94,7 @@ pub(crate) async fn diff( // With the store as context, we can determine if a tail exists locally or not and therefore if it needs uploading or download. // In theory this could be done as a part of the diffing stage, but it's easier to reason // about and test this way -pub(crate) fn operations( - diffs: Vec<Diff>, - _store: &SqliteStore, -) -> Result<Vec<Operation>, SyncError> { +fn operations(diffs: Vec<Diff>, _store: &SqliteStore) -> Result<Vec<Operation>, SyncError> { let mut operations = Vec::with_capacity(diffs.len()); for diff in diffs { @@ -283,7 +280,7 @@ async fn sync_download( Ok(ret) } -pub(crate) async fn sync_remote( +async fn sync_remote( client: &Client<'_>, operations: Vec<Operation>, local_store: &SqliteStore, @@ -323,7 +320,7 @@ pub(crate) async fn sync_remote( Ok((uploaded, downloaded)) } -pub(crate) async fn check_encryption_key( +async fn check_encryption_key( client: &Client<'_>, remote_index: &RecordStatus, encryption_key: &[u8; 32], diff --git a/crates/daemon/src/aclient/settings/meta.rs b/crates/daemon/src/aclient/settings/meta.rs index 7993ef6d..1c9b9cd1 100644 --- a/crates/daemon/src/aclient/settings/meta.rs +++ b/crates/daemon/src/aclient/settings/meta.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone)] pub(crate) struct Settings { - pub(crate) db_path: String, + pub(super) db_path: String, } impl Default for Settings { diff --git a/crates/daemon/src/aclient/settings/mod.rs b/crates/daemon/src/aclient/settings/mod.rs index 58afd17c..10c84f50 100644 --- a/crates/daemon/src/aclient/settings/mod.rs +++ b/crates/daemon/src/aclient/settings/mod.rs @@ -24,8 +24,7 @@ static DATA_DIR: OnceLock<PathBuf> = OnceLock::new(); static META_CONFIG: OnceLock<(String, f64)> = OnceLock::new(); static META_STORE: OnceCell<crate::aclient::meta::MetaStore> = OnceCell::const_new(); -pub(crate) mod meta; -pub(crate) mod watcher; +mod meta; #[derive(Clone, Debug, Deserialize, Copy, ValueEnum, PartialEq, Serialize)] pub(crate) enum SearchMode { @@ -48,7 +47,7 @@ pub(crate) enum SearchMode { } impl SearchMode { - pub(crate) fn as_str(self) -> &'static str { + fn as_str(self) -> &'static str { match self { Self::Prefix => "PREFIX", Self::FullText => "FULLTXT", @@ -57,7 +56,7 @@ impl SearchMode { Self::DaemonFuzzy => "DAEMON", } } - pub(crate) fn next(self, settings: &Settings) -> Self { + fn next(self, settings: &Settings) -> Self { match self { Self::Prefix => Self::FullText, // if the user is using skim, we go to skim @@ -93,7 +92,7 @@ pub(crate) enum FilterMode { } impl FilterMode { - pub(crate) fn as_str(self) -> &'static str { + fn as_str(self) -> &'static str { match self { Self::Global => "GLOBAL", Self::Host => "HOST", @@ -106,7 +105,7 @@ impl FilterMode { } #[derive(Clone, Debug, Deserialize, Copy, Serialize)] -pub(crate) enum ExitMode { +enum ExitMode { #[serde(rename = "return-original")] ReturnOriginal, @@ -117,7 +116,7 @@ pub(crate) enum ExitMode { // FIXME: Can use upstream Dialect enum if https://github.com/stevedonovan/chrono-english/pull/16 is merged // FIXME: Above PR was merged, but dependency was changed to interim (fork of chrono-english) in the ... interim #[derive(Clone, Debug, Deserialize, Copy, Serialize)] -pub(crate) enum Dialect { +enum Dialect { #[serde(rename = "us")] Us, @@ -141,7 +140,7 @@ impl From<Dialect> for interim::Dialect { /// /// See: <https://github.com/atuinsh/atuin/pull/1517#discussion_r1447516426> #[derive(Clone, Copy, Debug, Eq, PartialEq, DeserializeFromStr, Serialize)] -pub(crate) struct Timezone(pub(crate) UtcOffset); +struct Timezone(UtcOffset); impl fmt::Display for Timezone { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) @@ -185,7 +184,7 @@ impl FromStr for Timezone { } #[derive(Clone, Debug, Deserialize, Copy, Serialize)] -pub(crate) enum Style { +enum Style { #[serde(rename = "auto")] Auto, @@ -197,7 +196,7 @@ pub(crate) enum Style { } #[derive(Clone, Debug, Deserialize, Copy, Serialize)] -pub(crate) enum WordJumpMode { +enum WordJumpMode { #[serde(rename = "emacs")] Emacs, @@ -206,7 +205,7 @@ pub(crate) enum WordJumpMode { } #[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum, Serialize)] -pub(crate) enum KeymapMode { +enum KeymapMode { #[serde(rename = "emacs")] Emacs, @@ -226,7 +225,7 @@ pub(crate) enum KeymapMode { // used in HashMap (https://stackoverflow.com/questions/67142663). We instead // define an adapter type. #[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum, Serialize)] -pub(crate) enum CursorStyle { +enum CursorStyle { #[serde(rename = "default")] DefaultUserShape, @@ -250,13 +249,13 @@ pub(crate) enum CursorStyle { } #[derive(Clone, Debug, Deserialize, Serialize)] -pub(crate) struct Stats { +struct Stats { #[serde(default = "Stats::common_prefix_default")] - pub(crate) common_prefix: Vec<String>, // sudo, etc. commands we want to strip off + common_prefix: Vec<String>, // sudo, etc. commands we want to strip off #[serde(default = "Stats::common_subcommands_default")] - pub(crate) common_subcommands: Vec<String>, // kubectl, commands we should consider subcommands for + common_subcommands: Vec<String>, // kubectl, commands we should consider subcommands for #[serde(default = "Stats::ignored_commands_default")] - pub(crate) ignored_commands: Vec<String>, // cd, ls, etc. commands we want to completely hide from stats + ignored_commands: Vec<String>, // cd, ls, etc. commands we want to completely hide from stats } impl Stats { @@ -310,19 +309,19 @@ impl Default for Stats { #[derive(Clone, Debug, Deserialize, Default, Serialize)] #[expect(clippy::struct_excessive_bools)] -pub(crate) struct Keys { - pub(crate) scroll_exits: bool, - pub(crate) exit_past_line_start: bool, - pub(crate) accept_past_line_end: bool, - pub(crate) accept_past_line_start: bool, - pub(crate) accept_with_backspace: bool, - pub(crate) prefix: String, +struct Keys { + scroll_exits: bool, + exit_past_line_start: bool, + accept_past_line_end: bool, + accept_past_line_start: bool, + accept_with_backspace: bool, + prefix: String, } impl Keys { /// The standard default values for all `[keys]` options. /// These match the config defaults set in `builder_with_data_dir()`. - pub(crate) fn standard_defaults() -> Self { + fn standard_defaults() -> Self { Self { scroll_exits: true, exit_past_line_start: true, @@ -336,19 +335,19 @@ impl Keys { /// A single rule within a conditional keybinding config. #[derive(Clone, Debug, Deserialize, Serialize)] -pub(crate) struct KeyRuleConfig { +struct KeyRuleConfig { /// Optional condition expression (e.g. "cursor-at-start", "input-empty && no-results"). /// If absent, the rule always matches. #[serde(default)] - pub(crate) when: Option<String>, + when: Option<String>, /// The action to perform (e.g. "exit", "cursor-left", "accept"). - pub(crate) action: String, + action: String, } /// A keybinding config value: either a simple action string or an ordered list of conditional rules. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(untagged)] -pub(crate) enum KeyBindingConfig { +enum KeyBindingConfig { /// Simple unconditional binding: `"ctrl-c" = "return-original"` Simple(String), /// Conditional binding: `"left" = [{ when = "cursor-at-start", action = "exit" }, { action = "cursor-left" }]` @@ -358,22 +357,22 @@ pub(crate) enum KeyBindingConfig { /// User-facing keymap configuration. Each mode maps key strings to bindings. /// Keys present here override the defaults for that key; unmentioned keys keep defaults. #[derive(Clone, Debug, Deserialize, Serialize, Default)] -pub(crate) struct KeymapConfig { +struct KeymapConfig { #[serde(default)] - pub(crate) emacs: HashMap<String, KeyBindingConfig>, + emacs: HashMap<String, KeyBindingConfig>, #[serde(default, rename = "vim-normal")] - pub(crate) vim_normal: HashMap<String, KeyBindingConfig>, + vim_normal: HashMap<String, KeyBindingConfig>, #[serde(default, rename = "vim-insert")] - pub(crate) vim_insert: HashMap<String, KeyBindingConfig>, + vim_insert: HashMap<String, KeyBindingConfig>, #[serde(default)] - pub(crate) inspector: HashMap<String, KeyBindingConfig>, + inspector: HashMap<String, KeyBindingConfig>, #[serde(default)] - pub(crate) prefix: HashMap<String, KeyBindingConfig>, + prefix: HashMap<String, KeyBindingConfig>, } impl KeymapConfig { /// Returns true if no keybinding overrides are configured in any mode. - pub(crate) fn is_empty(&self) -> bool { + fn is_empty(&self) -> bool { self.emacs.is_empty() && self.vim_normal.is_empty() && self.vim_insert.is_empty() @@ -383,50 +382,50 @@ impl KeymapConfig { } #[derive(Clone, Debug, Deserialize, Serialize)] -pub(crate) struct Preview { - pub(crate) strategy: PreviewStrategy, +struct Preview { + strategy: PreviewStrategy, } #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Daemon { +pub(crate) struct Daemon { /// The daemon will handle sync on an interval. How often to sync, in seconds. - pub sync_frequency: u64, + pub(crate) sync_frequency: u64, /// The path to the unix socket used by the daemon - pub socket_path: String, + pub(crate) socket_path: String, /// Path to the daemon pidfile used for process coordination. - pub pidfile_path: String, + pub(crate) pidfile_path: String, /// Use a socket passed via systemd's socket activation protocol, instead of the path - pub systemd_socket: bool, + pub(crate) systemd_socket: bool, /// The port that should be used for TCP on non unix systems - pub tcp_port: u64, + tcp_port: u64, } #[derive(Clone, Debug, Deserialize, Serialize)] -pub(crate) struct Search { +struct Search { /// The list of enabled filter modes, in order of priority. - pub(crate) filters: Vec<FilterMode>, + filters: Vec<FilterMode>, /// The recency score multiplier for the search index (default: 1.0). /// Values < 1.0 reduce weight, > 1.0 increase weight, 0.0 disables. - pub(crate) recency_score_multiplier: f64, + recency_score_multiplier: f64, /// The frequency score multiplier for the search index (default: 1.0). /// Values < 1.0 reduce weight, > 1.0 increase weight, 0.0 disables. - pub(crate) frequency_score_multiplier: f64, + frequency_score_multiplier: f64, /// The overall frecency score multiplier for the search index (default: 1.0). /// Applied after combining recency and frequency scores. - pub(crate) frecency_score_multiplier: f64, + frecency_score_multiplier: f64, } /// Log level for file logging. Maps to tracing's [`LevelFilter`]. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] -pub(crate) enum LogLevel { +enum LogLevel { Trace, Debug, #[default] @@ -437,7 +436,7 @@ pub(crate) enum LogLevel { impl LogLevel { /// Convert to a tracing directive string for use with [`EnvFilter`]. - pub(crate) fn as_directive(self) -> &'static str { + fn as_directive(self) -> &'static str { match self { Self::Trace => "trace", Self::Debug => "debug", @@ -450,45 +449,45 @@ impl LogLevel { /// Configuration for a specific log type (search or daemon). #[derive(Clone, Debug, Default, Deserialize, Serialize)] -pub(crate) struct LogConfig { +struct LogConfig { /// Log file name (relative to dir) or absolute path. - pub(crate) file: String, + file: String, /// Override global enabled setting for this log type. - pub(crate) enabled: Option<bool>, + enabled: Option<bool>, /// Override global level setting for this log type. - pub(crate) level: Option<LogLevel>, + level: Option<LogLevel>, /// Override global retention days setting for this log type. - pub(crate) retention: Option<u64>, + retention: Option<u64>, } #[derive(Clone, Debug, Deserialize, Serialize)] -pub(crate) struct Logs { +struct Logs { /// Enable file logging globally. Defaults to true. #[serde(default = "Logs::default_enabled")] - pub(crate) enabled: bool, + enabled: bool, /// Directory for log files. Defaults to ~/.atuin/logs - pub(crate) dir: String, + dir: String, /// Default log level for file logging. Defaults to "info". /// Note: [`ATUIN_LOG`] environment variable overrides this. #[serde(default)] - pub(crate) level: LogLevel, + level: LogLevel, /// Default retention days for log files. Defaults to 4. #[serde(default = "Logs::default_retention")] - pub(crate) retention: u64, + retention: u64, /// Search log settings #[serde(default)] - pub(crate) search: LogConfig, + search: LogConfig, /// Daemon log settings #[serde(default)] - pub(crate) daemon: LogConfig, + daemon: LogConfig, } impl Default for Preview { @@ -541,37 +540,37 @@ impl Logs { /// Returns whether search logging is enabled. /// Uses search-specific setting if set, otherwise falls back to global. - pub(crate) fn search_enabled(&self) -> bool { + fn search_enabled(&self) -> bool { self.search.enabled.unwrap_or(self.enabled) } /// Returns whether daemon logging is enabled. /// Uses daemon-specific setting if set, otherwise falls back to global. - pub(crate) fn daemon_enabled(&self) -> bool { + fn daemon_enabled(&self) -> bool { self.daemon.enabled.unwrap_or(self.enabled) } /// Returns the log level for search logging. /// Uses search-specific setting if set, otherwise falls back to global. - pub(crate) fn search_level(&self) -> LogLevel { + fn search_level(&self) -> LogLevel { self.search.level.unwrap_or(self.level) } /// Returns the log level for daemon logging. /// Uses daemon-specific setting if set, otherwise falls back to global. - pub(crate) fn daemon_level(&self) -> LogLevel { + fn daemon_level(&self) -> LogLevel { self.daemon.level.unwrap_or(self.level) } /// Returns the retention days for search logging. /// Uses search-specific setting if set, otherwise falls back to global. - pub(crate) fn search_retention(&self) -> u64 { + fn search_retention(&self) -> u64 { self.search.retention.unwrap_or(self.retention) } /// Returns the retention days for daemon logging. /// Uses daemon-specific setting if set, otherwise falls back to global. - pub(crate) fn daemon_retention(&self) -> u64 { + fn daemon_retention(&self) -> u64 { self.daemon.retention.unwrap_or(self.retention) } } @@ -597,7 +596,7 @@ impl Default for Search { // The preview height strategy also takes max_preview_height into account. #[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum, Serialize)] -pub(crate) enum PreviewStrategy { +enum PreviewStrategy { // Preview height is calculated for the length of the selected command. #[serde(rename = "auto")] Auto, @@ -614,7 +613,7 @@ pub(crate) enum PreviewStrategy { /// Column types available for the interactive search UI. #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(rename_all = "lowercase")] -pub(crate) enum UiColumnType { +enum UiColumnType { /// Command execution duration (e.g., "123ms") Duration, /// Relative time since execution (e.g., "59s ago") @@ -636,7 +635,7 @@ pub(crate) enum UiColumnType { impl UiColumnType { /// Returns the default width for this column type (in characters). /// The Command column returns 0 as it expands to fill remaining space. - pub(crate) fn default_width(self) -> u16 { + fn default_width(self) -> u16 { match self { Self::Duration => 5, // "814ms" Self::Time => 9, // "459ms ago" @@ -659,15 +658,15 @@ impl UiColumnType { /// A column configuration with type and optional custom width. /// Can be specified as just a string (uses default width) or as an object with type and width. #[derive(Clone, Debug, Serialize)] -pub(crate) struct UiColumn { - pub(crate) column_type: UiColumnType, - pub(crate) width: u16, +struct UiColumn { + column_type: UiColumnType, + width: u16, /// If true, this column expands to fill remaining space. Only one column should expand. - pub(crate) expand: bool, + expand: bool, } impl UiColumn { - pub(crate) fn new(column_type: UiColumnType) -> Self { + fn new(column_type: UiColumnType) -> Self { Self { width: column_type.default_width(), expand: column_type == UiColumnType::Command, @@ -747,13 +746,13 @@ impl<'de> Deserialize<'de> for UiColumn { /// UI-specific settings for the interactive search. #[derive(Clone, Debug, Deserialize, Serialize)] -pub(crate) struct Ui { +struct Ui { /// Columns to display in interactive search, from left to right. /// The indicator column (" > ") is always shown first implicitly. /// The "command" column should be last as it expands to fill remaining space. /// Can be simple strings or objects with type and width. #[serde(default = "Ui::default_columns")] - pub(crate) columns: Vec<UiColumn>, + columns: Vec<UiColumn>, } impl Ui { @@ -767,7 +766,7 @@ impl Ui { /// Validate the UI configuration. /// Returns an error if more than one column has expand = true. - pub(crate) fn validate(&self) -> Result<()> { + fn validate(&self) -> Result<()> { let expand_count = self.columns.iter().filter(|c| c.expand).count(); if expand_count > 1 { bail!( @@ -794,13 +793,13 @@ pub(crate) struct Sync { pub(crate) address: String, #[serde(default)] - pub(crate) frequency: String, + frequency: String, #[serde(default)] pub(crate) auto: bool, #[serde(default)] - pub(crate) user_id_path: Option<PathBuf>, + user_id_path: Option<PathBuf>, #[serde(default)] pub(crate) encryption_key_path: Option<PathBuf>, @@ -853,93 +852,93 @@ impl Sync { #[derive(Clone, Debug, Deserialize, Serialize)] #[expect(clippy::struct_excessive_bools)] -pub struct Settings { - pub(crate) data_dir: Option<String>, - pub(crate) dialect: Dialect, - pub(crate) timezone: Timezone, - pub(crate) style: Style, +pub(crate) struct Settings { + data_dir: Option<String>, + dialect: Dialect, + timezone: Timezone, + style: Style, - pub db_path: String, - pub record_store_path: String, - pub(crate) search_mode: SearchMode, - pub(crate) filter_mode: Option<FilterMode>, - pub(crate) filter_mode_shell_up_key_binding: Option<FilterMode>, - pub(crate) search_mode_shell_up_key_binding: Option<SearchMode>, - pub(crate) shell_up_key_binding: bool, - pub(crate) inline_height: u16, - pub(crate) inline_height_shell_up_key_binding: Option<u16>, - pub(crate) invert: bool, - pub(crate) show_preview: bool, - pub(crate) max_preview_height: u16, - pub(crate) show_help: bool, - pub(crate) show_tabs: bool, - pub(crate) show_numeric_shortcuts: bool, - pub(crate) auto_hide_height: u16, - pub(crate) exit_mode: ExitMode, - pub(crate) keymap_mode: KeymapMode, - pub(crate) keymap_mode_shell: KeymapMode, - pub(crate) keymap_cursor: HashMap<String, CursorStyle>, - pub(crate) word_jump_mode: WordJumpMode, - pub(crate) word_chars: String, - pub(crate) scroll_context_lines: usize, - pub(crate) history_format: String, - pub(crate) strip_trailing_whitespace: bool, - pub(crate) prefers_reduced_motion: bool, - pub(crate) store_failed: bool, - pub(crate) no_mouse: bool, + pub(crate) db_path: String, + pub(crate) record_store_path: String, + search_mode: SearchMode, + filter_mode: Option<FilterMode>, + filter_mode_shell_up_key_binding: Option<FilterMode>, + search_mode_shell_up_key_binding: Option<SearchMode>, + shell_up_key_binding: bool, + inline_height: u16, + inline_height_shell_up_key_binding: Option<u16>, + invert: bool, + show_preview: bool, + max_preview_height: u16, + show_help: bool, + show_tabs: bool, + show_numeric_shortcuts: bool, + auto_hide_height: u16, + exit_mode: ExitMode, + keymap_mode: KeymapMode, + keymap_mode_shell: KeymapMode, + keymap_cursor: HashMap<String, CursorStyle>, + word_jump_mode: WordJumpMode, + word_chars: String, + scroll_context_lines: usize, + history_format: String, + strip_trailing_whitespace: bool, + prefers_reduced_motion: bool, + store_failed: bool, + no_mouse: bool, #[serde(with = "serde_regex", default = "RegexSet::empty", skip_serializing)] - pub(crate) history_filter: RegexSet, + history_filter: RegexSet, #[serde(with = "serde_regex", default = "RegexSet::empty", skip_serializing)] - pub(crate) cwd_filter: RegexSet, + cwd_filter: RegexSet, - pub(crate) secrets_filter: bool, - pub(crate) workspaces: bool, - pub(crate) ctrl_n_shortcuts: bool, + secrets_filter: bool, + workspaces: bool, + ctrl_n_shortcuts: bool, pub(crate) network_connect_timeout: u64, pub(crate) network_timeout: u64, - pub local_timeout: f64, - pub(crate) enter_accept: bool, - pub(crate) smart_sort: bool, - pub(crate) command_chaining: bool, + pub(crate) local_timeout: f64, + enter_accept: bool, + smart_sort: bool, + command_chaining: bool, #[serde(default)] pub(crate) sync: Sync, #[serde(default)] - pub(crate) stats: Stats, + stats: Stats, #[serde(default)] - pub(crate) keys: Keys, + keys: Keys, #[serde(default)] - pub(crate) keymap: KeymapConfig, + keymap: KeymapConfig, #[serde(default)] - pub(crate) preview: Preview, + preview: Preview, #[serde(default)] - pub daemon: Daemon, + pub(crate) daemon: Daemon, #[serde(default)] - pub(crate) search: Search, + search: Search, #[serde(default)] - pub(crate) ui: Ui, + ui: Ui, #[serde(default)] - pub(crate) logs: Logs, + logs: Logs, #[serde(default)] - pub(crate) meta: meta::Settings, + meta: meta::Settings, } impl Settings { // -- Meta store: lazily initialized on first access -- - pub(crate) async fn meta_store() -> Result<&'static crate::aclient::meta::MetaStore> { + async fn meta_store() -> Result<&'static crate::aclient::meta::MetaStore> { META_STORE .get_or_try_init(|| async { let (db_path, timeout) = META_CONFIG.get().ok_or_else(|| { @@ -954,7 +953,7 @@ impl Settings { Self::meta_store().await?.host_id().await } - pub(crate) async fn last_sync() -> Result<OffsetDateTime> { + async fn last_sync() -> Result<OffsetDateTime> { Self::meta_store().await?.last_sync().await } @@ -962,7 +961,7 @@ impl Settings { Self::meta_store().await?.save_sync_time().await } - pub(crate) fn default_filter_mode(&self, git_root: bool) -> FilterMode { + fn default_filter_mode(&self, git_root: bool) -> FilterMode { self.filter_mode .filter(|x| self.search.filters.contains(x)) .or_else(|| { @@ -979,7 +978,7 @@ impl Settings { .unwrap_or(FilterMode::Global) } - pub(crate) fn builder() -> Result<ConfigBuilder<DefaultState>> { + fn builder() -> Result<ConfigBuilder<DefaultState>> { Self::builder_with_data_dir(&utils::data_dir()) } @@ -1230,7 +1229,7 @@ impl Settings { /// Returns the effective value after merging defaults, config file, and /// environment — without the side-effects of full `Settings` construction /// (meta store init, path expansion, etc.). - pub(crate) fn get_config_value(key: &str) -> Result<String> { + fn get_config_value(key: &str) -> Result<String> { let config = Self::build_config()?; let value: config::Value = config .get(key) @@ -1289,7 +1288,7 @@ impl Settings { } } - pub fn new() -> Result<Self> { + pub(crate) fn new() -> Result<Self> { let config = Self::build_config()?; let settings: Self = config .try_deserialize() @@ -1312,7 +1311,7 @@ impl Settings { .map_err(|e| eyre!("failed to expand path: {}", e)) } - pub(crate) fn paths_ok(&self) -> bool { + fn paths_ok(&self) -> bool { // TODO(@bpeetz): Add the `sync.*` paths <2026-06-11> let paths = [&self.db_path, &self.record_store_path, &self.meta.db_path]; paths.iter().all(|p| !utils::broken_symlink(p)) diff --git a/crates/daemon/src/aclient/settings/watcher.rs b/crates/daemon/src/aclient/settings/watcher.rs deleted file mode 100644 index 01d20855..00000000 --- a/crates/daemon/src/aclient/settings/watcher.rs +++ /dev/null @@ -1,260 +0,0 @@ -//! Config file watching for automatic settings reload. -//! -//! This module provides a `SettingsWatcher` that monitors the config file -//! for changes and broadcasts updated settings via a `tokio::sync::watch` channel. -//! -//! # Example -//! -//! ```no_run -//! use crate::aclient::settings::watcher::global_settings_watcher; -//! -//! async fn example() -> eyre::Result<()> { -//! let watcher = global_settings_watcher()?; -//! let mut rx = watcher.subscribe(); -//! -//! // React to settings changes -//! while rx.changed().await.is_ok() { -//! let settings = rx.borrow(); -//! println!("Settings updated!"); -//! } -//! Ok(()) -//! } -//! ``` - -use std::{ - path::{Path, PathBuf}, - sync::{Arc, OnceLock}, - time::Duration, -}; - -use eyre::{Result, WrapErr}; -use log::{debug, error, info, warn}; -use notify::{ - Config as NotifyConfig, RecommendedWatcher, RecursiveMode, Watcher, - event::{EventKind, ModifyKind}, -}; -use tokio::sync::watch; - -use super::Settings; - -/// Global singleton for the settings watcher. -static SETTINGS_WATCHER: OnceLock<Result<SettingsWatcher, String>> = OnceLock::new(); - -/// Get the global settings watcher singleton. -/// -/// Initializes the watcher on first call. Subsequent calls return the same instance. -/// The watcher monitors the config file for changes and broadcasts updates. -pub(crate) fn global_settings_watcher() -> Result<&'static SettingsWatcher> { - let result = SETTINGS_WATCHER.get_or_init(|| SettingsWatcher::new().map_err(|e| e.to_string())); - - match result { - Ok(watcher) => Ok(watcher), - Err(e) => Err(eyre::eyre!("{}", e)), - } -} - -/// Watches the config file for changes and broadcasts updated settings. -/// -/// Uses `notify` for cross-platform file watching and `tokio::sync::watch` -/// for efficient broadcast to multiple subscribers. -pub(crate) struct SettingsWatcher { - /// Receiver for settings updates. Clone this to subscribe. - rx: watch::Receiver<Arc<Settings>>, - /// Keeps the file watcher alive for the lifetime of this struct. - _watcher: RecommendedWatcher, -} - -impl SettingsWatcher { - /// Create a new settings watcher. - /// - /// Loads initial settings and starts watching the config file for changes. - /// Changes are debounced (500ms) to avoid multiple reloads during saves. - pub(crate) fn new() -> Result<Self> { - let initial_settings = Arc::new(Settings::new()?); - let (tx, rx) = watch::channel(initial_settings); - - let config_path = Self::config_path(); - info!("starting config file watcher: {}", config_path.display()); - - let watcher = Self::create_watcher(tx, &config_path)?; - - Ok(Self { - rx, - _watcher: watcher, - }) - } - - /// Subscribe to settings updates. - /// - /// Returns a receiver that will be notified when settings change. - /// Use `changed().await` to wait for the next update, then `borrow()` - /// to access the current settings. - pub(crate) fn subscribe(&self) -> watch::Receiver<Arc<Settings>> { - self.rx.clone() - } - - /// Get the config file path. - fn config_path() -> PathBuf { - let config_dir = std::env::var("ATUIN_CONFIG_DIR") - .map_or_else(|_| turtle_common::utils::config_dir(), PathBuf::from); - config_dir.join("config.toml") - } - - /// Create the file watcher with debouncing. - fn create_watcher( - tx: watch::Sender<Arc<Settings>>, - config_path: &Path, - ) -> Result<RecommendedWatcher> { - // Channel for debouncing file events - let (debounce_tx, debounce_rx) = std::sync::mpsc::channel::<()>(); - - // Spawn debounce thread - let config_path_clone = config_path.to_owned(); - std::thread::spawn(move || { - Self::debounce_loop(&debounce_rx, &tx, &config_path_clone); - }); - - // Clone config_path for use in the watcher callback - let config_path_for_watcher = config_path.to_owned(); - - // Canonicalize config path for reliable comparison on macOS - // (handles symlinks like /var -> /private/var) - let canonical_config_path = config_path_for_watcher - .canonicalize() - .unwrap_or_else(|_| config_path_for_watcher.clone()); - - // Create file watcher - let mut watcher = RecommendedWatcher::new( - move |res: Result<notify::Event, notify::Error>| { - match res { - Ok(event) => { - // Defensive: if paths is empty, we can't filter, so assume - // it might be our config file and trigger a reload to be safe - if event.paths.is_empty() { - warn!( - "config watcher: event has no paths, triggering reload to be safe" - ); - debounce_tx.send(()).expect("should still be active"); - return; - } - - // Only react to events for our specific config file - // (filter out editor temp files, backups, etc.) - let is_config_file = event.paths.iter().any(|path| { - // Canonicalize for reliable comparison (handles macOS symlinks) - let canonical_event_path = - path.canonicalize().unwrap_or_else(|_| path.clone()); - - // Check if this event is for our config file - // (either exact match or the file was renamed to our config) - canonical_event_path == canonical_config_path - || path.file_name() == config_path_for_watcher.file_name() - }); - - if !is_config_file { - return; - } - - // Only react to modify events (content changes) or creates - if matches!( - event.kind, - EventKind::Modify(ModifyKind::Data(_) | ModifyKind::Any) - | EventKind::Create(_) - ) { - debug!("config file event detected: {event:?}"); - // Send to debounce channel (ignore send errors - receiver might be gone) - debounce_tx.send(()).ok(); - } - } - Err(e) => { - error!("file watcher error: {e}"); - } - } - }, - NotifyConfig::default(), - ) - .wrap_err("failed to create file watcher")?; - - // Watch the config file's parent directory (some editors create new files) - let watch_path = config_path.parent().unwrap_or(config_path); - - // Defensive: ensure watch path exists before trying to watch - if !watch_path.exists() { - warn!( - "config directory does not exist, creating it: {}", - watch_path.display() - ); - std::fs::create_dir_all(watch_path).wrap_err_with(|| { - format!( - "failed to create config directory: {}", - watch_path.display() - ) - })?; - } - - watcher - .watch(watch_path, RecursiveMode::NonRecursive) - .wrap_err_with(|| { - format!("failed to watch config directory: {}", watch_path.display()) - })?; - - info!( - "config file watcher initialized for: {}", - watch_path.display() - ); - Ok(watcher) - } - - /// Debounce loop that batches file events and reloads settings. - fn debounce_loop( - rx: &std::sync::mpsc::Receiver<()>, - tx: &watch::Sender<Arc<Settings>>, - config_path: &Path, - ) { - const DEBOUNCE_DURATION: Duration = Duration::from_millis(500); - - loop { - // Wait for first event - if rx.recv().is_err() { - // Channel closed, watcher was dropped - debug!("config watcher debounce loop exiting"); - return; - } - - // Drain any additional events within debounce window - while rx.recv_timeout(DEBOUNCE_DURATION).is_ok() { - // Keep draining - } - - // Defensive: check if config file exists before reloading - // (handles case where file was deleted - we'll get notified when it's recreated) - if !config_path.exists() { - debug!( - "config file does not exist, skipping reload: {}", - config_path.display() - ); - continue; - } - - // Now reload settings - info!( - "config file changed, reloading settings: {}", - config_path.display() - ); - match Settings::new() { - Ok(settings) => { - if tx.send(Arc::new(settings)).is_err() { - // All receivers dropped - debug!("all settings subscribers dropped, exiting"); - return; - } - info!("settings reloaded successfully"); - } - Err(e) => { - warn!("failed to reload settings: {e}"); - // Keep the old settings, don't broadcast the error - } - } - } - } -} diff --git a/crates/daemon/src/api/control.rs b/crates/daemon/src/api/control.rs index a9d9cff3..94fb7ce9 100644 --- a/crates/daemon/src/api/control.rs +++ b/crates/daemon/src/api/control.rs @@ -95,7 +95,9 @@ impl Control for ControlService { &self, _request: Request<ForceSyncRequest>, ) -> Result<Response<ForceSyncReply>, Status> { - let reply = ForceSyncReply { accepted: false }; + let reply = ForceSyncReply { accepted: true }; + + self.handle.emit(DaemonEvent::ForceSync); Ok(Response::new(reply)) } @@ -212,7 +214,6 @@ async fn do_sync_tick( Err(e) => { tracing::error!("sync tick failed with {e}"); - // Emit failure event handle.emit(DaemonEvent::SyncFailed { error: e.to_string(), }); @@ -251,9 +252,6 @@ async fn do_sync_tick( tracing::error!("failed to build history from downloaded records: {e}"); } - // Emit the records added event (for search indexing) - handle.emit(DaemonEvent::RecordsAdded(downloaded_records.clone())); - // Emit sync completed event handle.emit(DaemonEvent::SyncCompleted { uploaded: uploaded_count as usize, diff --git a/crates/daemon/src/api/history.rs b/crates/daemon/src/api/history.rs index bcd2ee5a..d024ea92 100644 --- a/crates/daemon/src/api/history.rs +++ b/crates/daemon/src/api/history.rs @@ -8,11 +8,7 @@ use tonic::{Request, Response, Status}; use tracing::{Level, instrument}; use crate::{ - aclient::{ - database::{ClientSqlite, current_context}, - history::store::HistoryStore, - settings::Settings, - }, + aclient::{history::store::HistoryStore, settings::Settings}, daemon::DaemonHandle, events::DaemonEvent, }; @@ -41,12 +37,10 @@ pub(crate) struct HistoryService { /// History store for pushing records history_store: HistoryStore, - - history_db: ClientSqlite, } impl HistoryService { - pub(crate) async fn new(handle: DaemonHandle, history_db: ClientSqlite) -> Result<Self> { + pub(crate) async fn new(handle: DaemonHandle) -> Result<Self> { let host_id = Settings::host_id().await?; let history_store = HistoryStore::new(handle.store().clone(), host_id, *handle.encryption_key()); @@ -55,7 +49,6 @@ impl HistoryService { running: DashMap::new(), handle, history_store, - history_db, }) } @@ -91,18 +84,15 @@ impl HistorySvc for HistoryService { ) -> Result<Response<HistoryReply>, Status> { let req = request.into_inner(); - let context = current_context(req.session) - .await - .map_err(|e| Status::internal(format!("failed to aquire context: {e:?}")))?; - 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(); - self.history_db.range(from, to).await + self.handle.history_db().range(from, to).await } else { - self.history_db - .list(&[], &context, None, false, false) + self.handle + .history_db() + .list(None, None, false, false) .await } .map_err(|e| Status::internal(format!("failed to read db: {e:?}")))? @@ -203,7 +193,6 @@ impl HistorySvc for HistoryService { } #[instrument(skip_all, level = Level::INFO)] - #[expect(clippy::significant_drop_tightening, reason = "Would be a logic-bug")] async fn tail_history( &self, _request: Request<TailHistoryRequest>, diff --git a/crates/daemon/src/daemon.rs b/crates/daemon/src/daemon.rs index f3eead19..1c3afcde 100644 --- a/crates/daemon/src/daemon.rs +++ b/crates/daemon/src/daemon.rs @@ -110,16 +110,6 @@ impl DaemonHandle { self.state.settings.read().await } - /// Apply already-loaded settings and emit a [`SettingsReloaded`] event. - /// - /// Use this when settings have already been loaded (e.g., from a file watcher) - /// to avoid parsing the config file twice. - pub(crate) async fn apply_settings(&self, settings: Settings) { - *self.state.settings.write().await = settings; - self.emit(DaemonEvent::SettingsReloaded); - tracing::info!("settings applied"); - } - /// Get the encryption key. pub(crate) fn encryption_key(&self) -> &[u8; 32] { &self.state.encryption_key @@ -180,9 +170,7 @@ impl Daemon { } /// Run the daemon event loop. - /// - /// This processes events until a [`ShutdownRequested`] event is received. - pub(crate) async fn run_event_loop(&mut self) -> Result<()> { + pub(crate) async fn wait_for_shutdown(&mut self) -> Result<()> { let mut event_rx = self.handle.subscribe(); loop { match event_rx.recv().await { @@ -191,8 +179,7 @@ impl Daemon { break; } Ok(event) => { - tracing::debug!(?event, "processing event"); - self.dispatch_event(&event).await; + tracing::debug!(?event, "event received"); } Err(broadcast::error::RecvError::Lagged(n)) => { tracing::warn!( @@ -208,10 +195,6 @@ impl Daemon { } Ok(()) } - - async fn dispatch_event(&mut self, event: &DaemonEvent) { - todo!() - } } // ============================================================================ diff --git a/crates/daemon/src/events.rs b/crates/daemon/src/events.rs index 1864d224..3b6fa5d8 100644 --- a/crates/daemon/src/events.rs +++ b/crates/daemon/src/events.rs @@ -7,8 +7,7 @@ //! External processes (like CLI commands) can also inject events via the //! Control gRPC service. -use turtle::history::{History, HistoryId}; -use turtle_common::record::RecordId; +use turtle::history::History; /// Events that flow through the daemon's event bus. /// @@ -23,12 +22,6 @@ pub(crate) enum DaemonEvent { /// A command has finished running. HistoryEnded(History), - // ---- Sync ---- - /// Records were synced from the server. - /// - /// The search component uses this to update its index with new history. - RecordsAdded(Vec<RecordId>), - /// Sync completed successfully. SyncCompleted { /// Number of records uploaded. @@ -49,28 +42,6 @@ pub(crate) enum DaemonEvent { /// Request an immediate sync (external trigger). ForceSync, - // ---- External commands ---- - /// History was pruned - search index needs a full rebuild. - /// - /// Emitted when the user runs `atuin history prune` or similar. - HistoryPruned, - - /// History was rebuilt - search index needs a full rebuild. - /// - /// Emitted when the user runs `atuin store rebuild history` or similar. - HistoryRebuilt, - - /// Specific history items were deleted. - /// - /// The search component should remove these from its index. - HistoryDeleted { - /// IDs of the deleted history entries. - ids: Vec<HistoryId>, - }, - - /// Settings have changed, components should reload if needed. - SettingsReloaded, - // ---- Lifecycle ---- /// Request graceful shutdown of the daemon. ShutdownRequested, diff --git a/crates/daemon/src/main.rs b/crates/daemon/src/main.rs index 59d4c7ff..1d94b5fa 100644 --- a/crates/daemon/src/main.rs +++ b/crates/daemon/src/main.rs @@ -4,14 +4,13 @@ use std::{ fs::{self, File, OpenOptions}, io::Write, path::{Path, PathBuf}, - time::{Duration, Instant}, }; use clap::Parser; use eyre::WrapErr; -use eyre::{Context, Result, bail, eyre}; +use eyre::{Result, bail}; use fs4::fs_std::FileExt; -use tokio::time::sleep; +use tracing_subscriber::util::SubscriberInitExt; use crate::{ aclient::{database::ClientSqlite, record::sqlite_store::SqliteStore, settings::Settings}, @@ -40,6 +39,10 @@ enum Cmd { #[tokio::main] async fn main() -> Result<()> { + if let Err(e) = tracing_subscriber::registry().try_init() { + eprintln!("failed to initialize logging: {e}"); + } + let settings = Settings::new().wrap_err("could not load client settings")?; let db_path = PathBuf::from(settings.db_path.as_str()); let record_store_path = PathBuf::from(settings.record_store_path.as_str()); @@ -62,7 +65,7 @@ async fn boot(settings: Settings, store: SqliteStore, history_db: ClientSqlite) let mut daemon = Daemon::builder(settings.clone()) .store(store) - .history_db(history_db.clone()) + .history_db(history_db) .build()?; let handle = { @@ -79,7 +82,7 @@ async fn boot(settings: Settings, store: SqliteStore, history_db: ClientSqlite) handle }; - let history_service = HistoryService::new(handle.clone(), history_db).await?; + let history_service = HistoryService::new(handle.clone()).await?; let control_service = ControlService::new(handle.clone()); server::run_grpc_server( @@ -89,7 +92,7 @@ async fn boot(settings: Settings, store: SqliteStore, history_db: ClientSqlite) handle, )?; - daemon.run_event_loop().await?; + daemon.wait_for_shutdown().await?; tracing::info!("daemon shut down complete"); Ok(()) @@ -154,33 +157,3 @@ fn open_lock_file(path: &Path) -> Result<File> { .open(path) .wrap_err_with(|| format!("could not open lock file {}", path.display())) } - -async fn wait_for_lock(path: &Path, timeout: Duration) -> Result<File> { - const LOCK_POLL: Duration = Duration::from_millis(20); - - let file = open_lock_file(path)?; - let start = Instant::now(); - - loop { - match file.try_lock_exclusive() { - Ok(true) => return Ok(file), - Ok(false) => { - if start.elapsed() >= timeout { - bail!("timed out waiting for lock at {}", path.display()); - } - - sleep(LOCK_POLL).await; - } - Err(err) => { - return Err(eyre!("could not lock {}: {err}", path.display())); - } - } - } -} - -async fn wait_for_pidfile_available(path: &Path, timeout: Duration) -> Result<()> { - let file = wait_for_lock(path, timeout).await?; - file.unlock() - .wrap_err_with(|| format!("failed to unlock {}", path.display()))?; - Ok(()) -} diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 0afd678b..7343c0fc 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -12,6 +12,7 @@ homepage = { workspace = true } repository = { workspace = true } [dependencies] +tracing-subscriber = { version = "0.3", features = ["ansi", "fmt", "registry", "env-filter", "json"] } axum = "0.8" config = { version = "0.15.8", default-features = false, features = ["toml"] } clap = { workspace = true } diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index fb587754..56c44d2d 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -7,6 +7,7 @@ use database::db::ServerPostgres; use eyre::{Context, Result, eyre}; use tokio::net::TcpListener; use tokio::signal; +use tracing_subscriber::util::SubscriberInitExt; use crate::database::DbType; use crate::settings::Settings; @@ -69,6 +70,10 @@ impl Cmd { #[tokio::main] async fn main() -> Result<()> { + if let Err(e) = tracing_subscriber::registry().try_init() { + eprintln!("failed to initialize logging: {e}"); + } + Cmd::parse().run().await } diff --git a/crates/turtle/Cargo.toml b/crates/turtle/Cargo.toml index 102c5e9a..547f8530 100644 --- a/crates/turtle/Cargo.toml +++ b/crates/turtle/Cargo.toml @@ -12,84 +12,18 @@ homepage = { workspace = true } repository = { workspace = true } [dependencies] -turtle-common = {workspace = true} -async-trait = "0.1.58" -axum = "0.8" -base64 = "0.22" -clap = { version = "4.5.7", features = ["derive"] } -clap_complete = "4.5.8" -clap_complete_nushell = "4.5.4" -colored = "2.0.4" -config = { version = "0.15.8", default-features = false, features = ["toml"] } -crossterm = {version = "0.29.0", features = ["use-dev-tty", "serde"] } -crypto_secretbox = "0.1.1" -dashmap = "6.1.0" -directories = "6.0.0" eyre = "0.6" -fs-err = "3.1" -fs4 = "0.13.1" -fuzzy-matcher = "0.3.7" hyper-util = "0.1" -indicatif = "0.18.0" -interim = { version = "0.2.0", features = ["time_0_3"] } -itertools = "0.14.0" -lasso = { version = "0.7", features = ["multi-threaded"] } -log = "0.4" -metrics = "0.24" -metrics-exporter-prometheus = { version = "0.18", default-features = false } -minspan = "0.1.5" -norm = { version = "0.1.1", features = ["fzf-v2"] } -notify = "7" prost = "0.14" -rand = { version = "0.8.5", features = ["std"] } -ratatui = "0.30.0" regex = "1.10.5" -reqwest = { version = "0.13", features = ["json", "rustls-no-provider", "stream"], default-features = false } -rmp = { version = "0.8.14" } -runtime-format = "0.1.3" -rustix = { version = "1.1.4", features = ["process", "fs"] } -rustls = { version = "0.23", default-features = false, features = [ "ring", "std", "tls12", ] } -rusty_paserk = { version = "0.5.0", default-features = false, features = [ "v4", "serde", ] } -rusty_paseto = { version = "0.8.0", default-features = false } -semver = "1.0.20" -serde = { version = "1.0.202", features = ["derive"] } -serde_json = "1.0.119" -serde_regex = "1.1.0" -serde_with = "3.8.1" -shellexpand = "3" -sql-builder = "3" sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "time", "postgres", "uuid", "sqlite", "regexp"] } -thiserror = "2" time = { version = "0.3.47", features = [ "serde-human-readable", "macros", "local-offset", "macros", "formatting", "parsing"] } tokio = { version = "1", features = ["full"] } -tokio-stream = { version = "0.1.14", features = ["net"] } -toml_edit = "0.25.4" tonic = "0.14" tonic-prost = "0.14" tower = "0.5" -tower-http = { version = "0.6", features = ["trace"] } -tracing = "0.1" -tracing-appender = "0.2" -tracing-subscriber = { version = "0.3", features = ["ansi", "fmt", "registry", "env-filter", "json"] } +turtle-common = {workspace = true} typed-builder = "0.18.2" -unicode-segmentation = "1.11.0" -unicode-width = "0.2" -url = "2.5.2" -uuid = { version = "1.9", features = ["v4", "v7", "serde"] } -vt100 = "0.16" -whoami = "2.1.0" - -[target.'cfg(target_os = "linux")'.dependencies] -arboard = { version = "3.4", default-features = false, features = [ "wayland-data-control", ] } -listenfd = "1.0.1" - -[target.'cfg(unix)'.dependencies] -daemonize = "0.5.0" -portable-pty = "0.9" -signal-hook = "0.3" - -[dev-dependencies] -tokio = { version = "1", features = ["full"] } [build-dependencies] protox = "0.9" diff --git a/crates/turtle/src/client/mod.rs b/crates/turtle/src/client/mod.rs index 07f01e6c..ec97c994 100644 --- a/crates/turtle/src/client/mod.rs +++ b/crates/turtle/src/client/mod.rs @@ -54,12 +54,12 @@ pub fn history_entry_to_history(entry: HistoryEntry) -> History { } #[must_use] -pub fn daemon_matches_expected(version: &str, protocol: u32) -> bool { +pub fn daemon_matches_expected(protocol: u32) -> bool { protocol == DAEMON_PROTOCOL_VERSION } #[must_use] -pub fn daemon_mismatch_message(version: &str, protocol: u32) -> String { +pub fn daemon_mismatch_message(protocol: u32) -> String { if protocol == DAEMON_PROTOCOL_VERSION { unreachable!() } else { @@ -110,10 +110,10 @@ pub async fn probe(path: String) -> Probe { match client.status().await { Ok(status) => { - if daemon_matches_expected(&status.version, status.protocol) { + if daemon_matches_expected(status.protocol) { Probe::Ready(client) } else { - Probe::NeedsRestart(daemon_mismatch_message(&status.version, status.protocol)) + Probe::NeedsRestart(daemon_mismatch_message(status.protocol)) } } Err(err) => Probe::Unreachable(err), @@ -129,6 +129,7 @@ pub struct HistoryClient { client: HistoryServiceClient<Channel>, } +#[derive(Clone, Copy, Debug)] pub struct Range { pub start: OffsetDateTime, pub end: OffsetDateTime, diff --git a/crates/turtle/src/history/builder.rs b/crates/turtle/src/history/builder.rs index 7eca0491..08d26f7f 100644 --- a/crates/turtle/src/history/builder.rs +++ b/crates/turtle/src/history/builder.rs @@ -68,8 +68,8 @@ impl From<HistoryDaemonCapture> for History { captured.cwd, -1, -1, - Some(captured.session), - Some(captured.hostname), + captured.session, + captured.hostname, captured.author, captured.intent, None, diff --git a/crates/turtle/src/history/mod.rs b/crates/turtle/src/history/mod.rs index 10e74d8e..27755bbe 100644 --- a/crates/turtle/src/history/mod.rs +++ b/crates/turtle/src/history/mod.rs @@ -30,22 +30,6 @@ impl From<String> for HistoryId { } } -pub(crate) fn get_hostname() -> String { - env::var("ATUIN_HOST_NAME") - .unwrap_or_else(|_| whoami::hostname().unwrap_or_else(|_| "unknown-host".to_string())) -} - -pub(crate) fn get_username() -> String { - env::var("ATUIN_HOST_USER") - .unwrap_or_else(|_| whoami::username().unwrap_or_else(|_| "unknown-user".to_string())) -} - -/// Returns a pair of the hostname and username, separated by a colon. -#[must_use] -pub fn get_host_user() -> String { - format!("{}:{}", get_hostname(), get_username()) -} - /// Client-side history entry. /// /// Client stores data unencrypted, and only encrypts it before sending to the server. @@ -123,16 +107,12 @@ impl History { cwd: String, exit: i64, duration: i64, - session: Option<String>, - hostname: Option<String>, + session: String, + hostname: String, author: Option<String>, intent: Option<String>, deleted_at: Option<OffsetDateTime>, ) -> Self { - let session = session - .or_else(|| env::var("ATUIN_SESSION").ok()) - .unwrap_or_else(|| uuid_v7().as_simple().to_string()); - let hostname = hostname.unwrap_or_else(get_host_user); let author = Self::normalize_optional_field(author) .or_else(|| Self::normalize_optional_field(env::var(HISTORY_AUTHOR_ENV).ok())) .unwrap_or_else(|| Self::author_from_hostname(hostname.as_str())); diff --git a/crates/turtle/src/lib.rs b/crates/turtle/src/lib.rs index c78b0475..fbee6761 100644 --- a/crates/turtle/src/lib.rs +++ b/crates/turtle/src/lib.rs @@ -1,5 +1,3 @@ -#![expect(unused_crate_dependencies)] - pub mod client; pub mod generated; pub mod history; |
