tinty 0.32.2

Change the theme of your terminal, text editor and anything else with one command!
use crate::{
    constants::ARTIFACTS_DIR,
    operations::list::{scheme_entries_json, schemes_dir_path},
    utils::{ensure_directory_exists, write_to_file},
};
use anyhow::{Context, Result};
use std::{
    fs::File,
    io::Write,
    path::{Path, PathBuf},
    process::{Command, Stdio},
};

const GALLERY_DIR_NAME: &str = "gallery";
const INDEX_HTML: &str = include_str!("gallery/index.html");
const GALLERY_CSS: &str = include_str!("gallery/gallery.css");
const GALLERY_JS: &str = include_str!("gallery/gallery.js");
const LOGO_BYTES: &[u8] = include_bytes!("../../assets/tinted-theming-logo.png");
const FAVICON_BYTES: &[u8] = include_bytes!("../../assets/favicon.png");
const FONT_DM_SERIF_400: &[u8] = include_bytes!("gallery/fonts/dm-serif-display-400.woff2");
const FONT_DM_SERIF_400_ITALIC: &[u8] =
    include_bytes!("gallery/fonts/dm-serif-display-400-italic.woff2");
const FONT_IBM_PLEX_MONO_400: &[u8] = include_bytes!("gallery/fonts/ibm-plex-mono-400.woff2");
const FONT_IBM_PLEX_MONO_500: &[u8] = include_bytes!("gallery/fonts/ibm-plex-mono-500.woff2");

const SNIPPETS: &[(&str, &str)] = &[
    ("rust", include_str!("gallery/snippets/rust.html")),
    ("kotlin", include_str!("gallery/snippets/kotlin.html")),
    ("lisp", include_str!("gallery/snippets/lisp.html")),
    ("elixir", include_str!("gallery/snippets/elixir.html")),
    ("haskell", include_str!("gallery/snippets/haskell.html")),
    ("diff", include_str!("gallery/snippets/diff.html")),
    ("terminal", include_str!("gallery/snippets/terminal.html")),
];

fn snippet_templates() -> String {
    SNIPPETS
        .iter()
        .map(|(id, body)| format!("<template id=\"snippet-{id}\">{body}</template>"))
        .collect::<Vec<_>>()
        .join("\n")
}

pub fn gallery(
    data_path: &Path,
    is_custom: bool,
    dump_dir: Option<&str>,
    should_open: bool,
) -> Result<PathBuf> {
    let schemes_path = schemes_dir_path(data_path, is_custom)?;
    let schemes_json = scheme_entries_json(&schemes_path)?;
    let output_dir = dump_dir.map_or_else(
        || data_path.join(ARTIFACTS_DIR).join(GALLERY_DIR_NAME),
        PathBuf::from,
    );

    write_gallery_files(&output_dir, &schemes_json)?;

    let index_path = output_dir.join("index.html");
    if should_open {
        open_in_browser(&index_path)?;
    }

    println!("Gallery written to {}", index_path.display());

    Ok(index_path)
}

fn write_gallery_files(output_dir: &Path, schemes_json: &str) -> Result<()> {
    let assets_dir = output_dir.join("assets");
    let fonts_dir = assets_dir.join("fonts");

    ensure_directory_exists(output_dir)?;
    ensure_directory_exists(&assets_dir)?;
    ensure_directory_exists(&fonts_dir)?;

    let index_html = INDEX_HTML.replace("<!--SNIPPETS-->", &snippet_templates());
    write_to_file(output_dir.join("index.html"), &index_html)?;
    write_to_file(assets_dir.join("gallery.css"), GALLERY_CSS)?;
    let gallery_js = GALLERY_JS.replace("__TINTY_SCHEMES__", schemes_json);
    write_to_file(assets_dir.join("gallery.js"), &gallery_js)?;
    write_binary_file(assets_dir.join("tinted-theming-logo.png"), LOGO_BYTES)?;
    write_binary_file(assets_dir.join("favicon.png"), FAVICON_BYTES)?;
    write_binary_file(
        fonts_dir.join("dm-serif-display-400.woff2"),
        FONT_DM_SERIF_400,
    )?;
    write_binary_file(
        fonts_dir.join("dm-serif-display-400-italic.woff2"),
        FONT_DM_SERIF_400_ITALIC,
    )?;
    write_binary_file(
        fonts_dir.join("ibm-plex-mono-400.woff2"),
        FONT_IBM_PLEX_MONO_400,
    )?;
    write_binary_file(
        fonts_dir.join("ibm-plex-mono-500.woff2"),
        FONT_IBM_PLEX_MONO_500,
    )?;

    Ok(())
}

fn write_binary_file(path: impl AsRef<Path>, contents: &[u8]) -> Result<()> {
    let mut file = File::create(path.as_ref())
        .map_err(anyhow::Error::new)
        .with_context(|| format!("Unable to create file: {}", path.as_ref().display()))?;

    file.write_all(contents)?;

    Ok(())
}

fn open_in_browser(index_path: &Path) -> Result<()> {
    let index_path = index_path
        .canonicalize()
        .with_context(|| format!("Unable to resolve {}", index_path.display()))?;

    let mut command = browser_command(&index_path);
    let status = command
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .with_context(|| format!("Unable to open gallery at {}", index_path.display()))?;

    if !status.success() {
        return Err(anyhow::anyhow!(
            "Unable to open gallery at {}",
            index_path.display()
        ));
    }

    Ok(())
}

#[cfg(target_os = "macos")]
fn browser_command(path: &Path) -> Command {
    let mut command = Command::new("open");
    command.arg(path);
    command
}

#[cfg(target_os = "windows")]
fn browser_command(path: &Path) -> Command {
    let mut command = Command::new("cmd");
    command.args(["/C", "start", ""]).arg(path);
    command
}

#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
fn browser_command(path: &Path) -> Command {
    let mut command = Command::new("xdg-open");
    command.arg(path);
    command
}