0

I am using windows on my laptop. I also remapped caps lock to esc for smoother vim experience.
Now my caps lock light indicator is always off and I start wondering "Is it possible to use caps lock indicator for something else?". Something like blinking on request etc.

I never did something like that so I have very basic questions:
Can light indicator be separately turned on\off without actual caps lock functionality?

Stepan Loginov
  • 1,667
  • 4
  • 22
  • 49
  • 1
    Have you see this https://support.microsoft.com/en-us/topic/howto-toggle-the-num-lock-caps-lock-and-scroll-lock-keys-1718b9bd-5ebf-f3ab-c249-d5312c93d2d7 – Simon Mourier Jun 19 '22 at 19:22
  • @SimonMourier that toggles the actual state of the key, which is not what the OP wants. They just want to toggle the LED – Remy Lebeau Jun 19 '22 at 20:01
  • 3
    https://learn.microsoft.com/en-us/windows/win32/api/ntddkbd/ni-ntddkbd-ioctl_keyboard_set_indicators – Hans Passant Jun 19 '22 at 21:18

2 Answers2

2

Unless your keyboard has its own API to control the lights (gaming keyboards sometimes do), or you have a custom keyboard driver that can control the lights, then what you are asking for is generally not possible with most keyboards, as Windows simply has no API to control just a keyboard's lights without also updating its key states.

UPDATE:

Apparently, there is such an API, after all:

IOCTL_KEYBOARD_QUERY_INDICATORS IOCTL

The IOCTL_KEYBOARD_QUERY_INDICATORS request returns information about the keyboard indicators.

IOCTL_KEYBOARD_SET_INDICATORS IOCTL

The IOCTL_KEYBOARD_SET_INDICATORS request sets the keyboard indicators.

You can use DefineDosDevice() and CreateFile() to open a HANDLE to the desired keyboard, and then use DeviceIoControl() to query the keyboard for its current KEYBOARD_INDICATOR_PARAMETERS, and then update its LedFlags field to include or omit the KEYBOARD_CAPS_LOCK_ON flag to turn the CapLock key's LED light on/off as needed.

See Manipulating the Keyboard Lights in Windows NT for an example.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • It didn't work for me. Probably it works on older OS or something wrong with my keyboard. – Stepan Loginov Jun 20 '22 at 11:10
  • "*It didn't work for me*" - in what way, exactly? Where did it fail? – Remy Lebeau Jun 20 '22 at 16:05
  • I tried this example https://www.codeguru.com/windows/manipulating-the-keyboard-lights-in-windows-nt/ . It looks pretty legit for me. I drop files to new empty project in VS, I also change `KEYBOARD_SCROLL_LOCK_ON` to `KEYBOARD_CAPS_LOCK_ON` . No errors and no effect during run. I read at some random forum that this stop working on newer Windows's. But I have no trust in this info. I will try later when I got normal keyboard (not laptop) – Stepan Loginov Jun 21 '22 at 00:43
  • "*No errors and no effect during run*" - are you checking API function calls for error codes? – Remy Lebeau Jun 21 '22 at 01:04
0

