@kya-os/cli
Version:
CLI for MCP-I setup and management
280 lines • 8.06 kB
JavaScript
/**
* Utility functions for terminal effects
*/
import { isRGBColor, isXTermColor, } from "./types.js";
/**
* ANSI escape codes
*/
export const ANSI = {
reset: "\x1b[0m",
cursor: {
hide: "\x1b[?25l",
show: "\x1b[?25h",
save: "\x1b[s",
restore: "\x1b[u",
home: "\x1b[H",
moveTo: (x, y) => `\x1b[${y + 1};${x + 1}H`,
},
clear: {
screen: "\x1b[2J",
line: "\x1b[2K",
toEnd: "\x1b[0J",
toStart: "\x1b[1J",
},
color: {
fg: (color) => {
if (isRGBColor(color)) {
const r = parseInt(color.substr(0, 2), 16);
const g = parseInt(color.substr(2, 2), 16);
const b = parseInt(color.substr(4, 2), 16);
return `\x1b[38;2;${r};${g};${b}m`;
}
else if (isXTermColor(color)) {
return `\x1b[38;5;${color}m`;
}
return "";
},
bg: (color) => {
if (isRGBColor(color)) {
const r = parseInt(color.substr(0, 2), 16);
const g = parseInt(color.substr(2, 2), 16);
const b = parseInt(color.substr(4, 2), 16);
return `\x1b[48;2;${r};${g};${b}m`;
}
else if (isXTermColor(color)) {
return `\x1b[48;5;${color}m`;
}
return "";
},
},
};
/**
* Detect terminal capabilities
*/
export function detectTerminalCapabilities() {
const isInteractive = process.stdout.isTTY || false;
const colorLevel = getColorLevel();
return {
supportsColor: colorLevel > 0,
supports256Color: colorLevel >= 2,
supportsTrueColor: colorLevel >= 3,
dimensions: getTerminalDimensions(),
isInteractive,
};
}
/**
* Get color support level
* 0 = no color, 1 = basic color, 2 = 256 color, 3 = true color
*/
function getColorLevel() {
if (process.env.NO_COLOR || process.env.TERM === "dumb") {
return 0;
}
if (process.env.COLORTERM === "truecolor" ||
process.env.TERM_PROGRAM === "iTerm.app") {
return 3;
}
if (process.env.TERM && /256/.test(process.env.TERM)) {
return 2;
}
if (process.stdout.hasColors?.()) {
return process.stdout.hasColors(256) ? 2 : 1;
}
return 0;
}
/**
* Get terminal dimensions
*/
export function getTerminalDimensions() {
if (process.stdout.isTTY) {
return {
width: process.stdout.columns || 80,
height: process.stdout.rows || 24,
};
}
return { width: 80, height: 24 };
}
/**
* Convert RGB hex to XTerm-256 color
* Based on the algorithm from https://github.com/Qix-/color-convert
*/
export function rgbToXterm(hex) {
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
// Check for exact matches in the basic 16 colors
const basicColors = [
[0, 0, 0],
[128, 0, 0],
[0, 128, 0],
[128, 128, 0],
[0, 0, 128],
[128, 0, 128],
[0, 128, 128],
[192, 192, 192],
[128, 128, 128],
[255, 0, 0],
[0, 255, 0],
[255, 255, 0],
[0, 0, 255],
[255, 0, 255],
[0, 255, 255],
[255, 255, 255],
];
for (let i = 0; i < basicColors.length; i++) {
const [br, bg, bb] = basicColors[i];
if (r === br && g === bg && b === bb) {
return i;
}
}
// Check grayscale
if (r === g && g === b) {
if (r < 8)
return 16;
if (r > 248)
return 231;
return Math.round(((r - 8) / 247) * 24) + 232;
}
// Use 6x6x6 color cube
const levels = [0, 95, 135, 175, 215, 255];
const findClosest = (value) => {
let minDist = Infinity;
let closest = 0;
for (let i = 0; i < levels.length; i++) {
const dist = Math.abs(value - levels[i]);
if (dist < minDist) {
minDist = dist;
closest = i;
}
}
return closest;
};
const ri = findClosest(r);
const gi = findClosest(g);
const bi = findClosest(b);
return 16 + 36 * ri + 6 * gi + bi;
}
/**
* Strip ANSI codes from string
*/
export function stripAnsi(str) {
// eslint-disable-next-line no-control-regex
return str.replace(/\x1b\[[0-9;]*m/g, "");
}
/**
* Measure the visible length of a string (excluding ANSI codes)
*/
export function measureString(str) {
return stripAnsi(str).length;
}
/**
* Pad string to specified width (accounting for ANSI codes)
*/
export function padString(str, width, align = "left") {
const visibleLength = measureString(str);
const padding = Math.max(0, width - visibleLength);
switch (align) {
case "right":
return " ".repeat(padding) + str;
case "center":
const leftPad = Math.floor(padding / 2);
const rightPad = padding - leftPad;
return " ".repeat(leftPad) + str + " ".repeat(rightPad);
default:
return str + " ".repeat(padding);
}
}
/**
* Create a buffer for double-buffered rendering
*/
export class RenderBuffer {
constructor(width, height) {
this.buffer = [];
this.width = width;
this.height = height;
this.clear();
}
clear() {
this.buffer = Array(this.height)
.fill(null)
.map(() => Array(this.width).fill(" "));
}
setChar(x, y, char, color) {
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
this.buffer[y][x] = color ? `${color}${char}${ANSI.reset}` : char;
}
}
toString() {
return this.buffer.map((row) => row.join("")).join("\n");
}
getLines() {
return this.buffer.map((row) => row.join(""));
}
}
/**
* Sleep for specified milliseconds
*/
export function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Calculate FPS delay from frame rate
*/
export function calculateFrameDelay(fps) {
if (fps <= 0)
return 0;
return Math.floor(1000 / fps);
}
/**
* Easing functions for smooth animations
*/
export const Easing = {
linear: (t) => t,
easeIn: (t) => t * t,
easeOut: (t) => t * (2 - t),
easeInOut: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
easeInCubic: (t) => t * t * t,
easeOutCubic: (t) => --t * t * t + 1,
easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
};
// Export individual easing functions for convenience
export const easeInQuad = Easing.easeIn;
export const easeOutQuad = Easing.easeOut;
export const easeInOutQuad = Easing.easeInOut;
export const easeInOutSine = Easing.easeInOutSine;
/**
* Interpolate between two values
*/
export function lerp(start, end, t) {
return start + (end - start) * t;
}
/**
* Clamp a value between min and max
*/
export function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
/**
* Create a gradient between two colors
*/
export function createGradient(startColor, endColor, steps) {
const colors = [];
const sr = parseInt(startColor.substr(0, 2), 16);
const sg = parseInt(startColor.substr(2, 2), 16);
const sb = parseInt(startColor.substr(4, 2), 16);
const er = parseInt(endColor.substr(0, 2), 16);
const eg = parseInt(endColor.substr(2, 2), 16);
const eb = parseInt(endColor.substr(4, 2), 16);
for (let i = 0; i < steps; i++) {
const t = i / (steps - 1);
const r = Math.round(lerp(sr, er, t));
const g = Math.round(lerp(sg, eg, t));
const b = Math.round(lerp(sb, eb, t));
colors.push(r.toString(16).padStart(2, "0") +
g.toString(16).padStart(2, "0") +
b.toString(16).padStart(2, "0"));
}
return colors;
}
//# sourceMappingURL=utils.js.map