use std::path::PathBuf;
use anyhow::Result;
use edamame::app::{diff_label, difftool, is_markdown_pair, read_side, App};
use edamame::cli::{self, Invocation, RunOpts};
use edamame::config::{self, Config, LoadedConfig};
use edamame::terminal::{self, Capabilities, ColorDepth, TerminalSetup};
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
const EXIT_USAGE: i32 = 2;
enum Session {
Open(Option<PathBuf>),
Diff {
old: String,
new: String,
label: String,
},
}
fn main() -> Result<()> {
let invocation = Invocation::parse(std::env::args_os().skip(1)).unwrap_or_else(|e| {
eprintln!("edamame: {e}\n\n{}", cli::USAGE);
std::process::exit(EXIT_USAGE);
});
match invocation {
Invocation::Help => {
print!("{}", cli::help_text());
Ok(())
}
Invocation::Version => {
println!("{}", cli::version_line());
Ok(())
}
Invocation::Doctor => cli::run_doctor(),
Invocation::Run { file, opts } => run(Session::Open(file), opts),
Invocation::Diff { old, new, opts } => {
let label = diff_label(&old, &new);
if !is_markdown_pair(&old, &new) {
eprintln!("edamame: {label} is not Markdown — skipped");
return Ok(());
}
let sides = read_side(&old).and_then(|o| read_side(&new).map(|n| (o, n)));
let (old_text, new_text) = match sides {
Ok(pair) => pair,
Err(e) => {
eprintln!("edamame: {e:#}");
return Ok(());
}
};
if old_text == new_text {
eprintln!("edamame: {label} has no differences to review");
return Ok(());
}
run(
Session::Diff {
old: old_text,
new: new_text,
label,
},
opts,
)
}
}
}
fn run(session: Session, opts: RunOpts) -> Result<()> {
let file_path = match &session {
Session::Open(path) => path.clone(),
Session::Diff { .. } => None,
};
let (file_path, startup_anchor) = match file_path {
Some(path) => {
let (path, anchor) = cli::split_startup_anchor(&path);
if let Err(e) = preflight_open(&path) {
eprintln!("edamame: {e}");
std::process::exit(1);
}
(Some(path), anchor)
}
None => (None, None),
};
let truecolor_at_load = Capabilities::detect_color_depth_from_env() == ColorDepth::TrueColor;
let loaded = if opts.no_config {
LoadedConfig::default()
} else {
Config::ensure_default_files(truecolor_at_load);
Config::load(truecolor_at_load, true).unwrap_or_else(|e| {
eprintln!("Warning: failed to load config: {e}. Using defaults.");
LoadedConfig::default()
})
};
let LoadedConfig {
mut config,
keybindings,
theme,
state,
warnings: config_warnings,
} = loaded;
if opts.no_config {
config::disable_config_dir();
config.editor.show_welcome = false;
}
config.dev.logging |= opts.log;
let log_guard = if config.dev.logging {
setup_logging()
} else {
None
};
let TerminalSetup {
terminal,
keyboard_enhancement,
} = terminal::setup()?;
let orig_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
if terminal::panic_is_expected() {
tracing::warn!(%info, "caught a panic in a guarded section");
return;
}
let _ = terminal::restore();
orig_hook(info);
}));
let capabilities = Capabilities::detect(keyboard_enhancement, config.images.sharp_scrolling);
log_capabilities(&capabilities);
if capabilities.mouse {
if let Err(e) = terminal::enable_mouse() {
tracing::warn!(error = %e, "failed to enable mouse capture");
}
}
let mut app = match App::new(
config,
state,
keybindings,
theme,
file_path,
capabilities,
config_warnings,
) {
Ok(app) => app
.with_startup_anchor(startup_anchor)
.with_clipboard(edamame::clipboard::default_source()),
Err(e) => {
let _ = terminal::restore();
return Err(e);
}
};
if let Session::Diff { old, new, label } = session {
app.set_diff_label(Some(label));
if !app.enter_read_only_diff(old, new) {
terminal::restore()?;
anyhow::bail!("no differences to review");
}
}
let run_result = app.run(terminal);
let diff_stop_walk = app.diff_stop_walk();
terminal::restore()?;
let logging_started = log_guard.is_some();
drop(log_guard);
if opts.log {
match Config::log_dir().filter(|_| logging_started) {
Some(dir) => eprintln!(
"edamame: debug log written under {} (debug.log.<date>)",
dir.display()
),
None => eprintln!("edamame: --log could not open a log file; no log was written"),
}
}
run_result?;
if diff_stop_walk && difftool::under_git_difftool() {
difftool::stop_walk();
}
Ok(())
}
fn preflight_open(path: &std::path::Path) -> Result<(), String> {
let Ok(meta) = std::fs::metadata(path) else {
return Ok(());
};
if meta.is_dir() {
return Err(format!("{} is a directory", path.display()));
}
if let Err(e) = std::fs::read_to_string(path) {
return Err(format!("cannot open {}: {e}", path.display()));
}
Ok(())
}
fn setup_logging() -> Option<tracing_appender::non_blocking::WorkerGuard> {
let log_dir = Config::log_dir()?;
if std::fs::create_dir_all(&log_dir).is_err() {
return None;
}
let file_appender = tracing_appender::rolling::daily(&log_dir, "debug.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("debug"));
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(non_blocking)
.with_ansi(false)
.init();
tracing::info!("edamame starting");
Some(guard)
}
fn log_capabilities(caps: &Capabilities) {
tracing::info!(
color_depth = ?caps.color_depth,
mouse = caps.mouse,
image_protocol = ?caps.image_protocol,
unicode_full = caps.unicode_full,
keyboard_enhancement = caps.keyboard_enhancement,
"terminal capabilities detected"
);
}