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 process::{InteractiveProcess, Process, ProcessOutput},
10 service::IntelliShellService,
11 tui::{Event, Tui},
12};
13
14#[derive(Default)]
16pub enum Action {
17 #[default]
19 NoOp,
20 Quit(ProcessOutput),
22 SwitchComponent(Box<dyn Component>),
24}
25
26pub struct App {
28 active_component: Box<dyn Component>,
29}
30impl App {
31 pub fn new() -> Result<Self> {
33 Ok(Self {
34 active_component: Box::new(EmptyComponent),
35 })
36 }
37
38 #[instrument(skip_all)]
45 pub async fn run(
46 self,
47 config: Config,
48 service: IntelliShellService,
49 process: CliProcess,
50 extra_line: bool,
51 ) -> Result<ProcessOutput> {
52 match process {
53 #[cfg(debug_assertions)]
54 CliProcess::Query(query_process) => {
55 tracing::info!("Running 'query' process");
56 tracing::debug!("Options: {:?}", query_process);
57 service.load_workspace_commands().await?;
58 self.run_non_interactive(query_process, config, service).await
59 }
60 CliProcess::Init(_) => unreachable!("Handled in main"),
61 CliProcess::New(bookmark_command) => {
62 tracing::info!("Running 'new' process");
63 tracing::debug!("Options: {:?}", bookmark_command);
64 self.run_interactive(bookmark_command, config, service, extra_line)
65 .await
66 }
67 CliProcess::Search(search_commands) => {
68 tracing::info!("Running 'search' process");
69 tracing::debug!("Options: {:?}", search_commands);
70 service.load_workspace_commands().await?;
71 self.run_interactive(search_commands, config, service, extra_line).await
72 }
73 CliProcess::Replace(variable_replace) => {
74 tracing::info!("Running 'replace' process");
75 tracing::debug!("Options: {:?}", variable_replace);
76 self.run_interactive(variable_replace, config, service, extra_line)
77 .await
78 }
79 CliProcess::Export(export_commands) => {
80 tracing::info!("Running 'export' process");
81 tracing::debug!("Options: {:?}", export_commands);
82 self.run_non_interactive(export_commands, config, service).await
83 }
84 CliProcess::Import(import_commands) => {
85 tracing::info!("Running 'import' process");
86 tracing::debug!("Options: {:?}", import_commands);
87 self.run_non_interactive(import_commands, config, service).await
88 }
89 CliProcess::Tldr(TldrProcess::Fetch(tldr_fetch)) => {
90 tracing::info!("Running tldr 'fetch' process");
91 tracing::debug!("Options: {:?}", tldr_fetch);
92 self.run_non_interactive(tldr_fetch, config, service).await
93 }
94 CliProcess::Tldr(TldrProcess::Clear(tldr_clear)) => {
95 tracing::info!("Running tldr 'clear' process");
96 tracing::debug!("Options: {:?}", tldr_clear);
97 self.run_non_interactive(tldr_clear, config, service).await
98 }
99 }
100 }
101
102 async fn run_non_interactive(
106 self,
107 process: impl Process,
108 config: Config,
109 service: IntelliShellService,
110 ) -> Result<ProcessOutput> {
111 process.execute(config, service).await
112 }
113
114 async fn run_interactive(
116 mut self,
117 it: Interactive<impl InteractiveProcess>,
118 config: Config,
119 service: IntelliShellService,
120 extra_line: bool,
121 ) -> Result<ProcessOutput> {
122 if !it.opts.interactive {
124 return self.run_non_interactive(it.process, config, service).await;
125 }
126
127 let new_version = match service.check_new_version().await {
129 Ok(None) => None,
130 Ok(Some(v)) => {
131 tracing::info!("New version available: v{v}");
132 Some(v)
133 }
134 Err(err) => {
135 tracing::error!("Failed to check for new version: {err:#?}");
136 None
137 }
138 };
139
140 let inline = it.opts.inline || (!it.opts.full_screen && config.inline);
142 let keybindings = config.keybindings.clone();
143 self.active_component = it.process.into_component(config, service, inline, new_version)?;
144 self.active_component.init().await?;
145
146 let peek_action = self.active_component.peek().await?;
148 if let Some(output) = self.process_action(peek_action).await? {
149 tracing::debug!("A result was received from `peek`, returning it");
150 return Ok(output);
151 }
152
153 let mut tui = Tui::new()?.paste(true).mouse(true);
155 if inline {
156 tracing::debug!("Displaying inline {} interactively", self.active_component.name());
157 tui.enter_inline(extra_line, self.active_component.min_inline_height())?;
158 } else {
159 tracing::debug!("Displaying full-screen {} interactively", self.active_component.name());
160 tui.enter()?;
161 }
162
163 loop {
165 let Some(tui_event) = tui.next_event().await else {
167 tracing::error!("TUI closed unexpectedly, no event received");
168 break;
169 };
170 let action = self.handle_tui_event(tui_event, &mut tui, &keybindings).await?;
172 if let Some(output) = self.process_action(action).await? {
174 return Ok(output);
176 }
177 }
178
179 Ok(ProcessOutput::success())
180 }
181
182 #[instrument(skip_all)]
188 async fn handle_tui_event(
189 &mut self,
190 event: Event,
191 tui: &mut Tui,
192 keybindings: &KeyBindingsConfig,
193 ) -> Result<Action> {
194 if event != Event::Tick
195 && event != Event::Render
196 && !matches!(event, Event::Mouse(m) if m.kind == MouseEventKind::Moved )
197 {
198 tracing::trace!("{event:?}");
199 }
200 let ac = &mut self.active_component;
201 Ok(match event {
202 Event::Render => {
204 tui.render(|frame, area| ac.render(frame, area))?;
205 Action::NoOp
206 }
207 Event::Tick => ac.tick()?,
209 Event::FocusGained => ac.focus_gained()?,
210 Event::FocusLost => ac.focus_lost()?,
211 Event::Resize(width, height) => ac.resize(width, height)?,
212 Event::Paste(content) => ac.process_paste_event(content)?,
213 Event::Key(key) => ac.process_key_event(keybindings, key).await?,
214 Event::Mouse(mouse) => ac.process_mouse_event(mouse)?,
215 })
216 }
217
218 #[instrument(skip_all)]
222 async fn process_action(&mut self, action: Action) -> Result<Option<ProcessOutput>> {
223 match action {
224 Action::NoOp => (),
225 Action::Quit(output) => return Ok(Some(output)),
226 Action::SwitchComponent(next_component) => {
227 tracing::debug!(
228 "Switching active component: {} -> {}",
229 self.active_component.name(),
230 next_component.name()
231 );
232 self.active_component = next_component;
233 self.active_component.init().await?;
234 let peek_action = self.active_component.peek().await?;
236 if let Some(output) = Box::pin(self.process_action(peek_action)).await? {
237 tracing::debug!("A result was received from `peek`, returning it");
238 return Ok(Some(output));
239 }
240 }
241 }
242 Ok(None)
243 }
244}