4runr-os 2.4.0 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,362 +1,362 @@
1
- /// Settings Screen
2
- /// Configure system settings, AI providers, and mode
3
-
4
- use ratatui::prelude::*;
5
- use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
6
- use crate::app::AppState;
7
-
8
- // === 4RUNR BRAND COLORS (matching layout.rs) ===
9
- const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
10
- const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
11
- const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
12
- const AMBER_WARN: Color = Color::Rgb(255, 191, 0);
13
- const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
14
- const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
15
- const TEXT_MUTED: Color = Color::Rgb(60, 60, 80);
16
- const BG_PANEL: Color = Color::Rgb(18, 18, 25);
17
-
18
- /// Settings state
19
- #[derive(Debug, Clone)]
20
- pub struct SettingsState {
21
- /// Current focused section
22
- pub focused_section: SettingsSection,
23
-
24
- /// Mode: Local or Connected
25
- pub mode: OperationMode,
26
-
27
- /// AI Provider settings
28
- pub ai_provider: String, // "openai", "anthropic", "local"
29
- pub ai_model: String, // "gpt-4", "claude-3", etc.
30
- pub api_key_set: bool, // Whether API key is configured
31
-
32
- /// Gateway settings
33
- pub gateway_url: String,
34
- pub gateway_connected: bool,
35
-
36
- /// Auto-update setting
37
- pub auto_update: bool,
38
-
39
- /// Unsaved changes
40
- pub has_changes: bool,
41
- }
42
-
43
- #[derive(Debug, Clone, PartialEq)]
44
- pub enum SettingsSection {
45
- Mode,
46
- AIProvider,
47
- Gateway,
48
- AutoUpdate,
49
- }
50
-
51
- #[derive(Debug, Clone, PartialEq)]
52
- pub enum OperationMode {
53
- Local, // Local-only, no gateway
54
- Connected, // Connected to gateway
55
- }
56
-
57
- impl OperationMode {
58
- pub fn as_str(&self) -> &str {
59
- match self {
60
- OperationMode::Local => "Local",
61
- OperationMode::Connected => "Connected",
62
- }
63
- }
64
-
65
- pub fn description(&self) -> &str {
66
- match self {
67
- OperationMode::Local => "Offline mode - local tools and models only",
68
- OperationMode::Connected => "Full mode - gateway access, server tools, Shield, Sentinel",
69
- }
70
- }
71
- }
72
-
73
- impl Default for SettingsState {
74
- fn default() -> Self {
75
- Self {
76
- focused_section: SettingsSection::Mode,
77
- mode: OperationMode::Local,
78
- ai_provider: "openai".to_string(),
79
- ai_model: "gpt-4".to_string(),
80
- api_key_set: false,
81
- gateway_url: "http://localhost:3001".to_string(),
82
- gateway_connected: false,
83
- auto_update: true,
84
- has_changes: false,
85
- }
86
- }
87
- }
88
-
89
- impl SettingsState {
90
- /// Toggle mode
91
- pub fn toggle_mode(&mut self) {
92
- self.mode = match self.mode {
93
- OperationMode::Local => OperationMode::Connected,
94
- OperationMode::Connected => OperationMode::Local,
95
- };
96
- self.has_changes = true;
97
- }
98
-
99
- /// Cycle to next AI provider
100
- pub fn next_provider(&mut self) {
101
- self.ai_provider = match self.ai_provider.as_str() {
102
- "openai" => "anthropic".to_string(),
103
- "anthropic" => "local".to_string(),
104
- _ => "openai".to_string(),
105
- };
106
-
107
- // Update default model for provider
108
- self.ai_model = match self.ai_provider.as_str() {
109
- "openai" => "gpt-4".to_string(),
110
- "anthropic" => "claude-3-opus".to_string(),
111
- "local" => "llama2".to_string(),
112
- _ => "gpt-4".to_string(),
113
- };
114
-
115
- self.has_changes = true;
116
- }
117
-
118
- /// Toggle auto-update
119
- pub fn toggle_auto_update(&mut self) {
120
- self.auto_update = !self.auto_update;
121
- self.has_changes = true;
122
- }
123
-
124
- /// Move to next section
125
- pub fn next_section(&mut self) {
126
- self.focused_section = match self.focused_section {
127
- SettingsSection::Mode => SettingsSection::AIProvider,
128
- SettingsSection::AIProvider => SettingsSection::Gateway,
129
- SettingsSection::Gateway => SettingsSection::AutoUpdate,
130
- SettingsSection::AutoUpdate => SettingsSection::Mode,
131
- };
132
- }
133
-
134
- /// Move to previous section
135
- pub fn prev_section(&mut self) {
136
- self.focused_section = match self.focused_section {
137
- SettingsSection::Mode => SettingsSection::AutoUpdate,
138
- SettingsSection::AIProvider => SettingsSection::Mode,
139
- SettingsSection::Gateway => SettingsSection::AIProvider,
140
- SettingsSection::AutoUpdate => SettingsSection::Gateway,
141
- };
142
- }
143
-
144
- /// Save settings
145
- pub fn save(&mut self) {
146
- self.has_changes = false;
147
- // TODO: Persist settings to config file
148
- }
149
-
150
- /// Discard changes
151
- pub fn discard(&mut self) {
152
- // TODO: Reload from config file
153
- self.has_changes = false;
154
- }
155
- }
156
-
157
- /// Render the Settings screen
158
- pub fn render(f: &mut Frame, state: &AppState) {
159
- let area = f.size();
160
-
161
- // Get settings state from AppState
162
- let settings = &state.settings;
163
-
164
- // Split screen into sections
165
- use ratatui::layout::{Constraint, Direction, Layout};
166
-
167
- let chunks = Layout::default()
168
- .direction(Direction::Vertical)
169
- .constraints([
170
- Constraint::Length(3), // Header
171
- Constraint::Min(15), // Settings content
172
- Constraint::Length(3), // Actions
173
- ])
174
- .split(area);
175
-
176
- render_header(f, chunks[0], settings);
177
- render_settings_content(f, chunks[1], settings);
178
- render_actions(f, chunks[2], settings);
179
- }
180
-
181
- fn render_header(f: &mut Frame, area: Rect, settings: &SettingsState) {
182
- let title = if settings.has_changes {
183
- " ⚙️ Settings (Unsaved Changes ●) "
184
- } else {
185
- " ⚙️ Settings "
186
- };
187
-
188
- let block = Block::default()
189
- .title(title)
190
- .borders(Borders::ALL)
191
- .border_style(Style::default().fg(if settings.has_changes {
192
- AMBER_WARN
193
- } else {
194
- BRAND_PURPLE
195
- }))
196
- .style(Style::default().bg(BG_PANEL));
197
-
198
- f.render_widget(block, area);
199
- }
200
-
201
- fn render_settings_content(f: &mut Frame, area: Rect, settings: &SettingsState) {
202
- let block = Block::default()
203
- .borders(Borders::ALL)
204
- .border_style(Style::default().fg(CYBER_CYAN))
205
- .style(Style::default().bg(BG_PANEL));
206
-
207
- let inner = block.inner(area);
208
- f.render_widget(block, area);
209
-
210
- let is_mode_focused = settings.focused_section == SettingsSection::Mode;
211
- let is_provider_focused = settings.focused_section == SettingsSection::AIProvider;
212
- let is_gateway_focused = settings.focused_section == SettingsSection::Gateway;
213
- let is_autoupdate_focused = settings.focused_section == SettingsSection::AutoUpdate;
214
-
215
- let text = vec![
216
- Line::from(""),
217
- // Mode Section
218
- Line::from(vec![
219
- Span::styled(
220
- if is_mode_focused { "▶ " } else { " " },
221
- Style::default().fg(BRAND_PURPLE).bold()
222
- ),
223
- Span::styled("🌐 Operation Mode", Style::default().fg(TEXT_PRIMARY).bold()),
224
- ]),
225
- Line::from(vec![
226
- Span::raw(" "),
227
- Span::styled(
228
- format!("[{}] Local [{}] Connected",
229
- if settings.mode == OperationMode::Local { "●" } else { " " },
230
- if settings.mode == OperationMode::Connected { "●" } else { " " }
231
- ),
232
- Style::default().fg(if is_mode_focused { CYBER_CYAN } else { TEXT_PRIMARY })
233
- ),
234
- ]),
235
- Line::from(vec![
236
- Span::raw(" "),
237
- Span::styled(
238
- settings.mode.description(),
239
- Style::default().fg(TEXT_DIM).italic()
240
- ),
241
- ]),
242
- Line::from(""),
243
-
244
- // AI Provider Section
245
- Line::from(vec![
246
- Span::styled(
247
- if is_provider_focused { "▶ " } else { " " },
248
- Style::default().fg(BRAND_PURPLE).bold()
249
- ),
250
- Span::styled("🤖 AI Provider", Style::default().fg(TEXT_PRIMARY).bold()),
251
- ]),
252
- Line::from(vec![
253
- Span::styled(" Provider: ", Style::default().fg(TEXT_DIM)),
254
- Span::styled(
255
- &settings.ai_provider,
256
- Style::default().fg(if is_provider_focused { CYBER_CYAN } else { TEXT_PRIMARY }).bold()
257
- ),
258
- ]),
259
- Line::from(vec![
260
- Span::styled(" Model: ", Style::default().fg(TEXT_DIM)),
261
- Span::styled(
262
- &settings.ai_model,
263
- Style::default().fg(if is_provider_focused { CYBER_CYAN } else { TEXT_PRIMARY })
264
- ),
265
- ]),
266
- Line::from(vec![
267
- Span::styled(" API Key: ", Style::default().fg(TEXT_DIM)),
268
- Span::styled(
269
- if settings.api_key_set { "Configured ✓" } else { "Not set" },
270
- Style::default().fg(if settings.api_key_set { NEON_GREEN } else { Color::Rgb(255, 69, 69) })
271
- ),
272
- ]),
273
- Line::from(""),
274
-
275
- // Gateway Section
276
- Line::from(vec![
277
- Span::styled(
278
- if is_gateway_focused { "▶ " } else { " " },
279
- Style::default().fg(BRAND_PURPLE).bold()
280
- ),
281
- Span::styled("🔗 Gateway Connection", Style::default().fg(TEXT_PRIMARY).bold()),
282
- ]),
283
- Line::from(vec![
284
- Span::styled(" URL: ", Style::default().fg(TEXT_DIM)),
285
- Span::styled(
286
- &settings.gateway_url,
287
- Style::default().fg(if is_gateway_focused { CYBER_CYAN } else { TEXT_PRIMARY })
288
- ),
289
- ]),
290
- Line::from(vec![
291
- Span::styled(" Status: ", Style::default().fg(TEXT_DIM)),
292
- Span::styled(
293
- if settings.gateway_connected { "Connected ✓" } else { "Disconnected" },
294
- Style::default().fg(if settings.gateway_connected { NEON_GREEN } else { TEXT_MUTED })
295
- ),
296
- ]),
297
- Line::from(""),
298
-
299
- // Auto-update Section
300
- Line::from(vec![
301
- Span::styled(
302
- if is_autoupdate_focused { "▶ " } else { " " },
303
- Style::default().fg(BRAND_PURPLE).bold()
304
- ),
305
- Span::styled("🔄 Auto-Update", Style::default().fg(TEXT_PRIMARY).bold()),
306
- ]),
307
- Line::from(vec![
308
- Span::raw(" "),
309
- Span::styled(
310
- format!("[{}] Enable automatic updates",
311
- if settings.auto_update { "✓" } else { " " }
312
- ),
313
- Style::default().fg(if is_autoupdate_focused { CYBER_CYAN } else { TEXT_PRIMARY })
314
- ),
315
- ]),
316
- ];
317
-
318
- let paragraph = Paragraph::new(text)
319
- .wrap(Wrap { trim: false })
320
- .alignment(Alignment::Left);
321
-
322
- f.render_widget(paragraph, inner);
323
- }
324
-
325
- fn render_actions(f: &mut Frame, area: Rect, settings: &SettingsState) {
326
- let block = Block::default()
327
- .title(" ⌨️ Actions ")
328
- .borders(Borders::ALL)
329
- .border_style(Style::default().fg(TEXT_DIM))
330
- .style(Style::default().bg(BG_PANEL));
331
-
332
- let inner = block.inner(area);
333
- f.render_widget(block, area);
334
-
335
- let text = if settings.has_changes {
336
- Line::from(vec![
337
- Span::styled("↑/↓", Style::default().fg(CYBER_CYAN).bold()),
338
- Span::styled(" Navigate │ ", Style::default().fg(TEXT_DIM)),
339
- Span::styled("Space", Style::default().fg(AMBER_WARN).bold()),
340
- Span::styled(" Toggle │ ", Style::default().fg(TEXT_DIM)),
341
- Span::styled("Enter", Style::default().fg(NEON_GREEN).bold()),
342
- Span::styled(" Save │ ", Style::default().fg(TEXT_DIM)),
343
- Span::styled("ESC", Style::default().fg(Color::Rgb(255, 69, 69)).bold()),
344
- Span::styled(" Discard & Close", Style::default().fg(TEXT_DIM)),
345
- ])
346
- } else {
347
- Line::from(vec![
348
- Span::styled("↑/↓", Style::default().fg(CYBER_CYAN).bold()),
349
- Span::styled(" Navigate │ ", Style::default().fg(TEXT_DIM)),
350
- Span::styled("Space", Style::default().fg(AMBER_WARN).bold()),
351
- Span::styled(" Toggle │ ", Style::default().fg(TEXT_DIM)),
352
- Span::styled("ESC", Style::default().fg(BRAND_PURPLE).bold()),
353
- Span::styled(" Close", Style::default().fg(TEXT_DIM)),
354
- ])
355
- };
356
-
357
- let paragraph = Paragraph::new(text)
358
- .style(Style::default().fg(TEXT_PRIMARY))
359
- .alignment(Alignment::Center);
360
-
361
- f.render_widget(paragraph, inner);
362
- }
1
+ /// Settings Screen
2
+ /// Configure system settings, AI providers, and mode
3
+
4
+ use ratatui::prelude::*;
5
+ use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
6
+ use crate::app::AppState;
7
+
8
+ // === 4RUNR BRAND COLORS (matching layout.rs) ===
9
+ const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
10
+ const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
11
+ const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
12
+ const AMBER_WARN: Color = Color::Rgb(255, 191, 0);
13
+ const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
14
+ const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
15
+ const TEXT_MUTED: Color = Color::Rgb(60, 60, 80);
16
+ const BG_PANEL: Color = Color::Rgb(18, 18, 25);
17
+
18
+ /// Settings state
19
+ #[derive(Debug, Clone)]
20
+ pub struct SettingsState {
21
+ /// Current focused section
22
+ pub focused_section: SettingsSection,
23
+
24
+ /// Mode: Local or Connected
25
+ pub mode: OperationMode,
26
+
27
+ /// AI Provider settings
28
+ pub ai_provider: String, // "openai", "anthropic", "local"
29
+ pub ai_model: String, // "gpt-4", "claude-3", etc.
30
+ pub api_key_set: bool, // Whether API key is configured
31
+
32
+ /// Gateway settings
33
+ pub gateway_url: String,
34
+ pub gateway_connected: bool,
35
+
36
+ /// Auto-update setting
37
+ pub auto_update: bool,
38
+
39
+ /// Unsaved changes
40
+ pub has_changes: bool,
41
+ }
42
+
43
+ #[derive(Debug, Clone, PartialEq)]
44
+ pub enum SettingsSection {
45
+ Mode,
46
+ AIProvider,
47
+ Gateway,
48
+ AutoUpdate,
49
+ }
50
+
51
+ #[derive(Debug, Clone, PartialEq)]
52
+ pub enum OperationMode {
53
+ Local, // Local-only, no gateway
54
+ Connected, // Connected to gateway
55
+ }
56
+
57
+ impl OperationMode {
58
+ pub fn as_str(&self) -> &str {
59
+ match self {
60
+ OperationMode::Local => "Local",
61
+ OperationMode::Connected => "Connected",
62
+ }
63
+ }
64
+
65
+ pub fn description(&self) -> &str {
66
+ match self {
67
+ OperationMode::Local => "Offline mode - local tools and models only",
68
+ OperationMode::Connected => "Full mode - gateway access, server tools, Shield, Sentinel",
69
+ }
70
+ }
71
+ }
72
+
73
+ impl Default for SettingsState {
74
+ fn default() -> Self {
75
+ Self {
76
+ focused_section: SettingsSection::Mode,
77
+ mode: OperationMode::Local,
78
+ ai_provider: "openai".to_string(),
79
+ ai_model: "gpt-4".to_string(),
80
+ api_key_set: false,
81
+ gateway_url: "http://localhost:3001".to_string(),
82
+ gateway_connected: false,
83
+ auto_update: true,
84
+ has_changes: false,
85
+ }
86
+ }
87
+ }
88
+
89
+ impl SettingsState {
90
+ /// Toggle mode
91
+ pub fn toggle_mode(&mut self) {
92
+ self.mode = match self.mode {
93
+ OperationMode::Local => OperationMode::Connected,
94
+ OperationMode::Connected => OperationMode::Local,
95
+ };
96
+ self.has_changes = true;
97
+ }
98
+
99
+ /// Cycle to next AI provider
100
+ pub fn next_provider(&mut self) {
101
+ self.ai_provider = match self.ai_provider.as_str() {
102
+ "openai" => "anthropic".to_string(),
103
+ "anthropic" => "local".to_string(),
104
+ _ => "openai".to_string(),
105
+ };
106
+
107
+ // Update default model for provider
108
+ self.ai_model = match self.ai_provider.as_str() {
109
+ "openai" => "gpt-4".to_string(),
110
+ "anthropic" => "claude-3-opus".to_string(),
111
+ "local" => "llama2".to_string(),
112
+ _ => "gpt-4".to_string(),
113
+ };
114
+
115
+ self.has_changes = true;
116
+ }
117
+
118
+ /// Toggle auto-update
119
+ pub fn toggle_auto_update(&mut self) {
120
+ self.auto_update = !self.auto_update;
121
+ self.has_changes = true;
122
+ }
123
+
124
+ /// Move to next section
125
+ pub fn next_section(&mut self) {
126
+ self.focused_section = match self.focused_section {
127
+ SettingsSection::Mode => SettingsSection::AIProvider,
128
+ SettingsSection::AIProvider => SettingsSection::Gateway,
129
+ SettingsSection::Gateway => SettingsSection::AutoUpdate,
130
+ SettingsSection::AutoUpdate => SettingsSection::Mode,
131
+ };
132
+ }
133
+
134
+ /// Move to previous section
135
+ pub fn prev_section(&mut self) {
136
+ self.focused_section = match self.focused_section {
137
+ SettingsSection::Mode => SettingsSection::AutoUpdate,
138
+ SettingsSection::AIProvider => SettingsSection::Mode,
139
+ SettingsSection::Gateway => SettingsSection::AIProvider,
140
+ SettingsSection::AutoUpdate => SettingsSection::Gateway,
141
+ };
142
+ }
143
+
144
+ /// Save settings
145
+ pub fn save(&mut self) {
146
+ self.has_changes = false;
147
+ // TODO: Persist settings to config file
148
+ }
149
+
150
+ /// Discard changes
151
+ pub fn discard(&mut self) {
152
+ // TODO: Reload from config file
153
+ self.has_changes = false;
154
+ }
155
+ }
156
+
157
+ /// Render the Settings screen
158
+ pub fn render(f: &mut Frame, state: &AppState) {
159
+ let area = f.size();
160
+
161
+ // Get settings state from AppState
162
+ let settings = &state.settings;
163
+
164
+ // Split screen into sections
165
+ use ratatui::layout::{Constraint, Direction, Layout};
166
+
167
+ let chunks = Layout::default()
168
+ .direction(Direction::Vertical)
169
+ .constraints([
170
+ Constraint::Length(3), // Header
171
+ Constraint::Min(15), // Settings content
172
+ Constraint::Length(3), // Actions
173
+ ])
174
+ .split(area);
175
+
176
+ render_header(f, chunks[0], settings);
177
+ render_settings_content(f, chunks[1], settings);
178
+ render_actions(f, chunks[2], settings);
179
+ }
180
+
181
+ fn render_header(f: &mut Frame, area: Rect, settings: &SettingsState) {
182
+ let title = if settings.has_changes {
183
+ " ⚙️ Settings (Unsaved Changes ●) "
184
+ } else {
185
+ " ⚙️ Settings "
186
+ };
187
+
188
+ let block = Block::default()
189
+ .title(title)
190
+ .borders(Borders::ALL)
191
+ .border_style(Style::default().fg(if settings.has_changes {
192
+ AMBER_WARN
193
+ } else {
194
+ BRAND_PURPLE
195
+ }))
196
+ .style(Style::default().bg(BG_PANEL));
197
+
198
+ f.render_widget(block, area);
199
+ }
200
+
201
+ fn render_settings_content(f: &mut Frame, area: Rect, settings: &SettingsState) {
202
+ let block = Block::default()
203
+ .borders(Borders::ALL)
204
+ .border_style(Style::default().fg(CYBER_CYAN))
205
+ .style(Style::default().bg(BG_PANEL));
206
+
207
+ let inner = block.inner(area);
208
+ f.render_widget(block, area);
209
+
210
+ let is_mode_focused = settings.focused_section == SettingsSection::Mode;
211
+ let is_provider_focused = settings.focused_section == SettingsSection::AIProvider;
212
+ let is_gateway_focused = settings.focused_section == SettingsSection::Gateway;
213
+ let is_autoupdate_focused = settings.focused_section == SettingsSection::AutoUpdate;
214
+
215
+ let text = vec![
216
+ Line::from(""),
217
+ // Mode Section
218
+ Line::from(vec![
219
+ Span::styled(
220
+ if is_mode_focused { "▶ " } else { " " },
221
+ Style::default().fg(BRAND_PURPLE).bold()
222
+ ),
223
+ Span::styled("🌐 Operation Mode", Style::default().fg(TEXT_PRIMARY).bold()),
224
+ ]),
225
+ Line::from(vec![
226
+ Span::raw(" "),
227
+ Span::styled(
228
+ format!("[{}] Local [{}] Connected",
229
+ if settings.mode == OperationMode::Local { "●" } else { " " },
230
+ if settings.mode == OperationMode::Connected { "●" } else { " " }
231
+ ),
232
+ Style::default().fg(if is_mode_focused { CYBER_CYAN } else { TEXT_PRIMARY })
233
+ ),
234
+ ]),
235
+ Line::from(vec![
236
+ Span::raw(" "),
237
+ Span::styled(
238
+ settings.mode.description(),
239
+ Style::default().fg(TEXT_DIM).italic()
240
+ ),
241
+ ]),
242
+ Line::from(""),
243
+
244
+ // AI Provider Section
245
+ Line::from(vec![
246
+ Span::styled(
247
+ if is_provider_focused { "▶ " } else { " " },
248
+ Style::default().fg(BRAND_PURPLE).bold()
249
+ ),
250
+ Span::styled("🤖 AI Provider", Style::default().fg(TEXT_PRIMARY).bold()),
251
+ ]),
252
+ Line::from(vec![
253
+ Span::styled(" Provider: ", Style::default().fg(TEXT_DIM)),
254
+ Span::styled(
255
+ &settings.ai_provider,
256
+ Style::default().fg(if is_provider_focused { CYBER_CYAN } else { TEXT_PRIMARY }).bold()
257
+ ),
258
+ ]),
259
+ Line::from(vec![
260
+ Span::styled(" Model: ", Style::default().fg(TEXT_DIM)),
261
+ Span::styled(
262
+ &settings.ai_model,
263
+ Style::default().fg(if is_provider_focused { CYBER_CYAN } else { TEXT_PRIMARY })
264
+ ),
265
+ ]),
266
+ Line::from(vec![
267
+ Span::styled(" API Key: ", Style::default().fg(TEXT_DIM)),
268
+ Span::styled(
269
+ if settings.api_key_set { "Configured ✓" } else { "Not set" },
270
+ Style::default().fg(if settings.api_key_set { NEON_GREEN } else { Color::Rgb(255, 69, 69) })
271
+ ),
272
+ ]),
273
+ Line::from(""),
274
+
275
+ // Gateway Section
276
+ Line::from(vec![
277
+ Span::styled(
278
+ if is_gateway_focused { "▶ " } else { " " },
279
+ Style::default().fg(BRAND_PURPLE).bold()
280
+ ),
281
+ Span::styled("🔗 Gateway Connection", Style::default().fg(TEXT_PRIMARY).bold()),
282
+ ]),
283
+ Line::from(vec![
284
+ Span::styled(" URL: ", Style::default().fg(TEXT_DIM)),
285
+ Span::styled(
286
+ &settings.gateway_url,
287
+ Style::default().fg(if is_gateway_focused { CYBER_CYAN } else { TEXT_PRIMARY })
288
+ ),
289
+ ]),
290
+ Line::from(vec![
291
+ Span::styled(" Status: ", Style::default().fg(TEXT_DIM)),
292
+ Span::styled(
293
+ if settings.gateway_connected { "Connected ✓" } else { "Disconnected" },
294
+ Style::default().fg(if settings.gateway_connected { NEON_GREEN } else { TEXT_MUTED })
295
+ ),
296
+ ]),
297
+ Line::from(""),
298
+
299
+ // Auto-update Section
300
+ Line::from(vec![
301
+ Span::styled(
302
+ if is_autoupdate_focused { "▶ " } else { " " },
303
+ Style::default().fg(BRAND_PURPLE).bold()
304
+ ),
305
+ Span::styled("🔄 Auto-Update", Style::default().fg(TEXT_PRIMARY).bold()),
306
+ ]),
307
+ Line::from(vec![
308
+ Span::raw(" "),
309
+ Span::styled(
310
+ format!("[{}] Enable automatic updates",
311
+ if settings.auto_update { "✓" } else { " " }
312
+ ),
313
+ Style::default().fg(if is_autoupdate_focused { CYBER_CYAN } else { TEXT_PRIMARY })
314
+ ),
315
+ ]),
316
+ ];
317
+
318
+ let paragraph = Paragraph::new(text)
319
+ .wrap(Wrap { trim: false })
320
+ .alignment(Alignment::Left);
321
+
322
+ f.render_widget(paragraph, inner);
323
+ }
324
+
325
+ fn render_actions(f: &mut Frame, area: Rect, settings: &SettingsState) {
326
+ let block = Block::default()
327
+ .title(" ⌨️ Actions ")
328
+ .borders(Borders::ALL)
329
+ .border_style(Style::default().fg(TEXT_DIM))
330
+ .style(Style::default().bg(BG_PANEL));
331
+
332
+ let inner = block.inner(area);
333
+ f.render_widget(block, area);
334
+
335
+ let text = if settings.has_changes {
336
+ Line::from(vec![
337
+ Span::styled("↑/↓", Style::default().fg(CYBER_CYAN).bold()),
338
+ Span::styled(" Navigate │ ", Style::default().fg(TEXT_DIM)),
339
+ Span::styled("Space", Style::default().fg(AMBER_WARN).bold()),
340
+ Span::styled(" Toggle │ ", Style::default().fg(TEXT_DIM)),
341
+ Span::styled("Enter", Style::default().fg(NEON_GREEN).bold()),
342
+ Span::styled(" Save │ ", Style::default().fg(TEXT_DIM)),
343
+ Span::styled("ESC", Style::default().fg(Color::Rgb(255, 69, 69)).bold()),
344
+ Span::styled(" Discard & Close", Style::default().fg(TEXT_DIM)),
345
+ ])
346
+ } else {
347
+ Line::from(vec![
348
+ Span::styled("↑/↓", Style::default().fg(CYBER_CYAN).bold()),
349
+ Span::styled(" Navigate │ ", Style::default().fg(TEXT_DIM)),
350
+ Span::styled("Space", Style::default().fg(AMBER_WARN).bold()),
351
+ Span::styled(" Toggle │ ", Style::default().fg(TEXT_DIM)),
352
+ Span::styled("ESC", Style::default().fg(BRAND_PURPLE).bold()),
353
+ Span::styled(" Close", Style::default().fg(TEXT_DIM)),
354
+ ])
355
+ };
356
+
357
+ let paragraph = Paragraph::new(text)
358
+ .style(Style::default().fg(TEXT_PRIMARY))
359
+ .alignment(Alignment::Center);
360
+
361
+ f.render_widget(paragraph, inner);
362
+ }