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) {
65 let factor = factor.clamp(0.0, 1.0);
66 let dim_amount = (255.0 * factor) as u8;
67
68 for cell in buffer.content.iter_mut() {
69 if let ratatui::style::Color::Rgb(r, g, b) = cell.bg {
70 cell.bg = ratatui::style::Color::Rgb(
71 r.saturating_sub(dim_amount),
72 g.saturating_sub(dim_amount),
73 b.saturating_sub(dim_amount),
74 );
75 }
76 }
77}
78
79#[derive(Clone)]
81pub struct BannerItem<'a> {
82 pub key: &'a str,
84 pub label: &'a str,
86 pub key_style: Style,
88}
89
90impl<'a> BannerItem<'a> {
91 pub fn new(key: &'a str, label: &'a str, key_style: Style) -> Self {
93 Self {
94 key,
95 label,
96 key_style,
97 }
98 }
99}
100
101pub struct DebugBanner<'a> {
115 title: Option<&'a str>,
116 title_style: Style,
117 items: Vec<BannerItem<'a>>,
118 label_style: Style,
119 background: Style,
120}
121
122impl<'a> Default for DebugBanner<'a> {
123 fn default() -> Self {
124 Self::new()
125 }
126}
127
128impl<'a> DebugBanner<'a> {
129 pub fn new() -> Self {
131 Self {
132 title: None,
133 title_style: Style::default(),
134 items: Vec::new(),
135 label_style: Style::default(),
136 background: Style::default(),
137 }
138 }
139
140 pub fn title(mut self, title: &'a str) -> Self {
142 self.title = Some(title);
143 self
144 }
145
146 pub fn title_style(mut self, style: Style) -> Self {
148 self.title_style = style;
149 self
150 }
151
152 pub fn item(mut self, item: BannerItem<'a>) -> Self {
154 self.items.push(item);
155 self
156 }
157
158 pub fn label_style(mut self, style: Style) -> Self {
160 self.label_style = style;
161 self
162 }
163
164 pub fn background(mut self, style: Style) -> Self {
166 self.background = style;
167 self
168 }
169}
170
171impl Widget for DebugBanner<'_> {
172 fn render(self, area: Rect, buf: &mut Buffer) {
173 if area.height == 0 || area.width == 0 {
174 return;
175 }
176
177 Block::default().style(self.background).render(area, buf);
179
180 let mut spans = Vec::new();
181
182 if let Some(title) = self.title {
184 spans.push(Span::styled(format!(" {title} "), self.title_style));
185 spans.push(Span::raw(" "));
186 }
187
188 for item in &self.items {
190 spans.push(Span::styled(format!(" {} ", item.key), item.key_style));
191 spans.push(Span::styled(format!(" {} ", item.label), self.label_style));
192 }
193
194 let line = Paragraph::new(Line::from(spans)).style(self.background);
195 line.render(area, buf);
196 }
197}
198
199#[derive(Clone)]
201pub struct DebugTableStyle {
202 pub header: Style,
204 pub section: Style,
206 pub key: Style,
208 pub value: Style,
210 pub row_styles: (Style, Style),
212}
213
214impl Default for DebugTableStyle {
215 fn default() -> Self {
216 use super::config::DebugStyle;
217 Self {
218 header: Style::default()
219 .fg(DebugStyle::neon_cyan())
220 .add_modifier(Modifier::BOLD),
221 section: Style::default()
222 .fg(DebugStyle::neon_purple())
223 .add_modifier(Modifier::BOLD),
224 key: Style::default()
225 .fg(DebugStyle::neon_amber())
226 .add_modifier(Modifier::BOLD),
227 value: Style::default().fg(DebugStyle::text_primary()),
228 row_styles: (
229 Style::default().bg(DebugStyle::bg_panel()),
230 Style::default().bg(DebugStyle::bg_surface()),
231 ),
232 }
233 }
234}
235
236pub struct DebugTableWidget<'a> {
238 table: &'a DebugTableOverlay,
239 style: DebugTableStyle,
240}
241
242impl<'a> DebugTableWidget<'a> {
243 pub fn new(table: &'a DebugTableOverlay) -> Self {
245 Self {
246 table,
247 style: DebugTableStyle::default(),
248 }
249 }
250
251 pub fn style(mut self, style: DebugTableStyle) -> Self {
253 self.style = style;
254 self
255 }
256}
257
258impl Widget for DebugTableWidget<'_> {
259 fn render(self, area: Rect, buf: &mut Buffer) {
260 if area.height < 2 || area.width < 10 {
261 return;
262 }
263
264 let max_key_len = self
266 .table
267 .rows
268 .iter()
269 .filter_map(|row| match row {
270 DebugTableRow::Entry { key, .. } => Some(key.chars().count()),
271 DebugTableRow::Section(_) => None,
272 })
273 .max()
274 .unwrap_or(0) as u16;
275
276 let max_label = area.width.saturating_sub(8).max(10);
277 let label_width = max_key_len.saturating_add(2).clamp(12, 30).min(max_label);
278 let constraints = [Constraint::Length(label_width), Constraint::Min(0)];
279
280 let header = Row::new(vec![
282 Cell::from("Field").style(self.style.header),
283 Cell::from("Value").style(self.style.header),
284 ]);
285
286 let rows: Vec<Row> = self
288 .table
289 .rows
290 .iter()
291 .enumerate()
292 .map(|(idx, row)| match row {
293 DebugTableRow::Section(title) => Row::new(vec![
294 Cell::from(format!(" {title} ")).style(self.style.section),
295 Cell::from(""),
296 ]),
297 DebugTableRow::Entry { key, value } => {
298 let row_style = if idx % 2 == 0 {
299 self.style.row_styles.0
300 } else {
301 self.style.row_styles.1
302 };
303 Row::new(vec![
304 Cell::from(key.clone()).style(self.style.key),
305 Cell::from(value.clone()).style(self.style.value),
306 ])
307 .style(row_style)
308 }
309 })
310 .collect();
311
312 let table = Table::new(rows, constraints)
313 .header(header)
314 .column_spacing(2);
315
316 table.render(area, buf);
317 }
318}
319
320pub struct CellPreviewWidget<'a> {
322 preview: &'a CellPreview,
323 label_style: Style,
324 value_style: Style,
325}
326
327impl<'a> CellPreviewWidget<'a> {
328 pub fn new(preview: &'a CellPreview) -> Self {
330 use super::config::DebugStyle;
331 Self {
332 preview,
333 label_style: Style::default().fg(DebugStyle::text_secondary()),
334 value_style: Style::default().fg(DebugStyle::text_primary()),
335 }
336 }
337
338 pub fn label_style(mut self, style: Style) -> Self {
340 self.label_style = style;
341 self
342 }
343
344 pub fn value_style(mut self, style: Style) -> Self {
346 self.value_style = style;
347 self
348 }
349}
350
351impl Widget for CellPreviewWidget<'_> {
352 fn render(self, area: Rect, buf: &mut Buffer) {
353 use super::config::DebugStyle;
354
355 if area.width < 20 || area.height < 1 {
356 return;
357 }
358
359 let char_style = Style::default()
361 .fg(self.preview.fg)
362 .bg(self.preview.bg)
363 .add_modifier(self.preview.modifier);
364
365 let fg_str = format_color_compact(self.preview.fg);
367 let bg_str = format_color_compact(self.preview.bg);
368 let mod_str = format_modifier_compact(self.preview.modifier);
369
370 let char_bg = Style::default().bg(DebugStyle::bg_surface());
372 let mod_style = Style::default().fg(DebugStyle::neon_purple());
373
374 let mut spans = vec![
376 Span::styled(" ", char_bg),
377 Span::styled(self.preview.symbol.clone(), char_style),
378 Span::styled(" ", char_bg),
379 Span::styled(" fg ", self.label_style),
380 Span::styled("█", Style::default().fg(self.preview.fg)),
381 Span::styled(format!(" {fg_str}"), self.value_style),
382 Span::styled(" bg ", self.label_style),
383 Span::styled("█", Style::default().fg(self.preview.bg)),
384 Span::styled(format!(" {bg_str}"), self.value_style),
385 ];
386
387 if !mod_str.is_empty() {
388 spans.push(Span::styled(format!(" {mod_str}"), mod_style));
389 }
390
391 let line = Paragraph::new(Line::from(spans));
392 line.render(area, buf);
393 }
394}
395
396#[derive(Clone)]
402pub struct ActionLogStyle {
403 pub header: Style,
405 pub sequence: Style,
407 pub name: Style,
409 pub summary: Style,
411 pub elapsed: Style,
413 pub changed_yes: Style,
415 pub changed_no: Style,
417 pub selected: Style,
419 pub row_styles: (Style, Style),
421}
422
423impl Default for ActionLogStyle {
424 fn default() -> Self {
425 use super::config::DebugStyle;
426 Self {
427 header: Style::default()
428 .fg(DebugStyle::neon_cyan())
429 .add_modifier(Modifier::BOLD),
430 sequence: Style::default().fg(DebugStyle::text_secondary()),
431 name: Style::default()
432 .fg(DebugStyle::neon_amber())
433 .add_modifier(Modifier::BOLD),
434 summary: Style::default().fg(DebugStyle::text_primary()),
435 elapsed: Style::default().fg(DebugStyle::text_secondary()),
436 changed_yes: Style::default().fg(DebugStyle::neon_green()),
437 changed_no: Style::default().fg(DebugStyle::text_secondary()),
438 selected: Style::default()
439 .bg(DebugStyle::bg_highlight())
440 .add_modifier(Modifier::BOLD),
441 row_styles: (
442 Style::default().bg(DebugStyle::bg_panel()),
443 Style::default().bg(DebugStyle::bg_surface()),
444 ),
445 }
446 }
447}
448
449pub struct ActionLogWidget<'a> {
458 log: &'a ActionLogOverlay,
459 style: ActionLogStyle,
460 visible_rows: usize,
462}
463
464impl<'a> ActionLogWidget<'a> {
465 pub fn new(log: &'a ActionLogOverlay) -> Self {
467 Self {
468 log,
469 style: ActionLogStyle::default(),
470 visible_rows: 10, }
472 }
473
474 pub fn style(mut self, style: ActionLogStyle) -> Self {
476 self.style = style;
477 self
478 }
479
480 pub fn visible_rows(mut self, rows: usize) -> Self {
482 self.visible_rows = rows;
483 self
484 }
485}
486
487impl Widget for ActionLogWidget<'_> {
488 fn render(self, area: Rect, buf: &mut Buffer) {
489 if area.height < 2 || area.width < 30 {
490 return;
491 }
492
493 let visible_rows = (area.height.saturating_sub(1)) as usize;
495
496 let constraints = [
498 Constraint::Length(5), Constraint::Length(24), Constraint::Min(20), Constraint::Length(8), Constraint::Length(3), ];
504
505 let header = Row::new(vec![
507 Cell::from("#").style(self.style.header),
508 Cell::from("Action").style(self.style.header),
509 Cell::from("Summary").style(self.style.header),
510 Cell::from("Elapsed").style(self.style.header),
511 Cell::from("Chg").style(self.style.header),
512 ]);
513
514 let scroll_offset = if self.log.selected >= visible_rows {
516 self.log.selected - visible_rows + 1
517 } else {
518 0
519 };
520
521 let rows: Vec<Row> = self
523 .log
524 .entries
525 .iter()
526 .enumerate()
527 .skip(scroll_offset)
528 .take(visible_rows)
529 .map(|(idx, entry)| {
530 let is_selected = idx == self.log.selected;
531 let base_style = if is_selected {
532 self.style.selected
533 } else if idx % 2 == 0 {
534 self.style.row_styles.0
535 } else {
536 self.style.row_styles.1
537 };
538
539 let changed_cell = match entry.state_changed {
540 Some(true) => Cell::from("Y").style(self.style.changed_yes),
541 Some(false) => Cell::from("N").style(self.style.changed_no),
542 None => Cell::from("-").style(self.style.elapsed),
543 };
544
545 let summary = if entry.summary.chars().count() > 60 {
547 let truncated: String = entry.summary.chars().take(57).collect();
548 format!("{}...", truncated)
549 } else {
550 entry.summary.clone()
551 };
552
553 Row::new(vec![
554 Cell::from(format!("{}", entry.sequence)).style(self.style.sequence),
555 Cell::from(entry.name.clone()).style(self.style.name),
556 Cell::from(summary).style(self.style.summary),
557 Cell::from(entry.elapsed.clone()).style(self.style.elapsed),
558 changed_cell,
559 ])
560 .style(base_style)
561 })
562 .collect();
563
564 let table = Table::new(rows, constraints)
565 .header(header)
566 .column_spacing(1);
567
568 table.render(area, buf);
569 }
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575 use ratatui::layout::Rect;
576
577 #[test]
578 fn test_buffer_to_text() {
579 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
580
581 buffer[(0, 0)].set_char('H');
583 buffer[(1, 0)].set_char('i');
584 buffer[(0, 1)].set_char('!');
585
586 let text = buffer_to_text(&buffer);
587 let lines: Vec<&str> = text.lines().collect();
588
589 assert_eq!(lines[0], "Hi");
590 assert_eq!(lines[1], "!");
591 }
592
593 #[test]
594 fn test_debug_banner() {
595 let banner =
596 DebugBanner::new()
597 .title("TEST")
598 .item(BannerItem::new("F1", "help", Style::default()));
599
600 let mut buffer = Buffer::empty(Rect::new(0, 0, 40, 1));
601 banner.render(Rect::new(0, 0, 40, 1), &mut buffer);
602
603 let text = buffer_to_text(&buffer);
604 assert!(text.contains("TEST"));
605 assert!(text.contains("F1"));
606 assert!(text.contains("help"));
607 }
608}