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