holodeck_simctl_tui/state/
app_state.rs1use holodeck_core::models::{DeviceType, InstalledApp, PrivacyAction, PrivacyPermission, Runtime, Simulator};
2use uuid::Uuid;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum PendingOperation {
12 Boot,
13 Shutdown,
14 Erase,
15 Delete,
16}
17
18#[derive(Debug, Clone, PartialEq)]
19pub enum Modal {
20 Appearance(i64),
22 ConfirmErase(Uuid, i64),
24 ConfirmDelete(Uuid, i64),
26 CreateWizard(CreateWizard),
27 PrivacyWizard(PrivacyWizard),
28 Inspector(Uuid),
29 OpenUrl(OpenUrlPrompt),
30 CommandPalette(CommandPalette),
31 Help,
32}
33
34impl Modal {
35 pub fn referenced_simulator(&self) -> Option<Uuid> {
39 match self {
40 Modal::ConfirmErase(id, _) | Modal::ConfirmDelete(id, _) | Modal::Inspector(id) => Some(*id),
41 Modal::OpenUrl(prompt) => Some(prompt.simulator_id),
42 Modal::CommandPalette(palette) => palette.simulator_id,
43 Modal::Appearance(_) | Modal::CreateWizard(_) | Modal::PrivacyWizard(_) | Modal::Help => None,
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Default)]
49pub struct CommandPalette {
50 pub simulator_id: Option<Uuid>,
55 pub query: String,
56}
57
58#[derive(Debug, Clone, PartialEq)]
59pub struct OpenUrlPrompt {
60 pub simulator_id: Uuid,
61 pub url: String,
62 pub history_index: i64,
63 pub is_submitting: bool,
64 pub error: Option<String>,
65}
66
67impl OpenUrlPrompt {
68 pub fn new(simulator_id: Uuid) -> Self {
69 Self { simulator_id, url: String::new(), history_index: -1, is_submitting: false, error: None }
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum PrivacyWizardStep {
75 LoadingApps,
76 PickApp,
77 PickAction,
78 PickPermission,
79 Submitting,
80}
81
82#[derive(Debug, Clone, PartialEq)]
83pub struct PrivacyWizard {
84 pub simulator_id: Uuid,
85 pub step: PrivacyWizardStep,
86 pub all_apps: Vec<InstalledApp>,
87 pub app_index: i64,
88 pub app_scroll_offset: i64,
89 pub action_index: i64,
90 pub permission_index: i64,
91 pub show_system: bool,
92 pub error: Option<String>,
93}
94
95impl PrivacyWizard {
96 pub fn new(simulator_id: Uuid) -> Self {
97 Self {
98 simulator_id,
99 step: PrivacyWizardStep::LoadingApps,
100 all_apps: Vec::new(),
101 app_index: 0,
102 app_scroll_offset: 0,
103 action_index: 0,
104 permission_index: 0,
105 show_system: false,
106 error: None,
107 }
108 }
109
110 pub fn app_viewport(rows: i64) -> i64 {
113 (rows - 5).max(3)
114 }
115
116 pub fn apps(&self) -> Vec<&InstalledApp> {
117 self.all_apps.iter().filter(|app| self.show_system || app.is_user_app).collect()
118 }
119
120 pub fn selected_app(&self) -> Option<&InstalledApp> {
121 let list = self.apps();
122 usize::try_from(self.app_index).ok().and_then(|i| list.get(i).copied())
123 }
124
125 pub fn selected_action(&self) -> Option<PrivacyAction> {
126 usize::try_from(self.action_index).ok().and_then(|i| PrivacyAction::ALL.get(i).copied())
127 }
128
129 pub fn selected_permission(&self) -> Option<PrivacyPermission> {
130 usize::try_from(self.permission_index).ok().and_then(|i| PrivacyPermission::ALL.get(i).copied())
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
135pub enum CreateWizardStep {
136 #[default]
137 Loading,
138 PickDeviceType,
139 PickRuntime,
140 Confirm,
141 Submitting,
142}
143
144#[derive(Debug, Clone, PartialEq, Default)]
145pub struct CreateWizard {
146 pub step: CreateWizardStep,
147 pub device_types: Vec<DeviceType>,
148 pub runtimes: Vec<Runtime>,
149 pub device_type_index: i64,
150 pub device_type_scroll_offset: i64,
151 pub runtime_index: i64,
152 pub runtime_scroll_offset: i64,
153 pub device_type_filter: String,
154 pub is_device_type_filter_focused: bool,
155 pub error: Option<String>,
156}
157
158impl CreateWizard {
159 pub fn new() -> Self {
160 Self::default()
161 }
162
163 pub fn viewport(rows: i64) -> i64 {
164 (rows - 5).max(3)
165 }
166
167 pub fn device_type_viewport(&self, rows: i64) -> i64 {
171 let banner = if self.is_device_type_filter_focused || !self.device_type_filter.is_empty() { 1 } else { 0 };
172 (Self::viewport(rows) - banner).max(1)
173 }
174
175 pub fn visible_device_types(&self) -> Vec<&DeviceType> {
176 if self.device_type_filter.is_empty() {
177 return self.device_types.iter().collect();
178 }
179 let needle = self.device_type_filter.to_lowercase();
180 self.device_types.iter().filter(|d| d.name.to_lowercase().contains(&needle)).collect()
181 }
182
183 pub fn selected_device_type(&self) -> Option<&DeviceType> {
184 let list = self.visible_device_types();
185 usize::try_from(self.device_type_index).ok().and_then(|i| list.get(i).copied())
186 }
187
188 pub fn selected_runtime(&self) -> Option<&Runtime> {
189 usize::try_from(self.runtime_index).ok().and_then(|i| self.runtimes.get(i))
190 }
191
192 pub fn default_name(&self) -> String {
193 match (self.selected_device_type(), self.selected_runtime()) {
194 (Some(device_type), Some(runtime)) => format!("{} ({})", device_type.name, runtime.display_name()),
195 _ => "Simulator".to_string(),
196 }
197 }
198}
199
200#[derive(Debug, Clone, PartialEq)]
201pub struct AppState {
202 pub simulators: Vec<Simulator>,
203 pub selected_index: i64,
204 pub main_scroll_offset: i64,
205 pub filter_query: String,
206 pub is_filter_focused: bool,
207 pub status_message: Option<String>,
208 pub last_error: Option<String>,
209 pub pending_operations: std::collections::HashMap<Uuid, PendingOperation>,
210 pub is_quitting: bool,
211 pub rows: i64,
212 pub cols: i64,
213 pub recording_device_id: Option<Uuid>,
214 pub recording_path: Option<std::path::PathBuf>,
215 pub modal: Option<Modal>,
216 pub url_history: Vec<String>,
217}
218
219impl Default for AppState {
220 fn default() -> Self {
221 Self {
222 simulators: Vec::new(),
223 selected_index: 0,
224 main_scroll_offset: 0,
225 filter_query: String::new(),
226 is_filter_focused: false,
227 status_message: None,
228 last_error: None,
229 pending_operations: std::collections::HashMap::new(),
230 is_quitting: false,
231 rows: 24,
232 cols: 80,
233 recording_device_id: None,
234 recording_path: None,
235 modal: None,
236 url_history: Vec::new(),
237 }
238 }
239}
240
241impl AppState {
242 pub fn is_recording(&self) -> bool {
243 self.recording_device_id.is_some()
244 }
245
246 pub fn visible_simulators(&self) -> Vec<&Simulator> {
247 if self.filter_query.is_empty() {
248 return self.simulators.iter().collect();
249 }
250 let needle = self.filter_query.to_lowercase();
251 self.simulators.iter().filter(|sim| sim.name.to_lowercase().contains(&needle)).collect()
252 }
253
254 pub fn selected_simulator(&self) -> Option<&Simulator> {
255 if self.filter_query.is_empty() {
258 return usize::try_from(self.selected_index).ok().and_then(|i| self.simulators.get(i));
259 }
260 let list = self.visible_simulators();
261 usize::try_from(self.selected_index).ok().and_then(|i| list.get(i).copied())
262 }
263
264 pub fn main_list_viewport(&self) -> i64 {
273 let banner = i64::from(self.is_recording());
274 (self.rows - 4 - banner - 2).max(1)
275 }
276
277 pub fn scroll(offset: i64, index: i64, viewport: i64) -> i64 {
280 if index < offset {
281 return index;
282 }
283 if index >= offset + viewport {
284 return index - viewport + 1;
285 }
286 offset
287 }
288
289 pub fn sort(mut simulators: Vec<Simulator>) -> Vec<Simulator> {
290 simulators.sort_by(|lhs, rhs| rhs.runtime.cmp(&lhs.runtime).then_with(|| lhs.name.cmp(&rhs.name)));
291 simulators
292 }
293}