ghostscope_ui/components/ebpf_panel/
renderer.rs1use crate::model::panel_state::{DisplayMode, EbpfPanelState, EbpfViewMode};
2use crate::ui::themes::UIThemes;
3use ratatui::{
4 layout::Rect,
5 style::{Color, Modifier, Style},
6 text::{Line, Span},
7 widgets::{Block, BorderType, Borders, Paragraph},
8 Frame,
9};
10
11#[derive(Debug)]
13pub struct EbpfPanelRenderer;
14
15impl EbpfPanelRenderer {
16 pub fn new() -> Self {
17 Self
18 }
19
20 pub fn render(
22 &mut self,
23 state: &mut EbpfPanelState,
24 frame: &mut Frame,
25 area: Rect,
26 is_focused: bool,
27 ) {
28 let border_style = if is_focused {
30 UIThemes::panel_focused()
31 } else {
32 UIThemes::panel_unfocused()
33 };
34 let panel_block = Block::default()
35 .borders(Borders::ALL)
36 .border_type(if is_focused {
37 BorderType::Thick
38 } else {
39 BorderType::Plain
40 })
41 .title(format!(
42 "eBPF Trace Output ({} events)",
43 state.trace_events.len()
44 ))
45 .border_style(border_style);
46 frame.render_widget(panel_block, area);
47
48 if area.width <= 2 || area.height <= 2 {
49 return;
50 }
51 let content_area = Rect {
52 x: area.x + 1,
53 y: area.y + 1,
54 width: area.width - 2,
55 height: area.height - 2,
56 };
57 let content_width = content_area.width as usize;
58
59 struct Card {
61 header_no_bold: String,
62 header_number: String,
63 header_rest: String,
64 body_lines: Vec<Line<'static>>,
65 total_height: u16,
66 is_error: bool,
67 is_latest: bool,
68 }
69 let mut cards: Vec<Card> = Vec::new();
70
71 let total_traces = state.trace_events.len();
72 for (trace_index, cached_trace) in state.trace_events.iter().enumerate() {
73 let trace = &cached_trace.event;
74 let is_latest = trace_index == total_traces - 1;
75 let is_error = trace.instructions.last().is_some_and(|inst| {
76 if let ghostscope_protocol::ParsedInstruction::EndInstruction {
77 execution_status,
78 ..
79 } = inst
80 {
81 *execution_status == 1 || *execution_status == 2
82 } else {
83 false
84 }
85 });
86
87 let header_no_bold = String::from("[No:");
88 let message_id = (trace_index + 1) as u64;
89 let formatted_timestamp = &cached_trace.formatted_timestamp;
90 let header_number = message_id.to_string();
91 let header_rest = format!(
92 "] {} TraceID:{} PID:{} TID:{}",
93 formatted_timestamp, trace.trace_id, trace.pid, trace.tid
94 );
95
96 let mut body_lines: Vec<Line> = Vec::new();
97 for output_line in trace.to_formatted_output() {
98 let color = if output_line.contains("ERROR") || output_line.contains("Error") {
99 Color::Red
100 } else if output_line.contains("WARN") || output_line.contains("Warning") {
101 Color::Yellow
102 } else {
103 Color::Cyan
104 };
105 let inner_width = content_width.saturating_sub(2);
107 let first_width = inner_width.saturating_sub(2); let cont_width = inner_width.saturating_sub(4); let wrapped_lines =
110 Self::wrap_text_with_widths(&output_line, first_width, cont_width);
111 for (i, seg) in wrapped_lines.into_iter().enumerate() {
112 let line_indent = if i == 0 { " " } else { " " };
113 body_lines.push(Line::from(vec![
114 Span::raw(line_indent),
115 Span::styled(
116 seg,
117 Style::default().fg(color).add_modifier(Modifier::empty()),
118 ),
119 ]));
120 }
121 }
122
123 let (body_for_display, inner_height): (Vec<Line>, u16) = match state.view_mode {
125 EbpfViewMode::List => {
126 let mut b = Vec::new();
127 if body_lines.is_empty() {
128 b.push(Line::from(""));
129 } else {
130 let max_body = 3usize;
131 let truncated = body_lines.len() > max_body;
132 let take_n = body_lines.len().min(max_body);
133 b.extend(body_lines.iter().take(take_n).cloned());
134 if truncated {
135 if let Some(last) = b.last_mut() {
136 let ellipsis = Span::styled(
138 " …",
139 Style::default()
140 .fg(Color::Yellow)
141 .add_modifier(Modifier::BOLD),
142 );
143 if last.spans.len() >= 2 {
144 let indent = last.spans[0].content.clone();
145 let style = last.spans[1].style;
146 let original = last.spans[1].content.to_string();
147 let trimmed = Self::trim_chars_from_end(&original, 2);
149 last.spans.clear();
150 last.spans.push(Span::raw(indent));
151 last.spans.push(Span::styled(trimmed, style));
152 last.spans.push(ellipsis);
153 } else {
154 last.spans.push(ellipsis);
155 }
156 }
157 }
158 }
159 (b.clone(), u16::max(1, b.len() as u16))
160 }
161 EbpfViewMode::Expanded { .. } => {
162 (body_lines.clone(), u16::max(1, body_lines.len() as u16))
163 }
164 };
165 let total_height = inner_height + 2;
166 cards.push(Card {
167 header_no_bold,
168 header_number,
169 header_rest,
170 body_lines: body_for_display,
171 total_height,
172 is_error,
173 is_latest,
174 });
175 }
176
177 if let EbpfViewMode::Expanded { index, scroll } = state.view_mode {
179 if let Some(card) = cards.get(index) {
180 let border_style_l = Style::default().fg(Color::Green);
181 let title_color = Color::Green;
182 let card_block = Block::default()
183 .borders(Borders::ALL)
184 .border_type(BorderType::Thick)
185 .border_style(border_style_l)
186 .title(Line::from(vec![
187 Span::styled(
188 card.header_no_bold.clone(),
189 Style::default().fg(title_color),
190 ),
191 Span::styled(
192 card.header_number.clone(),
193 Style::default()
194 .fg(Color::LightMagenta)
195 .add_modifier(Modifier::BOLD),
196 ),
197 Span::styled(card.header_rest.clone(), Style::default().fg(title_color)),
198 ]));
199 frame.render_widget(card_block, content_area);
200
201 if content_area.width > 2 && content_area.height > 2 {
202 let hint_h: u16 = 1;
204 let inner_h = content_area.height.saturating_sub(2 + hint_h);
205 let inner = Rect {
206 x: content_area.x + 1,
207 y: content_area.y + 1,
208 width: content_area.width - 2,
209 height: inner_h,
210 };
211 state.last_inner_height = inner.height as usize;
213 let max_body_lines = inner.height as usize;
214 let total = card.body_lines.len();
215 let max_scroll = total.saturating_sub(max_body_lines);
216 let start = scroll.min(max_scroll);
217 let end = (start + max_body_lines).min(total);
218 if start != scroll {
220 state.set_expanded_scroll(start);
221 }
222 let lines = card.body_lines[start..end].to_vec();
223 let para = Paragraph::new(lines);
224 frame.render_widget(para, inner);
225 let hint_rect = Rect {
227 x: content_area.x + 1,
228 y: content_area.y + content_area.height.saturating_sub(1),
229 width: content_area.width.saturating_sub(2),
230 height: 1,
231 };
232 let hint = "Esc/Ctrl+C to exit • j/k/↑/↓ scroll • Ctrl+U/D half-page • PgUp/PgDn page";
233 let hint_line =
234 Line::from(Span::styled(hint, Style::default().fg(Color::Gray)));
235 let hint_para = Paragraph::new(vec![hint_line]);
236 frame.render_widget(hint_para, hint_rect);
237 }
238 }
239 return;
240 }
241
242 let viewport_height = content_area.height;
244 let start_index = match state.display_mode {
245 DisplayMode::AutoRefresh => {
246 let mut accumulated: u16 = 0;
247 let mut idx = cards.len();
248 while idx > 0 {
249 let next_height = accumulated.saturating_add(cards[idx - 1].total_height);
250 if next_height > viewport_height {
251 break;
252 }
253 accumulated = next_height;
254 idx -= 1;
255 }
256 idx
257 }
258 DisplayMode::Scroll => {
259 let cursor = state.cursor_trace_index.min(cards.len().saturating_sub(1));
260 let mut height_below: u16 = 0;
261 let mut end = cursor;
262 while end < cards.len() {
263 let card_height = cards[end].total_height;
264 if height_below + card_height > viewport_height {
265 break;
266 }
267 height_below += card_height;
268 end += 1;
269 }
270
271 let mut height_above: u16 = 0;
272 let mut idx = cursor;
273 while idx > 0 {
274 let card_height = cards[idx - 1].total_height;
275 if height_above + height_below + card_height > viewport_height {
276 break;
277 }
278 height_above += card_height;
279 idx -= 1;
280 }
281 idx
282 }
283 };
284
285 let mut y = content_area.y;
287 for (idx, card) in cards.iter().enumerate().skip(start_index) {
288 if y >= content_area.y + content_area.height {
289 break;
290 }
291 let remaining = (content_area.y + content_area.height).saturating_sub(y);
293 let height = card.total_height.min(remaining);
294 if height < 2 {
295 break;
296 }
297
298 let is_cursor = state.show_cursor && idx == state.cursor_trace_index;
299 let mut border_style_l = Style::default();
300 let mut border_type = BorderType::Plain;
301 if is_cursor {
302 border_style_l = Style::default().fg(Color::Yellow);
303 border_type = BorderType::Thick;
304 } else if card.is_latest {
305 border_style_l = Style::default().fg(Color::Green);
306 border_type = BorderType::Thick;
307 } else if card.is_error {
308 border_style_l = Style::default().fg(Color::Red);
309 }
310 let title_color = if is_cursor {
311 Color::Yellow
312 } else if card.is_latest {
313 Color::Green
314 } else {
315 Color::Gray
316 };
317
318 let card_block = Block::default()
319 .borders(Borders::ALL)
320 .border_type(border_type)
321 .border_style(border_style_l)
322 .title(Line::from(vec![
323 Span::styled(
324 card.header_no_bold.clone(),
325 Style::default().fg(title_color),
326 ),
327 Span::styled(
328 card.header_number.clone(),
329 Style::default()
330 .fg(Color::LightMagenta)
331 .add_modifier(Modifier::BOLD),
332 ),
333 Span::styled(card.header_rest.clone(), Style::default().fg(title_color)),
334 ]));
335
336 let card_area = Rect {
337 x: content_area.x,
338 y,
339 width: content_area.width,
340 height,
341 };
342 frame.render_widget(card_block, card_area);
343
344 if card_area.width > 2 && card_area.height > 2 {
345 let inner = Rect {
346 x: card_area.x + 1,
347 y: card_area.y + 1,
348 width: card_area.width - 2,
349 height: card_area.height - 2,
350 };
351 let body = if card.body_lines.is_empty() {
352 vec![Line::from("")]
353 } else {
354 card.body_lines.clone()
355 };
356 let para = Paragraph::new(body);
357 frame.render_widget(para, inner);
358 }
359
360 y = y.saturating_add(height);
361 }
362
363 if state.g_pressed || state.numeric_prefix.is_some() {
365 let input_text = if let Some(ref s) = state.numeric_prefix {
366 s.clone()
367 } else {
368 "g".to_string()
369 };
370 let hint_text = if state.g_pressed && state.numeric_prefix.is_none() {
371 " Press 'g' again for top"
372 } else if state.numeric_prefix.is_some() {
373 " Press 'G' to jump to message"
374 } else {
375 ""
376 };
377 let full_text = if hint_text.is_empty() {
378 input_text.clone()
379 } else {
380 let hint_body = &hint_text[1..];
381 format!("{input_text} ({hint_body})")
382 };
383
384 let text_width = full_text.len() as u16;
385 let display_x = content_area.x + content_area.width.saturating_sub(text_width + 2);
386 let display_y = content_area.y + content_area.height.saturating_sub(1);
387
388 let mut spans = vec![Span::styled(
389 input_text,
390 Style::default().fg(Color::Green).bg(Color::Rgb(30, 30, 30)),
391 )];
392 if !hint_text.is_empty() {
393 let hint_body = &hint_text[1..];
394 spans.push(Span::styled(
395 format!(" ({hint_body})"),
396 Style::default()
397 .fg(border_style.fg.unwrap_or(Color::White))
398 .bg(Color::Rgb(30, 30, 30)),
399 ));
400 }
401 let text = ratatui::text::Text::from(ratatui::text::Line::from(spans));
402 frame.render_widget(
403 ratatui::widgets::Paragraph::new(text).alignment(ratatui::layout::Alignment::Right),
404 Rect::new(display_x, display_y, text_width + 2, 1),
405 );
406 }
407 }
408
409 fn wrap_text_with_widths(text: &str, first_width: usize, cont_width: usize) -> Vec<String> {
411 if text.is_empty() {
412 return vec![String::new()];
413 }
414
415 let fw = first_width.max(1);
416 let cw = cont_width.max(1);
417 let mut width = fw;
418 let mut lines = Vec::new();
419 let mut current_line = String::new();
420
421 for ch in text.chars() {
422 if ch == '\n' {
423 lines.push(current_line);
424 current_line = String::new();
425 width = cw; continue;
427 }
428 if current_line.len() >= width {
429 lines.push(std::mem::take(&mut current_line));
430 width = cw; }
432 current_line.push(ch);
433 }
434
435 lines.push(current_line);
436 lines
437 }
438
439 fn trim_chars_from_end(s: &str, n: usize) -> String {
441 if n == 0 || s.is_empty() {
442 return s.to_string();
443 }
444 let mut end = s.len();
445 let mut iter = s.char_indices().rev();
446 for _ in 0..n {
447 if let Some((idx, _)) = iter.next() {
448 end = idx;
449 } else {
450 end = 0;
451 break;
452 }
453 }
454 s[..end].to_string()
455 }
456}
457
458impl Default for EbpfPanelRenderer {
459 fn default() -> Self {
460 Self::new()
461 }
462}