use crossterm::event::{
self, Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, KeyModifiers,
};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::sync::broadcast;
use crate::core::event_router::{CaptureEvent, Event, EventRouter, FlashEvent, LifecycleEvent};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HotKey {
F2,
F3,
}
impl HotKey {
fn from_keycode(code: KeyCode) -> Option<Self> {
match code {
KeyCode::F(2) => Some(HotKey::F2),
KeyCode::F(3) => Some(HotKey::F3),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct HotkeyConfig {
pub screenshot: Option<HotKey>,
pub toggle_keystroke_capturing: Option<HotKey>,
}
impl Default for HotkeyConfig {
fn default() -> Self {
Self {
screenshot: Some(HotKey::F2),
toggle_keystroke_capturing: None,
}
}
}
#[derive(Debug, Clone)]
pub enum InputEvent {
Keystroke {
_key_name: String,
_instant: Instant,
_timecode_ms: u128,
},
}
#[derive(Default)]
pub struct InputState {
pub keystrokes: Mutex<Vec<InputEvent>>,
pub keystroke_capture_enabled: AtomicBool,
}
impl InputState {
pub fn new() -> Self {
Self {
keystrokes: Mutex::default(),
keystroke_capture_enabled: AtomicBool::new(false),
}
}
pub fn toggle_capture(&self) -> bool {
let current = self.keystroke_capture_enabled.load(Ordering::Acquire);
self.keystroke_capture_enabled
.store(!current, Ordering::Release);
!current
}
pub fn is_capture_enabled(&self) -> bool {
self.keystroke_capture_enabled.load(Ordering::Acquire)
}
pub fn push_keystroke(&self, key_name: String, instant: Instant, timecode_ms: u128) {
self.keystrokes.lock().unwrap().push(InputEvent::Keystroke {
_key_name: key_name,
_instant: instant,
_timecode_ms: timecode_ms,
});
}
}
enum KeyAction {
Handled,
Forward(Vec<u8>),
Exit,
}
pub struct KeyboardMonitor {
input_state: Arc<InputState>,
idle_duration: Arc<Mutex<Duration>>,
recording_start: Instant,
hotkey_config: HotkeyConfig,
router: EventRouter,
}
impl KeyboardMonitor {
pub fn new(
input_state: Arc<InputState>,
idle_duration: Arc<Mutex<Duration>>,
recording_start: Instant,
hotkey_config: HotkeyConfig,
router: EventRouter,
) -> Self {
Self {
input_state,
idle_duration,
recording_start,
hotkey_config,
router,
}
}
pub fn run<W: Write>(
&self,
mut shell_stdin: W,
event_rx: broadcast::Receiver<Event>,
) -> anyhow::Result<()> {
log::debug!("Keyboard monitor starting - F2=screenshot, F3=toggle capture, Ctrl+D=exit");
enable_raw_mode()?;
log::debug!("Raw mode enabled");
let result = self.run_loop(&mut shell_stdin, event_rx);
let _ = disable_raw_mode();
result
}
fn run_loop<W: Write>(
&self,
shell_stdin: &mut W,
mut event_rx: broadcast::Receiver<Event>,
) -> anyhow::Result<()> {
loop {
match event_rx.try_recv() {
Ok(Event::Lifecycle(LifecycleEvent::Shutdown)) => {
log::debug!("Keyboard monitor received shutdown signal");
break;
}
Ok(_) => {} Err(broadcast::error::TryRecvError::Empty) => {}
Err(broadcast::error::TryRecvError::Closed) => break,
Err(broadcast::error::TryRecvError::Lagged(_)) => {}
}
if event::poll(Duration::from_millis(50))? {
match event::read()? {
CrosstermEvent::Key(key_event) => {
if key_event.kind != KeyEventKind::Press {
continue;
}
match self.handle_key(key_event) {
KeyAction::Handled => {}
KeyAction::Forward(bytes) => {
shell_stdin.write_all(&bytes)?;
shell_stdin.flush()?;
}
KeyAction::Exit => break,
}
}
CrosstermEvent::Resize(_, _) => {
}
_ => {}
}
}
}
Ok(())
}
fn handle_key(&self, key: KeyEvent) -> KeyAction {
let code = key.code;
let modifiers = key.modifiers;
log::debug!("Key event: {:?} modifiers: {:?}", code, modifiers);
if let Some(hot_key) = HotKey::from_keycode(code) {
log::debug!("Function key detected: {:?}", hot_key);
if self.hotkey_config.screenshot.as_ref() == Some(&hot_key) {
log::debug!("Screenshot hotkey detected");
self.trigger_screenshot();
return KeyAction::Handled;
}
if self.hotkey_config.toggle_keystroke_capturing.as_ref() == Some(&hot_key) {
let enabled = self.input_state.toggle_capture();
log::debug!("Keystroke capture: {}", if enabled { "ON" } else { "OFF" });
return KeyAction::Handled;
}
}
if code == KeyCode::Char('d') && modifiers.contains(KeyModifiers::CONTROL) {
self.router.send(Event::Capture(CaptureEvent::Stop));
return KeyAction::Exit;
}
if self.input_state.is_capture_enabled() {
let key_name = self.format_key_name(&key);
let timecode_ms = self.current_timecode();
self.input_state
.push_keystroke(key_name, Instant::now(), timecode_ms);
}
KeyAction::Forward(self.key_to_bytes(&key))
}
fn current_timecode(&self) -> u128 {
let idle = *self.idle_duration.lock().unwrap();
Instant::now()
.duration_since(self.recording_start)
.saturating_sub(idle)
.as_millis()
}
fn trigger_screenshot(&self) {
let timecode_ms = self.current_timecode();
self.router
.send(Event::Capture(CaptureEvent::Screenshot { timecode_ms }));
self.router.send(Event::Flash(FlashEvent::ScreenshotTaken));
log::debug!("Screenshot triggered at timecode {}", timecode_ms);
}
fn format_key_name(&self, key: &KeyEvent) -> String {
let mut name = String::new();
if key.modifiers.contains(KeyModifiers::CONTROL) {
name.push_str("Ctrl+");
}
if key.modifiers.contains(KeyModifiers::ALT) {
name.push_str("Alt+");
}
if key.modifiers.contains(KeyModifiers::SHIFT) && !matches!(key.code, KeyCode::Char(_)) {
name.push_str("Shift+");
}
match key.code {
KeyCode::Char(c) => {
if key.modifiers.contains(KeyModifiers::CONTROL) {
name.push(c.to_ascii_uppercase());
} else {
name.push(c);
}
}
KeyCode::Enter => name.push_str("Return"),
KeyCode::Tab => name.push_str("Tab"),
KeyCode::Backspace => name.push_str("Backspace"),
KeyCode::Esc => name.push_str("Escape"),
KeyCode::Delete => name.push_str("Delete"),
KeyCode::F(n) => name.push_str(&format!("F{}", n)),
KeyCode::Left => name.push_str("Left"),
KeyCode::Right => name.push_str("Right"),
KeyCode::Up => name.push_str("Up"),
KeyCode::Down => name.push_str("Down"),
KeyCode::Home => name.push_str("Home"),
KeyCode::End => name.push_str("End"),
KeyCode::PageUp => name.push_str("PageUp"),
KeyCode::PageDown => name.push_str("PageDown"),
KeyCode::Insert => name.push_str("Insert"),
_ => name.push_str("Unknown"),
}
name
}
fn key_to_bytes(&self, key: &KeyEvent) -> Vec<u8> {
match key.code {
KeyCode::Char(c) => {
if key.modifiers.contains(KeyModifiers::CONTROL) {
let ctrl_code = (c.to_ascii_lowercase() as u8)
.wrapping_sub(b'a')
.wrapping_add(1);
if ctrl_code <= 26 {
vec![ctrl_code]
} else {
let mut buf = [0u8; 4];
let s = c.encode_utf8(&mut buf);
s.as_bytes().to_vec()
}
} else {
let mut buf = [0u8; 4];
let s = c.encode_utf8(&mut buf);
s.as_bytes().to_vec()
}
}
KeyCode::Enter => vec![0x0D],
KeyCode::Tab => vec![0x09],
KeyCode::Backspace => vec![0x7F],
KeyCode::Esc => vec![0x1B],
KeyCode::Delete => vec![0x1B, b'[', b'3', b'~'],
KeyCode::F(1) => vec![0x1B, b'O', b'P'],
KeyCode::F(2) => vec![0x1B, b'O', b'Q'],
KeyCode::F(3) => vec![0x1B, b'O', b'R'],
KeyCode::F(4) => vec![0x1B, b'O', b'S'],
KeyCode::F(5) => vec![0x1B, b'[', b'1', b'5', b'~'],
KeyCode::F(6) => vec![0x1B, b'[', b'1', b'7', b'~'],
KeyCode::F(7) => vec![0x1B, b'[', b'1', b'8', b'~'],
KeyCode::F(8) => vec![0x1B, b'[', b'1', b'9', b'~'],
KeyCode::F(9) => vec![0x1B, b'[', b'2', b'0', b'~'],
KeyCode::F(10) => vec![0x1B, b'[', b'2', b'1', b'~'],
KeyCode::F(11) => vec![0x1B, b'[', b'2', b'3', b'~'],
KeyCode::F(12) => vec![0x1B, b'[', b'2', b'4', b'~'],
KeyCode::Up => vec![0x1B, b'[', b'A'],
KeyCode::Down => vec![0x1B, b'[', b'B'],
KeyCode::Right => vec![0x1B, b'[', b'C'],
KeyCode::Left => vec![0x1B, b'[', b'D'],
KeyCode::Home => vec![0x1B, b'[', b'H'],
KeyCode::End => vec![0x1B, b'[', b'F'],
KeyCode::PageUp => vec![0x1B, b'[', b'5', b'~'],
KeyCode::PageDown => vec![0x1B, b'[', b'6', b'~'],
KeyCode::Insert => vec![0x1B, b'[', b'2', b'~'],
_ => vec![],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_keycombo_from_keycode() {
assert_eq!(HotKey::from_keycode(KeyCode::F(2)), Some(HotKey::F2));
assert_eq!(HotKey::from_keycode(KeyCode::F(12)), None);
assert_eq!(HotKey::from_keycode(KeyCode::Char('a')), None);
}
#[test]
fn test_hotkey_config_default() {
let config = HotkeyConfig::default();
assert_eq!(config.screenshot, Some(HotKey::F2));
assert_eq!(config.toggle_keystroke_capturing, None);
}
#[test]
fn test_input_state_default() {
let state = InputState::new();
assert!(!state.keystroke_capture_enabled.load(Ordering::Acquire));
assert!(state.keystrokes.lock().unwrap().is_empty());
}
#[test]
fn test_input_state_toggle_capture() {
let state = InputState::new();
assert!(!state.is_capture_enabled());
let result = state.toggle_capture();
assert!(result);
assert!(state.is_capture_enabled());
let result = state.toggle_capture();
assert!(!result);
assert!(!state.is_capture_enabled());
}
}