mod config;
mod db;
mod edit;
mod frontparse;
mod init;
mod list;
mod new;
use clap::Parser;
use is_terminal::IsTerminal;
use std::io::{self, Read};
use std::sync::atomic::{AtomicBool, Ordering};
pub static VERBOSE: AtomicBool = AtomicBool::new(false);
#[macro_export]
macro_rules! vprintln {
($($arg:tt)*) => {
if $crate::VERBOSE.load(std::sync::atomic::Ordering::Relaxed) {
std::println!($($arg)*);
}
}
}
#[derive(Parser)]
#[command(arg_required_else_help = false)]
#[command(
name = "smarana",
bin_name = "sma",
about = "An extensible note taking system for typst.",
long_about = "Smarana helps take notes in typst by providing an extensible interface to \
manage notes by itself or by attaching itself to the editor of your choice.",
version,
allow_external_subcommands = true
)]
struct Cli {
#[arg(help_heading = "Notebook")]
#[arg(short = 'I', long, value_name = "PATH", default_missing_value = ".", num_args = 0..=1)]
init: Option<String>,
#[arg(help_heading = "Notebook")]
#[arg(long)]
update_library: bool,
#[arg(help_heading = "Note")]
#[arg(short = 'i', long)]
interactive: bool,
#[arg(help_heading = "Notebook")]
#[arg(short = 's', long)]
sync: bool,
#[arg(help_heading = "Note")]
#[arg(short = 'n', long, value_name = "NAME", num_args = 0..=1, default_missing_value = "")]
new: Option<String>,
#[arg(help_heading = "Note")]
#[arg(short = 't', long, value_name = "TEMPLATE")]
template: Option<String>,
#[arg(help_heading = "Note")]
#[arg(long, num_args = 1..)]
tags: Option<Vec<String>>,
#[arg(help_heading = "Note")]
#[arg(short = 'T', long = "type", value_name = "TYPE", default_value = "fleeting")]
note_type: String,
#[arg(help_heading = "Note")]
#[arg(short = 'e', long, value_name = "NAME")]
edit: Option<String>,
#[arg(help_heading = "Note")]
#[arg(short = 'l', long)]
list: bool,
#[arg(help_heading = "Note")]
#[arg(short = 'j', long)]
json: bool,
#[arg(help_heading = "Note")]
#[arg(long)]
no_edit: bool,
#[arg(help_heading = "Config")]
#[arg(short = 'c', long)]
config: bool,
#[arg(help_heading = "Config")]
#[arg(short = 'a', long)]
alias: bool,
#[arg(trailing_var_arg = true, hide = true)]
args: Vec<String>,
#[arg(short = 'v', long)]
verbose: bool,
}
fn main() {
let cli = Cli::parse();
if cli.verbose {
VERBOSE.store(true, Ordering::Relaxed);
}
let cfg = config::AppConfig::load();
let stdin_data = get_stdin_data();
if let Some(path) = cli.init {
init::initialize(&path);
return;
}
if cli.sync {
let global = config::GlobalConfig::load();
if let Some(path) = global.notebook_path() {
crate::vprintln!("Synchronizing notebook database...");
db::sync(&path);
crate::vprintln!("Completed synchronization.");
} else {
eprintln!("Notebook not initialized");
std::process::exit(1);
}
}
if cli.update_library {
let global = config::GlobalConfig::load();
if let Some(path) = global.notebook_path() {
crate::vprintln!("Updating library...");
init::update_library(path.to_str().unwrap());
crate::vprintln!("Completed library update.");
return;
} else {
eprintln!("Notebook not initialized");
std::process::exit(1);
}
}
if let Some(name) = cli.new {
let title_opt = Some(name).filter(|n| !n.is_empty());
let body_content = if cli.interactive { stdin_data } else { None };
new::create_note(title_opt, cli.template, Some(cli.note_type), cli.tags, body_content, cli.no_edit);
return;
}
if let Some(name) = cli.edit {
edit::run(name, cli.no_edit);
return;
}
if let Some(data) = stdin_data {
if !cli.sync && !cli.list && !cli.json && !cli.alias && !cli.config && cli.args.is_empty() {
if !data.is_empty() {
edit::run(data, cli.no_edit);
return;
}
}
}
if cli.list || cli.json {
list::run(cli.json);
return;
}
if cli.alias {
cfg.list_aliases();
}
if cli.config {
config::open_config();
}
if let Some(name) = cli.args.first() {
if !cfg.run_alias(name) {
eprintln!("Unknown alias: '{name}'. Use `sma --alias` to list available aliases.");
std::process::exit(1);
}
} else if std::env::args().len() == 1 {
use clap::CommandFactory;
Cli::command().print_help().unwrap();
}
}
fn get_stdin_data() -> Option<String> {
if !io::stdin().is_terminal() {
let mut buffer = String::new();
if io::stdin().read_to_string(&mut buffer).is_ok() {
let trimmed = buffer.trim().to_string();
return if !trimmed.is_empty() { Some(trimmed) } else { None };
}
}
None
}