From 84f50145871bef821153b8aa1b1d29ef45e3764e Mon Sep 17 00:00:00 2001 From: Niloth-p <20315308+Niloth-p@users.noreply.github.com> Date: Tue, 14 May 2024 19:57:32 +0530 Subject: [PATCH 1/4] keys: Add a common suffix for all urwid_readline hotkeys. Tests updated to not use urwid_readline hotkeys, to avoid reliance on external libraries for hotkey commands. --- tests/config/test_keys.py | 2 +- zulipterminal/config/keys.py | 35 +++++++++++++++++++---------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/tests/config/test_keys.py b/tests/config/test_keys.py index 752e69af29..994050ea92 100644 --- a/tests/config/test_keys.py +++ b/tests/config/test_keys.py @@ -142,7 +142,7 @@ def test_display_key_for_urwid_key(urwid_key: str, display_key: str) -> None: COMMAND_TO_DISPLAY_KEYS = [ - ("NEXT_LINE", ["Down", "Ctrl n"]), + ("SEND_MESSAGE", ["Ctrl d", "Meta Enter"]), ("TOGGLE_STAR_STATUS", ["Ctrl s", "*"]), ("ALL_PM", ["P"]), ] diff --git a/zulipterminal/config/keys.py b/zulipterminal/config/keys.py index 473d19c0f9..8bc5df981b 100644 --- a/zulipterminal/config/keys.py +++ b/zulipterminal/config/keys.py @@ -17,6 +17,9 @@ ) +READLINE_SUFFIX = "_READLINE" + + class KeyBinding(TypedDict): keys: List[str] help_text: str @@ -320,82 +323,82 @@ class KeyBinding(TypedDict): 'help_text': 'Show/hide user information (from users list)', 'key_category': 'general', }, - 'BEGINNING_OF_LINE': { + 'BEGINNING_OF_LINE' + READLINE_SUFFIX: { 'keys': ['ctrl a', 'home'], 'help_text': 'Start of line', 'key_category': 'editor_navigation', }, - 'END_OF_LINE': { + 'END_OF_LINE' + READLINE_SUFFIX: { 'keys': ['ctrl e', 'end'], 'help_text': 'End of line', 'key_category': 'editor_navigation', }, - 'ONE_WORD_BACKWARD': { + 'ONE_WORD_BACKWARD' + READLINE_SUFFIX: { 'keys': ['meta b', 'shift left'], 'help_text': 'Start of current or previous word', 'key_category': 'editor_navigation', }, - 'ONE_WORD_FORWARD': { + 'ONE_WORD_FORWARD' + READLINE_SUFFIX: { 'keys': ['meta f', 'shift right'], 'help_text': 'Start of next word', 'key_category': 'editor_navigation', }, - 'PREV_LINE': { + 'PREV_LINE' + READLINE_SUFFIX: { 'keys': ['up', 'ctrl p'], 'help_text': 'Previous line', 'key_category': 'editor_navigation', }, - 'NEXT_LINE': { + 'NEXT_LINE' + READLINE_SUFFIX: { 'keys': ['down', 'ctrl n'], 'help_text': 'Next line', 'key_category': 'editor_navigation', }, - 'UNDO_LAST_ACTION': { + 'UNDO_LAST_ACTION' + READLINE_SUFFIX: { 'keys': ['ctrl _'], 'help_text': 'Undo last action', 'key_category': 'editor_text_manipulation', }, - 'CLEAR_MESSAGE': { + 'CLEAR_MESSAGE' + READLINE_SUFFIX: { 'keys': ['ctrl l'], 'help_text': 'Clear text box', 'key_category': 'editor_text_manipulation', }, - 'CUT_TO_END_OF_LINE': { + 'CUT_TO_END_OF_LINE' + READLINE_SUFFIX: { 'keys': ['ctrl k'], 'help_text': 'Cut forwards to the end of the line', 'key_category': 'editor_text_manipulation', }, - 'CUT_TO_START_OF_LINE': { + 'CUT_TO_START_OF_LINE' + READLINE_SUFFIX: { 'keys': ['ctrl u'], 'help_text': 'Cut backwards to the start of the line', 'key_category': 'editor_text_manipulation', }, - 'CUT_TO_END_OF_WORD': { + 'CUT_TO_END_OF_WORD' + READLINE_SUFFIX: { 'keys': ['meta d'], 'help_text': 'Cut forwards to the end of the current word', 'key_category': 'editor_text_manipulation', }, - 'CUT_TO_START_OF_WORD': { + 'CUT_TO_START_OF_WORD' + READLINE_SUFFIX: { 'keys': ['ctrl w', 'meta backspace'], 'help_text': 'Cut backwards to the start of the current word', 'key_category': 'editor_text_manipulation', }, - 'CUT_WHOLE_LINE': { + 'CUT_WHOLE_LINE' + READLINE_SUFFIX: { 'keys': ['meta x'], 'help_text': 'Cut the current line', 'key_category': 'editor_text_manipulation', }, - 'PASTE_LAST_CUT': { + 'PASTE_LAST_CUT' + READLINE_SUFFIX: { 'keys': ['ctrl y'], 'help_text': 'Paste last cut section', 'key_category': 'editor_text_manipulation', }, - 'DELETE_LAST_CHARACTER': { + 'DELETE_LAST_CHARACTER' + READLINE_SUFFIX: { 'keys': ['ctrl h'], 'help_text': 'Delete previous character', 'key_category': 'editor_text_manipulation', }, - 'TRANSPOSE_CHARACTERS': { + 'TRANSPOSE_CHARACTERS' + READLINE_SUFFIX: { 'keys': ['ctrl t'], 'help_text': 'Swap with previous character', 'key_category': 'editor_text_manipulation', From 514db27fdb7260061f59629dc114962d9be809c0 Mon Sep 17 00:00:00 2001 From: Niloth-p <20315308+Niloth-p@users.noreply.github.com> Date: Sun, 12 May 2024 20:55:56 +0530 Subject: [PATCH 2/4] lint/regex: keys: Add linting for usage of external commands. Identify external commands using the added suffix. Search for both the suffix variable and the actual suffix. --- tools/lint-hotkeys | 63 ++++++++++++++++++++++++++++++--- zulipterminal/config/regexes.py | 7 ++++ 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/tools/lint-hotkeys b/tools/lint-hotkeys index 503a024e98..e119443912 100755 --- a/tools/lint-hotkeys +++ b/tools/lint-hotkeys @@ -11,13 +11,18 @@ from zulipterminal.config.keys import ( KEY_BINDINGS, display_keys_for_command, ) +from zulipterminal.config.regexes import REGEX_READLINE_COMMANDS -KEYS_FILE = ( - Path(__file__).resolve().parent.parent / "zulipterminal" / "config" / "keys.py" -) +# absolute path to zulip-terminal +ROOT_DIRECTORY = Path(__file__).resolve().parent.parent + +# absolute path to zulip-terminal/zulipterminal to be passed as parameter +ZULIPTERMINAL = ROOT_DIRECTORY / "zulipterminal" + +KEYS_FILE = ZULIPTERMINAL / "config" / "keys.py" KEYS_FILE_NAME = KEYS_FILE.name -OUTPUT_FILE = Path(__file__).resolve().parent.parent / "docs" / "hotkeys.md" +OUTPUT_FILE = ROOT_DIRECTORY / "docs" / "hotkeys.md" OUTPUT_FILE_NAME = OUTPUT_FILE.name SCRIPT_NAME = PurePath(__file__).name HELP_TEXT_STYLE = re.compile(r"^[a-zA-Z /()',&@#:_-]*$") @@ -25,14 +30,64 @@ HELP_TEXT_STYLE = re.compile(r"^[a-zA-Z /()',&@#:_-]*$") # Exclude keys from duplicate keys checking KEYS_TO_EXCLUDE = ["q", "e", "m", "r"] +# Exclude files from being checked for external command usage +EXCLUDED_FILES = [ + KEYS_FILE, + ZULIPTERMINAL / "config" / "regexes.py", +] + def main(fix: bool) -> None: + lint_all_external_commands() if fix: generate_hotkeys_file() else: lint_hotkeys_file() +def lint_all_external_commands() -> None: + lint_external_commands_by_type( + regex_pattern=REGEX_READLINE_COMMANDS, + command_type="Urwid Readline", + suffix="READLINE_SUFFIX", + ) + print("All external commands have been linted successfully.") + + +def lint_external_commands_by_type( + regex_pattern: str, command_type: str, suffix: str +) -> None: + """ + Lint src directory for the usage of external commands + in the codebase by checking for their regex + """ + error_flag = 0 + for file_path in ZULIPTERMINAL.rglob("*.py"): + if file_path in EXCLUDED_FILES: + continue + with file_path.open() as f: + contents = f.read() + regex_matches = re.finditer(regex_pattern, contents) + suffix_matches = re.finditer(suffix, contents) + count_matches = sum(1 for _ in regex_matches) + sum( + 1 for _ in suffix_matches + ) + if count_matches > 0: + print( + f"{file_path.name} contains {count_matches} mentions of {command_type} commands." + ) + error_flag = 1 + if error_flag == 1: + print( + f"{command_type} commands are not intended for direct use or modification." + ) + print( + f"Please refer to {KEYS_FILE_NAME} for identifying the {command_type} commands." + ) + print("Rerun this command after removing the usage of external commands.") + sys.exit(error_flag) + + def lint_hotkeys_file() -> None: """ Lint KEYS_FILE for key description, then compare if in sync with diff --git a/zulipterminal/config/regexes.py b/zulipterminal/config/regexes.py index ae9f2c121b..c3fe8e27f7 100644 --- a/zulipterminal/config/regexes.py +++ b/zulipterminal/config/regexes.py @@ -7,6 +7,9 @@ # (*) Stream and topic regexes +from zulipterminal.config.keys import READLINE_SUFFIX + + REGEX_STREAM_NAME = r"([^*>]+)" REGEX_TOPIC_NAME = r"([^*]*)" @@ -46,3 +49,7 @@ # Example: 6-test-stream REGEX_INTERNAL_LINK_STREAM_ID = r"^[0-9]+-" + + +# Example: UNDO_LAST_ACTION_READLINE +REGEX_READLINE_COMMANDS = rf"([A-Z_]+{READLINE_SUFFIX})" From fa925114562eeaeff7c7e0064bcbbd3b15acf772 Mon Sep 17 00:00:00 2001 From: Niloth-p <20315308+Niloth-p@users.noreply.github.com> Date: Sun, 12 May 2024 21:47:31 +0530 Subject: [PATCH 3/4] run: Check sync of readline shortcuts with urwid_readline's keymap. Lint exclusions updated. --- tools/lint-hotkeys | 1 + zulipterminal/cli/run.py | 41 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/tools/lint-hotkeys b/tools/lint-hotkeys index e119443912..969ff228ac 100755 --- a/tools/lint-hotkeys +++ b/tools/lint-hotkeys @@ -34,6 +34,7 @@ KEYS_TO_EXCLUDE = ["q", "e", "m", "r"] EXCLUDED_FILES = [ KEYS_FILE, ZULIPTERMINAL / "config" / "regexes.py", + ZULIPTERMINAL / "cli" / "run.py", ] diff --git a/zulipterminal/cli/run.py b/zulipterminal/cli/run.py index 46d13e8992..6c5223d893 100755 --- a/zulipterminal/cli/run.py +++ b/zulipterminal/cli/run.py @@ -15,8 +15,10 @@ import requests from urwid import display_common, set_encoding +from urwid_readline import ReadlineEdit from zulipterminal.api_types import ServerSettings +from zulipterminal.config.keys import KEY_BINDINGS, READLINE_SUFFIX from zulipterminal.config.themes import ( ThemeError, aliased_themes, @@ -392,6 +394,34 @@ def list_themes() -> str: ) +class ReadlineShortcutError(Exception): + pass + + +def check_readline_shortcuts_availability() -> None: + readline_edit = ReadlineEdit() + commands_to_exclude = ["PREV_LINE" + READLINE_SUFFIX, "NEXT_LINE" + READLINE_SUFFIX] + filtered_commands = [ + command + for command in KEY_BINDINGS + if command.endswith(READLINE_SUFFIX) and command not in commands_to_exclude + ] + + missing_keys = [] + for command in filtered_commands: + for key in KEY_BINDINGS[command]["keys"]: + if key not in readline_edit.keymap: + key_missing_error = ( + f'Key "{key}" for command "{KEY_BINDINGS[command]["help_text"]}" ' + f"is not found." + ) + missing_keys.append(key_missing_error) + + if missing_keys: + error_message = "\n".join(missing_keys) + raise ReadlineShortcutError(error_message) + + def main(options: Optional[List[str]] = None) -> None: """ Launch Zulip Terminal. @@ -566,6 +596,17 @@ def print_setting(setting: str, data: SettingData, suffix: str = "") -> None: for setting, valid_boolean_values in VALID_BOOLEAN_SETTINGS.items(): boolean_settings[setting] = zterm[setting].value == valid_boolean_values[0] + try: + check_readline_shortcuts_availability() + except ReadlineShortcutError as e: + print( + "\nThe following readline shortcuts " + + "are missing in urwid_readline's keymap.\n" + + str(e) + + "\n", + file=sys.stderr, + ) + Controller( config_file=zuliprc_path, maximum_footlinks=maximum_footlinks, From 0495e29dda761f8e2e3cd7f305f18dec4afcf406 Mon Sep 17 00:00:00 2001 From: Niloth-p <20315308+Niloth-p@users.noreply.github.com> Date: Sun, 12 May 2024 21:23:58 +0530 Subject: [PATCH 4/4] keys/lint/regexes: Add suffix & linting to general terminal commands. --- tools/lint-hotkeys | 10 +++++++++- zulipterminal/config/keys.py | 5 +++-- zulipterminal/config/regexes.py | 4 +++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tools/lint-hotkeys b/tools/lint-hotkeys index 969ff228ac..f127d2ee35 100755 --- a/tools/lint-hotkeys +++ b/tools/lint-hotkeys @@ -11,7 +11,10 @@ from zulipterminal.config.keys import ( KEY_BINDINGS, display_keys_for_command, ) -from zulipterminal.config.regexes import REGEX_READLINE_COMMANDS +from zulipterminal.config.regexes import ( + REGEX_READLINE_COMMANDS, + REGEX_TERMINAL_COMMANDS, +) # absolute path to zulip-terminal @@ -52,6 +55,11 @@ def lint_all_external_commands() -> None: command_type="Urwid Readline", suffix="READLINE_SUFFIX", ) + lint_external_commands_by_type( + regex_pattern=REGEX_TERMINAL_COMMANDS, + command_type="General terminal", + suffix="GENERAL_TERMINAL_SUFFIX", + ) print("All external commands have been linted successfully.") diff --git a/zulipterminal/config/keys.py b/zulipterminal/config/keys.py index 8bc5df981b..c807664efb 100644 --- a/zulipterminal/config/keys.py +++ b/zulipterminal/config/keys.py @@ -18,6 +18,7 @@ READLINE_SUFFIX = "_READLINE" +GENERAL_TERMINAL_SUFFIX = "_GENERAL_TERMINAL" class KeyBinding(TypedDict): @@ -308,12 +309,12 @@ class KeyBinding(TypedDict): 'excluded_from_random_tips': True, 'key_category': 'stream_list', }, - 'REDRAW': { + 'REDRAW' + GENERAL_TERMINAL_SUFFIX: { 'keys': ['ctrl l'], 'help_text': 'Redraw screen', 'key_category': 'general', }, - 'QUIT': { + 'QUIT' + GENERAL_TERMINAL_SUFFIX: { 'keys': ['ctrl c'], 'help_text': 'Quit', 'key_category': 'general', diff --git a/zulipterminal/config/regexes.py b/zulipterminal/config/regexes.py index c3fe8e27f7..26f0e15bf1 100644 --- a/zulipterminal/config/regexes.py +++ b/zulipterminal/config/regexes.py @@ -7,7 +7,7 @@ # (*) Stream and topic regexes -from zulipterminal.config.keys import READLINE_SUFFIX +from zulipterminal.config.keys import GENERAL_TERMINAL_SUFFIX, READLINE_SUFFIX REGEX_STREAM_NAME = r"([^*>]+)" @@ -53,3 +53,5 @@ # Example: UNDO_LAST_ACTION_READLINE REGEX_READLINE_COMMANDS = rf"([A-Z_]+{READLINE_SUFFIX})" +# Example: REDRAW_GENERAL_TERMINAL +REGEX_TERMINAL_COMMANDS = rf"^.*([A-Z_]+{GENERAL_TERMINAL_SUFFIX})"