Skip to content

Commit 49669a9

Browse files
committed
initial implementation
1 parent 435e703 commit 49669a9

File tree

5 files changed

+334
-1
lines changed

5 files changed

+334
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
title: 'VT320 Soft Character (DRCS)'
3+
language: 'en'
4+
---
5+
6+
# VT320 Soft Character (DRCS)
7+
8+
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.
9+
10+
## Overview
11+
12+
The VT320 terminal supported downloadable character sets that allowed applications to define custom glyphs within the terminal's character grid. This implementation provides:
13+
14+
1. A parser for OSC (Operating System Command) sequences used to define and select soft characters.
15+
2. A handler trait for terminal implementations to handle soft character operations.
16+
3. Storage for multiple DRCS character sets
17+
4. Utility functions for converting between DRCS bitmaps and text representations
18+
19+
## OSC Commands
20+
21+
This implementation supports the following OSC commands:
22+
23+
- **OSC 53** - Define a soft character:
24+
25+
```bash
26+
OSC 53 ; char_code ; width ; height ; base64_data ST
27+
```
28+
29+
- **OSC 54** - Select a DRCS character set:
30+
31+
```bash
32+
OSC 54 ; set_id ST
33+
```
34+
35+
- **OSC 153** - Reset all soft characters:
36+
37+
```bash
38+
OSC 153 ST
39+
```
40+
41+
## Usage Example
42+
43+
Here's how to use the DRCS implementation in your terminal:
44+
45+
```rust
46+
// Define a custom smiley face character as character code 65 ('A') with 8x8 dimensions
47+
let smiley_data = vec![0x3C, 0x42, 0xA5, 0x81, 0x99, 0xA5, 0x42, 0x3C];
48+
terminal.define_soft_character(65, 8, 8, smiley_data);
49+
50+
// Select DRCS set 1
51+
terminal.select_drcs_set(1);
52+
53+
// Display the character
54+
if terminal.is_drcs_character(65) {
55+
// Render the character at the current cursor position
56+
terminal.render_drcs_character(65);
57+
}
58+
```
59+
60+
## Integration with Terminal Emulator
61+
62+
To integrate this DRCS implementation into your terminal emulator:
63+
64+
1. Import the `drcs` module
65+
2. Implement the `DrcsHandler` trait for your terminal state
66+
3. Add handling for OSC 53, OSC 54, and OSC 153 in your OSC dispatch function
67+
68+
## Character Bitmap Format
69+
70+
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.
71+
72+
For example, an 8x8 character would require 8 bytes of data (one byte per row).
73+
74+
## Resources
75+
76+
- [VT320 Terminal Reference Manual](https://vt100.net/dec/vt320/soft_characters)
77+
- [DRCS Technical Documentation](https://vt100.net/docs/vt320-uu/chapter4.html#S4.10.5)

rio-backend/src/ansi/drcs.rs

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/// DRCS (Dynamically Redefinable Character Set) support for VT320 terminal
2+
/// Implements handling for the soft character set functionality
3+
/// Based on https://vt100.net/dec/vt320/soft_characters
4+
5+
use rustc_hash::FxHashMap;
6+
use std::str;
7+
use base64::engine::general_purpose::STANDARD as Base64;
8+
use base64::Engine;
9+
10+
#[derive(Debug)]
11+
pub struct DrcsCharacter {
12+
pub data: Vec<u8>,
13+
pub width: u8,
14+
pub height: u8,
15+
}
16+
17+
#[derive(Default, Debug)]
18+
pub struct DrcsSet {
19+
characters: FxHashMap<u8, DrcsCharacter>,
20+
// Current active DRCS set ID
21+
active_set: u8,
22+
}
23+
24+
impl DrcsSet {
25+
pub fn new() -> Self {
26+
Self {
27+
characters: FxHashMap::default(),
28+
active_set: 0,
29+
}
30+
}
31+
pub fn define_character(&mut self, char_code: u8, width: u8, height: u8, data: Vec<u8>) {
32+
self.characters.insert(
33+
char_code,
34+
DrcsCharacter {
35+
data,
36+
width,
37+
height,
38+
},
39+
);
40+
}
41+
pub fn set_active_set(&mut self, set_id: u8) {
42+
// Set the active DRCS character set
43+
self.active_set = set_id;
44+
}
45+
pub fn get_character(&self, char_code: u8) -> Option<&DrcsCharacter> {
46+
self.characters.get(&char_code)
47+
}
48+
pub fn clear(&mut self) {
49+
self.characters.clear();
50+
}
51+
}
52+
53+
/// Parse OSC parameters for DRCS soft character definition
54+
pub fn parse_drcs(params: &[&[u8]]) -> Option<(u8, u8, u8, Vec<u8>)> {
55+
// Format: OSC 53 ; char_code ; width ; height ; base64_data ST
56+
if params.len() < 5 {
57+
return None;
58+
}
59+
60+
// Parse character code
61+
let char_code = str::from_utf8(params[1])
62+
.ok()?
63+
.parse::<u8>()
64+
.ok()?;
65+
66+
// Parse width and height
67+
let width = str::from_utf8(params[2])
68+
.ok()?
69+
.parse::<u8>()
70+
.ok()?;
71+
72+
let height = str::from_utf8(params[3])
73+
.ok()?
74+
.parse::<u8>()
75+
.ok()?;
76+
77+
// Parse base64 data
78+
let bitmap_data = Base64.decode(params[4]).ok()?;
79+
80+
// Verify the bitmap data has the expected size
81+
let expected_size = ((width as usize) * (height as usize) + 7) / 8; // ceil(width * height / 8)
82+
if bitmap_data.len() != expected_size {
83+
return None;
84+
}
85+
86+
Some((char_code, width, height, bitmap_data))
87+
}
88+
89+
/// Parse OSC parameters for selecting a DRCS set
90+
pub fn parse_drcs_select(params: &[&[u8]]) -> Option<u8> {
91+
// Format: OSC 54 ; set_id ST
92+
if params.len() < 2 {
93+
return None;
94+
}
95+
96+
// Parse set ID
97+
str::from_utf8(params[1])
98+
.ok()?
99+
.parse::<u8>()
100+
.ok()
101+
}
102+
103+
104+
// Convert a DRCS bitmap to a displayable format as string
105+
// Rio case need to be sugarloaf
106+
pub fn drcs_to_string(data: &[u8], width: u8, height: u8) -> String {
107+
let mut result = String::new();
108+
109+
for y in 0..height {
110+
for x in 0..width {
111+
let byte_index = (y as usize * width as usize + x as usize) / 8;
112+
let bit_index = 7 - ((y as usize * width as usize + x as usize) % 8);
113+
114+
if byte_index < data.len() {
115+
let pixel = (data[byte_index] >> bit_index) & 1;
116+
result.push(if pixel == 1 { '█' } else { ' ' });
117+
} else {
118+
result.push('?');
119+
}
120+
}
121+
result.push('\n');
122+
}
123+
124+
result
125+
}
126+
127+
/// Create a DRCS bitmap from a text representation
128+
pub fn string_to_drcs(text: &str, width: u8, height: u8) -> Vec<u8> {
129+
let mut data = vec![0u8; ((width as usize * height as usize) + 7) / 8];
130+
131+
for (i, c) in text.chars().enumerate() {
132+
if i >= width as usize * height as usize {
133+
break;
134+
}
135+
136+
let y = i / width as usize;
137+
let x = i % width as usize;
138+
139+
if c != ' ' {
140+
let byte_index = (y * width as usize + x) / 8;
141+
let bit_index = 7 - ((y * width as usize + x) % 8);
142+
143+
data[byte_index] |= 1 << bit_index;
144+
}
145+
}
146+
147+
data
148+
}
149+
150+
pub fn test() {
151+
// Define a simple character (a smiley face)
152+
let _smiley_data = string_to_drcs(
153+
" #### \
154+
# #\
155+
# # # #\
156+
# #\
157+
# ## #\
158+
# # # #\
159+
# #\
160+
# #\
161+
#### ",
162+
8, 8
163+
);
164+
165+
// Define the smiley face as character code 65 ('A')
166+
// terminal.define_soft_character(65, 8, 8, smiley_data);
167+
168+
// if terminal.is_drcs_character(65) {
169+
// if let Some(bitmap) = terminal.render_drcs_character(65) {
170+
// let visual = utils::drcs_to_string(&bitmap, 8, 8);
171+
// println!("DRCS character 65:\n{}", visual);
172+
// }
173+
}

rio-backend/src/ansi/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
44
pub mod charset;
55
pub mod control;
66
pub mod graphics;
7+
pub mod drcs;
78
pub mod iterm2_image_protocol;
89
pub mod mode;
910
pub mod sixel;

rio-backend/src/crosswords/mod.rs

+48
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ pub mod search;
2121
pub mod square;
2222
pub mod vi_mode;
2323

24+
use crate::ansi::drcs::DrcsSet;
25+
use rustc_hash::FxHashMap;
2426
use crate::ansi::graphics::GraphicCell;
2527
use crate::ansi::graphics::Graphics;
2628
use crate::ansi::graphics::TextureRef;
@@ -456,6 +458,12 @@ where
456458

457459
// Currently inactive keyboard mode stack.
458460
inactive_keyboard_mode_stack: Vec<KeyboardModes>,
461+
462+
/// The DRCS character sets
463+
drcs_sets: FxHashMap<u8, DrcsSet>,
464+
465+
/// The currently active DRCS set
466+
active_drcs_set: u8,
459467
}
460468

461469
impl<U: EventListener> Crosswords<U> {
@@ -506,6 +514,8 @@ impl<U: EventListener> Crosswords<U> {
506514
current_directory: None,
507515
keyboard_mode_stack: Default::default(),
508516
inactive_keyboard_mode_stack: Default::default(),
517+
drcs_sets: FxHashMap::default(),
518+
active_drcs_set: 0,
509519
}
510520
}
511521

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

13511361
point
13521362
}
1363+
1364+
fn get_or_create_drcs_set(&mut self, set_id: u8) -> &mut DrcsSet {
1365+
self.drcs_sets.entry(set_id).or_insert_with(DrcsSet::new)
1366+
}
1367+
1368+
fn active_drcs_set(&mut self) -> &mut DrcsSet {
1369+
self.get_or_create_drcs_set(self.active_drcs_set)
1370+
}
1371+
1372+
pub fn is_drcs_character(&self, char_code: u8) -> bool {
1373+
self.drcs_sets
1374+
.get(&self.active_drcs_set)
1375+
.and_then(|set| set.get_character(char_code))
1376+
.is_some()
1377+
}
1378+
1379+
/// Render a DRCS character to a bitmap
1380+
pub fn render_drcs_character(&self, char_code: u8) -> Option<Vec<u8>> {
1381+
self.drcs_sets
1382+
.get(&self.active_drcs_set)
1383+
.and_then(|set| set.get_character(char_code))
1384+
.map(|character| character.data.clone())
1385+
}
13531386
}
13541387

