antigravity-remote 3.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,199 @@
1
+ """Monitoring command handlers for Antigravity Remote."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import time
6
+ from datetime import datetime
7
+
8
+ from telegram import Update
9
+ from telegram.ext import ContextTypes
10
+ from telegram.constants import ParseMode
11
+
12
+ from .base import is_authorized
13
+ from ..config import config
14
+ from ..state import state
15
+ from ..utils import take_screenshot, cleanup_screenshot, scan_screen, detect_keywords
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ async def heartbeat_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
21
+ """Start/stop heartbeat screenshots."""
22
+ if not await is_authorized(update):
23
+ return
24
+
25
+ args = context.args
26
+
27
+ # Cancel existing heartbeat
28
+ if state.heartbeat_task:
29
+ state.heartbeat_task.cancel()
30
+ state.heartbeat_task = None
31
+
32
+ if not args or args[0] == "off":
33
+ await update.message.reply_text("💓 Heartbeat stopped.")
34
+ return
35
+
36
+ try:
37
+ minutes = int(args[0])
38
+ except ValueError:
39
+ await update.message.reply_text("Usage: /heartbeat <minutes> or /heartbeat off")
40
+ return
41
+
42
+ async def heartbeat_loop():
43
+ while True:
44
+ await asyncio.sleep(minutes * 60)
45
+ path = await asyncio.to_thread(take_screenshot)
46
+ if path:
47
+ await context.bot.send_photo(
48
+ chat_id=update.effective_chat.id,
49
+ photo=open(path, 'rb'),
50
+ caption=f"💓 Heartbeat - {datetime.now().strftime('%H:%M')}"
51
+ )
52
+ cleanup_screenshot(path)
53
+
54
+ state.heartbeat_task = asyncio.create_task(heartbeat_loop())
55
+ await update.message.reply_text(f"💓 Heartbeat started! Screenshot every {minutes} minutes.")
56
+
57
+
58
+ async def watchdog_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
59
+ """Start/stop smart watchdog monitoring."""
60
+ if not await is_authorized(update):
61
+ return
62
+
63
+ args = context.args
64
+
65
+ # Cancel existing watchdog
66
+ if state.watchdog_task:
67
+ state.watchdog_task.cancel()
68
+ state.watchdog_task = None
69
+
70
+ if args and args[0] == "off":
71
+ await update.message.reply_text("🐕 Watchdog stopped.")
72
+ return
73
+
74
+ check_interval = config.watchdog_interval
75
+ alert_cooldown = config.alert_cooldown
76
+
77
+ async def watchdog_loop():
78
+ while True:
79
+ await asyncio.sleep(check_interval)
80
+
81
+ try:
82
+ screen_text, current_hash = await asyncio.to_thread(scan_screen)
83
+ current_time = time.time()
84
+
85
+ # Activity monitoring
86
+ if current_hash == state.watchdog_last_hash:
87
+ state.watchdog_idle_count += 1
88
+ else:
89
+ state.watchdog_idle_count = 0
90
+ state.watchdog_last_hash = current_hash
91
+
92
+ # Check for keywords
93
+ detection = detect_keywords(screen_text)
94
+
95
+ if detection and current_time - state.watchdog_last_alert > alert_cooldown:
96
+ category, keyword = detection
97
+ state.watchdog_last_alert = current_time
98
+
99
+ path = await asyncio.to_thread(take_screenshot)
100
+ if path:
101
+ captions = {
102
+ 'approval': f"🚨 *Approval needed!*\nDetected: `{keyword}`",
103
+ 'done': f"✅ *Task appears complete!*\nDetected: `{keyword}`",
104
+ 'error': f"⚠️ *Error detected!*\nDetected: `{keyword}`",
105
+ }
106
+
107
+ await context.bot.send_photo(
108
+ chat_id=update.effective_chat.id,
109
+ photo=open(path, 'rb'),
110
+ caption=captions.get(category, f"Detected: `{keyword}`"),
111
+ parse_mode=ParseMode.MARKDOWN
112
+ )
113
+ cleanup_screenshot(path)
114
+
115
+ # Idle detection (2+ cycles with no change)
116
+ if (state.watchdog_idle_count >= 2 and
117
+ current_time - state.watchdog_last_alert > 60):
118
+
119
+ state.watchdog_last_alert = current_time
120
+ state.watchdog_idle_count = 0
121
+
122
+ path = await asyncio.to_thread(take_screenshot)
123
+ if path:
124
+ await context.bot.send_photo(
125
+ chat_id=update.effective_chat.id,
126
+ photo=open(path, 'rb'),
127
+ caption="💤 *Screen idle* - No activity detected",
128
+ parse_mode=ParseMode.MARKDOWN
129
+ )
130
+ cleanup_screenshot(path)
131
+
132
+ except Exception as e:
133
+ logger.error(f"Watchdog error: {e}")
134
+
135
+ state.watchdog_task = asyncio.create_task(watchdog_loop())
136
+
137
+ await update.message.reply_text(
138
+ "🐕 *Watchdog started!*\n\n"
139
+ "I'll alert you when:\n"
140
+ "• 🚨 Approval is needed\n"
141
+ "• ✅ Task appears complete\n"
142
+ "• ⚠️ Errors are detected\n"
143
+ "• 💤 Screen goes idle\n\n"
144
+ "Use `/watchdog off` to stop.",
145
+ parse_mode=ParseMode.MARKDOWN
146
+ )
147
+
148
+
149
+ async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
150
+ """Schedule a command for later."""
151
+ if not await is_authorized(update):
152
+ return
153
+
154
+ args = context.args
155
+ if len(args) < 2:
156
+ await update.message.reply_text("Usage: /schedule 5m /status\nTime: 30s, 5m, 1h")
157
+ return
158
+
159
+ time_str = args[0].lower()
160
+ scheduled_cmd = ' '.join(args[1:])
161
+
162
+ # Parse time
163
+ try:
164
+ if time_str.endswith('s'):
165
+ seconds = int(time_str[:-1])
166
+ elif time_str.endswith('m'):
167
+ seconds = int(time_str[:-1]) * 60
168
+ elif time_str.endswith('h'):
169
+ seconds = int(time_str[:-1]) * 3600
170
+ else:
171
+ seconds = int(time_str)
172
+ except ValueError:
173
+ await update.message.reply_text("Invalid time format. Use: 30s, 5m, 1h")
174
+ return
175
+
176
+ await update.message.reply_text(
177
+ f"⏰ Scheduled `{scheduled_cmd}` in {time_str}",
178
+ parse_mode=ParseMode.MARKDOWN
179
+ )
180
+
181
+ async def run_scheduled():
182
+ await asyncio.sleep(seconds)
183
+
184
+ if 'status' in scheduled_cmd.lower() or 'screenshot' in scheduled_cmd.lower():
185
+ path = await asyncio.to_thread(take_screenshot)
186
+ if path:
187
+ await context.bot.send_photo(
188
+ chat_id=update.effective_chat.id,
189
+ photo=open(path, 'rb'),
190
+ caption="⏰ Scheduled screenshot"
191
+ )
192
+ cleanup_screenshot(path)
193
+ else:
194
+ await context.bot.send_message(
195
+ chat_id=update.effective_chat.id,
196
+ text=f"⏰ Timer complete for: {scheduled_cmd}"
197
+ )
198
+
199
+ asyncio.create_task(run_scheduled())
@@ -0,0 +1,135 @@
1
+ """Quick command handlers for Antigravity Remote."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import os
6
+ import tempfile
7
+
8
+ from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
9
+ from telegram.ext import ContextTypes
10
+ from telegram.constants import ParseMode
11
+
12
+ from .base import is_authorized
13
+ from ..utils import send_to_antigravity, take_screenshot, cleanup_screenshot
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Quick reply options
18
+ QUICK_REPLIES = [
19
+ ("✅ Yes", "quick_yes"),
20
+ ("❌ No", "quick_no"),
21
+ ("▶️ Proceed", "quick_proceed"),
22
+ ("⏹️ Cancel", "quick_cancel"),
23
+ ("👍 Approve", "quick_approve"),
24
+ ("⏭️ Skip", "quick_skip"),
25
+ ]
26
+
27
+ QUICK_TEXTS = {
28
+ "quick_yes": "Yes",
29
+ "quick_no": "No",
30
+ "quick_proceed": "Proceed",
31
+ "quick_cancel": "Cancel",
32
+ "quick_approve": "Approve",
33
+ "quick_skip": "Skip",
34
+ }
35
+
36
+
37
+ async def quick_replies_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
38
+ """Show quick reply buttons."""
39
+ if not await is_authorized(update):
40
+ return
41
+
42
+ keyboard = [
43
+ [InlineKeyboardButton("✅ Yes", callback_data="quick_yes"),
44
+ InlineKeyboardButton("❌ No", callback_data="quick_no")],
45
+ [InlineKeyboardButton("▶️ Proceed", callback_data="quick_proceed"),
46
+ InlineKeyboardButton("⏹️ Cancel", callback_data="quick_cancel")],
47
+ [InlineKeyboardButton("👍 Approve", callback_data="quick_approve"),
48
+ InlineKeyboardButton("⏭️ Skip", callback_data="quick_skip")],
49
+ ]
50
+
51
+ await update.message.reply_text(
52
+ "⚡ *Quick Replies* - Tap to send:",
53
+ reply_markup=InlineKeyboardMarkup(keyboard),
54
+ parse_mode=ParseMode.MARKDOWN
55
+ )
56
+
57
+
58
+ async def handle_quick_callback(
59
+ query,
60
+ context: ContextTypes.DEFAULT_TYPE,
61
+ action: str
62
+ ) -> None:
63
+ """Handle quick reply callback."""
64
+ text = QUICK_TEXTS.get(action, action.capitalize())
65
+ success = await asyncio.to_thread(send_to_antigravity, text)
66
+
67
+ if success:
68
+ await query.message.reply_text(f"📤 Sent: *{text}*", parse_mode=ParseMode.MARKDOWN)
69
+ else:
70
+ await query.message.reply_text("❌ Failed to send")
71
+
72
+
73
+ async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
74
+ """Handle voice messages - download, transcribe, and relay."""
75
+ if not await is_authorized(update):
76
+ return
77
+
78
+ voice = update.message.voice
79
+ status_msg = await update.message.reply_text("🎤 Processing voice message...")
80
+
81
+ try:
82
+ # Download voice file
83
+ file = await context.bot.get_file(voice.file_id)
84
+ voice_path = os.path.join(tempfile.gettempdir(), f"voice_{voice.file_id}.ogg")
85
+ await file.download_to_drive(voice_path)
86
+
87
+ # Try to transcribe with speech recognition
88
+ try:
89
+ import speech_recognition as sr
90
+ recognizer = sr.Recognizer()
91
+
92
+ # Convert ogg to wav
93
+ wav_path = voice_path.replace('.ogg', '.wav')
94
+ os.system(f'ffmpeg -i "{voice_path}" -y "{wav_path}" 2>nul')
95
+
96
+ with sr.AudioFile(wav_path) as source:
97
+ audio = recognizer.record(source)
98
+ text = recognizer.recognize_google(audio)
99
+
100
+ # Cleanup
101
+ if os.path.exists(wav_path):
102
+ os.remove(wav_path)
103
+ if os.path.exists(voice_path):
104
+ os.remove(voice_path)
105
+
106
+ # Relay transcribed text
107
+ await status_msg.edit_text(
108
+ f"🎤 Transcribed: *{text}*\n\nSending...",
109
+ parse_mode=ParseMode.MARKDOWN
110
+ )
111
+ success = await asyncio.to_thread(send_to_antigravity, text)
112
+
113
+ if success:
114
+ keyboard = [[InlineKeyboardButton("📸 Get Result", callback_data="screenshot")]]
115
+ await status_msg.edit_text(
116
+ f"✅ *Voice relayed:*\n`{text}`",
117
+ reply_markup=InlineKeyboardMarkup(keyboard),
118
+ parse_mode=ParseMode.MARKDOWN
119
+ )
120
+ else:
121
+ await status_msg.edit_text("❌ Failed to relay voice message")
122
+
123
+ except ImportError:
124
+ await status_msg.edit_text(
125
+ "⚠️ Voice transcription not available.\n"
126
+ "Install: `pip install SpeechRecognition`",
127
+ parse_mode=ParseMode.MARKDOWN
128
+ )
129
+ except Exception as e:
130
+ await status_msg.edit_text(f"⚠️ Transcription failed: {e}")
131
+ if os.path.exists(voice_path):
132
+ os.remove(voice_path)
133
+
134
+ except Exception as e:
135
+ await status_msg.edit_text(f"❌ Error processing voice: {e}")
@@ -0,0 +1,108 @@
1
+ """Screen command handlers for Antigravity Remote."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import time
6
+
7
+ import pyautogui
8
+
9
+ from telegram import Update
10
+ from telegram.ext import ContextTypes
11
+ from telegram.constants import ParseMode
12
+
13
+ from .base import is_authorized
14
+ from ..utils import focus_antigravity, take_screenshot, cleanup_screenshot, scroll_screen
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
20
+ """Take and send a screenshot."""
21
+ if not await is_authorized(update):
22
+ return
23
+
24
+ msg = await update.message.reply_text("📸 Capturing...")
25
+ path = await asyncio.to_thread(take_screenshot)
26
+
27
+ if path:
28
+ await context.bot.send_photo(
29
+ chat_id=update.effective_chat.id,
30
+ photo=open(path, 'rb'),
31
+ caption="🖥️ Current screen"
32
+ )
33
+ cleanup_screenshot(path)
34
+ else:
35
+ await update.message.reply_text("❌ Failed to capture screenshot")
36
+
37
+ await msg.delete()
38
+
39
+
40
+ async def scroll_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
41
+ """Scroll the screen."""
42
+ if not await is_authorized(update):
43
+ return
44
+
45
+ args = context.args
46
+ direction = "down"
47
+ multiplier = 1
48
+
49
+ # Parse args: /scroll up x50 or /scroll down 10 or /scroll bottom
50
+ for arg in args:
51
+ if arg == "bottom":
52
+ direction = "down"
53
+ multiplier = 100
54
+ elif arg == "top":
55
+ direction = "up"
56
+ multiplier = 100
57
+ elif arg in ["up", "down"]:
58
+ direction = arg
59
+ elif arg.startswith("x") and arg[1:].isdigit():
60
+ multiplier = int(arg[1:])
61
+ elif arg.isdigit():
62
+ multiplier = int(arg)
63
+
64
+ # Calculate scroll amount
65
+ base_clicks = 25
66
+ clicks = base_clicks * multiplier
67
+ if direction == "down":
68
+ clicks = -clicks
69
+
70
+ success = await asyncio.to_thread(scroll_screen, clicks)
71
+
72
+ if success:
73
+ await update.message.reply_text(f"📜 Scrolled {direction} x{multiplier}")
74
+ else:
75
+ await update.message.reply_text("❌ Failed to scroll")
76
+
77
+
78
+ async def accept_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
79
+ """Send Accept (Alt+Enter)."""
80
+ if not await is_authorized(update):
81
+ return
82
+
83
+ await asyncio.to_thread(
84
+ lambda: (focus_antigravity(), time.sleep(0.2), pyautogui.hotkey('alt', 'enter'))
85
+ )
86
+ await update.message.reply_text("✅ Sent Accept (Alt+Enter)")
87
+
88
+
89
+ async def reject_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
90
+ """Send Reject (Escape)."""
91
+ if not await is_authorized(update):
92
+ return
93
+
94
+ await asyncio.to_thread(
95
+ lambda: (focus_antigravity(), time.sleep(0.2), pyautogui.press('escape'))
96
+ )
97
+ await update.message.reply_text("❌ Sent Reject (Escape)")
98
+
99
+
100
+ async def undo_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
101
+ """Send Undo (Ctrl+Z)."""
102
+ if not await is_authorized(update):
103
+ return
104
+
105
+ await asyncio.to_thread(
106
+ lambda: (focus_antigravity(), pyautogui.hotkey('ctrl', 'z'))
107
+ )
108
+ await update.message.reply_text("↩️ Sent Undo (Ctrl+Z)")
@@ -0,0 +1,76 @@
1
+ """State management for Antigravity Remote bot."""
2
+
3
+ import asyncio
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime
6
+ from typing import Any, Optional
7
+
8
+
9
+ @dataclass
10
+ class CommandLogEntry:
11
+ """A single command log entry."""
12
+ timestamp: datetime
13
+ message: str
14
+
15
+ def to_dict(self) -> dict[str, Any]:
16
+ return {
17
+ "time": self.timestamp.strftime("%H:%M:%S"),
18
+ "msg": self.message
19
+ }
20
+
21
+
22
+ @dataclass
23
+ class BotState:
24
+ """Mutable state for the Antigravity Remote bot."""
25
+
26
+ # Control state
27
+ paused: bool = False
28
+ locked: bool = False
29
+
30
+ # Background tasks
31
+ heartbeat_task: Optional[asyncio.Task] = None
32
+ watchdog_task: Optional[asyncio.Task] = None
33
+
34
+ # Watchdog state
35
+ watchdog_last_alert: float = 0.0
36
+ watchdog_last_hash: Optional[int] = None
37
+ watchdog_idle_count: int = 0
38
+
39
+ # Command history
40
+ command_log: list[CommandLogEntry] = field(default_factory=list)
41
+ max_log_entries: int = 100
42
+
43
+ def log_command(self, message: str) -> None:
44
+ """Add a command to the log."""
45
+ self.command_log.append(
46
+ CommandLogEntry(timestamp=datetime.now(), message=message)
47
+ )
48
+ # Trim log if too long
49
+ if len(self.command_log) > self.max_log_entries:
50
+ self.command_log = self.command_log[-50:]
51
+
52
+ def get_recent_logs(self, count: int = 10) -> list[CommandLogEntry]:
53
+ """Get the most recent log entries."""
54
+ return self.command_log[-count:]
55
+
56
+ def cancel_tasks(self) -> None:
57
+ """Cancel all background tasks."""
58
+ if self.heartbeat_task:
59
+ self.heartbeat_task.cancel()
60
+ self.heartbeat_task = None
61
+ if self.watchdog_task:
62
+ self.watchdog_task.cancel()
63
+ self.watchdog_task = None
64
+
65
+ def reset(self) -> None:
66
+ """Reset state to defaults."""
67
+ self.paused = False
68
+ self.locked = False
69
+ self.cancel_tasks()
70
+ self.watchdog_last_alert = 0.0
71
+ self.watchdog_last_hash = None
72
+ self.watchdog_idle_count = 0
73
+
74
+
75
+ # Global state instance
76
+ state = BotState()
@@ -0,0 +1,16 @@
1
+ """Utility modules for Antigravity Remote."""
2
+
3
+ from .automation import focus_antigravity, send_to_antigravity, send_key_combo, scroll_screen
4
+ from .screenshot import take_screenshot, cleanup_screenshot
5
+ from .ocr import scan_screen, detect_keywords
6
+
7
+ __all__ = [
8
+ "focus_antigravity",
9
+ "send_to_antigravity",
10
+ "send_key_combo",
11
+ "scroll_screen",
12
+ "take_screenshot",
13
+ "cleanup_screenshot",
14
+ "scan_screen",
15
+ "detect_keywords",
16
+ ]
@@ -0,0 +1,143 @@
1
+ """Window automation utilities for Antigravity Remote."""
2
+
3
+ import logging
4
+ import time
5
+ from typing import Optional
6
+
7
+ import pyautogui
8
+ import pygetwindow as gw
9
+ import pyperclip
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Configure pyautogui safety settings
14
+ pyautogui.FAILSAFE = True
15
+ pyautogui.PAUSE = 0.1
16
+
17
+
18
+ def focus_antigravity() -> bool:
19
+ """
20
+ Focus the Antigravity/VS Code/Cursor window.
21
+
22
+ Returns:
23
+ True if window was focused, False otherwise.
24
+ """
25
+ try:
26
+ # Try different window titles in priority order
27
+ window_titles = ['Antigravity IDE', 'Antigravity', 'Visual Studio Code', 'Cursor']
28
+
29
+ for title in window_titles:
30
+ windows = gw.getWindowsWithTitle(title)
31
+ if windows:
32
+ win = windows[0]
33
+ if win.isMinimized:
34
+ win.restore()
35
+ win.activate()
36
+ time.sleep(0.3)
37
+ return True
38
+
39
+ logger.warning("No Antigravity/VS Code/Cursor window found")
40
+ return False
41
+
42
+ except Exception as e:
43
+ logger.error(f"Error focusing window: {e}")
44
+ return False
45
+
46
+
47
+ def send_to_antigravity(message: str) -> bool:
48
+ """
49
+ Send a message to the Antigravity chat input.
50
+
51
+ Args:
52
+ message: The message to send.
53
+
54
+ Returns:
55
+ True if message was sent, False otherwise.
56
+ """
57
+ try:
58
+ if not focus_antigravity():
59
+ return False
60
+
61
+ time.sleep(0.3)
62
+ screen_width, screen_height = pyautogui.size()
63
+
64
+ # Click in the chat input area (right side, near bottom)
65
+ chat_input_x = int(screen_width * 0.75)
66
+ chat_input_y = int(screen_height * 0.92)
67
+
68
+ pyautogui.click(chat_input_x, chat_input_y)
69
+ time.sleep(0.3)
70
+
71
+ # Select all and paste new message
72
+ pyautogui.hotkey('ctrl', 'a')
73
+ time.sleep(0.1)
74
+
75
+ pyperclip.copy(message)
76
+ pyautogui.hotkey('ctrl', 'v')
77
+ time.sleep(0.2)
78
+
79
+ # Send message
80
+ pyautogui.press('enter')
81
+
82
+ logger.info(f"Sent message to Antigravity: {message[:50]}...")
83
+ return True
84
+
85
+ except Exception as e:
86
+ logger.error(f"Error sending to Antigravity: {e}")
87
+ return False
88
+
89
+
90
+ def send_key_combo(keys: list[str]) -> bool:
91
+ """
92
+ Send a keyboard shortcut.
93
+
94
+ Args:
95
+ keys: List of keys to press together (e.g., ['ctrl', 's']).
96
+
97
+ Returns:
98
+ True if keys were sent, False otherwise.
99
+ """
100
+ try:
101
+ if not focus_antigravity():
102
+ return False
103
+
104
+ time.sleep(0.2)
105
+ pyautogui.hotkey(*keys)
106
+ logger.info(f"Sent key combo: {'+'.join(keys)}")
107
+ return True
108
+
109
+ except Exception as e:
110
+ logger.error(f"Error sending key combo: {e}")
111
+ return False
112
+
113
+
114
+ def scroll_screen(clicks: int, x_percent: float = 0.80, y_percent: float = 0.40) -> bool:
115
+ """
116
+ Scroll the screen at specified position.
117
+
118
+ Args:
119
+ clicks: Number of scroll clicks (positive = up, negative = down).
120
+ x_percent: Horizontal position as percentage of screen width.
121
+ y_percent: Vertical position as percentage of screen height.
122
+
123
+ Returns:
124
+ True if scroll was performed, False otherwise.
125
+ """
126
+ try:
127
+ if not focus_antigravity():
128
+ return False
129
+
130
+ screen_width, screen_height = pyautogui.size()
131
+ x = int(screen_width * x_percent)
132
+ y = int(screen_height * y_percent)
133
+
134
+ pyautogui.moveTo(x, y)
135
+ time.sleep(0.1)
136
+ pyautogui.scroll(clicks)
137
+
138
+ logger.info(f"Scrolled {clicks} clicks at ({x}, {y})")
139
+ return True
140
+
141
+ except Exception as e:
142
+ logger.error(f"Error scrolling: {e}")
143
+ return False