rustact 0.1.0

Async terminal UI framework inspired by React, built on top of ratatui and tokio.
Documentation
use std::time::Duration;

use crossterm::event::{
    Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent,
    MouseEventKind,
};
use tokio::sync::broadcast;
use tracing::trace;

#[derive(Clone, Debug)]
pub enum FrameworkEvent {
    Key(KeyEvent),
    Mouse(MouseEvent),
    Resize(u16, u16),
    Tick,
}

#[derive(Clone)]
pub struct EventBus {
    tx: broadcast::Sender<FrameworkEvent>,
}

impl EventBus {
    pub fn new(buffer: usize) -> Self {
        let (tx, _) = broadcast::channel(buffer);
        Self { tx }
    }

    pub fn publish(&self, event: FrameworkEvent) {
        trace!(event = ?event, "publishing framework event");
        let _ = self.tx.send(event);
    }

    pub fn subscribe(&self) -> broadcast::Receiver<FrameworkEvent> {
        self.tx.subscribe()
    }
}

pub fn map_terminal_event(event: CrosstermEvent) -> Option<FrameworkEvent> {
    match event {
        CrosstermEvent::Key(key) => Some(FrameworkEvent::Key(key)),
        CrosstermEvent::Mouse(mouse) => Some(FrameworkEvent::Mouse(mouse)),
        CrosstermEvent::Resize(cols, rows) => Some(FrameworkEvent::Resize(cols, rows)),
        CrosstermEvent::FocusGained | CrosstermEvent::FocusLost | CrosstermEvent::Paste(_) => None,
    }
}

pub fn is_ctrl_c(event: &FrameworkEvent) -> bool {
    match event {
        FrameworkEvent::Key(key) => match key.code {
            KeyCode::Char('c') | KeyCode::Char('C') => {
                key.modifiers.contains(KeyModifiers::CONTROL)
            }
            _ => false,
        },
        _ => false,
    }
}

pub fn is_mouse_click(event: &FrameworkEvent, button: MouseButton) -> bool {
    matches!(
        event,
        FrameworkEvent::Mouse(mouse)
            if matches!(mouse.kind, MouseEventKind::Down(btn) if btn == button)
    )
}

pub fn mouse_scroll_delta(event: &FrameworkEvent) -> i32 {
    if let FrameworkEvent::Mouse(mouse) = event {
        match mouse.kind {
            MouseEventKind::ScrollUp => 1,
            MouseEventKind::ScrollDown => -1,
            _ => 0,
        }
    } else {
        0
    }
}

pub fn mouse_position(event: &FrameworkEvent) -> Option<(u16, u16)> {
    if let FrameworkEvent::Mouse(mouse) = event {
        Some((mouse.column, mouse.row))
    } else {
        None
    }
}

pub const DEFAULT_TICK_RATE: Duration = Duration::from_millis(250);

#[cfg(test)]
mod tests;