nmtui: use select button to select available devices

Since it is error prone to manually type in interface names to match existing
ones, we introduce a select button that allows a user to chose from a list of devices.

- Show "Select..." button for physical devices to choose from available
  devices in a popup dialog.
- devices are sorted in alphabetical order.
- Only for physical devices (ethernet, infiniband, wifi, etc)

Resolves: https://issues.redhat.com/browse/RHEL-129186
This commit is contained in:
Rahul Rajesh 2026-01-13 16:23:03 -05:00
parent 574411b8a5
commit e10fac49bb
8 changed files with 183 additions and 24 deletions

3
NEWS
View file

@ -37,6 +37,9 @@ USE AT YOUR OWN RISK. NOT RECOMMENDED FOR PRODUCTION USE!
* The "band" property of Wi-fi connections now accepts the "6GHz"
value.
* Show the Wi-Fi band of APs in the scan results from nmcli.
* New <Select...> button in nmtui that allows users to chose from list of
available devices when creating connection profiles for physical interfaces
(Ethernet, Wi-Fi, etc.).
=============================================
NetworkManager-1.56

View file

@ -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);
}
@ -333,6 +334,133 @@ entry_text_changed(GObject *object, GParamSpec *pspec, gpointer deventry)
g_free(mac);
}
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)
{
@ -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));
}

View file

@ -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,

View file

@ -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);
}
}

View file

@ -310,6 +310,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 +334,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 +353,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,

View file

@ -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,

View file

@ -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,

View file

@ -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,