1use ratatui::{
2 layout::{Alignment, Constraint, Direction, Layout, Rect},
3 style::{Color, Modifier, Style},
4 text::{Line, Span},
5 widgets::{Block, BorderType, Borders, Clear, Paragraph},
6 Frame,
7};
8use std::time::Instant;
9
10use super::{LoadingProgress, LoadingState, ProgressRenderer};
11
12#[derive(Clone, Debug)]
14pub struct LoadingUI {
15 start_time: Instant,
16 spinner_chars: Vec<char>,
17 current_spinner_idx: usize,
18 pub progress: LoadingProgress,
19}
20
21impl LoadingUI {
22 pub fn new() -> Self {
23 Self {
24 start_time: Instant::now(),
25 spinner_chars: vec!['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
26 current_spinner_idx: 0,
27 progress: LoadingProgress::new(),
28 }
29 }
30
31 pub fn update(&mut self) {
33 let elapsed = self.start_time.elapsed();
34 let frames = (elapsed.as_millis() / 100) as usize;
36 self.current_spinner_idx = frames % self.spinner_chars.len();
37 }
38
39 fn current_spinner(&self) -> char {
41 self.spinner_chars[self.current_spinner_idx]
42 }
43
44 fn elapsed_time(&self) -> String {
46 let elapsed = self.start_time.elapsed();
47 let total_seconds = elapsed.as_secs_f64();
48 if total_seconds < 60.0 {
49 format!("{total_seconds:.1}s")
50 } else {
51 let minutes = (total_seconds / 60.0).floor() as u64;
52 let remaining_seconds = total_seconds - (minutes as f64) * 60.0;
53 format!("{minutes}m{remaining_seconds:.1}s")
54 }
55 }
56
57 pub fn render_dwarf_loading(
59 f: &mut Frame,
60 loading_ui: &mut LoadingUI,
61 loading_state: &LoadingState,
62 pid: Option<u32>,
63 ) {
64 loading_ui.update();
65
66 f.render_widget(Clear, f.area());
68
69 let main_chunks = Layout::default()
71 .direction(Direction::Vertical)
72 .constraints([
73 Constraint::Fill(1),
74 Constraint::Length(19), Constraint::Fill(1),
76 ])
77 .split(f.area());
78
79 let horizontal_chunks = Layout::default()
80 .direction(Direction::Horizontal)
81 .constraints([
82 Constraint::Fill(1),
83 Constraint::Length(78), Constraint::Fill(1),
85 ])
86 .split(main_chunks[1]);
87
88 let loading_area = horizontal_chunks[1];
89
90 let loading_block = Block::default()
92 .title(" Ghostscope Tracer ")
93 .title_alignment(Alignment::Center)
94 .borders(Borders::ALL)
95 .border_type(BorderType::Rounded)
96 .border_style(Style::default().fg(Color::Cyan));
97
98 f.render_widget(loading_block, loading_area);
99
100 let inner_area = loading_area.inner(ratatui::layout::Margin {
102 vertical: 1,
103 horizontal: 2,
104 });
105
106 let content_chunks = Layout::default()
108 .direction(Direction::Vertical)
109 .constraints([
110 Constraint::Length(2), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(4), Constraint::Length(1), Constraint::Length(1), ])
122 .split(inner_area);
123
124 use ratatui::text::Text;
126 use ratatui::widgets::Wrap;
127
128 let header_text = Text::from(vec![Line::from(vec![
129 Span::styled("🔍 ", Style::default().fg(Color::Yellow)),
130 Span::styled(
131 format!("Ghostscope v{} - A DWARF-aware eBPF tracer with cgdb-like TUI - explore live processes at runtime", env!("CARGO_PKG_VERSION")),
132 Style::default()
133 .fg(Color::White)
134 .add_modifier(Modifier::BOLD),
135 ),
136 ])]);
137 let header_paragraph = Paragraph::new(header_text)
138 .alignment(Alignment::Center)
139 .wrap(Wrap { trim: true });
140 f.render_widget(header_paragraph, content_chunks[0]);
141
142 let copyright_line = Line::from(Span::styled(
144 "Copyright (C) 2025 Ghostscope Project",
145 Style::default().fg(Color::Gray),
146 ));
147 let copyright_paragraph = Paragraph::new(copyright_line).alignment(Alignment::Center);
148 f.render_widget(copyright_paragraph, content_chunks[1]);
149
150 let license_line = Line::from(Span::styled(
152 "Licensed under GPL License",
153 Style::default().fg(Color::Gray),
154 ));
155 let license_paragraph = Paragraph::new(license_line).alignment(Alignment::Center);
156 f.render_widget(license_paragraph, content_chunks[2]);
157
158 let status_message = if let Some(pid) = pid {
160 format!("Loading debug information for PID {pid}...")
161 } else {
162 loading_state.message().to_string()
163 };
164
165 let status_line = Line::from(vec![
166 Span::styled(
167 format!("{} ", loading_ui.current_spinner()),
168 Style::default()
169 .fg(Color::Yellow)
170 .add_modifier(Modifier::BOLD),
171 ),
172 Span::styled(status_message, Style::default().fg(Color::White)),
173 ]);
174 let status_paragraph = Paragraph::new(status_line).alignment(Alignment::Center);
175 f.render_widget(status_paragraph, content_chunks[4]);
176
177 if !loading_ui.progress.modules.is_empty() {
179 ProgressRenderer::render_progress_bar(f, content_chunks[6], &loading_ui.progress);
180 }
181
182 ProgressRenderer::render_recent_modules(f, content_chunks[8], &loading_ui.progress, 4);
184
185 ProgressRenderer::render_current_status(f, content_chunks[9], &loading_ui.progress);
187
188 ProgressRenderer::render_stats(f, content_chunks[10], &loading_ui.progress);
190 }
191
192 pub fn create_welcome_message(&self, total_time: f64) -> Vec<ratatui::text::Line<'static>> {
194 use ratatui::style::{Color, Modifier, Style};
195 use ratatui::text::{Line, Span};
196
197 let total_stats = self.progress.total_stats();
198 let total_modules = self.progress.total_modules();
199 let failed_count = self.progress.failed_count;
200 let successful_modules = total_modules - failed_count;
201
202 let mut lines = vec![
203 Line::from(Span::styled(
204 format!("🔍 Ghostscope v{}", env!("CARGO_PKG_VERSION")),
205 Style::default()
206 .fg(Color::Cyan)
207 .add_modifier(Modifier::BOLD),
208 )),
209 Line::from(Span::styled(
210 "Licensed under GPL",
211 Style::default().fg(Color::Gray),
212 )),
213 Line::from(""),
214 Line::from(Span::styled(
215 "✅ Debug Information Loaded:",
216 Style::default()
217 .fg(Color::Green)
218 .add_modifier(Modifier::BOLD),
219 )),
220 ];
221
222 if failed_count > 0 {
224 lines.push(Line::from(Span::styled(
225 format!(
226 "• {successful_modules} modules loaded successfully ({failed_count} failed) in {total_time:.1} seconds"
227 ),
228 Style::default().fg(Color::White),
229 )));
230 } else {
231 lines.push(Line::from(Span::styled(
232 format!(
233 "• {successful_modules} modules loaded successfully in {total_time:.1} seconds"
234 ),
235 Style::default().fg(Color::White),
236 )));
237 }
238
239 let functions = total_stats.functions;
241 let variables = total_stats.variables;
242 let types = total_stats.types;
243 lines.push(Line::from(Span::styled(
244 format!("• {functions} functions, {variables} variables, {types} types indexed"),
245 Style::default().fg(Color::Yellow),
246 )));
247
248 lines.push(Line::from(""));
250
251 lines.push(Line::from(Span::styled(
253 "For bug reporting instructions, please see:",
254 Style::default().fg(Color::Gray),
255 )));
256
257 lines.push(Line::from(Span::styled(
259 "https://github.com/swananan/ghostscope/issues",
260 Style::default().fg(Color::White),
261 )));
262
263 lines
264 }
265
266 pub fn generate_completion_summary(&self, total_time: f64) -> Vec<String> {
268 self.create_welcome_message(total_time)
270 .into_iter()
271 .map(|line| {
272 line.spans
273 .into_iter()
274 .map(|span| span.content.to_string())
275 .collect::<String>()
276 })
277 .collect()
278 }
279
280 pub fn render_simple(
282 f: &mut Frame,
283 loading_ui: &mut LoadingUI,
284 message: &str,
285 progress: Option<f64>,
286 ) {
287 loading_ui.update();
288
289 f.render_widget(Clear, f.area());
291
292 let vertical_chunks = Layout::default()
294 .direction(Direction::Vertical)
295 .constraints([
296 Constraint::Fill(1),
297 Constraint::Length(8), Constraint::Fill(1),
299 ])
300 .split(f.area());
301
302 let horizontal_chunks = Layout::default()
303 .direction(Direction::Horizontal)
304 .constraints([
305 Constraint::Fill(1),
306 Constraint::Length(60), Constraint::Fill(1),
308 ])
309 .split(vertical_chunks[1]);
310
311 let loading_area = horizontal_chunks[1];
312
313 let loading_block = Block::default()
315 .title(" Ghostscope ")
316 .title_alignment(Alignment::Center)
317 .borders(Borders::ALL)
318 .border_type(BorderType::Rounded)
319 .border_style(Style::default().fg(Color::Cyan));
320
321 f.render_widget(loading_block, loading_area);
322
323 let inner_area = loading_area.inner(ratatui::layout::Margin {
325 vertical: 1,
326 horizontal: 2,
327 });
328
329 let content_chunks = Layout::default()
330 .direction(Direction::Vertical)
331 .constraints([
332 Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), ])
338 .split(inner_area);
339
340 let spinner_line = Line::from(vec![
342 Span::styled(
343 format!("{} ", loading_ui.current_spinner()),
344 Style::default()
345 .fg(Color::Yellow)
346 .add_modifier(Modifier::BOLD),
347 ),
348 Span::styled(
349 "Loading Ghostscope...",
350 Style::default()
351 .fg(Color::White)
352 .add_modifier(Modifier::BOLD),
353 ),
354 ]);
355
356 let spinner_paragraph = Paragraph::new(spinner_line).alignment(Alignment::Center);
357 f.render_widget(spinner_paragraph, content_chunks[0]);
358
359 let message_paragraph = Paragraph::new(Line::from(Span::styled(
361 message,
362 Style::default().fg(Color::Gray),
363 )))
364 .alignment(Alignment::Center);
365 f.render_widget(message_paragraph, content_chunks[1]);
366
367 if let Some(progress_value) = progress {
369 use ratatui::widgets::{Gauge, Padding};
370 let progress_bar = Gauge::default()
371 .block(
372 Block::default()
373 .borders(Borders::NONE)
374 .padding(Padding::horizontal(1)),
375 )
376 .gauge_style(Style::default().fg(Color::Cyan))
377 .ratio(progress_value.clamp(0.0, 1.0))
378 .label(format!("{:.0}%", progress_value * 100.0));
379 f.render_widget(progress_bar, content_chunks[3]);
380 }
381
382 let time_line = Line::from(Span::styled(
384 format!("Elapsed: {}", loading_ui.elapsed_time()),
385 Style::default().fg(Color::DarkGray),
386 ));
387 let time_paragraph = Paragraph::new(time_line).alignment(Alignment::Center);
388 f.render_widget(time_paragraph, content_chunks[4]);
389 }
390
391 pub fn render_inline(f: &mut Frame, area: Rect, loading_ui: &mut LoadingUI, message: &str) {
393 loading_ui.update();
394
395 let spinner_text = format!("{} {}", loading_ui.current_spinner(), message);
396 let paragraph = Paragraph::new(Line::from(Span::styled(
397 spinner_text,
398 Style::default().fg(Color::Yellow),
399 )));
400
401 f.render_widget(paragraph, area);
402 }
403}
404
405impl Default for LoadingUI {
406 fn default() -> Self {
407 Self::new()
408 }
409}