Table of Contents

Keyboard

In this article, we will discuss keyboard input on Cosmos Gen3: how to read text through the standard Console API, how to react to individual key presses, and how to switch keyboard layouts.

The main differences if you come from Gen2:

Gen2 Gen3
Manager API Cosmos.System.KeyboardManager Same API, in Cosmos.Kernel.System.Keyboard
Console.ReadLine / Console.ReadKey Plugged, backed by the manager Plugged, backed by the manager
Key events KeyEvent (KeyChar, Key, Modifiers) Same
Layouts US, FR, DE, ES, GB, TR, Dvorak scan maps Same set, in Cosmos.Kernel.System.Keyboard.ScanMaps
Devices PS/2 keyboard PS/2 keyboard (x64), virtio-keyboard (x64 PCI and ARM64 MMIO)

If you find bugs or something abnormal, please submit an issue on our repository.

Enable the keyboard in your kernel

Keyboard support is behind a feature switch. Make sure your kernel's .csproj does not turn it off (it defaults to true):

<PropertyGroup>
  <CosmosEnableKeyboard>true</CosmosEnableKeyboard>
</PropertyGroup>

These are the usings the snippets below rely on:

using Cosmos.Kernel.System.Keyboard;
using Cosmos.Kernel.System.Keyboard.ScanMaps;

There is nothing to initialize by hand: at boot the kernel probes the PS/2 controller and the virtio bus, and registers every keyboard it finds with KeyboardManager.

Reading a line

Console.ReadLine() works the way it does everywhere else in .NET, so most kernels never need anything more:

Console.WriteLine("What is your name?");
Console.Write("> ");

string? name = Console.ReadLine();
Console.WriteLine("Hello, " + name + "!");

The line editor supports more than typing: Left and Right arrows move the cursor inside the input, Home and End jump to either end, Backspace and Delete remove characters, and typing in the middle of the line inserts rather than overwrites.

Reading single keys

For anything below the line level, KeyboardManager.ReadKey() blocks until a key is pressed and returns a KeyEvent:

Console.WriteLine("Press keys to inspect them (Escape to leave):");

while (true)
{
    KeyEvent key = KeyboardManager.ReadKey();
    if (key.Key == ConsoleKeyEx.Escape)
    {
        break;
    }

    Console.WriteLine("Key: " + key.Key + "   Char: '" + key.KeyChar
        + "'   Modifiers: " + key.Modifiers);
}

A KeyEvent carries three things:

Property Type Meaning
Key ConsoleKeyEx The physical key, independent of layout and modifiers (A, D5, F1, LeftArrow, ...)
KeyChar char The text character the key produces, '\0' if it produces none (function keys, arrows, Ctrl combinations)
Modifiers ConsoleModifiers The Shift, Alt and Control flags active at the time of the press

The standard Console.ReadKey() also works and returns a regular ConsoleKeyInfo; the ReadKey(true) overload suppresses the echo. It is a thin wrapper over KeyboardManager.ReadKey(), so use whichever fits your code.

Polling without blocking

A render loop cannot afford to block on ReadKey(). KeyboardManager.TryReadKey() dequeues a pending key press and returns false when there is none, so the frame goes on. Here it drives a square over a canvas from the Graphics article:

Canvas canvas = Canvas.GetFullScreen();
Font font = PCScreenFont.DefaultFont;

int x = (canvas.Width - 60) / 2;
int y = (canvas.Height - 60) / 2;
const int Step = 20;

bool running = true;
while (running)
{
    /* Drain every key pressed since the last frame */
    while (KeyboardManager.TryReadKey(out KeyEvent? key))
    {
        switch (key.Key)
        {
            case ConsoleKeyEx.UpArrow: y -= Step; break;
            case ConsoleKeyEx.DownArrow: y += Step; break;
            case ConsoleKeyEx.LeftArrow: x -= Step; break;
            case ConsoleKeyEx.RightArrow: x += Step; break;
            case ConsoleKeyEx.Escape: running = false; break;
        }
    }

    x = Math.Clamp(x, 0, canvas.Width - 60);
    y = Math.Clamp(y, 0, canvas.Height - 60);

    canvas.Clear(Color.MidnightBlue);
    canvas.DrawString("Move the square with the arrow keys", font, Color.White, 40, 40);
    canvas.DrawFilledRectangle(Color.Gold, x, y, 60, 60);
    canvas.Display();

    Thread.Sleep(15);
}

KeyboardManager.KeyAvailable (or the plugged Console.KeyAvailable) tells you whether a key press is waiting without consuming it, and KeyboardManager.Peek() returns the next KeyEvent while leaving it in the queue.

Modifiers and lock keys

KeyboardManager tracks the modifier and lock state globally, outside of any key event:

Property Meaning
ShiftPressed A Shift key is currently held
ControlPressed A Control key is currently held
AltPressed An Alt key is currently held
CapsLock Caps Lock is toggled on
NumLock Num Lock is toggled on
ScrollLock Scroll Lock is toggled on

The lock keys toggle their state on each press and update the keyboard LEDs. Held modifiers also arrive on every KeyEvent through its Modifiers flags, which is usually the more convenient form.

Keyboard layouts

Key presses come out of the hardware as layout-neutral scan codes; a scan map turns them into characters. The default is US QWERTY, and SetKeyLayout switches at any time:

KeyboardManager.SetKeyLayout(new FRStandardLayout());

Seven layouts ship in Cosmos.Kernel.System.Keyboard.ScanMaps:

Class Layout
USStandardLayout US QWERTY (default)
FRStandardLayout French AZERTY
DEStandardLayout German QWERTZ
ESStandardLayout Spanish QWERTY
GBStandardLayout British QWERTY
TRStandardLayout Turkish Q
USDvorakLayout US Dvorak

The switch is visible immediately: below, the same six physical keys are typed twice, first under the US layout, then under the French one.

KeyboardManager.GetKeyLayout() returns the active scan map, and a custom layout is a class deriving from ScanMapBase that fills the Keys list with KeyMapping entries.

Current limitations

  • Key releases are not queued: KeyEvent.Type has a Break value, but only presses reach the buffer. Releases of Shift, Ctrl and Alt update the modifier state and are otherwise dropped.
  • The bundled scan maps cover the base and shifted characters only: AltGr combinations (@, #, { on AZERTY) and dead keys are not mapped.
  • Devices are detected once at boot; there is no keyboard hotplug.

How it works

Every key press raises an interrupt (IRQ1 for the PS/2 keyboard on x64, a virtio-input event on ARM64). The handler feeds the raw scan code to KeyboardManager.HandleScanCode, which routes lock and modifier keys to the state properties and converts everything else through the active scan map into a KeyEvent, queued in the key buffer. ReadKey() halts the CPU until an interrupt delivers the next event; TryReadKey() just dequeues. Console.ReadLine and Console.ReadKey are plugs on top of the same queue, so console input and raw key events never conflict.

Console.ReadLine / Console.ReadKey        (plugs, Cosmos.Kernel.Plugs)
        │
KeyboardManager ── KeyEvent queue ◀── scan map (active layout)
        │                                    ▲
        │                              raw scan codes
        │                                    │
PS/2 keyboard, IRQ1 (x64)  /  virtio-keyboard (x64 PCI, ARM64 MMIO)