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