Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Features
--------
* Remove support for Python 3.10.
* Exit faster when using a Boundary tunnel.
* Add <kbd>C-o</kbd> <kbd>u</kbd> keybindings to insert literal Unix timestamps.


Documentation
Expand Down
8 changes: 8 additions & 0 deletions doc/key_bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,11 @@ Insert the current datetime at cursor.
## <kbd>C-o</kbd> <kbd>C-t</kbd> _(Emacs-mode)_

Insert the quoted current datetime at cursor.

## <kbd>C-o</kbd> <kbd>u</kbd> _(Emacs-mode)_

Insert the current literal `UNIX_TIMESTAMP()` at cursor, in seconds.

## <kbd>C-o</kbd> <kbd>C-u</kbd> _(Emacs-mode)_

Insert the current literal `UNIX_TIMESTAMP()` at cursor, in microseconds.
2 changes: 2 additions & 0 deletions mycli/TIPS
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ insert the current datetime using keystrokes control-o + t!

insert the quoted current date using keystrokes control-o + control-t!

insert the literal UNIX_TIMESTAMP() with keystroke control-o + u!

search query history using keystroke control-r!

use keystroke control-g to cancel completion popups!
Expand Down
18 changes: 18 additions & 0 deletions mycli/key_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,24 @@ def _(event: KeyPressEvent) -> None:

event.app.current_buffer.insert_text(key_binding_utils.server_datetime(mycli.sqlexecute, quoted=True))

@kb.add('c-o', 'u', filter=emacs_mode)
def _(event: KeyPressEvent) -> None:
"""
Insert the current unix timestamp in seconds.
"""
_logger.debug('Detected <C-o u> key.')

event.app.current_buffer.insert_text(key_binding_utils.unix_timestamp())

@kb.add('c-o', 'c-u', filter=emacs_mode)
def _(event: KeyPressEvent) -> None:
"""
Insert the current unix timestamp in microseconds.
"""
_logger.debug('Detected <C-o C-u> key.')

event.app.current_buffer.insert_text(key_binding_utils.unix_timestamp(microseconds=True))

@kb.add("c-r", filter=control_is_searchable)
def _(event: KeyPressEvent) -> None:
"""Search history using fzf or reverse incremental search."""
Expand Down
9 changes: 9 additions & 0 deletions mycli/packages/key_binding_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import math
import time
from typing import TYPE_CHECKING, Callable

from prompt_toolkit.shortcuts import PromptSession
Expand Down Expand Up @@ -28,6 +30,13 @@ def server_datetime(sqlexecute: SQLExecute, quoted: bool = False) -> str:
return server_datetime_str


def unix_timestamp(microseconds: bool = False) -> str:
if microseconds:
return str(math.floor(time.time() * 1e6))
else:
return str(math.floor(time.time()))


# todo: maybe these handlers belong in a repl_handlers.py (which does not exist yet)
# \clip doesn't even have a keybinding
def handle_clip_command(mycli: 'MyCli', text: str) -> bool:
Expand Down
33 changes: 33 additions & 0 deletions test/pytests/test_key_binding_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,39 @@ def test_server_datetime_returns_quoted_and_unquoted_values() -> None:
assert key_binding_utils.server_datetime(cast(Any, sqlexecute), quoted=True) == "'2026-04-03 14:05:06'"


@pytest.mark.parametrize(
('timestamp', 'expected'),
[(0.0, '0'), (1700000000.875, '1700000000'), (-0.125, '-1')],
)
def test_unix_timestamp_returns_seconds(
monkeypatch: pytest.MonkeyPatch,
timestamp: float,
expected: str,
) -> None:
monkeypatch.setattr(key_binding_utils.time, 'time', lambda: timestamp)

assert key_binding_utils.unix_timestamp() == expected


