Skip to main content

jugar_probar/brick/
tui.rs

1//! TUI Brick Traits (PROBAR-SPEC-009-P13)
2//!
3//! Implements the three-layer TUI brick architecture based on ttop patterns:
4//! - `CollectorBrick`: Gathers system/audio metrics
5//! - `AnalyzerBrick`: Produces insights from metrics
6//! - `PanelBrick`: Renders TUI panels with state machine
7//!
8//! # Architecture
9//!
10//! ```text
11//! ┌─────────────────────────────────────────────────────────────┐
12//! │                     PanelBrick Layer                        │
13//! │  (focus/explode state, layout, rendering)                   │
14//! ├─────────────────────────────────────────────────────────────┤
15//! │                    AnalyzerBrick Layer                      │
16//! │  (RTF calculation, trend detection, alerts)                 │
17//! ├─────────────────────────────────────────────────────────────┤
18//! │                   CollectorBrick Layer                      │
19//! │  (audio levels, system metrics, buffer stats)               │
20//! └─────────────────────────────────────────────────────────────┘
21//! ```
22//!
23//! # Example
24//!
25//! ```rust,ignore
26//! use probar::brick::tui::{CollectorBrick, RingBuffer, PanelState};
27//!
28//! // Collect audio levels into ring buffer
29//! let mut buffer: RingBuffer<f32> = RingBuffer::new(60);
30//! buffer.push(0.5);
31//! buffer.push(0.7);
32//!
33//! // Manage panel focus
34//! let mut state = PanelState::default();
35//! state.focus_next();
36//! ```
37
38use super::Brick;
39use std::collections::VecDeque;
40use std::time::Duration;
41
42// ============================================================================
43// Error Types
44// ============================================================================
45
46/// Error type for collector operations
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum CollectorError {
49    /// Feature not available on this platform
50    NotAvailable,
51    /// Collection failed with message
52    Failed(String),
53    /// Collector is disabled
54    Disabled,
55    /// Timeout during collection
56    Timeout,
57}
58
59impl std::fmt::Display for CollectorError {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        match self {
62            Self::NotAvailable => write!(f, "Feature not available on this platform"),
63            Self::Failed(msg) => write!(f, "Collection failed: {msg}"),
64            Self::Disabled => write!(f, "Collector is disabled"),
65            Self::Timeout => write!(f, "Collection timed out"),
66        }
67    }
68}
69
70impl std::error::Error for CollectorError {}
71
72// ============================================================================
73// Collector Brick
74// ============================================================================
75
76/// Trait for bricks that collect metrics from system or audio sources.
77///
78/// Collectors are feature-gated and can report availability.
79///
80/// # Example
81///
82/// ```rust,ignore
83/// struct AudioLevelCollector {
84///     sample_rate: u32,
85/// }
86///
87/// impl CollectorBrick for AudioLevelCollector {
88///     type Metrics = f32;
89///
90///     fn is_available(&self) -> bool { true }
91///
92///     fn collect(&mut self) -> Result<f32, CollectorError> {
93///         Ok(0.5) // Return current audio level
94///     }
95/// }
96/// ```
97pub trait CollectorBrick: Brick + Send + Sync {
98    /// Metrics type produced by this collector
99    type Metrics;
100
101    /// Check if collector is available on current platform.
102    ///
103    /// Returns `false` if the feature requires hardware or OS support
104    /// that isn't present.
105    fn is_available(&self) -> bool;
106
107    /// Collect metrics.
108    ///
109    /// # Errors
110    ///
111    /// Returns `CollectorError` if collection fails.
112    fn collect(&mut self) -> Result<Self::Metrics, CollectorError>;
113
114    /// Optional feature gate name for conditional compilation.
115    ///
116    /// Returns `None` if always available.
117    fn feature_gate(&self) -> Option<&'static str> {
118        None
119    }
120
121    /// Collection interval hint for schedulers.
122    fn collection_interval(&self) -> Duration {
123        Duration::from_millis(100)
124    }
125}
126
127// ============================================================================
128// Analyzer Brick
129// ============================================================================
130
131/// Trait for bricks that analyze collected metrics.
132///
133/// Analyzers transform raw metrics into insights (trends, alerts, summaries).
134///
135/// # Example
136///
137/// ```rust,ignore
138/// struct RtfAnalyzer;
139///
140/// impl AnalyzerBrick for RtfAnalyzer {
141///     type Input = (f64, f64); // (audio_duration, processing_time)
142///     type Output = RtfResult;
143///
144///     fn analyze(&self, input: &Self::Input) -> RtfResult {
145///         let rtf = input.1 / input.0;
146///         RtfResult { rtf, is_realtime: rtf < 1.0 }
147///     }
148/// }
149/// ```
150pub trait AnalyzerBrick: Brick + Send + Sync {
151    /// Input metrics type
152    type Input;
153    /// Output analysis type
154    type Output;
155
156    /// Analyze metrics and produce insights.
157    fn analyze(&self, input: &Self::Input) -> Self::Output;
158
159    /// Check if analysis is stale and needs refresh.
160    fn is_stale(&self, _age: Duration) -> bool {
161        false
162    }
163}
164
165// ============================================================================
166// Panel Brick
167// ============================================================================
168
169/// Trait for bricks that render TUI panels.
170///
171/// Panels support focus/explode behavior for keyboard navigation.
172pub trait PanelBrick: Brick + Send + Sync {
173    /// Render panel content to the given area.
174    ///
175    /// Returns lines of text to display.
176    fn render(&self, width: u16, height: u16) -> Vec<String>;
177
178    /// Panel title for border display.
179    fn title(&self) -> &str;
180
181    /// Whether this panel can be focused.
182    fn focusable(&self) -> bool {
183        true
184    }
185
186    /// Whether this panel can be exploded to full screen.
187    fn explodable(&self) -> bool {
188        true
189    }
190
191    /// Minimum height required for meaningful display.
192    fn min_height(&self) -> u16 {
193        3
194    }
195
196    /// Preferred height as fraction of available space (0.0-1.0).
197    fn preferred_height_fraction(&self) -> f32 {
198        0.25
199    }
200}
201
202// ============================================================================
203// Panel State Machine
204// ============================================================================
205
206/// Panel type identifier for focus/explode tracking.
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
208pub enum PanelId {
209    /// Waveform display
210    Waveform,
211    /// Spectrogram visualization
212    Spectrogram,
213    /// Transcription text
214    Transcription,
215    /// Performance metrics
216    Metrics,
217    /// VU meter
218    VuMeter,
219    /// Status bar
220    Status,
221    /// Custom panel with ID
222    Custom(u32),
223}
224
225/// Panel state for focus/explode behavior.
226///
227/// Implements the ttop pattern for keyboard-navigable TUI panels.
228#[derive(Debug, Clone)]
229pub struct PanelState {
230    /// Currently focused panel
231    pub focused: Option<PanelId>,
232    /// Currently exploded (full-screen) panel
233    pub exploded: Option<PanelId>,
234    /// Visible panels in display order
235    pub visible: Vec<PanelId>,
236}
237
238impl Default for PanelState {
239    fn default() -> Self {
240        Self {
241            focused: None,
242            exploded: None,
243            visible: vec![
244                PanelId::Waveform,
245                PanelId::Transcription,
246                PanelId::Metrics,
247                PanelId::Status,
248            ],
249        }
250    }
251}
252
253impl PanelState {
254    /// Create with custom panel list.
255    #[must_use]
256    pub fn with_panels(panels: Vec<PanelId>) -> Self {
257        Self {
258            focused: panels.first().copied(),
259            exploded: None,
260            visible: panels,
261        }
262    }
263
264    /// Focus next panel in list.
265    pub fn focus_next(&mut self) {
266        if self.visible.is_empty() {
267            self.focused = None;
268            return;
269        }
270
271        let current_idx = self
272            .focused
273            .and_then(|f| self.visible.iter().position(|p| *p == f));
274
275        let next_idx = current_idx
276            .map(|i| (i + 1) % self.visible.len())
277            .unwrap_or(0);
278
279        self.focused = self.visible.get(next_idx).copied();
280    }
281
282    /// Focus previous panel in list.
283    pub fn focus_prev(&mut self) {
284        if self.visible.is_empty() {
285            self.focused = None;
286            return;
287        }
288
289        let current_idx = self
290            .focused
291            .and_then(|f| self.visible.iter().position(|p| *p == f));
292
293        let prev_idx = current_idx
294            .map(|i| {
295                if i == 0 {
296                    self.visible.len() - 1
297                } else {
298                    i - 1
299                }
300            })
301            .unwrap_or(0);
302
303        self.focused = self.visible.get(prev_idx).copied();
304    }
305
306    /// Toggle exploded state for focused panel.
307    pub fn toggle_explode(&mut self) {
308        if self.exploded.is_some() {
309            self.exploded = None;
310        } else {
311            self.exploded = self.focused;
312        }
313    }
314
315    /// Check if a panel is focused.
316    #[must_use]
317    pub fn is_focused(&self, panel: PanelId) -> bool {
318        self.focused == Some(panel)
319    }
320
321    /// Check if a panel is exploded.
322    #[must_use]
323    pub fn is_exploded(&self, panel: PanelId) -> bool {
324        self.exploded == Some(panel)
325    }
326
327    /// Check if any panel is exploded.
328    #[must_use]
329    pub fn has_exploded(&self) -> bool {
330        self.exploded.is_some()
331    }
332
333    /// Focus a specific panel.
334    pub fn focus(&mut self, panel: PanelId) {
335        if self.visible.contains(&panel) {
336            self.focused = Some(panel);
337        }
338    }
339
340    /// Add a panel to the visible list.
341    pub fn add_panel(&mut self, panel: PanelId) {
342        if !self.visible.contains(&panel) {
343            self.visible.push(panel);
344        }
345    }
346
347    /// Remove a panel from the visible list.
348    pub fn remove_panel(&mut self, panel: PanelId) {
349        self.visible.retain(|p| *p != panel);
350        if self.focused == Some(panel) {
351            self.focused = self.visible.first().copied();
352        }
353        if self.exploded == Some(panel) {
354            self.exploded = None;
355        }
356    }
357}
358
359// ============================================================================
360// Ring Buffer
361// ============================================================================
362
363/// Ring buffer for time-series data.
364///
365/// Implements the ttop pattern for efficient sliding window storage.
366/// Oldest values are evicted when capacity is reached.
367///
368/// # Example
369///
370/// ```rust,ignore
371/// use probar::brick::tui::RingBuffer;
372///
373/// let mut buf: RingBuffer<f32> = RingBuffer::new(60);
374/// for i in 0..100 {
375///     buf.push(i as f32);
376/// }
377/// assert_eq!(buf.len(), 60);
378/// assert_eq!(*buf.last().unwrap(), 99.0);
379/// ```
380#[derive(Debug, Clone)]
381pub struct RingBuffer<T> {
382    data: VecDeque<T>,
383    capacity: usize,
384}
385
386impl<T> RingBuffer<T> {
387    /// Create a new ring buffer with given capacity.
388    #[must_use]
389    pub fn new(capacity: usize) -> Self {
390        Self {
391            data: VecDeque::with_capacity(capacity),
392            capacity,
393        }
394    }
395
396    /// Push a value, evicting oldest if at capacity.
397    pub fn push(&mut self, value: T) {
398        if self.data.len() >= self.capacity {
399            self.data.pop_front();
400        }
401        self.data.push_back(value);
402    }
403
404    /// Get iterator over values (oldest first).
405    pub fn iter(&self) -> impl Iterator<Item = &T> {
406        self.data.iter()
407    }
408
409    /// Get mutable iterator over values.
410    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
411        self.data.iter_mut()
412    }
413
414    /// Get number of elements.
415    #[must_use]
416    pub fn len(&self) -> usize {
417        self.data.len()
418    }
419
420    /// Check if buffer is empty.
421    #[must_use]
422    pub fn is_empty(&self) -> bool {
423        self.data.is_empty()
424    }
425
426    /// Get capacity.
427    #[must_use]
428    pub fn capacity(&self) -> usize {
429        self.capacity
430    }
431
432    /// Get most recent value.
433    #[must_use]
434    pub fn last(&self) -> Option<&T> {
435        self.data.back()
436    }
437
438    /// Get oldest value.
439    #[must_use]
440    pub fn first(&self) -> Option<&T> {
441        self.data.front()
442    }
443
444    /// Get value at index (0 = oldest).
445    #[must_use]
446    pub fn get(&self, index: usize) -> Option<&T> {
447        self.data.get(index)
448    }
449
450    /// Clear the buffer.
451    pub fn clear(&mut self) {
452        self.data.clear();
453    }
454
455    /// Check if buffer is at capacity.
456    #[must_use]
457    pub fn is_full(&self) -> bool {
458        self.data.len() >= self.capacity
459    }
460}
461
462impl<T: Clone> RingBuffer<T> {
463    /// Make buffer contiguous and return as Vec.
464    #[must_use]
465    pub fn to_vec(&self) -> Vec<T> {
466        self.data.iter().cloned().collect()
467    }
468}
469
470impl<T: Copy + Default> RingBuffer<T> {
471    /// Fill with default values to capacity.
472    pub fn fill_default(&mut self) {
473        while self.data.len() < self.capacity {
474            self.data.push_back(T::default());
475        }
476    }
477}
478
479impl<T: Copy + Into<f64>> RingBuffer<T> {
480    /// Calculate average of all values.
481    #[must_use]
482    pub fn average(&self) -> f64 {
483        if self.is_empty() {
484            return 0.0;
485        }
486        let sum: f64 = self.data.iter().map(|v| (*v).into()).sum();
487        sum / self.data.len() as f64
488    }
489
490    /// Calculate min value.
491    #[must_use]
492    pub fn min(&self) -> Option<f64> {
493        self.data
494            .iter()
495            .map(|v| (*v).into())
496            .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
497    }
498
499    /// Calculate max value.
500    #[must_use]
501    pub fn max(&self) -> Option<f64> {
502        self.data
503            .iter()
504            .map(|v| (*v).into())
505            .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
506    }
507}
508
509// ============================================================================
510// CIELAB Color
511// ============================================================================
512
513/// CIELAB color for perceptually uniform gradients.
514///
515/// CIELAB (L*a*b*) is designed so that equal distances in the color space
516/// correspond to equal perceived color differences.
517///
518/// # Example
519///
520/// ```rust,ignore
521/// use probar::brick::tui::CielabColor;
522///
523/// // Create gradient from green (0%) to red (100%)
524/// let green = CielabColor::percent_gradient(0.0);
525/// let red = CielabColor::percent_gradient(1.0);
526///
527/// // Interpolate for 50%
528/// let yellow = green.lerp(&red, 0.5);
529/// ```
530#[derive(Debug, Clone, Copy, PartialEq)]
531pub struct CielabColor {
532    /// Lightness (0-100)
533    pub l: f32,
534    /// Green-red axis (approx -128 to 127)
535    pub a: f32,
536    /// Blue-yellow axis (approx -128 to 127)
537    pub b: f32,
538}
539
540impl CielabColor {
541    /// Create a CIELAB color.
542    #[must_use]
543    pub const fn new(l: f32, a: f32, b: f32) -> Self {
544        Self { l, a, b }
545    }
546
547    /// Interpolate between two colors.
548    #[must_use]
549    pub fn lerp(&self, other: &Self, t: f32) -> Self {
550        let t = t.clamp(0.0, 1.0);
551        Self {
552            l: self.l + (other.l - self.l) * t,
553            a: self.a + (other.a - self.a) * t,
554            b: self.b + (other.b - self.b) * t,
555        }
556    }
557
558    /// Convert to approximate sRGB (0-255).
559    ///
560    /// Note: This is a simplified conversion. For accurate results,
561    /// use a proper color management library.
562    #[must_use]
563    #[allow(
564        clippy::cast_possible_truncation,
565        clippy::cast_sign_loss,
566        clippy::many_single_char_names // x,y,z,r,g,b are standard color space variables
567    )]
568    pub fn to_rgb(&self) -> (u8, u8, u8) {
569        // Convert L*a*b* to XYZ (D65 illuminant)
570        let fy = (self.l + 16.0) / 116.0;
571        let fx = self.a / 500.0 + fy;
572        let fz = fy - self.b / 200.0;
573
574        let xr = if fx.powi(3) > 0.008856 {
575            fx.powi(3)
576        } else {
577            (116.0 * fx - 16.0) / 903.3
578        };
579        let yr = if self.l > 7.9996 {
580            fy.powi(3)
581        } else {
582            self.l / 903.3
583        };
584        let zr = if fz.powi(3) > 0.008856 {
585            fz.powi(3)
586        } else {
587            (116.0 * fz - 16.0) / 903.3
588        };
589
590        // D65 reference white
591        let x = xr * 0.95047;
592        let y = yr * 1.0;
593        let z = zr * 1.08883;
594
595        // XYZ to sRGB
596        let r = x * 3.2406 - y * 1.5372 - z * 0.4986;
597        let g = -x * 0.9689 + y * 1.8758 + z * 0.0415;
598        let b = x * 0.0557 - y * 0.2040 + z * 1.0570;
599
600        // Gamma correction
601        let gamma = |c: f32| -> f32 {
602            if c > 0.0031308 {
603                1.055 * c.powf(1.0 / 2.4) - 0.055
604            } else {
605                12.92 * c
606            }
607        };
608
609        let r = (gamma(r) * 255.0).clamp(0.0, 255.0) as u8;
610        let g = (gamma(g) * 255.0).clamp(0.0, 255.0) as u8;
611        let b = (gamma(b) * 255.0).clamp(0.0, 255.0) as u8;
612
613        (r, g, b)
614    }
615
616    /// Convert to hex string (e.g., "#ff0000").
617    #[must_use]
618    pub fn to_hex(&self) -> String {
619        let (r, g, b) = self.to_rgb();
620        format!("#{r:02x}{g:02x}{b:02x}")
621    }
622
623    /// Create perceptually uniform gradient from green to red.
624    ///
625    /// Uses green -> yellow -> red transition in CIELAB space.
626    #[must_use]
627    pub fn percent_gradient(percent: f32) -> Self {
628        let percent = percent.clamp(0.0, 1.0);
629
630        // Perceptually uniform green/yellow/red
631        let green = Self::new(87.0, -86.0, 83.0);
632        let yellow = Self::new(97.0, -21.0, 94.0);
633        let red = Self::new(53.0, 80.0, 67.0);
634
635        if percent < 0.5 {
636            green.lerp(&yellow, percent * 2.0)
637        } else {
638            yellow.lerp(&red, (percent - 0.5) * 2.0)
639        }
640    }
641
642    /// Create gradient for meter display (blue -> green -> yellow -> red).
643    #[must_use]
644    pub fn meter_gradient(level: f32) -> Self {
645        let level = level.clamp(0.0, 1.0);
646
647        let blue = Self::new(50.0, -10.0, -50.0);
648        let green = Self::new(87.0, -86.0, 83.0);
649        let yellow = Self::new(97.0, -21.0, 94.0);
650        let red = Self::new(53.0, 80.0, 67.0);
651
652        if level < 0.33 {
653            blue.lerp(&green, level * 3.0)
654        } else if level < 0.66 {
655            green.lerp(&yellow, (level - 0.33) * 3.0)
656        } else {
657            yellow.lerp(&red, (level - 0.66) * 3.0)
658        }
659    }
660}
661
662impl Default for CielabColor {
663    fn default() -> Self {
664        Self::new(50.0, 0.0, 0.0) // Neutral gray
665    }
666}
667
668// ============================================================================
669// Tests
670// ============================================================================
671
672#[cfg(test)]
673#[allow(clippy::unwrap_used, clippy::expect_used)]
674mod tests {
675    use super::*;
676
677    // RingBuffer tests
678    #[test]
679    fn test_ring_buffer_basic() {
680        let mut buf: RingBuffer<i32> = RingBuffer::new(3);
681        buf.push(1);
682        buf.push(2);
683        buf.push(3);
684        buf.push(4); // Should evict 1
685
686        let values: Vec<_> = buf.iter().copied().collect();
687        assert_eq!(values, vec![2, 3, 4]);
688    }
689
690    #[test]
691    fn test_ring_buffer_capacity() {
692        let mut buf: RingBuffer<i32> = RingBuffer::new(5);
693        for i in 0..10 {
694            buf.push(i);
695        }
696        assert_eq!(buf.len(), 5);
697        assert_eq!(*buf.last().unwrap(), 9);
698        assert_eq!(*buf.first().unwrap(), 5);
699    }
700
701    #[test]
702    fn test_ring_buffer_to_vec() {
703        let mut buf: RingBuffer<i32> = RingBuffer::new(3);
704        buf.push(1);
705        buf.push(2);
706        buf.push(3);
707        buf.push(4);
708
709        assert_eq!(buf.to_vec(), vec![2, 3, 4]);
710    }
711
712    #[test]
713    fn test_ring_buffer_average() {
714        let mut buf: RingBuffer<f32> = RingBuffer::new(4);
715        buf.push(1.0);
716        buf.push(2.0);
717        buf.push(3.0);
718        buf.push(4.0);
719
720        assert!((buf.average() - 2.5).abs() < 0.001);
721    }
722
723    #[test]
724    fn test_ring_buffer_min_max() {
725        let mut buf: RingBuffer<f32> = RingBuffer::new(5);
726        buf.push(3.0);
727        buf.push(1.0);
728        buf.push(4.0);
729        buf.push(1.5);
730        buf.push(9.0);
731
732        assert!((buf.min().unwrap() - 1.0).abs() < 0.001);
733        assert!((buf.max().unwrap() - 9.0).abs() < 0.001);
734    }
735
736    // PanelState tests
737    #[test]
738    fn test_panel_state_focus() {
739        let mut state = PanelState::default();
740        state.focused = Some(PanelId::Waveform);
741
742        state.focus_next();
743        assert_eq!(state.focused, Some(PanelId::Transcription));
744
745        state.focus_next();
746        assert_eq!(state.focused, Some(PanelId::Metrics));
747
748        state.focus_prev();
749        assert_eq!(state.focused, Some(PanelId::Transcription));
750    }
751
752    #[test]
753    fn test_panel_state_focus_wrap() {
754        let mut state = PanelState::with_panels(vec![PanelId::Waveform, PanelId::Metrics]);
755        state.focused = Some(PanelId::Metrics);
756
757        state.focus_next();
758        assert_eq!(state.focused, Some(PanelId::Waveform));
759
760        state.focus_prev();
761        assert_eq!(state.focused, Some(PanelId::Metrics));
762    }
763
764    #[test]
765    fn test_panel_state_explode() {
766        let mut state = PanelState::default();
767        state.focused = Some(PanelId::Transcription);
768
769        assert!(!state.has_exploded());
770
771        state.toggle_explode();
772        assert!(state.is_exploded(PanelId::Transcription));
773        assert!(state.has_exploded());
774
775        state.toggle_explode();
776        assert!(!state.has_exploded());
777    }
778
779    #[test]
780    fn test_panel_state_add_remove() {
781        let mut state = PanelState::default();
782        let custom = PanelId::Custom(42);
783
784        state.add_panel(custom);
785        assert!(state.visible.contains(&custom));
786
787        state.focus(custom);
788        assert_eq!(state.focused, Some(custom));
789
790        state.remove_panel(custom);
791        assert!(!state.visible.contains(&custom));
792        assert_ne!(state.focused, Some(custom));
793    }
794
795    // CIELAB tests
796    #[test]
797    fn test_cielab_lerp() {
798        let green = CielabColor::new(87.0, -86.0, 83.0);
799        let red = CielabColor::new(53.0, 80.0, 67.0);
800
801        let mid = green.lerp(&red, 0.5);
802        assert!((mid.l - 70.0).abs() < 0.1);
803        assert!((mid.a - (-3.0)).abs() < 0.1);
804    }
805
806    #[test]
807    fn test_cielab_gradient() {
808        let start = CielabColor::percent_gradient(0.0);
809        let end = CielabColor::percent_gradient(1.0);
810
811        // Start should be greenish (negative a)
812        assert!(start.a < 0.0);
813        // End should be reddish (positive a)
814        assert!(end.a > 0.0);
815    }
816
817    #[test]
818    fn test_cielab_to_rgb() {
819        let white = CielabColor::new(100.0, 0.0, 0.0);
820        let (r, g, b) = white.to_rgb();
821        // Should be close to white
822        assert!(r > 250);
823        assert!(g > 250);
824        assert!(b > 250);
825
826        let black = CielabColor::new(0.0, 0.0, 0.0);
827        let (r, g, b) = black.to_rgb();
828        // Should be close to black
829        assert!(r < 5);
830        assert!(g < 5);
831        assert!(b < 5);
832    }
833
834    #[test]
835    fn test_cielab_to_hex() {
836        let color = CielabColor::new(50.0, 0.0, 0.0);
837        let hex = color.to_hex();
838        assert!(hex.starts_with('#'));
839        assert_eq!(hex.len(), 7);
840    }
841
842    #[test]
843    fn test_cielab_meter_gradient() {
844        let low = CielabColor::meter_gradient(0.0);
845        let mid = CielabColor::meter_gradient(0.5);
846        let high = CielabColor::meter_gradient(1.0);
847
848        // Low should be bluish (negative b)
849        assert!(low.b < 0.0);
850        // High should be reddish (positive a)
851        assert!(high.a > 0.0);
852        // Mid should be greenish/yellowish
853        assert!(mid.l > 80.0);
854    }
855
856    // CollectorError tests
857    #[test]
858    fn test_collector_error_display() {
859        assert_eq!(
860            CollectorError::NotAvailable.to_string(),
861            "Feature not available on this platform"
862        );
863        assert_eq!(
864            CollectorError::Failed("test".into()).to_string(),
865            "Collection failed: test"
866        );
867    }
868
869    // ========================================================================
870    // Additional comprehensive tests for 95%+ coverage
871    // ========================================================================
872
873    #[test]
874    fn test_collector_error_disabled() {
875        assert_eq!(
876            CollectorError::Disabled.to_string(),
877            "Collector is disabled"
878        );
879    }
880
881    #[test]
882    fn test_collector_error_timeout() {
883        assert_eq!(CollectorError::Timeout.to_string(), "Collection timed out");
884    }
885
886    #[test]
887    fn test_collector_error_eq() {
888        assert_eq!(CollectorError::NotAvailable, CollectorError::NotAvailable);
889        assert_eq!(CollectorError::Disabled, CollectorError::Disabled);
890        assert_eq!(CollectorError::Timeout, CollectorError::Timeout);
891        assert_eq!(
892            CollectorError::Failed("a".into()),
893            CollectorError::Failed("a".into())
894        );
895        assert_ne!(
896            CollectorError::Failed("a".into()),
897            CollectorError::Failed("b".into())
898        );
899    }
900
901    #[test]
902    fn test_collector_error_clone() {
903        let err = CollectorError::Failed("test".into());
904        let cloned = err.clone();
905        assert_eq!(err, cloned);
906    }
907
908    #[test]
909    fn test_collector_error_is_error() {
910        let err = CollectorError::Timeout;
911        let _: &dyn std::error::Error = &err;
912    }
913
914    #[test]
915    fn test_ring_buffer_is_empty() {
916        let buf: RingBuffer<i32> = RingBuffer::new(5);
917        assert!(buf.is_empty());
918
919        let mut buf2: RingBuffer<i32> = RingBuffer::new(5);
920        buf2.push(1);
921        assert!(!buf2.is_empty());
922    }
923
924    #[test]
925    fn test_ring_buffer_capacity_getter() {
926        let buf: RingBuffer<i32> = RingBuffer::new(10);
927        assert_eq!(buf.capacity(), 10);
928    }
929
930    #[test]
931    fn test_ring_buffer_first_last_empty() {
932        let buf: RingBuffer<i32> = RingBuffer::new(5);
933        assert!(buf.first().is_none());
934        assert!(buf.last().is_none());
935    }
936
937    #[test]
938    fn test_ring_buffer_get() {
939        let mut buf: RingBuffer<i32> = RingBuffer::new(5);
940        buf.push(10);
941        buf.push(20);
942        buf.push(30);
943
944        assert_eq!(buf.get(0), Some(&10));
945        assert_eq!(buf.get(1), Some(&20));
946        assert_eq!(buf.get(2), Some(&30));
947        assert_eq!(buf.get(3), None);
948    }
949
950    #[test]
951    fn test_ring_buffer_clear() {
952        let mut buf: RingBuffer<i32> = RingBuffer::new(5);
953        buf.push(1);
954        buf.push(2);
955        buf.push(3);
956
957        assert_eq!(buf.len(), 3);
958        buf.clear();
959        assert_eq!(buf.len(), 0);
960        assert!(buf.is_empty());
961    }
962
963    #[test]
964    fn test_ring_buffer_is_full() {
965        let mut buf: RingBuffer<i32> = RingBuffer::new(3);
966        assert!(!buf.is_full());
967
968        buf.push(1);
969        assert!(!buf.is_full());
970
971        buf.push(2);
972        assert!(!buf.is_full());
973
974        buf.push(3);
975        assert!(buf.is_full());
976
977        buf.push(4); // Evicts oldest
978        assert!(buf.is_full());
979    }
980
981    #[test]
982    fn test_ring_buffer_iter_mut() {
983        let mut buf: RingBuffer<i32> = RingBuffer::new(5);
984        buf.push(1);
985        buf.push(2);
986        buf.push(3);
987
988        for val in buf.iter_mut() {
989            *val *= 2;
990        }
991
992        let values: Vec<_> = buf.iter().copied().collect();
993        assert_eq!(values, vec![2, 4, 6]);
994    }
995
996    #[test]
997    fn test_ring_buffer_fill_default() {
998        let mut buf: RingBuffer<i32> = RingBuffer::new(5);
999        buf.push(1);
1000        buf.push(2);
1001
1002        assert_eq!(buf.len(), 2);
1003        buf.fill_default();
1004        assert_eq!(buf.len(), 5);
1005        assert!(buf.is_full());
1006    }
1007
1008    #[test]
1009    fn test_ring_buffer_average_empty() {
1010        let buf: RingBuffer<f32> = RingBuffer::new(5);
1011        assert!((buf.average() - 0.0).abs() < 0.001);
1012    }
1013
1014    #[test]
1015    fn test_ring_buffer_min_max_empty() {
1016        let buf: RingBuffer<f32> = RingBuffer::new(5);
1017        assert!(buf.min().is_none());
1018        assert!(buf.max().is_none());
1019    }
1020
1021    #[test]
1022    fn test_ring_buffer_clone() {
1023        let mut buf: RingBuffer<i32> = RingBuffer::new(3);
1024        buf.push(1);
1025        buf.push(2);
1026
1027        let cloned = buf.clone();
1028        assert_eq!(buf.len(), cloned.len());
1029        assert_eq!(buf.to_vec(), cloned.to_vec());
1030    }
1031
1032    #[test]
1033    fn test_panel_id_eq() {
1034        assert_eq!(PanelId::Waveform, PanelId::Waveform);
1035        assert_eq!(PanelId::Custom(1), PanelId::Custom(1));
1036        assert_ne!(PanelId::Custom(1), PanelId::Custom(2));
1037        assert_ne!(PanelId::Waveform, PanelId::Spectrogram);
1038    }
1039
1040    #[test]
1041    fn test_panel_id_hash() {
1042        use std::collections::HashSet;
1043        let mut set = HashSet::new();
1044        set.insert(PanelId::Waveform);
1045        set.insert(PanelId::Spectrogram);
1046        set.insert(PanelId::Waveform); // Duplicate
1047        assert_eq!(set.len(), 2);
1048    }
1049
1050    #[test]
1051    fn test_panel_id_all_variants() {
1052        let ids = [
1053            PanelId::Waveform,
1054            PanelId::Spectrogram,
1055            PanelId::Transcription,
1056            PanelId::Metrics,
1057            PanelId::VuMeter,
1058            PanelId::Status,
1059            PanelId::Custom(0),
1060        ];
1061        for id in ids {
1062            // Just verify they can be created and compared
1063            assert_eq!(id, id);
1064        }
1065    }
1066
1067    #[test]
1068    fn test_panel_state_default() {
1069        let state = PanelState::default();
1070        assert!(state.focused.is_none());
1071        assert!(state.exploded.is_none());
1072        assert_eq!(state.visible.len(), 4);
1073        assert!(state.visible.contains(&PanelId::Waveform));
1074        assert!(state.visible.contains(&PanelId::Transcription));
1075        assert!(state.visible.contains(&PanelId::Metrics));
1076        assert!(state.visible.contains(&PanelId::Status));
1077    }
1078
1079    #[test]
1080    fn test_panel_state_with_panels() {
1081        let panels = vec![PanelId::Spectrogram, PanelId::VuMeter];
1082        let state = PanelState::with_panels(panels);
1083        assert_eq!(state.visible.len(), 2);
1084        assert_eq!(state.focused, Some(PanelId::Spectrogram));
1085    }
1086
1087    #[test]
1088    fn test_panel_state_with_panels_empty() {
1089        let state = PanelState::with_panels(vec![]);
1090        assert!(state.visible.is_empty());
1091        assert!(state.focused.is_none());
1092    }
1093
1094    #[test]
1095    fn test_panel_state_focus_next_empty() {
1096        let mut state = PanelState::with_panels(vec![]);
1097        state.focus_next();
1098        assert!(state.focused.is_none());
1099    }
1100
1101    #[test]
1102    fn test_panel_state_focus_prev_empty() {
1103        let mut state = PanelState::with_panels(vec![]);
1104        state.focus_prev();
1105        assert!(state.focused.is_none());
1106    }
1107
1108    #[test]
1109    fn test_panel_state_focus_next_no_current_focus() {
1110        let mut state = PanelState::with_panels(vec![PanelId::Waveform, PanelId::Metrics]);
1111        state.focused = None;
1112
1113        state.focus_next();
1114        assert_eq!(state.focused, Some(PanelId::Waveform));
1115    }
1116
1117    #[test]
1118    fn test_panel_state_focus_prev_no_current_focus() {
1119        let mut state = PanelState::with_panels(vec![PanelId::Waveform, PanelId::Metrics]);
1120        state.focused = None;
1121
1122        state.focus_prev();
1123        assert_eq!(state.focused, Some(PanelId::Waveform));
1124    }
1125
1126    #[test]
1127    fn test_panel_state_focus_prev_at_start() {
1128        let mut state = PanelState::with_panels(vec![PanelId::Waveform, PanelId::Metrics]);
1129        state.focused = Some(PanelId::Waveform);
1130
1131        state.focus_prev();
1132        assert_eq!(state.focused, Some(PanelId::Metrics)); // Wraps to end
1133    }
1134
1135    #[test]
1136    fn test_panel_state_toggle_explode_no_focus() {
1137        let mut state = PanelState::default();
1138        state.focused = None;
1139
1140        state.toggle_explode();
1141        // exploded should be set to None (focused)
1142        assert!(state.exploded.is_none());
1143    }
1144
1145    #[test]
1146    fn test_panel_state_is_focused() {
1147        let mut state = PanelState::default();
1148        state.focused = Some(PanelId::Waveform);
1149
1150        assert!(state.is_focused(PanelId::Waveform));
1151        assert!(!state.is_focused(PanelId::Metrics));
1152    }
1153
1154    #[test]
1155    fn test_panel_state_is_exploded() {
1156        let mut state = PanelState::default();
1157        state.exploded = Some(PanelId::Transcription);
1158
1159        assert!(state.is_exploded(PanelId::Transcription));
1160        assert!(!state.is_exploded(PanelId::Waveform));
1161    }
1162
1163    #[test]
1164    fn test_panel_state_focus_invalid_panel() {
1165        let mut state = PanelState::default();
1166        state.focused = Some(PanelId::Waveform);
1167
1168        // Try to focus a panel not in visible list
1169        state.focus(PanelId::VuMeter);
1170        // Focus should remain unchanged
1171        assert_eq!(state.focused, Some(PanelId::Waveform));
1172    }
1173
1174    #[test]
1175    fn test_panel_state_add_panel_duplicate() {
1176        let mut state = PanelState::default();
1177        let initial_len = state.visible.len();
1178
1179        state.add_panel(PanelId::Waveform); // Already in list
1180        assert_eq!(state.visible.len(), initial_len);
1181    }
1182
1183    #[test]
1184    fn test_panel_state_remove_panel_updates_focus() {
1185        let mut state = PanelState::default();
1186        state.focused = Some(PanelId::Metrics);
1187
1188        state.remove_panel(PanelId::Metrics);
1189        // Focus should move to first visible
1190        assert_eq!(state.focused, Some(PanelId::Waveform));
1191    }
1192
1193    #[test]
1194    fn test_panel_state_remove_panel_clears_exploded() {
1195        let mut state = PanelState::default();
1196        state.exploded = Some(PanelId::Metrics);
1197
1198        state.remove_panel(PanelId::Metrics);
1199        assert!(state.exploded.is_none());
1200    }
1201
1202    #[test]
1203    fn test_panel_state_clone() {
1204        let state = PanelState::default();
1205        let cloned = state.clone();
1206        assert_eq!(state.visible.len(), cloned.visible.len());
1207    }
1208
1209    #[test]
1210    fn test_cielab_default() {
1211        let color = CielabColor::default();
1212        assert!((color.l - 50.0).abs() < 0.001);
1213        assert!((color.a - 0.0).abs() < 0.001);
1214        assert!((color.b - 0.0).abs() < 0.001);
1215    }
1216
1217    #[test]
1218    fn test_cielab_lerp_clamp() {
1219        let c1 = CielabColor::new(0.0, 0.0, 0.0);
1220        let c2 = CielabColor::new(100.0, 100.0, 100.0);
1221
1222        // t < 0 should clamp to 0
1223        let result = c1.lerp(&c2, -0.5);
1224        assert!((result.l - 0.0).abs() < 0.001);
1225
1226        // t > 1 should clamp to 1
1227        let result = c1.lerp(&c2, 1.5);
1228        assert!((result.l - 100.0).abs() < 0.001);
1229    }
1230
1231    #[test]
1232    fn test_cielab_percent_gradient_clamp() {
1233        // Below 0 should clamp
1234        let color = CielabColor::percent_gradient(-0.5);
1235        assert!(color.a < 0.0); // Should be green
1236
1237        // Above 1 should clamp
1238        let color = CielabColor::percent_gradient(1.5);
1239        assert!(color.a > 0.0); // Should be red
1240    }
1241
1242    #[test]
1243    fn test_cielab_percent_gradient_midpoint() {
1244        // Test the transition point (0.5)
1245        let color = CielabColor::percent_gradient(0.5);
1246        // At 0.5, should be yellow-ish
1247        assert!(color.l > 90.0);
1248    }
1249
1250    #[test]
1251    fn test_cielab_meter_gradient_clamp() {
1252        let low = CielabColor::meter_gradient(-1.0);
1253        let high = CielabColor::meter_gradient(2.0);
1254        // Should clamp to valid range
1255        assert!(low.b < 0.0); // Blue
1256        assert!(high.a > 0.0); // Red
1257    }
1258
1259    #[test]
1260    fn test_cielab_meter_gradient_transitions() {
1261        // Test at boundary points
1262        let at_033 = CielabColor::meter_gradient(0.33);
1263        let at_066 = CielabColor::meter_gradient(0.66);
1264
1265        // Both should be valid colors
1266        assert!(at_033.l > 0.0);
1267        assert!(at_066.l > 0.0);
1268    }
1269
1270    #[test]
1271    fn test_cielab_to_rgb_edge_cases() {
1272        // Test with low L (dark)
1273        let dark = CielabColor::new(5.0, 0.0, 0.0);
1274        let (r, g, b) = dark.to_rgb();
1275        // Should be very dark
1276        assert!(r < 20);
1277        assert!(g < 20);
1278        assert!(b < 20);
1279    }
1280
1281    #[test]
1282    fn test_cielab_eq() {
1283        let c1 = CielabColor::new(50.0, 10.0, -20.0);
1284        let c2 = CielabColor::new(50.0, 10.0, -20.0);
1285        let c3 = CielabColor::new(51.0, 10.0, -20.0);
1286
1287        assert_eq!(c1, c2);
1288        assert_ne!(c1, c3);
1289    }
1290
1291    #[test]
1292    fn test_cielab_clone() {
1293        let c1 = CielabColor::new(75.0, 25.0, -50.0);
1294        let c2 = c1;
1295        assert_eq!(c1, c2);
1296    }
1297
1298    #[test]
1299    fn test_ring_buffer_f64() {
1300        let mut buf: RingBuffer<f64> = RingBuffer::new(3);
1301        buf.push(1.5);
1302        buf.push(2.5);
1303        buf.push(3.5);
1304
1305        assert!((buf.average() - 2.5).abs() < 0.001);
1306        assert!((buf.min().unwrap() - 1.5).abs() < 0.001);
1307        assert!((buf.max().unwrap() - 3.5).abs() < 0.001);
1308    }
1309}