pyvisionauto 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyvisionauto-0.1.0/LICENSE +8 -0
- pyvisionauto-0.1.0/PKG-INFO +64 -0
- pyvisionauto-0.1.0/README.md +36 -0
- pyvisionauto-0.1.0/pyproject.toml +49 -0
- pyvisionauto-0.1.0/setup.cfg +4 -0
- pyvisionauto-0.1.0/src/pyvisionauto/__init__.py +36 -0
- pyvisionauto-0.1.0/src/pyvisionauto/config.py +23 -0
- pyvisionauto-0.1.0/src/pyvisionauto/envcheck.py +87 -0
- pyvisionauto-0.1.0/src/pyvisionauto/errors.py +26 -0
- pyvisionauto-0.1.0/src/pyvisionauto/highlighter.py +74 -0
- pyvisionauto-0.1.0/src/pyvisionauto/input.py +103 -0
- pyvisionauto-0.1.0/src/pyvisionauto/models.py +77 -0
- pyvisionauto-0.1.0/src/pyvisionauto/overlay.py +51 -0
- pyvisionauto-0.1.0/src/pyvisionauto/py.typed +0 -0
- pyvisionauto-0.1.0/src/pyvisionauto/recorder.py +61 -0
- pyvisionauto-0.1.0/src/pyvisionauto/screen.py +457 -0
- pyvisionauto-0.1.0/src/pyvisionauto/vision.py +196 -0
- pyvisionauto-0.1.0/src/pyvisionauto.egg-info/PKG-INFO +64 -0
- pyvisionauto-0.1.0/src/pyvisionauto.egg-info/SOURCES.txt +22 -0
- pyvisionauto-0.1.0/src/pyvisionauto.egg-info/dependency_links.txt +1 -0
- pyvisionauto-0.1.0/src/pyvisionauto.egg-info/requires.txt +8 -0
- pyvisionauto-0.1.0/src/pyvisionauto.egg-info/top_level.txt +1 -0
- pyvisionauto-0.1.0/tests/test_input_deterministic.py +61 -0
- pyvisionauto-0.1.0/tests/test_screen_api.py +99 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright (c) 2026 PyVisionAuto contributors. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software and its source code are proprietary and confidential.
|
|
4
|
+
Redistribution, modification, or use of the source code in any form is
|
|
5
|
+
prohibited without explicit written permission from the copyright holder.
|
|
6
|
+
|
|
7
|
+
The compiled/distributed package may be used in accordance with the terms
|
|
8
|
+
provided at: https://pypi.org/project/pyvisionauto/
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyvisionauto
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: PyVisionAuto: Linux end-to-end automation toolkit with visual image matching, mouse/keyboard control, and screen recording
|
|
5
|
+
Author: PyVisionAuto contributors
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://pypi.org/project/pyvisionauto/
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Environment :: X11 Applications
|
|
16
|
+
Classifier: Topic :: Software Development :: Testing
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: opencv-python>=4.8.0
|
|
21
|
+
Requires-Dist: mss>=9.0.0
|
|
22
|
+
Requires-Dist: numpy>=1.24.0
|
|
23
|
+
Requires-Dist: pyautogui>=0.9.54
|
|
24
|
+
Requires-Dist: pillow>=10.0.0
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# PyVisionAuto
|
|
30
|
+
|
|
31
|
+
PyVisionAuto (`pyvisionauto`) is a Linux end-to-end automation testing toolkit.
|
|
32
|
+
It is centered on visual image matching and also includes screen recording, mouse automation, and keyboard automation capabilities.
|
|
33
|
+
|
|
34
|
+
## Scope
|
|
35
|
+
|
|
36
|
+
- Linux only
|
|
37
|
+
- X11 session only
|
|
38
|
+
- Real physical display required
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install pyvisionauto
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## System dependencies
|
|
47
|
+
|
|
48
|
+
- python3-tk (for border overlay highlight)
|
|
49
|
+
- xdotool (preferred for window activation)
|
|
50
|
+
- wmctrl (fallback for window activation)
|
|
51
|
+
- ffmpeg (optional, only for recording APIs)
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from pyvisionauto import Screen
|
|
57
|
+
|
|
58
|
+
screen = Screen()
|
|
59
|
+
screen.wait("login_button.png", timeout=10).highlight().click()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Notes
|
|
63
|
+
|
|
64
|
+
Wayland-first and headless-only environments are not supported in v0.1.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# PyVisionAuto
|
|
2
|
+
|
|
3
|
+
PyVisionAuto (`pyvisionauto`) is a Linux end-to-end automation testing toolkit.
|
|
4
|
+
It is centered on visual image matching and also includes screen recording, mouse automation, and keyboard automation capabilities.
|
|
5
|
+
|
|
6
|
+
## Scope
|
|
7
|
+
|
|
8
|
+
- Linux only
|
|
9
|
+
- X11 session only
|
|
10
|
+
- Real physical display required
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install pyvisionauto
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## System dependencies
|
|
19
|
+
|
|
20
|
+
- python3-tk (for border overlay highlight)
|
|
21
|
+
- xdotool (preferred for window activation)
|
|
22
|
+
- wmctrl (fallback for window activation)
|
|
23
|
+
- ffmpeg (optional, only for recording APIs)
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from pyvisionauto import Screen
|
|
29
|
+
|
|
30
|
+
screen = Screen()
|
|
31
|
+
screen.wait("login_button.png", timeout=10).highlight().click()
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Notes
|
|
35
|
+
|
|
36
|
+
Wayland-first and headless-only environments are not supported in v0.1.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyvisionauto"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "PyVisionAuto: Linux end-to-end automation toolkit with visual image matching, mouse/keyboard control, and screen recording"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "LicenseRef-Proprietary"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "PyVisionAuto contributors" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"opencv-python>=4.8.0",
|
|
17
|
+
"mss>=9.0.0",
|
|
18
|
+
"numpy>=1.24.0",
|
|
19
|
+
"pyautogui>=0.9.54",
|
|
20
|
+
"pillow>=10.0.0"
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 3 - Alpha",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Operating System :: POSIX :: Linux",
|
|
30
|
+
"Environment :: X11 Applications",
|
|
31
|
+
"Topic :: Software Development :: Testing"
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://pypi.org/project/pyvisionauto/"
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
test = [
|
|
39
|
+
"pytest>=8.0"
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[tool.setuptools]
|
|
43
|
+
package-dir = {"" = "src"}
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.package-data]
|
|
46
|
+
pyvisionauto = ["py.typed"]
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.packages.find]
|
|
49
|
+
where = ["src"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from .config import DEFAULT_CONFIDENCE, DEFAULT_POLL_INTERVAL
|
|
2
|
+
from .envcheck import EnvCheck
|
|
3
|
+
from .errors import (
|
|
4
|
+
EnvironmentNotSupportedError,
|
|
5
|
+
OverlayError,
|
|
6
|
+
PyVisionAutoError,
|
|
7
|
+
RecorderError,
|
|
8
|
+
TemplateNotFoundError,
|
|
9
|
+
VanishTimeoutError,
|
|
10
|
+
WaitTimeoutError,
|
|
11
|
+
)
|
|
12
|
+
from .highlighter import Highlighter
|
|
13
|
+
from .input import Input
|
|
14
|
+
from .models import EnvironmentReport, Match, TimingProfile
|
|
15
|
+
from .recorder import Recorder
|
|
16
|
+
from .screen import Screen
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"DEFAULT_CONFIDENCE",
|
|
20
|
+
"DEFAULT_POLL_INTERVAL",
|
|
21
|
+
"EnvCheck",
|
|
22
|
+
"EnvironmentNotSupportedError",
|
|
23
|
+
"EnvironmentReport",
|
|
24
|
+
"Highlighter",
|
|
25
|
+
"Input",
|
|
26
|
+
"Match",
|
|
27
|
+
"OverlayError",
|
|
28
|
+
"PyVisionAutoError",
|
|
29
|
+
"Recorder",
|
|
30
|
+
"RecorderError",
|
|
31
|
+
"Screen",
|
|
32
|
+
"TemplateNotFoundError",
|
|
33
|
+
"TimingProfile",
|
|
34
|
+
"VanishTimeoutError",
|
|
35
|
+
"WaitTimeoutError",
|
|
36
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .models import TimingProfile
|
|
2
|
+
|
|
3
|
+
DEFAULT_CONFIDENCE = 0.88
|
|
4
|
+
DEFAULT_POLL_INTERVAL = 0.5
|
|
5
|
+
|
|
6
|
+
HIGHLIGHT_ENABLED = True
|
|
7
|
+
HIGHLIGHT_COLOR = "#ff0000"
|
|
8
|
+
HIGHLIGHT_THICKNESS = 3
|
|
9
|
+
HIGHLIGHT_DURATION_MS = 700
|
|
10
|
+
|
|
11
|
+
DEFAULT_TIMING = TimingProfile(
|
|
12
|
+
typing_delay_min=0.04,
|
|
13
|
+
typing_delay_max=0.12,
|
|
14
|
+
special_char_extra_delay=0.03,
|
|
15
|
+
key_press_delay_min=0.01,
|
|
16
|
+
key_press_delay_max=0.04,
|
|
17
|
+
key_post_delay_min=0.03,
|
|
18
|
+
key_post_delay_max=0.08,
|
|
19
|
+
hotkey_gap_min=0.02,
|
|
20
|
+
hotkey_gap_max=0.06,
|
|
21
|
+
human_like_default=True,
|
|
22
|
+
deterministic_mode=False,
|
|
23
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
from .errors import EnvironmentNotSupportedError
|
|
9
|
+
from .models import EnvironmentReport
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EnvCheck:
|
|
13
|
+
"""Validate runtime prerequisites for PyVisionAuto on Linux desktops."""
|
|
14
|
+
|
|
15
|
+
def check(self, strict: bool = True) -> EnvironmentReport:
|
|
16
|
+
"""Run platform and dependency checks.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
strict: If ``True``, raise when environment is not supported.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Structured environment report.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
EnvironmentNotSupportedError: If ``strict=True`` and checks fail.
|
|
26
|
+
"""
|
|
27
|
+
is_linux = platform.system().lower() == "linux"
|
|
28
|
+
has_display = bool(os.environ.get("DISPLAY"))
|
|
29
|
+
|
|
30
|
+
x11_ok = False
|
|
31
|
+
if has_display:
|
|
32
|
+
try:
|
|
33
|
+
out = subprocess.run(
|
|
34
|
+
["sh", "-lc", "echo ${XDG_SESSION_TYPE:-unknown}"],
|
|
35
|
+
capture_output=True,
|
|
36
|
+
text=True,
|
|
37
|
+
check=False,
|
|
38
|
+
)
|
|
39
|
+
x11_ok = out.stdout.strip().lower() in {"x11", "unknown"}
|
|
40
|
+
except Exception:
|
|
41
|
+
x11_ok = False
|
|
42
|
+
|
|
43
|
+
tk_ok = True
|
|
44
|
+
try:
|
|
45
|
+
import tkinter # noqa: F401
|
|
46
|
+
except Exception:
|
|
47
|
+
tk_ok = False
|
|
48
|
+
|
|
49
|
+
xdotool_ok = shutil.which("xdotool") is not None
|
|
50
|
+
wmctrl_ok = shutil.which("wmctrl") is not None
|
|
51
|
+
ibus_ok = shutil.which("ibus") is not None
|
|
52
|
+
ffmpeg_ok = shutil.which("ffmpeg") is not None
|
|
53
|
+
|
|
54
|
+
messages: list[str] = []
|
|
55
|
+
if not is_linux:
|
|
56
|
+
messages.append("Only Linux is supported in v0.1")
|
|
57
|
+
if not has_display:
|
|
58
|
+
messages.append("DISPLAY is missing")
|
|
59
|
+
if not x11_ok:
|
|
60
|
+
messages.append("X11 session not detected")
|
|
61
|
+
if not tk_ok:
|
|
62
|
+
messages.append("tkinter is unavailable (install python3-tk)")
|
|
63
|
+
if not (xdotool_ok or wmctrl_ok):
|
|
64
|
+
messages.append("Neither xdotool nor wmctrl is installed")
|
|
65
|
+
|
|
66
|
+
supported = is_linux and has_display and x11_ok and tk_ok and (xdotool_ok or wmctrl_ok)
|
|
67
|
+
report = EnvironmentReport(
|
|
68
|
+
is_supported=supported,
|
|
69
|
+
platform_ok=is_linux,
|
|
70
|
+
display_ok=has_display,
|
|
71
|
+
x11_ok=x11_ok,
|
|
72
|
+
tk_ok=tk_ok,
|
|
73
|
+
xdotool_ok=xdotool_ok,
|
|
74
|
+
wmctrl_ok=wmctrl_ok,
|
|
75
|
+
ibus_ok=ibus_ok,
|
|
76
|
+
ffmpeg_ok=ffmpeg_ok,
|
|
77
|
+
messages=messages,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if strict and not report.is_supported:
|
|
81
|
+
raise EnvironmentNotSupportedError("; ".join(report.messages) or "Unsupported environment")
|
|
82
|
+
return report
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def check_env(strict: bool = True) -> EnvironmentReport:
|
|
86
|
+
"""Convenience wrapper for :meth:`EnvCheck.check`."""
|
|
87
|
+
return EnvCheck().check(strict=strict)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class PyVisionAutoError(Exception):
|
|
2
|
+
"""Base exception for PyVisionAuto."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TemplateNotFoundError(PyVisionAutoError):
|
|
6
|
+
"""Raised when an image template file path does not exist."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WaitTimeoutError(PyVisionAutoError):
|
|
10
|
+
"""Raised when waiting for a match times out."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VanishTimeoutError(PyVisionAutoError):
|
|
14
|
+
"""Raised when waiting for an image to vanish times out."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EnvironmentNotSupportedError(PyVisionAutoError):
|
|
18
|
+
"""Raised when runtime prerequisites are not met."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OverlayError(PyVisionAutoError):
|
|
22
|
+
"""Raised when highlight overlay cannot be rendered."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RecorderError(PyVisionAutoError):
|
|
26
|
+
"""Raised when recording actions fail."""
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .config import HIGHLIGHT_COLOR, HIGHLIGHT_DURATION_MS, HIGHLIGHT_THICKNESS
|
|
9
|
+
from .errors import OverlayError
|
|
10
|
+
from .models import Match
|
|
11
|
+
|
|
12
|
+
LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Highlighter:
|
|
16
|
+
"""Overlay helper used to draw temporary highlight borders for matches."""
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
"""Initialize overlay script path."""
|
|
20
|
+
self._overlay_script = Path(__file__).with_name("overlay.py")
|
|
21
|
+
|
|
22
|
+
def show(
|
|
23
|
+
self,
|
|
24
|
+
match: Match,
|
|
25
|
+
duration_ms: int | None = None,
|
|
26
|
+
color: str | None = None,
|
|
27
|
+
thickness: int | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Spawn overlay process to highlight one match rectangle.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
match: Match rectangle to visualize.
|
|
33
|
+
duration_ms: Overlay lifetime in milliseconds. ``None`` uses default.
|
|
34
|
+
color: Border color. ``None`` uses default.
|
|
35
|
+
thickness: Border width in pixels. ``None`` uses default.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
OverlayError: If the overlay process cannot be started.
|
|
39
|
+
"""
|
|
40
|
+
resolved_duration = duration_ms if duration_ms is not None else HIGHLIGHT_DURATION_MS
|
|
41
|
+
resolved_color = color if color is not None else HIGHLIGHT_COLOR
|
|
42
|
+
resolved_thickness = thickness if thickness is not None else HIGHLIGHT_THICKNESS
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
subprocess.Popen(
|
|
46
|
+
[
|
|
47
|
+
sys.executable,
|
|
48
|
+
str(self._overlay_script),
|
|
49
|
+
str(match.x),
|
|
50
|
+
str(match.y),
|
|
51
|
+
str(match.w),
|
|
52
|
+
str(match.h),
|
|
53
|
+
str(resolved_duration),
|
|
54
|
+
resolved_color,
|
|
55
|
+
str(resolved_thickness),
|
|
56
|
+
],
|
|
57
|
+
stdout=subprocess.DEVNULL,
|
|
58
|
+
stderr=subprocess.DEVNULL,
|
|
59
|
+
)
|
|
60
|
+
except Exception as exc: # pragma: no cover
|
|
61
|
+
raise OverlayError(str(exc)) from exc
|
|
62
|
+
|
|
63
|
+
def safe_show(
|
|
64
|
+
self,
|
|
65
|
+
match: Match,
|
|
66
|
+
duration_ms: int | None = None,
|
|
67
|
+
color: str | None = None,
|
|
68
|
+
thickness: int | None = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Best-effort highlight wrapper that never interrupts user actions."""
|
|
71
|
+
try:
|
|
72
|
+
self.show(match, duration_ms=duration_ms, color=color, thickness=thickness)
|
|
73
|
+
except OverlayError as exc:
|
|
74
|
+
LOGGER.warning("Highlight failed but action continues: %s", exc)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import random
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from .config import DEFAULT_TIMING
|
|
8
|
+
from .models import TimingProfile
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Input:
|
|
12
|
+
"""Keyboard text/input helper with optional human-like timing behavior."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, timing: TimingProfile | None = None, deterministic: bool | None = None) -> None:
|
|
15
|
+
"""Create input helper.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
timing: Timing profile. ``None`` uses ``DEFAULT_TIMING``.
|
|
19
|
+
deterministic: Override deterministic mode. ``None`` follows timing profile.
|
|
20
|
+
"""
|
|
21
|
+
self.timing = timing or DEFAULT_TIMING
|
|
22
|
+
self.deterministic = self.timing.deterministic_mode if deterministic is None else deterministic
|
|
23
|
+
|
|
24
|
+
def _get_pyautogui(self):
|
|
25
|
+
"""Lazily import pyautogui to reduce module import constraints."""
|
|
26
|
+
return importlib.import_module("pyautogui")
|
|
27
|
+
|
|
28
|
+
def _pause(self, low: float, high: float) -> None:
|
|
29
|
+
"""Sleep for a delay interval, deterministic or random based on settings."""
|
|
30
|
+
if self.deterministic:
|
|
31
|
+
time.sleep((low + high) / 2.0)
|
|
32
|
+
return
|
|
33
|
+
time.sleep(random.uniform(low, high))
|
|
34
|
+
|
|
35
|
+
def type_text(
|
|
36
|
+
self,
|
|
37
|
+
text: str,
|
|
38
|
+
human_like: bool | None = None,
|
|
39
|
+
delay_min: float | None = None,
|
|
40
|
+
delay_max: float | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Type text into the active UI control.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
text: Text content to type.
|
|
46
|
+
human_like: Whether to use per-character delays. ``None`` uses timing default.
|
|
47
|
+
delay_min: Minimum delay between characters in seconds.
|
|
48
|
+
delay_max: Maximum delay between characters in seconds.
|
|
49
|
+
"""
|
|
50
|
+
pyautogui = self._get_pyautogui()
|
|
51
|
+
use_human = self.timing.human_like_default if human_like is None else human_like
|
|
52
|
+
if not use_human:
|
|
53
|
+
pyautogui.write(text)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
low = self.timing.typing_delay_min if delay_min is None else delay_min
|
|
57
|
+
high = self.timing.typing_delay_max if delay_max is None else delay_max
|
|
58
|
+
|
|
59
|
+
for ch in text:
|
|
60
|
+
pyautogui.write(ch)
|
|
61
|
+
self._pause(low, high)
|
|
62
|
+
if ch in "/-_.":
|
|
63
|
+
time.sleep(self.timing.special_char_extra_delay)
|
|
64
|
+
|
|
65
|
+
def press(self, key: str, human_like: bool | None = None) -> None:
|
|
66
|
+
"""Press one key.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
key: Key name understood by pyautogui (for example ``"enter"``).
|
|
70
|
+
human_like: Whether to apply pre/post action delays.
|
|
71
|
+
"""
|
|
72
|
+
pyautogui = self._get_pyautogui()
|
|
73
|
+
use_human = self.timing.human_like_default if human_like is None else human_like
|
|
74
|
+
if use_human:
|
|
75
|
+
self._pause(self.timing.key_press_delay_min, self.timing.key_press_delay_max)
|
|
76
|
+
pyautogui.press(key)
|
|
77
|
+
if use_human:
|
|
78
|
+
self._pause(self.timing.key_post_delay_min, self.timing.key_post_delay_max)
|
|
79
|
+
|
|
80
|
+
def hotkey(self, *keys: str, human_like: bool | None = None) -> None:
|
|
81
|
+
"""Send a key combination.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
*keys: Ordered key sequence, for example ``("ctrl", "v")``.
|
|
85
|
+
human_like: Whether to apply delay between key down/up events.
|
|
86
|
+
"""
|
|
87
|
+
pyautogui = self._get_pyautogui()
|
|
88
|
+
use_human = self.timing.human_like_default if human_like is None else human_like
|
|
89
|
+
if not use_human:
|
|
90
|
+
pyautogui.hotkey(*keys)
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
for key in keys:
|
|
94
|
+
pyautogui.keyDown(key)
|
|
95
|
+
self._pause(self.timing.hotkey_gap_min, self.timing.hotkey_gap_max)
|
|
96
|
+
for key in reversed(keys):
|
|
97
|
+
pyautogui.keyUp(key)
|
|
98
|
+
self._pause(self.timing.hotkey_gap_min, self.timing.hotkey_gap_max)
|
|
99
|
+
|
|
100
|
+
def clear_text(self) -> None:
|
|
101
|
+
"""Clear active input field using Ctrl+A followed by Backspace."""
|
|
102
|
+
self.hotkey("ctrl", "a")
|
|
103
|
+
self.press("backspace")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class Match:
|
|
8
|
+
"""Rectangle and confidence returned by template matching.
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
x: Absolute screen X of the matched top-left corner.
|
|
12
|
+
y: Absolute screen Y of the matched top-left corner.
|
|
13
|
+
w: Matched width in pixels.
|
|
14
|
+
h: Matched height in pixels.
|
|
15
|
+
score: Normalized confidence score in [0.0, 1.0].
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
x: int
|
|
19
|
+
y: int
|
|
20
|
+
w: int
|
|
21
|
+
h: int
|
|
22
|
+
score: float
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def center(self) -> tuple[int, int]:
|
|
26
|
+
"""Return center point ``(x + w // 2, y + h // 2)``."""
|
|
27
|
+
return (self.x + self.w // 2, self.y + self.h // 2)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class TimingProfile:
|
|
32
|
+
"""Timing configuration used by :class:`pyvisionauto.input.Input`."""
|
|
33
|
+
|
|
34
|
+
typing_delay_min: float
|
|
35
|
+
typing_delay_max: float
|
|
36
|
+
special_char_extra_delay: float
|
|
37
|
+
key_press_delay_min: float
|
|
38
|
+
key_press_delay_max: float
|
|
39
|
+
key_post_delay_min: float
|
|
40
|
+
key_post_delay_max: float
|
|
41
|
+
hotkey_gap_min: float
|
|
42
|
+
hotkey_gap_max: float
|
|
43
|
+
human_like_default: bool = True
|
|
44
|
+
deterministic_mode: bool = False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class EnvironmentReport:
|
|
49
|
+
"""Result container for environment compatibility checks."""
|
|
50
|
+
|
|
51
|
+
is_supported: bool
|
|
52
|
+
platform_ok: bool
|
|
53
|
+
display_ok: bool
|
|
54
|
+
x11_ok: bool
|
|
55
|
+
tk_ok: bool
|
|
56
|
+
xdotool_ok: bool
|
|
57
|
+
wmctrl_ok: bool
|
|
58
|
+
ibus_ok: bool
|
|
59
|
+
ffmpeg_ok: bool
|
|
60
|
+
messages: list[str] = field(default_factory=list)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class Region:
|
|
65
|
+
"""Search/capture rectangle in absolute screen coordinates.
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
68
|
+
x: Absolute screen X of the region top-left corner.
|
|
69
|
+
y: Absolute screen Y of the region top-left corner.
|
|
70
|
+
w: Region width in pixels.
|
|
71
|
+
h: Region height in pixels.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
x: int
|
|
75
|
+
y: int
|
|
76
|
+
w: int
|
|
77
|
+
h: int
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import tkinter as tk
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main() -> int:
|
|
8
|
+
if len(sys.argv) != 8:
|
|
9
|
+
return 2
|
|
10
|
+
|
|
11
|
+
x = int(sys.argv[1])
|
|
12
|
+
y = int(sys.argv[2])
|
|
13
|
+
w = int(sys.argv[3])
|
|
14
|
+
h = int(sys.argv[4])
|
|
15
|
+
duration_ms = int(sys.argv[5])
|
|
16
|
+
color = sys.argv[6]
|
|
17
|
+
thickness = int(sys.argv[7])
|
|
18
|
+
|
|
19
|
+
# Keep center visually transparent by drawing border only on a canvas.
|
|
20
|
+
root = tk.Tk()
|
|
21
|
+
root.overrideredirect(True)
|
|
22
|
+
root.attributes("-topmost", True)
|
|
23
|
+
root.geometry(f"{w}x{h}+{x}+{y}")
|
|
24
|
+
|
|
25
|
+
bg = "black"
|
|
26
|
+
root.configure(bg=bg)
|
|
27
|
+
try:
|
|
28
|
+
root.wm_attributes("-transparentcolor", bg)
|
|
29
|
+
except tk.TclError:
|
|
30
|
+
# Some X11 window managers do not support transparentcolor.
|
|
31
|
+
root.attributes("-alpha", 0.35)
|
|
32
|
+
|
|
33
|
+
canvas = tk.Canvas(root, width=w, height=h, highlightthickness=0, bg=bg)
|
|
34
|
+
canvas.pack(fill="both", expand=True)
|
|
35
|
+
inset = max(1, thickness // 2)
|
|
36
|
+
canvas.create_rectangle(
|
|
37
|
+
inset,
|
|
38
|
+
inset,
|
|
39
|
+
max(inset + 1, w - inset),
|
|
40
|
+
max(inset + 1, h - inset),
|
|
41
|
+
outline=color,
|
|
42
|
+
width=thickness,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
root.after(duration_ms, root.destroy)
|
|
46
|
+
root.mainloop()
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
raise SystemExit(main())
|
|
File without changes
|