Skip to content

Commit bfddac1

Browse files
committed
Preserve Windows key shortcuts
1 parent 1b85064 commit bfddac1

4 files changed

Lines changed: 70 additions & 35 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Primary goals:
2323
risk or complexity.
2424
- Preserve the low-level keyboard hook behavior in `Program.cs`.
2525
- Do not send `Win+Space` as the switching implementation.
26+
- Windows-key shortcuts such as `Win+L` and `Win+Shift+S` must always pass
27+
through untouched.
2628
- Prefer Win32/UI Automation APIs over simulated key chords for language
2729
switching, caret detection, and state management.
2830
- Keyboard-triggered indicators should anchor to the text insertion caret when

CapsLang.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
<Authors>NakornCode</Authors>
1515
<Company>NakornCode</Company>
1616
<Copyright>Copyright © NakornCode</Copyright>
17-
<Version>0.1.1</Version>
18-
<FileVersion>0.1.1.0</FileVersion>
19-
<AssemblyVersion>0.1.1.0</AssemblyVersion>
17+
<Version>0.1.2</Version>
18+
<FileVersion>0.1.2.0</FileVersion>
19+
<AssemblyVersion>0.1.2.0</AssemblyVersion>
2020
</PropertyGroup>
2121
<ItemGroup>
2222
<PackageReference Include="Interop.UIAutomationClient" Version="10.19041.0" />

Program.cs

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ internal static class Program
2121
private const int VK_CAPITAL = 0x14;
2222
private const int VK_SHIFT = 0x10;
2323
private const int VK_CONTROL = 0x11;
24+
private const int VK_LWIN = 0x5B;
25+
private const int VK_RWIN = 0x5C;
2426
private const int WM_INPUTLANGCHANGEREQUEST = 0x0050;
2527
private const int INPUTLANGCHANGE_FORWARD = 0x0002;
28+
private const int LLKHF_INJECTED = 0x10;
2629
private static readonly IntPtr HKL_NEXT = new(1);
2730

2831
private static IntPtr hookId = IntPtr.Zero;
@@ -167,43 +170,51 @@ private static IntPtr SetHook(LowLevelKeyboardProc proc)
167170

168171
private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
169172
{
173+
if (nCode < 0 || IsWindowsKeyDown())
174+
{
175+
return CallNextHookEx(hookId, nCode, wParam, lParam);
176+
}
177+
170178
if (!appSettings.IsCapsLangEnabled)
171179
{
172180
return CallNextHookEx(hookId, nCode, wParam, lParam);
173181
}
174182

175-
if (nCode >= 0)
183+
var message = wParam.ToInt32();
184+
var keyboardEvent = Marshal.PtrToStructure<KBDLLHOOKSTRUCT>(lParam);
185+
var vkCode = keyboardEvent.vkCode;
186+
187+
if (vkCode == VK_CAPITAL)
176188
{
177-
var message = wParam.ToInt32();
178-
var vkCode = Marshal.ReadInt32(lParam);
189+
if ((keyboardEvent.flags & LLKHF_INJECTED) != 0)
190+
{
191+
return CallNextHookEx(hookId, nCode, wParam, lParam);
192+
}
179193

180-
if (vkCode == VK_CAPITAL)
194+
if (message is WM_KEYDOWN or WM_SYSKEYDOWN)
181195
{
182-
if (message is WM_KEYDOWN or WM_SYSKEYDOWN)
196+
if (IsKeyDown(VK_CONTROL))
183197
{
184-
if (IsKeyDown(VK_CONTROL))
185-
{
186-
ForceCapsLockOff();
187-
ShowIndicator("CAPS OFF", force: true);
188-
}
189-
else if (IsKeyDown(VK_SHIFT))
190-
{
191-
ToggleCapsLock();
192-
ShowIndicator(IsCapsLockOn() ? "CAPS ON" : "CAPS OFF", force: true);
193-
}
194-
else
195-
{
196-
ForceCapsLockOff();
197-
SwitchToNextInputLanguage();
198-
languagePopupTimer?.Stop();
199-
languagePopupTimer?.Start();
200-
}
198+
ForceCapsLockOff();
199+
ShowIndicator("CAPS OFF", force: true);
201200
}
202-
203-
if (message is WM_KEYDOWN or WM_KEYUP or WM_SYSKEYDOWN or WM_SYSKEYUP)
201+
else if (IsKeyDown(VK_SHIFT))
204202
{
205-
return new IntPtr(1);
203+
SetCapsLockState(!IsCapsLockOn());
204+
ShowIndicator(IsCapsLockOn() ? "CAPS ON" : "CAPS OFF", force: true);
206205
}
206+
else
207+
{
208+
ForceCapsLockOff();
209+
SwitchToNextInputLanguage();
210+
languagePopupTimer?.Stop();
211+
languagePopupTimer?.Start();
212+
}
213+
}
214+
215+
if (message is WM_KEYDOWN or WM_KEYUP or WM_SYSKEYDOWN or WM_SYSKEYUP)
216+
{
217+
return new IntPtr(1);
207218
}
208219
}
209220

@@ -221,10 +232,7 @@ private static void SwitchToNextInputLanguage()
221232

222233
private static void ForceCapsLockOff()
223234
{
224-
if (IsCapsLockOn())
225-
{
226-
ToggleCapsLock();
227-
}
235+
SetCapsLockState(false);
228236
}
229237

230238
private static bool IsCapsLockOn()
@@ -237,7 +245,20 @@ private static bool IsKeyDown(int virtualKey)
237245
return (GetAsyncKeyState(virtualKey) & 0x8000) != 0;
238246
}
239247