13551388
impl<U: EventListener> Handler for Crosswords<U> {
@@ -1712,6 +1745,8 @@ impl<U: EventListener> Handler for Crosswords<U> {
17121745
.damage_line(line, self.grid.cursor.pos.col.0, old_col);
17131746
}
17141747

1748+
1749+
17151750
#[inline]
17161751
fn goto_line(&mut self, line: Line) {
17171752
self.goto(line, self.grid.cursor.pos.col)
@@ -2612,6 +2647,19 @@ impl<U: EventListener> Handler for Crosswords<U> {
26122647
.send_event(RioEvent::PtyWrite(text), self.window_id);
26132648
}
26142649

2650+
fn define_soft_character(&mut self, char_code: u8, width: u8, height: u8, data: Vec<u8>) {
2651+
let drcs_set = self.active_drcs_set();
2652+
drcs_set.define_character(char_code, width, height, data);
2653+
}
2654+
2655+
fn select_drcs_set(&mut self, set_id: u8) {
2656+
self.active_drcs_set = set_id;
2657+
}
2658+
2659+
fn reset_soft_characters(&mut self) {
2660+
self.drcs_sets.clear();
2661+
}
2662+
26152663
#[inline]
26162664
fn graphics_attribute(&mut self, pi: u16, pa: u16) {
26172665
// From Xterm documentation:

0 commit comments

Comments
 (0)