This is an automated email from the git hooks/post-receive script.

git pushed a commit to branch master
in repository enlightenment.

View the commit online.

commit f6f9a0a87afc302d3b9ca7753e27dfd6de4bb852
Author: Carsten Haitzler <[email protected]>
AuthorDate: Tue May 26 18:53:11 2026 +0100

    network manager - add bt and improve usability
    
      * add bt connections
      * make connections mutually exclusive. eg select bt and wifi disconnects.
      * make wired connections able to be connected/disconnected
      * show which selected device connection or vpn as a selected item
      * make popup a little bigger by default
---
 src/modules/networkmanager/e_mod_main.c       | 514 ++++++++++++++++++++++++--
 src/modules/networkmanager/e_mod_main.h       |   3 +
 src/modules/networkmanager/e_networkmanager.c | 485 ++++++++++++++++++++++--
 src/modules/networkmanager/e_networkmanager.h |  51 +++
 4 files changed, 986 insertions(+), 67 deletions(-)

diff --git a/src/modules/networkmanager/e_mod_main.c b/src/modules/networkmanager/e_mod_main.c
index 38a7367c2..56bb02991 100644
--- a/src/modules/networkmanager/e_mod_main.c
+++ b/src/modules/networkmanager/e_mod_main.c
@@ -131,9 +131,11 @@ enm_popup_del(E_NM_Instance *inst)
     * fresh in _enm_popup_update. */
    inst->ui.popup.group_eth = NULL;
    inst->ui.popup.group_wifi = NULL;
+   inst->ui.popup.group_bt = NULL;
    inst->ui.popup.group_vpn = NULL;
    E_FREE_FUNC(inst->ui.popup.itc_group, elm_genlist_item_class_free);
    E_FREE_FUNC(inst->ui.popup.itc_group_wifi, elm_genlist_item_class_free);
+   E_FREE_FUNC(inst->ui.popup.itc_bt, elm_genlist_item_class_free);
    E_FREE_FUNC(inst->ui.popup.itc_group_vpn, elm_genlist_item_class_free);
    E_FREE_FUNC(inst->ui.popup.itc_ap, elm_genlist_item_class_free);
    E_FREE_FUNC(inst->ui.popup.itc_eth, elm_genlist_item_class_free);
@@ -175,6 +177,10 @@ static Evas_Object *_enm_ap_end_new(struct NM_Manager *nm,
                                      Evas_Object *parent);
 static Evas_Object *_enm_eth_icon_new(struct NM_Device *dev,
                                        Evas_Object *parent);
+static Evas_Object *_enm_bt_icon_new(struct NM_Manager *nm,
+                                      struct NM_Bluetooth_Connection *bc,
+                                      struct NM_Device *dev,
+                                      Evas_Object *parent);
 
 /* Per-item data for genlist AP and ethernet rows */
 typedef struct _Enm_Item_Data
@@ -182,8 +188,10 @@ typedef struct _Enm_Item_Data
    struct NM_Manager      *nm;
    struct NM_Access_Point *ap;   /* NULL for ethernet */
    struct NM_Device       *dev;
+   struct NM_Bluetooth_Connection *bt;
    const char             *ap_path;   /* stringshare — AP D-Bus object path */
    const char             *ssid;      /* stringshare */
+   const char             *connection_path; /* stringshare — bluetooth settings path */
 } Enm_Item_Data;
 
 static void
@@ -192,6 +200,7 @@ _enm_item_data_free(Enm_Item_Data *id)
    if (!id) return;
    eina_stringshare_del(id->ap_path);
    eina_stringshare_del(id->ssid);
+   eina_stringshare_del(id->connection_path);
    free(id);
 }
 
@@ -336,6 +345,64 @@ _enm_itc_eth_content_get(void *data, Evas_Object *obj, const char *part)
    return NULL;
 }
 
+static char *
+_enm_itc_bt_text_get(void *data, Evas_Object *obj EINA_UNUSED,
+                     const char *part)
+{
+   Enm_Item_Data *id = data;
+
+   if (!strcmp(part, "elm.text"))
+     return id->bt ? strdup(id->bt->name ?: _("Bluetooth")) : NULL;
+
+   if (!strcmp(part, "elm.text.sub"))
+     {
+        struct NM_Manager *nm = id->nm;
+
+        if (nm && id->bt && id->dev &&
+            nm->state >= NM_STATE_CONNECTED_LOCAL &&
+            nm->active_conn_type == NM_DEVICE_TYPE_BLUETOOTH &&
+            nm->ip_address)
+          return strdup(nm->ip_address);
+
+        return id->dev ? strdup(id->dev->interface ?: _("Bluetooth")) : NULL;
+     }
+
+   return NULL;
+}
+
+static Evas_Object *
+_enm_itc_bt_content_get(void *data, Evas_Object *obj, const char *part)
+{
+   Enm_Item_Data *id = data;
+
+   if (!id->bt) return NULL;
+
+   if (!strcmp(part, "elm.swallow.icon"))
+     {
+        Evas_Object *ic, *tbl, *rect;
+
+        tbl = elm_table_add(obj);
+
+        ic = _enm_bt_icon_new(id->nm, id->bt, id->dev, obj);
+        if (!ic)
+          {
+             evas_object_del(tbl);
+             return NULL;
+          }
+        evas_object_show(ic);
+        elm_table_pack(tbl, ic, 0, 0, 1, 1);
+
+        rect = evas_object_rectangle_add(evas_object_evas_get(obj));
+        evas_object_color_set(rect, 0, 0, 0, 0);
+        evas_object_size_hint_min_set(rect, ELM_SCALE_SIZE(32),
+                                      ELM_SCALE_SIZE(32));
+        elm_table_pack(tbl, rect, 0, 0, 1, 1);
+
+        return tbl;
+     }
+   return NULL;
+}
+
 /* ---- VPN item class callbacks --------------------------------------------- */
 
 static void
@@ -752,7 +819,43 @@ _enm_deselect_timer_cb(void *data)
    E_NM_Instance *inst = data;
 
    if (inst->ui.popup.deselect_item)
-     elm_genlist_item_selected_set(inst->ui.popup.deselect_item, EINA_FALSE);
+     {
+        Elm_Object_Item *it = inst->ui.popup.deselect_item;
+        Eina_Bool keep_selected = EINA_FALSE;
+        const Elm_Genlist_Item_Class *icl;
+
+        icl = elm_genlist_item_item_class_get(it);
+        if (icl == inst->ui.popup.itc_vpn)
+          {
+             struct NM_VPN_Connection *vc = elm_object_item_data_get(it);
+             keep_selected = !!(vc && vc->active_path);
+          }
+        else
+          {
+             Enm_Item_Data *id = elm_object_item_data_get(it);
+
+             if (id && id->nm)
+               {
+                  if (id->ap)
+                    keep_selected = _enm_ssid_is_active(id->nm, id->ap->ssid);
+                  else if (id->bt && id->dev)
+                    keep_selected =
+                       (id->dev->state >= NM_DEVICE_STATE_ACTIVATED) &&
+                       (id->nm->active_conn_type == NM_DEVICE_TYPE_BLUETOOTH) &&
+                       id->nm->active_connection_path &&
+                       id->dev->active_conn_path &&
+                       !strcmp(id->nm->active_connection_path,
+                               id->dev->active_conn_path);
+                  else if (id->dev)
+                    keep_selected =
+                       (id->dev->type == NM_DEVICE_TYPE_ETHERNET) &&
+                       (id->dev->state >= NM_DEVICE_STATE_ACTIVATED);
+               }
+          }
+
+        if (!keep_selected)
+          elm_genlist_item_selected_set(it, EINA_FALSE);
+     }
    inst->ui.popup.deselect_item = NULL;
    inst->ui.popup.deselect_timer = NULL;
    return ECORE_CALLBACK_CANCEL;
@@ -767,18 +870,20 @@ _enm_deselect_timer_schedule(E_NM_Instance *inst, Elm_Object_Item *it)
       ecore_timer_add(0.5, _enm_deselect_timer_cb, inst);
 }
 
-/* Activated smart callback — handles connect/disconnect on row tap */
+/* Genlist click smart callback — handles connect/disconnect on row tap */
 static void
