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#[derive(Debug, Clone)]
15pub enum ComputableStyle {
16 Static(Style),
17 Closure(Closure, Span),
18}
19
20pub type StyleMapping = HashMap<String, ComputableStyle>;
22
23#[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 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 pub fn compute(&self, style_name: &str, value: &Value) -> Style {
52 match self.map.get(style_name) {
53 Some(ComputableStyle::Static(s)) => *s,
55 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 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 _ => Style::default(),
80 }
81 }
82
83 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 pub fn from_config(engine_state: &'a EngineState, stack: &'a Stack) -> StyleComputer<'a> {
112 let config = stack.get_config(engine_state);
113
114 #[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 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 _ => (),
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 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#[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 assert!(
243 actual_repl
244 .err
245 .contains("nu::shell::operator_incompatible_types")
246 );
247 assert!(actual_repl.out.contains("bell"));
249}