How to Create Global Keyboard Hook with C# in Linux

For my intents and purposes, I wanted to have some code that would publish an event any time the user pressed a key anywhere on the system. While developing this, I found that I could also hook into mouse events as well.

It’s important to note that the code here (due to the Linux) doesn’t really distinguish between a keyboard button press and a mouse button press. To Linux, they’re both just buttons.

Understanding that you can actually expand this code to work with other items like gamepads and special input peripherals if you desire.

Additional Gotchas – This code here will just listen to events, but won’t block them from being consumed by the rest of the operating system. This means that if you want an event handler to the power off button or volume buttons you’ll run into issues with other processes consuming those updates.

Setting Up Permissions

In order to run this code, the user that runs this program will have to be in the input user group otherwise it will throw an exception. Run this command to add the current user to that group.

sudo gpasswd -a $USER input

EventType.cs

Since the folder /dev/input is essentially an event bus of a bunch of input/output devices for the Linux, there are a variety of event types that you may want to consume. Here is the enumeration to make deciphering the event types a little easier.

public enum EventType
{
    /// <summary>
    /// Used as markers to separate events. Events may be separated in time or in space, such as with the multitouch protocol.
    /// </summary>
    EV_SYN,

    /// <summary>
    /// Used to describe state changes of keyboards, buttons, or other key-like devices.
    /// </summary>
    EV_KEY,

    /// <summary>
    /// Used to describe relative axis value changes, e.g. moving the mouse 5 units to the left.
    /// </summary>
    EV_REL,

    /// <summary>
    /// Used to describe absolute axis value changes, e.g. describing the coordinates of a touch on a touchscreen.
    /// </summary>
    EV_ABS,

    /// <summary>
    /// Used to describe miscellaneous input data that do not fit into other types.
    /// </summary>
    EV_MSC,

    /// <summary>
    /// Used to describe binary state input switches.
    /// </summary>
    EV_SW,

    /// <summary>
    /// Used to turn LEDs on devices on and off.
    /// </summary>
    EV_LED,

    /// <summary>
    /// Used to output sound to devices.
    /// </summary>
    EV_SND,

    /// <summary>
    /// Used for autorepeating devices.
    /// </summary>
    EV_REP,

    /// <summary>
    /// Used to send force feedback commands to an input device.
    /// </summary>
    EV_FF,

    /// <summary>
    /// A special type for power button and switch input.
    /// </summary>
    EV_PWR,

    /// <summary>
    /// Used to receive force feedback device status.
    /// </summary>
    EV_FF_STATUS,
}

KeyState.cs

Like many other event handling systems there are multiple events that happen each time that the user presses a key. Once when the key is pressed down, another when the key is pressed up, and another if the user decides to hold the key down.

public enum KeyState
{
    KeyUp,
    KeyDown,
    KeyHold
}

EventCode.cs

Each distinct button is associated with an event code. Whether it’s a button on a keyboard or a button on a mouse, you’ll probably be able to find it here. He is a helper enum class to make deciphering those codes easier.

