]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
terminal-util: when prompting for a choice from a list, preselect longest prefix
authorLennart Poettering <lennart@amutable.com>
Tue, 5 May 2026 08:45:14 +0000 (10:45 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Tue, 5 May 2026 15:47:20 +0000 (17:47 +0200)
If all entries of a menu prompt start with the same prefix, let's
preselect the prefix to enhance user experience.

This is particularly relevant when prompting for a disk to install
things on, as typically they all start with the same prefix /dev/, and
if there's only a single target medium discoverable, then we can even
fill it out fully.

src/basic/terminal-util.c
src/basic/terminal-util.h
src/home/homectl.c
src/shared/prompt-util.c

index e5e66a1864777a2009a4856ffb3b74ce4f3f8b70..d241f5e7f8998ed05ec03c3cbed4a1dce85c7c78 100644 (file)
@@ -293,17 +293,33 @@ int ask_string_full(
         assert(ret);
         assert(text);
 
+        _cleanup_free_ char *string = NULL;
+        size_t n = 0;
+
+        if (get_completions) {
+                /* Figure out what string to preselect the query with */
+                _cleanup_strv_free_ char **completions = NULL;
+                r = get_completions("", GET_COMPLETIONS_PRESELECT, &completions, userdata);
+                if (r < 0)
+                        return r;
+
+                CompletionResult cr = pick_completion(string, completions, &string);
+                if (cr < 0)
+                        return cr;
+
+                n = strlen_ptr(string);
+        }
+
         /* Output the prompt */
         fputs(ansi_highlight(), stdout);
         va_start(ap, text);
         vprintf(text, ap);
         va_end(ap);
         fputs(ansi_normal(), stdout);
+        if (string)
+                fputs(string, stdout);
         fflush(stdout);
 
-        _cleanup_free_ char *string = NULL;
-        size_t n = 0;
-
         /* Do interactive logic only if stdin + stdout are connected to the same place. And yes, we could use
          * STDIN_FILENO and STDOUT_FILENO here, but let's be overly correct for once, after all libc allows
          * swapping out stdin/stdout. */
@@ -344,7 +360,7 @@ int ask_string_full(
 
                         _cleanup_strv_free_ char **completions = NULL;
                         if (get_completions) {
-                                r = get_completions(string, &completions, userdata);
+                                r = get_completions(string, /* flags= */ 0, &completions, userdata);
                                 if (r < 0)
                                         return r;
                         }
@@ -450,6 +466,7 @@ int ask_string_full(
 
 fallback:
         /* A simple fallback without TTY magic */
+        string = mfree(string);
         r = read_line(stdin, LONG_LINE_MAX, &string);
         if (r < 0)
                 return r;
index 7ac5661104159316af469146f902738f179eb0af..abf999e6d5562603a4a57b338d14b5d6b213b6c2 100644 (file)
@@ -88,7 +88,13 @@ int chvt(int vt);
 int read_one_char(FILE *f, char *ret, usec_t timeout, bool echo, bool *need_nl);
 int ask_char(char *ret, const char *replies, const char *fmt, ...) _printf_(3, 4);
 
-typedef int (*GetCompletionsCallback)(const char *key, char ***ret_list, void *userdata);
+typedef enum GetCompletionsFlags {
+        /* Only return the items subject to preselection: typically you want to suppress meta entries such as
+         * "list" or alias entries if this flag is set. */
+        GET_COMPLETIONS_PRESELECT = 1 << 0,
+} GetCompletionsFlags;
+
+typedef int (*GetCompletionsCallback)(const char *key, GetCompletionsFlags flags, char ***ret_list, void *userdata);
 int ask_string_full(char **ret, GetCompletionsCallback get_completions, void *userdata, const char *text, ...) _printf_(4, 5);
 #define ask_string(ret, text, ...) ask_string_full(ret, NULL, NULL, text, ##__VA_ARGS__)
 
index 271e03587502b10b3329ff837708ffefb8ea1a5d..37714aa2b018527c7ace1681cbf4bf0ff76c5842 100644 (file)
@@ -2697,7 +2697,7 @@ static int acquire_group_list(char ***ret) {
         return !!*ret;
 }
 
-static int group_completion_callback(const char *key, char ***ret_list, void *userdata) {
+static int group_completion_callback(const char *key, GetCompletionsFlags flags, char ***ret_list, void *userdata) {
         char ***available = userdata;
         int r;
 
@@ -2711,9 +2711,11 @@ static int group_completion_callback(const char *key, char ***ret_list, void *us
         if (!l)
                 return -ENOMEM;
 
-        r = strv_extend(&l, "list");
-        if (r < 0)
-                return r;
+        if (!FLAGS_SET(flags, GET_COMPLETIONS_PRESELECT)) {
+                r = strv_extend(&l, "list");
+                if (r < 0)
+                        return r;
+        }
 
         *ret_list = TAKE_PTR(l);
         return 0;
@@ -2745,10 +2747,13 @@ static int prompt_groups(const char *username, char ***ret_groups) {
                 }
 
                 _cleanup_free_ char *s = NULL;
-                r = ask_string_full(&s,
-                               group_completion_callback, &available,
-                               "%s Please enter an auxiliary group for user %s (empty to continue, \"list\" to list available groups): ",
-                               glyph(GLYPH_LABEL), username);
+                r = ask_string_full(
+                                &s,
+                                group_completion_callback,
+                                &available,
+                                "%s Please enter an auxiliary group for user %s (empty to continue, \"list\" to list available groups): ",
+                                glyph(GLYPH_LABEL),
+                                username);
                 if (r < 0)
                         return log_error_errno(r, "Failed to query user for auxiliary group: %m");
 
index 9811cb152787345362ec7aee73f2ae9cb05c2960..7cead706fd95f44d83c07a0c9b390cbba8d90985 100644 (file)
 #include "strv.h"
 #include "terminal-util.h"
 
+typedef struct CompletionData {
+        char **menu;      /* What to show in menu */
+        char **accepted;  /* What to accept (usually larger than the menu, but may be NULL if same) */
+} CompletionData;
+
 static int get_completions(
                 const char *key,
+                GetCompletionsFlags flags,
                 char ***ret_list,
                 void *userdata) {
 
+        CompletionData *data = ASSERT_PTR(userdata);
         int r;
 
         assert(ret_list);
 
-        if (!userdata) {
+        /* Figure out the list to operate on. We'll generally work based on the "accepted" list, if it is
+         * set. If not we'll operate with the full menu. When doing pre-selection we'll also pick the menu */
+        char **l = data->accepted && !FLAGS_SET(flags, GET_COMPLETIONS_PRESELECT) ? data->accepted : data->menu;
+
+        if (strv_isempty(l)) {
                 *ret_list = NULL;
                 return 0;
         }
 
-        _cleanup_strv_free_ char **copy = strv_copy(userdata);
+        _cleanup_strv_free_ char **copy = strv_copy(l);
         if (!copy)
                 return -ENOMEM;
 
-        r = strv_extend(&copy, "list");
-        if (r < 0)
-                return r;
+        /* Never consider "list" for preselecting an item, but do consider it when doing a regular completion */
+        if (!FLAGS_SET(flags, GET_COMPLETIONS_PRESELECT)) {
+                r = strv_extend(&copy, "list");
+                if (r < 0)
+                        return r;
+        }
 
         *ret_list = TAKE_PTR(copy);
         return 0;
@@ -45,8 +59,8 @@ static int get_completions(
 int prompt_loop(
                 const char *text,
                 Glyph emoji,
-                char **menu,        /* if non-NULL: choices to suggest */
-                char **accepted,    /* if non-NULL: choices to accept (should be a superset of 'menu') */
+                char **menu,             /* if non-NULL: choices to suggest */
+                char **accepted,         /* if non-NULL: choices to accept (should be a superset of 'menu') */
                 unsigned ellipsize_percentage,
                 size_t n_columns,
                 size_t column_width,
@@ -102,7 +116,7 @@ int prompt_loop(
                 r = ask_string_full(
                                 &p,
                                 get_completions,
-                                accepted ?: menu,
+                                &(CompletionData) { menu, accepted },
                                 "%s%s%s%s: ",
                                 emoji >= 0 ? glyph(emoji) : "",
                                 emoji >= 0 ? " " : "",