1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "lowercase")]
18pub enum Encoding {
19 #[default]
20 Cp437,
21 Utf8,
22 Petscii,
24 Atascii,
26 Avatar,
28 Rip,
30 Viewdata,
32 Igs,
34 Skypix,
36}
37
38impl Encoding {
39 pub const ALL: [Encoding; 9] = [
40 Encoding::Cp437,
41 Encoding::Utf8,
42 Encoding::Petscii,
43 Encoding::Atascii,
44 Encoding::Viewdata,
45 Encoding::Igs,
46 Encoding::Skypix,
47 Encoding::Avatar,
48 Encoding::Rip,
49 ];
50
51 pub fn name(self) -> &'static str {
52 match self {
53 Encoding::Cp437 => "CP437",
54 Encoding::Utf8 => "UTF-8",
55 Encoding::Petscii => "PETSCII",
56 Encoding::Atascii => "ATASCII",
57 Encoding::Avatar => "Avatar",
58 Encoding::Rip => "RIPscrip",
59 Encoding::Viewdata => "Viewdata",
60 Encoding::Igs => "IGS",
61 Encoding::Skypix => "SkyPix",
62 }
63 }
64
65 pub fn is_retro(self) -> bool {
67 matches!(self, Encoding::Petscii | Encoding::Atascii)
68 }
69
70 pub fn screen_size(self) -> Option<(usize, usize)> {
72 match self {
73 Encoding::Petscii => Some((crate::petscii::COLS, crate::petscii::ROWS)),
74 Encoding::Atascii => Some((crate::atascii::COLS, crate::atascii::ROWS)),
75 Encoding::Rip => Some((crate::rip::COLS, crate::rip::ROWS)),
76 Encoding::Viewdata => Some((crate::viewdata::COLS, crate::viewdata::ROWS)),
77 Encoding::Igs => Some((crate::igs::COLS, crate::igs::ROWS)),
78 Encoding::Skypix => Some((crate::skypix::COLS, crate::skypix::ROWS)),
79 _ => None,
80 }
81 }
82
83 pub fn encode_text(self, text: &str) -> Vec<u8> {
86 match self {
87 Encoding::Cp437 | Encoding::Avatar | Encoding::Rip | Encoding::Igs => cp437::encode(text),
88 Encoding::Skypix => text.chars().map(|c| if (c as u32) < 256 { c as u8 } else { b'?' }).collect(),
90 Encoding::Utf8 => text.as_bytes().to_vec(),
91 Encoding::Petscii => text
92 .chars()
93 .filter_map(|c| if c == '\r' { Some(0x0D) } else { crate::petscii::encode_char(c) })
94 .collect(),
95 Encoding::Atascii => text
96 .chars()
97 .filter_map(|c| if c == '\r' { Some(0x9B) } else { crate::atascii::encode_char(c) })
98 .collect(),
99 Encoding::Viewdata => text.chars().filter_map(crate::viewdata::encode_char).collect(),
100 }
101 }
102}
103
104const MAX_PARAMS: usize = 32;
105const MAX_STRING: usize = 4096;
106pub const CTERM_DA: &[u8] = b"\x1b[=67;84;101;114;109;1;332c";
109use crate::screen::FIRST_LOADABLE_FONT;
110const MAX_DCS: usize = 32 << 20;
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114enum State {
115 Ground,
116 Escape,
117 EscIntermediate,
119 Csi,
120 Osc,
122 Dcs,
124 Apc,
126 IgnoreString,
128 FontBlock(u8, usize),
130 StringEsc,
132 Music,
134}
135
136#[derive(Clone, Debug, PartialEq)]
138pub enum TermEvent {
139 Bell,
140 Title(String),
141 Music(Vec<Tone>),
143 Audio(crate::audio::AudioOp),
145}
146
147pub struct Terminal {
148 pub screen: Screen,
149 pub encoding: Encoding,
150 pub music_on_csi_m: bool,
154 pub sixel_display_mode: bool,
157 state: State,
158 string_kind: State,
160 dcs: Vec<u8>,
161 pub syncterm: crate::syncterm::SyncTerm,
163 pub mouse: crate::mouse::MouseModes,
165 private: u8,
166 intermediate: u8,
167 params: Vec<u32>,
168 param_present: bool,
169 osc: Vec<u8>,
170 utf8_buf: [u8; 4],
171 utf8_len: usize,
172 utf8_need: usize,
173 replies: Vec<u8>,
174 events: Vec<TermEvent>,
175 petscii: crate::petscii::Petscii,
176 atascii: crate::atascii::Atascii,
177 avatar: crate::avatar::Avatar,
178 viewdata: crate::viewdata::Viewdata,
179 rip: Option<Box<crate::rip::Rip>>,
181 igs: Option<Box<crate::igs::Igs>>,
183 skypix: Option<Box<crate::skypix::Skypix>>,
185 line_start: bool,
188 held: Vec<u8>,
191}
192
193impl Terminal {
194 pub fn new(cols: usize, rows: usize, scrollback: usize) -> Self {
195 Terminal {
196 screen: Screen::new(cols, rows, scrollback),
197 encoding: Encoding::Cp437,
198 music_on_csi_m: false,
199 sixel_display_mode: false,
200 state: State::Ground,
201 string_kind: State::IgnoreString,
202 dcs: Vec::new(),
203 syncterm: Default::default(),
204 mouse: Default::default(),
205 private: 0,
206 intermediate: 0,
207 params: Vec::with_capacity(MAX_PARAMS),
208 param_present: false,
209 osc: Vec::new(),
210 utf8_buf: [0; 4],
211 utf8_len: 0,
212 utf8_need: 0,
213 replies: Vec::new(),
214 events: Vec::new(),
215 petscii: Default::default(),
216 atascii: Default::default(),
217 avatar: Default::default(),
218 viewdata: Default::default(),
219 rip: None,
220 igs: None,
221 skypix: None,
222 line_start: true,
223 held: Vec::new(),
224 }
225 }
226
227 pub fn start_emulation(&mut self) {
230 match self.encoding {
231 Encoding::Petscii => self.petscii.start(&mut self.screen),
232 Encoding::Atascii => self.atascii.start(&mut self.screen),
233 Encoding::Rip => self.rip = Some(Box::default()),
234 Encoding::Viewdata => self.viewdata.start(&mut self.screen),
235 Encoding::Igs => self.igs = Some(Box::default()),
236 Encoding::Skypix => self.skypix = Some(Box::default()),
237 _ => {}
238 }
239 }
240
241 pub fn rip(&self) -> Option<&crate::rip::Rip> {
243 self.rip.as_deref()
244 }
245
246 pub fn rip_mut(&mut self) -> Option<&mut crate::rip::Rip> {
247 self.rip.as_deref_mut()
248 }
249
250 pub fn igs(&self) -> Option<&crate::igs::Igs> {
252 self.igs.as_deref()
253 }
254
255 pub fn igs_mut(&mut self) -> Option<&mut crate::igs::Igs> {
256 self.igs.as_deref_mut()
257 }
258
259 pub fn skypix(&self) -> Option<&crate::skypix::Skypix> {
261 self.skypix.as_deref()
262 }
263
264 fn skypix_done(&mut self) {
266 let Some(sky) = self.skypix.as_deref_mut() else { return };
267 self.replies.append(&mut sky.replies);
268 for _ in 0..std::mem::take(&mut sky.bells) {
269 self.events.push(TermEvent::Bell);
270 }
271 let (x, y) = sky.cursor();
272 self.screen.move_to(x, y);
273 self.screen.cursor_visible = sky.cursor_on;
274 }
275
276 fn igs_done(&mut self) {
278 let Some(igs) = self.igs.as_deref_mut() else { return };
279 self.replies.append(&mut igs.replies);
280 for _ in 0..std::mem::take(&mut igs.bells) {
281 self.events.push(TermEvent::Bell);
282 }
283 let (x, y) = igs.cursor();
284 self.screen.move_to(x, y);
285 self.screen.cursor_visible = igs.cursor_on;
286 }
287
288 pub fn take_replies(&mut self) -> Vec<u8> {
290 std::mem::take(&mut self.replies)
291 }
292
293 pub fn take_events(&mut self) -> Vec<TermEvent> {
294 std::mem::take(&mut self.events)
295 }
296
297 pub fn feed(&mut self, data: &[u8]) {
298 match self.encoding {
299 Encoding::Petscii => {
300 for &b in data {
301 self.petscii.byte(&mut self.screen, b, &mut self.events);
302 }
303 }
304 Encoding::Atascii => {
305 for &b in data {
306 self.atascii.byte(&mut self.screen, b, &mut self.events);
307 }
308 }
309 Encoding::Viewdata => self.viewdata.feed(&mut self.screen, data, &mut self.events),
310 Encoding::Igs => {
311 if let Some(igs) = self.igs.as_deref_mut() {
312 igs.feed(data);
313 }
314 self.igs_done();
315 }
316 Encoding::Skypix => {
317 if let Some(sky) = self.skypix.as_deref_mut() {
318 sky.feed(data);
319 }
320 self.skypix_done();
321 }
322 _ => {
323 if self.syncterm.hold.is_some() {
324 self.held.extend_from_slice(data);
325 return;
326 }
327 for (i, &b) in data.iter().enumerate() {
328 self.byte(b);
329 if self.syncterm.hold.is_some() {
330 self.held.extend_from_slice(&data[i + 1..]);
331 break;
332 }
333 }
334 }
335 }
336 }
337
338 pub fn poll(&mut self, now: std::time::Instant) {
341 self.syncterm.audio.poll(now, &mut self.replies);
342 if let Some(igs) = self.igs.as_deref_mut().filter(|g| g.hold.is_some()) {
343 igs.poll(now);
344 self.igs_done();
345 }
346 if let Some(sky) = self.skypix.as_deref_mut().filter(|s| s.hold.is_some()) {
347 sky.poll(now);
348 self.skypix_done();
349 }
350 if self.syncterm.hold.is_some_and(|t| now >= t) {
351 self.syncterm.hold = None;
352 let held = std::mem::take(&mut self.held);
353 self.feed(&held);
354 }
355 }
356
357 pub fn interrupt(&mut self) {
359 if let Some(igs) = self.igs.as_deref_mut() {
360 igs.interrupt();
361 self.igs_done();
362 }
363 }
364
365 pub fn next_poll(&self) -> Option<std::time::Instant> {
367 let igs = self.igs.as_ref().and_then(|g| g.hold);
368 let sky = self.skypix.as_ref().and_then(|s| s.hold);
369 [self.syncterm.hold, self.syncterm.audio.next_due(), igs, sky].into_iter().flatten().min()
370 }
371
372 fn byte(&mut self, b: u8) {
373 if self.state == State::Ground {
374 let taken = match self.encoding {
375 Encoding::Avatar => self.avatar_byte(b),
376 Encoding::Rip => self.rip_byte(b),
377 _ => false,
378 };
379 if taken {
380 return;
381 }
382 }
383 if self.state != State::Ground && !matches!(self.state, State::FontBlock(..)) && (b == 0x18 || b == 0x1A) {
385 self.state = State::Ground;
386 return;
387 }
388 match self.state {
389 State::Ground => self.ground(b),
390 State::Escape => self.escape(b),
391 State::EscIntermediate => self.state = State::Ground,
392 State::Csi => self.csi(b),
393 State::Osc | State::IgnoreString => match b {
394 0x07 if self.state == State::Osc => self.finish_string(),
395 0x1B => self.state = State::StringEsc,
396 _ if self.state == State::Osc && self.osc.len() < MAX_STRING => self.osc.push(b),
397 _ => {}
398 },
399 State::FontBlock(slot, len) => {
400 self.dcs.push(b);
401 if self.dcs.len() >= len {
402 self.state = State::Ground;
403 let font = std::mem::take(&mut self.dcs);
404 self.screen.look.load_font(slot, &font);
405 }
406 }
407 State::Dcs | State::Apc => match b {
408 0x1B => self.state = State::StringEsc,
409 _ if self.dcs.len() < MAX_DCS => self.dcs.push(b),
410 _ => {}
411 },
412 State::Music => match b {
413 0x0E => {
414 self.state = State::Ground;
415 let tones = music::parse(&self.osc);
416 if !tones.is_empty() {
417 let ms: u64 = tones.iter().map(|t| t.ms as u64).sum();
418 self.syncterm.audio.music(std::time::Duration::from_millis(ms));
419 self.events.push(TermEvent::Music(tones));
420 }
421 }
422 _ if self.osc.len() < MAX_STRING => self.osc.push(b),
423 _ => self.state = State::Ground,
425 },
426 State::StringEsc => {
427 if b == b'\\' {
428 self.finish_string();
429 } else {
430 self.state = State::Escape;
432 self.escape(b);
433 }
434 }
435 }
436 }
437
438 fn avatar_byte(&mut self, b: u8) -> bool {
440 use crate::avatar::Outcome;
441 match self.avatar.byte(&mut self.screen, b) {
442 Outcome::Pass => false,
443 Outcome::Done => true,
444 Outcome::Show(bytes) => {
445 for b in bytes {
446 self.byte(b);
447 }
448 true
449 }
450 }
451 }
452
453 fn rip_byte(&mut self, b: u8) -> bool {
455 use crate::rip::Feed;
456 let Some(rip) = self.rip.as_mut() else { return false };
457 match rip.byte(&mut self.screen, b, self.line_start) {
458 Feed::Pass => false,
459 Feed::Done => true,
460 Feed::Text(bytes) => {
461 self.line_start = false;
462 for b in bytes {
463 self.byte(b);
464 }
465 true
466 }
467 }
468 }
469
470 fn ground(&mut self, b: u8) {
471 if self.encoding == Encoding::Utf8 && self.utf8_need > 0 {
472 if b & 0xC0 == 0x80 {
473 self.utf8_buf[self.utf8_len] = b;
474 self.utf8_len += 1;
475 if self.utf8_len == self.utf8_need {
476 let ch = std::str::from_utf8(&self.utf8_buf[..self.utf8_len]).ok().and_then(|s| s.chars().next());
477 self.utf8_need = 0;
478 self.screen.put(ch.and_then(cp437::from_unicode).unwrap_or(b'?'));
479 }
480 return;
481 }
482 self.utf8_need = 0;
484 self.screen.put(b'?');
485 }
486 match b {
487 0x00 | 0x7F => {}
488 0x07 => self.events.push(TermEvent::Bell),
489 0x08 => self.screen.backspace(),
490 0x09 => self.screen.tab(),
491 0x0A => {
492 self.screen.line_feed();
493 self.line_start = true;
494 }
495 0x0C => self.screen.erase_display(2),
496 0x0D => {
497 self.screen.carriage_return();
498 self.line_start = true;
499 }
500 0x1B => self.state = State::Escape,
501 _ => match self.encoding {
502 Encoding::Cp437
505 | Encoding::Petscii
506 | Encoding::Atascii
507 | Encoding::Avatar
508 | Encoding::Rip
509 | Encoding::Viewdata
510 | Encoding::Igs
511 | Encoding::Skypix => {
512 if self.avatar.insert {
513 self.screen.insert_chars(1);
514 }
515 self.line_start = false;
516 self.screen.put(b)
517 }
518 Encoding::Utf8 => match b {
519 0x01..=0x1F => {}
520 0x20..=0x7E => self.screen.put(b),
521 0xC2..=0xDF => self.utf8_start(b, 2),
522 0xE0..=0xEF => self.utf8_start(b, 3),
523 0xF0..=0xF4 => self.utf8_start(b, 4),
524 _ => self.screen.put(b'?'),
525 },
526 },
527 }
528 }
529
530 fn utf8_start(&mut self, b: u8, need: usize) {
531 self.utf8_buf[0] = b;
532 self.utf8_len = 1;
533 self.utf8_need = need;
534 }
535
536 fn escape(&mut self, b: u8) {
537 self.state = State::Ground;
538 match b {
539 b'[' => {
540 self.state = State::Csi;
541 self.private = 0;
542 self.intermediate = 0;
543 self.params.clear();
544 self.param_present = false;
545 }
546 b']' => {
547 self.state = State::Osc;
548 self.string_kind = State::Osc;
549 self.osc.clear();
550 }
551 b'P' => {
552 self.state = State::Dcs;
553 self.string_kind = State::Dcs;
554 self.dcs.clear();
555 }
556 b'_' => {
557 self.state = State::Apc;
558 self.string_kind = State::Apc;
559 self.dcs.clear();
560 }
561 b'X' | b'^' => {
562 self.state = State::IgnoreString;
563 self.string_kind = State::IgnoreString;
564 }
565 0x20..=0x2F => self.state = State::EscIntermediate,
566 b'7' => self.screen.save_cursor(),
567 b'8' => self.screen.restore_cursor(),
568 b'c' => {
569 self.screen.reset();
570 self.mouse = Default::default();
571 }
572 b'D' => self.screen.line_feed(),
573 b'E' => {
574 self.screen.carriage_return();
575 self.screen.line_feed();
576 }
577 b'M' => self.screen.reverse_index(),
578 0x1B => self.state = State::Escape,
579 _ => log::debug!("unhandled ESC {:?}", b as char),
580 }
581 }
582
583 fn finish_string(&mut self) {
584 self.state = State::Ground;
585 match self.string_kind {
586 State::Osc => {}
587 State::Dcs => return self.finish_dcs(),
588 State::Apc => {
589 let apc = std::mem::take(&mut self.dcs);
590 if !self.syncterm.apc(&apc, &mut self.screen, &mut self.replies) {
591 log::debug!("unhandled APC {:?}", String::from_utf8_lossy(&apc[..apc.len().min(32)]));
592 }
593 self.events.extend(self.syncterm.audio_ops.drain(..).map(TermEvent::Audio));
594 if apc.capacity() <= 1 << 20 {
595 self.dcs = apc;
596 self.dcs.clear();
597 }
598 return;
599 }
600 _ => return,
601 }
602 let s = String::from_utf8_lossy(&self.osc).into_owned();
603 let (code, rest) = s.split_once(';').unwrap_or((s.as_str(), ""));
604 match code {
605 "0" | "2" => self.events.push(TermEvent::Title(rest.to_string())),
607 "4" => self.set_palette(rest),
608 "104" => self.reset_palette(rest),
609 _ => log::debug!("unhandled OSC {code}"),
610 }
611 }
612
613 fn finish_dcs(&mut self) {
615 let dcs = std::mem::take(&mut self.dcs);
616 if let Some(rest) = dcs.strip_prefix(b"CTerm:Font:") {
618 let text = String::from_utf8_lossy(rest);
619 if let Some((slot, b64)) = text.split_once(':')
620 && let (Ok(slot), Some(data)) = (slot.trim().parse::<u8>(), crate::syncterm::decode_b64(b64))
621 && !self.screen.look.load_font(slot, &data)
622 {
623 log::debug!("not loading a {}-byte font into slot {slot}", data.len());
624 }
625 return;
626 }
627 let start = dcs.iter().position(|b| !matches!(b, b'0'..=b'9' | b';')).unwrap_or(dcs.len());
628 if dcs.get(start) == Some(&b'q') {
629 let params: Vec<u32> =
630 String::from_utf8_lossy(&dcs[..start]).split(';').map(|p| p.parse().unwrap_or(0)).collect();
631 let background = self.screen.look.rgb(self.screen.pen.bg);
632 let img = crate::sixel::decode(¶ms, &dcs[start + 1..], background);
633 self.screen.place_image(&img, !self.sixel_display_mode);
634 } else {
635 log::debug!("unhandled DCS {:?}", String::from_utf8_lossy(&dcs[..dcs.len().min(32)]));
636 }
637 if dcs.capacity() <= 1 << 20 {
639 self.dcs = dcs;
640 self.dcs.clear();
641 }
642 }
643
644 fn set_palette(&mut self, args: &str) {
647 let mut parts = args.split(';');
648 while let (Some(i), Some(spec)) = (parts.next(), parts.next()) {
649 let Ok(i) = i.trim().parse::<u8>() else { continue };
650 if spec == "?" {
651 let c = self.screen.look.palette[i as usize];
652 let reply = format!("\x1b]4;{i};rgb:{:02x}/{:02x}/{:02x}\x1b\\", c >> 16, (c >> 8) & 0xFF, c & 0xFF);
653 self.replies.extend_from_slice(reply.as_bytes());
654 } else if let Some(c) = parse_color(spec) {
655 self.screen.look.palette[i as usize] = c;
656 }
657 }
658 }
659
660 fn reset_palette(&mut self, args: &str) {
662 let standard = &Look::standard().palette;
663 if args.trim().is_empty() {
664 self.screen.look.palette = *standard;
665 return;
666 }
667 for i in args.split(';').filter_map(|i| i.trim().parse::<u8>().ok()) {
668 self.screen.look.palette[i as usize] = standard[i as usize];
669 }
670 }
671
672 fn csi(&mut self, b: u8) {
673 match b {
674 b'0'..=b'9' => {
675 if self.params.is_empty() {
676 self.params.push(0);
677 }
678 let p = self.params.last_mut().unwrap();
679 *p = p.saturating_mul(10).saturating_add((b - b'0') as u32);
680 self.param_present = true;
681 }
682 b';' | b':' => {
683 if self.params.is_empty() {
684 self.params.push(0);
685 }
686 if self.params.len() < MAX_PARAMS {
687 self.params.push(0);
688 }
689 self.param_present = true;
690 }
691 0x3C..=0x3F => {
692 if !self.param_present && self.private == 0 {
693 self.private = b;
694 }
695 }
696 b'!' if self.encoding == Encoding::Rip && self.params.is_empty() && self.private == 0 => {
698 self.state = State::Ground;
699 self.replies.extend_from_slice(crate::rip::VERSION_REPLY);
700 }
701 0x20..=0x2F => self.intermediate = b,
702 0x40..=0x7E => {
703 self.state = State::Ground;
704 self.dispatch_csi(b);
705 }
706 0x00..=0x1F => {
708 if b == 0x1B {
709 self.state = State::Escape;
710 } else {
711 self.ground(b);
712 }
713 }
714 _ => self.state = State::Ground,
715 }
716 }
717
718 fn p(&self, i: usize, default: u32) -> u32 {
720 match self.params.get(i) {
721 Some(&v) if v != 0 => v,
722 _ => default,
723 }
724 }
725
726 fn dispatch_csi(&mut self, b: u8) {
727 if self.intermediate != 0 {
728 if self.intermediate == b' ' && b == b'D' && self.private == 0 {
730 let slot = self.params.first().copied().unwrap_or(0) as usize;
731 let font = self.params.get(1).copied().unwrap_or(0);
732 if slot < 4 && font <= 255 {
733 self.screen.look.fonts[slot] = font as u8;
734 }
735 return;
736 }
737 log::debug!("unhandled CSI with intermediate {:?} {:?}", self.intermediate as char, b as char);
738 return;
739 }
740 match self.private {
741 0 => {}
742 b'?' if b == b'S' => return self.graphics_attributes(),
743 b'?' => return self.dec_mode(b),
744 b'=' if b == b'n' => return self.cterm_report(),
745 b'=' if b == b'{' => {
747 let slot = self.params.first().copied().unwrap_or(255).min(255) as u8;
748 let len = match self.params.get(1).copied().unwrap_or(0) {
749 1 => 3584,
750 2 => 2048,
751 _ => 4096,
752 };
753 self.dcs.clear();
754 self.state = State::FontBlock(slot, len);
755 return;
756 }
757 b'<' if b == b'c' && self.params.first().is_none_or(|&v| v == 0) => {
759 return self.replies.extend_from_slice(b"\x1b[<0;1;2;3;4;5;6;7c");
760 }
761 _ => {
762 log::debug!("unhandled private CSI {:?}{:?} {:?}", self.private as char, b as char, self.params);
763 return;
764 }
765 }
766 let no_params = self.params.is_empty();
767 if b == b'|' || no_params && (b == b'N' || b == b'M' && self.music_on_csi_m) {
768 self.state = State::Music;
769 self.osc.clear();
770 return;
771 }
772 let n = self.p(0, 1) as usize;
773 let s = &mut self.screen;
774 match b {
775 b'@' => s.insert_chars(n),
776 b'A' => s.move_by(0, -(n as isize)),
777 b'B' | b'e' => s.move_by(0, n as isize),
778 b'C' | b'a' => s.move_by(n as isize, 0),
779 b'D' => s.move_by(-(n as isize), 0),
780 b'E' => {
781 s.move_by(0, n as isize);
782 s.carriage_return();
783 }
784 b'F' => {
785 s.move_by(0, -(n as isize));
786 s.carriage_return();
787 }
788 b'G' | b'`' => {
789 let y = s.cursor().1;
790 s.move_to(n - 1, y);
791 }
792 b'd' => {
793 let x = s.cursor().0;
794 s.move_to(x, n - 1);
795 }
796 b'H' | b'f' => {
797 let (row, col) = (self.p(0, 1) as usize, self.p(1, 1) as usize);
798 self.screen.move_to(col - 1, row - 1);
799 }
800 b'J' => s.erase_display(self.params.first().copied().unwrap_or(0)),
801 b'K' => s.erase_line(self.params.first().copied().unwrap_or(0)),
802 b'L' => s.insert_lines(n),
803 b'M' => s.delete_lines(n),
804 b'P' => s.delete_chars(n),
805 b'S' => s.scroll_up(n),
806 b'T' => s.scroll_down(n),
807 b'X' => s.erase_chars(n),
808 b'Z' => s.back_tab(n),
809 b'c' if self.params.first().is_none_or(|&v| v == 0) => self.replies.extend_from_slice(CTERM_DA),
813 b'm' => self.sgr(),
814 b'n' => match self.params.first().copied().unwrap_or(0) {
815 5 => self.replies.extend_from_slice(b"\x1b[0n"),
816 6 => {
817 let (x, y) = self.screen.cursor();
818 self.replies.extend_from_slice(format!("\x1b[{};{}R", y + 1, x + 1).as_bytes());
819 }
820 _ => {}
821 },
822 b'r' => {
823 let rows = self.screen.rows() as u32;
824 let (top, bottom) = (self.p(0, 1), self.p(1, rows));
825 self.screen.set_scroll_region(top as usize - 1, bottom as usize - 1);
826 }
827 b's' => s.save_cursor(),
828 b'u' => s.restore_cursor(),
829 b't' if self.params.len() == 4 => {
831 let c = Color::Rgb(rgb(self.params[1] as u8, self.params[2] as u8, self.params[3] as u8));
832 match self.params[0] {
833 0 => self.screen.pen.bg = c,
834 1 => self.screen.pen.fg = c,
835 _ => {}
836 }
837 }
838 b'h' | b'l' => {}
839 _ => log::debug!("unhandled CSI {:?} {:?}", b as char, self.params),
840 }
841 }
842
843 fn cterm_report(&mut self) {
845 let reply = match self.params.first().copied().unwrap_or(1) {
846 1 => {
849 let f = self.screen.look.fonts;
850 format!("\x1b[=1;{FIRST_LOADABLE_FONT};99;{};{};{};{}n", f[0], f[1], f[2], f[3])
851 }
852 2 => {
854 let s = &self.screen;
855 let l = &s.look;
856 let set = [
857 (7, s.autowrap),
858 (25, s.cursor_visible),
859 (31, l.bright_font),
860 (32, l.bright_plain),
861 (33, s.ice_colors),
862 (34, l.blink_font),
863 (35, l.blink_steady),
864 (80, self.sixel_display_mode),
865 ];
866 let mut modes: Vec<String> = set.iter().filter(|m| m.1).map(|m| m.0.to_string()).collect();
867 modes.extend(self.mouse.set_modes().iter().map(|m| m.to_string()));
868 format!("\x1b[=2;{}n", modes.join(";"))
869 }
870 3 => {
872 let (w, h) = self.screen.cell_px();
873 format!("\x1b[=3;{h};{w}n")
874 }
875 4 => "\x1b[=4;0n".to_string(),
877 5 => "\x1b[=5;0n".to_string(),
878 6 => "\x1b[=6;0n".to_string(),
879 7 => self.syncterm.audio.report(self.params.get(1).map(|&c| c as usize)),
881 _ => return,
882 };
883 self.replies.extend_from_slice(reply.as_bytes());
884 }
885
886 fn graphics_attributes(&mut self) {
889 let reply = match (self.params.first().copied(), self.params.get(1).copied()) {
890 (Some(1), Some(1 | 4)) => "\x1b[?1;0;256S".to_string(),
891 (Some(2), Some(1 | 4)) => {
892 let (w, h) = self.screen.size_px();
893 format!("\x1b[?2;0;{w};{h}S")
894 }
895 (Some(i @ 1..=3), _) => format!("\x1b[?{i};3;0S"),
896 (Some(i), _) => format!("\x1b[?{i};1;0S"),
897 (None, _) => return,
898 };
899 self.replies.extend_from_slice(reply.as_bytes());
900 }
901
902 fn dec_mode(&mut self, b: u8) {
903 let on = match b {
904 b'h' => true,
905 b'l' => false,
906 _ => return,
907 };
908 for &mode in &self.params {
909 if self.mouse.set(mode, on) {
910 continue;
911 }
912 match mode {
913 7 => self.screen.autowrap = on,
914 80 => self.sixel_display_mode = on,
915 25 => self.screen.cursor_visible = on,
916 31 => self.screen.look.bright_font = on,
920 32 => self.screen.look.bright_plain = on,
921 33 => self.screen.ice_colors = on,
922 34 => self.screen.look.blink_font = on,
923 35 => self.screen.look.blink_steady = on,
924 _ => log::debug!("unhandled DEC mode {mode} {on}"),
925 }
926 }
927 }
928
929 fn sgr(&mut self) {
930 if self.params.is_empty() {
931 self.params.push(0);
932 }
933 let pen = &mut self.screen.pen;
934 let mut i = 0;
935 while i < self.params.len() {
936 let p = self.params[i];
937 match p {
938 0 => *pen = Cell { ch: b' ', ..Cell::default() },
939 1 => pen.flags = (pen.flags | flags::BOLD) & !flags::FAINT,
940 2 => pen.flags = (pen.flags | flags::FAINT) & !flags::BOLD,
941 4 => pen.flags |= flags::UNDERLINE,
942 5 | 6 => pen.flags |= flags::BLINK,
943 7 => pen.flags |= flags::REVERSE,
944 8 => pen.flags |= flags::CONCEAL,
945 21 | 22 => pen.flags &= !(flags::BOLD | flags::FAINT),
946 24 => pen.flags &= !flags::UNDERLINE,
947 25 => pen.flags &= !flags::BLINK,
948 27 => pen.flags &= !flags::REVERSE,
949 28 => pen.flags &= !flags::CONCEAL,
950 30..=37 => pen.fg = Color::Pc(ANSI_TO_VGA[(p - 30) as usize]),
951 39 => pen.fg = DEFAULT_FG,
952 40..=47 => pen.bg = Color::Pc(ANSI_TO_VGA[(p - 40) as usize]),
953 49 => pen.bg = DEFAULT_BG,
954 90..=97 => pen.fg = Color::Pc(ANSI_TO_VGA[(p - 90) as usize] + 8),
955 100..=107 => pen.bg = Color::Pc(ANSI_TO_VGA[(p - 100) as usize] + 8),
956 38 | 48 => {
957 let color = match self.params.get(i + 1) {
958 Some(5) => {
959 let c = self.params.get(i + 2).map(|&v| Color::Xterm(v as u8));
960 i += 2;
961 c
962 }
963 Some(2) => {
964 let c = (i + 4 < self.params.len()).then(|| {
965 let v = &self.params[i + 2..i + 5];
966 Color::Rgb(rgb(v[0] as u8, v[1] as u8, v[2] as u8))
967 });
968 i += 4;
969 c
970 }
971 _ => None,
972 };
973 if let Some(c) = color {
974 if p == 38 { pen.fg = c } else { pen.bg = c }
975 }
976 }
977 _ => log::debug!("unhandled SGR {p}"),
978 }
979 i += 1;
980 }
981 }
982}
983
984fn parse_color(spec: &str) -> Option<crate::palette::Rgb> {
987 let spec = spec.trim();
988 if let Some(hex) = spec.strip_prefix('#') {
989 return (hex.len() == 6).then(|| u32::from_str_radix(hex, 16).ok()).flatten();
990 }
991 let mut channels = spec.strip_prefix("rgb:")?.split('/').map(|c| {
992 let v = u32::from_str_radix(c, 16).ok().filter(|_| (1..=4).contains(&c.len()))?;
993 Some((v * 255 / ((1 << (4 * c.len())) - 1)) as u8)
995 });
996 let (r, g, b) = (channels.next()??, channels.next()??, channels.next()??);
997 channels.next().is_none().then(|| rgb(r, g, b))
998}
999
1000#[cfg(test)]
1001mod tests {
1002 use super::*;
1003
1004 fn term() -> Terminal {
1005 Terminal::new(80, 25, 100)
1006 }
1007
1008 fn line(t: &Terminal, y: usize) -> String {
1009 Screen::row_text(t.screen.row(y))
1010 }
1011
1012 #[test]
1013 fn cursor_position_and_text() {
1014 let mut t = term();
1015 t.feed(b"\x1b[3;5HHi");
1016 assert_eq!(line(&t, 2), " Hi");
1017 assert_eq!(t.screen.cursor(), (6, 2));
1018 }
1019
1020 #[test]
1021 fn sgr_colors() {
1022 let mut t = term();
1023 t.feed(b"\x1b[1;31;44mX");
1024 let c = t.screen.row(0)[0];
1025 assert_eq!(c.fg, Color::Pc(4));
1026 assert_eq!(c.bg, Color::Pc(1));
1027 assert!(c.flags & flags::BOLD != 0);
1028 t.feed(b"\x1b[0;38;2;10;20;30mY");
1029 assert_eq!(t.screen.row(0)[1].fg, Color::Rgb(0x0A141E));
1030 t.feed(b"\x1b[38;5;196mZ");
1031 assert_eq!(t.screen.row(0)[2].fg, Color::Xterm(196));
1032 }
1033
1034 #[test]
1035 fn cursor_position_report() {
1036 let mut t = term();
1037 t.feed(b"\x1b[10;20H\x1b[6n");
1038 assert_eq!(t.take_replies(), b"\x1b[10;20R");
1039 }
1040
1041 #[test]
1042 fn device_attributes() {
1043 let mut t = term();
1044 t.feed(b"\x1b[c\x1b[0c");
1045 assert_eq!(t.take_replies(), [CTERM_DA, CTERM_DA].concat());
1046 t.feed(b"\x1b[>c\x1b[1c");
1048 assert!(t.take_replies().is_empty());
1049 }
1050
1051 #[test]
1052 fn ice_mode_toggle() {
1053 let mut t = term();
1054 t.feed(b"\x1b[?33h");
1055 assert!(t.screen.ice_colors);
1056 t.feed(b"\x1b[?33l");
1057 assert!(!t.screen.ice_colors);
1058 }
1059
1060 #[test]
1061 fn cp437_glyphs_and_utf8() {
1062 let mut t = term();
1063 t.feed(&[0xDB, 0x03]);
1064 assert_eq!(t.screen.row(0)[0].ch, 0xDB);
1065 assert_eq!(t.screen.row(0)[1].ch, 0x03);
1066 t.encoding = Encoding::Utf8;
1067 t.feed("ۂ".as_bytes());
1068 assert_eq!(t.screen.row(0)[2].ch, 0xDB);
1069 assert_eq!(t.screen.row(0)[3].ch, 0x82);
1070 }
1071
1072 #[test]
1073 fn osc_title_and_strings_ignored() {
1074 let mut t = term();
1075 t.feed(b"\x1b]0;My BBS\x07\x1bPjunk\x1b\\ok");
1076 assert_eq!(t.take_events(), vec![TermEvent::Title("My BBS".into())]);
1077 assert_eq!(line(&t, 0), "ok");
1078 }
1079
1080 #[test]
1081 fn ansi_art_line_of_exactly_80_columns() {
1082 let mut t = term();
1083 let mut data = vec![b'#'; 80];
1084 data.extend_from_slice(b"\r\nnext");
1085 t.feed(&data);
1086 assert_eq!(line(&t, 1), "next");
1087 }
1088
1089 #[test]
1090 fn save_restore_cursor() {
1091 let mut t = term();
1092 t.feed(b"\x1b[5;5H\x1b[s\x1b[1;1H\x1b[uX");
1093 assert_eq!(t.screen.row(4)[4].ch, b'X');
1094 }
1095
1096 #[test]
1097 fn ansi_music() {
1098 let mut t = term();
1099 t.feed(b"Hi\x1b[NMLO3A\x0e there\x1b[|MLO3A8\x0e");
1100 assert_eq!(line(&t, 0), "Hi there");
1101 let a = Tone { hz: 440, ms: 500 };
1102 assert_eq!(t.take_events(), [TermEvent::Music(vec![a]), TermEvent::Music(vec![Tone { ms: 250, ..a }])]);
1103
1104 t.feed(b"\x1b[2;1Hx\x1b[1;1H\x1b[M");
1106 assert_eq!(line(&t, 0), "x");
1107 t.music_on_csi_m = true;
1108 t.feed(b"\x1b[MMLO3A\x0e\x1b[1M");
1109 assert_eq!(t.take_events(), [TermEvent::Music(vec![a])]);
1110 assert_eq!(line(&t, 0), "");
1111 }
1112
1113 #[test]
1114 fn cterm_palette_and_fonts() {
1115 let mut t = Terminal::new(80, 25, 0);
1116 t.feed(b"\x1b]4;1;rgb:ff/80/00;12;#123456\x1b\\");
1118 assert_eq!(t.screen.look.palette[1], 0xFF8000);
1119 assert_eq!(t.screen.look.rgb(Color::Pc(4)), 0xFF8000);
1120 assert_eq!(t.screen.look.rgb(Color::Pc(9)), 0x123456);
1121 t.feed(b"\x1b[31mX");
1122 assert_eq!(t.screen.row(0)[0].resolve_with(false, &t.screen.look).0, 0xFF8000);
1123 t.feed(b"\x1b]4;1;?\x07");
1125 assert_eq!(t.take_replies(), b"\x1b]4;1;rgb:ff/80/00\x1b\\");
1126 t.feed(b"\x1b]104;1\x07");
1127 assert_eq!(t.screen.look.palette[1], 0xAA0000);
1128 assert_eq!(t.screen.look.palette[12], 0x123456);
1129 t.feed(b"\x1b]104\x07");
1130 assert_eq!(t.screen.look, *Look::standard());
1131
1132 t.feed(b"\x1b[0;42 D\x1b[1;40 D\x1b[?31h\x1b[?32h\x1b[1;32mB");
1134 let cell = t.screen.row(0)[1];
1135 assert_eq!(t.screen.look.fonts, [42, 40, 0, 0]);
1136 assert_eq!(t.screen.look.font_for(&cell), 40);
1137 assert_eq!(cell.resolve_with(false, &t.screen.look).0, 0x00AA00);
1138 t.feed(b"\x1b[?31l\x1b[?32l");
1139 assert_eq!(t.screen.look.font_for(&cell), 42);
1140 assert_eq!(cell.resolve_with(false, &t.screen.look).0, 0x55FF55);
1141 assert_eq!(parse_color("rgb:f/0/8"), Some(0xFF0088));
1142 assert_eq!(parse_color("rgb:ffff/0000/8080"), Some(0xFF0080));
1143 assert_eq!(parse_color("rgb:1/2"), None);
1144 }
1145
1146 #[test]
1147 fn sixel_pictures() {
1148 let mut t = term();
1149 let mut seq = b"\x1b[3;5HA\x1bP0;1;0q\"1;1#1;2;100;0;0".to_vec();
1151 for _ in 0..6 {
1152 seq.extend_from_slice(b"!16~-");
1153 }
1154 seq.extend_from_slice(b"\x1b\\B");
1155 t.feed(&seq);
1156 let s = &t.screen;
1157 let tile = s.tiles.get(s.row(2)[5].tile).expect("a tile");
1162 assert_eq!((tile.width, tile.height), (8, 16));
1163 assert_eq!(tile.pixels[0], crate::sixel::solid(0xFF0000));
1164 assert!(s.row(4)[6].tile != 0 && s.row(2)[7].tile == 0);
1165 assert_eq!(s.row(4)[5].ch, b'B');
1166 assert_eq!(s.row(4)[5].tile, 0, "text replaces the picture");
1167 assert_eq!(t.screen.cursor(), (6, 4));
1168
1169 t.feed(b"\x1b[?2;1S\x1b[?1;1S\x1b[<c");
1171 assert_eq!(t.take_replies(), b"\x1b[?2;0;640;400S\x1b[?1;0;256S\x1b[<0;1;2;3;4;5;6;7c");
1172 t.feed(b"\x1b[=1n\x1b[=2n\x1b[=3n\x1b[=7n");
1173 assert_eq!(t.take_replies(), b"\x1b[=1;43;99;0;0;0;0n\x1b[=2;7;25n\x1b[=3;16;8n\x1b[=7n");
1174 }
1175
1176 #[test]
1177 fn sixel_pictures_scroll_the_screen() {
1178 let mut t = term();
1179 t.feed(b"\x1b[25;1HX\x1bPq\"1;1;8;32\x1b\\");
1180 assert_eq!(line(&t, 22), "X");
1183 assert!(t.screen.row(22)[1].tile != 0 && t.screen.row(23)[1].tile != 0);
1184 assert_eq!(t.screen.cursor(), (1, 24));
1185 t.feed(b"\x1b[?80h\x1b[10;10H\x1bPq\"1;1;8;16\x1b\\");
1187 assert!(t.screen.row(0)[0].tile != 0);
1188 assert_eq!(t.screen.cursor(), (9, 9));
1189 }
1190
1191 #[test]
1192 fn waiting_for_a_sound_holds_the_text() {
1193 let mut t = term();
1194 t.feed(b"\x1b_SyncTERM:A;Synth;S=1;W=SIN;F=440;T=100\x1b\\\x1b_SyncTERM:A;Queue;C=2;S=1\x1b\\");
1196 assert!(matches!(t.take_events()[..], [TermEvent::Audio(crate::audio::AudioOp::Queue { channel: 2, .. })]));
1197 t.feed(b"before\x1b_SyncTERM:A;Wait;C=2\x1b\\after");
1198 t.feed(b" more");
1199 assert_eq!(line(&t, 0), "before");
1200 let at = t.next_poll().expect("a time to read again");
1201 t.poll(at);
1202 assert_eq!(line(&t, 0), "beforeafter more");
1203 t.feed(b"\x1b[=7;2n\x1b_SyncTERM:A;Update;C=2\x1b\\");
1205 t.poll(at + std::time::Duration::from_secs(1));
1206 let replies = String::from_utf8(t.take_replies()).unwrap();
1207 assert!(replies.ends_with("\x1b[=7;2;0n"), "{replies:?}");
1208 t.feed(b"\x1b_SyncTERM:Q;libsndfileFormat;0x20;100\x1b\\");
1209 assert_eq!(t.take_replies(), b"\x1b[=7;101;32;100;1n");
1210 }
1211
1212 #[test]
1213 fn loadable_fonts() {
1214 use data_encoding::BASE64;
1215 let mut t = term();
1216 let font = vec![0xFFu8; 4096];
1218 let dcs = format!("\x1bPCTerm:Font:43:{}\x1b\\\x1b[0;43 D", BASE64.encode(&font));
1219 t.feed(dcs.as_bytes());
1220 let f = t.screen.look.loaded_font(43).expect("loaded");
1221 assert_eq!((f.height, f.data.len()), (16, 4096));
1222 assert_eq!(t.screen.look.fonts[0], 43);
1223 let mut old = b"\x1b[=44;2{".to_vec();
1225 old.extend(vec![0x18u8; 2048]);
1226 old.extend(b"ok");
1227 t.feed(&old);
1228 assert_eq!(t.screen.look.loaded_font(44).unwrap().height, 8);
1229 assert!(line(&t, 0).ends_with("ok"), "the font's bytes aren't text: {:?}", line(&t, 0));
1230 t.feed(format!("\x1bPCTerm:Font:5:{}\x1b\\\x1bc", BASE64.encode(&font)).as_bytes());
1232 assert!(t.screen.look.loaded_font(5).is_none());
1233 assert!(t.screen.look.loaded_font(43).is_some());
1234 t.feed(dcs.as_bytes());
1236 t.feed(dcs.replace(":43:", ":45:").as_bytes());
1237 let (a, b) = (t.screen.look.loaded_font(43).unwrap(), t.screen.look.loaded_font(45).unwrap());
1238 assert!(std::ptr::eq(a.data, b.data));
1239 }
1240
1241 #[test]
1242 fn mouse_modes() {
1243 let mut t = term();
1244 t.feed(b"\x1b[?1003h\x1b[?1006h");
1245 assert!(t.mouse.any && t.mouse.sgr && t.mouse.tracking());
1246 t.feed(b"\x1b[=2n");
1247 assert_eq!(t.take_replies(), b"\x1b[=2;7;25;1003;1006n");
1248 t.feed(b"\x1b[?1003l\x1b[?1006l");
1249 assert!(!t.mouse.tracking());
1250 }
1251
1252 #[test]
1253 fn reset_returns_to_the_entrys_ice_setting() {
1254 let mut t = Terminal::new(80, 25, 0);
1255 t.screen.base_ice = true;
1256 t.feed(b"\x1b[?33l");
1257 assert!(!t.screen.ice_colors);
1258 t.feed(b"\x1bc");
1259 assert!(t.screen.ice_colors);
1260 }
1261}