DEV Community

Cover image for Handling CTRL-C while using crossterm
Ken Salter
Ken Salter

Posted on

Handling CTRL-C while using crossterm

Normally, I would use a crate called ctrlc to gracefully handle when a user wants to stop my app with CTRL-C.

In a recent app I wanted to implement a nice text UI for selecting items from a list. I turned to the crate crossterm to help with cursor positioning and keyboard events.

In order to accomplish these goals, you must enable raw mode:

terminal::enable_raw_mode()
Enter fullscreen mode Exit fullscreen mode

However, this intercepts CTRL-C, and your ctrlc handler will never be called.

My solution?

The only workaround I could come up with is to implement a background thread to monitor keystrokes, and if we see a CTRL-C, then signal the main thread.

std::thread::spawn(move || -> Result<()> {
        loop {
            if event::poll(std::time::Duration::from_millis(100))? {
                if let Event::Key(key_event) = event::read()? {
                    if key_event.code == KeyCode::Char('c') && key_event.modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
                        // signal!
                        r.store(false, Ordering::SeqCst);
                    }
                }
            }
        }
    });
Enter fullscreen mode Exit fullscreen mode

Here we use an Atomic boolean, but we can easily switch to messaging or some other means of communication.

You can find the complete example project here:

ctrlc-crossterm

Top comments (3)

Collapse
 
bbkr profile image
Paweł bbkr Pabian • Edited

Your workaround is the valid pattern for GUI/TUI.
Main thread should never listen to key strokes.

Idiomatic solution would be:

Encapsulate events that your application is interested in:

enum Event {
    Key(crossterm::event::KeyEvent),
    Foo(...),
    Bar,
    ...
}
Enter fullscreen mode Exit fullscreen mode

Create method that will listen to key strokes and convert them to events:

fn listen_to_key_strokes (tx: mpsc::Sender<Event>) {
    loop {
        match crossterm::event::read().unwrap() {
            crossterm::event::Event::Key(key) => tx.send(Event::Key(key)).unwrap(),
            _ => {}
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Create method that will redraw your GUI/TUI:

fn run (&mut self, terminal: &mut DefaultTerminal, rx: mpsc::Receiver<Event>) -> io::Result<()> {
        terminal.draw(...frame bla bla...)?; # initial draw, before first event happened
        loop {
            match rx.recv().unwrap() {
                Event::Key(key) => { HANDLE KEYS HERE, BREAK LOOP IF QUITTING APP},
                Event::Foo(...) => { ... },
                Event::Bar => ...
            }
            terminal.draw(...frame bla bla...)?;
        }
        Ok(())
    }
Enter fullscreen mode Exit fullscreen mode

Connect key listener with main thread:

fn main() -> io::Result<()> {
    let (tx, rx) = mpsc::channel::<Event>();
    ...
    let input_tx = tx.clone();
    spawn(
        || { listen_to_key_strokes(input_tx);}
    );

    let app_result = run(&mut terminal, rx);
    ...
Enter fullscreen mode Exit fullscreen mode

This way your app will be super responsive, without fixed 100ms delay and you can easily add heavy computation jobs on threads as long as they produce some kind of Event to MPSC channel.

Collapse
 
plecos profile image
Ken Salter

Cool! Thanks for the comments. Agreed, main thread should never listen for keystrokes.

Collapse
 
bbkr profile image
Paweł bbkr Pabian

BTW: youtube.com/watch?v=awX7DUp-r14 is great introduction for everyone who wants to start working with crossterm and ratatui widgets for terminal applications.