From 3223d93cb3c77ab02aa0a35f2a8314e447cee9a4 Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Thu, 9 Jul 2026 21:43:23 +0200 Subject: chore: Separate daemon, client, server, and lib into crates --- crates/client/src/command/mod.rs | 141 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 crates/client/src/command/mod.rs (limited to 'crates/client/src/command/mod.rs') diff --git a/crates/client/src/command/mod.rs b/crates/client/src/command/mod.rs new file mode 100644 index 00000000..9a648254 --- /dev/null +++ b/crates/client/src/command/mod.rs @@ -0,0 +1,141 @@ +use clap::Subcommand; +use eyre::Result; + +#[cfg(not(windows))] +use rustix::{fs::Mode, process::umask}; + +mod client; +mod contributors; +mod gen_completions; + +#[derive(Subcommand)] +#[command(infer_subcommands = true)] +#[expect(clippy::large_enum_variant)] +pub(crate) enum AtuinCmd { + #[command(flatten)] + Client(client::Cmd), + + /// PTY proxy for atuin + #[command(alias = "hex")] + PtyProxy(crate::atuin_pty_proxy::PtyProxy), + + /// Generate a UUID + Uuid, + + Contributors, + + /// Generate shell completions + GenCompletions(gen_completions::Cmd), +} + +impl AtuinCmd { + pub(crate) fn run(self) -> Result<()> { + #[cfg(not(windows))] + { + // set umask before we potentially open/create files + // or in other words, 077. Do not allow any access to any other user + let mode = Mode::RWXG | Mode::RWXO; + umask(mode); + } + + match self { + Self::Client(client) => client.run(), + + Self::PtyProxy(proxy) => { + run_pty_proxy(proxy); + Ok(()) + } + + Self::Contributors => { + contributors::run(); + Ok(()) + } + Self::Uuid => { + println!("{}", crate::atuin_common::utils::uuid_v7().as_simple()); + Ok(()) + } + Self::GenCompletions(gen_completions) => gen_completions.run(), + } + } +} + +#[cfg(unix)] +fn run_pty_proxy(proxy: crate::atuin_pty_proxy::PtyProxy) { + proxy.run(semantic_command_capture_sink()); +} + +#[cfg(unix)] +fn semantic_command_capture_sink() -> Option { + use std::sync::mpsc; + use std::time::Duration; + + if is_truthy_env("ATUIN_TERMINAL") { + return None; + } + + let settings = crate::atuin_client::settings::Settings::new().ok()?; + let (tx, rx) = mpsc::sync_channel::(128); + + std::thread::spawn(move || { + let Ok(runtime) = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + else { + return; + }; + + while let Ok(first) = rx.recv() { + let mut batch = vec![first]; + + while batch.len() < 64 { + match rx.recv_timeout(Duration::from_millis(25)) { + Ok(capture) => batch.push(capture), + Err(mpsc::RecvTimeoutError::Timeout | mpsc::RecvTimeoutError::Disconnected) => { + break; + } + } + } + + runtime.block_on(send_semantic_command_captures(&settings, batch)); + } + }); + + Some(Box::new(move |capture| { + drop(tx.try_send(capture)); + })) +} + +#[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") +} + +#[cfg(unix)] +async fn send_semantic_command_captures( + settings: &crate::atuin_client::settings::Settings, + batch: Vec, +) { + use crate::atuin_daemon::generated; + + let captures = batch + .into_iter() + .map(|capture| generated::semantic::CommandCapture { + prompt: capture.prompt, + command: capture.command, + output: capture.output, + exit_code: capture.exit_code, + history_id: capture.history_id, + session_id: capture.session_id, + output_truncated: capture.output_truncated, + output_observed_bytes: capture.output_observed_bytes, + }) + .collect(); + + if let Ok(mut client) = crate::atuin_daemon::SemanticClient::from_settings(settings).await { + drop(client.record_commands(captures).await); + } +} -- cgit v1.3.1