UNPKG

react-curse

Version:

Fastest terminal UI for react (TUI, CLI, curses-like)

1,439 lines (1,420 loc) 58.9 kB
// input.ts import EventEmitter from "events"; var Input = class { ee; queue = []; constructor() { this.ee = new EventEmitter(); } terminate() { this.ee.removeAllListeners(); } onData = (key) => { const raw = key.toString(); const chunks = this.parse(raw); if (chunks.length > 1) this.queue = chunks.slice(1); this.ee.emit("data", chunks[0], () => { this.queue = []; return raw; }); }; parse(input) { const chars = input.split(""); let res; const chunks = []; while (res = chars.shift()) { if (["", "\x1B"].includes(res)) { res += chars.shift() || ""; if (res.endsWith("[")) { res += chars.shift() || ""; if (res.endsWith("1") || res.endsWith("4") || res.endsWith("5") || res.endsWith("6")) { res += chars.shift() || ""; } else if (res.endsWith("M")) { res += chars.shift() || ""; res += chars.shift() || ""; res += chars.shift() || ""; } } } chunks.push(res); } return chunks; } on(callback) { if (this.ee.listenerCount("data") === 0) process.stdin.on("data", this.onData); this.ee.on("data", callback); } off(callback) { this.ee.off("data", callback); if (this.ee.listenerCount("data") === 0) process.stdin.off("data", this.onData); } render() { const chunk2 = this.queue.shift(); if (chunk2) setTimeout(() => this.ee.emit("data", chunk2), 0); } }; // reconciler.ts import Reconciler from "react-reconciler"; var currentUpdatePriority = 0; var TextElement = class { props; parent; children; constructor(props = {}) { this.props = props; this.parent = null; this.children = []; } terminate() { this.children = []; } appendChild(child) { this.children = [...this.children, child]; } commitUpdate(nextProps) { this.props = nextProps; } insertBefore(child, beforeChild) { const index = this.children.indexOf(beforeChild); if (index !== -1) this.children.splice(index, 0, child); } removeChild(child) { const index = this.children.indexOf(child); if (index !== -1) this.children.splice(index, 1); } }; var TextInstance = class { value; constructor(value) { this.value = value; } commitTextUpdate(value) { this.value = value; } toString() { return this.value; } }; var reconciler_default = (resetAfterCommit) => { const reconciler = Reconciler({ supportsMutation: true, appendChild(parentInstance, child) { parentInstance.appendChild(child); }, appendChildToContainer(container, child) { container.appendChild(child); }, appendInitialChild(parentInstance, child) { parentInstance.appendChild(child); }, clearContainer() { }, commitTextUpdate(textInstance, _oldText, newText) { textInstance.commitTextUpdate(newText); }, commitUpdate(instance, _type, _prevProps, nextProps) { instance.commitUpdate(nextProps); }, createInstance(type, props) { if (type === "text") { return new TextElement(props); } else { throw new Error("must be <Text>"); } }, createTextInstance(text) { return new TextInstance(text); }, detachDeletedInstance() { }, finalizeInitialChildren() { return false; }, getChildHostContext() { return {}; }, getPublicInstance(instance) { return instance; }, getRootHostContext(rootContainer) { return rootContainer; }, insertBefore(parentInstance, child, beforeChild) { parentInstance.insertBefore(child, beforeChild); }, insertInContainerBefore(container, child, beforeChild) { container.insertBefore(child, beforeChild); }, prepareForCommit() { return null; }, // @ts-expect-error any prepareUpdate() { return true; }, removeChild(parentInstance, child) { parentInstance.removeChild(child); }, removeChildFromContainer(container, child) { container.removeChild(child); }, resetAfterCommit() { resetAfterCommit(); }, shouldSetTextContent() { return false; }, setCurrentUpdatePriority(newPriority) { currentUpdatePriority = newPriority; }, getCurrentUpdatePriority() { return currentUpdatePriority; }, resolveUpdatePriority() { return currentUpdatePriority !== 0 ? currentUpdatePriority : 16; }, maySuspendCommit() { return false; } }); return reconciler; }; // screen.ts var Screen = class { buffer; cursor = { x: 0, y: 0 }; size = { x1: 0, y1: 0, x2: 0, y2: 0 }; constructor() { this.buffer = this.generateBuffer(); } generateBuffer() { this.size = { x1: 0, y1: 0, x2: process.stdout.columns, y2: process.stdout.rows }; return [...Array(this.size.y2)].map(() => [...Array(this.size.x2)].map(() => [" ", {}])); } clearBuffer() { this.buffer = this.generateBuffer(); this.cursor = { x: 0, y: 0 }; } render(elements) { this.clearBuffer(); this.renderElement(elements, { ...this.cursor, ...this.size }); } stringAt(value, limit) { const percent = parseFloat(value); let diff = ""; const index = value.search(/%[+-]\d+$/); if (index !== -1) diff = value.substring(index + 1); if (!value.endsWith("%" + diff) || isNaN(percent)) throw new Error("must be percent"); return Math.round(limit / 100 * percent) + parseInt(diff || "0"); } renderElement(element, prevBounds, prevProps = {}) { if (Array.isArray(element)) return element.forEach((i) => this.renderElement(i, prevBounds, prevProps)); const { children, ...props } = element.props ?? { children: element }; if (typeof props.x === "string") props.x = this.stringAt(props.x, props.absolute ? this.buffer[0].length : prevBounds.x2 - prevBounds.x); if (typeof props.y === "string") props.y = this.stringAt(props.y, props.absolute ? this.buffer.length : prevBounds.y2 - prevBounds.y); if (typeof props.width === "string") props.width = this.stringAt(props.width, props.absolute ? this.buffer[0].length : prevBounds.x2 - prevBounds.x); if (typeof props.height === "string") props.height = this.stringAt(props.height, props.absolute ? this.buffer.length : prevBounds.y2 - prevBounds.y); if (props.width !== void 0 && isNaN(props.width)) props.width = 0; if (props.height !== void 0 && isNaN(props.height)) props.height = 0; const x = props.x !== void 0 ? (props.absolute ? 0 : prevBounds.x) + props.x : this.cursor.x; const y = props.y !== void 0 ? (props.absolute ? 0 : prevBounds.y) + props.y : this.cursor.y; const x1 = props.x !== void 0 ? props.absolute ? props.x : Math.max(prevBounds.x, prevBounds.x + props.x) : prevBounds.x1; const y1 = props.y !== void 0 ? props.absolute ? props.y : Math.max(prevBounds.y, prevBounds.y + props.y) : prevBounds.y1; const x2 = props.width !== void 0 ? Math.min(props.absolute ? this.buffer[0].length : prevBounds.x2, props.width + x) : props.absolute ? this.buffer[0].length : prevBounds.x2; const y2 = props.height !== void 0 ? Math.min(props.absolute ? this.buffer.length : prevBounds.y2, props.height + y) : props.absolute ? this.buffer.length : prevBounds.y2; const bounds = { x, y, x1, y1, x2, y2 }; this.cursor.x = bounds.x; this.cursor.y = bounds.y; const modifiers = Object.fromEntries( ["color", "background", "bold", "dim", "italic", "underline", "blinking", "inverse", "strikethrough"].map((i) => [i, props[i] ?? prevProps[i]]).filter((i) => i[1]) ); if ((props.background || props.clear) && (props.width || props.height)) this.fill(bounds, props.absolute ? bounds : prevBounds, modifiers); if (Array.isArray(children) || children?.props) { this.renderElement(element.children, bounds, modifiers); } else if (typeof children === "number" || children) { const text = children.toString(); if (text.includes("\n")) { const lines = children.toString().split("\n"); lines.forEach((line, index) => { this.renderElement(line, bounds, modifiers); if (index < lines.length - 1) this.carret(prevBounds); }); } else { this.cursor.x = this.put(text, bounds, modifiers); } } if (props.block) this.carret(prevBounds); if (props.width || props.height) { this.cursor.x = props.block ? prevBounds.x : bounds.x2; this.cursor.y = props.block ? bounds.y2 : prevBounds.y; } } fill(bounds, prevBounds, modifiers) { for (let y = bounds.y; y < bounds.y2; y++) { if (y < Math.max(0, prevBounds.y1) || y >= Math.min(prevBounds.y2, this.buffer.length)) continue; for (let x = bounds.x; x < bounds.x2; x++) { if (x < Math.max(0, prevBounds.x1) || x >= Math.min(prevBounds.x2, this.buffer[y].length)) continue; this.buffer[y][x] = [" ", modifiers]; } } } put(text, bounds, modifiers) { const { x, y } = bounds; let i; for (i = 0; i < text.length; i++) { if (y < Math.max(0, bounds.y1) || y >= Math.min(this.buffer.length, bounds.y2)) break; if (x + i < Math.max(0, bounds.x1) || x + i >= Math.min(this.buffer[y].length, bounds.x2)) continue; this.buffer[y][x + i] = [text[i], modifiers]; } return x + i; } carret(bounds) { this.cursor.x = bounds.x ?? 0; this.cursor.y++; } }; var screen_default = Screen; // term.ts var ESC = "\x1B"; var Term = class { fullscreen = true; print = false; isResized = false; isMouseEnabled = false; prevBuffer; prevModifier = {}; nextWritePrefix = ""; size = { width: process.stdout.columns, height: process.stdout.rows }; offset = { x: 0, y: 0 }; cursor = { x: 0, y: 0 }; maxCursor = { x: 0, y: 0 }; result; async init(fullscreen, print) { this.fullscreen = fullscreen; this.print = print; process.stdout.on("resize", () => { this.isResized = true; this.size = { width: process.stdout.columns, height: process.stdout.rows }; }); process.on("exit", this.onExit); if (fullscreen) { this.append(`${ESC}[?1049h`); this.append(`${ESC}c`); } else { const cursor = await this.termGetCursor(); this.offset = cursor; } this.append(`${ESC}[?25l`); } reinit() { this.prevModifier = {}; this.prevBuffer = void 0; this.append(`${ESC}[?1049h${ESC}c${ESC}[?25l`); } onExit = (code) => { if (code !== 0) return; process.stdout.write(this.terminate()); process.exit(0); }; terminate() { process.off("exit", this.onExit); const sequence = []; if (this.fullscreen) { sequence.push(`${ESC}[?1049l`); } else { const y = this.maxCursor.y - this.cursor.y; if (y > 0) sequence.push(`${ESC}[${y}B`); const x = this.maxCursor.x - this.cursor.x + 1; if (x > 0) sequence.push(`${ESC}[${x}C`); sequence.push(` `); } sequence.push(`${ESC}[?25h`); if (this.isMouseEnabled) sequence.push(`${ESC}[?1000l`); return sequence.join(""); } append(value) { this.nextWritePrefix += value; } setResult(result) { this.result = result; } enableMouse() { this.append(`${ESC}[?1000h${ESC}[?1005h`); this.isMouseEnabled = true; } async termGetCursor() { process.stdin.setRawMode(true); process.stdout.write("\x1B[6n"); return await new Promise((resolve) => { process.stdin.once("data", (data) => { const [x, y] = data.toString().slice(2, -1).split(";").reverse().map((i) => parseInt(i) - 1); resolve({ x, y }); }); }); } parseHexColor(color) { if (!color.match(/^([\da-f]{6})|([\da-f]{3})$/i)) return; return (color.length === 4 ? color.substring(1, 4).split("").map((i) => i + i) : color.substring(1, 7).match(/.{2}/g)).map((i) => parseInt(i, 16)); } parseColor(color, offset = 0) { if (typeof color === "number") { if (color < 0 || color > 255) throw new Error("color not found"); return `${38 + offset};5;${color}`; } if (color.startsWith("#")) { const [r, g, b] = this.parseHexColor(color); return `${38 + offset};2;${r};${g};${b}`; } const names = { black: 30, red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37, brightblack: 90, brightred: 91, brightgreen: 92, brightyellow: 93, brightblue: 94, brightmagenta: 95, brightcyan: 96, brightwhite: 97 }; const colorFromName = names[color.toLowerCase()]; if (colorFromName === void 0) throw new Error("color not found"); return colorFromName + offset; } createModifierSequence(modifier) { if (JSON.stringify(modifier) === "{}") return "0"; const { prevModifier } = this; const sequence = []; if (modifier.color !== prevModifier.color) sequence.push(modifier.color ? this.parseColor(modifier.color) : 39); if (modifier.background !== prevModifier.background) sequence.push(modifier.background ? this.parseColor(modifier.background, 10) : 49); if (modifier.bold !== prevModifier.bold) sequence.push(modifier.bold ? 1 : modifier.dim ? "22;2" : 22); if (modifier.dim !== prevModifier.dim) sequence.push(modifier.dim ? 2 : modifier.bold ? "22;1" : 22); if (modifier.italic !== prevModifier.italic) sequence.push(modifier.italic ? 3 : 23); if (modifier.underline !== prevModifier.underline) sequence.push(modifier.underline ? 4 : 24); if (modifier.blinking !== prevModifier.blinking) sequence.push(modifier.blinking ? 5 : 25); if (modifier.inverse !== prevModifier.inverse) sequence.push(modifier.inverse ? 7 : 27); if (modifier.strikethrough !== prevModifier.strikethrough) sequence.push(modifier.strikethrough ? 9 : 29); return sequence.join(";"); } isIcon(char) { const code = char.charCodeAt(0); return code >= 9211 && code <= 9214 || [9829, 9889, 11096].includes(code) || code >= 57344 && code <= 64838; } render(buffer) { let full = false; let result = ""; if (this.isResized) { if (this.fullscreen) { result += `${ESC}[H`; this.cursor = { x: 0, y: 0 }; full = true; } this.isResized = false; } for (let y = 0; y < buffer.length; y++) { const line = buffer[y]; const prevLine = this.prevBuffer?.[y]; let includesEmoji = false; let includesIcon = false; const diffLine = full ? line : line.map((i, x) => { const [prevChar, prevModifier] = prevLine && prevLine[x] ? prevLine[x] : [" ", {}]; const [char, modifier] = i; return prevChar !== char || JSON.stringify(prevModifier) !== JSON.stringify(modifier) ? i : null; }).filter((i) => i !== void 0); const chunks = {}; let chunksAt = 0; diffLine.forEach((value, x) => { if (value === null) { chunksAt = x + 1; return; } const [char, modifier] = value; if (chunks[chunksAt] === void 0) chunks[chunksAt] = ["", ""]; if (JSON.stringify(modifier) !== JSON.stringify(this.prevModifier)) { chunks[chunksAt][1] += `\x1B[${this.createModifierSequence(modifier)}m`; this.prevModifier = modifier; } chunks[chunksAt][0] += char; chunks[chunksAt][1] += char; }); Object.entries(chunks).map(([index, value]) => { const [str, strWithModifiers] = value; const x = parseInt(index); if (/\p{Emoji}/u.test(str)) includesEmoji = true; if (!includesIcon && str.split("").find((i) => this.isIcon(i))) includesIcon = true; if (x === 0 && y === this.cursor.y + 1) { if (!this.fullscreen && y > this.maxCursor.y) { this.offset.y -= 1; } result += "\n"; } else { if (!this.fullscreen && y > this.cursor.y && y > this.maxCursor.y) { const diff = y - this.maxCursor.y; result += "\n".repeat(diff); this.cursor = { y: this.cursor.y + diff, x: 0 }; const rows = this.offset.y + y - (process.stdout.rows - 1); if (rows > 0) this.offset.y -= rows; } if (y !== this.cursor.y && x !== this.cursor.x) { result += `${ESC}[${y + 1 + this.offset.y};${x + 1}H`; } else if (y > this.cursor.y) { const diff = y - this.cursor.y; result += `${ESC}[${diff > 1 ? diff : ""}B`; } else if (y < this.cursor.y) { const diff = this.cursor.y - y; result += `${ESC}[${diff > 1 ? diff : ""}A`; } else if (x > this.cursor.x) { if (includesEmoji || includesIcon) { result += `${ESC}[G${ESC}[${x > 1 ? x : ""}C`; } else { const diff = x - this.cursor.x; result += `${ESC}[${diff > 1 ? diff : ""}C`; } } else if (x < this.cursor.x) { if (includesEmoji) { result += `${ESC}[G${ESC}[${x > 1 ? x : ""}C`; } else { const diff = this.cursor.x - x; result += `${ESC}[${diff > 1 ? diff : ""}D`; } } } result += strWithModifiers; this.cursor = { x: x + str.length, y }; }); if (this.cursor.x > this.maxCursor.x) this.maxCursor.x = this.cursor.x; if (this.cursor.y > this.maxCursor.y) this.maxCursor.y = this.cursor.y; } this.prevBuffer = buffer; if (this.nextWritePrefix) { result = this.nextWritePrefix + result; this.nextWritePrefix = ""; } if (this.result !== void 0 || this.print) { result += this.terminate(); } if (result) { if (this.print) return renderer_default.terminate(result); process.stdout.write(result); } if (this.result !== void 0) { renderer_default.terminate(this.result); } } }; var term_default = Term; // renderer.ts import { spawnSync } from "child_process"; var Renderer = class { container; screen; input; term; reconciler; callback; throttleAt = 0; throttleTimeout; constructor() { this.container = new TextElement(); this.screen = new screen_default(); this.input = new Input(); this.reconciler = reconciler_default(this.throttle); this.term = new term_default(); } render(reactElement, options = { fullscreen: true, print: false }) { this.term = new term_default(); this.term.init(options.fullscreen, options.print).then(() => { this.reconciler.updateContainer( reactElement, this.reconciler.createContainer(this.container, 0, null, false, null, "", () => { }, null) ); }); } inline(reactElement, options = { fullscreen: false, print: false }) { this.render(reactElement, options); } prompt(reactElement, options = { fullscreen: false, print: false }) { this.render(reactElement, options); return new Promise((resolve) => { this.callback = resolve; }); } print(reactElement, options = { fullscreen: false, print: true }) { this.render(reactElement, options); return new Promise((resolve) => { this.callback = resolve; }); } frame(reactElement, options = { fullscreen: false, print: true }) { this.render(reactElement, options); return new Promise((resolve) => { this.callback = (value) => { process.stdout.write(value); resolve(value); }; }); } terminate(value) { this.container.terminate(); this.input.terminate(); if (this.term) { this.term.terminate(); } this.callback?.(value); } spawnSync(command, args, options) { const res = spawnSync(command, args, options); this.term?.reinit(); this.term?.render(this.screen.buffer); return res; } bell() { process.stdout.write("\x07"); } exit(code = 0) { if (typeof code === "number") process.exit(code); this.term?.setResult(code); } throttle = () => { const at = Date.now(); const nextAt = Math.max(0, 1e3 / 60 - (at - this.throttleAt)); clearTimeout(this.throttleTimeout); this.throttleTimeout = setTimeout(() => { this.throttleAt = at; this.screen.render(this.container.children); this.term?.render(this.screen.buffer); this.input.render(); }, nextAt); }; }; var renderer_default = new Renderer(); // utils/chunk.ts function chunk(arr, size, cache = []) { const tmp = [...arr]; while (tmp.length) cache.push(tmp.splice(0, size)); return cache; } // components/Text.tsx import { jsx } from "react/jsx-runtime"; function Text({ children, ...props }) { return /* @__PURE__ */ jsx("text", { ...props, children }); } // components/Banner.tsx import { useMemo } from "react"; import { Fragment, jsx as jsx2 } from "react/jsx-runtime"; var FONT = "BAQEAAQAqq4KDgoA6oLkKOpAZIRAoOAAKERERCgAAETuRKAAAAAOAECAAgQEBEgA5KykpO4A7iLugu4ArqjuIi4A7oLiouIA7qruou4AAEQAAEQIIE6ATiAAjkIkQIQATqqO6koA7qjIqO4AzqisqM4A7ojIio4ArqTkpK4A6iosquoAio6KiuoAzqqqqq4A7qrqio4C7qjOoq4A6kpKSk4AqqqqrkoAqqpEpKQA5iREhOYAjERERCwAQKAAAA4AhkQIBAYATERCREwAUKAAAAAA"; var letters = chunk(Buffer.from(FONT, "base64"), 6); var Letter = ({ children }) => { const text = useMemo(() => { let code = children.toUpperCase().charCodeAt(0); if (code >= 123 && code <= 126) code -= 26; const font = letters[Math.floor((code - 32) / 2)]; if (!font) return; const bits = code % 2 === 0 ? 4 : 0; return chunk(font, 2).map(([top, bot]) => { return [3, 2, 1, 0].map((i) => { const b = Math.pow(2, i + bits); const code2 = 0 | (top & b && 4) | (bot & b && 8); return code2 ? String.fromCharCode(9596 + code2) : " "; }).join(""); }).join("\n"); }, [children]); return /* @__PURE__ */ jsx2(Fragment, { children: text }); }; function Banner({ children, ...props }) { if (children === void 0 || children === null) return null; const lines = children.toString().split("\n"); const length = Math.max(...lines.map((i) => i.length)); return /* @__PURE__ */ jsx2(Text, { ...props, height: lines.length * 3, width: length * 4, children: lines.map((line, key) => /* @__PURE__ */ jsx2(Text, { x: 0, y: key * 3, children: line.split("").map((char, key2) => /* @__PURE__ */ jsx2(Text, { x: key2 * 4, y: 0, children: /* @__PURE__ */ jsx2(Letter, { children: char }) }, key2)) }, key)) }); } // components/Bar.tsx import { jsx as jsx3, jsxs } from "react/jsx-runtime"; var getSize = (offset, size) => { offset = Math.round(offset * 8); size = Math.round(size * 8); if (offset < 0) { size += offset; offset = 0; } size = Math.max(0, size); return [offset, size]; }; var getSections = (offset, size) => [ size >= 8 || (offset + size) % 8 === 0 && size > 0 && size < 8, size >= 8, (size >= 8 || offset % 8 === 0 && size < 8) && (offset + size) % 8 !== 0 ]; var Vertical = (y, height, props) => { const [offset, size] = getSize(y, height); const sections = getSections(offset, size); const char = (value) => { if (value > 7) return String.fromCharCode(9608); return String.fromCharCode(9608 - Math.min(8, value)); }; return /* @__PURE__ */ jsxs(Text, { ...props, y: Math.floor(offset / 8), children: [ sections[0] && /* @__PURE__ */ jsx3(Text, { block: true, children: char(offset % 8) }), sections[1] && [...Array(Math.floor((offset % 8 + size) / 8) - 1)].map((_, key) => /* @__PURE__ */ jsx3(Text, { block: true, children: char(8) }, key)), sections[2] && /* @__PURE__ */ jsx3(Text, { inverse: true, children: char((offset + size) % 8) }) ] }); }; var Horizontal = (x, width, props) => { const [offset, size] = getSize(x, width); const sections = getSections(offset, size); const char = (value) => { if (value <= 0) return " "; return String.fromCharCode(9616 - Math.min(8, value)); }; return /* @__PURE__ */ jsxs(Text, { ...props, x: Math.floor(offset / 8), children: [ sections[0] && /* @__PURE__ */ jsx3(Text, { inverse: true, children: char(offset % 8) }), sections[1] && /* @__PURE__ */ jsx3(Text, { children: char(8).repeat(Math.floor((offset % 8 + size) / 8) - 1) }), sections[2] && /* @__PURE__ */ jsx3(Text, { children: char((offset + size) % 8) }) ] }); }; function Bar({ type = "vertical", y, x, height, width, ...props }) { if (type === "vertical") return Vertical(y || 0, height || 0, { x, width, ...props }); if (type === "horizontal") return Horizontal(x || 0, width || 0, { y, height, ...props }); return null; } // hooks/useSize.ts import { useEffect, useState } from "react"; var subscribers = /* @__PURE__ */ new Set(); var getSize2 = () => { const { columns: width, rows: height } = process.stdout; return { width, height }; }; process.stdout.on("resize", () => { const size = getSize2(); subscribers.forEach((_, fn) => fn(size)); }); var useSize_default = () => { const [size, setSize] = useState(getSize2()); useEffect(() => { subscribers.add(setSize); return () => { subscribers.delete(setSize); }; }, []); return size; }; // components/Block.tsx import { jsx as jsx4 } from "react/jsx-runtime"; function Block({ width = void 0, align = "left", children, ...props }) { const handle = (line, key = void 0) => { if (typeof line === "object") return line; if (line === "\n") return; let x = 0; switch (align) { case "center": width ??= useSize_default().width; x = Math.round(width / 2 - line.length / 2); break; case "right": x = `100%-${line.length}`; break; } return /* @__PURE__ */ jsx4(Text, { x, ...props, block: true, children: line }, key); }; if (Array.isArray(children)) return children.map(handle); return handle(children); } // components/Canvas.tsx import { Children, useEffect as useEffect2, useMemo as useMemo2, useRef } from "react"; import { Fragment as Fragment2, jsx as jsx5 } from "react/jsx-runtime"; var CanvasClass = class { // prettier-ignore MODES = { "1x1": { map: [[1]], table: [32, 136] }, "1x2": { map: [[1], [2]], table: [32, 128, 132, 136] }, "2x2": { map: [[1, 4], [2, 8]], table: [32, 152, 150, 140, 157, 128, 158, 155, 151, 154, 132, 153, 144, 156, 159, 136] }, "2x4": { map: [[1, 8], [2, 16], [4, 32], [64, 128]] } }; mode; multicolor; w; h; buffer; colors; constructor(width, height, mode = { w: 1, h: 2 }) { this.mode = mode; this.multicolor = mode.w === 1 && mode.h === 2; this.w = Math.ceil(width / this.mode.w) * this.mode.w; this.h = Math.ceil(height / this.mode.h) * this.mode.h; const size = this.w / this.mode.w * this.h / this.mode.h; this.buffer = Buffer.alloc(size); this.colors = [...Array(size * (this.multicolor ? 2 : 1))]; } clear() { this.buffer.fill(0); this.colors.fill(0); } set(x, y, color) { if (x < 0 || x >= this.w || y < 0 || y >= this.h) return; const index = this.w / this.mode.w * Math.floor(y / this.mode.h) + Math.floor(x / this.mode.w); this.buffer[index] |= this.MODES[`${this.mode.w}x${this.mode.h}`].map[y % this.mode.h][x % this.mode.w]; if (color) this.colors[this.multicolor ? this.w * y + x : index] = color; } line(x0, y0, x1, y1, color) { const dx = x1 - x0; const dy = y1 - y0; const adx = Math.abs(dx); const ady = Math.abs(dy); let eps = 0; const sx = dx > 0 ? 1 : -1; const sy = dy > 0 ? 1 : -1; if (adx > ady) { for (let x = x0, y = y0; sx < 0 ? x >= x1 : x <= x1; x += sx) { this.set(x, y, color); eps += ady; if (eps << 1 >= adx) { y += sy; eps -= adx; } } } else { for (let x = x0, y = y0; sy < 0 ? y >= y1 : y <= y1; y += sy) { this.set(x, y, color); eps += adx; if (eps << 1 >= ady) { x += sx; eps -= ady; } } } } render() { return [...this.buffer].map((i, index) => { const table = this.MODES[`${this.mode.w}x${this.mode.h}`].table; let res = String.fromCharCode(table ? (i && 9472) + table[i] : 10240 + i); let colors = []; if (res !== " ") { if (this.multicolor) { const y = Math.floor(index / this.w) * this.mode.h; const x = index % this.w * this.mode.w; const color1 = this.colors[this.w * y + x]; const color2 = this.colors[this.w * (y + 1) + x]; if (res === "\u2588" && color1 !== color2) { res = "\u2580"; colors = [color1, color2]; } else { colors = [color1 || color2]; } } else { colors = [this.colors[index]]; } } return [res, colors]; }); } }; var Point = (_props) => /* @__PURE__ */ jsx5(Fragment2, {}); var Line = (_props) => /* @__PURE__ */ jsx5(Fragment2, {}); function Canvas({ mode = { w: 1, h: 2 }, width, height, children, ...props }) { const canvas = useRef(new CanvasClass(width, height, mode)); useEffect2(() => { canvas.current = new CanvasClass(width, height, mode); }, [width, height, mode]); const text = useMemo2(() => { canvas.current.clear(); Children.forEach(children, (i) => { if (i.type === Point) { const { x, y, color } = i.props; canvas.current.set(x, y, color); } else if (i.type === Line) { const { x, y, dx, dy, color } = i.props; canvas.current.line(x, y, dx, dy, color); } }); return canvas.current.render(); }, [children]); return /* @__PURE__ */ jsx5(Text, { ...props, children: chunk(text, canvas.current.w / canvas.current.mode.w).map((line, y) => /* @__PURE__ */ jsx5(Text, { x: 0, y, children: line.map( ([char, [color, background]], x) => char !== " " && /* @__PURE__ */ jsx5( Text, { x, y: 0, color: color ? color : void 0, background: background ? background : void 0, children: char }, x ) ) }, y)) }); } // hooks/useChildrenSize.ts import { useEffect as useEffect3, useState as useState2 } from "react"; var render = (element) => { if (Array.isArray(element)) return element.map((i) => render(i)).join(""); const { children } = element.props ?? { children: element }; if (Array.isArray(children) || children?.props) return render(children); return children.toString(); }; var getSize3 = (children) => { const string = render(children).split("\n"); const width = string.reduce((acc, i) => Math.max(acc, i.length), 0); const height = string.length; return { width, height }; }; var useChildrenSize_default = (children) => { const [size, setSize] = useState2(getSize3(children)); useEffect3(() => { setSize(getSize3(children)); }, [children]); return size; }; // components/Frame.tsx import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime"; var FRAMES = { single: "\u250C\u2500\u2510\u2502\u2514\u2518", double: "\u2554\u2550\u2557\u2551\u255A\u255D", rounded: "\u256D\u2500\u256E\u2502\u2570\u256F" }; function Frame({ type = "single", height: _height, width: _width, children, ...props }) { const frames = FRAMES[type]; const size = _height === void 0 || _width === void 0 ? useChildrenSize_default(children) : void 0; const height = _height ?? size.height; const width = _width ?? size.width; const { color } = props; return /* @__PURE__ */ jsxs2(Text, { ...props, children: [ /* @__PURE__ */ jsxs2(Text, { color, block: true, children: [ frames[0], frames[1].repeat(width), frames[2] ] }), [...Array(height)].map((_, key) => /* @__PURE__ */ jsxs2(Text, { block: true, children: [ /* @__PURE__ */ jsx6(Text, { color, children: frames[3] }), " ".repeat(width), /* @__PURE__ */ jsx6(Text, { color, children: frames[3] }) ] }, key)), /* @__PURE__ */ jsx6(Text, { y: 1, x: 1, block: true, children }), /* @__PURE__ */ jsxs2(Text, { y: height + 1, color, children: [ frames[4], frames[1].repeat(width), frames[5] ] }) ] }); } // hooks/useInput.ts import { useEffect as useEffect4 } from "react"; var useInput_default = (callback = () => { }, deps = []) => { useEffect4(() => { if (!process.stdin.isRaw) process.stdin.setRawMode?.(true); }, []); useEffect4(() => { const handler = (input, raw) => { if (input === "") process.exit(); if (input.startsWith("\x1B[M")) return; callback(input, raw); }; renderer_default.input.on(handler); return () => { renderer_default.input.off(handler); }; }, deps); }; // components/Input.tsx import { useEffect as useEffect5, useMemo as useMemo3, useRef as useRef2, useState as useState3 } from "react"; import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime"; var mutate = (value, pos, str, multiline) => { const edit = (value2, pos2, callback) => { const left = callback(value2.substring(0, pos2)); const right = value2.substring(pos2); return [left, right].join(""); }; const arr = renderer_default.input.parse(str); for (const input of arr) { switch (input) { case "": // C-a case "\x1B[1~": { if (pos > 0) pos = 0; break; } case "": // C-e case "\x1B[4~": { if (pos < value.length) pos = value.length; break; } case "": // C-b case "\x1B[D": { if (pos > 0) pos -= 1; break; } case "": // C-f case "\x1B[C": { if (pos < value.length) pos += 1; break; } case "\x1B": { return [value, pos, "cancel"]; } case "": // C-d case "\r": { if (input === "\r" && multiline) { value = edit(value, pos, (i) => i + "\n"); pos += 1; break; } return [value, pos, "submit"]; } case "\b": // C-h case "\x7F": { if (pos < 1) break; value = edit(value, pos, (i) => i.substring(0, i.length - 1)); pos -= 1; break; } case "": { if (pos < 1) break; value = edit(value, pos, () => ""); pos = 0; break; } case "\v": { if (pos > value.length - 1) break; value = value.substring(0, pos); break; } case "\x1Bb": // M-b case "": { if (pos < 1) break; const index = value.substring(0, pos).trimEnd().lastIndexOf(" "); if (input === "") value = edit(value, pos, (i) => index !== -1 ? i.substring(0, index + 1) : ""); pos = Math.max(0, index + 1); break; } case "\x1Bf": { if (pos > value.length - 1) break; const nextWordIndex = value.substring(pos).match(/\s(\w)/)?.index ?? -1; pos = nextWordIndex === -1 ? value.length : pos + nextWordIndex + 1; break; } case "\x1Bd": { const nextEndIndex = value.substring(pos).match(/\w(\b)/)?.index ?? -1; value = value.substring(0, pos) + (nextEndIndex !== -1 ? value.substring(pos + nextEndIndex + 1) : ""); break; } case "\x1B[A": { if (!multiline) break; const currentLine = value.substring(0, pos).lastIndexOf("\n"); if (currentLine === -1) break; const targetLine = value.substring(0, currentLine).lastIndexOf("\n"); pos = targetLine + Math.min(pos - currentLine, currentLine - targetLine); break; } case "\x1B[B": { if (!multiline) break; let targetLine_ = value.substring(pos).indexOf("\n"); if (targetLine_ === -1) break; targetLine_ += pos + 1; let nextLine = value.substring(targetLine_).indexOf("\n"); nextLine = (nextLine !== -1 ? targetLine_ + nextLine : value.length) + 1; const currentLine_ = value.substring(0, pos).lastIndexOf("\n"); pos = targetLine_ + Math.min(pos - currentLine_ - 1, nextLine - targetLine_ - 1); break; } default: { if (input.charCodeAt(0) < 32) break; value = edit(value, pos, (i) => i + input); pos += 1; } } } return [value, pos, null]; }; function Input2({ focus = true, type = "text", initialValue = "", cursorBackground = void 0, onCancel = () => { }, onChange = (_) => { }, onSubmit = (_) => { }, width = void 0, height = void 0, ...props }) { const [value, setValue] = useState3(initialValue); const [pos, setPos] = useState3(initialValue.length); const offset = useRef2({ y: 0, x: 0 }); const multiline = useMemo3(() => { return typeof height === "number" && height > 1; }, [height]); useInput_default( (_, raw) => { if (raw === void 0) return; if (!focus) return; const [valueNew, posNew, action] = mutate(value, pos, raw(), multiline); switch (action) { case "cancel": onCancel(); break; case "submit": onSubmit(valueNew); setValue(""); setPos(0); break; default: setValue(valueNew); setPos(posNew); } }, [focus, value, pos, onCancel, onSubmit] ); useEffect5(() => { onChange(value); }, [value]); if (type === "hidden") return null; const text = useMemo3(() => { if (type === "password") return "*".repeat(value.length); return value; }, [value, type]); const { y: yo, x: xo } = useMemo3(() => { if (typeof width !== "number") return offset.current; let posLine = pos; let valueLine = value; if (multiline && typeof height === "number") { const line = value.substring(0, pos).split("\n").length - 1; if (offset.current.y < line - height + 1) offset.current.y = line - height + 1; if (offset.current.y > line) offset.current.y = line; const currentLine = value.substring(0, pos).lastIndexOf("\n"); posLine = pos - (currentLine !== -1 ? currentLine + 1 : 0); const nextLine = value.substring(pos).indexOf("\n"); valueLine = value.substring(currentLine + 1, nextLine !== -1 ? pos + nextLine : value.length); } if (!multiline && offset.current.x + valueLine.length + 1 > width) offset.current.x = Math.max(0, valueLine.length - width + 1); if (offset.current.x < posLine - width + 1) offset.current.x = posLine - width + 1; if (offset.current.x > posLine) offset.current.x = posLine; return offset.current; }, [value, pos, width]); return /* @__PURE__ */ jsx7(Text, { height, width, ...props, children: /* @__PURE__ */ jsxs3(Text, { y: -yo, x: -xo, children: [ text.substring(0, pos), focus && /* @__PURE__ */ jsxs3(Fragment3, { children: [ /* @__PURE__ */ jsx7(Text, { inverse: cursorBackground === void 0, background: cursorBackground, children: text[pos] !== "\n" && text[pos] || " " }), text[pos] === "\n" && "\n" ] }), text.length > pos && text.substring(pos + (focus ? 1 : 0)) ] }) }); } // components/Scrollbar.tsx import { jsx as jsx8 } from "react/jsx-runtime"; function Scrollbar({ type = "vertical", offset, limit, length, background, color }) { length ||= limit; offset = limit / length * offset; let size = limit / (length / limit); if (size < 1) { offset *= (length - limit / size) / (length - limit); size = 1; } return /* @__PURE__ */ jsx8(Text, { background, height: type === "vertical" ? limit : 1, width: type === "horizontal" ? limit : 1, children: /* @__PURE__ */ jsx8( Bar, { type, y: type === "vertical" ? offset : void 0, x: type === "horizontal" ? offset : void 0, height: type === "vertical" ? size : void 0, width: type === "horizontal" ? size : void 0, color } ) }); } // components/List.tsx import { useEffect as useEffect6, useMemo as useMemo4, useState as useState4 } from "react"; import { jsx as jsx9, jsxs as jsxs4 } from "react/jsx-runtime"; var getYO = (offset, limit, y) => { if (offset <= y - limit) return y - limit + 1; if (offset > y) return y; return offset; }; var inputHandler = (vi, pos, setPos, height, dataLength, onChange) => (input) => { let y; let yo; if ((vi && input === "k" || input === "\x1B[A") && pos.y > 0) y = pos.y - 1; if ((vi && input === "j" || input === "\x1B[B") && pos.y < dataLength - 1) y = pos.y + 1; if ((vi && input === "" || input === "\x1B[5~") && pos.y > 0) y = Math.max(0, pos.y - height); if ((vi && input === "" || input === "\x1B[6~") && pos.y < dataLength - 1) y = Math.min(dataLength - 1, pos.y + height); if (vi && input === "" && pos.y > 0) y = Math.max(0, pos.y - Math.floor(height / 2)); if (vi && input === "" && pos.y < dataLength - 1) y = Math.min(dataLength - 1, pos.y + Math.floor(height / 2)); if ((vi && input === "g" || input === "\x1B[1~") && pos.y > 0) y = 0; if ((vi && input === "G" || input === "\x1B[4~") && pos.y < dataLength - 1) y = dataLength - 1; if (y !== void 0) yo = getYO(pos.yo, height, y); if (vi && input === "H") y = pos.yo; if (vi && input === "M") y = pos.yo + Math.floor(height / 2); if (vi && input === "L") y = pos.yo + height - 1; if (y !== void 0) { let newPos = { ...pos, y }; if (yo !== void 0) newPos = { ...newPos, yo }; setPos(newPos); onChange(newPos); } }; function List({ focus = true, initialPos = { y: 0, x: 0, yo: 0, xo: 0, x1: 0, x2: 0 }, data = [""], renderItem = (_) => /* @__PURE__ */ jsx9(Text, {}), height: _height = void 0, width: _width = void 0, scrollbar = void 0, scrollbarBackground = void 0, scrollbarColor = void 0, vi = true, pass = void 0, onChange = (_) => { }, onSubmit = (_) => { } }) { const size = _height === void 0 || _width === void 0 ? useSize_default() : void 0; const height = _height ?? size.height; const width = _width ?? size.width; const [pos, setPos] = useState4({ ...{ y: 0, x: 0, yo: 0, xo: 0, x1: 0, x2: 0 }, ...initialPos }); const isScrollbarRequired = useMemo4(() => { return scrollbar === void 0 ? data.length > height : scrollbar; }, [scrollbar, data.length, height]); useEffect6(() => { let newPos; let { y } = initialPos; if (y > 0 && y >= data.length) { y = data.length - 1; onChange({ ...pos, y }); } if (y !== pos.y) { y = Math.max(0, y); newPos = { ...newPos || pos, y, yo: getYO(pos.yo, height - 1, y) }; } if (newPos) { setPos(newPos); onChange(newPos); } }, [initialPos.y]); useEffect6(() => { if (pos.y > 0 && pos.y > data.length - 1) { const y = Math.max(0, data.length - 1); const newPos = { ...pos, y, yo: getYO(pos.yo, data.length, y) }; setPos(newPos); onChange(newPos); } }, [data]); useInput_default( (input) => { if (!focus) return; inputHandler(vi, pos, setPos, height, data.length, onChange)(input); if (input === "\r") onSubmit(pos); }, [focus, vi, pos, setPos, height, data, onChange, onSubmit] ); return /* @__PURE__ */ jsxs4(Text, { width, height, children: [ data.filter((_, index) => index >= pos.yo && index < height + pos.yo).map((row, index) => /* @__PURE__ */ jsx9(Text, { height: 1, block: true, children: renderItem({ focus, item: row, selected: index + pos.yo === pos.y, pass }) }, index)), isScrollbarRequired && /* @__PURE__ */ jsx9(Text, { y: 0, x: "100%-1", children: /* @__PURE__ */ jsx9( Scrollbar, { offset: pos.yo, limit: height, length: data.length, background: scrollbarBackground, color: scrollbarColor } ) }) ] }); } // components/ListTable.tsx import { useEffect as useEffect7, useMemo as useMemo5, useState as useState5 } from "react"; import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime"; var getX = (index, widths) => { const [x1, x2] = widths.reduce((acc, i, k) => [acc[0] + (k < index ? i : 0), acc[1] + (k <= index ? i : 0)], [0, 0]); return { x1, x2 }; }; var getXO = (offsetX, limit, x1, x2) => { if (x1 <= offsetX) return x1; if (x2 >= offsetX + limit) return x2 - limit + 1; return offsetX; }; function List2({ mode = "cell", focus = true, initialPos = { y: 0, x: 0, yo: 0, xo: 0, x1: 0, x2: 0 }, height: _height = void 0, width: _width = void 0, head = [""], renderHead = (_) => /* @__PURE__ */ jsx10(Text, {}), data = [[""]], renderItem = (_) => /* @__PURE__ */ jsx10(Text, {}), scrollbar = void 0, scrollbarBackground = void 0, scrollbarColor = void 0, vi = true, pass = void 0, onChange = (_pos) => { }, onSubmit = (_pos) => { } }) { const size = _height === void 0 || _width === void 0 ? useSize_default() : void 0; const height = _height ?? size.height; const width = _width ?? size.width; const [pos, setPos] = useState5({ ...{ y: 0, x: 0, yo: 0, xo: 0, x1: 0, x2: 0 }, ...initialPos }); const isScrollbarRequired = useMemo5(() => { return scrollbar === void 0 ? data.length > height - 1 : scrollbar; }, [scrollbar, data.length, height]); const widths = useMemo5(() => { const widths2 = data.reduce( (acc, row) => { row.forEach((i, k) => acc[k] = Math.max(acc[k], (i?.toString() || "null").length)); return acc; }, head.map((i) => i.toString().length) ).map((i, index) => i + (index <= head.length - 2 ? 2 : 0)); const sum = widths2.reduce((acc, i) => acc + i, 0); if (sum >= width - 1) return widths2.map((i) => Math.min(32, i)); return widths2; }, [data, head, width]); const isCropped = useMemo5(() => { const sum = widths.reduce((acc, i) => acc + i, 0); return sum - pos.xo >= width + (isScrollbarRequired ? -1 : 0); }, [widths, pos.xo, width, isScrollbarRequired]); const dataFiltered = useMemo5(() => { return data.filter((_, index) => index >= pos.yo && index < height + pos.yo); }, [data, pos.yo, height]); useEffect7(() => { let newPos; let { y, x } = initialPos; if (y > 0 && y >= data.length) { y = data.length - 1; onChange({ ...pos, y }); } if (y !== pos.y) { y = Math.max(0, y); newPos = { ...newPos || pos, y, yo: getYO(pos.yo, height - 1, y) }; } if (initialPos.xm) { let acc = 0; x = widths.map((i) => acc += i).findIndex((i) => i >= Math.min(acc, (initialPos.xm ?? 0) + pos.xo)); } if (x > 0 && x >= head.length) { x = head.length - 1; onChange({ ...pos, x }); } if (x !== pos.x) { const { x1, x2 } = getX(x, widths); newPos = { ...newPos || pos, x, xo: getXO(pos.xo, width + (isScrollbarRequired ? -1 : 0), x1, x2) }; } if (newPos) { setPos(newPos); onChange(newPos); } }, [initialPos.y, initialPos.x, initialPos.xm]); useEffect7(() => { if (pos.y > 0 && head.length > 0 && pos.y > data.length - 1) { const y = Math.max(0, data.length - 1); const newPos = { ...pos, y, yo: getYO(pos.yo, data.length, y) }; setPos(newPos); onChange(newPos); } if (pos.x > 0 && head.length > 0 && pos.x > head.length - 1) { const newPos = { ...pos, x: head.length - 1 }; setPos(newPos); onChange(newPos); } }, [head, data]); useInput_default( (input) => { if (!focus) return; inputHandler(vi, pos, setPos, height - 1, data.length, onChange)(input); let x; switch (mode) { case "cell": if ((vi && input === "h" || input === "\x1B[D") && pos.x > 0) x = pos.x - 1; if ((vi && input === "l" || input === "\x1B[C") && pos.x < head.length - 1) x = pos.x + 1; if (vi && input === "^" && pos.x > 0) x = 0; if (vi && input === "$" && pos.x < head.length - 1) x = head.length - 1; if (x !== void 0) { const { x1, x2 } = getX(x, widths); const xo = getXO(pos.xo, width + (isScrollbarRequired ? -1 : 0), x1, x2); const newPos = { ...pos, x, xo, x1, x2 }; setPos(newPos); onChange(newPos); } break; case "row": if ((vi && input === "h" || input === "\x1B[D") && pos.x > 0) x = pos.x - 1; if ((vi && input === "l" || input === "\x1B[C") && pos.x < head.length - 1) x = pos.x + 1; if (x !== void 0) { const { x1, x2 } = getX(x, widths); const newPos = { ...pos, x, xo: x1, x1, x2 }; setPos(newPos); onChange(newPos); } break; } if (input === "\r") onSubmit({ ...pos, ...getX(pos.x, widths) }); }, [focus, vi, pos, width, height, head, data, widths, isScrollbarRequired, setPos, onChange, onSubmit] ); return /* @__PURE__ */ jsxs5(Text, { width, height, children: [ /* @__PURE__ */ jsx10(Text, { x: -pos.xo, height: 1, children: renderHead({ focus, item: head, widths, pass }) }), /* @__PURE__ */ jsx10(Text, { y: 1, x: -pos.xo, children: dataFiltered.map((item, index) => /* @__PURE__ */ jsx10(Text, { height: 1, block: true, children: renderItem({ mode, focus, item, y: pos.y, x: pos.x, widths, index: index + pos.yo, pass }) }, index)) }), isCropped && /* @__PURE__ */ jsx10(Text, { y: 0, x: "100%-1", dim: true, children: "~" }), isScrollbarRequired && /* @__PURE__ */ jsx10(Text, { y: 1, x: "100%-1", children: /* @__PURE__ */ jsx10( Scrollbar, { offset: pos.yo, limit: height - 1, length: data.length, background: scrollbarBackground, color: scrollbarColor } ) }) ] }); } // components/Separator.tsx import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime"; function Separator({ type = "vertical", height: _height, width: _width, ...props }) { const size = _height === void 0 || _width === void 0 ? useSize_default() : void 0; co