like curses, but not evil [WIP]
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bless/src/stdin_term.rs

136 lines
4.1 KiB

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();
}
}
}
}