Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
42fc19e
#396 Add reusable editbox and dropdown UI components
burnhamrobertp Jun 30, 2026
081b3ee
#396 Add keybind model and curated command catalog
burnhamrobertp Jun 30, 2026
c5689f5
#396 Add in-game keybind editor to the Keys panel
burnhamrobertp Jun 30, 2026
c85bfa9
#396 Add keybind editor localization strings
burnhamrobertp Jun 30, 2026
54752db
#396 Scope editbox and dropdown to the keybind editor
burnhamrobertp Jul 8, 2026
e154fef
#396 Reword editbox and dropdown headers to reflect editor scope
burnhamrobertp Jul 8, 2026
98163e3
#396 Ignore non-left clicks and hold text owner during modal/dropdown
burnhamrobertp Jul 8, 2026
c0420a7
#396 Left-align the keybind editor preset dropdown label
burnhamrobertp Jul 9, 2026
99e9007
#396 Close the Keys panel on Escape from the editor
burnhamrobertp Jul 9, 2026
f19ded7
#396 Support prefix-matched command categories in the editor
burnhamrobertp Jul 9, 2026
0acdb2f
#396 Categorize remaining commands and fix multi-arg action ids
burnhamrobertp Jul 9, 2026
2b85d58
#396 Match Settings preset behavior and gate editing to Custom
burnhamrobertp Jul 9, 2026
61b5359
#396 Defer keybind writes to panel exit and log preset switches
burnhamrobertp Jul 9, 2026
da6fdf6
#396 Use a single key column in the keybind list
burnhamrobertp Jul 14, 2026
f3bb072
#396 Dedupe keybinds per action and fix read-only chip padding
burnhamrobertp Jul 14, 2026
858ae00
#396 Support binding side mouse buttons in the keybind editor
burnhamrobertp Jul 14, 2026
04449bf
#396 Recategorize and reorder the keybind catalog
burnhamrobertp Jul 14, 2026
1d8341c
#396 Restore ally/spectator chat info rows
burnhamrobertp Jul 14, 2026
94e1ae0
#396 Remove the orphaned keybindings info panel
burnhamrobertp Jul 14, 2026
0d49c52
#396 Add reset-to-preset for custom keybinds
burnhamrobertp Jul 15, 2026
a18a0d0
#396 Trim comments in the keybind editor
burnhamrobertp Jul 15, 2026
c717f9e
#396 Remove the Keys panel tab bar and its reference images
burnhamrobertp Jul 15, 2026
26645e6
#396 Remove localization keys orphaned by the info panel removal
burnhamrobertp Jul 15, 2026
fc100cd
#396 Clear the search filter when the keybind panel closes
burnhamrobertp Jul 17, 2026
c2f529e
#396 Show and control the Any modifier qualifier in the keybind editor
burnhamrobertp Jul 18, 2026
de511a7
#396 Add keychain support and a capture modal to the keybind editor
burnhamrobertp Jul 20, 2026
36158a1
#396 Label and regroup the default keybinds in the editor catalog
burnhamrobertp Jul 21, 2026
5a1b6cb
Hide keybinds by exact action id instead of prefix
burnhamrobertp Jul 22, 2026
799072b
Refresh the keybind editor snapshot each time the panel opens
burnhamrobertp Jul 24, 2026
a9d26e2
Relabel the Any capture toggle to Ignore Modifiers and drop held mods
burnhamrobertp Jul 27, 2026
88a4aaa
Fix comma keysym display, control group labels, and preset reset guard
burnhamrobertp Jul 27, 2026
7123d9a
Truncate keybind editor text by character instead of byte
burnhamrobertp Jul 28, 2026
c59f738
Keep hidden and prefix claims authoritative over exact catalog rows
burnhamrobertp Jul 28, 2026
b4aa693
Clamp the keybind row click target to the rows actually drawn
burnhamrobertp Jul 28, 2026
dc35c60
Let the keybind editor handle Escape before closing the panel
burnhamrobertp Jul 28, 2026
be182fe
Drop the stale conflict detection note from the keybind model header
burnhamrobertp Jul 28, 2026
23ff888
Restore the untouched localization formatting
burnhamrobertp Jul 28, 2026
99303b9
Resolve engine functions once and name the query and custom keys file
burnhamrobertp Jul 28, 2026
8bef453
Use KEYSYMS names for the editbox key comparisons
burnhamrobertp Jul 28, 2026
01eb83c
Sentence-case the keybind capture prompt
burnhamrobertp Jul 28, 2026
f67906b
Sentence-case the keybind headings and use the game name for set unit…
burnhamrobertp Jul 29, 2026
1efb75a
Merge remote-tracking branch 'origin/master' into feature/396-keybind…
burnhamrobertp Jul 29, 2026
080ffe6
Merge branch 'master' into feature/396-keybind-editor
burnhamrobertp Jul 29, 2026
8f26592
Note that the console is the only way to write keybindings
burnhamrobertp Aug 3, 2026
784cf93
Rename switchToPreset, name the capture gap, and align the control gu…
burnhamrobertp Aug 3, 2026
50bf830
Keep the search filter between openings of the keybind panel
burnhamrobertp Aug 3, 2026
d2c227a
Merge leftover keybinds into a catalog Other category when one exists
burnhamrobertp Aug 3, 2026
d70a5ce
Externalize the keybind catalog to shared JSON
burnhamrobertp Jul 16, 2026
aea4391
Externalize the keybind preset registry to shared JSON
burnhamrobertp Jul 16, 2026
45e71db
Add JSON schemas and contract doc for the shared keybind config
burnhamrobertp Jul 16, 2026
23bead1
Add busted spec validating the keybind config contract
burnhamrobertp Jul 16, 2026
ecd0cd6
Note command descriptions as deferred future scope in the keybind con…
burnhamrobertp Jul 17, 2026
5b88ee7
Align the externalized keybind catalog with the labeled editor catalog
burnhamrobertp Jul 21, 2026
df49dbb
Fall back gracefully when the shared keybind JSON fails to load
burnhamrobertp Jul 22, 2026
b0ea08e
Add the skip-allied-upgrade toggle to the keybind catalog
burnhamrobertp Jul 22, 2026
a093a1c
Match hidden keybinds by exact id in the shared catalog
burnhamrobertp Jul 22, 2026
59d0159
Match the shared catalog control group entries to the editor catalog
burnhamrobertp Jul 27, 2026
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
810 changes: 810 additions & 0 deletions common/configs/keybind_catalog.json

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions common/configs/keybind_catalog.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://raw.githubusercontent.com/beyond-all-reason/Beyond-All-Reason/master/common/configs/keybind_catalog.schema.json",
"title": "Keybind catalog",
"description": "Ordered, cross-surface catalog of keybindable commands grouped into categories. Shared by the in-game editor and external lobbies. Holds i18n keys and bind-action ids, not resolved strings or actual key bindings.",
"type": "array",
"items": {
"oneOf": [
{
"type": "object",
"description": "Category of keybindable commands.",
"required": ["category", "items"],
"additionalProperties": false,
"properties": {
"category": {
"type": "string",
"description": "i18n key for the category title."
},
"items": {
"type": "array",
"items": { "$ref": "#/definitions/item" }
}
}
},
{
"type": "object",
"description": "Actions bound but intentionally never shown, matched by exact id (not prefix).",
"required": ["hidden"],
"additionalProperties": false,
"properties": {
"hidden": {
"type": "array",
"items": { "type": "string", "description": "Exact action id to suppress." }
}
}
}
]
},
"definitions": {
"item": {
"oneOf": [
{
"type": "object",
"description": "Editable binding the user can rebind.",
"required": ["action", "label"],
"additionalProperties": false,
"properties": {
"action": { "type": "string", "description": "Bind command (command + space-separated args), exactly as passed to /bind and reported by GetKeyBindings." },
"label": { "type": "string", "description": "i18n key for the display label." }
}
},
{
"type": "object",
"description": "Informational row: shown read-only with a fixed key hint.",
"required": ["label", "keyLabel"],
"additionalProperties": false,
"properties": {
"label": { "type": "string", "description": "i18n key for the display label." },
"keyLabel": { "type": "string", "description": "i18n key for the non-editable key hint." }
}
},
{
"type": "object",
"description": "Prefix group: claims every bound action whose id starts with this prefix. With a label the arg after the prefix is interpolated as %{n} (or its two whitespace-split tokens as %{row}/%{col}); without one, rows show the raw id. With unit=true the arg is resolved to a unit's translated name.",
"required": ["prefix"],
"additionalProperties": false,
"properties": {
"prefix": { "type": "string", "description": "Action id prefix." },
"label": { "type": "string", "description": "i18n key for the display label, interpolated per matched action." },
"unit": { "type": "boolean", "description": "When true, the arg after the prefix is a unit codename resolved to its translated human name." }
}
}
]
}
}
}
9 changes: 9 additions & 0 deletions common/configs/keybind_presets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"presets": [
{ "name": "Grid", "file": "luaui/configs/hotkeys/grid_keys.txt" },
{ "name": "Grid (60% Keyboard)", "file": "luaui/configs/hotkeys/grid_keys_60pct.txt" },
{ "name": "Legacy", "file": "luaui/configs/hotkeys/legacy_keys.txt" },
{ "name": "Legacy (60% Keyboard)", "file": "luaui/configs/hotkeys/legacy_keys_60pct.txt" },
{ "name": "Custom", "file": "uikeys.txt", "custom": true }
]
}
25 changes: 25 additions & 0 deletions common/configs/keybind_presets.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://raw.githubusercontent.com/beyond-all-reason/Beyond-All-Reason/master/common/configs/keybind_presets.schema.json",
"title": "Keybind preset registry",
"description": "Ordered registry of keybind presets shared across surfaces. The first entry is the default fallback. The active preset is whichever file the engine's KeybindingFile config points at; uikeys.txt is Custom.",
"type": "object",
"required": ["presets"],
"additionalProperties": false,
"properties": {
"presets": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["name", "file"],
"additionalProperties": false,
"properties": {
"name": { "type": "string", "description": "Display name; also the key used to look a preset up by name." },
"file": { "type": "string", "description": "Bind file this preset loads (the engine KeybindingFile value). uikeys.txt is the Custom preset." },
"custom": { "type": "boolean", "description": "True for the user-editable Custom preset (uikeys.txt)." }
}
}
}
}
}
81 changes: 81 additions & 0 deletions common/configs/keybinds.README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Shared keybind contract

