Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions docs/docs/features/soft-character-protocol.md
Original file line number Diff line number Diff line change
@@ -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)
173 changes: 173 additions & 0 deletions rio-backend/src/ansi/drcs.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
pub width: u8,
pub height: u8,
}

#[derive(Default, Debug)]
pub struct DrcsSet {
characters: FxHashMap<u8, DrcsCharacter>,
// 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<u8>) {
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<u8>)> {
// 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::<u8>()
.ok()?;

// Parse width and height
let width = str::from_utf8(params[2])
.ok()?
.parse::<u8>()
.ok()?;

let height = str::from_utf8(params[3])
.ok()?
.parse::<u8>()
.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<u8> {
// Format: OSC 54 ; set_id ST
if params.len() < 2 {
return None;
}

// Parse set ID
str::from_utf8(params[1])
.ok()?
.parse::<u8>()
.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<u8> {
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);
// }
}
1 change: 1 addition & 0 deletions rio-backend/src/ansi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
48 changes: 48 additions & 0 deletions rio-backend/src/crosswords/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -456,6 +458,12 @@ where

// Currently inactive keyboard mode stack.
inactive_keyboard_mode_stack: Vec<KeyboardModes>,

/// The DRCS character sets
drcs_sets: FxHashMap<u8, DrcsSet>,

/// The currently active DRCS set
active_drcs_set: u8,
}

impl<U: EventListener> Crosswords<U> {
Expand Down Expand Up @@ -506,6 +514,8 @@ impl<U: EventListener> Crosswords<U> {
current_directory: None,
keyboard_mode_stack: Default::default(),
inactive_keyboard_mode_stack: Default::default(),
drcs_sets: FxHashMap::default(),
active_drcs_set: 0,
}
}

Expand Down Expand Up @@ -1350,6 +1360,29 @@ impl<U: EventListener> Crosswords<U> {

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<Vec<u8>> {
self.drcs_sets
.get(&self.active_drcs_set)
.and_then(|set| set.get_character(char_code))
.map(|character| character.data.clone())
}
}

impl<U: EventListener> Handler for Crosswords<U> {
Expand Down Expand Up @@ -1712,6 +1745,8 @@ impl<U: EventListener> Handler for Crosswords<U> {
.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)
Expand Down Expand Up @@ -2612,6 +2647,19 @@ impl<U: EventListener> Handler for Crosswords<U> {
.send_event(RioEvent::PtyWrite(text), self.window_id);
}

fn define_soft_character(&mut self, char_code: u8, width: u8, height: u8, data: Vec<u8>) {
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:
Expand Down
36 changes: 35 additions & 1 deletion rio-backend/src/performer/handler.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<u8>);

/// 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 {
Expand Down Expand Up @@ -832,6 +841,26 @@ impl<U: Handler, T: Timeout> 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
Expand Down Expand Up @@ -890,6 +919,11 @@ impl<U: Handler, T: Timeout> 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" => {
Expand Down
Loading