Keymaps #4970
Replies: 2 comments 4 replies
-
Make it so. |
Beta Was this translation helpful? Give feedback.
-
In the past, when binding keys, I used to reassign them by inheriting classes. There seem to be some issues with binding, Later on, I implemented this function through drawing about Footer. I am a dictionary translation obtained at runtime (after the textual program selects the current language), which means it cannot be constructed before the class. My expectation is that the description can be obtained through the return value of a method. Is this possible? |
Beta Was this translation helpful? Give feedback.
-
Keybindings in Textual applications are statically declared via the
BINDINGS
ClassVar at theApp
,Screen
, orWidget
level. There is currently no API which allows modifying the chosen keybindings after the developer adds it toBINDINGS
.Here, I sketch out an API which would allow developers to declare "overrides" for those statically declared keys. It aims to resolve one of the most common requests I see from Textual app end-users: "hey - can you please make it such that I can press X instead of Y to perform Z"?
This is not an attempt to add ultimate flexibility to the bindings system, such as the ability to customise any binding in any way at runtime. That is a considerably more niche (and harder to achieve) requirement!
Feel free to leave thoughts below!
Motivation
Simply put, it's impossible for a Textual app developer to choose a set of keybindings which satisfies all user preferences and environments.
ctrl+up
to an action, unaware that MacOS will eat this keypress by default.ctrl+\
for the command palette, which seemed like a good choice with our UK and US keyboard layouts, but caused problems for folks with some European keyboard layouts.Anecdotally, requests to change keybindings are probably the most common issue that falls under Textual's "remit" that has been asked in issues and discussions for my own projects. We've also seen popular Textual-based applications such as Harlequin roll their own system to allow end-users to customise keybinds, further suggesting the need for such an API.
Proposal
Binding IDs
In order to update a specific binding, we need to be able to reference it.
The
Binding
object will be extended with a newid
parameter which will be used to identify it. Textual will not enforce uniqueness of these binding IDs in any way. In the future, this ID could be used as part of a more comprehensive runtime binding update API.Here's an example of some bindings defined at the App-level that have binding IDs:
and some at the Widget-level:
These IDs are my own choices. Developers can choose any string ID they wish in order to reference the binding.
Bindings with no key assigned
The
key
parameter ofBinding
will acceptNone
as a valid value.This will indicate the
Binding
is not currently mapped to an action, but that it can be mapped via a keymap.Keymap API
This API makes it easy for developers to allow end-users to override the default bindings (those defined via the
BINDINGS
classvar) in their app.A
Keymap
maps binding IDs to the key combination required to trigger those bindings. AnApp
can have a single keymap, which should be returned from theload_keymap
method. The default keymap is empty, which means none of the default bindings will be overridden.The keymap will override the
ctrl+j
keybinding in theCountInput
which we gave the binding ID"count.submit"
withalt+enter
. It'll replace the binding with ID"increment"
, so that you instead have to press+
to activate it.A binding ID mapped to
None
in the keymap will unbind that binding.Since a keymap is just instantiated with a
dict
, it's easy to load from an external source (config file/keymap file/external API).The
Keymap
is returned from a method onApp
because it suggest to the developer that the keymap is loaded once, by the app, on startup. If it were areactive
, it'd suggest that keymaps can be swapped out on the fly.It may be possible to do this swapping at runtime (for example, swap out a bunch of the
text_area.*
bindings in the keymap when we change from insert to normal mode in a modal text editor), but that would require further research on my part. Perhaps those kind of runtime changes are a different API altogether, however.If a binding ID supplied in the keymap is not valid, it does not cause an exception as doing so may cause backwards compatibility issues for Textual app developers as they make changes which may not align with their users config/keymaps - likely sourced from an external file. If a developer wishes, they can track the binding IDs used themselves and warn end-users if necessary if an invalid binding ID is supplied.
Questions
What happens if two binding IDs are the same?
If two Bindings share an ID, they'll both be updated should that binding ID appear in the
Keymap
. This could actually be useful as a means of ensuring consistent keybindings across an app. For example, Posting has several subclasses ofInput
. If all theseInput
subclasses shared an ID for "submitting" the Input, then a single entry in theKeymap
would apply to all of theInput
subclasses, ensuring the same key is used for submission across the whole application.How do Binding IDs work when a widget is subclassed?
Applying the overrides from the keymap happens before we attempt to inherit bindings from a parent widget. After the keymap is applied, binding inheritance works as normal.
Let's work through some different cases:
Same binding ID in child and parent:
Input
with bindingBinding("ctrl+j", "submit", id="submit")
MyInput(Input)
withBinding("enter", "submit", id="submit")
With the keymap
Keymap({"submit": "ctrl+o"})
, allInput
andMyInput
widgets in the app will be updated such that onlyctrl+o
will submit. The original bindings (ctrl+j
on Input andenter
on MyInput) are lost.Child has a different ID:
Input
with bindingBinding("ctrl+j", "submit", id="submit")
MyInput(Input)
withBinding("enter", "submit", id="foo.submit")
With a keymap
Keymap({"submit": "ctrl+o"})
, theInput
will have it's "submit" binding overridden toctrl+o
. SinceMyInput
inherits the "submit" binding fromInput
,ctrl+o
will work their too. As far as Textual is concerned (even today), these are "different" bindings.Input
withBinding("ctrl+j", "submit", id="submit")
MyInput(Input)
withBinding("enter", "submit")
With
Keymap({"submit": "ctrl+o"})
, the binding inInput
will be updated toctrl+o
, and that will be inherited byMyInput
.Overriding a binding with same key, action, but no ID:
This case is a little harder to reason about.
Input
withBinding("ctrl+j", "submit", id="submit")
MyInput(Input)
withBinding("ctrl+j", "submit")
With
Keymap({"submit": "ctrl+o"})
: Thesubmit
binding in the parentInput
should be overridden before the "inheritance" takes place. (Inheritance takes place when the same key is used, it's unrelated to binding IDs). Since the keys no longer match between parent and child, the inheritance of the binding no longer occurs.What about comma-separated bindings
Some bindings are declared like
Binding("down,j", ...)
(i.e. press either down or j to perform the action). If you give this binding an ID and then override it using a keymap, then it'll replace both keys.Can I use the
Keymap
to disable a binding?This is probably better handled using separate app-specific config defined by the app developer. In other words, allow the binding to still fire, but add a check at the top of the
action_
handler to no-op.Beta Was this translation helpful? Give feedback.
All reactions