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/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(new Uint8Array(bytes), encodingForPath(url, encoding, inferMdcui));
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: runmd.createTui(md, Math.max(1, Math.trunc(Number(width) || 80))),
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
- demo: false,
959
- demoSelect: false,
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.demo = true;
1004
- else if (arg === "--demo-select") flags.demoSelect = true;
1005
- else if (arg === "--demo-imgtool") flags.demoImgtool = true;
1006
- else if (arg === "--demo-imgtool-zh") flags.demoImgtoolZh = true;
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-select
1093
- Use the existing ./select.md without overwriting it, or write the bundled demo if missing
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
- Use the existing ./image-processor.md without overwriting it, or write the bundled demo if missing
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
- Use the existing ./image-processor.zh-TW.md without overwriting it, or write the bundled demo if missing
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 to cwd, write 5 generated files, and allow its code to run
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({ mouse: DEFAULT_SETTINGS.mouse !== false });
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
- const demoFilename = flags.demo
7640
- ? "testapp.md"
7641
- : flags.demoSelect
7642
- ? "select.md"
7643
- : flags.demoImgtool
7644
- ? "image-processor.md"
7645
- : flags.demoImgtoolZh
7646
- ? "image-processor.zh-TW.md"
7647
- : "";
7648
- if (demoFilename) {
7649
- const demoPath = resolve(demoFilename);
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, await bundledMarkdownSource(demoFilename));
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 = { colorscheme, syntaxDefinitions, config, runtime, jsPlugins, encodingExplicit, allowUrl: flags.allowUrl };
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