Sfoglia il codice sorgente

feat(tui): bordered input box with label separator

- Input prompt wrapped in ╭─ inkos ───╮ / ╰────╯ box
- Prompt shows │ ❯ inside the box frame
- Box closes after each input, reopens for next prompt
- Clean separation between output and input areas
Ma 7 mesi fa
parent
commit
1e1324131a
2 ha cambiato i file con 37 aggiunte e 16 eliminazioni
  1. 23 13
      packages/cli/src/tui/app.ts
  2. 14 3
      packages/cli/src/tui/effects.ts

+ 23 - 13
packages/cli/src/tui/app.ts

@@ -28,6 +28,8 @@ import {
   printStyledHelp,
   printStyledHelp,
   printStyledStatus,
   printStyledStatus,
   printInputSeparator,
   printInputSeparator,
+  drawInputBoxTop,
+  drawInputBoxBottom,
 } from "./effects.js";
 } from "./effects.js";
 
 
 /* ── Version ── */
 /* ── Version ── */
@@ -209,7 +211,7 @@ export async function launchTui(
   const rl = readline.createInterface({
   const rl = readline.createInterface({
     input: process.stdin,
     input: process.stdin,
     output: process.stdout,
     output: process.stdout,
-    prompt: `  ${c("❯", brightCyan)} `,
+    prompt: `  ${c("│", gray)} ${c("❯", brightCyan)} `,
   });
   });
 
 
   const cleanup = () => {
   const cleanup = () => {
@@ -217,21 +219,32 @@ export async function launchTui(
     rl.close();
     rl.close();
   };
   };
 
 
+  const promptInput = () => {
+    drawInputBoxTop();
+    rl.prompt();
+  };
+
+  const closeInputBox = () => {
+    drawInputBoxBottom();
+  };
+
   process.on("SIGINT", () => {
   process.on("SIGINT", () => {
     console.log();
     console.log();
+    closeInputBox();
     console.log(c("  ◇ goodbye", dim));
     console.log(c("  ◇ goodbye", dim));
     console.log();
     console.log();
     cleanup();
     cleanup();
     process.exit(0);
     process.exit(0);
   });
   });
 
 
-  rl.prompt();
+  promptInput();
 
 
   for await (const line of rl) {
   for await (const line of rl) {
+    closeInputBox();
     const input = line.trim();
     const input = line.trim();
 
 
     if (!input) {
     if (!input) {
-      rl.prompt();
+      promptInput();
       continue;
       continue;
     }
     }
 
 
@@ -243,14 +256,14 @@ export async function launchTui(
     }
     }
 
 
     if (/^\/help$/i.test(input) || /^(help|帮助)$/i.test(input)) {
     if (/^\/help$/i.test(input) || /^(help|帮助)$/i.test(input)) {
-      printStyledHelp();
-      printInputSeparator();
       console.log();
       console.log();
-      rl.prompt();
+      printStyledHelp();
+      promptInput();
       continue;
       continue;
     }
     }
 
 
     if (/^\/status$/i.test(input) || /^(status|状态)$/i.test(input)) {
     if (/^\/status$/i.test(input) || /^(status|状态)$/i.test(input)) {
+      console.log();
       try {
       try {
         const s = await loadProjectSession(projectRoot);
         const s = await loadProjectSession(projectRoot);
         const bId = await resolveSessionActiveBook(projectRoot, s);
         const bId = await resolveSessionActiveBook(projectRoot, s);
@@ -263,25 +276,22 @@ export async function launchTui(
       } catch {
       } catch {
         console.log(c("  Could not load session.", dim));
         console.log(c("  Could not load session.", dim));
       }
       }
-      printInputSeparator();
-      console.log();
-      rl.prompt();
+      promptInput();
       continue;
       continue;
     }
     }
 
 
     if (/^\/clear$/i.test(input)) {
     if (/^\/clear$/i.test(input)) {
       console.clear();
       console.clear();
-      rl.prompt();
+      promptInput();
       continue;
       continue;
     }
     }
 
 
     // Delegate to interaction layer with themed animation
     // Delegate to interaction layer with themed animation
-    await processInput(projectRoot, input, tools);
     console.log();
     console.log();
-    printInputSeparator();
+    await processInput(projectRoot, input, tools);
     console.log();
     console.log();
 
 
-    rl.prompt();
+    promptInput();
   }
   }
 
 
   cleanup();
   cleanup();

+ 14 - 3
packages/cli/src/tui/effects.ts

@@ -161,7 +161,20 @@ const ASCII_LOGO = [
   " ╚═╝╚═╝  ╚═══╝╚═╝  ╚═╝ ╚═════╝ ╚══════╝",
   " ╚═╝╚═╝  ╚═══╝╚═╝  ╚═╝ ╚═════╝ ╚══════╝",
 ];
 ];
 
 
-/* ── Input separator ── */
+/* ── Input box ── */
+
+export function drawInputBoxTop(): void {
+  const w = Math.min(process.stdout.columns ?? 60, 60);
+  const label = c(" inkos ", dim);
+  const labelLen = 7; // visible length of " inkos "
+  const lineLen = w - 4 - 2 - labelLen; // ╭─ + label + ─...─
+  process.stdout.write(`  ${c("╭─", gray)}${label}${c("─".repeat(Math.max(0, lineLen)), gray)}\n`);
+}
+
+export function drawInputBoxBottom(): void {
+  const w = Math.min(process.stdout.columns ?? 60, 60);
+  process.stdout.write(`  ${c("╰" + "─".repeat(w - 5), gray)}\n`);
+}
 
 
 export function printInputSeparator(): void {
 export function printInputSeparator(): void {
   const w = Math.min(process.stdout.columns ?? 60, 60);
   const w = Math.min(process.stdout.columns ?? 60, 60);
@@ -220,8 +233,6 @@ export async function animateStartup(version: string, projectName: string, bookT
   console.log();
   console.log();
   console.log(c("  /help for commands. Type anything to begin.", dim));
   console.log(c("  /help for commands. Type anything to begin.", dim));
   console.log();
   console.log();
-  printInputSeparator();
-  console.log();
 }
 }
 
 
 /* ── Typewriter effect ── */
 /* ── Typewriter effect ── */