mirror of
https://github.com/cccb/servicepoint.git
synced 2025-01-18 10:00:14 +01:00
conversion between UTF-8 and CP-437
This commit is contained in:
parent
c7764c49e1
commit
3d47b41106
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -617,6 +617,7 @@ dependencies = [
|
|||
"clap",
|
||||
"flate2",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"rust-lzma",
|
||||
"tungstenite",
|
||||
|
|
|
@ -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
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<u8>;
|
||||
|
||||
/// A grid containing UTF-8 characters.
|
||||
pub type CharGrid = PrimitiveGrid<char>;
|
||||
|
||||
#[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 <https://en.wikipedia.org/wiki/Code_page_437#Character_set>
|
||||
///
|
||||
/// 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<HashMap<char, u8>> =
|
||||
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::<Vec<_>>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -12,6 +12,13 @@ pub struct Origin<Unit: DisplayUnit> {
|
|||
}
|
||||
|
||||
impl<Unit: DisplayUnit> Origin<Unit> {
|
||||
/// 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 {
|
||||
|
|
Loading…
Reference in a new issue