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/key_binding_actions.py b/mycli/key_binding_actions.py new file mode 100644 index 00000000..c48a6699 --- /dev/null +++ b/mycli/key_binding_actions.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import logging +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 + +_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..68321481 100644 --- a/mycli/key_bindings.py +++ b/mycli/key_bindings.py @@ -1,9 +1,6 @@ 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, @@ -14,10 +11,9 @@ 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 +31,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 +126,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 +158,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 +171,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 +257,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: 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 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( diff --git a/mycli/sqlcompleter.py b/mycli/sqlcompleter.py index 112effae..7584b921 100644 --- a/mycli/sqlcompleter.py +++ b/mycli/sqlcompleter.py @@ -948,7 +948,12 @@ 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