/** * Fun Placeholder - 给 pi 输入框加上有趣的随机提示语 * * 默认「自动识别」系统语言(LANG/LC_ALL/LC_CTYPE 以 zh 开头 → 中文池,否则 English 池)。 * 可用命令随时切换,选择会持久化(重启后依然生效): * /placeholder 查看当前状态与用法 * /placeholder auto|zh|en|all|off 切换 * 输入框为空时以浅灰色显示在光标之后。 * 想改提示语?编辑下方 ZH/EN 数组即可(本地包直接改,npm 包需重发版本)。 */ import { join } from "node:path"; import { readFileSync, writeFileSync } from "node:fs"; import { type ExtensionAPI, CustomEditor, type KeybindingsManager, getAgentDir, } from "@earendil-works/pi-coding-agent"; import { type TUI, type EditorTheme, type AutocompleteItem, visibleWidth, } from "@earendil-works/pi-tui"; // ── 中文提示语池 ────────────────────────────────────────────────── const ZH_PLACEHOLDERS = [ "╰(*°▽°*)╯ 想好要让 pi 帮你做什么了吗?", "✨ 在这里输入你的魔法咒语...", "🎯 一个 prompt 改变世界,开始吧", "🤖 人类,请下达指令!", "💡 灵感来了?快打出来!", "🦊 别问我是狐狸还是浣熊,先问你的问题", "🎪 欢迎来到 pi 马戏团,请输入你的节目单", "🍵 摸鱼时间到... 哦你已经在了", "🚀 正在等待来自人类的输入...", "🌈 输入任何内容,奇迹就会发生", "⚡ 别光看着,说点什么!", "🎮 按 Enter 开始冒险", "📝 我有一个好主意,但你先说", "🔥 今天的代码写了吗?", "💤 Zzz... 哦你来了!问吧", "🎲 你的键盘在呼唤你", "🌟 行家一出手,就知有没有", "🥷 别紧张,我不会笑你的 bug 的(可能)", "🔮 你想修什么 bug?说出来让我乐呵乐呵", "🎭 假装我是莎士比亚,请给 prompt", "🪄 Expecto Prompt-o!", "🧩 拼图已经准备好了,就差你的一块", "⚙️ *风扇呼呼转* 正在等待任务...", "📡 信号已连接,等待你的指令", "🌙 深夜写代码?注意休息呀", "🍕 写代码饿了?我也帮不了你订餐... 但可以帮你写代码", "🧠 脑细胞已就绪,请发送刺激", ]; // ── English placeholder pool ───────────────────────────────────── const EN_PLACEHOLDERS = [ "🐞 Found a bug? Let's squash it together!", "🚀 Ready for liftoff. Just type your mission...", "🎬 Action! Your script, my execution.", "🤯 Enter your prompt and watch the magic.", "💻 Keyboard go click clack... make it count!", "🔮 I see... a prompt in your future.", "🎲 Roll the dice! (Or just type something)", "☕ 0xCAFEBABE — brew some code.", "🦄 This prompt will be legendary. I can feel it.", "🏗️ 0 errors, 0 warnings... so far.", "🎪 Step right up! Ask your burning question!", "🌊 The answer is out there. Start typing.", "🔧 I fix bugs while you sleep. And while you're awake too.", "📖 Every great program starts with a single prompt.", "🎯 You aim, I fire. What's the target?", "⚡ Compiling patience module... done. Ready!", "🌀 Go ahead, make my day. With a prompt.", "♲ Not all those who wander are lost. But you found pi.", "📝 Your wish is my `execute()` command.", "🎭 I'm listening... and I don't even have ears!", ]; // 混合池 = 中文 + 英文 const ALL_PLACEHOLDERS = [...ZH_PLACEHOLDERS, ...EN_PLACEHOLDERS]; // ── 模式 ───────────────────────────────────────────────────────── // auto = 默认,按系统语言自动识别;zh/en/all 固定池;off = 关闭提示语 type Mode = "auto" | "zh" | "en" | "all" | "off"; const MODES: readonly Mode[] = ["auto", "zh", "en", "all", "off"]; function isMode(v: string): v is Mode { return (MODES as readonly string[]).includes(v); } // ── 配置持久化(~/.pi/agent/fun-placeholder.json)────────────── const CONFIG_FILE = join(getAgentDir(), "fun-placeholder.json"); function loadMode(): Mode { try { const raw = readFile(CONFIG_FILE); if (raw !== null) { const parsed = JSON.parse(raw) as { mode?: unknown }; if (typeof parsed.mode === "string" && isMode(parsed.mode)) { return parsed.mode; } } } catch { // 配置损坏 → 回退默认 } return "auto"; } function readFile(path: string): string | null { try { return readFileSync(path, "utf8"); } catch { return null; } } function saveMode(mode: Mode): void { try { writeFileSync(CONFIG_FILE, JSON.stringify({ mode }, null, "\t") + "\n"); } catch { // 写入失败不阻塞命令(仅本次运行生效) } } // ── 语言识别 ───────────────────────────────────────────────────── function detectZhLocale(): boolean { const lang = ( process.env.LC_ALL ?? process.env.LC_CTYPE ?? process.env.LANG ?? "" ).toLowerCase(); return lang.startsWith("zh"); } function resolvePool(mode: Mode): { list: string[]; label: string } { switch (mode) { case "zh": return { list: ZH_PLACEHOLDERS, label: "中文" }; case "en": return { list: EN_PLACEHOLDERS, label: "English" }; case "all": return { list: ALL_PLACEHOLDERS, label: "中英混合" }; case "off": return { list: [], label: "关闭" }; case "auto": return detectZhLocale() ? { list: ZH_PLACEHOLDERS, label: "自动识别(中文环境)" } : { list: EN_PLACEHOLDERS, label: "自动识别(非中文环境)" }; } } function describeMode(mode: Mode): string { const { list, label } = resolvePool(mode); return list.length > 0 ? `${label}(${list.length} 条)` : label; } function pickRandom(list: string[]): string { return list.length === 0 ? "" : list[Math.floor(Math.random() * list.length)]!; } // 模块级当前模式:session_start 与命令共享同一状态 let currentMode: Mode = loadMode(); // ── 自定义编辑器 ────────────────────────────────────────────────── class PlaceholderEditor extends CustomEditor { private placeholder: string; constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) { super(tui, theme, keybindings); // 构建时按当前模式抽取一条;off 时为空字符串 = 不显示提示语 this.placeholder = pickRandom(resolvePool(currentMode).list); } /** * 当编辑器为空时,在光标位置后显示主题色提示语 */ override render(width: number): string[] { // 非空或已关闭:正常渲染 if (this.getText() !== "" || this.placeholder === "") { return super.render(width); } // 空编辑器:先渲染正常框架(边框 + 空行 + 光标) const lines = super.render(width); if (lines.length < 3) return lines; // 空编辑器时 lines 结构: // [0] = 顶边框 (ANSI 包裹的 ── 线) // [1] = 内容行: 左padding + CURSOR_MARKER + 反色空格光标 + 右padding // [2] = 底边框 const contentLine = lines[1]!; // 找到反色空格光标的位置 // 光标渲染为: CURSOR_MARKER + \x1b[7m \x1b[0m const cursorSeq = `\x1b[7m \x1b[0m`; const cursorEnd = contentLine.indexOf(cursorSeq) + cursorSeq.length; if (cursorEnd < cursorSeq.length) return lines; // 没找到光标,回退 // 光标之后的所有内容 (= 填充 padding + 右padding) const afterCursor = contentLine.slice(cursorEnd); // 光标之前的内容 (= 左padding + CURSOR_MARKER) const beforeCursor = contentLine.slice(0, cursorEnd); // 计算可用的填充宽度 const afterCursorWidth = visibleWidth(afterCursor); // 构建提示语文本(浅灰色) const placeholder = "\x1b[38;5;250m" + this.placeholder + "\x1b[0m"; const placeholderWidth = visibleWidth(this.placeholder); // 根据剩余空间决定是否截断 const newPaddingWidth = Math.max(0, afterCursorWidth - placeholderWidth); // 重建内容行 lines[1] = beforeCursor + placeholder + " ".repeat(newPaddingWidth); return lines; } } // ── 扩展入口 ────────────────────────────────────────────────────── export default function (pi: ExtensionAPI) { pi.on("session_start", (_event, ctx) => { ctx.ui.setEditorComponent( (tui, theme, keybindings) => new PlaceholderEditor(tui, theme, keybindings), ); }); pi.registerCommand("placeholder", { description: "切换输入框提示语: auto(识别) / zh / en / all / off", getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => { const items = MODES.map((m) => ({ value: m, label: m })); const filtered = items.filter((i) => i.value.startsWith(prefix)); return filtered.length > 0 ? filtered : null; }, handler: async (args, ctx) => { const arg = (args ?? "").trim().toLowerCase(); // 无参数:显示当前状态 if (arg === "") { ctx.ui.notify( `提示语当前: ${describeMode(currentMode)}。用法: /placeholder auto|zh|en|all|off`, "info", ); return; } // 非法参数:提示用法 if (!isMode(arg)) { ctx.ui.notify(`未知模式 "${arg}"。用法: /placeholder auto|zh|en|all|off`, "error"); return; } // 切换模式并持久化 currentMode = arg; saveMode(arg); // 重建编辑器,让新模式立即生效(命令执行后输入框已清空,无内容丢失风险) if (ctx.hasUI) { ctx.ui.setEditorComponent( (tui, theme, keybindings) => new PlaceholderEditor(tui, theme, keybindings), ); } ctx.ui.notify(`提示语已切换 → ${describeMode(arg)}`, "info"); }, }); }