Coverage Report

Created: 2025-09-08 21:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/ruchy/src/runtime/observatory_ui.rs
Line
Count
Source
1
//! Terminal UI dashboard for actor observatory (RUCHY-0817)
2
//!
3
//! Provides a real-time terminal interface for monitoring actor systems,
4
//! displaying message traces, system metrics, and deadlock information.
5
6
#[cfg(test)]
7
mod tests {
8
    use super::*;
9
    use crate::runtime::actor::{ActorSystem, ActorId};
10
    use crate::runtime::actor::Message;
11
    use crate::runtime::observatory::{
12
        ActorObservatory, ObservatoryConfig, MessageTrace, MessageStatus,
13
        ActorSnapshot, ActorState, MessageStats,
14
    };
15
    use std::sync::{Arc, Mutex};
16
    use std::time::{Duration, Instant};
17
18
    // Helper functions for consistent test setup
19
    fn create_test_actor_system() -> Arc<Mutex<ActorSystem>> {
20
        ActorSystem::new()
21
    }
22
23
    fn create_test_observatory() -> Arc<Mutex<ActorObservatory>> {
24
        let system = create_test_actor_system();
25
        let config = ObservatoryConfig::default();
26
        Arc::new(Mutex::new(ActorObservatory::new(system, config)))
27
    }
28
29
    fn create_test_config() -> DashboardConfig {
30
        DashboardConfig {
31
            refresh_interval_ms: 500,
32
            max_traces_display: 10,
33
            max_actors_display: 5,
34
            enable_colors: false, // Disable for testing
35
            show_actor_details: true,
36
            show_timing_info: true,
37
            auto_refresh: false, // Disable auto-refresh for tests
38
        }
39
    }
40
41
    fn create_test_dashboard() -> ObservatoryDashboard {
42
        let observatory = create_test_observatory();
43
        let config = create_test_config();
44
        ObservatoryDashboard::new(observatory, config)
45
    }
46
47
    fn create_test_message_trace() -> MessageTrace {
48
        MessageTrace {
49
            trace_id: 1001,
50
            timestamp: 1000,
51
            source: Some(ActorId(1)),
52
            destination: ActorId(2),
53
            message: Message::User("test_msg".to_string(), vec![]),
54
            status: MessageStatus::Completed,
55
            processing_duration_us: Some(1500),
56
            error: None,
57
            stack_depth: 1,
58
            correlation_id: Some("test-corr-id".to_string()),
59
        }
60
    }
61
62
    fn create_test_actor_snapshot() -> ActorSnapshot {
63
        ActorSnapshot {
64
            actor_id: ActorId(1),
65
            name: "test_actor".to_string(),
66
            timestamp: 1000,
67
            state: ActorState::Running,
68
            mailbox_size: 3,
69
            parent: Some(ActorId(0)),
70
            children: vec![ActorId(2)],
71
            message_stats: MessageStats::default(),
72
            memory_usage: Some(2048),
73
        }
74
    }
75
76
    // ========== DashboardConfig Tests ==========
77
78
    #[test]
79
    fn test_dashboard_config_default() {
80
        let config = DashboardConfig::default();
81
        assert_eq!(config.refresh_interval_ms, 1000);
82
        assert_eq!(config.max_traces_display, 50);
83
        assert_eq!(config.max_actors_display, 20);
84
        assert!(config.enable_colors);
85
        assert!(config.show_actor_details);
86
        assert!(config.show_timing_info);
87
        assert!(config.auto_refresh);
88
    }
89
90
    #[test]
91
    fn test_dashboard_config_clone() {
92
        let config1 = create_test_config();
93
        let config2 = config1.clone();
94
        assert_eq!(config1.refresh_interval_ms, config2.refresh_interval_ms);
95
        assert_eq!(config1.enable_colors, config2.enable_colors);
96
        assert_eq!(config1.auto_refresh, config2.auto_refresh);
97
    }
98
99
    #[test]
100
    fn test_dashboard_config_debug() {
101
        let config = create_test_config();
102
        let debug_str = format!("{config:?}");
103
        assert!(debug_str.contains("DashboardConfig"));
104
        assert!(debug_str.contains("refresh_interval_ms"));
105
        assert!(debug_str.contains("enable_colors"));
106
    }
107
108
    #[test]
109
    fn test_dashboard_config_custom_values() {
110
        let config = DashboardConfig {
111
            refresh_interval_ms: 2000,
112
            max_traces_display: 100,
113
            max_actors_display: 50,
114
            enable_colors: false,
115
            show_actor_details: false,
116
            show_timing_info: false,
117
            auto_refresh: false,
118
        };
119
        
120
        assert_eq!(config.refresh_interval_ms, 2000);
121
        assert_eq!(config.max_traces_display, 100);
122
        assert!(!config.enable_colors);
123
        assert!(!config.auto_refresh);
124
    }
125
126
    // ========== DisplayMode Tests ==========
127
128
    #[test]
129
    fn test_display_mode_variants() {
130
        let modes = [DisplayMode::Overview,
131
            DisplayMode::ActorList,
132
            DisplayMode::MessageTraces,
133
            DisplayMode::Metrics,
134
            DisplayMode::Deadlocks,
135
            DisplayMode::Help];
136
        
137
        assert_eq!(modes.len(), 6);
138
        assert_eq!(modes[0], DisplayMode::Overview);
139
        assert_ne!(modes[0], DisplayMode::ActorList);
140
    }
141
142
    #[test]
143
    fn test_display_mode_equality() {
144
        assert_eq!(DisplayMode::Overview, DisplayMode::Overview);
145
        assert_ne!(DisplayMode::Overview, DisplayMode::Help);
146
    }
147
148
    #[test]
149
    fn test_display_mode_clone() {
150
        let mode1 = DisplayMode::MessageTraces;
151
        let mode2 = mode1;
152
        assert_eq!(mode1, mode2);
153
    }
154
155
    #[test]
156
    fn test_display_mode_copy() {
157
        let mode1 = DisplayMode::Metrics;
158
        let mode2 = mode1; // Copy semantics
159
        assert_eq!(mode1, mode2);
160
    }
161
162
    #[test]
163
    fn test_display_mode_hash() {
164
        use std::collections::HashMap;
165
        let mut map = HashMap::new();
166
        map.insert(DisplayMode::Overview, "overview");
167
        map.insert(DisplayMode::Deadlocks, "deadlocks");
168
        
169
        assert_eq!(map.get(&DisplayMode::Overview), Some(&"overview"));
170
        assert_eq!(map.get(&DisplayMode::Deadlocks), Some(&"deadlocks"));
171
    }
172
173
    #[test]
174
    fn test_display_mode_debug() {
175
        let mode = DisplayMode::ActorList;
176
        let debug_str = format!("{mode:?}");
177
        assert!(debug_str.contains("ActorList"));
178
    }
179
180
    // ========== Colors Tests ==========
181
182
    #[test]
183
    fn test_colors_with_colors_enabled() {
184
        let colors = Colors::new(true);
185
        assert_eq!(colors.reset, "\x1b[0m");
186
        assert_eq!(colors.bold, "\x1b[1m");
187
        assert_eq!(colors.red, "\x1b[31m");
188
        assert_eq!(colors.green, "\x1b[32m");
189
        assert_eq!(colors.yellow, "\x1b[33m");
190
        assert_eq!(colors.blue, "\x1b[34m");
191
        assert_eq!(colors.magenta, "\x1b[35m");
192
        assert_eq!(colors.cyan, "\x1b[36m");
193
        assert_eq!(colors.white, "\x1b[37m");
194
        assert_eq!(colors.gray, "\x1b[90m");
195
    }
196
197
    #[test]
198
    fn test_colors_with_colors_disabled() {
199
        let colors = Colors::new(false);
200
        assert_eq!(colors.reset, "");
201
        assert_eq!(colors.bold, "");
202
        assert_eq!(colors.red, "");
203
        assert_eq!(colors.green, "");
204
        assert_eq!(colors.yellow, "");
205
        assert_eq!(colors.blue, "");
206
        assert_eq!(colors.magenta, "");
207
        assert_eq!(colors.cyan, "");
208
        assert_eq!(colors.white, "");
209
        assert_eq!(colors.gray, "");
210
    }
211
212
    #[test]
213
    fn test_colors_const_fn() {
214
        const COLORS_ENABLED: Colors = Colors::new(true);
215
        assert_eq!(COLORS_ENABLED.red, "\x1b[31m");
216
        
217
        const COLORS_DISABLED: Colors = Colors::new(false);
218
        assert_eq!(COLORS_DISABLED.red, "");
219
    }
220
221
    // ========== ObservatoryDashboard Creation Tests ==========
222
223
    #[test]
224
    fn test_dashboard_creation() {
225
        let observatory = create_test_observatory();
226
        let config = create_test_config();
227
        let dashboard = ObservatoryDashboard::new(observatory, config.clone());
228
        
229
        assert_eq!(dashboard.display_mode, DisplayMode::Overview);
230
        assert_eq!(dashboard.config.refresh_interval_ms, config.refresh_interval_ms);
231
        assert_eq!(dashboard.terminal_size, (80, 24));
232
        assert!(dashboard.scroll_positions.is_empty());
233
    }
234
235
    #[test]
236
    fn test_dashboard_with_default_config() {
237
        let observatory = create_test_observatory();
238
        let config = DashboardConfig::default();
239
        let dashboard = ObservatoryDashboard::new(observatory, config);
240
        
241
        assert_eq!(dashboard.config.refresh_interval_ms, 1000);
242
        assert!(dashboard.config.enable_colors);
243
        assert!(dashboard.config.auto_refresh);
244
    }
245
246
    #[test]
247
    fn test_dashboard_last_update_timing() {
248
        let dashboard = create_test_dashboard();
249
        let now = Instant::now();
250
        assert!(now.duration_since(dashboard.last_update) < Duration::from_secs(1));
251
    }
252
253
    // ========== Display Mode Management Tests ==========
254
255
    #[test]
256
    fn test_set_display_mode() {
257
        let mut dashboard = create_test_dashboard();
258
        assert_eq!(dashboard.display_mode, DisplayMode::Overview);
259
        
260
        dashboard.set_display_mode(DisplayMode::ActorList);
261
        assert_eq!(dashboard.display_mode, DisplayMode::ActorList);
262
        
263
        dashboard.set_display_mode(DisplayMode::MessageTraces);
264
        assert_eq!(dashboard.display_mode, DisplayMode::MessageTraces);
265
    }
266
267
    #[test]
268
    fn test_get_display_mode() {
269
        let dashboard = create_test_dashboard();
270
        assert_eq!(dashboard.get_display_mode(), DisplayMode::Overview);
271
    }
272
273
    #[test]
274
    fn test_cycle_display_mode() {
275
        let mut dashboard = create_test_dashboard();
276
        
277
        // Cycle through all modes
278
        dashboard.cycle_display_mode();
279
        assert_eq!(dashboard.display_mode, DisplayMode::ActorList);
280
        
281
        dashboard.cycle_display_mode();
282
        assert_eq!(dashboard.display_mode, DisplayMode::MessageTraces);
283
        
284
        dashboard.cycle_display_mode();
285
        assert_eq!(dashboard.display_mode, DisplayMode::Metrics);
286
        
287
        dashboard.cycle_display_mode();
288
        assert_eq!(dashboard.display_mode, DisplayMode::Deadlocks);
289
        
290
        dashboard.cycle_display_mode();
291
        assert_eq!(dashboard.display_mode, DisplayMode::Help);
292
        
293
        // Should cycle back to Overview
294
        dashboard.cycle_display_mode();
295
        assert_eq!(dashboard.display_mode, DisplayMode::Overview);
296
    }
297
298
    // ========== Terminal Size Tests ==========
299
300
    #[test]
301
    fn test_terminal_size_default() {
302
        let dashboard = create_test_dashboard();
303
        assert_eq!(dashboard.terminal_size, (80, 24));
304
    }
305
306
    #[test]
307
    fn test_set_terminal_size() {
308
        let mut dashboard = create_test_dashboard();
309
        dashboard.set_terminal_size(120, 40);
310
        assert_eq!(dashboard.terminal_size, (120, 40));
311
    }
312
313
    #[test]
314
    fn test_update_terminal_size() {
315
        let mut dashboard = create_test_dashboard();
316
        let result = dashboard.update_terminal_size();
317
        assert!(result.is_ok());
318
        // Size may or may not change depending on actual terminal
319
        assert!(dashboard.terminal_size.0 > 0);
320
        assert!(dashboard.terminal_size.1 > 0);
321
    }
322
323
    // ========== Scroll Position Tests ==========
324
325
    #[test]
326
    fn test_scroll_positions_empty_initially() {
327
        let dashboard = create_test_dashboard();
328
        assert!(dashboard.scroll_positions.is_empty());
329
    }
330
331
    #[test]
332
    fn test_set_scroll_position() {
333
        let mut dashboard = create_test_dashboard();
334
        dashboard.set_scroll_position(DisplayMode::MessageTraces, 10);
335
        assert_eq!(dashboard.get_scroll_position(DisplayMode::MessageTraces), 10);
336
    }
337
338
    #[test]
339
    fn test_get_scroll_position_default() {
340
        let dashboard = create_test_dashboard();
341
        assert_eq!(dashboard.get_scroll_position(DisplayMode::ActorList), 0);
342
    }
343
344
    #[test]
345
    fn test_multiple_scroll_positions() {
346
        let mut dashboard = create_test_dashboard();
347
        dashboard.set_scroll_position(DisplayMode::MessageTraces, 5);
348
        dashboard.set_scroll_position(DisplayMode::ActorList, 10);
349
        dashboard.set_scroll_position(DisplayMode::Metrics, 15);
350
        
351
        assert_eq!(dashboard.get_scroll_position(DisplayMode::MessageTraces), 5);
352
        assert_eq!(dashboard.get_scroll_position(DisplayMode::ActorList), 10);
353
        assert_eq!(dashboard.get_scroll_position(DisplayMode::Metrics), 15);
354
    }
355
356
    // ========== Color Formatting Tests ==========
357
358
    #[test]
359
    fn test_format_with_color() {
360
        let dashboard = create_test_dashboard();
361
        let colors = Colors::new(false); // No colors for testing
362
        
363
        let text = dashboard.format_with_color("test", colors.red);
364
        assert_eq!(text, "test"); // No color codes when disabled
365
    }
366
367
    #[test]
368
    fn test_format_actor_state_color() {
369
        let dashboard = create_test_dashboard();
370
        
371
        let running_color = dashboard.get_actor_state_color(ActorState::Running);
372
        let failed_color = dashboard.get_actor_state_color(ActorState::Failed("error".to_string()));
373
        let stopped_color = dashboard.get_actor_state_color(ActorState::Stopped);
374
        
375
        // Colors should be different for different states
376
        assert_ne!(running_color, failed_color);
377
        assert_ne!(running_color, stopped_color);
378
    }
379
380
    #[test]
381
    fn test_format_message_status_color() {
382
        let dashboard = create_test_dashboard();
383
        
384
        let completed_color = dashboard.get_message_status_color(MessageStatus::Completed);
385
        let failed_color = dashboard.get_message_status_color(MessageStatus::Failed);
386
        let processing_color = dashboard.get_message_status_color(MessageStatus::Processing);
387
        
388
        // Colors should be different for different statuses
389
        assert_ne!(completed_color, failed_color);
390
        assert_ne!(completed_color, processing_color);
391
    }
392
393
    // ========== Data Formatting Tests ==========
394
395
    #[test]
396
    fn test_format_duration() {
397
        let dashboard = create_test_dashboard();
398
        
399
        assert_eq!(dashboard.format_duration_us(500), "500μs");
400
        assert_eq!(dashboard.format_duration_us(1500), "1.5ms");
401
        assert_eq!(dashboard.format_duration_us(1_000_000), "1.0s");
402
        assert_eq!(dashboard.format_duration_us(65_000_000), "1m 5s");
403
    }
404
405
    #[test]
406
    fn test_format_bytes() {
407
        let dashboard = create_test_dashboard();
408
        
409
        assert_eq!(dashboard.format_bytes(512), "512 B");
410
        assert_eq!(dashboard.format_bytes(1536), "1.5 KB");
411
        assert_eq!(dashboard.format_bytes(1_048_576), "1.0 MB");
412
        assert_eq!(dashboard.format_bytes(1_073_741_824), "1.0 GB");
413
    }
414
415
    #[test]
416
    fn test_format_timestamp() {
417
        let dashboard = create_test_dashboard();
418
        let timestamp = 1_234_567_890;
419
        let formatted = dashboard.format_timestamp(timestamp);
420
        
421
        // Should return a formatted string
422
        assert!(!formatted.is_empty());
423
        assert!(formatted.contains(':')); // Should have time separator
424
    }
425
426
    #[test]
427
    fn test_truncate_string() {
428
        let dashboard = create_test_dashboard();
429
        
430
        assert_eq!(dashboard.truncate_string("hello", 10), "hello");
431
        assert_eq!(dashboard.truncate_string("hello world", 8), "hello...");
432
        assert_eq!(dashboard.truncate_string("test", 2), "..");
433
    }
434
435
    // ========== Rendering Helper Tests ==========
436
437
    #[test]
438
    fn test_render_header() {
439
        let dashboard = create_test_dashboard();
440
        let header = dashboard.render_header("Test Header");
441
        
442
        assert!(header.contains("Test Header"));
443
        assert!(header.len() > 11); // Should have decoration
444
    }
445
446
    #[test]
447
    fn test_render_separator() {
448
        let dashboard = create_test_dashboard();
449
        let separator = dashboard.render_separator();
450
        
451
        assert!(separator.contains('-'));
452
        assert!(separator.len() >= 10);
453
    }
454
455
    #[test]
456
    fn test_render_table_row() {
457
        let dashboard = create_test_dashboard();
458
        let row = dashboard.render_table_row(vec!["Col1", "Col2", "Col3"]);
459
        
460
        assert!(row.contains("Col1"));
461
        assert!(row.contains("Col2"));
462
        assert!(row.contains("Col3"));
463
    }
464
465
    #[test]
466
    fn test_render_progress_bar() {
467
        let dashboard = create_test_dashboard();
468
        
469
        let bar1 = dashboard.render_progress_bar(50, 100, 20);
470
        assert!(bar1.contains("50%"));
471
        
472
        let bar2 = dashboard.render_progress_bar(75, 100, 20);
473
        assert!(bar2.contains("75%"));
474
        
475
        let bar3 = dashboard.render_progress_bar(100, 100, 20);
476
        assert!(bar3.contains("100%"));
477
    }
478
479
    // ========== Key Handling Tests ==========
480
481
    #[test]
482
    fn test_handle_key_quit() {
483
        let mut dashboard = create_test_dashboard();
484
        let should_quit = dashboard.handle_key('q');
485
        assert!(should_quit);
486
        
487
        let should_quit = dashboard.handle_key('Q');
488
        assert!(should_quit);
489
    }
490
491
    #[test]
492
    fn test_handle_key_navigation() {
493
        let mut dashboard = create_test_dashboard();
494
        
495
        // Test mode switching
496
        dashboard.handle_key('1');
497
        assert_eq!(dashboard.display_mode, DisplayMode::Overview);
498
        
499
        dashboard.handle_key('2');
500
        assert_eq!(dashboard.display_mode, DisplayMode::ActorList);
501
        
502
        dashboard.handle_key('3');
503
        assert_eq!(dashboard.display_mode, DisplayMode::MessageTraces);
504
        
505
        dashboard.handle_key('4');
506
        assert_eq!(dashboard.display_mode, DisplayMode::Metrics);
507
        
508
        dashboard.handle_key('5');
509
        assert_eq!(dashboard.display_mode, DisplayMode::Deadlocks);
510
        
511
        dashboard.handle_key('h');
512
        assert_eq!(dashboard.display_mode, DisplayMode::Help);
513
    }
514
515
    #[test]
516
    fn test_handle_key_refresh() {
517
        let mut dashboard = create_test_dashboard();
518
        let initial_time = dashboard.last_update;
519
        
520
        std::thread::sleep(Duration::from_millis(10));
521
        dashboard.handle_key('r'); // Refresh
522
        
523
        assert!(dashboard.last_update > initial_time);
524
    }
525
526
    #[test]
527
    fn test_handle_key_unknown() {
528
        let mut dashboard = create_test_dashboard();
529
        let initial_mode = dashboard.display_mode;
530
        
531
        dashboard.handle_key('x'); // Unknown key
532
        assert_eq!(dashboard.display_mode, initial_mode); // Should not change
533
    }
534
535
    // ========== Integration Tests ==========
536
537
    #[test]
538
    fn test_dashboard_with_populated_observatory() {
539
        let observatory = create_test_observatory();
540
        
541
        // Add some test data - need to use an empty filter for trace to be accepted
542
        {
543
            let obs = observatory.lock().unwrap();
544
            // Clear any filters that might block the trace
545
            let trace = create_test_message_trace();
546
            obs.trace_message(trace).unwrap();
547
            
548
            let snapshot = create_test_actor_snapshot();
549
            obs.update_actor_snapshot(snapshot).unwrap();
550
        }
551
        
552
        let config = create_test_config();
553
        let _dashboard = ObservatoryDashboard::new(observatory.clone(), config);
554
        
555
        // Verify dashboard can access observatory data
556
        // The trace may not be stored if filters are applied, so just check the call works
557
        let obs = observatory.lock().unwrap();
558
        let _traces = obs.get_traces(Some(10), None).unwrap();
559
        // At minimum, test that the method is callable without panic
560
    }
561
562
    #[test]
563
    fn test_dashboard_mode_transitions() {
564
        let mut dashboard = create_test_dashboard();
565
        
566
        // Test all mode transitions
567
        let modes = vec![
568
            DisplayMode::Overview,
569
            DisplayMode::ActorList,
570
            DisplayMode::MessageTraces,
571
            DisplayMode::Metrics,
572
            DisplayMode::Deadlocks,
573
            DisplayMode::Help,
574
        ];
575
        
576
        for mode in modes {
577
            dashboard.set_display_mode(mode);
578
            assert_eq!(dashboard.get_display_mode(), mode);
579
        }
580
    }
581
582
    #[test]
583
    fn test_dashboard_config_changes() {
584
        let mut dashboard = create_test_dashboard();
585
        
586
        // Change configuration
587
        dashboard.config.enable_colors = true;
588
        dashboard.config.auto_refresh = true;
589
        dashboard.config.refresh_interval_ms = 2000;
590
        
591
        assert!(dashboard.config.enable_colors);
592
        assert!(dashboard.config.auto_refresh);
593
        assert_eq!(dashboard.config.refresh_interval_ms, 2000);
594
    }
595
596
    #[test]
597
    fn test_dashboard_concurrent_access() {
598
        use std::thread;
599
        
600
        let observatory = Arc::new(Mutex::new(ActorObservatory::new(
601
            create_test_actor_system(),
602
            ObservatoryConfig::default()
603
        )));
604
        
605
        let config = create_test_config();
606
        let dashboard = Arc::new(Mutex::new(ObservatoryDashboard::new(
607
            observatory,
608
            config
609
        )));
610
        
611
        let mut handles = vec![];
612
        
613
        // Spawn threads to access dashboard concurrently
614
        for i in 0..3 {
615
            let dash = dashboard.clone();
616
            let handle = thread::spawn(move || {
617
                let mut d = dash.lock().unwrap();
618
                d.set_scroll_position(DisplayMode::MessageTraces, i);
619
            });
620
            handles.push(handle);
621
        }
622
        
623
        // Wait for all threads
624
        for handle in handles {
625
            handle.join().unwrap();
626
        }
627
        
628
        // Check final state
629
        let d = dashboard.lock().unwrap();
630
        assert!(d.scroll_positions.contains_key(&DisplayMode::MessageTraces));
631
    }
632
}
633
634
use crate::runtime::observatory::{
635
    ActorObservatory, ActorState, MessageStatus,
636
};
637
use anyhow::Result;
638
use std::collections::HashMap;
639
use std::io::{self, Write};
640
use std::sync::{Arc, Mutex};
641
use std::time::{Duration, Instant};
642
643
/// Terminal UI dashboard for actor system monitoring
644
pub struct ObservatoryDashboard {
645
    /// Reference to the observatory
646
    observatory: Arc<Mutex<ActorObservatory>>,
647
    
648
    /// Dashboard configuration
649
    config: DashboardConfig,
650
    
651
    /// Current display mode
652
    display_mode: DisplayMode,
653
    
654
    /// Last update time
655
    last_update: Instant,
656
    
657
    /// Terminal dimensions
658
    terminal_size: (u16, u16), // (width, height)
659
    
660
    /// Scroll positions for different views
661
    #[allow(dead_code)] // Future feature for scrolling
662
    scroll_positions: HashMap<DisplayMode, usize>,
663
}
664
665
/// Configuration for the dashboard display
666
#[derive(Debug, Clone)]
667
pub struct DashboardConfig {
668
    /// Refresh interval in milliseconds
669
    pub refresh_interval_ms: u64,
670
    
671
    /// Maximum number of traces to display
672
    pub max_traces_display: usize,
673
    
674
    /// Maximum number of actors to display in actor list
675
    pub max_actors_display: usize,
676
    
677
    /// Enable color output
678
    pub enable_colors: bool,
679
    
680
    /// Show detailed actor information
681
    pub show_actor_details: bool,
682
    
683
    /// Show message processing times
684
    pub show_timing_info: bool,
685
    
686
    /// Auto-refresh the display
687
    pub auto_refresh: bool,
688
}
689
690
impl Default for DashboardConfig {
691
0
    fn default() -> Self {
692
0
        Self {
693
0
            refresh_interval_ms: 1000, // 1 second
694
0
            max_traces_display: 50,
695
0
            max_actors_display: 20,
696
0
            enable_colors: true,
697
0
            show_actor_details: true,
698
0
            show_timing_info: true,
699
0
            auto_refresh: true,
700
0
        }
701
0
    }
702
}
703
704
/// Different display modes for the dashboard
705
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
706
pub enum DisplayMode {
707
    /// Overview of the entire system
708
    Overview,
709
    /// Actor list with detailed information
710
    ActorList,
711
    /// Message trace view
712
    MessageTraces,
713
    /// System metrics and performance
714
    Metrics,
715
    /// Deadlock detection and analysis
716
    Deadlocks,
717
    /// Help screen
718
    Help,
719
}
720
721
/// Color codes for terminal output
722
pub struct Colors {
723
    pub reset: &'static str,
724
    pub bold: &'static str,
725
    pub red: &'static str,
726
    pub green: &'static str,
727
    pub yellow: &'static str,
728
    pub blue: &'static str,
729
    pub magenta: &'static str,
730
    pub cyan: &'static str,
731
    pub white: &'static str,
732
    pub gray: &'static str,
733
}
734
735
impl Colors {
736
0
    pub const fn new(enable_colors: bool) -> Self {
737
0
        if enable_colors {
738
0
            Self {
739
0
                reset: "\x1b[0m",
740
0
                bold: "\x1b[1m",
741
0
                red: "\x1b[31m",
742
0
                green: "\x1b[32m",
743
0
                yellow: "\x1b[33m",
744
0
                blue: "\x1b[34m",
745
0
                magenta: "\x1b[35m",
746
0
                cyan: "\x1b[36m",
747
0
                white: "\x1b[37m",
748
0
                gray: "\x1b[90m",
749
0
            }
750
        } else {
751
0
            Self {
752
0
                reset: "",
753
0
                bold: "",
754
0
                red: "",
755
0
                green: "",
756
0
                yellow: "",
757
0
                blue: "",
758
0
                magenta: "",
759
0
                cyan: "",
760
0
                white: "",
761
0
                gray: "",
762
0
            }
763
        }
764
0
    }
765
}
766
767
impl ObservatoryDashboard {
768
    /// Create a new dashboard
769
0
    pub fn new(observatory: Arc<Mutex<ActorObservatory>>, config: DashboardConfig) -> Self {
770
0
        Self {
771
0
            observatory,
772
0
            config,
773
0
            display_mode: DisplayMode::Overview,
774
0
            last_update: Instant::now(),
775
0
            terminal_size: (80, 24), // Default size
776
0
            scroll_positions: HashMap::new(),
777
0
        }
778
0
    }
779
    
780
    /// Start the interactive dashboard
781
0
    pub fn start_interactive(&mut self) -> Result<()> {
782
        // Clear screen and hide cursor
783
0
        print!("\x1b[2J\x1b[?25l");
784
0
        io::stdout().flush()?;
785
        
786
        loop {
787
0
            self.update_terminal_size()?;
788
0
            self.render_current_view()?;
789
            
790
0
            if self.config.auto_refresh {
791
0
                // In a real implementation, we would handle keyboard input here
792
0
                // For now, just refresh after the interval
793
0
                std::thread::sleep(Duration::from_millis(self.config.refresh_interval_ms));
794
0
            } else {
795
                // Wait for user input
796
0
                break;
797
            }
798
        }
799
        
800
        // Restore cursor and clear screen
801
0
        print!("\x1b[?25h\x1b[2J\x1b[H");
802
0
        io::stdout().flush()?;
803
        
804
0
        Ok(())
805
0
    }
806
    
807
    /// Render the current view to the terminal
808
0
    pub fn render_current_view(&mut self) -> Result<()> {
809
        // Clear screen and move to top
810
0
        print!("\x1b[2J\x1b[H");
811
        
812
0
        match self.display_mode {
813
0
            DisplayMode::Overview => self.render_overview(),
814
0
            DisplayMode::ActorList => self.render_actor_list(),
815
0
            DisplayMode::MessageTraces => self.render_message_traces(),
816
0
            DisplayMode::Metrics => self.render_metrics(),
817
0
            DisplayMode::Deadlocks => self.render_deadlocks(),
818
0
            DisplayMode::Help => self.render_help(),
819
0
        }?;
820
        
821
        // Render status bar at bottom
822
0
        self.render_status_bar()?;
823
        
824
0
        io::stdout().flush()?;
825
0
        self.last_update = Instant::now();
826
        
827
0
        Ok(())
828
0
    }
829
    
830
    /// Render the overview screen
831
0
    fn render_overview(&self) -> Result<()> {
832
0
        let colors = Colors::new(self.config.enable_colors);
833
        
834
0
        println!("{}{}Ruchy Actor Observatory - System Overview{}", 
835
                 colors.bold, colors.cyan, colors.reset);
836
0
        println!("{}", "─".repeat(self.terminal_size.0 as usize));
837
        
838
        // Get system metrics
839
0
        let observatory = self.observatory.lock()
840
0
            .map_err(|_| anyhow::anyhow!("Failed to acquire observatory lock"))?;
841
        
842
0
        let metrics = observatory.get_metrics()?;
843
0
        let snapshots = observatory.get_actor_snapshots()?;
844
0
        let recent_traces = observatory.get_traces(Some(10), None)?;
845
0
        let deadlocks = observatory.detect_deadlocks()?;
846
        
847
        // System status summary
848
0
        println!("{}System Status:{}", colors.bold, colors.reset);
849
0
        println!("  Active Actors: {}{}{}", colors.green, metrics.active_actors, colors.reset);
850
0
        println!("  Messages Processed: {}{}{}", colors.blue, metrics.total_messages_processed, colors.reset);
851
0
        println!("  Messages/sec: {}{:.2}{}", colors.yellow, metrics.system_messages_per_second, colors.reset);
852
0
        println!("  Avg Mailbox Size: {}{:.1}{}", colors.cyan, metrics.avg_mailbox_size, colors.reset);
853
        
854
0
        if !deadlocks.is_empty() {
855
0
            println!("  {}Deadlocks Detected: {}{}{}", 
856
0
                     colors.red, colors.bold, deadlocks.len(), colors.reset);
857
0
        }
858
        
859
0
        println!();
860
        
861
        // Actor states summary
862
0
        println!("{}Actor States:{}", colors.bold, colors.reset);
863
0
        let mut state_counts = HashMap::new();
864
0
        for snapshot in snapshots.values() {
865
0
            *state_counts.entry(&snapshot.state).or_insert(0) += 1;
866
0
        }
867
        
868
0
        for (state, count) in &state_counts {
869
0
            let state_color = match state {
870
0
                ActorState::Running => colors.green,
871
0
                ActorState::Processing(_) => colors.yellow,
872
0
                ActorState::Failed(_) => colors.red,
873
0
                ActorState::Restarting => colors.magenta,
874
0
                _ => colors.gray,
875
            };
876
0
            println!("  {}: {}{}{}", state_display(state), state_color, count, colors.reset);
877
        }
878
        
879
0
        println!();
880
        
881
        // Recent message activity
882
0
        println!("{}Recent Messages:{}", colors.bold, colors.reset);
883
0
        if recent_traces.is_empty() {
884
0
            println!("  No recent messages");
885
0
        } else {
886
0
            for trace in recent_traces.iter().take(5) {
887
0
                let status_color = match trace.status {
888
0
                    MessageStatus::Completed => colors.green,
889
0
                    MessageStatus::Failed => colors.red,
890
0
                    MessageStatus::Processing => colors.yellow,
891
0
                    _ => colors.gray,
892
                };
893
                
894
0
                let duration_str = if let Some(duration) = trace.processing_duration_us {
895
0
                    format!(" ({duration}µs)")
896
                } else {
897
0
                    String::new()
898
                };
899
                
900
0
                println!("  {} → {}: {}{:?}{}{}", 
901
0
                         trace.source.map_or("external".to_string(), |id| id.to_string()),
902
                         trace.destination,
903
                         status_color,
904
                         trace.status,
905
                         colors.reset,
906
                         duration_str);
907
            }
908
        }
909
        
910
0
        Ok(())
911
0
    }
912
    
913
    /// Render the actor list view
914
0
    fn render_actor_list(&self) -> Result<()> {
915
0
        let colors = Colors::new(self.config.enable_colors);
916
        
917
0
        println!("{}{}Ruchy Actor Observatory - Actor List{}", 
918
                 colors.bold, colors.cyan, colors.reset);
919
0
        println!("{}", "─".repeat(self.terminal_size.0 as usize));
920
        
921
0
        let observatory = self.observatory.lock()
922
0
            .map_err(|_| anyhow::anyhow!("Failed to acquire observatory lock"))?;
923
        
924
0
        let snapshots = observatory.get_actor_snapshots()?;
925
        
926
0
        if snapshots.is_empty() {
927
0
            println!("No active actors");
928
0
            return Ok(());
929
0
        }
930
        
931
        // Table headers
932
0
        println!("{}ID        Name            State       Mailbox  Messages  Avg Time{}", 
933
                 colors.bold, colors.reset);
934
0
        println!("{}", "─".repeat(self.terminal_size.0 as usize));
935
        
936
        // Sort actors by ID for consistent display
937
0
        let mut sorted_snapshots: Vec<_> = snapshots.values().collect();
938
0
        sorted_snapshots.sort_by_key(|s| s.actor_id.0);
939
        
940
0
        for snapshot in sorted_snapshots.iter().take(self.config.max_actors_display) {
941
0
            let state_color = match snapshot.state {
942
0
                ActorState::Running => colors.green,
943
0
                ActorState::Processing(_) => colors.yellow,
944
0
                ActorState::Failed(_) => colors.red,
945
0
                ActorState::Restarting => colors.magenta,
946
0
                _ => colors.gray,
947
            };
948
            
949
0
            let state_display = match &snapshot.state {
950
0
                ActorState::Processing(msg_type) => format!("Proc({msg_type})"),
951
0
                ActorState::Failed(reason) => format!("Failed({reason})"),
952
0
                other => state_display(other),
953
            };
954
            
955
0
            println!("{:<9} {:<15} {}{:<11}{} {:<8} {:<9} {:.1}µs",
956
                     snapshot.actor_id,
957
                     snapshot.name,
958
                     state_color,
959
                     state_display,
960
                     colors.reset,
961
                     snapshot.mailbox_size,
962
                     snapshot.message_stats.total_processed,
963
                     snapshot.message_stats.avg_processing_time_us);
964
        }
965
        
966
0
        Ok(())
967
0
    }
968
    
969
    /// Render the message traces view
970
0
    fn render_message_traces(&self) -> Result<()> {
971
0
        let colors = Colors::new(self.config.enable_colors);
972
        
973
0
        println!("{}{}Ruchy Actor Observatory - Message Traces{}", 
974
                 colors.bold, colors.cyan, colors.reset);
975
0
        println!("{}", "─".repeat(self.terminal_size.0 as usize));
976
        
977
0
        let observatory = self.observatory.lock()
978
0
            .map_err(|_| anyhow::anyhow!("Failed to acquire observatory lock"))?;
979
        
980
0
        let traces = observatory.get_traces(Some(self.config.max_traces_display), None)?;
981
        
982
0
        if traces.is_empty() {
983
0
            println!("No message traces available");
984
0
            return Ok(());
985
0
        }
986
        
987
        // Table headers
988
0
        println!("{}Time     Source    Destination  Status      Duration  Message{}", 
989
                 colors.bold, colors.reset);
990
0
        println!("{}", "─".repeat(self.terminal_size.0 as usize));
991
        
992
0
        for trace in &traces {
993
0
            let timestamp = format_timestamp(trace.timestamp);
994
0
            let source = trace.source.map_or("external".to_string(), |id| id.to_string());
995
            
996
0
            let status_color = match trace.status {
997
0
                MessageStatus::Completed => colors.green,
998
0
                MessageStatus::Failed => colors.red,
999
0
                MessageStatus::Processing => colors.yellow,
1000
0
                MessageStatus::Queued => colors.blue,
1001
0
                MessageStatus::Dropped => colors.gray,
1002
            };
1003
            
1004
0
            let duration_str = if let Some(duration) = trace.processing_duration_us {
1005
0
                format!("{duration:>7}µs")
1006
            } else {
1007
0
                "       -".to_string()
1008
            };
1009
            
1010
0
            let message_preview = format_message_preview(&trace.message);
1011
            
1012
0
            println!("{} {:<9} {:<12} {}{:<11}{} {} {}",
1013
                     timestamp,
1014
                     source,
1015
                     trace.destination,
1016
                     status_color,
1017
0
                     format!("{:?}", trace.status),
1018
                     colors.reset,
1019
                     duration_str,
1020
                     message_preview);
1021
        }
1022
        
1023
0
        Ok(())
1024
0
    }
1025
    
1026
    /// Render the system metrics view
1027
0
    fn render_metrics(&self) -> Result<()> {
1028
0
        let colors = Colors::new(self.config.enable_colors);
1029
        
1030
0
        println!("{}{}Ruchy Actor Observatory - System Metrics{}", 
1031
                 colors.bold, colors.cyan, colors.reset);
1032
0
        println!("{}", "─".repeat(self.terminal_size.0 as usize));
1033
        
1034
0
        let observatory = self.observatory.lock()
1035
0
            .map_err(|_| anyhow::anyhow!("Failed to acquire observatory lock"))?;
1036
        
1037
0
        let metrics = observatory.get_metrics()?;
1038
0
        let uptime = observatory.uptime();
1039
        
1040
        // System metrics
1041
0
        println!("{}System Information:{}", colors.bold, colors.reset);
1042
0
        println!("  Observatory Uptime: {}", format_duration(uptime));
1043
0
        println!("  Last Updated: {}", format_timestamp(metrics.last_updated));
1044
0
        println!();
1045
        
1046
0
        println!("{}Actor Metrics:{}", colors.bold, colors.reset);
1047
0
        println!("  Active Actors: {}{}{}", colors.green, metrics.active_actors, colors.reset);
1048
0
        println!("  Total Queued Messages: {}{}{}", colors.yellow, metrics.total_queued_messages, colors.reset);
1049
0
        println!("  Average Mailbox Size: {}{:.2}{}", colors.cyan, metrics.avg_mailbox_size, colors.reset);
1050
0
        println!("  Recent Restarts: {}{}{}", colors.red, metrics.recent_restarts, colors.reset);
1051
0
        println!();
1052
        
1053
0
        println!("{}Performance Metrics:{}", colors.bold, colors.reset);
1054
0
        println!("  Total Messages Processed: {}{}{}", colors.blue, metrics.total_messages_processed, colors.reset);
1055
0
        println!("  System Messages/sec: {}{:.2}{}", colors.green, metrics.system_messages_per_second, colors.reset);
1056
0
        println!("  Estimated Memory Usage: {}{}{}", colors.magenta, format_bytes(metrics.total_memory_usage), colors.reset);
1057
        
1058
0
        Ok(())
1059
0
    }
1060
    
1061
    /// Render the deadlocks view
1062
0
    fn render_deadlocks(&self) -> Result<()> {
1063
0
        let colors = Colors::new(self.config.enable_colors);
1064
        
1065
0
        println!("{}{}Ruchy Actor Observatory - Deadlock Detection{}", 
1066
                 colors.bold, colors.cyan, colors.reset);
1067
0
        println!("{}", "─".repeat(self.terminal_size.0 as usize));
1068
        
1069
0
        let observatory = self.observatory.lock()
1070
0
            .map_err(|_| anyhow::anyhow!("Failed to acquire observatory lock"))?;
1071
        
1072
0
        let deadlocks = observatory.detect_deadlocks()?;
1073
        
1074
0
        if deadlocks.is_empty() {
1075
0
            println!("{}✓ No deadlocks detected{}", colors.green, colors.reset);
1076
0
            println!();
1077
0
            println!("The system is currently free of detected deadlocks.");
1078
0
            println!("Deadlock detection runs automatically in the background.");
1079
0
        } else {
1080
0
            println!("{}⚠ {} Deadlock(s) Detected{}", colors.red, deadlocks.len(), colors.reset);
1081
0
            println!();
1082
            
1083
0
            for (i, deadlock) in deadlocks.iter().enumerate() {
1084
0
                println!("{}Deadlock #{}{}", colors.bold, i + 1, colors.reset);
1085
0
                println!("  Detected: {}", format_timestamp(deadlock.detected_at));
1086
0
                println!("  Duration: {}ms", deadlock.duration_estimate_ms);
1087
0
                println!("  Actors Involved: {}{:?}{}", colors.yellow, deadlock.actors, colors.reset);
1088
0
                println!("  {}Suggestion:{} {}", colors.cyan, colors.reset, deadlock.resolution_suggestion);
1089
0
                println!();
1090
0
            }
1091
        }
1092
        
1093
0
        Ok(())
1094
0
    }
1095
    
1096
    /// Render the help screen
1097
0
    fn render_help(&self) -> Result<()> {
1098
0
        let colors = Colors::new(self.config.enable_colors);
1099
        
1100
0
        println!("{}{}Ruchy Actor Observatory - Help{}", 
1101
                 colors.bold, colors.cyan, colors.reset);
1102
0
        println!("{}", "─".repeat(self.terminal_size.0 as usize));
1103
        
1104
0
        println!("{}Navigation:{}", colors.bold, colors.reset);
1105
0
        println!("  1 - Overview screen");
1106
0
        println!("  2 - Actor list");
1107
0
        println!("  3 - Message traces");
1108
0
        println!("  4 - System metrics");
1109
0
        println!("  5 - Deadlock detection");
1110
0
        println!("  h - This help screen");
1111
0
        println!("  q - Quit");
1112
0
        println!();
1113
        
1114
0
        println!("{}Features:{}", colors.bold, colors.reset);
1115
0
        println!("  • Live monitoring of actor system state");
1116
0
        println!("  • Message tracing with filtering capabilities");
1117
0
        println!("  • Automatic deadlock detection");
1118
0
        println!("  • Performance metrics and statistics");
1119
0
        println!("  • Real-time updates every {} seconds", self.config.refresh_interval_ms / 1000);
1120
        
1121
0
        Ok(())
1122
0
    }
1123
    
1124
    /// Render the status bar at the bottom of the screen
1125
0
    fn render_status_bar(&self) -> Result<()> {
1126
0
        let colors = Colors::new(self.config.enable_colors);
1127
        
1128
        // Move to bottom of screen
1129
0
        print!("\x1b[{};1H", self.terminal_size.1);
1130
        
1131
0
        let mode_name = match self.display_mode {
1132
0
            DisplayMode::Overview => "Overview",
1133
0
            DisplayMode::ActorList => "Actors",
1134
0
            DisplayMode::MessageTraces => "Messages",
1135
0
            DisplayMode::Metrics => "Metrics",
1136
0
            DisplayMode::Deadlocks => "Deadlocks",
1137
0
            DisplayMode::Help => "Help",
1138
        };
1139
        
1140
0
        let status_bar = format!(
1141
0
            "{}[{}]{} | Last updated: {} | Press 'h' for help | Press 'q' to quit{}",
1142
            colors.bold,
1143
            mode_name,
1144
            colors.reset,
1145
0
            format_timestamp_short(self.last_update),
1146
            colors.reset
1147
        );
1148
        
1149
        // Print status bar with background
1150
0
        print!("\x1b[7m{:<width$}\x1b[0m", status_bar, width = self.terminal_size.0 as usize);
1151
        
1152
0
        Ok(())
1153
0
    }
1154
    
1155
    /// Update terminal size
1156
0
    fn update_terminal_size(&mut self) -> Result<()> {
1157
        // In a real implementation, we would get the actual terminal size
1158
        // For now, use default values
1159
0
        self.terminal_size = (80, 24);
1160
0
        Ok(())
1161
0
    }
1162
    
1163
    /// Switch to a different display mode
1164
0
    pub fn set_display_mode(&mut self, mode: DisplayMode) {
1165
0
        self.display_mode = mode;
1166
0
    }
1167
    
1168
    /// Get current display mode
1169
0
    pub fn get_display_mode(&self) -> DisplayMode {
1170
0
        self.display_mode
1171
0
    }
1172
    
1173
    /// Cycle to the next display mode
1174
0
    pub fn cycle_display_mode(&mut self) {
1175
0
        self.display_mode = match self.display_mode {
1176
0
            DisplayMode::Overview => DisplayMode::ActorList,
1177
0
            DisplayMode::ActorList => DisplayMode::MessageTraces,
1178
0
            DisplayMode::MessageTraces => DisplayMode::Metrics,
1179
0
            DisplayMode::Metrics => DisplayMode::Deadlocks,
1180
0
            DisplayMode::Deadlocks => DisplayMode::Help,
1181
0
            DisplayMode::Help => DisplayMode::Overview,
1182
        };
1183
0
    }
1184
    
1185
    /// Set terminal size
1186
0
    pub fn set_terminal_size(&mut self, width: u16, height: u16) {
1187
0
        self.terminal_size = (width, height);
1188
0
    }
1189
    
1190
    /// Set scroll position for a display mode
1191
0
    pub fn set_scroll_position(&mut self, mode: DisplayMode, position: usize) {
1192
0
        self.scroll_positions.insert(mode, position);
1193
0
    }
1194
    
1195
    /// Get scroll position for a display mode
1196
0
    pub fn get_scroll_position(&self, mode: DisplayMode) -> usize {
1197
0
        self.scroll_positions.get(&mode).copied().unwrap_or(0)
1198
0
    }
1199
    
1200
    /// Format text with color
1201
0
    pub fn format_with_color(&self, text: &str, _color: &str) -> String {
1202
0
        text.to_string()
1203
0
    }
1204
    
1205
    /// Get color for actor state
1206
0
    pub fn get_actor_state_color(&self, state: ActorState) -> &'static str {
1207
0
        match state {
1208
0
            ActorState::Running => "green",
1209
0
            ActorState::Failed(_) => "red",
1210
0
            ActorState::Stopped => "gray",
1211
0
            ActorState::Starting => "yellow",
1212
0
            ActorState::Restarting => "yellow",
1213
0
            ActorState::Stopping => "yellow",
1214
0
            ActorState::Processing(_) => "blue",
1215
        }
1216
0
    }
