aboutsummaryrefslogtreecommitdiffstats
path: root/crates/client/src
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-20 23:00:06 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-20 23:00:06 +0200
commit2f671f196e245ac1a791c001286e401a2fab9d3a (patch)
treebe90d8f16d5c8b65c6b77ebe1359d22e9738bbf6 /crates/client/src
parentchore: Commit (diff)
downloadatuin-2f671f196e245ac1a791c001286e401a2fab9d3a.zip
chore: Commit
Diffstat (limited to 'crates/client/src')
-rw-r--r--crates/client/src/atuin_client/mod.rs16
-rw-r--r--crates/client/src/atuin_client/settings/mod.rs393
-rw-r--r--crates/client/src/atuin_client/theme.rs41
-rw-r--r--crates/client/src/atuin_history/stats.rs2
-rw-r--r--crates/client/src/command/client.rs31
-rw-r--r--crates/client/src/command/client/daemon.rs2
-rw-r--r--crates/client/src/command/client/history/end.rs6
-rw-r--r--crates/client/src/command/client/history/mod.rs563
-rw-r--r--crates/client/src/command/client/history/start.rs21
-rw-r--r--crates/client/src/command/client/history/tail.rs10
-rw-r--r--crates/client/src/command/client/info.rs2
-rw-r--r--crates/client/src/command/client/stats.rs6
-rw-r--r--crates/client/src/command/client/sync.rs2
-rw-r--r--crates/client/src/command/client/wrapped.rs15
-rw-r--r--crates/client/src/command/mod.rs9
-rw-r--r--crates/client/src/main.rs5
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()
}