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(_) | CliProcess::Config(_) | CliProcess::Logs(_) => 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 CliProcess::Changelog(changelog) => {
136 tracing::info!("Running 'changelog' process");
137 tracing::debug!("Options: {:?}", changelog);
138 self.run_non_interactive(changelog, config, service, extra_line).await
139 }
140 }
141 }
142
143 async fn run_non_interactive(
147 self,
148 process: impl Process,
149 config: Config,
150 service: IntelliShellService,
151 extra_line: bool,
152 ) -> Result<ProcessOutput> {
153 if extra_line {
154 println!();
155 }
156 process.execute(config, service, self.cancellation_token).await
157 }
158
159 async fn run_interactive(
161 mut self,
162 it: Interactive<impl InteractiveProcess>,
163 config: Config,
164 service: IntelliShellService,
165 extra_line: bool,
166 ) -> Result<ProcessOutput> {
167 if !it.opts.interactive {
169 return self.run_non_interactive(it.process, config, service, extra_line).await;
170 }
171
172 let inline = it.opts.inline || (!it.opts.full_screen && config.inline);
174 let keybindings = config.keybindings.clone();
175 let keyboard_enhancement = config.tui.keyboard_enhancement;
176 self.active_component = it
177 .process
178 .into_component(config, service, inline, self.cancellation_token.clone())?;
179
180 let peek_action = self.active_component.init_and_peek().await?;
182 if let Some(output) = self.process_action(peek_action).await? {
183 tracing::debug!("A result was received from `init_and_peek`, returning it");
184 return Ok(output);
185 }
186
187 let mut tui = Tui::new(self.cancellation_token.clone())?
189 .paste(true)
190 .mouse(true)
191 .keyboard_enhancement(keyboard_enhancement);
192 if inline {
193 tracing::debug!("Displaying inline {} interactively", self.active_component.name());
194 tui.enter_inline(extra_line, self.active_component.min_inline_height())?;
195 } else {
196 tracing::debug!("Displaying full-screen {} interactively", self.active_component.name());
197 tui.enter()?;
198 }
199
200 loop {
202 tokio::select! {
203 biased;
204 _ = self.cancellation_token.cancelled() => {
206 tracing::info!("Cancellation token received, exiting TUI loop");
207 return Ok(ProcessOutput::fail());
208 }
209 maybe_event = tui.next_event() => {
211 let Some(tui_event) = maybe_event else {
212 tracing::error!("TUI closed unexpectedly, no event received");
213 break;
214 };
215 let action = self.handle_tui_event(tui_event, &mut tui, &keybindings).await?;
217 if let Some(output) = self.process_action(action).await? {
219 return Ok(output);
221 }
222 }
223 }
224 }
225
226 Ok(ProcessOutput::success())
227 }
228
229 #[instrument(skip_all)]
235 async fn handle_tui_event(
236 &mut self,
237 event: Event,
238 tui: &mut Tui,
239 keybindings: &KeyBindingsConfig,
240 ) -> Result<Action> {
241 if event != Event::Tick
242 && event != Event::Render
243 && !matches!(event, Event::Mouse(m) if m.kind == MouseEventKind::Moved )
244 {
245 tracing::trace!("{event:?}");
246 }
247 let ac = &mut self.active_component;
248 Ok(match event {
249 Event::Render => {
251 tui.render(|frame, area| ac.render(frame, area))?;
252 Action::NoOp
253 }
254 Event::Tick => ac.tick()?,
256 Event::FocusGained => ac.focus_gained()?,
257 Event::FocusLost => ac.focus_lost()?,
258 Event::Resize(width, height) => ac.resize(width, height)?,
259 Event::Paste(content) => ac.process_paste_event(content)?,
260 Event::Key(key) => ac.process_key_event(keybindings, key).await?,
261 Event::Mouse(mouse) => ac.process_mouse_event(mouse)?,
262 })
263 }
264
265 #[instrument(skip_all)]
269 async fn process_action(&mut self, action: Action) -> Result<Option<ProcessOutput>> {
270 match action {
271 Action::NoOp => (),
272 Action::Quit(output) => return Ok(Some(output)),
273 Action::SwitchComponent(next_component) => {
274 tracing::debug!(
275 "Switching active component: {} -> {}",
276 self.active_component.name(),
277 next_component.name()
278 );
279 self.active_component = next_component;
280 let peek_action = self.active_component.init_and_peek().await?;
282 if let Some(output) = Box::pin(self.process_action(peek_action)).await? {
283 tracing::debug!("A result was received from `init_and_peek`, returning it");
284 return Ok(Some(output));
285 }
286 }
287 }
288 Ok(None)
289 }
290}