# Design: VTX — VT Export Protocol

Copyright (c) 2026 Nicolas Pitre <nico@fluxnic.net>
SPDX-License-Identifier: MIT

## Status

Draft — 2026-04-09

## Authors

Nicolas Pitre <nico@fluxnic.net> (BRLTTY community), with design
assistance from Claude (Anthropic)

## Summary

This document defines the VTX (VT Export) protocol — an interface for
exposing terminal screen content and control to accessibility tools.
The protocol uses a shared memory segment for efficient screen content
access and a Unix domain socket for control operations and event
signaling.

While VTX is designed to be implementable by any terminal emulator
(including the Linux kernel's VT subsystem), this document scopes the
first implementation to kmscon — a userspace replacement for the kernel
virtual terminal — and BRLTTY — the Linux braille display daemon.

The companion file `vtx_protocol.h` is a standalone C header that
defines all protocol structures, TLV types, message constants, cell
format, and helper macros. It is intended to be shared between server
and client implementations.

## Contents

- Background: kmscon, BRLTTY, and the problem being solved
- Design goals: efficiency, robustness, extensibility
- Architecture overview: two-channel design (shm + socket)
- Typical operation flow: startup, steady-state, resize, session
  switching, keyboard, character injection, disconnection, server
  shutdown
- Detailed design:
  - Shared memory: lifecycle, preamble, consistency model, update
    flow control with sequence numbers and change bitfields
  - TLV format, type ranges, shared data types
  - Cell format, combining codepoints, overflow area
  - Screen export renderer
  - Socket: connection/discovery, byte order, unknown types,
    connection handshake, SOCK_SEQPACKET framing, fd passing
  - Message types: screen/shm, keyboard, input injection
  - Keyboard: ownership, first-refusal, ack/timeout, combinations
  - Session management, multiple clients, area highlighting
- Security: socket permissions (vtx group), peer credentials,
  anonymous shm, rationale for group-based over seat-based access
- Alternatives considered: five approaches evaluated and discarded
- Implementation roadmap: phased plan for both projects
- Open questions

## Background

### kmscon

kmscon is a userspace terminal emulator that renders directly to
DRM/KMS display hardware, replacing the kernel's built-in virtual
console. It uses libtsm (Terminal-emulator State Machine) for all
terminal emulation and state management. kmscon supports multiple
sessions (analogous to kernel VTs), multiple display outputs, and
multiple font/rendering backends.

- Repository: https://github.com/kmscon/kmscon
- Build system: meson
- Key dependency: libtsm (https://github.com/kmscon/libtsm)

### BRLTTY

BRLTTY is a background daemon that provides braille display access to
the Linux console. It supports dozens of braille display hardware models
and includes multiple screen drivers for different environments (Linux
VT, tmux, AT-SPI2, Android, etc.).

- Website: http://brltty.app/
- Maintainer: Dave Mielke

### The problem

BRLTTY's Linux screen driver depends entirely on kernel VT interfaces:

| Interface | Purpose |
|-----------|---------|
| `/dev/vcsa[N]` | Read screen content (characters + VGA attributes), cursor position, dimensions |
| `/dev/vcsu[N]` | Read screen content as 32-bit Unicode codepoints (preferred over vcsa when available) |
| `/dev/tty0` + `VT_GETSTATE` | Query active VT number |
| `KDGETMODE` | Determine text vs. graphics mode |
| `TIOCSTI` | Inject keystrokes into the terminal |
| `TIOCL_SETSEL` | Text selection |
| `TIOCL_GETBRACKETEDPASTE` | Query bracketed paste mode |
| `GIO_UNIMAP` | Font-to-Unicode mapping (fallback when `/dev/vcsu` is not available) |

`/dev/vcsu` is the primary text interface on modern kernels. It provides
full Unicode codepoints (one `uint32_t` per cell) without requiring
font-to-Unicode reverse mapping via `GIO_UNIMAP`. However, it does not
carry attributes — BRLTTY reads those from `/dev/vcsa` alongside.
Double-width characters are represented by placing a zero-width space
(U+200B) in the continuation cell.

When kmscon replaces the kernel VT, **all of these interfaces
disappear**. BRLTTY's Linux screen driver becomes non-functional.
A new integration path is needed.

## Design goals

1. **Efficiency**: BRLTTY should only read the cells it needs (the
   braille window region), not the entire screen on every update.

2. **Event-driven**: BRLTTY should sleep when the screen is idle and
   wake only when content changes. No busy-polling.

3. **Robustness**: If BRLTTY crashes or disconnects, kmscon must
   continue to function normally with no user-visible impact.

4. **Extensibility**: The protocol must allow new metadata fields and
   capabilities to be added without breaking existing implementations.

5. **Minimal conversion overhead**: Data formats should be close to
   what each side uses natively to minimize per-cell transformation
   costs.

6. **No artificial limits**: The design must not assume maximum screen
   dimensions. Configurations such as 3840×2160 at 8×8 font (480×270
   cells) are realistic.

## Architecture overview

```
┌─────────────────────────────┐        ┌────────────────────────────┐
│           kmscon             │        │           BRLTTY            │
│                              │        │                             │
│  ┌────────────────────────┐  │        │  ┌───────────────────────┐  │
│  │ tsm_screen (libtsm)    │  │        │  │ Screen Driver "vtx"   │  │
│  │ terminal state machine  │  │        │  │ (new)                 │  │
│  └──────────┬─────────────┘  │        │  └───────┬───────────────┘  │
│             │                │        │          │                  │
│  ┌──────────▼─────────────┐  │  shm   │  ┌───────▼───────────────┐  │
│  │ Screen export renderer │  ├───────►│  │ shm reader            │  │
│  │ (new)                  │  │ mmap   │  │ (read-only)           │  │
│  └──────────┬─────────────┘  │        │  └───────────────────────┘  │
│             │                │        │                             │
│  ┌──────────▼─────────────┐  │ socket │  ┌───────────────────────┐  │
│  │ Control socket         │◄─┼───────►│  │ Control client        │  │
│  │ (new)                  │  │        │  │                       │  │
│  └────────────────────────┘  │        │  └───────────────────────┘  │
│                              │        │                             │
│  ┌────────────────────────┐  │        │                             │
│  │ uterm_input            │  │        │                             │
│  │ (keyboard via evdev)   │──┼────────┼──► key events offered      │
│  └────────────────────────┘  │        │    to BRLTTY first         │
└─────────────────────────────┘        └────────────────────────────┘
```

The design has two communication channels:

- **Shared memory**: Screen cell data (read-only for BRLTTY). This is
  the hot path — BRLTTY reads only the cells under its braille window
  directly from mapped memory with no IPC round-trip.

- **Unix domain socket**: Control operations, event notifications, and
  keyboard interaction. This carries low-volume, low-frequency messages.

## Typical operation flow

### Startup and connection

1. kmscon starts, creates the VTX socket for the seat.
2. A terminal session becomes active. kmscon tracks the session's
   screen state but does not allocate shm yet — there are no clients.
3. BRLTTY starts, discovers the VTX socket, and connects.
4. kmscon accepts the connection. Since this is the first client and a
   terminal session is active, kmscon creates the shm segment on demand.
   The segment contains the preamble, TLV header, and cell array,
   allocated in page-sized increments. The preamble's `shm_size` records
   the actual data extent.
5. kmscon sends a `Shm update` message (type `0x0101`) with the
   `INITIAL` flag, carrying the shm mapping size and file descriptor
   via `SCM_RIGHTS`.
6. BRLTTY receives the fd, maps the shm segment using the mapping size
   from the message, and reads the preamble and TLV header to discover
   screen dimensions, cursor position, terminal state, and the cell
   array location.

When the last client disconnects, kmscon frees the shm segment. No
resources are consumed when no accessibility clients are connected.

### Steady-state screen reading

1. BRLTTY blocks in `poll()` on the socket fd.
2. Terminal output arrives (e.g., the user runs a command). kmscon
   updates the cell array in shm, increments the sequence number, and
   sends a `Screen updated` notification (type `0x0100`) with the
   sequence number — but only if no notification is already pending for
   this client.
3. BRLTTY wakes from `poll()`, reads the cells under its braille window
   directly from shm (typically 40–80 cells), and updates the braille
   display.
4. BRLTTY sends an `Update acknowledged` message (type `0x0200`) with
   the sequence number from the notification.
5. kmscon receives the ack. If the sequence number matches the current
   state, the client is up to date. If the screen changed again during
   steps 3–4, the sequence number will be behind and kmscon sends a new
   notification immediately.

### Screen resize

1. The terminal dimensions change (e.g., display resolution or
   configuration change).
2. kmscon creates a new shm segment with the new dimensions and sends a
   `Shm update` message (type `0x0101`) with the `RESIZE` flag,
   carrying the new mapping size and shm fd.
3. BRLTTY receives the new fd, maps the new segment, unmaps the old one,
   and re-reads the TLV header for the new dimensions.

### Session switching

1. The user switches to a different terminal session (e.g., Alt+Right).
2. kmscon creates a new shm segment for the new session's screen and
   sends a `Shm update` message (type `0x0101`) with the `SESSION` flag
   and the new fd.
3. BRLTTY receives the new fd, remaps, and reads the new session's
   content. The session number in the shm TLV header tells BRLTTY which
   session is now active.

### Keyboard input (when key offers are enabled)

1. BRLTTY sends `Enable key offers` (type `0x0211`).
2. A physical key is pressed. kmscon offers it to BRLTTY via a `Key
   event offer` message (type `0x0110`).
3. BRLTTY responds with `Key event accepted` (type `0x0210`).
4. If the key matches a BRLTTY binding, BRLTTY consumes it.
5. If the key does not match, BRLTTY sends it back as a `Key event
   injection` (type `0x0220`). kmscon processes it through the VTE as
   if it were a physical keypress.

### Character injection

1. BRLTTY sends a `Character injection` message (type `0x0221`) with a
   UCS-4 codepoint (e.g., from braille keyboard input or clipboard
   paste).
2. The server delivers the character to the terminal application (e.g.,
   kmscon encodes it as UTF-8 and writes it to the PTY).

### Client disconnection

1. BRLTTY closes the socket (or crashes).
2. kmscon detects the closed connection, removes the client from its
   list, and disables any key offers for that client.
3. kmscon continues operating normally — no impact on the terminal.

### Server shutdown

1. The VTX server closes the listening socket and all client connections.
2. BRLTTY detects EOF on `recvmsg()` (returns 0).
3. BRLTTY unmaps the shm segment and may attempt to reconnect
   periodically, as the server may restart.

## Detailed design

### Shared memory segment

#### Lifecycle

kmscon creates an anonymous shared memory segment (`memfd_create()`) for
each terminal session. The segment is sized to hold a fixed preamble, a
variable-length TLV header, and the cell array for the current screen
dimensions. The fd is passed to BRLTTY over the Unix socket at
connection time.

When the screen is resized, kmscon creates a **new** shm segment with
the new dimensions, populates it, and passes the new file descriptor to
BRLTTY over the socket. BRLTTY maps the new segment, then unmaps and
closes the old one. This avoids race conditions during resize — the old
segment remains valid until BRLTTY releases it.

This safety relies on standard file descriptor semantics: the underlying
memory is not freed until all file descriptors and mappings are closed.
The sequence is:

1. kmscon creates and populates the new shm segment.
2. kmscon sends the new segment's fd to BRLTTY over the socket.
3. kmscon closes its fd to the old segment.
4. BRLTTY receives the new fd and maps the new segment.
5. BRLTTY unmaps and closes the old segment — its memory is now freed.

#### Preamble (fixed format)

The first bytes of the shm segment are a fixed-size preamble:

```c
struct vtx_shm_preamble {
    uint32_t magic;          /* Identifies this as a VTX shm segment */
    uint16_t version;        /* Preamble/TLV framing version         */
    uint16_t header_size;    /* Total header size (preamble + TLV)   */
    uint32_t shm_size;       /* Total shm segment size in bytes      */
};
```

The `shm_size` field tells the client exactly how much to `mmap`. This
is more reliable than using `fstat()` and is needed to locate the
overflow area for combining codepoints (see below).

The `version` field only changes if the preamble or TLV framing format
itself changes. Adding new TLV fields does not require a version bump.

#### Consistency model

No locking is used on the shm segment. If BRLTTY reads cells while
kmscon is mid-write, the braille display may show a momentary
inconsistency. This is resolved by the socket notification mechanism
described below.

This is the same general model the Linux kernel uses for `/dev/vcs*`.
The VT layer clears a poll event flag when a read starts. If the screen
changes during the read, the flag is set again, causing `poll()` to
return immediately and trigger a re-read. No locking is involved —
eventual consistency is sufficient for display output.

#### Update notification flow control

Screen update notifications carry a sequence number and a change
bitfield, and are flow-controlled to prevent flooding the socket when
the client is slow. The server maintains a monotonically increasing
sequence number that is incremented after every shm write. Each client
accumulates a change bitfield (`CELLS`, `CURSOR_POS`, `TERMINAL_STATE`,
`MOUSE_POS`) indicating what has changed since its last acknowledged
update. The protocol is:

1. BRLTTY blocks in `poll()` on the socket fd.
2. kmscon updates shm, increments its sequence number to N, and OR's
   the appropriate change bits into each client's pending changes.
3. If no notification is pending for this client, kmscon sends a
   `Screen updated` notification (type `0x0100`) with seq=N and the
   accumulated change bits, then clears the bits and marks the client
   as having a notification in flight.
4. If a notification is already pending, kmscon does nothing — the
   change bits accumulate until the client acks.
5. BRLTTY wakes, checks the change bitfield to determine what to read
   from shm (e.g., only cursor position if only `CURSOR_POS` is set),
   and sends an `Update acknowledged` message (type `0x0200`) with the
   sequence number from the notification.
6. kmscon receives the ack. If the acked sequence number matches the
   current sequence number and no new changes have accumulated, the
   client is up to date — clear the pending flag. If changes have
   accumulated since the notification was sent, send a new notification
   immediately with the current sequence number and accumulated bits.

This ensures at most one notification is in flight per client at any
time. The socket never accumulates a backlog of stale notifications.
The change bitfield lets the client skip reading unchanged parts of
the shm. The sequence number guarantees the client converges to the
latest state.

#### TLV format

Both the shm header and socket messages use the same TLV (type-length-
value) framing and share a unified type numbering space. Each entry is:

```c
struct vtx_tlv_entry {
    uint16_t type;     /* Field type identifier         */
    uint16_t length;   /* Value length in bytes          */
    /* uint8_t value[length], padded to 4-byte alignment */
};
```

The `length` field contains the actual data size. The TLV walker is
responsible for rounding up to the next 4-byte boundary to find the
start of the next entry:

```
next_entry = current_value_ptr + ((length + 3) & ~3)
```

In the shm header, type `0x0000` is a sentinel marking the end of the
TLV area. On the socket, each message carries one or more TLV entries.

Each side skips types it does not recognize. The server can add new fields
without breaking older BRLTTY drivers. BRLTTY can look for a field and
gracefully handle its absence.

#### Type number ranges

| Range | Usage |
|-------|-------|
| `0x0001–0x00FF` | Shared data types — used in both the shm header (as a full state snapshot) and socket messages (as incremental updates). Parsed with the same code path. |
| `0x0100–0x01FF` | Server → client control messages — socket only. |
| `0x0200–0x02FF` | Client → server control messages — socket only. |

This unification means that, for example, a cursor position change can
be communicated as a single TLV entry (type `0x0002`) on the socket
rather than requiring a full shm header rewrite and a separate "screen
updated" notification. BRLTTY can apply the delta directly or re-read
from shm — whichever is more appropriate for the specific field.

#### Shared data types (0x0001–0x00FF)

| Type | Name | Size | Value format |
|------|------|------|-------------|
| `0x0001` | Screen dimensions | 4 | `uint16_t cols, uint16_t rows` |
| `0x0002` | Cursor position | 4 | `uint16_t col, uint16_t row` |
| `0x0003` | Terminal state flags | 4 | `uint32_t flags` (see below) |
| `0x0004` | Mouse pointer position | 4 | `uint16_t col, uint16_t row` |
| `0x0005` | Active session number | 2 | `uint16_t number` |
| `0x0006` | Cell array descriptor | 12 | `uint32_t offset, uint32_t count, uint16_t cell_stride, uint16_t format_version`. Offset is from shm start. |
| `0x0007` | Overflow descriptor | 8 | `uint32_t offset, uint32_t size`. Offset is from shm start. |

All offsets in the protocol are byte offsets from the start of the shm
segment.

Terminal state flags:

| Bit | Attribute |
|-----|-----------|
| 0 | Cursor visible |
| 1 | Cursor blinking |
| 2–3 | Cursor shape: 0 = default, 1 = block, 2 = underline, 3 = bar |
| 4 | Bracketed paste mode active |
| 5 | Mouse mode active (application is listening for mouse events) |
| 6 | Graphics mode (no text content available) |
| 7–31 | Reserved |

This field combines cursor state and VTE mode flags into a single
entry. Cursor shape (DECSCUSR) and blink are not yet implemented in
libtsm but are standard terminal features likely to be added. The
mouse mode flag indicates whether the application has enabled mouse
tracking (any of the X10, VT200, button, or any-event modes). BRLTTY
could use this to decide whether to send a mouse click or use iterative
arrow key routing to reach a target position.

The cell array descriptor tells BRLTTY where the cell data begins in
the shm segment and how large each cell is. If the cell format grows
in the future (e.g., to add hyperlink IDs or underline style), the
stride increases but older readers can safely ignore trailing bytes per
cell by relying on the stride value.

#### Cell format

Each cell in the array represents one screen position:

```c
struct vtx_shm_cell {
    uint32_t codepoint;    /* Unicode codepoint (UCS-4)       */
    uint16_t flags;        /* Width + attribute flags          */
    uint8_t  fr, fg, fb;   /* Foreground color (RGB)           */
    uint8_t  br, bg, bb;   /* Background color (RGB)           */
};
```

12 bytes per cell, naturally aligned.

The `flags` field encodes both character width and text attributes:

| Bits | Attribute |
|------|-----------|
| 0 | Single-width |
| 1 | Double-width |
| 2 | Bold |
| 3 | Italic |
| 4 | Underline |
| 5 | Blink |
| 6 | Inverse (resolved — informational only) |
| 7–15 | Reserved |

Character width is obtained by masking the low two bits: `flags & 0x03`.
A normal character has bit 0 set (width = 1). A double-width character
has bit 1 set (width = 2). A continuation cell following a double-width
character has neither bit set (width = 0).

libtsm stores double-width characters (e.g., CJK ideographs) as a
primary cell with width 2 followed by a continuation cell with width 0.
The shm format preserves this representation directly. BRLTTY can use
the width for correct braille display alignment and cursor routing
without needing to inspect character values or peek at adjacent cells.

This is an improvement over the kernel VT's `/dev/vcsu` interface,
which represents double-width characters by placing a zero-width space
(U+200B) in the continuation cell — requiring the reader to recognize
the sentinel character value.

##### Combining codepoints and grapheme clusters

A grapheme cluster — what the user perceives as a single character —
may consist of a base codepoint plus one or more combining codepoints
(e.g., 'e' + combining acute accent, or complex Indic/Tibetan
clusters with many combiners). Not all combinations have precomposed
Unicode equivalents, so the protocol must support multi-codepoint
clusters.

The cell format handles this in three layers:

1. **Common case**: The cell's `codepoint` field holds a single UCS-4
   value. No combining marks. This covers the vast majority of cells.

2. **Double-width with combiners**: The continuation cell(s) following
   a double-width character (with `width=0`) can hold additional
   codepoints for combining marks. The reader collects codepoints from
   the primary cell and its continuation cells to form the full cluster.
   This handles most combining emoji cases.

3. **Overflow area**: When more codepoints are needed than fit in
   available cells (e.g., single-width base with combiners, or more
   combiners than continuation cells), the cell's `codepoint` field is
   set to a special value: `0xFF000000 | offset`. The offset is a byte
   offset from the start of the shm segment to the overflow entry.
   The overflow entry contains:

```c
   uint32_t count;          /* number of codepoints in the cluster */
   uint32_t codepoints[];   /* the full codepoint sequence         */
```

The overflow area is described by TLV type `0x0007` (overflow
descriptor) which gives its offset and size within the shm segment.
The total shm segment size is in the preamble's `shm_size` field.

The overflow area is allocated on demand — if no cells use combining
codepoints, no overflow area exists and no memory is wasted.

The shm segment is always allocated in page-sized increments (typically
4 KB). The preamble's `shm_size` reflects the actual data extent, which
may be smaller than the mapping size sent with the fd. The gap between
`shm_size` and the mapping size is free space available for overflow
growth without creating a new segment. When overflow entries are needed,
the server writes them into this free space, updates `shm_size` and the
overflow descriptor TLV, and sends a normal `Screen updated` notification
— no remap required.

If the overflow area exceeds the current mapping size, the server creates
a new shm segment with a larger allocation and sends the new fd to
clients — the same mechanism as a screen resize.

The RGB values are **always final resolved colors**. When inverse mode
is active, kmscon swaps foreground and background before writing to shm.
Color codes from libtsm's palette are resolved to RGB at write time.
BRLTTY receives ready-to-use colors with no palette lookup needed.

##### Mapping to BRLTTY's ScreenCharacter

The conversion from `vtx_shm_cell` to BRLTTY's `ScreenCharacter` is
nearly trivial:

| shm cell field | ScreenCharacter field |
|---|---|
| `codepoint` | `text` (wchar_t) |
| `fr, fg, fb` | `color.foreground` (RGBColor) |
| `br, bg, bb` | `color.background` (RGBColor) |
| `flags & BOLD` | `color.isBold` |
| `flags & ITALIC` | `color.isItalic` |
| `flags & UNDERLINE` | `color.hasUnderline` |
| `flags & BLINK` | `color.isBlinking` |
| `flags & 0x03` | character width |
| (always) | `color.usingRGB = 1` |

This conversion is performed only for the cells within BRLTTY's current
braille window — typically 40 or 80 cells — not the entire screen.

Unlike other BRLTTY screen drivers (Linux VT, tmux) which must maintain
a shadow copy of the screen content in the driver's own memory, the
VTX driver does not need one. The shm segment is persistently mapped
and always reflects the current screen state, so BRLTTY can read any
cell at any time without having to cache or refresh a local copy.

##### Mapping from libtsm's tsm_screen_attr

kmscon's screen export renderer receives per-cell data via the
`tsm_screen_draw()` callback. Writing to shm is similarly direct:

| draw callback parameter | shm cell field |
|---|---|
| `ch` (UCS-4 array, first element) | `codepoint` |
| `width` | `flags` bits 0–1 (1 << (width - 1) for width > 0, else 0) |
| `attr.fr, attr.fg, attr.fb` | `fr, fg, fb` |
| `attr.br, attr.bg, attr.bb` | `br, bg, bb` |
| `attr.bold, attr.italic, ...` | `flags` bits 2+ |

libtsm's `to_rgb()` function resolves palette color codes to RGB values
before the draw callback fires, so the RGB fields are always populated
regardless of whether the original color was specified as a palette
index, a 256-color code, or a true-color RGB value.

### Screen export renderer

kmscon's rendering architecture already supports multiple renderers per
terminal session. Each display gets its own renderer (bbulk for software
rendering, gltex for OpenGL), and `redraw_all()` iterates through all
of them, calling `tsm_screen_draw()` once per renderer.

The screen export renderer is a new renderer that participates in this
same cycle. Instead of drawing to a framebuffer, it writes cell data to
the shared memory segment. It uses libtsm's age tracking to skip cells
that have not changed since the last pass — the same optimization the
visual renderers use.

After updating the shm segment, the export renderer sends a `Screen
updated` notification on the control socket to wake BRLTTY.

### Unix domain socket

The control socket carries all operations that are not read-only screen
content access.

#### Connection and discovery

VTX sockets are located under a well-known directory:

    /run/vtx/

The directory is owned by `root:vtx` with mode `0750`. Sockets within
it are created with mode `0660` and group `vtx`. Only members of the
`vtx` group can connect, preventing unauthorized applications from
snooping screen content or injecting keystrokes.

Socket naming is server-specific. For example, kmscon uses
`kmscon_seat0.sock`. Other terminal emulators may use their own
naming convention. Servers must ensure their socket names do not
conflict with other servers in the same directory.

The VTX screen driver in BRLTTY accepts:

- `socket` — explicit socket path, overriding auto-discovery.

When no parameter is given, the driver scans `/run/vtx/` and connects
to the first `.sock` file found.

#### Message framing

Socket messages use the same TLV framing as the shm header. Each
message consists of one or more TLV entries. This means the same
parsing code handles both shm header fields and socket messages.

Shared data types (0x0001–0x00FF) are primarily used in the shm header.
The `Screen updated` notification's change bitfield tells the client
which parts of the shm header to re-read, avoiding the need to send
individual TLV updates on the socket for cursor moves, terminal state
changes, etc.

#### Byte order

All multi-byte values in the protocol (TLV headers, payloads, shm
preamble, cell fields) use native byte order. Since VTX operates over
Unix domain sockets and shared memory on the same machine, both sides
always share the same architecture. The protocol is not designed for
network transport.

#### Unknown message types

Both sides must silently ignore TLV types they do not recognize, whether
in the shm header or on the socket. This allows new types to be added
without breaking existing implementations.

#### Connection handshake

On accepting a new client, the server must send a `Shm update` message
(type `0x0101`) with the `INITIAL` flag set as the first message. This
carries the shm fd and establishes the shared memory mapping. If no
terminal session is active, the shm has a cell count of 0. The client
must expect type `0x0101` as the first message; any other type indicates
an incompatible server and the client should close the connection. After
mapping the shm, the client can verify the protocol version in the
preamble.

#### Socket type and message framing

The VTX socket uses `SOCK_SEQPACKET`, which provides message-boundary
preservation on Unix domain sockets. Each `sendmsg()` call produces
exactly one message received by a single `recvmsg()` call. This
guarantees that:

- Each message is received atomically — no partial reads, no need to
  reassemble fragments across multiple reads.
- File descriptors passed via `SCM_RIGHTS` are unambiguously associated
  with their message.
- Multiple messages in the socket buffer remain distinct.

Clients must use `recvmsg()` to read from the socket.

#### File descriptor passing

Some messages (Resize and Session switched) carry a shared memory file
descriptor alongside their TLV payload. The fd is passed using the Unix
`SCM_RIGHTS` ancillary data mechanism: the server includes the fd in
the `sendmsg()` control data, and the client extracts it from the
`recvmsg()` result. The fd is not part of the TLV payload — it travels
as out-of-band ancillary data. The kernel duplicates the fd into the
receiving process's file descriptor table.

If multiple fd-carrying messages accumulate before the client reads
(e.g., several resizes), each `recvmsg()` returns one message with its
associated fd. The client processes them in order, mapping the latest
and closing any intermediate fds.

#### Control types: server → client (0x0100–0x01FF)

**Screen/shm messages (0x0100–0x010F):**

| Type | Name | Size | Purpose |
|------|------|------|---------|
| `0x0100` | Screen updated | 8 | Shm content has changed. Payload: `uint32_t sequence_number, uint32_t changes`. The changes bitfield indicates what parts of the shm were modified: `CELLS` (1) = cell content, `CURSOR_POS` (2) = cursor position, `TERMINAL_STATE` (4) = terminal flags, `MOUSE_POS` (8) = mouse pointer. Bits are cumulative across missed updates. Flow-controlled: only sent when no notification is pending for this client. |
| `0x0101` | Shm update | 8 | New shm segment. Payload: `uint32_t map_size, uint32_t flags`. Shm fd passed out-of-band via `SCM_RIGHTS`. The client must remap using the new fd and map_size. Screen dimensions and other state are in the shm header. Flag bits: `INITIAL` (1) = first shm after connect, `RESIZE` (2) = screen dimensions changed, `SESSION` (4) = active session changed, `OVERFLOW` (8) = overflow area grew. There is always a valid shm; if no terminal session is active, the cell count is 0. |
| `0x0102` | Bell | 0 | The terminal bell was rung. |

**Keyboard messages (0x0110–0x011F):**

| Type | Name | Size | Purpose |
|------|------|------|---------|
| `0x0110` | Key event offer | 0 or 7 | A key event for the client to accept. Normal (7 bytes): `uint16_t keycode, uint8_t value, uint32_t modifiers`. End-of-offers (0 bytes): server has stopped offering key events (see below). |

#### Control types: client → server (0x0200–0x02FF)

**Screen/update messages (0x0200–0x020F):**

| Type | Name | Size | Purpose |
|------|------|------|---------|
| `0x0200` | Update acknowledged | 4 | Client acknowledges receipt of screen update. Payload: `uint32_t sequence_number` (from the notification). |
| `0x0201` | Highlight region | 8 | Set visual highlight. Payload: `uint16_t left, uint16_t top, uint16_t right, uint16_t bottom`. |
| `0x0202` | Unhighlight | 0 | Clear visual highlight. |
| `0x0203` | Switch session | 2 | Request switch to session number. Payload: `uint16_t number`. |
| `0x0204` | Enable mouse tracking | 0 | Client requests mouse position updates. Without this, the server does not set `MOUSE_POS` in change bits. |
| `0x0205` | Disable mouse tracking | 0 | Client no longer needs mouse position updates. |

**Keyboard messages (0x0210–0x021F):**

| Type | Name | Size | Purpose |
|------|------|------|---------|
| `0x0210` | Key event accepted | 0 | Client acknowledges receipt of one or more key event offers. Must be sent within 1 second of the first unacknowledged offer. |
| `0x0211` | Enable key offers | 0 | Client requests the server to start offering key events. |
| `0x0212` | Disable key offers | 0 | Client requests the server to stop offering key events. |

**Input injection (0x0220–0x022F):**

| Type | Name | Size | Purpose |
|------|------|------|---------|
| `0x0220` | Key event injection | 8 | Inject a key event. Payload: `uint16_t keycode, uint8_t value, uint8_t padding, uint32_t modifiers`. Uses the Linux `input_event` keycode space. `value`: 0 = release, 1 = press, 2 = repeat. |
| `0x0221` | Character injection | 4 | Inject a Unicode character. Payload: `uint32_t codepoint` (UCS-4). The server delivers it to the terminal application. Used for braille input, clipboard paste, etc. |
| `0x0222` | Mouse click | 5 | Simulate a mouse click. Payload: `uint16_t col, uint16_t row, uint8_t button`. The client can check the mouse mode flag in terminal state flags (type `0x0003`, bit 5) to determine whether the application is listening for mouse events. |

Note: cursor routing is not a VTX server operation. Unlike a mouse click
which is a single event that the application interprets, cursor routing
is an iterative process. BRLTTY implements it as a state machine that
sends arrow key presses (via key injection, type `0x0220`) and monitors
the cursor position response (via type `0x0002` in shm or socket
updates). The routing algorithm handles overshooting, vertical scrolling
detection, timeouts, and multi-phase adjustment (vertical then
horizontal) entirely within BRLTTY. No dedicated cursor routing message
is needed.

#### Key events and character injection

The protocol distinguishes two forms of input injection:

- **Key event injection** (type `0x0220`): Carries a Linux keycode,
  press/release/repeat state, and modifier flags. The keycode and value
  fields use the same encoding as the Linux input subsystem (`struct
  input_event` from `<linux/input.h>`), ensuring key events can flow
  between the VTX server, BRLTTY, and the kernel input layer without
  translation. The server processes injected key events through xkbcommon
  with the full keymap context (dead keys, compose sequences, keyboard
  layout), just as it would for a physical keypress. This is used for
  keyboard event bounceback and cursor routing (arrow keys).

- **Character injection** (type `0x0221`): Carries a Unicode codepoint
  (UCS-4). The server delivers it to the terminal application
  master, bypassing xkbcommon. This is used for braille keyboard input,
  clipboard paste, and any other source that produces text rather than
  key events.

This separation preserves the full fidelity of key events throughout
the pipeline. A bounced-back key sequence replays exactly as if the
keys were pressed on the physical keyboard. Character data goes straight
to the application without being reinterpreted through a keymap.

Bracketed paste wrapping, if needed, is handled by BRLTTY before
sending character injection messages, informed by the bracketed paste
mode flag in the terminal state flags (type `0x0003`, bit 4).

### Keyboard input

#### Ownership model

kmscon owns the keyboard. It opens evdev devices directly and processes
key events through libxkbcommon. This is critical for robustness: if
BRLTTY crashes, keyboard input continues to work with no interruption
or reconfiguration.

#### First-refusal protocol

Key event offering is opt-in. By default, kmscon processes all keyboard
input itself. A client sends `Enable key offers` (type `0x0211`) when it
has keyboard bindings that need interception, and `Disable key offers`
(type `0x0212`) when it no longer needs them. If no keyboard bindings
are defined, the client never enables key offers and there is zero
overhead on the keyboard path.

When multiple clients have key offers enabled, they form a chain.
kmscon offers key events to the first client in the chain. That client
either consumes the key (binding matched) or injects it back (via type
`0x0200`). Injected keys from the first client are offered to the
second client, and so on. The last client's injections reach the
terminal emulator. If a client disconnects, it is removed from the
chain.

For a single client, the flow is:

1. kmscon reads a key event from evdev.
2. kmscon sends a `Key event offer` (type `0x0110`) to the client with
   the linux keycode, press/release/repeat state, and modifier flags.
3. The client responds with `Key event accepted` (type `0x0210`).
4. The client either consumes the key or injects it back via type
   `0x0200`, at which point kmscon feeds it to the terminal emulator.

From kmscon's perspective, every offered key is accepted. The client
takes full ownership of the key and decides what to do with it.

#### Key offer acknowledgement and timeout

The client must send `Key event accepted` (type `0x0210`) after
receiving one or more key event offers, no later than 1 second after
the first unacknowledged offer. The ack may cover multiple offers —
it simply confirms the client is alive and processing keys.

If the client fails to acknowledge within 1 second, the server
automatically disables key offers for that client, removes it from
the key handler chain, and sends a `Key event offer` with length=0
(the end-of-offers signal). This prevents a malfunctioning client
from silently sitting on all keyboard input. Subsequent key events
flow to the next client in the chain, or directly to the terminal
if no clients remain.

The end-of-offers signal (type `0x0110`, length=0) is also sent when
a client voluntarily disables key offers (type `0x0212`). This
confirms to the client that no more key events are queued and it is
safe to stop processing them. In both cases — timeout or voluntary
disable — the client must send `Enable key offers` (type `0x0211`)
again if it wishes to resume interception.

#### Key combination handling

BRLTTY implements its own key bindings, some of which are multi-key
combinations. When BRLTTY sees the beginning of a potential binding, it
accepts each key as it arrives. If subsequent keys complete the binding,
BRLTTY acts on it and the keys are consumed. If the combination does
not match any binding, BRLTTY sends the accumulated keys back to kmscon
as individual key event injection messages (type `0x0220`). kmscon
processes these through xkbcommon exactly as if they came from the
physical keyboard.

This mirrors BRLTTY's existing behavior with `EVIOCGRAB` and uinput,
where it grabs the keyboard, buffers potential binding keys, and
re-injects unmatched sequences through a virtual input device. The
latency characteristics are comparable.

### Session management

kmscon sessions map directly to BRLTTY's virtual terminal concept.
Each session has its own:

- `tsm_screen` instance (terminal state)
- shm segment (screen content)
- Active/inactive state

BRLTTY tracks which session is active via the `Active session number`
TLV field and the `Session switched` socket notification. When a session
switch occurs, the server sends the new session's shm fd over the VTX socket.
BRLTTY's `currentVirtualTerminal()` returns the active session number,
and `switchVirtualTerminal()` sends a `Switch session` request.

### Multiple clients

kmscon supports multiple simultaneous accessibility client connections.
This is a common scenario during BRLTTY development and testing, where
two BRLTTY instances run with different braille displays against the
same console.

- **Shared memory**: Read-only for all clients. Multiple readers can
  mmap the same segment concurrently with no conflict.
- **Socket notifications**: kmscon sends screen update and session
  notifications to all connected clients.
- **Key injection and character injection**: Any client can inject
  input. kmscon processes injections from all clients identically.
- **Key offers**: Key events are cascaded through all clients that have
  enabled key offers, forming a chain. kmscon offers key events to the
  first client. Key injections from the first client are offered to the
  second client (instead of going directly to the terminal). Key
  injections from the second client are offered to the third, and so
  on. The last client in the chain injects into the terminal emulator.
  If no clients have key offers enabled, key events go directly to the
  terminal emulator. Clients that have not enabled key offers are
  skipped in the chain.
- **Highlight regions**: Each client may set its own highlight region.
  kmscon renders all active highlights simultaneously, allowing a
  sighted helper to see where each braille display is positioned on
  screen.

### Area highlighting

BRLTTY can visually mark the region under the braille window on screen,
allowing a sighted helper to see where the braille user is reading.
BRLTTY sends a `Highlight region` message with the bounding rectangle.
kmscon applies inverse or distinct color attributes to those cells
during visual rendering, similar to how libtsm already renders
selections via `attr.inverse` in the draw callback.
`Unhighlight` clears the region.

## Security

The shm segment exposes the full terminal content — including passwords
and sensitive data as they are displayed — and the control socket allows
keystroke injection. Access control is critical.

### Socket permissions

The `/run/vtx/` directory is owned by `root:vtx` with mode `0750`.
Sockets within it are created with mode `0660` and group `vtx`. Only
processes running as root or in the `vtx` group can connect. The kernel
enforces supplementary group membership on `connect()`, so BRLTTY does
not need to run as root — membership in the `vtx` group is sufficient.

BRLTTY runs as a system service (started at boot, before any user logs
in) with the appropriate group membership configured via its systemd
unit (`SupplementaryGroups=vtx`).

### Peer credential verification

When a client connects, kmscon can retrieve the peer credentials via
`SO_PEERCRED` (`getsockopt` with `SOL_SOCKET`), which returns the
peer's uid, gid, and pid as reported by the kernel. Note that this only
provides the primary gid, not supplementary groups. It is therefore
useful for logging and uid-level policy, but the socket filesystem
permissions are the primary gate for group-based access control.

### Anonymous shared memory

The shm segments are created using `memfd_create()` rather than
`shm_open()`. This produces anonymous memory with no filesystem name —
the segment cannot be discovered or opened by other processes by name.
The only way to access it is to receive the file descriptor over the
authenticated socket connection.

This provides defense in depth: even if a process somehow learns that
a shm segment exists, it cannot open it without the fd. The fd is only
sent to clients that have passed the socket permission checks.

### Why not seat-based access control

systemd-logind provides seat-based access for devices like audio and
video hardware — granting access to the active session on a seat. While
kmscon is inherently seat-aware, this model does not apply to BRLTTY:
BRLTTY is a system service that must be running before any user session
exists (to enable login via braille display). It does not belong to any
logind session and cannot rely on seat-based grants.

Group-based access is the appropriate mechanism for system services that
need persistent access to console resources regardless of session state.

### Summary of access control layers

1. **Socket filesystem permissions**: group-based access control on the
   socket path, enforced by the kernel including supplementary groups.
2. **Peer credentials**: kmscon can verify the connecting process
   identity for logging and additional policy.
3. **Anonymous shm**: no filesystem name, fd only available to
   authenticated clients.

## Alternatives considered

### Alternative 1: Emulate /dev/vcsa from kmscon

kmscon could expose a virtual `/dev/vcsa*` device (via FUSE or a
character device) that mirrors the `tsm_screen` state. BRLTTY's
existing Linux screen driver would work without modification.

**Why we discarded this:**

- The vcsa format is limited — originally 8-bit characters plus VGA
  attributes. The unicode device (`/dev/vcsu*`) provides 32-bit
  codepoints but no attributes; BRLTTY must read both devices.
- vcsa only provides screen content. BRLTTY also needs VT ioctls
  (`VT_GETSTATE`, `KDGETMODE`, etc.) which go through `/dev/tty*`.
  Emulating this ioctl surface is large and fragile.
- BRLTTY's Linux driver polls vcsa with `POLLPRI`, which would need
  to be emulated.
- It forces a modern Unicode-native terminal into a legacy VGA model.
- Any vcsa reader would still receive the full screen on every read.

### Alternative 2: Socket-only protocol (no shared memory)

Inspired by BRLTTY's tmux driver, which communicates entirely through
pipes. kmscon could expose a control-mode protocol and BRLTTY would
request screen content over the socket.

**Why we discarded this:**

- The tmux driver performs a full `capture-pane` on every update,
  transferring the entire screen content even when only one character
  changed and even when the braille display shows a different region.
- For a primary console, this inefficiency is significant.
- Shared memory allows BRLTTY to read only the cells it needs (the
  braille window) with zero IPC overhead on the read path.
- We kept the socket for control operations and notifications, where
  its request/response model is appropriate.

### Alternative 3: BRLTTY owns the keyboard

BRLTTY grabs the keyboard via `EVIOCGRAB` as it does today, and
forwards unhandled keys over the VTX control socket.

**Why we discarded this:**

- If BRLTTY crashes, kmscon loses all keyboard input. The terminal
  becomes unusable until BRLTTY is restarted. This is a critical
  robustness failure for a system console.
- Similarly, BRLTTY's uinput-based re-injection creates a dependency:
  kmscon would read from a virtual device that disappears when BRLTTY
  exits.
- The chosen design (kmscon owns the keyboard, offers keys to BRLTTY)
  degrades gracefully: BRLTTY disconnection has zero impact on terminal
  usability.

### Alternative 4: Fixed maximum shm allocation

Allocate shared memory for the largest plausible screen (e.g., 256×256)
to avoid remapping on resize.

**Why we discarded this:**

- Real-world configurations exceed any reasonable "maximum". Users run
  3840×2160 displays with 8×8 fonts, yielding 480×270 cells.
  Future displays and smaller fonts could push this further.
- The fd-passing approach for resize (create new segment, pass fd over
  socket) is clean and imposes no artificial limits.

### Alternative 5: Shared library for terminal emulators and accessibility tools

Implement the screen export and access protocol as a shared library
that any terminal emulator could link against (writer side) and any
accessibility tool could link against (reader side).

**Why we discarded this:**

- Each terminal emulator has its own internal screen state
  representation. kmscon uses libtsm with a draw callback. Other
  emulators (foot, alacritty, kitty) have completely different
  architectures. A shared library would either impose a common internal
  format (invasive, unlikely to be accepted upstream) or provide an
  adapter layer that each emulator populates — which is essentially the
  shm cell format we already define, with extra ceremony.
- The writer side is a few hundred lines of straightforward code:
  create shm, write cells, send socket notifications. The reader side
  is similarly simple: mmap, parse TLV, read cells. Wrapping this in a
  library adds a dependency, a versioning burden, and an API surface to
  maintain for very little code reuse.
- A shared library introduces practical problems: packaging
  dependencies across distributions, ABI stability commitments, version
  skew between the library and its consumers, and a governance question
  about who maintains it.
- The real interoperability contract is the **protocol** — the shm
  format, TLV types, and socket message definitions documented in this
  specification. Any terminal emulator can implement the writer side
  and any accessibility tool can implement the reader side by following
  the spec, without depending on a shared library.
- Reference implementations or helper code can be provided as copyable
  source files or header-only utilities without the constraints of a
  shared library.

## Implementation roadmap

### Phase 1: kmscon side

1. **Screen export renderer**: New source file in `src/` implementing
   the shm writer as a `tsm_screen_draw()` consumer. Hooks into the
   existing `redraw_all()` cycle.

2. **Shared memory management**: Create/destroy anonymous shm segments
   (`memfd_create`) per session. Handle resize by creating new segments
   and passing fds over the socket.

3. **Control socket listener**: Unix domain socket accepting BRLTTY
   connections. Handle key injection, session switching, highlight
   requests.

4. **Keyboard first-refusal**: When BRLTTY enables key offers, offer
   key events before processing. Fall back to normal processing on
   disconnect.

5. **Build integration**: New meson option (e.g., `screen_export` or
   `accessibility`) to enable/disable the feature and its dependencies.

### Phase 2: BRLTTY side

1. **New screen driver**: `Drivers/Screen/Vtx/screen.c` implementing
   the `BaseScreen` interface.

2. **shm reader**: Map the shm segment read-only. Implement
   `readCharacters()` by reading directly from the shm cell array.
   No shadow screen copy needed.

3. **Socket client**: Connect to the VTX socket. Register for
   async I/O notifications. Implement key event and character injection,
   session switching, highlight, and keyboard event handling.

4. **Driver registration**: Add to BRLTTY's screen driver list and
   build system.

### Phase 3: Refinements

- Negotiation of optional capabilities at connection time

## Open questions

1. **libtsm changes**: Adding a direct cell-read API to libtsm (instead
   of relying solely on the draw callback) would simplify the export
   renderer. Is the libtsm maintainer open to this?

2. **Upstream acceptance**: Both projects need to agree on the protocol.
   A shared header file defining the shm and socket message formats
   would ensure consistency.
