Skip to main content

sbom_tools/tui/widgets/
change_badge.rs

1//! Change type badge widget for consistent change status display.
2
3use crate::tui::theme::colors;
4use ratatui::{prelude::*, widgets::Widget};
5
6/// Types of changes that can occur in a diff.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ChangeType {
9    Added,
10    Removed,
11    Modified,
12    Unchanged,
13}
14
15impl ChangeType {
16    /// Parse a change type from a label. Returns Unchanged for unrecognized values.
17    pub fn from_label(s: &str) -> Self {
18        match s.to_lowercase().as_str() {
19            "added" | "new" | "introduced" => Self::Added,
20            "removed" | "deleted" | "resolved" => Self::Removed,
21            "modified" | "changed" | "updated" => Self::Modified,
22            _ => Self::Unchanged,
23        }
24    }
25
26    /// Get the symbol for this change type.
27    pub fn symbol(&self) -> &'static str {
28        match self {
29            Self::Added => "+",
30            Self::Removed => "-",
31            Self::Modified => "~",
32            Self::Unchanged => "=",
33        }
34    }
35
36    /// Get the label for this change type.
37    pub fn label(&self) -> &'static str {
38        match self {
39            Self::Added => "ADDED",
40            Self::Removed => "REMOVED",
41            Self::Modified => "MODIFIED",
42            Self::Unchanged => "UNCHANGED",
43        }
44    }
45
46    /// Get the background color for this change type (uses theme colors).
47    pub fn color(&self) -> Color {
48        let scheme = colors();
49        match self {
50            Self::Added => scheme.added,
51            Self::Removed => scheme.removed,
52            Self::Modified => scheme.modified,
53            Self::Unchanged => scheme.unchanged,
54        }
55    }
56
57    /// Get the foreground color for badges (uses theme colors).
58    pub fn badge_fg(&self) -> Color {
59        colors().change_badge_fg()
60    }
61}
62
63/// A styled badge showing change status.
64#[derive(Debug, Clone)]
65pub struct ChangeTypeBadge {
66    change_type: ChangeType,
67    compact: bool,
68}
69
70impl ChangeTypeBadge {
71    /// Create a new change type badge.
72    pub fn new(change_type: ChangeType) -> Self {
73        Self {
74            change_type,
75            compact: false,
76        }
77    }
78
79    /// Create a badge from a label (e.g., "added", "removed", etc.).
80    pub fn from_label(s: &str) -> Self {
81        Self::new(ChangeType::from_label(s))
82    }
83
84    /// Use compact mode (symbol only).
85    pub fn compact(mut self) -> Self {
86        self.compact = true;
87        self
88    }
89
90    /// Get the style for this badge.
91    pub fn style(&self) -> Style {
92        Style::default()
93            .fg(self.change_type.badge_fg())
94            .bg(self.change_type.color())
95            .bold()
96    }
97
98    /// Get just the foreground color for text display (not badge).
99    pub fn fg_color(&self) -> Color {
100        self.change_type.color()
101    }
102
103    /// Convert to a Span for inline use.
104    pub fn to_span(&self) -> Span<'static> {
105        let text = if self.compact {
106            format!(" {} ", self.change_type.symbol())
107        } else {
108            format!(
109                " {} {} ",
110                self.change_type.symbol(),
111                self.change_type.label()
112            )
113        };
114
115        Span::styled(text, self.style())
116    }
117
118    /// Get the change type.
119    pub fn change_type(&self) -> ChangeType {
120        self.change_type
121    }
122}
123
124impl Widget for ChangeTypeBadge {
125    fn render(self, area: Rect, buf: &mut Buffer) {
126        if area.width < 3 || area.height < 1 {
127            return;
128        }
129
130        let text = if self.compact {
131            format!(" {} ", self.change_type.symbol())
132        } else {
133            let full = format!(
134                " {} {} ",
135                self.change_type.symbol(),
136                self.change_type.label()
137            );
138            if area.width as usize >= full.len() {
139                full
140            } else {
141                format!(" {} ", self.change_type.symbol())
142            }
143        };
144
145        let style = self.style();
146        let x = area.x;
147        let y = area.y;
148
149        for (i, ch) in text.chars().enumerate() {
150            if i < area.width as usize {
151                if let Some(cell) = buf.cell_mut((x + i as u16, y)) {
152                    cell.set_char(ch).set_style(style);
153                }
154            }
155        }
156    }
157}
158
159/// A compact change indicator (just the symbol with color, no background).
160#[derive(Debug, Clone)]
161pub struct ChangeIndicator {
162    change_type: ChangeType,
163}
164
165impl ChangeIndicator {
166    pub fn new(change_type: ChangeType) -> Self {
167        Self { change_type }
168    }
169
170    pub fn from_label(s: &str) -> Self {
171        Self::new(ChangeType::from_label(s))
172    }
173
174    /// Convert to a styled span (colored symbol without background).
175    pub fn to_span(&self) -> Span<'static> {
176        Span::styled(
177            self.change_type.symbol().to_string(),
178            Style::default().fg(self.change_type.color()).bold(),
179        )
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn test_change_type_from_label() {
189        assert_eq!(ChangeType::from_label("added"), ChangeType::Added);
190        assert_eq!(ChangeType::from_label("NEW"), ChangeType::Added);
191        assert_eq!(ChangeType::from_label("removed"), ChangeType::Removed);
192        assert_eq!(ChangeType::from_label("modified"), ChangeType::Modified);
193        assert_eq!(ChangeType::from_label("unknown"), ChangeType::Unchanged);
194    }
195
196    #[test]
197    fn test_change_type_symbols() {
198        assert_eq!(ChangeType::Added.symbol(), "+");
199        assert_eq!(ChangeType::Removed.symbol(), "-");
200        assert_eq!(ChangeType::Modified.symbol(), "~");
201        assert_eq!(ChangeType::Unchanged.symbol(), "=");
202    }
203
204    #[test]
205    fn test_change_type_colors_use_theme() {
206        let scheme = colors();
207        assert_eq!(ChangeType::Added.color(), scheme.added);
208        assert_eq!(ChangeType::Removed.color(), scheme.removed);
209        assert_eq!(ChangeType::Modified.color(), scheme.modified);
210    }
211}