nmtui: add vim-style '/' search to connection lists

The connection lists in "nmtui connect" and "nmtui edit" have no way to
filter, so finding one profile among many means scrolling. Press '/' to
reveal a search box that hides non-matching rows as you type; Enter keeps
the filter applied, Esc clears it and restores the list.

A row below the list is always present, so revealing the box does not
resize the form. While typing it shows "Search:"; once a filter is
applied and the box is hidden, it reports the active filter and the match
count ("Matching '...' (N)") so it is clear the list is filtered. Key
interception uses the NmtNewtForm hotkey mechanism.
This commit is contained in:
Josephine Pfeiffer 2026-06-16 14:10:50 +02:00
parent 9da53cb4a7
commit 5e2360afc1
No known key found for this signature in database
GPG key ID: ABD48F465F4434BD
13 changed files with 449 additions and 8 deletions

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,9 +31,11 @@ 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;
@ -184,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)
@ -476,6 +487,21 @@ nmt_newt_form_add_hotkey(NmtNewtForm *form, int key)
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)
{

View file

@ -48,4 +48,6 @@ 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;
}