1use serde::{Deserialize, Serialize};
8
9use crate::cp437;
10use crate::music::{self, Tone};
11use crate::palette::{ANSI_TO_VGA, rgb};
12use crate::screen::{Cell, Color, DEFAULT_BG, DEFAULT_FG, Look, Screen, flags};
13
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum Encoding {
18 #[default]
19 Cp437,
20 Utf8,
21 Petscii,
23 Atascii,
25}
26
27impl Encoding {
28 pub const ALL: [Encoding; 4] = [Encoding::Cp437, Encoding::Utf8, Encoding::Petscii, Encoding::Atascii];
29
30 pub fn name(self) -> &'static str {
31 match self {
32 Encoding::Cp437 => "CP437",
33 Encoding::Utf8 => "UTF-8",
34 Encoding::Petscii => "PETSCII",
35 Encoding::Atascii => "ATASCII",
36 }
37 }
38
39 pub fn is_retro(self) -> bool {
41 matches!(self, Encoding::Petscii | Encoding::Atascii)
42 }
43
44 pub fn screen_size(self) -> Option<(usize, usize)> {
46 match self {
47 Encoding::Petscii => Some((crate::petscii::COLS, crate::petscii::ROWS)),
48 Encoding::Atascii => Some((crate::atascii::COLS, crate::atascii::ROWS)),
49 _ => None,
50 }
51 }
52
53 pub fn encode_text(self, text: &str) -> Vec<u8> {
56 match self {
57 Encoding::Cp437 => cp437::encode(text),
58 Encoding::Utf8 => text.as_bytes().to_vec(),
59 Encoding::Petscii => text
60 .chars()
61 .filter_map(|c| if c == '\r' { Some(0x0D) } else { crate::petscii::encode_char(c) })
62 .collect(),
63 Encoding::Atascii => text
64 .chars()
65 .filter_map(|c| if c == '\r' { Some(0x9B) } else { crate::atascii::encode_char(c) })
66 .collect(),
67 }
68 }
69}
70
71const MAX_PARAMS: usize = 32;
72const MAX_STRING: usize = 4096;
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75enum State {
76 Ground,
77 Escape,
78 EscIntermediate,
80 Csi,
81 Osc,
83 IgnoreString,
85 StringEsc,
87 Music,
89}
90
91#[derive(Clone, Debug, PartialEq, Eq)]
93pub enum TermEvent {
94 Bell,
95 Title(String),
96 Music(Vec<Tone>),
98}
99
100pub struct Terminal {
101 pub screen: Screen,
102 pub encoding: Encoding,
103 pub music_on_csi_m: bool,
107 state: State,
108 string_is_osc: bool,
109 private: u8,
110 intermediate: u8,
111 params: Vec<u32>,
112 param_present: bool,
113 osc: Vec<u8>,
114 utf8_buf: [u8; 4],
115 utf8_len: usize,
116 utf8_need: usize,
117 replies: Vec<u8>,
118 events: Vec<TermEvent>,
119 petscii: crate::petscii::Petscii,
120 atascii: crate::atascii::Atascii,
121}
122
123impl Terminal {
124 pub fn new(cols: usize, rows: usize, scrollback: usize) -> Self {
125 Terminal {
126 screen: Screen::new(cols, rows, scrollback),
127 encoding: Encoding::Cp437,
128 music_on_csi_m: false,
129 state: State::Ground,
130 string_is_osc: false,
131 private: 0,
132 intermediate: 0,
133 params: Vec::with_capacity(MAX_PARAMS),
134 param_present: false,
135 osc: Vec::new(),
136 utf8_buf: [0; 4],
137 utf8_len: 0,
138 utf8_need: 0,
139 replies: Vec::new(),
140 events: Vec::new(),
141 petscii: Default::default(),
142 atascii: Default::default(),
143 }
144 }
145
146 pub fn start_emulation(&mut self) {
149 match self.encoding {
150 Encoding::Petscii => self.petscii.start(&mut self.screen),
151 Encoding::Atascii => self.atascii.start(&mut self.screen),
152 _ => {}
153 }
154 }
155
156 pub fn take_replies(&mut self) -> Vec<u8> {
158 std::mem::take(&mut self.replies)
159 }
160
161 pub fn take_events(&mut self) -> Vec<TermEvent> {
162 std::mem::take(&mut self.events)
163 }
164
165 pub fn feed(&mut self, data: &[u8]) {
166 match self.encoding {
167 Encoding::Petscii => {
168 for &b in data {
169 self.petscii.byte(&mut self.screen, b, &mut self.events);
170 }
171 }
172 Encoding::Atascii => {
173 for &b in data {
174 self.atascii.byte(&mut self.screen, b, &mut self.events);
175 }
176 }
177 _ => {
178 for &b in data {
179 self.byte(b);
180 }
181 }
182 }
183 }
184
185 fn byte(&mut self, b: u8) {
186 if self.state != State::Ground && (b == 0x18 || b == 0x1A) {
188 self.state = State::Ground;
189 return;
190 }
191 match self.state {
192 State::Ground => self.ground(b),
193 State::Escape => self.escape(b),
194 State::EscIntermediate => self.state = State::Ground,
195 State::Csi => self.csi(b),
196 State::Osc | State::IgnoreString => match b {
197 0x07 if self.state == State::Osc => self.finish_string(),
198 0x1B => self.state = State::StringEsc,
199 _ if self.state == State::Osc && self.osc.len() < MAX_STRING => self.osc.push(b),
200 _ => {}
201 },
202 State::Music => match b {
203 0x0E => {
204 self.state = State::Ground;
205 let tones = music::parse(&self.osc);
206 if !tones.is_empty() {
207 self.events.push(TermEvent::Music(tones));
208 }
209 }
210 _ if self.osc.len() < MAX_STRING => self.osc.push(b),
211 _ => self.state = State::Ground,
213 },
214 State::StringEsc => {
215 if b == b'\\' {
216 self.finish_string();
217 } else {
218 self.state = State::Escape;
220 self.escape(b);
221 }
222 }
223 }
224 }
225
226 fn ground(&mut self, b: u8) {
227 if self.encoding == Encoding::Utf8 && self.utf8_need > 0 {
228 if b & 0xC0 == 0x80 {
229 self.utf8_buf[self.utf8_len] = b;
230 self.utf8_len += 1;
231 if self.utf8_len == self.utf8_need {
232 let ch = std::str::from_utf8(&self.utf8_buf[..self.utf8_len]).ok().and_then(|s| s.chars().next());
233 self.utf8_need = 0;
234 self.screen.put(ch.and_then(cp437::from_unicode).unwrap_or(b'?'));
235 }
236 return;
237 }
238 self.utf8_need = 0;
240 self.screen.put(b'?');
241 }
242 match b {
243 0x00 | 0x7F => {}
244 0x07 => self.events.push(TermEvent::Bell),
245 0x08 => self.screen.backspace(),
246 0x09 => self.screen.tab(),
247 0x0A => self.screen.line_feed(),
248 0x0C => self.screen.erase_display(2),
249 0x0D => self.screen.carriage_return(),
250 0x1B => self.state = State::Escape,
251 _ => match self.encoding {
252 Encoding::Cp437 | Encoding::Petscii | Encoding::Atascii => self.screen.put(b),
255 Encoding::Utf8 => match b {
256 0x01..=0x1F => {}
257 0x20..=0x7E => self.screen.put(b),
258 0xC2..=0xDF => self.utf8_start(b, 2),
259 0xE0..=0xEF => self.utf8_start(b, 3),
260 0xF0..=0xF4 => self.utf8_start(b, 4),
261 _ => self.screen.put(b'?'),
262 },
263 },
264 }
265 }
266
267 fn utf8_start(&mut self, b: u8, need: usize) {
268 self.utf8_buf[0] = b;
269 self.utf8_len = 1;
270 self.utf8_need = need;
271 }
272
273 fn escape(&mut self, b: u8) {
274 self.state = State::Ground;
275 match b {
276 b'[' => {
277 self.state = State::Csi;
278 self.private = 0;
279 self.intermediate = 0;
280 self.params.clear();
281 self.param_present = false;
282 }
283 b']' => {
284 self.state = State::Osc;
285 self.string_is_osc = true;
286 self.osc.clear();
287 }
288 b'P' | b'X' | b'^' | b'_' => {
289 self.state = State::IgnoreString;
290 self.string_is_osc = false;
291 }
292 0x20..=0x2F => self.state = State::EscIntermediate,
293 b'7' => self.screen.save_cursor(),
294 b'8' => self.screen.restore_cursor(),
295 b'c' => self.screen.reset(),
296 b'D' => self.screen.line_feed(),
297 b'E' => {
298 self.screen.carriage_return();
299 self.screen.line_feed();
300 }
301 b'M' => self.screen.reverse_index(),
302 0x1B => self.state = State::Escape,
303 _ => log::debug!("unhandled ESC {:?}", b as char),
304 }
305 }
306
307 fn finish_string(&mut self) {
308 self.state = State::Ground;
309 if !self.string_is_osc {
310 return;
311 }
312 let s = String::from_utf8_lossy(&self.osc).into_owned();
313 let (code, rest) = s.split_once(';').unwrap_or((s.as_str(), ""));
314 match code {
315 "0" | "2" => self.events.push(TermEvent::Title(rest.to_string())),
317 "4" => self.set_palette(rest),
318 "104" => self.reset_palette(rest),
319 _ => log::debug!("unhandled OSC {code}"),
320 }
321 }
322
323 fn set_palette(&mut self, args: &str) {
326 let mut parts = args.split(';');
327 while let (Some(i), Some(spec)) = (parts.next(), parts.next()) {
328 let Ok(i) = i.trim().parse::<u8>() else { continue };
329 if spec == "?" {
330 let c = self.screen.look.palette[i as usize];
331 let reply = format!("\x1b]4;{i};rgb:{:02x}/{:02x}/{:02x}\x1b\\", c >> 16, (c >> 8) & 0xFF, c & 0xFF);
332 self.replies.extend_from_slice(reply.as_bytes());
333 } else if let Some(c) = parse_color(spec) {
334 self.screen.look.palette[i as usize] = c;
335 }
336 }
337 }
338
339 fn reset_palette(&mut self, args: &str) {
341 let standard = &Look::standard().palette;
342 if args.trim().is_empty() {
343 self.screen.look.palette = *standard;
344 return;
345 }
346 for i in args.split(';').filter_map(|i| i.trim().parse::<u8>().ok()) {
347 self.screen.look.palette[i as usize] = standard[i as usize];
348 }
349 }
350
351 fn csi(&mut self, b: u8) {
352 match b {
353 b'0'..=b'9' => {
354 if self.params.is_empty() {
355 self.params.push(0);
356 }
357 let p = self.params.last_mut().unwrap();
358 *p = p.saturating_mul(10).saturating_add((b - b'0') as u32);
359 self.param_present = true;
360 }
361 b';' | b':' => {
362 if self.params.is_empty() {
363 self.params.push(0);
364 }
365 if self.params.len() < MAX_PARAMS {
366 self.params.push(0);
367 }
368 self.param_present = true;
369 }
370 0x3C..=0x3F => {
371 if !self.param_present && self.private == 0 {
372 self.private = b;
373 }
374 }
375 0x20..=0x2F => self.intermediate = b,
376 0x40..=0x7E => {
377 self.state = State::Ground;
378 self.dispatch_csi(b);
379 }
380 0x00..=0x1F => {
382 if b == 0x1B {
383 self.state = State::Escape;
384 } else {
385 self.ground(b);
386 }
387 }
388 _ => self.state = State::Ground,
389 }
390 }
391
392 fn p(&self, i: usize, default: u32) -> u32 {
394 match self.params.get(i) {
395 Some(&v) if v != 0 => v,
396 _ => default,
397 }
398 }
399
400 fn dispatch_csi(&mut self, b: u8) {
401 if self.intermediate != 0 {
402 if self.intermediate == b' ' && b == b'D' && self.private == 0 {
404 let slot = self.params.first().copied().unwrap_or(0) as usize;
405 let font = self.params.get(1).copied().unwrap_or(0);
406 if slot < 4 && font <= 255 {
407 self.screen.look.fonts[slot] = font as u8;
408 }
409 return;
410 }
411 log::debug!("unhandled CSI with intermediate {:?} {:?}", self.intermediate as char, b as char);
412 return;
413 }
414 match self.private {
415 0 => {}
416 b'?' => return self.dec_mode(b),
417 _ => {
418 log::debug!("unhandled private CSI {:?}{:?} {:?}", self.private as char, b as char, self.params);
419 return;
420 }
421 }
422 let no_params = self.params.is_empty();
423 if b == b'|' || no_params && (b == b'N' || b == b'M' && self.music_on_csi_m) {
424 self.state = State::Music;
425 self.osc.clear();
426 return;
427 }
428 let n = self.p(0, 1) as usize;
429 let s = &mut self.screen;
430 match b {
431 b'@' => s.insert_chars(n),
432 b'A' => s.move_by(0, -(n as isize)),
433 b'B' | b'e' => s.move_by(0, n as isize),
434 b'C' | b'a' => s.move_by(n as isize, 0),
435 b'D' => s.move_by(-(n as isize), 0),
436 b'E' => {
437 s.move_by(0, n as isize);
438 s.carriage_return();
439 }
440 b'F' => {
441 s.move_by(0, -(n as isize));
442 s.carriage_return();
443 }
444 b'G' | b'`' => {
445 let y = s.cursor().1;
446 s.move_to(n - 1, y);
447 }
448 b'd' => {
449 let x = s.cursor().0;
450 s.move_to(x, n - 1);
451 }
452 b'H' | b'f' => {
453 let (row, col) = (self.p(0, 1) as usize, self.p(1, 1) as usize);
454 self.screen.move_to(col - 1, row - 1);
455 }
456 b'J' => s.erase_display(self.params.first().copied().unwrap_or(0)),
457 b'K' => s.erase_line(self.params.first().copied().unwrap_or(0)),
458 b'L' => s.insert_lines(n),
459 b'M' => s.delete_lines(n),
460 b'P' => s.delete_chars(n),
461 b'S' => s.scroll_up(n),
462 b'T' => s.scroll_down(n),
463 b'X' => s.erase_chars(n),
464 b'Z' => s.back_tab(n),
465 b'c' if self.params.first().is_none_or(|&v| v == 0) => self.replies.extend_from_slice(b"\x1b[?1;0c"),
468 b'm' => self.sgr(),
469 b'n' => match self.params.first().copied().unwrap_or(0) {
470 5 => self.replies.extend_from_slice(b"\x1b[0n"),
471 6 => {
472 let (x, y) = self.screen.cursor();
473 self.replies.extend_from_slice(format!("\x1b[{};{}R", y + 1, x + 1).as_bytes());
474 }
475 _ => {}
476 },
477 b'r' => {
478 let rows = self.screen.rows() as u32;
479 let (top, bottom) = (self.p(0, 1), self.p(1, rows));
480 self.screen.set_scroll_region(top as usize - 1, bottom as usize - 1);
481 }
482 b's' => s.save_cursor(),
483 b'u' => s.restore_cursor(),
484 b't' if self.params.len() == 4 => {
486 let c = Color::Rgb(rgb(self.params[1] as u8, self.params[2] as u8, self.params[3] as u8));
487 match self.params[0] {
488 0 => self.screen.pen.bg = c,
489 1 => self.screen.pen.fg = c,
490 _ => {}
491 }
492 }
493 b'h' | b'l' => {}
494 _ => log::debug!("unhandled CSI {:?} {:?}", b as char, self.params),
495 }
496 }
497
498 fn dec_mode(&mut self, b: u8) {
499 let on = match b {
500 b'h' => true,
501 b'l' => false,
502 _ => return,
503 };
504 for &mode in &self.params {
505 match mode {
506 7 => self.screen.autowrap = on,
507 25 => self.screen.cursor_visible = on,
508 31 => self.screen.look.bright_font = on,
512 32 => self.screen.look.bright_plain = on,
513 33 => self.screen.ice_colors = on,
514 34 => self.screen.look.blink_font = on,
515 35 => self.screen.look.blink_steady = on,
516 _ => log::debug!("unhandled DEC mode {mode} {on}"),
517 }
518 }
519 }
520
521 fn sgr(&mut self) {
522 if self.params.is_empty() {
523 self.params.push(0);
524 }
525 let pen = &mut self.screen.pen;
526 let mut i = 0;
527 while i < self.params.len() {
528 let p = self.params[i];
529 match p {
530 0 => *pen = Cell { ch: b' ', ..Cell::default() },
531 1 => pen.flags = (pen.flags | flags::BOLD) & !flags::FAINT,
532 2 => pen.flags = (pen.flags | flags::FAINT) & !flags::BOLD,
533 4 => pen.flags |= flags::UNDERLINE,
534 5 | 6 => pen.flags |= flags::BLINK,
535 7 => pen.flags |= flags::REVERSE,
536 8 => pen.flags |= flags::CONCEAL,
537 21 | 22 => pen.flags &= !(flags::BOLD | flags::FAINT),
538 24 => pen.flags &= !flags::UNDERLINE,
539 25 => pen.flags &= !flags::BLINK,
540 27 => pen.flags &= !flags::REVERSE,
541 28 => pen.flags &= !flags::CONCEAL,
542 30..=37 => pen.fg = Color::Pc(ANSI_TO_VGA[(p - 30) as usize]),
543 39 => pen.fg = DEFAULT_FG,
544 40..=47 => pen.bg = Color::Pc(ANSI_TO_VGA[(p - 40) as usize]),
545 49 => pen.bg = DEFAULT_BG,
546 90..=97 => pen.fg = Color::Pc(ANSI_TO_VGA[(p - 90) as usize] + 8),
547 100..=107 => pen.bg = Color::Pc(ANSI_TO_VGA[(p - 100) as usize] + 8),
548 38 | 48 => {
549 let color = match self.params.get(i + 1) {
550 Some(5) => {
551 let c = self.params.get(i + 2).map(|&v| Color::Xterm(v as u8));
552 i += 2;
553 c
554 }
555 Some(2) => {
556 let c = (i + 4 < self.params.len()).then(|| {
557 let v = &self.params[i + 2..i + 5];
558 Color::Rgb(rgb(v[0] as u8, v[1] as u8, v[2] as u8))
559 });
560 i += 4;
561 c
562 }
563 _ => None,
564 };
565 if let Some(c) = color {
566 if p == 38 { pen.fg = c } else { pen.bg = c }
567 }
568 }
569 _ => log::debug!("unhandled SGR {p}"),
570 }
571 i += 1;
572 }
573 }
574}
575
576fn parse_color(spec: &str) -> Option<crate::palette::Rgb> {
579 let spec = spec.trim();
580 if let Some(hex) = spec.strip_prefix('#') {
581 return (hex.len() == 6).then(|| u32::from_str_radix(hex, 16).ok()).flatten();
582 }
583 let mut channels = spec.strip_prefix("rgb:")?.split('/').map(|c| {
584 let v = u32::from_str_radix(c, 16).ok().filter(|_| (1..=4).contains(&c.len()))?;
585 Some((v * 255 / ((1 << (4 * c.len())) - 1)) as u8)
587 });
588 let (r, g, b) = (channels.next()??, channels.next()??, channels.next()??);
589 channels.next().is_none().then(|| rgb(r, g, b))
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595
596 fn term() -> Terminal {
597 Terminal::new(80, 25, 100)
598 }
599
600 fn line(t: &Terminal, y: usize) -> String {
601 Screen::row_text(t.screen.row(y))
602 }
603
604 #[test]
605 fn cursor_position_and_text() {
606 let mut t = term();
607 t.feed(b"\x1b[3;5HHi");
608 assert_eq!(line(&t, 2), " Hi");
609 assert_eq!(t.screen.cursor(), (6, 2));
610 }
611
612 #[test]
613 fn sgr_colors() {
614 let mut t = term();
615 t.feed(b"\x1b[1;31;44mX");
616 let c = t.screen.row(0)[0];
617 assert_eq!(c.fg, Color::Pc(4));
618 assert_eq!(c.bg, Color::Pc(1));
619 assert!(c.flags & flags::BOLD != 0);
620 t.feed(b"\x1b[0;38;2;10;20;30mY");
621 assert_eq!(t.screen.row(0)[1].fg, Color::Rgb(0x0A141E));
622 t.feed(b"\x1b[38;5;196mZ");
623 assert_eq!(t.screen.row(0)[2].fg, Color::Xterm(196));
624 }
625
626 #[test]
627 fn cursor_position_report() {
628 let mut t = term();
629 t.feed(b"\x1b[10;20H\x1b[6n");
630 assert_eq!(t.take_replies(), b"\x1b[10;20R");
631 }
632
633 #[test]
634 fn device_attributes() {
635 let mut t = term();
636 t.feed(b"\x1b[c\x1b[0c");
637 assert_eq!(t.take_replies(), b"\x1b[?1;0c\x1b[?1;0c");
638 t.feed(b"\x1b[>c\x1b[1c");
640 assert!(t.take_replies().is_empty());
641 }
642
643 #[test]
644 fn ice_mode_toggle() {
645 let mut t = term();
646 t.feed(b"\x1b[?33h");
647 assert!(t.screen.ice_colors);
648 t.feed(b"\x1b[?33l");
649 assert!(!t.screen.ice_colors);
650 }
651
652 #[test]
653 fn cp437_glyphs_and_utf8() {
654 let mut t = term();
655 t.feed(&[0xDB, 0x03]);
656 assert_eq!(t.screen.row(0)[0].ch, 0xDB);
657 assert_eq!(t.screen.row(0)[1].ch, 0x03);
658 t.encoding = Encoding::Utf8;
659 t.feed("ۂ".as_bytes());
660 assert_eq!(t.screen.row(0)[2].ch, 0xDB);
661 assert_eq!(t.screen.row(0)[3].ch, 0x82);
662 }
663
664 #[test]
665 fn osc_title_and_strings_ignored() {
666 let mut t = term();
667 t.feed(b"\x1b]0;My BBS\x07\x1bPjunk\x1b\\ok");
668 assert_eq!(t.take_events(), vec![TermEvent::Title("My BBS".into())]);
669 assert_eq!(line(&t, 0), "ok");
670 }
671
672 #[test]
673 fn ansi_art_line_of_exactly_80_columns() {
674 let mut t = term();
675 let mut data = vec![b'#'; 80];
676 data.extend_from_slice(b"\r\nnext");
677 t.feed(&data);
678 assert_eq!(line(&t, 1), "next");
679 }
680
681 #[test]
682 fn save_restore_cursor() {
683 let mut t = term();
684 t.feed(b"\x1b[5;5H\x1b[s\x1b[1;1H\x1b[uX");
685 assert_eq!(t.screen.row(4)[4].ch, b'X');
686 }
687
688 #[test]
689 fn ansi_music() {
690 let mut t = term();
691 t.feed(b"Hi\x1b[NMLO3A\x0e there\x1b[|MLO3A8\x0e");
692 assert_eq!(line(&t, 0), "Hi there");
693 let a = Tone { hz: 440, ms: 500 };
694 assert_eq!(t.take_events(), [TermEvent::Music(vec![a]), TermEvent::Music(vec![Tone { ms: 250, ..a }])]);
695
696 t.feed(b"\x1b[2;1Hx\x1b[1;1H\x1b[M");
698 assert_eq!(line(&t, 0), "x");
699 t.music_on_csi_m = true;
700 t.feed(b"\x1b[MMLO3A\x0e\x1b[1M");
701 assert_eq!(t.take_events(), [TermEvent::Music(vec![a])]);
702 assert_eq!(line(&t, 0), "");
703 }
704
705 #[test]
706 fn cterm_palette_and_fonts() {
707 let mut t = Terminal::new(80, 25, 0);
708 t.feed(b"\x1b]4;1;rgb:ff/80/00;12;#123456\x1b\\");
710 assert_eq!(t.screen.look.palette[1], 0xFF8000);
711 assert_eq!(t.screen.look.rgb(Color::Pc(4)), 0xFF8000);
712 assert_eq!(t.screen.look.rgb(Color::Pc(9)), 0x123456);
713 t.feed(b"\x1b[31mX");
714 assert_eq!(t.screen.row(0)[0].resolve_with(false, &t.screen.look).0, 0xFF8000);
715 t.feed(b"\x1b]4;1;?\x07");
717 assert_eq!(t.take_replies(), b"\x1b]4;1;rgb:ff/80/00\x1b\\");
718 t.feed(b"\x1b]104;1\x07");
719 assert_eq!(t.screen.look.palette[1], 0xAA0000);
720 assert_eq!(t.screen.look.palette[12], 0x123456);
721 t.feed(b"\x1b]104\x07");
722 assert_eq!(t.screen.look, *Look::standard());
723
724 t.feed(b"\x1b[0;42 D\x1b[1;40 D\x1b[?31h\x1b[?32h\x1b[1;32mB");
726 let cell = t.screen.row(0)[1];
727 assert_eq!(t.screen.look.fonts, [42, 40, 0, 0]);
728 assert_eq!(t.screen.look.font_for(&cell), 40);
729 assert_eq!(cell.resolve_with(false, &t.screen.look).0, 0x00AA00);
730 t.feed(b"\x1b[?31l\x1b[?32l");
731 assert_eq!(t.screen.look.font_for(&cell), 42);
732 assert_eq!(cell.resolve_with(false, &t.screen.look).0, 0x55FF55);
733 assert_eq!(parse_color("rgb:f/0/8"), Some(0xFF0088));
734 assert_eq!(parse_color("rgb:ffff/0000/8080"), Some(0xFF0080));
735 assert_eq!(parse_color("rgb:1/2"), None);
736 }
737
738 #[test]
739 fn reset_returns_to_the_entrys_ice_setting() {
740 let mut t = Terminal::new(80, 25, 0);
741 t.screen.base_ice = true;
742 t.feed(b"\x1b[?33l");
743 assert!(!t.screen.ice_colors);
744 t.feed(b"\x1bc");
745 assert!(t.screen.ice_colors);
746 }
747}