local-control 0.1.2__py3-none-any.whl

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.
@@ -0,0 +1,342 @@
1
+ """
2
+ Clipboard synchronization helpers for text and image payloads.
3
+
4
+ This module attempts to use native OS facilities first and falls back to an
5
+ in-memory clipboard replica when platform commands are unavailable.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import platform
12
+ import subprocess
13
+ import tempfile
14
+ import textwrap
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import Iterable, List, Optional
18
+
19
+ OS_NAME = platform.system()
20
+
21
+
22
+ @dataclass
23
+ class ClipboardData:
24
+ kind: str # "text" or "image"
25
+ data: str # text content or base64 payload
26
+ mime: Optional[str] = None
27
+
28
+
29
+ _FALLBACK_CLIPBOARD: Optional[ClipboardData] = None
30
+
31
+
32
+ def get_clipboard() -> Optional[ClipboardData]:
33
+ """
34
+ Return the clipboard contents as ClipboardData.
35
+ Attempts to retrieve images first (PNG), then text. Falls back to the
36
+ in-memory replica if OS access fails.
37
+ """
38
+ global _FALLBACK_CLIPBOARD
39
+ for getter in (_get_image_clipboard, _get_text_clipboard):
40
+ try:
41
+ data = getter()
42
+ except Exception:
43
+ data = None
44
+ if data:
45
+ _FALLBACK_CLIPBOARD = data
46
+ return data
47
+ return _FALLBACK_CLIPBOARD
48
+
49
+
50
+ def set_clipboard(data: ClipboardData) -> None:
51
+ """
52
+ Update the host clipboard with the provided payload.
53
+ """
54
+ global _FALLBACK_CLIPBOARD
55
+ handlers = {
56
+ "text": _set_text_clipboard,
57
+ "image": _set_image_clipboard,
58
+ }
59
+ handler = handlers.get(data.kind)
60
+ if not handler:
61
+ raise ValueError(f"Unsupported clipboard type: {data.kind}")
62
+ try:
63
+ handler(data)
64
+ _FALLBACK_CLIPBOARD = data
65
+ except Exception:
66
+ # Fallback to in-memory replica when OS update fails.
67
+ _FALLBACK_CLIPBOARD = data
68
+
69
+
70
+ # --------------------------------------------------------------------------- #
71
+ # Text clipboard helpers
72
+
73
+
74
+ def _get_text_clipboard() -> Optional[ClipboardData]:
75
+ text = None
76
+ if OS_NAME == "Darwin":
77
+ text = _run_command_capture(["pbpaste"])
78
+ if text is not None:
79
+ text = text.decode("utf-8", errors="replace")
80
+ elif OS_NAME == "Windows":
81
+ script = (
82
+ "Add-Type -AssemblyName System.Windows.Forms;"
83
+ '[System.Windows.Forms.Clipboard]::GetText()'
84
+ )
85
+ text = _run_command_capture(
86
+ ["powershell", "-NoProfile", "-Command", script], text_mode=True
87
+ )
88
+ else:
89
+ text = _first_success(
90
+ [
91
+ ["wl-paste", "--no-newline"],
92
+ ["xclip", "-selection", "clipboard", "-out"],
93
+ ["xsel", "--clipboard", "--output"],
94
+ ],
95
+ decode=True,
96
+ )
97
+ if text:
98
+ return ClipboardData(kind="text", data=text)
99
+ return None
100
+
101
+
102
+ def _set_text_clipboard(data: ClipboardData) -> None:
103
+ payload = data.data
104
+ if OS_NAME == "Darwin":
105
+ _run_command_capture(
106
+ ["pbcopy"],
107
+ input_data=payload.encode("utf-8"),
108
+ )
109
+ elif OS_NAME == "Windows":
110
+ script = textwrap.dedent(
111
+ """
112
+ Add-Type -AssemblyName System.Windows.Forms;
113
+ $inputText = [Console]::In.ReadToEnd();
114
+ [System.Windows.Forms.Clipboard]::SetText($inputText);
115
+ """
116
+ )
117
+ _run_command_capture(
118
+ ["powershell", "-NoProfile", "-Command", script],
119
+ input_data=payload,
120
+ text_mode=True,
121
+ )
122
+ else:
123
+ commands = [
124
+ ["wl-copy"],
125
+ ["xclip", "-selection", "clipboard"],
126
+ ["xsel", "--clipboard", "--input"],
127
+ ]
128
+ for cmd in commands:
129
+ result = _run_command_capture(cmd, input_data=payload.encode("utf-8"))
130
+ if result is not None:
131
+ return
132
+ raise RuntimeError("Failed to set clipboard text via wl-copy/xclip/xsel.")
133
+
134
+
135
+ # --------------------------------------------------------------------------- #
136
+ # Image clipboard helpers (PNG base64)
137
+
138
+
139
+ def _get_image_clipboard() -> Optional[ClipboardData]:
140
+ if OS_NAME == "Darwin":
141
+ return _mac_get_image()
142
+ if OS_NAME == "Windows":
143
+ return _windows_get_image()
144
+ if OS_NAME == "Linux":
145
+ return _linux_get_image()
146
+ return None
147
+
148
+
149
+ def _set_image_clipboard(data: ClipboardData) -> None:
150
+ if OS_NAME == "Darwin":
151
+ _mac_set_image(data)
152
+ elif OS_NAME == "Windows":
153
+ _windows_set_image(data)
154
+ elif OS_NAME == "Linux":
155
+ _linux_set_image(data)
156
+ else:
157
+ raise RuntimeError("Image clipboard unsupported on this platform.")
158
+
159
+
160
+ def _mac_get_image() -> Optional[ClipboardData]:
161
+ script = textwrap.dedent(
162
+ """
163
+ on run argv
164
+ set outPath to POSIX file (item 1 of argv)
165
+ try
166
+ set theData to the clipboard as «class PNGf»
167
+ on error
168
+ return ""
169
+ end try
170
+ set fileRef to open for access outPath with write permission
171
+ set eof of fileRef to 0
172
+ write theData to fileRef
173
+ close access fileRef
174
+ return POSIX path of outPath
175
+ end run
176
+ """
177
+ )
178
+ with tempfile.TemporaryDirectory() as tmp:
179
+ script_path = Path(tmp) / "extract_clipboard.scpt"
180
+ script_path.write_text(script, encoding="utf-8")
181
+ output_path = Path(tmp) / "clipboard.png"
182
+ result = _run_command_capture(
183
+ ["osascript", str(script_path), str(output_path)],
184
+ text_mode=True,
185
+ )
186
+ if not result:
187
+ return None
188
+ if not output_path.exists():
189
+ return None
190
+ data = output_path.read_bytes()
191
+ encoded = base64.b64encode(data).decode("ascii")
192
+ return ClipboardData(kind="image", data=encoded, mime="image/png")
193
+
194
+
195
+ def _mac_set_image(data: ClipboardData) -> None:
196
+ if data.mime and data.mime != "image/png":
197
+ raise ValueError("macOS clipboard only accepts PNG payloads in this implementation.")
198
+ raw = base64.b64decode(data.data)
199
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
200
+ tmp.write(raw)
201
+ tmp_path = Path(tmp.name)
202
+ script = textwrap.dedent(
203
+ """
204
+ on run argv
205
+ set inPath to POSIX file (item 1 of argv)
206
+ set the clipboard to (read inPath as «class PNGf»)
207
+ end run
208
+ """
209
+ )
210
+ script_path = tmp_path.with_suffix(".scpt")
211
+ try:
212
+ script_path.write_text(script, encoding="utf-8")
213
+ _run_command_capture(["osascript", str(script_path), str(tmp_path)], text_mode=True)
214
+ finally:
215
+ if tmp_path.exists():
216
+ tmp_path.unlink()
217
+ if script_path.exists():
218
+ script_path.unlink()
219
+
220
+
221
+ def _windows_get_image() -> Optional[ClipboardData]:
222
+ script = textwrap.dedent(
223
+ """
224
+ Add-Type -AssemblyName System.Windows.Forms
225
+ Add-Type -AssemblyName System.Drawing
226
+ $image = [Windows.Forms.Clipboard]::GetImage()
227
+ if ($image -eq $null) { return "" }
228
+ $path = [System.IO.Path]::GetTempFileName() + ".png"
229
+ $image.Save($path, [System.Drawing.Imaging.ImageFormat]::Png)
230
+ Write-Output $path
231
+ """
232
+ )
233
+ result = _run_command_capture(
234
+ ["powershell", "-NoProfile", "-Command", script],
235
+ text_mode=True,
236
+ )
237
+ if not result:
238
+ return None
239
+ path = Path(result.strip())
240
+ if not path.exists():
241
+ return None
242
+ data = path.read_bytes()
243
+ try:
244
+ path.unlink()
245
+ except OSError:
246
+ pass
247
+ encoded = base64.b64encode(data).decode("ascii")
248
+ return ClipboardData(kind="image", data=encoded, mime="image/png")
249
+
250
+
251
+ def _windows_set_image(data: ClipboardData) -> None:
252
+ if data.mime and data.mime != "image/png":
253
+ raise ValueError("Windows clipboard helper expects PNG payloads.")
254
+ raw = base64.b64decode(data.data)
255
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
256
+ tmp.write(raw)
257
+ tmp_path = Path(tmp.name)
258
+ script = textwrap.dedent(
259
+ """
260
+ Param([string]$imagePath)
261
+ Add-Type -AssemblyName System.Windows.Forms
262
+ Add-Type -AssemblyName System.Drawing
263
+ $img = [System.Drawing.Image]::FromFile($imagePath)
264
+ [Windows.Forms.Clipboard]::SetImage($img)
265
+ $img.Dispose()
266
+ """
267
+ )
268
+ try:
269
+ _run_command_capture(
270
+ ["powershell", "-NoProfile", "-Command", script, str(tmp_path)],
271
+ text_mode=True,
272
+ )
273
+ finally:
274
+ if tmp_path.exists():
275
+ tmp_path.unlink()
276
+
277
+
278
+ def _linux_get_image() -> Optional[ClipboardData]:
279
+ commands = [
280
+ ["wl-paste", "--type", "image/png"],
281
+ ["xclip", "-selection", "clipboard", "-t", "image/png", "-out"],
282
+ ]
283
+ for cmd in commands:
284
+ blob = _run_command_capture(cmd)
285
+ if blob:
286
+ encoded = base64.b64encode(blob).decode("ascii")
287
+ return ClipboardData(kind="image", data=encoded, mime="image/png")
288
+ return None
289
+
290
+
291
+ def _linux_set_image(data: ClipboardData) -> None:
292
+ payload = base64.b64decode(data.data)
293
+ commands = [
294
+ ["wl-copy", "--type", "image/png"],
295
+ ["xclip", "-selection", "clipboard", "-t", "image/png", "-in"],
296
+ ]
297
+ for cmd in commands:
298
+ result = _run_command_capture(cmd, input_data=payload)
299
+ if result is not None:
300
+ return
301
+ raise RuntimeError("No clipboard image utility (wl-copy/xclip) available.")
302
+
303
+
304
+ # --------------------------------------------------------------------------- #
305
+ # Utility helpers
306
+
307
+
308
+ def _run_command_capture(
309
+ command: Iterable[str],
310
+ input_data: Optional[bytes | str] = None,
311
+ text_mode: bool = False,
312
+ timeout: int = 5,
313
+ ) -> Optional[str | bytes]:
314
+ if text_mode and isinstance(input_data, bytes):
315
+ input_value: Optional[bytes | str] = input_data.decode("utf-8", errors="ignore")
316
+ else:
317
+ input_value = input_data
318
+ try:
319
+ result = subprocess.run(
320
+ list(command),
321
+ input=input_value,
322
+ stdout=subprocess.PIPE,
323
+ stderr=subprocess.DEVNULL,
324
+ timeout=timeout,
325
+ check=False,
326
+ text=text_mode,
327
+ )
328
+ except (FileNotFoundError, subprocess.SubprocessError, TimeoutError):
329
+ return None
330
+ if result.returncode != 0:
331
+ return None
332
+ return result.stdout
333
+
334
+
335
+ def _first_success(commands: Iterable[List[str]], decode: bool = False) -> Optional[str]:
336
+ for cmd in commands:
337
+ output = _run_command_capture(cmd)
338
+ if output:
339
+ if decode:
340
+ return output.decode("utf-8", errors="replace")
341
+ return output
342
+ return None
@@ -0,0 +1,47 @@
1
+ """
2
+ Configuration helpers for locating persistent storage and secrets.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import os
9
+ import platform
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ APP_NAME = "local_control"
14
+
15
+
16
+ def data_dir() -> Path:
17
+ """
18
+ Return the directory used to persist trusted device metadata.
19
+ The directory is created lazily when first accessed.
20
+ """
21
+ system = platform.system()
22
+ if system == "Windows":
23
+ base = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
24
+ elif system == "Darwin":
25
+ base = Path.home() / "Library" / "Application Support"
26
+ else:
27
+ base = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
28
+
29
+ target = base / APP_NAME
30
+ target.mkdir(parents=True, exist_ok=True)
31
+ return target
32
+
33
+
34
+ def load_json(path: Path, default: Any) -> Any:
35
+ if not path.exists():
36
+ return default
37
+ try:
38
+ return json.loads(path.read_text(encoding="utf-8"))
39
+ except json.JSONDecodeError:
40
+ return default
41
+
42
+
43
+ def save_json(path: Path, payload: Any) -> None:
44
+ path.parent.mkdir(parents=True, exist_ok=True)
45
+ tmp_path = path.with_suffix(".tmp")
46
+ tmp_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
47
+ tmp_path.replace(path)