1use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
30use crossterm::style::Color;
31use std::rc::Rc;
32use std::time::Duration;
33
34use crate::Scope;
35use crate::View;
36
37type FrameCallback = Option<Rc<dyn Fn(&mut DrawContext, u64)>>;
39
40#[derive(Clone)]
42pub struct PixelBuffer {
43 width: u16,
44 height: u16,
45 data: Vec<u8>,
47}
48
49impl PixelBuffer {
50 pub fn new(width: u16, height: u16) -> Self {
52 let size = (width as usize) * (height as usize) * 4;
53 Self {
54 width,
55 height,
56 data: vec![0; size],
57 }
58 }
59
60 pub fn dimensions(&self) -> (u16, u16) {
62 (self.width, self.height)
63 }
64
65 pub fn get(&self, x: u16, y: u16) -> (u8, u8, u8, u8) {
67 if x >= self.width || y >= self.height {
68 return (0, 0, 0, 0);
69 }
70 let idx = ((y as usize) * (self.width as usize) + (x as usize)) * 4;
71 (
72 self.data[idx],
73 self.data[idx + 1],
74 self.data[idx + 2],
75 self.data[idx + 3],
76 )
77 }
78
79 pub fn set(&mut self, x: u16, y: u16, r: u8, g: u8, b: u8, a: u8) {
81 if x >= self.width || y >= self.height {
82 return;
83 }
84 let idx = ((y as usize) * (self.width as usize) + (x as usize)) * 4;
85 self.data[idx] = r;
86 self.data[idx + 1] = g;
87 self.data[idx + 2] = b;
88 self.data[idx + 3] = a;
89 }
90
91 pub fn clear(&mut self, r: u8, g: u8, b: u8, a: u8) {
93 for i in (0..self.data.len()).step_by(4) {
94 self.data[i] = r;
95 self.data[i + 1] = g;
96 self.data[i + 2] = b;
97 self.data[i + 3] = a;
98 }
99 }
100
101 pub fn as_bytes(&self) -> &[u8] {
103 &self.data
104 }
105}
106
107pub struct DrawContext<'a> {
111 buffer: &'a mut PixelBuffer,
112}
113
114impl<'a> DrawContext<'a> {
115 pub fn new(buffer: &'a mut PixelBuffer) -> Self {
117 Self { buffer }
118 }
119
120 pub fn dimensions(&self) -> (u16, u16) {
122 self.buffer.dimensions()
123 }
124
125 pub fn clear(&mut self, color: Color) {
127 let (r, g, b) = color_to_rgb(color);
128 self.buffer.clear(r, g, b, 255);
129 }
130
131 pub fn pixel(&mut self, x: u16, y: u16, color: Color) {
133 let (r, g, b) = color_to_rgb(color);
134 self.buffer.set(x, y, r, g, b, 255);
135 }
136
137 pub fn pixel_alpha(&mut self, x: u16, y: u16, color: Color, alpha: u8) {
139 let (r, g, b) = color_to_rgb(color);
140 self.buffer.set(x, y, r, g, b, alpha);
141 }
142
143 pub fn line(&mut self, x1: i32, y1: i32, x2: i32, y2: i32, color: Color) {
145 let (r, g, b) = color_to_rgb(color);
146
147 let dx = (x2 - x1).abs();
148 let dy = -(y2 - y1).abs();
149 let sx = if x1 < x2 { 1 } else { -1 };
150 let sy = if y1 < y2 { 1 } else { -1 };
151 let mut err = dx + dy;
152
153 let mut x = x1;
154 let mut y = y1;
155
156 loop {
157 if x >= 0 && y >= 0 {
158 self.buffer.set(x as u16, y as u16, r, g, b, 255);
159 }
160
161 if x == x2 && y == y2 {
162 break;
163 }
164
165 let e2 = 2 * err;
166 if e2 >= dy {
167 err += dy;
168 x += sx;
169 }
170 if e2 <= dx {
171 err += dx;
172 y += sy;
173 }
174 }
175 }
176
177 pub fn stroke_rect(&mut self, x: u16, y: u16, w: u16, h: u16, color: Color) {
179 if w == 0 || h == 0 {
180 return;
181 }
182 let x2 = x.saturating_add(w).saturating_sub(1);
183 let y2 = y.saturating_add(h).saturating_sub(1);
184
185 self.line(x as i32, y as i32, x2 as i32, y as i32, color);
187 self.line(x as i32, y2 as i32, x2 as i32, y2 as i32, color);
188 self.line(x as i32, y as i32, x as i32, y2 as i32, color);
190 self.line(x2 as i32, y as i32, x2 as i32, y2 as i32, color);
191 }
192
193 pub fn fill_rect(&mut self, x: u16, y: u16, w: u16, h: u16, color: Color) {
195 let (r, g, b) = color_to_rgb(color);
196 for dy in 0..h {
197 for dx in 0..w {
198 self.buffer
199 .set(x.saturating_add(dx), y.saturating_add(dy), r, g, b, 255);
200 }
201 }
202 }
203
204 pub fn circle(&mut self, cx: u16, cy: u16, radius: u16, color: Color) {
206 if radius == 0 {
207 self.pixel(cx, cy, color);
208 return;
209 }
210
211 let (r, g, b) = color_to_rgb(color);
212 let cx = cx as i32;
213 let cy = cy as i32;
214 let mut x = radius as i32;
215 let mut y = 0i32;
216 let mut err = 1 - x;
217
218 while x >= y {
219 self.set_pixel_safe(cx + x, cy + y, r, g, b);
221 self.set_pixel_safe(cx + y, cy + x, r, g, b);
222 self.set_pixel_safe(cx - y, cy + x, r, g, b);
223 self.set_pixel_safe(cx - x, cy + y, r, g, b);
224 self.set_pixel_safe(cx - x, cy - y, r, g, b);
225 self.set_pixel_safe(cx - y, cy - x, r, g, b);
226 self.set_pixel_safe(cx + y, cy - x, r, g, b);
227 self.set_pixel_safe(cx + x, cy - y, r, g, b);
228
229 y += 1;
230 if err < 0 {
231 err += 2 * y + 1;
232 } else {
233 x -= 1;
234 err += 2 * (y - x + 1);
235 }
236 }
237 }
238
239 pub fn fill_circle(&mut self, cx: u16, cy: u16, radius: u16, color: Color) {
241 let (r, g, b) = color_to_rgb(color);
242 let cx = cx as i32;
243 let cy = cy as i32;
244 let radius = radius as i32;
245
246 for dy in -radius..=radius {
247 for dx in -radius..=radius {
248 if dx * dx + dy * dy <= radius * radius {
249 self.set_pixel_safe(cx + dx, cy + dy, r, g, b);
250 }
251 }
252 }
253 }
254
255 fn set_pixel_safe(&mut self, x: i32, y: i32, r: u8, g: u8, b: u8) {
257 if x >= 0 && y >= 0 {
258 self.buffer.set(x as u16, y as u16, r, g, b, 255);
259 }
260 }
261}
262
263fn color_to_rgb(color: Color) -> (u8, u8, u8) {
265 match color {
266 Color::Rgb { r, g, b } => (r, g, b),
267 Color::Black => (0, 0, 0),
268 Color::DarkGrey => (128, 128, 128),
269 Color::Red => (255, 0, 0),
270 Color::DarkRed => (139, 0, 0),
271 Color::Green => (0, 255, 0),
272 Color::DarkGreen => (0, 100, 0),
273 Color::Yellow => (255, 255, 0),
274 Color::DarkYellow => (128, 128, 0),
275 Color::Blue => (0, 0, 255),
276 Color::DarkBlue => (0, 0, 139),
277 Color::Magenta => (255, 0, 255),
278 Color::DarkMagenta => (139, 0, 139),
279 Color::Cyan => (0, 255, 255),
280 Color::DarkCyan => (0, 139, 139),
281 Color::White => (255, 255, 255),
282 Color::Grey => (192, 192, 192),
283 Color::Reset => (0, 0, 0), Color::AnsiValue(v) => ansi_to_rgb(v),
285 }
286}
287
288fn ansi_to_rgb(code: u8) -> (u8, u8, u8) {
290 match code {
291 0..=15 => {
292 let colors = [
294 (0, 0, 0),
295 (128, 0, 0),
296 (0, 128, 0),
297 (128, 128, 0),
298 (0, 0, 128),
299 (128, 0, 128),
300 (0, 128, 128),
301 (192, 192, 192),
302 (128, 128, 128),
303 (255, 0, 0),
304 (0, 255, 0),
305 (255, 255, 0),
306 (0, 0, 255),
307 (255, 0, 255),
308 (0, 255, 255),
309 (255, 255, 255),
310 ];
311 colors[code as usize]
312 }
313 16..=231 => {
314 let n = code - 16;
316 let r = (n / 36) % 6;
317 let g = (n / 6) % 6;
318 let b = n % 6;
319 let to_rgb = |v: u8| if v == 0 { 0 } else { 55 + v * 40 };
320 (to_rgb(r), to_rgb(g), to_rgb(b))
321 }
322 232..=255 => {
323 let gray = 8 + (code - 232) * 10;
325 (gray, gray, gray)
326 }
327 }
328}
329
330#[derive(Clone)]
336pub struct PendingCanvas {
337 pub cell_x: u16,
339 pub cell_y: u16,
341 pub pixels: PixelBuffer,
343 pub id: u32,
345}
346
347pub fn supports_kitty_graphics() -> bool {
349 if let Ok(term) = std::env::var("TERM") {
350 let term_lower = term.to_lowercase();
351 if term_lower.contains("kitty") || term_lower.contains("ghostty") {
352 return true;
353 }
354 }
355
356 if let Ok(term_program) = std::env::var("TERM_PROGRAM") {
358 let program_lower = term_program.to_lowercase();
359 if program_lower.contains("kitty")
360 || program_lower.contains("ghostty")
361 || program_lower.contains("wezterm")
362 {
363 return true;
364 }
365 }
366
367 false
368}
369
370pub fn encode_kitty_graphics(
374 pixels: &PixelBuffer,
375 cell_x: u16,
376 cell_y: u16,
377 image_id: u32,
378) -> String {
379 let (width, height) = pixels.dimensions();
380 if width == 0 || height == 0 {
381 return String::new();
382 }
383
384 let b64_data = BASE64.encode(pixels.as_bytes());
386
387 const CHUNK_SIZE: usize = 4096;
390
391 let mut result = String::new();
392 let chunks: Vec<&str> = b64_data
393 .as_bytes()
394 .chunks(CHUNK_SIZE)
395 .map(|c| std::str::from_utf8(c).unwrap_or(""))
396 .collect();
397
398 let total_chunks = chunks.len();
399
400 for (i, chunk) in chunks.iter().enumerate() {
401 let is_first = i == 0;
402 let is_last = i == total_chunks - 1;
403 let more = if is_last { 0 } else { 1 };
404
405 if is_first {
406 result.push_str(&format!(
414 "\x1b_Ga=T,f=32,s={},v={},i={},t=d,m={},q=2;{}\x1b\\",
415 width, height, image_id, more, chunk
416 ));
417 } else {
418 result.push_str(&format!("\x1b_Gm={};{}\x1b\\", more, chunk));
420 }
421 }
422
423 result.insert_str(0, &format!("\x1b[{};{}H", cell_y + 1, cell_x + 1));
426
427 result
428}
429
430pub fn delete_kitty_image(image_id: u32) -> String {
432 format!("\x1b_Ga=d,d=I,i={},q=2\x1b\\", image_id)
435}
436
437pub fn delete_all_kitty_images() -> String {
439 "\x1b_Ga=d,d=a,q=2\x1b\\".to_string()
442}
443
444pub fn animated_canvas(cx: Scope) -> AnimatedCanvasBuilder {
478 AnimatedCanvasBuilder::new(cx)
479}
480
481pub struct AnimatedCanvasBuilder {
483 cx: Scope,
484 width: u16,
485 height: u16,
486 fps: u32,
487 on_frame: FrameCallback,
488}
489
490impl AnimatedCanvasBuilder {
491 pub fn new(cx: Scope) -> Self {
493 Self {
494 cx,
495 width: 100,
496 height: 50,
497 fps: 30,
498 on_frame: None,
499 }
500 }
501
502 pub fn width(mut self, width: u16) -> Self {
504 self.width = width;
505 self
506 }
507
508 pub fn height(mut self, height: u16) -> Self {
510 self.height = height;
511 self
512 }
513
514 pub fn fps(mut self, fps: u32) -> Self {
519 self.fps = fps.max(1); self
521 }
522
523 pub fn on_frame<F>(mut self, f: F) -> Self
528 where
529 F: Fn(&mut DrawContext, u64) + 'static,
530 {
531 self.on_frame = Some(Rc::new(f));
532 self
533 }
534
535 pub fn build(self) -> View {
541 let delay_ms = 1000 / self.fps as u64;
542
543 struct AnimatedCanvasStreamKey;
545 let frame_stream = self.cx.use_stream_keyed::<AnimatedCanvasStreamKey, _, _, _>(move || {
546 (0u64..).inspect(move |&i| {
547 if i > 0 {
548 std::thread::sleep(Duration::from_millis(delay_ms));
549 }
550 })
551 });
552
553 let current_frame = frame_stream.get();
554 let on_frame = self.on_frame;
555 let width = self.width;
556 let height = self.height;
557
558 View::canvas()
560 .width(width)
561 .height(height)
562 .on_draw(move |ctx| {
563 if let Some(ref callback) = on_frame {
564 callback(ctx, current_frame);
565 }
566 })
567 .build()
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574
575 #[test]
576 fn test_pixel_buffer_new() {
577 let buf = PixelBuffer::new(10, 10);
578 assert_eq!(buf.dimensions(), (10, 10));
579 assert_eq!(buf.get(0, 0), (0, 0, 0, 0));
580 }
581
582 #[test]
583 fn test_pixel_buffer_set_get() {
584 let mut buf = PixelBuffer::new(10, 10);
585 buf.set(5, 5, 255, 128, 64, 255);
586 assert_eq!(buf.get(5, 5), (255, 128, 64, 255));
587 }
588
589 #[test]
590 fn test_pixel_buffer_clear() {
591 let mut buf = PixelBuffer::new(10, 10);
592 buf.clear(100, 150, 200, 255);
593 assert_eq!(buf.get(0, 0), (100, 150, 200, 255));
594 assert_eq!(buf.get(9, 9), (100, 150, 200, 255));
595 }
596
597 #[test]
598 fn test_draw_context_line() {
599 let mut buf = PixelBuffer::new(10, 10);
600 {
601 let mut ctx = DrawContext::new(&mut buf);
602 ctx.line(0, 0, 9, 0, Color::White);
603 }
604 assert_eq!(buf.get(0, 0), (255, 255, 255, 255));
606 assert_eq!(buf.get(5, 0), (255, 255, 255, 255));
607 assert_eq!(buf.get(9, 0), (255, 255, 255, 255));
608 }
609
610 #[test]
611 fn test_draw_context_fill_rect() {
612 let mut buf = PixelBuffer::new(10, 10);
613 {
614 let mut ctx = DrawContext::new(&mut buf);
615 ctx.fill_rect(2, 2, 3, 3, Color::Red);
616 }
617 assert_eq!(buf.get(2, 2), (255, 0, 0, 255));
618 assert_eq!(buf.get(4, 4), (255, 0, 0, 255));
619 assert_eq!(buf.get(1, 1), (0, 0, 0, 0)); }
621
622 #[test]
623 fn test_color_to_rgb() {
624 assert_eq!(color_to_rgb(Color::Red), (255, 0, 0));
625 assert_eq!(color_to_rgb(Color::Green), (0, 255, 0));
626 assert_eq!(color_to_rgb(Color::Blue), (0, 0, 255));
627 assert_eq!(
628 color_to_rgb(Color::Rgb {
629 r: 100,
630 g: 150,
631 b: 200
632 }),
633 (100, 150, 200)
634 );
635 }
636}