Enable user-defined actions
Also fix pclose() handling within Info plugins, and prevent them from screwing up the terminal with error output on initialization. This is still rather crude, but at least it's possible.
This commit is contained in:
parent
eec0706333
commit
641803df35
3
NEWS
3
NEWS
|
@ -2,6 +2,9 @@ Unreleased
|
||||||
|
|
||||||
* Made global search indicate the search terms, and match on filenames
|
* Made global search indicate the search terms, and match on filenames
|
||||||
|
|
||||||
|
* Added ability to configure bindable user-defined actions;
|
||||||
|
these can launch arbitrary shell commands
|
||||||
|
|
||||||
* X11: added support for font fallbacks to the editor as well
|
* X11: added support for font fallbacks to the editor as well
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# nncmpp.actions.awk: produce C code for a list of user actions
|
# nncmpp.actions.awk: produce C code for a list of user actions
|
||||||
#
|
#
|
||||||
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
|
# Copyright (c) 2022 - 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||||
# SPDX-License-Identifier: 0BSD
|
# SPDX-License-Identifier: 0BSD
|
||||||
#
|
#
|
||||||
# Usage: env LC_ALL=C A=0 B=1 awk -f nncmpp.actions.awk \
|
# Usage: env LC_ALL=C A=0 B=1 awk -f nncmpp.actions.awk \
|
||||||
|
@ -91,7 +91,7 @@ END {
|
||||||
print "enum action {"
|
print "enum action {"
|
||||||
for (i in Constants)
|
for (i in Constants)
|
||||||
print "\t" "ACTION_" Constants[i] ","
|
print "\t" "ACTION_" Constants[i] ","
|
||||||
print "\t" "ACTION_COUNT"
|
print "\t" "ACTION_USER_0"
|
||||||
print "};"
|
print "};"
|
||||||
print ""
|
print ""
|
||||||
print "static const char *g_action_names[] = {"
|
print "static const char *g_action_names[] = {"
|
||||||
|
|
12
nncmpp.adoc
12
nncmpp.adoc
|
@ -85,6 +85,18 @@ To adjust key bindings, put them within a *normal* or *editor* object.
|
||||||
Run *nncmpp* with the *--debug* option to find out key combinations names.
|
Run *nncmpp* with the *--debug* option to find out key combinations names.
|
||||||
Press *?* in the help tab to learn the action identifiers to use.
|
Press *?* in the help tab to learn the action identifiers to use.
|
||||||
|
|
||||||
|
You may also define and bind your own actions, launching arbitrary
|
||||||
|
shell commands. Note that you cannot override internal actions in this manner.
|
||||||
|
|
||||||
|
....
|
||||||
|
actions = {
|
||||||
|
"pioneer-on-off" = {
|
||||||
|
description = "Pioneer amplifier: turn on/off"
|
||||||
|
command = "elksmart-comm --nec A538"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
....
|
||||||
|
|
||||||
Spectrum visualiser
|
Spectrum visualiser
|
||||||
-------------------
|
-------------------
|
||||||
When built against the FFTW library, *nncmpp* can make use of MPD's "fifo"
|
When built against the FFTW library, *nncmpp* can make use of MPD's "fifo"
|
||||||
|
|
215
nncmpp.c
215
nncmpp.c
|
@ -1256,6 +1256,9 @@ static struct app_context
|
||||||
struct config config; ///< Program configuration
|
struct config config; ///< Program configuration
|
||||||
struct strv streams; ///< List of "name NUL URI NUL"
|
struct strv streams; ///< List of "name NUL URI NUL"
|
||||||
struct strv enqueue; ///< Items to enqueue once connected
|
struct strv enqueue; ///< Items to enqueue once connected
|
||||||
|
struct strv action_names; ///< User-defined action names
|
||||||
|
struct strv action_descriptions; ///< User-defined action descriptions
|
||||||
|
struct strv action_commands; ///< User-defined action commands
|
||||||
|
|
||||||
struct tab *help_tab; ///< Special help tab
|
struct tab *help_tab; ///< Special help tab
|
||||||
struct tab *tabs; ///< All other tabs
|
struct tab *tabs; ///< All other tabs
|
||||||
|
@ -1410,6 +1413,17 @@ static const struct config_schema g_config_colors[] =
|
||||||
{}
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static const struct config_schema g_config_actions[] =
|
||||||
|
{
|
||||||
|
{ .name = "description",
|
||||||
|
.comment = "Human-readable description of the action",
|
||||||
|
.type = CONFIG_ITEM_STRING },
|
||||||
|
{ .name = "command",
|
||||||
|
.comment = "Shell command to run",
|
||||||
|
.type = CONFIG_ITEM_STRING },
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
static const char *
|
static const char *
|
||||||
get_config_string (struct config_item *root, const char *key)
|
get_config_string (struct config_item *root, const char *key)
|
||||||
{
|
{
|
||||||
|
@ -1484,6 +1498,35 @@ load_config_streams (struct config_item *subtree, void *user_data)
|
||||||
sizeof *g.streams.vector, strv_sort_utf8_cb);
|
sizeof *g.streams.vector, strv_sort_utf8_cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
load_config_actions (struct config_item *subtree, void *user_data)
|
||||||
|
{
|
||||||
|
(void) user_data;
|
||||||
|
|
||||||
|
struct str_map_iter iter = str_map_iter_make (&subtree->value.object);
|
||||||
|
while (str_map_iter_next (&iter))
|
||||||
|
strv_append (&g.action_names, iter.link->key);
|
||||||
|
qsort (g.action_names.vector, g.action_names.len,
|
||||||
|
sizeof *g.action_names.vector, strv_sort_utf8_cb);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < g.action_names.len; i++)
|
||||||
|
{
|
||||||
|
const char *name = g.action_names.vector[i];
|
||||||
|
struct config_item *item = config_item_get (subtree, name, NULL);
|
||||||
|
hard_assert (item != NULL);
|
||||||
|
if (item->type != CONFIG_ITEM_OBJECT)
|
||||||
|
exit_fatal ("`%s': invalid user action, expected an object", name);
|
||||||
|
|
||||||
|
config_schema_apply_to_object (g_config_actions, item, NULL);
|
||||||
|
config_schema_call_changed (item);
|
||||||
|
|
||||||
|
const char *description = get_config_string (item, "description");
|
||||||
|
const char *command = get_config_string (item, "command");
|
||||||
|
strv_append (&g.action_descriptions, description ? description : name);
|
||||||
|
strv_append (&g.action_commands, command ? command : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
app_load_configuration (void)
|
app_load_configuration (void)
|
||||||
{
|
{
|
||||||
|
@ -1491,6 +1534,8 @@ app_load_configuration (void)
|
||||||
config_register_module (config, "settings", load_config_settings, NULL);
|
config_register_module (config, "settings", load_config_settings, NULL);
|
||||||
config_register_module (config, "colors", load_config_colors, NULL);
|
config_register_module (config, "colors", load_config_colors, NULL);
|
||||||
config_register_module (config, "streams", load_config_streams, NULL);
|
config_register_module (config, "streams", load_config_streams, NULL);
|
||||||
|
// This must run before bindings are parsed in app_init_ui().
|
||||||
|
config_register_module (config, "actions", load_config_actions, NULL);
|
||||||
|
|
||||||
// Bootstrap configuration, so that we can access schema items at all
|
// Bootstrap configuration, so that we can access schema items at all
|
||||||
config_load (config, config_item_object ());
|
config_load (config, config_item_object ());
|
||||||
|
@ -1548,6 +1593,9 @@ app_init_context (void)
|
||||||
g.config = config_make ();
|
g.config = config_make ();
|
||||||
g.streams = strv_make ();
|
g.streams = strv_make ();
|
||||||
g.enqueue = strv_make ();
|
g.enqueue = strv_make ();
|
||||||
|
g.action_names = strv_make ();
|
||||||
|
g.action_descriptions = strv_make ();
|
||||||
|
g.action_commands = strv_make ();
|
||||||
|
|
||||||
g.playback_info = str_map_make (free);
|
g.playback_info = str_map_make (free);
|
||||||
g.playback_info.key_xfrm = tolower_ascii_strxfrm;
|
g.playback_info.key_xfrm = tolower_ascii_strxfrm;
|
||||||
|
@ -1570,6 +1618,9 @@ app_free_context (void)
|
||||||
str_map_free (&g.playback_info);
|
str_map_free (&g.playback_info);
|
||||||
strv_free (&g.streams);
|
strv_free (&g.streams);
|
||||||
strv_free (&g.enqueue);
|
strv_free (&g.enqueue);
|
||||||
|
strv_free (&g.action_names);
|
||||||
|
strv_free (&g.action_descriptions);
|
||||||
|
strv_free (&g.action_commands);
|
||||||
item_list_free (&g.playlist);
|
item_list_free (&g.playlist);
|
||||||
|
|
||||||
#ifdef WITH_FFTW
|
#ifdef WITH_FFTW
|
||||||
|
@ -2379,12 +2430,52 @@ app_goto_tab (int tab_index)
|
||||||
static int
|
static int
|
||||||
action_resolve (const char *name)
|
action_resolve (const char *name)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < ACTION_COUNT; i++)
|
for (int i = 0; i < ACTION_USER_0; i++)
|
||||||
if (!strcasecmp_ascii (g_action_names[i], name))
|
if (!strcasecmp_ascii (g_action_names[i], name))
|
||||||
return i;
|
return i;
|
||||||
|
|
||||||
|
// We could put this lookup first, and accordingly adjust
|
||||||
|
// app_init_bindings() to do action_resolve(action_name(action)),
|
||||||
|
// however the ability to override internal actions seems pointless.
|
||||||
|
for (size_t i = 0; i < g.action_names.len; i++)
|
||||||
|
if (!strcasecmp_ascii (g.action_names.vector[i], name))
|
||||||
|
return ACTION_USER_0 + i;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const char *
|
||||||
|
action_name (enum action action)
|
||||||
|
{
|
||||||
|
if (action < ACTION_USER_0)
|
||||||
|
return g_action_names[action];
|
||||||
|
|
||||||
|
size_t user_action = action - ACTION_USER_0;
|
||||||
|
hard_assert (user_action < g.action_names.len);
|
||||||
|
return g.action_names.vector[user_action];
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *
|
||||||
|
action_description (enum action action)
|
||||||
|
{
|
||||||
|
if (action < ACTION_USER_0)
|
||||||
|
return g_action_descriptions[action];
|
||||||
|
|
||||||
|
size_t user_action = action - ACTION_USER_0;
|
||||||
|
hard_assert (user_action < g.action_descriptions.len);
|
||||||
|
return g.action_descriptions.vector[user_action];
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *
|
||||||
|
action_command (enum action action)
|
||||||
|
{
|
||||||
|
if (action < ACTION_USER_0)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
size_t user_action = action - ACTION_USER_0;
|
||||||
|
hard_assert (user_action < g.action_commands.len);
|
||||||
|
return g.action_commands.vector[user_action];
|
||||||
|
}
|
||||||
|
|
||||||
// --- User input handling -----------------------------------------------------
|
// --- User input handling -----------------------------------------------------
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
@ -2516,6 +2607,64 @@ incremental_search_on_end (bool confirmed)
|
||||||
|
|
||||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
static bool
|
||||||
|
run_command (const char *command, struct str *output, struct error **e)
|
||||||
|
{
|
||||||
|
char *adjusted = xstrdup_printf ("2>&1 %s", command);
|
||||||
|
print_debug ("running command: %s", adjusted);
|
||||||
|
|
||||||
|
FILE *fp = popen (adjusted, "r");
|
||||||
|
free (adjusted);
|
||||||
|
if (!fp)
|
||||||
|
return error_set (e, "%s", strerror (errno));
|
||||||
|
|
||||||
|
char buf[BUFSIZ];
|
||||||
|
size_t len;
|
||||||
|
while ((len = fread (buf, 1, sizeof buf, fp)) == sizeof buf)
|
||||||
|
str_append_data (output, buf, len);
|
||||||
|
str_append_data (output, buf, len);
|
||||||
|
|
||||||
|
int status = pclose (fp);
|
||||||
|
if (status < 0)
|
||||||
|
return error_set (e, "%s", strerror (errno));
|
||||||
|
if (WIFEXITED (status) && WEXITSTATUS (status))
|
||||||
|
return error_set (e, "exit status %d", WEXITSTATUS (status));
|
||||||
|
if (WIFSIGNALED (status))
|
||||||
|
return error_set (e, "terminated on signal %d", WTERMSIG (status));
|
||||||
|
if (WIFSTOPPED (status))
|
||||||
|
return error_set (e, "stopped on signal %d", WSTOPSIG (status));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
app_process_action_command (enum action action)
|
||||||
|
{
|
||||||
|
const char *command = action_command (action);
|
||||||
|
if (!command)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
struct str output = str_make ();
|
||||||
|
struct error *error = NULL;
|
||||||
|
(void) run_command (command, &output, &error);
|
||||||
|
str_enforce_utf8 (&output);
|
||||||
|
|
||||||
|
struct strv lines = strv_make ();
|
||||||
|
cstr_split (output.str, "\r\n", false, &lines);
|
||||||
|
str_free (&output);
|
||||||
|
while (lines.len && !*lines.vector[lines.len - 1])
|
||||||
|
free (strv_steal (&lines, lines.len - 1));
|
||||||
|
for (size_t i = 0; i < lines.len; i++)
|
||||||
|
print_debug ("output: %s", lines.vector[i]);
|
||||||
|
strv_free (&lines);
|
||||||
|
|
||||||
|
if (error)
|
||||||
|
{
|
||||||
|
print_error ("\"%s\": %s", action_description (action), error->message);
|
||||||
|
error_free (error);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
app_mpd_toggle (const char *name)
|
app_mpd_toggle (const char *name)
|
||||||
{
|
{
|
||||||
|
@ -2562,10 +2711,6 @@ app_process_action (enum action action)
|
||||||
xui_invalidate ();
|
xui_invalidate ();
|
||||||
app_hide_message ();
|
app_hide_message ();
|
||||||
return true;
|
return true;
|
||||||
default:
|
|
||||||
print_error ("\"%s\" is not allowed here",
|
|
||||||
g_action_descriptions[action]);
|
|
||||||
return false;
|
|
||||||
|
|
||||||
case ACTION_MULTISELECT:
|
case ACTION_MULTISELECT:
|
||||||
if (!tab->can_multiselect
|
if (!tab->can_multiselect
|
||||||
|
@ -2677,8 +2822,14 @@ app_process_action (enum action action)
|
||||||
case ACTION_GOTO_VIEW_BOTTOM:
|
case ACTION_GOTO_VIEW_BOTTOM:
|
||||||
g.active_tab->item_selected = g.active_tab->item_top;
|
g.active_tab->item_selected = g.active_tab->item_top;
|
||||||
return app_move_selection (MAX (0, app_visible_items () - 1));
|
return app_move_selection (MAX (0, app_visible_items () - 1));
|
||||||
}
|
|
||||||
|
default:
|
||||||
|
if (app_process_action_command (action))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
print_error ("\"%s\" is not allowed here", action_description (action));
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
|
@ -2696,8 +2847,7 @@ app_editor_process_action (enum action action)
|
||||||
g.editor.on_end = NULL;
|
g.editor.on_end = NULL;
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
print_error ("\"%s\" is not allowed here",
|
print_error ("\"%s\" is not allowed here", action_description (action));
|
||||||
g_action_descriptions[action]);
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
case ACTION_EDITOR_B_CHAR:
|
case ACTION_EDITOR_B_CHAR:
|
||||||
|
@ -4110,24 +4260,16 @@ info_tab_plugin_load (const char *path)
|
||||||
// Shell quoting is less annoying than process management.
|
// Shell quoting is less annoying than process management.
|
||||||
struct str escaped = str_make ();
|
struct str escaped = str_make ();
|
||||||
shell_quote (path, &escaped);
|
shell_quote (path, &escaped);
|
||||||
FILE *fp = popen (escaped.str, "r");
|
|
||||||
str_free (&escaped);
|
|
||||||
if (!fp)
|
|
||||||
{
|
|
||||||
print_error ("%s: %s", path, strerror (errno));
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct str description = str_make ();
|
struct str description = str_make ();
|
||||||
char buf[BUFSIZ];
|
struct error *error = NULL;
|
||||||
size_t len;
|
(void) run_command (escaped.str, &description, &error);
|
||||||
while ((len = fread (buf, 1, sizeof buf, fp)) == sizeof buf)
|
str_free (&escaped);
|
||||||
str_append_data (&description, buf, len);
|
if (error)
|
||||||
str_append_data (&description, buf, len);
|
|
||||||
if (pclose (fp))
|
|
||||||
{
|
{
|
||||||
|
print_error ("%s: %s", path, error->message);
|
||||||
|
error_free (error);
|
||||||
str_free (&description);
|
str_free (&description);
|
||||||
print_error ("%s: %s", path, strerror (errno));
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4140,8 +4282,8 @@ info_tab_plugin_load (const char *path)
|
||||||
str_enforce_utf8 (&description);
|
str_enforce_utf8 (&description);
|
||||||
if (!description.len)
|
if (!description.len)
|
||||||
{
|
{
|
||||||
str_free (&description);
|
|
||||||
print_error ("%s: %s", path, "missing description");
|
print_error ("%s: %s", path, "missing description");
|
||||||
|
str_free (&description);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4579,7 +4721,7 @@ help_tab_on_action (enum action action)
|
||||||
if (action == ACTION_DESCRIBE)
|
if (action == ACTION_DESCRIBE)
|
||||||
{
|
{
|
||||||
app_show_message (xstrdup ("Configuration name: "),
|
app_show_message (xstrdup ("Configuration name: "),
|
||||||
xstrdup (g_action_names[a]));
|
xstrdup (action_name (a)));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (action != ACTION_CHOOSE || a == ACTION_CHOOSE /* avoid recursion */)
|
if (action != ACTION_CHOOSE || a == ACTION_CHOOSE /* avoid recursion */)
|
||||||
|
@ -4604,9 +4746,9 @@ help_tab_assign_action (enum action action)
|
||||||
|
|
||||||
static void
|
static void
|
||||||
help_tab_group (struct binding *keys, size_t len, struct strv *out,
|
help_tab_group (struct binding *keys, size_t len, struct strv *out,
|
||||||
bool bound[ACTION_COUNT])
|
bool bound[], size_t action_count)
|
||||||
{
|
{
|
||||||
for (enum action i = 0; i < ACTION_COUNT; i++)
|
for (enum action i = 0; i < action_count; i++)
|
||||||
{
|
{
|
||||||
struct strv ass = strv_make ();
|
struct strv ass = strv_make ();
|
||||||
for (size_t k = 0; k < len; k++)
|
for (size_t k = 0; k < len; k++)
|
||||||
|
@ -4616,7 +4758,7 @@ help_tab_group (struct binding *keys, size_t len, struct strv *out,
|
||||||
{
|
{
|
||||||
char *joined = strv_join (&ass, ", ");
|
char *joined = strv_join (&ass, ", ");
|
||||||
strv_append_owned (out, xstrdup_printf
|
strv_append_owned (out, xstrdup_printf
|
||||||
(" %s%c%s", g_action_descriptions[i], 0, joined));
|
(" %s%c%s", action_description (i), 0, joined));
|
||||||
free (joined);
|
free (joined);
|
||||||
|
|
||||||
bound[i] = true;
|
bound[i] = true;
|
||||||
|
@ -4627,13 +4769,13 @@ help_tab_group (struct binding *keys, size_t len, struct strv *out,
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
help_tab_unbound (struct strv *out, bool bound[ACTION_COUNT])
|
help_tab_unbound (struct strv *out, bool bound[], size_t action_count)
|
||||||
{
|
{
|
||||||
for (enum action i = 0; i < ACTION_COUNT; i++)
|
for (enum action i = 0; i < action_count; i++)
|
||||||
if (!bound[i])
|
if (!bound[i])
|
||||||
{
|
{
|
||||||
strv_append_owned (out,
|
strv_append_owned (out,
|
||||||
xstrdup_printf (" %s%c", g_action_descriptions[i], 0));
|
xstrdup_printf (" %s%c", action_description (i), 0));
|
||||||
help_tab_assign_action (i);
|
help_tab_assign_action (i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4663,27 +4805,30 @@ help_tab_init (void)
|
||||||
struct strv *lines = &g_help_tab.lines;
|
struct strv *lines = &g_help_tab.lines;
|
||||||
*lines = strv_make ();
|
*lines = strv_make ();
|
||||||
|
|
||||||
bool bound[ACTION_COUNT] = { [ACTION_NONE] = true };
|
size_t bound_len = ACTION_USER_0 + g.action_names.len;
|
||||||
|
bool *bound = xcalloc (bound_len, sizeof *bound);
|
||||||
|
bound[ACTION_NONE] = true;
|
||||||
|
|
||||||
strv_append (lines, "Normal mode actions");
|
strv_append (lines, "Normal mode actions");
|
||||||
help_tab_group (g_normal_keys, g_normal_keys_len, lines, bound);
|
help_tab_group (g_normal_keys, g_normal_keys_len, lines, bound, bound_len);
|
||||||
strv_append (lines, "");
|
strv_append (lines, "");
|
||||||
|
|
||||||
strv_append (lines, "Editor mode actions");
|
strv_append (lines, "Editor mode actions");
|
||||||
help_tab_group (g_editor_keys, g_editor_keys_len, lines, bound);
|
help_tab_group (g_editor_keys, g_editor_keys_len, lines, bound, bound_len);
|
||||||
strv_append (lines, "");
|
strv_append (lines, "");
|
||||||
|
|
||||||
bool have_unbound = false;
|
bool have_unbound = false;
|
||||||
for (enum action i = 0; i < ACTION_COUNT; i++)
|
for (size_t i = 0; i < bound_len; i++)
|
||||||
if (!bound[i])
|
if (!bound[i])
|
||||||
have_unbound = true;
|
have_unbound = true;
|
||||||
|
|
||||||
if (have_unbound)
|
if (have_unbound)
|
||||||
{
|
{
|
||||||
strv_append (lines, "Unbound actions");
|
strv_append (lines, "Unbound actions");
|
||||||
help_tab_unbound (lines, bound);
|
help_tab_unbound (lines, bound, bound_len);
|
||||||
strv_append (lines, "");
|
strv_append (lines, "");
|
||||||
}
|
}
|
||||||
|
free (bound);
|
||||||
|
|
||||||
struct tab *super = &g_help_tab.super;
|
struct tab *super = &g_help_tab.super;
|
||||||
tab_init (super, "Help");
|
tab_init (super, "Help");
|
||||||
|
|
Loading…
Reference in New Issue