1use 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 pub max_last_result_size: Filesize,
97 pub plugins: HashMap<String, Value>,
103 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 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 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 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 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 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 #[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 #[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 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}