Skip to content

Commit d668bd2

Browse files
committed
create key_binding_utils.py
and migrate functions to it. Motivation: move code out of main.py. This may be a bit overenthusiastic, as all command handlers are moved there, even the \clip handler, which does not have a keybinding. And maybe "handlers" are not a fit with "utils". As a comment notes, the handlers might be better moved later to a repl_handlers.py. But that move would be premature before we have a repl.py.
1 parent c4d7c48 commit d668bd2

10 files changed

Lines changed: 423 additions & 365 deletions

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Internal
4343
* Skip more tests when a database connection is not present.
4444
* Move SQL utilities to a new `sql_utils.py`.
4545
* Move CLI utilities to a new `cli_utils.py`.
46+
* Move keybinding utilities to a new `key_binding_utils.py`.
4647

4748

4849
1.67.1 (2026/03/28)

mycli/key_bindings.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import partial
12
import logging
23
import webbrowser
34

@@ -11,11 +12,12 @@
1112
emacs_mode,
1213
)
1314
from prompt_toolkit.key_binding import KeyBindings
15+
from prompt_toolkit.key_binding.bindings.named_commands import register as ptoolkit_register
1416
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
1517
from prompt_toolkit.selection import SelectionType
1618

1719
from mycli.constants import DOCS_URL
18-
from mycli.packages import shortcuts
20+
from mycli.packages import key_binding_utils
1921
from mycli.packages.ptoolkit.fzf import search_history
2022
from mycli.packages.ptoolkit.utils import safe_invalidate_display
2123

@@ -53,6 +55,14 @@ def print_f1_help():
5355
app.print_text('\n')
5456

5557

58+
@ptoolkit_register("edit-and-execute-command")
59+
def edit_and_execute(event: KeyPressEvent) -> None:
60+
"""Different from the prompt-toolkit default, we want to have a choice not
61+
to execute a query after editing, hence validate_and_handle=False."""
62+
buff = event.current_buffer
63+
buff.open_in_editor(validate_and_handle=False)
64+
65+
5666
def mycli_bindings(mycli) -> KeyBindings:
5767
"""Custom key bindings for mycli."""
5868
kb = KeyBindings()
@@ -207,7 +217,7 @@ def _(event: KeyPressEvent) -> None:
207217

208218
b = event.app.current_buffer
209219
if b.text:
210-
b.transform_region(0, len(b.text), mycli.handle_prettify_binding)
220+
b.transform_region(0, len(b.text), partial(key_binding_utils.handle_prettify_binding, mycli))
211221

212222
@kb.add("c-x", "u", filter=emacs_mode)
213223
def _(event: KeyPressEvent) -> None:
@@ -220,7 +230,7 @@ def _(event: KeyPressEvent) -> None:
220230

221231
b = event.app.current_buffer
222232
if b.text:
223-
b.transform_region(0, len(b.text), mycli.handle_unprettify_binding)
233+
b.transform_region(0, len(b.text), partial(key_binding_utils.handle_unprettify_binding, mycli))
224234

225235
@kb.add("c-o", "d", filter=emacs_mode)
226236
def _(event: KeyPressEvent) -> None:
@@ -229,7 +239,7 @@ def _(event: KeyPressEvent) -> None:
229239
"""
230240
_logger.debug("Detected <C-o d> key.")
231241

232-
event.app.current_buffer.insert_text(shortcuts.server_date(mycli.sqlexecute))
242+
event.app.current_buffer.insert_text(key_binding_utils.server_date(mycli.sqlexecute))
233243

234244
@kb.add("c-o", "c-d", filter=emacs_mode)
235245
def _(event: KeyPressEvent) -> None:
@@ -238,7 +248,7 @@ def _(event: KeyPressEvent) -> None:
238248
"""
239249
_logger.debug("Detected <C-o C-d> key.")
240250

241-
event.app.current_buffer.insert_text(shortcuts.server_date(mycli.sqlexecute, quoted=True))
251+
event.app.current_buffer.insert_text(key_binding_utils.server_date(mycli.sqlexecute, quoted=True))
242252

