Skip to content

Instantly share code, notes, and snippets.

@githusr
Forked from sedm0784/CapsLockCtrlEscape.ahk
Last active January 16, 2026 15:23
Show Gist options
  • Select an option

  • Save githusr/7355e6038cd2af4b234fa9a9002f2c71 to your computer and use it in GitHub Desktop.

Select an option

Save githusr/7355e6038cd2af4b234fa9a9002f2c71 to your computer and use it in GitHub Desktop.
AutoHotkey script to map Caps Lock to Escape when it's pressed on its own and Ctrl when used in combination with another key, à la Steve Losh. Adapted from one that does something similar with the Ctrl Key on the Vim Tips Wiki (http://vim.wikia.com/wiki/Map_caps_lock_to_escape_in_Windows?oldid=32281). (Plus contribs from @randy909 & @mmikeww.)
#Requires AutoHotkey 2.0.19 64-bit
#SingleInstance Force
; #NoTrayIcon
ProcessSetPriority "High"
LShift & RShift:: SetCapsLockState !GetKeyState("CapsLock", "T")
*CapsLock:: {
Send "{LCtrl down}"
LastCtrlKeyDownTime := A_TickCount
KeyWait "CapsLock"
Send "{LCtrl up}"
if A_PriorKey = "CapsLock" && A_TickCount - LastCtrlKeyDownTime <= 250 {
Send "{Esc}"
}
}
<!q::!F4
<!b::^b
<!f::^f
<!h::^h
<!n::^n
<!p::^p
*<^b:: Send "{Blind^}{Left}"
*<^f:: Send "{Blind^}{Right}"
*<^h:: Send "{Blind^}{BS}"
*<^n:: Send "{Blind^}{Down}"
*<^p:: Send "{Blind^}{Up}"
#HotIf WinActive("ahk_exe msedge.exe")
<^q::^Tab
<^Tab::^q
#HotIf
#e:: {
if WinExist(" - File Explorer") {
WinActivate
} else {
Run "explorer"
}
}
(defcfg
process-unmapped-keys (all-except ralt)
log-layer-changes false
concurrent-tap-hold true
notify-cfg-reload false
notify-cfg-reload-silent true
)
(defsrc
;; mlft
mrgt mmid mbck mfwd
mwu mwd mwl mwr
)
(deftemplate emacs (src dst)
$src (switch
(lctl) (unmod (lctl) $dst) break
(lalt) (multi rctl (macro (unmod (lalt) $src))) break
() _ break
)
)
(deflayermap base
caps (tap-hold-press 200 200 esc lctl)
rsft (fork _ caps (lsft))
(t! emacs p up)
(t! emacs n down)
(t! emacs b lft)
(t! emacs f rght)
(t! emacs h bspc)
q (fork _ f4 (lalt))
s (fork _ (unmod f1) (lalt))
)
/*
cl /O2 /GL /EHsc /DUNICODE /D_UNICODE kanata_launcher.cpp `
/link /SUBSYSTEM:WINDOWS /LTCG /OPT:REF /OPT:ICF /INCREMENTAL:NO `
/OUT:kanata-launcher.exe; `
if ($LASTEXITCODE -eq 0) { Remove-Item -Force kanata_launcher.obj }
Usage:
Put kanata-launcher.exe in the SAME directory as:
- kanata.exe (non-GUI/console build)
- kanata.kbd (your config)
Run kanata-launcher.exe (e.g., via Task Scheduler “At log on”).
*/
#include <wchar.h>
#include <windows.h>
static void GetSelfDir(wchar_t *dir, size_t cap) {
DWORD n = GetModuleFileNameW(nullptr, dir, (DWORD)cap);
if (n == 0 || n >= cap) {
dir[0] = L'.';
dir[1] = L'\0';
return;
}
wchar_t *p = wcsrchr(dir, L'\\');
if (p) *p = L'\0';
}
// FNV-1a 64-bit hash for a stable mutex name derived from the kanata.exe full
// path.
static unsigned long long HashPath64(const wchar_t *s) {
unsigned long long h = 14695981039346656037ull;
while (*s) {
h ^= (unsigned long long)(unsigned short)(*s++);
h *= 1099511628211ull;
}
return h;
}
static void BuildMutexName(const wchar_t *exePath, wchar_t *out, size_t cap) {
unsigned long long h = HashPath64(exePath);
swprintf_s(out, cap, L"Local\\kanata-launcher-%016llx", h);
}
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) {
wchar_t dir[MAX_PATH];
GetSelfDir(dir, _countof(dir));
// Absolute paths (same directory as launcher)
wchar_t exe[MAX_PATH];
wchar_t cfg[MAX_PATH];
swprintf_s(exe, _countof(exe), L"%s\\kanata.exe", dir);
swprintf_s(cfg, _countof(cfg), L"%s\\kanata.kbd", dir);
// Idempotency gate: if already running, do nothing.
HANDLE hGate = nullptr;
{
wchar_t mname[64];
BuildMutexName(exe, mname, _countof(mname));
hGate = CreateMutexW(nullptr, FALSE, mname);
if (hGate) {
if (GetLastError() == ERROR_ALREADY_EXISTS) {
CloseHandle(hGate);
return 0;
}
// Let kanata.exe inherit this handle, so the mutex persists after
// launcher exits.
SetHandleInformation(hGate, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
}
}
// Ensure predictable relative-path behavior (if kanata config references
// relative files).
SetCurrentDirectoryW(dir);
// Mutable command line buffer required by CreateProcessW.
// Include the quoted exe path as argv[0] to avoid edge-case argv parsing
// differences.
wchar_t cmd[2 * MAX_PATH + 64];
swprintf_s(cmd, _countof(cmd), L"\"%s\" --cfg \"%s\"", exe, cfg);
STARTUPINFOW si{};
si.cb = sizeof(si);
PROCESS_INFORMATION pi{};
const DWORD flags = CREATE_NO_WINDOW | DETACHED_PROCESS;
BOOL ok = CreateProcessW(
exe, // lpApplicationName (absolute path; avoids PATH searching)
cmd, // lpCommandLine (mutable)
nullptr, nullptr,
(hGate != nullptr) ? TRUE
: FALSE, // inherit gate handle if we created it
flags, nullptr,
dir, // lpCurrentDirectory
&si, &pi);
if (hGate) CloseHandle(hGate);
if (ok) {
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}
// Silent failure by design (no logs/popups as requested).
return 1;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment