1use std::{
2 cmp::Ordering,
3 collections::HashSet,
4 sync::{Arc, Mutex},
5};
6
7use async_trait::async_trait;
8use color_eyre::Result;
9use crossterm::event::{MouseEvent, MouseEventKind};
10use futures_util::StreamExt;
11use parking_lot::RwLock;
12use ratatui::{
13 Frame,
14 layout::{Constraint, Layout, Rect},
15};
16use tokio_util::sync::CancellationToken;
17use tracing::instrument;
18
19use super::Component;
20use crate::{
21 app::Action,
22 config::Theme,
23 errors::AppError,
24 format_msg,
25 model::{CommandTemplate, VariableValue},
26 process::ProcessOutput,
27 service::IntelliShellService,
28 widgets::{
29 CommandTemplateWidget, CustomList, CustomTextArea, ErrorPopup, LoadingSpinner, NewVersionBanner,
30 items::VariableSuggestionItem,
31 },
32};
33
34#[derive(Clone)]
36pub struct VariableReplacementComponent {
37 theme: Theme,
39 inline: bool,
41 service: IntelliShellService,
43 layout: Layout,
45 execute_mode: bool,
47 replace_process: bool,
49 cancellation_token: Arc<Mutex<Option<CancellationToken>>>,
51 state: Arc<RwLock<VariableReplacementComponentState<'static>>>,
53}
54struct VariableReplacementComponentState<'a> {
55 template: CommandTemplateWidget,
57 flat_variable_name: String,
59 variable_suggestions: Vec<VariableSuggestionItem<'static>>,
61 suggestions: CustomList<'a, VariableSuggestionItem<'a>>,
63 error: ErrorPopup<'a>,
65 loading: Option<LoadingSpinner<'a>>,
67 last_unset_value: Option<String>,
69}
70
71impl VariableReplacementComponent {
72 pub fn new(
74 service: IntelliShellService,
75 theme: Theme,
76 inline: bool,
77 execute_mode: bool,
78 replace_process: bool,
79 command: CommandTemplate,
80 ) -> Self {
81 let command = CommandTemplateWidget::new(&theme, inline, command);
82
83 let suggestions = CustomList::new(theme.clone(), inline, Vec::new());
84
85 let error = ErrorPopup::empty(&theme);
86
87 let layout = if inline {
88 Layout::vertical([Constraint::Length(1), Constraint::Min(3)])
89 } else {
90 Layout::vertical([Constraint::Length(3), Constraint::Min(5)]).margin(1)
91 };
92
93 Self {
94 theme,
95 inline,
96 service,
97 layout,
98 execute_mode,
99 replace_process,
100 cancellation_token: Arc::new(Mutex::new(None)),
101 state: Arc::new(RwLock::new(VariableReplacementComponentState {
102 template: command,
103 flat_variable_name: String::new(),
104 variable_suggestions: Vec::new(),
105 suggestions,
106 error,
107 loading: None,
108 last_unset_value: None,
109 })),
110 }
111 }
112}
113
114#[async_trait]
115impl Component for VariableReplacementComponent {
116 fn name(&self) -> &'static str {
117 "VariableReplacementComponent"
118 }
119
120 fn min_inline_height(&self) -> u16 {
121 1 + 5
123 }
124
125 #[instrument(skip_all)]
126 async fn init_and_peek(&mut self) -> Result<Action> {
127 self.update_variable_context(true).await
128 }
129
130 #[instrument(skip_all)]
131 fn render(&mut self, frame: &mut Frame, area: Rect) {
132 let [cmd_area, suggestions_area] = self.layout.areas(area);
134
135 let mut state = self.state.write();
136
137 frame.render_widget(&state.template, cmd_area);
139
140 frame.render_widget(&mut state.suggestions, suggestions_area);
142
143 if let Some(new_version) = self.service.check_new_version() {
145 NewVersionBanner::new(&self.theme, new_version).render_in(frame, area);
146 }
147 state.error.render_in(frame, area);
148 if let Some(loading) = &state.loading {
150 let loading_area = if self.inline {
151 Rect {
152 x: suggestions_area.x,
153 y: suggestions_area.y + suggestions_area.height.saturating_sub(1),
154 width: 1,
155 height: 1,
156 }
157 } else {
158 Rect {
159 x: suggestions_area.x.saturating_add(1),
160 y: suggestions_area.y + suggestions_area.height.saturating_sub(2),
161 width: 1,
162 height: 1,
163 }
164 };
165 loading.render_in(frame, loading_area);
166 }
167 }
168
169 fn tick(&mut self) -> Result<Action> {
170 let mut state = self.state.write();
171 state.error.tick();
172 if let Some(loading) = &mut state.loading {
173 loading.tick();
174 }
175
176 Ok(Action::NoOp)
177 }
178
179 fn exit(&mut self) -> Result<Action> {
180 {
181 let mut token_guard = self.cancellation_token.lock().unwrap();
182 if let Some(token) = token_guard.take() {
183 token.cancel();
184 }
185 }
186 let mut state = self.state.write();
187 if let Some(VariableSuggestionItem::Existing { editing, .. }) = state.suggestions.selected_mut()
188 && editing.is_some()
189 {
190 tracing::debug!("Closing variable value edit mode: user request");
191 *editing = None;
192 Ok(Action::NoOp)
193 } else {
194 tracing::info!("User requested to exit");
195 Ok(Action::Quit(
196 ProcessOutput::success().fileout(state.template.to_string()),
197 ))
198 }
199 }
200
201 fn process_mouse_event(&mut self, mouse: MouseEvent) -> Result<Action> {
202 match mouse.kind {
203 MouseEventKind::ScrollDown => Ok(self.move_next()?),
204 MouseEventKind::ScrollUp => Ok(self.move_prev()?),
205 _ => Ok(Action::NoOp),
206 }
207 }
208
209 fn move_up(&mut self) -> Result<Action> {
210 let mut state = self.state.write();
211 match state.suggestions.selected() {
212 Some(VariableSuggestionItem::Existing { editing: Some(_), .. }) => (),
213 _ => state.suggestions.select_prev(),
214 }
215 Ok(Action::NoOp)
216 }
217
218 fn move_down(&mut self) -> Result<Action> {
219 let mut state = self.state.write();
220 match state.suggestions.selected() {
221 Some(VariableSuggestionItem::Existing { editing: Some(_), .. }) => (),
222 _ => state.suggestions.select_next(),
223 }
224 Ok(Action::NoOp)
225 }
226
227 fn move_left(&mut self, word: bool) -> Result<Action> {
228 let mut state = self.state.write();
229 match state.suggestions.selected_mut() {
230 Some(VariableSuggestionItem::New { textarea, .. }) => {
231 textarea.move_cursor_left(word);
232 }
233 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
234 ta.move_cursor_left(word);
235 }
236 _ => (),
237 }
238 Ok(Action::NoOp)
239 }
240
241 fn move_right(&mut self, word: bool) -> Result<Action> {
242 let mut state = self.state.write();
243 match state.suggestions.selected_mut() {
244 Some(VariableSuggestionItem::New { textarea, .. }) => {
245 textarea.move_cursor_right(word);
246 }
247 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
248 ta.move_cursor_right(word);
249 }
250 _ => (),
251 }
252 Ok(Action::NoOp)
253 }
254
255 fn move_prev(&mut self) -> Result<Action> {
256 self.move_up()
257 }
258
259 fn move_next(&mut self) -> Result<Action> {
260 self.move_down()
261 }
262
263 fn move_home(&mut self, absolute: bool) -> Result<Action> {
264 let mut state = self.state.write();
265 match state.suggestions.selected_mut() {
266 Some(VariableSuggestionItem::New { textarea, .. }) => {
267 textarea.move_home(absolute);
268 }
269 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
270 ta.move_home(absolute);
271 }
272 _ => state.suggestions.select_first(),
273 }
274 Ok(Action::NoOp)
275 }
276
277 fn move_end(&mut self, absolute: bool) -> Result<Action> {
278 let mut state = self.state.write();
279 match state.suggestions.selected_mut() {
280 Some(VariableSuggestionItem::New { textarea, .. }) => {
281 textarea.move_end(absolute);
282 }
283 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
284 ta.move_end(absolute);
285 }
286 _ => state.suggestions.select_last(),
287 }
288 Ok(Action::NoOp)
289 }
290
291 fn undo(&mut self) -> Result<Action> {
292 let mut state = self.state.write();
293 match state.suggestions.selected_mut() {
294 Some(VariableSuggestionItem::New {
295 textarea, is_secret, ..
296 }) => {
297 textarea.undo();
298 if !*is_secret {
299 let query = textarea.lines_as_string();
300 state.filter_suggestions(&query);
301 }
302 }
303 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
304 ta.undo();
305 }
306 _ => {
307 if let Some(unset_value) = state.template.unset_last_variable() {
308 state.last_unset_value = Some(unset_value);
309 self.debounced_update_variable_context();
310 }
311 }
312 }
313 Ok(Action::NoOp)
314 }
315
316 fn redo(&mut self) -> Result<Action> {
317 let mut state = self.state.write();
318 match state.suggestions.selected_mut() {
319 Some(VariableSuggestionItem::New {
320 textarea, is_secret, ..
321 }) => {
322 textarea.redo();
323 if !*is_secret {
324 let query = textarea.lines_as_string();
325 state.filter_suggestions(&query);
326 }
327 }
328 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
329 ta.redo();
330 }
331 _ => {
332 if let Some(value_to_set) = state.last_unset_value.take() {
333 state.template.set_next_variable(value_to_set);
334 self.debounced_update_variable_context();
335 }
336 }
337 }
338 Ok(Action::NoOp)
339 }
340
341 fn insert_text(&mut self, mut text: String) -> Result<Action> {
342 let mut state = self.state.write();
343 if let Some(variable) = state.template.current_variable() {
344 text = variable.apply_functions_to(text);
345 }
346 match state.suggestions.selected_mut() {
347 Some(VariableSuggestionItem::New {
348 textarea, is_secret, ..
349 }) => {
350 textarea.insert_str(text);
351 if !*is_secret {
352 let query = textarea.lines_as_string();
353 state.filter_suggestions(&query);
354 }
355 }
356 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
357 ta.insert_str(text);
358 }
359 _ => (),
360 }
361 Ok(Action::NoOp)
362 }
363
364 fn insert_char(&mut self, c: char) -> Result<Action> {
365 let mut state = self.state.write();
366 let maybe_replacement = state
367 .template
368 .current_variable()
369 .and_then(|variable| variable.check_functions_char(c));
370 let insert_content = |ta: &mut CustomTextArea<'_>| {
371 if let Some(r) = &maybe_replacement {
372 ta.insert_str(r);
373 } else {
374 ta.insert_char(c);
375 }
376 };
377 match state.suggestions.selected_mut() {
378 Some(VariableSuggestionItem::New {
379 textarea, is_secret, ..
380 }) => {
381 insert_content(textarea);
382 if !*is_secret {
383 let query = textarea.lines_as_string();
384 state.filter_suggestions(&query);
385 }
386 }
387 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
388 insert_content(ta);
389 }
390 _ => {
391 if let Some(VariableSuggestionItem::New { .. }) = state.suggestions.items().iter().next() {
392 state.suggestions.select_first();
393 if let Some(VariableSuggestionItem::New {
394 textarea, is_secret, ..
395 }) = state.suggestions.selected_mut()
396 {
397 insert_content(textarea);
398 if !*is_secret {
399 let query = textarea.lines_as_string();
400 state.filter_suggestions(&query);
401 }
402 }
403 }
404 }
405 }
406 Ok(Action::NoOp)
407 }
408
409 fn delete(&mut self, backspace: bool, word: bool) -> Result<Action> {
410 let mut state = self.state.write();
411 match state.suggestions.selected_mut() {
412 Some(VariableSuggestionItem::New {
413 textarea, is_secret, ..
414 }) => {
415 textarea.delete(backspace, word);
416 if !*is_secret {
417 let query = textarea.lines_as_string();
418 state.filter_suggestions(&query);
419 }
420 }
421 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
422 ta.delete(backspace, word);
423 }
424 _ => (),
425 }
426 Ok(Action::NoOp)
427 }
428
429 #[instrument(skip_all)]
430 async fn selection_delete(&mut self) -> Result<Action> {
431 let deleted_id = {
432 let mut state = self.state.write();
433 match state.suggestions.selected_mut() {
434 Some(VariableSuggestionItem::New { .. }) => return Ok(Action::NoOp),
435 Some(VariableSuggestionItem::Existing {
436 value: VariableValue { id: Some(id), .. },
437 editing,
438 ..
439 }) => {
440 if editing.is_none() {
441 let id = *id;
442 state.suggestions.delete_selected();
443 id
444 } else {
445 return Ok(Action::NoOp);
446 }
447 }
448 _ => {
449 state.error.set_temp_message("This value is not yet stored");
450 return Ok(Action::NoOp);
451 }
452 }
453 };
454
455 self.service
456 .delete_variable_value(deleted_id)
457 .await
458 .map_err(AppError::into_report)?;
459
460 self.state
461 .write()
462 .variable_suggestions
463 .retain(|s| !matches!(s, VariableSuggestionItem::Existing { value, .. } if value.id == Some(deleted_id)));
464
465 Ok(Action::NoOp)
466 }
467
468 #[instrument(skip_all)]
469 async fn selection_update(&mut self) -> Result<Action> {
470 let mut state = self.state.write();
471
472 match state.suggestions.selected_mut() {
473 Some(VariableSuggestionItem::New { .. }) => (),
474 Some(i @ VariableSuggestionItem::Existing { .. }) => {
475 if let VariableSuggestionItem::Existing { value, editing, .. } = i {
476 if let Some(id) = value.id {
477 if editing.is_none() {
478 tracing::debug!("Entering edit mode for existing variable value: {id}");
479 i.enter_edit_mode();
480 }
481 } else {
482 state.error.set_temp_message("This value is not yet stored");
483 }
484 }
485 }
486 _ => state.error.set_temp_message("This value is not yet stored"),
487 }
488 Ok(Action::NoOp)
489 }
490
491 async fn selection_confirm(&mut self) -> Result<Action> {
492 {
493 let mut token_guard = self.cancellation_token.lock().unwrap();
494 if let Some(token) = token_guard.take() {
495 token.cancel();
496 }
497 }
498
499 enum NextAction {
501 NoOp,
502 ConfirmNewSecret(String),
503 ConfirmNewRegular(String),
504 ConfirmExistingEdition(VariableValue, String),
505 ConfirmExistingValue(VariableValue),
506 ConfirmLiteral(String, bool),
507 }
508
509 let next_action = {
510 let mut state = self.state.write();
511 match state.suggestions.selected_mut() {
512 None => NextAction::NoOp,
513 Some(VariableSuggestionItem::New {
514 textarea,
515 is_secret: true,
516 ..
517 }) => NextAction::ConfirmNewSecret(textarea.lines_as_string()),
518 Some(VariableSuggestionItem::New {
519 textarea,
520 is_secret: false,
521 ..
522 }) => NextAction::ConfirmNewRegular(textarea.lines_as_string()),
523 Some(VariableSuggestionItem::Existing { value, editing, .. }) => match editing.take() {
524 Some(ta) => NextAction::ConfirmExistingEdition(value.clone(), ta.lines_as_string()),
525 None => NextAction::ConfirmExistingValue(value.clone()),
526 },
527 Some(VariableSuggestionItem::Environment {
528 content,
529 is_value: false,
530 ..
531 }) => NextAction::ConfirmLiteral(content.clone(), false),
532 Some(VariableSuggestionItem::Environment {
533 content: value,
534 is_value: true,
535 ..
536 })
537 | Some(VariableSuggestionItem::Completion { value, .. })
538 | Some(VariableSuggestionItem::Derived { value, .. }) => {
539 NextAction::ConfirmLiteral(value.clone(), true)
540 }
541 }
542 };
543
544 match next_action {
545 NextAction::NoOp => Ok(Action::NoOp),
546 NextAction::ConfirmNewSecret(value) => self.confirm_new_secret_value(value).await,
547 NextAction::ConfirmNewRegular(value) => self.confirm_new_regular_value(value).await,
548 NextAction::ConfirmExistingEdition(val, new_val) => self.confirm_existing_edition(val, new_val).await,
549 NextAction::ConfirmExistingValue(val) => self.confirm_existing_value(val, false).await,
550 NextAction::ConfirmLiteral(val, is_value) => self.confirm_literal_value(val, is_value).await,
551 }
552 }
553
554 async fn selection_execute(&mut self) -> Result<Action> {
555 self.selection_confirm().await
556 }
557}
558
559impl<'a> VariableReplacementComponentState<'a> {
560 fn filter_suggestions(&mut self, query: &str) {
562 tracing::debug!("Filtering suggestions for: {query}");
563 let mut filtered_suggestions = self.variable_suggestions.clone();
565 filtered_suggestions.retain(|s| match s {
566 VariableSuggestionItem::New { .. } => false,
567 VariableSuggestionItem::Existing { value, .. } => value.value.contains(query),
568 VariableSuggestionItem::Environment { content: value, .. }
569 | VariableSuggestionItem::Completion { value, .. }
570 | VariableSuggestionItem::Derived { value, .. } => value.contains(query),
571 });
572
573 let new_row = self
575 .suggestions
576 .items()
577 .iter()
578 .find(|s| matches!(s, VariableSuggestionItem::New { .. }));
579 if let Some(new_row) = new_row.cloned() {
580 filtered_suggestions.insert(0, new_row);
581 }
582 let selected_id = self.suggestions.selected().map(|s| s.identifier());
584 self.suggestions.update_items(filtered_suggestions, false);
586 if let Some(selected_id) = selected_id {
588 self.suggestions.select_matching(|i| i.identifier() == selected_id);
589 }
590 }
591}
592
593impl VariableReplacementComponent {
594 fn debounced_update_variable_context(&self) {
596 let this = self.clone();
597 tokio::spawn(async move {
598 if let Err(err) = this.update_variable_context(false).await {
599 tracing::error!("Error updating variable context: {err:?}");
600 }
601 });
602 }
603
604 async fn update_variable_context(&self, peek: bool) -> Result<Action> {
606 let cancellation_token = {
608 let mut token_guard = self.cancellation_token.lock().unwrap();
609 if let Some(token) = token_guard.take() {
610 token.cancel();
611 }
612 let new_token = CancellationToken::new();
613 *token_guard = Some(new_token.clone());
614 new_token
615 };
616
617 let (flat_root_cmd, current_variable, context) = {
619 let state = self.state.read();
620 match state.template.current_variable().cloned() {
621 Some(variable) => (
622 state.template.flat_root_cmd.clone(),
623 variable,
624 state.template.current_variable_context(),
625 ),
626 None => {
627 if peek {
628 tracing::info!("There are no variables to replace");
629 } else {
630 tracing::info!("There are no more variables");
631 }
632 return self.quit_action(peek, state.template.to_string());
633 }
634 }
635 };
636
637 let (initial_suggestions, completion_stream) = self
639 .service
640 .search_variable_suggestions(&flat_root_cmd, ¤t_variable, context)
641 .await
642 .map_err(AppError::into_report)?;
643
644 let mut state = self.state.write();
646 let suggestions = initial_suggestions
647 .into_iter()
648 .map(VariableSuggestionItem::from)
649 .collect::<Vec<_>>();
650 state.flat_variable_name = current_variable.flat_name.clone();
651 state.variable_suggestions = suggestions.clone();
652
653 state.suggestions.update_items(suggestions, false);
655
656 if let Some(idx) = state.suggestions.items().iter().position(|s| {
658 !matches!(
659 s,
660 VariableSuggestionItem::New { .. } | VariableSuggestionItem::Derived { .. }
661 )
662 }) {
663 state.suggestions.select(idx);
664 }
665
666 if let Some(mut stream) = completion_stream {
668 let token = cancellation_token.clone();
669 let state_clone = self.state.clone();
670
671 state.loading = Some(LoadingSpinner::new(&self.theme));
673
674 tokio::spawn(async move {
676 while let Some((score_boost, result)) = tokio::select! {
677 biased;
678 _ = token.cancelled() => None,
679 item = stream.next() => item,
680 } {
681 match result {
682 Err(err) => {
684 let mut state = state_clone.write();
685 if let Some(line) = err.lines().next() {
686 state.error.set_temp_message(line.to_string());
687 }
688 }
689 Ok(completion_suggestions) => {
691 let mut state = state_clone.write();
692
693 let master_suggestions = &mut state.variable_suggestions;
695
696 let completion_set = completion_suggestions.iter().collect::<HashSet<_>>();
698 master_suggestions.retain_mut(|item| {
699 !matches!(
700 item,
701 VariableSuggestionItem::Derived { value, .. }
702 if completion_set.contains(value)
703 )
704 });
705
706 for suggestion in completion_suggestions {
708 let mut skip_completion = false;
710 for item in master_suggestions.iter_mut() {
711 match item {
712 VariableSuggestionItem::New { .. } => (),
714 VariableSuggestionItem::Derived { .. } => (),
716 VariableSuggestionItem::Environment { content, is_value, .. } => {
718 if *is_value && content == &suggestion {
719 skip_completion = true;
720 break;
721 }
722 }
723 VariableSuggestionItem::Existing {
725 value,
726 score,
727 completion_merged,
728 ..
729 } => {
730 if value.value == suggestion {
731 if !*completion_merged {
732 *score += score_boost;
733 *completion_merged = true;
734 }
735 skip_completion = true;
736 break;
737 }
738 }
739 VariableSuggestionItem::Completion { value, score, .. } => {
741 if value == &suggestion {
742 *score += score_boost.max(*score);
743 skip_completion = true;
744 break;
745 }
746 }
747 }
748 }
749 if skip_completion {
750 continue;
751 }
752
753 master_suggestions.push(VariableSuggestionItem::Completion {
755 sort_index: 3,
756 value: suggestion,
757 score: score_boost,
758 });
759 }
760
761 master_suggestions.sort_by(|a, b| {
763 a.sort_index()
764 .cmp(&b.sort_index())
765 .then_with(|| b.score().partial_cmp(&a.score()).unwrap_or(Ordering::Equal))
766 });
767
768 let query = state
770 .suggestions
771 .items()
772 .iter()
773 .find_map(|s| match s {
774 VariableSuggestionItem::New {
775 textarea,
776 is_secret: false,
777 ..
778 } => Some(textarea.lines_as_string()),
779 _ => None,
780 })
781 .unwrap_or_default();
782 state.filter_suggestions(&query);
783 }
784 }
785 }
786 state_clone.write().loading = None;
787 });
788 }
789
790 Ok(Action::NoOp)
791 }
792
793 #[instrument(skip_all)]
794 async fn confirm_new_secret_value(&mut self, value: String) -> Result<Action> {
795 tracing::debug!("Secret variable value selected");
796 self.state.write().template.set_next_variable(value);
797 self.update_variable_context(false).await
798 }
799
800 #[instrument(skip_all)]
801 async fn confirm_new_regular_value(&mut self, value: String) -> Result<Action> {
802 if !value.trim().is_empty() {
803 let variable_value = {
804 let state = self.state.read();
805 state.template.new_variable_value_for(&state.flat_variable_name, &value)
806 };
807 match self.service.insert_variable_value(variable_value).await {
808 Ok(v) => {
809 tracing::debug!("New variable value stored");
810 self.confirm_existing_value(v, true).await
811 }
812 Err(AppError::UserFacing(err)) => {
813 tracing::warn!("{err}");
814 self.state.write().error.set_temp_message(err.to_string());
815 Ok(Action::NoOp)
816 }
817 Err(AppError::Unexpected(report)) => Err(report),
818 }
819 } else {
820 tracing::debug!("New empty variable value selected");
821 self.state.write().template.set_next_variable(value);
822 self.update_variable_context(false).await
823 }
824 }
825
826 #[instrument(skip_all)]
827 async fn confirm_existing_edition(&mut self, mut value: VariableValue, new_value: String) -> Result<Action> {
828 value.value = new_value;
829 match self.service.update_variable_value(value).await {
830 Ok(v) => {
831 let mut state = self.state.write();
832 if let VariableSuggestionItem::Existing { value, .. } = state.suggestions.selected_mut().unwrap() {
833 *value = v;
834 };
835 Ok(Action::NoOp)
836 }
837 Err(AppError::UserFacing(err)) => {
838 tracing::warn!("{err}");
839 self.state.write().error.set_temp_message(err.to_string());
840 Ok(Action::NoOp)
841 }
842 Err(AppError::Unexpected(report)) => Err(report),
843 }
844 }
845
846 #[instrument(skip_all)]
847 async fn confirm_existing_value(&mut self, mut value: VariableValue, new: bool) -> Result<Action> {
848 let value_id = match value.id {
849 Some(id) => id,
850 None => {
851 value = self
852 .service
853 .insert_variable_value(value)
854 .await
855 .map_err(AppError::into_report)?;
856 value.id.expect("just inserted")
857 }
858 };
859 let context = self.state.read().template.current_variable_context();
860 match self
861 .service
862 .increment_variable_value_usage(value_id, context)
863 .await
864 .map_err(AppError::into_report)
865 {
866 Ok(_) => {
867 if !new {
868 tracing::debug!("Existing variable value selected");
869 }
870 self.state.write().template.set_next_variable(value.value);
871 self.update_variable_context(false).await
872 }
873 Err(report) => Err(report),
874 }
875 }
876
877 #[instrument(skip_all)]
878 async fn confirm_literal_value(&mut self, value: String, store: bool) -> Result<Action> {
879 if store && !value.trim().is_empty() {
880 let variable_value = {
881 let state = self.state.read();
882 state.template.new_variable_value_for(&state.flat_variable_name, &value)
883 };
884 match self.service.insert_variable_value(variable_value).await {
885 Ok(v) => {
886 tracing::debug!("Literal variable value selected and stored");
887 self.confirm_existing_value(v, true).await
888 }
889 Err(AppError::UserFacing(err)) => {
890 tracing::debug!("Literal variable value selected but couldn't be stored: {err}");
891 self.state.write().template.set_next_variable(value);
892 self.update_variable_context(false).await
893 }
894 Err(AppError::Unexpected(report)) => Err(report),
895 }
896 } else {
897 tracing::debug!("Literal variable value selected");
898 self.state.write().template.set_next_variable(value);
899 self.update_variable_context(false).await
900 }
901 }
902
903 fn quit_action(&self, peek: bool, cmd: String) -> Result<Action> {
905 if self.execute_mode {
906 Ok(Action::Quit(ProcessOutput::execute(cmd)))
907 } else if self.replace_process && peek {
908 Ok(Action::Quit(
909 ProcessOutput::success()
910 .stderr(format_msg!(self.theme, "There are no variables to replace"))
911 .stdout(&cmd)
912 .fileout(cmd),
913 ))
914 } else {
915 Ok(Action::Quit(ProcessOutput::success().stdout(&cmd).fileout(cmd)))
916 }
917 }
918}