243253
@kb.add("c-o", "t", filter=emacs_mode)
244254
def _(event: KeyPressEvent) -> None:
@@ -247,7 +257,7 @@ def _(event: KeyPressEvent) -> None:
247257
"""
248258
_logger.debug("Detected <C-o t> key.")
249259

250-
event.app.current_buffer.insert_text(shortcuts.server_datetime(mycli.sqlexecute))
260+
event.app.current_buffer.insert_text(key_binding_utils.server_datetime(mycli.sqlexecute))
251261

252262
@kb.add("c-o", "c-t", filter=emacs_mode)
253263
def _(event: KeyPressEvent) -> None:
@@ -256,7 +266,7 @@ def _(event: KeyPressEvent) -> None:
256266
"""
257267
_logger.debug("Detected <C-o C-t> key.")
258268

259-
event.app.current_buffer.insert_text(shortcuts.server_datetime(mycli.sqlexecute, quoted=True))
269+
event.app.current_buffer.insert_text(key_binding_utils.server_datetime(mycli.sqlexecute, quoted=True))
260270

261271
@kb.add("c-r", filter=control_is_searchable)
262272
def _(event: KeyPressEvent) -> None:

mycli/main.py

Lines changed: 8 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import sys
1515
import threading
1616
import traceback
17-
from typing import IO, Any, Callable, Generator, Iterable, Literal
17+
from typing import IO, Any, Generator, Iterable, Literal
1818

1919
try:
2020
from pwd import getpwuid
@@ -50,8 +50,6 @@
5050
to_formatted_text,
5151
to_plain_text,
5252
)
53-
from prompt_toolkit.key_binding.bindings.named_commands import register as prompt_register
54-
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
5553
from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
5654
from prompt_toolkit.lexers import PygmentsLexer
5755
from prompt_toolkit.output import ColorDepth
@@ -60,7 +58,6 @@
6058
from pymysql.constants.CR import CR_SERVER_LOST
6159
from pymysql.constants.ER import ACCESS_DENIED_ERROR, HANDSHAKE_ERROR
6260
from pymysql.cursors import Cursor
63-
import sqlglot
6461
import sqlparse
6562

