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)] |
| 35 | pub 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 | |
| 43 | impl 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. |
| 76 | fn 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)] |
| 123 | pub 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 | |
| 130 | impl 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 | } |