use atuin_common::logs::LogConfig;
use clap::Subcommand;
use eyre::Result;
#[cfg(not(windows))]
use rustix::{fs::Mode, process::umask};
use crate::i18n::fl;
#[cfg(feature = "client")]
mod client;
mod contributors;
mod gen_completions;
mod external;
#[derive(Subcommand)]
#[command(infer_subcommands = true)]
#[allow(clippy::large_enum_variant)]
pub enum AtuinCmd {
#[cfg(feature = "client")]
#[command(flatten)]
Client(client::Cmd),
#[cfg(feature = "pty-proxy")]
#[command(alias = "hex", about = fl!("cmd-pty-proxy"))]
PtyProxy(atuin_pty_proxy::PtyProxy),
#[command(hide = true, about = fl!("cmd-uuid"))]
Uuid,
#[command(about = fl!("cmd-contributors"))]
Contributors,
#[command(about = fl!("cmd-gen-completions"))]
GenCompletions(gen_completions::Cmd),
#[command(external_subcommand)]
External(Vec<String>),
}
impl AtuinCmd {
pub fn run(self) -> Result<()> {
#[cfg(not(windows))]
let prev_umask = umask(Mode::RWXG | Mode::RWXO);
let _log_guard = match &self {
#[cfg(feature = "client")]
Self::Client(_) => None,
_ => Some(crate::logs::LogCtx::try_enable("atuin", &LogConfig::stderr_only())?),
};
match self {
#[cfg(feature = "client")]
Self::Client(client) => client.run(),
#[cfg(all(feature = "pty-proxy", unix))]
Self::PtyProxy(proxy) => {
run_pty_proxy(proxy, prev_umask);
Ok(())
}
#[cfg(all(feature = "pty-proxy", not(unix)))]
Self::PtyProxy(_) => {
eprintln!("atuin pty-proxy currently supports unix platforms");
std::process::exit(1);
}
Self::Contributors => {
contributors::run();
Ok(())
}
Self::Uuid => {
println!("{}", atuin_common::utils::uuid_v7().as_simple());
Ok(())
}
Self::GenCompletions(gen_completions) => gen_completions.run(),
Self::External(args) => external::run(&args),
}
}
}
#[cfg(all(feature = "pty-proxy", unix))]
fn run_pty_proxy(proxy: atuin_pty_proxy::PtyProxy, prev_umask: Mode) {
#[allow(clippy::useless_conversion)]
let child_umask = Some(u32::from(prev_umask.bits()));
#[cfg(feature = "daemon")]
proxy.run(semantic_command_capture_config(), child_umask);
#[cfg(not(feature = "daemon"))]
proxy.run(None, child_umask);
}
#[cfg(all(feature = "daemon", feature = "pty-proxy", unix))]
fn semantic_command_capture_config() -> Option<atuin_pty_proxy::CaptureConfig> {
use std::borrow::Cow;
use std::sync::mpsc;
if is_truthy_env("ATUIN_TERMINAL") {
return None;
}
let settings = atuin_client::settings::Settings::new().ok()?;
let max_output_bytes =
usize::try_from(settings.output.limits()?.max_output_size.as_u64()).unwrap_or(usize::MAX);
let (tx, rx) = mpsc::sync_channel::<(
atuin_client::history::HistoryId,
atuin_pty_proxy::CommandCapture,
)>(128);
std::thread::spawn(move || {
let Ok(runtime) = tokio::runtime::Builder::new_current_thread().enable_all().build() else {
return;
};
runtime.block_on(async move {
let Ok(mut client) = atuin_daemon::HistoryClient::from_settings(&settings).await else {
return;
};
let redactor = settings.security.redactor(settings.secrets_filter);
while let Ok((history_id, capture)) = rx.recv() {
let redact = |output: &mut String| {
match redactor.redact_within(output, atuin_common::secrets::REDACT_BUDGET) {
Some(Cow::Borrowed(_)) => true,
Some(Cow::Owned(redacted)) => {
*output = redacted;
true
}
None => false,
}
};
let mut output_start = capture.output_start;
let mut output_end = capture.output_end;
let redacted = redact(&mut output_start)
&& output_end.as_mut().is_none_or(redact);
if !redacted {
tracing::debug!(%history_id, "output took too long to redact; dropping it");
continue;
}
if let Err(err) = client
.register_command_output(
history_id,
output_start,
output_end,
capture.output_observed_bytes,
capture.terminal_width,
capture.terminal_height,
)
.await
{
tracing::debug!(%history_id, ?err, "could not record command output; dropping it");
}
}
});
});
let sink: atuin_pty_proxy::CommandCaptureSink = Box::new(move |history_id, capture| {
let _ = tx.try_send((history_id, capture));
});
Some(atuin_pty_proxy::CaptureConfig {
sink,
max_output_bytes,
})
}
#[cfg(all(feature = "daemon", feature = "pty-proxy", unix))]
#[inline]
fn is_truthy_env(name: &str) -> bool {
atuin_common::env::var(name)
.ok()
.as_ref()
.is_some_and(|value| !value.trim().is_empty() && value.trim() != "false")
}