aboutsummaryrefslogtreecommitdiffstats
path: root/src/key_reader.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/key_reader.rs')
-rw-r--r--src/key_reader.rs89
1 files changed, 89 insertions, 0 deletions
diff --git a/src/key_reader.rs b/src/key_reader.rs
new file mode 100644
index 0000000..a59259c
--- /dev/null
+++ b/src/key_reader.rs
@@ -0,0 +1,89 @@
+use futures::sink::Sink as _;
+use snafu::ResultExt as _;
+
+#[derive(Debug, snafu::Snafu)]
+pub enum Error {
+ #[snafu(display("failed to read from event channel: {}", source))]
+ ReadChannel {
+ source: tokio::sync::mpsc::error::UnboundedRecvError,
+ },
+
+ #[snafu(display(
+ "failed to spawn a background thread to read terminal input: {}",
+ source
+ ))]
+ TerminalInputReadingThread { source: std::io::Error },
+}
+
+pub struct KeyReader {
+ events:
+ Option<tokio::sync::mpsc::UnboundedReceiver<crossterm::InputEvent>>,
+ quit: Option<tokio::sync::oneshot::Sender<()>>,
+}
+
+impl KeyReader {
+ pub fn new() -> Self {
+ Self {
+ events: None,
+ quit: None,
+ }
+ }
+}
+
+impl futures::stream::Stream for KeyReader {
+ type Item = crossterm::InputEvent;
+ type Error = Error;
+
+ fn poll(&mut self) -> futures::Poll<Option<Self::Item>, Self::Error> {
+ if self.events.is_none() {
+ let task = futures::task::current();
+ let reader = crossterm::input().read_sync();
+ let (events_tx, events_rx) =
+ tokio::sync::mpsc::unbounded_channel();
+ let mut events_tx = events_tx.wait();
+ let (quit_tx, mut quit_rx) = tokio::sync::oneshot::channel();
+ // TODO: this is pretty janky - it'd be better to build in more
+ // useful support to crossterm directly
+ std::thread::Builder::new()
+ .spawn(move || {
+ for event in reader {
+ // sigh, this is extra janky, but otherwise the thread
+ // will outlive the current instance and eat the first
+ // character typed that was supposed to go to the
+ // thread spawned by the next instance
+ let newline = event
+ == crossterm::InputEvent::Keyboard(
+ crossterm::KeyEvent::Char('\n'),
+ );
+ // unwrap is unpleasant, but so is figuring out how to
+ // propagate the error back to the main thread
+ events_tx.send(event).unwrap();
+ task.notify();
+ if newline {
+ break;
+ }
+ if quit_rx.try_recv().is_ok() {
+ break;
+ }
+ }
+ })
+ .context(TerminalInputReadingThread)?;
+
+ self.events = Some(events_rx);
+ self.quit = Some(quit_tx);
+ }
+
+ self.events.as_mut().unwrap().poll().context(ReadChannel)
+ }
+}
+
+impl Drop for KeyReader {
+ fn drop(&mut self) {
+ if let Some(quit_tx) = self.quit.take() {
+ // don't care if it fails to send, this can happen if the thread
+ // terminates due to seeing a newline before the keyreader goes
+ // out of scope
+ let _ = quit_tx.send(());
+ }
+ }
+}