6663
from mycli import __version__
@@ -93,6 +90,10 @@
9390
from mycli.packages.cli_utils import filtered_sys_argv, is_valid_connection_scheme
9491
from mycli.packages.filepaths import dir_path_exists, guess_socket_location
9592
from mycli.packages.hybrid_redirection import get_redirect_components, is_redirect_command
93+
from mycli.packages.key_binding_utils import (
94+
handle_clip_command,
95+
handle_editor_command,
96+
)
9697
from mycli.packages.prompt_utils import confirm, confirm_destructive_query
9798
from mycli.packages.ptoolkit.history import FileHistoryWithTimestamp
9899
from mycli.packages.special.favoritequeries import FavoriteQueries
@@ -871,99 +872,6 @@ def _connect(
871872
self.echo(str(e), err=True, fg="red")
872873
sys.exit(1)
873874

874-
def handle_editor_command(
875-
self,
876-
text: str,
877-
inputhook: Callable | None,
878-
loaded_message_fn: Callable,
879-
) -> str:
880-
r"""Editor command is any query that is prefixed or suffixed by a '\e'.
881-
The reason for a while loop is because a user might edit a query
882-
multiple times. For eg:
883-
884-
"select * from \e"<enter> to edit it in vim, then come
885-
back to the prompt with the edited query "select * from
886-
blah where q = 'abc'\e" to edit it again.
887-
:param text: Document
888-
:return: Document
889-
890-
"""
891-
892-
while special.editor_command(text):
893-
filename = special.get_filename(text)
894-
query = special.get_editor_query(text) or self.get_last_query()
895-
sql, message = special.open_external_editor(filename=filename, sql=query)
896-
if message:
897-
# Something went wrong. Raise an exception and bail.
898-
raise RuntimeError(message)
899-
while True:
900-
try:
901-
assert isinstance(self.prompt_app, PromptSession)
902-
text = self.prompt_app.prompt(
903-
default=sql,
904-
inputhook=inputhook,
905-
message=loaded_message_fn,
906-
)
907-
break
908-
except KeyboardInterrupt:
909-
sql = ""
910-
911-
continue
912-
return text
913-
914-
def handle_clip_command(self, text: str) -> bool:
915-
r"""A clip command is any query that is prefixed or suffixed by a
916-
'\clip'.
917-
918-
:param text: Document
919-
:return: Boolean
920-
921-
"""
922-
923-
if special.clip_command(text):
924-
query = special.get_clip_query(text) or self.get_last_query()
925-
message = special.copy_query_to_clipboard(sql=query)
926-
if message:
927-
raise RuntimeError(message)
928-
return True
929-
return False
930-
931-
def handle_prettify_binding(self, text: str) -> str:
932-
if not text:
933-
return ''
934-
try:
935-
statements = sqlglot.parse(text, read='mysql')
936-
except Exception:
937-
statements = []
938-
if len(statements) == 1 and statements[0]:
939-
parse_succeeded = True
940-
pretty_text = statements[0].sql(pretty=True, pad=4, dialect='mysql')
941-
else:
942-
parse_succeeded = False
943-
pretty_text = text.rstrip(';')
944-
self.toolbar_error_message = 'Prettify failed to parse single statement'
945-
if pretty_text and parse_succeeded:
946-
pretty_text = pretty_text + ';'
947-
return pretty_text
948-
949-
def handle_unprettify_binding(self, text: str) -> str:
950-
if not text:
951-
return ''
952-
try:
953-
statements = sqlglot.parse(text, read='mysql')
954-
except Exception:
955-
statements = []
956-
if len(statements) == 1 and statements[0]:
957-
parse_succeeded = True
958-
unpretty_text = statements[0].sql(pretty=False, dialect='mysql')
959-
else:
960-
parse_succeeded = False
961-
unpretty_text = text.rstrip(';')
962-
self.toolbar_error_message = 'Unprettify failed to parse single statement'
963-
if unpretty_text and parse_succeeded:
964-
unpretty_text = unpretty_text + ';'
965-
return unpretty_text
966-
967875
def output_timing(self, timing: str, is_warnings_style: bool = False) -> None:
968876
self.log_output(timing)
969877
add_style = 'class:warnings.timing' if is_warnings_style else 'class:output.timing'
@@ -1168,7 +1076,8 @@ def one_iteration(text: str | None = None) -> None:
11681076
special.set_forced_horizontal_output(False)
11691077

11701078
try:
1171-
text = self.handle_editor_command(
1079+
text = handle_editor_command(
1080+
self,
11721081
text,
11731082
inputhook,
11741083
loaded_message_fn,
@@ -1180,7 +1089,7 @@ def one_iteration(text: str | None = None) -> None:
11801089
return
11811090

11821091
try:
1183-
if self.handle_clip_command(text):
1092+
if handle_clip_command(self, text):
11841093
return
11851094
except RuntimeError as e:
11861095
logger.error("sql: %r, error: %r", text, e)
@@ -2698,14 +2607,6 @@ def tips_picker() -> str:
26982607
return choice(tips) if tips else r'\? or "help" for help!'
26992608

27002609

2701-
@prompt_register("edit-and-execute-command")
2702-
def edit_and_execute(event: KeyPressEvent) -> None:
2703-
"""Different from the prompt-toolkit default, we want to have a choice not
2704-
to execute a query after editing, hence validate_and_handle=False."""
2705-
buff = event.current_buffer
2706-
buff.open_in_editor(validate_and_handle=False)
2707-
2708-
27092610
def main() -> int | None:
27102611
try:
27112612
result = click_entrypoint.main(

0 commit comments

Comments
 (0)