From: Lennart Poettering Date: Tue, 5 May 2026 08:45:14 +0000 (+0200) Subject: terminal-util: when prompting for a choice from a list, preselect longest prefix X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=fc05165fce386c8cf991f18abecc5098e7261b6f;p=thirdparty%2Fsystemd.git terminal-util: when prompting for a choice from a list, preselect longest prefix 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. --- diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index e5e66a18647..d241f5e7f89 100644 --- a/src/basic/terminal-util.c +++ b/src/basic/terminal-util.c @@ -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; diff --git a/src/basic/terminal-util.h b/src/basic/terminal-util.h index 7ac56611041..abf999e6d55 100644 --- a/src/basic/terminal-util.h +++ b/src/basic/terminal-util.h @@ -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__) diff --git a/src/home/homectl.c b/src/home/homectl.c index 271e0358750..37714aa2b01 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -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"); diff --git a/src/shared/prompt-util.c b/src/shared/prompt-util.c index 9811cb15278..7cead706fd9 100644 --- a/src/shared/prompt-util.c +++ b/src/shared/prompt-util.c @@ -16,27 +16,41 @@ #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(©, "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(©, "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 ? " " : "",