Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion examples/keyboard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,19 @@ await main(function* () {
pointer.state = undefined;
}

let { output, events } = term.render(keyboard(context), {
let { output, events, cursor } = term.render(keyboard(context), {
pointer: pointer.state,
trackCursor: true,
});

for (let event of events) {
yield* pointer.events.send(event);
}

writeStdout(output);
if (cursor) {
writeStdout(cursor);
}

yield* each.next();
}
Expand Down Expand Up @@ -229,6 +233,7 @@ function key(ops: Op[], k: KeyDef, ctx: AppContext): void {
alignY: "center",
},
bg,
cursor: "pointer",
border: hover
? { color: highlight, left: 1, right: 1, top: 1, bottom: 1 }
: undefined,
Expand Down
13 changes: 13 additions & 0 deletions input-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const EVENT_KEY = 1;
export const EVENT_MOUSE = 2;
export const EVENT_RESIZE = 3;
export const EVENT_CURSOR = 4;
export const EVENT_POINTERSHAPE = 5;

export const MOD_ALT = 1;
export const MOD_CTRL = 2;
Expand Down Expand Up @@ -89,6 +90,7 @@ import {
} from "./typedef.ts";

const MAX_TEXT_CODEPOINTS = 8;
const MAX_REPORT_BYTES = 64;

const InputEventLayout = struct({
type: uint8(),
Expand All @@ -104,6 +106,8 @@ const InputEventLayout = struct({
base: uint32(),
text: array(uint32(), MAX_TEXT_CODEPOINTS),
text_len: uint8(),
report_len: uint16(),
report: array(uint8(), MAX_REPORT_BYTES),
});

const {
Expand All @@ -120,6 +124,8 @@ const {
shifted: OFFSET_SHIFTED,
base: OFFSET_BASE,
text: OFFSET_TEXT,
report_len: OFFSET_REPORT_LEN,
report: OFFSET_REPORT,
} = offsets(InputEventLayout);

export interface NativeInputEvent {
Expand All @@ -135,6 +141,7 @@ export interface NativeInputEvent {
shifted: number;
base: number;
text: number[];
report: number[];
}

export function readEvent(view: DataView, ptr: number): NativeInputEvent {
Expand All @@ -143,6 +150,11 @@ export function readEvent(view: DataView, ptr: number): NativeInputEvent {
for (let i = 0; i < len && i < MAX_TEXT_CODEPOINTS; i++) {
text.push(view.getUint32(ptr + OFFSET_TEXT + i * 4, true));
}
let reportLen = view.getUint16(ptr + OFFSET_REPORT_LEN, true);
let report: number[] = [];
for (let i = 0; i < reportLen && i < MAX_REPORT_BYTES; i++) {
report.push(view.getUint8(ptr + OFFSET_REPORT + i));
}
return {
type: view.getUint8(ptr + OFFSET_TYPE),
mod: view.getUint8(ptr + OFFSET_MOD),
Expand All @@ -156,6 +168,7 @@ export function readEvent(view: DataView, ptr: number): NativeInputEvent {
shifted: view.getUint32(ptr + OFFSET_SHIFTED, true),
base: view.getUint32(ptr + OFFSET_BASE, true),
text,
report,
};
}

Expand Down
24 changes: 24 additions & 0 deletions input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
EVENT_CURSOR,
EVENT_KEY,
EVENT_MOUSE,
EVENT_POINTERSHAPE,
EVENT_RESIZE,
KEY_ALT_LEFT,
KEY_ALT_RIGHT,
Expand Down Expand Up @@ -371,6 +372,22 @@ export interface CursorEvent {
column: number;
}

/**
* Reply to an OSC 22 mouse-pointer-shape query.
*
* Emitted when the terminal answers a pointer-shape query (sent by the
* output side) on the input stream. The `report` is the raw payload the
* terminal returned between `OSC 22 ;` and the terminator — for example a
* shape name (`"pointer"`), `"0"` for an empty stack, or a comma-separated
* list of `1`/`0` support flags. The parser does not interpret it;
* correlating a reply with the query that produced it is the caller's
* responsibility.
*/
export interface PointerShapeEvent {
type: "pointershape";
report: string;
}

import type { PointerEvent } from "./term.ts";

export type InputEvent =
Expand All @@ -381,6 +398,7 @@ export type InputEvent =
| WheelEvent
| ResizeEvent
| CursorEvent
| PointerShapeEvent
| PointerEvent;

/**
Expand Down Expand Up @@ -684,6 +702,12 @@ function mapEvent(native: NativeInputEvent): InputEvent {
case EVENT_CURSOR: {
return { type: "cursor", row: native.y, column: native.x };
}
case EVENT_POINTERSHAPE: {
return {
type: "pointershape",
report: new TextDecoder().decode(new Uint8Array(native.report)),
};
}
default: {
return mapKeyEvent(native);
}
Expand Down
15 changes: 15 additions & 0 deletions ops.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { CursorShape } from "./termcodes.ts";

/* Command buffer opcodes — mirrors ops.h */
const OP_OPEN_ELEMENT = 0x02;
const OP_TEXT = 0x03;
Expand Down Expand Up @@ -323,6 +325,14 @@ export interface OpenElement {
bottom?: number;
};
clip?: { horizontal?: boolean; vertical?: boolean };
/**
* Mouse pointer shape to request while the pointer is over this element.
*
* This is a pure annotation: it does not affect layout or output and is not
* sent to the WASM module. It is consumed only when pointer-shape tracking is
* enabled via the `trackCursor` render option.
*/
cursor?: CursorShape;
floating?: {
x?: number;
y?: number;
Expand Down Expand Up @@ -436,6 +446,11 @@ export function close(): CloseElement {
return { directive: OP_CLOSE_ELEMENT };
}

/** Narrow an `Op` to an element-open directive. */
export function isOpen(op: Op): op is OpenElement {
return op.directive === OP_OPEN_ELEMENT;
}

function packSize(ops: Op[]): number {
let n = 0;
for (let op of ops) {
Expand Down
59 changes: 59 additions & 0 deletions specs/input-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ surface and guide future stabilization.
- The scan API and its return type
- The `InputEvent` discriminated union and its variants
- The ESC timeout resolution model
- Decoding inbound OSC 22 mouse-pointer-shape replies into events

### Out of scope

Expand Down Expand Up @@ -128,11 +129,69 @@ current variants are:
- **`ResizeEvent`** (`type: "resize"`) — A terminal resize notification. Fields
include `columns` and `rows`.

- **`PointerShapeEvent`** (`type: "pointershape"`) — A terminal reply to an OSC
22 mouse-pointer-shape query. Carries a single `report` field: the raw payload
string the terminal returned between `OSC 22 ;` and the string terminator. The
parser does not interpret the payload and does not correlate it with any
outstanding query; correlation is the caller's responsibility. See Section
5.1.

The discriminant values and the type splits are deliberate design decisions.
However, the field sets within each variant are expected to grow when Kitty
progressive enhancement types are surfaced in the TypeScript layer (the C struct
has already been extended with fields that are not yet mapped to the TS types).

### 5.1 Pointer shape reports (OSC 22)

> **Status:** Implemented. Code conforms to the spec, not the reverse (see
> AGENTS.md).

Some terminals implement the OSC 22 mouse-pointer-shape protocol, under which an
application can _query_ the terminal's current pointer shape or its support for
named shapes. The terminal answers a query with a reply on the input stream. The
input parser recognizes these replies and surfaces them as `PointerShapeEvent`s.

The parser's role is strictly inbound decoding. It never sends OSC 22 queries
and never sets the pointer shape — emitting OSC 22 is an output concern
specified separately (see [Renderer Specification](renderer-spec.md)). This
preserves the parser's independence from the renderer (INV-8): the parser only
decodes bytes it is given.

**Recognized reply grammar.** A reply has the form:

```
ESC ] 22 ; <payload> <terminator>
```

where `<terminator>` is either ST (`ESC \`, bytes `0x1B 0x5C`) or BEL (`0x07`),
and `<payload>` is the run of bytes up to the terminator. The parser emits one
`PointerShapeEvent` per complete reply, with `report` set to the decoded
`<payload>` string. Payloads are truncated to 64 bytes; this comfortably fits
any shape name and a support-query reply for a reasonable number of shapes. The
parser does not validate or interpret the payload. Per the kitty pointer-shape
protocol the payload may be:

- a shape name (reply to `?__current__`, `?__default__`, or `?__grabbed__`),
- `0` (current-shape query when the shape stack is empty), or
- a comma-separated list of `1`/`0` flags (reply to a support query of the form
`?name1,name2,...`).

Which interpretation applies depends on the query the caller sent; the parser
does not track outstanding queries, so the caller is responsible for that
correlation.

**Graceful degradation.** Terminals that do not implement the query side of OSC
22 (for example, set-only implementations) never send a reply, and therefore
never produce a `PointerShapeEvent`. A caller that issues a query and receives
no event within a timeout MUST treat the feature as unsupported. Absence of the
event is the contract for unsupported terminals; it is not an error.

**Incremental bytes.** An OSC 22 reply split across multiple `scan()` calls is
buffered like any other escape sequence and surfaced as a single event once the
terminator arrives. A lone `ESC` does not apply here: the `]` that follows
disambiguates immediately, so OSC 22 replies do not participate in ESC timeout
resolution.

---

## 6. Deferred / Future Areas
Expand Down
Loading
Loading