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