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#[derive(Default)]
17pub enum Action {
18 #[default]
20 NoOp,
21 Quit(ProcessOutput),
23 SwitchComponent(Box<dyn Component>),
25}
26
27pub struct App {
29 active_component: Box<dyn Component>,
30}
31impl App {
32 pub fn new() -> Result<Self> {
34 Ok(Self {
35 active_component: Box::new(EmptyComponent),
36 })
37 }
38
39 #[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 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 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 !it.opts.interactive {
135 return self.run_non_interactive(it.process, config, service, extra_line).await;
136 }
137
138 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 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 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 loop {
162 let Some(tui_event) = tui.next_event().await else {
164 tracing::error!("TUI closed unexpectedly, no event received");
165 break;
166 };
167 let action = self.handle_tui_event(tui_event, &mut tui, &keybindings).await?;
169 if let Some(output) = self.process_action(action).await? {
171 return Ok(output);
173 }
174 }
175
176 Ok(ProcessOutput::success())
177 }
178
179 #[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 Event::Render => {
201 tui.render(|frame, area| ac.render(frame, area))?;
202 Action::NoOp
203 }
204 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 #[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 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}