1use crate::action::{Action, ActionExt, NullActionExt};
2use crate::binds::BindMap;
3use crate::message::{BindDirective, Event, RenderCommand};
4use anyhow::Result;
5use cli_boilerplate_automation::bait::ResultExt;
6use cli_boilerplate_automation::bath::PathExt;
7use cli_boilerplate_automation::unwrap;
8use crokey::{Combiner, KeyCombination, KeyCombinationFormat, key};
9use crossterm::event::{
10 Event as CrosstermEvent, EventStream, KeyModifiers, MouseEvent, MouseEventKind,
11};
12use futures::stream::StreamExt;
13use log::{debug, error, info, warn};
14use ratatui::layout::Rect;
15use std::collections::btree_map::Entry;
16use std::path::PathBuf;
17use tokio::sync::mpsc;
18use tokio::time::{self};
19
20pub type RenderSender<A = NullActionExt> = mpsc::UnboundedSender<RenderCommand<A>>;
21pub type EventSender = mpsc::UnboundedSender<Event>;
22pub type BindSender<A> = mpsc::UnboundedSender<BindDirective<A>>;
23
24#[derive(Debug)]
25pub struct EventLoop<A: ActionExt> {
26 txs: Vec<mpsc::UnboundedSender<RenderCommand<A>>>,
27 tick_interval: time::Duration,
28
29 pub binds: BindMap<A>,
30 combiner: Combiner,
31 fmt: KeyCombinationFormat,
32
33 mouse_events: bool,
34 paused: bool,
35 event_stream: Option<EventStream>,
36
37 rx: mpsc::UnboundedReceiver<Event>,
38 controller_tx: mpsc::UnboundedSender<Event>,
39
40 bind_rx: mpsc::UnboundedReceiver<BindDirective<A>>,
41 bind_tx: BindSender<A>,
42
43 key_file: Option<PathBuf>,
44 current_task: Option<tokio::task::JoinHandle<Result<()>>>,
45}
46
47impl<A: ActionExt> Default for EventLoop<A> {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl<A: ActionExt> EventLoop<A> {
54 pub fn new() -> Self {
55 let combiner = Combiner::default();
56 let fmt = KeyCombinationFormat::default();
57 let (controller_tx, controller_rx) = tokio::sync::mpsc::unbounded_channel();
58
59 let (bind_tx, bind_rx) = tokio::sync::mpsc::unbounded_channel();
60
61 Self {
62 txs: vec![],
63 tick_interval: time::Duration::from_millis(200),
64
65 binds: BindMap::new(),
66 combiner,
67 fmt,
68 event_stream: None, rx: controller_rx,
70 controller_tx,
71
72 mouse_events: false,
73 paused: false,
74 key_file: None,
75 current_task: None,
76
77 bind_rx,
78 bind_tx,
79 }
80 }
81
82 pub fn with_binds(binds: BindMap<A>) -> Self {
83 let mut ret = Self::new();
84 ret.binds = binds;
85 ret
86 }
87
88 pub fn record_last_key(&mut self, path: PathBuf) -> &mut Self {
89 self.key_file = Some(path);
90 self
91 }
92
93 pub fn with_tick_rate(mut self, tick_rate: u8) -> Self {
94 self.tick_interval = time::Duration::from_secs_f64(1.0 / tick_rate as f64);
95 self
96 }
97
98 pub fn add_tx(&mut self, handler: mpsc::UnboundedSender<RenderCommand<A>>) -> &mut Self {
99 self.txs.push(handler);
100 self
101 }
102
103 pub fn with_mouse_events(mut self) -> Self {
104 self.mouse_events = true;
105 self
106 }
107
108 pub fn clear_txs(&mut self) {
109 self.txs.clear();
110 }
111
112 pub fn controller(&self) -> EventSender {
113 self.controller_tx.clone()
114 }
115 pub fn bind_controller(&self) -> BindSender<A> {
116 self.bind_tx.clone()
117 }
118
119 fn handle_event(&mut self, e: Event) {
120 debug!("Received: {e}");
121
122 match e {
123 Event::Pause => {
124 self.paused = true;
125 self.send(RenderCommand::Ack);
126 self.event_stream = None; }
128 Event::Refresh => {
129 self.send(RenderCommand::Refresh);
130 }
131 _ => {}
132 }
133 if let Some(actions) = self.binds.get(&e.into()).cloned() {
134 self.send_actions(actions);
135 }
136 }
137
138 fn handle_rebind(&mut self, e: BindDirective<A>) {
139 debug!("Received: {e:?}");
140
141 match e {
142 BindDirective::Bind(k, v) => {
143 self.binds.insert(k, v);
144 }
145
146 BindDirective::PushBind(k, v) => match self.binds.entry(k) {
147 Entry::Occupied(mut entry) => {
148 entry.get_mut().0.extend(v);
149 }
150 Entry::Vacant(entry) => {
151 entry.insert(v);
152 }
153 },
154
155 BindDirective::Unbind(k) => {
156 self.binds.remove(&k);
157 }
158
159 BindDirective::PopBind(k) => {
160 if let Some(actions) = self.binds.get_mut(&k) {
161 actions.0.pop();
162
163 if actions.0.is_empty() {
164 self.binds.remove(&k);
165 }
166 }
167 }
168 }
169 }
170
171 pub fn binds(&mut self, binds: BindMap<A>) -> &mut Self {
172 self.binds = binds;
173 self
174 }
175
176 pub async fn run(&mut self) {
178 self.event_stream = Some(EventStream::new());
179 let mut interval = time::interval(self.tick_interval);
180
181 if let Some(path) = self.key_file.clone() {
182 log::error!("Cleaning up temp files @ {path:?}");
183 tokio::spawn(async move {
184 cleanup_tmp_files(&path).await._elog();
185 });
186 }
187
188 loop {
190 self.txs.retain(|tx| !tx.is_closed());
191 if self.txs.is_empty() {
192 break;
193 }
194
195 while self.paused {
197 if let Some(event) = self.rx.recv().await {
198 if matches!(event, Event::Resume) {
199 debug!("Resumed from pause");
200 self.paused = false;
201 self.send(RenderCommand::Ack);
202 self.event_stream = Some(EventStream::new());
203 break;
204 }
205 } else {
206 error!("Event controller closed while paused.");
207 break;
208 }
209 }
210
211 let event = if let Some(stream) = &mut self.event_stream {
217 stream.next()
218 } else {
219 continue; };
221
222 tokio::select! {
223 _ = interval.tick() => {
224 self.send(RenderCommand::Tick)
225 }
226
227 _ = tokio::signal::ctrl_c() => {
229 self.record_key("ctrl-c".into());
230 if let Some(actions) = self.binds.get(&key!(ctrl-c).into()).cloned() {
231 self.send_actions(actions);
232 } else {
233 self.send(RenderCommand::quit());
234 info!("Received ctrl-c");
235 }
236 }
237
238 Some(event) = self.rx.recv() => {
239 self.handle_event(event)
240 }
241
242 Some(directive) = self.bind_rx.recv() => {
243 self.handle_rebind(directive)
244 }
245
246 maybe_event = event => {
248 match maybe_event {
249 Some(Ok(event)) => {
250 if !matches!(
251 event,
252 CrosstermEvent::Mouse(MouseEvent {
253 kind: crossterm::event::MouseEventKind::Moved,
254 ..
255 }) | CrosstermEvent::Key {..}
256 ) {
257 info!("Event {event:?}");
258 }
259 match event {
260 CrosstermEvent::Key(k) => {
261 if let Some(key) = self.combiner.transform(k) {
262 info!("{key:?}");
263 let key = KeyCombination::normalized(key);
264 if let Some(actions) = self.binds.get(&key.into()).cloned() {
265 self.record_key(key.to_string());
266 self.send_actions(actions);
267 } else if let Some(c) = key_code_as_letter(key) {
268 self.send(RenderCommand::Action(Action::Char(c)));
269 } else {
270 let mut matched = true;
271 match key {
273 key!(ctrl-c) | key!(esc) => {
274 self.send(RenderCommand::quit())
275 },
276 key!(up) => self.send_action(Action::Up(1)),
277 key!(down) => self.send_action(Action::Down(1)),
278 key!(enter) => {
279 self.record_key(key.to_string());
280 self.send_action(Action::Accept)
281 }
282 key!(right) => self.send_action(Action::ForwardChar),
283 key!(left) => self.send_action(Action::BackwardChar),
284 key!(ctrl-right) => self.send_action(Action::ForwardWord),
285 key!(ctrl-left) => self.send_action(Action::BackwardWord),
286 key!(backspace) => self.send_action(Action::DeleteChar),
287 key!(ctrl-h) => self.send_action(Action::DeleteWord),
288 key!(ctrl-u) => self.send_action(Action::Cancel),
289 key!(alt-h) => self.send_action(Action::Help("".to_string())),
290 key!(ctrl-'[') => self.send_action(Action::ToggleWrap),
291 key!(ctrl-']') => self.send_action(Action::TogglePreviewWrap),
292 _ => {
293 matched = false
294 }
295 }
296 if matched {
297 self.record_key(key.to_string());
298 }
299 }
300 }
301 }
302 CrosstermEvent::Mouse(mouse) => {
303 if let Some(actions) = self.binds.get(&mouse.into()).cloned() {
304 self.send_actions(actions);
305 } else if !matches!(mouse.kind, MouseEventKind::Moved) {
306 self.send(RenderCommand::Mouse(mouse));
309 }
310 }
311 CrosstermEvent::Resize(width, height) => {
312 self.send(RenderCommand::Resize(Rect::new(0, 0, width, height)));
313 }
314 #[allow(unused_variables)]
315 CrosstermEvent::Paste(content) => {
316 #[cfg(feature = "bracketed-paste")]
317 {
318 self.send(RenderCommand::Paste(content));
319 }
320 #[cfg(not(feature = "bracketed-paste"))]
321 {
322 unreachable!()
323 }
324 }
325 _ => {},
330 }
331 }
332 Some(Err(e)) => warn!("Failed to read crossterm event: {e}"),
333 None => {
334 warn!("Reader closed");
335 break
336 }
337 }
338 }
339 }
340 }
341 }
342
343 fn send(&self, action: RenderCommand<A>) {
344 for tx in &self.txs {
345 tx.send(action.clone())
346 .unwrap_or_else(|_| debug!("Failed to send {action}"));
347 }
348 }
349
350 fn record_key(&mut self, content: String) {
351 let Some(path) = self.key_file.clone() else {
352 return;
353 };
354
355 if let Some(handle) = self.current_task.take() {
357 handle.abort();
358 }
359
360 let handle = tokio::spawn(write_to_file(path, content));
361
362 self.current_task = Some(handle);
363 }
364
365 fn send_actions<'a>(&self, actions: impl IntoIterator<Item = Action<A>>) {
366 for action in actions {
367 self.send(action.into());
368 }
369 }
370
371 pub fn print_key(&self, key_combination: KeyCombination) -> String {
372 self.fmt.to_string(key_combination)
373 }
374
375 fn send_action(&self, action: Action<A>) {
376 self.send(RenderCommand::Action(action));
377 }
378}
379
380fn key_code_as_letter(key: KeyCombination) -> Option<char> {
381 match key {
382 KeyCombination {
383 codes: crokey::OneToThree::One(crossterm::event::KeyCode::Char(l)),
384 modifiers: KeyModifiers::NONE,
385 } => Some(l),
386 KeyCombination {
387 codes: crokey::OneToThree::One(crossterm::event::KeyCode::Char(l)),
388 modifiers: KeyModifiers::SHIFT,
389 } => Some(l.to_ascii_uppercase()),
390 _ => None,
391 }
392}
393
394use std::path::Path;
395use tokio::fs;
396
397async fn cleanup_tmp_files(path: &Path) -> Result<()> {
399 let parent = unwrap!(path.parent(); Ok(()));
400 let name = unwrap!(path.file_name().and_then(|s| s.to_str()); Ok(()));
401
402 let mut entries = fs::read_dir(parent).await?;
403
404 while let Some(entry) = entries.next_entry().await? {
405 let entry_path = entry.path();
406
407 if let Ok(filename) = entry_path._filename()
408 && let Some(e) = filename.strip_prefix(name)
409 && e.starts_with('.')
410 && e.ends_with(".tmp")
411 {
412 fs::remove_file(entry_path).await._elog();
413 }
414 }
415
416 Ok(())
417}
418
419pub async fn write_to_file(path: PathBuf, content: String) -> Result<()> {
422 let suffix = std::time::SystemTime::now()
423 .duration_since(std::time::UNIX_EPOCH)
424 .unwrap()
425 .as_nanos();
426
427 let tmp_path = path.with_file_name(format!("{}.{}.tmp", path._filename()?, suffix));
428
429 fs::write(&tmp_path, &content).await?;
431
432 fs::rename(&tmp_path, &path).await?;
434
435 Ok(())
436}