diff --git a/docs/docs/features/soft-character-protocol.md b/docs/docs/features/soft-character-protocol.md new file mode 100644 index 0000000000..ac1f147f8d --- /dev/null +++ b/docs/docs/features/soft-character-protocol.md @@ -0,0 +1,41 @@ +--- +title: 'VT320 Soft Character (DRCS)' +language: 'en' +--- + +# VT320 Soft Character (DRCS) + +Rio implements VT320 terminal's Dynamically Redefinable Character Set (DRCS) functionality, also known as "soft characters". The implementation allows terminals to receive, store, and display custom character glyphs defined by the application. + +## OSC Commands + +This implementation supports the following OSC commands: + +- **OSC 53** - Define a soft character: + +```bash +OSC 53 ; char_code ; width ; height ; base64_data ST +``` + +- **OSC 54** - Select a DRCS character set: + +```bash +OSC 54 ; set_id ST +``` + +- **OSC 153** - Reset all soft characters: + +```bash +OSC 153 ST +``` + +## Character Bitmap Format + +The DRCS characters are stored as 1-bit-per-pixel bitmaps, packed into bytes. The bitmap data is organized row by row, with each row padded to a byte boundary. The bits are ordered from most significant to least significant within each byte. + +For example, an 8x8 character would require 8 bytes of data (one byte per row). + +## Resources + +- [VT320 Terminal Reference Manual](https://vt100.net/dec/vt320/soft_characters) +- [DRCS Technical Documentation](https://vt100.net/docs/vt320-uu/chapter4.html#S4.10.5) diff --git a/rio-backend/src/ansi/drcs.rs b/rio-backend/src/ansi/drcs.rs new file mode 100644 index 0000000000..6f8c86ba86 --- /dev/null +++ b/rio-backend/src/ansi/drcs.rs @@ -0,0 +1,173 @@ +/// DRCS (Dynamically Redefinable Character Set) support for VT320 terminal +/// Implements handling for the soft character set functionality +/// Based on https://vt100.net/dec/vt320/soft_characters + +use rustc_hash::FxHashMap; +use std::str; +use base64::engine::general_purpose::STANDARD as Base64; +use base64::Engine; + +#[derive(Debug)] +pub struct DrcsCharacter { + pub data: Vec, + pub width: u8, + pub height: u8, +} + +#[derive(Default, Debug)] +pub struct DrcsSet { + characters: FxHashMap, + // Current active DRCS set ID + active_set: u8, +} + +impl DrcsSet { + pub fn new() -> Self { + Self { + characters: FxHashMap::default(), + active_set: 0, + } + } + pub fn define_character(&mut self, char_code: u8, width: u8, height: u8, data: Vec) { + self.characters.insert( + char_code, + DrcsCharacter { + data, + width, + height, + }, + ); + } + pub fn set_active_set(&mut self, set_id: u8) { + // Set the active DRCS character set + self.active_set = set_id; + } + pub fn get_character(&self, char_code: u8) -> Option<&DrcsCharacter> { + self.characters.get(&char_code) + } + pub fn clear(&mut self) { + self.characters.clear(); + } +} + +/// Parse OSC parameters for DRCS soft character definition +pub fn parse_drcs(params: &[&[u8]]) -> Option<(u8, u8, u8, Vec)> { + // Format: OSC 53 ; char_code ; width ; height ; base64_data ST + if params.len() < 5 { + return None; + } + + // Parse character code + let char_code = str::from_utf8(params[1]) + .ok()? + .parse::() + .ok()?; + + // Parse width and height + let width = str::from_utf8(params[2]) + .ok()? + .parse::() + .ok()?; + + let height = str::from_utf8(params[3]) + .ok()? + .parse::() + .ok()?; + + // Parse base64 data + let bitmap_data = Base64.decode(params[4]).ok()?; + + // Verify the bitmap data has the expected size + let expected_size = ((width as usize) * (height as usize) + 7) / 8; // ceil(width * height / 8) + if bitmap_data.len() != expected_size { + return None; + } + + Some((char_code, width, height, bitmap_data)) +} + +/// Parse OSC parameters for selecting a DRCS set +pub fn parse_drcs_select(params: &[&[u8]]) -> Option { + // Format: OSC 54 ; set_id ST + if params.len() < 2 { + return None; + } + + // Parse set ID + str::from_utf8(params[1]) + .ok()? + .parse::() + .ok() +} + + +// Convert a DRCS bitmap to a displayable format as string +// Rio case need to be sugarloaf +pub fn drcs_to_string(data: &[u8], width: u8, height: u8) -> String { + let mut result = String::new(); + + for y in 0..height { + for x in 0..width { + let byte_index = (y as usize * width as usize + x as usize) / 8; + let bit_index = 7 - ((y as usize * width as usize + x as usize) % 8); + + if byte_index < data.len() { + let pixel = (data[byte_index] >> bit_index) & 1; + result.push(if pixel == 1 { '█' } else { ' ' }); + } else { + result.push('?'); + } + } + result.push('\n'); + } + + result +} + +/// Create a DRCS bitmap from a text representation +pub fn string_to_drcs(text: &str, width: u8, height: u8) -> Vec { + let mut data = vec![0u8; ((width as usize * height as usize) + 7) / 8]; + + for (i, c) in text.chars().enumerate() { + if i >= width as usize * height as usize { + break; + } + + let y = i / width as usize; + let x = i % width as usize; + + if c != ' ' { + let byte_index = (y * width as usize + x) / 8; + let bit_index = 7 - ((y * width as usize + x) % 8); + + data[byte_index] |= 1 << bit_index; + } + } + + data +} + +pub fn test() { + // Define a simple character (a smiley face) + let _smiley_data = string_to_drcs( + " #### \ + # #\ + # # # #\ + # #\ + # ## #\ + # # # #\ + # #\ + # #\ + #### ", + 8, 8 + ); + + // Define the smiley face as character code 65 ('A') + // terminal.define_soft_character(65, 8, 8, smiley_data); + + // if terminal.is_drcs_character(65) { + // if let Some(bitmap) = terminal.render_drcs_character(65) { + // let visual = utils::drcs_to_string(&bitmap, 8, 8); + // println!("DRCS character 65:\n{}", visual); + // } +} diff --git a/rio-backend/src/ansi/mod.rs b/rio-backend/src/ansi/mod.rs index 4fbf45dc90..cfe1b776f8 100644 --- a/rio-backend/src/ansi/mod.rs +++ b/rio-backend/src/ansi/mod.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; pub mod charset; pub mod control; pub mod graphics; +pub mod drcs; pub mod iterm2_image_protocol; pub mod mode; pub mod sixel; diff --git a/rio-backend/src/crosswords/mod.rs b/rio-backend/src/crosswords/mod.rs index de6b996266..4d1265c5a3 100644 --- a/rio-backend/src/crosswords/mod.rs +++ b/rio-backend/src/crosswords/mod.rs @@ -21,6 +21,8 @@ pub mod search; pub mod square; pub mod vi_mode; +use crate::ansi::drcs::DrcsSet; +use rustc_hash::FxHashMap; use crate::ansi::graphics::GraphicCell; use crate::ansi::graphics::Graphics; use crate::ansi::graphics::TextureRef; @@ -456,6 +458,12 @@ where // Currently inactive keyboard mode stack. inactive_keyboard_mode_stack: Vec, + + /// The DRCS character sets + drcs_sets: FxHashMap, + + /// The currently active DRCS set + active_drcs_set: u8, } impl Crosswords { @@ -506,6 +514,8 @@ impl Crosswords { current_directory: None, keyboard_mode_stack: Default::default(), inactive_keyboard_mode_stack: Default::default(), + drcs_sets: FxHashMap::default(), + active_drcs_set: 0, } } @@ -1350,6 +1360,29 @@ impl Crosswords { point } + + fn get_or_create_drcs_set(&mut self, set_id: u8) -> &mut DrcsSet { + self.drcs_sets.entry(set_id).or_insert_with(DrcsSet::new) + } + + fn active_drcs_set(&mut self) -> &mut DrcsSet { + self.get_or_create_drcs_set(self.active_drcs_set) + } + + pub fn is_drcs_character(&self, char_code: u8) -> bool { + self.drcs_sets + .get(&self.active_drcs_set) + .and_then(|set| set.get_character(char_code)) + .is_some() + } + + /// Render a DRCS character to a bitmap + pub fn render_drcs_character(&self, char_code: u8) -> Option> { + self.drcs_sets + .get(&self.active_drcs_set) + .and_then(|set| set.get_character(char_code)) + .map(|character| character.data.clone()) + } } impl Handler for Crosswords { @@ -1712,6 +1745,8 @@ impl Handler for Crosswords { .damage_line(line, self.grid.cursor.pos.col.0, old_col); } + + #[inline] fn goto_line(&mut self, line: Line) { self.goto(line, self.grid.cursor.pos.col) @@ -2612,6 +2647,19 @@ impl Handler for Crosswords { .send_event(RioEvent::PtyWrite(text), self.window_id); } + fn define_soft_character(&mut self, char_code: u8, width: u8, height: u8, data: Vec) { + let drcs_set = self.active_drcs_set(); + drcs_set.define_character(char_code, width, height, data); + } + + fn select_drcs_set(&mut self, set_id: u8) { + self.active_drcs_set = set_id; + } + + fn reset_soft_characters(&mut self) { + self.drcs_sets.clear(); + } + #[inline] fn graphics_attribute(&mut self, pi: u16, pa: u16) { // From Xterm documentation: diff --git a/rio-backend/src/performer/handler.rs b/rio-backend/src/performer/handler.rs index 81f80e82db..877279576a 100644 --- a/rio-backend/src/performer/handler.rs +++ b/rio-backend/src/performer/handler.rs @@ -1,4 +1,4 @@ -use crate::ansi::iterm2_image_protocol; +use crate::ansi::{drcs, iterm2_image_protocol}; use crate::ansi::CursorShape; use crate::ansi::{sixel, KeyboardModes, KeyboardModesApplyBehavior}; use crate::config::colors::{AnsiColor, ColorRgb, NamedColor}; @@ -388,6 +388,15 @@ pub trait Handler { _behavior: KeyboardModesApplyBehavior, ) { } + + /// Define a new soft character + fn define_soft_character(&mut self, char_code: u8, width: u8, height: u8, data: Vec); + + /// Select a specific DRCS character set + fn select_drcs_set(&mut self, set_id: u8); + + /// Reset all soft characters + fn reset_soft_characters(&mut self); } pub trait Timeout: Default { @@ -832,6 +841,26 @@ impl copa::Perform for Performer<'_, U, T> { } } + // Define soft character (DRCS - VT320 Soft Characters) + // OSC 53 ; char_code ; width ; height ; base64_data ST + b"53" => { + if let Some((char_code, width, height, data)) = drcs::parse_drcs(params) { + self.handler.define_soft_character(char_code, width, height, data); + return; + } + unhandled(params); + } + + // Select DRCS character set + // OSC 54 ; set_id ST + b"54" => { + if let Some(set_id) = drcs::parse_drcs_select(params) { + self.handler.select_drcs_set(set_id); + return; + } + unhandled(params); + } + // Set cursor style. b"50" => { if params.len() >= 2 @@ -890,6 +919,11 @@ impl copa::Perform for Performer<'_, U, T> { // Reset text cursor color. b"112" => self.handler.reset_color(NamedColor::Cursor as usize), + // Reset soft characters (DRCS) + b"153" => { + self.handler.reset_soft_characters(); + } + // OSC 1337 is not necessarily only used by iTerm2 protocol // OSC 1337 is equal to xterm OSC 50 b"1337" => {