Skip to main content

par_term_config/
status_bar.rs

1//! Status bar widget configuration types.
2//!
3//! Defines the widget identifiers, section layout, and per-widget configuration
4//! used by the status bar system.
5
6use serde::de::{self, Deserializer};
7use serde::ser::Serializer;
8use serde::{Deserialize, Serialize};
9
10/// Section of the status bar where a widget is placed.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
12#[serde(rename_all = "lowercase")]
13pub enum StatusBarSection {
14    /// Left-aligned section (default)
15    #[default]
16    Left,
17    /// Center-aligned section
18    Center,
19    /// Right-aligned section
20    Right,
21}
22
23/// Identifier for a built-in or custom status bar widget.
24///
25/// Serialized as a single plain string (`as_key`/`from_key`) so it round-trips
26/// through `config.yaml`, which embeds the status bar via `#[serde(flatten)]`.
27/// Built-in widgets use their snake_case name (e.g. `git_branch`); custom
28/// widgets use `custom:<name>`; plugin widgets use `plugin:<id>`. serde's
29/// flatten path cannot deserialize the
30/// externally-tagged `Custom(String)` map form (`"untagged and internally tagged
31/// enums do not support enum input"`), so a manual scalar representation is
32/// used instead of the derived one.
33#[derive(Debug, Clone, PartialEq, Eq, Hash)]
34pub enum WidgetId {
35    /// Current time (HH:MM:SS)
36    Clock,
37    /// user@hostname
38    UsernameHostname,
39    /// Current working directory
40    CurrentDirectory,
41    /// Git branch name with icon
42    GitBranch,
43    /// CPU usage percentage
44    CpuUsage,
45    /// Memory usage (used / total)
46    MemoryUsage,
47    /// Network throughput (rx/tx rates)
48    NetworkStatus,
49    /// Free disk space (percent + bytes)
50    DiskFree,
51    /// Bell indicator with count
52    BellIndicator,
53    /// Currently running command name
54    CurrentCommand,
55    /// Update available notification
56    UpdateAvailable,
57    /// Agent subscription usage summary (self-hiding; click opens the usage panel)
58    AgentUsage,
59    /// par-mux agent roster summary (self-hiding; click opens the command
60    /// palette). Empty without an attached mux session.
61    AgentRoster,
62    /// Custom widget (user-defined via format string)
63    Custom(String),
64    /// Plugin-provided status bar widget (`plugin:<id>` key)
65    Plugin(String),
66}
67
68impl WidgetId {
69    /// Human-readable label for UI display.
70    pub fn label(&self) -> &str {
71        match self {
72            WidgetId::Clock => "Clock",
73            WidgetId::UsernameHostname => "User@Host",
74            WidgetId::CurrentDirectory => "Directory",
75            WidgetId::GitBranch => "Git Branch",
76            WidgetId::CpuUsage => "CPU Usage",
77            WidgetId::MemoryUsage => "Memory Usage",
78            WidgetId::NetworkStatus => "Network Status",
79            WidgetId::DiskFree => "Disk Free",
80            WidgetId::BellIndicator => "Bell Indicator",
81            WidgetId::CurrentCommand => "Current Command",
82            WidgetId::UpdateAvailable => "Update Available",
83            WidgetId::AgentUsage => "Agent Usage",
84            WidgetId::AgentRoster => "Agent Roster",
85            WidgetId::Custom(name) => name.as_str(),
86            WidgetId::Plugin(id) => id.as_str(),
87        }
88    }
89
90    /// Icon/prefix character for the widget.
91    pub fn icon(&self) -> &str {
92        match self {
93            WidgetId::Clock => "\u{1f551}",            // clock emoji
94            WidgetId::UsernameHostname => "\u{1f464}", // bust in silhouette
95            WidgetId::CurrentDirectory => "\u{1f4c2}", // open file folder
96            WidgetId::GitBranch => "\u{1f500}",        // twisted rightwards arrows (branch)
97            WidgetId::CpuUsage => "\u{1f4bb}",         // laptop
98            WidgetId::MemoryUsage => "\u{1f4be}",      // floppy disk
99            WidgetId::NetworkStatus => "\u{1f310}",    // globe with meridians
100            WidgetId::DiskFree => "\u{1f4bf}",         // optical disk
101            WidgetId::BellIndicator => "\u{1f514}",    // bell
102            WidgetId::CurrentCommand => "\u{25b6}",    // play button
103            WidgetId::UpdateAvailable => "\u{2b06}",   // upwards arrow
104            WidgetId::AgentUsage => "\u{25c6}",        // diamond (matches the summary glyph)
105            WidgetId::AgentRoster => "\u{1f465}",      // busts in silhouette (a roster of agents)
106            WidgetId::Custom(_) => "\u{2699}",         // gear
107            WidgetId::Plugin(_) => "\u{1f9e9}",        // puzzle piece
108        }
109    }
110
111    /// Whether this widget requires the system monitor to be running.
112    pub fn needs_system_monitor(&self) -> bool {
113        matches!(
114            self,
115            WidgetId::CpuUsage | WidgetId::MemoryUsage | WidgetId::NetworkStatus
116        )
117    }
118
119    /// Whether this widget requires the disk monitor to be running.
120    pub fn needs_disk_monitor(&self) -> bool {
121        matches!(self, WidgetId::DiskFree)
122    }
123
124    /// Stable string key used for YAML serialization. Built-in widgets use their
125    /// snake_case name; custom widgets are prefixed with `custom:`.
126    fn as_key(&self) -> String {
127        match self {
128            WidgetId::Clock => "clock".to_string(),
129            WidgetId::UsernameHostname => "username_hostname".to_string(),
130            WidgetId::CurrentDirectory => "current_directory".to_string(),
131            WidgetId::GitBranch => "git_branch".to_string(),
132            WidgetId::CpuUsage => "cpu_usage".to_string(),
133            WidgetId::MemoryUsage => "memory_usage".to_string(),
134            WidgetId::NetworkStatus => "network_status".to_string(),
135            WidgetId::DiskFree => "disk_free".to_string(),
136            WidgetId::BellIndicator => "bell_indicator".to_string(),
137            WidgetId::CurrentCommand => "current_command".to_string(),
138            WidgetId::UpdateAvailable => "update_available".to_string(),
139            WidgetId::AgentUsage => "agent_usage".to_string(),
140            WidgetId::AgentRoster => "agent_roster".to_string(),
141            WidgetId::Custom(name) => format!("custom:{name}"),
142            WidgetId::Plugin(id) => format!("plugin:{id}"),
143        }
144    }
145
146    /// Parse a serialization key back into a [`WidgetId`]. Returns `None` for
147    /// unrecognized built-in names.
148    fn from_key(key: &str) -> Option<WidgetId> {
149        if let Some(name) = key.strip_prefix("custom:") {
150            return Some(WidgetId::Custom(name.to_string()));
151        }
152        if let Some(id) = key.strip_prefix("plugin:") {
153            return Some(WidgetId::Plugin(id.to_string()));
154        }
155        Some(match key {
156            "clock" => WidgetId::Clock,
157            "username_hostname" => WidgetId::UsernameHostname,
158            "current_directory" => WidgetId::CurrentDirectory,
159            "git_branch" => WidgetId::GitBranch,
160            "cpu_usage" => WidgetId::CpuUsage,
161            "memory_usage" => WidgetId::MemoryUsage,
162            "network_status" => WidgetId::NetworkStatus,
163            "disk_free" => WidgetId::DiskFree,
164            "bell_indicator" => WidgetId::BellIndicator,
165            "current_command" => WidgetId::CurrentCommand,
166            "update_available" => WidgetId::UpdateAvailable,
167            "agent_usage" => WidgetId::AgentUsage,
168            "agent_roster" => WidgetId::AgentRoster,
169            _ => return None,
170        })
171    }
172}
173
174impl Serialize for WidgetId {
175    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
176    where
177        S: Serializer,
178    {
179        serializer.serialize_str(&self.as_key())
180    }
181}
182
183impl<'de> Deserialize<'de> for WidgetId {
184    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
185    where
186        D: Deserializer<'de>,
187    {
188        let key = String::deserialize(deserializer)?;
189        WidgetId::from_key(&key)
190            .ok_or_else(|| de::Error::custom(format!("unknown status bar widget id: `{key}`")))
191    }
192}
193
194/// Configuration for a single status bar widget.
195#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
196pub struct StatusBarWidgetConfig {
197    /// Which widget to display
198    pub id: WidgetId,
199    /// Whether this widget is enabled
200    #[serde(default = "default_true")]
201    pub enabled: bool,
202    /// Section placement (left, center, right)
203    #[serde(default)]
204    pub section: StatusBarSection,
205    /// Sort order within the section (lower values first)
206    #[serde(default)]
207    pub order: i32,
208    /// Optional format override string with `\(variable)` interpolation
209    #[serde(default, skip_serializing_if = "Option::is_none")]
210    pub format: Option<String>,
211}
212
213fn default_true() -> bool {
214    true
215}
216
217/// Default widget configuration set.
218///
219/// Returns a sensible starting set of widgets covering common use-cases.
220/// System monitor widgets (CPU, memory, network) are disabled by default
221/// to avoid unnecessary resource usage.
222pub fn default_widgets() -> Vec<StatusBarWidgetConfig> {
223    vec![
224        StatusBarWidgetConfig {
225            id: WidgetId::UsernameHostname,
226            enabled: true,
227            section: StatusBarSection::Left,
228            order: 0,
229            format: None,
230        },
231        StatusBarWidgetConfig {
232            id: WidgetId::CurrentDirectory,
233            enabled: true,
234            section: StatusBarSection::Left,
235            order: 1,
236            format: None,
237        },
238        StatusBarWidgetConfig {
239            id: WidgetId::GitBranch,
240            enabled: true,
241            section: StatusBarSection::Left,
242            order: 2,
243            format: None,
244        },
245        StatusBarWidgetConfig {
246            id: WidgetId::CurrentCommand,
247            enabled: true,
248            section: StatusBarSection::Center,
249            order: 0,
250            format: None,
251        },
252        StatusBarWidgetConfig {
253            id: WidgetId::CpuUsage,
254            enabled: false,
255            section: StatusBarSection::Right,
256            order: 0,
257            format: None,
258        },
259        StatusBarWidgetConfig {
260            id: WidgetId::MemoryUsage,
261            enabled: false,
262            section: StatusBarSection::Right,
263            order: 1,
264            format: None,
265        },
266        StatusBarWidgetConfig {
267            id: WidgetId::NetworkStatus,
268            enabled: false,
269            section: StatusBarSection::Right,
270            order: 2,
271            format: None,
272        },
273        StatusBarWidgetConfig {
274            id: WidgetId::DiskFree,
275            enabled: false,
276            section: StatusBarSection::Right,
277            order: 3,
278            format: None,
279        },
280        StatusBarWidgetConfig {
281            id: WidgetId::BellIndicator,
282            enabled: true,
283            section: StatusBarSection::Right,
284            order: 4,
285            format: None,
286        },
287        StatusBarWidgetConfig {
288            id: WidgetId::Clock,
289            enabled: true,
290            section: StatusBarSection::Right,
291            order: 5,
292            format: None,
293        },
294        StatusBarWidgetConfig {
295            id: WidgetId::UpdateAvailable,
296            enabled: true,
297            section: StatusBarSection::Right,
298            order: 6,
299            format: None,
300        },
301        StatusBarWidgetConfig {
302            id: WidgetId::AgentUsage,
303            enabled: false,
304            section: StatusBarSection::Right,
305            order: 7,
306            format: None,
307        },
308        StatusBarWidgetConfig {
309            id: WidgetId::AgentRoster,
310            enabled: false,
311            section: StatusBarSection::Right,
312            order: 8,
313            format: None,
314        },
315    ]
316}