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,344 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
require "stringio"
|
|
5
|
+
|
|
6
|
+
module AutoGUI
|
|
7
|
+
# Packed RGB bitmap used by screenshot and locate* functions.
|
|
8
|
+
# +data+ is a binary string of 3-byte RGB pixels, row-major, top-down.
|
|
9
|
+
class Image
|
|
10
|
+
attr_reader :width, :height, :data
|
|
11
|
+
|
|
12
|
+
def initialize(width, height, data = nil)
|
|
13
|
+
@width = width.to_i
|
|
14
|
+
@height = height.to_i
|
|
15
|
+
expected = @width * @height * 3
|
|
16
|
+
@data =
|
|
17
|
+
if data.nil?
|
|
18
|
+
"\x00".b * expected
|
|
19
|
+
else
|
|
20
|
+
d = data.dup.force_encoding(Encoding::BINARY)
|
|
21
|
+
raise AutoGUIException, "image data size mismatch" unless d.bytesize == expected
|
|
22
|
+
|
|
23
|
+
d
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def size
|
|
28
|
+
[@width, @height]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def [](x, y = nil)
|
|
32
|
+
if y.nil? && x.is_a?(Array)
|
|
33
|
+
y = x[1]
|
|
34
|
+
x = x[0]
|
|
35
|
+
end
|
|
36
|
+
getpixel(x, y)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def getpixel(x, y)
|
|
40
|
+
x = x.to_i
|
|
41
|
+
y = y.to_i
|
|
42
|
+
raise ArgumentError, "pixel out of bounds" unless x.between?(0, @width - 1) && y.between?(0, @height - 1)
|
|
43
|
+
|
|
44
|
+
i = (y * @width + x) * 3
|
|
45
|
+
[@data.getbyte(i), @data.getbyte(i + 1), @data.getbyte(i + 2)]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def putpixel(x, y, rgb)
|
|
49
|
+
x = x.to_i
|
|
50
|
+
y = y.to_i
|
|
51
|
+
raise ArgumentError, "pixel out of bounds" unless x.between?(0, @width - 1) && y.between?(0, @height - 1)
|
|
52
|
+
|
|
53
|
+
i = (y * @width + x) * 3
|
|
54
|
+
@data.setbyte(i, rgb[0].to_i)
|
|
55
|
+
@data.setbyte(i + 1, rgb[1].to_i)
|
|
56
|
+
@data.setbyte(i + 2, rgb[2].to_i)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def grayscale
|
|
60
|
+
out = String.new(capacity: @width * @height * 3, encoding: Encoding::BINARY)
|
|
61
|
+
i = 0
|
|
62
|
+
while i < @data.bytesize
|
|
63
|
+
r = @data.getbyte(i)
|
|
64
|
+
g = @data.getbyte(i + 1)
|
|
65
|
+
b = @data.getbyte(i + 2)
|
|
66
|
+
y = ((r * 299) + (g * 587) + (b * 114)) / 1000
|
|
67
|
+
out << y.chr << y.chr << y.chr
|
|
68
|
+
i += 3
|
|
69
|
+
end
|
|
70
|
+
Image.new(@width, @height, out)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def crop(left, top, width, height)
|
|
74
|
+
left = left.to_i
|
|
75
|
+
top = top.to_i
|
|
76
|
+
width = width.to_i
|
|
77
|
+
height = height.to_i
|
|
78
|
+
raise ArgumentError, "invalid crop" if width <= 0 || height <= 0
|
|
79
|
+
raise ArgumentError, "crop out of bounds" unless left >= 0 && top >= 0 && left + width <= @width && top + height <= @height
|
|
80
|
+
|
|
81
|
+
out = String.new(capacity: width * height * 3, encoding: Encoding::BINARY)
|
|
82
|
+
height.times do |row|
|
|
83
|
+
src = ((top + row) * @width + left) * 3
|
|
84
|
+
out << @data.byteslice(src, width * 3)
|
|
85
|
+
end
|
|
86
|
+
Image.new(width, height, out)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def save(path)
|
|
90
|
+
path = path.to_s
|
|
91
|
+
ext = File.extname(path).downcase
|
|
92
|
+
File.binwrite(path, ext == ".bmp" ? to_bmp : to_png)
|
|
93
|
+
self
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def to_png
|
|
97
|
+
raw = String.new(capacity: @height * (1 + @width * 3), encoding: Encoding::BINARY)
|
|
98
|
+
@height.times do |y|
|
|
99
|
+
raw << "\x00"
|
|
100
|
+
raw << @data.byteslice(y * @width * 3, @width * 3)
|
|
101
|
+
end
|
|
102
|
+
compressed = Zlib::Deflate.deflate(raw, Zlib::BEST_SPEED)
|
|
103
|
+
png = "\x89PNG\r\n\x1a\n".b
|
|
104
|
+
png << png_chunk("IHDR", [@width, @height, 8, 2, 0, 0, 0].pack("NNC5"))
|
|
105
|
+
png << png_chunk("IDAT", compressed)
|
|
106
|
+
png << png_chunk("IEND", "".b)
|
|
107
|
+
png
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def to_bmp
|
|
111
|
+
row_stride = ((@width * 3 + 3) / 4) * 4
|
|
112
|
+
pixel_size = row_stride * @height
|
|
113
|
+
file_size = 54 + pixel_size
|
|
114
|
+
file_header = ["BM", file_size, 0, 0, 54].pack("a2VvvV")
|
|
115
|
+
info_header = [40, @width, @height, 1, 24, 0, pixel_size, 0, 0, 0, 0].pack("VllvvVVllVV")
|
|
116
|
+
pixels = String.new(capacity: pixel_size, encoding: Encoding::BINARY)
|
|
117
|
+
pad = "\x00".b * (row_stride - @width * 3)
|
|
118
|
+
(@height - 1).downto(0) do |y|
|
|
119
|
+
row = @data.byteslice(y * @width * 3, @width * 3).dup
|
|
120
|
+
i = 0
|
|
121
|
+
while i < row.bytesize
|
|
122
|
+
r = row.getbyte(i)
|
|
123
|
+
b = row.getbyte(i + 2)
|
|
124
|
+
row.setbyte(i, b)
|
|
125
|
+
row.setbyte(i + 2, r)
|
|
126
|
+
i += 3
|
|
127
|
+
end
|
|
128
|
+
pixels << row << pad
|
|
129
|
+
end
|
|
130
|
+
file_header + info_header + pixels
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.open(path_or_image)
|
|
134
|
+
return path_or_image if path_or_image.is_a?(Image)
|
|
135
|
+
|
|
136
|
+
path = path_or_image.to_s
|
|
137
|
+
bytes = File.binread(path)
|
|
138
|
+
if bytes.start_with?("\x89PNG".b)
|
|
139
|
+
from_png(bytes)
|
|
140
|
+
elsif bytes.start_with?("BM")
|
|
141
|
+
from_bmp(bytes)
|
|
142
|
+
else
|
|
143
|
+
raise AutoGUIException, "unsupported image format: #{path} (use PNG or BMP)"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def self.from_png(bytes)
|
|
148
|
+
raise AutoGUIException, "invalid PNG signature" unless bytes.start_with?("\x89PNG\r\n\x1a\n".b)
|
|
149
|
+
|
|
150
|
+
offset = 8
|
|
151
|
+
width = height = nil
|
|
152
|
+
bit_depth = color_type = nil
|
|
153
|
+
idat = String.new(encoding: Encoding::BINARY)
|
|
154
|
+
palette = nil
|
|
155
|
+
until offset >= bytes.bytesize
|
|
156
|
+
length = bytes.byteslice(offset, 4).unpack1("N")
|
|
157
|
+
type = bytes.byteslice(offset + 4, 4)
|
|
158
|
+
data = bytes.byteslice(offset + 8, length)
|
|
159
|
+
offset += 12 + length
|
|
160
|
+
case type
|
|
161
|
+
when "IHDR"
|
|
162
|
+
width, height, bit_depth, color_type = data.unpack("NNC2")
|
|
163
|
+
when "PLTE"
|
|
164
|
+
palette = data
|
|
165
|
+
when "IDAT"
|
|
166
|
+
idat << data
|
|
167
|
+
when "IEND"
|
|
168
|
+
break
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
raise AutoGUIException, "PNG missing IHDR" if width.nil?
|
|
172
|
+
raise AutoGUIException, "only 8-bit PNG is supported" unless bit_depth == 8
|
|
173
|
+
|
|
174
|
+
inflated = Zlib::Inflate.inflate(idat)
|
|
175
|
+
channels =
|
|
176
|
+
case color_type
|
|
177
|
+
when 0 then 1
|
|
178
|
+
when 2 then 3
|
|
179
|
+
when 3 then 1
|
|
180
|
+
when 4 then 2
|
|
181
|
+
when 6 then 4
|
|
182
|
+
else
|
|
183
|
+
raise AutoGUIException, "unsupported PNG color type #{color_type}"
|
|
184
|
+
end
|
|
185
|
+
rgb = unfilter_png(inflated, width, height, channels, color_type, palette)
|
|
186
|
+
new(width, height, rgb)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def self.from_bmp(bytes)
|
|
190
|
+
_magic, _file_size, _res1, _res2, offset = bytes.unpack("a2VvvV")
|
|
191
|
+
header_size, width, height, _planes, bits, compression =
|
|
192
|
+
bytes.byteslice(14, 24).unpack("VllvvV")
|
|
193
|
+
raise AutoGUIException, "compressed BMP is not supported" unless compression.to_i.zero?
|
|
194
|
+
raise AutoGUIException, "unsupported BMP bit depth" unless [24, 32].include?(bits)
|
|
195
|
+
|
|
196
|
+
top_down = height.negative?
|
|
197
|
+
height = height.abs
|
|
198
|
+
bytes_pp = bits / 8
|
|
199
|
+
row_stride = ((width * bytes_pp + 3) / 4) * 4
|
|
200
|
+
rgb = String.new(capacity: width * height * 3, encoding: Encoding::BINARY)
|
|
201
|
+
height.times do |row|
|
|
202
|
+
src_row = top_down ? row : (height - 1 - row)
|
|
203
|
+
src = offset + src_row * row_stride
|
|
204
|
+
width.times do |x|
|
|
205
|
+
i = src + x * bytes_pp
|
|
206
|
+
b = bytes.getbyte(i)
|
|
207
|
+
g = bytes.getbyte(i + 1)
|
|
208
|
+
r = bytes.getbyte(i + 2)
|
|
209
|
+
rgb << r.chr << g.chr << b.chr
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
new(width, height, rgb)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def self.from_bgra(width, height, bgra, stride = nil)
|
|
216
|
+
stride ||= width * 4
|
|
217
|
+
rgb = String.new(capacity: width * height * 3, encoding: Encoding::BINARY)
|
|
218
|
+
height.times do |y|
|
|
219
|
+
row = y * stride
|
|
220
|
+
width.times do |x|
|
|
221
|
+
i = row + x * 4
|
|
222
|
+
rgb << bgra.getbyte(i + 2).chr << bgra.getbyte(i + 1).chr << bgra.getbyte(i).chr
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
new(width, height, rgb)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def self.from_pixels(width, height, pixels)
|
|
229
|
+
data = String.new(capacity: width * height * 3, encoding: Encoding::BINARY)
|
|
230
|
+
pixels.each do |r, g, b|
|
|
231
|
+
data << r.chr << g.chr << b.chr
|
|
232
|
+
end
|
|
233
|
+
new(width, height, data)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
class << self
|
|
237
|
+
private
|
|
238
|
+
|
|
239
|
+
def png_chunk_crc(type, data)
|
|
240
|
+
Zlib.crc32(type + data)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def unfilter_png(inflated, width, height, channels, color_type, palette)
|
|
244
|
+
bpp = channels
|
|
245
|
+
stride = width * bpp
|
|
246
|
+
prev = "\x00".b * stride
|
|
247
|
+
rgb = String.new(capacity: width * height * 3, encoding: Encoding::BINARY)
|
|
248
|
+
pos = 0
|
|
249
|
+
height.times do
|
|
250
|
+
filter = inflated.getbyte(pos)
|
|
251
|
+
pos += 1
|
|
252
|
+
raw = inflated.byteslice(pos, stride).dup
|
|
253
|
+
pos += stride
|
|
254
|
+
recon = apply_filter(filter, raw, prev, bpp)
|
|
255
|
+
prev = recon
|
|
256
|
+
case color_type
|
|
257
|
+
when 2
|
|
258
|
+
rgb << recon
|
|
259
|
+
when 0
|
|
260
|
+
recon.bytesize.times { |i| rgb << recon[i] << recon[i] << recon[i] }
|
|
261
|
+
when 4
|
|
262
|
+
(width).times do |x|
|
|
263
|
+
g = recon.getbyte(x * 2)
|
|
264
|
+
a = recon.getbyte(x * 2 + 1)
|
|
265
|
+
v = composite_white(g, a)
|
|
266
|
+
rgb << v.chr << v.chr << v.chr
|
|
267
|
+
end
|
|
268
|
+
when 6
|
|
269
|
+
width.times do |x|
|
|
270
|
+
i = x * 4
|
|
271
|
+
a = recon.getbyte(i + 3)
|
|
272
|
+
rgb << composite_white(recon.getbyte(i), a).chr
|
|
273
|
+
rgb << composite_white(recon.getbyte(i + 1), a).chr
|
|
274
|
+
rgb << composite_white(recon.getbyte(i + 2), a).chr
|
|
275
|
+
end
|
|
276
|
+
when 3
|
|
277
|
+
raise AutoGUIException, "PNG palette missing" if palette.nil?
|
|
278
|
+
|
|
279
|
+
width.times do |x|
|
|
280
|
+
idx = recon.getbyte(x) * 3
|
|
281
|
+
rgb << palette.getbyte(idx).chr << palette.getbyte(idx + 1).chr << palette.getbyte(idx + 2).chr
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
rgb
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def apply_filter(filter, raw, prev, bpp)
|
|
289
|
+
recon = raw.dup
|
|
290
|
+
case filter
|
|
291
|
+
when 0
|
|
292
|
+
recon
|
|
293
|
+
when 1
|
|
294
|
+
recon.bytesize.times do |i|
|
|
295
|
+
left = i >= bpp ? recon.getbyte(i - bpp) : 0
|
|
296
|
+
recon.setbyte(i, (raw.getbyte(i) + left) & 255)
|
|
297
|
+
end
|
|
298
|
+
recon
|
|
299
|
+
when 2
|
|
300
|
+
recon.bytesize.times do |i|
|
|
301
|
+
recon.setbyte(i, (raw.getbyte(i) + prev.getbyte(i)) & 255)
|
|
302
|
+
end
|
|
303
|
+
recon
|
|
304
|
+
when 3
|
|
305
|
+
recon.bytesize.times do |i|
|
|
306
|
+
left = i >= bpp ? recon.getbyte(i - bpp) : 0
|
|
307
|
+
up = prev.getbyte(i)
|
|
308
|
+
recon.setbyte(i, (raw.getbyte(i) + ((left + up) / 2)) & 255)
|
|
309
|
+
end
|
|
310
|
+
recon
|
|
311
|
+
when 4
|
|
312
|
+
recon.bytesize.times do |i|
|
|
313
|
+
a = i >= bpp ? recon.getbyte(i - bpp) : 0
|
|
314
|
+
b = prev.getbyte(i)
|
|
315
|
+
c = i >= bpp ? prev.getbyte(i - bpp) : 0
|
|
316
|
+
recon.setbyte(i, (raw.getbyte(i) + paeth(a, b, c)) & 255)
|
|
317
|
+
end
|
|
318
|
+
recon
|
|
319
|
+
else
|
|
320
|
+
raise AutoGUIException, "unsupported PNG filter #{filter}"
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def paeth(a, b, c)
|
|
325
|
+
p = a + b - c
|
|
326
|
+
pa = (p - a).abs
|
|
327
|
+
pb = (p - b).abs
|
|
328
|
+
pc = (p - c).abs
|
|
329
|
+
return a if pa <= pb && pa <= pc
|
|
330
|
+
return b if pb <= pc
|
|
331
|
+
|
|
332
|
+
c
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def composite_white(value, alpha)
|
|
336
|
+
((value * alpha) + (255 * (255 - alpha))) / 255
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def png_chunk(type, data)
|
|
341
|
+
[data.bytesize].pack("N") + type + data + [Zlib.crc32(type + data)].pack("N")
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
data/lib/autogui/keys.rb
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AutoGUI
|
|
4
|
+
KEY_NAMES = [
|
|
5
|
+
"\t", "\n", "\r", " ", "!", "\"", "#", "$", "%", "&", "'", "(",
|
|
6
|
+
")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7",
|
|
7
|
+
"8", "9", ":", ";", "<", "=", ">", "?", "@", "[", "\\", "]", "^", "_", "`",
|
|
8
|
+
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o",
|
|
9
|
+
"p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "~",
|
|
10
|
+
"accept", "add", "alt", "altleft", "altright", "apps", "backspace",
|
|
11
|
+
"browserback", "browserfavorites", "browserforward", "browserhome",
|
|
12
|
+
"browserrefresh", "browsersearch", "browserstop", "capslock", "clear",
|
|
13
|
+
"convert", "ctrl", "ctrlleft", "ctrlright", "decimal", "del", "delete",
|
|
14
|
+
"divide", "down", "end", "enter", "esc", "escape", "execute", "f1", "f10",
|
|
15
|
+
"f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", "f2", "f20",
|
|
16
|
+
"f21", "f22", "f23", "f24", "f3", "f4", "f5", "f6", "f7", "f8", "f9",
|
|
17
|
+
"final", "fn", "hanguel", "hangul", "hanja", "help", "home", "insert",
|
|
18
|
+
"junja", "kana", "kanji", "launchapp1", "launchapp2", "launchmail",
|
|
19
|
+
"launchmediaselect", "left", "modechange", "multiply", "nexttrack",
|
|
20
|
+
"nonconvert", "num0", "num1", "num2", "num3", "num4", "num5", "num6",
|
|
21
|
+
"num7", "num8", "num9", "numlock", "pagedown", "pageup", "pause", "pgdn",
|
|
22
|
+
"pgup", "playpause", "prevtrack", "print", "printscreen", "prntscrn",
|
|
23
|
+
"prtsc", "prtscr", "return", "right", "scrolllock", "select", "separator",
|
|
24
|
+
"shift", "shiftleft", "shiftright", "sleep", "space", "stop", "subtract",
|
|
25
|
+
"tab", "up", "volumedown", "volumemute", "volumeup", "win", "winleft",
|
|
26
|
+
"winright", "yen", "command", "option", "optionleft", "optionright"
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
KEYBOARD_KEYS = KEY_NAMES
|
|
30
|
+
|
|
31
|
+
LEFT = "left"
|
|
32
|
+
MIDDLE = "middle"
|
|
33
|
+
RIGHT = "right"
|
|
34
|
+
PRIMARY = "primary"
|
|
35
|
+
SECONDARY = "secondary"
|
|
36
|
+
|
|
37
|
+
QWERTY = "`1234567890-=qwertyuiop[]\\asdfghjkl;'zxcvbnm,./~!@\#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:\"ZXCVBNM<>?".freeze
|
|
38
|
+
QWERTZ = "=1234567890/0qwertzuiop89-asdfghjkl,\\yxcvbnm,.7+!@\#$%^&*()?)QWERTZUIOP*(_ASDFGHJKL<|YXCVBNM<>&".freeze
|
|
39
|
+
|
|
40
|
+
SHIFT_CHARS = ['~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', '{', '}', '|', ':', '"', '<', '>', '?'].freeze
|
|
41
|
+
|
|
42
|
+
def self.shift_character?(character)
|
|
43
|
+
return false if character.nil? || character.empty?
|
|
44
|
+
|
|
45
|
+
character != character.downcase || SHIFT_CHARS.include?(character)
|
|
46
|
+
end
|
|
47
|
+
class << self
|
|
48
|
+
alias isShiftCharacter shift_character?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
module AutoGUI
|
|
7
|
+
module MessageBox
|
|
8
|
+
MB_OK = 0x00000000
|
|
9
|
+
MB_OKCANCEL = 0x00000001
|
|
10
|
+
MB_YESNOCANCEL = 0x00000003
|
|
11
|
+
MB_YESNO = 0x00000004
|
|
12
|
+
IDOK = 1
|
|
13
|
+
IDCANCEL = 2
|
|
14
|
+
IDYES = 6
|
|
15
|
+
IDNO = 7
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def alert(text = "", title = "AutoGUI Alert", button = "OK")
|
|
20
|
+
if windows?
|
|
21
|
+
Platform.current.message_box(text.to_s, title.to_s, MB_OK)
|
|
22
|
+
button.to_s
|
|
23
|
+
else
|
|
24
|
+
dialog_alert(text, title, button)
|
|
25
|
+
button.to_s
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def confirm(text = "", title = "AutoGUI Confirm", buttons = %w[OK Cancel])
|
|
30
|
+
buttons = Array(buttons)
|
|
31
|
+
if windows? && buttons.map(&:to_s) == %w[OK Cancel]
|
|
32
|
+
result = Platform.current.message_box(text.to_s, title.to_s, MB_OKCANCEL)
|
|
33
|
+
result == IDOK ? "OK" : "Cancel"
|
|
34
|
+
elsif windows? && buttons.map(&:to_s) == %w[Yes No]
|
|
35
|
+
result = Platform.current.message_box(text.to_s, title.to_s, MB_YESNO)
|
|
36
|
+
result == IDYES ? "Yes" : "No"
|
|
37
|
+
else
|
|
38
|
+
dialog_choice(text, title, buttons)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def prompt(text = "", title = "AutoGUI Prompt", default = "")
|
|
43
|
+
dialog_input(text, title, default, password: false)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def password(text = "", title = "AutoGUI Password", default = "", mask = "*")
|
|
47
|
+
dialog_input(text, title, default, password: true, mask: mask)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def windows?
|
|
51
|
+
RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/i
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def dialog_alert(text, title, _button)
|
|
55
|
+
if windows?
|
|
56
|
+
Platform.current.message_box(text.to_s, title.to_s, MB_OK)
|
|
57
|
+
else
|
|
58
|
+
system("osascript", "-e", %(display dialog #{js(text)} with title #{js(title)} buttons {"OK"} default button 1)) ||
|
|
59
|
+
system("zenity", "--info", "--title=#{title}", "--text=#{text}")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def dialog_choice(text, title, buttons)
|
|
64
|
+
if windows?
|
|
65
|
+
vbs = <<~VBS
|
|
66
|
+
Set fso = CreateObject("Scripting.FileSystemObject")
|
|
67
|
+
Set stdout = fso.GetStandardStream(1)
|
|
68
|
+
r = MsgBox(#{vbs_str(text)}, vbYesNoCancel, #{vbs_str(title)})
|
|
69
|
+
If r = vbYes Then stdout.Write "Yes"
|
|
70
|
+
If r = vbNo Then stdout.Write "No"
|
|
71
|
+
If r = vbCancel Then stdout.Write "Cancel"
|
|
72
|
+
VBS
|
|
73
|
+
out = run_vbs(vbs)
|
|
74
|
+
mapped = { "Yes" => buttons[0], "No" => buttons[1], "Cancel" => buttons[2] || buttons[-1] }
|
|
75
|
+
mapped[out] || buttons[-1]
|
|
76
|
+
else
|
|
77
|
+
btns = buttons.map(&:to_s)
|
|
78
|
+
script = "set r to button returned of (display dialog #{js(text)} with title #{js(title)} buttons {#{btns.map { |b| js(b) }.join(',')}} default button 1)"
|
|
79
|
+
out, = Open3.capture2("osascript", "-e", script)
|
|
80
|
+
out.strip.empty? ? nil : out.strip
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def dialog_input(text, title, default, password: false, mask: "*")
|
|
85
|
+
if windows?
|
|
86
|
+
vbs = <<~VBS
|
|
87
|
+
Set fso = CreateObject("Scripting.FileSystemObject")
|
|
88
|
+
Set stdout = fso.GetStandardStream(1)
|
|
89
|
+
r = InputBox(#{vbs_str(text)}, #{vbs_str(title)}, #{vbs_str(default)})
|
|
90
|
+
If IsEmpty(r) Then
|
|
91
|
+
stdout.Write Chr(0)
|
|
92
|
+
Else
|
|
93
|
+
stdout.Write r
|
|
94
|
+
End If
|
|
95
|
+
VBS
|
|
96
|
+
out = run_vbs(vbs)
|
|
97
|
+
return nil if out == "\u0000" || out.nil?
|
|
98
|
+
|
|
99
|
+
out
|
|
100
|
+
else
|
|
101
|
+
hidden = password ? " with hidden answer" : ""
|
|
102
|
+
script = "text returned of (display dialog #{js(text)} with title #{js(title)} default answer #{js(default)}#{hidden})"
|
|
103
|
+
out, status = Open3.capture2("osascript", "-e", script)
|
|
104
|
+
status.success? ? out.sub(/\r?\n\z/, "") : nil
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def run_vbs(script)
|
|
109
|
+
file = Tempfile.new(["autogui", ".vbs"])
|
|
110
|
+
file.write(script)
|
|
111
|
+
file.close
|
|
112
|
+
out, = Open3.capture2("cscript", "//Nologo", file.path)
|
|
113
|
+
File.unlink(file.path) rescue nil
|
|
114
|
+
out
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def vbs_str(s)
|
|
118
|
+
%("#{s.to_s.gsub('"', '""')}")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def js(s)
|
|
122
|
+
s.to_s.inspect
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|