#include "constants.h"
#include "devnum-util.h"
#include "env-util.h"
+#include "errno-list.h"
#include "fd-util.h"
#include "fileio.h"
#include "fs-util.h"
}
}
-int ask_string(char **ret, const char *text, ...) {
- _cleanup_free_ char *line = NULL;
+typedef enum CompletionResult{
+ COMPLETION_ALREADY, /* the input string is already complete */
+ COMPLETION_FULL, /* completed the input string to be complete now */
+ COMPLETION_PARTIAL, /* completed the input string so that is still incomplete */
+ COMPLETION_NONE, /* found no matching completion */
+ _COMPLETION_RESULT_MAX,
+ _COMPLETION_RESULT_INVALID = -EINVAL,
+ _COMPLETION_RESULT_ERRNO_MAX = -ERRNO_MAX,
+} CompletionResult;
+
+static CompletionResult pick_completion(const char *string, char *const*completions, char **ret) {
+ _cleanup_free_ char *found = NULL;
+ bool partial = false;
+
+ string = strempty(string);
+
+ STRV_FOREACH(c, completions) {
+
+ /* Ignore entries that are not actually completions */
+ if (!startswith(*c, string))
+ continue;
+
+ /* Store first completion that matches */
+ if (!found) {
+ found = strdup(*c);
+ if (!found)
+ return -ENOMEM;
+
+ continue;
+ }
+
+ /* If there's another completion that works truncate the one we already found by common
+ * prefix */
+ size_t n = str_common_prefix(found, *c);
+ if (n == SIZE_MAX)
+ continue;
+
+ found[n] = 0;
+ partial = true;
+ }
+
+ *ret = TAKE_PTR(found);
+
+ if (!*ret)
+ return COMPLETION_NONE;
+ if (partial)
+ return COMPLETION_PARTIAL;
+
+ return streq(string, *ret) ? COMPLETION_ALREADY : COMPLETION_FULL;
+}
+
+static void clear_by_backspace(size_t n) {
+ /* Erase the specified number of character cells backwards on the terminal */
+ for (size_t i = 0; i < n; i++)
+ fputs("\b \b", stdout);
+}
+
+int ask_string_full(
+ char **ret,
+ GetCompletionsCallback get_completions,
+ void *userdata,
+ const char *text, ...) {
+
va_list ap;
int r;
assert(ret);
assert(text);
+ /* Output the prompt */
fputs(ansi_highlight(), stdout);
-
va_start(ap, text);
vprintf(text, ap);
va_end(ap);
-
fputs(ansi_normal(), stdout);
-
fflush(stdout);
- r = read_line(stdin, LONG_LINE_MAX, &line);
+ _cleanup_free_ char *string = NULL;
+ size_t n = 0;
+
+ /* Do interactive logic only if stdin + stdout are connected to the same place. And yes, we could use
+ * STDIN_FILENO and STDOUT_FILENO here, but let's be overly correct for once, after all libc allows
+ * swapping out stdin/stdout. */
+ int fd_input = fileno(stdin);
+ int fd_output = fileno(stdout);
+ if (fd_input < 0 || fd_output < 0 || same_fd(fd_input, fd_output) <= 0)
+ goto fallback;
+
+ /* Try to disable echo, which also tells us if this even is a terminal */
+ struct termios old_termios;
+ if (tcgetattr(fd_input, &old_termios) < 0)
+ goto fallback;
+
+ struct termios new_termios = old_termios;
+ termios_disable_echo(&new_termios);
+ if (tcsetattr(fd_input, TCSADRAIN, &new_termios) < 0)
+ return -errno;
+
+ for (;;) {
+ int c = fgetc(stdin);
+
+ /* On EOF or NUL, end the request, don't output anything anymore */
+ if (IN_SET(c, EOF, 0))
+ break;
+
+ /* On Return also end the request, but make this visible */
+ if (IN_SET(c, '\n', '\r')) {
+ fputc('\n', stdout);
+ break;
+ }
+
+ if (c == '\t') {
+ /* Tab */
+
+ _cleanup_strv_free_ char **completions = NULL;
+ if (get_completions) {
+ r = get_completions(string, &completions, userdata);
+ if (r < 0)
+ goto fail;
+ }
+
+ _cleanup_free_ char *new_string = NULL;
+ CompletionResult cr = pick_completion(string, completions, &new_string);
+ if (cr < 0) {
+ r = cr;
+ goto fail;
+ }
+ if (IN_SET(cr, COMPLETION_PARTIAL, COMPLETION_FULL)) {
+ /* Output the new suffix we learned */
+ fputs(ASSERT_PTR(startswith(new_string, strempty(string))), stdout);
+
+ /* And update the whole string */
+ free_and_replace(string, new_string);
+ n = strlen(string);
+ }
+ if (cr == COMPLETION_NONE)
+ fputc('\a', stdout); /* BEL */
+
+ if (IN_SET(cr, COMPLETION_PARTIAL, COMPLETION_ALREADY)) {
+ /* If this worked only partially, or if the user hit TAB even though we were
+ * complete already, then show the remaining options (in the latter case just
+ * the one). */
+ fputc('\n', stdout);
+
+ _cleanup_strv_free_ char **filtered = strv_filter_prefix(completions, string);
+ if (!filtered) {
+ r = -ENOMEM;
+ goto fail;
+ }
+
+ r = show_menu(filtered,
+ /* n_columns= */ SIZE_MAX,
+ /* column_width= */ SIZE_MAX,
+ /* ellipsize_percentage= */ 0,
+ /* grey_prefix=*/ string,
+ /* with_numbers= */ false);
+ if (r < 0)
+ goto fail;
+
+ /* Show the prompt again */
+ fputs(ansi_highlight(), stdout);
+ va_start(ap, text);
+ vprintf(text, ap);
+ va_end(ap);
+ fputs(ansi_normal(), stdout);
+ fputs(string, stdout);
+ }
+
+ } else if (IN_SET(c, '\b', 127)) {
+ /* Backspace */
+
+ if (n == 0)
+ fputc('\a', stdout); /* BEL */
+ else {
+ size_t m = utf8_last_length(string, n);
+
+ char *e = string + n - m;
+ clear_by_backspace(utf8_console_width(e));
+
+ *e = 0;
+ n -= m;
+ }
+
+ } else if (c == 21) {
+ /* Ctrl-u → erase all input */
+
+ clear_by_backspace(utf8_console_width(string));
+ string[n = 0] = 0;
+
+ } else if (c == 4) {
+ /* Ctrl-d → cancel this field input */
+
+ r = -ECANCELED;
+ goto fail;
+
+ } else if (char_is_cc(c) || n >= LINE_MAX)
+ /* refuse control characters and too long strings */
+ fputc('\a', stdout); /* BEL */
+ else {
+ /* Regular char */
+
+ if (!GREEDY_REALLOC(string, n+2)) {
+ r = -ENOMEM;
+ goto fail;
+ }
+
+ string[n++] = (char) c;
+ string[n] = 0;
+
+ fputc(c, stdout);
+ }
+
+ fflush(stdout);
+ }
+
+ if (tcsetattr(fd_input, TCSADRAIN, &old_termios) < 0)
+ return -errno;
+
+ if (!string) {
+ string = strdup("");
+ if (!string)
+ return -ENOMEM;
+ }
+
+ *ret = TAKE_PTR(string);
+ return 0;
+
+fail:
+ (void) tcsetattr(fd_input, TCSADRAIN, &old_termios);
+ return r;
+
+fallback:
+ /* A simple fallback without TTY magic */
+ r = read_line(stdin, LONG_LINE_MAX, &string);
if (r < 0)
return r;
if (r == 0)
return -EIO;
- *ret = TAKE_PTR(line);
+ *ret = TAKE_PTR(string);
return 0;
}