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