tukai 0.2.3

The app provides an interactive typing experience with switchable templates, designed to help users improve their typing speed and accuracy.
use std::time::Duration;

use anyhow::{Result, anyhow};
use ratatui::crossterm::event::{Event, EventStream, KeyEvent};

#[cfg(target_os = "windows")]
use ratatui::crossterm::event::KeyEventKind;

use tokio::sync::mpsc;

use futures::{FutureExt, StreamExt};

/// Represents events generated by the Tukai application.
///
/// - `Tick`: A periodic timer event used for time remainder on the typing screen.
/// - `Key`: An event representing a keyboard input, resolution change, wrapping a [`KeyEvent`].
#[derive(Clone, Copy, Debug)]
pub enum TukaiEvent {
  Tick,
  Key(KeyEvent),
}

/// Handles sending and receiving `TukaiEvent`s asynchronously.
///
/// This struct wraps an unbounded channel sender and receiver pair,
/// allowing for event-driven communication within the application.
///
/// - `_tx`: The sending half of the channel for emitting `TukaiEvent`s.
/// - `rx`: The receiving half of the channel for consuming `TukaiEvent`s.
pub struct EventHandler {
  _tx: mpsc::UnboundedSender<TukaiEvent>,
  rx: mpsc::UnboundedReceiver<TukaiEvent>,
}

impl EventHandler {
  /// Spawns a background asynchronous task that:
  /// - Listens for terminal input events and forwards keyboard events as `TukaiEvent::Key`.
  /// - Sends periodic `TukaiEvent::Tick` events every second.
  ///
  /// The event loop uses Tokio’s async runtime and crossterm’s `EventStream` to handle input.
  pub fn new() -> Self {
    let tick_rate = Duration::from_secs(1);
    let (_tx, rx) = mpsc::unbounded_channel::<TukaiEvent>();

    let tx_clone = _tx.clone();

    tokio::spawn(async move {
      let mut reader = EventStream::new();
      let mut interval = tokio::time::interval(tick_rate);

      loop {
        let tick_delay = interval.tick();
        let crossterm_event = reader.next().fuse();

        tokio::select! {
          Some(Ok(Event::Key(key_event))) = crossterm_event => {
            // On Windows terminal takes press and release
            // To avoid symbols duplications checks a event kind
            #[cfg(target_os = "windows")]
            {
              if key_event.kind != KeyEventKind::Press {
                continue
              }
            }
            tx_clone.send(TukaiEvent::Key(key_event)).unwrap();
          },
          _ = tick_delay => {
            tx_clone.send(TukaiEvent::Tick).unwrap();
          },
        }
      }
    });

    Self { _tx, rx }
  }

  /// Asynchronously received the next `TukaiEvent` from the event channel.
  ///
  /// # Returns
  /// A [`Result`] containing the next `TukaiEvent` on success,
  /// or an error if the event stream has been closed.
  pub async fn next(&mut self) -> Result<TukaiEvent> {
    self
      .rx
      .recv()
      .await
      .ok_or_else(|| anyhow!("Some IO error occurred"))
  }
}