terminal-util: tweaks for show_menu() (#42822)
This commit is contained in:
+12
-10
@@ -529,8 +529,8 @@ int show_menu(char **x,
|
||||
const char *grey_prefix,
|
||||
bool with_numbers) {
|
||||
|
||||
assert(x);
|
||||
assert(n_columns > 0);
|
||||
assert(column_width > 0);
|
||||
|
||||
if (n_columns == SIZE_MAX)
|
||||
n_columns = 3;
|
||||
@@ -542,18 +542,20 @@ int show_menu(char **x,
|
||||
size_t column_max = (columns()-1) / n_columns;
|
||||
|
||||
/* Subtract room for numbers */
|
||||
if (with_numbers && column_max > 6)
|
||||
column_max -= 6;
|
||||
if (with_numbers)
|
||||
column_max = LESS_BY(column_max, 6U);
|
||||
|
||||
/* If columns would get too tight let's make this a linear list instead. */
|
||||
if (column_max < 10 && widest > 10) {
|
||||
n_columns = 1;
|
||||
column_max = columns()-1;
|
||||
|
||||
if (with_numbers && column_max > 6)
|
||||
column_max -= 6;
|
||||
if (with_numbers)
|
||||
column_max = LESS_BY(column_max, 6U);
|
||||
}
|
||||
|
||||
/* Never make this narrower than 10 characters */
|
||||
column_max = MAX(column_max, 10U);
|
||||
column_width = CLAMP(widest+1, 10U, column_max);
|
||||
}
|
||||
|
||||
@@ -564,8 +566,7 @@ int show_menu(char **x,
|
||||
if (break_lines > 2)
|
||||
break_lines--;
|
||||
|
||||
/* The first page gets two extra lines, since we want to show
|
||||
* a title */
|
||||
/* The first page gets two extra lines, since we want to show a title */
|
||||
size_t break_modulo = break_lines;
|
||||
if (break_modulo > 3)
|
||||
break_modulo -= 3;
|
||||
@@ -575,17 +576,18 @@ int show_menu(char **x,
|
||||
for (size_t j = 0; j < n_columns; j++) {
|
||||
_cleanup_free_ char *e = NULL;
|
||||
|
||||
if (j * per_column + i >= n)
|
||||
size_t p = j * per_column + i;
|
||||
if (p >= n)
|
||||
break;
|
||||
|
||||
e = ellipsize(x[j * per_column + i], column_width, ellipsize_percentage);
|
||||
e = ellipsize(x[p], column_width, ellipsize_percentage);
|
||||
if (!e)
|
||||
return -ENOMEM;
|
||||
|
||||
if (with_numbers)
|
||||
printf("%s%4zu)%s ",
|
||||
ansi_grey(),
|
||||
j * per_column + i + 1,
|
||||
p + 1,
|
||||
ansi_normal());
|
||||
|
||||
if (grey_prefix && startswith(e, grey_prefix)) {
|
||||
|
||||
@@ -9,11 +9,15 @@
|
||||
|
||||
#include "alloc-util.h"
|
||||
#include "ansi-color.h"
|
||||
#include "env-util.h"
|
||||
#include "errno-util.h"
|
||||
#include "fd-util.h"
|
||||
#include "fileio.h"
|
||||
#include "memfd-util.h"
|
||||
#include "path-util.h"
|
||||
#include "process-util.h"
|
||||
#include "stat-util.h"
|
||||
#include "string-util.h"
|
||||
#include "strv.h"
|
||||
#include "terminal-util.h"
|
||||
#include "tests.h"
|
||||
@@ -417,4 +421,157 @@ TEST(terminal_new_session) {
|
||||
}
|
||||
}
|
||||
|
||||
static void show_menu_capture(
|
||||
char **menu,
|
||||
size_t n_columns,
|
||||
size_t column_width,
|
||||
const char *grey_prefix,
|
||||
bool with_numbers,
|
||||
const char *columns_env,
|
||||
char **ret) {
|
||||
|
||||
int r;
|
||||
|
||||
/* Runs show_menu() in a forked child whose stdout is connected to a memfd, so we can capture its
|
||||
* output verbatim and byte-compare it. The child sets $COLUMNS explicitly so we can exercise
|
||||
* show_menu() at various terminal widths regardless of the actual terminal the test runs on. */
|
||||
|
||||
_cleanup_close_ int mfd = ASSERT_OK(memfd_new("test-show-menu"));
|
||||
|
||||
r = pidref_safe_fork_full(
|
||||
"test-show-menu",
|
||||
(int[]) { -EBADF, mfd, STDERR_FILENO },
|
||||
/* except_fds= */ NULL, /* n_except_fds= */ 0,
|
||||
FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL|FORK_LOG|FORK_WAIT|FORK_REARRANGE_STDIO|FORK_FLUSH_STDIO,
|
||||
/* ret= */ NULL);
|
||||
ASSERT_OK(r);
|
||||
if (r == 0) {
|
||||
/* Child: stdout is now the memfd. */
|
||||
|
||||
ASSERT_OK(set_unset_env("COLUMNS", columns_env, /* overwrite= */ true));
|
||||
|
||||
/* Pin $LINES to a large value so the "press any key to proceed" pager (which would block on
|
||||
* stdin) is never reached, and turn off colors so the captured output stays free of ANSI
|
||||
* escape sequences. */
|
||||
ASSERT_OK_ERRNO(setenv("LINES", "1000", /* overwrite= */ true));
|
||||
ASSERT_OK_ERRNO(setenv("SYSTEMD_COLORS", "0", /* overwrite= */ true));
|
||||
reset_terminal_feature_caches();
|
||||
|
||||
ASSERT_OK(show_menu(menu, n_columns, column_width, /* ellipsize_percentage= */ 50, grey_prefix, with_numbers));
|
||||
ASSERT_OK_ZERO_ERRNO(fflush(stdout));
|
||||
|
||||
_exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
/* Parent: read back whatever the child wrote. The memfd's file offset is shared with the child's
|
||||
* dup'd fd, so rewind before reading. */
|
||||
_cleanup_fclose_ FILE *f = ASSERT_NOT_NULL(fdopen(TAKE_FD(mfd), "re"));
|
||||
rewind(f);
|
||||
|
||||
_cleanup_free_ char *content = NULL;
|
||||
ASSERT_OK(read_full_stream(f, &content, /* ret_size= */ NULL));
|
||||
|
||||
log_info("=== show_menu(n_columns=%zu, column_width=%zu, grey_prefix=%s, with_numbers=%s, COLUMNS=%s) ===\n%s",
|
||||
n_columns, column_width, strnull(grey_prefix), yes_no(with_numbers), strnull(columns_env), content);
|
||||
|
||||
if (ret)
|
||||
*ret = TAKE_PTR(content);
|
||||
}
|
||||
|
||||
TEST(show_menu) {
|
||||
char **menu = STRV_MAKE("alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf");
|
||||
|
||||
/* NULL list: must not crash (the assert(x) was dropped) and must produce no output at all. */
|
||||
FOREACH_STRING(cols, "200", "80", "10", "1") {
|
||||
_cleanup_free_ char *content = NULL;
|
||||
show_menu_capture(/* menu= */ NULL, /* n_columns= */ 3, /* column_width= */ SIZE_MAX,
|
||||
/* grey_prefix= */ NULL, /* with_numbers= */ true, /* columns_env= */ cols, &content);
|
||||
ASSERT_STREQ(content, "");
|
||||
}
|
||||
|
||||
/* Empty (but non-NULL) list: also no output. */
|
||||
{
|
||||
_cleanup_free_ char *content = NULL;
|
||||
show_menu_capture(/* menu= */ STRV_EMPTY, /* n_columns= */ 3, /* column_width= */ SIZE_MAX,
|
||||
/* grey_prefix= */ NULL, /* with_numbers= */ false, /* columns_env= */ "80", &content);
|
||||
ASSERT_STREQ(content, "");
|
||||
}
|
||||
|
||||
/* The longest entry in 'menu' is 7 cells wide, so the column width is always clamped up to the 10-cell
|
||||
* minimum, no matter how wide or narrow $COLUMNS is — every width must yield byte-identical output.
|
||||
* This pins down the multi-column layout (3 columns, column-major numbering 1..7), the "never narrower
|
||||
* than 10" clamp and the LESS_BY() underflow guard at absurdly tiny widths. */
|
||||
static const char menu_expected[] =
|
||||
" 1) alpha 4) delta 7) golf \n"
|
||||
" 2) bravo 5) echo \n"
|
||||
" 3) charlie 6) foxtrot \n";
|
||||
|
||||
FOREACH_STRING(cols, "200", "80", "40", "20", "10", "5", "1") {
|
||||
_cleanup_free_ char *content = NULL;
|
||||
show_menu_capture(menu, /* n_columns= */ 3, /* column_width= */ SIZE_MAX,
|
||||
/* grey_prefix= */ NULL, /* with_numbers= */ true, /* columns_env= */ cols, &content);
|
||||
ASSERT_STREQ(content, menu_expected);
|
||||
}
|
||||
|
||||
/* $COLUMNS unset: stdout is a memfd (not a tty), so columns() falls back to its 80-column default,
|
||||
* which gives the very same layout. */
|
||||
{
|
||||
_cleanup_free_ char *content = NULL;
|
||||
show_menu_capture(menu, /* n_columns= */ 3, /* column_width= */ SIZE_MAX,
|
||||
/* grey_prefix= */ NULL, /* with_numbers= */ true, /* columns_env= */ NULL, &content);
|
||||
ASSERT_STREQ(content, menu_expected);
|
||||
}
|
||||
|
||||
/* Single column, no numbers: one entry per row, each padded to the 10-cell minimum. */
|
||||
{
|
||||
_cleanup_free_ char *content = NULL;
|
||||
show_menu_capture(menu, /* n_columns= */ 1, /* column_width= */ SIZE_MAX,
|
||||
/* grey_prefix= */ NULL, /* with_numbers= */ false, /* columns_env= */ "200", &content);
|
||||
ASSERT_STREQ(content,
|
||||
"alpha \n"
|
||||
"bravo \n"
|
||||
"charlie \n"
|
||||
"delta \n"
|
||||
"echo \n"
|
||||
"foxtrot \n"
|
||||
"golf \n");
|
||||
}
|
||||
|
||||
/* A menu whose longest entry (14 cells) exceeds the 10-cell minimum, so the column width now tracks
|
||||
* the content (widest+1 == 15). At a wide terminal it stays a 3-column grid... */
|
||||
char **menu2 = STRV_MAKE("fourteen-chars", "short", "two");
|
||||
{
|
||||
_cleanup_free_ char *content = NULL;
|
||||
show_menu_capture(menu2, /* n_columns= */ 3, /* column_width= */ SIZE_MAX,
|
||||
/* grey_prefix= */ NULL, /* with_numbers= */ true, /* columns_env= */ "200", &content);
|
||||
ASSERT_STREQ(content,
|
||||
" 1) fourteen-chars 2) short 3) two \n");
|
||||
}
|
||||
|
||||
/* ...but at a narrow terminal show_menu() falls back to a single linear column (n_columns forced to
|
||||
* 1), still wide enough to print every entry in full. */
|
||||
{
|
||||
_cleanup_free_ char *content = NULL;
|
||||
show_menu_capture(menu2, /* n_columns= */ 3, /* column_width= */ SIZE_MAX,
|
||||
/* grey_prefix= */ NULL, /* with_numbers= */ true, /* columns_env= */ "30", &content);
|
||||
ASSERT_STREQ(content,
|
||||
" 1) fourteen-chars \n"
|
||||
" 2) short \n"
|
||||
" 3) two \n");
|
||||
}
|
||||
|
||||
/* Explicit column width (the COLUMNS-derived path is skipped entirely) together with a grey prefix:
|
||||
* matching entries get the prefix printed via the grey branch, non-matching ones don't. The captured
|
||||
* bytes are identical either way since colors are disabled. */
|
||||
char **prefixed = STRV_MAKE("net.foo", "net.bar", "other", "net.baz");
|
||||
{
|
||||
_cleanup_free_ char *content = NULL;
|
||||
show_menu_capture(prefixed, /* n_columns= */ 2, /* column_width= */ 20,
|
||||
/* grey_prefix= */ "net.", /* with_numbers= */ true, /* columns_env= */ "200", &content);
|
||||
ASSERT_STREQ(content,
|
||||
" 1) net.foo 3) other \n"
|
||||
" 2) net.bar 4) net.baz \n");
|
||||
}
|
||||
}
|
||||
|
||||
DEFINE_TEST_MAIN(LOG_INFO);
|
||||
|
||||
Reference in New Issue
Block a user