Add preliminary canvas module (doesn't work)

This commit is contained in:
Eric Rumsey 2025-05-19 15:31:58 -05:00
parent 9ea2787ba2
commit cedbab4989
2 changed files with 131 additions and 0 deletions

View File

@ -0,0 +1,125 @@
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 };
}

View File

@ -0,0 +1,6 @@
export const createCanvas = (
paint: () => Promise<number, Error>,
cleanup?: () => Promise<number, Error>
) => {
return [1];
};