@aryacode/dsh-engineering 0.0.0-stage → 0.4.0
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.
- package/CHANGELOG.md +128 -0
- package/LICENSE +21 -0
- package/README.md +230 -2
- package/cordis.patch.yml +123 -0
- package/lib/client.js +3448 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +9 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +94 -3
- package/vendor/ftview/README.md +55 -0
- package/vendor/ftview/SKILL.md +74 -0
- package/vendor/ftview/lib/checks.d.ts +27 -0
- package/vendor/ftview/lib/checks.js +39 -0
- package/vendor/ftview/lib/core.d.ts +208 -0
- package/vendor/ftview/lib/core.js +160 -0
- package/vendor/ftview/lib/edit/drafts.d.ts +49 -0
- package/vendor/ftview/lib/edit/drafts.js +84 -0
- package/vendor/ftview/lib/edit/operations.d.ts +62 -0
- package/vendor/ftview/lib/edit/operations.js +126 -0
- package/vendor/ftview/lib/edit/prepare.d.ts +38 -0
- package/vendor/ftview/lib/edit/prepare.js +133 -0
- package/vendor/ftview/lib/edit/tools.d.ts +25 -0
- package/vendor/ftview/lib/edit/tools.js +198 -0
- package/vendor/ftview/lib/edit.d.ts +30 -0
- package/vendor/ftview/lib/edit.js +38 -0
- package/vendor/ftview/lib/index.d.ts +31 -0
- package/vendor/ftview/lib/index.js +36 -0
- package/vendor/ftview/lib/model.d.ts +80 -0
- package/vendor/ftview/lib/model.js +267 -0
- package/vendor/ftview/lib/session.d.ts +48 -0
- package/vendor/ftview/lib/session.js +98 -0
- package/vendor/ftview/lib/skill.d.ts +29 -0
- package/vendor/ftview/lib/skill.js +43 -0
- package/vendor/ftview/lib/studio/discover.d.ts +36 -0
- package/vendor/ftview/lib/studio/discover.js +87 -0
- package/vendor/ftview/lib/studio/preflight.d.ts +51 -0
- package/vendor/ftview/lib/studio/preflight.js +64 -0
- package/vendor/ftview/lib/studio/tools.d.ts +15 -0
- package/vendor/ftview/lib/studio/tools.js +83 -0
- package/vendor/ftview/lib/studio.d.ts +30 -0
- package/vendor/ftview/lib/studio.js +37 -0
- package/vendor/ftview/lib/tools.d.ts +14 -0
- package/vendor/ftview/lib/tools.js +202 -0
- package/vendor/l5x/README.md +62 -0
- package/vendor/l5x/SKILL.md +81 -0
- package/vendor/l5x/lib/approvals.d.ts +50 -0
- package/vendor/l5x/lib/approvals.js +93 -0
- package/vendor/l5x/lib/client.js +412 -0
- package/vendor/l5x/lib/client.js.map +1 -0
- package/vendor/l5x/lib/core/errors.d.ts +38 -0
- package/vendor/l5x/lib/core/errors.js +48 -0
- package/vendor/l5x/lib/core/params.d.ts +49 -0
- package/vendor/l5x/lib/core/params.js +39 -0
- package/vendor/l5x/lib/core/result.d.ts +46 -0
- package/vendor/l5x/lib/core/result.js +21 -0
- package/vendor/l5x/lib/core/tool.d.ts +102 -0
- package/vendor/l5x/lib/core/tool.js +19 -0
- package/vendor/l5x/lib/drafts.d.ts +66 -0
- package/vendor/l5x/lib/drafts.js +98 -0
- package/vendor/l5x/lib/editing.d.ts +30 -0
- package/vendor/l5x/lib/editing.js +42 -0
- package/vendor/l5x/lib/engine/acd-edit.d.ts +13 -0
- package/vendor/l5x/lib/engine/acd-edit.js +115 -0
- package/vendor/l5x/lib/engine/analysis.d.ts +22 -0
- package/vendor/l5x/lib/engine/analysis.js +258 -0
- package/vendor/l5x/lib/engine/capabilities.d.ts +9 -0
- package/vendor/l5x/lib/engine/capabilities.js +40 -0
- package/vendor/l5x/lib/engine/change-set.d.ts +122 -0
- package/vendor/l5x/lib/engine/change-set.js +446 -0
- package/vendor/l5x/lib/engine/configuration.d.ts +21 -0
- package/vendor/l5x/lib/engine/configuration.js +42 -0
- package/vendor/l5x/lib/engine/details.d.ts +47 -0
- package/vendor/l5x/lib/engine/details.js +173 -0
- package/vendor/l5x/lib/engine/direct-polling.d.ts +91 -0
- package/vendor/l5x/lib/engine/direct-polling.js +193 -0
- package/vendor/l5x/lib/engine/download-review.d.ts +15 -0
- package/vendor/l5x/lib/engine/download-review.js +63 -0
- package/vendor/l5x/lib/engine/logic-syntax.d.ts +60 -0
- package/vendor/l5x/lib/engine/logic-syntax.js +237 -0
- package/vendor/l5x/lib/engine/observations.d.ts +41 -0
- package/vendor/l5x/lib/engine/observations.js +67 -0
- package/vendor/l5x/lib/engine/project-analysis.d.ts +131 -0
- package/vendor/l5x/lib/engine/project-analysis.js +314 -0
- package/vendor/l5x/lib/engine/review-evidence.d.ts +8 -0
- package/vendor/l5x/lib/engine/review-evidence.js +49 -0
- package/vendor/l5x/lib/engine/review.d.ts +18 -0
- package/vendor/l5x/lib/engine/review.js +123 -0
- package/vendor/l5x/lib/engine/studio-sdk.d.ts +101 -0
- package/vendor/l5x/lib/engine/studio-sdk.js +571 -0
- package/vendor/l5x/lib/engine/tag-resolution.d.ts +20 -0
- package/vendor/l5x/lib/engine/tag-resolution.js +169 -0
- package/vendor/l5x/lib/engine/usage.d.ts +16 -0
- package/vendor/l5x/lib/engine/usage.js +92 -0
- package/vendor/l5x/lib/engine/xml-regions.d.ts +23 -0
- package/vendor/l5x/lib/engine/xml-regions.js +77 -0
- package/vendor/l5x/lib/fs.d.ts +17 -0
- package/vendor/l5x/lib/fs.js +36 -0
- package/vendor/l5x/lib/index.d.ts +36 -0
- package/vendor/l5x/lib/index.js +42 -0
- package/vendor/l5x/lib/live.d.ts +48 -0
- package/vendor/l5x/lib/live.js +48 -0
- package/vendor/l5x/lib/model/aois.d.ts +39 -0
- package/vendor/l5x/lib/model/aois.js +112 -0
- package/vendor/l5x/lib/model/errors.d.ts +15 -0
- package/vendor/l5x/lib/model/errors.js +18 -0
- package/vendor/l5x/lib/model/index.d.ts +20 -0
- package/vendor/l5x/lib/model/index.js +19 -0
- package/vendor/l5x/lib/model/module.d.ts +37 -0
- package/vendor/l5x/lib/model/module.js +54 -0
- package/vendor/l5x/lib/model/modules.d.ts +24 -0
- package/vendor/l5x/lib/model/modules.js +55 -0
- package/vendor/l5x/lib/model/parse.d.ts +32 -0
- package/vendor/l5x/lib/model/parse.js +116 -0
- package/vendor/l5x/lib/model/project.d.ts +15 -0
- package/vendor/l5x/lib/model/project.js +31 -0
- package/vendor/l5x/lib/model/routines.d.ts +71 -0
- package/vendor/l5x/lib/model/routines.js +183 -0
- package/vendor/l5x/lib/model/tags.d.ts +51 -0
- package/vendor/l5x/lib/model/tags.js +119 -0
- package/vendor/l5x/lib/model/types.d.ts +304 -0
- package/vendor/l5x/lib/model/types.js +12 -0
- package/vendor/l5x/lib/model/udts.d.ts +34 -0
- package/vendor/l5x/lib/model/udts.js +81 -0
- package/vendor/l5x/lib/model/xml.d.ts +139 -0
- package/vendor/l5x/lib/model/xml.js +291 -0
- package/vendor/l5x/lib/model/xref.d.ts +28 -0
- package/vendor/l5x/lib/model/xref.js +154 -0
- package/vendor/l5x/lib/session.d.ts +119 -0
- package/vendor/l5x/lib/session.js +236 -0
- package/vendor/l5x/lib/skill.d.ts +30 -0
- package/vendor/l5x/lib/skill.js +44 -0
- package/vendor/l5x/lib/studio-state.d.ts +77 -0
- package/vendor/l5x/lib/studio-state.js +79 -0
- package/vendor/l5x/lib/studio.d.ts +29 -0
- package/vendor/l5x/lib/studio.js +40 -0
- package/vendor/l5x/lib/tools/checks.d.ts +19 -0
- package/vendor/l5x/lib/tools/checks.js +140 -0
- package/vendor/l5x/lib/tools/common.d.ts +41 -0
- package/vendor/l5x/lib/tools/common.js +84 -0
- package/vendor/l5x/lib/tools/controller.d.ts +21 -0
- package/vendor/l5x/lib/tools/controller.js +170 -0
- package/vendor/l5x/lib/tools/drafts.d.ts +23 -0
- package/vendor/l5x/lib/tools/drafts.js +270 -0
- package/vendor/l5x/lib/tools/live.d.ts +21 -0
- package/vendor/l5x/lib/tools/live.js +175 -0
- package/vendor/l5x/lib/tools/read.d.ts +19 -0
- package/vendor/l5x/lib/tools/read.js +245 -0
- package/vendor/l5x/lib/tools/references.d.ts +20 -0
- package/vendor/l5x/lib/tools/references.js +269 -0
- package/vendor/l5x/lib/tools/studio.d.ts +21 -0
- package/vendor/l5x/lib/tools/studio.js +193 -0
- package/vendor/l5x/poll-worker.mjs +77 -0
- package/vendor/l5x/studio-bridge/NuGet.Config +1 -0
- package/vendor/l5x/studio-bridge/Program.cs +304 -0
- package/vendor/l5x/studio-bridge/StudioBridge.csproj +11 -0
- package/vendor/mwi/README.md +60 -0
- package/vendor/mwi/SKILL.md +87 -0
- package/vendor/mwi/bridge/README.md +8 -0
- package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -0
- package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -0
- package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -0
- package/vendor/mwi/bridge/start_bridge.cmd +21 -0
- package/vendor/mwi/bridge/watch_dialogs.cmd +13 -0
- package/vendor/mwi/bridge/watch_dialogs.ps1 +153 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__init__.py +14 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/block_interfaces.py +275 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/capture.py +448 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/com.py +268 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/datatypes.py +204 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/descriptions.py +30 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/eip.py +292 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/errors.py +36 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/graphical_listing.py +208 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/grid.py +227 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +317 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/iec.py +317 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/installed_help.py +151 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/knowledge.py +218 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/libraries.py +250 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/navigation.py +44 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/pou_writer.py +1547 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/program_checks.py +360 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/project.py +704 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/server.py +1913 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/snapshot.py +138 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/st_comments.py +25 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +179 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +676 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/structure.py +98 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/task_bindings.py +68 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tasks.py +162 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/transaction.py +157 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree.py +563 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/uia.py +465 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/validation.py +41 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/variable_edit.py +183 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/variables.py +472 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/workflow.py +130 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +666 -0
- package/vendor/mwi/code/mw_code.py +1258 -0
- package/vendor/mwi/legacy/edit-session.js +50 -0
- package/vendor/mwi/legacy/fb-insertion.js +74 -0
- package/vendor/mwi/legacy/native-code.js +83 -0
- package/vendor/mwi/legacy/native-groups.js +75 -0
- package/vendor/mwi/legacy/native-structure.js +143 -0
- package/vendor/mwi/legacy/native-variables.js +77 -0
- package/vendor/mwi/legacy/pou-conversion.js +47 -0
- package/vendor/mwi/legacy/pou-package.js +66 -0
- package/vendor/mwi/lib/core/bridge.d.ts +71 -0
- package/vendor/mwi/lib/core/bridge.js +53 -0
- package/vendor/mwi/lib/core/code-engine.d.ts +47 -0
- package/vendor/mwi/lib/core/code-engine.js +109 -0
- package/vendor/mwi/lib/core/errors.d.ts +37 -0
- package/vendor/mwi/lib/core/errors.js +47 -0
- package/vendor/mwi/lib/core/params.d.ts +52 -0
- package/vendor/mwi/lib/core/params.js +46 -0
- package/vendor/mwi/lib/core/ps-bridge.d.ts +62 -0
- package/vendor/mwi/lib/core/ps-bridge.js +127 -0
- package/vendor/mwi/lib/core/result.d.ts +45 -0
- package/vendor/mwi/lib/core/result.js +20 -0
- package/vendor/mwi/lib/core/tool.d.ts +80 -0
- package/vendor/mwi/lib/core/tool.js +55 -0
- package/vendor/mwi/lib/edit/compare.d.ts +33 -0
- package/vendor/mwi/lib/edit/compare.js +70 -0
- package/vendor/mwi/lib/edit.d.ts +20 -0
- package/vendor/mwi/lib/edit.js +32 -0
- package/vendor/mwi/lib/index.d.ts +21 -0
- package/vendor/mwi/lib/index.js +33 -0
- package/vendor/mwi/lib/read/service.d.ts +117 -0
- package/vendor/mwi/lib/read/service.js +229 -0
- package/vendor/mwi/lib/read.d.ts +21 -0
- package/vendor/mwi/lib/read.js +33 -0
- package/vendor/mwi/lib/session/project.d.ts +34 -0
- package/vendor/mwi/lib/session/project.js +54 -0
- package/vendor/mwi/lib/session/service.d.ts +167 -0
- package/vendor/mwi/lib/session/service.js +566 -0
- package/vendor/mwi/lib/session/stage.d.ts +84 -0
- package/vendor/mwi/lib/session/stage.js +187 -0
- package/vendor/mwi/lib/session/workspace.d.ts +52 -0
- package/vendor/mwi/lib/session/workspace.js +90 -0
- package/vendor/mwi/lib/skill.d.ts +36 -0
- package/vendor/mwi/lib/skill.js +56 -0
- package/vendor/mwi/lib/tools/edit.d.ts +20 -0
- package/vendor/mwi/lib/tools/edit.js +286 -0
- package/vendor/mwi/lib/tools/read.d.ts +19 -0
- package/vendor/mwi/lib/tools/read.js +161 -0
- package/vendor/mwi/lib/tools/session.d.ts +20 -0
- package/vendor/mwi/lib/tools/session.js +166 -0
- package/vendor/mwi/lib/tools/verify.d.ts +19 -0
- package/vendor/mwi/lib/tools/verify.js +85 -0
- package/vendor/mwi/lib/verify.d.ts +17 -0
- package/vendor/mwi/lib/verify.js +29 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""Screenshot capture: screen, a window, or the desktop, for agent observation.
|
|
2
|
+
|
|
3
|
+
An agent driving a GUI has to be able to *see* it. This module provides the eyes:
|
|
4
|
+
capture the whole virtual desktop, or one window found by title substring or handle.
|
|
5
|
+
Images are written to PNG under a scratch directory and the **path** is returned, so
|
|
6
|
+
the caller can attach the file rather than receive a wall of base64.
|
|
7
|
+
|
|
8
|
+
Pillow does the encoding, and ``ImageGrab`` handles the multi-monitor case --
|
|
9
|
+
measured on this machine, the virtual desktop is 6400x1615 across three screens, so
|
|
10
|
+
"grab the primary screen" would miss most of it.
|
|
11
|
+
|
|
12
|
+
Everything here is read-only: it captures pixels and never sends input. Input lives in
|
|
13
|
+
:mod:`motionworks_iec_mcp.uia`, deliberately separate, because looking and touching
|
|
14
|
+
carry very different risk.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
#: Where captures are written. Kept in the workspace so the agent can read them.
|
|
24
|
+
CAPTURE_DIR = Path(__file__).resolve().parents[2] / ".mcp_captures"
|
|
25
|
+
|
|
26
|
+
#: How many captures to keep. A full-desktop grab of this three-monitor setup is
|
|
27
|
+
#: about 4 MB, so an unbounded directory would grow by that much per screenshot and
|
|
28
|
+
#: fill the workspace during a long session.
|
|
29
|
+
KEEP_CAPTURES = 40
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CaptureError(RuntimeError):
|
|
33
|
+
"""A screenshot could not be taken."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _prepare() -> None:
|
|
37
|
+
CAPTURE_DIR.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def prune(keep: int = KEEP_CAPTURES) -> int:
|
|
41
|
+
"""Delete the oldest captures beyond ``keep``. Returns how many were removed.
|
|
42
|
+
|
|
43
|
+
Called after every capture. Screenshots are cheap to retake and only useful while
|
|
44
|
+
investigating something, so keeping the newest few is the right trade against
|
|
45
|
+
filling the workspace.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
files = sorted(
|
|
49
|
+
(p for p in CAPTURE_DIR.glob("*.png") if p.is_file()),
|
|
50
|
+
key=lambda p: p.stat().st_mtime,
|
|
51
|
+
reverse=True,
|
|
52
|
+
)
|
|
53
|
+
except OSError:
|
|
54
|
+
return 0
|
|
55
|
+
removed = 0
|
|
56
|
+
for path in files[keep:]:
|
|
57
|
+
try:
|
|
58
|
+
path.unlink()
|
|
59
|
+
removed += 1
|
|
60
|
+
except OSError:
|
|
61
|
+
continue
|
|
62
|
+
return removed
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def capture_screen(name: str = "screen") -> Path:
|
|
66
|
+
"""Capture every monitor into one image and return the file path."""
|
|
67
|
+
if sys.platform != "win32":
|
|
68
|
+
raise CaptureError("screen capture is only implemented on Windows")
|
|
69
|
+
try:
|
|
70
|
+
from PIL import ImageGrab
|
|
71
|
+
except ImportError as exc: # pragma: no cover
|
|
72
|
+
raise CaptureError(f"Pillow is required for capture: {exc}") from exc
|
|
73
|
+
|
|
74
|
+
_prepare()
|
|
75
|
+
try:
|
|
76
|
+
image = ImageGrab.grab(all_screens=True)
|
|
77
|
+
except Exception as exc: # noqa: BLE001
|
|
78
|
+
raise CaptureError(f"could not capture the screen: {exc}") from exc
|
|
79
|
+
|
|
80
|
+
path = CAPTURE_DIR / f"{name}-{int(time.time())}.png"
|
|
81
|
+
image.save(path)
|
|
82
|
+
prune()
|
|
83
|
+
return path
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def list_windows(min_width: int = 40, min_height: int = 40) -> list[dict[str, object]]:
|
|
87
|
+
"""Visible top-level windows, largest first.
|
|
88
|
+
|
|
89
|
+
Windows below ``min_width``/``min_height`` are skipped: a GUI app owns many tiny
|
|
90
|
+
invisible helper windows, and listing them buries the one that matters. Titles are
|
|
91
|
+
lower-cased for matching, because the same application reports different casing.
|
|
92
|
+
"""
|
|
93
|
+
if sys.platform != "win32":
|
|
94
|
+
return []
|
|
95
|
+
import win32gui
|
|
96
|
+
|
|
97
|
+
found: list[dict[str, object]] = []
|
|
98
|
+
|
|
99
|
+
def visit(hwnd, _):
|
|
100
|
+
if not win32gui.IsWindowVisible(hwnd):
|
|
101
|
+
return True
|
|
102
|
+
left, top, right, bottom = win32gui.GetWindowRect(hwnd)
|
|
103
|
+
width, height = right - left, bottom - top
|
|
104
|
+
if width < min_width or height < min_height:
|
|
105
|
+
return True
|
|
106
|
+
title = win32gui.GetWindowText(hwnd)
|
|
107
|
+
found.append(
|
|
108
|
+
{
|
|
109
|
+
"handle": hwnd,
|
|
110
|
+
"title": title,
|
|
111
|
+
"lower": title.casefold(),
|
|
112
|
+
"left": left,
|
|
113
|
+
"top": top,
|
|
114
|
+
"right": right,
|
|
115
|
+
"bottom": bottom,
|
|
116
|
+
"width": width,
|
|
117
|
+
"height": height,
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
win32gui.EnumWindows(visit, None)
|
|
123
|
+
found.sort(key=lambda w: int(w["width"]) * int(w["height"]), reverse=True)
|
|
124
|
+
return found
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def find_window(
|
|
128
|
+
title_contains: str | None = None,
|
|
129
|
+
handle: int | None = None,
|
|
130
|
+
exclude: tuple[str, ...] = (),
|
|
131
|
+
) -> dict[str, object] | None:
|
|
132
|
+
"""The best window matching a handle or a title substring.
|
|
133
|
+
|
|
134
|
+
Matching is case-insensitive and substring-based, so ``"motionworks"`` finds
|
|
135
|
+
``"MotionWorks IEC 3 Pro - TopCutter"`` without the caller knowing the version
|
|
136
|
+
string. The **largest** match wins, so a splash or tooltip does not shadow the
|
|
137
|
+
main window.
|
|
138
|
+
|
|
139
|
+
``exclude`` rejects titles containing any of those substrings. It is needed
|
|
140
|
+
because a substring search is otherwise fooled by *other* windows that merely
|
|
141
|
+
mention the application: measured here, a Chrome tab titled "MCP server for
|
|
142
|
+
**MotionWorks** IEC offline files" is 1936x1048 and therefore LARGER than the
|
|
143
|
+
733x441 licence dialog, so it won and the dialog was never found.
|
|
144
|
+
"""
|
|
145
|
+
if handle is not None:
|
|
146
|
+
import win32gui
|
|
147
|
+
|
|
148
|
+
if not win32gui.IsWindow(handle):
|
|
149
|
+
return None
|
|
150
|
+
left, top, right, bottom = win32gui.GetWindowRect(handle)
|
|
151
|
+
title = win32gui.GetWindowText(handle)
|
|
152
|
+
return {
|
|
153
|
+
"handle": handle,
|
|
154
|
+
"title": title,
|
|
155
|
+
"lower": title.casefold(),
|
|
156
|
+
"left": left,
|
|
157
|
+
"top": top,
|
|
158
|
+
"right": right,
|
|
159
|
+
"bottom": bottom,
|
|
160
|
+
"width": right - left,
|
|
161
|
+
"height": bottom - top,
|
|
162
|
+
}
|
|
163
|
+
if not title_contains:
|
|
164
|
+
return None
|
|
165
|
+
needle = title_contains.casefold()
|
|
166
|
+
matches = [
|
|
167
|
+
w
|
|
168
|
+
for w in list_windows()
|
|
169
|
+
if needle in str(w["lower"])
|
|
170
|
+
and not any(word.casefold() in str(w["lower"]) for word in exclude)
|
|
171
|
+
]
|
|
172
|
+
return matches[0] if matches else None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _grab_window_pixels(hwnd: int, path: Path) -> bool:
|
|
176
|
+
"""Capture a window's rectangle from the screen, or ask it to paint itself.
|
|
177
|
+
|
|
178
|
+
Two approaches, in this order, because measurement here rejected the more
|
|
179
|
+
"correct-looking" one:
|
|
180
|
+
|
|
181
|
+
* **``PrintWindow``** — asks the window to render into a bitmap, immune to
|
|
182
|
+
occlusion and screen coordinates. On the MotionWorks trial dialog it returns 0
|
|
183
|
+
for both ``PW_RENDERFULLCONTENT`` and the plain flag, and the result is an
|
|
184
|
+
entirely black bitmap, so it cannot be relied on for this application.
|
|
185
|
+
* **Screen rectangle** — grab the window's rectangle out of a full-desktop
|
|
186
|
+
capture. This works, but the **offset must be right**: ``ImageGrab`` returns the
|
|
187
|
+
virtual desktop starting at its top-left, which on this machine is x=-4480. A
|
|
188
|
+
crop taken with the raw window coordinates is 4480 pixels off and comes back
|
|
189
|
+
black, which is exactly the bug this replaces.
|
|
190
|
+
|
|
191
|
+
Returns True when pixels were written.
|
|
192
|
+
"""
|
|
193
|
+
import ctypes
|
|
194
|
+
from ctypes import wintypes
|
|
195
|
+
|
|
196
|
+
from PIL import Image
|
|
197
|
+
|
|
198
|
+
if _print_window_pixels(hwnd, path):
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
user32 = ctypes.windll.user32
|
|
202
|
+
rectangle = wintypes.RECT()
|
|
203
|
+
if not user32.GetWindowRect(hwnd, ctypes.byref(rectangle)):
|
|
204
|
+
return False
|
|
205
|
+
virtual_left = user32.GetSystemMetrics(76) # SM_XVIRTUALSCREEN
|
|
206
|
+
virtual_top = user32.GetSystemMetrics(77)
|
|
207
|
+
virtual_width = user32.GetSystemMetrics(78)
|
|
208
|
+
virtual_height = user32.GetSystemMetrics(79)
|
|
209
|
+
if virtual_width <= 0 or virtual_height <= 0:
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
from PIL import ImageGrab
|
|
214
|
+
|
|
215
|
+
full = ImageGrab.grab(all_screens=True)
|
|
216
|
+
except Exception: # noqa: BLE001
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
box = (
|
|
220
|
+
rectangle.left - virtual_left,
|
|
221
|
+
rectangle.top - virtual_top,
|
|
222
|
+
rectangle.right - virtual_left,
|
|
223
|
+
rectangle.bottom - virtual_top,
|
|
224
|
+
)
|
|
225
|
+
box = (
|
|
226
|
+
max(0, box[0]),
|
|
227
|
+
max(0, box[1]),
|
|
228
|
+
min(full.size[0], box[2]),
|
|
229
|
+
min(full.size[1], box[3]),
|
|
230
|
+
)
|
|
231
|
+
if box[2] - box[0] <= 0 or box[3] - box[1] <= 0:
|
|
232
|
+
return False
|
|
233
|
+
try:
|
|
234
|
+
full.crop(box).save(path)
|
|
235
|
+
except Exception: # noqa: BLE001
|
|
236
|
+
return False
|
|
237
|
+
prune()
|
|
238
|
+
return True
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _print_window_pixels(hwnd: int, path: Path) -> bool:
|
|
242
|
+
"""The ``PrintWindow`` route. Returns False when the window paints nothing."""
|
|
243
|
+
import ctypes
|
|
244
|
+
from ctypes import wintypes
|
|
245
|
+
|
|
246
|
+
from PIL import Image
|
|
247
|
+
|
|
248
|
+
user32 = ctypes.windll.user32
|
|
249
|
+
gdi32 = ctypes.windll.gdi32
|
|
250
|
+
|
|
251
|
+
rectangle = wintypes.RECT()
|
|
252
|
+
if not user32.GetWindowRect(hwnd, ctypes.byref(rectangle)):
|
|
253
|
+
return False
|
|
254
|
+
width = rectangle.right - rectangle.left
|
|
255
|
+
height = rectangle.bottom - rectangle.top
|
|
256
|
+
if width <= 0 or height <= 0:
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
window_dc = user32.GetWindowDC(hwnd)
|
|
260
|
+
if not window_dc:
|
|
261
|
+
return False
|
|
262
|
+
memory_dc = gdi32.CreateCompatibleDC(window_dc)
|
|
263
|
+
bitmap = gdi32.CreateCompatibleBitmap(window_dc, width, height)
|
|
264
|
+
if not memory_dc or not bitmap:
|
|
265
|
+
gdi32.DeleteDC(memory_dc)
|
|
266
|
+
user32.ReleaseDC(hwnd, window_dc)
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
gdi32.SelectObject(memory_dc, bitmap)
|
|
271
|
+
if not user32.PrintWindow(hwnd, memory_dc, 2) and not user32.PrintWindow(
|
|
272
|
+
hwnd, memory_dc, 0
|
|
273
|
+
):
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
class BITMAPINFOHEADER(ctypes.Structure):
|
|
277
|
+
_fields_ = [
|
|
278
|
+
("biSize", wintypes.DWORD),
|
|
279
|
+
("biWidth", wintypes.LONG),
|
|
280
|
+
("biHeight", wintypes.LONG),
|
|
281
|
+
("biPlanes", wintypes.WORD),
|
|
282
|
+
("biBitCount", wintypes.WORD),
|
|
283
|
+
("biCompression", wintypes.DWORD),
|
|
284
|
+
("biSizeImage", wintypes.DWORD),
|
|
285
|
+
("biXPelsPerMeter", wintypes.LONG),
|
|
286
|
+
("biYPelsPerMeter", wintypes.LONG),
|
|
287
|
+
("biClrUsed", wintypes.DWORD),
|
|
288
|
+
("biClrImportant", wintypes.DWORD),
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
header = BITMAPINFOHEADER()
|
|
292
|
+
header.biSize = ctypes.sizeof(BITMAPINFOHEADER)
|
|
293
|
+
header.biWidth = width
|
|
294
|
+
header.biHeight = -height # negative: top-down rows
|
|
295
|
+
header.biPlanes = 1
|
|
296
|
+
header.biBitCount = 32
|
|
297
|
+
header.biCompression = 0 # BI_RGB
|
|
298
|
+
|
|
299
|
+
buffer = ctypes.create_string_buffer(width * height * 4)
|
|
300
|
+
if not gdi32.GetDIBits(
|
|
301
|
+
memory_dc, bitmap, 0, height, buffer, ctypes.byref(header), 0
|
|
302
|
+
):
|
|
303
|
+
return False
|
|
304
|
+
# A composited window reports success but yields an all-black bitmap, so
|
|
305
|
+
# treat "no non-zero pixel" as a failure and let the caller fall back.
|
|
306
|
+
if not any(buffer[: min(len(buffer), 4096)]):
|
|
307
|
+
return False
|
|
308
|
+
image = Image.frombuffer("RGB", (width, height), buffer, "raw", "BGRX", 0, 1)
|
|
309
|
+
image.save(path)
|
|
310
|
+
prune()
|
|
311
|
+
return True
|
|
312
|
+
finally:
|
|
313
|
+
gdi32.DeleteObject(bitmap)
|
|
314
|
+
gdi32.DeleteDC(memory_dc)
|
|
315
|
+
user32.ReleaseDC(hwnd, window_dc)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def capture_window(
|
|
319
|
+
title_contains: str | None = None,
|
|
320
|
+
handle: int | None = None,
|
|
321
|
+
name: str = "window",
|
|
322
|
+
padding: int = 0,
|
|
323
|
+
exclude: tuple[str, ...] = (),
|
|
324
|
+
) -> tuple[Path, dict[str, object]]:
|
|
325
|
+
"""Capture one window and return ``(path, window)``.
|
|
326
|
+
|
|
327
|
+
The window is brought to the foreground first, so the pixels captured are the
|
|
328
|
+
window's own rather than whatever was covering it.
|
|
329
|
+
"""
|
|
330
|
+
if sys.platform != "win32":
|
|
331
|
+
raise CaptureError("window capture is only implemented on Windows")
|
|
332
|
+
window = find_window(
|
|
333
|
+
title_contains=title_contains, handle=handle, exclude=exclude
|
|
334
|
+
)
|
|
335
|
+
if window is None:
|
|
336
|
+
wanted = title_contains if title_contains else f"handle {handle}"
|
|
337
|
+
raise CaptureError(f"no visible window matching {wanted!r}")
|
|
338
|
+
|
|
339
|
+
_prepare()
|
|
340
|
+
import win32gui
|
|
341
|
+
|
|
342
|
+
hwnd = int(window["handle"])
|
|
343
|
+
try:
|
|
344
|
+
if win32gui.IsIconic(hwnd):
|
|
345
|
+
win32gui.ShowWindow(hwnd, 9) # SW_RESTORE
|
|
346
|
+
win32gui.SetForegroundWindow(hwnd)
|
|
347
|
+
except Exception: # noqa: BLE001 - foreground rights can be denied; still capture
|
|
348
|
+
pass
|
|
349
|
+
time.sleep(0.35)
|
|
350
|
+
|
|
351
|
+
path = CAPTURE_DIR / f"{name}-{hwnd}-{int(time.time())}.png"
|
|
352
|
+
if not _grab_window_pixels(hwnd, path):
|
|
353
|
+
raise CaptureError(f"could not capture window {hwnd}")
|
|
354
|
+
|
|
355
|
+
window["capture"] = str(path)
|
|
356
|
+
return path, window
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def describe_windows(limit: int = 25) -> str:
|
|
360
|
+
"""A readable listing of the visible windows, for a tool reply."""
|
|
361
|
+
windows = list_windows()[:limit]
|
|
362
|
+
if not windows:
|
|
363
|
+
return "No visible windows of a usable size."
|
|
364
|
+
lines = [f"{len(windows)} visible window(s), largest first:"]
|
|
365
|
+
for w in windows:
|
|
366
|
+
title = w["title"] or "(untitled)"
|
|
367
|
+
lines.append(
|
|
368
|
+
f" {w['width']:>5}x{w['height']:<5} at ({w['left']},{w['top']}) "
|
|
369
|
+
f"hwnd={w['handle']} {str(title)[:70]}"
|
|
370
|
+
)
|
|
371
|
+
return "\n".join(lines)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
#: Common install locations for the Tesseract CLI.
|
|
375
|
+
TESSERACT_CANDIDATES = (
|
|
376
|
+
Path(r"C:\Program Files\Tesseract-OCR\tesseract.exe"),
|
|
377
|
+
Path(r"C:\Program Files (x86)\Tesseract-OCR\tesseract.exe"),
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def tesseract_path() -> Path | None:
|
|
382
|
+
"""The Tesseract executable, if it is installed."""
|
|
383
|
+
import shutil as _shutil
|
|
384
|
+
|
|
385
|
+
found = _shutil.which("tesseract")
|
|
386
|
+
if found:
|
|
387
|
+
return Path(found)
|
|
388
|
+
for candidate in TESSERACT_CANDIDATES:
|
|
389
|
+
if candidate.is_file():
|
|
390
|
+
return candidate
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def read_text(image: Path | None = None, psm: int = 6) -> str:
|
|
395
|
+
"""OCR an image (or the screen, if none is given) into plain text.
|
|
396
|
+
|
|
397
|
+
OCR matters even though UI Automation exists, because **accessibility is not
|
|
398
|
+
always available**. The MotionWorks trial dialog exposes six elements and reports
|
|
399
|
+
every clickable control as a ``Pane``; anything the application draws itself is
|
|
400
|
+
invisible to UI Automation, and only pixels remain.
|
|
401
|
+
|
|
402
|
+
Tesseract is invoked as a **subprocess**, not through ``pytesseract``, so there is
|
|
403
|
+
no extra Python dependency -- only the executable, which is already installed
|
|
404
|
+
here. ``--psm 6`` treats the image as a uniform block of text, which suits dialogs
|
|
405
|
+
better than the default page-segmentation mode.
|
|
406
|
+
"""
|
|
407
|
+
import subprocess
|
|
408
|
+
import tempfile
|
|
409
|
+
|
|
410
|
+
executable = tesseract_path()
|
|
411
|
+
if executable is None:
|
|
412
|
+
raise CaptureError(
|
|
413
|
+
"Tesseract is not installed, so text cannot be read from the image. "
|
|
414
|
+
"Install it (winget install UB-Mannheim.TesseractOCR) or use "
|
|
415
|
+
"mw_ui_elements for windows that expose accessibility."
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if image is None:
|
|
419
|
+
image = capture_screen("ocr")
|
|
420
|
+
if not Path(image).is_file():
|
|
421
|
+
raise CaptureError(f"no such image: {image}")
|
|
422
|
+
|
|
423
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
424
|
+
stem = Path(tmp) / "out"
|
|
425
|
+
try:
|
|
426
|
+
subprocess.run(
|
|
427
|
+
[
|
|
428
|
+
str(executable),
|
|
429
|
+
str(image),
|
|
430
|
+
str(stem),
|
|
431
|
+
"--psm",
|
|
432
|
+
str(psm),
|
|
433
|
+
],
|
|
434
|
+
stdout=subprocess.DEVNULL,
|
|
435
|
+
stderr=subprocess.DEVNULL,
|
|
436
|
+
timeout=120,
|
|
437
|
+
creationflags=0x08000000,
|
|
438
|
+
)
|
|
439
|
+
except Exception as exc: # noqa: BLE001
|
|
440
|
+
raise CaptureError(f"OCR failed to run: {exc}") from exc
|
|
441
|
+
text_file = stem.with_suffix(".txt")
|
|
442
|
+
try:
|
|
443
|
+
raw = text_file.read_text(encoding="utf-8", errors="replace")
|
|
444
|
+
except OSError as exc:
|
|
445
|
+
raise CaptureError(f"OCR produced no text: {exc}") from exc
|
|
446
|
+
|
|
447
|
+
lines = [line.rstrip() for line in raw.splitlines()]
|
|
448
|
+
return "\n".join(line for line in lines if line.strip())
|