1use color_eyre::Result;
2use crossterm::event::MouseEventKind;
3use tokio_util::sync::CancellationToken;
4use tracing::instrument;
5
6use crate::{
7 cli::{CliProcess, CompletionProcess, Interactive},
8 component::{Component, EmptyComponent},
9 config::{Config, KeyBindingsConfig},
10 errors::AppError,
11 process::{InteractiveProcess, Process, ProcessOutput},
12 service::IntelliShellService,
13 tui::{Event, Tui},
14};
15
16#[derive(Default)]
18pub enum Action {
19 #[default]
21 NoOp,
22 Quit(ProcessOutput),
24 SwitchComponent(Box<dyn Component>),
26}
27
28pub struct App {
30 cancellation_token: CancellationToken,
31 active_component: Box<dyn Component>,
32}
33impl App {
34 pub fn new(cancellation_token: CancellationToken) -> Result<Self> {
36 Ok(Self {
37 cancellation_token,
38 active_component: Box::new(EmptyComponent),
39 })
40 }
41
42 #[instrument(skip_all)]
49 pub async fn run(
50 self,
51 config: Config,
52 service: IntelliShellService,
53 process: CliProcess,
54 extra_line: bool,
55 ) -> Result<ProcessOutput> {
56 match process {
57 #[cfg(debug_assertions)]
58 CliProcess::Query(query_process) => {
59 tracing::info!("Running 'query' process");
60 tracing::debug!("Options: {:?}", query_process);
61 service.load_workspace_items().await.map_err(AppError::into_report)?;
62 self.run_non_interactive(query_process, config, service, extra_line)
63 .await
64 }
65 CliProcess::Init(_) => unreachable!("Handled in main"),
66 CliProcess::New(bookmark_command) => {
67 tracing::info!("Running 'new' process");
68 tracing::debug!("Options: {:?}", bookmark_command);
69 self.run_interactive(bookmark_command, config, service, extra_line)
70 .await
71 }
72 CliProcess::Search(search_commands) => {
73 tracing::info!("Running 'search' process");
74 tracing::debug!("Options: {:?}", search_commands);
75 service.load_workspace_items().await.map_err(AppError::into_report)?;
76 self.run_interactive(search_commands, config, service, extra_line).await
77 }
78 CliProcess::Replace(variable_replace) => {
79 tracing::info!("Running 'replace' process");
80 tracing::debug!("Options: {:?}", variable_replace);
81 service.load_workspace_items().await.map_err(AppError::into_report)?;
82 self.run_interactive(variable_replace, config, service, extra_line)
83 .await
84 }
85 CliProcess::Fix(fix_command) => {
86 tracing::info!("Running 'fix' process");
87 tracing::debug!("Options: {:?}", fix_command);
88 self.run_non_interactive(fix_command, config, service, extra_line).await
89 }
90 CliProcess::Export(export_commands) => {
91 tracing::info!("Running 'export' process");
92 tracing::debug!("Options: {:?}", export_commands);
93 self.run_interactive(export_commands, config, service, extra_line).await
94 }
95 CliProcess::Import(import_commands) => {
96 tracing::info!("Running 'import' process");
97 tracing::debug!("Options: {:?}", import_commands);
98 self.run_interactive(import_commands, config, service, extra_line).await
99 }
100 #[cfg(feature = "tldr")]
101 CliProcess::Tldr(crate::cli::TldrProcess::Fetch(tldr_fetch)) => {
102 tracing::info!("Running tldr 'fetch' process");
103 tracing::debug!("Options: {:?}", tldr_fetch);
104 self.run_non_interactive(tldr_fetch, config, service, extra_line).await
105 }
106 #[cfg(feature = "tldr")]
107 CliProcess::Tldr(crate::cli::TldrProcess::Clear(tldr_clear)) => {
108 tracing::info!("Running tldr 'clear' process");
109 tracing::debug!("Options: {:?}", tldr_clear);
110 self.run_non_interactive(tldr_clear, config, service, extra_line).await
111 }
112 CliProcess::Completion(CompletionProcess::New(completion_new)) => {
113 tracing::info!("Running 'completion new' process");
114 tracing::debug!("Options: {:?}", completion_new);
115 self.run_interactive(completion_new, config, service, extra_line).await
116 }
117 CliProcess::Completion(CompletionProcess::Delete(completion_delete)) => {
118 tracing::info!("Running 'completion delete' process");
119 tracing::debug!("Options: {:?}", completion_delete);
120 self.run_non_interactive(completion_delete, config, service, extra_line)
121 .await
122 }
123 CliProcess::Completion(CompletionProcess::List(completion_list)) => {
124 tracing::info!("Running 'completion list' process");
125 tracing::debug!("Options: {:?}", completion_list);
126 service.load_workspace_items().await.map_err(AppError::into_report)?;
127 self.run_interactive(completion_list, config, service, extra_line).await
128 }
129 #[cfg(feature = "self-update")]
130 CliProcess::Update(update) => {
131 tracing::info!("Running 'update' process");
132 tracing::debug!("Options: {:?}", update);
133 self.run_non_interactive(update, config, service, extra_line).await
134 }
135 }
136 }
137
138 async fn run_non_interactive(
142 self,
143 process: impl Process,
144 config: Config,
145 service: IntelliShellService,
146 extra_line: bool,
147 ) -> Result<ProcessOutput> {
148 if extra_line {
149 println!();
150 }
151 process.execute(config, service, self.cancellation_token).await
152 }
153
154 async fn run_interactive(
156 mut self,
157 it: Interactive<impl InteractiveProcess>,
158 config: Config,
159 service: IntelliShellService,
160 extra_line: bool,
161 ) -> Result<ProcessOutput> {
162 if !it.opts.interactive {
164 return self.run_non_interactive(it.process, config, service, extra_line).await;
165 }
166
167 let inline = it.opts.inline || (!it.opts.full_screen && config.inline);
169 let keybindings = config.keybindings.clone();
170 self.active_component = it
171 .process
172 .into_component(config, service, inline, self.cancellation_token.clone())?;
173
174 let peek_action = self.active_component.init_and_peek().await?;
176 if let Some(output) = self.process_action(peek_action).await? {
177 tracing::debug!("A result was received from `init_and_peek`, returning it");
178 return Ok(output);
179 }
180
181 let mut tui = Tui::new(self.cancellation_token.clone())?.paste(true).mouse(true);
183 if inline {
184 tracing::debug!("Displaying inline {} interactively", self.active_component.name());
185 tui.enter_inline(extra_line, self.active_component.min_inline_height())?;
186 } else {
187 tracing::debug!("Displaying full-screen {} interactively", self.active_component.name());
188 tui.enter()?;
189 }
190
191 loop {
193 tokio::select! {
194 biased;
195 _ = self.cancellation_token.cancelled() => {
197 tracing::info!("Cancellation token received, exiting TUI loop");
198 return Ok(ProcessOutput::fail());
199 }
200 maybe_event = tui.next_event() => {
202 let Some(tui_event) = maybe_event else {
203 tracing::error!("TUI closed unexpectedly, no event received");
204 break;
205 };
206 let action = self.handle_tui_event(tui_event, &mut tui, &keybindings).await?;
208 if let Some(output) = self.process_action(action).await? {
210 return Ok(output);
212 }
213 }
214 }
215 }
216
217 Ok(ProcessOutput::success())
218 }
219
220 #[instrument(skip_all)]
226 async fn handle_tui_event(
227 &mut self,
228 event: Event,
229 tui: &mut Tui,
230 keybindings: &KeyBindingsConfig,
231 ) -> Result<Action> {
232 if event != Event::Tick
233 && event != Event::Render
234 && !matches!(event, Event::Mouse(m) if m.kind == MouseEventKind::Moved )
235 {
236 tracing::trace!("{event:?}");
237 }
238 let ac = &mut self.active_component;
239 Ok(match event {
240 Event::Render => {
242 tui.render(|frame, area| ac.render(frame, area))?;
243 Action::NoOp
244 }
245 Event::Tick => ac.tick()?,
247 Event::FocusGained => ac.focus_gained()?,
248 Event::FocusLost => ac.focus_lost()?,
249 Event::Resize(width, height) => ac.resize(width, height)?,
250 Event::Paste(content) => ac.process_paste_event(content)?,
251 Event::Key(key) => ac.process_key_event(keybindings, key).await?,
252 Event::Mouse(mouse) => ac.process_mouse_event(mouse)?,
253 })
254 }
255
256 #[instrument(skip_all)]
260 async fn process_action(&mut self, action: Action) -> Result<Option<ProcessOutput>> {
261 match action {
262 Action::NoOp => (),
263 Action::Quit(output) => return Ok(Some(output)),
264 Action::SwitchComponent(next_component) => {
265 tracing::debug!(
266 "Switching active component: {} -> {}",
267 self.active_component.name(),
268 next_component.name()
269 );
270 self.active_component = next_component;
271 let peek_action = self.active_component.init_and_peek().await?;
273 if let Some(output) = Box::pin(self.process_action(peek_action)).await? {
274 tracing::debug!("A result was received from `init_and_peek`, returning it");
275 return Ok(Some(output));
276 }
277 }
278 }
279 Ok(None)
280 }
281}