From 788d37d7d27dbf6c26cb82ecafe48525504fa810 Mon Sep 17 00:00:00 2001 From: Alberto Fanjul Date: Sun, 13 Nov 2022 11:29:16 +0100 Subject: [PATCH] CLI commands autocompletion --- src/ctl/ctl-cli.c | 321 +++++++++++++++++++++++++++++++++++++++++++++- src/ctl/ctl.h | 13 ++ src/ctl/sinkctl.c | 37 ++++-- src/ctl/wifictl.c | 38 ++++-- 4 files changed, 386 insertions(+), 23 deletions(-) diff --git a/src/ctl/ctl-cli.c b/src/ctl/ctl-cli.c index 3e77921..15ce621 100644 --- a/src/ctl/ctl-cli.c +++ b/src/ctl/ctl-cli.c @@ -36,9 +36,6 @@ #include #include -/* *sigh* readline doesn't include all their deps, so put them last */ -#include -#include /* * Helpers for interactive commands @@ -246,7 +243,10 @@ static void cli_handler_fn(char *input) else if (!r) return; - add_history(original); + if (!(strcmp(original, "quit") == 0 || strcmp(original, "exit") == 0)) { + add_history(original); + write_history(get_history_filename()); + } r = cli_do(cli_cmds, args, r); if (r != -EAGAIN) return; @@ -325,6 +325,315 @@ void cli_destroy(void) cli_event = NULL; } +char *yes_no_options[] = {"yes", "no", NULL}; + +char * +yes_no_generator (const char *text, int state) +{ + static int list_index, len; + char *name; + + /* If this is a new word to complete, initialize now. This includes + saving the length of TEXT for efficiency, and initializing the index + variable to 0. */ + if (!state) + { + list_index = 0; + len = strlen (text); + } + + /* Return the next name which partially matches from the command list. */ + while (name = yes_no_options[list_index]) + { + list_index++; + + if (strncmp (name, text, len) == 0) + return (strdup(name)); + } + + /* If no names matched, then return NULL. */ + return ((char *)NULL); +} + +char * +links_peers_generator (const char *text, int state) +{ + static int list_index, len; + char *name; + size_t peer_cnt = 0; + size_t link_cnt = 0; + struct shl_dlist *i, *j; + struct ctl_link *l; + struct ctl_peer *p; + + /* If this is a new word to complete, initialize now. This includes + saving the length of TEXT for efficiency, and initializing the index + variable to 0. */ + if (!state) + { + list_index = 0; + len = strlen (text); + } + + shl_dlist_for_each(i, &get_wifi()->links) { + l = link_from_dlist(i); + + char *name = l->label; + if (strncmp (name, text, len) == 0) + { + if (link_cnt == list_index) + { + list_index++; + return strdup(name); + } + link_cnt++; + } + + name = l->friendly_name; + if (!shl_isempty(name)) + { + if (strncmp (name, text, len) == 0) + { + if (link_cnt == list_index) + { + list_index++; + return strdup(name); + } + link_cnt++; + } + } + } + + peer_cnt = link_cnt; + + shl_dlist_for_each(i, &get_wifi()->links) { + l = link_from_dlist(i); + + shl_dlist_for_each(j, &l->peers) { + p = peer_from_dlist(j); + char *name = p->label; + if (strncmp (name, text, len) == 0) + { + if (peer_cnt == list_index) + { + list_index++; + return strdup(name); + } + peer_cnt++; + } + name = p->friendly_name; + if (!shl_isempty(name)) + { + if (strncmp (name, text, len) == 0) + { + if (peer_cnt == list_index) + { + list_index++; + return strdup(name); + } + peer_cnt++; + } + } + } + } + /* If no names matched, then return NULL. */ + return ((char *)NULL); +} + +char * +peers_generator (const char *text, int state) +{ + static int list_index, len; + char *name; + size_t peer_cnt = 0; + struct shl_dlist *i, *j; + struct ctl_link *l; + struct ctl_peer *p; + + /* If this is a new word to complete, initialize now. This includes + saving the length of TEXT for efficiency, and initializing the index + variable to 0. */ + if (!state) + { + list_index = 0; + len = strlen (text); + } + + shl_dlist_for_each(i, &get_wifi()->links) { + l = link_from_dlist(i); + + shl_dlist_for_each(j, &l->peers) { + p = peer_from_dlist(j); + char *name = p->label; + if (strncmp (name, text, len) == 0) + { + if (peer_cnt == list_index) + { + list_index++; + return strdup(name); + } + peer_cnt++; + } + name = p->friendly_name; + if (!shl_isempty(name)) + { + if (strncmp (name, text, len) == 0) + { + if (peer_cnt == list_index) + { + list_index++; + return strdup(name); + } + peer_cnt++; + } + } + } + } + /* If no names matched, then return NULL. */ + return ((char *)NULL); +} + +char * +links_generator (const char *text, int state) +{ + static int list_index, len; + char *name; + size_t link_cnt = 0; + struct shl_dlist *i; + struct ctl_link *l; + + + /* If this is a new word to complete, initialize now. This includes + saving the length of TEXT for efficiency, and initializing the index + variable to 0. */ + if (!state) + { + list_index = 0; + len = strlen (text); + } + + shl_dlist_for_each(i, &get_wifi()->links) { + l = link_from_dlist(i); + + + char *name = l->label; + if (strncmp (name, text, len) == 0) + { + if (link_cnt == list_index) + { + list_index++; + return strdup(name); + } + link_cnt++; + } + name = l->friendly_name; + if (!shl_isempty(name)) + { + if (strncmp (name, text, len) == 0) + { + if (link_cnt == list_index) + { + list_index++; + return strdup(name); + } + link_cnt++; + } + } + } + /* If no names matched, then return NULL. */ + return ((char *)NULL); +} + +/* Generator function for command completion. STATE lets us know whether + * to start from scratch; without any state (i.e. STATE == 0), then we + * start at the top of the list. + */ + +char * +command_generator (const char *text, int state) +{ + static int list_index, len; + char *name; + + /* If this is a new word to complete, initialize now. This includes + saving the length of TEXT for efficiency, and initializing the index + variable to 0. */ + if (!state) + { + list_index = 0; + len = strlen (text); + } + + /* Return the next name which partially matches from the command list. */ + while (name = cli_cmds[list_index].cmd) + { + list_index++; + + if (strncmp (name, text, len) == 0) + return (strdup(name)); + } + + /* If no names matched, then return NULL. */ + return ((char *)NULL); +} + +int get_args(char* line) +{ + char* tmp = line; + char* last_delim = tmp; + int count = 0; + + /* Count how many elements will be extracted. */ + while (*tmp) + { + if (' ' == *tmp) + { + if (last_delim+1 < tmp) + count++; + last_delim = tmp; + } + tmp++; + } + if (" " != *last_delim) + count++; + return count; +} + +/* + * Attempt to complete on the contents of TEXT. START and END bound the + * region of rl_line_buffer that contains the word to complete. TEXT is + * the word to complete. We can use the entire contents of rl_line_buffer + * in case we want to do some simple parsing. Return the array of matches, + * or NULL if there aren't any. + */ +char ** +completion_fn (const char *text, int start, int end) +{ + char **matches; + + rl_attempted_completion_over = 1; + if (start == 0) + matches = rl_completion_matches (text, command_generator); + else + { + matches = (char **)NULL; + struct cli_cmd cmd; + int cmd_pos = 0; + while ((cmd = cli_cmds[cmd_pos++]).cmd) + { + if (strncmp(cmd.cmd, rl_line_buffer, strlen(cmd.cmd)) == 0) + { + int nargs = get_args(rl_line_buffer); + rl_compentry_func_t* completion_fn = cmd.completion_fns[nargs-2]; + if (completion_fn) + matches = rl_completion_matches (text, completion_fn); + } + } + } + + return (matches); +} + int cli_init(sd_bus *bus, const struct cli_cmd *cmds) { static const int sigs[] = { @@ -382,7 +691,11 @@ int cli_init(sd_bus *bus, const struct cli_cmd *cmds) cli_rl = true; rl_erase_empty_line = 1; + rl_attempted_completion_function = completion_fn; rl_callback_handler_install(NULL, cli_handler_fn); + using_history(); + read_history(get_history_filename()); + rl_end_of_history(0, 0); rl_set_prompt(CLI_PROMPT); printf("\r"); diff --git a/src/ctl/ctl.h b/src/ctl/ctl.h index 2ddf8fb..69b0db8 100644 --- a/src/ctl/ctl.h +++ b/src/ctl/ctl.h @@ -29,6 +29,11 @@ #include "shl_dlist.h" #include "shl_log.h" +/* *sigh* readline doesn't include all their deps, so put them last */ +#include +#include +#include + #ifndef CTL_CTL_H #define CTL_CTL_H @@ -36,6 +41,13 @@ struct ctl_wifi; struct ctl_link; struct ctl_peer; +char* get_history_filename (); +struct ctl_wifi * get_wifi (); +char * links_peers_generator (const char *text, int state); +char * links_generator (const char *text, int state); +char * peers_generator (const char *text, int state); +char * yes_no_generator (const char *text, int state); + /* wifi handling */ struct ctl_peer { @@ -209,6 +221,7 @@ struct cli_cmd { int argc; int (*fn) (char **args, unsigned int n); const char *desc; + rl_compentry_func_t *completion_fns[2]; }; extern sd_event *cli_event; diff --git a/src/ctl/sinkctl.c b/src/ctl/sinkctl.c index eb932aa..fde3e9f 100644 --- a/src/ctl/sinkctl.c +++ b/src/ctl/sinkctl.c @@ -47,6 +47,10 @@ #include "util.h" #include "config.h" +#include + +#define HISTORY_FILENAME ".miracle-sink.history" + static sd_bus *bus; static struct ctl_wifi *wifi; static struct ctl_sink *sink; @@ -78,6 +82,21 @@ unsigned int wfd_supported_res_cea = 0x0001ffff; unsigned int wfd_supported_res_vesa = 0x1fffffff; unsigned int wfd_supported_res_hh = 0x00001fff; +struct ctl_wifi *get_wifi() +{ + return wifi; +} + + +/* + * get history filename + */ + +char* get_history_filename() +{ + return HISTORY_FILENAME; +} + /* * cmd list */ @@ -412,15 +431,15 @@ static int sink_timeout_fn(sd_event_source *s, uint64_t usec, void *data) } static const struct cli_cmd cli_cmds[] = { - { "list", NULL, CLI_M, CLI_LESS, 0, cmd_list, "List all objects" }, - { "show", "", CLI_M, CLI_LESS, 1, cmd_show, "Show detailed object information" }, - { "run", "", CLI_M, CLI_EQUAL, 1, cmd_run, "Run sink on given link" }, - { "bind", "", CLI_M, CLI_EQUAL, 1, cmd_bind, "Like 'run' but bind the link name to run when it is hotplugged" }, - { "set-friendly-name", "[link] ", CLI_M, CLI_LESS, 2, cmd_set_friendly_name, "Set friendly name of an object" }, - { "set-managed", " ", CLI_M, CLI_EQUAL, 2, cmd_set_managed, "Manage or unmnage a link" }, - { "quit", NULL, CLI_Y, CLI_MORE, 0, cmd_quit, "Quit program" }, - { "exit", NULL, CLI_Y, CLI_MORE, 0, cmd_quit, NULL }, - { "help", NULL, CLI_M, CLI_MORE, 0, NULL, "Print help" }, + { "list", NULL, CLI_M, CLI_LESS, 0, cmd_list, "List all objects", {NULL} }, + { "show", "", CLI_M, CLI_LESS, 1, cmd_show, "Show detailed object information", {links_peers_generator, NULL} }, + { "run", "", CLI_M, CLI_EQUAL, 1, cmd_run, "Run sink on given link", {links_generator, NULL} }, + { "bind", "", CLI_M, CLI_EQUAL, 1, cmd_bind, "Like 'run' but bind the link name to run when it is hotplugged", {links_generator, NULL} }, + { "set-friendly-name", "[link] ", CLI_M, CLI_LESS, 2, cmd_set_friendly_name, "Set friendly name of an object", {links_generator, NULL} }, + { "set-managed", " ", CLI_M, CLI_EQUAL, 2, cmd_set_managed, "Manage or unmnage a link", {links_generator, yes_no_generator, NULL} }, + { "quit", NULL, CLI_Y, CLI_MORE, 0, cmd_quit, "Quit program", {NULL} }, + { "exit", NULL, CLI_Y, CLI_MORE, 0, cmd_quit, NULL, {NULL} }, + { "help", NULL, CLI_M, CLI_MORE, 0, NULL, "Print help", {NULL} }, { }, }; diff --git a/src/ctl/wifictl.c b/src/ctl/wifictl.c index 5b511fb..9720b75 100644 --- a/src/ctl/wifictl.c +++ b/src/ctl/wifictl.c @@ -34,11 +34,29 @@ #include "util.h" #include "config.h" +#include + +#define HISTORY_FILENAME ".miracle-wifi.history" + static sd_bus *bus; static struct ctl_wifi *wifi; static struct ctl_link *selected_link; +/* + * get history filename + */ + +char* get_history_filename() +{ + return HISTORY_FILENAME; +} + +struct ctl_wifi *get_wifi() +{ + return wifi; +} + /* * cmd list */ @@ -386,17 +404,17 @@ static int cmd_quit(char **args, unsigned int n) */ static const struct cli_cmd cli_cmds[] = { - { "list", NULL, CLI_M, CLI_LESS, 0, cmd_list, "List all objects" }, - { "select", "[link]", CLI_Y, CLI_LESS, 1, cmd_select, "Select default link" }, - { "show", "[link|peer]", CLI_M, CLI_LESS, 1, cmd_show, "Show detailed object information" }, - { "set-friendly-name", "[link] ", CLI_M, CLI_LESS, 2, cmd_set_friendly_name, "Set friendly name of an object" }, + { "list", NULL, CLI_M, CLI_LESS, 0, cmd_list, "List all objects", {NULL}}, + { "select", "[link]", CLI_Y, CLI_LESS, 1, cmd_select, "Select default link", {links_generator, NULL} }, + { "show", "[link|peer]", CLI_M, CLI_LESS, 1, cmd_show, "Show detailed object information", {links_peers_generator, NULL} }, + { "set-friendly-name", "[link] ", CLI_M, CLI_LESS, 2, cmd_set_friendly_name, "Set friendly name of an object", {links_generator, yes_no_generator, NULL} }, { "set-managed", "[link] ", CLI_M, CLI_LESS, 2, cmd_set_managed, "Manage or unmnage a link" }, - { "p2p-scan", "[link] [stop]", CLI_Y, CLI_LESS, 2, cmd_p2p_scan, "Control neighborhood P2P scanning" }, - { "connect", " [provision] [pin]", CLI_M, CLI_LESS, 3, cmd_connect, "Connect to peer" }, - { "disconnect", "", CLI_M, CLI_EQUAL, 1, cmd_disconnect, "Disconnect from peer" }, - { "quit", NULL, CLI_Y, CLI_MORE, 0, cmd_quit, "Quit program" }, - { "exit", NULL, CLI_Y, CLI_MORE, 0, cmd_quit, NULL }, - { "help", NULL, CLI_M, CLI_MORE, 0, NULL, "Print help" }, + { "p2p-scan", "[link] [stop]", CLI_Y, CLI_LESS, 2, cmd_p2p_scan, "Control neighborhood P2P scanning", {links_generator, NULL} }, + { "connect", " [provision] [pin]", CLI_M, CLI_LESS, 3, cmd_connect, "Connect to peer", {peers_generator, NULL} }, + { "disconnect", "", CLI_M, CLI_EQUAL, 1, cmd_disconnect, "Disconnect from peer", {peers_generator, NULL} }, + { "quit", NULL, CLI_Y, CLI_MORE, 0, cmd_quit, "Quit program", {NULL} }, + { "exit", NULL, CLI_Y, CLI_MORE, 0, cmd_quit, NULL , {NULL}}, + { "help", NULL, CLI_M, CLI_MORE, 0, NULL, "Print help" , {NULL} }, { }, };