nmrs_gui/ui/
mod.rs

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}