240-
private static void ToggleCapsLock()
248+
private static bool IsWindowsKeyDown()
249+
{
250+
return IsKeyDown(VK_LWIN) || IsKeyDown(VK_RWIN);
251+
}
252+
253+
private static void SetCapsLockState(bool enabled)
254+
{
255+
if (IsCapsLockOn() != enabled)
256+
{
257+
SetKeyboardStateCapsLock(enabled);
258+
}
259+
}
260+
261+
private static void SetKeyboardStateCapsLock(bool enabled)
241262
{
242263
keybd_event(VK_CAPITAL, 0x45, KEYEVENTF_EXTENDEDKEY, UIntPtr.Zero);
243264
keybd_event(VK_CAPITAL, 0x45, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, UIntPtr.Zero);
@@ -507,6 +528,16 @@ private static Point GetPointerPopupAnchor()
507528

508529
private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
509530

531+
[StructLayout(LayoutKind.Sequential)]
532+
private struct KBDLLHOOKSTRUCT
533+
{
534+
public int vkCode;
535+
public int scanCode;
536+
public int flags;
537+
public int time;
538+
public UIntPtr dwExtraInfo;
539+
}
540+
510541
[StructLayout(LayoutKind.Sequential)]
511542
private struct GUITHREADINFO
512543
{
@@ -550,10 +581,10 @@ private struct RECT
550581
private static extern short GetAsyncKeyState(int vKey);
551582

552583
[DllImport("user32.dll")]
553-
private static extern void keybd_event(int bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
584+
private static extern IntPtr GetForegroundWindow();
554585

555586
[DllImport("user32.dll")]
556-
private static extern IntPtr GetForegroundWindow();
587+
private static extern void keybd_event(int bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
557588

558589
[DllImport("user32.dll")]
559590
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ shortcuts such as `Win+Space+1` or `Win+Space+D`.
1212

1313
- Use `CapsLock` to switch to the next Windows input language.
1414
- Keep real CapsLock off during normal typing.
15+
- Leave normal Windows-key shortcuts such as `Win+L` and `Win+Shift+S`
16+
untouched.
1517
- Show the active Windows input language code near the text insertion caret
1618
when the active app exposes caret geometry through Windows UI Automation.
1719
- Avoid mouse-pointer based typing feedback.

0 commit comments

Comments
 (0)