firstboot: some love (#39070)

This is split out of #38764.

It mostly introduces the "chrome" stuff that puts a blue at the top of
bottom of the terminal screen when going through interactive tools such
as firstboot, homed-firstboot, and (in future) systemd-sysinstall).

it also introduces a generic "prompt_loop()" helper thatn queries the
user for some option in a loop until the rsponse matches certain
requirements. It's a generalization of a function of the same name that
so far only existed in firstboot.c. The more generic version will be
reused in a later PR by homed-firstboot and by sysinstall.
This commit is contained in:
Yu Watanabe
2025-09-25 00:38:23 +09:00
committed by GitHub
14 changed files with 768 additions and 266 deletions

View File

@@ -439,15 +439,26 @@
<varlistentry>
<term><varname>ANSI_COLOR=</varname></term>
<listitem><para>A suggested presentation color when showing the OS name on the console. This should
be specified as string suitable for inclusion in the ESC [ m ANSI/ECMA-48 escape code for setting
graphical rendition. This field is optional.</para>
<listitem><para>A suggested presentation (foreground) text color when showing the OS name on the
console. This should be specified as string suitable for inclusion in the ESC [ m ANSI/ECMA-48
escape code for setting graphical rendition. This field is optional.</para>
<para>Examples: <literal>ANSI_COLOR="0;31"</literal> for red, <literal>ANSI_COLOR="1;34"</literal>
for light blue, or <literal>ANSI_COLOR="0;38;2;60;110;180"</literal> for Fedora blue.
</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>ANSI_COLOR_REVERSE=</varname></term>
<listitem><para>Similar to <varname>ANSI_COLOR=</varname>, but should encode the desired
presentation color as background color, along with a suitable foreground color. This is may be used
by console applications to set off "chrome" UI elements from the main terminal contents. This field
is optional.</para>
<xi:include href="version-info.xml" xpointer="v259"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>VENDOR_NAME=</varname></term>

View File

@@ -344,6 +344,16 @@
<xi:include href="version-info.xml" xpointer="v246"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--chrome=</option></term>
<listitem><para>Takes a boolean argument. By default the initial setup scren will show reverse color
"chrome" bars at the top and and the bottom of the terminal screen, which may be disabled by setting
this option to false.</para>
<xi:include href="version-info.xml" xpointer="v259"/></listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" />
</variablelist>

View File

@@ -88,6 +88,12 @@ const char* glyph_full(Glyph code, bool force_utf) {
[GLYPH_SUPERHERO] = "S",
[GLYPH_IDCARD] = "@",
[GLYPH_HOME] = "^",
[GLYPH_ROCKET] = "^",
[GLYPH_BROOM] = "/",
[GLYPH_KEYBOARD] = "K",
[GLYPH_CLOCK] = "O",
[GLYPH_LABEL] = "L",
[GLYPH_SHELL] = "$",
},
/* UTF-8 */
@@ -155,7 +161,6 @@ const char* glyph_full(Glyph code, bool force_utf) {
[GLYPH_WARNING_SIGN] = UTF8("⚠️"),
[GLYPH_COMPUTER_DISK] = UTF8("💽"),
[GLYPH_WORLD] = UTF8("🌍"),
[GLYPH_RED_CIRCLE] = UTF8("🔴"),
[GLYPH_YELLOW_CIRCLE] = UTF8("🟡"),
[GLYPH_BLUE_CIRCLE] = UTF8("🔵"),
@@ -163,6 +168,12 @@ const char* glyph_full(Glyph code, bool force_utf) {
[GLYPH_SUPERHERO] = UTF8("🦸"),
[GLYPH_IDCARD] = UTF8("🪪"),
[GLYPH_HOME] = UTF8("🏠"),
[GLYPH_ROCKET] = UTF8("🚀"),
[GLYPH_BROOM] = UTF8("🧹"),
[GLYPH_KEYBOARD] = UTF8("⌨️"),
[GLYPH_CLOCK] = UTF8("🕗"),
[GLYPH_LABEL] = UTF8("🏷️"),
[GLYPH_SHELL] = UTF8("🐚"),
},
};

View File

@@ -56,6 +56,12 @@ typedef enum Glyph {
GLYPH_SUPERHERO,
GLYPH_IDCARD,
GLYPH_HOME,
GLYPH_ROCKET,
GLYPH_BROOM,
GLYPH_KEYBOARD,
GLYPH_CLOCK,
GLYPH_LABEL,
GLYPH_SHELL,
_GLYPH_MAX,
_GLYPH_INVALID = -EINVAL,
} Glyph;

View File

@@ -206,3 +206,16 @@ static inline size_t size_add(size_t x, size_t y) {
for (typeof(entry) _va_sentinel_[1] = {}, _entries_[] = { __VA_ARGS__ __VA_OPT__(,) _va_sentinel_[0] }, *_current_ = _entries_; \
((long)(_current_ - _entries_) < (long)(ELEMENTSOF(_entries_) - 1)) && ({ entry = *_current_; true; }); \
_current_++)
typedef void (*void_func_t)(void);
static inline void dispatch_void_func(void_func_t *f) {
assert(f);
assert(*f);
(*f)();
}
/* Inspired by Go's "defer" construct, but much more basic. This basically just calls a void function when
* the current scope is left. Doesn't do function parameters (i.e. no closures). */
#define DEFER_VOID_CALL(x) _DEFER_VOID_CALL(UNIQ, x)
#define _DEFER_VOID_CALL(uniq, x) _unused_ _cleanup_(dispatch_void_func) void_func_t UNIQ_T(defer, uniq) = (x)

View File

@@ -1855,6 +1855,226 @@ int terminal_set_cursor_position(int fd, unsigned row, unsigned column) {
return loop_write(fd, cursor_position, SIZE_MAX);
}
static int terminal_verify_same(int input_fd, int output_fd) {
assert(input_fd >= 0);
assert(output_fd >= 0);
/* Validates that the specified fds reference the same TTY */
if (input_fd != output_fd) {
struct stat sti;
if (fstat(input_fd, &sti) < 0)
return -errno;
if (!S_ISCHR(sti.st_mode)) /* TTYs are character devices */
return -ENOTTY;
struct stat sto;
if (fstat(output_fd, &sto) < 0)
return -errno;
if (!S_ISCHR(sto.st_mode))
return -ENOTTY;
if (sti.st_rdev != sto.st_rdev)
return -ENOLINK;
}
if (!isatty_safe(input_fd)) /* The check above was just for char device, but now let's ensure it's actually a tty */
return -ENOTTY;
return 0;
}
typedef enum CursorPositionState {
CURSOR_TEXT,
CURSOR_ESCAPE,
CURSOR_ROW,
CURSOR_COLUMN,
} CursorPositionState;
typedef struct CursorPositionContext {
CursorPositionState state;
unsigned row, column;
} CursorPositionContext;
static int scan_cursor_position_response(
CursorPositionContext *context,
const char *buf,
size_t size,
size_t *ret_processed) {
assert(context);
assert(buf);
assert(ret_processed);
for (size_t i = 0; i < size; i++) {
char c = buf[i];
switch (context->state) {
case CURSOR_TEXT:
context->state = c == '\x1B' ? CURSOR_ESCAPE : CURSOR_TEXT;
break;
case CURSOR_ESCAPE:
context->state = c == '[' ? CURSOR_ROW : CURSOR_TEXT;
break;
case CURSOR_ROW:
if (c == ';')
context->state = context->row > 0 ? CURSOR_COLUMN : CURSOR_TEXT;
else {
int d = undecchar(c);
/* We read a decimal character, let's suffix it to the number we so far read,
* but let's do an overflow check first. */
if (d < 0 || context->row > (UINT_MAX-d)/10)
context->state = CURSOR_TEXT;
else
context->row = context->row * 10 + d;
}
break;
case CURSOR_COLUMN:
if (c == 'R') {
if (context->column > 0) {
*ret_processed = i + 1;
return 1; /* success! */
}
context->state = CURSOR_TEXT;
} else {
int d = undecchar(c);
/* As above, add the decimal character to our column number */
if (d < 0 || context->column > (UINT_MAX-d)/10)
context->state = CURSOR_TEXT;
else
context->column = context->column * 10 + d;
}
break;
}
/* Reset any positions we might have picked up */
if (IN_SET(context->state, CURSOR_TEXT, CURSOR_ESCAPE))
context->row = context->column = 0;
}
*ret_processed = size;
return 0; /* all good, but not enough data yet */
}
int terminal_get_cursor_position(
int input_fd,
int output_fd,
unsigned *ret_row,
unsigned *ret_column) {
_cleanup_close_ int nonblock_input_fd = -EBADF;
int r;
assert(input_fd >= 0);
assert(output_fd >= 0);
if (terminal_is_dumb())
return -EOPNOTSUPP;
r = terminal_verify_same(input_fd, output_fd);
if (r < 0)
return log_debug_errno(r, "Called with distinct input/output fds: %m");
struct termios old_termios;
if (tcgetattr(input_fd, &old_termios) < 0)
return log_debug_errno(errno, "Failed to get terminal settings: %m");
struct termios new_termios = old_termios;
termios_disable_echo(&new_termios);
if (tcsetattr(input_fd, TCSANOW, &new_termios) < 0)
return log_debug_errno(errno, "Failed to set new terminal settings: %m");
/* Request cursor position (DSR/CPR) */
r = loop_write(output_fd, "\x1B[6n", SIZE_MAX);
if (r < 0)
goto finish;
/* Open a 2nd input fd, in non-blocking mode, so that we won't ever hang in read() should someone
* else process the POLLIN. */
nonblock_input_fd = r = fd_reopen(input_fd, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
if (r < 0)
goto finish;
usec_t end = usec_add(now(CLOCK_MONOTONIC), CONSOLE_REPLY_WAIT_USEC);
char buf[STRLEN("\x1B[1;1R")]; /* The shortest valid reply possible */
size_t buf_full = 0;
CursorPositionContext context = {};
for (bool first = true;; first = false) {
if (buf_full == 0) {
usec_t n = now(CLOCK_MONOTONIC);
if (n >= end) {
r = -EOPNOTSUPP;
goto finish;
}
r = fd_wait_for_event(nonblock_input_fd, POLLIN, usec_sub_unsigned(end, n));
if (r < 0)
goto finish;
if (r == 0) {
r = -EOPNOTSUPP;
goto finish;
}
/* On the first try, read multiple characters, i.e. the shortest valid
* reply. Afterwards read byte-wise, since we don't want to read too much, and
* unnecessarily drop too many characters from the input queue. */
ssize_t l = read(nonblock_input_fd, buf, first ? sizeof(buf) : 1);
if (l < 0) {
if (errno == EAGAIN)
continue;
r = -errno;
goto finish;
}
assert((size_t) l <= sizeof(buf));
buf_full = l;
}
size_t processed;
r = scan_cursor_position_response(&context, buf, buf_full, &processed);
if (r < 0)
goto finish;
assert(processed <= buf_full);
buf_full -= processed;
memmove(buf, buf + processed, buf_full);
if (r > 0) {
/* Superficial validity check */
if (context.row >= 32766 || context.column >= 32766) {
r = -ENODATA;
goto finish;
}
if (ret_row)
*ret_row = context.row;
if (ret_column)
*ret_column = context.column;
r = 0;
goto finish;
}
}
finish:
RET_GATHER(r, RET_NERRNO(tcsetattr(input_fd, TCSANOW, &old_termios)));
return r;
}
int terminal_reset_defensive(int fd, TerminalResetFlags flags) {
int r = 0;
@@ -1894,37 +2114,6 @@ void termios_disable_echo(struct termios *termios) {
termios->c_cc[VTIME] = 0;
}
static int terminal_verify_same(int input_fd, int output_fd) {
assert(input_fd >= 0);
assert(output_fd >= 0);
/* Validates that the specified fds reference the same TTY */
if (input_fd != output_fd) {
struct stat sti;
if (fstat(input_fd, &sti) < 0)
return -errno;
if (!S_ISCHR(sti.st_mode)) /* TTYs are character devices */
return -ENOTTY;
struct stat sto;
if (fstat(output_fd, &sto) < 0)
return -errno;
if (!S_ISCHR(sto.st_mode))
return -ENOTTY;
if (sti.st_rdev != sto.st_rdev)
return -ENOLINK;
}
if (!isatty_safe(input_fd)) /* The check above was just for char device, but now let's ensure it's actually a tty */
return -ENOTTY;
return 0;
}
typedef enum BackgroundColorState {
BACKGROUND_TEXT,
BACKGROUND_ESCAPE,
@@ -2174,86 +2363,6 @@ finish:
return r;
}
typedef enum CursorPositionState {
CURSOR_TEXT,
CURSOR_ESCAPE,
CURSOR_ROW,
CURSOR_COLUMN,
} CursorPositionState;
typedef struct CursorPositionContext {
CursorPositionState state;
unsigned row, column;
} CursorPositionContext;
static int scan_cursor_position_response(
CursorPositionContext *context,
const char *buf,
size_t size,
size_t *ret_processed) {
assert(context);
assert(buf);
assert(ret_processed);
for (size_t i = 0; i < size; i++) {
char c = buf[i];
switch (context->state) {
case CURSOR_TEXT:
context->state = c == '\x1B' ? CURSOR_ESCAPE : CURSOR_TEXT;
break;
case CURSOR_ESCAPE:
context->state = c == '[' ? CURSOR_ROW : CURSOR_TEXT;
break;
case CURSOR_ROW:
if (c == ';')
context->state = context->row > 0 ? CURSOR_COLUMN : CURSOR_TEXT;
else {
int d = undecchar(c);
/* We read a decimal character, let's suffix it to the number we so far read,
* but let's do an overflow check first. */
if (d < 0 || context->row > (UINT_MAX-d)/10)
context->state = CURSOR_TEXT;
else
context->row = context->row * 10 + d;
}
break;
case CURSOR_COLUMN:
if (c == 'R') {
if (context->column > 0) {
*ret_processed = i + 1;
return 1; /* success! */
}
context->state = CURSOR_TEXT;
} else {
int d = undecchar(c);
/* As above, add the decimal character to our column number */
if (d < 0 || context->column > (UINT_MAX-d)/10)
context->state = CURSOR_TEXT;
else
context->column = context->column * 10 + d;
}
break;
}
/* Reset any positions we might have picked up */
if (IN_SET(context->state, CURSOR_TEXT, CURSOR_ESCAPE))
context->row = context->column = 0;
}
*ret_processed = size;
return 0; /* all good, but not enough data yet */
}
int terminal_get_size_by_dsr(
int input_fd,
int output_fd,

View File

@@ -46,6 +46,7 @@ int terminal_reset_defensive(int fd, TerminalResetFlags flags);
int terminal_reset_defensive_locked(int fd, TerminalResetFlags flags);
int terminal_set_cursor_position(int fd, unsigned row, unsigned column);
int terminal_get_cursor_position(int input_fd, int output_fd, unsigned *ret_rows, unsigned *ret_column);
int open_terminal(const char *name, int mode);

View File

@@ -45,6 +45,7 @@
#include "path-util.h"
#include "pretty-print.h"
#include "proc-cmdline.h"
#include "prompt-util.h"
#include "runtime-scope.h"
#include "smack-util.h"
#include "stat-util.h"
@@ -84,6 +85,7 @@ static bool arg_root_password_is_hashed = false;
static bool arg_welcome = true;
static bool arg_reset = false;
static ImagePolicy *arg_image_policy = NULL;
static bool arg_chrome = true;
STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
@@ -113,6 +115,11 @@ static void print_welcome(int rfd) {
return;
}
(void) terminal_reset_defensive_locked(STDOUT_FILENO, /* flags= */ 0);
if (arg_chrome)
chrome_show("Initial Setup", /* bottom= */ NULL);
r = parse_os_release_at(rfd,
"PRETTY_NAME", &pretty_name,
"NAME", &os_name,
@@ -124,13 +131,10 @@ static void print_welcome(int rfd) {
pn = os_release_pretty_name(pretty_name, os_name);
ac = isempty(ansi_color) ? "0" : ansi_color;
(void) terminal_reset_defensive_locked(STDOUT_FILENO, /* flags= */ 0);
if (colors_enabled())
printf("\n"
ANSI_HIGHLIGHT "Welcome to your new installation of " ANSI_NORMAL "\x1B[%sm%s" ANSI_HIGHLIGHT "!" ANSI_NORMAL "\n", ac, pn);
printf(ANSI_HIGHLIGHT "Welcome to your new installation of " ANSI_NORMAL "\x1B[%sm%s" ANSI_HIGHLIGHT "!" ANSI_NORMAL "\n", ac, pn);
else
printf("\nWelcome to your new installation of %s!\n", pn);
printf("Welcome to your new installation of %s!\n", pn);
putchar('\n');
if (emoji_enabled()) {
@@ -144,102 +148,6 @@ 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,
char **l,
unsigned ellipsize_percentage,
bool (*is_valid)(int rfd, const char *name),
char **ret) {
int r;
assert(text);
assert(is_valid);
assert(ret);
for (;;) {
_cleanup_free_ char *p = NULL;
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): ",
glyph(GLYPH_TRIANGULAR_BULLET), text);
if (r < 0)
return log_error_errno(r, "Failed to query user: %m");
if (isempty(p)) {
log_info("No data entered, skipping.");
return 0;
}
if (!strv_isempty(l)) {
if (streq(p, "list")) {
r = show_menu(l,
/* n_columns= */ 3,
/* column_width= */ 20,
ellipsize_percentage,
/* grey_prefix= */ NULL,
/* with_numbers= */ true);
if (r < 0)
return log_error_errno(r, "Failed to show menu: %m");
putchar('\n');
continue;
}
unsigned u;
r = safe_atou(p, &u);
if (r >= 0) {
if (u <= 0 || u > strv_length(l)) {
log_error("Specified entry number out of range.");
continue;
}
log_info("Selected '%s'.", l[u-1]);
return free_and_strdup_warn(ret, l[u-1]);
}
}
if (is_valid(rfd, p))
return free_and_replace(*ret, p);
/* Be more helpful to the user, and give a hint what the user might have wanted to type. */
const char *best_match = strv_find_closest(l, p);
if (best_match)
log_error("Invalid data '%s', did you mean '%s'?", p, best_match);
else
log_error("Invalid data '%s'.", p);
}
}
static int should_configure(int dir_fd, const char *filename) {
_cleanup_fclose_ FILE *passwd = NULL, *shadow = NULL;
int r;
@@ -309,20 +217,14 @@ static int should_configure(int dir_fd, const char *filename) {
return true;
}
static bool locale_is_installed_bool(const char *name) {
return locale_is_installed(name) > 0;
}
static bool locale_is_ok(int rfd, const char *name) {
int r;
assert(rfd >= 0);
static int locale_is_ok(const char *name, void *userdata) {
int rfd = ASSERT_FD(PTR_TO_FD(userdata)), r;
r = dir_fd_is_root(rfd);
if (r < 0)
log_debug_errno(r, "Unable to determine if operating on host root directory, assuming we are: %m");
return r != 0 ? locale_is_installed_bool(name) : locale_is_valid(name);
return r != 0 ? locale_is_installed(name) > 0 : locale_is_valid(name);
}
static int prompt_locale(int rfd) {
@@ -379,16 +281,35 @@ static int prompt_locale(int rfd) {
} else {
print_welcome(rfd);
r = prompt_loop(rfd, "Please enter the new system locale name or number",
locales, 60, locale_is_ok, &arg_locale);
r = prompt_loop("Please enter the new system locale name or number",
GLYPH_WORLD,
locales,
/* accepted= */ NULL,
/* ellipsize_percentage= */ 60,
/* n_columns= */ 3,
/* column_width= */ 20,
locale_is_ok,
/* refresh= */ NULL,
FD_TO_PTR(rfd),
PROMPT_MAY_SKIP|PROMPT_SHOW_MENU,
&arg_locale);
if (r < 0)
return r;
if (isempty(arg_locale))
return 0;
r = prompt_loop(rfd, "Please enter the new system message locale name or number",
locales, 60, locale_is_ok, &arg_locale_messages);
r = prompt_loop("Please enter the new system message locale name or number",
GLYPH_WORLD,
locales,
/* accepted= */ NULL,
/* ellipsize_percentage= */ 60,
/* n_columns= */ 3,
/* column_width= */ 20,
locale_is_ok,
/* refresh= */ NULL,
FD_TO_PTR(rfd),
PROMPT_MAY_SKIP|PROMPT_SHOW_MENU,
&arg_locale_messages);
if (r < 0)
return r;
@@ -463,20 +384,14 @@ static int process_locale(int rfd) {
return 1;
}
static bool keymap_exists_bool(const char *name) {
return keymap_exists(name) > 0;
}
static bool keymap_is_ok(int rfd, const char* name) {
int r;
assert(rfd >= 0);
static int keymap_is_ok(const char* name, void *userdata) {
int rfd = ASSERT_FD(PTR_TO_FD(userdata)), r;
r = dir_fd_is_root(rfd);
if (r < 0)
log_debug_errno(r, "Unable to determine if operating on host root directory, assuming we are: %m");
return r != 0 ? keymap_exists_bool(name) : keymap_is_valid(name);
return r != 0 ? keymap_exists(name) > 0 : keymap_is_valid(name);
}
static int prompt_keymap(int rfd) {
@@ -509,8 +424,19 @@ static int prompt_keymap(int rfd) {
print_welcome(rfd);
return prompt_loop(rfd, "Please enter the new keymap name or number",
kmaps, 60, keymap_is_ok, &arg_keymap);
return prompt_loop(
"Please enter the new keymap name or number",
GLYPH_KEYBOARD,
kmaps,
/* accepted= */ NULL,
/* ellipsize_percentage= */ 60,
/* n_columns= */ 3,
/* column_width= */ 20,
keymap_is_ok,
/* refresh= */ NULL,
FD_TO_PTR(rfd),
PROMPT_MAY_SKIP|PROMPT_SHOW_MENU,
&arg_keymap);
}
static int process_keymap(int rfd) {
@@ -578,9 +504,7 @@ static int process_keymap(int rfd) {
return 1;
}
static bool timezone_is_ok(int rfd, const char *name) {
assert(rfd >= 0);
static int timezone_is_ok(const char *name, void *userdata) {
return timezone_is_valid(name, LOG_DEBUG);
}
@@ -612,8 +536,19 @@ static int prompt_timezone(int rfd) {
print_welcome(rfd);
return prompt_loop(rfd, "Please enter the new timezone name or number",
zones, 30, timezone_is_ok, &arg_timezone);
return prompt_loop(
"Please enter the new timezone name or number",
GLYPH_CLOCK,
zones,
/* accepted= */ NULL,
/* ellipsize_percentage= */ 30,
/* n_columns= */ 3,
/* column_width= */ 20,
timezone_is_ok,
/* refresh= */ NULL,
FD_TO_PTR(rfd),
PROMPT_MAY_SKIP|PROMPT_SHOW_MENU,
&arg_timezone);
}
static int process_timezone(int rfd) {
@@ -677,9 +612,7 @@ static int process_timezone(int rfd) {
return 0;
}
static bool hostname_is_ok(int rfd, const char *name) {
assert(rfd >= 0);
static int hostname_is_ok(const char *name, void *userdata) {
return hostname_is_valid(name, VALID_HOSTNAME_TRAILING_DOT);
}
@@ -698,8 +631,18 @@ static int prompt_hostname(int rfd) {
print_welcome(rfd);
r = prompt_loop(rfd, "Please enter the new hostname",
NULL, 0, hostname_is_ok, &arg_hostname);
r = prompt_loop("Please enter the new hostname",
GLYPH_LABEL,
/* menu= */ NULL,
/* accepted= */ NULL,
/* ellipsize_percentage= */ 100,
/* n_columns= */ 3,
/* column_width= */ 20,
hostname_is_ok,
/* refresh= */ NULL,
FD_TO_PTR(rfd),
PROMPT_MAY_SKIP,
&arg_hostname);
if (r < 0)
return r;
@@ -796,8 +739,8 @@ static int prompt_root_password(int rfd) {
print_welcome(rfd);
msg1 = strjoina(glyph(GLYPH_TRIANGULAR_BULLET), " Please enter the new root password (empty to skip):");
msg2 = strjoina(glyph(GLYPH_TRIANGULAR_BULLET), " Please enter the new root password again:");
msg1 = strjoina("Please enter the new root password (empty to skip):");
msg2 = strjoina("Please enter the new root password again:");
suggest_passwords();
@@ -868,8 +811,8 @@ static int find_shell(int rfd, const char *path) {
return 0;
}
static bool shell_is_ok(int rfd, const char *path) {
assert(rfd >= 0);
static int shell_is_ok(const char *path, void *userdata) {
int rfd = ASSERT_FD(PTR_TO_FD(userdata));
return find_shell(rfd, path) >= 0;
}
@@ -897,8 +840,19 @@ static int prompt_root_shell(int rfd) {
print_welcome(rfd);
return prompt_loop(rfd, "Please enter the new root shell",
NULL, 0, shell_is_ok, &arg_root_shell);
return prompt_loop(
"Please enter the new root shell",
GLYPH_SHELL,
/* menu= */ NULL,
/* accepted= */ NULL,
/* ellipsize_percentage= */ 0,
/* n_columns= */ 3,
/* column_width= */ 20,
shell_is_ok,
/* refresh= */ NULL,
FD_TO_PTR(rfd),
PROMPT_MAY_SKIP,
&arg_root_shell);
}
static int write_root_passwd(int rfd, int etc_fd, const char *password, const char *shell) {
@@ -1254,8 +1208,8 @@ static int help(void) {
if (r < 0)
return log_oom();
printf("%s [OPTIONS...]\n\n"
"Configures basic settings of the system.\n\n"
printf("%1$s [OPTIONS...]\n"
"\n%3$sConfigures basic settings of the system.%4$s\n\n"
" -h --help Show this help\n"
" --version Show package version\n"
" --root=PATH Operate on an alternate filesystem root\n"
@@ -1290,10 +1244,14 @@ static int help(void) {
" --force Overwrite existing files\n"
" --delete-root-password Delete root password\n"
" --welcome=no Disable the welcome text\n"
" --chrome=no Don't show color bar at top and bottom of\n"
" terminal\n"
" --reset Remove existing files\n"
"\nSee the %s for details.\n",
"\nSee the %2$s for details.\n",
program_invocation_short_name,
link);
link,
ansi_highlight(),
ansi_normal());
return 0;
}
@@ -1333,6 +1291,7 @@ static int parse_argv(int argc, char *argv[]) {
ARG_FORCE,
ARG_DELETE_ROOT_PASSWORD,
ARG_WELCOME,
ARG_CHROME,
ARG_RESET,
};
@@ -1370,6 +1329,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "force", no_argument, NULL, ARG_FORCE },
{ "delete-root-password", no_argument, NULL, ARG_DELETE_ROOT_PASSWORD },
{ "welcome", required_argument, NULL, ARG_WELCOME },
{ "chrome", required_argument, NULL, ARG_CHROME },
{ "reset", no_argument, NULL, ARG_RESET },
{}
};
@@ -1579,6 +1539,13 @@ static int parse_argv(int argc, char *argv[]) {
arg_welcome = r;
break;
case ARG_CHROME:
r = parse_boolean_argument("--chrome=", optarg, &arg_chrome);
if (r < 0)
return r;
break;
case ARG_RESET:
arg_reset = true;
break;
@@ -1723,15 +1690,16 @@ static int run(int argc, char *argv[]) {
}
LOG_SET_PREFIX(arg_image ?: arg_root);
DEFER_VOID_CALL(chrome_hide);
/* We check these conditions here instead of in parse_argv() so that we can take the root directory
* into account. */
if (arg_keymap && !keymap_is_ok(rfd, arg_keymap))
if (arg_keymap && !keymap_is_ok(arg_keymap, FD_TO_PTR(rfd)))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Keymap %s is not installed.", arg_keymap);
if (arg_locale && !locale_is_ok(rfd, arg_locale))
if (arg_locale && !locale_is_ok(arg_locale, FD_TO_PTR(rfd)))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale %s is not installed.", arg_locale);
if (arg_locale_messages && !locale_is_ok(rfd, arg_locale_messages))
if (arg_locale_messages && !locale_is_ok(arg_locale_messages, FD_TO_PTR(rfd)))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale %s is not installed.", arg_locale_messages);
if (arg_root_shell) {

View File

@@ -154,6 +154,7 @@ shared_sources = files(
'polkit-agent.c',
'portable-util.c',
'pretty-print.c',
'prompt-util.c',
'ptyfwd.c',
'qrcode-util.c',
'quota-util.c',

View File

@@ -92,3 +92,6 @@ static inline void fflush_and_disable_bufferingp(FILE **p) {
#define WITH_BUFFERED_STDERR \
_WITH_BUFFERED_STREAM(stderr, LONG_LINE_MAX, UNIQ_T(p, UNIQ))
#define WITH_BUFFERED_STDOUT \
_WITH_BUFFERED_STREAM(stdout, LONG_LINE_MAX, UNIQ_T(p, UNIQ))

332
src/shared/prompt-util.c Normal file
View File

@@ -0,0 +1,332 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <unistd.h>
#include "alloc-util.h"
#include "glyph-util.h"
#include "log.h"
#include "macro.h"
#include "os-util.h"
#include "parse-util.h"
#include "pretty-print.h"
#include "prompt-util.h"
#include "string-util.h"
#include "strv.h"
#include "terminal-util.h"
static int get_completions(
const char *key,
char ***ret_list,
void *userdata) {
int r;
assert(ret_list);
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;
}
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') */
unsigned ellipsize_percentage,
size_t n_columns,
size_t column_width,
int (*is_valid)(const char *name, void *userdata),
int (*refresh)(char ***ret_menu, char ***ret_accepted, void *userdata),
void *userdata,
PromptFlags flags,
char **ret) {
_cleanup_strv_free_ char **refreshed_menu = NULL, **refreshed_accepted = NULL;
int r;
assert(text);
assert(ret);
if (!emoji_enabled()) /* If emojis aren't available, simpler unicode chars might still be around,
* hence try to downgrade. (Consider the Linux Console!) */
emoji = GLYPH_TRIANGULAR_BULLET;
/* If requested show menu right-away */
if (FLAGS_SET(flags, PROMPT_SHOW_MENU_NOW) && !strv_isempty(menu)) {
r = show_menu(menu,
n_columns,
column_width,
ellipsize_percentage,
/* grey_prefix= */ NULL,
/* with_numbers= */ true);
if (r < 0)
return log_error_errno(r, "Failed to show menu: %m");
putchar('\n');
}
for (;;) {
_cleanup_free_ char *a = NULL;
if (!FLAGS_SET(flags, PROMPT_HIDE_MENU_HINT) && !strv_isempty(menu))
if (!strextend_with_separator(&a, ", ", "\"list\" to list options"))
return log_oom();
if (!FLAGS_SET(flags, PROMPT_HIDE_SKIP_HINT) && FLAGS_SET(flags, PROMPT_MAY_SKIP))
if (!strextend_with_separator(&a, ", ", "empty to skip"))
return log_oom();
if (a) {
char *b = strjoin(" (", a, ")");
if (!b)
return log_oom();
free_and_replace(a, b);
}
_cleanup_free_ char *p = NULL;
r = ask_string_full(
&p,
get_completions,
accepted ?: menu,
"%s%s%s%s: ",
emoji >= 0 ? glyph(emoji) : "",
emoji >= 0 ? " " : "",
text,
strempty(a));
if (r < 0)
return log_error_errno(r, "Failed to query user: %m");
if (isempty(p)) {
if (FLAGS_SET(flags, PROMPT_MAY_SKIP)) {
log_info("No data entered, skipping.");
*ret = NULL;
return 0;
}
log_info("No data entered, try again.");
continue;
}
/* NB: here we treat non-NULL but empty list different from NULL list. In the former case we
* support the "list" command, in the latter we don't. */
if (FLAGS_SET(flags, PROMPT_SHOW_MENU) && streq(p, "list")) {
putchar('\n');
if (refresh) {
_cleanup_strv_free_ char **rm = NULL, **ra = NULL;
/* If a refresh method is provided, then use it now to refresh the menu
* before redisplaying it. */
r = refresh(&rm, &ra, userdata);
if (r < 0)
return r;
strv_free_and_replace(refreshed_menu, rm);
strv_free_and_replace(refreshed_accepted, ra);
menu = refreshed_menu;
accepted = refreshed_accepted;
}
if (strv_isempty(menu)) {
log_warning("No entries known.");
continue;
}
r = show_menu(menu,
n_columns,
column_width,
ellipsize_percentage,
/* grey_prefix= */ NULL,
/* with_numbers= */ true);
if (r < 0)
return log_error_errno(r, "Failed to show menu: %m");
putchar('\n');
continue;
}
unsigned u;
if (safe_atou(p, &u) >= 0) {
if (u <= 0 || u > strv_length(menu)) {
log_error("Specified entry number out of range.");
continue;
}
log_info("Selected '%s'.", menu[u-1]);
return strdup_to_full(ret, menu[u-1]);
}
bool good = accepted ? strv_contains(accepted, p) : true;
if (good && is_valid) {
r = is_valid(p, userdata);
if (r < 0)
return r;
good = good && r;
}
if (good) {
*ret = TAKE_PTR(p);
return 1;
}
if (!FLAGS_SET(flags, PROMPT_SILENT_VALIDATE)) {
/* Be more helpful to the user, and give a hint what the user might have wanted to type. */
const char *best_match = strv_find_closest(accepted ?: menu, p);
if (best_match)
log_error("Invalid input '%s', did you mean '%s'?", p, best_match);
else
log_error("Invalid input '%s'.", p);
}
}
}
/* Default: bright white on blue background */
#define ANSI_COLOR_CHROME "\x1B[0;44;1;37m"
static unsigned chrome_visible = 0; /* if non-zero chrome is visible and value is saved number of lines */
int chrome_show(
const char *top,
const char *bottom) {
int r;
assert(top);
/* Shows our "chrome", i.e. a blue bar at top and bottom. Reduces the scrolling area to the area in
* between */
if (terminal_is_dumb())
return 0;
unsigned n = lines();
if (n < 12) /* Do not bother with the chrome on tiny screens */
return 0;
_cleanup_free_ char *b = NULL, *ansi_color_reverse = NULL;
if (!bottom) {
_cleanup_free_ char *pretty_name = NULL, *os_name = NULL, *ansi_color = NULL, *documentation_url = NULL;
r = parse_os_release(
/* root= */ NULL,
"PRETTY_NAME", &pretty_name,
"NAME", &os_name,
"ANSI_COLOR", &ansi_color,
"ANSI_COLOR_REVERSE", &ansi_color_reverse,
"DOCUMENTATION_URL", &documentation_url);
if (r < 0)
log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r,
"Failed to read os-release file, ignoring: %m");
const char *m = os_release_pretty_name(pretty_name, os_name);
const char *c = ansi_color ?: "0";
if (ansi_color_reverse) {
_cleanup_free_ char *j = strjoin("\x1B[0;", ansi_color_reverse, "m");
if (!j)
return log_oom_debug();
free_and_replace(ansi_color_reverse, j);
}
if (asprintf(&b, "\x1B[0;%sm %s %s", c, m, ansi_color_reverse ?: ANSI_COLOR_CHROME) < 0)
return log_oom_debug();
if (documentation_url) {
_cleanup_free_ char *u = NULL;
if (terminal_urlify(documentation_url, "documentation", &u) < 0)
return log_oom_debug();
if (!strextend(&b, " - See ", u, " for more information."))
return log_oom_debug();
}
bottom = b;
}
const char *chrome_color = ansi_color_reverse ?: ANSI_COLOR_CHROME;
WITH_BUFFERED_STDOUT;
fputs("\033[H" /* move home */
"\033[2J", /* clear screen */
stdout);
/* Blue bar on top (followed by one empty regular one) */
printf("\x1B[1;1H" /* jump to top left */
"%1$s" ANSI_ERASE_TO_END_OF_LINE "\n"
"%1$s %2$s" ANSI_ERASE_TO_END_OF_LINE "\n"
"%1$s" ANSI_ERASE_TO_END_OF_LINE "\n"
ANSI_NORMAL ANSI_ERASE_TO_END_OF_LINE,
chrome_color,
top);
/* Blue bar on bottom (with one empty regular one before) */
printf("\x1B[%1$u;1H" /* jump to bottom left, above the blue bar */
ANSI_NORMAL ANSI_ERASE_TO_END_OF_LINE "\n"
"%2$s" ANSI_ERASE_TO_END_OF_LINE "\n"
"%2$s %3$s" ANSI_ERASE_TO_END_OF_LINE "\n"
"%2$s" ANSI_ERASE_TO_END_OF_LINE ANSI_NORMAL,
n - 3,
chrome_color,
bottom);
/* Reduce scrolling area (DECSTBM), cutting off top and bottom bars */
printf("\x1B[5;%ur", n - 4);
/* Position cursor in fifth line */
fputs("\x1B[5;1H", stdout);
fflush(stdout);
chrome_visible = n;
return 1;
}
void chrome_hide(void) {
int r;
if (chrome_visible == 0)
return;
unsigned n = chrome_visible;
chrome_visible = 0;
unsigned saved_row = 0;
r = terminal_get_cursor_position(STDIN_FILENO, STDOUT_FILENO, &saved_row, /* ret_column= */ NULL);
if (r < 0)
return (void) log_debug_errno(r, "Failed to get terminal cursor position, skipping chrome hiding: %m");
WITH_BUFFERED_STDOUT;
/* Erase Blue bar on bottom */
assert(n >= 2);
printf("\x1B[%u;1H"
ANSI_NORMAL ANSI_ERASE_TO_END_OF_LINE "\n"
ANSI_NORMAL ANSI_ERASE_TO_END_OF_LINE "\n"
ANSI_NORMAL ANSI_ERASE_TO_END_OF_LINE,
n - 2);
/* Reset scrolling area (DECSTBM) */
fputs("\x1B[r\n", stdout);
/* Place the cursor where it was again, but not in the former blue bars */
assert(n >= 9);
unsigned k = CLAMP(saved_row, 5U, n - 4);
printf("\x1B[%u;1H", k);
fflush(stdout);
}

31
src/shared/prompt-util.h Normal file
View File

@@ -0,0 +1,31 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include <stdbool.h>
#include "forward.h"
typedef enum PromptFlags {
PROMPT_MAY_SKIP = 1 << 0, /* Question may be skipped */
PROMPT_SHOW_MENU = 1 << 1, /* Show menu list on "list" */
PROMPT_SHOW_MENU_NOW = 1 << 2, /* Show menu list right away, rather than only on request */
PROMPT_HIDE_MENU_HINT = 1 << 3, /* Don't show hint regarding "list" */
PROMPT_HIDE_SKIP_HINT = 1 << 4, /* Don't show hint regarding skipping */
PROMPT_SILENT_VALIDATE = 1 << 5, /* The validation log message logs on its own, don't log again */
} PromptFlags;
int prompt_loop(const char *text,
Glyph emoji,
char **menu,
char **accepted,
unsigned ellipsize_percentage,
size_t n_columns,
size_t column_width,
int (*is_valid)(const char *name, void *userdata),
int (*refresh)(char ***ret_menu, char ***ret_accepted, void *userdata),
void *userdata,
PromptFlags flags,
char **ret);
int chrome_show(const char *top, const char *bottom);
void chrome_hide(void);

View File

@@ -81,7 +81,7 @@ TEST(keymaps) {
#define dump_glyph(x) log_info(STRINGIFY(x) ": %s", glyph(x))
TEST(dump_glyphs) {
assert_cc(GLYPH_HOME + 1 == _GLYPH_MAX);
assert_cc(GLYPH_SHELL + 1 == _GLYPH_MAX);
log_info("is_locale_utf8: %s", yes_no(is_locale_utf8()));
@@ -135,6 +135,12 @@ TEST(dump_glyphs) {
dump_glyph(GLYPH_SUPERHERO);
dump_glyph(GLYPH_IDCARD);
dump_glyph(GLYPH_HOME);
dump_glyph(GLYPH_ROCKET);
dump_glyph(GLYPH_BROOM);
dump_glyph(GLYPH_KEYBOARD);
dump_glyph(GLYPH_CLOCK);
dump_glyph(GLYPH_LABEL);
dump_glyph(GLYPH_SHELL);
}
DEFINE_TEST_MAIN(LOG_INFO);

View File

@@ -8,7 +8,7 @@
# (at your option) any later version.
[Unit]
Description=First Boot Wizard
Description=Initial Setup
Documentation=man:systemd-firstboot(1)
ConditionPathIsReadWrite=/etc