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:
parent
b6478aa12f
commit
94a2b1cd25
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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(©, "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");
|
||||
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user