1use color_eyre::Result;
2use crossterm::event::MouseEventKind;
3use tracing::instrument;
4
5use crate::{
6 cli::{CliProcess, CompletionProcess, 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_items().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_items().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 service.load_workspace_items().await.map_err(AppError::into_report)?;
79 self.run_interactive(variable_replace, config, service, extra_line)
80 .await
81 }
82 CliProcess::Fix(fix_command) => {
83 tracing::info!("Running 'fix' process");
84 tracing::debug!("Options: {:?}", fix_command);
85 self.run_non_interactive(fix_command, config, service, extra_line).await
86 }
87 CliProcess::Export(export_commands) => {
88 tracing::info!("Running 'export' process");
89 tracing::debug!("Options: {:?}", export_commands);
90 self.run_interactive(export_commands, config, service, extra_line).await
91 }
92 CliProcess::Import(import_commands) => {
93 tracing::info!("Running 'import' process");
94 tracing::debug!("Options: {:?}", import_commands);
95 self.run_interactive(import_commands, config, service, extra_line).await
96 }
97 CliProcess::Tldr(TldrProcess::Fetch(tldr_fetch)) => {
98 tracing::info!("Running tldr 'fetch' process");
99 tracing::debug!("Options: {:?}", tldr_fetch);
100 self.run_non_interactive(tldr_fetch, config, service, extra_line).await
101 }
102 CliProcess::Tldr(TldrProcess::Clear(tldr_clear)) => {
103 tracing::info!("Running tldr 'clear' process");
104 tracing::debug!("Options: {:?}", tldr_clear);
105 self.run_non_interactive(tldr_clear, config, service, extra_line).await
106 }
107 CliProcess::Completion(CompletionProcess::New(completion_new)) => {
108 tracing::info!("Running 'completion new' process");
109 tracing::debug!("Options: {:?}", completion_new);
110 self.run_interactive(completion_new, config, service, extra_line).await
111 }
112 CliProcess::Completion(CompletionProcess::Delete(completion_delete)) => {
113 tracing::info!("Running 'completion delete' process");
114 tracing::debug!("Options: {:?}", completion_delete);
115 self.run_non_interactive(completion_delete, config, service, extra_line)
116 .await
117 }
118 CliProcess::Completion(CompletionProcess::List(completion_list)) => {
119 tracing::info!("Running 'completion list' process");
120 tracing::debug!("Options: {:?}", completion_list);
121 service.load_workspace_items().await.map_err(AppError::into_report)?;
122 self.run_interactive(completion_list, config, service, extra_line).await
123 }
124 CliProcess::Update(update) => {
125 tracing::info!("Running 'update' process");
126 tracing::debug!("Options: {:?}", update);
127 self.run_non_interactive(update, config, service, extra_line).await
128 }
129 }
130 }
131
132 async fn run_non_interactive(
136 self,
137 process: impl Process,
138 config: Config,
139 service: IntelliShellService,
140 extra_line: bool,
141 ) -> Result<ProcessOutput> {
142 if extra_line {
143 println!();
144 }
145 process.execute(config, service).await
146 }
147
148 async fn run_interactive(
150 mut self,
151 it: Interactive<impl InteractiveProcess>,
152 config: Config,
153 service: IntelliShellService,
154 extra_line: bool,
155 ) -> Result<ProcessOutput> {
156 if !it.opts.interactive {
158 return self.run_non_interactive(it.process, config, service, extra_line).await;
159 }
160
161 let inline = it.opts.inline || (!it.opts.full_screen && config.inline);
163 let keybindings = config.keybindings.clone();
164 self.active_component = it.process.into_component(config, service, inline)?;
165
166 let peek_action = self.active_component.init_and_peek().await?;
168 if let Some(output) = self.process_action(peek_action).await? {
169 tracing::debug!("A result was received from `init_and_peek`, returning it");
170 return Ok(output);
171 }
172
173 let mut tui = Tui::new()?.paste(true).mouse(true);
175 if inline {
176 tracing::debug!("Displaying inline {} interactively", self.active_component.name());
177 tui.enter_inline(extra_line, self.active_component.min_inline_height())?;
178 } else {
179 tracing::debug!("Displaying full-screen {} interactively", self.active_component.name());
180 tui.enter()?;
181 }
182
183 loop {
185 let Some(tui_event) = tui.next_event().await else {
187 tracing::error!("TUI closed unexpectedly, no event received");
188 break;
189 };
190 let action = self.handle_tui_event(tui_event, &mut tui, &keybindings).await?;
192 if let Some(output) = self.process_action(action).await? {
194 return Ok(output);
196 }
197 }
198
199 Ok(ProcessOutput::success())
200 }
201
202 #[instrument(skip_all)]
208 async fn handle_tui_event(
209 &mut self,
210 event: Event,
211 tui: &mut Tui,
212 keybindings: &KeyBindingsConfig,
213 ) -> Result<Action> {
214 if event != Event::Tick
215 && event != Event::Render
216 && !matches!(event, Event::Mouse(m) if m.kind == MouseEventKind::Moved )
217 {
218 tracing::trace!("{event:?}");
219 }
220 let ac = &mut self.active_component;
221 Ok(match event {
222 Event::Render => {
224 tui.render(|frame, area| ac.render(frame, area))?;
225 Action::NoOp
226 }
227 Event::Tick => ac.tick()?,
229 Event::FocusGained => ac.focus_gained()?,
230 Event::FocusLost => ac.focus_lost()?,
231 Event::Resize(width, height) => ac.resize(width, height)?,
232 Event::Paste(content) => ac.process_paste_event(content)?,
233 Event::Key(key) => ac.process_key_event(keybindings, key).await?,
234 Event::Mouse(mouse) => ac.process_mouse_event(mouse)?,
235 })
236 }
237
238 #[instrument(skip_all)]
242 async fn process_action(&mut self, action: Action) -> Result<Option<ProcessOutput>> {
243 match action {
244 Action::NoOp => (),
245 Action::Quit(output) => return Ok(Some(output)),
246 Action::SwitchComponent(next_component) => {
247 tracing::debug!(
248 "Switching active component: {} -> {}",
249 self.active_component.name(),
250 next_component.name()
251 );
252 self.active_component = next_component;
253 let peek_action = self.active_component.init_and_peek().await?;
255 if let Some(output) = Box::pin(self.process_action(peek_action)).await? {
256 tracing::debug!("A result was received from `init_and_peek`, returning it");
257 return Ok(Some(output));
258 }
259 }
260 }
261 Ok(None)
262 }
263}