Skip to content

Commit 00fb481

Browse files
committed
Add heuristic method to catch potentially disruptive keybinds
1 parent 9c87028 commit 00fb481

File tree

5 files changed

+71
-36
lines changed

5 files changed

+71
-36
lines changed

docs/dev/Lua API.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,6 +1471,12 @@ Hotkey module
14711471
Reads the latest saved keybind input that was requested.
14721472
Returns a keyspec string for the input, or nil if no input has been saved.
14731473

1474+
* ``dfhack.hotkey.isDisruptiveKeybind(keyspec)``
1475+
1476+
Determines if the provided keyspec could be disruptive to the game experience.
1477+
This includes the majority of standard characters and special keys such as escape,
1478+
backspace, and return when lacking modifiers other than Shift.
1479+
14741480
Units module
14751481
------------
14761482

library/LuaApi.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1874,6 +1874,13 @@ static bool hotkey_addKeybind(const std::string spec, const std::string cmd) {
18741874
return hotkey_mgr->addKeybind(spec, cmd);
18751875
}
18761876

1877+
static bool hotkey_isDisruptiveKeybind(const std::string spec) {
1878+
auto key = Hotkey::KeySpec::parse(spec);
1879+
if (!key.has_value())
1880+
return true;
1881+
return key.value().isDisruptive();
1882+
}
1883+
18771884
static int hotkey_requestKeybindingInput(lua_State *L) {
18781885
auto hotkey_mgr = Core::getInstance().getHotkeyManager();
18791886
if (!hotkey_mgr) return 0;
@@ -1935,7 +1942,7 @@ void hotkey_pushBindArray(lua_State *L, const std::vector<Hotkey::KeyBinding>& b
19351942
lua_createtable(L, 0, 2);
19361943

19371944
lua_pushlstring(L, "spec", 4);
1938-
auto spec_str = Hotkey::keyspec_to_string(bind.spec, true);
1945+
auto spec_str = bind.spec.toString(true);
19391946
lua_pushlstring(L, spec_str.data(), spec_str.size());
19401947
lua_settable(L, -3);
19411948

@@ -1973,6 +1980,7 @@ static const luaL_Reg dfhack_hotkey_funcs[] = {
19731980

19741981
static const LuaWrapper::FunctionReg dfhack_hotkey_module[] = {
19751982
WRAPN(addKeybind, hotkey_addKeybind),
1983+
WRAPN(isDisruptiveKeybind, hotkey_isDisruptiveKeybind),
19761984
{ NULL, NULL }
19771985
};
19781986

library/include/modules/Hotkey.h

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,26 @@
1111

1212
namespace DFHack {
1313
namespace Hotkey {
14-
struct KeySpec {
15-
int modifiers = 0;
16-
// Negative numbers denote mouse buttons
17-
int sym = 0;
18-
std::vector<std::string> focus;
14+
class DFHACK_EXPORT KeySpec {
15+
public:
16+
int modifiers = 0;
17+
// Negative numbers denote mouse buttons
18+
int sym = 0;
19+
std::vector<std::string> focus;
20+
21+
static std::optional<Hotkey::KeySpec> parse(std::string spec, std::string* err = nullptr);
22+
std::string toString(bool include_focus=true) const;
23+
24+
// Determines if a keybind could be disruptive to normal gameplay,
25+
// including typing and navigating the UI.
26+
bool isDisruptive() const;
1927
};
2028

2129
struct KeyBinding {
2230
KeySpec spec;
2331
std::string command;
2432
std::string cmdline;
2533
};
26-
27-
DFHACK_EXPORT std::optional<Hotkey::KeySpec> parseKeySpec(std::string spec, std::string* err = nullptr);
28-
DFHACK_EXPORT std::string keyspec_to_string(const KeySpec& spec, bool include_focus=false);
2934
}
3035
class DFHACK_EXPORT HotkeyManager {
3136
friend class Core;

library/modules/Hotkey.cpp

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -38,39 +38,38 @@ bool operator==(const KeyBinding& a, const KeyBinding& b) {
3838
a.cmdline == b.cmdline;
3939
}
4040

41-
std::string Hotkey::keyspec_to_string(const KeySpec &spec, bool include_focus) {
42-
std::string sym;
43-
if (spec.modifiers & DFH_MOD_CTRL) sym += "Ctrl-";
44-
if (spec.modifiers & DFH_MOD_ALT) sym += "Alt-";
45-
if (spec.modifiers & DFH_MOD_SUPER) sym += "Super-";
46-
if (spec.modifiers & DFH_MOD_SHIFT) sym += "Shift-";
41+
std::string KeySpec::toString(bool include_focus) const {
42+
std::string out;
43+
if (modifiers & DFH_MOD_CTRL) out += "Ctrl-";
44+
if (modifiers & DFH_MOD_ALT) out += "Alt-";
45+
if (modifiers & DFH_MOD_SUPER) out += "Super-";
46+
if (modifiers & DFH_MOD_SHIFT) out += "Shift-";
4747

4848
std::string key_name;
49-
if (spec.sym < 0) {
50-
key_name = "MOUSE" + std::to_string(-spec.sym);
49+
if (this->sym < 0) {
50+
key_name = "MOUSE" + std::to_string(-this->sym);
5151
} else {
52-
key_name = DFSDL::DFSDL_GetKeyName(spec.sym);
52+
key_name = DFSDL::DFSDL_GetKeyName(this->sym);
5353
}
54-
sym += key_name;
54+
out += key_name;
5555

56-
if (include_focus && !spec.focus.empty()) {
57-
sym += "@";
56+
if (include_focus && !this->focus.empty()) {
57+
out += "@";
5858
bool first = true;
59-
for (const auto& focus : spec.focus) {
59+
for (const auto& fc : this->focus) {
6060
if (first) {
6161
first = false;
62-
sym += focus;
62+
out += fc;
6363
} else {
64-
sym += "|" + focus;
64+
out += "|" + fc;
6565
}
6666
}
6767
}
6868

69-
return sym;
69+
return out;
7070
}
7171

72-
73-
std::optional<KeySpec> Hotkey::parseKeySpec(std::string spec, std::string* err) {
72+
std::optional<KeySpec> KeySpec::parse(std::string spec, std::string* err) {
7473
KeySpec out;
7574

7675
// Determine focus string, if present
@@ -126,6 +125,23 @@ std::optional<KeySpec> Hotkey::parseKeySpec(std::string spec, std::string* err)
126125
return std::nullopt;
127126
}
128127

128+
bool KeySpec::isDisruptive() const {
129+
// Miscellaneous essential keys
130+
const std::string essential_key_set = "\r\x1B\b\t -=[]\\;',./";
131+
132+
// Letters A-Z, 0-9, and other special keys such as return/escape, and other general typing keys
133+
bool is_essential_key = (this->sym >= SDLK_a && this->sym <= SDLK_z)
134+
|| (this->sym >= SDLK_0 && this->sym <= SDLK_9)
135+
|| essential_key_set.find(this->sym) != std::string::npos
136+
|| (this->sym >= SDLK_LEFT && this->sym <= SDLK_UP);
137+
138+
// Essential keys are safe, so long as they have a modifier that isn't Shift
139+
if (is_essential_key && !(this->modifiers & ~DFH_MOD_SHIFT))
140+
return true;
141+
142+
return false;
143+
}
144+
129145
// Hotkeys actions are executed from an external thread to avoid deadlocks
130146
// that may occur if running commands from the render or simulation threads.
131147
void HotkeyManager::hotkey_thread_fn() {
@@ -179,7 +195,7 @@ bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) {
179195
}
180196

181197
bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) {
182-
std::optional<KeySpec> spec_opt = Hotkey::parseKeySpec(keyspec);
198+
std::optional<KeySpec> spec_opt = KeySpec::parse(keyspec);
183199
if (!spec_opt.has_value())
184200
return false;
185201
return this->addKeybind(spec_opt.value(), cmd);
@@ -205,7 +221,7 @@ bool HotkeyManager::removeKeybind(const KeySpec& spec, bool match_focus, std::st
205221
}
206222

207223
bool HotkeyManager::removeKeybind(std::string keyspec, bool match_focus, std::string_view cmdline) {
208-
std::optional<KeySpec> spec_opt = Hotkey::parseKeySpec(keyspec);
224+
std::optional<KeySpec> spec_opt = KeySpec::parse(keyspec);
209225
if (!spec_opt.has_value())
210226
return false;
211227
return this->removeKeybind(spec_opt.value(), match_focus, cmdline);
@@ -243,7 +259,7 @@ std::vector<std::string> HotkeyManager::listKeybinds(const KeySpec& spec) {
243259

244260
std::vector<std::string> HotkeyManager::listKeybinds(std::string keyspec) {
245261
std::lock_guard<std::mutex> l(lock);
246-
std::optional<KeySpec> spec_opt = Hotkey::parseKeySpec(keyspec);
262+
std::optional<KeySpec> spec_opt = KeySpec::parse(keyspec);
247263
if (!spec_opt.has_value())
248264
return {};
249265
return this->listKeybinds(spec_opt.value());
@@ -305,7 +321,7 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) {
305321
KeySpec spec;
306322
spec.sym = sym;
307323
spec.modifiers = modifiers;
308-
requested_keybind = Hotkey::keyspec_to_string(spec);
324+
requested_keybind = spec.toString(false);
309325
keybind_save_requested = false;
310326
return true;
311327
}
@@ -377,7 +393,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto
377393
if (parts[0] == "set")
378394
removeKeybind(keystr);
379395
for (const auto& part : parts | std::views::drop(2) | std::views::reverse) {
380-
auto spec = Hotkey::parseKeySpec(keystr, &parse_error);
396+
auto spec = KeySpec::parse(keystr, &parse_error);
381397
if (!spec.has_value()) {
382398
con.printerr("%s\n", parse_error.c_str());
383399
break;
@@ -390,7 +406,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto
390406
}
391407
else if (parts.size() >= 2 && parts[0] == "clear") {
392408
for (const auto& part : parts | std::views::drop(1)) {
393-
auto spec = Hotkey::parseKeySpec(part, &parse_error);
409+
auto spec = KeySpec::parse(part, &parse_error);
394410
if (!spec.has_value()) {
395411
con.printerr("%s\n", parse_error.c_str());
396412
}
@@ -401,7 +417,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto
401417
}
402418
}
403419
else if (parts.size() == 2 && parts[0] == "list") {
404-
auto spec = Hotkey::parseKeySpec(parts[1], &parse_error);
420+
auto spec = KeySpec::parse(parts[1], &parse_error);
405421
if (!spec.has_value()) {
406422
con.printerr("%s\n", parse_error.c_str());
407423
return;
@@ -419,7 +435,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto
419435
<< " keybinding set <key>[@context] \"cmdline\" \"cmdline\"..." << std::endl
420436
<< " keybinding add <key>[@context] \"cmdline\" \"cmdline\"..." << std::endl
421437
<< "Later adds, and earlier items within one command have priority." << std::endl
422-
<< "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl
438+
<< "Supported keys: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.)." << std::endl
423439
<< "Context may be used to limit the scope of the binding, by" << std::endl
424440
<< "requiring the current context to have a certain prefix." << std::endl
425441
<< "Current UI context is: " << std::endl

plugins/hotkeys.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ static void find_active_keybindings(color_ostream &out, df::viewscreen *screen,
9494

9595
auto active_binds = Core::getInstance().getHotkeyManager()->listActiveKeybinds();
9696
for (const auto& bind : active_binds) {
97-
string sym = Hotkey::keyspec_to_string(bind.spec);
97+
string sym = bind.spec.toString(false);
9898
add_binding_if_valid(out, sym, bind.cmdline, screen, filtermenu);
9999
}
100100

0 commit comments

Comments
 (0)