use std::collections::VecDeque;
use std::io::{Read, Write};
use std::process::{Command, Stdio};
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, TryRecvError, channel};
use super::chardev::{CharDevice, CharPort};
const INTERRUPT: u8 = 0x03;
#[derive(Debug)]
pub struct Terminal {
input: Mutex<Input>,
saved: Option<String>,
interrupted: AtomicBool,
}
#[derive(Debug)]
struct Input {
rx: Receiver<Vec<u8>>,
pending: VecDeque<u8>,
closed: bool,
}
impl Terminal {
#[must_use]
pub fn open() -> Terminal {
let saved = enter_raw_mode();
let (tx, rx) = channel();
std::thread::Builder::new()
.name(String::from("rsemu-stdin"))
.spawn(move || {
let mut stdin = std::io::stdin();
let mut buf = [0u8; 256];
loop {
match stdin.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
if tx.send(buf[..n].to_vec()).is_err() {
break;
}
}
}
}
})
.ok();
Terminal {
input: Mutex::new(Input {
rx,
pending: VecDeque::new(),
closed: false,
}),
saved,
interrupted: AtomicBool::new(false),
}
}
#[must_use]
pub fn is_raw(&self) -> bool {
self.saved.is_some()
}
#[must_use]
pub fn interrupted(&self) -> bool {
self.interrupted.load(Ordering::Relaxed)
}
#[must_use]
pub fn at_eof(&self) -> bool {
let mut input = self.input.lock().expect("terminal input lock");
self.refill(&mut input);
input.closed && input.pending.is_empty()
}
pub fn pump(&self, port: &CharPort) -> usize {
let mut moved = 0;
let mut buf = [0u8; 256];
loop {
let n = self.read(&mut buf);
if n == 0 {
break;
}
moved += port.feed(&buf[..n]);
}
let out = port.drain();
if !out.is_empty() {
moved += self.write(&out);
self.flush();
}
moved
}
fn refill(&self, input: &mut Input) {
loop {
match input.rx.try_recv() {
Ok(chunk) => input.pending.extend(chunk),
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => {
input.closed = true;
break;
}
}
}
}
}
impl CharDevice for Terminal {
fn read(&self, dst: &mut [u8]) -> usize {
let mut input = self.input.lock().expect("terminal input lock");
self.refill(&mut input);
let mut taken = 0;
while taken < dst.len() {
let Some(byte) = input.pending.pop_front() else {
break;
};
if byte == INTERRUPT && self.saved.is_some() {
self.interrupted.store(true, Ordering::Relaxed);
continue;
}
dst[taken] = byte;
taken += 1;
}
taken
}
fn write(&self, src: &[u8]) -> usize {
let mut buf = Vec::with_capacity(src.len() + 8);
let mut i = 0;
while i < src.len() {
match src[i] {
b'\r' => {
buf.extend_from_slice(b"\r\n");
if src.get(i + 1) == Some(&b'\n') {
i += 1;
}
}
b'\n' => buf.extend_from_slice(b"\r\n"),
byte => buf.push(byte),
}
i += 1;
}
let ok = std::io::stdout().lock().write_all(&buf);
if ok.is_err() { 0 } else { src.len() }
}
fn flush(&self) {
let _ = std::io::stdout().flush();
}
}
impl Drop for Terminal {
fn drop(&mut self) {
if let Some(saved) = self.saved.take() {
let _ = stty(&[&saved]);
}
}
}
fn enter_raw_mode() -> Option<String> {
let saved = stty(&["-g"])?;
let saved = saved.trim().to_string();
if saved.is_empty() {
return None;
}
stty(&["raw", "-echo"])?;
Some(saved)
}
fn stty(args: &[&str]) -> Option<String> {
let output = Command::new("stty")
.args(args)
.stdin(Stdio::inherit())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.output()
.ok()?;
output
.status
.success()
.then(|| String::from_utf8_lossy(&output.stdout).into_owned())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_terminal_opens_without_a_tty_and_reports_that_it_is_cooked() {
let term = Terminal::open();
assert!(!term.is_raw(), "cargo test has no controlling terminal");
assert!(!term.interrupted());
let mut buf = [0u8; 4];
let _ = term.read(&mut buf);
}
#[test]
fn pumping_moves_guest_output_toward_the_host() {
let term = Terminal::open();
let port = CharPort::new();
port.write(b"");
assert_eq!(port.pending_output(), 0);
term.pump(&port);
assert_eq!(port.pending_output(), 0);
assert_eq!(port.pending_input(), 0);
}
}