Show More
@@ -0,0 +1,255 b'' | |||
|
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 | } |
@@ -876,6 +876,7 b' dependencies = [' | |||
|
876 | 876 | name = "rhg" |
|
877 | 877 | version = "0.1.0" |
|
878 | 878 | dependencies = [ |
|
879 | "atty", | |
|
879 | 880 | "chrono", |
|
880 | 881 | "clap", |
|
881 | 882 | "derive_more", |
@@ -13,4 +13,4 b' mod config;' | |||
|
13 | 13 | mod layer; |
|
14 | 14 | mod values; |
|
15 | 15 | pub use config::{Config, ConfigSource, ConfigValueParseError}; |
|
16 | pub use layer::{ConfigError, ConfigParseError}; | |
|
16 | pub use layer::{ConfigError, ConfigOrigin, ConfigParseError}; |
@@ -398,6 +398,16 b' impl Config {' | |||
|
398 | 398 | .map(|(_, value)| value.bytes.as_ref()) |
|
399 | 399 | } |
|
400 | 400 | |
|
401 | /// Returns the raw value bytes of the first one found, or `None`. | |
|
402 | pub fn get_with_origin( | |
|
403 | &self, | |
|
404 | section: &[u8], | |
|
405 | item: &[u8], | |
|
406 | ) -> Option<(&[u8], &ConfigOrigin)> { | |
|
407 | self.get_inner(section, item) | |
|
408 | .map(|(layer, value)| (value.bytes.as_ref(), &layer.origin)) | |
|
409 | } | |
|
410 | ||
|
401 | 411 | /// Returns the layer and the value of the first one found, or `None`. |
|
402 | 412 | fn get_inner( |
|
403 | 413 | &self, |
@@ -295,7 +295,7 b' pub struct ConfigValue {' | |||
|
295 | 295 | pub line: Option<usize>, |
|
296 | 296 | } |
|
297 | 297 | |
|
298 | #[derive(Clone, Debug)] | |
|
298 | #[derive(Clone, Debug, PartialEq, Eq)] | |
|
299 | 299 | pub enum ConfigOrigin { |
|
300 | 300 | /// From a configuration file |
|
301 | 301 | File(PathBuf), |
@@ -8,6 +8,7 b' authors = [' | |||
|
8 | 8 | edition = "2018" |
|
9 | 9 | |
|
10 | 10 | [dependencies] |
|
11 | atty = "0.2" | |
|
11 | 12 | hg-core = { path = "../hg-core"} |
|
12 | 13 | chrono = "0.4.19" |
|
13 | 14 | clap = "2.33.1" |
@@ -17,6 +17,7 b' use std::path::PathBuf;' | |||
|
17 | 17 | use std::process::Command; |
|
18 | 18 | |
|
19 | 19 | mod blackbox; |
|
20 | mod color; | |
|
20 | 21 | mod error; |
|
21 | 22 | mod ui; |
|
22 | 23 | pub mod utils { |
@@ -1,4 +1,7 b'' | |||
|
1 | use crate::color::ColorConfig; | |
|
2 | use crate::color::Effect; | |
|
1 | 3 | use format_bytes::format_bytes; |
|
4 | use format_bytes::write_bytes; | |
|
2 | 5 | use hg::config::Config; |
|
3 | 6 | use hg::errors::HgError; |
|
4 | 7 | use hg::utils::files::get_bytes_from_os_string; |
@@ -7,10 +10,10 b' use std::env;' | |||
|
7 | 10 | use std::io; |
|
8 | 11 | use std::io::{ErrorKind, Write}; |
|
9 | 12 | |
|
10 | #[derive(Debug)] | |
|
11 | 13 | pub struct Ui { |
|
12 | 14 | stdout: std::io::Stdout, |
|
13 | 15 | stderr: std::io::Stderr, |
|
16 | colors: Option<ColorConfig>, | |
|
14 | 17 | } |
|
15 | 18 | |
|
16 | 19 | /// The kind of user interface error |
@@ -23,20 +26,26 b' pub enum UiError {' | |||
|
23 | 26 | |
|
24 | 27 | /// The commandline user interface |
|
25 | 28 | impl Ui { |
|
26 |
pub fn new( |
|
|
29 | pub fn new(config: &Config) -> Result<Self, HgError> { | |
|
27 | 30 | Ok(Ui { |
|
31 | // If using something else, also adapt `isatty()` below. | |
|
28 | 32 | stdout: std::io::stdout(), |
|
33 | ||
|
29 | 34 | stderr: std::io::stderr(), |
|
35 | colors: ColorConfig::new(config)?, | |
|
30 | 36 | }) |
|
31 | 37 | } |
|
32 | 38 | |
|
33 | 39 | /// Default to no color if color configuration errors. |
|
34 | 40 | /// |
|
35 | 41 | /// Useful when we’re already handling another error. |
|
36 |
pub fn new_infallible( |
|
|
42 | pub fn new_infallible(config: &Config) -> Self { | |
|
37 | 43 | Ui { |
|
44 | // If using something else, also adapt `isatty()` below. | |
|
38 | 45 | stdout: std::io::stdout(), |
|
46 | ||
|
39 | 47 | stderr: std::io::stderr(), |
|
48 | colors: ColorConfig::new(config).unwrap_or(None), | |
|
40 | 49 | } |
|
41 | 50 | } |
|
42 | 51 | |
@@ -48,6 +57,11 b' impl Ui {' | |||
|
48 | 57 | |
|
49 | 58 | /// Write bytes to stdout |
|
50 | 59 | pub fn write_stdout(&self, bytes: &[u8]) -> Result<(), UiError> { |
|
60 | // Hack to silence "unused" warnings | |
|
61 | if false { | |
|
62 | return self.write_stdout_labelled(bytes, ""); | |
|
63 | } | |
|
64 | ||
|
51 | 65 | let mut stdout = self.stdout.lock(); |
|
52 | 66 | |
|
53 | 67 | stdout.write_all(bytes).or_else(handle_stdout_error)?; |
@@ -64,6 +78,61 b' impl Ui {' | |||
|
64 | 78 | stderr.flush().or_else(handle_stderr_error) |
|
65 | 79 | } |
|
66 | 80 | |
|
81 | /// Write bytes to stdout with the given label | |
|
82 | /// | |
|
83 | /// Like the optional `label` parameter in `mercurial/ui.py`, | |
|
84 | /// this label influences the color used for this output. | |
|
85 | pub fn write_stdout_labelled( | |
|
86 | &self, | |
|
87 | bytes: &[u8], | |
|
88 | label: &str, | |
|
89 | ) -> Result<(), UiError> { | |
|
90 | if let Some(colors) = &self.colors { | |
|
91 | if let Some(effects) = colors.styles.get(label.as_bytes()) { | |
|
92 | if !effects.is_empty() { | |
|
93 | return self | |
|
94 | .write_stdout_with_effects(bytes, effects) | |
|
95 | .or_else(handle_stdout_error); | |
|
96 | } | |
|
97 | } | |
|
98 | } | |
|
99 | self.write_stdout(bytes) | |
|
100 | } | |
|
101 | ||
|
102 | fn write_stdout_with_effects( | |
|
103 | &self, | |
|
104 | bytes: &[u8], | |
|
105 | effects: &[Effect], | |
|
106 | ) -> io::Result<()> { | |
|
107 | let stdout = &mut self.stdout.lock(); | |
|
108 | let mut write_line = |line: &[u8], first: bool| { | |
|
109 | // `line` does not include the newline delimiter | |
|
110 | if !first { | |
|
111 | stdout.write_all(b"\n")?; | |
|
112 | } | |
|
113 | if line.is_empty() { | |
|
114 | return Ok(()); | |
|
115 | } | |
|
116 | /// 0x1B == 27 == 0o33 | |
|
117 | const ASCII_ESCAPE: &[u8] = b"\x1b"; | |
|
118 | write_bytes!(stdout, b"{}[0", ASCII_ESCAPE)?; | |
|
119 | for effect in effects { | |
|
120 | write_bytes!(stdout, b";{}", effect)?; | |
|
121 | } | |
|
122 | write_bytes!(stdout, b"m")?; | |
|
123 | stdout.write_all(line)?; | |
|
124 | write_bytes!(stdout, b"{}[0m", ASCII_ESCAPE) | |
|
125 | }; | |
|
126 | let mut lines = bytes.split(|&byte| byte == b'\n'); | |
|
127 | if let Some(first) = lines.next() { | |
|
128 | write_line(first, true)?; | |
|
129 | for line in lines { | |
|
130 | write_line(line, false)? | |
|
131 | } | |
|
132 | } | |
|
133 | stdout.flush() | |
|
134 | } | |
|
135 | ||
|
67 | 136 | /// Return whether plain mode is active. |
|
68 | 137 | /// |
|
69 | 138 | /// Plain mode means that all configuration variables which affect |
@@ -83,7 +152,7 b' impl Ui {' | |||
|
83 | 152 | } |
|
84 | 153 | } |
|
85 | 154 | |
|
86 | fn plain(opt_feature: Option<&str>) -> bool { | |
|
155 | pub fn plain(opt_feature: Option<&str>) -> bool { | |
|
87 | 156 | if let Some(except) = env::var_os("HGPLAINEXCEPT") { |
|
88 | 157 | opt_feature.map_or(true, |feature| { |
|
89 | 158 | get_bytes_from_os_string(except) |
@@ -154,3 +223,23 b' pub fn utf8_to_local(s: &str) -> Cow<[u8' | |||
|
154 | 223 | let bytes = s.as_bytes(); |
|
155 | 224 | Cow::Borrowed(bytes) |
|
156 | 225 | } |
|
226 | ||
|
227 | /// Should formatted output be used? | |
|
228 | /// | |
|
229 | /// Note: rhg does not have the formatter mechanism yet, | |
|
230 | /// but this is also used when deciding whether to use color. | |
|
231 | pub fn formatted(config: &Config) -> Result<bool, HgError> { | |
|
232 | if let Some(formatted) = config.get_option(b"ui", b"formatted")? { | |
|
233 | Ok(formatted) | |
|
234 | } else { | |
|
235 | isatty(config) | |
|
236 | } | |
|
237 | } | |
|
238 | ||
|
239 | fn isatty(config: &Config) -> Result<bool, HgError> { | |
|
240 | Ok(if config.get_bool(b"ui", b"nontty")? { | |
|
241 | false | |
|
242 | } else { | |
|
243 | atty::is(atty::Stream::Stdout) | |
|
244 | }) | |
|
245 | } |
@@ -313,6 +313,7 b' test unknown color' | |||
|
313 | 313 | ignoring unknown color/effect 'periwinkle' (configured in color.status.modified) |
|
314 | 314 | ignoring unknown color/effect 'periwinkle' (configured in color.status.modified) |
|
315 | 315 | ignoring unknown color/effect 'periwinkle' (configured in color.status.modified) |
|
316 | ignoring unknown color/effect 'periwinkle' (configured in color.status.modified) (rhg !) | |
|
316 | 317 | M modified |
|
317 | 318 | \x1b[0;32;1mA \x1b[0m\x1b[0;32;1madded\x1b[0m (esc) |
|
318 | 319 | \x1b[0;32;1mA \x1b[0m\x1b[0;32;1mcopied\x1b[0m (esc) |
General Comments 0
You need to be logged in to leave comments.
Login now