1use crate::config::reedline::{KeyIdentity, name_of};
4use crate::{self as nu_protocol, Filesize};
5use crate::{ConfigWarning, FromValue};
6use helper::*;
7use prelude::*;
8use std::collections::{BTreeSet, HashMap};
9
10pub use ansi_coloring::UseAnsiColoring;
11pub use clip::ClipConfig;
12pub use completions::{
13 CompletionAlgorithm, CompletionConfig, CompletionSort, ExternalCompleterConfig,
14};
15pub use datetime_format::DatetimeFormatConfig;
16pub use defaults::default_color_config;
17pub use display_errors::DisplayErrors;
18pub use duration_max_unit::DurationMaxUnit;
19pub use filesize::FilesizeConfig;
20pub use helper::extract_value;
21pub use hinter::HinterConfig;
22pub use history::{HistoryConfig, HistoryFileFormat, HistoryPath};
23pub use hooks::Hooks;
24pub use ls::LsConfig;
25pub use output::{BannerKind, ErrorStyle};
26pub use plugin_gc::{PluginGcConfig, PluginGcConfigs};
27pub use reedline::{CursorShapeConfig, EditBindings, NuCursorShape, ParsedKeybinding, ParsedMenu};
28pub use rm::RmConfig;
29pub use shell_integration::ShellIntegrationConfig;
30pub use table::{FooterMode, TableConfig, TableIndent, TableIndexMode, TableMode, TrimStrategy};
31
32mod ansi_coloring;
33mod clip;
34mod completions;
35mod datetime_format;
36mod defaults;
37mod display_errors;
38mod duration_max_unit;
39mod error;
40mod filesize;
41mod helper;
42mod hinter;
43mod history;
44mod hooks;
45mod ls;
46mod output;
47mod plugin_gc;
48mod prelude;
49mod reedline;
50mod rm;
51mod shell_integration;
52mod table;
53
54#[derive(Clone, Debug, IntoValue, Serialize, Deserialize)]
55pub struct Config {
56 pub filesize: FilesizeConfig,
57 pub table: TableConfig,
58 pub ls: LsConfig,
59 pub clip: ClipConfig,
60 pub color_config: HashMap<String, Value>,
61 pub footer_mode: FooterMode,
62 pub float_precision: i64,
63 pub recursion_limit: i64,
64 pub use_ansi_coloring: UseAnsiColoring,
65 pub completions: CompletionConfig,
66 pub edit_mode: EditBindings,
67 pub show_hints: bool,
68 pub hinter: HinterConfig,
69 pub history: HistoryConfig,
70 pub keybindings: Vec<ParsedKeybinding>,
71 pub abbreviations: HashMap<String, String>,
72 pub menus: Vec<ParsedMenu>,
73 pub hooks: Hooks,
74 pub rm: RmConfig,
75 pub shell_integration: ShellIntegrationConfig,
76 pub buffer_editor: Value,
77 pub show_banner: BannerKind,
78 pub bracketed_paste: bool,
79 pub render_right_prompt_on_last_line: bool,
80 pub explore: HashMap<String, Value>,
81 pub cursor_shape: CursorShapeConfig,
82 pub datetime_format: DatetimeFormatConfig,
83 pub error_style: ErrorStyle,
84 pub error_lines: i64,
85 pub display_errors: DisplayErrors,
86 pub use_kitty_protocol: bool,
87 pub highlight_resolved_externals: bool,
88 pub auto_cd_implicit: bool,
89 pub duration_max_unit: DurationMaxUnit,
90 pub max_last_result_size: Filesize,
98 pub plugins: HashMap<String, Value>,
104 pub plugin_gc: PluginGcConfigs,
106}
107
108impl Default for Config {
109 fn default() -> Config {
110 Config {
111 show_banner: BannerKind::default(),
112
113 table: TableConfig::default(),
114 rm: RmConfig::default(),
115 ls: LsConfig::default(),
116
117 datetime_format: DatetimeFormatConfig::default(),
118
119 explore: defaults::default_explore(),
120
121 history: HistoryConfig::default(),
122
123 completions: CompletionConfig::default(),
124
125 recursion_limit: 50,
126
127 filesize: FilesizeConfig::default(),
128
129 cursor_shape: CursorShapeConfig::default(),
130
131 clip: ClipConfig::default(),
132
133 color_config: defaults::default_color_config(),
134 footer_mode: FooterMode::RowCount(25),
135 float_precision: 2,
136 buffer_editor: Value::nothing(Span::unknown()),
137 use_ansi_coloring: UseAnsiColoring::default(),
138 bracketed_paste: true,
139 edit_mode: EditBindings::default(),
140 show_hints: true,
141 hinter: HinterConfig::default(),
142
143 shell_integration: ShellIntegrationConfig::default(),
144
145 render_right_prompt_on_last_line: false,
146
147 hooks: Hooks::new(),
148
149 menus: defaults::default_menus(),
150
151 keybindings: defaults::default_keybindings(),
152 abbreviations: HashMap::new(),
153
154 error_style: ErrorStyle::default(),
155 error_lines: 1,
156 display_errors: DisplayErrors::default(),
157
158 use_kitty_protocol: false,
159 highlight_resolved_externals: false,
160
161 auto_cd_implicit: false,
162 duration_max_unit: DurationMaxUnit::default(),
163
164 max_last_result_size: Filesize::ZERO,
166
167 plugins: HashMap::new(),
168 plugin_gc: PluginGcConfigs::default(),
169 }
170 }
171}
172
173impl UpdateFromValue for Config {
174 fn update<'a>(
175 &mut self,
176 value: &'a Value,
177 path: &mut ConfigPath<'a>,
178 errors: &mut ConfigErrors,
179 ) {
180 let Value::Record { val: record, .. } = value else {
181 errors.type_mismatch(path, Type::record(), value);
182 return;
183 };
184
185 for (col, val) in record.iter() {
186 let current_path = &mut path.push(col);
187
188 match col.as_str() {
189 "ls" => self.ls.update(val, current_path, errors),
190 "rm" => self.rm.update(val, current_path, errors),
191 "history" => self.history.update(val, current_path, errors),
192 "completions" => self.completions.update(val, current_path, errors),
193 "cursor_shape" => self.cursor_shape.update(val, current_path, errors),
194 "table" => self.table.update(val, current_path, errors),
195 "filesize" => self.filesize.update(val, current_path, errors),
196 "explore" => self.explore.update(val, current_path, errors),
197 "color_config" => self.color_config.update(val, current_path, errors),
198 "clip" => self.clip.update(val, current_path, errors),
199 "footer_mode" => self.footer_mode.update(val, current_path, errors),
200 "float_precision" => self.float_precision.update(val, current_path, errors),
201 "use_ansi_coloring" => self.use_ansi_coloring.update(val, current_path, errors),
202 "edit_mode" => self.edit_mode.update(val, current_path, errors),
203 "show_hints" => self.show_hints.update(val, current_path, errors),
204 "hinter" => self.hinter.update(val, current_path, errors),
205 "shell_integration" => self.shell_integration.update(val, current_path, errors),
206 "show_banner" => self.show_banner.update(val, current_path, errors),
207 "display_errors" => self.display_errors.update(val, current_path, errors),
208 "render_right_prompt_on_last_line" => {
209 self.render_right_prompt_on_last_line
210 .update(val, current_path, errors)
211 }
212 "bracketed_paste" => self.bracketed_paste.update(val, current_path, errors),
213 "use_kitty_protocol" => self.use_kitty_protocol.update(val, current_path, errors),
214 "highlight_resolved_externals" => {
215 self.highlight_resolved_externals
216 .update(val, current_path, errors)
217 }
218 "auto_cd_implicit" => self.auto_cd_implicit.update(val, current_path, errors),
219 "duration_max_unit" => self.duration_max_unit.update(val, current_path, errors),
220 "plugins" => self.plugins.update(val, current_path, errors),
221 "plugin_gc" => self.plugin_gc.update(val, current_path, errors),
222 "abbreviations" => self.abbreviations.update(val, current_path, errors),
223 "hooks" => self.hooks.update(val, current_path, errors),
224 "datetime_format" => self.datetime_format.update(val, current_path, errors),
225 "error_style" => self.error_style.update(val, current_path, errors),
226
227 "buffer_editor" => match val {
228 Value::Nothing { .. } | Value::String { .. } => {
229 self.buffer_editor = val.clone();
230 }
231 Value::List { vals: values, .. }
232 if values
233 .iter()
234 .all(|list_element| matches!(list_element, Value::String { .. })) =>
235 {
236 self.buffer_editor = val.clone();
237 }
238 _ => errors.type_mismatch(
239 current_path,
240 Type::custom("string, list<string>, or nothing"),
241 val,
242 ),
243 },
244
245 "max_last_result_size" => {
246 self.max_last_result_size.update(val, current_path, errors)
247 }
248
249 "menus" => match Vec::<ParsedMenu>::from_value(val.clone()) {
250 Ok(menus) => {
251 for menu in menus {
252 let target_name = menu.name.to_expanded_string("", self);
253
254 let found_index = self.menus.iter().position(|existing_menu| {
255 existing_menu.name.to_expanded_string("", self) == target_name
256 });
257
258 if let Some(index) = found_index {
259 self.menus[index] = menu;
260 } else {
261 self.menus.push(menu);
262 }
263 }
264 }
265 Err(error) => errors.error(error.into()),
266 },
267
268 "keybindings" => match Vec::<ParsedKeybinding>::from_value(val.clone()) {
269 Ok(keybindings) => self.merge_keybindings(keybindings, val.span(), errors),
270 Err(error) => errors.error(error.into()),
271 },
272
273 "error_lines" => match val.as_int() {
274 Ok(integer) if integer >= 0 => self.error_lines = integer,
275 Ok(_) => {
276 errors.invalid_value(current_path, "an int greater than or equal to 0", val)
277 }
278 Err(_) => errors.type_mismatch(current_path, Type::Int, val),
279 },
280
281 "recursion_limit" => match val.as_int() {
282 Ok(integer) if integer > 1 => self.recursion_limit = integer,
283 Ok(_) => errors.invalid_value(current_path, "an int greater than 1", val),
284 Err(_) => errors.type_mismatch(current_path, Type::Int, val),
285 },
286
287 _ => errors.unknown_option(current_path, val),
288 }
289 }
290 }
291}
292
293impl UpdateFromValue for Filesize {
294 fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
295 match value.as_filesize() {
296 Ok(size) if !size.is_negative() => *self = size,
297 Ok(_) => errors.invalid_value(path, "a non-negative filesize", value),
298 Err(_) => errors.type_mismatch(path, Type::Filesize, value),
299 }
300 }
301}
302
303impl Config {
304 pub fn max_last_result_size_bytes(&self) -> usize {
306 self.max_last_result_size.get().max(0) as usize
307 }
308
309 pub fn update_from_value(
310 &mut self,
311 old: &Config,
312 value: &Value,
313 ) -> Result<Option<ShellWarning>, ShellError> {
314 self.update_from_value_with_options(old, value, false)
315 }
316
317 pub fn update_from_value_with_options(
326 &mut self,
327 old: &Config,
328 value: &Value,
329 history_locked_after_startup: bool,
330 ) -> Result<Option<ShellWarning>, ShellError> {
331 let mut errors =
335 ConfigErrors::new(old).with_history_locked_after_startup(history_locked_after_startup);
336 let mut path = ConfigPath::new();
337
338 self.update(value, &mut path, &mut errors);
339
340 errors.check()
341 }
342
343 fn merge_keybindings(
344 &mut self,
345 incoming: Vec<ParsedKeybinding>,
346 span: Span,
347 errors: &mut ConfigErrors,
348 ) {
349 if incoming.is_empty() {
350 self.keybindings.clear();
351 return;
352 }
353
354 let mut shared_names = BTreeSet::new();
355 let identities = incoming.into_iter().map(|kb| {
356 let name = name_of(&kb);
357 let id = KeyIdentity::of(&kb);
358 (kb, name, id)
359 });
360
361 struct Existing {
366 name: Option<String>,
367 id: KeyIdentity,
368 claimed: bool,
369 }
370 let mut ex_kbs: Vec<Existing> = self
371 .keybindings
372 .iter()
373 .map(|ex| Existing {
374 name: name_of(ex),
375 id: KeyIdentity::of(ex),
376 claimed: false,
377 })
378 .collect();
379
380 for (kb, name, id) in identities {
381 if let Some(i) = ex_kbs.iter().position(|ex| ex.name == name && ex.id == id) {
384 ex_kbs[i].claimed = true;
385 self.keybindings[i] = kb;
386 continue;
387 }
388
389 if name.is_some()
392 && let Some(i) = ex_kbs.iter().position(|ex| ex.name == name && !ex.claimed)
393 {
394 ex_kbs[i].id = id;
395 ex_kbs[i].claimed = true;
396 self.keybindings[i] = kb;
397 continue;
398 }
399
400 if let Some(name) = &name
403 && ex_kbs.iter().any(|ex| ex.name.as_ref() == Some(name))
404 {
405 shared_names.insert(name.clone());
406 }
407 ex_kbs.push(Existing {
408 name,
409 id,
410 claimed: true,
411 });
412 self.keybindings.push(kb);
413 }
414 if !shared_names.is_empty() {
415 errors.warn(ConfigWarning::SharedKeybindingName {
416 names: shared_names.into_iter().collect::<Vec<_>>().join(", "),
417 span,
418 });
419 }
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426
427 #[test]
432 fn reassigning_a_named_list_field_keeps_unmentioned_defaults() {
433 let old = Config::default();
434 let mut new = old.clone();
435
436 let mut extra_menu = old.menus[0].clone();
437 extra_menu.name = Value::test_string("added_menu");
438 let mut extra_keybinding = old.keybindings[0].clone();
439 extra_keybinding.name = Some(Value::test_string("added_binding"));
440
441 let value = Value::test_record(record! {
442 "menus" => Value::test_list(vec![extra_menu.into_value(Span::test_data())]),
443 "keybindings" => Value::test_list(vec![extra_keybinding.into_value(Span::test_data())]),
444 });
445 new.update_from_value(&old, &value)
446 .expect("update should succeed");
447
448 for default_menu in &old.menus {
449 let name = default_menu.name.to_expanded_string("", &old);
450 assert!(
451 new.menus
452 .iter()
453 .any(|m| m.name.to_expanded_string("", &new) == name),
454 "default menu {name:?} was lost after reassigning `menus`"
455 );
456 }
457 for default_keybinding in &old.keybindings {
458 let Some(name) = default_keybinding
459 .name
460 .as_ref()
461 .map(|n| n.to_expanded_string("", &old))
462 else {
463 continue;
464 };
465 assert!(
466 new.keybindings.iter().any(|k| k
467 .name
468 .as_ref()
469 .is_some_and(|n| n.to_expanded_string("", &new) == name)),
470 "default keybinding {name:?} was lost after reassigning `keybindings`"
471 );
472 }
473 }
474
475 #[test]
478 fn reassigning_an_unnamed_keybinding_does_not_duplicate_it() {
479 let old = Config::default();
480 let mut new = old.clone();
481
482 let mut unnamed = old.keybindings[0].clone();
483 unnamed.name = None;
484 unnamed.modifier = Value::test_string("alt");
485 unnamed.keycode = Value::test_string("char_j");
486 new.keybindings.push(unnamed);
487
488 let expected = new.keybindings.len();
489
490 for _ in 0..2 {
493 let value = Value::test_record(record! {
494 "keybindings" => Value::test_list(
495 new.keybindings
496 .iter()
497 .map(|keybinding| keybinding.clone().into_value(Span::test_data()))
498 .collect(),
499 ),
500 });
501 new.update_from_value(&old, &value)
502 .expect("update should succeed");
503 }
504
505 assert_eq!(
506 new.keybindings.len(),
507 expected,
508 "reassigning `keybindings` duplicated the unnamed binding"
509 );
510 }
511
512 fn keybinding(
515 name: Option<&str>,
516 modifier: &str,
517 keycode: &str,
518 mode: Value,
519 ) -> ParsedKeybinding {
520 ParsedKeybinding {
521 name: name.map(Value::test_string),
522 modifier: Value::test_string(modifier),
523 keycode: Value::test_string(keycode),
524 event: Value::test_nothing(),
525 mode,
526 }
527 }
528
529 fn assign(
531 config: &mut Config,
532 old: &Config,
533 keybindings: Vec<ParsedKeybinding>,
534 ) -> Option<ShellWarning> {
535 let value = Value::test_record(record! {
536 "keybindings" => Value::test_list(
537 keybindings
538 .into_iter()
539 .map(|kb| kb.into_value(Span::test_data()))
540 .collect(),
541 ),
542 });
543 config
544 .update_from_value(old, &value)
545 .expect("update should succeed")
546 }
547
548 fn count_named(config: &Config, name: &str) -> usize {
549 config
550 .keybindings
551 .iter()
552 .filter(|kb| {
553 kb.name
554 .as_ref()
555 .is_some_and(|n| n.to_expanded_string("", config) == name)
556 })
557 .count()
558 }
559
560 #[test]
563 fn a_shared_name_on_different_keys_keeps_both_bindings_and_warns() {
564 let old = Config::default();
565 let mut new = old.clone();
566
567 let warning = assign(
568 &mut new,
569 &old,
570 vec![
571 keybinding(
572 Some("atuin"),
573 "control",
574 "char_r",
575 Value::test_string("emacs"),
576 ),
577 keybinding(Some("atuin"), "none", "up", Value::test_string("emacs")),
578 ],
579 );
580
581 assert_eq!(
582 count_named(&new, "atuin"),
583 2,
584 "one of the bindings was dropped"
585 );
586 assert!(warning.is_some(), "sharing a name should warn");
587 }
588
589 #[test]
591 fn reassigning_the_same_binding_replaces_it_without_warning() {
592 let old = Config::default();
593 let mut new = old.clone();
594
595 let atuin = || {
596 keybinding(
597 Some("atuin"),
598 "control",
599 "char_r",
600 Value::test_string("emacs"),
601 )
602 };
603 assign(&mut new, &old, vec![atuin()]);
604 let len = new.keybindings.len();
605
606 let warning = assign(&mut new, &old, vec![atuin()]);
607 assert_eq!(
608 new.keybindings.len(),
609 len,
610 "re-sourcing duplicated the binding"
611 );
612 assert!(
613 warning.is_none(),
614 "an identical re-assignment must not warn"
615 );
616 }
617
618 #[test]
620 fn a_new_event_on_the_same_key_replaces_the_binding() {
621 let old = Config::default();
622 let mut new = old.clone();
623
624 assign(
625 &mut new,
626 &old,
627 vec![keybinding(
628 Some("atuin"),
629 "control",
630 "char_r",
631 Value::test_string("emacs"),
632 )],
633 );
634 let len = new.keybindings.len();
635
636 let mut updated = keybinding(
637 Some("atuin"),
638 "control",
639 "char_r",
640 Value::test_string("emacs"),
641 );
642 updated.event = Value::test_string("marker");
643 assign(&mut new, &old, vec![updated]);
644
645 assert_eq!(new.keybindings.len(), len);
646 let event = new
647 .keybindings
648 .iter()
649 .rev()
650 .find(|kb| {
651 kb.name
652 .as_ref()
653 .is_some_and(|n| n.to_expanded_string("", &new) == "atuin")
654 })
655 .map(|kb| kb.event.clone());
656 assert_eq!(
657 event,
658 Some(Value::test_string("marker")),
659 "event was not updated"
660 );
661 }
662
663 #[test]
665 fn a_bare_mode_and_its_singleton_list_merge_into_one_binding() {
666 let old = Config::default();
667 let mut new = old.clone();
668
669 assign(
670 &mut new,
671 &old,
672 vec![keybinding(
673 Some("atuin"),
674 "control",
675 "char_r",
676 Value::test_string("emacs"),
677 )],
678 );
679 let warning = assign(
680 &mut new,
681 &old,
682 vec![keybinding(
683 Some("atuin"),
684 "control",
685 "char_r",
686 Value::test_list(vec![Value::test_string("emacs")]),
687 )],
688 );
689
690 assert_eq!(
691 count_named(&new, "atuin"),
692 1,
693 "the mode spellings did not merge"
694 );
695 assert!(warning.is_none());
696 }
697
698 #[test]
701 fn the_same_binding_twice_in_one_assignment_is_stored_once() {
702 let old = Config::default();
703 let mut new = old.clone();
704
705 let atuin = || {
706 keybinding(
707 Some("atuin"),
708 "control",
709 "char_r",
710 Value::test_string("emacs"),
711 )
712 };
713 assign(&mut new, &old, vec![atuin(), atuin()]);
714
715 assert_eq!(count_named(&new, "atuin"), 1, "the duplicate was appended");
716 }
717
718 #[test]
722 fn a_named_binding_with_a_new_key_replaces_in_place() {
723 let old = Config::default();
724 let mut new = old.clone();
725
726 assign(
727 &mut new,
728 &old,
729 vec![keybinding(
730 Some("atuin"),
731 "control",
732 "char_r",
733 Value::test_string("emacs"),
734 )],
735 );
736 let len = new.keybindings.len();
737 let index = new
738 .keybindings
739 .iter()
740 .position(|kb| {
741 kb.name
742 .as_ref()
743 .is_some_and(|n| n.to_expanded_string("", &new) == "atuin")
744 })
745 .expect("binding was added");
746
747 let warning = assign(
748 &mut new,
749 &old,
750 vec![keybinding(
751 Some("atuin"),
752 "none",
753 "up",
754 Value::test_string("emacs"),
755 )],
756 );
757
758 assert_eq!(new.keybindings.len(), len, "re-keying must not append");
759 assert_eq!(
760 new.keybindings[index].keycode,
761 Value::test_string("up"),
762 "the binding was not re-keyed in place"
763 );
764 assert!(warning.is_none(), "re-keying a lone name must not warn");
765 }
766
767 #[test]
769 fn a_named_binding_with_a_changed_mode_replaces_instead_of_appending() {
770 let old = Config::default();
771 let mut new = old.clone();
772
773 assign(
774 &mut new,
775 &old,
776 vec![keybinding(
777 Some("atuin"),
778 "control",
779 "char_r",
780 Value::test_string("emacs"),
781 )],
782 );
783 let warning = assign(
784 &mut new,
785 &old,
786 vec![keybinding(
787 Some("atuin"),
788 "control",
789 "char_r",
790 Value::test_list(vec![
791 Value::test_string("vi_normal"),
792 Value::test_string("vi_insert"),
793 ]),
794 )],
795 );
796
797 assert_eq!(count_named(&new, "atuin"), 1, "the mode change appended");
798 assert!(warning.is_none());
799 }
800
801 #[test]
803 fn assigning_an_empty_list_clears_the_keybindings() {
804 let old = Config::default();
805 let mut new = old.clone();
806
807 assign(&mut new, &old, vec![]);
808 assert!(new.keybindings.is_empty(), "`= []` should reset the list");
809 }
810}