These files are the cross-surface source of truth for keybind editing, so the in-game
editor, Chobby, and the new lobby can each build their own UI without duplicating the
data or the rules. They hold *data and rules only* - no rendering, no engine calls.

## Files

| File | What it is | Schema |
|---|---|---|
| `keybind_catalog.json` | Ordered categories of keybindable commands, with i18n label keys and bind-action ids. | `keybind_catalog.schema.json` |
| `keybind_presets.json` | Ordered registry of presets (display name -> bind file), with the Custom preset flagged. | `keybind_presets.schema.json` |

Both are validated in CI by `spec/keybind_catalog_spec.lua` (structure + referential
integrity: preset files exist, every i18n key resolves).

The actual bindings live in the plain-text bind files the registry points at
(`luaui/configs/hotkeys/*.txt`, and `uikeys.txt` for Custom). Those are already portable -
a consumer reads or copies them directly; only the registry (which file is which preset)
needs sharing.

## Catalog item kinds

Each category's `items` entry is exactly one of:

- `{ "action": "<bind command>", "label": "<i18n key>" }` - editable; user can rebind it.
- `{ "label": "<i18n key>", "keyLabel": "<i18n key>" }` - informational, read-only key hint.
- `{ "prefix": "<action id prefix>" }` - claims every bound action whose id starts with the
prefix, listed by raw id (for numbered families like `group 1`, `group 2`, ...). An optional
`"label"` is interpolated per matched action with the arg after the prefix as `%{n}` (or its
two whitespace-split tokens as `%{row}`/`%{col}`); an optional `"unit": true` resolves that
arg from a unit codename to its translated name.

