I am trying to build a pausable pomodoro timer, but can't figure out how my threads should be set up. Specifically I am having a hard time on finding a way to pause/quit the timer thread. Here's what I am currently trying to do:
I have an input thread collecting crossterm input events and sending them via an mpsc-channel to my main thread
My main thread runs the timer (via thread::sleep
) and receives the input messages from the other thread
However because sleep is a blocking operation, the main thread is often unable to react to my key-events.
Any advice how I could be going about this?
Here is a minimum reproducable example:
pub fn run() {
enable_raw_mode().expect("Can run in raw mode");
let (input_worker_tx, input_worker_rx): (Sender<CliEvent<Event>>, Receiver<CliEvent<Event>>) =
mpsc::channel();
poll_input_thread(input_worker_tx);
main_loop(input_worker_rx);
}
fn main_loop(input_worker_rx: Receiver<CliEvent<Event>>) {
let stdout = std::io::stdout();
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend).expect("Terminal could be created");
terminal.clear().expect("Terminal could be cleared");
let mut remaining_time = 20;
while remaining_time > 0 {
if let InputEvent::Quit = handle_input(&input_worker_rx) {
break;
};
remaining_time -= 1;
let time = util::seconds_to_time(remaining_time);
view::render(&mut terminal, &time);
sleep(time::Duration::new(1, 0));
}
quit(&mut terminal, Some("Cya!"));
}
pub fn poll_input_thread(input_worker_tx: Sender<CliEvent<Event>>) {
let tick_rate = Duration::from_millis(200);
thread::spawn(move || {
let mut last_tick = Instant::now();
loop {
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if event::poll(timeout).expect("poll works") {
let crossterm_event = event::read().expect("can read events");
input_worker_tx
.send(CliEvent::Input(crossterm_event))
.expect("can send events");
}
if last_tick.elapsed() >= tick_rate && input_worker_tx.send(CliEvent::Tick).is_ok() {
last_tick = Instant::now();
}
}
});
}
pub fn handle_input(input_worker_rx: &Receiver<CliEvent<Event>>) -> InputEvent {
match input_worker_rx.try_recv() {
Ok(CliEvent::Input(Event::Key(key_event))) => match key_event {
KeyEvent {
code: KeyCode::Char('q'),
..
}
| KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
..
} => {
return InputEvent::Quit;
}
_ => {}
},
Ok(CliEvent::Tick) => {}
Err(TryRecvError::Empty) => {}
Err(TryRecvError::Disconnected) => eject("Input handler disconnected"),
_ => {}
}
InputEvent::Continue
}
Don't use sleep
and don't use try_recv
. Instead use recv_timeout
with a timeout corresponding to your sleep
duration:
pub fn run() {
enable_raw_mode().expect("Can run in raw mode");
let (input_worker_tx, input_worker_rx): (Sender<CliEvent<Event>>, Receiver<CliEvent<Event>>) =
mpsc::channel();
poll_input_thread(input_worker_tx);
main_loop(input_worker_rx);
}
fn main_loop(input_worker_rx: Receiver<CliEvent<Event>>) {
let stdout = std::io::stdout();
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend).expect("Terminal could be created");
terminal.clear().expect("Terminal could be cleared");
let mut target_time = Instant::now() + Duration::from_secs(20);
let mut paused = false;
let mut remaining = Duration::from_secs (0);
while paused || (target_time > Instant::now()) {
match handle_input(&input_worker_rx, Duration::from_secs (1)) {
InpuEvent::Quit => break,
InputEvent::Pause => {
if paused {
target_time = Instant::now() + remaining;
} else {
remaining = target_time - Instant::now();
}
paused = !paused;
},
_ => {},
}
let time = (if paused { remaining } else { target_time - Instant::now() })
.as_secs();
view::render(&mut terminal, &time);
}
quit(&mut terminal, Some("Cya!"));
}
pub fn poll_input_thread(input_worker_tx: Sender<CliEvent<Event>>) {
let tick_rate = Duration::from_millis(200);
thread::spawn(move || {
let mut last_tick = Instant::now();
loop {
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if event::poll(timeout).expect("poll works") {
let crossterm_event = event::read().expect("can read events");
input_worker_tx
.send(CliEvent::Input(crossterm_event))
.expect("can send events");
}
if last_tick.elapsed() >= tick_rate && input_worker_tx.send(CliEvent::Tick).is_ok() {
last_tick = Instant::now();
}
}
});
}
pub fn handle_input(input_worker_rx: &Receiver<CliEvent<Event>>, timeout: Duration) -> InputEvent {
match input_worker_rx.recv_timeout(timeout) {
Ok(CliEvent::Input(Event::Key(key_event))) => match key_event {
KeyEvent {
code: KeyCode::Char('q'),
..
}
| KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
..
} => {
return InputEvent::Quit;
}
KeyEvent {
code: KeyCode::Char(' '),
..
} => {
return InputEvent::Pause;
}
_ => {}
},
Ok(CliEvent::Tick) => {}
Err(TryRecvError::Empty) => {}
Err(TryRecvError::Disconnected) => eject("Input handler disconnected"),
_ => {}
}
InputEvent::Continue
}