| 1 | //! The dot-whale pet as a ratatui widget. |
| 2 | //! |
| 3 | //! `pet_core` is the same `rs/pet_sim.rs` the standalone runner compiles — |
| 4 | //! one file, one authority. This crate adds only the render path: particles → |
| 5 | //! braille cells → ratatui `Buffer`, coloured with the frame's computed RGB. |
| 6 | //! |
| 7 | //! Demo mode (no real terminal needed — renders through TestBackend and emits |
| 8 | //! truecolor ANSI so you can see the actual frame): |
| 9 | //! cargo run --offline -- ../tape.tsv [--frame N] |
| 10 | //! With `--live` it runs the tape looped on a real crossterm-less ANSI stream. |
| 11 | |
| 12 | use ratatui::buffer::Buffer; |
| 13 | use ratatui::layout::Rect; |
| 14 | use ratatui::style::Color; |
| 15 | |
| 16 | #[path = "../../rs/pet_sim.rs"] |
| 17 | mod pet_sim; |
| 18 | use pet_sim::*; |
| 19 | |
| 20 | #[path = "../../../crates/tui/src/tui/ambient_life/pet_widget.rs"] |
| 21 | mod pet_widget; |
| 22 | use pet_widget::PetWidget; |
| 23 | |
| 24 | fn parse_row(c: Vec<&str>) -> Option<(f64, PetState)> { |
| 25 | if c.len() < 10 || c[0] == "dt" { return None; } |
| 26 | let channel = ChannelId::from_key(c[4]).expect("known tape channel"); |
| 27 | Some((c[0].parse().unwrap(), PetState { |
| 28 | activity: c[1].parse().unwrap(), coherence: c[2].parse().unwrap(), |
| 29 | attention: c[3].parse().unwrap(), channel, |
| 30 | observed: c[5].parse().unwrap(), roam_x: c[6].parse().unwrap(), |
| 31 | roam_y: c[7].parse().unwrap(), flip: c[8].parse().unwrap(), lit: c[9].parse().unwrap(), |
| 32 | })) |
| 33 | } |
| 34 | |
| 35 | /// Render one frame through ratatui's TestBackend and emit it as truecolor |
| 36 | /// ANSI — what a real terminal would show, without needing a PTY. |
| 37 | fn emit_ansi(buf: &Buffer, area: Rect) { |
| 38 | for y in 0..area.height { |
| 39 | for x in 0..area.width { |
| 40 | let cell = &buf[(x, y)]; |
| 41 | let sym = cell.symbol(); |
| 42 | if sym == " " { print!(" "); continue; } |
| 43 | match cell.fg { |
| 44 | Color::Rgb(r, g, b) => print!("\x1b[38;2;{r};{g};{b}m{sym}\x1b[0m"), |
| 45 | _ => print!("{sym}"), |
| 46 | } |
| 47 | } |
| 48 | println!(); |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | fn main() { |
| 53 | let args: Vec<String> = std::env::args().collect(); |
| 54 | let motion = !args.iter().any(|s| s == "--reduced-motion"); |
| 55 | let tape_path = args.get(1).map(|s| s.as_str()).unwrap_or("../tape.tsv"); |
| 56 | let frame_target: Option<usize> = args.iter().position(|a| a == "--frame") |
| 57 | .and_then(|i| args.get(i + 1)).and_then(|s| s.parse().ok()); |
| 58 | let tape = std::fs::read_to_string(tape_path).expect("tape.tsv"); |
| 59 | let rows: Vec<(f64, PetState)> = |
| 60 | tape.trim().lines().filter_map(|l| parse_row(l.split('\t').collect())).collect(); |
| 61 | |
| 62 | let mut sim = PetSim::whale(); |
| 63 | let (w, h) = (90u16, 30u16); |
| 64 | let backend = ratatui::backend::TestBackend::new(w, h); |
| 65 | let mut terminal = ratatui::Terminal::new(backend).unwrap(); |
| 66 | |
| 67 | let frames: Vec<usize> = match frame_target { |
| 68 | Some(f) => vec![f], |
| 69 | None => vec![60, 240, 480, 780, 1080, 1320, 1560, 1800], |
| 70 | }; |
| 71 | |
| 72 | let mut next = frames.iter().peekable(); |
| 73 | for (i, (dt, s)) in rows.iter().enumerate() { |
| 74 | sim.step(*dt, s, motion, 1.0); |
| 75 | if next.peek() == Some(&&i) { |
| 76 | terminal.draw(|f| { |
| 77 | let wgt = PetWidget { sim: &sim, state: s }; |
| 78 | f.render_widget(wgt, f.area()); |
| 79 | }).unwrap(); |
| 80 | println!("── tape frame {i} · {} · {} ──", sim.frame.channel, sim.frame.arch); |
| 81 | emit_ansi(terminal.backend().buffer(), Rect::new(0, 0, w, h)); |
| 82 | next.next(); |
| 83 | if next.peek().is_none() { break; } |
| 84 | } |
| 85 | } |
| 86 | } |
| 87 |