crossterm-rs / crossterm

Cross platform terminal library rust
MIT License
3.3k stars 280 forks source link

Temporarily disable async key event polling #837

Open fritzrehde opened 1 year ago

fritzrehde commented 1 year ago

Is your feature request related to a problem? Please describe. In the TUI I am building, I would like to temporarily disable the entire TUI I am painting, execute a command in a subshell that paints its own TUI (e.g. text editor like vim), and then resume my TUI. I use async Rust with Tokio for the key event polling. I have key event polling setup on a separate thread. If I just "leave this polling on" (meaning disable the painting of my TUI, but keep listening to crossterm key events), then my experience in the other TUI (the text editor) is affected by crossterm, as key presses become slugish and take longer to recognize (I'm guessing this is maybe because crossterm is somehow consuming and re-emitting the key presses?). This behaviour is fine though, I shouldn't be listening for key events in my app if I expect the child TUI I am executing to listen to the same key presses. Therefore, I wanted to temporarily (as long as the child TUI is executing) disable key event polling. This is how I currently have that implemented:

/// A command sent to a polling thread.
enum PollingCommand {
    /// Continue listening/polling for terminal events.
    Listen,
    /// Pause listening/polling for terminal events. Notifies event's sender
    /// once polling has actually been paused.
    Pause(Sender<()>),
}

/// Continuously listens for terminal-related events, and sends relevant events
/// back to the main thread.
async fn poll_terminal_events(event_tx: Sender<Event>, mut polling_rx: Receiver<PollingCommand>) {
    let mut terminal_event_reader = EventStream::new();

    'polling_loop: loop {
        tokio::select! {
            // Wait for receival of a polling command from main event loop thread.
            polling = polling_rx.recv() => match polling {
                Some(PollingCommand::Pause(polling_paused_tx)) => {
                    log::info!("Terminal event listener has been paused.");

                    // Notify sender thread that polling has been paused.
                    let _ = polling_paused_tx.send(()).await;

                    // Wait until another Listen command is received.
                    'wait_for_listen: while let Some(polling) = polling_rx.recv().await {
                        if let PollingCommand::Listen = polling {
                            break 'wait_for_listen;
                        }
                    }

                    log::info!("Terminal event listener is listening again.");
                },
                // Currently already listening for terminal events.
                Some(PollingCommand::Listen) => {},
                // Channel has been closed.
                None => break 'polling_loop,
            },
            // Wait for a terminal event.
            event = terminal_event_reader.next().fuse() => match event {
                Some(Ok(CrosstermEvent::Key(key_event))) => {
                    if event_tx.send(Event::KeyPressed(key_event)).await.is_err() {
                        break 'polling_loop;
                    };
                }
                _ => {}
            }
        }
    }

    log::info!("Shutting down terminal event listener task");
}

This does mostly solve the issue I had before. Key events are handled smoothly in the child TUI (text editor). However, some key presses are still exclusively handled by crossterm, i.e. I press a key inside the child TUI and it doesn't register there, but seems to be somehow "saved" by crossterm and then emitted once I leave the child TUI. I am assuming this has something to do with terminal_event_reader.next().fuse(). I don't know anything about how EventStream works. Is it buffering some events? I was thinking maybe I should clear/delete the entire buffer somehow once I re-enter the Listen state, but I don't know how to do that.

Describe the solution you'd like Any help? Maybe I am approaching this problem in a wrong way (with all the channels etc.) and there is a simpler solution. I would appreciate any help/comments you have!

Additional context You can check out the exact code in my project: watchbind.

TyberiusPrime commented 8 months ago

Currently stuck on the same issue.

If I drop my EventStream, the child appears to get all the key strokes. But recreating the event stream doesn't get my app the events again.

(edit: That turned to out to be an issue between chair and keyboard. I had another level of 'suspend input' in my layers that needed cleaning up. So I can confirm, dropping the EventStream and re recreating it works as one would expect.).