ghostscope_ui/components/loading/
ui.rs

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/// Enhanced Loading UI component with detailed progress tracking
13#[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    /// Update spinner animation based on elapsed time
32    pub fn update(&mut self) {
33        let elapsed = self.start_time.elapsed();
34        // Update spinner every 100ms
35        let frames = (elapsed.as_millis() / 100) as usize;
36        self.current_spinner_idx = frames % self.spinner_chars.len();
37    }
38
39    /// Get current spinner character
40    fn current_spinner(&self) -> char {
41        self.spinner_chars[self.current_spinner_idx]
42    }
43
44    /// Get formatted elapsed time
45    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    /// Render enhanced loading screen with DWARF loading progress
58    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        // Clear the entire screen
67        f.render_widget(Clear, f.area());
68
69        // Create main layout - more space for content
70        let main_chunks = Layout::default()
71            .direction(Direction::Vertical)
72            .constraints([
73                Constraint::Fill(1),
74                Constraint::Length(19), // Height for loading box - increased for wrap support
75                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), // Width for loading box - increased
84                Constraint::Fill(1),
85            ])
86            .split(main_chunks[1]);
87
88        let loading_area = horizontal_chunks[1];
89
90        // Main loading container with enhanced styling
91        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        // Inner content area
101        let inner_area = loading_area.inner(ratatui::layout::Margin {
102            vertical: 1,
103            horizontal: 2,
104        });
105
106        // Create content layout
107        let content_chunks = Layout::default()
108            .direction(Direction::Vertical)
109            .constraints([
110                Constraint::Length(2), // Header line (2 lines for potential wrap)
111                Constraint::Length(1), // Copyright line
112                Constraint::Length(1), // License line
113                Constraint::Length(1), // Empty line
114                Constraint::Length(1), // Loading status line
115                Constraint::Length(1), // Empty line
116                Constraint::Length(1), // Progress bar
117                Constraint::Length(1), // Empty line
118                Constraint::Length(4), // Recently loaded modules (4 lines)
119                Constraint::Length(1), // Current loading status
120                Constraint::Length(1), // Stats line
121            ])
122            .split(inner_area);
123
124        // Header - with wrap support for narrow terminals
125        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        // Copyright
143        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        // License
151        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        // Loading status with PID
159        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        // Progress bar - only show if we have modules
178        if !loading_ui.progress.modules.is_empty() {
179            ProgressRenderer::render_progress_bar(f, content_chunks[6], &loading_ui.progress);
180        }
181
182        // Recently loaded modules
183        ProgressRenderer::render_recent_modules(f, content_chunks[8], &loading_ui.progress, 4);
184
185        // Current loading status
186        ProgressRenderer::render_current_status(f, content_chunks[9], &loading_ui.progress);
187
188        // Stats line
189        ProgressRenderer::render_stats(f, content_chunks[10], &loading_ui.progress);
190    }
191
192    /// Generate styled welcome message for command panel
193    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        // Module loading stats in white
223        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        // DWARF statistics in yellow
240        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        // Empty line
249        lines.push(Line::from(""));
250
251        // Bug reporting info in gray
252        lines.push(Line::from(Span::styled(
253            "For bug reporting instructions, please see:",
254            Style::default().fg(Color::Gray),
255        )));
256
257        // GitHub URL in white
258        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    /// Generate completion summary for command panel (backward compatibility)
267    pub fn generate_completion_summary(&self, total_time: f64) -> Vec<String> {
268        // Convert styled lines back to strings for backward compatibility
269        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    /// Render the simple loading screen (fallback for non-DWARF loading)
281    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        // Clear the entire screen
290        f.render_widget(Clear, f.area());
291
292        // Create centered layout
293        let vertical_chunks = Layout::default()
294            .direction(Direction::Vertical)
295            .constraints([
296                Constraint::Fill(1),
297                Constraint::Length(8), // Height for loading box
298                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), // Width for loading box
307                Constraint::Fill(1),
308            ])
309            .split(vertical_chunks[1]);
310
311        let loading_area = horizontal_chunks[1];
312
313        // Main loading container
314        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        // Inner content area
324        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), // Spinner line
333                Constraint::Length(1), // Message line
334                Constraint::Length(1), // Empty line
335                Constraint::Length(1), // Progress bar (if present)
336                Constraint::Length(1), // Time line
337            ])
338            .split(inner_area);
339
340        // Spinner and status line
341        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        // Message line
360        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        // Progress bar (if progress is provided)
368        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        // Elapsed time
383        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    /// Render a smaller loading indicator in a specific area
392    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}