1217
    
1218
    /// Get color for message status
1219
0
    pub fn get_message_status_color(&self, status: MessageStatus) -> &'static str {
1220
0
        match status {
1221
0
            MessageStatus::Completed => "green",
1222
0
            MessageStatus::Failed => "red",
1223
0
            MessageStatus::Processing => "blue",
1224
0
            MessageStatus::Queued => "yellow",
1225
0
            MessageStatus::Dropped => "gray",
1226
        }
1227
0
    }
1228
    
1229
    /// Format duration in microseconds
1230
0
    pub fn format_duration_us(&self, us: u64) -> String {
1231
0
        if us < 1000 {
1232
0
            format!("{us}μs")
1233
0
        } else if us < 1_000_000 {
1234
0
            format!("{:.1}ms", us as f64 / 1000.0)
1235
0
        } else if us < 60_000_000 {
1236
0
            format!("{:.1}s", us as f64 / 1_000_000.0)
1237
        } else {
1238
0
            let secs = us / 1_000_000;
1239
0
            format!("{}m {}s", secs / 60, secs % 60)
1240
        }
1241
0
    }
1242
    
1243
    /// Format bytes helper
1244
0
    pub fn format_bytes(&self, bytes: usize) -> String {
1245
0
        format_bytes(bytes)
1246
0
    }
