Search code examples
multithreadingrusttimer

How can I quit/pause/unpause a timer thread


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
}


Solution

  • 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
    }