robotframework-appiumwindows 0.1.0__py3-none-any.whl → 0.1.2__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.
@@ -1,553 +1,551 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- import base64
4
- import math
5
- import os
6
- from pathlib import Path
7
-
8
- from appium.webdriver.mobilecommand import MobileCommand as Command
9
- from robot.libraries.BuiltIn import BuiltIn
10
- from selenium.common import InvalidArgumentException
11
-
12
- from AppiumLibrary import utils
13
- from AppiumLibrary.locators import ElementFinder
14
- from .keywordgroup import KeywordGroup
15
-
16
- SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "scripts"))
17
-
18
-
19
- class _PowershellKeywords(KeywordGroup):
20
- def __init__(self):
21
- self._element_finder = ElementFinder()
22
- self._bi = BuiltIn()
23
-
24
- # Public, element lookups
25
- # Powershell command, need appium server allow shell, eg: appium --relaxed-security
26
-
27
- def appium_ps_click(
28
- self,
29
- locator=None,
30
- x=0,
31
- y=0,
32
- button='left',
33
- **kwargs
34
- ):
35
- """
36
- Click using PowerShell-level mouse simulation.
37
-
38
- Supports:
39
- - Absolute coordinates or element-based location via locator
40
- - Optional offsets (absolute or 'center')
41
- - Custom mouse button
42
-
43
- kwargs:
44
- - offset: sets both x/y offset globally (overrides individual)
45
- - x_offset / y_offset
46
-
47
- @param locator:
48
- @param x:
49
- @param y:
50
- @param button:
51
- 'left' - single left click
52
- 'right' - single right click
53
- 'middle' - single middle click
54
- 'double' - double left click
55
- 'triple' - triple left click
56
- 'right-double' - double right click
57
-
58
- """
59
- self._info(f"Appium Ps Click")
60
-
61
- if locator:
62
- rect = self.get_element_rect(locator)
63
- x, y = self._parse_location(rect, kwargs, 'x_offset', 'y_offset')
64
- root = self._current_application().get_window_rect()
65
- self._info(f"[DEBUG] App window rect: {root}")
66
- x, y = x + root['x'], y + root['y']
67
-
68
- self._info(f"[CLICK] {button.upper()} Button: ({x},{y}))")
69
-
70
- ps_command = self._generate_click_command(x, y, button)
71
- self._info(f"ps_command: \n{ps_command}")
72
-
73
- self.appium_execute_powershell_command(ps_command)
74
-
75
- def appium_ps_sendkeys(self, text: str):
76
- """
77
- SendKeys can found at: https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.sendkeys?view=windowsdesktop-10.0
78
-
79
- To specify that any combination of SHIFT, CTRL, and ALT should be held down while several other keys are pressed,
80
- enclose the code for those keys in parentheses. For example, to specify to hold down SHIFT while E and C are pressed,
81
- use "+(EC)". To specify to hold down SHIFT while E is pressed, followed by C without SHIFT, use "+EC".
82
-
83
- To specify repeating keys, use the form {key number}. You must put a space between key and number.
84
- For example, {LEFT 42} means press the LEFT ARROW key 42 times; {h 10} means press H 10 times.
85
-
86
- @param text: text to sendkeys
87
- eg1: text = 123qwe{TAB}iop{ENTER}+a~ABC~
88
- eg2: text = "{+}" ({%}) ({^})
89
- eg3: text = This is test{LEFT}{BACKSPACE}x
90
- eg4: text = 123qwe{BACKSPACE 3}{TAB}{ENTER}
91
- @return:
92
- """
93
- self._info(f"Appium Ps Sendkeys: {text}")
94
- text = text.replace('"', '""')
95
-
96
- ps_command = (
97
- f'Add-Type -AssemblyName System.Windows.Forms;'
98
- f'[System.Windows.Forms.SendKeys]::SendWait("{text}")'
99
- )
100
- self._info(f'command: {ps_command}')
101
-
102
- self.appium_execute_powershell_command(ps_command)
103
-
104
- def appium_ps_drag_and_drop(
105
- self,
106
- start_locator=None,
107
- end_locator=None,
108
- x_start=0,
109
- y_start=0,
110
- x_end=0,
111
- y_end=0,
112
- button='left',
113
- **kwargs
114
- ):
115
- """
116
- Drag and drop using PowerShell-level mouse simulation.
117
-
118
- Supports:
119
- - Absolute coordinates or element-based location via start_locator/end_locator
120
- - Optional offsets (absolute or 'center')
121
- - Custom mouse button (left/right/mid)
122
- - Drag duration (default 0.5 seconds)
123
-
124
- kwargs:
125
- - offset: sets both x/y offset globally (overrides individual)
126
- - x_start_offset / y_start_offset
127
- - x_end_offset / y_end_offset
128
- - duration_sec: total drag time in seconds (default 0.5)
129
- """
130
- self._info(f"Appium Ps Drag And Drop")
131
- # Get actual coordinates from locators (if any)
132
- if start_locator:
133
- start_rect = self.get_element_rect(start_locator)
134
- x_start, y_start = self._parse_location(start_rect, kwargs, 'x_start_offset', 'y_start_offset')
135
-
136
- if end_locator:
137
- end_rect = self.get_element_rect(end_locator)
138
- x_end, y_end = self._parse_location(end_rect, kwargs, 'x_end_offset', 'y_end_offset')
139
-
140
- # Normalize against window position
141
- root = self._current_application().get_window_rect()
142
- self._info(f"[DEBUG] App window rect: {root}")
143
-
144
- x_start, y_start = x_start + root['x'], y_start + root['y']
145
- x_end, y_end = x_end + root['x'], y_end + root['y']
146
-
147
- self._info(f"[DRAG] {button.upper()} Button: From ({x_start},{y_start}) → ({x_end},{y_end})")
148
-
149
- # Get drag duration
150
- duration_sec = float(kwargs.get('duration_sec', 0.5))
151
- ps_command = self._generate_drag_command(x_start, y_start, x_end, y_end, button, duration_sec)
152
- self._info(f"ps_command: \n{ps_command}")
153
-
154
- # Execute PowerShell command
155
- self.appium_execute_powershell_command(ps_command)
156
-
157
- def appium_execute_powershell_command(self, command, handle_exception=False):
158
- """
159
- Executes a PowerShell command using Appium's execute_script method.
160
-
161
- Note:
162
- PowerShell command execution must be allowed on the Appium server.
163
- For this, Appium must be started with the `--relaxed-security` flag:
164
- appium --relaxed-security
165
-
166
- Args:
167
- command (str): The PowerShell command to be executed.
168
- handle_exception (bool): If True, return the exception object on error. Otherwise, return None.
169
-
170
- Returns:
171
- str | dict | Exception: The result of the execution or the exception object.
172
-
173
- Raises:
174
- Exception: If handle_exception is False and an error occurs.
175
- """
176
- try:
177
- driver = self._current_application()
178
- result = driver.execute_script("powerShell", {"command": command})
179
- return result
180
- except Exception as exc:
181
- if handle_exception:
182
- return exc
183
- raise
184
-
185
- def appium_execute_powershell_script(self, ps_script=None, file_path=None, handle_exception=False):
186
- """
187
- Executes a PowerShell script using Appium's execute_script method.
188
-
189
- Note:
190
- PowerShell command execution must be allowed on the Appium server.
191
- For this, Appium must be started with the `--relaxed-security` flag:
192
- appium --relaxed-security
193
-
194
- Args:
195
- ps_script (str): The full PowerShell script to be executed.
196
- file_path (str): The file ps1 to be executed.
197
- handle_exception (bool): If True, return the exception object on failure. If False, return None on failure.
198
-
199
- Returns:
200
- str | dict | Exception: The result of the script execution or the exception object.
201
-
202
- Raises:
203
- Exception: If handle_exception is False and an error occurs.
204
- """
205
- try:
206
- if file_path:
207
- self._info(f'file_path: {file_path}')
208
- ps_script = utils.read_file(file_path)
209
- self._info(f"ps_script: \n{ps_script}")
210
- driver = self._current_application()
211
- result = driver.execute_script("powerShell", {"script": ps_script})
212
- return result
213
- except Exception as exc:
214
- if handle_exception:
215
- return exc
216
- raise
217
-
218
- def appium_pull_file(self, path: str, save_path: str = None) -> str:
219
- """Retrieves the file at `path`.
220
-
221
- Powershell command must be allowed. eg: appium --relaxed-security
222
-
223
- Args:
224
- path: the path to the file on the device, eg: c:/users/user1/desktop/screenshot_file.png
225
- save_path: path to save, eg: /Users/user1/desktop/screenshot.png
226
-
227
- Returns:
228
- The file's contents encoded as Base64.
229
- """
230
-
231
- # base64data = self._current_application().pull_file(path)
232
- base64data = self._current_application().execute(Command.PULL_FILE, {'path': path})['value']
233
-
234
- if save_path:
235
- with open(save_path, "wb") as file:
236
- file.write(base64.b64decode(base64data))
237
-
238
- return base64data
239
-
240
- def appium_pull_folder(self, path: str, save_path_as_zip: str = '') -> str:
241
- """Retrieves a folder at `path`.
242
-
243
- Powershell command must be allowed. eg: appium --relaxed-security
244
-
245
- Args:
246
- path: the path to the folder on the device. eg: c:/users/user1/desktop/folder1
247
- save_path_as_zip: zip file. eg: /Users/user1/desktop/file.zip
248
-
249
- Returns:
250
- The folder's contents zipped and encoded as Base64.
251
- """
252
- # base64data = self._current_application().pull_folder(path)
253
- base64data = self._current_application().execute(Command.PULL_FOLDER, {'path': path})['value']
254
-
255
- if save_path_as_zip:
256
- with open(save_path_as_zip, "wb") as file:
257
- file.write(base64.b64decode(base64data))
258
-
259
- return base64data
260
-
261
- def appium_push_file(self, destination_path: str, source_path: str = None, base64data: str = None):
262
- """Puts the data from the file at `source_path`, encoded as Base64, in the file specified as `path`.
263
-
264
- Specify either `base64data` or `source_path`, if both specified default to `source_path`
265
-
266
- Powershell command must be allowed. eg: appium --relaxed-security
267
-
268
- Args:
269
- destination_path: the location on the device/simulator where the local file contents should be saved.
270
- eg: c:/users/user1/desktop/screenshot_file.png
271
- base64data: file contents, encoded as Base64, to be written
272
- to the file on the device/simulator. Eg: iVBORw0KGgoAAAANSUh...
273
- source_path: local file path for the file to be loaded on device. Eg: /Users/user1/desktop/source_file.png
274
-
275
- Returns:
276
- base64data
277
- """
278
- if source_path is None and base64data is None:
279
- raise InvalidArgumentException('Must either pass base64 data or a local file path')
280
-
281
- if source_path is not None:
282
- try:
283
- with open(source_path, 'rb') as f:
284
- file_data = f.read()
285
- except IOError as e:
286
- message = f'source_path "{source_path}" could not be found. Are you sure the file exists?'
287
- raise InvalidArgumentException(message) from e
288
- base64data = base64.b64encode(file_data).decode('utf-8')
289
-
290
- # result = self._current_application().push_file(destination_path, base64data, source_path)
291
-
292
- self._current_application().execute(Command.PUSH_FILE, {'path': destination_path, 'data': base64data})
293
-
294
- return base64data
295
-
296
- def appium_transfer_file(self, file_path, remote_path):
297
- """
298
- Streams a binary file, base64-encodes it chunk by chunk,
299
- and sends it directly to a remote machine via PowerShell commands.
300
-
301
- Powershell command must be allowed. eg: appium --relaxed-security
302
-
303
- file_path: source file path, eg: c:/users/user1/desktop/screenshot_file.png
304
- remote_path: destination path, eg: c:/users/user1/download/screenshot_file.png
305
- """
306
- file_path = Path(file_path)
307
- remote_path = str(remote_path)
308
- remote_b64_path = remote_path + ".b64.tmp"
309
- chunk_size = 6000 # chunk size in raw bytes (will expand when base64 encoded)
310
-
311
- # 1. Ensure remote parent directory exists
312
- remote_directory = str(Path(remote_path).parent)
313
- mkdir_cmd = f'New-Item -Path "{remote_directory}" -ItemType Directory -Force'
314
- self.execute_script("powerShell", command=mkdir_cmd)
315
- self._info(f"Ensured remote directory: {remote_directory}")
316
-
317
- # 2. Open and stream file, encoding and sending each chunk
318
- with open(file_path, "rb") as f:
319
- chunk_index = 0
320
- while True:
321
- chunk = f.read(chunk_size)
322
- if not chunk:
323
- break
324
- chunk_b64 = base64.b64encode(chunk).decode('utf-8')
325
- escaped_chunk = chunk_b64.replace("`", "``").replace('"', '`"')
326
- ps = f'Add-Content -Path "{remote_b64_path}" -Value "{escaped_chunk}"'
327
- self.execute_script("powerShell", command=ps)
328
- chunk_index += 1
329
- self._info(f"Sent chunk {chunk_index}")
330
-
331
- # 3. Decode and write binary file on remote side
332
- decode_script = (
333
- f'[IO.File]::WriteAllBytes("{remote_path}";'
334
- f'[Convert]::FromBase64String((Get-Content "{remote_b64_path}" -Raw)))'
335
- )
336
- self.execute_script("powerShell", command=decode_script)
337
- self._info(f"File written to: {remote_path}")
338
-
339
- # 4. Optional cleanup
340
- cleanup_script = f'Remove-Item "{remote_b64_path}" -ErrorAction SilentlyContinue'
341
- self.execute_script("powerShell", command=cleanup_script)
342
- self._info("Cleaned up temporary base64 file.")
343
-
344
- def appium_split_and_push_file(self, source_path: str, remote_path: str, chunk_size_mb: int = 20):
345
- """
346
- Splits a binary file into chunks, pushes each to a remote machine via Appium,
347
- then executes a PowerShell script remotely to recombine and clean up chunk files.
348
-
349
- Powershell command must be allowed. eg: appium --relaxed-security
350
-
351
- Parameters:
352
- source_path (str): Local path to the binary file.
353
- remote_path (str): Full remote file path to create from recombination.
354
- chunk_size_mb (int): Size of each chunk in MB (default: 20MB).
355
- """
356
- chunk_size = chunk_size_mb * 1024 * 1024
357
- file_size = os.path.getsize(source_path)
358
-
359
- file_name = os.path.basename(source_path)
360
- remote_dir = os.path.dirname(remote_path)
361
- total_chunks = math.ceil(file_size / chunk_size)
362
- # chunk_index_digits = max(4, len(str(total_chunks - 1))) # at least 4 digits
363
- chunk_index_digits = len(str(total_chunks - 1))
364
-
365
- self._info(f"Splitting '{file_name}' ({file_size} bytes) into {total_chunks} chunks of {chunk_size_mb}MB each")
366
-
367
- # Step 1: Split and push chunks
368
- with open(source_path, "rb") as f:
369
- for index in range(total_chunks):
370
- chunk = f.read(chunk_size)
371
- if not chunk:
372
- break
373
-
374
- b64_chunk = base64.b64encode(chunk).decode("utf-8")
375
- chunk_suffix = f"{index:0{chunk_index_digits}d}"
376
- remote_chunk_path = os.path.join(remote_dir, f"{file_name}.part{chunk_suffix}")
377
-
378
- self.appium_push_file(destination_path=remote_chunk_path, base64data=b64_chunk)
379
- self._info(f"Pushed chunk {chunk_suffix} to {remote_chunk_path}")
380
-
381
- # Step 2: Build PowerShell recombine script
382
- escaped_dir = remote_dir.replace("'", "''")
383
- escaped_out = remote_path.replace("'", "''")
384
- escaped_base = file_name.replace("'", "''")
385
- pad_format = f"D{chunk_index_digits}"
386
-
387
- ps_script_lines = [
388
- f"$out = [System.IO.File]::OpenWrite('{escaped_out}')",
389
- "$out.SetLength(0)",
390
- f"for ($i = 0; $i -lt {total_chunks}; $i++) {{",
391
- f" $chunkName = '{escaped_base}.part' + $i.ToString('{pad_format}')",
392
- f" $chunkPath = Join-Path '{escaped_dir}' $chunkName",
393
- ' if (-not (Test-Path $chunkPath)) { throw "Missing chunk: $chunkPath" }',
394
- " $in = [System.IO.File]::OpenRead($chunkPath)",
395
- " $buffer = New-Object byte[] (1MB)",
396
- " while (($n = $in.Read($buffer, 0, $buffer.Length)) -gt 0) {",
397
- " $out.Write($buffer, 0, $n)",
398
- " }",
399
- " $in.Close()",
400
- "}",
401
- "$out.Close()",
402
- f"for ($i = 0; $i -lt {total_chunks}; $i++) {{",
403
- f" $chunkName = '{escaped_base}.part' + $i.ToString('{pad_format}')",
404
- f" $chunkPath = Join-Path '{escaped_dir}' $chunkName",
405
- " Remove-Item -Path $chunkPath -Force -ErrorAction SilentlyContinue",
406
- "}",
407
- f"Write-Output 'Combine and cleanup complete: {escaped_out}'"
408
- ]
409
-
410
- ps_script = "\n".join(ps_script_lines)
411
-
412
- # Step 3: Execute recombine script remotely
413
- self._info("Combining chunks and cleaning up on remote machine...")
414
- result = self.appium_execute_powershell_script(ps_script)
415
- self._info(f"Remote PowerShell result: {result}")
416
- return escaped_out
417
-
418
- # Private
419
-
420
- def _parse_location(self, rect, kwargs, x_offset_key, y_offset_key):
421
- """Parse offset inside a rect."""
422
- offset = kwargs.get('offset')
423
- x_offset = offset if offset is not None else kwargs.get(x_offset_key, 'center')
424
- y_offset = offset if offset is not None else kwargs.get(y_offset_key, 'center')
425
-
426
- x = rect['x'] + (rect['width'] // 2 if x_offset == 'center' else int(x_offset))
427
- y = rect['y'] + (rect['height'] // 2 if y_offset == 'center' else int(y_offset))
428
- return x, y
429
-
430
- def _generate_click_command(self, x, y, button='left'):
431
- """
432
- Generate a PowerShell command to click at the given coordinates.
433
-
434
- https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mouse_event
435
-
436
- @param x: X coordinate
437
- @param y: Y coordinate
438
- @param button:
439
- 'left' - single left click
440
- 'right' - single right click
441
- 'middle' - single middle click
442
- 'double' - double left click
443
- 'triple' - triple left click
444
- 'right-double' - double right click
445
- @return: PowerShell one-liner as string
446
- """
447
- button = button.lower()
448
-
449
- # Mouse event codes
450
- left_code = 6 # MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP
451
- right_code = 24 # MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP
452
- middle_code = 40 # MOUSEEVENTF_MIDDLEDOWN | MOUSEEVENTF_MIDDLEUP
453
-
454
- # Define all buttons
455
- click_events = {
456
- 'left': f'[M]::mouse_event({left_code},0,0,0,[UIntPtr]::Zero);',
457
- 'right': f'[M]::mouse_event({right_code},0,0,0,[UIntPtr]::Zero);',
458
- 'middle': f'[M]::mouse_event({middle_code},0,0,0,[UIntPtr]::Zero);',
459
- 'double': (
460
- f'[M]::mouse_event({left_code},0,0,0,[UIntPtr]::Zero);'
461
- f'Start-Sleep -m 100;'
462
- f'[M]::mouse_event({left_code},0,0,0,[UIntPtr]::Zero);'
463
- ),
464
- 'triple': (
465
- f'[M]::mouse_event({left_code},0,0,0,[UIntPtr]::Zero);'
466
- f'Start-Sleep -m 100;'
467
- f'[M]::mouse_event({left_code},0,0,0,[UIntPtr]::Zero);'
468
- f'Start-Sleep -m 100;'
469
- f'[M]::mouse_event({left_code},0,0,0,[UIntPtr]::Zero);'
470
- ),
471
- 'right-double': (
472
- f'[M]::mouse_event({right_code},0,0,0,[UIntPtr]::Zero);'
473
- f'Start-Sleep -m 100;'
474
- f'[M]::mouse_event({right_code},0,0,0,[UIntPtr]::Zero);'
475
- )
476
- }
477
-
478
- if button not in click_events:
479
- valid = ', '.join(click_events.keys())
480
- raise ValueError(f"button must be one of: {valid}")
481
-
482
- event_code = click_events[button]
483
-
484
- ps_cmd = (
485
- 'if (-not ("M" -as [type])) {'
486
- "Add-Type -TypeDefinition 'using System;using System.Runtime.InteropServices;"
487
- 'public class M{'
488
- '[DllImport("user32.dll")]public static extern bool SetCursorPos(int x,int y);'
489
- '[DllImport("user32.dll")]public static extern void mouse_event(uint f,uint dx,uint dy,uint d,UIntPtr i);}'
490
- "'|Out-Null;};"
491
- f'[M]::SetCursorPos({x},{y});Start-Sleep -m 300;'
492
- f'{event_code}'
493
- )
494
-
495
- return ps_cmd
496
-
497
- def _generate_drag_command(self, x_start, y_start, x_end, y_end, button='left', duration_sec=0.5):
498
- """
499
- https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mouse_event
500
-
501
- Generate a PowerShell command to simulate mouse drag from (x_start, y_start) to (x_end, y_end)
502
- over the given duration in seconds.
503
-
504
- @param x_start: Start X coordinate
505
- @param y_start: Start Y coordinate
506
- @param x_end: End X coordinate
507
- @param y_end: End Y coordinate
508
- @param button: 'left' or 'right'
509
- @param duration_sec: Total drag duration in seconds
510
- @return: PowerShell command string
511
- """
512
- button = button.lower()
513
- button_codes = {
514
- 'left': (0x0002, 0x0004), # MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP
515
- 'right': (0x0008, 0x0010), # MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP
516
- 'middle': (0x0020, 0x0040), # MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP
517
- }
518
-
519
- if button not in button_codes:
520
- raise ValueError("button must be 'left', 'right', or 'middle'")
521
-
522
- down_code, up_code = button_codes[button]
523
-
524
- delay_ms = 50
525
- steps = round((float(duration_sec) * 1000) / delay_ms)
526
- dx = x_end - x_start
527
- dy = y_end - y_start
528
-
529
- ps_cmd = (
530
- 'if (-not ("M" -as [type])) {'
531
- "Add-Type -TypeDefinition 'using System;using System.Runtime.InteropServices;"
532
- 'public class M{'
533
- '[DllImport("user32.dll")]public static extern bool SetCursorPos(int x,int y);'
534
- '[DllImport("user32.dll")]public static extern void mouse_event(uint f,uint dx,uint dy,uint d,UIntPtr i);}'
535
- "'|Out-Null;};"
536
- f'[M]::SetCursorPos({x_start},{y_start})|Out-Null; Start-Sleep -m 300;'
537
- f'[M]::mouse_event({down_code},0,0,0,[UIntPtr]::Zero); Start-Sleep -m 300;'
538
- f'for ($i=1; $i -le {steps}; $i++){{'
539
- f'$x={x_start}+[math]::Round({dx}*$i/{steps});'
540
- f'$y={y_start}+[math]::Round({dy}*$i/{steps});'
541
- f'[M]::SetCursorPos($x,$y)|Out-Null; Start-Sleep -m {delay_ms};'
542
- '};'
543
- f'[M]::SetCursorPos({x_end},{y_end})|Out-Null; Start-Sleep -m 300;'
544
- f'[M]::mouse_event({up_code},0,0,0,[UIntPtr]::Zero);'
545
- )
546
-
547
- return ps_cmd
548
-
549
- def _generate_keyboard_command(self, sequences):
550
- raise Exception('Not Implement yet')
551
-
552
- def _script_path(self, name):
553
- return os.path.join(SCRIPTS_DIR, name)
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import base64
4
+ import math
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from appium.webdriver.mobilecommand import MobileCommand as Command
9
+ from robot.libraries.BuiltIn import BuiltIn
10
+ from selenium.common import InvalidArgumentException
11
+
12
+ from AppiumLibrary import utils
13
+ from AppiumLibrary.locators import ElementFinder
14
+ from .keywordgroup import KeywordGroup
15
+
16
+
17
+ class _PowershellKeywords(KeywordGroup):
18
+ def __init__(self):
19
+ self._element_finder = ElementFinder()
20
+ self._bi = BuiltIn()
21
+
22
+ # Public, element lookups
23
+ # Powershell command, need appium server allow shell, eg: appium --relaxed-security
24
+
25
+ def appium_ps_click(
26
+ self,
27
+ locator=None,
28
+ x=0,
29
+ y=0,
30
+ button='left',
31
+ **kwargs
32
+ ):
33
+ """
34
+ Click using PowerShell-level mouse simulation.
35
+
36
+ Supports:
37
+ - Absolute coordinates or element-based location via locator
38
+ - Optional offsets (absolute or 'center')
39
+ - Custom mouse button
40
+
41
+ kwargs:
42
+ - offset: sets both x/y offset globally (overrides individual)
43
+ - x_offset / y_offset
44
+
45
+ @param locator:
46
+ @param x:
47
+ @param y:
48
+ @param button:
49
+ 'left' - single left click
50
+ 'right' - single right click
51
+ 'middle' - single middle click
52
+ 'double' - double left click
53
+ 'triple' - triple left click
54
+ 'right-double' - double right click
55
+
56
+ """
57
+ self._info(f"Appium Ps Click")
58
+
59
+ if locator:
60
+ rect = self.get_element_rect(locator)
61
+ x, y = self._parse_location(rect, kwargs, 'x_offset', 'y_offset')
62
+ root = self._current_application().get_window_rect()
63
+ self._info(f"[DEBUG] App window rect: {root}")
64
+ x, y = x + root['x'], y + root['y']
65
+
66
+ self._info(f"[CLICK] {button.upper()} Button: ({x},{y}))")
67
+
68
+ ps_command = self._generate_click_command(x, y, button)
69
+ self._info(f"ps_command: \n{ps_command}")
70
+
71
+ self.appium_execute_powershell_command(ps_command)
72
+
73
+ def appium_ps_sendkeys(self, text: str):
74
+ """
75
+ SendKeys can found at: https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.sendkeys?view=windowsdesktop-10.0
76
+
77
+ To specify that any combination of SHIFT, CTRL, and ALT should be held down while several other keys are pressed,
78
+ enclose the code for those keys in parentheses. For example, to specify to hold down SHIFT while E and C are pressed,
79
+ use "+(EC)". To specify to hold down SHIFT while E is pressed, followed by C without SHIFT, use "+EC".
80
+
81
+ To specify repeating keys, use the form {key number}. You must put a space between key and number.
82
+ For example, {LEFT 42} means press the LEFT ARROW key 42 times; {h 10} means press H 10 times.
83
+
84
+ @param text: text to sendkeys
85
+ eg1: text = 123qwe{TAB}iop{ENTER}+a~ABC~
86
+ eg2: text = "{+}" ({%}) ({^})
87
+ eg3: text = This is test{LEFT}{BACKSPACE}x
88
+ eg4: text = 123qwe{BACKSPACE 3}{TAB}{ENTER}
89
+ @return:
90
+ """
91
+ self._info(f"Appium Ps Sendkeys: {text}")
92
+ text = text.replace('"', '""')
93
+
94
+ ps_command = (
95
+ f'Add-Type -AssemblyName System.Windows.Forms;'
96
+ f'[System.Windows.Forms.SendKeys]::SendWait("{text}")'
97
+ )
98
+ self._info(f'command: {ps_command}')
99
+
100
+ self.appium_execute_powershell_command(ps_command)
101
+
102
+ def appium_ps_drag_and_drop(
103
+ self,
104
+ start_locator=None,
105
+ end_locator=None,
106
+ x_start=0,
107
+ y_start=0,
108
+ x_end=0,
109
+ y_end=0,
110
+ button='left',
111
+ **kwargs
112
+ ):
113
+ """
114
+ Drag and drop using PowerShell-level mouse simulation.
115
+
116
+ Supports:
117
+ - Absolute coordinates or element-based location via start_locator/end_locator
118
+ - Optional offsets (absolute or 'center')
119
+ - Custom mouse button (left/right/mid)
120
+ - Drag duration (default 0.5 seconds)
121
+
122
+ kwargs:
123
+ - offset: sets both x/y offset globally (overrides individual)
124
+ - x_start_offset / y_start_offset
125
+ - x_end_offset / y_end_offset
126
+ - duration_sec: total drag time in seconds (default 0.5)
127
+ """
128
+ self._info(f"Appium Ps Drag And Drop")
129
+ # Get actual coordinates from locators (if any)
130
+ if start_locator:
131
+ start_rect = self.get_element_rect(start_locator)
132
+ x_start, y_start = self._parse_location(start_rect, kwargs, 'x_start_offset', 'y_start_offset')
133
+
134
+ if end_locator:
135
+ end_rect = self.get_element_rect(end_locator)
136
+ x_end, y_end = self._parse_location(end_rect, kwargs, 'x_end_offset', 'y_end_offset')
137
+
138
+ # Normalize against window position
139
+ root = self._current_application().get_window_rect()
140
+ self._info(f"[DEBUG] App window rect: {root}")
141
+
142
+ x_start, y_start = x_start + root['x'], y_start + root['y']
143
+ x_end, y_end = x_end + root['x'], y_end + root['y']
144
+
145
+ self._info(f"[DRAG] {button.upper()} Button: From ({x_start},{y_start}) → ({x_end},{y_end})")
146
+
147
+ # Get drag duration
148
+ duration_sec = float(kwargs.get('duration_sec', 0.5))
149
+ ps_command = self._generate_drag_command(x_start, y_start, x_end, y_end, button, duration_sec)
150
+ self._info(f"ps_command: \n{ps_command}")
151
+
152
+ # Execute PowerShell command
153
+ self.appium_execute_powershell_command(ps_command)
154
+
155
+ def appium_execute_powershell_command(self, command, handle_exception=False):
156
+ """
157
+ Executes a PowerShell command using Appium's execute_script method.
158
+
159
+ Note:
160
+ PowerShell command execution must be allowed on the Appium server.
161
+ For this, Appium must be started with the `--relaxed-security` flag:
162
+ appium --relaxed-security
163
+
164
+ Args:
165
+ command (str): The PowerShell command to be executed.
166
+ handle_exception (bool): If True, return the exception object on error. Otherwise, return None.
167
+
168
+ Returns:
169
+ str | dict | Exception: The result of the execution or the exception object.
170
+
171
+ Raises:
172
+ Exception: If handle_exception is False and an error occurs.
173
+ """
174
+ try:
175
+ driver = self._current_application()
176
+ result = driver.execute_script("powerShell", {"command": command})
177
+ return result
178
+ except Exception as exc:
179
+ if handle_exception:
180
+ return exc
181
+ raise
182
+
183
+ def appium_execute_powershell_script(self, ps_script=None, file_path=None, handle_exception=False):
184
+ """
185
+ Executes a PowerShell script using Appium's execute_script method.
186
+
187
+ Note:
188
+ PowerShell command execution must be allowed on the Appium server.
189
+ For this, Appium must be started with the `--relaxed-security` flag:
190
+ appium --relaxed-security
191
+
192
+ Args:
193
+ ps_script (str): The full PowerShell script to be executed.
194
+ file_path (str): The file ps1 to be executed.
195
+ handle_exception (bool): If True, return the exception object on failure. If False, return None on failure.
196
+
197
+ Returns:
198
+ str | dict | Exception: The result of the script execution or the exception object.
199
+
200
+ Raises:
201
+ Exception: If handle_exception is False and an error occurs.
202
+ """
203
+ try:
204
+ if file_path:
205
+ self._info(f'Read file: {file_path}')
206
+ ps_script = utils.read_file(file_path)
207
+ self._info(f"PowerShell script: \n{ps_script}")
208
+ driver = self._current_application()
209
+ result = driver.execute_script("powerShell", {"script": ps_script})
210
+ return result
211
+ except Exception as exc:
212
+ if handle_exception:
213
+ return exc
214
+ raise
215
+
216
+ def appium_pull_file(self, path: str, save_path: str = None) -> str:
217
+ """Retrieves the file at `path`.
218
+
219
+ Powershell command must be allowed. eg: appium --relaxed-security
220
+
221
+ Args:
222
+ path: the path to the file on the device, eg: c:/users/user1/desktop/screenshot_file.png
223
+ save_path: path to save, eg: /Users/user1/desktop/screenshot.png
224
+
225
+ Returns:
226
+ The file's contents encoded as Base64.
227
+ """
228
+
229
+ # base64data = self._current_application().pull_file(path)
230
+ base64data = self._current_application().execute(Command.PULL_FILE, {'path': path})['value']
231
+
232
+ if save_path:
233
+ with open(save_path, "wb") as file:
234
+ file.write(base64.b64decode(base64data))
235
+
236
+ return base64data
237
+
238
+ def appium_pull_folder(self, path: str, save_path_as_zip: str = '') -> str:
239
+ """Retrieves a folder at `path`.
240
+
241
+ Powershell command must be allowed. eg: appium --relaxed-security
242
+
243
+ Args:
244
+ path: the path to the folder on the device. eg: c:/users/user1/desktop/folder1
245
+ save_path_as_zip: zip file. eg: /Users/user1/desktop/file.zip
246
+
247
+ Returns:
248
+ The folder's contents zipped and encoded as Base64.
249
+ """
250
+ # base64data = self._current_application().pull_folder(path)
251
+ base64data = self._current_application().execute(Command.PULL_FOLDER, {'path': path})['value']
252
+
253
+ if save_path_as_zip:
254
+ with open(save_path_as_zip, "wb") as file:
255
+ file.write(base64.b64decode(base64data))
256
+
257
+ return base64data
258
+
259
+ def appium_push_file(self, destination_path: str, source_path: str = None, base64data: str = None):
260
+ """Puts the data from the file at `source_path`, encoded as Base64, in the file specified as `path`.
261
+
262
+ Specify either `base64data` or `source_path`, if both specified default to `source_path`
263
+
264
+ Powershell command must be allowed. eg: appium --relaxed-security
265
+
266
+ Args:
267
+ destination_path: the location on the device/simulator where the local file contents should be saved.
268
+ eg: c:/users/user1/desktop/screenshot_file.png
269
+ base64data: file contents, encoded as Base64, to be written
270
+ to the file on the device/simulator. Eg: iVBORw0KGgoAAAANSUh...
271
+ source_path: local file path for the file to be loaded on device. Eg: /Users/user1/desktop/source_file.png
272
+
273
+ Returns:
274
+ base64data
275
+ """
276
+ if source_path is None and base64data is None:
277
+ raise InvalidArgumentException('Must either pass base64 data or a local file path')
278
+
279
+ if source_path is not None:
280
+ try:
281
+ with open(source_path, 'rb') as f:
282
+ file_data = f.read()
283
+ except IOError as e:
284
+ message = f'source_path "{source_path}" could not be found. Are you sure the file exists?'
285
+ raise InvalidArgumentException(message) from e
286
+ base64data = base64.b64encode(file_data).decode('utf-8')
287
+
288
+ # result = self._current_application().push_file(destination_path, base64data, source_path)
289
+
290
+ self._current_application().execute(Command.PUSH_FILE, {'path': destination_path, 'data': base64data})
291
+
292
+ return base64data
293
+
294
+ def appium_transfer_file(self, file_path, remote_path):
295
+ """
296
+ Streams a binary file, base64-encodes it chunk by chunk,
297
+ and sends it directly to a remote machine via PowerShell commands.
298
+
299
+ Powershell command must be allowed. eg: appium --relaxed-security
300
+
301
+ file_path: source file path, eg: c:/users/user1/desktop/screenshot_file.png
302
+ remote_path: destination path, eg: c:/users/user1/download/screenshot_file.png
303
+ """
304
+ file_path = Path(file_path)
305
+ remote_path = str(remote_path)
306
+ remote_b64_path = remote_path + ".b64.tmp"
307
+ chunk_size = 6000 # chunk size in raw bytes (will expand when base64 encoded)
308
+
309
+ # 1. Ensure remote parent directory exists
310
+ remote_directory = str(Path(remote_path).parent)
311
+ mkdir_cmd = f'New-Item -Path "{remote_directory}" -ItemType Directory -Force'
312
+ self.execute_script("powerShell", command=mkdir_cmd)
313
+ self._info(f"Ensured remote directory: {remote_directory}")
314
+
315
+ # 2. Open and stream file, encoding and sending each chunk
316
+ with open(file_path, "rb") as f:
317
+ chunk_index = 0
318
+ while True:
319
+ chunk = f.read(chunk_size)
320
+ if not chunk:
321
+ break
322
+ chunk_b64 = base64.b64encode(chunk).decode('utf-8')
323
+ escaped_chunk = chunk_b64.replace("`", "``").replace('"', '`"')
324
+ ps = f'Add-Content -Path "{remote_b64_path}" -Value "{escaped_chunk}"'
325
+ self.execute_script("powerShell", command=ps)
326
+ chunk_index += 1
327
+ self._info(f"Sent chunk {chunk_index}")
328
+
329
+ # 3. Decode and write binary file on remote side
330
+ decode_script = (
331
+ f'[IO.File]::WriteAllBytes("{remote_path}";'
332
+ f'[Convert]::FromBase64String((Get-Content "{remote_b64_path}" -Raw)))'
333
+ )
334
+ self.execute_script("powerShell", command=decode_script)
335
+ self._info(f"File written to: {remote_path}")
336
+
337
+ # 4. Optional cleanup
338
+ cleanup_script = f'Remove-Item "{remote_b64_path}" -ErrorAction SilentlyContinue'
339
+ self.execute_script("powerShell", command=cleanup_script)
340
+ self._info("Cleaned up temporary base64 file.")
341
+
342
+ def appium_split_and_push_file(self, source_path: str, remote_path: str, chunk_size_mb: int = 20):
343
+ """
344
+ Splits a binary file into chunks, pushes each to a remote machine via Appium,
345
+ then executes a PowerShell script remotely to recombine and clean up chunk files.
346
+
347
+ Powershell command must be allowed. eg: appium --relaxed-security
348
+
349
+ Parameters:
350
+ source_path (str): Local path to the binary file.
351
+ remote_path (str): Full remote file path to create from recombination.
352
+ chunk_size_mb (int): Size of each chunk in MB (default: 20MB).
353
+ """
354
+ chunk_size = chunk_size_mb * 1024 * 1024
355
+ file_size = os.path.getsize(source_path)
356
+
357
+ file_name = os.path.basename(source_path)
358
+ remote_dir = os.path.dirname(remote_path)
359
+ total_chunks = math.ceil(file_size / chunk_size)
360
+ # chunk_index_digits = max(4, len(str(total_chunks - 1))) # at least 4 digits
361
+ chunk_index_digits = len(str(total_chunks - 1))
362
+
363
+ self._info(f"Splitting '{file_name}' ({file_size} bytes) into {total_chunks} chunks of {chunk_size_mb}MB each")
364
+
365
+ # Step 1: Split and push chunks
366
+ with open(source_path, "rb") as f:
367
+ for index in range(total_chunks):
368
+ chunk = f.read(chunk_size)
369
+ if not chunk:
370
+ break
371
+
372
+ b64_chunk = base64.b64encode(chunk).decode("utf-8")
373
+ chunk_suffix = f"{index:0{chunk_index_digits}d}"
374
+ remote_chunk_path = os.path.join(remote_dir, f"{file_name}.part{chunk_suffix}")
375
+
376
+ self.appium_push_file(destination_path=remote_chunk_path, base64data=b64_chunk)
377
+ self._info(f"Pushed chunk {chunk_suffix} to {remote_chunk_path}")
378
+
379
+ # Step 2: Build PowerShell recombine script
380
+ escaped_dir = remote_dir.replace("'", "''")
381
+ escaped_out = remote_path.replace("'", "''")
382
+ escaped_base = file_name.replace("'", "''")
383
+ pad_format = f"D{chunk_index_digits}"
384
+
385
+ ps_script_lines = [
386
+ f"$out = [System.IO.File]::OpenWrite('{escaped_out}')",
387
+ "$out.SetLength(0)",
388
+ f"for ($i = 0; $i -lt {total_chunks}; $i++) {{",
389
+ f" $chunkName = '{escaped_base}.part' + $i.ToString('{pad_format}')",
390
+ f" $chunkPath = Join-Path '{escaped_dir}' $chunkName",
391
+ ' if (-not (Test-Path $chunkPath)) { throw "Missing chunk: $chunkPath" }',
392
+ " $in = [System.IO.File]::OpenRead($chunkPath)",
393
+ " $buffer = New-Object byte[] (1MB)",
394
+ " while (($n = $in.Read($buffer, 0, $buffer.Length)) -gt 0) {",
395
+ " $out.Write($buffer, 0, $n)",
396
+ " }",
397
+ " $in.Close()",
398
+ "}",
399
+ "$out.Close()",
400
+ f"for ($i = 0; $i -lt {total_chunks}; $i++) {{",
401
+ f" $chunkName = '{escaped_base}.part' + $i.ToString('{pad_format}')",
402
+ f" $chunkPath = Join-Path '{escaped_dir}' $chunkName",
403
+ " Remove-Item -Path $chunkPath -Force -ErrorAction SilentlyContinue",
404
+ "}",
405
+ f"Write-Output 'Combine and cleanup complete: {escaped_out}'"
406
+ ]
407
+
408
+ ps_script = "\n".join(ps_script_lines)
409
+
410
+ # Step 3: Execute recombine script remotely
411
+ self._info("Combining chunks and cleaning up on remote machine...")
412
+ result = self.appium_execute_powershell_script(ps_script)
413
+ self._info(f"Remote PowerShell result: {result}")
414
+ return escaped_out
415
+
416
+ # Private
417
+
418
+ def _parse_location(self, rect, kwargs, x_offset_key, y_offset_key):
419
+ """Parse offset inside a rect."""
420
+ offset = kwargs.get('offset')
421
+ x_offset = offset if offset is not None else kwargs.get(x_offset_key, 'center')
422
+ y_offset = offset if offset is not None else kwargs.get(y_offset_key, 'center')
423
+
424
+ x = rect['x'] + (rect['width'] // 2 if x_offset == 'center' else int(x_offset))
425
+ y = rect['y'] + (rect['height'] // 2 if y_offset == 'center' else int(y_offset))
426
+ return x, y
427
+
428
+ def _generate_click_command(self, x, y, button='left'):
429
+ """
430
+ Generate a PowerShell command to click at the given coordinates.
431
+
432
+ https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mouse_event
433
+
434
+ @param x: X coordinate
435
+ @param y: Y coordinate
436
+ @param button:
437
+ 'left' - single left click
438
+ 'right' - single right click
439
+ 'middle' - single middle click
440
+ 'double' - double left click
441
+ 'triple' - triple left click
442
+ 'right-double' - double right click
443
+ @return: PowerShell one-liner as string
444
+ """
445
+ button = button.lower()
446
+
447
+ # Mouse event codes
448
+ left_code = 6 # MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP
449
+ right_code = 24 # MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP
450
+ middle_code = 40 # MOUSEEVENTF_MIDDLEDOWN | MOUSEEVENTF_MIDDLEUP
451
+
452
+ # Define all buttons
453
+ click_events = {
454
+ 'left': f'[M]::mouse_event({left_code},0,0,0,[UIntPtr]::Zero);',
455
+ 'right': f'[M]::mouse_event({right_code},0,0,0,[UIntPtr]::Zero);',
456
+ 'middle': f'[M]::mouse_event({middle_code},0,0,0,[UIntPtr]::Zero);',
457
+ 'double': (
458
+ f'[M]::mouse_event({left_code},0,0,0,[UIntPtr]::Zero);'
459
+ f'Start-Sleep -m 100;'
460
+ f'[M]::mouse_event({left_code},0,0,0,[UIntPtr]::Zero);'
461
+ ),
462
+ 'triple': (
463
+ f'[M]::mouse_event({left_code},0,0,0,[UIntPtr]::Zero);'
464
+ f'Start-Sleep -m 100;'
465
+ f'[M]::mouse_event({left_code},0,0,0,[UIntPtr]::Zero);'
466
+ f'Start-Sleep -m 100;'
467
+ f'[M]::mouse_event({left_code},0,0,0,[UIntPtr]::Zero);'
468
+ ),
469
+ 'right-double': (
470
+ f'[M]::mouse_event({right_code},0,0,0,[UIntPtr]::Zero);'
471
+ f'Start-Sleep -m 100;'
472
+ f'[M]::mouse_event({right_code},0,0,0,[UIntPtr]::Zero);'
473
+ )
474
+ }
475
+
476
+ if button not in click_events:
477
+ valid = ', '.join(click_events.keys())
478
+ raise ValueError(f"button must be one of: {valid}")
479
+
480
+ event_code = click_events[button]
481
+
482
+ ps_cmd = (
483
+ 'if (-not ("M" -as [type])) {'
484
+ "Add-Type -TypeDefinition 'using System;using System.Runtime.InteropServices;"
485
+ 'public class M{'
486
+ '[DllImport("user32.dll")]public static extern bool SetCursorPos(int x,int y);'
487
+ '[DllImport("user32.dll")]public static extern void mouse_event(uint f,uint dx,uint dy,uint d,UIntPtr i);}'
488
+ "'|Out-Null;};"
489
+ f'[M]::SetCursorPos({x},{y});Start-Sleep -m 300;'
490
+ f'{event_code}'
491
+ )
492
+
493
+ return ps_cmd
494
+
495
+ def _generate_drag_command(self, x_start, y_start, x_end, y_end, button='left', duration_sec=0.5):
496
+ """
497
+ https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mouse_event
498
+
499
+ Generate a PowerShell command to simulate mouse drag from (x_start, y_start) to (x_end, y_end)
500
+ over the given duration in seconds.
501
+
502
+ @param x_start: Start X coordinate
503
+ @param y_start: Start Y coordinate
504
+ @param x_end: End X coordinate
505
+ @param y_end: End Y coordinate
506
+ @param button: 'left' or 'right'
507
+ @param duration_sec: Total drag duration in seconds
508
+ @return: PowerShell command string
509
+ """
510
+ button = button.lower()
511
+ button_codes = {
512
+ 'left': (0x0002, 0x0004), # MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP
513
+ 'right': (0x0008, 0x0010), # MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP
514
+ 'middle': (0x0020, 0x0040), # MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP
515
+ }
516
+
517
+ if button not in button_codes:
518
+ raise ValueError("button must be 'left', 'right', or 'middle'")
519
+
520
+ down_code, up_code = button_codes[button]
521
+
522
+ delay_ms = 50
523
+ steps = round((float(duration_sec) * 1000) / delay_ms)
524
+ dx = x_end - x_start
525
+ dy = y_end - y_start
526
+
527
+ ps_cmd = (
528
+ 'if (-not ("M" -as [type])) {'
529
+ "Add-Type -TypeDefinition 'using System;using System.Runtime.InteropServices;"
530
+ 'public class M{'
531
+ '[DllImport("user32.dll")]public static extern bool SetCursorPos(int x,int y);'
532
+ '[DllImport("user32.dll")]public static extern void mouse_event(uint f,uint dx,uint dy,uint d,UIntPtr i);}'
533
+ "'|Out-Null;};"
534
+ f'[M]::SetCursorPos({x_start},{y_start})|Out-Null; Start-Sleep -m 300;'
535
+ f'[M]::mouse_event({down_code},0,0,0,[UIntPtr]::Zero); Start-Sleep -m 300;'
536
+ f'for ($i=1; $i -le {steps}; $i++){{'
537
+ f'$x={x_start}+[math]::Round({dx}*$i/{steps});'
538
+ f'$y={y_start}+[math]::Round({dy}*$i/{steps});'
539
+ f'[M]::SetCursorPos($x,$y)|Out-Null; Start-Sleep -m {delay_ms};'
540
+ '};'
541
+ f'[M]::SetCursorPos({x_end},{y_end})|Out-Null; Start-Sleep -m 300;'
542
+ f'[M]::mouse_event({up_code},0,0,0,[UIntPtr]::Zero);'
543
+ )
544
+
545
+ return ps_cmd
546
+
547
+ def _generate_keyboard_command(self, sequences):
548
+ raise Exception('Not Implement yet')
549
+
550
+ def _script_path(self, name):
551
+ return Exception('Not Implement yet')