##// END OF EJS Templates
rhg: Add support for colored output...
Simon Sapin -
r49584:39c447e0 default
parent child Browse files
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 name = "rhg"
876 name = "rhg"
877 version = "0.1.0"
877 version = "0.1.0"
878 dependencies = [
878 dependencies = [
879 "atty",
879 "chrono",
880 "chrono",
880 "clap",
881 "clap",
881 "derive_more",
882 "derive_more",
@@ -13,4 +13,4 b' mod config;'
13 mod layer;
13 mod layer;
14 mod values;
14 mod values;
15 pub use config::{Config, ConfigSource, ConfigValueParseError};
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 .map(|(_, value)| value.bytes.as_ref())
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 /// Returns the layer and the value of the first one found, or `None`.
411 /// Returns the layer and the value of the first one found, or `None`.
402 fn get_inner(
412 fn get_inner(
403 &self,
413 &self,
@@ -295,7 +295,7 b' pub struct ConfigValue {'
295 pub line: Option<usize>,
295 pub line: Option<usize>,
296 }
296 }
297
297
298 #[derive(Clone, Debug)]
298 #[derive(Clone, Debug, PartialEq, Eq)]
299 pub enum ConfigOrigin {
299 pub enum ConfigOrigin {
300 /// From a configuration file
300 /// From a configuration file
301 File(PathBuf),
301 File(PathBuf),
@@ -8,6 +8,7 b' authors = ['
8 edition = "2018"
8 edition = "2018"
9
9
10 [dependencies]
10 [dependencies]
11 atty = "0.2"
11 hg-core = { path = "../hg-core"}
12 hg-core = { path = "../hg-core"}
12 chrono = "0.4.19"
13 chrono = "0.4.19"
13 clap = "2.33.1"
14 clap = "2.33.1"
@@ -17,6 +17,7 b' use std::path::PathBuf;'
17 use std::process::Command;
17 use std::process::Command;
18
18
19 mod blackbox;
19 mod blackbox;
20 mod color;
20 mod error;
21 mod error;
21 mod ui;
22 mod ui;
22 pub mod utils {
23 pub mod utils {
@@ -1,4 +1,7 b''
1 use crate::color::ColorConfig;
2 use crate::color::Effect;
1 use format_bytes::format_bytes;
3 use format_bytes::format_bytes;
4 use format_bytes::write_bytes;
2 use hg::config::Config;
5 use hg::config::Config;
3 use hg::errors::HgError;
6 use hg::errors::HgError;
4 use hg::utils::files::get_bytes_from_os_string;
7 use hg::utils::files::get_bytes_from_os_string;
@@ -7,10 +10,10 b' use std::env;'
7 use std::io;
10 use std::io;
8 use std::io::{ErrorKind, Write};
11 use std::io::{ErrorKind, Write};
9
12
10 #[derive(Debug)]
11 pub struct Ui {
13 pub struct Ui {
12 stdout: std::io::Stdout,
14 stdout: std::io::Stdout,
13 stderr: std::io::Stderr,
15 stderr: std::io::Stderr,
16 colors: Option<ColorConfig>,
14 }
17 }
15
18
16 /// The kind of user interface error
19 /// The kind of user interface error
@@ -23,20 +26,26 b' pub enum UiError {'
23
26
24 /// The commandline user interface
27 /// The commandline user interface
25 impl Ui {
28 impl Ui {
26 pub fn new(_config: &Config) -> Result<Self, HgError> {
29 pub fn new(config: &Config) -> Result<Self, HgError> {
27 Ok(Ui {
30 Ok(Ui {
31 // If using something else, also adapt `isatty()` below.
28 stdout: std::io::stdout(),
32 stdout: std::io::stdout(),
33
29 stderr: std::io::stderr(),
34 stderr: std::io::stderr(),
35 colors: ColorConfig::new(config)?,
30 })
36 })
31 }
37 }
32
38
33 /// Default to no color if color configuration errors.
39 /// Default to no color if color configuration errors.
34 ///
40 ///
35 /// Useful when we’re already handling another error.
41 /// Useful when we’re already handling another error.
36 pub fn new_infallible(_config: &Config) -> Self {
42 pub fn new_infallible(config: &Config) -> Self {
37 Ui {
43 Ui {
44 // If using something else, also adapt `isatty()` below.
38 stdout: std::io::stdout(),
45 stdout: std::io::stdout(),
46
39 stderr: std::io::stderr(),
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 /// Write bytes to stdout
58 /// Write bytes to stdout
50 pub fn write_stdout(&self, bytes: &[u8]) -> Result<(), UiError> {
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 let mut stdout = self.stdout.lock();
65 let mut stdout = self.stdout.lock();
52
66
53 stdout.write_all(bytes).or_else(handle_stdout_error)?;
67 stdout.write_all(bytes).or_else(handle_stdout_error)?;
@@ -64,6 +78,61 b' impl Ui {'
64 stderr.flush().or_else(handle_stderr_error)
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 /// Return whether plain mode is active.
136 /// Return whether plain mode is active.
68 ///
137 ///
69 /// Plain mode means that all configuration variables which affect
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 if let Some(except) = env::var_os("HGPLAINEXCEPT") {
156 if let Some(except) = env::var_os("HGPLAINEXCEPT") {
88 opt_feature.map_or(true, |feature| {
157 opt_feature.map_or(true, |feature| {
89 get_bytes_from_os_string(except)
158 get_bytes_from_os_string(except)
@@ -154,3 +223,23 b' pub fn utf8_to_local(s: &str) -> Cow<[u8'
154 let bytes = s.as_bytes();
223 let bytes = s.as_bytes();
155 Cow::Borrowed(bytes)
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 ignoring unknown color/effect 'periwinkle' (configured in color.status.modified)
313 ignoring unknown color/effect 'periwinkle' (configured in color.status.modified)
314 ignoring unknown color/effect 'periwinkle' (configured in color.status.modified)
314 ignoring unknown color/effect 'periwinkle' (configured in color.status.modified)
315 ignoring unknown color/effect 'periwinkle' (configured in color.status.modified)
315 ignoring unknown color/effect 'periwinkle' (configured in color.status.modified)
316 ignoring unknown color/effect 'periwinkle' (configured in color.status.modified) (rhg !)
316 M modified
317 M modified
317 \x1b[0;32;1mA \x1b[0m\x1b[0;32;1madded\x1b[0m (esc)
318 \x1b[0;32;1mA \x1b[0m\x1b[0;32;1madded\x1b[0m (esc)
318 \x1b[0;32;1mA \x1b[0m\x1b[0;32;1mcopied\x1b[0m (esc)
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