1use std::time::{Duration, Instant};
19
20use crate::palette::Rgb;
21
22static TOPAZ: &[u8; 2048] = include_bytes!("../assets/topaz_8x8.bin");
23
24pub const WIDTH: usize = 640;
25pub const HEIGHT: usize = 200;
26const SAMPLE_CHANNEL: usize = 2;
28const SAMPLE_RATE: u32 = 8363;
30
31pub const COLS: usize = 80;
32pub const ROWS: usize = 25;
33
34const PALETTE: [u16; 16] =
36 [0x000, 0x11F, 0xDDD, 0xF00, 0x0F1, 0x3AF, 0xFF2, 0xC0E, 0x0B6, 0x0DD, 0x0AF, 0x07C, 0x00F, 0x70F, 0xC0E, 0xC08];
37const ANSI_PENS: [u8; 8] = [0, 3, 4, 6, 1, 7, 5, 2];
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42enum State {
43 Ground,
44 Escape,
45 Csi,
46 Text,
48}
49
50#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct Transfer {
53 pub mode: u32,
55 pub name: String,
56}
57
58struct Brush {
60 w: usize,
61 h: usize,
62 pixels: Vec<u8>,
63}
64
65pub struct Skypix {
66 pixels: Vec<u8>,
67 palette: [u16; 16],
68 colors: u8,
70 x: i32,
72 y: i32,
73 saved: (i32, i32),
74 pen_a: u8,
76 pen_b: u8,
77 reverse: bool,
78 pen: (i32, i32),
80 font: Option<i32>,
82 disk: Option<crate::amigafont::AmigaFont>,
84 pub fonts_dir: Option<std::path::PathBuf>,
86 sample: Option<crate::audio::Sound>,
88 pub audio: Vec<crate::audio::AudioOp>,
90 pub transfer: Option<(Transfer, Vec<u8>)>,
92 starting: Option<Transfer>,
93 brush: Option<Brush>,
94 gadgets: Vec<(i32, i32, i32, i32)>,
95 pub cursor_on: bool,
96 state: State,
97 params: Vec<u32>,
98 digits: Vec<bool>,
100 have: bool,
101 text: Vec<u8>,
102 held: Vec<u8>,
104 pub hold: Option<Instant>,
105 pub replies: Vec<u8>,
106 pub bells: usize,
107}
108
109impl Default for Skypix {
110 fn default() -> Skypix {
111 Skypix {
112 pixels: vec![0; WIDTH * HEIGHT],
113 palette: PALETTE,
114 colors: 16,
116 x: 0,
117 y: 0,
118 saved: (0, 0),
119 pen_a: 2,
120 pen_b: 0,
121 reverse: false,
122 pen: (0, 0),
123 font: None,
124 disk: None,
125 fonts_dir: None,
126 sample: None,
127 audio: Vec::new(),
128 transfer: None,
129 starting: None,
130 brush: None,
131 gadgets: Vec::new(),
132 cursor_on: true,
133 state: State::Ground,
134 params: Vec::new(),
135 digits: Vec::new(),
136 have: false,
137 text: Vec::new(),
138 held: Vec::new(),
139 hold: None,
140 replies: Vec::new(),
141 bells: 0,
142 }
143 }
144}
145
146impl Skypix {
147 pub fn pixels(&self) -> &[u8] {
148 &self.pixels
149 }
150
151 pub fn palette(&self) -> [Rgb; 16] {
152 std::array::from_fn(|i| {
153 let c = self.palette[if self.colors == 8 { i & 7 } else { i }] as u32;
154 let ch = |shift: u32| ((c >> shift) & 15) * 0x11;
155 (ch(8) << 16) | (ch(4) << 8) | ch(0)
156 })
157 }
158
159 pub fn cursor(&self) -> (usize, usize) {
161 ((self.x.max(0) / 8).min(COLS as i32 - 1) as usize, (self.y.max(0) / 8).min(ROWS as i32 - 1) as usize)
162 }
163
164 fn mask(&self) -> u8 {
165 self.colors - 1
166 }
167
168 pub fn feed(&mut self, data: &[u8]) {
169 if self.hold.is_some() {
170 self.held.extend_from_slice(data);
171 return;
172 }
173 for (i, &b) in data.iter().enumerate() {
174 self.byte(b);
175 if self.hold.is_some() {
176 self.held.extend_from_slice(&data[i + 1..]);
177 return;
178 }
179 if let Some(t) = self.starting.take() {
181 self.transfer = Some((t, data[i + 1..].to_vec()));
182 return;
183 }
184 }
185 }
186
187 pub fn load(&mut self, mode: u32, data: &[u8]) {
189 match mode {
190 1 => match crate::iff::ilbm(data) {
191 Some((w, h, pixels)) => self.brush = Some(Brush { w, h, pixels }),
192 None => log::warn!("SkyPix: the brush isn't an IFF picture"),
193 },
194 2 | 3 => {
195 self.sample = Some(crate::audio::decode(data).unwrap_or_else(|| crate::audio::raw8(data, SAMPLE_RATE)))
196 }
197 _ => {}
198 }
199 }
200
201 fn advance(&self, c: u8) -> i32 {
203 match &self.disk {
204 Some(f) => f.advance(c),
205 None => self.cell().0,
206 }
207 }
208
209 pub fn poll(&mut self, now: Instant) {
211 if self.hold.is_some_and(|t| now >= t) {
212 self.hold = None;
213 let held = std::mem::take(&mut self.held);
214 self.feed(&held);
215 }
216 }
217
218 fn byte(&mut self, b: u8) {
219 match self.state {
220 State::Ground => self.ground(b),
221 State::Escape => {
222 self.state = if b == b'[' { State::Csi } else { State::Ground };
223 self.params.clear();
224 self.digits.clear();
225 self.have = false;
226 }
227 State::Csi => match b {
228 b'0'..=b'9' => {
229 if self.params.is_empty() {
230 self.params.push(0);
231 self.digits.push(false);
232 }
233 if let Some(d) = self.digits.last_mut() {
234 *d = true;
235 }
236 let p = self.params.last_mut().unwrap();
237 *p = p.saturating_mul(10).saturating_add((b - b'0') as u32);
238 self.have = true;
239 }
240 b';' => {
241 if self.params.is_empty() {
242 self.params.push(0);
243 self.digits.push(false);
244 }
245 if self.params.len() < 32 {
246 self.params.push(0);
247 self.digits.push(false);
248 }
249 }
250 b'!' => {
251 if matches!(self.params.first(), Some(0 | 10 | 16)) && !self.closes_font() {
253 self.text.clear();
254 self.state = State::Text;
255 } else {
256 self.state = State::Ground;
257 self.command(&[]);
258 }
259 }
260 0x40..=0x7E => {
261 self.state = State::Ground;
262 self.csi(b);
263 }
264 0x20..=0x3F => {}
265 _ => self.state = State::Ground,
266 },
267 State::Text => {
268 if b == b'!' || self.text.len() > 256 {
269 self.state = State::Ground;
270 let text = std::mem::take(&mut self.text);
271 self.command(&text);
272 } else {
273 self.text.push(b);
274 }
275 }
276 }
277 }
278
279 fn closes_font(&self) -> bool {
281 self.params.first() == Some(&10) && self.params.get(1).copied().unwrap_or(0) == 0
282 }
283
284 fn given(&self, i: usize) -> bool {
286 self.digits.get(i).copied().unwrap_or(false)
287 }
288
289 fn p(&self, i: usize) -> i32 {
290 self.params.get(i).copied().unwrap_or(0) as i32
291 }
292
293 fn ground(&mut self, b: u8) {
294 let (cw, ch) = self.cell();
295 match b {
296 0x07 => self.bells += 1,
297 0x08 => self.x = (self.x - cw).max(0),
298 0x09 => self.x = ((self.x / 64 + 1) * 64).min(WIDTH as i32 - cw),
299 0x0A => {
300 if self.font.is_none() {
303 self.x = 0;
304 self.line_feed();
305 }
306 }
307 0x0C => self.clear(),
308 0x0D => {
309 self.x = 0;
310 if self.font.is_some() {
311 self.line_feed();
312 }
313 }
314 0x1B => self.state = State::Escape,
315 0x00..=0x1F => {}
316 _ => {
317 if self.x + cw > WIDTH as i32 && self.font.is_none() {
319 self.x = 0;
320 self.line_feed();
321 }
322 self.glyph(b);
323 self.x += self.advance(b);
324 }
325 }
326 let _ = ch;
327 }
328
329 fn cell(&self) -> (i32, i32) {
331 if let Some(f) = &self.disk {
332 return (f.advance(b'n').max(1), f.height as i32);
333 }
334 let s = self.font.unwrap_or(1);
335 (8 * s, 8 * s)
336 }
337
338 fn line_feed(&mut self) {
339 let ch = self.cell().1;
340 self.y += ch;
341 if self.y + ch > HEIGHT as i32 {
342 let up = (self.y + ch - HEIGHT as i32) as usize;
343 self.scroll(up);
344 self.y -= up as i32;
345 }
346 }
347
348 fn scroll(&mut self, rows: usize) {
349 let rows = rows.min(HEIGHT);
350 self.pixels.copy_within(rows * WIDTH.., 0);
351 let bg = self.pen_b;
352 self.pixels[(HEIGHT - rows) * WIDTH..].fill(bg);
353 }
354
355 fn clear(&mut self) {
356 let bg = self.pen_b;
357 self.pixels.fill(bg);
358 (self.x, self.y) = (0, 0);
359 }
360
361 fn set(&mut self, x: i32, y: i32, pen: u8) {
362 if x >= 0 && y >= 0 && (x as usize) < WIDTH && (y as usize) < HEIGHT {
363 self.pixels[y as usize * WIDTH + x as usize] = pen & self.mask();
364 }
365 }
366
367 fn get(&self, x: i32, y: i32) -> Option<u8> {
368 (x >= 0 && y >= 0 && (x as usize) < WIDTH && (y as usize) < HEIGHT)
369 .then(|| self.pixels[y as usize * WIDTH + x as usize])
370 }
371
372 fn glyph(&mut self, c: u8) {
375 if let Some(f) = self.disk.take() {
376 let fg = if self.reverse { self.pen_b } else { self.pen_a };
377 for (x, y) in f.pixels(c) {
378 self.set(self.x + x, self.y + y, fg);
379 }
380 self.disk = Some(f);
381 return;
382 }
383 let s = self.font.unwrap_or(1);
384 let (fg, bg) = if self.reverse { (self.pen_b, self.pen_a) } else { (self.pen_a, self.pen_b) };
385 for gy in 0..8 * s {
386 let bits = TOPAZ[c as usize * 8 + (gy / s) as usize];
387 for gx in 0..8 * s {
388 let on = bits & (0x80 >> (gx / s)) != 0;
389 if on {
390 self.set(self.x + gx, self.y + gy, fg);
391 } else if self.font.is_none() {
392 self.set(self.x + gx, self.y + gy, bg);
393 }
394 }
395 }
396 }
397
398 fn csi(&mut self, f: u8) {
399 let (cw, ch) = self.cell();
400 let n = self.p(0).max(1);
401 match f {
402 b'A' => self.y = (self.y - n * ch).max(0),
403 b'B' => self.y = (self.y + n * ch).min(HEIGHT as i32 - ch),
404 b'C' => self.x = (self.x + n * cw).min(WIDTH as i32 - cw),
405 b'D' => self.x = (self.x - n * cw).max(0),
406 b'H' | b'f' => {
407 let (row, col) = (self.p(0).max(1), self.p(1).max(1));
408 self.x = ((col - 1) * 8).min(WIDTH as i32 - 8);
409 self.y = ((row - 1) * 8).min(HEIGHT as i32 - 8);
410 }
411 b'J' => match self.p(0) {
412 0 => {
413 self.fill_rect(self.x, self.y, WIDTH as i32 - 1, self.y + ch - 1, self.pen_b);
414 self.fill_rect(0, self.y + ch, WIDTH as i32 - 1, HEIGHT as i32 - 1, self.pen_b);
415 }
416 1 => {
417 self.fill_rect(0, 0, WIDTH as i32 - 1, self.y - 1, self.pen_b);
418 self.fill_rect(0, self.y, self.x + cw - 1, self.y + ch - 1, self.pen_b);
419 }
420 _ => self.clear(),
422 },
423 b'K' => {
424 let (a, b) = match self.p(0) {
425 0 => (self.x, WIDTH as i32 - 1),
426 1 => (0, self.x + cw - 1),
427 _ => (0, WIDTH as i32 - 1),
428 };
429 self.fill_rect(a, self.y, b, self.y + ch - 1, self.pen_b);
430 }
431 b'm' => self.sgr(),
432 b's' => self.saved = (self.x, self.y),
433 b'u' => (self.x, self.y) = self.saved,
434 b'n' if self.p(0) == 6 => {
435 let reply = format!("\x1b[{};{}R", self.y / 8 + 1, self.x / 8 + 1);
436 self.replies.extend_from_slice(reply.as_bytes());
437 }
438 b'h' | b'l' if self.params.first() == Some(&25) => self.cursor_on = f == b'h',
439 _ => {}
440 }
441 }
442
443 fn sgr(&mut self) {
444 if self.params.is_empty() {
445 self.params.push(0);
446 }
447 for i in 0..self.params.len() {
448 match self.params[i] {
449 0 => {
450 self.pen_a = 2;
451 self.pen_b = 0;
452 self.reverse = false;
453 }
454 7 => self.reverse = true,
455 27 => self.reverse = false,
456 p @ 30..=37 => self.pen_a = ANSI_PENS[(p - 30) as usize],
457 39 => self.pen_a = 2,
458 p @ 40..=47 => self.pen_b = ANSI_PENS[(p - 40) as usize],
459 49 => self.pen_b = 0,
460 _ => {}
461 }
462 }
463 }
464
465 fn fill_rect(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, pen: u8) {
466 let (x0, x1) = (x0.min(x1).max(0), x0.max(x1).min(WIDTH as i32 - 1));
467 let (y0, y1) = (y0.min(y1).max(0), y0.max(y1).min(HEIGHT as i32 - 1));
468 for y in y0..=y1 {
469 for x in x0..=x1 {
470 self.set(x, y, pen);
471 }
472 }
473 }
474
475 fn line(&mut self, (x0, y0): (i32, i32), (x1, y1): (i32, i32)) {
476 let (dx, dy) = ((x1 - x0).abs(), -(y1 - y0).abs());
477 let (sx, sy) = (if x0 < x1 { 1 } else { -1 }, if y0 < y1 { 1 } else { -1 });
478 let (mut x, mut y, mut err) = (x0, y0, dx + dy);
479 loop {
480 self.set(x, y, self.pen_a);
481 if x == x1 && y == y1 {
482 break;
483 }
484 let e2 = 2 * err;
485 if e2 >= dy {
486 err += dy;
487 x += sx;
488 }
489 if e2 <= dx {
490 err += dx;
491 y += sy;
492 }
493 }
494 }
495
496 fn ellipse(&mut self, cx: i32, cy: i32, a: i32, b: i32, filled: bool) {
498 let (a, b) = (a.max(0), b.max(0));
499 if a == 0 || b == 0 {
500 self.line((cx - a, cy - b), (cx + a, cy + b));
501 return;
502 }
503 let pen = self.pen_a;
504 let half = |dy: i32| (a as f64 * (1.0 - (dy as f64 / b as f64).powi(2)).max(0.0).sqrt()).round() as i32;
505 let mut prev = half(-b);
506 for dy in -b..=b {
507 let w = half(dy);
508 if filled {
509 for x in cx - w..=cx + w {
510 self.set(x, cy + dy, pen);
511 }
512 } else {
513 let (near, far) = (w.min(prev), w.max(prev));
516 for x in near..=far {
517 self.set(cx - x, cy + dy, pen);
518 self.set(cx + x, cy + dy, pen);
519 }
520 }
521 prev = w;
522 }
523 }
524
525 fn flood(&mut self, mode: i32, x: i32, y: i32) {
528 let Some(under) = self.get(x, y) else { return };
529 let pen = self.pen_a & self.mask();
530 let fills = |p: u8| if mode == 0 { p != pen } else { p == under };
531 if mode != 0 && under == pen || !fills(under) {
532 return;
533 }
534 let mut stack = vec![(x, y)];
535 while let Some((x, y)) = stack.pop() {
536 if !self.get(x, y).is_some_and(fills) {
537 continue;
538 }
539 let (mut a, mut b) = (x, x);
540 while self.get(a - 1, y).is_some_and(fills) {
541 a -= 1;
542 }
543 while self.get(b + 1, y).is_some_and(fills) {
544 b += 1;
545 }
546 for i in a..=b {
547 self.set(i, y, pen);
548 }
549 for i in a..=b {
550 for ny in [y - 1, y + 1] {
551 if self.get(i, ny).is_some_and(fills) {
552 stack.push((i, ny));
553 }
554 }
555 }
556 }
557 }
558
559 fn command(&mut self, text: &[u8]) {
561 let p = |i: usize| self.p(i);
562 match p(0) {
563 1 => self.set(p(1), p(2), self.pen_a),
564 2 => {
565 let to = (p(1), p(2));
566 self.line(self.pen, to);
567 self.pen = to;
568 }
569 3 => self.flood(p(1), p(2), p(3)),
570 4 => self.fill_rect(p(1), p(2), p(3), p(4), self.pen_a),
571 5 => self.ellipse(p(1), p(2), p(3), p(4), false),
572 6 => {
573 let (x, y, w, h) = (p(1), p(2), p(3).clamp(0, WIDTH as i32), p(4).clamp(0, HEIGHT as i32));
574 let pixels = (0..h)
575 .flat_map(|j| (0..w).map(move |i| (x + i, y + j)))
576 .map(|(x, y)| self.get(x, y).unwrap_or(0))
577 .collect();
578 self.brush = Some(Brush { w: w as usize, h: h as usize, pixels });
579 }
580 7 => self.blit(p(1), p(2), p(3), p(4), p(5), p(6), p(7) as u8, p(8) as u8),
581 8 => self.pen = (p(1), p(2)),
582 10 => {
583 let name = String::from_utf8_lossy(text).to_ascii_lowercase();
584 let size = p(1);
585 let font = if size == 0 || name.starts_with("topaz") && size <= 9 {
587 None
588 } else {
589 Some(((size + 4) / 8).max(1))
590 };
591 let was = self.cell().1;
593 if font.is_none() && self.font.is_some() {
594 self.y = (self.y + was - 8).min(HEIGHT as i32 - 8);
595 }
596 self.font = font;
597 self.disk = match (&self.fonts_dir, font) {
599 (Some(dir), Some(_)) => crate::amigafont::AmigaFont::find(dir, &name, size.max(1) as usize),
600 _ => None,
601 };
602 }
603 11 => {
604 for i in 0..16 {
605 self.palette[i] = (self.p(i + 1) as u16) & 0xFFF;
606 }
607 }
608 12 => self.palette = PALETTE,
609 13 => self.ellipse(p(1), p(2), p(3), p(4), true),
610 14 => self.hold = Some(Instant::now() + Duration::from_millis(p(1).clamp(0, 6000) as u64 * 1000 / 60)),
611 15 if !self.given(1) => self.pen_a = 2,
613 15 => self.pen_a = p(1) as u8 & 15,
614 9 => {
615 let loops = p(4).clamp(1, 20);
616 if let Some(sound) = self.sample.clone() {
617 use crate::audio::AudioOp;
618 self.audio.push(AudioOp::Flush { channel: SAMPLE_CHANNEL, fade_out: 0 });
619 for _ in 0..loops {
620 self.audio.push(AudioOp::Queue {
621 channel: SAMPLE_CHANNEL,
622 sound: sound.clone(),
623 fade_in: 0,
624 fade_out: 0,
625 crossfade: false,
626 looped: false,
627 volume: (0.0, 0.0),
628 });
629 }
630 }
631 }
632 16 => {
634 let name = String::from_utf8_lossy(text).trim().to_string();
635 let mode = p(1).max(0) as u32;
636 if matches!(mode, 1 | 2 | 3 | 20) && !name.is_empty() {
637 self.starting = Some(Transfer { mode, name });
638 } else {
639 self.replies.extend_from_slice(&[0x18, 0x18, 0x18]);
640 }
641 }
642 17 => {
643 self.colors = if p(1) == 2 { 16 } else { 8 };
644 self.palette = PALETTE;
645 }
646 18 if !self.given(1) => self.pen_b = 0,
647 18 => self.pen_b = p(1) as u8 & 15,
648 19 => (self.x, self.y) = (p(1).clamp(0, WIDTH as i32 - 1), p(2).clamp(0, HEIGHT as i32 - 1)),
649 22 => {
650 let (x0, y0, x1, y1) = (p(3), p(4), p(5), p(6));
651 for (a, b) in [((x0, y0), (x1, y0)), ((x1, y0), (x1, y1)), ((x1, y1), (x0, y1)), ((x0, y1), (x0, y0))] {
652 self.line(a, b);
653 }
654 self.gadgets.push((x0, y0, x1, y1));
655 }
656 99 => {
658 self.palette = PALETTE;
659 self.font = None;
660 self.pen_a = 2;
661 self.pen_b = 0;
662 }
663 _ => {}
664 }
665 }
666
667 #[allow(clippy::too_many_arguments)]
670 fn blit(&mut self, sx: i32, sy: i32, dx: i32, dy: i32, w: i32, h: i32, minterm: u8, mask: u8) {
671 let Some(brush) = self.brush.take() else { return };
672 for j in 0..h.max(0) {
673 for i in 0..w.max(0) {
674 let (bx, by) = (sx + i, sy + j);
675 if bx < 0 || by < 0 || bx as usize >= brush.w || by as usize >= brush.h {
676 continue;
677 }
678 let b = brush.pixels[by as usize * brush.w + bx as usize];
679 let Some(c) = self.get(dx + i, dy + j) else { continue };
680 let mut out = 0u8;
681 for plane in 0..4 {
682 let (bb, cb) = ((b >> plane) & 1, (c >> plane) & 1);
683 let bit = if mask & (1 << plane) != 0 { (minterm >> (4 + (bb << 1 | cb))) & 1 } else { cb };
684 out |= bit << plane;
685 }
686 self.set(dx + i, dy + j, out);
687 }
688 }
689 self.brush = Some(brush);
690 }
691
692 pub fn click(&self, x: i32, y: i32) -> Vec<u8> {
694 format!("\x1b[21;1;{x};{y}!").into_bytes()
695 }
696}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
701
702 fn sky(data: &[u8]) -> Skypix {
703 let mut s = Skypix::default();
704 s.feed(data);
705 s
706 }
707
708 fn at(s: &Skypix, x: usize, y: usize) -> u8 {
709 s.pixels[y * WIDTH + x]
710 }
711
712 #[test]
713 fn disk_fonts_brushes_and_samples() {
714 let dir = std::env::temp_dir().join(format!("badterm-skyfont-{}", std::process::id()));
716 std::fs::create_dir_all(dir.join("tiny")).unwrap();
717 std::fs::write(dir.join("tiny/4"), crate::amigafont::tests::sample_font()).unwrap();
718 let mut s = Skypix { fonts_dir: Some(dir.clone()), ..Skypix::default() };
719 s.feed(b"\x1b[18;1!\x1b[10;4!tiny.font!\x1b[19;100;50!AIA");
720 assert_eq!(
722 (at(&s, 100, 50), at(&s, 103, 50), at(&s, 104, 50), at(&s, 105, 50), at(&s, 106, 53)),
723 (2, 0, 2, 0, 2)
724 );
725 let _ = std::fs::remove_dir_all(&dir);
726 let mut s = sky(b"\x1b[16;1;4;2!brush.iff!PROTOCOL");
728 let (t, rest) = s.transfer.take().unwrap();
729 assert_eq!((t.mode, t.name.as_str(), rest.as_slice()), (1, "brush.iff", &b"PROTOCOL"[..]));
730 let mut f = b"FORM\0\0\0\0ILBM".to_vec();
732 let bmhd = [0, 2, 0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 10, 11, 0, 2, 0, 1];
733 for (id, d) in [(b"BMHD", &bmhd[..]), (b"BODY", &[0x40, 0, 0xC0, 0][..])] {
734 f.extend(id);
735 f.extend((d.len() as u32).to_be_bytes());
736 f.extend(d);
737 }
738 s.load(1, &f);
739 s.feed(b"\x1b[7;0;0;10;10;2;1;192;255!");
740 assert_eq!((at(&s, 10, 10), at(&s, 11, 10)), (2, 3));
741 s.load(2, &[0, 64, 127, 64, 0, 192, 128, 192]);
743 s.feed(b"\x1b[9;0;0;0;3!");
744 assert_eq!(s.audio.len(), 4);
745 assert_eq!(sky(b"\x1b[16;9;0;0!x!").replies, [0x18; 3]);
747 }
748
749 #[test]
750 fn drawing_commands() {
751 let s =
754 sky(b"\x1b[15;3!\x1b[1;5;5!\x1b[8;10;10!\x1b[2;20;10!\x1b[4;30;30;40;40!\x1b[15;4!\x1b[13;100;100;10;5!");
755 assert_eq!(at(&s, 5, 5), 3);
756 assert_eq!((at(&s, 10, 10), at(&s, 20, 10), at(&s, 21, 10)), (3, 3, 0));
757 assert_eq!(at(&s, 35, 35), 3);
758 assert_eq!((at(&s, 100, 100), at(&s, 109, 100), at(&s, 112, 100)), (4, 4, 0));
759 }
760
761 #[test]
762 fn flood_fill_modes() {
763 let mut s = sky(b"\x1b[15;3!\x1b[5;50;50;20;10!\x1b[3;0;50;50!");
765 assert_eq!((at(&s, 50, 50), at(&s, 75, 50)), (3, 0));
766 s.feed(b"\x1b[15;4!\x1b[3;1;50;50!");
768 assert_eq!(at(&s, 50, 50), 4);
769 }
770
771 #[test]
772 fn ansi_text_in_pens() {
773 let s = sky(b"\x1b[31;45mA\x1b[2;1HB");
775 let pens: Vec<u8> = (0..8).flat_map(|y| (0..8).map(move |x| (x, y))).map(|(x, y)| at(&s, x, y)).collect();
776 assert!(pens.contains(&3) && pens.contains(&7), "{pens:?}");
777 assert!((8..16).any(|y| (0..8).any(|x| at(&s, x, y) == 3)), "B on the next row");
778 assert_eq!(s.cursor(), (1, 1));
779 }
780
781 #[test]
782 fn fonts_position_and_brushes() {
783 let s = sky(b"\x1b[18;1!\x1b[10;16!garnet.font!\x1b[19;100;50!I");
785 assert!((50..66).any(|y| (100..116).any(|x| at(&s, x, y) == 2)));
786 assert_eq!(at(&s, 100, 50), 0, "no background drawn");
787 assert_eq!(s.cursor(), (14, 6));
788 let s = sky(b"\x1b[15;5!\x1b[4;0;0;3;3!\x1b[6;0;0;4;4!\x1b[7;0;0;200;100;4;4;192;255!");
790 assert_eq!((at(&s, 202, 102), at(&s, 205, 102)), (5, 0));
791 let s = sky(b"\x1b[11;3840;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0!");
793 assert_eq!(s.palette()[0], 0xFF0000);
794 }
795}