diff --git a/docs/hotkeys.md b/docs/hotkeys.md index 1c9e4bf528..f2819fe8c1 100644 --- a/docs/hotkeys.md +++ b/docs/hotkeys.md @@ -24,13 +24,13 @@ |Scroll up|PgUp / K| |Scroll down|PgDn / J| |Go to bottom / Last message|End / G| +|Trigger the selected entry|Enter / Space| |Narrow to all messages|a / Esc| |Narrow to all direct messages|P| |Narrow to all starred messages|f| |Narrow to messages in which you're mentioned|#| |Next unread topic|n| |Next unread direct message|p| -|Perform current action|Enter| ## Searching |Command|Key Combination| @@ -40,6 +40,7 @@ |Search streams|q| |Search topics in a stream|q| |Search emojis from emoji picker|p| +|Submit search and browse results|Enter| ## Message actions |Command|Key Combination| @@ -83,6 +84,7 @@ |Autocomplete @mentions, #stream_names, :emoji: and topics|Ctrl + f| |Cycle through autocomplete suggestions in reverse|Ctrl + r| |Narrow to compose box message recipient|Meta + .| +|Insert new line|Enter| ## Editor: Navigation |Command|Key Combination| diff --git a/tests/ui_tools/test_boxes.py b/tests/ui_tools/test_boxes.py index 8fc050391f..b6c763c0cb 100644 --- a/tests/ui_tools/test_boxes.py +++ b/tests/ui_tools/test_boxes.py @@ -1819,7 +1819,7 @@ def test_valid_char( @pytest.mark.parametrize( "log, expect_body_focus_set", [([], False), (["SOMETHING"], True)] ) - @pytest.mark.parametrize("enter_key", keys_for_command("ENTER")) + @pytest.mark.parametrize("enter_key", keys_for_command("EXECUTE_SEARCH")) def test_keypress_ENTER( self, panel_search_box: PanelSearchBox, diff --git a/tests/ui_tools/test_buttons.py b/tests/ui_tools/test_buttons.py index 3705bbaefe..adc2faa127 100644 --- a/tests/ui_tools/test_buttons.py +++ b/tests/ui_tools/test_buttons.py @@ -262,7 +262,7 @@ def test_keypress_TOGGLE_MUTE_STREAM( class TestUserButton: # FIXME Place this in a general test of a derived class? - @pytest.mark.parametrize("enter_key", keys_for_command("ENTER")) + @pytest.mark.parametrize("enter_key", keys_for_command("ACTIVATE_BUTTON")) def test_activate_called_once_on_keypress( self, mocker: MockerFixture, @@ -358,7 +358,7 @@ def test_init_calls_top_button( assert emoji_button.emoji_name == emoji_unit[0] assert emoji_button.reaction_count == count - @pytest.mark.parametrize("key", keys_for_command("ENTER")) + @pytest.mark.parametrize("key", keys_for_command("ACTIVATE_BUTTON")) @pytest.mark.parametrize( "emoji, has_user_reacted, is_selected_final, expected_reaction_count", [ diff --git a/tests/ui_tools/test_messages.py b/tests/ui_tools/test_messages.py index 8deb4ebf3c..08ba7d0661 100644 --- a/tests/ui_tools/test_messages.py +++ b/tests/ui_tools/test_messages.py @@ -7,7 +7,7 @@ from pytest import param as case from urwid import Columns, Divider, Padding, Text -from zulipterminal.config.keys import keys_for_command +from zulipterminal.config.keys import keys_for_command, primary_key_for_command from zulipterminal.config.symbols import ( ALL_MESSAGES_MARKER, DIRECT_MESSAGE_MARKER, @@ -1961,11 +1961,14 @@ def test_footlinks_limit(self, maximum_footlinks, expected_instance): assert isinstance(footlinks, expected_instance) @pytest.mark.parametrize( - "key", keys_for_command("ENTER"), ids=lambda param: f"left_click-key:{param}" + "key", + keys_for_command("ACTIVATE_BUTTON"), + ids=lambda param: f"left_click-key:{param}", ) def test_mouse_event_left_click( self, mocker, msg_box, key, widget_size, compose_box_is_open ): + expected_keypress = primary_key_for_command("ACTIVATE_BUTTON") size = widget_size(msg_box) col = 1 row = 1 @@ -1979,4 +1982,4 @@ def test_mouse_event_left_click( if compose_box_is_open: msg_box.keypress.assert_not_called() else: - msg_box.keypress.assert_called_once_with(size, key) + msg_box.keypress.assert_called_once_with(size, expected_keypress) diff --git a/tests/ui_tools/test_popups.py b/tests/ui_tools/test_popups.py index cb26395896..c4f55a1bb3 100644 --- a/tests/ui_tools/test_popups.py +++ b/tests/ui_tools/test_popups.py @@ -830,7 +830,7 @@ def test_init(self, edit_mode_view: EditModeView) -> None: (2, "change_all"), ], ) - @pytest.mark.parametrize("key", keys_for_command("ENTER")) + @pytest.mark.parametrize("key", keys_for_command("ACTIVATE_BUTTON")) def test_select_edit_mode( self, edit_mode_view: EditModeView, @@ -1523,7 +1523,7 @@ def test_keypress_exit_popup( self.stream_info_view.keypress(size, key) assert self.controller.exit_popup.called - @pytest.mark.parametrize("key", (*keys_for_command("ENTER"), " ")) + @pytest.mark.parametrize("key", (*keys_for_command("ACTIVATE_BUTTON"), " ")) def test_checkbox_toggle_mute_stream( self, key: str, widget_size: Callable[[Widget], urwid_Size] ) -> None: @@ -1536,7 +1536,7 @@ def test_checkbox_toggle_mute_stream( toggle_mute_status.assert_called_once_with(stream_id) - @pytest.mark.parametrize("key", (*keys_for_command("ENTER"), " ")) + @pytest.mark.parametrize("key", (*keys_for_command("ACTIVATE_BUTTON"), " ")) def test_checkbox_toggle_pin_stream( self, key: str, widget_size: Callable[[Widget], urwid_Size] ) -> None: @@ -1549,7 +1549,7 @@ def test_checkbox_toggle_pin_stream( toggle_pin_status.assert_called_once_with(stream_id) - @pytest.mark.parametrize("key", (*keys_for_command("ENTER"), " ")) + @pytest.mark.parametrize("key", (*keys_for_command("ACTIVATE_BUTTON"), " ")) def test_checkbox_toggle_visual_notification( self, key: str, widget_size: Callable[[Widget], urwid_Size] ) -> None: diff --git a/zulipterminal/config/keys.py b/zulipterminal/config/keys.py index 473d19c0f9..89e25355fa 100644 --- a/zulipterminal/config/keys.py +++ b/zulipterminal/config/keys.py @@ -6,6 +6,7 @@ from typing_extensions import NotRequired, TypedDict from urwid.command_map import ( + ACTIVATE, CURSOR_DOWN, CURSOR_LEFT, CURSOR_MAX_RIGHT, @@ -91,6 +92,11 @@ class KeyBinding(TypedDict): 'help_text': 'Go to bottom / Last message', 'key_category': 'navigation', }, + 'ACTIVATE_BUTTON': { + 'keys': ['enter', ' '], + 'help_text': 'Trigger the selected entry', + 'key_category': 'navigation', + }, 'REPLY_MESSAGE': { 'keys': ['r', 'enter'], 'help_text': 'Reply to the current message', @@ -244,16 +250,16 @@ class KeyBinding(TypedDict): 'excluded_from_random_tips': True, 'key_category': 'searching', }, + 'EXECUTE_SEARCH': { + 'keys': ['enter'], + 'help_text': 'Submit search and browse results', + 'key_category': 'searching', + }, 'TOGGLE_MUTE_STREAM': { 'keys': ['m'], 'help_text': 'Mute/unmute streams', 'key_category': 'stream_list', }, - 'ENTER': { - 'keys': ['enter'], - 'help_text': 'Perform current action', - 'key_category': 'navigation', - }, 'THUMBS_UP': { 'keys': ['+'], 'help_text': 'Toggle thumbs-up reaction to the current message', @@ -400,6 +406,14 @@ class KeyBinding(TypedDict): 'help_text': 'Swap with previous character', 'key_category': 'editor_text_manipulation', }, + 'NEW_LINE': { + # urwid_readline's command + # This obvious hotkey is added to clarify against 'enter' to send + # and to differentiate from other hotkeys using 'enter'. + 'keys': ['enter'], + 'help_text': 'Insert new line', + 'key_category': 'msg_compose', + }, 'FULL_RENDERED_MESSAGE': { 'keys': ['f'], 'help_text': 'Show/hide full rendered message (from message information)', @@ -432,6 +446,7 @@ class KeyBinding(TypedDict): "SCROLL_UP": CURSOR_PAGE_UP, "SCROLL_DOWN": CURSOR_PAGE_DOWN, "GO_TO_BOTTOM": CURSOR_MAX_RIGHT, + "ACTIVATE_BUTTON": ACTIVATE, } @@ -477,6 +492,9 @@ def display_key_for_urwid_key(urwid_key: str) -> str: """ Returns a displayable user-centric format of the urwid key. """ + if urwid_key == " ": + return "Space" + for urwid_map_key, display_map_key in URWID_KEY_TO_DISPLAY_KEY_MAPPING.items(): if urwid_map_key in urwid_key: urwid_key = urwid_key.replace(urwid_map_key, display_map_key) diff --git a/zulipterminal/ui_tools/boxes.py b/zulipterminal/ui_tools/boxes.py index f6d3294241..91e2f80966 100644 --- a/zulipterminal/ui_tools/boxes.py +++ b/zulipterminal/ui_tools/boxes.py @@ -947,14 +947,14 @@ def main_view(self) -> Any: def keypress(self, size: urwid_Size, key: str) -> Optional[str]: if ( - is_command_key("ENTER", key) and self.text_box.edit_text == "" + is_command_key("EXECUTE_SEARCH", key) and self.text_box.edit_text == "" ) or is_command_key("GO_BACK", key): self.text_box.set_edit_text("") self.controller.exit_editor_mode() self.controller.view.middle_column.set_focus("body") return key - elif is_command_key("ENTER", key): + elif is_command_key("EXECUTE_SEARCH", key): self.controller.exit_editor_mode() self.controller.search_messages(self.text_box.edit_text) self.controller.view.middle_column.set_focus("body") @@ -1003,7 +1003,7 @@ def valid_char(self, ch: str) -> bool: def keypress(self, size: urwid_Size, key: str) -> Optional[str]: if ( - is_command_key("ENTER", key) and self.get_edit_text() == "" + is_command_key("EXECUTE_SEARCH", key) and self.get_edit_text() == "" ) or is_command_key("GO_BACK", key): self.panel_view.view.controller.exit_editor_mode() self.reset_search_text() @@ -1011,7 +1011,7 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: # Don't call 'Esc' when inside a popup search-box. if not self.panel_view.view.controller.is_any_popup_open(): self.panel_view.keypress(size, primary_key_for_command("GO_BACK")) - elif is_command_key("ENTER", key) and not self.panel_view.empty_search: + elif is_command_key("EXECUTE_SEARCH", key) and not self.panel_view.empty_search: self.panel_view.view.controller.exit_editor_mode() self.set_caption([("filter_results", " Search Results "), " "]) self.panel_view.set_focus("body") diff --git a/zulipterminal/ui_tools/buttons.py b/zulipterminal/ui_tools/buttons.py index 9ecb2d32e0..91c428a2b7 100644 --- a/zulipterminal/ui_tools/buttons.py +++ b/zulipterminal/ui_tools/buttons.py @@ -120,7 +120,7 @@ def activate(self, key: Any) -> None: self.show_function() def keypress(self, size: urwid_Size, key: str) -> Optional[str]: - if is_command_key("ENTER", key): + if is_command_key("ACTIVATE_BUTTON", key): self.activate(key) return None else: # This is in the else clause, to avoid multiple activation @@ -417,7 +417,7 @@ def mouse_event( self, size: urwid_Size, event: str, button: int, col: int, row: int, focus: int ) -> bool: if event == "mouse press" and button == 1: - self.keypress(size, primary_key_for_command("ENTER")) + self.keypress(size, primary_key_for_command("ACTIVATE_BUTTON")) return True return super().mouse_event(size, event, button, col, row, focus) diff --git a/zulipterminal/ui_tools/messages.py b/zulipterminal/ui_tools/messages.py index 3ddfa10a70..b8552fdc92 100644 --- a/zulipterminal/ui_tools/messages.py +++ b/zulipterminal/ui_tools/messages.py @@ -898,7 +898,7 @@ def mouse_event( if event == "mouse press" and button == 1: if self.model.controller.is_in_editor_mode(): return True - self.keypress(size, primary_key_for_command("ENTER")) + self.keypress(size, primary_key_for_command("ACTIVATE_BUTTON")) return True return super().mouse_event(size, event, button, col, row, focus) diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 30fcc42a49..85dad25b79 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -1740,7 +1740,11 @@ def __init__(self, controller: Any, button: Any) -> None: for mode in EDIT_MODE_CAPTIONS: self.add_radio_button(mode) super().__init__( - controller, self.widgets, "ENTER", 62, "Topic edit propagation mode" + controller, + self.widgets, + "ACTIVATE_BUTTON", + 62, + "Topic edit propagation mode", ) # Set cursor to marked checkbox. for i in range(len(self.widgets)):