-
Notifications
You must be signed in to change notification settings - Fork 3
/
vimium-everywhere.ahk
348 lines (320 loc) · 9.17 KB
/
vimium-everywhere.ahk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Change to 1 via env var for a simple Alt+F mapping and to skip the window detection magic:
simple_mode = 0
EnvGet, ENV_SIMPLE_MODE, SIMPLE_MODE
If ENV_SIMPLE_MODE <>
If ENV_SIMPLE_MODE != 0
simple_mode = 1
; Comma-separated list of window classes where this script should not be active. This property is ignored in simple mode. You can determine class names using e.g. WindowSpy or xprop.
exclude_windows = VSCodium
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Must be something else than the script or binary name to avoid mismatches from terminal title etc.
app_name = Vimium Everywhere Gui
; Can be any color, but this value prevents a flickering until TransColor comes into effect (Linux):
win_trans_color = rgba(0`,0`,0`,0)
DetectHiddenWindows, On
; Figure out the offset which an invisible, caption-less Gui window still always has.
; Expected is 0/0, but can be a few pixels.
Gui, -Caption +ToolWindow +0x80000000 +E0x8000000
Gui, Show, x0 y0, %app_name% ; TODO also match pid somehow
; TODO: Use WinWaitExist *command* instead (AHK_X11 does not yet offer it)
GoSub, WinWaitExist
; TODO: For all matches by app_name use compare by Gui WID instead (AHK_X11 does not yet offer it)
WinGet, gui_win_id, ID, %app_name%
WinGetPos, gui_win_offset_x, gui_win_offset_y, , , ahk_id %gui_win_id%
Gui, Destroy
/*
There are generally two operations:
1. Build
Runs `WinGet,, ControlList` and `ControlGetPos` for each of those controls.
This is the performance bottleneck of this script (Linux). It can take *seconds*
with windows with many elements, even though these commands are quite optimized.
Also builds the Gui (invisible with visible action numbers as buttons)
2. Show
Shows the previously built Gui and asks the user for input, then interacts
with the selected control, if any.
- Simple mode just runs these two one after another whenever fired via Hotkey.
- Non-simple mode continuously listens for key or mouse input or active window
title change and then (debounced) runs `Build` on the currently active window,
so that the `Show` Hotkey action appears to be almost instant. This approach
is obviously much more CPU intensive, and more prone to bugs.
Also, it adds `k` and `j` as alternative keys for `up` and `down`.
Also, it adds an "input" mode which can be activated and deactivated with `i`
and `Escape`, respectively. Once in input mode, all hotkeys (j, k and esp. f)
are deactivated which is handy for typing.
*/
If simple_mode = 1
{
; Alt-F
Hotkey, !f, Build_Show
} Else {
Hotkey, i, Start_Input_Mode
end_input_mode_hotkey = Escape
Hotkey, f, Show
; Listening for mouse input:
Hotkey, ~LButton up, User_Input
Hotkey, ~WheelUp up, User_Input
Hotkey, ~WheelDown up, User_Input
Hotkey, ~MButton up, User_Input
Hotkey, j, Scroll_Down
Hotkey, k, Scroll_Up
input_mode = 0
keyboard_event_loop_stopped = 1
keyboard_event_loop_running = 0
build_pending = 0
is_building = 0
is_showing = 0
is_exclude_window = 0
show_queued = 0
GoSub, Build
keyboard_event_loop_running = 0
GoSub, Start_Keyboard_Event_Loop
GoSub, Start_Win_Title_Change_Detection_Loop
}
Return
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Listening for key input.
; This non-blocking input loop runs all the time in the background, except in
; link selection when it is temporarily superceded by a one-key blocking input.
Start_Keyboard_Event_Loop:
keyboard_event_loop_stopped = 0
; Continue as independent subthread so this does not block:
SetTimer, _kbd_event_loop, 1
Return
_kbd_event_loop:
SetTimer, _kbd_event_loop, OFF
If keyboard_event_loop_running <> 0
Return
If keyboard_event_loop_stopped = 1
Return
keyboard_event_loop_running = 1
Loop
{
; This is the official AHK way of listening for any key press:
Input, key, V L1 B, {LControl}{RControl}{LAlt}{RAlt}{LShift}{RShift}{LWin}{RWin}{AppsKey}{F1}{F2}{F3}{F4}{F5}{F6}{F7}{F8}{F9}{F10}{F11}{F12}{Left}{Right}{Up}{Down}{Home}{End}{PgUp}{PgDn}{Del}{Ins}{BS}{CapsLock}{NumLock}{PrintScreen}
If keyboard_event_loop_stopped = 1
{
keyboard_event_loop_running = 0
Return
}
IfNotInString, key, f
GoSub, User_Input
}
Return
Stop_Keyboard_Event_Loop:
keyboard_event_loop_stopped = 1
Input
Return
User_Input:
; So that should the `Show` Hotkey be fired before the next 350ms, a rebuild is preponed
build_pending = 1
SetTimer, Build, 250 ; Debounce
Return
; Watching window title
Start_Win_Title_Change_Detection_Loop:
SetTimer, _title_change_detection_loop, 300
Return
_title_change_detection_loop:
WinGetTitle, change_detection_active_win_title, A
If change_detection_active_win_title <> %build_active_win_title%
GoSub, User_Input
Return
Start_Input_Mode:
input_mode = 1
Hotkey, %end_input_mode_hotkey%, End_Input_Mode
ToolTip, %A_Space%i%A_Space%, 0, 0
Suspend, On
return
End_Input_Mode:
Suspend, Off
WinGetClass, win_class, A
If win_class in %exclude_windows%
{
Suspend, On
Return
}
input_mode = 0
Hotkey, %end_input_mode_hotkey%, OFF
ToolTip
GoSub, Build
return
; Simple mode
Build_Show:
GoSub, Build
GoSub, Show
Return
Build:
SetTimer, Build, OFF
build_pending = 0
If is_building = 1
Return
If is_showing = 1
Return
WinGet, build_active_win_id, ID, A
If simple_mode <> 1
{
WinGetClass, win_class, ahk_id %build_active_win_id%
If win_class in %exclude_windows%
{
is_exclude_window = 1
If input_mode = 0
; Exclude windows are excluded by simply running in input mode permanently
GoSub, Start_Input_Mode
Return
}
If is_exclude_window = 1
{
is_exclude_window = 0
GoSub, End_Input_Mode
Return
}
If input_mode = 1
Return
WinGetTitle, build_active_win_title, ahk_id %build_active_win_id%
If build_active_win_title = %app_name%
Return
}
is_building = 1
Gui, Destroy
Gui, Color, %win_trans_color%
Gui, -Caption +ToolWindow
ToolTip, Building..., 0, 0
; Can be very slow in general (~1 second on Firefox).
; Also, at-spi initialization can take even longer the first time a control command runs.
WinGet, all_controls, ControlList, ahk_id %build_active_win_id%
WinGetPos, win_offset_x, win_offset_y, , , ahk_id %build_active_win_id%
win_offset_x -= %gui_win_offset_x%
win_offset_y -= %gui_win_offset_y%
control_count = 0
available_letters = QFDSAGWERT
decimal_factors = 1000|100|10|1 ; Exponentials of available_letters.length
Loop, PARSE, all_controls, `n
{
ControlGetPos, x, y, , , %A_LoopField%, ahk_id %build_active_win_id%
If x < 0
Continue
control_count++
If control_count > 999
Break
control = %A_LoopField%
x += %win_offset_x%
y += %win_offset_y%
match_letters =
rest = %control_count%
; Transform %control_count% into %match_letters% (by index) with the weirdess
; that is ahk legacy syntax:
Loop, PARSE, decimal_factors, |
{
If control_count < %A_LoopField%
Continue
factor_pos = %rest%
factor_pos /= %A_LoopField%
factor_val = %factor_pos%
factor_val *= %A_LoopField%
rest -= %factor_val%
factor_pos++
StringMid, letter, available_letters, %factor_pos%, 1
match_letters = %match_letters%%letter%
}
; Does not yet show the Gui
Gui, Add, Button, x%x% y%y% w10 h10, %match_letters%
match_controls_%match_letters% = %control%
}
all_controls =
ToolTip
is_building = 0
If show_queued = 1
{
show_queued = 0
GoSub, Show
}
Return
Show:
If control_count <= 0
Return
If is_building = 1
{
show_queued = 1
Return
}
If is_showing = 1
Return
If build_pending = 1
{
show_queued = 1
GoSub, Build
Return
}
is_showing = 1
WinGet, show_active_win_id, ID, A
Gui, Show, x0 y0, %app_name%
If show_active_win_id <> %build_active_win_id%
{
; User switched application while building, need to rebuild
Sleep, 10
show_queued = 1
is_showing = 0
GoSub, Build
Return
}
; TODO: Use WinWaitExist *command* instead (AHK_X11 does not yet offer it)
GoSub, WinWaitExist
; Because of 0x80000000, our gui never takes focus unless we force it with WinActivate.
WinGet, gui_win_id, ID, %app_name%
WinSet, TransColor, %win_trans_color%, ahk_id %gui_win_id%
WinSet, Transparent, 230, ahk_id %gui_win_id%
WinSet, AlwaysOnTop, ON, ahk_id %gui_win_id%
If simple_mode <> 1
GoSub, Stop_Keyboard_Event_Loop
selection =
Loop
{
Input, key, L1, {Escape}{Space}
If ErrorLevel = EndKey:escape
selection =
If ErrorLevel <> Max
Break
selection = %selection%%key%
StringUpper, selection, selection
ToolTip, %A_Space%%selection%%A_Space% ... (Press SPACE to confirm or ESCAPE to cancel), 0, 0
}
ToolTip
control =
; Array access:
StringLeft, control, match_controls_%selection%, 10000
If control <>
ControlClick, %control%, ahk_id %show_active_win_id%
Gui, Hide
is_showing = 0
If simple_mode <> 1
{
GoSub, Start_Keyboard_Event_Loop
If control <>
{
; To give the application some time to update its view based on the previous ControlClick.
; Will not work for network-based actions such as buttons in browsers though
Sleep, 50
GoSub, Build
}
}
return
Scroll_Down:
; MouseClick, WD is not the right solution as this can change window focus in Linux
Send, {Down}
Return
Scroll_Up:
Send, {Up}
Return
WinWaitExist:
Loop
{
IfWinExist, %app_name%
Break
If A_Index > 20
{
MsgBox, Vimium Everywhere's internal window could not be found
Break
}
Sleep, 50
}
Return