@teammates/consolonia 0.6.3 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/ansi.test.js +211 -5
- package/dist/__tests__/input.test.js +157 -0
- package/dist/ansi/esc.d.ts +48 -4
- package/dist/ansi/esc.js +86 -4
- package/dist/ansi/terminal-env.d.ts +34 -0
- package/dist/ansi/terminal-env.js +206 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.js +19 -21
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/input/mouse-matcher.d.ts +21 -3
- package/dist/input/mouse-matcher.js +123 -30
- package/dist/widgets/chat-view.d.ts +28 -35
- package/dist/widgets/chat-view.js +198 -251
- package/dist/widgets/feed-store.d.ts +58 -0
- package/dist/widgets/feed-store.js +69 -0
- package/dist/widgets/virtual-list.d.ts +85 -0
- package/dist/widgets/virtual-list.js +262 -0
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Writable } from "node:stream";
|
|
2
|
-
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
3
|
import * as esc from "../ansi/esc.js";
|
|
4
4
|
import { AnsiOutput } from "../ansi/output.js";
|
|
5
5
|
import { stripAnsi, truncateAnsi, visibleLength } from "../ansi/strip.js";
|
|
6
|
+
import { detectTerminal } from "../ansi/terminal-env.js";
|
|
6
7
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
7
8
|
const ESC = "\x1b[";
|
|
8
9
|
/** Build a mock writable stream that accumulates output to a string. */
|
|
@@ -123,11 +124,11 @@ describe("esc", () => {
|
|
|
123
124
|
expect(esc.bracketedPasteOn).toBe(`${ESC}?2004h`);
|
|
124
125
|
expect(esc.bracketedPasteOff).toBe(`${ESC}?2004l`);
|
|
125
126
|
});
|
|
126
|
-
it("mouseTrackingOn enables
|
|
127
|
-
expect(esc.mouseTrackingOn).toBe(`${ESC}?1003h${ESC}?1006h`);
|
|
127
|
+
it("mouseTrackingOn enables all mouse tracking modes", () => {
|
|
128
|
+
expect(esc.mouseTrackingOn).toBe(`${ESC}?1000h${ESC}?1003h${ESC}?1005h${ESC}?1006h${ESC}?1015h${ESC}?1016h`);
|
|
128
129
|
});
|
|
129
|
-
it("mouseTrackingOff disables
|
|
130
|
-
expect(esc.mouseTrackingOff).toBe(`${ESC}?1006l${ESC}?1003l`);
|
|
130
|
+
it("mouseTrackingOff disables all mouse tracking modes", () => {
|
|
131
|
+
expect(esc.mouseTrackingOff).toBe(`${ESC}?1016l${ESC}?1015l${ESC}?1006l${ESC}?1005l${ESC}?1003l${ESC}?1000l`);
|
|
131
132
|
});
|
|
132
133
|
});
|
|
133
134
|
describe("setTitle", () => {
|
|
@@ -518,3 +519,208 @@ describe("AnsiOutput", () => {
|
|
|
518
519
|
});
|
|
519
520
|
});
|
|
520
521
|
});
|
|
522
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
523
|
+
// terminal-env.ts
|
|
524
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
525
|
+
describe("detectTerminal", () => {
|
|
526
|
+
const origEnv = { ...process.env };
|
|
527
|
+
const origPlatform = process.platform;
|
|
528
|
+
const origIsTTY = process.stdout.isTTY;
|
|
529
|
+
afterEach(() => {
|
|
530
|
+
// Restore environment
|
|
531
|
+
for (const key of Object.keys(process.env)) {
|
|
532
|
+
if (!(key in origEnv))
|
|
533
|
+
delete process.env[key];
|
|
534
|
+
}
|
|
535
|
+
Object.assign(process.env, origEnv);
|
|
536
|
+
Object.defineProperty(process, "platform", { value: origPlatform });
|
|
537
|
+
Object.defineProperty(process.stdout, "isTTY", { value: origIsTTY, writable: true });
|
|
538
|
+
});
|
|
539
|
+
function setEnv(overrides) {
|
|
540
|
+
for (const [k, v] of Object.entries(overrides)) {
|
|
541
|
+
if (v === undefined)
|
|
542
|
+
delete process.env[k];
|
|
543
|
+
else
|
|
544
|
+
process.env[k] = v;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
it("returns pipe caps when stdout is not a TTY", () => {
|
|
548
|
+
Object.defineProperty(process.stdout, "isTTY", { value: false, writable: true });
|
|
549
|
+
const caps = detectTerminal();
|
|
550
|
+
expect(caps.isTTY).toBe(false);
|
|
551
|
+
expect(caps.mouse).toBe(false);
|
|
552
|
+
expect(caps.alternateScreen).toBe(false);
|
|
553
|
+
expect(caps.name).toBe("pipe");
|
|
554
|
+
});
|
|
555
|
+
it("detects Windows Terminal via WT_SESSION", () => {
|
|
556
|
+
Object.defineProperty(process, "platform", { value: "win32" });
|
|
557
|
+
Object.defineProperty(process.stdout, "isTTY", { value: true, writable: true });
|
|
558
|
+
setEnv({ WT_SESSION: "some-guid" });
|
|
559
|
+
const caps = detectTerminal();
|
|
560
|
+
expect(caps.name).toBe("windows-terminal");
|
|
561
|
+
expect(caps.sgrMouse).toBe(true);
|
|
562
|
+
expect(caps.truecolor).toBe(true);
|
|
563
|
+
});
|
|
564
|
+
it("detects VS Code terminal on Windows", () => {
|
|
565
|
+
Object.defineProperty(process, "platform", { value: "win32" });
|
|
566
|
+
Object.defineProperty(process.stdout, "isTTY", { value: true, writable: true });
|
|
567
|
+
setEnv({ WT_SESSION: undefined, TERM_PROGRAM: "vscode" });
|
|
568
|
+
const caps = detectTerminal();
|
|
569
|
+
expect(caps.name).toBe("vscode");
|
|
570
|
+
expect(caps.sgrMouse).toBe(true);
|
|
571
|
+
});
|
|
572
|
+
it("detects ConEmu on Windows", () => {
|
|
573
|
+
Object.defineProperty(process, "platform", { value: "win32" });
|
|
574
|
+
Object.defineProperty(process.stdout, "isTTY", { value: true, writable: true });
|
|
575
|
+
setEnv({ WT_SESSION: undefined, TERM_PROGRAM: undefined, ConEmuPID: "1234" });
|
|
576
|
+
const caps = detectTerminal();
|
|
577
|
+
expect(caps.name).toBe("conemu");
|
|
578
|
+
});
|
|
579
|
+
it("detects mintty via TERM + MSYSTEM", () => {
|
|
580
|
+
Object.defineProperty(process, "platform", { value: "win32" });
|
|
581
|
+
Object.defineProperty(process.stdout, "isTTY", { value: true, writable: true });
|
|
582
|
+
setEnv({ WT_SESSION: undefined, TERM_PROGRAM: undefined, ConEmuPID: undefined, TERM: "xterm-256color", MSYSTEM: "MINGW64" });
|
|
583
|
+
const caps = detectTerminal();
|
|
584
|
+
expect(caps.name).toBe("mintty");
|
|
585
|
+
});
|
|
586
|
+
it("detects tmux on Unix", () => {
|
|
587
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
588
|
+
Object.defineProperty(process.stdout, "isTTY", { value: true, writable: true });
|
|
589
|
+
setEnv({ TMUX: "/tmp/tmux-1000/default,1234,0", TERM: "screen-256color" });
|
|
590
|
+
const caps = detectTerminal();
|
|
591
|
+
expect(caps.name).toBe("tmux");
|
|
592
|
+
expect(caps.sgrMouse).toBe(true);
|
|
593
|
+
});
|
|
594
|
+
it("detects GNU screen with limited caps", () => {
|
|
595
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
596
|
+
Object.defineProperty(process.stdout, "isTTY", { value: true, writable: true });
|
|
597
|
+
setEnv({ TMUX: undefined, TERM: "screen", TERM_PROGRAM: undefined, ITERM_SESSION_ID: undefined });
|
|
598
|
+
const caps = detectTerminal();
|
|
599
|
+
expect(caps.name).toBe("screen");
|
|
600
|
+
expect(caps.sgrMouse).toBe(false);
|
|
601
|
+
expect(caps.bracketedPaste).toBe(false);
|
|
602
|
+
});
|
|
603
|
+
it("detects iTerm2", () => {
|
|
604
|
+
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
605
|
+
Object.defineProperty(process.stdout, "isTTY", { value: true, writable: true });
|
|
606
|
+
setEnv({ TMUX: undefined, TERM: "xterm-256color", TERM_PROGRAM: "iTerm.app" });
|
|
607
|
+
const caps = detectTerminal();
|
|
608
|
+
expect(caps.name).toBe("iterm2");
|
|
609
|
+
expect(caps.truecolor).toBe(true);
|
|
610
|
+
});
|
|
611
|
+
it("detects xterm-compatible with COLORTERM truecolor", () => {
|
|
612
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
613
|
+
Object.defineProperty(process.stdout, "isTTY", { value: true, writable: true });
|
|
614
|
+
setEnv({ TMUX: undefined, TERM: "xterm-256color", TERM_PROGRAM: undefined, ITERM_SESSION_ID: undefined, COLORTERM: "truecolor" });
|
|
615
|
+
const caps = detectTerminal();
|
|
616
|
+
expect(caps.truecolor).toBe(true);
|
|
617
|
+
expect(caps.color256).toBe(true);
|
|
618
|
+
});
|
|
619
|
+
it("detects dumb terminal with minimal caps", () => {
|
|
620
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
621
|
+
Object.defineProperty(process.stdout, "isTTY", { value: true, writable: true });
|
|
622
|
+
setEnv({ TMUX: undefined, TERM: "dumb", TERM_PROGRAM: undefined, ITERM_SESSION_ID: undefined });
|
|
623
|
+
const caps = detectTerminal();
|
|
624
|
+
expect(caps.name).toBe("dumb");
|
|
625
|
+
expect(caps.mouse).toBe(false);
|
|
626
|
+
expect(caps.alternateScreen).toBe(false);
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
630
|
+
// esc.ts — environment-aware init/restore sequences
|
|
631
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
632
|
+
describe("esc environment-aware sequences", () => {
|
|
633
|
+
/** Helper to build a TerminalCaps with overrides. */
|
|
634
|
+
function makeCaps(overrides = {}) {
|
|
635
|
+
return {
|
|
636
|
+
isTTY: true,
|
|
637
|
+
alternateScreen: true,
|
|
638
|
+
bracketedPaste: true,
|
|
639
|
+
mouse: true,
|
|
640
|
+
sgrMouse: true,
|
|
641
|
+
truecolor: true,
|
|
642
|
+
color256: true,
|
|
643
|
+
name: "test",
|
|
644
|
+
...overrides,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
describe("mouseOn / mouseOff", () => {
|
|
648
|
+
it("returns full mouse sequence when SGR is supported", () => {
|
|
649
|
+
const caps = makeCaps({ sgrMouse: true });
|
|
650
|
+
expect(esc.mouseOn(caps)).toBe(esc.mouseTrackingOn);
|
|
651
|
+
expect(esc.mouseOff(caps)).toBe(esc.mouseTrackingOff);
|
|
652
|
+
});
|
|
653
|
+
it("returns minimal mouse sequence when SGR is not supported", () => {
|
|
654
|
+
const caps = makeCaps({ sgrMouse: false });
|
|
655
|
+
const on = esc.mouseOn(caps);
|
|
656
|
+
expect(on).toContain("?1000h");
|
|
657
|
+
expect(on).toContain("?1003h");
|
|
658
|
+
expect(on).not.toContain("?1006h");
|
|
659
|
+
expect(on).not.toContain("?1015h");
|
|
660
|
+
});
|
|
661
|
+
it("returns empty string when mouse is not supported", () => {
|
|
662
|
+
const caps = makeCaps({ mouse: false });
|
|
663
|
+
expect(esc.mouseOn(caps)).toBe("");
|
|
664
|
+
expect(esc.mouseOff(caps)).toBe("");
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
describe("initSequence", () => {
|
|
668
|
+
it("includes all features for a full-caps terminal", () => {
|
|
669
|
+
const caps = makeCaps();
|
|
670
|
+
const seq = esc.initSequence(caps, { alternateScreen: true, mouse: true });
|
|
671
|
+
expect(seq).toContain(esc.alternateScreenOn);
|
|
672
|
+
expect(seq).toContain(esc.hideCursor);
|
|
673
|
+
expect(seq).toContain(esc.bracketedPasteOn);
|
|
674
|
+
expect(seq).toContain(esc.mouseTrackingOn);
|
|
675
|
+
expect(seq).toContain(esc.clearScreen);
|
|
676
|
+
});
|
|
677
|
+
it("skips alternate screen when not supported", () => {
|
|
678
|
+
const caps = makeCaps({ alternateScreen: false });
|
|
679
|
+
const seq = esc.initSequence(caps, { alternateScreen: true, mouse: false });
|
|
680
|
+
expect(seq).not.toContain(esc.alternateScreenOn);
|
|
681
|
+
});
|
|
682
|
+
it("skips alternate screen when app opts out", () => {
|
|
683
|
+
const caps = makeCaps();
|
|
684
|
+
const seq = esc.initSequence(caps, { alternateScreen: false, mouse: false });
|
|
685
|
+
expect(seq).not.toContain(esc.alternateScreenOn);
|
|
686
|
+
});
|
|
687
|
+
it("skips bracketed paste when not supported", () => {
|
|
688
|
+
const caps = makeCaps({ bracketedPaste: false });
|
|
689
|
+
const seq = esc.initSequence(caps, { alternateScreen: false, mouse: false });
|
|
690
|
+
expect(seq).not.toContain(esc.bracketedPasteOn);
|
|
691
|
+
});
|
|
692
|
+
it("skips mouse when app opts out even if caps support it", () => {
|
|
693
|
+
const caps = makeCaps();
|
|
694
|
+
const seq = esc.initSequence(caps, { alternateScreen: false, mouse: false });
|
|
695
|
+
expect(seq).not.toContain("?1000h");
|
|
696
|
+
});
|
|
697
|
+
it("returns empty string for non-TTY", () => {
|
|
698
|
+
const caps = makeCaps({ isTTY: false });
|
|
699
|
+
const seq = esc.initSequence(caps, { alternateScreen: true, mouse: true });
|
|
700
|
+
expect(seq).toBe("");
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
describe("restoreSequence", () => {
|
|
704
|
+
it("includes all restore features for a full-caps terminal", () => {
|
|
705
|
+
const caps = makeCaps();
|
|
706
|
+
const seq = esc.restoreSequence(caps, { alternateScreen: true, mouse: true });
|
|
707
|
+
expect(seq).toContain(esc.reset);
|
|
708
|
+
expect(seq).toContain(esc.mouseTrackingOff);
|
|
709
|
+
expect(seq).toContain(esc.bracketedPasteOff);
|
|
710
|
+
expect(seq).toContain(esc.showCursor);
|
|
711
|
+
expect(seq).toContain(esc.alternateScreenOff);
|
|
712
|
+
});
|
|
713
|
+
it("mirrors initSequence — skips what init skipped", () => {
|
|
714
|
+
const caps = makeCaps({ bracketedPaste: false, alternateScreen: false });
|
|
715
|
+
const seq = esc.restoreSequence(caps, { alternateScreen: true, mouse: false });
|
|
716
|
+
expect(seq).not.toContain(esc.bracketedPasteOff);
|
|
717
|
+
expect(seq).not.toContain(esc.alternateScreenOff);
|
|
718
|
+
expect(seq).not.toContain(esc.mouseTrackingOff);
|
|
719
|
+
});
|
|
720
|
+
it("returns empty string for non-TTY", () => {
|
|
721
|
+
const caps = makeCaps({ isTTY: false });
|
|
722
|
+
const seq = esc.restoreSequence(caps, { alternateScreen: true, mouse: true });
|
|
723
|
+
expect(seq).toBe("");
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
});
|
|
@@ -28,6 +28,9 @@ function collectEvents(data) {
|
|
|
28
28
|
processor.destroy();
|
|
29
29
|
return collected;
|
|
30
30
|
}
|
|
31
|
+
function x10Seq(cb, x, y) {
|
|
32
|
+
return `\x1b[M${String.fromCharCode(cb + 32)}${String.fromCharCode(x + 32)}${String.fromCharCode(y + 32)}`;
|
|
33
|
+
}
|
|
31
34
|
// =====================================================================
|
|
32
35
|
// EscapeMatcher
|
|
33
36
|
// =====================================================================
|
|
@@ -675,6 +678,137 @@ describe("MouseMatcher", () => {
|
|
|
675
678
|
expect(results[results.length - 1]).toBe(MatchResult.Complete);
|
|
676
679
|
});
|
|
677
680
|
});
|
|
681
|
+
// ── URXVT sequences ────────────────────────────────────────────────
|
|
682
|
+
describe("URXVT sequences", () => {
|
|
683
|
+
it("parses URXVT left press at (9,19) from \\x1b[0;10;20M", () => {
|
|
684
|
+
matcher = new MouseMatcher();
|
|
685
|
+
expect(feedString(matcher, "\x1b[0;10;20M")).toBe(MatchResult.Complete);
|
|
686
|
+
const ev = matcher.flush();
|
|
687
|
+
expect(ev).not.toBeNull();
|
|
688
|
+
expect(ev.type).toBe("mouse");
|
|
689
|
+
if (ev.type === "mouse") {
|
|
690
|
+
expect(ev.event.button).toBe("left");
|
|
691
|
+
expect(ev.event.type).toBe("press");
|
|
692
|
+
expect(ev.event.x).toBe(9);
|
|
693
|
+
expect(ev.event.y).toBe(19);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
it("parses URXVT right press", () => {
|
|
697
|
+
matcher = new MouseMatcher();
|
|
698
|
+
expect(feedString(matcher, "\x1b[2;5;5M")).toBe(MatchResult.Complete);
|
|
699
|
+
const ev = matcher.flush();
|
|
700
|
+
if (ev.type === "mouse") {
|
|
701
|
+
expect(ev.event.button).toBe("right");
|
|
702
|
+
expect(ev.event.type).toBe("press");
|
|
703
|
+
expect(ev.event.x).toBe(4);
|
|
704
|
+
expect(ev.event.y).toBe(4);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
it("parses URXVT release (button bits = 3)", () => {
|
|
708
|
+
matcher = new MouseMatcher();
|
|
709
|
+
expect(feedString(matcher, "\x1b[3;10;20M")).toBe(MatchResult.Complete);
|
|
710
|
+
const ev = matcher.flush();
|
|
711
|
+
if (ev.type === "mouse") {
|
|
712
|
+
expect(ev.event.button).toBe("none");
|
|
713
|
+
expect(ev.event.type).toBe("release");
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
it("parses URXVT wheel up (cb=64)", () => {
|
|
717
|
+
matcher = new MouseMatcher();
|
|
718
|
+
expect(feedString(matcher, "\x1b[64;5;5M")).toBe(MatchResult.Complete);
|
|
719
|
+
const ev = matcher.flush();
|
|
720
|
+
if (ev.type === "mouse") {
|
|
721
|
+
expect(ev.event.button).toBe("none");
|
|
722
|
+
expect(ev.event.type).toBe("wheelup");
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
it("parses URXVT wheel down (cb=65)", () => {
|
|
726
|
+
matcher = new MouseMatcher();
|
|
727
|
+
expect(feedString(matcher, "\x1b[65;5;5M")).toBe(MatchResult.Complete);
|
|
728
|
+
const ev = matcher.flush();
|
|
729
|
+
if (ev.type === "mouse") {
|
|
730
|
+
expect(ev.event.button).toBe("none");
|
|
731
|
+
expect(ev.event.type).toBe("wheeldown");
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
it("parses URXVT motion with left button (cb=32)", () => {
|
|
735
|
+
matcher = new MouseMatcher();
|
|
736
|
+
expect(feedString(matcher, "\x1b[32;10;10M")).toBe(MatchResult.Complete);
|
|
737
|
+
const ev = matcher.flush();
|
|
738
|
+
if (ev.type === "mouse") {
|
|
739
|
+
expect(ev.event.type).toBe("move");
|
|
740
|
+
expect(ev.event.button).toBe("left");
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
it("parses URXVT shift+left press (cb=4)", () => {
|
|
744
|
+
matcher = new MouseMatcher();
|
|
745
|
+
expect(feedString(matcher, "\x1b[4;1;1M")).toBe(MatchResult.Complete);
|
|
746
|
+
const ev = matcher.flush();
|
|
747
|
+
if (ev.type === "mouse") {
|
|
748
|
+
expect(ev.event.button).toBe("left");
|
|
749
|
+
expect(ev.event.type).toBe("press");
|
|
750
|
+
expect(ev.event.shift).toBe(true);
|
|
751
|
+
expect(ev.event.ctrl).toBe(false);
|
|
752
|
+
expect(ev.event.alt).toBe(false);
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
it("parses URXVT with large coordinates (beyond X10 range)", () => {
|
|
756
|
+
matcher = new MouseMatcher();
|
|
757
|
+
expect(feedString(matcher, "\x1b[0;300;200M")).toBe(MatchResult.Complete);
|
|
758
|
+
const ev = matcher.flush();
|
|
759
|
+
if (ev.type === "mouse") {
|
|
760
|
+
expect(ev.event.button).toBe("left");
|
|
761
|
+
expect(ev.event.type).toBe("press");
|
|
762
|
+
expect(ev.event.x).toBe(299);
|
|
763
|
+
expect(ev.event.y).toBe(199);
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
it("rejects URXVT with wrong param count (2 params)", () => {
|
|
767
|
+
matcher = new MouseMatcher();
|
|
768
|
+
expect(feedString(matcher, "\x1b[0;10M")).toBe(MatchResult.NoMatch);
|
|
769
|
+
});
|
|
770
|
+
it("rejects URXVT with wrong param count (4 params)", () => {
|
|
771
|
+
matcher = new MouseMatcher();
|
|
772
|
+
expect(feedString(matcher, "\x1b[0;10;20;30M")).toBe(MatchResult.NoMatch);
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
describe("classic X10 sequences", () => {
|
|
776
|
+
it("parses left press from classic CSI M encoding", () => {
|
|
777
|
+
matcher = new MouseMatcher();
|
|
778
|
+
expect(feedString(matcher, x10Seq(0, 10, 20))).toBe(MatchResult.Complete);
|
|
779
|
+
const ev = matcher.flush();
|
|
780
|
+
expect(ev).not.toBeNull();
|
|
781
|
+
expect(ev.type).toBe("mouse");
|
|
782
|
+
if (ev.type === "mouse") {
|
|
783
|
+
expect(ev.event.button).toBe("left");
|
|
784
|
+
expect(ev.event.type).toBe("press");
|
|
785
|
+
expect(ev.event.x).toBe(9);
|
|
786
|
+
expect(ev.event.y).toBe(19);
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
it("parses release from classic CSI M encoding", () => {
|
|
790
|
+
matcher = new MouseMatcher();
|
|
791
|
+
expect(feedString(matcher, x10Seq(3, 10, 20))).toBe(MatchResult.Complete);
|
|
792
|
+
const ev = matcher.flush();
|
|
793
|
+
expect(ev).not.toBeNull();
|
|
794
|
+
expect(ev.type).toBe("mouse");
|
|
795
|
+
if (ev.type === "mouse") {
|
|
796
|
+
expect(ev.event.button).toBe("none");
|
|
797
|
+
expect(ev.event.type).toBe("release");
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
it("parses wheel down from classic CSI M encoding", () => {
|
|
801
|
+
matcher = new MouseMatcher();
|
|
802
|
+
expect(feedString(matcher, x10Seq(65, 5, 5))).toBe(MatchResult.Complete);
|
|
803
|
+
const ev = matcher.flush();
|
|
804
|
+
expect(ev).not.toBeNull();
|
|
805
|
+
expect(ev.type).toBe("mouse");
|
|
806
|
+
if (ev.type === "mouse") {
|
|
807
|
+
expect(ev.event.button).toBe("none");
|
|
808
|
+
expect(ev.event.type).toBe("wheeldown");
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
});
|
|
678
812
|
});
|
|
679
813
|
// =====================================================================
|
|
680
814
|
// TextMatcher
|
|
@@ -853,6 +987,29 @@ describe("InputProcessor", () => {
|
|
|
853
987
|
expect(events[0].event.y).toBe(0);
|
|
854
988
|
}
|
|
855
989
|
});
|
|
990
|
+
it("URXVT mouse sequences are recognized through parallel matching", () => {
|
|
991
|
+
// URXVT: \x1b[0;5;10M — left press at (4,9)
|
|
992
|
+
const events = collectEvents("\x1b[0;5;10M");
|
|
993
|
+
expect(events).toHaveLength(1);
|
|
994
|
+
expect(events[0].type).toBe("mouse");
|
|
995
|
+
if (events[0].type === "mouse") {
|
|
996
|
+
expect(events[0].event.button).toBe("left");
|
|
997
|
+
expect(events[0].event.type).toBe("press");
|
|
998
|
+
expect(events[0].event.x).toBe(4);
|
|
999
|
+
expect(events[0].event.y).toBe(9);
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
it("classic X10 mouse sequences are recognized through parallel matching", () => {
|
|
1003
|
+
const events = collectEvents(x10Seq(0, 1, 1));
|
|
1004
|
+
expect(events).toHaveLength(1);
|
|
1005
|
+
expect(events[0].type).toBe("mouse");
|
|
1006
|
+
if (events[0].type === "mouse") {
|
|
1007
|
+
expect(events[0].event.button).toBe("left");
|
|
1008
|
+
expect(events[0].event.type).toBe("press");
|
|
1009
|
+
expect(events[0].event.x).toBe(0);
|
|
1010
|
+
expect(events[0].event.y).toBe(0);
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
856
1013
|
it("handles text after a paste sequence", () => {
|
|
857
1014
|
const events = collectEvents("\x1b[200~pasted\x1b[201~after");
|
|
858
1015
|
expect(events).toHaveLength(6); // 1 paste + 5 chars
|
package/dist/ansi/esc.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* ANSI escape sequence constants and builder functions.
|
|
3
3
|
* All functions return raw escape strings — nothing is written to stdout.
|
|
4
4
|
*/
|
|
5
|
+
import type { TerminalCaps } from "./terminal-env.js";
|
|
5
6
|
export declare const reset = "\u001B[0m";
|
|
6
7
|
export declare const bold = "\u001B[1m";
|
|
7
8
|
export declare const dim = "\u001B[2m";
|
|
@@ -53,9 +54,52 @@ export declare const eraseLine = "\u001B[2K";
|
|
|
53
54
|
export declare const bracketedPasteOn = "\u001B[?2004h";
|
|
54
55
|
/** Disable bracketed paste mode. */
|
|
55
56
|
export declare const bracketedPasteOff = "\u001B[?2004l";
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Enable mouse tracking with all supported reporting modes.
|
|
59
|
+
*
|
|
60
|
+
* Modes enabled (in order):
|
|
61
|
+
* ?1000h — Normal/VT200 click tracking (press + release)
|
|
62
|
+
* ?1003h — Any-event tracking (all mouse movement)
|
|
63
|
+
* ?1005h — UTF-8 coordinate encoding (extends X10 beyond col/row 223)
|
|
64
|
+
* ?1006h — SGR extended coordinates (";"-delimited decimals, M/m terminator)
|
|
65
|
+
* ?1015h — URXVT extended coordinates (decimal params, no "<" prefix)
|
|
66
|
+
* ?1016h — SGR-Pixels (same wire format as SGR, pixel coordinates)
|
|
67
|
+
*
|
|
68
|
+
* Terminals pick the highest mode they support. SGR is preferred by most
|
|
69
|
+
* modern terminals; URXVT and UTF-8 provide fallback for older ones.
|
|
70
|
+
*/
|
|
71
|
+
export declare const mouseTrackingOn = "\u001B[?1000h\u001B[?1003h\u001B[?1005h\u001B[?1006h\u001B[?1015h\u001B[?1016h";
|
|
72
|
+
/**
|
|
73
|
+
* Disable all mouse tracking modes (reverse order of enable).
|
|
74
|
+
*/
|
|
75
|
+
export declare const mouseTrackingOff = "\u001B[?1016l\u001B[?1015l\u001B[?1006l\u001B[?1005l\u001B[?1003l\u001B[?1000l";
|
|
60
76
|
/** Set the terminal window title. */
|
|
61
77
|
export declare function setTitle(title: string): string;
|
|
78
|
+
/**
|
|
79
|
+
* Mouse tracking sequence tailored to detected capabilities.
|
|
80
|
+
*
|
|
81
|
+
* When the terminal supports SGR, request all six modes so the terminal
|
|
82
|
+
* picks the highest one it handles. When SGR is not available (e.g. GNU
|
|
83
|
+
* screen), fall back to normal + any-event tracking only — UTF-8 and
|
|
84
|
+
* URXVT modes could confuse terminals that don't understand them.
|
|
85
|
+
*/
|
|
86
|
+
export declare function mouseOn(caps: TerminalCaps): string;
|
|
87
|
+
/** Matching disable sequence for mouseOn(). */
|
|
88
|
+
export declare function mouseOff(caps: TerminalCaps): string;
|
|
89
|
+
/**
|
|
90
|
+
* Build the full terminal preparation sequence for the given environment.
|
|
91
|
+
*
|
|
92
|
+
* @param caps - Detected terminal capabilities
|
|
93
|
+
* @param opts - Which optional features the app requested
|
|
94
|
+
*/
|
|
95
|
+
export declare function initSequence(caps: TerminalCaps, opts: {
|
|
96
|
+
alternateScreen: boolean;
|
|
97
|
+
mouse: boolean;
|
|
98
|
+
}): string;
|
|
99
|
+
/**
|
|
100
|
+
* Build the full terminal restore sequence (mirror of initSequence).
|
|
101
|
+
*/
|
|
102
|
+
export declare function restoreSequence(caps: TerminalCaps, opts: {
|
|
103
|
+
alternateScreen: boolean;
|
|
104
|
+
mouse: boolean;
|
|
105
|
+
}): string;
|
package/dist/ansi/esc.js
CHANGED
|
@@ -74,12 +74,94 @@ export const eraseLine = `${ESC}2K`;
|
|
|
74
74
|
export const bracketedPasteOn = `${ESC}?2004h`;
|
|
75
75
|
/** Disable bracketed paste mode. */
|
|
76
76
|
export const bracketedPasteOff = `${ESC}?2004l`;
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Enable mouse tracking with all supported reporting modes.
|
|
79
|
+
*
|
|
80
|
+
* Modes enabled (in order):
|
|
81
|
+
* ?1000h — Normal/VT200 click tracking (press + release)
|
|
82
|
+
* ?1003h — Any-event tracking (all mouse movement)
|
|
83
|
+
* ?1005h — UTF-8 coordinate encoding (extends X10 beyond col/row 223)
|
|
84
|
+
* ?1006h — SGR extended coordinates (";"-delimited decimals, M/m terminator)
|
|
85
|
+
* ?1015h — URXVT extended coordinates (decimal params, no "<" prefix)
|
|
86
|
+
* ?1016h — SGR-Pixels (same wire format as SGR, pixel coordinates)
|
|
87
|
+
*
|
|
88
|
+
* Terminals pick the highest mode they support. SGR is preferred by most
|
|
89
|
+
* modern terminals; URXVT and UTF-8 provide fallback for older ones.
|
|
90
|
+
*/
|
|
91
|
+
export const mouseTrackingOn = `${ESC}?1000h${ESC}?1003h${ESC}?1005h${ESC}?1006h${ESC}?1015h${ESC}?1016h`;
|
|
92
|
+
/**
|
|
93
|
+
* Disable all mouse tracking modes (reverse order of enable).
|
|
94
|
+
*/
|
|
95
|
+
export const mouseTrackingOff = `${ESC}?1016l${ESC}?1015l${ESC}?1006l${ESC}?1005l${ESC}?1003l${ESC}?1000l`;
|
|
81
96
|
// ── Window ──────────────────────────────────────────────────────────
|
|
82
97
|
/** Set the terminal window title. */
|
|
83
98
|
export function setTitle(title) {
|
|
84
99
|
return `${OSC}0;${title}\x07`;
|
|
85
100
|
}
|
|
101
|
+
// ── Environment-aware init/restore ─────────────────────────────────
|
|
102
|
+
/**
|
|
103
|
+
* Mouse tracking sequence tailored to detected capabilities.
|
|
104
|
+
*
|
|
105
|
+
* When the terminal supports SGR, request all six modes so the terminal
|
|
106
|
+
* picks the highest one it handles. When SGR is not available (e.g. GNU
|
|
107
|
+
* screen), fall back to normal + any-event tracking only — UTF-8 and
|
|
108
|
+
* URXVT modes could confuse terminals that don't understand them.
|
|
109
|
+
*/
|
|
110
|
+
export function mouseOn(caps) {
|
|
111
|
+
if (!caps.mouse)
|
|
112
|
+
return "";
|
|
113
|
+
if (caps.sgrMouse)
|
|
114
|
+
return mouseTrackingOn;
|
|
115
|
+
// Minimal: normal click + any-event only
|
|
116
|
+
return `${ESC}?1000h${ESC}?1003h`;
|
|
117
|
+
}
|
|
118
|
+
/** Matching disable sequence for mouseOn(). */
|
|
119
|
+
export function mouseOff(caps) {
|
|
120
|
+
if (!caps.mouse)
|
|
121
|
+
return "";
|
|
122
|
+
if (caps.sgrMouse)
|
|
123
|
+
return mouseTrackingOff;
|
|
124
|
+
return `${ESC}?1003l${ESC}?1000l`;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Build the full terminal preparation sequence for the given environment.
|
|
128
|
+
*
|
|
129
|
+
* @param caps - Detected terminal capabilities
|
|
130
|
+
* @param opts - Which optional features the app requested
|
|
131
|
+
*/
|
|
132
|
+
export function initSequence(caps, opts) {
|
|
133
|
+
if (!caps.isTTY)
|
|
134
|
+
return "";
|
|
135
|
+
let seq = "";
|
|
136
|
+
if (opts.alternateScreen && caps.alternateScreen) {
|
|
137
|
+
seq += alternateScreenOn;
|
|
138
|
+
}
|
|
139
|
+
seq += hideCursor;
|
|
140
|
+
if (caps.bracketedPaste) {
|
|
141
|
+
seq += bracketedPasteOn;
|
|
142
|
+
}
|
|
143
|
+
if (opts.mouse) {
|
|
144
|
+
seq += mouseOn(caps);
|
|
145
|
+
}
|
|
146
|
+
seq += clearScreen;
|
|
147
|
+
return seq;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Build the full terminal restore sequence (mirror of initSequence).
|
|
151
|
+
*/
|
|
152
|
+
export function restoreSequence(caps, opts) {
|
|
153
|
+
if (!caps.isTTY)
|
|
154
|
+
return "";
|
|
155
|
+
let seq = reset;
|
|
156
|
+
if (opts.mouse) {
|
|
157
|
+
seq += mouseOff(caps);
|
|
158
|
+
}
|
|
159
|
+
if (caps.bracketedPaste) {
|
|
160
|
+
seq += bracketedPasteOff;
|
|
161
|
+
}
|
|
162
|
+
seq += showCursor;
|
|
163
|
+
if (opts.alternateScreen && caps.alternateScreen) {
|
|
164
|
+
seq += alternateScreenOff;
|
|
165
|
+
}
|
|
166
|
+
return seq;
|
|
167
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal environment detection.
|
|
3
|
+
*
|
|
4
|
+
* Probes environment variables and process state to determine which
|
|
5
|
+
* terminal capabilities are available. Used by App to send only the
|
|
6
|
+
* escape sequences the host terminal actually supports.
|
|
7
|
+
*/
|
|
8
|
+
export interface TerminalCaps {
|
|
9
|
+
/** Terminal is a TTY (not piped). */
|
|
10
|
+
isTTY: boolean;
|
|
11
|
+
/** Supports alternate screen buffer (?1049h). */
|
|
12
|
+
alternateScreen: boolean;
|
|
13
|
+
/** Supports bracketed paste mode (?2004h). */
|
|
14
|
+
bracketedPaste: boolean;
|
|
15
|
+
/** Supports escape-based mouse tracking (?1000h and above). */
|
|
16
|
+
mouse: boolean;
|
|
17
|
+
/** Supports SGR extended mouse encoding (?1006h). */
|
|
18
|
+
sgrMouse: boolean;
|
|
19
|
+
/** Supports truecolor (24-bit) SGR sequences. */
|
|
20
|
+
truecolor: boolean;
|
|
21
|
+
/** Supports 256-color SGR sequences. */
|
|
22
|
+
color256: boolean;
|
|
23
|
+
/** Detected terminal name (for diagnostics). */
|
|
24
|
+
name: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Detect terminal capabilities from the current environment.
|
|
28
|
+
*
|
|
29
|
+
* On Windows the main differentiator is whether we're running under
|
|
30
|
+
* Windows Terminal / ConPTY (full VT support) or legacy conhost
|
|
31
|
+
* (very limited escape handling). On Unix the TERM variable and
|
|
32
|
+
* TERM_PROGRAM give us enough signal.
|
|
33
|
+
*/
|
|
34
|
+
export declare function detectTerminal(): TerminalCaps;
|