use std::io::{self, ErrorKind};
use std::net::TcpStream;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use intuituive::interact::Event;
use intuituive::{App, Driver};
use tungstenite::protocol::CloseFrame;
use tungstenite::protocol::frame::coding::CloseCode;
use tungstenite::{Message, WebSocket};
use crate::Renderer;
use crate::dom::Dom;
use crate::input::decode;
const SETUP: &str = "\x1b[?1049h\x1b[H\x1b[2J\x1b[?1000h\x1b[?1002h\x1b[?1006h\x1b[?2004h\x1b[?25l";
const TEARDOWN: &str = "\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1006l\x1b[?2004l";
pub(crate) const MAX_CELLS: u16 = 1000;
const CLOSE_WAIT: Duration = Duration::from_secs(2);
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum PageMessage {
Input(Vec<Event>),
Resize(u16, u16),
Copied(u64, bool),
Shown(u64),
Unknown,
}
pub(crate) fn parse_message(text: &str) -> PageMessage {
if let Some(input) = text.strip_prefix('d') {
return PageMessage::Input(decode(input));
}
if let Some(size) = text.strip_prefix('r')
&& let Some((columns, rows)) = parse_size(size)
{
return PageMessage::Resize(columns, rows);
}
if let Some((number, ok)) = text.strip_prefix('c').and_then(|t| t.split_once(':'))
&& let (Ok(number), "0" | "1") = (number.parse(), ok)
{
return PageMessage::Copied(number, ok == "1");
}
if let Some(Ok(bytes)) = text.strip_prefix('a').map(str::parse) {
return PageMessage::Shown(bytes);
}
PageMessage::Unknown
}
pub(crate) fn parse_size(text: &str) -> Option<(u16, u16)> {
let (columns, rows) = text.split_once(',')?;
let columns: u16 = columns.trim().parse().ok()?;
let rows: u16 = rows.trim().parse().ok()?;
let ok = |n: u16| (1..=MAX_CELLS).contains(&n);
(ok(columns) && ok(rows)).then_some((columns, rows))
}
pub(crate) fn run(
socket: &mut WebSocket<TcpStream>,
app: App,
columns: u16,
rows: u16,
stopping: &AtomicBool,
renderer: Renderer,
z: Compress,
) -> io::Result<()> {
let start = Instant::now();
let mut driver = app.driver(columns, rows);
driver.set_clipboard(true);
let mut copies = Copies::default();
let mut dom = (renderer == Renderer::Dom).then(Dom::new);
match &dom {
Some(dom) => send(socket, &dom.hello(), z)?,
None => send(socket, SETUP, z)?,
}
let result = (|| -> io::Result<()> {
loop {
if stopping.load(Ordering::SeqCst) {
return Ok(());
}
driver.update(start.elapsed());
tell(&mut driver, socket, &mut copies, dom.as_mut(), z)?;
if driver.is_done() {
return Ok(());
}
if let Some(out) = driver.render() {
match &mut dom {
Some(dom) => {
for message in dom.update(&driver) {
send(socket, &message, z)?;
}
}
None if !out.is_empty() => send(socket, &out, z)?,
None => {}
}
}
tell(&mut driver, socket, &mut copies, dom.as_mut(), z)?;
if driver.is_done() {
return Ok(());
}
let wait = driver
.timeout(start.elapsed())
.max(Duration::from_millis(1));
socket.get_mut().set_read_timeout(Some(wait))?;
let message = match socket.read() {
Ok(message) => message,
Err(tungstenite::Error::Io(e))
if matches!(e.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) =>
{
continue;
}
Err(tungstenite::Error::ConnectionClosed | tungstenite::Error::AlreadyClosed) => {
return Ok(());
}
Err(tungstenite::Error::Io(e)) => return Err(e),
Err(e) => return Err(io::Error::other(e)),
};
let text = match &message {
Message::Text(text) => text.as_str(),
Message::Binary(bytes) => std::str::from_utf8(bytes).unwrap_or(""),
Message::Close(_) => return Ok(()),
_ => continue,
};
match parse_message(text) {
PageMessage::Input(events) => {
for event in events {
driver.event(event);
}
}
PageMessage::Resize(columns, rows) => driver.event(Event::Resize { columns, rows }),
PageMessage::Copied(number, ok) => copies.answered(&mut driver, number, ok),
PageMessage::Shown(_) | PageMessage::Unknown => {}
}
tell(&mut driver, socket, &mut copies, dom.as_mut(), z)?;
}
})();
if socket.can_write() && dom.is_none() {
let mut last = driver.finish();
last.push_str(TEARDOWN);
let _ = send(socket, &last, z);
}
close(socket, None);
result
}
fn tell(
driver: &mut Driver,
socket: &mut WebSocket<TcpStream>,
copies: &mut Copies,
dom: Option<&mut Dom>,
z: Compress,
) -> io::Result<()> {
let dom = dom.is_some();
copies.send(driver, socket, dom, z)?;
let said = driver.take_announcements();
if dom {
for announcement in said {
send(socket, &crate::dom::say(&announcement), z)?;
}
}
Ok(())
}
pub(crate) fn send(socket: &mut WebSocket<TcpStream>, text: &str, z: Compress) -> io::Result<()> {
deliver(socket, z.text(text))
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct Compress(pub Option<usize>);
impl Compress {
pub(crate) fn negotiate(server: Option<usize>, page_asks: bool) -> Compress {
Compress(page_asks.then(|| server.unwrap_or(usize::MAX)))
}
pub(crate) fn text(self, text: &str) -> Message {
match self.0 {
Some(at) if text.len() >= at => Message::binary(deflated(text.as_bytes())),
_ => Message::text(text),
}
}
pub(crate) fn bytes(self, bytes: &[u8]) -> Message {
match self.0 {
None => Message::binary(bytes.to_vec()),
Some(at) if bytes.len() >= at => Message::binary(deflated(bytes)),
Some(_) => {
let mut out = Vec::with_capacity(bytes.len() + 1);
out.push(0);
out.extend_from_slice(bytes);
Message::binary(out)
}
}
}
}
fn deflated(bytes: &[u8]) -> Vec<u8> {
use std::io::Write;
let mut z = flate2::write::DeflateEncoder::new(vec![1u8], flate2::Compression::new(1));
let _ = z.write_all(bytes);
z.finish().unwrap_or_default()
}
pub(crate) fn deliver(socket: &mut WebSocket<TcpStream>, message: Message) -> io::Result<()> {
socket.send(message).map_err(|e| match e {
tungstenite::Error::Io(e) => e,
e => io::Error::other(e),
})
}
pub(crate) fn close(socket: &mut WebSocket<TcpStream>, reason: Option<&str>) {
if socket.can_write() {
let frame = reason.map(|reason| {
let mut end = reason.len().min(123);
while !reason.is_char_boundary(end) {
end -= 1;
}
CloseFrame {
code: CloseCode::Normal,
reason: reason[..end].to_string().into(),
}
});
let _ = socket.close(frame);
}
let _ = socket.flush();
let deadline = Instant::now() + CLOSE_WAIT;
let _ = socket
.get_mut()
.set_read_timeout(Some(Duration::from_millis(50)));
while Instant::now() < deadline {
match socket.read() {
Ok(_) => {}
Err(tungstenite::Error::Io(e))
if matches!(e.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => {}
Err(_) => break,
}
}
}
#[derive(Default)]
struct Copies {
sent: u64,
waiting: std::collections::VecDeque<(u64, String)>,
}
const COPIES_WAITING: usize = 16;
impl Copies {
fn send(
&mut self,
driver: &mut Driver,
socket: &mut WebSocket<TcpStream>,
dom: bool,
z: Compress,
) -> io::Result<()> {
for text in driver.take_copies() {
self.sent += 1;
let message = if dom {
crate::dom::copy(self.sent, &text)
} else {
let encoded = data_encoding::BASE64.encode(text.as_bytes());
format!("\x1b]52;c;{encoded}\x07")
};
send(socket, &message, z)?;
self.waiting.push_back((self.sent, text));
if self.waiting.len() > COPIES_WAITING {
self.waiting.pop_front();
}
}
Ok(())
}
fn answered(&mut self, driver: &mut Driver, number: u64, ok: bool) {
if let Some(at) = self.waiting.iter().position(|(n, _)| *n == number) {
let (_, text) = self.waiting.remove(at).expect("found");
if ok {
driver.copied(&text);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use intuituive::interact::Key;
#[test]
fn a_copy_is_copied_only_once_the_page_says_so() {
use intuituive::prelude::*;
let mut driver = App::new(|| label("x")).driver(30, 4);
let toast = |driver: &mut Driver| {
driver.update(std::time::Duration::ZERO);
let _ = driver.render();
driver.screen().plain().join("\n").contains("Copied")
};
let mut copies = Copies::default();
copies.waiting.push_back((1, "abc".into()));
copies.waiting.push_back((2, "de".into()));
copies.answered(&mut driver, 1, false);
copies.answered(&mut driver, 9, true);
assert!(!toast(&mut driver));
copies.answered(&mut driver, 2, true);
assert!(toast(&mut driver));
assert!(copies.waiting.is_empty());
}
#[test]
fn messages_parse() {
assert_eq!(
parse_message("dq\r"),
PageMessage::Input(vec![
Event::Key(Key::char('q')),
Event::Key(Key::parse("enter").unwrap())
])
);
assert_eq!(parse_message("r120,40"), PageMessage::Resize(120, 40));
assert_eq!(parse_message("r0,40"), PageMessage::Unknown);
assert_eq!(parse_message("r5000,40"), PageMessage::Unknown);
assert_eq!(parse_message("rx,y"), PageMessage::Unknown);
assert_eq!(parse_message("zzz"), PageMessage::Unknown);
assert_eq!(parse_message(""), PageMessage::Unknown);
assert_eq!(parse_message("d"), PageMessage::Input(vec![]));
assert_eq!(parse_message("c3:1"), PageMessage::Copied(3, true));
assert_eq!(parse_message("c3:0"), PageMessage::Copied(3, false));
assert_eq!(parse_message("c3:2"), PageMessage::Unknown);
assert_eq!(parse_message("cx:1"), PageMessage::Unknown);
assert_eq!(parse_message("a4096"), PageMessage::Shown(4096));
assert_eq!(parse_message("a-1"), PageMessage::Unknown);
assert_eq!(parse_message("a"), PageMessage::Unknown);
}
#[test]
fn a_page_that_asks_gets_every_binary_message_framed() {
let payload = |message: Message| match message {
Message::Binary(bytes) => bytes.to_vec(),
other => panic!("not binary: {other:?}"),
};
let z = Compress::negotiate(None, true);
assert_eq!(payload(z.bytes(b"hello\r\n")), b"\0hello\r\n");
assert_eq!(z.text("short"), Message::text("short"));
assert_eq!(
payload(Compress::negotiate(Some(1), false).bytes(b"hi")),
b"hi"
);
let z = Compress::negotiate(Some(4), true);
assert_eq!(payload(z.bytes(b"hi")), b"\0hi");
assert_eq!(payload(z.bytes(b"hello")).first(), Some(&1));
}
}