`action` is the bind command exactly as `/bind` expects and `GetKeyBindings` reports it
(command plus space-separated args, e.g. `select AllMap++_ClearSelection_SelectAll+`).

A single leading `{ "hidden": ["<action id>", ...] }` entry (not a category) lists actions
that are bound but never shown - matched by exact id, not prefix, so a future action can't be
suppressed by coincidence - so they surface neither as a row nor under "Other".

## The config contract (behavior each surface implements)

Structure lives in the schemas; these are the operations, which a schema can't express.
Every surface answers the same questions from the same facts:

- **Which preset are we on?** Read the engine config string `KeybindingFile`. Match it
against `keybind_presets.json` `file` values. `uikeys.txt` means **Custom**; anything
else is the named preset. If it points at nothing valid, fall back to the first entry.
- **Switch preset.** Set `KeybindingFile` to the target preset's `file`, then reload the
bindings.
- **Seed Custom.** The first time Custom is selected, `uikeys.txt` may not exist yet - seed
it from the currently active bindings so the user starts from what they had.
- **Reset Custom to a preset.** Overwrite `uikeys.txt` with the chosen preset's bindings,
then reload. (Destructive to the user's custom binds - confirm first.)

### Same rules, different plumbing

| Operation | In-game (LuaUI) | Lobby (Chobby / web) |
|---|---|---|
| Read current preset | `Spring.GetConfigString("KeybindingFile")` | read the same engine config value |
| Switch preset | `Spring.SetConfigString` + `keyreload` | write the config value the game reads on launch |
| Seed / reset Custom | `keysave` a preset's binds into `uikeys.txt` | copy the preset file's contents into `uikeys.txt` |

## i18n

The catalog carries i18n *keys*, not resolved strings. Each surface resolves them through
its own localization store - in-game that's `Spring.I18N` (sourced from
`language/en/interface.json`, translated via Transifex). Sharing the *strings* across
surfaces is a separate concern from sharing this structure.

## Not covered yet

- A widget/mod action-declaration API, so widgets register their own bindable actions
(with label + category + description) into the catalog at runtime instead of only being
editable when already bound.
- Command descriptions / tooltips. The engine ships per-command descriptions in the shared
`cmd.*` i18n namespace (in `interface.json`, localized like everything else), so a future
iteration can show them by resolving `cmd.<command>` (falling back to `cmd.<command>._description`
for the few structured commands, and `cmd.luarules.<command>` for gadget commands) at display
time - no catalog change needed, since the catalog already carries the command per row. Widget/mod
actions have no `cmd.*` entry, so their descriptions depend on the declaration API above.
Loading
Loading