summary refs log tree commit diff
path: root/src/nmtui
diff options
context:
space:
mode:
Diffstat (limited to 'src/nmtui')
-rw-r--r--src/nmtui/meson.build4
-rw-r--r--src/nmtui/nm-editor-bindings.c4
-rw-r--r--src/nmtui/nmt-address-list.h43
-rw-r--r--src/nmtui/nmt-connect-connection-list.c56
-rw-r--r--src/nmtui/nmt-connect-connection-list.h4
-rw-r--r--src/nmtui/nmt-device-entry.c173
-rw-r--r--src/nmtui/nmt-device-entry.h5
-rw-r--r--src/nmtui/nmt-edit-connection-list.c129
-rw-r--r--src/nmtui/nmt-edit-connection-list.h3
-rw-r--r--src/nmtui/nmt-editor-grid.c6
-rw-r--r--src/nmtui/nmt-editor.c45
-rw-r--r--src/nmtui/nmt-list.c (renamed from src/nmtui/nmt-address-list.c)134
-rw-r--r--src/nmtui/nmt-list.h41
-rw-r--r--src/nmtui/nmt-page-bond.c112
-rw-r--r--src/nmtui/nmt-page-bridge.c2
-rw-r--r--src/nmtui/nmt-page-ip-tunnel.c2
-rw-r--r--src/nmtui/nmt-page-ip4.c8
-rw-r--r--src/nmtui/nmt-page-ip6.c8
-rw-r--r--src/nmtui/nmt-page-macsec.c2
-rw-r--r--src/nmtui/nmt-page-vlan.c2
-rw-r--r--src/nmtui/nmt-page-wifi.c7
-rw-r--r--src/nmtui/nmt-password-dialog.c33
-rw-r--r--src/nmtui/nmt-utils.c209
-rw-r--r--src/nmtui/nmt-utils.h35
-rw-r--r--src/nmtui/nmt-widget-list.c2
-rw-r--r--src/nmtui/nmt-wifi-qr-dialog.c165
-rw-r--r--src/nmtui/nmt-wifi-qr-dialog.h13
-rw-r--r--src/nmtui/nmt-wireguard-peer-list.c1
-rw-r--r--src/nmtui/nmtui-connect.c305
-rw-r--r--src/nmtui/nmtui-connect.h2
-rw-r--r--src/nmtui/nmtui-edit.c16
-rw-r--r--src/nmtui/nmtui.c18
32 files changed, 1371 insertions, 218 deletions
diff --git a/src/nmtui/meson.build b/src/nmtui/meson.build
index 40bb40b9..56812cfa 100644
--- a/src/nmtui/meson.build
+++ b/src/nmtui/meson.build
@@ -6,7 +6,6 @@ executable(
     'nm-editor-bindings.c',
     'nm-editor-utils.c',
     'nmt-8021x-fields.c',
-    'nmt-address-list.c',
     'nmt-connect-connection-list.c',
     'nmt-device-entry.c',
     'nmt-edit-connection-list.c',
@@ -16,6 +15,7 @@ executable(
     'nmt-editor-page-device.c',
     'nmt-editor-section.c',
     'nmt-ip-entry.c',
+    'nmt-list.c',
     'nmt-mac-entry.c',
     'nmt-mtu-entry.c',
     'nmt-page-bond.c',
@@ -42,6 +42,7 @@ executable(
     'nmt-route-editor.c',
     'nmt-route-entry.c',
     'nmt-route-table.c',
+    'nmt-wifi-qr-dialog.c',
     'nmt-port-list.c',
     'nmt-wireguard-peer-list.c',
     'nmt-wireguard-peer-editor.c',
@@ -56,6 +57,7 @@ executable(
   dependencies: [
     libnm_dep,
     newt_dep,
+    slang_dep,
     glib_dep,
   ],
   link_with: [
diff --git a/src/nmtui/nm-editor-bindings.c b/src/nmtui/nm-editor-bindings.c
index 360cf3ad..1408d263 100644
--- a/src/nmtui/nm-editor-bindings.c
+++ b/src/nmtui/nm-editor-bindings.c
@@ -195,7 +195,7 @@ ip_addresses_with_prefix_from_strv(GBinding     *binding,
  * @source: the source object (eg, an #NMSettingIP4Config)
  * @source_property: the property on @source to bind (eg,
  *   %NM_SETTING_IP4_CONFIG_ADDRESSES)
- * @target: the target object (eg, an #NmtAddressList)
+ * @target: the target object (eg, an #NmtList)
  * @target_property: the property on @target to bind
  *   (eg, "strings")
  * @flags: %GBindingFlags
@@ -253,7 +253,7 @@ ip_addresses_check_and_copy(GBinding     *binding,
  * @source: the source object (eg, an #NMSettingIP4Config)
  * @source_property: the property on @source to bind (eg,
  *   %NM_SETTING_IP4_CONFIG_DNS)
- * @target: the target object (eg, an #NmtAddressList)
+ * @target: the target object (eg, an #NmtList)
  * @target_property: the property on @target to bind
  *   (eg, "strings")
  * @flags: %GBindingFlags
diff --git a/src/nmtui/nmt-address-list.h b/src/nmtui/nmt-address-list.h
deleted file mode 100644
index 01669fbd..00000000
--- a/src/nmtui/nmt-address-list.h
+++ /dev/null
@@ -1,43 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/*
- * Copyright (C) 2013 Red Hat, Inc.
- */
-
-#ifndef NMT_ADDRESS_LIST_H
-#define NMT_ADDRESS_LIST_H
-
-#include "nmt-widget-list.h"
-
-#define NMT_TYPE_ADDRESS_LIST (nmt_address_list_get_type())
-#define NMT_ADDRESS_LIST(obj) \
-    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NMT_TYPE_ADDRESS_LIST, NmtAddressList))
-#define NMT_ADDRESS_LIST_CLASS(klass) \
-    (G_TYPE_CHECK_CLASS_CAST((klass), NMT_TYPE_ADDRESS_LIST, NmtAddressListClass))
-#define NMT_IS_ADDRESS_LIST(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NMT_TYPE_ADDRESS_LIST))
-#define NMT_IS_ADDRESS_LIST_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NMT_TYPE_ADDRESS_LIST))
-#define NMT_ADDRESS_LIST_GET_CLASS(obj) \
-    (G_TYPE_INSTANCE_GET_CLASS((obj), NMT_TYPE_ADDRESS_LIST, NmtAddressListClass))
-
-typedef struct {
-    NmtWidgetList parent;
-
-} NmtAddressList;
-
-typedef struct {
-    NmtWidgetListClass parent;
-
-} NmtAddressListClass;
-
-GType nmt_address_list_get_type(void);
-
-typedef enum {
-    NMT_ADDRESS_LIST_IP4_WITH_PREFIX,
-    NMT_ADDRESS_LIST_IP4,
-    NMT_ADDRESS_LIST_IP6_WITH_PREFIX,
-    NMT_ADDRESS_LIST_IP6,
-    NMT_ADDRESS_LIST_HOSTNAME
-} NmtAddressListType;
-
-NmtNewtWidget *nmt_address_list_new(NmtAddressListType list_type);
-
-#endif /* NMT_ADDRESS_LIST_H */
diff --git a/src/nmtui/nmt-connect-connection-list.c b/src/nmtui/nmt-connect-connection-list.c
index 8e7dc807..1980c30d 100644
--- a/src/nmtui/nmt-connect-connection-list.c
+++ b/src/nmtui/nmt-connect-connection-list.c
@@ -17,6 +17,7 @@
 
 #include "nmtui.h"
 #include "nmt-connect-connection-list.h"
+#include "nmt-utils.h"
 #include "libnmc-base/nm-client-utils.h"
 
 G_DEFINE_TYPE(NmtConnectConnectionList, nmt_connect_connection_list, NMT_TYPE_NEWT_LISTBOX)
@@ -47,6 +48,8 @@ typedef struct {
 
 typedef struct {
     GSList *nmt_devices;
+    char   *filter_text;
+    int     match_count;
 } NmtConnectConnectionListPrivate;
 
 /**
@@ -444,6 +447,13 @@ connection_find_ac(NMConnection *conn, const GPtrArray *acs)
     return NULL;
 }
 
+static gboolean
+connection_matches(NmtConnectConnection *nmtconn, const char *needle)
+{
+    return nmt_utils_filter_match(nmtconn->name, needle)
+           || nmt_utils_filter_match(nmtconn->ssid, needle);
+}
+
 static void
 nmt_connect_connection_list_rebuild(NmtConnectConnectionList *list)
 {
@@ -456,6 +466,8 @@ nmt_connect_connection_list_rebuild(NmtConnectConnectionList *list)
     GSList                          *nmt_devices, *diter, *citer;
     NmtConnectDevice                *nmtdev;
     NmtConnectConnection            *nmtconn;
+    gboolean                         did_group;
+    int                              n_matches = 0;
 
     g_slist_free_full(priv->nmt_devices, (GDestroyNotify) nmt_connect_device_free);
     priv->nmt_devices = NULL;
@@ -486,18 +498,32 @@ nmt_connect_connection_list_rebuild(NmtConnectConnectionList *list)
         }
     }
 
+    did_group = FALSE;
     for (diter = nmt_devices; diter; diter = diter->next) {
+        gboolean dev_matches = FALSE;
+
         nmtdev = diter->data;
 
-        if (nmtdev->conns) {
-            if (diter != nmt_devices)
-                nmt_newt_listbox_append(listbox, "", NULL);
-            nmt_newt_listbox_append(listbox, nmtdev->name, NULL);
+        for (citer = nmtdev->conns; citer; citer = citer->next) {
+            if (connection_matches(citer->data, priv->filter_text)) {
+                dev_matches = TRUE;
+                break;
+            }
         }
+        if (!dev_matches)
+            continue;
+
+        if (did_group)
+            nmt_newt_listbox_append(listbox, "", NULL);
+        nmt_newt_listbox_append(listbox, nmtdev->name, NULL);
+        did_group = TRUE;
 
         for (citer = nmtdev->conns; citer; citer = citer->next) {
             nmtconn = citer->data;
 
+            if (!connection_matches(nmtconn, priv->filter_text))
+                continue;
+
             if (nmtconn->conn)
                 nmtconn->active = connection_find_ac(nmtconn->conn, acs);
             if (nmtconn->active) {
@@ -523,15 +549,36 @@ nmt_connect_connection_list_rebuild(NmtConnectConnectionList *list)
 
             nmt_newt_listbox_append(listbox, row, nmtconn);
             g_free(row);
+            n_matches++;
         }
     }
 
     priv->nmt_devices = nmt_devices;
+    priv->match_count = n_matches;
 
     g_object_notify(G_OBJECT(listbox), "active");
     g_object_notify(G_OBJECT(listbox), "active-key");
 }
 
+void
+nmt_connect_connection_list_set_filter_text(NmtConnectConnectionList *list, const char *text)
+{
+    NmtConnectConnectionListPrivate *priv = NMT_CONNECT_CONNECTION_LIST_GET_PRIVATE(list);
+
+    if (nm_streq0(text, priv->filter_text))
+        return;
+
+    g_free(priv->filter_text);
+    priv->filter_text = g_strdup(text);
+    nmt_connect_connection_list_rebuild(list);
+}
+
+int
+nmt_connect_connection_list_get_match_count(NmtConnectConnectionList *list)
+{
+    return NMT_CONNECT_CONNECTION_LIST_GET_PRIVATE(list)->match_count;
+}
+
 static void
 rebuild_on_property_changed(GObject *object, GParamSpec *spec, gpointer list)
 {
@@ -567,6 +614,7 @@ nmt_connect_connection_list_finalize(GObject *object)
     NmtConnectConnectionListPrivate *priv = NMT_CONNECT_CONNECTION_LIST_GET_PRIVATE(object);
 
     g_slist_free_full(priv->nmt_devices, (GDestroyNotify) nmt_connect_device_free);
+    nm_clear_g_free(&priv->filter_text);
 
     g_signal_handlers_disconnect_by_func(nm_client,
                                          G_CALLBACK(rebuild_on_property_changed),
diff --git a/src/nmtui/nmt-connect-connection-list.h b/src/nmtui/nmt-connect-connection-list.h
index bfc0c007..7b8d4a4b 100644
--- a/src/nmtui/nmt-connect-connection-list.h
+++ b/src/nmtui/nmt-connect-connection-list.h
@@ -40,6 +40,10 @@ GType nmt_connect_connection_list_get_type(void);
 
 NmtNewtWidget *nmt_connect_connection_list_new(void);
 
+void nmt_connect_connection_list_set_filter_text(NmtConnectConnectionList *list, const char *text);
+
+int nmt_connect_connection_list_get_match_count(NmtConnectConnectionList *list);
+
 gboolean nmt_connect_connection_list_get_connection(NmtConnectConnectionList *list,
                                                     const char               *identifier,
                                                     NMConnection            **connection,
diff --git a/src/nmtui/nmt-device-entry.c b/src/nmtui/nmt-device-entry.c
index 99cc43ad..2034aabd 100644
--- a/src/nmtui/nmt-device-entry.c
+++ b/src/nmtui/nmt-device-entry.c
@@ -14,11 +14,6 @@
  * the entry recognizes the interface name or mac address typed in as
  * matching a known #NMDevice, then it will also display the other
  * property in parentheses.
- *
- * FIXME: #NmtDeviceEntry is currently an #NmtEditorGrid object, so that
- * we can possibly eventually add a button to its "extra" field, that
- * would pop up a form for selecting a device. But if we're not going
- * to implement that then we should make it just an #NmtNewtEntry.
  */
 
 #include "libnm-client-aux-extern/nm-default-client.h"
@@ -49,6 +44,7 @@ typedef struct {
     NmtNewtWidget *button;
 
     gboolean updating;
+    gboolean show_select_button;
 } NmtDeviceEntryPrivate;
 
 enum {
@@ -58,6 +54,7 @@ enum {
     PROP_HARDWARE_TYPE,
     PROP_INTERFACE_NAME,
     PROP_MAC_ADDRESS,
+    PROP_SHOW_SELECT_BUTTON,
 
     LAST_PROP
 };
@@ -68,16 +65,18 @@ enum {
  * @width: the width of the entry
  * @hardware_type: the type of #NMDevice to be selected, or
  *   %G_TYPE_NONE if this is for a virtual device type.
+ * @show_select_button: whether to show select button or not.
  *
  * Creates a new #NmtDeviceEntry, for identifying a device of type
  * @hardware_type. If @hardware_type is %G_TYPE_NONE (and you do not
  * set a #NmtDeviceEntryDeviceFilter), then this will only allow
- * specifying an interface name, not a hardware address.
+ * specifying an interface name, not a hardware address. @show_select_button
+ * will allow the user to select from a list of available devices of type @hardware_type.
  *
  * Returns: a new #NmtDeviceEntry.
  */
 NmtNewtWidget *
-nmt_device_entry_new(const char *label, int width, GType hardware_type)
+nmt_device_entry_new(const char *label, int width, GType hardware_type, gboolean show_select_button)
 {
     return g_object_new(NMT_TYPE_DEVICE_ENTRY,
                         "label",
@@ -86,6 +85,8 @@ nmt_device_entry_new(const char *label, int width, GType hardware_type)
                         width,
                         "hardware-type",
                         hardware_type,
+                        "show-select-button",
+                        show_select_button,
                         NULL);
 }
 
@@ -334,6 +335,133 @@ entry_text_changed(GObject *object, GParamSpec *pspec, gpointer deventry)
 }
 
 static void
+device_selected(NmtNewtWidget *listbox, gpointer user_data)
+{
+    NMDevice       *candidate = nmt_newt_listbox_get_active_key(NMT_NEWT_LISTBOX(listbox));
+    NmtDeviceEntry *deventry  = NMT_DEVICE_ENTRY(user_data);
+    const char     *ifname;
+
+    if (!candidate)
+        return;
+
+    ifname = nm_device_get_iface(candidate);
+    if (!ifname)
+        return;
+
+    if (nmt_device_entry_set_interface_name(deventry, ifname))
+        update_entry(deventry);
+}
+
+static int
+compare_devices_by_name(gconstpointer a, gconstpointer b)
+{
+    NMDevice **dev_a = (NMDevice **) a;
+    NMDevice **dev_b = (NMDevice **) b;
+
+    return nm_strcmp0(nm_device_get_iface(*dev_a), nm_device_get_iface(*dev_b));
+}
+
+static void
+do_select_dialog(NmtNewtWidget *button, gpointer user_data)
+{
+    NmtDeviceEntry              *deventry;
+    NmtDeviceEntryPrivate       *priv;
+    gs_unref_object NmtNewtForm *popup_form = NULL;
+    NmtNewtWidget               *listbox_widget;
+    NmtNewtForm                 *parent_form;
+    const GPtrArray             *devices;
+    gs_unref_ptrarray GPtrArray *matching_devices = NULL;
+    const char                  *ifname, *driver;
+    int                          i;
+    int                          entry_x, entry_y;
+    int                          window_x, window_y;
+    int                          popup_x, popup_y;
+    int                          list_w, list_h;
+    newtComponent                entry_component;
+
+    deventry    = NMT_DEVICE_ENTRY(user_data);
+    priv        = NMT_DEVICE_ENTRY_GET_PRIVATE(deventry);
+    parent_form = nmt_newt_widget_get_form(NMT_NEWT_WIDGET(deventry));
+    if (!parent_form)
+        return;
+
+    matching_devices = g_ptr_array_new();
+
+    entry_component = nmt_newt_component_get_component(NMT_NEWT_COMPONENT(priv->entry));
+    newtComponentGetPosition(entry_component, &entry_x, &entry_y);
+    g_object_get(parent_form, "x", &window_x, "y", &window_y, NULL);
+
+    listbox_widget = nmt_newt_listbox_new(5, NMT_NEWT_LISTBOX_SCROLL);
+    nmt_newt_widget_set_exit_on_activate(listbox_widget, TRUE);
+
+    nmt_newt_widget_set_padding(listbox_widget, 1, 0, 1, 0);
+
+    devices = nm_client_get_devices(nm_client);
+    for (i = 0; i < devices->len; i++) {
+        NMDevice *candidate = devices->pdata[i];
+
+        if (!G_TYPE_CHECK_INSTANCE_TYPE(candidate, priv->hardware_type))
+            continue;
+
+        if (priv->device_filter
+            && !priv->device_filter(deventry, candidate, priv->device_filter_data))
+            continue;
+
+        ifname = nm_device_get_iface(candidate);
+        if (!ifname)
+            continue;
+
+        g_ptr_array_add(matching_devices, candidate);
+    }
+
+    if (matching_devices->len == 0) {
+        nmt_newt_message_dialog(_("No devices available"));
+        return;
+    }
+
+    g_ptr_array_sort(matching_devices, compare_devices_by_name);
+
+    for (i = 0; i < matching_devices->len; i++) {
+        gs_free char *display_text = NULL;
+        NMDevice     *candidate    = matching_devices->pdata[i];
+
+        ifname = nm_device_get_iface(candidate);
+
+        driver = nm_device_get_driver(candidate);
+
+        if (driver && driver[0] != '\0') {
+            display_text = g_strdup_printf("%s (%s)", ifname, driver);
+        } else {
+            display_text = g_strdup(ifname);
+        }
+
+        nmt_newt_listbox_append(NMT_NEWT_LISTBOX(listbox_widget), display_text, candidate);
+    }
+
+    g_signal_connect(listbox_widget, "activated", G_CALLBACK(device_selected), deventry);
+
+    nmt_newt_widget_size_request(listbox_widget, &list_w, &list_h);
+    popup_x = window_x + entry_x + 1;
+    popup_y = window_y + entry_y + 1;
+
+    popup_form = g_object_new(NMT_TYPE_NEWT_FORM,
+                              "x",
+                              popup_x,
+                              "y",
+                              popup_y,
+                              "width",
+                              list_w,
+                              "height",
+                              list_h,
+                              "padding",
+                              0,
+                              NULL);
+
+    nmt_newt_form_set_content(popup_form, listbox_widget);
+    nmt_newt_form_show(popup_form);
+}
+
+static void
 nmt_device_entry_init(NmtDeviceEntry *deventry)
 {
     NmtDeviceEntryPrivate *priv = NMT_DEVICE_ENTRY_GET_PRIVATE(deventry);
@@ -346,11 +474,9 @@ nmt_device_entry_init(NmtDeviceEntry *deventry)
     nmt_newt_entry_set_validator(priv->entry, device_entry_validate, deventry);
     g_signal_connect(priv->entry, "notify::text", G_CALLBACK(entry_text_changed), deventry);
 
-#if 0
-    priv->button = nmt_newt_button_new (_("Select..."));
-    g_signal_connect (priv->button, "clicked",
-                      G_CALLBACK (do_select_dialog), deventry);
-#endif
+    priv->button = nmt_newt_button_new(_("Select..."));
+    nmt_newt_widget_set_visible(priv->button, FALSE);
+    g_signal_connect(priv->button, "clicked", G_CALLBACK(do_select_dialog), deventry);
 }
 
 static void
@@ -361,7 +487,7 @@ nmt_device_entry_constructed(GObject *object)
     nmt_editor_grid_append(NMT_EDITOR_GRID(object),
                            priv->label,
                            NMT_NEWT_WIDGET(priv->entry),
-                           NULL);
+                           priv->button);
 
     G_OBJECT_CLASS(nmt_device_entry_parent_class)->constructed(object);
 }
@@ -446,6 +572,10 @@ nmt_device_entry_set_property(GObject      *object,
         if (nmt_device_entry_set_mac_address(deventry, mac_address))
             update_entry(deventry);
         break;
+    case PROP_SHOW_SELECT_BUTTON:
+        priv->show_select_button = g_value_get_boolean(value);
+        nmt_newt_widget_set_visible(priv->button, priv->show_select_button);
+        break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
         break;
@@ -473,6 +603,9 @@ nmt_device_entry_get_property(GObject *object, guint prop_id, GValue *value, GPa
     case PROP_MAC_ADDRESS:
         g_value_set_string(value, priv->mac_address);
         break;
+    case PROP_SHOW_SELECT_BUTTON:
+        g_value_set_boolean(value, priv->show_select_button);
+        break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
         break;
@@ -554,4 +687,18 @@ nmt_device_entry_class_init(NmtDeviceEntryClass *deventry_class)
                             "",
                             NULL,
                             G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+    /**
+     * NmtDeviceEntry:show-select-button:
+     *
+     * Display select button to select device from available devices.
+     */
+    g_object_class_install_property(
+        object_class,
+        PROP_SHOW_SELECT_BUTTON,
+        g_param_spec_boolean("show-select-button",
+                             "",
+                             "",
+                             TRUE,
+                             G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
 }
diff --git a/src/nmtui/nmt-device-entry.h b/src/nmtui/nmt-device-entry.h
index 6265ce01..21be28c7 100644
--- a/src/nmtui/nmt-device-entry.h
+++ b/src/nmtui/nmt-device-entry.h
@@ -30,7 +30,10 @@ typedef struct {
 
 GType nmt_device_entry_get_type(void);
 
-NmtNewtWidget *nmt_device_entry_new(const char *label, int width, GType hardware_type);
+NmtNewtWidget *nmt_device_entry_new(const char *label,
+                                    int         width,
+                                    GType       hardware_type,
+                                    gboolean    show_select_button);
 
 typedef gboolean (*NmtDeviceEntryDeviceFilter)(NmtDeviceEntry *deventry,
                                                NMDevice       *device,
diff --git a/src/nmtui/nmt-edit-connection-list.c b/src/nmtui/nmt-edit-connection-list.c
index 1ea44007..45e6eb44 100644
--- a/src/nmtui/nmt-edit-connection-list.c
+++ b/src/nmtui/nmt-edit-connection-list.c
@@ -17,6 +17,7 @@
 #include "nmtui-edit.h"
 #include "nmt-edit-connection-list.h"
 #include "nmt-editor.h"
+#include "nmt-utils.h"
 
 #include "nm-editor-utils.h"
 
@@ -35,9 +36,14 @@ typedef struct {
     NmtNewtListbox   *listbox;
     NmtNewtButtonBox *buttons;
 
+    NmtSearch *search;
+    char      *filter_text;
+    int        match_count;
+
     NmtNewtWidget *add;
     NmtNewtWidget *edit;
     NmtNewtWidget *delete;
+    NmtNewtWidget *share;
     NmtNewtWidget *extra;
 } NmtEditConnectionListPrivate;
 
@@ -58,6 +64,7 @@ enum {
     ADD_CONNECTION,
     EDIT_CONNECTION,
     REMOVE_CONNECTION,
+    SHARE_CONNECTION,
 
     LAST_SIGNAL
 };
@@ -67,14 +74,19 @@ static guint signals[LAST_SIGNAL] = {0};
 static void add_clicked(NmtNewtButton *button, gpointer list);
 static void edit_clicked(NmtNewtButton *button, gpointer list);
 static void delete_clicked(NmtNewtButton *button, gpointer list);
+static void share_clicked(NmtNewtButton *button, gpointer list);
 static void listbox_activated(NmtNewtWidget *listbox, gpointer list);
+static void edit_search_apply(gpointer list, const char *text);
+static int  edit_search_count(gpointer list);
+static void update_share_sensitive(NmtEditConnectionList *list);
 
 static void
 nmt_edit_connection_list_init(NmtEditConnectionList *list)
 {
     NmtEditConnectionListPrivate *priv = NMT_EDIT_CONNECTION_LIST_GET_PRIVATE(list);
-    NmtNewtWidget                *listbox, *buttons;
+    NmtNewtWidget                *listbox, *buttons, *search_row, *search_label, *search;
     NmtNewtGrid                  *grid = NMT_NEWT_GRID(list);
+    NmtNewtGrid                  *search_grid;
 
     listbox       = g_object_new(NMT_TYPE_NEWT_LISTBOX,
                            "flags",
@@ -89,6 +101,32 @@ nmt_edit_connection_list_init(NmtEditConnectionList *list)
                             NMT_NEWT_GRID_FILL_X | NMT_NEWT_GRID_FILL_Y | NMT_NEWT_GRID_EXPAND_X
                                 | NMT_NEWT_GRID_EXPAND_Y);
     g_signal_connect(priv->listbox, "activated", G_CALLBACK(listbox_activated), list);
+    g_signal_connect_swapped(priv->listbox,
+                             "notify::active",
+                             G_CALLBACK(update_share_sensitive),
+                             list);
+
+    /* Search row below the listbox. The row is always present so revealing the
+     * entry does not resize the form; vim-style '/' shows the entry, and once a
+     * filter is applied the label reports it ("Matching '...' (N)"). */
+    search_row  = nmt_newt_grid_new();
+    search_grid = NMT_NEWT_GRID(search_row);
+    nmt_newt_grid_add(grid, search_row, 0, 1);
+    nmt_newt_grid_set_flags(grid, search_row, NMT_NEWT_GRID_FILL_X | NMT_NEWT_GRID_EXPAND_X);
+
+    search_label = nmt_newt_label_new("");
+    nmt_newt_grid_add(search_grid, search_label, 0, 0);
+
+    search = nmt_newt_entry_new(NMT_SEARCH_ENTRY_WIDTH, 0);
+    nmt_newt_grid_add(search_grid, search, 1, 0);
+    nmt_newt_widget_set_padding(search, 1, 0, 0, 0);
+
+    priv->search = nmt_search_new(NMT_NEWT_ENTRY(search),
+                                  NMT_NEWT_LABEL(search_label),
+                                  NMT_NEWT_WIDGET(priv->listbox),
+                                  edit_search_apply,
+                                  edit_search_count,
+                                  list);
 
     buttons       = nmt_newt_button_box_new(NMT_NEWT_BUTTON_BOX_VERTICAL);
     priv->buttons = NMT_NEWT_BUTTON_BOX(buttons);
@@ -106,6 +144,9 @@ nmt_edit_connection_list_init(NmtEditConnectionList *list)
 
     priv->delete = nmt_newt_button_box_add_start(priv->buttons, _("Delete"));
     g_signal_connect(priv->delete, "clicked", G_CALLBACK(delete_clicked), list);
+
+    priv->share = nmt_newt_button_box_add_start(priv->buttons, _("Share QR..."));
+    g_signal_connect(priv->share, "clicked", G_CALLBACK(share_clicked), list);
 }
 
 static int
@@ -157,7 +198,7 @@ nmt_edit_connection_list_rebuild(NmtEditConnectionList *list)
     gboolean                      did_header = FALSE, did_vpn = FALSE, did_any = FALSE;
     NMEditorConnectionTypeData  **types;
     NMConnection                 *conn, *selected_conn;
-    int                           i, row, selected_row;
+    int                           i, row, selected_row, n_matches = 0;
 
     selected_row  = nmt_newt_listbox_get_active(priv->listbox);
     selected_conn = nmt_newt_listbox_get_active_key(priv->listbox);
@@ -185,17 +226,21 @@ nmt_edit_connection_list_rebuild(NmtEditConnectionList *list)
 
     if (!priv->grouped) {
         /* Just add the connections in order */
-        for (iter = priv->connections, row = 0; iter; iter = iter->next, row++) {
+        for (iter = priv->connections, row = 0; iter; iter = iter->next) {
             conn = iter->data;
+            if (!nmt_utils_filter_match(nm_connection_get_id(conn), priv->filter_text))
+                continue;
             nmt_newt_listbox_append(priv->listbox, nm_connection_get_id(conn), conn);
             if (conn == selected_conn)
                 selected_row = row;
+            row++;
+            n_matches++;
         }
         if (selected_row >= row)
             selected_row = row - 1;
         nmt_newt_listbox_set_active(priv->listbox, selected_row);
 
-        did_any = !!priv->connections;
+        did_any = n_matches > 0;
 
         goto done;
     }
@@ -220,6 +265,8 @@ nmt_edit_connection_list_rebuild(NmtEditConnectionList *list)
                 continue;
             if (!nm_connection_is_type(conn, nm_setting_get_name(setting)))
                 continue;
+            if (!nmt_utils_filter_match(nm_connection_get_id(conn), priv->filter_text))
+                continue;
 
             if (!did_header) {
                 nmt_newt_listbox_append(priv->listbox, types[i]->name, NULL);
@@ -240,6 +287,7 @@ nmt_edit_connection_list_rebuild(NmtEditConnectionList *list)
             if (conn == selected_conn)
                 selected_row = row;
             row++;
+            n_matches++;
         }
     }
 
@@ -248,8 +296,12 @@ nmt_edit_connection_list_rebuild(NmtEditConnectionList *list)
     nmt_newt_listbox_set_active(priv->listbox, selected_row);
 
 done:
+    priv->match_count = n_matches;
+    if (priv->search)
+        nmt_search_update_label(priv->search);
     nmt_newt_component_set_sensitive(NMT_NEWT_COMPONENT(priv->edit), did_any);
     nmt_newt_component_set_sensitive(NMT_NEWT_COMPONENT(priv->delete), did_any);
+    update_share_sensitive(list);
 }
 
 static void
@@ -308,6 +360,29 @@ delete_clicked(NmtNewtButton *button, gpointer list)
 }
 
 static void
+share_clicked(NmtNewtButton *button, gpointer list)
+{
+    NmtEditConnectionListPrivate *priv = NMT_EDIT_CONNECTION_LIST_GET_PRIVATE(list);
+    NMConnection                 *connection;
+
+    connection = nmt_newt_listbox_get_active_key(priv->listbox);
+    g_return_if_fail(connection != NULL);
+
+    g_signal_emit(list, signals[SHARE_CONNECTION], 0, connection);
+}
+
+static void
+update_share_sensitive(NmtEditConnectionList *list)
+{
+    NmtEditConnectionListPrivate *priv = NMT_EDIT_CONNECTION_LIST_GET_PRIVATE(list);
+    NMConnection                 *connection;
+
+    connection = nmt_newt_listbox_get_active_key(priv->listbox);
+    nmt_newt_component_set_sensitive(NMT_NEWT_COMPONENT(priv->share),
+                                     connection && nm_connection_get_setting_wireless(connection));
+}
+
+static void
 listbox_activated(NmtNewtWidget *listbox, gpointer list)
 {
     NmtEditConnectionListPrivate *priv = NMT_EDIT_CONNECTION_LIST_GET_PRIVATE(list);
@@ -316,6 +391,31 @@ listbox_activated(NmtNewtWidget *listbox, gpointer list)
 }
 
 static void
+edit_search_apply(gpointer list, const char *text)
+{
+    NmtEditConnectionListPrivate *priv = NMT_EDIT_CONNECTION_LIST_GET_PRIVATE(list);
+
+    if (nm_streq0(text, priv->filter_text))
+        return;
+
+    g_free(priv->filter_text);
+    priv->filter_text = g_strdup(text);
+    nmt_edit_connection_list_rebuild(list);
+}
+
+static int
+edit_search_count(gpointer list)
+{
+    return NMT_EDIT_CONNECTION_LIST_GET_PRIVATE(list)->match_count;
+}
+
+void
+nmt_edit_connection_list_bind_search(NmtEditConnectionList *list, NmtNewtForm *form)
+{
+    nmt_search_bind_form(NMT_EDIT_CONNECTION_LIST_GET_PRIVATE(list)->search, form);
+}
+
+static void
 connection_saved(GObject *conn, GAsyncResult *result, gpointer user_data)
 {
     nm_remote_connection_save_finish(NM_REMOTE_CONNECTION(conn), result, NULL);
@@ -348,6 +448,8 @@ nmt_edit_connection_list_finalize(GObject *object)
 
     free_connections(NMT_EDIT_CONNECTION_LIST(object));
     g_clear_object(&priv->extra);
+    nm_clear_pointer(&priv->search, g_free);
+    nm_clear_g_free(&priv->filter_text);
 
     G_OBJECT_CLASS(nmt_edit_connection_list_parent_class)->finalize(object);
 }
@@ -490,6 +592,25 @@ nmt_edit_connection_list_class_init(NmtEditConnectionListClass *list_class)
                      1,
                      NM_TYPE_CONNECTION);
 
+    /**
+     * NmtEditConnectionList::share-connection:
+     * @list: the #NmtEditConnectionList
+     * @connection: the connection to share
+     *
+     * Emitted when the user clicks the list's "Share QR..." button.
+     */
+    signals[SHARE_CONNECTION] =
+        g_signal_new("share-connection",
+                     G_OBJECT_CLASS_TYPE(object_class),
+                     G_SIGNAL_RUN_FIRST,
+                     G_STRUCT_OFFSET(NmtEditConnectionListClass, share_connection),
+                     NULL,
+                     NULL,
+                     NULL,
+                     G_TYPE_NONE,
+                     1,
+                     NM_TYPE_CONNECTION);
+
     /* properties */
 
     /**
diff --git a/src/nmtui/nmt-edit-connection-list.h b/src/nmtui/nmt-edit-connection-list.h
index 0d23f8b1..1ae91d5f 100644
--- a/src/nmtui/nmt-edit-connection-list.h
+++ b/src/nmtui/nmt-edit-connection-list.h
@@ -32,6 +32,7 @@ typedef struct {
     void (*add_connection)(NmtEditConnectionList *list);
     void (*edit_connection)(NmtEditConnectionList *list, NMConnection *connection);
     void (*remove_connection)(NmtEditConnectionList *list, NMRemoteConnection *connection);
+    void (*share_connection)(NmtEditConnectionList *list, NMConnection *connection);
 } NmtEditConnectionListClass;
 
 GType nmt_edit_connection_list_get_type(void);
@@ -42,4 +43,6 @@ typedef gboolean (*NmtEditConnectionListFilter)(NmtEditConnectionList *list,
 
 void nmt_edit_connection_list_recommit(NmtEditConnectionList *list);
 
+void nmt_edit_connection_list_bind_search(NmtEditConnectionList *list, NmtNewtForm *form);
+
 #endif /* NMT_EDIT_CONNECTION_LIST_H */
diff --git a/src/nmtui/nmt-editor-grid.c b/src/nmtui/nmt-editor-grid.c
index bdcc02e2..21d362c4 100644
--- a/src/nmtui/nmt-editor-grid.c
+++ b/src/nmtui/nmt-editor-grid.c
@@ -244,8 +244,10 @@ nmt_editor_grid_get_components(NmtNewtWidget *widget)
 
         if (rows[i].extra) {
             child_cos = nmt_newt_widget_get_components(rows[i].extra);
-            for (c = 0; child_cos[c]; c++)
-                g_ptr_array_add(cos, child_cos[c]);
+            if (child_cos) {
+                for (c = 0; child_cos[c]; c++)
+                    g_ptr_array_add(cos, child_cos[c]);
+            }
             g_free(child_cos);
         }
     }
diff --git a/src/nmtui/nmt-editor.c b/src/nmtui/nmt-editor.c
index 474d08c2..b9f25ad0 100644
--- a/src/nmtui/nmt-editor.c
+++ b/src/nmtui/nmt-editor.c
@@ -14,6 +14,7 @@
 
 #include "nmt-editor.h"
 
+#include "libnm-core-aux-intern/nm-libnm-core-utils.h"
 #include "nm-utils.h"
 
 #include "nmtui.h"
@@ -153,10 +154,16 @@ save_connection_and_exit(NmtNewtButton *button, gpointer user_data)
     NmtEditor        *editor = user_data;
     NmtEditorPrivate *priv   = NMT_EDITOR_GET_PRIVATE(editor);
     NmtSyncOp         op;
-    GError           *error = NULL;
+    GError           *error      = NULL;
+    gs_free char     *gw_warning = NULL;
 
     nm_connection_replace_settings_from_connection(priv->orig_connection, priv->edit_connection);
 
+    gw_warning = nm_connection_get_unreachable_gateways_warning(priv->orig_connection, TRUE);
+    if (gw_warning) {
+        nmt_newt_message_dialog(_("Warning: %s"), gw_warning);
+    }
+
     nmt_sync_op_init(&op);
     if (NM_IS_REMOTE_CONNECTION(priv->orig_connection)) {
         nm_remote_connection_commit_changes_async(NM_REMOTE_CONNECTION(priv->orig_connection),
@@ -194,19 +201,6 @@ save_connection_and_exit(NmtNewtButton *button, gpointer user_data)
     nmt_newt_form_quit(NMT_NEWT_FORM(editor));
 }
 
-static void
-got_secrets(GObject *object, GAsyncResult *result, gpointer op)
-{
-    GVariant *secrets;
-    GError   *error = NULL;
-
-    secrets = nm_remote_connection_get_secrets_finish(NM_REMOTE_CONNECTION(object), result, &error);
-    if (secrets)
-        g_variant_ref(secrets);
-    nmt_sync_op_complete_pointer(op, secrets, error);
-    g_clear_error(&error);
-}
-
 static NMConnection *
 build_edit_connection(NMConnection *orig_connection)
 {
@@ -214,7 +208,6 @@ build_edit_connection(NMConnection *orig_connection)
     GVariant     *settings, *secrets;
     GVariantIter  iter;
     const char   *setting_name;
-    NmtSyncOp     op;
 
     edit_connection = nm_simple_connection_new_clone(orig_connection);
 
@@ -227,14 +220,8 @@ build_edit_connection(NMConnection *orig_connection)
         if (!nm_meta_setting_info_editor_has_secrets(
                 nm_meta_setting_info_editor_find_by_name(setting_name, FALSE)))
             continue;
-        nmt_sync_op_init(&op);
-        nm_remote_connection_get_secrets_async(NM_REMOTE_CONNECTION(orig_connection),
-                                               setting_name,
-                                               NULL,
-                                               got_secrets,
-                                               &op);
         /* FIXME: error handling */
-        secrets = nmt_sync_op_wait_pointer(&op, NULL);
+        secrets = nmt_sync_get_secrets(NM_REMOTE_CONNECTION(orig_connection), setting_name, NULL);
         if (secrets) {
             (void) nm_connection_update_secrets(edit_connection, setting_name, secrets, NULL);
             g_variant_unref(secrets);
@@ -310,6 +297,7 @@ nmt_editor_constructed(GObject *object)
     GType                hardware_type;
     const char          *port_type;
     NmtEditorPage       *page;
+    gboolean             show_select_button;
 
     if (G_OBJECT_CLASS(nmt_editor_parent_class)->constructed)
         G_OBJECT_CLASS(nmt_editor_parent_class)->constructed(object);
@@ -333,10 +321,13 @@ nmt_editor_constructed(GObject *object)
                            G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
     nmt_editor_grid_append(grid, _("Profile name"), widget, NULL);
 
-    if (priv->type_data->virtual)
-        hardware_type = G_TYPE_NONE;
-    else
-        hardware_type = priv->type_data->device_type;
+    if (priv->type_data->virtual) {
+        hardware_type      = G_TYPE_NONE;
+        show_select_button = FALSE;
+    } else {
+        hardware_type      = priv->type_data->device_type;
+        show_select_button = TRUE;
+    }
 
     if (nm_connection_is_type(priv->edit_connection, NM_SETTING_LOOPBACK_SETTING_NAME)) {
         g_object_set(s_con, NM_SETTING_CONNECTION_INTERFACE_NAME, "lo", NULL);
@@ -349,7 +340,7 @@ nmt_editor_constructed(GObject *object)
         else
             deventry_label = _("Device");
 
-        widget = nmt_device_entry_new(deventry_label, 40, hardware_type);
+        widget = nmt_device_entry_new(deventry_label, 40, hardware_type, show_select_button);
         nmt_editor_grid_append(grid, NULL, widget, NULL);
         deventry = NMT_DEVICE_ENTRY(widget);
         g_object_bind_property(s_con,
diff --git a/src/nmtui/nmt-address-list.c b/src/nmtui/nmt-list.c
index ff4bf1a4..3058638a 100644
--- a/src/nmtui/nmt-address-list.c
+++ b/src/nmtui/nmt-list.c
@@ -4,20 +4,19 @@
  */
 
 /**
- * SECTION:nmt-address-list
- * @short_description: An editable list of IP addresses or hostnames
+ * SECTION:nmt-list
+ * @short_description: An editable list of IP addresses, hostnames, or key=value pairs
  *
- * #NmtAddressList is a subclass of #NmtWidgetList that contains
- * entries displaying IP addresses, address/prefix strings, or
- * hostnames. This is designed for binding its #NmtAddressList:strings
- * property to an appropriate #NMSettingIP4Config or
- * #NMSettingIP6Config property via one of the nm-editor-bindings
- * functions.
+ * #NmtList is a subclass of #NmtWidgetList that contains
+ * entries displaying IP addresses, address/prefix strings,
+ * hostnames, or key=value pairs. This is designed for binding its
+ * #NmtList:strings property to an appropriate property via one
+ * of the nm-editor-bindings functions.
  */
 
 #include "libnm-client-aux-extern/nm-default-client.h"
 
-#include "nmt-address-list.h"
+#include "nmt-list.h"
 
 #include <arpa/inet.h>
 #include <netinet/in.h>
@@ -25,15 +24,14 @@
 
 #include "nmt-ip-entry.h"
 
-G_DEFINE_TYPE(NmtAddressList, nmt_address_list, NMT_TYPE_WIDGET_LIST)
+G_DEFINE_TYPE(NmtList, nmt_list, NMT_TYPE_WIDGET_LIST)
 
-#define NMT_ADDRESS_LIST_GET_PRIVATE(o) \
-    (G_TYPE_INSTANCE_GET_PRIVATE((o), NMT_TYPE_ADDRESS_LIST, NmtAddressListPrivate))
+#define NMT_LIST_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE((o), NMT_TYPE_LIST, NmtListPrivate))
 
 typedef struct {
-    NmtAddressListType list_type;
-    char             **strings;
-} NmtAddressListPrivate;
+    NmtListType list_type;
+    char      **strings;
+} NmtListPrivate;
 
 enum {
     PROP_0,
@@ -44,32 +42,32 @@ enum {
 };
 
 /**
- * NmtAddressListType:
- * @NMT_ADDRESS_LIST_IP4_WITH_PREFIX: IPv4 address/prefix strings
- * @NMT_ADDRESS_LIST_IP4: IPv4 addresses
- * @NMT_ADDRESS_LIST_IP6_WITH_PREFIX: IPv6 address/prefix strings
- * @NMT_ADDRESS_LIST_IP6: IPv6 addresses
- * @NMT_ADDRESS_LIST_HOSTNAME: hostnames
+ * NmtListType:
+ * @NMT_LIST_IP4_WITH_PREFIX: IPv4 address/prefix strings
+ * @NMT_LIST_IP4: IPv4 addresses
+ * @NMT_LIST_IP6_WITH_PREFIX: IPv6 address/prefix strings
+ * @NMT_LIST_IP6: IPv6 addresses
+ * @NMT_LIST_HOSTNAME: hostnames
  *
- * The type of address in an #NmtAddressList
+ * The type of address in an #NmtList
  */
 
 /**
- * nmt_address_list_new:
+ * nmt_list_new:
  * @list_type: the type of address the list will contain
  *
- * Creates a new #NmtAddressList
+ * Creates a new #NmtList
  *
- * Returns: a new #NmtAddressList
+ * Returns: a new #NmtList
  */
 NmtNewtWidget *
-nmt_address_list_new(NmtAddressListType list_type)
+nmt_list_new(NmtListType list_type)
 {
-    return g_object_new(NMT_TYPE_ADDRESS_LIST, "list-type", list_type, NULL);
+    return g_object_new(NMT_TYPE_LIST, "list-type", list_type, NULL);
 }
 
 static void
-nmt_address_list_init(NmtAddressList *list)
+nmt_list_init(NmtList *list)
 {}
 
 static gboolean
@@ -95,9 +93,9 @@ strings_transform_from_entry(GBinding     *binding,
                              GValue       *target_value,
                              gpointer      user_data)
 {
-    NmtAddressList        *list = NMT_ADDRESS_LIST(g_binding_get_source(binding));
-    NmtAddressListPrivate *priv = NMT_ADDRESS_LIST_GET_PRIVATE(list);
-    int                    n    = GPOINTER_TO_INT(user_data);
+    NmtList        *list = NMT_LIST(g_binding_get_source(binding));
+    NmtListPrivate *priv = NMT_LIST_GET_PRIVATE(list);
+    int             n    = GPOINTER_TO_INT(user_data);
 
     if (n >= g_strv_length(priv->strings))
         return FALSE;
@@ -112,26 +110,41 @@ strings_transform_from_entry(GBinding     *binding,
 static gboolean
 hostname_filter(NmtNewtEntry *entry, const char *text, int ch, int position, gpointer user_data)
 {
-    return g_ascii_isalnum(ch) || ch == '.' || ch == '-';
+    return g_ascii_isalnum(ch) || ch == '.' || ch == '-' || ch == '~';
+}
+
+static gboolean
+key_value_validate(NmtNewtEntry *entry, const char *text, gpointer user_data)
+{
+    const char *val;
+
+    if (!text || !text[0])
+        return TRUE;
+
+    val = strchr(text, '=');
+    return val && val != text && val[1];
 }
 
 static NmtNewtWidget *
-nmt_address_list_create_widget(NmtWidgetList *list, int num)
+nmt_list_create_widget(NmtWidgetList *list, int num)
 {
-    NmtAddressListPrivate *priv = NMT_ADDRESS_LIST_GET_PRIVATE(list);
-    NmtNewtWidget         *entry;
+    NmtListPrivate *priv = NMT_LIST_GET_PRIVATE(list);
+    NmtNewtWidget  *entry;
 
-    if (priv->list_type == NMT_ADDRESS_LIST_IP4_WITH_PREFIX) {
+    if (priv->list_type == NMT_LIST_IP4_WITH_PREFIX) {
         entry = nmt_ip_entry_new(25, AF_INET, TRUE, FALSE);
-    } else if (priv->list_type == NMT_ADDRESS_LIST_IP4) {
+    } else if (priv->list_type == NMT_LIST_IP4) {
         entry = nmt_ip_entry_new(25, AF_INET, FALSE, FALSE);
-    } else if (priv->list_type == NMT_ADDRESS_LIST_IP6_WITH_PREFIX) {
+    } else if (priv->list_type == NMT_LIST_IP6_WITH_PREFIX) {
         entry = nmt_ip_entry_new(25, AF_INET6, TRUE, FALSE);
-    } else if (priv->list_type == NMT_ADDRESS_LIST_IP6) {
+    } else if (priv->list_type == NMT_LIST_IP6) {
         entry = nmt_ip_entry_new(25, AF_INET6, FALSE, FALSE);
-    } else if (priv->list_type == NMT_ADDRESS_LIST_HOSTNAME) {
+    } else if (priv->list_type == NMT_LIST_HOSTNAME) {
         entry = nmt_newt_entry_new(25, NMT_NEWT_ENTRY_NONEMPTY);
         nmt_newt_entry_set_filter(NMT_NEWT_ENTRY(entry), hostname_filter, list);
+    } else if (priv->list_type == NMT_LIST_KEY_VALUE) {
+        entry = nmt_newt_entry_new(40, 0);
+        nmt_newt_entry_set_validator(NMT_NEWT_ENTRY(entry), key_value_validate, NULL);
     } else {
         g_return_val_if_reached(NULL);
     }
@@ -150,10 +163,10 @@ nmt_address_list_create_widget(NmtWidgetList *list, int num)
 }
 
 static void
-nmt_address_list_add_clicked(NmtWidgetList *list)
+nmt_list_add_clicked(NmtWidgetList *list)
 {
-    NmtAddressListPrivate *priv = NMT_ADDRESS_LIST_GET_PRIVATE(list);
-    int                    len;
+    NmtListPrivate *priv = NMT_LIST_GET_PRIVATE(list);
+    int             len;
 
     len                    = priv->strings ? g_strv_length(priv->strings) : 0;
     priv->strings          = g_renew(char *, priv->strings, len + 2);
@@ -165,10 +178,10 @@ nmt_address_list_add_clicked(NmtWidgetList *list)
 }
 
 static void
-nmt_address_list_remove_clicked(NmtWidgetList *list, int num)
+nmt_list_remove_clicked(NmtWidgetList *list, int num)
 {
-    NmtAddressListPrivate *priv = NMT_ADDRESS_LIST_GET_PRIVATE(list);
-    int                    len;
+    NmtListPrivate *priv = NMT_LIST_GET_PRIVATE(list);
+    int             len;
 
     len = g_strv_length(priv->strings);
     g_free(priv->strings[num]);
@@ -179,12 +192,9 @@ nmt_address_list_remove_clicked(NmtWidgetList *list, int num)
 }
 
 static void
-nmt_address_list_set_property(GObject      *object,
-                              guint         prop_id,
-                              const GValue *value,
-                              GParamSpec   *pspec)
+nmt_list_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
 {
-    NmtAddressListPrivate *priv = NMT_ADDRESS_LIST_GET_PRIVATE(object);
+    NmtListPrivate *priv = NMT_LIST_GET_PRIVATE(object);
 
     switch (prop_id) {
     case PROP_LIST_TYPE:
@@ -204,9 +214,9 @@ nmt_address_list_set_property(GObject      *object,
 }
 
 static void
-nmt_address_list_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+nmt_list_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
 {
-    NmtAddressListPrivate *priv = NMT_ADDRESS_LIST_GET_PRIVATE(object);
+    NmtListPrivate *priv = NMT_LIST_GET_PRIVATE(object);
 
     switch (prop_id) {
     case PROP_LIST_TYPE:
@@ -222,23 +232,23 @@ nmt_address_list_get_property(GObject *object, guint prop_id, GValue *value, GPa
 }
 
 static void
-nmt_address_list_class_init(NmtAddressListClass *list_class)
+nmt_list_class_init(NmtListClass *list_class)
 {
     GObjectClass       *object_class      = G_OBJECT_CLASS(list_class);
     NmtWidgetListClass *widget_list_class = NMT_WIDGET_LIST_CLASS(list_class);
 
-    g_type_class_add_private(list_class, sizeof(NmtAddressListPrivate));
+    g_type_class_add_private(list_class, sizeof(NmtListPrivate));
 
     /* virtual methods */
-    object_class->set_property = nmt_address_list_set_property;
-    object_class->get_property = nmt_address_list_get_property;
+    object_class->set_property = nmt_list_set_property;
+    object_class->get_property = nmt_list_get_property;
 
-    widget_list_class->create_widget  = nmt_address_list_create_widget;
-    widget_list_class->add_clicked    = nmt_address_list_add_clicked;
-    widget_list_class->remove_clicked = nmt_address_list_remove_clicked;
+    widget_list_class->create_widget  = nmt_list_create_widget;
+    widget_list_class->add_clicked    = nmt_list_add_clicked;
+    widget_list_class->remove_clicked = nmt_list_remove_clicked;
 
     /**
-     * NmtAddressList:list-type:
+     * NmtList:list-type:
      *
      * The type of address the list holds.
      */
@@ -253,7 +263,7 @@ nmt_address_list_class_init(NmtAddressListClass *list_class)
                           0,
                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
     /**
-     * NmtAddressList:strings:
+     * NmtList:strings:
      *
      * The strings in the list's entries.
      */
diff --git a/src/nmtui/nmt-list.h b/src/nmtui/nmt-list.h
new file mode 100644
index 00000000..65def938
--- /dev/null
+++ b/src/nmtui/nmt-list.h
@@ -0,0 +1,41 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 Red Hat, Inc.
+ */
+
+#ifndef NMT_LIST_H
+#define NMT_LIST_H
+
+#include "nmt-widget-list.h"
+
+#define NMT_TYPE_LIST            (nmt_list_get_type())
+#define NMT_LIST(obj)            (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NMT_TYPE_LIST, NmtList))
+#define NMT_LIST_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), NMT_TYPE_LIST, NmtListClass))
+#define NMT_IS_LIST(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NMT_TYPE_LIST))
+#define NMT_IS_LIST_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NMT_TYPE_LIST))
+#define NMT_LIST_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), NMT_TYPE_LIST, NmtListClass))
+
+typedef struct {
+    NmtWidgetList parent;
+
+} NmtList;
+
+typedef struct {
+    NmtWidgetListClass parent;
+
+} NmtListClass;
+
+GType nmt_list_get_type(void);
+
+typedef enum {
+    NMT_LIST_IP4_WITH_PREFIX,
+    NMT_LIST_IP4,
+    NMT_LIST_IP6_WITH_PREFIX,
+    NMT_LIST_IP6,
+    NMT_LIST_HOSTNAME,
+    NMT_LIST_KEY_VALUE
+} NmtListType;
+
+NmtNewtWidget *nmt_list_new(NmtListType list_type);
+
+#endif /* NMT_LIST_H */
diff --git a/src/nmtui/nmt-page-bond.c b/src/nmtui/nmt-page-bond.c
index f74bac27..eeea0b0b 100644
--- a/src/nmtui/nmt-page-bond.c
+++ b/src/nmtui/nmt-page-bond.c
@@ -21,7 +21,7 @@
 
 #include "libnm-core-aux-intern/nm-libnm-core-utils.h"
 #include "nmt-mac-entry.h"
-#include "nmt-address-list.h"
+#include "nmt-list.h"
 #include "nmt-port-list.h"
 
 G_DEFINE_TYPE(NmtPageBond, nmt_page_bond, NMT_TYPE_EDITOR_PAGE_DEVICE)
@@ -41,14 +41,15 @@ typedef struct {
     /* Note: when adding new options to the UI also ensure they are
      * initialized in bond_connection_setup_func()
      */
-    NmtNewtPopup   *mode;
-    NmtNewtEntry   *primary;
-    NmtNewtPopup   *monitoring;
-    NmtNewtEntry   *miimon;
-    NmtNewtEntry   *updelay;
-    NmtNewtEntry   *downdelay;
-    NmtNewtEntry   *arp_interval;
-    NmtAddressList *arp_ip_target;
+    NmtNewtPopup *mode;
+    NmtNewtEntry *primary;
+    NmtNewtPopup *monitoring;
+    NmtNewtEntry *miimon;
+    NmtNewtEntry *updelay;
+    NmtNewtEntry *downdelay;
+    NmtNewtEntry *arp_interval;
+    NmtList      *arp_ip_target;
+    NmtList      *other_options;
 
     NmtPageBondMonitoringMode monitoring_mode;
 
@@ -63,6 +64,45 @@ static void arp_ip_target_widget_changed(GObject *object, GParamSpec *pspec, gpo
 
 /*****************************************************************************/
 
+static gboolean
+_is_other_option(const char *option)
+{
+    return !NM_IN_STRSET(option,
+                         NM_SETTING_BOND_OPTION_MODE,
+                         NM_SETTING_BOND_OPTION_PRIMARY,
+                         NM_SETTING_BOND_OPTION_MIIMON,
+                         NM_SETTING_BOND_OPTION_UPDELAY,
+                         NM_SETTING_BOND_OPTION_DOWNDELAY,
+                         NM_SETTING_BOND_OPTION_ARP_INTERVAL,
+                         NM_SETTING_BOND_OPTION_ARP_IP_TARGET);
+}
+
+static void
+_bond_update_other_options(NMSettingBond *s_bond, NmtList *list)
+{
+    gs_unref_ptrarray GPtrArray *arr      = g_ptr_array_new_with_free_func(g_free);
+    guint                        num_opts = nm_setting_bond_get_num_options(s_bond);
+    guint                        i;
+
+    for (i = 0; i < num_opts; i++) {
+        const char *opt_name;
+        const char *opt_value;
+        gboolean    ret;
+
+        ret = nm_setting_bond_get_option(s_bond, i, &opt_name, &opt_value);
+        nm_assert(ret);
+
+        if (_is_other_option(opt_name)) {
+            g_ptr_array_add(arr, g_strdup_printf("%s=%s", opt_name, opt_value));
+        }
+    }
+
+    g_ptr_array_add(arr, NULL);
+    g_object_set(G_OBJECT(list), "strings", arr->pdata, NULL);
+}
+
+/*****************************************************************************/
+
 NmtEditorPage *
 nmt_page_bond_new(NMConnection *conn, NmtDeviceEntry *deventry)
 {
@@ -155,6 +195,8 @@ bond_options_changed(GObject *object, GParamSpec *pspec, gpointer user_data)
     nmt_newt_widget_set_visible(NMT_NEWT_WIDGET(priv->arp_interval), !visible_mii);
     nmt_newt_widget_set_visible(NMT_NEWT_WIDGET(priv->arp_ip_target), !visible_mii);
 
+    _bond_update_other_options(s_bond, priv->other_options);
+
     priv->updating = FALSE;
 }
 
@@ -308,6 +350,49 @@ arp_ip_target_widget_changed(GObject *object, GParamSpec *pspec, gpointer user_d
     g_strfreev(ips);
 }
 
+static void
+other_options_widget_changed(GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+    NmtPageBond        *bond          = NMT_PAGE_BOND(user_data);
+    NmtPageBondPrivate *priv          = NMT_PAGE_BOND_GET_PRIVATE(bond);
+    gs_strfreev char  **other_options = NULL;
+    const char         *name;
+    gboolean            ret;
+    guint               num;
+    guint               i;
+
+    if (priv->updating)
+        return;
+
+    priv->updating = TRUE;
+
+    g_object_get(G_OBJECT(priv->other_options), "strings", &other_options, NULL);
+
+again:
+    num = nm_setting_bond_get_num_options(priv->s_bond);
+    for (i = 0; i < num; i++) {
+        ret = nm_setting_bond_get_option(priv->s_bond, i, &name, NULL);
+        nm_assert(ret);
+
+        if (_is_other_option(name)) {
+            nm_setting_bond_remove_option(priv->s_bond, name);
+            goto again;
+        }
+    }
+
+    for (i = 0; other_options && other_options[i]; i++) {
+        char *val = strchr(other_options[i], '=');
+
+        if (val && val != other_options[i] && val[1]) {
+            *val = '\0';
+            if (_is_other_option(other_options[i]))
+                nm_setting_bond_add_option(priv->s_bond, other_options[i], val + 1);
+        }
+    }
+
+    priv->updating = FALSE;
+}
+
 static gboolean
 bond_connection_type_filter(GType connection_type, gpointer user_data)
 {
@@ -390,10 +475,15 @@ nmt_page_bond_constructed(GObject *object)
     nmt_editor_grid_append(grid, _("Monitoring frequency"), widget, label);
     priv->arp_interval = NMT_NEWT_ENTRY(widget);
 
-    widget = nmt_address_list_new(NMT_ADDRESS_LIST_IP4);
+    widget = nmt_list_new(NMT_LIST_IP4);
     g_signal_connect(widget, "notify::strings", G_CALLBACK(arp_ip_target_widget_changed), bond);
     nmt_editor_grid_append(grid, _("ARP targets"), widget, NULL);
-    priv->arp_ip_target = NMT_ADDRESS_LIST(widget);
+    priv->arp_ip_target = NMT_LIST(widget);
+
+    widget = nmt_list_new(NMT_LIST_KEY_VALUE);
+    g_signal_connect(widget, "notify::strings", G_CALLBACK(other_options_widget_changed), bond);
+    nmt_editor_grid_append(grid, _("Other options (key=value)"), widget, NULL);
+    priv->other_options = NMT_LIST(widget);
 
     widget = nmt_mac_entry_new(40, ETH_ALEN, NMT_MAC_ENTRY_TYPE_CLONED_ETHERNET);
     g_object_bind_property(s_wired,
diff --git a/src/nmtui/nmt-page-bridge.c b/src/nmtui/nmt-page-bridge.c
index da765b14..97c444be 100644
--- a/src/nmtui/nmt-page-bridge.c
+++ b/src/nmtui/nmt-page-bridge.c
@@ -13,7 +13,7 @@
 #include "nmt-page-bridge.h"
 
 #include "libnm-core-aux-intern/nm-libnm-core-utils.h"
-#include "nmt-address-list.h"
+#include "nmt-list.h"
 #include "nmt-port-list.h"
 
 G_DEFINE_TYPE(NmtPageBridge, nmt_page_bridge, NMT_TYPE_EDITOR_PAGE_DEVICE)
diff --git a/src/nmtui/nmt-page-ip-tunnel.c b/src/nmtui/nmt-page-ip-tunnel.c
index c74037b6..4932c0dc 100644
--- a/src/nmtui/nmt-page-ip-tunnel.c
+++ b/src/nmtui/nmt-page-ip-tunnel.c
@@ -127,7 +127,7 @@ nmt_page_ip_tunnel_constructed(GObject *object)
                                          w2s);
     nmt_editor_grid_append(grid, _("Mode"), widget, NULL);
 
-    widget = parent = nmt_device_entry_new(_("Parent"), 40, G_TYPE_NONE);
+    widget = parent = nmt_device_entry_new(_("Parent"), 40, G_TYPE_NONE, FALSE);
     g_object_bind_property(s_ip_tunnel,
                            NM_SETTING_IP_TUNNEL_PARENT,
                            widget,
diff --git a/src/nmtui/nmt-page-ip4.c b/src/nmtui/nmt-page-ip4.c
index 4d01b356..ebbc7682 100644
--- a/src/nmtui/nmt-page-ip4.c
+++ b/src/nmtui/nmt-page-ip4.c
@@ -16,7 +16,7 @@
 
 #include "libnm-core-aux-intern/nm-libnm-core-utils.h"
 #include "nmt-ip-entry.h"
-#include "nmt-address-list.h"
+#include "nmt-list.h"
 #include "nmt-route-editor.h"
 
 #include "nm-editor-bindings.h"
@@ -112,7 +112,7 @@ nmt_page_ip4_constructed(GObject *object)
     section = nmt_editor_section_new(_("IPv4 CONFIGURATION"), widget, show_by_default);
     grid    = nmt_editor_section_get_body(section);
 
-    widget = nmt_address_list_new(NMT_ADDRESS_LIST_IP4_WITH_PREFIX);
+    widget = nmt_list_new(NMT_LIST_IP4_WITH_PREFIX);
     nm_editor_bind_ip_addresses_with_prefix_to_strv(AF_INET,
                                                     s_ip4,
                                                     NM_SETTING_IP_CONFIG_ADDRESSES,
@@ -131,7 +131,7 @@ nmt_page_ip4_constructed(GObject *object)
                                         G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
     nmt_editor_grid_append(grid, _("Gateway"), widget, NULL);
 
-    widget = nmt_address_list_new(NMT_ADDRESS_LIST_IP4);
+    widget = nmt_list_new(NMT_LIST_IP4);
     nm_editor_bind_ip_addresses_to_strv(AF_INET,
                                         s_ip4,
                                         NM_SETTING_IP_CONFIG_DNS,
@@ -140,7 +140,7 @@ nmt_page_ip4_constructed(GObject *object)
                                         G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
     nmt_editor_grid_append(grid, _("DNS servers"), widget, NULL);
 
-    widget = nmt_address_list_new(NMT_ADDRESS_LIST_HOSTNAME);
+    widget = nmt_list_new(NMT_LIST_HOSTNAME);
     g_object_bind_property(s_ip4,
                            NM_SETTING_IP_CONFIG_DNS_SEARCH,
                            widget,
diff --git a/src/nmtui/nmt-page-ip6.c b/src/nmtui/nmt-page-ip6.c
index bd29a3a7..c0dc88f3 100644
--- a/src/nmtui/nmt-page-ip6.c
+++ b/src/nmtui/nmt-page-ip6.c
@@ -16,7 +16,7 @@
 
 #include "libnm-core-aux-intern/nm-libnm-core-utils.h"
 #include "nmt-ip-entry.h"
-#include "nmt-address-list.h"
+#include "nmt-list.h"
 #include "nmt-route-editor.h"
 
 #include "nm-editor-bindings.h"
@@ -114,7 +114,7 @@ nmt_page_ip6_constructed(GObject *object)
     section = nmt_editor_section_new(_("IPv6 CONFIGURATION"), widget, show_by_default);
     grid    = nmt_editor_section_get_body(section);
 
-    widget = nmt_address_list_new(NMT_ADDRESS_LIST_IP6_WITH_PREFIX);
+    widget = nmt_list_new(NMT_LIST_IP6_WITH_PREFIX);
     nm_editor_bind_ip_addresses_with_prefix_to_strv(AF_INET6,
                                                     s_ip6,
                                                     NM_SETTING_IP_CONFIG_ADDRESSES,
@@ -133,7 +133,7 @@ nmt_page_ip6_constructed(GObject *object)
                                         G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
     nmt_editor_grid_append(grid, _("Gateway"), widget, NULL);
 
-    widget = nmt_address_list_new(NMT_ADDRESS_LIST_IP6);
+    widget = nmt_list_new(NMT_LIST_IP6);
     nm_editor_bind_ip_addresses_to_strv(AF_INET6,
                                         s_ip6,
                                         NM_SETTING_IP_CONFIG_DNS,
@@ -142,7 +142,7 @@ nmt_page_ip6_constructed(GObject *object)
                                         G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
     nmt_editor_grid_append(grid, _("DNS servers"), widget, NULL);
 
-    widget = nmt_address_list_new(NMT_ADDRESS_LIST_HOSTNAME);
+    widget = nmt_list_new(NMT_LIST_HOSTNAME);
     g_object_bind_property(s_ip6,
                            NM_SETTING_IP_CONFIG_DNS_SEARCH,
                            widget,
diff --git a/src/nmtui/nmt-page-macsec.c b/src/nmtui/nmt-page-macsec.c
index 6a017486..cd6a5494 100644
--- a/src/nmtui/nmt-page-macsec.c
+++ b/src/nmtui/nmt-page-macsec.c
@@ -99,7 +99,7 @@ nmt_page_macsec_constructed(GObject *object)
     section = nmt_editor_section_new(_("MACsec"), NULL, TRUE);
     grid    = nmt_editor_section_get_body(section);
 
-    widget = nmt_device_entry_new(_("Parent device"), 40, G_TYPE_NONE);
+    widget = nmt_device_entry_new(_("Parent device"), 40, G_TYPE_NONE, FALSE);
     g_object_bind_property(s_macsec,
                            NM_SETTING_MACSEC_PARENT,
                            widget,
diff --git a/src/nmtui/nmt-page-vlan.c b/src/nmtui/nmt-page-vlan.c
index 238dbafb..64c10ef7 100644
--- a/src/nmtui/nmt-page-vlan.c
+++ b/src/nmtui/nmt-page-vlan.c
@@ -59,7 +59,7 @@ nmt_page_vlan_constructed(GObject *object)
 
     nm_editor_bind_vlan_name(s_vlan, nm_connection_get_setting_connection(conn));
 
-    widget = parent = nmt_device_entry_new(_("Parent"), 40, G_TYPE_NONE);
+    widget = parent = nmt_device_entry_new(_("Parent"), 40, G_TYPE_NONE, FALSE);
     nmt_device_entry_set_device_filter(NMT_DEVICE_ENTRY(widget), vlan_device_filter, vlan);
     g_object_bind_property(s_vlan,
                            NM_SETTING_VLAN_PARENT,
diff --git a/src/nmtui/nmt-page-wifi.c b/src/nmtui/nmt-page-wifi.c
index 9767597b..d2d0024d 100644
--- a/src/nmtui/nmt-page-wifi.c
+++ b/src/nmtui/nmt-page-wifi.c
@@ -54,10 +54,9 @@ static NmtNewtPopupEntry wifi_mode[] = {{NC_("Wi-Fi", "Client"), NM_SETTING_WIRE
                                         {NULL, NULL}};
 
 static NmtNewtPopupEntry wifi_band[] = {{NC_("Wi-Fi", "Automatic"), NULL},
-                                        /* 802.11a Wi-Fi network */
-                                        {N_("A (5 GHz)"), "a"},
-                                        /* 802.11b / 802.11g Wi-Fi network */
-                                        {N_("B/G (2.4 GHz)"), "bg"},
+                                        {N_("5 GHz"), "a"},
+                                        {N_("2.4 GHz"), "bg"},
+                                        {N_("6 GHz"), "6GHz"},
                                         {NULL, NULL}};
 
 static struct {
diff --git a/src/nmtui/nmt-password-dialog.c b/src/nmtui/nmt-password-dialog.c
index 6f1a5f03..1c2e6b00 100644
--- a/src/nmtui/nmt-password-dialog.c
+++ b/src/nmtui/nmt-password-dialog.c
@@ -109,12 +109,13 @@ maybe_save_input_and_exit(NmtNewtWidget *widget, gpointer dialog)
 static void
 nmt_password_dialog_constructed(GObject *object)
 {
-    NmtPasswordDialog        *dialog = NMT_PASSWORD_DIALOG(object);
-    NmtPasswordDialogPrivate *priv   = NMT_PASSWORD_DIALOG_GET_PRIVATE(dialog);
-    NmtNewtWidget            *widget;
-    NmtNewtGrid              *grid, *secret_grid;
-    NmtNewtButtonBox         *bbox;
-    int                       i;
+    NmtPasswordDialog           *dialog = NMT_PASSWORD_DIALOG(object);
+    NmtPasswordDialogPrivate    *priv   = NMT_PASSWORD_DIALOG_GET_PRIVATE(dialog);
+    NmtNewtWidget               *widget;
+    NmtNewtGrid                 *grid, *secret_grid;
+    NmtNewtButtonBox            *bbox;
+    gs_unref_ptrarray GPtrArray *masked_entries = g_ptr_array_new();
+    int                          i;
 
     widget = nmt_newt_grid_new();
     nmt_newt_form_set_content(NMT_NEWT_FORM(dialog), widget);
@@ -133,19 +134,23 @@ nmt_password_dialog_constructed(GObject *object)
     for (i = 0; i < priv->secrets->len; i++) {
         NMSecretAgentSimpleSecret *secret = priv->secrets->pdata[i];
         NmtNewtEntryFlags          flags;
+        gboolean                   masked;
 
         widget = nmt_newt_label_new(secret->pretty_name);
         nmt_newt_grid_add(secret_grid, widget, 0, i);
         nmt_newt_widget_set_padding(widget, 4, 0, 1, 0);
 
-        flags = NMT_NEWT_ENTRY_NONEMPTY;
-        if (secret->is_secret && !secret->force_echo)
+        flags  = NMT_NEWT_ENTRY_NONEMPTY;
+        masked = secret->is_secret && !secret->force_echo;
+        if (masked)
             flags |= NMT_NEWT_ENTRY_PASSWORD;
         widget = nmt_newt_entry_new(30, flags);
         if (secret->value)
             nmt_newt_entry_set_text(NMT_NEWT_ENTRY(widget), secret->value);
         nmt_newt_grid_add(secret_grid, widget, 1, i);
         g_ptr_array_add(priv->entries, widget);
+        if (masked)
+            g_ptr_array_add(masked_entries, widget);
 
         if (i == priv->secrets->len - 1) {
             priv->last_entry = widget;
@@ -153,6 +158,18 @@ nmt_password_dialog_constructed(GObject *object)
         }
     }
 
+    if (masked_entries->len > 0) {
+        widget = nmt_newt_checkbox_new(_("Show password"));
+        nmt_newt_grid_add(secret_grid, widget, 1, priv->secrets->len);
+        for (i = 0; i < masked_entries->len; i++) {
+            g_object_bind_property(widget,
+                                   "active",
+                                   masked_entries->pdata[i],
+                                   "password",
+                                   G_BINDING_INVERT_BOOLEAN | G_BINDING_SYNC_CREATE);
+        }
+    }
+
     widget = nmt_newt_button_box_new(NMT_NEWT_BUTTON_BOX_HORIZONTAL);
     nmt_newt_grid_add(grid, widget, 0, 2);
     bbox = NMT_NEWT_BUTTON_BOX(widget);
diff --git a/src/nmtui/nmt-utils.c b/src/nmtui/nmt-utils.c
index 7a500542..90e3bacb 100644
--- a/src/nmtui/nmt-utils.c
+++ b/src/nmtui/nmt-utils.c
@@ -12,6 +12,8 @@
 
 #include "nmt-utils.h"
 
+#include "libnmt-newt/nmt-newt.h"
+
 /**
  * NmtSyncOp:
  *
@@ -128,3 +130,210 @@ nmt_sync_op_complete_pointer(NmtSyncOp *op, gpointer result, GError *error)
     real->error    = error ? g_error_copy(error) : NULL;
     real->complete = GUINT_TO_POINTER(TRUE);
 }
+
+/**
+ * nmt_utils_filter_match:
+ * @haystack: (nullable): the string to search in
+ * @needle: (nullable): the search term
+ *
+ * Case-insensitive UTF-8 substring test. An empty or %NULL @needle matches
+ * anything; a %NULL @haystack matches only an empty @needle.
+ *
+ * Returns: %TRUE if @haystack contains @needle.
+ */
+gboolean
+nmt_utils_filter_match(const char *haystack, const char *needle)
+{
+    gs_free char *h = NULL;
+    gs_free char *n = NULL;
+
+    if (!needle || !needle[0])
+        return TRUE;
+    if (!haystack)
+        return FALSE;
+
+    h = g_utf8_casefold(haystack, -1);
+    n = g_utf8_casefold(needle, -1);
+    return strstr(h, n) != NULL;
+}
+
+/*
+ * Renders the search state into @label: "Search:" while typing,
+ * "Matching '...' (N)" once a filter is applied with the entry hidden, or
+ * empty when idle.
+ *
+ * When the entry is hidden the text is padded to the width the row occupies
+ * while searching ("Search:" plus the entry), so revealing or hiding the entry
+ * never changes the form's width.
+ */
+static void
+set_search_label(NmtNewtLabel *label, const char *filter_text, int match_count, gboolean searching)
+{
+    gs_free char *body = NULL;
+    int           reserve, body_width;
+
+    if (searching) {
+        nmt_newt_label_set_text(label, _("Search:"));
+        return;
+    }
+
+    reserve = nmt_newt_text_width(_("Search:")) + 1 + NMT_SEARCH_ENTRY_WIDTH;
+
+    if (!nm_str_is_empty(filter_text)) {
+        gs_free char *shown = NULL;
+        int           overhead;
+
+        /* Echo the filter, but elide it so the confirmed label never exceeds
+         * the reserved width; otherwise the form grows when a long search is
+         * confirmed with Enter. */
+        body     = g_strdup_printf(_("Matching '%s' (%d)"), "", match_count);
+        overhead = nmt_newt_text_width(body);
+        nm_clear_g_free(&body);
+
+        shown = nmt_newt_text_truncate(filter_text, reserve - overhead);
+        body  = g_strdup_printf(_("Matching '%s' (%d)"), shown, match_count);
+    } else
+        body = g_strdup("");
+
+    body_width = nmt_newt_text_width(body);
+    if (body_width < reserve) {
+        gs_free char *padded = NULL;
+
+        padded = g_strdup_printf("%s%*s", body, reserve - body_width, "");
+        nmt_newt_label_set_text(label, padded);
+    } else
+        nmt_newt_label_set_text(label, body);
+}
+
+struct _NmtSearch {
+    NmtNewtEntry      *entry;
+    NmtNewtLabel      *label;
+    NmtNewtWidget     *focus;
+    NmtSearchApplyFunc apply;
+    NmtSearchCountFunc count;
+    gpointer           user_data;
+};
+
+void
+nmt_search_update_label(NmtSearch *search)
+{
+    set_search_label(search->label,
+                     nmt_newt_entry_get_text(search->entry),
+                     search->count(search->user_data),
+                     nmt_newt_widget_get_visible(NMT_NEWT_WIDGET(search->entry)));
+}
+
+static void
+search_text_changed(GObject *entry, GParamSpec *pspec, gpointer user_data)
+{
+    NmtSearch *search = user_data;
+
+    search->apply(search->user_data, nmt_newt_entry_get_text(search->entry));
+    nmt_search_update_label(search);
+}
+
+static void
+search_activated(NmtNewtWidget *entry, gpointer user_data)
+{
+    NmtSearch   *search = user_data;
+    NmtNewtForm *form   = nmt_newt_widget_get_form(search->focus);
+
+    /* Enter: hide the entry but keep the filter; the label now reports it. */
+    nmt_newt_widget_set_visible(NMT_NEWT_WIDGET(search->entry), FALSE);
+    nmt_search_update_label(search);
+    if (form)
+        nmt_newt_form_set_focus(form, search->focus);
+}
+
+static gboolean
+search_hotkey(NmtNewtForm *form, int key, gpointer user_data)
+{
+    NmtSearch  *search = user_data;
+    const char *filter = nmt_newt_entry_get_text(search->entry);
+
+    if (key == '/') {
+        nmt_newt_widget_set_visible(NMT_NEWT_WIDGET(search->entry), TRUE);
+        nmt_search_update_label(search);
+        nmt_newt_form_set_focus(form, NMT_NEWT_WIDGET(search->entry));
+        return TRUE;
+    }
+    if (key == NEWT_KEY_ESCAPE
+        && (nmt_newt_widget_get_visible(NMT_NEWT_WIDGET(search->entry))
+            || !nm_str_is_empty(filter))) {
+        /* Esc: clear the filter (via notify::text) and return to the list. */
+        nmt_newt_entry_set_text(search->entry, "");
+        nmt_newt_widget_set_visible(NMT_NEWT_WIDGET(search->entry), FALSE);
+        nmt_search_update_label(search);
+        nmt_newt_form_set_focus(form, search->focus);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+NmtSearch *
+nmt_search_new(NmtNewtEntry      *entry,
+               NmtNewtLabel      *label,
+               NmtNewtWidget     *focus,
+               NmtSearchApplyFunc apply,
+               NmtSearchCountFunc count,
+               gpointer           user_data)
+{
+    NmtSearch *search = g_new0(NmtSearch, 1);
+
+    search->entry     = entry;
+    search->label     = label;
+    search->focus     = focus;
+    search->apply     = apply;
+    search->count     = count;
+    search->user_data = user_data;
+
+    nmt_newt_widget_set_visible(NMT_NEWT_WIDGET(entry), FALSE);
+    g_signal_connect(entry, "notify::text", G_CALLBACK(search_text_changed), search);
+    g_signal_connect(entry, "activated", G_CALLBACK(search_activated), search);
+
+    return search;
+}
+
+void
+nmt_search_bind_form(NmtSearch *search, NmtNewtForm *form)
+{
+    g_signal_connect(form, "hotkey", G_CALLBACK(search_hotkey), search);
+    nmt_newt_form_add_hotkey(form, '/');
+    nmt_newt_form_set_stable_width(form);
+
+    /* Reserve the search row's width up front so the form does not grow when
+     * the entry is first revealed. */
+    nmt_search_update_label(search);
+}
+
+static void
+get_secrets_cb(GObject *object, GAsyncResult *result, gpointer op)
+{
+    GVariant *secrets;
+    GError   *error = NULL;
+
+    secrets = nm_remote_connection_get_secrets_finish(NM_REMOTE_CONNECTION(object), result, &error);
+    nmt_sync_op_complete_pointer(op, secrets, error);
+    g_clear_error(&error);
+}
+
+/**
+ * nmt_sync_get_secrets:
+ * @connection: the #NMRemoteConnection to fetch secrets from
+ * @setting_name: the setting to fetch secrets for
+ * @error: return location for a #GError
+ *
+ * Synchronously requests @setting_name's secrets for @connection, running the
+ * main loop until the request completes.
+ *
+ * Returns: (transfer full): the secrets variant, or %NULL on error.
+ */
+GVariant *
+nmt_sync_get_secrets(NMRemoteConnection *connection, const char *setting_name, GError **error)
+{
+    NmtSyncOp op;
+
+    nmt_sync_op_init(&op);
+    nm_remote_connection_get_secrets_async(connection, setting_name, NULL, get_secrets_cb, &op);
+    return nmt_sync_op_wait_pointer(&op, error);
+}
diff --git a/src/nmtui/nmt-utils.h b/src/nmtui/nmt-utils.h
index 3b780ae7..79058c0d 100644
--- a/src/nmtui/nmt-utils.h
+++ b/src/nmtui/nmt-utils.h
@@ -6,6 +6,8 @@
 #ifndef NMT_UTILS_H
 #define NMT_UTILS_H
 
+#include "libnmt-newt/nmt-newt-types.h"
+
 typedef struct {
     gpointer private[3];
 } NmtSyncOp;
@@ -18,4 +20,37 @@ void     nmt_sync_op_complete_boolean(NmtSyncOp *op, gboolean result, GError *er
 gpointer nmt_sync_op_wait_pointer(NmtSyncOp *op, GError **error);
 void     nmt_sync_op_complete_pointer(NmtSyncOp *op, gpointer result, GError *error);
 
+gboolean nmt_utils_filter_match(const char *haystack, const char *needle);
+
+#define NMT_SEARCH_ENTRY_WIDTH 34
+
+/**
+ * NmtSearch:
+ *
+ * Vim-style '/' search glue shared by the connection lists. The caller builds
+ * the @label and @entry into its own layout and hands them over; NmtSearch
+ * wires '/' to reveal the entry, Enter to confirm, Esc to clear, keeps the
+ * status label in sync, and pins the form width so the list does not jump.
+ *
+ * @apply is invoked with the current text whenever the filter changes; @count
+ * returns the live match count for the label.
+ */
+typedef struct _NmtSearch NmtSearch;
+
+typedef void (*NmtSearchApplyFunc)(gpointer user_data, const char *text);
+typedef int (*NmtSearchCountFunc)(gpointer user_data);
+
+NmtSearch *nmt_search_new(NmtNewtEntry      *entry,
+                          NmtNewtLabel      *label,
+                          NmtNewtWidget     *focus,
+                          NmtSearchApplyFunc apply,
+                          NmtSearchCountFunc count,
+                          gpointer           user_data);
+
+void nmt_search_bind_form(NmtSearch *search, NmtNewtForm *form);
+void nmt_search_update_label(NmtSearch *search);
+
+GVariant *
+nmt_sync_get_secrets(NMRemoteConnection *connection, const char *setting_name, GError **error);
+
 #endif /* NMT_UTILS_H */
diff --git a/src/nmtui/nmt-widget-list.c b/src/nmtui/nmt-widget-list.c
index d150eee0..c827db04 100644
--- a/src/nmtui/nmt-widget-list.c
+++ b/src/nmtui/nmt-widget-list.c
@@ -11,7 +11,7 @@
  * buttons next to each one, and an "Add" button at the button to add
  * new ones.
  *
- * It is the base class for #NmtAddressList, and is used internally by
+ * It is the base class for #NmtList, and is used internally by
  * #NmtRouteTable.
  *
  * FIXME: The way this works is sort of weird.
diff --git a/src/nmtui/nmt-wifi-qr-dialog.c b/src/nmtui/nmt-wifi-qr-dialog.c
new file mode 100644
index 00000000..d8e7711f
--- /dev/null
+++ b/src/nmtui/nmt-wifi-qr-dialog.c
@@ -0,0 +1,165 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026 Red Hat, Inc.
+ */
+
+/**
+ * SECTION:nmt-wifi-qr-dialog
+ * @short_description: A dialog that shares a Wi-Fi connection as a QR code
+ *
+ * Shows the "WIFI:" provisioning URI of a saved Wi-Fi connection as a
+ * scannable QR code, along with the SSID and password. If the password is a
+ * passphrase that cannot be read, it shows a warning instead of a QR code.
+ */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "nmt-wifi-qr-dialog.h"
+
+#include "libnm-glib-aux/nm-secret-utils.h"
+#include "libnmc-base/nm-client-utils.h"
+#include "nmt-utils.h"
+
+static NMConnection *
+clone_with_secrets(NMConnection *connection)
+{
+    NMConnection              *clone   = nm_simple_connection_new_clone(connection);
+    gs_unref_variant GVariant *secrets = NULL;
+
+    if (!NM_IS_REMOTE_CONNECTION(connection))
+        return clone;
+
+    secrets = nmt_sync_get_secrets(NM_REMOTE_CONNECTION(connection),
+                                   NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                                   NULL);
+    if (secrets)
+        nm_connection_update_secrets(clone,
+                                     NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                                     secrets,
+                                     NULL);
+
+    return clone;
+}
+
+static gboolean
+qr_fits_screen(const char *qr)
+{
+    gs_strfreev char **lines = NULL;
+    int                screen_w;
+    int                screen_h;
+    int                qr_rows;
+    int                qr_cols;
+
+    newtGetScreenSize(&screen_w, &screen_h);
+
+    lines   = g_strsplit(qr, "\n", -1);
+    qr_rows = g_strv_length(lines);
+    qr_cols = lines[0] ? (int) g_utf8_strlen(lines[0], -1) : 0;
+
+    /* Reserve room for the form border, title, the SSID/password labels and
+     * the button row. */
+    return qr_cols + 4 <= screen_w && qr_rows + 8 <= screen_h;
+}
+
+/**
+ * nmt_wifi_qr_dialog_run:
+ * @connection: a Wi-Fi #NMConnection
+ *
+ * Fetches @connection's secrets and shows a modal dialog with a QR code that
+ * encodes its Wi-Fi credentials, or a warning if the password cannot be read.
+ * Returns when the user closes the dialog.
+ */
+void
+nmt_wifi_qr_dialog_run(NMConnection *connection)
+{
+    gs_unref_object NMConnection *clone = NULL;
+    gs_unref_object NmtNewtForm  *form  = NULL;
+    NMSettingWireless            *s_wireless;
+    NMSettingWirelessSecurity    *s_wsec;
+    NmtNewtGrid                  *grid;
+    NmtNewtWidget                *widget;
+    NmtNewtButtonBox             *bbox;
+    GBytes                       *ssid_bytes;
+    gs_free char                 *ssid     = NULL;
+    nm_auto_free_secret char     *uri      = NULL;
+    nm_auto_free_secret char     *qr       = NULL;
+    const char                   *key_mgmt = NULL;
+    const char                   *psk      = NULL;
+    gboolean                      no_password;
+    int                           row = 0;
+
+    g_return_if_fail(NM_IS_CONNECTION(connection));
+
+    if (!nm_connection_get_setting_wireless(connection))
+        return;
+
+    clone      = clone_with_secrets(connection);
+    s_wireless = nm_connection_get_setting_wireless(clone);
+
+    ssid_bytes = nm_setting_wireless_get_ssid(s_wireless);
+    if (ssid_bytes)
+        ssid =
+            nm_utils_ssid_to_utf8(g_bytes_get_data(ssid_bytes, NULL), g_bytes_get_size(ssid_bytes));
+
+    s_wsec = nm_connection_get_setting_wireless_security(clone);
+    if (s_wsec) {
+        key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wsec);
+        psk      = nm_setting_wireless_security_get_psk(s_wsec);
+    }
+
+    /* A QR code for a secured network without its password connects to nothing,
+     * so don't render one; show a warning instead. */
+    no_password = nmc_wifi_key_mgmt_uses_psk(key_mgmt) && (!psk || !psk[0]);
+
+    if (!no_password) {
+        uri = nmc_wifi_qr_uri_new(ssid, key_mgmt, psk, nm_setting_wireless_get_hidden(s_wireless));
+        qr  = nmc_wifi_qr_render_string(uri);
+    }
+
+    form = nmt_newt_form_new(_("Share Wi-Fi"));
+
+    widget = nmt_newt_grid_new();
+    nmt_newt_form_set_content(form, widget);
+    grid = NMT_NEWT_GRID(widget);
+
+    if (no_password) {
+        widget = nmt_newt_textbox_new(0, 50);
+        nmt_newt_textbox_set_text(
+            NMT_NEWT_TEXTBOX(widget),
+            _("Warning: cannot read the Wi-Fi password due to insufficient privileges."));
+    } else if (qr && qr_fits_screen(qr)) {
+        widget = nmt_newt_textbox_new(NMT_NEWT_TEXTBOX_SET_BACKGROUND, 0);
+        nmt_newt_textbox_set_text(NMT_NEWT_TEXTBOX(widget), qr);
+    } else {
+        widget = nmt_newt_textbox_new(0, 50);
+        nmt_newt_textbox_set_text(NMT_NEWT_TEXTBOX(widget),
+                                  qr ? _("The terminal is too small to display the QR code.")
+                                     : _("The Wi-Fi credentials could not be encoded."));
+    }
+    nmt_newt_grid_add(grid, widget, 0, row++);
+    nmt_newt_widget_set_padding(widget, 0, 0, 0, 1);
+
+    if (ssid) {
+        gs_free char *label = g_strdup_printf("%s: %s", _("SSID"), ssid);
+
+        widget = nmt_newt_label_new(label);
+        nmt_newt_grid_add(grid, widget, 0, row++);
+    }
+
+    if (psk) {
+        nm_auto_free_secret char *label = g_strdup_printf("%s: %s", _("Password"), psk);
+
+        widget = nmt_newt_label_new(label);
+        nmt_newt_grid_add(grid, widget, 0, row++);
+    }
+
+    widget = nmt_newt_button_box_new(NMT_NEWT_BUTTON_BOX_HORIZONTAL);
+    nmt_newt_grid_add(grid, widget, 0, row++);
+    nmt_newt_widget_set_padding(widget, 0, 1, 0, 0);
+    bbox = NMT_NEWT_BUTTON_BOX(widget);
+
+    widget = nmt_newt_button_box_add_end(bbox, _("Close"));
+    nmt_newt_widget_set_exit_on_activate(widget, TRUE);
+
+    nmt_newt_form_run_sync(form);
+}
diff --git a/src/nmtui/nmt-wifi-qr-dialog.h b/src/nmtui/nmt-wifi-qr-dialog.h
new file mode 100644
index 00000000..310bb7c8
--- /dev/null
+++ b/src/nmtui/nmt-wifi-qr-dialog.h
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026 Red Hat, Inc.
+ */
+
+#ifndef NMT_WIFI_QR_DIALOG_H
+#define NMT_WIFI_QR_DIALOG_H
+
+#include "libnmt-newt/nmt-newt.h"
+
+void nmt_wifi_qr_dialog_run(NMConnection *connection);
+
+#endif /* NMT_WIFI_QR_DIALOG_H */
diff --git a/src/nmtui/nmt-wireguard-peer-list.c b/src/nmtui/nmt-wireguard-peer-list.c
index c6db61a6..38987a97 100644
--- a/src/nmtui/nmt-wireguard-peer-list.c
+++ b/src/nmtui/nmt-wireguard-peer-list.c
@@ -170,6 +170,7 @@ nmt_wireguard_peer_list_add_peer(NmtWireguardPeerList *list)
     NmtNewtForm                 *editor;
 
     editor = nmt_wireguard_peer_editor_new(priv->setting, peer);
+    nm_wireguard_peer_unref(peer);
 
     if (!editor)
         return;
diff --git a/src/nmtui/nmtui-connect.c b/src/nmtui/nmtui-connect.c
index 4f90adf3..3fcf527b 100644
--- a/src/nmtui/nmtui-connect.c
+++ b/src/nmtui/nmtui-connect.c
@@ -342,16 +342,268 @@ listbox_active_changed(GObject *object, GParamSpec *pspec, gpointer button)
     }
 }
 
+/* Contains both the UI and batch data for wifi-rescans */
+typedef struct {
+    NmtNewtWidget *activate_button;
+    NmtNewtWidget *listbox;
+    NmtNewtForm   *rescan_form;
+    NmtNewtWidget *rescan_button;
+    int            pending;
+    GCancellable  *cancellable;
+    gboolean       cancelled;
+    gboolean       has_error;
+    gboolean       timeout_occurred;
+} RescanBatch;
+
+typedef struct {
+    NMDeviceWifi *wifi_device;
+    GSource      *timeout_id;
+    gulong        signal_id;
+    RescanBatch  *batch;
+} RescanData;
+
+static GPtrArray *
+active_wifi_devices(void)
+{
+    const GPtrArray *devices;
+    GPtrArray       *active_wifi_devices;
+
+    devices = nm_client_get_devices(nm_client);
+
+    if (!devices)
+        return NULL;
+
+    active_wifi_devices = g_ptr_array_new_with_free_func(g_object_unref);
+
+    for (guint i = 0; i < devices->len; i++) {
+        NMDevice *dev = g_ptr_array_index((GPtrArray *) devices, i);
+
+        if (!NM_IS_DEVICE_WIFI(dev))
+            continue;
+        if (NM_IN_SET(nm_device_get_state(dev),
+                      NM_DEVICE_STATE_UNAVAILABLE,
+                      NM_DEVICE_STATE_UNMANAGED,
+                      NM_DEVICE_STATE_FAILED))
+            continue;
+
+        g_ptr_array_add(active_wifi_devices, g_object_ref(dev));
+    }
+    return active_wifi_devices;
+}
+
+static void
+on_rescan_cancel(NmtNewtForm *form, gpointer user_data)
+{
+    RescanBatch *batch_data = user_data;
+
+    if (!batch_data || batch_data->cancelled)
+        return;
+
+    batch_data->cancelled = TRUE;
+
+    /* Cancel the async operations using nm_clear_g_cancellable */
+    if (batch_data->cancellable)
+        nm_clear_g_cancellable(&batch_data->cancellable);
+
+    g_object_unref(batch_data->rescan_form);
+    batch_data->rescan_form = NULL;
+}
+
+/* creates the wifi-rescan form and manages other ui properties while rescanning */
+static void
+wifi_rescan_form(RescanBatch *batch_data)
+{
+    NmtNewtForm   *rescan_form;
+    NmtNewtWidget *label;
+
+    nmt_newt_component_set_sensitive(NMT_NEWT_COMPONENT(batch_data->listbox), FALSE);
+    nmt_newt_component_set_sensitive(NMT_NEWT_COMPONENT(batch_data->rescan_button), FALSE);
+    nmt_newt_component_set_sensitive(NMT_NEWT_COMPONENT(batch_data->activate_button), FALSE);
+
+    /* open the scanning form*/
+    rescan_form = g_object_new(NMT_TYPE_NEWT_FORM, NULL);
+    label       = nmt_newt_label_new(_("Rescanning Wi-Fi devices..."));
+    nmt_newt_form_set_content(rescan_form, label);
+
+    /* connect the quit signal to rescan cancel */
+    g_signal_connect(rescan_form, "quit", G_CALLBACK(on_rescan_cancel), batch_data);
+
+    nmt_newt_form_show(rescan_form);
+    batch_data->rescan_form = rescan_form;
+}
+
+/* rebuilds the connection list and changes back the
+ * state for various UI components which were
+ * changed during rescanning */
+static void
+on_rescan_complete(gpointer data)
+{
+    NmtConnectConnectionList *list;
+    RescanData               *rescan_data;
+    RescanBatch              *batch_data;
+    NMDeviceWifi             *wifi_device;
+
+    rescan_data = data;
+    batch_data  = rescan_data->batch;
+    wifi_device = rescan_data->wifi_device;
+
+    g_free(rescan_data);
+    rescan_data = NULL;
+
+    if (batch_data->has_error) {
+        nmt_newt_message_dialog(_("Wi-Fi scan failed for device : %s"),
+                                nm_device_get_iface(NM_DEVICE(wifi_device)));
+        batch_data->has_error = FALSE;
+    }
+
+    if (batch_data->timeout_occurred) {
+        nmt_newt_message_dialog(_("Wi-Fi scan timed out for device : %s"),
+                                nm_device_get_iface(NM_DEVICE(wifi_device)));
+        batch_data->timeout_occurred = FALSE;
+    }
+
+    /* If the scans are not complete then simply wait for them to complete
+     * This also ensures that UI is only enabled either after all
+     * scans complete (Either complete or cancel ). Preventing any
+     * overlapping scans caused due to the user pressing rescan multiple times
+     */
+
+    if (--batch_data->pending > 0)
+        return;
+
+    /* If the scan is not cancelled quit normally */
+    if (!batch_data->cancelled) {
+        if (nmt_newt_widget_get_realized(NMT_NEWT_WIDGET(batch_data->rescan_form))) {
+            nmt_newt_form_quit(batch_data->rescan_form);
+            nm_clear_g_object(&batch_data->rescan_form);
+            batch_data->rescan_form = NULL;
+        }
+        list = NMT_CONNECT_CONNECTION_LIST(batch_data->listbox);
+        nmt_newt_listbox_clear(NMT_NEWT_LISTBOX(list));
+
+        g_object_notify(G_OBJECT(nm_client), NM_CLIENT_CONNECTIONS);
+    }
+
+    /* The following cleanup  is required regardless of any cancellation */
+    nmt_newt_component_set_sensitive(NMT_NEWT_COMPONENT(batch_data->listbox), TRUE);
+    nmt_newt_component_set_sensitive(NMT_NEWT_COMPONENT(batch_data->rescan_button), TRUE);
+    nmt_newt_component_set_sensitive(NMT_NEWT_COMPONENT(batch_data->activate_button), TRUE);
+
+    g_clear_object(&batch_data->cancellable);
+}
+
+static gboolean
+scan_timeout_callback(gpointer user_data)
+{
+    RescanData *data = user_data;
+
+    nm_clear_g_source_inst(&data->timeout_id);
+
+    /* Timeout reached - scan took too long */
+    if (data->signal_id) {
+        g_signal_handler_disconnect(data->wifi_device, data->signal_id);
+        data->signal_id = 0;
+    }
+
+    data->batch->timeout_occurred = TRUE;
+
+    on_rescan_complete(data);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+on_last_scan_changed(GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+    RescanData *data = user_data;
+
+    /* Scan completed successfully */
+    nm_clear_g_source_inst(&data->timeout_id);
+    if (data->signal_id) {
+        g_signal_handler_disconnect(data->wifi_device, data->signal_id);
+        data->signal_id = 0;
+    }
+
+    on_rescan_complete(data);
+}
+
+static void
+wifi_rescan_callback(GObject *source_object, GAsyncResult *result, gpointer rescan_data)
+{
+    RescanData *data = rescan_data;
+
+    if (!nm_device_wifi_request_scan_finish(data->wifi_device, result, NULL)) {
+        data->batch->has_error = TRUE;
+        on_rescan_complete(data);
+        return;
+    }
+
+    /* Listen for last-scan property changes */
+    data->signal_id = g_signal_connect(data->wifi_device,
+                                       "notify::last-scan",
+                                       G_CALLBACK(on_last_scan_changed),
+                                       data);
+
+    /* Set a 10-second timeout in case scan doesn't complete */
+    data->timeout_id = nm_g_timeout_add_source(RESCAN_TIMEOUT_MS, scan_timeout_callback, data);
+}
+
+static void
+wifi_rescan(NmtNewtButton *button, gpointer data_batch)
+{
+    gs_unref_ptrarray GPtrArray *devices = active_wifi_devices();
+    RescanData                  *data;
+    RescanBatch                 *batch_data = data_batch;
+
+    if (!devices || devices->len == 0) {
+        nmt_newt_message_dialog(_("No active Wi-Fi devices found"));
+        return;
+    }
+
+    /* create a shared batch for all the devices*/
+    batch_data->pending     = devices->len;
+    batch_data->cancelled   = FALSE;
+    batch_data->cancellable = g_cancellable_new();
+
+    wifi_rescan_form(batch_data);
+
+    for (guint i = 0; i < devices->len; i++) {
+        NMDevice *dev = g_ptr_array_index(devices, i);
+
+        /* per-device data */
+        data              = g_new0(RescanData, 1);
+        data->wifi_device = NM_DEVICE_WIFI(dev);
+        data->batch       = batch_data;
+
+        nm_device_wifi_request_scan_async(NM_DEVICE_WIFI(dev),
+                                          batch_data->cancellable,
+                                          wifi_rescan_callback,
+                                          data);
+    }
+}
+
+static void
+connect_search_apply(gpointer list, const char *text)
+{
+    nmt_connect_connection_list_set_filter_text(NMT_CONNECT_CONNECTION_LIST(list), text);
+}
+
+static int
+connect_search_count(gpointer list)
+{
+    return nmt_connect_connection_list_get_match_count(NMT_CONNECT_CONNECTION_LIST(list));
+}
+
 static NmtNewtForm *
 nmt_connect_connection_list(gboolean is_top)
 {
-    int            screen_width, screen_height;
-    NmtNewtForm   *form;
-    NmtNewtWidget *list, *activate, *quit, *bbox, *grid;
+    NmtNewtForm                 *form;
+    NmtNewtWidget               *list, *activate, *quit, *bbox, *grid, *rescan;
+    NmtNewtWidget               *search_row, *search_label, *search_entry;
+    NmtSearch                   *search;
+    RescanBatch                 *batch_data;
+    gs_unref_ptrarray GPtrArray *all_active_wifi_devices = NULL;
 
-    newtGetScreenSize(&screen_width, &screen_height);
-
-    form = g_object_new(NMT_TYPE_NEWT_FORM, "y", 2, "height", screen_height - 4, NULL);
+    form = g_object_new(NMT_TYPE_NEWT_FORM, "fullscreen-vertical", TRUE, NULL);
 
     grid = nmt_newt_grid_new();
 
@@ -372,9 +624,50 @@ nmt_connect_connection_list(gboolean is_top)
     listbox_active_changed(G_OBJECT(list), NULL, activate);
     g_signal_connect(activate, "clicked", G_CALLBACK(activate_clicked), list);
 
+    all_active_wifi_devices = active_wifi_devices();
+    if (all_active_wifi_devices && all_active_wifi_devices->len > 0) {
+        rescan = nmt_newt_button_box_add_start(NMT_NEWT_BUTTON_BOX(bbox), _("Rescan Wi-Fi"));
+
+        batch_data                  = g_new0(RescanBatch, 1);
+        batch_data->activate_button = activate;
+        batch_data->listbox         = list;
+        batch_data->rescan_button   = rescan;
+
+        /* Bind the lifecycle of batch_data to the rescan button.
+         * The data will be freed automatically when the button is destroyed i.e form quits. */
+        g_object_set_data_full(G_OBJECT(rescan), "rescan-batch-data", batch_data, g_free);
+
+        g_signal_connect(rescan, "clicked", G_CALLBACK(wifi_rescan), batch_data);
+    }
+
     quit = nmt_newt_button_box_add_end(NMT_NEWT_BUTTON_BOX(bbox), is_top ? _("Quit") : _("Back"));
     nmt_newt_widget_set_exit_on_activate(quit, TRUE);
 
+    /* Search row below the list. The row is always present so revealing the
+     * entry does not resize the form; vim-style '/' shows the entry, and once a
+     * filter is applied the label reports it ("Matching '...' (N)"). */
+    search_row = nmt_newt_grid_new();
+    nmt_newt_grid_add(NMT_NEWT_GRID(grid), search_row, 0, 1);
+    nmt_newt_grid_set_flags(NMT_NEWT_GRID(grid),
+                            search_row,
+                            NMT_NEWT_GRID_FILL_X | NMT_NEWT_GRID_EXPAND_X);
+
+    search_label = nmt_newt_label_new("");
+    nmt_newt_grid_add(NMT_NEWT_GRID(search_row), search_label, 0, 0);
+
+    search_entry = nmt_newt_entry_new(NMT_SEARCH_ENTRY_WIDTH, 0);
+    nmt_newt_grid_add(NMT_NEWT_GRID(search_row), search_entry, 1, 0);
+    nmt_newt_widget_set_padding(search_entry, 1, 0, 0, 0);
+
+    search = nmt_search_new(NMT_NEWT_ENTRY(search_entry),
+                            NMT_NEWT_LABEL(search_label),
+                            list,
+                            connect_search_apply,
+                            connect_search_count,
+                            list);
+    nmt_search_bind_form(search, form);
+    g_object_set_data_full(G_OBJECT(form), "search-data", search, g_free);
+
     nmt_newt_form_set_content(form, grid);
     return form;
 }
diff --git a/src/nmtui/nmtui-connect.h b/src/nmtui/nmtui-connect.h
index 811893e8..3e42714b 100644
--- a/src/nmtui/nmtui-connect.h
+++ b/src/nmtui/nmtui-connect.h
@@ -6,6 +6,8 @@
 #ifndef NMTUI_CONNECT_H
 #define NMTUI_CONNECT_H
 
+#define RESCAN_TIMEOUT_MS 10000
+
 NmtNewtForm *nmtui_connect(gboolean is_top, int argc, char **argv);
 
 #endif /* NMTUI_CONNECT_H */
diff --git a/src/nmtui/nmtui-edit.c b/src/nmtui/nmtui-edit.c
index 0ba3bd0d..dcdce568 100644
--- a/src/nmtui/nmtui-edit.c
+++ b/src/nmtui/nmtui-edit.c
@@ -19,6 +19,7 @@
 #include "nmt-edit-connection-list.h"
 #include "nmt-editor.h"
 #include "nmt-utils.h"
+#include "nmt-wifi-qr-dialog.h"
 
 #include "nm-editor-utils.h"
 
@@ -43,6 +44,13 @@ list_remove_connection(NmtEditConnectionList *list, NMRemoteConnection *connecti
     nmt_newt_form_set_focus(form, NMT_NEWT_WIDGET(list));
 }
 
+static void
+list_share_connection(NmtEditConnectionList *list, NMConnection *connection, gpointer form)
+{
+    nmt_wifi_qr_dialog_run(connection);
+    nmt_newt_form_set_focus(form, NMT_NEWT_WIDGET(list));
+}
+
 static gboolean
 edit_connection_list_filter(NmtEditConnectionList *list,
                             NMConnection          *connection,
@@ -85,13 +93,10 @@ edit_connection_list_filter(NmtEditConnectionList *list,
 static NmtNewtForm *
 nmt_edit_main_connection_list(gboolean is_top)
 {
-    int            screen_width, screen_height;
     NmtNewtForm   *form;
     NmtNewtWidget *quit, *list;
 
-    newtGetScreenSize(&screen_width, &screen_height);
-
-    form = g_object_new(NMT_TYPE_NEWT_FORM, "y", 2, "height", screen_height - 4, NULL);
+    form = g_object_new(NMT_TYPE_NEWT_FORM, "fullscreen-vertical", TRUE, NULL);
 
     quit = nmt_newt_button_new(is_top ? _("Quit") : _("Back"));
     nmt_newt_widget_set_exit_on_activate(quit, TRUE);
@@ -106,6 +111,9 @@ nmt_edit_main_connection_list(gboolean is_top)
     g_signal_connect(list, "add-connection", G_CALLBACK(list_add_connection), form);
     g_signal_connect(list, "edit-connection", G_CALLBACK(list_edit_connection), form);
     g_signal_connect(list, "remove-connection", G_CALLBACK(list_remove_connection), form);
+    g_signal_connect(list, "share-connection", G_CALLBACK(list_share_connection), form);
+
+    nmt_edit_connection_list_bind_search(NMT_EDIT_CONNECTION_LIST(list), form);
 
     nmt_newt_form_set_content(form, list);
     return form;
diff --git a/src/nmtui/nmtui.c b/src/nmtui/nmtui.c
index 9927e40a..0838187a 100644
--- a/src/nmtui/nmtui.c
+++ b/src/nmtui/nmtui.c
@@ -75,12 +75,11 @@ main_list_activated(NmtNewtWidget *widget, NmtNewtListbox *listbox)
 static NmtNewtForm *
 nmtui_main(gboolean is_top, int argc, char **argv)
 {
-    NmtNewtForm      *form;
-    NmtNewtWidget    *widget, *ok;
-    NmtNewtGrid      *grid;
-    NmtNewtListbox   *listbox;
-    NmtNewtButtonBox *bbox;
-    int               i;
+    NmtNewtForm    *form;
+    NmtNewtWidget  *widget;
+    NmtNewtGrid    *grid;
+    NmtNewtListbox *listbox;
+    int             i;
 
     form = g_object_new(NMT_TYPE_NEWT_FORM, "title", _("NetworkManager TUI"), NULL);
 
@@ -108,13 +107,6 @@ nmtui_main(gboolean is_top, int argc, char **argv)
     nmt_newt_listbox_append(listbox, "", NULL);
     nmt_newt_listbox_append(listbox, _("Quit"), quit_func);
 
-    widget = nmt_newt_button_box_new(NMT_NEWT_BUTTON_BOX_HORIZONTAL);
-    nmt_newt_grid_add(grid, widget, 0, 2);
-    bbox = NMT_NEWT_BUTTON_BOX(widget);
-
-    ok = nmt_newt_button_box_add_end(bbox, _("OK"));
-    g_signal_connect(ok, "activated", G_CALLBACK(main_list_activated), listbox);
-
     toplevel_form = form;
 
     return form;