DEV Community

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

Posted on

1

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

Image of Datadog

The Essential Toolkit for Front-end Developers

Take a user-centric approach to front-end monitoring that evolves alongside increasingly complex frameworks and single-page applications.

Get The Kit

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.

Image of Datadog

Create and maintain end-to-end frontend tests

Learn best practices on creating frontend tests, testing on-premise apps, integrating tests into your CI/CD pipeline, and using Datadog’s testing tunnel.

Download The Guide

👋 Kindness is contagious

Engage with a sea of insights in this enlightening article, highly esteemed within the encouraging DEV Community. Programmers of every skill level are invited to participate and enrich our shared knowledge.

A simple "thank you" can uplift someone's spirits. Express your appreciation in the comments section!

On DEV, sharing knowledge smooths our journey and strengthens our community bonds. Found this useful? A brief thank you to the author can mean a lot.

Okay