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};
7
8#[derive(PartialEq, Eq)]
9pub enum SettingsField {
10    BaseUrl,
11    DownloadDir,
12    UseHttps,
13}
14
15/// Interactive settings screen for editing current config.
16pub 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            // Toggle HTTPS directly and keep the Base URL scheme in sync.
91            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; // UseHttps toggle is "saved" immediately in memory
113        }
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), // Header info
158                Constraint::Min(10),   // Editable list
159                Constraint::Length(3), // Message/Hint
160                Constraint::Length(3), // Footer help
161            ])
162            .direction(ratatui::layout::Direction::Vertical)
163            .split(area);
164
165        // -- Header Info --
166        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        // -- Editable List --
180        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        // -- Message Area --
226        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        // -- Footer Help --
240        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        // Both branches were identical; keep the constant for cursor alignment.
269        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}