diff --git a/Program.cs b/Program.cs index 9d44a5c..150d7eb 100644 --- a/Program.cs +++ b/Program.cs @@ -1,27 +1,66 @@ -using System.Diagnostics; +// FuckCapsLock — Permanently disable the vestigial Caps Lock key. +// Copyright (c) 2026 Llama Chile Shop. MIT License. +// +// This application was pair-programmed by: +// gramps@llamachile.shop (senior developer, direct supervision) +// opencode (AI pair programmer) +// +// Under direct, senior supervision, paired programming can be an +// effective and efficient tool. +// +// Installs a low-level keyboard hook (WH_KEYBOARD_LL) that swallows +// VK_CAPITAL (0x14) before it reaches any window. Runs in the system +// tray until the user exits via the context menu. +// +// System calls reference: +// SetWindowsHookEx / UnhookWindowsHookEx / CallNextHookEx — user32.dll +// GetModuleHandle — kernel32.dll +// Registry.CurrentUser\...\Run — auto-start on boot + +using System.Diagnostics; using System.Runtime.InteropServices; using Microsoft.Win32; static class Program { + /// Low-level keyboard hook identifier — intercepts keys before any window. const int WH_KEYBOARD_LL = 13; + + /// Virtual-key code for Caps Lock — the only key this app blocks. const int VK_CAPITAL = 0x14; + /// Callback signature for LowLevelKeyboardProc used by SetWindowsHookEx. + /// Hook code — >= 0 means the hook should process the message. + /// Message type (WM_KEYDOWN, WM_KEYUP, etc.). + /// Pointer to a KBDLLHOOKSTRUCT with the raw key data. + /// 1 to swallow the key, or CallNextHookEx to let it through. delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam); + /// Installs a low-level keyboard hook into the global hook chain. + /// Hook type — WH_KEYBOARD_LL (13) for keyboard events. + /// Pointer to the callback function. + /// Module handle for the current process. + /// 0 = hook applies globally to all threads in this desktop. + /// Hook handle on success, IntPtr.Zero on failure. [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId); + /// Removes a previously installed hook from the hook chain. + /// Hook handle returned by SetWindowsHookEx. + /// True on success. [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] static extern bool UnhookWindowsHookEx(IntPtr hhk); + /// Passes the hook event to the next hook in the chain. [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); + /// Retrieves the module handle for the calling process — required by SetWindowsHookEx. [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] static extern IntPtr GetModuleHandle(string lpModuleName); + /// Structure deserialized from the lParam pointer in the hook callback. [StructLayout(LayoutKind.Sequential)] struct KBDLLHOOKSTRUCT { @@ -32,30 +71,44 @@ static class Program public IntPtr dwExtraInfo; } + /// Cached delegate — prevents garbage collection of the callback. static LowLevelKeyboardProc _proc = HookCallback; + + /// Active hook handle returned by SetWindowsHookEx. static IntPtr _hookId = IntPtr.Zero; + + /// System tray icon — visible while the app is running. static NotifyIcon? _trayIcon; + /// + /// Entry point. Registers auto-start, enforces a single instance via a + /// named mutex, installs the keyboard hook, and runs the WinForms message + /// loop. The app lives in the system tray until Exit is selected. + /// [STAThread] static void Main() { RegisterStartup(); + // Named mutex prevents multiple instances — second launch exits silently. using var mutex = new Mutex(true, @"Global\FuckCapsLock", out var createdNew); if (!createdNew) return; + // Install the low-level hook; bail if it fails (e.g., insufficient permissions). _hookId = SetHook(_proc); if (_hookId == IntPtr.Zero) return; ApplicationConfiguration.Initialize(); + // Hidden window — required to pump messages and keep the hook alive. using var form = new Form(); form.WindowState = FormWindowState.Minimized; form.ShowInTaskbar = false; form.Load += (_, _) => form.Hide(); + // Tray icon with Exit context menu to cleanly unhook and quit. _trayIcon = new NotifyIcon { Icon = SystemIcons.Application, @@ -69,11 +122,18 @@ static class Program }); _trayIcon.Visible = true; + // Blocks until the form closes (Exit is clicked or process is killed). Application.Run(form); + // Remove the hook when the app exits. UnhookWindowsHookEx(_hookId); } + /// + /// Writes the current executable path to HKCU\Software\Microsoft\Windows\ + /// CurrentVersion\Run so the app auto-starts on login. Silently ignores + /// errors (no admin rights needed for HKCU). + /// static void RegisterStartup() { try @@ -85,8 +145,10 @@ static class Program var path = Environment.ProcessPath; if (path is null) return; + // Quote the path if it contains spaces (standard Windows practice). var value = path.Contains(' ') ? $"\"{path}\"" : path; + // Only write if the path has changed — avoids unnecessary registry churn. var existing = key.GetValue("FuckCapsLock") as string; if (!string.Equals(existing, value, StringComparison.OrdinalIgnoreCase)) key.SetValue("FuckCapsLock", value); @@ -94,6 +156,11 @@ static class Program catch { } } + /// + /// Installs the low-level keyboard hook using SetWindowsHookEx. + /// + /// Delegate to the callback that processes each key event. + /// Hook handle or IntPtr.Zero if installation failed. static IntPtr SetHook(LowLevelKeyboardProc proc) { using var curProcess = Process.GetCurrentProcess(); @@ -102,13 +169,22 @@ static class Program GetModuleHandle(curModule!.ModuleName), 0); } + /// + /// Hook callback invoked for every keyboard event. If the key is VK_CAPITAL (Caps Lock), + /// the event is swallowed by returning 1 without calling CallNextHookEx. All other keys + /// pass through normally. + /// + /// >= 0 means the hook should process the event. + /// Window message type (WM_KEYDOWN, WM_SYSKEYUP, etc.). + /// Pointer to a KBDLLHOOKSTRUCT with the raw key data. + /// 1 if Caps Lock was blocked; otherwise the result of CallNextHookEx. static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0) { var hookStruct = Marshal.PtrToStructure(lParam); if (hookStruct.vkCode == VK_CAPITAL) - return (IntPtr)1; + return (IntPtr)1; // Swallow the key — never forward it. } return CallNextHookEx(_hookId, nCode, wParam, lParam); }