jsmdcui 0.5.0 → 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 +228 -0
- package/README.md +305 -25
- package/clean.sh +9 -0
- package/demo.resized.jpg +0 -0
- package/demos/image-processor.md +361 -0
- package/demos/image-processor.zh-TW.md +361 -0
- package/demos/select.md +63 -0
- package/demos/todo-zh.md +92 -0
- package/demos/todo.md +93 -0
- package/package.json +1 -1
- package/runmd.mjs +110 -17
- package/runtime/help/help.md +305 -25
- package/single-exe/packAssets.sh +1 -1
- package/src/cui/id-collision.mjs +151 -0
- package/src/cui/kitty-debug.mjs +25 -0
- package/src/cui/kitty-images.mjs +190 -0
- package/src/cui/rpc.mjs +227 -1
- package/src/cui/server.mjs +1 -1
- package/src/cui/task-checkbox.mjs +60 -0
- package/src/index.js +297 -23
- package/src/plugins/js-bridge.js +346 -4
- package/src/screen/screen.js +108 -1
- package/tests/cat-markdown.test.js +6 -5
- package/tests/demo.test.js +203 -4
- package/tests/heading-list-selector.test.js +346 -0
- package/tests/id-collision.test.js +147 -0
- 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/tests/wui.test.js +30 -0
- package/tui +4 -2
- package/wui +6 -2
package/src/index.js
CHANGED
|
@@ -98,11 +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, updateAnsiTaskCheckbox } from "./cui/task-checkbox.mjs";
|
|
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";
|
|
101
105
|
import { Config } from "./config/config.js";
|
|
102
106
|
import { defaultAllSettings, OPTION_CHOICES, LOCAL_SETTINGS } from "./config/defaults.js";
|
|
103
107
|
import { cleanConfig } from "./config/clean.js";
|
|
104
108
|
import { RuntimeRegistry, RTColorscheme, RTHelp } from "./runtime/registry.js";
|
|
105
|
-
import { assetPath, hasInternalAssets, listInternalAssetDirs, readInternalAssetText } from "./runtime/assets.js";
|
|
109
|
+
import { assetPath, hasInternalAssets, listInternalAssetDirs, listInternalAssetPaths, readInternalAssetText } from "./runtime/assets.js";
|
|
106
110
|
//import { PluginManager } from "./plugins/manager.js";
|
|
107
111
|
import { JsPluginManager, buildMicroGlobal, runAction, listActions } from "./plugins/js-bridge.js";
|
|
108
112
|
import { Colorscheme } from "./config/colorscheme.js";
|
|
@@ -117,6 +121,10 @@ import { shellSplit } from "./shell/shell.js";
|
|
|
117
121
|
import { styleToAnsi } from "./display/ansi-style.js";
|
|
118
122
|
import { encodeBinaryToBuffer, decodeBinaryBytes } from "./buffer/fixed3-codec.js";
|
|
119
123
|
import { writeBackup, removeBackup, applyBackup } from "./buffer/backup.js";
|
|
124
|
+
|
|
125
|
+
let kittyImageMode = "off";
|
|
126
|
+
let allowRemoteKittyImages = false;
|
|
127
|
+
const remoteMarkdownSources = new Map();
|
|
120
128
|
import { isHex3Encoding, isMdcuiEncoding } from "./runtime/encodings.js";
|
|
121
129
|
import { createInterface } from "node:readline/promises";
|
|
122
130
|
|
|
@@ -311,7 +319,12 @@ async function readTextFileWithEncoding(path, encoding = "utf-8", inferMdcui = t
|
|
|
311
319
|
|
|
312
320
|
async function fetchTextWithEncoding(url, encoding = "utf-8", inferMdcui = true) {
|
|
313
321
|
const bytes = await fetchHttpBytes(url);
|
|
314
|
-
return decodeAndRenderTextBytes(
|
|
322
|
+
return decodeAndRenderTextBytes(
|
|
323
|
+
new Uint8Array(bytes),
|
|
324
|
+
encodingForPath(url, encoding, inferMdcui),
|
|
325
|
+
process.stdout.columns || 80,
|
|
326
|
+
url,
|
|
327
|
+
);
|
|
315
328
|
}
|
|
316
329
|
|
|
317
330
|
function normalizeEncodingLabel(encoding = "utf-8") {
|
|
@@ -335,20 +348,30 @@ async function decodeAndRenderTextBytes(bytes, encoding = "utf-8", width = proce
|
|
|
335
348
|
const decoded = decodeTextBytesWithEncoding(bytes, encoding);
|
|
336
349
|
if (!isMdcuiEncoding(decoded.encoding)) return decoded;
|
|
337
350
|
const renderWidth = Math.max(1, Math.trunc(Number(width) || 80));
|
|
338
|
-
const { rendered, tuiSourceText } = await renderMdcui(decoded.text, renderWidth, mdpath);
|
|
351
|
+
const { rendered, tuiSourceText, images } = await renderMdcui(decoded.text, renderWidth, mdpath, mdpath);
|
|
339
352
|
const styled = parseAnsiStyledText(rendered);
|
|
340
|
-
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 };
|
|
341
354
|
}
|
|
342
355
|
|
|
343
|
-
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) {
|
|
344
357
|
const runmd = await import("../runmd.mjs");
|
|
345
358
|
let md = String(markdown);
|
|
346
359
|
if (mdpath && !isHttpUrl(mdpath)) {
|
|
347
360
|
md = await runmd.extractJs(md, mdpath);
|
|
348
361
|
await runmd.createWui(md, mdpath);
|
|
349
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
|
+
});
|
|
350
372
|
return {
|
|
351
|
-
rendered:
|
|
373
|
+
rendered: prepared.rendered,
|
|
374
|
+
images: prepared.images,
|
|
352
375
|
tuiSourceText: md,
|
|
353
376
|
};
|
|
354
377
|
}
|
|
@@ -588,6 +611,12 @@ function resizeMdcuiTextBlock(buf, y, x) {
|
|
|
588
611
|
ansiLines.splice(row, deleteCount, ...replacement);
|
|
589
612
|
buf._mdcuiAnsiText = ansiLines.join("\n");
|
|
590
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
|
+
}
|
|
591
620
|
|
|
592
621
|
if (insertAt >= 0) {
|
|
593
622
|
if (buf.cursor.y >= insertAt) buf.cursor.y++;
|
|
@@ -947,11 +976,14 @@ function parseArgs(argv) {
|
|
|
947
976
|
options: false,
|
|
948
977
|
help: false,
|
|
949
978
|
clean: false,
|
|
979
|
+
check: false,
|
|
950
980
|
cat: false,
|
|
951
981
|
docs: false,
|
|
982
|
+
exportReadme: false,
|
|
952
983
|
changelog: false,
|
|
953
984
|
testapp: false,
|
|
954
|
-
|
|
985
|
+
demoList: false,
|
|
986
|
+
demo: null,
|
|
955
987
|
allowUrl: false,
|
|
956
988
|
buildExe: false,
|
|
957
989
|
buildFor: "",
|
|
@@ -961,6 +993,7 @@ function parseArgs(argv) {
|
|
|
961
993
|
plugin: "",
|
|
962
994
|
cdpPort: 0,
|
|
963
995
|
cdpAddress: "",
|
|
996
|
+
kittyMode: "off",
|
|
964
997
|
settings: new Map(),
|
|
965
998
|
};
|
|
966
999
|
const files = [];
|
|
@@ -971,6 +1004,7 @@ function parseArgs(argv) {
|
|
|
971
1004
|
else if (arg === "-options") flags.options = true;
|
|
972
1005
|
else if (arg === "-help" || arg === "--help" || arg === "-h") flags.help = true;
|
|
973
1006
|
else if (arg === "-clean") flags.clean = true;
|
|
1007
|
+
else if (arg === "--check") flags.check = true;
|
|
974
1008
|
else if (arg === "--cat" || arg === "-cat" || arg === "--ccat" || arg === "-ccat" || arg === "--bat" || arg === "-bat" || arg === "--glow" || arg === "-glow") flags.cat = true;
|
|
975
1009
|
else if (arg === "--xxd" || arg === "--hexdump") {
|
|
976
1010
|
flags.cat = true;
|
|
@@ -989,10 +1023,28 @@ function parseArgs(argv) {
|
|
|
989
1023
|
flags.settings.set("encoding", "utf-8");
|
|
990
1024
|
}
|
|
991
1025
|
else if (arg === "--docs" || arg === "--readme") flags.docs = true;
|
|
1026
|
+
else if (arg === "--export-readme") flags.exportReadme = true;
|
|
992
1027
|
else if (arg === "--changelog") flags.changelog = true;
|
|
993
1028
|
else if (arg === "--testapp.md") flags.testapp = true;
|
|
994
|
-
else if (arg === "--demo") flags.
|
|
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
|
+
}
|
|
995
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";
|
|
996
1048
|
else if (arg === "--build-exe") flags.buildExe = true;
|
|
997
1049
|
else if (arg === "--build-for") flags.buildFor = argv[++i] ?? "";
|
|
998
1050
|
else if (arg === "-debug") flags.debug = true;
|
|
@@ -1024,9 +1076,13 @@ function usage() {
|
|
|
1024
1076
|
${pkg.name} --wui [FILE.md]
|
|
1025
1077
|
|
|
1026
1078
|
Modes:
|
|
1079
|
+
--check FILE.md
|
|
1080
|
+
Check heading and fenced-block IDs for collisions, print details, and exit
|
|
1081
|
+
Exits 0 when IDs are unique, 1 on collisions, and 2 on usage/read errors
|
|
1027
1082
|
--wui [FILE.md]
|
|
1028
1083
|
Generate or overwrite Markdown UI files beside FILE.md and start the server
|
|
1029
|
-
Without FILE.md, use testapp.md
|
|
1084
|
+
Without FILE.md, use the existing ./testapp.md without overwriting it
|
|
1085
|
+
If ./testapp.md is missing, write the bundled demo there first
|
|
1030
1086
|
--cat, --ccat, --bat, --glow
|
|
1031
1087
|
Render file(s) and write to stdout, then exit (.md uses mdcui/createTui)
|
|
1032
1088
|
A local .md file also writes or overwrites five generated files beside it
|
|
@@ -1040,6 +1096,10 @@ Modes:
|
|
|
1040
1096
|
-encoding mdcui
|
|
1041
1097
|
Render Markdown through runmd.mjs#createTui; .md files use this automatically
|
|
1042
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
|
|
1043
1103
|
|
|
1044
1104
|
Settings:
|
|
1045
1105
|
-SETTING VALUE
|
|
@@ -1060,6 +1120,8 @@ Information:
|
|
|
1060
1120
|
Show version+backend info & exit
|
|
1061
1121
|
--docs, --readme
|
|
1062
1122
|
Show ${pkg.name}'s README.md & exit
|
|
1123
|
+
--export-readme
|
|
1124
|
+
Write or overwrite ./README.md with the bundled README.md & exit
|
|
1063
1125
|
--changelog
|
|
1064
1126
|
Show CHANGELOG.md & exit
|
|
1065
1127
|
-profile, --profile
|
|
@@ -1068,12 +1130,23 @@ Information:
|
|
|
1068
1130
|
Demo:
|
|
1069
1131
|
--testapp.md
|
|
1070
1132
|
Write the bundled testapp.md to stdout & exit
|
|
1133
|
+
--demo-list
|
|
1134
|
+
List the bundled demos and their command-line options, then exit
|
|
1071
1135
|
--demo
|
|
1072
|
-
|
|
1136
|
+
Use the existing ./testapp.md without overwriting it, or write the bundled demo if missing
|
|
1137
|
+
Open it in the TUI and write 5 generated files beside it
|
|
1138
|
+
--demo-<filename>
|
|
1139
|
+
Load demos/<filename>.md, preserving an existing ./<filename>.md or writing the bundled copy
|
|
1140
|
+
Open it in the TUI and write 5 generated files beside it
|
|
1141
|
+
For example: --demo-select, --demo-todo, or --demo-todo-zh
|
|
1142
|
+
--demo-imgtool
|
|
1143
|
+
Alias for --demo-image-processor
|
|
1144
|
+
--demo-imgtool-zh
|
|
1145
|
+
Alias for --demo-image-processor.zh-TW
|
|
1073
1146
|
|
|
1074
1147
|
Remote Markdown:
|
|
1075
1148
|
--allow-url
|
|
1076
|
-
Download HTTP(S) Markdown
|
|
1149
|
+
Download HTTP(S) Markdown and, with Kitty mode, its HTTP(S) images; allow its code to run
|
|
1077
1150
|
|
|
1078
1151
|
Experimental:
|
|
1079
1152
|
--build-exe Build a Bun single-file executable and exit
|
|
@@ -1121,7 +1194,7 @@ class BufferModel {
|
|
|
1121
1194
|
get searchPattern() { return this._searchPattern ?? ""; }
|
|
1122
1195
|
set searchPattern(v) { this._searchPattern = v ?? ""; this.searchMatches?.clear(); }
|
|
1123
1196
|
|
|
1124
|
-
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 } = {}) {
|
|
1125
1198
|
this.path = path;
|
|
1126
1199
|
this.type = type;
|
|
1127
1200
|
this.name = path ? basename(path) : "No name";
|
|
@@ -1134,6 +1207,7 @@ class BufferModel {
|
|
|
1134
1207
|
this._mdcuiAnsiText = isMdcuiEncoding(this.encoding) ? String(ansiText ?? "") : null;
|
|
1135
1208
|
this._mdcuiSourceText = isMdcuiEncoding(this.encoding) ? String(sourceText ?? text) : null;
|
|
1136
1209
|
this._mdcuiTuiSourceText = isMdcuiEncoding(this.encoding) ? String(tuiSourceText ?? sourceText ?? text) : null;
|
|
1210
|
+
this._mdcuiImages = isMdcuiEncoding(this.encoding) ? (mdcuiImages ?? []) : [];
|
|
1137
1211
|
this._mdcuiRenderWidth = Math.trunc(Number(mdcuiRenderWidth) || 0);
|
|
1138
1212
|
this.cursor = { x: 0, y: 0 };
|
|
1139
1213
|
this.scroll = { x: 0, y: 0, row: 0 };
|
|
@@ -1234,6 +1308,7 @@ class BufferModel {
|
|
|
1234
1308
|
let sourceText = null;
|
|
1235
1309
|
let tuiSourceText = null;
|
|
1236
1310
|
let mdcuiRenderWidth = 0;
|
|
1311
|
+
let mdcuiImages = null;
|
|
1237
1312
|
if (existsSync(path)) {
|
|
1238
1313
|
const info = statSync(path);
|
|
1239
1314
|
if (info.isDirectory()) throw new Error(`${path} is a directory`);
|
|
@@ -1247,9 +1322,10 @@ class BufferModel {
|
|
|
1247
1322
|
sourceText = decoded.sourceText ?? null;
|
|
1248
1323
|
tuiSourceText = decoded.tuiSourceText ?? null;
|
|
1249
1324
|
mdcuiRenderWidth = decoded.mdcuiRenderWidth ?? 0;
|
|
1325
|
+
mdcuiImages = decoded.mdcuiImages ?? null;
|
|
1250
1326
|
if (isMdcuiEncoding(encoding)) readonly = true;
|
|
1251
1327
|
}
|
|
1252
|
-
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 });
|
|
1253
1329
|
buffer._configDir = context?.config?.configDir ?? null;
|
|
1254
1330
|
attachSyntax(buffer, context, path, text);
|
|
1255
1331
|
return buffer;
|
|
@@ -1650,6 +1726,7 @@ class BufferModel {
|
|
|
1650
1726
|
this._mdcuiSourceText = isMdcuiEncoding(this.encoding) ? String(decoded.sourceText ?? text) : null;
|
|
1651
1727
|
this._mdcuiTuiSourceText = isMdcuiEncoding(this.encoding) ? String(decoded.tuiSourceText ?? decoded.sourceText ?? text) : null;
|
|
1652
1728
|
this._mdcuiRenderWidth = decoded.mdcuiRenderWidth ?? 0;
|
|
1729
|
+
this._mdcuiImages = decoded.mdcuiImages ?? [];
|
|
1653
1730
|
const readonly = isMdcuiEncoding(this.encoding);
|
|
1654
1731
|
this.fileformat = detectFileFormat(text, this.Settings.fileformat ?? DEFAULT_SETTINGS.fileformat);
|
|
1655
1732
|
this.Settings.fileformat = this.fileformat;
|
|
@@ -1682,6 +1759,7 @@ class BufferModel {
|
|
|
1682
1759
|
this._mdcuiSourceText = isMdcuiEncoding(this.encoding) ? String(decoded.sourceText ?? text) : null;
|
|
1683
1760
|
this._mdcuiTuiSourceText = isMdcuiEncoding(this.encoding) ? String(decoded.tuiSourceText ?? decoded.sourceText ?? text) : null;
|
|
1684
1761
|
this._mdcuiRenderWidth = decoded.mdcuiRenderWidth ?? 0;
|
|
1762
|
+
this._mdcuiImages = decoded.mdcuiImages ?? [];
|
|
1685
1763
|
this.fileformat = detectFileFormat(text, this.Settings.fileformat ?? DEFAULT_SETTINGS.fileformat);
|
|
1686
1764
|
this.Settings.fileformat = this.fileformat;
|
|
1687
1765
|
this.lines = normalizeBufferText(text).split("\n");
|
|
@@ -1705,12 +1783,13 @@ class BufferModel {
|
|
|
1705
1783
|
if (!isMdcuiEncoding(this.encoding) || this._mdcuiTuiSourceText == null) return false;
|
|
1706
1784
|
const renderWidth = Math.max(1, Math.trunc(Number(width) || 80));
|
|
1707
1785
|
if (renderWidth === this._mdcuiRenderWidth) return false;
|
|
1708
|
-
const { rendered } = await renderMdcui(this._mdcuiTuiSourceText, renderWidth);
|
|
1786
|
+
const { rendered, images } = await renderMdcui(this._mdcuiTuiSourceText, renderWidth, null, this.path);
|
|
1709
1787
|
const styled = parseAnsiStyledText(rendered);
|
|
1710
1788
|
this.lines = normalizeBufferText(styled.text).split("\n");
|
|
1711
1789
|
if (this.lines.length === 0) this.lines = [""];
|
|
1712
1790
|
this._ansiStyleLines = styled.styleLines;
|
|
1713
1791
|
this._mdcuiAnsiText = rendered;
|
|
1792
|
+
this._mdcuiImages = images;
|
|
1714
1793
|
this._mdcuiRenderWidth = renderWidth;
|
|
1715
1794
|
this.fileformat = detectFileFormat(styled.text, this.Settings.fileformat ?? DEFAULT_SETTINGS.fileformat);
|
|
1716
1795
|
this.Settings.fileformat = this.fileformat;
|
|
@@ -2376,7 +2455,10 @@ class App {
|
|
|
2376
2455
|
this.clipboard = new ClipboardManager();
|
|
2377
2456
|
this.context = context;
|
|
2378
2457
|
this.shellRunning = false;
|
|
2379
|
-
this.screen = new Screen({
|
|
2458
|
+
this.screen = new Screen({
|
|
2459
|
+
mouse: DEFAULT_SETTINGS.mouse !== false,
|
|
2460
|
+
kittyMode: context.kittyMode ?? "off",
|
|
2461
|
+
});
|
|
2380
2462
|
this.tabRects = [];
|
|
2381
2463
|
this._escBuf = null; // pending lone ESC bytes waiting for alt-key combo
|
|
2382
2464
|
this._escTimer = null;
|
|
@@ -2610,6 +2692,7 @@ class App {
|
|
|
2610
2692
|
|
|
2611
2693
|
const defaultStyle = this.context.colorscheme?.defaultStyle ?? {};
|
|
2612
2694
|
this.screen.fill(" ", defaultStyle);
|
|
2695
|
+
this._kittyFrameImages = [];
|
|
2613
2696
|
|
|
2614
2697
|
this.tabRects = [];
|
|
2615
2698
|
if (tabBarHeight) this.renderTabbar(defaultStyle);
|
|
@@ -2785,6 +2868,7 @@ class App {
|
|
|
2785
2868
|
this.screen.setCursor(0, 0, false);
|
|
2786
2869
|
}
|
|
2787
2870
|
|
|
2871
|
+
this.screen.setKittyImages(this._kittyFrameImages);
|
|
2788
2872
|
this.screen.show();
|
|
2789
2873
|
}
|
|
2790
2874
|
|
|
@@ -2967,6 +3051,59 @@ class App {
|
|
|
2967
3051
|
? (this.context.colorscheme.get("cursor-line")?.fg ?? null)
|
|
2968
3052
|
: null;
|
|
2969
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
|
+
|
|
2970
3107
|
const hasDiff = (buf.Settings?.diffgutter ?? false) && !!buf.diffBase;
|
|
2971
3108
|
if (hasDiff && !buf._diffOnUpdate) buf._diffOnUpdate = () => this.render();
|
|
2972
3109
|
const diffMarks = hasDiff ? getDiffMarkers(buf) : null;
|
|
@@ -2981,6 +3118,18 @@ class App {
|
|
|
2981
3118
|
const msgWarnStyle = cs?.styles?.has("gutter-warning") ? cs.get("gutter-warning") : defaultStyle;
|
|
2982
3119
|
const msgErrStyle = cs?.styles?.has("gutter-error") ? cs.get("gutter-error") : defaultStyle;
|
|
2983
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
|
+
|
|
2984
3133
|
const renderGutter = (lineNo, row, screenRow, subRow = 0) => {
|
|
2985
3134
|
// Message indicator: 2 cols, '> ' with kind-based style (Go: drawGutter)
|
|
2986
3135
|
if (msgW > 0) {
|
|
@@ -3024,6 +3173,7 @@ class App {
|
|
|
3024
3173
|
if (lineNo < buf.lines.length) {
|
|
3025
3174
|
const cells = renderHighlightedCells(buf, lineNo, buf.scroll.x, maxW, this.context.colorscheme, pane.selection, getLineSearchRanges(buf, lineNo), braceMatches, isCL ? clBg : null);
|
|
3026
3175
|
putCells(this.screen, pane.x + gutterW, screenRow, cells, maxW);
|
|
3176
|
+
addKittyImage(lineNo, screenRow);
|
|
3027
3177
|
}
|
|
3028
3178
|
}
|
|
3029
3179
|
} else {
|
|
@@ -3047,6 +3197,7 @@ class App {
|
|
|
3047
3197
|
|
|
3048
3198
|
const cells = renderHighlightedCells(buf, lineNo, segStart, maxW, this.context.colorscheme, pane.selection, _swSearchRanges, braceMatches, isCL ? clBg : null);
|
|
3049
3199
|
putCells(this.screen, pane.x + gutterW, screenRow, cells, maxW);
|
|
3200
|
+
addKittyImage(lineNo, screenRow, subRow);
|
|
3050
3201
|
|
|
3051
3202
|
if (subRow + 1 < breaks.length) {
|
|
3052
3203
|
sloc = { line: lineNo, row: subRow + 1 };
|
|
@@ -4062,6 +4213,28 @@ class App {
|
|
|
4062
4213
|
return true;
|
|
4063
4214
|
}
|
|
4064
4215
|
}
|
|
4216
|
+
|
|
4217
|
+
const checkboxResult = toggleTaskCheckboxBeforeColumn(payload.line, x);
|
|
4218
|
+
if (checkboxResult.toggled) {
|
|
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
|
+
}
|
|
4236
|
+
buf.modified = true;
|
|
4237
|
+
}
|
|
4065
4238
|
|
|
4066
4239
|
this.message = `event ${payload.trigger}:`+
|
|
4067
4240
|
`(${payload.row},${payload.col})`+
|
|
@@ -6754,6 +6927,7 @@ async function loadBufferForPath(pathOrUrl, context, command = {}, { interactive
|
|
|
6754
6927
|
if (!name.toLowerCase().endsWith(".md")) name += ".md";
|
|
6755
6928
|
const localPath = resolve(name);
|
|
6756
6929
|
await Bun.write(localPath, await fetchHttpBytes(pathOrUrl));
|
|
6930
|
+
remoteMarkdownSources.set(localPath, new URL(pathOrUrl).href);
|
|
6757
6931
|
return await loadBufferForPath(localPath, loadContext, command, { interactive });
|
|
6758
6932
|
}
|
|
6759
6933
|
let buffer;
|
|
@@ -6773,6 +6947,7 @@ async function loadBufferForPath(pathOrUrl, context, command = {}, { interactive
|
|
|
6773
6947
|
sourceText: decoded.sourceText ?? null,
|
|
6774
6948
|
tuiSourceText: decoded.tuiSourceText ?? null,
|
|
6775
6949
|
mdcuiRenderWidth: decoded.mdcuiRenderWidth ?? 0,
|
|
6950
|
+
mdcuiImages: decoded.mdcuiImages ?? null,
|
|
6776
6951
|
});
|
|
6777
6952
|
buffer._configDir = context?.config?.configDir ?? null;
|
|
6778
6953
|
attachSyntax(buffer, loadContext, urlPath, text);
|
|
@@ -7483,6 +7658,7 @@ async function loadBuffers(files, command) {
|
|
|
7483
7658
|
sourceText: decoded.sourceText ?? null,
|
|
7484
7659
|
tuiSourceText: decoded.tuiSourceText ?? null,
|
|
7485
7660
|
mdcuiRenderWidth: decoded.mdcuiRenderWidth ?? 0,
|
|
7661
|
+
mdcuiImages: decoded.mdcuiImages ?? null,
|
|
7486
7662
|
});
|
|
7487
7663
|
if (loadBuffers.context) attachSyntax(stdinBuf, loadBuffers.context, "", stdinText);
|
|
7488
7664
|
buffers.push(stdinBuf);
|
|
@@ -7498,8 +7674,17 @@ async function loadBuffers(files, command) {
|
|
|
7498
7674
|
}
|
|
7499
7675
|
|
|
7500
7676
|
async function printReadmeDocs() {
|
|
7501
|
-
|
|
7502
|
-
|
|
7677
|
+
process.stdout.write(Bun.markdown.ansi(await bundledReadmeSource(), { hyperlinks: true }));
|
|
7678
|
+
}
|
|
7679
|
+
|
|
7680
|
+
async function bundledReadmeSource() {
|
|
7681
|
+
return readInternalAssetText("README.md") ?? await Bun.file(join(REPO_ROOT, "README.md")).text();
|
|
7682
|
+
}
|
|
7683
|
+
|
|
7684
|
+
async function exportReadme() {
|
|
7685
|
+
const readmePath = resolve("README.md");
|
|
7686
|
+
await Bun.write(readmePath, await bundledReadmeSource());
|
|
7687
|
+
console.log(`Wrote ${readmePath}`);
|
|
7503
7688
|
}
|
|
7504
7689
|
|
|
7505
7690
|
async function printChangelogDocs() {
|
|
@@ -7508,11 +7693,47 @@ async function printChangelogDocs() {
|
|
|
7508
7693
|
}
|
|
7509
7694
|
|
|
7510
7695
|
async function printTestappSource() {
|
|
7511
|
-
process.stdout.write(await
|
|
7696
|
+
process.stdout.write(await bundledMarkdownSource("testapp.md"));
|
|
7697
|
+
}
|
|
7698
|
+
|
|
7699
|
+
async function bundledMarkdownSource(filename) {
|
|
7700
|
+
return readInternalAssetText(filename) ?? await Bun.file(join(REPO_ROOT, filename)).text();
|
|
7701
|
+
}
|
|
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);
|
|
7512
7720
|
}
|
|
7513
7721
|
|
|
7514
|
-
|
|
7515
|
-
|
|
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");
|
|
7516
7737
|
}
|
|
7517
7738
|
|
|
7518
7739
|
async function main() {
|
|
@@ -7528,6 +7749,8 @@ async function main() {
|
|
|
7528
7749
|
await buildEarlyExit(null,DEFAULT_BUILD_OUTFILE)
|
|
7529
7750
|
|
|
7530
7751
|
const { flags, files: rawFiles } = parseArgs(process.argv.slice(2));
|
|
7752
|
+
kittyImageMode = flags.kittyMode;
|
|
7753
|
+
allowRemoteKittyImages = flags.allowUrl;
|
|
7531
7754
|
|
|
7532
7755
|
if (flags.help) {
|
|
7533
7756
|
console.log(usage());
|
|
@@ -7558,10 +7781,33 @@ async function main() {
|
|
|
7558
7781
|
console.log("Clipboard:", backends);
|
|
7559
7782
|
return;
|
|
7560
7783
|
}
|
|
7784
|
+
if (flags.check) {
|
|
7785
|
+
if (rawFiles.length !== 1) {
|
|
7786
|
+
console.error("Usage: jsmdcui --check FILE.md");
|
|
7787
|
+
process.exitCode = 2;
|
|
7788
|
+
return;
|
|
7789
|
+
}
|
|
7790
|
+
const checkPath = resolve(rawFiles[0]);
|
|
7791
|
+
try {
|
|
7792
|
+
const file = Bun.file(checkPath);
|
|
7793
|
+
if (!(await file.exists())) throw new Error("file not found");
|
|
7794
|
+
const result = checkMarkdownIdCollisions(await file.text());
|
|
7795
|
+
process.stdout.write(formatMarkdownIdCheckAnsi(checkPath, result));
|
|
7796
|
+
if (result.collisions.length) process.exitCode = 1;
|
|
7797
|
+
} catch (error) {
|
|
7798
|
+
console.error(`Cannot check ${checkPath}: ${error?.message || error}`);
|
|
7799
|
+
process.exitCode = 2;
|
|
7800
|
+
}
|
|
7801
|
+
return;
|
|
7802
|
+
}
|
|
7561
7803
|
if (flags.docs) {
|
|
7562
7804
|
await printReadmeDocs();
|
|
7563
7805
|
return;
|
|
7564
7806
|
}
|
|
7807
|
+
if (flags.exportReadme) {
|
|
7808
|
+
await exportReadme();
|
|
7809
|
+
return;
|
|
7810
|
+
}
|
|
7565
7811
|
if (flags.changelog) {
|
|
7566
7812
|
await printChangelogDocs();
|
|
7567
7813
|
return;
|
|
@@ -7570,9 +7816,28 @@ async function main() {
|
|
|
7570
7816
|
await printTestappSource();
|
|
7571
7817
|
return;
|
|
7572
7818
|
}
|
|
7819
|
+
if (flags.demoList) {
|
|
7820
|
+
printDemoList();
|
|
7821
|
+
return;
|
|
7822
|
+
}
|
|
7573
7823
|
if (flags.demo) {
|
|
7574
|
-
|
|
7575
|
-
|
|
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);
|
|
7838
|
+
if (!(await Bun.file(demoPath).exists())) {
|
|
7839
|
+
await Bun.write(demoPath, demoSource);
|
|
7840
|
+
}
|
|
7576
7841
|
rawFiles.splice(0, rawFiles.length, demoPath);
|
|
7577
7842
|
}
|
|
7578
7843
|
if (flags.options) {
|
|
@@ -7655,7 +7920,16 @@ async function main() {
|
|
|
7655
7920
|
|
|
7656
7921
|
const { files, command } = parseInput(rawFiles);
|
|
7657
7922
|
const jsPlugins = new JsPluginManager();
|
|
7658
|
-
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
|
+
};
|
|
7659
7933
|
jsPlugins.setContext(context);
|
|
7660
7934
|
buildMicroGlobal(jsPlugins); // sets globalThis.micro
|
|
7661
7935
|
|