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
29pub struct VariableReplacementComponent {
31 theme: Theme,
33 service: IntelliShellService,
35 layout: Layout,
37 execute_mode: bool,
39 replace_process: bool,
41 command: DynamicCommandWidget,
43 variable_ctx: Option<CurrentVariableContext>,
45 suggestions: CustomList<'static, VariableSuggestionRow<'static>>,
47 new_version: NewVersionBanner,
49 error: ErrorPopup<'static>,
51}
52struct CurrentVariableContext {
53 name: String,
55 suggestions: Vec<VariableSuggestionRow<'static>>,
57}
58
59impl VariableReplacementComponent {
60 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 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 let [cmd_area, suggestions_area] = self.layout.areas(area);
130
131 frame.render_widget(&self.command, cmd_area);
133
134 frame.render_widget(&mut self.suggestions, suggestions_area);
136
137 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 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 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 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 self.suggestions.update_items(filtered_suggestions);
457 } else if !self.suggestions.is_empty() {
458 self.suggestions.update_items(Vec::new());
459 }
460 }
461
462 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 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 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(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 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}