@dungle-scrubs/tallow 0.9.4 → 0.9.6
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/cli.js +7 -4
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/interactive-mode-patch.d.ts +24 -12
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +229 -146
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/interactive-reset.d.ts +49 -0
- package/dist/interactive-reset.d.ts.map +1 -0
- package/dist/interactive-reset.js +40 -0
- package/dist/interactive-reset.js.map +1 -0
- package/dist/pi-tui-editor-patch.d.ts +10 -0
- package/dist/pi-tui-editor-patch.d.ts.map +1 -0
- package/dist/pi-tui-editor-patch.js +159 -0
- package/dist/pi-tui-editor-patch.js.map +1 -0
- package/dist/pi-tui-patch.d.ts +2 -0
- package/dist/pi-tui-patch.d.ts.map +1 -0
- package/dist/pi-tui-patch.js +563 -0
- package/dist/pi-tui-patch.js.map +1 -0
- package/dist/pi-tui-settings-list-patch.d.ts +11 -0
- package/dist/pi-tui-settings-list-patch.d.ts.map +1 -0
- package/dist/pi-tui-settings-list-patch.js +38 -0
- package/dist/pi-tui-settings-list-patch.js.map +1 -0
- package/dist/reset-diagnostics.d.ts +69 -0
- package/dist/reset-diagnostics.d.ts.map +1 -0
- package/dist/reset-diagnostics.js +41 -0
- package/dist/reset-diagnostics.js.map +1 -0
- package/dist/sdk.d.ts +5 -21
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +180 -149
- package/dist/sdk.js.map +1 -1
- package/dist/workspace-transition-interactive.d.ts +1 -0
- package/dist/workspace-transition-interactive.d.ts.map +1 -1
- package/dist/workspace-transition-interactive.js +7 -17
- package/dist/workspace-transition-interactive.js.map +1 -1
- package/extensions/__integration__/audit-findings.test.ts +4 -5
- package/extensions/_icons/index.ts +2 -4
- package/extensions/_shared/__tests__/image-metadata.test.ts +33 -0
- package/extensions/_shared/__tests__/terminal-links.test.ts +18 -0
- package/extensions/_shared/image-metadata.ts +99 -0
- package/extensions/_shared/inline-preview.ts +1 -1
- package/extensions/_shared/terminal-links.ts +22 -0
- package/extensions/ask-user-question-tool/index.ts +0 -3
- package/extensions/clear/__tests__/clear.test.ts +269 -2
- package/extensions/command-expansion/index.ts +1 -1
- package/extensions/context-files/index.ts +5 -1
- package/extensions/context-fork/__tests__/context-fork.test.ts +94 -1
- package/extensions/context-fork/extension.json +1 -1
- package/extensions/context-fork/index.ts +32 -0
- package/extensions/edit-tool-enhanced/index.ts +2 -1
- package/extensions/hooks/index.ts +33 -11
- package/extensions/loop/index.ts +14 -1
- package/extensions/lsp/index.ts +64 -13
- package/extensions/lsp/package.json +2 -2
- package/extensions/random-spinner/index.ts +7 -642
- package/extensions/read-tool-enhanced/index.ts +6 -8
- package/extensions/render-stabilizer/__tests__/render-stabilizer.test.ts +2 -3
- package/extensions/render-stabilizer/index.ts +6 -6
- package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +26 -0
- package/extensions/slash-command-bridge/index.ts +14 -2
- package/extensions/subagent-tool/model-resolver.ts +274 -7
- package/extensions/tasks/commands/register-tasks-extension.ts +9 -9
- package/extensions/teams-tool/tools/register-extension.ts +1 -3
- package/extensions/web-search-tool/index.ts +2 -1
- package/extensions/write-tool-enhanced/index.ts +2 -1
- package/node_modules/@mariozechner/pi-tui/README.md +56 -34
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts +18 -13
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js +182 -113
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js +3 -3
- package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts +45 -36
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.js +489 -325
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts +1 -99
- package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/image.js +17 -192
- package/node_modules/@mariozechner/pi-tui/dist/components/image.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/input.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/input.js +57 -60
- package/node_modules/@mariozechner/pi-tui/dist/components/input.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts +2 -69
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.js +5 -102
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/markdown.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js +111 -53
- package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts +19 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js +78 -67
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts +1 -25
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js +13 -50
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +8 -10
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/index.js +6 -9
- package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +108 -238
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +108 -365
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +33 -48
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.js +239 -155
- package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts +14 -94
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js +44 -186
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +13 -58
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js +78 -111
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +24 -110
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.js +188 -435
- package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts +0 -18
- package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/utils.js +251 -119
- package/node_modules/@mariozechner/pi-tui/dist/utils.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/package.json +6 -6
- package/node_modules/@mariozechner/pi-tui/src/__tests__/__snapshots__/render.test.ts.snap +3 -40
- package/node_modules/@mariozechner/pi-tui/src/__tests__/image-component.test.ts +71 -81
- package/node_modules/@mariozechner/pi-tui/src/__tests__/render.test.ts +0 -33
- package/node_modules/@mariozechner/pi-tui/src/__tests__/terminal-image.test.ts +93 -334
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +1 -1
- package/node_modules/@mariozechner/pi-tui/src/__tests__/utils.test.ts +11 -196
- package/node_modules/@mariozechner/pi-tui/src/autocomplete.ts +228 -142
- package/node_modules/@mariozechner/pi-tui/src/components/cancellable-loader.ts +3 -3
- package/node_modules/@mariozechner/pi-tui/src/components/editor.ts +624 -390
- package/node_modules/@mariozechner/pi-tui/src/components/image.ts +17 -227
- package/node_modules/@mariozechner/pi-tui/src/components/input.ts +71 -63
- package/node_modules/@mariozechner/pi-tui/src/components/loader.ts +5 -137
- package/node_modules/@mariozechner/pi-tui/src/components/markdown.ts +143 -52
- package/node_modules/@mariozechner/pi-tui/src/components/select-list.ts +136 -70
- package/node_modules/@mariozechner/pi-tui/src/components/settings-list.ts +12 -51
- package/node_modules/@mariozechner/pi-tui/src/index.ts +17 -36
- package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +148 -421
- package/node_modules/@mariozechner/pi-tui/src/keys.ts +253 -181
- package/node_modules/@mariozechner/pi-tui/src/terminal-image.ts +51 -252
- package/node_modules/@mariozechner/pi-tui/src/terminal.ts +78 -133
- package/node_modules/@mariozechner/pi-tui/src/tui.ts +202 -478
- package/node_modules/@mariozechner/pi-tui/src/utils.ts +289 -125
- package/node_modules/@mariozechner/pi-tui/tsconfig.build.json +1 -0
- package/package.json +13 -13
- package/packages/tallow-tui/node_modules/@types/mime-types/README.md +8 -2
- package/packages/tallow-tui/node_modules/@types/mime-types/index.d.ts +6 -0
- package/packages/tallow-tui/node_modules/@types/mime-types/package.json +9 -3
- package/packages/tallow-tui/node_modules/get-east-asian-width/lookup-data.js +18 -0
- package/packages/tallow-tui/node_modules/get-east-asian-width/lookup.js +116 -384
- package/packages/tallow-tui/node_modules/get-east-asian-width/package.json +5 -4
- package/packages/tallow-tui/node_modules/get-east-asian-width/utilities.js +24 -0
- package/packages/tallow-tui/node_modules/marked/README.md +5 -4
- package/packages/tallow-tui/node_modules/marked/bin/main.js +10 -8
- package/packages/tallow-tui/node_modules/marked/bin/marked.js +2 -1
- package/packages/tallow-tui/node_modules/marked/lib/marked.d.ts +156 -125
- package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js +67 -2179
- package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js.map +3 -3
- package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js +67 -2201
- package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js.map +3 -3
- package/packages/tallow-tui/node_modules/marked/man/marked.1 +4 -2
- package/packages/tallow-tui/node_modules/marked/man/marked.1.md +2 -1
- package/packages/tallow-tui/node_modules/marked/package.json +26 -34
- package/skills/tallow-expert/SKILL.md +1 -3
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts +0 -32
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.js +0 -46
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.js.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts +0 -52
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js +0 -89
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts +0 -14
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js +0 -55
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js.map +0 -1
- package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-change-listener.test.ts +0 -121
- package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-ghost-text.test.ts +0 -112
- package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +0 -134
- package/node_modules/@mariozechner/pi-tui/src/__tests__/settings-list.test.ts +0 -81
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +0 -555
- package/node_modules/@mariozechner/pi-tui/src/border-styles.ts +0 -60
- package/node_modules/@mariozechner/pi-tui/src/components/bordered-box.ts +0 -113
- package/node_modules/@mariozechner/pi-tui/src/test-utils/capability-env.ts +0 -56
- package/packages/tallow-tui/node_modules/marked/lib/marked.cjs +0 -2211
- package/packages/tallow-tui/node_modules/marked/lib/marked.cjs.map +0 -7
- package/packages/tallow-tui/node_modules/marked/lib/marked.d.cts +0 -728
- package/packages/tallow-tui/node_modules/marked/marked.min.js +0 -69
|
@@ -1,363 +1,122 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for image
|
|
3
|
-
* natural width clamping, and aspect ratio preservation.
|
|
2
|
+
* Tests for upstream terminal-image primitives kept in the fork.
|
|
4
3
|
*/
|
|
5
4
|
import { describe, expect, it } from "bun:test";
|
|
6
5
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
calculateImageRows,
|
|
7
|
+
getGifDimensions,
|
|
8
|
+
getImageDimensions,
|
|
9
|
+
getPngDimensions,
|
|
10
|
+
getWebpDimensions,
|
|
11
11
|
type ImageDimensions,
|
|
12
|
-
imageFormatToMime,
|
|
13
12
|
renderImage,
|
|
13
|
+
resetCapabilitiesCache,
|
|
14
14
|
} from "../terminal-image.js";
|
|
15
|
-
import { withCapabilityEnv } from "../test-utils/capability-env.js";
|
|
16
15
|
|
|
17
|
-
const DEFAULT_CELL = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
function
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const dims: ImageDimensions = { widthPx: 100, heightPx: 1 };
|
|
48
|
-
const layout = calculateImageLayout(dims, 60, DEFAULT_CELL);
|
|
49
|
-
expect(layout.rows).toBeGreaterThanOrEqual(1);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe("natural width clamping", () => {
|
|
54
|
-
it("clamps small images to their natural column count", () => {
|
|
55
|
-
const dims: ImageDimensions = { widthPx: 100, heightPx: 100 };
|
|
56
|
-
const layout = calculateImageLayout(dims, 60, DEFAULT_CELL);
|
|
57
|
-
// Natural cols = ceil(100/9) = 12 — should NOT stretch to 60
|
|
58
|
-
expect(layout.columns).toBe(12);
|
|
59
|
-
expect(layout.columns).toBeLessThan(60);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("does not clamp images wider than maxWidth", () => {
|
|
63
|
-
const dims: ImageDimensions = { widthPx: 3000, heightPx: 2000 };
|
|
64
|
-
const layout = calculateImageLayout(dims, 60, DEFAULT_CELL);
|
|
65
|
-
expect(layout.columns).toBe(60);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("uses natural width when it equals maxWidth", () => {
|
|
69
|
-
// 540px / 9px = 60 cols exactly
|
|
70
|
-
const dims: ImageDimensions = { widthPx: 540, heightPx: 270 };
|
|
71
|
-
const layout = calculateImageLayout(dims, 60, DEFAULT_CELL);
|
|
72
|
-
expect(layout.columns).toBe(60);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe("maxHeightCells clamping", () => {
|
|
77
|
-
it("reduces columns proportionally when height-clamped (portrait)", () => {
|
|
78
|
-
const dims: ImageDimensions = { widthPx: 1000, heightPx: 2000 };
|
|
79
|
-
const layout = calculateImageLayout(dims, 60, DEFAULT_CELL, 25);
|
|
80
|
-
expect(layout.rows).toBe(25);
|
|
81
|
-
// Unclamped: 60 cols → rows = ceil(2000*(540/1000)/18) = 60
|
|
82
|
-
// Clamped: heightScale = (25*18)/2000 = 0.225, cols = floor(1000*0.225/9) = 25
|
|
83
|
-
expect(layout.columns).toBe(25);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("does not clamp landscape images below maxHeightCells", () => {
|
|
87
|
-
const dims: ImageDimensions = { widthPx: 2000, heightPx: 1000 };
|
|
88
|
-
const layout = calculateImageLayout(dims, 60, DEFAULT_CELL, 25);
|
|
89
|
-
// Landscape at 60 cols: rows = ceil(1000 * (540/2000) / 18) = 15
|
|
90
|
-
expect(layout.rows).toBe(15);
|
|
91
|
-
expect(layout.columns).toBe(60);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("reduces columns proportionally when height-clamped (tall portrait)", () => {
|
|
95
|
-
const dims: ImageDimensions = { widthPx: 500, heightPx: 3000 };
|
|
96
|
-
const layout = calculateImageLayout(dims, 60, DEFAULT_CELL, 25);
|
|
97
|
-
expect(layout.rows).toBe(25);
|
|
98
|
-
// heightScale = 450/3000 = 0.15, cols = floor(500*0.15/9) = 8
|
|
99
|
-
expect(layout.columns).toBe(8);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("reduces columns proportionally when height-clamped (square)", () => {
|
|
103
|
-
const dims: ImageDimensions = { widthPx: 1024, heightPx: 1024 };
|
|
104
|
-
const layout = calculateImageLayout(dims, 60, DEFAULT_CELL, 25);
|
|
105
|
-
expect(layout.rows).toBe(25);
|
|
106
|
-
// heightScale = 450/1024 = 0.4395, cols = floor(1024*0.4395/9) = 50
|
|
107
|
-
expect(layout.columns).toBe(50);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("does nothing when maxHeightCells is undefined", () => {
|
|
111
|
-
const dims: ImageDimensions = { widthPx: 500, heightPx: 3000 };
|
|
112
|
-
const layout = calculateImageLayout(dims, 60, DEFAULT_CELL);
|
|
113
|
-
// No clamp — natural cols = ceil(500/9) = 56, rows = ceil(3000*(56*9/500)/18) = 168
|
|
114
|
-
expect(layout.rows).toBeGreaterThan(100);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("columns never exceed maxWidthCells", () => {
|
|
118
|
-
const dims: ImageDimensions = { widthPx: 4000, heightPx: 4001 };
|
|
119
|
-
const layout = calculateImageLayout(dims, 60, DEFAULT_CELL, 25);
|
|
120
|
-
expect(layout.columns).toBeLessThanOrEqual(60);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("columns are at least 1 after clamping", () => {
|
|
124
|
-
// Extremely tall, narrow image
|
|
125
|
-
const dims: ImageDimensions = { widthPx: 10, heightPx: 10000 };
|
|
126
|
-
const layout = calculateImageLayout(dims, 60, DEFAULT_CELL, 5);
|
|
127
|
-
expect(layout.columns).toBeGreaterThanOrEqual(1);
|
|
128
|
-
expect(layout.rows).toBe(5);
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
describe("combined natural width + height clamping", () => {
|
|
133
|
-
it("clamps width first, then height reduces columns further", () => {
|
|
134
|
-
// Small AND tall: 100×1000
|
|
135
|
-
const dims: ImageDimensions = { widthPx: 100, heightPx: 1000 };
|
|
136
|
-
const layout = calculateImageLayout(dims, 60, DEFAULT_CELL, 25);
|
|
137
|
-
// Natural cols = ceil(100/9) = 12 (clamped from 60)
|
|
138
|
-
// At 12 cols: rows = ceil(1000*(108/100)/18) = 60
|
|
139
|
-
// Clamped: heightScale = 450/1000 = 0.45, cols = floor(100*0.45/9) = 5
|
|
140
|
-
expect(layout.rows).toBe(25);
|
|
141
|
-
expect(layout.columns).toBe(5);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe("narrow-width regression", () => {
|
|
146
|
-
it("keeps portrait aspect-ratio error bounded at narrow widths", () => {
|
|
147
|
-
const portrait: ImageDimensions = { widthPx: 1179, heightPx: 2556 };
|
|
148
|
-
for (const width of [8, 12, 20, 30]) {
|
|
149
|
-
const layout = calculateImageLayout(portrait, width, DEFAULT_CELL);
|
|
150
|
-
expect(layout.columns).toBeGreaterThanOrEqual(1);
|
|
151
|
-
expect(layout.rows).toBeGreaterThanOrEqual(1);
|
|
152
|
-
expect(getAspectError(portrait, layout.columns, layout.rows)).toBeLessThan(0.12);
|
|
16
|
+
const DEFAULT_CELL = { heightPx: 18, widthPx: 9 };
|
|
17
|
+
|
|
18
|
+
const TINY_PNG_BASE64 =
|
|
19
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO5W2fkAAAAASUVORK5CYII=";
|
|
20
|
+
const GIF_BASE64 = "R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=";
|
|
21
|
+
const WEBP_BASE64 =
|
|
22
|
+
"UklGRiYAAABXRUJQVlA4IBoAAAAQAgCdASoBAAEAAUAmJaACdLoB+AADsAD+8ut//NgVzXPv9//S4P0uD9LgAAA=";
|
|
23
|
+
|
|
24
|
+
type CapabilityEnvOverrides = Readonly<Record<string, string | undefined>>;
|
|
25
|
+
|
|
26
|
+
function withCapabilityEnv<T>(overrides: CapabilityEnvOverrides, run: () => T): T {
|
|
27
|
+
const keys = [
|
|
28
|
+
"COLORTERM",
|
|
29
|
+
"GHOSTTY_RESOURCES_DIR",
|
|
30
|
+
"ITERM_SESSION_ID",
|
|
31
|
+
"KITTY_WINDOW_ID",
|
|
32
|
+
"TERM",
|
|
33
|
+
"TERM_PROGRAM",
|
|
34
|
+
"TMUX",
|
|
35
|
+
"WEZTERM_PANE",
|
|
36
|
+
] as const;
|
|
37
|
+
const previous: Partial<Record<(typeof keys)[number], string | undefined>> = {};
|
|
38
|
+
for (const key of keys) {
|
|
39
|
+
previous[key] = process.env[key];
|
|
40
|
+
if (Object.hasOwn(overrides, key)) {
|
|
41
|
+
const value = overrides[key];
|
|
42
|
+
if (value === undefined) {
|
|
43
|
+
delete process.env[key];
|
|
44
|
+
} else {
|
|
45
|
+
process.env[key] = value;
|
|
153
46
|
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// ── detectImageFormat ────────────────────────────────────────────────────────
|
|
178
|
-
|
|
179
|
-
describe("detectImageFormat", () => {
|
|
180
|
-
it("detects PNG from magic bytes", () => {
|
|
181
|
-
const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0]);
|
|
182
|
-
expect(detectImageFormat(png)).toBe("png");
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it("detects JPEG from magic bytes", () => {
|
|
186
|
-
const jpeg = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
|
187
|
-
expect(detectImageFormat(jpeg)).toBe("jpeg");
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("detects JPEG with EXIF marker", () => {
|
|
191
|
-
const jpeg = Buffer.from([0xff, 0xd8, 0xff, 0xe1, 0, 0, 0, 0, 0, 0, 0, 0]);
|
|
192
|
-
expect(detectImageFormat(jpeg)).toBe("jpeg");
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("detects GIF87a", () => {
|
|
196
|
-
// "GIF87a" = 47 49 46 38 37 61
|
|
197
|
-
const gif = Buffer.from([0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0, 0, 0, 0, 0, 0]);
|
|
198
|
-
expect(detectImageFormat(gif)).toBe("gif");
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("detects GIF89a", () => {
|
|
202
|
-
const gif = Buffer.from([0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0, 0, 0, 0, 0, 0]);
|
|
203
|
-
expect(detectImageFormat(gif)).toBe("gif");
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("detects WebP from RIFF...WEBP header", () => {
|
|
207
|
-
// "RIFF" + 4 bytes size + "WEBP"
|
|
208
|
-
const webp = Buffer.from([
|
|
209
|
-
0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50,
|
|
210
|
-
]);
|
|
211
|
-
expect(detectImageFormat(webp)).toBe("webp");
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("returns null for text content", () => {
|
|
215
|
-
const text = Buffer.from("Hello, world! This is plain text.");
|
|
216
|
-
expect(detectImageFormat(text)).toBeNull();
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it("returns null for empty buffer", () => {
|
|
220
|
-
expect(detectImageFormat(Buffer.alloc(0))).toBeNull();
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it("returns null for buffer too short for any format", () => {
|
|
224
|
-
expect(detectImageFormat(Buffer.from([0xff, 0xd8]))).toBeNull();
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it("returns null for partial PNG header", () => {
|
|
228
|
-
// Only 4 bytes of PNG header (needs 8)
|
|
229
|
-
expect(detectImageFormat(Buffer.from([0x89, 0x50, 0x4e, 0x47]))).toBeNull();
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it("returns null for RIFF without WEBP marker", () => {
|
|
233
|
-
// RIFF + size + "AVI " instead of "WEBP"
|
|
234
|
-
const avi = Buffer.from([
|
|
235
|
-
0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x41, 0x56, 0x49, 0x20,
|
|
236
|
-
]);
|
|
237
|
-
expect(detectImageFormat(avi)).toBeNull();
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
// ── imageFormatToMime ────────────────────────────────────────────────────────
|
|
242
|
-
|
|
243
|
-
describe("imageFormatToMime", () => {
|
|
244
|
-
it("maps png to image/png", () => {
|
|
245
|
-
expect(imageFormatToMime("png")).toBe("image/png");
|
|
246
|
-
});
|
|
47
|
+
} else {
|
|
48
|
+
delete process.env[key];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
resetCapabilitiesCache();
|
|
52
|
+
try {
|
|
53
|
+
return run();
|
|
54
|
+
} finally {
|
|
55
|
+
for (const key of keys) {
|
|
56
|
+
const value = previous[key];
|
|
57
|
+
if (value === undefined) {
|
|
58
|
+
delete process.env[key];
|
|
59
|
+
} else {
|
|
60
|
+
process.env[key] = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
resetCapabilitiesCache();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
247
66
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
67
|
+
function kittyResult(maxWidthCells: number, imageDimensions: ImageDimensions) {
|
|
68
|
+
return withCapabilityEnv({ TERM_PROGRAM: "kitty", TMUX: undefined }, () =>
|
|
69
|
+
renderImage("AA==", imageDimensions, { maxWidthCells })
|
|
70
|
+
);
|
|
71
|
+
}
|
|
251
72
|
|
|
252
|
-
|
|
253
|
-
|
|
73
|
+
describe("terminal-image", () => {
|
|
74
|
+
it("calculates image rows from target width and cell size", () => {
|
|
75
|
+
const rows = calculateImageRows({ heightPx: 900, widthPx: 1800 }, 60, DEFAULT_CELL);
|
|
76
|
+
expect(rows).toBe(15);
|
|
254
77
|
});
|
|
255
78
|
|
|
256
|
-
it("
|
|
257
|
-
expect(
|
|
79
|
+
it("parses PNG dimensions", () => {
|
|
80
|
+
expect(getPngDimensions(TINY_PNG_BASE64)).toEqual({ heightPx: 1, widthPx: 1 });
|
|
258
81
|
});
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
// ── createImageMetadata ──────────────────────────────────────────────────────
|
|
262
82
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const dims = { widthPx: 1920, heightPx: 1080 };
|
|
266
|
-
const meta = createImageMetadata(dims, dims, "png", 245000);
|
|
267
|
-
expect(meta.resized).toBe(false);
|
|
268
|
-
expect(meta.originalWidth).toBe(1920);
|
|
269
|
-
expect(meta.displayWidth).toBe(1920);
|
|
270
|
-
expect(meta.format).toBe("png");
|
|
271
|
-
expect(meta.sizeBytes).toBe(245000);
|
|
83
|
+
it("parses GIF dimensions", () => {
|
|
84
|
+
expect(getGifDimensions(GIF_BASE64)).toEqual({ heightPx: 1, widthPx: 1 });
|
|
272
85
|
});
|
|
273
86
|
|
|
274
|
-
it("
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
expect(
|
|
279
|
-
expect(meta.originalWidth).toBe(3840);
|
|
280
|
-
expect(meta.displayWidth).toBe(1920);
|
|
281
|
-
expect(meta.sizeBytes).toBeUndefined();
|
|
87
|
+
it("parses WEBP dimensions", () => {
|
|
88
|
+
const dims = getWebpDimensions(WEBP_BASE64);
|
|
89
|
+
expect(dims).not.toBeNull();
|
|
90
|
+
expect(dims?.widthPx).toBeGreaterThanOrEqual(1);
|
|
91
|
+
expect(dims?.heightPx).toBeGreaterThanOrEqual(1);
|
|
282
92
|
});
|
|
283
93
|
|
|
284
|
-
it("
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
expect(createImageMetadata(original, display, null).resized).toBe(true);
|
|
94
|
+
it("dispatches getImageDimensions by mime type", () => {
|
|
95
|
+
expect(getImageDimensions(TINY_PNG_BASE64, "image/png")).toEqual({ heightPx: 1, widthPx: 1 });
|
|
96
|
+
expect(getImageDimensions(GIF_BASE64, "image/gif")).toEqual({ heightPx: 1, widthPx: 1 });
|
|
288
97
|
});
|
|
289
98
|
|
|
290
|
-
it("
|
|
291
|
-
const
|
|
292
|
-
expect(
|
|
99
|
+
it("renders kitty images with rows derived from the image size", () => {
|
|
100
|
+
const result = kittyResult(8, { heightPx: 2556, widthPx: 1179 });
|
|
101
|
+
expect(result).not.toBeNull();
|
|
102
|
+
expect(result?.sequence).toContain("\x1b_G");
|
|
103
|
+
expect(result?.rows).toBe(calculateImageRows({ heightPx: 2556, widthPx: 1179 }, 8));
|
|
293
104
|
});
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
// ── formatImageDimensions ────────────────────────────────────────────────────
|
|
297
105
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
{ widthPx: 1920, heightPx: 1080 },
|
|
302
|
-
{ widthPx: 1920, heightPx: 1080 },
|
|
303
|
-
"png"
|
|
106
|
+
it("renders iTerm images with auto height", () => {
|
|
107
|
+
const result = withCapabilityEnv({ TERM_PROGRAM: "iTerm.app", TMUX: undefined }, () =>
|
|
108
|
+
renderImage("AA==", { heightPx: 1080, widthPx: 1920 }, { maxWidthCells: 30 })
|
|
304
109
|
);
|
|
305
|
-
expect(
|
|
110
|
+
expect(result).not.toBeNull();
|
|
111
|
+
expect(result?.sequence).toContain("\x1b]1337;File=");
|
|
112
|
+
expect(result?.sequence).toContain("height=auto");
|
|
306
113
|
});
|
|
307
114
|
|
|
308
|
-
it("
|
|
309
|
-
const
|
|
310
|
-
{
|
|
311
|
-
{ widthPx:
|
|
312
|
-
"png"
|
|
115
|
+
it("returns null when image protocols are unavailable", () => {
|
|
116
|
+
const result = withCapabilityEnv(
|
|
117
|
+
{ TERM: "tmux-256color", TERM_PROGRAM: "unknown", TMUX: "1" },
|
|
118
|
+
() => renderImage("AA==", { heightPx: 1080, widthPx: 1920 }, { maxWidthCells: 30 })
|
|
313
119
|
);
|
|
314
|
-
expect(
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// ── renderImage ─────────────────────────────────────────────────────────────
|
|
319
|
-
|
|
320
|
-
describe("renderImage", () => {
|
|
321
|
-
it("emits Kitty sequences with valid columns and no explicit rows", () => {
|
|
322
|
-
withCapabilityEnv({ TERM_PROGRAM: "kitty" }, () => {
|
|
323
|
-
const result = renderImage("AA==", { widthPx: 1179, heightPx: 2556 }, { maxWidthCells: 8 });
|
|
324
|
-
expect(result).not.toBeNull();
|
|
325
|
-
|
|
326
|
-
const columnsMatch = result?.sequence.match(/c=(\d+)/);
|
|
327
|
-
expect(columnsMatch).not.toBeNull();
|
|
328
|
-
expect(Number(columnsMatch?.[1])).toBe(result?.columns);
|
|
329
|
-
expect(result?.sequence).not.toContain(",r=");
|
|
330
|
-
expect(result?.columns).toBeGreaterThanOrEqual(1);
|
|
331
|
-
expect(result?.rows).toBeGreaterThanOrEqual(1);
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
it("keeps render metadata aligned with calculated layout at narrow widths", () => {
|
|
336
|
-
withCapabilityEnv({ TERM_PROGRAM: "kitty" }, () => {
|
|
337
|
-
const portrait: ImageDimensions = { widthPx: 1179, heightPx: 2556 };
|
|
338
|
-
for (const width of [8, 12, 20, 30]) {
|
|
339
|
-
const result = renderImage("AA==", portrait, { maxWidthCells: width });
|
|
340
|
-
expect(result).not.toBeNull();
|
|
341
|
-
|
|
342
|
-
const layout = calculateImageLayout(portrait, width, DEFAULT_CELL);
|
|
343
|
-
expect(result?.columns).toBe(layout.columns);
|
|
344
|
-
expect(result?.rows).toBe(layout.rows);
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it("emits iTerm2 sequence width/height params that match computed layout", () => {
|
|
350
|
-
withCapabilityEnv({ TERM_PROGRAM: "iterm.app" }, () => {
|
|
351
|
-
const result = renderImage("AA==", { widthPx: 1920, heightPx: 1080 }, { maxWidthCells: 30 });
|
|
352
|
-
expect(result).not.toBeNull();
|
|
353
|
-
expect(result?.sequence).toContain("\x1b]1337;File=");
|
|
354
|
-
expect(result?.sequence).toContain("height=auto");
|
|
355
|
-
|
|
356
|
-
const widthMatch = result?.sequence.match(/width=(\d+)/);
|
|
357
|
-
expect(widthMatch).not.toBeNull();
|
|
358
|
-
expect(Number(widthMatch?.[1])).toBe(result?.columns);
|
|
359
|
-
expect(result?.columns).toBeGreaterThanOrEqual(1);
|
|
360
|
-
expect(result?.rows).toBeGreaterThanOrEqual(1);
|
|
361
|
-
});
|
|
120
|
+
expect(result).toBeNull();
|
|
362
121
|
});
|
|
363
122
|
});
|
|
@@ -250,7 +250,7 @@ describe("TUI render scheduling", () => {
|
|
|
250
250
|
for (let index = 0; index < 25; index += 1) {
|
|
251
251
|
tui.requestRender();
|
|
252
252
|
}
|
|
253
|
-
await
|
|
253
|
+
await waitFor(() => component.renderCount === 1 && terminal.writes.length === 1);
|
|
254
254
|
|
|
255
255
|
expect(component.renderCount).toBe(1);
|
|
256
256
|
expect(terminal.writes).toHaveLength(1);
|