jsmdcui 0.6.3 → 0.7.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/CHANGELOG.md +60 -0
- package/README.md +105 -16
- package/clean.sh +9 -0
- package/demo.resized.jpg +0 -0
- package/{image-processor.md → demos/image-processor.md} +1 -0
- package/{image-processor.zh-TW.md → demos/image-processor.zh-TW.md} +1 -0
- package/{select.md → demos/select.md} +2 -0
- package/demos/todo-zh.md +92 -0
- package/demos/todo.md +93 -0
- package/package.json +1 -1
- package/runmd.mjs +33 -14
- package/runtime/help/help.md +105 -16
- package/single-exe/packAssets.sh +1 -1
- package/src/cui/kitty-debug.mjs +25 -0
- package/src/cui/kitty-images.mjs +190 -0
- package/src/cui/rpc.mjs +163 -7
- package/src/cui/server.mjs +1 -1
- package/src/cui/task-checkbox.mjs +44 -0
- package/src/index.js +236 -39
- package/src/plugins/js-bridge.js +244 -17
- package/src/screen/screen.js +108 -1
- package/tests/cat-markdown.test.js +6 -5
- package/tests/demo.test.js +99 -9
- package/tests/heading-list-selector.test.js +346 -0
- package/tests/id-collision.test.js +3 -2
- package/tests/kitty-demo.md +184 -0
- package/tests/kitty-images.test.js +147 -0
- package/tests/task-checkbox.test.js +33 -0
- package/tests/wui-responsive-images.test.js +35 -0
- package/tui +4 -2
- package/wui +6 -2
package/src/index.js
CHANGED
|
@@ -98,13 +98,15 @@ import { mkdir } from "node:fs/promises";
|
|
|
98
98
|
import { dirname, basename, join, resolve, sep } from "node:path";
|
|
99
99
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
100
100
|
import process from "node:process";
|
|
101
|
-
import { toggleTaskCheckboxBeforeColumn } from "./cui/task-checkbox.mjs";
|
|
101
|
+
import { toggleTaskCheckboxBeforeColumn, updateAnsiTaskCheckbox } from "./cui/task-checkbox.mjs";
|
|
102
102
|
import { checkMarkdownIdCollisions, formatMarkdownIdCheckAnsi } from "./cui/id-collision.mjs";
|
|
103
|
+
import { fitKittyImageToWidth, prepareKittyImages } from "./cui/kitty-images.mjs";
|
|
104
|
+
import { logKittyPlacement } from "./cui/kitty-debug.mjs";
|
|
103
105
|
import { Config } from "./config/config.js";
|
|
104
106
|
import { defaultAllSettings, OPTION_CHOICES, LOCAL_SETTINGS } from "./config/defaults.js";
|
|
105
107
|
import { cleanConfig } from "./config/clean.js";
|
|
106
108
|
import { RuntimeRegistry, RTColorscheme, RTHelp } from "./runtime/registry.js";
|
|
107
|
-
import { assetPath, hasInternalAssets, listInternalAssetDirs, readInternalAssetText } from "./runtime/assets.js";
|
|
109
|
+
import { assetPath, hasInternalAssets, listInternalAssetDirs, listInternalAssetPaths, readInternalAssetText } from "./runtime/assets.js";
|
|
108
110
|
//import { PluginManager } from "./plugins/manager.js";
|
|
109
111
|
import { JsPluginManager, buildMicroGlobal, runAction, listActions } from "./plugins/js-bridge.js";
|
|
110
112
|
import { Colorscheme } from "./config/colorscheme.js";
|
|
@@ -119,6 +121,10 @@ import { shellSplit } from "./shell/shell.js";
|
|
|
119
121
|
import { styleToAnsi } from "./display/ansi-style.js";
|
|
120
122
|
import { encodeBinaryToBuffer, decodeBinaryBytes } from "./buffer/fixed3-codec.js";
|
|
121
123
|
import { writeBackup, removeBackup, applyBackup } from "./buffer/backup.js";
|
|
124
|
+
|
|
125
|
+
let kittyImageMode = "off";
|
|
126
|
+
let allowRemoteKittyImages = false;
|
|
127
|
+
const remoteMarkdownSources = new Map();
|
|
122
128
|
import { isHex3Encoding, isMdcuiEncoding } from "./runtime/encodings.js";
|
|
123
129
|
import { createInterface } from "node:readline/promises";
|
|
124
130
|
|
|
@@ -313,7 +319,12 @@ async function readTextFileWithEncoding(path, encoding = "utf-8", inferMdcui = t
|
|
|
313
319
|
|
|
314
320
|
async function fetchTextWithEncoding(url, encoding = "utf-8", inferMdcui = true) {
|
|
315
321
|
const bytes = await fetchHttpBytes(url);
|
|
316
|
-
return decodeAndRenderTextBytes(
|
|
322
|
+
return decodeAndRenderTextBytes(
|
|
323
|
+
new Uint8Array(bytes),
|
|
324
|
+
encodingForPath(url, encoding, inferMdcui),
|
|
325
|
+
process.stdout.columns || 80,
|
|
326
|
+
url,
|
|
327
|
+
);
|
|
317
328
|
}
|
|
318
329
|
|
|
319
330
|
function normalizeEncodingLabel(encoding = "utf-8") {
|
|
@@ -337,20 +348,30 @@ async function decodeAndRenderTextBytes(bytes, encoding = "utf-8", width = proce
|
|
|
337
348
|
const decoded = decodeTextBytesWithEncoding(bytes, encoding);
|
|
338
349
|
if (!isMdcuiEncoding(decoded.encoding)) return decoded;
|
|
339
350
|
const renderWidth = Math.max(1, Math.trunc(Number(width) || 80));
|
|
340
|
-
const { rendered, tuiSourceText } = await renderMdcui(decoded.text, renderWidth, mdpath);
|
|
351
|
+
const { rendered, tuiSourceText, images } = await renderMdcui(decoded.text, renderWidth, mdpath, mdpath);
|
|
341
352
|
const styled = parseAnsiStyledText(rendered);
|
|
342
|
-
return { text: styled.text, encoding: "mdcui", sourceText: decoded.text, tuiSourceText, ansiText: rendered, ansiStyleLines: styled.styleLines, mdcuiRenderWidth: renderWidth };
|
|
353
|
+
return { text: styled.text, encoding: "mdcui", sourceText: decoded.text, tuiSourceText, ansiText: rendered, ansiStyleLines: styled.styleLines, mdcuiImages: images, mdcuiRenderWidth: renderWidth };
|
|
343
354
|
}
|
|
344
355
|
|
|
345
|
-
async function renderMdcui(markdown, width = process.stdout.columns || 80, mdpath = null) {
|
|
356
|
+
async function renderMdcui(markdown, width = process.stdout.columns || 80, mdpath = null, imageBasePath = mdpath) {
|
|
346
357
|
const runmd = await import("../runmd.mjs");
|
|
347
358
|
let md = String(markdown);
|
|
348
359
|
if (mdpath && !isHttpUrl(mdpath)) {
|
|
349
360
|
md = await runmd.extractJs(md, mdpath);
|
|
350
361
|
await runmd.createWui(md, mdpath);
|
|
351
362
|
}
|
|
363
|
+
const tui = runmd.createTui(md, Math.max(1, Math.trunc(Number(width) || 80)));
|
|
364
|
+
const resolvedImageBasePath = mdpath && !isHttpUrl(mdpath)
|
|
365
|
+
? remoteMarkdownSources.get(resolve(mdpath)) ?? imageBasePath
|
|
366
|
+
: imageBasePath;
|
|
367
|
+
const prepared = kittyImageMode === "off"
|
|
368
|
+
? { rendered: tui, images: [] }
|
|
369
|
+
: await prepareKittyImages(tui, resolvedImageBasePath, width, {
|
|
370
|
+
allowUrl: allowRemoteKittyImages,
|
|
371
|
+
});
|
|
352
372
|
return {
|
|
353
|
-
rendered:
|
|
373
|
+
rendered: prepared.rendered,
|
|
374
|
+
images: prepared.images,
|
|
354
375
|
tuiSourceText: md,
|
|
355
376
|
};
|
|
356
377
|
}
|
|
@@ -590,6 +611,12 @@ function resizeMdcuiTextBlock(buf, y, x) {
|
|
|
590
611
|
ansiLines.splice(row, deleteCount, ...replacement);
|
|
591
612
|
buf._mdcuiAnsiText = ansiLines.join("\n");
|
|
592
613
|
}
|
|
614
|
+
if (Array.isArray(buf._mdcuiImages)) {
|
|
615
|
+
const delta = replacement.length - deleteCount;
|
|
616
|
+
buf._mdcuiImages = buf._mdcuiImages
|
|
617
|
+
.filter((image) => image.line < row || image.line >= row + deleteCount)
|
|
618
|
+
.map((image) => image.line >= row + deleteCount ? { ...image, line: image.line + delta } : image);
|
|
619
|
+
}
|
|
593
620
|
|
|
594
621
|
if (insertAt >= 0) {
|
|
595
622
|
if (buf.cursor.y >= insertAt) buf.cursor.y++;
|
|
@@ -955,10 +982,8 @@ function parseArgs(argv) {
|
|
|
955
982
|
exportReadme: false,
|
|
956
983
|
changelog: false,
|
|
957
984
|
testapp: false,
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
demoImgtool: false,
|
|
961
|
-
demoImgtoolZh: false,
|
|
985
|
+
demoList: false,
|
|
986
|
+
demo: null,
|
|
962
987
|
allowUrl: false,
|
|
963
988
|
buildExe: false,
|
|
964
989
|
buildFor: "",
|
|
@@ -968,6 +993,7 @@ function parseArgs(argv) {
|
|
|
968
993
|
plugin: "",
|
|
969
994
|
cdpPort: 0,
|
|
970
995
|
cdpAddress: "",
|
|
996
|
+
kittyMode: "off",
|
|
971
997
|
settings: new Map(),
|
|
972
998
|
};
|
|
973
999
|
const files = [];
|
|
@@ -1000,11 +1026,25 @@ function parseArgs(argv) {
|
|
|
1000
1026
|
else if (arg === "--export-readme") flags.exportReadme = true;
|
|
1001
1027
|
else if (arg === "--changelog") flags.changelog = true;
|
|
1002
1028
|
else if (arg === "--testapp.md") flags.testapp = true;
|
|
1003
|
-
else if (arg === "--demo") flags.
|
|
1004
|
-
else if (arg === "--demo
|
|
1005
|
-
|
|
1006
|
-
|
|
1029
|
+
else if (arg === "--demo-list") flags.demoList = true;
|
|
1030
|
+
else if (arg === "--demo") {
|
|
1031
|
+
flags.demo = { option: arg, filename: "testapp.md", asset: "testapp.md" };
|
|
1032
|
+
}
|
|
1033
|
+
else if (arg === "--demo-imgtool") {
|
|
1034
|
+
flags.demo = { option: arg, filename: "image-processor.md", asset: "demos/image-processor.md" };
|
|
1035
|
+
}
|
|
1036
|
+
else if (arg === "--demo-imgtool-zh") {
|
|
1037
|
+
flags.demo = { option: arg, filename: "image-processor.zh-TW.md", asset: "demos/image-processor.zh-TW.md" };
|
|
1038
|
+
}
|
|
1039
|
+
else if (arg.startsWith("--demo-")) {
|
|
1040
|
+
const name = arg.slice("--demo-".length);
|
|
1041
|
+
flags.demo = /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)
|
|
1042
|
+
? { option: arg, filename: `${name}.md`, asset: `demos/${name}.md` }
|
|
1043
|
+
: { option: arg, error: "demo filename must contain only letters, numbers, dots, underscores, and hyphens" };
|
|
1044
|
+
}
|
|
1007
1045
|
else if (arg === "--allow-url") flags.allowUrl = true;
|
|
1046
|
+
else if (arg === "--kitty") flags.kittyMode = "extended";
|
|
1047
|
+
else if (arg === "--kitty-compat") flags.kittyMode = "compat";
|
|
1008
1048
|
else if (arg === "--build-exe") flags.buildExe = true;
|
|
1009
1049
|
else if (arg === "--build-for") flags.buildFor = argv[++i] ?? "";
|
|
1010
1050
|
else if (arg === "-debug") flags.debug = true;
|
|
@@ -1056,6 +1096,10 @@ Modes:
|
|
|
1056
1096
|
-encoding mdcui
|
|
1057
1097
|
Render Markdown through runmd.mjs#createTui; .md files use this automatically
|
|
1058
1098
|
Writes .front.js, .back.js, .html, -rpc.js, and -server.js beside the .md file
|
|
1099
|
+
--kitty
|
|
1100
|
+
Display Markdown images with Kitty graphics and the jsgotty MIME extension
|
|
1101
|
+
--kitty-compat
|
|
1102
|
+
Display Markdown images with Kitty graphics without the non-standard MIME U field
|
|
1059
1103
|
|
|
1060
1104
|
Settings:
|
|
1061
1105
|
-SETTING VALUE
|
|
@@ -1086,22 +1130,23 @@ Information:
|
|
|
1086
1130
|
Demo:
|
|
1087
1131
|
--testapp.md
|
|
1088
1132
|
Write the bundled testapp.md to stdout & exit
|
|
1133
|
+
--demo-list
|
|
1134
|
+
List the bundled demos and their command-line options, then exit
|
|
1089
1135
|
--demo
|
|
1090
1136
|
Use the existing ./testapp.md without overwriting it, or write the bundled demo if missing
|
|
1091
1137
|
Open it in the TUI and write 5 generated files beside it
|
|
1092
|
-
--demo
|
|
1093
|
-
|
|
1138
|
+
--demo-<filename>
|
|
1139
|
+
Load demos/<filename>.md, preserving an existing ./<filename>.md or writing the bundled copy
|
|
1094
1140
|
Open it in the TUI and write 5 generated files beside it
|
|
1141
|
+
For example: --demo-select, --demo-todo, or --demo-todo-zh
|
|
1095
1142
|
--demo-imgtool
|
|
1096
|
-
|
|
1097
|
-
Open the Bun.Image processor in the TUI and write 5 generated files beside it
|
|
1143
|
+
Alias for --demo-image-processor
|
|
1098
1144
|
--demo-imgtool-zh
|
|
1099
|
-
|
|
1100
|
-
Open the Traditional Chinese Bun.Image processor in the TUI and write 5 generated files beside it
|
|
1145
|
+
Alias for --demo-image-processor.zh-TW
|
|
1101
1146
|
|
|
1102
1147
|
Remote Markdown:
|
|
1103
1148
|
--allow-url
|
|
1104
|
-
Download HTTP(S) Markdown
|
|
1149
|
+
Download HTTP(S) Markdown and, with Kitty mode, its HTTP(S) images; allow its code to run
|
|
1105
1150
|
|
|
1106
1151
|
Experimental:
|
|
1107
1152
|
--build-exe Build a Bun single-file executable and exit
|
|
@@ -1149,7 +1194,7 @@ class BufferModel {
|
|
|
1149
1194
|
get searchPattern() { return this._searchPattern ?? ""; }
|
|
1150
1195
|
set searchPattern(v) { this._searchPattern = v ?? ""; this.searchMatches?.clear(); }
|
|
1151
1196
|
|
|
1152
|
-
constructor({ path = "", text = "", command = {}, type = "default", readonly = false, modTimeMs = null, encoding = DEFAULT_SETTINGS.encoding, ansiStyleLines = null, ansiText = null, sourceText = null, tuiSourceText = null, mdcuiRenderWidth = 0 } = {}) {
|
|
1197
|
+
constructor({ path = "", text = "", command = {}, type = "default", readonly = false, modTimeMs = null, encoding = DEFAULT_SETTINGS.encoding, ansiStyleLines = null, ansiText = null, sourceText = null, tuiSourceText = null, mdcuiImages = null, mdcuiRenderWidth = 0 } = {}) {
|
|
1153
1198
|
this.path = path;
|
|
1154
1199
|
this.type = type;
|
|
1155
1200
|
this.name = path ? basename(path) : "No name";
|
|
@@ -1162,6 +1207,7 @@ class BufferModel {
|
|
|
1162
1207
|
this._mdcuiAnsiText = isMdcuiEncoding(this.encoding) ? String(ansiText ?? "") : null;
|
|
1163
1208
|
this._mdcuiSourceText = isMdcuiEncoding(this.encoding) ? String(sourceText ?? text) : null;
|
|
1164
1209
|
this._mdcuiTuiSourceText = isMdcuiEncoding(this.encoding) ? String(tuiSourceText ?? sourceText ?? text) : null;
|
|
1210
|
+
this._mdcuiImages = isMdcuiEncoding(this.encoding) ? (mdcuiImages ?? []) : [];
|
|
1165
1211
|
this._mdcuiRenderWidth = Math.trunc(Number(mdcuiRenderWidth) || 0);
|
|
1166
1212
|
this.cursor = { x: 0, y: 0 };
|
|
1167
1213
|
this.scroll = { x: 0, y: 0, row: 0 };
|
|
@@ -1262,6 +1308,7 @@ class BufferModel {
|
|
|
1262
1308
|
let sourceText = null;
|
|
1263
1309
|
let tuiSourceText = null;
|
|
1264
1310
|
let mdcuiRenderWidth = 0;
|
|
1311
|
+
let mdcuiImages = null;
|
|
1265
1312
|
if (existsSync(path)) {
|
|
1266
1313
|
const info = statSync(path);
|
|
1267
1314
|
if (info.isDirectory()) throw new Error(`${path} is a directory`);
|
|
@@ -1275,9 +1322,10 @@ class BufferModel {
|
|
|
1275
1322
|
sourceText = decoded.sourceText ?? null;
|
|
1276
1323
|
tuiSourceText = decoded.tuiSourceText ?? null;
|
|
1277
1324
|
mdcuiRenderWidth = decoded.mdcuiRenderWidth ?? 0;
|
|
1325
|
+
mdcuiImages = decoded.mdcuiImages ?? null;
|
|
1278
1326
|
if (isMdcuiEncoding(encoding)) readonly = true;
|
|
1279
1327
|
}
|
|
1280
|
-
const buffer = new BufferModel({ path, text, command, readonly, modTimeMs, encoding, ansiStyleLines, ansiText, sourceText, tuiSourceText, mdcuiRenderWidth });
|
|
1328
|
+
const buffer = new BufferModel({ path, text, command, readonly, modTimeMs, encoding, ansiStyleLines, ansiText, sourceText, tuiSourceText, mdcuiImages, mdcuiRenderWidth });
|
|
1281
1329
|
buffer._configDir = context?.config?.configDir ?? null;
|
|
1282
1330
|
attachSyntax(buffer, context, path, text);
|
|
1283
1331
|
return buffer;
|
|
@@ -1678,6 +1726,7 @@ class BufferModel {
|
|
|
1678
1726
|
this._mdcuiSourceText = isMdcuiEncoding(this.encoding) ? String(decoded.sourceText ?? text) : null;
|
|
1679
1727
|
this._mdcuiTuiSourceText = isMdcuiEncoding(this.encoding) ? String(decoded.tuiSourceText ?? decoded.sourceText ?? text) : null;
|
|
1680
1728
|
this._mdcuiRenderWidth = decoded.mdcuiRenderWidth ?? 0;
|
|
1729
|
+
this._mdcuiImages = decoded.mdcuiImages ?? [];
|
|
1681
1730
|
const readonly = isMdcuiEncoding(this.encoding);
|
|
1682
1731
|
this.fileformat = detectFileFormat(text, this.Settings.fileformat ?? DEFAULT_SETTINGS.fileformat);
|
|
1683
1732
|
this.Settings.fileformat = this.fileformat;
|
|
@@ -1710,6 +1759,7 @@ class BufferModel {
|
|
|
1710
1759
|
this._mdcuiSourceText = isMdcuiEncoding(this.encoding) ? String(decoded.sourceText ?? text) : null;
|
|
1711
1760
|
this._mdcuiTuiSourceText = isMdcuiEncoding(this.encoding) ? String(decoded.tuiSourceText ?? decoded.sourceText ?? text) : null;
|
|
1712
1761
|
this._mdcuiRenderWidth = decoded.mdcuiRenderWidth ?? 0;
|
|
1762
|
+
this._mdcuiImages = decoded.mdcuiImages ?? [];
|
|
1713
1763
|
this.fileformat = detectFileFormat(text, this.Settings.fileformat ?? DEFAULT_SETTINGS.fileformat);
|
|
1714
1764
|
this.Settings.fileformat = this.fileformat;
|
|
1715
1765
|
this.lines = normalizeBufferText(text).split("\n");
|
|
@@ -1733,12 +1783,13 @@ class BufferModel {
|
|
|
1733
1783
|
if (!isMdcuiEncoding(this.encoding) || this._mdcuiTuiSourceText == null) return false;
|
|
1734
1784
|
const renderWidth = Math.max(1, Math.trunc(Number(width) || 80));
|
|
1735
1785
|
if (renderWidth === this._mdcuiRenderWidth) return false;
|
|
1736
|
-
const { rendered } = await renderMdcui(this._mdcuiTuiSourceText, renderWidth);
|
|
1786
|
+
const { rendered, images } = await renderMdcui(this._mdcuiTuiSourceText, renderWidth, null, this.path);
|
|
1737
1787
|
const styled = parseAnsiStyledText(rendered);
|
|
1738
1788
|
this.lines = normalizeBufferText(styled.text).split("\n");
|
|
1739
1789
|
if (this.lines.length === 0) this.lines = [""];
|
|
1740
1790
|
this._ansiStyleLines = styled.styleLines;
|
|
1741
1791
|
this._mdcuiAnsiText = rendered;
|
|
1792
|
+
this._mdcuiImages = images;
|
|
1742
1793
|
this._mdcuiRenderWidth = renderWidth;
|
|
1743
1794
|
this.fileformat = detectFileFormat(styled.text, this.Settings.fileformat ?? DEFAULT_SETTINGS.fileformat);
|
|
1744
1795
|
this.Settings.fileformat = this.fileformat;
|
|
@@ -2404,7 +2455,10 @@ class App {
|
|
|
2404
2455
|
this.clipboard = new ClipboardManager();
|
|
2405
2456
|
this.context = context;
|
|
2406
2457
|
this.shellRunning = false;
|
|
2407
|
-
this.screen = new Screen({
|
|
2458
|
+
this.screen = new Screen({
|
|
2459
|
+
mouse: DEFAULT_SETTINGS.mouse !== false,
|
|
2460
|
+
kittyMode: context.kittyMode ?? "off",
|
|
2461
|
+
});
|
|
2408
2462
|
this.tabRects = [];
|
|
2409
2463
|
this._escBuf = null; // pending lone ESC bytes waiting for alt-key combo
|
|
2410
2464
|
this._escTimer = null;
|
|
@@ -2638,6 +2692,7 @@ class App {
|
|
|
2638
2692
|
|
|
2639
2693
|
const defaultStyle = this.context.colorscheme?.defaultStyle ?? {};
|
|
2640
2694
|
this.screen.fill(" ", defaultStyle);
|
|
2695
|
+
this._kittyFrameImages = [];
|
|
2641
2696
|
|
|
2642
2697
|
this.tabRects = [];
|
|
2643
2698
|
if (tabBarHeight) this.renderTabbar(defaultStyle);
|
|
@@ -2813,6 +2868,7 @@ class App {
|
|
|
2813
2868
|
this.screen.setCursor(0, 0, false);
|
|
2814
2869
|
}
|
|
2815
2870
|
|
|
2871
|
+
this.screen.setKittyImages(this._kittyFrameImages);
|
|
2816
2872
|
this.screen.show();
|
|
2817
2873
|
}
|
|
2818
2874
|
|
|
@@ -2995,6 +3051,59 @@ class App {
|
|
|
2995
3051
|
? (this.context.colorscheme.get("cursor-line")?.fg ?? null)
|
|
2996
3052
|
: null;
|
|
2997
3053
|
|
|
3054
|
+
const addKittyImage = (lineNo, screenRow, subRow = 0) => {
|
|
3055
|
+
if (subRow !== 0 || !Array.isArray(buf._mdcuiImages)) return;
|
|
3056
|
+
for (const image of buf._mdcuiImages) {
|
|
3057
|
+
if (image.line !== lineNo) continue;
|
|
3058
|
+
const { cols, rows } = fitKittyImageToWidth(image, maxW);
|
|
3059
|
+
const visibleTop = Math.max(pane.y, screenRow);
|
|
3060
|
+
const visibleBottom = Math.min(pane.y + pane.h, screenRow + rows);
|
|
3061
|
+
const visibleRows = visibleBottom - visibleTop;
|
|
3062
|
+
if (visibleRows < 1 || cols < 1) continue;
|
|
3063
|
+
const clippedTopRows = visibleTop - screenRow;
|
|
3064
|
+
const pixelHeight = Math.max(1, Math.trunc(Number(image.pixelHeight) || 1));
|
|
3065
|
+
const sourceY = Math.min(pixelHeight - 1, Math.round(pixelHeight * clippedTopRows / rows));
|
|
3066
|
+
const sourceBottom = Math.min(pixelHeight, Math.round(pixelHeight * (clippedTopRows + visibleRows) / rows));
|
|
3067
|
+
const sourceHeight = Math.max(1, sourceBottom - sourceY);
|
|
3068
|
+
const sourceWidth = Math.max(1, Math.trunc(Number(image.pixelWidth) || 1));
|
|
3069
|
+
const x = pane.x + gutterW;
|
|
3070
|
+
const placementId = ((image.id ^ Math.imul(x + 1, 73856093) ^ Math.imul(visibleTop + 1, 19349663) ^ Math.imul(sourceY + 1, 83492791)) >>> 0) % 2147483646 + 1;
|
|
3071
|
+
logKittyPlacement("app-placement", {
|
|
3072
|
+
buffer: buf.path,
|
|
3073
|
+
imagePath: image.path,
|
|
3074
|
+
imageId: image.id,
|
|
3075
|
+
placementId,
|
|
3076
|
+
imageLogicalLine: image.line,
|
|
3077
|
+
renderedLine: lineNo,
|
|
3078
|
+
subRow,
|
|
3079
|
+
pane: { x: pane.x, y: pane.y, width: pane.w, height: pane.h },
|
|
3080
|
+
scroll: { ...buf.scroll },
|
|
3081
|
+
gutterWidth: gutterW,
|
|
3082
|
+
maxWidth: maxW,
|
|
3083
|
+
screenX: x,
|
|
3084
|
+
nominalScreenY: screenRow,
|
|
3085
|
+
screenY: visibleTop,
|
|
3086
|
+
cols,
|
|
3087
|
+
rows: visibleRows,
|
|
3088
|
+
originalCols: image.cols,
|
|
3089
|
+
originalRows: image.rows,
|
|
3090
|
+
sourceRect: { x: 0, y: sourceY, width: sourceWidth, height: sourceHeight },
|
|
3091
|
+
});
|
|
3092
|
+
this._kittyFrameImages.push({
|
|
3093
|
+
...image,
|
|
3094
|
+
x,
|
|
3095
|
+
y: visibleTop,
|
|
3096
|
+
cols,
|
|
3097
|
+
rows: visibleRows,
|
|
3098
|
+
sourceX: 0,
|
|
3099
|
+
sourceY,
|
|
3100
|
+
sourceWidth,
|
|
3101
|
+
sourceHeight,
|
|
3102
|
+
placementId,
|
|
3103
|
+
});
|
|
3104
|
+
}
|
|
3105
|
+
};
|
|
3106
|
+
|
|
2998
3107
|
const hasDiff = (buf.Settings?.diffgutter ?? false) && !!buf.diffBase;
|
|
2999
3108
|
if (hasDiff && !buf._diffOnUpdate) buf._diffOnUpdate = () => this.render();
|
|
3000
3109
|
const diffMarks = hasDiff ? getDiffMarkers(buf) : null;
|
|
@@ -3009,6 +3118,18 @@ class App {
|
|
|
3009
3118
|
const msgWarnStyle = cs?.styles?.has("gutter-warning") ? cs.get("gutter-warning") : defaultStyle;
|
|
3010
3119
|
const msgErrStyle = cs?.styles?.has("gutter-error") ? cs.get("gutter-error") : defaultStyle;
|
|
3011
3120
|
|
|
3121
|
+
// When the image anchor has scrolled above the pane, its lower portion can
|
|
3122
|
+
// still intersect the viewport. It will not be encountered by the normal
|
|
3123
|
+
// visible-line loop, so add that clipped placement explicitly.
|
|
3124
|
+
for (const image of buf._mdcuiImages ?? []) {
|
|
3125
|
+
if (image.line >= buf.scroll.y) continue;
|
|
3126
|
+
const nominalScreenRow = pane.y + image.line - buf.scroll.y;
|
|
3127
|
+
const { rows } = fitKittyImageToWidth(image, maxW);
|
|
3128
|
+
if (nominalScreenRow < pane.y && nominalScreenRow + rows > pane.y) {
|
|
3129
|
+
addKittyImage(image.line, nominalScreenRow);
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3012
3133
|
const renderGutter = (lineNo, row, screenRow, subRow = 0) => {
|
|
3013
3134
|
// Message indicator: 2 cols, '> ' with kind-based style (Go: drawGutter)
|
|
3014
3135
|
if (msgW > 0) {
|
|
@@ -3052,6 +3173,7 @@ class App {
|
|
|
3052
3173
|
if (lineNo < buf.lines.length) {
|
|
3053
3174
|
const cells = renderHighlightedCells(buf, lineNo, buf.scroll.x, maxW, this.context.colorscheme, pane.selection, getLineSearchRanges(buf, lineNo), braceMatches, isCL ? clBg : null);
|
|
3054
3175
|
putCells(this.screen, pane.x + gutterW, screenRow, cells, maxW);
|
|
3176
|
+
addKittyImage(lineNo, screenRow);
|
|
3055
3177
|
}
|
|
3056
3178
|
}
|
|
3057
3179
|
} else {
|
|
@@ -3075,6 +3197,7 @@ class App {
|
|
|
3075
3197
|
|
|
3076
3198
|
const cells = renderHighlightedCells(buf, lineNo, segStart, maxW, this.context.colorscheme, pane.selection, _swSearchRanges, braceMatches, isCL ? clBg : null);
|
|
3077
3199
|
putCells(this.screen, pane.x + gutterW, screenRow, cells, maxW);
|
|
3200
|
+
addKittyImage(lineNo, screenRow, subRow);
|
|
3078
3201
|
|
|
3079
3202
|
if (subRow + 1 < breaks.length) {
|
|
3080
3203
|
sloc = { line: lineNo, row: subRow + 1 };
|
|
@@ -4094,6 +4217,22 @@ class App {
|
|
|
4094
4217
|
const checkboxResult = toggleTaskCheckboxBeforeColumn(payload.line, x);
|
|
4095
4218
|
if (checkboxResult.toggled) {
|
|
4096
4219
|
buf.lines[y] = checkboxResult.line;
|
|
4220
|
+
const checkboxStyle = checkboxResult.checked ? { fg: "green" } : null;
|
|
4221
|
+
const styleLine = buf._ansiStyleLines?.[y];
|
|
4222
|
+
if (Array.isArray(styleLine)) {
|
|
4223
|
+
styleLine[checkboxResult.checkboxAt] = checkboxStyle;
|
|
4224
|
+
if (checkboxResult.line[checkboxResult.checkboxAt + 1] === " ")
|
|
4225
|
+
styleLine[checkboxResult.checkboxAt + 1] = checkboxStyle;
|
|
4226
|
+
}
|
|
4227
|
+
if (typeof buf._mdcuiAnsiText === "string") {
|
|
4228
|
+
const ansiLines = buf._mdcuiAnsiText.split("\n");
|
|
4229
|
+
ansiLines[y] = updateAnsiTaskCheckbox(
|
|
4230
|
+
ansiLines[y],
|
|
4231
|
+
checkboxResult.checkboxAt,
|
|
4232
|
+
checkboxResult.checked,
|
|
4233
|
+
);
|
|
4234
|
+
buf._mdcuiAnsiText = ansiLines.join("\n");
|
|
4235
|
+
}
|
|
4097
4236
|
buf.modified = true;
|
|
4098
4237
|
}
|
|
4099
4238
|
|
|
@@ -6788,6 +6927,7 @@ async function loadBufferForPath(pathOrUrl, context, command = {}, { interactive
|
|
|
6788
6927
|
if (!name.toLowerCase().endsWith(".md")) name += ".md";
|
|
6789
6928
|
const localPath = resolve(name);
|
|
6790
6929
|
await Bun.write(localPath, await fetchHttpBytes(pathOrUrl));
|
|
6930
|
+
remoteMarkdownSources.set(localPath, new URL(pathOrUrl).href);
|
|
6791
6931
|
return await loadBufferForPath(localPath, loadContext, command, { interactive });
|
|
6792
6932
|
}
|
|
6793
6933
|
let buffer;
|
|
@@ -6807,6 +6947,7 @@ async function loadBufferForPath(pathOrUrl, context, command = {}, { interactive
|
|
|
6807
6947
|
sourceText: decoded.sourceText ?? null,
|
|
6808
6948
|
tuiSourceText: decoded.tuiSourceText ?? null,
|
|
6809
6949
|
mdcuiRenderWidth: decoded.mdcuiRenderWidth ?? 0,
|
|
6950
|
+
mdcuiImages: decoded.mdcuiImages ?? null,
|
|
6810
6951
|
});
|
|
6811
6952
|
buffer._configDir = context?.config?.configDir ?? null;
|
|
6812
6953
|
attachSyntax(buffer, loadContext, urlPath, text);
|
|
@@ -7517,6 +7658,7 @@ async function loadBuffers(files, command) {
|
|
|
7517
7658
|
sourceText: decoded.sourceText ?? null,
|
|
7518
7659
|
tuiSourceText: decoded.tuiSourceText ?? null,
|
|
7519
7660
|
mdcuiRenderWidth: decoded.mdcuiRenderWidth ?? 0,
|
|
7661
|
+
mdcuiImages: decoded.mdcuiImages ?? null,
|
|
7520
7662
|
});
|
|
7521
7663
|
if (loadBuffers.context) attachSyntax(stdinBuf, loadBuffers.context, "", stdinText);
|
|
7522
7664
|
buffers.push(stdinBuf);
|
|
@@ -7558,6 +7700,42 @@ async function bundledMarkdownSource(filename) {
|
|
|
7558
7700
|
return readInternalAssetText(filename) ?? await Bun.file(join(REPO_ROOT, filename)).text();
|
|
7559
7701
|
}
|
|
7560
7702
|
|
|
7703
|
+
function availableDemoAssets() {
|
|
7704
|
+
let paths;
|
|
7705
|
+
if (hasInternalAssets()) {
|
|
7706
|
+
paths = listInternalAssetPaths("demos");
|
|
7707
|
+
} else {
|
|
7708
|
+
try {
|
|
7709
|
+
paths = readdirSync(join(REPO_ROOT, "demos"), { withFileTypes: true })
|
|
7710
|
+
.filter((entry) => entry.isFile())
|
|
7711
|
+
.map((entry) => assetPath("demos", entry.name));
|
|
7712
|
+
} catch {
|
|
7713
|
+
paths = [];
|
|
7714
|
+
}
|
|
7715
|
+
}
|
|
7716
|
+
|
|
7717
|
+
return [...new Set(paths)]
|
|
7718
|
+
.filter((path) => /^demos\/[^/]+\.md$/i.test(path))
|
|
7719
|
+
.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
7720
|
+
}
|
|
7721
|
+
|
|
7722
|
+
function printDemoList() {
|
|
7723
|
+
const demos = availableDemoAssets().map((asset) => ({
|
|
7724
|
+
option: `--demo-${asset.slice("demos/".length, -".md".length)}`,
|
|
7725
|
+
asset,
|
|
7726
|
+
}));
|
|
7727
|
+
const entries = [{ option: "--demo", asset: "testapp.md" }, ...demos];
|
|
7728
|
+
const optionWidth = Math.max(...entries.map(({ option }) => option.length));
|
|
7729
|
+
|
|
7730
|
+
console.log("Available demos:\n");
|
|
7731
|
+
for (const { option, asset } of entries) {
|
|
7732
|
+
console.log(` ${option.padEnd(optionWidth)} ${asset}`);
|
|
7733
|
+
}
|
|
7734
|
+
console.log("\nCompatibility aliases:\n");
|
|
7735
|
+
console.log(" --demo-imgtool --demo-image-processor");
|
|
7736
|
+
console.log(" --demo-imgtool-zh --demo-image-processor.zh-TW");
|
|
7737
|
+
}
|
|
7738
|
+
|
|
7561
7739
|
async function main() {
|
|
7562
7740
|
if (process.argv[2] === "--wui") {
|
|
7563
7741
|
process.argv.splice(2, 1);
|
|
@@ -7571,6 +7749,8 @@ async function main() {
|
|
|
7571
7749
|
await buildEarlyExit(null,DEFAULT_BUILD_OUTFILE)
|
|
7572
7750
|
|
|
7573
7751
|
const { flags, files: rawFiles } = parseArgs(process.argv.slice(2));
|
|
7752
|
+
kittyImageMode = flags.kittyMode;
|
|
7753
|
+
allowRemoteKittyImages = flags.allowUrl;
|
|
7574
7754
|
|
|
7575
7755
|
if (flags.help) {
|
|
7576
7756
|
console.log(usage());
|
|
@@ -7636,19 +7816,27 @@ async function main() {
|
|
|
7636
7816
|
await printTestappSource();
|
|
7637
7817
|
return;
|
|
7638
7818
|
}
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7819
|
+
if (flags.demoList) {
|
|
7820
|
+
printDemoList();
|
|
7821
|
+
return;
|
|
7822
|
+
}
|
|
7823
|
+
if (flags.demo) {
|
|
7824
|
+
if (flags.demo.error) {
|
|
7825
|
+
console.error(`Invalid demo option ${flags.demo.option}: ${flags.demo.error}`);
|
|
7826
|
+
process.exitCode = 2;
|
|
7827
|
+
return;
|
|
7828
|
+
}
|
|
7829
|
+
let demoSource;
|
|
7830
|
+
try {
|
|
7831
|
+
demoSource = await bundledMarkdownSource(flags.demo.asset);
|
|
7832
|
+
} catch {
|
|
7833
|
+
console.error(`Unknown demo ${flags.demo.option}: ${flags.demo.asset} was not found`);
|
|
7834
|
+
process.exitCode = 2;
|
|
7835
|
+
return;
|
|
7836
|
+
}
|
|
7837
|
+
const demoPath = resolve(flags.demo.filename);
|
|
7650
7838
|
if (!(await Bun.file(demoPath).exists())) {
|
|
7651
|
-
await Bun.write(demoPath,
|
|
7839
|
+
await Bun.write(demoPath, demoSource);
|
|
7652
7840
|
}
|
|
7653
7841
|
rawFiles.splice(0, rawFiles.length, demoPath);
|
|
7654
7842
|
}
|
|
@@ -7732,7 +7920,16 @@ async function main() {
|
|
|
7732
7920
|
|
|
7733
7921
|
const { files, command } = parseInput(rawFiles);
|
|
7734
7922
|
const jsPlugins = new JsPluginManager();
|
|
7735
|
-
const context = {
|
|
7923
|
+
const context = {
|
|
7924
|
+
colorscheme,
|
|
7925
|
+
syntaxDefinitions,
|
|
7926
|
+
config,
|
|
7927
|
+
runtime,
|
|
7928
|
+
jsPlugins,
|
|
7929
|
+
encodingExplicit,
|
|
7930
|
+
allowUrl: flags.allowUrl,
|
|
7931
|
+
kittyMode: flags.kittyMode,
|
|
7932
|
+
};
|
|
7736
7933
|
jsPlugins.setContext(context);
|
|
7737
7934
|
buildMicroGlobal(jsPlugins); // sets globalThis.micro
|
|
7738
7935
|
|