Skip to content

Commit f73fa07

Browse files
committed
✨ add termcodes module and make CursorEvent 1-based
Rename csi.ts to termcodes.ts and add ESC(), SHOWCURSOR(), HIDECURSOR(), ALTSCREEN(), and MAINSCREEN() helpers. Make CursorEvent.row/column 1-based to match DSR native format. Replace all raw escape sequences in the demo with termcodes. Use DECSC/DECRC (ESC 7/8) for cursor save/restore instead of SCO (CSI s/u).
1 parent 9f838c1 commit f73fa07

7 files changed

Lines changed: 159 additions & 55 deletions

File tree

demo/inline-region.ts

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,23 @@ import {
1313
close,
1414
createInput,
1515
createTerm,
16+
CSI,
1617
type CursorEvent,
18+
DSR,
19+
ESC,
1720
fixed,
1821
grow,
1922
type Op,
2023
open,
2124
rgba,
25+
SHOWCURSOR,
2226
text,
2327
} from "../mod.ts";
2428
import { cursor, settings } from "../settings.ts";
2529
import { validated } from "../validate.ts";
2630

27-
let write = (b: Uint8Array) => Deno.stdout.writeSync(b);
2831
let encode = (s: string) => new TextEncoder().encode(s);
29-
let esc = (s: string) => write(encode(s));
32+
let write = (b: Uint8Array) => Deno.stdout.writeSync(b);
3033

3134
let GREEN = rgba(80, 250, 123);
3235
let GRAY = rgba(100, 100, 100);
@@ -44,7 +47,7 @@ let BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "
4447

