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" => {
201 self.tui.update(val, current_path, errors);
202 check_tui_border_type(&mut self.tui, current_path, errors);
203 }
204 "color_config" => self.color_config.update(val, current_path, errors),
205 "clip" => self.clip.update(val, current_path, errors),
206 "footer_mode" => self.footer_mode.update(val, current_path, errors),
207 "float_precision" => self.float_precision.update(val, current_path, errors),
208 "use_ansi_coloring" => self.use_ansi_coloring.update(val, current_path, errors),
209 "edit_mode" => self.edit_mode.update(val, current_path, errors),
210 "show_hints" => self.show_hints.update(val, current_path, errors),
211 "hinter" => self.hinter.update(val, current_path, errors),
212 "shell_integration" => self.shell_integration.update(val, current_path, errors),
213 "show_banner" => self.show_banner.update(val, current_path, errors),
214 "display_errors" => self.display_errors.update(val, current_path, errors),
215 "render_right_prompt_on_last_line" => {
216 self.render_right_prompt_on_last_line
217 .update(val, current_path, errors)
218 }
219 "bracketed_paste" => self.bracketed_paste.update(val, current_path, errors),
220 "use_kitty_protocol" => self.use_kitty_protocol.update(val, current_path, errors),
221 "highlight_resolved_externals" => {
222 self.highlight_resolved_externals
223 .update(val, current_path, errors)
224 }
225 "auto_cd_implicit" => self.auto_cd_implicit.update(val, current_path, errors),
226 "duration_max_unit" => self.duration_max_unit.update(val, current_path, errors),
227 "plugins" => self.plugins.update(val, current_path, errors),
228 "plugin_gc" => self.plugin_gc.update(val, current_path, errors),
229 "abbreviations" => self.abbreviations.update(val, current_path, errors),
230 "hooks" => self.hooks.update(val, current_path, errors),
231 "datetime_format" => self.datetime_format.update(val, current_path, errors),
232 "error_style" => self.error_style.update(val, current_path, errors),
233
234 "buffer_editor" => match val {
235 Value::Nothing { .. } | Value::String { .. } => {
236 self.buffer_editor = val.clone();
237 }
238 Value::List { vals: values, .. }
239 if values
240 .iter()
241 .all(|list_element| matches!(list_element, Value::String { .. })) =>
242 {
243 self.buffer_editor = val.clone();
244 }
245 _ => errors.type_mismatch(
246 current_path,
247 Type::custom("string, list<string>, or nothing"),
248 val,
249 ),
250 },
251
252 "max_last_result_size" => {
253 self.max_last_result_size.update(val, current_path, errors)
254 }
255
256 "menus" => match Vec::<ParsedMenu>::from_value(val.clone()) {
257 Ok(menus) => {
258 for menu in menus {
259 let target_name = menu.name.to_expanded_string("", self);
260
261 let found_index = self.menus.iter().position(|existing_menu| {
262 existing_menu.name.to_expanded_string("", self) == target_name
263 });
264
265 if let Some(index) = found_index {
266 self.menus[index] = menu;
267 } else {
268 self.menus.push(menu);
269 }
270 }
271 }
272 Err(error) => errors.error(error.into()),
273 },
274
275 "keybindings" => match Vec::<ParsedKeybinding>::from_value(val.clone()) {
276 Ok(keybindings) => self.merge_keybindings(keybindings, val.span(), errors),
277 Err(error) => errors.error(error.into()),
278 },
279
280 "error_lines" => match val.as_int() {
281 Ok(integer) if integer >= 0 => self.error_lines = integer,
282 Ok(_) => {
283 errors.invalid_value(current_path, "an int greater than or equal to 0", val)
284 }
285 Err(_) => errors.type_mismatch(current_path, Type::Int, val),
286 },
287
288 "recursion_limit" => match val.as_int() {
289 Ok(integer) if integer > 1 => self.recursion_limit = integer,
290 Ok(_) => errors.invalid_value(current_path, "an int greater than 1", val),
291 Err(_) => errors.type_mismatch(current_path, Type::Int, val),
292 },
293
294 _ => errors.unknown_option(current_path, val),
295 }
296 }
297 }
298}
299
300fn check_tui_border_type(
305 tui: &mut HashMap<String, Value>,
306 path: &mut ConfigPath,
307 errors: &mut ConfigErrors,
308) {
309 const KEY: &str = "border_type";
310 let Some(value) = tui.get(KEY) else {
311 return;
312 };
313 let path = &mut path.push(KEY);
314 match value.as_str() {
315 Ok(name) if TableMode::from_tui_border(name).is_some() => return,
316 Ok(_) => {
317 let names = TableMode::tui_border_names().collect::<Vec<_>>().join(", ");
318 errors.invalid_value(path, format!("one of {names}"), value);
319 }
320 Err(_) => errors.type_mismatch(path, Type::String, value),
321 }
322 match errors.config().tui.get(KEY).cloned() {
323 Some(old) => tui.insert(KEY.into(), old),
324 None => tui.remove(KEY),
325 };
326}
327
328impl UpdateFromValue for Filesize {
329 fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
330 match value.as_filesize() {
331 Ok(size) if !size.is_negative() => *self = size,
332 Ok(_) => errors.invalid_value(path, "a non-negative filesize", value),
333 Err(_) => errors.type_mismatch(path, Type::Filesize, value),
334 }
335 }
336}
337
338impl Config {
339 pub fn max_last_result_size_bytes(&self) -> usize {
341 self.max_last_result_size.get().max(0) as usize
342 }
343
344 pub fn update_from_value(
345 &mut self,
346 old: &Config,
347 value: &Value,
348 ) -> Result<Option<ShellWarning>, ShellError> {
349 self.update_from_value_with_options(old, value, false)
350 }
351
352 pub fn update_from_value_with_options(
361 &mut self,
362 old: &Config,
363 value: &Value,
364 history_locked_after_startup: bool,
365 ) -> Result<Option<ShellWarning>, ShellError> {
366 let mut errors =
370 ConfigErrors::new(old).with_history_locked_after_startup(history_locked_after_startup);
371 let mut path = ConfigPath::new();
372
373 self.update(value, &mut path, &mut errors);
374
375 errors.check()
376 }
377
378 fn merge_keybindings(
379 &mut self,
380 incoming: Vec<ParsedKeybinding>,
381 span: Span,
382 errors: &mut ConfigErrors,
383 ) {
384 if incoming.is_empty() {
385 self.keybindings.clear();
386 return;
387 }
388
389 let mut shared_names = BTreeSet::new();
390 let identities = incoming.into_iter().map(|kb| {
391 let name = name_of(&kb);
392 let id = KeyIdentity::of(&kb);
393 (kb, name, id)
394 });
395
396 struct Existing {
401 name: Option<String>,
402 id: KeyIdentity,
403 claimed: bool,
404 }
405 let mut ex_kbs: Vec<Existing> = self
406 .keybindings
407 .iter()
408 .map(|ex| Existing {
409 name: name_of(ex),
410 id: KeyIdentity::of(ex),
411 claimed: false,
412 })
413 .collect();
414
415 for (kb, name, id) in identities {
416 if let Some(i) = ex_kbs.iter().position(|ex| ex.name == name && ex.id == id) {
419 ex_kbs[i].claimed = true;
420 self.keybindings[i] = kb;
421 continue;
422 }
423
424 if name.is_some()
427 && let Some(i) = ex_kbs.iter().position(|ex| ex.name == name && !ex.claimed)
428 {
429 ex_kbs[i].id = id;
430 ex_kbs[i].claimed = true;
431 self.keybindings[i] = kb;
432 continue;
433 }
434
435 if let Some(name) = &name
438 && ex_kbs.iter().any(|ex| ex.name.as_ref() == Some(name))
439 {
440 shared_names.insert(name.clone());
441 }
442 ex_kbs.push(Existing {
443 name,
444 id,
445 claimed: true,
446 });
447 self.keybindings.push(kb);
448 }
449 if !shared_names.is_empty() {
450 errors.warn(ConfigWarning::SharedKeybindingName {
451 names: shared_names.into_iter().collect::<Vec<_>>().join(", "),
452 span,
453 });
454 }
455 }
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461
462 #[test]
467 fn reassigning_a_named_list_field_keeps_unmentioned_defaults() {
468 let old = Config::default();
469 let mut new = old.clone();
470
471 let mut extra_menu = old.menus[0].clone();
472 extra_menu.name = Value::test_string("added_menu");
473 let mut extra_keybinding = old.keybindings[0].clone();
474 extra_keybinding.name = Some(Value::test_string("added_binding"));
475
476 let value = Value::test_record(record! {
477 "menus" => Value::test_list(vec![extra_menu.into_value(Span::test_data())]),
478 "keybindings" => Value::test_list(vec![extra_keybinding.into_value(Span::test_data())]),
479 });
480 new.update_from_value(&old, &value)
481 .expect("update should succeed");
482
483 for default_menu in &old.menus {
484 let name = default_menu.name.to_expanded_string("", &old);
485 assert!(
486 new.menus
487 .iter()
488 .any(|m| m.name.to_expanded_string("", &new) == name),
489 "default menu {name:?} was lost after reassigning `menus`"
490 );
491 }
492 for default_keybinding in &old.keybindings {
493 let Some(name) = default_keybinding
494 .name
495 .as_ref()
496 .map(|n| n.to_expanded_string("", &old))
497 else {
498 continue;
499 };
500 assert!(
501 new.keybindings.iter().any(|k| k
502 .name
503 .as_ref()
504 .is_some_and(|n| n.to_expanded_string("", &new) == name)),
505 "default keybinding {name:?} was lost after reassigning `keybindings`"
506 );
507 }
508 }
509
510 #[test]
513 fn reassigning_an_unnamed_keybinding_does_not_duplicate_it() {
514 let old = Config::default();
515 let mut new = old.clone();
516
517 let mut unnamed = old.keybindings[0].clone();
518 unnamed.name = None;
519 unnamed.modifier = Value::test_string("alt");
520 unnamed.keycode = Value::test_string("char_j");
521 new.keybindings.push(unnamed);
522
523 let expected = new.keybindings.len();
524
525 for _ in 0..2 {
528 let value = Value::test_record(record! {
529 "keybindings" => Value::test_list(
530 new.keybindings
531 .iter()
532 .map(|keybinding| keybinding.clone().into_value(Span::test_data()))
533 .collect(),
534 ),
535 });
536 new.update_from_value(&old, &value)
537 .expect("update should succeed");
538 }
539
540 assert_eq!(
541 new.keybindings.len(),
542 expected,
543 "reassigning `keybindings` duplicated the unnamed binding"
544 );
545 }
546
547 #[test]
550 fn tui_border_type_rejects_non_borders_and_keeps_the_old_one() {
551 let old = Config::default();
552 let tui = |border_type: Value| {
553 Value::test_record(record! {
554 "tui" => Value::test_record(record! { "border_type" => border_type }),
555 })
556 };
557
558 let mut new = old.clone();
559 new.update_from_value(&old, &tui(Value::test_string("Double")))
560 .expect("a table theme name is a border");
561 assert_eq!(
562 new.tui.get("border_type"),
563 Some(&Value::test_string("Double"))
564 );
565
566 for bad in [
567 Value::test_string("none"),
568 Value::test_string("default"),
569 Value::test_string("wavy"),
570 Value::test_int(1),
571 ] {
572 let mut new = old.clone();
573 let err = new
574 .update_from_value(&old, &tui(bad.clone()))
575 .expect_err("not a tui border");
576 assert!(
577 format!("{err:?}").contains("$env.config.tui.border_type"),
578 "{bad:?}: {err:?}"
579 );
580 assert_eq!(new.tui.get("border_type"), old.tui.get("border_type"));
581 }
582 }
583
584 #[test]
588 fn table_mode_names_match_from_str() {
589 for name in TableMode::NAMES {
590 assert!(name.parse::<TableMode>().is_ok(), "{name}");
591 }
592 let listed = "wavy".parse::<TableMode>().expect_err("not a table mode");
593 let listed: Vec<&str> = listed.split('\'').skip(1).step_by(2).collect();
594 for name in &listed {
595 assert!(TableMode::NAMES.contains(name), "{name} is not in NAMES");
596 }
597 assert_eq!(listed.len() + 1, TableMode::NAMES.len());
598 }
599
600 fn keybinding(
603 name: Option<&str>,
604 modifier: &str,
605 keycode: &str,
606 mode: Value,
607 ) -> ParsedKeybinding {
608 ParsedKeybinding {
609 name: name.map(Value::test_string),
610 modifier: Value::test_string(modifier),
611 keycode: Value::test_string(keycode),
612 event: Value::test_nothing(),
613 mode,
614 }
615 }
616
617 fn assign(
619 config: &mut Config,
620 old: &Config,
621 keybindings: Vec<ParsedKeybinding>,
622 ) -> Option<ShellWarning> {
623 let value = Value::test_record(record! {
624 "keybindings" => Value::test_list(
625 keybindings
626 .into_iter()
627 .map(|kb| kb.into_value(Span::test_data()))
628 .collect(),
629 ),
630 });
631 config
632 .update_from_value(old, &value)
633 .expect("update should succeed")
634 }
635
636 fn count_named(config: &Config, name: &str) -> usize {
637 config
638 .keybindings
639 .iter()
640 .filter(|kb| {
641 kb.name
642 .as_ref()
643 .is_some_and(|n| n.to_expanded_string("", config) == name)
644 })
645 .count()
646 }
647
648 #[test]
651 fn a_shared_name_on_different_keys_keeps_both_bindings_and_warns() {
652 let old = Config::default();
653 let mut new = old.clone();
654
655 let warning = assign(
656 &mut new,
657 &old,
658 vec![
659 keybinding(
660 Some("atuin"),
661 "control",
662 "char_r",
663 Value::test_string("emacs"),
664 ),
665 keybinding(Some("atuin"), "none", "up", Value::test_string("emacs")),
666 ],
667 );
668
669 assert_eq!(
670 count_named(&new, "atuin"),
671 2,
672 "one of the bindings was dropped"
673 );
674 assert!(warning.is_some(), "sharing a name should warn");
675 }
676
677 #[test]
679 fn reassigning_the_same_binding_replaces_it_without_warning() {
680 let old = Config::default();
681 let mut new = old.clone();
682
683 let atuin = || {
684 keybinding(
685 Some("atuin"),
686 "control",
687 "char_r",
688 Value::test_string("emacs"),
689 )
690 };
691 assign(&mut new, &old, vec![atuin()]);
692 let len = new.keybindings.len();
693
694 let warning = assign(&mut new, &old, vec![atuin()]);
695 assert_eq!(
696 new.keybindings.len(),
697 len,
698 "re-sourcing duplicated the binding"
699 );
700 assert!(
701 warning.is_none(),
702 "an identical re-assignment must not warn"
703 );
704 }
705
706 #[test]
708 fn a_new_event_on_the_same_key_replaces_the_binding() {
709 let old = Config::default();
710 let mut new = old.clone();
711
712 assign(
713 &mut new,
714 &old,
715 vec![keybinding(
716 Some("atuin"),
717 "control",
718 "char_r",
719 Value::test_string("emacs"),
720 )],
721 );
722 let len = new.keybindings.len();
723
724 let mut updated = keybinding(
725 Some("atuin"),
726 "control",
727 "char_r",
728 Value::test_string("emacs"),
729 );
730 updated.event = Value::test_string("marker");
731 assign(&mut new, &old, vec![updated]);
732
733 assert_eq!(new.keybindings.len(), len);
734 let event = new
735 .keybindings
736 .iter()
737 .rev()
738 .find(|kb| {
739 kb.name
740 .as_ref()
741 .is_some_and(|n| n.to_expanded_string("", &new) == "atuin")
742 })
743 .map(|kb| kb.event.clone());
744 assert_eq!(
745 event,
746 Some(Value::test_string("marker")),
747 "event was not updated"
748 );
749 }
750
751 #[test]
753 fn a_bare_mode_and_its_singleton_list_merge_into_one_binding() {
754 let old = Config::default();
755 let mut new = old.clone();
756
757 assign(
758 &mut new,
759 &old,
760 vec![keybinding(
761 Some("atuin"),
762 "control",
763 "char_r",
764 Value::test_string("emacs"),
765 )],
766 );
767 let warning = assign(
768 &mut new,
769 &old,
770 vec![keybinding(
771 Some("atuin"),
772 "control",
773 "char_r",
774 Value::test_list(vec![Value::test_string("emacs")]),
775 )],
776 );
777
778 assert_eq!(
779 count_named(&new, "atuin"),
780 1,
781 "the mode spellings did not merge"
782 );
783 assert!(warning.is_none());
784 }
785
786 #[test]
789 fn the_same_binding_twice_in_one_assignment_is_stored_once() {
790 let old = Config::default();
791 let mut new = old.clone();
792
793 let atuin = || {
794 keybinding(
795 Some("atuin"),
796 "control",
797 "char_r",
798 Value::test_string("emacs"),
799 )
800 };
801 assign(&mut new, &old, vec![atuin(), atuin()]);
802
803 assert_eq!(count_named(&new, "atuin"), 1, "the duplicate was appended");
804 }
805
806 #[test]
810 fn a_named_binding_with_a_new_key_replaces_in_place() {
811 let old = Config::default();
812 let mut new = old.clone();
813
814 assign(
815 &mut new,
816 &old,
817 vec![keybinding(
818 Some("atuin"),
819 "control",
820 "char_r",
821 Value::test_string("emacs"),
822 )],
823 );
824 let len = new.keybindings.len();
825 let index = new
826 .keybindings
827 .iter()
828 .position(|kb| {
829 kb.name
830 .as_ref()
831 .is_some_and(|n| n.to_expanded_string("", &new) == "atuin")
832 })
833 .expect("binding was added");
834
835 let warning = assign(
836 &mut new,
837 &old,
838 vec![keybinding(
839 Some("atuin"),
840 "none",
841 "up",
842 Value::test_string("emacs"),
843 )],
844 );
845
846 assert_eq!(new.keybindings.len(), len, "re-keying must not append");
847 assert_eq!(
848 new.keybindings[index].keycode,
849 Value::test_string("up"),
850 "the binding was not re-keyed in place"
851 );
852 assert!(warning.is_none(), "re-keying a lone name must not warn");
853 }
854
855 #[test]
857 fn a_named_binding_with_a_changed_mode_replaces_instead_of_appending() {
858 let old = Config::default();
859 let mut new = old.clone();
860
861 assign(
862 &mut new,
863 &old,
864 vec![keybinding(
865 Some("atuin"),
866 "control",
867 "char_r",
868 Value::test_string("emacs"),
869 )],
870 );
871 let warning = assign(
872 &mut new,
873 &old,
874 vec![keybinding(
875 Some("atuin"),
876 "control",
877 "char_r",
878 Value::test_list(vec![
879 Value::test_string("vi_normal"),
880 Value::test_string("vi_insert"),
881 ]),
882 )],
883 );
884
885 assert_eq!(count_named(&new, "atuin"), 1, "the mode change appended");
886 assert!(warning.is_none());
887 }
888
889 #[test]
891 fn assigning_an_empty_list_clears_the_keybindings() {
892 let old = Config::default();
893 let mut new = old.clone();
894
895 assign(&mut new, &old, vec![]);
896 assert!(new.keybindings.is_empty(), "`= []` should reset the list");
897 }
898}