autogui 1.0.0

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,400 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fiddle"
4
+ require "fiddle/import"
5
+ require "fiddle/types"
6
+
7
+ module AutoGUI
8
+ module Platform
9
+ module Windows
10
+ extend Fiddle::Importer
11
+ dlload "user32", "gdi32", "kernel32"
12
+
13
+ typealias "HWND", "void*"
14
+ typealias "HDC", "void*"
15
+ typealias "HBITMAP", "void*"
16
+ typealias "HGDIOBJ", "void*"
17
+ typealias "DWORD", "unsigned long"
18
+ typealias "UINT", "unsigned int"
19
+ typealias "LONG", "long"
20
+ typealias "WORD", "unsigned short"
21
+ typealias "BYTE", "unsigned char"
22
+ typealias "BOOL", "int"
23
+ typealias "UINT_PTR", "size_t"
24
+ typealias "ULONG_PTR", "size_t"
25
+ typealias "SHORT", "short"
26
+ typealias "WCHAR", "unsigned short"
27
+
28
+ extern "int GetCursorPos(void*)"
29
+ extern "int SetCursorPos(int, int)"
30
+ extern "int GetSystemMetrics(int)"
31
+ extern "void mouse_event(DWORD, DWORD, DWORD, DWORD, ULONG_PTR)"
32
+ extern "void keybd_event(BYTE, BYTE, DWORD, ULONG_PTR)"
33
+ extern "int SetProcessDPIAware()"
34
+ extern "SHORT VkKeyScanW(WCHAR)"
35
+ extern "UINT SendInput(UINT, void*, int)"
36
+ extern "int MessageBoxW(HWND, void*, void*, UINT)"
37
+ extern "HDC GetDC(HWND)"
38
+ extern "int ReleaseDC(HWND, HDC)"
39
+ extern "DWORD GetPixel(HDC, int, int)"
40
+ extern "HDC CreateCompatibleDC(HDC)"
41
+ extern "HBITMAP CreateCompatibleBitmap(HDC, int, int)"
42
+ extern "HGDIOBJ SelectObject(HDC, HGDIOBJ)"
43
+ extern "int BitBlt(HDC, int, int, int, int, HDC, int, int, DWORD)"
44
+ extern "int GetDIBits(HDC, HBITMAP, UINT, UINT, void*, void*, UINT)"
45
+ extern "int DeleteObject(HGDIOBJ)"
46
+ extern "int DeleteDC(HDC)"
47
+ extern "HWND GetForegroundWindow()"
48
+ extern "int SetForegroundWindow(HWND)"
49
+ extern "int GetWindowTextW(HWND, void*, int)"
50
+ extern "int GetWindowTextLengthW(HWND)"
51
+ extern "int GetWindowRect(HWND, void*)"
52
+ extern "int IsWindowVisible(HWND)"
53
+ extern "int IsIconic(HWND)"
54
+ extern "int IsZoomed(HWND)"
55
+ extern "int ShowWindow(HWND, int)"
56
+ extern "int MoveWindow(HWND, int, int, int, int, int)"
57
+ extern "HWND GetTopWindow(HWND)"
58
+ extern "HWND GetWindow(HWND, UINT)"
59
+ extern "int IsWindow(HWND)"
60
+ extern "int PostMessageW(HWND, UINT, UINT_PTR, LONG)"
61
+ extern "DWORD GetWindowThreadProcessId(HWND, void*)"
62
+ extern "DWORD GetCurrentThreadId()"
63
+ extern "int AttachThreadInput(DWORD, DWORD, int)"
64
+ extern "int BringWindowToTop(HWND)"
65
+ extern "int SetWindowPos(HWND, HWND, int, int, int, int, UINT)"
66
+ extern "HWND WindowFromPoint(long long)"
67
+
68
+ MOUSEEVENTF_MOVE = 0x0001
69
+ MOUSEEVENTF_LEFTDOWN = 0x0002
70
+ MOUSEEVENTF_LEFTUP = 0x0004
71
+ MOUSEEVENTF_RIGHTDOWN = 0x0008
72
+ MOUSEEVENTF_RIGHTUP = 0x0010
73
+ MOUSEEVENTF_MIDDLEDOWN = 0x0020
74
+ MOUSEEVENTF_MIDDLEUP = 0x0040
75
+ MOUSEEVENTF_WHEEL = 0x0800
76
+ MOUSEEVENTF_HWHEEL = 0x01000
77
+ MOUSEEVENTF_ABSOLUTE = 0x8000
78
+ KEYEVENTF_KEYUP = 0x0002
79
+ KEYEVENTF_UNICODE = 0x0004
80
+ INPUT_KEYBOARD = 1
81
+ SRCCOPY = 0x00CC0020
82
+ CAPTUREBLT = 0x40000000
83
+ DIB_RGB_COLORS = 0
84
+ SM_CXSCREEN = 0
85
+ SM_CYSCREEN = 1
86
+ SM_SWAPBUTTON = 23
87
+ GW_HWNDNEXT = 2
88
+ SW_HIDE = 0
89
+ SW_SHOWNORMAL = 1
90
+ SW_SHOWMINIMIZED = 2
91
+ SW_SHOWMAXIMIZED = 3
92
+ SW_RESTORE = 9
93
+ WM_CLOSE = 0x0010
94
+ HWND_TOP = 0
95
+ SWP_NOSIZE = 0x0001
96
+ SWP_NOMOVE = 0x0002
97
+ SWP_SHOWWINDOW = 0x0040
98
+ WHEEL_DELTA = 120
99
+
100
+ VK_MAP = {
101
+ "backspace" => 0x08, "\b" => 0x08, "super" => 0x5B, "tab" => 0x09, "\t" => 0x09,
102
+ "clear" => 0x0c, "enter" => 0x0d, "\n" => 0x0d, "\r" => 0x0d, "return" => 0x0d,
103
+ "shift" => 0x10, "ctrl" => 0x11, "alt" => 0x12, "pause" => 0x13, "capslock" => 0x14,
104
+ "kana" => 0x15, "hanguel" => 0x15, "hangul" => 0x15, "junja" => 0x17, "final" => 0x18,
105
+ "hanja" => 0x19, "kanji" => 0x19, "esc" => 0x1b, "escape" => 0x1b, "convert" => 0x1c,
106
+ "nonconvert" => 0x1d, "accept" => 0x1e, "modechange" => 0x1f, " " => 0x20, "space" => 0x20,
107
+ "pgup" => 0x21, "pgdn" => 0x22, "pageup" => 0x21, "pagedown" => 0x22, "end" => 0x23,
108
+ "home" => 0x24, "left" => 0x25, "up" => 0x26, "right" => 0x27, "down" => 0x28,
109
+ "select" => 0x29, "print" => 0x2a, "execute" => 0x2b, "prtsc" => 0x2c, "prtscr" => 0x2c,
110
+ "prntscrn" => 0x2c, "printscreen" => 0x2c, "insert" => 0x2d, "del" => 0x2e, "delete" => 0x2e,
111
+ "help" => 0x2f, "win" => 0x5b, "winleft" => 0x5b, "winright" => 0x5c, "apps" => 0x5d,
112
+ "sleep" => 0x5f, "num0" => 0x60, "num1" => 0x61, "num2" => 0x62, "num3" => 0x63,
113
+ "num4" => 0x64, "num5" => 0x65, "num6" => 0x66, "num7" => 0x67, "num8" => 0x68,
114
+ "num9" => 0x69, "multiply" => 0x6a, "add" => 0x6b, "separator" => 0x6c, "subtract" => 0x6d,
115
+ "decimal" => 0x6e, "divide" => 0x6f, "f1" => 0x70, "f2" => 0x71, "f3" => 0x72, "f4" => 0x73,
116
+ "f5" => 0x74, "f6" => 0x75, "f7" => 0x76, "f8" => 0x77, "f9" => 0x78, "f10" => 0x79,
117
+ "f11" => 0x7a, "f12" => 0x7b, "f13" => 0x7c, "f14" => 0x7d, "f15" => 0x7e, "f16" => 0x7f,
118
+ "f17" => 0x80, "f18" => 0x81, "f19" => 0x82, "f20" => 0x83, "f21" => 0x84, "f22" => 0x85,
119
+ "f23" => 0x86, "f24" => 0x87, "numlock" => 0x90, "scrolllock" => 0x91, "shiftleft" => 0xa0,
120
+ "shiftright" => 0xa1, "ctrlleft" => 0xa2, "ctrlright" => 0xa3, "altleft" => 0xa4,
121
+ "altright" => 0xa5, "browserback" => 0xa6, "browserforward" => 0xa7, "browserrefresh" => 0xa8,
122
+ "browserstop" => 0xa9, "browsersearch" => 0xaa, "browserfavorites" => 0xab,
123
+ "browserhome" => 0xac, "volumemute" => 0xad, "volumedown" => 0xae, "volumeup" => 0xaf,
124
+ "nexttrack" => 0xb0, "prevtrack" => 0xb1, "stop" => 0xb2, "playpause" => 0xb3,
125
+ "launchmail" => 0xb4, "launchmediaselect" => 0xb5, "launchapp1" => 0xb6, "launchapp2" => 0xb7,
126
+ "command" => 0x5b, "option" => 0x12, "optionleft" => 0xa4, "optionright" => 0xa5
127
+ }.freeze
128
+
129
+ module_function
130
+
131
+ def init!
132
+ SetProcessDPIAware()
133
+ rescue StandardError
134
+ nil
135
+ end
136
+
137
+ def keyboard_mapping
138
+ @keyboard_mapping ||= begin
139
+ map = {}
140
+ KEY_NAMES.each { |k| map[k] = nil }
141
+ map.merge!(VK_MAP)
142
+ (32...127).each do |code|
143
+ ch = code.chr
144
+ map[ch] = VkKeyScanW(code)
145
+ end
146
+ map
147
+ end
148
+ end
149
+
150
+ def position
151
+ buf = "\x00".b * 8
152
+ GetCursorPos(buf)
153
+ buf.unpack("ll")
154
+ end
155
+
156
+ def size
157
+ [GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN)]
158
+ end
159
+
160
+ def move_to(x, y)
161
+ SetCursorPos(x.to_i, y.to_i)
162
+ end
163
+
164
+ def mouse_is_swapped?
165
+ GetSystemMetrics(SM_SWAPBUTTON) != 0
166
+ end
167
+
168
+ def mouse_down(x, y, button)
169
+ send_mouse(button_flag(button, :down), x, y)
170
+ end
171
+
172
+ def mouse_up(x, y, button)
173
+ send_mouse(button_flag(button, :up), x, y)
174
+ end
175
+
176
+ def click(x, y, button)
177
+ send_mouse(button_flag(button, :down) | button_flag(button, :up), x, y)
178
+ end
179
+
180
+ def scroll(clicks, x, y)
181
+ send_mouse(MOUSEEVENTF_WHEEL, x, y, clicks.to_i * WHEEL_DELTA)
182
+ end
183
+
184
+ def hscroll(clicks, x, y)
185
+ send_mouse(MOUSEEVENTF_HWHEEL, x, y, clicks.to_i * WHEEL_DELTA)
186
+ end
187
+
188
+ alias vscroll scroll
189
+
190
+ def key_down(key)
191
+ dispatch_key(key, up: false)
192
+ end
193
+
194
+ def key_up(key)
195
+ dispatch_key(key, up: true)
196
+ end
197
+
198
+ def screenshot(region = nil)
199
+ screen_w, screen_h = size
200
+ if region
201
+ left, top, width, height = region.to_a
202
+ else
203
+ left = 0
204
+ top = 0
205
+ width = screen_w
206
+ height = screen_h
207
+ end
208
+ hdc_screen = GetDC(nil)
209
+ hdc_mem = CreateCompatibleDC(hdc_screen)
210
+ hbmp = CreateCompatibleBitmap(hdc_screen, width, height)
211
+ old = SelectObject(hdc_mem, hbmp)
212
+ BitBlt(hdc_mem, 0, 0, width, height, hdc_screen, left, top, SRCCOPY | CAPTUREBLT)
213
+
214
+ bmi = [40, width, -height, 1, 32, 0, 0, 0, 0, 0, 0].pack("LllSSLLllLL")
215
+ buf = "\x00".b * (width * height * 4)
216
+ GetDIBits(hdc_mem, hbmp, 0, height, buf, bmi, DIB_RGB_COLORS)
217
+
218
+ SelectObject(hdc_mem, old)
219
+ DeleteObject(hbmp)
220
+ DeleteDC(hdc_mem)
221
+ ReleaseDC(nil, hdc_screen)
222
+ Image.from_bgra(width, height, buf)
223
+ end
224
+
225
+ def pixel(x, y)
226
+ hdc = GetDC(nil)
227
+ color = GetPixel(hdc, x.to_i, y.to_i) & 0xFFFFFFFF
228
+ ReleaseDC(nil, hdc)
229
+ r = color & 0xFF
230
+ g = (color >> 8) & 0xFF
231
+ b = (color >> 16) & 0xFF
232
+ [r, g, b]
233
+ end
234
+
235
+ def message_box(text, title, flags)
236
+ MessageBoxW(nil, wide(text), wide(title), flags)
237
+ end
238
+
239
+ def window_from_point(x, y)
240
+ packed = [x.to_i, y.to_i].pack("ll").unpack1("q<")
241
+ hwnd = WindowFromPoint(packed)
242
+ hwnd.null? ? nil : hwnd
243
+ rescue StandardError
244
+ nil
245
+ end
246
+
247
+ def each_hwnd
248
+ hwnds = []
249
+ callback = Fiddle::Closure::BlockCaller.new(
250
+ Fiddle::TYPE_INT,
251
+ [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP]
252
+ ) do |hwnd, _lparam|
253
+ hwnds << hwnd
254
+ 1
255
+ end
256
+ begin
257
+ enum_windows_fn.call(callback, 0)
258
+ rescue StandardError
259
+ hwnd = GetTopWindow(0)
260
+ while hwnd && !hwnd.null? && hwnd.to_i != 0
261
+ hwnds << hwnd
262
+ hwnd = GetWindow(hwnd, GW_HWNDNEXT)
263
+ break if hwnd.nil? || hwnd.null? || hwnd.to_i.zero?
264
+ end
265
+ end
266
+ hwnds.each { |h| yield h }
267
+ end
268
+
269
+ def enum_windows_fn
270
+ @enum_windows_fn ||= Fiddle::Function.new(
271
+ Fiddle::Handle.new("user32")["EnumWindows"],
272
+ [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP],
273
+ Fiddle::TYPE_INT
274
+ )
275
+ end
276
+
277
+ def window_title(hwnd)
278
+ len = GetWindowTextLengthW(hwnd)
279
+ return "" if len <= 0
280
+
281
+ buf = "\x00".b * ((len + 1) * 2)
282
+ GetWindowTextW(hwnd, buf, len + 1)
283
+ buf.force_encoding("UTF-16LE").encode("UTF-8").delete("\x00")
284
+ end
285
+
286
+ def window_rect(hwnd)
287
+ buf = "\x00".b * 16
288
+ GetWindowRect(hwnd, buf)
289
+ left, top, right, bottom = buf.unpack("l4")
290
+ [left, top, right - left, bottom - top]
291
+ end
292
+
293
+ def window_visible?(hwnd)
294
+ IsWindowVisible(hwnd) != 0
295
+ end
296
+
297
+ def window_minimized?(hwnd)
298
+ IsIconic(hwnd) != 0
299
+ end
300
+
301
+ def window_maximized?(hwnd)
302
+ IsZoomed(hwnd) != 0
303
+ end
304
+
305
+ def activate_window(hwnd)
306
+ tid_fg = GetWindowThreadProcessId(GetForegroundWindow(), nil)
307
+ tid_this = GetCurrentThreadId()
308
+ AttachThreadInput(tid_this, tid_fg, 1) unless tid_fg == tid_this
309
+ ShowWindow(hwnd, SW_RESTORE) if window_minimized?(hwnd)
310
+ BringWindowToTop(hwnd)
311
+ SetForegroundWindow(hwnd)
312
+ SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW)
313
+ ensure
314
+ AttachThreadInput(tid_this, tid_fg, 0) unless tid_fg == tid_this
315
+ end
316
+
317
+ def show_window(hwnd, cmd)
318
+ ShowWindow(hwnd, cmd)
319
+ end
320
+
321
+ def move_window(hwnd, x, y, w, h)
322
+ MoveWindow(hwnd, x, y, w, h, 1)
323
+ end
324
+
325
+ def close_window(hwnd)
326
+ PostMessageW(hwnd, WM_CLOSE, 0, 0)
327
+ end
328
+
329
+ def valid_window?(hwnd)
330
+ hwnd && !hwnd.null? && hwnd.to_i != 0 && IsWindow(hwnd) != 0
331
+ end
332
+
333
+ def send_mouse(flags, x, y, data = 0)
334
+ width, height = size
335
+ cx = ((65_536 * x.to_i) / width) + 1
336
+ cy = ((65_536 * y.to_i) / height) + 1
337
+ mouse_event(flags, cx, cy, data, 0)
338
+ end
339
+
340
+ def button_flag(button, dir)
341
+ case [button.to_s, dir]
342
+ when ["left", :down] then MOUSEEVENTF_LEFTDOWN
343
+ when ["left", :up] then MOUSEEVENTF_LEFTUP
344
+ when ["middle", :down] then MOUSEEVENTF_MIDDLEDOWN
345
+ when ["middle", :up] then MOUSEEVENTF_MIDDLEUP
346
+ when ["right", :down] then MOUSEEVENTF_RIGHTDOWN
347
+ when ["right", :up] then MOUSEEVENTF_RIGHTUP
348
+ else
349
+ raise AutoGUIException, "button must be left, middle, or right (got #{button.inspect})"
350
+ end
351
+ end
352
+
353
+ def dispatch_key(key, up:)
354
+ if key.length == 1 && (keyboard_mapping[key].nil? || key.ord > 127)
355
+ send_unicode(key, up: up)
356
+ return
357
+ end
358
+
359
+ mapped = keyboard_mapping[key]
360
+ if mapped.nil?
361
+ if key.length == 1
362
+ send_unicode(key, up: up)
363
+ end
364
+ return
365
+ end
366
+
367
+ needs_shift = AutoGUI.shift_character?(key)
368
+ mods, vk = mapped.divmod(0x100)
369
+ mods_order_down = [[mods & 4, 0x12], [mods & 2, 0x11], [mods & 1 != 0 || needs_shift, 0x10]]
370
+ mods_order_up = mods_order_down.reverse
371
+ unless up
372
+ mods_order_down.each { |apply, vk_mod| keybd_event(vk_mod, 0, 0, 0) if apply }
373
+ keybd_event(vk, 0, 0, 0)
374
+ mods_order_up.each { |apply, vk_mod| keybd_event(vk_mod, 0, KEYEVENTF_KEYUP, 0) if apply }
375
+ else
376
+ mods_order_down.each { |apply, vk_mod| keybd_event(vk_mod, 0, 0, 0) if apply }
377
+ keybd_event(vk, 0, KEYEVENTF_KEYUP, 0)
378
+ mods_order_up.each { |apply, vk_mod| keybd_event(vk_mod, 0, KEYEVENTF_KEYUP, 0) if apply }
379
+ end
380
+ end
381
+
382
+ def send_unicode(char, up:)
383
+ flags = KEYEVENTF_UNICODE
384
+ flags |= KEYEVENTF_KEYUP if up
385
+ scan = char.ord
386
+ ki = [0, scan, flags, 0].pack("SSLL")
387
+ pad = 32 - ki.bytesize
388
+ pad = 0 if pad.negative?
389
+ union = ki + ("\x00".b * pad)
390
+ union = union.byteslice(0, 32).ljust(32, "\x00".b)
391
+ input = [INPUT_KEYBOARD].pack("L") + [0].pack("L") + union
392
+ SendInput(1, input, input.bytesize)
393
+ end
394
+
395
+ def wide(str)
396
+ (str.to_s.encode("UTF-16LE") + "\x00\x00".b).b
397
+ end
398
+ end
399
+ end
400
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ module AutoGUI
6
+ module Platform
7
+ module_function
8
+
9
+ def current
10
+ @current ||=
11
+ case RbConfig::CONFIG["host_os"]
12
+ when /mswin|mingw|cygwin/i
13
+ require_relative "platform/windows"
14
+ Windows.init!
15
+ Windows
16
+ when /darwin/i
17
+ require_relative "platform/darwin"
18
+ Darwin.init!
19
+ Darwin
20
+ when /linux|bsd/i
21
+ require_relative "platform/linux"
22
+ Linux.init!
23
+ Linux
24
+ else
25
+ raise NotImplementedError, "Your platform (#{RbConfig::CONFIG['host_os']}) is not supported by AutoGUI."
26
+ end
27
+ end
28
+
29
+ def windows?
30
+ current == Windows
31
+ end
32
+
33
+ def darwin?
34
+ current.name.end_with?("Darwin")
35
+ end
36
+
37
+ def linux?
38
+ current.name.end_with?("Linux")
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AutoGUI
4
+ module Run
5
+ module_function
6
+
7
+ def tokenize(command_str)
8
+ command_pattern = /^(su|sd|ss|c|l|m|r|g|d|k|w|h|f|s|a|p)/
9
+ command_list = []
10
+ i = 0
11
+ while i < command_str.length
12
+ if command_str[i] =~ /[ \t\n\r]/
13
+ i += 1
14
+ next
15
+ end
16
+
17
+ mo = command_pattern.match(command_str[i..])
18
+ raise AutoGUIException, "Invalid command at index #{i}: #{command_str[i]} is not a valid command" if mo.nil?
19
+
20
+ individual = mo[1]
21
+ command_list << individual
22
+ i += individual.length
23
+
24
+ case individual
25
+ when "c", "l", "m", "r", "su", "sd", "ss"
26
+ nil
27
+ when "g", "d"
28
+ x = get_number_token(command_str[i..])
29
+ i += x.length
30
+ comma = get_comma_token(command_str[i..])
31
+ i += comma.length
32
+ y = get_number_token(command_str[i..])
33
+ i += y.length
34
+ command_list << x.lstrip << y.lstrip
35
+ when "s", "p"
36
+ num = get_number_token(command_str[i..])
37
+ i += num.length
38
+ command_list << num.lstrip
39
+ when "k", "w", "h", "a"
40
+ quoted = get_quoted_string_token(command_str[i..])
41
+ i += quoted.length
42
+ command_list << quoted.strip[1..-2]
43
+ when "f"
44
+ loops = get_number_token(command_str[i..])
45
+ i += loops.length
46
+ sub = get_parens_token(command_str[i..])
47
+ i += sub.length
48
+ command_list << loops.lstrip
49
+ inner = sub.lstrip[1..-2]
50
+ command_list << tokenize(inner)
51
+ end
52
+ end
53
+ command_list
54
+ end
55
+
56
+ def run(command_str, ss_count = nil)
57
+ ss_count ||= [0]
58
+ original_pause = AutoGUI.pause
59
+ run_list(tokenize(command_str), ss_count)
60
+ ensure
61
+ AutoGUI.pause = original_pause
62
+ end
63
+
64
+ def run_list(command_list, ss_count)
65
+ i = 0
66
+ while i < command_list.length
67
+ command = command_list[i]
68
+ case command
69
+ when "c" then AutoGUI.click(button: PRIMARY)
70
+ when "l" then AutoGUI.click(button: LEFT)
71
+ when "m" then AutoGUI.click(button: MIDDLE)
72
+ when "r" then AutoGUI.click(button: RIGHT)
73
+ when "su" then AutoGUI.scroll(1)
74
+ when "sd" then AutoGUI.scroll(-1)
75
+ when "ss"
76
+ AutoGUI.screenshot("screenshot#{ss_count[0]}.png")
77
+ ss_count[0] += 1
78
+ when "s"
79
+ AutoGUI.sleep(command_list[i + 1].to_f)
80
+ i += 1
81
+ when "p"
82
+ AutoGUI.pause = command_list[i + 1].to_f
83
+ i += 1
84
+ when "g"
85
+ x = command_list[i + 1]
86
+ y = command_list[i + 2]
87
+ if x.start_with?("+", "-") && y.start_with?("+", "-")
88
+ AutoGUI.move(x.to_i, y.to_i)
89
+ else
90
+ AutoGUI.moveTo(x.to_i, y.to_i)
91
+ end
92
+ i += 2
93
+ when "d"
94
+ x = command_list[i + 1]
95
+ y = command_list[i + 2]
96
+ if x.start_with?("+", "-") && y.start_with?("+", "-")
97
+ AutoGUI.drag(x.to_i, y.to_i)
98
+ else
99
+ AutoGUI.dragTo(x.to_i, y.to_i)
100
+ end
101
+ i += 2
102
+ when "k"
103
+ AutoGUI.press(command_list[i + 1])
104
+ i += 1
105
+ when "w"
106
+ AutoGUI.write(command_list[i + 1])
107
+ i += 1
108
+ when "h"
109
+ AutoGUI.hotkey(*command_list[i + 1].delete(" ").split(","))
110
+ i += 1
111
+ when "a"
112
+ AutoGUI.alert(command_list[i + 1])
113
+ i += 1
114
+ when "f"
115
+ command_list[i + 1].to_i.times { run_list(command_list[i + 2], ss_count) }
116
+ i += 2
117
+ end
118
+ i += 1
119
+ end
120
+ end
121
+
122
+ def get_number_token(command_str)
123
+ mo = /^\s*[+-]?\d+(?:\.\d+)?/.match(command_str)
124
+ raise AutoGUIException, "Invalid command at index 0: a number was expected" if mo.nil?
125
+
126
+ mo[0]
127
+ end
128
+
129
+ def get_quoted_string_token(command_str)
130
+ mo = /^\s*'(.*?)'/.match(command_str)
131
+ raise AutoGUIException, "Invalid command at index 0: a quoted string was expected" if mo.nil?
132
+
133
+ mo[0]
134
+ end
135
+
136
+ def get_comma_token(command_str)
137
+ mo = /^\s*,/.match(command_str)
138
+ raise AutoGUIException, "Invalid command at index 0: a comma was expected" if mo.nil?
139
+
140
+ mo[0]
141
+ end
142
+
143
+ def get_parens_token(command_str)
144
+ raise AutoGUIException, "Invalid command at index 0: No open parenthesis found." unless command_str =~ /^\s*\(/
145
+
146
+ i = 0
147
+ open_count = 0
148
+ while i < command_str.length
149
+ case command_str[i]
150
+ when "("
151
+ open_count += 1
152
+ when ")"
153
+ open_count -= 1
154
+ if open_count.zero?
155
+ i += 1
156
+ break
157
+ elsif open_count.negative?
158
+ raise AutoGUIException, "Invalid command at index 0: No open parenthesis for this close parenthesis."
159
+ end
160
+ end
161
+ i += 1
162
+ end
163
+ raise AutoGUIException, "Invalid command at index 0: Not enough close parentheses." if open_count.positive?
164
+
165
+ command_str[0...i]
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AutoGUI
4
+ module Screenshot
5
+ module_function
6
+
7
+ def load_image(image)
8
+ Image.open(image)
9
+ end
10
+
11
+ def center(box)
12
+ left, top, width, height =
13
+ if box.respond_to?(:left)
14
+ [box.left, box.top, box.width, box.height]
15
+ else
16
+ box.to_a
17
+ end
18
+ Point.new(left + width / 2, top + height / 2)
19
+ end
20
+
21
+ def pixel_matches_color(x, y, expected_rgb, tolerance = 0)
22
+ r, g, b = AutoGUI.pixel(x, y)
23
+ er, eg, eb = expected_rgb.to_a
24
+ (r - er).abs <= tolerance && (g - eg).abs <= tolerance && (b - eb).abs <= tolerance
25
+ end
26
+
27
+ def locate(needle, haystack, grayscale: false, confidence: nil)
28
+ locate_all(needle, haystack, grayscale: grayscale, confidence: confidence, limit: 1).first
29
+ end
30
+
31
+ def locate_all(needle, haystack, grayscale: false, confidence: nil, limit: nil)
32
+ return enum_for(:locate_all, needle, haystack, grayscale: grayscale, confidence: confidence, limit: limit) unless block_given?
33
+
34
+ needle = load_image(needle)
35
+ haystack = load_image(haystack)
36
+ if grayscale
37
+ needle = needle.grayscale
38
+ haystack = haystack.grayscale
39
+ end
40
+
41
+ nw = needle.width
42
+ nh = needle.height
43
+ hw = haystack.width
44
+ hh = haystack.height
45
+ raise AutoGUIException, "needle is larger than haystack" if nw > hw || nh > hh
46
+
47
+ found = 0
48
+ max_x = hw - nw
49
+ max_y = hh - nh
50
+ nd = needle.data
51
+ hd = haystack.data
52
+ nsize = nw * nh * 3
53
+
54
+ (0..max_y).each do |y|
55
+ (0..max_x).each do |x|
56
+ matched =
57
+ if confidence.nil?
58
+ exact_match?(hd, hw, nd, nw, nh, x, y)
59
+ else
60
+ score = match_score(hd, hw, nd, nw, nh, x, y, nsize)
61
+ score >= confidence
62
+ end
63
+ next unless matched
64
+
65
+ yield Box.new(x, y, nw, nh)
66
+ found += 1
67
+ return if limit && found >= limit
68
+ end
69
+ end
70
+ end
71
+
72
+ def exact_match?(hay, hw, needle, nw, nh, x, y)
73
+ row_bytes = nw * 3
74
+ nh.times do |row|
75
+ hay_off = ((y + row) * hw + x) * 3
76
+ needle_off = row * row_bytes
77
+ return false unless hay.byteslice(hay_off, row_bytes) == needle.byteslice(needle_off, row_bytes)
78
+ end
79
+ true
80
+ end
81
+
82
+ def match_score(hay, hw, needle, nw, nh, x, y, nsize)
83
+ diff = 0
84
+ ni = 0
85
+ nh.times do |row|
86
+ hay_off = ((y + row) * hw + x) * 3
87
+ (nw * 3).times do |c|
88
+ diff += (hay.getbyte(hay_off + c) - needle.getbyte(ni)).abs
89
+ ni += 1
90
+ end
91
+ end
92
+ 1.0 - (diff.to_f / (nsize * 255.0))
93
+ end
94
+ end
95
+ end