nu_explore/
explore.rs

1use crate::{
2    run_pager,
3    util::{create_lscolors, create_map},
4    PagerConfig,
5};
6use nu_ansi_term::{Color, Style};
7use nu_color_config::{get_color_map, StyleComputer};
8use nu_engine::command_prelude::*;
9use nu_protocol::Config;
10
11/// A `less` like program to render a [`Value`] as a table.
12#[derive(Clone)]
13pub struct Explore;
14
15impl Command for Explore {
16    fn name(&self) -> &str {
17        "explore"
18    }
19
20    fn description(&self) -> &str {
21        "Explore acts as a table pager, just like `less` does for text."
22    }
23
24    fn signature(&self) -> nu_protocol::Signature {
25        // todo: Fix error message when it's empty
26        // if we set h i short flags it panics????
27
28        Signature::build("explore")
29            .input_output_types(vec![(Type::Any, Type::Any)])
30            .named(
31                "head",
32                SyntaxShape::Boolean,
33                "Show or hide column headers (default true)",
34                None,
35            )
36            .switch("index", "Show row indexes when viewing a list", Some('i'))
37            .switch(
38                "tail",
39                "Start with the viewport scrolled to the bottom",
40                Some('t'),
41            )
42            .switch(
43                "peek",
44                "When quitting, output the value of the cell the cursor was on",
45                Some('p'),
46            )
47            .category(Category::Viewers)
48    }
49
50    fn extra_description(&self) -> &str {
51        r#"Press `:` then `h` to get a help menu."#
52    }
53
54    fn run(
55        &self,
56        engine_state: &EngineState,
57        stack: &mut Stack,
58        call: &Call,
59        input: PipelineData,
60    ) -> Result<PipelineData, ShellError> {
61        let show_head: bool = call.get_flag(engine_state, stack, "head")?.unwrap_or(true);
62        let show_index: bool = call.has_flag(engine_state, stack, "index")?;
63        let tail: bool = call.has_flag(engine_state, stack, "tail")?;
64        let peek_value: bool = call.has_flag(engine_state, stack, "peek")?;
65
66        let nu_config = stack.get_config(engine_state);
67        let style_computer = StyleComputer::from_config(engine_state, stack);
68
69        let mut explore_config = ExploreConfig::from_nu_config(&nu_config);
70        explore_config.table.show_header = show_head;
71        explore_config.table.show_index = show_index;
72        explore_config.table.separator_style = lookup_color(&style_computer, "separator");
73
74        let lscolors = create_lscolors(engine_state, stack);
75        let cwd = engine_state.cwd(Some(stack)).map_or(String::new(), |path| {
76            path.to_str().unwrap_or("").to_string()
77        });
78
79        let config = PagerConfig::new(
80            &nu_config,
81            &explore_config,
82            &style_computer,
83            &lscolors,
84            peek_value,
85            tail,
86            &cwd,
87        );
88
89        let result = run_pager(engine_state, &mut stack.clone(), input, config);
90
91        match result {
92            Ok(Some(value)) => Ok(PipelineData::Value(value, None)),
93            Ok(None) => Ok(PipelineData::Value(Value::default(), None)),
94            Err(err) => {
95                let shell_error = match err.downcast::<ShellError>() {
96                    Ok(e) => e,
97                    Err(e) => ShellError::GenericError {
98                        error: e.to_string(),
99                        msg: "".into(),
100                        span: None,
101                        help: None,
102                        inner: vec![],
103                    },
104                };
105
106                Ok(PipelineData::Value(
107                    Value::error(shell_error, call.head),
108                    None,
109                ))
110            }
111        }
112    }
113
114    fn examples(&self) -> Vec<Example> {
115        vec![
116            Example {
117                description: "Explore the system host information record",
118                example: r#"sys host | explore"#,
119                result: None,
120            },
121            Example {
122                description: "Explore the output of `ls` without column names",
123                example: r#"ls | explore --head false"#,
124                result: None,
125            },
126            Example {
127                description: "Explore a list of Markdown files' contents, with row indexes",
128                example: r#"glob *.md | each {|| open } | explore --index"#,
129                result: None,
130            },
131            Example {
132                description:
133                    "Explore a JSON file, then save the last visited sub-structure to a file",
134                example: r#"open file.json | explore --peek | to json | save part.json"#,
135                result: None,
136            },
137        ]
138    }
139}
140
141#[derive(Debug, Clone)]
142pub struct ExploreConfig {
143    pub table: TableConfig,
144    pub selected_cell: Style,
145    pub status_info: Style,
146    pub status_success: Style,
147    pub status_warn: Style,
148    pub status_error: Style,
149    pub status_bar_background: Style,
150    pub status_bar_text: Style,
151    pub cmd_bar_text: Style,
152    pub cmd_bar_background: Style,
153    pub highlight: Style,
154    /// if true, the explore view will immediately try to run the command as it is typed
155    pub try_reactive: bool,
156}
157
158impl Default for ExploreConfig {
159    fn default() -> Self {
160        Self {
161            table: TableConfig::default(),
162            selected_cell: color(None, Some(Color::LightBlue)),
163            status_info: color(None, None),
164            status_success: color(Some(Color::Black), Some(Color::Green)),
165            status_warn: color(None, None),
166            status_error: color(Some(Color::White), Some(Color::Red)),
167            status_bar_background: color(
168                Some(Color::Rgb(29, 31, 33)),
169                Some(Color::Rgb(196, 201, 198)),
170            ),
171            status_bar_text: color(None, None),
172            cmd_bar_text: color(Some(Color::Rgb(196, 201, 198)), None),
173            cmd_bar_background: color(None, None),
174            highlight: color(Some(Color::Black), Some(Color::Yellow)),
175            try_reactive: false,
176        }
177    }
178}
179impl ExploreConfig {
180    /// take the default explore config and update it with relevant values from the nu config
181    pub fn from_nu_config(config: &Config) -> Self {
182        let mut ret = Self::default();
183
184        ret.table.column_padding_left = config.table.padding.left;
185        ret.table.column_padding_right = config.table.padding.right;
186
187        let explore_cfg_hash_map = config.explore.clone();
188        let colors = get_color_map(&explore_cfg_hash_map);
189
190        if let Some(s) = colors.get("status_bar_text") {
191            ret.status_bar_text = *s;
192        }
193
194        if let Some(s) = colors.get("status_bar_background") {
195            ret.status_bar_background = *s;
196        }
197
198        if let Some(s) = colors.get("command_bar_text") {
199            ret.cmd_bar_text = *s;
200        }
201
202        if let Some(s) = colors.get("command_bar_background") {
203            ret.cmd_bar_background = *s;
204        }
205
206        if let Some(s) = colors.get("command_bar_background") {
207            ret.cmd_bar_background = *s;
208        }
209
210        if let Some(s) = colors.get("selected_cell") {
211            ret.selected_cell = *s;
212        }
213
214        if let Some(hm) = explore_cfg_hash_map.get("status").and_then(create_map) {
215            let colors = get_color_map(&hm);
216
217            if let Some(s) = colors.get("info") {
218                ret.status_info = *s;
219            }
220
221            if let Some(s) = colors.get("success") {
222                ret.status_success = *s;
223            }
224
225            if let Some(s) = colors.get("warn") {
226                ret.status_warn = *s;
227            }
228
229            if let Some(s) = colors.get("error") {
230                ret.status_error = *s;
231            }
232        }
233
234        if let Some(hm) = explore_cfg_hash_map.get("try").and_then(create_map) {
235            if let Some(reactive) = hm.get("reactive") {
236                if let Ok(b) = reactive.as_bool() {
237                    ret.try_reactive = b;
238                }
239            }
240        }
241
242        ret
243    }
244}
245
246#[derive(Debug, Default, Clone, Copy)]
247pub struct TableConfig {
248    pub separator_style: Style,
249    pub show_index: bool,
250    pub show_header: bool,
251    pub column_padding_left: usize,
252    pub column_padding_right: usize,
253}
254
255const fn color(foreground: Option<Color>, background: Option<Color>) -> Style {
256    Style {
257        background,
258        foreground,
259        is_blink: false,
260        is_bold: false,
261        is_dimmed: false,
262        is_hidden: false,
263        is_italic: false,
264        is_reverse: false,
265        is_strikethrough: false,
266        is_underline: false,
267        prefix_with_reset: false,
268    }
269}
270
271fn lookup_color(style_computer: &StyleComputer, key: &str) -> nu_ansi_term::Style {
272    style_computer.compute(key, &Value::nothing(Span::unknown()))
273}