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}