jsmdcui 0.6.3 → 0.8.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 +121 -0
- package/README.md +216 -33
- 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/demos/maze.md +144 -0
- package/{select.md → demos/select.md} +2 -0
- package/demos/todo-zh.md +190 -0
- package/demos/todo.md +191 -0
- package/edit +9 -0
- package/package.json +1 -1
- package/runmd.mjs +77 -16
- package/runtime/help/help.md +216 -33
- package/runtime/jsplugins/cdp/cdp.js +5 -2
- package/runtime/jsplugins/chapter/chapter.js +4 -2
- package/runtime/jsplugins/example/example.js +10 -7
- package/runtime/syntax/markdown.yaml +1 -0
- package/single-exe/packAssets.sh +1 -1
- package/src/cui/fence-events.mjs +106 -0
- package/src/cui/id-collision.mjs +10 -28
- package/src/cui/kitty-debug.mjs +25 -0
- package/src/cui/kitty-images.mjs +200 -0
- package/src/cui/rpc.mjs +169 -10
- package/src/cui/server.mjs +14 -3
- package/src/cui/task-checkbox.mjs +44 -0
- package/src/index.js +615 -101
- package/src/platform/terminal.js +5 -0
- package/src/plugins/js-bridge.js +354 -21
- package/src/screen/screen.js +108 -1
- package/tests/cat-markdown.test.js +6 -5
- package/tests/demo.test.js +99 -9
- package/tests/fence-events.test.js +405 -0
- package/tests/heading-list-selector.test.js +346 -0
- package/tests/id-collision.test.js +14 -2
- package/tests/js-plugin-prompts.test.js +46 -0
- package/tests/key-event.md +10 -0
- package/tests/kitty-demo.md +184 -0
- package/tests/kitty-images.test.js +169 -0
- package/tests/platform-terminal.test.js +8 -0
- package/tests/task-checkbox.test.js +33 -0
- package/tests/textarea.md +8 -0
- package/tests/wui-responsive-images.test.js +35 -0
- package/tests/wui.test.js +16 -1
- package/tui +4 -2
- package/wui +6 -2
package/src/plugins/js-bridge.js
CHANGED
|
@@ -752,6 +752,56 @@ function _findBlock(lines, selector) {
|
|
|
752
752
|
return null;
|
|
753
753
|
}
|
|
754
754
|
|
|
755
|
+
export function findTuiBlockAtLine(lines, lineIndex) {
|
|
756
|
+
return findTuiBlockInIndex(buildTuiBlockIndex(lines), lineIndex);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
export function buildTuiBlockIndex(lines, declarations = null) {
|
|
760
|
+
const blocks = [];
|
|
761
|
+
for (let start = 0; start < lines.length; start++) {
|
|
762
|
+
const header = _blockHeader(lines[start]);
|
|
763
|
+
if (!header) continue;
|
|
764
|
+
let end = lines.length;
|
|
765
|
+
for (let y = start + 1; y < lines.length; y++) {
|
|
766
|
+
const line = String(lines[y] ?? "");
|
|
767
|
+
const rest = line.startsWith(header.indent)
|
|
768
|
+
? line.slice(header.indent.length)
|
|
769
|
+
: line;
|
|
770
|
+
if (header.kind === "fenced") {
|
|
771
|
+
const closing = rest.match(/^(`{3,})\s*$/);
|
|
772
|
+
if (!closing || closing[1].length < header.fenceLength) continue;
|
|
773
|
+
} else if (!/^(?:└─|╰─|\+-)\s*$/.test(rest)) {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
end = y;
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
const declaration = header.id ? declarations?.get(header.id) : null;
|
|
780
|
+
if (
|
|
781
|
+
declarations == null
|
|
782
|
+
|| (
|
|
783
|
+
declaration?.tag === header.tag
|
|
784
|
+
&& declaration.events?.size > 0
|
|
785
|
+
)
|
|
786
|
+
) blocks.push({ start, end, header });
|
|
787
|
+
start = Math.max(start, end);
|
|
788
|
+
}
|
|
789
|
+
return blocks;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export function findTuiBlockInIndex(blocks, lineIndex) {
|
|
793
|
+
let low = 0;
|
|
794
|
+
let high = blocks.length - 1;
|
|
795
|
+
while (low <= high) {
|
|
796
|
+
const middle = (low + high) >> 1;
|
|
797
|
+
const block = blocks[middle];
|
|
798
|
+
if (lineIndex <= block.start) high = middle - 1;
|
|
799
|
+
else if (lineIndex >= block.end) low = middle + 1;
|
|
800
|
+
else return block;
|
|
801
|
+
}
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
|
|
755
805
|
function _blockValue(lines, selector) {
|
|
756
806
|
const block = _findBlock(lines, selector);
|
|
757
807
|
if (!block) return undefined;
|
|
@@ -840,32 +890,188 @@ function _tuiHeadingLines(buffer) {
|
|
|
840
890
|
}
|
|
841
891
|
|
|
842
892
|
function _headingCheckboxValue(buffer, heading, id) {
|
|
893
|
+
const list = _tuiHeadingTaskList(buffer, heading);
|
|
894
|
+
if (!list) return id.startsWith("select") ? null : [];
|
|
895
|
+
const selected = [];
|
|
896
|
+
for (const item of list.items) {
|
|
897
|
+
const checkbox = String(buffer.lines[item.start] ?? "").match(/^(\s*)([☐☒])(?:\s+|$)(.*)$/);
|
|
898
|
+
if (checkbox?.[2] !== "☒") continue;
|
|
899
|
+
if (id.startsWith("select")) return item.value;
|
|
900
|
+
selected.push(item.value);
|
|
901
|
+
}
|
|
902
|
+
return id.startsWith("select") ? null : selected;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function _tuiHeadingTaskList(buffer, heading) {
|
|
906
|
+
const savedAnchor = buffer?._mdcuiHeadingTaskListAnchors?.get(heading.ordinal);
|
|
907
|
+
if (savedAnchor) {
|
|
908
|
+
return {
|
|
909
|
+
first: savedAnchor.index,
|
|
910
|
+
end: savedAnchor.index,
|
|
911
|
+
indent: savedAnchor.indent,
|
|
912
|
+
items: [],
|
|
913
|
+
};
|
|
914
|
+
}
|
|
843
915
|
const headingLine = _headingTuiLine(buffer, heading);
|
|
844
|
-
if (!headingLine || !Array.isArray(buffer?.lines))
|
|
845
|
-
return id.startsWith("select") ? null : [];
|
|
916
|
+
if (!headingLine || !Array.isArray(buffer?.lines)) return null;
|
|
846
917
|
|
|
847
918
|
const tuiHeadings = _tuiHeadingLines(buffer);
|
|
848
919
|
const nextHeading = tuiHeadings[heading.ordinal + 1];
|
|
849
920
|
const endLine = nextHeading?.line ?? buffer.lines.length + 1;
|
|
921
|
+
let first = -1;
|
|
922
|
+
let indent = "";
|
|
850
923
|
|
|
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+|$)(.*)$/);
|
|
924
|
+
for (let y = headingLine; y < endLine - 1; y++) {
|
|
925
|
+
const checkbox = String(buffer.lines[y] ?? "").match(/^(\s*)[☐☒](?:\s+|$)/);
|
|
856
926
|
if (!checkbox) continue;
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
const value = checkbox[3].trim();
|
|
861
|
-
if (id.startsWith("select")) return value;
|
|
862
|
-
selected.push(value);
|
|
927
|
+
first = y;
|
|
928
|
+
indent = checkbox[1];
|
|
929
|
+
break;
|
|
863
930
|
}
|
|
864
|
-
|
|
931
|
+
if (first < 0) return null;
|
|
932
|
+
|
|
933
|
+
let end = endLine - 1;
|
|
934
|
+
let pendingBlank = -1;
|
|
935
|
+
for (let y = first + 1; y < endLine - 1; y++) {
|
|
936
|
+
const line = String(buffer.lines[y] ?? "");
|
|
937
|
+
if (line.trim() === "") {
|
|
938
|
+
if (pendingBlank < 0) pendingBlank = y;
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
const leading = line.match(/^\s*/)?.[0].length ?? 0;
|
|
942
|
+
const checkbox = line.match(/^(\s*)[☐☒](?:\s+|$)/);
|
|
943
|
+
if (checkbox || leading > indent.length) {
|
|
944
|
+
pendingBlank = -1;
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
end = pendingBlank >= 0 ? pendingBlank : y;
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
950
|
+
if (end === endLine - 1 && pendingBlank >= 0) end = pendingBlank;
|
|
951
|
+
|
|
952
|
+
const items = [];
|
|
953
|
+
for (let y = first; y < end; y++) {
|
|
954
|
+
const checkbox = String(buffer.lines[y] ?? "").match(/^(\s*)([☐☒])(?:\s+|$)(.*)$/);
|
|
955
|
+
if (checkbox && checkbox[1] === indent) {
|
|
956
|
+
items.push({
|
|
957
|
+
start: y,
|
|
958
|
+
end,
|
|
959
|
+
value: checkbox[3].trim(),
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
for (let index = 0; index < items.length - 1; index++)
|
|
964
|
+
items[index].end = items[index + 1].start;
|
|
965
|
+
|
|
966
|
+
return { first, end, indent, items };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function _normalizedSpliceRange(length, argumentCount, start, deleteCount) {
|
|
970
|
+
if (argumentCount === 0) return { start: 0, deleteCount: 0 };
|
|
971
|
+
let relativeStart = Number(start);
|
|
972
|
+
if (Number.isNaN(relativeStart)) relativeStart = 0;
|
|
973
|
+
relativeStart = Math.trunc(relativeStart);
|
|
974
|
+
const actualStart = relativeStart < 0
|
|
975
|
+
? Math.max(length + relativeStart, 0)
|
|
976
|
+
: Math.min(relativeStart, length);
|
|
977
|
+
if (argumentCount === 1) return { start: actualStart, deleteCount: length - actualStart };
|
|
978
|
+
let requestedDelete = Number(deleteCount);
|
|
979
|
+
if (Number.isNaN(requestedDelete)) requestedDelete = 0;
|
|
980
|
+
requestedDelete = Math.max(0, Math.trunc(requestedDelete));
|
|
981
|
+
return {
|
|
982
|
+
start: actualStart,
|
|
983
|
+
deleteCount: Math.min(requestedDelete, length - actualStart),
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function _tuiTaskItemReplacement(indent, inputs) {
|
|
988
|
+
const normalized = inputs.map((input) => {
|
|
989
|
+
const item = input && typeof input === "object"
|
|
990
|
+
? { value: input.value ?? input.label ?? "", checked: Boolean(input.checked) }
|
|
991
|
+
: { value: input ?? "", checked: false };
|
|
992
|
+
return {
|
|
993
|
+
value: String(item.value).replace(/\r?\n/g, " "),
|
|
994
|
+
checked: item.checked,
|
|
995
|
+
};
|
|
996
|
+
});
|
|
997
|
+
return {
|
|
998
|
+
lines: normalized.map((item) =>
|
|
999
|
+
`${indent}${item.checked ? "☒" : "☐"} ${item.value}`,
|
|
1000
|
+
),
|
|
1001
|
+
styles: normalized.map((item) => {
|
|
1002
|
+
const styles = [];
|
|
1003
|
+
if (item.checked) {
|
|
1004
|
+
styles[indent.length] = { fg: "green" };
|
|
1005
|
+
styles[indent.length + 1] = { fg: "green" };
|
|
1006
|
+
}
|
|
1007
|
+
return styles;
|
|
1008
|
+
}),
|
|
1009
|
+
ansi: normalized.map((item) =>
|
|
1010
|
+
`${indent}\x1b[${item.checked ? "32" : "2"}m${item.checked ? "☒" : "☐"} \x1b[0m${item.value}`,
|
|
1011
|
+
),
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function _mutateTuiHeadingList(buffer, heading, method, args) {
|
|
1016
|
+
const list = _tuiHeadingTaskList(buffer, heading);
|
|
1017
|
+
if (!list) {
|
|
1018
|
+
if (method === "push" || method === "unshift") return 0;
|
|
1019
|
+
if (method === "splice") return [];
|
|
1020
|
+
return undefined;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (method === "splice") {
|
|
1024
|
+
const range = _normalizedSpliceRange(list.items.length, args.length, args[0], args[1]);
|
|
1025
|
+
const removedItems = list.items.slice(range.start, range.start + range.deleteCount);
|
|
1026
|
+
const removed = removedItems.map((item) => item.value);
|
|
1027
|
+
const replacement = _tuiTaskItemReplacement(list.indent, args.slice(2));
|
|
1028
|
+
if (range.deleteCount === 0 && replacement.lines.length === 0) return [];
|
|
1029
|
+
const start = list.items[range.start]?.start ?? list.end;
|
|
1030
|
+
const end = removedItems.at(-1)?.end ?? start;
|
|
1031
|
+
if (range.deleteCount === list.items.length && replacement.lines.length === 0 && list.items.length > 0) {
|
|
1032
|
+
if (!(buffer._mdcuiHeadingTaskListAnchors instanceof Map))
|
|
1033
|
+
buffer._mdcuiHeadingTaskListAnchors = new Map();
|
|
1034
|
+
buffer._mdcuiHeadingTaskListAnchors.set(heading.ordinal, { index: start, indent: list.indent });
|
|
1035
|
+
}
|
|
1036
|
+
spliceTuiBufferLines(buffer, start, end - start, replacement.lines, {
|
|
1037
|
+
styles: replacement.styles,
|
|
1038
|
+
ansi: replacement.ansi,
|
|
1039
|
+
});
|
|
1040
|
+
if (replacement.lines.length > 0)
|
|
1041
|
+
buffer._mdcuiHeadingTaskListAnchors?.delete(heading.ordinal);
|
|
1042
|
+
return removed;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (method === "pop" || method === "shift") {
|
|
1046
|
+
const item = method === "pop" ? list.items.at(-1) : list.items[0];
|
|
1047
|
+
if (!item) return undefined;
|
|
1048
|
+
if (list.items.length === 1) {
|
|
1049
|
+
if (!(buffer._mdcuiHeadingTaskListAnchors instanceof Map))
|
|
1050
|
+
buffer._mdcuiHeadingTaskListAnchors = new Map();
|
|
1051
|
+
buffer._mdcuiHeadingTaskListAnchors.set(heading.ordinal, {
|
|
1052
|
+
index: item.start,
|
|
1053
|
+
indent: list.indent,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
spliceTuiBufferLines(buffer, item.start, item.end - item.start, []);
|
|
1057
|
+
return item.value;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const replacement = _tuiTaskItemReplacement(list.indent, args);
|
|
1061
|
+
if (replacement.lines.length === 0) return list.items.length;
|
|
1062
|
+
const start = method === "push" ? list.end : list.first;
|
|
1063
|
+
spliceTuiBufferLines(buffer, start, 0, replacement.lines, {
|
|
1064
|
+
styles: replacement.styles,
|
|
1065
|
+
ansi: replacement.ansi,
|
|
1066
|
+
});
|
|
1067
|
+
if (replacement.lines.length > 0) buffer._mdcuiHeadingTaskListAnchors?.delete(heading.ordinal);
|
|
1068
|
+
return list.items.length + replacement.lines.length;
|
|
865
1069
|
}
|
|
866
1070
|
|
|
867
|
-
function
|
|
1071
|
+
export function spliceTuiBufferLines(buffer, start, deleteCount, replacement, replacementMeta = {}) {
|
|
868
1072
|
const oldCursor = buffer.cursor ? { ...buffer.cursor } : null;
|
|
1073
|
+
buffer._mdcuiFenceBlockIndex = null;
|
|
1074
|
+
buffer._mdcuiControlBlockIndex = null;
|
|
869
1075
|
buffer.lines.splice(start, deleteCount, ...replacement);
|
|
870
1076
|
|
|
871
1077
|
if (Array.isArray(buffer._ansiStyleLines)) {
|
|
@@ -873,15 +1079,29 @@ function _spliceBufferLines(buffer, start, deleteCount, replacement) {
|
|
|
873
1079
|
buffer._ansiStyleLines.splice(
|
|
874
1080
|
start,
|
|
875
1081
|
deleteCount,
|
|
876
|
-
...replacement.map(() => template),
|
|
1082
|
+
...(replacementMeta.styles ?? replacement.map(() => template)),
|
|
877
1083
|
);
|
|
878
1084
|
}
|
|
879
1085
|
|
|
880
1086
|
if (typeof buffer._mdcuiAnsiText === "string") {
|
|
881
1087
|
const ansiLines = buffer._mdcuiAnsiText.split("\n");
|
|
882
|
-
ansiLines.splice(start, deleteCount, ...replacement);
|
|
1088
|
+
ansiLines.splice(start, deleteCount, ...(replacementMeta.ansi ?? replacement));
|
|
883
1089
|
buffer._mdcuiAnsiText = ansiLines.join("\n");
|
|
884
1090
|
}
|
|
1091
|
+
if (Array.isArray(buffer._mdcuiImages)) {
|
|
1092
|
+
const delta = replacement.length - deleteCount;
|
|
1093
|
+
buffer._mdcuiImages = buffer._mdcuiImages
|
|
1094
|
+
.filter((image) => image.line < start || image.line >= start + deleteCount)
|
|
1095
|
+
.map((image) => image.line >= start + deleteCount ? { ...image, line: image.line + delta } : image);
|
|
1096
|
+
}
|
|
1097
|
+
if (buffer._mdcuiHeadingTaskListAnchors instanceof Map) {
|
|
1098
|
+
const oldEnd = start + deleteCount;
|
|
1099
|
+
const delta = replacement.length - deleteCount;
|
|
1100
|
+
for (const anchor of buffer._mdcuiHeadingTaskListAnchors.values()) {
|
|
1101
|
+
if (anchor.index >= oldEnd) anchor.index += delta;
|
|
1102
|
+
else if (anchor.index >= start) anchor.index = start;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
885
1105
|
|
|
886
1106
|
buffer.invalidateHighlightFrom?.(start, { force: replacement.length !== deleteCount });
|
|
887
1107
|
if (oldCursor) {
|
|
@@ -897,24 +1117,70 @@ function _spliceBufferLines(buffer, start, deleteCount, replacement) {
|
|
|
897
1117
|
buffer.ensureCursor?.();
|
|
898
1118
|
}
|
|
899
1119
|
|
|
1120
|
+
export function insertTuiTextareaNewline(buffer, block) {
|
|
1121
|
+
if (!buffer || block?.header?.tag !== "textarea") return false;
|
|
1122
|
+
const prefix = block.header.indent + block.header.bodyMarker + " ";
|
|
1123
|
+
const line = String(buffer.lines?.[buffer.cursor.y] ?? "");
|
|
1124
|
+
const left = line.slice(0, buffer.cursor.x);
|
|
1125
|
+
const right = line.slice(buffer.cursor.x);
|
|
1126
|
+
buffer.lines[buffer.cursor.y] = left;
|
|
1127
|
+
spliceTuiBufferLines(buffer, buffer.cursor.y + 1, 0, [prefix + right]);
|
|
1128
|
+
buffer.cursor.y++;
|
|
1129
|
+
buffer.cursor.x = prefix.length;
|
|
1130
|
+
buffer.ensureCursor?.();
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
export function mergeTuiTextareaBackward(buffer, block) {
|
|
1135
|
+
if (!buffer || block?.header?.tag !== "textarea") return false;
|
|
1136
|
+
const prefix = block.header.indent + block.header.bodyMarker + " ";
|
|
1137
|
+
if (buffer.cursor.x > prefix.length || buffer.cursor.y <= block.start + 1) return false;
|
|
1138
|
+
const previousY = buffer.cursor.y - 1;
|
|
1139
|
+
const previousLength = String(buffer.lines?.[previousY] ?? "").length;
|
|
1140
|
+
const line = String(buffer.lines?.[buffer.cursor.y] ?? "");
|
|
1141
|
+
const body = line.startsWith(prefix) ? line.slice(prefix.length) : line;
|
|
1142
|
+
buffer.lines[previousY] += body;
|
|
1143
|
+
spliceTuiBufferLines(buffer, buffer.cursor.y, 1, []);
|
|
1144
|
+
buffer.cursor.y = previousY;
|
|
1145
|
+
buffer.cursor.x = previousLength;
|
|
1146
|
+
buffer.ensureCursor?.();
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
export function mergeTuiTextareaForward(buffer, block) {
|
|
1151
|
+
if (!buffer || block?.header?.tag !== "textarea") return false;
|
|
1152
|
+
const line = String(buffer.lines?.[buffer.cursor.y] ?? "");
|
|
1153
|
+
if (buffer.cursor.x < line.length || buffer.cursor.y >= block.end - 1) return false;
|
|
1154
|
+
const prefix = block.header.indent + block.header.bodyMarker + " ";
|
|
1155
|
+
const nextLine = String(buffer.lines?.[buffer.cursor.y + 1] ?? "");
|
|
1156
|
+
const body = nextLine.startsWith(prefix) ? nextLine.slice(prefix.length) : nextLine;
|
|
1157
|
+
buffer.lines[buffer.cursor.y] += body;
|
|
1158
|
+
spliceTuiBufferLines(buffer, buffer.cursor.y + 1, 1, []);
|
|
1159
|
+
buffer.ensureCursor?.();
|
|
1160
|
+
return true;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
900
1163
|
function _setBlockValue(buffer, selector, value) {
|
|
901
1164
|
const lines = buffer.lines;
|
|
902
1165
|
const block = _findBlock(lines, selector);
|
|
903
1166
|
if (!block) return false;
|
|
904
1167
|
|
|
905
|
-
const
|
|
1168
|
+
const normalizedValue = String(value ?? "").replace(/\r\n?/g, "\n");
|
|
1169
|
+
if ((_blockValue(lines, selector) ?? "") === normalizedValue) return true;
|
|
1170
|
+
buffer.pushUndo?.(true);
|
|
1171
|
+
const values = normalizedValue.split("\n");
|
|
906
1172
|
const contentStart = block.start + 1;
|
|
907
1173
|
const capacity = Math.max(0, block.end - contentStart);
|
|
908
1174
|
|
|
909
1175
|
if (block.header.kind === "fenced") {
|
|
910
1176
|
const replacement = values.map((line) => block.header.indent + line);
|
|
911
|
-
|
|
1177
|
+
spliceTuiBufferLines(buffer, contentStart, capacity, replacement);
|
|
912
1178
|
return true;
|
|
913
1179
|
}
|
|
914
1180
|
|
|
915
1181
|
const rowPrefix = block.header.indent + block.header.bodyMarker + " ";
|
|
916
1182
|
const replacement = values.map((line) => rowPrefix + line);
|
|
917
|
-
|
|
1183
|
+
spliceTuiBufferLines(buffer, contentStart, capacity, replacement);
|
|
918
1184
|
return true;
|
|
919
1185
|
}
|
|
920
1186
|
|
|
@@ -961,6 +1227,65 @@ export function createTuiSelector(getBuffer) {
|
|
|
961
1227
|
return args.length > 0 ? selection : "";
|
|
962
1228
|
}
|
|
963
1229
|
},
|
|
1230
|
+
push(...items) {
|
|
1231
|
+
try {
|
|
1232
|
+
const buffer = getBuffer?.();
|
|
1233
|
+
const heading = _findHeading(buffer, selector);
|
|
1234
|
+
return heading ? _mutateTuiHeadingList(buffer, heading, "push", items) : 0;
|
|
1235
|
+
} catch {
|
|
1236
|
+
return 0;
|
|
1237
|
+
}
|
|
1238
|
+
},
|
|
1239
|
+
pop() {
|
|
1240
|
+
try {
|
|
1241
|
+
const buffer = getBuffer?.();
|
|
1242
|
+
const heading = _findHeading(buffer, selector);
|
|
1243
|
+
return heading ? _mutateTuiHeadingList(buffer, heading, "pop", []) : undefined;
|
|
1244
|
+
} catch {
|
|
1245
|
+
return undefined;
|
|
1246
|
+
}
|
|
1247
|
+
},
|
|
1248
|
+
shift() {
|
|
1249
|
+
try {
|
|
1250
|
+
const buffer = getBuffer?.();
|
|
1251
|
+
const heading = _findHeading(buffer, selector);
|
|
1252
|
+
return heading ? _mutateTuiHeadingList(buffer, heading, "shift", []) : undefined;
|
|
1253
|
+
} catch {
|
|
1254
|
+
return undefined;
|
|
1255
|
+
}
|
|
1256
|
+
},
|
|
1257
|
+
unshift(...items) {
|
|
1258
|
+
try {
|
|
1259
|
+
const buffer = getBuffer?.();
|
|
1260
|
+
const heading = _findHeading(buffer, selector);
|
|
1261
|
+
return heading ? _mutateTuiHeadingList(buffer, heading, "unshift", items) : 0;
|
|
1262
|
+
} catch {
|
|
1263
|
+
return 0;
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1266
|
+
splice(...args) {
|
|
1267
|
+
try {
|
|
1268
|
+
const buffer = getBuffer?.();
|
|
1269
|
+
const heading = _findHeading(buffer, selector);
|
|
1270
|
+
return heading ? _mutateTuiHeadingList(buffer, heading, "splice", args) : [];
|
|
1271
|
+
} catch {
|
|
1272
|
+
return [];
|
|
1273
|
+
}
|
|
1274
|
+
},
|
|
1275
|
+
slice(...args) {
|
|
1276
|
+
try {
|
|
1277
|
+
const buffer = getBuffer?.();
|
|
1278
|
+
const heading = _findHeading(buffer, selector);
|
|
1279
|
+
const list = heading ? _tuiHeadingTaskList(buffer, heading) : null;
|
|
1280
|
+
if (!list) return [];
|
|
1281
|
+
return list.items.map((item) => ({
|
|
1282
|
+
value: item.value,
|
|
1283
|
+
checked: String(buffer.lines[item.start] ?? "").match(/^(\s*)([☐☒])(?:\s+|$)/)?.[2] === "☒",
|
|
1284
|
+
})).slice(...args);
|
|
1285
|
+
} catch {
|
|
1286
|
+
return [];
|
|
1287
|
+
}
|
|
1288
|
+
},
|
|
964
1289
|
};
|
|
965
1290
|
return selection;
|
|
966
1291
|
};
|
|
@@ -1002,7 +1327,12 @@ export function buildMicroGlobal(jsManager) {
|
|
|
1002
1327
|
// ── Messaging ─────────────────────────────────────────────────
|
|
1003
1328
|
Log: (...args) => console.log(...args),
|
|
1004
1329
|
TermMessage: (msg) => { const app = getApp(); if (app) { app.message = String(msg); if (app._started) app.render?.(); } },
|
|
1005
|
-
alert:
|
|
1330
|
+
alert: (msg) => { const app = getApp(); return app ? app.protectedAlert(msg) : console.log(String(msg)); },
|
|
1331
|
+
confirm: (msg) => { const app = getApp(); return app ? app.protectedConfirm(msg) : false; },
|
|
1332
|
+
prompt: (msg, defaultValue = "") => {
|
|
1333
|
+
const app = getApp();
|
|
1334
|
+
return app ? app.protectedPrompt(msg, defaultValue) : defaultValue;
|
|
1335
|
+
},
|
|
1006
1336
|
|
|
1007
1337
|
// ── Buffer line access (1-based line numbers; omit → cursor line) ─
|
|
1008
1338
|
|
|
@@ -1023,6 +1353,7 @@ export function buildMicroGlobal(jsManager) {
|
|
|
1023
1353
|
const y = lineNumber != null ? Number(lineNumber) - 1 : buf.cursor.y;
|
|
1024
1354
|
if (y < 0 || y >= buf.lines.length) return;
|
|
1025
1355
|
buf.pushUndo?.();
|
|
1356
|
+
buf._mdcuiFenceBlockIndex = null;
|
|
1026
1357
|
const parts = String(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
1027
1358
|
buf.lines.splice(y, 1, ...parts);
|
|
1028
1359
|
buf.invalidateHighlightFrom?.(y, { force: parts.length > 1 });
|
|
@@ -1039,6 +1370,7 @@ export function buildMicroGlobal(jsManager) {
|
|
|
1039
1370
|
const y = lineNumber != null ? Number(lineNumber) - 1 : buf.cursor.y;
|
|
1040
1371
|
if (y < 0 || y >= buf.lines.length) return;
|
|
1041
1372
|
buf.pushUndo?.();
|
|
1373
|
+
buf._mdcuiFenceBlockIndex = null;
|
|
1042
1374
|
if (buf.lines.length === 1) {
|
|
1043
1375
|
buf.lines[0] = "";
|
|
1044
1376
|
} else {
|
|
@@ -1079,6 +1411,7 @@ export function buildMicroGlobal(jsManager) {
|
|
|
1079
1411
|
if (!app?.buffer) return;
|
|
1080
1412
|
const buf = app.buffer;
|
|
1081
1413
|
buf.pushUndo?.();
|
|
1414
|
+
buf._mdcuiFenceBlockIndex = null;
|
|
1082
1415
|
buf.lines = String(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
1083
1416
|
if (buf.lines.length === 0) buf.lines = [""];
|
|
1084
1417
|
buf.invalidateHighlightFrom?.(0, { force: true });
|
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",
|