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/plugins/js-bridge.js
CHANGED
|
@@ -840,31 +840,185 @@ function _tuiHeadingLines(buffer) {
|
|
|
840
840
|
}
|
|
841
841
|
|
|
842
842
|
function _headingCheckboxValue(buffer, heading, id) {
|
|
843
|
+
const list = _tuiHeadingTaskList(buffer, heading);
|
|
844
|
+
if (!list) return id.startsWith("select") ? null : [];
|
|
845
|
+
const selected = [];
|
|
846
|
+
for (const item of list.items) {
|
|
847
|
+
const checkbox = String(buffer.lines[item.start] ?? "").match(/^(\s*)([☐☒])(?:\s+|$)(.*)$/);
|
|
848
|
+
if (checkbox?.[2] !== "☒") continue;
|
|
849
|
+
if (id.startsWith("select")) return item.value;
|
|
850
|
+
selected.push(item.value);
|
|
851
|
+
}
|
|
852
|
+
return id.startsWith("select") ? null : selected;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function _tuiHeadingTaskList(buffer, heading) {
|
|
856
|
+
const savedAnchor = buffer?._mdcuiHeadingTaskListAnchors?.get(heading.ordinal);
|
|
857
|
+
if (savedAnchor) {
|
|
858
|
+
return {
|
|
859
|
+
first: savedAnchor.index,
|
|
860
|
+
end: savedAnchor.index,
|
|
861
|
+
indent: savedAnchor.indent,
|
|
862
|
+
items: [],
|
|
863
|
+
};
|
|
864
|
+
}
|
|
843
865
|
const headingLine = _headingTuiLine(buffer, heading);
|
|
844
|
-
if (!headingLine || !Array.isArray(buffer?.lines))
|
|
845
|
-
return id.startsWith("select") ? null : [];
|
|
866
|
+
if (!headingLine || !Array.isArray(buffer?.lines)) return null;
|
|
846
867
|
|
|
847
868
|
const tuiHeadings = _tuiHeadingLines(buffer);
|
|
848
869
|
const nextHeading = tuiHeadings[heading.ordinal + 1];
|
|
849
870
|
const endLine = nextHeading?.line ?? buffer.lines.length + 1;
|
|
871
|
+
let first = -1;
|
|
872
|
+
let indent = "";
|
|
850
873
|
|
|
851
|
-
let
|
|
852
|
-
|
|
853
|
-
for (let lineNumber = headingLine + 1; lineNumber < endLine; lineNumber++) {
|
|
854
|
-
const line = String(buffer.lines[lineNumber - 1] ?? "");
|
|
855
|
-
const checkbox = line.match(/^(\s*)([☐☒])(?:\s+|$)(.*)$/);
|
|
874
|
+
for (let y = headingLine; y < endLine - 1; y++) {
|
|
875
|
+
const checkbox = String(buffer.lines[y] ?? "").match(/^(\s*)[☐☒](?:\s+|$)/);
|
|
856
876
|
if (!checkbox) continue;
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
const value = checkbox[3].trim();
|
|
861
|
-
if (id.startsWith("select")) return value;
|
|
862
|
-
selected.push(value);
|
|
877
|
+
first = y;
|
|
878
|
+
indent = checkbox[1];
|
|
879
|
+
break;
|
|
863
880
|
}
|
|
864
|
-
|
|
881
|
+
if (first < 0) return null;
|
|
882
|
+
|
|
883
|
+
let end = endLine - 1;
|
|
884
|
+
let pendingBlank = -1;
|
|
885
|
+
for (let y = first + 1; y < endLine - 1; y++) {
|
|
886
|
+
const line = String(buffer.lines[y] ?? "");
|
|
887
|
+
if (line.trim() === "") {
|
|
888
|
+
if (pendingBlank < 0) pendingBlank = y;
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
const leading = line.match(/^\s*/)?.[0].length ?? 0;
|
|
892
|
+
const checkbox = line.match(/^(\s*)[☐☒](?:\s+|$)/);
|
|
893
|
+
if (checkbox || leading > indent.length) {
|
|
894
|
+
pendingBlank = -1;
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
end = pendingBlank >= 0 ? pendingBlank : y;
|
|
898
|
+
break;
|
|
899
|
+
}
|
|
900
|
+
if (end === endLine - 1 && pendingBlank >= 0) end = pendingBlank;
|
|
901
|
+
|
|
902
|
+
const items = [];
|
|
903
|
+
for (let y = first; y < end; y++) {
|
|
904
|
+
const checkbox = String(buffer.lines[y] ?? "").match(/^(\s*)([☐☒])(?:\s+|$)(.*)$/);
|
|
905
|
+
if (checkbox && checkbox[1] === indent) {
|
|
906
|
+
items.push({
|
|
907
|
+
start: y,
|
|
908
|
+
end,
|
|
909
|
+
value: checkbox[3].trim(),
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
for (let index = 0; index < items.length - 1; index++)
|
|
914
|
+
items[index].end = items[index + 1].start;
|
|
915
|
+
|
|
916
|
+
return { first, end, indent, items };
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function _normalizedSpliceRange(length, argumentCount, start, deleteCount) {
|
|
920
|
+
if (argumentCount === 0) return { start: 0, deleteCount: 0 };
|
|
921
|
+
let relativeStart = Number(start);
|
|
922
|
+
if (Number.isNaN(relativeStart)) relativeStart = 0;
|
|
923
|
+
relativeStart = Math.trunc(relativeStart);
|
|
924
|
+
const actualStart = relativeStart < 0
|
|
925
|
+
? Math.max(length + relativeStart, 0)
|
|
926
|
+
: Math.min(relativeStart, length);
|
|
927
|
+
if (argumentCount === 1) return { start: actualStart, deleteCount: length - actualStart };
|
|
928
|
+
let requestedDelete = Number(deleteCount);
|
|
929
|
+
if (Number.isNaN(requestedDelete)) requestedDelete = 0;
|
|
930
|
+
requestedDelete = Math.max(0, Math.trunc(requestedDelete));
|
|
931
|
+
return {
|
|
932
|
+
start: actualStart,
|
|
933
|
+
deleteCount: Math.min(requestedDelete, length - actualStart),
|
|
934
|
+
};
|
|
865
935
|
}
|
|
866
936
|
|
|
867
|
-
function
|
|
937
|
+
function _tuiTaskItemReplacement(indent, inputs) {
|
|
938
|
+
const normalized = inputs.map((input) => {
|
|
939
|
+
const item = input && typeof input === "object"
|
|
940
|
+
? { value: input.value ?? input.label ?? "", checked: Boolean(input.checked) }
|
|
941
|
+
: { value: input ?? "", checked: false };
|
|
942
|
+
return {
|
|
943
|
+
value: String(item.value).replace(/\r?\n/g, " "),
|
|
944
|
+
checked: item.checked,
|
|
945
|
+
};
|
|
946
|
+
});
|
|
947
|
+
return {
|
|
948
|
+
lines: normalized.map((item) =>
|
|
949
|
+
`${indent}${item.checked ? "☒" : "☐"} ${item.value}`,
|
|
950
|
+
),
|
|
951
|
+
styles: normalized.map((item) => {
|
|
952
|
+
const styles = [];
|
|
953
|
+
if (item.checked) {
|
|
954
|
+
styles[indent.length] = { fg: "green" };
|
|
955
|
+
styles[indent.length + 1] = { fg: "green" };
|
|
956
|
+
}
|
|
957
|
+
return styles;
|
|
958
|
+
}),
|
|
959
|
+
ansi: normalized.map((item) =>
|
|
960
|
+
`${indent}\x1b[${item.checked ? "32" : "2"}m${item.checked ? "☒" : "☐"} \x1b[0m${item.value}`,
|
|
961
|
+
),
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function _mutateTuiHeadingList(buffer, heading, method, args) {
|
|
966
|
+
const list = _tuiHeadingTaskList(buffer, heading);
|
|
967
|
+
if (!list) {
|
|
968
|
+
if (method === "push" || method === "unshift") return 0;
|
|
969
|
+
if (method === "splice") return [];
|
|
970
|
+
return undefined;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (method === "splice") {
|
|
974
|
+
const range = _normalizedSpliceRange(list.items.length, args.length, args[0], args[1]);
|
|
975
|
+
const removedItems = list.items.slice(range.start, range.start + range.deleteCount);
|
|
976
|
+
const removed = removedItems.map((item) => item.value);
|
|
977
|
+
const replacement = _tuiTaskItemReplacement(list.indent, args.slice(2));
|
|
978
|
+
if (range.deleteCount === 0 && replacement.lines.length === 0) return [];
|
|
979
|
+
const start = list.items[range.start]?.start ?? list.end;
|
|
980
|
+
const end = removedItems.at(-1)?.end ?? start;
|
|
981
|
+
if (range.deleteCount === list.items.length && replacement.lines.length === 0 && list.items.length > 0) {
|
|
982
|
+
if (!(buffer._mdcuiHeadingTaskListAnchors instanceof Map))
|
|
983
|
+
buffer._mdcuiHeadingTaskListAnchors = new Map();
|
|
984
|
+
buffer._mdcuiHeadingTaskListAnchors.set(heading.ordinal, { index: start, indent: list.indent });
|
|
985
|
+
}
|
|
986
|
+
_spliceBufferLines(buffer, start, end - start, replacement.lines, {
|
|
987
|
+
styles: replacement.styles,
|
|
988
|
+
ansi: replacement.ansi,
|
|
989
|
+
});
|
|
990
|
+
if (replacement.lines.length > 0)
|
|
991
|
+
buffer._mdcuiHeadingTaskListAnchors?.delete(heading.ordinal);
|
|
992
|
+
return removed;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (method === "pop" || method === "shift") {
|
|
996
|
+
const item = method === "pop" ? list.items.at(-1) : list.items[0];
|
|
997
|
+
if (!item) return undefined;
|
|
998
|
+
if (list.items.length === 1) {
|
|
999
|
+
if (!(buffer._mdcuiHeadingTaskListAnchors instanceof Map))
|
|
1000
|
+
buffer._mdcuiHeadingTaskListAnchors = new Map();
|
|
1001
|
+
buffer._mdcuiHeadingTaskListAnchors.set(heading.ordinal, {
|
|
1002
|
+
index: item.start,
|
|
1003
|
+
indent: list.indent,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
_spliceBufferLines(buffer, item.start, item.end - item.start, []);
|
|
1007
|
+
return item.value;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const replacement = _tuiTaskItemReplacement(list.indent, args);
|
|
1011
|
+
if (replacement.lines.length === 0) return list.items.length;
|
|
1012
|
+
const start = method === "push" ? list.end : list.first;
|
|
1013
|
+
_spliceBufferLines(buffer, start, 0, replacement.lines, {
|
|
1014
|
+
styles: replacement.styles,
|
|
1015
|
+
ansi: replacement.ansi,
|
|
1016
|
+
});
|
|
1017
|
+
if (replacement.lines.length > 0) buffer._mdcuiHeadingTaskListAnchors?.delete(heading.ordinal);
|
|
1018
|
+
return list.items.length + replacement.lines.length;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function _spliceBufferLines(buffer, start, deleteCount, replacement, replacementMeta = {}) {
|
|
868
1022
|
const oldCursor = buffer.cursor ? { ...buffer.cursor } : null;
|
|
869
1023
|
buffer.lines.splice(start, deleteCount, ...replacement);
|
|
870
1024
|
|
|
@@ -873,15 +1027,29 @@ function _spliceBufferLines(buffer, start, deleteCount, replacement) {
|
|
|
873
1027
|
buffer._ansiStyleLines.splice(
|
|
874
1028
|
start,
|
|
875
1029
|
deleteCount,
|
|
876
|
-
...replacement.map(() => template),
|
|
1030
|
+
...(replacementMeta.styles ?? replacement.map(() => template)),
|
|
877
1031
|
);
|
|
878
1032
|
}
|
|
879
1033
|
|
|
880
1034
|
if (typeof buffer._mdcuiAnsiText === "string") {
|
|
881
1035
|
const ansiLines = buffer._mdcuiAnsiText.split("\n");
|
|
882
|
-
ansiLines.splice(start, deleteCount, ...replacement);
|
|
1036
|
+
ansiLines.splice(start, deleteCount, ...(replacementMeta.ansi ?? replacement));
|
|
883
1037
|
buffer._mdcuiAnsiText = ansiLines.join("\n");
|
|
884
1038
|
}
|
|
1039
|
+
if (Array.isArray(buffer._mdcuiImages)) {
|
|
1040
|
+
const delta = replacement.length - deleteCount;
|
|
1041
|
+
buffer._mdcuiImages = buffer._mdcuiImages
|
|
1042
|
+
.filter((image) => image.line < start || image.line >= start + deleteCount)
|
|
1043
|
+
.map((image) => image.line >= start + deleteCount ? { ...image, line: image.line + delta } : image);
|
|
1044
|
+
}
|
|
1045
|
+
if (buffer._mdcuiHeadingTaskListAnchors instanceof Map) {
|
|
1046
|
+
const oldEnd = start + deleteCount;
|
|
1047
|
+
const delta = replacement.length - deleteCount;
|
|
1048
|
+
for (const anchor of buffer._mdcuiHeadingTaskListAnchors.values()) {
|
|
1049
|
+
if (anchor.index >= oldEnd) anchor.index += delta;
|
|
1050
|
+
else if (anchor.index >= start) anchor.index = start;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
885
1053
|
|
|
886
1054
|
buffer.invalidateHighlightFrom?.(start, { force: replacement.length !== deleteCount });
|
|
887
1055
|
if (oldCursor) {
|
|
@@ -961,6 +1129,65 @@ export function createTuiSelector(getBuffer) {
|
|
|
961
1129
|
return args.length > 0 ? selection : "";
|
|
962
1130
|
}
|
|
963
1131
|
},
|
|
1132
|
+
push(...items) {
|
|
1133
|
+
try {
|
|
1134
|
+
const buffer = getBuffer?.();
|
|
1135
|
+
const heading = _findHeading(buffer, selector);
|
|
1136
|
+
return heading ? _mutateTuiHeadingList(buffer, heading, "push", items) : 0;
|
|
1137
|
+
} catch {
|
|
1138
|
+
return 0;
|
|
1139
|
+
}
|
|
1140
|
+
},
|
|
1141
|
+
pop() {
|
|
1142
|
+
try {
|
|
1143
|
+
const buffer = getBuffer?.();
|
|
1144
|
+
const heading = _findHeading(buffer, selector);
|
|
1145
|
+
return heading ? _mutateTuiHeadingList(buffer, heading, "pop", []) : undefined;
|
|
1146
|
+
} catch {
|
|
1147
|
+
return undefined;
|
|
1148
|
+
}
|
|
1149
|
+
},
|
|
1150
|
+
shift() {
|
|
1151
|
+
try {
|
|
1152
|
+
const buffer = getBuffer?.();
|
|
1153
|
+
const heading = _findHeading(buffer, selector);
|
|
1154
|
+
return heading ? _mutateTuiHeadingList(buffer, heading, "shift", []) : undefined;
|
|
1155
|
+
} catch {
|
|
1156
|
+
return undefined;
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
unshift(...items) {
|
|
1160
|
+
try {
|
|
1161
|
+
const buffer = getBuffer?.();
|
|
1162
|
+
const heading = _findHeading(buffer, selector);
|
|
1163
|
+
return heading ? _mutateTuiHeadingList(buffer, heading, "unshift", items) : 0;
|
|
1164
|
+
} catch {
|
|
1165
|
+
return 0;
|
|
1166
|
+
}
|
|
1167
|
+
},
|
|
1168
|
+
splice(...args) {
|
|
1169
|
+
try {
|
|
1170
|
+
const buffer = getBuffer?.();
|
|
1171
|
+
const heading = _findHeading(buffer, selector);
|
|
1172
|
+
return heading ? _mutateTuiHeadingList(buffer, heading, "splice", args) : [];
|
|
1173
|
+
} catch {
|
|
1174
|
+
return [];
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
slice(...args) {
|
|
1178
|
+
try {
|
|
1179
|
+
const buffer = getBuffer?.();
|
|
1180
|
+
const heading = _findHeading(buffer, selector);
|
|
1181
|
+
const list = heading ? _tuiHeadingTaskList(buffer, heading) : null;
|
|
1182
|
+
if (!list) return [];
|
|
1183
|
+
return list.items.map((item) => ({
|
|
1184
|
+
value: item.value,
|
|
1185
|
+
checked: String(buffer.lines[item.start] ?? "").match(/^(\s*)([☐☒])(?:\s+|$)/)?.[2] === "☒",
|
|
1186
|
+
})).slice(...args);
|
|
1187
|
+
} catch {
|
|
1188
|
+
return [];
|
|
1189
|
+
}
|
|
1190
|
+
},
|
|
964
1191
|
};
|
|
965
1192
|
return selection;
|
|
966
1193
|
};
|
package/src/screen/screen.js
CHANGED
|
@@ -2,6 +2,7 @@ import process from "node:process";
|
|
|
2
2
|
import { styleToAnsi } from "../display/ansi-style.js";
|
|
3
3
|
import { CellBuffer } from "./cell-buffer.js";
|
|
4
4
|
import { DISABLE_MOUSE, DISABLE_PASTE, ENABLE_MOUSE, ENABLE_PASTE, ResizeEvent } from "./events.js";
|
|
5
|
+
import { logKittyPlacement } from "../cui/kitty-debug.mjs";
|
|
5
6
|
|
|
6
7
|
const CURSOR_SHAPE_SEQUENCE = {
|
|
7
8
|
default: "\x1b[0 q",
|
|
@@ -13,28 +14,45 @@ const CURSOR_SHAPE_SEQUENCE = {
|
|
|
13
14
|
bar: "\x1b[6 q",
|
|
14
15
|
};
|
|
15
16
|
|
|
17
|
+
const KITTY_APC = "\x1b_G";
|
|
18
|
+
const KITTY_ST = "\x1b\\";
|
|
19
|
+
const KITTY_CHUNK_SIZE = 4096;
|
|
20
|
+
|
|
16
21
|
export function cursorShapeSequence(shape) {
|
|
17
22
|
return CURSOR_SHAPE_SEQUENCE[shape] ?? CURSOR_SHAPE_SEQUENCE.block;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export class Screen {
|
|
21
|
-
constructor({ mouse = true } = {}) {
|
|
26
|
+
constructor({ mouse = true, kittyMode = "off" } = {}) {
|
|
22
27
|
this.mouse = mouse;
|
|
28
|
+
this.kittyMode = kittyMode;
|
|
23
29
|
this.cols = process.stdout.columns || 80;
|
|
24
30
|
this.rows = process.stdout.rows || 24;
|
|
25
31
|
this.cells = new CellBuffer(this.cols, this.rows);
|
|
26
32
|
this.previous = null;
|
|
27
33
|
this.cursor = null;
|
|
28
34
|
this.cursorVisible = false;
|
|
35
|
+
this.kittyImages = [];
|
|
36
|
+
this._shownKittySignature = "";
|
|
37
|
+
this._shownKittyIds = [];
|
|
38
|
+
this._transmittedKittyIds = new Set();
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
init() {
|
|
42
|
+
this._shownKittySignature = "";
|
|
43
|
+
this._shownKittyIds = [];
|
|
44
|
+
this._transmittedKittyIds.clear();
|
|
32
45
|
this.write("\x1b[?1049h\x1b[?25l");
|
|
33
46
|
if (this.mouse) this.write(ENABLE_MOUSE);
|
|
34
47
|
this.write(ENABLE_PASTE);
|
|
35
48
|
}
|
|
36
49
|
|
|
37
50
|
fini() {
|
|
51
|
+
for (const id of this._transmittedKittyIds)
|
|
52
|
+
this.write(`${KITTY_APC}a=d,d=I,i=${id},q=2;${KITTY_ST}`);
|
|
53
|
+
this._shownKittySignature = "";
|
|
54
|
+
this._shownKittyIds = [];
|
|
55
|
+
this._transmittedKittyIds.clear();
|
|
38
56
|
if (this.mouse) this.write(DISABLE_MOUSE);
|
|
39
57
|
this.write(DISABLE_PASTE);
|
|
40
58
|
this.write("\x1b[0 q\x1b[?25h\x1b[?1049l\x1b[0m");
|
|
@@ -97,6 +115,74 @@ export class Screen {
|
|
|
97
115
|
if (this.cursor && this.cursorVisible) {
|
|
98
116
|
out += cursorShapeSequence(this.cursor.shape) + this.move(this.cursor.y + 1, this.cursor.x + 1) + "\x1b[?25h";
|
|
99
117
|
} else out += "\x1b[?25l";
|
|
118
|
+
const kittySignature = this.kittyImages.map((image) =>
|
|
119
|
+
`${image.id}:${image.placementId ?? image.id}:${image.x}:${image.y}:${image.cols}:${image.rows}:${image.sourceX ?? 0}:${image.sourceY ?? 0}:${image.sourceWidth ?? 0}:${image.sourceHeight ?? 0}`
|
|
120
|
+
).join("|");
|
|
121
|
+
if (kittySignature !== this._shownKittySignature) {
|
|
122
|
+
logKittyPlacement("screen-overlay-change", {
|
|
123
|
+
previousSignature: this._shownKittySignature,
|
|
124
|
+
nextSignature: kittySignature,
|
|
125
|
+
deletingImageIds: this._shownKittyIds,
|
|
126
|
+
terminalCols: this.cols,
|
|
127
|
+
terminalRows: this.rows,
|
|
128
|
+
textCursor: this.cursor,
|
|
129
|
+
textCursorVisible: this.cursorVisible,
|
|
130
|
+
});
|
|
131
|
+
for (const id of this._shownKittyIds)
|
|
132
|
+
out += `${KITTY_APC}a=d,d=i,i=${id},q=2;${KITTY_ST}`;
|
|
133
|
+
for (const image of this.kittyImages) {
|
|
134
|
+
const transmitted = this._transmittedKittyIds.has(image.id);
|
|
135
|
+
logKittyPlacement("screen-placement-packet", {
|
|
136
|
+
imageId: image.id,
|
|
137
|
+
placementId: image.placementId ?? image.id,
|
|
138
|
+
zeroBasedX: image.x,
|
|
139
|
+
zeroBasedY: image.y,
|
|
140
|
+
csiCol: image.x + 1,
|
|
141
|
+
csiRow: image.y + 1,
|
|
142
|
+
cols: image.cols,
|
|
143
|
+
rows: image.rows,
|
|
144
|
+
sourceRect: image.sourceWidth && image.sourceHeight ? {
|
|
145
|
+
x: image.sourceX ?? 0,
|
|
146
|
+
y: image.sourceY ?? 0,
|
|
147
|
+
width: image.sourceWidth,
|
|
148
|
+
height: image.sourceHeight,
|
|
149
|
+
} : null,
|
|
150
|
+
C: 1,
|
|
151
|
+
mime: image.mime,
|
|
152
|
+
bytes: image.data?.length ?? 0,
|
|
153
|
+
action: transmitted ? "p" : "T",
|
|
154
|
+
});
|
|
155
|
+
out += this.move(image.y + 1, image.x + 1);
|
|
156
|
+
const placementFields = [
|
|
157
|
+
`i=${image.id}`, `p=${image.placementId ?? image.id}`, "q=2",
|
|
158
|
+
...(image.sourceWidth && image.sourceHeight ? [
|
|
159
|
+
`x=${image.sourceX ?? 0}`,
|
|
160
|
+
`y=${image.sourceY ?? 0}`,
|
|
161
|
+
`w=${image.sourceWidth}`,
|
|
162
|
+
`h=${image.sourceHeight}`,
|
|
163
|
+
] : []),
|
|
164
|
+
`c=${image.cols}`, `r=${image.rows}`, "C=1",
|
|
165
|
+
...(this.kittyMode === "extended" && image.mime ? [`U=${image.mime}`] : []),
|
|
166
|
+
];
|
|
167
|
+
if (transmitted) {
|
|
168
|
+
out += `${KITTY_APC}a=p,${placementFields.join(",")};${KITTY_ST}`;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const base64 = Buffer.from(image.data).toString("base64");
|
|
172
|
+
for (let offset = 0; offset < base64.length; offset += KITTY_CHUNK_SIZE) {
|
|
173
|
+
const payload = base64.slice(offset, offset + KITTY_CHUNK_SIZE);
|
|
174
|
+
const more = offset + KITTY_CHUNK_SIZE < base64.length;
|
|
175
|
+
const fields = [
|
|
176
|
+
"a=T", "f=100", "t=d", ...placementFields, `m=${more ? 1 : 0}`,
|
|
177
|
+
];
|
|
178
|
+
out += `${KITTY_APC}${fields.join(",")};${payload}${KITTY_ST}`;
|
|
179
|
+
}
|
|
180
|
+
this._transmittedKittyIds.add(image.id);
|
|
181
|
+
}
|
|
182
|
+
if (this.cursor && this.cursorVisible) out += this.move(this.cursor.y + 1, this.cursor.x + 1);
|
|
183
|
+
this._shownKittySignature = kittySignature;
|
|
184
|
+
this._shownKittyIds = [...new Set(this.kittyImages.map((image) => image.id))];
|
|
185
|
+
}
|
|
100
186
|
this.write(out);
|
|
101
187
|
this.previous = this.cells.clone();
|
|
102
188
|
}
|
|
@@ -110,6 +196,26 @@ export class Screen {
|
|
|
110
196
|
this.cursorVisible = visible;
|
|
111
197
|
}
|
|
112
198
|
|
|
199
|
+
setKittyImages(images = []) {
|
|
200
|
+
this.kittyImages = this.kittyMode === "off" ? [] : images;
|
|
201
|
+
logKittyPlacement("screen-frame-images", {
|
|
202
|
+
count: this.kittyImages.length,
|
|
203
|
+
kittyMode: this.kittyMode,
|
|
204
|
+
images: this.kittyImages.map((image) => ({
|
|
205
|
+
imageId: image.id,
|
|
206
|
+
placementId: image.placementId ?? image.id,
|
|
207
|
+
x: image.x,
|
|
208
|
+
y: image.y,
|
|
209
|
+
cols: image.cols,
|
|
210
|
+
rows: image.rows,
|
|
211
|
+
sourceX: image.sourceX ?? null,
|
|
212
|
+
sourceY: image.sourceY ?? null,
|
|
213
|
+
sourceWidth: image.sourceWidth ?? null,
|
|
214
|
+
sourceHeight: image.sourceHeight ?? null,
|
|
215
|
+
})),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
113
219
|
hideCursor() {
|
|
114
220
|
this.cursorVisible = false;
|
|
115
221
|
this.write("\x1b[?25l");
|
|
@@ -133,6 +239,7 @@ export class Screen {
|
|
|
133
239
|
this.rows = process.stdout.rows || this.rows;
|
|
134
240
|
this.cells.resize(this.cols, this.rows);
|
|
135
241
|
this.previous = null;
|
|
242
|
+
this._shownKittySignature = "";
|
|
136
243
|
return new ResizeEvent(this.cols, this.rows);
|
|
137
244
|
}
|
|
138
245
|
|
|
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
|
|
7
7
|
const tui = join(import.meta.dir, "..", "tui");
|
|
8
|
+
const bunBin = Bun.which("bun") || process.argv0;
|
|
8
9
|
|
|
9
10
|
test("cat renders .md files once with the implicit mdcui encoding", async () => {
|
|
10
11
|
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-cat-"));
|
|
@@ -12,12 +13,12 @@ test("cat renders .md files once with the implicit mdcui encoding", async () =>
|
|
|
12
13
|
await writeFile(markdownPath, "# Heading\n\n- one\n- two\n");
|
|
13
14
|
|
|
14
15
|
try {
|
|
15
|
-
const implicit = Bun.spawnSync([tui, "-cat", markdownPath], {
|
|
16
|
+
const implicit = Bun.spawnSync([bunBin, tui, "-cat", markdownPath], {
|
|
16
17
|
cwd: dir,
|
|
17
18
|
stdout: "pipe",
|
|
18
19
|
stderr: "pipe",
|
|
19
20
|
});
|
|
20
|
-
const explicit = Bun.spawnSync([tui, "-cat", "-encoding", "mdcui", markdownPath], {
|
|
21
|
+
const explicit = Bun.spawnSync([bunBin, tui, "-cat", "-encoding", "mdcui", markdownPath], {
|
|
21
22
|
cwd: dir,
|
|
22
23
|
stdout: "pipe",
|
|
23
24
|
stderr: "pipe",
|
|
@@ -37,7 +38,7 @@ test("an explicit utf8 encoding overrides the .md mdcui default", async () => {
|
|
|
37
38
|
await writeFile(markdownPath, "# Heading\n\n```js front\nalert('kept')\n```\n");
|
|
38
39
|
|
|
39
40
|
try {
|
|
40
|
-
const result = Bun.spawnSync([tui, "-cat", "-encoding", "utf8", markdownPath], {
|
|
41
|
+
const result = Bun.spawnSync([bunBin, tui, "-cat", "-encoding", "utf8", markdownPath], {
|
|
41
42
|
cwd: dir,
|
|
42
43
|
stdout: "pipe",
|
|
43
44
|
stderr: "pipe",
|
|
@@ -58,12 +59,12 @@ test("--edit overrides the .md mdcui default with utf8", async () => {
|
|
|
58
59
|
await writeFile(markdownPath, "# Heading\n\n```js front\nalert('editable')\n```\n");
|
|
59
60
|
|
|
60
61
|
try {
|
|
61
|
-
const edit = Bun.spawnSync([tui, "-cat", "--edit", markdownPath], {
|
|
62
|
+
const edit = Bun.spawnSync([bunBin, tui, "-cat", "--edit", markdownPath], {
|
|
62
63
|
cwd: dir,
|
|
63
64
|
stdout: "pipe",
|
|
64
65
|
stderr: "pipe",
|
|
65
66
|
});
|
|
66
|
-
const utf8 = Bun.spawnSync([tui, "-cat", "-encoding", "utf8", markdownPath], {
|
|
67
|
+
const utf8 = Bun.spawnSync([bunBin, tui, "-cat", "-encoding", "utf8", markdownPath], {
|
|
67
68
|
cwd: dir,
|
|
68
69
|
stdout: "pipe",
|
|
69
70
|
stderr: "pipe",
|
package/tests/demo.test.js
CHANGED
|
@@ -4,9 +4,10 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
6
|
const tui = join(import.meta.dir, "..", "tui");
|
|
7
|
+
const bunBin = Bun.which("bun") || process.argv0;
|
|
7
8
|
|
|
8
9
|
test("--help describes the non-overwriting demo behavior", () => {
|
|
9
|
-
const result = Bun.spawnSync([tui, "--help"], {
|
|
10
|
+
const result = Bun.spawnSync([bunBin, tui, "--help"], {
|
|
10
11
|
stdout: "pipe",
|
|
11
12
|
stderr: "pipe",
|
|
12
13
|
});
|
|
@@ -15,14 +16,32 @@ test("--help describes the non-overwriting demo behavior", () => {
|
|
|
15
16
|
const output = result.stdout.toString();
|
|
16
17
|
expect(output).toContain("use the existing ./testapp.md without overwriting it");
|
|
17
18
|
expect(output).toContain("If ./testapp.md is missing, write the bundled demo there first");
|
|
19
|
+
expect(output).toContain("--demo-<filename>");
|
|
20
|
+
expect(output).toContain("demos/<filename>.md");
|
|
21
|
+
expect(output.match(/Open it in the TUI and write 5 generated files beside it/g)?.length).toBe(2);
|
|
18
22
|
expect(output).toContain("--demo-imgtool");
|
|
19
23
|
expect(output).toContain("--demo-imgtool-zh");
|
|
20
24
|
});
|
|
21
25
|
|
|
26
|
+
test("--demo-list lists root and automatically discovered demos", () => {
|
|
27
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo-list"], {
|
|
28
|
+
stdout: "pipe",
|
|
29
|
+
stderr: "pipe",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(result.exitCode).toBe(0);
|
|
33
|
+
const output = result.stdout.toString();
|
|
34
|
+
expect(output).toMatch(/--demo\s+testapp\.md/);
|
|
35
|
+
expect(output).toMatch(/--demo-image-processor\s+demos\/image-processor\.md/);
|
|
36
|
+
expect(output).toMatch(/--demo-select\s+demos\/select\.md/);
|
|
37
|
+
expect(output).toMatch(/--demo-todo-zh\s+demos\/todo-zh\.md/);
|
|
38
|
+
expect(output).toContain("--demo-imgtool --demo-image-processor");
|
|
39
|
+
});
|
|
40
|
+
|
|
22
41
|
test("--demo writes bundled testapp.md to cwd before opening it", async () => {
|
|
23
42
|
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-demo-"));
|
|
24
43
|
try {
|
|
25
|
-
const result = Bun.spawnSync([tui, "--demo", "-cat", "-encoding", "utf8"], {
|
|
44
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo", "-cat", "-encoding", "utf8"], {
|
|
26
45
|
cwd: dir,
|
|
27
46
|
stdout: "pipe",
|
|
28
47
|
stderr: "pipe",
|
|
@@ -30,8 +49,8 @@ test("--demo writes bundled testapp.md to cwd before opening it", async () => {
|
|
|
30
49
|
|
|
31
50
|
expect(result.exitCode).toBe(0);
|
|
32
51
|
const written = await readFile(join(dir, "testapp.md"), "utf8");
|
|
33
|
-
expect(written).toContain("
|
|
34
|
-
expect(Bun.stripANSI(result.stdout.toString())).toContain("
|
|
52
|
+
expect(written).toContain("計算機");
|
|
53
|
+
expect(Bun.stripANSI(result.stdout.toString())).toContain("計算機");
|
|
35
54
|
} finally {
|
|
36
55
|
await rm(dir, { recursive: true, force: true });
|
|
37
56
|
}
|
|
@@ -42,7 +61,7 @@ test("--demo preserves an existing testapp.md", async () => {
|
|
|
42
61
|
const existing = "# Keep my demo\n";
|
|
43
62
|
try {
|
|
44
63
|
await writeFile(join(dir, "testapp.md"), existing);
|
|
45
|
-
const result = Bun.spawnSync([tui, "--demo", "-cat", "-encoding", "utf8"], {
|
|
64
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo", "-cat", "-encoding", "utf8"], {
|
|
46
65
|
cwd: dir,
|
|
47
66
|
stdout: "pipe",
|
|
48
67
|
stderr: "pipe",
|
|
@@ -56,10 +75,81 @@ test("--demo preserves an existing testapp.md", async () => {
|
|
|
56
75
|
}
|
|
57
76
|
});
|
|
58
77
|
|
|
78
|
+
test("--demo-<filename> automatically loads a matching demos file", async () => {
|
|
79
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-demo-generic-"));
|
|
80
|
+
try {
|
|
81
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo-todo", "-cat", "-encoding", "utf8"], {
|
|
82
|
+
cwd: dir,
|
|
83
|
+
stdout: "pipe",
|
|
84
|
+
stderr: "pipe",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(result.exitCode).toBe(0);
|
|
88
|
+
const written = await readFile(join(dir, "todo.md"), "utf8");
|
|
89
|
+
expect(written).toContain("# Todo List");
|
|
90
|
+
expect(written).toContain("javascript:addTodo()");
|
|
91
|
+
expect(Bun.stripANSI(result.stdout.toString())).toContain("Show Completed");
|
|
92
|
+
} finally {
|
|
93
|
+
await rm(dir, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("--demo-<filename> preserves an existing local copy", async () => {
|
|
98
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-demo-generic-existing-"));
|
|
99
|
+
const existing = "# Keep my selector demo\n";
|
|
100
|
+
try {
|
|
101
|
+
await writeFile(join(dir, "select.md"), existing);
|
|
102
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo-select", "-cat", "-encoding", "utf8"], {
|
|
103
|
+
cwd: dir,
|
|
104
|
+
stdout: "pipe",
|
|
105
|
+
stderr: "pipe",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(result.exitCode).toBe(0);
|
|
109
|
+
expect(await readFile(join(dir, "select.md"), "utf8")).toBe(existing);
|
|
110
|
+
expect(Bun.stripANSI(result.stdout.toString())).toContain("Keep my selector demo");
|
|
111
|
+
} finally {
|
|
112
|
+
await rm(dir, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("an unknown --demo-<filename> reports an error without creating a file", async () => {
|
|
117
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-demo-unknown-"));
|
|
118
|
+
try {
|
|
119
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo-does-not-exist", "-cat", "-encoding", "utf8"], {
|
|
120
|
+
cwd: dir,
|
|
121
|
+
stdout: "pipe",
|
|
122
|
+
stderr: "pipe",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(result.exitCode).toBe(2);
|
|
126
|
+
expect(result.stderr.toString()).toContain("Unknown demo --demo-does-not-exist");
|
|
127
|
+
expect(await Bun.file(join(dir, "does-not-exist.md")).exists()).toBe(false);
|
|
128
|
+
} finally {
|
|
129
|
+
await rm(dir, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("--demo-image-processor works through generic demo discovery", async () => {
|
|
134
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-imgtool-generic-"));
|
|
135
|
+
try {
|
|
136
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo-image-processor", "-cat", "-encoding", "utf8"], {
|
|
137
|
+
cwd: dir,
|
|
138
|
+
stdout: "pipe",
|
|
139
|
+
stderr: "pipe",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(result.exitCode).toBe(0);
|
|
143
|
+
expect(await readFile(join(dir, "image-processor.md"), "utf8")).toContain("# Bun.Image Processor");
|
|
144
|
+
} finally {
|
|
145
|
+
await rm(dir, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
59
149
|
test("--demo-imgtool writes the bundled image processor to cwd", async () => {
|
|
60
150
|
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-imgtool-"));
|
|
61
151
|
try {
|
|
62
|
-
const result = Bun.spawnSync([tui, "--demo-imgtool", "-cat", "-encoding", "utf8"], {
|
|
152
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo-imgtool", "-cat", "-encoding", "utf8"], {
|
|
63
153
|
cwd: dir,
|
|
64
154
|
stdout: "pipe",
|
|
65
155
|
stderr: "pipe",
|
|
@@ -80,7 +170,7 @@ test("--demo-imgtool preserves an existing image-processor.md", async () => {
|
|
|
80
170
|
const existing = "# Keep my image tool\n";
|
|
81
171
|
try {
|
|
82
172
|
await writeFile(join(dir, "image-processor.md"), existing);
|
|
83
|
-
const result = Bun.spawnSync([tui, "--demo-imgtool", "-cat", "-encoding", "utf8"], {
|
|
173
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo-imgtool", "-cat", "-encoding", "utf8"], {
|
|
84
174
|
cwd: dir,
|
|
85
175
|
stdout: "pipe",
|
|
86
176
|
stderr: "pipe",
|
|
@@ -97,7 +187,7 @@ test("--demo-imgtool preserves an existing image-processor.md", async () => {
|
|
|
97
187
|
test("--demo-imgtool-zh writes the bundled Traditional Chinese image processor to cwd", async () => {
|
|
98
188
|
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-imgtool-zh-"));
|
|
99
189
|
try {
|
|
100
|
-
const result = Bun.spawnSync([tui, "--demo-imgtool-zh", "-cat", "-encoding", "utf8"], {
|
|
190
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo-imgtool-zh", "-cat", "-encoding", "utf8"], {
|
|
101
191
|
cwd: dir,
|
|
102
192
|
stdout: "pipe",
|
|
103
193
|
stderr: "pipe",
|
|
@@ -118,7 +208,7 @@ test("--demo-imgtool-zh preserves an existing image-processor.zh-TW.md", async (
|
|
|
118
208
|
const existing = "# 保留我的圖片工具\n";
|
|
119
209
|
try {
|
|
120
210
|
await writeFile(join(dir, "image-processor.zh-TW.md"), existing);
|
|
121
|
-
const result = Bun.spawnSync([tui, "--demo-imgtool-zh", "-cat", "-encoding", "utf8"], {
|
|
211
|
+
const result = Bun.spawnSync([bunBin, tui, "--demo-imgtool-zh", "-cat", "-encoding", "utf8"], {
|
|
122
212
|
cwd: dir,
|
|
123
213
|
stdout: "pipe",
|
|
124
214
|
stderr: "pipe",
|