marustdown 0.1.2

A fast, configurable terminal markdown viewer with syntax highlighting, task toggling and keyboard link following
mod cli;
mod config;
mod doc;
mod highlight;
mod keys;
mod layout;
mod links;
mod pager;
mod render;
mod search;
mod source;
mod table;
mod theme;
mod wrap;

use std::io::{self, IsTerminal};
use std::process::ExitCode;

use clap::error::ErrorKind;
use clap::{CommandFactory, Parser};

use cli::{Args, ColorMode};
use doc::Document;
use keys::Keymap;
use source::Source;
use theme::Theme;

fn main() -> ExitCode {
    match run(Args::parse()) {
        Ok(()) => ExitCode::SUCCESS,
        Err(e) => {
            eprintln!("mar: {e}");
            ExitCode::FAILURE
        }
    }
}

fn run(args: Args) -> Result<(), String> {
    let color = if args.no_color {
        ColorMode::Never
    } else {
        args.color
    };
    let overrides = config::Overrides {
        theme: args.theme.as_deref(),
        no_icons: args.no_icons,
        no_color: color == ColorMode::Never
            || std::env::var_os("NO_COLOR").is_some_and(|v| !v.is_empty()),
    };
    let mut cfg = config::load(args.config.as_deref(), &overrides)?;
    if let Some(width) = args.width {
        cfg.layout.width = width;
    }
    let terminal = io::stdout().is_terminal();
    let paged = !args.cat && terminal;
    let plain = match color {
        ColorMode::Auto => !terminal,
        ColorMode::Always => false,
        ColorMode::Never => true,
    };
    if !paged {
        cfg.glyphs.code_copy.clear();
    }
    let theme = Theme::new(cfg)?;
    let keys = Keymap::new(&theme.keys)?;

    let path = args.file.filter(|p| p.as_os_str() != "-");
    let source = match &path {
        Some(p) => Source::open(p).map_err(|e| format!("{}: {e}", p.display()))?,
        None if io::stdin().is_terminal() => Args::command()
            .error(
                ErrorKind::MissingRequiredArgument,
                "no input: pass a file or pipe markdown to stdin",
            )
            .exit(),
        None => Source::stdin().map_err(|e| format!("stdin: {e}"))?,
    };

    let result = if paged {
        pager::run(source, path, &theme, keys)
    } else {
        cat(&source, &theme, plain)
    };
    match result {
        Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(()),
        r => r.map_err(|e| e.to_string()),
    }
}

fn cat(source: &Source, theme: &Theme, plain: bool) -> io::Result<()> {
    let (width, margin) = match crossterm::terminal::size() {
        Ok((cols, _)) if io::stdout().is_terminal() => theme.layout.fit(cols as usize),
        _ => (theme.layout.width, 0),
    };
    let mut doc = Document::new();
    doc.layout(source.text(), width, theme);
    render::cat(&doc, theme, margin, plain, &mut io::stdout().lock())
}