Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
207e443
Refactor hotkey code into a new Hotkey module
NicksWorld Nov 20, 2025
1554b55
Reorder imports
NicksWorld Nov 20, 2025
ec2b309
Move keybinding command handling into Hotkey module
NicksWorld Nov 20, 2025
3c2dc3c
Properly populate the command field on keybinds
NicksWorld Nov 20, 2025
b5d8899
Lua api for hotkeys and library support for future keybinding gui
NicksWorld Nov 20, 2025
74737b4
Prefer using to typedef
NicksWorld Nov 20, 2025
1fcee14
Improve keybinding command error messages for invalid keyspecs
NicksWorld Nov 20, 2025
e49d220
Make keyspecs case-insensitive (excluding focus strings)
NicksWorld Nov 20, 2025
43ecdad
Address code review concerns
NicksWorld Nov 21, 2025
2ee9d7e
Initial documentation pass, and improvements to lua api ergonomics
NicksWorld Nov 21, 2025
7ce44a5
Improve api ergonomics and add missing locks
NicksWorld Nov 23, 2025
68e0d5b
First pass at changelog
NicksWorld Nov 23, 2025
9c87028
Allow "Super" as a hotkey modifier
NicksWorld Nov 23, 2025
05f6440
Add heuristic method to catch potentially disruptive keybinds
NicksWorld Nov 23, 2025
b89d747
Cleanup pass
NicksWorld Nov 25, 2025
f0e98b5
Address tidy checks
NicksWorld Nov 25, 2025
f65e4bd
Remove redundant lock causing deadlock when listing keybinds
NicksWorld Dec 4, 2025
32bd846
Merge branch 'develop' into refactor/hotkey_module
NicksWorld Dec 4, 2025
0e341b7
Relocate keybinding command handling
NicksWorld Dec 4, 2025
c080015
Merge branch 'develop' into refactor/hotkey_module
ab9rf Dec 6, 2025
bfee603
Merge branch 'develop' into refactor/hotkey_module
ab9rf Dec 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions docs/builtins/keybinding.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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/Super/Shift with any key recognized by SDL.
You can also represent mouse buttons beyond the first three with ``MOUSE4``
through ``MOUSE15``.

Usage
-----
Expand All @@ -27,12 +27,13 @@ Usage
``keybinding set <key> "<cmdline>" ["<cmdline>" ...]``
Clear, and then add bindings for the specified key.

The ``<key>`` parameter above has the following **case-sensitive** syntax::
The ``<key>`` 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.
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
Expand Down
8 changes: 8 additions & 0 deletions docs/changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,22 @@ Template for new versions:

## Misc Improvements
- `createitem`: created items can now be placed onto/into tables, nests, bookcases, display cases, and altars
- `keybinding`: keybinds may now include the super key, and are no longer limited to particular keys ranges of keys, allowing any recognized by SDL.
- The ``fpause`` console command can now be used to force world generation to pause (as it did prior to version 50).

## Documentation

## API
- ``Hotkey``: New module for hotkey functionality

## Lua
- The ``Lua interactive interpreter`` banner now documents keywords such as ``unit`` and ``item`` which reference the currently-selected object in the DF UI.
- ``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

Expand Down
49 changes: 48 additions & 1 deletion docs/dev/Lua API.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1430,6 +1430,53 @@ 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.removeKeybind(keyspec, [match_focus=true, command])``

Removes keybinds matching the provided keyspec.
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()``

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.requestKeybindingInput([cancel=false])``

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.getKeybindingInput()``

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
------------

Expand Down Expand Up @@ -3494,7 +3541,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)``
Expand Down
2 changes: 2 additions & 0 deletions library/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,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
Expand Down Expand Up @@ -190,6 +191,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
Expand Down
79 changes: 43 additions & 36 deletions library/Commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -282,56 +283,62 @@ namespace DFHack

command_result Commands::keybinding(color_ostream& con, Core& core, const std::string& first, const std::vector<std::string>& 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<std::string> 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<std::string> 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 <key>" << std::endl
<< " keybinding clear <key>[@context]..." << std::endl
<< " keybinding set <key>[@context] \"cmdline\" \"cmdline\"..." << std::endl
<< " keybinding add <key>[@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 <key>\n"
<< " keybinding clear <key>[@context]...\n"
<< " keybinding set <key>[@context] \"cmdline\" \"cmdline\"...\n"
<< " keybinding add <key>[@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;
}

Expand Down
Loading
Loading