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 /crates/client/src | |
| parent | chore: Commit (diff) | |
| download | atuin-2f671f196e245ac1a791c001286e401a2fab9d3a.zip | |
chore: Commit
Diffstat (limited to '')
| -rw-r--r-- | crates/client/src/atuin_client/mod.rs | 16 | ||||
| -rw-r--r-- | crates/client/src/atuin_client/settings/mod.rs | 393 | ||||
| -rw-r--r-- | crates/client/src/atuin_client/theme.rs | 41 | ||||
| -rw-r--r-- | crates/client/src/atuin_history/stats.rs | 2 | ||||
| -rw-r--r-- | crates/client/src/command/client.rs | 31 | ||||
| -rw-r--r-- | crates/client/src/command/client/daemon.rs | 2 | ||||
| -rw-r--r-- | crates/client/src/command/client/history/end.rs | 6 | ||||
| -rw-r--r-- | crates/client/src/command/client/history/mod.rs | 563 | ||||
| -rw-r--r-- | crates/client/src/command/client/history/start.rs | 21 | ||||
| -rw-r--r-- | crates/client/src/command/client/history/tail.rs | 10 | ||||
| -rw-r--r-- | crates/client/src/command/client/info.rs | 2 | ||||
| -rw-r--r-- | crates/client/src/command/client/stats.rs | 6 | ||||
| -rw-r--r-- | crates/client/src/command/client/sync.rs | 2 | ||||
| -rw-r--r-- | crates/client/src/command/client/wrapped.rs | 15 | ||||
| -rw-r--r-- | crates/client/src/command/mod.rs | 9 | ||||
| -rw-r--r-- | crates/client/src/main.rs | 5 |
16 files changed, 72 insertions, 1052 deletions
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() } |
