Skip to main content

nmrs_gui/ui/
connect.rs

1use glib::Propagation;
2use gtk::{
3    ApplicationWindow, Box as GtkBox, Button, CheckButton, Dialog, Entry, EventControllerKey,
4    FileChooserAction, FileChooserDialog, Label, Orientation, ResponseType, prelude::*,
5};
6use log::{debug, error};
7use nmrs::{
8    NetworkManager,
9    models::{EapMethod, EapOptions, Phase2, WifiSecurity},
10};
11use std::rc::Rc;
12
13pub fn connect_modal(
14    nm: Rc<NetworkManager>,
15    parent: &ApplicationWindow,
16    ssid: &str,
17    is_eap: bool,
18    on_connection_success: Rc<dyn Fn()>,
19) {
20    let ssid_owned = ssid.to_string();
21    let parent_weak = parent.downgrade();
22
23    glib::MainContext::default().spawn_local(async move {
24        if let Some(current) = nm.current_ssid().await
25            && current == ssid_owned
26        {
27            debug!("Already connected to {current}, skipping modal");
28            return;
29        }
30
31        if let Some(parent) = parent_weak.upgrade() {
32            draw_connect_modal(nm, &parent, &ssid_owned, is_eap, on_connection_success);
33        }
34    });
35}
36
37fn draw_connect_modal(
38    nm: Rc<NetworkManager>,
39    parent: &ApplicationWindow,
40    ssid: &str,
41    is_eap: bool,
42    on_connection_success: Rc<dyn Fn()>,
43) {
44    let dialog = Dialog::new();
45    dialog.set_title(Some("Connect to Network"));
46    dialog.set_transient_for(Some(parent));
47    dialog.set_modal(true);
48    dialog.add_css_class("diag-buttons");
49
50    let content_area = dialog.content_area();
51    let vbox = GtkBox::new(Orientation::Vertical, 8);
52    vbox.set_margin_top(32);
53    vbox.set_margin_bottom(32);
54    vbox.set_margin_start(48);
55    vbox.set_margin_end(48);
56
57    let user_entry = if is_eap {
58        let user_label = Label::new(Some("Username:"));
59        let user_entry = Entry::new();
60        user_entry.add_css_class("pw-entry");
61        user_entry.set_placeholder_text(Some("email, username, id..."));
62        vbox.append(&user_label);
63        vbox.append(&user_entry);
64        Some(user_entry)
65    } else {
66        None
67    };
68
69    let label = Label::new(Some("Password:"));
70    let entry = Entry::new();
71    entry.add_css_class("pw-entry");
72    entry.set_placeholder_text(Some("Password"));
73    entry.set_visibility(false);
74    vbox.append(&label);
75    vbox.append(&entry);
76
77    let (cert_entry, use_system_certs, browse_btn) = if is_eap {
78        let cert_label = Label::new(Some("CA Certificate (optional):"));
79        cert_label.set_margin_top(8);
80        let cert_entry = Entry::new();
81        cert_entry.add_css_class("pw-entry");
82        cert_entry.set_placeholder_text(Some("/path/to/ca-cert.pem"));
83
84        let cert_hbox = GtkBox::new(Orientation::Horizontal, 8);
85        let browse_btn = Button::with_label("Browse...");
86        browse_btn.add_css_class("cert-browse-btn");
87        cert_hbox.append(&cert_entry);
88        cert_hbox.append(&browse_btn);
89
90        vbox.append(&cert_label);
91        vbox.append(&cert_hbox);
92
93        let system_certs_check = CheckButton::with_label("Use system CA certificates");
94        system_certs_check.set_active(true);
95        system_certs_check.set_margin_top(4);
96        vbox.append(&system_certs_check);
97
98        (Some(cert_entry), Some(system_certs_check), Some(browse_btn))
99    } else {
100        (None, None, None)
101    };
102
103    content_area.append(&vbox);
104
105    let dialog_rc = Rc::new(dialog);
106    let ssid_owned = ssid.to_string();
107    let user_entry_clone = user_entry.clone();
108
109    let status_label = Label::new(Some(""));
110    status_label.add_css_class("status-label");
111    vbox.append(&status_label);
112
113    if let Some(browse_btn) = browse_btn {
114        let cert_entry_for_browse = cert_entry.clone();
115        let dialog_weak = dialog_rc.downgrade();
116        browse_btn.connect_clicked(move |_| {
117            if let Some(parent_dialog) = dialog_weak.upgrade() {
118                let file_dialog = FileChooserDialog::new(
119                    Some("Select CA Certificate"),
120                    Some(&parent_dialog),
121                    FileChooserAction::Open,
122                    &[
123                        ("Cancel", ResponseType::Cancel),
124                        ("Open", ResponseType::Accept),
125                    ],
126                );
127
128                let cert_entry = cert_entry_for_browse.clone();
129                file_dialog.connect_response(move |dialog, response| {
130                    if response == ResponseType::Accept
131                        && let Some(file) = dialog.file()
132                        && let Some(path) = file.path()
133                    {
134                        cert_entry
135                            .as_ref()
136                            .unwrap()
137                            .set_text(&path.to_string_lossy());
138                    }
139                    dialog.close();
140                });
141
142                file_dialog.show();
143            }
144        });
145    }
146
147    {
148        let dialog_rc = dialog_rc.clone();
149        let status_label = status_label.clone();
150        let refresh_callback = on_connection_success.clone();
151        let nm = nm.clone();
152        let cert_entry_clone = cert_entry.clone();
153        let use_system_certs_clone = use_system_certs.clone();
154
155        entry.connect_activate(move |entry| {
156            let pwd = entry.text().to_string();
157
158            let username = user_entry_clone
159                .as_ref()
160                .map(|e| e.text().to_string())
161                .unwrap_or_default();
162
163            let cert_path = cert_entry_clone.as_ref().and_then(|e| {
164                let text = e.text().to_string();
165                if text.trim().is_empty() {
166                    None
167                } else {
168                    Some(text)
169                }
170            });
171
172            let use_system_ca = use_system_certs_clone
173                .as_ref()
174                .map(|cb| cb.is_active())
175                .unwrap_or(true);
176
177            let ssid = ssid_owned.clone();
178            let dialog = dialog_rc.clone();
179            let status = status_label.clone();
180            let entry = entry.clone();
181            let user_entry = user_entry_clone.clone();
182            let on_success = refresh_callback.clone();
183            let nm = nm.clone();
184
185            entry.set_sensitive(false);
186            if let Some(ref user_entry) = user_entry {
187                user_entry.set_sensitive(false);
188            }
189            status.set_text("Connecting...");
190
191            glib::MainContext::default().spawn_local(async move {
192                let creds = if is_eap {
193                    let mut opts = EapOptions::new(username, pwd)
194                        .with_method(EapMethod::Peap)
195                        .with_phase2(Phase2::Mschapv2)
196                        .with_system_ca_certs(use_system_ca);
197
198                    if let Some(cert) = cert_path {
199                        opts = opts.with_ca_cert_path(format!("file://{}", cert));
200                    }
201
202                    WifiSecurity::WpaEap { opts }
203                } else {
204                    WifiSecurity::WpaPsk { psk: pwd }
205                };
206
207                debug!("Calling nm.connect() for '{ssid}'");
208                match nm.connect(&ssid, creds).await {
209                    Ok(_) => {
210                        debug!("nm.connect() succeeded!");
211                        status.set_text("✓ Connected!");
212                        on_success();
213                        glib::timeout_future_seconds(1).await;
214                        dialog.close();
215                    }
216                    Err(err) => {
217                        error!("nm.connect() failed: {err}");
218                        let err_str = err.to_string().to_lowercase();
219                        if err_str.contains("authentication")
220                            || err_str.contains("supplicant")
221                            || err_str.contains("password")
222                            || err_str.contains("psk")
223                            || err_str.contains("wrong")
224                        {
225                            status.set_text("Wrong password, try again");
226                            entry.set_text("");
227                            entry.grab_focus();
228                        } else {
229                            status.set_text(&format!("✗ Failed: {err}"));
230                        }
231                        entry.set_sensitive(true);
232                        if let Some(ref user_entry) = user_entry {
233                            user_entry.set_sensitive(true);
234                        }
235                    }
236                }
237            });
238        });
239    }
240
241    {
242        let dialog_rc = dialog_rc.clone();
243        let key_controller = EventControllerKey::new();
244        key_controller.connect_key_pressed(move |_, key, _, _| {
245            if key == gtk::gdk::Key::Escape {
246                dialog_rc.close();
247                Propagation::Stop
248            } else {
249                Propagation::Proceed
250            }
251        });
252        entry.add_controller(key_controller);
253    }
254
255    dialog_rc.show();
256}