diff --git a/README.rst b/README.rst
index 5d1c297..981e67c 100644
--- a/README.rst
+++ b/README.rst
@@ -5,6 +5,9 @@ This repository contains the code of an experiment, in order to understand
how hard would it be (if possible) to have an application that can monitor
the access of other apps to the clipboard on Linux machines.
+This app can also ask the user for permission before providing the clipboard
+contents.
+
At the moment it only supports desktop environments that use X.
To learn more please read the
@@ -13,19 +16,13 @@ To learn more please read the
Installation
------------
-To build and run this demo app, you will need to have the following libraries
-on your system:
-
-* ``libdbus-1-dev``
-* ``libglib2.0-dev``
-
-You will also need to have `Poetry `_ in order to be
+To build and run this demo app, you will need to have `Poetry `_ in order to be
able to execute the following commands:
.. code-block::
$ poetry install
- $ poetry run watcher
+ $ poetry run watcher --help
Contributions
-------------
diff --git a/clipboard_watcher/app.py b/clipboard_watcher/app.py
index 234a6b5..b3a17aa 100644
--- a/clipboard_watcher/app.py
+++ b/clipboard_watcher/app.py
@@ -43,6 +43,12 @@ def main() -> None:
"Monitors the access of other processes to the clipboard contents."
)
parser.add_argument("-l", "--loglevel", help="Choose the log level")
+ parser.add_argument(
+ "-p",
+ "--permission",
+ action="store_true",
+ help="Ask for permission before sending clipboard data",
+ )
args = parser.parse_args()
if args.loglevel and args.loglevel in ["DEBUG", "INFO", "WARNING", "ERROR"]:
set_logger_settings(args.loglevel)
@@ -63,7 +69,9 @@ def main() -> None:
job_queue = Queue()
# Thread 1
event_worker = Thread(
- target=process_event_loop, args=(disp, window, job_queue, cb_data), daemon=True
+ target=process_event_loop,
+ args=(disp, window, job_queue, cb_data, args.permission),
+ daemon=True,
)
# Thread 2
notif_worker = Thread(
diff --git a/clipboard_watcher/dialog.py b/clipboard_watcher/dialog.py
new file mode 100644
index 0000000..8a4aaa3
--- /dev/null
+++ b/clipboard_watcher/dialog.py
@@ -0,0 +1,6 @@
+from tkinter.messagebox import askokcancel
+
+
+def ask_for_permission(window_name, pid, path) -> bool:
+ details = f"The process {pid} ({path}) with the window named '{window_name}' wants to access your clipboard data."
+ return askokcancel("Clipboard-Watcher", details)
diff --git a/clipboard_watcher/notifications.py b/clipboard_watcher/notifications.py
index 8024ab6..8457a9a 100644
--- a/clipboard_watcher/notifications.py
+++ b/clipboard_watcher/notifications.py
@@ -1,16 +1,12 @@
-import dbus
+from desktop_notifier import DesktopNotifier
import logging
logger = logging.getLogger("ClipboardWatcher")
+notifier = DesktopNotifier(app_name="Clipboard-Watcher", app_icon=None)
-def display_desktop_notification(title: str, details: str = "", icon: str = "") -> None:
- interface = "org.freedesktop.Notifications"
- path = "/org/freedesktop/Notifications"
- notification = dbus.Interface(
- dbus.SessionBus().get_object(interface, path), interface
- )
+def display_desktop_notification(title: str, details: str = "") -> None:
try:
- notification.Notify("Clipboard-Watcher", 0, icon, title, details, [], [], 7000)
+ notifier.send_sync(title=title, message=details)
except Exception:
logger.error("Unable to publish notification due to dbus error")
diff --git a/clipboard_watcher/xoperations.py b/clipboard_watcher/xoperations.py
index 0480c79..4675229 100644
--- a/clipboard_watcher/xoperations.py
+++ b/clipboard_watcher/xoperations.py
@@ -13,14 +13,18 @@ The current functionality contains:
import logging
from typing import List, Optional, Tuple
from queue import Queue
+from collections import namedtuple
from Xlib import X, Xatom
from Xlib.ext.res import query_client_ids, LocalClientPIDMask
from Xlib.protocol import event
+from clipboard_watcher.dialog import ask_for_permission
+
from .process_info import ProcessInfo
logger = logging.getLogger("ClipboardWatcher")
+FakeData = namedtuple("FakeData", ["primary", "clipboard"])
def get_selection_targets(disp, win, selection: str) -> List[str]:
@@ -214,7 +218,7 @@ def process_selection_clear_event(d, cb_data, e) -> None:
logger.warning("Owner again")
-def process_event_loop(d, w, q: Queue, cb_data) -> None:
+def process_event_loop(d, w, q: Queue, cb_data, permission=False) -> None:
while True:
e = d.next_event()
if (
@@ -237,7 +241,15 @@ def process_event_loop(d, w, q: Queue, cb_data) -> None:
# due to the risk of the requestor no longer be running afterwards
proc_info = ProcessInfo.collect(req_pid) if req_pid else None
- process_selection_request_event(d, cb_data, e)
+ if permission and d.get_atom_name(e.target) != "TARGETS":
+ if ask_for_permission(req_name, proc_info.pid, proc_info.path):
+ data = cb_data
+ else:
+ data = FakeData({}, {})
+ else:
+ data = cb_data
+
+ process_selection_request_event(d, data, e)
if d.get_atom_name(e.target) != "TARGETS":
q.put(
{
diff --git a/poetry.lock b/poetry.lock
index 7b3a0dc..64f1854 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -66,12 +66,30 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
-name = "dbus-python"
-version = "1.2.18"
-description = "Python bindings for libdbus"
+name = "dbus-next"
+version = "0.2.3"
+description = "A zero-dependency DBus library for Python with asyncio support"
category = "main"
optional = false
-python-versions = "*"
+python-versions = ">=3.6.0"
+
+[[package]]
+name = "desktop-notifier"
+version = "3.4.0"
+description = "Python library for cross-platform desktop notifications"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+dbus-next = {version = "*", markers = "sys_platform == \"linux\""}
+packaging = "*"
+rubicon-objc = {version = "*", markers = "sys_platform == \"darwin\""}
+winsdk = {version = "*", markers = "sys_platform == \"win32\""}
+
+[package.extras]
+dev = ["black", "bump2version", "flake8", "mypy", "pre-commit", "pytest", "pytest-cov"]
+docs = ["sphinx", "m2r2", "sphinx-autoapi", "sphinx-rtd-theme"]
[[package]]
name = "more-itertools"
@@ -93,7 +111,7 @@ python-versions = "*"
name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
-category = "dev"
+category = "main"
optional = false
python-versions = ">=3.6"
@@ -154,7 +172,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
name = "pyparsing"
version = "3.0.6"
description = "Python parsing module"
-category = "dev"
+category = "main"
optional = false
python-versions = ">=3.6"
@@ -194,6 +212,14 @@ python-versions = "*"
[package.dependencies]
six = ">=1.10.0"
+[[package]]
+name = "rubicon-objc"
+version = "0.4.2"
+description = "A bridge between an Objective C runtime environment and Python."
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
[[package]]
name = "six"
version = "1.16.0"
@@ -226,10 +252,18 @@ category = "dev"
optional = false
python-versions = "*"
+[[package]]
+name = "winsdk"
+version = "1.0.0b6"
+description = "Python bindings for the Windows SDK"
+category = "main"
+optional = false
+python-versions = "*"
+
[metadata]
lock-version = "1.1"
python-versions = ">=3.9,<3.11"
-content-hash = "5de6bab639719a04f7cf4b791199fabcec0f9c75e6bb12ce6039a299313a9f3c"
+content-hash = "fc4b2112388189c48f90187d3601f965cb6c402781eed13c0ccc4614cefc0bdd"
[metadata.files]
atomicwrites = [
@@ -252,8 +286,13 @@ colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
-dbus-python = [
- {file = "dbus-python-1.2.18.tar.gz", hash = "sha256:92bdd1e68b45596c833307a5ff4b217ee6929a1502f5341bae28fd120acf7260"},
+dbus-next = [
+ {file = "dbus_next-0.2.3-py3-none-any.whl", hash = "sha256:58948f9aff9db08316734c0be2a120f6dc502124d9642f55e90ac82ffb16a18b"},
+ {file = "dbus_next-0.2.3.tar.gz", hash = "sha256:f4eae26909332ada528c0a3549dda8d4f088f9b365153952a408e28023a626a5"},
+]
+desktop-notifier = [
+ {file = "desktop-notifier-3.4.0.tar.gz", hash = "sha256:92b10dfe97ea5599adbe2c03520cae5a4343017c3bb1f1dc2256c17b224608a9"},
+ {file = "desktop_notifier-3.4.0-py3-none-any.whl", hash = "sha256:f7151171ef78b9c46bb3509eb80c95b5108d7b482e4c0215cbc44fe35a61a4f3"},
]
more-itertools = [
{file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"},
@@ -329,6 +368,10 @@ python-xlib = [
{file = "python-xlib-0.31.tar.gz", hash = "sha256:74d83a081f532bc07f6d7afcd6416ec38403d68f68b9b9dc9e1f28fbf2d799e9"},
{file = "python_xlib-0.31-py2.py3-none-any.whl", hash = "sha256:1ec6ce0de73d9e6592ead666779a5732b384e5b8fb1f1886bd0a81cafa477759"},
]
+rubicon-objc = [
+ {file = "rubicon-objc-0.4.2.tar.gz", hash = "sha256:6fbc8e12bd66c84427cfb95634c4bd10ade356ae2b2ae0d2b51dcbf5810d2602"},
+ {file = "rubicon_objc-0.4.2-py3-none-any.whl", hash = "sha256:c8780b9d6c3c906642080a9f8b710f5823a498c17e9bbea7f70781ea6ede7962"},
+]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
@@ -345,3 +388,14 @@ wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
]
+winsdk = [
+ {file = "winsdk-1.0.0b6-cp310-cp310-win32.whl", hash = "sha256:3589f0535d159b6e64f25e9688adc7896acc01bfa0f0f1e3d08dfb3ed5809104"},
+ {file = "winsdk-1.0.0b6-cp310-cp310-win_amd64.whl", hash = "sha256:c0352706fa68cd28064f82b934ec709494e8f32efda23bf8f92e452ec2543df8"},
+ {file = "winsdk-1.0.0b6-cp37-cp37m-win32.whl", hash = "sha256:daef8d49c653a516430ac681f92ff28f96b2ff2f11fa805f8b2073b1a5864f2f"},
+ {file = "winsdk-1.0.0b6-cp37-cp37m-win_amd64.whl", hash = "sha256:20e384b50dbc2bd360dbfd33804be78679337ed4a31afb376abb9ddfae453010"},
+ {file = "winsdk-1.0.0b6-cp38-cp38-win32.whl", hash = "sha256:410c2323af51c8e11c41bc88e431b9dc22b6aa9a1f36d45ad758170cc9fed098"},
+ {file = "winsdk-1.0.0b6-cp38-cp38-win_amd64.whl", hash = "sha256:493eeb3807d2a50c4c203420a4cea55a83a675b53ab1309f3be3fda09ff143fe"},
+ {file = "winsdk-1.0.0b6-cp39-cp39-win32.whl", hash = "sha256:01886275aae8842135c0029e4c249b88356f6df95b1ff522ba2ac01647c01e4b"},
+ {file = "winsdk-1.0.0b6-cp39-cp39-win_amd64.whl", hash = "sha256:22048f379d46232961b1d98fed467a8603f6f9c138750a1e310f12063d340207"},
+ {file = "winsdk-1.0.0b6.tar.gz", hash = "sha256:c72248967311145d6544744d98aaf413161f24ec6e0849c3bfa86565ab2100cd"},
+]
diff --git a/pyproject.toml b/pyproject.toml
index 0d4c038..af2ccb2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,8 +7,8 @@ authors = ["Gonçalo Valério "]
[tool.poetry.dependencies]
python = ">=3.9,<3.11"
python-xlib = "^0.31"
-dbus-python = "^1.2.18"
psutil = "^5.9.0"
+desktop-notifier = "^3.4.0"
[tool.poetry.dev-dependencies]
pytest = "^5.2"