Skip to main content

nm_wifi/ui/
modals.rs

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}