tui-crawler
Version:
A terminal-based dungeon crawler game with ASCII graphics
1,657 lines (1,636 loc) • 616 kB
JavaScript
#!/usr/bin/env bun
// @bun
var __create = Object.create;
var __getProtoOf = Object.getPrototypeOf;
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __toESM = (mod, isNodeMode, target) => {
target = mod != null ? __create(__getProtoOf(mod)) : {};
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(to, key))
__defProp(to, key, {
get: () => mod[key],
enumerable: true
});
return to;
};
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
var __require = import.meta.require;
// node_modules/node-gyp-build/node-gyp-build.js
var require_node_gyp_build = __commonJS((exports, module) => {
var fs2 = __require("fs");
var path2 = __require("path");
var os2 = __require("os");
var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
var vars = process.config && process.config.variables || {};
var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
var abi = process.versions.modules;
var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
var arch = process.env.npm_config_arch || os2.arch();
var platform = process.env.npm_config_platform || os2.platform();
var libc = process.env.LIBC || (isAlpine(platform) ? "musl" : "glibc");
var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
var uv = (process.versions.uv || "").split(".")[0];
module.exports = load;
function load(dir) {
return runtimeRequire(load.resolve(dir));
}
load.resolve = load.path = function(dir) {
dir = path2.resolve(dir || ".");
try {
var name = runtimeRequire(path2.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
if (process.env[name + "_PREBUILD"])
dir = process.env[name + "_PREBUILD"];
} catch (err) {}
if (!prebuildsOnly) {
var release = getFirst(path2.join(dir, "build/Release"), matchBuild);
if (release)
return release;
var debug = getFirst(path2.join(dir, "build/Debug"), matchBuild);
if (debug)
return debug;
}
var prebuild = resolve(dir);
if (prebuild)
return prebuild;
var nearby = resolve(path2.dirname(process.execPath));
if (nearby)
return nearby;
var target = [
"platform=" + platform,
"arch=" + arch,
"runtime=" + runtime,
"abi=" + abi,
"uv=" + uv,
armv ? "armv=" + armv : "",
"libc=" + libc,
"node=" + process.versions.node,
process.versions.electron ? "electron=" + process.versions.electron : "",
typeof __webpack_require__ === "function" ? "webpack=true" : ""
].filter(Boolean).join(" ");
throw new Error("No native build was found for " + target + `
loaded from: ` + dir + `
`);
function resolve(dir2) {
var tuples = readdirSync(path2.join(dir2, "prebuilds")).map(parseTuple);
var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0];
if (!tuple)
return;
var prebuilds = path2.join(dir2, "prebuilds", tuple.name);
var parsed = readdirSync(prebuilds).map(parseTags);
var candidates = parsed.filter(matchTags(runtime, abi));
var winner = candidates.sort(compareTags(runtime))[0];
if (winner)
return path2.join(prebuilds, winner.file);
}
};
function readdirSync(dir) {
try {
return fs2.readdirSync(dir);
} catch (err) {
return [];
}
}
function getFirst(dir, filter) {
var files = readdirSync(dir).filter(filter);
return files[0] && path2.join(dir, files[0]);
}
function matchBuild(name) {
return /\.node$/.test(name);
}
function parseTuple(name) {
var arr = name.split("-");
if (arr.length !== 2)
return;
var platform2 = arr[0];
var architectures = arr[1].split("+");
if (!platform2)
return;
if (!architectures.length)
return;
if (!architectures.every(Boolean))
return;
return { name, platform: platform2, architectures };
}
function matchTuple(platform2, arch2) {
return function(tuple) {
if (tuple == null)
return false;
if (tuple.platform !== platform2)
return false;
return tuple.architectures.includes(arch2);
};
}
function compareTuples(a, b) {
return a.architectures.length - b.architectures.length;
}
function parseTags(file) {
var arr = file.split(".");
var extension = arr.pop();
var tags = { file, specificity: 0 };
if (extension !== "node")
return;
for (var i = 0;i < arr.length; i++) {
var tag = arr[i];
if (tag === "node" || tag === "electron" || tag === "node-webkit") {
tags.runtime = tag;
} else if (tag === "napi") {
tags.napi = true;
} else if (tag.slice(0, 3) === "abi") {
tags.abi = tag.slice(3);
} else if (tag.slice(0, 2) === "uv") {
tags.uv = tag.slice(2);
} else if (tag.slice(0, 4) === "armv") {
tags.armv = tag.slice(4);
} else if (tag === "glibc" || tag === "musl") {
tags.libc = tag;
} else {
continue;
}
tags.specificity++;
}
return tags;
}
function matchTags(runtime2, abi2) {
return function(tags) {
if (tags == null)
return false;
if (tags.runtime && tags.runtime !== runtime2 && !runtimeAgnostic(tags))
return false;
if (tags.abi && tags.abi !== abi2 && !tags.napi)
return false;
if (tags.uv && tags.uv !== uv)
return false;
if (tags.armv && tags.armv !== armv)
return false;
if (tags.libc && tags.libc !== libc)
return false;
return true;
};
}
function runtimeAgnostic(tags) {
return tags.runtime === "node" && tags.napi;
}
function compareTags(runtime2) {
return function(a, b) {
if (a.runtime !== b.runtime) {
return a.runtime === runtime2 ? -1 : 1;
} else if (a.abi !== b.abi) {
return a.abi ? -1 : 1;
} else if (a.specificity !== b.specificity) {
return a.specificity > b.specificity ? -1 : 1;
} else {
return 0;
}
};
}
function isNwjs() {
return !!(process.versions && process.versions.nw);
}
function isElectron() {
if (process.versions && process.versions.electron)
return true;
if (process.env.ELECTRON_RUN_AS_NODE)
return true;
return typeof window !== "undefined" && window.process && window.process.type === "renderer";
}
function isAlpine(platform2) {
return platform2 === "linux" && fs2.existsSync("/etc/alpine-release");
}
load.parseTags = parseTags;
load.matchTags = matchTags;
load.compareTags = compareTags;
load.parseTuple = parseTuple;
load.matchTuple = matchTuple;
load.compareTuples = compareTuples;
});
// node_modules/node-gyp-build/index.js
var require_node_gyp_build2 = __commonJS((exports, module) => {
var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
if (typeof runtimeRequire.addon === "function") {
module.exports = runtimeRequire.addon.bind(runtimeRequire);
} else {
module.exports = require_node_gyp_build();
}
});
// node_modules/uiohook-napi/dist/index.js
var require_dist = __commonJS((exports) => {
var __dirname = "G:\\code\\opentui-crawler-game\\node_modules\\uiohook-napi\\dist";
Object.defineProperty(exports, "__esModule", { value: true });
exports.uIOhook = exports.UiohookKey = exports.WheelDirection = exports.EventType = undefined;
var events_1 = __require("events");
var path_1 = __require("path");
var lib = require_node_gyp_build2()((0, path_1.join)(__dirname, ".."));
var KeyToggle;
(function(KeyToggle2) {
KeyToggle2[KeyToggle2["Tap"] = 0] = "Tap";
KeyToggle2[KeyToggle2["Down"] = 1] = "Down";
KeyToggle2[KeyToggle2["Up"] = 2] = "Up";
})(KeyToggle || (KeyToggle = {}));
var EventType;
(function(EventType2) {
EventType2[EventType2["EVENT_KEY_PRESSED"] = 4] = "EVENT_KEY_PRESSED";
EventType2[EventType2["EVENT_KEY_RELEASED"] = 5] = "EVENT_KEY_RELEASED";
EventType2[EventType2["EVENT_MOUSE_CLICKED"] = 6] = "EVENT_MOUSE_CLICKED";
EventType2[EventType2["EVENT_MOUSE_PRESSED"] = 7] = "EVENT_MOUSE_PRESSED";
EventType2[EventType2["EVENT_MOUSE_RELEASED"] = 8] = "EVENT_MOUSE_RELEASED";
EventType2[EventType2["EVENT_MOUSE_MOVED"] = 9] = "EVENT_MOUSE_MOVED";
EventType2[EventType2["EVENT_MOUSE_WHEEL"] = 11] = "EVENT_MOUSE_WHEEL";
})(EventType = exports.EventType || (exports.EventType = {}));
var WheelDirection;
(function(WheelDirection2) {
WheelDirection2[WheelDirection2["VERTICAL"] = 3] = "VERTICAL";
WheelDirection2[WheelDirection2["HORIZONTAL"] = 4] = "HORIZONTAL";
})(WheelDirection = exports.WheelDirection || (exports.WheelDirection = {}));
exports.UiohookKey = {
Backspace: 14,
Tab: 15,
Enter: 28,
CapsLock: 58,
Escape: 1,
Space: 57,
PageUp: 3657,
PageDown: 3665,
End: 3663,
Home: 3655,
ArrowLeft: 57419,
ArrowUp: 57416,
ArrowRight: 57421,
ArrowDown: 57424,
Insert: 3666,
Delete: 3667,
0: 11,
1: 2,
2: 3,
3: 4,
4: 5,
5: 6,
6: 7,
7: 8,
8: 9,
9: 10,
A: 30,
B: 48,
C: 46,
D: 32,
E: 18,
F: 33,
G: 34,
H: 35,
I: 23,
J: 36,
K: 37,
L: 38,
M: 50,
N: 49,
O: 24,
P: 25,
Q: 16,
R: 19,
S: 31,
T: 20,
U: 22,
V: 47,
W: 17,
X: 45,
Y: 21,
Z: 44,
Numpad0: 82,
Numpad1: 79,
Numpad2: 80,
Numpad3: 81,
Numpad4: 75,
Numpad5: 76,
Numpad6: 77,
Numpad7: 71,
Numpad8: 72,
Numpad9: 73,
NumpadMultiply: 55,
NumpadAdd: 78,
NumpadSubtract: 74,
NumpadDecimal: 83,
NumpadDivide: 3637,
NumpadEnter: 3584 | 28,
NumpadEnd: 60928 | 79,
NumpadArrowDown: 60928 | 80,
NumpadPageDown: 60928 | 81,
NumpadArrowLeft: 60928 | 75,
NumpadArrowRight: 60928 | 77,
NumpadHome: 60928 | 71,
NumpadArrowUp: 60928 | 72,
NumpadPageUp: 60928 | 73,
NumpadInsert: 60928 | 82,
NumpadDelete: 60928 | 83,
F1: 59,
F2: 60,
F3: 61,
F4: 62,
F5: 63,
F6: 64,
F7: 65,
F8: 66,
F9: 67,
F10: 68,
F11: 87,
F12: 88,
F13: 91,
F14: 92,
F15: 93,
F16: 99,
F17: 100,
F18: 101,
F19: 102,
F20: 103,
F21: 104,
F22: 105,
F23: 106,
F24: 107,
Semicolon: 39,
Equal: 13,
Comma: 51,
Minus: 12,
Period: 52,
Slash: 53,
Backquote: 41,
BracketLeft: 26,
Backslash: 43,
BracketRight: 27,
Quote: 40,
Ctrl: 29,
CtrlRight: 3613,
Alt: 56,
AltRight: 3640,
Shift: 42,
ShiftRight: 54,
Meta: 3675,
MetaRight: 3676,
NumLock: 69,
ScrollLock: 70,
PrintScreen: 3639
};
class UiohookNapi extends events_1.EventEmitter {
handler(e) {
this.emit("input", e);
switch (e.type) {
case EventType.EVENT_KEY_PRESSED:
this.emit("keydown", e);
break;
case EventType.EVENT_KEY_RELEASED:
this.emit("keyup", e);
break;
case EventType.EVENT_MOUSE_CLICKED:
this.emit("click", e);
break;
case EventType.EVENT_MOUSE_MOVED:
this.emit("mousemove", e);
break;
case EventType.EVENT_MOUSE_PRESSED:
this.emit("mousedown", e);
break;
case EventType.EVENT_MOUSE_RELEASED:
this.emit("mouseup", e);
break;
case EventType.EVENT_MOUSE_WHEEL:
this.emit("wheel", e);
break;
}
}
start() {
lib.start(this.handler.bind(this));
}
stop() {
lib.stop();
}
keyTap(key, modifiers = []) {
if (!modifiers.length) {
lib.keyTap(key, KeyToggle.Tap);
return;
}
for (const modKey of modifiers) {
lib.keyTap(modKey, KeyToggle.Down);
}
lib.keyTap(key, KeyToggle.Tap);
let i = modifiers.length;
while (i--) {
lib.keyTap(modifiers[i], KeyToggle.Up);
}
}
keyToggle(key, toggle) {
lib.keyTap(key, toggle === "down" ? KeyToggle.Down : KeyToggle.Up);
}
}
exports.uIOhook = new UiohookNapi;
});
// src/ansi.ts
var ANSI = {
switchToAlternateScreen: "\x1B[?1049h",
switchToMainScreen: "\x1B[?1049l",
reset: "\x1B[0m",
hideCursor: "\x1B[?25l",
showCursor: "\x1B[?25h",
resetCursorColor: "\x1B]12;default\x07",
saveCursorState: "\x1B[s",
restoreCursorState: "\x1B[u",
enableMouseTracking: "\x1B[?1000h",
disableMouseTracking: "\x1B[?1000l",
enableButtonEventTracking: "\x1B[?1002h",
disableButtonEventTracking: "\x1B[?1002l",
enableAnyEventTracking: "\x1B[?1003h",
disableAnyEventTracking: "\x1B[?1003l",
enableSGRMouseMode: "\x1B[?1006h",
disableSGRMouseMode: "\x1B[?1006l"
};
// src/Renderable.ts
import { EventEmitter } from "events";
var renderableNumber = 1;
class Renderable extends EventEmitter {
static renderablesByNumber = new Map;
id;
num;
ctx = null;
_x;
_y;
_width;
_height;
_zIndex;
visible;
selectable = false;
renderableMap = new Map;
renderableArray = [];
needsZIndexSort = false;
parent = null;
constructor(id, options) {
super();
this.id = id;
this.num = renderableNumber++;
this._x = options.x;
this._y = options.y;
this._width = options.width;
this._height = options.height;
this._zIndex = options.zIndex;
this.visible = options.visible !== false;
Renderable.renderablesByNumber.set(this.num, this);
}
hasSelection() {
return false;
}
onSelectionChanged(selection) {
return false;
}
getSelectedText() {
return "";
}
shouldStartSelection(x, y) {
return false;
}
set needsUpdate(value) {
if (this.parent) {
this.parent.needsUpdate = value;
}
}
get x() {
if (this.parent) {
return this.parent.x + this._x;
}
return this._x;
}
set x(value) {
this._x = value;
}
get y() {
if (this.parent) {
return this.parent.y + this._y;
}
return this._y;
}
set y(value) {
this._y = value;
}
get width() {
return this._width;
}
set width(value) {
this._width = value;
}
get height() {
return this._height;
}
set height(value) {
this._height = value;
}
get zIndex() {
return this._zIndex;
}
set zIndex(value) {
if (this._zIndex !== value) {
this._zIndex = value;
this.parent?.requestZIndexSort();
}
}
requestZIndexSort() {
this.needsZIndexSort = true;
}
ensureZIndexSorted() {
if (this.needsZIndexSort) {
this.renderableArray.sort((a, b) => a.zIndex > b.zIndex ? 1 : a.zIndex < b.zIndex ? -1 : 0);
this.needsZIndexSort = false;
}
}
clear() {
for (const child of this.renderableArray) {
this.remove(child.id);
}
}
add(obj) {
if (this.renderableMap.has(obj.id)) {
this.remove(obj.id);
}
if (obj.parent) {
obj.parent.remove(obj.id);
}
obj.parent = this;
if (this.ctx) {
obj.ctx = this.ctx;
}
this.renderableArray.push(obj);
this.needsZIndexSort = true;
this.renderableMap.set(obj.id, obj);
this.emit("child:added", obj);
}
propagateContext(ctx) {
this.ctx = ctx;
for (const child of this.renderableArray) {
child.propagateContext(ctx);
}
}
getRenderable(id) {
return this.renderableMap.get(id);
}
remove(id) {
if (!id) {
return;
}
if (this.renderableMap.has(id)) {
const obj = this.renderableMap.get(id);
if (obj) {
obj.parent = null;
obj.propagateContext(null);
}
this.renderableMap.delete(id);
const index = this.renderableArray.findIndex((obj2) => obj2.id === id);
if (index !== -1) {
this.renderableArray.splice(index, 1);
}
this.emit("child:removed", id);
}
}
getAllElementIds() {
return Array.from(this.renderableMap.keys());
}
getChildren() {
return [...this.renderableArray];
}
render(buffer, deltaTime) {
if (!this.visible)
return;
this.renderSelf(buffer, deltaTime);
this.ctx?.addToHitGrid(this.x, this.y, this.width, this.height, this.num);
this.ensureZIndexSorted();
for (const child of this.renderableArray) {
child.render(buffer, deltaTime);
}
}
renderSelf(buffer, deltaTime) {}
destroy() {
if (this.parent) {
throw new Error(`Cannot destroy ${this.id} while it still has a parent (${this.parent.id}). Remove from parent first.`);
}
for (const child of this.renderableArray) {
child.parent = null;
child.destroy();
}
this.renderableArray = [];
this.renderableMap.clear();
Renderable.renderablesByNumber.delete(this.num);
this.destroySelf();
}
destroySelf() {}
processMouseEvent(event) {
this.onMouseEvent(event);
if (this.parent && !event.defaultPrevented) {
this.parent.processMouseEvent(event);
}
}
onMouseEvent(event) {}
}
// src/types.ts
class RGBA {
buffer;
constructor(buffer) {
this.buffer = buffer;
}
static fromArray(array) {
return new RGBA(array);
}
static fromValues(r, g, b, a = 1) {
return new RGBA(new Float32Array([r, g, b, a]));
}
static fromInts(r, g, b, a = 255) {
return new RGBA(new Float32Array([r / 255, g / 255, b / 255, a / 255]));
}
static fromHex(hex) {
return hexToRgb(hex);
}
get r() {
return this.buffer[0];
}
set r(value) {
this.buffer[0] = value;
}
get g() {
return this.buffer[1];
}
set g(value) {
this.buffer[1] = value;
}
get b() {
return this.buffer[2];
}
set b(value) {
this.buffer[2] = value;
}
get a() {
return this.buffer[3];
}
set a(value) {
this.buffer[3] = value;
}
map(fn) {
return [fn(this.r), fn(this.g), fn(this.b), fn(this.a)];
}
toString() {
return `rgba(${this.r.toFixed(2)}, ${this.g.toFixed(2)}, ${this.b.toFixed(2)}, ${this.a.toFixed(2)})`;
}
}
var TextAttributes = {
NONE: 0,
BOLD: 1 << 0,
DIM: 1 << 1,
ITALIC: 1 << 2,
UNDERLINE: 1 << 3,
BLINK: 1 << 4,
INVERSE: 1 << 5,
HIDDEN: 1 << 6,
STRIKETHROUGH: 1 << 7
};
// src/utils.ts
function hexToRgb(hex) {
hex = hex.replace(/^#/, "");
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
if (!/^[0-9A-Fa-f]{6}$/.test(hex)) {
console.warn(`Invalid hex color: ${hex}, defaulting to magenta`);
return RGBA.fromValues(1, 0, 1, 1);
}
const r = parseInt(hex.substring(0, 2), 16) / 255;
const g = parseInt(hex.substring(2, 4), 16) / 255;
const b = parseInt(hex.substring(4, 6), 16) / 255;
return RGBA.fromValues(r, g, b, 1);
}
var CSS_COLOR_NAMES = {
black: "#000000",
white: "#FFFFFF",
red: "#FF0000",
green: "#008000",
blue: "#0000FF",
yellow: "#FFFF00",
cyan: "#00FFFF",
magenta: "#FF00FF",
silver: "#C0C0C0",
gray: "#808080",
grey: "#808080",
maroon: "#800000",
olive: "#808000",
lime: "#00FF00",
aqua: "#00FFFF",
teal: "#008080",
navy: "#000080",
fuchsia: "#FF00FF",
purple: "#800080",
orange: "#FFA500",
brightblack: "#666666",
brightred: "#FF6666",
brightgreen: "#66FF66",
brightblue: "#6666FF",
brightyellow: "#FFFF66",
brightcyan: "#66FFFF",
brightmagenta: "#FF66FF",
brightwhite: "#FFFFFF"
};
function parseColor(color) {
if (typeof color === "string") {
const lowerColor = color.toLowerCase();
if (lowerColor === "transparent") {
return RGBA.fromValues(0, 0, 0, 0);
}
if (CSS_COLOR_NAMES[lowerColor]) {
return hexToRgb(CSS_COLOR_NAMES[lowerColor]);
}
return hexToRgb(color);
}
return color;
}
function createTextAttributes({
bold = false,
italic = false,
underline = false,
dim = false,
blink = false,
inverse = false,
hidden = false,
strikethrough = false
} = {}) {
let attributes = TextAttributes.NONE;
if (bold)
attributes |= TextAttributes.BOLD;
if (italic)
attributes |= TextAttributes.ITALIC;
if (underline)
attributes |= TextAttributes.UNDERLINE;
if (dim)
attributes |= TextAttributes.DIM;
if (blink)
attributes |= TextAttributes.BLINK;
if (inverse)
attributes |= TextAttributes.INVERSE;
if (hidden)
attributes |= TextAttributes.HIDDEN;
if (strikethrough)
attributes |= TextAttributes.STRIKETHROUGH;
return attributes;
}
// src/ui/lib/border.ts
var BorderChars = {
single: {
topLeft: "\u250C",
topRight: "\u2510",
bottomLeft: "\u2514",
bottomRight: "\u2518",
horizontal: "\u2500",
vertical: "\u2502",
topT: "\u252C",
bottomT: "\u2534",
leftT: "\u251C",
rightT: "\u2524",
cross: "\u253C"
},
double: {
topLeft: "\u2554",
topRight: "\u2557",
bottomLeft: "\u255A",
bottomRight: "\u255D",
horizontal: "\u2550",
vertical: "\u2551",
topT: "\u2566",
bottomT: "\u2569",
leftT: "\u2560",
rightT: "\u2563",
cross: "\u256C"
},
rounded: {
topLeft: "\u256D",
topRight: "\u256E",
bottomLeft: "\u2570",
bottomRight: "\u256F",
horizontal: "\u2500",
vertical: "\u2502",
topT: "\u252C",
bottomT: "\u2534",
leftT: "\u251C",
rightT: "\u2524",
cross: "\u253C"
},
heavy: {
topLeft: "\u250F",
topRight: "\u2513",
bottomLeft: "\u2517",
bottomRight: "\u251B",
horizontal: "\u2501",
vertical: "\u2503",
topT: "\u2533",
bottomT: "\u253B",
leftT: "\u2523",
rightT: "\u252B",
cross: "\u254B"
}
};
function getBorderSides(border) {
return border === true ? { top: true, right: true, bottom: true, left: true } : Array.isArray(border) ? {
top: border.includes("top"),
right: border.includes("right"),
bottom: border.includes("bottom"),
left: border.includes("left")
} : { top: false, right: false, bottom: false, left: false };
}
function drawBorder(buffer, options) {
const borderColor = parseColor(options.borderColor);
const backgroundColor = parseColor(options.backgroundColor);
const borderSides = getBorderSides(options.border);
const borders = options.customBorderChars || BorderChars[options.borderStyle];
const startX = Math.max(0, options.x);
const startY = Math.max(0, options.y);
const endX = Math.min(buffer.getWidth() - 1, options.x + options.width - 1);
const endY = Math.min(buffer.getHeight() - 1, options.y + options.height - 1);
const drawTop = borderSides.top;
let shouldDrawTitle = false;
let titleX = startX;
let titleStartX = 0;
let titleEndX = 0;
if (options.title && options.title.length > 0 && drawTop) {
const titleLength = options.title.length;
const minTitleSpace = 4;
shouldDrawTitle = options.width >= titleLength + minTitleSpace;
if (shouldDrawTitle) {
const padding = 2;
if (options.titleAlignment === "center") {
titleX = startX + Math.max(padding, Math.floor((options.width - titleLength) / 2));
} else if (options.titleAlignment === "right") {
titleX = startX + options.width - padding - titleLength;
} else {
titleX = startX + padding;
}
titleX = Math.max(startX + padding, Math.min(titleX, endX - titleLength));
titleStartX = titleX;
titleEndX = titleX + titleLength - 1;
}
}
const drawBottom = borderSides.bottom;
const drawLeft = borderSides.left;
const drawRight = borderSides.right;
const leftBorderOnly = drawLeft && !drawTop && !drawBottom;
const rightBorderOnly = drawRight && !drawTop && !drawBottom;
const bottomOnlyWithVerticals = drawBottom && !drawTop && (drawLeft || drawRight);
const topOnlyWithVerticals = drawTop && !drawBottom && (drawLeft || drawRight);
const extendVerticalsToTop = leftBorderOnly || rightBorderOnly || bottomOnlyWithVerticals;
const extendVerticalsToBottom = leftBorderOnly || rightBorderOnly || topOnlyWithVerticals;
if (drawTop || drawBottom) {
if (drawTop) {
for (let x = startX;x <= endX; x++) {
if (startY >= 0 && startY < buffer.getHeight()) {
let char = borders.horizontal;
if (x === startX) {
char = drawLeft ? borders.topLeft : borders.horizontal;
} else if (x === endX) {
char = drawRight ? borders.topRight : borders.horizontal;
}
if (shouldDrawTitle && x >= titleStartX && x <= titleEndX) {
continue;
}
buffer.setCellWithAlphaBlending(x, startY, char, borderColor, backgroundColor);
}
}
}
if (drawBottom) {
for (let x = startX;x <= endX; x++) {
if (endY >= 0 && endY < buffer.getHeight()) {
let char = borders.horizontal;
if (x === startX) {
char = drawLeft ? borders.bottomLeft : borders.horizontal;
} else if (x === endX) {
char = drawRight ? borders.bottomRight : borders.horizontal;
}
buffer.setCellWithAlphaBlending(x, endY, char, borderColor, backgroundColor);
}
}
}
}
const verticalStartY = extendVerticalsToTop ? startY : startY + (drawTop ? 1 : 0);
const verticalEndY = extendVerticalsToBottom ? endY : endY - (drawBottom ? 1 : 0);
if (drawLeft || drawRight) {
for (let y = verticalStartY;y <= verticalEndY; y++) {
if (drawLeft && startX >= 0 && startX < buffer.getWidth()) {
buffer.setCellWithAlphaBlending(startX, y, borders.vertical, borderColor, backgroundColor);
}
if (drawRight && endX >= 0 && endX < buffer.getWidth()) {
buffer.setCellWithAlphaBlending(endX, y, borders.vertical, borderColor, backgroundColor);
}
}
}
if (shouldDrawTitle && options.title) {
buffer.drawText(options.title, titleX, startY, borderColor, backgroundColor, 0);
}
}
// src/selection.ts
class Selection {
_anchor;
_focus;
_selectedRenderables = [];
constructor(anchor, focus) {
this._anchor = { ...anchor };
this._focus = { ...focus };
}
get anchor() {
return { ...this._anchor };
}
get focus() {
return { ...this._focus };
}
get bounds() {
return {
startX: Math.min(this._anchor.x, this._focus.x),
startY: Math.min(this._anchor.y, this._focus.y),
endX: Math.max(this._anchor.x, this._focus.x),
endY: Math.max(this._anchor.y, this._focus.y)
};
}
updateSelectedRenderables(selectedRenderables) {
this._selectedRenderables = selectedRenderables;
}
getSelectedText() {
const selectedTexts = this._selectedRenderables.sort((a, b) => {
const aY = a.y;
const bY = b.y;
if (aY !== bY) {
return aY - bY;
}
return a.x - b.x;
}).map((renderable) => renderable.getSelectedText()).filter((text) => text);
return selectedTexts.join(`
`);
}
}
class TextSelectionHelper {
getX;
getY;
getTextLength;
getLineInfo;
localSelection = null;
cachedGlobalSelection = null;
constructor(getX, getY, getTextLength, getLineInfo) {
this.getX = getX;
this.getY = getY;
this.getTextLength = getTextLength;
this.getLineInfo = getLineInfo;
}
hasSelection() {
return this.localSelection !== null;
}
getSelection() {
return this.localSelection;
}
reevaluateSelection(width, height = 1) {
if (!this.cachedGlobalSelection) {
return false;
}
return this.onSelectionChanged(this.cachedGlobalSelection, width, height);
}
shouldStartSelection(x, y, width, height) {
const localX = x - this.getX();
const localY = y - this.getY();
return localX >= 0 && localX < width && localY >= 0 && localY < height;
}
onSelectionChanged(selection, width, height = 1) {
this.cachedGlobalSelection = selection;
const previousSelection = this.localSelection;
if (!selection?.isActive) {
this.localSelection = null;
return previousSelection !== null;
}
const myY = this.getY();
const myEndY = myY + height - 1;
if (myEndY < selection.anchor.y || myY > selection.focus.y) {
this.localSelection = null;
return previousSelection !== null;
}
if (height === 1) {
this.localSelection = this.calculateSingleLineSelection(myY, selection.anchor.y, selection.focus.y, selection.anchor.x, selection.focus.x, width);
} else {
this.localSelection = this.calculateMultiLineSelection(myY, selection.anchor.y, selection.focus.y, selection.anchor.x, selection.focus.x);
}
return this.localSelection !== null !== (previousSelection !== null) || this.localSelection?.start !== previousSelection?.start || this.localSelection?.end !== previousSelection?.end;
}
calculateSingleLineSelection(lineY, anchorY, focusY, anchorX, focusX, width) {
const textLength = this.getTextLength();
const myX = this.getX();
if (lineY > anchorY && lineY < focusY) {
return { start: 0, end: textLength };
}
if (lineY === anchorY && lineY === focusY) {
const start = Math.max(0, Math.min(anchorX - myX, textLength));
const end = Math.max(0, Math.min(focusX - myX, textLength));
return start < end ? { start, end } : null;
}
if (lineY === anchorY) {
const start = Math.max(0, Math.min(anchorX - myX, textLength));
return start < textLength ? { start, end: textLength } : null;
}
if (lineY === focusY) {
const end = Math.max(0, Math.min(focusX - myX, textLength));
return end > 0 ? { start: 0, end } : null;
}
return null;
}
calculateMultiLineSelection(startY, anchorY, focusY, anchorX, focusX) {
const lineInfo = this.getLineInfo?.();
if (!lineInfo) {
return { start: 0, end: this.getTextLength() };
}
const myX = this.getX();
let selectionStart = null;
let selectionEnd = null;
for (let i = 0;i < lineInfo.lineStarts.length; i++) {
const lineY = startY + i;
if (lineY < anchorY || lineY > focusY)
continue;
const lineStart = lineInfo.lineStarts[i];
const lineEnd = i < lineInfo.lineStarts.length - 1 ? lineInfo.lineStarts[i + 1] - 1 : this.getTextLength();
const lineWidth = lineInfo.lineWidths[i];
if (lineY > anchorY && lineY < focusY) {
if (selectionStart === null)
selectionStart = lineStart;
selectionEnd = lineEnd;
} else if (lineY === anchorY && lineY === focusY) {
const localStartX = Math.max(0, Math.min(anchorX - myX, lineWidth));
const localEndX = Math.max(0, Math.min(focusX - myX, lineWidth));
if (localStartX < localEndX) {
selectionStart = lineStart + localStartX;
selectionEnd = lineStart + localEndX;
}
} else if (lineY === anchorY) {
const localStartX = Math.max(0, Math.min(anchorX - myX, lineWidth));
if (localStartX < lineWidth) {
selectionStart = lineStart + localStartX;
selectionEnd = lineEnd;
}
} else if (lineY === focusY) {
const localEndX = Math.max(0, Math.min(focusX - myX, lineWidth));
if (localEndX > 0) {
if (selectionStart === null)
selectionStart = lineStart;
selectionEnd = lineStart + localEndX;
}
}
}
return selectionStart !== null && selectionEnd !== null && selectionStart < selectionEnd ? { start: selectionStart, end: selectionEnd } : null;
}
}
// src/objects.ts
function sanitizeText(text, tabStopWidth) {
return text.replace(/\t/g, " ".repeat(tabStopWidth));
}
class TextRenderable extends Renderable {
selectable = true;
_content = "";
_fg;
_bg;
attributes = 0;
tabStopWidth = 2;
selectionHelper;
constructor(id, options) {
super(id, { ...options, width: 0, height: 0 });
const fgRgb = parseColor(options.fg || RGBA.fromInts(255, 255, 255, 255));
this.selectionHelper = new TextSelectionHelper(() => this.x, () => this.y, () => this._content.length);
this.tabStopWidth = options.tabStopWidth || 2;
this.setContent(options.content);
this._fg = fgRgb;
this._bg = options.bg !== undefined ? parseColor(options.bg) : RGBA.fromValues(0, 0, 0, 0);
this.attributes = options.attributes || 0;
}
setContent(value) {
this._content = sanitizeText(value, this.tabStopWidth);
this.width = this._content.length;
this.height = 1;
const changed = this.selectionHelper.reevaluateSelection(this.width);
if (changed) {
this.needsUpdate = true;
}
}
get fg() {
return this._fg;
}
get bg() {
return this._bg;
}
set fg(value) {
if (value) {
this._fg = parseColor(value);
this.needsUpdate = true;
}
}
set bg(value) {
if (value) {
this._bg = parseColor(value);
this.needsUpdate = true;
}
}
set content(value) {
this.setContent(value);
this.needsUpdate = true;
}
get content() {
return this._content;
}
shouldStartSelection(x, y) {
return this.selectionHelper.shouldStartSelection(x, y, this.width, this.height);
}
onSelectionChanged(selection) {
const changed = this.selectionHelper.onSelectionChanged(selection, this.width);
if (changed) {
this.needsUpdate = true;
}
return this.selectionHelper.hasSelection();
}
getSelectedText() {
const selection = this.selectionHelper.getSelection();
if (!selection)
return "";
return this._content.slice(selection.start, selection.end);
}
hasSelection() {
return this.selectionHelper.hasSelection();
}
renderSelf(buffer) {
const selection = this.selectionHelper.getSelection();
buffer.drawText(this._content, this.x, this.y, this._fg, this._bg, this.attributes, selection);
}
}
class BoxRenderable extends Renderable {
_bg;
_border;
_borderStyle;
borderColor;
customBorderChars;
borderSides;
shouldFill;
title;
titleAlignment;
constructor(id, options) {
super(id, options);
const bgRgb = parseColor(options.bg);
const borderRgb = parseColor(options.borderColor || RGBA.fromValues(255, 255, 255, 255));
this.width = options.width;
this.height = options.height;
this._bg = bgRgb;
this._border = options.border ?? true;
this._borderStyle = options.borderStyle || "single";
this.borderColor = borderRgb;
this.customBorderChars = options.customBorderChars || BorderChars[this._borderStyle];
this.borderSides = getBorderSides(this._border);
this.shouldFill = options.shouldFill !== false;
this.title = options.title;
this.titleAlignment = options.titleAlignment || "left";
}
get bg() {
return this._bg;
}
set bg(value) {
if (value) {
this._bg = parseColor(value);
}
}
set border(value) {
this._border = value;
this.borderSides = getBorderSides(value);
this.needsUpdate = true;
}
set borderStyle(value) {
this._borderStyle = value;
this.customBorderChars = BorderChars[this._borderStyle];
this.needsUpdate = true;
}
renderSelf(buffer) {
if (this.x >= buffer.getWidth() || this.y >= buffer.getHeight() || this.x + this.width <= 0 || this.y + this.height <= 0) {
return;
}
const startX = Math.max(0, this.x);
const startY = Math.max(0, this.y);
const endX = Math.min(buffer.getWidth() - 1, this.x + this.width - 1);
const endY = Math.min(buffer.getHeight() - 1, this.y + this.height - 1);
if (this.shouldFill) {
if (this.border === false) {
buffer.fillRect(startX, startY, endX - startX + 1, endY - startY + 1, this._bg);
} else {
const innerStartX = startX + (this.borderSides.left ? 1 : 0);
const innerStartY = startY + (this.borderSides.top ? 1 : 0);
const innerEndX = endX - (this.borderSides.right ? 1 : 0);
const innerEndY = endY - (this.borderSides.bottom ? 1 : 0);
if (innerEndX >= innerStartX && innerEndY >= innerStartY) {
buffer.fillRect(innerStartX, innerStartY, innerEndX - innerStartX + 1, innerEndY - innerStartY + 1, this._bg);
}
}
}
if (this.border !== false) {
drawBorder(buffer, {
x: this.x,
y: this.y,
width: this.width,
height: this.height,
borderStyle: this._borderStyle,
border: this._border,
borderColor: this.borderColor,
backgroundColor: this._bg,
customBorderChars: this.customBorderChars,
title: this.title,
titleAlignment: this.titleAlignment
});
}
}
}
class FrameBufferRenderable extends Renderable {
frameBuffer;
constructor(id, buffer, options) {
super(id, options);
this.frameBuffer = buffer;
}
renderSelf(buffer) {
buffer.drawFrameBuffer(this.x, this.y, this.frameBuffer);
}
destroySelf() {
this.frameBuffer.destroy();
}
}
class GroupRenderable extends Renderable {
constructor(id, options) {
super(id, { ...options, width: 0, height: 0 });
}
}
class StyledTextRenderable extends Renderable {
selectable = true;
frameBuffer;
_fragment;
_defaultFg;
_defaultBg;
_selectionBg;
_selectionFg;
selectionHelper;
_plainText = "";
_lineInfo = { lineStarts: [], lineWidths: [] };
constructor(id, buffer, options) {
super(id, options);
this.selectionHelper = new TextSelectionHelper(() => this.x, () => this.y, () => this._plainText.length, () => this._lineInfo);
this.frameBuffer = buffer;
this._fragment = options.fragment;
this._defaultFg = options.defaultFg ? parseColor(options.defaultFg) : RGBA.fromValues(1, 1, 1, 1);
this._defaultBg = options.defaultBg ? parseColor(options.defaultBg) : RGBA.fromValues(0, 0, 0, 0);
this._selectionBg = options.selectionBg ? parseColor(options.selectionBg) : undefined;
this._selectionFg = options.selectionFg ? parseColor(options.selectionFg) : undefined;
this.updateTextInfo();
this.renderFragmentToBuffer();
}
get fragment() {
return this._fragment;
}
set fragment(value) {
this._fragment = value;
this.updateTextInfo();
this.renderFragmentToBuffer();
this.needsUpdate = true;
}
get defaultFg() {
return this._defaultFg;
}
set defaultFg(value) {
if (value) {
this._defaultFg = parseColor(value);
this.renderFragmentToBuffer();
this.needsUpdate = true;
}
}
get defaultBg() {
return this._defaultBg;
}
set defaultBg(value) {
if (value) {
this._defaultBg = parseColor(value);
this.renderFragmentToBuffer();
this.needsUpdate = true;
}
}
updateTextInfo() {
this._plainText = this._fragment.toString();
this._lineInfo.lineStarts = [0];
this._lineInfo.lineWidths = [];
let currentLineWidth = 0;
for (let i = 0;i < this._plainText.length; i++) {
if (this._plainText[i] === `
`) {
this._lineInfo.lineWidths.push(currentLineWidth);
this._lineInfo.lineStarts.push(i + 1);
currentLineWidth = 0;
} else {
currentLineWidth++;
}
}
this._lineInfo.lineWidths.push(currentLineWidth);
const changed = this.selectionHelper.reevaluateSelection(this.width, this.height);
if (changed) {
this.renderFragmentToBuffer();
this.needsUpdate = true;
}
}
shouldStartSelection(x, y) {
return this.selectionHelper.shouldStartSelection(x, y, this.width, this.height);
}
onSelectionChanged(selection) {
const changed = this.selectionHelper.onSelectionChanged(selection, this.width, this.height);
if (changed) {
this.renderFragmentToBuffer();
this.needsUpdate = true;
}
return this.selectionHelper.hasSelection();
}
getSelectedText() {
const selection = this.selectionHelper.getSelection();
if (!selection)
return "";
return this._plainText.slice(selection.start, selection.end);
}
hasSelection() {
return this.selectionHelper.hasSelection();
}
renderFragmentToBuffer() {
this.frameBuffer.clear(this._defaultBg);
const selection = this.selectionHelper.getSelection();
this.frameBuffer.drawStyledTextFragment(this._fragment, 0, 0, this._defaultFg, this._defaultBg, selection ? { ...selection, bgColor: this._selectionBg, fgColor: this._selectionFg } : undefined);
}
renderSelf(buffer) {
buffer.drawFrameBuffer(this.x, this.y, this.frameBuffer);
}
destroySelf() {
this.frameBuffer.destroy();
}
}
// src/zig.ts
import { dlopen, suffix, toArrayBuffer } from "bun:ffi";
import { join } from "path";
import { existsSync } from "fs";
import os from "os";
// src/buffer.ts
var fbIdCounter = 0;
function isRGBAWithAlpha(color) {
return color.a < 1;
}
function blendColors(overlay, text) {
const [overlayR, overlayG, overlayB, overlayA] = overlay.buffer;
const [textR, textG, textB, textA] = text.buffer;
if (overlayA === 1) {
return overlay;
}
const alpha = overlayA;
let perceptualAlpha;
if (alpha > 0.8) {
const normalizedHighAlpha = (alpha - 0.8) * 5;
const curvedHighAlpha = Math.pow(normalizedHighAlpha, 0.2);
perceptualAlpha = 0.8 + curvedHighAlpha * 0.2;
} else {
perceptualAlpha = Math.pow(alpha, 0.9);
}
const r = overlayR * perceptualAlpha + textR * (1 - perceptualAlpha);
const g = overlayG * perceptualAlpha + textG * (1 - perceptualAlpha);
const b = overlayB * perceptualAlpha + textB * (1 - perceptualAlpha);
return RGBA.fromValues(r, g, b, textA);
}
class OptimizedBuffer {
id;
lib;
bufferPtr;
buffer;
width;
height;
respectAlpha = false;
useFFI = true;
get ptr() {
return this.bufferPtr;
}
constructor(lib, ptr, buffer, width, height, options) {
this.id = `fb_${fbIdCounter++}`;
this.lib = lib;
this.respectAlpha = options.respectAlpha || false;
this.width = width;
this.height = height;
this.bufferPtr = ptr;
this.buffer = buffer;
}
static create(width, height, options = {}) {
const lib = resolveRenderLib();
const respectAlpha = options.respectAlpha || false;
return lib.createOptimizedBuffer(width, height, respectAlpha);
}
get buffers() {
return this.buffer;
}
coordsToIndex(x, y) {
return y * this.width + x;
}
getWidth() {
return this.width;
}
getHeight() {
return this.height;
}
setRespectAlpha(respectAlpha) {
this.lib.bufferSetRespectAlpha(this.bufferPtr, respectAlpha);
this.respectAlpha = respectAlpha;
}
clear(bg = RGBA.fromValues(0, 0, 0, 1)) {
if (this.useFFI) {
this.clearFFI(bg);
} else {
this.clearLocal(bg);
}
}
clearLocal(bg = RGBA.fromValues(0, 0, 0, 1)) {
this.buffer.char.fill(32);
this.buffer.attributes.fill(0);
for (let i = 0;i < this.width * this.height; i++) {
const index = i * 4;
this.buffer.fg[index] = 1;
this.buffer.fg[index + 1] = 1;
this.buffer.fg[index + 2] = 1;
this.buffer.fg[index + 3] = 1;
this.buffer.bg[index] = bg.r;
this.buffer.bg[index + 1] = bg.g;
this.buffer.bg[index + 2] = bg.b;
this.buffer.bg[index + 3] = bg.a;
}
}
setCell(x, y, char, fg, bg, attributes = 0) {
if (x < 0 || x >= this.width || y < 0 || y >= this.height)
return;
const index = this.coordsToIndex(x, y);
const colorIndex = index * 4;
this.buffer.char[index] = char.charCodeAt(0);
this.buffer.attributes[index] = attributes;
this.buffer.fg[colorIndex] = fg.r;
this.buffer.fg[colorIndex + 1] = fg.g;
this.buffer.fg[colorIndex + 2] = fg.b;
this.buffer.fg[colorIndex + 3] = fg.a;
this.buffer.bg[colorIndex] = bg.r;
this.buffer.bg[colorIndex + 1] = bg.g;
this.buffer.bg[colorIndex + 2] = bg.b;
this.buffer.bg[colorIndex + 3] = bg.a;
}
get(x, y) {
if (x < 0 || x >= this.width || y < 0 || y >= this.height)
return null;
const index = this.coordsToIndex(x, y);
const colorIndex = index * 4;
return {
char: this.buffer.char[index],
fg: RGBA.fromArray(this.buffer.fg.slice(colorIndex, colorIndex + 4)),
bg: RGBA.fromArray(this.buffer.bg.slice(colorIndex, colorIndex + 4)),
attributes: this.buffer.attributes[index]
};
}
setCellWithAlphaBlending(x, y, char, fg, bg, attributes = 0) {
if (this.useFFI) {
this.setCellWithAlphaBlendingFFI(x, y, char, fg, bg, attributes);
} else {
this.setCellWithAlphaBlendingLocal(x, y, char, fg, bg, attributes);
}
}
setCellWithAlphaBlendingLocal(x, y, char, fg, bg, attributes = 0) {
if (x < 0 || x >= this.width || y < 0 || y >= this.height)
return;
const hasBgAlpha = isRGBAWithAlpha(bg);
const hasFgAlpha = isRGBAWithAlpha(fg);
if (hasBgAlpha || hasFgAlpha) {
const destCell = this.get(x, y);
if (destCell) {
const blendedBgRgb = hasBgAlpha ? blendColors(bg, destCell.bg) : bg;
const preserveChar = char === " " && destCell.char !== 0 && String.fromCharCode(destCell.char) !== " ";
const finalChar = preserveChar ? destCell.char : char.charCodeAt(0);
let finalFg;
if (preserveChar) {
finalFg = blendColors(bg, destCell.fg);
} else {
finalFg = hasFgAlpha ? blendColors(fg, destCell.bg) : fg;
}
const finalAttributes = preserveChar ? destCell.attributes : attributes;
const finalBg = RGBA.fromValues(blendedBgRgb.r, blendedBgRgb.g, blendedBgRgb.b, bg.a);
this.setCell(x, y, String.fromCharCode(finalChar), finalFg, finalBg, finalAttributes);
return;
}
}
this.setCell(x, y, char, fg, bg, attributes);
}
drawText(text, x, y, fg, bg, attributes = 0, selection) {
const method = this.useFFI ? this.drawTextFFI : this.drawTextLocal;
if (!selection) {
method.call(this, text, x, y, fg, bg, attributes);
return;
}
const { start, end } = selection;
let selectionBg;
let selectionFg;
if (selection.bgColor) {
selectionBg = selection.bgColor;
selectionFg = selection.fgColor || fg;
} else {
const defaultBg = bg || RGBA.fromValues(0, 0, 0, 0);
selectionFg = defaultBg.a > 0 ? defaultBg : RGBA.fromValues(0, 0, 0, 1);
selectionBg = fg;
}
if (start > 0) {
const beforeText = text.slice(0, start);
method.call(this, beforeText, x, y, fg, bg, attributes);
}
if (end > start) {
const selectedText = text.slice(start, end);
method.call(this, selectedText, x + start, y, selectionFg, selectionBg, attributes);
}
if (end < text.length) {
const afterText = text.slice(end);
method.call(this, afterText, x + end, y, fg, bg, attributes);
}
}
drawTextLocal(text, x, y, fg, bg, attributes = 0) {
if (y < 0 || y >= this.height)
return;
if (!text || typeof text !== "string") {
console.warn("drawTextLocal called with invalid text:", { text, x, y, fg, bg });
return;
}
let startX = this.width;
let endX = 0;
let i = 0;
for (const char of text) {
const charX = x + i;
i++;
if (charX < 0 || charX >= this.width)
continue;
startX = Math.min(startX, charX);
endX = Math.max(endX, charX);
let bgColor = bg;
if (!bgColor) {
const existingCell = this.get(charX, y);
if (existingCell) {
bgColor = existingCell.bg;
} else {
bgColor = RGBA.fromValues(0, 0, 0, 1);
}
}
this.setCellWithAlphaBlending(charX, y, char, fg, bgColor, attributes);
}
}
fillRect(x, y, width, height, bg) {
if (this.useFFI) {
this.fillRectFFI(x, y, width, height, bg);
} else {
this.fillRectLocal(x, y, width, height, bg);
}
}
fillRectLocal(x, y, width, height, bg) {
const startX = Math.max(0, x);
const startY = Math.max(0, y);
const endX = Math.min(this.getWidth() - 1, x + width - 1);
const endY = Math.min(this.getHeight() - 1, y + height - 1);
if (startX > endX || startY > endY)
return;
const hasAlpha = isRGBAWithAlpha(bg);
if (hasAlpha) {
const fg = RGBA.fromValues(1, 1, 1, 1);
for (let fillY = startY;fillY <= endY; fillY++) {
for (let fillX = startX;fillX <= endX; fillX++) {
this.setCellWithAlphaBlending(fillX, fillY, " ", fg, bg, 0);
}
}
} else {
for (let fillY = startY;fillY <= endY; fillY++) {
for (let fillX = startX;fillX <= endX; fillX++) {
const index = this.coordsToIndex(fillX, fillY);
const colorIndex = index * 4;
this.buffer.char[index] = 32;
this.buffer.attributes[index] = 0;
this.buffer.fg[colorIndex] = 1;
this.buffer.fg[colorIndex + 1] = 1;
this.buffer.fg[colorIndex + 2] = 1;
this.buffer.fg[colorIndex + 3] = 1;
this.buffer.bg[colorIndex] = bg.r;
this.buffer.bg[colorIndex + 1] = bg.g;
this.buffer.bg[colorIndex + 2] = bg.b;
this.buffer.bg[colorIndex + 3] = bg.a;
}
}
}
}
drawFrameBuffer(destX, destY, frameBuffer, sourceX, sourceY, sourceWidth, sourceHeight) {
this.drawFrameBufferFFI(destX, destY, frameBuffer, sourceX, sourceY, sourceWidth, sourceHeight);
}
drawFrameBufferLocal(destX, destY, frameBuffer, sourceX, sourceY, sourceWidth, sourceHeight) {
const srcX = sourceX ?? 0;
const srcY = sourceY ?? 0;
const srcWidth = sourceWidth ?? frameBuffer.getWidth();
const srcHeight = sourceHeight ?? frameBuffer.getHeight();
if (srcX >= frameBuffer.getWidth() || srcY >= frameBuffer.getHeight())
return;
if (srcWidth === 0 || srcHeight === 0)
return;
const clampedSrcWidth = Math.min(srcWidth, frameBuffer.getWidth() - srcX);
const clampedSrcHeight = Math.min(srcHeight, frameBuffer.getHeight() - srcY);
const startDestX = Math.max(0, destX);
const startDestY = Math.max(0, destY);
const endDestX = Math.min(this.width - 1, destX + clampedSrcWidth -