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
41 changes: 37 additions & 4 deletions demo/keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const highlight = rgba(255, 220, 80);

const KEY_W = 5;
const GAP = 1;
const WRITE_CHUNK_SIZE = 1024;

interface KeyDef {
label: string;
Expand All @@ -60,6 +61,33 @@ function matches(k: KeyDef, event: InputEvent | PointerEvent): boolean {

const hovered = rgba(80, 80, 100);

function writeOutput(output: Uint8Array): void {
if (Deno.build.os !== "windows") {
Deno.stdout.writeSync(output);
return;
}

// VS Code's Windows terminal path can corrupt large fullscreen writes from
// Deno, so flush complete rendered rows instead of one large write.
let start = 0;
let lastBreak = -1;

for (let i = 0; i < output.length; i++) {
if (output[i] === 0x0a) {
lastBreak = i + 1;
}

if (i - start + 1 >= WRITE_CHUNK_SIZE && lastBreak > start) {
Deno.stdout.writeSync(output.subarray(start, lastBreak));
start = lastBreak;
}
}

if (start < output.length) {
Deno.stdout.writeSync(output.subarray(start));
}
}

function key(ops: Op[], k: KeyDef, ctx: AppContext): void {
let pressed = ctx.event && matches(k, ctx.event);
let hover = ctx.entered.has(`key:${k.code}`);
Expand Down Expand Up @@ -567,11 +595,12 @@ await main(function* () {
? Deno.consoleSize()
: { columns: 80, rows: 24 };

Deno.stdin.setRaw(true);
if (Deno.stdin.isTerminal()) {
Deno.stdin.setRaw(true);
}

let stdin = yield* useStdin();
let input = useInput(stdin);

let term = yield* until(createTerm({ width: columns, height: rows }));

let tty = settings(alternateBuffer(), cursor(false));
Expand All @@ -584,13 +613,17 @@ await main(function* () {
Deno.stdout.writeSync(flags.apply);

yield* ensure(() => {
// Restore so Backspace and normal shell editing work after exit.
if (Deno.stdin.isTerminal()) {
Deno.stdin.setRaw(false);
}
Deno.stdout.writeSync(flags.revert);
Deno.stdout.writeSync(tty.revert);
});

let { output } = term.render(keyboard(context));

Deno.stdout.writeSync(output);
writeOutput(output);

let pointer = {
events: createChannel<PointerEvent, void>(),
Expand Down Expand Up @@ -640,7 +673,7 @@ await main(function* () {
yield* pointer.events.send(event);
}

Deno.stdout.writeSync(output);
writeOutput(output);

yield* each.next();
}
Expand Down
7 changes: 6 additions & 1 deletion input-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,12 @@ export async function createInputNative(
let memory = new WebAssembly.Memory({ initial: 4 });

let instance = await WebAssembly.instantiate(compiled, {
env: { memory },
env: {
memory,
debugLog(_ptr: number, _len: number) {
// no-op debug logger for wasm imports
},
},
clay: {
measureTextFunction() {},
queryScrollOffsetFunction(ret: number) {
Expand Down
7 changes: 6 additions & 1 deletion term-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ export async function createTermNative(
let exports: Record<string, CallableFunction> = {};

let instance = await WebAssembly.instantiate(compiled, {
env: { memory },
env: {
memory,
debugLog(_ptr: number, _len: number) {
// no-op debug logger for wasm imports
},
},
clay: {
measureTextFunction(
ret: number,
Expand Down
73 changes: 72 additions & 1 deletion term.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,59 @@ export interface ElementInfo {
bounds: BoundingBox;
}

const WINDOWS_WRAP_DISABLE = new Uint8Array([0x1b, 0x5b, 0x3f, 0x37, 0x6c]);
const WINDOWS_WRAP_ENABLE = new Uint8Array([0x1b, 0x5b, 0x3f, 0x37, 0x68]);

function normalizeWindowsLineOutput(output: Uint8Array): Uint8Array {
// Windows fullscreen line-mode output needs an explicit home cursor move and
// CRLF row separators; bare LF can leave the cursor in the wrong column and
// visually clip later rows in some terminal stacks.
let extra = 0;
for (let i = 0; i < output.length; i++) {
if (output[i] === 0x0a && (i === 0 || output[i - 1] !== 0x0d)) {
extra++;
}
}

let prefix = new Uint8Array([
...WINDOWS_WRAP_DISABLE,
0x1b,
0x5b,
0x48,
]);
let suffix = WINDOWS_WRAP_ENABLE;
let normalized = new Uint8Array(
prefix.length + output.length + extra + suffix.length,
);
normalized.set(prefix, 0);

let offset = prefix.length;
for (let i = 0; i < output.length; i++) {
let byte = output[i];
if (byte === 0x0a && (i === 0 || output[i - 1] !== 0x0d)) {
normalized[offset++] = 0x0d;
}
normalized[offset++] = byte;
}

normalized.set(suffix, offset);

return normalized as Uint8Array;
}

function wrapWindowsFullscreenOutput(output: Uint8Array): Uint8Array {
// Disabling autowrap around a fullscreen frame avoids Windows terminal
// redraw quirks observed at the right edge.
// xterm defines CSI ? 7 h / CSI ? 7 l as auto-wrap on/off.
let wrapped = new Uint8Array(
WINDOWS_WRAP_DISABLE.length + output.length + WINDOWS_WRAP_ENABLE.length,
);
wrapped.set(WINDOWS_WRAP_DISABLE, 0);
wrapped.set(output, WINDOWS_WRAP_DISABLE.length);
wrapped.set(WINDOWS_WRAP_ENABLE, WINDOWS_WRAP_DISABLE.length + output.length);
return wrapped;
}

const ERROR_TYPES = [
"TEXT_MEASUREMENT_FUNCTION_NOT_PROVIDED",
"ARENA_CAPACITY_EXCEEDED",
Expand Down Expand Up @@ -84,19 +137,37 @@ export async function createTerm(options: TermOptions): Promise<Term> {
let len = pack(ops, memory.buffer, opsBuf, memory.buffer.byteLength);
let mode = options?.mode === "line" ? 1 : 0;
let row = options?.row ?? 1;
let autoLineMode = false;
let windowsFullscreen = row === 1 && Deno.build.os === "windows";

// Windows terminals have historically been less reliable with many
// absolute cursor CUP updates in full-screen diff mode. Use the
// line-oriented render path by default on Windows for fullscreen
// layouts to improve redraw reliability.
if (mode === 0 && options?.mode === undefined && windowsFullscreen) {
mode = 1;
autoLineMode = true;
}

native.reduce(statePtr, opsBuf, len, mode, row);

if (options?.pointer) {
let { x, y, down } = options.pointer;
native.setPointer(x, y, down);
}

let output = new Uint8Array(
let output: Uint8Array<ArrayBufferLike> = new Uint8Array(
memory.buffer,
native.output(statePtr),
native.length(statePtr),
);

if (autoLineMode) {
output = normalizeWindowsLineOutput(output);
} else if (windowsFullscreen) {
output = wrapWindowsFullscreenOutput(output);
}

let current = new Set(
options?.pointer ? native.getPointerOverIds() : [],
);
Expand Down
13 changes: 12 additions & 1 deletion test/term.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ describe("term", () => {
│ │
│ │
└──────────────────┘`.trim());

if (Deno.build.os === "windows") {
expect(out.startsWith("\x1b[?7l")).toBe(true);
expect(out.endsWith("\x1b[?7h")).toBe(true);
}
});

it("primes front buffer for subsequent diff render", async () => {
Expand All @@ -147,7 +152,13 @@ describe("term", () => {
│ │
└──────────────────┘`.trim());

expect(second.length).toBeLessThan(first.length);
if (Deno.build.os === "windows") {
expect(second.startsWith("\x1b[?7l\x1b[H")).toBe(true);
expect(second).toContain("\r\n");
expect(second.endsWith("\x1b[?7h")).toBe(true);
} else {
expect(second.length).toBeLessThan(first.length);
}
});
});

Expand Down
Loading