1use crossterm::event::KeyEvent;
10use ratatui::layout::Rect;
11
12use crate::cache::CacheConfig;
13use crate::change::Change;
14use crate::cli::Command;
15use crate::lock::NestedInput;
16
17use super::completions::uri_completion_items;
18use super::components::confirm::ConfirmAction;
19use super::components::input::{Input, InputAction, InputResult, InputState};
20use super::components::list::{ListAction, ListResult, ListState};
21use super::workflow::{AddStep, ConfirmResultAction, FollowStep, WorkflowData};
22
23pub use super::workflow::{AppResult, MultiSelectResultData, SingleSelectResult, UpdateResult};
25
26const MAX_LIST_HEIGHT: u16 = 12;
27
28#[derive(Debug, Clone)]
29pub struct App {
30 context: String,
31 flake_text: String,
32 show_diff: bool,
33 cache_config: CacheConfig,
34 screen: Screen,
35 data: WorkflowData,
36}
37
38#[derive(Debug, Clone)]
40pub enum Screen {
41 Input(InputScreen),
42 List(ListScreen),
43 Confirm(ConfirmScreen),
44}
45
46#[derive(Debug, Clone)]
48pub struct InputScreen {
49 pub state: InputState,
50 pub prompt: String,
51 pub label: Option<String>,
52}
53
54#[derive(Debug, Clone)]
56pub struct ListScreen {
57 pub state: ListState,
58 pub prompt: String,
59}
60
61impl ListScreen {
62 pub fn single(items: Vec<String>, prompt: impl Into<String>, show_diff: bool) -> Self {
63 Self {
64 state: ListState::new(items, false, show_diff),
65 prompt: prompt.into(),
66 }
67 }
68
69 pub fn multi(items: Vec<String>, prompt: impl Into<String>, show_diff: bool) -> Self {
70 Self {
71 state: ListState::new(items, true, show_diff),
72 prompt: prompt.into(),
73 }
74 }
75}
76
77#[derive(Debug, Clone)]
79pub struct ConfirmScreen {
80 pub diff: String,
81}
82
83impl App {
84 pub fn add(
89 context: impl Into<String>,
90 flake_text: impl Into<String>,
91 prefill_uri: Option<&str>,
92 cache_config: CacheConfig,
93 ) -> Self {
94 let completions = uri_completion_items(None, &cache_config);
95 Self {
96 context: context.into(),
97 flake_text: flake_text.into(),
98 show_diff: false,
99 cache_config,
100 screen: Screen::Input(InputScreen {
101 state: InputState::with_completions(prefill_uri, completions),
102 prompt: "Enter flake URI".into(),
103 label: None,
104 }),
105 data: WorkflowData::Add {
106 step: AddStep::Uri,
107 uri: None,
108 id: None,
109 },
110 }
111 }
112
113 pub fn change(
118 context: impl Into<String>,
119 flake_text: impl Into<String>,
120 inputs: Vec<(String, String)>,
121 cache_config: CacheConfig,
122 ) -> Self {
123 let input_ids: Vec<String> = inputs.iter().map(|(id, _)| id.clone()).collect();
124 let input_uris: std::collections::HashMap<String, String> = inputs.into_iter().collect();
125 Self {
126 context: context.into(),
127 flake_text: flake_text.into(),
128 show_diff: false,
129 cache_config,
130 screen: Screen::List(ListScreen::single(
131 input_ids.clone(),
132 "Select input to change",
133 false,
134 )),
135 data: WorkflowData::Change {
136 selected_input: None,
137 uri: None,
138 input_uris,
139 all_inputs: input_ids,
140 },
141 }
142 }
143
144 pub fn remove(
146 context: impl Into<String>,
147 flake_text: impl Into<String>,
148 inputs: Vec<String>,
149 ) -> Self {
150 Self {
151 context: context.into(),
152 flake_text: flake_text.into(),
153 show_diff: false,
154 cache_config: CacheConfig::default(),
155 screen: Screen::List(ListScreen::multi(
156 inputs.clone(),
157 "Select inputs to remove",
158 false,
159 )),
160 data: WorkflowData::Remove {
161 selected_inputs: Vec::new(),
162 all_inputs: inputs,
163 },
164 }
165 }
166
167 pub fn change_uri(
171 context: impl Into<String>,
172 flake_text: impl Into<String>,
173 id: impl Into<String>,
174 current_uri: Option<&str>,
175 show_diff: bool,
176 cache_config: CacheConfig,
177 ) -> Self {
178 let id_string = id.into();
179 let completions = uri_completion_items(Some(&id_string), &cache_config);
180 Self {
181 context: context.into(),
182 flake_text: flake_text.into(),
183 show_diff,
184 cache_config,
185 screen: Screen::Input(InputScreen {
186 state: InputState::with_completions(current_uri, completions),
187 prompt: format!("for {}", id_string),
188 label: Some("URI".into()),
189 }),
190 data: WorkflowData::Change {
191 selected_input: Some(id_string),
192 uri: None,
193 input_uris: std::collections::HashMap::new(),
194 all_inputs: Vec::new(),
195 },
196 }
197 }
198
199 pub fn select_one(
201 context: impl Into<String>,
202 prompt: impl Into<String>,
203 items: Vec<String>,
204 initial_diff: bool,
205 ) -> Self {
206 Self {
207 context: context.into(),
208 flake_text: String::new(),
209 show_diff: initial_diff,
210 cache_config: CacheConfig::default(),
211 screen: Screen::List(ListScreen::single(items, prompt, initial_diff)),
212 data: WorkflowData::SelectOne {
213 selected_input: None,
214 },
215 }
216 }
217
218 pub fn select_many(
220 context: impl Into<String>,
221 prompt: impl Into<String>,
222 items: Vec<String>,
223 initial_diff: bool,
224 ) -> Self {
225 Self {
226 context: context.into(),
227 flake_text: String::new(),
228 show_diff: initial_diff,
229 cache_config: CacheConfig::default(),
230 screen: Screen::List(ListScreen::multi(items, prompt, initial_diff)),
231 data: WorkflowData::SelectMany {
232 selected_inputs: Vec::new(),
233 },
234 }
235 }
236
237 pub fn confirm(context: impl Into<String>, diff: impl Into<String>) -> Self {
239 Self {
240 context: context.into(),
241 flake_text: String::new(),
242 show_diff: true,
243 cache_config: CacheConfig::default(),
244 screen: Screen::Confirm(ConfirmScreen { diff: diff.into() }),
245 data: WorkflowData::ConfirmOnly { action: None },
246 }
247 }
248
249 pub fn follow(
255 context: impl Into<String>,
256 flake_text: impl Into<String>,
257 nested_inputs: Vec<NestedInput>,
258 top_level_inputs: Vec<String>,
259 ) -> Self {
260 let display_items: Vec<String> = nested_inputs
262 .iter()
263 .map(|i| i.to_display_string())
264 .collect();
265 Self {
266 context: context.into(),
267 flake_text: flake_text.into(),
268 show_diff: false,
269 cache_config: CacheConfig::default(),
270 screen: Screen::List(ListScreen::single(
271 display_items,
272 "Select input to add follows",
273 false,
274 )),
275 data: WorkflowData::Follow {
276 step: FollowStep::SelectInput,
277 selected_input: None,
278 selected_target: None,
279 nested_inputs,
280 top_level_inputs,
281 },
282 }
283 }
284
285 pub fn follow_target(
289 context: impl Into<String>,
290 flake_text: impl Into<String>,
291 input: impl Into<String>,
292 top_level_inputs: Vec<String>,
293 ) -> Self {
294 let input = input.into();
295 Self {
296 context: context.into(),
297 flake_text: flake_text.into(),
298 show_diff: false,
299 cache_config: CacheConfig::default(),
300 screen: Screen::List(ListScreen::single(
301 top_level_inputs.clone(),
302 format!("Select target for {input}"),
303 false,
304 )),
305 data: WorkflowData::Follow {
306 step: FollowStep::SelectTarget,
307 selected_input: Some(input),
308 selected_target: None,
309 nested_inputs: Vec::<NestedInput>::new(),
310 top_level_inputs,
311 },
312 }
313 }
314
315 pub fn from_command(
328 command: &Command,
329 flake_text: impl Into<String>,
330 inputs: Vec<(String, String)>,
331 diff: bool,
332 cache_config: CacheConfig,
333 ) -> Option<Self> {
334 let flake_text = flake_text.into();
335 let input_ids: Vec<String> = inputs.iter().map(|(id, _)| id.clone()).collect();
336
337 match command {
338 Command::Add { id, uri, .. } => {
340 if id.is_some() && uri.is_some() {
341 None } else {
343 let prefill = id.as_deref();
345 Some(Self::add("Add", flake_text, prefill, cache_config).with_diff(diff))
346 }
347 }
348
349 Command::Remove { id } => {
351 if id.is_some() {
352 None
353 } else {
354 Some(Self::remove("Remove", flake_text, input_ids).with_diff(diff))
355 }
356 }
357
358 Command::Change { id, uri, .. } => {
360 if id.is_some() && uri.is_some() {
361 None } else if let Some(id) = id {
363 let current_uri = inputs
365 .iter()
366 .find(|(i, _)| i == id)
367 .map(|(_, u)| u.as_str());
368 Some(Self::change_uri(
369 "Change",
370 flake_text,
371 id,
372 current_uri,
373 diff,
374 cache_config,
375 ))
376 } else {
377 Some(Self::change("Change", flake_text, inputs, cache_config).with_diff(diff))
379 }
380 }
381
382 Command::Pin { id, .. } => {
384 if id.is_some() {
385 None
386 } else {
387 Some(Self::select_one(
388 "Pin",
389 "Select input to pin",
390 input_ids,
391 diff,
392 ))
393 }
394 }
395
396 Command::Unpin { id } => {
398 if id.is_some() {
399 None
400 } else {
401 Some(Self::select_one(
402 "Unpin",
403 "Select input to unpin",
404 input_ids,
405 diff,
406 ))
407 }
408 }
409
410 Command::Update { id, .. } => {
412 if id.is_some() {
413 None
414 } else {
415 Some(Self::select_many(
416 "Update",
417 "Space select, U all, ^D diff",
418 input_ids,
419 diff,
420 ))
421 }
422 }
423
424 Command::List { .. }
426 | Command::Follow { .. }
427 | Command::AddFollow { .. }
428 | Command::Toggle { .. }
429 | Command::Config { .. } => None,
430 }
431 }
432
433 pub fn show_diff(&self) -> bool {
434 self.show_diff
435 }
436
437 pub fn screen(&self) -> &Screen {
438 &self.screen
439 }
440
441 pub fn context(&self) -> &str {
442 &self.context
443 }
444
445 pub fn pending_diff(&self) -> String {
451 let change = self.build_preview_change();
452 self.compute_diff(&change)
453 }
454
455 fn build_preview_change(&self) -> Change {
458 match &self.screen {
459 Screen::Input(screen) => {
461 let current_text = screen.state.text();
462 if current_text.is_empty() {
463 return Change::None;
464 }
465 match &self.data {
466 WorkflowData::Add { step, uri, .. } => match step {
467 AddStep::Uri => Change::Add {
468 id: None,
469 uri: Some(current_text.to_string()),
470 flake: true,
471 },
472 AddStep::Id => Change::Add {
473 id: crate::change::ChangeId::parse(current_text).ok(),
474 uri: uri.clone(),
475 flake: true,
476 },
477 },
478 WorkflowData::Change { selected_input, .. } => Change::Change {
479 id: selected_input
480 .as_deref()
481 .and_then(|s| crate::change::ChangeId::parse(s).ok()),
482 uri: Some(current_text.to_string()),
483 },
484 _ => self.build_change(),
485 }
486 }
487 Screen::List(screen) => {
489 let selected_items: Vec<String> = screen
490 .state
491 .selected_indices()
492 .iter()
493 .filter_map(|&i| screen.state.items().get(i).cloned())
494 .collect();
495
496 if !selected_items.is_empty() {
497 return match &self.data {
498 WorkflowData::Remove { .. } => Change::Remove {
499 ids: selected_items
500 .into_iter()
501 .filter_map(|s| crate::change::ChangeId::parse(&s).ok())
502 .collect(),
503 },
504 WorkflowData::Follow {
505 step,
506 selected_input,
507 ..
508 } => {
509 if *step == FollowStep::SelectTarget {
512 if let Some(input) = selected_input {
513 let target_str: String =
514 selected_items.into_iter().next().unwrap_or_default();
515 let parsed = crate::change::ChangeId::parse(input)
516 .ok()
517 .zip(crate::follows::AttrPath::parse(&target_str).ok());
518 if let Some((change_id, target_path)) = parsed {
519 Change::Follows {
520 input: change_id,
521 target: target_path,
522 }
523 } else {
524 Change::None
525 }
526 } else {
527 Change::None
528 }
529 } else {
530 Change::None
532 }
533 }
534 _ => self.build_change(),
535 };
536 }
537 if matches!(
538 &self.data,
539 WorkflowData::Follow { .. } | WorkflowData::Change { .. }
540 ) {
541 return Change::None;
542 }
543 self.build_change()
544 }
545 Screen::Confirm(_) => self.build_change(),
546 }
547 }
548
549 pub fn with_diff(mut self, show_diff: bool) -> Self {
551 self.show_diff = show_diff;
552 if let Screen::List(ref mut screen) = self.screen {
554 screen.state.set_show_diff(show_diff);
555 }
556 self
557 }
558
559 pub fn update(&mut self, key: KeyEvent) -> UpdateResult {
560 let screen = self.screen.clone();
561 match screen {
562 Screen::Input(s) => self.update_input(s, key),
563 Screen::List(s) => self.update_list(s, key),
564 Screen::Confirm(_) => self.update_confirm(key),
565 }
566 }
567
568 fn update_input(&mut self, mut screen: InputScreen, key: KeyEvent) -> UpdateResult {
569 let action = InputAction::from_key(key);
570 match action {
571 InputAction::ToggleDiff => {
572 self.show_diff = !self.show_diff;
573 UpdateResult::Continue
574 }
575 _ => {
576 if let Some(result) = screen.state.handle(action) {
577 match result {
578 InputResult::Submit(text) => self.handle_input_submit(text),
579 InputResult::Cancel => {
580 if let WorkflowData::Add { step, uri, .. } = &mut self.data
582 && *step == AddStep::Id
583 {
584 *step = AddStep::Uri;
585 self.screen = Screen::Input(InputScreen {
586 state: InputState::with_completions(
587 uri.as_deref(),
588 uri_completion_items(None, &self.cache_config),
589 ),
590 prompt: "Enter flake URI".into(),
591 label: None,
592 });
593 return UpdateResult::Continue;
594 }
595 if let WorkflowData::Change { all_inputs, .. } = &self.data
597 && !all_inputs.is_empty()
598 {
599 self.screen = Screen::List(ListScreen::single(
600 all_inputs.clone(),
601 "Select input to change",
602 self.show_diff,
603 ));
604 return UpdateResult::Continue;
605 }
606 UpdateResult::Cancelled
607 }
608 }
609 } else {
610 if let Screen::Input(s) = &mut self.screen {
611 s.state = screen.state;
612 }
613 UpdateResult::Continue
614 }
615 }
616 }
617 }
618
619 fn update_list(&mut self, mut screen: ListScreen, key: KeyEvent) -> UpdateResult {
620 let action = ListAction::from_key(key, screen.state.search_active());
621 if let Some(result) = screen.state.handle(action) {
622 match result {
623 ListResult::Select(indices, show_diff) => {
624 self.show_diff = show_diff;
625 let items: Vec<String> = indices
626 .iter()
627 .map(|&i| screen.state.items()[i].clone())
628 .collect();
629 self.handle_list_submit(indices, items)
630 }
631 ListResult::Cancel => {
632 if let WorkflowData::Follow {
634 step,
635 nested_inputs,
636 ..
637 } = &mut self.data
638 && *step == FollowStep::SelectTarget
639 && !nested_inputs.is_empty()
640 {
641 *step = FollowStep::SelectInput;
642 let display_items: Vec<String> = nested_inputs
643 .iter()
644 .map(|i| i.to_display_string())
645 .collect();
646 self.screen = Screen::List(ListScreen::single(
647 display_items,
648 "Select input to add follows",
649 self.show_diff,
650 ));
651 return UpdateResult::Continue;
652 }
653 UpdateResult::Cancelled
654 }
655 }
656 } else {
657 if let Screen::List(s) = &mut self.screen {
658 s.state = screen.state;
659 }
660 UpdateResult::Continue
661 }
662 }
663
664 fn update_confirm(&mut self, key: KeyEvent) -> UpdateResult {
665 let action = ConfirmAction::from_key(key);
666 match action {
667 ConfirmAction::Apply => {
668 if let WorkflowData::ConfirmOnly { action, .. } = &mut self.data {
669 *action = Some(ConfirmResultAction::Apply);
670 }
671 UpdateResult::Done
672 }
673 ConfirmAction::Back => {
674 if let WorkflowData::ConfirmOnly { action, .. } = &mut self.data {
676 *action = Some(ConfirmResultAction::Back);
677 UpdateResult::Done
678 } else {
679 self.go_back();
680 UpdateResult::Continue
681 }
682 }
683 ConfirmAction::Exit => {
684 if let WorkflowData::ConfirmOnly { action, .. } = &mut self.data {
685 *action = Some(ConfirmResultAction::Exit);
686 }
687 UpdateResult::Cancelled
688 }
689 ConfirmAction::None => UpdateResult::Continue,
690 }
691 }
692
693 fn handle_input_submit(&mut self, text: String) -> UpdateResult {
694 match &mut self.data {
695 WorkflowData::Add { step, uri, id } => match step {
696 AddStep::Uri => {
697 let (inferred_id, normalized_uri) = Self::parse_uri_and_infer_id(&text);
698 *uri = Some(normalized_uri);
699 *step = AddStep::Id;
700 self.screen = Screen::Input(InputScreen {
701 state: InputState::new(inferred_id.as_deref()),
702 prompt: format!("for {}", text),
703 label: Some("ID".into()),
704 });
705 UpdateResult::Continue
706 }
707 AddStep::Id => {
708 *id = Some(text);
709 self.transition_to_confirm()
710 }
711 },
712 WorkflowData::Change { uri, .. } => {
713 *uri = Some(text);
714 self.transition_to_confirm()
715 }
716 WorkflowData::Remove { .. }
718 | WorkflowData::SelectOne { .. }
719 | WorkflowData::SelectMany { .. }
720 | WorkflowData::ConfirmOnly { .. }
721 | WorkflowData::Follow { .. } => UpdateResult::Continue,
722 }
723 }
724
725 fn handle_list_submit(&mut self, indices: Vec<usize>, items: Vec<String>) -> UpdateResult {
726 match &mut self.data {
727 WorkflowData::Change {
728 selected_input,
729 input_uris,
730 ..
731 } => {
732 let item = items.into_iter().next().unwrap_or_default();
734 let current_uri = input_uris.get(&item).map(|s| s.as_str());
735 *selected_input = Some(item.clone());
736 self.screen = Screen::Input(InputScreen {
737 state: InputState::with_completions(
738 current_uri,
739 uri_completion_items(Some(&item), &self.cache_config),
740 ),
741 prompt: "Enter new URI".into(),
742 label: Some(item),
743 });
744 UpdateResult::Continue
745 }
746 WorkflowData::SelectOne { selected_input } => {
747 *selected_input = items.into_iter().next();
749 UpdateResult::Done
750 }
751 WorkflowData::Remove {
752 selected_inputs, ..
753 } => {
754 *selected_inputs = items;
755 self.transition_to_confirm()
756 }
757 WorkflowData::SelectMany { selected_inputs } => {
758 *selected_inputs = items;
759 UpdateResult::Done
760 }
761 WorkflowData::Follow {
762 step,
763 selected_input,
764 selected_target,
765 nested_inputs,
766 top_level_inputs,
767 } => {
768 match step {
769 FollowStep::SelectInput => {
770 let index = indices.first().copied().unwrap_or(0);
772 let path = nested_inputs
773 .get(index)
774 .map(|i| i.path.to_string())
775 .unwrap_or_default();
776 *selected_input = Some(path.clone());
777 *step = FollowStep::SelectTarget;
778 self.screen = Screen::List(ListScreen::single(
779 top_level_inputs.clone(),
780 format!("Select target for {path}"),
781 self.show_diff,
782 ));
783 UpdateResult::Continue
784 }
785 FollowStep::SelectTarget => {
786 let item = items.into_iter().next().unwrap_or_default();
787 *selected_target = Some(item);
788 self.transition_to_confirm()
789 }
790 }
791 }
792 _ => UpdateResult::Continue,
793 }
794 }
795
796 fn transition_to_confirm(&mut self) -> UpdateResult {
797 if !self.show_diff {
798 return UpdateResult::Done;
799 }
800
801 let change = self.build_change();
802 let diff_str = self.compute_diff(&change);
803 self.screen = Screen::Confirm(ConfirmScreen { diff: diff_str });
804 UpdateResult::Continue
805 }
806
807 fn go_back(&mut self) {
808 match &mut self.data {
809 WorkflowData::Add { step, id, uri } => {
810 *step = AddStep::Id;
811 self.screen = Screen::Input(InputScreen {
812 state: InputState::new(id.as_deref()),
813 prompt: format!("for {}", uri.as_deref().unwrap_or("")),
814 label: Some("ID".into()),
815 });
816 }
817 WorkflowData::Change {
818 selected_input,
819 uri,
820 ..
821 } => {
822 self.screen = Screen::Input(InputScreen {
823 state: InputState::with_completions(
824 uri.as_deref(),
825 uri_completion_items(selected_input.as_deref(), &self.cache_config),
826 ),
827 prompt: "Enter new URI".into(),
828 label: selected_input.clone(),
829 });
830 }
831 WorkflowData::Remove { all_inputs, .. } => {
832 self.screen = Screen::List(ListScreen::multi(
833 all_inputs.clone(),
834 "Select inputs to remove",
835 self.show_diff,
836 ));
837 }
838 WorkflowData::Follow {
839 step,
840 nested_inputs,
841 top_level_inputs,
842 ..
843 } => {
844 if *step == FollowStep::SelectTarget {
847 self.screen = Screen::List(ListScreen::single(
848 top_level_inputs.clone(),
849 "Select target to follow",
850 self.show_diff,
851 ));
852 } else if !nested_inputs.is_empty() {
853 let display_items: Vec<String> = nested_inputs
855 .iter()
856 .map(|i| i.to_display_string())
857 .collect();
858 self.screen = Screen::List(ListScreen::single(
859 display_items,
860 "Select input to add follows",
861 self.show_diff,
862 ));
863 }
864 }
865 WorkflowData::SelectOne { .. }
867 | WorkflowData::SelectMany { .. }
868 | WorkflowData::ConfirmOnly { .. } => {}
869 }
870 }
871
872 fn build_change(&self) -> Change {
873 self.data.build_change()
874 }
875
876 fn compute_diff(&self, change: &Change) -> String {
877 super::workflow::compute_diff(&self.flake_text, change)
878 }
879
880 fn parse_uri_and_infer_id(uri: &str) -> (Option<String>, String) {
881 super::workflow::parse_uri_and_infer_id(uri)
882 }
883
884 pub fn cursor_position(&self, area: Rect) -> Option<(u16, u16)> {
885 match &self.screen {
886 Screen::Input(screen) => {
887 let input = Input::new(
888 &screen.state,
889 &screen.prompt,
890 &self.context,
891 screen.label.as_deref(),
892 self.show_diff,
893 );
894 Some(input.cursor_position(area))
895 }
896 _ => None,
897 }
898 }
899
900 pub fn terminal_height(&self) -> u16 {
901 match &self.screen {
902 Screen::Input(screen) => {
903 let input = Input::new(
904 &screen.state,
905 &screen.prompt,
906 &self.context,
907 screen.label.as_deref(),
908 self.show_diff,
909 );
910 input.required_height()
911 }
912 Screen::List(s) => super::helpers::list_height(s.state.len(), MAX_LIST_HEIGHT),
913 Screen::Confirm(s) => super::helpers::diff_height(s.diff.lines().count()),
914 }
915 }
916
917 pub fn extract_result(self) -> Option<AppResult> {
918 match self.data {
919 WorkflowData::Add { .. }
920 | WorkflowData::Change { .. }
921 | WorkflowData::Remove { .. }
922 | WorkflowData::Follow { .. } => {
923 let change = self.build_change();
924 if matches!(change, Change::None) {
925 None
926 } else {
927 Some(AppResult::Change(change))
928 }
929 }
930 WorkflowData::SelectOne { selected_input } => selected_input.map(|item| {
931 AppResult::SingleSelect(SingleSelectResult {
932 item,
933 show_diff: self.show_diff,
934 })
935 }),
936 WorkflowData::SelectMany { selected_inputs } => {
937 if selected_inputs.is_empty() {
938 None
939 } else {
940 Some(AppResult::MultiSelect(MultiSelectResultData {
941 items: selected_inputs,
942 show_diff: self.show_diff,
943 }))
944 }
945 }
946 WorkflowData::ConfirmOnly { action } => action.map(AppResult::Confirm),
947 }
948 }
949}