scli
scli copied to clipboard
Configurable key bindings
Allow users to remap the default key bindings.
The syntax can look something like this:
sclirc
------
key-bindings = {"copy_message": "y", "select_next_contact": ["ctrl n", "meta down"], ...}
# OR
[key_bindings]
copy_message = y
select_next_contact = ctrl n, meta down
insert_newline : meta enter
...
A potential difficulty might come from the multiple "contexts" in which the keypress events are processed in scli. Unlike say, vim, which only has a handful of "modes" (normal, insert, visual, etc), in scli every other widget has its own keypress method. The easy part is more of a "nomenclature" issue: how to keep the naming scheme consistent and future-proof. A more difficult question: how to check that a new key binding does not conflict with another one?
Not every keypress in every widget needs to be made re-mappable, just the ones that users would likely want to customize.
I think key bindings should be configured per pane basis. This should fix all the problems related to naming schema. Of course this will bring some verbosity to the configuration file, like if you need to remap j to a different key, then you need to do this twice. For contacts pane and for messages pane. I also think that this will not be a real issue in a sense that the most you repeat yourself will be at most two or three times and I also see this as a feature.
This will also keep the changes at quite minimum as this is how inner keybinding logic currently works. One might see this as coupling implementation details with the configuration but panes are currently quite essential to how scli works and I don't think it's going to change.
I'm also in favor of keeping the configuration file in a flat hierarchy:
bind-messages-copy = y
bind-contacts-next = ctrl n, meta down
bind-input-newline = meta enter
bind-messages-next-pane = Tab, J
...
So it's in the form of bind-[pane]-[action]. This will also make every action remappable, scli can also read its initial bindings from this format. So no need for putting a mental effort for selecting which actions should be mappable or not.
I will look into implementing this, just noting here to not to duplicate the effort. Also please point out if I'm missing anything obvious.
Config line format
Currently scli does config parsing by looking for the same arguments that are supplied on the command line. So specifying the mapping in form bind-... for each action would mean adding all of those individual command-line options
scli --bind-contacts-next=... --bind-...=...
This would completely dominate the --help output :). A better way would be to have a single command that either reads a json dict, like --color currently does, or a single command that uses argparse's append action:
scli --bind messages_copy:y --bind=input_newline:'meta enter' ...
In config file this would look like:
bind = messages_copy:y
bind = input_newline : meta enter
Other solutions are JSON or INI, as above. With JSON it would be nice to be able put each binding on individual line:
key-bindings = {
"copy_message": "y",
"select_next_contact": ["ctrl n", "meta down"],
...
}
Still there's an annoyance of having to type those double quotes.. Multiline config parsing can be done easiest with configparser module.
I was also thinking of making some kind of equivalence between options that can take json values and the ini headers, like in my original syntax example.
Panes-based naming
To me it makes most sense to have a single 'binding' option for actions that do the same thing in all contexts (for j: "move the cursor down", whether in chat, contacts or msg info). It's conceivable that someone would want one key for moving down in contacts pane and another in chat view. Seems like an edge case, but if we want to, we could have optional "qualifiers", like
--bind cursor_down:j:chat
But most people would probably want to just do
--bind cursor_down:j
and not have to explicitly specify it two more times for other contexts.
Currently scli does config parsing by looking for the same arguments that are supplied on the command line. So specifying the mapping in form bind-... for each action would mean adding all of those individual command-line options
scli --bind-contacts-next=... --bind-...=...This would completely dominate the --help output :).
We can simply hide those away from the --help output and put a dummy switch that explains the overall idea:
--bind-[PANE]-[ACTION] KEY Binds ACTION in PANE to KEY. See ACTIONS below...
Or something along these lines. I'm also fine with append like approach but this will require more logic to be added into the code.
But most people would probably want to just do
--bind cursor_down:jand not have to explicitly specify it two more times for other contexts.
This is true for some keys like j, k and Tab (and they can be handled specially as they are quite unambiguous, something like --bind-global-down?), but for the rest of them, it's not really the case. Let's take o key as an example. It's meant to "open things". It opens the URL or attachment if message have one. So if we name this global action as open, then when someone re-maps it they may also expect it to open current contacts chat. To remove ambiguity it can be renamed into something else but then the problem of naming things appears.
So all commands does not need to be remappable but this introduces a complexity. Some keys will be handled differently and some keys will not. Implementation-wise, it's desirable to have them all implemented in same way. Having "qualifiers" will make implementation even more complex, or other questions like which commands deserves to be included or not etc.
Just to summarize, I'm not against any of the ideas you put out but I just prefer having a simpler implementation with the cost of losing a little bit of convenience. Considering now that scli is just below 4K lines (nothing much but a bit too much for a program like scli), I think it would be nicer to use simpler solutions.
We can simply hide those away from the --help output and put a dummy switch that explains the overall idea:
--bind-[PANE]-[ACTION] KEY
Yes, but I'm not sure why we would want to: seems like having a single command / option (--bind or whatever we name it) is more logical then adding a new argument to argparser for every action / key (--bind-action-1, --bind-action-2, ..).
I'm also fine with
appendlike approach but this will require more logic to be added into the code.
I bet it would actually be more convoluted to do this with "one argparse flag per action".
So if we name this global action as open, then when someone re-maps it they may also expect it to open current contacts chat.
Sure, naming an action just open is simply inviting ambiguity. Why not open_attach_or_url? And would the per-pane alternatives be any better?messages_open_attach_or_url, message_info_open_attach_or_url.. If we go with per-pane, but only messages_open or message_info_open that is just as ambiguous as naming it simply open, except now we have two of them :).
I'm not very fond of the idea of having to specify a key multiple times for the same action. Maybe one user in a thousand (assuming there are that many :) would want to have different keys for the same action in different contexts (like mentioned in prev comment with j). But if we do choose to accommodate them, it should not come at the expense of "regular" folks, who will just wonder why they have to specify a key for the same action twice (or thrice, or more).
To remove ambiguity it can be renamed into something else but then the problem of naming things appears.
I don't think there actually is a naming problem. In the OP I've mentioned a potential for there to be one; this was just me sounding out what to look out for when getting to implementing it. But now that I actually look at all the keypress actions in scli, I think we can give them all unambiguous and natural names that do not have to involve the context. We can still add it if we want to, of course, like input_newline.
EDIT: I've realized you must be referring to this part: "how to check that a new key binding does not conflict with another one?". Yes, that's still a potential issue. In the spirit of keeping things simple, we can decide not to worry about it in a "first iteration" solution. But in any case, this issue can be independent of having a natural naming scheme. We could add all keypress actions to the KEY_BINDINGS dict (see below) and explicitly check for conflicts.
So all commands does not need to be remappable but this introduces a complexity.
It can be as simple as adding a global KEY_BINDINGS dictionary. For the customizable actions/keys, we consult that dict instead of hard-coding the key. Currently, the keypress() logic in every widget looks like this:
...
if key == 'y':
<code to copy message>
elif key == ...:
...
Substituting if key == 'y': with if key == KEY_BINDINGS['copy_message']: is straightforward, and only requires the modifications to keys that we want to be customizable.
Having "qualifiers" will make implementation even more complex
Agreed! 99.9% of user will never need them. If we really want to though, this can be added, and without overwhelming complexity.
Just a side note: this is what, for example, newsboat does:
~/.newsboat/config
------------------
bind-key h quit
bind-key h quit articlelist
bind-key h quit article
(the last argument specifies the context).
I just prefer having a simpler implementation with the cost of losing a little bit of convenience
I'm with you! I think we can have both though :) In this case at least.
I've made a prototype implementation in #111.
It handles all the keys that people are likely to want to modify. More (or all) of the keys can be added if desired.
Nothing too complex there: parsing values of the --bind argument is just four lines of code.
One solution for checking for conflicts in key mapping is to specify the "scopes" / "contexts" attribute for each action. The "context" does not have to be a part of action's name; this frees us to pick any natural names we like.
The scopes can be based on "panes", plus a few other to cover the gaps: keypress events that do not fit in any of the "pane" contexts, e.g. a 'popup' scope for the MessageInfo or Help pop-ups. Despite my initial apprehensions, I don't think we need many of them. I've added a sample implementation (2d7fd74d08b73d8c9765d962308af3a595233cc1) to the key-bindings PR that makes do with just these scopes: contacts, chat, input, popup, other.
For this to work reliably, all keypress events need to be added to KEY_BINDINGS dict.
Is this issue still relevant? I'd be happy to take a whack at it!
Yes, there's a working prototype in #111. You are welcome to try it out, give feedback and implement additional key bindings.
I haven't had time to attend to scli in a while, but I should be able to soon enough, and this feature (way overdue) would be high on the list.