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>; 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 { $( 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 { 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, 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, HgError> { Ok(match ColorMode::get(config)? { None => None, Some(ColorMode::Ansi) => Some(ColorConfig { styles: effects_from_config(config), }), }) } }