From 207e443b82d3161cd4d8e9e4961dd50ae215a5ac Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 19 Nov 2025 20:52:33 -0500 Subject: [PATCH 01/18] Refactor hotkey code into a new Hotkey module --- library/CMakeLists.txt | 2 + library/Core.cpp | 333 ++---------------- library/include/Core.h | 39 +- library/include/modules/DFSDL.h | 4 + library/include/modules/Hotkey.h | 57 +++ library/modules/DFSDL.cpp | 18 + library/modules/Hotkey.cpp | 300 ++++++++++++++++ plugins/blueprint.cpp | 3 +- plugins/embark-assistant/embark-assistant.cpp | 3 +- plugins/hotkeys.cpp | 68 ++-- 10 files changed, 442 insertions(+), 385 deletions(-) create mode 100644 library/include/modules/Hotkey.h create mode 100644 library/modules/Hotkey.cpp diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index fa5c482bf6..6f91c6c08b 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -156,6 +156,7 @@ set(MODULE_HEADERS include/modules/Graphic.h include/modules/Gui.h include/modules/GuiHooks.h + include/modules/Hotkey.h include/modules/Items.h include/modules/Job.h include/modules/Kitchen.h @@ -186,6 +187,7 @@ set(MODULE_SOURCES modules/Filesystem.cpp modules/Graphic.cpp modules/Gui.cpp + modules/Hotkey.cpp modules/Items.cpp modules/Job.cpp modules/Kitchen.cpp diff --git a/library/Core.cpp b/library/Core.cpp index 33f66d767e..6cc60aaf85 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -47,6 +47,7 @@ distribution. #include "modules/EventManager.h" #include "modules/Filesystem.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/Textures.h" #include "modules/World.h" #include "modules/Persistence.h" @@ -104,11 +105,9 @@ using std::string; // FIXME: A lot of code in one file, all doing different things... there's something fishy about it. -static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus = nullptr); static size_t loadScriptFiles(Core* core, color_ostream& out, std::span prefix, const std::filesystem::path& folder); namespace DFHack { - DBG_DECLARE(core, keybinding, DebugCategory::LINFO); DBG_DECLARE(core, script, DebugCategory::LINFO); @@ -274,34 +273,6 @@ struct IODATA PluginManager * plug_mgr; }; -// A thread function... for handling hotkeys. This is needed because -// all the plugin commands are expected to be run from foreign threads. -// Running them from one of the main DF threads will result in deadlock! -static void fHKthread(IODATA * iodata) -{ - Core * core = iodata->core; - PluginManager * plug_mgr = iodata->plug_mgr; - if(!plug_mgr || !core) - { - std::cerr << "Hotkey thread has croaked." << std::endl; - return; - } - bool keep_going = true; - while(keep_going) - { - std::string stuff = core->getHotkeyCmd(keep_going); // waits on mutex! - if(!stuff.empty()) - { - color_ostream_proxy out(core->getConsole()); - - auto rv = core->runCommand(out, stuff); - - if (rv == CR_NOT_IMPLEMENTED) - out.printerr("Invalid hotkey command: '%s'\n", stuff.c_str()); - } - } -} - static std::string dfhack_version_desc() { std::stringstream s; @@ -990,11 +961,11 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s { std::string keystr = parts[1]; if (parts[0] == "set") - ClearKeyBindings(keystr); + hotkey_mgr->clearKeybind(keystr); // for (int i = parts.size()-1; i >= 2; i--) for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { - if (!AddKeyBinding(keystr, part)) { + if (!hotkey_mgr->addKeybind(keystr, part)) { con.printerr("Invalid key spec: %s\n", keystr.c_str()); break; } @@ -1005,7 +976,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s // for (size_t i = 1; i < parts.size(); i++) for (const auto& part : parts | std::views::drop(1)) { - if (!ClearKeyBindings(part)) { + if (!hotkey_mgr->clearKeybind(part)) { con.printerr("Invalid key spec: %s\n", part.c_str()); break; } @@ -1013,7 +984,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (parts.size() == 2 && parts[0] == "list") { - std::vector list = ListKeyBindings(parts[1]); + std::vector list = hotkey_mgr->listKeybinds(parts[1]); if (list.empty()) con << "No bindings." << std::endl; for (const auto& kb : list) @@ -1471,8 +1442,7 @@ Core::~Core() Core::Core() : d(std::make_unique()), script_path_mutex{}, - HotkeyMutex{}, - HotkeyCond{}, + armok_mutex{}, alias_mutex{}, started{false}, CoreSuspendMutex{}, @@ -1488,7 +1458,6 @@ Core::Core() : // set up hotkey capture suppress_duplicate_keyboard_events = true; - hotkey_set = NO; last_world_data_ptr = nullptr; last_local_map_ptr = nullptr; last_pause_state = false; @@ -1841,6 +1810,8 @@ bool Core::InitSimulationThread() plug_mgr->init(); std::cerr << "Starting the TCP listener.\n"; auto listen = ServerMain::listen(RemoteClient::GetDefaultPort()); + this->hotkey_mgr = new HotkeyManager(); + auto *temp = new IODATA; temp->core = this; temp->plug_mgr = plug_mgr; @@ -1858,8 +1829,6 @@ bool Core::InitSimulationThread() } std::cerr << "Starting DF input capture thread.\n"; - // set up hotkey capture - d->hotkeythread = std::thread(fHKthread, temp); started = true; modstate = 0; @@ -1933,31 +1902,6 @@ bool Core::InitSimulationThread() return true; } -/// sets the current hotkey command -bool Core::setHotkeyCmd( std::string cmd ) -{ - // access command - std::lock_guard lock(HotkeyMutex); - hotkey_set = SET; - hotkey_cmd = std::move(cmd); - HotkeyCond.notify_all(); - return true; -} -/// removes the hotkey command and gives it to the caller thread -std::string Core::getHotkeyCmd( bool &keep_going ) -{ - std::string returner; - std::unique_lock lock(HotkeyMutex); - HotkeyCond.wait(lock, [this]() -> bool {return this->hotkey_set;}); - if (hotkey_set == SHUTDOWN) { - keep_going = false; - return returner; - } - hotkey_set = NO; - returner = hotkey_cmd; - hotkey_cmd.clear(); - return returner; -} void Core::print(const char *format, ...) { @@ -2405,17 +2349,14 @@ int Core::Shutdown ( void ) con.shutdown(); } - if (d->hotkeythread.joinable()) { - std::unique_lock hot_lock(HotkeyMutex); - hotkey_set = SHUTDOWN; - HotkeyCond.notify_one(); - } - ServerMain::block(); - d->hotkeythread.join(); d->iothread.join(); + if (hotkey_mgr) { + delete hotkey_mgr; + } + if(plug_mgr) { delete plug_mgr; @@ -2490,16 +2431,24 @@ void Core::setSuppressDuplicateKeyboardEvents(bool suppress) { } void Core::setMortalMode(bool value) { - std::lock_guard lock(HotkeyMutex); - mortal_mode = value; + mortal_mode.exchange(value); +} + +bool Core::getMortalMode() { + return mortal_mode.load(); } void Core::setArmokTools(const std::vector &tool_names) { - std::lock_guard lock(HotkeyMutex); + std::lock_guard lock(armok_mutex); armok_tools.clear(); armok_tools.insert(tool_names.begin(), tool_names.end()); } +bool Core::isArmokTool(const std::string& tool) { + std::lock_guard lock(armok_mutex); + return armok_tools.contains(tool); +} + // returns true if the event is handled bool Core::DFH_SDL_Event(SDL_Event* ev) { uint32_t start_ms = p->getTickCount(); @@ -2540,7 +2489,7 @@ bool Core::doSdlInputEvent(SDL_Event* ev) { // the check against hotkey_states[sym] ensures we only process keybindings once per keypress DEBUG(keybinding).print("key down: sym=%d (%c)\n", sym, sym); - if (SelectHotkey(sym, modstate)) { + if (hotkey_mgr->handleKeybind(sym, modstate)) { hotkey_states[sym] = true; if (modstate & (DFH_MOD_CTRL | DFH_MOD_ALT)) { DEBUG(keybinding).print("modifier key detected; not inhibiting SDL key down event\n"); @@ -2561,8 +2510,8 @@ bool Core::doSdlInputEvent(SDL_Event* ev) DEBUG(keybinding).print("mouse button down: button=%d\n", but.button); // don't mess with the first three buttons, which are critical elements of DF's control scheme if (but.button > 3) { - SDL_Keycode sym = SDLK_F13 + but.button - 4; - if (sym <= SDLK_F24 && SelectHotkey(sym, modstate)) + SDL_Keycode sym = -but.button; + if (sym >= -15 && sym <= -1 && hotkey_mgr->handleKeybind(sym, modstate)) return suppress_duplicate_keyboard_events; } } else if (ev->type == SDL_TEXTINPUT) { @@ -2582,236 +2531,6 @@ bool Core::doSdlInputEvent(SDL_Event* ev) return false; } -bool Core::SelectHotkey(int sym, int modifiers) -{ - // Find the topmost viewscreen - if (!df::global::gview || !df::global::plotinfo) - return false; - - df::viewscreen *screen = &df::global::gview->view; - while (screen->child) - screen = screen->child; - - if (sym == SDLK_KP_ENTER) - sym = SDLK_RETURN; - - std::string cmd; - - DEBUG(keybinding).print("checking hotkeys for sym=%d (%c), modifiers=%x\n", sym, sym, modifiers); - - { - std::lock_guard lock(HotkeyMutex); - - // Check the internal keybindings - std::vector &bindings = key_bindings[sym]; - //for (int i = bindings.size()-1; i >= 0; --i) { - for (const auto& binding : bindings | std::views::reverse) { - DEBUG(keybinding).print("examining hotkey with commandline: '%s'\n", binding.cmdline.c_str()); - - if (binding.modifiers != modifiers) { - DEBUG(keybinding).print("skipping keybinding due to modifiers mismatch: 0x%x != 0x%x\n", - binding.modifiers, modifiers); - continue; - } - if (!binding.focus.empty()) { - if (!Gui::matchFocusString(binding.focus)) { - std::vector focusStrings = Gui::getCurFocus(true); - DEBUG(keybinding).print("skipping keybinding due to focus string mismatch: '%s' != '%s'\n", - join_strings(", ", focusStrings).c_str(), binding.focus.c_str()); - continue; - } - } - if (!plug_mgr->CanInvokeHotkey(binding.command[0], screen)) { - DEBUG(keybinding).print("skipping keybinding due to hotkey guard rejection (command: '%s')\n", - binding.command[0].c_str()); - continue; - } - if (mortal_mode && armok_tools.contains(binding.command[0])) { - DEBUG(keybinding).print("skipping keybinding due to mortal mode (command: '%s')\n", - binding.command[0].c_str()); - continue; - } - - cmd = binding.cmdline; - DEBUG(keybinding).print("matched hotkey\n"); - break; - } - - if (cmd.empty()) { - // Check the hotkey keybindings - int idx = sym - SDLK_F1; - if(idx >= 0 && idx < 8) - { -/* TODO: understand how this changes for v50 - if (modifiers & 1) - idx += 8; - - if (strict_virtual_cast(screen) && - df::global::plotinfo->main.mode != ui_sidebar_mode::Hotkeys && - df::global::plotinfo->main.hotkeys[idx].cmd == df::ui_hotkey::T_cmd::None) - { - cmd = df::global::plotinfo->main.hotkeys[idx].name; - } -*/ - } - } - } - - if (!cmd.empty()) { - setHotkeyCmd(cmd); - return true; - } - - return false; -} - -static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus) -{ - *pmod = 0; - - if (pfocus) - { - *pfocus = ""; - - size_t idx = keyspec.find('@'); - if (idx != std::string::npos) - { - *pfocus = keyspec.substr(idx+1); - keyspec = keyspec.substr(0, idx); - } - } - - // ugh, ugly - for (;;) { - if (keyspec.size() > 6 && keyspec.starts_with("Shift-")) { - *pmod |= 1; - keyspec = keyspec.substr(6); - } else if (keyspec.size() > 5 && keyspec.starts_with("Ctrl-")) { - *pmod |= 2; - keyspec = keyspec.substr(5); - } else if (keyspec.size() > 4 && keyspec.starts_with("Alt-")) { - *pmod |= 4; - keyspec = keyspec.substr(4); - } else - break; - } - - if (keyspec.size() == 1 && keyspec[0] >= 'A' && keyspec[0] <= 'Z') { - *psym = SDLK_a + (keyspec[0]-'A'); - return true; - } else if (keyspec.size() == 1 && keyspec[0] == '`') { - *psym = SDLK_BACKQUOTE; - return true; - } else if (keyspec.size() == 1 && keyspec[0] >= '0' && keyspec[0] <= '9') { - *psym = SDLK_0 + (keyspec[0]-'0'); - return true; - } else if (keyspec.size() == 2 && keyspec[0] == 'F' && keyspec[1] >= '1' && keyspec[1] <= '9') { - *psym = SDLK_F1 + (keyspec[1]-'1'); - return true; - } else if (keyspec.size() == 3 && keyspec.starts_with("F1") && keyspec[2] >= '0' && keyspec[2] <= '2') { - *psym = SDLK_F10 + (keyspec[2]-'0'); - return true; - } else if (keyspec.size() == 6 && keyspec.starts_with("MOUSE") && keyspec[5] >= '4' && keyspec[5] <= '9') { - *psym = SDLK_F13 + (keyspec[5]-'4'); - return true; - } else if (keyspec.size() == 7 && keyspec.starts_with("MOUSE1") && keyspec[5] >= '0' && keyspec[5] <= '5') { - *psym = SDLK_F19 + (keyspec[5]-'0'); - return true; - } else if (keyspec == "Enter") { - *psym = SDLK_RETURN; - return true; - } else - return false; -} - -bool Core::ClearKeyBindings(std::string keyspec) -{ - int sym, mod; - std::string focus; - if (!parseKeySpec(keyspec, &sym, &mod, &focus)) - return false; - - std::lock_guard lock(HotkeyMutex); - - std::vector &bindings = key_bindings[sym]; - for (int i = bindings.size()-1; i >= 0; --i) { - if (bindings[i].modifiers == mod && prefix_matches(focus, bindings[i].focus)) - bindings.erase(bindings.begin()+i); - } - - return true; -} - -bool Core::AddKeyBinding(std::string keyspec, std::string cmdline) -{ - size_t at_pos = keyspec.find('@'); - if (at_pos != std::string::npos) - { - std::string raw_spec = keyspec.substr(0, at_pos); - std::string raw_focus = keyspec.substr(at_pos + 1); - if (raw_focus.find('|') != std::string::npos) - { - std::vector focus_strings; - split_string(&focus_strings, raw_focus, "|"); - for (const auto& fs : focus_strings) - { - if (!AddKeyBinding(raw_spec + "@" + fs, cmdline)) - return false; - } - return true; - } - } - int sym; - KeyBinding binding; - if (!parseKeySpec(keyspec, &sym, &binding.modifiers, &binding.focus)) - return false; - - cheap_tokenise(cmdline, binding.command); - if (binding.command.empty()) - return false; - - std::lock_guard lock(HotkeyMutex); - - // Don't add duplicates - std::vector &bindings = key_bindings[sym]; - for (int i = bindings.size()-1; i >= 0; --i) { - if (bindings[i].modifiers == binding.modifiers && - bindings[i].cmdline == cmdline && - bindings[i].focus == binding.focus) - return true; - } - - binding.cmdline = cmdline; - bindings.push_back(binding); - return true; -} - -std::vector Core::ListKeyBindings(std::string keyspec) -{ - int sym, mod; - std::vector rv; - std::string focus; - if (!parseKeySpec(keyspec, &sym, &mod, &focus)) - return rv; - - std::lock_guard lock(HotkeyMutex); - - std::vector &bindings = key_bindings[sym]; - for (int i = bindings.size()-1; i >= 0; --i) { - if (focus.size() && focus != bindings[i].focus) - continue; - if (bindings[i].modifiers == mod) - { - std::string cmd = bindings[i].cmdline; - if (!bindings[i].focus.empty()) - cmd = "@" + bindings[i].focus + ": " + cmd; - rv.push_back(cmd); - } - } - - return rv; -} - bool Core::AddAlias(const std::string &name, const std::vector &command, bool replace) { std::lock_guard lock(alias_mutex); diff --git a/library/include/Core.h b/library/include/Core.h index 3ea8f68ef1..08c7ca3d2e 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -68,6 +68,7 @@ namespace DFHack class Core; class ServerMain; class CoreSuspender; + class HotkeyManager; namespace Lua { namespace Core { DFHACK_EXPORT void Reset(color_ostream &out, const char *where); @@ -164,10 +165,6 @@ namespace DFHack Materials * getMaterials(); /// get the graphic module Graphic * getGraphic(); - /// sets the current hotkey command - bool setHotkeyCmd( std::string cmd ); - /// removes the hotkey command and gives it to the caller thread - std::string getHotkeyCmd( bool &keep_going ); command_result runCommand(color_ostream &out, const std::string &command, std::vector ¶meters, bool no_autocomplete = false); command_result runCommand(color_ostream &out, const std::string &command); @@ -182,11 +179,10 @@ namespace DFHack bool getSuppressDuplicateKeyboardEvents() const; void setSuppressDuplicateKeyboardEvents(bool suppress); void setMortalMode(bool value); + bool getMortalMode(); void setArmokTools(const std::vector &tool_names); + bool isArmokTool(const std::string& name); - bool ClearKeyBindings(std::string keyspec); - bool AddKeyBinding(std::string keyspec, std::string cmdline); - std::vector ListKeyBindings(std::string keyspec); int8_t getModstate() { return modstate; } bool AddAlias(const std::string &name, const std::vector &command, bool replace = false); @@ -213,6 +209,7 @@ namespace DFHack static void printerr(const char *format, ...) Wformat(printf,1,2); PluginManager *getPluginManager() { return plug_mgr; } + HotkeyManager *getHotkeyManager() { return hotkey_mgr; } static void cheap_tokenise(std::string const& input, std::vector &output); @@ -267,39 +264,25 @@ namespace DFHack Graphic * pGraphic; } s_mods; std::vector> allModules; - DFHack::PluginManager * plug_mgr; + DFHack::PluginManager *plug_mgr; + // Hotkey Manager + DFHack::HotkeyManager *hotkey_mgr; + + // FIXME: remove all this junk (hotkey related) std::vector script_paths[3]; std::mutex script_path_mutex; - // hotkey-related stuff - struct KeyBinding { - int modifiers; - std::vector command; - std::string cmdline; - std::string focus; - }; int8_t modstate; bool suppress_duplicate_keyboard_events; - bool mortal_mode; + std::atomic mortal_mode; std::unordered_set armok_tools; - std::map > key_bindings; - std::string hotkey_cmd; - enum hotkey_set_t { - NO, - SET, - SHUTDOWN, - }; - hotkey_set_t hotkey_set; - std::mutex HotkeyMutex; - std::condition_variable HotkeyCond; + std::mutex armok_mutex; std::map> aliases; std::recursive_mutex alias_mutex; - bool SelectHotkey(int key, int modifiers); - // for state change tracking df::world_data *last_world_data_ptr; // for state change tracking diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index ff8e81ab2c..161cc35171 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -4,12 +4,14 @@ #include "ColorText.h" #include +#include struct SDL_Surface; struct SDL_Rect; struct SDL_PixelFormat; struct SDL_Window; union SDL_Event; +typedef int32_t SDL_Keycode; namespace DFHack { @@ -63,6 +65,8 @@ namespace DFHack::DFSDL DFHACK_EXPORT char* DFSDL_GetPrefPath(const char* org, const char* app); DFHACK_EXPORT char* DFSDL_GetBasePath(); + DFHACK_EXPORT SDL_Keycode DFSDL_GetKeyFromName(const char* name); + DFHACK_EXPORT const char* DFSDL_GetKeyName(SDL_Keycode key); } namespace DFHack diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h new file mode 100644 index 0000000000..581ebe43e0 --- /dev/null +++ b/library/include/modules/Hotkey.h @@ -0,0 +1,57 @@ +#pragma once + +#include "Export.h" + +#include +#include +#include +#include +#include + +namespace DFHack { + class DFHACK_EXPORT HotkeyManager { + public: + HotkeyManager(); + ~HotkeyManager(); + + struct KeySpec { + int modifiers = 0; + // Negative numbers denote mouse buttons + int sym = 0; + std::vector focus; + }; + + struct KeyBinding { + HotkeyManager::KeySpec spec; + std::string command; + std::string cmdline; + }; + + bool addKeybind(std::string keyspec, std::string cmd); + bool addKeybind(KeySpec spec, std::string cmd); + bool clearKeybind(std::string keyspec, bool any_focus=false); + bool clearKeybind(const KeySpec& spec, bool any_focus=false); + + std::vector listKeybinds(std::string keyspec); + std::vector listKeybinds(const KeySpec& spec); + + std::vector listActiveKeybinds(); + + bool handleKeybind(int sym, int modifiers); + + void setHotkeyCommand(std::string cmd); + + std::optional parseKeySpec(std::string spec); + private: + std::thread hotkey_thread; + std::mutex lock {}; + std::condition_variable cond {}; + + int hotkey_sig = 0; + std::string queued_command = ""; + + std::map> bindings; + + void hotkey_thread_fn(); + }; +} diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 9da2bd8057..028422d75a 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -7,6 +7,7 @@ #include "PluginManager.h" #include +#include #ifdef WIN32 # include @@ -62,6 +63,9 @@ int (*g_SDL_ShowSimpleMessageBox)(uint32_t flags, const char *title, const char char* (*g_SDL_GetPrefPath)(const char* org, const char* app) = nullptr; char* (*g_SDL_GetBasePath)() = nullptr; +SDL_Keycode (*g_SDL_GetKeyFromName)(const char* name) = nullptr; +const char* (*g_SDL_GetKeyName)(SDL_Keycode key) = nullptr; + bool DFSDL::init(color_ostream &out) { for (auto &lib_str : SDL_LIBS) { if ((g_sdl_handle = OpenPlugin(lib_str.c_str()))) @@ -106,6 +110,8 @@ bool DFSDL::init(color_ostream &out) { bind(g_sdl_handle, SDL_ShowSimpleMessageBox); bind(g_sdl_handle, SDL_GetPrefPath); bind(g_sdl_handle, SDL_GetBasePath); + bind(g_sdl_handle, SDL_GetKeyFromName); + bind(g_sdl_handle, SDL_GetKeyName); #undef bind DEBUG(dfsdl,out).print("sdl successfully loaded\n"); @@ -196,6 +202,18 @@ int DFSDL::DFSDL_ShowSimpleMessageBox(uint32_t flags, const char *title, const c return g_SDL_ShowSimpleMessageBox(flags, title, message, window); } +SDL_Keycode DFSDL::DFSDL_GetKeyFromName(const char* name) { + if (!g_SDL_GetKeyFromName) + return SDLK_UNKNOWN; + return g_SDL_GetKeyFromName(name); +} + +const char* DFSDL::DFSDL_GetKeyName(SDL_Keycode key) { + if (!g_SDL_GetKeyName) + return ""; + return g_SDL_GetKeyName(key); +} + // convert tabs to spaces so they don't get converted to '?' static char * tabs_to_spaces(char *str) { for (char *c = str; *c; ++c) { diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp new file mode 100644 index 0000000000..2cf87129d4 --- /dev/null +++ b/library/modules/Hotkey.cpp @@ -0,0 +1,300 @@ +#include "modules/Hotkey.h" +#include "Core.h" +#include "CoreDefs.h" +#include "ColorText.h" +#include "MiscUtils.h" +#include "modules/DFSDL.h" +#include "modules/Gui.h" +#include "PluginManager.h" +#include "df/global_objects.h" +#include "df/viewscreen.h" +#include "df/interfacest.h" + +#include + +#include "SDL_keycode.h" + +using namespace DFHack; + +enum HotkeySignal { + None = 0, + CmdReady, + Shutdown, +}; + +bool operator==(const HotkeyManager::KeySpec& a, const HotkeyManager::KeySpec& b) { + return a.modifiers == b.modifiers && a.sym == b.sym && + a.focus.size() == b.focus.size() && + std::equal(a.focus.begin(), a.focus.end(), b.focus.begin()); +} + +// Equality operator for key bindings +bool operator==(const HotkeyManager::KeyBinding& a, const HotkeyManager::KeyBinding& b) { + return a.spec == b.spec && + a.command == b.command && + a.cmdline == b.cmdline; +} + +// Hotkeys actions are executed from an external thread to avoid deadlocks +// that may occur if running commands from the render or simulation threads. +void HotkeyManager::hotkey_thread_fn() { + auto& core = DFHack::Core::getInstance(); + + std::unique_lock l(lock); + while (true) { + cond.wait(l, [this]() { return this->hotkey_sig != HotkeySignal::None; }); + if (hotkey_sig == HotkeySignal::Shutdown) + return; + if (hotkey_sig != HotkeySignal::CmdReady) + continue; + + // Copy and reset important data, then release the lock + this->hotkey_sig = HotkeySignal::None; + std::string cmd = this->queued_command; + this->queued_command.clear(); + l.unlock(); + + // Attempt execution of command + DFHack::color_ostream_proxy out(core.getConsole()); + auto res = core.runCommand(out, cmd); + if (res == DFHack::CR_NOT_IMPLEMENTED) + out.printerr("Invalid hotkey command: '%s'\n", cmd.c_str()); + l.lock(); + } +} + +std::optional HotkeyManager::parseKeySpec(std::string spec) { + KeySpec out; + + // Determine focus string, if present + size_t focus_idx = spec.find('@'); + if (focus_idx != std::string::npos) { + split_string(&out.focus, spec.substr(focus_idx + 1), "|"); + spec.erase(focus_idx); + } + + // Determine modifier flags + auto match_modifier = [&out, &spec](std::string_view prefix, int mod) { + bool found = spec.starts_with(prefix); + if (found) { + out.modifiers |= mod; + spec.erase(0, prefix.size()); + } + return found; + }; + while (match_modifier("Shift-", DFH_MOD_SHIFT) || match_modifier("Ctrl-", DFH_MOD_CTRL) || match_modifier("Alt-", DFH_MOD_ALT)) {} + + out.sym = DFSDL::DFSDL_GetKeyFromName(spec.c_str()); + if (out.sym != SDLK_UNKNOWN) + return out; + + // Attempt to parse as a mouse binding + if (spec.starts_with("MOUSE")) { + spec.erase(0, 5); + // Read button number, ensuring between 1 and 15 inclusive + try { + int mbutton = std::stoi(spec); + if (mbutton >= 1 && mbutton <= 15) { + out.sym = -mbutton; + return out; + } + } catch (...) { + // If integer parsing fails, it isn't valid + } + } + + // Invalid key binding + return std::nullopt; +} + +bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { + // No point in a hotkey with no action + if (cmd.empty()) + return false; + + KeyBinding binding; + binding.spec = spec; + binding.cmdline = cmd; + + std::lock_guard l(lock); + auto& bindings = this->bindings[binding.spec.sym]; + for (auto& bind : bindings) { + // Don't set a keybind twice, but return true as there isn't an issue + if (bind == binding) + return true; + } + + bindings.emplace_back(binding); + return true; +} + +bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { + std::optional spec_opt = parseKeySpec(keyspec); + if (!spec_opt.has_value()) + return false; + return this->addKeybind(spec_opt.value(), cmd); +} + +bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus) { + std::lock_guard l(lock); + if (!bindings.contains(spec.sym)) + return false; + auto& binds = bindings[spec.sym]; + + auto new_end = std::remove_if(binds.begin(), binds.end(), [any_focus, spec](const auto& v) { + return any_focus + ? v.spec.sym == spec.sym && v.spec.modifiers == spec.modifiers + : v.spec == spec; + }); + if (new_end == binds.end()) + return false; // No bindings removed + + binds.erase(new_end, binds.end()); + return true; +} + +bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus) { + std::optional spec_opt = parseKeySpec(keyspec); + if (!spec_opt.has_value()) + return false; + return this->clearKeybind(spec_opt.value(), any_focus); +} + +std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { + std::lock_guard l(lock); + if (!bindings.contains(spec.sym)) + return {}; + + std::vector out; + + auto& binds = bindings[spec.sym]; + for (const auto& bind : binds) { + if (bind.spec.modifiers != spec.modifiers) + continue; + + // If no focus is required, it is always active + if (spec.focus.empty() || bind.spec.focus.empty()) { + out.push_back(bind.cmdline); + continue; + } + + // If a focus is required, determine if search spec if the same or more specific + for (const auto& requested : spec.focus) { + for (const auto& to_match : bind.spec.focus) { + if (prefix_matches(to_match, requested)) + out.push_back("@" + to_match + ":" + bind.cmdline); + } + } + } + + return out; +} + +std::vector HotkeyManager::listKeybinds(std::string keyspec) { + std::optional spec_opt = parseKeySpec(keyspec); + if (!spec_opt.has_value()) + return {}; + return this->listKeybinds(spec_opt.value()); +} + +std::vector HotkeyManager::listActiveKeybinds() { + std::vector out; + + for(const auto& [_, bind_set] : bindings) { + for (const auto& binding : bind_set) { + if (binding.spec.focus.empty()) { + // Binding always active + out.emplace_back(binding); + continue; + } + for (const auto& focus : binding.spec.focus) { + // Determine if focus string allows this binding + if (Gui::matchFocusString(focus)) { + out.emplace_back(binding); + break; + } + } + } + } + + return out; +} + +bool HotkeyManager::handleKeybind(int sym, int modifiers) { + // Ensure gamestate is ready + if (!df::global::gview || !df::global::plotinfo) + return false; + + // Get bottommost active screen + df::viewscreen *screen = &df::global::gview->view; + while (screen->child) + screen = screen->child; + + if (sym == SDLK_KP_ENTER) + sym = SDLK_RETURN; + + std::unique_lock l(lock); + if (!bindings.contains(sym)) + return false; + auto& binds = bindings[sym]; + + auto& core = Core::getInstance(); + bool mortal_mode = core.getMortalMode(); + + for (const auto& bind : binds | std::views::reverse) { + if (bind.spec.modifiers != modifiers) + continue; + + if (!bind.spec.focus.empty()) { + bool matched = false; + for (const auto& focus : bind.spec.focus) { + printf("Focus check for: %s", focus.c_str()); + if (Gui::matchFocusString(focus)) { + printf("Matched\n"); + matched = true; + break; + } + } + if (!matched) + continue; + } + + if (!Core::getInstance().getPluginManager()->CanInvokeHotkey(bind.command, screen)) + continue; + + if (mortal_mode && core.isArmokTool(bind.command)) + continue; + + queued_command = bind.cmdline; + hotkey_sig = HotkeySignal::CmdReady; + l.unlock(); + cond.notify_all(); + return true; + } + + return false; +} + +void HotkeyManager::setHotkeyCommand(std::string cmd) { + std::unique_lock l(lock); + queued_command = cmd; + hotkey_sig = HotkeySignal::CmdReady; + l.unlock(); + cond.notify_all(); +} + +HotkeyManager::HotkeyManager() { + this->hotkey_thread = std::thread(&HotkeyManager::hotkey_thread_fn, this); +} + +HotkeyManager::~HotkeyManager() { + // Set shutdown signal and notify thread + { + std::lock_guard l(lock); + this->hotkey_sig = HotkeySignal::Shutdown; + } + cond.notify_all(); + + if (this->hotkey_thread.joinable()) + this->hotkey_thread.join(); +} diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 98b4c1c155..330a0938f9 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -19,6 +19,7 @@ #include "modules/Constructions.h" #include "modules/Filesystem.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/Maps.h" #include "modules/World.h" @@ -1668,7 +1669,7 @@ static command_result do_blueprint(color_ostream &out, string command_str = command.str(); out.print("launching %s\n", command_str.c_str()); - Core::getInstance().setHotkeyCmd(command_str); + Core::getInstance().getHotkeyManager()->setHotkeyCommand(command_str); return CR_OK; } diff --git a/plugins/embark-assistant/embark-assistant.cpp b/plugins/embark-assistant/embark-assistant.cpp index db04308d0b..5b9d11f78a 100644 --- a/plugins/embark-assistant/embark-assistant.cpp +++ b/plugins/embark-assistant/embark-assistant.cpp @@ -5,6 +5,7 @@ #include "PluginManager.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/Screen.h" #include "../uicommon.h" @@ -161,7 +162,7 @@ struct start_site_hook : df::viewscreen_choose_start_sitest { { if (!embark_assist::main::state && input->count(interface_key::CUSTOM_A)) { - Core::getInstance().setHotkeyCmd("embark-assistant"); + Core::getInstance().getHotkeyManager()->setHotkeyCommand("embark-assistant"); return; } INTERPOSE_NEXT(feed)(input); diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 13988a21f1..197b0d685d 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -2,8 +2,10 @@ #include #include +#include "modules/DFSDL.h" #include "modules/Gui.h" #include "modules/Screen.h" +#include "modules/Hotkey.h" #include "Debug.h" #include "LuaTools.h" @@ -44,7 +46,7 @@ static int cleanupHotkeys(lua_State *) { std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) { string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str()); - Core::getInstance().ClearKeyBindings(keyspec); + Core::getInstance().getHotkeyManager()->clearKeybind(keyspec); }); valid = false; sorted_keys.clear(); @@ -83,60 +85,30 @@ static void add_binding_if_valid(color_ostream &out, const string &sym, const st string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; string binding = "hotkeys invoke " + int_to_string(sorted_keys.size() - 1); DEBUG(log).print("adding keybinding: %s -> %s\n", keyspec.c_str(), binding.c_str()); - Core::getInstance().AddKeyBinding(keyspec, binding); + Core::getInstance().getHotkeyManager()->addKeybind(keyspec, binding); } static void find_active_keybindings(color_ostream &out, df::viewscreen *screen, bool filtermenu) { - DEBUG(log).print("scanning for active keybindings\n"); if (valid) cleanupHotkeys(NULL); - vector valid_keys; - - for (char c = '0'; c <= '9'; c++) { - valid_keys.push_back(string(&c, 1)); - } - - for (char c = 'A'; c <= 'Z'; c++) { - valid_keys.push_back(string(&c, 1)); - } - - for (int i = 1; i <= 12; i++) { - valid_keys.push_back('F' + int_to_string(i)); - } - - valid_keys.push_back("`"); - - for (int shifted = 0; shifted < 2; shifted++) { - for (int alt = 0; alt < 2; alt++) { - for (int ctrl = 0; ctrl < 2; ctrl++) { - for (auto it = valid_keys.begin(); it != valid_keys.end(); it++) { - string sym; - if (ctrl) sym += "Ctrl-"; - if (alt) sym += "Alt-"; - if (shifted) sym += "Shift-"; - sym += *it; - - auto list = Core::getInstance().ListKeyBindings(sym); - for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++) { - string::size_type colon_pos = invoke_cmd->find(":"); - // colons at location 0 are for commands like ":lua" - if (colon_pos == string::npos || colon_pos == 0) { - add_binding_if_valid(out, sym, *invoke_cmd, screen, filtermenu); - } - else { - vector tokens; - split_string(&tokens, *invoke_cmd, ":"); - string focus = tokens[0].substr(1); - if(Gui::matchFocusString(focus)) { - auto cmdline = trim(tokens[1]); - add_binding_if_valid(out, sym, cmdline, screen, filtermenu); - } - } - } - } - } + auto active_binds = Core::getInstance().getHotkeyManager()->listActiveKeybinds(); + for (const auto& bind : active_binds) { + string sym; + if (bind.spec.modifiers & DFH_MOD_CTRL) sym += "Ctrl-"; + if (bind.spec.modifiers & DFH_MOD_ALT) sym += "Alt-"; + if (bind.spec.modifiers & DFH_MOD_SHIFT) sym += "Shift-"; + + std::string key_name; + if (bind.spec.sym < 0) { + key_name = "MOUSE" + std::to_string(-bind.spec.sym); + } else { + key_name = DFSDL::DFSDL_GetKeyName(bind.spec.sym); } + if (key_name.empty()) continue; + sym += key_name; + + add_binding_if_valid(out, sym, bind.cmdline, screen, filtermenu); } valid = true; From 1554b55fb8ba91ddd0717c2eec8d9b0d0bd92b89 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 19 Nov 2025 20:59:57 -0500 Subject: [PATCH 02/18] Reorder imports --- library/include/modules/Hotkey.h | 2 +- library/modules/Hotkey.cpp | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 581ebe43e0..d6dbf591d8 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -4,9 +4,9 @@ #include #include +#include #include #include -#include namespace DFHack { class DFHACK_EXPORT HotkeyManager { diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 2cf87129d4..329db01fb7 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -1,18 +1,19 @@ #include "modules/Hotkey.h" + #include "Core.h" -#include "CoreDefs.h" #include "ColorText.h" #include "MiscUtils.h" +#include "PluginManager.h" + #include "modules/DFSDL.h" #include "modules/Gui.h" -#include "PluginManager.h" + #include "df/global_objects.h" #include "df/viewscreen.h" #include "df/interfacest.h" #include - -#include "SDL_keycode.h" +#include using namespace DFHack; From ec2b30972541130ddecc664dc5108939b9982b81 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 19 Nov 2025 21:12:03 -0500 Subject: [PATCH 03/18] Move keybinding command handling into Hotkey module --- library/Core.cpp | 48 +------------------------------- library/include/modules/Hotkey.h | 3 ++ library/modules/Hotkey.cpp | 43 ++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 47 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 6cc60aaf85..93b5f93da6 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -957,53 +957,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "keybinding") { - if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) - { - std::string keystr = parts[1]; - if (parts[0] == "set") - hotkey_mgr->clearKeybind(keystr); - // for (int i = parts.size()-1; i >= 2; i--) - for (const auto& part : parts | std::views::drop(2) | std::views::reverse) - { - if (!hotkey_mgr->addKeybind(keystr, part)) { - con.printerr("Invalid key spec: %s\n", keystr.c_str()); - break; - } - } - } - else if (parts.size() >= 2 && parts[0] == "clear") - { - // for (size_t i = 1; i < parts.size(); i++) - for (const auto& part : parts | std::views::drop(1)) - { - if (!hotkey_mgr->clearKeybind(part)) { - con.printerr("Invalid key spec: %s\n", part.c_str()); - break; - } - } - } - else if (parts.size() == 2 && parts[0] == "list") - { - std::vector list = hotkey_mgr->listKeybinds(parts[1]); - if (list.empty()) - con << "No bindings." << std::endl; - for (const auto& kb : list) - con << " " << kb << std::endl; - } - else - { - con << "Usage:" << std::endl - << " keybinding list " << std::endl - << " keybinding clear [@context]..." << std::endl - << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl - << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl - << "Later adds, and earlier items within one command have priority." << std::endl - << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl - << "Context may be used to limit the scope of the binding, by" << std::endl - << "requiring the current context to have a certain prefix." << std::endl - << "Current UI context is: " << std::endl - << join_strings("\n", Gui::getCurFocus(true)) << std::endl; - } + this->hotkey_mgr->handleKeybindingCommand(con, parts); } else if (first == "alias") { diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index d6dbf591d8..6c4eefaf25 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -1,6 +1,7 @@ #pragma once #include "Export.h" +#include "ColorText.h" #include #include @@ -10,6 +11,7 @@ namespace DFHack { class DFHACK_EXPORT HotkeyManager { + friend class Core; public: HotkeyManager(); ~HotkeyManager(); @@ -53,5 +55,6 @@ namespace DFHack { std::map> bindings; void hotkey_thread_fn(); + void handleKeybindingCommand(color_ostream& out, const std::vector& parts); }; } diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 329db01fb7..800bafa674 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -284,6 +284,49 @@ void HotkeyManager::setHotkeyCommand(std::string cmd) { cond.notify_all(); } + +void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vector& parts) { + if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { + std::string keystr = parts[1]; + if (parts[0] == "set") + clearKeybind(keystr); + for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { + if (!addKeybind(keystr, part)) { + con.printerr("Invalid key spec: %s\n", keystr.c_str()); + break; + } + } + } + else if (parts.size() >= 2 && parts[0] == "clear") { + for (const auto& part : parts | std::views::drop(1)) { + if (!clearKeybind(part)) { + con.printerr("Invalid key spec: %s\n", part.c_str()); + break; + } + } + } + else if (parts.size() == 2 && parts[0] == "list") { + std::vector list = listKeybinds(parts[1]); + if (list.empty()) + con << "No bindings." << std::endl; + for (const auto& kb : list) + con << " " << kb << std::endl; + } + else { + con << "Usage:" << std::endl + << " keybinding list " << std::endl + << " keybinding clear [@context]..." << std::endl + << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl + << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl + << "Later adds, and earlier items within one command have priority." << std::endl + << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl + << "Context may be used to limit the scope of the binding, by" << std::endl + << "requiring the current context to have a certain prefix." << std::endl + << "Current UI context is: " << std::endl + << join_strings("\n", Gui::getCurFocus(true)) << std::endl; + } +} + HotkeyManager::HotkeyManager() { this->hotkey_thread = std::thread(&HotkeyManager::hotkey_thread_fn, this); } From 3c2dc3cfa04d10e110d26999a7ff9a53391f9989 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 11:39:15 -0500 Subject: [PATCH 04/18] Properly populate the command field on keybinds --- library/modules/Hotkey.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 800bafa674..52657ac4f2 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -116,6 +116,8 @@ bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { KeyBinding binding; binding.spec = spec; binding.cmdline = cmd; + size_t space_idx = cmd.find(' '); + binding.command = space_idx == std::string::npos ? cmd : cmd.substr(0, space_idx); std::lock_guard l(lock); auto& bindings = this->bindings[binding.spec.sym]; From b5d8899a8bb72d50b2fb0e1480e25f4a294e16db Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 13:32:12 -0500 Subject: [PATCH 05/18] Lua api for hotkeys and library support for future keybinding gui --- library/LuaApi.cpp | 73 +++++++++++++++++++++++++++ library/include/modules/Hotkey.h | 54 ++++++++++++-------- library/modules/Hotkey.cpp | 84 ++++++++++++++++++++++++++++---- plugins/hotkeys.cpp | 15 +----- 4 files changed, 182 insertions(+), 44 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 1989b9a78b..6ae5c071e8 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -47,6 +47,7 @@ distribution. #include "modules/EventManager.h" #include "modules/Filesystem.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/Items.h" #include "modules/Job.h" #include "modules/Kitchen.h" @@ -1866,6 +1867,77 @@ static const luaL_Reg dfhack_gui_funcs[] = { { NULL, NULL } }; +/***** Hotkey module *****/ +static bool hotkey_addKeybind(const std::string spec, const std::string cmd) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (!hotkey_mgr) return false; + return hotkey_mgr->addKeybind(spec, cmd); +} + +static bool hotkey_clearKeybind(const std::string spec, bool any_focus, std::string cmd) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (!hotkey_mgr) return false; + return hotkey_mgr->clearKeybind(spec, any_focus, cmd); +} + +static void hotkey_requestKeybindingInput() { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (hotkey_mgr) hotkey_mgr->requestKeybindInput(); +} + +static std::string hotkey_readKeybindInput() { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (!hotkey_mgr) return ""; + return hotkey_mgr->readKeybindInput(); +} + +void hotkey_pushBindArray(lua_State *L, const std::vector& binds) { + lua_createtable(L, binds.size(), 0); + int i = 1; + for (const auto& bind : binds) { + lua_createtable(L, 0, 2); + + lua_pushstring(L, "spec"); + lua_pushstring(L, Hotkey::keyspec_to_string(bind.spec, true).c_str()); + lua_settable(L, -3); + + lua_pushstring(L, "command"); + lua_pushstring(L, bind.cmdline.c_str()); + lua_settable(L, -3); + lua_rawseti(L, -2, i++); + } +} + +static int hotkey_listActiveKeybinds(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + auto binds = hotkey_mgr->listActiveKeybinds(); + + hotkey_pushBindArray(L, binds); + return 1; +} + +static int hotkey_listAllKeybinds(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + auto binds = hotkey_mgr->listAllKeybinds(); + + hotkey_pushBindArray(L, binds); + return 1; +} + +static const luaL_Reg dfhack_hotkey_funcs[] = { + { "listActiveKeybinds", hotkey_listActiveKeybinds }, + { "listAllKeybinds", hotkey_listAllKeybinds }, + { NULL, NULL } +}; + +static const LuaWrapper::FunctionReg dfhack_hotkey_module[] = { + WRAPN(addKeybind, hotkey_addKeybind), + WRAPN(clearKeybind, hotkey_clearKeybind), + WRAPN(requestKeybindInput, hotkey_requestKeybindingInput), + WRAPN(readKeybindInput, hotkey_readKeybindInput), + { NULL, NULL } +}; + /***** Job module *****/ static bool jobEqual(const df::job *job1, const df::job *job2) @@ -4304,6 +4376,7 @@ void OpenDFHackApi(lua_State *state) luaL_setfuncs(state, dfhack_funcs, 0); OpenModule(state, "translation", dfhack_translation_module); OpenModule(state, "gui", dfhack_gui_module, dfhack_gui_funcs); + OpenModule(state, "hotkey", dfhack_hotkey_module, dfhack_hotkey_funcs); OpenModule(state, "job", dfhack_job_module, dfhack_job_funcs); OpenModule(state, "textures", dfhack_textures_funcs); OpenModule(state, "units", dfhack_units_module, dfhack_units_funcs); diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 6c4eefaf25..7eed1200be 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -10,49 +10,63 @@ #include namespace DFHack { + namespace Hotkey { + struct KeySpec { + int modifiers = 0; + // Negative numbers denote mouse buttons + int sym = 0; + std::vector focus; + }; + + struct KeyBinding { + KeySpec spec; + std::string command; + std::string cmdline; + }; + + DFHACK_EXPORT std::string keyspec_to_string(const KeySpec& spec, bool include_focus=false); + } class DFHACK_EXPORT HotkeyManager { friend class Core; public: HotkeyManager(); ~HotkeyManager(); - struct KeySpec { - int modifiers = 0; - // Negative numbers denote mouse buttons - int sym = 0; - std::vector focus; - }; - - struct KeyBinding { - HotkeyManager::KeySpec spec; - std::string command; - std::string cmdline; - }; bool addKeybind(std::string keyspec, std::string cmd); - bool addKeybind(KeySpec spec, std::string cmd); - bool clearKeybind(std::string keyspec, bool any_focus=false); - bool clearKeybind(const KeySpec& spec, bool any_focus=false); + bool addKeybind(Hotkey::KeySpec spec, std::string cmd); + // Clear a keybind with the given keyspec, optionally for any focus, or with a specific command + bool clearKeybind(std::string keyspec, bool any_focus=false, std::string cmdline=""); + bool clearKeybind(const Hotkey::KeySpec& spec, bool any_focus=false, std::string cmdline=""); std::vector listKeybinds(std::string keyspec); - std::vector listKeybinds(const KeySpec& spec); + std::vector listKeybinds(const Hotkey::KeySpec& spec); - std::vector listActiveKeybinds(); + std::vector listActiveKeybinds(); + std::vector listAllKeybinds(); bool handleKeybind(int sym, int modifiers); - void setHotkeyCommand(std::string cmd); - std::optional parseKeySpec(std::string spec); + // Used to request the next keybind input is saved. + // This is to allow for graphical keybinding menus. + void requestKeybindInput(); + // Returns the latest requested keybind input + std::string readKeybindInput(); + + std::optional parseKeySpec(std::string spec); private: std::thread hotkey_thread; std::mutex lock {}; std::condition_variable cond {}; + bool keybind_save_requested = false; + std::string requested_keybind; + int hotkey_sig = 0; std::string queued_command = ""; - std::map> bindings; + std::map> bindings; void hotkey_thread_fn(); void handleKeybindingCommand(color_ostream& out, const std::vector& parts); diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 52657ac4f2..7b57e032bd 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -16,6 +16,8 @@ #include using namespace DFHack; +using Hotkey::KeySpec; +using Hotkey::KeyBinding; enum HotkeySignal { None = 0, @@ -23,19 +25,49 @@ enum HotkeySignal { Shutdown, }; -bool operator==(const HotkeyManager::KeySpec& a, const HotkeyManager::KeySpec& b) { +bool operator==(const KeySpec& a, const KeySpec& b) { return a.modifiers == b.modifiers && a.sym == b.sym && a.focus.size() == b.focus.size() && std::equal(a.focus.begin(), a.focus.end(), b.focus.begin()); } // Equality operator for key bindings -bool operator==(const HotkeyManager::KeyBinding& a, const HotkeyManager::KeyBinding& b) { +bool operator==(const KeyBinding& a, const KeyBinding& b) { return a.spec == b.spec && a.command == b.command && a.cmdline == b.cmdline; } +std::string Hotkey::keyspec_to_string(const KeySpec &spec, bool include_focus) { + std::string sym; + if (spec.modifiers & DFH_MOD_CTRL) sym += "Ctrl-"; + if (spec.modifiers & DFH_MOD_ALT) sym += "Alt-"; + if (spec.modifiers & DFH_MOD_SHIFT) sym += "Shift-"; + + std::string key_name; + if (spec.sym < 0) { + key_name = "MOUSE" + std::to_string(-spec.sym); + } else { + key_name = DFSDL::DFSDL_GetKeyName(spec.sym); + } + sym += key_name; + + if (include_focus && !spec.focus.empty()) { + sym += "@"; + bool first = true; + for (const auto& focus : spec.focus) { + if (first) { + first = false; + sym += focus; + } else { + sym += "|" + focus; + } + } + } + + return sym; +} + // Hotkeys actions are executed from an external thread to avoid deadlocks // that may occur if running commands from the render or simulation threads. void HotkeyManager::hotkey_thread_fn() { @@ -64,7 +96,7 @@ void HotkeyManager::hotkey_thread_fn() { } } -std::optional HotkeyManager::parseKeySpec(std::string spec) { +std::optional HotkeyManager::parseKeySpec(std::string spec) { KeySpec out; // Determine focus string, if present @@ -138,16 +170,16 @@ bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { return this->addKeybind(spec_opt.value(), cmd); } -bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus) { +bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::string cmdline) { std::lock_guard l(lock); if (!bindings.contains(spec.sym)) return false; auto& binds = bindings[spec.sym]; - auto new_end = std::remove_if(binds.begin(), binds.end(), [any_focus, spec](const auto& v) { - return any_focus + auto new_end = std::remove_if(binds.begin(), binds.end(), [any_focus, spec, &cmdline](const auto& v) { + return (any_focus ? v.spec.sym == spec.sym && v.spec.modifiers == spec.modifiers - : v.spec == spec; + : v.spec == spec) && (cmdline.empty() || v.cmdline == cmdline); }); if (new_end == binds.end()) return false; // No bindings removed @@ -156,11 +188,11 @@ bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus) { return true; } -bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus) { +bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus, std::string cmdline) { std::optional spec_opt = parseKeySpec(keyspec); if (!spec_opt.has_value()) return false; - return this->clearKeybind(spec_opt.value(), any_focus); + return this->clearKeybind(spec_opt.value(), any_focus, cmdline); } std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { @@ -200,7 +232,7 @@ std::vector HotkeyManager::listKeybinds(std::string keyspec) { return this->listKeybinds(spec_opt.value()); } -std::vector HotkeyManager::listActiveKeybinds() { +std::vector HotkeyManager::listActiveKeybinds() { std::vector out; for(const auto& [_, bind_set] : bindings) { @@ -223,6 +255,17 @@ std::vector HotkeyManager::listActiveKeybinds() { return out; } +std::vector HotkeyManager::listAllKeybinds() { + std::vector out; + + for (const auto& [_, bind_set] : bindings) { + for (const auto& bind : bind_set) { + out.emplace_back(bind); + } + } + return out; +} + bool HotkeyManager::handleKeybind(int sym, int modifiers) { // Ensure gamestate is ready if (!df::global::gview || !df::global::plotinfo) @@ -237,6 +280,17 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { sym = SDLK_RETURN; std::unique_lock l(lock); + + // If reading input for a keybinding screen, save the input and exit early + if (keybind_save_requested) { + KeySpec spec; + spec.sym = sym; + spec.modifiers = modifiers; + requested_keybind = Hotkey::keyspec_to_string(spec); + keybind_save_requested = false; + return true; + } + if (!bindings.contains(sym)) return false; auto& binds = bindings[sym]; @@ -286,6 +340,16 @@ void HotkeyManager::setHotkeyCommand(std::string cmd) { cond.notify_all(); } +void HotkeyManager::requestKeybindInput() { + std::lock_guard l(lock); + keybind_save_requested = true; + requested_keybind = ""; +} + +std::string HotkeyManager::readKeybindInput() { + std::lock_guard l(lock); + return requested_keybind; +} void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vector& parts) { if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 197b0d685d..1e87d7296d 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -94,20 +94,7 @@ static void find_active_keybindings(color_ostream &out, df::viewscreen *screen, auto active_binds = Core::getInstance().getHotkeyManager()->listActiveKeybinds(); for (const auto& bind : active_binds) { - string sym; - if (bind.spec.modifiers & DFH_MOD_CTRL) sym += "Ctrl-"; - if (bind.spec.modifiers & DFH_MOD_ALT) sym += "Alt-"; - if (bind.spec.modifiers & DFH_MOD_SHIFT) sym += "Shift-"; - - std::string key_name; - if (bind.spec.sym < 0) { - key_name = "MOUSE" + std::to_string(-bind.spec.sym); - } else { - key_name = DFSDL::DFSDL_GetKeyName(bind.spec.sym); - } - if (key_name.empty()) continue; - sym += key_name; - + string sym = Hotkey::keyspec_to_string(bind.spec); add_binding_if_valid(out, sym, bind.cmdline, screen, filtermenu); } From 74737b415d21b2781e3d77b54ebf7623fe408f8b Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 16:18:59 -0500 Subject: [PATCH 06/18] Prefer using to typedef --- library/include/modules/DFSDL.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index 161cc35171..9bba7eb4ab 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -11,7 +11,7 @@ struct SDL_Rect; struct SDL_PixelFormat; struct SDL_Window; union SDL_Event; -typedef int32_t SDL_Keycode; +using SDL_Keycode = int32_t; namespace DFHack { From 1fcee14fe8c18d542814edbcb5d967a40247adc8 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 16:44:05 -0500 Subject: [PATCH 07/18] Improve keybinding command error messages for invalid keyspecs --- library/include/modules/Hotkey.h | 2 +- library/modules/Hotkey.cpp | 92 +++++++++++++++++++------------- 2 files changed, 57 insertions(+), 37 deletions(-) diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 7eed1200be..75f1af1604 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -24,6 +24,7 @@ namespace DFHack { std::string cmdline; }; + DFHACK_EXPORT std::optional parseKeySpec(std::string spec, std::string* err = nullptr); DFHACK_EXPORT std::string keyspec_to_string(const KeySpec& spec, bool include_focus=false); } class DFHACK_EXPORT HotkeyManager { @@ -54,7 +55,6 @@ namespace DFHack { // Returns the latest requested keybind input std::string readKeybindInput(); - std::optional parseKeySpec(std::string spec); private: std::thread hotkey_thread; std::mutex lock {}; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 7b57e032bd..ab64f2e141 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -68,35 +68,8 @@ std::string Hotkey::keyspec_to_string(const KeySpec &spec, bool include_focus) { return sym; } -// Hotkeys actions are executed from an external thread to avoid deadlocks -// that may occur if running commands from the render or simulation threads. -void HotkeyManager::hotkey_thread_fn() { - auto& core = DFHack::Core::getInstance(); - - std::unique_lock l(lock); - while (true) { - cond.wait(l, [this]() { return this->hotkey_sig != HotkeySignal::None; }); - if (hotkey_sig == HotkeySignal::Shutdown) - return; - if (hotkey_sig != HotkeySignal::CmdReady) - continue; - // Copy and reset important data, then release the lock - this->hotkey_sig = HotkeySignal::None; - std::string cmd = this->queued_command; - this->queued_command.clear(); - l.unlock(); - - // Attempt execution of command - DFHack::color_ostream_proxy out(core.getConsole()); - auto res = core.runCommand(out, cmd); - if (res == DFHack::CR_NOT_IMPLEMENTED) - out.printerr("Invalid hotkey command: '%s'\n", cmd.c_str()); - l.lock(); - } -} - -std::optional HotkeyManager::parseKeySpec(std::string spec) { +std::optional Hotkey::parseKeySpec(std::string spec, std::string* err) { KeySpec out; // Determine focus string, if present @@ -136,10 +109,42 @@ std::optional HotkeyManager::parseKeySpec(std::string spec) { } } + if (err) + *err = "Unknown key '" + spec + "'"; + // Invalid key binding return std::nullopt; } +// Hotkeys actions are executed from an external thread to avoid deadlocks +// that may occur if running commands from the render or simulation threads. +void HotkeyManager::hotkey_thread_fn() { + auto& core = DFHack::Core::getInstance(); + + std::unique_lock l(lock); + while (true) { + cond.wait(l, [this]() { return this->hotkey_sig != HotkeySignal::None; }); + if (hotkey_sig == HotkeySignal::Shutdown) + return; + if (hotkey_sig != HotkeySignal::CmdReady) + continue; + + // Copy and reset important data, then release the lock + this->hotkey_sig = HotkeySignal::None; + std::string cmd = this->queued_command; + this->queued_command.clear(); + l.unlock(); + + // Attempt execution of command + DFHack::color_ostream_proxy out(core.getConsole()); + auto res = core.runCommand(out, cmd); + if (res == DFHack::CR_NOT_IMPLEMENTED) + out.printerr("Invalid hotkey command: '%s'\n", cmd.c_str()); + l.lock(); + } +} + + bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { // No point in a hotkey with no action if (cmd.empty()) @@ -164,7 +169,7 @@ bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { } bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { - std::optional spec_opt = parseKeySpec(keyspec); + std::optional spec_opt = Hotkey::parseKeySpec(keyspec); if (!spec_opt.has_value()) return false; return this->addKeybind(spec_opt.value(), cmd); @@ -189,7 +194,7 @@ bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::strin } bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus, std::string cmdline) { - std::optional spec_opt = parseKeySpec(keyspec); + std::optional spec_opt = Hotkey::parseKeySpec(keyspec); if (!spec_opt.has_value()) return false; return this->clearKeybind(spec_opt.value(), any_focus, cmdline); @@ -226,7 +231,7 @@ std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { } std::vector HotkeyManager::listKeybinds(std::string keyspec) { - std::optional spec_opt = parseKeySpec(keyspec); + std::optional spec_opt = Hotkey::parseKeySpec(keyspec); if (!spec_opt.has_value()) return {}; return this->listKeybinds(spec_opt.value()); @@ -352,27 +357,42 @@ std::string HotkeyManager::readKeybindInput() { } void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vector& parts) { + std::string parse_error; if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { std::string keystr = parts[1]; if (parts[0] == "set") clearKeybind(keystr); for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { - if (!addKeybind(keystr, part)) { - con.printerr("Invalid key spec: %s\n", keystr.c_str()); + auto spec = Hotkey::parseKeySpec(keystr, &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + break; + } + if (!addKeybind(spec.value(), part)) { + con.printerr("Invalid command: '%s'\n", part.c_str()); break; } } } else if (parts.size() >= 2 && parts[0] == "clear") { for (const auto& part : parts | std::views::drop(1)) { - if (!clearKeybind(part)) { - con.printerr("Invalid key spec: %s\n", part.c_str()); + auto spec = Hotkey::parseKeySpec(part, &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + } + if (!clearKeybind(spec.value())) { + con.printerr("No matching keybinds to remove\n"); break; } } } else if (parts.size() == 2 && parts[0] == "list") { - std::vector list = listKeybinds(parts[1]); + auto spec = Hotkey::parseKeySpec(parts[1], &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + return; + } + std::vector list = listKeybinds(spec.value()); if (list.empty()) con << "No bindings." << std::endl; for (const auto& kb : list) From e49d2204cc393b03b7d0f71144f54dbddf89013f Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 17:20:53 -0500 Subject: [PATCH 08/18] Make keyspecs case-insensitive (excluding focus strings) --- library/modules/Hotkey.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index ab64f2e141..aa38773865 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -79,6 +79,9 @@ std::optional Hotkey::parseKeySpec(std::string spec, std::string* err) spec.erase(focus_idx); } + // Treat remaining keyspec as lowercase for case-insensitivity. + std::transform(spec.begin(), spec.end(), spec.begin(), tolower); + // Determine modifier flags auto match_modifier = [&out, &spec](std::string_view prefix, int mod) { bool found = spec.starts_with(prefix); @@ -88,25 +91,28 @@ std::optional Hotkey::parseKeySpec(std::string spec, std::string* err) } return found; }; - while (match_modifier("Shift-", DFH_MOD_SHIFT) || match_modifier("Ctrl-", DFH_MOD_CTRL) || match_modifier("Alt-", DFH_MOD_ALT)) {} + while (match_modifier("shift-", DFH_MOD_SHIFT) || match_modifier("ctrl-", DFH_MOD_CTRL) || match_modifier("alt-", DFH_MOD_ALT)) {} out.sym = DFSDL::DFSDL_GetKeyFromName(spec.c_str()); if (out.sym != SDLK_UNKNOWN) return out; // Attempt to parse as a mouse binding - if (spec.starts_with("MOUSE")) { + if (spec.starts_with("mouse")) { spec.erase(0, 5); // Read button number, ensuring between 1 and 15 inclusive try { int mbutton = std::stoi(spec); - if (mbutton >= 1 && mbutton <= 15) { + if (mbutton >= 4 && mbutton <= 15) { out.sym = -mbutton; return out; } } catch (...) { // If integer parsing fails, it isn't valid } + if (err) + *err = "Invalid mouse button '" + spec + "', only 4-15 are valid"; + return std::nullopt; } if (err) From 43ecdad98912e6a20882603033bd9215e2a6c44c Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 20:48:52 -0500 Subject: [PATCH 09/18] Address code review concerns --- library/Core.cpp | 3 ++- library/LuaApi.cpp | 9 +++++---- library/include/modules/Hotkey.h | 4 ++-- library/modules/Hotkey.cpp | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 93b5f93da6..12ee89ed15 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2464,8 +2464,9 @@ bool Core::doSdlInputEvent(SDL_Event* ev) DEBUG(keybinding).print("mouse button down: button=%d\n", but.button); // don't mess with the first three buttons, which are critical elements of DF's control scheme if (but.button > 3) { + // We represent mouse buttons as a negative number, permitting buttons 4-15 SDL_Keycode sym = -but.button; - if (sym >= -15 && sym <= -1 && hotkey_mgr->handleKeybind(sym, modstate)) + if (sym >= -15 && sym <= -4 && hotkey_mgr->handleKeybind(sym, modstate)) return suppress_duplicate_keyboard_events; } } else if (ev->type == SDL_TEXTINPUT) { diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 6ae5c071e8..de6ffd7864 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1897,12 +1897,13 @@ void hotkey_pushBindArray(lua_State *L, const std::vector& b for (const auto& bind : binds) { lua_createtable(L, 0, 2); - lua_pushstring(L, "spec"); - lua_pushstring(L, Hotkey::keyspec_to_string(bind.spec, true).c_str()); + lua_pushlstring(L, "spec", 4); + auto spec_str = Hotkey::keyspec_to_string(bind.spec, true); + lua_pushlstring(L, spec_str.data(), spec_str.size()); lua_settable(L, -3); - lua_pushstring(L, "command"); - lua_pushstring(L, bind.cmdline.c_str()); + lua_pushlstring(L, "command", 7); + lua_pushlstring(L, bind.cmdline.data(), bind.cmdline.size()); lua_settable(L, -3); lua_rawseti(L, -2, i++); } diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 75f1af1604..8f3690b846 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -37,8 +37,8 @@ namespace DFHack { bool addKeybind(std::string keyspec, std::string cmd); bool addKeybind(Hotkey::KeySpec spec, std::string cmd); // Clear a keybind with the given keyspec, optionally for any focus, or with a specific command - bool clearKeybind(std::string keyspec, bool any_focus=false, std::string cmdline=""); - bool clearKeybind(const Hotkey::KeySpec& spec, bool any_focus=false, std::string cmdline=""); + bool clearKeybind(std::string keyspec, bool any_focus=false, std::string_view cmdline=""); + bool clearKeybind(const Hotkey::KeySpec& spec, bool any_focus=false, std::string_view cmdline=""); std::vector listKeybinds(std::string keyspec); std::vector listKeybinds(const Hotkey::KeySpec& spec); diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index aa38773865..e3b301e8f4 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -181,7 +181,7 @@ bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { return this->addKeybind(spec_opt.value(), cmd); } -bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::string cmdline) { +bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::string_view cmdline) { std::lock_guard l(lock); if (!bindings.contains(spec.sym)) return false; @@ -199,7 +199,7 @@ bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::strin return true; } -bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus, std::string cmdline) { +bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus, std::string_view cmdline) { std::optional spec_opt = Hotkey::parseKeySpec(keyspec); if (!spec_opt.has_value()) return false; From 2ee9d7e08afad751808de4af956fe1917237aa0c Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 20 Nov 2025 21:31:32 -0500 Subject: [PATCH 10/18] Initial documentation pass, and improvements to lua api ergonomics --- docs/builtins/keybinding.rst | 8 ++++---- docs/dev/Lua API.rst | 40 ++++++++++++++++++++++++++++++++++++ library/LuaApi.cpp | 14 +++++++++---- 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/docs/builtins/keybinding.rst b/docs/builtins/keybinding.rst index 3e6970b6fc..ea27b6bcf3 100644 --- a/docs/builtins/keybinding.rst +++ b/docs/builtins/keybinding.rst @@ -9,9 +9,9 @@ Like any other command, it can be used at any time from the console, but bindings are not remembered between runs of the game unless re-created in :file:`dfhack-config/init/dfhack.init`. -Hotkeys can be any combinations of Ctrl/Alt/Shift with A-Z, 0-9, F1-F12, or ` -(the key below the :kbd:`Esc` key on most keyboards). You can also represent -mouse buttons beyond the first three with ``MOUSE4`` through ``MOUSE15``. +Hotkeys can be any combinations of Ctrl/Alt/Shift with any key recognized by SDL. +You can also represent mouse buttons beyond the first three with ``MOUSE4`` +through ``MOUSE15``. Usage ----- @@ -27,7 +27,7 @@ Usage ``keybinding set "" ["" ...]`` Clear, and then add bindings for the specified key. -The ```` parameter above has the following **case-sensitive** syntax:: +The ```` parameter above has the following case-insensitive syntax:: [Ctrl-][Alt-][Shift-]KEY[@context[|context...]] diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index a4a039f409..582e0b7e53 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1430,6 +1430,46 @@ Job module Returns the job's description, as seen in the Units and Jobs screens. +Hotkey module +------------- + +* ``dfhack.hotkey.addKeybind(keyspec, command)`` + + Creates a new keybind with the provided keyspec (see the `keybinding` documentation + for details on format). + Returns false on failure to create keybind. + +* ``dfhack.hotkey.clearKeybind(keyspec, any_focus, command)`` + + Removes keybinds matching the provided keyspec. + If any_focus is true, the focus portion of the keyspec is ignored. + If command is not an empty string, the command is matched against as well. + Returns false if no keybinds were removed. + +* ``dfhack.hotkey.listActiveKeybinds()`` + + Returns a list of keybinds active within the current context. + The items are tables with the following attributes: + :spec: The keyspec for the hotkey + :command: The command the hotkey runs when pressed + +* ``dfhack.hotkey.listAllKeybinds()`` + + Returns a list of all keybinds currently registered. + The items are tables with the following attributes: + :spec: The keyspec for the hotkey + :command: The command the hotkey runs when pressed + +* ``dfhack.hotkey.requestKeybindInput()`` + + Requests that the next hotkey-compatible input is saved and not processed, + retrievable with ``dfhack.hotkey.readKeybindInput()``. + +* ``dfhack.hotkey.readKeybindInput()`` + + Reads the latest saved keybind input that was requested. + Returns a keyspec string for the input, or nil if no input is saved. + Units module ------------ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index de6ffd7864..319e1f3375 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1885,10 +1885,16 @@ static void hotkey_requestKeybindingInput() { if (hotkey_mgr) hotkey_mgr->requestKeybindInput(); } -static std::string hotkey_readKeybindInput() { +static int hotkey_readKeybindInput(lua_State *L) { auto hotkey_mgr = Core::getInstance().getHotkeyManager(); - if (!hotkey_mgr) return ""; - return hotkey_mgr->readKeybindInput(); + auto input = hotkey_mgr->readKeybindInput(); + + if (input.empty()) { + lua_pushnil(L); + } else { + lua_pushlstring(L, input.data(), input.size()); + } + return 1; } void hotkey_pushBindArray(lua_State *L, const std::vector& binds) { @@ -1928,6 +1934,7 @@ static int hotkey_listAllKeybinds(lua_State *L) { static const luaL_Reg dfhack_hotkey_funcs[] = { { "listActiveKeybinds", hotkey_listActiveKeybinds }, { "listAllKeybinds", hotkey_listAllKeybinds }, + { "readKeybindInput", hotkey_readKeybindInput }, { NULL, NULL } }; @@ -1935,7 +1942,6 @@ static const LuaWrapper::FunctionReg dfhack_hotkey_module[] = { WRAPN(addKeybind, hotkey_addKeybind), WRAPN(clearKeybind, hotkey_clearKeybind), WRAPN(requestKeybindInput, hotkey_requestKeybindingInput), - WRAPN(readKeybindInput, hotkey_readKeybindInput), { NULL, NULL } }; From 7ce44a55da0ff9a5dc6c17b6a97842aae31f75fc Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 22 Nov 2025 19:48:17 -0500 Subject: [PATCH 11/18] Improve api ergonomics and add missing locks --- docs/dev/Lua API.rst | 17 +++++----- library/LuaApi.cpp | 57 ++++++++++++++++++++++++-------- library/include/modules/Hotkey.h | 12 +++---- library/modules/Hotkey.cpp | 30 +++++++++-------- plugins/hotkeys.cpp | 2 +- 5 files changed, 77 insertions(+), 41 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 582e0b7e53..c7c35c5441 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1439,11 +1439,11 @@ Hotkey module for details on format). Returns false on failure to create keybind. -* ``dfhack.hotkey.clearKeybind(keyspec, any_focus, command)`` +* ``dfhack.hotkey.removeKeybind(keyspec, [match_focus=true, command])`` Removes keybinds matching the provided keyspec. - If any_focus is true, the focus portion of the keyspec is ignored. - If command is not an empty string, the command is matched against as well. + If match_focus is set, the focus portion of the keyspec is matched against. + If command is provided and not an empty string, the command is matched against. Returns false if no keybinds were removed. * ``dfhack.hotkey.listActiveKeybinds()`` @@ -1460,15 +1460,16 @@ Hotkey module :spec: The keyspec for the hotkey :command: The command the hotkey runs when pressed -* ``dfhack.hotkey.requestKeybindInput()`` +* ``dfhack.hotkey.requestKeybindingInput([cancel=false])`` - Requests that the next hotkey-compatible input is saved and not processed, - retrievable with ``dfhack.hotkey.readKeybindInput()``. + Enqueues or cancels a request that the next hotkey-compatible input is saved + and not processed, retrievable with ``dfhack.hotkey.getKeybindingInput()``. + If cancel is true, any current request is cancelled. -* ``dfhack.hotkey.readKeybindInput()`` +* ``dfhack.hotkey.getKeybindingInput()`` Reads the latest saved keybind input that was requested. - Returns a keyspec string for the input, or nil if no input is saved. + Returns a keyspec string for the input, or nil if no input has been saved. Units module ------------ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 319e1f3375..e7b0d1197b 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1874,20 +1874,19 @@ static bool hotkey_addKeybind(const std::string spec, const std::string cmd) { return hotkey_mgr->addKeybind(spec, cmd); } -static bool hotkey_clearKeybind(const std::string spec, bool any_focus, std::string cmd) { +static int hotkey_requestKeybindingInput(lua_State *L) { auto hotkey_mgr = Core::getInstance().getHotkeyManager(); - if (!hotkey_mgr) return false; - return hotkey_mgr->clearKeybind(spec, any_focus, cmd); -} - -static void hotkey_requestKeybindingInput() { - auto hotkey_mgr = Core::getInstance().getHotkeyManager(); - if (hotkey_mgr) hotkey_mgr->requestKeybindInput(); + if (!hotkey_mgr) return 0; + bool cancel = false; + if (lua_gettop(L) == 1) + cancel = lua_toboolean(L, -1); + hotkey_mgr->requestKeybindingInput(cancel); + return 0; } -static int hotkey_readKeybindInput(lua_State *L) { +static int hotkey_getKeybindingInput(lua_State *L) { auto hotkey_mgr = Core::getInstance().getHotkeyManager(); - auto input = hotkey_mgr->readKeybindInput(); + auto input = hotkey_mgr->getKeybindingInput(); if (input.empty()) { lua_pushnil(L); @@ -1897,6 +1896,38 @@ static int hotkey_readKeybindInput(lua_State *L) { return 1; } +static int hotkey_removeKeybind(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (!hotkey_mgr) { + lua_pushboolean(L, false); + return 1; + } + + bool res = false; + switch (lua_gettop(L)) { + case 1: + luaL_checkstring(L, -1); + res = hotkey_mgr->removeKeybind(lua_tostring(L, -1)); + break; + case 2: + luaL_checkstring(L, -2); + res = hotkey_mgr->removeKeybind(lua_tostring(L, -2), lua_toboolean(L, -1)); + break; + case 3: + luaL_checkstring(L, -3); + luaL_checkstring(L, -1); + res = hotkey_mgr->removeKeybind( + lua_tostring(L, -3), + lua_toboolean(L, -2), + lua_tostring(L, -1) + ); + break; + } + + lua_pushboolean(L, res); + return 1; +} + void hotkey_pushBindArray(lua_State *L, const std::vector& binds) { lua_createtable(L, binds.size(), 0); int i = 1; @@ -1932,16 +1963,16 @@ static int hotkey_listAllKeybinds(lua_State *L) { } static const luaL_Reg dfhack_hotkey_funcs[] = { + { "removeKeybind", hotkey_removeKeybind }, { "listActiveKeybinds", hotkey_listActiveKeybinds }, { "listAllKeybinds", hotkey_listAllKeybinds }, - { "readKeybindInput", hotkey_readKeybindInput }, + { "requestKeybindingInput", hotkey_requestKeybindingInput }, + { "getKeybindingInput", hotkey_getKeybindingInput }, { NULL, NULL } }; static const LuaWrapper::FunctionReg dfhack_hotkey_module[] = { WRAPN(addKeybind, hotkey_addKeybind), - WRAPN(clearKeybind, hotkey_clearKeybind), - WRAPN(requestKeybindInput, hotkey_requestKeybindingInput), { NULL, NULL } }; diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 8f3690b846..6414ec9204 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -37,8 +37,8 @@ namespace DFHack { bool addKeybind(std::string keyspec, std::string cmd); bool addKeybind(Hotkey::KeySpec spec, std::string cmd); // Clear a keybind with the given keyspec, optionally for any focus, or with a specific command - bool clearKeybind(std::string keyspec, bool any_focus=false, std::string_view cmdline=""); - bool clearKeybind(const Hotkey::KeySpec& spec, bool any_focus=false, std::string_view cmdline=""); + bool removeKeybind(std::string keyspec, bool match_focus=true, std::string_view cmdline=""); + bool removeKeybind(const Hotkey::KeySpec& spec, bool match_focus=true, std::string_view cmdline=""); std::vector listKeybinds(std::string keyspec); std::vector listKeybinds(const Hotkey::KeySpec& spec); @@ -49,11 +49,11 @@ namespace DFHack { bool handleKeybind(int sym, int modifiers); void setHotkeyCommand(std::string cmd); - // Used to request the next keybind input is saved. + // Used to request the next hotkey-compatible input is saved. // This is to allow for graphical keybinding menus. - void requestKeybindInput(); + void requestKeybindingInput(bool cancel=false); // Returns the latest requested keybind input - std::string readKeybindInput(); + std::string getKeybindingInput(); private: std::thread hotkey_thread; @@ -64,7 +64,7 @@ namespace DFHack { std::string requested_keybind; int hotkey_sig = 0; - std::string queued_command = ""; + std::string queued_command; std::map> bindings; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index e3b301e8f4..761a237672 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -181,16 +181,17 @@ bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { return this->addKeybind(spec_opt.value(), cmd); } -bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::string_view cmdline) { +bool HotkeyManager::removeKeybind(const KeySpec& spec, bool match_focus, std::string_view cmdline) { std::lock_guard l(lock); if (!bindings.contains(spec.sym)) return false; auto& binds = bindings[spec.sym]; - auto new_end = std::remove_if(binds.begin(), binds.end(), [any_focus, spec, &cmdline](const auto& v) { - return (any_focus - ? v.spec.sym == spec.sym && v.spec.modifiers == spec.modifiers - : v.spec == spec) && (cmdline.empty() || v.cmdline == cmdline); + auto new_end = std::remove_if(binds.begin(), binds.end(), [match_focus, spec, &cmdline](const auto& v) { + return v.spec.sym == spec.sym + && v.spec.modifiers == spec.modifiers + && (!match_focus || v.spec.focus == spec.focus) + && (cmdline.empty() || v.cmdline == cmdline); }); if (new_end == binds.end()) return false; // No bindings removed @@ -199,11 +200,11 @@ bool HotkeyManager::clearKeybind(const KeySpec& spec, bool any_focus, std::strin return true; } -bool HotkeyManager::clearKeybind(std::string keyspec, bool any_focus, std::string_view cmdline) { +bool HotkeyManager::removeKeybind(std::string keyspec, bool match_focus, std::string_view cmdline) { std::optional spec_opt = Hotkey::parseKeySpec(keyspec); if (!spec_opt.has_value()) return false; - return this->clearKeybind(spec_opt.value(), any_focus, cmdline); + return this->removeKeybind(spec_opt.value(), match_focus, cmdline); } std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { @@ -237,6 +238,7 @@ std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { } std::vector HotkeyManager::listKeybinds(std::string keyspec) { + std::lock_guard l(lock); std::optional spec_opt = Hotkey::parseKeySpec(keyspec); if (!spec_opt.has_value()) return {}; @@ -244,6 +246,7 @@ std::vector HotkeyManager::listKeybinds(std::string keyspec) { } std::vector HotkeyManager::listActiveKeybinds() { + std::lock_guard l(lock); std::vector out; for(const auto& [_, bind_set] : bindings) { @@ -267,6 +270,7 @@ std::vector HotkeyManager::listActiveKeybinds() { } std::vector HotkeyManager::listAllKeybinds() { + std::lock_guard l(lock); std::vector out; for (const auto& [_, bind_set] : bindings) { @@ -351,13 +355,13 @@ void HotkeyManager::setHotkeyCommand(std::string cmd) { cond.notify_all(); } -void HotkeyManager::requestKeybindInput() { +void HotkeyManager::requestKeybindingInput(bool cancel) { std::lock_guard l(lock); - keybind_save_requested = true; - requested_keybind = ""; + keybind_save_requested = !cancel; + requested_keybind.clear(); } -std::string HotkeyManager::readKeybindInput() { +std::string HotkeyManager::getKeybindingInput() { std::lock_guard l(lock); return requested_keybind; } @@ -367,7 +371,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { std::string keystr = parts[1]; if (parts[0] == "set") - clearKeybind(keystr); + removeKeybind(keystr); for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { auto spec = Hotkey::parseKeySpec(keystr, &parse_error); if (!spec.has_value()) { @@ -386,7 +390,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto if (!spec.has_value()) { con.printerr("%s\n", parse_error.c_str()); } - if (!clearKeybind(spec.value())) { + if (!removeKeybind(spec.value())) { con.printerr("No matching keybinds to remove\n"); break; } diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 1e87d7296d..c0a5f4fccd 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -46,7 +46,7 @@ static int cleanupHotkeys(lua_State *) { std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) { string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str()); - Core::getInstance().getHotkeyManager()->clearKeybind(keyspec); + Core::getInstance().getHotkeyManager()->removeKeybind(keyspec); }); valid = false; sorted_keys.clear(); From 68e0d5b59e8ed6cee9c69c6f27f5c8c8eb7abc4e Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 22 Nov 2025 20:34:38 -0500 Subject: [PATCH 12/18] First pass at changelog --- docs/changelog.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 8551676b26..6cfa16e4bf 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -65,8 +65,15 @@ Template for new versions: ## Documentation ## API +- ``Hotkey``: New module for hotkey functionality ## Lua +- ``dfhack.hotkey.addKeybind``: Creates new keybindings +- ``dfhack.hotkey.removeKeybind``: Removes existing keybindings +- ``dfhack.hotkey.listActiveKeybinds``: Lists all keybinds for the current context +- ``dfhack.hotkey.listAllKeybinds``: Lists all keybinds for all contexts +- ``dfhack.hotkey.requestKeybindingInput``: Requests the next keybind-compatible input is saved +- ``dfhack.hotkey.getKeybindingInput``: Reads the input saved in response to a request. ## Removed From 9c87028411cad69ee1102513485d3563c573d7de Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sun, 23 Nov 2025 17:28:57 -0500 Subject: [PATCH 13/18] Allow "Super" as a hotkey modifier --- .gitignore | 2 +- docs/builtins/keybinding.rst | 4 ++-- docs/dev/Lua API.rst | 2 +- library/Core.cpp | 2 ++ library/LuaApi.cpp | 3 +++ library/include/Core.h | 1 + library/modules/Hotkey.cpp | 6 +++++- 7 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 60a0e1b6b2..6142448b7a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # linux backup files *~ - +.cache # Kdevelop project files *.kdev4 .kdev4 diff --git a/docs/builtins/keybinding.rst b/docs/builtins/keybinding.rst index ea27b6bcf3..f77a081650 100644 --- a/docs/builtins/keybinding.rst +++ b/docs/builtins/keybinding.rst @@ -9,7 +9,7 @@ Like any other command, it can be used at any time from the console, but bindings are not remembered between runs of the game unless re-created in :file:`dfhack-config/init/dfhack.init`. -Hotkeys can be any combinations of Ctrl/Alt/Shift with any key recognized by SDL. +Hotkeys can be any combinations of Ctrl/Alt/Super/Shift with any key recognized by SDL. You can also represent mouse buttons beyond the first three with ``MOUSE4`` through ``MOUSE15``. @@ -29,7 +29,7 @@ Usage The ```` parameter above has the following case-insensitive syntax:: - [Ctrl-][Alt-][Shift-]KEY[@context[|context...]] + [Ctrl-][Alt-][Super-][Shift-]KEY[@context[|context...]] where the ``KEY`` part can be any recognized key and :kbd:`[`:kbd:`]` denote optional parts. diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index c7c35c5441..27dcf62558 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3535,7 +3535,7 @@ and are only documented here for completeness: * ``dfhack.internal.getModifiers()`` Returns the state of the keyboard modifier keys in a table of string -> - boolean. The keys are ``ctrl``, ``shift``, and ``alt``. + boolean. The keys are ``ctrl``, ``shift``, ``super``, and ``alt``. * ``dfhack.internal.getSuppressDuplicateKeyboardEvents()`` * ``dfhack.internal.setSuppressDuplicateKeyboardEvents(suppress)`` diff --git a/library/Core.cpp b/library/Core.cpp index 12ee89ed15..e8ddf47270 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2439,6 +2439,8 @@ bool Core::doSdlInputEvent(SDL_Event* ev) modstate = (ev->type == SDL_KEYDOWN) ? modstate | DFH_MOD_CTRL : modstate & ~DFH_MOD_CTRL; else if (sym == SDLK_LALT || sym == SDLK_RALT) modstate = (ev->type == SDL_KEYDOWN) ? modstate | DFH_MOD_ALT : modstate & ~DFH_MOD_ALT; + else if (sym == SDLK_LGUI || sym == SDLK_RGUI) // Renamed to LMETA/RMETA in SDL3 + modstate = (ev->type == SDL_KEYDOWN) ? modstate | DFH_MOD_SUPER : modstate & ~DFH_MOD_SUPER; else if (ke.state == SDL_PRESSED && !hotkey_states[sym]) { // the check against hotkey_states[sym] ensures we only process keybindings once per keypress diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index e7b0d1197b..3021a70b1d 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -4073,6 +4073,9 @@ static int internal_getModifiers(lua_State *L) lua_pushstring(L, "alt"); lua_pushboolean(L, modstate & DFH_MOD_ALT); lua_settable(L, -3); + lua_pushstring(L, "super"); + lua_pushboolean(L, modstate & DFH_MOD_SUPER); + lua_settable(L, -3); return 1; } diff --git a/library/include/Core.h b/library/include/Core.h index 08c7ca3d2e..bf0378b05d 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -58,6 +58,7 @@ namespace DFHack constexpr auto DFH_MOD_SHIFT = 1; constexpr auto DFH_MOD_CTRL = 2; constexpr auto DFH_MOD_ALT = 4; + constexpr auto DFH_MOD_SUPER = 8; class Process; class Module; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 761a237672..11a462dec9 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -42,6 +42,7 @@ std::string Hotkey::keyspec_to_string(const KeySpec &spec, bool include_focus) { std::string sym; if (spec.modifiers & DFH_MOD_CTRL) sym += "Ctrl-"; if (spec.modifiers & DFH_MOD_ALT) sym += "Alt-"; + if (spec.modifiers & DFH_MOD_SUPER) sym += "Super-"; if (spec.modifiers & DFH_MOD_SHIFT) sym += "Shift-"; std::string key_name; @@ -91,7 +92,10 @@ std::optional Hotkey::parseKeySpec(std::string spec, std::string* err) } return found; }; - while (match_modifier("shift-", DFH_MOD_SHIFT) || match_modifier("ctrl-", DFH_MOD_CTRL) || match_modifier("alt-", DFH_MOD_ALT)) {} + while (match_modifier("shift-", DFH_MOD_SHIFT) + || match_modifier("ctrl-", DFH_MOD_CTRL) + || match_modifier("alt-", DFH_MOD_ALT) + || match_modifier("super-", DFH_MOD_SUPER)) {} out.sym = DFSDL::DFSDL_GetKeyFromName(spec.c_str()); if (out.sym != SDLK_UNKNOWN) From 05f6440c3aab8a036024d6140c804774b0afb794 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sun, 23 Nov 2025 18:51:38 -0500 Subject: [PATCH 14/18] Add heuristic method to catch potentially disruptive keybinds --- docs/dev/Lua API.rst | 6 +++ library/LuaApi.cpp | 10 ++++- library/include/modules/Hotkey.h | 21 ++++++---- library/modules/Hotkey.cpp | 68 ++++++++++++++++++++------------ plugins/hotkeys.cpp | 2 +- 5 files changed, 71 insertions(+), 36 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 27dcf62558..27103b2d9d 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1471,6 +1471,12 @@ Hotkey module Reads the latest saved keybind input that was requested. Returns a keyspec string for the input, or nil if no input has been saved. +* ``dfhack.hotkey.isDisruptiveKeybind(keyspec)`` + + Determines if the provided keyspec could be disruptive to the game experience. + This includes the majority of standard characters and special keys such as escape, + backspace, and return when lacking modifiers other than Shift. + Units module ------------ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 3021a70b1d..6353f5c07f 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1874,6 +1874,13 @@ static bool hotkey_addKeybind(const std::string spec, const std::string cmd) { return hotkey_mgr->addKeybind(spec, cmd); } +static bool hotkey_isDisruptiveKeybind(const std::string spec) { + auto key = Hotkey::KeySpec::parse(spec); + if (!key.has_value()) + return true; + return key.value().isDisruptive(); +} + static int hotkey_requestKeybindingInput(lua_State *L) { auto hotkey_mgr = Core::getInstance().getHotkeyManager(); if (!hotkey_mgr) return 0; @@ -1935,7 +1942,7 @@ void hotkey_pushBindArray(lua_State *L, const std::vector& b lua_createtable(L, 0, 2); lua_pushlstring(L, "spec", 4); - auto spec_str = Hotkey::keyspec_to_string(bind.spec, true); + auto spec_str = bind.spec.toString(true); lua_pushlstring(L, spec_str.data(), spec_str.size()); lua_settable(L, -3); @@ -1973,6 +1980,7 @@ static const luaL_Reg dfhack_hotkey_funcs[] = { static const LuaWrapper::FunctionReg dfhack_hotkey_module[] = { WRAPN(addKeybind, hotkey_addKeybind), + WRAPN(isDisruptiveKeybind, hotkey_isDisruptiveKeybind), { NULL, NULL } }; diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 6414ec9204..2e9a400cd1 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -11,11 +11,19 @@ namespace DFHack { namespace Hotkey { - struct KeySpec { - int modifiers = 0; - // Negative numbers denote mouse buttons - int sym = 0; - std::vector focus; + class DFHACK_EXPORT KeySpec { + public: + int modifiers = 0; + // Negative numbers denote mouse buttons + int sym = 0; + std::vector focus; + + static std::optional parse(std::string spec, std::string* err = nullptr); + std::string toString(bool include_focus=true) const; + + // Determines if a keybind could be disruptive to normal gameplay, + // including typing and navigating the UI. + bool isDisruptive() const; }; struct KeyBinding { @@ -23,9 +31,6 @@ namespace DFHack { std::string command; std::string cmdline; }; - - DFHACK_EXPORT std::optional parseKeySpec(std::string spec, std::string* err = nullptr); - DFHACK_EXPORT std::string keyspec_to_string(const KeySpec& spec, bool include_focus=false); } class DFHACK_EXPORT HotkeyManager { friend class Core; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index 11a462dec9..eb7b483af0 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -38,39 +38,38 @@ bool operator==(const KeyBinding& a, const KeyBinding& b) { a.cmdline == b.cmdline; } -std::string Hotkey::keyspec_to_string(const KeySpec &spec, bool include_focus) { - std::string sym; - if (spec.modifiers & DFH_MOD_CTRL) sym += "Ctrl-"; - if (spec.modifiers & DFH_MOD_ALT) sym += "Alt-"; - if (spec.modifiers & DFH_MOD_SUPER) sym += "Super-"; - if (spec.modifiers & DFH_MOD_SHIFT) sym += "Shift-"; +std::string KeySpec::toString(bool include_focus) const { + std::string out; + if (modifiers & DFH_MOD_CTRL) out += "Ctrl-"; + if (modifiers & DFH_MOD_ALT) out += "Alt-"; + if (modifiers & DFH_MOD_SUPER) out += "Super-"; + if (modifiers & DFH_MOD_SHIFT) out += "Shift-"; std::string key_name; - if (spec.sym < 0) { - key_name = "MOUSE" + std::to_string(-spec.sym); + if (this->sym < 0) { + key_name = "MOUSE" + std::to_string(-this->sym); } else { - key_name = DFSDL::DFSDL_GetKeyName(spec.sym); + key_name = DFSDL::DFSDL_GetKeyName(this->sym); } - sym += key_name; + out += key_name; - if (include_focus && !spec.focus.empty()) { - sym += "@"; + if (include_focus && !this->focus.empty()) { + out += "@"; bool first = true; - for (const auto& focus : spec.focus) { + for (const auto& fc : this->focus) { if (first) { first = false; - sym += focus; + out += fc; } else { - sym += "|" + focus; + out += "|" + fc; } } } - return sym; + return out; } - -std::optional Hotkey::parseKeySpec(std::string spec, std::string* err) { +std::optional KeySpec::parse(std::string spec, std::string* err) { KeySpec out; // Determine focus string, if present @@ -126,6 +125,23 @@ std::optional Hotkey::parseKeySpec(std::string spec, std::string* err) return std::nullopt; } +bool KeySpec::isDisruptive() const { + // Miscellaneous essential keys + const std::string essential_key_set = "\r\x1B\b\t -=[]\\;',./"; + + // Letters A-Z, 0-9, and other special keys such as return/escape, and other general typing keys + bool is_essential_key = (this->sym >= SDLK_a && this->sym <= SDLK_z) + || (this->sym >= SDLK_0 && this->sym <= SDLK_9) + || essential_key_set.find(this->sym) != std::string::npos + || (this->sym >= SDLK_LEFT && this->sym <= SDLK_UP); + + // Essential keys are safe, so long as they have a modifier that isn't Shift + if (is_essential_key && !(this->modifiers & ~DFH_MOD_SHIFT)) + return true; + + return false; +} + // Hotkeys actions are executed from an external thread to avoid deadlocks // that may occur if running commands from the render or simulation threads. void HotkeyManager::hotkey_thread_fn() { @@ -179,7 +195,7 @@ bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { } bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { - std::optional spec_opt = Hotkey::parseKeySpec(keyspec); + std::optional spec_opt = KeySpec::parse(keyspec); if (!spec_opt.has_value()) return false; return this->addKeybind(spec_opt.value(), cmd); @@ -205,7 +221,7 @@ bool HotkeyManager::removeKeybind(const KeySpec& spec, bool match_focus, std::st } bool HotkeyManager::removeKeybind(std::string keyspec, bool match_focus, std::string_view cmdline) { - std::optional spec_opt = Hotkey::parseKeySpec(keyspec); + std::optional spec_opt = KeySpec::parse(keyspec); if (!spec_opt.has_value()) return false; return this->removeKeybind(spec_opt.value(), match_focus, cmdline); @@ -243,7 +259,7 @@ std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { std::vector HotkeyManager::listKeybinds(std::string keyspec) { std::lock_guard l(lock); - std::optional spec_opt = Hotkey::parseKeySpec(keyspec); + std::optional spec_opt = KeySpec::parse(keyspec); if (!spec_opt.has_value()) return {}; return this->listKeybinds(spec_opt.value()); @@ -305,7 +321,7 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { KeySpec spec; spec.sym = sym; spec.modifiers = modifiers; - requested_keybind = Hotkey::keyspec_to_string(spec); + requested_keybind = spec.toString(false); keybind_save_requested = false; return true; } @@ -377,7 +393,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto if (parts[0] == "set") removeKeybind(keystr); for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { - auto spec = Hotkey::parseKeySpec(keystr, &parse_error); + auto spec = KeySpec::parse(keystr, &parse_error); if (!spec.has_value()) { con.printerr("%s\n", parse_error.c_str()); break; @@ -390,7 +406,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto } else if (parts.size() >= 2 && parts[0] == "clear") { for (const auto& part : parts | std::views::drop(1)) { - auto spec = Hotkey::parseKeySpec(part, &parse_error); + auto spec = KeySpec::parse(part, &parse_error); if (!spec.has_value()) { con.printerr("%s\n", parse_error.c_str()); } @@ -401,7 +417,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto } } else if (parts.size() == 2 && parts[0] == "list") { - auto spec = Hotkey::parseKeySpec(parts[1], &parse_error); + auto spec = KeySpec::parse(parts[1], &parse_error); if (!spec.has_value()) { con.printerr("%s\n", parse_error.c_str()); return; @@ -419,7 +435,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl << "Later adds, and earlier items within one command have priority." << std::endl - << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl + << "Supported keys: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.)." << std::endl << "Context may be used to limit the scope of the binding, by" << std::endl << "requiring the current context to have a certain prefix." << std::endl << "Current UI context is: " << std::endl diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index c0a5f4fccd..448d2a1c10 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -94,7 +94,7 @@ static void find_active_keybindings(color_ostream &out, df::viewscreen *screen, auto active_binds = Core::getInstance().getHotkeyManager()->listActiveKeybinds(); for (const auto& bind : active_binds) { - string sym = Hotkey::keyspec_to_string(bind.spec); + string sym = bind.spec.toString(false); add_binding_if_valid(out, sym, bind.cmdline, screen, filtermenu); } From b89d747492fcc7eb33970f0605cedac5e58d54cc Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Tue, 25 Nov 2025 11:01:46 -0500 Subject: [PATCH 15/18] Cleanup pass --- .gitignore | 2 +- docs/builtins/keybinding.rst | 3 ++- docs/changelog.txt | 1 + library/include/Core.h | 1 - library/modules/Hotkey.cpp | 26 ++++++++++++++------------ 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 6142448b7a..60a0e1b6b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # linux backup files *~ -.cache + # Kdevelop project files *.kdev4 .kdev4 diff --git a/docs/builtins/keybinding.rst b/docs/builtins/keybinding.rst index f77a081650..e8f206848d 100644 --- a/docs/builtins/keybinding.rst +++ b/docs/builtins/keybinding.rst @@ -32,7 +32,8 @@ The ```` parameter above has the following case-insensitive syntax:: [Ctrl-][Alt-][Super-][Shift-]KEY[@context[|context...]] where the ``KEY`` part can be any recognized key and :kbd:`[`:kbd:`]` denote -optional parts. +optional parts. It is important to note that the key is the non-shifted version +of the key. For example ``!`` would be defined as ``Shift-0``. DFHack commands can advertise the contexts in which they can be usefully run. For example, a command that acts on a selected unit can tell `keybinding` that diff --git a/docs/changelog.txt b/docs/changelog.txt index 6cfa16e4bf..05ebf4907b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `keybinding`: keybinds may now include the super key, and are no longer limited to particular keys ranges of keys, allowing any recognized by SDL. ## Documentation diff --git a/library/include/Core.h b/library/include/Core.h index bf0378b05d..c250eef2e2 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -270,7 +270,6 @@ namespace DFHack // Hotkey Manager DFHack::HotkeyManager *hotkey_mgr; - // FIXME: remove all this junk (hotkey related) std::vector script_paths[3]; std::mutex script_path_mutex; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index eb7b483af0..d6c72e60b9 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -1,5 +1,8 @@ #include "modules/Hotkey.h" +#include +#include + #include "Core.h" #include "ColorText.h" #include "MiscUtils.h" @@ -12,8 +15,6 @@ #include "df/viewscreen.h" #include "df/interfacest.h" -#include -#include using namespace DFHack; using Hotkey::KeySpec; @@ -103,7 +104,7 @@ std::optional KeySpec::parse(std::string spec, std::string* err) { // Attempt to parse as a mouse binding if (spec.starts_with("mouse")) { spec.erase(0, 5); - // Read button number, ensuring between 1 and 15 inclusive + // Read button number, ensuring between 4 and 15 inclusive try { int mbutton = std::stoi(spec); if (mbutton >= 4 && mbutton <= 15) { @@ -126,14 +127,15 @@ std::optional KeySpec::parse(std::string spec, std::string* err) { } bool KeySpec::isDisruptive() const { - // Miscellaneous essential keys + // SDLK enum uses the actual characters for a key as its value. + // Escaped values included are Return, Escape, Backspace, and Tab const std::string essential_key_set = "\r\x1B\b\t -=[]\\;',./"; // Letters A-Z, 0-9, and other special keys such as return/escape, and other general typing keys - bool is_essential_key = (this->sym >= SDLK_a && this->sym <= SDLK_z) - || (this->sym >= SDLK_0 && this->sym <= SDLK_9) + bool is_essential_key = (this->sym >= SDLK_a && this->sym <= SDLK_z) // A-Z + || (this->sym >= SDLK_0 && this->sym <= SDLK_9) // 0-9 || essential_key_set.find(this->sym) != std::string::npos - || (this->sym >= SDLK_LEFT && this->sym <= SDLK_UP); + || (this->sym >= SDLK_LEFT && this->sym <= SDLK_UP); // Arrow keys // Essential keys are safe, so long as they have a modifier that isn't Shift if (is_essential_key && !(this->modifiers & ~DFH_MOD_SHIFT)) @@ -306,11 +308,12 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { if (!df::global::gview || !df::global::plotinfo) return false; - // Get bottommost active screen + // Get topmost active screen df::viewscreen *screen = &df::global::gview->view; while (screen->child) screen = screen->child; + // Map keypad return to return if (sym == SDLK_KP_ENTER) sym = SDLK_RETURN; @@ -333,6 +336,7 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { auto& core = Core::getInstance(); bool mortal_mode = core.getMortalMode(); + // Iterate in reverse, prioritizing the last added keybinds for (const auto& bind : binds | std::views::reverse) { if (bind.spec.modifiers != modifiers) continue; @@ -340,9 +344,7 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { if (!bind.spec.focus.empty()) { bool matched = false; for (const auto& focus : bind.spec.focus) { - printf("Focus check for: %s", focus.c_str()); if (Gui::matchFocusString(focus)) { - printf("Matched\n"); matched = true; break; } @@ -351,7 +353,7 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { continue; } - if (!Core::getInstance().getPluginManager()->CanInvokeHotkey(bind.command, screen)) + if (!core.getPluginManager()->CanInvokeHotkey(bind.command, screen)) continue; if (mortal_mode && core.isArmokTool(bind.command)) @@ -435,7 +437,7 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl << "Later adds, and earlier items within one command have priority." << std::endl - << "Supported keys: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.)." << std::endl + << "Key format: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.)." << std::endl << "Context may be used to limit the scope of the binding, by" << std::endl << "requiring the current context to have a certain prefix." << std::endl << "Current UI context is: " << std::endl From f0e98b5ab51cf5712670f8d995b7b50fb1309c00 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Tue, 25 Nov 2025 12:10:31 -0500 Subject: [PATCH 16/18] Address tidy checks --- library/include/modules/Hotkey.h | 10 +++---- library/modules/Hotkey.cpp | 50 ++++++++++++++++---------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 2e9a400cd1..78978f29f6 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -39,8 +39,8 @@ namespace DFHack { ~HotkeyManager(); - bool addKeybind(std::string keyspec, std::string cmd); - bool addKeybind(Hotkey::KeySpec spec, std::string cmd); + bool addKeybind(std::string keyspec, std::string_view cmd); + bool addKeybind(Hotkey::KeySpec spec, std::string_view cmd); // Clear a keybind with the given keyspec, optionally for any focus, or with a specific command bool removeKeybind(std::string keyspec, bool match_focus=true, std::string_view cmdline=""); bool removeKeybind(const Hotkey::KeySpec& spec, bool match_focus=true, std::string_view cmdline=""); @@ -62,13 +62,13 @@ namespace DFHack { private: std::thread hotkey_thread; - std::mutex lock {}; - std::condition_variable cond {}; + std::mutex lock; + std::condition_variable cond; bool keybind_save_requested = false; std::string requested_keybind; - int hotkey_sig = 0; + uint8_t hotkey_sig = 0; std::string queued_command; std::map> bindings; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index d6c72e60b9..d7ef91ed10 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -20,20 +20,20 @@ using namespace DFHack; using Hotkey::KeySpec; using Hotkey::KeyBinding; -enum HotkeySignal { +enum HotkeySignal : uint8_t { None = 0, CmdReady, Shutdown, }; -bool operator==(const KeySpec& a, const KeySpec& b) { +static bool operator==(const KeySpec& a, const KeySpec& b) { return a.modifiers == b.modifiers && a.sym == b.sym && a.focus.size() == b.focus.size() && std::equal(a.focus.begin(), a.focus.end(), b.focus.begin()); } // Equality operator for key bindings -bool operator==(const KeyBinding& a, const KeyBinding& b) { +static bool operator==(const KeyBinding& a, const KeyBinding& b) { return a.spec == b.spec && a.command == b.command && a.cmdline == b.cmdline; @@ -134,7 +134,7 @@ bool KeySpec::isDisruptive() const { // Letters A-Z, 0-9, and other special keys such as return/escape, and other general typing keys bool is_essential_key = (this->sym >= SDLK_a && this->sym <= SDLK_z) // A-Z || (this->sym >= SDLK_0 && this->sym <= SDLK_9) // 0-9 - || essential_key_set.find(this->sym) != std::string::npos + || (this->sym < CHAR_MAX && essential_key_set.find((char)this->sym) != std::string::npos) || (this->sym >= SDLK_LEFT && this->sym <= SDLK_UP); // Arrow keys // Essential keys are safe, so long as they have a modifier that isn't Shift @@ -173,13 +173,13 @@ void HotkeyManager::hotkey_thread_fn() { } -bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { +bool HotkeyManager::addKeybind(KeySpec spec, std::string_view cmd) { // No point in a hotkey with no action if (cmd.empty()) return false; KeyBinding binding; - binding.spec = spec; + binding.spec = std::move(spec); binding.cmdline = cmd; size_t space_idx = cmd.find(' '); binding.command = space_idx == std::string::npos ? cmd : cmd.substr(0, space_idx); @@ -196,8 +196,8 @@ bool HotkeyManager::addKeybind(KeySpec spec, std::string cmd) { return true; } -bool HotkeyManager::addKeybind(std::string keyspec, std::string cmd) { - std::optional spec_opt = KeySpec::parse(keyspec); +bool HotkeyManager::addKeybind(std::string keyspec, std::string_view cmd) { + std::optional spec_opt = KeySpec::parse(std::move(keyspec)); if (!spec_opt.has_value()) return false; return this->addKeybind(spec_opt.value(), cmd); @@ -223,7 +223,7 @@ bool HotkeyManager::removeKeybind(const KeySpec& spec, bool match_focus, std::st } bool HotkeyManager::removeKeybind(std::string keyspec, bool match_focus, std::string_view cmdline) { - std::optional spec_opt = KeySpec::parse(keyspec); + std::optional spec_opt = KeySpec::parse(std::move(keyspec)); if (!spec_opt.has_value()) return false; return this->removeKeybind(spec_opt.value(), match_focus, cmdline); @@ -261,7 +261,7 @@ std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { std::vector HotkeyManager::listKeybinds(std::string keyspec) { std::lock_guard l(lock); - std::optional spec_opt = KeySpec::parse(keyspec); + std::optional spec_opt = KeySpec::parse(std::move(keyspec)); if (!spec_opt.has_value()) return {}; return this->listKeybinds(spec_opt.value()); @@ -371,7 +371,7 @@ bool HotkeyManager::handleKeybind(int sym, int modifiers) { void HotkeyManager::setHotkeyCommand(std::string cmd) { std::unique_lock l(lock); - queued_command = cmd; + queued_command = std::move(cmd); hotkey_sig = HotkeySignal::CmdReady; l.unlock(); cond.notify_all(); @@ -391,7 +391,7 @@ std::string HotkeyManager::getKeybindingInput() { void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vector& parts) { std::string parse_error; if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { - std::string keystr = parts[1]; + const std::string& keystr = parts[1]; if (parts[0] == "set") removeKeybind(keystr); for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { @@ -426,22 +426,22 @@ void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vecto } std::vector list = listKeybinds(spec.value()); if (list.empty()) - con << "No bindings." << std::endl; + con << "No bindings.\n"; for (const auto& kb : list) - con << " " << kb << std::endl; + con << " " << kb << "\n"; } else { - con << "Usage:" << std::endl - << " keybinding list " << std::endl - << " keybinding clear [@context]..." << std::endl - << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl - << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl - << "Later adds, and earlier items within one command have priority." << std::endl - << "Key format: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.)." << std::endl - << "Context may be used to limit the scope of the binding, by" << std::endl - << "requiring the current context to have a certain prefix." << std::endl - << "Current UI context is: " << std::endl - << join_strings("\n", Gui::getCurFocus(true)) << std::endl; + con << "Usage:\n" + << " keybinding list \n" + << " keybinding clear [@context]...\n" + << " keybinding set [@context] \"cmdline\" \"cmdline\"...\n" + << " keybinding add [@context] \"cmdline\" \"cmdline\"...\n" + << "Later adds, and earlier items within one command have priority.\n" + << "Key format: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.).\n" + << "Context may be used to limit the scope of the binding, by\n" + << "requiring the current context to have a certain prefix.\n" + << "Current UI context is: \n" + << join_strings("\n", Gui::getCurFocus(true)) << "\n"; } } From f65e4bdcc49de55979bc576f68158bae50c392ff Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 4 Dec 2025 16:17:45 -0500 Subject: [PATCH 17/18] Remove redundant lock causing deadlock when listing keybinds --- library/modules/Hotkey.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index d7ef91ed10..e5fbf77f78 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -260,7 +260,6 @@ std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { } std::vector HotkeyManager::listKeybinds(std::string keyspec) { - std::lock_guard l(lock); std::optional spec_opt = KeySpec::parse(std::move(keyspec)); if (!spec_opt.has_value()) return {}; From 0e341b76b5b96d7b27616c77bace2df69728e4b7 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 4 Dec 2025 16:33:33 -0500 Subject: [PATCH 18/18] Relocate keybinding command handling --- library/Commands.cpp | 79 +++++++++++++++++--------------- library/include/modules/Hotkey.h | 2 - library/modules/Hotkey.cpp | 57 ----------------------- 3 files changed, 43 insertions(+), 95 deletions(-) diff --git a/library/Commands.cpp b/library/Commands.cpp index 796a1f2701..289219c594 100644 --- a/library/Commands.cpp +++ b/library/Commands.cpp @@ -9,6 +9,7 @@ #include "RemoteTools.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/World.h" #include "df/viewscreen_new_regionst.h" @@ -282,56 +283,62 @@ namespace DFHack command_result Commands::keybinding(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) { - if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) - { - std::string keystr = parts[1]; + using Hotkey::KeySpec; + auto hotkey_mgr = core.getHotkeyManager(); + std::string parse_error; + if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { + const std::string& keystr = parts[1]; if (parts[0] == "set") - core.ClearKeyBindings(keystr); - // for (int i = parts.size()-1; i >= 2; i--) - for (const auto& part : parts | std::views::drop(2) | std::views::reverse) - { - if (!core.AddKeyBinding(keystr, part)) - { - con.printerr("Invalid key spec: %s\n", keystr.c_str()); - return CR_FAILURE; + hotkey_mgr->removeKeybind(keystr); + for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { + auto spec = KeySpec::parse(keystr, &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + break; + } + if (!hotkey_mgr->addKeybind(spec.value(), part)) { + con.printerr("Invalid command: '%s'\n", part.c_str()); + break; } } } - else if (parts.size() >= 2 && parts[0] == "clear") - { - // for (size_t i = 1; i < parts.size(); i++) - for (const auto& part : parts | std::views::drop(1)) - { - if (!core.ClearKeyBindings(part)) - { - con.printerr("Invalid key spec: %s\n", part.c_str()); - return CR_FAILURE; + else if (parts.size() >= 2 && parts[0] == "clear") { + for (const auto& part : parts | std::views::drop(1)) { + auto spec = KeySpec::parse(part, &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + } + if (!hotkey_mgr->removeKeybind(spec.value())) { + con.printerr("No matching keybinds to remove\n"); + break; } } } - else if (parts.size() == 2 && parts[0] == "list") - { - std::vector list = core.ListKeyBindings(parts[1]); + else if (parts.size() == 2 && parts[0] == "list") { + auto spec = KeySpec::parse(parts[1], &parse_error); + if (!spec.has_value()) { + con.printerr("%s\n", parse_error.c_str()); + return CR_FAILURE; + } + std::vector list = hotkey_mgr->listKeybinds(spec.value()); if (list.empty()) con << "No bindings." << std::endl; for (const auto& kb : list) con << " " << kb << std::endl; } - else - { - con << "Usage:" << std::endl - << " keybinding list " << std::endl - << " keybinding clear [@context]..." << std::endl - << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl - << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl - << "Later adds, and earlier items within one command have priority." << std::endl - << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl - << "Context may be used to limit the scope of the binding, by" << std::endl - << "requiring the current context to have a certain prefix." << std::endl - << "Current UI context is: " << std::endl + else { + con << "Usage:\n" + << " keybinding list \n" + << " keybinding clear [@context]...\n" + << " keybinding set [@context] \"cmdline\" \"cmdline\"...\n" + << " keybinding add [@context] \"cmdline\" \"cmdline\"...\n" + << "Later adds, and earlier items within one command have priority.\n" + << "Key format: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.).\n" + << "Context may be used to limit the scope of the binding, by\n" + << "requiring the current context to have a certain prefix.\n" + << "Current UI context is: \n" << join_strings("\n", Gui::getCurFocus(true)) << std::endl; } - return CR_OK; } diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h index 78978f29f6..25129b2ff5 100644 --- a/library/include/modules/Hotkey.h +++ b/library/include/modules/Hotkey.h @@ -33,7 +33,6 @@ namespace DFHack { }; } class DFHACK_EXPORT HotkeyManager { - friend class Core; public: HotkeyManager(); ~HotkeyManager(); @@ -74,6 +73,5 @@ namespace DFHack { std::map> bindings; void hotkey_thread_fn(); - void handleKeybindingCommand(color_ostream& out, const std::vector& parts); }; } diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp index e5fbf77f78..6433879d85 100644 --- a/library/modules/Hotkey.cpp +++ b/library/modules/Hotkey.cpp @@ -387,63 +387,6 @@ std::string HotkeyManager::getKeybindingInput() { return requested_keybind; } -void HotkeyManager::handleKeybindingCommand(color_ostream &con, const std::vector& parts) { - std::string parse_error; - if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { - const std::string& keystr = parts[1]; - if (parts[0] == "set") - removeKeybind(keystr); - for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { - auto spec = KeySpec::parse(keystr, &parse_error); - if (!spec.has_value()) { - con.printerr("%s\n", parse_error.c_str()); - break; - } - if (!addKeybind(spec.value(), part)) { - con.printerr("Invalid command: '%s'\n", part.c_str()); - break; - } - } - } - else if (parts.size() >= 2 && parts[0] == "clear") { - for (const auto& part : parts | std::views::drop(1)) { - auto spec = KeySpec::parse(part, &parse_error); - if (!spec.has_value()) { - con.printerr("%s\n", parse_error.c_str()); - } - if (!removeKeybind(spec.value())) { - con.printerr("No matching keybinds to remove\n"); - break; - } - } - } - else if (parts.size() == 2 && parts[0] == "list") { - auto spec = KeySpec::parse(parts[1], &parse_error); - if (!spec.has_value()) { - con.printerr("%s\n", parse_error.c_str()); - return; - } - std::vector list = listKeybinds(spec.value()); - if (list.empty()) - con << "No bindings.\n"; - for (const auto& kb : list) - con << " " << kb << "\n"; - } - else { - con << "Usage:\n" - << " keybinding list \n" - << " keybinding clear [@context]...\n" - << " keybinding set [@context] \"cmdline\" \"cmdline\"...\n" - << " keybinding add [@context] \"cmdline\" \"cmdline\"...\n" - << "Later adds, and earlier items within one command have priority.\n" - << "Key format: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.).\n" - << "Context may be used to limit the scope of the binding, by\n" - << "requiring the current context to have a certain prefix.\n" - << "Current UI context is: \n" - << join_strings("\n", Gui::getCurFocus(true)) << "\n"; - } -} - HotkeyManager::HotkeyManager() { this->hotkey_thread = std::thread(&HotkeyManager::hotkey_thread_fn, this); }