common/
help.rs

1use crate::color::spec_color;
2use crate::utils::{into_static_str, str_from_utf8};
3use lazy_static::lazy_static;
4use std::io::{Result, Write};
5use termcolor::{Buffer, Color, WriteColor};
6
7const HEADING_PREFIX: &str = "# ";
8const PADDED_BLOCK_PREFIX: &str = "    ";
9const SHELL_PREFIX: &str = "$>";
10
11const CODE_CHAR: char = '`';
12const COMMENT_CHAR: char = '#';
13
14const PRIMARY_COLOR: Color = Color::Yellow;
15const SECONDARY_COLOR: Color = Color::Cyan;
16const CODE_COLOR: Color = Color::Green;
17
18lazy_static! {
19    static ref COLORED_HELP_ENABLED: bool = atty::is(atty::Stream::Stdout)
20        && std::env::args().any(|arg| arg == "-h" || arg == "--help");
21}
22
23pub fn highlight_static(text: &'static str) -> &'static str {
24    if *COLORED_HELP_ENABLED {
25        highlight_to_string(text).map_or(text, into_static_str)
26    } else {
27        text
28    }
29}
30
31fn highlight_to_string(text: &str) -> Result<String> {
32    let mut buffer = Buffer::ansi();
33    highlight(&mut buffer, text)?;
34    str_from_utf8(buffer.as_slice()).map(String::from)
35}
36
37pub fn highlight<O: Write + WriteColor>(output: &mut O, text: &str) -> Result<()> {
38    for line in text.lines() {
39        if let Some(header) = line.strip_prefix(HEADING_PREFIX) {
40            output.set_color(&spec_color(PRIMARY_COLOR))?;
41            write!(output, "{}", header)?;
42        } else if let Some(block) = line.strip_prefix(PADDED_BLOCK_PREFIX) {
43            write!(output, "{}", PADDED_BLOCK_PREFIX)?;
44
45            if let Some(command) = block.strip_prefix(SHELL_PREFIX) {
46                output.set_color(&spec_color(SECONDARY_COLOR))?;
47                write!(output, "{}", SHELL_PREFIX)?;
48                output.set_color(&spec_color(CODE_COLOR))?;
49
50                if let Some(comment_index) = command.rfind(COMMENT_CHAR) {
51                    write!(output, "{}", &command[..comment_index])?;
52                    output.set_color(&spec_color(SECONDARY_COLOR))?;
53                    write!(output, "{}", &command[comment_index..])?;
54                } else {
55                    write!(output, "{}", command)?;
56                }
57            } else {
58                output.set_color(&spec_color(SECONDARY_COLOR))?;
59                write!(output, "{}", block)?;
60            }
61        } else {
62            highlight_code(output, line)?;
63        }
64
65        output.reset()?;
66        writeln!(output)?;
67    }
68
69    Ok(())
70}
71
72fn highlight_code<O: Write + WriteColor>(output: &mut O, line: &str) -> Result<()> {
73    let mut in_code = false;
74    let mut last_index = 0;
75
76    for (index, char) in line.char_indices() {
77        if char == CODE_CHAR {
78            write!(output, "{}", &line[last_index..index])?;
79            if in_code {
80                output.reset()?;
81            } else {
82                output.set_color(&spec_color(CODE_COLOR))?;
83            }
84            last_index = index + 1;
85            in_code = !in_code;
86        }
87    }
88
89    write!(output, "{}", &line[last_index..])
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::testing::{ColoredOuput, OutputChunk};
96    use claim::*;
97    use indoc::indoc;
98
99    const SAMPLE_HELP: &str = indoc! {"
100        # Heading
101
102        Text.
103        Text with `code`.
104        Text with `code` and `more code`.
105
106            Padded block.
107            Padded block with `code`.
108
109        Text.
110
111            $> ls -la
112            $> ls -la # Shell comment
113    "};
114
115    #[test]
116    fn highlight_to_string() {
117        assert_gt!(super::highlight_to_string(SAMPLE_HELP).unwrap().len(), 0);
118    }
119
120    #[test]
121    fn highlight() {
122        let mut ouput = ColoredOuput::new();
123        super::highlight(&mut ouput, SAMPLE_HELP).unwrap();
124        assert_eq!(
125            ouput.chunks(),
126            &[
127                OutputChunk::color(Color::Yellow, "Heading"),
128                OutputChunk::plain("\n\nText.\nText with "),
129                OutputChunk::color(Color::Green, "code"),
130                OutputChunk::plain(".\nText with "),
131                OutputChunk::color(Color::Green, "code"),
132                OutputChunk::plain(" and "),
133                OutputChunk::color(Color::Green, "more code"),
134                OutputChunk::plain(".\n\n    "),
135                OutputChunk::color(Color::Cyan, "Padded block."),
136                OutputChunk::plain("\n    "),
137                OutputChunk::color(Color::Cyan, "Padded block with `code`."),
138                OutputChunk::plain("\n\nText.\n\n    "),
139                OutputChunk::color(Color::Cyan, "$>"),
140                OutputChunk::color(Color::Green, " ls -la"),
141                OutputChunk::plain("\n    "),
142                OutputChunk::color(Color::Cyan, "$>"),
143                OutputChunk::color(Color::Green, " ls -la "),
144                OutputChunk::color(Color::Cyan, "# Shell comment"),
145                OutputChunk::plain("\n")
146            ]
147        );
148    }
149}