1use std::path::{Path, PathBuf};
28
29use crate::palette::Rgb;
30use crate::screen::{Cell, Screen};
31
32pub const WIDTH: usize = 640;
33pub const HEIGHT: usize = 350;
34pub const COLS: usize = 80;
36pub const ROWS: usize = 43;
37
38pub const VERSION_REPLY: &[u8] = b"RIPSCRIP015400";
40
41const MAX_SCENE_DEPTH: usize = 4;
43
44const MAX_FILE: u64 = 4 << 20;
46
47pub struct Io<'a> {
49 pub cache: Option<&'a Path>,
51 pub downloads: Option<&'a Path>,
53 pub replies: &'a mut Vec<u8>,
54}
55
56impl Io<'_> {
57 fn rip_dir(&self) -> Option<PathBuf> {
58 Some(self.cache?.join("RIP"))
59 }
60
61 fn find(&self, name: &str) -> Option<PathBuf> {
64 let dirs = [self.rip_dir(), self.downloads.map(Path::to_path_buf)];
65 dirs.into_iter().flatten().find_map(|dir| {
66 let exact = dir.join(name);
67 if exact.is_file() {
68 return Some(exact);
69 }
70 std::fs::read_dir(&dir).ok()?.flatten().map(|e| e.path()).find(|p| {
71 p.is_file() && p.file_name().and_then(|n| n.to_str()).is_some_and(|n| n.eq_ignore_ascii_case(name))
72 })
73 })
74 }
75}
76
77fn file_name(text: &[u8]) -> Option<String> {
79 let name = std::str::from_utf8(text).ok()?.trim();
80 let bad = |c: char| matches!(c, '/' | '\\' | ':' | '\0') || c.is_control();
81 (!name.is_empty() && !name.contains("..") && !name.chars().any(bad)).then(|| name.to_string())
82}
83
84fn decode_icon(data: &[u8]) -> Option<(usize, usize, Vec<u8>)> {
86 let w = u16::from_le_bytes([*data.first()?, *data.get(1)?]) as usize + 1;
87 let h = u16::from_le_bytes([*data.get(2)?, *data.get(3)?]) as usize + 1;
88 let row = w.div_ceil(8);
89 let body = data.get(4..4 + row * 4 * h)?;
90 let mut px = Vec::with_capacity(w * h);
91 for planes in body.chunks_exact(row * 4) {
92 for x in 0..w {
93 let bit = |p: usize| (planes[p * row + x / 8] >> (7 - (x & 7))) & 1;
94 px.push(bit(0) << 3 | bit(1) << 2 | bit(2) << 1 | bit(3));
95 }
96 }
97 Some((w, h, px))
98}
99
100fn encode_icon(w: usize, h: usize, px: &[u8]) -> Vec<u8> {
101 let row = w.div_ceil(8);
102 let mut out = Vec::with_capacity(4 + row * 4 * h);
103 out.extend_from_slice(&(w.saturating_sub(1) as u16).to_le_bytes());
104 out.extend_from_slice(&(h.saturating_sub(1) as u16).to_le_bytes());
105 for line in px.chunks_exact(w.max(1)).take(h) {
106 let mut planes = vec![0u8; row * 4];
107 for (x, &c) in line.iter().enumerate() {
108 for p in 0..4 {
109 if c >> (3 - p) & 1 != 0 {
110 planes[p * row + x / 8] |= 0x80 >> (x & 7);
111 }
112 }
113 }
114 out.extend_from_slice(&planes);
115 }
116 out
117}
118
119fn rip_time(t: std::time::SystemTime) -> String {
121 let secs = t.duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs());
122 let (days, rem) = ((secs / 86400) as i64, secs % 86400);
123 let z = days + 719_468;
125 let era = z.div_euclid(146_097);
126 let doe = z - era * 146_097;
127 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
128 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
129 let mp = (5 * doy + 2) / 153;
130 let day = doy - (153 * mp + 2) / 5 + 1;
131 let month = if mp < 10 { mp + 3 } else { mp - 9 };
132 let year = yoe + era * 400 + i64::from(month <= 2);
133 format!("{month:02}/{day:02}/{:02}.{:02}:{:02}:{:02}", year % 100, rem / 3600, rem / 60 % 60, rem % 60)
134}
135
136const MAX_LINE: usize = 64 * 1024;
138
139static FONT_8X8: &[u8; 2048] = include_bytes!("../assets/vga_8x8.bin");
140
141const DEFAULT_PALETTE: [u8; 16] = [0, 1, 2, 3, 4, 5, 20, 7, 56, 57, 58, 59, 60, 61, 62, 63];
143
144const FILL_PATTERNS: [[u8; 8]; 12] = [
146 [0x00; 8],
147 [0xFF; 8],
148 [0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00],
149 [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80],
150 [0xE0, 0xC1, 0x83, 0x07, 0x0E, 0x1C, 0x38, 0x70],
151 [0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x87, 0xC3, 0xE1],
152 [0xA5, 0xD2, 0x69, 0xB4, 0x5A, 0x2D, 0x96, 0x4B],
153 [0xFF, 0x88, 0x88, 0x88, 0xFF, 0x88, 0x88, 0x88],
154 [0x81, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x81],
155 [0xCC, 0x33, 0xCC, 0x33, 0xCC, 0x33, 0xCC, 0x33],
156 [0x80, 0x00, 0x08, 0x00, 0x80, 0x00, 0x08, 0x00],
157 [0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00],
158];
159
160const LINE_STYLES: [u16; 4] = [0xFFFF, 0xCCCC, 0xFC78, 0xF8F8];
162
163const ASPECT: f64 = 0.775;
166
167pub fn ega_rgb(c: u8) -> Rgb {
169 let level = |hi: u8, lo: u8| (((c >> hi) & 1) * 0xAA + ((c >> lo) & 1) * 0x55) as u32;
170 (level(2, 5) << 16) | (level(1, 4) << 8) | level(0, 3)
171}
172
173#[derive(Clone, Debug, PartialEq, Eq)]
175pub struct MouseRegion {
176 pub x0: usize,
177 pub y0: usize,
178 pub x1: usize,
179 pub y1: usize,
180 pub invert: bool,
182 pub text: Vec<u8>,
183}
184
185#[derive(Clone, Copy, Debug, Default)]
186struct ButtonStyle {
187 width: usize,
188 height: usize,
189 bevel: usize,
190 dfore: u8,
191 dback: u8,
192 bright: u8,
193 dark: u8,
194 surface: u8,
195}
196
197#[derive(Clone, Copy, Debug)]
198struct Viewport {
199 x0: usize,
200 y0: usize,
201 x1: usize,
202 y1: usize,
203}
204
205const FULL: Viewport = Viewport { x0: 0, y0: 0, x1: WIDTH - 1, y1: HEIGHT - 1 };
206
207struct TextBlock {
209 x0: i32,
210 x1: i32,
211 y: i32,
212}
213
214pub struct Rip {
215 pixels: Vec<u8>,
216 palette: [u8; 16],
218 color: u8,
219 fill_color: u8,
220 fill: [u8; 8],
221 line_style: u16,
222 thick: bool,
223 xor: bool,
224 view: Viewport,
225 pos: (i32, i32),
227 font_size: usize,
228 vertical: bool,
229 button: ButtonStyle,
230 regions: Vec<MouseRegion>,
231 clipboard: Option<(usize, usize, Vec<u8>)>,
232 text_block: Option<TextBlock>,
233 text_window: (usize, usize, usize, usize),
235 line: Option<Vec<u8>>,
237 pub(crate) eat_lf: bool,
239 scene: Option<Vec<u8>>,
241 pub scene_depth: usize,
243 pub dirty: bool,
245}
246
247pub enum Feed {
249 Pass,
251 Done,
253 Text(Vec<u8>),
255}
256
257impl Default for Rip {
258 fn default() -> Self {
259 Rip {
260 pixels: vec![0; WIDTH * HEIGHT],
261 palette: DEFAULT_PALETTE,
262 color: 15,
263 fill_color: 15,
264 fill: FILL_PATTERNS[1],
265 line_style: 0xFFFF,
266 thick: false,
267 xor: false,
268 view: FULL,
269 pos: (0, 0),
270 font_size: 1,
271 vertical: false,
272 button: ButtonStyle::default(),
273 regions: Vec::new(),
274 clipboard: None,
275 text_block: None,
276 text_window: (0, 0, COLS - 1, ROWS - 1),
277 line: None,
278 eat_lf: false,
279 scene: None,
280 scene_depth: 0,
281 dirty: true,
282 }
283 }
284}
285
286impl Rip {
287 pub fn pixels(&self) -> &[u8] {
289 &self.pixels
290 }
291
292 pub fn palette(&self) -> [Rgb; 16] {
294 self.palette.map(ega_rgb)
295 }
296
297 pub fn regions(&self) -> &[MouseRegion] {
298 &self.regions
299 }
300
301 pub fn click(&self, x: usize, y: usize) -> Option<Vec<u8>> {
304 self.regions
305 .iter()
306 .rev()
307 .find(|r| (r.x0..=r.x1).contains(&x) && (r.y0..=r.y1).contains(&y))
308 .map(|r| r.text.clone())
309 }
310
311 pub fn byte(&mut self, s: &mut Screen, b: u8, at_line_start: bool, io: &mut Io) -> Feed {
314 if std::mem::take(&mut self.eat_lf) && b == b'\n' {
315 return Feed::Done;
316 }
317 let Some(line) = self.line.as_mut() else {
318 if at_line_start && b == b'!' {
319 self.line = Some(vec![b]);
320 return Feed::Done;
321 }
322 return Feed::Pass;
323 };
324 if line.len() == 1 && b != b'|' {
326 let mut text = self.line.take().unwrap_or_default();
327 text.push(b);
328 return Feed::Text(text);
329 }
330 match b {
331 b'\r' | b'\n' => {
332 if line.last() == Some(&b'\\') && !line.ends_with(b"\\\\") {
334 line.pop();
335 line.push(0);
336 return Feed::Done;
337 }
338 if line.last() == Some(&0) {
339 return Feed::Done;
341 }
342 let line = self.line.take().unwrap_or_default();
343 self.run_line(s, &line, io);
344 self.eat_lf = b == b'\r';
346 Feed::Done
347 }
348 _ => {
349 if line.last() == Some(&0) {
350 line.pop();
351 }
352 if line.len() < MAX_LINE {
353 line.push(b);
354 }
355 Feed::Done
356 }
357 }
358 }
359
360 pub fn take_scene(&mut self) -> Option<Vec<u8>> {
363 self.scene.take()
364 }
365
366 fn run_line(&mut self, s: &mut Screen, line: &[u8], io: &mut Io) {
367 let mut cmds: Vec<Vec<u8>> = vec![Vec::new()];
369 let mut i = 1;
370 while i < line.len() {
371 match line[i] {
372 b'\\' if i + 1 < line.len() => {
373 cmds.last_mut().expect("never empty").push(line[i + 1]);
374 i += 1;
375 }
376 b'|' => cmds.push(Vec::new()),
377 c => cmds.last_mut().expect("never empty").push(c),
378 }
379 i += 1;
380 }
381 for cmd in cmds.iter().filter(|c| !c.is_empty()) {
382 self.command(s, cmd, io);
383 }
384 self.dirty = true;
385 }
386
387 fn command(&mut self, s: &mut Screen, cmd: &[u8], io: &mut Io) {
388 let (level, op, args) = match cmd {
389 [d @ b'1'..=b'9', op, args @ ..] => (d - b'0', *op, args),
390 [op, args @ ..] => (0, *op, args),
391 [] => return,
392 };
393 let mut a = Args(args);
394 match (level, op) {
395 (0, b'w') => {
396 let (x0, y0, x1, y1) = (a.n2(), a.n2(), a.n2(), a.n2());
397 let (x1, y1) = (x1.clamp(x0, COLS - 1), y1.clamp(y0, ROWS - 1));
398 self.text_window = (x0.min(COLS - 1), y0.min(ROWS - 1), x1, y1);
399 s.set_scroll_region(self.text_window.1, self.text_window.3);
400 s.move_to(self.text_window.0, self.text_window.1);
401 }
402 (0, b'v') => {
403 let (x0, y0, x1, y1) = (a.n2(), a.n2(), a.n2(), a.n2());
404 self.view = Viewport {
405 x0: x0.min(WIDTH - 1),
406 y0: y0.min(HEIGHT - 1),
407 x1: x1.clamp(x0.min(WIDTH - 1), WIDTH - 1),
408 y1: y1.clamp(y0.min(HEIGHT - 1), HEIGHT - 1),
409 };
410 }
411 (0, b'*') => self.reset(s),
412 (0, b'e') => {
413 let (x0, y0, x1, y1) = self.text_window;
414 s.fill_rect(x0, y0, x1 - x0 + 1, y1 - y0 + 1, Cell { ch: b' ', ..s.pen });
415 s.move_to(x0, y0);
416 }
417 (0, b'E') => {
418 let v = self.view;
419 for y in v.y0..=v.y1 {
420 self.pixels[y * WIDTH + v.x0..=y * WIDTH + v.x1].fill(0);
421 }
422 }
423 (0, b'g') => {
424 let (x, y) = (a.n2(), a.n2());
425 s.move_to(self.text_window.0 + x, self.text_window.1 + y);
426 }
427 (0, b'H') => s.move_to(self.text_window.0, self.text_window.1),
428 (0, b'>') => s.erase_line(0),
429 (0, b'c') => self.color = a.n2() as u8 & 15,
430 (0, b'Q') => {
431 for i in 0..16 {
432 self.palette[i] = a.n2() as u8 & 63;
433 }
434 }
435 (0, b'a') => {
436 let (i, c) = (a.n2(), a.n2());
437 self.palette[i & 15] = c as u8 & 63;
438 }
439 (0, b'W') => self.xor = a.n2() == 1,
440 (0, b'm') => self.pos = (a.i2(), a.i2()),
441 (0, b'T') => {
442 let text = a.rest().to_vec();
443 self.text(&text);
444 }
445 (0, b'@') => {
446 self.pos = (a.i2(), a.i2());
447 let text = a.rest().to_vec();
448 self.text(&text);
449 }
450 (0, b'Y') => {
451 let (_font, dir, size) = (a.n2(), a.n2(), a.n2());
452 self.vertical = dir == 1;
453 self.font_size = size.clamp(1, 10);
454 }
455 (0, b'X') => {
456 let (x, y) = (a.i2(), a.i2());
457 self.plot(x, y, self.color);
458 }
459 (0, b'L') => {
460 let (x0, y0, x1, y1) = (a.i2(), a.i2(), a.i2(), a.i2());
461 self.line(x0, y0, x1, y1);
462 }
463 (0, b'R') => {
464 let (x0, y0, x1, y1) = (a.i2(), a.i2(), a.i2(), a.i2());
465 self.polyline(&[(x0, y0), (x1, y0), (x1, y1), (x0, y1)], true);
466 }
467 (0, b'B') => {
468 let (x0, y0, x1, y1) = (a.i2(), a.i2(), a.i2(), a.i2());
469 self.bar(x0, y0, x1, y1);
470 }
471 (0, b'C') => {
472 let (x, y, r) = (a.i2(), a.i2(), a.i2());
473 let pts = arc_points(x, y, 0, 360, r as f64, r as f64 * ASPECT);
474 self.polyline(&pts, false);
475 }
476 (0, b'O') => {
477 let (x, y, st, end, xr, yr) = (a.i2(), a.i2(), a.i2(), a.i2(), a.i2(), a.i2());
478 let pts = arc_points(x, y, st, end, xr as f64, yr as f64);
479 self.polyline(&pts, false);
480 }
481 (0, b'o') => {
482 let (x, y, xr, yr) = (a.i2(), a.i2(), a.i2(), a.i2());
483 let pts = arc_points(x, y, 0, 360, xr as f64, yr as f64);
484 self.fill_polygon(&pts);
485 self.polyline(&pts, false);
486 }
487 (0, b'A') => {
488 let (x, y, st, end, r) = (a.i2(), a.i2(), a.i2(), a.i2(), a.i2());
489 let pts = arc_points(x, y, st, end, r as f64, r as f64 * ASPECT);
490 self.polyline(&pts, false);
491 }
492 (0, b'V') => {
493 let (x, y, st, end, xr, yr) = (a.i2(), a.i2(), a.i2(), a.i2(), a.i2(), a.i2());
494 let pts = arc_points(x, y, st, end, xr as f64, yr as f64);
495 self.polyline(&pts, false);
496 }
497 (0, b'I') => {
498 let (x, y, st, end, r) = (a.i2(), a.i2(), a.i2(), a.i2(), a.i2());
499 self.pie(x, y, st, end, r as f64, r as f64 * ASPECT);
500 }
501 (0, b'i') => {
502 let (x, y, st, end, xr, yr) = (a.i2(), a.i2(), a.i2(), a.i2(), a.i2(), a.i2());
503 self.pie(x, y, st, end, xr as f64, yr as f64);
504 }
505 (0, b'Z') => {
506 let p: Vec<(f64, f64)> = (0..4).map(|_| (a.i2() as f64, a.i2() as f64)).collect();
507 let n = a.n2().max(1);
508 let pts: Vec<(i32, i32)> = (0..=n)
509 .map(|i| {
510 let t = i as f64 / n as f64;
511 let u = 1.0 - t;
512 let (b0, b1, b2, b3) = (u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t);
513 let x = b0 * p[0].0 + b1 * p[1].0 + b2 * p[2].0 + b3 * p[3].0;
514 let y = b0 * p[0].1 + b1 * p[1].1 + b2 * p[2].1 + b3 * p[3].1;
515 (x.round() as i32, y.round() as i32)
516 })
517 .collect();
518 self.polyline(&pts, false);
519 }
520 (0, b'P' | b'p' | b'l') => {
521 let n = a.n2();
522 let pts: Vec<(i32, i32)> = (0..n).map(|_| (a.i2(), a.i2())).collect();
523 if op == b'p' {
524 self.fill_polygon(&pts);
525 }
526 self.polyline(&pts, op != b'l');
527 }
528 (0, b'F') => {
529 let (x, y, border) = (a.i2(), a.i2(), a.n2() as u8);
530 self.flood(x, y, border & 15);
531 }
532 (0, b'=') => {
533 let (style, user, thick) = (a.n2(), a.n4(), a.n2());
534 self.line_style = LINE_STYLES.get(style).copied().unwrap_or(user as u16);
535 self.thick = thick >= 3;
536 }
537 (0, b'S') => {
538 let (pattern, color) = (a.n2(), a.n2());
539 self.fill = FILL_PATTERNS.get(pattern).copied().unwrap_or(FILL_PATTERNS[1]);
540 self.fill_color = color as u8 & 15;
541 }
542 (0, b's') => {
543 for row in self.fill.iter_mut() {
544 *row = a.n2() as u8;
545 }
546 self.fill_color = a.n2() as u8 & 15;
547 }
548 (0, b'#') => {}
549 (1, b'M') => {
550 let (_num, x0, y0, x1, y1, clk, _clr) = (a.n2(), a.n2(), a.n2(), a.n2(), a.n2(), a.n1(), a.n1());
551 a.skip(5);
552 let text = host_command(a.rest());
553 self.region(x0, y0, x1, y1, clk == 1, text);
554 }
555 (1, b'K') => self.regions.clear(),
556 (1, b'T') => {
557 let (x0, y0, x1) = (a.i2(), a.i2(), a.i2());
558 self.text_block = Some(TextBlock { x0, x1, y: y0 });
559 }
560 (1, b't') => {
561 let _justify = a.n1();
562 let text = a.rest().to_vec();
563 if let Some(tb) = self.text_block.as_mut() {
564 let (x0, x1, y) = (tb.x0, tb.x1, tb.y);
565 tb.y += 8 * self.font_size as i32;
566 let room = ((x1 - x0).max(0) as usize / (8 * self.font_size)).max(1);
567 self.pos = (x0, y);
568 self.text(&text[..text.len().min(room)]);
569 }
570 }
571 (1, b'E') => self.text_block = None,
572 (1, b'C') => {
573 let (x0, y0, x1, y1) = (a.i2(), a.i2(), a.i2(), a.i2());
574 self.copy_to_clipboard(x0, y0, x1, y1);
575 }
576 (1, b'P') => {
577 let (x, y, mode) = (a.i2(), a.i2(), a.n2());
578 self.paste(x, y, mode);
579 }
580 (1, b'G') => {
581 let (x0, y0, x1, y1) = (a.i2(), a.i2(), a.i2(), a.i2());
582 a.skip(2);
583 let dest = a.i2();
584 self.copy_rows(x0, y0, x1, y1, dest);
585 }
586 (1, b'B') => {
587 self.button = ButtonStyle {
588 width: a.n2(),
589 height: a.n2(),
590 bevel: {
591 a.skip(6);
594 a.n2()
595 },
596 dfore: a.n2() as u8 & 15,
597 dback: a.n2() as u8 & 15,
598 bright: a.n2() as u8 & 15,
599 dark: a.n2() as u8 & 15,
600 surface: a.n2() as u8 & 15,
601 };
602 }
603 (1, b'U') => {
604 let (x0, y0, x1, y1, _hotkey, _flags) = (a.i2(), a.i2(), a.i2(), a.i2(), a.n2(), a.n1());
605 a.skip(1);
606 let text = a.rest().to_vec();
607 self.draw_button(x0, y0, x1, y1, &text);
608 }
609 (1, b'I') => {
610 let (x, y, mode, clip) = (a.i2(), a.i2(), a.n2(), a.n1() == 1);
611 a.skip(2);
612 if let Some(name) = file_name(a.rest()) {
613 let name = if name.contains('.') { name } else { format!("{name}.ICN") };
614 self.load_icon(x, y, mode, clip, &name, io);
615 }
616 }
617 (1, b'W') => {
618 a.skip(1);
619 if let (Some(name), Some((w, h, px))) = (file_name(a.rest()), &self.clipboard) {
620 let data = encode_icon(*w, *h, px);
621 let saved = io.rip_dir().ok_or_else(|| anyhow::anyhow!("no file cache")).and_then(|dir| {
622 std::fs::create_dir_all(&dir)?;
623 Ok(std::fs::write(dir.join(&name), data)?)
624 });
625 if let Err(e) = saved {
626 log::warn!("RIP icon {name}: {e:#}");
627 }
628 }
629 }
630 (1, b'R') => {
631 a.skip(8);
632 let found = file_name(a.rest()).and_then(|n| io.find(&n));
633 match found.map(std::fs::read) {
634 _ if self.scene_depth >= MAX_SCENE_DEPTH => log::warn!("RIP scenes nested too deep"),
635 Some(Ok(data)) if data.len() as u64 <= MAX_FILE => self.scene = Some(data),
636 _ => log::info!("RIP scene {:?} not found", String::from_utf8_lossy(a.rest())),
637 }
638 }
639 (1, b'F') => {
640 let mode = a.n2();
641 a.skip(4);
642 let Some(name) = file_name(a.rest()) else { return };
643 let meta = io.find(&name).and_then(|p| std::fs::metadata(p).ok());
644 let reply = match (mode, meta) {
645 (0, m) => if m.is_some() { "1" } else { "0" }.to_string(),
646 (1, m) => if m.is_some() { "1\r\n" } else { "0\r\n" }.to_string(),
647 (_, None) => "0\r\n".to_string(),
648 (2, Some(m)) => format!("1.{}\r\n", m.len()),
649 (3, Some(m)) => {
650 format!("1.{}.{}\r\n", m.len(), rip_time(m.modified().unwrap_or(std::time::UNIX_EPOCH)))
651 }
652 (_, Some(m)) => {
653 let t = rip_time(m.modified().unwrap_or(std::time::UNIX_EPOCH));
654 format!("1.{}.{}.{t}\r\n", name.to_ascii_uppercase(), m.len())
655 }
656 };
657 io.replies.extend_from_slice(reply.as_bytes());
658 }
659 _ => log::debug!("unhandled RIP command {level}{}", op as char),
660 }
661 }
662
663 fn reset(&mut self, s: &mut Screen) {
665 *self = Rip { line: self.line.take(), eat_lf: self.eat_lf, scene_depth: self.scene_depth, ..Rip::default() };
666 s.set_scroll_region(0, s.rows() - 1);
667 s.erase_display(2);
668 s.move_to(0, 0);
669 }
670
671 fn plot(&mut self, x: i32, y: i32, c: u8) {
673 let v = self.view;
674 let (x, y) = (x + v.x0 as i32, y + v.y0 as i32);
675 if x < v.x0 as i32 || y < v.y0 as i32 || x > v.x1 as i32 || y > v.y1 as i32 {
676 return;
677 }
678 let p = &mut self.pixels[y as usize * WIDTH + x as usize];
679 *p = if self.xor { *p ^ c } else { c };
680 }
681
682 fn get(&self, x: i32, y: i32) -> Option<u8> {
683 let v = self.view;
684 let (x, y) = (x + v.x0 as i32, y + v.y0 as i32);
685 (x >= v.x0 as i32 && y >= v.y0 as i32 && x <= v.x1 as i32 && y <= v.y1 as i32)
686 .then(|| self.pixels[y as usize * WIDTH + x as usize])
687 }
688
689 fn fill_at(&mut self, x: i32, y: i32) {
692 let v = self.view;
693 let bit = self.fill[(y + v.y0 as i32).rem_euclid(8) as usize] & (0x80 >> (x + v.x0 as i32).rem_euclid(8));
694 let c = if bit != 0 { self.fill_color } else { 0 };
695 self.plot(x, y, c);
696 }
697
698 fn line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) {
700 let (dx, dy) = ((x1 - x0).abs(), -(y1 - y0).abs());
701 let (sx, sy) = (if x0 < x1 { 1 } else { -1 }, if y0 < y1 { 1 } else { -1 });
702 let steep = -dy > dx;
703 let (mut x, mut y, mut err) = (x0, y0, dx + dy);
704 let mut n = 0u32;
705 loop {
706 if self.line_style & (0x8000 >> (n % 16)) != 0 {
707 self.plot(x, y, self.color);
708 if self.thick {
709 let (ox, oy) = if steep { (1, 0) } else { (0, 1) };
710 self.plot(x - ox, y - oy, self.color);
711 self.plot(x + ox, y + oy, self.color);
712 }
713 }
714 n += 1;
715 if x == x1 && y == y1 {
716 break;
717 }
718 let e2 = 2 * err;
719 if e2 >= dy {
720 err += dy;
721 x += sx;
722 }
723 if e2 <= dx {
724 err += dx;
725 y += sy;
726 }
727 }
728 }
729
730 fn polyline(&mut self, pts: &[(i32, i32)], closed: bool) {
731 for w in pts.windows(2) {
732 self.line(w[0].0, w[0].1, w[1].0, w[1].1);
733 }
734 if closed && pts.len() > 2 {
735 let (a, b) = (pts[pts.len() - 1], pts[0]);
736 self.line(a.0, a.1, b.0, b.1);
737 }
738 }
739
740 fn bar(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) {
741 for y in y0.min(y1)..=y0.max(y1) {
742 for x in x0.min(x1)..=x0.max(x1) {
743 self.fill_at(x, y);
744 }
745 }
746 }
747
748 fn fill_polygon(&mut self, pts: &[(i32, i32)]) {
750 if pts.len() < 3 {
751 return;
752 }
753 let (top, bottom) = pts.iter().fold((i32::MAX, i32::MIN), |(t, b), &(_, y)| (t.min(y), b.max(y)));
754 for y in top..=bottom {
755 let yc = y as f64 + 0.5;
756 let mut xs: Vec<f64> = Vec::new();
757 for i in 0..pts.len() {
758 let (a, b) = (pts[i], pts[(i + 1) % pts.len()]);
759 let (ay, by) = (a.1 as f64, b.1 as f64);
760 if (ay <= yc) != (by <= yc) {
761 xs.push(a.0 as f64 + (yc - ay) / (by - ay) * (b.0 - a.0) as f64);
762 }
763 }
764 xs.sort_by(f64::total_cmp);
765 for pair in xs.chunks(2) {
766 if let [l, r] = pair {
767 for x in l.round() as i32..=r.round() as i32 {
768 self.fill_at(x, y);
769 }
770 }
771 }
772 }
773 }
774
775 fn pie(&mut self, x: i32, y: i32, st: i32, end: i32, xr: f64, yr: f64) {
776 let mut pts = vec![(x, y)];
777 pts.extend(arc_points(x, y, st, end, xr, yr));
778 self.fill_polygon(&pts);
779 self.polyline(&pts, true);
780 }
781
782 fn flood(&mut self, x: i32, y: i32, border: u8) {
785 let v = self.view;
786 let (w, h) = ((v.x1 - v.x0 + 1) as i32, (v.y1 - v.y0 + 1) as i32);
787 if self.get(x, y).is_none_or(|c| c == border) {
788 return;
789 }
790 let mut seen = vec![false; (w * h) as usize];
791 let mut stack = vec![(x, y)];
792 while let Some((px, py)) = stack.pop() {
793 if px < 0 || py < 0 || px >= w || py >= h {
794 continue;
795 }
796 let i = (py * w + px) as usize;
797 if seen[i] || self.get(px, py) == Some(border) {
798 continue;
799 }
800 let mut l = px;
802 while l > 0 && !seen[(py * w + l - 1) as usize] && self.get(l - 1, py) != Some(border) {
803 l -= 1;
804 }
805 let mut r = px;
806 while r + 1 < w && !seen[(py * w + r + 1) as usize] && self.get(r + 1, py) != Some(border) {
807 r += 1;
808 }
809 for xx in l..=r {
810 seen[(py * w + xx) as usize] = true;
811 self.fill_at(xx, py);
812 stack.push((xx, py - 1));
813 stack.push((xx, py + 1));
814 }
815 }
816 }
817
818 fn text(&mut self, text: &[u8]) {
821 let k = self.font_size as i32;
822 for &ch in text {
823 let (x0, y0) = self.pos;
824 for gy in 0..8 {
825 let bits = FONT_8X8[ch as usize * 8 + gy as usize];
826 for gx in 0..8 {
827 if bits & (0x80 >> gx) == 0 {
828 continue;
829 }
830 for sy in 0..k {
831 for sx in 0..k {
832 let (px, py) = if self.vertical {
834 (x0 + gy * k + sy, y0 - gx * k - sx)
835 } else {
836 (x0 + gx * k + sx, y0 + gy * k + sy)
837 };
838 self.plot(px, py, self.color);
839 }
840 }
841 }
842 }
843 if self.vertical {
844 self.pos.1 -= 8 * k;
845 } else {
846 self.pos.0 += 8 * k;
847 }
848 }
849 }
850
851 fn region(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, invert: bool, text: Vec<u8>) {
852 let (x1, y1) = (x1.max(x0).min(WIDTH - 1), y1.max(y0).min(HEIGHT - 1));
853 self.regions.push(MouseRegion { x0, y0, x1, y1, invert, text });
854 }
855
856 fn copy_to_clipboard(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) {
857 let (w, h) = ((x1 - x0 + 1).max(0) as usize, (y1 - y0 + 1).max(0) as usize);
858 let data = (0..h as i32)
859 .flat_map(|dy| (0..w as i32).map(move |dx| (dx, dy)))
860 .map(|(dx, dy)| self.get(x0 + dx, y0 + dy).unwrap_or(0))
861 .collect();
862 self.clipboard = Some((w, h, data));
863 }
864
865 fn paste(&mut self, x: i32, y: i32, mode: usize) {
867 let Some((w, h, data)) = self.clipboard.clone() else { return };
868 self.stamp(x, y, mode, w, h, &data);
869 }
870
871 fn load_icon(&mut self, x: i32, y: i32, mode: usize, clip: bool, name: &str, io: &Io) {
875 let file = io.find(name).filter(|p| std::fs::metadata(p).is_ok_and(|m| m.len() <= MAX_FILE));
876 match file.and_then(|p| std::fs::read(p).ok()) {
877 Some(data) => {
878 let Some((w, h, px)) = decode_icon(&data) else {
879 log::warn!("RIP icon {name}: not an icon file");
880 return;
881 };
882 let v = self.view;
883 if v.x0 + x as usize + w - 1 > v.x1 || v.y0 + y as usize + h - 1 > v.y1 {
884 return;
885 }
886 self.stamp(x, y, mode, w, h, &px);
887 if clip {
888 self.clipboard = Some((w, h, px));
889 }
890 }
891 None => {
892 let label = format!("Icon {}", name.to_ascii_uppercase());
893 let saved = (self.color, self.line_style, self.thick, self.font_size, self.vertical, self.xor);
894 (self.color, self.line_style, self.thick, self.font_size, self.vertical, self.xor) =
895 (12, 0xCCCC, false, 1, false, false);
896 let width = 8 * label.len().max(13) as i32;
897 let (x1, y1) = (x + width + 24, y + 37);
898 self.polyline(&[(x, y), (x1, y), (x1, y1), (x, y1)], true);
899 for (text, dy) in [(label.as_bytes(), 6), (b"not available".as_slice(), 20)] {
900 self.pos = (x + (x1 - x + 1 - 8 * text.len() as i32) / 2, y + dy);
901 self.text(text);
902 }
903 (self.color, self.line_style, self.thick, self.font_size, self.vertical, self.xor) = saved;
904 if clip {
905 self.copy_to_clipboard(x, y, x1, y1);
906 }
907 }
908 }
909 }
910
911 fn stamp(&mut self, x: i32, y: i32, mode: usize, w: usize, h: usize, data: &[u8]) {
912 let xor = self.xor;
913 self.xor = false;
914 for dy in 0..h {
915 for dx in 0..w {
916 let (px, py) = (x + dx as i32, y + dy as i32);
917 let (src, dst) = (data[dy * w + dx], self.get(px, py).unwrap_or(0));
918 let c = match mode {
919 1 => dst ^ src,
920 2 => dst | src,
921 3 => dst & src,
922 4 => !src & 15,
923 _ => src,
924 };
925 self.plot(px, py, c);
926 }
927 }
928 self.xor = xor;
929 }
930
931 fn copy_rows(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, dest: i32) {
933 let clipboard = self.clipboard.take();
934 self.copy_to_clipboard(x0, y0, x1, y1);
935 self.paste(x0, dest, 0);
936 self.clipboard = clipboard;
937 }
938
939 fn draw_button(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, text: &[u8]) {
942 let st = self.button;
943 let (x1, y1) = if st.width > 0 && st.height > 0 {
944 (x0 + st.width as i32 - 1, y0 + st.height as i32 - 1)
945 } else {
946 (x1, y1)
947 };
948 let mut parts = split_on(text, b"<>");
949 let (_icon, label, command) =
950 (parts.next().unwrap_or_default(), parts.next().unwrap_or_default(), parts.next().unwrap_or_default());
951 let saved = (self.fill, self.fill_color, self.color, self.line_style, self.thick, self.xor);
952 (self.fill, self.line_style, self.thick, self.xor) = (FILL_PATTERNS[1], 0xFFFF, false, false);
953 let b = st.bevel as i32;
954 if b > 0 {
956 self.fill_color = st.bright;
957 self.fill_polygon(&[(x0 - b, y0 - b), (x1 + b, y0 - b), (x1, y0), (x0, y0), (x0, y1), (x0 - b, y1 + b)]);
958 self.fill_color = st.dark;
959 self.fill_polygon(&[(x1 + b, y0 - b), (x1 + b, y1 + b), (x0 - b, y1 + b), (x0, y1), (x1, y1), (x1, y0)]);
960 }
961 self.fill_color = st.surface;
962 self.bar(x0, y0, x1, y1);
963 let k = self.font_size as i32;
965 let (lw, lh) = (label.len() as i32 * 8 * k, 8 * k);
966 let (lx, ly) = (x0 + (x1 - x0 + 1 - lw) / 2, y0 + (y1 - y0 + 1 - lh) / 2);
967 let vertical = std::mem::replace(&mut self.vertical, false);
968 if st.dback != st.surface {
969 self.color = st.dback;
970 self.pos = (lx + 1, ly + 1);
971 self.text(label);
972 }
973 self.color = st.dfore;
974 self.pos = (lx, ly);
975 self.text(label);
976 self.vertical = vertical;
977 (self.fill, self.fill_color, self.color, self.line_style, self.thick, self.xor) = saved;
978 if !command.is_empty() {
979 let v = self.view;
980 let at = |p: i32, o: usize| (p + o as i32).max(0) as usize;
981 self.region(at(x0, v.x0), at(y0, v.y0), at(x1, v.x0), at(y1, v.y0), true, host_command(command));
982 }
983 }
984}
985
986fn arc_points(cx: i32, cy: i32, st: i32, end: i32, xr: f64, yr: f64) -> Vec<(i32, i32)> {
989 let end = if end <= st { end + 360 } else { end };
990 let steps = ((end - st) as f64 * (xr.max(yr) / 20.0).max(1.0) / 3.0).ceil().clamp(4.0, 720.0) as usize;
991 (0..=steps)
992 .map(|i| {
993 let a = (st as f64 + (end - st) as f64 * i as f64 / steps as f64).to_radians();
994 ((cx as f64 + xr * a.cos()).round() as i32, (cy as f64 - yr * a.sin()).round() as i32)
995 })
996 .collect()
997}
998
999fn host_command(text: &[u8]) -> Vec<u8> {
1001 let mut out = Vec::with_capacity(text.len());
1002 let mut i = 0;
1003 while i < text.len() {
1004 match (text[i], text.get(i + 1)) {
1005 (b'^', Some(&c)) if (b'@'..=b'_').contains(&c.to_ascii_uppercase()) => {
1006 out.push(c.to_ascii_uppercase() - b'@');
1007 i += 2;
1008 }
1009 (c, _) => {
1010 out.push(c);
1011 i += 1;
1012 }
1013 }
1014 }
1015 out
1016}
1017
1018fn split_on<'a>(text: &'a [u8], sep: &'a [u8]) -> impl Iterator<Item = &'a [u8]> {
1019 let mut rest = Some(text);
1020 std::iter::from_fn(move || {
1021 let r = rest?;
1022 match r.windows(sep.len()).position(|w| w == sep) {
1023 Some(i) => {
1024 rest = Some(&r[i + sep.len()..]);
1025 Some(&r[..i])
1026 }
1027 None => {
1028 rest = None;
1029 Some(r)
1030 }
1031 }
1032 })
1033}
1034
1035struct Args<'a>(&'a [u8]);
1037
1038impl Args<'_> {
1039 fn digits(&mut self, n: usize) -> usize {
1040 let (head, tail) = self.0.split_at(n.min(self.0.len()));
1041 self.0 = tail;
1042 head.iter().fold(0, |v, &c| {
1043 let d = match c {
1044 b'0'..=b'9' => c - b'0',
1045 b'A'..=b'Z' => c - b'A' + 10,
1046 b'a'..=b'z' => c - b'a' + 10,
1047 _ => 0,
1048 };
1049 v * 36 + d as usize
1050 })
1051 }
1052
1053 fn n1(&mut self) -> usize {
1054 self.digits(1)
1055 }
1056
1057 fn n2(&mut self) -> usize {
1058 self.digits(2)
1059 }
1060
1061 fn n4(&mut self) -> usize {
1062 self.digits(4)
1063 }
1064
1065 fn i2(&mut self) -> i32 {
1066 self.n2() as i32
1067 }
1068
1069 fn skip(&mut self, n: usize) {
1070 self.0 = &self.0[n.min(self.0.len())..];
1071 }
1072
1073 fn rest(&self) -> &[u8] {
1074 self.0
1075 }
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080 use super::*;
1081 use crate::ansi::{Encoding, Terminal};
1082
1083 fn term(data: &[u8]) -> Terminal {
1084 let mut t = Terminal::new(COLS, ROWS, 0);
1085 t.encoding = Encoding::Rip;
1086 t.start_emulation();
1087 t.feed(data);
1088 t
1089 }
1090
1091 fn px(t: &Terminal, x: usize, y: usize) -> u8 {
1092 t.rip().expect("RIP is on").pixels()[y * WIDTH + x]
1093 }
1094
1095 #[test]
1096 fn megnums_and_escapes() {
1097 let mut a = Args(b"0A1Z00hello\\|");
1098 assert_eq!((a.n2(), a.n2(), a.n2()), (10, 71, 0));
1099 assert_eq!(host_command(b"go^m"), b"go\r");
1100 assert_eq!(split_on(b"a<>b<>c", b"<>").collect::<Vec<_>>(), [b"a" as &[u8], b"b", b"c"]);
1101 }
1102
1103 #[test]
1104 fn draws_lines_bars_and_text_beside_ansi() {
1105 let t = term(b"!|c0E|L00000A00|S0104|B0A0A0F0F\r\nHello\r\n");
1107 assert_eq!((px(&t, 0, 0), px(&t, 10, 0), px(&t, 11, 0)), (14, 14, 0));
1108 assert_eq!((px(&t, 12, 12), px(&t, 16, 16)), (4, 0));
1109 assert_eq!(Screen::row_text(t.screen.row(0)).trim_end(), "Hello");
1110 let t = term(b"!Hi\r\n");
1112 assert_eq!(Screen::row_text(t.screen.row(0)).trim_end(), "!Hi");
1113 }
1114
1115 #[test]
1116 fn continuation_fill_and_reset() {
1117 let t = term(b"!|c0F|R0A0A1414|S01\\\r\n02|F0F0F0F\r\n");
1119 assert_eq!((px(&t, 10, 10), px(&t, 15, 15), px(&t, 39, 39), px(&t, 50, 50)), (15, 2, 2, 0));
1120 let t = term(b"!|c0F|L00000Z00|*\r\n");
1121 assert_eq!(px(&t, 5, 0), 0);
1122 }
1123
1124 #[test]
1125 fn buttons_and_mouse_regions() {
1126 let t = term(b"!|1B000000000002000F0F0807000000000000|1U0A0A1E140000<>OK<>yes^M\r\n");
1127 let rip = t.rip().unwrap();
1128 assert_eq!(rip.click(15, 15), Some(b"yes\r".to_vec()));
1129 assert_eq!(rip.click(100, 100), None);
1130 assert_eq!(px(&t, 11, 19), 7);
1132 assert_eq!(px(&t, 9, 12), 15);
1133 let t = term(b"!|1M00000010101100000say hi\r\n");
1134 assert_eq!(t.rip().unwrap().click(5, 5), Some(b"say hi".to_vec()));
1135 let t = term(b"!|1M00000010101100000say hi\r\n!|1K\r\n");
1136 assert_eq!(t.rip().unwrap().click(5, 5), None);
1137 }
1138
1139 #[test]
1140 fn icons_scenes_and_file_queries() {
1141 let dir = std::env::temp_dir().join(format!("badterm-rip-test-{}", std::process::id()));
1142 let _ = std::fs::remove_dir_all(&dir);
1143 let mut t = Terminal::new(COLS, ROWS, 0);
1144 t.encoding = Encoding::Rip;
1145 t.start_emulation();
1146 t.syncterm.cache_dir = Some(dir.clone());
1147 t.feed(b"!|S0109|B00000902|1C00000902|1W0BAR.ICN\r\n");
1150 let icn = std::fs::read(dir.join("RIP/BAR.ICN")).unwrap();
1151 assert_eq!(icn.len(), 4 + 2 * 4 * 3);
1152 assert_eq!(decode_icon(&icn), Some((10, 3, vec![9; 30])));
1153 t.feed(b"!|*|S0103|B0K0K0K0K|1I0K0K01000bar\r\n");
1155 assert_eq!((px(&t, 20, 20), px(&t, 21, 20), px(&t, 29, 22), px(&t, 30, 22)), (3 ^ 9, 9, 9, 0));
1156 t.feed(b"!|1IHN0000000BAR\r\n");
1158 assert_eq!(px(&t, 636, 0), 0);
1159 t.feed(b"!|*|1I0A0A00000NONE\r\n");
1161 assert_eq!((px(&t, 10, 10), px(&t, 12, 10), px(&t, 10, 47)), (12, 0, 12));
1162 let _ = t.take_replies();
1164 t.feed(b"!|1F000000BAR.ICN|1F010000nope.icn|1F020000bar.icn\r\n");
1165 assert_eq!(t.take_replies(), b"10\r\n1.28\r\n");
1166 t.feed(b"!|1F040000BAR.ICN|1F000000../x\r\n");
1167 let r = String::from_utf8(t.take_replies()).unwrap();
1168 assert!(r.starts_with("1.BAR.ICN.28.") && r.ends_with("\r\n") && r.len() == 13 + 17 + 2, "{r}");
1169 std::fs::write(dir.join("RIP/HELLO.RIP"), b"!|c0E|L00000500\r\nfrom the scene\r\n").unwrap();
1172 t.feed(b"!|*|1R00000000hello.rip\r\nafter\r\n");
1173 assert_eq!(px(&t, 3, 0), 14);
1174 assert_eq!(Screen::row_text(t.screen.row(0)).trim_end(), "from the scene");
1175 assert_eq!(Screen::row_text(t.screen.row(1)).trim_end(), "after");
1176 std::fs::write(dir.join("RIP/LOOP.RIP"), b"!|1R00000000LOOP.RIP\r\n").unwrap();
1178 t.feed(b"!|1R00000000LOOP.RIP\r\n");
1179 let _ = std::fs::remove_dir_all(&dir);
1180 assert_eq!(
1181 rip_time(std::time::UNIX_EPOCH + std::time::Duration::from_secs(951_782_400 + 3725)),
1182 "02/29/00.01:02:05"
1183 );
1184 assert_eq!(file_name(b"a/b"), None);
1185 assert_eq!(file_name(b" x.icn "), Some("x.icn".into()));
1186 }
1187
1188 #[test]
1189 fn answers_the_rip_query() {
1190 let mut t = term(b"\x1b[!");
1191 assert_eq!(t.take_replies(), VERSION_REPLY);
1192 assert_eq!(ega_rgb(63), 0xFFFFFF);
1193 assert_eq!(ega_rgb(20), 0xAA5500);
1194 }
1195}