4548
function* queryCursor(): Operation<CursorEvent> {
4649
let parser = yield* until(createInput({ escLatency: 100 }));
47-
esc("\x1b[6n");
50+
write(DSR());
4851

4952
let buf = new Uint8Array(32);
5053
while (true) {
@@ -67,7 +70,7 @@ function waitKey() {
6770
for (let i = 0; i < n; i++) {
6871
if (buf[i] === 0x03) {
6972
Deno.stdin.setRaw(false);
70-
esc("\x1b[?25h");
73+
write(SHOWCURSOR());
7174
Deno.exit(0);
7275
}
7376
}
@@ -111,13 +114,13 @@ function* transaction(
111114
): Operation<void> {
112115
let { columns } = Deno.consoleSize();
113116

114-
esc("\n".repeat(height));
117+
write(encode("\n".repeat(height)));
115118

116119
let pos = yield* queryCursor();
117120
/** 1-based terminal row where the region starts */
118-
let row = pos.top - height + 1;
121+
let row = pos.row - height + 1;
119122

120-
esc("\x1b[s");
123+
write(ESC("7"));
121124
let tty = settings(cursor(false));
122125
write(tty.apply);
123126

@@ -131,17 +134,17 @@ function* transaction(
131134
}
132135

133136
write(tty.revert);
134-
esc("\x1b[u");
135-
esc("\n");
137+
write(ESC("8"));
138+
write(encode("\n"));
136139
}
137140

138141
function say(msg: string) {
139-
esc(msg + "\n");
142+
write(encode(msg + "\n"));
140143
}
141144

142145
function pause() {
143146
waitKey();
144-
esc("\n");
147+
write(encode("\n"));
145148
}
146149

147150
await main(function* () {
@@ -157,13 +160,13 @@ await main(function* () {
157160
say("");
158161

159162
// Demo 1: Spinner box
160-
esc("\n\n\n");
163+
write(encode("\n\n\n"));
161164

162165
let pos = yield* queryCursor();
163166
/** 1-based terminal row where the region starts */
164-
let row = pos.top - 2;
167+
let row = pos.row - 2;
165168

166-
esc("\x1b[s");
169+
write(ESC("7"));
167170

168171
let frames = 30;
169172
let term = validated(
@@ -195,14 +198,16 @@ await main(function* () {
195198
yield* sleep(80);
196199
}
197200

198-
esc("\x1b[u");
199-
esc("\x1b[0m");
200-
esc("\n");
201+
write(ESC("8"));
202+
write(CSI("0m"));
203+
write(encode("\n"));
201204

202205
yield* sleep(500);
203206

204-
esc(
205-
"\nRegions can be multi-line, but they can be a single line too. (continue...)",
207+
write(
208+
encode(
209+
"\nRegions can be multi-line, but they can be a single line too. (continue...)",
210+
),
206211
);
207212
pause();
208213

@@ -251,9 +256,9 @@ await main(function* () {
251256
50,
252257
);
253258

254-
esc("\x1b[0m");
259+
write(CSI("0m"));
255260
yield* sleep(500);
256-
esc("\nGoodbye sadness with limitless sky. (continue...)");
261+
write(encode("\nGoodbye sadness with limitless sky. (continue...)"));
257262
pause();
258263

259264
// Demo 3: Nyan cat
@@ -331,7 +336,8 @@ await main(function* () {
331336
60,
332337
);
333338

334-
esc("\x1b[0m\n");
339+
write(CSI("0m"));
340+
write(encode("\n"));
335341
write(tty.revert);
336342
Deno.stdin.setRaw(false);
337343
});

input.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -361,14 +361,14 @@ export interface CursorEvent {
361361
type: "cursor";
362362

363363
/**
364-
* Cursor row (0-based).
364+
* Cursor row (1-based). Matches ECMA-48 DSR native format.
365365
*/
366-
top: number;
366+
row: number;
367367

368368
/**
369-
* Cursor column (0-based).
369+
* Cursor column (1-based). Matches ECMA-48 DSR native format.
370370
*/
371-
left: number;
371+
column: number;
372372
}
373373

374374
import type { PointerEvent } from "./term.ts";
@@ -682,7 +682,7 @@ function mapEvent(native: NativeInputEvent): InputEvent {
682682
return { type: "resize", width: native.w, height: native.h };
683683
}
684684
case EVENT_CURSOR: {
685-
return { type: "cursor", top: native.y, left: native.x };
685+
return { type: "cursor", row: native.y, column: native.x };
686686
}
687687
default: {
688688
return mapKeyEvent(native);

mod.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export * from "./ops.ts";
22
export * from "./term.ts";
33
export * from "./input.ts";
4+
export * from "./settings.ts";
5+
export * from "./termcodes.ts";

settings.ts

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
import {
2+
ALTSCREEN,
3+
CSI,
4+
ESC,
5+
HIDECURSOR,
6+
MAINSCREEN,
7+
SHOWCURSOR,
8+
} from "./termcodes.ts";
9+
110
export interface Setting {
211
apply: Uint8Array;
312
revert: Uint8Array;
@@ -12,47 +21,50 @@ export function settings(...sequence: Setting[]): Setting {
1221

1322
export function alternateBuffer(): Setting {
1423
return {
15-
apply: csi("?1049h"),
16-
revert: csi("?1049l"),
24+
apply: ALTSCREEN(),
25+
revert: MAINSCREEN(),
1726
};
1827
}
1928

2029
export function cursor(visible: boolean): Setting {
2130
if (visible) {
2231
return {
23-
apply: csi("?25h"),
24-
revert: csi("?25l"),
32+
apply: SHOWCURSOR(),
33+
revert: HIDECURSOR(),
2534
};
2635
} else {
2736
return {
28-
apply: csi("?25l"),
29-
revert: csi("?25h"),
37+
apply: HIDECURSOR(),
38+
revert: SHOWCURSOR(),
3039
};
3140
}
3241
}
3342

34-
export function progressiveInput(level: number): Setting {
43+
/**
44+
* Save and restore cursor position using DECSC (`ESC 7`) / DECRC (`ESC 8`).
45+
*
46+
* @see {@link https://vt100.net/docs/vt510-rm/DECSC.html | VT510 DECSC}
47+
* @see {@link https://vt100.net/docs/vt510-rm/DECRC.html | VT510 DECRC}
48+
*/
49+
export function saveCursorPosition(): Setting {
3550
return {
36-
apply: csi(`>${level}u`),
37-
revert: csi("<u"),
51+
apply: ESC("7"),
52+
revert: ESC("8"),
3853
};
3954
}
4055

41-
export function mouseTracking(): Setting {
56+
export function progressiveInput(level: number): Setting {
4257
return {
43-
apply: concat([csi("?1003h"), csi("?1006h")]),
44-
revert: concat([csi("?1006l"), csi("?1003l")]),
58+
apply: CSI(`>${level}u`),
59+
revert: CSI("<u"),
4560
};
4661
}
4762

48-
let encoder = new TextEncoder();
49-
50-
function encode(str: string): Uint8Array {
51-
return encoder.encode(str);
52-
}
53-
54-
function csi(str: string): Uint8Array {
55-
return encode(`\x1b[${str}`);
63+
export function mouseTracking(): Setting {
64+
return {
65+
apply: concat([CSI("?1003h"), CSI("?1006h")]),
66+
revert: concat([CSI("?1006l"), CSI("?1003l")]),
67+
};
5668
}
5769

5870
function concat(arrays: Uint8Array[]): Uint8Array {

src/input.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -604,8 +604,8 @@ static int parse_cursor(struct InputState *st, struct InputEvent *ev) {
604604
return PARSE_ERR;
605605
i++;
606606
ev->type = EVENT_CURSOR;
607-
ev->y = row - 1;
608-
ev->x = col - 1;
607+
ev->y = row;
608+
ev->x = col;
609609
shift(st, i);
610610
return PARSE_OK;
611611
} else {

termcodes.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* Encode a plain escape sequence.
3+
*
4+
* Prepends `ESC` (`\x1b`) to the given string and returns the result as bytes.
5+
*
6+
* @see {@link https://www.ecma-international.org/publications-and-standards/standards/ecma-48/ | ECMA-48}
7+
*/
8+
export function ESC(str: string): Uint8Array {
9+
return encode(`\x1b${str}`);
10+
}
11+
12+
/**
13+
* Encode a Control Sequence Introducer (CSI) command.
14+
*
15+
* Prepends `ESC[` to the given string and returns the result as bytes.
16+
*
17+
* @see {@link https://www.ecma-international.org/publications-and-standards/standards/ecma-48/ | ECMA-48}
18+
*/
19+
export function CSI(str: string): Uint8Array {
20+
return ESC(`[${str}`);
21+
}
22+
23+
/**
24+
* Request the cursor position via Device Status Report (DSR).
25+
*
26+
* Sends `CSI 6n`. The terminal responds with a Cursor Position Report
27+
* (`CSI row ; column R`) where row and column are 1-based.
28+
*
29+
* @see {@link https://www.ecma-international.org/publications-and-standards/standards/ecma-48/ | ECMA-48}
30+
*/
31+
export function DSR(): Uint8Array {
32+
return CSI("6n");
33+
}
34+
35+
/**
36+
* Show the cursor (DECTCEM set).
37+
*
38+
* DEC private mode 25. Not part of ECMA-48; originates from the VT220.
39+
*
40+
* @see {@link https://vt100.net/docs/vt510-rm/DECTCEM.html | VT510 DECTCEM}
41+
*/
42+
export function SHOWCURSOR(): Uint8Array {
43+
return CSI("?25h");
44+
}
45+
46+
/**
47+
* Hide the cursor (DECTCEM reset).
48+
*
49+
* DEC private mode 25. Not part of ECMA-48; originates from the VT220.
50+
*
51+
* @see {@link https://vt100.net/docs/vt510-rm/DECTCEM.html | VT510 DECTCEM}
52+
*/
53+
export function HIDECURSOR(): Uint8Array {
54+
return CSI("?25l");
55+
}
56+
57+
/**
58+
* Switch to the alternate screen buffer (xterm private mode 1049).
59+
*
60+
* Saves the cursor and switches to a clean alternate screen. Use
61+
* {@link MAINSCREEN} to switch back.
62+
*
63+
* @see {@link https://invisible-island.net/xterm/ctlseqs/ctlseqs.html | xterm control sequences}
64+
*/
65+
export function ALTSCREEN(): Uint8Array {
66+
return CSI("?1049h");
67+
}
68+
69+
/**
70+
* Switch back to the main screen buffer (xterm private mode 1049).
71+
*
72+
* Restores the cursor and returns to the main screen with scrollback intact.
73+
*
74+
* @see {@link https://invisible-island.net/xterm/ctlseqs/ctlseqs.html | xterm control sequences}
75+
*/
76+
export function MAINSCREEN(): Uint8Array {
77+
return CSI("?1049l");
78+
}
79+
80+
const encoder = new TextEncoder();
81+
82+
function encode(str: string): Uint8Array {
83+
return encoder.encode(str);
84+
}

test/input.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -687,8 +687,8 @@ describe("input", () => {
687687
expect(result.events.length).toBe(1);
688688
expect(result.events[0]).toMatchObject({
689689
type: "cursor",
690-
top: 23,
691-
left: 79,
690+
row: 24,
691+
column: 80,
692692
});
693693
});
694694

@@ -697,8 +697,8 @@ describe("input", () => {
697697
expect(result.events.length).toBe(1);
698698
expect(result.events[0]).toMatchObject({
699699
type: "cursor",
700-
top: 0,
701-
left: 0,
700+
row: 1,
701+
column: 1,
702702
});
703703
});
704704

@@ -708,8 +708,8 @@ describe("input", () => {
708708
expect(result.events[0]).toMatchObject({ type: "keydown", key: "a" });
709709
expect(result.events[1]).toMatchObject({
710710
type: "cursor",
711-
top: 9,
712-
left: 4,
711+
row: 10,
712+
column: 5,
713713
});
714714
expect(result.events[2]).toMatchObject({ type: "keydown", key: "b" });
715715
});

0 commit comments

Comments
 (0)