Skip to main content

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,
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    // User's custom style.css must be registered after the theme so that it
52    // takes precedence when both run at STYLE_PROVIDER_PRIORITY_USER.
53    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}