Add toggle extended command

Allows the user to configure a key binding to toggle any boolean
option, for example:

BIND=':toggle(price_quotes)
BIND=v:toggle(autodig)

The option must be settable in-game.
This commit is contained in:
Pasi Kallinen
2026-03-21 11:52:36 +02:00
parent 62a50413d3
commit 63a78edcfa
7 changed files with 167 additions and 12 deletions

106
src/cmd.c
View File

@@ -1368,6 +1368,21 @@ dolookaround(void)
return ECMD_OK;
}
/* #toggle extended command
BIND=':toggle(price_quotes)
BIND=@:toggle(autopickup) */
int
dotoggleoption(void)
{
if (gc.cmd_bind && gc.cmd_bind->param) {
return toggle_bool_option(gc.cmd_bind->param);
} else {
pline("Use #optionsfull to set any option instead.");
return ECMD_OK;
}
}
void
set_move_cmd(int dir, int run)
{
@@ -1889,6 +1904,8 @@ struct ext_func_tab extcmdlist[] = {
wiz_timeout_queue, IFBURIED | AUTOCOMPLETE | WIZMODECMD, NULL },
{ M('T'), "tip", "empty a container",
dotip, AUTOCOMPLETE | CMD_M_PREFIX, NULL },
{ '\0', "toggle", "toggle boolean option",
dotoggleoption, IFBURIED | GENERALCMD | CMD_PARAM, NULL },
{ '_', "travel", "travel to a specific location on the map",
dotravel, CMD_M_PREFIX, NULL },
{ M('t'), "turn", "turn undead away",
@@ -2119,11 +2136,16 @@ cmdbind_add(uchar key, const struct ext_func_tab *extcmd, boolean user)
if (bind) {
bind->cmd = extcmd;
bind->userbind = user;
if (bind->param) {
free(bind->param);
bind->param = NULL;
}
return;
} else {
bind = (struct Cmd_bind *) alloc(sizeof(struct Cmd_bind));
bind->key = key;
bind->userbind = user;
bind->param = NULL;
bind->cmd = extcmd;
bind->next = gc.Cmd.cmdbinds;
gc.Cmd.cmdbinds = bind;
@@ -2142,6 +2164,8 @@ cmdbind_remove(uchar key)
prev->next = bind->next;
else
gc.Cmd.cmdbinds = bind->next;
if (bind->param)
free(bind->param);
free(bind);
return;
}
@@ -2157,6 +2181,8 @@ cmdbind_freeall(void)
while (gc.Cmd.cmdbinds) {
next = gc.Cmd.cmdbinds->next;
if (gc.Cmd.cmdbinds->param)
free(gc.Cmd.cmdbinds->param);
free(gc.Cmd.cmdbinds);
gc.Cmd.cmdbinds = next;
}
@@ -2222,9 +2248,15 @@ get_changed_key_binds(strbuf_t *sbuf)
while (bind) {
keys[bind->key] = 1;
if (bind->userbind && bind->cmd && bind->cmd->key != bind->key) {
Sprintf(buf, "BIND=%s:%s%s", key2txt(bind->key, buf2),
bind->cmd->ef_txt,
sbuf ? "\n" : "");
if ((bind->cmd->flags & CMD_PARAM) != 0)
Sprintf(buf, "BIND=%s:%s(%s)%s", key2txt(bind->key, buf2),
bind->cmd->ef_txt,
bind->param,
sbuf ? "\n" : "");
else
Sprintf(buf, "BIND=%s:%s%s", key2txt(bind->key, buf2),
bind->cmd->ef_txt,
sbuf ? "\n" : "");
if (sbuf)
strbuf_append(sbuf, buf);
else
@@ -2319,18 +2351,31 @@ handler_rebind_keys_add(boolean keyfirst)
destroy_nhwindow(win);
if (npick > 0) {
struct Cmd_bind *prevcmd;
const char *cmdstr;
char cmdstr[BUFSZ];
i = picks->item.a_int;
free((genericptr_t) picks);
if (i == -1) {
ec = NULL;
cmdstr = "nothing";
Strcat(cmdstr, "nothing");
goto bindit;
} else {
ec = &extcmdlist[i-1];
cmdstr = ec->ef_txt;
if ((ec->flags & CMD_PARAM) != 0) {
char parambuf[BUFSZ];
char querybuf[BUFSZ];
parambuf[0] = '\0';
Sprintf(querybuf, "Command %s requires a parameter:", ec->ef_txt);
getlin(querybuf, parambuf);
(void) mungspaces(parambuf);
Snprintf(cmdstr, BUFSZ-1, "%s(%s)", ec->ef_txt, parambuf);
cmdstr[BUFSZ-1] = '\0';
} else {
Strcat(cmdstr, ec->ef_txt);
}
}
bindit:
if (!key) {
@@ -2615,6 +2660,8 @@ boolean
bind_key(uchar key, const char *command, boolean user)
{
struct ext_func_tab *extcmd;
long len;
char *buf, *p = NULL, *lastp = NULL;
/* special case: "nothing" is reserved for unbinding */
if (!strcmpi(command, "nothing")) {
@@ -2622,12 +2669,46 @@ bind_key(uchar key, const char *command, boolean user)
return TRUE;
}
/* copy command to buf for modification */
len = strlen(command) + 1;
buf = (char *)alloc(len);
(void) strncpy(buf, command, len);
/* does buf have a parameter in parenthesis? */
if ((p = strchr(buf, '(')) != 0
&& (lastp = strrchr(buf, ')')) != 0
&& (lastp > p)) {
*p = '\0';
*lastp = '\0';
/* p points to the parameter */
p++;
}
for (extcmd = extcmdlist; extcmd->ef_txt; extcmd++) {
if (strcmpi(command, extcmd->ef_txt))
if (strcmpi(buf, extcmd->ef_txt))
continue;
if ((extcmd->flags & INTERNALCMD) != 0)
continue;
cmdbind_add(key, extcmd, user);
if ((extcmd->flags & CMD_PARAM) != 0) {
if (!p) {
config_error_add("'%s' requires a parameter", buf);
} else {
struct Cmd_bind *bind = cmdbind_get(key);
int maxlen = min(30, strlen(p)) + 1;
if (maxlen <= 1) {
config_error_add("Required parameter cannot be empty");
} else {
bind->param = (char *) alloc(maxlen);
(void) strncpy(bind->param, p, maxlen);
bind->param[maxlen-1] = '\0';
}
}
} else if (p && strlen(p) > 0)
config_error_add("'%s' does not take a parameter", buf);
#if 0 /* silently accept key binding for unavailable command (!SHELL,&c) */
if ((extcmd->flags & CMD_NOT_AVAILABLE) != 0) {
char buf[BUFSZ];
@@ -2636,9 +2717,11 @@ bind_key(uchar key, const char *command, boolean user)
config_error_add("%s", buf);
}
#endif
free(buf);
return TRUE;
}
free(buf);
return FALSE;
}
@@ -2742,8 +2825,13 @@ keylist_putcmds(winid datawin, boolean docount,
count++;
continue;
}
Sprintf(buf, "%-7s %-13s %s", key2txt(key, buf2),
bind->cmd->ef_txt, bind->cmd->ef_desc);
if ((bind->cmd->flags & CMD_PARAM) != 0)
Sprintf(buf, "%-7s %-13s %s \"%s\"", key2txt(key, buf2),
bind->cmd->ef_txt, bind->cmd->ef_desc,
bind->param);
else
Sprintf(buf, "%-7s %-13s %s", key2txt(key, buf2),
bind->cmd->ef_txt, bind->cmd->ef_desc);
putstr(datawin, 0, buf);
keys_used[i] = TRUE;
}

View File

@@ -390,6 +390,7 @@ staticfn boolean parse_role_opt(int, boolean, const char *, char *, char **);
staticfn char *get_cnf_role_opt(int);
staticfn unsigned int longest_option_name(int, int);
staticfn int doset_simple_menu(void);
staticfn void reset_needed_visuals(void);
staticfn void doset_add_menu(winid, const char *, const char *, int, int);
staticfn int handle_add_list_remove(const char *, int);
staticfn void all_options_conds(strbuf_t *);
@@ -9012,6 +9013,15 @@ doset(void) /* changing options via menu by Per Liboriussen */
goto rerun;
}
reset_needed_visuals();
return ECMD_OK;
}
#undef HELP_IDX
staticfn void
reset_needed_visuals(void)
{
if (go.opt_need_glyph_reset) {
reset_glyphmap(gm_optionchange);
}
@@ -9039,11 +9049,13 @@ doset(void) /* changing options via menu by Per Liboriussen */
if (disp.botl || disp.botlx) {
bot();
}
return ECMD_OK;
go.opt_need_redraw = FALSE;
go.opt_need_glyph_reset = FALSE;
go.opt_reset_customcolors = FALSE;
go.opt_reset_customsymbols = FALSE;
go.opt_update_basic_palette = FALSE;
}
#undef HELP_IDX
/* doset(#optionsfull command) menu entries for compound options */
staticfn void
doset_add_menu(
@@ -9304,6 +9316,29 @@ dotogglepickup(void)
return ECMD_OK;
}
/* toggle any (settable in-game) boolean option by name */
int
toggle_bool_option(const char *p)
{
int i;
int ret = ECMD_FAIL;
for (i = 0; i < OPTCOUNT; i++)
if (!strncmpi(allopt[i].name, p, strlen(p))
&& allopt[i].opttyp == BoolOpt
&& allopt[i].setwhere == set_in_game
&& allopt[i].addr != 0) {
char buf[BUFSZ];
Sprintf(buf, "%s%s", *allopt[i].addr ? "!" : "", allopt[i].name);
if (parseoptions(buf, FALSE, FALSE))
ret = ECMD_OK;
reset_needed_visuals();
}
return ret;
}
int
add_autopickup_exception(const char *mapping)
{