mouse-keepalive 1.5.2__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.
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 motoish
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include LICENSE
3
+ recursive-include src/mouse_keepalive *.py
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.4
2
+ Name: mouse-keepalive
3
+ Version: 1.5.2
4
+ Summary: A cross-platform tool to automatically move the mouse at specified intervals to prevent system sleep or lock
5
+ Author: motoish
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/motoish/mouse-keepalive
8
+ Project-URL: Documentation, https://github.com/motoish/mouse-keepalive#readme
9
+ Project-URL: Repository, https://github.com/motoish/mouse-keepalive
10
+ Project-URL: Issues, https://github.com/motoish/mouse-keepalive/issues
11
+ Keywords: mouse,automation,prevent-sleep,idle,keep-alive,cross-platform,cli,tool,macos,windows,linux
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: End Users/Desktop
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: System :: Hardware
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.11
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: pyautogui>=0.9.54
27
+ Provides-Extra: dev
28
+ Requires-Dist: flake8>=6.0.0; extra == "dev"
29
+ Requires-Dist: black>=23.0.0; extra == "dev"
30
+ Requires-Dist: pylint>=2.17.0; extra == "dev"
31
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
32
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
33
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
34
+ Requires-Dist: yamllint>=1.32.0; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # Mouse Keepalive
38
+
39
+ **中文** | [English](README_EN.md)
40
+
41
+ 跨平台 CLI 工具:按固定间隔轻微移动鼠标并立即回位,避免系统因长时间无操作而休眠或锁屏。
42
+ 支持 **macOS / Windows / Linux**。
43
+
44
+ ---
45
+
46
+ ## ✨ 功能亮点
47
+
48
+ - 跨平台(Python + [pyautogui](https://pypi.org/project/PyAutoGUI/))
49
+ - 可配置移动间隔与总运行时长
50
+ - 瞬时微移(约 1–2 像素),日常使用几乎无感
51
+ - **PyPI** 与 **npm** 均可安装;命令行别名 `mka`
52
+ - 默认输出每次移动日志;`-q` 可完全静默
53
+
54
+ ---
55
+
56
+ ## 📋 环境要求
57
+
58
+ | 方式 | 要求 |
59
+ |------|------|
60
+ | **PyPI / pipx**(推荐) | Python **3.11+** |
61
+ | **npm** | Node.js **14+**,且本机已安装 **Python 3.11+**(npm 包为启动器,实际逻辑在 Python 中) |
62
+ | **从源码运行** | Python 3.11+;见下方「本地开发」 |
63
+
64
+ ---
65
+
66
+ ## 📦 安装
67
+
68
+ ### PyPI(推荐)
69
+
70
+ ```bash
71
+ pip install mouse-keepalive
72
+ # 或(隔离环境,推荐桌面使用)
73
+ pipx install mouse-keepalive
74
+ ```
75
+
76
+ ### npm
77
+
78
+ ```bash
79
+ npm install -g mouse-keepalive
80
+ ```
81
+
82
+ 首次运行前请确保已安装 Python 3.11+;若缺少 `pyautogui`,npm 入口会尝试自动 `pip install pyautogui`。
83
+
84
+ ---
85
+
86
+ ## 🚀 快速开始
87
+
88
+ ```bash
89
+ # 默认:每 60 秒移动一次,Ctrl+C 停止
90
+ mka
91
+
92
+ # 每 30 秒移动一次
93
+ mka -i 30
94
+
95
+ # 每 2 分钟移动一次,共运行 1 小时
96
+ mka -i 120 -d 3600
97
+
98
+ # 静默模式(无日志)
99
+ mka -q
100
+
101
+ # 帮助
102
+ mka --help
103
+ ```
104
+
105
+ 等效命令:
106
+
107
+ ```text
108
+ mouse-keepalive
109
+ python -m mouse_keepalive
110
+ ```
111
+
112
+ ---
113
+
114
+ ## ⚙️ 命令行参数
115
+
116
+ | 参数 | 说明 | 默认 |
117
+ |------|------|------|
118
+ | `-i, --interval` | 移动间隔(秒) | `60` |
119
+ | `-d, --duration` | 运行总时长(秒);省略则一直运行 | 无限 |
120
+ | `-q, --quiet` | 静默模式,不输出日志 | 关闭 |
121
+ | `-h, --help` | 显示帮助 | — |
122
+
123
+ ---
124
+
125
+ ## 🧠 工作原理
126
+
127
+ 程序按间隔获取当前光标位置,偏移 1–2 像素后**立即移回**,使系统仍检测到鼠标活动,同时尽量不打扰正常使用。
128
+
129
+ ---
130
+
131
+ ## 🛠 本地开发
132
+
133
+ 仓库采用 **src 布局**,包路径为 `src/mouse_keepalive/`(对外导入名仍为 `mouse_keepalive`)。
134
+
135
+ ```bash
136
+ git clone https://github.com/motoish/mouse-keepalive.git
137
+ cd mouse-keepalive
138
+
139
+ # 可编辑安装 + 开发依赖
140
+ pip install -e ".[dev]"
141
+
142
+ # 方式一:仓库根目录脚本(改代码后无需重装)
143
+ ./move_mouse.sh
144
+ ./move_mouse.sh -i 30 -q
145
+
146
+ # 方式二:Makefile
147
+ make run ARGS="-i 30"
148
+
149
+ # 方式三:安装后的 CLI
150
+ mka -i 30
151
+
152
+ # 测试与检查
153
+ make test
154
+ make lint
155
+ ```
156
+
157
+ `move_mouse.sh` 会优先使用 `.venv/bin/python`,并通过 `PYTHONPATH=src` 加载源码;**不会**随 PyPI/npm 包发布。
158
+
159
+ ### 目录结构(简要)
160
+
161
+ ```text
162
+ mouse-keepalive/
163
+ ├── src/mouse_keepalive/ # Python 包
164
+ ├── bin/ # npm CLI(转发到 python -m mouse_keepalive)
165
+ ├── tests/
166
+ ├── move_mouse.sh # 仅本地开发
167
+ └── pyproject.toml
168
+ ```
169
+
170
+ ---
171
+
172
+ ## ⚠️ 注意事项
173
+
174
+ - 使用 **Ctrl + C** 可随时退出
175
+ - **macOS** 需在「系统设置 → 隐私与安全性 → 辅助功能」中允许终端或 Python
176
+ - 企业安全策略可能拦截模拟鼠标;若无效请联系管理员或加入白名单
177
+
178
+ ---
179
+
180
+ ## 📦 发布与版本
181
+
182
+ 用户安装版本以 [PyPI](https://pypi.org/project/mouse-keepalive/) / [npm](https://www.npmjs.com/package/mouse-keepalive) 为准。
183
+ 变更记录见 [CHANGELOG.md](CHANGELOG.md)。仓库使用 [release-please](https://github.com/googleapis/release-please) 管理版本;合并 Release PR 后会发布 `v*` 标签并自动发布到 npm / PyPI。
184
+
185
+ ---
186
+
187
+ ## 📄 许可证
188
+
189
+ [MIT](LICENSE)
@@ -0,0 +1,153 @@
1
+ # Mouse Keepalive
2
+
3
+ **中文** | [English](README_EN.md)
4
+
5
+ 跨平台 CLI 工具:按固定间隔轻微移动鼠标并立即回位,避免系统因长时间无操作而休眠或锁屏。
6
+ 支持 **macOS / Windows / Linux**。
7
+
8
+ ---
9
+
10
+ ## ✨ 功能亮点
11
+
12
+ - 跨平台(Python + [pyautogui](https://pypi.org/project/PyAutoGUI/))
13
+ - 可配置移动间隔与总运行时长
14
+ - 瞬时微移(约 1–2 像素),日常使用几乎无感
15
+ - **PyPI** 与 **npm** 均可安装;命令行别名 `mka`
16
+ - 默认输出每次移动日志;`-q` 可完全静默
17
+
18
+ ---
19
+
20
+ ## 📋 环境要求
21
+
22
+ | 方式 | 要求 |
23
+ |------|------|
24
+ | **PyPI / pipx**(推荐) | Python **3.11+** |
25
+ | **npm** | Node.js **14+**,且本机已安装 **Python 3.11+**(npm 包为启动器,实际逻辑在 Python 中) |
26
+ | **从源码运行** | Python 3.11+;见下方「本地开发」 |
27
+
28
+ ---
29
+
30
+ ## 📦 安装
31
+
32
+ ### PyPI(推荐)
33
+
34
+ ```bash
35
+ pip install mouse-keepalive
36
+ # 或(隔离环境,推荐桌面使用)
37
+ pipx install mouse-keepalive
38
+ ```
39
+
40
+ ### npm
41
+
42
+ ```bash
43
+ npm install -g mouse-keepalive
44
+ ```
45
+
46
+ 首次运行前请确保已安装 Python 3.11+;若缺少 `pyautogui`,npm 入口会尝试自动 `pip install pyautogui`。
47
+
48
+ ---
49
+
50
+ ## 🚀 快速开始
51
+
52
+ ```bash
53
+ # 默认:每 60 秒移动一次,Ctrl+C 停止
54
+ mka
55
+
56
+ # 每 30 秒移动一次
57
+ mka -i 30
58
+
59
+ # 每 2 分钟移动一次,共运行 1 小时
60
+ mka -i 120 -d 3600
61
+
62
+ # 静默模式(无日志)
63
+ mka -q
64
+
65
+ # 帮助
66
+ mka --help
67
+ ```
68
+
69
+ 等效命令:
70
+
71
+ ```text
72
+ mouse-keepalive
73
+ python -m mouse_keepalive
74
+ ```
75
+
76
+ ---
77
+
78
+ ## ⚙️ 命令行参数
79
+
80
+ | 参数 | 说明 | 默认 |
81
+ |------|------|------|
82
+ | `-i, --interval` | 移动间隔(秒) | `60` |
83
+ | `-d, --duration` | 运行总时长(秒);省略则一直运行 | 无限 |
84
+ | `-q, --quiet` | 静默模式,不输出日志 | 关闭 |
85
+ | `-h, --help` | 显示帮助 | — |
86
+
87
+ ---
88
+
89
+ ## 🧠 工作原理
90
+
91
+ 程序按间隔获取当前光标位置,偏移 1–2 像素后**立即移回**,使系统仍检测到鼠标活动,同时尽量不打扰正常使用。
92
+
93
+ ---
94
+
95
+ ## 🛠 本地开发
96
+
97
+ 仓库采用 **src 布局**,包路径为 `src/mouse_keepalive/`(对外导入名仍为 `mouse_keepalive`)。
98
+
99
+ ```bash
100
+ git clone https://github.com/motoish/mouse-keepalive.git
101
+ cd mouse-keepalive
102
+
103
+ # 可编辑安装 + 开发依赖
104
+ pip install -e ".[dev]"
105
+
106
+ # 方式一:仓库根目录脚本(改代码后无需重装)
107
+ ./move_mouse.sh
108
+ ./move_mouse.sh -i 30 -q
109
+
110
+ # 方式二:Makefile
111
+ make run ARGS="-i 30"
112
+
113
+ # 方式三:安装后的 CLI
114
+ mka -i 30
115
+
116
+ # 测试与检查
117
+ make test
118
+ make lint
119
+ ```
120
+
121
+ `move_mouse.sh` 会优先使用 `.venv/bin/python`,并通过 `PYTHONPATH=src` 加载源码;**不会**随 PyPI/npm 包发布。
122
+
123
+ ### 目录结构(简要)
124
+
125
+ ```text
126
+ mouse-keepalive/
127
+ ├── src/mouse_keepalive/ # Python 包
128
+ ├── bin/ # npm CLI(转发到 python -m mouse_keepalive)
129
+ ├── tests/
130
+ ├── move_mouse.sh # 仅本地开发
131
+ └── pyproject.toml
132
+ ```
133
+
134
+ ---
135
+
136
+ ## ⚠️ 注意事项
137
+
138
+ - 使用 **Ctrl + C** 可随时退出
139
+ - **macOS** 需在「系统设置 → 隐私与安全性 → 辅助功能」中允许终端或 Python
140
+ - 企业安全策略可能拦截模拟鼠标;若无效请联系管理员或加入白名单
141
+
142
+ ---
143
+
144
+ ## 📦 发布与版本
145
+
146
+ 用户安装版本以 [PyPI](https://pypi.org/project/mouse-keepalive/) / [npm](https://www.npmjs.com/package/mouse-keepalive) 为准。
147
+ 变更记录见 [CHANGELOG.md](CHANGELOG.md)。仓库使用 [release-please](https://github.com/googleapis/release-please) 管理版本;合并 Release PR 后会发布 `v*` 标签并自动发布到 npm / PyPI。
148
+
149
+ ---
150
+
151
+ ## 📄 许可证
152
+
153
+ [MIT](LICENSE)
@@ -0,0 +1,108 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mouse-keepalive"
7
+ version = "1.5.2"
8
+ description = "A cross-platform tool to automatically move the mouse at specified intervals to prevent system sleep or lock"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "motoish"}
14
+ ]
15
+ keywords = [
16
+ "mouse",
17
+ "automation",
18
+ "prevent-sleep",
19
+ "idle",
20
+ "keep-alive",
21
+ "cross-platform",
22
+ "cli",
23
+ "tool",
24
+ "macos",
25
+ "windows",
26
+ "linux"
27
+ ]
28
+ classifiers = [
29
+ "Development Status :: 4 - Beta",
30
+ "Intended Audience :: End Users/Desktop",
31
+ "License :: OSI Approved :: MIT License",
32
+ "Operating System :: MacOS",
33
+ "Operating System :: Microsoft :: Windows",
34
+ "Operating System :: POSIX :: Linux",
35
+ "Programming Language :: Python :: 3",
36
+ "Programming Language :: Python :: 3.11",
37
+ "Programming Language :: Python :: 3.12",
38
+ "Topic :: System :: Hardware",
39
+ "Topic :: Utilities",
40
+ ]
41
+ dependencies = [
42
+ "pyautogui>=0.9.54",
43
+ ]
44
+
45
+ [project.optional-dependencies]
46
+ dev = [
47
+ "flake8>=6.0.0",
48
+ "black>=23.0.0",
49
+ "pylint>=2.17.0",
50
+ "mypy>=1.0.0",
51
+ "pytest>=7.0.0",
52
+ "pytest-cov>=4.0.0",
53
+ "yamllint>=1.32.0",
54
+ ]
55
+
56
+ [project.scripts]
57
+ mouse-keepalive = "mouse_keepalive.move_mouse:main"
58
+ mka = "mouse_keepalive.move_mouse:main"
59
+
60
+ [project.urls]
61
+ Homepage = "https://github.com/motoish/mouse-keepalive"
62
+ Documentation = "https://github.com/motoish/mouse-keepalive#readme"
63
+ Repository = "https://github.com/motoish/mouse-keepalive"
64
+ Issues = "https://github.com/motoish/mouse-keepalive/issues"
65
+
66
+ [tool.setuptools.package-dir]
67
+ "" = "src"
68
+
69
+ [tool.setuptools.packages.find]
70
+ where = ["src"]
71
+
72
+ [tool.pytest.ini_options]
73
+ pythonpath = ["src"]
74
+ testpaths = ["tests"]
75
+
76
+ [tool.black]
77
+ line-length = 120
78
+ target-version = ['py311']
79
+ include = '\.pyi?$'
80
+ extend-exclude = '''
81
+ /(
82
+ # directories
83
+ \.eggs
84
+ | \.git
85
+ | \.hg
86
+ | \.mypy_cache
87
+ | \.tox
88
+ | \.venv
89
+ | venv
90
+ | _build
91
+ | buck-out
92
+ | build
93
+ | dist
94
+ )/
95
+ '''
96
+
97
+ [tool.mypy]
98
+ python_version = "3.11"
99
+ warn_return_any = true
100
+ warn_unused_configs = true
101
+ ignore_missing_imports = true
102
+ disallow_untyped_defs = false
103
+ no_implicit_optional = true
104
+
105
+ [[tool.mypy.overrides]]
106
+ module = "pyautogui.*"
107
+ ignore_missing_imports = true
108
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,16 @@
1
+ """
2
+ 自动移动鼠标工具 / Mouse Keepalive Tool
3
+ 支持 macOS、Windows 和 Linux 系统 / Supports macOS, Windows and Linux
4
+
5
+ 模块化架构:
6
+ - 核心逻辑在 move_mouse 模块中实现
7
+ - 所有入口点(CLI、模块调用)都使用相同的核心逻辑
8
+ - 支持依赖注入,便于测试
9
+ """
10
+
11
+ __version__ = "1.5.2"
12
+ __author__ = "motoish"
13
+
14
+ from .move_mouse import move_mouse, main, MouseMover, MouseController, MousePosition, ScreenSize
15
+
16
+ __all__ = ["move_mouse", "main", "MouseMover", "MouseController", "MousePosition", "ScreenSize"]
@@ -0,0 +1,9 @@
1
+ """
2
+ 命令行入口点 / Command Line Entry Point
3
+ 允许使用 python -m mouse_keepalive 运行 / Allows running with python -m mouse_keepalive
4
+ """
5
+
6
+ from .move_mouse import main
7
+
8
+ if __name__ == "__main__":
9
+ main()
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 自动移动鼠标脚本 / Mouse Keepalive Script
4
+ 支持 macOS、Windows 和 Linux 系统 / Supports macOS, Windows and Linux
5
+ """
6
+
7
+ import argparse
8
+ import sys
9
+ import time
10
+ import platform
11
+ from typing import Optional, Callable, Tuple
12
+ from dataclasses import dataclass
13
+
14
+ try:
15
+ import pyautogui
16
+ except ImportError:
17
+ print("错误: 未安装 pyautogui 库")
18
+ print("Error: pyautogui library not installed")
19
+ print("请运行: pip install pyautogui")
20
+ print("Please run: pip install pyautogui")
21
+ sys.exit(1)
22
+
23
+ # 禁用 pyautogui 的安全功能(防止鼠标移到屏幕角落时停止)
24
+ # Disable pyautogui failsafe (prevents stopping when mouse moves to screen corner)
25
+ pyautogui.FAILSAFE = False
26
+
27
+
28
+ @dataclass
29
+ class MousePosition:
30
+ """鼠标位置数据类 / Mouse position data class"""
31
+
32
+ x: int
33
+ y: int
34
+
35
+
36
+ @dataclass
37
+ class ScreenSize:
38
+ """屏幕尺寸数据类 / Screen size data class"""
39
+
40
+ width: int
41
+ height: int
42
+
43
+
44
+ class MouseController:
45
+ """
46
+ 鼠标控制器接口 / Mouse controller interface
47
+ 封装鼠标操作,便于测试时替换实现
48
+ """
49
+
50
+ def get_position(self) -> MousePosition:
51
+ """获取当前鼠标位置 / Get current mouse position"""
52
+ x, y = pyautogui.position()
53
+ return MousePosition(x, y)
54
+
55
+ def get_screen_size(self) -> ScreenSize:
56
+ """获取屏幕尺寸 / Get screen size"""
57
+ width, height = pyautogui.size()
58
+ return ScreenSize(width, height)
59
+
60
+ def move_to(self, x: int, y: int, duration: float = 0) -> None:
61
+ """移动鼠标到指定位置 / Move mouse to specified position"""
62
+ pyautogui.moveTo(x, y, duration=duration)
63
+
64
+
65
+ class MouseMover:
66
+ """
67
+ 核心鼠标移动逻辑类 / Core mouse movement logic class
68
+ 这个类包含所有核心逻辑,不依赖具体的 I/O 实现,便于测试
69
+ """
70
+
71
+ def __init__(
72
+ self,
73
+ controller: Optional[MouseController] = None,
74
+ time_func: Optional[Callable[[], float]] = None,
75
+ sleep_func: Optional[Callable[[float], None]] = None,
76
+ print_func: Optional[Callable[[str], None]] = None,
77
+ ):
78
+ """
79
+ 初始化鼠标移动器 / Initialize mouse mover
80
+
81
+ Args:
82
+ controller: 鼠标控制器,默认使用真实实现 / Mouse controller, defaults to real implementation
83
+ time_func: 时间函数,默认使用 time.time / Time function, defaults to time.time
84
+ sleep_func: 睡眠函数,默认使用 time.sleep / Sleep function, defaults to time.sleep
85
+ print_func: 打印函数,默认使用 print / Print function, defaults to print
86
+ """
87
+ self.controller = controller or MouseController()
88
+ self.time_func = time_func or time.time
89
+ self.sleep_func = sleep_func or time.sleep
90
+ self.print_func = print_func or print
91
+
92
+ def calculate_next_position(
93
+ self, current_pos: MousePosition, screen_size: ScreenSize, move_count: int
94
+ ) -> Tuple[int, int]:
95
+ """
96
+ 计算下一个鼠标位置 / Calculate next mouse position
97
+
98
+ Args:
99
+ current_pos: 当前鼠标位置 / Current mouse position
100
+ screen_size: 屏幕尺寸 / Screen size
101
+ move_count: 移动计数(用于交替方向)/ Move count (for alternating direction)
102
+
103
+ Returns:
104
+ 下一个位置的 (x, y) 坐标 / Next position (x, y) coordinates
105
+ """
106
+ # 移动鼠标到稍微不同的位置(移动1-2像素)
107
+ # Move mouse to slightly different position (move 1-2 pixels)
108
+ offset_x = 1 if move_count % 2 == 0 else -1
109
+ offset_y = 1 if move_count % 2 == 0 else -1
110
+
111
+ new_x = current_pos.x + offset_x
112
+ new_y = current_pos.y + offset_y
113
+
114
+ # 确保坐标在屏幕范围内 / Ensure coordinates are within screen bounds
115
+ new_x = max(1, min(new_x, screen_size.width - 1))
116
+ new_y = max(1, min(new_y, screen_size.height - 1))
117
+
118
+ return new_x, new_y
119
+
120
+ def perform_move(self, move_count: int) -> Tuple[MousePosition, int]:
121
+ """
122
+ 执行一次鼠标移动 / Perform one mouse movement
123
+
124
+ Args:
125
+ move_count: 当前移动计数 / Current move count
126
+
127
+ Returns:
128
+ (原始位置, 新的移动计数) / (Original position, new move count)
129
+ """
130
+ # 获取当前鼠标位置 / Get current mouse position
131
+ current_pos = self.controller.get_position()
132
+
133
+ # 获取屏幕尺寸 / Get screen size
134
+ screen_size = self.controller.get_screen_size()
135
+
136
+ # 计算下一个位置 / Calculate next position
137
+ new_x, new_y = self.calculate_next_position(current_pos, screen_size, move_count)
138
+
139
+ # 移动鼠标 / Move mouse
140
+ self.controller.move_to(new_x, new_y, duration=0)
141
+ move_count += 1
142
+
143
+ # 立即移回原位置(这样用户感觉不到鼠标移动)
144
+ # Immediately move back to original position (user won't notice the movement)
145
+ self.controller.move_to(current_pos.x, current_pos.y, duration=0)
146
+
147
+ return current_pos, move_count
148
+
149
+ def run(
150
+ self,
151
+ interval: int = 60,
152
+ duration: Optional[int] = None,
153
+ *,
154
+ on_start: Optional[Callable[[], None]] = None,
155
+ on_move: Optional[Callable[[int, MousePosition, float], None]] = None,
156
+ on_finish: Optional[Callable[[int, float], None]] = None,
157
+ ) -> Tuple[int, float]:
158
+ """
159
+ 运行鼠标移动循环 / Run mouse movement loop
160
+
161
+ Args:
162
+ interval: 移动间隔(秒)/ Movement interval (seconds)
163
+ duration: 运行时长(秒),None 表示无限运行 / Duration (seconds), None means infinite
164
+ on_start: 启动回调函数 / Start callback function
165
+ on_move: 移动回调函数 / Move callback function
166
+ on_finish: 完成回调函数 / Finish callback function
167
+
168
+ Returns:
169
+ (移动次数, 运行时长) / (Move count, duration)
170
+ """
171
+ if on_start:
172
+ on_start()
173
+
174
+ start_time = self.time_func()
175
+ move_count = 0
176
+
177
+ try:
178
+ while True:
179
+ # 执行移动 / Perform move
180
+ current_pos, move_count = self.perform_move(move_count)
181
+
182
+ elapsed = self.time_func() - start_time
183
+
184
+ if on_move:
185
+ on_move(move_count, current_pos, elapsed)
186
+
187
+ # 检查是否达到运行时长 / Check if duration is reached
188
+ if duration and elapsed >= duration:
189
+ if on_finish:
190
+ on_finish(move_count, elapsed)
191
+ break
192
+
193
+ # 等待指定间隔 / Wait for specified interval
194
+ self.sleep_func(interval)
195
+
196
+ except KeyboardInterrupt:
197
+ elapsed = self.time_func() - start_time
198
+ if on_finish:
199
+ on_finish(move_count, elapsed)
200
+ raise
201
+
202
+ elapsed = self.time_func() - start_time
203
+ return move_count, elapsed
204
+
205
+
206
+ def _make_log_callbacks(
207
+ mover: MouseMover,
208
+ interval: int,
209
+ duration: Optional[int],
210
+ quiet: bool,
211
+ ) -> Tuple[
212
+ Optional[Callable[[], None]],
213
+ Optional[Callable[[int, MousePosition, float], None]],
214
+ Optional[Callable[[int, float], None]],
215
+ ]:
216
+ """构建日志回调;静默模式返回 (None, None, None)。"""
217
+ if quiet:
218
+ return None, None, None
219
+
220
+ def on_start() -> None:
221
+ mover.print_func("开始自动移动鼠标... / Starting mouse keepalive...")
222
+ mover.print_func(f"间隔 {interval}s" + (f",时长 {duration}s" if duration else ",无限运行(Ctrl+C 停止)"))
223
+ mover.print_func(f"OS: {platform.system()}")
224
+ mover.print_func("-" * 50)
225
+
226
+ def on_move(move_count: int, current_pos: MousePosition, elapsed: float) -> None:
227
+ mover.print_func(f"[{int(elapsed)}s] 已移动 {move_count} 次 " f"(位置: {current_pos.x}, {current_pos.y})")
228
+
229
+ def on_finish(move_count: int, elapsed: float) -> None:
230
+ if duration:
231
+ mover.print_func(f"\n已达运行时长 {duration}s,共移动 {move_count} 次")
232
+ else:
233
+ mover.print_func(f"\n已停止,共移动 {move_count} 次,运行 {int(elapsed)}s")
234
+
235
+ return on_start, on_move, on_finish
236
+
237
+
238
+ def move_mouse(interval: int = 60, duration: Optional[int] = None, quiet: bool = False) -> None:
239
+ """
240
+ 自动移动鼠标 / Automatically move mouse
241
+ 这是主要的公共 API,保持向后兼容性
242
+
243
+ Args:
244
+ interval: 移动间隔(秒),默认60秒 / Movement interval (seconds), default 60
245
+ duration: 运行时长(秒),None表示无限运行 / Duration (seconds), None means infinite
246
+ quiet: 静默模式,不输出日志 / Quiet mode, no log output
247
+ """
248
+ mover = MouseMover()
249
+ on_start, on_move, on_finish = _make_log_callbacks(mover, interval, duration, quiet)
250
+
251
+ try:
252
+ mover.run(
253
+ interval=interval,
254
+ duration=duration,
255
+ on_start=on_start,
256
+ on_move=on_move,
257
+ on_finish=on_finish,
258
+ )
259
+ except KeyboardInterrupt:
260
+ # on_finish 已经在 KeyboardInterrupt 处理中调用
261
+ pass
262
+
263
+
264
+ def main():
265
+ """命令行入口函数 / Command line entry function"""
266
+ parser = argparse.ArgumentParser(
267
+ description="自动移动鼠标工具(防止系统进入休眠或锁定) / Mouse keepalive tool (prevents system sleep or lock)",
268
+ formatter_class=argparse.RawDescriptionHelpFormatter,
269
+ epilog="""
270
+ 示例 / Examples:
271
+ mouse-keepalive # 每60秒移动一次,无限运行 / Move every 60s, infinite
272
+ mouse-keepalive -i 30 # 每30秒移动一次 / Move every 30s
273
+ mouse-keepalive -i 120 -d 3600 # 每120秒移动一次,运行1小时 / Move every 120s, run for 1 hour
274
+ mouse-keepalive -q # 静默模式 / Quiet mode
275
+ mka -i 30 # 使用简短别名 / Use short alias
276
+ python -m mouse_keepalive # 使用模块方式运行 / Run as module
277
+ """,
278
+ )
279
+
280
+ parser.add_argument(
281
+ "-i",
282
+ "--interval",
283
+ type=int,
284
+ default=60,
285
+ help="鼠标移动间隔(秒),默认60秒 / Mouse movement interval (seconds), default 60",
286
+ )
287
+
288
+ parser.add_argument(
289
+ "-d",
290
+ "--duration",
291
+ type=int,
292
+ default=None,
293
+ help="运行时长(秒),默认无限运行 / Duration (seconds), default infinite",
294
+ )
295
+
296
+ parser.add_argument(
297
+ "-q",
298
+ "--quiet",
299
+ action="store_true",
300
+ help="静默模式,不输出日志 / Quiet mode, no log output",
301
+ )
302
+
303
+ args = parser.parse_args()
304
+
305
+ if args.interval < 1:
306
+ print("错误: 移动间隔必须大于0")
307
+ print("Error: Interval must be greater than 0")
308
+ sys.exit(1)
309
+
310
+ if args.duration is not None and args.duration < 1:
311
+ print("错误: 运行时长必须大于0")
312
+ print("Error: Duration must be greater than 0")
313
+ sys.exit(1)
314
+
315
+ move_mouse(interval=args.interval, duration=args.duration, quiet=args.quiet)
316
+
317
+
318
+ if __name__ == "__main__":
319
+ main()
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.4
2
+ Name: mouse-keepalive
3
+ Version: 1.5.2
4
+ Summary: A cross-platform tool to automatically move the mouse at specified intervals to prevent system sleep or lock
5
+ Author: motoish
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/motoish/mouse-keepalive
8
+ Project-URL: Documentation, https://github.com/motoish/mouse-keepalive#readme
9
+ Project-URL: Repository, https://github.com/motoish/mouse-keepalive
10
+ Project-URL: Issues, https://github.com/motoish/mouse-keepalive/issues
11
+ Keywords: mouse,automation,prevent-sleep,idle,keep-alive,cross-platform,cli,tool,macos,windows,linux
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: End Users/Desktop
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: System :: Hardware
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.11
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: pyautogui>=0.9.54
27
+ Provides-Extra: dev
28
+ Requires-Dist: flake8>=6.0.0; extra == "dev"
29
+ Requires-Dist: black>=23.0.0; extra == "dev"
30
+ Requires-Dist: pylint>=2.17.0; extra == "dev"
31
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
32
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
33
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
34
+ Requires-Dist: yamllint>=1.32.0; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # Mouse Keepalive
38
+
39
+ **中文** | [English](README_EN.md)
40
+
41
+ 跨平台 CLI 工具:按固定间隔轻微移动鼠标并立即回位,避免系统因长时间无操作而休眠或锁屏。
42
+ 支持 **macOS / Windows / Linux**。
43
+
44
+ ---
45
+
46
+ ## ✨ 功能亮点
47
+
48
+ - 跨平台(Python + [pyautogui](https://pypi.org/project/PyAutoGUI/))
49
+ - 可配置移动间隔与总运行时长
50
+ - 瞬时微移(约 1–2 像素),日常使用几乎无感
51
+ - **PyPI** 与 **npm** 均可安装;命令行别名 `mka`
52
+ - 默认输出每次移动日志;`-q` 可完全静默
53
+
54
+ ---
55
+
56
+ ## 📋 环境要求
57
+
58
+ | 方式 | 要求 |
59
+ |------|------|
60
+ | **PyPI / pipx**(推荐) | Python **3.11+** |
61
+ | **npm** | Node.js **14+**,且本机已安装 **Python 3.11+**(npm 包为启动器,实际逻辑在 Python 中) |
62
+ | **从源码运行** | Python 3.11+;见下方「本地开发」 |
63
+
64
+ ---
65
+
66
+ ## 📦 安装
67
+
68
+ ### PyPI(推荐)
69
+
70
+ ```bash
71
+ pip install mouse-keepalive
72
+ # 或(隔离环境,推荐桌面使用)
73
+ pipx install mouse-keepalive
74
+ ```
75
+
76
+ ### npm
77
+
78
+ ```bash
79
+ npm install -g mouse-keepalive
80
+ ```
81
+
82
+ 首次运行前请确保已安装 Python 3.11+;若缺少 `pyautogui`,npm 入口会尝试自动 `pip install pyautogui`。
83
+
84
+ ---
85
+
86
+ ## 🚀 快速开始
87
+
88
+ ```bash
89
+ # 默认:每 60 秒移动一次,Ctrl+C 停止
90
+ mka
91
+
92
+ # 每 30 秒移动一次
93
+ mka -i 30
94
+
95
+ # 每 2 分钟移动一次,共运行 1 小时
96
+ mka -i 120 -d 3600
97
+
98
+ # 静默模式(无日志)
99
+ mka -q
100
+
101
+ # 帮助
102
+ mka --help
103
+ ```
104
+
105
+ 等效命令:
106
+
107
+ ```text
108
+ mouse-keepalive
109
+ python -m mouse_keepalive
110
+ ```
111
+
112
+ ---
113
+
114
+ ## ⚙️ 命令行参数
115
+
116
+ | 参数 | 说明 | 默认 |
117
+ |------|------|------|
118
+ | `-i, --interval` | 移动间隔(秒) | `60` |
119
+ | `-d, --duration` | 运行总时长(秒);省略则一直运行 | 无限 |
120
+ | `-q, --quiet` | 静默模式,不输出日志 | 关闭 |
121
+ | `-h, --help` | 显示帮助 | — |
122
+
123
+ ---
124
+
125
+ ## 🧠 工作原理
126
+
127
+ 程序按间隔获取当前光标位置,偏移 1–2 像素后**立即移回**,使系统仍检测到鼠标活动,同时尽量不打扰正常使用。
128
+
129
+ ---
130
+
131
+ ## 🛠 本地开发
132
+
133
+ 仓库采用 **src 布局**,包路径为 `src/mouse_keepalive/`(对外导入名仍为 `mouse_keepalive`)。
134
+
135
+ ```bash
136
+ git clone https://github.com/motoish/mouse-keepalive.git
137
+ cd mouse-keepalive
138
+
139
+ # 可编辑安装 + 开发依赖
140
+ pip install -e ".[dev]"
141
+
142
+ # 方式一:仓库根目录脚本(改代码后无需重装)
143
+ ./move_mouse.sh
144
+ ./move_mouse.sh -i 30 -q
145
+
146
+ # 方式二:Makefile
147
+ make run ARGS="-i 30"
148
+
149
+ # 方式三:安装后的 CLI
150
+ mka -i 30
151
+
152
+ # 测试与检查
153
+ make test
154
+ make lint
155
+ ```
156
+
157
+ `move_mouse.sh` 会优先使用 `.venv/bin/python`,并通过 `PYTHONPATH=src` 加载源码;**不会**随 PyPI/npm 包发布。
158
+
159
+ ### 目录结构(简要)
160
+
161
+ ```text
162
+ mouse-keepalive/
163
+ ├── src/mouse_keepalive/ # Python 包
164
+ ├── bin/ # npm CLI(转发到 python -m mouse_keepalive)
165
+ ├── tests/
166
+ ├── move_mouse.sh # 仅本地开发
167
+ └── pyproject.toml
168
+ ```
169
+
170
+ ---
171
+
172
+ ## ⚠️ 注意事项
173
+
174
+ - 使用 **Ctrl + C** 可随时退出
175
+ - **macOS** 需在「系统设置 → 隐私与安全性 → 辅助功能」中允许终端或 Python
176
+ - 企业安全策略可能拦截模拟鼠标;若无效请联系管理员或加入白名单
177
+
178
+ ---
179
+
180
+ ## 📦 发布与版本
181
+
182
+ 用户安装版本以 [PyPI](https://pypi.org/project/mouse-keepalive/) / [npm](https://www.npmjs.com/package/mouse-keepalive) 为准。
183
+ 变更记录见 [CHANGELOG.md](CHANGELOG.md)。仓库使用 [release-please](https://github.com/googleapis/release-please) 管理版本;合并 Release PR 后会发布 `v*` 标签并自动发布到 npm / PyPI。
184
+
185
+ ---
186
+
187
+ ## 📄 许可证
188
+
189
+ [MIT](LICENSE)
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ src/mouse_keepalive/__init__.py
6
+ src/mouse_keepalive/__main__.py
7
+ src/mouse_keepalive/move_mouse.py
8
+ src/mouse_keepalive.egg-info/PKG-INFO
9
+ src/mouse_keepalive.egg-info/SOURCES.txt
10
+ src/mouse_keepalive.egg-info/dependency_links.txt
11
+ src/mouse_keepalive.egg-info/entry_points.txt
12
+ src/mouse_keepalive.egg-info/requires.txt
13
+ src/mouse_keepalive.egg-info/top_level.txt
14
+ tests/test_move_mouse.py
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ mka = mouse_keepalive.move_mouse:main
3
+ mouse-keepalive = mouse_keepalive.move_mouse:main
@@ -0,0 +1,10 @@
1
+ pyautogui>=0.9.54
2
+
3
+ [dev]
4
+ flake8>=6.0.0
5
+ black>=23.0.0
6
+ pylint>=2.17.0
7
+ mypy>=1.0.0
8
+ pytest>=7.0.0
9
+ pytest-cov>=4.0.0
10
+ yamllint>=1.32.0
@@ -0,0 +1 @@
1
+ mouse_keepalive
@@ -0,0 +1,210 @@
1
+ """
2
+ Tests for mouse_keepalive.move_mouse module
3
+ """
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ import pytest
8
+ from unittest.mock import Mock, patch, MagicMock
9
+
10
+ src_root = Path(__file__).parent.parent / "src"
11
+ sys.path.insert(0, str(src_root))
12
+
13
+ from mouse_keepalive.move_mouse import (
14
+ MouseMover,
15
+ MouseController,
16
+ MousePosition,
17
+ ScreenSize,
18
+ move_mouse,
19
+ main,
20
+ )
21
+
22
+ # Get the module object from sys.modules for patching
23
+ move_mouse_module = sys.modules['mouse_keepalive.move_mouse']
24
+
25
+
26
+ class TestMouseMoverCore:
27
+ def test_calculate_next_position(self):
28
+ mover = MouseMover()
29
+ screen = ScreenSize(1920, 1080)
30
+
31
+ assert mover.calculate_next_position(MousePosition(100, 200), screen, 0) == (101, 201)
32
+ assert mover.calculate_next_position(MousePosition(100, 200), screen, 1) == (99, 199)
33
+
34
+ x, y = mover.calculate_next_position(MousePosition(0, 0), screen, 0)
35
+ assert (x, y) == (1, 1)
36
+
37
+ x, y = mover.calculate_next_position(MousePosition(1920, 1080), screen, 1)
38
+ assert (x, y) == (1919, 1079)
39
+
40
+ def test_perform_move_calls_controller(self):
41
+ ctrl = Mock(spec=MouseController)
42
+ ctrl.get_position.return_value = MousePosition(100, 200)
43
+ ctrl.get_screen_size.return_value = ScreenSize(1920, 1080)
44
+
45
+ mover = MouseMover(controller=ctrl)
46
+ original, next_count = mover.perform_move(move_count=0)
47
+
48
+ assert original == MousePosition(100, 200)
49
+ assert next_count == 1
50
+ assert ctrl.move_to.call_count == 2
51
+
52
+ def test_run_interval_and_duration(self):
53
+ ctrl = Mock(spec=MouseController)
54
+ ctrl.get_position.return_value = MousePosition(100, 200)
55
+ ctrl.get_screen_size.return_value = ScreenSize(1920, 1080)
56
+
57
+ times = [0, 0.1, 1.1, 2.1, 2.1]
58
+ mock_time = MagicMock(side_effect=times)
59
+ mock_sleep = MagicMock()
60
+
61
+ mover = MouseMover(controller=ctrl, time_func=mock_time, sleep_func=mock_sleep)
62
+ move_count, elapsed = mover.run(interval=1, duration=2)
63
+
64
+ assert move_count >= 2
65
+ assert elapsed == 2.1
66
+ assert mock_sleep.call_count >= 2
67
+
68
+ def test_run_callbacks_are_called(self):
69
+ ctrl = Mock(spec=MouseController)
70
+ ctrl.get_position.return_value = MousePosition(100, 200)
71
+ ctrl.get_screen_size.return_value = ScreenSize(1920, 1080)
72
+
73
+ times = [0, 0.1, 1.1, 1.1]
74
+ mock_time = MagicMock(side_effect=times)
75
+ mock_sleep = MagicMock()
76
+
77
+ on_start = Mock()
78
+ on_move = Mock()
79
+ on_finish = Mock()
80
+
81
+ mover = MouseMover(controller=ctrl, time_func=mock_time, sleep_func=mock_sleep)
82
+ mover.run(
83
+ interval=1, duration=1,
84
+ on_start=on_start, on_move=on_move, on_finish=on_finish
85
+ )
86
+
87
+ on_start.assert_called_once()
88
+ assert on_move.call_count >= 1
89
+ on_finish.assert_called_once()
90
+
91
+
92
+ class TestMoveMouseAPI:
93
+ @patch.object(move_mouse_module, "MouseController")
94
+ @patch("time.sleep")
95
+ @patch("time.time")
96
+ def test_move_mouse_basic(self, mock_time, mock_sleep, mock_controller_cls):
97
+ mock_controller = Mock(spec=MouseController)
98
+ mock_controller.get_position.return_value = MousePosition(100, 200)
99
+ mock_controller.get_screen_size.return_value = ScreenSize(1920, 1080)
100
+ mock_controller_cls.return_value = mock_controller
101
+
102
+ mock_time.side_effect = [0, 0.1, 1.1, 1.1]
103
+
104
+ move_mouse(interval=1, duration=1)
105
+
106
+ mock_controller.get_position.assert_called()
107
+ mock_controller.move_to.assert_called()
108
+
109
+ @patch.object(move_mouse_module, "MouseController")
110
+ @patch("time.sleep")
111
+ @patch("time.time")
112
+ def test_move_mouse_bounds(self, mock_time, mock_sleep, mock_controller_cls):
113
+ mock_controller = Mock(spec=MouseController)
114
+ mock_controller.get_position.return_value = MousePosition(0, 0)
115
+ mock_controller.get_screen_size.return_value = ScreenSize(1920, 1080)
116
+ mock_controller_cls.return_value = mock_controller
117
+
118
+ mock_time.side_effect = [0, 0.1, 1.1, 1.1]
119
+
120
+ move_mouse(interval=1, duration=1)
121
+
122
+ # Check that move_to was called with valid coordinates
123
+ # First call should be to new position (clamped to bounds), second back to original
124
+ move_calls = mock_controller.move_to.call_args_list
125
+ assert len(move_calls) >= 2
126
+
127
+ # First call: move to new position (should be clamped to 1, 1)
128
+ first_call = move_calls[0]
129
+ x1, y1 = first_call.args[0], first_call.args[1]
130
+ assert 1 <= x1 < 1920
131
+ assert 1 <= y1 < 1080
132
+
133
+ # Second call: move back to original position (0, 0) - this is expected
134
+ # The bounds check ensures the new position is valid, not the original
135
+
136
+ @patch.object(move_mouse_module, "MouseController")
137
+ @patch("time.sleep", side_effect=KeyboardInterrupt())
138
+ @patch("time.time")
139
+ def test_interrupt_does_not_crash(self, mock_time, mock_sleep, mock_controller_cls):
140
+ mock_controller = Mock(spec=MouseController)
141
+ mock_controller.get_position.return_value = MousePosition(100, 200)
142
+ mock_controller.get_screen_size.return_value = ScreenSize(1920, 1080)
143
+ mock_controller_cls.return_value = mock_controller
144
+
145
+ mock_time.side_effect = [0, 0.1, 0.1]
146
+ move_mouse(interval=1, duration=None)
147
+
148
+
149
+ class TestMainCLI:
150
+ @patch.object(move_mouse_module, "move_mouse")
151
+ def test_main_with_interval(self, mock_move):
152
+ import sys
153
+ orig = sys.argv
154
+ sys.argv = ["mouse-keepalive", "-i", "30"]
155
+ try:
156
+ main()
157
+ finally:
158
+ sys.argv = orig
159
+
160
+ mock_move.assert_called_once_with(interval=30, duration=None, quiet=False)
161
+
162
+ @patch.object(move_mouse_module, "move_mouse")
163
+ def test_main_with_quiet(self, mock_move):
164
+ import sys
165
+ orig = sys.argv
166
+ sys.argv = ["mouse-keepalive", "-q"]
167
+ try:
168
+ main()
169
+ finally:
170
+ sys.argv = orig
171
+
172
+ mock_move.assert_called_once_with(interval=60, duration=None, quiet=True)
173
+
174
+ @patch.object(move_mouse_module, "move_mouse")
175
+ @patch("builtins.print")
176
+ def test_main_invalid_params_exit(self, mock_print, mock_move_mouse):
177
+ """Test that main() handles invalid parameters correctly"""
178
+ import sys
179
+ orig_argv = sys.argv
180
+
181
+ for argv in [
182
+ ["mouse-keepalive", "-i", "0"],
183
+ ["mouse-keepalive", "-i", "-5"],
184
+ ["mouse-keepalive", "-d", "0"],
185
+ ]:
186
+ sys.argv = argv
187
+ # sys.exit will be called, which raises SystemExit
188
+ # pytest may handle this, so we just verify error message was printed
189
+ try:
190
+ main()
191
+ except SystemExit:
192
+ pass # Expected - sys.exit raises SystemExit
193
+
194
+ # Verify error message was printed
195
+ print_messages = []
196
+ for call in mock_print.call_args_list:
197
+ if call.args:
198
+ print_messages.append(str(call.args[0]))
199
+
200
+ assert any("错误" in msg or "Error" in msg for msg in print_messages), \
201
+ f"Expected error message for argv {argv}, got: {print_messages}"
202
+
203
+ # move_mouse should not be called for invalid params
204
+ # (even if sys.exit doesn't work, we've mocked move_mouse to prevent infinite loops)
205
+ mock_move_mouse.assert_not_called()
206
+
207
+ mock_print.reset_mock()
208
+ mock_move_mouse.reset_mock()
209
+
210
+ sys.argv = orig_argv