rust/rhg/src/color.rs
changeset 48733 39c447e03dbc
child 49914 58074252db3c
equal deleted inserted replaced
48732:d4a5c2197208 48733:39c447e03dbc
       
     1 use crate::ui::formatted;
       
     2 use crate::ui::plain;
       
     3 use format_bytes::write_bytes;
       
     4 use hg::config::Config;
       
     5 use hg::config::ConfigOrigin;
       
     6 use hg::errors::HgError;
       
     7 use std::collections::HashMap;
       
     8 
       
     9 pub type Effect = u32;
       
    10 
       
    11 pub type EffectsMap = HashMap<Vec<u8>, Vec<Effect>>;
       
    12 
       
    13 macro_rules! effects {
       
    14     ($( $name: ident: $value: expr ,)+) => {
       
    15 
       
    16         #[allow(non_upper_case_globals)]
       
    17         mod effects {
       
    18             $(
       
    19                 pub const $name: super::Effect = $value;
       
    20             )+
       
    21         }
       
    22 
       
    23         fn effect(name: &[u8]) -> Option<Effect> {
       
    24             $(
       
    25                 if name == stringify!($name).as_bytes() {
       
    26                     Some(effects::$name)
       
    27                 } else
       
    28             )+
       
    29             {
       
    30                 None
       
    31             }
       
    32         }
       
    33     };
       
    34 }
       
    35 
       
    36 effects! {
       
    37     none: 0,
       
    38     black: 30,
       
    39     red: 31,
       
    40     green: 32,
       
    41     yellow: 33,
       
    42     blue: 34,
       
    43     magenta: 35,
       
    44     cyan: 36,
       
    45     white: 37,
       
    46     bold: 1,
       
    47     italic: 3,
       
    48     underline: 4,
       
    49     inverse: 7,
       
    50     dim: 2,
       
    51     black_background: 40,
       
    52     red_background: 41,
       
    53     green_background: 42,
       
    54     yellow_background: 43,
       
    55     blue_background: 44,
       
    56     purple_background: 45,
       
    57     cyan_background: 46,
       
    58     white_background: 47,
       
    59 }
       
    60 
       
    61 macro_rules! default_styles {
       
    62     ($( $key: expr => [$($value: expr),*],)+) => {
       
    63         fn default_styles() -> EffectsMap {
       
    64             use effects::*;
       
    65             let mut map = HashMap::new();
       
    66             $(
       
    67                 map.insert($key[..].to_owned(), vec![$( $value ),*]);
       
    68             )+
       
    69             map
       
    70         }
       
    71     };
       
    72 }
       
    73 
       
    74 default_styles! {
       
    75     b"grep.match" => [red, bold],
       
    76     b"grep.linenumber" => [green],
       
    77     b"grep.rev" => [blue],
       
    78     b"grep.sep" => [cyan],
       
    79     b"grep.filename" => [magenta],
       
    80     b"grep.user" => [magenta],
       
    81     b"grep.date" => [magenta],
       
    82     b"grep.inserted" => [green, bold],
       
    83     b"grep.deleted" => [red, bold],
       
    84     b"bookmarks.active" => [green],
       
    85     b"branches.active" => [none],
       
    86     b"branches.closed" => [black, bold],
       
    87     b"branches.current" => [green],
       
    88     b"branches.inactive" => [none],
       
    89     b"diff.changed" => [white],
       
    90     b"diff.deleted" => [red],
       
    91     b"diff.deleted.changed" => [red, bold, underline],
       
    92     b"diff.deleted.unchanged" => [red],
       
    93     b"diff.diffline" => [bold],
       
    94     b"diff.extended" => [cyan, bold],
       
    95     b"diff.file_a" => [red, bold],
       
    96     b"diff.file_b" => [green, bold],
       
    97     b"diff.hunk" => [magenta],
       
    98     b"diff.inserted" => [green],
       
    99     b"diff.inserted.changed" => [green, bold, underline],
       
   100     b"diff.inserted.unchanged" => [green],
       
   101     b"diff.tab" => [],
       
   102     b"diff.trailingwhitespace" => [bold, red_background],
       
   103     b"changeset.public" => [],
       
   104     b"changeset.draft" => [],
       
   105     b"changeset.secret" => [],
       
   106     b"diffstat.deleted" => [red],
       
   107     b"diffstat.inserted" => [green],
       
   108     b"formatvariant.name.mismatchconfig" => [red],
       
   109     b"formatvariant.name.mismatchdefault" => [yellow],
       
   110     b"formatvariant.name.uptodate" => [green],
       
   111     b"formatvariant.repo.mismatchconfig" => [red],
       
   112     b"formatvariant.repo.mismatchdefault" => [yellow],
       
   113     b"formatvariant.repo.uptodate" => [green],
       
   114     b"formatvariant.config.special" => [yellow],
       
   115     b"formatvariant.config.default" => [green],
       
   116     b"formatvariant.default" => [],
       
   117     b"histedit.remaining" => [red, bold],
       
   118     b"ui.addremove.added" => [green],
       
   119     b"ui.addremove.removed" => [red],
       
   120     b"ui.error" => [red],
       
   121     b"ui.prompt" => [yellow],
       
   122     b"log.changeset" => [yellow],
       
   123     b"patchbomb.finalsummary" => [],
       
   124     b"patchbomb.from" => [magenta],
       
   125     b"patchbomb.to" => [cyan],
       
   126     b"patchbomb.subject" => [green],
       
   127     b"patchbomb.diffstats" => [],
       
   128     b"rebase.rebased" => [blue],
       
   129     b"rebase.remaining" => [red, bold],
       
   130     b"resolve.resolved" => [green, bold],
       
   131     b"resolve.unresolved" => [red, bold],
       
   132     b"shelve.age" => [cyan],
       
   133     b"shelve.newest" => [green, bold],
       
   134     b"shelve.name" => [blue, bold],
       
   135     b"status.added" => [green, bold],
       
   136     b"status.clean" => [none],
       
   137     b"status.copied" => [none],
       
   138     b"status.deleted" => [cyan, bold, underline],
       
   139     b"status.ignored" => [black, bold],
       
   140     b"status.modified" => [blue, bold],
       
   141     b"status.removed" => [red, bold],
       
   142     b"status.unknown" => [magenta, bold, underline],
       
   143     b"tags.normal" => [green],
       
   144     b"tags.local" => [black, bold],
       
   145     b"upgrade-repo.requirement.preserved" => [cyan],
       
   146     b"upgrade-repo.requirement.added" => [green],
       
   147     b"upgrade-repo.requirement.removed" => [red],
       
   148 }
       
   149 
       
   150 fn parse_effect(config_key: &[u8], effect_name: &[u8]) -> Option<Effect> {
       
   151     let found = effect(effect_name);
       
   152     if found.is_none() {
       
   153         // TODO: have some API for warnings
       
   154         // TODO: handle IO errors during warnings
       
   155         let stderr = std::io::stderr();
       
   156         let _ = write_bytes!(
       
   157             &mut stderr.lock(),
       
   158             b"ignoring unknown color/effect '{}' \
       
   159               (configured in color.{})\n",
       
   160             effect_name,
       
   161             config_key,
       
   162         );
       
   163     }
       
   164     found
       
   165 }
       
   166 
       
   167 fn effects_from_config(config: &Config) -> EffectsMap {
       
   168     let mut styles = default_styles();
       
   169     for (key, _value) in config.iter_section(b"color") {
       
   170         if !key.contains(&b'.')
       
   171             || key.starts_with(b"color.")
       
   172             || key.starts_with(b"terminfo.")
       
   173         {
       
   174             continue;
       
   175         }
       
   176         // `unwrap` shouldn’t panic since we just got this key from
       
   177         // iteration
       
   178         let list = config.get_list(b"color", key).unwrap();
       
   179         let parsed = list
       
   180             .iter()
       
   181             .filter_map(|name| parse_effect(key, name))
       
   182             .collect();
       
   183         styles.insert(key.to_owned(), parsed);
       
   184     }
       
   185     styles
       
   186 }
       
   187 
       
   188 enum ColorMode {
       
   189     // TODO: support other modes
       
   190     Ansi,
       
   191 }
       
   192 
       
   193 impl ColorMode {
       
   194     // Similar to _modesetup in mercurial/color.py
       
   195     fn get(config: &Config) -> Result<Option<Self>, HgError> {
       
   196         if plain(Some("color")) {
       
   197             return Ok(None);
       
   198         }
       
   199         let enabled_default = b"auto";
       
   200         // `origin` is only used when `!auto`, so its default doesn’t matter
       
   201         let (enabled, origin) = config
       
   202             .get_with_origin(b"ui", b"color")
       
   203             .unwrap_or((enabled_default, &ConfigOrigin::CommandLineColor));
       
   204         if enabled == b"debug" {
       
   205             return Err(HgError::unsupported("debug color mode"));
       
   206         }
       
   207         let auto = enabled == b"auto";
       
   208         let always;
       
   209         if !auto {
       
   210             let enabled_bool = config.get_bool(b"ui", b"color")?;
       
   211             if !enabled_bool {
       
   212                 return Ok(None);
       
   213             }
       
   214             always = enabled == b"always"
       
   215                 || *origin == ConfigOrigin::CommandLineColor
       
   216         } else {
       
   217             always = false
       
   218         };
       
   219         let formatted = always
       
   220             || (std::env::var_os("TERM").unwrap_or_default() != "dumb"
       
   221                 && formatted(config)?);
       
   222 
       
   223         let mode_default = b"auto";
       
   224         let mode = config.get(b"color", b"mode").unwrap_or(mode_default);
       
   225 
       
   226         if formatted {
       
   227             match mode {
       
   228                 b"ansi" | b"auto" => Ok(Some(ColorMode::Ansi)),
       
   229                 // TODO: support other modes
       
   230                 _ => Err(HgError::UnsupportedFeature(format!(
       
   231                     "color mode {}",
       
   232                     String::from_utf8_lossy(mode)
       
   233                 ))),
       
   234             }
       
   235         } else {
       
   236             Ok(None)
       
   237         }
       
   238     }
       
   239 }
       
   240 
       
   241 pub struct ColorConfig {
       
   242     pub styles: EffectsMap,
       
   243 }
       
   244 
       
   245 impl ColorConfig {
       
   246     // Similar to _modesetup in mercurial/color.py
       
   247     pub fn new(config: &Config) -> Result<Option<Self>, HgError> {
       
   248         Ok(match ColorMode::get(config)? {
       
   249             None => None,
       
   250             Some(ColorMode::Ansi) => Some(ColorConfig {
       
   251                 styles: effects_from_config(config),
       
   252             }),
       
   253         })
       
   254     }
       
   255 }