[This is a duplicate of my answer here: https://stackoverflow.com/a/76222691/12975599]

Putting @Remy's answer into a working C# program, I got the code below. It simply flips the Caps Lock LED, of course you can customize it to do whatever.

The basic procedure is

  • obtain a handle for the keyboard device
  • read the current LED status
  • flip the bit corresponding to the Caps Lock LED <-- this is the place where you could do other random things
  • write the status back to the keyboard
  • clean up
// to compile: powershell> C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc /out:capsblink.exe /target:exe capsblink.cs
//
// Following:
//  - original motivation: https://stackoverflow.com/questions/72679665/is-it-possible-to-control-capslock-light-without-actual-capslocking/72679984#72679984
//  - partial answer using C-API: https://stackoverflow.com/questions/2248358/way-to-turn-on-keyboards-caps-lock-light-without-actually-turning-on-caps-lock
//  - full version of the partial answer from above (in C, 1999): https://www.codeguru.com/windows/manipulating-the-keyboard-lights-in-windows-nt/
//  - calling C-API from C# via p/invoke: https://learn.microsoft.com/en-us/archive/msdn-magazine/2003/july/net-column-calling-win32-dlls-in-csharp-with-p-invoke
//  - Win32 API docs: https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-deviceiocontrol (and related)

using System.Runtime.InteropServices;
using System.ComponentModel;
using System;

class CapsLockLight
{
    [DllImport("kernel32.dll", SetLastError=true)]
    public static extern Boolean DefineDosDevice(UInt32 flags, String deviceName, String targetPath);

    [DllImport("kernel32.dll", SetLastError=true)]
    public static extern IntPtr CreateFile(String fileName,
                       UInt32 desiredAccess, UInt32 shareMode, IntPtr securityAttributes,
                       UInt32 creationDisposition, UInt32 flagsAndAttributes, IntPtr templateFile
                      );

    [StructLayout(LayoutKind.Sequential)]
    public struct KEYBOARD_INDICATOR_PARAMETERS
    {
        public UInt16 unitID;
        public UInt16 LEDflags;
    }

    [DllImport("kernel32.dll", SetLastError=true)]
    public static extern Boolean DeviceIoControl(IntPtr device, UInt32 ioControlCode,
                          ref KEYBOARD_INDICATOR_PARAMETERS KIPin,  UInt32  inBufferSize,
                          ref KEYBOARD_INDICATOR_PARAMETERS KIPout, UInt32 outBufferSize,
                          ref UInt32 bytesReturned, IntPtr overlapped
                         );
    [DllImport("kernel32.dll", SetLastError=true)]
    public static extern Boolean DeviceIoControl(IntPtr device, UInt32 ioControlCode,
                          IntPtr KIPin,  UInt32  inBufferSize,
                          ref KEYBOARD_INDICATOR_PARAMETERS KIPout, UInt32 outBufferSize,
                          ref UInt32 bytesReturned, IntPtr overlapped
                         );
    [DllImport("kernel32.dll", SetLastError=true)]
    public static extern Boolean DeviceIoControl(IntPtr device, UInt32 ioControlCode,
                          ref KEYBOARD_INDICATOR_PARAMETERS KIPin,  UInt32  inBufferSize,
                          IntPtr KIPout, UInt32 outBufferSize,
                          ref UInt32 bytesReturned, IntPtr overlapped
                         );

    [DllImport("kernel32.dll", SetLastError=true)]
    public static extern Boolean CloseHandle(IntPtr handle);

    static void Main(string[] args)
    {
        UInt32 bytesReturned = 0;
        IntPtr device;
        KEYBOARD_INDICATOR_PARAMETERS KIPbuf = new KEYBOARD_INDICATOR_PARAMETERS { unitID = 0, LEDflags = 0 };

        if(!DefineDosDevice(Flags.DDD_RAW_TARGET_PATH, "myKBD", "\\Device\\KeyboardClass0"))
        {
            Int32 err = Marshal.GetLastWin32Error();
            throw new Win32Exception(err);
        }

        // Console.WriteLine("Created device");

        device = CreateFile("\\\\.\\myKBD", Flags.GENERIC_WRITE, 0, IntPtr.Zero, Flags.OPEN_EXISTING, 0, IntPtr.Zero);
        if(device == Flags.INVALID_HANDLE_VALUE)
        {
            Int32 err = Marshal.GetLastWin32Error();
            throw new Win32Exception(err);
        }

        // Console.WriteLine("Opened device");

        if(!DeviceIoControl(device, Flags.IOCTL_KEYBOARD_QUERY_INDICATORS, IntPtr.Zero, 0, ref KIPbuf, (UInt32)Marshal.SizeOf(KIPbuf), ref bytesReturned, IntPtr.Zero))
        {
            Int32 err = Marshal.GetLastWin32Error();
            throw new Win32Exception(err);
        }

        // Console.WriteLine(String.Format("Read LED status: {0:x}", KIPbuf.LEDflags));

        KIPbuf.LEDflags = (UInt16)(KIPbuf.LEDflags ^ Flags.KEYBOARD_CAPS_LOCK_ON);

        // Console.WriteLine(String.Format("Changed LED status to: {0:x}", KIPbuf.LEDflags));

        if(!DeviceIoControl(device, Flags.IOCTL_KEYBOARD_SET_INDICATORS, ref KIPbuf, (UInt32)Marshal.SizeOf(KIPbuf), IntPtr.Zero, 0, ref bytesReturned, IntPtr.Zero))
        {
            Int32 err = Marshal.GetLastWin32Error();
            throw new Win32Exception(err);
        }

        // Console.WriteLine("Set new LED status");

        if(!CloseHandle(device))
        {
            Int32 err = Marshal.GetLastWin32Error();
            throw new Win32Exception(err);
        }

        // Console.WriteLine("Closed device handle");

        if(!DefineDosDevice(Flags.DDD_REMOVE_DEFINITION, "myKBD", null))
        {
            Int32 err = Marshal.GetLastWin32Error();
            throw new Win32Exception(err);
        }

        // Console.WriteLine("Removed device definition");
    }
};

class Flags
{
    public static IntPtr INVALID_HANDLE_VALUE = (IntPtr)(-1);
    public const UInt32 IOCTL_KEYBOARD_SET_INDICATORS   = (0x0000000b << 16) | (0 << 14) | (0x0002 << 2) | 0; // from ntddkbd.h, ntddk.h
    public const UInt32 IOCTL_KEYBOARD_QUERY_INDICATORS = (0x0000000b << 16) | (0 << 14) | (0x0010 << 2) | 0; // from ntddkbd.h, ntddk.h

    public const UInt32 DDD_RAW_TARGET_PATH       = 0x00000001;
    public const UInt32 DDD_REMOVE_DEFINITION     = 0x00000002;
    public const UInt32 DDD_EXACT_MATCH_ON_REMOVE = 0x00000004;
    public const UInt32 DDD_NO_BROADCAST_SYSTEM   = 0x00000008;

    public const UInt32 GENERIC_ALL     = 0x10000000;
    public const UInt32 GENERIC_EXECUTE = 0x20000000;
    public const UInt32 GENERIC_WRITE   = 0x40000000;
    public const UInt32 GENERIC_READ    = 0x80000000;

    public const UInt32 CREATE_NEW        = 1;
    public const UInt32 CREATE_ALWAYS     = 2;
    public const UInt32 OPEN_EXISTING     = 3;
    public const UInt32 OPEN_ALWAYS       = 4;
    public const UInt32 TRUNCATE_EXISTING = 5;

    public const UInt16 KEYBOARD_SCROLL_LOCK_ON = 1;
    public const UInt16 KEYBOARD_NUM_LOCK_ON    = 2;
    public const UInt16 KEYBOARD_CAPS_LOCK_ON   = 4;
    public const UInt16 KEYBOARD_SHADOW         = 0x4000;
    public const UInt16 KEYBOARD_LED_INJECTED   = 0x8000;
};