diff --git a/changelog.md b/changelog.md
index 54bb2c15..a47fedfc 100644
--- a/changelog.md
+++ b/changelog.md
@@ -5,6 +5,7 @@ Features
--------
* Remove support for Python 3.10.
* Exit faster when using a Boundary tunnel.
+* Add C-o u keybindings to insert literal Unix timestamps.
Documentation
diff --git a/doc/key_bindings.md b/doc/key_bindings.md
index bec58a0e..fc353b33 100644
--- a/doc/key_bindings.md
+++ b/doc/key_bindings.md
@@ -65,3 +65,11 @@ Insert the current datetime at cursor.
## C-o C-t _(Emacs-mode)_
Insert the quoted current datetime at cursor.
+
+## C-o u _(Emacs-mode)_
+
+Insert the current literal `UNIX_TIMESTAMP()` at cursor, in seconds.
+
+## C-o C-u _(Emacs-mode)_
+
+Insert the current literal `UNIX_TIMESTAMP()` at cursor, in microseconds.
diff --git a/mycli/TIPS b/mycli/TIPS
index 39d5c1ab..9fcdfcee 100644
--- a/mycli/TIPS
+++ b/mycli/TIPS
@@ -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!
diff --git a/mycli/key_bindings.py b/mycli/key_bindings.py
index 70038e43..4f325655 100644
--- a/mycli/key_bindings.py
+++ b/mycli/key_bindings.py
@@ -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 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 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."""
diff --git a/mycli/packages/key_binding_utils.py b/mycli/packages/key_binding_utils.py
index e77efee6..8ad176d2 100644
--- a/mycli/packages/key_binding_utils.py
+++ b/mycli/packages/key_binding_utils.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+import math
+import time
from typing import TYPE_CHECKING, Callable
from prompt_toolkit.shortcuts import PromptSession
@@ -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:
diff --git a/test/pytests/test_key_binding_utils.py b/test/pytests/test_key_binding_utils.py
index bbb3d619..9f08e955 100644
--- a/test/pytests/test_key_binding_utils.py
+++ b/test/pytests/test_key_binding_utils.py
@@ -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()
diff --git a/test/pytests/test_key_bindings.py b/test/pytests/test_key_bindings.py
index c92fc293..2843e7dc 100644
--- a/test/pytests/test_key_bindings.py
+++ b/test/pytests/test_key_bindings.py
@@ -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)
@@ -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)