From 74b8ab014b12807d7ca68ee617cf54ad5c6553c5 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 Aug 2025 15:17:13 +0200 Subject: [PATCH 1/9] glyph-util: add more emojis --- src/basic/glyph-util.c | 13 ++++++++++++- src/basic/glyph-util.h | 6 ++++++ src/test/test-locale-util.c | 8 +++++++- 3 files changed, 25 insertions(+), 2 deletions(-) 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/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); From fa350969ab10669ff2467359ebf785ce0be39fd9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 28 Aug 2025 13:41:24 +0200 Subject: [PATCH 2/9] prompt-util: add generic prompt loop implementation This is a generalization of the logic in systemd-firstboot. This also ports over firstboot.c to make use of the new generalization. --- src/firstboot/firstboot.c | 231 +++++++++++++++----------------------- src/shared/meson.build | 1 + src/shared/prompt-util.c | 191 +++++++++++++++++++++++++++++++ src/shared/prompt-util.h | 28 +++++ 4 files changed, 311 insertions(+), 140 deletions(-) create mode 100644 src/shared/prompt-util.c create mode 100644 src/shared/prompt-util.h diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index 5ed6d3a9d2..e311cb9fa7 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" @@ -144,102 +145,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 +214,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 +278,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 +381,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 +421,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 +501,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 +533,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 +609,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 +628,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 +736,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 +808,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 +837,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) { @@ -1727,11 +1678,11 @@ static int run(int argc, char *argv[]) { /* 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/prompt-util.c b/src/shared/prompt-util.c new file mode 100644 index 0000000000..42ddf19189 --- /dev/null +++ b/src/shared/prompt-util.c @@ -0,0 +1,191 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "glyph-util.h" +#include "log.h" +#include "macro.h" +#include "parse-util.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); + } + } +} diff --git a/src/shared/prompt-util.h b/src/shared/prompt-util.h new file mode 100644 index 0000000000..179f056b53 --- /dev/null +++ b/src/shared/prompt-util.h @@ -0,0 +1,28 @@ +/* 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); From 30aeab7883975330ed8deda7a785845e6b347f76 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 Aug 2025 23:15:45 +0200 Subject: [PATCH 3/9] terminal-util: add terminal_get_cursor_position() helper --- src/basic/terminal-util.c | 331 +++++++++++++++++++++++++------------- src/basic/terminal-util.h | 1 + 2 files changed, 221 insertions(+), 111 deletions(-) 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); From 32e0af083541b8b980499d3e285fea0f4a8baad0 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 Aug 2025 23:16:01 +0200 Subject: [PATCH 4/9] pretty-print: add WITH_BUFFERED_STDOUT() helper --- src/shared/pretty-print.h | 3 +++ 1 file changed, 3 insertions(+) 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)) From 71f3f1818f0e1cba565c6a7dde2ca1390a31ed6a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 Aug 2025 23:19:35 +0200 Subject: [PATCH 5/9] macro: add simple DEFER_VOID_CALL() helper --- src/basic/macro.h | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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) From 8191bbd23cb85ed92c7021a03dd6fe370bcfb9ff Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 Aug 2025 23:24:33 +0200 Subject: [PATCH 6/9] prompt-util: add helpers that paint some "chrome" on top/bottom of screen We'll soon have three different kind of interactive "wizard"-like console UIs: systemd-firstboot, homectl firstboot and soon systemd-sysinstall. Let's give them a limited, recognizable visual identity, to distinguish them from the usual console output: let's add a bit of "chrome" to the top and bottom of the screen, that we show during ther wizards, but hide again afterwards. This makes use of the DECSTBM sequence that reduces the scrolling area by chopping off blocks from the top or bottom of the screen. The sequence is quite standard, given it has been part of VT100 already. xterm, vte, Linux console all support it just fine. --- man/os-release.xml | 17 ++++- src/shared/prompt-util.c | 141 +++++++++++++++++++++++++++++++++++++++ src/shared/prompt-util.h | 3 + 3 files changed, 158 insertions(+), 3 deletions(-) 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/src/shared/prompt-util.c b/src/shared/prompt-util.c index 42ddf19189..5c3fa98a92 100644 --- a/src/shared/prompt-util.c +++ b/src/shared/prompt-util.c @@ -1,10 +1,14 @@ /* 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" @@ -189,3 +193,140 @@ int prompt_loop( } } } + +/* 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 chrom 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 ANSI_NORMAL, + 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 index 179f056b53..06dd2c7f0c 100644 --- a/src/shared/prompt-util.h +++ b/src/shared/prompt-util.h @@ -26,3 +26,6 @@ int prompt_loop(const char *text, void *userdata, PromptFlags flags, char **ret); + +int chrome_show(const char *top, const char *bottom); +void chrome_hide(void); From 73ee723aa77bfb9f9988b80228b646d65d1770b2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 11 Sep 2025 11:25:00 +0200 Subject: [PATCH 7/9] firstboot: show blue "chrome" bar at top --- man/systemd-firstboot.xml | 10 ++++++++++ src/firstboot/firstboot.c | 25 ++++++++++++++++++++----- src/shared/prompt-util.c | 4 ++-- 3 files changed, 32 insertions(+), 7 deletions(-) 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/firstboot/firstboot.c b/src/firstboot/firstboot.c index e311cb9fa7..b7b09ba875 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -85,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); @@ -114,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, @@ -125,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()) { @@ -1241,6 +1244,8 @@ 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", program_invocation_short_name, @@ -1284,6 +1289,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_FORCE, ARG_DELETE_ROOT_PASSWORD, ARG_WELCOME, + ARG_CHROME, ARG_RESET, }; @@ -1321,6 +1327,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 }, {} }; @@ -1530,6 +1537,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; @@ -1674,6 +1688,7 @@ 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. */ diff --git a/src/shared/prompt-util.c b/src/shared/prompt-util.c index 5c3fa98a92..927ef0770e 100644 --- a/src/shared/prompt-util.c +++ b/src/shared/prompt-util.c @@ -213,7 +213,7 @@ int chrome_show( return 0; unsigned n = lines(); - if (n < 12) /* Do not bother with the chrom on tiny screens */ + if (n < 12) /* Do not bother with the chrome on tiny screens */ return 0; _cleanup_free_ char *b = NULL, *ansi_color_reverse = NULL; @@ -317,7 +317,7 @@ void chrome_hide(void) { 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 ANSI_NORMAL, + ANSI_NORMAL ANSI_ERASE_TO_END_OF_LINE, n - 2); /* Reset scrolling area (DECSTBM) */ From 875a618ed39102f28f605fc36c3b713175bfa0df Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 11 Sep 2025 11:25:13 +0200 Subject: [PATCH 8/9] firstboot: modernize --help output --- src/firstboot/firstboot.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index b7b09ba875..f92011b614 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -1208,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" @@ -1247,9 +1247,11 @@ static int help(void) { " --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; } From d7b966ad13e569decd768ad7e6b525425a0f93fa Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 11 Sep 2025 21:12:15 +0200 Subject: [PATCH 9/9] firstboot: don't call this thing a "wizard" It isn't really, it's an initial setup tool, which is what GNOME calls their equivalent too. --- units/systemd-firstboot.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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