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
28pub struct VariableReplacementComponent {
30 theme: Theme,
32 service: IntelliShellService,
34 layout: Layout,
36 execute_mode: bool,
38 replace_process: bool,
40 command: DynamicCommandWidget,
42 variable_ctx: Option<CurrentVariableContext>,
44 suggestions: CustomList<'static, VariableSuggestionRow<'static>>,
46 error: ErrorPopup<'static>,
48}
49struct CurrentVariableContext {
50 name: String,
52 suggestions: Vec<VariableSuggestionRow<'static>>,
54}
55
56impl VariableReplacementComponent {
57 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 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 let [cmd_area, suggestions_area] = self.layout.areas(area);
120
121 frame.render_widget(&self.command, cmd_area);
123
124 frame.render_widget(&mut self.suggestions, suggestions_area);
126
127 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 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 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 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 self.suggestions.update_items(filtered_suggestions);
456 } else if !self.suggestions.is_empty() {
457 self.suggestions.update_items(Vec::new());
458 }
459 }
460
461 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 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 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 self.variable_ctx = Some(CurrentVariableContext {
510 name: current_variable.name.clone(),
511 suggestions: suggestion_widgets.clone(),
512 });
513
514 self.suggestions.update_items(suggestion_widgets);
516 self.suggestions.reset_selection();
517
518 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 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}