1use holodeck_core::models::{DeviceType, InstalledApp, LanguageOption, 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 LaunchApp(LaunchAppPrompt),
29 Inspector(Uuid),
30 OpenUrl(OpenUrlPrompt),
31 CommandPalette(CommandPalette),
32 Help,
33}
34
35impl Modal {
36 pub fn referenced_simulator(&self) -> Option<Uuid> {
40 match self {
41 Modal::ConfirmErase(id, _) | Modal::ConfirmDelete(id, _) | Modal::Inspector(id) => Some(*id),
42 Modal::OpenUrl(prompt) => Some(prompt.simulator_id),
43 Modal::LaunchApp(prompt) => Some(prompt.simulator_id),
44 Modal::CommandPalette(palette) => palette.simulator_id,
45 Modal::Appearance(_) | Modal::CreateWizard(_) | Modal::PrivacyWizard(_) | Modal::Help => None,
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Default)]
51pub struct CommandPalette {
52 pub simulator_id: Option<Uuid>,
57 pub query: String,
58}
59
60#[derive(Debug, Clone, PartialEq)]
61pub struct OpenUrlPrompt {
62 pub simulator_id: Uuid,
63 pub url: String,
64 pub history_index: i64,
65 pub is_submitting: bool,
66 pub error: Option<String>,
67}
68
69impl OpenUrlPrompt {
70 pub fn new(simulator_id: Uuid) -> Self {
71 Self { simulator_id, url: String::new(), history_index: -1, is_submitting: false, error: None }
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum PrivacyWizardStep {
77 LoadingApps,
78 PickApp,
79 PickAction,
80 PickPermission,
81 Submitting,
82}
83
84#[derive(Debug, Clone, PartialEq)]
85pub struct PrivacyWizard {
86 pub simulator_id: Uuid,
87 pub step: PrivacyWizardStep,
88 pub all_apps: Vec<InstalledApp>,
89 pub app_index: i64,
90 pub app_scroll_offset: i64,
91 pub action_index: i64,
92 pub permission_index: i64,
93 pub show_system: bool,
94 pub error: Option<String>,
95}
96
97impl PrivacyWizard {
98 pub fn new(simulator_id: Uuid) -> Self {
99 Self {
100 simulator_id,
101 step: PrivacyWizardStep::LoadingApps,
102 all_apps: Vec::new(),
103 app_index: 0,
104 app_scroll_offset: 0,
105 action_index: 0,
106 permission_index: 0,
107 show_system: false,
108 error: None,
109 }
110 }
111
112 pub fn app_viewport(rows: i64) -> i64 {
115 (rows - 5).max(3)
116 }
117
118 pub fn apps(&self) -> Vec<&InstalledApp> {
119 self.all_apps.iter().filter(|app| self.show_system || app.is_user_app).collect()
120 }
121
122 pub fn selected_app(&self) -> Option<&InstalledApp> {
123 let list = self.apps();
124 usize::try_from(self.app_index).ok().and_then(|i| list.get(i).copied())
125 }
126
127 pub fn selected_action(&self) -> Option<PrivacyAction> {
128 usize::try_from(self.action_index).ok().and_then(|i| PrivacyAction::ALL.get(i).copied())
129 }
130
131 pub fn selected_permission(&self) -> Option<PrivacyPermission> {
132 usize::try_from(self.permission_index).ok().and_then(|i| PrivacyPermission::ALL.get(i).copied())
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum LaunchAppStep {
138 LoadingApps,
139 PickApp,
140 PickLanguage,
141 Submitting,
142}
143
144#[derive(Debug, Clone, PartialEq)]
145pub struct LaunchAppPrompt {
146 pub simulator_id: Uuid,
147 pub step: LaunchAppStep,
148 pub all_apps: Vec<InstalledApp>,
149 pub app_index: i64,
150 pub app_scroll_offset: i64,
151 pub show_system: bool,
152 pub language_index: i64,
153 pub language_scroll_offset: i64,
154 pub language_filter: String,
155 pub is_language_filter_focused: bool,
156 pub error: Option<String>,
157}
158
159impl LaunchAppPrompt {
160 pub fn new(simulator_id: Uuid) -> Self {
161 Self {
162 simulator_id,
163 step: LaunchAppStep::LoadingApps,
164 all_apps: Vec::new(),
165 app_index: 0,
166 app_scroll_offset: 0,
167 show_system: false,
168 language_index: 0,
169 language_scroll_offset: 0,
170 language_filter: String::new(),
171 is_language_filter_focused: false,
172 error: None,
173 }
174 }
175
176 pub fn app_viewport(rows: i64) -> i64 {
177 (rows - 5).max(3)
178 }
179
180 pub fn language_viewport(&self, rows: i64) -> i64 {
185 let banner = i64::from(self.is_language_filter_focused || !self.language_filter.is_empty());
186 (Self::app_viewport(rows) - banner).max(1)
187 }
188
189 pub fn apps(&self) -> Vec<&InstalledApp> {
190 self.all_apps.iter().filter(|app| self.show_system || app.is_user_app).collect()
191 }
192
193 pub fn selected_app(&self) -> Option<&InstalledApp> {
194 let list = self.apps();
195 usize::try_from(self.app_index).ok().and_then(|i| list.get(i).copied())
196 }
197
198 pub fn visible_languages(&self) -> Vec<&'static LanguageOption> {
199 if self.language_filter.is_empty() {
200 return LanguageOption::ALL.iter().collect();
201 }
202 let needle = self.language_filter.to_lowercase();
203 LanguageOption::ALL.iter().filter(|l| l.display_name.to_lowercase().contains(&needle)).collect()
204 }
205
206 pub fn selected_language(&self) -> Option<&'static LanguageOption> {
207 let list = self.visible_languages();
208 usize::try_from(self.language_index).ok().and_then(|i| list.get(i).copied())
209 }
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
213pub enum CreateWizardStep {
214 #[default]
215 Loading,
216 PickDeviceType,
217 PickRuntime,
218 Confirm,
219 Submitting,
220}
221
222#[derive(Debug, Clone, PartialEq, Default)]
223pub struct CreateWizard {
224 pub step: CreateWizardStep,
225 pub device_types: Vec<DeviceType>,
226 pub runtimes: Vec<Runtime>,
227 pub device_type_index: i64,
228 pub device_type_scroll_offset: i64,
229 pub runtime_index: i64,
230 pub runtime_scroll_offset: i64,
231 pub device_type_filter: String,
232 pub is_device_type_filter_focused: bool,
233 pub error: Option<String>,
234}
235
236impl CreateWizard {
237 pub fn new() -> Self {
238 Self::default()
239 }
240
241 pub fn viewport(rows: i64) -> i64 {
242 (rows - 5).max(3)
243 }
244
245 pub fn device_type_viewport(&self, rows: i64) -> i64 {
249 let banner = if self.is_device_type_filter_focused || !self.device_type_filter.is_empty() { 1 } else { 0 };
250 (Self::viewport(rows) - banner).max(1)
251 }
252
253 pub fn visible_device_types(&self) -> Vec<&DeviceType> {
254 if self.device_type_filter.is_empty() {
255 return self.device_types.iter().collect();
256 }
257 let needle = self.device_type_filter.to_lowercase();
258 self.device_types.iter().filter(|d| d.name.to_lowercase().contains(&needle)).collect()
259 }
260
261 pub fn selected_device_type(&self) -> Option<&DeviceType> {
262 let list = self.visible_device_types();
263 usize::try_from(self.device_type_index).ok().and_then(|i| list.get(i).copied())
264 }
265
266 pub fn selected_runtime(&self) -> Option<&Runtime> {
267 usize::try_from(self.runtime_index).ok().and_then(|i| self.runtimes.get(i))
268 }
269
270 pub fn default_name(&self) -> String {
271 match (self.selected_device_type(), self.selected_runtime()) {
272 (Some(device_type), Some(runtime)) => format!("{} ({})", device_type.name, runtime.display_name()),
273 _ => "Simulator".to_string(),
274 }
275 }
276}
277
278#[derive(Debug, Clone, PartialEq)]
279pub struct AppState {
280 pub simulators: Vec<Simulator>,
281 pub selected_index: i64,
282 pub main_scroll_offset: i64,
283 pub filter_query: String,
284 pub is_filter_focused: bool,
285 pub status_message: Option<String>,
286 pub last_error: Option<String>,
287 pub pending_operations: std::collections::HashMap<Uuid, PendingOperation>,
288 pub is_quitting: bool,
289 pub rows: i64,
290 pub cols: i64,
291 pub recording_device_id: Option<Uuid>,
292 pub recording_path: Option<std::path::PathBuf>,
293 pub modal: Option<Modal>,
294 pub url_history: Vec<String>,
295}
296
297impl Default for AppState {
298 fn default() -> Self {
299 Self {
300 simulators: Vec::new(),
301 selected_index: 0,
302 main_scroll_offset: 0,
303 filter_query: String::new(),
304 is_filter_focused: false,
305 status_message: None,
306 last_error: None,
307 pending_operations: std::collections::HashMap::new(),
308 is_quitting: false,
309 rows: 24,
310 cols: 80,
311 recording_device_id: None,
312 recording_path: None,
313 modal: None,
314 url_history: Vec::new(),
315 }
316 }
317}
318
319impl AppState {
320 pub fn is_recording(&self) -> bool {
321 self.recording_device_id.is_some()
322 }
323
324 pub fn visible_simulators(&self) -> Vec<&Simulator> {
325 if self.filter_query.is_empty() {
326 return self.simulators.iter().collect();
327 }
328 let needle = self.filter_query.to_lowercase();
329 self.simulators.iter().filter(|sim| sim.name.to_lowercase().contains(&needle)).collect()
330 }
331
332 pub fn selected_simulator(&self) -> Option<&Simulator> {
333 if self.filter_query.is_empty() {
336 return usize::try_from(self.selected_index).ok().and_then(|i| self.simulators.get(i));
337 }
338 let list = self.visible_simulators();
339 usize::try_from(self.selected_index).ok().and_then(|i| list.get(i).copied())
340 }
341
342 pub fn main_list_viewport(&self) -> i64 {
351 let banner = i64::from(self.is_recording());
352 (self.rows - 4 - banner - 2).max(1)
353 }
354
355 pub fn scroll(offset: i64, index: i64, viewport: i64) -> i64 {
358 if index < offset {
359 return index;
360 }
361 if index >= offset + viewport {
362 return index - viewport + 1;
363 }
364 offset
365 }
366
367 pub fn sort(mut simulators: Vec<Simulator>) -> Vec<Simulator> {
368 simulators.sort_by(|lhs, rhs| rhs.runtime.cmp(&lhs.runtime).then_with(|| lhs.name.cmp(&rhs.name)));
369 simulators
370 }
371}