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
22pub 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 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 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; }
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), Constraint::Min(10), Constraint::Length(3), Constraint::Length(3), ])
211 .direction(ratatui::layout::Direction::Vertical)
212 .split(area);
213
214 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 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 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 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; let x = list_area.x + 1 + 3 + label_len + self.edit_cursor as u16;
342
343 Some((x, y))
344 }
345}