From ae59185f6fa0cb7876f4cb73c80461e9689fb097 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Mon, 27 Sep 2021 07:33:19 +1000 Subject: [PATCH] pw-cli: use readline() in interactive mode With history and a simple command completion hook this makes the interactive mode a lot easier to deal with. --- meson.build | 1 + src/tools/meson.build | 9 ++- src/tools/pw-cli.c | 125 ++++++++++++++++++++++++++++++------------ 3 files changed, 100 insertions(+), 35 deletions(-) diff --git a/meson.build b/meson.build index 56599ebd1..e6cc10155 100644 --- a/meson.build +++ b/meson.build @@ -325,6 +325,7 @@ pthread_lib = dependency('threads') dbus_dep = dependency('dbus-1') sdl_dep = dependency('sdl2', required : get_option('sdl2')) summary({'SDL 2': sdl_dep.found()}, bool_yn: true, section: 'Misc dependencies') +readline_dep = dependency('readline', required : false) ncurses_dep = dependency('ncursesw', required : false) sndfile_dep = dependency('sndfile', version : '>= 1.0.20', required : get_option('sndfile')) summary({'sndfile': sndfile_dep.found()}, bool_yn: true, section: 'pw-cat/pw-play/pw-dump/filter-chain') diff --git a/src/tools/meson.build b/src/tools/meson.build index c7c6e8e19..dfed27e10 100644 --- a/src/tools/meson.build +++ b/src/tools/meson.build @@ -1,6 +1,5 @@ tools_sources = [ [ 'pw-mon', [ 'pw-mon.c' ] ], - [ 'pw-cli', [ 'pw-cli.c' ] ], [ 'pw-dot', [ 'pw-dot.c' ] ], [ 'pw-dump', [ 'pw-dump.c' ] ], [ 'pw-profiler', [ 'pw-profiler.c' ] ], @@ -18,6 +17,14 @@ foreach t : tools_sources ) endforeach +if readline_dep.found() + executable('pw-cli', + 'pw-cli.c', + install: true, + dependencies: [pipewire_dep, readline_dep] + ) +endif + if ncurses_dep.found() executable('pw-top', 'pw-top.c', diff --git a/src/tools/pw-cli.c b/src/tools/pw-cli.c index a043df5af..7cf75a072 100644 --- a/src/tools/pw-cli.c +++ b/src/tools/pw-cli.c @@ -32,6 +32,8 @@ #include #endif #include +#include +#include #define spa_debug(...) fprintf(stdout,__VA_ARGS__);fputc('\n', stdout) @@ -48,6 +50,7 @@ #include static const char WHITESPACE[] = " \t"; +static char prompt[64]; struct remote_data; @@ -279,11 +282,10 @@ static void on_core_info(void *_data, const struct pw_core_info *info) fprintf(stdout, "remote %d is named '%s'\n", rd->id, rd->name); } -static void show_prompt(struct remote_data *rd) +static void set_prompt(struct remote_data *rd) { - rd->data->monitoring = true; - fprintf(stdout, "%s>>", rd->name); - fflush(stdout); + snprintf(prompt, sizeof(prompt), "%s>> ", rd->name); + rl_set_prompt(prompt); } static void on_core_done(void *_data, uint32_t id, int seq) @@ -292,10 +294,12 @@ static void on_core_done(void *_data, uint32_t id, int seq) struct data *d = rd->data; if (seq == rd->prompt_pending) { - if (d->interactive) - show_prompt(rd); - else + if (d->interactive) { + set_prompt(rd); + rd->data->monitoring = true; + } else { pw_main_loop_quit(d->loop); + } } } @@ -2880,7 +2884,7 @@ usage: return false; } -static bool parse(struct data *data, char *buf, size_t size, char **error) +static bool parse(struct data *data, char *buf, char **error) { char *a[2]; int n; @@ -2912,35 +2916,35 @@ static bool parse(struct data *data, char *buf, size_t size, char **error) return false; } -static void do_input(void *data, int fd, uint32_t mask) +/* We need a global variable, readline doesn't have a closure arg */ +static struct data *readline_dataptr; + +static void readline_process_line(char *line) { - struct data *d = data; - char buf[4096], *error; - ssize_t r; + struct data *d = readline_dataptr; + char *error; - if (mask & SPA_IO_IN) { - while (true) { - r = read(fd, buf, sizeof(buf)-1); - if (r < 0) { - if (errno == EAGAIN) - continue; - perror("read"); - r = 0; - break; - } - break; - } - if (r == 0) { - fprintf(stdout, "\n"); - pw_main_loop_quit(d->loop); - return; - } - buf[r] = '\0'; + if (!line) + line = strdup("quit"); - if (!parse(d, buf, r, &error)) { + if (line[0] != '\0') { + add_history(line); + if (!parse(d, line, &error)) { fprintf(stdout, "Error: \"%s\"\n", error); free(error); } + } + free(line); +} + +static void do_input(void *data, int fd, uint32_t mask) +{ + struct data *d = data; + + if (mask & SPA_IO_IN) { + readline_dataptr = d; + rl_callback_read_char(); + if (d->current == NULL) pw_main_loop_quit(d->loop); else { @@ -2951,6 +2955,55 @@ static void do_input(void *data, int fd, uint32_t mask) } } +static char * +readline_match_command(const char *text, int state) +{ + static size_t idx; + static int len; + + if (!state) { + idx = 0; + len = strlen(text); + } + + while (idx < SPA_N_ELEMENTS(command_list)) { + const char *name = command_list[idx].name; + const char *alias = command_list[idx].alias; + + idx++; + if (spa_strneq(name, text, len) || spa_strneq(alias, text, len)) + return strdup(name); + } + + return NULL; +} + +static char ** +readline_command_completion(const char *text, int start, int end) +{ + char **matches = NULL; + + /* Only try to complete the first word in a line */ + if (start == 0) + matches = rl_completion_matches(text, readline_match_command); + + /* Don't fall back to filename completion */ + rl_attempted_completion_over = true; + + return matches; +} + +static void readline_init() +{ + rl_attempted_completion_function = readline_command_completion; + rl_callback_handler_install(">> ", readline_process_line); +} + +static void readline_cleanup() +{ + rl_callback_handler_remove(); +} + static void do_quit_on_signal(void *data, int signal_number) { struct data *d = data; @@ -3046,12 +3099,16 @@ int main(int argc, char *argv[]) if (optind == argc) { data.interactive = true; - pw_loop_add_io(l, STDIN_FILENO, SPA_IO_IN|SPA_IO_HUP, false, do_input, &data); - fprintf(stdout, "Welcome to PipeWire version %s. Type 'help' for usage.\n", pw_get_library_version()); + readline_init(); + + pw_loop_add_io(l, STDIN_FILENO, SPA_IO_IN|SPA_IO_HUP, false, do_input, &data); + pw_main_loop_run(data.loop); + + readline_cleanup(); } else { char buf[4096], *p, *error; @@ -3063,7 +3120,7 @@ int main(int argc, char *argv[]) pw_main_loop_run(data.loop); - if (!parse(&data, buf, p - buf, &error)) { + if (!parse(&data, buf, &error)) { fprintf(stdout, "Error: \"%s\"\n", error); free(error); }