use crate::event::Event;
use crate::input::{Input, MAX_TIMEOUT};
use crate::raw_mode::{RawMode, MOUSE_OFF, MOUSE_ON};
use std::io::{self, Read, Write};
use std::sync::mpsc::{channel, Receiver};
use std::thread;
use std::time::Duration;
use terminal_graphics::Display;
const HIDE_CURSOR: &str = "\x1b[?25l";
const SHOW_CURSOR: &str = "\x1b[?25h";
const ENTER_ALT_SCREEN: &str = "\x1b[?1049h";
const LEAVE_ALT_SCREEN: &str = "\x1b[?1049l";
const PASTE_ON: &str = "\x1b[?2004h";
const PASTE_OFF: &str = "\x1b[?2004l";
const DEFAULT_SIZE: (u32, u32) = (80, 24);
const SIZE_REPLY_WAIT: Duration = Duration::from_millis(300);
fn start_sequence() -> String {
format!("{ENTER_ALT_SCREEN}{MOUSE_ON}{PASTE_ON}{HIDE_CURSOR}")
}
fn stop_sequence() -> String {
format!("{MOUSE_OFF}{PASTE_OFF}{SHOW_CURSOR}{LEAVE_ALT_SCREEN}")
}
fn size_query_sequence() -> String {
"[999;999H[6n[H".to_string()
}
fn size_from_reply(reply: Option<(u32, u32)>) -> (u32, u32) {
match reply {
Some((width, height)) if width > 0 && height > 0 => (width, height),
_ => DEFAULT_SIZE,
}
}
fn frame_for(display: &Display) -> String {
let (_, height) = display.size();
if height == 0 {
return String::new();
}
format!("[{};1H{}", height + 1, display.render())
}
fn spawn_reader(mut reader: impl Read + Send + 'static) -> io::Result<Receiver<Vec<u8>>> {
let (tx, rx) = channel();
thread::Builder::new()
.name("terminal_graphics_interactive-input".into())
.spawn(move || {
let mut buffer = [0u8; 1024];
loop {
match reader.read(&mut buffer) {
Ok(0) => break,
Ok(n) => {
if tx.send(buffer[..n].to_vec()).is_err() {
break;
}
}
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
Err(_) => break,
}
}
})?;
Ok(rx)
}
pub struct Terminal {
input: Input,
size: (u32, u32),
_raw: RawMode,
}
impl Terminal {
pub fn start() -> io::Result<Terminal> {
let raw = RawMode::enable()?;
let mut out = io::stdout();
out.write_all(start_sequence().as_bytes())?;
out.flush()?;
let chunks = spawn_reader(io::stdin())?;
let mut input = Input::new(chunks);
out.write_all(size_query_sequence().as_bytes())?;
out.flush()?;
let size = size_from_reply(input.request_size(SIZE_REPLY_WAIT));
Ok(Terminal {
input,
size,
_raw: raw,
})
}
pub fn size(&self) -> (u32, u32) {
self.size
}
pub fn draw(&mut self, display: &Display) -> io::Result<()> {
let mut out = io::stdout();
out.write_all(frame_for(display).as_bytes())?;
out.flush()
}
pub fn poll(&mut self, timeout: Duration) -> io::Result<Option<Event>> {
self.input.poll(timeout)
}
pub fn wait(&mut self) -> io::Result<Event> {
loop {
if let Some(event) = self.input.poll(MAX_TIMEOUT)? {
return Ok(event);
}
}
}
}
impl Drop for Terminal {
fn drop(&mut self) {
let mut out = io::stdout();
let _ = out.write_all(stop_sequence().as_bytes());
let _ = out.flush();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
use std::time::Duration;
#[test]
fn start_turns_on_mouse_reporting_and_hides_the_cursor() {
let start = start_sequence();
assert!(start.contains(MOUSE_ON));
assert!(start.contains(HIDE_CURSOR));
}
#[test]
fn stop_turns_mouse_reporting_off_and_shows_the_cursor_again() {
let stop = stop_sequence();
assert!(stop.contains(MOUSE_OFF));
assert!(stop.contains(SHOW_CURSOR));
}
#[test]
fn start_enters_the_alternate_screen_and_stop_leaves_it_last() {
assert!(start_sequence().contains(ENTER_ALT_SCREEN));
let stop = stop_sequence();
assert!(stop.ends_with(LEAVE_ALT_SCREEN), "{stop:?}");
}
#[test]
fn start_asks_for_bracketed_paste_and_stop_turns_it_off_before_leaving() {
assert!(start_sequence().contains("\x1b[?2004h"));
let stop = stop_sequence();
let off = stop.find("\x1b[?2004l").expect("turns bracketed paste off");
let leave = stop
.find(LEAVE_ALT_SCREEN)
.expect("leaves the alternate screen");
assert!(
off < leave,
"paste mode is reset while still on the alternate screen"
);
}
#[test]
fn the_size_query_goes_to_the_far_corner_asks_for_the_cursor_and_comes_back() {
let query = size_query_sequence();
let corner = query
.find("\x1b[999;999H")
.expect("moves to the far corner");
let ask = query.find("\x1b[6n").expect("asks for the cursor position");
assert!(corner < ask);
assert!(
query.ends_with("\x1b[H"),
"returns the cursor home: {query:?}"
);
}
#[test]
fn size_from_a_reply_uses_it() {
assert_eq!(size_from_reply(Some((120, 40))), (120, 40));
}
#[test]
fn size_without_a_usable_reply_falls_back_to_80_by_24() {
assert_eq!(size_from_reply(None), (80, 24));
assert_eq!(size_from_reply(Some((0, 0))), (80, 24));
assert_eq!(size_from_reply(Some((0, 30))), (80, 24));
assert_eq!(size_from_reply(Some((90, 0))), (80, 24));
}
#[test]
fn a_frame_puts_the_cursor_one_row_below_the_display_then_renders_it() {
let mut display = Display::new(10, 3);
display.set_pixel(
0,
0,
'x',
terminal_graphics::Colour::Red,
terminal_graphics::Colour::Blue,
);
let frame = frame_for(&display);
assert!(frame.starts_with("\x1b[4;1H"), "{frame:?}");
assert!(frame.ends_with(&display.render()));
}
#[test]
fn a_frame_for_an_empty_display_is_empty() {
assert_eq!(frame_for(&Display::new(10, 0)), "");
}
#[test]
fn the_reader_sends_what_it_reads_then_closes_the_channel() {
let rx = spawn_reader(Cursor::new(b"hello".to_vec())).unwrap();
let mut all = Vec::new();
while let Ok(chunk) = rx.recv_timeout(Duration::from_secs(2)) {
assert!(!chunk.is_empty(), "empty chunks must not be sent");
all.extend(chunk);
}
assert_eq!(all, b"hello");
assert!(matches!(
rx.recv_timeout(Duration::from_millis(50)),
Err(std::sync::mpsc::RecvTimeoutError::Disconnected)
));
}
#[test]
fn the_reader_stops_when_the_reader_fails() {
struct Broken;
impl Read for Broken {
fn read(&mut self, _: &mut [u8]) -> io::Result<usize> {
Err(io::Error::new(io::ErrorKind::Other, "boom"))
}
}
let rx = spawn_reader(Broken).unwrap();
assert!(matches!(
rx.recv_timeout(Duration::from_secs(2)),
Err(std::sync::mpsc::RecvTimeoutError::Disconnected)
));
}
#[test]
fn the_reader_stops_when_the_receiver_is_dropped() {
struct Endless;
impl Read for Endless {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
buf[0] = b'x';
Ok(1)
}
}
let rx = spawn_reader(Endless).unwrap();
assert!(rx.recv_timeout(Duration::from_secs(2)).is_ok());
drop(rx);
}
}