Skip to main content

nu_color_config/
style_computer.rs

1use crate::{TextStyle, color_record_to_nustyle, lookup_ansi_color_style, text_style::Alignment};
2use nu_ansi_term::{Color, Style};
3use nu_engine::ClosureEvalOnce;
4use nu_protocol::{
5    Span, Value,
6    engine::{Closure, EngineState, Stack},
7    report_shell_error,
8};
9use std::{collections::HashMap, fmt::Debug};
10
11// ComputableStyle represents the valid user style types: a single color value, or a closure which
12// takes an input value and produces a color value. The latter represents a value which
13// is computed at use-time.
14#[derive(Debug, Clone)]
15pub enum ComputableStyle {
16    Static(Style),
17    Closure(Closure, Span),
18}
19
20// An alias for the mapping used internally by StyleComputer.
21pub type StyleMapping = HashMap<String, ComputableStyle>;
22
23// A StyleComputer is an all-in-one way to compute styles. A nu command can
24// simply create it with from_config(), and then use it with compute().
25// It stores the engine state and stack needed to run closures that
26// may be defined as a user style.
27
28#[derive(Debug)]
29pub struct StyleComputer<'a> {
30    engine_state: &'a EngineState,
31    stack: &'a Stack,
32    map: StyleMapping,
33}
34
35impl<'a> StyleComputer<'a> {
36    // This is NOT meant to be used in most cases - please use from_config() instead.
37    // This only exists for testing purposes.
38    pub fn new(
39        engine_state: &'a EngineState,
40        stack: &'a Stack,
41        map: StyleMapping,
42    ) -> StyleComputer<'a> {
43        StyleComputer {
44            engine_state,
45            stack,
46            map,
47        }
48    }
49    // The main method. Takes a string name which maps to a color_config style name,
50    // and a Nu value to pipe into any closures that may have been defined there.
51    pub fn compute(&self, style_name: &str, value: &Value) -> Style {
52        match self.map.get(style_name) {
53            // Static values require no computation.
54            Some(ComputableStyle::Static(s)) => *s,
55            // Closures are run here.
56            Some(ComputableStyle::Closure(closure, span)) => {
57                let result = ClosureEvalOnce::new(self.engine_state, self.stack, closure.clone())
58                    .debug(false)
59                    .run_with_value(value.clone())
60                    .and_then(|data| data.into_value(*span));
61
62                match result {
63                    Ok(value) => {
64                        // These should be the same color data forms supported by color_config.
65                        match value {
66                            Value::Record { .. } => color_record_to_nustyle(&value),
67                            Value::String { val, .. } => lookup_ansi_color_style(&val),
68                            _ => Style::default(),
69                        }
70                    }
71                    Err(err) => {
72                        report_shell_error(Some(self.stack), self.engine_state, &err);
73                        Style::default()
74                    }
75                }
76            }
77            // There should be no other kinds of values (due to create_map() in config.rs filtering them out)
78            // so this is just a fallback.
79            _ => Style::default(),
80        }
81    }
82
83    // Used only by the `table` command.
84    pub fn style_primitive(&self, value: &Value) -> TextStyle {
85        use Alignment::*;
86        let s = self.compute(&value.get_type().get_non_specified_string(), value);
87        match *value {
88            Value::Bool { .. } => TextStyle::with_style(Left, s),
89            Value::Int { .. } => TextStyle::with_style(Right, s),
90            Value::Filesize { .. } => TextStyle::with_style(Right, s),
91            Value::Duration { .. } => TextStyle::with_style(Right, s),
92            Value::Date { .. } => TextStyle::with_style(Left, s),
93            Value::Range { .. } => TextStyle::with_style(Left, s),
94            Value::Float { .. } => TextStyle::with_style(Right, s),
95            Value::String { .. } => TextStyle::with_style(Left, s),
96            Value::Glob { .. } => TextStyle::with_style(Left, s),
97            Value::Nothing { .. } => TextStyle::with_style(Left, s),
98            Value::Binary { .. } => TextStyle::with_style(Left, s),
99            Value::CellPath { .. } => TextStyle::with_style(Left, s),
100            Value::Record { .. } | Value::List { .. } => TextStyle::with_style(Left, s),
101            Value::Closure { .. } | Value::Error { .. } => TextStyle::basic_left(),
102            Value::Custom { ref val, .. } => {
103                let type_name = val.type_name();
104                let custom_style = self.compute(&type_name, value);
105                TextStyle::with_style(Left, custom_style)
106            }
107        }
108    }
109
110    // The main constructor.
111    pub fn from_config(engine_state: &'a EngineState, stack: &'a Stack) -> StyleComputer<'a> {
112        let config = stack.get_config(engine_state);
113
114        // Create the hashmap
115        #[rustfmt::skip]
116        let mut map: StyleMapping = [
117            ("separator".to_string(), ComputableStyle::Static(Color::Default.normal())),
118            ("leading_trailing_space_bg".to_string(), ComputableStyle::Static(Style::default().on(Color::Rgb(128, 128, 128)))),
119            ("header".to_string(), ComputableStyle::Static(Color::Green.bold())),
120            ("empty".to_string(), ComputableStyle::Static(Color::Blue.normal())),
121            ("bool".to_string(), ComputableStyle::Static(Color::LightCyan.normal())),
122            ("int".to_string(), ComputableStyle::Static(Color::Default.normal())),
123            ("filesize".to_string(), ComputableStyle::Static(Color::Cyan.normal())),
124            ("duration".to_string(), ComputableStyle::Static(Color::Default.normal())),
125            ("datetime".to_string(), ComputableStyle::Static(Color::Purple.normal())),
126            ("range".to_string(), ComputableStyle::Static(Color::Default.normal())),
127            ("float".to_string(), ComputableStyle::Static(Color::Default.normal())),
128            ("string".to_string(), ComputableStyle::Static(Color::Default.normal())),
129            ("nothing".to_string(), ComputableStyle::Static(Color::Default.normal())),
130            ("binary".to_string(), ComputableStyle::Static(Color::Default.normal())),
131            ("binary_null_char".to_string(), ComputableStyle::Static(Color::Fixed(242).normal())),
132            ("binary_printable".to_string(), ComputableStyle::Static(Color::Cyan.bold())),
133            ("binary_whitespace".to_string(), ComputableStyle::Static(Color::Green.bold())),
134            ("binary_ascii_other".to_string(), ComputableStyle::Static(Color::Purple.bold())),
135            ("binary_non_ascii".to_string(), ComputableStyle::Static(Color::Yellow.bold())),
136            ("cell-path".to_string(), ComputableStyle::Static(Color::Default.normal())),
137            ("row_index".to_string(), ComputableStyle::Static(Color::Green.bold())),
138            ("record".to_string(), ComputableStyle::Static(Color::Default.normal())),
139            ("list".to_string(), ComputableStyle::Static(Color::Default.normal())),
140            ("block".to_string(), ComputableStyle::Static(Color::Default.normal())),
141            ("hints".to_string(), ComputableStyle::Static(Color::DarkGray.normal())),
142            ("search_result".to_string(), ComputableStyle::Static(Color::Default.normal().on(Color::Red))),
143            ("semver".to_string(), ComputableStyle::Static(Color::Cyan.bold())),
144            ("semver-range".to_string(), ComputableStyle::Static(Color::Cyan.bold())),
145        ].into_iter().collect();
146
147        for (key, value) in &config.color_config {
148            let span = value.span();
149            match value {
150                Value::Closure { val, .. } => {
151                    map.insert(
152                        key.to_string(),
153                        ComputableStyle::Closure(*val.clone(), span),
154                    );
155                }
156                Value::Record { .. } => {
157                    map.insert(
158                        key.to_string(),
159                        ComputableStyle::Static(color_record_to_nustyle(value)),
160                    );
161                }
162                Value::String { val, .. } => {
163                    // update the stylemap with the found key
164                    let color = lookup_ansi_color_style(val.as_str());
165                    if let Some(v) = map.get_mut(key) {
166                        *v = ComputableStyle::Static(color);
167                    } else {
168                        map.insert(key.to_string(), ComputableStyle::Static(color));
169                    }
170                }
171                // This should never occur.
172                _ => (),
173            }
174        }
175        StyleComputer::new(engine_state, stack, map)
176    }
177}
178
179#[test]
180fn test_computable_style_static() {
181    use nu_protocol::Span;
182
183    let style1 = Style::default().italic();
184    let style2 = Style::default().underline();
185    // Create a "dummy" style_computer for this test.
186    let dummy_engine_state = EngineState::new();
187    let dummy_stack = Stack::new();
188    let style_computer = StyleComputer::new(
189        &dummy_engine_state,
190        &dummy_stack,
191        [
192            ("string".into(), ComputableStyle::Static(style1)),
193            ("row_index".into(), ComputableStyle::Static(style2)),
194        ]
195        .into_iter()
196        .collect(),
197    );
198    assert_eq!(
199        style_computer.compute("string", &Value::nothing(Span::unknown())),
200        style1
201    );
202    assert_eq!(
203        style_computer.compute("row_index", &Value::nothing(Span::unknown())),
204        style2
205    );
206}
207
208// Because each closure currently runs in a separate environment, checks that the closures have run
209// must use the filesystem.
210#[test]
211fn test_computable_style_closure_basic() {
212    use nu_test_support::{nu, nu_repl_code, playground::Playground};
213    Playground::setup("computable_style_closure_basic", |dirs, _| {
214        let inp = [
215            "$env.config = {
216                color_config: {
217                    string: {|e| touch ($e + '.obj'); 'red' }
218                }
219            };",
220            "[bell book candle] | table | ignore",
221            "ls | get name | to nuon",
222        ];
223        let actual_repl = nu!(cwd: dirs.test(), nu_repl_code(&inp));
224        assert_eq!(actual_repl.err, "");
225        assert_eq!(actual_repl.out, r#"["bell.obj", "book.obj", "candle.obj"]"#);
226    });
227}
228
229#[test]
230fn test_computable_style_closure_errors() {
231    use nu_test_support::{nu, nu_repl_code};
232    let inp = [
233        "$env.config = {
234            color_config: {
235                string: {|e| $e + 2 }
236            }
237        };",
238        "[bell] | table",
239    ];
240    let actual_repl = nu!(nu_repl_code(&inp));
241    // Check that the error was printed
242    assert!(
243        actual_repl
244            .err
245            .contains("nu::shell::operator_incompatible_types")
246    );
247    // Check that the value was printed
248    assert!(actual_repl.out.contains("bell"));
249}