Add key rebinding

This is a modified version of Jason Dorje Short's key rebinding
patch, and allows also binding special keys, such as the ones
used in getloc and getpos.

One of the ways to play NetHack on nethack.alt.org is via a HTML
terminal in browser. Unfortunately this means several ctrl-key
combinations cannot be entered, because the browser intercepts
those. Similar thing applies to some international keyboard layouts
on Windows. With this patch, the user can just rebind the command
to a key that works best for them.

I've tested this on Linux TTY, X11, and Windows TTY and GUI.
This commit is contained in:
Pasi Kallinen
2016-10-05 17:19:06 +03:00
parent d0783facdc
commit 680c8a542c
15 changed files with 1696 additions and 422 deletions

View File

@@ -34,9 +34,6 @@ boolean resuming;
monstr_init(); /* monster strengths */
objects_init();
if (wizard)
add_debug_extended_commands();
/* if a save file created in normal mode is now being restored in
explore mode, treat it as normal restore followed by 'X' command
to use up the save file and require confirmation for explore mode */

940
src/cmd.c

File diff suppressed because it is too large Load Diff

View File

@@ -59,45 +59,84 @@ const char *goal;
putstr(tmpwin, 0, sbuf);
putstr(tmpwin, 0, "Use 'H', 'J', 'K', 'L' to move the cursor 8 units at a time.");
putstr(tmpwin, 0, "Or enter a background symbol (ex. '<').");
putstr(tmpwin, 0, "Use '@' to move the cursor on yourself.");
if (!iflags.terrainmode || (iflags.terrainmode & TER_MON) != 0)
putstr(tmpwin, 0, "Use 'm' or 'M' to move the cursor to next monster.");
if (!iflags.terrainmode || (iflags.terrainmode & TER_OBJ) != 0)
putstr(tmpwin, 0, "Use 'o' or 'O' to move the cursor to next object.");
Sprintf(sbuf, "Use '%s' to move the cursor on yourself.",
visctrl(Cmd.spkeys[NHKF_GETPOS_SELF]));
putstr(tmpwin, 0, sbuf);
if (!iflags.terrainmode || (iflags.terrainmode & TER_MON) != 0) {
Sprintf(sbuf, "Use '%s' or '%s' to move the cursor to next monster.",
visctrl(Cmd.spkeys[NHKF_GETPOS_MON_NEXT]),
visctrl(Cmd.spkeys[NHKF_GETPOS_MON_PREV]));
putstr(tmpwin, 0, sbuf);
}
if (!iflags.terrainmode || (iflags.terrainmode & TER_OBJ) != 0) {
Sprintf(sbuf, "Use '%s' or '%s' to move the cursor to next object.",
visctrl(Cmd.spkeys[NHKF_GETPOS_OBJ_NEXT]),
visctrl(Cmd.spkeys[NHKF_GETPOS_OBJ_PREV]));
putstr(tmpwin, 0, sbuf);
}
if (!iflags.terrainmode || (iflags.terrainmode & TER_MAP) != 0) {
/* both of these are primarily useful when choosing a travel
destination for the '_' command */
putstr(tmpwin, 0,
"Use 'd' or 'D' to move the cursor to next door or doorway.");
putstr(tmpwin, 0,
"Use 'x' or 'X' to move the cursor to unexplored location.");
Sprintf(sbuf,
"Use '%s' or '%s' to move the cursor to next door or doorway.",
visctrl(Cmd.spkeys[NHKF_GETPOS_DOOR_NEXT]),
visctrl(Cmd.spkeys[NHKF_GETPOS_DOOR_PREV]));
putstr(tmpwin, 0, sbuf);
Sprintf(sbuf,
"Use '%s' or '%s' to move the cursor to unexplored location.",
visctrl(Cmd.spkeys[NHKF_GETPOS_UNEX_NEXT]),
visctrl(Cmd.spkeys[NHKF_GETPOS_UNEX_PREV]));
putstr(tmpwin, 0, sbuf);
}
if (!iflags.terrainmode) {
if (getpos_hilitefunc)
putstr(tmpwin, 0, "Use '$' to display valid locations.");
putstr(tmpwin, 0, "Use '#' to toggle automatic description.");
if (iflags.cmdassist) /* assisting the '/' command, I suppose... */
putstr(tmpwin, 0,
(iflags.getpos_coords == GPCOORDS_NONE)
? "(Set 'whatis_coord' option to include coordinates with '#' text.)"
: "(Reset 'whatis_coord' option to omit coordinates from '#' text.)");
char kbuf[BUFSZ];
if (getpos_hilitefunc) {
Sprintf(sbuf, "Use '%s' to display valid locations.",
visctrl(Cmd.spkeys[NHKF_GETPOS_SHOWVALID]));
putstr(tmpwin, 0, sbuf);
}
Sprintf(sbuf, "Use '%s' to toggle automatic description.",
visctrl(Cmd.spkeys[NHKF_GETPOS_AUTODESC]));
putstr(tmpwin, 0, sbuf);
if (iflags.cmdassist) { /* assisting the '/' command, I suppose... */
Sprintf(sbuf,
(iflags.getpos_coords == GPCOORDS_NONE)
? "(Set 'whatis_coord' option to include coordinates with '%s' text.)"
: "(Reset 'whatis_coord' option to omit coordinates from '%s' text.)",
visctrl(Cmd.spkeys[NHKF_GETPOS_AUTODESC]));
}
/* disgusting hack; the alternate selection characters work for any
getpos call, but only matter for dowhatis (and doquickwhatis) */
doing_what_is = (goal == what_is_an_unknown_object);
Sprintf(sbuf, "Type a '.'%s when you are at the right place.",
doing_what_is ? " or ',' or ';' or ':'" : "");
doing_what_is = (goal == what_is_an_unknown_object);
if (doing_what_is) {
Sprintf(kbuf, "'%s' or '%s' or '%s' or '%s'",
visctrl(Cmd.spkeys[NHKF_GETPOS_PICK]),
visctrl(Cmd.spkeys[NHKF_GETPOS_PICK_Q]),
visctrl(Cmd.spkeys[NHKF_GETPOS_PICK_O]),
visctrl(Cmd.spkeys[NHKF_GETPOS_PICK_V]));
} else {
Sprintf(kbuf, "'%s'", visctrl(Cmd.spkeys[NHKF_GETPOS_PICK]));
}
Sprintf(sbuf, "Type a %s when you are at the right place.", kbuf);
putstr(tmpwin, 0, sbuf);
if (doing_what_is) {
putstr(tmpwin, 0,
" ':' describe current spot, show 'more info', move to another spot.");
Sprintf(sbuf,
" '.' describe current spot,%s move to another spot;",
" '%s' describe current spot, show 'more info', move to another spot.",
visctrl(Cmd.spkeys[NHKF_GETPOS_PICK_V]));
putstr(tmpwin, 0, sbuf);
Sprintf(sbuf,
" '%s' describe current spot,%s move to another spot;",
visctrl(Cmd.spkeys[NHKF_GETPOS_PICK]),
flags.help ? " prompt if 'more info'," : "");
putstr(tmpwin, 0, sbuf);
putstr(tmpwin, 0,
" ',' describe current spot, move to another spot;");
putstr(tmpwin, 0,
" ';' describe current spot, stop looking at things;");
Sprintf(sbuf,
" '%s' describe current spot, move to another spot;",
visctrl(Cmd.spkeys[NHKF_GETPOS_PICK_Q]));
putstr(tmpwin, 0, sbuf);
Sprintf(sbuf,
" '%s' describe current spot, stop looking at things;",
visctrl(Cmd.spkeys[NHKF_GETPOS_PICK_O]));
putstr(tmpwin, 0, sbuf);
}
}
if (!force)
@@ -337,9 +376,27 @@ coord *ccp;
boolean force;
const char *goal;
{
static const char pick_chars[] = ".,;:",
mMoOdDxX[] = "mMoOdDxX";
const char *cp;
struct {
int nhkf, ret;
} const pick_chars_def[] = {
{ NHKF_GETPOS_PICK, LOOK_TRADITIONAL },
{ NHKF_GETPOS_PICK_Q, LOOK_QUICK },
{ NHKF_GETPOS_PICK_O, LOOK_ONCE },
{ NHKF_GETPOS_PICK_V, LOOK_VERBOSE }
};
const int mMoOdDxX_def[] = {
NHKF_GETPOS_MON_NEXT,
NHKF_GETPOS_MON_PREV,
NHKF_GETPOS_OBJ_NEXT,
NHKF_GETPOS_OBJ_PREV,
NHKF_GETPOS_DOOR_NEXT,
NHKF_GETPOS_DOOR_PREV,
NHKF_GETPOS_UNEX_NEXT,
NHKF_GETPOS_UNEX_PREV
};
char pick_chars[6];
char mMoOdDxX[9];
int result = 0;
int cx, cy, i, c;
int sidx, tx, ty;
@@ -350,10 +407,19 @@ const char *goal;
int gcount[NUM_GLOCS] = DUMMY;
int gidx[NUM_GLOCS] = DUMMY;
for (i = 0; i < SIZE(pick_chars_def); i++)
pick_chars[i] = Cmd.spkeys[pick_chars_def[i].nhkf];
pick_chars[SIZE(pick_chars_def)] = '\0';
for (i = 0; i < SIZE(mMoOdDxX_def); i++)
mMoOdDxX[i] = Cmd.spkeys[mMoOdDxX_def[i]];
mMoOdDxX[SIZE(mMoOdDxX_def)] = '\0';
if (!goal)
goal = "desired location";
if (flags.verbose) {
pline("(For instructions type a '?')");
pline("(For instructions type a '%s')",
visctrl(Cmd.spkeys[NHKF_GETPOS_HELP]));
msg_given = TRUE;
}
cx = ccp->x;
@@ -388,7 +454,7 @@ const char *goal;
if (iflags.autodescribe)
msg_given = FALSE;
if (c == '\033') {
if (c == Cmd.spkeys[NHKF_ESC]) {
cx = cy = -10;
msg_given = TRUE; /* force clear */
result = -1;
@@ -404,7 +470,7 @@ const char *goal;
}
if ((cp = index(pick_chars, c)) != 0) {
/* '.' => 0, ',' => 1, ';' => 2, ':' => 3 */
result = (int) (cp - pick_chars);
result = pick_chars_def[(int) (cp - pick_chars)].ret;
break;
}
for (i = 0; i < 8; i++) {
@@ -442,26 +508,23 @@ const char *goal;
goto nxtc;
}
if (c == '?' || redraw_cmd(c)) {
if (c == '?')
if (c == Cmd.spkeys[NHKF_GETPOS_HELP] || redraw_cmd(c)) {
if (c == Cmd.spkeys[NHKF_GETPOS_HELP])
getpos_help(force, goal);
else /* ^R */
docrt(); /* redraw */
/* update message window to reflect that we're still targetting */
show_goal_msg = TRUE;
msg_given = TRUE;
} else if (c == '$' && getpos_hilitefunc) {
} else if (c == Cmd.spkeys[NHKF_GETPOS_SHOWVALID]
&& getpos_hilitefunc) {
if (!hilite_state) {
(*getpos_hilitefunc)(0);
(*getpos_hilitefunc)(1);
hilite_state = TRUE;
}
goto nxtc;
} else if (c == '#') {
/* unfortunately, using '#' as a command means we can't move
cursor to sinks, iron bars, and poison clouds; perhaps
when autodescribe is already on, next '#' should try to
move to '#' rather than to toggle off? (or ask; ick...) */
} else if (c == Cmd.spkeys[NHKF_GETPOS_AUTODESC]) {
iflags.autodescribe = !iflags.autodescribe;
pline("Automatic description %sis %s.",
flags.verbose ? "of features under cursor " : "",
@@ -470,7 +533,7 @@ const char *goal;
show_goal_msg = TRUE;
msg_given = TRUE;
goto nxtc;
} else if (c == '@') { /* return to hero's spot */
} else if (c == Cmd.spkeys[NHKF_GETPOS_SELF]) {
/* reset 'm&M', 'o&O', &c; otherwise, there's no way for player
to achieve that except by manually cycling through all spots */
for (i = 0; i < NUM_GLOCS; i++)
@@ -568,9 +631,10 @@ const char *goal;
if (!force)
Strcpy(note, "aborted");
else
Sprintf(note, "use '%c', '%c', '%c', '%c' or '.'", /* hjkl */
Sprintf(note, "use '%c', '%c', '%c', '%c' or '%s'", /* hjkl */
Cmd.move_W, Cmd.move_S, Cmd.move_N,
Cmd.move_E);
Cmd.move_E,
visctrl(Cmd.spkeys[NHKF_GETPOS_PICK]));
pline("Unknown direction: '%s' (%s).", visctrl((char) c),
note);
msg_given = TRUE;

View File

@@ -2152,6 +2152,10 @@ int src;
parseoptions(bufp, TRUE, TRUE);
} else if (match_varname(buf, "AUTOPICKUP_EXCEPTION", 5)) {
add_autopickup_exception(bufp);
} else if (match_varname(buf, "BINDINGS", 4)) {
parsebindings(bufp);
} else if (match_varname(buf, "AUTOCOMPLETE", 5)) {
parseautocomplete(bufp, TRUE);
} else if (match_varname(buf, "MSGTYPE", 7)) {
(void) msgtype_parse_add(bufp);
#ifdef NOCWD_ASSUMPTIONS

View File

@@ -18,6 +18,7 @@
char * ucase (char *)
char * upstart (char *)
char * mungspaces (char *)
char * trimspaces (char *)
char * strip_newline (char *)
char * eos (char *)
boolean str_end_is (const char *, const char *)
@@ -159,6 +160,22 @@ char *bp;
return bp;
}
/* remove leading and trailing whitespace, in place */
char*
trimspaces(txt)
char* txt;
{
char* end;
while (*txt == ' ' || *txt == '\t')
txt++;
end = eos(txt);
while (--end >= txt && (*end == ' ' || *end == '\t'))
*end = '\0';
return txt;
}
/* remove \n from end of line; remove \r too if one is there */
char *
strip_newline(str)
@@ -385,13 +402,17 @@ char *sbuf;
return strcpy(sbuf, buf);
}
#define VISCTRL_NBUF 5
/* make a displayable string from a character */
char *
visctrl(c)
char c;
{
Static char ccc[5];
Static char visctrl_bufs[VISCTRL_NBUF][5];
static int nbuf = 0;
register int i = 0;
char *ccc = visctrl_bufs[nbuf];
nbuf = (nbuf + 1) % VISCTRL_NBUF;
if ((uchar) c & 0200) {
ccc[i++] = 'M';

View File

@@ -833,6 +833,10 @@ int maxlen;
* has the effect of 'meta'-ing the value which follows (so that the
* alternate character set will be enabled).
*
* X normal key X
* ^X control-X
* \mX meta-X
*
* For 3.4.3 and earlier, input ending with "\M", backslash, or caret
* prior to terminating '\0' would pull that '\0' into the output and then
* keep processing past it, potentially overflowing the output buffer.
@@ -3241,7 +3245,6 @@ boolean tinitial, tfrom_file;
if (negated) {
bad_negation(fullname, FALSE);
} else if ((op = string_for_opt(opts, FALSE)) != 0) {
int j;
char c, op_buf[BUFSZ];
escapes(op, op_buf);
@@ -3475,6 +3478,56 @@ boolean tinitial, tfrom_file;
badoption(opts);
}
/* parse key:command */
void
parsebindings(bindings)
char* bindings;
{
char *bind;
char key;
int i;
/* break off first binding from the rest; parse the rest */
if ((bind = index(bindings, ',')) != 0) {
*bind++ = 0;
parsebindings(bind);
}
/* parse a single binding: first split around : */
if (! (bind = index(bindings, ':'))) return; /* it's not a binding */
*bind++ = 0;
/* read the key to be bound */
key = txt2key(bindings);
if (!key) {
raw_printf("Bad binding %s.", bindings);
wait_synch();
return;
}
bind = trimspaces(bind);
/* is it a special key? */
if (bind_specialkey(key, bind))
return;
/* is it a menu command? */
for (i = 0; i < NUM_MENU_CMDS; i++) {
if (!strcmp(default_menu_cmd_info[i].name, bind)) {
if (illegal_menu_cmd_key(key)) {
char tmp[BUFSZ];
Sprintf(tmp, "Bad menu key %s:%s", visctrl(key), bind);
badoption(tmp);
} else
add_menu_cmd_alias(key, default_menu_cmd_info[i].cmd);
return;
}
}
/* extended command? */
bind_key(key, bind);
}
static NEARDATA const char *menutype[] = { "traditional", "combination",
"full", "partial" };

View File

@@ -1371,6 +1371,7 @@ whatdoes_help()
destroy_nhwindow(tmpwin);
}
#if 0
#define WD_STACKLIMIT 5
struct wd_stack_frame {
Bitfield(active, 1);
@@ -1501,18 +1502,31 @@ int *depth, lnum;
}
return stack[*depth].active ? TRUE : FALSE;
}
#endif /* 0 */
char *
dowhatdoes_core(q, cbuf)
char q;
char *cbuf;
{
dlb *fp;
char buf[BUFSZ];
#if 0
dlb *fp;
struct wd_stack_frame stack[WD_STACKLIMIT];
boolean cond;
int ctrl, meta, depth = 0, lnum = 0;
#endif /* 0 */
const char *ec_desc;
if ((ec_desc = key2extcmddesc(q)) != NULL) {
char keybuf[QBUFSZ];
Sprintf(buf, "%-8s%s.", key2txt(q, keybuf), ec_desc);
Strcpy(cbuf, buf);
return cbuf;
}
return 0;
#if 0
fp = dlb_fopen(CMDHELPFILE, "r");
if (!fp) {
pline("Cannot open \"%s\" data file!", CMDHELPFILE);
@@ -1568,6 +1582,7 @@ char *cbuf;
if (depth != 0)
impossible("cmdhelp: mismatched &? &: &. conditionals.");
return (char *) 0;
#endif /* 0 */
}
int
@@ -1718,6 +1733,7 @@ static struct {
{ hmenu_dowhatdoes, "Info on what a given key does." },
{ option_help, "List of game options." },
{ dispfile_optionfile, "Longer explanation of game options." },
{ dokeylist, "Full list of keyboard commands" },
{ hmenu_doextlist, "List of extended commands." },
{ dispfile_license, "The NetHack license." },
{ docontact, "Support information." },