contextzip 0.4.5__tar.gz → 0.4.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {contextzip-0.4.5 → contextzip-0.4.6}/PKG-INFO +1 -1
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/__init__.py +1 -1
- contextzip-0.4.6/contextzip/_x11_clip.py +338 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/cli_display.py +6 -3
- contextzip-0.4.6/contextzip/clipboard.py +552 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip.egg-info/PKG-INFO +1 -1
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip.egg-info/SOURCES.txt +1 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/pyproject.toml +1 -1
- contextzip-0.4.5/contextzip/clipboard.py +0 -223
- {contextzip-0.4.5 → contextzip-0.4.6}/LICENSE +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/README.md +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/ai/__init__.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/ai/gemini.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/ai/heuristic.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/ai/selector.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/api.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/applier.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/cleanup.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/cli.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/cli_ai.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/cli_onboard.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/config.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/detector.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/error_parser.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/filters.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/git.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/packager.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/project_config.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/redact.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/report.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/rules/__init__.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/rules/base.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/rules/errors/__init__.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/rules/errors/node.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/rules/errors/python.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/rules/go.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/rules/node.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/rules/python.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/rules/ruby.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/rules/rust.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/updater.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/watcher.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/webui/__init__.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/webui/assets.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/webui/persist.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/webui/server.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip/webui/suggestions.py +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip.egg-info/dependency_links.txt +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip.egg-info/entry_points.txt +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip.egg-info/requires.txt +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/contextzip.egg-info/top_level.txt +0 -0
- {contextzip-0.4.5 → contextzip-0.4.6}/setup.cfg +0 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""
|
|
2
|
+
_x11_clip.py — a tiny X11 CLIPBOARD owner written against the raw X protocol.
|
|
3
|
+
|
|
4
|
+
Why this exists: on Linux, "a file on the clipboard" means owning the X
|
|
5
|
+
CLIPBOARD selection and answering requests for ``text/uri-list`` (what
|
|
6
|
+
browsers read when you paste) with a ``file:///…`` URL. Normally that needs
|
|
7
|
+
``xclip`` — a system package that ``pip install`` can't provide. The protocol
|
|
8
|
+
subset needed is small, so contextzip speaks it itself: standard library only,
|
|
9
|
+
no dependencies.
|
|
10
|
+
|
|
11
|
+
It offers the same targets a file manager offers when you press Ctrl+C on a
|
|
12
|
+
file, so it works for browsers (text/uri-list), GNOME-family file managers
|
|
13
|
+
(x-special/gnome-copied-files) and KDE (application/x-kde-cutselection).
|
|
14
|
+
|
|
15
|
+
Runs two ways:
|
|
16
|
+
* imported: ``read_selection()`` reads the clipboard back (verification)
|
|
17
|
+
* as a script ``python _x11_clip.py serve <abs-path>`` — the background
|
|
18
|
+
process that keeps owning the selection until another app copies
|
|
19
|
+
something (SelectionClear) or the X connection closes. It is launched by
|
|
20
|
+
path, so this file must stay importable without the contextzip package.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
import socket
|
|
27
|
+
import struct
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
URI_LIST = "text/uri-list"
|
|
32
|
+
GNOME_COPIED = "x-special/gnome-copied-files"
|
|
33
|
+
KDE_CUT = "application/x-kde-cutselection"
|
|
34
|
+
|
|
35
|
+
_ATOM = 4 # predefined atom: ATOM
|
|
36
|
+
_SELECTION_CLEAR = 29
|
|
37
|
+
_SELECTION_REQUEST = 30
|
|
38
|
+
_SELECTION_NOTIFY = 31
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class X11Error(Exception):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _pad(n: int) -> int:
|
|
46
|
+
return (4 - n % 4) % 4
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def payloads_for(path: str) -> dict[str, bytes]:
|
|
50
|
+
"""Clipboard targets (and their data) for one file, as a file manager sets them."""
|
|
51
|
+
uri = Path(path).as_uri()
|
|
52
|
+
return {
|
|
53
|
+
URI_LIST: (uri + "\r\n").encode(),
|
|
54
|
+
GNOME_COPIED: ("copy\n" + uri).encode(),
|
|
55
|
+
KDE_CUT: b"0",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Connection
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _parse_display(display: str) -> tuple[str, int]:
|
|
65
|
+
host, sep, rest = display.rpartition(":")
|
|
66
|
+
if not sep or not rest:
|
|
67
|
+
raise X11Error(f"unusable DISPLAY {display!r}")
|
|
68
|
+
try:
|
|
69
|
+
return host, int(rest.split(".")[0])
|
|
70
|
+
except ValueError:
|
|
71
|
+
raise X11Error(f"unusable DISPLAY {display!r}") from None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _open_socket(host: str, num: int, timeout: float | None) -> socket.socket:
|
|
75
|
+
if host in ("", "unix"):
|
|
76
|
+
# Xorg/XWayland listen on a filesystem socket and (on Linux) an abstract one.
|
|
77
|
+
for addr in (f"/tmp/.X11-unix/X{num}", f"\0/tmp/.X11-unix/X{num}"):
|
|
78
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
79
|
+
sock.settimeout(timeout)
|
|
80
|
+
try:
|
|
81
|
+
sock.connect(addr)
|
|
82
|
+
return sock
|
|
83
|
+
except OSError:
|
|
84
|
+
sock.close()
|
|
85
|
+
raise X11Error(f"cannot connect to X display :{num}")
|
|
86
|
+
try:
|
|
87
|
+
return socket.create_connection((host, 6000 + num), timeout)
|
|
88
|
+
except OSError as exc:
|
|
89
|
+
raise X11Error(f"cannot connect to X display {host}:{num}") from exc
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _read_xauthority() -> list[tuple[int, bytes, bytes, bytes, bytes]]:
|
|
93
|
+
path = os.environ.get("XAUTHORITY") or os.path.join(os.path.expanduser("~"), ".Xauthority")
|
|
94
|
+
try:
|
|
95
|
+
data = Path(path).read_bytes()
|
|
96
|
+
except OSError:
|
|
97
|
+
return []
|
|
98
|
+
entries, i = [], 0
|
|
99
|
+
try:
|
|
100
|
+
while i + 2 <= len(data):
|
|
101
|
+
(family,) = struct.unpack(">H", data[i : i + 2])
|
|
102
|
+
i += 2
|
|
103
|
+
fields = []
|
|
104
|
+
for _ in range(4): # address, display number, auth name, auth data
|
|
105
|
+
(n,) = struct.unpack(">H", data[i : i + 2])
|
|
106
|
+
i += 2
|
|
107
|
+
fields.append(data[i : i + n])
|
|
108
|
+
i += n
|
|
109
|
+
entries.append((family, *fields))
|
|
110
|
+
except struct.error:
|
|
111
|
+
pass
|
|
112
|
+
return entries
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _auth_for(host: str, num: int) -> tuple[bytes, bytes]:
|
|
116
|
+
"""MIT-MAGIC-COOKIE-1 for this display from ~/.Xauthority ($XAUTHORITY), if any."""
|
|
117
|
+
cookies = [
|
|
118
|
+
e
|
|
119
|
+
for e in _read_xauthority()
|
|
120
|
+
if e[3] == b"MIT-MAGIC-COOKIE-1" and e[2] in (b"", str(num).encode())
|
|
121
|
+
]
|
|
122
|
+
local = host in ("", "unix")
|
|
123
|
+
me = socket.gethostname().encode()
|
|
124
|
+
for family, addr, _number, name, value in cookies:
|
|
125
|
+
if local and (family == 65535 or (family == 256 and addr == me)):
|
|
126
|
+
return name, value
|
|
127
|
+
if cookies: # hostname changed (containers, renamed machines): number match is enough
|
|
128
|
+
return cookies[0][3], cookies[0][4]
|
|
129
|
+
return b"", b"" # open display (xhost +local:, Xvfb -ac)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Connection:
|
|
133
|
+
"""Just enough X11 client for selection handling. Little-endian, core protocol only."""
|
|
134
|
+
|
|
135
|
+
def __init__(self, display: str | None = None, timeout: float | None = 5.0):
|
|
136
|
+
display = display if display is not None else os.environ.get("DISPLAY", "")
|
|
137
|
+
host, num = _parse_display(display)
|
|
138
|
+
self.sock = _open_socket(host, num, timeout)
|
|
139
|
+
self.sock.settimeout(timeout)
|
|
140
|
+
self.seq = 0
|
|
141
|
+
self.events: list[tuple[int, bytes]] = []
|
|
142
|
+
self._handshake(*_auth_for(host, num))
|
|
143
|
+
self._next_id = 0
|
|
144
|
+
|
|
145
|
+
# -- wire helpers -------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
def _recv(self, n: int) -> bytes:
|
|
148
|
+
buf = b""
|
|
149
|
+
while len(buf) < n:
|
|
150
|
+
chunk = self.sock.recv(n - len(buf))
|
|
151
|
+
if not chunk:
|
|
152
|
+
raise X11Error("X connection closed")
|
|
153
|
+
buf += chunk
|
|
154
|
+
return buf
|
|
155
|
+
|
|
156
|
+
def _handshake(self, auth_name: bytes, auth_data: bytes) -> None:
|
|
157
|
+
req = struct.pack("<BxHHHH2x", 0x6C, 11, 0, len(auth_name), len(auth_data))
|
|
158
|
+
req += auth_name + b"\0" * _pad(len(auth_name))
|
|
159
|
+
req += auth_data + b"\0" * _pad(len(auth_data))
|
|
160
|
+
self.sock.sendall(req)
|
|
161
|
+
status, reason_len, _maj, _min, words = struct.unpack("<BBHHH", self._recv(8))
|
|
162
|
+
body = self._recv(words * 4)
|
|
163
|
+
if status != 1:
|
|
164
|
+
reason = body[:reason_len].decode("latin-1", "replace") or "authorization failed"
|
|
165
|
+
raise X11Error(f"X server refused connection: {reason}")
|
|
166
|
+
self.id_base, self.id_mask = struct.unpack("<II", body[4:12])
|
|
167
|
+
(vendor_len,) = struct.unpack("<H", body[16:18])
|
|
168
|
+
n_formats = body[21]
|
|
169
|
+
off = 32 + vendor_len + _pad(vendor_len) + n_formats * 8
|
|
170
|
+
(self.root,) = struct.unpack("<I", body[off : off + 4])
|
|
171
|
+
|
|
172
|
+
def _request(self, opcode: int, data_byte: int, body: bytes) -> None:
|
|
173
|
+
assert len(body) % 4 == 0
|
|
174
|
+
self.sock.sendall(struct.pack("<BBH", opcode, data_byte, 1 + len(body) // 4) + body)
|
|
175
|
+
self.seq = (self.seq + 1) & 0xFFFF
|
|
176
|
+
|
|
177
|
+
def _read_packet(self) -> tuple[int, bytes]:
|
|
178
|
+
head = self._recv(32)
|
|
179
|
+
code = head[0] & 0x7F
|
|
180
|
+
if code == 1: # reply: may carry extra data
|
|
181
|
+
(extra,) = struct.unpack("<I", head[4:8])
|
|
182
|
+
head += self._recv(extra * 4)
|
|
183
|
+
return code, head
|
|
184
|
+
|
|
185
|
+
def _reply(self) -> bytes:
|
|
186
|
+
while True:
|
|
187
|
+
code, pkt = self._read_packet()
|
|
188
|
+
if code == 0:
|
|
189
|
+
raise X11Error(f"X error {pkt[1]} (request {struct.unpack('<H', pkt[2:4])[0]})")
|
|
190
|
+
if code == 1:
|
|
191
|
+
return pkt
|
|
192
|
+
self.events.append((code, pkt))
|
|
193
|
+
|
|
194
|
+
def next_event(self) -> tuple[int, bytes]:
|
|
195
|
+
"""Next event; asynchronous errors are ignored."""
|
|
196
|
+
while True:
|
|
197
|
+
if self.events:
|
|
198
|
+
return self.events.pop(0)
|
|
199
|
+
code, pkt = self._read_packet()
|
|
200
|
+
if code not in (0, 1):
|
|
201
|
+
return code, pkt
|
|
202
|
+
|
|
203
|
+
# -- requests -----------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
def intern(self, name: str) -> int:
|
|
206
|
+
raw = name.encode()
|
|
207
|
+
self._request(16, 0, struct.pack("<HH", len(raw), 0) + raw + b"\0" * _pad(len(raw)))
|
|
208
|
+
return struct.unpack("<I", self._reply()[8:12])[0]
|
|
209
|
+
|
|
210
|
+
def create_window(self) -> int:
|
|
211
|
+
"""A 1x1 InputOnly window: selections need a window to be owned from."""
|
|
212
|
+
step = self.id_mask & -self.id_mask
|
|
213
|
+
wid = self.id_base | (step * (self._next_id + 1))
|
|
214
|
+
self._next_id += 1
|
|
215
|
+
# wid, parent, x, y, w, h, border, class=InputOnly, visual=CopyFromParent, mask=0
|
|
216
|
+
self._request(1, 0, struct.pack("<IIhhHHHHII", wid, self.root, 0, 0, 1, 1, 0, 2, 0, 0))
|
|
217
|
+
return wid
|
|
218
|
+
|
|
219
|
+
def set_selection_owner(self, window: int, selection: int) -> None:
|
|
220
|
+
self._request(22, 0, struct.pack("<III", window, selection, 0)) # CurrentTime
|
|
221
|
+
|
|
222
|
+
def get_selection_owner(self, selection: int) -> int:
|
|
223
|
+
self._request(23, 0, struct.pack("<I", selection))
|
|
224
|
+
return struct.unpack("<I", self._reply()[8:12])[0]
|
|
225
|
+
|
|
226
|
+
def change_property(self, window: int, prop: int, typ: int, fmt: int, data: bytes) -> None:
|
|
227
|
+
units = len(data) // (fmt // 8)
|
|
228
|
+
body = struct.pack("<IIIB3xI", window, prop, typ, fmt, units)
|
|
229
|
+
self._request(18, 0, body + data + b"\0" * _pad(len(data))) # mode Replace
|
|
230
|
+
|
|
231
|
+
def send_selection_notify(
|
|
232
|
+
self, requestor: int, selection: int, target: int, prop: int, time: int
|
|
233
|
+
) -> None:
|
|
234
|
+
event = struct.pack("<BBHIIIII", _SELECTION_NOTIFY, 0, 0, time, requestor, selection, target, prop)
|
|
235
|
+
self._request(25, 0, struct.pack("<II", requestor, 0) + event + b"\0" * 8)
|
|
236
|
+
|
|
237
|
+
def convert_selection(self, requestor: int, selection: int, target: int, prop: int) -> None:
|
|
238
|
+
self._request(24, 0, struct.pack("<IIIII", requestor, selection, target, prop, 0))
|
|
239
|
+
|
|
240
|
+
def get_property(self, window: int, prop: int) -> tuple[int, bytes]:
|
|
241
|
+
self._request(20, 1, struct.pack("<IIIII", window, prop, 0, 0, 0x1FFFFF)) # delete=True
|
|
242
|
+
pkt = self._reply()
|
|
243
|
+
fmt = pkt[1]
|
|
244
|
+
(typ,) = struct.unpack("<I", pkt[8:12])
|
|
245
|
+
(units,) = struct.unpack("<I", pkt[16:20])
|
|
246
|
+
return typ, pkt[32 : 32 + units * (fmt // 8 if fmt else 0)]
|
|
247
|
+
|
|
248
|
+
def close(self) -> None:
|
|
249
|
+
try:
|
|
250
|
+
self.sock.close()
|
|
251
|
+
except OSError:
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# ---------------------------------------------------------------------------
|
|
256
|
+
# Reading (used to verify the copy, and by the tests)
|
|
257
|
+
# ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def read_selection(
|
|
261
|
+
target: str = URI_LIST, *, display: str | None = None, timeout: float = 2.0
|
|
262
|
+
) -> bytes | None:
|
|
263
|
+
"""Ask the current CLIPBOARD owner for *target*. None if nobody offers it."""
|
|
264
|
+
conn = Connection(display, timeout=timeout)
|
|
265
|
+
try:
|
|
266
|
+
win = conn.create_window()
|
|
267
|
+
clipboard, dest = conn.intern("CLIPBOARD"), conn.intern("CZ_CLIPBOARD_READ")
|
|
268
|
+
conn.convert_selection(win, clipboard, conn.intern(target), dest)
|
|
269
|
+
while True:
|
|
270
|
+
code, pkt = conn.next_event()
|
|
271
|
+
if code == _SELECTION_NOTIFY:
|
|
272
|
+
(prop,) = struct.unpack("<I", pkt[20:24])
|
|
273
|
+
if not prop:
|
|
274
|
+
return None
|
|
275
|
+
return conn.get_property(win, prop)[1]
|
|
276
|
+
except (OSError, X11Error):
|
|
277
|
+
return None
|
|
278
|
+
finally:
|
|
279
|
+
conn.close()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
# Owning (the background process)
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def serve(path: str, display: str | None = None, on_ready=None) -> None:
|
|
288
|
+
"""
|
|
289
|
+
Own the CLIPBOARD selection for *path* until something else takes it.
|
|
290
|
+
*on_ready* is called once ownership is confirmed (the launcher waits for it).
|
|
291
|
+
"""
|
|
292
|
+
conn = Connection(display, timeout=5.0)
|
|
293
|
+
win = conn.create_window()
|
|
294
|
+
clipboard, targets = conn.intern("CLIPBOARD"), conn.intern("TARGETS")
|
|
295
|
+
offered = {conn.intern(name): data for name, data in payloads_for(path).items()}
|
|
296
|
+
|
|
297
|
+
conn.set_selection_owner(win, clipboard)
|
|
298
|
+
if conn.get_selection_owner(clipboard) != win:
|
|
299
|
+
raise X11Error("could not become CLIPBOARD owner")
|
|
300
|
+
conn.sock.settimeout(None) # from here on: block until the server says something
|
|
301
|
+
if on_ready:
|
|
302
|
+
on_ready()
|
|
303
|
+
|
|
304
|
+
while True:
|
|
305
|
+
code, pkt = conn.next_event()
|
|
306
|
+
if code == _SELECTION_CLEAR:
|
|
307
|
+
return # someone else copied something — our job is done
|
|
308
|
+
if code != _SELECTION_REQUEST:
|
|
309
|
+
continue
|
|
310
|
+
time, _owner, requestor, selection, target, prop = struct.unpack("<IIIIII", pkt[4:28])
|
|
311
|
+
reply_prop = prop or target # obsolete clients send no property
|
|
312
|
+
if selection == clipboard and target == targets:
|
|
313
|
+
atoms = [targets, *offered]
|
|
314
|
+
conn.change_property(requestor, reply_prop, _ATOM, 32, struct.pack(f"<{len(atoms)}I", *atoms))
|
|
315
|
+
elif selection == clipboard and target in offered:
|
|
316
|
+
conn.change_property(requestor, reply_prop, target, 8, offered[target])
|
|
317
|
+
else:
|
|
318
|
+
reply_prop = 0 # refuse (TIMESTAMP, MULTIPLE, other selections, …)
|
|
319
|
+
conn.send_selection_notify(requestor, selection, target, reply_prop, time)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def main(argv: list[str]) -> int:
|
|
323
|
+
if len(argv) == 3 and argv[1] == "serve":
|
|
324
|
+
def ready() -> None:
|
|
325
|
+
sys.stdout.write("ready\n")
|
|
326
|
+
sys.stdout.flush()
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
serve(argv[2], on_ready=ready)
|
|
330
|
+
except (OSError, X11Error):
|
|
331
|
+
return 1
|
|
332
|
+
return 0
|
|
333
|
+
sys.stderr.write("usage: _x11_clip.py serve <absolute-path>\n")
|
|
334
|
+
return 2
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
if __name__ == "__main__":
|
|
338
|
+
sys.exit(main(sys.argv))
|
|
@@ -299,13 +299,16 @@ def print_ai_selection(
|
|
|
299
299
|
|
|
300
300
|
|
|
301
301
|
def print_clipboard_result(cb, *, con: Console = console) -> None:
|
|
302
|
-
"""One line
|
|
302
|
+
"""One line when the ZIP is on the clipboard; otherwise say why it isn't."""
|
|
303
303
|
if cb.tier == Tier.FILE_ON_CLIPBOARD:
|
|
304
304
|
ok("Ready to paste", con=con)
|
|
305
|
-
|
|
306
|
-
|
|
305
|
+
return
|
|
306
|
+
if cb.tier == Tier.FOLDER_OPENED:
|
|
307
|
+
warn("Couldn't copy the ZIP to the clipboard — opened its folder instead", con=con)
|
|
307
308
|
else:
|
|
308
309
|
info(cb.message, con=con)
|
|
310
|
+
if cb.hint:
|
|
311
|
+
info(cb.hint, con=con)
|
|
309
312
|
|
|
310
313
|
|
|
311
314
|
# ---------------------------------------------------------------------------
|