Skip to main content

badterm_core/
ansi.rs

1//! ANSI-BBS / ANSI X3.64 terminal emulation.
2//!
3//! [`Terminal`] consumes the byte stream from the remote system, drives a
4//! [`Screen`], and collects any replies (cursor position reports, etc.) that
5//! must be sent back.
6
7use 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/// How incoming bytes are interpreted: the character set, and the
15/// emulations that go with it.
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "lowercase")]
18pub enum Encoding {
19    #[default]
20    Cp437,
21    Utf8,
22    /// Commodore 64 (see [`crate::petscii`]).
23    Petscii,
24    /// Atari 8-bit (see [`crate::atascii`]).
25    Atascii,
26    /// CP437 with AVT/0+ codes as well as ANSI (see [`crate::avatar`]).
27    Avatar,
28    /// CP437 ANSI with RIPscrip graphics (see [`crate::rip`]).
29    Rip,
30    /// Prestel viewdata (see [`crate::viewdata`]).
31    Viewdata,
32    /// Atari ST: VT-52 with IGS graphics (see [`crate::igs`]).
33    Igs,
34    /// Amiga: ANSI with SkyPix graphics (see [`crate::skypix`]).
35    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    /// A C64 or Atari system, with its own screen size and font.
66    pub fn is_retro(self) -> bool {
67        matches!(self, Encoding::Petscii | Encoding::Atascii)
68    }
69
70    /// The screen such a system has, if it has its own.
71    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    /// Typed or pasted text as the remote expects it. A CR (Enter) becomes
84    /// the system's own end of line.
85    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            // The Amiga's character set is ISO-8859-1.
89            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;
106/// The Device Attributes reply: "CTerm" and the revision whose sequences
107/// BadTerm follows (1.332: the blob, flip and zoom graphics commands).
108pub const CTERM_DA: &[u8] = b"\x1b[=67;84;101;114;109;1;332c";
109use crate::screen::FIRST_LOADABLE_FONT;
110/// A DCS string (a sixel picture) can be large; longer ones are cut off.
111const MAX_DCS: usize = 32 << 20;
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114enum State {
115    Ground,
116    Escape,
117    /// ESC followed by an intermediate byte, e.g. `ESC ( B`; waiting for the final byte.
118    EscIntermediate,
119    Csi,
120    /// OSC: collected for the window title.
121    Osc,
122    /// DCS: collected (sixel graphics).
123    Dcs,
124    /// APC: collected (SyncTERM's commands).
125    Apc,
126    /// SOS, PM: ignored until ST.
127    IgnoreString,
128    /// `CSI = slot ; size {`: this many bytes of a font follow.
129    FontBlock(u8, usize),
130    /// Saw ESC inside a string; `\` terminates it.
131    StringEsc,
132    /// An ANSI music string, up to Ctrl-N.
133    Music,
134}
135
136/// Things the front end may want to react to.
137#[derive(Clone, Debug, PartialEq)]
138pub enum TermEvent {
139    Bell,
140    Title(String),
141    /// ANSI music to play.
142    Music(Vec<Tone>),
143    /// Sound from SyncTERM's audio commands.
144    Audio(crate::audio::AudioOp),
145}
146
147pub struct Terminal {
148    pub screen: Screen,
149    pub encoding: Encoding,
150    /// Treat `ESC [ M` with no parameter as the start of ANSI music, as
151    /// many BBS doors expect, instead of "delete one line". `ESC [ N` and
152    /// `ESC [ |` always start music.
153    pub music_on_csi_m: bool,
154    /// `CSI ? 80 h` (DECSDM): sixel pictures go at the top left of the
155    /// screen and don't scroll it.
156    pub sixel_display_mode: bool,
157    state: State,
158    /// The kind of string being read: OSC, DCS or another.
159    string_kind: State,
160    dcs: Vec<u8>,
161    /// SyncTERM's file cache, pictures and pixel buffers.
162    pub syncterm: crate::syncterm::SyncTerm,
163    /// The mouse reporting the BBS has asked for.
164    pub mouse: crate::mouse::MouseModes,
165    private: u8,
166    intermediate: u8,
167    params: Vec<u32>,
168    param_present: bool,
169    osc: Vec<u8>,
170    utf8_buf: [u8; 4],
171    utf8_len: usize,
172    utf8_need: usize,
173    replies: Vec<u8>,
174    events: Vec<TermEvent>,
175    petscii: crate::petscii::Petscii,
176    atascii: crate::atascii::Atascii,
177    avatar: crate::avatar::Avatar,
178    viewdata: crate::viewdata::Viewdata,
179    /// The RIPscrip canvas, when the encoding is RIP.
180    rip: Option<Box<crate::rip::Rip>>,
181    /// The Atari ST screen, when the encoding is IGS.
182    igs: Option<Box<crate::igs::Igs>>,
183    /// The Amiga screen, when the encoding is SkyPix.
184    skypix: Option<Box<crate::skypix::Skypix>>,
185    /// Nothing has been printed on this line yet (where a RIP line can
186    /// start).
187    line_start: bool,
188    /// Bytes not read yet because the BBS asked to wait for a sound
189    /// (`A;Wait`), until then.
190    held: Vec<u8>,
191}
192
193impl Terminal {
194    pub fn new(cols: usize, rows: usize, scrollback: usize) -> Self {
195        Terminal {
196            screen: Screen::new(cols, rows, scrollback),
197            encoding: Encoding::Cp437,
198            music_on_csi_m: false,
199            sixel_display_mode: false,
200            state: State::Ground,
201            string_kind: State::IgnoreString,
202            dcs: Vec::new(),
203            syncterm: Default::default(),
204            mouse: Default::default(),
205            private: 0,
206            intermediate: 0,
207            params: Vec::with_capacity(MAX_PARAMS),
208            param_present: false,
209            osc: Vec::new(),
210            utf8_buf: [0; 4],
211            utf8_len: 0,
212            utf8_need: 0,
213            replies: Vec::new(),
214            events: Vec::new(),
215            petscii: Default::default(),
216            atascii: Default::default(),
217            avatar: Default::default(),
218            viewdata: Default::default(),
219            rip: None,
220            igs: None,
221            skypix: None,
222            line_start: true,
223            held: Vec::new(),
224        }
225    }
226
227    /// Set up the screen for the encoding: a C64 or Atari system gets that
228    /// machine's colors and font.
229    pub fn start_emulation(&mut self) {
230        match self.encoding {
231            Encoding::Petscii => self.petscii.start(&mut self.screen),
232            Encoding::Atascii => self.atascii.start(&mut self.screen),
233            Encoding::Rip => self.rip = Some(Box::default()),
234            Encoding::Viewdata => self.viewdata.start(&mut self.screen),
235            Encoding::Igs => self.igs = Some(Box::default()),
236            Encoding::Skypix => self.skypix = Some(Box::default()),
237            _ => {}
238        }
239    }
240
241    /// The RIPscrip canvas, for a RIP system.
242    pub fn rip(&self) -> Option<&crate::rip::Rip> {
243        self.rip.as_deref()
244    }
245
246    pub fn rip_mut(&mut self) -> Option<&mut crate::rip::Rip> {
247        self.rip.as_deref_mut()
248    }
249
250    /// The Atari ST screen, for an IGS system.
251    pub fn igs(&self) -> Option<&crate::igs::Igs> {
252        self.igs.as_deref()
253    }
254
255    pub fn igs_mut(&mut self) -> Option<&mut crate::igs::Igs> {
256        self.igs.as_deref_mut()
257    }
258
259    /// The Amiga screen, for a SkyPix system.
260    pub fn skypix(&self) -> Option<&crate::skypix::Skypix> {
261        self.skypix.as_deref()
262    }
263
264    /// After SkyPix has drawn: its replies, bells and cursor.
265    fn skypix_done(&mut self) {
266        let Some(sky) = self.skypix.as_deref_mut() else { return };
267        self.replies.append(&mut sky.replies);
268        for _ in 0..std::mem::take(&mut sky.bells) {
269            self.events.push(TermEvent::Bell);
270        }
271        let (x, y) = sky.cursor();
272        self.screen.move_to(x, y);
273        self.screen.cursor_visible = sky.cursor_on;
274    }
275
276    /// After IGS has drawn: its replies, bells and cursor.
277    fn igs_done(&mut self) {
278        let Some(igs) = self.igs.as_deref_mut() else { return };
279        self.replies.append(&mut igs.replies);
280        for _ in 0..std::mem::take(&mut igs.bells) {
281            self.events.push(TermEvent::Bell);
282        }
283        let (x, y) = igs.cursor();
284        self.screen.move_to(x, y);
285        self.screen.cursor_visible = igs.cursor_on;
286    }
287
288    /// Bytes the terminal wants sent to the remote (e.g. CPR replies).
289    pub fn take_replies(&mut self) -> Vec<u8> {
290        std::mem::take(&mut self.replies)
291    }
292
293    pub fn take_events(&mut self) -> Vec<TermEvent> {
294        std::mem::take(&mut self.events)
295    }
296
297    pub fn feed(&mut self, data: &[u8]) {
298        match self.encoding {
299            Encoding::Petscii => {
300                for &b in data {
301                    self.petscii.byte(&mut self.screen, b, &mut self.events);
302                }
303            }
304            Encoding::Atascii => {
305                for &b in data {
306                    self.atascii.byte(&mut self.screen, b, &mut self.events);
307                }
308            }
309            Encoding::Viewdata => self.viewdata.feed(&mut self.screen, data, &mut self.events),
310            Encoding::Igs => {
311                if let Some(igs) = self.igs.as_deref_mut() {
312                    igs.feed(data);
313                }
314                self.igs_done();
315            }
316            Encoding::Skypix => {
317                if let Some(sky) = self.skypix.as_deref_mut() {
318                    sky.feed(data);
319                }
320                self.skypix_done();
321            }
322            _ => {
323                if self.syncterm.hold.is_some() {
324                    self.held.extend_from_slice(data);
325                    return;
326                }
327                for (i, &b) in data.iter().enumerate() {
328                    self.byte(b);
329                    if self.syncterm.hold.is_some() {
330                        self.held.extend_from_slice(&data[i + 1..]);
331                        break;
332                    }
333                }
334            }
335        }
336    }
337
338    /// Time passing: sound reports the BBS asked for, and reading again
339    /// after a wait for a sound.
340    pub fn poll(&mut self, now: std::time::Instant) {
341        self.syncterm.audio.poll(now, &mut self.replies);
342        if let Some(igs) = self.igs.as_deref_mut().filter(|g| g.hold.is_some()) {
343            igs.poll(now);
344            self.igs_done();
345        }
346        if let Some(sky) = self.skypix.as_deref_mut().filter(|s| s.hold.is_some()) {
347            sky.poll(now);
348            self.skypix_done();
349        }
350        if self.syncterm.hold.is_some_and(|t| now >= t) {
351            self.syncterm.hold = None;
352            let held = std::mem::take(&mut self.held);
353            self.feed(&held);
354        }
355    }
356
357    /// A key was pressed: an IGS pause or color cycle stops for it.
358    pub fn interrupt(&mut self) {
359        if let Some(igs) = self.igs.as_deref_mut() {
360            igs.interrupt();
361            self.igs_done();
362        }
363    }
364
365    /// When [`Terminal::poll`] has something to do.
366    pub fn next_poll(&self) -> Option<std::time::Instant> {
367        let igs = self.igs.as_ref().and_then(|g| g.hold);
368        let sky = self.skypix.as_ref().and_then(|s| s.hold);
369        [self.syncterm.hold, self.syncterm.audio.next_due(), igs, sky].into_iter().flatten().min()
370    }
371
372    fn byte(&mut self, b: u8) {
373        if self.state == State::Ground {
374            let taken = match self.encoding {
375                Encoding::Avatar => self.avatar_byte(b),
376                Encoding::Rip => self.rip_byte(b),
377                _ => false,
378            };
379            if taken {
380                return;
381            }
382        }
383        // CAN and SUB abort any sequence in progress (ECMA-48).
384        if self.state != State::Ground && !matches!(self.state, State::FontBlock(..)) && (b == 0x18 || b == 0x1A) {
385            self.state = State::Ground;
386            return;
387        }
388        match self.state {
389            State::Ground => self.ground(b),
390            State::Escape => self.escape(b),
391            State::EscIntermediate => self.state = State::Ground,
392            State::Csi => self.csi(b),
393            State::Osc | State::IgnoreString => match b {
394                0x07 if self.state == State::Osc => self.finish_string(),
395                0x1B => self.state = State::StringEsc,
396                _ if self.state == State::Osc && self.osc.len() < MAX_STRING => self.osc.push(b),
397                _ => {}
398            },
399            State::FontBlock(slot, len) => {
400                self.dcs.push(b);
401                if self.dcs.len() >= len {
402                    self.state = State::Ground;
403                    let font = std::mem::take(&mut self.dcs);
404                    self.screen.look.load_font(slot, &font);
405                }
406            }
407            State::Dcs | State::Apc => match b {
408                0x1B => self.state = State::StringEsc,
409                _ if self.dcs.len() < MAX_DCS => self.dcs.push(b),
410                _ => {}
411            },
412            State::Music => match b {
413                0x0E => {
414                    self.state = State::Ground;
415                    let tones = music::parse(&self.osc);
416                    if !tones.is_empty() {
417                        let ms: u64 = tones.iter().map(|t| t.ms as u64).sum();
418                        self.syncterm.audio.music(std::time::Duration::from_millis(ms));
419                        self.events.push(TermEvent::Music(tones));
420                    }
421                }
422                _ if self.osc.len() < MAX_STRING => self.osc.push(b),
423                // Too long to be music: give up on it.
424                _ => self.state = State::Ground,
425            },
426            State::StringEsc => {
427                if b == b'\\' {
428                    self.finish_string();
429                } else {
430                    // Not ST: abandon the string and treat this as a new escape.
431                    self.state = State::Escape;
432                    self.escape(b);
433                }
434            }
435        }
436    }
437
438    /// AVATAR codes, in the text stream (see [`crate::avatar`]).
439    fn avatar_byte(&mut self, b: u8) -> bool {
440        use crate::avatar::Outcome;
441        match self.avatar.byte(&mut self.screen, b) {
442            Outcome::Pass => false,
443            Outcome::Done => true,
444            Outcome::Show(bytes) => {
445                for b in bytes {
446                    self.byte(b);
447                }
448                true
449            }
450        }
451    }
452
453    /// RIP lines, which start a line with `!|` (see [`crate::rip`]).
454    fn rip_byte(&mut self, b: u8) -> bool {
455        use crate::rip::Feed;
456        let Some(rip) = self.rip.as_mut() else { return false };
457        match rip.byte(&mut self.screen, b, self.line_start) {
458            Feed::Pass => false,
459            Feed::Done => true,
460            Feed::Text(bytes) => {
461                self.line_start = false;
462                for b in bytes {
463                    self.byte(b);
464                }
465                true
466            }
467        }
468    }
469
470    fn ground(&mut self, b: u8) {
471        if self.encoding == Encoding::Utf8 && self.utf8_need > 0 {
472            if b & 0xC0 == 0x80 {
473                self.utf8_buf[self.utf8_len] = b;
474                self.utf8_len += 1;
475                if self.utf8_len == self.utf8_need {
476                    let ch = std::str::from_utf8(&self.utf8_buf[..self.utf8_len]).ok().and_then(|s| s.chars().next());
477                    self.utf8_need = 0;
478                    self.screen.put(ch.and_then(cp437::from_unicode).unwrap_or(b'?'));
479                }
480                return;
481            }
482            // Truncated sequence: show a replacement and handle this byte normally.
483            self.utf8_need = 0;
484            self.screen.put(b'?');
485        }
486        match b {
487            0x00 | 0x7F => {}
488            0x07 => self.events.push(TermEvent::Bell),
489            0x08 => self.screen.backspace(),
490            0x09 => self.screen.tab(),
491            0x0A => {
492                self.screen.line_feed();
493                self.line_start = true;
494            }
495            0x0C => self.screen.erase_display(2),
496            0x0D => {
497                self.screen.carriage_return();
498                self.line_start = true;
499            }
500            0x1B => self.state = State::Escape,
501            _ => match self.encoding {
502                // Like ANSI.SYS, every other byte is a glyph, including the
503                // "graphic" control characters (☺, ♥, ►, ...).
504                Encoding::Cp437
505                | Encoding::Petscii
506                | Encoding::Atascii
507                | Encoding::Avatar
508                | Encoding::Rip
509                | Encoding::Viewdata
510                | Encoding::Igs
511                | Encoding::Skypix => {
512                    if self.avatar.insert {
513                        self.screen.insert_chars(1);
514                    }
515                    self.line_start = false;
516                    self.screen.put(b)
517                }
518                Encoding::Utf8 => match b {
519                    0x01..=0x1F => {}
520                    0x20..=0x7E => self.screen.put(b),
521                    0xC2..=0xDF => self.utf8_start(b, 2),
522                    0xE0..=0xEF => self.utf8_start(b, 3),
523                    0xF0..=0xF4 => self.utf8_start(b, 4),
524                    _ => self.screen.put(b'?'),
525                },
526            },
527        }
528    }
529
530    fn utf8_start(&mut self, b: u8, need: usize) {
531        self.utf8_buf[0] = b;
532        self.utf8_len = 1;
533        self.utf8_need = need;
534    }
535
536    fn escape(&mut self, b: u8) {
537        self.state = State::Ground;
538        match b {
539            b'[' => {
540                self.state = State::Csi;
541                self.private = 0;
542                self.intermediate = 0;
543                self.params.clear();
544                self.param_present = false;
545            }
546            b']' => {
547                self.state = State::Osc;
548                self.string_kind = State::Osc;
549                self.osc.clear();
550            }
551            b'P' => {
552                self.state = State::Dcs;
553                self.string_kind = State::Dcs;
554                self.dcs.clear();
555            }
556            b'_' => {
557                self.state = State::Apc;
558                self.string_kind = State::Apc;
559                self.dcs.clear();
560            }
561            b'X' | b'^' => {
562                self.state = State::IgnoreString;
563                self.string_kind = State::IgnoreString;
564            }
565            0x20..=0x2F => self.state = State::EscIntermediate,
566            b'7' => self.screen.save_cursor(),
567            b'8' => self.screen.restore_cursor(),
568            b'c' => {
569                self.screen.reset();
570                self.mouse = Default::default();
571            }
572            b'D' => self.screen.line_feed(),
573            b'E' => {
574                self.screen.carriage_return();
575                self.screen.line_feed();
576            }
577            b'M' => self.screen.reverse_index(),
578            0x1B => self.state = State::Escape,
579            _ => log::debug!("unhandled ESC {:?}", b as char),
580        }
581    }
582
583    fn finish_string(&mut self) {
584        self.state = State::Ground;
585        match self.string_kind {
586            State::Osc => {}
587            State::Dcs => return self.finish_dcs(),
588            State::Apc => {
589                let apc = std::mem::take(&mut self.dcs);
590                if !self.syncterm.apc(&apc, &mut self.screen, &mut self.replies) {
591                    log::debug!("unhandled APC {:?}", String::from_utf8_lossy(&apc[..apc.len().min(32)]));
592                }
593                self.events.extend(self.syncterm.audio_ops.drain(..).map(TermEvent::Audio));
594                if apc.capacity() <= 1 << 20 {
595                    self.dcs = apc;
596                    self.dcs.clear();
597                }
598                return;
599            }
600            _ => return,
601        }
602        let s = String::from_utf8_lossy(&self.osc).into_owned();
603        let (code, rest) = s.split_once(';').unwrap_or((s.as_str(), ""));
604        match code {
605            // Set the window title.
606            "0" | "2" => self.events.push(TermEvent::Title(rest.to_string())),
607            "4" => self.set_palette(rest),
608            "104" => self.reset_palette(rest),
609            _ => log::debug!("unhandled OSC {code}"),
610        }
611    }
612
613    /// A DCS string: `p1 ; p2 q ...` is a sixel picture.
614    fn finish_dcs(&mut self) {
615        let dcs = std::mem::take(&mut self.dcs);
616        // CTerm's loadable font: `CTerm:Font:slot:base64`.
617        if let Some(rest) = dcs.strip_prefix(b"CTerm:Font:") {
618            let text = String::from_utf8_lossy(rest);
619            if let Some((slot, b64)) = text.split_once(':')
620                && let (Ok(slot), Some(data)) = (slot.trim().parse::<u8>(), crate::syncterm::decode_b64(b64))
621                && !self.screen.look.load_font(slot, &data)
622            {
623                log::debug!("not loading a {}-byte font into slot {slot}", data.len());
624            }
625            return;
626        }
627        let start = dcs.iter().position(|b| !matches!(b, b'0'..=b'9' | b';')).unwrap_or(dcs.len());
628        if dcs.get(start) == Some(&b'q') {
629            let params: Vec<u32> =
630                String::from_utf8_lossy(&dcs[..start]).split(';').map(|p| p.parse().unwrap_or(0)).collect();
631            let background = self.screen.look.rgb(self.screen.pen.bg);
632            let img = crate::sixel::decode(&params, &dcs[start + 1..], background);
633            self.screen.place_image(&img, !self.sixel_display_mode);
634        } else {
635            log::debug!("unhandled DCS {:?}", String::from_utf8_lossy(&dcs[..dcs.len().min(32)]));
636        }
637        // Keep the buffer for the next one unless it was a big picture.
638        if dcs.capacity() <= 1 << 20 {
639            self.dcs = dcs;
640            self.dcs.clear();
641        }
642    }
643
644    /// `OSC 4 ; index ; color [; index ; color ...]`, where a color is
645    /// `rgb:R/G/B` (1-4 hex digits each), `#RRGGBB`, or `?` to ask for it.
646    fn set_palette(&mut self, args: &str) {
647        let mut parts = args.split(';');
648        while let (Some(i), Some(spec)) = (parts.next(), parts.next()) {
649            let Ok(i) = i.trim().parse::<u8>() else { continue };
650            if spec == "?" {
651                let c = self.screen.look.palette[i as usize];
652                let reply = format!("\x1b]4;{i};rgb:{:02x}/{:02x}/{:02x}\x1b\\", c >> 16, (c >> 8) & 0xFF, c & 0xFF);
653                self.replies.extend_from_slice(reply.as_bytes());
654            } else if let Some(c) = parse_color(spec) {
655                self.screen.look.palette[i as usize] = c;
656            }
657        }
658    }
659
660    /// `OSC 104` resets the whole palette; `OSC 104 ; index ...` just those.
661    fn reset_palette(&mut self, args: &str) {
662        let standard = &Look::standard().palette;
663        if args.trim().is_empty() {
664            self.screen.look.palette = *standard;
665            return;
666        }
667        for i in args.split(';').filter_map(|i| i.trim().parse::<u8>().ok()) {
668            self.screen.look.palette[i as usize] = standard[i as usize];
669        }
670    }
671
672    fn csi(&mut self, b: u8) {
673        match b {
674            b'0'..=b'9' => {
675                if self.params.is_empty() {
676                    self.params.push(0);
677                }
678                let p = self.params.last_mut().unwrap();
679                *p = p.saturating_mul(10).saturating_add((b - b'0') as u32);
680                self.param_present = true;
681            }
682            b';' | b':' => {
683                if self.params.is_empty() {
684                    self.params.push(0);
685                }
686                if self.params.len() < MAX_PARAMS {
687                    self.params.push(0);
688                }
689                self.param_present = true;
690            }
691            0x3C..=0x3F => {
692                if !self.param_present && self.private == 0 {
693                    self.private = b;
694                }
695            }
696            // `ESC [ !` asks whether this is a RIPscrip terminal.
697            b'!' if self.encoding == Encoding::Rip && self.params.is_empty() && self.private == 0 => {
698                self.state = State::Ground;
699                self.replies.extend_from_slice(crate::rip::VERSION_REPLY);
700            }
701            0x20..=0x2F => self.intermediate = b,
702            0x40..=0x7E => {
703                self.state = State::Ground;
704                self.dispatch_csi(b);
705            }
706            // C0 controls are executed even in the middle of a sequence.
707            0x00..=0x1F => {
708                if b == 0x1B {
709                    self.state = State::Escape;
710                } else {
711                    self.ground(b);
712                }
713            }
714            _ => self.state = State::Ground,
715        }
716    }
717
718    /// Parameter `i`, with 0 or missing replaced by `default`.
719    fn p(&self, i: usize, default: u32) -> u32 {
720        match self.params.get(i) {
721            Some(&v) if v != 0 => v,
722            _ => default,
723        }
724    }
725
726    fn dispatch_csi(&mut self, b: u8) {
727        if self.intermediate != 0 {
728            // CTerm font selection: `CSI slot ; font SP D`.
729            if self.intermediate == b' ' && b == b'D' && self.private == 0 {
730                let slot = self.params.first().copied().unwrap_or(0) as usize;
731                let font = self.params.get(1).copied().unwrap_or(0);
732                if slot < 4 && font <= 255 {
733                    self.screen.look.fonts[slot] = font as u8;
734                }
735                return;
736            }
737            log::debug!("unhandled CSI with intermediate {:?} {:?}", self.intermediate as char, b as char);
738            return;
739        }
740        match self.private {
741            0 => {}
742            b'?' if b == b'S' => return self.graphics_attributes(),
743            b'?' => return self.dec_mode(b),
744            b'=' if b == b'n' => return self.cterm_report(),
745            // CTerm's older way to load a font: the font's bytes follow.
746            b'=' if b == b'{' => {
747                let slot = self.params.first().copied().unwrap_or(255).min(255) as u8;
748                let len = match self.params.get(1).copied().unwrap_or(0) {
749                    1 => 3584,
750                    2 => 2048,
751                    _ => 4096,
752                };
753                self.dcs.clear();
754                self.state = State::FontBlock(slot, len);
755                return;
756            }
757            // CTerm Device Attributes: what this CTerm-like terminal can do.
758            b'<' if b == b'c' && self.params.first().is_none_or(|&v| v == 0) => {
759                return self.replies.extend_from_slice(b"\x1b[<0;1;2;3;4;5;6;7c");
760            }
761            _ => {
762                log::debug!("unhandled private CSI {:?}{:?} {:?}", self.private as char, b as char, self.params);
763                return;
764            }
765        }
766        let no_params = self.params.is_empty();
767        if b == b'|' || no_params && (b == b'N' || b == b'M' && self.music_on_csi_m) {
768            self.state = State::Music;
769            self.osc.clear();
770            return;
771        }
772        let n = self.p(0, 1) as usize;
773        let s = &mut self.screen;
774        match b {
775            b'@' => s.insert_chars(n),
776            b'A' => s.move_by(0, -(n as isize)),
777            b'B' | b'e' => s.move_by(0, n as isize),
778            b'C' | b'a' => s.move_by(n as isize, 0),
779            b'D' => s.move_by(-(n as isize), 0),
780            b'E' => {
781                s.move_by(0, n as isize);
782                s.carriage_return();
783            }
784            b'F' => {
785                s.move_by(0, -(n as isize));
786                s.carriage_return();
787            }
788            b'G' | b'`' => {
789                let y = s.cursor().1;
790                s.move_to(n - 1, y);
791            }
792            b'd' => {
793                let x = s.cursor().0;
794                s.move_to(x, n - 1);
795            }
796            b'H' | b'f' => {
797                let (row, col) = (self.p(0, 1) as usize, self.p(1, 1) as usize);
798                self.screen.move_to(col - 1, row - 1);
799            }
800            b'J' => s.erase_display(self.params.first().copied().unwrap_or(0)),
801            b'K' => s.erase_line(self.params.first().copied().unwrap_or(0)),
802            b'L' => s.insert_lines(n),
803            b'M' => s.delete_lines(n),
804            b'P' => s.delete_chars(n),
805            b'S' => s.scroll_up(n),
806            b'T' => s.scroll_down(n),
807            b'X' => s.erase_chars(n),
808            b'Z' => s.back_tab(n),
809            // Device Attributes: CTerm, SyncTERM's emulation, which BBSes
810            // look for before sending its pictures and sounds; what it can
811            // do is asked next (`CSI < c`), and answered truthfully.
812            b'c' if self.params.first().is_none_or(|&v| v == 0) => self.replies.extend_from_slice(CTERM_DA),
813            b'm' => self.sgr(),
814            b'n' => match self.params.first().copied().unwrap_or(0) {
815                5 => self.replies.extend_from_slice(b"\x1b[0n"),
816                6 => {
817                    let (x, y) = self.screen.cursor();
818                    self.replies.extend_from_slice(format!("\x1b[{};{}R", y + 1, x + 1).as_bytes());
819                }
820                _ => {}
821            },
822            b'r' => {
823                let rows = self.screen.rows() as u32;
824                let (top, bottom) = (self.p(0, 1), self.p(1, rows));
825                self.screen.set_scroll_region(top as usize - 1, bottom as usize - 1);
826            }
827            b's' => s.save_cursor(),
828            b'u' => s.restore_cursor(),
829            // CTerm/PabloDraw 24-bit color: CSI 0;r;g;b t (bg) / CSI 1;r;g;b t (fg).
830            b't' if self.params.len() == 4 => {
831                let c = Color::Rgb(rgb(self.params[1] as u8, self.params[2] as u8, self.params[3] as u8));
832                match self.params[0] {
833                    0 => self.screen.pen.bg = c,
834                    1 => self.screen.pen.fg = c,
835                    _ => {}
836                }
837            }
838            b'h' | b'l' => {}
839            _ => log::debug!("unhandled CSI {:?} {:?}", b as char, self.params),
840        }
841    }
842
843    /// CTerm's state reports (`CSI = Ps n`).
844    fn cterm_report(&mut self) {
845        let reply = match self.params.first().copied().unwrap_or(1) {
846            // Fonts: the first loadable slot, the last selection's result
847            // (none yet), and the four slots' fonts.
848            1 => {
849                let f = self.screen.look.fonts;
850                format!("\x1b[=1;{FIRST_LOADABLE_FONT};99;{};{};{};{}n", f[0], f[1], f[2], f[3])
851            }
852            // The DEC modes that are set.
853            2 => {
854                let s = &self.screen;
855                let l = &s.look;
856                let set = [
857                    (7, s.autowrap),
858                    (25, s.cursor_visible),
859                    (31, l.bright_font),
860                    (32, l.bright_plain),
861                    (33, s.ice_colors),
862                    (34, l.blink_font),
863                    (35, l.blink_steady),
864                    (80, self.sixel_display_mode),
865                ];
866                let mut modes: Vec<String> = set.iter().filter(|m| m.1).map(|m| m.0.to_string()).collect();
867                modes.extend(self.mouse.set_modes().iter().map(|m| m.to_string()));
868                format!("\x1b[=2;{}n", modes.join(";"))
869            }
870            // A cell's height and width in pixels.
871            3 => {
872                let (w, h) = self.screen.cell_px();
873                format!("\x1b[=3;{h};{w}n")
874            }
875            // No last column flag, no OSC 8 links.
876            4 => "\x1b[=4;0n".to_string(),
877            5 => "\x1b[=5;0n".to_string(),
878            6 => "\x1b[=6;0n".to_string(),
879            // Audio: the channels playing.
880            7 => self.syncterm.audio.report(self.params.get(1).map(|&c| c as usize)),
881            _ => return,
882        };
883        self.replies.extend_from_slice(reply.as_bytes());
884    }
885
886    /// XTSMGRAPHICS (`CSI ? Pi ; Pa S`): the number of sixel color
887    /// registers (1) and the graphics size (2), read only.
888    fn graphics_attributes(&mut self) {
889        let reply = match (self.params.first().copied(), self.params.get(1).copied()) {
890            (Some(1), Some(1 | 4)) => "\x1b[?1;0;256S".to_string(),
891            (Some(2), Some(1 | 4)) => {
892                let (w, h) = self.screen.size_px();
893                format!("\x1b[?2;0;{w};{h}S")
894            }
895            (Some(i @ 1..=3), _) => format!("\x1b[?{i};3;0S"),
896            (Some(i), _) => format!("\x1b[?{i};1;0S"),
897            (None, _) => return,
898        };
899        self.replies.extend_from_slice(reply.as_bytes());
900    }
901
902    fn dec_mode(&mut self, b: u8) {
903        let on = match b {
904            b'h' => true,
905            b'l' => false,
906            _ => return,
907        };
908        for &mode in &self.params {
909            if self.mouse.set(mode, on) {
910                continue;
911            }
912            match mode {
913                7 => self.screen.autowrap = on,
914                80 => self.sixel_display_mode = on,
915                25 => self.screen.cursor_visible = on,
916                // CTerm: blink attribute selects bright background (iCE colors).
917                // CTerm: bright and blink select fonts, and need not
918                // brighten or blink.
919                31 => self.screen.look.bright_font = on,
920                32 => self.screen.look.bright_plain = on,
921                33 => self.screen.ice_colors = on,
922                34 => self.screen.look.blink_font = on,
923                35 => self.screen.look.blink_steady = on,
924                _ => log::debug!("unhandled DEC mode {mode} {on}"),
925            }
926        }
927    }
928
929    fn sgr(&mut self) {
930        if self.params.is_empty() {
931            self.params.push(0);
932        }
933        let pen = &mut self.screen.pen;
934        let mut i = 0;
935        while i < self.params.len() {
936            let p = self.params[i];
937            match p {
938                0 => *pen = Cell { ch: b' ', ..Cell::default() },
939                1 => pen.flags = (pen.flags | flags::BOLD) & !flags::FAINT,
940                2 => pen.flags = (pen.flags | flags::FAINT) & !flags::BOLD,
941                4 => pen.flags |= flags::UNDERLINE,
942                5 | 6 => pen.flags |= flags::BLINK,
943                7 => pen.flags |= flags::REVERSE,
944                8 => pen.flags |= flags::CONCEAL,
945                21 | 22 => pen.flags &= !(flags::BOLD | flags::FAINT),
946                24 => pen.flags &= !flags::UNDERLINE,
947                25 => pen.flags &= !flags::BLINK,
948                27 => pen.flags &= !flags::REVERSE,
949                28 => pen.flags &= !flags::CONCEAL,
950                30..=37 => pen.fg = Color::Pc(ANSI_TO_VGA[(p - 30) as usize]),
951                39 => pen.fg = DEFAULT_FG,
952                40..=47 => pen.bg = Color::Pc(ANSI_TO_VGA[(p - 40) as usize]),
953                49 => pen.bg = DEFAULT_BG,
954                90..=97 => pen.fg = Color::Pc(ANSI_TO_VGA[(p - 90) as usize] + 8),
955                100..=107 => pen.bg = Color::Pc(ANSI_TO_VGA[(p - 100) as usize] + 8),
956                38 | 48 => {
957                    let color = match self.params.get(i + 1) {
958                        Some(5) => {
959                            let c = self.params.get(i + 2).map(|&v| Color::Xterm(v as u8));
960                            i += 2;
961                            c
962                        }
963                        Some(2) => {
964                            let c = (i + 4 < self.params.len()).then(|| {
965                                let v = &self.params[i + 2..i + 5];
966                                Color::Rgb(rgb(v[0] as u8, v[1] as u8, v[2] as u8))
967                            });
968                            i += 4;
969                            c
970                        }
971                        _ => None,
972                    };
973                    if let Some(c) = color {
974                        if p == 38 { pen.fg = c } else { pen.bg = c }
975                    }
976                }
977                _ => log::debug!("unhandled SGR {p}"),
978            }
979            i += 1;
980        }
981    }
982}
983
984/// An X11-style color: `rgb:R/G/B` with 1-4 hex digits per channel, or
985/// `#RRGGBB`.
986fn parse_color(spec: &str) -> Option<crate::palette::Rgb> {
987    let spec = spec.trim();
988    if let Some(hex) = spec.strip_prefix('#') {
989        return (hex.len() == 6).then(|| u32::from_str_radix(hex, 16).ok()).flatten();
990    }
991    let mut channels = spec.strip_prefix("rgb:")?.split('/').map(|c| {
992        let v = u32::from_str_radix(c, 16).ok().filter(|_| (1..=4).contains(&c.len()))?;
993        // Scale to 8 bits: "f" and "ffff" are both full.
994        Some((v * 255 / ((1 << (4 * c.len())) - 1)) as u8)
995    });
996    let (r, g, b) = (channels.next()??, channels.next()??, channels.next()??);
997    channels.next().is_none().then(|| rgb(r, g, b))
998}
999
1000#[cfg(test)]
1001mod tests {
1002    use super::*;
1003
1004    fn term() -> Terminal {
1005        Terminal::new(80, 25, 100)
1006    }
1007
1008    fn line(t: &Terminal, y: usize) -> String {
1009        Screen::row_text(t.screen.row(y))
1010    }
1011
1012    #[test]
1013    fn cursor_position_and_text() {
1014        let mut t = term();
1015        t.feed(b"\x1b[3;5HHi");
1016        assert_eq!(line(&t, 2), "    Hi");
1017        assert_eq!(t.screen.cursor(), (6, 2));
1018    }
1019
1020    #[test]
1021    fn sgr_colors() {
1022        let mut t = term();
1023        t.feed(b"\x1b[1;31;44mX");
1024        let c = t.screen.row(0)[0];
1025        assert_eq!(c.fg, Color::Pc(4));
1026        assert_eq!(c.bg, Color::Pc(1));
1027        assert!(c.flags & flags::BOLD != 0);
1028        t.feed(b"\x1b[0;38;2;10;20;30mY");
1029        assert_eq!(t.screen.row(0)[1].fg, Color::Rgb(0x0A141E));
1030        t.feed(b"\x1b[38;5;196mZ");
1031        assert_eq!(t.screen.row(0)[2].fg, Color::Xterm(196));
1032    }
1033
1034    #[test]
1035    fn cursor_position_report() {
1036        let mut t = term();
1037        t.feed(b"\x1b[10;20H\x1b[6n");
1038        assert_eq!(t.take_replies(), b"\x1b[10;20R");
1039    }
1040
1041    #[test]
1042    fn device_attributes() {
1043        let mut t = term();
1044        t.feed(b"\x1b[c\x1b[0c");
1045        assert_eq!(t.take_replies(), [CTERM_DA, CTERM_DA].concat());
1046        // Secondary DA (`CSI > c`) and `CSI 1 c` are not answered.
1047        t.feed(b"\x1b[>c\x1b[1c");
1048        assert!(t.take_replies().is_empty());
1049    }
1050
1051    #[test]
1052    fn ice_mode_toggle() {
1053        let mut t = term();
1054        t.feed(b"\x1b[?33h");
1055        assert!(t.screen.ice_colors);
1056        t.feed(b"\x1b[?33l");
1057        assert!(!t.screen.ice_colors);
1058    }
1059
1060    #[test]
1061    fn cp437_glyphs_and_utf8() {
1062        let mut t = term();
1063        t.feed(&[0xDB, 0x03]);
1064        assert_eq!(t.screen.row(0)[0].ch, 0xDB);
1065        assert_eq!(t.screen.row(0)[1].ch, 0x03);
1066        t.encoding = Encoding::Utf8;
1067        t.feed("ۂ".as_bytes());
1068        assert_eq!(t.screen.row(0)[2].ch, 0xDB);
1069        assert_eq!(t.screen.row(0)[3].ch, 0x82);
1070    }
1071
1072    #[test]
1073    fn osc_title_and_strings_ignored() {
1074        let mut t = term();
1075        t.feed(b"\x1b]0;My BBS\x07\x1bPjunk\x1b\\ok");
1076        assert_eq!(t.take_events(), vec![TermEvent::Title("My BBS".into())]);
1077        assert_eq!(line(&t, 0), "ok");
1078    }
1079
1080    #[test]
1081    fn ansi_art_line_of_exactly_80_columns() {
1082        let mut t = term();
1083        let mut data = vec![b'#'; 80];
1084        data.extend_from_slice(b"\r\nnext");
1085        t.feed(&data);
1086        assert_eq!(line(&t, 1), "next");
1087    }
1088
1089    #[test]
1090    fn save_restore_cursor() {
1091        let mut t = term();
1092        t.feed(b"\x1b[5;5H\x1b[s\x1b[1;1H\x1b[uX");
1093        assert_eq!(t.screen.row(4)[4].ch, b'X');
1094    }
1095
1096    #[test]
1097    fn ansi_music() {
1098        let mut t = term();
1099        t.feed(b"Hi\x1b[NMLO3A\x0e there\x1b[|MLO3A8\x0e");
1100        assert_eq!(line(&t, 0), "Hi there");
1101        let a = Tone { hz: 440, ms: 500 };
1102        assert_eq!(t.take_events(), [TermEvent::Music(vec![a]), TermEvent::Music(vec![Tone { ms: 250, ..a }])]);
1103
1104        // ESC [ M is delete-line unless music is wanted there.
1105        t.feed(b"\x1b[2;1Hx\x1b[1;1H\x1b[M");
1106        assert_eq!(line(&t, 0), "x");
1107        t.music_on_csi_m = true;
1108        t.feed(b"\x1b[MMLO3A\x0e\x1b[1M");
1109        assert_eq!(t.take_events(), [TermEvent::Music(vec![a])]);
1110        assert_eq!(line(&t, 0), "");
1111    }
1112
1113    #[test]
1114    fn cterm_palette_and_fonts() {
1115        let mut t = Terminal::new(80, 25, 0);
1116        // Red (ANSI 1, VGA 4) becomes orange; bright blue (ANSI 12) too.
1117        t.feed(b"\x1b]4;1;rgb:ff/80/00;12;#123456\x1b\\");
1118        assert_eq!(t.screen.look.palette[1], 0xFF8000);
1119        assert_eq!(t.screen.look.rgb(Color::Pc(4)), 0xFF8000);
1120        assert_eq!(t.screen.look.rgb(Color::Pc(9)), 0x123456);
1121        t.feed(b"\x1b[31mX");
1122        assert_eq!(t.screen.row(0)[0].resolve_with(false, &t.screen.look).0, 0xFF8000);
1123        // Queried, then reset.
1124        t.feed(b"\x1b]4;1;?\x07");
1125        assert_eq!(t.take_replies(), b"\x1b]4;1;rgb:ff/80/00\x1b\\");
1126        t.feed(b"\x1b]104;1\x07");
1127        assert_eq!(t.screen.look.palette[1], 0xAA0000);
1128        assert_eq!(t.screen.look.palette[12], 0x123456);
1129        t.feed(b"\x1b]104\x07");
1130        assert_eq!(t.screen.look, *Look::standard());
1131
1132        // Font slots, and bright text in slot 1 without brightening.
1133        t.feed(b"\x1b[0;42 D\x1b[1;40 D\x1b[?31h\x1b[?32h\x1b[1;32mB");
1134        let cell = t.screen.row(0)[1];
1135        assert_eq!(t.screen.look.fonts, [42, 40, 0, 0]);
1136        assert_eq!(t.screen.look.font_for(&cell), 40);
1137        assert_eq!(cell.resolve_with(false, &t.screen.look).0, 0x00AA00);
1138        t.feed(b"\x1b[?31l\x1b[?32l");
1139        assert_eq!(t.screen.look.font_for(&cell), 42);
1140        assert_eq!(cell.resolve_with(false, &t.screen.look).0, 0x55FF55);
1141        assert_eq!(parse_color("rgb:f/0/8"), Some(0xFF0088));
1142        assert_eq!(parse_color("rgb:ffff/0000/8080"), Some(0xFF0080));
1143        assert_eq!(parse_color("rgb:1/2"), None);
1144    }
1145
1146    #[test]
1147    fn sixel_pictures() {
1148        let mut t = term();
1149        // 16 x 36 pixels (a pixel per sixel row, six bands), red, at 3;5.
1150        let mut seq = b"\x1b[3;5HA\x1bP0;1;0q\"1;1#1;2;100;0;0".to_vec();
1151        for _ in 0..6 {
1152            seq.extend_from_slice(b"!16~-");
1153        }
1154        seq.extend_from_slice(b"\x1b\\B");
1155        t.feed(&seq);
1156        let s = &t.screen;
1157        // The picture covers columns 5-6 (from the cursor, which was after
1158        // the A) of rows 3-5; the last band ends at pixel 36, so the cursor
1159        // is on the row holding pixel 36 (row 3 + 36 / 16), in the column it
1160        // started in, where the B went.
1161        let tile = s.tiles.get(s.row(2)[5].tile).expect("a tile");
1162        assert_eq!((tile.width, tile.height), (8, 16));
1163        assert_eq!(tile.pixels[0], crate::sixel::solid(0xFF0000));
1164        assert!(s.row(4)[6].tile != 0 && s.row(2)[7].tile == 0);
1165        assert_eq!(s.row(4)[5].ch, b'B');
1166        assert_eq!(s.row(4)[5].tile, 0, "text replaces the picture");
1167        assert_eq!(t.screen.cursor(), (6, 4));
1168
1169        // The size of the screen in pixels, and the color registers.
1170        t.feed(b"\x1b[?2;1S\x1b[?1;1S\x1b[<c");
1171        assert_eq!(t.take_replies(), b"\x1b[?2;0;640;400S\x1b[?1;0;256S\x1b[<0;1;2;3;4;5;6;7c");
1172        t.feed(b"\x1b[=1n\x1b[=2n\x1b[=3n\x1b[=7n");
1173        assert_eq!(t.take_replies(), b"\x1b[=1;43;99;0;0;0;0n\x1b[=2;7;25n\x1b[=3;16;8n\x1b[=7n");
1174    }
1175
1176    #[test]
1177    fn sixel_pictures_scroll_the_screen() {
1178        let mut t = term();
1179        t.feed(b"\x1b[25;1HX\x1bPq\"1;1;8;32\x1b\\");
1180        // 32 pixels: two rows; the screen scrolled two lines (the cursor
1181        // ends on the row holding pixel 32).
1182        assert_eq!(line(&t, 22), "X");
1183        assert!(t.screen.row(22)[1].tile != 0 && t.screen.row(23)[1].tile != 0);
1184        assert_eq!(t.screen.cursor(), (1, 24));
1185        // In display mode it goes at the top left and the cursor stays.
1186        t.feed(b"\x1b[?80h\x1b[10;10H\x1bPq\"1;1;8;16\x1b\\");
1187        assert!(t.screen.row(0)[0].tile != 0);
1188        assert_eq!(t.screen.cursor(), (9, 9));
1189    }
1190
1191    #[test]
1192    fn waiting_for_a_sound_holds_the_text() {
1193        let mut t = term();
1194        // A 100 ms tone, then wait for it before showing "after".
1195        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\\");
1196        assert!(matches!(t.take_events()[..], [TermEvent::Audio(crate::audio::AudioOp::Queue { channel: 2, .. })]));
1197        t.feed(b"before\x1b_SyncTERM:A;Wait;C=2\x1b\\after");
1198        t.feed(b" more");
1199        assert_eq!(line(&t, 0), "before");
1200        let at = t.next_poll().expect("a time to read again");
1201        t.poll(at);
1202        assert_eq!(line(&t, 0), "beforeafter more");
1203        // Its state is asked for and reported when it ends.
1204        t.feed(b"\x1b[=7;2n\x1b_SyncTERM:A;Update;C=2\x1b\\");
1205        t.poll(at + std::time::Duration::from_secs(1));
1206        let replies = String::from_utf8(t.take_replies()).unwrap();
1207        assert!(replies.ends_with("\x1b[=7;2;0n"), "{replies:?}");
1208        t.feed(b"\x1b_SyncTERM:Q;libsndfileFormat;0x20;100\x1b\\");
1209        assert_eq!(t.take_replies(), b"\x1b[=7;101;32;100;1n");
1210    }
1211
1212    #[test]
1213    fn loadable_fonts() {
1214        use data_encoding::BASE64;
1215        let mut t = term();
1216        // An 8x16 font of solid glyphs, by DCS, into slot 43; then chosen.
1217        let font = vec![0xFFu8; 4096];
1218        let dcs = format!("\x1bPCTerm:Font:43:{}\x1b\\\x1b[0;43 D", BASE64.encode(&font));
1219        t.feed(dcs.as_bytes());
1220        let f = t.screen.look.loaded_font(43).expect("loaded");
1221        assert_eq!((f.height, f.data.len()), (16, 4096));
1222        assert_eq!(t.screen.look.fonts[0], 43);
1223        // The older way: CSI = slot ; size { and the bytes (8x8 here).
1224        let mut old = b"\x1b[=44;2{".to_vec();
1225        old.extend(vec![0x18u8; 2048]);
1226        old.extend(b"ok");
1227        t.feed(&old);
1228        assert_eq!(t.screen.look.loaded_font(44).unwrap().height, 8);
1229        assert!(line(&t, 0).ends_with("ok"), "the font's bytes aren't text: {:?}", line(&t, 0));
1230        // Built-in slots can't be replaced; a reset keeps loaded fonts.
1231        t.feed(format!("\x1bPCTerm:Font:5:{}\x1b\\\x1bc", BASE64.encode(&font)).as_bytes());
1232        assert!(t.screen.look.loaded_font(5).is_none());
1233        assert!(t.screen.look.loaded_font(43).is_some());
1234        // The same font twice is kept once.
1235        t.feed(dcs.as_bytes());
1236        t.feed(dcs.replace(":43:", ":45:").as_bytes());
1237        let (a, b) = (t.screen.look.loaded_font(43).unwrap(), t.screen.look.loaded_font(45).unwrap());
1238        assert!(std::ptr::eq(a.data, b.data));
1239    }
1240
1241    #[test]
1242    fn mouse_modes() {
1243        let mut t = term();
1244        t.feed(b"\x1b[?1003h\x1b[?1006h");
1245        assert!(t.mouse.any && t.mouse.sgr && t.mouse.tracking());
1246        t.feed(b"\x1b[=2n");
1247        assert_eq!(t.take_replies(), b"\x1b[=2;7;25;1003;1006n");
1248        t.feed(b"\x1b[?1003l\x1b[?1006l");
1249        assert!(!t.mouse.tracking());
1250    }
1251
1252    #[test]
1253    fn reset_returns_to_the_entrys_ice_setting() {
1254        let mut t = Terminal::new(80, 25, 0);
1255        t.screen.base_ice = true;
1256        t.feed(b"\x1b[?33l");
1257        assert!(!t.screen.ice_colors);
1258        t.feed(b"\x1bc");
1259        assert!(t.screen.ice_colors);
1260    }
1261}