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 C128,
26 Atascii,
28 Avatar,
30 Rip,
32 Viewdata,
34 Mode7,
36 Igs,
38 Skypix,
40}
41
42impl Encoding {
43 pub const ALL: [Encoding; 11] = [
44 Encoding::Cp437,
45 Encoding::Utf8,
46 Encoding::Petscii,
47 Encoding::C128,
48 Encoding::Atascii,
49 Encoding::Viewdata,
50 Encoding::Mode7,
51 Encoding::Igs,
52 Encoding::Skypix,
53 Encoding::Avatar,
54 Encoding::Rip,
55 ];
56
57 pub fn name(self) -> &'static str {
58 match self {
59 Encoding::Cp437 => "CP437",
60 Encoding::Utf8 => "UTF-8",
61 Encoding::Petscii => "PETSCII",
62 Encoding::C128 => "C128",
63 Encoding::Atascii => "ATASCII",
64 Encoding::Avatar => "Avatar",
65 Encoding::Rip => "RIPscrip",
66 Encoding::Viewdata => "Viewdata",
67 Encoding::Mode7 => "Mode 7",
68 Encoding::Igs => "IGS",
69 Encoding::Skypix => "SkyPix",
70 }
71 }
72
73 pub fn is_retro(self) -> bool {
75 matches!(self, Encoding::Petscii | Encoding::C128 | Encoding::Atascii)
76 }
77
78 pub fn screen_size(self) -> Option<(usize, usize)> {
80 match self {
81 Encoding::Petscii => Some((crate::petscii::COLS, crate::petscii::ROWS)),
82 Encoding::C128 => Some((crate::petscii::C128_COLS, crate::petscii::ROWS)),
83 Encoding::Atascii => Some((crate::atascii::COLS, crate::atascii::ROWS)),
84 Encoding::Rip => Some((crate::rip::COLS, crate::rip::ROWS)),
85 Encoding::Viewdata => Some((crate::viewdata::COLS, crate::viewdata::ROWS)),
86 Encoding::Mode7 => Some((crate::viewdata::COLS, crate::viewdata::BEEB_ROWS)),
87 Encoding::Igs => Some((crate::igs::COLS, crate::igs::ROWS)),
88 Encoding::Skypix => Some((crate::skypix::COLS, crate::skypix::ROWS)),
89 _ => None,
90 }
91 }
92
93 pub fn encode_text(self, text: &str) -> Vec<u8> {
96 match self {
97 Encoding::Cp437 | Encoding::Avatar | Encoding::Rip | Encoding::Igs => cp437::encode(text),
98 Encoding::Skypix => text.chars().map(|c| if (c as u32) < 256 { c as u8 } else { b'?' }).collect(),
100 Encoding::Utf8 => text.as_bytes().to_vec(),
101 Encoding::Petscii | Encoding::C128 => text
102 .chars()
103 .filter_map(|c| if c == '\r' { Some(0x0D) } else { crate::petscii::encode_char(c) })
104 .collect(),
105 Encoding::Atascii => text
106 .chars()
107 .filter_map(|c| if c == '\r' { Some(0x9B) } else { crate::atascii::encode_char(c) })
108 .collect(),
109 Encoding::Viewdata => text.chars().filter_map(crate::viewdata::encode_char).collect(),
110 Encoding::Mode7 => text.chars().filter_map(crate::viewdata::beeb_encode_char).collect(),
111 }
112 }
113}
114
115const MAX_PARAMS: usize = 32;
116const MAX_STRING: usize = 4096;
117pub const CTERM_DA: &[u8] = b"\x1b[=67;84;101;114;109;1;332c";
120use crate::screen::FIRST_LOADABLE_FONT;
121const MAX_DCS: usize = 32 << 20;
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
125enum State {
126 Ground,
127 Escape,
128 EscIntermediate,
130 Csi,
131 Osc,
133 Dcs,
135 Apc,
137 IgnoreString,
139 FontBlock(u8, usize),
141 StringEsc,
143 Music,
145}
146
147#[derive(Clone, Debug, PartialEq)]
149pub enum TermEvent {
150 Bell,
151 Title(String),
152 Music(Vec<Tone>),
154 Audio(crate::audio::AudioOp),
156 RipQuestion(crate::rip::Question),
158}
159
160pub struct Terminal {
161 pub screen: Screen,
162 pub encoding: Encoding,
163 pub music_on_csi_m: bool,
167 pub music_enabled: bool,
169 music_fore: bool,
172 music_via_m: bool,
174 pub sixel_display_mode: bool,
177 state: State,
178 string_kind: State,
180 dcs: Vec<u8>,
181 pub syncterm: crate::syncterm::SyncTerm,
183 pub mouse: crate::mouse::MouseModes,
185 pub bracketed_paste: bool,
188 private: u8,
189 intermediate: u8,
190 params: Vec<u32>,
191 param_present: bool,
192 osc: Vec<u8>,
193 utf8_buf: [u8; 4],
194 utf8_len: usize,
195 utf8_need: usize,
196 replies: Vec<u8>,
197 events: Vec<TermEvent>,
198 petscii: crate::petscii::Petscii,
199 atascii: crate::atascii::Atascii,
200 avatar: crate::avatar::Avatar,
201 viewdata: crate::viewdata::Viewdata,
202 rip: Option<Box<crate::rip::Rip>>,
204 pub download_dir: Option<std::path::PathBuf>,
206 pub fonts_dir: Option<std::path::PathBuf>,
208 rip_block: Option<(crate::rip::BlockTransfer, Vec<u8>)>,
211 igs: Option<Box<crate::igs::Igs>>,
213 skypix: Option<Box<crate::skypix::Skypix>>,
215 line_start: bool,
218 held: Vec<u8>,
221}
222
223impl Terminal {
224 pub fn new(cols: usize, rows: usize, scrollback: usize) -> Self {
225 Terminal {
226 screen: Screen::new(cols, rows, scrollback),
227 encoding: Encoding::Cp437,
228 music_on_csi_m: false,
229 music_enabled: true,
230 music_fore: true,
231 music_via_m: false,
232 sixel_display_mode: false,
233 state: State::Ground,
234 string_kind: State::IgnoreString,
235 dcs: Vec::new(),
236 syncterm: Default::default(),
237 mouse: Default::default(),
238 bracketed_paste: false,
239 private: 0,
240 intermediate: 0,
241 params: Vec::with_capacity(MAX_PARAMS),
242 param_present: false,
243 osc: Vec::new(),
244 utf8_buf: [0; 4],
245 utf8_len: 0,
246 utf8_need: 0,
247 replies: Vec::new(),
248 events: Vec::new(),
249 petscii: Default::default(),
250 atascii: Default::default(),
251 avatar: Default::default(),
252 viewdata: Default::default(),
253 rip: None,
254 download_dir: None,
255 fonts_dir: None,
256 rip_block: None,
257 igs: None,
258 skypix: None,
259 line_start: true,
260 held: Vec::new(),
261 }
262 }
263
264 pub fn start_emulation(&mut self) {
267 match self.encoding {
268 Encoding::Petscii => self.petscii.start(&mut self.screen),
269 Encoding::C128 => {
270 self.petscii = crate::petscii::Petscii::c128();
271 self.petscii.start(&mut self.screen);
272 }
273 Encoding::Atascii => self.atascii.start(&mut self.screen),
274 Encoding::Rip => self.rip = Some(Box::default()),
275 Encoding::Viewdata => self.viewdata.start(&mut self.screen),
276 Encoding::Mode7 => {
277 self.viewdata = crate::viewdata::Viewdata::new(crate::viewdata::Kind::Beeb);
278 self.viewdata.start(&mut self.screen);
279 }
280 Encoding::Igs => self.igs = Some(Box::default()),
281 Encoding::Skypix => self.skypix = Some(Box::default()),
282 _ => {}
283 }
284 }
285
286 pub fn rip(&self) -> Option<&crate::rip::Rip> {
288 self.rip.as_deref()
289 }
290
291 pub fn rip_mut(&mut self) -> Option<&mut crate::rip::Rip> {
292 self.rip.as_deref_mut()
293 }
294
295 pub fn toggle_reveal(&mut self) -> Option<bool> {
298 matches!(self.encoding, Encoding::Viewdata | Encoding::Mode7)
299 .then(|| self.viewdata.toggle_reveal(&mut self.screen))
300 }
301
302 pub fn igs(&self) -> Option<&crate::igs::Igs> {
304 self.igs.as_deref()
305 }
306
307 pub fn igs_mut(&mut self) -> Option<&mut crate::igs::Igs> {
308 self.igs.as_deref_mut()
309 }
310
311 pub fn skypix_mut(&mut self) -> Option<&mut crate::skypix::Skypix> {
313 self.skypix.as_deref_mut()
314 }
315
316 pub fn take_skypix_transfer(&mut self) -> Option<(crate::skypix::Transfer, Vec<u8>)> {
318 self.skypix.as_deref_mut()?.transfer.take()
319 }
320
321 pub fn skypix(&self) -> Option<&crate::skypix::Skypix> {
322 self.skypix.as_deref()
323 }
324
325 fn skypix_done(&mut self) {
327 let Some(sky) = self.skypix.as_deref_mut() else { return };
328 self.replies.append(&mut sky.replies);
329 for _ in 0..std::mem::take(&mut sky.bells) {
330 self.events.push(TermEvent::Bell);
331 }
332 for op in sky.audio.drain(..) {
333 if self.music_enabled {
334 self.events.push(TermEvent::Audio(op));
335 }
336 }
337 let (x, y) = sky.cursor();
338 self.screen.move_to(x, y);
339 self.screen.cursor_visible = sky.cursor_on;
340 }
341
342 fn igs_done(&mut self) {
344 let Some(igs) = self.igs.as_deref_mut() else { return };
345 self.replies.append(&mut igs.replies);
346 for _ in 0..std::mem::take(&mut igs.bells) {
347 self.events.push(TermEvent::Bell);
348 }
349 for op in igs.audio.drain(..) {
350 if self.music_enabled {
351 self.events.push(TermEvent::Audio(op));
352 }
353 }
354 let (x, y) = igs.cursor();
355 self.screen.move_to(x, y);
356 self.screen.cursor_visible = igs.cursor_on;
357 }
358
359 pub fn codepage(&self) -> Option<&'static crate::codepage::CodePage> {
363 matches!(self.encoding, Encoding::Cp437 | Encoding::Avatar | Encoding::Rip)
364 .then(|| crate::codepage::for_font(self.screen.look.fonts[0]))
365 .flatten()
366 }
367
368 pub fn text_codepage(&self) -> Option<&'static crate::codepage::CodePage> {
371 match self.encoding {
372 Encoding::Petscii | Encoding::C128 | Encoding::Atascii => {
373 crate::codepage::screen_codes(self.screen.look.fonts[0])
374 }
375 _ => self.codepage(),
376 }
377 }
378
379 pub fn encode_text(&self, text: &str) -> Vec<u8> {
381 match self.codepage() {
382 Some(cp) => cp.encode_str(&text.replace("\r\n", "\r")),
383 None => self.encoding.encode_text(text),
384 }
385 }
386
387 pub fn paste(&self, text: &str) -> Vec<u8> {
390 if !self.bracketed_paste {
391 return self.encode_text(text);
392 }
393 let text: String = text.chars().filter(|&c| c != '\x1b').collect();
394 let mut out = b"\x1b[200~".to_vec();
395 out.extend(self.encode_text(&text));
396 out.extend_from_slice(b"\x1b[201~");
397 out
398 }
399
400 pub fn take_replies(&mut self) -> Vec<u8> {
402 std::mem::take(&mut self.replies)
403 }
404
405 pub fn take_events(&mut self) -> Vec<TermEvent> {
406 std::mem::take(&mut self.events)
407 }
408
409 pub fn feed(&mut self, data: &[u8]) {
410 match self.encoding {
411 Encoding::Petscii | Encoding::C128 => {
412 for &b in data {
413 self.petscii.byte(&mut self.screen, b, &mut self.events);
414 }
415 }
416 Encoding::Atascii => {
417 for &b in data {
418 self.atascii.byte(&mut self.screen, b, &mut self.events);
419 }
420 }
421 Encoding::Viewdata | Encoding::Mode7 => self.viewdata.feed(&mut self.screen, data, &mut self.events),
422 Encoding::Igs => {
423 if let Some(igs) = self.igs.as_deref_mut() {
424 igs.feed(data);
425 }
426 self.igs_done();
427 }
428 Encoding::Skypix => {
429 if let Some(sky) = self.skypix.as_deref_mut() {
430 if sky.fonts_dir.is_none() {
431 sky.fonts_dir.clone_from(&self.fonts_dir);
432 }
433 sky.feed(data);
434 }
435 self.skypix_done();
436 }
437 _ => {
438 if self.syncterm.hold.is_some() {
439 self.held.extend_from_slice(data);
440 return;
441 }
442 for (i, &b) in data.iter().enumerate() {
443 self.byte(b);
444 if self.syncterm.hold.is_some() {
445 self.held.extend_from_slice(&data[i + 1..]);
446 break;
447 }
448 if let Some(block) = self.rip.as_mut().and_then(|r| r.block.take()) {
450 let rest = data[i + 1..].strip_prefix(b"\n").unwrap_or(&data[i + 1..]);
451 self.rip_block = Some((block, rest.to_vec()));
452 break;
453 }
454 }
455 }
456 }
457 }
458
459 pub fn poll(&mut self, now: std::time::Instant) {
462 self.syncterm.audio.poll(now, &mut self.replies);
463 if let Some(igs) = self.igs.as_deref_mut().filter(|g| g.hold.is_some()) {
464 igs.poll(now);
465 self.igs_done();
466 }
467 if let Some(sky) = self.skypix.as_deref_mut().filter(|s| s.hold.is_some()) {
468 sky.poll(now);
469 self.skypix_done();
470 }
471 if self.syncterm.hold.is_some_and(|t| now >= t) {
472 self.syncterm.hold = None;
473 let held = std::mem::take(&mut self.held);
474 self.feed(&held);
475 }
476 }
477
478 pub fn interrupt(&mut self) {
480 if let Some(igs) = self.igs.as_deref_mut() {
481 igs.interrupt();
482 self.igs_done();
483 }
484 }
485
486 pub fn next_poll(&self) -> Option<std::time::Instant> {
488 let igs = self.igs.as_ref().and_then(|g| g.hold);
489 let sky = self.skypix.as_ref().and_then(|s| s.hold);
490 [self.syncterm.hold, self.syncterm.audio.next_due(), igs, sky].into_iter().flatten().min()
491 }
492
493 fn byte(&mut self, b: u8) {
494 if self.state == State::Ground {
495 let taken = match self.encoding {
496 Encoding::Avatar => self.avatar_byte(b),
497 Encoding::Rip => self.rip_byte(b),
498 _ => false,
499 };
500 if taken {
501 return;
502 }
503 }
504 if self.state != State::Ground && !matches!(self.state, State::FontBlock(..)) && (b == 0x18 || b == 0x1A) {
506 self.state = State::Ground;
507 return;
508 }
509 match self.state {
510 State::Ground => self.ground(b),
511 State::Escape => self.escape(b),
512 State::EscIntermediate => self.state = State::Ground,
513 State::Csi => self.csi(b),
514 State::Osc | State::IgnoreString => match b {
515 0x07 if self.state == State::Osc => self.finish_string(),
516 0x1B => self.state = State::StringEsc,
517 _ if self.state == State::Osc && self.osc.len() < MAX_STRING => self.osc.push(b),
518 _ => {}
519 },
520 State::FontBlock(slot, len) => {
521 self.dcs.push(b);
522 if self.dcs.len() >= len {
523 self.state = State::Ground;
524 let font = std::mem::take(&mut self.dcs);
525 self.screen.look.load_font(slot, &font);
526 }
527 }
528 State::Dcs | State::Apc => match b {
529 0x1B => self.state = State::StringEsc,
530 _ if self.dcs.len() < MAX_DCS => self.dcs.push(b),
531 _ => {}
532 },
533 State::Music => match b {
534 0x0E => {
535 self.state = State::Ground;
536 let mut tune = std::mem::take(&mut self.osc);
539 if self.music_via_m {
540 let first = tune.first().map(|b| b.to_ascii_uppercase());
541 let digit_next = tune.get(1).is_some_and(u8::is_ascii_digit);
542 if matches!(first, Some(b'B' | b'F' | b'L' | b'S')) || first == Some(b'N') && !digit_next {
543 tune.insert(0, b'M');
544 }
545 }
546 for w in tune.windows(2) {
548 if w[0].eq_ignore_ascii_case(&b'M') {
549 match w[1].to_ascii_uppercase() {
550 b'F' => self.music_fore = true,
551 b'B' => self.music_fore = false,
552 _ => {}
553 }
554 }
555 }
556 let tones = music::parse(&tune);
557 self.osc = tune;
558 if !tones.is_empty() {
559 let ms: u64 = tones.iter().map(|t| t.ms as u64).sum();
560 let length = std::time::Duration::from_millis(ms);
561 self.syncterm.audio.music(length);
562 self.events.push(TermEvent::Music(tones));
563 if self.music_fore && self.music_enabled {
566 let wait = length.min(std::time::Duration::from_secs(60));
567 self.syncterm.hold = Some(std::time::Instant::now() + wait);
568 }
569 }
570 }
571 _ if self.osc.len() < MAX_STRING => self.osc.push(b),
572 _ => self.state = State::Ground,
574 },
575 State::StringEsc => {
576 if b == b'\\' {
577 self.finish_string();
578 } else {
579 self.state = State::Escape;
581 self.escape(b);
582 }
583 }
584 }
585 }
586
587 fn avatar_byte(&mut self, b: u8) -> bool {
589 use crate::avatar::Outcome;
590 match self.avatar.byte(&mut self.screen, b) {
591 Outcome::Pass => false,
592 Outcome::Done => true,
593 Outcome::Show(bytes) => {
594 for b in bytes {
595 self.byte(b);
596 }
597 true
598 }
599 }
600 }
601
602 fn rip_io(&mut self) -> Option<(&mut crate::rip::Rip, &mut Screen, crate::rip::Io<'_>)> {
603 let rip = self.rip.as_deref_mut()?;
604 let io = crate::rip::Io {
605 cache: self.syncterm.cache_dir.as_deref(),
606 downloads: self.download_dir.as_deref(),
607 replies: &mut self.replies,
608 };
609 Some((rip, &mut self.screen, io))
610 }
611
612 pub fn take_rip_block(&mut self) -> Option<(crate::rip::BlockTransfer, Vec<u8>)> {
614 self.rip_block.take()
615 }
616
617 fn rip_events(&mut self) {
619 let Some(rip) = self.rip.as_deref_mut() else { return };
620 if std::mem::take(&mut rip.bell) {
621 self.events.push(TermEvent::Bell);
622 }
623 if let Some(q) = rip.take_question() {
624 self.events.push(TermEvent::RipQuestion(q));
625 }
626 }
627
628 pub fn rip_press(&mut self, x: usize, y: usize) -> Option<Vec<u8>> {
631 let (rip, screen, mut io) = self.rip_io()?;
632 let out = rip.press(screen, x, y, &mut io);
633 self.rip_events();
634 out
635 }
636
637 pub fn rip_answer(&mut self, answer: Option<String>) {
640 if let Some((rip, screen, mut io)) = self.rip_io() {
641 rip.answer(screen, answer, &mut io);
642 }
643 self.rip_events();
644 }
645
646 fn rip_byte(&mut self, b: u8) -> bool {
648 use crate::rip::Feed;
649 let Some(rip) = self.rip.as_mut() else { return false };
650 let mut io = crate::rip::Io {
651 cache: self.syncterm.cache_dir.as_deref(),
652 downloads: self.download_dir.as_deref(),
653 replies: &mut self.replies,
654 };
655 match rip.byte(&mut self.screen, b, self.line_start, &mut io) {
656 Feed::Pass => false,
657 Feed::Done => {
658 let ask = rip.bell;
659 if let Some(scene) = rip.take_scene() {
661 rip.scene_depth += 1;
662 let eat_lf = std::mem::take(&mut rip.eat_lf);
664 self.line_start = true;
665 for b in scene {
666 self.byte(b);
667 }
668 if let Some(rip) = self.rip.as_mut() {
669 rip.scene_depth = rip.scene_depth.saturating_sub(1);
670 rip.eat_lf = eat_lf;
671 }
672 self.line_start = true;
673 }
674 if ask || self.rip.as_ref().is_some_and(|r| r.has_question()) {
675 self.rip_events();
676 }
677 true
678 }
679 Feed::Text(bytes) => {
680 self.line_start = false;
681 for b in bytes {
682 self.byte(b);
683 }
684 true
685 }
686 }
687 }
688
689 fn ground(&mut self, b: u8) {
690 if self.encoding == Encoding::Utf8 && self.utf8_need > 0 {
691 if b & 0xC0 == 0x80 {
692 self.utf8_buf[self.utf8_len] = b;
693 self.utf8_len += 1;
694 if self.utf8_len == self.utf8_need {
695 let ch = std::str::from_utf8(&self.utf8_buf[..self.utf8_len]).ok().and_then(|s| s.chars().next());
696 self.utf8_need = 0;
697 self.screen.put(ch.and_then(cp437::from_unicode).unwrap_or(b'?'));
698 }
699 return;
700 }
701 self.utf8_need = 0;
703 self.screen.put(b'?');
704 }
705 match b {
706 0x00 | 0x7F => {}
707 0x07 => self.events.push(TermEvent::Bell),
708 0x08 => self.screen.backspace(),
709 0x09 => self.screen.tab(),
710 0x0A => {
711 self.screen.line_feed();
712 self.line_start = true;
713 }
714 0x0C => self.screen.erase_display(2),
715 0x0D => {
716 self.screen.carriage_return();
717 self.line_start = true;
718 }
719 0x1B => self.state = State::Escape,
720 _ => match self.encoding {
721 Encoding::Cp437
724 | Encoding::Petscii
725 | Encoding::C128
726 | Encoding::Atascii
727 | Encoding::Avatar
728 | Encoding::Rip
729 | Encoding::Viewdata
730 | Encoding::Mode7
731 | Encoding::Igs
732 | Encoding::Skypix => {
733 if self.avatar.insert {
734 self.screen.insert_chars(1);
735 }
736 self.line_start = false;
737 self.screen.put(b)
738 }
739 Encoding::Utf8 => match b {
740 0x01..=0x1F => {}
741 0x20..=0x7E => self.screen.put(b),
742 0xC2..=0xDF => self.utf8_start(b, 2),
743 0xE0..=0xEF => self.utf8_start(b, 3),
744 0xF0..=0xF4 => self.utf8_start(b, 4),
745 _ => self.screen.put(b'?'),
746 },
747 },
748 }
749 }
750
751 fn utf8_start(&mut self, b: u8, need: usize) {
752 self.utf8_buf[0] = b;
753 self.utf8_len = 1;
754 self.utf8_need = need;
755 }
756
757 fn escape(&mut self, b: u8) {
758 self.state = State::Ground;
759 match b {
760 b'[' => {
761 self.state = State::Csi;
762 self.private = 0;
763 self.intermediate = 0;
764 self.params.clear();
765 self.param_present = false;
766 }
767 b']' => {
768 self.state = State::Osc;
769 self.string_kind = State::Osc;
770 self.osc.clear();
771 }
772 b'P' => {
773 self.state = State::Dcs;
774 self.string_kind = State::Dcs;
775 self.dcs.clear();
776 }
777 b'_' => {
778 self.state = State::Apc;
779 self.string_kind = State::Apc;
780 self.dcs.clear();
781 }
782 b'X' | b'^' => {
783 self.state = State::IgnoreString;
784 self.string_kind = State::IgnoreString;
785 }
786 0x20..=0x2F => self.state = State::EscIntermediate,
787 b'7' => self.screen.save_cursor(),
788 b'8' => self.screen.restore_cursor(),
789 b'c' => {
790 self.screen.reset();
791 self.mouse = Default::default();
792 self.bracketed_paste = false;
793 }
794 b'D' => self.screen.line_feed(),
795 b'E' => {
796 self.screen.carriage_return();
797 self.screen.line_feed();
798 }
799 b'M' => self.screen.reverse_index(),
800 0x1B => self.state = State::Escape,
801 _ => log::debug!("unhandled ESC {:?}", b as char),
802 }
803 }
804
805 fn finish_string(&mut self) {
806 self.state = State::Ground;
807 match self.string_kind {
808 State::Osc => {}
809 State::Dcs => return self.finish_dcs(),
810 State::Apc => {
811 let apc = std::mem::take(&mut self.dcs);
812 if !self.syncterm.apc(&apc, &mut self.screen, &mut self.replies) {
813 log::debug!("unhandled APC {:?}", String::from_utf8_lossy(&apc[..apc.len().min(32)]));
814 }
815 self.events.extend(self.syncterm.audio_ops.drain(..).map(TermEvent::Audio));
816 if apc.capacity() <= 1 << 20 {
817 self.dcs = apc;
818 self.dcs.clear();
819 }
820 return;
821 }
822 _ => return,
823 }
824 let s = String::from_utf8_lossy(&self.osc).into_owned();
825 let (code, rest) = s.split_once(';').unwrap_or((s.as_str(), ""));
826 match code {
827 "0" | "2" => self.events.push(TermEvent::Title(rest.to_string())),
829 "4" => self.set_palette(rest),
830 "104" => self.reset_palette(rest),
831 _ => log::debug!("unhandled OSC {code}"),
832 }
833 }
834
835 fn finish_dcs(&mut self) {
837 let dcs = std::mem::take(&mut self.dcs);
838 if let Some(rest) = dcs.strip_prefix(b"CTerm:Font:") {
840 let text = String::from_utf8_lossy(rest);
841 if let Some((slot, b64)) = text.split_once(':')
842 && let (Ok(slot), Some(data)) = (slot.trim().parse::<u8>(), crate::syncterm::decode_b64(b64))
843 && !self.screen.look.load_font(slot, &data)
844 {
845 log::debug!("not loading a {}-byte font into slot {slot}", data.len());
846 }
847 return;
848 }
849 let start = dcs.iter().position(|b| !matches!(b, b'0'..=b'9' | b';')).unwrap_or(dcs.len());
850 if dcs.get(start) == Some(&b'q') {
851 let params: Vec<u32> =
852 String::from_utf8_lossy(&dcs[..start]).split(';').map(|p| p.parse().unwrap_or(0)).collect();
853 let background = self.screen.look.rgb(self.screen.pen.bg);
854 let img = crate::sixel::decode(¶ms, &dcs[start + 1..], background);
855 self.screen.place_image(&img, !self.sixel_display_mode);
856 } else {
857 log::debug!("unhandled DCS {:?}", String::from_utf8_lossy(&dcs[..dcs.len().min(32)]));
858 }
859 if dcs.capacity() <= 1 << 20 {
861 self.dcs = dcs;
862 self.dcs.clear();
863 }
864 }
865
866 fn set_palette(&mut self, args: &str) {
869 let mut parts = args.split(';');
870 while let (Some(i), Some(spec)) = (parts.next(), parts.next()) {
871 let Ok(i) = i.trim().parse::<u8>() else { continue };
872 if spec == "?" {
873 let c = self.screen.look.palette[i as usize];
874 let reply = format!("\x1b]4;{i};rgb:{:02x}/{:02x}/{:02x}\x1b\\", c >> 16, (c >> 8) & 0xFF, c & 0xFF);
875 self.replies.extend_from_slice(reply.as_bytes());
876 } else if let Some(c) = parse_color(spec) {
877 self.screen.look.palette[i as usize] = c;
878 }
879 }
880 }
881
882 fn reset_palette(&mut self, args: &str) {
884 let standard = &Look::standard().palette;
885 if args.trim().is_empty() {
886 self.screen.look.palette = *standard;
887 return;
888 }
889 for i in args.split(';').filter_map(|i| i.trim().parse::<u8>().ok()) {
890 self.screen.look.palette[i as usize] = standard[i as usize];
891 }
892 }
893
894 fn csi(&mut self, b: u8) {
895 match b {
896 b'0'..=b'9' => {
897 if self.params.is_empty() {
898 self.params.push(0);
899 }
900 let p = self.params.last_mut().unwrap();
901 *p = p.saturating_mul(10).saturating_add((b - b'0') as u32);
902 self.param_present = true;
903 }
904 b';' | b':' => {
905 if self.params.is_empty() {
906 self.params.push(0);
907 }
908 if self.params.len() < MAX_PARAMS {
909 self.params.push(0);
910 }
911 self.param_present = true;
912 }
913 0x3C..=0x3F => {
914 if !self.param_present && self.private == 0 {
915 self.private = b;
916 }
917 }
918 b'!' if self.encoding == Encoding::Rip && self.params.is_empty() && self.private == 0 => {
920 self.state = State::Ground;
921 self.replies.extend_from_slice(crate::rip::VERSION_REPLY);
922 }
923 0x20..=0x2F => self.intermediate = b,
924 0x40..=0x7E => {
925 self.state = State::Ground;
926 self.dispatch_csi(b);
927 }
928 0x00..=0x1F => {
930 if b == 0x1B {
931 self.state = State::Escape;
932 } else {
933 self.ground(b);
934 }
935 }
936 _ => self.state = State::Ground,
937 }
938 }
939
940 fn p(&self, i: usize, default: u32) -> u32 {
942 match self.params.get(i) {
943 Some(&v) if v != 0 => v,
944 _ => default,
945 }
946 }
947
948 fn dispatch_csi(&mut self, b: u8) {
949 if self.intermediate != 0 {
950 if self.intermediate == b' ' && b == b'D' && self.private == 0 {
952 let slot = self.params.first().copied().unwrap_or(0) as usize;
953 let font = self.params.get(1).copied().unwrap_or(0);
954 if slot < 4 && font <= 255 {
955 self.screen.look.fonts[slot] = font as u8;
956 }
957 return;
958 }
959 log::debug!("unhandled CSI with intermediate {:?} {:?}", self.intermediate as char, b as char);
960 return;
961 }
962 match self.private {
963 0 => {}
964 b'?' if b == b'S' => return self.graphics_attributes(),
965 b'?' => return self.dec_mode(b),
966 b'=' if b == b'n' => return self.cterm_report(),
967 b'=' if b == b'{' => {
969 let slot = self.params.first().copied().unwrap_or(255).min(255) as u8;
970 let len = match self.params.get(1).copied().unwrap_or(0) {
971 1 => 3584,
972 2 => 2048,
973 _ => 4096,
974 };
975 self.dcs.clear();
976 self.state = State::FontBlock(slot, len);
977 return;
978 }
979 b'<' if b == b'c' && self.params.first().is_none_or(|&v| v == 0) => {
981 return self.replies.extend_from_slice(b"\x1b[<0;1;2;3;4;5;6;7c");
982 }
983 _ => {
984 log::debug!("unhandled private CSI {:?}{:?} {:?}", self.private as char, b as char, self.params);
985 return;
986 }
987 }
988 let no_params = self.params.is_empty();
989 if b == b'|' || no_params && (b == b'N' || b == b'M' && self.music_on_csi_m) {
990 self.state = State::Music;
991 self.music_via_m = b == b'M';
992 self.osc.clear();
993 return;
994 }
995 let n = self.p(0, 1) as usize;
996 let s = &mut self.screen;
997 match b {
998 b'@' => s.insert_chars(n),
999 b'A' => s.move_by(0, -(n as isize)),
1000 b'B' | b'e' => s.move_by(0, n as isize),
1001 b'C' | b'a' => s.move_by(n as isize, 0),
1002 b'D' => s.move_by(-(n as isize), 0),
1003 b'E' => {
1004 s.move_by(0, n as isize);
1005 s.carriage_return();
1006 }
1007 b'F' => {
1008 s.move_by(0, -(n as isize));
1009 s.carriage_return();
1010 }
1011 b'G' | b'`' => {
1012 let y = s.cursor().1;
1013 s.move_to(n - 1, y);
1014 }
1015 b'd' => {
1016 let x = s.cursor().0;
1017 s.move_to(x, n - 1);
1018 }
1019 b'H' | b'f' => {
1020 let (row, col) = (self.p(0, 1) as usize, self.p(1, 1) as usize);
1021 self.screen.move_to(col - 1, row - 1);
1022 }
1023 b'J' => s.erase_display(self.params.first().copied().unwrap_or(0)),
1024 b'K' => s.erase_line(self.params.first().copied().unwrap_or(0)),
1025 b'L' => s.insert_lines(n),
1026 b'M' => s.delete_lines(n),
1027 b'P' => s.delete_chars(n),
1028 b'S' => s.scroll_up(n),
1029 b'T' => s.scroll_down(n),
1030 b'X' => s.erase_chars(n),
1031 b'Z' => s.back_tab(n),
1032 b'c' if self.params.first().is_none_or(|&v| v == 0) => self.replies.extend_from_slice(CTERM_DA),
1036 b'm' => self.sgr(),
1037 b'n' => match self.params.first().copied().unwrap_or(0) {
1038 5 => self.replies.extend_from_slice(b"\x1b[0n"),
1039 6 => {
1040 let (x, y) = self.screen.cursor();
1041 self.replies.extend_from_slice(format!("\x1b[{};{}R", y + 1, x + 1).as_bytes());
1042 }
1043 _ => {}
1044 },
1045 b'r' => {
1046 let rows = self.screen.rows() as u32;
1047 let (top, bottom) = (self.p(0, 1), self.p(1, rows));
1048 self.screen.set_scroll_region(top as usize - 1, bottom as usize - 1);
1049 }
1050 b's' => s.save_cursor(),
1051 b'u' => s.restore_cursor(),
1052 b't' if self.params.len() == 4 => {
1054 let c = Color::Rgb(rgb(self.params[1] as u8, self.params[2] as u8, self.params[3] as u8));
1055 match self.params[0] {
1056 0 => self.screen.pen.bg = c,
1057 1 => self.screen.pen.fg = c,
1058 _ => {}
1059 }
1060 }
1061 b'h' | b'l' => {}
1062 _ => log::debug!("unhandled CSI {:?} {:?}", b as char, self.params),
1063 }
1064 }
1065
1066 fn cterm_report(&mut self) {
1068 let reply = match self.params.first().copied().unwrap_or(1) {
1069 1 => {
1072 let f = self.screen.look.fonts;
1073 format!("\x1b[=1;{FIRST_LOADABLE_FONT};99;{};{};{};{}n", f[0], f[1], f[2], f[3])
1074 }
1075 2 => {
1077 let s = &self.screen;
1078 let l = &s.look;
1079 let set = [
1080 (7, s.autowrap),
1081 (25, s.cursor_visible),
1082 (31, l.bright_font),
1083 (32, l.bright_plain),
1084 (33, s.ice_colors),
1085 (34, l.blink_font),
1086 (35, l.blink_steady),
1087 (80, self.sixel_display_mode),
1088 (2004, self.bracketed_paste),
1089 ];
1090 let mut modes: Vec<String> = set.iter().filter(|m| m.1).map(|m| m.0.to_string()).collect();
1091 modes.extend(self.mouse.set_modes().iter().map(|m| m.to_string()));
1092 format!("\x1b[=2;{}n", modes.join(";"))
1093 }
1094 3 => {
1096 let (w, h) = self.screen.cell_px();
1097 format!("\x1b[=3;{h};{w}n")
1098 }
1099 4 => "\x1b[=4;0n".to_string(),
1101 5 => "\x1b[=5;0n".to_string(),
1102 6 => "\x1b[=6;0n".to_string(),
1103 7 => self.syncterm.audio.report(self.params.get(1).map(|&c| c as usize)),
1105 _ => return,
1106 };
1107 self.replies.extend_from_slice(reply.as_bytes());
1108 }
1109
1110 fn graphics_attributes(&mut self) {
1113 let reply = match (self.params.first().copied(), self.params.get(1).copied()) {
1114 (Some(1), Some(1 | 4)) => "\x1b[?1;0;256S".to_string(),
1115 (Some(2), Some(1 | 4)) => {
1116 let (w, h) = self.screen.size_px();
1117 format!("\x1b[?2;0;{w};{h}S")
1118 }
1119 (Some(i @ 1..=3), _) => format!("\x1b[?{i};3;0S"),
1120 (Some(i), _) => format!("\x1b[?{i};1;0S"),
1121 (None, _) => return,
1122 };
1123 self.replies.extend_from_slice(reply.as_bytes());
1124 }
1125
1126 fn dec_mode(&mut self, b: u8) {
1127 let on = match b {
1128 b'h' => true,
1129 b'l' => false,
1130 _ => return,
1131 };
1132 for &mode in &self.params {
1133 if self.mouse.set(mode, on) {
1134 continue;
1135 }
1136 match mode {
1137 7 => self.screen.autowrap = on,
1138 80 => self.sixel_display_mode = on,
1139 2004 => self.bracketed_paste = on,
1140 25 => self.screen.cursor_visible = on,
1141 31 => self.screen.look.bright_font = on,
1145 32 => self.screen.look.bright_plain = on,
1146 33 => self.screen.ice_colors = on,
1147 34 => self.screen.look.blink_font = on,
1148 35 => self.screen.look.blink_steady = on,
1149 _ => log::debug!("unhandled DEC mode {mode} {on}"),
1150 }
1151 }
1152 }
1153
1154 fn sgr(&mut self) {
1155 if self.params.is_empty() {
1156 self.params.push(0);
1157 }
1158 let pen = &mut self.screen.pen;
1159 let mut i = 0;
1160 while i < self.params.len() {
1161 let p = self.params[i];
1162 match p {
1163 0 => *pen = Cell { ch: b' ', ..Cell::default() },
1164 1 => pen.flags = (pen.flags | flags::BOLD) & !flags::FAINT,
1165 2 => pen.flags = (pen.flags | flags::FAINT) & !flags::BOLD,
1166 4 => pen.flags |= flags::UNDERLINE,
1167 5 | 6 => pen.flags |= flags::BLINK,
1168 7 => pen.flags |= flags::REVERSE,
1169 8 => pen.flags |= flags::CONCEAL,
1170 21 | 22 => pen.flags &= !(flags::BOLD | flags::FAINT),
1171 24 => pen.flags &= !flags::UNDERLINE,
1172 25 => pen.flags &= !flags::BLINK,
1173 27 => pen.flags &= !flags::REVERSE,
1174 28 => pen.flags &= !flags::CONCEAL,
1175 30..=37 => pen.fg = Color::Pc(ANSI_TO_VGA[(p - 30) as usize]),
1176 39 => pen.fg = DEFAULT_FG,
1177 40..=47 => pen.bg = Color::Pc(ANSI_TO_VGA[(p - 40) as usize]),
1178 49 => pen.bg = DEFAULT_BG,
1179 90..=97 => pen.fg = Color::Pc(ANSI_TO_VGA[(p - 90) as usize] + 8),
1180 100..=107 => pen.bg = Color::Pc(ANSI_TO_VGA[(p - 100) as usize] + 8),
1181 38 | 48 => {
1182 let color = match self.params.get(i + 1) {
1183 Some(5) => {
1184 let c = self.params.get(i + 2).map(|&v| Color::Xterm(v as u8));
1185 i += 2;
1186 c
1187 }
1188 Some(2) => {
1189 let c = (i + 4 < self.params.len()).then(|| {
1190 let v = &self.params[i + 2..i + 5];
1191 Color::Rgb(rgb(v[0] as u8, v[1] as u8, v[2] as u8))
1192 });
1193 i += 4;
1194 c
1195 }
1196 _ => None,
1197 };
1198 if let Some(c) = color {
1199 if p == 38 { pen.fg = c } else { pen.bg = c }
1200 }
1201 }
1202 _ => log::debug!("unhandled SGR {p}"),
1203 }
1204 i += 1;
1205 }
1206 }
1207}
1208
1209fn parse_color(spec: &str) -> Option<crate::palette::Rgb> {
1212 let spec = spec.trim();
1213 if let Some(hex) = spec.strip_prefix('#') {
1214 return (hex.len() == 6).then(|| u32::from_str_radix(hex, 16).ok()).flatten();
1215 }
1216 let mut channels = spec.strip_prefix("rgb:")?.split('/').map(|c| {
1217 let v = u32::from_str_radix(c, 16).ok().filter(|_| (1..=4).contains(&c.len()))?;
1218 Some((v * 255 / ((1 << (4 * c.len())) - 1)) as u8)
1220 });
1221 let (r, g, b) = (channels.next()??, channels.next()??, channels.next()??);
1222 channels.next().is_none().then(|| rgb(r, g, b))
1223}
1224
1225#[cfg(test)]
1226mod tests {
1227 use super::*;
1228
1229 fn term() -> Terminal {
1230 Terminal::new(80, 25, 100)
1231 }
1232
1233 fn line(t: &Terminal, y: usize) -> String {
1234 Screen::row_text(t.screen.row(y))
1235 }
1236
1237 #[test]
1238 fn cursor_position_and_text() {
1239 let mut t = term();
1240 t.feed(b"\x1b[3;5HHi");
1241 assert_eq!(line(&t, 2), " Hi");
1242 assert_eq!(t.screen.cursor(), (6, 2));
1243 }
1244
1245 #[test]
1246 fn sgr_colors() {
1247 let mut t = term();
1248 t.feed(b"\x1b[1;31;44mX");
1249 let c = t.screen.row(0)[0];
1250 assert_eq!(c.fg, Color::Pc(4));
1251 assert_eq!(c.bg, Color::Pc(1));
1252 assert!(c.flags & flags::BOLD != 0);
1253 t.feed(b"\x1b[0;38;2;10;20;30mY");
1254 assert_eq!(t.screen.row(0)[1].fg, Color::Rgb(0x0A141E));
1255 t.feed(b"\x1b[38;5;196mZ");
1256 assert_eq!(t.screen.row(0)[2].fg, Color::Xterm(196));
1257 }
1258
1259 #[test]
1260 fn cursor_position_report() {
1261 let mut t = term();
1262 t.feed(b"\x1b[10;20H\x1b[6n");
1263 assert_eq!(t.take_replies(), b"\x1b[10;20R");
1264 }
1265
1266 #[test]
1267 fn device_attributes() {
1268 let mut t = term();
1269 t.feed(b"\x1b[c\x1b[0c");
1270 assert_eq!(t.take_replies(), [CTERM_DA, CTERM_DA].concat());
1271 t.feed(b"\x1b[>c\x1b[1c");
1273 assert!(t.take_replies().is_empty());
1274 }
1275
1276 #[test]
1277 fn ice_mode_toggle() {
1278 let mut t = term();
1279 t.feed(b"\x1b[?33h");
1280 assert!(t.screen.ice_colors);
1281 t.feed(b"\x1b[?33l");
1282 assert!(!t.screen.ice_colors);
1283 }
1284
1285 #[test]
1286 fn cp437_glyphs_and_utf8() {
1287 let mut t = term();
1288 t.feed(&[0xDB, 0x03]);
1289 assert_eq!(t.screen.row(0)[0].ch, 0xDB);
1290 assert_eq!(t.screen.row(0)[1].ch, 0x03);
1291 t.encoding = Encoding::Utf8;
1292 t.feed("ۂ".as_bytes());
1293 assert_eq!(t.screen.row(0)[2].ch, 0xDB);
1294 assert_eq!(t.screen.row(0)[3].ch, 0x82);
1295 }
1296
1297 #[test]
1298 fn osc_title_and_strings_ignored() {
1299 let mut t = term();
1300 t.feed(b"\x1b]0;My BBS\x07\x1bPjunk\x1b\\ok");
1301 assert_eq!(t.take_events(), vec![TermEvent::Title("My BBS".into())]);
1302 assert_eq!(line(&t, 0), "ok");
1303 }
1304
1305 #[test]
1306 fn ansi_art_line_of_exactly_80_columns() {
1307 let mut t = term();
1308 let mut data = vec![b'#'; 80];
1309 data.extend_from_slice(b"\r\nnext");
1310 t.feed(&data);
1311 assert_eq!(line(&t, 1), "next");
1312 }
1313
1314 #[test]
1315 fn save_restore_cursor() {
1316 let mut t = term();
1317 t.feed(b"\x1b[5;5H\x1b[s\x1b[1;1H\x1b[uX");
1318 assert_eq!(t.screen.row(4)[4].ch, b'X');
1319 }
1320
1321 #[test]
1322 fn ansi_music() {
1323 let mut t = term();
1324 t.music_enabled = false;
1326 t.feed(b"Hi\x1b[NMLO3A\x0e there\x1b[|MLO3A8\x0e");
1327 assert_eq!(line(&t, 0), "Hi there");
1328 let a = Tone { hz: 440, ms: 500 };
1329 assert_eq!(t.take_events(), [TermEvent::Music(vec![a]), TermEvent::Music(vec![Tone { ms: 250, ..a }])]);
1330
1331 t.feed(b"\x1b[2;1Hx\x1b[1;1H\x1b[M");
1333 assert_eq!(line(&t, 0), "x");
1334 t.music_on_csi_m = true;
1335 t.feed(b"\x1b[MMLO3A\x0e\x1b[1M");
1336 assert_eq!(t.take_events(), [TermEvent::Music(vec![a])]);
1337 assert_eq!(line(&t, 0), "");
1338 }
1339
1340 #[test]
1341 fn music_in_the_foreground_holds_the_text() {
1342 let mut t = term();
1343 t.feed(b"\x1b[NT120L4C\x0eafter");
1345 assert_eq!(line(&t, 0), "");
1346 let at = t.next_poll().expect("held");
1347 t.poll(at);
1348 assert_eq!(line(&t, 0), "after");
1349 t.feed(b"\x1b[NMBC\x0e!\x1b[NC\x0e?");
1351 assert_eq!(line(&t, 0), "after!?");
1352 t.music_on_csi_m = true;
1354 t.feed(b"\x1b[MFC\x0e#");
1355 assert_eq!(line(&t, 0), "after!?");
1356 assert!(matches!(t.take_events().last(), Some(TermEvent::Music(tones)) if tones.len() == 2));
1357 }
1358
1359 #[test]
1360 fn cterm_palette_and_fonts() {
1361 let mut t = Terminal::new(80, 25, 0);
1362 t.feed(b"\x1b]4;1;rgb:ff/80/00;12;#123456\x1b\\");
1364 assert_eq!(t.screen.look.palette[1], 0xFF8000);
1365 assert_eq!(t.screen.look.rgb(Color::Pc(4)), 0xFF8000);
1366 assert_eq!(t.screen.look.rgb(Color::Pc(9)), 0x123456);
1367 t.feed(b"\x1b[31mX");
1368 assert_eq!(t.screen.row(0)[0].resolve_with(false, &t.screen.look).0, 0xFF8000);
1369 t.feed(b"\x1b]4;1;?\x07");
1371 assert_eq!(t.take_replies(), b"\x1b]4;1;rgb:ff/80/00\x1b\\");
1372 t.feed(b"\x1b]104;1\x07");
1373 assert_eq!(t.screen.look.palette[1], 0xAA0000);
1374 assert_eq!(t.screen.look.palette[12], 0x123456);
1375 t.feed(b"\x1b]104\x07");
1376 assert_eq!(t.screen.look, *Look::standard());
1377
1378 t.feed(b"\x1b[0;42 D\x1b[1;40 D\x1b[?31h\x1b[?32h\x1b[1;32mB");
1380 let cell = t.screen.row(0)[1];
1381 assert_eq!(t.screen.look.fonts, [42, 40, 0, 0]);
1382 assert_eq!(t.screen.look.font_for(&cell), 40);
1383 assert_eq!(cell.resolve_with(false, &t.screen.look).0, 0x00AA00);
1384 t.feed(b"\x1b[?31l\x1b[?32l");
1385 assert_eq!(t.screen.look.font_for(&cell), 42);
1386 assert_eq!(cell.resolve_with(false, &t.screen.look).0, 0x55FF55);
1387 assert_eq!(parse_color("rgb:f/0/8"), Some(0xFF0088));
1388 assert_eq!(parse_color("rgb:ffff/0000/8080"), Some(0xFF0080));
1389 assert_eq!(parse_color("rgb:1/2"), None);
1390 }
1391
1392 #[test]
1393 fn sixel_pictures() {
1394 let mut t = term();
1395 let mut seq = b"\x1b[3;5HA\x1bP0;1;0q\"1;1#1;2;100;0;0".to_vec();
1397 for _ in 0..6 {
1398 seq.extend_from_slice(b"!16~-");
1399 }
1400 seq.extend_from_slice(b"\x1b\\B");
1401 t.feed(&seq);
1402 let s = &t.screen;
1403 let tile = s.tiles.get(s.row(2)[5].tile).expect("a tile");
1408 assert_eq!((tile.width, tile.height), (8, 16));
1409 assert_eq!(tile.pixels[0], crate::sixel::solid(0xFF0000));
1410 assert!(s.row(4)[6].tile != 0 && s.row(2)[7].tile == 0);
1411 assert_eq!(s.row(4)[5].ch, b'B');
1412 assert_eq!(s.row(4)[5].tile, 0, "text replaces the picture");
1413 assert_eq!(t.screen.cursor(), (6, 4));
1414
1415 t.feed(b"\x1b[?2;1S\x1b[?1;1S\x1b[<c");
1417 assert_eq!(t.take_replies(), b"\x1b[?2;0;640;400S\x1b[?1;0;256S\x1b[<0;1;2;3;4;5;6;7c");
1418 t.feed(b"\x1b[=1n\x1b[=2n\x1b[=3n\x1b[=7n");
1419 assert_eq!(t.take_replies(), b"\x1b[=1;43;99;0;0;0;0n\x1b[=2;7;25n\x1b[=3;16;8n\x1b[=7n");
1420 }
1421
1422 #[test]
1423 fn sixel_pictures_scroll_the_screen() {
1424 let mut t = term();
1425 t.feed(b"\x1b[25;1HX\x1bPq\"1;1;8;32\x1b\\");
1426 assert_eq!(line(&t, 22), "X");
1429 assert!(t.screen.row(22)[1].tile != 0 && t.screen.row(23)[1].tile != 0);
1430 assert_eq!(t.screen.cursor(), (1, 24));
1431 t.feed(b"\x1b[?80h\x1b[10;10H\x1bPq\"1;1;8;16\x1b\\");
1433 assert!(t.screen.row(0)[0].tile != 0);
1434 assert_eq!(t.screen.cursor(), (9, 9));
1435 }
1436
1437 #[test]
1438 fn waiting_for_a_sound_holds_the_text() {
1439 let mut t = term();
1440 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\\");
1442 assert!(matches!(t.take_events()[..], [TermEvent::Audio(crate::audio::AudioOp::Queue { channel: 2, .. })]));
1443 t.feed(b"before\x1b_SyncTERM:A;Wait;C=2\x1b\\after");
1444 t.feed(b" more");
1445 assert_eq!(line(&t, 0), "before");
1446 let at = t.next_poll().expect("a time to read again");
1447 t.poll(at);
1448 assert_eq!(line(&t, 0), "beforeafter more");
1449 t.feed(b"\x1b[=7;2n\x1b_SyncTERM:A;Update;C=2\x1b\\");
1451 t.poll(at + std::time::Duration::from_secs(1));
1452 let replies = String::from_utf8(t.take_replies()).unwrap();
1453 assert!(replies.ends_with("\x1b[=7;2;0n"), "{replies:?}");
1454 t.feed(b"\x1b_SyncTERM:Q;libsndfileFormat;0x20;100\x1b\\");
1455 assert_eq!(t.take_replies(), b"\x1b[=7;101;32;100;1n");
1456 }
1457
1458 #[test]
1459 fn loadable_fonts() {
1460 use data_encoding::BASE64;
1461 let mut t = term();
1462 let font = vec![0xFFu8; 4096];
1464 let dcs = format!("\x1bPCTerm:Font:43:{}\x1b\\\x1b[0;43 D", BASE64.encode(&font));
1465 t.feed(dcs.as_bytes());
1466 let f = t.screen.look.loaded_font(43).expect("loaded");
1467 assert_eq!((f.height, f.data.len()), (16, 4096));
1468 assert_eq!(t.screen.look.fonts[0], 43);
1469 let mut old = b"\x1b[=44;2{".to_vec();
1471 old.extend(vec![0x18u8; 2048]);
1472 old.extend(b"ok");
1473 t.feed(&old);
1474 assert_eq!(t.screen.look.loaded_font(44).unwrap().height, 8);
1475 assert!(line(&t, 0).ends_with("ok"), "the font's bytes aren't text: {:?}", line(&t, 0));
1476 t.feed(format!("\x1bPCTerm:Font:5:{}\x1b\\\x1bc", BASE64.encode(&font)).as_bytes());
1478 assert!(t.screen.look.loaded_font(5).is_none());
1479 assert!(t.screen.look.loaded_font(43).is_some());
1480 t.feed(dcs.as_bytes());
1482 t.feed(dcs.replace(":43:", ":45:").as_bytes());
1483 let (a, b) = (t.screen.look.loaded_font(43).unwrap(), t.screen.look.loaded_font(45).unwrap());
1484 assert!(std::ptr::eq(a.data, b.data));
1485 }
1486
1487 #[test]
1488 fn typing_in_the_bbs_code_page() {
1489 let mut t = term();
1490 assert_eq!(t.encode_text("é"), [0x82]);
1491 t.feed(b"\x1b[0;25 D");
1493 assert_eq!(t.encode_text("Да"), [0x84, 0xA0]);
1494 assert_eq!(t.paste("Ж\r"), [0x86, b'\r']);
1495 t.encoding = Encoding::Utf8;
1497 assert!(t.codepage().is_none());
1498 }
1499
1500 #[test]
1501 fn bracketed_paste() {
1502 let mut t = term();
1503 assert_eq!(t.paste("hi\r"), b"hi\r");
1504 t.feed(b"\x1b[?2004h");
1505 assert_eq!(t.paste("a\x1b[201~b"), b"\x1b[200~a[201~b\x1b[201~");
1506 t.feed(b"\x1bc");
1507 assert!(!t.bracketed_paste);
1508 }
1509
1510 #[test]
1511 fn mouse_modes() {
1512 let mut t = term();
1513 t.feed(b"\x1b[?1003h\x1b[?1006h");
1514 assert!(t.mouse.any && t.mouse.sgr && t.mouse.tracking());
1515 t.feed(b"\x1b[=2n");
1516 assert_eq!(t.take_replies(), b"\x1b[=2;7;25;1003;1006n");
1517 t.feed(b"\x1b[?1003l\x1b[?1006l");
1518 assert!(!t.mouse.tracking());
1519 }
1520
1521 #[test]
1522 fn reset_returns_to_the_entrys_ice_setting() {
1523 let mut t = Terminal::new(80, 25, 0);
1524 t.screen.base_ice = true;
1525 t.feed(b"\x1b[?33l");
1526 assert!(!t.screen.ice_colors);
1527 t.feed(b"\x1bc");
1528 assert!(t.screen.ice_colors);
1529 }
1530}