pygpt-net 2.6.58__py3-none-any.whl → 2.6.59__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.
@@ -6,13 +6,20 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2024.11.21 22:00:00 #
9
+ # Updated Date: 2025.09.23 07:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os.path
13
13
  import re
14
14
  import subprocess
15
- import docker
15
+ import json
16
+ import os
17
+ import platform
18
+ import time
19
+ from typing import Any, Dict, List, Optional, Tuple
20
+
21
+ from PySide6.QtGui import QGuiApplication
22
+ from PySide6.QtCore import QRect
16
23
 
17
24
  from pygpt_net.item.ctx import CtxItem
18
25
 
@@ -26,6 +33,8 @@ class Runner:
26
33
  """
27
34
  self.plugin = plugin
28
35
  self.signals = None
36
+ self._winapi = None # lazy
37
+ self._winapi_mod = None # lazy
29
38
 
30
39
  def attach_signals(self, signals):
31
40
  """
@@ -35,6 +44,9 @@ class Runner:
35
44
  """
36
45
  self.signals = signals
37
46
 
47
+ # -------------------------------
48
+ # Common helpers / logging
49
+ # -------------------------------
38
50
  def handle_result(self, stdout, stderr):
39
51
  """
40
52
  Handle result from subprocess
@@ -45,11 +57,13 @@ class Runner:
45
57
  """
46
58
  result = None
47
59
  if stdout:
48
- result = stdout.decode("utf-8")
60
+ result = stdout.decode("utf-8", errors="replace")
49
61
  self.log("STDOUT: {}".format(result))
50
62
  if stderr:
51
- result = stderr.decode("utf-8")
52
- self.log("STDERR: {}".format(result))
63
+ err = stderr.decode("utf-8", errors="replace")
64
+ # Prefer stderr if non-empty
65
+ result = err if err else result
66
+ self.log("STDERR: {}".format(err))
53
67
  if result is None:
54
68
  result = "No result (STDOUT/STDERR empty)"
55
69
  self.log(result)
@@ -64,7 +78,10 @@ class Runner:
64
78
  """
65
79
  result = None
66
80
  if response:
