1use ratatui::{
2 Frame,
3 layout::{Alignment, Constraint, Direction, Layout, Rect},
4 style::{Color, Modifier, Style},
5 text::{Line, Span},
6 widgets::{Block, Borders, Clear, Paragraph},
7};
8
9use super::format::get_frequency_band;
10use crate::{app_state::App, theme::CatppuccinColors, wifi::WifiNetwork};
11
12pub fn render_help_screen(f: &mut Frame, _app: &App, area: Rect) {
13 let help_text = vec![
14 Line::from(vec![Span::styled(
15 "Navigation",
16 Style::default()
17 .fg(CatppuccinColors::MAUVE)
18 .add_modifier(Modifier::BOLD),
19 )]),
20 Line::from(""),
21 Line::from("↑/k Move up"),
22 Line::from("↓/j Move down"),
23 Line::from(""),
24 Line::from(vec![Span::styled(
25 "Actions",
26 Style::default()
27 .fg(CatppuccinColors::MAUVE)
28 .add_modifier(Modifier::BOLD),
29 )]),
30 Line::from(""),
31 Line::from("Enter/c Connect or disconnect selection"),
32 Line::from("d Disconnect selected active network"),
33 Line::from("r Rescan networks"),
34 Line::from("i Show network details"),
35 Line::from(""),
36 Line::from(vec![Span::styled(
37 "Other",
38 Style::default()
39 .fg(CatppuccinColors::MAUVE)
40 .add_modifier(Modifier::BOLD),
41 )]),
42 Line::from(""),
43 Line::from("h Show help"),
44 Line::from("q/Esc Quit application"),
45 Line::from(""),
46 Line::from(vec![Span::styled(
47 "Markers",
48 Style::default()
49 .fg(CatppuccinColors::MAUVE)
50 .add_modifier(Modifier::BOLD),
51 )]),
52 Line::from(""),
53 Line::from("Link icon Connected network"),
54 Line::from("Lock icon Protected network"),
55 Line::from("2.4G/5G Frequency band"),
56 ];
57
58 let help_paragraph = Paragraph::new(help_text)
59 .block(
60 Block::default()
61 .borders(Borders::ALL)
62 .title("Help - nm-wifi")
63 .title_style(
64 Style::default()
65 .fg(CatppuccinColors::BLUE)
66 .add_modifier(Modifier::BOLD),
67 ),
68 )
69 .style(Style::default().bg(CatppuccinColors::BASE))
70 .alignment(Alignment::Left);
71
72 f.render_widget(help_paragraph, area);
73}
74
75pub fn render_network_details(f: &mut Frame, app: &App) {
76 if let Some(network) = app.selected_network_in_list() {
77 let popup_area = centered_rect(60, 70, f.area());
78 f.render_widget(Clear, popup_area);
79
80 let security_type = network.security.display_name();
81
82 let signal_description = match network.signal_strength {
83 80..=100 => "Excellent",
84 60..=79 => "Good",
85 40..=59 => "Fair",
86 20..=39 => "Weak",
87 _ => "Very Weak",
88 };
89
90 let signal_text =
91 format!("{}% ({})", network.signal_strength, signal_description);
92 let frequency_text = format!(
93 "{} MHz ({})",
94 network.frequency,
95 get_frequency_band(network.frequency)
96 );
97
98 let details_text = vec![
99 Line::from(vec![
100 Span::styled(
101 "SSID: ",
102 Style::default()
103 .fg(CatppuccinColors::MAUVE)
104 .add_modifier(Modifier::BOLD),
105 ),
106 Span::styled(
107 &network.ssid,
108 Style::default().fg(CatppuccinColors::TEXT),
109 ),
110 ]),
111 Line::from(""),
112 Line::from(vec![
113 Span::styled(
114 "Status: ",
115 Style::default()
116 .fg(CatppuccinColors::MAUVE)
117 .add_modifier(Modifier::BOLD),
118 ),
119 Span::styled(
120 if network.connected {
121 "Connected"
122 } else {
123 "Available"
124 },
125 Style::default().fg(if network.connected {
126 CatppuccinColors::GREEN
127 } else {
128 CatppuccinColors::TEXT
129 }),
130 ),
131 ]),
132 Line::from(""),
133 Line::from(vec![
134 Span::styled(
135 "Security: ",
136 Style::default()
137 .fg(CatppuccinColors::MAUVE)
138 .add_modifier(Modifier::BOLD),
139 ),
140 Span::styled(
141 security_type,
142 Style::default().fg(CatppuccinColors::TEXT),
143 ),
144 ]),
145 Line::from(""),
146 Line::from(vec![
147 Span::styled(
148 "Signal Strength: ",
149 Style::default()
150 .fg(CatppuccinColors::MAUVE)
151 .add_modifier(Modifier::BOLD),
152 ),
153 Span::styled(
154 &signal_text,
155 Style::default().fg(match network.signal_strength {
156 80..=100 => CatppuccinColors::GREEN,
157 60..=79 => CatppuccinColors::YELLOW,
158 40..=59 => CatppuccinColors::PEACH,
159 _ => CatppuccinColors::RED,
160 }),
161 ),
162 ]),
163 Line::from(""),
164 Line::from(vec![
165 Span::styled(
166 "Frequency: ",
167 Style::default()
168 .fg(CatppuccinColors::MAUVE)
169 .add_modifier(Modifier::BOLD),
170 ),
171 Span::styled(
172 &frequency_text,
173 Style::default().fg(CatppuccinColors::SAPPHIRE),
174 ),
175 ]),
176 Line::from(""),
177 Line::from(""),
178 Line::from(vec![
179 Span::styled(
180 "Press ",
181 Style::default().fg(CatppuccinColors::SUBTEXT1),
182 ),
183 Span::styled(
184 "i",
185 Style::default()
186 .fg(CatppuccinColors::GREEN)
187 .add_modifier(Modifier::BOLD),
188 ),
189 Span::styled(
190 " or ",
191 Style::default().fg(CatppuccinColors::SUBTEXT1),
192 ),
193 Span::styled(
194 "Esc",
195 Style::default()
196 .fg(CatppuccinColors::GREEN)
197 .add_modifier(Modifier::BOLD),
198 ),
199 Span::styled(
200 " to close",
201 Style::default().fg(CatppuccinColors::SUBTEXT1),
202 ),
203 ]),
204 ];
205
206 let details_paragraph = Paragraph::new(details_text)
207 .block(
208 Block::default()
209 .borders(Borders::ALL)
210 .title("Network Details")
211 .title_style(
212 Style::default()
213 .fg(CatppuccinColors::BLUE)
214 .add_modifier(Modifier::BOLD),
215 ),
216 )
217 .style(Style::default().bg(CatppuccinColors::BASE))
218 .alignment(Alignment::Left);
219
220 f.render_widget(details_paragraph, popup_area);
221 }
222}
223
224fn modal_shadow_area(popup_area: Rect) -> Rect {
225 Rect {
226 x: popup_area.x + 1,
227 y: popup_area.y + 1,
228 width: popup_area.width,
229 height: popup_area.height,
230 }
231}
232
233fn render_modal_shell(f: &mut Frame, popup_area: Rect) {
234 f.render_widget(Clear, popup_area);
235 f.render_widget(
236 Block::default().style(Style::default().bg(CatppuccinColors::SURFACE0)),
237 modal_shadow_area(popup_area),
238 );
239}
240
241fn modal_block<'a>(title: &'a str, border_color: Color) -> Block<'a> {
242 Block::default()
243 .borders(Borders::ALL)
244 .title(title)
245 .title_style(
246 Style::default()
247 .fg(border_color)
248 .add_modifier(Modifier::BOLD),
249 )
250 .border_style(Style::default().fg(border_color))
251}
252
253fn render_modal(
254 f: &mut Frame,
255 popup_area: Rect,
256 title: &str,
257 border_color: Color,
258 lines: Vec<Line<'static>>,
259) {
260 render_modal_shell(f, popup_area);
261 let modal = Paragraph::new(lines)
262 .block(modal_block(title, border_color))
263 .style(Style::default().bg(CatppuccinColors::BASE))
264 .alignment(Alignment::Left);
265
266 f.render_widget(modal, popup_area);
267}
268
269fn network_summary_lines(
270 network: &WifiNetwork,
271 include_signal: bool,
272) -> Vec<Line<'static>> {
273 let mut lines = vec![
274 Line::from(format!("Network: {}", network.ssid)),
275 Line::from(format!("Security: {}", network.security.display_name())),
276 ];
277
278 if include_signal {
279 lines.push(Line::from(format!(
280 "Signal: {}% ({})",
281 network.signal_strength,
282 get_frequency_band(network.frequency)
283 )));
284 }
285
286 lines
287}
288
289pub fn render_enhanced_password_modal(f: &mut Frame, app: &App) {
290 if let Some(network) = &app.selected_network {
291 let popup_area = centered_rect(64, 28, f.area());
292 let password_display = if app.password_visible {
293 app.password_input.clone()
294 } else {
295 "•".repeat(app.password_input.len())
296 };
297 let password_field = format!("{:<38}", password_display);
298
299 let mut password_text = network_summary_lines(network, false);
300 password_text.extend([
301 Line::from(""),
302 Line::from("Password:"),
303 Line::from(""),
304 Line::from(vec![
305 Span::styled(
306 "┌",
307 Style::default().fg(CatppuccinColors::SURFACE2),
308 ),
309 Span::styled(
310 "─".repeat(40),
311 Style::default().fg(CatppuccinColors::SURFACE2),
312 ),
313 Span::styled(
314 "┐",
315 Style::default().fg(CatppuccinColors::SURFACE2),
316 ),
317 ]),
318 Line::from(vec![
319 Span::styled(
320 "│ ",
321 Style::default().fg(CatppuccinColors::SURFACE2),
322 ),
323 Span::styled(
324 password_field,
325 Style::default()
326 .fg(CatppuccinColors::TEXT)
327 .bg(CatppuccinColors::SURFACE0),
328 ),
329 Span::styled(
330 " │",
331 Style::default().fg(CatppuccinColors::SURFACE2),
332 ),
333 ]),
334 Line::from(vec![
335 Span::styled(
336 "└",
337 Style::default().fg(CatppuccinColors::SURFACE2),
338 ),
339 Span::styled(
340 "─".repeat(40),
341 Style::default().fg(CatppuccinColors::SURFACE2),
342 ),
343 Span::styled(
344 "┘",
345 Style::default().fg(CatppuccinColors::SURFACE2),
346 ),
347 ]),
348 Line::from(""),
349 Line::from("Enter: connect"),
350 Line::from("Tab: show or hide password"),
351 Line::from("Esc: cancel"),
352 ]);
353
354 render_modal(
355 f,
356 popup_area,
357 "Password",
358 CatppuccinColors::BLUE,
359 password_text,
360 );
361 }
362}
363
364pub fn render_enhanced_connecting_modal(f: &mut Frame, app: &App) {
365 if let Some(network) = &app.selected_network {
366 let popup_area = centered_rect(64, 28, f.area());
367 let mut connecting_text = network_summary_lines(network, true);
368 connecting_text.extend([
369 Line::from(""),
370 Line::from("Activating connection via NetworkManager..."),
371 Line::from("Press Esc to quit the application."),
372 ]);
373
374 render_modal(
375 f,
376 popup_area,
377 "Connecting",
378 CatppuccinColors::YELLOW,
379 connecting_text,
380 );
381 }
382}
383
384pub fn render_enhanced_disconnecting_modal(f: &mut Frame, app: &App) {
385 if let Some(network) = &app.selected_network {
386 let popup_area = centered_rect(64, 24, f.area());
387 let mut disconnecting_text = network_summary_lines(network, false);
388 disconnecting_text.extend([
389 Line::from("Disconnecting via NetworkManager..."),
390 Line::from("Press Esc to quit the application."),
391 ]);
392
393 render_modal(
394 f,
395 popup_area,
396 "Disconnecting",
397 CatppuccinColors::PEACH,
398 disconnecting_text,
399 );
400 }
401}
402
403pub fn render_enhanced_result_modal(f: &mut Frame, app: &App) {
404 let popup_area = centered_rect(68, 38, f.area());
405
406 let (title, color) = if app.connection_success {
407 if app.is_disconnect_operation {
408 ("Disconnection complete", CatppuccinColors::GREEN)
409 } else {
410 ("Connection complete", CatppuccinColors::GREEN)
411 }
412 } else if app.is_disconnect_operation {
413 ("Disconnection failed", CatppuccinColors::RED)
414 } else {
415 ("Connection failed", CatppuccinColors::RED)
416 };
417
418 let mut result_text = vec![];
419
420 if let Some(network) = &app.selected_network {
421 result_text.extend(network_summary_lines(network, true));
422 } else {
423 result_text.push(Line::from("Network: Unknown"));
424 }
425
426 if let Some(interface_name) = app.adapter_name.as_deref() {
427 result_text.push(Line::from(format!("Interface: {}", interface_name)));
428 }
429
430 result_text.push(Line::from(""));
431
432 if app.connection_success {
433 result_text
434 .push(Line::from("Status: NetworkManager reported success."));
435 } else {
436 let error_msg =
437 app.connection_error.as_deref().unwrap_or("Unknown error");
438 result_text.push(Line::from(vec![
439 Span::styled(
440 "Error: ",
441 Style::default().fg(color).add_modifier(Modifier::BOLD),
442 ),
443 Span::styled(
444 error_msg.to_string(),
445 Style::default().fg(CatppuccinColors::TEXT),
446 ),
447 ]));
448 }
449
450 result_text.extend([
451 Line::from(""),
452 Line::from("Enter: return to the network list"),
453 Line::from("q/Esc: quit"),
454 ]);
455
456 render_modal(f, popup_area, title, color, result_text);
457}
458
459pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
460 let popup_layout = Layout::default()
461 .direction(Direction::Vertical)
462 .constraints([
463 Constraint::Percentage((100 - percent_y) / 2),
464 Constraint::Percentage(percent_y),
465 Constraint::Percentage((100 - percent_y) / 2),
466 ])
467 .split(r);
468
469 Layout::default()
470 .direction(Direction::Horizontal)
471 .constraints([
472 Constraint::Percentage((100 - percent_x) / 2),
473 Constraint::Percentage(percent_x),
474 Constraint::Percentage((100 - percent_x) / 2),
475 ])
476 .split(popup_layout[1])[1]
477}