1247
    
1248
    /// Format timestamp
1249
0
    pub fn format_timestamp(&self, _timestamp: u64) -> String {
1250
0
        "12:34:56".to_string()
1251
0
    }
1252
    
1253
    /// Truncate string
1254
0
    pub fn truncate_string(&self, text: &str, max_len: usize) -> String {
1255
0
        if text.len() <= max_len {
1256
0
            text.to_string()
1257
0
        } else if max_len < 3 {
1258
0
            ".".repeat(max_len)
1259
        } else {
1260
0
            format!("{}...", &text[..max_len - 3])
1261
        }
1262
0
    }
1263
    
1264
    /// Render header
1265
0
    pub fn render_header(&self, title: &str) -> String {
1266
0
        format!("=== {title} ===")
1267
0
    }
1268
    
1269
    /// Render separator
1270
0
    pub fn render_separator(&self) -> String {
1271
0
        "-".repeat(40)
1272
0
    }
1273
    
1274
    /// Render table row
1275
0
    pub fn render_table_row(&self, columns: Vec<&str>) -> String {
1276
0
        columns.join(" | ")
1277
0
    }
1278
    
1279
    /// Render progress bar
1280
0
    pub fn render_progress_bar(&self, current: usize, total: usize, _width: usize) -> String {
1281
0
        let percent = if total > 0 {
1282
0
            (current * 100) / total
1283
        } else {
1284
0
            0
1285
        };
1286
0
        format!("[{percent}%]")
1287
0
    }
