Last active
September 22, 2026 02:09
-
-
Save tsmd/2f7bdc512f85505910df5a7b2bbe292d to your computer and use it in GitHub Desktop.
Alt tap-hold(左: 無変換 / 右: 変換)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #Requires AutoHotkey v2.0 | |
| #SingleInstance Force | |
| ; 左 Alt の単独タップ = 無変換 / 右 Alt の単独タップ = 変換 | |
| ; 100 ms 以上、または他のキーとの同時押し = 通常の Alt | |
| ; 日本語キーボード配列向け。右 Alt を AltGr として使う配列は対象外。 | |
| ; | |
| ; AHK の Alt ホットキー / InputHook / Send は使用しない。 | |
| ; WH_KEYBOARD_LL で物理 Alt を抑止し、Win32 SendInput で送信する。 | |
| ; Alt+Tab も Alt down と最初のキー down を同じ配列で送る。 | |
| ; Esc やメニュー解除用のダミーキーは送らない。 | |
| ; | |
| ; 通常終了・再読み込み時には送信済み Alt を解放する。 | |
| ; 強制終了では終了処理が走らない。復旧は左右 Alt を押して離す。 | |
| ; 管理者権限のアプリが対象なら、このスクリプトも同じ権限で実行する。 | |
| ; 常駐開始前に全キーを離すこと。他の Alt リマップとは併用しない。 | |
| ; exe 化すると A_LineFile は実行ファイルのパスと異なるため別途判定する。 | |
| if A_IsCompiled || A_LineFile = A_ScriptFullPath { | |
| if A_Args.Length && A_Args[1] = "--check" | |
| ExitApp 0 | |
| AltTapHold.Start(100) ; タップ判定時間(ミリ秒) | |
| } | |
| ; Windows に触れない状態機械。events は [VK, scan code, KEYEVENTF flags]。 | |
| class AltTapState { | |
| __New(tapMs := 100) { | |
| this.tapMs := tapMs | |
| this.alts := Map(0xA4, {down: false, sent: false, at: 0}, | |
| 0xA5, {down: false, sent: false, at: 0}) | |
| this.held := Map() | |
| } | |
| AltEvent(vk, up := false) { | |
| return [vk, 0x38, (vk = 0xA5 ? 1 : 0) | (up ? 2 : 0)] | |
| } | |
| Promote(events) { | |
| for vk, state in this.alts { | |
| if state.down && !state.sent { | |
| ; DllCall による再入より前に状態を確定する。 | |
| state.sent := true | |
| events.Push(this.AltEvent(vk)) | |
| } | |
| } | |
| } | |
| Key(vk, sc, flags, now) { | |
| events := [] | |
| up := !!(flags & 0x80) | |
| if this.alts.Has(vk) { | |
| state := this.alts[vk] | |
| if !up { | |
| if !state.down { ; 自動リピートで開始時刻を更新しない。 | |
| state.down := true | |
| state.at := now | |
| other := this.alts[vk = 0xA4 ? 0xA5 : 0xA4] | |
| if other.down || this.held.Count | |
| this.Promote(events) | |
| } | |
| } else if state.down { | |
| if state.sent { | |
| events.Push(this.AltEvent(vk, true)) | |
| } else if ((now - state.at) & 0xFFFFFFFF) < this.tapMs { | |
| tapVk := vk = 0xA4 ? 0x1D : 0x1C | |
| events.Push([tapVk, 0, 0], [tapVk, 0, 2]) | |
| } else { | |
| ; タイマーの実行が遅れていても、長押しをタップにしない。 | |
| events.Push(this.AltEvent(vk), this.AltEvent(vk, true)) | |
| } | |
| state.down := false | |
| state.sent := false | |
| } else { | |
| ; 対応する down を記録していない up は Windows に通す。 | |
| return {block: false, events: events} | |
| } | |
| return {block: true, events: events} | |
| } | |
| if up { | |
| if this.held.Has(vk) | |
| this.held.Delete(vk) | |
| } else { | |
| this.held[vk] := true | |
| this.Promote(events) | |
| } | |
| if events.Length { | |
| ; 最初のキーだけ抑止し、Alt down の後に一度だけ再送する。 | |
| ; VK と拡張フラグを保持(Pause 等を含め SC のみへ変換しない)。 | |
| events.Push([vk, sc, flags & 1]) | |
| return {block: true, events: events} | |
| } | |
| return {block: false, events: events} | |
| } | |
| Tick(now) { | |
| events := [] | |
| for vk, state in this.alts { | |
| if state.down && !state.sent | |
| && ((now - state.at) & 0xFFFFFFFF) >= this.tapMs { | |
| state.sent := true | |
| events.Push(this.AltEvent(vk)) | |
| } | |
| } | |
| return events | |
| } | |
| Release() { | |
| events := [] | |
| for vk, state in this.alts { | |
| if state.sent | |
| events.Push(this.AltEvent(vk, true)) | |
| state.down := false | |
| state.sent := false | |
| } | |
| this.held.Clear() | |
| return events | |
| } | |
| } | |
| class AltTapHold { | |
| static hook := 0 | |
| static callback := 0 | |
| static failed := false | |
| static marker := 0x41544832 | |
| static sessionRegistered := false | |
| static sessionLocked := false | |
| static resumeUps := [] | |
| static Start(tapMs) { | |
| this.state := AltTapState(tapMs) | |
| OnExit(ObjBindMethod(this, "Stop")) | |
| this.sessionCallback := ObjBindMethod(this, "SessionChange") | |
| OnMessage(0x02B1, this.sessionCallback) ; WM_WTSSESSION_CHANGE | |
| this.sessionRegistered := DllCall("Wtsapi32\WTSRegisterSessionNotification", | |
| "Ptr", A_ScriptHwnd, "UInt", 0) ; このセッションのみ | |
| if !this.sessionRegistered | |
| throw OSError(A_LastError, "WTSRegisterSessionNotification") | |
| ; 通常モードの callback と Critical でタイマーとの割り込みを防ぐ。 | |
| this.callback := CallbackCreate(ObjBindMethod(this, "KeyboardProc"), , 3) | |
| this.hook := DllCall("SetWindowsHookExW", "Int", 13, | |
| "Ptr", this.callback, "Ptr", DllCall("GetModuleHandleW", "Ptr", 0, "Ptr"), | |
| "UInt", 0, "Ptr") | |
| if !this.hook | |
| throw OSError(A_LastError, "SetWindowsHookExW") | |
| A_IconTip := "Alt tap-hold(左: 無変換 / 右: 変換)" | |
| SetTimer(ObjBindMethod(this, "Tick"), 10) | |
| Persistent true | |
| } | |
| static SessionChange(event, *) { | |
| Critical 1000 | |
| if event != 7 && event != 8 ; WTS_SESSION_LOCK / UNLOCK | |
| return | |
| this.sessionLocked := event = 7 | |
| ; ロック画面ではキー up を受信できない。両境界で記録を捨てる。 | |
| ; 送信済み Alt の解放は通常デスクトップに戻ってから行う。 | |
| this.resumeUps.Push(this.state.Release()*) | |
| } | |
| static FlushResumeUps() { | |
| if !this.resumeUps.Length | |
| return true | |
| ; 解除通知直後はまだ送信できないことがある。失敗時は次回に再試行。 | |
| ; up だけなので、部分送信後に同じ配列を再送してもよい。 | |
| if this.NativeSend(this.resumeUps) != this.resumeUps.Length | |
| return false | |
| this.resumeUps := [] | |
| return true | |
| } | |
| static Next(code, msg, data) { | |
| return DllCall("CallNextHookEx", "Ptr", 0, "Int", code, | |
| "UPtr", msg, "Ptr", data, "Ptr") | |
| } | |
| static KeyboardProc(code, msg, data) { | |
| Critical 1000 | |
| code := code << 32 >> 32 | |
| if code < 0 || this.failed || this.sessionLocked | |
| return this.Next(code, msg, data) | |
| flags := NumGet(data, 8, "UInt") | |
| ; 自分や別プログラムが生成した入力を物理入力として数えない。 | |
| if (flags & 0x10) || NumGet(data, 16, "UPtr") = this.marker | |
| return this.Next(code, msg, data) | |
| try { | |
| if !this.FlushResumeUps() | |
| return this.Next(code, msg, data) | |
| result := this.state.Key(NumGet(data, 0, "UInt"), | |
| NumGet(data, 4, "UInt"), flags, NumGet(data, 12, "UInt")) | |
| this.Emit(result.events) | |
| if result.block | |
| return 1 | |
| } catch { | |
| this.Fail() | |
| } | |
| return this.Next(code, msg, data) | |
| } | |
| static Tick() { | |
| Critical 1000 | |
| if this.failed || this.sessionLocked | |
| return | |
| try { | |
| if this.FlushResumeUps() | |
| this.Emit(this.state.Tick(DllCall("GetTickCount", "UInt"))) | |
| } catch | |
| this.Fail() | |
| } | |
| static Pack(events) { | |
| size := A_PtrSize = 8 ? 40 : 28 | |
| base := A_PtrSize = 8 ? 8 : 4 | |
| extra := A_PtrSize = 8 ? 16 : 12 | |
| inputBuf := Buffer(events.Length * size, 0) | |
| for i, event in events { | |
| offset := (i - 1) * size | |
| NumPut("UInt", 1, inputBuf, offset) ; INPUT_KEYBOARD | |
| NumPut("UShort", event[1], "UShort", event[2], | |
| "UInt", event[3], inputBuf, offset + base) | |
| NumPut("UPtr", this.marker, inputBuf, offset + base + extra) | |
| } | |
| return inputBuf | |
| } | |
| static NativeSend(events) { | |
| inputBuf := this.Pack(events) | |
| return DllCall("SendInput", "UInt", events.Length, "Ptr", inputBuf, | |
| "Int", A_PtrSize = 8 ? 40 : 28, "UInt") | |
| } | |
| static Emit(events) { | |
| if !events.Length | |
| return | |
| if this.NativeSend(events) != events.Length { | |
| ; 部分送信もあり得るので両 Alt の up を復旧時に送る。 | |
| this.failed := true | |
| throw Error("Windows がキー入力を受け付けませんでした。") | |
| } | |
| } | |
| static Fail() { | |
| this.failed := true | |
| ; フック内ではダイアログも待機処理も実行しない。 | |
| SetTimer(ObjBindMethod(this, "ReportFailure"), -1) | |
| } | |
| static ReportFailure() { | |
| this.Stop() | |
| MsgBox "キー入力の送信に失敗したため終了します。`n" | |
| . "対象アプリとスクリプトの実行権限を確認してください。`n" | |
| . "Alt が残っている場合は、左右 Alt を一度押して離してください。", | |
| "Alt tap-hold", "Icon!" | |
| ExitApp 1 | |
| } | |
| static Stop(*) { | |
| Critical 1000 | |
| if this.sessionRegistered { | |
| DllCall("Wtsapi32\WTSUnRegisterSessionNotification", "Ptr", A_ScriptHwnd) | |
| this.sessionRegistered := false | |
| } | |
| if this.HasOwnProp("sessionCallback") | |
| OnMessage(0x02B1, this.sessionCallback, 0) | |
| if this.hook { | |
| DllCall("UnhookWindowsHookEx", "Ptr", this.hook) | |
| this.hook := 0 | |
| } | |
| events := this.state.Release() | |
| events.Push(this.resumeUps*) | |
| this.resumeUps := [] | |
| if this.failed | |
| events := [this.state.AltEvent(0xA4, true), this.state.AltEvent(0xA5, true)] | |
| try this.Emit(events) | |
| ; callback のメモリはプロセス終了時に OS が解放する。 | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment