Skip to main content

romm_cli/tui/screens/
settings.rs

1use ratatui::layout::{Constraint, Layout, Rect};
2use ratatui::style::{Color, Modifier, Style};
3use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph};
4use ratatui::Frame;
5
6use crate::config::{disk_has_unresolved_keyring_sentinel, Config};
7use crate::tui::path_picker::{PathPicker, PathPickerMode};
8
9#[derive(PartialEq, Eq)]
10pub enum SettingsField {
11    BaseUrl,
12    DownloadDir,
13    UseHttps,
14}
15
16#[derive(PartialEq, Eq)]
17pub enum SettingsConfirm {
18    Reset,
19    ClearCache,
20}
21
22/// Interactive settings screen for editing current config.
23pub struct SettingsScreen {
24    pub base_url: String,
25    pub download_dir: String,
26    pub use_https: bool,
27    pub auth_status: String,
28    pub version: String,
29    pub server_version: String,
30    pub github_url: String,
31
32    pub selected_index: usize,
33    pub editing: bool,
34    pub confirm: Option<SettingsConfirm>,
35    pub edit_buffer: String,
36    pub edit_cursor: usize,
37    /// ROMs directory browser (`None` when not choosing a folder).
38    pub path_picker: Option<PathPicker>,
39    pub message: Option<(String, Color)>,
40}
41
42impl SettingsScreen {
43    pub fn new(config: &Config, romm_server_version: Option<&str>) -> Self {
44        let auth_status = match &config.auth {
45            Some(crate::config::AuthConfig::Basic { username, .. }) => {
46                format!("Basic (user: {})", username)
47            }
48            Some(crate::config::AuthConfig::Bearer { .. }) => "API Token".to_string(),
49            Some(crate::config::AuthConfig::ApiKey { header, .. }) => {
50                format!("API key (header: {})", header)
51            }
52            None => {
53                if disk_has_unresolved_keyring_sentinel(config) {
54                    "None — disk still references keyring; set API_TOKEN / ROMM_TOKEN_FILE or see docs/troubleshooting-auth.md"
55                        .to_string()
56                } else {
57                    "None (no API credentials in env/keyring)".to_string()
58                }
59            }
60        };
61
62        let server_version = romm_server_version
63            .map(String::from)
64            .unwrap_or_else(|| "unavailable (heartbeat failed)".to_string());
65
66        Self {
67            base_url: config.base_url.clone(),
68            download_dir: config.download_dir.clone(),
69            use_https: config.use_https,
70            auth_status,
71            version: env!("CARGO_PKG_VERSION").to_string(),
72            server_version,
73            github_url: "https://github.com/patricksmill/romm-cli".to_string(),
74            selected_index: 0,
75            editing: false,
76            confirm: None,
77            edit_buffer: String::new(),
78            edit_cursor: 0,
79            path_picker: None,
80            message: None,
81        }
82    }
83
84    pub fn next(&mut self) {
85        if !self.editing && self.confirm.is_none() {
86            self.selected_index = (self.selected_index + 1) % 6;
87        }
88    }
89
90    pub fn previous(&mut self) {
91        if !self.editing && self.confirm.is_none() {
92            if self.selected_index == 0 {
93                self.selected_index = 5;
94            } else {
95                self.selected_index -= 1;
96            }
97        }
98    }
99
100    pub fn enter_edit(&mut self) {
101        if self.selected_index == 5 {
102            self.confirm = Some(SettingsConfirm::Reset);
103        } else if self.selected_index == 4 {
104            self.confirm = Some(SettingsConfirm::ClearCache);
105        } else if self.selected_index == 2 {
106            // Toggle HTTPS directly and keep the Base URL scheme in sync.
107            self.use_https = !self.use_https;
108            if self.use_https && self.base_url.starts_with("http://") {
109                self.base_url = self.base_url.replace("http://", "https://");
110                self.message = Some(("Updated URL scheme (HTTPS)".to_string(), Color::Green));
111            } else if !self.use_https && self.base_url.starts_with("https://") {
112                self.base_url = self.base_url.replace("https://", "http://");
113                self.message = Some(("Updated URL scheme (HTTP)".to_string(), Color::Green));
114            }
115        } else if self.selected_index == 1 {
116            self.path_picker = Some(PathPicker::new(
117                PathPickerMode::Directory,
118                self.download_dir.as_str(),
119            ));
120        } else {
121            self.editing = true;
122            self.edit_buffer = self.base_url.clone();
123            self.edit_cursor = self.edit_buffer.len();
124        }
125    }
126
127    pub fn save_edit(&mut self) -> bool {
128        if !self.editing {
129            return true; // UseHttps toggle is "saved" immediately in memory
130        }
131        if self.selected_index == 0 {
132            self.base_url = self.edit_buffer.trim().to_string();
133        }
134        self.editing = false;
135        true
136    }
137
138    pub fn cancel_edit(&mut self) {
139        self.editing = false;
140        self.confirm = None;
141        self.path_picker = None;
142        self.message = None;
143    }
144
145    pub fn add_char(&mut self, c: char) {
146        if self.editing {
147            self.edit_buffer.insert(self.edit_cursor, c);
148            self.edit_cursor += 1;
149        }
150    }
151
152    pub fn delete_char(&mut self) {
153        if self.editing && self.edit_cursor > 0 {
154            self.edit_buffer.remove(self.edit_cursor - 1);
155            self.edit_cursor -= 1;
156        }
157    }
158
159    pub fn move_cursor_left(&mut self) {
160        if self.editing && self.edit_cursor > 0 {
161            self.edit_cursor -= 1;
162        }
163    }
164
165    pub fn move_cursor_right(&mut self) {
166        if self.editing && self.edit_cursor < self.edit_buffer.len() {
167            self.edit_cursor += 1;
168        }
169    }
170
171    pub fn render(&mut self, f: &mut Frame, area: Rect) {
172        if let Some(ref mut picker) = self.path_picker {
173            let chunks = Layout::default()
174                .constraints([
175                    Constraint::Length(4),
176                    Constraint::Min(12),
177                    Constraint::Length(3),
178                ])
179                .direction(ratatui::layout::Direction::Vertical)
180                .split(area);
181            let info = [
182                format!(
183                    "romm-cli: v{} | RomM server: {}",
184                    self.version, self.server_version
185                ),
186                format!("GitHub:   {}", self.github_url),
187                format!("Auth:     {}", self.auth_status),
188            ];
189            f.render_widget(
190                Paragraph::new(info.join("\n")).block(Block::default().borders(Borders::BOTTOM)),
191                chunks[0],
192            );
193            let hint = "Esc: cancel   Ctrl+Enter: apply typed path (creates folders)   ↑ list top: path   Tab: path/list";
194            picker.render(f, chunks[1], "Choose ROMs directory", hint);
195            f.render_widget(
196                Paragraph::new("ROMs directory picker — Esc returns without changing")
197                    .style(Style::default().fg(Color::Cyan))
198                    .block(Block::default().borders(Borders::ALL)),
199                chunks[2],
200            );
201            return;
202        }
203
204        let chunks = Layout::default()
205            .constraints([
206                Constraint::Length(4), // Header info
207                Constraint::Min(10),   // Editable list
208                Constraint::Length(3), // Message/Hint
209                Constraint::Length(3), // Footer help
210            ])
211            .direction(ratatui::layout::Direction::Vertical)
212            .split(area);
213
214        // -- Header Info --
215        let info = [
216            format!(
217                "romm-cli: v{} | RomM server: {}",
218                self.version, self.server_version
219            ),
220            format!("GitHub:   {}", self.github_url),
221            format!("Auth:     {}", self.auth_status),
222        ];
223        f.render_widget(
224            Paragraph::new(info.join("\n")).block(Block::default().borders(Borders::BOTTOM)),
225            chunks[0],
226        );
227
228        // -- Editable List --
229        let items = [
230            ListItem::new(format!(
231                "Base URL:     {}",
232                if self.editing && self.selected_index == 0 {
233                    &self.edit_buffer
234                } else {
235                    &self.base_url
236                }
237            )),
238            ListItem::new(format!("Roms Dir:     {}", self.download_dir)),
239            ListItem::new(format!(
240                "Use HTTPS:    {}",
241                if self.use_https { "[X] Yes" } else { "[ ] No" }
242            )),
243            ListItem::new(format!(
244                "Auth:         {} (Enter to change)",
245                self.auth_status
246            )),
247            ListItem::new("Clear Cache (Remove cached ROM data)"),
248            ListItem::new("Reset Configuration (Delete settings from disk & keyring)"),
249        ];
250
251        let mut state = ListState::default();
252        state.select(Some(self.selected_index));
253
254        let list = List::new(items)
255            .block(
256                Block::default()
257                    .title(" Configuration ")
258                    .borders(Borders::ALL),
259            )
260            .highlight_style(
261                Style::default()
262                    .add_modifier(Modifier::BOLD)
263                    .fg(Color::Yellow),
264            )
265            .highlight_symbol(">> ");
266
267        f.render_stateful_widget(list, chunks[1], &mut state);
268
269        // -- Message Area --
270        if let Some(confirm) = &self.confirm {
271            let msg = match confirm {
272                SettingsConfirm::Reset => {
273                    "Are you sure you want to delete all settings? (Enter: Yes, Esc: Cancel)"
274                }
275                SettingsConfirm::ClearCache => {
276                    "Are you sure you want to clear the ROM cache? (Enter: Yes, Esc: Cancel)"
277                }
278            };
279            f.render_widget(
280                Paragraph::new(msg)
281                    .style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
282                chunks[2],
283            );
284        } else if let Some((msg, color)) = &self.message {
285            f.render_widget(
286                Paragraph::new(msg.as_str()).style(Style::default().fg(*color)),
287                chunks[2],
288            );
289        } else if self.editing {
290            f.render_widget(
291                Paragraph::new("Editing... Enter: save   Esc: cancel")
292                    .style(Style::default().fg(Color::Cyan)),
293                chunks[2],
294            );
295        }
296
297        // -- Footer Help --
298        let help = if self.confirm.is_some() {
299            "Enter: confirm   Esc: cancel"
300        } else if self.editing {
301            "Backspace: delete   Arrows: move cursor   Enter: save   Esc: cancel"
302        } else {
303            "↑/↓: select   Enter: edit/toggle   S: save to disk   Esc: back"
304        };
305        f.render_widget(
306            Paragraph::new(help).block(Block::default().borders(Borders::ALL)),
307            chunks[3],
308        );
309    }
310
311    pub fn cursor_position(&self, area: Rect) -> Option<(u16, u16)> {
312        if let Some(ref picker) = self.path_picker {
313            let chunks = Layout::default()
314                .constraints([
315                    Constraint::Length(4),
316                    Constraint::Min(12),
317                    Constraint::Length(3),
318                ])
319                .direction(ratatui::layout::Direction::Vertical)
320                .split(area);
321            return picker.cursor_position(chunks[1], "Choose ROMs directory");
322        }
323
324        if !self.editing {
325            return None;
326        }
327
328        let chunks = Layout::default()
329            .constraints([
330                Constraint::Length(4),
331                Constraint::Min(10),
332                Constraint::Length(3),
333                Constraint::Length(3),
334            ])
335            .direction(ratatui::layout::Direction::Vertical)
336            .split(area);
337
338        let list_area = chunks[1];
339        let y = list_area.y + 1 + self.selected_index as u16;
340        let label_len = 14; // "Base URL:     ".len()
341        let x = list_area.x + 1 /* border */ + 3 /* highlight symbol */ + label_len + self.edit_cursor as u16;
342
343        Some((x, y))
344    }
345}