diff --git a/Cargo.toml b/Cargo.toml index 7f87449..3670c11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } default = ["text", "binary"] text = [] binary = [] +palette = [] [dev-dependencies] tempfile = "*" diff --git a/src/color.rs b/src/color.rs index b372a72..2341176 100644 --- a/src/color.rs +++ b/src/color.rs @@ -2,13 +2,23 @@ use std::fmt::Display; use rand::{distributions::Standard, prelude::Distribution}; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum Color { RGB24(u8, u8, u8), RGBA32(u8, u8, u8, u8), W8(u8), } +impl Color { + pub fn to_bytes(&self) -> [u8; 4] { + match self { + Color::RGB24(r, g, b) => [*r, *g, *b, 0xff], + Color::RGBA32(r, g, b, a) => [*r, *g, *b, *a], + Color::W8(w) => [*w, *w, *w, 0xff], + } + } +} + impl Display for Color { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/src/flutclient.rs b/src/flutclient.rs index ddd9f2f..a2682b1 100644 --- a/src/flutclient.rs +++ b/src/flutclient.rs @@ -5,12 +5,18 @@ use std::{ use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}; +#[cfg(feature = "binary")] +use crate::protocols::BinaryParser; +#[cfg(feature = "palette")] +use crate::protocols::PaletteParser; +#[cfg(feature = "text")] +use crate::protocols::TextParser; use crate::{ get_pixel, grid::{self, Flut}, increment_counter, - protocols::{BinaryParser, IOProtocol, Parser, Responder, TextParser}, - set_pixel_rgba, Canvas, Color, Command, Coordinate, Protocol, Response, + protocols::{IOProtocol, Parser, Responder}, + set_pixel_rgba, Canvas, Color, Command, Coordinate, Protocol, ProtocolStatus, Response, }; macro_rules! build_parser_type_enum { @@ -26,16 +32,27 @@ macro_rules! build_parser_type_enum { impl std::default::Default for ParserTypes { // add code here + #[allow(unreachable_code)] fn default() -> Self { $( #[cfg(feature = $feat)] - #[allow(unreachable_code)] return ParserTypes::$name(<$t>::default()); )* } } impl ParserTypes { + pub fn get_status() -> Vec { + vec![ + $( + #[cfg(feature = $feat)] + ProtocolStatus::Enabled($feat), + #[cfg(not(feature = $feat))] + ProtocolStatus::Disabled($feat), + )* + ] + } + pub fn announce() { $( #[cfg(feature = $feat)] @@ -62,6 +79,7 @@ macro_rules! build_parser_type_enum { build_parser_type_enum! { TextParser: TextParser: "text", BinaryParser: BinaryParser: "binary", + PaletteParser: PaletteParser: "palette", } pub struct FlutClient @@ -88,6 +106,14 @@ where Ok(()) } + async fn protocols_command(&mut self) -> io::Result<()> { + match_parser! { + parser: self.parser => parser.unparse(Response::Protocols(ParserTypes::get_status()), &mut self.writer).await? + }; + self.writer.flush().await?; + Ok(()) + } + async fn size_command(&mut self, canvas: Canvas) -> io::Result<()> { let (x, y) = self.grids[canvas as usize].get_size(); match_parser!(parser: self.parser => parser.unparse( @@ -129,23 +155,37 @@ where match_parser!(parser: self.parser => parser.change_canvas(canvas)) } - fn change_protocol(&mut self, protocol: &Protocol) { + async fn change_protocol(&mut self, protocol: &Protocol) -> io::Result<()> { match protocol { #[cfg(feature = "text")] Protocol::Text => self.parser = ParserTypes::TextParser(TextParser::default()), #[cfg(not(feature = "text"))] Protocol::Text => { - self.writer.write(b"feature \"text\" is not enabled."); - self.writer.flush(); + self.writer + .write_all(b"feature \"text\" is not enabled.") + .await?; + self.writer.flush().await?; } #[cfg(feature = "binary")] Protocol::Binary => self.parser = ParserTypes::BinaryParser(BinaryParser::default()), #[cfg(not(feature = "binary"))] Protocol::Binary => { - self.writer.write(b"feature \"binary\" is not enabled."); - self.writer.flush(); + self.writer + .write_all(b"feature \"binary\" is not enabled.") + .await?; + self.writer.flush().await?; + } + #[cfg(feature = "palette")] + Protocol::Palette => self.parser = ParserTypes::PaletteParser(PaletteParser::default()), + #[cfg(not(feature = "palette"))] + Protocol::Palette => { + self.writer + .write_all(b"feature \"binary\" is not enabled.") + .await?; + self.writer.flush().await?; } } + Ok(()) } pub fn new(reader: R, writer: W, grids: Arc<[grid::Flut]>) -> Self { @@ -166,6 +206,7 @@ where match parsed { Ok(Command::Help) => self.help_command().await?, Ok(Command::Size(canvas)) => self.size_command(canvas).await?, + Ok(Command::Protocols) => self.protocols_command().await?, Ok(Command::GetPixel(canvas, x, y)) => self.get_pixel_command(canvas, x, y).await?, Ok(Command::SetPixel(canvas, x, y, color)) => self.set_pixel_command(canvas, x, y, &color), Ok(Command::ChangeCanvas(canvas)) => { @@ -173,7 +214,7 @@ where break 'outer; } Ok(Command::ChangeProtocol(protocol)) => { - self.change_protocol(&protocol); + self.change_protocol(&protocol).await?; break 'outer; } Err(err) if err.kind() == ErrorKind::UnexpectedEof => { diff --git a/src/lib.rs b/src/lib.rs index 4b35c6f..c4754e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,15 +53,23 @@ fn increment_counter(amount: u64) { COUNTER.fetch_add(amount, std::sync::atomic::Ordering::Relaxed); } +#[derive(Debug, PartialEq)] +pub enum ProtocolStatus { + Enabled(&'static str), + Disabled(&'static str), +} + #[derive(Debug, PartialEq)] pub enum Protocol { Text, Binary, + Palette, } #[derive(Debug, PartialEq)] pub enum Command { Help, + Protocols, Size(Canvas), GetPixel(Canvas, Coordinate, Coordinate), SetPixel(Canvas, Coordinate, Coordinate, Color), @@ -72,6 +80,7 @@ pub enum Command { #[derive(Debug, PartialEq)] pub enum Response { Help, + Protocols(Vec), Size(Coordinate, Coordinate), GetPixel(Coordinate, Coordinate, [u8; 3]), } diff --git a/src/protocols.rs b/src/protocols.rs index 12aa1a6..5fe8c4c 100644 --- a/src/protocols.rs +++ b/src/protocols.rs @@ -1,9 +1,11 @@ mod binary_protocol; +mod palette_protocol; mod text_protocol; use std::io; pub use binary_protocol::BinaryParser; +pub use palette_protocol::PaletteParser; pub use text_protocol::TextParser; use tokio::io::AsyncWriteExt; diff --git a/src/protocols/binary_protocol.rs b/src/protocols/binary_protocol.rs index e505112..5bb0a13 100644 --- a/src/protocols/binary_protocol.rs +++ b/src/protocols/binary_protocol.rs @@ -7,6 +7,7 @@ use crate::{Canvas, Color, Command, Response}; use super::{IOProtocol, Parser, Responder}; const SIZE_BIN: u8 = 115; +const PROTOCOLS_BIN: u8 = 116; const HELP_BIN: u8 = 104; const GET_PX_BIN: u8 = 32; const SET_PX_RGB_BIN: u8 = 128; @@ -22,6 +23,7 @@ impl Parser for Binar match fst { Ok(command) => match command { HELP_BIN => Ok(Command::Help), + PROTOCOLS_BIN => Ok(Command::Protocols), SIZE_BIN => { let canvas = reader.read_u8().await?; Ok(Command::Size(canvas)) @@ -106,6 +108,23 @@ To set a pixel using RGB, use ({SET_PX_RGB_BIN:02X}) (u8 canvas) (x as u16_le) ( ); writer.write_all(help_text.as_bytes()).await } + Response::Protocols(protos) => { + for protocol in protos { + match protocol { + crate::ProtocolStatus::Enabled(proto) => { + writer + .write_all(format!("Enabled: {}\n", proto).as_bytes()) + .await?; + } + crate::ProtocolStatus::Disabled(proto) => { + writer + .write_all(format!("Disabled: {}\n", proto).as_bytes()) + .await?; + } + } + } + Ok(()) + } Response::Size(x, y) => { writer.write_u16(x).await?; writer.write_u16(y).await diff --git a/src/protocols/palette_protocol.rs b/src/protocols/palette_protocol.rs new file mode 100644 index 0000000..992fedd --- /dev/null +++ b/src/protocols/palette_protocol.rs @@ -0,0 +1,139 @@ +use std::io::{self, Error, ErrorKind}; + +use image::EncodableLayout; +use rand::random; +use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt}; + +use crate::{Canvas, Color, Command, Response}; + +use super::{IOProtocol, Parser, Responder}; + +const SIZE_BIN: u8 = 115; +const PROTOCOLS_BIN: u8 = 116; +const HELP_BIN: u8 = 104; +const GET_PX_BIN: u8 = 32; +const SET_PX_PALETTE_BIN: u8 = 33; + +#[derive(Clone)] +pub struct PaletteParser { + colors: [Color; 256], +} + +impl Default for PaletteParser { + fn default() -> Self { + PaletteParser { + colors: [0; 256].map(|_| random()), + } + } +} + +impl Parser for PaletteParser { + async fn parse(&self, reader: &mut R) -> io::Result { + let fst = reader.read_u8().await; + match fst { + Ok(command) => match command { + HELP_BIN => Ok(Command::Help), + PROTOCOLS_BIN => Ok(Command::Protocols), + SIZE_BIN => { + let canvas = reader.read_u8().await?; + Ok(Command::Size(canvas)) + } + GET_PX_BIN => { + let canvas = reader.read_u8().await?; + let horizontal = reader.read_u16().await?; + let vertical = reader.read_u16().await?; + Ok(Command::GetPixel(canvas, horizontal, vertical)) + } + SET_PX_PALETTE_BIN => { + let canvas = reader.read_u8().await?; + let horizontal = reader.read_u16().await?; + let vertical = reader.read_u16().await?; + let color = reader.read_u8().await?; + Ok(Command::SetPixel(canvas, horizontal, vertical, unsafe { + self.colors.get_unchecked(color as usize).clone() + })) + } + _ => { + tracing::error!("received illegal command: {command}"); + Err(Error::from(ErrorKind::InvalidInput)) + } + }, + Err(err) => { + tracing::error!("{err}"); + Err(err) + } + } + } +} + +impl IOProtocol for PaletteParser { + fn change_canvas(&mut self, _canvas: Canvas) -> io::Result<()> { + Err(Error::from(ErrorKind::Unsupported)) + } +} + +impl Responder for PaletteParser { + async fn unparse(&self, response: Response, writer: &mut W) -> io::Result<()> { + match response { + Response::Help => { + writer + .write_all( + self.colors + .iter() + .map(|c| c.to_bytes()) + .collect::>() + .concat() + .as_bytes(), + ) + .await + } + Response::Protocols(protos) => { + for protocol in protos { + match protocol { + crate::ProtocolStatus::Enabled(proto) => { + writer + .write_all(format!("Enabled: {}\n", proto).as_bytes()) + .await?; + } + crate::ProtocolStatus::Disabled(proto) => { + writer + .write_all(format!("Disabled: {}\n", proto).as_bytes()) + .await?; + } + } + } + Ok(()) + } + Response::Size(x, y) => { + writer.write_u16(x).await?; + writer.write_u16(y).await + } + Response::GetPixel(_, _, c) => { + writer.write_u8(c[0]).await?; + writer.write_u8(c[1]).await?; + writer.write_u8(c[2]).await + } + } + } +} + +#[cfg(test)] +#[allow(clippy::needless_return)] +mod tests { + use super::*; + use tokio::io::BufReader; + + #[tokio::test] + async fn test_palette_px_set_parse() { + let parser = PaletteParser::default(); + let reader = tokio_test::io::Builder::new() + .read(&[SET_PX_PALETTE_BIN, 0x01, 0x69, 0x42, 0x42, 0x69, 0x82]) + .build(); + let mut bufreader = BufReader::new(reader); + let thingy = parser.parse(&mut bufreader).await.unwrap(); + assert_eq!( + thingy, + Command::SetPixel(1, 0x6942, 0x4269, parser.colors[0x82].clone()) + ); + } +} diff --git a/src/protocols/text_protocol.rs b/src/protocols/text_protocol.rs index 1c711a2..59fe728 100644 --- a/src/protocols/text_protocol.rs +++ b/src/protocols/text_protocol.rs @@ -106,6 +106,7 @@ impl TextParser { match protocol { "binary" => Ok(Command::ChangeProtocol(Protocol::Binary)), "text" => Ok(Command::ChangeProtocol(Protocol::Text)), + "palette" => Ok(Command::ChangeProtocol(Protocol::Palette)), _ => Err(Error::from(ErrorKind::InvalidInput)), } } @@ -117,6 +118,8 @@ impl Parser for TextP if reader.read_line(&mut line).await.is_ok() { if line.starts_with("HELP") { return Ok(Command::Help); + } else if line.starts_with("PROTOCOLS") { + return Ok(Command::Protocols); } else if line.starts_with("SIZE") { return Ok(Command::Size(self.canvas)); } else if line.starts_with("PX ") { @@ -146,6 +149,23 @@ impl Responder for TextParser { async fn unparse(&self, response: Response, writer: &mut W) -> io::Result<()> { match response { Response::Help => writer.write_all(HELP_TEXT).await, + Response::Protocols(protos) => { + for protocol in protos { + match protocol { + crate::ProtocolStatus::Enabled(proto) => { + writer + .write_all(format!("Enabled: {}\n", proto).as_bytes()) + .await?; + } + crate::ProtocolStatus::Disabled(proto) => { + writer + .write_all(format!("Disabled: {}\n", proto).as_bytes()) + .await?; + } + } + } + Ok(()) + } Response::Size(x, y) => writer.write_all(format!("SIZE {x} {y}\n").as_bytes()).await, Response::GetPixel(x, y, color) => { writer