view rust/rhg/src/color.rs @ 49314:ec3f88480dbf stable

tests: show that hg log --debug output differs from {file*} template keywords hg log --debug -T xml doesn't differ, but let's test it because we can.
author Anton Shestakov <av6@dwimlabs.net>
date Thu, 12 May 2022 13:52:10 +0400
parents 39c447e03dbc
children 58074252db3c
line wrap: on
line source

use crate::ui::formatted;
use crate::ui::plain;
use format_bytes::write_bytes;
use hg::config::Config;
use hg::config::ConfigOrigin;
use hg::errors::HgError;
use std::collections::HashMap;

pub type Effect = u32;

pub type EffectsMap = HashMap<Vec<u8>, Vec<Effect>>;

macro_rules! effects {
    ($( $name: ident: $value: expr ,)+) => {

        #[allow(non_upper_case_globals)]
        mod effects {
            $(
                pub const $name: super::Effect = $value;
            )+
        }

        fn effect(name: &[u8]) -> Option<Effect> {
            $(
                if name == stringify!($name).as_bytes() {
                    Some(effects::$name)
                } else
            )+
            {
                None
            }
        }
    };
}

effects! {
    none: 0,
    black: 30,
    red: 31,
    green: 32,
    yellow: 33,
    blue: 34,
    magenta: 35,
    cyan: 36,
    white: 37,
    bold: 1,
    italic: 3,
    underline: 4,
    inverse: 7,
    dim: 2,
    black_background: 40,
    red_background: 41,
    green_background: 42,
    yellow_background: 43,
    blue_background: 44,
    purple_background: 45,
    cyan_background: 46,
    white_background: 47,
}

macro_rules! default_styles {
    ($( $key: expr => [$($value: expr),*],)+) => {
        fn default_styles() -> EffectsMap {
            use effects::*;
            let mut map = HashMap::new();
            $(
                map.insert($key[..].to_owned(), vec![$( $value ),*]);
            )+
            map
        }
    };
}

default_styles! {
    b"grep.match" => [red, bold],
    b"grep.linenumber" => [green],
    b"grep.rev" => [blue],
    b"grep.sep" => [cyan],
    b"grep.filename" => [magenta],
    b"grep.user" => [magenta],
    b"grep.date" => [magenta],
    b"grep.inserted" => [green, bold],
    b"grep.deleted" => [red, bold],
    b"bookmarks.active" => [green],
    b"branches.active" => [none],
    b"branches.closed" => [black, bold],
    b"branches.current" => [green],
    b"branches.inactive" => [none],
    b"diff.changed" => [white],
    b"diff.deleted" => [red],
    b"diff.deleted.changed" => [red, bold, underline],
    b"diff.deleted.unchanged" => [red],
    b"diff.diffline" => [bold],
    b"diff.extended" => [cyan, bold],
    b"diff.file_a" => [red, bold],
    b"diff.file_b" => [green, bold],
    b"diff.hunk" => [magenta],
    b"diff.inserted" => [green],
    b"diff.inserted.changed" => [green, bold, underline],
    b"diff.inserted.unchanged" => [green],
    b"diff.tab" => [],
    b"diff.trailingwhitespace" => [bold, red_background],
    b"changeset.public" => [],
    b"changeset.draft" => [],
    b"changeset.secret" => [],
    b"diffstat.deleted" => [red],
    b"diffstat.inserted" => [green],
    b"formatvariant.name.mismatchconfig" => [red],
    b"formatvariant.name.mismatchdefault" => [yellow],
    b"formatvariant.name.uptodate" => [green],
    b"formatvariant.repo.mismatchconfig" => [red],
    b"formatvariant.repo.mismatchdefault" => [yellow],
    b"formatvariant.repo.uptodate" => [green],
    b"formatvariant.config.special" => [yellow],
    b"formatvariant.config.default" => [green],
    b"formatvariant.default" => [],
    b"histedit.remaining" => [red, bold],
    b"ui.addremove.added" => [green],
    b"ui.addremove.removed" => [red],
    b"ui.error" => [red],
    b"ui.prompt" => [yellow],
    b"log.changeset" => [yellow],
    b"patchbomb.finalsummary" => [],
    b"patchbomb.from" => [magenta],
    b"patchbomb.to" => [cyan],
    b"patchbomb.subject" => [green],
    b"patchbomb.diffstats" => [],
    b"rebase.rebased" => [blue],
    b"rebase.remaining" => [red, bold],
    b"resolve.resolved" => [green, bold],
    b"resolve.unresolved" => [red, bold],
    b"shelve.age" => [cyan],
    b"shelve.newest" => [green, bold],
    b"shelve.name" => [blue, bold],
    b"status.added" => [green, bold],
    b"status.clean" => [none],
    b"status.copied" => [none],
    b"status.deleted" => [cyan, bold, underline],
    b"status.ignored" => [black, bold],
    b"status.modified" => [blue, bold],
    b"status.removed" => [red, bold],
    b"status.unknown" => [magenta, bold, underline],
    b"tags.normal" => [green],
    b"tags.local" => [black, bold],
    b"upgrade-repo.requirement.preserved" => [cyan],
    b"upgrade-repo.requirement.added" => [green],
    b"upgrade-repo.requirement.removed" => [red],
}

