gentle-shell-bridge 0.0.0-stage → 0.3.0
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/.codegraph/.gitignore +5 -0
- package/PRD.md +144 -0
- package/README.md +745 -2
- package/docs/architecture.md +981 -0
- package/docs/contract.md +1266 -0
- package/docs/state.md +251 -0
- package/examples/bash-card.ts +1694 -0
- package/gentle-shell-bridge-0.3.0.tgz +0 -0
- package/odd/tasks/palette-sections.md +30 -0
- package/odd/tasks/plugin-keymap.md +32 -0
- package/odd/tasks/plugin-surface-contract.md +55 -0
- package/package.json +35 -4
- package/src/api.ts +492 -0
- package/src/arm.ts +83 -0
- package/src/bus.ts +558 -0
- package/src/capture.ts +886 -0
- package/src/card-style-line.ts +112 -0
- package/src/card-style-rows.ts +390 -0
- package/src/card-style-transform.ts +862 -0
- package/src/card-style-wiring.ts +472 -0
- package/src/card-styles.ts +159 -0
- package/src/config.ts +71 -0
- package/src/extension.ts +271 -0
- package/src/hook.ts +310 -0
- package/src/index.ts +71 -0
- package/src/notices.ts +137 -0
- package/src/plugin-lines.ts +865 -0
- package/src/plugin-surface.ts +218 -0
- package/src/presence.ts +259 -0
- package/src/repaint.ts +351 -0
- package/src/row-labels.ts +146 -0
- package/src/row-names.ts +273 -0
- package/src/row-origin.ts +144 -0
- package/src/rows.ts +248 -0
- package/src/section-panel.ts +378 -0
- package/src/section-pointer.ts +164 -0
- package/src/sections-rows.ts +128 -0
- package/src/sidebar-render.ts +535 -0
- package/src/sidebar.ts +919 -0
- package/src/status.ts +1161 -0
- package/src/store.ts +279 -0
- package/src/tool-design-adoption.ts +263 -0
- package/src/tool-design-rows.ts +246 -0
- package/src/tool-design-wiring.ts +428 -0
- package/src/tool-designs.ts +402 -0
- package/src/transform-pipeline.ts +351 -0
- package/src/types.ts +442 -0
- package/src/visibility.ts +154 -0
- package/test/api.test.ts +395 -0
- package/test/apply-on-move.test.ts +477 -0
- package/test/arm.test.ts +220 -0
- package/test/bridge-ready.test.ts +309 -0
- package/test/bus.test.ts +165 -0
- package/test/button-rows.test.ts +409 -0
- package/test/capture.test.ts +356 -0
- package/test/card-style-rows.test.ts +510 -0
- package/test/card-style-transform.test.ts +379 -0
- package/test/card-style-wiring.test.ts +258 -0
- package/test/card-styles-e2e.test.ts +491 -0
- package/test/card-styles.test.ts +324 -0
- package/test/card-transcript-path.test.ts +178 -0
- package/test/config.test.ts +93 -0
- package/test/e2e.test.ts +443 -0
- package/test/example-bash-identity.test.ts +372 -0
- package/test/example-bash-matrix.test.ts +856 -0
- package/test/example-designs.test.ts +734 -0
- package/test/example-neon-transform.test.ts +283 -0
- package/test/example-plain-style.test.ts +1268 -0
- package/test/extension.test.ts +160 -0
- package/test/fake-events.ts +58 -0
- package/test/fake-host.ts +211 -0
- package/test/fake-sidebar.ts +516 -0
- package/test/fake-transcript.ts +486 -0
- package/test/frame-telemetry.test.ts +417 -0
- package/test/hook.test.ts +196 -0
- package/test/manifest.test.ts +105 -0
- package/test/new-category.test.ts +979 -0
- package/test/notices.test.ts +328 -0
- package/test/plugin-integration-lines.test.ts +979 -0
- package/test/plugin-surface.test.ts +152 -0
- package/test/presence.test.ts +459 -0
- package/test/rail-surfaces.test.ts +457 -0
- package/test/rail-width.test.ts +423 -0
- package/test/regression.test.ts +483 -0
- package/test/repaint-api.test.ts +200 -0
- package/test/reserved-categories.test.ts +361 -0
- package/test/row-labels.test.ts +284 -0
- package/test/row-names.test.ts +473 -0
- package/test/row-origin.test.ts +395 -0
- package/test/section-interaction.test.ts +364 -0
- package/test/section-panel.test.ts +403 -0
- package/test/sections-rows.test.ts +291 -0
- package/test/sidebar-backend.test.ts +471 -0
- package/test/sidebar-e2e.test.ts +331 -0
- package/test/sidebar-hover.test.ts +276 -0
- package/test/sidebar-independent.test.ts +349 -0
- package/test/sidebar-render.test.ts +281 -0
- package/test/sidebar-sections.test.ts +245 -0
- package/test/status-card-styles.test.ts +201 -0
- package/test/status-section-frame.test.ts +181 -0
- package/test/status-sections.test.ts +243 -0
- package/test/status.test.ts +209 -0
- package/test/store.test.ts +192 -0
- package/test/surface-repaint-cost.test.ts +520 -0
- package/test/tool-design-chain.test.ts +483 -0
- package/test/tool-design-cost.test.ts +473 -0
- package/test/tool-design-rows.test.ts +359 -0
- package/test/tool-designs.test.ts +745 -0
- package/test/transform-pipeline.test.ts +465 -0
- package/test/transforms-bus.test.ts +233 -0
- package/test/verifier-width-regressions.test.ts +164 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verifier's width findings, pinned (W2-T2–W2-T4, A-T5, C-T2/C-T3 adjacent).
|
|
3
|
+
*
|
|
4
|
+
* Each expectation below is the value pi-tui's own `visibleWidth` produces for that string
|
|
5
|
+
* (`@earendil-works/pi-tui/dist/utils.js:126-268`, measured with the installed host copy), so this
|
|
6
|
+
* file is a differential fixture with pinned numbers instead of a second implementation:
|
|
7
|
+
*
|
|
8
|
+
* - a cluster is charged the way `graphemeWidth` charges it: zero for a cluster of marks, two only
|
|
9
|
+
* when `couldBeEmoji` AND `\p{RGI_Emoji}` both hold (so `©` is ONE column, `©️` is two), the base
|
|
10
|
+
* code point's East Asian width otherwise, plus every trailing halfwidth/fullwidth form and the
|
|
11
|
+
* Thai/Lao AM vowels;
|
|
12
|
+
* - a row that already fits is byte-identical, escapes included, even at width zero;
|
|
13
|
+
* - a cut never splits a grapheme cluster, even when an SGR sequence sits inside it.
|
|
14
|
+
*
|
|
15
|
+
* The README assertion at the end pins the exact footer string the host prints for a bridge row
|
|
16
|
+
* (A5).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
import test from "node:test";
|
|
22
|
+
|
|
23
|
+
import { registerTransform, resetTransformsForTests } from "../src/bus.ts";
|
|
24
|
+
import { clipToWidth, hostColumns } from "../src/card-style-transform.ts";
|
|
25
|
+
import { applyTransforms, resetTransformPipelineForTests } from "../src/transform-pipeline.ts";
|
|
26
|
+
|
|
27
|
+
test.beforeEach(() => {
|
|
28
|
+
resetTransformsForTests();
|
|
29
|
+
resetTransformPipelineForTests();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------------------------
|
|
33
|
+
// The host's measure, cluster by cluster
|
|
34
|
+
// ---------------------------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
test("a symbol that is not an RGI emoji is one column; a qualified or keycap emoji is two", () => {
|
|
37
|
+
for (const symbol of ["©", "®", "™", "☺"]) {
|
|
38
|
+
assert.equal(hostColumns(symbol), 1, `${symbol} is not an RGI emoji: the host charges one column`);
|
|
39
|
+
}
|
|
40
|
+
assert.equal(hostColumns("©️"), 2, "the emoji presentation selector makes it an RGI emoji");
|
|
41
|
+
assert.equal(hostColumns("1️⃣"), 2, "a keycap sequence is one two-column cluster");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("a trailing halfwidth/fullwidth form is charged its own column", () => {
|
|
45
|
+
assert.equal(hostColumns("ガ"), 2, "halfwidth KA plus the voiced sound mark");
|
|
46
|
+
assert.equal(hostColumns("ガギ"), 4, "two voiced halfwidth kana are four columns");
|
|
47
|
+
assert.equal(hostColumns("ア"), 1, "a bare halfwidth kana is one column");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("a cluster of marks charges nothing, and a leading mark rides its base", () => {
|
|
51
|
+
assert.equal(hostColumns("्"), 0, "a lone Devanagari virama occupies no cell");
|
|
52
|
+
assert.equal(hostColumns("़क"), 1, "the leading nukta is stripped before measuring क");
|
|
53
|
+
assert.equal(hostColumns("क्"), 1);
|
|
54
|
+
assert.equal(hostColumns("e\u0301"), 1, "a combining acute rides its base");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("a Devanagari spacing mark occupies its own cell, as the host allocates one", () => {
|
|
58
|
+
assert.equal(hostColumns("ि"), 1, "U+093F is a Spacing_Mark: the host charges it one column");
|
|
59
|
+
assert.equal(hostColumns("ा"), 1, "U+093E is a Spacing_Mark too");
|
|
60
|
+
assert.equal(hostColumns("नमस्ते"), 4, "the host's own number for नमस्ते (pi-tui 1.5.x)");
|
|
61
|
+
assert.equal(hostColumns("हिन्दी"), 5, "the host's own number for हिन्दी");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------------------------
|
|
65
|
+
// W2-3: a consonant that follows a mark is charged, so it cannot overflow a narrower frame
|
|
66
|
+
// ---------------------------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
test("an Indic conjunct that follows a mark is two columns, and is never cut into one column", () => {
|
|
69
|
+
// pi-tui 1.5.x charges the base (क) plus the visible consonant after the virama (ष): 2 columns.
|
|
70
|
+
// Charging the cluster as its base alone made a "one column" frame hold two.
|
|
71
|
+
assert.equal(hostColumns("क्ष"), 2, "the host charges the conjunct two columns");
|
|
72
|
+
assert.equal(hostColumns("क्ष"), 2, "with or without the joiner");
|
|
73
|
+
assert.equal(hostColumns("क्षा"), 3, "the trailing spacing mark adds its own cell");
|
|
74
|
+
|
|
75
|
+
assert.equal(
|
|
76
|
+
clipToWidth("क्षx", 1),
|
|
77
|
+
"",
|
|
78
|
+
"a two-column cluster cannot be emitted in a one-column frame",
|
|
79
|
+
);
|
|
80
|
+
assert.equal(clipToWidth("क्षx", 2), "क्ष", "at two columns the whole cluster fits");
|
|
81
|
+
assert.equal(hostColumns(clipToWidth("क्षx", 2)), 2, "and the result measures exactly two");
|
|
82
|
+
for (const width of [0, 1, 2, 3]) {
|
|
83
|
+
assert.ok(
|
|
84
|
+
hostColumns(clipToWidth("क्षx", width)) <= width,
|
|
85
|
+
`the cut at ${width} never exceeds the frame`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("the pipeline clips an overflowing Indic conjunct instead of emitting it", () => {
|
|
91
|
+
registerTransform({ id: "indic", transform: () => ["क्षx"] });
|
|
92
|
+
|
|
93
|
+
assert.deepEqual(applyTransforms(["host"], 1), [""], "nothing wider than the frame reaches the host");
|
|
94
|
+
assert.deepEqual(applyTransforms(["host"], 2), ["क्ष"]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("a Thai/Lao AM vowel adds a column, and an ignorable symbol adds none", () => {
|
|
98
|
+
assert.equal(hostColumns("กำ"), 2, "the trailing AM vowel is charged one extra column");
|
|
99
|
+
assert.equal(hostColumns("ເກຳ"), 3, "the same rule for Lao");
|
|
100
|
+
assert.equal(hostColumns("\u00ad"), 0, "a soft hyphen is default-ignorable: no columns");
|
|
101
|
+
assert.equal(hostColumns("👨👩👧👦"), 2, "a ZWJ family is one two-column cluster");
|
|
102
|
+
assert.equal(hostColumns("👍🏽"), 2, "a skin-tone modifier rides its base");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------------------------
|
|
106
|
+
// Fitting rows are byte-identical, at every width — including zero
|
|
107
|
+
// ---------------------------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
test("a row that fits at width zero keeps its bytes, escapes included", () => {
|
|
110
|
+
const sgr = "\u001b[31m";
|
|
111
|
+
const link = "\u001b]8;;http://example.com\u0007\u001b]8;;\u0007";
|
|
112
|
+
const background = "\u001b[48;5;236m\u001b[0m";
|
|
113
|
+
|
|
114
|
+
assert.equal(clipToWidth(sgr, 0), sgr, "an SGR-only row has no columns and must survive");
|
|
115
|
+
assert.equal(clipToWidth(link, 0), link, "an empty hyperlink is the same empty hyperlink");
|
|
116
|
+
assert.equal(clipToWidth(background, 0), background);
|
|
117
|
+
assert.equal(clipToWidth("\u001b[31m\u001b[39m", 0), "\u001b[31m\u001b[39m");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("a row with no columns is passed through, and a row with columns is emptied, at width zero", () => {
|
|
121
|
+
assert.equal(clipToWidth("", 0), "");
|
|
122
|
+
assert.equal(clipToWidth("A", 0), "", "a visible column cannot be drawn in zero columns");
|
|
123
|
+
assert.equal(clipToWidth("\u001b[31mA", 0), "", "the paint alone is not a row fit to draw");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("the pipeline returns an ANSI-only row untouched at width zero", () => {
|
|
127
|
+
registerTransform({ id: "ansi-only", transform: () => ["\u001b[31m", "A"] });
|
|
128
|
+
|
|
129
|
+
assert.deepEqual(applyTransforms(["host"], 0), ["\u001b[31m", ""], "only the visible column is cut");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------------------------
|
|
133
|
+
// The cut never splits a cluster, even with SGR interleaved
|
|
134
|
+
// ---------------------------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
test("an SGR inside a grapheme cluster does not split it, and the kept paint is reset", () => {
|
|
137
|
+
const row = "👨\u001b[31m👩👧👦AB";
|
|
138
|
+
assert.equal(hostColumns(row), 4, "the host measures the family as one cluster plus A and B");
|
|
139
|
+
|
|
140
|
+
const clipped = clipToWidth(row, 2);
|
|
141
|
+
|
|
142
|
+
assert.equal(clipped, "👨\u001b[31m👩👧👦\u001b[0m", "the whole family is kept, then the paint is reset");
|
|
143
|
+
assert.equal(clipped.split("\u200d").length - 1, 3, "every joiner of the family survived the cut");
|
|
144
|
+
assert.equal(hostColumns(clipped), 2, "and it measures the host's width exactly");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("the pipeline applies that cut to what a transform returns", () => {
|
|
148
|
+
registerTransform({ id: "family", transform: () => ["👨\u001b[31m👩👧👦AB"] });
|
|
149
|
+
|
|
150
|
+
assert.deepEqual(applyTransforms(["host"], 2), ["👨\u001b[31m👩👧👦\u001b[0m"]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------------------------
|
|
154
|
+
// A5 — the README states the host's exact footer
|
|
155
|
+
// ---------------------------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
test("the README states the host's exact footer for a focused bridge row (A5)", () => {
|
|
158
|
+
const readme = readFileSync(new URL("../README.md", import.meta.url), "utf8");
|
|
159
|
+
|
|
160
|
+
assert.ok(
|
|
161
|
+
readme.includes("←/→ choose · Enter apply"),
|
|
162
|
+
"the README must name the footer verbatim, as the host prints it",
|
|
163
|
+
);
|
|
164
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2024",
|
|
4
|
+
"lib": ["ES2024"],
|
|
5
|
+
"module": "nodenext",
|
|
6
|
+
"moduleResolution": "nodenext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"erasableSyntaxOnly": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"skipLibCheck": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*.ts", "test/**/*.ts"]
|
|
18
|
+
}
|