nu_explore/
explore.rs

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