diff --git a/demo/keyboard.ts b/demo/keyboard.ts index 933a924..4c5bfd2 100644 --- a/demo/keyboard.ts +++ b/demo/keyboard.ts @@ -22,6 +22,14 @@ import { rgba, text, } from "../mod.ts"; +import { + alternateBuffer, + cursor, + mouseTracking, + progressiveInput, + type Setting, + settings, +} from "../settings.ts"; import { useInput } from "./use-input.ts"; import { useStdin } from "./use-stdin.ts"; @@ -539,20 +547,19 @@ function keyboard(ctx: AppContext): Op[] { return ops; } -let encoder = new TextEncoder(); -let esc = (s: string) => Deno.stdout.writeSync(encoder.encode(s)); - -function ttyFlags(ctx: AppContext): Uint8Array { +function ttyFlags(ctx: AppContext): Setting { + let parts: Setting[] = []; let bits = 0; if (ctx["Disambiguate escape codes"]) bits |= 1; if (ctx["Report event types"]) bits |= 2; if (ctx["Report alternate keys"]) bits |= 4; if (ctx["Report all keys as escapes"]) bits |= 8; if (ctx["Report associated text"]) bits |= 16; - let mouse = ctx["Capture mouse events"] - ? "\x1b[?1003h\x1b[?1006h" - : "\x1b[?1003l\x1b[?1006l"; - return encoder.encode(`\x1b[${bits}u${mouse}`); + parts.push(progressiveInput(bits)); + if (ctx["Capture mouse events"]) { + parts.push(mouseTracking()); + } + return settings(...parts); } await main(function* () { @@ -567,16 +574,19 @@ await main(function* () { let term = yield* until(createTerm({ width: columns, height: rows })); - esc("\x1b[?1049h\x1b[?25l\x1b[>3u"); - yield* ensure(() => { - esc("\x1b[?1003l\x1b[?1006l\x1b[ { + Deno.stdout.writeSync(flags.revert); + Deno.stdout.writeSync(tty.revert); + }); let { output } = term.render(keyboard(context)); @@ -606,7 +616,9 @@ await main(function* () { context = { ...context, logged: prev }; } - Deno.stdout.writeSync(ttyFlags(context)); + Deno.stdout.writeSync(flags.revert); + flags = ttyFlags(context); + Deno.stdout.writeSync(flags.apply); if (context["Capture mouse events"]) { if ("x" in event) { diff --git a/settings.ts b/settings.ts new file mode 100644 index 0000000..0d9be06 --- /dev/null +++ b/settings.ts @@ -0,0 +1,67 @@ +export interface Setting { + apply: Uint8Array; + revert: Uint8Array; +} + +export function settings(...sequence: Setting[]): Setting { + return { + apply: concat(sequence.map((s) => s.apply)), + revert: concat(sequence.map((s) => s.revert).reverse()), + }; +} + +export function alternateBuffer(): Setting { + return { + apply: csi("?1049h"), + revert: csi("?1049l"), + }; +} + +export function cursor(visible: boolean): Setting { + if (visible) { + return { + apply: csi("?25h"), + revert: csi("?25l"), + }; + } else { + return { + apply: csi("?25l"), + revert: csi("?25h"), + }; + } +} + +export function progressiveInput(level: number): Setting { + return { + apply: csi(`>${level}u`), + revert: csi(" sum + a.length, 0); + let result = new Uint8Array(length); + let offset = 0; + for (let a of arrays) { + result.set(a, offset); + offset += a.length; + } + return result; +} diff --git a/test/settings.test.ts b/test/settings.test.ts new file mode 100644 index 0000000..daf9b23 --- /dev/null +++ b/test/settings.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from "./suite.ts"; +import { + alternateBuffer, + cursor, + mouseTracking, + progressiveInput, + settings, +} from "../settings.ts"; + +function str(bytes: Uint8Array): string { + return new TextDecoder().decode(bytes); +} + +describe("settings", () => { + describe("alternateBuffer", () => { + it("applies with enter and removes with leave", () => { + let s = alternateBuffer(); + expect(str(s.apply)).toBe("\x1b[?1049h"); + expect(str(s.revert)).toBe("\x1b[?1049l"); + }); + }); + + describe("cursor", () => { + it("hides on apply when visible is false", () => { + let s = cursor(false); + expect(str(s.apply)).toBe("\x1b[?25l"); + expect(str(s.revert)).toBe("\x1b[?25h"); + }); + + it("shows on apply when visible is true", () => { + let s = cursor(true); + expect(str(s.apply)).toBe("\x1b[?25h"); + expect(str(s.revert)).toBe("\x1b[?25l"); + }); + }); + + describe("progressiveInput", () => { + it("pushes the given level and pops on remove", () => { + let s = progressiveInput(3); + expect(str(s.apply)).toBe("\x1b[>3u"); + expect(str(s.revert)).toBe("\x1b[ { + it("enables any-event tracking with SGR encoding", () => { + let s = mouseTracking(); + expect(str(s.apply)).toBe("\x1b[?1003h\x1b[?1006h"); + expect(str(s.revert)).toBe("\x1b[?1006l\x1b[?1003l"); + }); + }); + + describe("settings()", () => { + it("concatenates apply in order", () => { + let s = settings(alternateBuffer(), cursor(false)); + expect(str(s.apply)).toBe("\x1b[?1049h\x1b[?25l"); + }); + + it("concatenates remove in reverse order", () => { + let s = settings(alternateBuffer(), cursor(false)); + expect(str(s.revert)).toBe("\x1b[?25h\x1b[?1049l"); + }); + + it("composes multiple settings", () => { + let s = settings( + alternateBuffer(), + cursor(false), + progressiveInput(3), + ); + expect(str(s.apply)).toBe("\x1b[?1049h\x1b[?25l\x1b[>3u"); + expect(str(s.revert)).toBe("\x1b[