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::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 => "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) % 4;
68        }
69    }
70
71    pub fn previous(&mut self) {
72        if !self.editing {
73            if self.selected_index == 0 {
74                self.selected_index = 3;
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            // Toggle HTTPS directly
84            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; // UseHttps toggle is "saved" immediately in memory
103        }
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), // Header info
148                Constraint::Min(10),   // Editable list
149                Constraint::Length(3), // Message/Hint
150                Constraint::Length(3), // Footer help
151            ])
152            .direction(ratatui::layout::Direction::Vertical)
153            .split(area);
154
155        // -- Header Info --
156        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        // -- Editable List --
170        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            ListItem::new(format!(
192                "Auth:         {} (Enter to change)",
193                self.auth_status
194            )),
195        ];
196
197        let mut state = ListState::default();
198        state.select(Some(self.selected_index));
199
200        let list = List::new(items)
201            .block(
202                Block::default()
203                    .title(" Configuration ")
204                    .borders(Borders::ALL),
205            )
206            .highlight_style(
207                Style::default()
208                    .add_modifier(Modifier::BOLD)
209                    .fg(Color::Yellow),
210            )
211            .highlight_symbol(">> ");
212
213        f.render_stateful_widget(list, chunks[1], &mut state);
214
215        // -- Message Area --
216        if let Some((msg, color)) = &self.message {
217            f.render_widget(
218                Paragraph::new(msg.as_str()).style(Style::default().fg(*color)),
219                chunks[2],
220            );
221        } else if self.editing {
222            f.render_widget(
223                Paragraph::new("Editing... Enter: save   Esc: cancel")
224                    .style(Style::default().fg(Color::Cyan)),
225                chunks[2],
226            );
227        }
228
229        // -- Footer Help --
230        let help = if self.editing {
231            "Backspace: delete   Arrows: move cursor   Enter: save   Esc: cancel"
232        } else {
233            "↑/↓: select   Enter: edit/toggle   S: save to disk   Esc: back"
234        };
235        f.render_widget(
236            Paragraph::new(help).block(Block::default().borders(Borders::ALL)),
237            chunks[3],
238        );
239    }
240
241    pub fn cursor_position(&self, area: Rect) -> Option<(u16, u16)> {
242        if !self.editing {
243            return None;
244        }
245
246        let chunks = Layout::default()
247            .constraints([
248                Constraint::Length(4),
249                Constraint::Min(10),
250                Constraint::Length(3),
251                Constraint::Length(3),
252            ])
253            .direction(ratatui::layout::Direction::Vertical)
254            .split(area);
255
256        let list_area = chunks[1];
257        let y = list_area.y + 1 + self.selected_index as u16;
258        // Both branches were identical; keep the constant for cursor alignment.
259        let label_len = 17;
260        let x = list_area.x + 3 + label_len + self.edit_cursor as u16;
261
262        Some((x, y))
263    }
264}