1use crate::Chip;
2
3use crate::template::{GeneratorOption, GeneratorOptionItem};
4
5#[derive(Debug)]
6pub struct ActiveConfiguration {
7 pub chip: Chip,
9 pub selected: Vec<usize>,
11 pub options: Vec<GeneratorOptionItem>,
13 pub flat_options: Vec<GeneratorOption>,
15}
16
17pub fn flatten_options(options: &[GeneratorOptionItem]) -> Vec<GeneratorOption> {
18 options
19 .iter()
20 .flat_map(|item| match item {
21 GeneratorOptionItem::Category(category) => flatten_options(&category.options),
22 GeneratorOptionItem::Option(option) => vec![option.clone()],
23 })
24 .collect()
25}
26
27impl ActiveConfiguration {
28 pub fn is_group_selected(&self, group: &str) -> bool {
29 self.selected
30 .iter()
31 .any(|s| self.flat_options[*s].selection_group == group)
32 }
33
34 pub fn is_selected(&self, option: &str) -> bool {
35 self.selected_index(option).is_some()
36 }
37
38 pub fn selected_index(&self, option: &str) -> Option<usize> {
39 self.selected
40 .iter()
41 .position(|s| self.flat_options[*s].name == option)
42 }
43
44 fn deselect_group(selected: &mut Vec<usize>, options: &[GeneratorOption], group: &str) -> bool {
47 if group.is_empty() {
49 return true;
50 }
51
52 if !selected.iter().copied().all(|s| {
54 let o = &options[s];
55 if o.selection_group == group {
56 Self::can_be_disabled_impl(selected, options, s, true)
59 } else {
60 true
61 }
62 }) {
63 return false;
64 }
65
66 selected.retain(|s| options[*s].selection_group != group);
67
68 true
69 }
70
71 pub fn select(&mut self, option: &str) {
72 let (index, _o) = find_option(option, &self.flat_options, self.chip).unwrap();
73 self.select_idx(index);
74 }
75
76 pub fn select_idx(&mut self, idx: usize) {
86 let o = self.flat_options[idx].clone();
87
88 for req in &o.requires {
90 if req.starts_with('!') {
91 continue;
92 }
93 if self.is_selected(req) {
94 continue;
95 }
96 if Self::group_exists(req, &self.flat_options) && self.is_group_selected(req) {
97 continue;
98 }
99 return;
100 }
101
102 if self.selected.contains(&idx) {
104 return;
105 }
106
107 if !Self::deselect_group(&mut self.selected, &self.flat_options, &o.selection_group) {
109 return;
110 }
111
112 for req in &o.requires {
114 let Some(disables) = req.strip_prefix('!') else {
115 continue;
116 };
117 if let Some(pos) = self.selected_index(disables) {
118 self.selected.swap_remove(pos);
119 }
120 }
121
122 self.selected.push(idx);
123
124 self.drop_unsatisfied();
126 }
127
128 pub fn deselect_idx(&mut self, idx: usize) {
134 let Some(pos) = self.selected.iter().position(|&s| s == idx) else {
135 return;
136 };
137 self.selected.swap_remove(pos);
138 self.drop_unsatisfied();
139 }
140
141 fn drop_unsatisfied(&mut self) {
144 loop {
145 let victim = self.selected.iter().position(|&idx| {
146 let opt = &self.flat_options[idx];
147 !self.requirements_met(&opt.requires)
148 });
149 match victim {
150 Some(pos) => {
151 self.selected.swap_remove(pos);
152 }
153 None => return,
154 }
155 }
156 }
157
158 pub fn would_force_deselect(&self, option: &GeneratorOption) -> Vec<String> {
175 let option_idx = find_option(&option.name, &self.flat_options, self.chip).map(|(i, _)| i);
176
177 let currently_selected = option_idx
178 .map(|idx| self.selected.contains(&idx))
179 .unwrap_or(false);
180
181 if !currently_selected && !self.is_option_toggleable(option) {
182 return Vec::new();
183 }
184
185 let mut simulated = self.selected.clone();
186 if currently_selected {
187 if let Some(idx) = option_idx {
189 if let Some(pos) = simulated.iter().position(|&i| i == idx) {
190 simulated.swap_remove(pos);
191 }
192 }
193 } else {
194 if !option.selection_group.is_empty() {
196 simulated.retain(|idx| {
197 let o = &self.flat_options[*idx];
198 o.selection_group != option.selection_group
199 });
200 }
201
202 for req in &option.requires {
204 let Some(disables) = req.strip_prefix('!') else {
205 continue;
206 };
207 if let Some(pos) = simulated
208 .iter()
209 .position(|&i| self.flat_options[i].name == disables)
210 {
211 simulated.swap_remove(pos);
212 }
213 }
214
215 if let Some(idx) = option_idx {
217 if !simulated.contains(&idx) {
218 simulated.push(idx);
219 }
220 }
221 }
222
223 loop {
225 let victim = simulated.iter().position(|&idx| {
226 let opt = &self.flat_options[idx];
227 !Self::requirements_met_against(opt, &simulated, &self.flat_options)
228 });
229 match victim {
230 Some(pos) => {
231 simulated.swap_remove(pos);
232 }
233 None => break,
234 }
235 }
236
237 self.selected
240 .iter()
241 .copied()
242 .filter(|idx| !simulated.contains(idx) && Some(*idx) != option_idx)
243 .map(|idx| self.flat_options[idx].name.clone())
244 .collect()
245 }
246
247 fn requirements_met_against(
249 option: &GeneratorOption,
250 selected: &[usize],
251 flat_options: &[GeneratorOption],
252 ) -> bool {
253 for requirement in &option.requires {
254 let (key, expected) = if let Some(rest) = requirement.strip_prefix('!') {
255 (rest, false)
256 } else {
257 (requirement.as_str(), true)
258 };
259
260 let is_selected = selected.iter().any(|s| flat_options[*s].name == key);
261 if is_selected == expected {
262 continue;
263 }
264
265 let is_group = flat_options.iter().any(|o| o.selection_group == key);
266 if is_group {
267 let group_selected = selected
268 .iter()
269 .any(|s| flat_options[*s].selection_group == key);
270 if group_selected == expected {
271 continue;
272 }
273 }
274
275 return false;
276 }
277 true
278 }
279
280 pub fn is_active(&self, item: &GeneratorOptionItem) -> bool {
291 match item {
292 GeneratorOptionItem::Category(category) => {
293 if !self.requirements_met(&category.requires) {
294 return false;
295 }
296 for sub in category.options.iter() {
297 if self.is_active(sub) {
298 return true;
299 }
300 }
301 false
302 }
303 GeneratorOptionItem::Option(option) => self.is_option_toggleable(option),
304 }
305 }
306
307 fn requirements_met(&self, requires: &[String]) -> bool {
318 for requirement in requires {
319 let (key, expected) = if let Some(requirement) = requirement.strip_prefix('!') {
320 (requirement, false)
321 } else {
322 (requirement.as_str(), true)
323 };
324
325 if self.is_selected(key) == expected {
327 continue;
328 }
329
330 let is_group = Self::group_exists(key, &self.flat_options);
332 if is_group && self.is_group_selected(key) == expected {
333 continue;
334 }
335
336 return false;
337 }
338
339 true
340 }
341
342 pub fn is_option_active(&self, option: &GeneratorOption) -> bool {
356 if !option.chips.is_empty() && !option.chips.contains(&self.chip) {
357 return false;
358 }
359
360 if !self.requirements_met(&option.requires) {
361 return false;
362 }
363
364 for selected in self.selected.iter().copied() {
365 let Some(selected_option) = self.flat_options.get(selected) else {
366 ratatui::restore();
367 panic!("selected option not found: {selected}");
368 };
369
370 for requirement in selected_option.requires.iter() {
371 if let Some(requirement) = requirement.strip_prefix('!') {
372 if requirement == option.name {
373 return false;
374 }
375 }
376 }
377 }
378
379 true
380 }
381
382 pub fn is_option_toggleable(&self, option: &GeneratorOption) -> bool {
394 if !option.chips.is_empty() && !option.chips.contains(&self.chip) {
395 return false;
396 }
397
398 for req in &option.requires {
399 if req.starts_with('!') {
400 continue;
401 }
402 if self.is_selected(req) {
403 continue;
404 }
405 if Self::group_exists(req, &self.flat_options) && self.is_group_selected(req) {
406 continue;
407 }
408 return false;
409 }
410
411 true
412 }
413
414 pub fn can_be_disabled(&self, option: &str) -> bool {
416 let (option, _) = find_option(option, &self.flat_options, self.chip).unwrap();
417 Self::can_be_disabled_impl(&self.selected, &self.flat_options, option, false)
418 }
419
420 fn can_be_disabled_impl(
421 selected: &[usize],
422 options: &[GeneratorOption],
423 option: usize,
424 allow_deselecting_group: bool,
425 ) -> bool {
426 let op = &options[option];
427 for selected in selected.iter().copied() {
428 let selected_option = &options[selected];
429 if selected_option
430 .requires
431 .iter()
432 .any(|o| o == &op.name || (o == &op.selection_group && !allow_deselecting_group))
433 {
434 return false;
435 }
436 }
437 true
438 }
439
440 pub fn collect_relationships<'a>(
441 &'a self,
442 option: &'a GeneratorOptionItem,
443 ) -> Relationships<'a> {
444 let mut requires = Vec::new();
445 let mut required_by = Vec::new();
446 let mut disabled_by = Vec::new();
447
448 self.selected.iter().for_each(|opt| {
449 let opt = &self.flat_options[*opt];
450 for o in opt.requires.iter() {
451 if let Some(disables) = o.strip_prefix("!") {
452 if disables == option.name() {
453 disabled_by.push(opt.name.as_str());
454 }
455 } else if o == option.name() {
456 required_by.push(opt.name.as_str());
457 }
458 }
459 });
460 for req in option.requires() {
461 if let Some(disables) = req.strip_prefix("!") {
462 if self.is_selected(disables) {
463 disabled_by.push(disables);
464 }
465 } else {
466 requires.push(req.as_str());
467 }
468 }
469
470 Relationships {
471 requires,
472 required_by,
473 disabled_by,
474 }
475 }
476
477 fn group_exists(key: &str, options: &[GeneratorOption]) -> bool {
478 options.iter().any(|o| o.selection_group == key)
479 }
480}
481
482pub struct Relationships<'a> {
483 pub requires: Vec<&'a str>,
484 pub required_by: Vec<&'a str>,
485 pub disabled_by: Vec<&'a str>,
486}
487
488pub fn find_option<'c>(
489 option: &str,
490 options: &'c [GeneratorOption],
491 chip: Chip,
492) -> Option<(usize, &'c GeneratorOption)> {
493 options
494 .iter()
495 .enumerate()
496 .find(|(_, opt)| opt.name == option && (opt.chips.is_empty() || opt.chips.contains(&chip)))
497}
498
499#[cfg(test)]
500mod test {
501 use crate::Chip;
502
503 use crate::{
504 config::*,
505 template::{GeneratorOption, GeneratorOptionCategory, GeneratorOptionItem},
506 };
507
508 #[test]
509 fn required_by_and_requires_pick_the_right_options() {
510 let options = vec![
511 GeneratorOptionItem::Option(GeneratorOption {
512 name: "option1".to_string(),
513 display_name: "Foobar".to_string(),
514 selection_group: "".to_string(),
515 help: "".to_string(),
516 chips: vec![Chip::Esp32],
517 requires: vec!["option2".to_string()],
518 }),
519 GeneratorOptionItem::Option(GeneratorOption {
520 name: "option2".to_string(),
521 display_name: "Barfoo".to_string(),
522 selection_group: "".to_string(),
523 help: "".to_string(),
524 chips: vec![Chip::Esp32],
525 requires: vec![],
526 }),
527 ];
528 let active = ActiveConfiguration {
529 chip: Chip::Esp32,
530 selected: vec![0],
531 flat_options: flatten_options(&options),
532 options,
533 };
534
535 let rels = active.collect_relationships(&active.options[0]);
536 assert_eq!(rels.requires, &["option2"]);
537 assert_eq!(rels.required_by, <&[&str]>::default());
538
539 let rels = active.collect_relationships(&active.options[1]);
540 assert_eq!(rels.requires, <&[&str]>::default());
541 assert_eq!(rels.required_by, &["option1"]);
542 }
543
544 #[test]
545 fn selecting_one_in_group_deselects_other() {
546 let options = vec![
547 GeneratorOptionItem::Option(GeneratorOption {
548 name: "option1".to_string(),
549 display_name: "Foobar".to_string(),
550 selection_group: "group".to_string(),
551 help: "".to_string(),
552 chips: vec![Chip::Esp32],
553 requires: vec![],
554 }),
555 GeneratorOptionItem::Option(GeneratorOption {
556 name: "option2".to_string(),
557 display_name: "Barfoo".to_string(),
558 selection_group: "group".to_string(),
559 help: "".to_string(),
560 chips: vec![Chip::Esp32],
561 requires: vec![],
562 }),
563 GeneratorOptionItem::Option(GeneratorOption {
564 name: "option3".to_string(),
565 display_name: "Prevents deselecting option2".to_string(),
566 selection_group: "".to_string(),
567 help: "".to_string(),
568 chips: vec![Chip::Esp32],
569 requires: vec!["option2".to_string()],
570 }),
571 ];
572 let mut active = ActiveConfiguration {
573 chip: Chip::Esp32,
574 selected: vec![],
575 flat_options: flatten_options(&options),
576 options,
577 };
578
579 active.select("option1");
580 assert_eq!(active.selected, &[0]);
581
582 active.select("option2");
583 assert_eq!(active.selected, &[1]);
584
585 active.select("option3");
587 assert_eq!(active.selected, &[1, 2]);
588
589 active.select("option1");
590 assert_eq!(active.selected, &[1, 2]);
591 }
592
593 #[test]
594 fn depending_on_group_allows_changing_group_option() {
595 let options = vec![
596 GeneratorOptionItem::Category(GeneratorOptionCategory {
597 name: "group-options".to_string(),
598 display_name: "Group options".to_string(),
599 help: "".to_string(),
600 requires: vec![],
601 options: vec![
602 GeneratorOptionItem::Option(GeneratorOption {
603 name: "option1".to_string(),
604 display_name: "Foobar".to_string(),
605 selection_group: "group".to_string(),
606 help: "".to_string(),
607 chips: vec![Chip::Esp32],
608 requires: vec![],
609 }),
610 GeneratorOptionItem::Option(GeneratorOption {
611 name: "option2".to_string(),
612 display_name: "Barfoo".to_string(),
613 selection_group: "group".to_string(),
614 help: "".to_string(),
615 chips: vec![Chip::Esp32],
616 requires: vec![],
617 }),
618 ],
619 }),
620 GeneratorOptionItem::Option(GeneratorOption {
621 name: "option3".to_string(),
622 display_name: "Requires any in group to be selected".to_string(),
623 selection_group: "".to_string(),
624 help: "".to_string(),
625 chips: vec![Chip::Esp32],
626 requires: vec!["group".to_string()],
627 }),
628 GeneratorOptionItem::Option(GeneratorOption {
629 name: "option4".to_string(),
630 display_name: "Extra option that depends on something".to_string(),
631 selection_group: "".to_string(),
632 help: "".to_string(),
633 chips: vec![Chip::Esp32],
634 requires: vec!["option3".to_string()],
635 }),
636 ];
637 let mut active = ActiveConfiguration {
638 chip: Chip::Esp32,
639 selected: vec![],
640 flat_options: flatten_options(&options),
641 options,
642 };
643
644 active.select("option3");
646 assert_eq!(active.selected, empty());
647
648 active.select("option1");
649 assert_eq!(active.selected, &[0]);
650
651 active.select("option3");
652 assert_eq!(active.selected, &[0, 2]);
653
654 active.select("option4");
658 assert_eq!(active.selected, &[0, 2, 3]);
659
660 active.select("option2");
661 assert_eq!(active.selected, &[2, 3, 1]);
662 }
663
664 #[test]
665 fn depending_on_group_prevents_deselecting() {
666 let options = vec![
667 GeneratorOptionItem::Option(GeneratorOption {
668 name: "option1".to_string(),
669 display_name: "Foobar".to_string(),
670 selection_group: "group".to_string(),
671 help: "".to_string(),
672 chips: vec![Chip::Esp32],
673 requires: vec![],
674 }),
675 GeneratorOptionItem::Option(GeneratorOption {
676 name: "option2".to_string(),
677 display_name: "Barfoo".to_string(),
678 selection_group: "".to_string(),
679 help: "".to_string(),
680 chips: vec![Chip::Esp32],
681 requires: vec!["group".to_string()],
682 }),
683 ];
684 let mut active = ActiveConfiguration {
685 chip: Chip::Esp32,
686 selected: vec![],
687 flat_options: flatten_options(&options),
688 options,
689 };
690
691 active.select("option1");
692 active.select("option2");
693
694 assert!(!active.can_be_disabled("option1"));
696 }
697
698 #[test]
699 fn negative_requirement_force_deselects_instead_of_blocking() {
700 let options = vec![
704 GeneratorOptionItem::Option(GeneratorOption {
705 name: "option1".to_string(),
706 display_name: "Foobar".to_string(),
707 selection_group: "".to_string(),
708 help: "".to_string(),
709 chips: vec![Chip::Esp32],
710 requires: vec![],
711 }),
712 GeneratorOptionItem::Option(GeneratorOption {
713 name: "option2".to_string(),
714 display_name: "Barfoo".to_string(),
715 selection_group: "".to_string(),
716 help: "".to_string(),
717 chips: vec![Chip::Esp32],
718 requires: vec!["!option1".to_string()],
719 }),
720 ];
721 let mut active = ActiveConfiguration {
722 chip: Chip::Esp32,
723 selected: vec![],
724 flat_options: flatten_options(&options),
725 options,
726 };
727
728 active.select("option1");
729 let opt2 = active.flat_options[1].clone();
730 assert!(!active.is_option_active(&opt2));
732 assert!(active.is_option_toggleable(&opt2));
734 assert_eq!(
735 active.would_force_deselect(&opt2),
736 vec!["option1".to_string()]
737 );
738
739 active.select("option2");
740 assert!(active.is_selected("option2"));
741 assert!(!active.is_selected("option1"));
742 }
743
744 #[test]
745 fn select_cascade_evicts_dependents_of_deselected_option() {
746 let options = vec![
753 GeneratorOptionItem::Option(GeneratorOption {
754 name: "probe-rs".to_string(),
755 display_name: "probe-rs".to_string(),
756 selection_group: "".to_string(),
757 help: "".to_string(),
758 chips: vec![Chip::Esp32],
759 requires: vec![],
760 }),
761 GeneratorOptionItem::Option(GeneratorOption {
762 name: "log".to_string(),
763 display_name: "log".to_string(),
764 selection_group: "".to_string(),
765 help: "".to_string(),
766 chips: vec![Chip::Esp32],
767 requires: vec!["!probe-rs".to_string()],
768 }),
769 GeneratorOptionItem::Option(GeneratorOption {
770 name: "embedded-test".to_string(),
771 display_name: "embedded-test".to_string(),
772 selection_group: "".to_string(),
773 help: "".to_string(),
774 chips: vec![Chip::Esp32],
775 requires: vec!["log".to_string()],
776 }),
777 GeneratorOptionItem::Option(GeneratorOption {
778 name: "panic-rtt-target".to_string(),
779 display_name: "panic-rtt-target".to_string(),
780 selection_group: "".to_string(),
781 help: "".to_string(),
782 chips: vec![Chip::Esp32],
783 requires: vec!["probe-rs".to_string()],
784 }),
785 GeneratorOptionItem::Option(GeneratorOption {
786 name: "wifi".to_string(),
787 display_name: "wifi".to_string(),
788 selection_group: "".to_string(),
789 help: "".to_string(),
790 chips: vec![Chip::Esp32],
791 requires: vec![],
792 }),
793 ];
794 let mut active = ActiveConfiguration {
795 chip: Chip::Esp32,
796 selected: vec![],
797 flat_options: flatten_options(&options),
798 options,
799 };
800
801 active.select("log");
802 active.select("embedded-test");
803 active.select("wifi");
804
805 let probe_rs = active.flat_options[0].clone();
808 let mut evicted = active.would_force_deselect(&probe_rs);
809 evicted.sort();
810 assert_eq!(
811 evicted,
812 vec!["embedded-test".to_string(), "log".to_string()]
813 );
814
815 active.select("probe-rs");
816 assert!(active.is_selected("probe-rs"));
817 assert!(!active.is_selected("log"));
818 assert!(!active.is_selected("embedded-test"));
819 assert!(active.is_selected("wifi"));
820
821 active.select("panic-rtt-target");
825 let probe_rs = active.flat_options[0].clone();
826 let mut evicted = active.would_force_deselect(&probe_rs);
827 evicted.sort();
828 assert_eq!(evicted, vec!["panic-rtt-target".to_string()]);
829 assert!(!evicted.contains(&"probe-rs".to_string())); assert!(!evicted.contains(&"wifi".to_string())); let (probe_rs_flat_idx, _) =
833 find_option("probe-rs", &active.flat_options, Chip::Esp32).unwrap();
834 active.deselect_idx(probe_rs_flat_idx);
835 assert!(!active.is_selected("probe-rs"));
836 assert!(!active.is_selected("panic-rtt-target"));
837 assert!(!active.is_selected("log"));
839 assert!(!active.is_selected("embedded-test"));
840 assert!(active.is_selected("wifi"));
841
842 let options = vec![
854 GeneratorOptionItem::Option(GeneratorOption {
855 name: "victim".to_string(),
856 display_name: "victim".to_string(),
857 selection_group: "".to_string(),
858 help: "".to_string(),
859 chips: vec![Chip::Esp32],
860 requires: vec![],
861 }),
862 GeneratorOptionItem::Option(GeneratorOption {
863 name: "wrong-chip".to_string(),
864 display_name: "wrong-chip".to_string(),
865 selection_group: "".to_string(),
866 help: "".to_string(),
867 chips: vec![Chip::Esp32c6],
868 requires: vec!["!victim".to_string()],
869 }),
870 GeneratorOptionItem::Option(GeneratorOption {
871 name: "unmet-pos".to_string(),
872 display_name: "unmet-pos".to_string(),
873 selection_group: "".to_string(),
874 help: "".to_string(),
875 chips: vec![Chip::Esp32],
876 requires: vec!["missing".to_string(), "!victim".to_string()],
877 }),
878 GeneratorOptionItem::Option(GeneratorOption {
879 name: "neg-conflict".to_string(),
880 display_name: "neg-conflict".to_string(),
881 selection_group: "".to_string(),
882 help: "".to_string(),
883 chips: vec![Chip::Esp32],
884 requires: vec!["!victim".to_string()],
885 }),
886 ];
887 let mut active = ActiveConfiguration {
888 chip: Chip::Esp32,
889 selected: vec![],
890 flat_options: flatten_options(&options),
891 options,
892 };
893 active.select("victim");
894
895 let wrong_chip = active
896 .flat_options
897 .iter()
898 .find(|o| o.name == "wrong-chip")
899 .cloned()
900 .unwrap();
901 let unmet_pos = active
902 .flat_options
903 .iter()
904 .find(|o| o.name == "unmet-pos")
905 .cloned()
906 .unwrap();
907 let neg_conflict = active
908 .flat_options
909 .iter()
910 .find(|o| o.name == "neg-conflict")
911 .cloned()
912 .unwrap();
913
914 assert!(active.would_force_deselect(&wrong_chip).is_empty());
915 assert!(active.would_force_deselect(&unmet_pos).is_empty());
916 assert_eq!(
918 active.would_force_deselect(&neg_conflict),
919 vec!["victim".to_string()]
920 );
921 }
922
923 fn empty() -> &'static [usize] {
924 &[]
925 }
926}