opencode_voice/input/
keyboard.rs1use anyhow::Result;
4use crossterm::{
5 event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
6 terminal,
7};
8use std::io;
9use std::time::{Duration, Instant};
10use tokio_util::sync::CancellationToken;
11
12use crate::state::InputEvent;
13
14pub fn is_tty() -> bool {
16 use std::io::IsTerminal;
17 io::stdin().is_terminal()
18}
19
20pub struct KeyboardInput {
22 toggle_key: char,
23 sender: tokio::sync::mpsc::UnboundedSender<InputEvent>,
24 cancel: CancellationToken,
25}
26
27impl KeyboardInput {
28 pub fn new(
29 toggle_key: char,
30 sender: tokio::sync::mpsc::UnboundedSender<InputEvent>,
31 cancel: CancellationToken,
32 ) -> Self {
33 KeyboardInput {
34 toggle_key,
35 sender,
36 cancel,
37 }
38 }
39
40 pub fn run(&self) -> Result<()> {
44 terminal::enable_raw_mode()?;
46
47 let _guard = RawModeGuard;
49
50 let mut last_toggle = Instant::now()
51 .checked_sub(Duration::from_millis(500))
52 .unwrap_or(Instant::now());
53
54 loop {
55 if self.cancel.is_cancelled() {
57 break;
58 }
59
60 if !event::poll(Duration::from_millis(100))? {
62 continue;
63 }
64
65 let ev = event::read()?;
66
67 match ev {
68 Event::Key(KeyEvent {
69 code, modifiers, ..
70 }) => match code {
71 KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) => {
73 let _ = self.sender.send(InputEvent::Quit);
74 break;
75 }
76 KeyCode::Char('q') => {
78 let _ = self.sender.send(InputEvent::Quit);
79 break;
80 }
81 KeyCode::Char(c) if c == self.toggle_key => {
83 let now = Instant::now();
84 if now.duration_since(last_toggle) >= Duration::from_millis(200) {
85 last_toggle = now;
86 let _ = self.sender.send(InputEvent::Toggle);
87 }
88 }
89 _ => {}
90 },
91 _ => {}
92 }
93 }
94
95 Ok(())
96 }
97}
98
99struct RawModeGuard;
101
102impl Drop for RawModeGuard {
103 fn drop(&mut self) {
104 let _ = terminal::disable_raw_mode();
105 }
106}