aboutsummaryrefslogtreecommitdiffstats
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/client/Cargo.toml9
-rw-r--r--crates/client/build.rs22
-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
-rw-r--r--crates/common/Cargo.toml1
-rw-r--r--crates/common/src/utils.rs18
-rw-r--r--crates/daemon/Cargo.toml1
-rw-r--r--crates/daemon/src/aclient/api_client.rs4
-rw-r--r--crates/daemon/src/aclient/database/mod.rs145
-rw-r--r--crates/daemon/src/aclient/encryption.rs8
-rw-r--r--crates/daemon/src/aclient/history/mod.rs16
-rw-r--r--crates/daemon/src/aclient/history/store.rs24
-rw-r--r--crates/daemon/src/aclient/meta.rs10
-rw-r--r--crates/daemon/src/aclient/mod.rs12
-rw-r--r--crates/daemon/src/aclient/record/encryption.rs6
-rw-r--r--crates/daemon/src/aclient/record/mod.rs3
-rw-r--r--crates/daemon/src/aclient/record/sqlite_store.rs24
-rw-r--r--crates/daemon/src/aclient/record/sync.rs17
-rw-r--r--crates/daemon/src/aclient/settings/meta.rs2
-rw-r--r--crates/daemon/src/aclient/settings/mod.rs277
-rw-r--r--crates/daemon/src/aclient/settings/watcher.rs260
-rw-r--r--crates/daemon/src/api/control.rs8
-rw-r--r--crates/daemon/src/api/history.rs23
-rw-r--r--crates/daemon/src/daemon.rs21
-rw-r--r--crates/daemon/src/events.rs31
-rw-r--r--crates/daemon/src/main.rs45
-rw-r--r--crates/server/Cargo.toml1
-rw-r--r--crates/server/src/main.rs5
-rw-r--r--crates/turtle/Cargo.toml68
-rw-r--r--crates/turtle/src/client/mod.rs9
-rw-r--r--crates/turtle/src/history/builder.rs4
-rw-r--r--crates/turtle/src/history/mod.rs24
-rw-r--r--crates/turtle/src/lib.rs2
47 files changed, 394 insertions, 1830 deletions
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;