From bbdf38018b47328b5faa2cef635c37095045be72 Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Fri, 12 Jun 2026 01:54:21 +0200 Subject: feat(server): Really make users stateless (with tests) This commit also remove another load of unneeded features. --- crates/turtle/src/command/client.rs | 56 ++- crates/turtle/src/command/client/daemon.rs | 6 +- crates/turtle/src/command/client/doctor.rs | 405 --------------------- crates/turtle/src/command/client/history.rs | 24 +- crates/turtle/src/command/client/import.rs | 186 ---------- crates/turtle/src/command/client/info.rs | 9 +- crates/turtle/src/command/client/init.rs | 4 +- crates/turtle/src/command/client/search.rs | 7 +- crates/turtle/src/command/client/search/engines.rs | 11 +- .../src/command/client/search/engines/daemon.rs | 11 +- .../turtle/src/command/client/search/engines/db.rs | 11 +- .../src/command/client/search/engines/skim.rs | 15 +- .../src/command/client/search/interactive.rs | 11 +- crates/turtle/src/command/client/server.rs | 1 - crates/turtle/src/command/client/stats.rs | 14 +- crates/turtle/src/command/client/store.rs | 6 +- crates/turtle/src/command/client/store/pull.rs | 9 +- crates/turtle/src/command/client/store/push.rs | 11 +- crates/turtle/src/command/client/store/rebuild.rs | 6 +- crates/turtle/src/command/client/store/rekey.rs | 12 +- crates/turtle/src/command/client/sync.rs | 40 +- crates/turtle/src/command/client/sync/status.rs | 34 +- crates/turtle/src/command/client/wrapped.rs | 5 +- crates/turtle/src/command/external.rs | 102 ------ crates/turtle/src/command/mod.rs | 6 - 25 files changed, 145 insertions(+), 857 deletions(-) delete mode 100644 crates/turtle/src/command/client/doctor.rs delete mode 100644 crates/turtle/src/command/client/import.rs delete mode 100644 crates/turtle/src/command/external.rs (limited to 'crates/turtle/src/command') diff --git a/crates/turtle/src/command/client.rs b/crates/turtle/src/command/client.rs index 9d5b4605..9ab28e15 100644 --- a/crates/turtle/src/command/client.rs +++ b/crates/turtle/src/command/client.rs @@ -5,7 +5,7 @@ use clap::Subcommand; use eyre::{Result, WrapErr}; use crate::atuin_client::{ - database::Sqlite, record::sqlite_store::SqliteStore, settings::Settings, theme, + database::ClientSqlite, record::sqlite_store::SqliteStore, settings::Settings, theme, }; use tracing_appender::rolling::{RollingFileAppender, Rotation}; use tracing_subscriber::{ @@ -48,9 +48,7 @@ mod daemon; mod config; mod default_config; -mod doctor; mod history; -mod import; mod info; mod init; mod search; @@ -66,18 +64,12 @@ pub(crate) enum Cmd { #[command(subcommand)] History(history::Cmd), - /// Import shell history from file - #[command(subcommand)] - Import(import::Cmd), - - /// Calculate statistics for your history - Stats(stats::Cmd), - /// Interactive history search Search(search::Cmd), #[cfg(feature = "sync")] - #[command(flatten)] + #[command(subcommand)] + /// Request a sync or view sync status Sync(sync::Cmd), /// Manage the atuin server @@ -96,11 +88,11 @@ pub(crate) enum Cmd { #[command()] Info, - /// Run the doctor to check for common issues - #[command()] - Doctor, + /// Calculate statistics for your history + Stats(stats::Cmd), #[command()] + /// Display a recap of your last year's history Wrapped { year: Option }, /// *Experimental* Manage the background daemon @@ -113,6 +105,7 @@ pub(crate) enum Cmd { DefaultConfig, #[command(subcommand)] + /// Manage your configuration Config(config::Cmd), } @@ -131,19 +124,27 @@ impl Cmd { let runtime = runtime.enable_all().build().unwrap(); - // For non-history commands, we want to initialize logging and the theme manager before - // doing anything else. History commands are performance-sensitive and run before and after - // every shell command, so we want to skip any unnecessary initialization for them. - let settings = Settings::new().wrap_err("could not load client settings")?; - let theme_manager = theme::ThemeManager::new(settings.theme.debug, None); - let res = runtime.block_on(self.run_inner(settings, theme_manager)); + // Start the server before descending into the client-specific setup code. + // We simply cannot setup settings or a theme on the server, because the client-specific + // stuff will error out. + let res = if let Self::Server(server) = self { + runtime.block_on(server.run()) + } else { + // For non-history commands, we want to initialize logging and the theme manager before + // doing anything else. History commands are performance-sensitive and run before and after + // every shell command, so we want to skip any unnecessary initialization for them. + let settings = Settings::new().wrap_err("could not load client settings")?; + let theme_manager = theme::ThemeManager::new(settings.theme.debug, None); + + runtime.block_on(self.run_inner(settings, theme_manager)) + }; runtime.shutdown_timeout(std::time::Duration::from_millis(50)); res } - #[expect(clippy::too_many_lines, clippy::future_not_send)] + #[expect(clippy::too_many_lines)] async fn run_inner( self, mut settings: Settings, @@ -306,7 +307,6 @@ impl Cmd { init.run(&settings); return Ok(()); } - Self::Doctor => return doctor::run(&settings).await, Self::Config(config) => return config.run(&settings).await, _ => {} } @@ -314,14 +314,13 @@ impl Cmd { let db_path = PathBuf::from(settings.db_path.as_str()); let record_store_path = PathBuf::from(settings.record_store_path.as_str()); - let db = Sqlite::new(db_path, settings.local_timeout).await?; + let db = ClientSqlite::new(db_path, settings.local_timeout).await?; let sqlite_store = SqliteStore::new(record_store_path, settings.local_timeout).await?; let theme_name = settings.theme.name.clone(); let theme = theme_manager.load_theme(theme_name.as_str(), settings.theme.max_depth); match self { - Self::Import(import) => import.run(&db).await, Self::Stats(stats) => stats.run(&db, &settings, theme).await, Self::Search(search) => search.run(db, &mut settings, sqlite_store, theme).await, @@ -330,12 +329,7 @@ impl Cmd { Self::Store(store) => store.run(&settings, &db, sqlite_store).await, - Self::Server(server) => server.run().await, - - Self::Info => { - info::run(&settings); - Ok(()) - } + Self::Info => info::run(&settings), Self::DefaultConfig => { default_config::run(); @@ -347,7 +341,7 @@ impl Cmd { #[cfg(feature = "daemon")] Self::Daemon(cmd) => cmd.run(settings, sqlite_store, db).await, - Self::History(_) | Self::Init(_) | Self::Doctor | Self::Config(_) => { + Self::History(_) | Self::Init(_) | Self::Config(_) | Self::Server(_) => { unreachable!() } } diff --git a/crates/turtle/src/command/client/daemon.rs b/crates/turtle/src/command/client/daemon.rs index 2fb090aa..cb5dd118 100644 --- a/crates/turtle/src/command/client/daemon.rs +++ b/crates/turtle/src/command/client/daemon.rs @@ -7,7 +7,7 @@ use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; use crate::atuin_client::{ - database::Sqlite, history::History, record::sqlite_store::SqliteStore, settings::Settings, + database::ClientSqlite, history::History, record::sqlite_store::SqliteStore, settings::Settings, }; use crate::atuin_daemon::DaemonEvent; use crate::atuin_daemon::client::{ @@ -86,7 +86,7 @@ impl Cmd { self, settings: Settings, store: SqliteStore, - history_db: Sqlite, + history_db: ClientSqlite, ) -> Result<()> { match self.subcmd { None => { @@ -634,7 +634,7 @@ pub(crate) fn daemonize_current_process() -> Result<()> { async fn run( settings: Settings, store: SqliteStore, - history_db: Sqlite, + history_db: ClientSqlite, force: bool, ) -> Result<()> { if force { diff --git a/crates/turtle/src/command/client/doctor.rs b/crates/turtle/src/command/client/doctor.rs deleted file mode 100644 index eec690a5..00000000 --- a/crates/turtle/src/command/client/doctor.rs +++ /dev/null @@ -1,405 +0,0 @@ -use std::process::Command; -use std::{env, str::FromStr}; - -use crate::atuin_client::database::Sqlite; -use crate::atuin_client::settings::Settings; -use crate::atuin_common::shell::{Shell, shell_name}; -use crate::atuin_common::utils; -use colored::Colorize; -use eyre::Result; -use serde::Serialize; - -use sysinfo::{Disks, System, get_current_pid}; - -#[derive(Debug, Serialize)] -struct ShellInfo { - pub(crate) name: String, - - // best-effort, not supported on all OSes - pub(crate) default: String, - - // Detect some shell plugins that the user has installed. - // I'm just going to start with preexec/blesh - pub(crate) plugins: Vec, - - // The preexec framework used in the current session, if Atuin is loaded. - pub(crate) preexec: Option, -} - -impl ShellInfo { - // HACK ALERT! - // Many of the shell vars we need to detect are not exported :( - // So, we're going to run a interactive session and directly check the - // variable. There's a chance this won't work, so it should not be fatal. - // - // Every shell we support handles `shell -ic 'command'` - fn shellvar_exists(shell: &str, var: &str) -> bool { - let cmd = Command::new(shell) - .args([ - "-ic", - format!("[ -z ${var} ] || echo ATUIN_DOCTOR_ENV_FOUND").as_str(), - ]) - .output() - .map_or(String::new(), |v| { - let out = v.stdout; - String::from_utf8(out).unwrap_or_default() - }); - - cmd.contains("ATUIN_DOCTOR_ENV_FOUND") - } - - fn detect_preexec_framework(shell: &str) -> Option { - if env::var("ATUIN_SESSION").ok().is_none() { - None - } else if shell.starts_with("bash") || shell == "sh" { - env::var("ATUIN_PREEXEC_BACKEND") - .ok() - .filter(|value| !value.is_empty()) - .and_then(|atuin_preexec_backend| { - atuin_preexec_backend.rfind(':').and_then(|pos_colon| { - u32::from_str(&atuin_preexec_backend[..pos_colon]) - .ok() - .is_some_and(|preexec_shlvl| { - env::var("SHLVL") - .ok() - .and_then(|shlvl| u32::from_str(&shlvl).ok()) - .is_some_and(|shlvl| shlvl == preexec_shlvl) - }) - .then(|| atuin_preexec_backend[pos_colon + 1..].to_string()) - }) - }) - } else { - Some("built-in".to_string()) - } - } - - fn validate_plugin_blesh( - _shell: &str, - shell_process: &sysinfo::Process, - ble_session_id: &str, - ) -> Option { - ble_session_id - .split('/') - .nth(1) - .and_then(|field| u32::from_str(field).ok()) - .filter(|&blesh_pid| blesh_pid == shell_process.pid().as_u32()) - .map(|_| "blesh".to_string()) - } - - pub(crate) fn plugins(shell: &str, shell_process: &sysinfo::Process) -> Vec { - // consider a different detection approach if there are plugins - // that don't set shell vars - - enum PluginShellType { - Any, - Bash, - - // Note: these are currently unused - #[expect(dead_code)] - Zsh, - #[expect(dead_code)] - Fish, - #[expect(dead_code)] - Nushell, - #[expect(dead_code)] - Xonsh, - } - - enum PluginProbeType { - EnvironmentVariable(&'static str), - InteractiveShellVariable(&'static str), - } - - type PluginValidator = fn(&str, &sysinfo::Process, &str) -> Option; - - let plugin_list: [( - &str, - PluginShellType, - PluginProbeType, - Option, - ); 3] = [ - ( - "atuin", - PluginShellType::Any, - PluginProbeType::EnvironmentVariable("ATUIN_SESSION"), - None, - ), - ( - "blesh", - PluginShellType::Bash, - PluginProbeType::EnvironmentVariable("BLE_SESSION_ID"), - Some(Self::validate_plugin_blesh), - ), - ( - "bash-preexec", - PluginShellType::Bash, - PluginProbeType::InteractiveShellVariable("bash_preexec_imported"), - None, - ), - ]; - - plugin_list - .into_iter() - .filter(|(_, shell_type, _, _)| match shell_type { - PluginShellType::Any => true, - PluginShellType::Bash => shell.starts_with("bash") || shell == "sh", - PluginShellType::Zsh => shell.starts_with("zsh"), - PluginShellType::Fish => shell.starts_with("fish"), - PluginShellType::Nushell => shell.starts_with("nu"), - PluginShellType::Xonsh => shell.starts_with("xonsh"), - }) - .filter_map(|(plugin, _, probe_type, validator)| -> Option { - match probe_type { - PluginProbeType::EnvironmentVariable(env) => { - env::var(env).ok().filter(|value| !value.is_empty()) - } - PluginProbeType::InteractiveShellVariable(shellvar) => { - ShellInfo::shellvar_exists(shell, shellvar).then_some(String::default()) - } - } - .and_then(|value| { - validator.map_or_else( - || Some(plugin.to_string()), - |validator| validator(shell, shell_process, &value), - ) - }) - }) - .collect() - } - - pub(crate) fn new() -> Self { - // TODO: rework to use crate::atuin_common::Shell - - let sys = System::new_all(); - - let process = sys - .process(get_current_pid().expect("Failed to get current PID")) - .expect("Process with current pid does not exist"); - - let parent = sys - .process(process.parent().expect("Atuin running with no parent!")) - .expect("Process with parent pid does not exist"); - - let name = shell_name(Some(parent)); - - let plugins = ShellInfo::plugins(name.as_str(), parent); - - let default = Shell::default_shell().unwrap_or(Shell::Unknown).to_string(); - - let preexec = Self::detect_preexec_framework(name.as_str()); - - Self { - name, - default, - plugins, - preexec, - } - } -} - -#[derive(Debug, Serialize)] -struct DiskInfo { - pub(crate) name: String, - pub(crate) filesystem: String, -} - -#[derive(Debug, Serialize)] -struct SystemInfo { - pub(crate) os: String, - - pub(crate) arch: String, - - pub(crate) version: String, - pub(crate) disks: Vec, -} - -impl SystemInfo { - pub(crate) fn new() -> Self { - let disks = Disks::new_with_refreshed_list(); - let disks = disks - .list() - .iter() - .map(|d| DiskInfo { - name: d.name().to_os_string().into_string().unwrap(), - filesystem: d.file_system().to_os_string().into_string().unwrap(), - }) - .collect(); - - Self { - os: System::name().unwrap_or_else(|| "unknown".to_string()), - arch: System::cpu_arch().unwrap_or_else(|| "unknown".to_string()), - version: System::os_version().unwrap_or_else(|| "unknown".to_string()), - disks, - } - } -} - -#[derive(Debug, Serialize)] -struct SyncInfo { - pub(crate) auth_state: String, - pub(crate) auto_sync: bool, - - pub(crate) last_sync: String, -} - -impl SyncInfo { - pub(crate) async fn new(settings: &Settings) -> Result { - let has_cli_token = settings.have_sync_key().await?; - - let auth_state = if has_cli_token { - "Self-hosted (authenticated)".into() - } else { - "Not authenticated".into() - }; - - Ok(Self { - auth_state, - auto_sync: settings.auto_sync, - last_sync: Settings::last_sync() - .await - .map_or_else(|_| "no last sync".to_string(), |v| v.to_string()), - }) - } -} - -#[derive(Debug)] -struct SettingPaths { - db: String, - record_store: String, - key: String, -} - -impl SettingPaths { - pub(crate) fn new(settings: &Settings) -> Self { - Self { - db: settings.db_path.clone(), - record_store: settings.record_store_path.clone(), - key: settings.key_path.clone(), - } - } - - pub(crate) fn verify(&self) { - let paths = vec![ - ("ATUIN_DB_PATH", &self.db), - ("ATUIN_RECORD_STORE", &self.record_store), - ("ATUIN_KEY", &self.key), - ]; - - for (path_env_var, path) in paths { - if utils::broken_symlink(path) { - eprintln!( - "{path} (${path_env_var}) is a broken symlink. This may cause issues with Atuin." - ); - } - } - } -} - -#[derive(Debug, Serialize)] -struct AtuinInfo { - pub(crate) version: String, - pub(crate) commit: String, - - /// Whether the main Atuin sync server is in use - /// I'm just calling it Atuin Cloud for lack of a better name atm - pub(crate) sync: Option, - - pub(crate) sqlite_version: String, - - #[serde(skip)] // probably unnecessary to expose this - pub(crate) setting_paths: SettingPaths, -} - -impl AtuinInfo { - pub(crate) async fn new(settings: &Settings) -> Result { - let logged_in = settings.have_sync_key().await?; - - let sync = if logged_in { - Some(SyncInfo::new(settings).await?) - } else { - None - }; - - let sqlite_version = match Sqlite::new("sqlite::memory:", 0.1).await { - Ok(db) => db - .sqlite_version() - .await - .unwrap_or_else(|_| "unknown".to_string()), - Err(_) => "error".to_string(), - }; - - Ok(Self { - version: crate::VERSION.to_string(), - commit: crate::SHA.to_string(), - sync, - sqlite_version, - setting_paths: SettingPaths::new(settings), - }) - } -} - -#[derive(Debug, Serialize)] -struct DoctorDump { - pub(crate) atuin: AtuinInfo, - pub(crate) shell: ShellInfo, - pub(crate) system: SystemInfo, -} - -impl DoctorDump { - pub(crate) async fn new(settings: &Settings) -> Result { - Ok(Self { - atuin: AtuinInfo::new(settings).await?, - shell: ShellInfo::new(), - system: SystemInfo::new(), - }) - } -} - -fn checks(info: &DoctorDump) { - println!(); // spacing - // - let zfs_error = "[Filesystem] ZFS is known to have some issues with SQLite. Atuin uses SQLite heavily. If you are having poor performance, there are some workarounds here: https://github.com/atuinsh/atuin/issues/952".bold().red(); - let bash_plugin_error = "[Shell] If you are using Bash, Atuin requires that either bash-preexec or ble.sh (>= 0.4) be installed. An older ble.sh may not be detected. so ignore this if you have ble.sh >= 0.4 set up! Read more here: https://docs.atuin.sh/guide/installation/#bash".bold().red(); - let blesh_integration_error = "[Shell] Atuin and ble.sh seem to be loaded in the session, but the integration does not seem to be working. Please check the setup in .bashrc.".bold().red(); - - // ZFS: https://github.com/atuinsh/atuin/issues/952 - if info.system.disks.iter().any(|d| d.filesystem == "zfs") { - println!("{zfs_error}"); - } - - info.atuin.setting_paths.verify(); - - // Shell - if info.shell.name == "bash" { - if !info - .shell - .plugins - .iter() - .any(|p| p == "blesh" || p == "bash-preexec") - { - println!("{bash_plugin_error}"); - } - - if info.shell.plugins.iter().any(|plugin| plugin == "atuin") - && info.shell.plugins.iter().any(|plugin| plugin == "blesh") - && info.shell.preexec.as_ref().is_some_and(|val| val == "none") - { - println!("{blesh_integration_error}"); - } - } -} - -pub(crate) async fn run(settings: &Settings) -> Result<()> { - println!("{}", "Atuin Doctor".bold()); - println!("Checking for diagnostics"); - let dump = DoctorDump::new(settings).await?; - - checks(&dump); - - let dump = serde_json::to_string_pretty(&dump)?; - - println!("\nPlease include the output below with any bug reports or issues\n"); - println!("{dump}"); - - Ok(()) -} diff --git a/crates/turtle/src/command/client/history.rs b/crates/turtle/src/command/client/history.rs index e533759b..693098c0 100644 --- a/crates/turtle/src/command/client/history.rs +++ b/crates/turtle/src/command/client/history.rs @@ -21,7 +21,7 @@ use serde::Serialize; use crate::atuin_daemon::history::{HistoryEventKind, TailHistoryReply}; use crate::atuin_client::{ - database::{Database, Sqlite, current_context}, + database::{ClientSqlite, current_context}, encryption, history::{History, store::HistoryStore}, record::sqlite_store::SqliteStore, @@ -411,7 +411,7 @@ fn normalize_command_for_storage<'a>(command: &'a str, settings: &Settings) -> & } async fn handle_start( - db: &impl Database, + db: &ClientSqlite, settings: &Settings, command: &str, author: Option<&str>, @@ -484,7 +484,7 @@ async fn handle_daemon_start( #[expect(unused_variables)] async fn handle_end( - db: &impl Database, + db: &ClientSqlite, store: SqliteStore, history_store: HistoryStore, settings: &Settings, @@ -527,7 +527,7 @@ async fn handle_end( db.update(&h).await?; history_store.push(h).await?; - if settings.should_sync().await? { + if settings.sync.should_sync().await? { let (_, downloaded) = record::sync::sync(settings, &store, &history_store.encryption_key).await?; Settings::save_sync_time().await?; @@ -564,7 +564,7 @@ pub(super) async fn start_history_entry( } let db_path = PathBuf::from(settings.db_path.as_str()); - let db = Sqlite::new(db_path, settings.local_timeout).await?; + let db = ClientSqlite::new(db_path, settings.local_timeout).await?; handle_start(&db, settings, command, author, intent).await } @@ -582,7 +582,7 @@ pub(super) async fn end_history_entry( let db_path = PathBuf::from(settings.db_path.as_str()); let record_store_path = PathBuf::from(settings.record_store_path.as_str()); - let db = Sqlite::new(db_path, settings.local_timeout).await?; + 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) @@ -922,7 +922,7 @@ impl Cmd { #[expect(clippy::too_many_arguments)] #[expect(clippy::fn_params_excessive_bools)] async fn handle_list( - db: &impl Database, + db: &ClientSqlite, settings: &Settings, context: crate::atuin_client::database::Context, session: bool, @@ -964,7 +964,7 @@ impl Cmd { } async fn handle_prune( - db: &impl Database, + db: &ClientSqlite, settings: &Settings, store: SqliteStore, context: crate::atuin_client::database::Context, @@ -1017,7 +1017,7 @@ impl Cmd { } async fn handle_dedup( - db: &impl Database, + db: &ClientSqlite, settings: &Settings, store: SqliteStore, before: i64, @@ -1119,7 +1119,7 @@ impl Cmd { let db_path = PathBuf::from(settings.db_path.as_str()); let record_store_path = PathBuf::from(settings.record_store_path.as_str()); - let db = Sqlite::new(db_path, settings.local_timeout).await?; + 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) @@ -1233,7 +1233,7 @@ mod tests { #[tokio::test] async fn handle_start_saves_trimmed_command() { - let db = Sqlite::new("sqlite::memory:", 2.0).await.unwrap(); + let db = ClientSqlite::new("sqlite::memory:", 2.0).await.unwrap(); let settings = Settings::utc(); handle_start(&db, &settings, "ls \t", None, None) @@ -1251,7 +1251,7 @@ mod tests { #[tokio::test] async fn handle_start_can_keep_trailing_whitespace() { - let db = Sqlite::new("sqlite::memory:", 2.0).await.unwrap(); + let db = ClientSqlite::new("sqlite::memory:", 2.0).await.unwrap(); let settings = Settings { strip_trailing_whitespace: false, ..Settings::utc() diff --git a/crates/turtle/src/command/client/import.rs b/crates/turtle/src/command/client/import.rs deleted file mode 100644 index 3ec524d2..00000000 --- a/crates/turtle/src/command/client/import.rs +++ /dev/null @@ -1,186 +0,0 @@ -use std::env; - -use async_trait::async_trait; -use clap::Parser; -use eyre::Result; -use indicatif::ProgressBar; - -use crate::atuin_client::{ - database::Database, - history::History, - import::{ - Importer, Loader, bash::Bash, fish::Fish, nu::Nu, nu_histdb::NuHistDb, - powershell::PowerShell, replxx::Replxx, resh::Resh, xonsh::Xonsh, - xonsh_sqlite::XonshSqlite, zsh::Zsh, zsh_histdb::ZshHistDb, - }, -}; - -#[derive(Parser, Debug)] -#[command(infer_subcommands = true)] -pub(crate) enum Cmd { - /// Import history for the current shell - Auto, - - /// Import history from the zsh history file - Zsh, - /// Import history from the zsh history file - ZshHistDb, - /// Import history from the bash history file - Bash, - /// Import history from the replxx history file - Replxx, - /// Import history from the resh history file - Resh, - /// Import history from the fish history file - Fish, - /// Import history from the nu history file - Nu, - /// Import history from the nu history file - NuHistDb, - /// Import history from xonsh json files - Xonsh, - /// Import history from xonsh sqlite db - XonshSqlite, - /// Import history from the powershell history file - Powershell, -} - -const BATCH_SIZE: usize = 100; - -impl Cmd { - #[expect(clippy::cognitive_complexity)] - pub(crate) async fn run(&self, db: &DB) -> Result<()> { - println!(" Atuin "); - println!("======================"); - println!(" \u{1f30d} "); - println!(" \u{1f418}\u{1f418}\u{1f418}\u{1f418} "); - println!(" \u{1f422} "); - println!("======================"); - println!("Importing history..."); - - match self { - Self::Auto => { - if cfg!(windows) { - return if env::var("PSModulePath").is_ok() { - println!("Detected PowerShell"); - import::(db).await - } else { - println!("Could not detect the current shell."); - println!("Please run atuin import ."); - println!("To view a list of shells, run atuin import."); - Ok(()) - }; - } - - // $XONSH_HISTORY_BACKEND isn't always set, but $XONSH_HISTORY_FILE is - let xonsh_histfile = - env::var("XONSH_HISTORY_FILE").unwrap_or_else(|_| String::new()); - let shell = env::var("SHELL").unwrap_or_else(|_| String::from("NO_SHELL")); - - if xonsh_histfile.to_lowercase().ends_with(".json") { - println!("Detected Xonsh"); - import::(db).await - } else if xonsh_histfile.to_lowercase().ends_with(".sqlite") { - println!("Detected Xonsh (SQLite backend)"); - import::(db).await - } else if shell.ends_with("/zsh") { - if ZshHistDb::histpath().is_ok() { - println!( - "Detected Zsh-HistDb, using :{}", - ZshHistDb::histpath().unwrap().to_str().unwrap() - ); - import::(db).await - } else { - println!("Detected ZSH"); - import::(db).await - } - } else if shell.ends_with("/fish") { - println!("Detected Fish"); - import::(db).await - } else if shell.ends_with("/bash") { - println!("Detected Bash"); - import::(db).await - } else if shell.ends_with("/nu") { - if NuHistDb::histpath().is_ok() { - println!( - "Detected Nu-HistDb, using :{}", - NuHistDb::histpath().unwrap().to_str().unwrap() - ); - import::(db).await - } else { - println!("Detected Nushell"); - import::(db).await - } - } else if shell.ends_with("/pwsh") { - println!("Detected PowerShell"); - import::(db).await - } else { - println!("cannot import {shell} history"); - Ok(()) - } - } - - Self::Zsh => import::(db).await, - Self::ZshHistDb => import::(db).await, - Self::Bash => import::(db).await, - Self::Replxx => import::(db).await, - Self::Resh => import::(db).await, - Self::Fish => import::(db).await, - Self::Nu => import::(db).await, - Self::NuHistDb => import::(db).await, - Self::Xonsh => import::(db).await, - Self::XonshSqlite => import::(db).await, - Self::Powershell => import::(db).await, - } - } -} - -pub(crate) struct HistoryImporter<'db, DB: Database> { - pb: ProgressBar, - buf: Vec, - db: &'db DB, -} - -impl<'db, DB: Database> HistoryImporter<'db, DB> { - fn new(db: &'db DB, len: usize) -> Self { - Self { - pb: ProgressBar::new(len as u64), - buf: Vec::with_capacity(BATCH_SIZE), - db, - } - } - - async fn flush(self) -> Result<()> { - if !self.buf.is_empty() { - self.db.save_bulk(&self.buf).await?; - } - self.pb.finish(); - Ok(()) - } -} - -#[async_trait] -impl Loader for HistoryImporter<'_, DB> { - async fn push(&mut self, hist: History) -> Result<()> { - self.pb.inc(1); - self.buf.push(hist); - if self.buf.len() == self.buf.capacity() { - self.db.save_bulk(&self.buf).await?; - self.buf.clear(); - } - Ok(()) - } -} - -async fn import(db: &DB) -> Result<()> { - println!("Importing history from {}", I::NAME); - - let mut importer = I::new().await?; - let len = importer.entries().await.unwrap(); - let mut loader = HistoryImporter::new(db, len); - importer.load(&mut loader).await?; - loader.flush().await?; - - println!("Import complete!"); - Ok(()) -} diff --git a/crates/turtle/src/command/client/info.rs b/crates/turtle/src/command/client/info.rs index fc944987..49c92193 100644 --- a/crates/turtle/src/command/client/info.rs +++ b/crates/turtle/src/command/client/info.rs @@ -1,8 +1,9 @@ use crate::atuin_client::settings::Settings; - use crate::{SHA, VERSION}; -pub(crate) fn run(settings: &Settings) { +use eyre::Result; + +pub(crate) fn run(settings: &Settings) -> Result<()> { let config = crate::atuin_common::utils::config_dir(); let mut config_file = config.clone(); config_file.push("config.toml"); @@ -14,7 +15,7 @@ pub(crate) fn run(settings: &Settings) { config_file.to_string_lossy(), sever_config.to_string_lossy(), settings.db_path, - settings.key_path, + settings.sync.encryption_key()?, settings.meta.db_path ); @@ -28,4 +29,6 @@ pub(crate) fn run(settings: &Settings) { let print_out = format!("{config_paths}\n\n{env_vars}\n\n{general_info}"); println!("{print_out}"); + + Ok(()) } diff --git a/crates/turtle/src/command/client/init.rs b/crates/turtle/src/command/client/init.rs index 0643cb73..0cdcd425 100644 --- a/crates/turtle/src/command/client/init.rs +++ b/crates/turtle/src/command/client/init.rs @@ -89,7 +89,7 @@ $env.config = ( } } - fn static_init(&self, settings: &Settings) { + fn static_init(&self) { match self.shell { Shell::Zsh => { zsh::init_static(self.disable_up_arrow, self.disable_ctrl_r); @@ -119,6 +119,6 @@ $env.config = ( ); } - self.static_init(settings); + self.static_init(); } } diff --git a/crates/turtle/src/command/client/search.rs b/crates/turtle/src/command/client/search.rs index 72112084..962e6b1e 100644 --- a/crates/turtle/src/command/client/search.rs +++ b/crates/turtle/src/command/client/search.rs @@ -1,12 +1,12 @@ use std::fs::File; use std::io::{IsTerminal as _, Write, stderr, stdout}; +use crate::atuin_client::database::ClientSqlite; use crate::atuin_common::utils::{self, Escapable as _}; use clap::Parser; use eyre::Result; use crate::atuin_client::{ - database::Database, database::{OptFilters, current_context}, encryption, history::{History, store::HistoryStore}, @@ -157,7 +157,7 @@ impl Cmd { #[expect(clippy::too_many_lines)] pub(crate) async fn run( self, - db: impl Database, + db: ClientSqlite, settings: &mut Settings, store: SqliteStore, theme: &Theme, @@ -253,7 +253,6 @@ impl Cmd { offset: self.offset, reverse: self.reverse, include_duplicates: self.include_duplicates, - authors: self.author.clone().unwrap_or_default(), }; let mut entries = @@ -310,7 +309,7 @@ async fn run_non_interactive( settings: &Settings, filter_options: OptFilters, query: &[String], - db: &impl Database, + db: &ClientSqlite, ) -> Result> { let dir = if filter_options.cwd.as_deref() == Some(".") { Some(utils::get_current_dir()) diff --git a/crates/turtle/src/command/client/search/engines.rs b/crates/turtle/src/command/client/search/engines.rs index d6335a38..a84c4798 100644 --- a/crates/turtle/src/command/client/search/engines.rs +++ b/crates/turtle/src/command/client/search/engines.rs @@ -1,9 +1,9 @@ -use async_trait::async_trait; use crate::atuin_client::{ - database::{Context, Database, OptFilters}, - history::{AUTHOR_FILTER_ALL_USER, History, HistoryId}, + database::{ClientSqlite, Context, OptFilters}, + history::{History, HistoryId}, settings::{FilterMode, SearchMode, Settings}, }; +use async_trait::async_trait; use eyre::Result; use super::cursor::Cursor; @@ -67,10 +67,10 @@ pub(crate) trait SearchEngine: Send + Sync + 'static { async fn full_query( &mut self, state: &SearchState, - db: &mut dyn Database, + db: &mut ClientSqlite, ) -> Result>; - async fn query(&mut self, state: &SearchState, db: &mut dyn Database) -> Result> { + async fn query(&mut self, state: &SearchState, db: &mut ClientSqlite) -> Result> { if state.input.as_str().is_empty() { Ok(db .search( @@ -80,7 +80,6 @@ pub(crate) trait SearchEngine: Send + Sync + 'static { "", OptFilters { limit: Some(200), - authors: vec![AUTHOR_FILTER_ALL_USER.to_string()], ..Default::default() }, ) diff --git a/crates/turtle/src/command/client/search/engines/daemon.rs b/crates/turtle/src/command/client/search/engines/daemon.rs index df5ab9f8..55b3c6f2 100644 --- a/crates/turtle/src/command/client/search/engines/daemon.rs +++ b/crates/turtle/src/command/client/search/engines/daemon.rs @@ -1,6 +1,6 @@ use crate::atuin_client::{ - database::{Database, OptFilters}, - history::{AUTHOR_FILTER_ALL_USER, History}, + database::{ClientSqlite, OptFilters}, + history::History, settings::{SearchMode, Settings}, }; use crate::atuin_daemon::client::{DaemonClientErrorKind, SearchClient, classify_error}; @@ -75,7 +75,7 @@ impl Search { async fn fallback_to_db_search( &self, state: &SearchState, - db: &dyn Database, + db: &ClientSqlite, ) -> Result> { let results = db .search( @@ -85,7 +85,6 @@ impl Search { state.input.as_str(), OptFilters { limit: Some(200), - authors: vec![AUTHOR_FILTER_ALL_USER.to_string()], ..Default::default() }, ) @@ -95,7 +94,7 @@ impl Search { } #[instrument(skip_all, level = Level::TRACE, name = "hydrate_from_db", fields(count = ids.len()))] - async fn hydrate_from_db(&self, db: &dyn Database, ids: &[String]) -> Result> { + async fn hydrate_from_db(&self, db: &ClientSqlite, ids: &[String]) -> Result> { let placeholders: Vec = ids.iter().map(|id| format!("'{id}'")).collect(); let sql_query = format!( "SELECT * FROM history WHERE id IN ({}) ORDER BY timestamp DESC", @@ -111,7 +110,7 @@ impl SearchEngine for Search { async fn full_query( &mut self, state: &SearchState, - db: &mut dyn Database, + db: &mut ClientSqlite, ) -> Result> { let query = state.input.as_str().to_string(); diff --git a/crates/turtle/src/command/client/search/engines/db.rs b/crates/turtle/src/command/client/search/engines/db.rs index 86917a02..e6657b17 100644 --- a/crates/turtle/src/command/client/search/engines/db.rs +++ b/crates/turtle/src/command/client/search/engines/db.rs @@ -1,12 +1,10 @@ use super::{SearchEngine, SearchState}; -use async_trait::async_trait; use crate::atuin_client::{ - database::Database, - database::OptFilters, - database::{QueryToken, QueryTokenizer}, - history::{AUTHOR_FILTER_ALL_USER, History}, + database::{ClientSqlite, OptFilters, QueryToken, QueryTokenizer}, + history::History, settings::SearchMode, }; +use async_trait::async_trait; use eyre::Result; use norm::Metric; use norm::fzf::{FzfParser, FzfV2}; @@ -21,7 +19,7 @@ impl SearchEngine for Search { async fn full_query( &mut self, state: &SearchState, - db: &mut dyn Database, + db: &mut ClientSqlite, ) -> Result> { let results = db .search( @@ -31,7 +29,6 @@ impl SearchEngine for Search { state.input.as_str(), OptFilters { limit: Some(200), - authors: vec![AUTHOR_FILTER_ALL_USER.to_string()], ..Default::default() }, ) diff --git a/crates/turtle/src/command/client/search/engines/skim.rs b/crates/turtle/src/command/client/search/engines/skim.rs index fe2bdea3..a6a77573 100644 --- a/crates/turtle/src/command/client/search/engines/skim.rs +++ b/crates/turtle/src/command/client/search/engines/skim.rs @@ -1,18 +1,13 @@ use std::path::Path; +use crate::atuin_client::{database::ClientSqlite, history::History, settings::FilterMode}; use async_trait::async_trait; -use crate::atuin_client::{ - database::Database, - history::{History, is_known_agent}, - settings::FilterMode, -}; use eyre::Result; use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2}; use itertools::Itertools; use time::OffsetDateTime; use tokio::task::yield_now; use tracing::{Level, instrument, warn}; -use uuid; use super::{SearchEngine, SearchState}; @@ -36,7 +31,7 @@ impl SearchEngine for Search { async fn full_query( &mut self, state: &SearchState, - db: &mut dyn Database, + db: &mut ClientSqlite, ) -> Result> { if self.all_history.is_empty() { self.all_history = load_all_history(db).await; @@ -56,7 +51,7 @@ impl SearchEngine for Search { } #[instrument(skip_all, level = Level::TRACE, name = "load_all_history")] -async fn load_all_history(db: &dyn Database) -> Vec<(History, i32)> { +async fn load_all_history(db: &ClientSqlite) -> Vec<(History, i32)> { db.all_with_count().await.unwrap() } @@ -76,9 +71,7 @@ async fn fuzzy_search( if i % 256 == 0 { yield_now().await; } - if is_known_agent(&history.author) { - continue; - } + let context = &state.context; let git_root = context .git_root diff --git a/crates/turtle/src/command/client/search/interactive.rs b/crates/turtle/src/command/client/search/interactive.rs index 380fc33b..1d067e50 100644 --- a/crates/turtle/src/command/client/search/interactive.rs +++ b/crates/turtle/src/command/client/search/interactive.rs @@ -6,7 +6,10 @@ use std::{ #[cfg(unix)] use std::io::Read as _; -use crate::atuin_common::{shell::Shell, utils::Escapable as _}; +use crate::{ + atuin_client::database::ClientSqlite, + atuin_common::{shell::Shell, utils::Escapable as _}, +}; use eyre::Result; use time::OffsetDateTime; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; @@ -17,7 +20,7 @@ use super::{ history_list::{HistoryList, ListState}, }; use crate::atuin_client::{ - database::{Context, Database, current_context}, + database::{Context, current_context}, history::{History, HistoryId, HistoryStats, store::HistoryStore}, settings::{ CursorStyle, ExitMode, FilterMode, KeymapMode, PreviewStrategy, SearchMode, Settings, @@ -149,7 +152,7 @@ struct StyleState { impl State { async fn query_results( &mut self, - db: &mut dyn Database, + db: &mut ClientSqlite, smart_sort: bool, ) -> Result> { let results = self.engine.query(&self.search, db).await?; @@ -1550,7 +1553,7 @@ fn compute_popup_placement( pub(crate) async fn history( query: &[String], settings: &Settings, - mut db: impl Database, + mut db: ClientSqlite, history_store: &HistoryStore, theme: &Theme, ) -> Result { diff --git a/crates/turtle/src/command/client/server.rs b/crates/turtle/src/command/client/server.rs index def1dfb3..d821d6f8 100644 --- a/crates/turtle/src/command/client/server.rs +++ b/crates/turtle/src/command/client/server.rs @@ -24,7 +24,6 @@ pub(crate) enum Cmd { } impl Cmd { - #[expect(clippy::too_many_lines)] pub(crate) async fn run(self) -> Result<()> { match self { Cmd::Start { host, port } => { diff --git a/crates/turtle/src/command/client/stats.rs b/crates/turtle/src/command/client/stats.rs index 98401cd3..17432bb2 100644 --- a/crates/turtle/src/command/client/stats.rs +++ b/crates/turtle/src/command/client/stats.rs @@ -3,11 +3,8 @@ use eyre::Result; use interim::parse_date_string; use time::{Duration, OffsetDateTime, Time}; -use crate::atuin_client::{ - database::{Database, current_context}, - settings::Settings, - theme::Theme, -}; +use crate::atuin_client::database::ClientSqlite; +use crate::atuin_client::{database::current_context, settings::Settings, theme::Theme}; use crate::atuin_history::stats::{compute, pretty_print}; @@ -39,7 +36,12 @@ pub(crate) struct Cmd { } impl Cmd { - pub(crate) async fn run(&self, db: &impl Database, settings: &Settings, theme: &Theme) -> Result<()> { + pub(crate) async fn run( + &self, + db: &ClientSqlite, + settings: &Settings, + theme: &Theme, + ) -> Result<()> { let context = current_context().await?; let words = if self.period.is_empty() { String::from("all") diff --git a/crates/turtle/src/command/client/store.rs b/crates/turtle/src/command/client/store.rs index 3e9355b5..347c4bee 100644 --- a/crates/turtle/src/command/client/store.rs +++ b/crates/turtle/src/command/client/store.rs @@ -2,9 +2,7 @@ use clap::Subcommand; use eyre::Result; use crate::atuin_client::{ - database::Database, - record::{sqlite_store::SqliteStore, store::Store}, - settings::Settings, + database::ClientSqlite, record::{sqlite_store::SqliteStore, store::Store}, settings::Settings }; use itertools::Itertools; use time::{OffsetDateTime, UtcOffset}; @@ -51,7 +49,7 @@ impl Cmd { pub(crate) async fn run( &self, settings: &Settings, - database: &dyn Database, + database: &ClientSqlite, store: SqliteStore, ) -> Result<()> { match self { diff --git a/crates/turtle/src/command/client/store/pull.rs b/crates/turtle/src/command/client/store/pull.rs index 6b709a64..f2e628d6 100644 --- a/crates/turtle/src/command/client/store/pull.rs +++ b/crates/turtle/src/command/client/store/pull.rs @@ -2,12 +2,7 @@ use clap::Args; use eyre::Result; use crate::atuin_client::{ - database::Database, - encryption::load_key, - record::store::Store, - record::sync::Operation, - record::{sqlite_store::SqliteStore, sync}, - settings::Settings, + database::ClientSqlite, encryption::load_key, record::{sqlite_store::SqliteStore, store::Store, sync::{self, Operation}}, settings::Settings }; #[derive(Args, Debug)] @@ -32,7 +27,7 @@ impl Pull { &self, settings: &Settings, store: SqliteStore, - db: &dyn Database, + db: &ClientSqlite, ) -> Result<()> { if self.force { println!("Forcing local overwrite!"); diff --git a/crates/turtle/src/command/client/store/push.rs b/crates/turtle/src/command/client/store/push.rs index 30177dbd..beec613c 100644 --- a/crates/turtle/src/command/client/store/push.rs +++ b/crates/turtle/src/command/client/store/push.rs @@ -1,6 +1,6 @@ use crate::atuin_common::record::HostId; use clap::Args; -use eyre::Result; +use eyre::{OptionExt, Result}; use uuid::Uuid; use crate::atuin_client::{ @@ -42,11 +42,12 @@ impl Push { println!("Clearing remote store"); let client = Client::new( - &settings.sync_address, - settings.sync_auth().await?.into_auth_token()?, + &settings.sync.address, settings.network_connect_timeout, - settings.network_timeout * 10, // we may be deleting a lot of data... so up the - // timeout + // we may be deleting a lot of data... so increase the + // timeout + settings.network_timeout * 10, + settings.sync.user_id()?.ok_or_eyre("no sync user-id")?, ) .expect("failed to create client"); diff --git a/crates/turtle/src/command/client/store/rebuild.rs b/crates/turtle/src/command/client/store/rebuild.rs index 0959b74e..bee1aa05 100644 --- a/crates/turtle/src/command/client/store/rebuild.rs +++ b/crates/turtle/src/command/client/store/rebuild.rs @@ -5,7 +5,7 @@ use eyre::{Result, bail}; use crate::command::client::daemon as daemon_cmd; use crate::atuin_client::{ - database::Database, encryption, history::store::HistoryStore, + database::ClientSqlite, encryption, history::store::HistoryStore, record::sqlite_store::SqliteStore, settings::Settings, }; @@ -19,7 +19,7 @@ impl Rebuild { &self, settings: &Settings, store: SqliteStore, - database: &dyn Database, + database: &ClientSqlite, ) -> Result<()> { // keep it as a string and not an enum atm // would be super cool to build this dynamically in the future @@ -41,7 +41,7 @@ impl Rebuild { &self, settings: &Settings, store: SqliteStore, - database: &dyn Database, + database: &ClientSqlite, ) -> Result<()> { let encryption_key: [u8; 32] = encryption::load_key(settings)?.into(); diff --git a/crates/turtle/src/command/client/store/rekey.rs b/crates/turtle/src/command/client/store/rekey.rs index 3472222f..b99fb16a 100644 --- a/crates/turtle/src/command/client/store/rekey.rs +++ b/crates/turtle/src/command/client/store/rekey.rs @@ -32,9 +32,15 @@ impl Rekey { store.re_encrypt(¤t_key, &new_key).await?; - println!("Store rewritten. Saving new key"); - let mut file = File::create(settings.key_path.clone()).await?; - file.write_all(key.as_bytes()).await?; + if let Some(key_path) = settings.sync.encryption_key_path.as_ref() { + println!("Store rewritten. Saving new key"); + let mut file = File::create(key_path).await?; + file.write_all(key.as_bytes()).await?; + } else { + println!( + "No key-path (settings.sync.encryption_key_path) set in config, will not save new key." + ); + } Ok(()) } diff --git a/crates/turtle/src/command/client/sync.rs b/crates/turtle/src/command/client/sync.rs index 7adf90ed..84b74cc1 100644 --- a/crates/turtle/src/command/client/sync.rs +++ b/crates/turtle/src/command/client/sync.rs @@ -1,12 +1,12 @@ use clap::Subcommand; use eyre::{Result, WrapErr}; +use serde_json::json; -use crate::atuin_client::{ - database::Database, - encryption, - history::store::HistoryStore, - record::{sqlite_store::SqliteStore, store::Store, sync}, - settings::Settings, +use crate::{ + atuin_client::{ + database::ClientSqlite, encryption, history::store::HistoryStore, record::{sqlite_store::SqliteStore, store::Store, sync}, settings::Settings + }, + atuin_common::utils, }; mod status; @@ -15,14 +15,14 @@ mod status; #[command(infer_subcommands = true)] pub(crate) enum Cmd { /// Sync with the configured server - Sync { + Perform { /// Force re-download everything #[arg(long, short)] force: bool, }, - /// Print the encryption key for transfer to another machine - Key {}, + /// Print (or generate) the encryption key and user id for transfer to another machine + KeyAndId {}, /// Display the sync status Status, @@ -32,18 +32,28 @@ impl Cmd { pub(crate) async fn run( self, settings: Settings, - db: &impl Database, + db: &ClientSqlite, store: SqliteStore, ) -> Result<()> { match self { - Self::Sync { force } => run(&settings, force, db, store).await, + Self::Perform { force } => run(&settings, force, db, store).await, Self::Status => status::run(&settings).await, - Self::Key {} => { + Self::KeyAndId {} => { use crate::atuin_client::encryption::{encode_key, load_key}; + let key = load_key(&settings).wrap_err("could not load encryption key")?; + let user_id = settings + .sync + .user_id() + .wrap_err("Failed to load user-id")? + .unwrap_or_else(utils::uuid_v7); + + let key = encode_key(&key).wrap_err("could not encode encryption key")?; + + let json = serde_json::to_string_pretty(&json!({ "key": key, "user_id": user_id })) + .expect("Will always be formattable"); - let encode = encode_key(&key).wrap_err("could not encode encryption key")?; - println!("{encode}"); + println!("{json}"); Ok(()) } @@ -54,7 +64,7 @@ impl Cmd { async fn run( settings: &Settings, force: bool, - db: &impl Database, + db: &ClientSqlite, store: SqliteStore, ) -> Result<()> { let encryption_key: [u8; 32] = encryption::load_key(settings) diff --git a/crates/turtle/src/command/client/sync/status.rs b/crates/turtle/src/command/client/sync/status.rs index 27b10dbd..e75171eb 100644 --- a/crates/turtle/src/command/client/sync/status.rs +++ b/crates/turtle/src/command/client/sync/status.rs @@ -1,36 +1,24 @@ -use crate::atuin_client::{api_client, settings::Settings}; +use crate::atuin_client::settings::Settings; use crate::{SHA, VERSION}; use colored::Colorize; use eyre::{Result, bail}; pub(crate) async fn run(settings: &Settings) -> Result<()> { - if !settings.have_sync_key().await? { - bail!("You are not logged in to a sync server - cannot show sync status"); - } - - let client = api_client::Client::new( - &settings.sync_address, - settings.sync_auth().await?.into_auth_token()?, - settings.network_connect_timeout, - settings.network_timeout, - )?; - - let me = client.me().await?; - let last_sync = Settings::last_sync().await?; + if let Some(me) = settings.sync.user_id()? { + let last_sync = Settings::last_sync().await?; - println!("Atuin v{VERSION} - Build rev {SHA}\n"); + println!("Atuin v{VERSION} - Build rev {SHA}\n"); - println!("{}", "[Local]".green()); - - if settings.auto_sync { - println!("Sync frequency: {}", settings.sync_frequency); + println!("{}", "[Local]".green()); + println!("Sync frequency: {}", settings.sync.frequency); println!("Last sync: {}", last_sync.to_offset(settings.timezone.0)); - } + println!("Auto sync: {}", settings.sync.auto); - if settings.auto_sync { println!("{}", "[Remote]".green()); - println!("Address: {}", settings.sync_address); - println!("Username: {}", me.username); + println!("Address: {}", settings.sync.address); + println!("User id: {}", me); + } else { + bail!("You are not logged in to a sync server - cannot show sync status"); } Ok(()) diff --git a/crates/turtle/src/command/client/wrapped.rs b/crates/turtle/src/command/client/wrapped.rs index 5e41657e..d502d3ec 100644 --- a/crates/turtle/src/command/client/wrapped.rs +++ b/crates/turtle/src/command/client/wrapped.rs @@ -3,7 +3,8 @@ use eyre::Result; use std::collections::{HashMap, HashSet}; use time::{Date, Duration, Month, OffsetDateTime, Time}; -use crate::atuin_client::{database::Database, settings::Settings, theme::Theme}; +use crate::atuin_client::database::ClientSqlite; +use crate::atuin_client::{settings::Settings, theme::Theme}; use crate::atuin_history::stats::{Stats, compute}; @@ -268,7 +269,7 @@ fn print_fun_facts(wrapped_stats: &WrappedStats, stats: &Stats, year: i32) { pub(crate) async fn run( year: Option, - db: &impl Database, + db: &ClientSqlite, settings: &Settings, theme: &Theme, ) -> Result<()> { diff --git a/crates/turtle/src/command/external.rs b/crates/turtle/src/command/external.rs deleted file mode 100644 index a5daea21..00000000 --- a/crates/turtle/src/command/external.rs +++ /dev/null @@ -1,102 +0,0 @@ -use std::fmt::Write as _; -use std::process::Command; -use std::{io, process}; - -#[cfg(feature = "client")] -use crate::atuin_client::plugin::{OfficialPluginRegistry, PluginContext}; -use clap::CommandFactory; -use clap::builder::{StyledStr, Styles}; -use eyre::Result; - -use crate::Atuin; - -pub(crate) fn run(args: &[String]) -> Result<()> { - let subcommand = &args[0]; - let bin = format!("atuin-{subcommand}"); - let mut cmd = Command::new(&bin); - cmd.args(&args[1..]); - - #[cfg(feature = "client")] - let context = PluginContext::new(subcommand); - - let spawn_result = match cmd.spawn() { - Ok(child) => Ok(child), - Err(e) => match e.kind() { - io::ErrorKind::NotFound => { - let output = render_not_found(subcommand, &bin); - Err(output) - } - _ => Err(e.to_string().into()), - }, - }; - - match spawn_result { - Ok(mut child) => { - let status = child.wait()?; - if status.success() { - Ok(()) - } else { - #[cfg(feature = "client")] - drop(context); - - process::exit(status.code().unwrap_or(1)); - } - } - Err(e) => { - eprintln!("{}", e.ansi()); - - #[cfg(feature = "client")] - drop(context); - - process::exit(1); - } - } -} - -fn render_not_found(subcommand: &str, bin: &str) -> StyledStr { - let mut output = StyledStr::new(); - let styles = Styles::styled(); - - let error = styles.get_error(); - let invalid = styles.get_invalid(); - let literal = styles.get_literal(); - - #[cfg(feature = "client")] - { - let registry = OfficialPluginRegistry::new(); - - // Check if this is an official plugin - if let Some(install_message) = registry.get_install_message(subcommand) { - let _ = write!(output, "{error}error:{error:#} "); - let _ = write!( - output, - "'{invalid}{subcommand}{invalid:#}' is an official atuin plugin, but it's not installed" - ); - let _ = write!(output, "\n\n"); - let _ = write!(output, "{install_message}"); - return output; - } - } - - let mut atuin_cmd = Atuin::command(); - let usage = atuin_cmd.render_usage(); - - let _ = write!(output, "{error}error:{error:#} "); - let _ = write!( - output, - "unrecognized subcommand '{invalid}{subcommand}{invalid:#}' " - ); - let _ = write!( - output, - "and no executable named '{invalid}{bin}{invalid:#}' found in your PATH" - ); - let _ = write!(output, "\n\n"); - let _ = write!(output, "{usage}"); - let _ = write!(output, "\n\n"); - let _ = write!( - output, - "For more information, try '{literal}--help{literal:#}'." - ); - - output -} diff --git a/crates/turtle/src/command/mod.rs b/crates/turtle/src/command/mod.rs index 5d5d839e..308e1970 100644 --- a/crates/turtle/src/command/mod.rs +++ b/crates/turtle/src/command/mod.rs @@ -11,8 +11,6 @@ mod contributors; mod gen_completions; -mod external; - #[derive(Subcommand)] #[command(infer_subcommands = true)] #[expect(clippy::large_enum_variant)] @@ -33,9 +31,6 @@ pub(crate) enum AtuinCmd { /// Generate shell completions GenCompletions(gen_completions::Cmd), - - #[command(external_subcommand)] - External(Vec), } impl AtuinCmd { @@ -67,7 +62,6 @@ impl AtuinCmd { Ok(()) } Self::GenCompletions(gen_completions) => gen_completions.run(), - Self::External(args) => external::run(&args), } } } -- cgit v1.3.1