#![deny(unsafe_code)]
use std::io::{self, IsTerminal, Write};
use std::path::PathBuf;
use std::process::ExitCode;
use clap::Parser;
use tracing_subscriber::EnvFilter;
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
fn main() -> ExitCode {
atomwrite::signal::reset_sigpipe();
atomwrite::platform::init_console();
let _early_shutdown = atomwrite::signal::install_handlers_early();
human_panic::setup_panic!();
if let Some(schema_cmd) = prescan_json_schema() {
let mut out = io::stdout().lock();
match atomwrite::emit_schema_by_name(&schema_cmd, &mut out) {
Ok(true) => return ExitCode::from(0),
Ok(false) => {}
Err(e) => {
let _ = writeln!(io::stderr(), "atomwrite: {e:#}");
return ExitCode::from(1);
}
}
}
let cli = match atomwrite::cli::Cli::try_parse() {
Ok(c) => c,
Err(clap_err) => {
use clap::error::ErrorKind;
match clap_err.kind() {
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => {
clap_err.exit();
}
_ => {
let msg = clap_err.to_string();
let suggestion = enrich_clap_suggestion(&msg);
let ej = atomwrite::error::ErrorJson {
error: true,
code: "ARGUMENT_PARSE_ERROR",
exit: 2,
message: msg,
path: None,
error_class: atomwrite::error::ErrorClass::Permanent.as_str(),
retryable: false,
suggestion,
workspace: None,
failed_pair_index: None,
pairs_total: None,
pair_results: None,
best_candidate: None,
candidates: None,
match_count: None,
similar_paths: None,
};
let mut out = io::stdout().lock();
if let Err(e) = serde_json::to_writer(&mut out, &ej) {
let _ =
writeln!(io::stderr(), "atomwrite: failed to write error JSON: {e}");
}
let _ = out.write_all(b"\n");
let _ = out.flush();
return ExitCode::from(2);
}
}
}
};
init_locale(cli.global.lang.as_deref());
let _guard = init_tracing(cli.global.verbose, cli.global.quiet, cli.global.no_color);
install_panic_hook();
let shutdown = atomwrite::signal::install_handlers()
.inspect_err(|e| tracing::warn!(%e, "signal handler registration failed"))
.ok();
let stdin = io::stdin();
let stdout = io::stdout();
let stdin_is_tty = stdin.is_terminal();
let exit = match atomwrite::run(&cli, stdin.lock(), stdout.lock(), stdin_is_tty) {
Ok(()) => {
if let Some(ref sig) = shutdown {
if sig.is_shutdown() {
atomwrite::signal::write_shutdown_message();
tracing::info!(signal = sig.exit_code(), "shutdown initiated");
ExitCode::from(sig.exit_code())
} else {
ExitCode::from(0)
}
} else {
ExitCode::from(0)
}
}
Err(err) => {
if let Some(aw_err) = err.downcast_ref::<atomwrite::error::AtomwriteError>() {
if matches!(aw_err, atomwrite::error::AtomwriteError::BrokenPipe) {
return ExitCode::from(141);
}
let mut out = io::stdout().lock();
let ctx = atomwrite::error::ErrorContext {
workspace_provided: cli.global.workspace.is_some(),
workspace: cli.global.workspace.clone(),
};
let _ =
atomwrite::output::write_error_json_with_context(&mut out, aw_err, None, &ctx);
let _ = out.flush();
ExitCode::from(aw_err.exit_code())
} else if let Some(io_err) = find_io_error(&err) {
let aw_err = io_to_atomwrite_error(io_err, &err);
let mut out = io::stdout().lock();
let ctx = atomwrite::error::ErrorContext {
workspace_provided: cli.global.workspace.is_some(),
workspace: cli.global.workspace.clone(),
};
let _ =
atomwrite::output::write_error_json_with_context(&mut out, &aw_err, None, &ctx);
let _ = out.flush();
ExitCode::from(aw_err.exit_code())
} else {
let _ = writeln!(io::stderr(), "atomwrite: {err:#}");
ExitCode::from(1)
}
}
};
tracing::info!("shutdown complete");
exit
}
fn init_tracing(
verbose: u8,
quiet: u8,
cli_no_color: bool,
) -> tracing_appender::non_blocking::WorkerGuard {
let level = match (verbose, quiet) {
(0, 0) => "warn",
(1, _) => "info",
(2, _) => "debug",
(3.., _) => "trace",
(_, 1) => "error",
(_, 2..) => "off",
};
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level));
let ansi = if no_color() || cli_no_color {
false
} else if force_color() {
true
} else {
std::io::IsTerminal::is_terminal(&std::io::stderr())
};
let show_source = matches!(level, "debug" | "trace");
let (non_blocking, guard) = tracing_appender::non_blocking(io::stderr());
let fmt_layer = tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_target(false)
.with_ansi(ansi)
.with_thread_ids(true)
.with_file(show_source)
.with_line_number(show_source)
.compact();
use tracing_subscriber::prelude::*;
tracing_subscriber::registry()
.with(filter)
.with(fmt_layer)
.with(tracing_error::ErrorLayer::default())
.init();
tracing::debug!(filter = %level, "tracing initialized");
guard
}
fn install_panic_hook() {
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let payload = if let Some(s) = info.payload().downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = info.payload().downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
};
let location = info
.location()
.map(|l| format!("{}:{}", l.file(), l.line()));
tracing::error!(
panic.payload = %payload,
panic.location = location.as_deref().unwrap_or("unknown"),
"process panicked"
);
default_hook(info);
}));
}
fn no_color() -> bool {
std::env::var_os("NO_COLOR").is_some_and(|v| !v.is_empty())
}
fn force_color() -> bool {
std::env::var_os("CLICOLOR_FORCE").is_some_and(|v| v == "1")
}
fn extract_clap_tip(msg: &str) -> Option<String> {
for line in msg.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("tip:") {
return Some(rest.trim().to_string());
}
}
None
}
fn enrich_clap_suggestion(msg: &str) -> Option<String> {
let clap_tip = extract_clap_tip(msg);
let msg_lower = msg.to_ascii_lowercase();
let mentions_edit_args = msg_lower.contains("--old")
|| msg_lower.contains("--new")
|| msg_lower.contains("--after-match")
|| msg_lower.contains("--before-match")
|| msg_lower.contains("--between");
let is_edit_subcommand =
msg.contains("Usage: atomwrite edit") || msg.contains("atomwrite edit ");
let hyphen_value_error = msg.contains("wasn't expected")
|| msg.contains("unexpected argument")
|| (msg.contains("tip:") && msg.contains("'--'"));
if mentions_edit_args || (hyphen_value_error && is_edit_subcommand) {
let base = "For content with special characters (hyphens, quotes, shell metacharacters), \
use --old-file <PATH> and --new-file <PATH> to read content from files \
instead of CLI arguments. This bypasses shell expansion and argument \
parsing entirely.";
return Some(match clap_tip {
Some(tip) => format!("{base} (original clap tip: {tip})"),
None => base.to_string(),
});
}
clap_tip
}
fn prescan_json_schema() -> Option<String> {
let args: Vec<String> = std::env::args().collect();
if !args.iter().any(|a| a == "--json-schema") {
return None;
}
const SUBCOMMANDS: &[&str] = &[
"read",
"write",
"edit",
"search",
"replace",
"hash",
"delete",
"count",
"diff",
"move",
"copy",
"list",
"extract",
"calc",
"regex",
"transform",
"scope",
"batch",
"backup",
"rollback",
"apply",
"completions",
"prune-backups",
"edit-loop",
"get",
"set",
"del",
"outline",
"query",
"case",
"semantic-merge",
"sparse",
"recipe",
"stat",
"agent-surface",
"watch",
"codemod",
"semantic-search",
"verify",
"wal-stats",
"wal-heal",
"progress",
"error",
"best-candidate",
"cancelled",
];
for arg in &args[1..] {
if SUBCOMMANDS.contains(&arg.as_str()) {
return Some(arg.clone());
}
}
None
}
fn init_locale(lang_override: Option<&str>) {
let locale = if let Some(lang) = lang_override {
lang.to_owned()
} else {
sys_locale::get_locale().unwrap_or_else(|| "en".to_string())
};
let resolved = if locale.starts_with("pt") {
"pt-BR"
} else {
"en"
};
rust_i18n::set_locale(resolved);
}
fn find_io_error(err: &anyhow::Error) -> Option<std::io::ErrorKind> {
for cause in err.chain() {
if let Some(io_err) = cause.downcast_ref::<std::io::Error>() {
return Some(io_err.kind());
}
}
None
}
fn io_to_atomwrite_error(
kind: std::io::ErrorKind,
err: &anyhow::Error,
) -> atomwrite::error::AtomwriteError {
let msg = format!("{err:#}");
match kind {
std::io::ErrorKind::PermissionDenied => {
atomwrite::error::AtomwriteError::PermissionDenied {
path: extract_path_from_message(&msg),
}
}
std::io::ErrorKind::NotFound => atomwrite::error::AtomwriteError::NotFound {
path: extract_path_from_message(&msg),
},
_ => atomwrite::error::AtomwriteError::Io {
source: std::io::Error::new(kind, msg),
},
}
}
fn extract_path_from_message(msg: &str) -> PathBuf {
if let Some(start) = msg.find('/') {
let rest = &msg[start..];
let end = rest.find(':').unwrap_or(rest.len());
return PathBuf::from(&rest[..end]);
}
PathBuf::from("unknown")
}