Skip to main content

studio_worker/ui/
tab.rs

1//! The tabs the UI exposes.  Pure data + tiny enum impl so the
2//! contract is testable without egui in scope.
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
5pub enum Tab {
6    #[default]
7    Status,
8    Jobs,
9    Models,
10    Config,
11    Logs,
12    About,
13}
14
15impl Tab {
16    pub const ALL: [Tab; 6] = [
17        Tab::Status,
18        Tab::Jobs,
19        Tab::Models,
20        Tab::Config,
21        Tab::Logs,
22        Tab::About,
23    ];
24
25    pub fn label(self) -> &'static str {
26        match self {
27            Tab::Status => "Status",
28            Tab::Jobs => "Jobs",
29            Tab::Models => "Models",
30            Tab::Config => "Config",
31            Tab::Logs => "Logs",
32            Tab::About => "About",
33        }
34    }
35
36    /// Parse a tab name (case-insensitive).  Used by the
37    /// `STUDIO_WORKER_UI_TAB` debug env var to seed the initial tab
38    /// during screenshot capture and headless UI inspection.
39    pub fn parse(name: &str) -> Option<Self> {
40        match name.trim().to_ascii_lowercase().as_str() {
41            "status" => Some(Self::Status),
42            "jobs" => Some(Self::Jobs),
43            "models" => Some(Self::Models),
44            "config" => Some(Self::Config),
45            "logs" => Some(Self::Logs),
46            "about" => Some(Self::About),
47            _ => None,
48        }
49    }
50
51    /// Resolve the initial tab on app launch: env override or default.
52    pub fn initial() -> Self {
53        std::env::var("STUDIO_WORKER_UI_TAB")
54            .ok()
55            .and_then(|s| Self::parse(&s))
56            .unwrap_or_default()
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn all_returns_every_tab_in_render_order() {
66        let labels: Vec<&str> = Tab::ALL.iter().map(|t| t.label()).collect();
67        assert_eq!(
68            labels,
69            ["Status", "Jobs", "Models", "Config", "Logs", "About"],
70            "tab labels + order are part of the UI contract"
71        );
72    }
73
74    #[test]
75    fn default_is_status() {
76        assert_eq!(Tab::default(), Tab::Status);
77    }
78
79    #[test]
80    fn parse_round_trips_with_label_case_insensitively() {
81        for tab in Tab::ALL {
82            assert_eq!(Tab::parse(tab.label()), Some(tab));
83            assert_eq!(Tab::parse(&tab.label().to_uppercase()), Some(tab));
84        }
85        assert!(Tab::parse("").is_none());
86        assert!(Tab::parse("nope").is_none());
87    }
88
89    #[test]
90    fn initial_falls_back_to_default_when_env_unset() {
91        // SAFETY: the Rust test harness runs tests concurrently, so this
92        // mutates the process-global STUDIO_WORKER_UI_TAB. Safe because
93        // this is the only test that touches that var; we snapshot it and
94        // restore it afterwards. (A panic here would skip the restore, but
95        // it also fails the run and no other test reads the var.)
96        let prev = std::env::var("STUDIO_WORKER_UI_TAB").ok();
97        std::env::remove_var("STUDIO_WORKER_UI_TAB");
98        assert_eq!(Tab::initial(), Tab::default());
99        if let Some(v) = prev {
100            std::env::set_var("STUDIO_WORKER_UI_TAB", v);
101        }
102    }
103
104    #[test]
105    fn labels_are_unique() {
106        use std::collections::HashSet;
107        let unique: HashSet<&str> = Tab::ALL.iter().map(|t| t.label()).collect();
108        assert_eq!(unique.len(), Tab::ALL.len());
109    }
110}