romm_cli/tui/screens/settings/
state.rs1use std::collections::HashMap;
2
3use crate::config::{
4 disk_has_unresolved_keyring_sentinel, Config, RomsLayoutConfig, SaveSyncConfig,
5};
6use crate::feature_compat::SaveSyncCompatibility;
7use crate::tui::path_picker::{PathPicker, PathPickerMode};
8
9use super::types::{
10 ConsolePathKind, SettingsConfirm, SettingsPickerKind, SettingsRow, SettingsScreen, SettingsTab,
11 APPEARANCE_ROWS, AUTH_MAINT_ROWS, CONNECTION_ROWS, EXTRAS_ROWS, SAVES_ROWS,
12};
13use crate::tui::theme::{next_theme_id, prev_theme_id, theme_display_name, MessageTone};
14
15impl SettingsScreen {
16 pub fn new(
17 config: &Config,
18 romm_server_version: Option<&str>,
19 save_sync_compat: SaveSyncCompatibility,
20 ) -> Self {
21 let auth_status = match &config.auth {
22 Some(crate::config::AuthConfig::Basic { username, .. }) => {
23 format!("Basic (user: {})", username)
24 }
25 Some(crate::config::AuthConfig::Bearer { .. }) => "API Token".to_string(),
26 Some(crate::config::AuthConfig::ApiKey { header, .. }) => {
27 format!("API key (header: {})", header)
28 }
29 None => {
30 if disk_has_unresolved_keyring_sentinel(config) {
31 "None — disk still references keyring; set API_TOKEN / ROMM_TOKEN_FILE or see docs/troubleshooting-auth.md"
32 .to_string()
33 } else {
34 "None (no API credentials in env/keyring)".to_string()
35 }
36 }
37 };
38
39 let server_version = romm_server_version
40 .map(String::from)
41 .unwrap_or_else(|| "unavailable (heartbeat failed)".to_string());
42
43 Self {
44 base_url: config.base_url.clone(),
45 download_dir: config.download_dir.clone(),
46 save_dir: crate::config::resolved_save_dir(config)
47 .display()
48 .to_string(),
49 sync_device_id: config.save_sync.device_id.clone(),
50 use_https: config.use_https,
51 extras_include_related_roms: config.extras_defaults.include_related_roms,
52 extras_include_cover: config.extras_defaults.include_cover,
53 extras_include_manual: config.extras_defaults.include_manual,
54 auth_status,
55 version: env!("CARGO_PKG_VERSION").to_string(),
56 server_version,
57 github_url: "https://github.com/patricksmill/romm-cli".to_string(),
58 theme_id: config.theme.clone(),
59 selected_tab: SettingsTab::Connection,
60 selected_indices: [0; SettingsTab::COUNT],
61 editing: false,
62 confirm: None,
63 edit_buffer: String::new(),
64 edit_cursor: 0,
65 path_picker: None,
66 devices: Vec::new(),
67 device_picker_open: false,
68 device_picker_loading: false,
69 device_picker_error: None,
70 device_selected_index: 0,
71 sync_inflight: false,
72 message: None,
73 save_sync_compat,
74 rom_platform_dirs: config.roms_layout.platform_dirs.clone(),
75 save_platform_dirs: config.save_sync.platform_dirs.clone(),
76 console_picker_open: false,
77 active_console_kind: None,
78 console_picker_loading: false,
79 console_picker_error: None,
80 console_platforms: Vec::new(),
81 console_selected_index: 0,
82 console_path_picker: None,
83 }
84 }
85
86 pub fn roms_layout_config(&self) -> RomsLayoutConfig {
87 let mut layout = RomsLayoutConfig::default();
88 layout.platform_dirs = self.rom_platform_dirs.clone();
89 layout
90 }
91
92 pub fn save_sync_config(&self) -> SaveSyncConfig {
93 SaveSyncConfig {
94 save_dir: Some(self.save_dir.clone()),
95 device_id: self.sync_device_id.clone(),
96 platform_dirs: self.save_platform_dirs.clone(),
97 }
98 }
99
100 pub fn has_unsaved_changes(&self, saved: &Config) -> bool {
102 if self.base_url != saved.base_url {
103 return true;
104 }
105 if self.download_dir != saved.download_dir {
106 return true;
107 }
108 if self.use_https != saved.use_https {
109 return true;
110 }
111 if self.extras_include_related_roms != saved.extras_defaults.include_related_roms {
112 return true;
113 }
114 if self.extras_include_cover != saved.extras_defaults.include_cover {
115 return true;
116 }
117 if self.extras_include_manual != saved.extras_defaults.include_manual {
118 return true;
119 }
120 if self.theme_id != saved.theme {
121 return true;
122 }
123 if self.roms_layout_config() != saved.roms_layout {
124 return true;
125 }
126 if self.sync_device_id != saved.save_sync.device_id {
127 return true;
128 }
129 if self.save_platform_dirs != saved.save_sync.platform_dirs {
130 return true;
131 }
132 let saved_save_dir = crate::config::resolved_save_dir(saved)
133 .display()
134 .to_string();
135 self.save_dir != saved_save_dir
136 }
137
138 pub(crate) fn console_dirs(&self, kind: ConsolePathKind) -> &HashMap<u64, String> {
139 match kind {
140 ConsolePathKind::Roms => &self.rom_platform_dirs,
141 ConsolePathKind::Saves => &self.save_platform_dirs,
142 }
143 }
144
145 pub(crate) fn console_dirs_mut(&mut self, kind: ConsolePathKind) -> &mut HashMap<u64, String> {
146 match kind {
147 ConsolePathKind::Roms => &mut self.rom_platform_dirs,
148 ConsolePathKind::Saves => &mut self.save_platform_dirs,
149 }
150 }
151
152 pub fn visible_rows(&self) -> Vec<SettingsRow> {
153 match self.selected_tab {
154 SettingsTab::Connection => CONNECTION_ROWS.to_vec(),
155 SettingsTab::Roms => vec![SettingsRow::RomsDir, SettingsRow::ConsolePaths],
156 SettingsTab::Saves => SAVES_ROWS.to_vec(),
157 SettingsTab::Extras => EXTRAS_ROWS.to_vec(),
158 SettingsTab::Appearance => APPEARANCE_ROWS.to_vec(),
159 SettingsTab::AuthMaintenance => AUTH_MAINT_ROWS.to_vec(),
160 }
161 }
162
163 pub fn save_sync_supported(&self) -> bool {
164 self.save_sync_compat.supported
165 }
166
167 pub fn set_save_sync_unsupported_message(&mut self) {
168 self.message = Some((
169 self.save_sync_compat.unsupported_message(),
170 MessageTone::Warning,
171 ));
172 }
173
174 pub fn selected_row_index(&self) -> usize {
175 let rows = self.visible_rows();
176 self.selected_indices[self.selected_tab.index()].min(rows.len().saturating_sub(1))
177 }
178
179 fn set_selected_row_index(&mut self, index: usize) {
180 let max = self.visible_rows().len().saturating_sub(1);
181 self.selected_indices[self.selected_tab.index()] = index.min(max);
182 }
183
184 pub fn selected_row(&self) -> SettingsRow {
185 let rows = self.visible_rows();
186 rows[self.selected_row_index()]
187 }
188
189 pub fn active_rows(&self) -> &[SettingsRow] {
190 match self.selected_tab {
192 SettingsTab::Connection => &CONNECTION_ROWS,
193 SettingsTab::Saves => &SAVES_ROWS,
194 SettingsTab::Extras => &EXTRAS_ROWS,
195 SettingsTab::Appearance => &APPEARANCE_ROWS,
196 SettingsTab::AuthMaintenance => &AUTH_MAINT_ROWS,
197 SettingsTab::Roms => &[],
198 }
199 }
200
201 pub fn cycle_theme_next(&mut self) {
202 self.theme_id = next_theme_id(&self.theme_id);
203 }
204
205 pub fn cycle_theme_prev(&mut self) {
206 self.theme_id = prev_theme_id(&self.theme_id);
207 }
208
209 pub fn theme_display_name(&self) -> String {
210 theme_display_name(&self.theme_id)
211 }
212
213 pub fn next_tab(&mut self) {
214 if self.editing || self.confirm.is_some() {
215 return;
216 }
217 let next = (self.selected_tab.index() + 1) % SettingsTab::COUNT;
218 self.selected_tab = SettingsTab::ALL[next];
219 self.set_selected_row_index(self.selected_row_index());
220 }
221
222 pub fn previous_tab(&mut self) {
223 if self.editing || self.confirm.is_some() {
224 return;
225 }
226 let previous = (self.selected_tab.index() + SettingsTab::COUNT - 1) % SettingsTab::COUNT;
227 self.selected_tab = SettingsTab::ALL[previous];
228 self.set_selected_row_index(self.selected_row_index());
229 }
230
231 pub fn next(&mut self) {
232 if !self.editing && self.confirm.is_none() {
233 let len = self.visible_rows().len();
234 if len > 0 {
235 self.set_selected_row_index((self.selected_row_index() + 1) % len);
236 }
237 }
238 }
239
240 pub fn previous(&mut self) {
241 if !self.editing && self.confirm.is_none() {
242 let len = self.visible_rows().len();
243 if len == 0 {
244 return;
245 }
246 if self.selected_row_index() == 0 {
247 self.set_selected_row_index(len - 1);
248 } else {
249 self.set_selected_row_index(self.selected_row_index() - 1);
250 }
251 }
252 }
253
254 pub fn enter_edit(&mut self) {
255 match self.selected_row() {
256 SettingsRow::ResetConfiguration => self.confirm = Some(SettingsConfirm::Reset),
257 SettingsRow::ClearCache => self.confirm = Some(SettingsConfirm::ClearCache),
258 SettingsRow::SyncDevice => {
259 if !self.save_sync_supported() {
260 self.set_save_sync_unsupported_message();
261 return;
262 }
263 self.device_picker_open = true;
264 self.device_picker_loading = true;
265 self.device_picker_error = None;
266 self.message = Some(("Loading devices...".to_string(), MessageTone::Warning));
267 }
268 SettingsRow::SyncNow => {
269 if !self.save_sync_supported() {
270 self.set_save_sync_unsupported_message();
271 return;
272 }
273 self.message = Some(("Starting save sync...".to_string(), MessageTone::Warning));
274 }
275 SettingsRow::ExtrasManual => {
276 self.extras_include_manual = !self.extras_include_manual;
277 self.message = Some((
278 format!(
279 "Extras default (manual): {}",
280 if self.extras_include_manual {
281 "on"
282 } else {
283 "off"
284 }
285 ),
286 MessageTone::Success,
287 ));
288 }
289 SettingsRow::ExtrasCover => {
290 self.extras_include_cover = !self.extras_include_cover;
291 self.message = Some((
292 format!(
293 "Extras default (cover): {}",
294 if self.extras_include_cover {
295 "on"
296 } else {
297 "off"
298 }
299 ),
300 MessageTone::Success,
301 ));
302 }
303 SettingsRow::ExtrasRelatedRoms => {
304 self.extras_include_related_roms = !self.extras_include_related_roms;
305 self.message = Some((
306 format!(
307 "Extras default (updates/DLC): {}",
308 if self.extras_include_related_roms {
309 "on"
310 } else {
311 "off"
312 }
313 ),
314 MessageTone::Success,
315 ));
316 }
317 SettingsRow::UseHttps => {
318 self.use_https = !self.use_https;
320 if self.use_https && self.base_url.starts_with("http://") {
321 self.base_url = self.base_url.replace("http://", "https://");
322 self.message = Some((
323 "Updated URL scheme (HTTPS)".to_string(),
324 MessageTone::Success,
325 ));
326 } else if !self.use_https && self.base_url.starts_with("https://") {
327 self.base_url = self.base_url.replace("https://", "http://");
328 self.message = Some((
329 "Updated URL scheme (HTTP)".to_string(),
330 MessageTone::Success,
331 ));
332 }
333 }
334 SettingsRow::RomsDir => {
335 self.path_picker = Some((
336 SettingsPickerKind::RomsDir,
337 PathPicker::new(PathPickerMode::Directory, self.download_dir.as_str()),
338 ));
339 }
340 SettingsRow::ConsolePaths | SettingsRow::SaveConsolePaths => {}
341 SettingsRow::SaveDir => {
342 self.path_picker = Some((
343 SettingsPickerKind::SaveDir,
344 PathPicker::new(PathPickerMode::Directory, self.save_dir.as_str()),
345 ));
346 }
347 SettingsRow::BaseUrl => {
348 self.editing = true;
349 self.edit_buffer = self.base_url.clone();
350 self.edit_cursor = self.edit_buffer.len();
351 }
352 SettingsRow::Theme => {}
353 SettingsRow::Auth => {}
354 }
355 }
356
357 pub fn save_edit(&mut self) -> bool {
358 if !self.editing {
359 return true; }
361 if self.selected_row() == SettingsRow::BaseUrl {
362 self.base_url = self.edit_buffer.trim().to_string();
363 }
364 self.editing = false;
365 true
366 }
367
368 pub fn cancel_edit(&mut self) {
369 self.editing = false;
370 self.confirm = None;
371 self.path_picker = None;
372 self.console_path_picker = None;
373 self.console_picker_open = false;
374 self.active_console_kind = None;
375 self.message = None;
376 }
377
378 pub fn add_char(&mut self, c: char) {
379 if self.editing {
380 self.edit_buffer.insert(self.edit_cursor, c);
381 self.edit_cursor += 1;
382 }
383 }
384
385 pub fn delete_char(&mut self) {
386 if self.editing && self.edit_cursor > 0 {
387 self.edit_buffer.remove(self.edit_cursor - 1);
388 self.edit_cursor -= 1;
389 }
390 }
391
392 pub fn move_cursor_left(&mut self) {
393 if self.editing && self.edit_cursor > 0 {
394 self.edit_cursor -= 1;
395 }
396 }
397
398 pub fn move_cursor_right(&mut self) {
399 if self.editing && self.edit_cursor < self.edit_buffer.len() {
400 self.edit_cursor += 1;
401 }
402 }
403}