Skip to main content

holodeck_simctl_tui/state/
palette_command.rs

1use holodeck_core::models::{Simulator, SimulatorState};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum PaletteCommand {
5    Appearance,
6    Boot,
7    Delete,
8    Erase,
9    Focus,
10    Inspect,
11    New,
12    Open,
13    Privacy,
14    Record,
15    Screenshot,
16    Shutdown,
17}
18
19impl PaletteCommand {
20    const CASES: [PaletteCommand; 12] = [
21        PaletteCommand::Appearance,
22        PaletteCommand::Boot,
23        PaletteCommand::Delete,
24        PaletteCommand::Erase,
25        PaletteCommand::Focus,
26        PaletteCommand::Inspect,
27        PaletteCommand::New,
28        PaletteCommand::Open,
29        PaletteCommand::Privacy,
30        PaletteCommand::Record,
31        PaletteCommand::Screenshot,
32        PaletteCommand::Shutdown,
33    ];
34
35    /// Alphabetical ordering, used for deterministic ghost-autocomplete
36    /// matching. All display names happen to already sort this way, so this
37    /// is just `CASES` — kept as a function to mirror the Swift `all` and to
38    /// stay correct if a display name ever changes.
39    pub fn all() -> Vec<PaletteCommand> {
40        let mut all = Self::CASES.to_vec();
41        all.sort_by_key(|c| c.display_name());
42        all
43    }
44
45    pub fn display_name(self) -> &'static str {
46        match self {
47            PaletteCommand::Appearance => "appearance",
48            PaletteCommand::Boot => "boot",
49            PaletteCommand::Delete => "delete",
50            PaletteCommand::Erase => "erase",
51            PaletteCommand::Focus => "focus",
52            PaletteCommand::Inspect => "inspect",
53            PaletteCommand::New => "new",
54            PaletteCommand::Open => "open",
55            PaletteCommand::Privacy => "privacy",
56            PaletteCommand::Record => "record",
57            PaletteCommand::Screenshot => "screenshot",
58            PaletteCommand::Shutdown => "shutdown",
59        }
60    }
61
62    pub fn description(self) -> &'static str {
63        match self {
64            PaletteCommand::Appearance => "Switch the booted simulator between light and dark",
65            PaletteCommand::Boot => "Boot the selected simulator",
66            PaletteCommand::Delete => "Delete the selected simulator",
67            PaletteCommand::Erase => "Erase the selected (shutdown) simulator",
68            PaletteCommand::Focus => "Bring Simulator.app to the front for the selection",
69            PaletteCommand::Inspect => "Open the inspector for the selected simulator",
70            PaletteCommand::New => "Create a new simulator (wizard)",
71            PaletteCommand::Open => "Open a URL or deep link on the booted simulator",
72            PaletteCommand::Privacy => "Grant or revoke privacy permissions for an app",
73            PaletteCommand::Record => "Start screen recording on the booted simulator",
74            PaletteCommand::Screenshot => "Capture a screenshot of the booted simulator",
75            PaletteCommand::Shutdown => "Shut down the selected simulator",
76        }
77    }
78
79    pub fn is_applicable(self, simulator: Option<&Simulator>, is_recording: bool) -> bool {
80        match self {
81            PaletteCommand::New => true,
82            PaletteCommand::Focus | PaletteCommand::Inspect | PaletteCommand::Delete => simulator.is_some(),
83            PaletteCommand::Boot => simulator.is_some_and(|s| s.state == SimulatorState::Shutdown),
84            PaletteCommand::Shutdown => simulator.is_some_and(|s| s.state == SimulatorState::Booted),
85            PaletteCommand::Erase => simulator.is_some_and(|s| s.state == SimulatorState::Shutdown),
86            PaletteCommand::Record => simulator.is_some_and(|s| s.state == SimulatorState::Booted) && !is_recording,
87            PaletteCommand::Screenshot | PaletteCommand::Appearance | PaletteCommand::Open | PaletteCommand::Privacy => {
88                simulator.is_some_and(|s| s.state == SimulatorState::Booted)
89            }
90        }
91    }
92
93    /// Case-insensitive prefix match. An empty prefix matches every command.
94    pub fn matches(self, prefix: &str) -> bool {
95        if prefix.is_empty() {
96            return true;
97        }
98        self.display_name().to_lowercase().starts_with(&prefix.to_lowercase())
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn all_is_sorted_alphabetically_by_display_name() {
108        let names: Vec<&str> = PaletteCommand::all().into_iter().map(PaletteCommand::display_name).collect();
109        let mut sorted = names.clone();
110        sorted.sort();
111        assert_eq!(names, sorted);
112    }
113
114    #[test]
115    fn empty_prefix_matches_everything() {
116        assert!(PaletteCommand::Boot.matches(""));
117    }
118
119    #[test]
120    fn prefix_match_is_case_insensitive() {
121        assert!(PaletteCommand::Boot.matches("BO"));
122        assert!(!PaletteCommand::Boot.matches("xyz"));
123    }
124}