rust/rhg/src/color.rs
changeset 48733 39c447e03dbc
child 49914 58074252db3c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/rhg/src/color.rs	Thu Feb 10 12:59:32 2022 +0100
@@ -0,0 +1,255 @@
+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),
+            }),
+        })
+    }
+}