New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added custom override of hotkeys based on zuliprc file #1446
base: main
Are you sure you want to change the base?
Conversation
@jamesdchu This looks like you're making progress towards this - are you looking for feedback yet? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jamesdchu Thanks for working on this 👍 I left the question re if you were asking for feedback in December, but only recently noticed you'd changed it from a draft to a regular PR before my comment, so here's some feedback in case you had intended that to be the case already :)
You currently have this as a lot of commits, so I've not left comments on individual commits, but rather the total set of changes. See the contributing notes in the README regarding how Zulip in general, and this repo in particular, structure commits and why. This is one reason why the PR is failing the 'isolated commits' phase of the automatic tests (CI).
I gave this a manual test and after adjusting a line in run.py I could get it to run OK, as per one comment inline. That comment discusses adding tests for that run.py part, which suggests one way to separate into two parts (commits) - implementing the functionality, and then integrating it into run.py, each commit with tests (and passing linting). The documentation part could also be separate, particularly since a FAQ entry could be useful too. Beyond that, you could also break the functionality part into stages, with tests added/adjusted as the functionality improves, but we can discuss that later if you're interested :)
## Custom Keybindings: keybindings can be customized for user preferences (see elsewhere for default) | ||
custom_hotkeys=GO_UP:G, GO_DOWN:B |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may be clearer here to have a second line ##
line indicating specifically where to find the defaults, and also the symbols themselves.
We likely also want the custom_hotkeys
line to be commented out by default, since this represents a departure from the default configuration - based on the introduction to this section.
@@ -554,6 +555,15 @@ def print_setting(setting: str, data: SettingData, suffix: str = "") -> None: | |||
print_setting("color depth setting", zterm["color-depth"]) | |||
print_setting("notify setting", zterm["notify"]) | |||
|
|||
if "custom_keybindings" in zterm: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we have this section earlier? The loading (and validation) of config is generally above this point.
@@ -112,3 +112,83 @@ def test_updated_urwid_command_map() -> None: | |||
assert key in keys.keys_for_command(zt_cmd) | |||
except KeyError: | |||
pass | |||
|
|||
|
|||
def test_override_keybindings_valid(mocker: MockerFixture) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I'd suggest using a double underscore to separate the function name from the variation you're testing. We possibly don't do this everywhere yet, but it makes it easier to distinguish the two parts :)
custom_keybindings_str = zterm["custom_keybindings"].value | ||
_, key_value_pairs = custom_keybindings_str.split("=") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't work correctly - maybe you had an older version here?
I tested manually by working around this.
You have tests for the function that generates the overrides, but not for this stage. We have a test file for this, so it would be useful to see some for this new code too.
# Split each pair and convert to a dictionary | ||
custom_keybindings = dict( | ||
pair.split(":") for pair in key_value_pairs.split(", ") | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good first version, but it'd be useful to see a little error handling (or more flexibility) here, since this crashes if the configuration is laid out incorrectly - such as being ,
-separated, instead of with a space too.
I would understand more if multiple keys could be provided, since you might be separating those without a space, but that seems not to be available at this stage? Of course it's not a problem to skip multiple keys with an initial version :)
|
||
# Mock the KEY_BINDINGS to use the test copy | ||
mocker.patch.object(keys, "KEY_BINDINGS", test_key_bindings) | ||
|
||
# Now this will modify the test copy, not the original | ||
keys.override_keybindings(custom_keybindings, test_key_bindings) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this mocking necessary?
test_key_bindings: Dict[str, keys.KeyBinding] = { | ||
"GO_UP": { | ||
"keys": ["up", "k"], | ||
"help_text": "Go up / Previous message", | ||
"key_category": "navigation", | ||
}, | ||
"GO_DOWN": { | ||
"keys": ["down", "j"], | ||
"help_text": "Go down / Next message", | ||
"key_category": "navigation", | ||
}, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like the same starting dict is used in all these tests and copy/pasted. Putting that into a fixture local to this file would reduce the duplication.
def override_keybindings( | ||
custom_keybindings: Dict[str, str], existing_keybindings: Dict[str, KeyBinding] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might read clearer with the existing_keybindings first, and the custom ones named slightly differently - they are a different type.
# Resolve direct swaps | ||
for command, new_key in custom_keybindings.items(): | ||
if command in conflicts: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tracking possible conflicts is a good idea, and a direct swap should likely always avoid a problem.
However, the problem is more complex. Note that we already have apparent conflicts depending on different scopes. Our lint-hotkeys
script works around some, but there are others (eg. i
). These are not a problem in practice due to scoping, but this is not sufficiently encoded in the keybindings data right now.
# Collect requested changes and detect conflicts | ||
for command, new_key in custom_keybindings.items(): | ||
if command not in existing_keybindings: | ||
raise InvalidCommand(f"Invalid command {command} in custom keybindings") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Errors from this method should be handled and cleanly expressed to the user in run.py, not just left to escape unhandled.
Heads up @jamesdchu, we just merged some commits that conflict with the changes you made in this pull request! You can review this repository's recent commits to see where the conflicts occur. Please rebase your feature branch against the |
What does this PR do, and why?
This PR edits the keys.py file to enable custom hotkey mapping. This means that users can change the zuliprc file to get their desired hotkey mappings.
External discussion & connections
topic
How did you test this?