fn parse_effect(config_key: &[u8], effect_name: &[u8]) -> Option<Effect> {
    let found = effect(effect_name);
    if found.is_none() {
        // TODO: have some API for warnings
        // TODO: handle IO errors during warnings
        let stderr = std::io::stderr();
        let _ = write_bytes!(
            &mut stderr.lock(),
            b"ignoring unknown color/effect '{}' \
              (configured in color.{})\n",
            effect_name,
            config_key,
        );
    }
    found
}

fn effects_from_config(config: &Config) -> EffectsMap {
    let mut styles = default_styles();
    for (key, _value) in config.iter_section(b"color") {
        if !key.contains(&b'.')
            || key.starts_with(b"color.")
            || key.starts_with(b"terminfo.")
        {
            continue;
        }
        // `unwrap` shouldn’t panic since we just got this key from
        // iteration
        let list = config.get_list(b"color", key).unwrap();
        let parsed = list
            .iter()
            .filter_map(|name| parse_effect(key, name))
            .collect();
        styles.insert(key.to_owned(), parsed);
    }
    styles
}

enum ColorMode {
    // TODO: support other modes
    Ansi,
}

impl ColorMode {
    // Similar to _modesetup in mercurial/color.py
    fn get(config: &Config) -> Result<Option<Self>, HgError> {
        if plain(Some("color")) {
            return Ok(None);
        }
        let enabled_default = b"auto";
        // `origin` is only used when `!auto`, so its default doesn’t matter
        let (enabled, origin) = config
            .get_with_origin(b"ui", b"color")
            .unwrap_or((enabled_default, &ConfigOrigin::CommandLineColor));
        if enabled == b"debug" {
            return Err(HgError::unsupported("debug color mode"));
        }
        let auto = enabled == b"auto";
        let always;
        if !auto {
            let enabled_bool = config.get_bool(b"ui", b"color")?;
            if !enabled_bool {
                return Ok(None);
            }
            always = enabled == b"always"
                || *origin == ConfigOrigin::CommandLineColor
        } else {
            always = false
        };
        let formatted = always
            || (std::env::var_os("TERM").unwrap_or_default() != "dumb"
                && formatted(config)?);

        let mode_default = b"auto";
        let mode = config.get(b"color", b"mode").unwrap_or(mode_default);

        if formatted {
            match mode {
                b"ansi" | b"auto" => Ok(Some(ColorMode::Ansi)),
                // TODO: support other modes
                _ => Err(HgError::UnsupportedFeature(format!(
                    "color mode {}",
                    String::from_utf8_lossy(mode)
                ))),
            }
        } else {
            Ok(None)
        }
    }
}

pub struct ColorConfig {
    pub styles: EffectsMap,
}

impl ColorConfig {
    // Similar to _modesetup in mercurial/color.py
    pub fn new(config: &Config) -> Result<Option<Self>, HgError> {
        Ok(match ColorMode::get(config)? {
            None => None,
            Some(ColorMode::Ansi) => Some(ColorConfig {
                styles: effects_from_config(config),
            }),
        })
    }
}