126 lines
3.2 KiB
TypeScript
126 lines
3.2 KiB
TypeScript
import { ANSI_BUFFERS, ANSI_DYNAMIC, writeAnsi } from '../util/ansi';
|
|
import { tryCatch } from '@maxmorozoff/try-catch-tuple';
|
|
|
|
type CanvasAPI = {
|
|
clear: () => Promise<void>;
|
|
moveToOrigin: () => Promise<void>;
|
|
availableHeight: number;
|
|
availableWidth: number;
|
|
};
|
|
|
|
type CleanupHandler = (api: {
|
|
clear: () => Promise<void>;
|
|
endPosition: { x: number; y: number };
|
|
}) => Promise<void>;
|
|
|
|
export function createCanvas(
|
|
paint: (api: CanvasAPI) => Promise<void>,
|
|
cleanup?: CleanupHandler
|
|
) {
|
|
let isActive = true;
|
|
let origin = { x: 0, y: 0 };
|
|
let currentEnd = { x: 0, y: 0 };
|
|
|
|
const canvasAPI: CanvasAPI = {
|
|
clear: async () => {
|
|
await writeAnsi([
|
|
ANSI_BUFFERS.SAVE_CURSOR,
|
|
ANSI_DYNAMIC.CURSOR_TO(origin.x, origin.y),
|
|
]);
|
|
|
|
// Clear each line in the canvas area
|
|
for (let line = 0; line <= currentEnd.y - origin.y; line++) {
|
|
await writeAnsi([
|
|
ANSI_BUFFERS.CLEAR_LINE,
|
|
...(line < currentEnd.y - origin.y
|
|
? [ANSI_DYNAMIC.CURSOR_DOWN(1)]
|
|
: []),
|
|
]);
|
|
}
|
|
|
|
await writeAnsi([ANSI_BUFFERS.RESTORE_CURSOR]);
|
|
},
|
|
|
|
moveToOrigin: async () => {
|
|
await writeAnsi([ANSI_DYNAMIC.CURSOR_TO(origin.x, origin.y)]);
|
|
},
|
|
|
|
get availableHeight() {
|
|
return currentEnd.y - origin.y;
|
|
},
|
|
|
|
get availableWidth() {
|
|
return currentEnd.x - origin.x;
|
|
},
|
|
};
|
|
|
|
async function trackPaintArea(fn: () => Promise<void>) {
|
|
// Save initial cursor position
|
|
const [_, saveError] = await writeAnsi([ANSI_BUFFERS.SAVE_CURSOR]);
|
|
if (saveError) throw saveError;
|
|
|
|
const [initialPos, initialPosError] = await tryCatch(getCursorPosition);
|
|
if (initialPosError) {
|
|
await writeAnsi([ANSI_BUFFERS.RESTORE_CURSOR]);
|
|
throw initialPosError;
|
|
}
|
|
if (initialPos) origin = initialPos;
|
|
|
|
// Execute painting
|
|
await fn();
|
|
|
|
// Record new end position
|
|
const [endPos, endPosError] = await tryCatch(getCursorPosition);
|
|
if (endPosError) {
|
|
await writeAnsi([ANSI_BUFFERS.RESTORE_CURSOR]);
|
|
throw endPosError;
|
|
}
|
|
if (endPos) currentEnd = endPos;
|
|
|
|
// Restore original cursor
|
|
await writeAnsi([ANSI_BUFFERS.RESTORE_CURSOR]);
|
|
}
|
|
|
|
return {
|
|
async paint() {
|
|
if (!isActive) return;
|
|
await trackPaintArea(async () => {
|
|
await canvasAPI.moveToOrigin();
|
|
await paint(canvasAPI);
|
|
});
|
|
},
|
|
|
|
async destroy() {
|
|
if (!isActive) return;
|
|
isActive = false;
|
|
|
|
if (cleanup) {
|
|
await cleanup({
|
|
clear: canvasAPI.clear,
|
|
endPosition: currentEnd,
|
|
});
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
async function getCursorPosition(): Promise<{ x: number; y: number }> {
|
|
const [pos, posError] = await tryCatch(async () => {
|
|
await Bun.write(Bun.stdout, ANSI_BUFFERS.GET_CURSOR_POSITION);
|
|
|
|
let response = '';
|
|
const decoder = new TextDecoder();
|
|
for await (const chunk of Bun.stdin.stream()) {
|
|
response += decoder.decode(chunk);
|
|
if (response.includes('R')) break;
|
|
}
|
|
|
|
const [_, y, x] = response.match(/\[(\d+);(\d+)R/) || [];
|
|
if (!x) throw TypeError("The 'x' position was unset.");
|
|
if (!y) throw TypeError("The 'y' position was unset.");
|
|
return { x: parseInt(x), y: parseInt(y) };
|
|
});
|
|
|
|
return pos || { x: 0, y: 0 };
|
|
}
|