ratatui_zonekit/
render.rs1use std::sync::Arc;
8
9use ratatui::buffer::Buffer;
10use ratatui::layout::Rect;
11use ratatui::style::Style;
12use ratatui::widgets::{Paragraph, Widget};
13
14use crate::plugin::{RenderContext, ZonePlugin};
15use crate::zone::ZoneId;
16
17pub struct SafeRenderer;
23
24impl SafeRenderer {
25 pub fn render(
30 plugin: &Arc<dyn ZonePlugin>,
31 zone_id: ZoneId,
32 ctx: &RenderContext,
33 area: Rect,
34 buf: &mut Buffer,
35 ) -> bool {
36 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
37 plugin.render(zone_id, ctx, area, buf)
38 }));
39
40 if let Ok(rendered) = result {
41 rendered
42 } else {
43 for y in area.top()..area.bottom() {
45 for x in area.left()..area.right() {
46 buf[(x, y)].reset();
47 }
48 }
49 let crash_msg = format!("[{} crashed]", plugin.id());
50 let style = Style::default().fg(ratatui::style::Color::Red);
51 Paragraph::new(crash_msg).style(style).render(area, buf);
52 false
53 }
54 }
55}
56
57#[cfg(test)]
58#[allow(clippy::unnecessary_literal_bound)]
59mod tests {
60 use super::*;
61
62 struct GoodPlugin;
63
64 impl ZonePlugin for GoodPlugin {
65 fn id(&self) -> &str {
66 "good"
67 }
68
69 fn render(&self, _: ZoneId, _: &RenderContext, area: Rect, buf: &mut Buffer) -> bool {
70 Paragraph::new("ok").render(area, buf);
71 true
72 }
73 }
74
75 struct CrashPlugin;
76
77 impl ZonePlugin for CrashPlugin {
78 fn id(&self) -> &str {
79 "crash"
80 }
81
82 fn render(&self, _: ZoneId, _: &RenderContext, _: Rect, _: &mut Buffer) -> bool {
83 panic!("plugin bug");
84 }
85 }
86
87 struct EmptyPlugin;
88
89 impl ZonePlugin for EmptyPlugin {
90 fn id(&self) -> &str {
91 "empty"
92 }
93
94 fn render(&self, _: ZoneId, _: &RenderContext, _: Rect, _: &mut Buffer) -> bool {
95 false
96 }
97 }
98
99 #[test]
100 fn good_plugin_renders() {
101 let plugin: Arc<dyn ZonePlugin> = Arc::new(GoodPlugin);
102 let area = Rect::new(0, 0, 20, 1);
103 let mut buf = Buffer::empty(area);
104 let ctx = RenderContext::new(Style::default(), 80, 24);
105 assert!(SafeRenderer::render(
106 &plugin,
107 ZoneId::new(1),
108 &ctx,
109 area,
110 &mut buf
111 ));
112 let content: String = buf
113 .content()
114 .iter()
115 .map(|c| c.symbol().to_string())
116 .collect();
117 assert!(content.contains("ok"));
118 }
119
120 #[test]
121 fn crash_plugin_is_caught() {
122 let plugin: Arc<dyn ZonePlugin> = Arc::new(CrashPlugin);
123 let area = Rect::new(0, 0, 30, 1);
124 let mut buf = Buffer::empty(area);
125 let ctx = RenderContext::new(Style::default(), 80, 24);
126 let rendered = SafeRenderer::render(&plugin, ZoneId::new(1), &ctx, area, &mut buf);
128 assert!(!rendered);
129 let content: String = buf
130 .content()
131 .iter()
132 .map(|c| c.symbol().to_string())
133 .collect();
134 assert!(content.contains("[crash crashed]"));
135 }
136
137 #[test]
138 fn empty_plugin_returns_false() {
139 let plugin: Arc<dyn ZonePlugin> = Arc::new(EmptyPlugin);
140 let area = Rect::new(0, 0, 20, 1);
141 let mut buf = Buffer::empty(area);
142 let ctx = RenderContext::new(Style::default(), 80, 24);
143 assert!(!SafeRenderer::render(
144 &plugin,
145 ZoneId::new(1),
146 &ctx,
147 area,
148 &mut buf
149 ));
150 }
151}