clipat 0.1.2

CLI tool for sharing clipboard with remote SSH sessions
Documentation
use std::io::{self, BufRead, BufReader, BufWriter, Read, Write};

use crate::clipboard::{Content, Image};

pub(crate) struct Connection<R: Read, W: Write> {
    reader: BufReader<R>,
    writer: BufWriter<W>,
}

impl<R: Read, W: Write> Connection<R, W> {
    pub(crate) fn new(reader: R, writer: W) -> Self {
        Self {
            reader: BufReader::new(reader),
            writer: BufWriter::new(writer),
        }
    }

    fn read_byte(&mut self) -> io::Result<u8> {
        let mut buf = [0];
        self.reader.read_exact(&mut buf)?;
        Ok(buf[0])
    }

    fn read_newline(&mut self) -> io::Result<()> {
        if self.read_byte()? != b'\n' {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                "Expected newline",
            ));
        }
        Ok(())
    }

    fn read_line(&mut self) -> io::Result<String> {
        let mut line = String::new();
        loop {
            let data = self.reader.fill_buf()?;
            if data.is_empty() {
                return Err(io::Error::new(
                    io::ErrorKind::UnexpectedEof,
                    "Unexpected EOF",
                ));
            }

            match data.iter().position(|b| *b == b'\n') {
                Some(pos) => {
                    line += std::str::from_utf8(&data[..pos])
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    self.reader.consume(pos + 1);
                    return Ok(line);
                }
                None => {
                    line += std::str::from_utf8(data)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    let len = data.len();
                    self.reader.consume(len);
                }
            }
        }
    }

    fn read_int<T: std::str::FromStr>(&mut self) -> io::Result<T>
    where
        T::Err: std::error::Error + Send + Sync + 'static,
    {
        self.read_line()?
            .parse()
            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
    }

    fn send_int<T: std::fmt::Display>(&mut self, int: T) -> io::Result<()> {
        self.writer.write_all(int.to_string().as_bytes())?;
        self.writer.write_all(b"\n")?;
        Ok(())
    }

    pub(crate) fn read_string(&mut self) -> io::Result<String> {
        let len = self.read_int()?;
        let mut data = vec![0; len];
        self.reader.read_exact(&mut data)?;
        self.read_newline()?;

        String::from_utf8(data).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
    }

    fn send_string(&mut self, string: &str) -> io::Result<()> {
        self.writer.write_all(string.len().to_string().as_bytes())?;
        self.writer.write_all(b"\n")?;
        self.writer.write_all(string.as_bytes())?;
        self.writer.write_all(b"\n")?;
        Ok(())
    }

    pub(crate) fn read_command(&mut self) -> io::Result<String> {
        self.read_line()
    }

    pub(crate) fn send_command(&mut self, command: &str) -> io::Result<()> {
        self.writer.write_all(command.as_bytes())?;
        self.writer.write_all(b"\n")?;
        self.writer.flush()
    }

    pub(crate) fn read_image(&mut self) -> io::Result<Image<'static>> {
        let width = self.read_int()?;
        let height = self.read_int()?;
        let mut bytes = vec![0; width * height * 4];
        self.reader.read_exact(&mut bytes)?;
        self.read_newline()?;

        Ok(Image {
            width,
            height,
            bytes: bytes.into(),
        })
    }

    pub(crate) fn send_clipboard(&mut self, content: &Option<Content<'_>>) -> io::Result<()> {
        let content = match content {
            Some(content) => content,
            None => {
                self.writer.write_all(b"COPY NONE\n")?;
                return self.writer.flush();
            }
        };

        match content {
            Content::Text(text) => {
                self.writer.write_all(b"COPY TEXT\n")?;
                self.send_string(text)?;
            }
            Content::Image(image) => {
                self.writer.write_all(b"COPY IMAGE\n")?;
                self.send_int(image.width)?;
                self.send_int(image.height)?;
                self.writer.write_all(&image.bytes)?;
                self.writer.write_all(b"\n")?;
            }
        }

        self.writer.flush()
    }

    pub(crate) fn send_error(&mut self, error: &str) -> io::Result<()> {
        self.writer.write_all(b"ERROR\n")?;
        self.send_string(error)?;
        self.writer.flush()
    }
}