From 1db78e34f55030c5adef25b253cf4e2b94ae9c2d Mon Sep 17 00:00:00 2001 From: rummanz Date: Tue, 11 Nov 2025 13:18:39 +0100 Subject: [PATCH 1/7] Added a wifi selection popover menu using nmcli as backend. --- src/panel/widgets/network.cpp | 492 ++++++++++++++++++++++++---------- src/panel/widgets/network.hpp | 25 +- src/util/wf-popover.hpp | 37 ++- 3 files changed, 387 insertions(+), 167 deletions(-) diff --git a/src/panel/widgets/network.cpp b/src/panel/widgets/network.cpp index 0ed71819..6cacbac8 100644 --- a/src/panel/widgets/network.cpp +++ b/src/panel/widgets/network.cpp @@ -3,6 +3,9 @@ #include #include #include +#include +#include +#include #define NM_DBUS_NAME "org.freedesktop.NetworkManager" #define ACTIVE_CONNECTION "PrimaryConnection" @@ -24,6 +27,8 @@ void WfNetworkConnectionInfo::spawn_control_center(DBusProxy& nm) Glib::spawn_command_line_async(command); } +/* --- ConnectionInfo implementations (same as before) --- */ + struct NoConnectionInfo : public WfNetworkConnectionInfo { std::string get_icon_name(WfConnectionState state) @@ -31,23 +36,10 @@ struct NoConnectionInfo : public WfNetworkConnectionInfo return "network-offline-symbolic"; } - int get_connection_strength() - { - return 0; - } - - std::string get_strength_str() - { - return "none"; - } - - std::string get_ip() - { - return "127.0.0.1"; - } - - virtual ~NoConnectionInfo() - {} + int get_connection_strength() { return 0; } + std::string get_strength_str() { return "none"; } + std::string get_ip() { return "127.0.0.1"; } + virtual ~NoConnectionInfo() {} }; struct WifiConnectionInfo : public WfNetworkConnectionInfo @@ -102,124 +94,65 @@ struct WifiConnectionInfo : public WfNetworkConnectionInfo { int value = get_strength(); - if (value > 80) - { - return "excellent"; - } - - if (value > 55) - { - return "good"; - } - - if (value > 30) - { - return "ok"; - } - - if (value > 5) - { - return "weak"; - } - + if (value > 80) return "excellent"; + if (value > 55) return "good"; + if (value > 30) return "ok"; + if (value > 5) return "weak"; return "none"; } virtual std::string get_icon_name(WfConnectionState state) { if ((state <= CSTATE_ACTIVATING) || (state == CSTATE_DEACTIVATING)) - { return "network-wireless-acquiring-symbolic"; - } if (state == CSTATE_DEACTIVATED) - { return "network-wireless-disconnected-symbolic"; - } if (ap) - { return "network-wireless-signal-" + get_strength_str() + "-symbolic"; - } else - { + else return "network-wireless-no-route-symbolic"; - } } virtual int get_connection_strength() { - if (ap) - { - return get_strength(); - } else - { - return 100; - } - } - - virtual std::string get_ip() - { - return "0.0.0.0"; + if (ap) return get_strength(); + return 100; } - virtual ~WifiConnectionInfo() - {} + virtual std::string get_ip() { return "0.0.0.0"; } + virtual ~WifiConnectionInfo() {} }; struct EthernetConnectionInfo : public WfNetworkConnectionInfo { DBusProxy ap; - EthernetConnectionInfo(const DBusConnection& connection, std::string path) - {} + EthernetConnectionInfo(const DBusConnection& connection, std::string path) {} virtual std::string get_icon_name(WfConnectionState state) { if ((state <= CSTATE_ACTIVATING) || (state == CSTATE_DEACTIVATING)) - { return "network-wired-acquiring-symbolic"; - } if (state == CSTATE_DEACTIVATED) - { return "network-wired-disconnected-symbolic"; - } return "network-wired-symbolic"; } - std::string get_connection_name() - { - return "Ethernet - " + connection_name; - } - - std::string get_strength_str() - { - return "excellent"; - } - - virtual int get_connection_strength() - { - return 100; - } - - virtual std::string get_ip() - { - return "0.0.0.0"; - } - - virtual ~EthernetConnectionInfo() - {} + std::string get_connection_name() { return "Ethernet - " + connection_name; } + std::string get_strength_str() { return "excellent"; } + virtual int get_connection_strength() { return 100; } + virtual std::string get_ip() { return "0.0.0.0"; } + virtual ~EthernetConnectionInfo() {} }; - -/* TODO: handle Connectivity */ +/* --- connection state helpers --- */ static WfConnectionState get_connection_state(DBusProxy connection) { - if (!connection) - { - return CSTATE_DEACTIVATED; - } + if (!connection) return CSTATE_DEACTIVATED; Glib::Variant state; connection->get_cached_property(state, "State"); @@ -228,55 +161,60 @@ static WfConnectionState get_connection_state(DBusProxy connection) void WayfireNetworkInfo::update_icon() { - auto icon_name = info->get_icon_name( - get_connection_state(active_connection_proxy)); - icon.set_from_icon_name(icon_name); -} - -struct status_color -{ - int point; - Gdk::RGBA rgba; -} status_colors[] = { - {0, Gdk::RGBA{"#ff0000"}}, - {25, Gdk::RGBA{"#ff0000"}}, - {40, Gdk::RGBA{"#ffff55"}}, - {100, Gdk::RGBA{"#00ff00"}}, -}; + // Defensive checks to avoid crashes during early init or missing DBus info + if (!info) + { + // ensure info is always valid + info = std::unique_ptr(new NoConnectionInfo()); + info->connection_name = "No connection"; + std::cerr << "[network] update_icon: info was null, created NoConnectionInfo" << std::endl; + } -#define MAX_COLORS (sizeof(status_colors) / sizeof(status_color)) + // get connection state safely + WfConnectionState state = CSTATE_DEACTIVATED; + try + { + state = get_connection_state(active_connection_proxy); + } + catch (...) + { + // shouldn't normally throw, but guard anyway + std::cerr << "[network] update_icon: exception in get_connection_state()" << std::endl; + state = CSTATE_DEACTIVATED; + } -static Gdk::RGBA get_color_for_pc(int pc) -{ - for (int i = MAX_COLORS - 2; i >= 0; i--) + // Attempt to ask the info object for icon name, but guard unexpected exceptions + std::string icon_name; + try { - if (status_colors[i].point <= pc) - { - auto& r1 = status_colors[i].rgba; - auto& r2 = status_colors[i + 1].rgba; - - double a = 1.0 * (pc - status_colors[i].point) / - (status_colors[i + 1].point - status_colors[i].point); - Gdk::RGBA result; - result.set_rgba( - r1.get_red() * (1 - a) + r2.get_red() * a, - r1.get_green() * (1 - a) + r2.get_green() * a, - r1.get_blue() * (1 - a) + r2.get_blue() * a, - r1.get_alpha() * (1 - a) + r2.get_alpha() * a); - - return result; - } + icon_name = info->get_icon_name(state); + } + catch (const std::exception& e) + { + std::cerr << "[network] update_icon: exception in get_icon_name(): " << e.what() << std::endl; + icon_name = "network-offline-symbolic"; + } + catch (...) + { + std::cerr << "[network] update_icon: unknown exception in get_icon_name()" << std::endl; + icon_name = "network-offline-symbolic"; } - return Gdk::RGBA{"#ffffff"}; + // Final sanity: if icon_name is empty, use fallback + if (icon_name.empty()) + icon_name = "network-offline-symbolic"; + + icon.set_from_icon_name(icon_name); } + +/* optional color helper omitted (not used) */ + void WayfireNetworkInfo::update_status() { std::string description = info->get_connection_name(); - status.set_text(description); - button.set_tooltip_text(description); + button->set_tooltip_text(description); status.get_style_context()->remove_class("excellent"); status.get_style_context()->remove_class("good"); @@ -332,12 +270,10 @@ void WayfireNetworkInfo::update_active_connection() { std::cout << "Unimplemented: bluetooth connection" << std::endl; set_no_connection(); - // TODO } else { std::cout << "Unimplemented: unknown connection type" << std::endl; set_no_connection(); - // TODO: implement Unknown connection } Glib::Variant vname; @@ -388,6 +324,273 @@ bool WayfireNetworkInfo::setup_dbus() return true; } +/* --- nmcli helper and parser --- */ + +struct WifiEntry { std::string ssid; int signal; std::string security; }; + +static std::string run_cmd_capture_stdout(const std::string& cmd, int *exit_status = nullptr) +{ + std::string stdout_out; + int status = 0; + try + { + Glib::spawn_command_line_sync(cmd, &stdout_out, nullptr, &status); + } + catch (const Glib::SpawnError& e) + { + std::cerr << "spawn error: " << e.what() << std::endl; + if (exit_status) *exit_status = -1; + return std::string(); + } + + if (exit_status) *exit_status = status; + return stdout_out; +} + +static std::vector parse_nmcli_wifi_list(const std::string& data) +{ + std::vector out; + std::istringstream iss(data); + std::string line; + while (std::getline(iss, line)) + { + if (line.empty()) continue; + // nmcli -t uses ':' as separator. SSIDs may contain ':' rarely but we keep it simple here. + size_t p1 = line.find(':'); + if (p1 == std::string::npos) continue; + size_t p2 = line.find(':', p1 + 1); + if (p2 == std::string::npos) continue; + std::string ssid = line.substr(0, p1); + std::string sigs = line.substr(p1 + 1, p2 - p1 - 1); + std::string sec = line.substr(p2 + 1); + int sig = 0; + try { sig = std::stoi(sigs); } catch (...) { sig = 0; } + // trim newline\r + ssid.erase(std::remove_if(ssid.begin(), ssid.end(), [](unsigned char c){ return c == '\r' || c == '\n'; }), ssid.end()); + out.push_back({ssid, sig, sec}); + } + return out; +} + +/* --- Popover handling --- */ + +void WayfireNetworkInfo::show_wifi_popover() +{ + auto popover = button->get_popover(); + + // ensure popover content is set + if (!popover->get_child()) + { + pop_list_box.set_orientation(Gtk::Orientation::VERTICAL); + pop_list_box.set_spacing(4); + pop_list_box.set_margin(6); + + pop_scrolled.set_child(pop_list_box); + pop_scrolled.set_min_content_height(200); + pop_scrolled.set_min_content_width(320); + + popover_box.set_orientation(Gtk::Orientation::VERTICAL); + popover_box.set_spacing(6); + popover_box.append(pop_scrolled); + + // prepare password box (hidden until needed) + pop_pass_box.set_orientation(Gtk::Orientation::VERTICAL); + pop_pass_box.set_spacing(4); + pop_pass_box.set_margin_top(6); + pop_pass_box.set_margin_bottom(6); + + popover->set_child(popover_box); + popover->set_size_request(320, 240); + popover->get_style_context()->add_class("network-popover"); + } + + populate_wifi_list(); + + // show + button->set_keyboard_interactive(false); + popover->popup(); +} + +void WayfireNetworkInfo::populate_wifi_list() +{ + // clear pop_list_box children + auto children = pop_list_box.get_children(); + for (auto *c : children) + pop_list_box.remove(*c); + + // header + auto header = Gtk::make_managed("Available Wi-Fi networks"); + header->set_margin_bottom(6); + pop_list_box.append(*header); + + // run nmcli + int st = 0; + std::string out = run_cmd_capture_stdout("nmcli -t -f SSID,SIGNAL,SECURITY dev wifi list", &st); + if (out.empty()) + { + auto lbl = Gtk::make_managed("Could not list networks (is nmcli installed?)"); + lbl->set_margin(6); + pop_list_box.append(*lbl); + + auto open_btn = Gtk::make_managed("Open network settings"); + open_btn->signal_clicked().connect([this]() { + info->spawn_control_center(nm_proxy); + button->get_popover()->popdown(); + }); + pop_list_box.append(*open_btn); + return; + } + + auto entries = parse_nmcli_wifi_list(out); + if (entries.empty()) + { + auto lbl = Gtk::make_managed("No networks found"); + lbl->set_margin(6); + pop_list_box.append(*lbl); + return; + } + + // sort by signal desc, then unique SSID + std::sort(entries.begin(), entries.end(), [](const WifiEntry &a, const WifiEntry &b) { + return a.signal > b.signal; + }); + + std::string last_ssid; + for (auto &e : entries) + { + if (e.ssid == last_ssid) continue; // dedupe identical SSID rows + last_ssid = e.ssid; + + std::ostringstream label; + label << (e.ssid.empty() ? "" : e.ssid) << " (" << e.signal << "%)"; + if (!e.security.empty()) label << " [" << e.security << "]"; + + auto btn = Gtk::make_managed(label.str()); + btn->set_halign(Gtk::Align::FILL); + + // capture ssid+security by value + btn->signal_clicked().connect([this, ssid = e.ssid, sec = e.security]() { + bool secured = (sec.find("WPA") != std::string::npos) || + (sec.find("WEP") != std::string::npos) || + (sec.find("RSN") != std::string::npos); + if (!secured) + { + attempt_connect_ssid(ssid, ""); + button->get_popover()->popdown(); + } + else + { + show_password_prompt_for(ssid, sec); + } + }); + + pop_list_box.append(*btn); + } + + // small spacer and status label + pop_status_label.set_text(""); + pop_status_label.set_margin_top(6); + pop_list_box.append(pop_status_label); +} + +void WayfireNetworkInfo::show_password_prompt_for(const std::string& ssid, const std::string& security) +{ + // clear popover_box and replace scroll with a password UI inside popover_box + auto popover = button->get_popover(); + + // remove all children of popover_box + auto pb_children = popover_box.get_children(); + for (auto *c : pb_children) popover_box.remove(*c); + + // title + auto title = Gtk::make_managed("Connect to: " + (ssid.empty() ? "" : ssid)); + title->set_margin_bottom(6); + popover_box.append(*title); + + // password entry + auto pass_label = Gtk::make_managed("Password:"); + popover_box.append(*pass_label); + + auto entry = Gtk::make_managed(); + entry->set_visibility(false); + entry->set_hexpand(true); + popover_box.append(*entry); + + // buttons box + auto hbox = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 6); + hbox->set_halign(Gtk::Align::END); + auto cancel_btn = Gtk::make_managed("Cancel"); + auto connect_btn = Gtk::make_managed("Connect"); + + cancel_btn->signal_clicked().connect([this]() { + // restore list + auto pb_children2 = popover_box.get_children(); + for (auto *c : pb_children2) popover_box.remove(*c); + popover_box.append(pop_scrolled); + populate_wifi_list(); + }); + + connect_btn->signal_clicked().connect([this, ssid, entry]() { + std::string pwd = entry->get_text(); + if (pwd.empty()) + { + pop_status_label.set_text("Password cannot be empty"); + return; + } + pop_status_label.set_text("Connecting..."); + // attempt connect (synchronous call to nmcli; it's quick) + attempt_connect_ssid(ssid, pwd); + button->get_popover()->popdown(); + }); + + hbox->append(*cancel_btn); + hbox->append(*connect_btn); + popover_box.append(*hbox); + + // status label + pop_status_label.set_text(""); + popover_box.append(pop_status_label); + + popover->present(); // show updated content +} + +void WayfireNetworkInfo::attempt_connect_ssid(const std::string& ssid, const std::string& password) +{ + if (ssid.empty()) + { + std::cerr << "Empty SSID, aborting connect" << std::endl; + return; + } + + std::string cmd = "nmcli device wifi connect \"" + ssid + "\""; + if (!password.empty()) + { + // Escape double quotes in password naively + std::string esc = password; + size_t pos = 0; + while ((pos = esc.find('"', pos)) != std::string::npos) { esc.replace(pos, 1, "\\\""); pos += 2; } + cmd += " password \"" + esc + "\""; + } + + int exit_status = 0; + std::string output = run_cmd_capture_stdout(cmd, &exit_status); + if (exit_status != 0) + { + std::cerr << "nmcli connect failed: " << output << std::endl; + // if popover exists, show temporary error + pop_status_label.set_text("Failed to connect. Check password or settings."); + } + else + { + pop_status_label.set_text("Connection started."); + } + + // trigger an update of state (NetworkManager DBus will also update via signals) + update_active_connection(); +} + +/* --- widget lifecycle --- */ + void WayfireNetworkInfo::on_click() { if ((std::string)click_command_opt != "default") @@ -395,7 +598,7 @@ void WayfireNetworkInfo::on_click() Glib::spawn_command_line_async((std::string)click_command_opt); } else { - info->spawn_control_center(nm_proxy); + show_wifi_popover(); } } @@ -407,16 +610,19 @@ void WayfireNetworkInfo::init(Gtk::Box *container) return; } - auto style = button.get_style_context(); - style->add_class("flat"); - style->add_class("network"); + // create WayfireMenuButton + button = std::make_unique("panel"); + button->get_style_context()->add_class("network"); + button->get_children()[0]->get_style_context()->add_class("flat"); - container->append(button); - button.set_child(button_content); - button.get_style_context()->add_class("flat"); + update_icon(); + button->set_child(icon); + container->append(*button); - button.signal_clicked().connect( - sigc::mem_fun(*this, &WayfireNetworkInfo::on_click)); + button->set_tooltip_text("Click to open Wi-Fi selector"); + + // connect click to show popover (also support middle-click etc if needed) + button->signal_clicked().connect(sigc::mem_fun(*this, &WayfireNetworkInfo::on_click)); button_content.set_valign(Gtk::Align::CENTER); button_content.append(icon); @@ -448,8 +654,6 @@ void WayfireNetworkInfo::handle_config_reload() } } - // TODO: show IP for "full" status - update_icon(); update_status(); } diff --git a/src/panel/widgets/network.hpp b/src/panel/widgets/network.hpp index 1ef806ef..1614d3de 100644 --- a/src/panel/widgets/network.hpp +++ b/src/panel/widgets/network.hpp @@ -6,8 +6,15 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include "../widget.hpp" +#include "../util/wf-popover.hpp" // assumes WayfireMenuButton header lives here using DBusConnection = Glib::RefPtr; using DBusProxy = Glib::RefPtr; @@ -56,11 +63,19 @@ class WayfireNetworkInfo : public WayfireWidget std::unique_ptr info; - Gtk::Button button; + // Use WayfireMenuButton like notification-center + std::unique_ptr button; Gtk::Box button_content; Gtk::Image icon; Gtk::Label status; + // Popover UI (owned by WayfireMenuButton) + Gtk::Box popover_box; // top-level box in popover + Gtk::ScrolledWindow pop_scrolled; + Gtk::Box pop_list_box; // list of networks + Gtk::Box pop_pass_box; // inline password prompt + Gtk::Label pop_status_label; // show temporary messages + bool enabled = true; WfOption status_opt{"panel/network_status"}; WfOption status_color_opt{"panel/network_status_use_color"}; @@ -74,6 +89,12 @@ class WayfireNetworkInfo : public WayfireWidget void on_click(); + // wifi helper methods (use nmcli) + void show_wifi_popover(); + void populate_wifi_list(); + void show_password_prompt_for(const std::string& ssid, const std::string& security); + void attempt_connect_ssid(const std::string& ssid, const std::string& password = ""); + public: void update_icon(); void update_status(); @@ -83,4 +104,4 @@ class WayfireNetworkInfo : public WayfireWidget virtual ~WayfireNetworkInfo(); }; -#endif /* end of include guard: WIDGETS_NETWORK_HPP */ +#endif /* end of include guard: WIDGETS_NETWORK_HPP */ \ No newline at end of file diff --git a/src/util/wf-popover.hpp b/src/util/wf-popover.hpp index 17ba17bd..a54c0c24 100644 --- a/src/util/wf-popover.hpp +++ b/src/util/wf-popover.hpp @@ -1,6 +1,7 @@ #ifndef WF_PANEL_POPOVER_HPP #define WF_PANEL_POPOVER_HPP +#include // Only signal.h to avoid incomplete type issues #include #include #include @@ -15,39 +16,33 @@ class WayfireMenuButton : public Gtk::MenuButton bool has_focus = false; WfOption panel_position; - /* Make the menu button active on its AutohideWindow */ void set_active_on_window(); - friend class WayfireAutohidingWindow; - /* Set the has_focus property */ void set_has_focus(bool focus); public: Gtk::Popover m_popover; WayfireMenuButton(const std::string& config_section); - virtual ~WayfireMenuButton() - {} + virtual ~WayfireMenuButton() = default; - /** - * Set whether the popup should grab input focus when opened - * By default, the menu button interacts with the keyboard. - */ void set_keyboard_interactive(bool interactive = true); - - /** @return Whether the menu button interacts with the keyboard */ bool is_keyboard_interactive() const; - - /** @return Whether the popover currently has keyboard focus */ bool is_popover_focused() const; - - /** - * Grab the keyboard focus. - * Also sets the popover to keyboard interactive. - * - * NOTE: this works only if the popover was already opened. - */ void grab_focus(); + + sigc::signal& signal_clicked() { return m_signal_clicked; } + + private: + sigc::signal m_signal_clicked; + + void on_realize() override + { + Gtk::MenuButton::on_realize(); + + // Emit clicked when the popover is shown + m_popover.signal_show().connect([this]() { m_signal_clicked.emit(); }); + } }; -#endif /* end of include guard: WF_PANEL_POPOVER_HPP */ +#endif /* WF_PANEL_POPOVER_HPP */ From f7e631a87f2574031e1cb4fc3b75d05b22c3e696 Mon Sep 17 00:00:00 2001 From: Rumman Zaman Date: Tue, 11 Nov 2025 17:23:05 +0100 Subject: [PATCH 2/7] added comments --- src/util/wf-popover.hpp | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/util/wf-popover.hpp b/src/util/wf-popover.hpp index a54c0c24..3db12e38 100644 --- a/src/util/wf-popover.hpp +++ b/src/util/wf-popover.hpp @@ -1,6 +1,5 @@ #ifndef WF_PANEL_POPOVER_HPP #define WF_PANEL_POPOVER_HPP - #include // Only signal.h to avoid incomplete type issues #include #include @@ -16,8 +15,11 @@ class WayfireMenuButton : public Gtk::MenuButton bool has_focus = false; WfOption panel_position; + /* Make the menu button active on its AutohideWindow */ void set_active_on_window(); + friend class WayfireAutohidingWindow; + /* Set the has_focus property */ void set_has_focus(bool focus); public: @@ -26,9 +28,25 @@ class WayfireMenuButton : public Gtk::MenuButton WayfireMenuButton(const std::string& config_section); virtual ~WayfireMenuButton() = default; + + /** + * Set whether the popup should grab input focus when opened + * By default, the menu button interacts with the keyboard. + */ void set_keyboard_interactive(bool interactive = true); + + /** @return Whether the menu button interacts with the keyboard */ bool is_keyboard_interactive() const; + + /** @return Whether the popover currently has keyboard focus */ bool is_popover_focused() const; + + /** + * Grab the keyboard focus. + * Also sets the popover to keyboard interactive. + * + * NOTE: this works only if the popover was already opened. + */ void grab_focus(); sigc::signal& signal_clicked() { return m_signal_clicked; } From 8bf419067c4223fc59bdaf0c7b9f58c903281711 Mon Sep 17 00:00:00 2001 From: rummanz Date: Fri, 14 Nov 2025 20:06:20 +0100 Subject: [PATCH 3/7] Drop nmcli invocation and added Dbus interface api calls to connect wifi. --- src/panel/widgets/network.cpp | 1311 +++++++++++++++++++-------------- src/panel/widgets/network.hpp | 57 +- 2 files changed, 816 insertions(+), 552 deletions(-) diff --git a/src/panel/widgets/network.cpp b/src/panel/widgets/network.cpp index 6cacbac8..f2ec2d6b 100644 --- a/src/panel/widgets/network.cpp +++ b/src/panel/widgets/network.cpp @@ -1,662 +1,905 @@ #include "network.hpp" -#include +#include #include -#include +#include #include +#include #include +#include +#include #include -#include +#include +#include +#include // sometimes needed for lower-level variant helpers + +#include +#include +#include + +#include +#include + +#include #define NM_DBUS_NAME "org.freedesktop.NetworkManager" #define ACTIVE_CONNECTION "PrimaryConnection" #define STRENGTH "Strength" -std::string WfNetworkConnectionInfo::get_control_center_section(DBusProxy& nm) +struct NetworkInfo +{ + std::string ssid; + std::string path; + int strength = 0; // Wi-Fi signal strength (0–100) + bool secured = false; // True if the network requires authentication +}; + +static std::vector get_available_networks() +{ + std::vector result; + + try + { + auto connection = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SYSTEM); + + auto nm_proxy = Gio::DBus::Proxy::create_for_bus_sync( + Gio::DBus::BusType::SYSTEM, + "org.freedesktop.NetworkManager", + "/org/freedesktop/NetworkManager", + "org.freedesktop.NetworkManager"); + + Glib::VariantContainerBase reply = nm_proxy->call_sync("GetDevices"); + auto device_child = reply.get_child(0); + auto devices_variant = Glib::VariantBase::cast_dynamic>>(device_child); + std::vector device_paths = devices_variant.get(); + + for (const auto &path : device_paths) + { + auto dev_proxy = Gio::DBus::Proxy::create_for_bus_sync( + Gio::DBus::BusType::SYSTEM, + "org.freedesktop.NetworkManager", + path, + "org.freedesktop.NetworkManager.Device"); + + // For glibmm-2.68, we use GetCachedProperty(const Glib::ustring&, Glib::VariantBase&) + Glib::VariantBase dev_type_variant; + dev_proxy->get_cached_property(dev_type_variant, "DeviceType"); // ✅ argument order + + guint32 dev_type = 0; + Glib::Variant vtype = Glib::VariantBase::cast_dynamic>(dev_type_variant); + dev_type = vtype.get(); + + if (dev_type != 2) // NM_DEVICE_TYPE_WIFI + continue; + + auto wifi_proxy = Gio::DBus::Proxy::create_for_bus_sync( + Gio::DBus::BusType::SYSTEM, + "org.freedesktop.NetworkManager", + path, + "org.freedesktop.NetworkManager.Device.Wireless"); + + Glib::VariantContainerBase aps_reply = wifi_proxy->call_sync("GetAllAccessPoints"); + auto ap_child = aps_reply.get_child(0); + auto ap_paths_variant = Glib::VariantBase::cast_dynamic>>(ap_child); + std::vector ap_paths = ap_paths_variant.get(); + + for (const auto &ap_path : ap_paths) + { + auto ap_proxy = Gio::DBus::Proxy::create_for_bus_sync( + Gio::DBus::BusType::SYSTEM, + "org.freedesktop.NetworkManager", + ap_path, + "org.freedesktop.NetworkManager.AccessPoint"); + + Glib::VariantBase ssid_var, wpa_var, rsn_var; + ap_proxy->get_cached_property(ssid_var, "Ssid"); // ✅ reversed order + ap_proxy->get_cached_property(wpa_var, "WpaFlags"); + ap_proxy->get_cached_property(rsn_var, "RsnFlags"); + + std::string ssid; + if (ssid_var) + { + Glib::Variant> ssid_bytes = + Glib::VariantBase::cast_dynamic>>(ssid_var); + auto vec = ssid_bytes.get(); + ssid.assign(vec.begin(), vec.end()); + } + + guint32 wpa_flags = 0, rsn_flags = 0; + if (wpa_var) + wpa_flags = Glib::VariantBase::cast_dynamic>(wpa_var).get(); + if (rsn_var) + rsn_flags = Glib::VariantBase::cast_dynamic>(rsn_var).get(); + + std::string security = (wpa_flags || rsn_flags) ? "Secure" : "Open"; + + if (!ssid.empty()) + result.push_back({ssid, security}); + } + } + } + catch (const Glib::Error &ex) + { + std::cerr << "D-Bus error in get_available_networks(): " << ex.what() << std::endl; + } + + return result; +} + +std::string WfNetworkConnectionInfo::get_control_center_section(DBusProxy &nm) { - Glib::Variant wifi; - nm->get_cached_property(wifi, "WirelessEnabled"); + Glib::Variant wifi; + nm->get_cached_property(wifi, "WirelessEnabled"); - return wifi.get() ? "wifi" : "network"; + return wifi.get() ? "wifi" : "network"; } -void WfNetworkConnectionInfo::spawn_control_center(DBusProxy& nm) +void WfNetworkConnectionInfo::spawn_control_center(DBusProxy &nm) { - std::string command = "env XDG_CURRENT_DESKTOP=GNOME gnome-control-center "; - command += get_control_center_section(nm); + std::string command = "env XDG_CURRENT_DESKTOP=GNOME gnome-control-center "; + command += get_control_center_section(nm); - Glib::spawn_command_line_async(command); + Glib::spawn_command_line_async(command); } /* --- ConnectionInfo implementations (same as before) --- */ struct NoConnectionInfo : public WfNetworkConnectionInfo { - std::string get_icon_name(WfConnectionState state) - { - return "network-offline-symbolic"; - } - - int get_connection_strength() { return 0; } - std::string get_strength_str() { return "none"; } - std::string get_ip() { return "127.0.0.1"; } - virtual ~NoConnectionInfo() {} + std::string get_icon_name(WfConnectionState state) + { + return "network-offline-symbolic"; + } + + int get_connection_strength() { return 0; } + std::string get_strength_str() { return "none"; } + std::string get_ip() { return "127.0.0.1"; } + virtual ~NoConnectionInfo() {} }; struct WifiConnectionInfo : public WfNetworkConnectionInfo { - WayfireNetworkInfo *widget; - DBusProxy ap; - WifiConnectionInfo(const DBusConnection& connection, std::string path, - WayfireNetworkInfo *widget) - { - this->widget = widget; + void scan_networks_async(); + void update_network_list(const std::vector &networks); - ap = Gio::DBus::Proxy::create_sync(connection, NM_DBUS_NAME, path, - "org.freedesktop.NetworkManager.AccessPoint"); + void show_error(const std::string &message); + WayfireNetworkInfo *widget; + DBusProxy ap; - if (ap) - { - ap->signal_properties_changed().connect( - sigc::mem_fun(*this, &WifiConnectionInfo::on_properties_changed)); - } - } + WifiConnectionInfo(const DBusConnection &connection, std::string path, + WayfireNetworkInfo *widget) + { + this->widget = widget; - void on_properties_changed(DBusPropMap changed, DBusPropList invalid) - { - bool needs_refresh = false; - for (auto& prop : changed) - { - if (prop.first == STRENGTH) - { - needs_refresh = true; - } - } + ap = Gio::DBus::Proxy::create_sync( + connection, NM_DBUS_NAME, path, + "org.freedesktop.NetworkManager.AccessPoint"); - if (needs_refresh) - { - widget->update_icon(); - widget->update_status(); - } + if (ap) + { + ap->signal_properties_changed().connect( + sigc::mem_fun(*this, &WifiConnectionInfo::on_properties_changed)); } + } - int get_strength() + void on_properties_changed(DBusPropMap changed, DBusPropList invalid) + { + bool needs_refresh = false; + for (auto &prop : changed) { - assert(ap); - - Glib::Variant vstr; - ap->get_cached_property(vstr, STRENGTH); - - return vstr.get(); + if (prop.first == STRENGTH) + { + needs_refresh = true; + } } - std::string get_strength_str() + if (needs_refresh) { - int value = get_strength(); - - if (value > 80) return "excellent"; - if (value > 55) return "good"; - if (value > 30) return "ok"; - if (value > 5) return "weak"; - return "none"; + widget->update_icon(); + widget->update_status(); } + } - virtual std::string get_icon_name(WfConnectionState state) - { - if ((state <= CSTATE_ACTIVATING) || (state == CSTATE_DEACTIVATING)) - return "network-wireless-acquiring-symbolic"; + int get_strength() + { + assert(ap); - if (state == CSTATE_DEACTIVATED) - return "network-wireless-disconnected-symbolic"; + Glib::Variant vstr; + ap->get_cached_property(vstr, STRENGTH); - if (ap) - return "network-wireless-signal-" + get_strength_str() + "-symbolic"; - else - return "network-wireless-no-route-symbolic"; - } + return vstr.get(); + } - virtual int get_connection_strength() - { - if (ap) return get_strength(); - return 100; - } + std::string get_strength_str() + { + int value = get_strength(); + + if (value > 80) + return "excellent"; + if (value > 55) + return "good"; + if (value > 30) + return "ok"; + if (value > 5) + return "weak"; + return "none"; + } + + virtual std::string get_icon_name(WfConnectionState state) + { + if ((state <= CSTATE_ACTIVATING) || (state == CSTATE_DEACTIVATING)) + return "network-wireless-acquiring-symbolic"; - virtual std::string get_ip() { return "0.0.0.0"; } - virtual ~WifiConnectionInfo() {} + if (state == CSTATE_DEACTIVATED) + return "network-wireless-disconnected-symbolic"; + + if (ap) + return "network-wireless-signal-" + get_strength_str() + "-symbolic"; + else + return "network-wireless-no-route-symbolic"; + } + + virtual int get_connection_strength() + { + if (ap) + return get_strength(); + return 100; + } + + virtual std::string get_ip() { return "0.0.0.0"; } + virtual ~WifiConnectionInfo() {} }; struct EthernetConnectionInfo : public WfNetworkConnectionInfo { - DBusProxy ap; - EthernetConnectionInfo(const DBusConnection& connection, std::string path) {} + DBusProxy ap; + EthernetConnectionInfo(const DBusConnection &connection, std::string path) {} - virtual std::string get_icon_name(WfConnectionState state) - { - if ((state <= CSTATE_ACTIVATING) || (state == CSTATE_DEACTIVATING)) - return "network-wired-acquiring-symbolic"; + virtual std::string get_icon_name(WfConnectionState state) + { + if ((state <= CSTATE_ACTIVATING) || (state == CSTATE_DEACTIVATING)) + return "network-wired-acquiring-symbolic"; - if (state == CSTATE_DEACTIVATED) - return "network-wired-disconnected-symbolic"; + if (state == CSTATE_DEACTIVATED) + return "network-wired-disconnected-symbolic"; - return "network-wired-symbolic"; - } + return "network-wired-symbolic"; + } - std::string get_connection_name() { return "Ethernet - " + connection_name; } - std::string get_strength_str() { return "excellent"; } - virtual int get_connection_strength() { return 100; } - virtual std::string get_ip() { return "0.0.0.0"; } - virtual ~EthernetConnectionInfo() {} + std::string get_connection_name() { return "Ethernet - " + connection_name; } + std::string get_strength_str() { return "excellent"; } + virtual int get_connection_strength() { return 100; } + virtual std::string get_ip() { return "0.0.0.0"; } + virtual ~EthernetConnectionInfo() {} }; /* --- connection state helpers --- */ static WfConnectionState get_connection_state(DBusProxy connection) { - if (!connection) return CSTATE_DEACTIVATED; + if (!connection) + return CSTATE_DEACTIVATED; - Glib::Variant state; - connection->get_cached_property(state, "State"); - return (WfConnectionState)state.get(); + Glib::Variant state; + connection->get_cached_property(state, "State"); + return (WfConnectionState)state.get(); } void WayfireNetworkInfo::update_icon() { - // Defensive checks to avoid crashes during early init or missing DBus info - if (!info) - { - // ensure info is always valid - info = std::unique_ptr(new NoConnectionInfo()); - info->connection_name = "No connection"; - std::cerr << "[network] update_icon: info was null, created NoConnectionInfo" << std::endl; - } - - // get connection state safely - WfConnectionState state = CSTATE_DEACTIVATED; - try - { - state = get_connection_state(active_connection_proxy); - } - catch (...) - { - // shouldn't normally throw, but guard anyway - std::cerr << "[network] update_icon: exception in get_connection_state()" << std::endl; - state = CSTATE_DEACTIVATED; - } - - // Attempt to ask the info object for icon name, but guard unexpected exceptions - std::string icon_name; - try - { - icon_name = info->get_icon_name(state); - } - catch (const std::exception& e) - { - std::cerr << "[network] update_icon: exception in get_icon_name(): " << e.what() << std::endl; - icon_name = "network-offline-symbolic"; - } - catch (...) - { - std::cerr << "[network] update_icon: unknown exception in get_icon_name()" << std::endl; - icon_name = "network-offline-symbolic"; - } - - // Final sanity: if icon_name is empty, use fallback - if (icon_name.empty()) - icon_name = "network-offline-symbolic"; - - icon.set_from_icon_name(icon_name); + // Defensive checks to avoid crashes during early init or missing DBus info + if (!info) + { + // ensure info is always valid + info = std::unique_ptr(new NoConnectionInfo()); + info->connection_name = "No connection"; + std::cerr + << "[network] update_icon: info was null, created NoConnectionInfo" + << std::endl; + } + + // get connection state safely + WfConnectionState state = CSTATE_DEACTIVATED; + try + { + state = get_connection_state(active_connection_proxy); + } + catch (...) + { + // shouldn't normally throw, but guard anyway + std::cerr << "[network] update_icon: exception in get_connection_state()" + << std::endl; + state = CSTATE_DEACTIVATED; + } + + // Attempt to ask the info object for icon name, but guard unexpected + // exceptions + std::string icon_name; + try + { + icon_name = info->get_icon_name(state); + } + catch (const std::exception &e) + { + std::cerr << "[network] update_icon: exception in get_icon_name(): " + << e.what() << std::endl; + icon_name = "network-offline-symbolic"; + } + catch (...) + { + std::cerr << "[network] update_icon: unknown exception in get_icon_name()" + << std::endl; + icon_name = "network-offline-symbolic"; + } + + // Final sanity: if icon_name is empty, use fallback + if (icon_name.empty()) + icon_name = "network-offline-symbolic"; + + icon.set_from_icon_name(icon_name); } - /* optional color helper omitted (not used) */ void WayfireNetworkInfo::update_status() { - std::string description = info->get_connection_name(); - status.set_text(description); - button->set_tooltip_text(description); - - status.get_style_context()->remove_class("excellent"); - status.get_style_context()->remove_class("good"); - status.get_style_context()->remove_class("weak"); - status.get_style_context()->remove_class("none"); - if (status_color_opt) - { - status.get_style_context()->add_class(info->get_strength_str()); - } + std::string description = info->get_connection_name(); + status.set_text(description); + button->set_tooltip_text(description); + + status.get_style_context()->remove_class("excellent"); + status.get_style_context()->remove_class("good"); + status.get_style_context()->remove_class("weak"); + status.get_style_context()->remove_class("none"); + if (status_color_opt) + { + status.get_style_context()->add_class(info->get_strength_str()); + } } void WayfireNetworkInfo::update_active_connection() { - Glib::Variant active_conn_path; - nm_proxy->get_cached_property(active_conn_path, ACTIVE_CONNECTION); - - if (active_conn_path && (active_conn_path.get() != "/")) - { - active_connection_proxy = Gio::DBus::Proxy::create_sync( - connection, NM_DBUS_NAME, active_conn_path.get(), - "org.freedesktop.NetworkManager.Connection.Active"); - } else - { - active_connection_proxy = DBusProxy(); + Glib::Variant active_conn_path; + nm_proxy->get_cached_property(active_conn_path, ACTIVE_CONNECTION); + + if (active_conn_path && (active_conn_path.get() != "/")) + { + active_connection_proxy = Gio::DBus::Proxy::create_sync( + connection, NM_DBUS_NAME, active_conn_path.get(), + "org.freedesktop.NetworkManager.Connection.Active"); + } + else + { + active_connection_proxy = DBusProxy(); + } + + auto set_no_connection = [=]() + { + info = std::unique_ptr(new NoConnectionInfo()); + info->connection_name = "No connection"; + }; + + if (!active_connection_proxy) + { + set_no_connection(); + } + else + { + Glib::Variant vtype, vobject; + active_connection_proxy->get_cached_property(vtype, "Type"); + active_connection_proxy->get_cached_property(vobject, "SpecificObject"); + auto type = vtype.get(); + auto object = vobject.get(); + + if (type.find("wireless") != type.npos) + { + info = std::unique_ptr( + new WifiConnectionInfo(connection, object, this)); + } + else if (type.find("ethernet") != type.npos) + { + info = std::unique_ptr( + new EthernetConnectionInfo(connection, object)); + } + else if (type.find("bluetooth")) + { + std::cout << "Unimplemented: bluetooth connection" << std::endl; + set_no_connection(); } - - auto set_no_connection = [=] () - { - info = std::unique_ptr(new NoConnectionInfo()); - info->connection_name = "No connection"; - }; - - if (!active_connection_proxy) - { - set_no_connection(); - } else + else { - Glib::Variant vtype, vobject; - active_connection_proxy->get_cached_property(vtype, "Type"); - active_connection_proxy->get_cached_property(vobject, "SpecificObject"); - auto type = vtype.get(); - auto object = vobject.get(); - - if (type.find("wireless") != type.npos) - { - info = std::unique_ptr( - new WifiConnectionInfo(connection, object, this)); - } else if (type.find("ethernet") != type.npos) - { - info = std::unique_ptr( - new EthernetConnectionInfo(connection, object)); - } else if (type.find("bluetooth")) - { - std::cout << "Unimplemented: bluetooth connection" << std::endl; - set_no_connection(); - } else - { - std::cout << "Unimplemented: unknown connection type" << std::endl; - set_no_connection(); - } - - Glib::Variant vname; - active_connection_proxy->get_cached_property(vname, "Id"); - info->connection_name = vname.get(); + std::cout << "Unimplemented: unknown connection type" << std::endl; + set_no_connection(); } - update_icon(); - update_status(); + Glib::Variant vname; + active_connection_proxy->get_cached_property(vname, "Id"); + info->connection_name = vname.get(); + } + + update_icon(); + update_status(); } void WayfireNetworkInfo::on_nm_properties_changed( - const Gio::DBus::Proxy::MapChangedProperties& properties, - const std::vector& invalidated) + const Gio::DBus::Proxy::MapChangedProperties &properties, + const std::vector &invalidated) { - for (auto & prop : properties) + for (auto &prop : properties) + { + if (prop.first == ACTIVE_CONNECTION) { - if (prop.first == ACTIVE_CONNECTION) - { - update_active_connection(); - } + update_active_connection(); } + } } bool WayfireNetworkInfo::setup_dbus() { - auto cancellable = Gio::Cancellable::create(); - connection = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SYSTEM, cancellable); - if (!connection) - { - std::cerr << "Failed to connect to dbus" << std::endl; - return false; - } - - nm_proxy = Gio::DBus::Proxy::create_sync(connection, NM_DBUS_NAME, - "/org/freedesktop/NetworkManager", - "org.freedesktop.NetworkManager"); - if (!nm_proxy) - { - std::cerr << "Failed to connect to network manager, " << - "are you sure it is running?" << std::endl; - return false; - } - - nm_proxy->signal_properties_changed().connect( - sigc::mem_fun(*this, &WayfireNetworkInfo::on_nm_properties_changed)); - - return true; + auto cancellable = Gio::Cancellable::create(); + connection = + Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SYSTEM, cancellable); + if (!connection) + { + std::cerr << "Failed to connect to dbus" << std::endl; + return false; + } + + nm_proxy = Gio::DBus::Proxy::create_sync(connection, NM_DBUS_NAME, + "/org/freedesktop/NetworkManager", + "org.freedesktop.NetworkManager"); + if (!nm_proxy) + { + std::cerr << "Failed to connect to network manager, " + << "are you sure it is running?" << std::endl; + return false; + } + + nm_proxy->signal_properties_changed().connect( + sigc::mem_fun(*this, &WayfireNetworkInfo::on_nm_properties_changed)); + + return true; } -/* --- nmcli helper and parser --- */ - -struct WifiEntry { std::string ssid; int signal; std::string security; }; - -static std::string run_cmd_capture_stdout(const std::string& cmd, int *exit_status = nullptr) -{ - std::string stdout_out; - int status = 0; - try - { - Glib::spawn_command_line_sync(cmd, &stdout_out, nullptr, &status); - } - catch (const Glib::SpawnError& e) - { - std::cerr << "spawn error: " << e.what() << std::endl; - if (exit_status) *exit_status = -1; - return std::string(); - } - - if (exit_status) *exit_status = status; - return stdout_out; -} +/* --- iwd helper and parser --- */ -static std::vector parse_nmcli_wifi_list(const std::string& data) +struct WifiEntry { - std::vector out; - std::istringstream iss(data); - std::string line; - while (std::getline(iss, line)) - { - if (line.empty()) continue; - // nmcli -t uses ':' as separator. SSIDs may contain ':' rarely but we keep it simple here. - size_t p1 = line.find(':'); - if (p1 == std::string::npos) continue; - size_t p2 = line.find(':', p1 + 1); - if (p2 == std::string::npos) continue; - std::string ssid = line.substr(0, p1); - std::string sigs = line.substr(p1 + 1, p2 - p1 - 1); - std::string sec = line.substr(p2 + 1); - int sig = 0; - try { sig = std::stoi(sigs); } catch (...) { sig = 0; } - // trim newline\r - ssid.erase(std::remove_if(ssid.begin(), ssid.end(), [](unsigned char c){ return c == '\r' || c == '\n'; }), ssid.end()); - out.push_back({ssid, sig, sec}); - } - return out; -} + std::string ssid; + int signal; + std::string security; +}; /* --- Popover handling --- */ void WayfireNetworkInfo::show_wifi_popover() { - auto popover = button->get_popover(); - - // ensure popover content is set - if (!popover->get_child()) - { - pop_list_box.set_orientation(Gtk::Orientation::VERTICAL); - pop_list_box.set_spacing(4); - pop_list_box.set_margin(6); - - pop_scrolled.set_child(pop_list_box); - pop_scrolled.set_min_content_height(200); - pop_scrolled.set_min_content_width(320); - - popover_box.set_orientation(Gtk::Orientation::VERTICAL); - popover_box.set_spacing(6); - popover_box.append(pop_scrolled); - - // prepare password box (hidden until needed) - pop_pass_box.set_orientation(Gtk::Orientation::VERTICAL); - pop_pass_box.set_spacing(4); - pop_pass_box.set_margin_top(6); - pop_pass_box.set_margin_bottom(6); - - popover->set_child(popover_box); - popover->set_size_request(320, 240); - popover->get_style_context()->add_class("network-popover"); - } - - populate_wifi_list(); - - // show - button->set_keyboard_interactive(false); - popover->popup(); + auto popover = button->get_popover(); + + // ensure popover content is set + if (!popover->get_child()) + { + pop_list_box.set_orientation(Gtk::Orientation::VERTICAL); + pop_list_box.set_spacing(4); + pop_list_box.set_margin(6); + + pop_scrolled.set_child(pop_list_box); + pop_scrolled.set_min_content_height(200); + pop_scrolled.set_min_content_width(320); + + popover_box.set_orientation(Gtk::Orientation::VERTICAL); + popover_box.set_spacing(6); + popover_box.append(pop_scrolled); + + // prepare password box (hidden until needed) + pop_pass_box.set_orientation(Gtk::Orientation::VERTICAL); + pop_pass_box.set_spacing(4); + pop_pass_box.set_margin_top(6); + pop_pass_box.set_margin_bottom(6); + + popover->set_child(popover_box); + popover->set_size_request(320, 240); + popover->get_style_context()->add_class("network-popover"); + } + + populate_wifi_list(); + + // show + button->set_keyboard_interactive(false); + popover->popup(); } void WayfireNetworkInfo::populate_wifi_list() { - // clear pop_list_box children - auto children = pop_list_box.get_children(); - for (auto *c : children) - pop_list_box.remove(*c); - - // header - auto header = Gtk::make_managed("Available Wi-Fi networks"); - header->set_margin_bottom(6); - pop_list_box.append(*header); - - // run nmcli - int st = 0; - std::string out = run_cmd_capture_stdout("nmcli -t -f SSID,SIGNAL,SECURITY dev wifi list", &st); - if (out.empty()) - { - auto lbl = Gtk::make_managed("Could not list networks (is nmcli installed?)"); - lbl->set_margin(6); - pop_list_box.append(*lbl); - - auto open_btn = Gtk::make_managed("Open network settings"); - open_btn->signal_clicked().connect([this]() { - info->spawn_control_center(nm_proxy); - button->get_popover()->popdown(); - }); - pop_list_box.append(*open_btn); - return; - } - - auto entries = parse_nmcli_wifi_list(out); - if (entries.empty()) - { - auto lbl = Gtk::make_managed("No networks found"); - lbl->set_margin(6); - pop_list_box.append(*lbl); - return; - } - - // sort by signal desc, then unique SSID - std::sort(entries.begin(), entries.end(), [](const WifiEntry &a, const WifiEntry &b) { - return a.signal > b.signal; - }); - - std::string last_ssid; - for (auto &e : entries) - { - if (e.ssid == last_ssid) continue; // dedupe identical SSID rows - last_ssid = e.ssid; - - std::ostringstream label; - label << (e.ssid.empty() ? "" : e.ssid) << " (" << e.signal << "%)"; - if (!e.security.empty()) label << " [" << e.security << "]"; - - auto btn = Gtk::make_managed(label.str()); - btn->set_halign(Gtk::Align::FILL); - - // capture ssid+security by value - btn->signal_clicked().connect([this, ssid = e.ssid, sec = e.security]() { - bool secured = (sec.find("WPA") != std::string::npos) || - (sec.find("WEP") != std::string::npos) || - (sec.find("RSN") != std::string::npos); - if (!secured) - { - attempt_connect_ssid(ssid, ""); - button->get_popover()->popdown(); - } - else - { - show_password_prompt_for(ssid, sec); - } - }); - - pop_list_box.append(*btn); - } - - // small spacer and status label - pop_status_label.set_text(""); - pop_status_label.set_margin_top(6); - pop_list_box.append(pop_status_label); + // Clear the list box + for (auto *child : pop_list_box.get_children()) + pop_list_box.remove(*child); + + // Header + auto header = Gtk::make_managed("Available Wi-Fi networks"); + header->set_margin_bottom(6); + pop_list_box.append(*header); + + // Show scanning message + pop_status_label.set_text("Scanning for networks…"); + pop_list_box.set_sensitive(false); + + // Launch network scan in background + std::thread([this]() + { + auto networks = get_available_networks(); + + Glib::signal_idle().connect_once([this, networks]() { + if (networks.empty()) { + auto lbl = Gtk::make_managed("No networks found"); + lbl->set_margin(6); + pop_list_box.append(*lbl); + } else { + for (auto &net : networks) { + std::ostringstream label; + label << net.ssid << " (" << net.strength << "%)"; + if (net.secured) + label << " [Secured]"; + + auto btn = Gtk::make_managed(label.str()); + btn->set_halign(Gtk::Align::FILL); + + // Capture SSID and secured flag + btn->signal_clicked().connect([this, ssid = net.ssid, secured = net.secured]() { + if (!secured) { + attempt_connect_ssid(ssid, ""); + button->get_popover()->popdown(); + } else { + show_password_prompt_for(ssid); + } + }); + + pop_list_box.append(*btn); + } + } + + pop_list_box.set_sensitive(true); + pop_status_label.set_text(""); // Clear scanning message + }); }) + .detach(); + + // Status label at the bottom + pop_status_label.set_margin_top(6); + pop_list_box.append(pop_status_label); } -void WayfireNetworkInfo::show_password_prompt_for(const std::string& ssid, const std::string& security) +void WayfireNetworkInfo::show_password_prompt_for(const std::string &ssid) { - // clear popover_box and replace scroll with a password UI inside popover_box - auto popover = button->get_popover(); - - // remove all children of popover_box - auto pb_children = popover_box.get_children(); - for (auto *c : pb_children) popover_box.remove(*c); - - // title - auto title = Gtk::make_managed("Connect to: " + (ssid.empty() ? "" : ssid)); - title->set_margin_bottom(6); - popover_box.append(*title); - - // password entry - auto pass_label = Gtk::make_managed("Password:"); - popover_box.append(*pass_label); - - auto entry = Gtk::make_managed(); - entry->set_visibility(false); - entry->set_hexpand(true); - popover_box.append(*entry); - - // buttons box - auto hbox = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 6); - hbox->set_halign(Gtk::Align::END); - auto cancel_btn = Gtk::make_managed("Cancel"); - auto connect_btn = Gtk::make_managed("Connect"); - - cancel_btn->signal_clicked().connect([this]() { - // restore list - auto pb_children2 = popover_box.get_children(); - for (auto *c : pb_children2) popover_box.remove(*c); - popover_box.append(pop_scrolled); - populate_wifi_list(); - }); - - connect_btn->signal_clicked().connect([this, ssid, entry]() { - std::string pwd = entry->get_text(); - if (pwd.empty()) - { - pop_status_label.set_text("Password cannot be empty"); - return; - } - pop_status_label.set_text("Connecting..."); - // attempt connect (synchronous call to nmcli; it's quick) - attempt_connect_ssid(ssid, pwd); - button->get_popover()->popdown(); - }); - - hbox->append(*cancel_btn); - hbox->append(*connect_btn); - popover_box.append(*hbox); - - // status label - pop_status_label.set_text(""); - popover_box.append(pop_status_label); - - popover->present(); // show updated content + // Clear popover content + for (auto *child : popover_box.get_children()) + popover_box.remove(*child); + + auto popover = button->get_popover(); + + // Title + auto title = Gtk::make_managed("Connect to: " + ssid); + title->set_margin_bottom(6); + popover_box.append(*title); + + // Password entry + auto pass_label = Gtk::make_managed("Password:"); + popover_box.append(*pass_label); + + auto entry = Gtk::make_managed(); + entry->set_visibility(false); + entry->set_hexpand(true); + popover_box.append(*entry); + + // Buttons + auto hbox = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 6); + hbox->set_halign(Gtk::Align::END); + + auto cancel_btn = Gtk::make_managed("Cancel"); + cancel_btn->signal_clicked().connect([this]() + { + // Restore Wi-Fi list + for (auto *child : popover_box.get_children()) + popover_box.remove(*child); + popover_box.append(pop_scrolled); + populate_wifi_list(); }); + + auto connect_btn = Gtk::make_managed("Connect"); + connect_btn->signal_clicked().connect([this, ssid, entry, popover]() + { + std::string pwd = entry->get_text(); + if (pwd.empty()) + { + pop_status_label.set_text("Password cannot be empty"); + return; + } + pop_status_label.set_text("Connecting..."); + attempt_connect_ssid(ssid, pwd); + popover->popdown(); // ✅ now captured + }); + + hbox->append(*cancel_btn); + hbox->append(*connect_btn); + popover_box.append(*hbox); + + // Status label + pop_status_label.set_text(""); + popover_box.append(pop_status_label); + + // Show updated popover + popover->present(); } -void WayfireNetworkInfo::attempt_connect_ssid(const std::string& ssid, const std::string& password) +void WayfireNetworkInfo::attempt_connect_ssid(const std::string &ssid, + const std::string &password) { - if (ssid.empty()) - { - std::cerr << "Empty SSID, aborting connect" << std::endl; - return; - } + if (ssid.empty() || ssid.find_first_not_of(" \t\r\n") == std::string::npos) + { + pop_status_label.set_text("Invalid SSID"); + return; + } + + try + { + auto bus = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SYSTEM); + auto nm = Gio::DBus::Proxy::create_sync(bus, + "org.freedesktop.NetworkManager", + "/org/freedesktop/NetworkManager", + "org.freedesktop.NetworkManager"); + + // --- Find Wi-Fi device --- + Glib::VariantBase devices_var; + nm->get_cached_property(devices_var, "Devices"); + if (!devices_var) + { + pop_status_label.set_text("No devices"); + return; + } + + auto dev_array = Glib::VariantBase::cast_dynamic>>(devices_var); + std::string wifi_device_path; + for (const auto &path : dev_array.get()) + { + auto dev_proxy = Gio::DBus::Proxy::create_sync(bus, "org.freedesktop.NetworkManager", path, "org.freedesktop.NetworkManager.Device"); + if (!dev_proxy) + continue; + Glib::VariantBase type_v; + dev_proxy->get_cached_property(type_v, "DeviceType"); + auto type_var = Glib::VariantBase::cast_dynamic>(type_v); + if (type_var && type_var.get() == 2) + { + wifi_device_path = path; + break; + } + } + + if (wifi_device_path.empty()) + { + pop_status_label.set_text("No Wi-Fi device"); + return; + } + + // --- Build settings using glibmm (correct types) --- + + // SSID as byte array 'ay' + std::vector ssid_bytes(ssid.begin(), ssid.end()); + auto ssid_variant = Glib::Variant>::create(ssid_bytes); + + // UUID + gchar *uuid_c = g_uuid_string_random(); + Glib::ustring uuid(uuid_c); + g_free(uuid_c); + + // ----- connection (a{sv}) + std::map connection_map; + connection_map["type"] = Glib::Variant::create("802-11-wireless"); + connection_map["id"] = Glib::Variant::create(ssid); + connection_map["uuid"] = Glib::Variant::create(uuid); + + // ----- 802-11-wireless (a{sv}) + std::map wifi_map; + wifi_map["ssid"] = ssid_variant; + wifi_map["mode"] = Glib::Variant::create("infrastructure"); + + // ----- 802-11-wireless-security (a{sv}) + std::map sec_map; + bool use_security = !password.empty(); + if (use_security) + { + sec_map["key-mgmt"] = Glib::Variant::create("wpa-psk"); + sec_map["psk"] = Glib::Variant::create(password); + } + + // ------------------------ + // TOP-LEVEL SETTINGS (a{sa{sv}}) + // ------------------------ + std::map< + Glib::ustring, + std::map> + settings_map; + + settings_map["connection"] = connection_map; + settings_map["802-11-wireless"] = wifi_map; + if (use_security) + settings_map["802-11-wireless-security"] = sec_map; + + auto settings = Glib::Variant>>::create(settings_map); + + // ------------------------ + // Object paths (o) + // ------------------------ + auto device_path = Glib::Variant::create(wifi_device_path); + + // Access point path is "/" → NM autoselects AP matching SSID + auto ap_path = Glib::Variant::create("/"); + + // ------------------------ + // FINAL TUPLE (a{sa{sv}}, o, o) + // ------------------------ + std::vector args_vec = { + settings, + device_path, + ap_path}; + + auto args = Glib::VariantContainerBase::create_tuple(args_vec); - std::string cmd = "nmcli device wifi connect \"" + ssid + "\""; - if (!password.empty()) - { - // Escape double quotes in password naively - std::string esc = password; - size_t pos = 0; - while ((pos = esc.find('"', pos)) != std::string::npos) { esc.replace(pos, 1, "\\\""); pos += 2; } - cmd += " password \"" + esc + "\""; - } - - int exit_status = 0; - std::string output = run_cmd_capture_stdout(cmd, &exit_status); - if (exit_status != 0) - { - std::cerr << "nmcli connect failed: " << output << std::endl; - // if popover exists, show temporary error - pop_status_label.set_text("Failed to connect. Check password or settings."); - } - else - { - pop_status_label.set_text("Connection started."); - } + // ------------------------ + // CALL NetworkManager + // ------------------------ + nm->call_sync("AddAndActivateConnection", args); - // trigger an update of state (NetworkManager DBus will also update via signals) + pop_status_label.set_text("Connecting..."); update_active_connection(); + } + catch (const Glib::Error &e) + { + std::cerr << "Connect failed: " << e.what() << std::endl; + pop_status_label.set_text("Failed: " + Glib::ustring(e.what())); + } + catch (...) + { + pop_status_label.set_text("Unknown error"); + } } /* --- widget lifecycle --- */ void WayfireNetworkInfo::on_click() { - if ((std::string)click_command_opt != "default") - { - Glib::spawn_command_line_async((std::string)click_command_opt); - } else - { - show_wifi_popover(); - } -} + // Instantly show popover + show_wifi_popover(); -void WayfireNetworkInfo::init(Gtk::Box *container) -{ - if (!setup_dbus()) - { - enabled = false; - return; - } - - // create WayfireMenuButton - button = std::make_unique("panel"); - button->get_style_context()->add_class("network"); - button->get_children()[0]->get_style_context()->add_class("flat"); - - update_icon(); - button->set_child(icon); - container->append(*button); + // Show scanning message + pop_status_label.set_text("Scanning for networks…"); + pop_list_box.set_sensitive(false); + while (auto child = pop_list_box.get_first_child()) + pop_list_box.remove(*child); - button->set_tooltip_text("Click to open Wi-Fi selector"); + // Launch scanning in background + std::thread([this]() + { + auto networks = get_available_networks(); - // connect click to show popover (also support middle-click etc if needed) - button->signal_clicked().connect(sigc::mem_fun(*this, &WayfireNetworkInfo::on_click)); - - button_content.set_valign(Gtk::Align::CENTER); - button_content.append(icon); - button_content.append(status); - button_content.set_spacing(6); - - icon.set_valign(Gtk::Align::CENTER); - icon.property_scale_factor().signal_changed().connect( - sigc::mem_fun(*this, &WayfireNetworkInfo::update_icon)); - icon.get_style_context()->add_class("network-icon"); + Glib::signal_idle().connect_once([this, networks]() + { + std::vector ssids; + for (auto &w : networks) + ssids.push_back(w.ssid); + + update_network_list(ssids); // <-- pass ssids, not networks + pop_list_box.set_sensitive(true); + }); }) + .detach(); +} - update_active_connection(); - handle_config_reload(); +void WayfireNetworkInfo::init(Gtk::Box *container) +{ + // --- Setup the popover initial UI --- + m_popover_box.set_margin(10); + m_popover_box.set_spacing(6); + m_popover_box.append(m_status_label); + m_popover_box.append(m_spinner); + m_popover.set_child(m_popover_box); + m_spinner.start(); + + if (!setup_dbus()) + { + enabled = false; + return; + } + + // create WayfireMenuButton + button = std::make_unique("panel"); + button->get_style_context()->add_class("network"); + button->get_children()[0]->get_style_context()->add_class("flat"); + + update_icon(); + button->set_child(icon); + container->append(*button); + + button->set_tooltip_text("Click to open Wi-Fi selector"); + + // connect click to show popover (also support middle-click etc if needed) + button->signal_clicked().connect( + sigc::mem_fun(*this, &WayfireNetworkInfo::on_click)); + + button_content.set_valign(Gtk::Align::CENTER); + button_content.append(icon); + button_content.append(status); + button_content.set_spacing(6); + + icon.set_valign(Gtk::Align::CENTER); + icon.property_scale_factor().signal_changed().connect( + sigc::mem_fun(*this, &WayfireNetworkInfo::update_icon)); + icon.get_style_context()->add_class("network-icon"); + + update_active_connection(); + handle_config_reload(); } void WayfireNetworkInfo::handle_config_reload() { - if (status_opt.value() == NETWORK_STATUS_ICON) + if (status_opt.value() == NETWORK_STATUS_ICON) + { + if (status.get_parent()) { - if (status.get_parent()) - { - button_content.remove(status); - } - } else + button_content.remove(status); + } + } + else + { + if (!status.get_parent()) { - if (!status.get_parent()) - { - button_content.append(status); - } + button_content.append(status); } + } + + update_icon(); + update_status(); +} - update_icon(); - update_status(); +void WayfireNetworkInfo::update_network_list( + const std::vector &networks) +{ + while (auto child = pop_list_box.get_first_child()) + pop_list_box.remove(*child); + + if (networks.empty()) + { + pop_status_label.set_text("No networks found."); + return; + } + + pop_status_label.set_text(""); + + for (const auto &ssid : networks) + { + auto row = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 6); + auto label = Gtk::make_managed(ssid); + label->set_hexpand(true); + row->append(*label); + + // Use a Button as a clickable row (GTK4 way) + auto btn = Gtk::make_managed(); + btn->set_child(*row); + btn->add_css_class("wifi-row"); + + btn->signal_clicked().connect([this, ssid]() + { + show_password_prompt_for(ssid); // adapt security as needed + }); + + pop_list_box.append(*btn); + } + + pop_list_box.show(); +} + +void WayfireNetworkInfo::show_error(const std::string &message) +{ + m_status_label.set_text("Error: " + message); + m_spinner.stop(); } -WayfireNetworkInfo::~WayfireNetworkInfo() -{} +WayfireNetworkInfo::~WayfireNetworkInfo() {} \ No newline at end of file diff --git a/src/panel/widgets/network.hpp b/src/panel/widgets/network.hpp index 1614d3de..ae9b8f52 100644 --- a/src/panel/widgets/network.hpp +++ b/src/panel/widgets/network.hpp @@ -12,6 +12,9 @@ #include #include #include +#include +#include +#include #include "../widget.hpp" #include "../util/wf-popover.hpp" // assumes WayfireMenuButton header lives here @@ -19,24 +22,24 @@ using DBusConnection = Glib::RefPtr; using DBusProxy = Glib::RefPtr; -using DBusPropMap = const Gio::DBus::Proxy::MapChangedProperties&; -using DBusPropList = const std::vector&; +using DBusPropMap = const Gio::DBus::Proxy::MapChangedProperties &; +using DBusPropList = const std::vector &; enum WfConnectionState // NmActiveConnectionState { - CSTATE_UNKNOWN = 0, - CSTATE_ACTIVATING = 1, - CSTATE_ACTIVATED = 2, + CSTATE_UNKNOWN = 0, + CSTATE_ACTIVATING = 1, + CSTATE_ACTIVATED = 2, CSTATE_DEACTIVATING = 3, - CSTATE_DEACTIVATED = 4, + CSTATE_DEACTIVATED = 4, }; struct WfNetworkConnectionInfo { std::string connection_name; - virtual void spawn_control_center(DBusProxy& nm); - virtual std::string get_control_center_section(DBusProxy& nm); + virtual void spawn_control_center(DBusProxy &nm); + virtual std::string get_control_center_section(DBusProxy &nm); virtual std::string get_connection_name() { @@ -49,12 +52,15 @@ struct WfNetworkConnectionInfo virtual std::string get_strength_str() = 0; virtual ~WfNetworkConnectionInfo() - {} + { + } }; + + static const std::string NETWORK_STATUS_ICON = "none"; static const std::string NETWORK_STATUS_CONN_NAME = "connection"; -static const std::string NETWORK_STATUS_NAME_IP = "full"; +static const std::string NETWORK_STATUS_NAME_IP = "full"; class WayfireNetworkInfo : public WayfireWidget { @@ -62,6 +68,7 @@ class WayfireNetworkInfo : public WayfireWidget DBusProxy nm_proxy, active_connection_proxy; std::unique_ptr info; + std::string run_iwd_cmd(const std::string &cmd); // Use WayfireMenuButton like notification-center std::unique_ptr button; @@ -70,11 +77,11 @@ class WayfireNetworkInfo : public WayfireWidget Gtk::Label status; // Popover UI (owned by WayfireMenuButton) - Gtk::Box popover_box; // top-level box in popover + Gtk::Box popover_box; // top-level box in popover Gtk::ScrolledWindow pop_scrolled; - Gtk::Box pop_list_box; // list of networks - Gtk::Box pop_pass_box; // inline password prompt - Gtk::Label pop_status_label; // show temporary messages + Gtk::Box pop_list_box; // list of networks + Gtk::Box pop_pass_box; // inline password prompt + Gtk::Label pop_status_label; // show temporary messages bool enabled = true; WfOption status_opt{"panel/network_status"}; @@ -85,23 +92,37 @@ class WayfireNetworkInfo : public WayfireWidget bool setup_dbus(); void update_active_connection(); void on_nm_properties_changed(DBusPropMap properties, - DBusPropList invalidated); + DBusPropList invalidated); void on_click(); // wifi helper methods (use nmcli) void show_wifi_popover(); void populate_wifi_list(); - void show_password_prompt_for(const std::string& ssid, const std::string& security); - void attempt_connect_ssid(const std::string& ssid, const std::string& password = ""); + void show_password_prompt_for(const std::string &ssid); + void attempt_connect_ssid(const std::string &ssid, + const std::string &password = ""); - public: +public: void update_icon(); void update_status(); void init(Gtk::Box *container); void handle_config_reload(); virtual ~WayfireNetworkInfo(); + +private: + // Async scanning UI state + Gtk::Popover m_popover; + Gtk::Box m_popover_box{Gtk::Orientation::VERTICAL, 6}; + Gtk::Label m_status_label{"Scanning for networks..."}; + Gtk::Spinner m_spinner; + bool scanning = false; + + // Async scanning helper functions + void scan_networks_async(); + void update_network_list(const std::vector &networks); + void show_error(const std::string &message); }; #endif /* end of include guard: WIDGETS_NETWORK_HPP */ \ No newline at end of file From 5fefa2c13e8f405e2f177452c3898f4076f545e4 Mon Sep 17 00:00:00 2001 From: rummanz Date: Mon, 17 Nov 2025 21:06:10 +0100 Subject: [PATCH 4/7] Added connected network information, implement dbus async and many UI improvements. -Trigger an async wifi scan in backgroud while the popover menu opens. -Return to main menu after connect to a network. -Connected network information now shows on the top of scan result with a disconnect button. -Added a detail button to show connected network information e.g. security, strength, frequency, ip address, subnet mask, dns, gateway. -Right click to open gnome-control-center and left click to show popover menu. --- src/panel/widgets/network.cpp | 1517 ++++++++++++++++++++++++--------- src/panel/widgets/network.hpp | 22 +- 2 files changed, 1129 insertions(+), 410 deletions(-) diff --git a/src/panel/widgets/network.cpp b/src/panel/widgets/network.cpp index f2ec2d6b..57d3768e 100644 --- a/src/panel/widgets/network.cpp +++ b/src/panel/widgets/network.cpp @@ -1,140 +1,655 @@ #include "network.hpp" -#include +#include #include +#include // sometimes needed for lower-level variant helpers +#include +#include +#include +#include +#include +#include #include +#include +#include #include +#include +#include +#include #include -#include +#include #include -#include #include -#include -#include -#include // sometimes needed for lower-level variant helpers - -#include -#include -#include - -#include -#include - -#include #define NM_DBUS_NAME "org.freedesktop.NetworkManager" #define ACTIVE_CONNECTION "PrimaryConnection" #define STRENGTH "Strength" -struct NetworkInfo -{ +struct NetworkInfo { std::string ssid; std::string path; int strength = 0; // Wi-Fi signal strength (0–100) bool secured = false; // True if the network requires authentication }; +// Map 0-100 strength to GNOME symbolic icon names +static std::string icon_name_for_strength(int value) { + if (value > 80) + return "network-wireless-signal-excellent-symbolic"; + if (value > 55) + return "network-wireless-signal-good-symbolic"; + if (value > 30) + return "network-wireless-signal-ok-symbolic"; + if (value > 5) + return "network-wireless-signal-weak-symbolic"; + return "network-wireless-signal-none-symbolic"; +} -static std::vector get_available_networks() -{ - std::vector result; - - try - { - auto connection = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SYSTEM); - - auto nm_proxy = Gio::DBus::Proxy::create_for_bus_sync( - Gio::DBus::BusType::SYSTEM, - "org.freedesktop.NetworkManager", - "/org/freedesktop/NetworkManager", - "org.freedesktop.NetworkManager"); - - Glib::VariantContainerBase reply = nm_proxy->call_sync("GetDevices"); - auto device_child = reply.get_child(0); - auto devices_variant = Glib::VariantBase::cast_dynamic>>(device_child); - std::vector device_paths = devices_variant.get(); - - for (const auto &path : device_paths) - { - auto dev_proxy = Gio::DBus::Proxy::create_for_bus_sync( - Gio::DBus::BusType::SYSTEM, - "org.freedesktop.NetworkManager", - path, - "org.freedesktop.NetworkManager.Device"); +// Interpolated color without CSS, based on connection strength percent +struct status_color { + int point; + Gdk::RGBA rgba; +}; - // For glibmm-2.68, we use GetCachedProperty(const Glib::ustring&, Glib::VariantBase&) - Glib::VariantBase dev_type_variant; - dev_proxy->get_cached_property(dev_type_variant, "DeviceType"); // ✅ argument order +static status_color status_colors[] = { + {0, Gdk::RGBA{"#ff0000"}}, + {25, Gdk::RGBA{"#ff0000"}}, + {40, Gdk::RGBA{"#ffff55"}}, + {100, Gdk::RGBA{"#00ff00"}}, +}; - guint32 dev_type = 0; - Glib::Variant vtype = Glib::VariantBase::cast_dynamic>(dev_type_variant); - dev_type = vtype.get(); +#define MAX_COLORS (sizeof(status_colors) / sizeof(status_color)) - if (dev_type != 2) // NM_DEVICE_TYPE_WIFI - continue; +static Gdk::RGBA get_color_for_pc(int pc) { + for (int i = MAX_COLORS - 2; i >= 0; i--) { + if (status_colors[i].point <= pc) { + auto &r1 = status_colors[i].rgba; + auto &r2 = status_colors[i + 1].rgba; - auto wifi_proxy = Gio::DBus::Proxy::create_for_bus_sync( - Gio::DBus::BusType::SYSTEM, - "org.freedesktop.NetworkManager", - path, - "org.freedesktop.NetworkManager.Device.Wireless"); - - Glib::VariantContainerBase aps_reply = wifi_proxy->call_sync("GetAllAccessPoints"); - auto ap_child = aps_reply.get_child(0); - auto ap_paths_variant = Glib::VariantBase::cast_dynamic>>(ap_child); - std::vector ap_paths = ap_paths_variant.get(); - - for (const auto &ap_path : ap_paths) - { - auto ap_proxy = Gio::DBus::Proxy::create_for_bus_sync( - Gio::DBus::BusType::SYSTEM, - "org.freedesktop.NetworkManager", - ap_path, - "org.freedesktop.NetworkManager.AccessPoint"); - - Glib::VariantBase ssid_var, wpa_var, rsn_var; - ap_proxy->get_cached_property(ssid_var, "Ssid"); // ✅ reversed order - ap_proxy->get_cached_property(wpa_var, "WpaFlags"); - ap_proxy->get_cached_property(rsn_var, "RsnFlags"); + double a = 1.0 * (pc - status_colors[i].point) / + (status_colors[i + 1].point - status_colors[i].point); + Gdk::RGBA result; + result.set_rgba(r1.get_red() * (1 - a) + r2.get_red() * a, + r1.get_green() * (1 - a) + r2.get_green() * a, + r1.get_blue() * (1 - a) + r2.get_blue() * a, + r1.get_alpha() * (1 - a) + r2.get_alpha() * a); - std::string ssid; - if (ssid_var) - { - Glib::Variant> ssid_bytes = - Glib::VariantBase::cast_dynamic>>(ssid_var); - auto vec = ssid_bytes.get(); - ssid.assign(vec.begin(), vec.end()); - } + return result; + } + } - guint32 wpa_flags = 0, rsn_flags = 0; - if (wpa_var) - wpa_flags = Glib::VariantBase::cast_dynamic>(wpa_var).get(); - if (rsn_var) - rsn_flags = Glib::VariantBase::cast_dynamic>(rsn_var).get(); + return Gdk::RGBA{"#ffffff"}; +} - std::string security = (wpa_flags || rsn_flags) ? "Secure" : "Open"; +static std::string rgba_to_hex(const Gdk::RGBA &c) { + auto to_hex = [](double v) { + int iv = (int)std::round(std::max(0.0, std::min(1.0, v)) * 255.0); + char buf[3]; + std::snprintf(buf, sizeof(buf), "%02X", iv); + return std::string(buf); + }; + return std::string("#") + to_hex(c.get_red()) + to_hex(c.get_green()) + + to_hex(c.get_blue()); +} - if (!ssid.empty()) - result.push_back({ssid, security}); - } +void WayfireNetworkInfo::trigger_wifi_scan_async( + std::function callback) { + // Use existing connection or create a new one synchronously + Glib::RefPtr conn = connection; + if (!conn) { + try { + conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SYSTEM); + } catch (const Glib::Error &e) { + std::cerr << "D-Bus connection error: " << e.what() << std::endl; + callback(); + return; } } - catch (const Glib::Error &ex) - { - std::cerr << "D-Bus error in get_available_networks(): " << ex.what() << std::endl; + + // Create NetworkManager proxy asynchronously + Gio::DBus::Proxy::create( + conn, NM_DBUS_NAME, "/org/freedesktop/NetworkManager", + "org.freedesktop.NetworkManager", + [callback, conn](const Glib::RefPtr &result) { + try { + auto nm_proxy = Gio::DBus::Proxy::create_finish(result); + + // Get devices to find WiFi device + nm_proxy->call( + "GetDevices", + [callback, conn, + nm_proxy](const Glib::RefPtr &result) { + try { + Glib::VariantContainerBase reply = + nm_proxy->call_finish(result); + auto device_child = reply.get_child(0); + auto devices_variant = Glib::VariantBase::cast_dynamic< + Glib::Variant>>( + device_child); + std::vector device_paths = + devices_variant.get(); + + // Find WiFi device and trigger scan + auto scan_state = std::make_shared>(false); + auto devices_checked = std::make_shared>(0); + int total_devices = device_paths.size(); + + if (total_devices == 0) { + callback(); // No devices found + return; + } + + for (const auto &path : device_paths) { + Gio::DBus::Proxy::create( + conn, NM_DBUS_NAME, path, + "org.freedesktop.NetworkManager.Device", + [callback, conn, path, scan_state, devices_checked, + total_devices]( + const Glib::RefPtr &result) { + try { + auto dev_proxy = + Gio::DBus::Proxy::create_finish(result); + + // Check device type + Glib::VariantBase dev_type_variant; + dev_proxy->get_cached_property(dev_type_variant, + "DeviceType"); + guint32 dev_type = 0; + if (dev_type_variant) { + Glib::Variant vtype = + Glib::VariantBase::cast_dynamic< + Glib::Variant>(dev_type_variant); + dev_type = vtype.get(); + } + + if (dev_type == 2) // NM_DEVICE_TYPE_WIFI + { + // Create wireless device proxy and trigger scan + Gio::DBus::Proxy::create( + conn, NM_DBUS_NAME, path, + "org.freedesktop.NetworkManager.Device." + "Wireless", + [callback, scan_state, devices_checked, + total_devices]( + const Glib::RefPtr + &result) { + try { + auto wifi_proxy = + Gio::DBus::Proxy::create_finish( + result); + // Trigger scan (RequestScan takes (a{sv}) + // - a tuple with an empty dict) Create + // empty dictionary a{sv} + std::map + scan_options; + auto scan_options_variant = Glib::Variant< + std::map>:: + create(scan_options); + // Wrap in tuple (a{sv}) + std::vector args_vec = + {scan_options_variant}; + auto args = Glib::VariantContainerBase:: + create_tuple(args_vec); + + wifi_proxy->call( + "RequestScan", + [callback, wifi_proxy, scan_state]( + const Glib::RefPtr< + Gio::AsyncResult> &result) { + try { + wifi_proxy->call_finish(result); + scan_state->store(true); + // Scan triggered successfully, + // wait a bit for it to complete + Glib::signal_timeout() + .connect_once( + [callback]() { + callback(); + }, + 1000); // Wait 1 second + // for scan to + // complete + } catch (const Glib::Error &e) { + std::cerr << "RequestScan error: " + << e.what() + << std::endl; + // Continue anyway - might have + // already scanned + Glib::signal_timeout() + .connect_once( + [callback]() { + callback(); + }, + 500); + } + }, + args); + } catch (const Glib::Error &e) { + std::cerr + << "Failed to create wireless proxy: " + << e.what() << std::endl; + int checked = ++(*devices_checked); + if (checked >= total_devices && + !scan_state->load()) { + callback(); + } + } + }); + } else { + int checked = ++(*devices_checked); + if (checked >= total_devices && + !scan_state->load()) { + callback(); // No WiFi device found + } + } + } catch (const Glib::Error &e) { + int checked = ++(*devices_checked); + if (checked >= total_devices && + !scan_state->load()) { + callback(); + } + } + }); + } + } catch (const Glib::Error &e) { + std::cerr << "GetDevices error: " << e.what() << std::endl; + callback(); + } + }, + Glib::VariantContainerBase()); + } catch (const Glib::Error &e) { + std::cerr << "Failed to create NM proxy: " << e.what() << std::endl; + callback(); + } + }); +} + +void WayfireNetworkInfo::get_available_networks_async( + std::function &)> callback) { + // Use shared state to safely track progress across async callbacks + struct ScanState { + std::vector all_networks; + std::atomic devices_processed{0}; + int total_devices = 0; + std::function &)> final_callback; + std::mutex networks_mutex; // Protect all_networks vector + + ScanState(std::function &)> cb) + : final_callback(cb) {} + + void check_complete() { + if (devices_processed.load() >= total_devices) { + final_callback(all_networks); + } + } + }; + + auto state = std::make_shared(callback); + + // Use existing connection or create a new one synchronously + // (The async work is in the D-Bus method calls, not the connection) + Glib::RefPtr conn = connection; + if (!conn) { + try { + conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SYSTEM); + } catch (const Glib::Error &e) { + std::cerr << "D-Bus connection error: " << e.what() << std::endl; + callback({}); + return; + } } - return result; + // Create NetworkManager proxy asynchronously + Gio::DBus::Proxy::create( + conn, NM_DBUS_NAME, "/org/freedesktop/NetworkManager", + "org.freedesktop.NetworkManager", + [state](const Glib::RefPtr &result) { + try { + auto nm_proxy = Gio::DBus::Proxy::create_finish(result); + + // Call GetDevices asynchronously (no parameters) + nm_proxy + ->call( + "GetDevices", + [state, + nm_proxy](const Glib::RefPtr &result) { + try { + Glib::VariantContainerBase reply = + nm_proxy->call_finish(result); + auto device_child = reply.get_child(0); + auto devices_variant = + Glib::VariantBase::cast_dynamic>>( + device_child); + std::vector device_paths = + devices_variant.get(); + + state->total_devices = device_paths.size(); + + if (state->total_devices == 0) { + state->final_callback({}); + return; + } + + // Process each device asynchronously + for (const auto &path : device_paths) { + // Create device proxy + Gio:: + DBus:: + Proxy:: + create( + nm_proxy->get_connection(), + NM_DBUS_NAME, path, + "org.freedesktop.NetworkManager.Device", + [state, path, nm_proxy]( + const Glib::RefPtr + &result) { + try { + auto dev_proxy = + Gio::DBus::Proxy::create_finish( + result); + + // Check device type + Glib::VariantBase dev_type_variant; + dev_proxy->get_cached_property( + dev_type_variant, "DeviceType"); + guint32 dev_type = 0; + if (dev_type_variant) { + Glib::Variant vtype = + Glib::VariantBase:: + cast_dynamic< + Glib::Variant< + guint32>>( + dev_type_variant); + dev_type = vtype.get(); + } + + if (dev_type != + 2) // NM_DEVICE_TYPE_WIFI + { + state->devices_processed++; + state->check_complete(); + return; + } + + // Create wireless device proxy + Gio::DBus::Proxy::create(dev_proxy + ->get_connection(), + NM_DBUS_NAME, + path, + "org." + "freedeskt" + "op." + "NetworkMa" + "nager." + "Device." + "Wireless", + [state, + path](const Glib:: + RefPtr< + Gio:: + AsyncResult> + & + result) { + try { + auto wifi_proxy = + Gio::DBus:: + Proxy::create_finish( + result); + + // Get + // all + // access + // points + // (no + // parameters) + wifi_proxy + ->call( + "GetAllAccessPoints", + [state, + wifi_proxy]( + const Glib::RefPtr< + Gio:: + AsyncResult> + &result) { + try { + Glib::VariantContainerBase + aps_reply = + wifi_proxy + ->call_finish( + result); + auto ap_child = + aps_reply + .get_child( + 0); + auto ap_paths_variant = + Glib::VariantBase::cast_dynamic< + Glib::Variant< + std::vector< + Glib:: + DBusObjectPathString>>>( + ap_child); + std::vector< + Glib:: + DBusObjectPathString> + ap_paths = + ap_paths_variant + .get(); + + int total_aps = + ap_paths + .size(); + auto aps_processed = + std::make_shared< + std::atomic< + int>>( + 0); + + if (total_aps == + 0) { + state + ->devices_processed++; + state + ->check_complete(); + return; + } + + // Process each access point + for ( + const auto + &ap_path : + ap_paths) { + Gio::DBus:: + Proxy::create( + wifi_proxy + ->get_connection(), + NM_DBUS_NAME, + ap_path, + "org.freedesktop.NetworkManager.AccessPoint", + [state, + total_aps, + aps_processed]( + const Glib::RefPtr< + Gio:: + AsyncResult> + &result) { + try { + auto ap_proxy = + Gio::DBus:: + Proxy::create_finish( + result); + if (ap_proxy) { + Glib::VariantBase + ssid_var, + wpa_var, + rsn_var, + strength_var; + ap_proxy + ->get_cached_property( + ssid_var, + "Ssid"); + ap_proxy + ->get_cached_property( + wpa_var, + "WpaFlags"); + ap_proxy + ->get_cached_property( + rsn_var, + "RsnFlags"); + ap_proxy + ->get_cached_property( + strength_var, + "Strength"); + + std::string + ssid; + if (ssid_var) { + Glib::Variant< + std::vector< + guint8>> + ssid_bytes = Glib::VariantBase::cast_dynamic< + Glib::Variant< + std::vector< + guint8>>>( + ssid_var); + auto vec = + ssid_bytes + .get(); + ssid.assign( + vec.begin(), + vec.end()); + } + + guint32 + wpa_flags = + 0, + rsn_flags = + 0; + if (wpa_var) + wpa_flags = + Glib::VariantBase::cast_dynamic< + Glib::Variant< + guint32>>( + wpa_var) + .get(); + if (rsn_var) + rsn_flags = + Glib::VariantBase::cast_dynamic< + Glib::Variant< + guint32>>( + rsn_var) + .get(); + + int strength = + 0; + if (strength_var) + strength = + Glib::VariantBase::cast_dynamic< + Glib::Variant< + guchar>>( + strength_var) + .get(); + + bool secured = + (wpa_flags || + rsn_flags); + + if (!ssid.empty()) { + std::lock_guard< + std:: + mutex> + lock( + state + ->networks_mutex); + state + ->all_networks + .push_back( + {ssid, + ap_proxy + ->get_object_path(), + strength, + secured}); + } + } + + int processed = + ++(*aps_processed); + if (processed >= + total_aps) { + state + ->devices_processed++; + state + ->check_complete(); + } + } catch ( + const Glib::Error + &e) { + // Ignore errors for individual access points + int processed = + ++(*aps_processed); + if (processed >= + total_aps) { + state + ->devices_processed++; + state + ->check_complete(); + } + } + }); + } + } catch ( + const Glib::Error + &e) { + std::cerr + << "Error getting access points: " + << e.what() + << std:: + endl; + state + ->devices_processed++; + state + ->check_complete(); + } + }, + Glib:: + VariantContainerBase()); + } catch ( + const Glib::Error + &e) { + state + ->devices_processed++; + state + ->check_complete(); + } + }); + } catch (const Glib::Error &e) { + state->devices_processed++; + state->check_complete(); + } + }); + } + } catch (const Glib::Error &e) { + std::cerr + << "D-Bus error in get_available_networks_async(): " + << e.what() << std::endl; + state->final_callback({}); + } + }, + Glib::VariantContainerBase()); + } catch (const Glib::Error &e) { + std::cerr << "Failed to create NM proxy: " << e.what() << std::endl; + state->final_callback({}); + } + }); } -std::string WfNetworkConnectionInfo::get_control_center_section(DBusProxy &nm) -{ +std::string WfNetworkConnectionInfo::get_control_center_section(DBusProxy &nm) { Glib::Variant wifi; nm->get_cached_property(wifi, "WirelessEnabled"); return wifi.get() ? "wifi" : "network"; } -void WfNetworkConnectionInfo::spawn_control_center(DBusProxy &nm) -{ +void WfNetworkConnectionInfo::spawn_control_center(DBusProxy &nm) { std::string command = "env XDG_CURRENT_DESKTOP=GNOME gnome-control-center "; command += get_control_center_section(nm); @@ -143,10 +658,8 @@ void WfNetworkConnectionInfo::spawn_control_center(DBusProxy &nm) /* --- ConnectionInfo implementations (same as before) --- */ -struct NoConnectionInfo : public WfNetworkConnectionInfo -{ - std::string get_icon_name(WfConnectionState state) - { +struct NoConnectionInfo : public WfNetworkConnectionInfo { + std::string get_icon_name(WfConnectionState state) { return "network-offline-symbolic"; } @@ -156,52 +669,41 @@ struct NoConnectionInfo : public WfNetworkConnectionInfo virtual ~NoConnectionInfo() {} }; -struct WifiConnectionInfo : public WfNetworkConnectionInfo -{ - - void scan_networks_async(); - void update_network_list(const std::vector &networks); +struct WifiConnectionInfo : public WfNetworkConnectionInfo { void show_error(const std::string &message); WayfireNetworkInfo *widget; DBusProxy ap; WifiConnectionInfo(const DBusConnection &connection, std::string path, - WayfireNetworkInfo *widget) - { + WayfireNetworkInfo *widget) { this->widget = widget; ap = Gio::DBus::Proxy::create_sync( connection, NM_DBUS_NAME, path, "org.freedesktop.NetworkManager.AccessPoint"); - if (ap) - { + if (ap) { ap->signal_properties_changed().connect( sigc::mem_fun(*this, &WifiConnectionInfo::on_properties_changed)); } } - void on_properties_changed(DBusPropMap changed, DBusPropList invalid) - { + void on_properties_changed(DBusPropMap changed, DBusPropList invalid) { bool needs_refresh = false; - for (auto &prop : changed) - { - if (prop.first == STRENGTH) - { + for (auto &prop : changed) { + if (prop.first == STRENGTH) { needs_refresh = true; } } - if (needs_refresh) - { + if (needs_refresh) { widget->update_icon(); widget->update_status(); } } - int get_strength() - { + int get_strength() { assert(ap); Glib::Variant vstr; @@ -210,8 +712,7 @@ struct WifiConnectionInfo : public WfNetworkConnectionInfo return vstr.get(); } - std::string get_strength_str() - { + std::string get_strength_str() { int value = get_strength(); if (value > 80) @@ -225,8 +726,7 @@ struct WifiConnectionInfo : public WfNetworkConnectionInfo return "none"; } - virtual std::string get_icon_name(WfConnectionState state) - { + virtual std::string get_icon_name(WfConnectionState state) { if ((state <= CSTATE_ACTIVATING) || (state == CSTATE_DEACTIVATING)) return "network-wireless-acquiring-symbolic"; @@ -239,8 +739,7 @@ struct WifiConnectionInfo : public WfNetworkConnectionInfo return "network-wireless-no-route-symbolic"; } - virtual int get_connection_strength() - { + virtual int get_connection_strength() { if (ap) return get_strength(); return 100; @@ -250,13 +749,11 @@ struct WifiConnectionInfo : public WfNetworkConnectionInfo virtual ~WifiConnectionInfo() {} }; -struct EthernetConnectionInfo : public WfNetworkConnectionInfo -{ +struct EthernetConnectionInfo : public WfNetworkConnectionInfo { DBusProxy ap; EthernetConnectionInfo(const DBusConnection &connection, std::string path) {} - virtual std::string get_icon_name(WfConnectionState state) - { + virtual std::string get_icon_name(WfConnectionState state) { if ((state <= CSTATE_ACTIVATING) || (state == CSTATE_DEACTIVATING)) return "network-wired-acquiring-symbolic"; @@ -275,8 +772,7 @@ struct EthernetConnectionInfo : public WfNetworkConnectionInfo /* --- connection state helpers --- */ -static WfConnectionState get_connection_state(DBusProxy connection) -{ +static WfConnectionState get_connection_state(DBusProxy connection) { if (!connection) return CSTATE_DEACTIVATED; @@ -285,11 +781,9 @@ static WfConnectionState get_connection_state(DBusProxy connection) return (WfConnectionState)state.get(); } -void WayfireNetworkInfo::update_icon() -{ +void WayfireNetworkInfo::update_icon() { // Defensive checks to avoid crashes during early init or missing DBus info - if (!info) - { + if (!info) { // ensure info is always valid info = std::unique_ptr(new NoConnectionInfo()); info->connection_name = "No connection"; @@ -300,12 +794,9 @@ void WayfireNetworkInfo::update_icon() // get connection state safely WfConnectionState state = CSTATE_DEACTIVATED; - try - { + try { state = get_connection_state(active_connection_proxy); - } - catch (...) - { + } catch (...) { // shouldn't normally throw, but guard anyway std::cerr << "[network] update_icon: exception in get_connection_state()" << std::endl; @@ -315,18 +806,13 @@ void WayfireNetworkInfo::update_icon() // Attempt to ask the info object for icon name, but guard unexpected // exceptions std::string icon_name; - try - { + try { icon_name = info->get_icon_name(state); - } - catch (const std::exception &e) - { + } catch (const std::exception &e) { std::cerr << "[network] update_icon: exception in get_icon_name(): " << e.what() << std::endl; icon_name = "network-offline-symbolic"; - } - catch (...) - { + } catch (...) { std::cerr << "[network] update_icon: unknown exception in get_icon_name()" << std::endl; icon_name = "network-offline-symbolic"; @@ -341,75 +827,75 @@ void WayfireNetworkInfo::update_icon() /* optional color helper omitted (not used) */ -void WayfireNetworkInfo::update_status() -{ +void WayfireNetworkInfo::update_status() { std::string description = info->get_connection_name(); - status.set_text(description); button->set_tooltip_text(description); - status.get_style_context()->remove_class("excellent"); - status.get_style_context()->remove_class("good"); - status.get_style_context()->remove_class("weak"); - status.get_style_context()->remove_class("none"); - if (status_color_opt) - { - status.get_style_context()->add_class(info->get_strength_str()); + if (status_color_opt) { + int pc = 0; + try { + pc = info->get_connection_strength(); + } catch (...) { + pc = 0; + } + auto rgba = get_color_for_pc(pc); + auto hex = rgba_to_hex(rgba); + auto escaped = Glib::Markup::escape_text(description); + status.set_use_markup(true); + status.set_markup("" + escaped + + ""); + } else { + status.set_use_markup(false); + status.set_text(description); } } -void WayfireNetworkInfo::update_active_connection() -{ +void WayfireNetworkInfo::update_active_connection() { + Glib::Variant active_conn_path; nm_proxy->get_cached_property(active_conn_path, ACTIVE_CONNECTION); - if (active_conn_path && (active_conn_path.get() != "/")) - { + if (active_conn_path && (active_conn_path.get() != "/")) { active_connection_proxy = Gio::DBus::Proxy::create_sync( connection, NM_DBUS_NAME, active_conn_path.get(), "org.freedesktop.NetworkManager.Connection.Active"); - } - else - { + } else { active_connection_proxy = DBusProxy(); } - auto set_no_connection = [=]() - { + auto set_no_connection = [=]() { info = std::unique_ptr(new NoConnectionInfo()); info->connection_name = "No connection"; }; - if (!active_connection_proxy) - { + if (!active_connection_proxy) { set_no_connection(); - } - else - { + // No active connection → ensure no SSID is highlighted as connected + current_ap_path = Glib::DBusObjectPathString{""}; + } else { Glib::Variant vtype, vobject; active_connection_proxy->get_cached_property(vtype, "Type"); active_connection_proxy->get_cached_property(vobject, "SpecificObject"); auto type = vtype.get(); auto object = vobject.get(); - if (type.find("wireless") != type.npos) - { + if (type.find("wireless") != type.npos) { info = std::unique_ptr( new WifiConnectionInfo(connection, object, this)); - } - else if (type.find("ethernet") != type.npos) - { + current_ap_path = + Glib::DBusObjectPathString{object}; // remember the connected AP path + } else if (type.find("ethernet") != type.npos) { info = std::unique_ptr( new EthernetConnectionInfo(connection, object)); - } - else if (type.find("bluetooth")) - { + current_ap_path = Glib::DBusObjectPathString{""}; + } else if (type.find("bluetooth") != type.npos) { std::cout << "Unimplemented: bluetooth connection" << std::endl; set_no_connection(); - } - else - { + current_ap_path = Glib::DBusObjectPathString{""}; + } else { std::cout << "Unimplemented: unknown connection type" << std::endl; set_no_connection(); + current_ap_path = Glib::DBusObjectPathString{""}; } Glib::Variant vname; @@ -423,24 +909,23 @@ void WayfireNetworkInfo::update_active_connection() void WayfireNetworkInfo::on_nm_properties_changed( const Gio::DBus::Proxy::MapChangedProperties &properties, - const std::vector &invalidated) -{ - for (auto &prop : properties) - { - if (prop.first == ACTIVE_CONNECTION) - { + const std::vector &invalidated) { + for (auto &prop : properties) { + if (prop.first == ACTIVE_CONNECTION) { update_active_connection(); + auto pop = button ? button->get_popover() : nullptr; + if (pop && pop->get_visible()) { + populate_wifi_list(); + } } } } -bool WayfireNetworkInfo::setup_dbus() -{ +bool WayfireNetworkInfo::setup_dbus() { auto cancellable = Gio::Cancellable::create(); connection = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SYSTEM, cancellable); - if (!connection) - { + if (!connection) { std::cerr << "Failed to connect to dbus" << std::endl; return false; } @@ -448,8 +933,7 @@ bool WayfireNetworkInfo::setup_dbus() nm_proxy = Gio::DBus::Proxy::create_sync(connection, NM_DBUS_NAME, "/org/freedesktop/NetworkManager", "org.freedesktop.NetworkManager"); - if (!nm_proxy) - { + if (!nm_proxy) { std::cerr << "Failed to connect to network manager, " << "are you sure it is running?" << std::endl; return false; @@ -461,47 +945,39 @@ bool WayfireNetworkInfo::setup_dbus() return true; } -/* --- iwd helper and parser --- */ - -struct WifiEntry -{ - std::string ssid; - int signal; - std::string security; -}; - /* --- Popover handling --- */ - -void WayfireNetworkInfo::show_wifi_popover() -{ +void WayfireNetworkInfo::show_wifi_popover() { auto popover = button->get_popover(); - // ensure popover content is set - if (!popover->get_child()) - { + if (!popover->get_child()) { + // in network.cpp constructor or init method pop_list_box.set_orientation(Gtk::Orientation::VERTICAL); pop_list_box.set_spacing(4); pop_list_box.set_margin(6); - pop_scrolled.set_child(pop_list_box); - pop_scrolled.set_min_content_height(200); - pop_scrolled.set_min_content_width(320); - + pop_scrolled.set_min_content_height(350); + pop_scrolled.set_min_content_width(260); popover_box.set_orientation(Gtk::Orientation::VERTICAL); popover_box.set_spacing(6); popover_box.append(pop_scrolled); - // prepare password box (hidden until needed) pop_pass_box.set_orientation(Gtk::Orientation::VERTICAL); pop_pass_box.set_spacing(4); pop_pass_box.set_margin_top(6); pop_pass_box.set_margin_bottom(6); - popover->set_child(popover_box); - popover->set_size_request(320, 240); + popover->set_size_request(260, 350); popover->get_style_context()->add_class("network-popover"); } + // Always restore the main network list view (in case we're coming from + // password prompt) Remove all children from popover_box + for (auto *child : popover_box.get_children()) + popover_box.remove(*child); + + // Restore the scrolled window with network list + popover_box.append(pop_scrolled); + populate_wifi_list(); // show @@ -509,67 +985,136 @@ void WayfireNetworkInfo::show_wifi_popover() popover->popup(); } -void WayfireNetworkInfo::populate_wifi_list() -{ +void WayfireNetworkInfo::populate_wifi_list() { // Clear the list box for (auto *child : pop_list_box.get_children()) pop_list_box.remove(*child); // Header - auto header = Gtk::make_managed("Available Wi-Fi networks"); - header->set_margin_bottom(6); + auto header = Gtk::make_managed("Available WiFi Networks"); + // header->set_margin_bottom(0); pop_list_box.append(*header); // Show scanning message pop_status_label.set_text("Scanning for networks…"); pop_list_box.set_sensitive(false); + trigger_wifi_scan_async([this]() { + // Launch network scan asynchronously using D-Bus async interface + get_available_networks_async([this]( + const std::vector &networks) { + if (networks.empty()) { + auto lbl = Gtk::make_managed("No networks found"); + lbl->set_margin(6); + pop_list_box.append(*lbl); + } else { + // Find connected entry (if any) so we can render it first + int connected_index = -1; + if (!current_ap_path.empty()) { + for (size_t i = 0; i < networks.size(); ++i) { + if (Glib::ustring{networks[i].path} == current_ap_path) { + connected_index = static_cast(i); + break; + } + } + } - // Launch network scan in background - std::thread([this]() - { - auto networks = get_available_networks(); - - Glib::signal_idle().connect_once([this, networks]() { - if (networks.empty()) { - auto lbl = Gtk::make_managed("No networks found"); - lbl->set_margin(6); - pop_list_box.append(*lbl); + auto render_row = [this](const NetworkInfo &net, bool is_connected) { + auto row = + Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 8); + row->set_halign(Gtk::Align::FILL); + row->set_hexpand(true); + + auto sig_img = Gtk::make_managed(); + sig_img->set_from_icon_name(icon_name_for_strength(net.strength)); + sig_img->set_pixel_size(16); + row->append(*sig_img); + + auto name_lbl = Gtk::make_managed(net.ssid); + name_lbl->set_halign(Gtk::Align::START); + name_lbl->set_hexpand(true); + if (is_connected) { + auto escaped = Glib::Markup::escape_text(net.ssid); + name_lbl->set_use_markup(true); + name_lbl->set_markup("" + + escaped + ""); + } + row->append(*name_lbl); + + if (is_connected) { + // Info button (shows details of current connection) + auto info_btn = Gtk::make_managed(); + info_btn->add_css_class("flat"); + info_btn->set_tooltip_text("Connection details"); + auto info_img = Gtk::make_managed(); + info_img->set_from_icon_name("dialog-information-symbolic"); + info_img->set_pixel_size(14); + info_btn->set_child(*info_img); + info_btn->signal_clicked().connect([this]() { show_connected_details(); }); + row->append(*info_btn); + + // Disconnect button + auto disc_btn = Gtk::make_managed(); + disc_btn->add_css_class("flat"); + disc_btn->set_tooltip_text("Disconnect"); + auto disc_img = Gtk::make_managed(); + disc_img->set_from_icon_name("network-offline-symbolic"); + disc_img->set_pixel_size(14); + disc_btn->set_child(*disc_img); + disc_btn->signal_clicked().connect( + [this]() { disconnect_current_network(); }); + row->append(*disc_btn); + + pop_list_box.append(*row); } else { - for (auto &net : networks) { - std::ostringstream label; - label << net.ssid << " (" << net.strength << "%)"; - if (net.secured) - label << " [Secured]"; - - auto btn = Gtk::make_managed(label.str()); - btn->set_halign(Gtk::Align::FILL); - - // Capture SSID and secured flag - btn->signal_clicked().connect([this, ssid = net.ssid, secured = net.secured]() { - if (!secured) { - attempt_connect_ssid(ssid, ""); - button->get_popover()->popdown(); - } else { - show_password_prompt_for(ssid); - } - }); - - pop_list_box.append(*btn); - } + if (net.secured) { + auto lock_img = Gtk::make_managed(); + lock_img->set_from_icon_name( + "network-wireless-encrypted-symbolic"); + lock_img->set_pixel_size(12); + row->append(*lock_img); + } + + auto btn = Gtk::make_managed(); + btn->set_child(*row); + btn->set_halign(Gtk::Align::FILL); + + btn->signal_clicked().connect( + [this, ssid = net.ssid, secured = net.secured]() { + if (!secured) { + attempt_connect_ssid(ssid, ""); + button->get_popover()->popdown(); + } else { + show_password_prompt_for(ssid); + } + }); + + pop_list_box.append(*btn); } + }; - pop_list_box.set_sensitive(true); - pop_status_label.set_text(""); // Clear scanning message - }); }) - .detach(); + // 1) Render connected (if found) + if (connected_index >= 0) { + render_row(networks[connected_index], true); + } + + // 2) Render the rest + for (size_t i = 0; i < networks.size(); ++i) { + if (static_cast(i) == connected_index) + continue; + render_row(networks[i], false); + } + } + pop_list_box.set_sensitive(true); + pop_status_label.set_text(""); // Clear scanning message + }); + }); // Status label at the bottom pop_status_label.set_margin_top(6); pop_list_box.append(pop_status_label); } -void WayfireNetworkInfo::show_password_prompt_for(const std::string &ssid) -{ +void WayfireNetworkInfo::show_password_prompt_for(const std::string &ssid) { // Clear popover content for (auto *child : popover_box.get_children()) popover_box.remove(*child); @@ -578,7 +1123,8 @@ void WayfireNetworkInfo::show_password_prompt_for(const std::string &ssid) // Title auto title = Gtk::make_managed("Connect to: " + ssid); - title->set_margin_bottom(6); + title->set_margin_bottom(12); + title->set_margin_top(12); popover_box.append(*title); // Password entry @@ -595,27 +1141,25 @@ void WayfireNetworkInfo::show_password_prompt_for(const std::string &ssid) hbox->set_halign(Gtk::Align::END); auto cancel_btn = Gtk::make_managed("Cancel"); - cancel_btn->signal_clicked().connect([this]() - { - // Restore Wi-Fi list - for (auto *child : popover_box.get_children()) - popover_box.remove(*child); - popover_box.append(pop_scrolled); - populate_wifi_list(); }); + cancel_btn->signal_clicked().connect([this]() { + // Restore Wi-Fi list + for (auto *child : popover_box.get_children()) + popover_box.remove(*child); + popover_box.append(pop_scrolled); + populate_wifi_list(); + }); auto connect_btn = Gtk::make_managed("Connect"); - connect_btn->signal_clicked().connect([this, ssid, entry, popover]() - { - std::string pwd = entry->get_text(); - if (pwd.empty()) - { - pop_status_label.set_text("Password cannot be empty"); - return; - } - pop_status_label.set_text("Connecting..."); - attempt_connect_ssid(ssid, pwd); - popover->popdown(); // ✅ now captured - }); + connect_btn->signal_clicked().connect([this, ssid, entry, popover]() { + std::string pwd = entry->get_text(); + if (pwd.empty()) { + pop_status_label.set_text("Password cannot be empty"); + return; + } + pop_status_label.set_text("Connecting..."); + attempt_connect_ssid(ssid, pwd); + popover->popdown(); // ✅ now captured + }); hbox->append(*cancel_btn); hbox->append(*connect_btn); @@ -630,50 +1174,46 @@ void WayfireNetworkInfo::show_password_prompt_for(const std::string &ssid) } void WayfireNetworkInfo::attempt_connect_ssid(const std::string &ssid, - const std::string &password) -{ - if (ssid.empty() || ssid.find_first_not_of(" \t\r\n") == std::string::npos) - { + const std::string &password) { + if (ssid.empty() || ssid.find_first_not_of(" \t\r\n") == std::string::npos) { pop_status_label.set_text("Invalid SSID"); return; } - try - { + try { auto bus = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SYSTEM); - auto nm = Gio::DBus::Proxy::create_sync(bus, - "org.freedesktop.NetworkManager", - "/org/freedesktop/NetworkManager", - "org.freedesktop.NetworkManager"); + auto nm = Gio::DBus::Proxy::create_sync( + bus, "org.freedesktop.NetworkManager", + "/org/freedesktop/NetworkManager", "org.freedesktop.NetworkManager"); // --- Find Wi-Fi device --- Glib::VariantBase devices_var; nm->get_cached_property(devices_var, "Devices"); - if (!devices_var) - { + if (!devices_var) { pop_status_label.set_text("No devices"); return; } - auto dev_array = Glib::VariantBase::cast_dynamic>>(devices_var); + auto dev_array = Glib::VariantBase::cast_dynamic< + Glib::Variant>>(devices_var); std::string wifi_device_path; - for (const auto &path : dev_array.get()) - { - auto dev_proxy = Gio::DBus::Proxy::create_sync(bus, "org.freedesktop.NetworkManager", path, "org.freedesktop.NetworkManager.Device"); + for (const auto &path : dev_array.get()) { + auto dev_proxy = Gio::DBus::Proxy::create_sync( + bus, "org.freedesktop.NetworkManager", path, + "org.freedesktop.NetworkManager.Device"); if (!dev_proxy) continue; Glib::VariantBase type_v; dev_proxy->get_cached_property(type_v, "DeviceType"); - auto type_var = Glib::VariantBase::cast_dynamic>(type_v); - if (type_var && type_var.get() == 2) - { + auto type_var = + Glib::VariantBase::cast_dynamic>(type_v); + if (type_var && type_var.get() == 2) { wifi_device_path = path; break; } } - if (wifi_device_path.empty()) - { + if (wifi_device_path.empty()) { pop_status_label.set_text("No Wi-Fi device"); return; } @@ -691,7 +1231,8 @@ void WayfireNetworkInfo::attempt_connect_ssid(const std::string &ssid, // ----- connection (a{sv}) std::map connection_map; - connection_map["type"] = Glib::Variant::create("802-11-wireless"); + connection_map["type"] = + Glib::Variant::create("802-11-wireless"); connection_map["id"] = Glib::Variant::create(ssid); connection_map["uuid"] = Glib::Variant::create(uuid); @@ -703,8 +1244,7 @@ void WayfireNetworkInfo::attempt_connect_ssid(const std::string &ssid, // ----- 802-11-wireless-security (a{sv}) std::map sec_map; bool use_security = !password.empty(); - if (use_security) - { + if (use_security) { sec_map["key-mgmt"] = Glib::Variant::create("wpa-psk"); sec_map["psk"] = Glib::Variant::create(password); } @@ -712,9 +1252,7 @@ void WayfireNetworkInfo::attempt_connect_ssid(const std::string &ssid, // ------------------------ // TOP-LEVEL SETTINGS (a{sa{sv}}) // ------------------------ - std::map< - Glib::ustring, - std::map> + std::map> settings_map; settings_map["connection"] = connection_map; @@ -722,14 +1260,15 @@ void WayfireNetworkInfo::attempt_connect_ssid(const std::string &ssid, if (use_security) settings_map["802-11-wireless-security"] = sec_map; - auto settings = Glib::Variant>>::create(settings_map); + auto settings = Glib::Variant< + std::map>>:: + create(settings_map); // ------------------------ // Object paths (o) // ------------------------ - auto device_path = Glib::Variant::create(wifi_device_path); + auto device_path = + Glib::Variant::create(wifi_device_path); // Access point path is "/" → NM autoselects AP matching SSID auto ap_path = Glib::Variant::create("/"); @@ -737,10 +1276,7 @@ void WayfireNetworkInfo::attempt_connect_ssid(const std::string &ssid, // ------------------------ // FINAL TUPLE (a{sa{sv}}, o, o) // ------------------------ - std::vector args_vec = { - settings, - device_path, - ap_path}; + std::vector args_vec = {settings, device_path, ap_path}; auto args = Glib::VariantContainerBase::create_tuple(args_vec); @@ -751,60 +1287,25 @@ void WayfireNetworkInfo::attempt_connect_ssid(const std::string &ssid, pop_status_label.set_text("Connecting..."); update_active_connection(); - } - catch (const Glib::Error &e) - { + } catch (const Glib::Error &e) { std::cerr << "Connect failed: " << e.what() << std::endl; pop_status_label.set_text("Failed: " + Glib::ustring(e.what())); - } - catch (...) - { + } catch (...) { pop_status_label.set_text("Unknown error"); } } /* --- widget lifecycle --- */ -void WayfireNetworkInfo::on_click() -{ +void WayfireNetworkInfo::on_click() { // Instantly show popover show_wifi_popover(); - - // Show scanning message - pop_status_label.set_text("Scanning for networks…"); - pop_list_box.set_sensitive(false); - while (auto child = pop_list_box.get_first_child()) - pop_list_box.remove(*child); - - // Launch scanning in background - std::thread([this]() - { - auto networks = get_available_networks(); - - Glib::signal_idle().connect_once([this, networks]() - { - std::vector ssids; - for (auto &w : networks) - ssids.push_back(w.ssid); - - update_network_list(ssids); // <-- pass ssids, not networks - pop_list_box.set_sensitive(true); - }); }) - .detach(); } -void WayfireNetworkInfo::init(Gtk::Box *container) -{ +void WayfireNetworkInfo::init(Gtk::Box *container) { // --- Setup the popover initial UI --- - m_popover_box.set_margin(10); - m_popover_box.set_spacing(6); - m_popover_box.append(m_status_label); - m_popover_box.append(m_spinner); - m_popover.set_child(m_popover_box); - m_spinner.start(); - - if (!setup_dbus()) - { + + if (!setup_dbus()) { enabled = false; return; } @@ -824,6 +1325,21 @@ void WayfireNetworkInfo::init(Gtk::Box *container) button->signal_clicked().connect( sigc::mem_fun(*this, &WayfireNetworkInfo::on_click)); + // Add right-click to open GNOME Control Center + auto right_click = Gtk::GestureClick::create(); + right_click->set_button(3); // secondary button + right_click->signal_pressed().connect([this](int n_press, double /*x*/, double /*y*/) + { + if (n_press == 1) + { + if (nm_proxy && info) + { + info->spawn_control_center(nm_proxy); + } + } + }); + button->add_controller(right_click); + button_content.set_valign(Gtk::Align::CENTER); button_content.append(icon); button_content.append(status); @@ -838,19 +1354,13 @@ void WayfireNetworkInfo::init(Gtk::Box *container) handle_config_reload(); } -void WayfireNetworkInfo::handle_config_reload() -{ - if (status_opt.value() == NETWORK_STATUS_ICON) - { - if (status.get_parent()) - { +void WayfireNetworkInfo::handle_config_reload() { + if (status_opt.value() == NETWORK_STATUS_ICON) { + if (status.get_parent()) { button_content.remove(status); } - } - else - { - if (!status.get_parent()) - { + } else { + if (!status.get_parent()) { button_content.append(status); } } @@ -859,47 +1369,254 @@ void WayfireNetworkInfo::handle_config_reload() update_status(); } -void WayfireNetworkInfo::update_network_list( - const std::vector &networks) -{ - while (auto child = pop_list_box.get_first_child()) - pop_list_box.remove(*child); +void WayfireNetworkInfo::disconnect_current_network() { + if (!nm_proxy) + return; - if (networks.empty()) - { - pop_status_label.set_text("No networks found."); + if (!active_connection_proxy) return; + + const Glib::ustring path = active_connection_proxy->get_object_path(); + if (path.empty()) + return; + + nm_proxy->call( + "DeactivateConnection", + [this](const Glib::RefPtr &result) { + try { + nm_proxy->call_finish(result); + pop_status_label.set_text("Disconnecting…"); + Glib::signal_timeout().connect_once( + [this]() { + pop_status_label.set_text(""); + auto pop = button ? button->get_popover() : nullptr; + if (pop && pop->get_visible()) + populate_wifi_list(); + }, + 600); + } catch (const Glib::Error &e) { + pop_status_label.set_text("Failed to disconnect: " + + Glib::ustring(e.what())); + } + }, + Glib::VariantContainerBase::create_tuple( + Glib::Variant::create( + Glib::DBusObjectPathString{path}))); +} + +// Helper: convert IPv4 prefix length (e.g. 24) to dotted netmask (255.255.255.0) +static std::string prefix_to_netmask(int prefix) { + if (prefix < 0 || prefix > 32) + return ""; + uint32_t mask = prefix == 0 ? 0 : (~0u << (32 - prefix)); + return std::to_string((mask >> 24) & 0xFF) + "." + + std::to_string((mask >> 16) & 0xFF) + "." + + std::to_string((mask >> 8) & 0xFF) + "." + + std::to_string(mask & 0xFF); +} + +void WayfireNetworkInfo::show_connected_details() { + if (!active_connection_proxy) { + return; // nothing to show } - pop_status_label.set_text(""); + auto popover = button->get_popover(); + if (!popover) + return; - for (const auto &ssid : networks) - { - auto row = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 6); - auto label = Gtk::make_managed(ssid); - label->set_hexpand(true); - row->append(*label); - - // Use a Button as a clickable row (GTK4 way) - auto btn = Gtk::make_managed(); - btn->set_child(*row); - btn->add_css_class("wifi-row"); - - btn->signal_clicked().connect([this, ssid]() - { - show_password_prompt_for(ssid); // adapt security as needed - }); + // Clear existing content + for (auto *child : popover_box.get_children()) + popover_box.remove(*child); + + auto title = Gtk::make_managed("Network Details"); + title->set_margin_top(8); + title->set_margin_bottom(8); + title->set_use_markup(true); + title->set_markup("Network Details"); + popover_box.append(*title); + + auto content = Gtk::make_managed(Gtk::Orientation::VERTICAL, 4); + content->set_margin_start(8); + content->set_margin_end(8); + content->set_margin_bottom(8); + + auto add_row = [content](const std::string &label, const std::string &value) { + auto h = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 6); + auto l = Gtk::make_managed(label + ":"); + l->set_xalign(0); + l->set_width_chars(13); + auto v = Gtk::make_managed(value.empty() ? "—" : value); + v->set_xalign(0); + v->set_selectable(true); + h->append(*l); + h->append(*v); + content->append(*h); + }; - pop_list_box.append(*btn); + // Basic info + std::string ssid = info ? info->connection_name : ""; + add_row("SSID", ssid); + + // Wireless specific (AP proxy) + std::string security; + std::string frequency_str; + std::string strength_str; + if (!current_ap_path.empty()) { + try { + auto ap_proxy = Gio::DBus::Proxy::create_sync( + connection, NM_DBUS_NAME, current_ap_path, "org.freedesktop.NetworkManager.AccessPoint"); + if (ap_proxy) { + Glib::VariantBase wpa_var, rsn_var, freq_var, strength_var; + ap_proxy->get_cached_property(wpa_var, "WpaFlags"); + ap_proxy->get_cached_property(rsn_var, "RsnFlags"); + ap_proxy->get_cached_property(freq_var, "Frequency"); + ap_proxy->get_cached_property(strength_var, "Strength"); + guint32 wpa_flags = 0, rsn_flags = 0; guchar strength = 0; guint32 freq = 0; + if (wpa_var) + wpa_flags = Glib::VariantBase::cast_dynamic>(wpa_var).get(); + if (rsn_var) + rsn_flags = Glib::VariantBase::cast_dynamic>(rsn_var).get(); + if (strength_var) + strength = Glib::VariantBase::cast_dynamic>(strength_var).get(); + if (freq_var) + freq = Glib::VariantBase::cast_dynamic>(freq_var).get(); + + security = (wpa_flags || rsn_flags) ? "Secured (WPA/WPA2)" : "Open"; + strength_str = std::to_string((int)strength) + "%"; + if (freq) + frequency_str = std::to_string(freq) + " MHz"; + } + } catch (const Glib::Error &e) { + security = "Unknown"; + } } + add_row("Security", security); + add_row("Strength", strength_str); + add_row("Frequency", frequency_str); + + // Active connection IP config objects + auto get_ip4_details = [this]() { + struct IP4Info { std::string addr; int prefix = -1; std::string gateway; std::vector dns; } info; + try { + Glib::Variant ip4_path_var; + active_connection_proxy->get_cached_property(ip4_path_var, "Ip4Config"); + auto path = ip4_path_var.get(); + if (!path.empty() && path != "/") { + auto ip4_proxy = Gio::DBus::Proxy::create_sync(connection, NM_DBUS_NAME, path, "org.freedesktop.NetworkManager.IP4Config"); + if (ip4_proxy) { + Glib::VariantBase addr_data_var; + ip4_proxy->get_cached_property(addr_data_var, "AddressData"); + if (addr_data_var) { + // AddressData: a( a{sv} ) -> represented as Variant of std::vector> maybe + // We'll attempt dynamic casts + typedef std::map Dict; + auto array_variant = Glib::VariantBase::cast_dynamic>>(addr_data_var); + auto vec = array_variant.get(); + if (!vec.empty()) { + auto &first = vec.front(); + auto it_addr = first.find("address"); + auto it_prefix = first.find("prefix"); + auto it_gateway = first.find("gateway"); + if (it_addr != first.end()) + info.addr = Glib::VariantBase::cast_dynamic>(it_addr->second).get(); + if (it_prefix != first.end()) + info.prefix = Glib::VariantBase::cast_dynamic>(it_prefix->second).get(); + if (it_gateway != first.end()) + info.gateway = Glib::VariantBase::cast_dynamic>(it_gateway->second).get(); + } + } + Glib::VariantBase dns_var; + ip4_proxy->get_cached_property(dns_var, "NameserverData"); + if (dns_var) { + typedef std::map Dict; + auto dns_vec = Glib::VariantBase::cast_dynamic>>(dns_var).get(); + for (auto &entry : dns_vec) { + auto it_addr = entry.find("address"); + if (it_addr != entry.end()) { + info.dns.push_back(Glib::VariantBase::cast_dynamic>(it_addr->second).get()); + } + } + } + } + } + } catch (...) {} + return info; + }; - pop_list_box.show(); -} + auto ip4 = get_ip4_details(); + add_row("IPv4 Address", ip4.addr); + std::string netmask = ip4.prefix >= 0 ? prefix_to_netmask(ip4.prefix) : ""; + add_row("IPv4 Prefix", ip4.prefix >= 0 ? std::to_string(ip4.prefix) : ""); + add_row("Subnet Mask", netmask); + add_row("Gateway", ip4.gateway); + if (!ip4.dns.empty()) { + std::string dns_join; + for (size_t i = 0; i < ip4.dns.size(); ++i) { + if (i) + dns_join += ", "; + dns_join += ip4.dns[i]; + } + add_row("DNS", dns_join); + } else { + add_row("DNS", ""); + } + + // IPv6 (simplified) + try { + Glib::Variant ip6_path_var; + active_connection_proxy->get_cached_property(ip6_path_var, "Ip6Config"); + auto path6 = ip6_path_var.get(); + std::string ipv6_addr; + if (!path6.empty() && path6 != "/") { + auto ip6_proxy = Gio::DBus::Proxy::create_sync(connection, NM_DBUS_NAME, path6, "org.freedesktop.NetworkManager.IP6Config"); + if (ip6_proxy) { + Glib::VariantBase addr6_var; + ip6_proxy->get_cached_property(addr6_var, "AddressData"); + if (addr6_var) { + typedef std::map Dict; + auto array_variant = Glib::VariantBase::cast_dynamic>>(addr6_var); + auto vec = array_variant.get(); + if (!vec.empty()) { + auto &first = vec.front(); + auto it = first.find("address"); + if (it != first.end()) { + ipv6_addr = Glib::VariantBase::cast_dynamic>(it->second).get(); + } + } + } + } + } + add_row("IPv6 Address", ipv6_addr); + } catch (...) { + add_row("IPv6 Address", ""); + } -void WayfireNetworkInfo::show_error(const std::string &message) -{ - m_status_label.set_text("Error: " + message); - m_spinner.stop(); + popover_box.append(*content); + + // Action buttons + auto actions = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 6); + actions->set_halign(Gtk::Align::END); + + auto back_btn = Gtk::make_managed("Back"); + back_btn->add_css_class("flat"); + back_btn->signal_clicked().connect([this]() { + // restore list + for (auto *child : popover_box.get_children()) + popover_box.remove(*child); + popover_box.append(pop_scrolled); + populate_wifi_list(); + }); + actions->append(*back_btn); + + auto close_btn = Gtk::make_managed("Close"); + close_btn->add_css_class("flat"); + close_btn->signal_clicked().connect([this]() { button->get_popover()->popdown(); }); + actions->append(*close_btn); + + popover_box.append(*actions); + pop_status_label.set_text(""); + popover_box.append(pop_status_label); + popover->present(); } WayfireNetworkInfo::~WayfireNetworkInfo() {} \ No newline at end of file diff --git a/src/panel/widgets/network.hpp b/src/panel/widgets/network.hpp index ae9b8f52..aad97027 100644 --- a/src/panel/widgets/network.hpp +++ b/src/panel/widgets/network.hpp @@ -11,8 +11,8 @@ #include #include #include +#include #include -#include #include #include @@ -68,7 +68,6 @@ class WayfireNetworkInfo : public WayfireWidget DBusProxy nm_proxy, active_connection_proxy; std::unique_ptr info; - std::string run_iwd_cmd(const std::string &cmd); // Use WayfireMenuButton like notification-center std::unique_ptr button; @@ -83,6 +82,10 @@ class WayfireNetworkInfo : public WayfireWidget Gtk::Box pop_pass_box; // inline password prompt Gtk::Label pop_status_label; // show temporary messages +Glib::DBusObjectPathString current_ap_path; + + + bool enabled = true; WfOption status_opt{"panel/network_status"}; WfOption status_color_opt{"panel/network_status_use_color"}; @@ -95,6 +98,7 @@ class WayfireNetworkInfo : public WayfireWidget DBusPropList invalidated); void on_click(); +void disconnect_current_network(); // wifi helper methods (use nmcli) void show_wifi_popover(); @@ -102,6 +106,7 @@ class WayfireNetworkInfo : public WayfireWidget void show_password_prompt_for(const std::string &ssid); void attempt_connect_ssid(const std::string &ssid, const std::string &password = ""); + void show_connected_details(); public: void update_icon(); @@ -110,19 +115,16 @@ class WayfireNetworkInfo : public WayfireWidget void init(Gtk::Box *container); void handle_config_reload(); virtual ~WayfireNetworkInfo(); - private: // Async scanning UI state - Gtk::Popover m_popover; - Gtk::Box m_popover_box{Gtk::Orientation::VERTICAL, 6}; - Gtk::Label m_status_label{"Scanning for networks..."}; - Gtk::Spinner m_spinner; - bool scanning = false; + + // Async scanning helper functions - void scan_networks_async(); + void trigger_wifi_scan_async(std::function callback); + void get_available_networks_async(std::function&)> callback); void update_network_list(const std::vector &networks); void show_error(const std::string &message); }; -#endif /* end of include guard: WIDGETS_NETWORK_HPP */ \ No newline at end of file +#endif /* end of include guard: WIDGETS_NETWORK_HPP */ From 54e3bcc9baf256003f61d64834c83a5e3440e69c Mon Sep 17 00:00:00 2001 From: rummanz Date: Mon, 17 Nov 2025 21:48:31 +0100 Subject: [PATCH 5/7] =?UTF-8?q?Removed=20nested=20async=20proxy=20calls,?= =?UTF-8?q?=20improve=20readablity=20-trigger=5Fwifi=5Fscan=5Fasync:=20Use?= =?UTF-8?q?s=20the=20existing=20`nm=5Fproxy`=20and=20does=20everything=20s?= =?UTF-8?q?ynchronously=20(devices=20=E2=86=92=20Wi=E2=80=91Fi=20device=20?= =?UTF-8?q?=E2=86=92=20RequestScan),=20then=20schedules=20a=20short=20time?= =?UTF-8?q?out=20before=20invoking=20the=20callback.=20Only=20the=20scan?= =?UTF-8?q?=20wait=20is=20async=20now.=20-get=5Favailable=5Fnetworks=5Fasy?= =?UTF-8?q?nc:=20Enumerates=20devices/APs=20synchronously=20via=20`call=5F?= =?UTF-8?q?sync`=20and=20cached=20properties,=20then=20calls=20the=20provi?= =?UTF-8?q?ded=20callback=20once=20with=20the=20full=20list.=20No=20nested?= =?UTF-8?q?=20async=20proxy=20creation/calls=20anymore.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/panel/widgets/network.cpp | 666 +++++++--------------------------- 1 file changed, 127 insertions(+), 539 deletions(-) diff --git a/src/panel/widgets/network.cpp b/src/panel/widgets/network.cpp index 57d3768e..885bb274 100644 --- a/src/panel/widgets/network.cpp +++ b/src/panel/widgets/network.cpp @@ -1,5 +1,4 @@ #include "network.hpp" -#include #include #include // sometimes needed for lower-level variant helpers #include @@ -16,7 +15,6 @@ #include #include #include -#include #include #include @@ -92,554 +90,143 @@ static std::string rgba_to_hex(const Gdk::RGBA &c) { void WayfireNetworkInfo::trigger_wifi_scan_async( std::function callback) { - // Use existing connection or create a new one synchronously - Glib::RefPtr conn = connection; - if (!conn) { - try { - conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SYSTEM); - } catch (const Glib::Error &e) { - std::cerr << "D-Bus connection error: " << e.what() << std::endl; - callback(); - return; - } + if (!nm_proxy) { + callback(); + return; } + try { + // Get devices synchronously + auto reply = nm_proxy->call_sync("GetDevices", Glib::VariantContainerBase()); + auto device_child = reply.get_child(0); + auto devices_variant = Glib::VariantBase::cast_dynamic< + Glib::Variant>>(device_child); + auto device_paths = devices_variant.get(); + + bool triggered = false; + for (const auto &path : device_paths) { + auto dev_proxy = Gio::DBus::Proxy::create_sync( + connection, NM_DBUS_NAME, path, + "org.freedesktop.NetworkManager.Device"); + if (!dev_proxy) + continue; + Glib::VariantBase dev_type_variant; + dev_proxy->get_cached_property(dev_type_variant, "DeviceType"); + guint32 dev_type = 0; + if (dev_type_variant) { + dev_type = Glib::VariantBase::cast_dynamic>(dev_type_variant).get(); + } + if (dev_type != 2) + continue; // not Wi-Fi - // Create NetworkManager proxy asynchronously - Gio::DBus::Proxy::create( - conn, NM_DBUS_NAME, "/org/freedesktop/NetworkManager", - "org.freedesktop.NetworkManager", - [callback, conn](const Glib::RefPtr &result) { - try { - auto nm_proxy = Gio::DBus::Proxy::create_finish(result); - - // Get devices to find WiFi device - nm_proxy->call( - "GetDevices", - [callback, conn, - nm_proxy](const Glib::RefPtr &result) { - try { - Glib::VariantContainerBase reply = - nm_proxy->call_finish(result); - auto device_child = reply.get_child(0); - auto devices_variant = Glib::VariantBase::cast_dynamic< - Glib::Variant>>( - device_child); - std::vector device_paths = - devices_variant.get(); - - // Find WiFi device and trigger scan - auto scan_state = std::make_shared>(false); - auto devices_checked = std::make_shared>(0); - int total_devices = device_paths.size(); - - if (total_devices == 0) { - callback(); // No devices found - return; - } + auto wifi_proxy = Gio::DBus::Proxy::create_sync( + connection, NM_DBUS_NAME, path, + "org.freedesktop.NetworkManager.Device.Wireless"); + if (!wifi_proxy) + continue; - for (const auto &path : device_paths) { - Gio::DBus::Proxy::create( - conn, NM_DBUS_NAME, path, - "org.freedesktop.NetworkManager.Device", - [callback, conn, path, scan_state, devices_checked, - total_devices]( - const Glib::RefPtr &result) { - try { - auto dev_proxy = - Gio::DBus::Proxy::create_finish(result); - - // Check device type - Glib::VariantBase dev_type_variant; - dev_proxy->get_cached_property(dev_type_variant, - "DeviceType"); - guint32 dev_type = 0; - if (dev_type_variant) { - Glib::Variant vtype = - Glib::VariantBase::cast_dynamic< - Glib::Variant>(dev_type_variant); - dev_type = vtype.get(); - } - - if (dev_type == 2) // NM_DEVICE_TYPE_WIFI - { - // Create wireless device proxy and trigger scan - Gio::DBus::Proxy::create( - conn, NM_DBUS_NAME, path, - "org.freedesktop.NetworkManager.Device." - "Wireless", - [callback, scan_state, devices_checked, - total_devices]( - const Glib::RefPtr - &result) { - try { - auto wifi_proxy = - Gio::DBus::Proxy::create_finish( - result); - // Trigger scan (RequestScan takes (a{sv}) - // - a tuple with an empty dict) Create - // empty dictionary a{sv} - std::map - scan_options; - auto scan_options_variant = Glib::Variant< - std::map>:: - create(scan_options); - // Wrap in tuple (a{sv}) - std::vector args_vec = - {scan_options_variant}; - auto args = Glib::VariantContainerBase:: - create_tuple(args_vec); - - wifi_proxy->call( - "RequestScan", - [callback, wifi_proxy, scan_state]( - const Glib::RefPtr< - Gio::AsyncResult> &result) { - try { - wifi_proxy->call_finish(result); - scan_state->store(true); - // Scan triggered successfully, - // wait a bit for it to complete - Glib::signal_timeout() - .connect_once( - [callback]() { - callback(); - }, - 1000); // Wait 1 second - // for scan to - // complete - } catch (const Glib::Error &e) { - std::cerr << "RequestScan error: " - << e.what() - << std::endl; - // Continue anyway - might have - // already scanned - Glib::signal_timeout() - .connect_once( - [callback]() { - callback(); - }, - 500); - } - }, - args); - } catch (const Glib::Error &e) { - std::cerr - << "Failed to create wireless proxy: " - << e.what() << std::endl; - int checked = ++(*devices_checked); - if (checked >= total_devices && - !scan_state->load()) { - callback(); - } - } - }); - } else { - int checked = ++(*devices_checked); - if (checked >= total_devices && - !scan_state->load()) { - callback(); // No WiFi device found - } - } - } catch (const Glib::Error &e) { - int checked = ++(*devices_checked); - if (checked >= total_devices && - !scan_state->load()) { - callback(); - } - } - }); - } - } catch (const Glib::Error &e) { - std::cerr << "GetDevices error: " << e.what() << std::endl; - callback(); - } - }, - Glib::VariantContainerBase()); - } catch (const Glib::Error &e) { - std::cerr << "Failed to create NM proxy: " << e.what() << std::endl; - callback(); - } - }); + // Build empty a{sv} tuple + std::map scan_options; + auto scan_options_variant = Glib::Variant< + std::map>::create(scan_options); + auto args = Glib::VariantContainerBase::create_tuple({scan_options_variant}); + try { + wifi_proxy->call_sync("RequestScan", args); + triggered = true; + } catch (const Glib::Error &e) { + std::cerr << "RequestScan error: " << e.what() << std::endl; + } + } + Glib::signal_timeout().connect_once([callback]() { callback(); }, + triggered ? 1000 : 500); + } catch (const Glib::Error &e) { + std::cerr << "GetDevices/scan error: " << e.what() << std::endl; + callback(); + } } void WayfireNetworkInfo::get_available_networks_async( std::function &)> callback) { - // Use shared state to safely track progress across async callbacks - struct ScanState { - std::vector all_networks; - std::atomic devices_processed{0}; - int total_devices = 0; - std::function &)> final_callback; - std::mutex networks_mutex; // Protect all_networks vector - - ScanState(std::function &)> cb) - : final_callback(cb) {} - - void check_complete() { - if (devices_processed.load() >= total_devices) { - final_callback(all_networks); - } - } - }; - - auto state = std::make_shared(callback); - - // Use existing connection or create a new one synchronously - // (The async work is in the D-Bus method calls, not the connection) - Glib::RefPtr conn = connection; - if (!conn) { - try { - conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SYSTEM); - } catch (const Glib::Error &e) { - std::cerr << "D-Bus connection error: " << e.what() << std::endl; - callback({}); - return; - } + std::vector networks; + if (!nm_proxy) { + callback(networks); + return; } + try { + auto reply = nm_proxy->call_sync("GetDevices", Glib::VariantContainerBase()); + auto device_child = reply.get_child(0); + auto devices_variant = Glib::VariantBase::cast_dynamic< + Glib::Variant>>(device_child); + auto device_paths = devices_variant.get(); - // Create NetworkManager proxy asynchronously - Gio::DBus::Proxy::create( - conn, NM_DBUS_NAME, "/org/freedesktop/NetworkManager", - "org.freedesktop.NetworkManager", - [state](const Glib::RefPtr &result) { - try { - auto nm_proxy = Gio::DBus::Proxy::create_finish(result); - - // Call GetDevices asynchronously (no parameters) - nm_proxy - ->call( - "GetDevices", - [state, - nm_proxy](const Glib::RefPtr &result) { - try { - Glib::VariantContainerBase reply = - nm_proxy->call_finish(result); - auto device_child = reply.get_child(0); - auto devices_variant = - Glib::VariantBase::cast_dynamic>>( - device_child); - std::vector device_paths = - devices_variant.get(); - - state->total_devices = device_paths.size(); - - if (state->total_devices == 0) { - state->final_callback({}); - return; - } - - // Process each device asynchronously - for (const auto &path : device_paths) { - // Create device proxy - Gio:: - DBus:: - Proxy:: - create( - nm_proxy->get_connection(), - NM_DBUS_NAME, path, - "org.freedesktop.NetworkManager.Device", - [state, path, nm_proxy]( - const Glib::RefPtr - &result) { - try { - auto dev_proxy = - Gio::DBus::Proxy::create_finish( - result); - - // Check device type - Glib::VariantBase dev_type_variant; - dev_proxy->get_cached_property( - dev_type_variant, "DeviceType"); - guint32 dev_type = 0; - if (dev_type_variant) { - Glib::Variant vtype = - Glib::VariantBase:: - cast_dynamic< - Glib::Variant< - guint32>>( - dev_type_variant); - dev_type = vtype.get(); - } + for (const auto &path : device_paths) { + auto dev_proxy = Gio::DBus::Proxy::create_sync( + connection, NM_DBUS_NAME, path, + "org.freedesktop.NetworkManager.Device"); + if (!dev_proxy) + continue; + Glib::VariantBase dev_type_variant; + dev_proxy->get_cached_property(dev_type_variant, "DeviceType"); + guint32 dev_type = 0; + if (dev_type_variant) + dev_type = Glib::VariantBase::cast_dynamic>(dev_type_variant).get(); + if (dev_type != 2) + continue; // only Wi-Fi + + auto wifi_proxy = Gio::DBus::Proxy::create_sync( + connection, NM_DBUS_NAME, path, + "org.freedesktop.NetworkManager.Device.Wireless"); + if (!wifi_proxy) + continue; - if (dev_type != - 2) // NM_DEVICE_TYPE_WIFI - { - state->devices_processed++; - state->check_complete(); - return; - } + auto aps_reply = wifi_proxy->call_sync("GetAllAccessPoints", + Glib::VariantContainerBase()); + auto ap_child = aps_reply.get_child(0); + auto ap_paths_variant = Glib::VariantBase::cast_dynamic< + Glib::Variant>>(ap_child); + auto ap_paths = ap_paths_variant.get(); - // Create wireless device proxy - Gio::DBus::Proxy::create(dev_proxy - ->get_connection(), - NM_DBUS_NAME, - path, - "org." - "freedeskt" - "op." - "NetworkMa" - "nager." - "Device." - "Wireless", - [state, - path](const Glib:: - RefPtr< - Gio:: - AsyncResult> - & - result) { - try { - auto wifi_proxy = - Gio::DBus:: - Proxy::create_finish( - result); - - // Get - // all - // access - // points - // (no - // parameters) - wifi_proxy - ->call( - "GetAllAccessPoints", - [state, - wifi_proxy]( - const Glib::RefPtr< - Gio:: - AsyncResult> - &result) { - try { - Glib::VariantContainerBase - aps_reply = - wifi_proxy - ->call_finish( - result); - auto ap_child = - aps_reply - .get_child( - 0); - auto ap_paths_variant = - Glib::VariantBase::cast_dynamic< - Glib::Variant< - std::vector< - Glib:: - DBusObjectPathString>>>( - ap_child); - std::vector< - Glib:: - DBusObjectPathString> - ap_paths = - ap_paths_variant - .get(); - - int total_aps = - ap_paths - .size(); - auto aps_processed = - std::make_shared< - std::atomic< - int>>( - 0); - - if (total_aps == - 0) { - state - ->devices_processed++; - state - ->check_complete(); - return; - } - - // Process each access point - for ( - const auto - &ap_path : - ap_paths) { - Gio::DBus:: - Proxy::create( - wifi_proxy - ->get_connection(), - NM_DBUS_NAME, - ap_path, - "org.freedesktop.NetworkManager.AccessPoint", - [state, - total_aps, - aps_processed]( - const Glib::RefPtr< - Gio:: - AsyncResult> - &result) { - try { - auto ap_proxy = - Gio::DBus:: - Proxy::create_finish( - result); - if (ap_proxy) { - Glib::VariantBase - ssid_var, - wpa_var, - rsn_var, - strength_var; - ap_proxy - ->get_cached_property( - ssid_var, - "Ssid"); - ap_proxy - ->get_cached_property( - wpa_var, - "WpaFlags"); - ap_proxy - ->get_cached_property( - rsn_var, - "RsnFlags"); - ap_proxy - ->get_cached_property( - strength_var, - "Strength"); - - std::string - ssid; - if (ssid_var) { - Glib::Variant< - std::vector< - guint8>> - ssid_bytes = Glib::VariantBase::cast_dynamic< - Glib::Variant< - std::vector< - guint8>>>( - ssid_var); - auto vec = - ssid_bytes - .get(); - ssid.assign( - vec.begin(), - vec.end()); - } - - guint32 - wpa_flags = - 0, - rsn_flags = - 0; - if (wpa_var) - wpa_flags = - Glib::VariantBase::cast_dynamic< - Glib::Variant< - guint32>>( - wpa_var) - .get(); - if (rsn_var) - rsn_flags = - Glib::VariantBase::cast_dynamic< - Glib::Variant< - guint32>>( - rsn_var) - .get(); - - int strength = - 0; - if (strength_var) - strength = - Glib::VariantBase::cast_dynamic< - Glib::Variant< - guchar>>( - strength_var) - .get(); - - bool secured = - (wpa_flags || - rsn_flags); - - if (!ssid.empty()) { - std::lock_guard< - std:: - mutex> - lock( - state - ->networks_mutex); - state - ->all_networks - .push_back( - {ssid, - ap_proxy - ->get_object_path(), - strength, - secured}); - } - } - - int processed = - ++(*aps_processed); - if (processed >= - total_aps) { - state - ->devices_processed++; - state - ->check_complete(); - } - } catch ( - const Glib::Error - &e) { - // Ignore errors for individual access points - int processed = - ++(*aps_processed); - if (processed >= - total_aps) { - state - ->devices_processed++; - state - ->check_complete(); - } - } - }); - } - } catch ( - const Glib::Error - &e) { - std::cerr - << "Error getting access points: " - << e.what() - << std:: - endl; - state - ->devices_processed++; - state - ->check_complete(); - } - }, - Glib:: - VariantContainerBase()); - } catch ( - const Glib::Error - &e) { - state - ->devices_processed++; - state - ->check_complete(); - } - }); - } catch (const Glib::Error &e) { - state->devices_processed++; - state->check_complete(); - } - }); - } - } catch (const Glib::Error &e) { - std::cerr - << "D-Bus error in get_available_networks_async(): " - << e.what() << std::endl; - state->final_callback({}); - } - }, - Glib::VariantContainerBase()); + for (const auto &ap_path : ap_paths) { + try { + auto ap_proxy = Gio::DBus::Proxy::create_sync( + connection, NM_DBUS_NAME, ap_path, + "org.freedesktop.NetworkManager.AccessPoint"); + if (!ap_proxy) + continue; + Glib::VariantBase ssid_var, wpa_var, rsn_var, strength_var; + ap_proxy->get_cached_property(ssid_var, "Ssid"); + ap_proxy->get_cached_property(wpa_var, "WpaFlags"); + ap_proxy->get_cached_property(rsn_var, "RsnFlags"); + ap_proxy->get_cached_property(strength_var, "Strength"); + + std::string ssid; + if (ssid_var) { + auto ssid_bytes = Glib::VariantBase::cast_dynamic< + Glib::Variant>>(ssid_var); + auto vec = ssid_bytes.get(); + ssid.assign(vec.begin(), vec.end()); + } + guint32 wpa_flags = 0, rsn_flags = 0; + if (wpa_var) + wpa_flags = Glib::VariantBase::cast_dynamic>(wpa_var).get(); + if (rsn_var) + rsn_flags = Glib::VariantBase::cast_dynamic>(rsn_var).get(); + int strength = 0; + if (strength_var) + strength = Glib::VariantBase::cast_dynamic>(strength_var).get(); + bool secured = (wpa_flags || rsn_flags); + if (!ssid.empty()) { + networks.push_back({ssid, ap_proxy->get_object_path(), strength, secured}); + } } catch (const Glib::Error &e) { - std::cerr << "Failed to create NM proxy: " << e.what() << std::endl; - state->final_callback({}); + // ignore AP-level errors } - }); + } + } + } catch (const Glib::Error &e) { + std::cerr << "D-Bus error in get_available_networks_async(): " << e.what() + << std::endl; + } + callback(networks); } std::string WfNetworkConnectionInfo::get_control_center_section(DBusProxy &nm) { @@ -1496,7 +1083,7 @@ void WayfireNetworkInfo::show_connected_details() { // Active connection IP config objects auto get_ip4_details = [this]() { - struct IP4Info { std::string addr; int prefix = -1; std::string gateway; std::vector dns; } info; + struct IP4Info { std::string addr; int prefix = -1; std::string gateway; std::vector dns; } info; try { Glib::Variant ip4_path_var; active_connection_proxy->get_cached_property(ip4_path_var, "Ip4Config"); @@ -1619,4 +1206,5 @@ void WayfireNetworkInfo::show_connected_details() { popover->present(); } -WayfireNetworkInfo::~WayfireNetworkInfo() {} \ No newline at end of file +WayfireNetworkInfo::~WayfireNetworkInfo() {} + From 86245f40025a0231836756d312026e0cc5ea37e0 Mon Sep 17 00:00:00 2001 From: rummanz Date: Tue, 18 Nov 2025 03:27:01 +0100 Subject: [PATCH 6/7] Add network sorting based on strength and make connected row clickable --- src/panel/widgets/network.cpp | 42 ++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/panel/widgets/network.cpp b/src/panel/widgets/network.cpp index 885bb274..e8eb2fc5 100644 --- a/src/panel/widgets/network.cpp +++ b/src/panel/widgets/network.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -226,6 +227,11 @@ void WayfireNetworkInfo::get_available_networks_async( std::cerr << "D-Bus error in get_available_networks_async(): " << e.what() << std::endl; } + // Sort by strength (descending). Tie-break by SSID for stable order. + std::sort(networks.begin(), networks.end(), [](const NetworkInfo &a, const NetworkInfo &b) { + if (a.strength != b.strength) return a.strength > b.strength; + return a.ssid < b.ssid; + }); callback(networks); } @@ -628,18 +634,19 @@ void WayfireNetworkInfo::populate_wifi_list() { row->append(*name_lbl); if (is_connected) { - // Info button (shows details of current connection) - auto info_btn = Gtk::make_managed(); - info_btn->add_css_class("flat"); - info_btn->set_tooltip_text("Connection details"); - auto info_img = Gtk::make_managed(); - info_img->set_from_icon_name("dialog-information-symbolic"); - info_img->set_pixel_size(14); - info_btn->set_child(*info_img); - info_btn->signal_clicked().connect([this]() { show_connected_details(); }); - row->append(*info_btn); - - // Disconnect button + // Wrap the content row in a button to get button visuals + auto main_btn = Gtk::make_managed(); + main_btn->set_child(*row); + main_btn->set_halign(Gtk::Align::FILL); + main_btn->set_hexpand(true); + main_btn->signal_clicked().connect([this]() { show_connected_details(); }); + + // Place main button and disconnect button side-by-side + auto line = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 6); + line->set_halign(Gtk::Align::FILL); + line->set_hexpand(true); + line->append(*main_btn); + auto disc_btn = Gtk::make_managed(); disc_btn->add_css_class("flat"); disc_btn->set_tooltip_text("Disconnect"); @@ -649,9 +656,9 @@ void WayfireNetworkInfo::populate_wifi_list() { disc_btn->set_child(*disc_img); disc_btn->signal_clicked().connect( [this]() { disconnect_current_network(); }); - row->append(*disc_btn); + line->append(*disc_btn); - pop_list_box.append(*row); + pop_list_box.append(*line); } else { if (net.secured) { auto lock_img = Gtk::make_managed(); @@ -1023,9 +1030,9 @@ void WayfireNetworkInfo::show_connected_details() { popover_box.append(*title); auto content = Gtk::make_managed(Gtk::Orientation::VERTICAL, 4); - content->set_margin_start(8); - content->set_margin_end(8); - content->set_margin_bottom(8); + content->set_margin_start(6); + content->set_margin_end(6); + content->set_margin_bottom(12); auto add_row = [content](const std::string &label, const std::string &value) { auto h = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 6); @@ -1207,4 +1214,3 @@ void WayfireNetworkInfo::show_connected_details() { } WayfireNetworkInfo::~WayfireNetworkInfo() {} - From 21829b6ca7c3f46f3088f87cdf802beeaf9d4e7f Mon Sep 17 00:00:00 2001 From: rummanz Date: Fri, 5 Dec 2025 00:18:27 +0100 Subject: [PATCH 7/7] Include network status, css classes, fixed duplicate connected networks while using wpa_supplicant as backend, keep connected network info while scanning. --- src/panel/widgets/network.cpp | 200 +++++++++++++++++++++++----------- 1 file changed, 135 insertions(+), 65 deletions(-) diff --git a/src/panel/widgets/network.cpp b/src/panel/widgets/network.cpp index e8eb2fc5..3b76e04c 100644 --- a/src/panel/widgets/network.cpp +++ b/src/panel/widgets/network.cpp @@ -12,12 +12,15 @@ #include #include #include +#include +#include #include #include #include #include #include #include +#include #define NM_DBUS_NAME "org.freedesktop.NetworkManager" #define ACTIVE_CONNECTION "PrimaryConnection" @@ -29,16 +32,26 @@ struct NetworkInfo { int strength = 0; // Wi-Fi signal strength (0–100) bool secured = false; // True if the network requires authentication }; -// Map 0-100 strength to GNOME symbolic icon names -static std::string icon_name_for_strength(int value) { +// Unified strength bucketing (0-100) used for labels and icons +static const char *strength_bucket(int value) { if (value > 80) - return "network-wireless-signal-excellent-symbolic"; + return "excellent"; if (value > 55) - return "network-wireless-signal-good-symbolic"; + return "good"; if (value > 30) - return "network-wireless-signal-ok-symbolic"; + return "ok"; if (value > 5) - return "network-wireless-signal-weak-symbolic"; + return "weak"; + return "none"; +} + +// Map bucketing to GNOME symbolic icon names +static std::string icon_name_for_strength(int value) { + std::string b = strength_bucket(value); + if (b == "excellent") return "network-wireless-signal-excellent-symbolic"; + if (b == "good") return "network-wireless-signal-good-symbolic"; + if (b == "ok") return "network-wireless-signal-ok-symbolic"; + if (b == "weak") return "network-wireless-signal-weak-symbolic"; return "network-wireless-signal-none-symbolic"; } @@ -228,6 +241,23 @@ void WayfireNetworkInfo::get_available_networks_async( << std::endl; } // Sort by strength (descending). Tie-break by SSID for stable order. + // Deduplicate by SSID: keep the strongest AP for each SSID + if (!networks.empty()) { + std::unordered_map best_by_ssid; + best_by_ssid.reserve(networks.size()); + for (const auto &n : networks) { + auto it = best_by_ssid.find(n.ssid); + if (it == best_by_ssid.end() || n.strength > it->second.strength) { + best_by_ssid[n.ssid] = n; + } else if (it != best_by_ssid.end()) { + // propagate secured=true if any AP for the SSID is secured + it->second.secured = it->second.secured || n.secured; + } + } + networks.clear(); + networks.reserve(best_by_ssid.size()); + for (auto &kv : best_by_ssid) networks.push_back(std::move(kv.second)); + } std::sort(networks.begin(), networks.end(), [](const NetworkInfo &a, const NetworkInfo &b) { if (a.strength != b.strength) return a.strength > b.strength; return a.ssid < b.ssid; @@ -305,19 +335,7 @@ struct WifiConnectionInfo : public WfNetworkConnectionInfo { return vstr.get(); } - std::string get_strength_str() { - int value = get_strength(); - - if (value > 80) - return "excellent"; - if (value > 55) - return "good"; - if (value > 30) - return "ok"; - if (value > 5) - return "weak"; - return "none"; - } + std::string get_strength_str() { return strength_bucket(get_strength()); } virtual std::string get_icon_name(WfConnectionState state) { if ((state <= CSTATE_ACTIVATING) || (state == CSTATE_DEACTIVATING)) @@ -441,6 +459,17 @@ void WayfireNetworkInfo::update_status() { status.set_use_markup(false); status.set_text(description); } + + // Sync CSS classes with strength buckets (like network.cpp.txt) + auto ctx = status.get_style_context(); + ctx->remove_class("excellent"); + ctx->remove_class("good"); + ctx->remove_class("ok"); + ctx->remove_class("weak"); + ctx->remove_class("none"); + if (status_color_opt) { + ctx->add_class(info->get_strength_str()); + } } void WayfireNetworkInfo::update_active_connection() { @@ -583,18 +612,78 @@ void WayfireNetworkInfo::populate_wifi_list() { for (auto *child : pop_list_box.get_children()) pop_list_box.remove(*child); - // Header - auto header = Gtk::make_managed("Available WiFi Networks"); - // header->set_margin_bottom(0); - pop_list_box.append(*header); + // Header placeholder: show scanning text + spinner here while scanning, + // then replace with the title when results arrive + auto header_line = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 6); + header_line->set_halign(Gtk::Align::CENTER); + header_line->set_hexpand(false); + auto header_label = Gtk::make_managed("Scanning for networks…"); + header_label->set_halign(Gtk::Align::CENTER); + header_label->set_xalign(0.5); + auto header_spinner = Gtk::make_managed(); + header_spinner->set_spinning(true); + header_line->append(*header_label); + header_line->append(*header_spinner); + pop_list_box.append(*header_line); // Show scanning message - pop_status_label.set_text("Scanning for networks…"); + pop_status_label.set_text(""); pop_list_box.set_sensitive(false); - trigger_wifi_scan_async([this]() { + // While scanning: show only the currently connected network (if any) + if (!current_ap_path.empty() && info) { + NetworkInfo cur{info->connection_name, current_ap_path, info->get_connection_strength(), true}; + auto row = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 8); + row->set_halign(Gtk::Align::FILL); + row->set_hexpand(true); + auto sig_img = Gtk::make_managed(); + sig_img->set_from_icon_name(icon_name_for_strength(cur.strength)); + sig_img->set_pixel_size(16); + row->append(*sig_img); + auto name_lbl = Gtk::make_managed(cur.ssid); + name_lbl->set_halign(Gtk::Align::START); + name_lbl->set_hexpand(true); + auto escaped = Glib::Markup::escape_text(cur.ssid); + name_lbl->set_use_markup(true); + name_lbl->set_markup("" + escaped + ""); + row->append(*name_lbl); + auto main_btn = Gtk::make_managed(); + main_btn->set_child(*row); + main_btn->set_halign(Gtk::Align::FILL); + main_btn->set_hexpand(true); + main_btn->signal_clicked().connect([this]() { show_connected_details(); }); + auto line = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 6); + line->set_halign(Gtk::Align::FILL); + line->set_hexpand(true); + line->append(*main_btn); + auto disc_btn = Gtk::make_managed(); + disc_btn->add_css_class("flat"); + disc_btn->set_tooltip_text("Disconnect"); + auto disc_img = Gtk::make_managed(); + disc_img->set_from_icon_name("network-offline-symbolic"); + disc_img->set_pixel_size(14); + disc_btn->set_child(*disc_img); + disc_btn->signal_clicked().connect([this]() { disconnect_current_network(); }); + line->append(*disc_btn); + pop_list_box.append(*line); + } + + trigger_wifi_scan_async([this, header_label, header_spinner]() { // Launch network scan asynchronously using D-Bus async interface - get_available_networks_async([this]( + get_available_networks_async([this, header_label, header_spinner]( const std::vector &networks) { + // Clear current temporary rows (including the connected preview) + for (auto *child : pop_list_box.get_children()) + pop_list_box.remove(*child); + + // Rebuild header as final title + auto final_header_line = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 6); + final_header_line->set_halign(Gtk::Align::CENTER); + final_header_line->set_hexpand(false); + auto final_header_label = Gtk::make_managed("Available WiFi Networks"); + final_header_label->set_halign(Gtk::Align::CENTER); + final_header_label->set_xalign(0.5); + final_header_line->append(*final_header_label); + pop_list_box.append(*final_header_line); if (networks.empty()) { auto lbl = Gtk::make_managed("No networks found"); lbl->set_margin(6); @@ -602,6 +691,7 @@ void WayfireNetworkInfo::populate_wifi_list() { } else { // Find connected entry (if any) so we can render it first int connected_index = -1; + // Prefer matching by AP object path if (!current_ap_path.empty()) { for (size_t i = 0; i < networks.size(); ++i) { if (Glib::ustring{networks[i].path} == current_ap_path) { @@ -610,6 +700,16 @@ void WayfireNetworkInfo::populate_wifi_list() { } } } + // Fallback: match by SSID when backend reports a different AP path + if (connected_index < 0 && info) { + const std::string connected_ssid = info->connection_name; + for (size_t i = 0; i < networks.size(); ++i) { + if (networks[i].ssid == connected_ssid) { + connected_index = static_cast(i); + break; + } + } + } auto render_row = [this](const NetworkInfo &net, bool is_connected) { auto row = @@ -701,11 +801,11 @@ void WayfireNetworkInfo::populate_wifi_list() { pop_list_box.set_sensitive(true); pop_status_label.set_text(""); // Clear scanning message + pop_status_label.set_margin_top(6); + pop_list_box.append(pop_status_label); }); }); - // Status label at the bottom - pop_status_label.set_margin_top(6); - pop_list_box.append(pop_status_label); + // Status label will be appended after results to avoid duplication } void WayfireNetworkInfo::show_password_prompt_for(const std::string &ssid) { @@ -725,14 +825,14 @@ void WayfireNetworkInfo::show_password_prompt_for(const std::string &ssid) { auto pass_label = Gtk::make_managed("Password:"); popover_box.append(*pass_label); - auto entry = Gtk::make_managed(); - entry->set_visibility(false); + auto entry = Gtk::make_managed(); + entry->set_show_peek_icon(true); entry->set_hexpand(true); popover_box.append(*entry); // Buttons auto hbox = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 6); - hbox->set_halign(Gtk::Align::END); + hbox->set_halign(Gtk::Align::CENTER); auto cancel_btn = Gtk::make_managed("Cancel"); cancel_btn->signal_clicked().connect([this]() { @@ -910,7 +1010,8 @@ void WayfireNetworkInfo::init(Gtk::Box *container) { button->get_children()[0]->get_style_context()->add_class("flat"); update_icon(); - button->set_child(icon); + // Use a container with icon + status so idle state shows connection info + button->set_child(button_content); container->append(*button); button->set_tooltip_text("Click to open Wi-Fi selector"); @@ -1142,7 +1243,6 @@ void WayfireNetworkInfo::show_connected_details() { std::string netmask = ip4.prefix >= 0 ? prefix_to_netmask(ip4.prefix) : ""; add_row("IPv4 Prefix", ip4.prefix >= 0 ? std::to_string(ip4.prefix) : ""); add_row("Subnet Mask", netmask); - add_row("Gateway", ip4.gateway); if (!ip4.dns.empty()) { std::string dns_join; for (size_t i = 0; i < ip4.dns.size(); ++i) { @@ -1155,36 +1255,6 @@ void WayfireNetworkInfo::show_connected_details() { add_row("DNS", ""); } - // IPv6 (simplified) - try { - Glib::Variant ip6_path_var; - active_connection_proxy->get_cached_property(ip6_path_var, "Ip6Config"); - auto path6 = ip6_path_var.get(); - std::string ipv6_addr; - if (!path6.empty() && path6 != "/") { - auto ip6_proxy = Gio::DBus::Proxy::create_sync(connection, NM_DBUS_NAME, path6, "org.freedesktop.NetworkManager.IP6Config"); - if (ip6_proxy) { - Glib::VariantBase addr6_var; - ip6_proxy->get_cached_property(addr6_var, "AddressData"); - if (addr6_var) { - typedef std::map Dict; - auto array_variant = Glib::VariantBase::cast_dynamic>>(addr6_var); - auto vec = array_variant.get(); - if (!vec.empty()) { - auto &first = vec.front(); - auto it = first.find("address"); - if (it != first.end()) { - ipv6_addr = Glib::VariantBase::cast_dynamic>(it->second).get(); - } - } - } - } - } - add_row("IPv6 Address", ipv6_addr); - } catch (...) { - add_row("IPv6 Address", ""); - } - popover_box.append(*content); // Action buttons @@ -1213,4 +1283,4 @@ void WayfireNetworkInfo::show_connected_details() { popover->present(); } -WayfireNetworkInfo::~WayfireNetworkInfo() {} +WayfireNetworkInfo::~WayfireNetworkInfo() {} \ No newline at end of file