diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 56ff59b..fe8d034 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,7 +2,13 @@ "permissions": { "allow": [ "Bash(cat:*)", - "Bash(npm install:*)" + "Bash(npm install:*)", + "Bash(node test-poem.js:*)", + "Bash(printf:*)", + "Bash(timeout 2 node:*)", + "Bash(npm ls:*)", + "Bash(timeout 1 node:*)", + "Bash(node -e:*)" ] } } diff --git a/cli.js b/cli.js index 481b494..a7f2bbd 100644 --- a/cli.js +++ b/cli.js @@ -1,35 +1,35 @@ #!/usr/bin/env node import color from "picocolors"; -import { checkToktx, scanImages, compressAll } from "./lib/compressor.js"; -import { runInteractive, showSummary } from "./lib/ui.js"; +import { showMainMenu } from "./lib/menu.js"; -// 检查 toktx -checkToktx(); +// 主循环 +while (true) { + const selected = await showMainMenu(); -// 运行交互界面 -const [exts, quality, encoding, mipmap, outputOpts] = await runInteractive(); + console.clear(); + console.log(color.cyan(`\n正在启动: ${selected.name}...\n`)); -const config = { exts, quality, encoding, mipmap, outputOpts }; - -// 显示配置摘要 -showSummary(config); - -// 扫描文件 -const { images, cwd } = scanImages(exts); - -if (images.length === 0) { - console.log(color.yellow("当前目录没有匹配的图片")); - process.exit(0); -} - -console.log(`📁 找到 ${color.cyan(images.length)} 个待转换文件\n`); - -// 执行压缩 -const { total, failed } = await compressAll(images, config, cwd); - -// 显示结果 -if (failed > 0) { - console.log(color.yellow(`\n⚠️ 完成,但有 ${failed} 个文件失败`)); -} else { - console.log(color.green("\n🎉 全部文件压缩完成!")); + try { + const tool = await import(selected.module); + const result = await tool.run(); + + // 返回主菜单 + if (result === "back") continue; + + // 工具完成后退出 + break; + } catch (err) { + console.log(color.yellow(`\n⚠️ ${selected.name} 模块尚未实现`)); + console.log(color.dim(err.message)); + console.log(color.dim("\n按任意键返回主菜单...")); + + // 等待按键 + await new Promise(resolve => { + process.stdin.setRawMode(true); + process.stdin.once("data", () => { + process.stdin.setRawMode(false); + resolve(); + }); + }); + } } diff --git a/lib/bigtext.js b/lib/bigtext.js new file mode 100644 index 0000000..e357d4e --- /dev/null +++ b/lib/bigtext.js @@ -0,0 +1,46 @@ +import color from "picocolors"; + +// 计算字符串显示宽度(中文2,英文1) +function strWidth(str) { + let width = 0; + for (const char of str) { + width += char.charCodeAt(0) > 127 ? 2 : 1; + } + return width; +} + +// 大字效果框 +export function bigText(text, options = {}) { + const { + style = "double", + textColor = "yellow", + padding = 2, + } = options; + + const chars = { + block: { tl: "█", tr: "█", bl: "█", br: "█", h: "█", v: "█" }, + double: { tl: "╔", tr: "╗", bl: "╚", br: "╝", h: "═", v: "║" }, + simple: { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│" }, + }; + + const c = chars[style] || chars.double; + const lines = text.split("\n"); + const maxWidth = Math.max(...lines.map(strWidth)); + const boxWidth = maxWidth + padding * 2; + + const result = []; + result.push(c.tl + c.h.repeat(boxWidth) + c.tr); + result.push(c.v + " ".repeat(boxWidth) + c.v); + + for (const line of lines) { + const lineWidth = strWidth(line); + const rightPad = maxWidth - lineWidth; + result.push(c.v + " ".repeat(padding) + line + " ".repeat(rightPad + padding) + c.v); + } + + result.push(c.v + " ".repeat(boxWidth) + c.v); + result.push(c.bl + c.h.repeat(boxWidth) + c.br); + + const box = result.join("\n"); + return color[textColor] ? color[textColor](box) : box; +} diff --git a/lib/grid.js b/lib/grid.js new file mode 100644 index 0000000..45e315f --- /dev/null +++ b/lib/grid.js @@ -0,0 +1,119 @@ +import color from "picocolors"; +import { initKeypress, onKey } from "./keyboard.js"; + +// 计算字符串显示宽度 +function strWidth(str) { + let width = 0; + for (const char of str) { + width += char.charCodeAt(0) > 127 ? 2 : 1; + } + return width; +} + +// 右填充到指定宽度 +function padEnd(str, width) { + const diff = width - strWidth(str); + return str + " ".repeat(Math.max(0, diff)); +} + +/** + * 网格选择菜单 + */ +export async function gridSelect(options) { + const { + items, + cols = 3, + colWidth = 24, + title = "", + renderHeader = null + } = options; + + let current = 0; + const rows = Math.ceil(items.length / cols); + const termWidth = process.stdout.columns || 80; + const totalWidth = cols * colWidth; + const pad = " ".repeat(Math.max(0, Math.floor((termWidth - totalWidth) / 2))); + + function render() { + console.clear(); + + if (renderHeader) { + console.log(renderHeader()); + } + + console.log(""); + + if (title) { + const titlePad = " ".repeat(Math.max(0, Math.floor((termWidth - title.length - 4) / 2))); + console.log(titlePad + color.bgMagenta(color.white(` ${title} `))); + } + + console.log(""); + console.log(pad + color.dim("↑ ↓ ← → 选择 | Enter 确认 | Esc 退出")); + console.log(""); + console.log(""); + + for (let row = 0; row < rows; row++) { + let line = ""; + for (let col = 0; col < cols; col++) { + const idx = row * cols + col; + if (idx < items.length) { + const item = items[idx]; + if (idx === current) { + line += color.cyan("[" + item.name + "]"); + line += " ".repeat(colWidth - strWidth(item.name) - 2); + } else { + line += " " + item.name + " "; + line += " ".repeat(colWidth - strWidth(item.name) - 2); + } + } + } + console.log(pad + line); + + let descLine = ""; + for (let col = 0; col < cols; col++) { + const idx = row * cols + col; + if (idx < items.length) { + descLine += padEnd(items[idx].desc || "", colWidth); + } + } + console.log(pad + color.dim(descLine)); + console.log(""); + console.log(""); + } + } + + initKeypress(); + + return new Promise((resolve) => { + render(); + + onKey((str, key) => { + if (!key) return; + + const row = Math.floor(current / cols); + const col = current % cols; + + if (key.name === "up" && row > 0) { + current -= cols; + render(); + } else if (key.name === "down" && row < rows - 1 && current + cols < items.length) { + current += cols; + render(); + } else if (key.name === "left" && col > 0) { + current--; + render(); + } else if (key.name === "right" && col < cols - 1 && current < items.length - 1) { + current++; + render(); + } else if (key.name === "return") { + setImmediate(() => resolve(items[current])); + } else if (key.name === "escape" || (key.ctrl && key.name === "c")) { + process.stdin.setRawMode(false); + console.clear(); + console.log(color.yellow("👋 再见!")); + process.exit(0); + } + }); + }); +} diff --git a/lib/keyboard.js b/lib/keyboard.js new file mode 100644 index 0000000..98142e3 --- /dev/null +++ b/lib/keyboard.js @@ -0,0 +1,18 @@ +import readline from "readline"; + +let initialized = false; + +export function initKeypress() { + if (!initialized) { + readline.emitKeypressEvents(process.stdin); + initialized = true; + } + process.stdin.removeAllListeners("keypress"); + if (process.stdin.isTTY) { + process.stdin.setRawMode(true); + } +} + +export function onKey(handler) { + process.stdin.on("keypress", handler); +} diff --git a/lib/ktx2/compressor.js b/lib/ktx2/compressor.js new file mode 100644 index 0000000..87072f1 --- /dev/null +++ b/lib/ktx2/compressor.js @@ -0,0 +1,115 @@ +import fs from "fs"; +import path from "path"; +import { spawn } from "child_process"; +import color from "picocolors"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const toktx = path.join(__dirname, "..", "..", "bin", "toktx.exe"); + +// 检查 toktx 是否存在 +export function checkToktx() { + if (!fs.existsSync(toktx)) { + console.error("❌ 找不到 toktx.exe"); + process.exit(1); + } +} + +// 扫描图片文件 +export function scanImages(exts) { + console.log("🔍 扫描目标文件中..."); + const cwd = process.cwd(); + const images = fs.readdirSync(cwd).filter(f => + exts.some(ext => f.toLowerCase().endsWith("." + ext)) + ); + return { images, cwd }; +} + +// 构建压缩参数 +export function buildArgs(input, output, config) { + const args = ["--t2"]; + + if (config.encoding === "uastc") { + args.push("--encode", "uastc"); + const zcmpLevel = { none: "0", standard: "10", high: "18", extreme: "22" }; + args.push("--zcmp", zcmpLevel[config.quality] || "10"); + } else if (config.encoding === "etc1s") { + args.push("--encode", "etc1s"); + } else if (config.encoding === "astc") { + args.push("--encode", "astc"); + const blkSize = { none: "8x8", standard: "6x6", high: "5x5", extreme: "4x4" }; + args.push("--astc_blk_d", blkSize[config.quality] || "6x6"); + } + + if (config.mipmap === "auto") { + args.push("--genmipmap"); + } + + args.push(output, input); + return args; +} + +// 压缩单个文件 +export function compressFile(img, config, cwd, progress) { + const baseName = img.replace(/\.[^.]+$/, ""); + const out = baseName + ".ktx2"; + + // 点动画 + let dots = 0; + const dotAnim = setInterval(() => { + const dotStr = ".".repeat(dots); + process.stdout.write(`\r${progress} ${img} 正在转换中${dotStr} `); + dots = dots >= 3 ? 0 : dots + 1; + }, 300); + + process.stdout.write(`${progress} ${img} 正在转换中.`); + + const args = buildArgs(img, out, config); + + return new Promise((resolve) => { + const proc = spawn(toktx, args, { cwd }); + + let stderr = ""; + proc.stderr?.on("data", data => { + stderr += data.toString(); + }); + + proc.on("close", code => { + clearInterval(dotAnim); + if (code === 0) { + console.log(`\r${progress} ${color.green("✓")} ${out} `); + resolve({ success: true }); + } else { + console.log(`\r${progress} ${color.red("✗")} ${img} 失败 `); + if (stderr) { + console.log(color.dim(` 错误: ${stderr.trim()}`)); + } + resolve({ success: false, error: stderr }); + } + }); + + proc.on("error", err => { + clearInterval(dotAnim); + console.log(`\r${progress} ${color.red("✗")} ${img} 失败 `); + console.log(color.dim(` 错误: ${err.message}`)); + resolve({ success: false, error: err.message }); + }); + }); +} + +// 批量压缩 +export async function compressAll(images, config, cwd) { + const total = images.length; + let finished = 0; + let failed = 0; + + for (const img of images) { + finished++; + const progress = `(${finished}/${total})`; + const result = await compressFile(img, config, cwd, progress); + if (!result.success) failed++; + } + + return { total, failed }; +} diff --git a/lib/ktx2/config.js b/lib/ktx2/config.js new file mode 100644 index 0000000..1d9f0da --- /dev/null +++ b/lib/ktx2/config.js @@ -0,0 +1,62 @@ +// 步骤配置 +export const steps = [ + { + name: "文件格式", + type: "multiselect", + message: "请选择要压缩的图片类型", + options: [ + { value: "png", label: "PNG (.png)(无损格式,适合图标和透明图)" }, + { value: "jpg", label: "JPG (.jpg)(有损格式,适合照片和复杂图像)" }, + { value: "jpeg", label: "JPEG (.jpeg)(同JPG,仅扩展名不同)" }, + { value: "webp", label: "WebP (.webp)(新一代格式,体积更小)" }, + { value: "tga", label: "TGA (.tga)(游戏纹理常用格式)" } + ], + default: ["png", "jpg"] + }, + { + name: "压缩程度", + type: "select", + message: "请选择压缩级别", + options: [ + { value: "none", label: "无压缩(原始质量)", hint: "保持原始文件大小,图片和内容无损" }, + { value: "standard", label: "标准压缩(推荐)", hint: "平衡文件大小与质量,压缩率约40%" }, + { value: "high", label: "高度压缩(最小体积)", hint: "最大程度减小文件体积,可能轻微影响清晰度" }, + { value: "extreme", label: "极限压缩(极致压缩)", hint: "牺牲部分质量换取最小体积,适合网络传输" } + ], + default: "standard" + }, + { + name: "编码格式", + type: "select", + message: "请选择编码格式", + options: [ + { value: "uastc", label: "UASTC(通用超压缩纹理)", hint: "高质量GPU纹理,解码快,适合实时渲染" }, + { value: "etc1s", label: "ETC1S(基础压缩纹理)", hint: "文件体积最小,兼容性好,适合移动端" }, + { value: "astc", label: "ASTC(自适应纹理压缩)", hint: "灵活块大小,质量与体积可调,适合高端设备" } + ], + default: "uastc" + }, + { + name: "Mipmap", + type: "select", + message: "请选择Mipmap生成方式", + options: [ + { value: "auto", label: "自动生成(推荐)", hint: "根据图片尺寸自动生成多级纹理,优化远距离渲染" }, + { value: "none", label: "不生成Mipmap", hint: "仅保留原始尺寸,文件更小但可能出现锯齿" }, + { value: "custom", label: "自定义层级", hint: "手动指定Mipmap层数,精细控制纹理细节" } + ], + default: "auto" + }, + { + name: "输出选项", + type: "multiselect", + message: "请选择输出选项", + options: [ + { value: "overwrite", label: "覆盖已存在文件(自动替换同名文件)" }, + { value: "keepOriginal", label: "保留原文件(压缩后不删除源文件)" }, + { value: "report", label: "生成压缩报告(输出详细的压缩统计信息)" }, + { value: "silent", label: "静默模式(减少控制台输出信息)" } + ], + default: ["overwrite", "keepOriginal"] + } +]; diff --git a/lib/ktx2/index.js b/lib/ktx2/index.js new file mode 100644 index 0000000..79a2468 --- /dev/null +++ b/lib/ktx2/index.js @@ -0,0 +1,40 @@ +import color from "picocolors"; +import { checkToktx, scanImages, compressAll } from "./compressor.js"; +import { runInteractive, showSummary } from "./ui.js"; + +export async function run() { + // 检查 toktx + checkToktx(); + + // 运行交互界面 + const result = await runInteractive(); + + // ESC 返回主菜单 + if (!result) return "back"; + + const [exts, quality, encoding, mipmap, outputOpts] = result; + const config = { exts, quality, encoding, mipmap, outputOpts }; + + // 显示配置摘要 + showSummary(config); + + // 扫描文件 + const { images, cwd } = scanImages(exts); + + if (images.length === 0) { + console.log(color.yellow("当前目录没有匹配的图片")); + return; + } + + console.log(`📁 找到 ${color.cyan(images.length)} 个待转换文件\n`); + + // 执行压缩 + const { total, failed } = await compressAll(images, config, cwd); + + // 显示结果 + if (failed > 0) { + console.log(color.yellow(`\n⚠️ 完成,但有 ${failed} 个文件失败`)); + } else { + console.log(color.green("\n🎉 全部文件压缩完成!")); + } +} diff --git a/lib/ktx2/ui.js b/lib/ktx2/ui.js new file mode 100644 index 0000000..5725a19 --- /dev/null +++ b/lib/ktx2/ui.js @@ -0,0 +1,151 @@ +import color from "picocolors"; +import { steps } from "./config.js"; +import { initKeypress, onKey } from "../keyboard.js"; + +// 存储结果和状态 +let results = []; +let completed = new Set(); +let currentStep = 0; +let currentOption = 0; + +// 初始化结果 +export function initResults() { + results = steps.map(s => s.type === "multiselect" ? [...s.default] : s.default); + completed = new Set(); + currentStep = 0; + currentOption = 0; +} + +// 渲染导航栏 +function renderNav() { + const nav = steps.map((step, i) => { + if (completed.has(i)) { + return color.green(`☑ ${step.name}`); + } else if (i === currentStep) { + return color.bgCyan(color.black(` ${step.name} `)); + } else { + return color.dim(`□ ${step.name}`); + } + }); + return `← ${nav.join(" ")} ${color.green("✓Submit")} →`; +} + +// 渲染选项列表 +function renderOptions() { + const step = steps[currentStep]; + const lines = []; + + lines.push(color.cyan(step.message)); + lines.push(""); + + step.options.forEach((opt, i) => { + const isCurrent = i === currentOption; + const isSelected = step.type === "multiselect" + ? results[currentStep].includes(opt.value) + : results[currentStep] === opt.value; + + let prefix; + if (step.type === "multiselect") { + prefix = isSelected ? color.green("◉ ") : "○ "; + } else { + prefix = isSelected ? color.green("● ") : "○ "; + } + + const cursor = isCurrent ? color.cyan("❯ ") : " "; + const label = isCurrent ? color.cyan(opt.label) : opt.label; + const check = isSelected ? color.green(" ✓") : ""; + + lines.push(`${cursor}${prefix}${label}${check}`); + if (opt.hint) { + lines.push(` ${color.dim(opt.hint)}`); + } + }); + + return lines.join("\n"); +} + +// 渲染整个界面 +function render() { + console.clear(); + console.log(color.bgCyan(color.black(" KTX2 纹理压缩工具 "))); + console.log("\n" + renderNav()); + console.log(color.dim("\n← → 切换步骤 | ↑ ↓ 选择 | Space 选中 | Tab 提交 | Esc 返回\n")); + console.log(renderOptions()); +} + +// 主交互循环 +export async function runInteractive() { + initResults(); + initKeypress(); + + return new Promise((resolve) => { + render(); + + onKey((str, key) => { + if (!key) return; + + const step = steps[currentStep]; + const optCount = step.options.length; + + if (key.name === "left") { + if (currentStep > 0) { + currentStep--; + currentOption = 0; + } + render(); + } else if (key.name === "right") { + if (currentStep < steps.length - 1) { + currentStep++; + currentOption = 0; + } + render(); + } else if (key.name === "up") { + currentOption = (currentOption - 1 + optCount) % optCount; + render(); + } else if (key.name === "down") { + currentOption = (currentOption + 1) % optCount; + render(); + } else if (key.name === "space") { + const opt = step.options[currentOption]; + if (step.type === "multiselect") { + const idx = results[currentStep].indexOf(opt.value); + if (idx >= 0) { + results[currentStep].splice(idx, 1); + } else { + results[currentStep].push(opt.value); + } + } else { + results[currentStep] = opt.value; + } + completed.add(currentStep); + render(); + } else if (key.name === "return") { + if (step.type === "select") { + results[currentStep] = step.options[currentOption].value; + } + completed.add(currentStep); + if (currentStep < steps.length - 1) { + currentStep++; + currentOption = 0; + } + render(); + } else if (key.name === "tab") { + resolve(results); + } else if (key.name === "escape" || (key.ctrl && key.name === "c")) { + resolve(null); + } + }); + }); +} + +// 显示配置摘要 +export function showSummary(config) { + console.clear(); + console.log(color.bgCyan(color.black(" KTX2 纹理压缩工具 "))); + console.log("\n" + color.green("配置完成!当前设置:")); + console.log(` 文件格式: ${config.exts.join(", ")}`); + console.log(` 压缩程度: ${config.quality}`); + console.log(` 编码格式: ${config.encoding}`); + console.log(` Mipmap: ${config.mipmap}`); + console.log(` 输出选项: ${config.outputOpts.join(", ")}\n`); +} diff --git a/lib/menu.js b/lib/menu.js new file mode 100644 index 0000000..fc95072 --- /dev/null +++ b/lib/menu.js @@ -0,0 +1,90 @@ +import color from "picocolors"; +import boxen from "boxen"; +import figlet from "figlet"; +import { gridSelect } from "./grid.js"; + +// 古诗配置 +let poemConfig = { + lines: ["你我皆牛马", "生在人世间", "终日奔波苦", "一刻不得闲"], + perLine: 2, + padding: { top: 2, bottom: 2, left: 6, right: 6 }, + borderStyle: "double", + borderColor: "cyan", +}; + +// 标题配置 +let titleConfig = { + text: "Zguiy Tool Box", + font: "Standard", + color: "magenta", +}; + +// 工具列表 +const tools = [ + { name: "KTX2 纹理压缩", desc: "图片转KTX2格式", module: "./lib/ktx2/index.js" }, + { name: "模型压缩", desc: "压缩glTF/GLB模型", module: "./lib/model/index.js" }, + { name: "图片批量处理", desc: "裁剪/缩放/转换", module: "./lib/image/index.js" }, + { name: "Sprite图集", desc: "合并精灵图集", module: "./lib/sprite/index.js" }, + { name: "LOD生成器", desc: "生成多级细节", module: "./lib/lod/index.js" }, + { name: "音频压缩", desc: "压缩音频文件", module: "./lib/audio/index.js" }, +]; + +// 设置古诗 +export function setPoem(lines, perLine = 2) { + poemConfig.lines = lines; + poemConfig.perLine = perLine; +} + +// 设置古诗框样式 +export function setPoemStyle(style) { + Object.assign(poemConfig, style); +} + +// 设置标题 +export function setTitle(text, font = "Standard", titleColor = "magenta") { + titleConfig.text = text; + titleConfig.font = font; + titleConfig.color = titleColor; +} + +// 渲染古诗 +function renderPoem() { + const merged = []; + for (let i = 0; i < poemConfig.lines.length; i += poemConfig.perLine) { + merged.push(poemConfig.lines.slice(i, i + poemConfig.perLine).join(",")); + } + return boxen(color.yellow(merged.join("\n")), { + padding: poemConfig.padding, + borderStyle: poemConfig.borderStyle, + borderColor: poemConfig.borderColor, + textAlignment: "center", + float: "center", + }); +} + +// 渲染标题 +function renderTitle() { + const art = figlet.textSync(titleConfig.text, { font: titleConfig.font }); + const termWidth = process.stdout.columns || 80; + return art.split("\n").map(line => { + const pad = Math.max(0, Math.floor((termWidth - line.length) / 2)); + return " ".repeat(pad) + color[titleConfig.color](line); + }).join("\n"); +} + +// 渲染头部 +function renderHeader() { + return renderPoem() + "\n\n" + renderTitle(); +} + +// 主菜单 +export async function showMainMenu() { + return gridSelect({ + items: tools, + cols: 3, + colWidth: 24, + renderHeader: renderHeader, + }); +} + +export { tools, poemConfig, titleConfig }; diff --git a/lib/poem.js b/lib/poem.js new file mode 100644 index 0000000..ff58e1a --- /dev/null +++ b/lib/poem.js @@ -0,0 +1,31 @@ +import boxen from "boxen"; +import color from "picocolors"; + +/** + * 显示带边框的古诗(固定在顶部居中) + * @param {string[]} lines - 诗句数组 + * @param {number} perLine - 每行显示几句,默认2 + */ +export function showPoem(lines, perLine = 2) { + // 合并诗句 + const merged = []; + for (let i = 0; i < lines.length; i += perLine) { + merged.push(lines.slice(i, i + perLine).join(",")); + } + + const content = merged.join("\n"); + + console.log( + boxen(color.yellow(content), { + padding: { top: 1, bottom: 1, left: 4, right: 4 }, + margin: { top: 1, bottom: 1, left: 0, right: 0 }, + borderStyle: "double", + borderColor: "cyan", + textAlignment: "center", + float: "center", + }) + ); +} + +// 默认古诗 +export const defaultPoem = ["你我皆牛马", "生在人世间", "终日奔波苦", "一刻不得闲"]; diff --git a/lib/ui.js b/lib/ui.js index 3e0a6bb..06ec138 100644 --- a/lib/ui.js +++ b/lib/ui.js @@ -1,5 +1,6 @@ import color from "picocolors"; import readline from "readline"; +import boxen from "boxen"; import { steps } from "./config.js"; // 存储结果和状态 @@ -8,6 +9,31 @@ let completed = new Set(); let currentStep = 0; let currentOption = 0; +// 古诗配置 +let poemLines = ["你我皆牛马", "生在人世间", "终日奔波苦", "一刻不得闲"]; +let poemPerLine = 2; + +// 设置古诗 +export function setPoem(lines, perLine = 2) { + poemLines = lines; + poemPerLine = perLine; +} + +// 渲染古诗 +function renderPoem() { + const merged = []; + for (let i = 0; i < poemLines.length; i += poemPerLine) { + merged.push(poemLines.slice(i, i + poemPerLine).join(",")); + } + return boxen(color.yellow(merged.join("\n")), { + padding: { top: 1, bottom: 1, left: 4, right: 4 }, + borderStyle: "double", + borderColor: "cyan", + textAlignment: "center", + float: "center", + }); +} + // 初始化结果 export function initResults() { results = steps.map(s => s.type === "multiselect" ? [...s.default] : s.default); @@ -67,6 +93,7 @@ function renderOptions() { // 渲染整个界面 function render() { console.clear(); + console.log(renderPoem()); console.log(color.bgCyan(color.black(" KTX2 纹理压缩工具 "))); console.log("\n" + renderNav()); console.log(color.dim("\n← → 切换步骤 | ↑ ↓ 选择选项 | Space 选中 | Enter 确认 | Tab 提交\n")); @@ -152,6 +179,7 @@ export async function runInteractive() { // 显示配置摘要 export function showSummary(config) { console.clear(); + console.log(renderPoem()); console.log(color.bgCyan(color.black(" KTX2 纹理压缩工具 "))); console.log("\n" + color.green("配置完成!当前设置:")); console.log(` 文件格式: ${config.exts.join(", ")}`); diff --git a/package-lock.json b/package-lock.json index a785e43..e968a3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "dependencies": { "@clack/prompts": "^0.11.0", + "boxen": "^8.0.1", + "figlet": "^1.9.4", "picocolors": "^1.1.1", "prompts": "^2.4.2" }, @@ -37,6 +39,189 @@ "sisteransi": "^1.0.5" } }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/boxen": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", + "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^8.0.0", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "string-width": "^7.2.0", + "type-fest": "^4.21.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/figlet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.9.4.tgz", + "integrity": "sha512-uN6QE+TrzTAHC1IWTyrc4FfGo2KH/82J8Jl1tyKB7+z5DBit/m3D++Iu5lg91qJMnQQ3vpJrj5gxcK/pk4R9tQ==", + "license": "MIT", + "dependencies": { + "commander": "^14.0.0" + }, + "bin": { + "figlet": "bin/index.js" + }, + "engines": { + "node": ">= 17.0.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -70,6 +255,82 @@ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "license": "MIT" + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } } } } diff --git a/package.json b/package.json index cd90938..83c3931 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ }, "dependencies": { "@clack/prompts": "^0.11.0", + "boxen": "^8.0.1", + "figlet": "^1.9.4", "picocolors": "^1.1.1", "prompts": "^2.4.2" } diff --git a/test-poem.js b/test-poem.js new file mode 100644 index 0000000..585638d --- /dev/null +++ b/test-poem.js @@ -0,0 +1,7 @@ +import { showPoem, defaultPoem } from "./lib/poem.js"; + +// 使用默认古诗 +showPoem(defaultPoem); + +// 自定义古诗 +// showPoem(["床前明月光", "疑是地上霜", "举头望明月", "低头思故乡"]);