mod app;
mod clipboard;
mod config;
mod diff;
mod export;
mod finder;
mod fs;
mod help;
mod image;
mod input;
mod render;
mod text;
mod transclude;
mod tree;
use std::path::PathBuf;
use anyhow::Result;
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "shoin", version, about = "A no-clutter terminal Markdown editor")]
struct Cli {
file: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
config: Option<PathBuf>,
#[arg(long)]
zen: bool,
#[arg(long)]
print_config: bool,
#[arg(long)]
init_config: bool,
#[arg(long)]
force: bool,
#[arg(long, value_name = "FILE")]
export: Option<PathBuf>,
#[arg(long, value_name = "FORMAT", default_value = "md")]
format: String,
#[arg(long)]
justify: bool,
#[arg(long, value_name = "PATH")]
out: Option<PathBuf>,
#[arg(long)]
stdout: bool,
}
fn main() -> Result<()> {
let cli = Cli::parse();
let explicit = cli.config.clone();
if cli.init_config {
return match &explicit {
Some(dir) => config::init::seed(dir, cli.force).map(|done| {
println!("wrote {} files to {}", done.written.len(), done.dir.display());
}),
None => config::init::run(cli.force),
};
}
let config = config::load(explicit.as_deref())?;
if cli.print_config {
println!("{config:#?}");
return Ok(());
}
if let Some(src) = &cli.export {
let format = export::Format::parse(&cli.format)
.ok_or_else(|| anyhow::anyhow!("{:?} is not md, txt, html or pdf", cli.format))?;
if cli.justify && !matches!(format, export::Format::Html | export::Format::Text) {
anyhow::bail!("--justify only applies to --format html or --format txt");
}
let page = export::html::Page {
theme: render::theme::Theme::authored(&config.theme).unwrap_or_default(),
measure: config.layout.measure,
base: src.parent().unwrap_or(std::path::Path::new("")).to_path_buf(),
justify: cli.justify,
};
let title = src
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "document".into());
if cli.stdout {
if format == export::Format::Pdf {
anyhow::bail!("--stdout cannot print a PDF; give --out a path");
}
let flat = transclude::compile::compile(src, &config.transclude)?;
match format {
export::Format::Text => print!("{}", export::plain::render(&flat, page.measure, page.justify)),
export::Format::Html => print!("{}", export::html::render(&flat, &title, &page)),
_ => print!("{flat}"),
}
return Ok(());
}
let dest = cli
.out
.clone()
.unwrap_or_else(|| export::default_path(src, format));
export::write(src, &dest, format, &config.transclude, &page)?;
eprintln!("{}", dest.display());
return Ok(());
}
let mouse = config.input.mouse;
let mut app = app::App::new(config, cli.file, explicit)?;
if cli.zen {
app.set_zen(true);
}
let mut terminal = ratatui::init();
use ratatui::crossterm::event::{
DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
EnableFocusChange, EnableMouseCapture,
};
if mouse {
let _ = ratatui::crossterm::execute!(std::io::stdout(), EnableMouseCapture);
}
let _ = ratatui::crossterm::execute!(std::io::stdout(), EnableFocusChange);
let _ = ratatui::crossterm::execute!(std::io::stdout(), EnableBracketedPaste);
let result = app.run_loop(&mut terminal);
let _ = ratatui::crossterm::execute!(std::io::stdout(), DisableBracketedPaste);
let _ = ratatui::crossterm::execute!(std::io::stdout(), DisableFocusChange);
let _ = ratatui::crossterm::execute!(std::io::stdout(), DisableMouseCapture);
ratatui::restore();
result
}