/// <summary>
/// Mapping for this can be found here: https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
/// </summary>
public enum EventCode
{
    Reserved = 0,
    Esc = 1,
    Num1 = 2,
    Num2 = 3,
    Num3 = 4,
    Num4 = 5,
    Num5 = 6,
    Num6 = 7,
    Num7 = 8,
    Num8 = 9,
    Num9 = 10,
    Num0 = 11,
    Minus = 12,
    Equal = 13,
    Backspace = 14,
    Tab = 15,
    Q = 16,
    W = 17,
    E = 18,
    R = 19,
    T = 20,
    Y = 21,
    U = 22,
    I = 23,
    O = 24,
    P = 25,
    LeftBrace = 26,
    RightBrace = 27,
    Enter = 28,
    LeftCtrl = 29,
    A = 30,
    S = 31,
    D = 32,
    F = 33,
    G = 34,
    H = 35,
    J = 36,
    K = 37,
    L = 38,
    Semicolon = 39,
    Apostrophe = 40,
    Grave = 41,
    LeftShift = 42,
    Backslash = 43,
    Z = 44,
    X = 45,
    C = 46,
    V = 47,
    B = 48,
    N = 49,
    M = 50,
    Comma = 51,
    Dot = 52,
    Slash = 53,
    RightShift = 54,
    KpAsterisk = 55,
    LeftAlt = 56,
    Space = 57,
    Capslock = 58,
    F1 = 59,
    Pf2 = 60,
    F3 = 61,
    F4 = 62,
    F5 = 63,
    F6 = 64,
    F7 = 65,
    F8 = 66,
    Pf9 = 67,
    F10 = 68,
    Numlock = 69,
    ScrollLock = 70,
    Kp7 = 71,
    Kp8 = 72,
    Kp9 = 73,
    PkpMinus = 74,
    Kp4 = 75,
    Kp5 = 76,
    Kp6 = 77,
    KpPlus = 78,
    Kp1 = 79,
    Kp2 = 80,
    Kp3 = 81,
    Kp0 = 82,
    KpDot = 83,

    Zenkakuhankaku = 85,
    //102ND = 86,
    F11 = 87,
    F12 = 88,
    Ro = 89,
    Katakana = 90,
    Hiragana = 91,
    Henkan = 92,
    Katakanahiragana = 93,
    Muhenkan = 94,
    KpJpComma = 95,
    KpEnter = 96,
    RightCtrl = 97,
    KpSlash = 98,
    SysRq = 99,
    RightAlt = 100,
    LineFeed = 101,
    Home = 102,
    Up = 103,
    Pageup = 104,
    Left = 105,
    Right = 106,
    End = 107,
    Down = 108,
    Pagedown = 109,
    Insert = 110,
    Delete = 111,
    Macro = 112,
    Mute = 113,
    VolumeDown = 114,
    VolumeUp = 115,
    Power = 116, // SC System Power Down
    KpEqual = 117,
    KpPlusMinus = 118,
    Pause = 119,
    Scale = 120, // AL Compiz Scale (Expose)

    KpComma = 121,
    Hangeul = 122,
    Hanja = 123,
    Yen = 124,
    LeftMeta = 125,
    RightMeta = 126,
    Compose = 127,

    Stop = 128, // AC Stop
    Again = 129,
    Props = 130, // AC Properties
    Undo = 131, // AC Undo
    Front = 132,
    Copy = 133, // AC Copy
    Open = 134, // AC Open
    Paste = 135, // AC Paste
    Find = 136, // AC Search
    Cut = 137, // AC Cut
    Help = 138, // AL Integrated Help Center
    Menu = 139, // Menu (show menu)
    Calc = 140, // AL Calculator
    Setup = 141,
    Sleep = 142, // SC System Sleep
    Wakeup = 143, // System Wake Up
    File = 144, // AL Local Machine Browser
    Sendfile = 145,
    DeleteFile = 146,
    Xfer = 147,
    Prog1 = 148,
    Prog2 = 149,
    Www = 150, // AL Internet Browser
    MsDos = 151,
    Coffee = 152, // AL Terminal Lock/Screensaver
    RotateDisplay = 153, // Display orientation for e.g. tablets
    CycleWindows = 154,
    Mail = 155,
    Bookmarks = 156, // AC Bookmarks
    Computer = 157,
    Back = 158, // AC Back
    Forward = 159, // AC Forward
    CloseCd = 160,
    EjectCd = 161,
    EjectCloseCd = 162,
    NextSong = 163,
    PlayPause = 164,
    PreviousSong = 165,
    StopCd = 166,
    Record = 167,
    Rewind = 168,
    Phone = 169, // Media Select Telephone
    Iso = 170,
    Config = 171, // AL Consumer Control Configuration
    Homepage = 172, // AC Home
    Refresh = 173, // AC Refresh
    Exit = 174, // AC Exit
    Move = 175,
    Edit = 176,
    ScrollUp = 177,
    ScrollDown = 178,
    KpLeftParen = 179,
    KpRightParen = 180,
    New = 181, // AC New
    Redo = 182, // AC Redo/Repeat
    
