1pub mod connect;
2pub mod header;
3pub mod network_page;
4pub mod networks;
5pub mod wired_devices;
6pub mod wired_page;
7
8use gtk::prelude::*;
9use gtk::{
10 Application, ApplicationWindow, Box as GtkBox, Label, Orientation,
11 STYLE_PROVIDER_PRIORITY_USER, ScrolledWindow, Spinner, Stack, pango::EllipsizeMode,
12};
13use std::cell::Cell;
14use std::rc::Rc;
15
16use crate::ui::header::THEMES;
17
18type Callback = Rc<dyn Fn()>;
19type CallbackCell = Rc<std::cell::RefCell<Option<Callback>>>;
20
21pub fn freq_to_band(freq: u32) -> Option<&'static str> {
22 match freq {
23 2400..=2500 => Some("2.4GHz"),
24 5150..=5925 => Some("5GHz"),
25 5926..=7125 => Some("6GHz"),
26 _ => None,
27 }
28}
29
30pub fn build_ui(app: &Application) {
31 let win = ApplicationWindow::new(app);
32 win.set_title(Some(""));
33 win.set_default_size(100, 600);
34
35 if let Some(key) = crate::theme_config::load_theme()
36 && let Some(theme) = THEMES.iter().find(|t| t.key == key.as_str())
37 {
38 let provider = gtk::CssProvider::new();
39 provider.load_from_data(theme.css);
40
41 let display = gtk::prelude::RootExt::display(&win);
42 gtk::style_context_add_provider_for_display(
43 &display,
44 &provider,
45 STYLE_PROVIDER_PRIORITY_USER,
46 );
47
48 win.add_css_class("dark-theme");
49 }
50
51 crate::style::load_user_css();
54
55 let vbox = GtkBox::new(Orientation::Vertical, 0);
56 let status = Label::new(None);
57 status.set_xalign(0.0);
58 status.set_ellipsize(EllipsizeMode::End);
59 status.set_max_width_chars(36);
60 let list_container = GtkBox::new(Orientation::Vertical, 0);
61 let stack = Stack::new();
62 let is_scanning = Rc::new(Cell::new(false));
63
64 let spinner = Spinner::new();
65 spinner.set_halign(gtk::Align::Center);
66 spinner.set_valign(gtk::Align::Center);
67 spinner.set_property("width-request", 24i32);
68 spinner.set_property("height-request", 24i32);
69 spinner.add_css_class("loading-spinner");
70 spinner.start();
71
72 stack.add_named(&spinner, Some("loading"));
73 stack.set_visible_child_name("loading");
74
75 let status_clone = status.clone();
76 let list_container_clone = list_container.clone();
77 let stack_clone = stack.clone();
78 let win_clone = win.clone();
79 let is_scanning_clone = is_scanning.clone();
80 let vbox_clone = vbox.clone();
81
82 glib::MainContext::default().spawn_local(async move {
83 match nmrs::NetworkManager::new().await {
84 Ok(nm) => {
85 let nm = Rc::new(nm);
86
87 let details_page = Rc::new(network_page::NetworkPage::new(&stack_clone));
88 let details_scroller = ScrolledWindow::new();
89 details_scroller.set_policy(gtk::PolicyType::Never, gtk::PolicyType::Automatic);
90 details_scroller.set_child(Some(details_page.widget()));
91 stack_clone.add_named(&details_scroller, Some("details"));
92
93 let wired_details_page = Rc::new(wired_page::WiredPage::new(&stack_clone));
94 let wired_details_scroller = ScrolledWindow::new();
95 wired_details_scroller
96 .set_policy(gtk::PolicyType::Never, gtk::PolicyType::Automatic);
97 wired_details_scroller.set_child(Some(wired_details_page.widget()));
98 stack_clone.add_named(&wired_details_scroller, Some("wired-details"));
99
100 let on_success: Rc<dyn Fn()> = {
101 let list_container = list_container_clone.clone();
102 let is_scanning = is_scanning_clone.clone();
103 let nm = nm.clone();
104 let status = status_clone.clone();
105 let stack = stack_clone.clone();
106 let parent_window = win_clone.clone();
107 let details_page = details_page.clone();
108 let wired_details_page = wired_details_page.clone();
109
110 let on_success_cell: CallbackCell = Rc::new(std::cell::RefCell::new(None));
111 let on_success_cell_clone = on_success_cell.clone();
112
113 let callback = Rc::new(move || {
114 let list_container = list_container.clone();
115 let is_scanning = is_scanning.clone();
116 let nm = nm.clone();
117 let status = status.clone();
118 let stack = stack.clone();
119 let parent_window = parent_window.clone();
120 let on_success_cell = on_success_cell.clone();
121 let details_page = details_page.clone();
122 let wired_details_page = wired_details_page.clone();
123
124 glib::MainContext::default().spawn_local(async move {
125 let callback = on_success_cell.borrow().as_ref().map(|cb| cb.clone());
126 let refresh_ctx = Rc::new(networks::NetworksContext {
127 nm,
128 on_success: callback.unwrap_or_else(|| Rc::new(|| {})),
129 status,
130 stack,
131 parent_window,
132 details_page: details_page.clone(),
133 wired_details_page: wired_details_page.clone(),
134 });
135 header::refresh_networks(refresh_ctx, &list_container, &is_scanning)
136 .await;
137 });
138 }) as Rc<dyn Fn()>;
139
140 *on_success_cell_clone.borrow_mut() = Some(callback.clone());
141
142 callback
143 };
144
145 let ctx = Rc::new(networks::NetworksContext {
146 nm: nm.clone(),
147 on_success: on_success.clone(),
148 status: status_clone.clone(),
149 stack: stack_clone.clone(),
150 parent_window: win_clone.clone(),
151 details_page: details_page.clone(),
152 wired_details_page,
153 });
154
155 details_page.set_on_success(on_success);
156
157 let header = header::build_header(
158 ctx.clone(),
159 &list_container_clone,
160 is_scanning_clone.clone(),
161 &win_clone,
162 );
163 vbox_clone.prepend(&header);
164
165 {
166 let nm_device_monitor = nm.clone();
167 let list_container_device = list_container_clone.clone();
168 let is_scanning_device = is_scanning_clone.clone();
169 let ctx_device = ctx.clone();
170 let pending_device_refresh = Rc::new(std::cell::RefCell::new(false));
171
172 glib::MainContext::default().spawn_local(async move {
173 loop {
174 let ctx_device_clone = ctx_device.clone();
175 let list_container_clone = list_container_device.clone();
176 let is_scanning_clone = is_scanning_device.clone();
177 let pending_device_refresh_clone = pending_device_refresh.clone();
178
179 let result = nm_device_monitor
180 .monitor_device_changes(move || {
181 let ctx = ctx_device_clone.clone();
182 let list_container = list_container_clone.clone();
183 let is_scanning = is_scanning_clone.clone();
184 let pending_refresh = pending_device_refresh_clone.clone();
185
186 if pending_refresh.replace(true) {
187 return;
188 }
189
190 glib::MainContext::default().spawn_local(async move {
191 glib::timeout_future_seconds(3).await;
192 *pending_refresh.borrow_mut() = false;
193
194 let current_page = ctx.stack.visible_child_name();
195 let on_networks_page =
196 current_page.as_deref() == Some("networks");
197
198 if !is_scanning.get() && on_networks_page {
199 header::refresh_networks_no_scan(
200 ctx,
201 &list_container,
202 &is_scanning,
203 )
204 .await;
205 }
206 });
207 })
208 .await;
209
210 if let Err(e) = result {
211 eprintln!("Device monitoring error: {}, restarting in 5s...", e)
212 }
213 glib::timeout_future_seconds(5).await;
214 }
215 });
216 }
217
218 {
219 let nm_network_monitor = nm.clone();
220 let list_container_network = list_container_clone.clone();
221 let is_scanning_network = is_scanning_clone.clone();
222 let ctx_network = ctx.clone();
223 let pending_network_refresh = Rc::new(std::cell::RefCell::new(false));
224
225 glib::MainContext::default().spawn_local(async move {
226 loop {
227 let ctx_network_clone = ctx_network.clone();
228 let list_container_clone = list_container_network.clone();
229 let is_scanning_clone = is_scanning_network.clone();
230 let pending_network_refresh_clone = pending_network_refresh.clone();
231
232 let result = nm_network_monitor
233 .monitor_network_changes(move || {
234 let ctx = ctx_network_clone.clone();
235 let list_container = list_container_clone.clone();
236 let is_scanning = is_scanning_clone.clone();
237 let pending_refresh = pending_network_refresh_clone.clone();
238
239 if pending_refresh.replace(true) {
240 return;
241 }
242
243 glib::MainContext::default().spawn_local(async move {
244 glib::timeout_future_seconds(8).await;
245 *pending_refresh.borrow_mut() = false;
246
247 let current_page = ctx.stack.visible_child_name();
248 let on_networks_page =
249 current_page.as_deref() == Some("networks");
250
251 if !is_scanning.get() && on_networks_page {
252 header::refresh_networks_no_scan(
253 ctx,
254 &list_container,
255 &is_scanning,
256 )
257 .await;
258 }
259 });
260 })
261 .await;
262
263 if let Err(e) = result {
264 eprintln!("Network monitoring error: {}, restarting in 5s...", e)
265 }
266 glib::timeout_future_seconds(5).await;
267 }
268 });
269 }
270 }
271 Err(err) => {
272 status_clone.set_text(&format!("Failed to initialize: {err}"));
273 }
274 }
275 });
276
277 let networks_scroller = ScrolledWindow::new();
278 networks_scroller.set_vexpand(true);
279 networks_scroller.set_policy(gtk::PolicyType::Never, gtk::PolicyType::Automatic);
280 networks_scroller.set_child(Some(&list_container));
281
282 stack.add_named(&networks_scroller, Some("networks"));
283
284 stack.set_vexpand(true);
285 vbox.append(&stack);
286
287 win.set_child(Some(&vbox));
288 win.show();
289}