1use ratatui::buffer::Buffer;
7use ratatui::layout::{Constraint, Rect};
8use ratatui::style::{Modifier, Style};
9use ratatui::text::{Line, Span};
10use ratatui::widgets::{Block, Cell, Clear, Paragraph, Row, Table, Widget};
11use ratatui::Frame;
12
13use super::cell::{format_color_compact, format_modifier_compact, CellPreview};
14use super::table::{ActionLogOverlay, DebugTableOverlay, DebugTableRow};
15
16pub fn buffer_to_text(buffer: &Buffer) -> String {
20 let area = buffer.area;
21 let mut out = String::new();
22
23 for y in area.y..area.y.saturating_add(area.height) {
24 let mut line = String::new();
25 for x in area.x..area.x.saturating_add(area.width) {
26 line.push_str(buffer[(x, y)].symbol());
27 }
28 out.push_str(line.trim_end_matches(' '));
29 if y + 1 < area.y.saturating_add(area.height) {
30 out.push('\n');
31 }
32 }
33
34 out
35}
36
37pub fn paint_snapshot(f: &mut Frame, snapshot: &Buffer) {
41 let screen = f.area();
42 f.render_widget(Clear, screen);
43
44 let snap_area = snapshot.area;
45 let x_end = screen
46 .x
47 .saturating_add(screen.width)
48 .min(snap_area.x.saturating_add(snap_area.width));
49 let y_end = screen
50 .y
51 .saturating_add(screen.height)
52 .min(snap_area.y.saturating_add(snap_area.height));
53
54 for y in screen.y..y_end {
55 for x in screen.x..x_end {
56 f.buffer_mut()[(x, y)] = snapshot[(x, y)].clone();
57 }
58 }
59}
60
61pub fn dim_buffer(buffer: &mut Buffer, factor: f32) {
67 let factor = factor.clamp(0.0, 1.0);
68 let scale = 1.0 - factor; for cell in buffer.content.iter_mut() {
71 if contains_emoji(cell.symbol()) {
73 cell.set_symbol(" ");
74 }
75 cell.fg = dim_color(cell.fg, scale);
76 cell.bg = dim_color(cell.bg, scale);
77 }
78}
79
80fn contains_emoji(s: &str) -> bool {
82 for c in s.chars() {
83 if is_emoji(c) {
84 return true;
85 }
86 }
87 false
88}
89
90fn is_emoji(c: char) -> bool {
96 let cp = c as u32;
97 matches!(cp,
99 0x1F300..=0x1F5FF |
101 0x1F600..=0x1F64F |
103 0x1F680..=0x1F6FF |
105 0x1F900..=0x1F9FF |
107 0x1FA00..=0x1FA6F |
109 0x1FA70..=0x1FAFF |
111 0x1F1E0..=0x1F1FF
113 )
114}
115
116fn dim_color(color: ratatui::style::Color, scale: f32) -> ratatui::style::Color {
118 use ratatui::style::Color;
119
120 match color {
121 Color::Rgb(r, g, b) => Color::Rgb(
122 ((r as f32) * scale) as u8,
123 ((g as f32) * scale) as u8,
124 ((b as f32) * scale) as u8,
125 ),
126 Color::Indexed(idx) => {
127 if let Some((r, g, b)) = indexed_to_rgb(idx) {
130 Color::Rgb(
131 ((r as f32) * scale) as u8,
132 ((g as f32) * scale) as u8,
133 ((b as f32) * scale) as u8,
134 )
135 } else {
136 color }
138 }
139 Color::Black => Color::Black,
141 Color::Red => dim_named_color(205, 0, 0, scale),
142 Color::Green => dim_named_color(0, 205, 0, scale),
143 Color::Yellow => dim_named_color(205, 205, 0, scale),
144 Color::Blue => dim_named_color(0, 0, 238, scale),
145 Color::Magenta => dim_named_color(205, 0, 205, scale),
146 Color::Cyan => dim_named_color(0, 205, 205, scale),
147 Color::Gray => dim_named_color(229, 229, 229, scale),
148 Color::DarkGray => dim_named_color(127, 127, 127, scale),
149 Color::LightRed => dim_named_color(255, 0, 0, scale),
150 Color::LightGreen => dim_named_color(0, 255, 0, scale),
151 Color::LightYellow => dim_named_color(255, 255, 0, scale),
152 Color::LightBlue => dim_named_color(92, 92, 255, scale),
153 Color::LightMagenta => dim_named_color(255, 0, 255, scale),
154 Color::LightCyan => dim_named_color(0, 255, 255, scale),
155 Color::White => dim_named_color(255, 255, 255, scale),
156 Color::Reset => Color::Reset,
157 }
158}
159
160fn dim_named_color(r: u8, g: u8, b: u8, scale: f32) -> ratatui::style::Color {
161 ratatui::style::Color::Rgb(
162 ((r as f32) * scale) as u8,
163 ((g as f32) * scale) as u8,
164 ((b as f32) * scale) as u8,
165 )
166}
167
168fn indexed_to_rgb(idx: u8) -> Option<(u8, u8, u8)> {
170 match idx {
171 0 => Some((0, 0, 0)), 1 => Some((128, 0, 0)), 2 => Some((0, 128, 0)), 3 => Some((128, 128, 0)), 4 => Some((0, 0, 128)), 5 => Some((128, 0, 128)), 6 => Some((0, 128, 128)), 7 => Some((192, 192, 192)), 8 => Some((128, 128, 128)), 9 => Some((255, 0, 0)), 10 => Some((0, 255, 0)), 11 => Some((255, 255, 0)), 12 => Some((0, 0, 255)), 13 => Some((255, 0, 255)), 14 => Some((0, 255, 255)), 15 => Some((255, 255, 255)), 16..=231 => {
190 let idx = idx - 16;
191 let r = (idx / 36) % 6;
192 let g = (idx / 6) % 6;
193 let b = idx % 6;
194 let to_rgb = |v: u8| if v == 0 { 0 } else { 55 + v * 40 };
195 Some((to_rgb(r), to_rgb(g), to_rgb(b)))
196 }
197 232..=255 => {
199 let gray = 8 + (idx - 232) * 10;
200 Some((gray, gray, gray))
201 }
202 }
203}
204
205#[derive(Clone)]
207pub struct BannerItem<'a> {
208 pub key: &'a str,
210 pub label: &'a str,
212 pub key_style: Style,
214}
215
216impl<'a> BannerItem<'a> {
217 pub fn new(key: &'a str, label: &'a str, key_style: Style) -> Self {
219 Self {
220 key,
221 label,
222 key_style,
223 }
224 }
225}
226
227pub struct DebugBanner<'a> {
241 title: Option<&'a str>,
242 title_style: Style,
243 items: Vec<BannerItem<'a>>,
244 label_style: Style,
245 background: Style,
246}
247
248impl<'a> Default for DebugBanner<'a> {
249 fn default() -> Self {
250 Self::new()
251 }
252}
253
254impl<'a> DebugBanner<'a> {
255 pub fn new() -> Self {
257 Self {
258 title: None,
259 title_style: Style::default(),
260 items: Vec::new(),
261 label_style: Style::default(),
262 background: Style::default(),
263 }
264 }
265
266 pub fn title(mut self, title: &'a str) -> Self {
268 self.title = Some(title);
269 self
270 }
271
272 pub fn title_style(mut self, style: Style) -> Self {
274 self.title_style = style;
275 self
276 }
277
278 pub fn item(mut self, item: BannerItem<'a>) -> Self {
280 self.items.push(item);
281 self
282 }
283
284 pub fn label_style(mut self, style: Style) -> Self {
286 self.label_style = style;
287 self
288 }
289
290 pub fn background(mut self, style: Style) -> Self {
292 self.background = style;
293 self
294 }
295}
296
297impl Widget for DebugBanner<'_> {
298 fn render(self, area: Rect, buf: &mut Buffer) {
299 if area.height == 0 || area.width == 0 {
300 return;
301 }
302
303 Block::default().style(self.background).render(area, buf);
305
306 let mut spans = Vec::new();
307
308 if let Some(title) = self.title {
310 spans.push(Span::styled(format!(" {title} "), self.title_style));
311 spans.push(Span::raw(" "));
312 }
313
314 for item in &self.items {
316 spans.push(Span::styled(format!(" {} ", item.key), item.key_style));
317 spans.push(Span::styled(format!(" {} ", item.label), self.label_style));
318 }
319
320 let line = Paragraph::new(Line::from(spans)).style(self.background);
321 line.render(area, buf);
322 }
323}
324
325#[derive(Clone)]
327pub struct DebugTableStyle {
328 pub header: Style,
330 pub section: Style,
332 pub key: Style,
334 pub value: Style,
336 pub row_styles: (Style, Style),
338}
339
340impl Default for DebugTableStyle {
341 fn default() -> Self {
342 use super::config::DebugStyle;
343 Self {
344 header: Style::default()
345 .fg(DebugStyle::neon_cyan())
346 .add_modifier(Modifier::BOLD),
347 section: Style::default()
348 .fg(DebugStyle::neon_purple())
349 .add_modifier(Modifier::BOLD),
350 key: Style::default()
351 .fg(DebugStyle::neon_amber())
352 .add_modifier(Modifier::BOLD),
353 value: Style::default().fg(DebugStyle::text_primary()),
354 row_styles: (
355 Style::default().bg(DebugStyle::bg_panel()),
356 Style::default().bg(DebugStyle::bg_surface()),
357 ),
358 }
359 }
360}
361
362pub struct DebugTableWidget<'a> {
364 table: &'a DebugTableOverlay,
365 style: DebugTableStyle,
366}
367
368impl<'a> DebugTableWidget<'a> {
369 pub fn new(table: &'a DebugTableOverlay) -> Self {
371 Self {
372 table,
373 style: DebugTableStyle::default(),
374 }
375 }
376
377 pub fn style(mut self, style: DebugTableStyle) -> Self {
379 self.style = style;
380 self
381 }
382}
383
384impl Widget for DebugTableWidget<'_> {
385 fn render(self, area: Rect, buf: &mut Buffer) {
386 if area.height < 2 || area.width < 10 {
387 return;
388 }
389
390 let max_key_len = self
392 .table
393 .rows
394 .iter()
395 .filter_map(|row| match row {
396 DebugTableRow::Entry { key, .. } => Some(key.chars().count()),
397 DebugTableRow::Section(_) => None,
398 })
399 .max()
400 .unwrap_or(0) as u16;
401
402 let max_label = area.width.saturating_sub(8).max(10);
403 let label_width = max_key_len.saturating_add(2).clamp(12, 30).min(max_label);
404 let constraints = [Constraint::Length(label_width), Constraint::Min(0)];
405
406 let header = Row::new(vec![
408 Cell::from("Field").style(self.style.header),
409 Cell::from("Value").style(self.style.header),
410 ]);
411
412 let rows: Vec<Row> = self
414 .table
415 .rows
416 .iter()
417 .enumerate()
418 .map(|(idx, row)| match row {
419 DebugTableRow::Section(title) => Row::new(vec![
420 Cell::from(format!(" {title} ")).style(self.style.section),
421 Cell::from(""),
422 ]),
423 DebugTableRow::Entry { key, value } => {
424 let row_style = if idx % 2 == 0 {
425 self.style.row_styles.0
426 } else {
427 self.style.row_styles.1
428 };
429 Row::new(vec![
430 Cell::from(key.clone()).style(self.style.key),
431 Cell::from(value.clone()).style(self.style.value),
432 ])
433 .style(row_style)
434 }
435 })
436 .collect();
437
438 let table = Table::new(rows, constraints)
439 .header(header)
440 .column_spacing(2);
441
442 table.render(area, buf);
443 }
444}
445
446pub struct CellPreviewWidget<'a> {
448 preview: &'a CellPreview,
449 label_style: Style,
450 value_style: Style,
451}
452
453impl<'a> CellPreviewWidget<'a> {
454 pub fn new(preview: &'a CellPreview) -> Self {
456 use super::config::DebugStyle;
457 Self {
458 preview,
459 label_style: Style::default().fg(DebugStyle::text_secondary()),
460 value_style: Style::default().fg(DebugStyle::text_primary()),
461 }
462 }
463
464 pub fn label_style(mut self, style: Style) -> Self {
466 self.label_style = style;
467 self
468 }
469
470 pub fn value_style(mut self, style: Style) -> Self {
472 self.value_style = style;
473 self
474 }
475}
476
477impl Widget for CellPreviewWidget<'_> {
478 fn render(self, area: Rect, buf: &mut Buffer) {
479 use super::config::DebugStyle;
480
481 if area.width < 20 || area.height < 1 {
482 return;
483 }
484
485 let char_style = Style::default()
487 .fg(self.preview.fg)
488 .bg(self.preview.bg)
489 .add_modifier(self.preview.modifier);
490
491 let fg_str = format_color_compact(self.preview.fg);
493 let bg_str = format_color_compact(self.preview.bg);
494 let mod_str = format_modifier_compact(self.preview.modifier);
495
496 let char_bg = Style::default().bg(DebugStyle::bg_surface());
498 let mod_style = Style::default().fg(DebugStyle::neon_purple());
499
500 let mut spans = vec![
502 Span::styled(" ", char_bg),
503 Span::styled(self.preview.symbol.clone(), char_style),
504 Span::styled(" ", char_bg),
505 Span::styled(" fg ", self.label_style),
506 Span::styled("█", Style::default().fg(self.preview.fg)),
507 Span::styled(format!(" {fg_str}"), self.value_style),
508 Span::styled(" bg ", self.label_style),
509 Span::styled("█", Style::default().fg(self.preview.bg)),
510 Span::styled(format!(" {bg_str}"), self.value_style),
511 ];
512
513 if !mod_str.is_empty() {
514 spans.push(Span::styled(format!(" {mod_str}"), mod_style));
515 }
516
517 let line = Paragraph::new(Line::from(spans));
518 line.render(area, buf);
519 }
520}
521
522#[derive(Clone)]
528pub struct ActionLogStyle {
529 pub header: Style,
531 pub sequence: Style,
533 pub name: Style,
535 pub params: Style,
537 pub elapsed: Style,
539 pub selected: Style,
541 pub row_styles: (Style, Style),
543}
544
545impl Default for ActionLogStyle {
546 fn default() -> Self {
547 use super::config::DebugStyle;
548 Self {
549 header: Style::default()
550 .fg(DebugStyle::neon_cyan())
551 .add_modifier(Modifier::BOLD),
552 sequence: Style::default().fg(DebugStyle::text_secondary()),
553 name: Style::default()
554 .fg(DebugStyle::neon_amber())
555 .add_modifier(Modifier::BOLD),
556 params: Style::default().fg(DebugStyle::text_primary()),
557 elapsed: Style::default().fg(DebugStyle::text_secondary()),
558 selected: Style::default()
559 .bg(DebugStyle::bg_highlight())
560 .add_modifier(Modifier::BOLD),
561 row_styles: (
562 Style::default().bg(DebugStyle::bg_panel()),
563 Style::default().bg(DebugStyle::bg_surface()),
564 ),
565 }
566 }
567}
568
569pub struct ActionLogWidget<'a> {
577 log: &'a ActionLogOverlay,
578 style: ActionLogStyle,
579 visible_rows: usize,
581}
582
583impl<'a> ActionLogWidget<'a> {
584 pub fn new(log: &'a ActionLogOverlay) -> Self {
586 Self {
587 log,
588 style: ActionLogStyle::default(),
589 visible_rows: 10, }
591 }
592
593 pub fn style(mut self, style: ActionLogStyle) -> Self {
595 self.style = style;
596 self
597 }
598
599 pub fn visible_rows(mut self, rows: usize) -> Self {
601 self.visible_rows = rows;
602 self
603 }
604}
605
606impl Widget for ActionLogWidget<'_> {
607 fn render(self, area: Rect, buf: &mut Buffer) {
608 if area.height < 2 || area.width < 30 {
609 return;
610 }
611
612 let visible_rows = (area.height.saturating_sub(1)) as usize;
614
615 let constraints = [
617 Constraint::Length(5), Constraint::Length(20), Constraint::Min(30), Constraint::Length(8), ];
622
623 let header = Row::new(vec![
625 Cell::from("#").style(self.style.header),
626 Cell::from("Action").style(self.style.header),
627 Cell::from("Params").style(self.style.header),
628 Cell::from("Elapsed").style(self.style.header),
629 ]);
630
631 let scroll_offset = if self.log.selected >= visible_rows {
633 self.log.selected - visible_rows + 1
634 } else {
635 0
636 };
637
638 let rows: Vec<Row> = self
640 .log
641 .entries
642 .iter()
643 .enumerate()
644 .skip(scroll_offset)
645 .take(visible_rows)
646 .map(|(idx, entry)| {
647 let is_selected = idx == self.log.selected;
648 let base_style = if is_selected {
649 self.style.selected
650 } else if idx % 2 == 0 {
651 self.style.row_styles.0
652 } else {
653 self.style.row_styles.1
654 };
655
656 let params = if entry.params.chars().count() > 60 {
658 let truncated: String = entry.params.chars().take(57).collect();
659 format!("{}...", truncated)
660 } else {
661 entry.params.clone()
662 };
663
664 Row::new(vec![
665 Cell::from(format!("{}", entry.sequence)).style(self.style.sequence),
666 Cell::from(entry.name.clone()).style(self.style.name),
667 Cell::from(params).style(self.style.params),
668 Cell::from(entry.elapsed.clone()).style(self.style.elapsed),
669 ])
670 .style(base_style)
671 })
672 .collect();
673
674 let table = Table::new(rows, constraints)
675 .header(header)
676 .column_spacing(1);
677
678 table.render(area, buf);
679 }
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685 use ratatui::layout::Rect;
686
687 #[test]
688 fn test_buffer_to_text() {
689 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
690
691 buffer[(0, 0)].set_char('H');
693 buffer[(1, 0)].set_char('i');
694 buffer[(0, 1)].set_char('!');
695
696 let text = buffer_to_text(&buffer);
697 let lines: Vec<&str> = text.lines().collect();
698
699 assert_eq!(lines[0], "Hi");
700 assert_eq!(lines[1], "!");
701 }
702
703 #[test]
704 fn test_debug_banner() {
705 let banner =
706 DebugBanner::new()
707 .title("TEST")
708 .item(BannerItem::new("F1", "help", Style::default()));
709
710 let mut buffer = Buffer::empty(Rect::new(0, 0, 40, 1));
711 banner.render(Rect::new(0, 0, 40, 1), &mut buffer);
712
713 let text = buffer_to_text(&buffer);
714 assert!(text.contains("TEST"));
715 assert!(text.contains("F1"));
716 assert!(text.contains("help"));
717 }
718}