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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +20 -0
- data/LICENSE +29 -0
- data/README.md +431 -0
- data/bin/autogui +19 -0
- data/docs/api.md +186 -0
- data/docs/guide.md +243 -0
- data/docs/publishing.md +90 -0
- data/examples/hello.rb +20 -0
- data/examples/locate_demo.rb +16 -0
- data/lib/autogui/exceptions.rb +9 -0
- data/lib/autogui/geometry.rb +98 -0
- data/lib/autogui/image.rb +344 -0
- data/lib/autogui/keys.rb +50 -0
- data/lib/autogui/message_box.rb +125 -0
- data/lib/autogui/platform/darwin.rb +185 -0
- data/lib/autogui/platform/linux.rb +172 -0
- data/lib/autogui/platform/windows.rb +400 -0
- data/lib/autogui/platform.rb +41 -0
- data/lib/autogui/run.rb +168 -0
- data/lib/autogui/screenshot.rb +95 -0
- data/lib/autogui/tween.rb +254 -0
- data/lib/autogui/version.rb +5 -0
- data/lib/autogui/window.rb +170 -0
- data/lib/autogui.rb +738 -0
- metadata +90 -0
|
@@ -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
|
data/lib/autogui/run.rb
ADDED
|
@@ -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
|