sauva 0.7.0

Terminal Unicode Explorer
mod app;
mod bookmarks;
mod browser;
mod cli;
mod clipboard;
mod config;
mod copy_dialog;
mod fixtures;
mod glyph;
mod graphics;
mod help;
mod image;
mod input;
mod inspector;
mod keybindings;
mod normalization;
mod output;
mod preview;
mod property;
mod report;
mod search;
mod sequence;
mod terminal;
mod ui;
mod unicode;
mod viewport;

use std::{error::Error, io, process::ExitCode};

fn main() -> ExitCode {
    let options = match cli::parse() {
        Ok(options) => options,
        Err(cli::ParseError::Arguments(error)) if !error.use_stderr() => error.exit(),
        Err(error) => {
            eprintln!("sauva: {error}");
            return ExitCode::from(error.exit_code());
        }
    };
    match run(options) {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => {
            eprintln!("sauva: {error}");
            ExitCode::FAILURE
        }
    }
}

fn run(options: cli::LaunchOptions) -> Result<(), Box<dyn Error>> {
    if options.print_default_config() {
        print!("{}", config::default_toml()?);
        return Ok(());
    }
    if let Some(format) = options.output() {
        output::write(
            format,
            options.target().expect("output requires an input source"),
            io::stdout().lock(),
        )?;
        return Ok(());
    }
    let config = config::load()?;
    let mut state = initial_state(options.demo(), options.target());
    match bookmarks::load() {
        Ok(entries) => state.set_bookmarks(entries),
        Err(error) => app::update(
            &mut state,
            app::Action::ShowFooterStatus(app::FooterStatus::warning(error.to_string())),
        ),
    }

    terminal::run(
        &mut state,
        options.graphics(),
        config.glyph_preview(),
        config.color_theme(),
        config.ui(),
        config.keymap(),
    )?;
    Ok(())
}

fn initial_state(demo: Option<cli::Demo>, target: Option<&cli::LaunchTarget>) -> app::AppState {
    debug_assert!(demo.is_none() || target.is_none());
    if let Some(target) = target {
        return match target {
            cli::LaunchTarget::CodePoint(code_point) => app::AppState::with_selected(*code_point),
            cli::LaunchTarget::Sequence(source) => app::AppState::with_sequence(source.clone()),
        };
    }

    match demo {
        None => fixtures::startup(),
        Some(cli::Demo::DefaultIgnorable) => fixtures::default_ignorable(),
        Some(cli::Demo::Unassigned) => fixtures::unassigned(),
        Some(cli::Demo::Surrogate) => fixtures::surrogate(),
        Some(cli::Demo::BrowsePlanes) => fixtures::browse_planes(),
        Some(cli::Demo::BrowseRanges) => fixtures::browse_ranges(),
        Some(cli::Demo::BrowseBlocks) => fixtures::browse_blocks(),
        Some(cli::Demo::BrowseCodePoints) => fixtures::browse_code_points(),
        Some(cli::Demo::BrowseSpecial) => fixtures::browse_special(),
        Some(cli::Demo::BrowsePlane16End) => fixtures::browse_plane_16_end(),
        Some(cli::Demo::SearchEmpty) => fixtures::search_empty(),
        Some(cli::Demo::SearchNameResults) => fixtures::search_name_results(),
        Some(cli::Demo::SearchCodePoint) => fixtures::search_code_point(),
        Some(cli::Demo::SearchLiteral) => fixtures::search_literal(),
        Some(cli::Demo::SearchNoResults) => fixtures::search_no_results(),
        Some(cli::Demo::SearchInvalidNotation) => fixtures::search_invalid_notation(),
        Some(cli::Demo::SearchSpecial) => fixtures::search_special(),
        Some(cli::Demo::DetailsAliases) => fixtures::details_aliases(),
        Some(cli::Demo::DetailsCanonicalDecomposition) => {
            fixtures::details_canonical_decomposition()
        }
        Some(cli::Demo::DetailsSupplementary) => fixtures::details_supplementary(),
        Some(cli::Demo::DetailsSurrogate) => fixtures::details_surrogate(),
        Some(cli::Demo::DetailsScrolledMinimum) => fixtures::details_scrolled_minimum(),
        Some(cli::Demo::GlyphBasic) => fixtures::glyph_basic(),
        Some(cli::Demo::GlyphJapanese) => fixtures::glyph_japanese(),
        Some(cli::Demo::GlyphCjk) => fixtures::glyph_cjk(),
        Some(cli::Demo::GlyphCombining) => fixtures::glyph_combining(),
        Some(cli::Demo::GlyphBlank) => fixtures::glyph_blank(),
        Some(cli::Demo::GlyphMissing) => fixtures::glyph_missing(),
        Some(cli::Demo::GlyphSurrogate) => fixtures::glyph_surrogate(),
        Some(cli::Demo::GlyphDisabled) => fixtures::glyph_disabled(),
    }
}

#[cfg(test)]
mod tests {
    use crate::{cli::LaunchTarget, initial_state, unicode::CodePoint};

    #[test]
    fn initial_code_point_selects_the_inspected_value() {
        let code_point = CodePoint::new(0x2192).unwrap();

        let state = initial_state(None, Some(&LaunchTarget::CodePoint(code_point)));

        assert_eq!(state.selected(), code_point);
    }

    #[test]
    fn initial_sequence_opens_the_sequence_view() {
        let source = "A\u{0301} 👩‍💻";

        let state = initial_state(None, Some(&LaunchTarget::Sequence(source.to_owned())));

        assert_eq!(state.view(), crate::app::View::Sequence);
        assert_eq!(state.sequence().unwrap().selected().value(), 0x0041);
        let analysis = state.sequence().unwrap().analysis();
        assert_eq!(analysis.source(), source);
        assert_eq!(analysis.code_points().len(), 6);
        assert_eq!(analysis.graphemes().len(), 3);
    }
}