import { ANSI_BUFFERS, ANSI_DYNAMIC, writeAnsi } from '../util/ansi'; import { tryCatch } from '@maxmorozoff/try-catch-tuple'; type CanvasAPI = { clear: () => Promise; moveToOrigin: () => Promise; availableHeight: number; availableWidth: number; }; type CleanupHandler = (api: { clear: () => Promise; endPosition: { x: number; y: number }; }) => Promise; export function createCanvas( paint: (api: CanvasAPI) => Promise, 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) { // 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 }; }