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