diff --git a/man/os-release.xml b/man/os-release.xml index 4bbb87b1bf..0c9b3de493 100644 --- a/man/os-release.xml +++ b/man/os-release.xml @@ -439,15 +439,26 @@ ANSI_COLOR= - 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. + 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. Examples: ANSI_COLOR="0;31" for red, ANSI_COLOR="1;34" for light blue, or ANSI_COLOR="0;38;2;60;110;180" for Fedora blue. + + ANSI_COLOR_REVERSE= + + Similar to ANSI_COLOR=, 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. + + + + VENDOR_NAME= diff --git a/man/systemd-firstboot.xml b/man/systemd-firstboot.xml index 2af354f872..449fe4ec4a 100644 --- a/man/systemd-firstboot.xml +++ b/man/systemd-firstboot.xml @@ -344,6 +344,16 @@ + + + + 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. + + + + diff --git a/src/basic/glyph-util.c b/src/basic/glyph-util.c index 5b4e32f35a..f540a68473 100644 --- a/src/basic/glyph-util.c +++ b/src/basic/glyph-util.c @@ -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("๐Ÿš"), }, }; diff --git a/src/basic/glyph-util.h b/src/basic/glyph-util.h index b1c90d00f6..92b325726b 100644 --- a/src/basic/glyph-util.h +++ b/src/basic/glyph-util.h @@ -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; diff --git a/src/basic/macro.h b/src/basic/macro.h index 3ddc5272b8..b53805d5b8 100644 --- a/src/basic/macro.h +++ b/src/basic/macro.h @@ -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) diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index 2175523e20..b7de72daed 100644 --- a/src/basic/terminal-util.c +++ b/src/basic/terminal-util.c @@ -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, diff --git a/src/basic/terminal-util.h b/src/basic/terminal-util.h index 6428d9a147..d18d33a181 100644 --- a/src/basic/terminal-util.h +++ b/src/basic/terminal-util.h @@ -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); diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index 5ed6d3a9d2..f92011b614 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -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(©, "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) { diff --git a/src/shared/meson.build b/src/shared/meson.build index a734c86891..9bf9f9b6bf 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -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', diff --git a/src/shared/pretty-print.h b/src/shared/pretty-print.h index 56491886fa..8ea711a09d 100644 --- a/src/shared/pretty-print.h +++ b/src/shared/pretty-print.h @@ -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)) diff --git a/src/shared/prompt-util.c b/src/shared/prompt-util.c new file mode 100644 index 0000000000..927ef0770e --- /dev/null +++ b/src/shared/prompt-util.c @@ -0,0 +1,332 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#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(©, "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); +} diff --git a/src/shared/prompt-util.h b/src/shared/prompt-util.h new file mode 100644 index 0000000000..06dd2c7f0c --- /dev/null +++ b/src/shared/prompt-util.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#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); diff --git a/src/test/test-locale-util.c b/src/test/test-locale-util.c index e264cff5dd..53ab3fcb81 100644 --- a/src/test/test-locale-util.c +++ b/src/test/test-locale-util.c @@ -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); diff --git a/units/systemd-firstboot.service b/units/systemd-firstboot.service index 31f02a8e00..ce6f984f93 100644 --- a/units/systemd-firstboot.service +++ b/units/systemd-firstboot.service @@ -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