|
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 } |