intelli_shell/
app.rs

1use color_eyre::Result;
2use crossterm::event::MouseEventKind;
3use tracing::instrument;
4
5use crate::{
6    cli::{CliProcess, CompletionProcess, Interactive, TldrProcess},
7    component::{Component, EmptyComponent},
8    config::{Config, KeyBindingsConfig},
9    errors::AppError,
10    process::{InteractiveProcess, Process, ProcessOutput},
11    service::IntelliShellService,
12    tui::{Event, Tui},
13};
14
15/// Represents actions that components or the application can signal to change the application state or flow.
16#[derive(Default)]
17pub enum Action {
18    /// No-op action, nothing has to be done
19    #[default]
20    NoOp,
21    /// Signals that the application should quit, providing the output
22    Quit(ProcessOutput),
23    /// Signals that the active component should be switched to the provided one
24    SwitchComponent(Box<dyn Component>),
25}
26
27/// The main application struct, holding configuration and managing the application flow
28pub struct App {
29    active_component: Box<dyn Component>,
30}
31impl App {
32    /// Creates a new instance of the application
33    pub fn new() -> Result<Self> {
34        Ok(Self {
35            active_component: Box::new(EmptyComponent),
36        })
37    }
38
39    /// Runs the main application logic based on the parsed CLI arguments.
40    ///
41    /// This method dispatches to either an interactive or non-interactive process execution based on the provided `Cli`
42    /// arguments and the specific subcommand.
43    ///
44    /// It returns the final [ProcessOutput] when the application finishes.
45    #[instrument(skip_all)]
46    pub async fn run(
47        self,
48        config: Config,
49        service: IntelliShellService,
50        process: CliProcess,
51        extra_line: bool,
52    ) -> Result<ProcessOutput> {
53        match process {
54            #[cfg(debug_assertions)]
55            CliProcess::Query(query_process) => {
56                tracing::info!("Running 'query' process");
57                tracing::debug!("Options: {:?}", query_process);
58                service.load_workspace_items().await.map_err(AppError::into_report)?;
59                self.run_non_interactive(query_process, config, service, extra_line)
60                    .await
61            }
62            CliProcess::Init(_) => unreachable!("Handled in main"),
63            CliProcess::New(bookmark_command) => {
64                tracing::info!("Running 'new' process");
65                tracing::debug!("Options: {:?}", bookmark_command);
66                self.run_interactive(bookmark_command, config, service, extra_line)
67                    .await
68            }
69            CliProcess::Search(search_commands) => {
70                tracing::info!("Running 'search' process");
71                tracing::debug!("Options: {:?}", search_commands);
72                service.load_workspace_items().await.map_err(AppError::into_report)?;
73                self.run_interactive(search_commands, config, service, extra_line).await
74            }
75            CliProcess::Replace(variable_replace) => {
76                tracing::info!("Running 'replace' process");
77                tracing::debug!("Options: {:?}", variable_replace);
78                service.load_workspace_items().await.map_err(AppError::into_report)?;
79                self.run_interactive(variable_replace, config, service, extra_line)
80                    .await
81            }
82            CliProcess::Fix(fix_command) => {
83                tracing::info!("Running 'fix' process");
84                tracing::debug!("Options: {:?}", fix_command);
85                self.run_non_interactive(fix_command, config, service, extra_line).await
86            }
87            CliProcess::Export(export_commands) => {
88                tracing::info!("Running 'export' process");
89                tracing::debug!("Options: {:?}", export_commands);
90                self.run_interactive(export_commands, config, service, extra_line).await
91            }
92            CliProcess::Import(import_commands) => {
93                tracing::info!("Running 'import' process");
94                tracing::debug!("Options: {:?}", import_commands);
95                self.run_interactive(import_commands, config, service, extra_line).await
96            }
97            CliProcess::Tldr(TldrProcess::Fetch(tldr_fetch)) => {
98                tracing::info!("Running tldr 'fetch' process");
99                tracing::debug!("Options: {:?}", tldr_fetch);
100                self.run_non_interactive(tldr_fetch, config, service, extra_line).await
101            }
102            CliProcess::Tldr(TldrProcess::Clear(tldr_clear)) => {
103                tracing::info!("Running tldr 'clear' process");
104                tracing::debug!("Options: {:?}", tldr_clear);
105                self.run_non_interactive(tldr_clear, config, service, extra_line).await
106            }
107            CliProcess::Completion(CompletionProcess::New(completion_new)) => {
108                tracing::info!("Running 'completion new' process");
109                tracing::debug!("Options: {:?}", completion_new);
110                self.run_interactive(completion_new, config, service, extra_line).await
111            }
112            CliProcess::Completion(CompletionProcess::Delete(completion_delete)) => {
113                tracing::info!("Running 'completion delete' process");
114                tracing::debug!("Options: {:?}", completion_delete);
115                self.run_non_interactive(completion_delete, config, service, extra_line)
116                    .await
117            }
118            CliProcess::Completion(CompletionProcess::List(completion_list)) => {
119                tracing::info!("Running 'completion list' process");
120                tracing::debug!("Options: {:?}", completion_list);
121                service.load_workspace_items().await.map_err(AppError::into_report)?;
122                self.run_interactive(completion_list, config, service, extra_line).await
123            }
124            CliProcess::Update(update) => {
125                tracing::info!("Running 'update' process");
126                tracing::debug!("Options: {:?}", update);
127                self.run_non_interactive(update, config, service, extra_line).await
128            }
129        }
130    }
131
132    /// Executes a process in non-interactive mode.
133    ///
134    /// Simply calls the `execute` method on the given [Process] implementation.
135    async fn run_non_interactive(
136        self,
137        process: impl Process,
138        config: Config,
139        service: IntelliShellService,
140        extra_line: bool,
141    ) -> Result<ProcessOutput> {
142        if extra_line {
143            println!();
144        }
145        process.execute(config, service).await
146    }
147
148    /// Executes a process that might require an interactive TUI
149    async fn run_interactive(
150        mut self,
151        it: Interactive<impl InteractiveProcess>,
152        config: Config,
153        service: IntelliShellService,
154        extra_line: bool,
155    ) -> Result<ProcessOutput> {
156        // If the process hasn't enabled the interactive flag, just run it
157        if !it.opts.interactive {
158            return self.run_non_interactive(it.process, config, service, extra_line).await;
159        }
160
161        // Converts the process into the renderable component and initializes it
162        let inline = it.opts.inline || (!it.opts.full_screen && config.inline);
163        let keybindings = config.keybindings.clone();
164        self.active_component = it.process.into_component(config, service, inline)?;
165
166        // Initialize and peek into the component, in case we can give a straight result
167        let peek_action = self.active_component.init_and_peek().await?;
168        if let Some(output) = self.process_action(peek_action).await? {
169            tracing::debug!("A result was received from `init_and_peek`, returning it");
170            return Ok(output);
171        }
172
173        // Enter the TUI (inline or fullscreen)
174        let mut tui = Tui::new()?.paste(true).mouse(true);
175        if inline {
176            tracing::debug!("Displaying inline {} interactively", self.active_component.name());
177            tui.enter_inline(extra_line, self.active_component.min_inline_height())?;
178        } else {
179            tracing::debug!("Displaying full-screen {} interactively", self.active_component.name());
180            tui.enter()?;
181        }
182
183        // Main loop
184        loop {
185            // Wait for the next event to come in
186            let Some(tui_event) = tui.next_event().await else {
187                tracing::error!("TUI closed unexpectedly, no event received");
188                break;
189            };
190            // Handle the event
191            let action = self.handle_tui_event(tui_event, &mut tui, &keybindings).await?;
192            // Process the action
193            if let Some(output) = self.process_action(action).await? {
194                // If the action generated an output, exit the loop by returning it
195                return Ok(output);
196            }
197        }
198
199        Ok(ProcessOutput::success())
200    }
201
202    /// Handles a single TUI event by dispatching it to the active component.
203    ///
204    /// Based on the type of [Event], calls the corresponding method on the currently active [Component].
205    ///
206    /// Returns an [Action] indicating the result of the event processing.
207    #[instrument(skip_all)]
208    async fn handle_tui_event(
209        &mut self,
210        event: Event,
211        tui: &mut Tui,
212        keybindings: &KeyBindingsConfig,
213    ) -> Result<Action> {
214        if event != Event::Tick
215            && event != Event::Render
216            && !matches!(event, Event::Mouse(m) if m.kind == MouseEventKind::Moved )
217        {
218            tracing::trace!("{event:?}");
219        }
220        let ac = &mut self.active_component;
221        Ok(match event {
222            // Render the active component using the TUI renderer
223            Event::Render => {
224                tui.render(|frame, area| ac.render(frame, area))?;
225                Action::NoOp
226            }
227            // Dispatch other events to the active component
228            Event::Tick => ac.tick()?,
229            Event::FocusGained => ac.focus_gained()?,
230            Event::FocusLost => ac.focus_lost()?,
231            Event::Resize(width, height) => ac.resize(width, height)?,
232            Event::Paste(content) => ac.process_paste_event(content)?,
233            Event::Key(key) => ac.process_key_event(keybindings, key).await?,
234            Event::Mouse(mouse) => ac.process_mouse_event(mouse)?,
235        })
236    }
237
238    /// Processes an [Action] returned by a component.
239    ///
240    /// Returns an optional [ProcessOutput] if the action signals the application should exit.
241    #[instrument(skip_all)]
242    async fn process_action(&mut self, action: Action) -> Result<Option<ProcessOutput>> {
243        match action {
244            Action::NoOp => (),
245            Action::Quit(output) => return Ok(Some(output)),
246            Action::SwitchComponent(next_component) => {
247                tracing::debug!(
248                    "Switching active component: {} -> {}",
249                    self.active_component.name(),
250                    next_component.name()
251                );
252                self.active_component = next_component;
253                // Initialize and peek into the new component to see if it can provide an immediate result
254                let peek_action = self.active_component.init_and_peek().await?;
255                if let Some(output) = Box::pin(self.process_action(peek_action)).await? {
256                    tracing::debug!("A result was received from `init_and_peek`, returning it");
257                    return Ok(Some(output));
258                }
259            }
260        }
261        Ok(None)
262    }
263}