Skip to main content

nu_protocol/config/
mod.rs

1//! Module containing the internal representation of user configuration
2
3use crate::FromValue;
4use crate::{self as nu_protocol, Filesize};
5use helper::*;
6use prelude::*;
7use std::collections::HashMap;
8
9pub use ansi_coloring::UseAnsiColoring;
10pub use clip::ClipConfig;
11pub use completions::{
12    CompletionAlgorithm, CompletionConfig, CompletionSort, ExternalCompleterConfig,
13};
14pub use datetime_format::DatetimeFormatConfig;
15pub use defaults::default_color_config;
16pub use display_errors::DisplayErrors;
17pub use duration_max_unit::DurationMaxUnit;
18pub use filesize::FilesizeConfig;
19pub use helper::extract_value;
20pub use hinter::HinterConfig;
21pub use history::{HistoryConfig, HistoryFileFormat, HistoryPath};
22pub use hooks::Hooks;
23pub use ls::LsConfig;
24pub use output::{BannerKind, ErrorStyle};
25pub use plugin_gc::{PluginGcConfig, PluginGcConfigs};
26pub use reedline::{CursorShapeConfig, EditBindings, NuCursorShape, ParsedKeybinding, ParsedMenu};
27pub use rm::RmConfig;
28pub use shell_integration::ShellIntegrationConfig;
29pub use table::{FooterMode, TableConfig, TableIndent, TableIndexMode, TableMode, TrimStrategy};
30
31mod ansi_coloring;
32mod clip;
33mod completions;
34mod datetime_format;
35mod defaults;
36mod display_errors;
37mod duration_max_unit;
38mod error;
39mod filesize;
40mod helper;
41mod hinter;
42mod history;
43mod hooks;
44mod ls;
45mod output;
46mod plugin_gc;
47mod prelude;
48mod reedline;
49mod rm;
50mod shell_integration;
51mod table;
52
53#[derive(Clone, Debug, IntoValue, Serialize, Deserialize)]
54pub struct Config {
55    pub filesize: FilesizeConfig,
56    pub table: TableConfig,
57    pub ls: LsConfig,
58    pub clip: ClipConfig,
59    pub color_config: HashMap<String, Value>,
60    pub footer_mode: FooterMode,
61    pub float_precision: i64,
62    pub recursion_limit: i64,
63    pub use_ansi_coloring: UseAnsiColoring,
64    pub completions: CompletionConfig,
65    pub edit_mode: EditBindings,
66    pub show_hints: bool,
67    pub hinter: HinterConfig,
68    pub history: HistoryConfig,
69    pub keybindings: Vec<ParsedKeybinding>,
70    pub abbreviations: HashMap<String, String>,
71    pub menus: Vec<ParsedMenu>,
72    pub hooks: Hooks,
73    pub rm: RmConfig,
74    pub shell_integration: ShellIntegrationConfig,
75    pub buffer_editor: Value,
76    pub show_banner: BannerKind,
77    pub bracketed_paste: bool,
78    pub render_right_prompt_on_last_line: bool,
79    pub explore: HashMap<String, Value>,
80    pub cursor_shape: CursorShapeConfig,
81    pub datetime_format: DatetimeFormatConfig,
82    pub error_style: ErrorStyle,
83    pub error_lines: i64,
84    pub display_errors: DisplayErrors,
85    pub use_kitty_protocol: bool,
86    pub highlight_resolved_externals: bool,
87    pub auto_cd_implicit: bool,
88    pub duration_max_unit: DurationMaxUnit,
89    /// Maximum estimated memory size of the interactive last-result payload (`$ans.last`).
90    ///
91    /// Measured with [`Value::memory_size`]. Default is `0` (no `.last` payload; opt-in).
92    /// Oversized results are truncated to fit this budget. The variable name itself is a code
93    /// constant (`LAST_RESULT_VAR_NAME`), not a config option. With a positive budget, `$ans`
94    /// is `{ last, exit_code, duration, command }`. With `0`, `$ans` still has `exit_code`,
95    /// `duration`, and `command` but omits `last` entirely.
96    pub max_last_result_size: Filesize,
97    /// Configuration for plugins.
98    ///
99    /// Users can provide configuration for a plugin through this entry.  The entry name must
100    /// match the registered plugin name so `plugin add nu_plugin_example` will be able to place
101    /// its configuration under a `nu_plugin_example` column.
102    pub plugins: HashMap<String, Value>,
103    /// Configuration for plugin garbage collection.
104    pub plugin_gc: PluginGcConfigs,
105}
106
107impl Default for Config {
108    fn default() -> Config {
109        Config {
110            show_banner: BannerKind::default(),
111
112            table: TableConfig::default(),
113            rm: RmConfig::default(),
114            ls: LsConfig::default(),
115
116            datetime_format: DatetimeFormatConfig::default(),
117
118            explore: defaults::default_explore(),
119
120            history: HistoryConfig::default(),
121
122            completions: CompletionConfig::default(),
123
124            recursion_limit: 50,
125
126            filesize: FilesizeConfig::default(),
127
128            cursor_shape: CursorShapeConfig::default(),
129
130            clip: ClipConfig::default(),
131
132            color_config: defaults::default_color_config(),
133            footer_mode: FooterMode::RowCount(25),
134            float_precision: 2,
135            buffer_editor: Value::nothing(Span::unknown()),
136            use_ansi_coloring: UseAnsiColoring::default(),
137            bracketed_paste: true,
138            edit_mode: EditBindings::default(),
139            show_hints: true,
140            hinter: HinterConfig::default(),
141
142            shell_integration: ShellIntegrationConfig::default(),
143
144            render_right_prompt_on_last_line: false,
145
146            hooks: Hooks::new(),
147
148            menus: defaults::default_menus(),
149
150            keybindings: defaults::default_keybindings(),
151            abbreviations: HashMap::new(),
152
153            error_style: ErrorStyle::default(),
154            error_lines: 1,
155            display_errors: DisplayErrors::default(),
156
157            use_kitty_protocol: false,
158            highlight_resolved_externals: false,
159
160            auto_cd_implicit: false,
161            duration_max_unit: DurationMaxUnit::default(),
162
163            // Opt-in for `.last` payload: 0 drops last, keeps exit_code/duration/command.
164            max_last_result_size: Filesize::ZERO,
165
166            plugins: HashMap::new(),
167            plugin_gc: PluginGcConfigs::default(),
168        }
169    }
170}
171
172impl UpdateFromValue for Config {
173    fn update<'a>(
174        &mut self,
175        value: &'a Value,
176        path: &mut ConfigPath<'a>,
177        errors: &mut ConfigErrors,
178    ) {
179        let Value::Record { val: record, .. } = value else {
180            errors.type_mismatch(path, Type::record(), value);
181            return;
182        };
183
184        for (col, val) in record.iter() {
185            let current_path = &mut path.push(col);
186
187            match col.as_str() {
188                "ls" => self.ls.update(val, current_path, errors),
189                "rm" => self.rm.update(val, current_path, errors),
190                "history" => self.history.update(val, current_path, errors),
191                "completions" => self.completions.update(val, current_path, errors),
192                "cursor_shape" => self.cursor_shape.update(val, current_path, errors),
193                "table" => self.table.update(val, current_path, errors),
194                "filesize" => self.filesize.update(val, current_path, errors),
195                "explore" => self.explore.update(val, current_path, errors),
196                "color_config" => self.color_config.update(val, current_path, errors),
197                "clip" => self.clip.update(val, current_path, errors),
198                "footer_mode" => self.footer_mode.update(val, current_path, errors),
199                "float_precision" => self.float_precision.update(val, current_path, errors),
200                "use_ansi_coloring" => self.use_ansi_coloring.update(val, current_path, errors),
201                "edit_mode" => self.edit_mode.update(val, current_path, errors),
202                "show_hints" => self.show_hints.update(val, current_path, errors),
203                "hinter" => self.hinter.update(val, current_path, errors),
204                "shell_integration" => self.shell_integration.update(val, current_path, errors),
205                "show_banner" => self.show_banner.update(val, current_path, errors),
206                "display_errors" => self.display_errors.update(val, current_path, errors),
207                "render_right_prompt_on_last_line" => {
208                    self.render_right_prompt_on_last_line
209                        .update(val, current_path, errors)
210                }
211                "bracketed_paste" => self.bracketed_paste.update(val, current_path, errors),
212                "use_kitty_protocol" => self.use_kitty_protocol.update(val, current_path, errors),
213                "highlight_resolved_externals" => {
214                    self.highlight_resolved_externals
215                        .update(val, current_path, errors)
216                }
217                "auto_cd_implicit" => self.auto_cd_implicit.update(val, current_path, errors),
218                "duration_max_unit" => self.duration_max_unit.update(val, current_path, errors),
219                "plugins" => self.plugins.update(val, current_path, errors),
220                "plugin_gc" => self.plugin_gc.update(val, current_path, errors),
221                "abbreviations" => self.abbreviations.update(val, current_path, errors),
222                "hooks" => self.hooks.update(val, current_path, errors),
223                "datetime_format" => self.datetime_format.update(val, current_path, errors),
224                "error_style" => self.error_style.update(val, current_path, errors),
225
226                "buffer_editor" => match val {
227                    Value::Nothing { .. } | Value::String { .. } => {
228                        self.buffer_editor = val.clone();
229                    }
230                    Value::List { vals: values, .. }
231                        if values
232                            .iter()
233                            .all(|list_element| matches!(list_element, Value::String { .. })) =>
234                    {
235                        self.buffer_editor = val.clone();
236                    }
237                    _ => errors.type_mismatch(
238                        current_path,
239                        Type::custom("string, list<string>, or nothing"),
240                        val,
241                    ),
242                },
243
244                "max_last_result_size" => {
245                    self.max_last_result_size.update(val, current_path, errors)
246                }
247
248                "menus" => match Vec::<ParsedMenu>::from_value(val.clone()) {
249                    Ok(menus) => {
250                        for menu in menus {
251                            let target_name = menu.name.to_expanded_string("", self);
252
253                            let found_index = self.menus.iter().position(|existing_menu| {
254                                existing_menu.name.to_expanded_string("", self) == target_name
255                            });
256
257                            if let Some(index) = found_index {
258                                self.menus[index] = menu;
259                            } else {
260                                self.menus.push(menu);
261                            }
262                        }
263                    }
264                    Err(error) => errors.error(error.into()),
265                },
266
267                "keybindings" => match Vec::<ParsedKeybinding>::from_value(val.clone()) {
268                    Ok(keybindings) => {
269                        for keybinding in keybindings {
270                            // An unnamed binding has no name to merge on, so match it on
271                            // the key it binds. Otherwise it matches nothing and is
272                            // appended again on every assignment, growing the list each
273                            // time a config is re-sourced.
274                            let found_index =
275                                self.keybindings.iter().position(|existing_keybinding| {
276                                    match (&keybinding.name, &existing_keybinding.name) {
277                                        (Some(name), Some(existing_name)) => {
278                                            name.to_expanded_string("", self)
279                                                == existing_name.to_expanded_string("", self)
280                                        }
281                                        (None, None) => {
282                                            keybinding.modifier == existing_keybinding.modifier
283                                                && keybinding.keycode == existing_keybinding.keycode
284                                                && keybinding.mode == existing_keybinding.mode
285                                        }
286                                        _ => false,
287                                    }
288                                });
289
290                            if let Some(index) = found_index {
291                                self.keybindings[index] = keybinding;
292                            } else {
293                                self.keybindings.push(keybinding);
294                            }
295                        }
296                    }
297                    Err(error) => errors.error(error.into()),
298                },
299
300                "error_lines" => match val.as_int() {
301                    Ok(integer) if integer >= 0 => self.error_lines = integer,
302                    Ok(_) => {
303                        errors.invalid_value(current_path, "an int greater than or equal to 0", val)
304                    }
305                    Err(_) => errors.type_mismatch(current_path, Type::Int, val),
306                },
307
308                "recursion_limit" => match val.as_int() {
309                    Ok(integer) if integer > 1 => self.recursion_limit = integer,
310                    Ok(_) => errors.invalid_value(current_path, "an int greater than 1", val),
311                    Err(_) => errors.type_mismatch(current_path, Type::Int, val),
312                },
313
314                _ => errors.unknown_option(current_path, val),
315            }
316        }
317    }
318}
319
320impl UpdateFromValue for Filesize {
321    fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
322        match value.as_filesize() {
323            Ok(size) if !size.is_negative() => *self = size,
324            Ok(_) => errors.invalid_value(path, "a non-negative filesize", value),
325            Err(_) => errors.type_mismatch(path, Type::Filesize, value),
326        }
327    }
328}
329
330impl Config {
331    /// Returns the configured last-result size budget in bytes (`0` disables `.last` only).
332    pub fn max_last_result_size_bytes(&self) -> usize {
333        self.max_last_result_size.get().max(0) as usize
334    }
335
336    pub fn update_from_value(
337        &mut self,
338        old: &Config,
339        value: &Value,
340    ) -> Result<Option<ShellWarning>, ShellError> {
341        self.update_from_value_with_options(old, value, false)
342    }
343
344    /// Like [`Config::update_from_value`], but allows callers to indicate that runtime-locked
345    /// options should refuse to change.
346    ///
347    /// `history_locked_after_startup` should be set to `true` once the REPL has finished
348    /// initializing reedline's history backend. After that point, changing any of the
349    /// startup-only history fields (`path`, `max_size`, `file_format`, `isolation`) has no
350    /// effect on the live history, so we reject the assignment with a clear error instead of
351    /// silently ignoring it.
352    pub fn update_from_value_with_options(
353        &mut self,
354        old: &Config,
355        value: &Value,
356        history_locked_after_startup: bool,
357    ) -> Result<Option<ShellWarning>, ShellError> {
358        // Current behaviour is that config errors are displayed, but do not prevent the rest
359        // of the config from being updated (fields with errors are skipped/not updated).
360        // Errors are simply collected one-by-one and wrapped into a ShellError variant at the end.
361        let mut errors =
362            ConfigErrors::new(old).with_history_locked_after_startup(history_locked_after_startup);
363        let mut path = ConfigPath::new();
364
365        self.update(value, &mut path, &mut errors);
366
367        errors.check()
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    /// A record-valued config field is a full-record replace on assignment (e.g.
376    /// `$env.config.keybindings = [...]`), but `update_from_value` must still merge
377    /// named defaults into place rather than silently dropping ones the caller
378    /// didn't mention.
379    #[test]
380    fn reassigning_a_named_list_field_keeps_unmentioned_defaults() {
381        let old = Config::default();
382        let mut new = old.clone();
383
384        let mut extra_menu = old.menus[0].clone();
385        extra_menu.name = Value::test_string("added_menu");
386        let mut extra_keybinding = old.keybindings[0].clone();
387        extra_keybinding.name = Some(Value::test_string("added_binding"));
388
389        let value = Value::test_record(record! {
390            "menus" => Value::test_list(vec![extra_menu.into_value(Span::test_data())]),
391            "keybindings" => Value::test_list(vec![extra_keybinding.into_value(Span::test_data())]),
392        });
393        new.update_from_value(&old, &value)
394            .expect("update should succeed");
395
396        for default_menu in &old.menus {
397            let name = default_menu.name.to_expanded_string("", &old);
398            assert!(
399                new.menus
400                    .iter()
401                    .any(|m| m.name.to_expanded_string("", &new) == name),
402                "default menu {name:?} was lost after reassigning `menus`"
403            );
404        }
405        for default_keybinding in &old.keybindings {
406            let Some(name) = default_keybinding
407                .name
408                .as_ref()
409                .map(|n| n.to_expanded_string("", &old))
410            else {
411                continue;
412            };
413            assert!(
414                new.keybindings.iter().any(|k| k
415                    .name
416                    .as_ref()
417                    .is_some_and(|n| n.to_expanded_string("", &new) == name)),
418                "default keybinding {name:?} was lost after reassigning `keybindings`"
419            );
420        }
421    }
422
423    /// Guards the unnamed case: with no `name` to merge on, every reassignment
424    /// used to append another copy.
425    #[test]
426    fn reassigning_an_unnamed_keybinding_does_not_duplicate_it() {
427        let old = Config::default();
428        let mut new = old.clone();
429
430        let mut unnamed = old.keybindings[0].clone();
431        unnamed.name = None;
432        unnamed.modifier = Value::test_string("alt");
433        unnamed.keycode = Value::test_string("char_j");
434        new.keybindings.push(unnamed);
435
436        let expected = new.keybindings.len();
437
438        // Feed the list back through `update_from_value` the way re-sourcing a
439        // config (or any `$env.config.keybindings = ...`) does.
440        for _ in 0..2 {
441            let value = Value::test_record(record! {
442                "keybindings" => Value::test_list(
443                    new.keybindings
444                        .iter()
445                        .map(|keybinding| keybinding.clone().into_value(Span::test_data()))
446                        .collect(),
447                ),
448            });
449            new.update_from_value(&old, &value)
450                .expect("update should succeed");
451        }
452
453        assert_eq!(
454            new.keybindings.len(),
455            expected,
456            "reassigning `keybindings` duplicated the unnamed binding"
457        );
458    }
459}