Skip to content

Instantly share code, notes, and snippets.

@tsmd
Last active September 22, 2026 02:09
Show Gist options
  • Select an option

  • Save tsmd/2f7bdc512f85505910df5a7b2bbe292d to your computer and use it in GitHub Desktop.

Select an option

Save tsmd/2f7bdc512f85505910df5a7b2bbe292d to your computer and use it in GitHub Desktop.
Alt tap-hold(左: 無変換 / 右: 変換)
#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