From 35c18183206440a87a12a69cd94fb694c9eff1be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 11 Dec 2022 13:10:50 +0100 Subject: [PATCH] abstraction --- Cargo.lock | 91 +++++++++++++++++++++++++++++++ Cargo.toml | 3 + src/main.rs | 97 +++++---------------------------- src/stdin_term.rs | 136 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 243 insertions(+), 84 deletions(-) create mode 100644 src/stdin_term.rs diff --git a/Cargo.lock b/Cargo.lock index 6493fc2..7b47890 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "bitflags" version = "1.3.2" @@ -23,9 +29,17 @@ version = "0.1.0" dependencies = [ "anes", "crossbeam-channel", + "smart-default", "termios", + "timeout-readwrite", ] +[[package]] +name = "cc" +version = "1.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" + [[package]] name = "cfg-if" version = "1.0.0" @@ -57,6 +71,68 @@ version = "0.2.138" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "proc-macro2" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "smart-default" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "termios" version = "0.3.3" @@ -65,3 +141,18 @@ checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" dependencies = [ "libc", ] + +[[package]] +name = "timeout-readwrite" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9b6279ee83562e5f8ac37bbd4ca032016616c9f5688b0d1b7775619619dce" +dependencies = [ + "nix", +] + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" diff --git a/Cargo.toml b/Cargo.toml index f6a9565..1aee06f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,9 @@ edition = "2021" [dependencies] anes = { version = "0.1.6", features = ["parser"] } crossbeam-channel = "0.5.6" +smart-default = "0.6.0" # For TTY interface termios = "0.3.3" +timeout-readwrite = "0.3.2" + diff --git a/src/main.rs b/src/main.rs index cb85db2..6325f14 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,101 +1,28 @@ -use std::io::{Read, stdin, stdout, Write}; -use std::os::unix::io::RawFd; +use std::io::{Write}; use anes::parser::{KeyCode, Sequence}; -use termios::{tcgetattr, tcsetattr, Termios}; +use crate::stdin_term::{StdinInterface, StdinInterfaceOptions}; -const STDIN_FILENO : RawFd = 0; +#[macro_use] +extern crate smart_default; -struct StdinNonblockingGuard { - original_termios : Termios, -} - -impl StdinNonblockingGuard { - pub fn new() -> std::io::Result { - let mut old_tio = unsafe { std::mem::zeroed::() }; - - /* get the terminal settings for stdin */ - tcgetattr(STDIN_FILENO, &mut old_tio)?; - - /* we want to keep the old setting to restore them a the end */ - let mut new_tio = old_tio; - - /* disable canonical mode (buffered i/o) and local echo */ - new_tio.c_lflag &= !termios::ICANON & !termios::ECHO; - - /* set the new settings immediately */ - tcsetattr(STDIN_FILENO, termios::TCSANOW, &new_tio)?; - - Ok(Self { - original_termios: old_tio, - }) - } -} - -impl Drop for StdinNonblockingGuard { - fn drop(&mut self) { - /* restore the former settings */ - let _ = tcsetattr(STDIN_FILENO, termios::TCSANOW, &self.original_termios); - } -} +mod stdin_term; +/// Event received from the input stream - can be keys, mouse, query response or shutdown (end of stream) enum InputEvent { Key(anes::parser::Sequence), EndOfStream, } +trait TerminalInterface : Write { + fn get_receiver(&mut self) -> Option>; -struct TerminalInterface { -} - -impl TerminalInterface { - pub fn new() -> (Self, crossbeam_channel::Receiver) { - let (sender, receiver) = crossbeam_channel::bounded(0); - std::thread::spawn(move || { - let stdin_guard = StdinNonblockingGuard::new().unwrap(); - let mut sin = stdin().lock(); - let mut parser = anes::parser::Parser::default(); - - 'input: loop { - let mut buf = [0u8; 64]; - // TODO add a way to kill the thread from outside - let numch = match sin.read(&mut buf[..]) { - Err(_) | Ok(0) => { - let _ = sender.send(InputEvent::EndOfStream); - break 'input; - }, - Ok(numch) => numch - }; - - let received_slice = &buf[0..numch]; - parser.advance(received_slice, numch == buf.len()); - - while let Some(item) = parser.next() { - if sender.send(InputEvent::Key(item)).is_err() { - break 'input; - } - } - } - - drop(stdin_guard); - }); - - (Self {}, receiver) - } -} - -impl Write for TerminalInterface { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - stdout().write(buf) - } - - fn flush(&mut self) -> std::io::Result<()> { - stdout().flush() - } + fn shutdown(&mut self); } fn main() -> std::io::Result<()> { - let (mut term, receiver) = TerminalInterface::new(); + let mut term = StdinInterface::new(StdinInterfaceOptions::default()); + let receiver = term.get_receiver().unwrap(); 'lp: loop { match receiver.recv() { @@ -113,5 +40,7 @@ fn main() -> std::io::Result<()> { } } + term.shutdown(); + Ok(()) } diff --git a/src/stdin_term.rs b/src/stdin_term.rs new file mode 100644 index 0000000..ad74e65 --- /dev/null +++ b/src/stdin_term.rs @@ -0,0 +1,136 @@ +use std::io; +use std::io::{Read, stdin, stdout, Write}; +use std::os::unix::io::RawFd; +use std::thread::JoinHandle; +use std::time::Duration; +use termios::{tcgetattr, tcsetattr, Termios}; +use timeout_readwrite::TimeoutReader; +use crate::{InputEvent, TerminalInterface}; + +const STDIN_FILENO : RawFd = 0; + +struct StdinNonblockingGuard { + original_termios : Termios, +} + +impl StdinNonblockingGuard { + pub fn new() -> std::io::Result { + let mut old_tio = unsafe { std::mem::zeroed::() }; + + /* get the terminal settings for stdin */ + tcgetattr(STDIN_FILENO, &mut old_tio)?; + + /* we want to keep the old setting to restore them a the end */ + let mut new_tio = old_tio; + + /* disable canonical mode (buffered i/o) and local echo */ + new_tio.c_lflag &= !termios::ICANON & !termios::ECHO; + + /* set the new settings immediately */ + tcsetattr(STDIN_FILENO, termios::TCSANOW, &new_tio)?; + + Ok(Self { + original_termios: old_tio, + }) + } +} + +impl Drop for StdinNonblockingGuard { + fn drop(&mut self) { + /* restore the former settings */ + let _ = tcsetattr(STDIN_FILENO, termios::TCSANOW, &self.original_termios); + } +} + +#[derive(SmartDefault)] +pub struct StdinInterfaceOptions { + #[default(Duration::from_millis(250))] + poll_interval: Duration, +} + +pub struct StdinInterface { + receiver : Option>, + shutdown_sender : crossbeam_channel::Sender<()>, + recv_thread_handle: Option>, +} + +impl StdinInterface { + pub fn new(opts : StdinInterfaceOptions) -> Self { + let (sender, receiver) = crossbeam_channel::bounded(0); + let (shutdown_sender, shutdown_receiver) = crossbeam_channel::bounded(0); + let handle = std::thread::spawn(move || { + let stdin_guard = StdinNonblockingGuard::new().unwrap(); + let mut sin = TimeoutReader::new(stdin().lock(), opts.poll_interval); + let mut parser = anes::parser::Parser::default(); + + 'input: loop { + match shutdown_receiver.try_recv() { + Ok(_) | Err(crossbeam_channel::TryRecvError::Disconnected) => { + break 'input; + } + Err(crossbeam_channel::TryRecvError::Empty) => { + // + } + } + + let mut buf = [0u8; 64]; + // TODO add a way to kill the thread from outside + let numch = match sin.read(&mut buf[..]) { + Err(e) => { + if e.kind() != io::ErrorKind::TimedOut { + let _ = sender.send(InputEvent::EndOfStream); + break 'input; + } + continue 'input; + } + Ok(0) => { + let _ = sender.send(InputEvent::EndOfStream); + break 'input; + }, + Ok(numch) => numch + }; + + let received_slice = &buf[0..numch]; + parser.advance(received_slice, numch == buf.len()); + + while let Some(item) = parser.next() { + if sender.send(InputEvent::Key(item)).is_err() { + break 'input; + } + } + } + + drop(stdin_guard); + }); + + Self { + receiver: Some(receiver), + shutdown_sender, + recv_thread_handle: Some(handle), + } + } +} + +impl Write for StdinInterface { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + stdout().write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + stdout().flush() + } +} + +impl TerminalInterface for StdinInterface { + fn get_receiver(&mut self) -> Option> { + self.receiver.take() + } + + fn shutdown(&mut self) { + if self.shutdown_sender.send(()).is_ok() { + if let Some(handle) = self.recv_thread_handle.take() { + let _ = handle.join(); + } + } + } +}