From 2177e2246ce5aed0df82073a12db6e89015084f9 Mon Sep 17 00:00:00 2001 From: alad Date: Tue, 17 Mar 2026 11:36:26 -0300 Subject: [PATCH 1/6] refactor: extract key binding actions --- mycli/key_binding_actions.py | 60 +++++++++++++++++ mycli/key_bindings.py | 123 +++++++++++------------------------ 2 files changed, 97 insertions(+), 86 deletions(-) create mode 100644 mycli/key_binding_actions.py diff --git a/mycli/key_binding_actions.py b/mycli/key_binding_actions.py new file mode 100644 index 00000000..afdd0853 --- /dev/null +++ b/mycli/key_binding_actions.py @@ -0,0 +1,60 @@ +from __future__ import annotations +import logging +import webbrowser +from typing import Any +import prompt_toolkit +from prompt_toolkit.application.current import get_app +from prompt_toolkit.enums import EditingMode +from prompt_toolkit.key_binding.key_processor import KeyPressEvent +from mycli.constants import DOCS_URL +from mycli.packages.toolkit.utils import safe_invalidate_display + +_logger = logging.getLogger(__name__) + + +class KeyBindingActions: + def __init__(self, mycli: Any) -> None: + self._mycli = mycli + + @staticmethod + def _print_docs_help() -> None: + app = get_app() + app.print_text('\n') + app.print_text([ + ('', 'Inline help — type "'), + ('bold', 'help'), + ('', '" or "'), + ('bold', r'\?'), + ('', '"\n'), + ]) + app.print_text([ + ('', 'Docs index — '), + ('bold', DOCS_URL), + ('', '\n'), + ]) + app.print_text('\n') + + def open_docs(self, event: KeyPressEvent, message: str) -> None: + _logger.debug(message) + webbrowser.open_new_tab(DOCS_URL) + prompt_toolkit.application.run_in_terminal(self._print_docs_help) + safe_invalidate_display(event.app) + + def toggle_smart_completion(self, message: str) -> None: + _logger.debug(message) + self._mycli.completer.smart_completion = not self._mycli.completer.smart_completion + + def toggle_multiline(self, message: str) -> None: + _logger.debug(message) + self._mycli.multi_line = not self._mycli.multi_line + + def toggle_editing_mode(self, event: KeyPressEvent, message: str) -> None: + _logger.debug(message) + if self._mycli.key_bindings == "vi": + event.app.editing_mode = EditingMode.EMACS + self._mycli.key_bindings = "emacs" + event.app.ttimeoutlen = self._mycli.emacs_ttimeoutlen + else: + event.app.editing_mode = EditingMode.VI + self._mycli.key_bindings = "vi" + event.app.ttimeoutlen = self._mycli.vi_ttimeoutlen diff --git a/mycli/key_bindings.py b/mycli/key_bindings.py index d209f726..af6a7499 100644 --- a/mycli/key_bindings.py +++ b/mycli/key_bindings.py @@ -1,9 +1,5 @@ import logging -import webbrowser - -import prompt_toolkit from prompt_toolkit.application.current import get_app -from prompt_toolkit.enums import EditingMode from prompt_toolkit.filters import ( Condition, completion_is_selected, @@ -13,11 +9,9 @@ from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.selection import SelectionType - -from mycli.constants import DOCS_URL +from mycli.key_binding_actions import KeyBindingActions from mycli.packages import shortcuts from mycli.packages.toolkit.fzf import search_history -from mycli.packages.toolkit.utils import safe_invalidate_display _logger = logging.getLogger(__name__) @@ -35,121 +29,78 @@ def in_completion() -> bool: return bool(app.current_buffer.complete_state) -def print_f1_help(): - app = get_app() - app.print_text('\n') - app.print_text([ - ('', 'Inline help — type "'), - ('bold', 'help'), - ('', '" or "'), - ('bold', r'\?'), - ('', '"\n'), - ]) - app.print_text([ - ('', 'Docs index — '), - ('bold', DOCS_URL), - ('', '\n'), - ]) - app.print_text('\n') - - def mycli_bindings(mycli) -> KeyBindings: """Custom key bindings for mycli.""" kb = KeyBindings() + actions = KeyBindingActions(mycli) @kb.add('f1') def _(event: KeyPressEvent) -> None: """Open browser to documentation index.""" - _logger.debug('Detected F1 key.') - webbrowser.open_new_tab(DOCS_URL) - prompt_toolkit.application.run_in_terminal(print_f1_help) - safe_invalidate_display(event.app) + actions.open_docs(event, 'Detected F1 key.') @kb.add('escape', '[', 'P') def _(event: KeyPressEvent) -> None: """Open browser to documentation index.""" - _logger.debug("Detected alternate F1 key sequence.") - webbrowser.open_new_tab(DOCS_URL) - prompt_toolkit.application.run_in_terminal(print_f1_help) - safe_invalidate_display(event.app) + actions.open_docs(event, "Detected alternate F1 key sequence.") @kb.add("f2") def _(_event: KeyPressEvent) -> None: """Enable/Disable SmartCompletion Mode.""" - _logger.debug("Detected F2 key.") - mycli.completer.smart_completion = not mycli.completer.smart_completion + actions.toggle_smart_completion("Detected F2 key.") @kb.add('escape', '[', 'Q') def _(_event: KeyPressEvent) -> None: """Enable/Disable SmartCompletion Mode.""" - _logger.debug("Detected alternate F2 key sequence.") - mycli.completer.smart_completion = not mycli.completer.smart_completion + actions.toggle_smart_completion("Detected alternate F2 key sequence.") @kb.add("f3") def _(_event: KeyPressEvent) -> None: """Enable/Disable Multiline Mode.""" - _logger.debug("Detected F3 key.") - mycli.multi_line = not mycli.multi_line + actions.toggle_multiline("Detected F3 key.") @kb.add('escape', '[', 'R') def _(_event: KeyPressEvent) -> None: """Enable/Disable Multiline Mode.""" - _logger.debug('Detected alternate F3 key sequence.') - mycli.multi_line = not mycli.multi_line + actions.toggle_multiline('Detected alternate F3 key sequence.') @kb.add("f4") def _(event: KeyPressEvent) -> None: """Toggle between Vi and Emacs mode.""" - _logger.debug("Detected F4 key.") - if mycli.key_bindings == "vi": - event.app.editing_mode = EditingMode.EMACS - mycli.key_bindings = "emacs" - event.app.ttimeoutlen = mycli.emacs_ttimeoutlen - else: - event.app.editing_mode = EditingMode.VI - mycli.key_bindings = "vi" - event.app.ttimeoutlen = mycli.vi_ttimeoutlen + actions.toggle_editing_mode(event, "Detected F4 key.") @kb.add('escape', '[', 'S') def _(event: KeyPressEvent) -> None: """Toggle between Vi and Emacs mode.""" - _logger.debug('Detected alternate F4 key sequence.') - if mycli.key_bindings == 'vi': - event.app.editing_mode = EditingMode.EMACS - mycli.key_bindings = 'emacs' - event.app.ttimeoutlen = mycli.emacs_ttimeoutlen - else: - event.app.editing_mode = EditingMode.VI - mycli.key_bindings = 'vi' - event.app.ttimeoutlen = mycli.vi_ttimeoutlen + actions.toggle_editing_mode(event, 'Detected alternate F4 key sequence.') @kb.add("tab") def _(event: KeyPressEvent) -> None: """Complete action at cursor.""" _logger.debug("Detected key.") - b = event.app.current_buffer + buffer = event.app.current_buffer behaviors = mycli.config['keys'].as_list('tab') if 'toolkit_default' in behaviors: - if b.complete_state: - b.complete_next() + if buffer.complete_state: + buffer.complete_next() else: - b.start_completion(select_first=True) + buffer.start_completion(select_first=True) - if b.complete_state: + if buffer.complete_state: if 'advance' in behaviors: - b.complete_next() + buffer.complete_next() elif 'cancel' in behaviors: - b.cancel_completion() + buffer.cancel_completion() return if 'advancing_summon' in behaviors: - b.start_completion(select_first=True) + buffer.start_completion(select_first=True) elif 'prefixing_summon' in behaviors: - b.start_completion(insert_common_part=True) + buffer.start_completion(insert_common_part=True) elif 'summon' in behaviors: - b.start_completion(select_first=False) + buffer.start_completion(select_first=False) @kb.add("escape", eager=True, filter=in_completion) def _(event: KeyPressEvent) -> None: @@ -173,28 +124,28 @@ def _(event: KeyPressEvent) -> None: """ _logger.debug("Detected key.") - b = event.app.current_buffer + buffer = event.app.current_buffer behaviors = mycli.config['keys'].as_list('control_space') if 'toolkit_default' in behaviors: - if b.text: - b.start_selection(selection_type=SelectionType.CHARACTERS) + if buffer.text: + buffer.start_selection(selection_type=SelectionType.CHARACTERS) return - if b.complete_state: + if buffer.complete_state: if 'advance' in behaviors: - b.complete_next() + buffer.complete_next() elif 'cancel' in behaviors: - b.cancel_completion() + buffer.cancel_completion() return if 'advancing_summon' in behaviors: - b.start_completion(select_first=True) + buffer.start_completion(select_first=True) elif 'prefixing_summon' in behaviors: - b.start_completion(insert_common_part=True) + buffer.start_completion(insert_common_part=True) elif 'summon' in behaviors: - b.start_completion(select_first=False) + buffer.start_completion(select_first=False) @kb.add("c-x", "p", filter=emacs_mode) def _(event: KeyPressEvent) -> None: @@ -205,9 +156,9 @@ def _(event: KeyPressEvent) -> None: """ _logger.debug("Detected /> key.") - b = event.app.current_buffer - if b.text: - b.transform_region(0, len(b.text), mycli.handle_prettify_binding) + buffer = event.app.current_buffer + if buffer.text: + buffer.transform_region(0, len(buffer.text), mycli.handle_prettify_binding) @kb.add("c-x", "u", filter=emacs_mode) def _(event: KeyPressEvent) -> None: @@ -218,9 +169,9 @@ def _(event: KeyPressEvent) -> None: """ _logger.debug("Detected /< key.") - b = event.app.current_buffer - if b.text: - b.transform_region(0, len(b.text), mycli.handle_unprettify_binding) + buffer = event.app.current_buffer + if buffer.text: + buffer.transform_region(0, len(buffer.text), mycli.handle_unprettify_binding) @kb.add("c-o", "d", filter=emacs_mode) def _(event: KeyPressEvent) -> None: @@ -304,8 +255,8 @@ def _(event: KeyPressEvent) -> None: _logger.debug("Detected enter key.") event.current_buffer.complete_state = None - b = event.app.current_buffer - b.complete_state = None + buffer = event.app.current_buffer + buffer.complete_state = None @kb.add("escape", "enter") def _(event: KeyPressEvent) -> None: From e59c54d5a37979cc21478ad5273e2b3464fd40d2 Mon Sep 17 00:00:00 2001 From: alad Date: Tue, 17 Mar 2026 11:36:44 -0300 Subject: [PATCH 2/6] refactor: clarify identifier quoting logic --- mycli/sqlcompleter.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/mycli/sqlcompleter.py b/mycli/sqlcompleter.py index 112effae..a73eb978 100644 --- a/mycli/sqlcompleter.py +++ b/mycli/sqlcompleter.py @@ -948,7 +948,16 @@ def __init__( self.reset_completions() def escape_name(self, name: str) -> str: - if name and ((not self.name_pattern.match(name)) or (name.upper() in self.reserved_words) or (name.upper() in self.functions)): + if not name: + return name + + name_upper = name.upper() + needs_quoting = ( + not self.name_pattern.match(name) + or name_upper in self.reserved_words + or name_upper in self.functions + ) + if needs_quoting: name = f'`{name}`' return name From dda47165d0a3d3db5b7ba70d2815dd6570986e99 Mon Sep 17 00:00:00 2001 From: alad Date: Tue, 17 Mar 2026 11:37:22 -0300 Subject: [PATCH 3/6] refactor: centralize support info --- mycli/constants.py | 1 + mycli/main.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mycli/constants.py b/mycli/constants.py index 88edaa76..2e23240f 100644 --- a/mycli/constants.py +++ b/mycli/constants.py @@ -2,6 +2,7 @@ REPO_URL = 'https://github.com/dbcli/mycli' DOCS_URL = f'{HOME_URL}/docs' ISSUES_URL = f'{REPO_URL}/issues' +SUPPORT_INFO = f"Home: {HOME_URL}\nBug tracker: {ISSUES_URL}" DEFAULT_CHARSET = 'utf8mb4' DEFAULT_DATABASE = 'mysql' diff --git a/mycli/main.py b/mycli/main.py index 5a8390ca..227702b2 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -72,9 +72,9 @@ DEFAULT_CHARSET, DEFAULT_HOST, DEFAULT_PORT, - HOME_URL, ISSUES_URL, REPO_URL, + SUPPORT_INFO, ) from mycli.key_bindings import mycli_bindings from mycli.lexer import MyCliLexer @@ -105,7 +105,6 @@ # Query tuples are used for maintaining history Query = namedtuple("Query", ["query", "successful", "mutating"]) -SUPPORT_INFO = f"Home: {HOME_URL}\nBug tracker: {ISSUES_URL}" DEFAULT_WIDTH = 80 DEFAULT_HEIGHT = 25 MIN_COMPLETION_TRIGGER = 1 From 7301782546d766251a758bda9f984861c789ed5d Mon Sep 17 00:00:00 2001 From: alad Date: Tue, 17 Mar 2026 11:39:02 -0300 Subject: [PATCH 4/6] refactor: dispatch special commands via handlers --- mycli/packages/special/main.py | 66 ++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/mycli/packages/special/main.py b/mycli/packages/special/main.py index e0ee43e1..f93ebec8 100644 --- a/mycli/packages/special/main.py +++ b/mycli/packages/special/main.py @@ -54,6 +54,61 @@ class Verbosity(Enum): VERBOSE = "verbose" +class ArgTypeExecutor: + def execute( + self, + special_cmd: SpecialCommand, + cur: Cursor, + sql: str, + arg: str, + verbosity: Verbosity, + ) -> list[SQLResult]: + raise NotImplementedError + + +class NoQueryExecutor(ArgTypeExecutor): + def execute( + self, + special_cmd: SpecialCommand, + cur: Cursor, + sql: str, + arg: str, + verbosity: Verbosity, + ) -> list[SQLResult]: + return special_cmd.handler() + + +class ParsedQueryExecutor(ArgTypeExecutor): + def execute( + self, + special_cmd: SpecialCommand, + cur: Cursor, + sql: str, + arg: str, + verbosity: Verbosity, + ) -> list[SQLResult]: + return special_cmd.handler(cur=cur, arg=arg, verbose=(verbosity == Verbosity.VERBOSE)) + + +class RawQueryExecutor(ArgTypeExecutor): + def execute( + self, + special_cmd: SpecialCommand, + cur: Cursor, + sql: str, + arg: str, + verbosity: Verbosity, + ) -> list[SQLResult]: + return special_cmd.handler(cur=cur, query=sql) + + +ARG_TYPE_EXECUTORS: dict[ArgType, ArgTypeExecutor] = { + ArgType.NO_QUERY: NoQueryExecutor(), + ArgType.PARSED_QUERY: ParsedQueryExecutor(), + ArgType.RAW_QUERY: RawQueryExecutor(), +} + + def parse_special_command(sql: str) -> tuple[str, Verbosity, str]: command, _, arg = sql.partition(" ") verbosity = Verbosity.NORMAL @@ -147,14 +202,11 @@ def execute(cur: Cursor, sql: str) -> list[SQLResult]: if command == "help" and arg: return show_keyword_help(cur=cur, arg=arg) - if special_cmd.arg_type == ArgType.NO_QUERY: - return special_cmd.handler() - elif special_cmd.arg_type == ArgType.PARSED_QUERY: - return special_cmd.handler(cur=cur, arg=arg, verbose=(verbosity == Verbosity.VERBOSE)) - elif special_cmd.arg_type == ArgType.RAW_QUERY: - return special_cmd.handler(cur=cur, query=sql) + executor = ARG_TYPE_EXECUTORS.get(special_cmd.arg_type) + if executor is None: + raise CommandNotFound(f"Command type not found: {command}") - raise CommandNotFound(f"Command type not found: {command}") + return executor.execute(special_cmd, cur=cur, sql=sql, arg=arg, verbosity=verbosity) @special_command( From 1035858ef57b0f7ba0082167ef4e4a1a42d81f2a Mon Sep 17 00:00:00 2001 From: alad Date: Wed, 18 Mar 2026 15:55:38 -0300 Subject: [PATCH 5/6] chore: fix ruff lint --- mycli/key_binding_actions.py | 5 ++++- mycli/sqlcompleter.py | 6 +----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/mycli/key_binding_actions.py b/mycli/key_binding_actions.py index afdd0853..c48a6699 100644 --- a/mycli/key_binding_actions.py +++ b/mycli/key_binding_actions.py @@ -1,11 +1,14 @@ from __future__ import annotations + import logging -import webbrowser from typing import Any +import webbrowser + import prompt_toolkit from prompt_toolkit.application.current import get_app from prompt_toolkit.enums import EditingMode from prompt_toolkit.key_binding.key_processor import KeyPressEvent + from mycli.constants import DOCS_URL from mycli.packages.toolkit.utils import safe_invalidate_display diff --git a/mycli/sqlcompleter.py b/mycli/sqlcompleter.py index a73eb978..7584b921 100644 --- a/mycli/sqlcompleter.py +++ b/mycli/sqlcompleter.py @@ -952,11 +952,7 @@ def escape_name(self, name: str) -> str: return name name_upper = name.upper() - needs_quoting = ( - not self.name_pattern.match(name) - or name_upper in self.reserved_words - or name_upper in self.functions - ) + needs_quoting = not self.name_pattern.match(name) or name_upper in self.reserved_words or name_upper in self.functions if needs_quoting: name = f'`{name}`' From 17cbe18d83b566af6334d6648e3203a631b29d37 Mon Sep 17 00:00:00 2001 From: alad Date: Wed, 18 Mar 2026 16:07:31 -0300 Subject: [PATCH 6/6] chore: fix ruff lint --- mycli/key_bindings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mycli/key_bindings.py b/mycli/key_bindings.py index af6a7499..68321481 100644 --- a/mycli/key_bindings.py +++ b/mycli/key_bindings.py @@ -1,4 +1,5 @@ import logging + from prompt_toolkit.application.current import get_app from prompt_toolkit.filters import ( Condition, @@ -9,6 +10,7 @@ from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.selection import SelectionType + from mycli.key_binding_actions import KeyBindingActions from mycli.packages import shortcuts from mycli.packages.toolkit.fzf import search_history