Skip to main content

scarab_plugin_api/status_bar/
mod.rs

1//! Status Bar Rendering API for Scarab terminal emulator
2//!
3//! This module provides types and utilities for programmable status bars.
4//! Similar to WezTerm's status bar API, it allows plugins to dynamically
5//! update status bar content with rich styling and formatting.
6//!
7//! ## Example
8//!
9//! ```rust
10//! use scarab_plugin_api::status_bar::{RenderItem, Color, StatusBarUpdate, StatusBarSide};
11//!
12//! // Create a styled status bar update
13//! let items = vec![
14//!     RenderItem::Foreground(Color::Hex("#7aa2f7".to_string())),
15//!     RenderItem::Text("~/project".to_string()),
16//!     RenderItem::Text(" | ".to_string()),
17//!     RenderItem::Bold,
18//!     RenderItem::Text("12:34".to_string()),
19//!     RenderItem::ResetAttributes,
20//! ];
21//!
22//! let update = StatusBarUpdate {
23//!     window_id: 1,
24//!     side: StatusBarSide::Right,
25//!     items,
26//! };
27//! ```
28
29use serde::{Deserialize, Serialize};
30
31/// A single rendering element for status bar content
32///
33/// RenderItems are processed sequentially to build styled text for the status bar.
34/// They can represent text content, color changes, text attributes, or layout elements.
35#[derive(Clone, Debug, Serialize, Deserialize)]
36#[serde(tag = "type", content = "data")]
37pub enum RenderItem {
38    /// Display text content
39    ///
40    /// Renders the provided string with the current text style.
41    ///
42    /// # Example
43    ///
44    /// ```rust
45    /// use scarab_plugin_api::status_bar::RenderItem;
46    ///
47    /// let item = RenderItem::Text("Hello, World!".to_string());
48    /// ```
49    Text(String),
50
51    /// Display a Nerd Font icon
52    ///
53    /// Renders an icon from the Nerd Font icon set by name.
54    /// The icon will use the current foreground color and attributes.
55    ///
56    /// # Example
57    ///
58    /// ```rust
59    /// use scarab_plugin_api::status_bar::RenderItem;
60    ///
61    /// let item = RenderItem::Icon("nf-fa-battery_full".to_string());
62    /// ```
63    Icon(String),
64
65    /// Set foreground (text) color
66    ///
67    /// Changes the text color for all subsequent text items until
68    /// another color change or reset.
69    ///
70    /// # Example
71    ///
72    /// ```rust
73    /// use scarab_plugin_api::status_bar::{RenderItem, Color};
74    ///
75    /// let item = RenderItem::Foreground(Color::Rgb(122, 162, 247));
76    /// ```
77    Foreground(Color),
78
79    /// Set foreground color using ANSI color
80    ///
81    /// Alternative to `Foreground` using standard ANSI color names.
82    ///
83    /// # Example
84    ///
85    /// ```rust
86    /// use scarab_plugin_api::status_bar::{RenderItem, AnsiColor};
87    ///
88    /// let item = RenderItem::ForegroundAnsi(AnsiColor::BrightBlue);
89    /// ```
90    ForegroundAnsi(AnsiColor),
91
92    /// Set background color
93    ///
94    /// Changes the background color for all subsequent text items.
95    ///
96    /// # Example
97    ///
98    /// ```rust
99    /// use scarab_plugin_api::status_bar::{RenderItem, Color};
100    ///
101    /// let item = RenderItem::Background(Color::Named("darkgray".to_string()));
102    /// ```
103    Background(Color),
104
105    /// Set background color using ANSI color
106    ///
107    /// Alternative to `Background` using standard ANSI color names.
108    ///
109    /// # Example
110    ///
111    /// ```rust
112    /// use scarab_plugin_api::status_bar::{RenderItem, AnsiColor};
113    ///
114    /// let item = RenderItem::BackgroundAnsi(AnsiColor::Black);
115    /// ```
116    BackgroundAnsi(AnsiColor),
117
118    /// Enable bold text attribute
119    ///
120    /// Makes all subsequent text bold until reset or another weight change.
121    ///
122    /// # Example
123    ///
124    /// ```rust
125    /// use scarab_plugin_api::status_bar::RenderItem;
126    ///
127    /// let items = vec![
128    ///     RenderItem::Bold,
129    ///     RenderItem::Text("Important".to_string()),
130    /// ];
131    /// ```
132    Bold,
133
134    /// Enable italic text attribute
135    ///
136    /// Makes all subsequent text italic until reset.
137    ///
138    /// # Example
139    ///
140    /// ```rust
141    /// use scarab_plugin_api::status_bar::RenderItem;
142    ///
143    /// let items = vec![
144    ///     RenderItem::Italic,
145    ///     RenderItem::Text("Emphasis".to_string()),
146    /// ];
147    /// ```
148    Italic,
149
150    /// Enable underline with specified style
151    ///
152    /// Underlines all subsequent text with the given style until reset.
153    ///
154    /// # Example
155    ///
156    /// ```rust
157    /// use scarab_plugin_api::status_bar::{RenderItem, UnderlineStyle};
158    ///
159    /// let items = vec![
160    ///     RenderItem::Underline(UnderlineStyle::Curly),
161    ///     RenderItem::Text("Warning".to_string()),
162    /// ];
163    /// ```
164    Underline(UnderlineStyle),
165
166    /// Enable strikethrough text attribute
167    ///
168    /// Strikes through all subsequent text until reset.
169    ///
170    /// # Example
171    ///
172    /// ```rust
173    /// use scarab_plugin_api::status_bar::RenderItem;
174    ///
175    /// let items = vec![
176    ///     RenderItem::Strikethrough,
177    ///     RenderItem::Text("Deprecated".to_string()),
178    /// ];
179    /// ```
180    Strikethrough,
181
182    /// Reset all text attributes to defaults
183    ///
184    /// Clears all colors, bold, italic, underline, and strikethrough attributes.
185    ///
186    /// # Example
187    ///
188    /// ```rust
189    /// use scarab_plugin_api::status_bar::RenderItem;
190    ///
191    /// let items = vec![
192    ///     RenderItem::Bold,
193    ///     RenderItem::Text("Bold".to_string()),
194    ///     RenderItem::ResetAttributes,
195    ///     RenderItem::Text("Normal".to_string()),
196    /// ];
197    /// ```
198    ResetAttributes,
199
200    /// Reset foreground color to default
201    ///
202    /// Clears only the foreground color, keeping other attributes.
203    ///
204    /// # Example
205    ///
206    /// ```rust
207    /// use scarab_plugin_api::status_bar::{RenderItem, Color};
208    ///
209    /// let items = vec![
210    ///     RenderItem::Bold,
211    ///     RenderItem::Foreground(Color::Hex("#ff0000".to_string())),
212    ///     RenderItem::Text("Red & Bold".to_string()),
213    ///     RenderItem::ResetForeground,
214    ///     RenderItem::Text("Default color, still bold".to_string()),
215    /// ];
216    /// ```
217    ResetForeground,
218
219    /// Reset background color to default
220    ///
221    /// Clears only the background color, keeping other attributes.
222    ///
223    /// # Example
224    ///
225    /// ```rust
226    /// use scarab_plugin_api::status_bar::{RenderItem, Color};
227    ///
228    /// let items = vec![
229    ///     RenderItem::Background(Color::Hex("#333333".to_string())),
230    ///     RenderItem::Text("Dark background".to_string()),
231    ///     RenderItem::ResetBackground,
232    ///     RenderItem::Text("Default background".to_string()),
233    /// ];
234    /// ```
235    ResetBackground,
236
237    /// Insert flexible spacing
238    ///
239    /// Creates space that expands to fill available room.
240    /// Useful for pushing content to opposite ends of the status bar.
241    ///
242    /// # Example
243    ///
244    /// ```rust
245    /// use scarab_plugin_api::status_bar::RenderItem;
246    ///
247    /// let items = vec![
248    ///     RenderItem::Text("Left".to_string()),
249    ///     RenderItem::Spacer,
250    ///     RenderItem::Text("Right".to_string()),
251    /// ];
252    /// ```
253    Spacer,
254
255    /// Insert fixed spacing
256    ///
257    /// Adds a fixed number of space characters (cells).
258    ///
259    /// # Example
260    ///
261    /// ```rust
262    /// use scarab_plugin_api::status_bar::RenderItem;
263    ///
264    /// let items = vec![
265    ///     RenderItem::Text("A".to_string()),
266    ///     RenderItem::Padding(3),
267    ///     RenderItem::Text("B".to_string()),
268    /// ];
269    /// ```
270    Padding(u8),
271
272    /// Insert a separator string
273    ///
274    /// Convenience item for common separators like " | " or " • ".
275    ///
276    /// # Example
277    ///
278    /// ```rust
279    /// use scarab_plugin_api::status_bar::RenderItem;
280    ///
281    /// let items = vec![
282    ///     RenderItem::Text("Section 1".to_string()),
283    ///     RenderItem::Separator(" | ".to_string()),
284    ///     RenderItem::Text("Section 2".to_string()),
285    /// ];
286    /// ```
287    Separator(String),
288}
289
290/// Color specification for text rendering
291///
292/// Supports RGB values, hex notation, and named colors.
293#[derive(Clone, Debug, Serialize, Deserialize)]
294#[serde(tag = "type", content = "value")]
295pub enum Color {
296    /// RGB color with 8-bit components
297    ///
298    /// # Example
299    ///
300    /// ```rust
301    /// use scarab_plugin_api::status_bar::Color;
302    ///
303    /// let color = Color::Rgb(122, 162, 247);  // Light blue
304    /// ```
305    Rgb(u8, u8, u8),
306
307    /// Hexadecimal color string
308    ///
309    /// Supports standard hex color notation with or without '#' prefix.
310    ///
311    /// # Example
312    ///
313    /// ```rust
314    /// use scarab_plugin_api::status_bar::Color;
315    ///
316    /// let color = Color::Hex("#7aa2f7".to_string());
317    /// ```
318    Hex(String),
319
320    /// Named color from theme or CSS color names
321    ///
322    /// Supports standard CSS color names like "red", "blue", "darkgray", etc.
323    ///
324    /// # Example
325    ///
326    /// ```rust
327    /// use scarab_plugin_api::status_bar::Color;
328    ///
329    /// let color = Color::Named("cornflowerblue".to_string());
330    /// ```
331    Named(String),
332}
333
334/// Standard 16-color ANSI palette
335///
336/// Provides the standard ANSI color set with both normal and bright variants.
337#[derive(Clone, Debug, Serialize, Deserialize)]
338pub enum AnsiColor {
339    /// Standard black (typically #000000)
340    Black,
341    /// Standard red (typically #800000)
342    Red,
343    /// Standard green (typically #008000)
344    Green,
345    /// Standard yellow (typically #808000)
346    Yellow,
347    /// Standard blue (typically #000080)
348    Blue,
349    /// Standard magenta (typically #800080)
350    Magenta,
351    /// Standard cyan (typically #008080)
352    Cyan,
353    /// Standard white (typically #c0c0c0)
354    White,
355    /// Bright/bold black, also known as gray (typically #808080)
356    BrightBlack,
357    /// Bright/bold red (typically #ff0000)
358    BrightRed,
359    /// Bright/bold green (typically #00ff00)
360    BrightGreen,
361    /// Bright/bold yellow (typically #ffff00)
362    BrightYellow,
363    /// Bright/bold blue (typically #0000ff)
364    BrightBlue,
365    /// Bright/bold magenta (typically #ff00ff)
366    BrightMagenta,
367    /// Bright/bold cyan (typically #00ffff)
368    BrightCyan,
369    /// Bright/bold white (typically #ffffff)
370    BrightWhite,
371}
372
373impl AnsiColor {
374    /// Convert ANSI color to RGB values
375    ///
376    /// Returns a tuple of (r, g, b) values for the color.
377    /// Uses a standard terminal color palette.
378    ///
379    /// # Example
380    ///
381    /// ```rust
382    /// use scarab_plugin_api::status_bar::AnsiColor;
383    ///
384    /// let (r, g, b) = AnsiColor::BrightBlue.to_rgb();
385    /// assert_eq!((r, g, b), (0, 0, 255));
386    /// ```
387    pub fn to_rgb(&self) -> (u8, u8, u8) {
388        match self {
389            AnsiColor::Black => (0, 0, 0),
390            AnsiColor::Red => (128, 0, 0),
391            AnsiColor::Green => (0, 128, 0),
392            AnsiColor::Yellow => (128, 128, 0),
393            AnsiColor::Blue => (0, 0, 128),
394            AnsiColor::Magenta => (128, 0, 128),
395            AnsiColor::Cyan => (0, 128, 128),
396            AnsiColor::White => (192, 192, 192),
397            AnsiColor::BrightBlack => (128, 128, 128),
398            AnsiColor::BrightRed => (255, 0, 0),
399            AnsiColor::BrightGreen => (0, 255, 0),
400            AnsiColor::BrightYellow => (255, 255, 0),
401            AnsiColor::BrightBlue => (0, 0, 255),
402            AnsiColor::BrightMagenta => (255, 0, 255),
403            AnsiColor::BrightCyan => (0, 255, 255),
404            AnsiColor::BrightWhite => (255, 255, 255),
405        }
406    }
407}
408
409/// Underline style options
410///
411/// Defines different visual styles for underlined text.
412#[derive(Clone, Debug, Serialize, Deserialize)]
413pub enum UnderlineStyle {
414    /// Single straight line
415    Single,
416    /// Double straight lines
417    Double,
418    /// Wavy/curly line (often used for spelling errors)
419    Curly,
420    /// Dotted line
421    Dotted,
422    /// Dashed line
423    Dashed,
424}
425
426/// Status bar side/position
427///
428/// Identifies which side of the status bar to update.
429#[derive(Clone, Debug, Serialize, Deserialize)]
430pub enum StatusBarSide {
431    /// Left side of the status bar
432    Left,
433    /// Right side of the status bar
434    Right,
435}
436
437/// IPC message for status bar updates
438///
439/// Sent from daemon to client (or from plugin to UI) to update
440/// status bar content.
441///
442/// # Example
443///
444/// ```rust
445/// use scarab_plugin_api::status_bar::{StatusBarUpdate, StatusBarSide, RenderItem};
446///
447/// let update = StatusBarUpdate {
448///     window_id: 1,
449///     side: StatusBarSide::Right,
450///     items: vec![
451///         RenderItem::Text("12:34 PM".to_string()),
452///     ],
453/// };
454/// ```
455#[derive(Clone, Debug, Serialize, Deserialize)]
456pub struct StatusBarUpdate {
457    /// ID of the window to update
458    pub window_id: u64,
459    /// Which side of the status bar to update
460    pub side: StatusBarSide,
461    /// Render items to display
462    pub items: Vec<RenderItem>,
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468
469    #[test]
470    fn test_ansi_color_to_rgb() {
471        assert_eq!(AnsiColor::Black.to_rgb(), (0, 0, 0));
472        assert_eq!(AnsiColor::Red.to_rgb(), (128, 0, 0));
473        assert_eq!(AnsiColor::BrightRed.to_rgb(), (255, 0, 0));
474        assert_eq!(AnsiColor::BrightWhite.to_rgb(), (255, 255, 255));
475    }
476
477    #[test]
478    fn test_render_item_serialization() {
479        let item = RenderItem::Text("Hello".to_string());
480        let json = serde_json::to_string(&item).unwrap();
481        let deserialized: RenderItem = serde_json::from_str(&json).unwrap();
482
483        match deserialized {
484            RenderItem::Text(s) => assert_eq!(s, "Hello"),
485            _ => panic!("Expected Text variant"),
486        }
487    }
488
489    #[test]
490    fn test_color_serialization() {
491        let color = Color::Rgb(255, 128, 64);
492        let json = serde_json::to_string(&color).unwrap();
493        let deserialized: Color = serde_json::from_str(&json).unwrap();
494
495        match deserialized {
496            Color::Rgb(r, g, b) => {
497                assert_eq!(r, 255);
498                assert_eq!(g, 128);
499                assert_eq!(b, 64);
500            }
501            _ => panic!("Expected Rgb variant"),
502        }
503    }
504
505    #[test]
506    fn test_status_bar_update() {
507        let update = StatusBarUpdate {
508            window_id: 42,
509            side: StatusBarSide::Left,
510            items: vec![RenderItem::Bold, RenderItem::Text("Test".to_string())],
511        };
512
513        assert_eq!(update.window_id, 42);
514        assert_eq!(update.items.len(), 2);
515    }
516
517    #[test]
518    fn test_complex_status_bar_styling() {
519        let items = vec![
520            RenderItem::Foreground(Color::Hex("#7aa2f7".to_string())),
521            RenderItem::Text("~/project".to_string()),
522            RenderItem::ResetForeground,
523            RenderItem::Separator(" | ".to_string()),
524            RenderItem::Bold,
525            RenderItem::Foreground(Color::Named("green".to_string())),
526            RenderItem::Text("100%".to_string()),
527            RenderItem::ResetAttributes,
528        ];
529
530        assert_eq!(items.len(), 8);
531
532        // Verify first item
533        match &items[0] {
534            RenderItem::Foreground(Color::Hex(s)) => assert_eq!(s, "#7aa2f7"),
535            _ => panic!("Expected Foreground item"),
536        }
537    }
538}