mirror of
https://github.com/morgan9e/systemd
synced 2026-04-14 00:14:32 +09:00
firstboot: some love (#39070)
This is split out of #38764. It mostly introduces the "chrome" stuff that puts a blue at the top of bottom of the terminal screen when going through interactive tools such as firstboot, homed-firstboot, and (in future) systemd-sysinstall). it also introduces a generic "prompt_loop()" helper thatn queries the user for some option in a loop until the rsponse matches certain requirements. It's a generalization of a function of the same name that so far only existed in firstboot.c. The more generic version will be reused in a later PR by homed-firstboot and by sysinstall.
This commit is contained in:
@@ -439,15 +439,26 @@
|
||||
<varlistentry>
|
||||
<term><varname>ANSI_COLOR=</varname></term>
|
||||
|
||||
<listitem><para>A suggested presentation color when showing the OS name on the console. This should
|
||||
be specified as string suitable for inclusion in the ESC [ m ANSI/ECMA-48 escape code for setting
|
||||
graphical rendition. This field is optional.</para>
|
||||
<listitem><para>A suggested presentation (foreground) text color when showing the OS name on the
|
||||
console. This should be specified as string suitable for inclusion in the ESC [ m ANSI/ECMA-48
|
||||
escape code for setting graphical rendition. This field is optional.</para>
|
||||
|
||||
<para>Examples: <literal>ANSI_COLOR="0;31"</literal> for red, <literal>ANSI_COLOR="1;34"</literal>
|
||||
for light blue, or <literal>ANSI_COLOR="0;38;2;60;110;180"</literal> for Fedora blue.
|
||||
</para></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>ANSI_COLOR_REVERSE=</varname></term>
|
||||
|
||||
<listitem><para>Similar to <varname>ANSI_COLOR=</varname>, but should encode the desired
|
||||
presentation color as background color, along with a suitable foreground color. This is may be used
|
||||
by console applications to set off "chrome" UI elements from the main terminal contents. This field
|
||||
is optional.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v259"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>VENDOR_NAME=</varname></term>
|
||||
|
||||
|
||||
@@ -344,6 +344,16 @@
|
||||
<xi:include href="version-info.xml" xpointer="v246"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><option>--chrome=</option></term>
|
||||
|
||||
<listitem><para>Takes a boolean argument. By default the initial setup scren will show reverse color
|
||||
"chrome" bars at the top and and the bottom of the terminal screen, which may be disabled by setting
|
||||
this option to false.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v259"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<xi:include href="standard-options.xml" xpointer="help" />
|
||||
<xi:include href="standard-options.xml" xpointer="version" />
|
||||
</variablelist>
|
||||
|
||||
@@ -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("🐚"),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -92,3 +92,6 @@ static inline void fflush_and_disable_bufferingp(FILE **p) {
|
||||
|
||||
#define WITH_BUFFERED_STDERR \
|
||||
_WITH_BUFFERED_STREAM(stderr, LONG_LINE_MAX, UNIQ_T(p, UNIQ))
|
||||
|
||||
#define WITH_BUFFERED_STDOUT \
|
||||
_WITH_BUFFERED_STREAM(stdout, LONG_LINE_MAX, UNIQ_T(p, UNIQ))
|
||||
|
||||
332
src/shared/prompt-util.c
Normal file
332
src/shared/prompt-util.c
Normal file
@@ -0,0 +1,332 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include "alloc-util.h"
|
||||
#include "glyph-util.h"
|
||||
#include "log.h"
|
||||
#include "macro.h"
|
||||
#include "os-util.h"
|
||||
#include "parse-util.h"
|
||||
#include "pretty-print.h"
|
||||
#include "prompt-util.h"
|
||||
#include "string-util.h"
|
||||
#include "strv.h"
|
||||
#include "terminal-util.h"
|
||||
|
||||
static int get_completions(
|
||||
const char *key,
|
||||
char ***ret_list,
|
||||
void *userdata) {
|
||||
|
||||
int r;
|
||||
|
||||
assert(ret_list);
|
||||
|
||||
if (!userdata) {
|
||||
*ret_list = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
_cleanup_strv_free_ char **copy = strv_copy(userdata);
|
||||
if (!copy)
|
||||
return -ENOMEM;
|
||||
|
||||
r = strv_extend(©, "list");
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
*ret_list = TAKE_PTR(copy);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int prompt_loop(
|
||||
const char *text,
|
||||
Glyph emoji,
|
||||
char **menu, /* if non-NULL: choices to suggest */
|
||||
char **accepted, /* if non-NULL: choices to accept (should be a superset of 'menu') */
|
||||
unsigned ellipsize_percentage,
|
||||
size_t n_columns,
|
||||
size_t column_width,
|
||||
int (*is_valid)(const char *name, void *userdata),
|
||||
int (*refresh)(char ***ret_menu, char ***ret_accepted, void *userdata),
|
||||
void *userdata,
|
||||
PromptFlags flags,
|
||||
char **ret) {
|
||||
|
||||
_cleanup_strv_free_ char **refreshed_menu = NULL, **refreshed_accepted = NULL;
|
||||
int r;
|
||||
|
||||
assert(text);
|
||||
assert(ret);
|
||||
|
||||
if (!emoji_enabled()) /* If emojis aren't available, simpler unicode chars might still be around,
|
||||
* hence try to downgrade. (Consider the Linux Console!) */
|
||||
emoji = GLYPH_TRIANGULAR_BULLET;
|
||||
|
||||
/* If requested show menu right-away */
|
||||
if (FLAGS_SET(flags, PROMPT_SHOW_MENU_NOW) && !strv_isempty(menu)) {
|
||||
r = show_menu(menu,
|
||||
n_columns,
|
||||
column_width,
|
||||
ellipsize_percentage,
|
||||
/* grey_prefix= */ NULL,
|
||||
/* with_numbers= */ true);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to show menu: %m");
|
||||
|
||||
putchar('\n');
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
_cleanup_free_ char *a = NULL;
|
||||
|
||||
if (!FLAGS_SET(flags, PROMPT_HIDE_MENU_HINT) && !strv_isempty(menu))
|
||||
if (!strextend_with_separator(&a, ", ", "\"list\" to list options"))
|
||||
return log_oom();
|
||||
if (!FLAGS_SET(flags, PROMPT_HIDE_SKIP_HINT) && FLAGS_SET(flags, PROMPT_MAY_SKIP))
|
||||
if (!strextend_with_separator(&a, ", ", "empty to skip"))
|
||||
return log_oom();
|
||||
|
||||
if (a) {
|
||||
char *b = strjoin(" (", a, ")");
|
||||
if (!b)
|
||||
return log_oom();
|
||||
|
||||
free_and_replace(a, b);
|
||||
}
|
||||
|
||||
_cleanup_free_ char *p = NULL;
|
||||
r = ask_string_full(
|
||||
&p,
|
||||
get_completions,
|
||||
accepted ?: menu,
|
||||
"%s%s%s%s: ",
|
||||
emoji >= 0 ? glyph(emoji) : "",
|
||||
emoji >= 0 ? " " : "",
|
||||
text,
|
||||
strempty(a));
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to query user: %m");
|
||||
|
||||
if (isempty(p)) {
|
||||
if (FLAGS_SET(flags, PROMPT_MAY_SKIP)) {
|
||||
log_info("No data entered, skipping.");
|
||||
*ret = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
log_info("No data entered, try again.");
|
||||
continue;
|
||||
}
|
||||
|
||||
/* NB: here we treat non-NULL but empty list different from NULL list. In the former case we
|
||||
* support the "list" command, in the latter we don't. */
|
||||
if (FLAGS_SET(flags, PROMPT_SHOW_MENU) && streq(p, "list")) {
|
||||
putchar('\n');
|
||||
|
||||
if (refresh) {
|
||||
_cleanup_strv_free_ char **rm = NULL, **ra = NULL;
|
||||
|
||||
/* If a refresh method is provided, then use it now to refresh the menu
|
||||
* before redisplaying it. */
|
||||
r = refresh(&rm, &ra, userdata);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
strv_free_and_replace(refreshed_menu, rm);
|
||||
strv_free_and_replace(refreshed_accepted, ra);
|
||||
|
||||
menu = refreshed_menu;
|
||||
accepted = refreshed_accepted;
|
||||
}
|
||||
|
||||
if (strv_isempty(menu)) {
|
||||
log_warning("No entries known.");
|
||||
continue;
|
||||
}
|
||||
|
||||
r = show_menu(menu,
|
||||
n_columns,
|
||||
column_width,
|
||||
ellipsize_percentage,
|
||||
/* grey_prefix= */ NULL,
|
||||
/* with_numbers= */ true);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to show menu: %m");
|
||||
|
||||
putchar('\n');
|
||||
continue;
|
||||
}
|
||||
|
||||
unsigned u;
|
||||
if (safe_atou(p, &u) >= 0) {
|
||||
if (u <= 0 || u > strv_length(menu)) {
|
||||
log_error("Specified entry number out of range.");
|
||||
continue;
|
||||
}
|
||||
|
||||
log_info("Selected '%s'.", menu[u-1]);
|
||||
return strdup_to_full(ret, menu[u-1]);
|
||||
}
|
||||
|
||||
bool good = accepted ? strv_contains(accepted, p) : true;
|
||||
if (good && is_valid) {
|
||||
r = is_valid(p, userdata);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
good = good && r;
|
||||
}
|
||||
if (good) {
|
||||
*ret = TAKE_PTR(p);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!FLAGS_SET(flags, PROMPT_SILENT_VALIDATE)) {
|
||||
/* Be more helpful to the user, and give a hint what the user might have wanted to type. */
|
||||
const char *best_match = strv_find_closest(accepted ?: menu, p);
|
||||
if (best_match)
|
||||
log_error("Invalid input '%s', did you mean '%s'?", p, best_match);
|
||||
else
|
||||
log_error("Invalid input '%s'.", p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Default: bright white on blue background */
|
||||
#define ANSI_COLOR_CHROME "\x1B[0;44;1;37m"
|
||||
|
||||
static unsigned chrome_visible = 0; /* if non-zero chrome is visible and value is saved number of lines */
|
||||
|
||||
int chrome_show(
|
||||
const char *top,
|
||||
const char *bottom) {
|
||||
int r;
|
||||
|
||||
assert(top);
|
||||
|
||||
/* Shows our "chrome", i.e. a blue bar at top and bottom. Reduces the scrolling area to the area in
|
||||
* between */
|
||||
|
||||
if (terminal_is_dumb())
|
||||
return 0;
|
||||
|
||||
unsigned n = lines();
|
||||
if (n < 12) /* Do not bother with the chrome on tiny screens */
|
||||
return 0;
|
||||
|
||||
_cleanup_free_ char *b = NULL, *ansi_color_reverse = NULL;
|
||||
if (!bottom) {
|
||||
_cleanup_free_ char *pretty_name = NULL, *os_name = NULL, *ansi_color = NULL, *documentation_url = NULL;
|
||||
|
||||
r = parse_os_release(
|
||||
/* root= */ NULL,
|
||||
"PRETTY_NAME", &pretty_name,
|
||||
"NAME", &os_name,
|
||||
"ANSI_COLOR", &ansi_color,
|
||||
"ANSI_COLOR_REVERSE", &ansi_color_reverse,
|
||||
"DOCUMENTATION_URL", &documentation_url);
|
||||
if (r < 0)
|
||||
log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r,
|
||||
"Failed to read os-release file, ignoring: %m");
|
||||
|
||||
const char *m = os_release_pretty_name(pretty_name, os_name);
|
||||
const char *c = ansi_color ?: "0";
|
||||
|
||||
if (ansi_color_reverse) {
|
||||
_cleanup_free_ char *j = strjoin("\x1B[0;", ansi_color_reverse, "m");
|
||||
if (!j)
|
||||
return log_oom_debug();
|
||||
|
||||
free_and_replace(ansi_color_reverse, j);
|
||||
}
|
||||
|
||||
if (asprintf(&b, "\x1B[0;%sm %s %s", c, m, ansi_color_reverse ?: ANSI_COLOR_CHROME) < 0)
|
||||
return log_oom_debug();
|
||||
|
||||
if (documentation_url) {
|
||||
_cleanup_free_ char *u = NULL;
|
||||
if (terminal_urlify(documentation_url, "documentation", &u) < 0)
|
||||
return log_oom_debug();
|
||||
|
||||
if (!strextend(&b, " - See ", u, " for more information."))
|
||||
return log_oom_debug();
|
||||
}
|
||||
|
||||
bottom = b;
|
||||
}
|
||||
|
||||
const char *chrome_color = ansi_color_reverse ?: ANSI_COLOR_CHROME;
|
||||
|
||||
WITH_BUFFERED_STDOUT;
|
||||
|
||||
fputs("\033[H" /* move home */
|
||||
"\033[2J", /* clear screen */
|
||||
stdout);
|
||||
|
||||
/* Blue bar on top (followed by one empty regular one) */
|
||||
printf("\x1B[1;1H" /* jump to top left */
|
||||
"%1$s" ANSI_ERASE_TO_END_OF_LINE "\n"
|
||||
"%1$s %2$s" ANSI_ERASE_TO_END_OF_LINE "\n"
|
||||
"%1$s" ANSI_ERASE_TO_END_OF_LINE "\n"
|
||||
ANSI_NORMAL ANSI_ERASE_TO_END_OF_LINE,
|
||||
chrome_color,
|
||||
top);
|
||||
|
||||
/* Blue bar on bottom (with one empty regular one before) */
|
||||
printf("\x1B[%1$u;1H" /* jump to bottom left, above the blue bar */
|
||||
ANSI_NORMAL ANSI_ERASE_TO_END_OF_LINE "\n"
|
||||
"%2$s" ANSI_ERASE_TO_END_OF_LINE "\n"
|
||||
"%2$s %3$s" ANSI_ERASE_TO_END_OF_LINE "\n"
|
||||
"%2$s" ANSI_ERASE_TO_END_OF_LINE ANSI_NORMAL,
|
||||
n - 3,
|
||||
chrome_color,
|
||||
bottom);
|
||||
|
||||
/* Reduce scrolling area (DECSTBM), cutting off top and bottom bars */
|
||||
printf("\x1B[5;%ur", n - 4);
|
||||
|
||||
/* Position cursor in fifth line */
|
||||
fputs("\x1B[5;1H", stdout);
|
||||
|
||||
fflush(stdout);
|
||||
|
||||
chrome_visible = n;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void chrome_hide(void) {
|
||||
int r;
|
||||
|
||||
if (chrome_visible == 0)
|
||||
return;
|
||||
|
||||
unsigned n = chrome_visible;
|
||||
chrome_visible = 0;
|
||||
|
||||
unsigned saved_row = 0;
|
||||
r = terminal_get_cursor_position(STDIN_FILENO, STDOUT_FILENO, &saved_row, /* ret_column= */ NULL);
|
||||
if (r < 0)
|
||||
return (void) log_debug_errno(r, "Failed to get terminal cursor position, skipping chrome hiding: %m");
|
||||
|
||||
WITH_BUFFERED_STDOUT;
|
||||
|
||||
/* Erase Blue bar on bottom */
|
||||
assert(n >= 2);
|
||||
printf("\x1B[%u;1H"
|
||||
ANSI_NORMAL ANSI_ERASE_TO_END_OF_LINE "\n"
|
||||
ANSI_NORMAL ANSI_ERASE_TO_END_OF_LINE "\n"
|
||||
ANSI_NORMAL ANSI_ERASE_TO_END_OF_LINE,
|
||||
n - 2);
|
||||
|
||||
/* Reset scrolling area (DECSTBM) */
|
||||
fputs("\x1B[r\n", stdout);
|
||||
|
||||
/* Place the cursor where it was again, but not in the former blue bars */
|
||||
assert(n >= 9);
|
||||
unsigned k = CLAMP(saved_row, 5U, n - 4);
|
||||
printf("\x1B[%u;1H", k);
|
||||
|
||||
fflush(stdout);
|
||||
}
|
||||
31
src/shared/prompt-util.h
Normal file
31
src/shared/prompt-util.h
Normal file
@@ -0,0 +1,31 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "forward.h"
|
||||
|
||||
typedef enum PromptFlags {
|
||||
PROMPT_MAY_SKIP = 1 << 0, /* Question may be skipped */
|
||||
PROMPT_SHOW_MENU = 1 << 1, /* Show menu list on "list" */
|
||||
PROMPT_SHOW_MENU_NOW = 1 << 2, /* Show menu list right away, rather than only on request */
|
||||
PROMPT_HIDE_MENU_HINT = 1 << 3, /* Don't show hint regarding "list" */
|
||||
PROMPT_HIDE_SKIP_HINT = 1 << 4, /* Don't show hint regarding skipping */
|
||||
PROMPT_SILENT_VALIDATE = 1 << 5, /* The validation log message logs on its own, don't log again */
|
||||
} PromptFlags;
|
||||
|
||||
int prompt_loop(const char *text,
|
||||
Glyph emoji,
|
||||
char **menu,
|
||||
char **accepted,
|
||||
unsigned ellipsize_percentage,
|
||||
size_t n_columns,
|
||||
size_t column_width,
|
||||
int (*is_valid)(const char *name, void *userdata),
|
||||
int (*refresh)(char ***ret_menu, char ***ret_accepted, void *userdata),
|
||||
void *userdata,
|
||||
PromptFlags flags,
|
||||
char **ret);
|
||||
|
||||
int chrome_show(const char *top, const char *bottom);
|
||||
void chrome_hide(void);
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user