use std::io::{self, Read, Write};
use std::sync::Arc;
use std::sync::mpsc::{Sender, channel};
use std::time::Duration;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::style::{
Attribute, Color as CtColor, Print, SetAttribute, SetBackgroundColor, SetForegroundColor,
};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use crossterm::{cursor, execute, queue};
use wimux_protocol::transport::PipeConn;
use wimux_protocol::{ClientMessage, Frame, ServerMessage, Version, recv, send};
use wimux_vt::{Cell, Color, Pen};
mod console;
#[derive(Debug, Clone)]
pub enum ExitReason {
Detached,
PaneExited(u32),
ServerGone,
}
pub fn is_server_compatible(server: Version) -> bool {
wimux_protocol::PROTOCOL_VERSION.is_compatible_with(server)
}
pub fn run(conn: PipeConn) -> io::Result<ExitReason> {
let conn = Arc::new(conn);
enable_raw_mode()?;
let mut out = io::stdout();
execute!(out, EnterAlternateScreen, cursor::Hide, EnableMouseCapture)?;
let _stdin_mode = console::RawStdinGuard::set();
let (exit_tx, exit_rx) = channel::<ExitReason>();
let (out_tx, out_rx) = channel::<ClientMessage>();
{
let conn = Arc::clone(&conn);
let exit_tx = exit_tx.clone();
std::thread::spawn(move || {
for msg in out_rx {
let mut w: &PipeConn = &conn;
if send(&mut w, &msg).is_err() {
let _ = exit_tx.send(ExitReason::ServerGone);
break;
}
}
});
}
{
let conn = Arc::clone(&conn);
let exit_tx = exit_tx.clone();
std::thread::spawn(move || {
let mut reader: &PipeConn = &conn;
loop {
match recv::<_, ServerMessage>(&mut reader) {
Ok(ServerMessage::Frame(frame)) => {
let _ = render(&frame);
}
Ok(ServerMessage::PaneExited { code }) => {
let _ = exit_tx.send(ExitReason::PaneExited(code));
break;
}
Ok(ServerMessage::Detached) => {
let _ = exit_tx.send(ExitReason::Detached);
break;
}
Ok(ServerMessage::SetClipboard(text)) => {
let _ = clipboard_win::set_clipboard_string(&text);
}
Ok(_) => {}
Err(_) => {
let _ = exit_tx.send(ExitReason::ServerGone);
break;
}
}
}
});
}
{
let out_tx = out_tx.clone();
std::thread::spawn(move || {
let mut last = crossterm::terminal::size().unwrap_or((80, 24));
loop {
std::thread::sleep(Duration::from_millis(200));
if let Ok(size) = crossterm::terminal::size()
&& size != last
{
last = size;
if out_tx
.send(ClientMessage::Resize {
cols: size.0,
rows: size.1,
})
.is_err()
{
break;
}
}
}
});
}
{
let out_tx = out_tx.clone();
std::thread::spawn(move || input_loop(out_tx));
}
let reason = exit_rx.recv().unwrap_or(ExitReason::ServerGone);
let mut out = io::stdout();
let _ = execute!(out, cursor::Show, DisableMouseCapture, LeaveAlternateScreen);
let _ = disable_raw_mode();
Ok(reason)
}
fn input_loop(out_tx: Sender<ClientMessage>) {
let mut stdin = io::stdin();
let mut buf = [0u8; 1024];
loop {
let n = match stdin.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => n,
};
if out_tx
.send(ClientMessage::Input(buf[..n].to_vec()))
.is_err()
{
break;
}
}
}
fn render(frame: &Frame) -> io::Result<()> {
let mut out = io::stdout();
queue!(out, cursor::Hide, cursor::MoveTo(0, 0))?;
let mut last_pen: Option<Pen> = None;
for row in 0..frame.rows {
queue!(out, cursor::MoveTo(0, row))?;
let mut col = 0u16;
while col < frame.cols {
let idx = row as usize * frame.cols as usize + col as usize;
let cell = &frame.cells[idx];
if cell.is_continuation() {
col += 1;
continue;
}
if last_pen != Some(cell.pen) {
apply_pen(&mut out, cell.pen)?;
last_pen = Some(cell.pen);
}
queue!(out, Print(cell.ch))?;
col += (cell.width as u16).max(1);
}
}
apply_pen(&mut io::stdout(), Pen::default())?;
queue!(
out,
cursor::MoveTo(frame.cursor_col, frame.cursor_row),
cursor::Show
)?;
out.flush()
}
fn apply_pen(out: &mut impl Write, pen: Pen) -> io::Result<()> {
queue!(out, SetAttribute(Attribute::Reset))?;
queue!(out, SetForegroundColor(to_ct(pen.fg, true)))?;
queue!(out, SetBackgroundColor(to_ct(pen.bg, false)))?;
if pen.attrs.bold {
queue!(out, SetAttribute(Attribute::Bold))?;
}
if pen.attrs.italic {
queue!(out, SetAttribute(Attribute::Italic))?;
}
if pen.attrs.underline {
queue!(out, SetAttribute(Attribute::Underlined))?;
}
if pen.attrs.reverse {
queue!(out, SetAttribute(Attribute::Reverse))?;
}
Ok(())
}
fn to_ct(color: Color, _foreground: bool) -> CtColor {
match color {
Color::Default => CtColor::Reset,
Color::Indexed(n) => CtColor::AnsiValue(n),
Color::Rgb(r, g, b) => CtColor::Rgb { r, g, b },
}
}
pub fn frame_to_text(frame: &Frame) -> String {
let mut lines = Vec::new();
for row in 0..frame.rows {
let mut line = String::new();
for cell in cells_of_row(frame, row) {
if !cell.is_continuation() {
line.push(cell.ch);
}
}
lines.push(line.trim_end().to_string());
}
lines.join("\n")
}
fn cells_of_row(frame: &Frame, row: u16) -> &[Cell] {
let start = row as usize * frame.cols as usize;
&frame.cells[start..start + frame.cols as usize]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serveur_meme_majeure_compatible() {
let server = Version {
major: wimux_protocol::PROTOCOL_VERSION.major,
minor: wimux_protocol::PROTOCOL_VERSION.minor + 5,
};
assert!(is_server_compatible(server));
}
}