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