-_enm_item_activated_cb(void *data, Evas_Object *obj EINA_UNUSED,
-                        void *event_info)
+_enm_item_clicked_cb(void *data, Evas_Object *obj EINA_UNUSED,
+                     void *event_info)
 {
    E_NM_Instance *inst = data;
    Elm_Object_Item *it = event_info;
    Enm_Item_Data *id;
    struct NM_Manager *nm;
    struct NM_Device *dev;
+   Eina_Bool selected = elm_genlist_item_selected_get(it);
 
    if (!it) return;
+   if (inst->ui.popup.syncing_selection) return;
    /* VPN rows carry a struct NM_VPN_Connection*, not Enm_Item_Data*.
     * Dispatch them inline and bail before any cast-as-Enm_Item_Data. */
    if (elm_genlist_item_item_class_get(it) == inst->ui.popup.itc_vpn)
@@ -793,15 +898,15 @@ _enm_item_activated_cb(void *data, Evas_Object *obj EINA_UNUSED,
               * activates.  Mirrors the wifi/ethernet row-tap UX. */
              if (vc->active_path)
                {
-                  DBG("VPN row tapped (active): deactivating '%s'",
+                  INF("VPN row tapped (active): deactivating '%s'\n",
                       vc->name ?: "?");
-                  enm_vpn_deactivate(ctxt->nm, vc);
+                  if (!selected) enm_vpn_deactivate(ctxt->nm, vc);
                }
              else
                {
-                  DBG("VPN row tapped (inactive): activating '%s'",
+                  INF("VPN row tapped (inactive): activating '%s'",
                       vc->name ?: "?");
-                  enm_vpn_activate(ctxt->nm, vc);
+                  if (selected) enm_vpn_activate(ctxt->nm, vc);
                }
           }
         return;
@@ -814,14 +919,56 @@ _enm_item_activated_cb(void *data, Evas_Object *obj EINA_UNUSED,
    nm = inst->ctxt->nm;
    if (!nm) return;
 
-   /* Ethernet row: no connect action from list tap */
-   if (!id->ap) return;
+   if (id->bt)
+     {
+        if (!id->dev || !id->connection_path) return;
+
+        if (nm->active_conn_type == NM_DEVICE_TYPE_BLUETOOTH &&
+            id->dev->state >= NM_DEVICE_STATE_ACTIVATED)
+          {
+             INF("Disconnect bluetooth connection %s",
+                 id->bt->name ?: id->connection_path);
+             if (!selected) enm_ap_disconnect(nm);
+          }
+        else
+          {
+             enm_disconnect_type(nm, NM_DEVICE_TYPE_ETHERNET);
+             enm_disconnect_type(nm, NM_DEVICE_TYPE_WIFI);
+             INF("Connect bluetooth profile %s on device %s",
+                 id->bt->name ?: id->connection_path,
+                 id->dev->interface ?: id->dev->path);
+             if (selected) enm_bluetooth_connect(nm, id->dev, id->connection_path);
+          }
+        return;
+     }
+
+   if (!id->ap)
+     {
+        if (!id->dev || id->dev->type != NM_DEVICE_TYPE_ETHERNET) return;
+
+        if (id->dev->state >= NM_DEVICE_STATE_ACTIVATED &&
+            id->dev->active_conn_path)
+          {
+             INF("Disconnect ethernet device %s",
+                 id->dev->interface ?: id->dev->path);
+             if (!selected) enm_disconnect_type(nm, NM_DEVICE_TYPE_ETHERNET);
+          }
+        else
+          {
+             enm_disconnect_type(nm, NM_DEVICE_TYPE_BLUETOOTH);
+             enm_disconnect_type(nm, NM_DEVICE_TYPE_WIFI);
+             INF("Connect ethernet device %s",
+                 id->dev->interface ?: id->dev->path);
+             if (selected) enm_ethernet_connect(nm, id->dev);
+          }
+        return;
+     }
 
    /* If this AP's SSID is currently active, disconnect */
    if (nm->active_ap_path && _enm_ssid_is_active(nm, id->ap->ssid))
      {
         INF("Disconnect from %s", id->ap->ssid ?: id->ap_path);
-        enm_ap_disconnect(nm);
+        if (!selected) enm_ap_disconnect(nm);
         return;
      }
 
@@ -831,12 +978,14 @@ _enm_item_activated_cb(void *data, Evas_Object *obj EINA_UNUSED,
         struct NM_Access_Point *a;
         EINA_INLIST_FOREACH(dev->access_points, a)
           {
-             if (a == id->ap)
+            if (a == id->ap)
                {
+                  enm_disconnect_type(nm, NM_DEVICE_TYPE_ETHERNET);
+                  enm_disconnect_type(nm, NM_DEVICE_TYPE_BLUETOOTH);
                   INF("Connect to %s on device %s",
                       id->ap->ssid ?: id->ap_path,
                       dev->interface ?: dev->path);
-                  enm_ap_connect(nm, dev, id->ap);
+                  if (selected) enm_ap_connect(nm, dev, id->ap);
                   return;
                }
           }
@@ -1019,8 +1168,8 @@ _enm_eth_icon_new(struct NM_Device *dev, Evas_Object *parent)
    evas_object_size_hint_align_set(icon, EVAS_HINT_FILL, EVAS_HINT_FILL);
    ed = elm_layout_edje_get(icon);
 
-   /* NM device state 100 = activated → ONLINE(5), otherwise IDLE(1) */
-   state_val = (dev->state >= 100) ? 5 : 1;
+   /* NM device state >= activated → ONLINE(5), otherwise IDLE(1) */
+   state_val = (dev->state >= NM_DEVICE_STATE_ACTIVATED) ? 5 : 1;
 
    msg = malloc(sizeof(*msg) + sizeof(int));
    if (msg)
@@ -1035,6 +1184,41 @@ _enm_eth_icon_new(struct NM_Device *dev, Evas_Object *parent)
    return icon;
 }
 
+static Evas_Object *
+_enm_bt_icon_new(struct NM_Manager *nm, struct NM_Bluetooth_Connection *bc,
+                 struct NM_Device *dev, Evas_Object *parent)
+{
+   Edje_Message_Int_Set *msg;
+   Evas_Object *icon, *ed;
+   int state_val = 0;
+
+   icon = elm_layout_add(parent);
+   _enm_theme_layout_file_set(icon, "icon/bluetooth");
+   evas_object_size_hint_weight_set(icon, EVAS_HINT_EXPAND, EVAS_HINT_EXPAND);
+   evas_object_size_hint_align_set(icon, EVAS_HINT_FILL, EVAS_HINT_FILL);
+   ed = elm_layout_edje_get(icon);
+
+   if (nm && bc && dev &&
+       nm->state >= NM_STATE_CONNECTED_LOCAL &&
+       nm->active_conn_type == NM_DEVICE_TYPE_BLUETOOTH &&
+       dev->state >= NM_DEVICE_STATE_ACTIVATED)
+     state_val = 5;
+   else if (dev)
+     state_val = 1;
+
+   msg = malloc(sizeof(*msg) + sizeof(int));
+   if (msg)
+     {
+        msg->count = 2;
+        msg->val[0] = state_val;
+        msg->val[1] = 100;
+        edje_object_message_send(ed, EDJE_MESSAGE_INT_SET, 1, msg);
+        free(msg);
+     }
+
+   return icon;
+}
+
 /* Find the best AP for a given SSID across all devices.
  * Prefers the active AP if it matches, otherwise picks highest strength.
  * Used to deduplicate multiple stations broadcasting the same network name. */
@@ -1077,10 +1261,17 @@ _enm_ssid_is_active(struct NM_Manager *nm, const char *ssid)
 /* Build the desired list of entries.  Returns count; caller frees labels/paths. */
 struct _Popup_Entry
 {
+   enum {
+      ENM_ENTRY_ETHERNET,
+      ENM_ENTRY_WIFI,
+      ENM_ENTRY_BLUETOOTH
+   } kind;
    const char *label;   /* SSID or interface name — owned by AP/dev, do not free */
    const char *ap_path; /* stringshare AP path for wifi, NULL for ethernet */
+   const char *connection_path; /* stringshare settings path for bluetooth */
    struct NM_Access_Point *ap; /* NULL for ethernet */
    struct NM_Device       *dev; /* only for ethernet */
+   struct NM_Bluetooth_Connection *bt;
 };
 
 static int
@@ -1093,17 +1284,38 @@ _enm_popup_build_entries(struct NM_Manager *nm,
 
    if (!nm) return 0;
 
-   /* Ethernet entries first */
+   /* Ethernet entries first: prioritize active connections,
+    * then include available idle devices. */
    EINA_INLIST_FOREACH(nm->devices, dev)
      {
         if (dev->type != NM_DEVICE_TYPE_ETHERNET) continue;
-        if (dev->state < 100) continue;
+        if (dev->state < NM_DEVICE_STATE_ACTIVATED) continue;
         if (n >= max) break;
 
+        out[n].kind = ENM_ENTRY_ETHERNET;
         out[n].label = dev->interface ?: _("Wired");
         out[n].ap_path = NULL;
+        out[n].connection_path = NULL;
         out[n].ap = NULL;
         out[n].dev = dev;
+        out[n].bt = NULL;
+        n++;
+     }
+
+   EINA_INLIST_FOREACH(nm->devices, dev)
+     {
+        if (dev->type != NM_DEVICE_TYPE_ETHERNET) continue;
+        if (dev->state >= NM_DEVICE_STATE_ACTIVATED) continue;
+        if (dev->state < NM_DEVICE_STATE_UNAVAILABLE) continue;
+        if (n >= max) break;
+
+        out[n].kind = ENM_ENTRY_ETHERNET;
+        out[n].label = dev->interface ?: _("Wired");
+        out[n].ap_path = NULL;
+        out[n].connection_path = NULL;
+        out[n].ap = NULL;
+        out[n].dev = dev;
+        out[n].bt = NULL;
         n++;
      }
 
@@ -1129,15 +1341,41 @@ _enm_popup_build_entries(struct NM_Manager *nm,
              eina_hash_add(seen_ssids, ap->ssid, (void *)1);
 
              if (n >= max) break;
+             out[n].kind = ENM_ENTRY_WIFI;
              out[n].label = best->ssid;
              out[n].ap_path = best->path;
+             out[n].connection_path = NULL;
              out[n].ap = best;
              out[n].dev = NULL;
+             out[n].bt = NULL;
              n++;
           }
      }
 
    eina_hash_free(seen_ssids);
+
+   {
+      struct NM_Bluetooth_Connection *bc;
+
+      EINA_INLIST_FOREACH(nm->bluetooth_connections, bc)
+        {
+           struct NM_Device *bt_dev;
+
+           if (n >= max) break;
+           bt_dev = enm_bluetooth_connection_device_find(nm, bc);
+           if (!bt_dev) continue;
+
+           out[n].kind = ENM_ENTRY_BLUETOOTH;
+           out[n].label = bc->name ?: _("Bluetooth");
+           out[n].ap_path = NULL;
+           out[n].connection_path = bc->path;
+           out[n].ap = NULL;
+           out[n].dev = bt_dev;
+           out[n].bt = bc;
+           n++;
+        }
+   }
+
    return n;
 }
 
@@ -1151,6 +1389,82 @@ _enm_has_wifi_device(struct NM_Manager *nm)
    return EINA_FALSE;
 }
 
+static Eina_Bool
+_enm_has_bt_entries(struct NM_Manager *nm)
+{
+   struct NM_Bluetooth_Connection *bc;
+
+   if (!nm) return EINA_FALSE;
+   EINA_INLIST_FOREACH(nm->bluetooth_connections, bc)
+     if (enm_bluetooth_connection_device_find(nm, bc))
+       return EINA_TRUE;
+   return EINA_FALSE;
+}
+
+static Eina_Bool
+_enm_popup_item_should_be_selected(E_NM_Instance *inst,
+                                   Elm_Object_Item *it)
+{
+   const Elm_Genlist_Item_Class *icl;
+
+   if (!it) return EINA_FALSE;
+
+   icl = elm_genlist_item_item_class_get(it);
+   if (icl == inst->ui.popup.itc_vpn)
+     {
+        struct NM_VPN_Connection *vc = elm_object_item_data_get(it);
+        return !!(vc && vc->active_path);
+     }
+
+   if ((icl == inst->ui.popup.itc_group) ||
+       (icl == inst->ui.popup.itc_group_wifi) ||
+       (icl == inst->ui.popup.itc_group_vpn))
+     return EINA_FALSE;
+
+   {
+      Enm_Item_Data *id = elm_object_item_data_get(it);
+
+      if (!id || !id->nm) return EINA_FALSE;
+
+      if (id->ap)
+        return _enm_ssid_is_active(id->nm, id->ap->ssid);
+
+      if (id->bt && id->dev)
+        return (id->dev->state >= NM_DEVICE_STATE_ACTIVATED) &&
+           (id->nm->active_conn_type == NM_DEVICE_TYPE_BLUETOOTH) &&
+           id->nm->active_connection_path &&
+           id->dev->active_conn_path &&
+           !strcmp(id->nm->active_connection_path, id->dev->active_conn_path);
+
+      if (id->dev)
+        return (id->dev->type == NM_DEVICE_TYPE_ETHERNET) &&
+           (id->dev->state >= NM_DEVICE_STATE_ACTIVATED);
+   }
+
+   return EINA_FALSE;
+}
+
+static void
+_enm_popup_selection_sync(E_NM_Instance *inst)
+{
+   Elm_Object_Item *it, *next;
+
+   if (!inst || !inst->ui.popup.genlist) return;
+
+   inst->ui.popup.syncing_selection = EINA_TRUE;
+   for (it = elm_genlist_first_item_get(inst->ui.popup.genlist);
+        it; it = next)
+     {
+        Eina_Bool want_selected;
+
+        next = elm_genlist_item_next_get(it);
+        want_selected = _enm_popup_item_should_be_selected(inst, it);
+        if (elm_genlist_item_selected_get(it) != want_selected)
+          elm_genlist_item_selected_set(it, want_selected);
+     }
+   inst->ui.popup.syncing_selection = EINA_FALSE;
+}
+
 /* Drop a tracked group-header pointer if it matches.  Used in the orphan
  * cleanup pass so we don't leave stale Elm_Object_Item * pointers in
  * inst->ui.popup.group_{eth,wifi,vpn}. */
@@ -1159,6 +1473,7 @@ _enm_drop_group_if(E_NM_Instance *inst, Elm_Object_Item *it)
 {
    if (inst->ui.popup.group_eth == it)  inst->ui.popup.group_eth = NULL;
    if (inst->ui.popup.group_wifi == it) inst->ui.popup.group_wifi = NULL;
+   if (inst->ui.popup.group_bt == it)   inst->ui.popup.group_bt = NULL;
    if (inst->ui.popup.group_vpn == it)  inst->ui.popup.group_vpn = NULL;
 }
 
@@ -1185,8 +1500,8 @@ _enm_popup_update(struct NM_Manager *nm, E_NM_Instance *inst)
 {
    Evas_Object *gl = inst->ui.popup.genlist;
    struct _Popup_Entry desired[256];
-   int want_n, i, eth_count = 0, wifi_count = 0;
-   Eina_Hash *eth_idx, *ap_idx, *vpn_idx;
+   int want_n, i, eth_count = 0, wifi_count = 0, bt_count = 0;
+   Eina_Hash *eth_idx, *ap_idx, *bt_idx, *vpn_idx;
    Eina_List *orphans = NULL;
    Elm_Object_Item *it, *prev;
    Eina_Iterator *itr;
@@ -1207,8 +1522,9 @@ _enm_popup_update(struct NM_Manager *nm, E_NM_Instance *inst)
    want_n = _enm_popup_build_entries(nm, desired, 256);
    for (i = 0; i < want_n; i++)
      {
-        if (desired[i].ap) wifi_count++;
-        else               eth_count++;
+        if (desired[i].kind == ENM_ENTRY_WIFI) wifi_count++;
+        else if (desired[i].kind == ENM_ENTRY_BLUETOOTH) bt_count++;
+        else eth_count++;
      }
 
    /* ------------------------------------------------------------------
@@ -1217,6 +1533,7 @@ _enm_popup_update(struct NM_Manager *nm, E_NM_Instance *inst)
     * ------------------------------------------------------------------ */
    eth_idx = eina_hash_pointer_new(NULL);   /* key: NM_Device *      */
    ap_idx  = eina_hash_string_superfast_new(NULL); /* key: ssid stringshare */
+   bt_idx  = eina_hash_string_superfast_new(NULL); /* key: settings path */
    vpn_idx = eina_hash_string_superfast_new(NULL); /* key: vc->path        */
 
    it = elm_genlist_first_item_get(gl);
@@ -1234,6 +1551,12 @@ _enm_popup_update(struct NM_Manager *nm, E_NM_Instance *inst)
              Enm_Item_Data *id = elm_object_item_data_get(it);
              if (id && id->ssid) eina_hash_add(ap_idx, id->ssid, it);
           }
+        else if (icl == inst->ui.popup.itc_bt)
+          {
+             Enm_Item_Data *id = elm_object_item_data_get(it);
+             if (id && id->connection_path)
+               eina_hash_add(bt_idx, id->connection_path, it);
+          }
         else if (icl == inst->ui.popup.itc_vpn)
           {
              struct NM_VPN_Connection *vc = elm_object_item_data_get(it);
@@ -1268,7 +1591,7 @@ _enm_popup_update(struct NM_Manager *nm, E_NM_Instance *inst)
 
         for (i = 0; i < want_n; i++)
           {
-             if (desired[i].ap) continue;
+             if (desired[i].kind != ENM_ENTRY_ETHERNET) continue;
              it = eina_hash_find(eth_idx, &desired[i].dev);
              if (it)
                {
@@ -1329,7 +1652,7 @@ _enm_popup_update(struct NM_Manager *nm, E_NM_Instance *inst)
 
         for (i = 0; i < want_n; i++)
           {
-             if (!desired[i].ap) continue;
+             if (desired[i].kind != ENM_ENTRY_WIFI) continue;
              it = eina_hash_find(ap_idx, desired[i].label);
              if (it)
                {
@@ -1377,7 +1700,67 @@ _enm_popup_update(struct NM_Manager *nm, E_NM_Instance *inst)
      }
 
    /* ------------------------------------------------------------------
-    * Pass 4: sync VPN section.  Always present.
+    * Pass 4: sync bluetooth section.
+    * ------------------------------------------------------------------ */
+   if (bt_count > 0 || _enm_has_bt_entries(nm))
+     {
+        if (!inst->ui.popup.group_bt)
+          {
+             inst->ui.popup.group_bt = elm_genlist_item_append(
+                gl, inst->ui.popup.itc_group, (void *)_("Bluetooth"), NULL,
+                ELM_GENLIST_ITEM_GROUP, NULL, NULL);
+             elm_genlist_item_select_mode_set(inst->ui.popup.group_bt,
+                ELM_OBJECT_SELECT_MODE_DISPLAY_ONLY);
+          }
+        prev = NULL;
+
+        for (i = 0; i < want_n; i++)
+          {
+             if (desired[i].kind != ENM_ENTRY_BLUETOOTH) continue;
+             it = eina_hash_find(bt_idx, desired[i].connection_path);
+             if (it)
+               {
+                  Enm_Item_Data *id = elm_object_item_data_get(it);
+                  if (id)
+                    {
+                       id->nm = nm;
+                       id->bt = desired[i].bt;
+                       id->dev = desired[i].dev;
+                    }
+                  elm_genlist_item_update(it);
+                  eina_hash_del(bt_idx, desired[i].connection_path, it);
+               }
+             else
+               {
+                  Enm_Item_Data *id = calloc(1, sizeof(*id));
+                  if (!id) continue;
+                  id->nm = nm;
+                  id->bt = desired[i].bt;
+                  id->dev = desired[i].dev;
+                  id->connection_path =
+                     eina_stringshare_add(desired[i].connection_path);
+                  if (!prev)
+                    it = elm_genlist_item_append(
+                       gl, inst->ui.popup.itc_bt, id,
+                       inst->ui.popup.group_bt,
+                       ELM_GENLIST_ITEM_NONE, NULL, NULL);
+                  else
+                    it = elm_genlist_item_insert_after(
+                       gl, inst->ui.popup.itc_bt, id,
+                       inst->ui.popup.group_bt, prev,
+                       ELM_GENLIST_ITEM_NONE, NULL, NULL);
+               }
+             prev = it;
+          }
+     }
+   else if (inst->ui.popup.group_bt)
+     {
+        elm_object_item_del(inst->ui.popup.group_bt);
+        inst->ui.popup.group_bt = NULL;
+     }
+
+   /* ------------------------------------------------------------------
+    * Pass 5: sync VPN section.  Always present.
     * ------------------------------------------------------------------ */
    {
       struct NM_VPN_Connection *vc;
@@ -1422,7 +1805,7 @@ _enm_popup_update(struct NM_Manager *nm, E_NM_Instance *inst)
    }
 
    /* ------------------------------------------------------------------
-    * Pass 5: any items left in the indexes are orphans — desired set
+    * Pass 6: any items left in the indexes are orphans — desired set
     * no longer contains them, so delete.  Their itc->func.del takes
     * care of freeing Enm_Item_Data; VPN rows have del=NULL because the
     * module owns NM_VPN_Connection lifetime.
@@ -1435,6 +1818,10 @@ _enm_popup_update(struct NM_Manager *nm, E_NM_Instance *inst)
    EINA_ITERATOR_FOREACH(itr, it) orphans = eina_list_append(orphans, it);
    eina_iterator_free(itr);
 
+   itr = eina_hash_iterator_data_new(bt_idx);
+   EINA_ITERATOR_FOREACH(itr, it) orphans = eina_list_append(orphans, it);
+   eina_iterator_free(itr);
+
    itr = eina_hash_iterator_data_new(vpn_idx);
    EINA_ITERATOR_FOREACH(itr, it) orphans = eina_list_append(orphans, it);
    eina_iterator_free(itr);
@@ -1452,8 +1839,11 @@ _enm_popup_update(struct NM_Manager *nm, E_NM_Instance *inst)
         elm_object_item_del(it);
      }
 
+   _enm_popup_selection_sync(inst);
+
    eina_hash_free(eth_idx);
    eina_hash_free(ap_idx);
+   eina_hash_free(bt_idx);
    eina_hash_free(vpn_idx);
 }
 
@@ -1523,6 +1913,8 @@ _enm_popup_new(E_NM_Instance *inst)
    evas_object_size_hint_weight_set(gl, EVAS_HINT_EXPAND, EVAS_HINT_EXPAND);
    evas_object_size_hint_align_set(gl, EVAS_HINT_FILL, EVAS_HINT_FILL);
    elm_scroller_bounce_set(gl, EINA_FALSE, EINA_TRUE);
+   elm_genlist_multi_select_set(gl, EINA_TRUE);
+   elm_genlist_select_mode_set(gl, ELM_OBJECT_SELECT_MODE_ALWAYS);
    evas_object_data_set(gl, "instance", inst);
    inst->ui.popup.genlist = gl;
 
@@ -1561,6 +1953,14 @@ _enm_popup_new(E_NM_Instance *inst)
    itc->func.del = _enm_itc_item_del;
    inst->ui.popup.itc_eth = itc;
 
+   itc = elm_genlist_item_class_new();
+   itc->item_style = "double_label";
+   itc->func.text_get = _enm_itc_bt_text_get;
+   itc->func.content_get = _enm_itc_bt_content_get;
+   itc->func.state_get = NULL;
+   itc->func.del = _enm_itc_item_del;
+   inst->ui.popup.itc_bt = itc;
+
    itc = elm_genlist_item_class_new();
    itc->item_style = "group_index";
    itc->func.text_get = _enm_itc_group_vpn_text_get;
@@ -1577,9 +1977,10 @@ _enm_popup_new(E_NM_Instance *inst)
    itc->func.del = NULL;
    inst->ui.popup.itc_vpn = itc;
 
-   /* Selected signal for row tap → connect/disconnect (single-click) */
-   evas_object_smart_callback_add(gl, "selected", _enm_item_activated_cb,
-                                   inst);
+   /* Click signal for row tap -> connect/disconnect. Avoid using selection
+    * callbacks here so already-selected active rows still react to clicks. */
+   evas_object_smart_callback_add(gl, "selected", _enm_item_clicked_cb, inst);
+   evas_object_smart_callback_add(gl, "unselected", _enm_item_clicked_cb, inst);
 
    /* Right-click context menu for VPN rows: use genlist's per-item
     * "clicked,right" smart signal.  event_info is the Elm_Object_Item. */
@@ -1616,9 +2017,9 @@ _enm_popup_new(E_NM_Instance *inst)
 
    {
       Evas_Object *wrapper = _enm_widget_size_wrap(inst, box,
-                                                    10, 30,
-                                                    192, 240,
-                                                    360, 400);
+                                                    15, 35,
+                                                    240, 240,
+                                                    480, 600);
       evas_object_show(wrapper);
       e_gadcon_popup_content_set(inst->popup, wrapper);
    }
@@ -1728,8 +2129,12 @@ _enm_mod_manager_update_inst(E_NM_Module_Context *ctxt EINA_UNUSED,
    int theme_state;
 
    /* Determine connection technology type for gadget icon */
-   typestr = (nm && nm->active_conn_type == NM_DEVICE_TYPE_ETHERNET)
-     ? "ethernet" : "wifi";
+   if (nm && nm->active_conn_type == NM_DEVICE_TYPE_ETHERNET)
+     typestr = "ethernet";
+   else if (nm && nm->active_conn_type == NM_DEVICE_TYPE_BLUETOOTH)
+     typestr = "bluetooth";
+   else
+     typestr = "wifi";
 
    /* Resolve active AP for real signal strength */
    if (nm && nm->active_ap_path)
@@ -1766,7 +2171,8 @@ _enm_mod_manager_update_inst(E_NM_Module_Context *ctxt EINA_UNUSED,
         struct NM_Device *dev;
         EINA_INLIST_FOREACH(nm->devices, dev)
           {
-             if (dev->type == NM_DEVICE_TYPE_ETHERNET && dev->state >= 100)
+             if (dev->type == NM_DEVICE_TYPE_ETHERNET &&
+                 dev->state >= NM_DEVICE_STATE_ACTIVATED)
                {
                   edje_object_part_text_set(o, "e.text.label",
                                             dev->interface ?: _("Wired"));
@@ -1774,6 +2180,26 @@ _enm_mod_manager_update_inst(E_NM_Module_Context *ctxt EINA_UNUSED,
                }
           }
      }
+   else if (nm && nm->active_conn_type == NM_DEVICE_TYPE_BLUETOOTH)
+     {
+        struct NM_Bluetooth_Connection *bc;
+        struct NM_Device *dev;
+        Eina_Bool found = EINA_FALSE;
+
+        EINA_INLIST_FOREACH(nm->bluetooth_connections, bc)
+          {
+             dev = enm_bluetooth_connection_device_find(nm, bc);
+             if (dev && dev->state >= NM_DEVICE_STATE_ACTIVATED)
+               {
+                  edje_object_part_text_set(o, "e.text.label",
+                                            bc->name ?: _("Bluetooth"));
+                  found = EINA_TRUE;
+                  break;
+               }
+          }
+        if (!found)
+          edje_object_part_text_set(o, "e.text.label", _("Bluetooth"));
+     }
    else
      edje_object_part_text_set(o, "e.text.label", "");
 
@@ -1837,7 +2263,7 @@ _enm_mod_manager_update_inst(E_NM_Module_Context *ctxt EINA_UNUSED,
                 EINA_INLIST_FOREACH(nm->devices, tdev)
                   {
                      if (tdev->type == NM_DEVICE_TYPE_ETHERNET &&
-                         tdev->state >= 100)
+                        tdev->state >= NM_DEVICE_STATE_ACTIVATED)
                        {
                           eina_strbuf_append(tbuf,
                               tdev->interface ?: _("Wired"));
@@ -2057,12 +2483,13 @@ _enm_active_interface(struct NM_Manager *nm)
 
    if (!nm) return NULL;
 
-   /* WiFi: find first connected WiFi device (state >= 100 = activated) */
+   /* WiFi: find first connected WiFi device (activated state) */
    if (nm->active_conn_type == NM_DEVICE_TYPE_WIFI)
      {
         EINA_INLIST_FOREACH(nm->devices, dev)
           {
-             if (dev->type == NM_DEVICE_TYPE_WIFI && dev->state >= 100)
+             if (dev->type == NM_DEVICE_TYPE_WIFI &&
+                 dev->state >= NM_DEVICE_STATE_ACTIVATED)
                return dev->interface;
           }
      }
@@ -2072,7 +2499,18 @@ _enm_active_interface(struct NM_Manager *nm)
      {
         EINA_INLIST_FOREACH(nm->devices, dev)
           {
-             if (dev->type == NM_DEVICE_TYPE_ETHERNET && dev->state >= 100)
+             if (dev->type == NM_DEVICE_TYPE_ETHERNET &&
+                 dev->state >= NM_DEVICE_STATE_ACTIVATED)
+               return dev->interface;
+          }
+     }
+
+   if (nm->active_conn_type == NM_DEVICE_TYPE_BLUETOOTH)
+     {
+        EINA_INLIST_FOREACH(nm->devices, dev)
+          {
+             if (dev->type == NM_DEVICE_TYPE_BLUETOOTH &&
+                 dev->state >= NM_DEVICE_STATE_ACTIVATED)
                return dev->interface;
           }
      }
diff --git a/src/modules/networkmanager/e_mod_main.h b/src/modules/networkmanager/e_mod_main.h
index 1a0ca106c..97615695e 100644
--- a/src/modules/networkmanager/e_mod_main.h
+++ b/src/modules/networkmanager/e_mod_main.h
@@ -39,16 +39,19 @@ struct E_NM_Instance
              Evas_Object            *genlist;
              Elm_Genlist_Item_Class *itc_group;
              Elm_Genlist_Item_Class *itc_group_wifi;
+             Elm_Genlist_Item_Class *itc_bt;
              Elm_Genlist_Item_Class *itc_group_vpn;   /* VPN section header */
              Elm_Genlist_Item_Class *itc_ap;
              Elm_Genlist_Item_Class *itc_eth;
              Elm_Genlist_Item_Class *itc_vpn;         /* VPN connection row */
              Ecore_Timer            *deselect_timer;
              Elm_Object_Item        *deselect_item;
+             Eina_Bool               syncing_selection;
              /* Section-header items, tracked across incremental updates so we
               * can lazily create/delete them without touching surrounding rows. */
              Elm_Object_Item        *group_eth;
              Elm_Object_Item        *group_wifi;
+             Elm_Object_Item        *group_bt;
              Elm_Object_Item        *group_vpn;
           } popup;
      } ui;
diff --git a/src/modules/networkmanager/e_networkmanager.c b/src/modules/networkmanager/e_networkmanager.c
index 089bf0d16..5f4ee1df9 100644
--- a/src/modules/networkmanager/e_networkmanager.c
+++ b/src/modules/networkmanager/e_networkmanager.c
@@ -1,5 +1,6 @@
 #include "e_mod_main.h"
 #include "e_networkmanager_vpn.h"
+#include <ctype.h>
 
 /* -------------------------------------------------------------------------- */
 /* D-Bus interface constants                                                   */
@@ -378,6 +379,203 @@ static void _manager_watch_ip4(struct NM_Manager *nm,
                                const char *ip4config_path);
 static void _try_wifi_adopt(struct NM_Device *dev);
 static void _try_ethernet_adopt(struct NM_Device *dev);
+static void _try_bluetooth_adopt(struct NM_Device *dev);
+
+static char *
+_nm_bt_addr_normalize(const char *addr)
+{
+   char buf[18];
+   int i = 0, j = 0;
+
+   if (!addr) return NULL;
+
+   while (addr[i] && j < 17)
+     {
+        if (isxdigit((unsigned char)addr[i]))
+          {
+             buf[j++] = toupper((unsigned char)addr[i]);
+             if ((j % 3) == 2 && j < 17) buf[j++] = ':';
+          }
+        i++;
+     }
+
+   if (j != 17) return NULL;
+   buf[17] = '\0';
+   return strdup(buf);
+}
+
+static char *
+_nm_bt_addr_from_iter(Eldbus_Message_Iter *variant)
+{
+   Eldbus_Message_Iter *bytes;
+   unsigned char b;
+   char buf[18];
+   int i = 0;
+
+   if (!variant) return NULL;
+   if (!eldbus_message_iter_arguments_get(variant, "ay", &bytes))
+     return NULL;
+
+   while (eldbus_message_iter_get_and_next(bytes, 'y', &b))
+     {
+        if (i >= 6) break;
+        snprintf(buf + (i * 3), sizeof(buf) - (i * 3),
+                 (i < 5) ? "%02X:" : "%02X", b);
+        i++;
+     }
+
+   if (i != 6) return NULL;
+   buf[17] = '\0';
+   return strdup(buf);
+}
+
+static void
+_nm_wired_connection_free(struct NM_Wired_Connection *wc)
+{
+   if (!wc) return;
+   eina_stringshare_del(wc->path);
+   free(wc->name);
+   free(wc->interface_name);
+   free(wc->hw_address);
+   free(wc);
+}
+
+static void
+_nm_wired_connections_clear(struct NM_Manager *nm)
+{
+   struct NM_Wired_Connection *wc;
+
+   if (!nm) return;
+   while (nm->wired_connections)
+     {
+        wc = EINA_INLIST_CONTAINER_GET(nm->wired_connections,
+                                       struct NM_Wired_Connection);
+        nm->wired_connections =
+           eina_inlist_remove(nm->wired_connections,
+                              nm->wired_connections);
+        _nm_wired_connection_free(wc);
+     }
+}
+
+static struct NM_Wired_Connection *
+_nm_wired_connection_add(struct NM_Manager *nm, const char *path,
+                         const char *name, const char *interface_name,
+                         const char *hw_address)
+{
+   struct NM_Wired_Connection *wc;
+
+   wc = calloc(1, sizeof(*wc));
+   if (!wc) return NULL;
+
+   wc->path = eina_stringshare_add(path);
+   wc->name = name ? strdup(name) : NULL;
+   wc->interface_name = interface_name ? strdup(interface_name) : NULL;
+   wc->hw_address = hw_address ? strdup(hw_address) : NULL;
+   nm->wired_connections =
+      eina_inlist_append(nm->wired_connections, EINA_INLIST_GET(wc));
+   return wc;
+}
+
+static struct NM_Wired_Connection *
+_nm_wired_connection_find_for_device(struct NM_Manager *nm, struct NM_Device *dev)
+{
+   struct NM_Wired_Connection *wc, *iface_match = NULL, *fallback = NULL;
+
+   if (!nm || !dev) return NULL;
+
+   EINA_INLIST_FOREACH(nm->wired_connections, wc)
+     {
+        if (wc->hw_address && dev->hw_address &&
+            !strcmp(wc->hw_address, dev->hw_address))
+          return wc;
+
+        if (!iface_match && wc->interface_name && dev->interface &&
+            !strcmp(wc->interface_name, dev->interface))
+          iface_match = wc;
+
+        if (!fallback && !wc->hw_address && !wc->interface_name)
+          fallback = wc;
+     }
+
+   if (iface_match) return iface_match;
+   return fallback;
+}
+
+static void
+_nm_bluetooth_connection_free(struct NM_Bluetooth_Connection *bc)
+{
+   if (!bc) return;
+   eina_stringshare_del(bc->path);
+   free(bc->name);
+   free(bc->bdaddr);
+   free(bc);
+}
+
+static void
+_nm_bluetooth_connections_clear(struct NM_Manager *nm)
+{
+   struct NM_Bluetooth_Connection *bc;
+
+   if (!nm) return;
+   while (nm->bluetooth_connections)
+     {
+        bc = EINA_INLIST_CONTAINER_GET(nm->bluetooth_connections,
+                                       struct NM_Bluetooth_Connection);
+        nm->bluetooth_connections =
+           eina_inlist_remove(nm->bluetooth_connections,
+                              nm->bluetooth_connections);
+        _nm_bluetooth_connection_free(bc);
+     }
+}
+
+static struct NM_Bluetooth_Connection *
+_nm_bluetooth_connection_add(struct NM_Manager *nm, const char *path,
+                             const char *name, const char *bdaddr)
+{
+   struct NM_Bluetooth_Connection *bc;
+
+   bc = calloc(1, sizeof(*bc));
+   if (!bc) return NULL;
+
+   bc->path = eina_stringshare_add(path);
+   bc->name = name ? strdup(name) : NULL;
+   bc->bdaddr = bdaddr ? strdup(bdaddr) : NULL;
+   nm->bluetooth_connections =
+      eina_inlist_append(nm->bluetooth_connections, EINA_INLIST_GET(bc));
+   return bc;
+}
+
+struct NM_Bluetooth_Connection *
+enm_bluetooth_connection_find(struct NM_Manager *nm, const char *connection_path)
+{
+   struct NM_Bluetooth_Connection *bc;
+
+   if (!nm || !connection_path) return NULL;
+   EINA_INLIST_FOREACH(nm->bluetooth_connections, bc)
+     if (bc->path && !strcmp(bc->path, connection_path))
+       return bc;
+   return NULL;
+}
+
+struct NM_Device *
+enm_bluetooth_connection_device_find(struct NM_Manager *nm,
+                                     struct NM_Bluetooth_Connection *bc)
+{
+   struct NM_Device *dev, *fallback = NULL;
+
+   if (!nm || !bc) return NULL;
+
+   EINA_INLIST_FOREACH(nm->devices, dev)
+     {
+        if (dev->type != NM_DEVICE_TYPE_BLUETOOTH) continue;
+        if (!fallback && !bc->bdaddr) fallback = dev;
+        if (bc->bdaddr && dev->hw_address &&
+            !strcmp(bc->bdaddr, dev->hw_address))
+          return dev;
+     }
+
+   return fallback;
+}
 
 static void
 _device_get_aps_cb(void *data, const Eldbus_Message *msg,
@@ -544,6 +742,8 @@ _device_prop_changed(void *data, const Eldbus_Message *msg)
      {
         if (dev->type == NM_DEVICE_TYPE_ETHERNET)
           _try_ethernet_adopt(dev);
+        else if (dev->type == NM_DEVICE_TYPE_BLUETOOTH)
+          _try_bluetooth_adopt(dev);
         else if (dev->type == NM_DEVICE_TYPE_WIFI)
           {
              /* Re-issue WiFi GetAll only if one is not already pending.
@@ -646,6 +846,44 @@ _try_wifi_adopt(struct NM_Device *dev)
    enm_saved_connections_get(nm_manager);
 }
 
+static void
+_try_bluetooth_adopt(struct NM_Device *dev)
+{
+   if (!nm_manager) return;
+   if (dev->state < 100 || !dev->active_conn_path) return;
+
+   if (nm_manager->active_connection_path) return;
+
+   DBG("Bluetooth device %s adopting connection %s (state=%u)",
+       dev->path, dev->active_conn_path, dev->state);
+
+   nm_manager->probe_generation++;
+   _manager_active_conn_watch_free(nm_manager);
+   _manager_ip4_watch_free(nm_manager);
+
+   eina_stringshare_replace(&nm_manager->active_connection_path,
+                            dev->active_conn_path);
+   nm_manager->active_conn_type = NM_DEVICE_TYPE_BLUETOOTH;
+   eina_stringshare_replace(&nm_manager->active_ap_path, NULL);
+
+   {
+      Eldbus_Object *aobj =
+         eldbus_object_get(conn, NM_BUS_NAME, dev->active_conn_path);
+      nm_manager->active_conn_obj = aobj;
+      nm_manager->active_conn_proxy = eldbus_proxy_get(aobj, NM_IFACE_PROPS);
+      nm_manager->active_conn_signal_handler =
+         eldbus_proxy_signal_handler_add(nm_manager->active_conn_proxy,
+                                         "PropertiesChanged",
+                                         _active_conn_prop_changed,
+                                         nm_manager);
+   }
+
+   if (dev->ip4_path)
+     _manager_watch_ip4(nm_manager, dev->ip4_path);
+
+   _notify_manager_update(nm_manager);
+}
+
 /* Handle PropertiesChanged on the Device.Wireless interface.
  * Watches for ActiveAccessPoint changes that may arrive after the initial
  * Device.Wireless.GetAll callback has already run (another cold-start window:
@@ -747,6 +985,18 @@ _device_get_props_cb(void *data, const Eldbus_Message *msg,
                   dev->interface = strdup(iface);
                }
           }
+        else if (!strcmp(key, "HwAddress"))
+          {
+             const char *addr;
+             char *norm;
+
+             if (eldbus_message_iter_arguments_get(var, "s", &addr))
+               {
+                  norm = _nm_bt_addr_normalize(addr);
+                  free(dev->hw_address);
+                  dev->hw_address = norm;
+               }
+          }
         else if (!strcmp(key, "DeviceType"))
           {
              uint32_t dtype;
@@ -829,6 +1079,10 @@ _device_get_props_cb(void *data, const Eldbus_Message *msg,
          * active_conn_path internally, and handles the "first wins" guard. */
         _try_ethernet_adopt(dev);
      }
+   else if (dev->type == NM_DEVICE_TYPE_BLUETOOTH)
+     {
+        _try_bluetooth_adopt(dev);
+     }
 }
 
 static void
@@ -939,6 +1193,7 @@ _device_free(struct NM_Device *dev)
      }
 
    free(dev->interface);
+   free(dev->hw_address);
    free(dev->active_conn_path);
    free(dev->active_ap_path);
    free(dev->ip4_path);
@@ -1297,10 +1552,10 @@ _active_conn_probe_cb(void *data, const Eldbus_Message *msg,
    /* NM uses "/" as the null/none sentinel for object paths */
    if (ap_path && !strcmp(ap_path, "/")) ap_path = NULL;
 
-   /* Skip non-network types (bridge, vpn, etc.) — they don't have
-    * useful SpecificObject or signal strength info. */
+   /* Skip non-network types (bridge, vpn, etc.). */
    if (conn_type != NM_DEVICE_TYPE_WIFI &&
-       conn_type != NM_DEVICE_TYPE_ETHERNET)
+       conn_type != NM_DEVICE_TYPE_ETHERNET &&
+       conn_type != NM_DEVICE_TYPE_BLUETOOTH)
      {
         DBG("ActiveConn type=%s (%d) — skipping", type_str ?: "?", conn_type);
         _active_conn_probe_free(probe);
@@ -1818,7 +2073,14 @@ _saved_conn_settings_cb(void *data, const Eldbus_Message *msg,
    struct _Saved_Conn_Ctx *ctx = data;
    Eldbus_Message_Iter *settings, *dict_entry, *setting_dict, *variant;
    const char *setting_name, *key;
+   const char *conn_type = NULL;
+   const char *conn_name = NULL;
+   const char *conn_iface = NULL;
    char ssid_str[256];
+   char *bt_addr = NULL;
+   char *eth_addr = NULL;
+
+   ssid_str[0] = '\0';
 
    /* Bail out if the manager was freed or a new batch was started while
     * this D-Bus call was in flight — avoids writing to freed/stale state. */
@@ -1851,43 +2113,86 @@ _saved_conn_settings_cb(void *data, const Eldbus_Message *msg,
                                                &setting_name, &setting_dict))
           continue;
 
-        if (strcmp(setting_name, "802-11-wireless") != 0)
-          continue;
-
         while (eldbus_message_iter_get_and_next(setting_dict, 'e', &inner_entry))
           {
              if (!eldbus_message_iter_arguments_get(inner_entry, "sv",
                                                     &key, &variant))
                continue;
 
-             if (!strcmp(key, "ssid"))
+             if (!strcmp(setting_name, "connection"))
                {
-                  Eldbus_Message_Iter *ssid_iter;
-                  unsigned char byte;
-                  int i = 0;
-
-                  if (!eldbus_message_iter_arguments_get(variant, "ay", &ssid_iter))
-                    continue;
-
-                  while (eldbus_message_iter_get_and_next(ssid_iter, 'y', &byte)
-                         && i < (int)(sizeof(ssid_str) - 1))
-                    ssid_str[i++] = (char)byte;
-                  ssid_str[i] = '\0';
-
-                  if (i > 0 && ctx->nm->saved_connections)
+                  if (!strcmp(key, "type"))
+                    eldbus_message_iter_arguments_get(variant, "s", &conn_type);
+                  else if (!strcmp(key, "id"))
+                    eldbus_message_iter_arguments_get(variant, "s", &conn_name);
+                  else if (!strcmp(key, "interface-name"))
+                    eldbus_message_iter_arguments_get(variant, "s", &conn_iface);
+               }
+             else if (!strcmp(setting_name, "802-11-wireless"))
+               {
+                  if (!strcmp(key, "ssid"))
                     {
-                       /* Remove old entry first to avoid leaking stringshare */
-                       eina_hash_del_by_key(ctx->nm->saved_connections, ssid_str);
-                       eina_hash_add(ctx->nm->saved_connections, ssid_str,
-                                     eina_stringshare_add(ctx->path));
-                       INF("saved_conn: added '%s' -> %s", ssid_str, ctx->path);
+                       Eldbus_Message_Iter *ssid_iter;
+                       unsigned char byte;
+                       int i = 0;
+
+                       if (!eldbus_message_iter_arguments_get(variant, "ay", &ssid_iter))
+                         continue;
+
+                       while (eldbus_message_iter_get_and_next(ssid_iter, 'y', &byte)
+                              && i < (int)(sizeof(ssid_str) - 1))
+                         ssid_str[i++] = (char)byte;
+                       ssid_str[i] = '\0';
+                    }
+               }
+             else if (!strcmp(setting_name, "bluetooth"))
+               {
+                  if (!strcmp(key, "bdaddr"))
+                    {
+                       free(bt_addr);
+                       bt_addr = _nm_bt_addr_from_iter(variant);
+                    }
+               }
+             else if (!strcmp(setting_name, "802-3-ethernet"))
+               {
+                  if (!strcmp(key, "mac-address"))
+                    {
+                       free(eth_addr);
+                       eth_addr = _nm_bt_addr_from_iter(variant);
                     }
-                  goto done;
                }
           }
      }
 
+   if (conn_type && !strcmp(conn_type, "802-11-wireless") &&
+       ssid_str[0] && ctx->nm->saved_connections)
+     {
+        eina_hash_del_by_key(ctx->nm->saved_connections, ssid_str);
+        eina_hash_add(ctx->nm->saved_connections, ssid_str,
+                      eina_stringshare_add(ctx->path));
+        INF("saved_conn: added '%s' -> %s", ssid_str, ctx->path);
+     }
+   else if (conn_type && !strcmp(conn_type, "bluetooth"))
+     {
+        _nm_bluetooth_connection_add(ctx->nm, ctx->path,
+                                     conn_name ?: _("Bluetooth"),
+                                     bt_addr);
+        INF("saved_bt_conn: added '%s' (%s) -> %s",
+            conn_name ?: _("Bluetooth"), bt_addr ?: "?", ctx->path);
+     }
+   else if (conn_type && !strcmp(conn_type, "802-3-ethernet"))
+     {
+        _nm_wired_connection_add(ctx->nm, ctx->path,
+                                 conn_name ?: _("Wired"),
+                                 conn_iface, eth_addr);
+        INF("saved_eth_conn: added '%s' (%s/%s) -> %s",
+            conn_name ?: _("Wired"), conn_iface ?: "?",
+            eth_addr ?: "?", ctx->path);
+     }
+
 done:
+   free(bt_addr);
+   free(eth_addr);
    eldbus_proxy_unref(ctx->proxy);
    eldbus_object_unref(ctx->obj);
    eina_stringshare_del(ctx->path);
@@ -1998,6 +2303,8 @@ enm_saved_connections_get(struct NM_Manager *nm)
                                     _saved_connections_free_cb);
          if (old) eina_hash_free(old);
       }
+      _nm_bluetooth_connections_clear(nm);
+      _nm_wired_connections_clear(nm);
 
       sctx->nm = nm;
       sctx->generation = nm->saved_conn_generation;
@@ -2249,6 +2556,8 @@ _manager_free(struct NM_Manager *nm)
         eina_hash_free(nm->saved_connections);
         nm->saved_connections = NULL;
      }
+   _nm_bluetooth_connections_clear(nm);
+   _nm_wired_connections_clear(nm);
 
    free(nm->ip_address);
    /* active_ap_path and active_connection_path already freed+NULLed
@@ -2406,6 +2715,23 @@ _deactivate_cb(void *data EINA_UNUSED, const Eldbus_Message *msg,
      INF("DeactivateConnection succeeded");
 }
 
+static void
+_enm_connection_deactivate(struct NM_Manager *nm, const char *connection_path)
+{
+   EINA_SAFETY_ON_NULL_RETURN(nm);
+   EINA_SAFETY_ON_NULL_RETURN(connection_path);
+
+   if (!strcmp(connection_path, "/"))
+     {
+        WRN("Refusing to disconnect null connection path");
+        return;
+     }
+
+   eldbus_proxy_call(nm->proxy, "DeactivateConnection",
+                     _deactivate_cb, NULL, -1,
+                     "o", connection_path);
+}
+
 void
 enm_ap_disconnect(struct NM_Manager *nm)
 {
@@ -2418,9 +2744,110 @@ enm_ap_disconnect(struct NM_Manager *nm)
         return;
      }
 
-   eldbus_proxy_call(nm->proxy, "DeactivateConnection",
-                     _deactivate_cb, NULL, -1,
-                     "o", nm->active_connection_path);
+   _enm_connection_deactivate(nm, nm->active_connection_path);
+}
+
+void
+enm_disconnect_type(struct NM_Manager *nm, enum NM_Device_Type type)
+{
+   struct NM_Device *dev;
+   Eina_Bool disconnected = EINA_FALSE;
+
+   EINA_SAFETY_ON_NULL_RETURN(nm);
+
+   EINA_INLIST_FOREACH(nm->devices, dev)
+     {
+        if (dev->type != type) continue;
+        if (dev->state < 100) continue;
+        if (!dev->active_conn_path) continue;
+
+        INF("Disconnect %s connection on device %s (%s)",
+            enm_device_type_to_str(type),
+            dev->interface ?: dev->path,
+            dev->active_conn_path);
+        _enm_connection_deactivate(nm, dev->active_conn_path);
+        disconnected = EINA_TRUE;
+     }
+
+   if (disconnected) return;
+
+   if ((nm->active_conn_type == type) &&
+       nm->active_connection_path &&
+       strcmp(nm->active_connection_path, "/"))
+     {
+        INF("Disconnect primary %s connection %s",
+            enm_device_type_to_str(type),
+            nm->active_connection_path);
+        _enm_connection_deactivate(nm, nm->active_connection_path);
+     }
+}
+
+void
+enm_bluetooth_connect(struct NM_Manager *nm, struct NM_Device *dev,
+                      const char *connection_path)
+{
+   struct connection_cb_data *cd;
+
+   EINA_SAFETY_ON_NULL_RETURN(nm);
+   EINA_SAFETY_ON_NULL_RETURN(dev);
+   EINA_SAFETY_ON_NULL_RETURN(connection_path);
+
+   cd = calloc(1, sizeof(*cd));
+   EINA_SAFETY_ON_NULL_RETURN(cd);
+
+   cd->nm = nm;
+   cd->dev = dev;
+
+   INF("ActivateConnection: bluetooth profile %s on device %s",
+       connection_path, dev->path);
+   eldbus_proxy_call(nm->proxy, "ActivateConnection",
+                     _activate_cb, cd, NM_CONNECTION_TIMEOUT,
+                     "ooo", connection_path, dev->path, "/");
+}
+
+void
+enm_ethernet_connect(struct NM_Manager *nm, struct NM_Device *dev)
+{
+   struct connection_cb_data *cd;
+   struct NM_Wired_Connection *wc;
+
+   EINA_SAFETY_ON_NULL_RETURN(nm);
+   EINA_SAFETY_ON_NULL_RETURN(dev);
+
+   cd = calloc(1, sizeof(*cd));
+   EINA_SAFETY_ON_NULL_RETURN(cd);
+
+   cd->nm = nm;
+   cd->dev = dev;
+   wc = _nm_wired_connection_find_for_device(nm, dev);
+
+   if (wc && wc->path)
+     {
+        INF("ActivateConnection: ethernet profile %s on device %s",
+            wc->path, dev->path);
+        eldbus_proxy_call(nm->proxy, "ActivateConnection",
+                          _activate_cb, cd, NM_CONNECTION_TIMEOUT,
+                          "ooo", wc->path, dev->path, "/");
+        return;
+     }
+
+   {
+      Eldbus_Message *msg_call;
+      Eldbus_Message_Iter *iter, *empty_dict;
+
+      INF("AddAndActivateConnection: no saved ethernet profile for device %s",
+          dev->path);
+
+      msg_call = eldbus_proxy_method_call_new(nm->proxy,
+                                              "AddAndActivateConnection");
+      iter = eldbus_message_iter_get(msg_call);
+      eldbus_message_iter_arguments_append(iter, "a{sa{sv}}", &empty_dict);
+      eldbus_message_iter_container_close(iter, empty_dict);
+      eldbus_message_iter_basic_append(iter, 'o', dev->path);
+      eldbus_message_iter_basic_append(iter, 'o', "/");
+      eldbus_proxy_send(nm->proxy, msg_call, _add_activate_cb, cd,
+                        NM_CONNECTION_TIMEOUT);
+   }
 }
 
 void
diff --git a/src/modules/networkmanager/e_networkmanager.h b/src/modules/networkmanager/e_networkmanager.h
index e320a0aaa..d27e26c67 100644
--- a/src/modules/networkmanager/e_networkmanager.h
+++ b/src/modules/networkmanager/e_networkmanager.h
@@ -99,6 +99,26 @@ enum NM_Device_Type
    NM_DEVICE_TYPE_MODEM     = 8,
 };
 
+/*
+ * NM device states, matching org.freedesktop.NetworkManager.Device.State
+ */
+enum NM_Device_State
+{
+   NM_DEVICE_STATE_UNKNOWN       = 0,
+   NM_DEVICE_STATE_UNMANAGED     = 10,
+   NM_DEVICE_STATE_UNAVAILABLE   = 20,
+   NM_DEVICE_STATE_DISCONNECTED  = 30,
+   NM_DEVICE_STATE_PREPARE       = 40,
+   NM_DEVICE_STATE_CONFIG        = 50,
+   NM_DEVICE_STATE_NEED_AUTH     = 60,
+   NM_DEVICE_STATE_IP_CONFIG     = 70,
+   NM_DEVICE_STATE_IP_CHECK      = 80,
+   NM_DEVICE_STATE_SECONDARIES   = 90,
+   NM_DEVICE_STATE_ACTIVATED     = 100,
+   NM_DEVICE_STATE_DEACTIVATING  = 110,
+   NM_DEVICE_STATE_FAILED        = 120,
+};
+
 /*
  * AP security flags — these are bitmask values from NM's WpaFlags / RsnFlags
  */
@@ -182,6 +202,25 @@ struct NM_Access_Point
    uint32_t      frequency;
 };
 
+struct NM_Bluetooth_Connection
+{
+   const char *path;               /* /org/freedesktop/NetworkManager/Settings/N (stringshare) */
+   EINA_INLIST;
+
+   char *name;                     /* connection.id */
+   char *bdaddr;                   /* normalized XX:XX:XX:XX:XX:XX, or NULL */
+};
+
+struct NM_Wired_Connection
+{
+   const char *path;               /* /org/freedesktop/NetworkManager/Settings/N (stringshare) */
+   EINA_INLIST;
+
+   char *name;                     /* connection.id */
+   char *interface_name;           /* connection.interface-name, or NULL */
+   char *hw_address;               /* normalized XX:XX:XX:XX:XX:XX, or NULL */
+};
+
 struct NM_Device
 {
    const char   *path;
@@ -195,6 +234,7 @@ struct NM_Device
    Eldbus_Signal_Handler *ap_removed_handler;
 
    char              *interface;
+   char              *hw_address;
    enum NM_Device_Type type;
    uint32_t            state;
 
@@ -257,6 +297,8 @@ struct NM_Manager
 
    /* Saved WiFi connections: SSID (string) -> connection D-Bus path (stringshare) */
    Eina_Hash    *saved_connections;
+   Eina_Inlist  *bluetooth_connections; /* NM_Bluetooth_Connection inlist */
+   Eina_Inlist  *wired_connections;     /* NM_Wired_Connection inlist */
    int           saved_conn_pending;    /* outstanding GetSettings calls */
    unsigned int  saved_conn_generation; /* increment to abort in-flight GetSettings */
 
@@ -300,6 +342,11 @@ void e_nm_scan(struct NM_Manager *nm);
 void enm_ap_connect(struct NM_Manager *nm, struct NM_Device *dev,
                     struct NM_Access_Point *ap);
 void enm_ap_disconnect(struct NM_Manager *nm);
+void enm_disconnect_type(struct NM_Manager *nm, enum NM_Device_Type type);
+void enm_bluetooth_connect(struct NM_Manager *nm,
+                           struct NM_Device *dev,
+                           const char *connection_path);
+void enm_ethernet_connect(struct NM_Manager *nm, struct NM_Device *dev);
 void enm_wireless_enabled_set(struct NM_Manager *nm, Eina_Bool enabled);
 
 /* Find AP across all devices */
@@ -342,6 +389,10 @@ const char *enm_vpn_type_label(const char *conn_type,
 /* Saved connections */
 void enm_saved_connections_get(struct NM_Manager *nm);
 void enm_connection_delete(struct NM_Manager *nm, const char *connection_path);
+struct NM_Bluetooth_Connection *enm_bluetooth_connection_find(
+   struct NM_Manager *nm, const char *connection_path);
+struct NM_Device *enm_bluetooth_connection_device_find(
+   struct NM_Manager *nm, struct NM_Bluetooth_Connection *bc);
 
 /* VPN data layer API: see e_networkmanager_vpn.h */
 

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to