rust/rhg/src/ui.rs
changeset 48733 39c447e03dbc
parent 48731 f591b377375f
child 48734 3e2b4bb286e7
--- a/rust/rhg/src/ui.rs	Thu Feb 10 13:56:43 2022 +0100
+++ b/rust/rhg/src/ui.rs	Thu Feb 10 12:59:32 2022 +0100
@@ -1,4 +1,7 @@
+use crate::color::ColorConfig;
+use crate::color::Effect;
 use format_bytes::format_bytes;
+use format_bytes::write_bytes;
 use hg::config::Config;
 use hg::errors::HgError;
 use hg::utils::files::get_bytes_from_os_string;
@@ -7,10 +10,10 @@
 use std::io;
 use std::io::{ErrorKind, Write};
 
-#[derive(Debug)]
 pub struct Ui {
     stdout: std::io::Stdout,
     stderr: std::io::Stderr,
+    colors: Option<ColorConfig>,
 }
 
 /// The kind of user interface error
@@ -23,20 +26,26 @@
 
 /// The commandline user interface
 impl Ui {
-    pub fn new(_config: &Config) -> Result<Self, HgError> {
+    pub fn new(config: &Config) -> Result<Self, HgError> {
         Ok(Ui {
+            // If using something else, also adapt `isatty()` below.
             stdout: std::io::stdout(),
+
             stderr: std::io::stderr(),
+            colors: ColorConfig::new(config)?,
         })
     }
 
     /// Default to no color if color configuration errors.
     ///
     /// Useful when we’re already handling another error.
-    pub fn new_infallible(_config: &Config) -> Self {
+    pub fn new_infallible(config: &Config) -> Self {
         Ui {
+            // If using something else, also adapt `isatty()` below.
             stdout: std::io::stdout(),
+
             stderr: std::io::stderr(),
+            colors: ColorConfig::new(config).unwrap_or(None),
         }
     }
 
@@ -48,6 +57,11 @@
 
     /// Write bytes to stdout
     pub fn write_stdout(&self, bytes: &[u8]) -> Result<(), UiError> {
+        // Hack to silence "unused" warnings
+        if false {
+            return self.write_stdout_labelled(bytes, "");
+        }
+
         let mut stdout = self.stdout.lock();
 
         stdout.write_all(bytes).or_else(handle_stdout_error)?;
@@ -64,6 +78,61 @@
         stderr.flush().or_else(handle_stderr_error)
     }
 
+    /// Write bytes to stdout with the given label
+    ///
+    /// Like the optional `label` parameter in `mercurial/ui.py`,
+    /// this label influences the color used for this output.
+    pub fn write_stdout_labelled(
+        &self,
+        bytes: &[u8],
+        label: &str,
+    ) -> Result<(), UiError> {
+        if let Some(colors) = &self.colors {
+            if let Some(effects) = colors.styles.get(label.as_bytes()) {
+                if !effects.is_empty() {
+                    return self
+                        .write_stdout_with_effects(bytes, effects)
+                        .or_else(handle_stdout_error);
+                }
+            }
+        }
+        self.write_stdout(bytes)
+    }
+
+    fn write_stdout_with_effects(
+        &self,
+        bytes: &[u8],
+        effects: &[Effect],
+    ) -> io::Result<()> {
+        let stdout = &mut self.stdout.lock();
+        let mut write_line = |line: &[u8], first: bool| {
+            // `line` does not include the newline delimiter
+            if !first {
+                stdout.write_all(b"\n")?;
+            }
+            if line.is_empty() {
+                return Ok(());
+            }
+            /// 0x1B == 27 == 0o33
+            const ASCII_ESCAPE: &[u8] = b"\x1b";
+            write_bytes!(stdout, b"{}[0", ASCII_ESCAPE)?;
+            for effect in effects {
+                write_bytes!(stdout, b";{}", effect)?;
+            }
+            write_bytes!(stdout, b"m")?;
+            stdout.write_all(line)?;
+            write_bytes!(stdout, b"{}[0m", ASCII_ESCAPE)
+        };
+        let mut lines = bytes.split(|&byte| byte == b'\n');
+        if let Some(first) = lines.next() {
+            write_line(first, true)?;
+            for line in lines {
+                write_line(line, false)?
+            }
+        }
+        stdout.flush()
+    }
+
     /// Return whether plain mode is active.
     ///
     /// Plain mode means that all configuration variables which affect
@@ -83,7 +152,7 @@
     }
 }
 
-fn plain(opt_feature: Option<&str>) -> bool {
+pub fn plain(opt_feature: Option<&str>) -> bool {
     if let Some(except) = env::var_os("HGPLAINEXCEPT") {
         opt_feature.map_or(true, |feature| {
             get_bytes_from_os_string(except)
@@ -154,3 +223,23 @@
     let bytes = s.as_bytes();
     Cow::Borrowed(bytes)
 }
+
+/// Should formatted output be used?
+///
+/// Note: rhg does not have the formatter mechanism yet,
+/// but this is also used when deciding whether to use color.
+pub fn formatted(config: &Config) -> Result<bool, HgError> {
+    if let Some(formatted) = config.get_option(b"ui", b"formatted")? {
+        Ok(formatted)
+    } else {
+        isatty(config)
+    }
+}
+
+fn isatty(config: &Config) -> Result<bool, HgError> {
+    Ok(if config.get_bool(b"ui", b"nontty")? {
+        false
+    } else {
+        atty::is(atty::Stream::Stdout)
+    })
+}