intelli_shell/component/
variable.rs

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
30/// A component for replacing the variables of a command
31pub struct VariableReplacementComponent {
32    /// Visual theme for styling the component
33    theme: Theme,
34    /// Whether the TUI is in inline mode or not
35    inline: bool,
36    /// Service for interacting with command storage
37    service: IntelliShellService,
38    /// Layout for arranging the input fields
39    layout: Layout,
40    /// Whether the command must be executed after replacing thw variables or just output it
41    execute_mode: bool,
42    /// Whether this component is part of the replace process (or maybe rendered after another process)
43    replace_process: bool,
44    /// Cancellation token for the background completions task
45    cancellation_token: CancellationToken,
46    /// The state of the component
47    state: Arc<RwLock<VariableReplacementComponentState<'static>>>,
48}
49struct VariableReplacementComponentState<'a> {
50    /// The command with variables to be replaced
51    template: CommandTemplateWidget,
52    /// Flat name of the current variable being set
53    flat_variable_name: String,
54    /// Full list of suggestions for the current variable
55    variable_suggestions: Vec<VariableSuggestionItem<'static>>,
56    /// Widget list of filtered suggestions for the variable value
57    suggestions: CustomList<'a, VariableSuggestionItem<'a>>,
58    /// Popup for displaying error messages
59    error: ErrorPopup<'a>,
60    /// A spinner to indicate that completions are being fetched
61    loading: Option<LoadingSpinner<'a>>,
62}
63
64impl VariableReplacementComponent {
65    /// Creates a new [`VariableReplacementComponent`]
66    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        // Command + Values
114        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        // Split the area according to the layout
125        let [cmd_area, suggestions_area] = self.layout.areas(area);
126
127        let mut state = self.state.write();
128
129        // Render the command widget
130        frame.render_widget(&state.template, cmd_area);
131
132        // Render the suggestions
133        frame.render_widget(&mut state.suggestions, suggestions_area);
134
135        // Render the new version banner and error message as an overlay
136        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        // Display the loading spinner, if any
141        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        // Helper enum to hold the data extracted from the lock
472        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    /// Filters the suggestions widget based on the query
533    fn filter_suggestions(&mut self, query: &str) {
534        tracing::debug!("Filtering suggestions for: {query}");
535        // From the original variable suggestions, keep those matching the query only
536        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        // Find and insert the new row, which contains the query
546        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        // Retrieve the identifier for the selected item
555        let selected_id = self.suggestions.selected().map(|s| s.identifier());
556        // Update the items
557        self.suggestions.update_items(filtered_suggestions, false);
558        // Restore the same selected item
559        if let Some(selected_id) = selected_id {
560            self.suggestions.select_matching(|i| i.identifier() == selected_id);
561        }
562    }
563}
564
565impl VariableReplacementComponent {
566    /// Updates the variable context and the suggestions widget, or returns an acton
567    async fn update_variable_context(&mut self, peek: bool) -> Result<Action> {
568        // Cancels previous completion task
569        self.cancellation_token.cancel();
570        self.cancellation_token = CancellationToken::new();
571
572        // Retrieves the current variable and its context
573        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        // Search for the variable suggestions
593        let (initial_suggestions, completion_stream) = self
594            .service
595            .search_variable_suggestions(&flat_root_cmd, &current_variable, context)
596            .await
597            .map_err(AppError::into_report)?;
598
599        // Update the context
600        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        // And the displayed items
609        state.suggestions.update_items(suggestions, false);
610
611        // Pre-select the first non-derived suggestion
612        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 there's some completions stream
622        if let Some(mut stream) = completion_stream {
623            let token = self.cancellation_token.clone();
624            let state_clone = self.state.clone();
625
626            // Show the loading spinner
627            state.loading = Some(LoadingSpinner::new(&self.theme));
628
629            // Spawn a background task to wait for them
630            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                        // If an error happens while resolving the completion, display the first line
638                        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                        // Otherwise, merge suggestions
645                        Ok(completion_suggestions) => {
646                            let mut state = state_clone.write();
647
648                            // Retrieve the current set of suggestions
649                            let master_suggestions = &mut state.variable_suggestions;
650
651                            // Remove all `Derived` items that are about to be added as a `Completion`
652                            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 each new suggestion given by the completion
662                            for suggestion in completion_suggestions {
663                                // Check if there's already a suggestion for the same value
664                                let mut skip_completion = false;
665                                for item in master_suggestions.iter_mut() {
666                                    match item {
667                                        // `New` items doesn't affect
668                                        VariableSuggestionItem::New { .. } => (),
669                                        // `Derived` are already handled above
670                                        VariableSuggestionItem::Derived { .. } => (),
671                                        // If already an environment, just skip completion
672                                        VariableSuggestionItem::Environment { content, is_value, .. } => {
673                                            if *is_value && content == &suggestion {
674                                                skip_completion = true;
675                                                break;
676                                            }
677                                        }
678                                        // If already an existing, boost its score and skip completion
679                                        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                                        // If already a completion, keep the maximum score and skip this one
695                                        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                                // Add the new suggestion
709                                master_suggestions.push(VariableSuggestionItem::Completion {
710                                    sort_index: 3,
711                                    value: suggestion,
712                                    score: score_boost,
713                                });
714                            }
715
716                            // Re-sort suggestions
717                            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                            // After sorting, filter suggestions
724                            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    /// Returns an action to quit the component, with the current variable content
859    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}