intelli_shell/
app.rs

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