From 3d47b4110695988bea735a9b7ed864d0750bcc94 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 12 Oct 2024 18:23:36 +0200 Subject: [PATCH] conversion between UTF-8 and CP-437 --- Cargo.lock | 1 + crates/servicepoint/Cargo.toml | 9 +- crates/servicepoint/examples/announce.rs | 20 ++-- crates/servicepoint/src/command.rs | 9 ++ crates/servicepoint/src/cp437.rs | 121 +++++++++++++++++++++++ crates/servicepoint/src/lib.rs | 2 +- crates/servicepoint/src/origin.rs | 7 ++ 7 files changed, 154 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90c56e3..4a84fab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -617,6 +617,7 @@ dependencies = [ "clap", "flate2", "log", + "once_cell", "rand", "rust-lzma", "tungstenite", diff --git a/crates/servicepoint/Cargo.toml b/crates/servicepoint/Cargo.toml index 09b9994..f66a607 100644 --- a/crates/servicepoint/Cargo.toml +++ b/crates/servicepoint/Cargo.toml @@ -21,9 +21,10 @@ zstd = { version = "0.13", optional = true } rust-lzma = { version = "0.6.0", optional = true } rand = { version = "0.8", optional = true } tungstenite = { version = "0.24.0", optional = true } +once_cell = { version = "1.20.2", optional = true } [features] -default = ["compression_lzma", "protocol_udp"] +default = ["compression_lzma", "protocol_udp", "cp437"] compression_zlib = ["dep:flate2"] compression_bzip2 = ["dep:bzip2"] compression_lzma = ["dep:rust-lzma"] @@ -32,15 +33,19 @@ all_compressions = ["compression_zlib", "compression_bzip2", "compression_lzma", rand = ["dep:rand"] protocol_udp = [] protocol_websocket = ["dep:tungstenite"] +cp437 = ["dep:once_cell"] [[example]] name = "random_brightness" required-features = ["rand"] +[[example]] +name = "game_of_life" +required-features = ["rand"] + [dev-dependencies] # for examples clap = { version = "4.5", features = ["derive"] } -rand = "0.8" [lints] workspace = true \ No newline at end of file diff --git a/crates/servicepoint/examples/announce.rs b/crates/servicepoint/examples/announce.rs index 75b9ba6..ff95479 100644 --- a/crates/servicepoint/examples/announce.rs +++ b/crates/servicepoint/examples/announce.rs @@ -2,7 +2,7 @@ use clap::Parser; -use servicepoint::{Command, Connection, Cp437Grid, Grid, Origin}; +use servicepoint::{CharGrid, Command, Connection, Cp437Grid, Origin}; #[derive(Parser, Debug)] struct Cli { @@ -31,19 +31,15 @@ fn main() { .expect("sending clear failed"); } - let max_width = cli.text.iter().map(|t| t.len()).max().unwrap(); + let text = cli.text.iter().fold(String::new(), move |str, line| { + let is_first = str.is_empty(); + str + if is_first { "" } else { "\n" } + line + }); - let mut chars = Cp437Grid::new(max_width, cli.text.len()); - for y in 0..cli.text.len() { - let row = &cli.text[y]; - - for (x, char) in row.chars().enumerate() { - let char = char.try_into().expect("invalid input char"); - chars.set(x, y, char); - } - } + let grid = CharGrid::from(&*text); + let cp437_grid = Cp437Grid::from(&grid); connection - .send(Command::Cp437Data(Origin::new(0, 0), chars)) + .send(Command::Cp437Data(Origin::new(0, 0), cp437_grid)) .expect("sending text failed"); } diff --git a/crates/servicepoint/src/command.rs b/crates/servicepoint/src/command.rs index 6095bb3..49c230d 100644 --- a/crates/servicepoint/src/command.rs +++ b/crates/servicepoint/src/command.rs @@ -86,6 +86,15 @@ pub enum Command { /// # Examples /// /// ```rust + /// # use servicepoint::{Command, Connection, Origin}; + /// # let connection = Connection::Fake; + /// use servicepoint::{CharGrid, Cp437Grid}; + /// let grid = CharGrid::from(&"Hello,\nWorld!"); + /// let grid = Cp437Grid::from(grid); + /// connection.send(Command::Cp437Data(Origin::ZERO, grid)).expect("send failed"); + /// ``` + /// + /// ```rust /// # use servicepoint::{Command, Connection, Cp437Grid, Origin}; /// # let connection = Connection::Fake; /// let grid = Cp437Grid::load_ascii("Hello\nWorld", 5, false).unwrap(); diff --git a/crates/servicepoint/src/cp437.rs b/crates/servicepoint/src/cp437.rs index b0fd991..dc40627 100644 --- a/crates/servicepoint/src/cp437.rs +++ b/crates/servicepoint/src/cp437.rs @@ -1,11 +1,15 @@ use crate::cp437::Cp437LoadError::InvalidChar; use crate::{Grid, PrimitiveGrid}; +use std::collections::HashMap; /// A grid containing codepage 437 characters. /// /// The encoding is currently not enforced. pub type Cp437Grid = PrimitiveGrid; +/// A grid containing UTF-8 characters. +pub type CharGrid = PrimitiveGrid; + #[derive(Debug)] pub enum Cp437LoadError { InvalidChar { index: usize, char: char }, @@ -72,6 +76,109 @@ impl Cp437Grid { } } +#[allow(unused)] // depends on features +pub use feature_cp437::*; + +#[cfg(feature = "cp437")] +mod feature_cp437 { + use super::*; + + /// An array of 256 elements, mapping most of the CP437 values to UTF-8 characters + /// + /// Mostly follows CP437, except for: + /// * 0x0A & 0x0D are kept for use as line endings. + /// * 0x1A is used for SAUCE. + /// * 0x1B is used for ANSI escape sequences. + /// + /// These exclusions should be fine since most programs can't even use them + /// without issues. And this makes rendering simpler too. + /// + /// See + /// + /// Copied from https://github.com/kip93/cp437-tools. License: GPL-3.0 + #[rustfmt::skip] + const CP437_TO_UTF8: [char; 256] = [ + /* 0X */ '\0', '☺', '☻', '♥', '♦', '♣', '♠', '•', '◘', '○', '\n', '♂', '♀', '\r', '♫', '☼', + /* 1X */ '►', '◄', '↕', '‼', '¶', '§', '▬', '↨', '↑', '↓', '', '', '∟', '↔', '▲', '▼', + /* 2X */ ' ', '!', '"', '#', '$', '%', '&', '\'','(', ')', '*', '+', ',', '-', '.', '/', + /* 3X */ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', + /* 4X */ '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + /* 5X */ 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\',']', '^', '_', + /* 6X */ '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + /* 7X */ 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '⌂', + /* 8X */ 'Ç', 'ü', 'é', 'â', 'ä', 'à', 'å', 'ç', 'ê', 'ë', 'è', 'ï', 'î', 'ì', 'Ä', 'Å', + /* 9X */ 'É', 'æ', 'Æ', 'ô', 'ö', 'ò', 'û', 'ù', 'ÿ', 'Ö', 'Ü', '¢', '£', '¥', '₧', 'ƒ', + /* AX */ 'á', 'í', 'ó', 'ú', 'ñ', 'Ñ', 'ª', 'º', '¿', '⌐', '¬', '½', '¼', '¡', '«', '»', + /* BX */ '░', '▒', '▓', '│', '┤', '╡', '╢', '╖', '╕', '╣', '║', '╗', '╝', '╜', '╛', '┐', + /* CX */ '└', '┴', '┬', '├', '─', '┼', '╞', '╟', '╚', '╔', '╩', '╦', '╠', '═', '╬', '╧', + /* DX */ '╨', '╤', '╥', '╙', '╘', '╒', '╓', '╫', '╪', '┘', '┌', '█', '▄', '▌', '▐', '▀', + /* EX */ 'α', 'ß', 'Γ', 'π', 'Σ', 'σ', 'µ', 'τ', 'Φ', 'Θ', 'Ω', 'δ', '∞', 'φ', 'ε', '∩', + /* FX */ '≡', '±', '≥', '≤', '⌠', '⌡', '÷', '≈', '°', '∙', '·', '√', 'ⁿ', '²', '■', ' ', + ]; + + const UTF8_TO_CP437: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + let pairs = CP437_TO_UTF8 + .iter() + .enumerate() + .map(move |(index, char)| (*char, index as u8)); + HashMap::from_iter(pairs) + }); + + const MISSING_CHAR_CP437: u8 = 0x3F; + + impl From<&Cp437Grid> for CharGrid { + fn from(value: &Cp437Grid) -> Self { + let mut grid = Self::new(value.width(), value.height()); + + for y in 0..grid.height() { + for x in 0..grid.width() { + let converted = CP437_TO_UTF8[value.get(x, y) as usize]; + grid.set(x, y, converted); + } + } + + grid + } + } + + impl From<&CharGrid> for Cp437Grid { + fn from(value: &CharGrid) -> Self { + let mut grid = Self::new(value.width(), value.height()); + + for y in 0..grid.height() { + for x in 0..grid.width() { + let char = value.get(x, y); + let converted = *UTF8_TO_CP437 + .get(&char) + .unwrap_or(&MISSING_CHAR_CP437); + grid.set(x, y, converted); + } + } + + grid + } + } + + impl From<&str> for CharGrid { + fn from(value: &str) -> Self { + let value = value.replace("\r\n", "\n"); + let lines = value.split('\n').collect::>(); + let width = + lines.iter().fold(0, move |a, x| std::cmp::max(a, x.len())); + + let mut grid = Self::new(width, lines.len()); + for (y, line) in lines.iter().enumerate() { + for (x, char) in line.chars().enumerate() { + grid.set(x, y, char); + } + } + + grid + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -98,3 +205,17 @@ mod tests { assert_eq!(actual, expected); } } + +#[cfg(test)] +#[cfg(feature = "cp437")] +mod tests_feature_cp437 { + use crate::{CharGrid, Cp437Grid}; + + #[test] + fn round_trip_cp437() { + let utf8 = CharGrid::load(2, 2, &['Ä', 'x', '\n', '$']); + let cp437 = Cp437Grid::from(&utf8); + let actual = CharGrid::from(&cp437); + assert_eq!(actual, utf8); + } +} diff --git a/crates/servicepoint/src/lib.rs b/crates/servicepoint/src/lib.rs index 4ed440f..fa5564e 100644 --- a/crates/servicepoint/src/lib.rs +++ b/crates/servicepoint/src/lib.rs @@ -44,7 +44,7 @@ pub use crate::brightness::{Brightness, BrightnessGrid}; pub use crate::command::{Command, Offset}; pub use crate::compression_code::CompressionCode; pub use crate::connection::Connection; -pub use crate::cp437::Cp437Grid; +pub use crate::cp437::{CharGrid, Cp437Grid}; pub use crate::data_ref::DataRef; pub use crate::grid::Grid; pub use crate::origin::{Origin, Pixels, Tiles}; diff --git a/crates/servicepoint/src/origin.rs b/crates/servicepoint/src/origin.rs index 16ba083..6c0f5d2 100644 --- a/crates/servicepoint/src/origin.rs +++ b/crates/servicepoint/src/origin.rs @@ -12,6 +12,13 @@ pub struct Origin { } impl Origin { + /// Top-left. Equivalent to `Origin::new(0, 0)`. + pub const ZERO: Self = Self { + x: 0, + y: 0, + phantom_data: PhantomData, + }; + /// Create a new [Origin] instance for the provided position. pub fn new(x: usize, y: usize) -> Self { Self {