try to polish it up again

This commit is contained in:
Michael Zhang 2023-01-08 01:47:35 -06:00
parent 9108e7d28d
commit 436c46ba12
10 changed files with 1067 additions and 486 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/target
/output.cast
.direnv
/logs

868
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,21 +1,25 @@
[package]
name = "asciinema"
name = "liveterm"
version = "0.1.0"
authors = ["Michael Zhang <iptq@protonmail.com>"]
edition = "2018"
edition = "2021"
[dependencies]
anyhow = "1.0.34"
futures = "0.3.7"
libc = "0.2.80"
nix = "0.19.0"
parking_lot = "0.11.0"
serde = "1.0.117"
serde_derive = "1.0.117"
serde_json = "1.0.59"
signal-hook = "0.1.16"
structopt = "0.3.20"
anyhow = { version = "1.0.68", features = ["backtrace"] }
chrono = "0.4.23"
clap = { version = "4.0.32", features = ["derive"] }
futures = "0.3.25"
libc = "0.2.139"
log = "0.4.17"
nix = "0.26.1"
parking_lot = "0.12.1"
serde = "1.0.152"
serde_derive = "1.0.152"
serde_json = "1.0.91"
signal-hook = "0.3.14"
termios = "0.3.3"
thiserror = "1.0.22"
tokio = { version = "0.3.3", features = ["full"] }
tokio-util = { version = "0.5.0", features = ["codec"] }
tokio = { version = "1.24.1", features = ["full"] }
tokio-util = { version = "0.7.4", features = ["codec"] }
tracing = "0.1.37"
tracing-appender = "0.2.2"
tracing-subscriber = "0.3.16"

View file

@ -1,34 +0,0 @@
[0.101333215,"o","\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[0.101467546,"o","\u001b]2;michael@manjaro: ~/Projects/asciinema\u0007\u001b]1;..cts/asciinema\u0007"]
[0.117281595,"o","\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[1m\u001b[34m#\u001b[00m \u001b[36mmichael \u001b[37m@ \u001b[32mmanjaro \u001b[37min \u001b[1m\u001b[33m~/Projects/asciinema\u001b[00m \u001b[37mon\u001b[00m git:\u001b[36mmaster \u001b[31mx\u001b[00m \u001b[37m[3:21:36] \r\n\u001b[1m\u001b[31m$ \u001b[00m\u001b[K"]
[0.117374656,"o","\u001b[?1h\u001b="]
[0.117667658,"o","\u001b[?2004h"]
[1.680256137,"i","w"]
[1.6861166810000001,"o","\u001b[32mw\u001b[39m"]
[1.7842199970000001,"i","h"]
[1.7917789800000001,"o","\r\r\u001b[A\u001b[A\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[1m\u001b[34m#\u001b[00m \u001b[36mmichael \u001b[37m@ \u001b[32mmanjaro \u001b[37min \u001b[1m\u001b[33m~/Projects/asciinema\u001b[00m \u001b[37mon\u001b[00m git:\u001b[36mmaster \u001b[31mx\u001b[00m \u001b[37m[3:21:38] \r\n\u001b[1m\u001b[31m$ \u001b[00mwh"]
[1.792416114,"o","\b\b\u001b[1m\u001b[31mw\u001b[1m\u001b[31mh\u001b[0m\u001b[39m"]
[1.8562032560000001,"i","a"]
[1.857965656,"o","\b\b\u001b[1m\u001b[31mw\u001b[1m\u001b[31mh\u001b[1m\u001b[31ma\u001b[0m\u001b[39m"]
[1.960175246,"i","t"]
[1.968143781,"o","\r\r\u001b[A\u001b[A\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[1m\u001b[34m#\u001b[00m \u001b[36mmichael \u001b[37m@ \u001b[32mmanjaro \u001b[37min \u001b[1m\u001b[33m~/Projects/asciinema\u001b[00m \u001b[37mon\u001b[00m git:\u001b[36mmaster \u001b[31mx\u001b[00m \u001b[37m[3:21:38] \r\n\u001b[1m\u001b[31m$ \u001b[00mwhat"]
[1.9686859540000001,"o","\b\b\b\b\u001b[1m\u001b[31mw\u001b[1m\u001b[31mh\u001b[1m\u001b[31ma\u001b[1m\u001b[31mt\u001b[0m\u001b[39m"]
[2.352188361,"i"," "]
[2.35392467,"o"," "]
[2.592205933,"i","u"]
[2.593708682,"o","u"]
[2.64813527,"i","p"]
[2.6493960469999998,"o","p"]
[3.232210615,"i","\r"]
[3.232716968,"o","\u001b[?1l\u001b>"]
[3.233917915,"o","\u001b[?2004l\r\r\n"]
[3.23484062,"o","\u001b]2;what up\u0007\u001b]1;what\u0007"]
[3.235505654,"o","zsh: command not found: what\r\n"]
[3.235709995,"o","\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[3.235766455,"o","\u001b]2;michael@manjaro: ~/Projects/asciinema\u0007"]
[3.235797955,"o","\u001b]1;..cts/asciinema\u0007"]
[3.242549144,"o","\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[1m\u001b[34m#\u001b[00m \u001b[36mmichael \u001b[37m@ \u001b[32mmanjaro \u001b[37min \u001b[1m\u001b[33m~/Projects/asciinema\u001b[00m \u001b[37mon\u001b[00m git:\u001b[36mmaster \u001b[31mx\u001b[00m \u001b[37m[3:21:39] C:\u001b[31m127\u001b[00m\r\n\u001b[1m\u001b[31m$ \u001b[00m\u001b[K"]
[3.242670815,"o","\u001b[?1h\u001b="]
[3.243217488,"o","\u001b[?2004h"]
[3.800181468,"i","\u0004"]
[3.800319309,"o","\u001b[?2004l\r\r\n"]

