| 23 |
23 |
|
Rgb(u8, u8, u8),
|
| 24 |
24 |
|
}
|
| 25 |
25 |
|
|
| 26 |
|
- |
/// SGR-set attributes for a cell. Not rendered visually yet; parsed for
|
| 27 |
|
- |
/// completeness and to keep the field shape stable.
|
|
26 |
+ |
/// SGR-set attributes for a cell.
|
| 28 |
27 |
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
| 29 |
28 |
|
pub struct Attrs {
|
| 30 |
29 |
|
pub bold: bool,
|
| 35 |
34 |
|
pub strikethrough: bool,
|
| 36 |
35 |
|
}
|
| 37 |
36 |
|
|
| 38 |
|
- |
/// One cell of the display grid.
|
| 39 |
|
- |
#[derive(Copy, Clone, Debug, PartialEq)]
|
|
37 |
+ |
/// One cell of the display grid — 12 bytes, matching foot's layout.
|
|
38 |
+ |
///
|
|
39 |
+ |
/// Three `u32`s so the struct aligns to 4 (a `u64` would pad to 16). Each
|
|
40 |
+ |
/// color word holds a 24-bit RGB payload, a 2-bit source tag, and a 6-bit
|
|
41 |
+ |
/// attribute half — the attribute bits live in `fg_word`, `bg_word`'s upper
|
|
42 |
+ |
/// 6 bits are spare.
|
|
43 |
+ |
///
|
|
44 |
+ |
/// Layout of `fg_word` / `bg_word`:
|
|
45 |
+ |
/// - bits 0–23: color payload — RGB if `src == Rgb`, low byte = palette index if Named/Indexed
|
|
46 |
+ |
/// - bits 24–25: source tag (`SRC_DEFAULT`/`NAMED`/`INDEXED`/`RGB`)
|
|
47 |
+ |
/// - bits 26–31: attribute flags (fg_word only; bg_word spare)
|
|
48 |
+ |
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
|
49 |
+ |
#[repr(C)]
|
| 40 |
50 |
|
pub struct Cell {
|
| 41 |
|
- |
pub c: char,
|
| 42 |
|
- |
pub fg: Color,
|
| 43 |
|
- |
pub bg: Color,
|
| 44 |
|
- |
pub attrs: Attrs,
|
|
51 |
+ |
c_raw: u32,
|
|
52 |
+ |
fg_word: u32,
|
|
53 |
+ |
bg_word: u32,
|
|
54 |
+ |
}
|
|
55 |
+ |
|
|
56 |
+ |
const SRC_DEFAULT: u32 = 0;
|
|
57 |
+ |
const SRC_NAMED: u32 = 1;
|
|
58 |
+ |
const SRC_INDEXED: u32 = 2;
|
|
59 |
+ |
const SRC_RGB: u32 = 3;
|
|
60 |
+ |
|
|
61 |
+ |
const SRC_SHIFT: u32 = 24;
|
|
62 |
+ |
const SRC_MASK: u32 = 0b11 << SRC_SHIFT;
|
|
63 |
+ |
const RGB_MASK: u32 = 0x00FF_FFFF;
|
|
64 |
+ |
|
|
65 |
+ |
const ATTR_BOLD: u32 = 1 << 26;
|
|
66 |
+ |
const ATTR_ITALIC: u32 = 1 << 27;
|
|
67 |
+ |
const ATTR_UNDERLINE: u32 = 1 << 28;
|
|
68 |
+ |
const ATTR_REVERSE: u32 = 1 << 29;
|
|
69 |
+ |
const ATTR_DIM: u32 = 1 << 30;
|
|
70 |
+ |
const ATTR_STRIKE: u32 = 1 << 31;
|
|
71 |
+ |
|
|
72 |
+ |
fn encode_color(c: Color) -> u32 {
|
|
73 |
+ |
match c {
|
|
74 |
+ |
Color::Default => SRC_DEFAULT << SRC_SHIFT,
|
|
75 |
+ |
Color::Named(i) => (SRC_NAMED << SRC_SHIFT) | i as u32,
|
|
76 |
+ |
Color::Indexed(i) => (SRC_INDEXED << SRC_SHIFT) | i as u32,
|
|
77 |
+ |
Color::Rgb(r, g, b) => {
|
|
78 |
+ |
(SRC_RGB << SRC_SHIFT)
|
|
79 |
+ |
| ((r as u32) << 16)
|
|
80 |
+ |
| ((g as u32) << 8)
|
|
81 |
+ |
| b as u32
|
|
82 |
+ |
}
|
|
83 |
+ |
}
|
|
84 |
+ |
}
|
|
85 |
+ |
|
|
86 |
+ |
fn decode_color(word: u32) -> Color {
|
|
87 |
+ |
let payload = word & RGB_MASK;
|
|
88 |
+ |
match (word & SRC_MASK) >> SRC_SHIFT {
|
|
89 |
+ |
SRC_NAMED => Color::Named(payload as u8),
|
|
90 |
+ |
SRC_INDEXED => Color::Indexed(payload as u8),
|
|
91 |
+ |
SRC_RGB => Color::Rgb(
|
|
92 |
+ |
(payload >> 16) as u8,
|
|
93 |
+ |
(payload >> 8) as u8,
|
|
94 |
+ |
payload as u8,
|
|
95 |
+ |
),
|
|
96 |
+ |
_ => Color::Default,
|
|
97 |
+ |
}
|
|
98 |
+ |
}
|
|
99 |
+ |
|
|
100 |
+ |
fn encode_attrs(a: Attrs) -> u32 {
|
|
101 |
+ |
let mut bits = 0u32;
|
|
102 |
+ |
if a.bold { bits |= ATTR_BOLD; }
|
|
103 |
+ |
if a.italic { bits |= ATTR_ITALIC; }
|
|
104 |
+ |
if a.underline { bits |= ATTR_UNDERLINE; }
|
|
105 |
+ |
if a.reverse { bits |= ATTR_REVERSE; }
|
|
106 |
+ |
if a.dim { bits |= ATTR_DIM; }
|
|
107 |
+ |
if a.strikethrough { bits |= ATTR_STRIKE; }
|
|
108 |
+ |
bits
|
|
109 |
+ |
}
|
|
110 |
+ |
|
|
111 |
+ |
impl Cell {
|
|
112 |
+ |
pub fn new(c: char, fg: Color, bg: Color, attrs: Attrs) -> Self {
|
|
113 |
+ |
Self {
|
|
114 |
+ |
c_raw: c as u32,
|
|
115 |
+ |
fg_word: encode_color(fg) | encode_attrs(attrs),
|
|
116 |
+ |
bg_word: encode_color(bg),
|
|
117 |
+ |
}
|
|
118 |
+ |
}
|
|
119 |
+ |
|
|
120 |
+ |
#[inline]
|
|
121 |
+ |
pub fn c(&self) -> char {
|
|
122 |
+ |
// Grid only ever writes valid `char` values into `c_raw`.
|
|
123 |
+ |
char::from_u32(self.c_raw).unwrap_or('\u{FFFD}')
|
|
124 |
+ |
}
|
|
125 |
+ |
|
|
126 |
+ |
#[inline]
|
|
127 |
+ |
pub fn fg(&self) -> Color {
|
|
128 |
+ |
decode_color(self.fg_word)
|
|
129 |
+ |
}
|
|
130 |
+ |
|
|
131 |
+ |
#[inline]
|
|
132 |
+ |
pub fn bg(&self) -> Color {
|
|
133 |
+ |
decode_color(self.bg_word)
|
|
134 |
+ |
}
|
|
135 |
+ |
|
|
136 |
+ |
pub fn attrs(&self) -> Attrs {
|
|
137 |
+ |
let b = self.fg_word;
|
|
138 |
+ |
Attrs {
|
|
139 |
+ |
bold: b & ATTR_BOLD != 0,
|
|
140 |
+ |
italic: b & ATTR_ITALIC != 0,
|
|
141 |
+ |
underline: b & ATTR_UNDERLINE != 0,
|
|
142 |
+ |
reverse: b & ATTR_REVERSE != 0,
|
|
143 |
+ |
dim: b & ATTR_DIM != 0,
|
|
144 |
+ |
strikethrough: b & ATTR_STRIKE != 0,
|
|
145 |
+ |
}
|
|
146 |
+ |
}
|
|
147 |
+ |
|
|
148 |
+ |
// Hot-path attribute readers — used by the renderer's per-frame fills
|
|
149 |
+ |
// scan, which touches every visible cell. Avoids materializing `Attrs`.
|
|
150 |
+ |
#[inline]
|
|
151 |
+ |
pub fn reverse(&self) -> bool {
|
|
152 |
+ |
self.fg_word & ATTR_REVERSE != 0
|
|
153 |
+ |
}
|
|
154 |
+ |
|
|
155 |
+ |
#[inline]
|
|
156 |
+ |
pub fn underline(&self) -> bool {
|
|
157 |
+ |
self.fg_word & ATTR_UNDERLINE != 0
|
|
158 |
+ |
}
|
|
159 |
+ |
|
|
160 |
+ |
/// Fast "does this cell need a bg fill drawn" check for the fills scan.
|
|
161 |
+ |
/// True when bg is not Default OR reverse is set.
|
|
162 |
+ |
#[inline]
|
|
163 |
+ |
pub fn has_bg(&self) -> bool {
|
|
164 |
+ |
(self.bg_word & SRC_MASK) != 0 || self.reverse()
|
|
165 |
+ |
}
|
| 45 |
166 |
|
}
|
| 46 |
167 |
|
|
| 47 |
168 |
|
impl Default for Cell {
|
| 48 |
169 |
|
fn default() -> Self {
|
| 49 |
170 |
|
Self {
|
| 50 |
|
- |
c: ' ',
|
| 51 |
|
- |
fg: Color::Default,
|
| 52 |
|
- |
bg: Color::Default,
|
| 53 |
|
- |
attrs: Attrs::default(),
|
|
171 |
+ |
c_raw: ' ' as u32,
|
|
172 |
+ |
fg_word: 0,
|
|
173 |
+ |
bg_word: 0,
|
| 54 |
174 |
|
}
|
| 55 |
175 |
|
}
|
| 56 |
176 |
|
}
|
| 57 |
177 |
|
|
|
178 |
+ |
const _: () = assert!(std::mem::size_of::<Cell>() == 12);
|
|
179 |
+ |
|
| 58 |
180 |
|
/// Shape hint from DECSCUSR (`CSI Ps SP q`). Blink flag is ignored — MVP
|
| 59 |
181 |
|
/// renders all as steady.
|
| 60 |
182 |
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
| 393 |
515 |
|
}
|
| 394 |
516 |
|
let row = self.cursor.row;
|
| 395 |
517 |
|
let idx = self.cell_index(row, self.cursor.col);
|
| 396 |
|
- |
self.active_cells_mut()[idx] = Cell {
|
| 397 |
|
- |
c,
|
| 398 |
|
- |
fg: self.pending_fg,
|
| 399 |
|
- |
bg: self.pending_bg,
|
| 400 |
|
- |
attrs: self.pending_attrs,
|
| 401 |
|
- |
};
|
|
518 |
+ |
self.active_cells_mut()[idx] =
|
|
519 |
+ |
Cell::new(c, self.pending_fg, self.pending_bg, self.pending_attrs);
|
| 402 |
520 |
|
self.mark_row_dirty(row);
|
| 403 |
521 |
|
if self.cursor.col + 1 >= self.cols {
|
| 404 |
522 |
|
self.cursor.wrap_next = true;
|
| 1001 |
1119 |
|
fn row_str(grid: &Grid, r: u16) -> String {
|
| 1002 |
1120 |
|
grid.row(r)
|
| 1003 |
1121 |
|
.iter()
|
| 1004 |
|
- |
.map(|c| c.c)
|
|
1122 |
+ |
.map(|cell| cell.c())
|
| 1005 |
1123 |
|
.collect::<String>()
|
| 1006 |
1124 |
|
.trim_end()
|
| 1007 |
1125 |
|
.to_string()
|
| 1154 |
1272 |
|
let mut g = Grid::new(10, 1);
|
| 1155 |
1273 |
|
feed(&mut g, b"\x1b[31;44mA");
|
| 1156 |
1274 |
|
let cell = g.row(0)[0];
|
| 1157 |
|
- |
assert_eq!(cell.fg, Color::Named(1)); // red
|
| 1158 |
|
- |
assert_eq!(cell.bg, Color::Named(4)); // blue
|
|
1275 |
+ |
assert_eq!(cell.fg(), Color::Named(1)); // red
|
|
1276 |
+ |
assert_eq!(cell.bg(), Color::Named(4)); // blue
|
| 1159 |
1277 |
|
}
|
| 1160 |
1278 |
|
|
| 1161 |
1279 |
|
#[test]
|
| 1162 |
1280 |
|
fn sgr_bright_named() {
|
| 1163 |
1281 |
|
let mut g = Grid::new(10, 1);
|
| 1164 |
1282 |
|
feed(&mut g, b"\x1b[92mA");
|
| 1165 |
|
- |
assert_eq!(g.row(0)[0].fg, Color::Named(10)); // bright green = 8+2
|
|
1283 |
+ |
assert_eq!(g.row(0)[0].fg(), Color::Named(10)); // bright green = 8+2
|
| 1166 |
1284 |
|
}
|
| 1167 |
1285 |
|
|
| 1168 |
1286 |
|
#[test]
|
| 1169 |
1287 |
|
fn sgr_indexed_256() {
|
| 1170 |
1288 |
|
let mut g = Grid::new(10, 1);
|
| 1171 |
1289 |
|
feed(&mut g, b"\x1b[38;5;123mA");
|
| 1172 |
|
- |
assert_eq!(g.row(0)[0].fg, Color::Indexed(123));
|
|
1290 |
+ |
assert_eq!(g.row(0)[0].fg(), Color::Indexed(123));
|
| 1173 |
1291 |
|
}
|
| 1174 |
1292 |
|
|
| 1175 |
1293 |
|
#[test]
|
| 1176 |
1294 |
|
fn sgr_truecolor_rgb() {
|
| 1177 |
1295 |
|
let mut g = Grid::new(10, 1);
|
| 1178 |
1296 |
|
feed(&mut g, b"\x1b[38;2;255;128;0mA");
|
| 1179 |
|
- |
assert_eq!(g.row(0)[0].fg, Color::Rgb(255, 128, 0));
|
|
1297 |
+ |
assert_eq!(g.row(0)[0].fg(), Color::Rgb(255, 128, 0));
|
| 1180 |
1298 |
|
}
|
| 1181 |
1299 |
|
|
| 1182 |
1300 |
|
#[test]
|
| 1185 |
1303 |
|
// subparams. Our SGR parser flattens both forms.
|
| 1186 |
1304 |
|
let mut g = Grid::new(10, 1);
|
| 1187 |
1305 |
|
feed(&mut g, b"\x1b[38:2::255:128:0mA");
|
| 1188 |
|
- |
assert_eq!(g.row(0)[0].fg, Color::Rgb(255, 128, 0));
|
|
1306 |
+ |
assert_eq!(g.row(0)[0].fg(), Color::Rgb(255, 128, 0));
|
| 1189 |
1307 |
|
}
|
| 1190 |
1308 |
|
|
| 1191 |
1309 |
|
#[test]
|
| 1192 |
1310 |
|
fn sgr_attrs_bold_italic_underline() {
|
| 1193 |
1311 |
|
let mut g = Grid::new(10, 1);
|
| 1194 |
1312 |
|
feed(&mut g, b"\x1b[1;3;4mA");
|
| 1195 |
|
- |
let a = g.row(0)[0].attrs;
|
|
1313 |
+ |
let a = g.row(0)[0].attrs();
|
| 1196 |
1314 |
|
assert!(a.bold && a.italic && a.underline);
|
| 1197 |
1315 |
|
}
|
| 1198 |
1316 |
|
|
| 1202 |
1320 |
|
feed(&mut g, b"\x1b[1;31;44mA\x1b[mB");
|
| 1203 |
1321 |
|
let a = g.row(0)[0];
|
| 1204 |
1322 |
|
let b = g.row(0)[1];
|
| 1205 |
|
- |
assert_eq!(a.fg, Color::Named(1));
|
| 1206 |
|
- |
assert!(a.attrs.bold);
|
| 1207 |
|
- |
assert_eq!(b.fg, Color::Default);
|
| 1208 |
|
- |
assert_eq!(b.bg, Color::Default);
|
| 1209 |
|
- |
assert!(!b.attrs.bold);
|
|
1323 |
+ |
assert_eq!(a.fg(), Color::Named(1));
|
|
1324 |
+ |
assert!(a.attrs().bold);
|
|
1325 |
+ |
assert_eq!(b.fg(), Color::Default);
|
|
1326 |
+ |
assert_eq!(b.bg(), Color::Default);
|
|
1327 |
+ |
assert!(!b.attrs().bold);
|
| 1210 |
1328 |
|
}
|
| 1211 |
1329 |
|
|
| 1212 |
1330 |
|
#[test]
|
| 1213 |
1331 |
|
fn sgr_selective_clears() {
|
| 1214 |
1332 |
|
let mut g = Grid::new(10, 1);
|
| 1215 |
1333 |
|
feed(&mut g, b"\x1b[1;3mA\x1b[22mB\x1b[23mC");
|
| 1216 |
|
- |
assert!(g.row(0)[0].attrs.bold && g.row(0)[0].attrs.italic);
|
|
1334 |
+ |
assert!(g.row(0)[0].attrs().bold && g.row(0)[0].attrs().italic);
|
| 1217 |
1335 |
|
// 22 clears bold + dim; italic stays.
|
| 1218 |
|
- |
assert!(!g.row(0)[1].attrs.bold && g.row(0)[1].attrs.italic);
|
|
1336 |
+ |
assert!(!g.row(0)[1].attrs().bold && g.row(0)[1].attrs().italic);
|
| 1219 |
1337 |
|
// 23 clears italic.
|
| 1220 |
|
- |
assert!(!g.row(0)[2].attrs.italic);
|
|
1338 |
+ |
assert!(!g.row(0)[2].attrs().italic);
|
| 1221 |
1339 |
|
}
|
| 1222 |
1340 |
|
|
| 1223 |
1341 |
|
// ---- cursor shape --------------------------------------------------
|