nmrs_gui/ui/
header.rs

1use glib::clone;
2use gtk::prelude::*;
3use gtk::STYLE_PROVIDER_PRIORITY_USER;
4use gtk::{glib, Align, Box as GtkBox, HeaderBar, Label, ListBox, Orientation, Switch};
5use std::cell::Cell;
6use std::collections::HashSet;
7use std::rc::Rc;
8
9use nmrs::models;
10
11use crate::ui::networks;
12use crate::ui::networks::NetworksContext;
13use crate::ui::wired_devices;
14
15pub struct ThemeDef {
16    pub key: &'static str,
17    pub name: &'static str,
18    pub css: &'static str,
19}
20
21pub static THEMES: &[ThemeDef] = &[
22    ThemeDef {
23        key: "gruvbox",
24        name: "Gruvbox",
25        css: include_str!("../themes/gruvbox.css"),
26    },
27    ThemeDef {
28        key: "nord",
29        name: "Nord",
30        css: include_str!("../themes/nord.css"),
31    },
32    ThemeDef {
33        key: "dracula",
34        name: "Dracula",
35        css: include_str!("../themes/dracula.css"),
36    },
37    ThemeDef {
38        key: "catppuccin",
39        name: "Catppuccin",
40        css: include_str!("../themes/catppuccin.css"),
41    },
42    ThemeDef {
43        key: "tokyo",
44        name: "Tokyo Night",
45        css: include_str!("../themes/tokyo.css"),
46    },
47];
48
49pub fn build_header(
50    ctx: Rc<NetworksContext>,
51    list_container: &GtkBox,
52    is_scanning: Rc<Cell<bool>>,
53    window: &gtk::ApplicationWindow,
54) -> HeaderBar {
55    let header = HeaderBar::new();
56    header.set_show_title_buttons(false);
57
58    let list_container = list_container.clone();
59
60    let wifi_box = GtkBox::new(Orientation::Horizontal, 6);
61    let wifi_label = Label::new(Some("Wi-Fi"));
62    wifi_label.set_halign(gtk::Align::Start);
63    wifi_label.add_css_class("wifi-label");
64
65    let names: Vec<&str> = THEMES.iter().map(|t| t.name).collect();
66    let dropdown = gtk::DropDown::from_strings(&names);
67
68    if let Some(saved) = crate::theme_config::load_theme() {
69        if let Some(idx) = THEMES.iter().position(|t| t.key == saved.as_str()) {
70            dropdown.set_selected(idx as u32);
71        }
72    }
73
74    dropdown.set_valign(gtk::Align::Center);
75    dropdown.add_css_class("dropdown");
76
77    let window_weak = window.downgrade();
78
79    dropdown.connect_selected_notify(move |dd| {
80        let idx = dd.selected() as usize;
81        if idx >= THEMES.len() {
82            return;
83        }
84
85        let theme = &THEMES[idx];
86
87        if let Some(window) = window_weak.upgrade() {
88            let provider = gtk::CssProvider::new();
89            provider.load_from_data(theme.css);
90
91            let display = gtk::prelude::RootExt::display(&window);
92
93            gtk::style_context_add_provider_for_display(
94                &display,
95                &provider,
96                STYLE_PROVIDER_PRIORITY_USER,
97            );
98
99            crate::theme_config::save_theme(theme.key);
100        }
101    });
102
103    wifi_box.append(&wifi_label);
104    wifi_box.append(&dropdown);
105    header.pack_start(&wifi_box);
106
107    let refresh_btn = gtk::Button::from_icon_name("view-refresh-symbolic");
108    refresh_btn.add_css_class("refresh-btn");
109    refresh_btn.set_tooltip_text(Some("Refresh networks and devices"));
110    header.pack_end(&refresh_btn);
111    refresh_btn.connect_clicked(clone!(
112        #[weak]
113        list_container,
114        #[strong]
115        ctx,
116        #[strong]
117        is_scanning,
118        move |_| {
119            let ctx = ctx.clone();
120            let list_container = list_container.clone();
121            let is_scanning = is_scanning.clone();
122
123            glib::MainContext::default().spawn_local(async move {
124                refresh_networks(ctx, &list_container, &is_scanning).await;
125            });
126        }
127    ));
128
129    let theme_btn = gtk::Button::new();
130    theme_btn.add_css_class("theme-toggle-btn");
131    theme_btn.set_valign(gtk::Align::Center);
132    theme_btn.set_has_frame(false);
133
134    let is_light = window.has_css_class("light-theme");
135    let initial_icon = if is_light {
136        "weather-clear-night-symbolic"
137    } else {
138        "weather-clear-symbolic"
139    };
140    theme_btn.set_icon_name(initial_icon);
141
142    let window_weak = window.downgrade();
143    theme_btn.connect_clicked(move |btn| {
144        if let Some(window) = window_weak.upgrade() {
145            let is_light = window.has_css_class("light-theme");
146
147            if is_light {
148                window.remove_css_class("light-theme");
149                window.add_css_class("dark-theme");
150                btn.set_icon_name("weather-clear-symbolic");
151                crate::theme_config::save_theme("light");
152            } else {
153                window.remove_css_class("dark-theme");
154                window.add_css_class("light-theme");
155                btn.set_icon_name("weather-clear-night-symbolic");
156                crate::theme_config::save_theme("dark");
157            }
158        }
159    });
160
161    header.pack_end(&theme_btn);
162
163    let wifi_switch = Switch::new();
164    wifi_switch.set_valign(gtk::Align::Center);
165    header.pack_end(&wifi_switch);
166    wifi_switch.set_size_request(24, 24);
167
168    header.pack_end(&ctx.status);
169
170    {
171        let list_container = list_container.clone();
172        let wifi_switch = wifi_switch.clone();
173        let ctx = ctx.clone();
174        let is_scanning = is_scanning.clone();
175
176        glib::MainContext::default().spawn_local(async move {
177            ctx.stack.set_visible_child_name("loading");
178            clear_children(&list_container);
179
180            match ctx.nm.wifi_enabled().await {
181                Ok(enabled) => {
182                    wifi_switch.set_active(enabled);
183                    if enabled {
184                        refresh_networks(ctx, &list_container, &is_scanning).await;
185                    }
186                }
187                Err(err) => {
188                    ctx.status
189                        .set_text(&format!("Error fetching networks: {err}"));
190                }
191            }
192        })
193    };
194
195    {
196        let ctx = ctx.clone();
197
198        wifi_switch.connect_active_notify(move |sw| {
199            let ctx = ctx.clone();
200            let list_container = list_container.clone();
201            let sw = sw.clone();
202            let is_scanning = is_scanning.clone();
203
204            glib::MainContext::default().spawn_local(async move {
205                clear_children(&list_container);
206
207                if let Err(err) = ctx.nm.set_wifi_enabled(sw.is_active()).await {
208                    ctx.status.set_text(&format!("Error setting Wi-Fi: {err}"));
209                    return;
210                }
211
212                if sw.is_active() {
213                    if ctx.nm.wait_for_wifi_ready().await.is_ok() {
214                        refresh_networks(ctx, &list_container, &is_scanning).await;
215                    } else {
216                        ctx.status.set_text("Wi-Fi failed to initialize");
217                    }
218                }
219            });
220        });
221    }
222
223    header
224}
225
226pub async fn refresh_networks(
227    ctx: Rc<NetworksContext>,
228    list_container: &GtkBox,
229    is_scanning: &Rc<Cell<bool>>,
230) {
231    if is_scanning.get() {
232        ctx.status.set_text("Scan already in progress");
233        return;
234    }
235    is_scanning.set(true);
236
237    clear_children(list_container);
238    ctx.status.set_text("Scanning...");
239
240    // Fetch wired devices first
241    match ctx.nm.list_wired_devices().await {
242        Ok(wired_devices) => {
243            // eprintln!("Found {} wired devices total", wired_devices.len());
244
245            let available_devices: Vec<_> = wired_devices
246                .into_iter()
247                .filter(|dev| {
248                    let show = matches!(
249                        dev.state,
250                        models::DeviceState::Activated
251                            | models::DeviceState::Disconnected
252                            | models::DeviceState::Prepare
253                            | models::DeviceState::Config
254                    );
255                    /* eprintln!(
256                        "  - {} ({}): {} -> {}",
257                        dev.interface,
258                        dev.device_type,
259                        dev.state,
260                        if show { "SHOW" } else { "HIDE" }
261                    ); */
262                    show
263                })
264                .collect();
265
266            /* eprintln!(
267                "Showing {} available wired devices",
268                available_devices.len()
269            ); */
270
271            if !available_devices.is_empty() {
272                let wired_header = Label::new(Some("Wired"));
273                wired_header.add_css_class("section-header");
274                wired_header.add_css_class("wired-section-header");
275                wired_header.set_halign(Align::Start);
276                wired_header.set_margin_top(8);
277                wired_header.set_margin_bottom(4);
278                wired_header.set_margin_start(12);
279                list_container.append(&wired_header);
280
281                // Create a wired devices context
282                let wired_ctx = wired_devices::WiredDevicesContext {
283                    nm: ctx.nm.clone(),
284                    on_success: ctx.on_success.clone(),
285                    status: ctx.status.clone(),
286                    stack: ctx.stack.clone(),
287                    parent_window: ctx.parent_window.clone(),
288                };
289                let wired_ctx = Rc::new(wired_ctx);
290
291                let wired_list = wired_devices::wired_devices_view(
292                    wired_ctx,
293                    &available_devices,
294                    ctx.wired_details_page.clone(),
295                );
296                wired_list.add_css_class("wired-devices-list");
297                list_container.append(&wired_list);
298
299                let separator = gtk::Separator::new(Orientation::Horizontal);
300                separator.add_css_class("device-separator");
301                separator.set_margin_top(12);
302                separator.set_margin_bottom(12);
303                list_container.append(&separator);
304            }
305        }
306        Err(e) => {
307            eprintln!("Failed to list wired devices: {}", e);
308        }
309    }
310
311    let wireless_header = Label::new(Some("Wireless"));
312    wireless_header.add_css_class("section-header");
313    wireless_header.add_css_class("wireless-section-header");
314    wireless_header.set_halign(Align::Start);
315    wireless_header.set_margin_top(8);
316    wireless_header.set_margin_bottom(4);
317    wireless_header.set_margin_start(12);
318    list_container.append(&wireless_header);
319
320    if let Err(err) = ctx.nm.scan_networks().await {
321        ctx.status.set_text(&format!("Scan failed: {err}"));
322        is_scanning.set(false);
323        return;
324    }
325
326    let mut last_len = 0;
327    for _ in 0..5 {
328        let nets = ctx.nm.list_networks().await.unwrap_or_default();
329        if nets.len() == last_len && last_len > 0 {
330            break;
331        }
332        last_len = nets.len();
333        glib::timeout_future_seconds(1).await;
334    }
335
336    match ctx.nm.list_networks().await {
337        Ok(mut nets) => {
338            let current_conn = ctx.nm.current_connection_info().await;
339            let (current_ssid, current_band) = if let Some((ssid, freq)) = current_conn {
340                let ssid_str = ssid.clone();
341                let band: Option<String> = freq
342                    .and_then(crate::ui::freq_to_band)
343                    .map(|s| s.to_string());
344                (Some(ssid_str), band)
345            } else {
346                (None, None)
347            };
348
349            nets.sort_by(|a, b| b.strength.unwrap_or(0).cmp(&a.strength.unwrap_or(0)));
350
351            let mut seen_combinations = HashSet::new();
352            nets.retain(|net| {
353                let band = net.frequency.and_then(crate::ui::freq_to_band);
354                let key = (net.ssid.clone(), band);
355                seen_combinations.insert(key)
356            });
357
358            ctx.status.set_text("");
359
360            let list: ListBox = networks::networks_view(
361                ctx.clone(),
362                &nets,
363                current_ssid.as_deref(),
364                current_band.as_deref(),
365            );
366            list_container.append(&list);
367            ctx.stack.set_visible_child_name("networks");
368        }
369        Err(err) => ctx
370            .status
371            .set_text(&format!("Error fetching networks: {err}")),
372    }
373
374    is_scanning.set(false);
375}
376
377pub fn clear_children(container: &gtk::Box) {
378    let mut child = container.first_child();
379    while let Some(widget) = child {
380        child = widget.next_sibling();
381        container.remove(&widget);
382    }
383}
384
385/// Refresh the network list WITHOUT triggering a new scan.
386/// This is useful for live updates when the network list changes
387/// (e.g., wired device state changes, AP added/removed).
388pub async fn refresh_networks_no_scan(
389    ctx: Rc<NetworksContext>,
390    list_container: &GtkBox,
391    is_scanning: &Rc<Cell<bool>>,
392) {
393    if is_scanning.get() {
394        // Don't interfere with an ongoing scan or refresh
395        return;
396    }
397
398    // Set flag to prevent concurrent refreshes
399    is_scanning.set(true);
400
401    clear_children(list_container);
402
403    // Fetch wired devices first
404    if let Ok(wired_devices) = ctx.nm.list_wired_devices().await {
405        // eprintln!("Found {} wired devices total", wired_devices.len());
406
407        // Filter out unavailable devices to reduce clutter
408        let available_devices: Vec<_> = wired_devices
409            .into_iter()
410            .filter(|dev| {
411                let show = matches!(
412                    dev.state,
413                    models::DeviceState::Activated
414                        | models::DeviceState::Disconnected
415                        | models::DeviceState::Prepare
416                        | models::DeviceState::Config
417                        | models::DeviceState::Unmanaged
418                );
419                /* eprintln!(
420                    "  - {} ({}): {} -> {}",
421                    dev.interface,
422                    dev.device_type,
423                    dev.state,
424                    if show { "SHOW" } else { "HIDE" }
425                ); */
426                show
427            })
428            .collect();
429
430        /* eprintln!(
431            "Showing {} available wired devices",
432            available_devices.len()
433        );*/
434
435        if !available_devices.is_empty() {
436            let wired_header = Label::new(Some("Wired"));
437            wired_header.add_css_class("section-header");
438            wired_header.add_css_class("wired-section-header");
439            wired_header.set_halign(Align::Start);
440            wired_header.set_margin_top(8);
441            wired_header.set_margin_bottom(4);
442            wired_header.set_margin_start(12);
443            list_container.append(&wired_header);
444
445            let wired_ctx = wired_devices::WiredDevicesContext {
446                nm: ctx.nm.clone(),
447                on_success: ctx.on_success.clone(),
448                status: ctx.status.clone(),
449                stack: ctx.stack.clone(),
450                parent_window: ctx.parent_window.clone(),
451            };
452            let wired_ctx = Rc::new(wired_ctx);
453
454            let wired_list = wired_devices::wired_devices_view(
455                wired_ctx,
456                &available_devices,
457                ctx.wired_details_page.clone(),
458            );
459            wired_list.add_css_class("wired-devices-list");
460            list_container.append(&wired_list);
461
462            let separator = gtk::Separator::new(Orientation::Horizontal);
463            separator.add_css_class("device-separator");
464            separator.set_margin_top(12);
465            separator.set_margin_bottom(12);
466            list_container.append(&separator);
467        }
468    }
469
470    let wireless_header = Label::new(Some("Wireless"));
471    wireless_header.add_css_class("section-header");
472    wireless_header.add_css_class("wireless-section-header");
473    wireless_header.set_halign(Align::Start);
474    wireless_header.set_margin_top(8);
475    wireless_header.set_margin_bottom(4);
476    wireless_header.set_margin_start(12);
477    list_container.append(&wireless_header);
478
479    match ctx.nm.list_networks().await {
480        Ok(mut nets) => {
481            let current_conn = ctx.nm.current_connection_info().await;
482            let (current_ssid, current_band) = if let Some((ssid, freq)) = current_conn {
483                let ssid_str = ssid.clone();
484                let band: Option<String> = freq
485                    .and_then(crate::ui::freq_to_band)
486                    .map(|s| s.to_string());
487                (Some(ssid_str), band)
488            } else {
489                (None, None)
490            };
491
492            nets.sort_by(|a, b| b.strength.unwrap_or(0).cmp(&a.strength.unwrap_or(0)));
493
494            let mut seen_combinations = HashSet::new();
495            nets.retain(|net| {
496                let band = net.frequency.and_then(crate::ui::freq_to_band);
497                let key = (net.ssid.clone(), band);
498                seen_combinations.insert(key)
499            });
500
501            let list: ListBox = networks::networks_view(
502                ctx.clone(),
503                &nets,
504                current_ssid.as_deref(),
505                current_band.as_deref(),
506            );
507            list_container.append(&list);
508            ctx.stack.set_visible_child_name("networks");
509        }
510        Err(err) => {
511            ctx.status
512                .set_text(&format!("Error fetching networks: {err}"));
513        }
514    }
515
516    // Release the lock
517    is_scanning.set(false);
518}