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