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/// Character set used to interpret incoming bytes.
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum Encoding {
18    #[default]
19    Cp437,
20    Utf8,
21    /// Commodore 64 (see [`crate::petscii`]).
22    Petscii,
23    /// Atari 8-bit (see [`crate::atascii`]).
24    Atascii,
25}
26
27impl Encoding {
28    pub const ALL: [Encoding; 4] = [Encoding::Cp437, Encoding::Utf8, Encoding::Petscii, Encoding::Atascii];
29
30    pub fn name(self) -> &'static str {
31        match self {
32            Encoding::Cp437 => "CP437",
33            Encoding::Utf8 => "UTF-8",
34            Encoding::Petscii => "PETSCII",
35            Encoding::Atascii => "ATASCII",
36        }
37    }
38
39    /// A C64 or Atari system, with its own screen size and font.
40    pub fn is_retro(self) -> bool {
41        matches!(self, Encoding::Petscii | Encoding::Atascii)
42    }
43
44    /// The screen such a system has, if it has its own.
45    pub fn screen_size(self) -> Option<(usize, usize)> {
46        match self {
47            Encoding::Petscii => Some((crate::petscii::COLS, crate::petscii::ROWS)),
48            Encoding::Atascii => Some((crate::atascii::COLS, crate::atascii::ROWS)),
49            _ => None,
50        }
51    }
52
53    /// Typed or pasted text as the remote expects it. A CR (Enter) becomes
54    /// the system's own end of line.
55    pub fn encode_text(self, text: &str) -> Vec<u8> {
56        match self {
57            Encoding::Cp437 => cp437::encode(text),
58            Encoding::Utf8 => text.as_bytes().to_vec(),
59            Encoding::Petscii => text
60                .chars()
61                .filter_map(|c| if c == '\r' { Some(0x0D) } else { crate::petscii::encode_char(c) })
62                .collect(),
63            Encoding::Atascii => text
64                .chars()
65                .filter_map(|c| if c == '\r' { Some(0x9B) } else { crate::atascii::encode_char(c) })
66                .collect(),
67        }
68    }
69}
70
71const MAX_PARAMS: usize = 32;
72const MAX_STRING: usize = 4096;
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75enum State {
76    Ground,
77    Escape,
78    /// ESC followed by an intermediate byte, e.g. `ESC ( B`; waiting for the final byte.
79    EscIntermediate,
80    Csi,
81    /// OSC: collected for the window title.
82    Osc,
83    /// DCS, SOS, PM, APC: ignored until ST.
84    IgnoreString,
85    /// Saw ESC inside a string; `\` terminates it.
86    StringEsc,
87    /// An ANSI music string, up to Ctrl-N.
88    Music,
89}
90
91/// Things the front end may want to react to.
92#[derive(Clone, Debug, PartialEq, Eq)]
93pub enum TermEvent {
94    Bell,
95    Title(String),
96    /// ANSI music to play.
97    Music(Vec<Tone>),
98}
99
100pub struct Terminal {
101    pub screen: Screen,
102    pub encoding: Encoding,
103    /// Treat `ESC [ M` with no parameter as the start of ANSI music, as
104    /// many BBS doors expect, instead of "delete one line". `ESC [ N` and
105    /// `ESC [ |` always start music.
106    pub music_on_csi_m: bool,
107    state: State,
108    string_is_osc: bool,
109    private: u8,
110    intermediate: u8,
111    params: Vec<u32>,
112    param_present: bool,
113    osc: Vec<u8>,
114    utf8_buf: [u8; 4],
115    utf8_len: usize,
116    utf8_need: usize,
117    replies: Vec<u8>,
118    events: Vec<TermEvent>,
119    petscii: crate::petscii::Petscii,
120    atascii: crate::atascii::Atascii,
121}
122
123impl Terminal {
124    pub fn new(cols: usize, rows: usize, scrollback: usize) -> Self {
125        Terminal {
126            screen: Screen::new(cols, rows, scrollback),
127            encoding: Encoding::Cp437,
128            music_on_csi_m: false,
129            state: State::Ground,
130            string_is_osc: false,
131            private: 0,
132            intermediate: 0,
133            params: Vec::with_capacity(MAX_PARAMS),
134            param_present: false,
135            osc: Vec::new(),
136            utf8_buf: [0; 4],
137            utf8_len: 0,
138            utf8_need: 0,
139            replies: Vec::new(),
140            events: Vec::new(),
141            petscii: Default::default(),
142            atascii: Default::default(),
143        }
144    }
145
146    /// Set up the screen for the encoding: a C64 or Atari system gets that
147    /// machine's colors and font.
148    pub fn start_emulation(&mut self) {
149        match self.encoding {
150            Encoding::Petscii => self.petscii.start(&mut self.screen),
151            Encoding::Atascii => self.atascii.start(&mut self.screen),
152            _ => {}
153        }
154    }
155
156    /// Bytes the terminal wants sent to the remote (e.g. CPR replies).
157    pub fn take_replies(&mut self) -> Vec<u8> {
158        std::mem::take(&mut self.replies)
159    }
160
161    pub fn take_events(&mut self) -> Vec<TermEvent> {
162        std::mem::take(&mut self.events)
163    }
164
165    pub fn feed(&mut self, data: &[u8]) {
166        match self.encoding {
167            Encoding::Petscii => {
168                for &b in data {
169                    self.petscii.byte(&mut self.screen, b, &mut self.events);
170                }
171            }
172            Encoding::Atascii => {
173                for &b in data {
174                    self.atascii.byte(&mut self.screen, b, &mut self.events);
175                }
176            }
177            _ => {
178                for &b in data {
179                    self.byte(b);
180                }
181            }
182        }
183    }
184
185    fn byte(&mut self, b: u8) {
186        // CAN and SUB abort any sequence in progress (ECMA-48).
187        if self.state != State::Ground && (b == 0x18 || b == 0x1A) {
188            self.state = State::Ground;
189            return;
190        }
191        match self.state {
192            State::Ground => self.ground(b),
193            State::Escape => self.escape(b),
194            State::EscIntermediate => self.state = State::Ground,
195            State::Csi => self.csi(b),
196            State::Osc | State::IgnoreString => match b {
197                0x07 if self.state == State::Osc => self.finish_string(),
198                0x1B => self.state = State::StringEsc,
199                _ if self.state == State::Osc && self.osc.len() < MAX_STRING => self.osc.push(b),
200                _ => {}
201            },
202            State::Music => match b {
203                0x0E => {
204                    self.state = State::Ground;
205                    let tones = music::parse(&self.osc);
206                    if !tones.is_empty() {
207                        self.events.push(TermEvent::Music(tones));
208                    }
209                }
210                _ if self.osc.len() < MAX_STRING => self.osc.push(b),
211                // Too long to be music: give up on it.
212                _ => self.state = State::Ground,
213            },
214            State::StringEsc => {
215                if b == b'\\' {
216                    self.finish_string();
217                } else {
218                    // Not ST: abandon the string and treat this as a new escape.
219                    self.state = State::Escape;
220                    self.escape(b);
221                }
222            }
223        }
224    }
225
226    fn ground(&mut self, b: u8) {
227        if self.encoding == Encoding::Utf8 && self.utf8_need > 0 {
228            if b & 0xC0 == 0x80 {
229                self.utf8_buf[self.utf8_len] = b;
230                self.utf8_len += 1;
231                if self.utf8_len == self.utf8_need {
232                    let ch = std::str::from_utf8(&self.utf8_buf[..self.utf8_len]).ok().and_then(|s| s.chars().next());
233                    self.utf8_need = 0;
234                    self.screen.put(ch.and_then(cp437::from_unicode).unwrap_or(b'?'));
235                }
236                return;
237            }
238            // Truncated sequence: show a replacement and handle this byte normally.
239            self.utf8_need = 0;
240            self.screen.put(b'?');
241        }
242        match b {
243            0x00 | 0x7F => {}
244            0x07 => self.events.push(TermEvent::Bell),
245            0x08 => self.screen.backspace(),
246            0x09 => self.screen.tab(),
247            0x0A => self.screen.line_feed(),
248            0x0C => self.screen.erase_display(2),
249            0x0D => self.screen.carriage_return(),
250            0x1B => self.state = State::Escape,
251            _ => match self.encoding {
252                // Like ANSI.SYS, every other byte is a glyph, including the
253                // "graphic" control characters (☺, ♥, ►, ...).
254                Encoding::Cp437 | Encoding::Petscii | Encoding::Atascii => self.screen.put(b),
255                Encoding::Utf8 => match b {
256                    0x01..=0x1F => {}
257                    0x20..=0x7E => self.screen.put(b),
258                    0xC2..=0xDF => self.utf8_start(b, 2),
259                    0xE0..=0xEF => self.utf8_start(b, 3),
260                    0xF0..=0xF4 => self.utf8_start(b, 4),
261                    _ => self.screen.put(b'?'),
262                },
263            },
264        }
265    }
266
267    fn utf8_start(&mut self, b: u8, need: usize) {
268        self.utf8_buf[0] = b;
269        self.utf8_len = 1;
270        self.utf8_need = need;
271    }
272
273    fn escape(&mut self, b: u8) {
274        self.state = State::Ground;
275        match b {
276            b'[' => {
277                self.state = State::Csi;
278                self.private = 0;
279                self.intermediate = 0;
280                self.params.clear();
281                self.param_present = false;
282            }
283            b']' => {
284                self.state = State::Osc;
285                self.string_is_osc = true;
286                self.osc.clear();
287            }
288            b'P' | b'X' | b'^' | b'_' => {
289                self.state = State::IgnoreString;
290                self.string_is_osc = false;
291            }
292            0x20..=0x2F => self.state = State::EscIntermediate,
293            b'7' => self.screen.save_cursor(),
294            b'8' => self.screen.restore_cursor(),
295            b'c' => self.screen.reset(),
296            b'D' => self.screen.line_feed(),
297            b'E' => {
298                self.screen.carriage_return();
299                self.screen.line_feed();
300            }
301            b'M' => self.screen.reverse_index(),
302            0x1B => self.state = State::Escape,
303            _ => log::debug!("unhandled ESC {:?}", b as char),
304        }
305    }
306
307    fn finish_string(&mut self) {
308        self.state = State::Ground;
309        if !self.string_is_osc {
310            return;
311        }
312        let s = String::from_utf8_lossy(&self.osc).into_owned();
313        let (code, rest) = s.split_once(';').unwrap_or((s.as_str(), ""));
314        match code {
315            // Set the window title.
316            "0" | "2" => self.events.push(TermEvent::Title(rest.to_string())),
317            "4" => self.set_palette(rest),
318            "104" => self.reset_palette(rest),
319            _ => log::debug!("unhandled OSC {code}"),
320        }
321    }
322
323    /// `OSC 4 ; index ; color [; index ; color ...]`, where a color is
324    /// `rgb:R/G/B` (1-4 hex digits each), `#RRGGBB`, or `?` to ask for it.
325    fn set_palette(&mut self, args: &str) {
326        let mut parts = args.split(';');
327        while let (Some(i), Some(spec)) = (parts.next(), parts.next()) {
328            let Ok(i) = i.trim().parse::<u8>() else { continue };
329            if spec == "?" {
330                let c = self.screen.look.palette[i as usize];
331                let reply = format!("\x1b]4;{i};rgb:{:02x}/{:02x}/{:02x}\x1b\\", c >> 16, (c >> 8) & 0xFF, c & 0xFF);
332                self.replies.extend_from_slice(reply.as_bytes());
333            } else if let Some(c) = parse_color(spec) {
334                self.screen.look.palette[i as usize] = c;
335            }
336        }
337    }
338
339    /// `OSC 104` resets the whole palette; `OSC 104 ; index ...` just those.
340    fn reset_palette(&mut self, args: &str) {
341        let standard = &Look::standard().palette;
342        if args.trim().is_empty() {
343            self.screen.look.palette = *standard;
344            return;
345        }
346        for i in args.split(';').filter_map(|i| i.trim().parse::<u8>().ok()) {
347            self.screen.look.palette[i as usize] = standard[i as usize];
348        }
349    }
350
351    fn csi(&mut self, b: u8) {
352        match b {
353            b'0'..=b'9' => {
354                if self.params.is_empty() {
355                    self.params.push(0);
356                }
357                let p = self.params.last_mut().unwrap();
358                *p = p.saturating_mul(10).saturating_add((b - b'0') as u32);
359                self.param_present = true;
360            }
361            b';' | b':' => {
362                if self.params.is_empty() {
363                    self.params.push(0);
364                }
365                if self.params.len() < MAX_PARAMS {
366                    self.params.push(0);
367                }
368                self.param_present = true;
369            }
370            0x3C..=0x3F => {
371                if !self.param_present && self.private == 0 {
372                    self.private = b;
373                }
374            }
375            0x20..=0x2F => self.intermediate = b,
376            0x40..=0x7E => {
377                self.state = State::Ground;
378                self.dispatch_csi(b);
379            }
380            // C0 controls are executed even in the middle of a sequence.
381            0x00..=0x1F => {
382                if b == 0x1B {
383                    self.state = State::Escape;
384                } else {
385                    self.ground(b);
386                }
387            }
388            _ => self.state = State::Ground,
389        }
390    }
391
392    /// Parameter `i`, with 0 or missing replaced by `default`.
393    fn p(&self, i: usize, default: u32) -> u32 {
394        match self.params.get(i) {
395            Some(&v) if v != 0 => v,
396            _ => default,
397        }
398    }
399
400    fn dispatch_csi(&mut self, b: u8) {
401        if self.intermediate != 0 {
402            // CTerm font selection: `CSI slot ; font SP D`.
403            if self.intermediate == b' ' && b == b'D' && self.private == 0 {
404                let slot = self.params.first().copied().unwrap_or(0) as usize;
405                let font = self.params.get(1).copied().unwrap_or(0);
406                if slot < 4 && font <= 255 {
407                    self.screen.look.fonts[slot] = font as u8;
408                }
409                return;
410            }
411            log::debug!("unhandled CSI with intermediate {:?} {:?}", self.intermediate as char, b as char);
412            return;
413        }
414        match self.private {
415            0 => {}
416            b'?' => return self.dec_mode(b),
417            _ => {
418                log::debug!("unhandled private CSI {:?}{:?} {:?}", self.private as char, b as char, self.params);
419                return;
420            }
421        }
422        let no_params = self.params.is_empty();
423        if b == b'|' || no_params && (b == b'N' || b == b'M' && self.music_on_csi_m) {
424            self.state = State::Music;
425            self.osc.clear();
426            return;
427        }
428        let n = self.p(0, 1) as usize;
429        let s = &mut self.screen;
430        match b {
431            b'@' => s.insert_chars(n),
432            b'A' => s.move_by(0, -(n as isize)),
433            b'B' | b'e' => s.move_by(0, n as isize),
434            b'C' | b'a' => s.move_by(n as isize, 0),
435            b'D' => s.move_by(-(n as isize), 0),
436            b'E' => {
437                s.move_by(0, n as isize);
438                s.carriage_return();
439            }
440            b'F' => {
441                s.move_by(0, -(n as isize));
442                s.carriage_return();
443            }
444            b'G' | b'`' => {
445                let y = s.cursor().1;
446                s.move_to(n - 1, y);
447            }
448            b'd' => {
449                let x = s.cursor().0;
450                s.move_to(x, n - 1);
451            }
452            b'H' | b'f' => {
453                let (row, col) = (self.p(0, 1) as usize, self.p(1, 1) as usize);
454                self.screen.move_to(col - 1, row - 1);
455            }
456            b'J' => s.erase_display(self.params.first().copied().unwrap_or(0)),
457            b'K' => s.erase_line(self.params.first().copied().unwrap_or(0)),
458            b'L' => s.insert_lines(n),
459            b'M' => s.delete_lines(n),
460            b'P' => s.delete_chars(n),
461            b'S' => s.scroll_up(n),
462            b'T' => s.scroll_down(n),
463            b'X' => s.erase_chars(n),
464            b'Z' => s.back_tab(n),
465            // Device Attributes: answer as a plain VT100. Claiming CTerm would
466            // invite font and palette sequences that are not supported yet.
467            b'c' if self.params.first().is_none_or(|&v| v == 0) => self.replies.extend_from_slice(b"\x1b[?1;0c"),
468            b'm' => self.sgr(),
469            b'n' => match self.params.first().copied().unwrap_or(0) {
470                5 => self.replies.extend_from_slice(b"\x1b[0n"),
471                6 => {
472                    let (x, y) = self.screen.cursor();
473                    self.replies.extend_from_slice(format!("\x1b[{};{}R", y + 1, x + 1).as_bytes());
474                }
475                _ => {}
476            },
477            b'r' => {
478                let rows = self.screen.rows() as u32;
479                let (top, bottom) = (self.p(0, 1), self.p(1, rows));
480                self.screen.set_scroll_region(top as usize - 1, bottom as usize - 1);
481            }
482            b's' => s.save_cursor(),
483            b'u' => s.restore_cursor(),
484            // CTerm/PabloDraw 24-bit color: CSI 0;r;g;b t (bg) / CSI 1;r;g;b t (fg).
485            b't' if self.params.len() == 4 => {
486                let c = Color::Rgb(rgb(self.params[1] as u8, self.params[2] as u8, self.params[3] as u8));
487                match self.params[0] {
488                    0 => self.screen.pen.bg = c,
489                    1 => self.screen.pen.fg = c,
490                    _ => {}
491                }
492            }
493            b'h' | b'l' => {}
494            _ => log::debug!("unhandled CSI {:?} {:?}", b as char, self.params),
495        }
496    }
497
498    fn dec_mode(&mut self, b: u8) {
499        let on = match b {
500            b'h' => true,
501            b'l' => false,
502            _ => return,
503        };
504        for &mode in &self.params {
505            match mode {
506                7 => self.screen.autowrap = on,
507                25 => self.screen.cursor_visible = on,
508                // CTerm: blink attribute selects bright background (iCE colors).
509                // CTerm: bright and blink select fonts, and need not
510                // brighten or blink.
511                31 => self.screen.look.bright_font = on,
512                32 => self.screen.look.bright_plain = on,
513                33 => self.screen.ice_colors = on,
514                34 => self.screen.look.blink_font = on,
515                35 => self.screen.look.blink_steady = on,
516                _ => log::debug!("unhandled DEC mode {mode} {on}"),
517            }
518        }
519    }
520
521    fn sgr(&mut self) {
522        if self.params.is_empty() {
523            self.params.push(0);
524        }
525        let pen = &mut self.screen.pen;
526        let mut i = 0;
527        while i < self.params.len() {
528            let p = self.params[i];
529            match p {
530                0 => *pen = Cell { ch: b' ', ..Cell::default() },
531                1 => pen.flags = (pen.flags | flags::BOLD) & !flags::FAINT,
532                2 => pen.flags = (pen.flags | flags::FAINT) & !flags::BOLD,
533                4 => pen.flags |= flags::UNDERLINE,
534                5 | 6 => pen.flags |= flags::BLINK,
535                7 => pen.flags |= flags::REVERSE,
536                8 => pen.flags |= flags::CONCEAL,
537                21 | 22 => pen.flags &= !(flags::BOLD | flags::FAINT),
538                24 => pen.flags &= !flags::UNDERLINE,
539                25 => pen.flags &= !flags::BLINK,
540                27 => pen.flags &= !flags::REVERSE,
541                28 => pen.flags &= !flags::CONCEAL,
542                30..=37 => pen.fg = Color::Pc(ANSI_TO_VGA[(p - 30) as usize]),
543                39 => pen.fg = DEFAULT_FG,
544                40..=47 => pen.bg = Color::Pc(ANSI_TO_VGA[(p - 40) as usize]),
545                49 => pen.bg = DEFAULT_BG,
546                90..=97 => pen.fg = Color::Pc(ANSI_TO_VGA[(p - 90) as usize] + 8),
547                100..=107 => pen.bg = Color::Pc(ANSI_TO_VGA[(p - 100) as usize] + 8),
548                38 | 48 => {
549                    let color = match self.params.get(i + 1) {
550                        Some(5) => {
551                            let c = self.params.get(i + 2).map(|&v| Color::Xterm(v as u8));
552                            i += 2;
553                            c
554                        }
555                        Some(2) => {
556                            let c = (i + 4 < self.params.len()).then(|| {
557                                let v = &self.params[i + 2..i + 5];
558                                Color::Rgb(rgb(v[0] as u8, v[1] as u8, v[2] as u8))
559                            });
560                            i += 4;
561                            c
562                        }
563                        _ => None,
564                    };
565                    if let Some(c) = color {
566                        if p == 38 { pen.fg = c } else { pen.bg = c }
567                    }
568                }
569                _ => log::debug!("unhandled SGR {p}"),
570            }
571            i += 1;
572        }
573    }
574}
575
576/// An X11-style color: `rgb:R/G/B` with 1-4 hex digits per channel, or
577/// `#RRGGBB`.
578fn parse_color(spec: &str) -> Option<crate::palette::Rgb> {
579    let spec = spec.trim();
580    if let Some(hex) = spec.strip_prefix('#') {
581        return (hex.len() == 6).then(|| u32::from_str_radix(hex, 16).ok()).flatten();
582    }
583    let mut channels = spec.strip_prefix("rgb:")?.split('/').map(|c| {
584        let v = u32::from_str_radix(c, 16).ok().filter(|_| (1..=4).contains(&c.len()))?;
585        // Scale to 8 bits: "f" and "ffff" are both full.
586        Some((v * 255 / ((1 << (4 * c.len())) - 1)) as u8)
587    });
588    let (r, g, b) = (channels.next()??, channels.next()??, channels.next()??);
589    channels.next().is_none().then(|| rgb(r, g, b))
590}
591
592#[cfg(test)]
593mod tests {
594    use super::*;
595
596    fn term() -> Terminal {
597        Terminal::new(80, 25, 100)
598    }
599
600    fn line(t: &Terminal, y: usize) -> String {
601        Screen::row_text(t.screen.row(y))
602    }
603
604    #[test]
605    fn cursor_position_and_text() {
606        let mut t = term();
607        t.feed(b"\x1b[3;5HHi");
608        assert_eq!(line(&t, 2), "    Hi");
609        assert_eq!(t.screen.cursor(), (6, 2));
610    }
611
612    #[test]
613    fn sgr_colors() {
614        let mut t = term();
615        t.feed(b"\x1b[1;31;44mX");
616        let c = t.screen.row(0)[0];
617        assert_eq!(c.fg, Color::Pc(4));
618        assert_eq!(c.bg, Color::Pc(1));
619        assert!(c.flags & flags::BOLD != 0);
620        t.feed(b"\x1b[0;38;2;10;20;30mY");
621        assert_eq!(t.screen.row(0)[1].fg, Color::Rgb(0x0A141E));
622        t.feed(b"\x1b[38;5;196mZ");
623        assert_eq!(t.screen.row(0)[2].fg, Color::Xterm(196));
624    }
625
626    #[test]
627    fn cursor_position_report() {
628        let mut t = term();
629        t.feed(b"\x1b[10;20H\x1b[6n");
630        assert_eq!(t.take_replies(), b"\x1b[10;20R");
631    }
632
633    #[test]
634    fn device_attributes() {
635        let mut t = term();
636        t.feed(b"\x1b[c\x1b[0c");
637        assert_eq!(t.take_replies(), b"\x1b[?1;0c\x1b[?1;0c");
638        // Secondary DA (`CSI > c`) and `CSI 1 c` are not answered.
639        t.feed(b"\x1b[>c\x1b[1c");
640        assert!(t.take_replies().is_empty());
641    }
642
643    #[test]
644    fn ice_mode_toggle() {
645        let mut t = term();
646        t.feed(b"\x1b[?33h");
647        assert!(t.screen.ice_colors);
648        t.feed(b"\x1b[?33l");
649        assert!(!t.screen.ice_colors);
650    }
651
652    #[test]
653    fn cp437_glyphs_and_utf8() {
654        let mut t = term();
655        t.feed(&[0xDB, 0x03]);
656        assert_eq!(t.screen.row(0)[0].ch, 0xDB);
657        assert_eq!(t.screen.row(0)[1].ch, 0x03);
658        t.encoding = Encoding::Utf8;
659        t.feed("ۂ".as_bytes());
660        assert_eq!(t.screen.row(0)[2].ch, 0xDB);
661        assert_eq!(t.screen.row(0)[3].ch, 0x82);
662    }
663
664    #[test]
665    fn osc_title_and_strings_ignored() {
666        let mut t = term();
667        t.feed(b"\x1b]0;My BBS\x07\x1bPjunk\x1b\\ok");
668        assert_eq!(t.take_events(), vec![TermEvent::Title("My BBS".into())]);
669        assert_eq!(line(&t, 0), "ok");
670    }
671
672    #[test]
673    fn ansi_art_line_of_exactly_80_columns() {
674        let mut t = term();
675        let mut data = vec![b'#'; 80];
676        data.extend_from_slice(b"\r\nnext");
677        t.feed(&data);
678        assert_eq!(line(&t, 1), "next");
679    }
680
681    #[test]
682    fn save_restore_cursor() {
683        let mut t = term();
684        t.feed(b"\x1b[5;5H\x1b[s\x1b[1;1H\x1b[uX");
685        assert_eq!(t.screen.row(4)[4].ch, b'X');
686    }
687
688    #[test]
689    fn ansi_music() {
690        let mut t = term();
691        t.feed(b"Hi\x1b[NMLO3A\x0e there\x1b[|MLO3A8\x0e");
692        assert_eq!(line(&t, 0), "Hi there");
693        let a = Tone { hz: 440, ms: 500 };
694        assert_eq!(t.take_events(), [TermEvent::Music(vec![a]), TermEvent::Music(vec![Tone { ms: 250, ..a }])]);
695
696        // ESC [ M is delete-line unless music is wanted there.
697        t.feed(b"\x1b[2;1Hx\x1b[1;1H\x1b[M");
698        assert_eq!(line(&t, 0), "x");
699        t.music_on_csi_m = true;
700        t.feed(b"\x1b[MMLO3A\x0e\x1b[1M");
701        assert_eq!(t.take_events(), [TermEvent::Music(vec![a])]);
702        assert_eq!(line(&t, 0), "");
703    }
704
705    #[test]
706    fn cterm_palette_and_fonts() {
707        let mut t = Terminal::new(80, 25, 0);
708        // Red (ANSI 1, VGA 4) becomes orange; bright blue (ANSI 12) too.
709        t.feed(b"\x1b]4;1;rgb:ff/80/00;12;#123456\x1b\\");
710        assert_eq!(t.screen.look.palette[1], 0xFF8000);
711        assert_eq!(t.screen.look.rgb(Color::Pc(4)), 0xFF8000);
712        assert_eq!(t.screen.look.rgb(Color::Pc(9)), 0x123456);
713        t.feed(b"\x1b[31mX");
714        assert_eq!(t.screen.row(0)[0].resolve_with(false, &t.screen.look).0, 0xFF8000);
715        // Queried, then reset.
716        t.feed(b"\x1b]4;1;?\x07");
717        assert_eq!(t.take_replies(), b"\x1b]4;1;rgb:ff/80/00\x1b\\");
718        t.feed(b"\x1b]104;1\x07");
719        assert_eq!(t.screen.look.palette[1], 0xAA0000);
720        assert_eq!(t.screen.look.palette[12], 0x123456);
721        t.feed(b"\x1b]104\x07");
722        assert_eq!(t.screen.look, *Look::standard());
723
724        // Font slots, and bright text in slot 1 without brightening.
725        t.feed(b"\x1b[0;42 D\x1b[1;40 D\x1b[?31h\x1b[?32h\x1b[1;32mB");
726        let cell = t.screen.row(0)[1];
727        assert_eq!(t.screen.look.fonts, [42, 40, 0, 0]);
728        assert_eq!(t.screen.look.font_for(&cell), 40);
729        assert_eq!(cell.resolve_with(false, &t.screen.look).0, 0x00AA00);
730        t.feed(b"\x1b[?31l\x1b[?32l");
731        assert_eq!(t.screen.look.font_for(&cell), 42);
732        assert_eq!(cell.resolve_with(false, &t.screen.look).0, 0x55FF55);
733        assert_eq!(parse_color("rgb:f/0/8"), Some(0xFF0088));
734        assert_eq!(parse_color("rgb:ffff/0000/8080"), Some(0xFF0080));
735        assert_eq!(parse_color("rgb:1/2"), None);
736    }
737
738    #[test]
739    fn reset_returns_to_the_entrys_ice_setting() {
740        let mut t = Terminal::new(80, 25, 0);
741        t.screen.base_ice = true;
742        t.feed(b"\x1b[?33l");
743        assert!(!t.screen.ice_colors);
744        t.feed(b"\x1bc");
745        assert!(t.screen.ice_colors);
746    }
747}