pygpt-net 2.6.58__py3-none-any.whl → 2.6.60__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.
- pygpt_net/CHANGELOG.txt +10 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +9 -5
- pygpt_net/controller/__init__.py +1 -0
- pygpt_net/controller/presets/editor.py +442 -39
- pygpt_net/core/agents/custom/__init__.py +275 -0
- pygpt_net/core/agents/custom/debug.py +64 -0
- pygpt_net/core/agents/custom/factory.py +109 -0
- pygpt_net/core/agents/custom/graph.py +71 -0
- pygpt_net/core/agents/custom/llama_index/__init__.py +10 -0
- pygpt_net/core/agents/custom/llama_index/factory.py +89 -0
- pygpt_net/core/agents/custom/llama_index/router_streamer.py +106 -0
- pygpt_net/core/agents/custom/llama_index/runner.py +529 -0
- pygpt_net/core/agents/custom/llama_index/stream.py +56 -0
- pygpt_net/core/agents/custom/llama_index/utils.py +242 -0
- pygpt_net/core/agents/custom/logging.py +50 -0
- pygpt_net/core/agents/custom/memory.py +51 -0
- pygpt_net/core/agents/custom/router.py +116 -0
- pygpt_net/core/agents/custom/router_streamer.py +187 -0
- pygpt_net/core/agents/custom/runner.py +454 -0
- pygpt_net/core/agents/custom/schema.py +125 -0
- pygpt_net/core/agents/custom/utils.py +181 -0
- pygpt_net/core/agents/provider.py +72 -7
- pygpt_net/core/agents/runner.py +7 -4
- pygpt_net/core/agents/runners/helpers.py +1 -1
- pygpt_net/core/agents/runners/llama_workflow.py +3 -0
- pygpt_net/core/agents/runners/openai_workflow.py +8 -1
- pygpt_net/core/filesystem/parser.py +37 -24
- pygpt_net/{ui/widget/builder → core/node_editor}/__init__.py +2 -2
- pygpt_net/core/{builder → node_editor}/graph.py +11 -218
- pygpt_net/core/node_editor/models.py +111 -0
- pygpt_net/core/node_editor/types.py +76 -0
- pygpt_net/core/node_editor/utils.py +17 -0
- pygpt_net/core/render/web/renderer.py +10 -8
- pygpt_net/data/config/config.json +3 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/locale/locale.en.ini +4 -4
- pygpt_net/data/locale/plugin.cmd_system.en.ini +68 -0
- pygpt_net/item/agent.py +5 -1
- pygpt_net/item/preset.py +19 -1
- pygpt_net/plugin/cmd_system/config.py +377 -1
- pygpt_net/plugin/cmd_system/plugin.py +52 -8
- pygpt_net/plugin/cmd_system/runner.py +508 -32
- pygpt_net/plugin/cmd_system/winapi.py +481 -0
- pygpt_net/plugin/cmd_system/worker.py +88 -15
- pygpt_net/provider/agents/base.py +33 -2
- pygpt_net/provider/agents/llama_index/flow_from_schema.py +92 -0
- pygpt_net/provider/agents/llama_index/workflow/supervisor.py +0 -0
- pygpt_net/provider/agents/openai/flow_from_schema.py +96 -0
- pygpt_net/provider/core/agent/json_file.py +11 -5
- pygpt_net/provider/llms/openai.py +6 -4
- pygpt_net/tools/agent_builder/tool.py +217 -52
- pygpt_net/tools/agent_builder/ui/dialogs.py +119 -24
- pygpt_net/tools/agent_builder/ui/list.py +37 -10
- pygpt_net/tools/code_interpreter/ui/html.py +2 -1
- pygpt_net/ui/dialog/preset.py +16 -1
- pygpt_net/ui/main.py +1 -1
- pygpt_net/{core/builder → ui/widget/node_editor}/__init__.py +2 -2
- pygpt_net/ui/widget/node_editor/command.py +373 -0
- pygpt_net/ui/widget/node_editor/editor.py +2038 -0
- pygpt_net/ui/widget/node_editor/item.py +492 -0
- pygpt_net/ui/widget/node_editor/node.py +1205 -0
- pygpt_net/ui/widget/node_editor/utils.py +17 -0
- pygpt_net/ui/widget/node_editor/view.py +247 -0
- pygpt_net/ui/widget/textarea/web.py +1 -1
- {pygpt_net-2.6.58.dist-info → pygpt_net-2.6.60.dist-info}/METADATA +135 -61
- {pygpt_net-2.6.58.dist-info → pygpt_net-2.6.60.dist-info}/RECORD +69 -42
- pygpt_net/core/agents/custom.py +0 -150
- pygpt_net/ui/widget/builder/editor.py +0 -2001
- {pygpt_net-2.6.58.dist-info → pygpt_net-2.6.60.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.58.dist-info → pygpt_net-2.6.60.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.58.dist-info → pygpt_net-2.6.60.dist-info}/entry_points.txt +0 -0
|
@@ -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:
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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) ->
|
|
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
|
-
|
|
185
|
-
|
|
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 "".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)}"}
|