    F13 = 183,
    F14 = 184,
    F15 = 185,
    F16 = 186,
    F17 = 187,
    F18 = 188,
    F19 = 189,
    F20 = 190,
    F21 = 191,
    F22 = 192,
    F23 = 193,
    F24 = 194,
    
    PlayCd = 200,
    PauseCd = 201,
    Prog3 = 202,
    Prog4 = 203,
    Dashboard = 204,    // AL Dashboard
    Suspend = 205,
    Close = 206,    // AC Close
    Play = 207,
    FastForward = 208,
    BassBoost = 209,
    Print = 210,    // AC Print
    Hp = 211,
    Camera = 212,
    Sound = 213,
    Question = 214,
    Email = 215,
    Chat = 216,
    Search = 217,
    Connect = 218,
    Finance = 219,  // AL Checkbook/Finance
    Sport = 220,
    Shop = 221,
    AltErase = 222,
    Cancel = 223,   // AC Cancel
    BrightnessDown = 224,
    BrightnessUp = 225,
    Media = 226,
    
    SwitchVideoMode = 227,  // Cycle between available video outputs (Monitor/LCD/TV-out/etc)
    KbdIllumToggle = 228,
    KbdIllumDown = 229,
    KbdIllumUp = 230,

    Send = 231, // AC Send
    Reply = 232,    // AC Reply
    ForwardMail = 233,  // AC Forward Msg
    Save = 234, // AC Save
    Documents = 235,

    Battery = 236,

    Bluetooth = 237,
    Wlan = 238,
    Uwb = 239,

    Unknown = 240,

    VideoNext = 241,    // drive next video source
    VideoPrev = 242,    // drive previous video source
    BrightnessCycle = 243,  // brightness up, after max is min
    BrightnessAuto = 244,   // Set Auto Brightness: manual brightness control is off, rely on ambient
    DisplayOff = 245,   // display device to off state

    Wwan = 246, // Wireless WAN (LTE, UMTS, GSM, etc.)
    RfKill = 247,   // Key that controls all radios

    MicMute = 248,  // Mute / unmute the microphone
    LeftMouse = 272,
    RightMouse = 273,
    MiddleMouse = 274,
    MouseBack = 275,
    MouseForward = 276,
    
    ToolFinger = 325,
    ToolQuintTap = 328,
    Touch = 330,
    ToolDoubleTap = 333, 
    ToolTripleTap = 334, 
    ToolQuadTap = 335,
    Mic = 582
}

MouseAxis.cs

Mouse movements are expressed in an amount moved and an axis associated with that change. 0 represents movements on the X axis and 1 represents movements on the Y axis.

public enum MouseAxis
{
    X,
    Y
}

KeypressEvent.cs

Here is the event that I use to process key press events.

public class KeyPressEvent : EventArgs
{
    public KeyPressEvent(EventCode code, KeyState state)
    {
        Code = code;
        State = state;
    }

    public EventCode Code { get; }
    
    public KeyState State { get; }
}

MouseMoveEvent.cs

Here is the event that I use process mouse movement change updates.

public class MouseMoveEvent : EventArgs
{
    public MouseMoveEvent(MouseAxis axis, int amount)
    {
        Axis = axis;
        Amount = amount;
    }
    
    public MouseAxis Axis { get; }
    
    public int Amount { get; set; }
}

InputReader.cs

This is where the bulk of the work happens. Here we have a class, where you provide the path to one of the event files and it publishes updates whenever it comes in. An example file that does this would be “/dev/input/event0”.

More research would be needed to support more events types, but I was only interested in keyboard and mouse input so it serves my purposes. I also opted to drop the timestamp that is included with each button event, but if you’re interested, you can find it on the first 16 bits on the buffer.

