intelli_shell/component/
variable.rs

1use std::ops::DerefMut;
2
3use async_trait::async_trait;
4use color_eyre::{Result, eyre::eyre};
5use crossterm::event::{MouseEvent, MouseEventKind};
6use ratatui::{
7    Frame,
8    layout::{Constraint, Layout, Rect},
9};
10use semver::Version;
11use tracing::instrument;
12
13use super::Component;
14use crate::{
15    app::Action,
16    config::Theme,
17    errors::{InsertError, UpdateError},
18    format_msg,
19    model::{DynamicCommand, VariableSuggestion, VariableValue},
20    process::ProcessOutput,
21    service::IntelliShellService,
22    utils::format_env_var,
23    widgets::{
24        CustomList, CustomTextArea, DynamicCommandWidget, ErrorPopup, ExistingVariableValue, LiteralVariableValue,
25        NewVariableValue, NewVersionBanner, VariableSuggestionRow,
26    },
27};
28
29/// A component for replacing the variables of a command
30pub struct VariableReplacementComponent {
31    /// Visual theme for styling the component
32    theme: Theme,
33    /// Service for interacting with command storage
34    service: IntelliShellService,
35    /// Layout for arranging the input fields
36    layout: Layout,
37    /// Whether the command must be executed after replacing thw variables or just output it
38    execute_mode: bool,
39    /// Whether this component is part of the replace process (or maybe rendered after another process)
40    replace_process: bool,
41    /// The command with variables to be replaced
42    command: DynamicCommandWidget,
43    /// Context of the current variable of the command or `None` if there are no more variables to replace
44    variable_ctx: Option<CurrentVariableContext>,
45    /// Widget list of filtered suggestions for the variable value
46    suggestions: CustomList<'static, VariableSuggestionRow<'static>>,
47    /// The new version banner
48    new_version: NewVersionBanner,
49    /// Popup for displaying error messages
50    error: ErrorPopup<'static>,
51}
52struct CurrentVariableContext {
53    /// Name of the variable being replaced
54    name: String,
55    /// Full list of suggestions for the variable value
56    suggestions: Vec<VariableSuggestionRow<'static>>,
57}
58
59impl VariableReplacementComponent {
60    /// Creates a new [`VariableReplacementComponent`]
61    pub fn new(
62        service: IntelliShellService,
63        theme: Theme,
64        inline: bool,
65        execute_mode: bool,
66        replace_process: bool,
67        new_version: Option<Version>,
68        command: DynamicCommand,
69    ) -> Self {
70        let command = DynamicCommandWidget::new(&theme, inline, command);
71
72        let suggestions = CustomList::new(theme.primary, inline, Vec::new())
73            .highlight_symbol(theme.highlight_symbol.clone())
74            .highlight_symbol_style(theme.highlight_primary_full().into());
75
76        let new_version = NewVersionBanner::new(&theme, new_version);
77        let error = ErrorPopup::empty(&theme);
78
79        let layout = if inline {
80            Layout::vertical([Constraint::Length(1), Constraint::Min(3)])
81        } else {
82            Layout::vertical([Constraint::Length(3), Constraint::Min(5)]).margin(1)
83        };
84
85        Self {
86            theme,
87            service,
88            layout,
89            execute_mode,
90            replace_process,
91            command,
92            variable_ctx: None,
93            suggestions,
94            new_version,
95            error,
96        }
97    }
98}
99
100#[async_trait]
101impl Component for VariableReplacementComponent {
102    fn name(&self) -> &'static str {
103        "VariableReplacementComponent"
104    }
105
106    fn min_inline_height(&self) -> u16 {
107        // Command + Values
108        1 + 3
109    }
110
111    async fn init(&mut self) -> Result<()> {
112        self.update_variable_context(false).await?;
113        Ok(())
114    }
115
116    #[instrument(skip_all)]
117    async fn peek(&mut self) -> Result<Action> {
118        if self.variable_ctx.is_none() {
119            tracing::info!("The command has no variables to replace");
120            self.quit_action(true)
121        } else {
122            Ok(Action::NoOp)
123        }
124    }
125
126    #[instrument(skip_all)]
127    fn render(&mut self, frame: &mut Frame, area: Rect) {
128        // Split the area according to the layout
129        let [cmd_area, suggestions_area] = self.layout.areas(area);
130
131        // Render the command widget
132        frame.render_widget(&self.command, cmd_area);
133
134        // Render the suggestions
135        frame.render_widget(&mut self.suggestions, suggestions_area);
136
137        // Render the new version banner and error message as an overlay
138        self.new_version.render_in(frame, area);
139        self.error.render_in(frame, area);
140    }
141
142    fn tick(&mut self) -> Result<Action> {
143        self.error.tick();
144
145        Ok(Action::NoOp)
146    }
147
148    fn exit(&mut self) -> Result<Option<ProcessOutput>> {
149        if let Some(VariableSuggestionRow::Existing(e)) = self.suggestions.selected_mut() {
150            if e.editing.is_some() {
151                tracing::debug!("Closing variable value edit mode: user request");
152                e.editing = None;
153                return Ok(None);
154            }
155        }
156        tracing::info!("User requested to exit");
157        Ok(Some(ProcessOutput::success().fileout(self.command.to_string())))
158    }
159
160    fn process_mouse_event(&mut self, mouse: MouseEvent) -> Result<Action> {
161        match mouse.kind {
162            MouseEventKind::ScrollDown => Ok(self.move_next()?),
163            MouseEventKind::ScrollUp => Ok(self.move_prev()?),
164            _ => Ok(Action::NoOp),
165        }
166    }
167
168    fn move_up(&mut self) -> Result<Action> {
169        match self.suggestions.selected() {
170            Some(VariableSuggestionRow::Existing(e)) if e.editing.is_some() => (),
171            _ => self.suggestions.select_prev(),
172        }
173        Ok(Action::NoOp)
174    }
175
176    fn move_down(&mut self) -> Result<Action> {
177        match self.suggestions.selected() {
178            Some(VariableSuggestionRow::Existing(e)) if e.editing.is_some() => (),
179            _ => self.suggestions.select_next(),
180        }
181        Ok(Action::NoOp)
182    }
183
184    fn move_left(&mut self, word: bool) -> Result<Action> {
185        match self.suggestions.selected_mut() {
186            Some(VariableSuggestionRow::New(n)) => {
187                n.move_cursor_left(word);
188            }
189            Some(VariableSuggestionRow::Existing(e)) => {
190                if let Some(ref mut ta) = e.editing {
191                    ta.move_cursor_left(word);
192                }
193            }
194            _ => (),
195        }
196        Ok(Action::NoOp)
197    }
198
199    fn move_right(&mut self, word: bool) -> Result<Action> {
200        match self.suggestions.selected_mut() {
201            Some(VariableSuggestionRow::New(n)) => {
202                n.move_cursor_right(word);
203            }
204            Some(VariableSuggestionRow::Existing(e)) => {
205                if let Some(ref mut ta) = e.editing {
206                    ta.move_cursor_right(word);
207                }
208            }
209            _ => (),
210        }
211        Ok(Action::NoOp)
212    }
213
214    fn move_prev(&mut self) -> Result<Action> {
215        self.move_up()
216    }
217
218    fn move_next(&mut self) -> Result<Action> {
219        self.move_down()
220    }
221
222    fn move_home(&mut self, absolute: bool) -> Result<Action> {
223        match self.suggestions.selected_mut() {
224            Some(VariableSuggestionRow::New(n)) => {
225                n.move_home(absolute);
226            }
227            Some(VariableSuggestionRow::Existing(e)) => {
228                if let Some(ref mut ta) = e.editing {
229                    ta.move_home(absolute);
230                }
231            }
232            _ => self.suggestions.select_first(),
233        }
234        Ok(Action::NoOp)
235    }
236
237    fn move_end(&mut self, absolute: bool) -> Result<Action> {
238        match self.suggestions.selected_mut() {
239            Some(VariableSuggestionRow::New(n)) => {
240                n.move_end(absolute);
241            }
242            Some(VariableSuggestionRow::Existing(e)) => {
243                if let Some(ref mut ta) = e.editing {
244                    ta.move_end(absolute);
245                }
246            }
247            _ => self.suggestions.select_last(),
248        }
249        Ok(Action::NoOp)
250    }
251
252    fn undo(&mut self) -> Result<Action> {
253        match self.suggestions.selected_mut() {
254            Some(VariableSuggestionRow::New(n)) => {
255                n.undo();
256                if !n.is_secret() {
257                    let query = n.lines_as_string();
258                    self.filter_suggestions(&query);
259                }
260            }
261            Some(VariableSuggestionRow::Existing(e)) => {
262                if let Some(ref mut ta) = e.editing {
263                    ta.undo();
264                }
265            }
266            _ => (),
267        }
268        Ok(Action::NoOp)
269    }
270
271    fn redo(&mut self) -> Result<Action> {
272        match self.suggestions.selected_mut() {
273            Some(VariableSuggestionRow::New(n)) => {
274                n.redo();
275                if !n.is_secret() {
276                    let query = n.lines_as_string();
277                    self.filter_suggestions(&query);
278                }
279            }
280            Some(VariableSuggestionRow::Existing(e)) => {
281                if let Some(ref mut ta) = e.editing {
282                    ta.redo();
283                }
284            }
285            _ => (),
286        }
287        Ok(Action::NoOp)
288    }
289
290    fn insert_text(&mut self, mut text: String) -> Result<Action> {
291        if let Some(variable) = self.command.current_variable() {
292            text = variable.apply_functions_to(text);
293        }
294        match self.suggestions.selected_mut() {
295            Some(VariableSuggestionRow::New(n)) => {
296                n.insert_str(text);
297                if !n.is_secret() {
298                    let query = n.lines_as_string();
299                    self.filter_suggestions(&query);
300                }
301            }
302            Some(VariableSuggestionRow::Existing(e)) => {
303                if let Some(ref mut ta) = e.editing {
304                    ta.insert_str(text);
305                }
306            }
307            _ => (),
308        }
309        Ok(Action::NoOp)
310    }
311
312    fn insert_char(&mut self, c: char) -> Result<Action> {
313        let insert_content = |ta: &mut CustomTextArea<'_>| {
314            if let Some(variable) = self.command.current_variable()
315                && let Some(r) = variable.check_functions_char(c)
316            {
317                ta.insert_str(&r);
318            } else {
319                ta.insert_char(c);
320            }
321        };
322        match self.suggestions.selected_mut() {
323            Some(VariableSuggestionRow::New(n)) => {
324                insert_content(n.deref_mut());
325                if !n.is_secret() {
326                    let query = n.lines_as_string();
327                    self.filter_suggestions(&query);
328                }
329            }
330            Some(VariableSuggestionRow::Existing(e)) if e.editing.is_some() => {
331                if let Some(ref mut ta) = e.editing {
332                    insert_content(ta);
333                }
334            }
335            _ => {
336                if let Some(VariableSuggestionRow::New(_)) = self.suggestions.items().iter().next() {
337                    self.suggestions.select_first();
338                    if let Some(VariableSuggestionRow::New(n)) = self.suggestions.selected_mut() {
339                        insert_content(n.deref_mut());
340                        if !n.is_secret() {
341                            let query = n.lines_as_string();
342                            self.filter_suggestions(&query);
343                        }
344                    }
345                }
346            }
347        }
348        Ok(Action::NoOp)
349    }
350
351    fn delete(&mut self, backspace: bool, word: bool) -> Result<Action> {
352        match self.suggestions.selected_mut() {
353            Some(VariableSuggestionRow::New(n)) => {
354                n.delete(backspace, word);
355                if !n.is_secret() {
356                    let query = n.lines_as_string();
357                    self.filter_suggestions(&query);
358                }
359            }
360            Some(VariableSuggestionRow::Existing(e)) => {
361                if let Some(ref mut ta) = e.editing {
362                    ta.delete(backspace, word);
363                }
364            }
365            _ => (),
366        }
367        Ok(Action::NoOp)
368    }
369
370    #[instrument(skip_all)]
371    async fn selection_delete(&mut self) -> Result<Action> {
372        let suggestion = match self.suggestions.selected_mut() {
373            Some(VariableSuggestionRow::Existing(e)) if e.editing.is_none() => self.suggestions.delete_selected(),
374            _ => return Ok(Action::NoOp),
375        };
376
377        let Some(VariableSuggestionRow::Existing(e)) = suggestion else {
378            return Err(eyre!("Unexpected selected suggestion after removal"));
379        };
380
381        self.service.delete_variable_value(e.value.id.unwrap()).await?;
382
383        Ok(Action::NoOp)
384    }
385
386    #[instrument(skip_all)]
387    async fn selection_update(&mut self) -> Result<Action> {
388        if let Some(VariableSuggestionRow::Existing(e)) = self.suggestions.selected_mut()
389            && e.editing.is_none()
390        {
391            tracing::debug!(
392                "Entering edit mode for existing variable value: {}",
393                e.value.id.unwrap_or_default()
394            );
395            e.enter_edit_mode();
396        }
397        Ok(Action::NoOp)
398    }
399
400    async fn selection_confirm(&mut self) -> Result<Action> {
401        match self.suggestions.selected_mut() {
402            None => Ok(Action::NoOp),
403            Some(VariableSuggestionRow::New(n)) if n.is_secret() => {
404                let value = n.lines_as_string();
405                self.confirm_new_secret_value(value).await
406            }
407            Some(VariableSuggestionRow::New(n)) => {
408                let value = n.lines_as_string();
409                self.confirm_new_regular_value(value).await
410            }
411            Some(VariableSuggestionRow::Existing(e)) => match e.editing.take() {
412                Some(ta) => {
413                    let value = e.value.clone();
414                    let new_value = ta.lines_as_string();
415                    self.confirm_existing_edition(value, new_value).await
416                }
417                None => {
418                    let value = e.value.clone();
419                    self.confirm_existing_value(value, false).await
420                }
421            },
422            Some(VariableSuggestionRow::Environment(l, false)) => {
423                let value = l.to_string();
424                self.confirm_literal_value(value, false).await
425            }
426            Some(VariableSuggestionRow::Environment(l, true)) | Some(VariableSuggestionRow::Derived(l)) => {
427                let value = l.to_string();
428                self.confirm_literal_value(value, true).await
429            }
430        }
431    }
432}
433
434impl VariableReplacementComponent {
435    /// Filters the suggestions widget based on the query
436    fn filter_suggestions(&mut self, query: &str) {
437        if let Some(ref mut ctx) = self.variable_ctx {
438            tracing::debug!("Filtering suggestions for: {query}");
439            // From the original variable suggestions, keep those matching the query only
440            let mut filtered_suggestions = ctx.suggestions.clone();
441            filtered_suggestions.retain(|s| match s {
442                VariableSuggestionRow::New(_) => false,
443                VariableSuggestionRow::Existing(e) => e.value.value.contains(query),
444                VariableSuggestionRow::Environment(l, _) | VariableSuggestionRow::Derived(l) => l.contains(query),
445            });
446            // Find and insert the new row, which contains the query
447            let new_row = self
448                .suggestions
449                .items()
450                .iter()
451                .find(|s| matches!(s, VariableSuggestionRow::New(_)));
452            if let Some(new_row) = new_row.cloned() {
453                filtered_suggestions.insert(0, new_row);
454            }
455            // Update the items
456            self.suggestions.update_items(filtered_suggestions);
457        } else if !self.suggestions.is_empty() {
458            self.suggestions.update_items(Vec::new());
459        }
460    }
461
462    /// Updates the variable context and the suggestions widget, or returns an acton
463    async fn update_variable_context(&mut self, quit_action: bool) -> Result<Action> {
464        let Some(current_variable) = self.command.current_variable() else {
465            if quit_action {
466                tracing::info!("There are no more variables");
467                return self.quit_action(false);
468            } else {
469                return Ok(Action::NoOp);
470            }
471        };
472
473        // Search for suggestions
474        let suggestions = self
475            .service
476            .search_variable_suggestions(
477                &self.command.root,
478                current_variable,
479                self.command.current_variable_context(),
480            )
481            .await?;
482
483        // Map the suggestions to the widget rows
484        let suggestion_widgets = suggestions
485            .into_iter()
486            .map(|s| match s {
487                VariableSuggestion::Secret => VariableSuggestionRow::New(NewVariableValue::new(&self.theme, true)),
488                VariableSuggestion::New => VariableSuggestionRow::New(NewVariableValue::new(&self.theme, false)),
489                VariableSuggestion::Environment { env_var_name, value } => {
490                    if let Some(value) = value {
491                        VariableSuggestionRow::Environment(LiteralVariableValue::new(&self.theme, value), true)
492                    } else {
493                        VariableSuggestionRow::Environment(
494                            LiteralVariableValue::new(&self.theme, format_env_var(env_var_name)),
495                            false,
496                        )
497                    }
498                }
499                VariableSuggestion::Existing(value) => {
500                    VariableSuggestionRow::Existing(ExistingVariableValue::new(&self.theme, value))
501                }
502                VariableSuggestion::Derived(value) => {
503                    VariableSuggestionRow::Derived(LiteralVariableValue::new(&self.theme, value))
504                }
505            })
506            .collect::<Vec<_>>();
507
508        // Update the context
509        self.variable_ctx = Some(CurrentVariableContext {
510            name: current_variable.name.clone(),
511            suggestions: suggestion_widgets.clone(),
512        });
513
514        // Update the suggestions list
515        self.suggestions.update_items(suggestion_widgets);
516        self.suggestions.reset_selection();
517
518        // Pre-select the first environment or existing suggestion
519        if let Some(idx) = self
520            .suggestions
521            .items()
522            .iter()
523            .position(|s| !matches!(s, VariableSuggestionRow::New(_) | VariableSuggestionRow::Derived(_)))
524        {
525            self.suggestions.select(idx);
526        }
527
528        Ok(Action::NoOp)
529    }
530
531    #[instrument(skip_all)]
532    async fn confirm_new_secret_value(&mut self, value: String) -> Result<Action> {
533        tracing::debug!("Secret variable value selected");
534        self.command.set_next_variable(value);
535        self.update_variable_context(true).await
536    }
537
538    #[instrument(skip_all)]
539    async fn confirm_new_regular_value(&mut self, value: String) -> Result<Action> {
540        if !value.trim().is_empty() {
541            let variable_name = &self.variable_ctx.as_ref().unwrap().name;
542            match self
543                .service
544                .insert_variable_value(self.command.new_variable_value_for(variable_name, &value))
545                .await
546            {
547                Ok(v) => {
548                    tracing::debug!("New variable value stored");
549                    self.confirm_existing_value(v, true).await
550                }
551                Err(InsertError::Invalid(err)) => {
552                    tracing::warn!("{err}");
553                    self.error.set_temp_message(err);
554                    Ok(Action::NoOp)
555                }
556                Err(InsertError::AlreadyExists) => {
557                    tracing::warn!("The value already exists");
558                    self.error.set_temp_message("The value already exists");
559                    Ok(Action::NoOp)
560                }
561                Err(InsertError::Unexpected(report)) => Err(report),
562            }
563        } else {
564            tracing::debug!("New empty variable value selected");
565            self.command.set_next_variable(value);
566            self.update_variable_context(true).await
567        }
568    }
569
570    #[instrument(skip_all)]
571    async fn confirm_existing_edition(&mut self, mut value: VariableValue, new_value: String) -> Result<Action> {
572        value.value = new_value;
573        match self.service.update_variable_value(value).await {
574            Ok(v) => {
575                if let VariableSuggestionRow::Existing(e) = self.suggestions.selected_mut().unwrap() {
576                    e.value = v;
577                };
578                Ok(Action::NoOp)
579            }
580            Err(UpdateError::Invalid(err)) => {
581                tracing::warn!("{err}");
582                self.error.set_temp_message(err);
583                Ok(Action::NoOp)
584            }
585            Err(UpdateError::AlreadyExists) => {
586                tracing::warn!("The value already exists");
587                self.error.set_temp_message("The value already exists");
588                Ok(Action::NoOp)
589            }
590            Err(UpdateError::Unexpected(report)) => Err(report),
591        }
592    }
593
594    #[instrument(skip_all)]
595    async fn confirm_existing_value(&mut self, value: VariableValue, new: bool) -> Result<Action> {
596        let value_id = value.id.expect("existing must have id");
597        match self
598            .service
599            .increment_variable_value_usage(value_id, self.command.current_variable_context())
600            .await
601            .map_err(UpdateError::into_report)
602        {
603            Ok(_) => {
604                if !new {
605                    tracing::debug!("Existing variable value selected");
606                }
607                self.command.set_next_variable(value.value);
608                self.update_variable_context(true).await
609            }
610            Err(report) => Err(report),
611        }
612    }
613
614    #[instrument(skip_all)]
615    async fn confirm_literal_value(&mut self, value: String, store: bool) -> Result<Action> {
616        if store && !value.trim().is_empty() {
617            let variable_name = &self.variable_ctx.as_ref().unwrap().name;
618            match self
619                .service
620                .insert_variable_value(self.command.new_variable_value_for(variable_name, &value))
621                .await
622            {
623                Ok(v) => {
624                    tracing::debug!("Literal variable value selected and stored");
625                    self.confirm_existing_value(v, true).await
626                }
627                Err(InsertError::Invalid(_) | InsertError::AlreadyExists) => {
628                    tracing::debug!("Literal variable value selected but couldn't be stored");
629                    self.command.set_next_variable(value);
630                    self.update_variable_context(true).await
631                }
632                Err(InsertError::Unexpected(report)) => Err(report),
633            }
634        } else {
635            tracing::debug!("Literal variable value selected");
636            self.command.set_next_variable(value);
637            self.update_variable_context(true).await
638        }
639    }
640
641    /// Returns an action to quit the component, with the current variable content
642    fn quit_action(&self, peek: bool) -> Result<Action> {
643        let cmd = self.command.to_string();
644        if self.execute_mode {
645            Ok(Action::Quit(ProcessOutput::execute(cmd)))
646        } else if self.replace_process && peek {
647            Ok(Action::Quit(
648                ProcessOutput::success()
649                    .stderr(format_msg!(self.theme, "There are no variables to replace"))
650                    .stdout(&cmd)
651                    .fileout(cmd),
652            ))
653        } else {
654            Ok(Action::Quit(ProcessOutput::success().stdout(&cmd).fileout(cmd)))
655        }
656    }
657}