Skip to main content

nmrs_gui/ui/
networks.rs

1use anyhow::Result;
2use gtk::Align;
3use gtk::GestureClick;
4use gtk::prelude::*;
5use gtk::{Box, Image, Label, ListBox, ListBoxRow, Orientation};
6use nmrs::models::WifiSecurity;
7use nmrs::{NetworkManager, models};
8use std::rc::Rc;
9
10use crate::ui::connect;
11use crate::ui::network_page::NetworkPage;
12
13pub struct NetworkRowController {
14    pub row: gtk::ListBoxRow,
15    pub arrow: gtk::Image,
16    pub ctx: Rc<NetworksContext>,
17    pub net: models::Network,
18    pub details_page: Rc<NetworkPage>,
19}
20
21pub struct NetworksContext {
22    pub nm: Rc<NetworkManager>,
23    pub on_success: Rc<dyn Fn()>,
24    pub status: Label,
25    pub stack: gtk::Stack,
26    pub parent_window: gtk::ApplicationWindow,
27    pub details_page: Rc<NetworkPage>,
28    pub wired_details_page: Rc<crate::ui::wired_page::WiredPage>,
29}
30
31impl NetworksContext {
32    pub async fn new(
33        on_success: Rc<dyn Fn()>,
34        status: &Label,
35        stack: &gtk::Stack,
36        parent_window: &gtk::ApplicationWindow,
37        details_page: Rc<NetworkPage>,
38        wired_details_page: Rc<crate::ui::wired_page::WiredPage>,
39    ) -> Result<Self> {
40        let nm = Rc::new(NetworkManager::new().await?);
41
42        Ok(Self {
43            nm,
44            on_success,
45            status: status.clone(),
46            stack: stack.clone(),
47            parent_window: parent_window.clone(),
48            details_page,
49            wired_details_page,
50        })
51    }
52}
53
54impl NetworkRowController {
55    pub fn new(
56        row: gtk::ListBoxRow,
57        arrow: gtk::Image,
58        ctx: Rc<NetworksContext>,
59        net: models::Network,
60        details_page: Rc<NetworkPage>,
61    ) -> Self {
62        Self {
63            row,
64            arrow,
65            ctx,
66            net,
67            details_page,
68        }
69    }
70
71    pub fn attach(&self) {
72        self.attach_arrow();
73        self.attach_row_double();
74    }
75
76    fn attach_arrow(&self) {
77        let click = GestureClick::new();
78
79        let ctx = self.ctx.clone();
80        let net = self.net.clone();
81        let stack = self.ctx.stack.clone();
82        let page = self.details_page.clone();
83
84        click.connect_pressed(move |_, _, _, _| {
85            let ctx_c = ctx.clone();
86            let net_c = net.clone();
87            let stack_c = stack.clone();
88            let page_c = page.clone();
89
90            glib::MainContext::default().spawn_local(async move {
91                if let Ok(info) = ctx_c.nm.show_details(&net_c).await {
92                    page_c.update(&info);
93                    stack_c.set_visible_child_name("details");
94                }
95            });
96        });
97
98        self.arrow.add_controller(click);
99    }
100
101    fn attach_row_double(&self) {
102        let click = GestureClick::new();
103
104        let ctx = self.ctx.clone();
105        let net = self.net.clone();
106        let ssid = net.ssid.clone();
107        let secured = net.secured;
108        let is_eap = net.is_eap;
109
110        let status = ctx.status.clone();
111        let window = ctx.parent_window.clone();
112        let on_success = ctx.on_success.clone();
113
114        click.connect_pressed(move |_, n, _, _| {
115            if n != 2 {
116                return;
117            }
118
119            status.set_text(&format!("Connecting to {ssid}..."));
120
121            let ssid_c = ssid.clone();
122            let nm_c = ctx.nm.clone();
123            let status_c = status.clone();
124            let window_c = window.clone();
125            let on_success_c = on_success.clone();
126
127            glib::MainContext::default().spawn_local(async move {
128                if secured {
129                    let have = nm_c.has_saved_connection(&ssid_c).await.unwrap_or(false);
130
131                    if have {
132                        status_c.set_text(&format!("Connecting to {}...", ssid_c));
133                        window_c.set_sensitive(false);
134                        let creds = WifiSecurity::WpaPsk { psk: "".into() };
135                        match nm_c.connect(&ssid_c, creds).await {
136                            Ok(_) => {
137                                status_c.set_text("");
138                                on_success_c();
139                            }
140                            Err(e) => status_c.set_text(&format!("Failed to connect: {e}")),
141                        }
142                        window_c.set_sensitive(true);
143                    } else {
144                        connect::connect_modal(
145                            nm_c.clone(),
146                            &window_c,
147                            &ssid_c,
148                            is_eap,
149                            on_success_c.clone(),
150                        );
151                    }
152                } else {
153                    status_c.set_text(&format!("Connecting to {}...", ssid_c));
154                    window_c.set_sensitive(false);
155                    let creds = WifiSecurity::Open;
156                    match nm_c.connect(&ssid_c, creds).await {
157                        Ok(_) => {
158                            status_c.set_text("");
159                            on_success_c();
160                        }
161                        Err(e) => status_c.set_text(&format!("Failed to connect: {e}")),
162                    }
163                    window_c.set_sensitive(true);
164                }
165
166                status_c.set_text("");
167            });
168        });
169
170        self.row.add_controller(click);
171    }
172}
173
174pub fn networks_view(
175    ctx: Rc<NetworksContext>,
176    networks: &[models::Network],
177    current_ssid: Option<&str>,
178    current_band: Option<&str>,
179) -> ListBox {
180    let conn_threshold = 75;
181    let list = ListBox::new();
182
183    let mut sorted_networks: Vec<_> = networks
184        .iter()
185        .filter(|net| !net.ssid.trim().is_empty())
186        .cloned()
187        .collect();
188
189    sorted_networks.sort_by(|a, b| {
190        let a_connected = is_current_network(a, current_ssid, current_band);
191        let b_connected = is_current_network(b, current_ssid, current_band);
192
193        match (a_connected, b_connected) {
194            (true, false) => std::cmp::Ordering::Less,
195            (false, true) => std::cmp::Ordering::Greater,
196            _ => b.strength.unwrap_or(0).cmp(&a.strength.unwrap_or(0)),
197        }
198    });
199
200    for net in sorted_networks {
201        let row = ListBoxRow::new();
202        let hbox = Box::new(Orientation::Horizontal, 6);
203
204        row.add_css_class("network-selection");
205
206        if is_current_network(&net, current_ssid, current_band) {
207            row.add_css_class("connected");
208        }
209
210        let display_name = match net.frequency.and_then(crate::ui::freq_to_band) {
211            Some(band) => format!("{} ({band})", net.ssid),
212            None => net.ssid.clone(),
213        };
214
215        hbox.append(&Label::new(Some(&display_name)));
216
217        if is_current_network(&net, current_ssid, current_band) {
218            let connected_label = Label::new(Some("Connected"));
219            connected_label.add_css_class("connected-label");
220            hbox.append(&connected_label);
221        }
222
223        let spacer = Box::new(Orientation::Horizontal, 0);
224        spacer.set_hexpand(true);
225        hbox.append(&spacer);
226
227        if let Some(s) = net.strength {
228            let icon_name = if net.secured {
229                "network-wireless-encrypted-symbolic"
230            } else {
231                "network-wireless-signal-excellent-symbolic"
232            };
233
234            let image = Image::from_icon_name(icon_name);
235            if net.secured {
236                image.add_css_class("wifi-secure");
237            } else {
238                image.add_css_class("wifi-open");
239            }
240
241            let strength_label = Label::new(Some(&format!("{s}%")));
242            hbox.append(&image);
243            hbox.append(&strength_label);
244
245            if s >= conn_threshold {
246                strength_label.add_css_class("network-good");
247            } else if s > 65 {
248                strength_label.add_css_class("network-okay");
249            } else {
250                strength_label.add_css_class("network-poor");
251            }
252        }
253
254        let arrow = Image::from_icon_name("go-next-symbolic");
255        arrow.set_halign(Align::End);
256        arrow.add_css_class("network-arrow");
257        arrow.set_cursor_from_name(Some("pointer"));
258        hbox.append(&arrow);
259
260        row.set_child(Some(&hbox));
261
262        let controller = NetworkRowController::new(
263            row.clone(),
264            arrow.clone(),
265            ctx.clone(),
266            net.clone(),
267            ctx.details_page.clone(),
268        );
269
270        controller.attach();
271
272        list.append(&row);
273    }
274    list
275}
276
277fn is_current_network(
278    net: &models::Network,
279    current_ssid: Option<&str>,
280    current_band: Option<&str>,
281) -> bool {
282    let ssid = match current_ssid {
283        Some(s) => s,
284        None => return false,
285    };
286
287    if net.ssid != ssid {
288        return false;
289    }
290
291    if let Some(band) = current_band {
292        let net_band = net.frequency.and_then(crate::ui::freq_to_band);
293
294        return net_band == Some(band);
295    }
296
297    true
298}