-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathAutoCorrectSystem.ahk
534 lines (456 loc) · 21.4 KB
/
AutoCorrectSystem.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
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
; This is AutoCorrectSystem.ahk
; Part of the AutoCorrect2 system
; Contains the logger and backspace detection functionality and other things
; Version: 3-28-2025
;===============================================================================
; AutoCorrect System Module
;===============================================================================
; Master switch to enable/disable logging globally
; Setting this to 0 will:
; 1. Disable the autocorrection log (AutoCorrectsLog.txt)
; 2. Disable the backspace context logger (ErrContextLog.txt)
; 3. Skip all logging operations but maintain other functionality
; This can improve performance and reduce disk writes for users
; who don't need the logging features
Global EnableLogging := 1
;===============================================================================
; Autocorrection Logger Settings
;===============================================================================
beepOnCorrection := 1 ; Beep when the f() function is used.
;===============================================================================
; Backspace Context Logger Settings
;===============================================================================
precedingWordCount := 6 ; Cache this many words for context logging.
followingWordCount := 4 ; Wait for this many additional words before logging.
beepOnContexLog := 1 ; Beep when an "on BS" error is logged.
;===============================================================================
; Logger file paths - these should match settings in the main script
If EnableLogging {
; AutoCorrectsLogFile := A_ScriptDir "\AutoCorrectsLog.txt"
; ErrContextLog := A_ScriptDir "\ErrContextLog.txt"
; Create log files if they don't exist
If not FileExist(Config.AutoCorrectsLogFile)
FileAppend("This will be the log of all autocorrections.`n", Config.AutoCorrectsLogFile)
If not FileExist(Config.ErrContextLog)
FileAppend(
"This will be the log of extended context information for backspaced autocorrections.`n"
"Date Y-M-D`titem`t`t`tcontext`n"
"===============================================", Config.ErrContextLog)
}
;================== Variable Declarations ======================================
Global IsRecent := 0 ; Flag to track if a hot string was recently triggered
Global lastTrigger := "No triggers logged yet." ; Tracks the last used trigger
;===============================================================================
; The main autocorrection logger f() function
; This function is called by all the f-style hotstrings in the library
;===============================================================================
f(replace := "") {
static HSInputBuffer := InputBuffer()
HSInputBuffer.Start()
trigger := A_ThisHotkey
endchar := A_EndChar
Global lastTrigger := StrReplace(trigger, "B0X", "") "::" replace ; Set 'lastTrigger' before removing options and colons.
trigger := SubStr(trigger, inStr(trigger, ":",,,2)+1) ; Use everything to right of 2nd colon.
TrigLen := StrLen(trigger) + StrLen(endchar) ; Determine number of backspaces needed.
; Rarify: Only remove and replace rightmost necessary chars.
trigArr := StrSplit(trigger)
replArr := StrSplit(replace)
endCh := StrLen(endchar)
ignorLen := 0
; Find matching left substring to optimize replacement
Loop Min(trigArr.Length, replArr.Length) {
If (trigArr[A_Index] == replArr[A_Index]) ; The double equal (==) makes it case-sensitive.
ignorLen++
else
break
}
replace := SubStr(replace, (ignorLen+1))
SendInput("{BS " (TrigLen - ignorLen) "}" replace StrReplace(endchar, "!", "{!}")) ; Type replacement and endchar.
HSInputBuffer.Stop()
If (beepOnCorrection = 1)
SoundBeep(900, 60) ; Notification of replacement.
; Only set up logging if it's enabled
if (EnableLogging = 1) {
SetTimer(keepText.bind(LastTrigger), -1)
; For onBsLogger function.
Global IsRecent := 1 ; Set IsRecent, then change back in 1 second.
setTimer (*) => IsRecent := 0, -1000 ; run only once, in 1 second.
}
}
;===============================================================================
; Automatically logs (if enabled) if an autocorrect happens, and if the
; user presses Backspace within the specified timeout
;===============================================================================
#MaxThreadsPerHotkey 5 ; Allow up to 5 instances of the function.
keepText(KeepForLog, *) {
KeepForLog := StrReplace(KeepForLog, "`n", "``n") ; Fixes triggers spanning two lines.
global lih := InputHook("B V I1 E T1", "{Backspace}") ; "logger input hook." T is time-out. T1 = 1 second.
lih.Start()
lih.Wait()
; Determine hyphen style based on whether Backspace was pressed
hyphen := (lih.EndKey = "Backspace") ? " << " : " -- "
; Log the autocorrection with timestamp and hyphen style
FileAppend("`n" A_YYYY "-" A_MM "-" A_DD hyphen KeepForLog, Config.AutoCorrectsLogFile)
}
#MaxThreadsPerHotkey 1
;===============================================================================
; Backspace Context Logger
; Constantly keeps a cache of the last several words typed. If an autocorrection
; is logged, and backspace is pressed within timeout, the cached words AND
; the next X words are logged to help identify misfires.
;===============================================================================
class BackspaceContextLogger {
static WordArr := []
static waitingForExtra := false
static extraWordsCount := 0
static LogEntry := ""
static timeoutTimer := 0
static bsih := 0
static isRunning := false
; Start the logger
static Start() {
; Don't start if logging is disabled globally
if (EnableLogging = 0)
return
if (this.isRunning)
return
this.isRunning := true
; Initialize the input hook for backspace context logging
this.bsih := InputHook("B V I1 E", "{Space}{Backspace}{Tab}{Enter}")
; Start the logger loop in a new thread
SetTimer(() => this.LoggerLoop(), -50)
}
; Process and format the log entry
static LogContent() {
; Skip logging if globally disabled
if (EnableLogging = 0) {
this.waitingForExtra := false
this.extraWordsCount := 0
this.WordArr := []
SetTimer(this.timeoutTimer, 0)
return
}
if !this.waitingForExtra ; Don't proceed if we're no longer waiting
return
this.LogEntry := ""
For wArr in this.WordArr {
this.LogEntry .= wArr
}
; Format the log entry with nicer symbols
this.LogEntry := StrReplace(this.LogEntry, "]Space]", "] - ]")
this.LogEntry := StrReplace(this.LogEntry, "]Backspace]", "] < ]")
this.LogEntry := StrReplace(this.LogEntry, "[Space[", "[ - [")
this.LogEntry := StrReplace(this.LogEntry, "[Backspace[", "[ < [")
; Remove doubles
this.LogEntry := StrReplace(this.LogEntry, "[[", "[")
this.LogEntry := StrReplace(this.LogEntry, "]]", "]")
this.LogEntry := StrReplace(this.LogEntry, " ", " ")
; Create the log entry with date stamp
dateStamp := "`n" A_YYYY "-" A_MM "-" A_DD
; Use the global lastTrigger variable
global lastTrigger
formattedTrigger := StrReplace(lastTrigger, "`n", "``n") ; Fix multiline triggers
; Add tabs based on length to align columns
tabs := StrLen(formattedTrigger) > 14 ? "`t" : "`t`t"
; Write to the log file
FileAppend(dateStamp " << " formattedTrigger tabs "---> " this.LogEntry, Config.ErrContextLog)
; Play sound notification if enabled
If (beepOnContexLog = 1)
SoundBeep(600, 200), SoundBeep(400, 200)
; Reset state
this.LogEntry := ""
this.waitingForExtra := false
this.extraWordsCount := 0
this.WordArr := []
; Clear the timeout timer
SetTimer(this.timeoutTimer, 0)
}
; The main logger loop
static LoggerLoop() {
try {
Loop {
try {
this.bsih.Start()
this.bsih.Wait()
; Skip digit-only input
If RegExMatch(this.bsih.Input, "\d")
continue
If (this.waitingForExtra) {
If (this.bsih.EndKey = "Space") {
this.extraWordsCount++
this.WordArr.Push(this.bsih.Input "]" this.bsih.EndKey "]")
; Log when we have enough following words
If (this.extraWordsCount > followingWordCount) {
this.LogContent()
}
}
}
else {
this.WordArr.Push(this.bsih.Input "[" this.bsih.EndKey "[")
; Maintain limited history
If (this.WordArr.Length > precedingWordCount)
this.WordArr.RemoveAt(1)
; Check if this is a backspace within a recent autocorrection
If (this.bsih.EndKey = "Backspace" && IsRecent = 1) {
this.waitingForExtra := true
; Set timeout to log after 8 seconds even if not enough words
this.timeoutTimer := ObjBindMethod(this, "LogContent")
SetTimer(this.timeoutTimer, -8000)
}
}
}
catch as e {
; If there's an error, log it and continue
FileAppend("Error in BackspaceContextLogger loop: " e.Message "`n", A_ScriptDir "\error_log.txt")
Sleep(100)
}
}
}
catch as e {
; Log outer error and mark as not running so we can restart if needed
FileAppend("Critical error in BackspaceContextLogger: " e.Message "`n", A_ScriptDir "\error_log.txt")
this.isRunning := false
}
}
}
;================================================================================================
/* InputBuffer Class by Descolada https://www.autohotkey.com/boards/viewtopic.php?f=83&t=122865
* Note: The mouse-relevant parts were removed by kunkel321, via ChatGPT4.
* InputBuffer can be used to buffer user input for keyboard, mouse, or both at once.
* The default InputBuffer (via the main class name) is keyboard only, but new instances
* can be created via InputBuffer().
*
* InputBuffer(keybd := true, mouse := false, timeout := 0)
* Creates a new InputBuffer instance. If keybd/mouse arguments are numeric then the default
* InputHook settings are used, and if they are a string then they are used as the Option
* arguments for InputHook and HotKey functions. Timeout can optionally be provided to call
* InputBuffer.Stop() automatically after the specified amount of milliseconds (as a failsafe).
*
* InputBuffer.Start() => initiates capturing input
* InputBuffer.Release() => releases buffered input and continues capturing input
* InputBuffer.Stop(release := true) => releases buffered input and then stops capturing input
* InputBuffer.ActiveCount => current number of Start() calls
* Capturing will stop only when this falls to 0 (Stop() decrements it by 1)
* InputBuffer.SendLevel => SendLevel of the InputHook
* InputBuffers default capturing SendLevel is A_SendLevel+2,
* and key release SendLevel is A_SendLevel+1.
* InputBuffer.IsReleasing => whether Release() is currently in action
* InputBuffer.Buffer => current buffered input in an array
*/
;================================================================================================
class InputBuffer {
Buffer := []
SendLevel := A_SendLevel + 2
ActiveCount := 0
IsReleasing := 0
static __New() => this.DefineProp("Default", {value:InputBuffer()})
static __Get(Name, Params) => this.Default.%Name%
static __Set(Name, Params, Value) => this.Default.%Name% := Value
static __Call(Name, Params) => this.Default.%Name%(Params*)
__New(keybd := true, timeout := 0) {
if !keybd
throw Error("Keyboard input type must be specified")
this.Timeout := timeout
this.Keybd := keybd
if keybd {
if keybd is String {
if RegExMatch(keybd, "i)I *(\d+)", &lvl)
this.SendLevel := Integer(lvl[1])
}
this.InputHook := InputHook(keybd is String ? keybd : "I" (this.SendLevel) " L0 *")
this.InputHook.NotifyNonText := true
this.InputHook.VisibleNonText := false
this.InputHook.OnKeyDown := this.BufferKey.Bind(this,,,, "Down")
this.InputHook.OnKeyUp := this.BufferKey.Bind(this,,,, "Up")
this.InputHook.KeyOpt("{All}", "N S")
}
this.HotIfIsActive := this.GetActiveCount.Bind(this)
}
BufferKey(ih, VK, SC, UD) => (this.Buffer.Push(Format("{{1} {2}}", GetKeyName(Format("vk{:x}sc{:x}", VK, SC)), UD)))
Start() {
this.ActiveCount += 1
SetTimer(this.Stop.Bind(this), -this.Timeout)
if this.ActiveCount > 1
return
this.Buffer := []
if this.Keybd
this.InputHook.Start()
}
Release() {
if this.IsReleasing
return []
sent := []
this.IsReleasing := 1
; Try to send all keystrokes, then check if any more were added
PrevSendLevel := A_SendLevel
SendLevel this.SendLevel - 1
while this.Buffer.Length {
key := this.Buffer.RemoveAt(1)
sent.Push(key)
Send(key)
}
SendLevel PrevSendLevel
this.IsReleasing := 0
return sent
}
Stop(release := true) {
if !this.ActiveCount
return
sent := release ? this.Release() : []
if --this.ActiveCount
return
if this.Keybd
this.InputHook.Stop()
return sent
}
GetActiveCount(HotkeyName) => this.ActiveCount
}
;===============================================================================
; Reports information about hotstrings or displays the last used trigger
; Can be called with:
; - "Button" (default): Shows statistics about hotstring usage
; - "lastTrigger": Shows the last used trigger
;===============================================================================
!+F3::StringAndFixReport("lastTrigger") ; Alt+Shift+F3: Show last used trigger
^F3::StringAndFixReport() ; Ctrl+F3: Show statistics report
StringAndFixReport(caller := "Button") {
; Handle different cases based on caller parameter
if (caller = "lastTrigger") {
if (EnableLogging = 0) {
thisMessage := "*Logging is currently disabled*`nEnable logging by setting`nEnableLogging := 1`nin AutoCorrectSystem.ahk"
} else {
thisMessage := "Last logged trigger:`n`n" lastTrigger
}
buttPos := ""
windowTitle := "Last Triggered Hotstring"
}
else {
; Generate hotstring statistics report
try {
HsLibContents := FileRead(Config.HotstringLibrary)
thisOptions := "", regulars := 0, begins := 0, middles := 0, ends := 0, fixes := 0, entries := 0, freq := 0
Loop Parse HsLibContents, '`n' {
If SubStr(Trim(A_LoopField),1,1) != ':'
continue
entries++
thisOptions := SubStr(Trim(A_LoopField), 1, InStr(A_LoopField, ':',,,2)) ; get options part of hotstring
If InStr(thisOptions, '*') and InStr(thisOptions, '?')
middles++
Else If InStr(thisOptions, '*')
begins++
Else If InStr(thisOptions, '?')
ends++
Else
regulars++
If RegExMatch(A_LoopField, 'Fixes\h*\K\d+', &fn) ; Extract fix count from comments
fixes += fn[]
; Extract web frequency value from comments
If RegExMatch(A_LoopField, 'Web Freq\h*\K[\d\.]+', &wf)
freq += wf[]
}
; Format numbers with commas
numberFormat(num) {
formattedNum := num
Loop 5 { ; '5' to prevent endless loop
oldNum := formattedNum
formattedNum := RegExReplace(formattedNum, "(\d)(\d{3}(\,|$))", "$1,$2")
if (formattedNum == oldNum) ; If no more changes, exit the loop
break
}
return formattedNum
}
thisMessage := (
'The ' Config.HotstringLibrary ' component of`n'
Config.ScriptName ' contains the following '
'`n Autocorrect hotstring stats.'
'`n================================'
'`n Regular Autocorrects:`t' numberFormat(regulars)
'`n Word Beginnings:`t`t' numberFormat(begins)
'`n Word Middles:`t`t' numberFormat(middles)
'`n Word Ends:`t`t' numberFormat(ends)
'`n================================'
'`n Total Entries:`t`t' numberFormat(entries)
'`n Potential Fixes:`t`t' numberFormat(fixes)
'`n Web Freq Billions:`t`t' numberFormat(Round(freq/1000,2))
)
}
catch Error as err {
thisMessage := "Could not read hotstring library`n" err.Message
LogError("StringAndFixReport: " err.Message)
}
buttPos := "x90"
windowTitle := "Hotstring Statistics"
}
; Create the GUI report window
fixRepGui := Gui(, windowTitle)
fixRepGui.SetFont(Config.FontColor)
fixRepGui.SetFont(Config.LargeFontSize)
fixRepGui.BackColor := Config.FormColor
; Use an Edit control for selectable text, styled as a label
editBackgroundOption := "Background" Config.FormColor
fixRepGui.Add('Edit', '-VScroll ReadOnly -E0x200 -WantReturn -TabStop ' editBackgroundOption, thisMessage)
; Add control buttons
closeBtn := fixRepGui.AddButton(buttPos, "Close")
fixRepGui.AddButton("x+8", "Copy Text").OnEvent("Click", (*) => A_Clipboard := thisMessage)
; Show the GUI
fixRepGui.Show("x" A_ScreenWidth/10) ; Appear to the left side
; Set up event handlers
closeBtn.Focus() ; Move focus to the close button to avoid highlighting text
closeBtn.OnEvent("click", (*) => fixRepGui.Destroy())
fixRepGui.OnEvent("Escape", (*) => fixRepGui.Destroy())
return fixRepGui ; Return the GUI object for reference
}
; ==============================================================================
; AUto-COrrect TWo COnsecutive CApitals
; This version by forum user Ntepa. Updated 8-7-2023.
; https://www.autohotkey.com/boards/viewtopic.php?p=533067#p533067
; Minor edits added by kunkel321 2-7-2024
fix_consecutive_caps()
fix_consecutive_caps() {
; Hotstring only works if CapsLock is off.
HotIf (*) => !GetKeyState("CapsLock", "T")
loop 26 {
char1 := Chr(A_Index + 64)
loop 26 {
char2 := Chr(A_Index + 64)
; Create hotstring for every possible combination of two letter capital letters.
;If (char1 char2 != "CO")
Hotstring(":*?CXB0Z:" char1 char2, fix.Bind(char1, char2))
}
}
HotIf
; Third letter is checked using InputHook.
fix(char1, char2, *) {
;ih := InputHook("V I101 L1")
ih := InputHook("V I101 L1 T.3")
ih.OnEnd := OnEnd
ih.Start()
OnEnd(ih) {
char3 := ih.Input
if (char3 ~= "[A-Z]") ; If char is UPPERcase alpha.
Hotstring "Reset"
else if (char3 ~= "[a-z]") ; If char is lowercase alpha.
|| (char3 = A_Space && char1 char2 ~= "OF|TO|IN|IT|IS|AS|AT|WE|HE|BY|ON|BE|NO") ; <--- Remove this line to prevent correction of those 2-letter words.
{ SendInput("{BS 2}" StrLower(char2) char3)
SoundBeep(800, 80) ; Case fix announcent.
}
}
}
}
; Extra thing not really related to AutoCorrect2.
;##################### WINDOW MOVER ##########################
^!Lbutton:: ; Ctrl+Alt+Left Mouse Click to drag a window
{
SetWinDelay(-1) ; Sets time between moves. -1 = no time
CoordMode("Mouse", "Screen")
WinGetPos(&BwX, &BwY, , , "A") ; Begin window X Y coord.
WinRestore("A") ; Unmaximizes window.
MouseGetPos(&BmX, &BmY) ; Begin mouse X Y coord
while GetKeyState("Lbutton", "P") ; While left mouse button is held down.
{ MouseGetPos(&CmX, &CmY) ; Keep getting current mouse X Y
WinMove((BwX+CmX-BmX), (BwY+CmY-BmY), , , "A")
}
SetWinDelay 100
CoordMode("Mouse", "Window") ; Put back, because window is mostly the default.
Return
}