abstraction

master
Ondřej Hruška 1 year ago
parent 99759854e2
commit 35c1818320
  1. 91
      Cargo.lock
  2. 3
      Cargo.toml
  3. 97
      src/main.rs
  4. 136
      src/stdin_term.rs

91
Cargo.lock generated

@ -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"

@ -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"

@ -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<Self> {
let mut old_tio = unsafe { std::mem::zeroed::<Termios>() };
/* 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<crossbeam_channel::Receiver<InputEvent>>;
struct TerminalInterface {
}
impl TerminalInterface {
pub fn new() -> (Self, crossbeam_channel::Receiver<InputEvent>) {
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<usize> {
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(())
}

@ -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<Self> {
let mut old_tio = unsafe { std::mem::zeroed::<Termios>() };
/* 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<crossbeam_channel::Receiver<InputEvent>>,
shutdown_sender : crossbeam_channel::Sender<()>,
recv_thread_handle: Option<JoinHandle<()>>,
}
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<usize> {
stdout().write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
stdout().flush()
}
}
impl TerminalInterface for StdinInterface {
fn get_receiver(&mut self) -> Option<crossbeam_channel::Receiver<InputEvent>> {
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();
}
}
}
}
Loading…
Cancel
Save