#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"
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;
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) {
} 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;
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) {
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) {
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);
}
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) {
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);
}
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;
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();
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;
}
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) {
/* 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) {
--- /dev/null
+/* 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);
+ }
+ }
+}