public class InputReader : IDisposable
{
    public delegate void RaiseKeyPress(KeyPressEvent e);

    public delegate void RaiseMouseMove(MouseMoveEvent e);

    public event RaiseKeyPress OnKeyPress;
    public event RaiseMouseMove OnMouseMove;

    private const int BufferLength = 24;
    
    private readonly byte[] _buffer = new byte[BufferLength];
    
    private FileStream _stream;
    private bool _disposing;

    public InputReader(string path)
    {
        _stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

        Task.Run(Run);
    }

    private void Run()
    {
        while (true)
        {
            if (_disposing)
                break;

            _stream.Read(_buffer, 0, BufferLength);

            var type = BitConverter.ToInt16(new[] {_buffer[16], _buffer[17]}, 0);
            var code = BitConverter.ToInt16(new[] {_buffer[18], _buffer[19]}, 0);
            var value = BitConverter.ToInt32(new[] {_buffer[20], _buffer[21], _buffer[22], _buffer[23]}, 0);

            var eventType = (EventType) type;

            switch (eventType)
            {
                case EventType.EV_KEY:
                    HandleKeyPressEvent(code, value);
                    break;
                case EventType.EV_REL:
                    var axis = (MouseAxis) code;
                    var e = new MouseMoveEvent(axis, value);
                    OnMouseMove?.Invoke(e);
                    break;
            }
        }
    }

    private void HandleKeyPressEvent(short code, int value)
    {
        var c = (EventCode) code;
        var s = (KeyState) value;
        var e = new KeyPressEvent(c, s);
        OnKeyPress?.Invoke(e);
    }

    public void Dispose()
    {
        _disposing = true;
        _stream.Dispose();
        _stream = null;
    }
}

AggregateInputReader.cs

I’m looking to handle input from every device anywhere on the system. I’ve put together this class to aggregate the input events from all the files in the “/dev/input” folder.

Known issue – This code will throw an exception if a usb device is removed while it’s running. I do intend to fix it in my own app implementation, but I don’t have time to take care of it now.

public class AggregateInputReader : IDisposable
{
    private List<InputReader> _readers = new();
    
    public event InputReader.RaiseKeyPress OnKeyPress;

    public AggregateInputReader()
    {
        var files = Directory.GetFiles("/dev/input/", "event*");

        foreach (var file in files)
        {
            var reader = new InputReader(file);
            
            reader.OnKeyPress += ReaderOnOnKeyPress;

            _readers.Add(reader);
        }
    }

    private void ReaderOnOnKeyPress(KeyPressEvent e)
    {
        OnKeyPress?.Invoke(e);
    }

    public void Dispose()
    {
        foreach (var d in _readers)
        {
            d.OnKeyPress -= ReaderOnOnKeyPress;
            d.Dispose();
        }

        _readers = null;
    }
}

Example Usage

Not bad that this can now be accomplished in two line of code.

public class Program
{
    public static void Main(string[] args)
    {
        using var aggHandler = new AggregateInputReader();

        aggHandler.OnKeyPress += (e) => { System.Console.WriteLine($"Code:{e.Code} State:{e.State}"); };

        System.Console.ReadLine();
    }
}

Thanks for sticking with this. I hope it works out for you!

2 thoughts on “How to Create Global Keyboard Hook with C# in Linux”

  1. Hi, Grant.

    I found your StackOverflow issue for this topic and I’m attempting to implement it in an IoT project. When running the program I only get my keypresses echoed back to the screen; “Code: [##] State: [KeyUp|KeyDown]” is not written to the console.

    The user is in the input group and the keyboard exists on event1 (verified by plugging/unplugging the keyboard and getting the contents of /dev/input)

    Any idea what might be wrong here? How can I troubleshoot? Thanks for any help!

Leave a Reply

Your email address will not be published. Required fields are marked *