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