Merge branch 'jp/nmtui-connection-search'

This commit is contained in:
Josephine Pfeiffer 2026-06-17 12:38:23 +02:00
commit 59b59c7046
No known key found for this signature in database
GPG key ID: ABD48F465F4434BD
14 changed files with 518 additions and 8 deletions

2
NEWS
View file

@ -71,6 +71,8 @@ USE AT YOUR OWN RISK. NOT RECOMMENDED FOR PRODUCTION USE!
secrets when activating a connection, matching the connection editor.
* Fix an out-of-bounds read in the internal DHCPv4 client that an on-link
attacker could trigger with a malformed UDP packet, crashing NetworkManager.
* The nmtui connection lists ("nmtui connect" and "nmtui edit") support a
vim-style "/" search that filters the list to matching entries as you type.
=============================================
NetworkManager-1.56

View file

@ -199,6 +199,7 @@ src/nmtui/nmt-password-fields.c
src/nmtui/nmt-port-list.c
src/nmtui/nmt-route-editor.c
src/nmtui/nmt-route-table.c
src/nmtui/nmt-utils.c
src/nmtui/nmt-widget-list.c
src/nmtui/nmt-wireguard-peer-editor.c
src/nmtui/nmt-wireguard-peer-list.c

View file

@ -31,13 +31,16 @@ typedef struct {
NmtNewtWidget *content;
guint x, y, width, height;
guint form_width_max;
guint padding;
gboolean fixed_x, fixed_y;
gboolean fixed_width, fixed_height;
gboolean stable_width;
char *title_lc;
gboolean dirty;
NmtNewtWidget *focus;
GArray *hotkeys;
#ifdef HAVE_NEWTFORMGETSCROLLPOSITION
int scroll_position = 0;
#endif
@ -60,6 +63,7 @@ enum {
enum {
QUIT,
HOTKEY,
LAST_SIGNAL
};
@ -118,6 +122,7 @@ nmt_newt_form_finalize(GObject *object)
g_free(priv->title_lc);
g_clear_object(&priv->focus);
nm_clear_pointer(&priv->hotkeys, g_array_unref);
G_OBJECT_CLASS(nmt_newt_form_parent_class)->finalize(object);
}
@ -181,6 +186,15 @@ nmt_newt_form_build(NmtNewtForm *form)
nmt_newt_widget_size_request(priv->content, &form_width, &form_height);
newtGetScreenSize(&screen_width, &screen_height);
if (priv->stable_width) {
/* Never let the form get narrower across rebuilds. Otherwise content
* that shrinks (eg, a filtered list) makes the window recenter and
* "jump". Only forms that opt in get this; see
* nmt_newt_form_set_stable_width(). */
priv->form_width_max = NM_MAX(priv->form_width_max, (guint) NM_MAX(form_width, 0));
form_width = priv->form_width_max;
}
if (!priv->fixed_width)
priv->width = NM_MIN(form_width + 2 * ((gint64) priv->padding), screen_width - 2);
if (!priv->fixed_height)
@ -211,6 +225,12 @@ nmt_newt_form_build(NmtNewtForm *form)
priv->form = newtForm(NULL, NULL, NEWT_FLAG_NOF12);
newtFormAddHotKey(priv->form, NEWT_KEY_ESCAPE);
if (priv->hotkeys) {
guint h;
for (h = 0; h < priv->hotkeys->len; h++)
newtFormAddHotKey(priv->form, nm_g_array_index(priv->hotkeys, int, h));
}
cos = nmt_newt_widget_get_components(priv->content);
for (i = 0; cos[i]; i++)
@ -270,6 +290,17 @@ nmt_newt_form_iterate(NmtNewtForm *form)
newtFormSetTimer(priv->form, 1);
newtFormRun(priv->form, &es);
if (es.reason == NEWT_EXIT_HOTKEY) {
gboolean handled = FALSE;
/* Give listeners (eg, the connection-list search) a chance to consume the
* key. Esc is included, so a listener can make it cancel a transient mode
* instead of closing the form. */
g_signal_emit(form, signals[HOTKEY], 0, (int) es.u.key, &handled);
if (handled)
return;
}
if (es.reason == NEWT_EXIT_HOTKEY || es.reason == NEWT_EXIT_ERROR) {
/* The user hit Esc or there was an error. */
g_clear_object(&priv->focus);
@ -434,6 +465,43 @@ nmt_newt_form_set_focus(NmtNewtForm *form, NmtNewtWidget *widget)
g_object_ref(priv->focus);
}
/**
* nmt_newt_form_add_hotkey:
* @form: an #NmtNewtForm
* @key: a key code (eg, a character, or an %NEWT_KEY_ value)
*
* Registers @key as a hotkey on @form. When the user presses it, the
* #NmtNewtForm::hotkey signal is emitted; a handler returning %TRUE consumes
* the key, otherwise the form's default handling applies.
*/
void
nmt_newt_form_add_hotkey(NmtNewtForm *form, int key)
{
NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(form);
if (!priv->hotkeys)
priv->hotkeys = g_array_new(FALSE, FALSE, sizeof(int));
g_array_append_val(priv->hotkeys, key);
if (priv->form)
newtFormAddHotKey(priv->form, key);
}
/**
* nmt_newt_form_set_stable_width:
* @form: an #NmtNewtForm
*
* Stops @form from getting narrower across rebuilds: its width grows to fit
* content but never shrinks. Use for forms whose content shrinks in place (eg,
* a list filtered by a search box) where recentering would make the window
* "jump".
*/
void
nmt_newt_form_set_stable_width(NmtNewtForm *form)
{
NMT_NEWT_FORM_GET_PRIVATE(form)->stable_width = TRUE;
}
static void
nmt_newt_form_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
@ -579,6 +647,28 @@ nmt_newt_form_class_init(NmtNewtFormClass *form_class)
G_TYPE_NONE,
0);
/**
* NmtNewtForm::hotkey:
* @form: the #NmtNewtForm
* @key: the key that was pressed
*
* Emitted when a key registered via nmt_newt_form_add_hotkey() (or Esc) is
* pressed. A handler returning %TRUE consumes the key; otherwise the form's
* default handling applies (Esc closes the form).
*
* Returns: %TRUE if the key was handled.
*/
signals[HOTKEY] = g_signal_new("hotkey",
G_OBJECT_CLASS_TYPE(object_class),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET(NmtNewtFormClass, hotkey),
g_signal_accumulator_true_handled,
NULL,
NULL,
G_TYPE_BOOLEAN,
1,
G_TYPE_INT);
/**
* NmtNewtForm:title:
*

View file

@ -26,6 +26,7 @@ typedef struct {
/* signals */
void (*quit)(NmtNewtForm *form);
gboolean (*hotkey)(NmtNewtForm *form, int key);
/* methods */
void (*show)(NmtNewtForm *form);
@ -45,4 +46,8 @@ void nmt_newt_form_quit(NmtNewtForm *form);
void nmt_newt_form_set_focus(NmtNewtForm *form, NmtNewtWidget *widget);
void nmt_newt_form_add_hotkey(NmtNewtForm *form, int key);
void nmt_newt_form_set_stable_width(NmtNewtForm *form);
#endif /* NMT_NEWT_FORM_H */

