Compare commits
No commits in common. "cedbab498941eacb102c01fbd4abe5f3f056cb24" and "bcdbf8f7c6d85a35bda076d6726e1c0f204937f1" have entirely different histories.
cedbab4989
...
bcdbf8f7c6
@ -1,125 +0,0 @@
|
|||||||
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 };
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
export const createCanvas = (
|
|
||||||
paint: () => Promise<number, Error>,
|
|
||||||
cleanup?: () => Promise<number, Error>
|
|
||||||
) => {
|
|
||||||
return [1];
|
|
||||||
};
|
|
||||||
@ -34,7 +34,6 @@ export const ANSI = {
|
|||||||
BG_WHITE: '\x1B[47m',
|
BG_WHITE: '\x1B[47m',
|
||||||
|
|
||||||
// Cursor Control
|
// Cursor Control
|
||||||
GET_CURSOR_POSITION: '\x1B[6n',
|
|
||||||
SAVE_CURSOR: '\x1B[s',
|
SAVE_CURSOR: '\x1B[s',
|
||||||
RESTORE_CURSOR: '\x1B[u',
|
RESTORE_CURSOR: '\x1B[u',
|
||||||
CURSOR_TO: (x: number, y: number) => `\x1B[${y};${x}H`,
|
CURSOR_TO: (x: number, y: number) => `\x1B[${y};${x}H`,
|
||||||
@ -48,8 +47,6 @@ export const ANSI = {
|
|||||||
CLEAR_LINE: '\x1B[2K',
|
CLEAR_LINE: '\x1B[2K',
|
||||||
CLEAR_END_LINE: '\x1B[0K',
|
CLEAR_END_LINE: '\x1B[0K',
|
||||||
CLEAR_START_LINE: '\x1B[1K',
|
CLEAR_START_LINE: '\x1B[1K',
|
||||||
CLEAR_TO_END: '\x1B[0J', // From cursor to end of screen
|
|
||||||
CLEAR_TO_START: '\x1B[1J', // From cursor to beginning of screen
|
|
||||||
CLEAR_BELOW: '\x1B[J',
|
CLEAR_BELOW: '\x1B[J',
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -85,31 +82,17 @@ export const ANSI_BUFFERS = {
|
|||||||
BG_WHITE: encoder.encode(ANSI.BG_WHITE),
|
BG_WHITE: encoder.encode(ANSI.BG_WHITE),
|
||||||
|
|
||||||
// Cursor Control
|
// Cursor Control
|
||||||
GET_CURSOR_POSITION: encoder.encode(ANSI.GET_CURSOR_POSITION),
|
|
||||||
SAVE_CURSOR: encoder.encode(ANSI.SAVE_CURSOR),
|
SAVE_CURSOR: encoder.encode(ANSI.SAVE_CURSOR),
|
||||||
RESTORE_CURSOR: encoder.encode(ANSI.RESTORE_CURSOR),
|
RESTORE_CURSOR: encoder.encode(ANSI.RESTORE_CURSOR),
|
||||||
|
|
||||||
// Screen Control
|
|
||||||
CLEAR_SCREEN: encoder.encode(ANSI.CLEAR_SCREEN),
|
CLEAR_SCREEN: encoder.encode(ANSI.CLEAR_SCREEN),
|
||||||
CLEAR_LINE: encoder.encode(ANSI.CLEAR_LINE),
|
CLEAR_LINE: encoder.encode(ANSI.CLEAR_LINE),
|
||||||
CLEAR_TO_END: encoder.encode(ANSI.CLEAR_TO_END),
|
|
||||||
CLEAR_TO_START: encoder.encode(ANSI.CLEAR_TO_START),
|
|
||||||
CLEAR_BELOW: encoder.encode(ANSI.CLEAR_BELOW),
|
CLEAR_BELOW: encoder.encode(ANSI.CLEAR_BELOW),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ANSI_DYNAMIC = {
|
|
||||||
// Cursor Control
|
|
||||||
CURSOR_TO: (x: number, y: number) => encoder.encode(ANSI.CURSOR_TO(x, y)),
|
|
||||||
CURSOR_UP: (x = 1) => encoder.encode(ANSI.CURSOR_UP(x)),
|
|
||||||
CURSOR_DOWN: (x = 1) => encoder.encode(ANSI.CURSOR_DOWN(x)),
|
|
||||||
CURSOR_FORWARD: (x = 1) => encoder.encode(ANSI.CURSOR_FORWARD(x)),
|
|
||||||
CURSOR_BACK: (x = 1) => encoder.encode(ANSI.CURSOR_BACK(x)),
|
|
||||||
};
|
|
||||||
|
|
||||||
type AnsiBufferKey = keyof typeof ANSI_BUFFERS;
|
type AnsiBufferKey = keyof typeof ANSI_BUFFERS;
|
||||||
|
|
||||||
export async function writeAnsi(
|
export async function writeAnsi(
|
||||||
codes: (AnsiBufferKey | Uint8Array)[],
|
codes: AnsiBufferKey[],
|
||||||
text?: string,
|
text?: string,
|
||||||
autoReset: boolean = true
|
autoReset: boolean = true
|
||||||
): Promise<Result<number, Error>> {
|
): Promise<Result<number, Error>> {
|
||||||
@ -117,11 +100,7 @@ export async function writeAnsi(
|
|||||||
|
|
||||||
// Add requested ANSI codes
|
// Add requested ANSI codes
|
||||||
for (const code of codes) {
|
for (const code of codes) {
|
||||||
if (code instanceof Uint8Array) {
|
buffers.push(ANSI_BUFFERS[code]);
|
||||||
buffers.push(code);
|
|
||||||
} else {
|
|
||||||
buffers.push(ANSI_BUFFERS[code]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add optional text
|
// Add optional text
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user