@teammates/consolonia 0.7.0 → 0.7.2
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 +332 -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/ansi/win32-console.d.ts +28 -0
- package/dist/ansi/win32-console.js +122 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.js +34 -28
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/input/mouse-matcher.d.ts +21 -3
- package/dist/input/mouse-matcher.js +123 -30
- package/package.json +4 -1
|
@@ -1,8 +1,10 @@
|
|
|
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";
|
|
7
|
+
import { enableWin32Mouse, restoreWin32Console, } from "../ansi/win32-console.js";
|
|
6
8
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
7
9
|
const ESC = "\x1b[";
|
|
8
10
|
/** Build a mock writable stream that accumulates output to a string. */
|
|
@@ -123,11 +125,11 @@ describe("esc", () => {
|
|
|
123
125
|
expect(esc.bracketedPasteOn).toBe(`${ESC}?2004h`);
|
|
124
126
|
expect(esc.bracketedPasteOff).toBe(`${ESC}?2004l`);
|
|
125
127
|
});
|
|
126
|
-
it("mouseTrackingOn enables
|
|
127
|
-
expect(esc.mouseTrackingOn).toBe(`${ESC}?1003h${ESC}?1006h`);
|
|
128
|
+
it("mouseTrackingOn enables all mouse tracking modes", () => {
|
|
129
|
+
expect(esc.mouseTrackingOn).toBe(`${ESC}?1000h${ESC}?1003h${ESC}?1005h${ESC}?1006h${ESC}?1015h${ESC}?1016h`);
|
|
128
130
|
});
|
|
129
|
-
it("mouseTrackingOff disables
|
|
130
|
-
expect(esc.mouseTrackingOff).toBe(`${ESC}?1006l${ESC}?1003l`);
|
|
131
|
+
it("mouseTrackingOff disables all mouse tracking modes", () => {
|
|
132
|
+
expect(esc.mouseTrackingOff).toBe(`${ESC}?1016l${ESC}?1015l${ESC}?1006l${ESC}?1005l${ESC}?1003l${ESC}?1000l`);
|
|
131
133
|
});
|
|
132
134
|
});
|
|
133
135
|
describe("setTitle", () => {
|
|
@@ -518,3 +520,328 @@ describe("AnsiOutput", () => {
|
|
|
518
520
|
});
|
|
519
521
|
});
|
|
520
522
|
});
|
|
523
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
524
|
+
// terminal-env.ts
|
|
525
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
526
|
+
describe("detectTerminal", () => {
|
|
527
|
+
const origEnv = { ...process.env };
|
|
528
|
+
const origPlatform = process.platform;
|
|
529
|
+
const origIsTTY = process.stdout.isTTY;
|
|
530
|
+
afterEach(() => {
|
|
531
|
+
// Restore environment
|
|
532
|
+
for (const key of Object.keys(process.env)) {
|
|
533
|
+
if (!(key in origEnv))
|
|
534
|
+
delete process.env[key];
|
|
535
|
+
}
|
|
536
|
+
Object.assign(process.env, origEnv);
|
|
537
|
+
Object.defineProperty(process, "platform", { value: origPlatform });
|
|
538
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
539
|
+
value: origIsTTY,
|
|
540
|
+
writable: true,
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
function setEnv(overrides) {
|
|
544
|
+
for (const [k, v] of Object.entries(overrides)) {
|
|
545
|
+
if (v === undefined)
|
|
546
|
+
delete process.env[k];
|
|
547
|
+
else
|
|
548
|
+
process.env[k] = v;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
it("returns pipe caps when stdout is not a TTY", () => {
|
|
552
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
553
|
+
value: false,
|
|
554
|
+
writable: true,
|
|
555
|
+
});
|
|
556
|
+
const caps = detectTerminal();
|
|
557
|
+
expect(caps.isTTY).toBe(false);
|
|
558
|
+
expect(caps.mouse).toBe(false);
|
|
559
|
+
expect(caps.alternateScreen).toBe(false);
|
|
560
|
+
expect(caps.name).toBe("pipe");
|
|
561
|
+
});
|
|
562
|
+
it("detects Windows Terminal via WT_SESSION", () => {
|
|
563
|
+
Object.defineProperty(process, "platform", { value: "win32" });
|
|
564
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
565
|
+
value: true,
|
|
566
|
+
writable: true,
|
|
567
|
+
});
|
|
568
|
+
setEnv({ WT_SESSION: "some-guid" });
|
|
569
|
+
const caps = detectTerminal();
|
|
570
|
+
expect(caps.name).toBe("windows-terminal");
|
|
571
|
+
expect(caps.sgrMouse).toBe(true);
|
|
572
|
+
expect(caps.truecolor).toBe(true);
|
|
573
|
+
});
|
|
574
|
+
it("detects VS Code terminal on Windows", () => {
|
|
575
|
+
Object.defineProperty(process, "platform", { value: "win32" });
|
|
576
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
577
|
+
value: true,
|
|
578
|
+
writable: true,
|
|
579
|
+
});
|
|
580
|
+
setEnv({ WT_SESSION: undefined, TERM_PROGRAM: "vscode" });
|
|
581
|
+
const caps = detectTerminal();
|
|
582
|
+
expect(caps.name).toBe("vscode");
|
|
583
|
+
expect(caps.sgrMouse).toBe(true);
|
|
584
|
+
});
|
|
585
|
+
it("detects ConEmu on Windows", () => {
|
|
586
|
+
Object.defineProperty(process, "platform", { value: "win32" });
|
|
587
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
588
|
+
value: true,
|
|
589
|
+
writable: true,
|
|
590
|
+
});
|
|
591
|
+
setEnv({
|
|
592
|
+
WT_SESSION: undefined,
|
|
593
|
+
TERM_PROGRAM: undefined,
|
|
594
|
+
ConEmuPID: "1234",
|
|
595
|
+
});
|
|
596
|
+
const caps = detectTerminal();
|
|
597
|
+
expect(caps.name).toBe("conemu");
|
|
598
|
+
});
|
|
599
|
+
it("detects mintty via TERM + MSYSTEM", () => {
|
|
600
|
+
Object.defineProperty(process, "platform", { value: "win32" });
|
|
601
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
602
|
+
value: true,
|
|
603
|
+
writable: true,
|
|
604
|
+
});
|
|
605
|
+
setEnv({
|
|
606
|
+
WT_SESSION: undefined,
|
|
607
|
+
TERM_PROGRAM: undefined,
|
|
608
|
+
ConEmuPID: undefined,
|
|
609
|
+
TERM: "xterm-256color",
|
|
610
|
+
MSYSTEM: "MINGW64",
|
|
611
|
+
});
|
|
612
|
+
const caps = detectTerminal();
|
|
613
|
+
expect(caps.name).toBe("mintty");
|
|
614
|
+
});
|
|
615
|
+
it("detects tmux on Unix", () => {
|
|
616
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
617
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
618
|
+
value: true,
|
|
619
|
+
writable: true,
|
|
620
|
+
});
|
|
621
|
+
setEnv({ TMUX: "/tmp/tmux-1000/default,1234,0", TERM: "screen-256color" });
|
|
622
|
+
const caps = detectTerminal();
|
|
623
|
+
expect(caps.name).toBe("tmux");
|
|
624
|
+
expect(caps.sgrMouse).toBe(true);
|
|
625
|
+
});
|
|
626
|
+
it("detects GNU screen with limited caps", () => {
|
|
627
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
628
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
629
|
+
value: true,
|
|
630
|
+
writable: true,
|
|
631
|
+
});
|
|
632
|
+
setEnv({
|
|
633
|
+
TMUX: undefined,
|
|
634
|
+
TERM: "screen",
|
|
635
|
+
TERM_PROGRAM: undefined,
|
|
636
|
+
ITERM_SESSION_ID: undefined,
|
|
637
|
+
});
|
|
638
|
+
const caps = detectTerminal();
|
|
639
|
+
expect(caps.name).toBe("screen");
|
|
640
|
+
expect(caps.sgrMouse).toBe(false);
|
|
641
|
+
expect(caps.bracketedPaste).toBe(false);
|
|
642
|
+
});
|
|
643
|
+
it("detects iTerm2", () => {
|
|
644
|
+
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
645
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
646
|
+
value: true,
|
|
647
|
+
writable: true,
|
|
648
|
+
});
|
|
649
|
+
setEnv({
|
|
650
|
+
TMUX: undefined,
|
|
651
|
+
TERM: "xterm-256color",
|
|
652
|
+
TERM_PROGRAM: "iTerm.app",
|
|
653
|
+
});
|
|
654
|
+
const caps = detectTerminal();
|
|
655
|
+
expect(caps.name).toBe("iterm2");
|
|
656
|
+
expect(caps.truecolor).toBe(true);
|
|
657
|
+
});
|
|
658
|
+
it("detects xterm-compatible with COLORTERM truecolor", () => {
|
|
659
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
660
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
661
|
+
value: true,
|
|
662
|
+
writable: true,
|
|
663
|
+
});
|
|
664
|
+
setEnv({
|
|
665
|
+
TMUX: undefined,
|
|
666
|
+
TERM: "xterm-256color",
|
|
667
|
+
TERM_PROGRAM: undefined,
|
|
668
|
+
ITERM_SESSION_ID: undefined,
|
|
669
|
+
COLORTERM: "truecolor",
|
|
670
|
+
});
|
|
671
|
+
const caps = detectTerminal();
|
|
672
|
+
expect(caps.truecolor).toBe(true);
|
|
673
|
+
expect(caps.color256).toBe(true);
|
|
674
|
+
});
|
|
675
|
+
it("detects dumb terminal with minimal caps", () => {
|
|
676
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
677
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
678
|
+
value: true,
|
|
679
|
+
writable: true,
|
|
680
|
+
});
|
|
681
|
+
setEnv({
|
|
682
|
+
TMUX: undefined,
|
|
683
|
+
TERM: "dumb",
|
|
684
|
+
TERM_PROGRAM: undefined,
|
|
685
|
+
ITERM_SESSION_ID: undefined,
|
|
686
|
+
});
|
|
687
|
+
const caps = detectTerminal();
|
|
688
|
+
expect(caps.name).toBe("dumb");
|
|
689
|
+
expect(caps.mouse).toBe(false);
|
|
690
|
+
expect(caps.alternateScreen).toBe(false);
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
694
|
+
// esc.ts — environment-aware init/restore sequences
|
|
695
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
696
|
+
describe("esc environment-aware sequences", () => {
|
|
697
|
+
/** Helper to build a TerminalCaps with overrides. */
|
|
698
|
+
function makeCaps(overrides = {}) {
|
|
699
|
+
return {
|
|
700
|
+
isTTY: true,
|
|
701
|
+
alternateScreen: true,
|
|
702
|
+
bracketedPaste: true,
|
|
703
|
+
mouse: true,
|
|
704
|
+
sgrMouse: true,
|
|
705
|
+
truecolor: true,
|
|
706
|
+
color256: true,
|
|
707
|
+
name: "test",
|
|
708
|
+
...overrides,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
describe("mouseOn / mouseOff", () => {
|
|
712
|
+
it("returns full mouse sequence when SGR is supported", () => {
|
|
713
|
+
const caps = makeCaps({ sgrMouse: true });
|
|
714
|
+
expect(esc.mouseOn(caps)).toBe(esc.mouseTrackingOn);
|
|
715
|
+
expect(esc.mouseOff(caps)).toBe(esc.mouseTrackingOff);
|
|
716
|
+
});
|
|
717
|
+
it("returns minimal mouse sequence when SGR is not supported", () => {
|
|
718
|
+
const caps = makeCaps({ sgrMouse: false });
|
|
719
|
+
const on = esc.mouseOn(caps);
|
|
720
|
+
expect(on).toContain("?1000h");
|
|
721
|
+
expect(on).toContain("?1003h");
|
|
722
|
+
expect(on).not.toContain("?1006h");
|
|
723
|
+
expect(on).not.toContain("?1015h");
|
|
724
|
+
});
|
|
725
|
+
it("returns empty string when mouse is not supported", () => {
|
|
726
|
+
const caps = makeCaps({ mouse: false });
|
|
727
|
+
expect(esc.mouseOn(caps)).toBe("");
|
|
728
|
+
expect(esc.mouseOff(caps)).toBe("");
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
describe("initSequence", () => {
|
|
732
|
+
it("includes all features for a full-caps terminal", () => {
|
|
733
|
+
const caps = makeCaps();
|
|
734
|
+
const seq = esc.initSequence(caps, {
|
|
735
|
+
alternateScreen: true,
|
|
736
|
+
mouse: true,
|
|
737
|
+
});
|
|
738
|
+
expect(seq).toContain(esc.alternateScreenOn);
|
|
739
|
+
expect(seq).toContain(esc.hideCursor);
|
|
740
|
+
expect(seq).toContain(esc.bracketedPasteOn);
|
|
741
|
+
expect(seq).toContain(esc.mouseTrackingOn);
|
|
742
|
+
expect(seq).toContain(esc.clearScreen);
|
|
743
|
+
});
|
|
744
|
+
it("skips alternate screen when not supported", () => {
|
|
745
|
+
const caps = makeCaps({ alternateScreen: false });
|
|
746
|
+
const seq = esc.initSequence(caps, {
|
|
747
|
+
alternateScreen: true,
|
|
748
|
+
mouse: false,
|
|
749
|
+
});
|
|
750
|
+
expect(seq).not.toContain(esc.alternateScreenOn);
|
|
751
|
+
});
|
|
752
|
+
it("skips alternate screen when app opts out", () => {
|
|
753
|
+
const caps = makeCaps();
|
|
754
|
+
const seq = esc.initSequence(caps, {
|
|
755
|
+
alternateScreen: false,
|
|
756
|
+
mouse: false,
|
|
757
|
+
});
|
|
758
|
+
expect(seq).not.toContain(esc.alternateScreenOn);
|
|
759
|
+
});
|
|
760
|
+
it("skips bracketed paste when not supported", () => {
|
|
761
|
+
const caps = makeCaps({ bracketedPaste: false });
|
|
762
|
+
const seq = esc.initSequence(caps, {
|
|
763
|
+
alternateScreen: false,
|
|
764
|
+
mouse: false,
|
|
765
|
+
});
|
|
766
|
+
expect(seq).not.toContain(esc.bracketedPasteOn);
|
|
767
|
+
});
|
|
768
|
+
it("skips mouse when app opts out even if caps support it", () => {
|
|
769
|
+
const caps = makeCaps();
|
|
770
|
+
const seq = esc.initSequence(caps, {
|
|
771
|
+
alternateScreen: false,
|
|
772
|
+
mouse: false,
|
|
773
|
+
});
|
|
774
|
+
expect(seq).not.toContain("?1000h");
|
|
775
|
+
});
|
|
776
|
+
it("returns empty string for non-TTY", () => {
|
|
777
|
+
const caps = makeCaps({ isTTY: false });
|
|
778
|
+
const seq = esc.initSequence(caps, {
|
|
779
|
+
alternateScreen: true,
|
|
780
|
+
mouse: true,
|
|
781
|
+
});
|
|
782
|
+
expect(seq).toBe("");
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
describe("restoreSequence", () => {
|
|
786
|
+
it("includes all restore features for a full-caps terminal", () => {
|
|
787
|
+
const caps = makeCaps();
|
|
788
|
+
const seq = esc.restoreSequence(caps, {
|
|
789
|
+
alternateScreen: true,
|
|
790
|
+
mouse: true,
|
|
791
|
+
});
|
|
792
|
+
expect(seq).toContain(esc.reset);
|
|
793
|
+
expect(seq).toContain(esc.mouseTrackingOff);
|
|
794
|
+
expect(seq).toContain(esc.bracketedPasteOff);
|
|
795
|
+
expect(seq).toContain(esc.showCursor);
|
|
796
|
+
expect(seq).toContain(esc.alternateScreenOff);
|
|
797
|
+
});
|
|
798
|
+
it("mirrors initSequence — skips what init skipped", () => {
|
|
799
|
+
const caps = makeCaps({ bracketedPaste: false, alternateScreen: false });
|
|
800
|
+
const seq = esc.restoreSequence(caps, {
|
|
801
|
+
alternateScreen: true,
|
|
802
|
+
mouse: false,
|
|
803
|
+
});
|
|
804
|
+
expect(seq).not.toContain(esc.bracketedPasteOff);
|
|
805
|
+
expect(seq).not.toContain(esc.alternateScreenOff);
|
|
806
|
+
expect(seq).not.toContain(esc.mouseTrackingOff);
|
|
807
|
+
});
|
|
808
|
+
it("returns empty string for non-TTY", () => {
|
|
809
|
+
const caps = makeCaps({ isTTY: false });
|
|
810
|
+
const seq = esc.restoreSequence(caps, {
|
|
811
|
+
alternateScreen: true,
|
|
812
|
+
mouse: true,
|
|
813
|
+
});
|
|
814
|
+
expect(seq).toBe("");
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
819
|
+
// win32-console.ts
|
|
820
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
821
|
+
describe("win32-console", () => {
|
|
822
|
+
const originalPlatform = process.platform;
|
|
823
|
+
afterEach(() => {
|
|
824
|
+
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
825
|
+
});
|
|
826
|
+
describe("enableWin32Mouse", () => {
|
|
827
|
+
it("returns false on non-win32 platforms", () => {
|
|
828
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
829
|
+
expect(enableWin32Mouse()).toBe(false);
|
|
830
|
+
});
|
|
831
|
+
it("returns false on darwin", () => {
|
|
832
|
+
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
833
|
+
expect(enableWin32Mouse()).toBe(false);
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
describe("restoreWin32Console", () => {
|
|
837
|
+
it("returns false on non-win32 platforms", () => {
|
|
838
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
839
|
+
expect(restoreWin32Console()).toBe(false);
|
|
840
|
+
});
|
|
841
|
+
it("returns false when no original mode was saved", () => {
|
|
842
|
+
// Even on win32, if enableWin32Mouse was never called, restore is a no-op
|
|
843
|
+
Object.defineProperty(process, "platform", { value: "win32" });
|
|
844
|
+
expect(restoreWin32Console()).toBe(false);
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
});
|
|
@@ -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;
|