diff --git a/CHANGELOG.md b/CHANGELOG.md index 355cdd87..4060f54b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ * Add non-negative counterparts of many `--no-...` command-line option, thus allowing to enable respective feature/behaviour even if disabled in the configuration. (Requires Python 3.9 or higher.) +* Add a `y` command to copy focused query to the system clipboard, using + OSC 52 escape sequence (#311). ### Fixed diff --git a/README.md b/README.md index 07d60e69..cba37d23 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,7 @@ bytes). If your SQL query text look truncated, you should increase | `c` | Sort by CPU%, descending | | `m` | Sort by MEM%, descending | | `t` | Sort by TIME+, descending | +| `y` | Copy focused query to clipboard | | `T` | Change duration mode: query, transaction, backend | | `Space` | Pause on/off | | `v` | Change queries display mode: full, indented, truncated | @@ -302,6 +303,14 @@ Information can also be given via PostgreSQL's environment variables The password file is preferred since it's more secure (security is deferred to the OS). Please avoid password in connection strings at all cost. +**How to copy/paste the query of focused process?** + +The `y` shortcut will copy the query of focused process to system clipboard +using OSC 52 escape sequence. This requires the terminal emulator to support +this escape sequence and set the clipboard accordingly. If so, the copy even +works across remote connections (SSH). In general, terminal emulators supporting +this would use `CTRL+SHIFT+V` to paste from this clipboard. + # Hacking In order to work on pg\_activity source code, in particular to run the tests diff --git a/docs/man/pg_activity.1 b/docs/man/pg_activity.1 index e98cae6e..96ce9b49 100644 --- a/docs/man/pg_activity.1 +++ b/docs/man/pg_activity.1 @@ -537,6 +537,8 @@ See: https://www.postgresql.org/docs/current/libpq\-envars.html .IX Item "m Sort by MEM%, descending." .IP "\fBt\fR Sort by \s-1TIME+,\s0 descending." 2 .IX Item "t Sort by TIME+, descending." +.IP "\fBy\fR Copy focused query to clipboard." 2 +.IX Item "y Copy focused query to clipboard." .IP "\fBT\fR Change duration mode: query, transaction, backend." 2 .IX Item "T Change duration mode: query, transaction, backend." .IP "\fBSpace\fR Pause on/off." 2 diff --git a/docs/man/pg_activity.pod b/docs/man/pg_activity.pod index fecc757b..c0ad5a6c 100644 --- a/docs/man/pg_activity.pod +++ b/docs/man/pg_activity.pod @@ -417,6 +417,8 @@ See: https://www.postgresql.org/docs/current/libpq-envars.html =item B Sort by TIME+, descending. +=item B Copy focused query to clipboard. + =item B Change duration mode: query, transaction, backend. =item B Pause on/off. diff --git a/pgactivity/keys.py b/pgactivity/keys.py index b2930653..3aa5ad2d 100644 --- a/pgactivity/keys.py +++ b/pgactivity/keys.py @@ -28,6 +28,7 @@ def __eq__(self, other: Any) -> bool: EXIT = "q" HELP = "h" SPACE = " " +COPY_TO_CLIPBOARD = "y" PROCESS_CANCEL = "C" PROCESS_KILL = "K" PROCESS_FIRST = "KEY_HOME" diff --git a/pgactivity/types.py b/pgactivity/types.py index ec6682aa..28119057 100644 --- a/pgactivity/types.py +++ b/pgactivity/types.py @@ -1168,6 +1168,20 @@ def toggle_pin_focused(self) -> None: except KeyError: self.pinned.add(self.focused) + def copy_focused_query_to_clipboard(self) -> str: + """Copy focused query to system clipboard using ANSI OSC 52 escape sequence.""" + assert self.focused is not None + for proc in self.items: + if proc.pid == self.focused: + break + else: + return "no focused process found" + if proc.query is None: + return "process has no query" + + utils.osc52_copy(proc.query) + return f"query of process {proc.pid} copied to clipboard" + ActivityStats = Union[ Iterable[WaitingProcess], diff --git a/pgactivity/ui.py b/pgactivity/ui.py index a08b014d..520926a0 100644 --- a/pgactivity/ui.py +++ b/pgactivity/ui.py @@ -97,6 +97,9 @@ def main( ui.start_interactive() elif key == keys.SPACE: pg_procs.toggle_pin_focused() + elif key == keys.COPY_TO_CLIPBOARD: + msg = pg_procs.copy_focused_query_to_clipboard() + msg_pile.send(msg) elif key.name == keys.CANCEL_SELECTION: pg_procs.reset() ui.end_interactive() diff --git a/pgactivity/utils.py b/pgactivity/utils.py index 6c01422f..9e33be70 100644 --- a/pgactivity/utils.py +++ b/pgactivity/utils.py @@ -1,7 +1,9 @@ from __future__ import annotations +import base64 import functools import re +import sys from datetime import datetime, timedelta, timezone from typing import IO, Any, Iterable, Mapping @@ -202,6 +204,12 @@ def short_state(state: str) -> str: }.get(state, state) +def osc52_copy(text: str) -> None: + buffer = sys.__stderr__.buffer + buffer.write(b";".join([b"\033]52", b"c", base64.b64encode(text.encode())]) + b"\a") + buffer.flush() + + def csv_write( fobj: IO[str], procs: Iterable[Mapping[str, Any]], diff --git a/tests/test_ui.txt b/tests/test_ui.txt index b33cab19..c5cfa79a 100644 --- a/tests/test_ui.txt +++ b/tests/test_ui.txt @@ -951,6 +951,116 @@ Interactive mode: (Note: we patch boxed() widget to disable border in order to make output independent of the number of digits in PIDs.) +>>> keys = ["j", "j", "y", "q"] +>>> with patch.object( +... widgets, "boxed", new=functools.partial(widgets.boxed, border=False), +... ): +... run_ui(options, keys, render_footer=True, render_header=False, width=140) # doctest: +ELLIPSIS,+NORMALIZE_WHITESPACE + RUNNING QUERIES +PID DATABASE state Query +... tests idle in trans SELECT 42 +... tests idle in trans SELECT 43 +... tests idle in trans UPDATE t SET s = 'blocking' +... tests active UPDATE t SET s = 'waiting' + + + + + + + + + + + + + + + + + + +F1/1 Running queries F2/2 Waiting queries F3/3 Blocking queries Space Pause/unpause q Quit h Help +------------------------------------------------------------- sending key 'j' -------------------------------------------------------------- + RUNNING QUERIES +PID DATABASE state Query +... tests idle in trans SELECT 42 +... tests idle in trans SELECT 43 +... tests idle in trans UPDATE t SET s = 'blocking' +... tests active UPDATE t SET s = 'waiting' + + + + + + + + + + + + + + + + + + +C Cancel current query K Terminate underlying ses Space Tag/untag current qu Other Back to activities q Quit +------------------------------------------------------------- sending key 'j' -------------------------------------------------------------- + RUNNING QUERIES +PID DATABASE state Query +... tests idle in trans SELECT 43 +... tests idle in trans UPDATE t SET s = 'blocking' +... tests active UPDATE t SET s = 'waiting' +... tests idle in trans SELECT 42 + + + + + + + + + + + + + + + + + + +C Cancel current query K Terminate underlying ses Space Tag/untag current qu Other Back to activities q Quit +------------------------------------------------------------- sending key 'y' -------------------------------------------------------------- + RUNNING QUERIES +PID DATABASE state Query +... tests idle in trans SELECT 43 +... tests idle in trans UPDATE t SET s = 'blocking' +... tests active UPDATE t SET s = 'waiting' +... tests idle in trans SELECT 42 + + + + + + + + + + + + + + + + + + + query of process ... copied to clipboard +------------------------------------------------------------- sending key 'q' -------------------------------------------------------------- + >>> key_down = {"ucs": "KEY_DOWN", "name": "KEY_DOWN"} >>> keys = [key_down, 2, 3, 1, key_down, "C", "n", "K", "y", ... key_down, " ", "j", " ", "K", "y", "q"]