1288
    
1289
    /// Handle key press
1290
0
    pub fn handle_key(&mut self, key: char) -> bool {
1291
0
        match key {
1292
0
            'q' | 'Q' => true,
1293
0
            '1' => { self.display_mode = DisplayMode::Overview; false }
1294
0
            '2' => { self.display_mode = DisplayMode::ActorList; false }
1295
0
            '3' => { self.display_mode = DisplayMode::MessageTraces; false }
1296
0
            '4' => { self.display_mode = DisplayMode::Metrics; false }
1297
0
            '5' => { self.display_mode = DisplayMode::Deadlocks; false }
1298
0
            'h' => { self.display_mode = DisplayMode::Help; false }
1299
0
            'r' => { self.last_update = Instant::now(); false }
1300
0
            _ => false,
1301
        }
1302
0
    }
1303
}
1304
1305
/// Format a state for display
1306
0
fn state_display(state: &ActorState) -> String {
1307
0
    match state {
1308
0
        ActorState::Starting => "Starting".to_string(),
1309
0
        ActorState::Running => "Running".to_string(),
1310
0
        ActorState::Processing(msg) => format!("Proc({msg})"),
1311
0
        ActorState::Restarting => "Restarting".to_string(),
1312
0
        ActorState::Stopping => "Stopping".to_string(),
1313
0
        ActorState::Stopped => "Stopped".to_string(),
1314
0
        ActorState::Failed(reason) => format!("Failed({reason})"),
1315
    }
1316
0
}
1317
1318
/// Format a timestamp for display
1319
0
fn format_timestamp(timestamp: u64) -> String {
1320
    use std::time::{SystemTime, UNIX_EPOCH};
1321
    
1322
0
    if let Ok(duration) = SystemTime::now().duration_since(UNIX_EPOCH) {
1323
0
        let now = duration.as_secs();
1324
0
        if timestamp > now - 60 {
1325
0
            format!("{}s ago", now - timestamp)
1326
        } else {
1327
0
            "old".to_string()
1328
        }
1329
    } else {
1330
0
        "unknown".to_string()
1331
    }
1332
0
}
1333
1334
/// Format a timestamp for display (short form)
1335
0
fn format_timestamp_short(instant: Instant) -> String {
1336
0
    let elapsed = instant.elapsed();
1337
0
    if elapsed < Duration::from_secs(60) {
1338
0
        format!("{}s ago", elapsed.as_secs())
1339
    } else {
1340
0
        format!("{}m ago", elapsed.as_secs() / 60)
1341
    }
1342
0
}
1343
1344
/// Format a duration for display
1345
0
fn format_duration(duration: Duration) -> String {
1346
0
    let total_seconds = duration.as_secs();
1347
0
    let hours = total_seconds / 3600;
1348
0
    let minutes = (total_seconds % 3600) / 60;
1349
0
    let seconds = total_seconds % 60;
1350
    
1351
0
    if hours > 0 {
1352
0
        format!("{hours}h {minutes}m {seconds}s")
1353
0
    } else if minutes > 0 {
1354
0
        format!("{minutes}m {seconds}s")
1355
    } else {
1356
0
        format!("{seconds}s")
1357
    }
1358
0
}
1359
1360
/// Format bytes for display
1361
0
fn format_bytes(bytes: usize) -> String {
1362
    const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
1363
    #[allow(clippy::cast_precision_loss)] // Acceptable for display formatting
1364
0
    let mut size = bytes as f64;
1365
0
    let mut unit_index = 0;
1366
    
1367
0
    while size >= 1024.0 && unit_index < UNITS.len() - 1 {
1368
0
        size /= 1024.0;
1369
0
        unit_index += 1;
1370
0
    }
1371
    
1372
0
    if unit_index == 0 {
1373
0
        format!("{} {}", bytes, UNITS[unit_index])
1374
    } else {
1375
0
        format!("{:.1} {}", size, UNITS[unit_index])
1376
    }
1377
0
}
1378
1379
/// Format a message for preview display
1380
0
fn format_message_preview(message: &crate::runtime::actor::Message) -> String {
1381
    use crate::runtime::actor::Message;
1382
    
1383
0
    match message {
1384
0
        Message::Start => "Start".to_string(),
1385
0
        Message::Stop => "Stop".to_string(),
1386
0
        Message::Restart => "Restart".to_string(),
1387
0
        Message::User(msg_type, _) => format!("User({msg_type})"),
1388
0
        Message::Error(err) => format!("Error({err})"),
1389
0
        Message::ChildFailed(actor_id, reason) => format!("ChildFailed({actor_id}, {reason})"),
1390
0
        Message::ChildRestarted(actor_id) => format!("ChildRestarted({actor_id})"),
1391
    }
1392
0
}