View file

@ -1,3 +1,7 @@
//! Data structures for representing the [asciicast v2] format
//!
//! [asciicast v2]: https://github.com/asciinema/asciinema/blob/develop/doc/asciicast-v2.md
use std::collections::HashMap;
use serde::ser::{Serialize, SerializeSeq, Serializer};
@ -24,14 +28,14 @@ pub struct Theme {
palette: String,
}
pub struct Event<'a>(pub f64, pub EventKind<'a>);
pub struct Event(pub f64, pub EventKind);
pub enum EventKind<'a> {
Output(&'a [u8]),
Input(&'a [u8]),
pub enum EventKind {
Output(Vec<u8>),
Input(Vec<u8>),
}
impl<'a> Serialize for Event<'a> {
impl Serialize for Event {
fn serialize<S: Serializer>(
&self,
s: S,

View file

@ -1,16 +0,0 @@
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, Error)]
pub enum Error {
#[error("generic nix error: {0}")]
Nix(#[from] nix::Error),
#[error("write(3) error (fd={1}, data={2:?}): {0}")]
NixWrite(nix::Error, i32, Vec<u8>),
#[error("generic io error: {0}")]
Io(#[from] std::io::Error),
#[error("generic serde_json error: {0}")]
SerdeJson(#[from] serde_json::Error),
}

10
src/lib.rs Normal file
View file

@ -0,0 +1,10 @@
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate tracing;
pub mod asciicast;
pub mod recorder;
// pub mod recorder;
// pub mod term;
// pub mod writer;

View file

@ -1,40 +1,67 @@
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate thiserror;
mod asciicast;
mod errors;
mod pty;
mod recorder;
mod term;
mod writer;
extern crate tracing;
use std::fs::File;
use anyhow::Result;
use structopt::StructOpt;
use clap::{ArgAction, Parser};
use liveterm::recorder::Terminal;
use tokio::process::Command;
use tracing::Level;
use crate::writer::FileWriter;
#[derive(Parser)]
struct Opt {
#[clap(subcommand)]
subcommand: Subcommand,
#[derive(StructOpt)]
enum Command {
#[clap(long, short = 'v', action = ArgAction::Count, global = true)]
verbose: u8,
}
#[derive(Parser)]
enum Subcommand {
/// Record terminal session.
#[structopt(name = "rec")]
Record,
}
fn record() -> Result<()> {
let file = File::create("output.cast")?;
pty::record(&["sh", "-c", "/usr/bin/zsh"], FileWriter::new(file))?;
let _file = File::create("output.cast")?;
let mut command = Command::new("/usr/bin/env");
command.arg("bash");
let (pty, rx) = Terminal::setup(command)?;
pty.wait_until_complete()?;
Ok(())
}
fn main() -> Result<()> {
match Command::from_args() {
Command::Record => {
let opt = Opt::parse();
setup_logging(&opt)?;
match opt.subcommand {
Subcommand::Record => {
record()?;
}
}
Ok(())
}
fn setup_logging(opts: &Opt) -> Result<()> {
let file_appender = tracing_appender::rolling::hourly("logs", "liveterm.log");
let max_level = match opts.verbose {
0 => Level::ERROR,
1 => Level::WARN,
2 => Level::INFO,
3 => Level::DEBUG,
_ => Level::TRACE,
};
tracing_subscriber::fmt()
.with_max_level(max_level)
.with_writer(file_appender)
.init();
Ok(())
}

View file

@ -1,203 +0,0 @@
use std::env;
use std::ffi::CString;
use std::io;
use std::os::unix::io::RawFd;
use std::time::Instant;
use nix::{
fcntl::{fcntl, FcntlArg, OFlag},
ioctl_write_buf,
pty::{forkpty, Winsize},
sys::{
select::{select, FdSet},
termios::{tcsetattr, SetArg, Termios},
wait::waitpid,
},
unistd::{execvpe, isatty, pipe, read, write, ForkResult},
};
use signal_hook::SigId;
use crate::errors::{Error, Result};
use crate::writer::Writer;
const STDIN_FILENO: RawFd = 0;
const STDOUT_FILENO: RawFd = 1;
pub fn record(args: &[&str], writer: impl Writer + Send) -> Result<()> {
let forkpty_result = forkpty(None, None)?;
let master_fd = forkpty_result.master;
let start_time = Instant::now();
let _set_pty_size = || -> Result<()> {
ioctl_write_buf!(helper_write, libc::TIOCGWINSZ, 104, Winsize);
let winsize = Winsize {
ws_row: 24,
ws_col: 80,
ws_xpixel: 0,
ws_ypixel: 0,
};
if isatty(STDOUT_FILENO)? {}
unsafe { helper_write(master_fd, &[winsize]) }?;
Ok(())
};
let write_stdout = |data: &[u8]| -> Result<()> {
write(STDOUT_FILENO, &data)?;
Ok(())
};
let mut master_writer = writer.clone();
let mut handle_master_read = move |data: &[u8]| -> Result<()> {
let elapsed = (Instant::now() - start_time).as_secs_f64();
master_writer.write_stdout(elapsed, data)?;
write_stdout(data)?;
Ok(())
};
let write_master = |data: &[u8]| -> Result<()> {
let mut offset = 0;
while offset < data.len() {
let len = write(master_fd, data)
.map_err(|err| Error::NixWrite(err, master_fd, data.to_vec()))?;
offset += len;
}
Ok(())
};
let mut stdin_writer = writer.clone();
let mut handle_stdin_read = |data: &[u8]| -> Result<()> {
write_master(data)?;
let elapsed = (Instant::now() - start_time).as_secs_f64();
stdin_writer.write_stdin(elapsed, data)?;
Ok(())
};
let mut copy = |signal_fd: RawFd| -> Result<()> {
let mut fdset = FdSet::new();
let mut buf = [0; 1024];
loop {
fdset.clear();
fdset.insert(master_fd);
fdset.insert(STDIN_FILENO);
fdset.insert(signal_fd);
select(None, &mut fdset, None, None, None)?;
if fdset.contains(master_fd) {
let len = read(master_fd, &mut buf)?;
if len == 0 {
fdset.remove(master_fd);
} else {
handle_master_read(&buf[..len])?;
}
} else if fdset.contains(STDIN_FILENO) {
let len = read(STDIN_FILENO, &mut buf)?;
if len == 0 {
fdset.remove(STDIN_FILENO);
} else {
handle_stdin_read(&buf[..len])?;
}
} else if fdset.contains(signal_fd) {
// TODO
}
}
};
let child_pid = match forkpty_result.fork_result {
ForkResult::Parent { child } => child,
ForkResult::Child => {
let cstr_args: Vec<_> = args
.into_iter()
.map(|s| CString::new(*s).unwrap())
.collect();
let args: Vec<_> = cstr_args.iter().map(|s| s.as_ref()).collect();
let cstr_env: Vec<_> = env::vars()
.map(|(k, v)| CString::new(format!("{}={}", k, v)).unwrap())
.collect();
let env: Vec<_> = cstr_env.iter().map(|s| s.as_ref()).collect();
execvpe(&args[0], &args, &env)?;
unreachable!();
}
};
let (pipe_r, pipe_w) = pipe()?;
let mut flags = fcntl(pipe_w, FcntlArg::F_GETFL)?;
flags |= libc::O_NONBLOCK;
fcntl(pipe_w, FcntlArg::F_SETFL(OFlag::from_bits(flags).unwrap()))?;
let old_handlers = set_signals(
[
signal_hook::SIGWINCH,
signal_hook::SIGCHLD,
signal_hook::SIGHUP,
signal_hook::SIGTERM,
signal_hook::SIGQUIT,
]
.iter()
.map(|sig| (*sig, move || eprintln!("HIT SIGNAL {:?}", *sig))),
)?;
// set_pty_size()?;
{
let _term = RawTerm::init(STDIN_FILENO)?;
copy(pipe_r)?;
}
unset_signals(old_handlers)?;
waitpid(child_pid, None)?;
Ok(())
}
fn set_signals<I, F>(signals_list: I) -> Result<Vec<SigId>, io::Error>
where
I: Iterator<Item = (i32, F)>,
F: Fn() + Sync + Send + 'static,
{
let mut old_handlers = Vec::new();
for (sig, handler) in signals_list {
old_handlers.push(unsafe { signal_hook::register(sig, handler) }?);
}
Ok(old_handlers)
}
fn unset_signals(handlers_list: Vec<SigId>) -> Result<(), io::Error> {
for handler in handlers_list {
signal_hook::unregister(handler);
}
Ok(())
}
struct RawTerm(RawFd, Termios);
impl RawTerm {
pub fn init(fd: RawFd) -> Result<Self> {
use nix::sys::termios::*;
let saved_mode = tcgetattr(fd)?;
let mut mode = saved_mode.clone();
mode.input_flags &= !(InputFlags::BRKINT
| InputFlags::ICRNL
| InputFlags::INPCK
| InputFlags::ISTRIP
| InputFlags::IXON);
mode.output_flags &= !OutputFlags::OPOST;
mode.control_flags &= !(ControlFlags::CSIZE | ControlFlags::PARENB);
mode.control_flags |= ControlFlags::CS8;
mode.local_flags &= !(LocalFlags::ECHO
| LocalFlags::ICANON
| LocalFlags::IEXTEN
| LocalFlags::ISIG);
mode.control_chars[libc::VMIN] = 1;
mode.control_chars[libc::VTIME] = 0;
tcsetattr(fd, SetArg::TCSAFLUSH, &mode)?;
Ok(RawTerm(fd, saved_mode))
}
}
impl Drop for RawTerm {
fn drop(&mut self) {
tcsetattr(self.0, SetArg::TCSAFLUSH, &self.1).unwrap();
}
}

View file

@ -1,30 +1,298 @@
use std::env;
use std::path::PathBuf;
//! Setting up a recorder
pub struct RecordOptions {
pub path: PathBuf,
pub append: Option<bool>,
pub command: Option<String>,
pub capture_env: Option<Vec<String>>,
pub title: Option<String>,
use std::os::unix::prelude::RawFd;
use std::sync::mpsc::{self, Receiver, Sender};
use anyhow::Result;
use libc::{STDIN_FILENO, STDOUT_FILENO};
use nix::pty::{openpty, OpenptyResult};
use nix::sys::select::{select, FdSet};
use nix::unistd::{dup, dup2, fsync, read, write};
use tokio::process::Command;
use crate::asciicast::Event;
pub struct Terminal {
/// The file descriptor that the parent process' original stdout got duplicated to.
///
/// This needs to be saved so we can restore it later.
parent_stdout_dup: RawFd,
pty: OpenptyResult,
event_tx: Sender<Event>,
}
pub fn record(options: RecordOptions) {
let _command = options
.command
.unwrap_or_else(|| env::var("SHELL").unwrap_or_else(|_| "sh".to_string()));
impl Terminal {
pub fn setup(mut command: Command) -> Result<(Self, Receiver<Event>)> {
command.env("TERM", "xterm-256color");
let _command_env = env::vars();
// Set up channels
let (event_tx, event_rx) = mpsc::channel::<Event>();
// let header_env = HashMap::new();
// Open a pty
let pty = openpty(None, None)?;
let parent_stdout_dup = dup(STDOUT_FILENO)?;
info!(parent_stdout_dup, "Duplicated parent process' stdout.");
dup2(pty.slave, STDOUT_FILENO)?;
info!(
redirected_to = pty.slave,
"Redirected parent process' stdout to new pty."
);
// let (width, height) = term::get_size();
eprintln!("Starting child process...");
// let header = Header {
// version: 2,
// width,
// height,
// Spawn the child
let child = command.spawn()?;
let _child_pid = child.id();
// title: options.title,
// };
let term = Terminal {
parent_stdout_dup,
pty,
event_tx,
};
Ok((term, event_rx))
}
pub fn wait_until_complete(self) -> Result<()> {
self.run()?;
// Restore stdout
fsync(STDOUT_FILENO)?;
dup2(self.parent_stdout_dup, STDOUT_FILENO)?;
Ok(())
}
fn run(&self) -> Result<()> {
let mut read_fd_set = FdSet::new();
let mut buf = [0; 1024];
info!("Starting read loop...");
loop {
// Set up fdsets
// asciinema does not capture stdin by default
read_fd_set.clear();
read_fd_set.insert(self.pty.master);
read_fd_set.insert(STDIN_FILENO);
// TODO: Signal file descriptor
select(None, &mut read_fd_set, None, None, None)?;
// Master is ready for read, which means child process stdout has data
// (child process stdout) -> (pty.slave) -> (pty.master)
if read_fd_set.contains(self.pty.master) {
let bytes_read = read(self.pty.master, &mut buf)?;
if bytes_read == 0 {
info!("Read 0 bytes, exiting the read loop.");
break;
}
let data = &buf[..bytes_read];
debug!(bytes_read, "Read data from master.");
// We should take this and rewrite this to the current stdout
write(STDOUT_FILENO, data)?;
}
// Stdin is ready for read, which means input from the user
else if read_fd_set.contains(STDIN_FILENO) {
let bytes_read = read(self.pty.master, &mut buf)?;
let data = &buf[..bytes_read];
debug!(bytes_read, "Read data from master.");
write(self.pty.master, data)?;
}
}
Ok(())
}
}
/*
pub fn record(args: &[&str], writer: impl Writer + Send) -> Result<()> {
// Setup
let forkpty_result = forkpty(None, None)?;
let master_fd = forkpty_result.master;
let start_time = Instant::now();
let _set_pty_size = || -> Result<()> {
ioctl_write_buf!(helper_write, libc::TIOCGWINSZ, 104, Winsize);
let winsize = Winsize {
ws_row: 24,
ws_col: 80,
ws_xpixel: 0,
ws_ypixel: 0,
};
if isatty(STDOUT_FILENO)? {}
unsafe { helper_write(master_fd, &[winsize]) }?;
Ok(())
};
let write_stdout = |data: &[u8]| -> Result<()> {
write(STDOUT_FILENO, &data)?;
Ok(())
};
let mut master_writer = writer.clone();
let mut handle_master_read = move |data: &[u8]| -> Result<()> {
let elapsed = (Instant::now() - start_time).as_secs_f64();
master_writer.write_stdout(elapsed, data)?;
write_stdout(data)?;
Ok(())
};
let write_master = |data: &[u8]| -> Result<()> {
let mut offset = 0;
while offset < data.len() {
let len = write(master_fd, data)
.map_err(|err| Error::NixWrite(err, master_fd, data.to_vec()))?;
offset += len;
}
Ok(())
};
let mut stdin_writer = writer.clone();
let mut handle_stdin_read = |data: &[u8]| -> Result<()> {
write_master(data)?;
let elapsed = (Instant::now() - start_time).as_secs_f64();
stdin_writer.write_stdin(elapsed, data)?;
Ok(())
};
// Copy fds?
let mut copy = |signal_fd: RawFd| -> Result<()> {
let mut fdset = FdSet::new();
let mut buf = [0; 1024];
loop {
fdset.clear();
fdset.insert(master_fd);
fdset.insert(STDIN_FILENO);
fdset.insert(signal_fd);
select(None, &mut fdset, None, None, None)?;
if fdset.contains(master_fd) {
let len = read(master_fd, &mut buf)?;
if len == 0 {
fdset.remove(master_fd);
} else {
handle_master_read(&buf[..len])?;
}
} else if fdset.contains(STDIN_FILENO) {
let len = read(STDIN_FILENO, &mut buf)?;
if len == 0 {
fdset.remove(STDIN_FILENO);
} else {
handle_stdin_read(&buf[..len])?;
}
} else if fdset.contains(signal_fd) {
// TODO
}
}
};
// Fork
let child_pid = match forkpty_result.fork_result {
ForkResult::Parent { child } => child,
ForkResult::Child => {
let cstr_args: Vec<_> = args
.into_iter()
.map(|s| CString::new(*s).unwrap())
.collect();
let args: Vec<_> = cstr_args.iter().map(|s| s.as_ref()).collect();
let cstr_env: Vec<_> = env::vars()
.map(|(k, v)| CString::new(format!("{}={}", k, v)).unwrap())
.collect();
let env: Vec<_> = cstr_env.iter().map(|s| s.as_ref()).collect();
execvpe(&args[0], &args, &env)?;
unreachable!();
}
};
let (pipe_r, pipe_w) = pipe()?;
let mut flags = fcntl(pipe_w, FcntlArg::F_GETFL)?;
flags |= libc::O_NONBLOCK;
fcntl(pipe_w, FcntlArg::F_SETFL(OFlag::from_bits(flags).unwrap()))?;
// Intercept signal handlers
let old_handlers = set_signals(
[
signal_hook::SIGWINCH,
signal_hook::SIGCHLD,
signal_hook::SIGHUP,
signal_hook::SIGTERM,
signal_hook::SIGQUIT,
]
.iter()
.map(|sig| (*sig, move || eprintln!("HIT SIGNAL {:?}", *sig))),
)?;
// set_pty_size()?;
{
let _term = RawTerm::init(STDIN_FILENO)?;
copy(pipe_r)?;
}
unset_signals(old_handlers)?;
waitpid(child_pid, None)?;
Ok(())
}
fn set_signals<I, F>(signals_list: I) -> Result<Vec<SigId>, io::Error>
where
I: Iterator<Item = (i32, F)>,
F: Fn() + Sync + Send + 'static,
{
let mut old_handlers = Vec::new();
for (sig, handler) in signals_list {
old_handlers.push(unsafe { signal_hook::register(sig, handler) }?);
}
Ok(old_handlers)
}
fn unset_signals(handlers_list: Vec<SigId>) -> Result<(), io::Error> {
for handler in handlers_list {
signal_hook::unregister(handler);
}
Ok(())
}
struct RawTerm(RawFd, Termios);
impl RawTerm {
pub fn init(fd: RawFd) -> Result<Self> {
use nix::sys::termios::*;
let saved_mode = tcgetattr(fd)?;
let mut mode = saved_mode.clone();
mode.input_flags &= !(InputFlags::BRKINT
| InputFlags::ICRNL
| InputFlags::INPCK
| InputFlags::ISTRIP
| InputFlags::IXON);
mode.output_flags &= !OutputFlags::OPOST;
mode.control_flags &= !(ControlFlags::CSIZE | ControlFlags::PARENB);
mode.control_flags |= ControlFlags::CS8;
mode.local_flags &= !(LocalFlags::ECHO
| LocalFlags::ICANON
| LocalFlags::IEXTEN
| LocalFlags::ISIG);
mode.control_chars[libc::VMIN] = 1;
mode.control_chars[libc::VTIME] = 0;
tcsetattr(fd, SetArg::TCSAFLUSH, &mode)?;
Ok(RawTerm(fd, saved_mode))
}
}
impl Drop for RawTerm {
fn drop(&mut self) {
tcsetattr(self.0, SetArg::TCSAFLUSH, &self.1).unwrap();
}
}
*/