67
- result = response.decode('utf-8')
81
+ try:
82
+ result = response.decode('utf-8', errors="replace")
83
+ except Exception:
84
+ result = str(response)
68
85
  self.log(
69
86
  "Result: {}".format(result),
70
87
  sandbox=True,
@@ -79,12 +96,13 @@ class Runner:
79
96
  """
80
97
  return self.plugin.get_option_value('sandbox_docker')
81
98
 
82
- def get_docker(self) -> docker.client.DockerClient:
99
+ def get_docker(self) -> Any:
83
100
  """
84
101
  Get docker client
85
102
 
86
103
  :return: docker client instance
87
104
  """
105
+ import docker
88
106
  return docker.from_env()
89
107
 
90
108
  def get_volumes(self) -> dict:
@@ -113,18 +131,12 @@ class Runner:
113
131
  try:
114
132
  response = self.plugin.docker.execute(cmd)
115
133
  except Exception as e:
116
- # self.error(e)
117
134
  response = str(e).encode("utf-8")
118
135
  return response
119
136
 
120
137
  def sys_exec_host(self, ctx: CtxItem, item: dict, request: dict) -> dict:
121
138
  """
122
139
  Execute system command on host
123
-
124
- :param ctx: CtxItem
125
- :param item: command item
126
- :param request: request item
127
- :return: response dict
128
140
  """
129
141
  msg = "Executing system command: {}".format(item["params"]['command'])
130
142
  self.log(msg)
@@ -151,11 +163,6 @@ class Runner:
151
163
  def sys_exec_sandbox(self, ctx: CtxItem, item: dict, request: dict) -> dict:
152
164
  """
153
165
  Execute system command in sandbox (docker)
154
-
155
- :param ctx: CtxItem
156
- :param item: command item
157
- :param request: request item
158
- :return: response dict
159
166
  """
160
167
  msg = "Executing system command: {}".format(item["params"]['command'])
161
168
  self.log(msg, sandbox=True)
@@ -181,8 +188,9 @@ class Runner:
181
188
  if result is None:
182
189
  return ""
183
190
  img_ext = ["png", "jpg", "jpeg", "gif", "bmp", "tiff"]
184
- if result.strip().split(".")[-1].lower() in img_ext:
185
- path = self.prepare_path(result.strip().replace("file://", ""), on_host=True)
191
+ s = str(result).strip()
192
+ if any(s.lower().endswith('.' + ext) for ext in img_ext):
193
+ path = self.prepare_path(s.replace("file://", ""), on_host=True)
186
194
  if os.path.isfile(path):
187
195
  return "![Image](file://{})".format(path)
188
196
  return str(result)
@@ -190,9 +198,6 @@ class Runner:
190
198
  def is_absolute_path(self, path: str) -> bool:
191
199
  """
192
200
  Check if path is absolute
193
-
194
- :param path: path to check
195
- :return: True if absolute
196
201
  """
197
202
  return os.path.isabs(path)
198
203
 
@@ -204,6 +209,8 @@ class Runner:
204
209
  :param on_host: is on host
205
210
  :return: prepared path
206
211
  """
212
+ if not path:
213
+ return path
207
214
  if self.is_absolute_path(path):
208
215
  return path
209
216
  else:
@@ -218,8 +225,6 @@ class Runner:
218
225
  def error(self, err: any):
219
226
  """
220
227
  Log error message
221
-
222
- :param err: exception or error message
223
228
  """
224
229
  if self.signals is not None:
225
230
  self.signals.error.emit(err)
@@ -227,8 +232,6 @@ class Runner:
227
232
  def status(self, msg: str):
228
233
  """
229
234
  Send status message
230
-
231
- :param msg: status message
232
235
  """
233
236
  if self.signals is not None:
234
237
  self.signals.status.emit(msg)
@@ -236,8 +239,6 @@ class Runner:
236
239
  def debug(self, msg: any):
237
240
  """
238
241
  Log debug message
239
-
240
- :param msg: message to log
241
242
  """
242
243
  if self.signals is not None:
243
244
  self.signals.debug.emit(msg)
@@ -245,9 +246,6 @@ class Runner:
245
246
  def log(self, msg, sandbox: bool = False):
246
247
  """
247
248
  Log message to console
248
-
249
- :param msg: message to log
250
- :param sandbox: is sandbox mode
251
249
  """
252
250
  prefix = ''
253
251
  if sandbox:
@@ -256,3 +254,481 @@ class Runner:
256
254
 
257
255
  if self.signals is not None:
258
256
  self.signals.log.emit(full_msg)
257
+
258
+ # -------------------------------
259
+ # WinAPI helpers
260
+ # -------------------------------
261
+ def _ensure_windows(self):
262
+ """Ensure the platform is Windows and WinAPI enabled."""
263
+ if platform.system() != "Windows":
264
+ raise RuntimeError("Windows API is available on Microsoft Windows only.")
265
+ if not self.plugin.get_option_value("winapi_enabled"):
266
+ raise RuntimeError("WinAPI is disabled in plugin options.")
267
+
268
+ def _load_winapi_module(self):
269
+ self._ensure_windows()
270
+ if self._winapi_mod is not None:
271
+ return self._winapi_mod
272
+ try:
273
+ from . import winapi as _win
274
+ except Exception as e:
275
+ raise RuntimeError("WinAPI backend unavailable: {}".format(e))
276
+ self._winapi_mod = _win
277
+ return self._winapi_mod
278
+
279
+ def _ensure_winapi(self) -> Any:
280
+ """Get or create WinAPI helper"""
281
+ self._ensure_windows()
282
+ if self._winapi is None:
283
+ _win = self._load_winapi_module()
284
+ self._winapi = _win.WinAPI()
285
+ return self._winapi
286
+
287
+ def _to_json(self, data: Any) -> str:
288
+ return json.dumps(data, ensure_ascii=False, indent=2)
289
+
290
+ def _resolve_window(self,
291
+ hwnd: Optional[int] = None,
292
+ title: Optional[str] = None,
293
+ exact: bool = False,
294
+ visible_only: bool = True,
295
+ class_name: Optional[str] = None,
296
+ exe: Optional[str] = None,
297
+ pid: Optional[int] = None) -> Tuple[Optional[int], Optional[List[Dict]], Optional[str]]:
298
+ """
299
+ Resolve a window handle by conditions. Returns (hwnd, candidates, error)
300
+ """
301
+ w = self._ensure_winapi()
302
+ if hwnd:
303
+ if w.is_window(hwnd):
304
+ return hwnd, None, None
305
+ return None, None, f"Window handle not valid: {hwnd}"
306
+
307
+ items = w.enum_windows(visible_only=visible_only)
308
+ def matches(item):
309
+ ok = True
310
+ if title:
311
+ low = title.lower()
312
+ ok = ok and ((item["title"] == title) if exact else (low in item["title"].lower()))
313
+ if class_name:
314
+ ok = ok and (item["class_name"].lower() == class_name.lower())
315
+ if exe:
316
+ ex = (item["exe"] or "")
317
+ ok = ok and (os.path.basename(ex).lower() == os.path.basename(exe).lower())
318
+ if pid is not None:
319
+ ok = ok and (item["pid"] == int(pid))
320
+ return ok
321
+
322
+ matched = [it for it in items if matches(it)]
323
+ if len(matched) == 0:
324
+ return None, [], "No window matched."
325
+ if len(matched) > 1:
326
+ return None, matched, "Ambiguous: multiple windows matched."
327
+ return matched[0]["hwnd"], None, None
328
+
329
+ # -------------------------------
330
+ # WinAPI: window listing / info
331
+ # -------------------------------
332
+ def win_list(self,
333
+ filter_title: Optional[str] = None,
334
+ visible_only: bool = True,
335
+ limit: Optional[int] = None) -> Dict:
336
+ w = self._ensure_winapi()
337
+ items = w.enum_windows(visible_only=visible_only)
338
+ if filter_title:
339
+ low = filter_title.lower()
340
+ items = [x for x in items if low in x["title"].lower()]
341
+ if limit and limit > 0:
342
+ items = items[:limit]
343
+ return {"result": self._to_json(items), "context": f"Found: {len(items)} windows"}
344
+
345
+ def win_find(self,
346
+ title: Optional[str] = None,
347
+ class_name: Optional[str] = None,
348
+ exe: Optional[str] = None,
349
+ pid: Optional[int] = None,
350
+ exact: bool = False,
351
+ visible_only: bool = True) -> Dict:
352
+ w = self._ensure_winapi()
353
+ items = w.enum_windows(visible_only=visible_only)
354
+
355
+ def matches(it):
356
+ ok = True
357
+ if title:
358
+ low = title.lower()
359
+ ok = ok and ((it["title"] == title) if exact else (low in it["title"].lower()))
360
+ if class_name:
361
+ ok = ok and (it["class_name"].lower() == class_name.lower())
362
+ if exe:
363
+ ex = (it["exe"] or "")
364
+ ok = ok and (os.path.basename(ex).lower() == os.path.basename(exe).lower())
365
+ if pid is not None:
366
+ ok = ok and (it["pid"] == int(pid))
367
+ return ok
368
+
369
+ out = [it for it in items if matches(it)]
370
+ return {"result": self._to_json(out), "context": f"Matches: {len(out)}"}
371
+
372
+ def win_children(self, hwnd: int) -> Dict:
373
+ w = self._ensure_winapi()
374
+ if not hwnd:
375
+ return {"result": "Missing hwnd", "context": "Param 'hwnd' is required."}
376
+ kids = w.enum_child_windows(hwnd)
377
+ return {"result": self._to_json(kids), "context": f"Children: {len(kids)}"}
378
+
379
+ def win_foreground(self) -> Dict:
380
+ w = self._ensure_winapi()
381
+ hwnd = w.get_foreground_window()
382
+ if not hwnd:
383
+ return {"result": "None", "context": "No foreground window."}
384
+ info = w.get_window_info(hwnd)
385
+ return {"result": self._to_json(info), "context": f"Foreground HWND: {hwnd}"}
386
+
387
+ def win_rect(self, hwnd: Optional[int] = None, title: Optional[str] = None, exact: bool = False) -> Dict:
388
+ w = self._ensure_winapi()
389
+ handle, candidates, err = self._resolve_window(hwnd, title, exact=exact, visible_only=False)
390
+ if candidates is not None:
391
+ return {"result": self._to_json(candidates), "context": "Multiple matches. Specify 'hwnd'."}
392
+ if err:
393
+ return {"result": err, "context": err}
394
+ r = w.get_window_rect(handle)
395
+ return {"result": self._to_json(r), "context": f"Rect: {r}"}
396
+
397
+ def win_get_state(self, hwnd: Optional[int] = None, title: Optional[str] = None, exact: bool = False) -> Dict:
398
+ w = self._ensure_winapi()
399
+ handle, candidates, err = self._resolve_window(hwnd, title, exact=exact, visible_only=False)
400
+ if candidates is not None:
401
+ return {"result": self._to_json(candidates), "context": "Multiple matches. Specify 'hwnd'."}
402
+ if err:
403
+ return {"result": err, "context": err}
404
+ info = w.get_window_info(handle)
405
+ return {"result": self._to_json(info), "context": "Window state"}
406
+
407
+ # -------------------------------
408
+ # WinAPI: window control
409
+ # -------------------------------
410
+ def win_focus(self, hwnd: Optional[int] = None, title: Optional[str] = None, exact: bool = False) -> Dict:
411
+ w = self._ensure_winapi()
412
+ handle, candidates, err = self._resolve_window(hwnd, title, exact=exact, visible_only=True)
413
+ if candidates is not None:
414
+ return {"result": self._to_json(candidates), "context": "Multiple matches. Specify 'hwnd'."}
415
+ if err:
416
+ return {"result": err, "context": err}
417
+ ok, msg = w.bring_to_foreground(handle)
418
+ return {"result": "OK" if ok else "FAILED", "context": msg}
419
+
420
+ def win_move_resize(self,
421
+ hwnd: Optional[int] = None,
422
+ title: Optional[str] = None,
423
+ exact: bool = False,
424
+ x: Optional[int] = None,
425
+ y: Optional[int] = None,
426
+ width: Optional[int] = None,
427
+ height: Optional[int] = None) -> Dict:
428
+ w = self._ensure_winapi()
429
+ handle, candidates, err = self._resolve_window(hwnd, title, exact=exact, visible_only=False)
430
+ if candidates is not None:
431
+ return {"result": self._to_json(candidates), "context": "Multiple matches. Specify 'hwnd'."}
432
+ if err:
433
+ return {"result": err, "context": err}
434
+ ok, msg = w.move_resize(handle, x, y, width, height)
435
+ return {"result": "OK" if ok else "FAILED", "context": msg}
436
+
437
+ def win_minimize(self, hwnd: Optional[int] = None, title: Optional[str] = None, exact: bool = False) -> Dict:
438
+ w = self._ensure_winapi()
439
+ handle, candidates, err = self._resolve_window(hwnd, title, exact=exact, visible_only=False)
440
+ if candidates is not None:
441
+ return {"result": self._to_json(candidates), "context": "Multiple matches. Specify 'hwnd'."}
442
+ if err:
443
+ return {"result": err, "context": err}
444
+ ok, msg = w.show_window(handle, state="minimize")
445
+ return {"result": "OK" if ok else "FAILED", "context": msg}
446
+
447
+ def win_maximize(self, hwnd: Optional[int] = None, title: Optional[str] = None, exact: bool = False) -> Dict:
448
+ w = self._ensure_winapi()
449
+ handle, candidates, err = self._resolve_window(hwnd, title, exact=exact, visible_only=False)
450
+ if candidates is not None:
451
+ return {"result": self._to_json(candidates), "context": "Multiple matches. Specify 'hwnd'."}
452
+ if err:
453
+ return {"result": err, "context": err}
454
+ ok, msg = w.show_window(handle, state="maximize")
455
+ return {"result": "OK" if ok else "FAILED", "context": msg}
456
+
457
+ def win_restore(self, hwnd: Optional[int] = None, title: Optional[str] = None, exact: bool = False) -> Dict:
458
+ w = self._ensure_winapi()
459
+ handle, candidates, err = self._resolve_window(hwnd, title, exact=exact, visible_only=False)
460
+ if candidates is not None:
461
+ return {"result": self._to_json(candidates), "context": "Multiple matches. Specify 'hwnd'."}
462
+ if err:
463
+ return {"result": err, "context": err}
464
+ ok, msg = w.show_window(handle, state="restore")
465
+ return {"result": "OK" if ok else "FAILED", "context": msg}
466
+
467
+ def win_close(self, hwnd: Optional[int] = None, title: Optional[str] = None, exact: bool = False) -> Dict:
468
+ w = self._ensure_winapi()
469
+ handle, candidates, err = self._resolve_window(hwnd, title, exact=exact, visible_only=False)
470
+ if candidates is not None:
471
+ return {"result": self._to_json(candidates), "context": "Multiple matches. Specify 'hwnd'."}
472
+ if err:
473
+ return {"result": err, "context": err}
474
+ ok, msg = w.close_window(handle)
475
+ return {"result": "OK" if ok else "FAILED", "context": msg}
476
+
477
+ def win_show(self, hwnd: Optional[int] = None, title: Optional[str] = None, exact: bool = False) -> Dict:
478
+ w = self._ensure_winapi()
479
+ handle, candidates, err = self._resolve_window(hwnd, title, exact=exact, visible_only=False)
480
+ if candidates is not None:
481
+ return {"result": self._to_json(candidates), "context": "Multiple matches. Specify 'hwnd'."}
482
+ if err:
483
+ return {"result": err, "context": err}
484
+ ok, msg = w.show_window(handle, state="show")
485
+ return {"result": "OK" if ok else "FAILED", "context": msg}
486
+
487
+ def win_hide(self, hwnd: Optional[int] = None, title: Optional[str] = None, exact: bool = False) -> Dict:
488
+ w = self._ensure_winapi()
489
+ handle, candidates, err = self._resolve_window(hwnd, title, exact=exact, visible_only=False)
490
+ if candidates is not None:
491
+ return {"result": self._to_json(candidates), "context": "Multiple matches. Specify 'hwnd'."}
492
+ if err:
493
+ return {"result": err, "context": err}
494
+ ok, msg = w.show_window(handle, state="hide")
495
+ return {"result": "OK" if ok else "FAILED", "context": msg}
496
+
497
+ def win_always_on_top(self,
498
+ topmost: bool,
499
+ hwnd: Optional[int] = None,
500
+ title: Optional[str] = None,
501
+ exact: bool = False) -> Dict:
502
+ w = self._ensure_winapi()
503
+ if topmost is None:
504
+ return {"result": "Missing 'topmost'", "context": "Param 'topmost' is required (true/false)."}
505
+ handle, candidates, err = self._resolve_window(hwnd, title, exact=exact, visible_only=False)
506
+ if candidates is not None:
507
+ return {"result": self._to_json(candidates), "context": "Multiple matches. Specify 'hwnd'."}
508
+ if err:
509
+ return {"result": err, "context": err}
510
+ ok, msg = w.set_topmost(handle, bool(topmost))
511
+ return {"result": "OK" if ok else "FAILED", "context": msg}
512
+
513
+ def win_set_opacity(self,
514
+ alpha: Optional[int] = None,
515
+ opacity: Optional[float] = None,
516
+ hwnd: Optional[int] = None,
517
+ title: Optional[str] = None,
518
+ exact: bool = False) -> Dict:
519
+ w = self._ensure_winapi()
520
+ handle, candidates, err = self._resolve_window(hwnd, title, exact=exact, visible_only=False)
521
+ if candidates is not None:
522
+ return {"result": self._to_json(candidates), "context": "Multiple matches. Specify 'hwnd'."}
523
+ if err:
524
+ return {"result": err, "context": err}
525
+ if alpha is None:
526
+ if opacity is None:
527
+ return {"result": "Missing 'alpha' or 'opacity'", "context": "Provide alpha (0..255) or opacity (0..1)."}
528
+ alpha = int(max(0, min(1.0, float(opacity))) * 255.0)
529
+ else:
530
+ alpha = int(max(0, min(255, int(alpha))))
531
+ ok, msg = w.set_opacity(handle, alpha)
532
+ return {"result": "OK" if ok else "FAILED", "context": msg}
533
+
534
+ # -------------------------------
535
+ # WinAPI: screenshots
536
+ # -------------------------------
537
+ def _save_pixmap(self, pix, path: str) -> Tuple[bool, str]:
538
+ """Save QPixmap to disk, ensure dir."""
539
+ abspath = self.prepare_path(path)
540
+ try:
541
+ os.makedirs(os.path.dirname(abspath), exist_ok=True)
542
+ except Exception:
543
+ pass
544
+ ok = pix.save(abspath, "PNG")
545
+ return ok, abspath
546
+
547
+ def win_screenshot(self,
548
+ hwnd: Optional[int] = None,
549
+ title: Optional[str] = None,
550
+ exact: bool = False,
551
+ path: Optional[str] = None) -> Dict:
552
+ self._ensure_windows()
553
+ handle, candidates, err = self._resolve_window(hwnd, title, exact=exact, visible_only=False)
554
+ if candidates is not None:
555
+ return {"result": self._to_json(candidates), "context": "Multiple matches. Specify 'hwnd'."}
556
+ if err:
557
+ return {"result": err, "context": err}
558
+
559
+ if not path:
560
+ ts = time.strftime("%Y%m%d_%H%M%S")
561
+ path = f"win_screenshot_{handle}_{ts}.png"
562
+
563
+ screen = QGuiApplication.primaryScreen()
564
+ if screen is None:
565
+ return {"result": "No screen", "context": "No QScreen available. Is Qt app running?"}
566
+
567
+ pix = screen.grabWindow(int(handle))
568
+ if pix.isNull():
569
+ return {"result": "Failed", "context": "grabWindow returned null pixmap."}
570
+
571
+ ok, abspath = self._save_pixmap(pix, path)
572
+ if not ok:
573
+ return {"result": "Failed", "context": f"Could not save screenshot to: {abspath}"}
574
+ context = "SYS OUTPUT:\n--------------------------------\n" + self.parse_result(abspath)
575
+ return {"result": abspath, "context": context}
576
+
577
+ def win_area_screenshot(self,
578
+ x: int, y: int, width: int, height: int,
579
+ hwnd: Optional[int] = None,
580
+ title: Optional[str] = None,
581
+ exact: bool = False,
582
+ relative: bool = False,
583
+ path: Optional[str] = None) -> Dict:
584
+ self._ensure_windows()
585
+ if any(v is None for v in [x, y, width, height]):
586
+ return {"result": "Missing geometry", "context": "Params x,y,width,height are required."}
587
+
588
+ x0, y0 = int(x), int(y)
589
+ if relative and (hwnd or title):
590
+ handle, candidates, err = self._resolve_window(hwnd, title, exact=exact, visible_only=False)
591
+ if candidates is not None:
592
+ return {"result": self._to_json(candidates), "context": "Multiple matches. Specify 'hwnd'."}
593
+ if err:
594
+ return {"result": err, "context": err}
595
+ rect = self._ensure_winapi().get_window_rect(handle)
596
+ x0 += int(rect["left"])
597
+ y0 += int(rect["top"])
598
+
599
+ if not path:
600
+ ts = time.strftime("%Y%m%d_%H%M%S")
601
+ path = f"win_area_{x0}_{y0}_{width}x{height}_{ts}.png"
602
+
603
+ screen = QGuiApplication.primaryScreen()
604
+ if screen is None:
605
+ return {"result": "No screen", "context": "No QScreen available. Is Qt app running?"}
606
+
607
+ pix = screen.grabWindow(0, x0, y0, int(width), int(height))
608
+ if pix.isNull():
609
+ return {"result": "Failed", "context": "grabWindow returned null pixmap."}
610
+
611
+ ok, abspath = self._save_pixmap(pix, path)
612
+ if not ok:
613
+ return {"result": "Failed", "context": f"Could not save screenshot to: {abspath}"}
614
+ context = "SYS OUTPUT:\n--------------------------------\n" + self.parse_result(abspath)
615
+ return {"result": abspath, "context": context}
616
+
617
+ # -------------------------------
618
+ # WinAPI: clipboard / cursor / input / monitors
619
+ # -------------------------------
620
+ def win_clipboard_get(self) -> Dict:
621
+ cb = QGuiApplication.clipboard()
622
+ text = cb.text()
623
+ return {"result": text, "context": "Clipboard text retrieved."}
624
+
625
+ def win_clipboard_set(self, text: str) -> Dict:
626
+ cb = QGuiApplication.clipboard()
627
+ cb.setText(text or "")
628
+ return {"result": "OK", "context": "Clipboard text set."}
629
+
630
+ def win_cursor_get(self) -> Dict:
631
+ w = self._ensure_winapi()
632
+ x, y = w.get_cursor_pos()
633
+ return {"result": self._to_json({"x": x, "y": y}), "context": f"Cursor: ({x}, {y})"}
634
+
635
+ def win_cursor_set(self, x: int, y: int) -> Dict:
636
+ w = self._ensure_winapi()
637
+ ok = w.set_cursor_pos(x, y)
638
+ return {"result": "OK" if ok else "FAILED", "context": f"Set cursor to ({x}, {y})"}
639
+
640
+ def win_keys_text(self, text: str, per_char_delay_ms: Optional[int] = None) -> Dict:
641
+ self._ensure_windows()
642
+ if not text:
643
+ return {"result": "No text", "context": "Param 'text' is required."}
644
+ if per_char_delay_ms is None:
645
+ per_char_delay_ms = int(self.plugin.get_option_value("win_keys_per_char_delay_ms"))
646
+ _win = self._load_winapi_module()
647
+ sender = _win.InputSender()
648
+ sender.send_unicode_text(text, per_char_delay_ms=per_char_delay_ms)
649
+ return {"result": "OK", "context": f"Typed {len(text)} characters."}
650
+
651
+ def win_keys_send(self,
652
+ keys: List[str],
653
+ hold_ms: Optional[int] = None,
654
+ gap_ms: Optional[int] = None) -> Dict:
655
+ self._ensure_windows()
656
+ if not keys or not isinstance(keys, list):
657
+ return {"result": "No keys", "context": "Param 'keys' must be a non-empty list of key tokens."}
658
+ if hold_ms is None:
659
+ hold_ms = int(self.plugin.get_option_value("win_keys_hold_ms"))
660
+ if gap_ms is None:
661
+ gap_ms = int(self.plugin.get_option_value("win_keys_gap_ms"))
662
+ _win = self._load_winapi_module()
663
+ sender = _win.InputSender()
664
+ ok, msg = sender.send_keys(keys, hold_ms=hold_ms, gap_ms=gap_ms)
665
+ return {"result": "OK" if ok else "FAILED", "context": msg}
666
+
667
+ def win_click(self,
668
+ x: Optional[int] = None,
669
+ y: Optional[int] = None,
670
+ button: str = "left",
671
+ double: bool = False,
672
+ hwnd: Optional[int] = None,
673
+ title: Optional[str] = None,
674
+ exact: bool = False,
675
+ relative: bool = False) -> Dict:
676
+ self._ensure_windows()
677
+ if x is None or y is None:
678
+ return {"result": "Missing coords", "context": "Params 'x' and 'y' are required."}
679
+ x0, y0 = int(x), int(y)
680
+ if relative and (hwnd or title):
681
+ handle, candidates, err = self._resolve_window(hwnd, title, exact=exact, visible_only=False)
682
+ if candidates is not None:
683
+ return {"result": self._to_json(candidates), "context": "Multiple matches. Specify 'hwnd'."}
684
+ if err:
685
+ return {"result": err, "context": err}
686
+ rect = self._ensure_winapi().get_window_rect(handle)
687
+ x0 += int(rect["left"])
688
+ y0 += int(rect["top"])
689
+ _win = self._load_winapi_module()
690
+ sender = _win.InputSender()
691
+ ok, msg = sender.click_at(x0, y0, button=button, double=bool(double))
692
+ return {"result": "OK" if ok else "FAILED", "context": msg}
693
+
694
+ def win_drag(self,
695
+ x1: int, y1: int, x2: int, y2: int,
696
+ hwnd: Optional[int] = None,
697
+ title: Optional[str] = None,
698
+ exact: bool = False,
699
+ relative: bool = False,
700
+ steps: int = 20,
701
+ hold_ms: Optional[int] = None) -> Dict:
702
+ self._ensure_windows()
703
+ if any(v is None for v in [x1, y1, x2, y2]):
704
+ return {"result": "Missing coords", "context": "Params x1,y1,x2,y2 are required."}
705
+ sx, sy, ex, ey = int(x1), int(y1), int(x2), int(y2)
706
+ if relative and (hwnd or title):
707
+ handle, candidates, err = self._resolve_window(hwnd, title, exact=exact, visible_only=False)
708
+ if candidates is not None:
709
+ return {"result": self._to_json(candidates), "context": "Multiple matches. Specify 'hwnd'."}
710
+ if err:
711
+ return {"result": err, "context": err}
712
+ rect = self._ensure_winapi().get_window_rect(handle)
713
+ offx, offy = int(rect["left"]), int(rect["top"])
714
+ sx, sy, ex, ey = sx + offx, sy + offy, ex + offx, ey + offy
715
+ if hold_ms is None:
716
+ hold_ms = int(self.plugin.get_option_value("win_drag_step_delay_ms"))
717
+ _win = self._load_winapi_module()
718
+ sender = _win.InputSender()
719
+ ok, msg = sender.drag_and_drop(sx, sy, ex, ey, steps=max(1, int(steps)), step_delay_ms=int(hold_ms))
720
+ return {"result": "OK" if ok else "FAILED", "context": msg}
721
+
722
+ def win_monitors(self) -> Dict:
723
+ screens = QGuiApplication.screens()
724
+ arr = []
725
+ for i, s in enumerate(screens):
726
+ g: QRect = s.geometry()
727
+ arr.append({
728
+ "index": i,
729
+ "name": s.name(),
730
+ "geometry": {"x": g.x(), "y": g.y(), "width": g.width(), "height": g.height()},
731
+ "dpi": s.logicalDotsPerInch(),
732
+ "scale": s.devicePixelRatio(),
733
+ })
734
+ return {"result": self._to_json(arr), "context": f"Monitors: {len(arr)}"}