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