1
0
mirror of https://github.com/systemd/systemd.git synced 2025-03-31 14:50:15 +03:00

firstboot: add auto-completion to various fields

This adds TAB-based auto-completion to various fields we query from the
user, such as locale, keymap, timezone, group membership.

It makes it a lot easier to quickly iterate through firstboot without
typing too much.
This commit is contained in:
Lennart Poettering 2025-02-05 10:55:48 +01:00
parent b6478aa12f
commit 94a2b1cd25
4 changed files with 284 additions and 12 deletions

View File

@ -26,6 +26,7 @@
#include "constants.h"
#include "devnum-util.h"
#include "env-util.h"
#include "errno-list.h"
#include "fd-util.h"
#include "fileio.h"
#include "fs-util.h"
@ -231,31 +232,246 @@ int ask_char(char *ret, const char *replies, const char *fmt, ...) {
}
}
int ask_string(char **ret, const char *text, ...) {
_cleanup_free_ char *line = NULL;
typedef enum CompletionResult{
COMPLETION_ALREADY, /* the input string is already complete */
COMPLETION_FULL, /* completed the input string to be complete now */
COMPLETION_PARTIAL, /* completed the input string so that is still incomplete */
COMPLETION_NONE, /* found no matching completion */
_COMPLETION_RESULT_MAX,
_COMPLETION_RESULT_INVALID = -EINVAL,
_COMPLETION_RESULT_ERRNO_MAX = -ERRNO_MAX,
} CompletionResult;
static CompletionResult pick_completion(const char *string, char *const*completions, char **ret) {
_cleanup_free_ char *found = NULL;
bool partial = false;
string = strempty(string);
STRV_FOREACH(c, completions) {
/* Ignore entries that are not actually completions */
if (!startswith(*c, string))
continue;
/* Store first completion that matches */
if (!found) {
found = strdup(*c);
if (!found)
return -ENOMEM;
continue;
}
/* If there's another completion that works truncate the one we already found by common
* prefix */
size_t n = str_common_prefix(found, *c);
if (n == SIZE_MAX)
continue;
found[n] = 0;
partial = true;
}
*ret = TAKE_PTR(found);
if (!*ret)
return COMPLETION_NONE;
if (partial)
return COMPLETION_PARTIAL;
return streq(string, *ret) ? COMPLETION_ALREADY : COMPLETION_FULL;
}
static void clear_by_backspace(size_t n) {
/* Erase the specified number of character cells backwards on the terminal */
for (size_t i = 0; i < n; i++)
fputs("\b \b", stdout);
}
int ask_string_full(
char **ret,
GetCompletionsCallback get_completions,
void *userdata,
const char *text, ...) {
va_list ap;
int r;
assert(ret);
assert(text);
/* Output the prompt */
fputs(ansi_highlight(), stdout);
va_start(ap, text);
vprintf(text, ap);
va_end(ap);
fputs(ansi_normal(), stdout);
fflush(stdout);
r = read_line(stdin, LONG_LINE_MAX, &line);
_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. */
int fd_input = fileno(stdin);
int fd_output = fileno(stdout);
if (fd_input < 0 || fd_output < 0 || same_fd(fd_input, fd_output) <= 0)
goto fallback;
/* Try to disable echo, which also tells us if this even is a terminal */
struct termios old_termios;
if (tcgetattr(fd_input, &old_termios) < 0)
goto fallback;
struct termios new_termios = old_termios;
termios_disable_echo(&new_termios);
if (tcsetattr(fd_input, TCSADRAIN, &new_termios) < 0)
return -errno;
for (;;) {
int c = fgetc(stdin);
/* On EOF or NUL, end the request, don't output anything anymore */
if (IN_SET(c, EOF, 0))
break;
/* On Return also end the request, but make this visible */
if (IN_SET(c, '\n', '\r')) {
fputc('\n', stdout);
break;
}
if (c == '\t') {
/* Tab */
_cleanup_strv_free_ char **completions = NULL;
if (get_completions) {
r = get_completions(string, &completions, userdata);
if (r < 0)
goto fail;
}
_cleanup_free_ char *new_string = NULL;
CompletionResult cr = pick_completion(string, completions, &new_string);
if (cr < 0) {
r = cr;
goto fail;
}
if (IN_SET(cr, COMPLETION_PARTIAL, COMPLETION_FULL)) {
/* Output the new suffix we learned */
fputs(ASSERT_PTR(startswith(new_string, strempty(string))), stdout);
/* And update the whole string */
free_and_replace(string, new_string);
n = strlen(string);
}
if (cr == COMPLETION_NONE)
fputc('\a', stdout); /* BEL */
if (IN_SET(cr, COMPLETION_PARTIAL, COMPLETION_ALREADY)) {
/* If this worked only partially, or if the user hit TAB even though we were
* complete already, then show the remaining options (in the latter case just
* the one). */
fputc('\n', stdout);
_cleanup_strv_free_ char **filtered = strv_filter_prefix(completions, string);
if (!filtered) {
r = -ENOMEM;
goto fail;
}
r = show_menu(filtered,
/* n_columns= */ SIZE_MAX,
/* column_width= */ SIZE_MAX,
/* ellipsize_percentage= */ 0,
/* grey_prefix=*/ string,
/* with_numbers= */ false);
if (r < 0)
goto fail;
/* Show the prompt again */
fputs(ansi_highlight(), stdout);
va_start(ap, text);
vprintf(text, ap);
va_end(ap);
fputs(ansi_normal(), stdout);
fputs(string, stdout);
}
} else if (IN_SET(c, '\b', 127)) {
/* Backspace */
if (n == 0)
fputc('\a', stdout); /* BEL */
else {
size_t m = utf8_last_length(string, n);
char *e = string + n - m;
clear_by_backspace(utf8_console_width(e));
*e = 0;
n -= m;
}
} else if (c == 21) {
/* Ctrl-u → erase all input */
clear_by_backspace(utf8_console_width(string));
string[n = 0] = 0;
} else if (c == 4) {
/* Ctrl-d → cancel this field input */
r = -ECANCELED;
goto fail;
} else if (char_is_cc(c) || n >= LINE_MAX)
/* refuse control characters and too long strings */
fputc('\a', stdout); /* BEL */
else {
/* Regular char */
if (!GREEDY_REALLOC(string, n+2)) {
r = -ENOMEM;
goto fail;
}
string[n++] = (char) c;
string[n] = 0;
fputc(c, stdout);
}
fflush(stdout);
}
if (tcsetattr(fd_input, TCSADRAIN, &old_termios) < 0)
return -errno;
if (!string) {
string = strdup("");
if (!string)
return -ENOMEM;
}
*ret = TAKE_PTR(string);
return 0;
fail:
(void) tcsetattr(fd_input, TCSADRAIN, &old_termios);
return r;
fallback:
/* A simple fallback without TTY magic */
r = read_line(stdin, LONG_LINE_MAX, &string);
if (r < 0)
return r;
if (r == 0)
return -EIO;
*ret = TAKE_PTR(line);
*ret = TAKE_PTR(string);
return 0;
}

View File

@ -82,7 +82,11 @@ 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 *text, ...) _printf_(3, 4);
int ask_string(char **ret, const char *text, ...) _printf_(2, 3);
typedef int (*GetCompletionsCallback)(const char *key, char ***ret_list, void *userdata);
int ask_string_full(char **ret, GetCompletionsCallback cb, void *userdata, const char *text, ...) _printf_(4, 5);
#define ask_string(ret, text, ...) ask_string_full(ret, NULL, NULL, text, ##__VA_ARGS__)
bool any_key_to_proceed(void);
int show_menu(char **x, size_t n_columns, size_t column_width, unsigned ellipsize_percentage, const char *grey_prefix, bool with_numbers);

