Skip to content
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 #1447

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ notify=disabled

## Color-depth: set to one of 1 (for monochrome), 16, 256, or 24bit
color-depth=256

## Custom Keybindings: keybindings can be customized for user preferences (see elsewhere for default)
custom_hotkeys=GO_UP:G, GO_DOWN:B
```

> **NOTE:** Most of these configuration settings may be specified on the
Expand Down
80 changes: 80 additions & 0 deletions tests/config/test_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
custom_keybindings = {"GO_UP": "y"}
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",
},
}

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

assert test_key_bindings["GO_UP"]["keys"] == ["y"]


def test_override_keybindings_invalid_command(mocker: MockerFixture) -> None:
custom_keybindings = {"INVALID_COMMAND": "x"}
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",
},
}
with pytest.raises(keys.InvalidCommand):
keys.override_keybindings(custom_keybindings, test_key_bindings)


def test_override_keybindings_conflict(mocker: MockerFixture) -> None:
custom_keybindings = {"GO_UP": "j"} # 'j' is originally for GO_DOWN
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",
},
}
keys.override_keybindings(custom_keybindings, test_key_bindings)
assert "j" in test_key_bindings["GO_DOWN"]["keys"] # unchanged
assert test_key_bindings["GO_UP"]["keys"] != ["j"] # not updated due to conflict


def test_override_multiple_keybindings_valid(mocker: MockerFixture) -> None:
custom_keybindings = {"GO_UP": "y", "GO_DOWN": "b"} # 'y' and 'b' are unused
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",
},
}
keys.override_keybindings(custom_keybindings, test_key_bindings)
assert test_key_bindings["GO_UP"]["keys"] == ["y"]
assert test_key_bindings["GO_DOWN"]["keys"] == ["b"]
9 changes: 9 additions & 0 deletions zulipterminal/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from urwid import display_common, set_encoding

from zulipterminal.api_types import ServerSettings
from zulipterminal.config.keys import KEY_BINDINGS, override_keybindings
from zulipterminal.config.themes import (
ThemeError,
aliased_themes,
Expand Down Expand Up @@ -554,6 +555,14 @@ 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:
custom_keybindings_str = zterm["custom_keybindings"].value
_, key_value_pairs = custom_keybindings_str.split("=")
# Split each pair and convert to a dictionary
custom_keybindings = dict(
pair.split(":") for pair in key_value_pairs.split(", ")
)
override_keybindings(custom_keybindings, KEY_BINDINGS)
### Generate data not output to user, but into Controller
# Generate urwid palette
color_depth_str = zterm["color-depth"].value
Expand Down
43 changes: 43 additions & 0 deletions zulipterminal/config/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,49 @@ def commands_for_random_tips() -> List[KeyBinding]:
]


def override_keybindings(
custom_keybindings: Dict[str, str], existing_keybindings: Dict[str, KeyBinding]
) -> None:
reverse_key_map = {
key: cmd
for cmd, binding in existing_keybindings.items()
for key in binding["keys"]
}
requested_changes = {}
conflicts = {}

# 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")

current_keys = existing_keybindings[command]["keys"]
if new_key not in current_keys:
requested_changes[command] = new_key
if new_key in reverse_key_map and reverse_key_map[new_key] != command:
conflicting_cmd = reverse_key_map[new_key]
conflicts[command] = conflicting_cmd

# Resolve direct swaps
for command, new_key in custom_keybindings.items():
if command in conflicts:
conflicting_cmd = conflicts[command]
if (
conflicting_cmd in custom_keybindings
and custom_keybindings[conflicting_cmd] in current_keys
):
del conflicts[command]
del conflicts[conflicting_cmd]

if conflicts:
# Handle unresolved conflicts, e.g., by warning the user
return

# Apply changes
for command, new_key in requested_changes.items():
existing_keybindings[command]["keys"] = [new_key]


# Refer urwid/command_map.py
# Adds alternate keys for standard urwid navigational commands.
for zt_cmd, urwid_cmd in ZT_TO_URWID_CMD_MAPPING.items():
Expand Down
Loading