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