## 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
    }