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