View File

@ -135,6 +135,30 @@ static void print_welcome(int rfd) {
done = true;
}
static int get_completions(
const char *key,
char ***ret_list,
void *userdata) {
int r;
if (!userdata) {
*ret_list = NULL;
return 0;
}
_cleanup_strv_free_ char **copy = strv_copy(userdata);
if (!copy)
return -ENOMEM;
r = strv_extend(&copy, "list");
if (r < 0)
return r;
*ret_list = TAKE_PTR(copy);
return 0;
}
static int prompt_loop(
int rfd,
const char *text,
@ -142,6 +166,7 @@ static int prompt_loop(
unsigned ellipsize_percentage,
bool (*is_valid)(int rfd, const char *name),
char **ret) {
int r;
assert(text);
@ -151,9 +176,13 @@ static int prompt_loop(
for (;;) {
_cleanup_free_ char *p = NULL;
r = ask_string(&p, strv_isempty(l) ? "%s %s (empty to skip): "
: "%s %s (empty to skip, \"list\" to list options): ",
special_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET), text);
r = ask_string_full(
&p,
get_completions,
l,
strv_isempty(l) ? "%s %s (empty to skip): "
: "%s %s (empty to skip, \"list\" to list options): ",
special_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET), text);
if (r < 0)
return log_error_errno(r, "Failed to query user: %m");

View File

@ -2476,6 +2476,28 @@ static int acquire_group_list(char ***ret) {
return !!*ret;
}
static int group_completion_callback(const char *key, char ***ret_list, void *userdata) {
char ***available = userdata;
int r;
if (!*available) {
r = acquire_group_list(available);
if (r < 0)
log_debug_errno(r, "Failed to enumerate available groups, ignoring: %m");
}
_cleanup_strv_free_ char **l = strv_copy(*available);
if (!l)
return -ENOMEM;
r = strv_extend(&l, "list");
if (r < 0)
return r;
*ret_list = TAKE_PTR(l);
return 0;
}
static int create_interactively(void) {
_cleanup_free_ char *username = NULL;
int r;
@ -2531,7 +2553,8 @@ static int create_interactively(void) {
for (;;) {
_cleanup_free_ char *s = NULL;
r = ask_string(&s,
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): ",
special_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET), username);
if (r < 0)