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