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