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