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;
82pub const CTERM_DA: &[u8] = b"\x1b[=67;84;101;114;109;1;332c";
85const FIRST_LOADABLE_FONT: u32 = 43;
87const MAX_DCS: usize = 32 << 20;
89
90#[derive(Clone, Copy, Debug, PartialEq, Eq)]
91enum State {
92 Ground,
93 Escape,
94 EscIntermediate,
96 Csi,
97 Osc,
99 Dcs,
101 Apc,
103 IgnoreString,
105 StringEsc,
107 Music,
109}
110
111#[derive(Clone, Debug, PartialEq)]
113pub enum TermEvent {
114 Bell,
115 Title(String),
116 Music(Vec<Tone>),
118 Audio(crate::audio::AudioOp),
120}
121
122pub struct Terminal {
123 pub screen: Screen,
124 pub encoding: Encoding,
125 pub music_on_csi_m: bool,
129 pub sixel_display_mode: bool,
132 state: State,
133 string_kind: State,
135 dcs: Vec<u8>,
136 pub syncterm: crate::syncterm::SyncTerm,
138 private: u8,
139 intermediate: u8,
140 params: Vec<u32>,
141 param_present: bool,
142 osc: Vec<u8>,
143 utf8_buf: [u8; 4],
144 utf8_len: usize,
145 utf8_need: usize,
146 replies: Vec<u8>,
147 events: Vec<TermEvent>,
148 petscii: crate::petscii::Petscii,
149 atascii: crate::atascii::Atascii,
150 avatar: crate::avatar::Avatar,
151 rip: Option<Box<crate::rip::Rip>>,
153 line_start: bool,
156 held: Vec<u8>,
159}
160
161impl Terminal {
162 pub fn new(cols: usize, rows: usize, scrollback: usize) -> Self {
163 Terminal {
164 screen: Screen::new(cols, rows, scrollback),
165 encoding: Encoding::Cp437,
166 music_on_csi_m: false,
167 sixel_display_mode: false,
168 state: State::Ground,
169 string_kind: State::IgnoreString,
170 dcs: Vec::new(),
171 syncterm: Default::default(),
172 private: 0,
173 intermediate: 0,
174 params: Vec::with_capacity(MAX_PARAMS),
175 param_present: false,
176 osc: Vec::new(),
177 utf8_buf: [0; 4],
178 utf8_len: 0,
179 utf8_need: 0,
180 replies: Vec::new(),
181 events: Vec::new(),
182 petscii: Default::default(),
183 atascii: Default::default(),
184 avatar: Default::default(),
185 rip: None,
186 line_start: true,
187 held: Vec::new(),
188 }
189 }
190
191 pub fn start_emulation(&mut self) {
194 match self.encoding {
195 Encoding::Petscii => self.petscii.start(&mut self.screen),
196 Encoding::Atascii => self.atascii.start(&mut self.screen),
197 Encoding::Rip => self.rip = Some(Box::default()),
198 _ => {}
199 }
200 }
201
202 pub fn rip(&self) -> Option<&crate::rip::Rip> {
204 self.rip.as_deref()
205 }
206
207 pub fn rip_mut(&mut self) -> Option<&mut crate::rip::Rip> {
208 self.rip.as_deref_mut()
209 }
210
211 pub fn take_replies(&mut self) -> Vec<u8> {
213 std::mem::take(&mut self.replies)
214 }
215
216 pub fn take_events(&mut self) -> Vec<TermEvent> {
217 std::mem::take(&mut self.events)
218 }
219
220 pub fn feed(&mut self, data: &[u8]) {
221 match self.encoding {
222 Encoding::Petscii => {
223 for &b in data {
224 self.petscii.byte(&mut self.screen, b, &mut self.events);
225 }
226 }
227 Encoding::Atascii => {
228 for &b in data {
229 self.atascii.byte(&mut self.screen, b, &mut self.events);
230 }
231 }
232 _ => {
233 if self.syncterm.hold.is_some() {
234 self.held.extend_from_slice(data);
235 return;
236 }
237 for (i, &b) in data.iter().enumerate() {
238 self.byte(b);
239 if self.syncterm.hold.is_some() {
240 self.held.extend_from_slice(&data[i + 1..]);
241 break;
242 }
243 }
244 }
245 }
246 }
247
248 pub fn poll(&mut self, now: std::time::Instant) {
251 self.syncterm.audio.poll(now, &mut self.replies);
252 if self.syncterm.hold.is_some_and(|t| now >= t) {
253 self.syncterm.hold = None;
254 let held = std::mem::take(&mut self.held);
255 self.feed(&held);
256 }
257 }
258
259 pub fn next_poll(&self) -> Option<std::time::Instant> {
261 [self.syncterm.hold, self.syncterm.audio.next_due()].into_iter().flatten().min()
262 }
263
264 fn byte(&mut self, b: u8) {
265 if self.state == State::Ground {
266 let taken = match self.encoding {
267 Encoding::Avatar => self.avatar_byte(b),
268 Encoding::Rip => self.rip_byte(b),
269 _ => false,
270 };
271 if taken {
272 return;
273 }
274 }
275 if self.state != State::Ground && (b == 0x18 || b == 0x1A) {
277 self.state = State::Ground;
278 return;
279 }
280 match self.state {
281 State::Ground => self.ground(b),
282 State::Escape => self.escape(b),
283 State::EscIntermediate => self.state = State::Ground,
284 State::Csi => self.csi(b),
285 State::Osc | State::IgnoreString => match b {
286 0x07 if self.state == State::Osc => self.finish_string(),
287 0x1B => self.state = State::StringEsc,
288 _ if self.state == State::Osc && self.osc.len() < MAX_STRING => self.osc.push(b),
289 _ => {}
290 },
291 State::Dcs | State::Apc => match b {
292 0x1B => self.state = State::StringEsc,
293 _ if self.dcs.len() < MAX_DCS => self.dcs.push(b),
294 _ => {}
295 },
296 State::Music => match b {
297 0x0E => {
298 self.state = State::Ground;
299 let tones = music::parse(&self.osc);
300 if !tones.is_empty() {
301 let ms: u64 = tones.iter().map(|t| t.ms as u64).sum();
302 self.syncterm.audio.music(std::time::Duration::from_millis(ms));
303 self.events.push(TermEvent::Music(tones));
304 }
305 }
306 _ if self.osc.len() < MAX_STRING => self.osc.push(b),
307 _ => self.state = State::Ground,
309 },
310 State::StringEsc => {
311 if b == b'\\' {
312 self.finish_string();
313 } else {
314 self.state = State::Escape;
316 self.escape(b);
317 }
318 }
319 }
320 }
321
322 fn avatar_byte(&mut self, b: u8) -> bool {
324 use crate::avatar::Outcome;
325 match self.avatar.byte(&mut self.screen, b) {
326 Outcome::Pass => false,
327 Outcome::Done => true,
328 Outcome::Show(bytes) => {
329 for b in bytes {
330 self.byte(b);
331 }
332 true
333 }
334 }
335 }
336
337 fn rip_byte(&mut self, b: u8) -> bool {
339 use crate::rip::Feed;
340 let Some(rip) = self.rip.as_mut() else { return false };
341 match rip.byte(&mut self.screen, b, self.line_start) {
342 Feed::Pass => false,
343 Feed::Done => true,
344 Feed::Text(bytes) => {
345 self.line_start = false;
346 for b in bytes {
347 self.byte(b);
348 }
349 true
350 }
351 }
352 }
353
354 fn ground(&mut self, b: u8) {
355 if self.encoding == Encoding::Utf8 && self.utf8_need > 0 {
356 if b & 0xC0 == 0x80 {
357 self.utf8_buf[self.utf8_len] = b;
358 self.utf8_len += 1;
359 if self.utf8_len == self.utf8_need {
360 let ch = std::str::from_utf8(&self.utf8_buf[..self.utf8_len]).ok().and_then(|s| s.chars().next());
361 self.utf8_need = 0;
362 self.screen.put(ch.and_then(cp437::from_unicode).unwrap_or(b'?'));
363 }
364 return;
365 }
366 self.utf8_need = 0;
368 self.screen.put(b'?');
369 }
370 match b {
371 0x00 | 0x7F => {}
372 0x07 => self.events.push(TermEvent::Bell),
373 0x08 => self.screen.backspace(),
374 0x09 => self.screen.tab(),
375 0x0A => {
376 self.screen.line_feed();
377 self.line_start = true;
378 }
379 0x0C => self.screen.erase_display(2),
380 0x0D => {
381 self.screen.carriage_return();
382 self.line_start = true;
383 }
384 0x1B => self.state = State::Escape,
385 _ => match self.encoding {
386 Encoding::Cp437 | Encoding::Petscii | Encoding::Atascii | Encoding::Avatar | Encoding::Rip => {
389 if self.avatar.insert {
390 self.screen.insert_chars(1);
391 }
392 self.line_start = false;
393 self.screen.put(b)
394 }
395 Encoding::Utf8 => match b {
396 0x01..=0x1F => {}
397 0x20..=0x7E => self.screen.put(b),
398 0xC2..=0xDF => self.utf8_start(b, 2),
399 0xE0..=0xEF => self.utf8_start(b, 3),
400 0xF0..=0xF4 => self.utf8_start(b, 4),
401 _ => self.screen.put(b'?'),
402 },
403 },
404 }
405 }
406
407 fn utf8_start(&mut self, b: u8, need: usize) {
408 self.utf8_buf[0] = b;
409 self.utf8_len = 1;
410 self.utf8_need = need;
411 }
412
413 fn escape(&mut self, b: u8) {
414 self.state = State::Ground;
415 match b {
416 b'[' => {
417 self.state = State::Csi;
418 self.private = 0;
419 self.intermediate = 0;
420 self.params.clear();
421 self.param_present = false;
422 }
423 b']' => {
424 self.state = State::Osc;
425 self.string_kind = State::Osc;
426 self.osc.clear();
427 }
428 b'P' => {
429 self.state = State::Dcs;
430 self.string_kind = State::Dcs;
431 self.dcs.clear();
432 }
433 b'_' => {
434 self.state = State::Apc;
435 self.string_kind = State::Apc;
436 self.dcs.clear();
437 }
438 b'X' | b'^' => {
439 self.state = State::IgnoreString;
440 self.string_kind = State::IgnoreString;
441 }
442 0x20..=0x2F => self.state = State::EscIntermediate,
443 b'7' => self.screen.save_cursor(),
444 b'8' => self.screen.restore_cursor(),
445 b'c' => self.screen.reset(),
446 b'D' => self.screen.line_feed(),
447 b'E' => {
448 self.screen.carriage_return();
449 self.screen.line_feed();
450 }
451 b'M' => self.screen.reverse_index(),
452 0x1B => self.state = State::Escape,
453 _ => log::debug!("unhandled ESC {:?}", b as char),
454 }
455 }
456
457 fn finish_string(&mut self) {
458 self.state = State::Ground;
459 match self.string_kind {
460 State::Osc => {}
461 State::Dcs => return self.finish_dcs(),
462 State::Apc => {
463 let apc = std::mem::take(&mut self.dcs);
464 if !self.syncterm.apc(&apc, &mut self.screen, &mut self.replies) {
465 log::debug!("unhandled APC {:?}", String::from_utf8_lossy(&apc[..apc.len().min(32)]));
466 }
467 self.events.extend(self.syncterm.audio_ops.drain(..).map(TermEvent::Audio));
468 if apc.capacity() <= 1 << 20 {
469 self.dcs = apc;
470 self.dcs.clear();
471 }
472 return;
473 }
474 _ => return,
475 }
476 let s = String::from_utf8_lossy(&self.osc).into_owned();
477 let (code, rest) = s.split_once(';').unwrap_or((s.as_str(), ""));
478 match code {
479 "0" | "2" => self.events.push(TermEvent::Title(rest.to_string())),
481 "4" => self.set_palette(rest),
482 "104" => self.reset_palette(rest),
483 _ => log::debug!("unhandled OSC {code}"),
484 }
485 }
486
487 fn finish_dcs(&mut self) {
489 let dcs = std::mem::take(&mut self.dcs);
490 let start = dcs.iter().position(|b| !matches!(b, b'0'..=b'9' | b';')).unwrap_or(dcs.len());
491 if dcs.get(start) == Some(&b'q') {
492 let params: Vec<u32> =
493 String::from_utf8_lossy(&dcs[..start]).split(';').map(|p| p.parse().unwrap_or(0)).collect();
494 let background = self.screen.look.rgb(self.screen.pen.bg);
495 let img = crate::sixel::decode(¶ms, &dcs[start + 1..], background);
496 self.screen.place_image(&img, !self.sixel_display_mode);
497 } else {
498 log::debug!("unhandled DCS {:?}", String::from_utf8_lossy(&dcs[..dcs.len().min(32)]));
499 }
500 if dcs.capacity() <= 1 << 20 {
502 self.dcs = dcs;
503 self.dcs.clear();
504 }
505 }
506
507 fn set_palette(&mut self, args: &str) {
510 let mut parts = args.split(';');
511 while let (Some(i), Some(spec)) = (parts.next(), parts.next()) {
512 let Ok(i) = i.trim().parse::<u8>() else { continue };
513 if spec == "?" {
514 let c = self.screen.look.palette[i as usize];
515 let reply = format!("\x1b]4;{i};rgb:{:02x}/{:02x}/{:02x}\x1b\\", c >> 16, (c >> 8) & 0xFF, c & 0xFF);
516 self.replies.extend_from_slice(reply.as_bytes());
517 } else if let Some(c) = parse_color(spec) {
518 self.screen.look.palette[i as usize] = c;
519 }
520 }
521 }
522
523 fn reset_palette(&mut self, args: &str) {
525 let standard = &Look::standard().palette;
526 if args.trim().is_empty() {
527 self.screen.look.palette = *standard;
528 return;
529 }
530 for i in args.split(';').filter_map(|i| i.trim().parse::<u8>().ok()) {
531 self.screen.look.palette[i as usize] = standard[i as usize];
532 }
533 }
534
535 fn csi(&mut self, b: u8) {
536 match b {
537 b'0'..=b'9' => {
538 if self.params.is_empty() {
539 self.params.push(0);
540 }
541 let p = self.params.last_mut().unwrap();
542 *p = p.saturating_mul(10).saturating_add((b - b'0') as u32);
543 self.param_present = true;
544 }
545 b';' | b':' => {
546 if self.params.is_empty() {
547 self.params.push(0);
548 }
549 if self.params.len() < MAX_PARAMS {
550 self.params.push(0);
551 }
552 self.param_present = true;
553 }
554 0x3C..=0x3F => {
555 if !self.param_present && self.private == 0 {
556 self.private = b;
557 }
558 }
559 b'!' if self.encoding == Encoding::Rip && self.params.is_empty() && self.private == 0 => {
561 self.state = State::Ground;
562 self.replies.extend_from_slice(crate::rip::VERSION_REPLY);
563 }
564 0x20..=0x2F => self.intermediate = b,
565 0x40..=0x7E => {
566 self.state = State::Ground;
567 self.dispatch_csi(b);
568 }
569 0x00..=0x1F => {
571 if b == 0x1B {
572 self.state = State::Escape;
573 } else {
574 self.ground(b);
575 }
576 }
577 _ => self.state = State::Ground,
578 }
579 }
580
581 fn p(&self, i: usize, default: u32) -> u32 {
583 match self.params.get(i) {
584 Some(&v) if v != 0 => v,
585 _ => default,
586 }
587 }
588
589 fn dispatch_csi(&mut self, b: u8) {
590 if self.intermediate != 0 {
591 if self.intermediate == b' ' && b == b'D' && self.private == 0 {
593 let slot = self.params.first().copied().unwrap_or(0) as usize;
594 let font = self.params.get(1).copied().unwrap_or(0);
595 if slot < 4 && font <= 255 {
596 self.screen.look.fonts[slot] = font as u8;
597 }
598 return;
599 }
600 log::debug!("unhandled CSI with intermediate {:?} {:?}", self.intermediate as char, b as char);
601 return;
602 }
603 match self.private {
604 0 => {}
605 b'?' if b == b'S' => return self.graphics_attributes(),
606 b'?' => return self.dec_mode(b),
607 b'=' if b == b'n' => return self.cterm_report(),
608 b'<' if b == b'c' && self.params.first().is_none_or(|&v| v == 0) => {
610 return self.replies.extend_from_slice(b"\x1b[<0;2;3;4;5;6c");
611 }
612 _ => {
613 log::debug!("unhandled private CSI {:?}{:?} {:?}", self.private as char, b as char, self.params);
614 return;
615 }
616 }
617 let no_params = self.params.is_empty();
618 if b == b'|' || no_params && (b == b'N' || b == b'M' && self.music_on_csi_m) {
619 self.state = State::Music;
620 self.osc.clear();
621 return;
622 }
623 let n = self.p(0, 1) as usize;
624 let s = &mut self.screen;
625 match b {
626 b'@' => s.insert_chars(n),
627 b'A' => s.move_by(0, -(n as isize)),
628 b'B' | b'e' => s.move_by(0, n as isize),
629 b'C' | b'a' => s.move_by(n as isize, 0),
630 b'D' => s.move_by(-(n as isize), 0),
631 b'E' => {
632 s.move_by(0, n as isize);
633 s.carriage_return();
634 }
635 b'F' => {
636 s.move_by(0, -(n as isize));
637 s.carriage_return();
638 }
639 b'G' | b'`' => {
640 let y = s.cursor().1;
641 s.move_to(n - 1, y);
642 }
643 b'd' => {
644 let x = s.cursor().0;
645 s.move_to(x, n - 1);
646 }
647 b'H' | b'f' => {
648 let (row, col) = (self.p(0, 1) as usize, self.p(1, 1) as usize);
649 self.screen.move_to(col - 1, row - 1);
650 }
651 b'J' => s.erase_display(self.params.first().copied().unwrap_or(0)),
652 b'K' => s.erase_line(self.params.first().copied().unwrap_or(0)),
653 b'L' => s.insert_lines(n),
654 b'M' => s.delete_lines(n),
655 b'P' => s.delete_chars(n),
656 b'S' => s.scroll_up(n),
657 b'T' => s.scroll_down(n),
658 b'X' => s.erase_chars(n),
659 b'Z' => s.back_tab(n),
660 b'c' if self.params.first().is_none_or(|&v| v == 0) => self.replies.extend_from_slice(CTERM_DA),
664 b'm' => self.sgr(),
665 b'n' => match self.params.first().copied().unwrap_or(0) {
666 5 => self.replies.extend_from_slice(b"\x1b[0n"),
667 6 => {
668 let (x, y) = self.screen.cursor();
669 self.replies.extend_from_slice(format!("\x1b[{};{}R", y + 1, x + 1).as_bytes());
670 }
671 _ => {}
672 },
673 b'r' => {
674 let rows = self.screen.rows() as u32;
675 let (top, bottom) = (self.p(0, 1), self.p(1, rows));
676 self.screen.set_scroll_region(top as usize - 1, bottom as usize - 1);
677 }
678 b's' => s.save_cursor(),
679 b'u' => s.restore_cursor(),
680 b't' if self.params.len() == 4 => {
682 let c = Color::Rgb(rgb(self.params[1] as u8, self.params[2] as u8, self.params[3] as u8));
683 match self.params[0] {
684 0 => self.screen.pen.bg = c,
685 1 => self.screen.pen.fg = c,
686 _ => {}
687 }
688 }
689 b'h' | b'l' => {}
690 _ => log::debug!("unhandled CSI {:?} {:?}", b as char, self.params),
691 }
692 }
693
694 fn cterm_report(&mut self) {
696 let reply = match self.params.first().copied().unwrap_or(1) {
697 1 => {
700 let f = self.screen.look.fonts;
701 format!("\x1b[=1;{FIRST_LOADABLE_FONT};99;{};{};{};{}n", f[0], f[1], f[2], f[3])
702 }
703 2 => {
705 let s = &self.screen;
706 let l = &s.look;
707 let set = [
708 (7, s.autowrap),
709 (25, s.cursor_visible),
710 (31, l.bright_font),
711 (32, l.bright_plain),
712 (33, s.ice_colors),
713 (34, l.blink_font),
714 (35, l.blink_steady),
715 (80, self.sixel_display_mode),
716 ];
717 let modes: Vec<String> = set.iter().filter(|m| m.1).map(|m| m.0.to_string()).collect();
718 format!("\x1b[=2;{}n", modes.join(";"))
719 }
720 3 => {
722 let (w, h) = self.screen.cell_px();
723 format!("\x1b[=3;{h};{w}n")
724 }
725 4 => "\x1b[=4;0n".to_string(),
727 5 => "\x1b[=5;0n".to_string(),
728 6 => "\x1b[=6;0n".to_string(),
729 7 => self.syncterm.audio.report(self.params.get(1).map(|&c| c as usize)),
731 _ => return,
732 };
733 self.replies.extend_from_slice(reply.as_bytes());
734 }
735
736 fn graphics_attributes(&mut self) {
739 let reply = match (self.params.first().copied(), self.params.get(1).copied()) {
740 (Some(1), Some(1 | 4)) => "\x1b[?1;0;256S".to_string(),
741 (Some(2), Some(1 | 4)) => {
742 let (w, h) = self.screen.size_px();
743 format!("\x1b[?2;0;{w};{h}S")
744 }
745 (Some(i @ 1..=3), _) => format!("\x1b[?{i};3;0S"),
746 (Some(i), _) => format!("\x1b[?{i};1;0S"),
747 (None, _) => return,
748 };
749 self.replies.extend_from_slice(reply.as_bytes());
750 }
751
752 fn dec_mode(&mut self, b: u8) {
753 let on = match b {
754 b'h' => true,
755 b'l' => false,
756 _ => return,
757 };
758 for &mode in &self.params {
759 match mode {
760 7 => self.screen.autowrap = on,
761 80 => self.sixel_display_mode = on,
762 25 => self.screen.cursor_visible = on,
763 31 => self.screen.look.bright_font = on,
767 32 => self.screen.look.bright_plain = on,
768 33 => self.screen.ice_colors = on,
769 34 => self.screen.look.blink_font = on,
770 35 => self.screen.look.blink_steady = on,
771 _ => log::debug!("unhandled DEC mode {mode} {on}"),
772 }
773 }
774 }
775
776 fn sgr(&mut self) {
777 if self.params.is_empty() {
778 self.params.push(0);
779 }
780 let pen = &mut self.screen.pen;
781 let mut i = 0;
782 while i < self.params.len() {
783 let p = self.params[i];
784 match p {
785 0 => *pen = Cell { ch: b' ', ..Cell::default() },
786 1 => pen.flags = (pen.flags | flags::BOLD) & !flags::FAINT,
787 2 => pen.flags = (pen.flags | flags::FAINT) & !flags::BOLD,
788 4 => pen.flags |= flags::UNDERLINE,
789 5 | 6 => pen.flags |= flags::BLINK,
790 7 => pen.flags |= flags::REVERSE,
791 8 => pen.flags |= flags::CONCEAL,
792 21 | 22 => pen.flags &= !(flags::BOLD | flags::FAINT),
793 24 => pen.flags &= !flags::UNDERLINE,
794 25 => pen.flags &= !flags::BLINK,
795 27 => pen.flags &= !flags::REVERSE,
796 28 => pen.flags &= !flags::CONCEAL,
797 30..=37 => pen.fg = Color::Pc(ANSI_TO_VGA[(p - 30) as usize]),
798 39 => pen.fg = DEFAULT_FG,
799 40..=47 => pen.bg = Color::Pc(ANSI_TO_VGA[(p - 40) as usize]),
800 49 => pen.bg = DEFAULT_BG,
801 90..=97 => pen.fg = Color::Pc(ANSI_TO_VGA[(p - 90) as usize] + 8),
802 100..=107 => pen.bg = Color::Pc(ANSI_TO_VGA[(p - 100) as usize] + 8),
803 38 | 48 => {
804 let color = match self.params.get(i + 1) {
805 Some(5) => {
806 let c = self.params.get(i + 2).map(|&v| Color::Xterm(v as u8));
807 i += 2;
808 c
809 }
810 Some(2) => {
811 let c = (i + 4 < self.params.len()).then(|| {
812 let v = &self.params[i + 2..i + 5];
813 Color::Rgb(rgb(v[0] as u8, v[1] as u8, v[2] as u8))
814 });
815 i += 4;
816 c
817 }
818 _ => None,
819 };
820 if let Some(c) = color {
821 if p == 38 { pen.fg = c } else { pen.bg = c }
822 }
823 }
824 _ => log::debug!("unhandled SGR {p}"),
825 }
826 i += 1;
827 }
828 }
829}
830
831fn parse_color(spec: &str) -> Option<crate::palette::Rgb> {
834 let spec = spec.trim();
835 if let Some(hex) = spec.strip_prefix('#') {
836 return (hex.len() == 6).then(|| u32::from_str_radix(hex, 16).ok()).flatten();
837 }
838 let mut channels = spec.strip_prefix("rgb:")?.split('/').map(|c| {
839 let v = u32::from_str_radix(c, 16).ok().filter(|_| (1..=4).contains(&c.len()))?;
840 Some((v * 255 / ((1 << (4 * c.len())) - 1)) as u8)
842 });
843 let (r, g, b) = (channels.next()??, channels.next()??, channels.next()??);
844 channels.next().is_none().then(|| rgb(r, g, b))
845}
846
847#[cfg(test)]
848mod tests {
849 use super::*;
850
851 fn term() -> Terminal {
852 Terminal::new(80, 25, 100)
853 }
854
855 fn line(t: &Terminal, y: usize) -> String {
856 Screen::row_text(t.screen.row(y))
857 }
858
859 #[test]
860 fn cursor_position_and_text() {
861 let mut t = term();
862 t.feed(b"\x1b[3;5HHi");
863 assert_eq!(line(&t, 2), " Hi");
864 assert_eq!(t.screen.cursor(), (6, 2));
865 }
866
867 #[test]
868 fn sgr_colors() {
869 let mut t = term();
870 t.feed(b"\x1b[1;31;44mX");
871 let c = t.screen.row(0)[0];
872 assert_eq!(c.fg, Color::Pc(4));
873 assert_eq!(c.bg, Color::Pc(1));
874 assert!(c.flags & flags::BOLD != 0);
875 t.feed(b"\x1b[0;38;2;10;20;30mY");
876 assert_eq!(t.screen.row(0)[1].fg, Color::Rgb(0x0A141E));
877 t.feed(b"\x1b[38;5;196mZ");
878 assert_eq!(t.screen.row(0)[2].fg, Color::Xterm(196));
879 }
880
881 #[test]
882 fn cursor_position_report() {
883 let mut t = term();
884 t.feed(b"\x1b[10;20H\x1b[6n");
885 assert_eq!(t.take_replies(), b"\x1b[10;20R");
886 }
887
888 #[test]
889 fn device_attributes() {
890 let mut t = term();
891 t.feed(b"\x1b[c\x1b[0c");
892 assert_eq!(t.take_replies(), [CTERM_DA, CTERM_DA].concat());
893 t.feed(b"\x1b[>c\x1b[1c");
895 assert!(t.take_replies().is_empty());
896 }
897
898 #[test]
899 fn ice_mode_toggle() {
900 let mut t = term();
901 t.feed(b"\x1b[?33h");
902 assert!(t.screen.ice_colors);
903 t.feed(b"\x1b[?33l");
904 assert!(!t.screen.ice_colors);
905 }
906
907 #[test]
908 fn cp437_glyphs_and_utf8() {
909 let mut t = term();
910 t.feed(&[0xDB, 0x03]);
911 assert_eq!(t.screen.row(0)[0].ch, 0xDB);
912 assert_eq!(t.screen.row(0)[1].ch, 0x03);
913 t.encoding = Encoding::Utf8;
914 t.feed("ۂ".as_bytes());
915 assert_eq!(t.screen.row(0)[2].ch, 0xDB);
916 assert_eq!(t.screen.row(0)[3].ch, 0x82);
917 }
918
919 #[test]
920 fn osc_title_and_strings_ignored() {
921 let mut t = term();
922 t.feed(b"\x1b]0;My BBS\x07\x1bPjunk\x1b\\ok");
923 assert_eq!(t.take_events(), vec![TermEvent::Title("My BBS".into())]);
924 assert_eq!(line(&t, 0), "ok");
925 }
926
927 #[test]
928 fn ansi_art_line_of_exactly_80_columns() {
929 let mut t = term();
930 let mut data = vec![b'#'; 80];
931 data.extend_from_slice(b"\r\nnext");
932 t.feed(&data);
933 assert_eq!(line(&t, 1), "next");
934 }
935
936 #[test]
937 fn save_restore_cursor() {
938 let mut t = term();
939 t.feed(b"\x1b[5;5H\x1b[s\x1b[1;1H\x1b[uX");
940 assert_eq!(t.screen.row(4)[4].ch, b'X');
941 }
942
943 #[test]
944 fn ansi_music() {
945 let mut t = term();
946 t.feed(b"Hi\x1b[NMLO3A\x0e there\x1b[|MLO3A8\x0e");
947 assert_eq!(line(&t, 0), "Hi there");
948 let a = Tone { hz: 440, ms: 500 };
949 assert_eq!(t.take_events(), [TermEvent::Music(vec![a]), TermEvent::Music(vec![Tone { ms: 250, ..a }])]);
950
951 t.feed(b"\x1b[2;1Hx\x1b[1;1H\x1b[M");
953 assert_eq!(line(&t, 0), "x");
954 t.music_on_csi_m = true;
955 t.feed(b"\x1b[MMLO3A\x0e\x1b[1M");
956 assert_eq!(t.take_events(), [TermEvent::Music(vec![a])]);
957 assert_eq!(line(&t, 0), "");
958 }
959
960 #[test]
961 fn cterm_palette_and_fonts() {
962 let mut t = Terminal::new(80, 25, 0);
963 t.feed(b"\x1b]4;1;rgb:ff/80/00;12;#123456\x1b\\");
965 assert_eq!(t.screen.look.palette[1], 0xFF8000);
966 assert_eq!(t.screen.look.rgb(Color::Pc(4)), 0xFF8000);
967 assert_eq!(t.screen.look.rgb(Color::Pc(9)), 0x123456);
968 t.feed(b"\x1b[31mX");
969 assert_eq!(t.screen.row(0)[0].resolve_with(false, &t.screen.look).0, 0xFF8000);
970 t.feed(b"\x1b]4;1;?\x07");
972 assert_eq!(t.take_replies(), b"\x1b]4;1;rgb:ff/80/00\x1b\\");
973 t.feed(b"\x1b]104;1\x07");
974 assert_eq!(t.screen.look.palette[1], 0xAA0000);
975 assert_eq!(t.screen.look.palette[12], 0x123456);
976 t.feed(b"\x1b]104\x07");
977 assert_eq!(t.screen.look, *Look::standard());
978
979 t.feed(b"\x1b[0;42 D\x1b[1;40 D\x1b[?31h\x1b[?32h\x1b[1;32mB");
981 let cell = t.screen.row(0)[1];
982 assert_eq!(t.screen.look.fonts, [42, 40, 0, 0]);
983 assert_eq!(t.screen.look.font_for(&cell), 40);
984 assert_eq!(cell.resolve_with(false, &t.screen.look).0, 0x00AA00);
985 t.feed(b"\x1b[?31l\x1b[?32l");
986 assert_eq!(t.screen.look.font_for(&cell), 42);
987 assert_eq!(cell.resolve_with(false, &t.screen.look).0, 0x55FF55);
988 assert_eq!(parse_color("rgb:f/0/8"), Some(0xFF0088));
989 assert_eq!(parse_color("rgb:ffff/0000/8080"), Some(0xFF0080));
990 assert_eq!(parse_color("rgb:1/2"), None);
991 }
992
993 #[test]
994 fn sixel_pictures() {
995 let mut t = term();
996 let mut seq = b"\x1b[3;5HA\x1bP0;1;0q\"1;1#1;2;100;0;0".to_vec();
998 for _ in 0..6 {
999 seq.extend_from_slice(b"!16~-");
1000 }
1001 seq.extend_from_slice(b"\x1b\\B");
1002 t.feed(&seq);
1003 let s = &t.screen;
1004 let tile = s.tiles.get(s.row(2)[5].tile).expect("a tile");
1009 assert_eq!((tile.width, tile.height), (8, 16));
1010 assert_eq!(tile.pixels[0], crate::sixel::solid(0xFF0000));
1011 assert!(s.row(4)[6].tile != 0 && s.row(2)[7].tile == 0);
1012 assert_eq!(s.row(4)[5].ch, b'B');
1013 assert_eq!(s.row(4)[5].tile, 0, "text replaces the picture");
1014 assert_eq!(t.screen.cursor(), (6, 4));
1015
1016 t.feed(b"\x1b[?2;1S\x1b[?1;1S\x1b[<c");
1018 assert_eq!(t.take_replies(), b"\x1b[?2;0;640;400S\x1b[?1;0;256S\x1b[<0;2;3;4;5;6c");
1019 t.feed(b"\x1b[=1n\x1b[=2n\x1b[=3n\x1b[=7n");
1020 assert_eq!(t.take_replies(), b"\x1b[=1;43;99;0;0;0;0n\x1b[=2;7;25n\x1b[=3;16;8n\x1b[=7n");
1021 }
1022
1023 #[test]
1024 fn sixel_pictures_scroll_the_screen() {
1025 let mut t = term();
1026 t.feed(b"\x1b[25;1HX\x1bPq\"1;1;8;32\x1b\\");
1027 assert_eq!(line(&t, 22), "X");
1030 assert!(t.screen.row(22)[1].tile != 0 && t.screen.row(23)[1].tile != 0);
1031 assert_eq!(t.screen.cursor(), (1, 24));
1032 t.feed(b"\x1b[?80h\x1b[10;10H\x1bPq\"1;1;8;16\x1b\\");
1034 assert!(t.screen.row(0)[0].tile != 0);
1035 assert_eq!(t.screen.cursor(), (9, 9));
1036 }
1037
1038 #[test]
1039 fn waiting_for_a_sound_holds_the_text() {
1040 let mut t = term();
1041 t.feed(b"\x1b_SyncTERM:A;Synth;S=1;W=SIN;F=440;T=100\x1b\\\x1b_SyncTERM:A;Queue;C=2;S=1\x1b\\");
1043 assert!(matches!(t.take_events()[..], [TermEvent::Audio(crate::audio::AudioOp::Queue { channel: 2, .. })]));
1044 t.feed(b"before\x1b_SyncTERM:A;Wait;C=2\x1b\\after");
1045 t.feed(b" more");
1046 assert_eq!(line(&t, 0), "before");
1047 let at = t.next_poll().expect("a time to read again");
1048 t.poll(at);
1049 assert_eq!(line(&t, 0), "beforeafter more");
1050 t.feed(b"\x1b[=7;2n\x1b_SyncTERM:A;Update;C=2\x1b\\");
1052 t.poll(at + std::time::Duration::from_secs(1));
1053 let replies = String::from_utf8(t.take_replies()).unwrap();
1054 assert!(replies.ends_with("\x1b[=7;2;0n"), "{replies:?}");
1055 t.feed(b"\x1b_SyncTERM:Q;libsndfileFormat;0x20;100\x1b\\");
1056 assert_eq!(t.take_replies(), b"\x1b[=7;101;32;100;1n");
1057 }
1058
1059 #[test]
1060 fn reset_returns_to_the_entrys_ice_setting() {
1061 let mut t = Terminal::new(80, 25, 0);
1062 t.screen.base_ice = true;
1063 t.feed(b"\x1b[?33l");
1064 assert!(!t.screen.ice_colors);
1065 t.feed(b"\x1bc");
1066 assert!(t.screen.ice_colors);
1067 }
1068}