@pytest.mark.parametrize(
('timestamp', 'expected'),
[
(0.0, '0'),
(1700000000.875, '1700000000875000'),
(0.00000175, '1'),
(-0.00000175, '-2'),
],
)
def test_unix_timestamp_returns_microseconds(
monkeypatch: pytest.MonkeyPatch,
timestamp: float,
expected: str,
) -> None:
monkeypatch.setattr(key_binding_utils.time, 'time', lambda: timestamp)

assert key_binding_utils.unix_timestamp(microseconds=True) == expected


def test_prettify_statement():
statement = 'SELECT 1'
mycli = FakeMyCli()
Expand Down
61 changes: 61 additions & 0 deletions test/pytests/test_key_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,29 @@ def test_escape_binding_cancels_completion_menu(
assert event.app.current_buffer.complete_state is None


@pytest.mark.parametrize('key, config_name', [(Keys.ControlI, 'tab'), (Keys.ControlAt, 'control_space')])
@pytest.mark.parametrize('completion_active', [False, True])
def test_completion_binding_without_configured_behaviors_is_noop(
key: Keys,
config_name: str,
completion_active: bool,
) -> None:
mycli = DummyMyCli(DummyKeysConfig(behaviors={config_name: []}))
kb = key_bindings.mycli_bindings(mycli)
complete_state = object() if completion_active else None
buffer = DummyBuffer(text='sel', complete_state=complete_state)
event = make_event(buffer)

binding_handler(kb, key)(event)

assert buffer.complete_state is complete_state
assert buffer.start_completion_calls == []
assert buffer.complete_next_calls == 0
assert buffer.cancel_completion_calls == 0
assert buffer.start_selection_calls == []
assert buffer.inserted_text == []


def test_control_space_toolkit_default_starts_selection_for_non_empty_text() -> None:
mycli = DummyMyCli(DummyKeysConfig(behaviors={'control_space': ['toolkit_default']}))
kb = key_bindings.mycli_bindings(mycli)
Expand Down Expand Up @@ -501,6 +524,44 @@ def test_date_and_datetime_bindings_insert_shortcuts(
assert event.app.current_buffer.inserted_text == [expected_text]


@pytest.mark.parametrize(
('keys', 'expected_text'),
[
((Keys.ControlO, 'u'), '1700000000'),
((Keys.ControlO, Keys.ControlU), '1700000000875000'),
],
)
def test_unix_timestamp_bindings_insert_numeric_literals(
monkeypatch: pytest.MonkeyPatch,
keys: tuple[str | Keys, ...],
expected_text: str,
) -> None:
mycli = DummyMyCli(DummyKeysConfig(), key_bindings_mode='emacs')
kb = key_bindings.mycli_bindings(mycli)
event = make_event()
monkeypatch.setattr(key_bindings.key_binding_utils.time, 'time', lambda: 1700000000.875)

binding_handler(kb, *keys)(event)

assert event.app.current_buffer.inserted_text == [expected_text]


@pytest.mark.parametrize('keys', [(Keys.ControlO, 'u'), (Keys.ControlO, Keys.ControlU)])
@pytest.mark.parametrize('editing_mode, enabled', [(EditingMode.EMACS, True), (EditingMode.VI, False)])
def test_unix_timestamp_bindings_are_emacs_only(
monkeypatch: pytest.MonkeyPatch,
keys: tuple[str | Keys, ...],
editing_mode: EditingMode,
enabled: bool,
) -> None:
mycli = DummyMyCli(DummyKeysConfig())
kb = key_bindings.mycli_bindings(mycli)
app = DummyApp(current_buffer=DummyBuffer(), editing_mode=editing_mode)
patch_filter_app(monkeypatch, app)

assert binding_filter(kb, *keys)() is enabled


def test_control_r_uses_reverse_isearch_mode_when_configured(monkeypatch) -> None:
mycli = DummyMyCli(DummyKeysConfig(options={'control_r': 'reverse_isearch'}), key_bindings_mode='emacs')
kb = key_bindings.mycli_bindings(mycli)
Expand Down
Loading