diff --git a/CHANGELOG.md b/CHANGELOG.md index 355cdd87..caa786da 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 clipboard, using + [pyperclip](https://pypi.org/project/pyperclip/) (#311). ### Fixed diff --git a/README.md b/README.md index af502d26..69de2cb6 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 | 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/mypy.ini b/mypy.ini index c802e39e..0f1f12b2 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2,3 +2,6 @@ files = pgactivity show_error_codes = true strict = true + +[mypy-pyperclip] +ignore_missing_imports = true 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..f27252ed 100644 --- a/pgactivity/types.py +++ b/pgactivity/types.py @@ -2,18 +2,22 @@ import enum import functools +import logging from datetime import timedelta from ipaddress import IPv4Address, IPv6Address from typing import Any, Tuple, TypeVar, Union, overload import attr import psutil +import pyperclip from attr import validators from . import colors, compat, pg, utils from .compat import Callable, Iterable, Iterator, Mapping, MutableSet, Sequence from .config import Configuration, Flag, HeaderSection, UISection +logger = logging.getLogger("pgactivity") + class Pct(float): """Used to distinguish percentage from float when displaying the header""" @@ -1168,6 +1172,23 @@ def toggle_pin_focused(self) -> None: except KeyError: self.pinned.add(self.focused) + def copy_focused_query_to_clipboard(self) -> str: + 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" + else: + try: + pyperclip.copy(proc.query) + except pyperclip.PyperclipException as exc: + logger.error(str(exc)) + return "failed to copy to clipboard" + 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/pyproject.toml b/pyproject.toml index d0eaa225..fc6c5767 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ dependencies = [ "humanize >= 0.5.1", "importlib_metadata; python_version < '3.8'", "psutil >= 2.0.0", + "pyperclip", ] [project.optional-dependencies] diff --git a/tests/test_ui.txt b/tests/test_ui.txt index b33cab19..e29944ef 100644 --- a/tests/test_ui.txt +++ b/tests/test_ui.txt @@ -951,6 +951,120 @@ 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' -------------------------------------------------------------- + +>>> import pyperclip +>>> pyperclip.paste() +'SELECT 43' + >>> 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"]