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