Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_tui/src/lib_tui/term/charset.rs

6.9 KiB, 1 run

created by r1870400018:20827, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! The character sets a designation escape puts in front of the printable ASCII range.
2//!
3//! An application that draws a box does not send `┌`. It sends `ESC ( 0`, then `l`, and expects the
4//! terminal to understand that `l` now means the top left corner. This is the DEC special graphics
5//! set, and it is how every curses programme has drawn a line since the VT100; `ncurses` emits it
6//! for `ACS_ULCORNER` whatever the locale, so a terminal that ignores the designation shows
7//! `lqqqqk` where the top of a dialogue box belongs.
8//!
9//! Four slots, G0 to G3, each hold a designated set, and one of them at a time is mapped over the
10//! printable ASCII range. `SI` maps G0 and `SO` maps G1, which is the pair a curses programme
11//! actually uses. The designation escapes are `ESC (` for G0, `ESC )` for G1, `ESC *` for G2 and
12//! `ESC +` for G3, each followed by a byte naming the set: `0` for the special graphics and `B` for
13//! ASCII.
14//!
15//! ## Where the table came from
16//!
17//! Every mapping below was read out of tmux 3.6. The sequence `ESC ( 0` followed by every byte from
18//! 0x20 to 0x7E was fed to a tmux pane whose output went to a pseudoterminal, and the UTF-8 tmux
19//! wrote to that pseudoterminal was decoded character by character. Thirty six of the ninety five
20//! came back changed; those thirty six are the table, and the other fifty nine are why [`Charset::map`]
21//! returns its argument unchanged by default.
22//!
23//! Two of tmux's answers are worth stating because a reader may expect otherwise. `_` is *not*
24//! mapped: the VT100 manual calls position 5/15 a blank and xterm draws a space there, but tmux
25//! leaves the underscore alone, and the oracle is what is followed here. And `ESC ( A`, the United
26//! Kingdom set in which `#` becomes `£`, is not implemented by tmux at all, so it is treated here as
27//! ASCII rather than guessed at.
28
29/// One of the character sets an escape sequence can designate into G0 to G3.
30///
31/// The set is deliberately narrow. A designation naming anything else is accepted and treated as
32/// ASCII, which is what leaves an unrecognised set harmless: the bytes are printed as they arrived
33/// rather than being dropped or substituted for something invented here.
34#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
35pub enum Charset {
36 /// Plain ASCII, designated by `B` and by every byte this model does not know.
37 #[default]
38 Ascii,
39 /// The DEC special graphics and line drawing set, designated by `0`.
40 DecSpecial,
41}
42
43impl Charset {
44
45 /// The set a designation byte names.
46 ///
47 /// The byte is the one after `ESC (`, `ESC )`, `ESC *` or `ESC +`.
48 pub fn from_designator(b: u8) -> Self {
49 match b {
50 b'0' => Self::DecSpecial,
51 _ => Self::Ascii,
52 }
53 }
54
55 /// Whether this set changes anything, which lets a caller skip the mapping altogether.
56 pub fn is_ascii(&self) -> bool {
57 matches!(self, Self::Ascii)
58 }
59
60 /// The character `c` stands for in this set.
61 ///
62 /// Only the printable ASCII range is mapped. A character that arrived as UTF-8 is outside the
63 /// range a designation covers and passes through untouched, which is what tmux does and what
64 /// keeps a programme that mixes the two from losing its accented letters.
65 pub fn map(&self, c: char) -> char {
66 match self {
67 Self::Ascii => c,
68 Self::DecSpecial => dec_special(c),
69 }
70 }
71}
72
73/// The DEC special graphics character `c` stands for, or `c` itself where the set agrees with ASCII.
74///
75/// Read out of tmux 3.6; see the module documentation for the method.
76fn dec_special(c: char) -> char {
77 match c {
78 '+' => '\u{2192}', // → rightwards arrow
79 ',' => '\u{2190}', // ← leftwards arrow
80 '-' => '\u{2191}', // ↑ upwards arrow
81 '.' => '\u{2193}', // ↓ downwards arrow
82 '0' => '\u{25AE}', // ▮ black vertical rectangle
83 '`' => '\u{25C6}', // ◆ black diamond
84 'a' => '\u{2592}', // ▒ medium shade
85 'b' => '\u{2409}', // ␉ symbol for horizontal tabulation
86 'c' => '\u{240C}', // ␌ symbol for form feed
87 'd' => '\u{240D}', // ␍ symbol for carriage return
88 'e' => '\u{240A}', // ␊ symbol for line feed
89 'f' => '\u{00B0}', // ° degree sign
90 'g' => '\u{00B1}', // ± plus minus sign
91 'h' => '\u{2424}', // ␤ symbol for newline
92 'i' => '\u{240B}', // ␋ symbol for vertical tabulation
93 'j' => '\u{2518}', // ┘ box drawings light up and left
94 'k' => '\u{2510}', // ┐ box drawings light down and left
95 'l' => '\u{250C}', // ┌ box drawings light down and right
96 'm' => '\u{2514}', // └ box drawings light up and right
97 'n' => '\u{253C}', // ┼ box drawings light vertical and horizontal
98 'o' => '\u{23BA}', // ⎺ horizontal scan line 1
99 'p' => '\u{23BB}', // ⎻ horizontal scan line 3
100 'q' => '\u{2500}', // ─ box drawings light horizontal
101 'r' => '\u{23BC}', // ⎼ horizontal scan line 7
102 's' => '\u{23BD}', // ⎽ horizontal scan line 9
103 't' => '\u{251C}', // ├ box drawings light vertical and right
104 'u' => '\u{2524}', // ┤ box drawings light vertical and left
105 'v' => '\u{2534}', // ┴ box drawings light up and horizontal
106 'w' => '\u{252C}', // ┬ box drawings light down and horizontal
107 'x' => '\u{2502}', // │ box drawings light vertical
108 'y' => '\u{2264}', // ≤ less than or equal to
109 'z' => '\u{2265}', // ≥ greater than or equal to
110 '{' => '\u{03C0}', // π greek small letter pi
111 '|' => '\u{2260}', // ≠ not equal to
112 '}' => '\u{00A3}', // £ pound sign
113 '~' => '\u{00B7}', // · middle dot
114 other => other,
115 }
116}
117
118/// The four designated sets and which of them is mapped over the printable ASCII range.
119///
120/// A terminal holds one of these. `SI` and `SO` move [`Charsets::shift`]; the designation escapes
121/// replace one of the four sets.
122#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
123pub struct Charsets {
124 /// G0 to G3.
125 sets: [Charset; 4],
126 /// Which of the four is mapped over the printable range: zero after `SI`, one after `SO`.
127 shift: usize,
128}
129
130impl Charsets {
131
132 /// The state a reset leaves behind: ASCII throughout, with G0 in front.
133 pub fn new() -> Self {
134 Self::default()
135 }
136
137 /// Puts `set` into slot `g`, which must be zero to three.
138 pub fn designate(&mut self, g: usize, set: Charset) {
139 if let Some(slot) = self.sets.get_mut(g) {
140 *slot = set;
141 }
142 }
143
144 /// The set in slot `g`, or ASCII if `g` is not a slot.
145 pub fn designated(&self, g: usize) -> Charset {
146 self.sets.get(g).copied().unwrap_or(Charset::Ascii)
147 }
148
149 /// Maps slot `g` over the printable ASCII range, which is what `SI` and `SO` do.
150 pub fn shift_to(&mut self, g: usize) {
151 if g < self.sets.len() {
152 self.shift = g;
153 }
154 }
155
156 /// Which slot is mapped over the printable ASCII range.
157 pub fn shift(&self) -> usize {
158 self.shift
159 }
160
161 /// The set currently in front.
162 pub fn active(&self) -> Charset {
163 self.designated(self.shift)
164 }
165
166 /// The character `c` stands for under the set currently in front.
167 pub fn map(&self, c: char) -> char {
168 self.active().map(c)
169 }
170
171 /// Returns every slot to ASCII and puts G0 in front.
172 pub fn reset(&mut self) {
173 *self = Self::default();
174 }
175}