jsmdcui 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +228 -0
- package/README.md +305 -25
- package/clean.sh +9 -0
- package/demo.resized.jpg +0 -0
- package/demos/image-processor.md +361 -0
- package/demos/image-processor.zh-TW.md +361 -0
- package/demos/select.md +63 -0
- package/demos/todo-zh.md +92 -0
- package/demos/todo.md +93 -0
- package/package.json +1 -1
- package/runmd.mjs +110 -17
- package/runtime/help/help.md +305 -25
- package/single-exe/packAssets.sh +1 -1
- package/src/cui/id-collision.mjs +151 -0
- package/src/cui/kitty-debug.mjs +25 -0
- package/src/cui/kitty-images.mjs +190 -0
- package/src/cui/rpc.mjs +227 -1
- package/src/cui/server.mjs +1 -1
- package/src/cui/task-checkbox.mjs +60 -0
- package/src/index.js +297 -23
- package/src/plugins/js-bridge.js +346 -4
- package/src/screen/screen.js +108 -1
- package/tests/cat-markdown.test.js +6 -5
- package/tests/demo.test.js +203 -4
- package/tests/heading-list-selector.test.js +346 -0
- package/tests/id-collision.test.js +147 -0
- package/tests/kitty-demo.md +184 -0
- package/tests/kitty-images.test.js +147 -0
- package/tests/task-checkbox.test.js +33 -0
- package/tests/wui-responsive-images.test.js +35 -0
- package/tests/wui.test.js +30 -0
- package/tui +4 -2
- package/wui +6 -2
package/src/plugins/js-bridge.js
CHANGED
|
@@ -770,7 +770,255 @@ function _blockValue(lines, selector) {
|
|
|
770
770
|
return value.join("\n");
|
|
771
771
|
}
|
|
772
772
|
|
|
773
|
-
function
|
|
773
|
+
function _headingSelectorId(input) {
|
|
774
|
+
const match = String(input ?? "").trim().match(/^#([^\s]+)$/);
|
|
775
|
+
return match?.[1] ?? null;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function _findHeading(buffer, selector) {
|
|
779
|
+
const id = _headingSelectorId(selector);
|
|
780
|
+
const markdown = buffer?._mdcuiTuiSourceText;
|
|
781
|
+
if (!id || markdown == null || typeof Bun?.markdown?.html !== "function") return undefined;
|
|
782
|
+
|
|
783
|
+
const html = String(Bun.markdown.html(markdown, { headings: { ids: true } }));
|
|
784
|
+
const headings = /<h([1-6])\b([^>]*)>([^]*?)<\/h\1\s*>/gi;
|
|
785
|
+
let match;
|
|
786
|
+
let ordinal = 0;
|
|
787
|
+
while ((match = headings.exec(html)) !== null) {
|
|
788
|
+
const headingId = match[2].match(/\bid="([^"]*)"/i)?.[1];
|
|
789
|
+
if (headingId === id) {
|
|
790
|
+
return {
|
|
791
|
+
html: match[3],
|
|
792
|
+
level: Number(match[1]),
|
|
793
|
+
ordinal,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
ordinal++;
|
|
797
|
+
}
|
|
798
|
+
return undefined;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
let _headingAnsiPrefixes;
|
|
802
|
+
function _tuiHeadingAnsiPrefixes() {
|
|
803
|
+
if (_headingAnsiPrefixes) return _headingAnsiPrefixes;
|
|
804
|
+
const marker = "MDCUI_HEADING_LINE_PROBE";
|
|
805
|
+
_headingAnsiPrefixes = new Map();
|
|
806
|
+
for (let level = 1; level <= 6; level++) {
|
|
807
|
+
const rendered = String(Bun.markdown.ansi(
|
|
808
|
+
`${"#".repeat(level)} ${marker}`,
|
|
809
|
+
{ hyperlinks: true, columns: 80 },
|
|
810
|
+
));
|
|
811
|
+
const line = rendered.split("\n").find((item) => item.includes(marker));
|
|
812
|
+
if (line) _headingAnsiPrefixes.set(level, line.slice(0, line.indexOf(marker)));
|
|
813
|
+
}
|
|
814
|
+
return _headingAnsiPrefixes;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function _headingTuiLine(buffer, heading) {
|
|
818
|
+
const ansiText = buffer?._mdcuiAnsiText;
|
|
819
|
+
if (!heading || typeof ansiText !== "string") return 0;
|
|
820
|
+
const tuiHeadings = _tuiHeadingLines(buffer);
|
|
821
|
+
const exact = tuiHeadings[heading.ordinal];
|
|
822
|
+
if (exact?.level === heading.level) return exact.line;
|
|
823
|
+
return 0;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function _tuiHeadingLines(buffer) {
|
|
827
|
+
const ansiText = buffer?._mdcuiAnsiText;
|
|
828
|
+
if (typeof ansiText !== "string") return [];
|
|
829
|
+
const prefixes = _tuiHeadingAnsiPrefixes();
|
|
830
|
+
const tuiHeadings = [];
|
|
831
|
+
for (const [lineIndex, line] of ansiText.split("\n").entries()) {
|
|
832
|
+
for (const [level, prefix] of prefixes) {
|
|
833
|
+
if (line.startsWith(prefix)) {
|
|
834
|
+
tuiHeadings.push({ level, line: lineIndex + 1 });
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return tuiHeadings;
|
|
840
|
+
}
|
|
841
|
+
|
|
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
|
+
}
|
|
865
|
+
const headingLine = _headingTuiLine(buffer, heading);
|
|
866
|
+
if (!headingLine || !Array.isArray(buffer?.lines)) return null;
|
|
867
|
+
|
|
868
|
+
const tuiHeadings = _tuiHeadingLines(buffer);
|
|
869
|
+
const nextHeading = tuiHeadings[heading.ordinal + 1];
|
|
870
|
+
const endLine = nextHeading?.line ?? buffer.lines.length + 1;
|
|
871
|
+
let first = -1;
|
|
872
|
+
let indent = "";
|
|
873
|
+
|
|
874
|
+
for (let y = headingLine; y < endLine - 1; y++) {
|
|
875
|
+
const checkbox = String(buffer.lines[y] ?? "").match(/^(\s*)[☐☒](?:\s+|$)/);
|
|
876
|
+
if (!checkbox) continue;
|
|
877
|
+
first = y;
|
|
878
|
+
indent = checkbox[1];
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
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
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
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 = {}) {
|
|
774
1022
|
const oldCursor = buffer.cursor ? { ...buffer.cursor } : null;
|
|
775
1023
|
buffer.lines.splice(start, deleteCount, ...replacement);
|
|
776
1024
|
|
|
@@ -779,15 +1027,29 @@ function _spliceBufferLines(buffer, start, deleteCount, replacement) {
|
|
|
779
1027
|
buffer._ansiStyleLines.splice(
|
|
780
1028
|
start,
|
|
781
1029
|
deleteCount,
|
|
782
|
-
...replacement.map(() => template),
|
|
1030
|
+
...(replacementMeta.styles ?? replacement.map(() => template)),
|
|
783
1031
|
);
|
|
784
1032
|
}
|
|
785
1033
|
|
|
786
1034
|
if (typeof buffer._mdcuiAnsiText === "string") {
|
|
787
1035
|
const ansiLines = buffer._mdcuiAnsiText.split("\n");
|
|
788
|
-
ansiLines.splice(start, deleteCount, ...replacement);
|
|
1036
|
+
ansiLines.splice(start, deleteCount, ...(replacementMeta.ansi ?? replacement));
|
|
789
1037
|
buffer._mdcuiAnsiText = ansiLines.join("\n");
|
|
790
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
|
+
}
|
|
791
1053
|
|
|
792
1054
|
buffer.invalidateHighlightFrom?.(start, { force: replacement.length !== deleteCount });
|
|
793
1055
|
if (oldCursor) {
|
|
@@ -828,11 +1090,32 @@ export function createTuiSelector(getBuffer) {
|
|
|
828
1090
|
return function $(selector) {
|
|
829
1091
|
const parsedSelector = _parseBlockIdentity(selector, { selector: true });
|
|
830
1092
|
const selection = {
|
|
1093
|
+
html() {
|
|
1094
|
+
try {
|
|
1095
|
+
return _findHeading(getBuffer?.(), selector)?.html ?? "";
|
|
1096
|
+
} catch {
|
|
1097
|
+
return "";
|
|
1098
|
+
}
|
|
1099
|
+
},
|
|
1100
|
+
line() {
|
|
1101
|
+
try {
|
|
1102
|
+
const buffer = getBuffer?.();
|
|
1103
|
+
return _headingTuiLine(buffer, _findHeading(buffer, selector));
|
|
1104
|
+
} catch {
|
|
1105
|
+
return 0;
|
|
1106
|
+
}
|
|
1107
|
+
},
|
|
831
1108
|
val(...args) {
|
|
832
1109
|
try {
|
|
833
|
-
if (!parsedSelector) return args.length > 0 ? selection : "";
|
|
834
1110
|
const buffer = getBuffer?.();
|
|
835
1111
|
if (!buffer) return args.length > 0 ? selection : "";
|
|
1112
|
+
const heading = _findHeading(buffer, selector);
|
|
1113
|
+
const headingId = _headingSelectorId(selector);
|
|
1114
|
+
if (heading && headingId) {
|
|
1115
|
+
if (args.length > 0) return selection;
|
|
1116
|
+
return _headingCheckboxValue(buffer, heading, headingId);
|
|
1117
|
+
}
|
|
1118
|
+
if (!parsedSelector) return args.length > 0 ? selection : "";
|
|
836
1119
|
const lines = Array.isArray(buffer.lines)
|
|
837
1120
|
? buffer.lines
|
|
838
1121
|
: String(buffer).replace(/\r\n?/g, "\n").split("\n");
|
|
@@ -846,6 +1129,65 @@ export function createTuiSelector(getBuffer) {
|
|
|
846
1129
|
return args.length > 0 ? selection : "";
|
|
847
1130
|
}
|
|
848
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
|
+
},
|
|
849
1191
|
};
|
|
850
1192
|
return selection;
|
|
851
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",
|