v0-ui-reviewer
Version:
Next-gen UI/UX reviewer with multi-model AI support (OpenAI, Claude, v0), style extraction, and live preview sandbox
159 lines (158 loc) ⢠6.48 kB
JavaScript
import { execSync } from 'child_process';
import { promises as fs } from 'fs';
import path from 'path';
import chalk from 'chalk';
import sharp from 'sharp';
import terminalImage from 'terminal-image';
export class HDImageDisplay {
terminalType = null;
constructor() {
this.detectTerminal();
}
detectTerminal() {
const term = process.env.TERM || '';
const termProgram = process.env.TERM_PROGRAM || '';
if (term.includes('kitty') || process.env.KITTY_WINDOW_ID) {
this.terminalType = 'kitty';
}
else if (termProgram === 'iTerm.app') {
this.terminalType = 'iterm2';
}
else if (this.checkSixelSupport()) {
this.terminalType = 'sixel';
}
else {
this.terminalType = 'ascii';
}
}
checkSixelSupport() {
try {
// Check if img2sixel is available
execSync('which img2sixel', { stdio: 'ignore' });
// Check terminal capabilities
const terminfo = process.env.TERM || '';
return terminfo.includes('sixel') || terminfo.includes('xterm');
}
catch {
return false;
}
}
async displayImage(imagePath, options = {}) {
const { width = process.stdout.columns - 4, height = Math.floor(process.stdout.rows * 0.7), preserveAspectRatio = true, verbose = false, forceMethod } = options;
try {
await fs.access(imagePath);
const metadata = await sharp(imagePath).metadata();
if (!verbose) {
console.log(chalk.gray(`\nš· Screenshot: ${path.basename(imagePath)}`));
console.log(chalk.gray(`š Dimensions: ${metadata.width}x${metadata.height}px`));
}
const method = forceMethod || this.terminalType;
if (verbose) {
console.log(chalk.gray(`š¼ļø Display method: ${method}`));
}
switch (method) {
case 'kitty':
await this.displayKitty(imagePath, { width, height });
break;
case 'iterm2':
await this.displayITerm2(imagePath, { width, height });
break;
case 'sixel':
await this.displaySixel(imagePath, { width, height });
break;
default:
await this.displayASCII(imagePath, { width, height, preserveAspectRatio });
}
if (!verbose) {
console.log(chalk.gray('ā'.repeat(Math.min(width, process.stdout.columns - 1)) + '\n'));
}
}
catch (error) {
if (verbose) {
console.warn(chalk.yellow(`ā ļø Could not display image: ${error instanceof Error ? error.message : String(error)}`));
}
console.log(chalk.blue(`š Screenshot saved: ${imagePath}\n`));
}
}
async displayKitty(imagePath, options) {
try {
// Use kitty's icat kitten for best quality
const { width, height } = options;
const cols = Math.min(width, process.stdout.columns);
const rows = Math.min(height, process.stdout.rows - 4);
// Calculate pixel dimensions (assuming 8x16 cell size)
const pixelWidth = cols * 8;
const pixelHeight = rows * 16;
execSync(`kitty +kitten icat --place=${pixelWidth}x${pixelHeight}@0x0 --scale-up "${imagePath}"`, {
stdio: 'inherit'
});
}
catch (error) {
// Fallback to inline protocol
await this.displayKittyInline(imagePath);
}
}
async displayKittyInline(imagePath) {
const data = await fs.readFile(imagePath);
const base64 = data.toString('base64');
// Kitty graphics protocol
const chunks = [];
const chunkSize = 4096;
for (let i = 0; i < base64.length; i += chunkSize) {
const chunk = base64.slice(i, i + chunkSize);
const isLast = i + chunkSize >= base64.length;
const control = isLast ? 'm=0' : 'm=1';
chunks.push(`\x1b_Gf=100,t=d,${control};${chunk}\x1b\\`);
}
process.stdout.write(chunks.join(''));
process.stdout.write('\n');
}
async displayITerm2(imagePath, options) {
const data = await fs.readFile(imagePath);
const base64 = data.toString('base64');
const name = Buffer.from(path.basename(imagePath)).toString('base64');
// iTerm2 inline images protocol
const sequence = `\x1b]1337;File=name=${name};inline=1;width=${options.width};height=${options.height};preserveAspectRatio=1:${base64}\x07`;
process.stdout.write(sequence);
process.stdout.write('\n');
}
async displaySixel(imagePath, options) {
try {
// Calculate pixel dimensions
const pixelWidth = Math.min(options.width * 8, 1920);
const pixelHeight = Math.min(options.height * 16, 1080);
// Use img2sixel with 24-bit color if available
const sixelCmd = `img2sixel -w ${pixelWidth} -h ${pixelHeight} "${imagePath}"`;
execSync(sixelCmd, { stdio: 'inherit' });
}
catch (error) {
// Fallback to ASCII
await this.displayASCII(imagePath, { ...options, preserveAspectRatio: true });
}
}
async displayASCII(imagePath, options) {
// For terminals without graphics support, resize for performance
const maxWidth = Math.min(options.width, 120);
const maxHeight = Math.min(options.height, 60);
// Resize image for better terminal display
const resized = await sharp(imagePath)
.resize(maxWidth * 4, maxHeight * 8, {
fit: 'inside',
withoutEnlargement: true
})
.toBuffer();
// Use terminal-image for ASCII art
const image = await terminalImage.buffer(resized, {
width: maxWidth,
height: maxHeight,
preserveAspectRatio: options.preserveAspectRatio
});
console.log('\n' + image);
}
// Helper to show images inline in Node/TS
static async showImage(file) {
const display = new HDImageDisplay();
await display.displayImage(file, { verbose: false });
}
}
export default HDImageDisplay;