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    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_commands().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_commands().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                self.run_interactive(variable_replace, config, service, extra_line)
79                    .await
80            }
81            CliProcess::Fix(fix_command) => {
82                tracing::info!("Running 'fix' process");
83                tracing::debug!("Options: {:?}", fix_command);
84                self.run_non_interactive(fix_command, config, service, extra_line).await
85            }
86            CliProcess::Export(export_commands) => {
87                tracing::info!("Running 'export' process");
88                tracing::debug!("Options: {:?}", export_commands);
89                self.run_interactive(export_commands, config, service, extra_line).await
90            }
91            CliProcess::Import(import_commands) => {
92                tracing::info!("Running 'import' process");
93                tracing::debug!("Options: {:?}", import_commands);
94                self.run_interactive(import_commands, config, service, extra_line).await
95            }
96            CliProcess::Tldr(TldrProcess::Fetch(tldr_fetch)) => {
97                tracing::info!("Running tldr 'fetch' process");
98                tracing::debug!("Options: {:?}", tldr_fetch);
99                self.run_non_interactive(tldr_fetch, config, service, extra_line).await
100            }
101            CliProcess::Tldr(TldrProcess::Clear(tldr_clear)) => {
102                tracing::info!("Running tldr 'clear' process");
103                tracing::debug!("Options: {:?}", tldr_clear);
104                self.run_non_interactive(tldr_clear, config, service, extra_line).await
105            }
106        }
107    }
108
109    /// Executes a process in non-interactive mode.
110    ///
111    /// Simply calls the `execute` method on the given [Process] implementation.
112    async fn run_non_interactive(
113        self,
114        process: impl Process,
115        config: Config,
116        service: IntelliShellService,
117        extra_line: bool,
118    ) -> Result<ProcessOutput> {
119        if extra_line {
120            println!();
121        }
122        process.execute(config, service).await
123    }
124
125    /// Executes a process that might require an interactive TUI
126    async fn run_interactive(
127        mut self,
128        it: Interactive<impl InteractiveProcess>,
129        config: Config,
130        service: IntelliShellService,
131        extra_line: bool,
132    ) -> Result<ProcessOutput> {
133        // If the process hasn't enabled the interactive flag, just run it
134        if !it.opts.interactive {
135            return self.run_non_interactive(it.process, config, service, extra_line).await;
136        }
137
138        // Converts the process into the renderable component and initializes it
139        let inline = it.opts.inline || (!it.opts.full_screen && config.inline);
140        let keybindings = config.keybindings.clone();
141        self.active_component = it.process.into_component(config, service, inline)?;
142
143        // Initialize and peek into the component, in case we can give a straight result
144        let peek_action = self.active_component.init_and_peek().await?;
145        if let Some(output) = self.process_action(peek_action).await? {
146            tracing::debug!("A result was received from `init_and_peek`, returning it");
147            return Ok(output);
148        }
149
150        // Enter the TUI (inline or fullscreen)
151        let mut tui = Tui::new()?.paste(true).mouse(true);
152        if inline {
153            tracing::debug!("Displaying inline {} interactively", self.active_component.name());
154            tui.enter_inline(extra_line, self.active_component.min_inline_height())?;
155        } else {
156            tracing::debug!("Displaying full-screen {} interactively", self.active_component.name());
157            tui.enter()?;
158        }
159
160        // Main loop
161        loop {
162            // Wait for the next event to come in
163            let Some(tui_event) = tui.next_event().await else {
164                tracing::error!("TUI closed unexpectedly, no event received");
165                break;
166            };
167            // Handle the event
168            let action = self.handle_tui_event(tui_event, &mut tui, &keybindings).await?;
169            // Process the action
170            if let Some(output) = self.process_action(action).await? {
171                // If the action generated an output, exit the loop by returning it
172                return Ok(output);
173            }
174        }
175
176        Ok(ProcessOutput::success())
177    }
178
179    /// Handles a single TUI event by dispatching it to the active component.
180    ///
181    /// Based on the type of [Event], calls the corresponding method on the currently active [Component].
182    ///
183    /// Returns an [Action] indicating the result of the event processing.
184    #[instrument(skip_all)]
185    async fn handle_tui_event(
186        &mut self,
187        event: Event,
188        tui: &mut Tui,
189        keybindings: &KeyBindingsConfig,
190    ) -> Result<Action> {
191        if event != Event::Tick
192            && event != Event::Render
193            && !matches!(event, Event::Mouse(m) if m.kind == MouseEventKind::Moved )
194        {
195            tracing::trace!("{event:?}");
196        }
197        let ac = &mut self.active_component;
198        Ok(match event {
199            // Render the active component using the TUI renderer
200            Event::Render => {
201                tui.render(|frame, area| ac.render(frame, area))?;
202                Action::NoOp
203            }
204            // Dispatch other events to the active component
205            Event::Tick => ac.tick()?,
206            Event::FocusGained => ac.focus_gained()?,
207            Event::FocusLost => ac.focus_lost()?,
208            Event::Resize(width, height) => ac.resize(width, height)?,
209            Event::Paste(content) => ac.process_paste_event(content)?,
210            Event::Key(key) => ac.process_key_event(keybindings, key).await?,
211            Event::Mouse(mouse) => ac.process_mouse_event(mouse)?,
212        })
213    }
214
215    /// Processes an [Action] returned by a component.
216    ///
217    /// Returns an optional [ProcessOutput] if the action signals the application should exit.
218    #[instrument(skip_all)]
219    async fn process_action(&mut self, action: Action) -> Result<Option<ProcessOutput>> {
220        match action {
221            Action::NoOp => (),
222            Action::Quit(output) => return Ok(Some(output)),
223            Action::SwitchComponent(next_component) => {
224                tracing::debug!(
225                    "Switching active component: {} -> {}",
226                    self.active_component.name(),
227                    next_component.name()
228                );
229                self.active_component = next_component;
230                // Initialize and peek into the new component to see if it can provide an immediate result
231                let peek_action = self.active_component.init_and_peek().await?;
232                if let Some(output) = Box::pin(self.process_action(peek_action)).await? {
233                    tracing::debug!("A result was received from `init_and_peek`, returning it");
234                    return Ok(Some(output));
235                }
236            }
237        }
238        Ok(None)
239    }
240}