smarana 0.10.11

An extensible note taking system for typst.
mod config;
mod db;
mod edit;
mod frontparse;
mod init;
mod links;
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 {
    /// Initialize a notebook (optionally at a given path)
    #[arg(help_heading = "Notebook")]
    #[arg(short = 'I', long, value_name = "PATH", default_missing_value = ".", num_args = 0..=1)]
    init: Option<String>,

    /// Update internal typst library framework
    #[arg(help_heading = "Notebook")]
    #[arg(long)]
    update_library: bool,

    /// Interactive mode: uses piped stdin as new note content
    #[arg(help_heading = "Note")]
    #[arg(short = 'i', long)]
    interactive: bool,

    /// Refresh and index all notes
    #[arg(help_heading = "Notebook")]
    #[arg(short = 's', long)]
    sync: bool,

    /// Create a new note
    #[arg(help_heading = "Note")]
    #[arg(short = 'n', long, value_name = "NAME", num_args = 0..=1, default_missing_value = "")]
    new: Option<String>,

    /// Template to use for the new note
    #[arg(help_heading = "Note")]
    #[arg(short = 't', long, value_name = "TEMPLATE")]
    template: Option<String>,

    /// Tags for the new note
    #[arg(help_heading = "Note")]
    #[arg(long, num_args = 1..)]
    tags: Option<Vec<String>>,

    /// Note type: fleeting, capture, or atomic
    #[arg(help_heading = "Note")]
    #[arg(short = 'T', long = "type", value_name = "TYPE", default_value = "fleeting")]
    note_type: String,

    /// Edit a note
    #[arg(help_heading = "Note")]
    #[arg(short = 'e', long, value_name = "NAME")]
    edit: Option<String>,

    /// List all individual notes
    #[arg(help_heading = "Note")]
    #[arg(short = 'l', long)]
    list: bool,

    /// Output all notes with metadata in JSON format
    #[arg(help_heading = "Note")]
    #[arg(short = 'j', long)]
    json: bool,

    /// Create/edit note without launching the editor
    #[arg(help_heading = "Note")]
    #[arg(long)]
    no_edit: bool,

    /// Opens the config file (depending on the working directory)
    #[arg(help_heading = "Config")]
    #[arg(short = 'c', long)]
    config: bool,

    /// List aliases set by the user
    #[arg(help_heading = "Config")]
    #[arg(short = 'a', long)]
    alias: bool,

    /// External subcommand (alias)
    #[arg(trailing_var_arg = true, hide = true)]
    args: Vec<String>,

    /// Enable verbose output (informational messages)
    #[arg(short = 'v', long)]
    verbose: bool,

    /// Show all notes that link to a given note (by slug or title)
    #[arg(help_heading = "Note")]
    #[arg(short = 'b', long, value_name = "SLUG_OR_TITLE")]
    backlinks: Option<String>,
}

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();

    // Notebook
    if let Some(path) = cli.init {
        init::initialize(&path);
        return;
    }

    // Sync Database
    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);
        }
    }

    // Update Library
    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);
        }
    }

    // New Note
    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;
    }

    // Edit Note
    if let Some(name) = cli.edit {
        edit::run(name, cli.no_edit);
        return;
    }

    // Default Stdin Behavior (Edit Mode)
    if let Some(data) = stdin_data {
        // If there are no other arguments, provide help if stdin is empty, otherwise edit.
        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;
            }
        }
    }

    // Listing
    if cli.list || (cli.json && cli.backlinks.is_none()) {
        list::run(cli.json);
        return;
    }

    // Backlinks
    if let Some(target) = cli.backlinks {
        let global = config::GlobalConfig::load();
        if let Some(path) = global.notebook_path() {
            match links::backlinks(&path, &target) {
                Ok(entries) if entries.is_empty() => {
                    println!("No backlinks found for '{}'.", target);
                }
                Ok(entries) => {
                    if cli.json {
                        println!("{}", serde_json::to_string_pretty(&entries).unwrap_or_default());
                    } else {
                        println!("Backlinks for '{}':", target);
                        for e in &entries {
                            println!("  {}{}", e.filename, e.title);
                        }
                    }
                }
                Err(e) => {
                    eprintln!("Backlinks query failed: {}", e);
                    std::process::exit(1);
                }
            }
        } else {
            eprintln!("Notebook not initialized");
            std::process::exit(1);
        }
        return;
    }

    // Config
    if cli.alias {
        cfg.list_aliases();
    }

    if cli.config {
        config::open_config();
    }

    // Alias execution: try the first positional arg as an alias name
    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 {
        // If no arguments at all were provided and no stdin, show help
        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
}