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