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