View file

@ -17,6 +17,7 @@
#include <sys/wait.h>
#include "libnm-glib-aux/nm-io-utils.h"
#include "libnm-glib-aux/nm-str-buf.h"
static void
nmt_newt_dialog_g_log_handler(const char *log_domain,
@ -386,6 +387,44 @@ nmt_newt_text_width(const char *str)
return width;
}
/**
* nmt_newt_text_truncate
* @str: a UTF-8 string
* @max_width: the maximum width in terminal columns
*
* Truncates @str to at most @max_width columns, appending an ellipsis if
* anything was dropped (the ellipsis is counted in @max_width).
*
* Returns: (transfer full): a newly-allocated truncated copy of @str.
*/
char *
nmt_newt_text_truncate(const char *str, int max_width)
{
nm_auto_str_buf NMStrBuf buf = NM_STR_BUF_INIT(0, FALSE);
const char *p;
int width = 0;
if (max_width <= 0)
return g_strdup("");
if (nmt_newt_text_width(str) <= max_width)
return g_strdup(str);
max_width--; /* reserve one column for the ellipsis */
for (p = str; *p; p = g_utf8_next_char(p)) {
gunichar ch = g_utf8_get_char(p);
int w = g_unichar_iszerowidth(ch) ? 0 : (g_unichar_iswide(ch) ? 2 : 1);
if (width + w > max_width)
break;
width += w;
nm_str_buf_append_len(&buf, p, g_utf8_next_char(p) - p);
}
nm_str_buf_append(&buf, "\xe2\x80\xa6"); /* U+2026 HORIZONTAL ELLIPSIS */
return nm_str_buf_dup_str(&buf);
}
/**
* nmt_newt_edit_string:
* @data: data to edit

View file

@ -23,6 +23,8 @@ char *nmt_newt_locale_from_utf8(const char *str_utf8);
int nmt_newt_text_width(const char *str);
char *nmt_newt_text_truncate(const char *str, int max_width);
void nmt_newt_message_dialog(const char *message, ...) _nm_printf(1, 2);
int nmt_newt_choice_dialog(const char *button1, const char *button2, const char *message, ...)
_nm_printf(3, 4);

View file

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

View file

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

View file

@ -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,6 +36,10 @@ typedef struct {
NmtNewtListbox *listbox;
NmtNewtButtonBox *buttons;
NmtSearch *search;
char *filter_text;
int match_count;
NmtNewtWidget *add;
NmtNewtWidget *edit;
NmtNewtWidget *delete;
@ -68,13 +73,16 @@ 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 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
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",
@ -90,6 +98,28 @@ nmt_edit_connection_list_init(NmtEditConnectionList *list)
| NMT_NEWT_GRID_EXPAND_Y);
g_signal_connect(priv->listbox, "activated", G_CALLBACK(listbox_activated), 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);
nmt_newt_grid_add(grid, buttons, 1, 0);
@ -157,7 +187,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 +215,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 +254,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 +276,7 @@ nmt_edit_connection_list_rebuild(NmtEditConnectionList *list)
if (conn == selected_conn)
selected_row = row;
row++;
n_matches++;
}
}
@ -248,6 +285,9 @@ 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);
}
@ -315,6 +355,31 @@ listbox_activated(NmtNewtWidget *listbox, gpointer list)
edit_clicked(NMT_NEWT_BUTTON(priv->edit), 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)
{
@ -348,6 +413,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);
}

View file

@ -42,4 +42,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 */

View file

@ -12,6 +12,8 @@
#include "nmt-utils.h"
#include "libnmt-newt/nmt-newt.h"
/**
* NmtSyncOp:
*
@ -128,3 +130,178 @@ 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);
}

View file

@ -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,34 @@ 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);
#endif /* NMT_UTILS_H */

View file

@ -583,12 +583,26 @@ wifi_rescan(NmtNewtButton *button, gpointer data_batch)
}
}
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, *rescan;
NmtNewtWidget *search_row, *search_label, *search_entry;
NmtSearch *search;
RescanBatch *batch_data;
gs_unref_ptrarray GPtrArray *all_active_wifi_devices = NULL;
@ -634,6 +648,31 @@ nmt_connect_connection_list(gboolean is_top)
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;
}

View file

@ -107,6 +107,8 @@ nmt_edit_main_connection_list(gboolean is_top)
g_signal_connect(list, "edit-connection", G_CALLBACK(list_edit_connection), form);
g_signal_connect(list, "remove-connection", G_CALLBACK(list_remove_connection), form);
nmt_edit_connection_list_bind_search(NMT_EDIT_CONNECTION_LIST(list), form);
nmt_newt_form_set_content(form, list);
return form;
}