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

git pushed a commit to branch nm-vpn-username-capture
in repository enlightenment.

View the commit online.

commit 2c5399a4bc0212172d12c790d746d4d7b7bc0b3b
Author: [email protected] <[email protected]>
AuthorDate: Sun Jun 21 20:48:24 2026 -0600

    docs: implementation plan for NM VPN username capture
    
    Six tasks: username predicate (+unit test), async nmcli writer, shared
    dialog + post-import probe, import connection-name reporting, connect-time
    username injection, and debug-printf cleanup.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---
 .../plans/2026-06-21-nm-vpn-username-capture.md    | 1096 ++++++++++++++++++++
 1 file changed, 1096 insertions(+)

diff --git a/docs/superpowers/plans/2026-06-21-nm-vpn-username-capture.md b/docs/superpowers/plans/2026-06-21-nm-vpn-username-capture.md
new file mode 100644
index 000000000..6d35b204f
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-21-nm-vpn-username-capture.md
@@ -0,0 +1,1096 @@
+# NM VPN Username Capture Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Capture a VPN username (a NetworkManager connection *property*, never prompted by the SecretAgent) both just after import and at connect time, so username-auth VPNs imported from `.ovpn` are usable.
+
+**Architecture:** A new `e_networkmanager_vpn_username.{c,h}` unit owns a pure predicate (`enm_vpn_username_needed`), an async `nmcli connection modify` writer (`enm_vpn_username_set`), a shared one-field dialog (`enm_vpn_username_dialog`), and a post-import orchestrator (`enm_vpn_username_maybe_prompt`). The post-import path (`e_mod_main.c`) calls the orchestrator after a successful import; the connect-time path (`agent.c` + `e_networkmanager.c`) detects the missing username from the al [...]
+
+**Tech Stack:** C, EFL (Eina/Elementary/Ecore), Eldbus, NetworkManager D-Bus + `nmcli`, meson.
+
+## Global Constraints
+
+- All code is **main-thread only** (Eldbus is bound to the Ecore main loop); follow the threading contract in `e_networkmanager_vpn.h`.
+- Reuse the existing `ecore_exe_pipe_run` + `_shell_escape_single` subprocess pattern from `e_networkmanager_import.c`; never build shell strings without escaping.
+- Username is a **property** (`vpn.data["username"]`), never sent through `e_nm_agent_reply_vpn_secrets`.
+- New source/header files MUST be added to `src/modules/networkmanager/meson.build`.
+- Bug references (full URLs) must appear verbatim in the new file's header comment:
+  - https://bugzilla.redhat.com/show_bug.cgi?id=1535517
+  - https://bugzilla.redhat.com/show_bug.cgi?id=1548873
+  - https://bbs.archlinux.org/viewtopic.php?id=286378
+  - https://bbs.archlinux.org/viewtopic.php?id=225395
+  - https://github.com/pop-os/cosmic-settings/issues/1820
+
+---
+
+## File Structure
+
+- **Create** `src/modules/networkmanager/e_networkmanager_vpn_username.c` — predicate, nmcli writer, shared dialog, post-import orchestrator.
+- **Create** `src/modules/networkmanager/e_networkmanager_vpn_username.h` — public declarations + callback typedefs.
+- **Create** `src/modules/networkmanager/test_vpn_username.c` — standalone unit test for the predicate (compiled by hand, not wired into the module build).
+- **Modify** `src/modules/networkmanager/meson.build` — add the two new module files.
+- **Modify** `src/modules/networkmanager/e_networkmanager_import.c` — capture stdout, parse connection name, extend done-cb.
+- **Modify** `src/modules/networkmanager/e_networkmanager_import.h` — add `conn_name` to `Enm_Import_Done_Cb`.
+- **Modify** `src/modules/networkmanager/e_mod_main.c` — call orchestrator on successful import.
+- **Modify** `src/modules/networkmanager/e_networkmanager.h` — add `need_username` + `conn_type` to the VPN request callback typedef.
+- **Modify** `src/modules/networkmanager/e_networkmanager.c` — detect missing username; pass new args; drop debug printfs.
+- **Modify** `src/modules/networkmanager/agent.c` — inject username row; persist on OK; drop debug printfs.
+
+---
+
+### Task 1: Username predicate + unit test + new unit skeleton
+
+**Files:**
+- Create: `src/modules/networkmanager/e_networkmanager_vpn_username.h`
+- Create: `src/modules/networkmanager/e_networkmanager_vpn_username.c`
+- Create: `src/modules/networkmanager/test_vpn_username.c`
+- Modify: `src/modules/networkmanager/meson.build`
+
+**Interfaces:**
+- Produces: `Eina_Bool enm_vpn_username_needed(const char *svc_short, const char *conn_type, const char *current_username)` — true when the VPN type uses username auth and `current_username` is NULL/empty.
+
+- [ ] **Step 1: Write the failing test**
+
+Create `src/modules/networkmanager/test_vpn_username.c`:
+
+```c
+/* Standalone unit test for enm_vpn_username_needed.
+ * Build: gcc -DENM_VPN_USERNAME_TEST test_vpn_username.c -o /tmp/t && /tmp/t */
+#include <assert.h>
+#include <stdio.h>
+
+#define ENM_VPN_USERNAME_TEST 1
+#include "e_networkmanager_vpn_username.c"
+
+int main(void)
+{
+   /* openvpn: only password / password-tls need a username */
+   assert(enm_vpn_username_needed("openvpn", "password", NULL));
+   assert(enm_vpn_username_needed("openvpn", "password-tls", ""));
+   assert(!enm_vpn_username_needed("openvpn", "tls", NULL));
+   assert(!enm_vpn_username_needed("openvpn", NULL, NULL));
+
+   /* already has a username -> never needed */
+   assert(!enm_vpn_username_needed("openvpn", "password", "alice"));
+
+   /* xauth / password-based types always need it when empty */
+   assert(enm_vpn_username_needed("pptp", NULL, NULL));
+   assert(enm_vpn_username_needed("l2tp", NULL, ""));
+   assert(enm_vpn_username_needed("vpnc", NULL, NULL));
+   assert(enm_vpn_username_needed("libreswan", NULL, NULL));
+   assert(!enm_vpn_username_needed("libreswan", NULL, "bob"));
+
+   /* unknown / NULL type -> not needed */
+   assert(!enm_vpn_username_needed("wireguard", NULL, NULL));
+   assert(!enm_vpn_username_needed(NULL, NULL, NULL));
+
+   printf("all enm_vpn_username_needed assertions passed\n");
+   return 0;
+}
+```
+
+- [ ] **Step 2: Create the header**
+
+Create `src/modules/networkmanager/e_networkmanager_vpn_username.h`:
+
+```c
+#ifndef E_NETWORKMANAGER_VPN_USERNAME_H
+#define E_NETWORKMANAGER_VPN_USERNAME_H
+
+#include <Eina.h>
+
+/*
+ * VPN username capture.
+ *
+ * NetworkManager stores a VPN username as a connection *property*
+ * (vpn.data["username"]), not as a secret, and the SecretAgent D-Bus API
+ * (GetSecrets) only ever deals with secrets.  NM therefore never prompts for
+ * a missing username: importing an .ovpn with a bare `auth-user-pass` yields a
+ * connection with password-flags set but no username, which is unusable.
+ *
+ * This long-standing NetworkManager behaviour is reported in:
+ *   - https://bugzilla.redhat.com/show_bug.cgi?id=1535517
+ *   - https://bugzilla.redhat.com/show_bug.cgi?id=1548873
+ *   - https://bbs.archlinux.org/viewtopic.php?id=286378
+ *   - https://bbs.archlinux.org/viewtopic.php?id=225395
+ *   - https://github.com/pop-os/cosmic-settings/issues/1820
+ *
+ * All functions here are MAIN THREAD ONLY.
+ */
+
+/* True when the VPN type uses username auth and current_username is empty.
+ * svc_short is the short service name ("openvpn", "pptp", ...); conn_type is
+ * vpn.data["connection-type"] (may be NULL). */
+Eina_Bool enm_vpn_username_needed(const char *svc_short,
+                                  const char *conn_type,
+                                  const char *current_username);
+
+/* Called with the entered username (NULL on cancel). */
+typedef void (*Enm_Username_Entered_Cb)(void *data, const char *username);
+
+/* Called when the nmcli modify completes. */
+typedef void (*Enm_Username_Done_Cb)(void *data, Eina_Bool ok);
+
+/* Persist username to vpn.data["username"] via `nmcli connection modify`. */
+void enm_vpn_username_set(const char *conn_name, const char *username,
+                          Enm_Username_Done_Cb cb, void *data);
+
+/* Single-field username dialog.  Calls cb(username) on OK, cb(NULL) on cancel. */
+void enm_vpn_username_dialog(const char *conn_name, const char *type_label,
+                             const char *initial,
+                             Enm_Username_Entered_Cb cb, void *data);
+
+/* Post-import orchestrator: query the connection, and if it needs a username,
+ * prompt for one and persist it.  Fire-and-forget. */
+void enm_vpn_username_maybe_prompt(const char *conn_name);
+
+#endif
+```
+
+- [ ] **Step 3: Create the source with the predicate (EFL parts guarded out for the test)**
+
+Create `src/modules/networkmanager/e_networkmanager_vpn_username.c`:
+
+```c
+#ifdef ENM_VPN_USERNAME_TEST
+/* Standalone unit-test build: no EFL, no config.h. */
+typedef unsigned char Eina_Bool;
+# define EINA_TRUE  1
+# define EINA_FALSE 0
+# include <string.h>
+#else
+# ifdef HAVE_CONFIG_H
+#  include "config.h"
+# endif
+# include <string.h>
+# include <stdlib.h>
+# include "e.h"
+# include "e_networkmanager.h"
+# include "e_networkmanager_import.h"
+# include "e_networkmanager_vpn_username.h"
+#endif
+
+Eina_Bool
+enm_vpn_username_needed(const char *svc_short, const char *conn_type,
+                        const char *current_username)
+{
+   if (current_username && current_username[0]) return EINA_FALSE;
+   if (!svc_short) return EINA_FALSE;
+
+   if (!strcmp(svc_short, "openvpn"))
+     {
+        if (!conn_type) return EINA_FALSE;
+        return (!strcmp(conn_type, "password") ||
+                !strcmp(conn_type, "password-tls"))
+               ? EINA_TRUE : EINA_FALSE;
+     }
+
+   if (!strcmp(svc_short, "pptp")        || !strcmp(svc_short, "l2tp") ||
+       !strcmp(svc_short, "fortisslvpn") || !strcmp(svc_short, "vpnc") ||
+       !strcmp(svc_short, "openswan")    || !strcmp(svc_short, "libreswan") ||
+       !strcmp(svc_short, "strongswan"))
+     return EINA_TRUE;
+
+   return EINA_FALSE;
+}
+
+#ifndef ENM_VPN_USERNAME_TEST
+/* enm_vpn_username_set / enm_vpn_username_dialog / enm_vpn_username_maybe_prompt
+ * are added in Tasks 2 and 3. */
+#endif
+```
+
+- [ ] **Step 4: Run the test to verify it fails to build (function present but unit not yet complete is fine — confirm the predicate logic passes)**
+
+Run: `cd src/modules/networkmanager && gcc -DENM_VPN_USERNAME_TEST test_vpn_username.c -o /tmp/t && /tmp/t`
+Expected: prints `all enm_vpn_username_needed assertions passed`, exit 0. (If any assert fails the binary aborts — fix the predicate.)
+
+- [ ] **Step 5: Add the new files to meson**
+
+In `src/modules/networkmanager/meson.build`, after the `e_networkmanager_import.h` entry (line ~10), add:
+
+```meson
+  'e_networkmanager_vpn_username.c',
+  'e_networkmanager_vpn_username.h',
+```
+
+(Do NOT add `test_vpn_username.c` — it is a hand-run standalone test.)
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/modules/networkmanager/e_networkmanager_vpn_username.c \
+        src/modules/networkmanager/e_networkmanager_vpn_username.h \
+        src/modules/networkmanager/test_vpn_username.c \
+        src/modules/networkmanager/meson.build
+git commit -m "networkmanager: add enm_vpn_username_needed predicate"
+```
+
+---
+
+### Task 2: Async nmcli username writer
+
+**Files:**
+- Modify: `src/modules/networkmanager/e_networkmanager_vpn_username.c`
+
+**Interfaces:**
+- Consumes: nothing new.
+- Produces: `void enm_vpn_username_set(const char *conn_name, const char *username, Enm_Username_Done_Cb cb, void *data)` — runs `nmcli connection modify <conn_name> vpn.data username=<username>` async; calls `cb(data, ok)` once.
+
+- [ ] **Step 1: Implement the writer**
+
+Replace the `#ifndef ENM_VPN_USERNAME_TEST` trailer block at the bottom of `e_networkmanager_vpn_username.c` with:
+
+```c
+#ifndef ENM_VPN_USERNAME_TEST
+
+/* ---- shared shell-escape (same scheme as e_networkmanager_import.c) ------- */
+static char *
+_username_shell_escape(const char *s)
+{
+   Eina_Strbuf *b;
+   char *out;
+
+   if (!s) return strdup("");
+   b = eina_strbuf_new();
+   if (!b) return NULL;
+   for (const char *p = s; *p; p++)
+     {
+        if (*p == '\'') eina_strbuf_append(b, "'\\''");
+        else            eina_strbuf_append_char(b, *p);
+     }
+   out = strdup(eina_strbuf_string_get(b));
+   eina_strbuf_free(b);
+   return out;
+}
+
+typedef struct _Username_Set_Ctx
+{
+   Enm_Username_Done_Cb  cb;
+   void                 *data;
+   Ecore_Exe            *exe;
+   Ecore_Event_Handler  *handler_del;
+} Username_Set_Ctx;
+
+static Eina_Bool
+_username_set_on_del(void *data, int type EINA_UNUSED, void *event)
+{
+   Username_Set_Ctx *ctx = data;
+   Ecore_Exe_Event_Del *ev = event;
+
+   if (!ev || !ev->exe || ev->exe != ctx->exe)
+     return ECORE_CALLBACK_PASS_ON;
+
+   if (ctx->handler_del) ecore_event_handler_del(ctx->handler_del);
+   if (ctx->cb)
+     ctx->cb(ctx->data, (ev->exited && ev->exit_code == 0));
+   free(ctx);
+   return ECORE_CALLBACK_DONE;
+}
+
+void
+enm_vpn_username_set(const char *conn_name, const char *username,
+                     Enm_Username_Done_Cb cb, void *data)
+{
+   const char *nmcli;
+   char *esc_nmcli, *esc_name, *esc_user;
+   char cmd[4096];
+   Username_Set_Ctx *ctx;
+
+   if (!conn_name) { if (cb) cb(data, EINA_FALSE); return; }
+
+   nmcli = enm_import_nmcli_path();
+   if (!nmcli) { if (cb) cb(data, EINA_FALSE); return; }
+
+   esc_nmcli = _username_shell_escape(nmcli);
+   esc_name  = _username_shell_escape(conn_name);
+   esc_user  = _username_shell_escape(username);
+   if (!esc_nmcli || !esc_name || !esc_user)
+     {
+        free(esc_nmcli); free(esc_name); free(esc_user);
+        if (cb) cb(data, EINA_FALSE);
+        return;
+     }
+
+   snprintf(cmd, sizeof(cmd),
+            "'%s' connection modify '%s' vpn.data 'username=%s'",
+            esc_nmcli, esc_name, esc_user);
+   free(esc_nmcli); free(esc_name); free(esc_user);
+
+   ctx = calloc(1, sizeof(*ctx));
+   if (!ctx) { if (cb) cb(data, EINA_FALSE); return; }
+   ctx->cb = cb;
+   ctx->data = ""
+   ctx->exe = ecore_exe_pipe_run(cmd,
+                                 ECORE_EXE_NOT_LEADER |
+                                 ECORE_EXE_TERM_WITH_PARENT, ctx);
+   if (!ctx->exe) { free(ctx); if (cb) cb(data, EINA_FALSE); return; }
+   ctx->handler_del = ecore_event_handler_add(ECORE_EXE_EVENT_DEL,
+                                              _username_set_on_del, ctx);
+}
+
+#endif
+```
+
+Note: `enm_import_nmcli_path()` is already declared in `e_networkmanager_import.h` (included above) and is reused as the single nmcli-locator.
+
+- [ ] **Step 2: Verify the predicate test still builds and passes**
+
+Run: `cd src/modules/networkmanager && gcc -DENM_VPN_USERNAME_TEST test_vpn_username.c -o /tmp/t && /tmp/t`
+Expected: `all enm_vpn_username_needed assertions passed` (the new code is excluded from the test build by the guard).
+
+- [ ] **Step 3: Verify the module still compiles**
+
+Run: `cd /home/cedric/work/enlightenment/core/enlightenment && ninja -C build src/modules/networkmanager/libnetworkmanager.so 2>&1 | tail -20` (use your actual build dir; if unsure run `ninja -C build` and check it reaches the networkmanager target with no errors).
+Expected: compiles with no errors/warnings in `e_networkmanager_vpn_username.c`.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/modules/networkmanager/e_networkmanager_vpn_username.c
+git commit -m "networkmanager: add async nmcli username writer"
+```
+
+---
+
+### Task 3: Shared username dialog + post-import orchestrator
+
+**Files:**
+- Modify: `src/modules/networkmanager/e_networkmanager_vpn_username.c`
+
+**Interfaces:**
+- Consumes: `enm_vpn_username_needed`, `enm_vpn_username_set`, `enm_import_nmcli_path`.
+- Produces:
+  - `void enm_vpn_username_dialog(const char *conn_name, const char *type_label, const char *initial, Enm_Username_Entered_Cb cb, void *data)`
+  - `void enm_vpn_username_maybe_prompt(const char *conn_name)`
+
+- [ ] **Step 1: Implement the dialog**
+
+Inside the `#ifndef ENM_VPN_USERNAME_TEST` block (before the closing `#endif`), add:
+
+```c
+typedef struct _Username_Dialog
+{
+   E_Dialog                *dialog;
+   Evas_Object             *entry;
+   Enm_Username_Entered_Cb  cb;
+   void                    *data;
+   Eina_Bool                replied;
+} Username_Dialog;
+
+static void
+_username_dialog_finish(Username_Dialog *ud, const char *value)
+{
+   if (!ud->replied)
+     {
+        ud->replied = EINA_TRUE;
+        if (ud->cb) ud->cb(ud->data, value);
+     }
+   e_object_del(E_OBJECT(ud->dialog));
+}
+
+static void
+_username_dialog_ok_cb(void *data, E_Dialog *dialog EINA_UNUSED)
+{
+   Username_Dialog *ud = data;
+   char *user = elm_entry_markup_to_utf8(elm_entry_entry_get(ud->entry));
+   _username_dialog_finish(ud, user ?: "");
+   free(user);
+}
+
+static void
+_username_dialog_cancel_cb(void *data, E_Dialog *dialog EINA_UNUSED)
+{
+   _username_dialog_finish(data, NULL);
+}
+
+static void
+_username_entry_activated_cb(void *data, Evas_Object *o EINA_UNUSED,
+                             void *ev EINA_UNUSED)
+{
+   _username_dialog_ok_cb(data, NULL);
+}
+
+static void
+_username_dialog_key_down_cb(void *data, Evas *e EINA_UNUSED,
+                             Evas_Object *o EINA_UNUSED, void *event)
+{
+   Evas_Event_Key_Down *ev = event;
+   if (!strcmp(ev->key, "Escape")) _username_dialog_finish(data, NULL);
+}
+
+static void
+_username_dialog_del_cb(void *data)
+{
+   E_Dialog *dialog = data;
+   Username_Dialog *ud = e_object_data_get(E_OBJECT(dialog));
+   if (!ud) return;
+   if (!ud->replied)
+     {
+        ud->replied = EINA_TRUE;
+        if (ud->cb) ud->cb(ud->data, NULL);   /* WM close == cancel */
+     }
+   free(ud);
+}
+
+void
+enm_vpn_username_dialog(const char *conn_name, const char *type_label,
+                        const char *initial,
+                        Enm_Username_Entered_Cb cb, void *data)
+{
+   Username_Dialog *ud;
+   E_Dialog *dialog;
+   Evas_Object *frame, *table, *label, *entry;
+   char header[160];
+
+   dialog = e_dialog_new(NULL, "E", "nm_vpn_username");
+   if (!dialog) { if (cb) cb(data, NULL); return; }
+
+   ud = E_NEW(Username_Dialog, 1);
+   ud->dialog = dialog;
+   ud->cb = cb;
+   ud->data = ""
+
+   e_dialog_resizable_set(dialog, 0);
+   e_dialog_title_set(dialog, _("VPN Username Required"));
+   e_dialog_border_icon_set(dialog, "dialog-password");
+   e_dialog_button_add(dialog, _("OK"), NULL, _username_dialog_ok_cb, ud);
+   e_dialog_button_add(dialog, _("Cancel"), NULL, _username_dialog_cancel_cb, ud);
+
+   snprintf(header, sizeof(header), "%s — %s",
+            conn_name ?: "VPN", type_label ?: _("VPN"));
+   frame = elm_frame_add(dialog->win);
+   elm_object_text_set(frame, header);
+   evas_object_size_hint_weight_set(frame, EVAS_HINT_EXPAND, 0);
+   evas_object_size_hint_align_set(frame, EVAS_HINT_FILL, EVAS_HINT_FILL);
+
+   table = elm_table_add(frame);
+   elm_table_padding_set(table, 8 * e_scale, 4 * e_scale);
+   elm_object_content_set(frame, table);
+   evas_object_show(table);
+
+   label = elm_label_add(table);
+   elm_object_text_set(label, _("Username"));
+   evas_object_size_hint_align_set(label, 1.0, 0.5);
+   elm_table_pack(table, label, 0, 0, 1, 1);
+   evas_object_show(label);
+
+   entry = elm_entry_add(table);
+   elm_entry_single_line_set(entry, EINA_TRUE);
+   elm_entry_scrollable_set(entry, EINA_TRUE);
+   if (initial && initial[0]) elm_object_text_set(entry, initial);
+   evas_object_size_hint_weight_set(entry, EVAS_HINT_EXPAND, 0);
+   evas_object_size_hint_align_set(entry, EVAS_HINT_FILL, 0.5);
+   evas_object_size_hint_min_set(entry, 240 * e_scale, 0);
+   evas_object_smart_callback_add(entry, "activated",
+                                  _username_entry_activated_cb, ud);
+   elm_table_pack(table, entry, 1, 0, 1, 1);
+   evas_object_show(entry);
+   ud->entry = entry;
+
+   evas_object_show(frame);
+   e_dialog_content_set(dialog, frame, 0, 0);
+   e_dialog_show(dialog);
+
+   evas_object_event_callback_add(dialog->bg_object, EVAS_CALLBACK_KEY_DOWN,
+                                  _username_dialog_key_down_cb, ud);
+   e_object_del_attach_func_set(E_OBJECT(dialog), _username_dialog_del_cb);
+   e_object_data_set(E_OBJECT(dialog), ud);
+   elm_object_focus_set(entry, EINA_TRUE);
+   elm_win_center(dialog->win, 1, 1);
+}
+```
+
+- [ ] **Step 2: Implement the post-import orchestrator**
+
+Append, still inside the `#ifndef ENM_VPN_USERNAME_TEST` block:
+
+```c
+/* Parse a `key = value, key = value` list (nmcli -g vpn.data output) for one
+ * key.  Returns a malloc'd value or NULL.  Tolerant of surrounding spaces. */
+static char *
+_nmcli_kv_find(const char *blob, const char *key)
+{
+   size_t klen = strlen(key);
+   const char *p = blob;
+
+   while (p && *p)
+     {
+        while (*p == ' ' || *p == ',') p++;
+        if (!strncmp(p, key, klen))
+          {
+             const char *q = p + klen;
+             while (*q == ' ') q++;
+             if (*q == '=')
+               {
+                  const char *v;
+                  const char *end;
+                  q++;
+                  while (*q == ' ') q++;
+                  v = q;
+                  end = strchr(v, ',');
+                  if (!end) end = v + strlen(v);
+                  while (end > v && (end[-1] == ' ')) end--;
+                  return strndup(v, (size_t)(end - v));
+               }
+          }
+        p = strchr(p, ',');
+        if (p) p++;
+     }
+   return NULL;
+}
+
+typedef struct _Username_Probe_Ctx
+{
+   char                *conn_name;
+   Ecore_Exe           *exe;
+   Eina_Strbuf         *out;
+   Ecore_Event_Handler *h_data;
+   Ecore_Event_Handler *h_del;
+} Username_Probe_Ctx;
+
+static Eina_Bool
+_probe_on_data(void *data, int type EINA_UNUSED, void *event)
+{
+   Username_Probe_Ctx *ctx = data;
+   Ecore_Exe_Event_Data *ev = event;
+   if (!ev || ev->exe != ctx->exe) return ECORE_CALLBACK_PASS_ON;
+   if (ev->data && ev->size > 0)
+     eina_strbuf_append_length(ctx->out, ev->data, ev->size);
+   return ECORE_CALLBACK_DONE;
+}
+
+static void
+_probe_username_entered(void *data, const char *username)
+{
+   char *conn_name = data;
+   if (username) enm_vpn_username_set(conn_name, username, NULL, NULL);
+   free(conn_name);
+}
+
+static Eina_Bool
+_probe_on_del(void *data, int type EINA_UNUSED, void *event)
+{
+   Username_Probe_Ctx *ctx = data;
+   Ecore_Exe_Event_Del *ev = event;
+   char *svc, *short_name, *conn_type, *username;
+   const char *blob;
+
+   if (!ev || ev->exe != ctx->exe) return ECORE_CALLBACK_PASS_ON;
+
+   if (ctx->h_data) ecore_event_handler_del(ctx->h_data);
+   if (ctx->h_del)  ecore_event_handler_del(ctx->h_del);
+
+   blob = eina_strbuf_string_get(ctx->out);
+   /* nmcli -g vpn.service-type,vpn.data prints service-type on line 1 and the
+    * data list on line 2. */
+   svc = NULL; conn_type = NULL; username = NULL;
+   {
+      const char *nl = strchr(blob, '\n');
+      if (nl)
+        {
+           svc = strndup(blob, (size_t)(nl - blob));
+           conn_type = _nmcli_kv_find(nl + 1, "connection-type");
+           username  = _nmcli_kv_find(nl + 1, "username");
+        }
+   }
+   short_name = NULL;
+   if (svc) { char *d = strrchr(svc, '.'); short_name = d ? d + 1 : svc; }
+
+   if (enm_vpn_username_needed(short_name, conn_type, username))
+     enm_vpn_username_dialog(ctx->conn_name,
+                             short_name,        /* type label: short name */
+                             username,
+                             _probe_username_entered,
+                             strdup(ctx->conn_name));
+
+   free(svc); free(conn_type); free(username);
+   free(ctx->conn_name);
+   eina_strbuf_free(ctx->out);
+   free(ctx);
+   return ECORE_CALLBACK_DONE;
+}
+
+void
+enm_vpn_username_maybe_prompt(const char *conn_name)
+{
+   const char *nmcli;
+   char *esc_nmcli, *esc_name, cmd[4096];
+   Username_Probe_Ctx *ctx;
+
+   if (!conn_name) return;
+   nmcli = enm_import_nmcli_path();
+   if (!nmcli) return;
+
+   esc_nmcli = _username_shell_escape(nmcli);
+   esc_name  = _username_shell_escape(conn_name);
+   if (!esc_nmcli || !esc_name) { free(esc_nmcli); free(esc_name); return; }
+
+   snprintf(cmd, sizeof(cmd),
+            "'%s' -g vpn.service-type,vpn.data connection show '%s'",
+            esc_nmcli, esc_name);
+   free(esc_nmcli); free(esc_name);
+
+   ctx = calloc(1, sizeof(*ctx));
+   if (!ctx) return;
+   ctx->conn_name = strdup(conn_name);
+   ctx->out = eina_strbuf_new();
+   if (!ctx->conn_name || !ctx->out)
+     { free(ctx->conn_name); if (ctx->out) eina_strbuf_free(ctx->out); free(ctx); return; }
+
+   ctx->exe = ecore_exe_pipe_run(cmd,
+                                 ECORE_EXE_PIPE_READ |
+                                 ECORE_EXE_PIPE_READ_LINE_BUFFERED |
+                                 ECORE_EXE_NOT_LEADER |
+                                 ECORE_EXE_TERM_WITH_PARENT, ctx);
+   if (!ctx->exe)
+     { free(ctx->conn_name); eina_strbuf_free(ctx->out); free(ctx); return; }
+   ctx->h_data = ecore_event_handler_add(ECORE_EXE_EVENT_DATA, _probe_on_data, ctx);
+   ctx->h_del  = ecore_event_handler_add(ECORE_EXE_EVENT_DEL,  _probe_on_del,  ctx);
+}
+```
+
+- [ ] **Step 3: Verify the predicate test still passes**
+
+Run: `cd src/modules/networkmanager && gcc -DENM_VPN_USERNAME_TEST test_vpn_username.c -o /tmp/t && /tmp/t`
+Expected: `all enm_vpn_username_needed assertions passed`.
+
+- [ ] **Step 4: Verify the module compiles**
+
+Run: `cd /home/cedric/work/enlightenment/core/enlightenment && ninja -C build 2>&1 | tail -20`
+Expected: networkmanager module compiles, no errors.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/modules/networkmanager/e_networkmanager_vpn_username.c
+git commit -m "networkmanager: add shared username dialog + post-import probe"
+```
+
+---
+
+### Task 4: Import reports the connection name; post-import prompt wired in
+
+**Files:**
+- Modify: `src/modules/networkmanager/e_networkmanager_import.h:35-41`
+- Modify: `src/modules/networkmanager/e_networkmanager_import.c`
+- Modify: `src/modules/networkmanager/e_mod_main.c:409-461`
+
+**Interfaces:**
+- Consumes: `enm_vpn_username_maybe_prompt`.
+- Produces: `Enm_Import_Done_Cb` gains a 4th parameter `const char *conn_name` (NULL on failure / when unparsable).
+
+- [ ] **Step 1: Extend the done-cb typedef**
+
+In `e_networkmanager_import.h`, replace the typedef:
+
+```c
+typedef void (*Enm_Import_Done_Cb)(void *data, Eina_Bool ok,
+                                   const char *stderr_text,
+                                   const char *conn_name);
+```
+
+- [ ] **Step 2: Capture stdout and parse the connection name in import.c**
+
+In `e_networkmanager_import.c`:
+
+(a) Add a stdout buffer + handler to `Import_Ctx`:
+
+```c
+typedef struct _Import_Ctx
+{
+   Enm_Import_Done_Cb   done_cb;
+   void                *data;
+   Ecore_Exe           *exe;
+   Eina_Strbuf         *stderr_buf;
+   Eina_Strbuf         *stdout_buf;
+   Ecore_Event_Handler *handler_err;
+   Ecore_Event_Handler *handler_out;
+   Ecore_Event_Handler *handler_del;
+} Import_Ctx;
+```
+
+(b) In `_import_ctx_free`, free the new members:
+
+```c
+   if (ctx->handler_out) ecore_event_handler_del(ctx->handler_out);
+   if (ctx->stdout_buf)  eina_strbuf_free(ctx->stdout_buf);
+```
+
+(c) Add a stdout handler mirroring `_on_stderr`:
+
+```c
+static Eina_Bool
+_on_stdout(void *data, int type EINA_UNUSED, void *event)
+{
+   Import_Ctx *ctx = data;
+   Ecore_Exe_Event_Data *ev = event;
+
+   if (!ev || !ev->exe || ev->exe != ctx->exe)
+     return ECORE_CALLBACK_PASS_ON;
+   if (ev->data && ev->size > 0)
+     eina_strbuf_append_length(ctx->stdout_buf, ev->data, ev->size);
+   return ECORE_CALLBACK_DONE;
+}
+
+/* Parse `Connection 'NAME' (uuid) successfully added.` -> malloc'd NAME. */
+static char *
+_import_parse_conn_name(const char *out)
+{
+   const char *a, *b;
+   if (!out) return NULL;
+   a = strchr(out, '\'');
+   if (!a) return NULL;
+   a++;
+   b = strchr(a, '\'');
+   if (!b) return NULL;
+   return strndup(a, (size_t)(b - a));
+}
+```
+
+(d) In `_on_del`, clear the new handler and pass the parsed name:
+
+```c
+   ecore_event_handler_del(ctx->handler_err);
+   ctx->handler_err = NULL;
+   if (ctx->handler_out) { ecore_event_handler_del(ctx->handler_out); ctx->handler_out = NULL; }
+   ecore_event_handler_del(ctx->handler_del);
+   ctx->handler_del = NULL;
+
+   Eina_Bool ok = (ev->exited && ev->exit_code == 0);
+   const char *stderr_text = eina_strbuf_string_get(ctx->stderr_buf);
+   char *conn_name = ok ? _import_parse_conn_name(
+                              eina_strbuf_string_get(ctx->stdout_buf)) : NULL;
+
+   ctx->done_cb(ctx->data, ok, stderr_text, conn_name);
+   free(conn_name);
+   _import_ctx_free(ctx);
+```
+
+(e) In `enm_import_run`, allocate `stdout_buf`, add `ECORE_EXE_PIPE_READ | ECORE_EXE_PIPE_READ_LINE_BUFFERED` to the flags, and register `handler_out`:
+
+```c
+   ctx->stdout_buf = eina_strbuf_new();
+   if (!ctx->stdout_buf)
+     {
+        done_cb(data, EINA_FALSE, "out of memory", NULL);
+        eina_strbuf_free(ctx->stderr_buf);
+        free(ctx);
+        return;
+     }
+```
+
+Update the `ecore_exe_pipe_run` flags to:
+
+```c
+   ctx->exe = ecore_exe_pipe_run(cmd,
+                                 ECORE_EXE_PIPE_READ |
+                                 ECORE_EXE_PIPE_READ_LINE_BUFFERED |
+                                 ECORE_EXE_PIPE_ERROR |
+                                 ECORE_EXE_PIPE_ERROR_LINE_BUFFERED |
+                                 ECORE_EXE_NOT_LEADER |
+                                 ECORE_EXE_TERM_WITH_PARENT,
+                                 ctx);
+```
+
+And after the existing `handler_err` registration add:
+
+```c
+   ctx->handler_out = ecore_event_handler_add(ECORE_EXE_EVENT_DATA,
+                                              _on_stdout, ctx);
+```
+
+Also update the four early `done_cb(data, EINA_FALSE, "...", )` error calls in `enm_import_run` to pass a trailing `NULL` (new 4th arg).
+
+- [ ] **Step 3: Update the consumer in e_mod_main.c**
+
+In `e_mod_main.c`, change the signature of `_enm_vpn_import_done_cb` and prompt on success. Replace its head:
+
+```c
+static void
+_enm_vpn_import_done_cb(void *data EINA_UNUSED, Eina_Bool ok,
+                        const char *err, const char *conn_name)
+{
+   E_Dialog *err_dlg;
+   if (ok)
+     {
+        INF("VPN import succeeded");
+        if (conn_name) enm_vpn_username_maybe_prompt(conn_name);
+        return;
+     }
+```
+
+And update the internal failure call in `_enm_vpn_fs_done_cb`:
+
+```c
+        _enm_vpn_import_done_cb(NULL, EINA_FALSE,
+              _("Could not detect VPN type from file extension. "
+                "Use a .conf (WireGuard) or .ovpn (OpenVPN) file."),
+              NULL);
+```
+
+Add the include near the other module includes at the top of `e_mod_main.c`:
+
+```c
+#include "e_networkmanager_vpn_username.h"
+```
+
+- [ ] **Step 4: Build**
+
+Run: `cd /home/cedric/work/enlightenment/core/enlightenment && ninja -C build 2>&1 | tail -20`
+Expected: no errors; all `Enm_Import_Done_Cb` call sites updated (compiler will flag any missed 4-arg mismatch).
+
+- [ ] **Step 5: Manual smoke test**
+
+Restart E (or reload the module), import `~/Downloads/vpnbook-ca196-tcp443.ovpn`. Expected: the username dialog appears after import.
+Then: `nmcli -g vpn.data connection show vpnbook-ca196-tcp443` → shows `username = <typed value>`.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/modules/networkmanager/e_networkmanager_import.c \
+        src/modules/networkmanager/e_networkmanager_import.h \
+        src/modules/networkmanager/e_mod_main.c
+git commit -m "networkmanager: prompt for VPN username after import"
+```
+
+---
+
+### Task 5: Connect-time username injection
+
+**Files:**
+- Modify: `src/modules/networkmanager/e_networkmanager.h:41-47`
+- Modify: `src/modules/networkmanager/e_networkmanager.c:3595-3648` (+ helpers above)
+- Modify: `src/modules/networkmanager/agent.c`
+
+**Interfaces:**
+- Consumes: `enm_vpn_username_needed`, `enm_vpn_username_set`, `_agent_vpn_data_find` (file-local in e_networkmanager.c), `_agent_vpn_service_short_name`.
+- Produces: `E_NM_Agent_VPN_Secrets_Request_Cb` gains two trailing params: `Eina_Bool need_username, const char *conn_name_for_username`. The agent dialog injects a username row when `need_username` is set and persists it on OK via `enm_vpn_username_set`.
+
+- [ ] **Step 1: Extend the VPN request callback typedef**
+
+In `e_networkmanager.h`, update `E_NM_Agent_VPN_Secrets_Request_Cb`:
+
+```c
+typedef void (*E_NM_Agent_VPN_Secrets_Request_Cb)(void *data,
+        E_NM_Agent_Request *req,
+        const char *conn_name,
+        const char *service_type,
+        const char *message,
+        const char *const *fields,
+        unsigned int n_fields,
+        Eina_Bool need_username);
+```
+
+(The connection name to modify is already passed as `conn_name`.)
+
+- [ ] **Step 2: Compute need_username and pass it in e_networkmanager.c**
+
+In `_agent_get_secrets` vpn branch, after `_agent_vpn_conn_info_parse(conn_props, &info);` compute the flag:
+
+```c
+        Eina_Bool need_username;
+        const char *short_name = _agent_vpn_service_short_name(info.svc_type);
+        const char *conn_type  = _agent_vpn_data_find(&info, "connection-type");
+        const char *cur_user   = _agent_vpn_data_find(&info, "username");
+
+        need_username = enm_vpn_username_needed(short_name, conn_type, cur_user);
+```
+
+Add the include at the top of `e_networkmanager.c` with the other module includes:
+
+```c
+#include "e_networkmanager_vpn_username.h"
+```
+
+Then update the `vpn_request` invocation to pass `need_username`:
+
+```c
+             _agent_cbs.vpn_request(_agent_cb_data, req,
+                                    info.conn_id ?: "VPN", info.svc_type,
+                                    fields.message,
+                                    (const char *const *)fields.items,
+                                    fields.len,
+                                    need_username);
+```
+
+Note: when `fields.len == 0` but `need_username` is true (a username-only config whose password is system-stored), the existing early `if (fields.len == 0)` guard cancels the request before reaching here. Change that guard so it only cancels when *neither* a secret nor a username is required:
+
+```c
+        if (fields.len == 0 && !need_username)
+          {
+             WRN("GetSecrets: no VPN secrets requested by hints or flags");
+             e_nm_agent_reply_cancel(req);
+             _agent_vpn_field_list_clear(&fields);
+             _agent_vpn_conn_info_clear(&info);
+             return NULL;
+          }
+```
+
+(Move the `need_username`/`short_name`/`conn_type`/`cur_user` computation above this guard. `_agent_vpn_data_find` returns a pointer into `info`, valid until `_agent_vpn_conn_info_clear`.)
+
+- [ ] **Step 3: Extend the agent dialog struct + signature in agent.c**
+
+In `agent.c`, add to `struct _E_NM_Agent_Dialog`:
+
+```c
+   /* VPN mode: when true, field_entries[0] is a username *property*, not a
+    * secret — persisted to vpn.data instead of returned via reply. */
+   Eina_Bool            has_username_field;
+   char                *conn_name;   /* strdup; for username persistence */
+```
+
+In `_dialog_del_cb`, free it:
+
+```c
+   free(ad->conn_name);
+```
+
+- [ ] **Step 4: Inject the username row in `_vpn_dialog_new`**
+
+Change `_vpn_dialog_new`'s signature to accept the flag and connection name (the name is already the `conn_name` param). Update its declaration/definition to add `Eina_Bool need_username` as the last parameter, store `ad->conn_name = conn_name ? strdup(conn_name) : NULL;` and `ad->has_username_field = need_username;`.
+
+When `need_username` is set, render an extra row at table row 0 (shifting the secret rows down by one). The simplest correct implementation: build a combined logical field list where index 0 is a synthetic username entry. Concretely, after allocating `ad->field_entries`, add one to the row budget:
+
+```c
+   unsigned int extra = need_username ? 1 : 0;
+   /* field_entries/field_names already sized to n_fields; widen by extra. */
+```
+
+To keep allocation simple, allocate `n_fields + extra` slots for `field_entries` only (the username has no NM field name). Render the username row first:
+
+```c
+   unsigned int base = 0;
+   if (need_username)
+     {
+        Evas_Object *label, *entry;
+        label = elm_label_add(table);
+        elm_object_text_set(label, _("Username"));
+        evas_object_size_hint_align_set(label, 1.0, 0.5);
+        elm_table_pack(table, label, 0, 0, 1, 1);
+        evas_object_show(label);
+
+        entry = elm_entry_add(table);
+        elm_entry_single_line_set(entry, EINA_TRUE);
+        elm_entry_scrollable_set(entry, EINA_TRUE);
+        evas_object_size_hint_weight_set(entry, EVAS_HINT_EXPAND, 0);
+        evas_object_size_hint_align_set(entry, EVAS_HINT_FILL, 0.5);
+        evas_object_smart_callback_add(entry, "activated",
+                                       _entry_activated_cb, ad);
+        elm_table_pack(table, entry, 1, 0, 1, 1);
+        evas_object_show(entry);
+        ad->username_entry = entry;
+        base = 1;
+     }
+```
+
+Add `Evas_Object *username_entry;` to the dialog struct (NULL when absent). In the existing secret-field loop, pack rows at `base + i` instead of `i`, and focus the username entry first when present.
+
+- [ ] **Step 5: Persist username on OK in `_dialog_send_ok`**
+
+In the `else` (vpn) branch of `_dialog_send_ok`, before sending secrets, persist the username property:
+
+```c
+        if (ad->has_username_field && ad->username_entry)
+          {
+             char *user = elm_entry_markup_to_utf8(
+                             elm_entry_entry_get(ad->username_entry));
+             if (user && user[0] && ad->conn_name)
+               enm_vpn_username_set(ad->conn_name, user, NULL, NULL);
+             free(user);
+          }
+```
+
+Leave the existing secret-collection/`e_nm_agent_reply_vpn_secrets` code unchanged (the username entry is not part of `field_entries`/`field_names`, so it is not sent as a secret). Add `#include "e_networkmanager_vpn_username.h"` at the top of `agent.c`.
+
+- [ ] **Step 6: Thread the new param through the UI callback**
+
+Update `_agent_ui_vpn_request_cb` in `agent.c` to accept and forward `Eina_Bool need_username`:
+
+```c
+static void
+_agent_ui_vpn_request_cb(void *data EINA_UNUSED, E_NM_Agent_Request *req,
+                         const char *conn_name, const char *service_type,
+                         const char *message,
+                         const char *const *fields, unsigned int n_fields,
+                         Eina_Bool need_username)
+{
+   if (_current_dialog)
+     {
+        _current_dialog->req = NULL;
+        e_object_del(E_OBJECT(_current_dialog->dialog));
+        _current_dialog = NULL;
+     }
+   _current_dialog = _vpn_dialog_new(req, conn_name, service_type,
+                                     message, fields, n_fields, need_username);
+   if (!_current_dialog) { e_nm_agent_reply_cancel(req); return; }
+}
+```
+
+- [ ] **Step 7: Build**
+
+Run: `cd /home/cedric/work/enlightenment/core/enlightenment && ninja -C build 2>&1 | tail -20`
+Expected: no errors; the typedef change forces every `vpn_request` call site and `_vpn_dialog_new` caller to match.
+
+- [ ] **Step 8: Manual test (connect-time path)**
+
+Pre-create a username-less connection to exercise this path directly:
+```bash
+nmcli connection modify vpnbook-ca196-tcp443 vpn.data 'username='
+```
+Then activate the VPN from the E module. Expected: the secrets dialog now shows a **Username** row above **Password**. Enter both, Connect.
+Verify: `nmcli -g vpn.data connection show vpnbook-ca196-tcp443` shows the username persisted, and the tunnel comes up (`journalctl` shows openvpn connecting, no "User canceled the secrets request").
+**If** the first connect attempt fails because the username was persisted after NM already read the connection, retry once; if it consistently needs two attempts, apply the spec's belt-and-suspenders note (Task 6 follow-up): also include `username` in the returned secrets dict. Record the observed behavior in the commit message.
+
+- [ ] **Step 9: Commit**
+
+```bash
+git add src/modules/networkmanager/e_networkmanager.h \
+        src/modules/networkmanager/e_networkmanager.c \
+        src/modules/networkmanager/agent.c
+git commit -m "networkmanager: inject VPN username field at connect time"
+```
+
+---
+
+### Task 6: Remove leftover debug printfs
+
+**Files:**
+- Modify: `src/modules/networkmanager/e_networkmanager.c`
+- Modify: `src/modules/networkmanager/agent.c`
+
+- [ ] **Step 1: Find every debug printf**
+
+Run: `cd src/modules/networkmanager && grep -n 'printf("NM:' e_networkmanager.c agent.c`
+Expected: lists the `printf("NM: …")` lines (e.g. e_networkmanager.c:3188, 3493, 3498, 3543, 3562, 3572, 3601, 3613, 3623, 3633, 3651, 3659 and any in agent.c).
+
+- [ ] **Step 2: Delete those lines**
+
+Remove each `printf("NM: …");` debugging line found above. Do not remove `WRN`/`ERR`/`INF`/`DBG` logging. Leave surrounding logic untouched.
+
+- [ ] **Step 3: Verify none remain**
+
+Run: `cd src/modules/networkmanager && grep -rn 'printf("NM:' . ; echo "exit: $?"`
+Expected: no matches (grep exit 1).
+
+- [ ] **Step 4: Build**
+
+Run: `cd /home/cedric/work/enlightenment/core/enlightenment && ninja -C build 2>&1 | tail -20`
+Expected: no errors.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/modules/networkmanager/e_networkmanager.c \
+        src/modules/networkmanager/agent.c
+git commit -m "networkmanager: drop leftover NM debug printfs"
+```
+
+---
+
+## Notes for the implementer
+
+- `e_scale`, `E_NEW`, `_()` (gettext), `INF/WRN/ERR/DBG`, and the `e_dialog_*` / `elm_*` APIs are all already used in `agent.c` — mirror that file for idiom and error handling.
+- `_agent_vpn_data_find` and `_agent_vpn_service_short_name` are file-local `static` functions in `e_networkmanager.c`; the new logic in Task 5 lives in the same file, so they are directly callable.
+- The standalone predicate test (`test_vpn_username.c`) is intentionally NOT part of the meson build; run it by hand as shown.
+- Build dir is assumed to be `build/`; adjust `ninja -C <dir>` to your actual configuration.

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

Reply via email to