aboutsummaryrefslogtreecommitdiffstats
path: root/crates/client/src/command/mod.rs
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-09 21:43:23 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-09 21:43:23 +0200
commit3223d93cb3c77ab02aa0a35f2a8314e447cee9a4 (patch)
tree3971a1f37f5fe3cadb747c5229a0d54e868e45e3 /crates/client/src/command/mod.rs
parentfix(client/sync): Pass through precise error on `SyncError::WrongKey` (diff)
downloadatuin-3223d93cb3c77ab02aa0a35f2a8314e447cee9a4.zip
chore: Separate daemon, client, server, and lib into crates
Diffstat (limited to 'crates/client/src/command/mod.rs')
-rw-r--r--crates/client/src/command/mod.rs141
1 files changed, 141 insertions, 0 deletions
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<crate::atuin_pty_proxy::CommandCaptureSink> {
+ 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::<crate::atuin_pty_proxy::CommandCapture>(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<crate::atuin_pty_proxy::CommandCapture>,
+) {
+ 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);
+ }
+}