### Implement Custom Select Prompt in TypeScript Source: https://github.com/drizzle-team/hanji/blob/master/readme.md Extend the `Prompt` class to create a custom `Select` prompt. This example demonstrates how to manage state, render options, and define the result. Ensure you have `kleur` and `hanji` installed. ```typescript import color from "kleur"; import { ITerminal, Prompt, render, SelectData } from "hanji"; export class Select extends Prompt<{ index: number; value: string }> { private readonly data: SelectState<{ label: string; value: string }>; private readonly spinner: () => string; private timeout: NodeJS.Timer | undefined; constructor(items: string[]) { super(); this.on("attach", (terminal) => terminal.toggleCursor("hide")); this.on("detach", () => clearInterval(this.timeout)); this.data = new SelectState( items.map((it) => ({ label: it, value: `${it}-value` })) ); this.data.bind(this); } render(status: "idle" | "submitted" | "aborted"): string { if (status === "submitted" || status === "aborted") { return ""; } let text = ""; this.data.items.forEach((it, idx) => { text += idx === this.data.selectedIdx ? `${color.green("❯ " + it.label)}` : ` ${it.label}`; text += idx != this.data.items.length - 1 ? "\n" : ""; }); return text; } result() { return { index: this.data.selectedIdx, value: this.data.items[this.data.selectedIdx]!.value!, }; } } const { status, data } = await render( new Select(["user1", "user2", "user3", "user4"]) ); if (status === "aborted") return; console.log(data); // { index: 0, value: 'users1' } ``` -------------------------------- ### Hide/Show Cursor and Request Layout with `ITerminal` Source: https://context7.com/drizzle-team/hanji/llms.txt This example demonstrates how to use `ITerminal` methods to hide the cursor during interaction and restore it on exit. It also shows how to manually trigger a re-render using `requestLayout()` when user input changes the prompt's state. ```typescript import { Prompt, ITerminal, render } from "hanji"; import color from "kleur"; class MultiStepPrompt extends Prompt { private selected = new Set(); private readonly items = ["TypeScript", "Rust", "Go", "Zig"]; constructor() { super(); this.on("attach", (terminal: ITerminal) => { terminal.toggleCursor("hide"); // hide blinking cursor during interaction }); this.on("detach", (terminal: ITerminal) => { terminal.toggleCursor("show"); // restore cursor on exit }); this.on("input", (_str, key) => { const num = parseInt(key.name ?? "", 10); if (num >= 1 && num <= this.items.length) { this.selected.has(num - 1) ? this.selected.delete(num - 1) : this.selected.add(num - 1); this.requestLayout(); // <-- manually trigger repaint } }); } render(status: "idle" | "submitted" | "aborted"): string { if (status !== "idle") return ""; return this.items .map((item, i) => this.selected.has(i) ? color.green(`[x] ${i + 1}. ${item}`) : `[ ] ${i + 1}. ${item}` ) .join("\n"); } result() { return [...this.selected]; } } const { status, data } = await render(new MultiStepPrompt()); if (status === "submitted") console.log("Chosen indices:", data); // Chosen indices: [0, 2] ``` -------------------------------- ### Create a Select Prompt with Hanji Source: https://context7.com/drizzle-team/hanji/llms.txt Extends the `Prompt` base class to create a custom select prompt. It uses `SelectState` for managing selection and binds arrow-key navigation. Requires `kleur` for styling. ```typescript import color from "kleur"; import { Prompt, ITerminal, render, SelectState } from "hanji"; interface Item { label: string; value: string } class Select extends Prompt<{ index: number; value: string }> { private readonly data: SelectState; constructor(items: string[]) { super(); // Hide cursor when prompt becomes active this.on("attach", (terminal: ITerminal) => terminal.toggleCursor("hide")); this.data = new SelectState( items.map((it) => ({ label: it, value: `${it}-value` })) ); // Wire arrow-key navigation to this prompt's layout cycle this.data.bind(this); } render(status: "idle" | "submitted" | "aborted"): string { if (status === "submitted" || status === "aborted") return ""; return this.data.items .map((it, idx) => idx === this.data.selectedIdx ? color.green(`❯ ${it.label}`) : ` ${it.label}` ) .join("\n"); } result() { return { index: this.data.selectedIdx, value: this.data.items[this.data.selectedIdx]!.value, }; } } const { status, data } = await render(new Select(["Alice", "Bob", "Carol"])); if (status === "aborted") { console.log("User cancelled"); process.exit(0); } console.log(data); // { index: 1, value: 'Bob-value' } ``` -------------------------------- ### Prompt.on() — Lifecycle Event Subscriptions Source: https://context7.com/drizzle-team/hanji/llms.txt Subscribes to lifecycle events on a Prompt instance. Supported events are 'attach', 'detach', and 'input'. Multiple listeners can be added for each event. ```APIDOC ## `Prompt.on()` — Lifecycle Event Subscriptions Subscribes to one of three events on a `Prompt` instance: `"attach"` (fired when the prompt is connected to a terminal), `"detach"` (fired on submission, abort, or Ctrl+C), and `"input"` (fired on every keypress). Multiple listeners per event are supported. ```typescript import { Prompt, ITerminal, render } from "hanji"; class TimedInput extends Prompt { private value = ""; private timeout: NodeJS.Timeout | undefined; constructor(private readonly limitMs: number) { super(); this.on("attach", (terminal: ITerminal) => { terminal.toggleCursor("hide"); // Auto-submit after timeout this.timeout = setTimeout(() => { this.requestLayout(); // trigger final render }, limitMs); }); this.on("detach", () => { clearTimeout(this.timeout); }); this.on("input", (str, key) => { if (str && !key.ctrl && !key.meta) { this.value += str; this.requestLayout(); } }); } render(status: "idle" | "submitted" | "aborted"): string { if (status !== "idle") return ""; const remaining = Math.ceil(this.limitMs / 1000); return `Type something [${remaining}s]: ${this.value}▌`; } result() { return this.value || null; } } const { status, data } = await render(new TimedInput(10_000)); console.log(status, data); // "submitted" "hello" ``` ``` -------------------------------- ### Render Static Strings and Interactive Prompts with Hanji Source: https://context7.com/drizzle-team/hanji/llms.txt The `render` function can output static strings to stdout or display interactive prompts. It requires a TTY for interactive prompts and rejects cleanly if one is not available. ```typescript import { render, Prompt } from "hanji"; // --- Overload 1: static string output (works without a TTY) --- render("Starting deployment…"); // Writes "Starting deployment…\n" to stdout // --- Overload 2: interactive prompt --- class ConfirmPrompt extends Prompt { private confirmed = false; constructor() { super(); this.on("input", (_str, key) => { if (key.name === "y") { this.confirmed = true; this.requestLayout(); } if (key.name === "n") { this.confirmed = false; this.requestLayout(); } }); } render(status: "idle" | "submitted" | "aborted"): string { if (status !== "idle") return ""; return `Proceed? (y/n) ${this.confirmed ? "yes" : "no"}`; } result() { return this.confirmed; } } const { status, data } = await render(new ConfirmPrompt()); if (status === "aborted") process.exit(1); console.log("Confirmed:", data); // Confirmed: true // --- Non-TTY guard: rejected promise --- // When stdin/stdout are pipes (e.g., CI), render(prompt) rejects cleanly: try { await render(new ConfirmPrompt()); } catch (err) { console.error(err.message); // "Interactive prompts require a TTY terminal (process.stdin.isTTY or // process.stdout.isTTY is false). This can happen when running in CI, // piped input, or non-interactive shells." } ``` -------------------------------- ### render(view) - Display a Prompt or Static String Source: https://context7.com/drizzle-team/hanji/llms.txt Overloaded function to display either an interactive prompt or a static string. When given a `Prompt`, it returns a `Promise>`. When given a string, it writes a line to stdout. ```APIDOC ## `render(view)` — Display a Prompt or Static String Overloaded function with two signatures: pass a `Prompt` to display an interactive prompt that returns `Promise>`, or pass a plain `string` to write a line to stdout without requiring a TTY. Throws a rejected promise if `process.stdin` or `process.stdout` is not a TTY when given a `Prompt`. ```typescript import { render, Prompt } from "hanji"; // --- Overload 1: static string output (works without a TTY) --- render("Starting deployment…"); // Writes "Starting deployment…\n" to stdout // --- Overload 2: interactive prompt --- class ConfirmPrompt extends Prompt { private confirmed = false; constructor() { super(); this.on("input", (_str, key) => { if (key.name === "y") { this.confirmed = true; this.requestLayout(); } if (key.name === "n") { this.confirmed = false; this.requestLayout(); } }); } render(status: "idle" | "submitted" | "aborted"): string { if (status !== "idle") return ""; return `Proceed? (y/n) ${this.confirmed ? "yes" : "no"}`; } result() { return this.confirmed; } } const { status, data } = await render(new ConfirmPrompt()); if (status === "aborted") process.exit(1); console.log("Confirmed:", data); // Confirmed: true // --- Non-TTY guard: rejected promise --- // When stdin/stdout are pipes (e.g., CI), render(prompt) rejects cleanly: try { await render(new ConfirmPrompt()); } catch (err) { console.error(err.message); // "Interactive prompts require a TTY terminal (process.stdin.isTTY or // process.stdout.isTTY is false). This can happen when running in CI, // piped input, or non-interactive shells." } ``` ``` -------------------------------- ### Create a Framework Picker with SelectState Source: https://context7.com/drizzle-team/hanji/llms.txt Use SelectState to manage highlighted items and arrow key navigation in a list. Bind it to a Prompt instance to automatically update the selection and re-render the view. ```typescript import { Prompt, SelectState, render } from "hanji"; import color from "kleur"; interface Framework { label: string; url: string; } class FrameworkPicker extends Prompt { private readonly state: SelectState; constructor(frameworks: Framework[]) { super(); this.on("attach", (t) => t.toggleCursor("hide")); this.state = new SelectState(frameworks); this.state.bind(this); // arrow keys now update state + re-render } render(status: "idle" | "submitted" | "aborted"): string { if (status !== "idle") return ""; return this.state.items .map((f, i) => i === this.state.selectedIdx ? color.cyan(`▶ ${f.label}`) : ` ${f.label}` ) .join("\n"); } result() { return this.state.items[this.state.selectedIdx]!; } } const { status, data } = await render( new FrameworkPicker([ { label: "Drizzle ORM", url: "https://orm.drizzle.team" }, { label: "Prisma", url: "https://www.prisma.io" }, { label: "Kysely", url: "https://kysely.dev" }, ]) ); if (status === "submitted") { console.log(`Selected: ${data.label} — ${data.url}`); // Selected: Prisma — https://www.prisma.io } ``` -------------------------------- ### Subscribe to Prompt Lifecycle Events with `Prompt.on()` Source: https://context7.com/drizzle-team/hanji/llms.txt Use `Prompt.on()` to subscribe to 'attach', 'detach', or 'input' events on a Prompt instance. The 'attach' event fires when the prompt connects to a terminal, 'detach' on submission or abort, and 'input' on every keypress. Ensure to clear any timeouts or subscriptions in the 'detach' handler. ```typescript import { Prompt, ITerminal, render } from "hanji"; class TimedInput extends Prompt { private value = ""; private timeout: NodeJS.Timeout | undefined; constructor(private readonly limitMs: number) { super(); this.on("attach", (terminal: ITerminal) => { terminal.toggleCursor("hide"); // Auto-submit after timeout this.timeout = setTimeout(() => { this.requestLayout(); // trigger final render }, limitMs); }); this.on("detach", () => { clearTimeout(this.timeout); }); this.on("input", (str, key) => { if (str && !key.ctrl && !key.meta) { this.value += str; this.requestLayout(); } }); } render(status: "idle" | "submitted" | "aborted"): string { if (status !== "idle") return ""; const remaining = Math.ceil(this.limitMs / 1000); return `Type something [${remaining}s]: ${this.value}▌`; } result() { return this.value || null; } } const { status, data } = await render(new TimedInput(10_000)); console.log(status, data); // "submitted" "hello" ``` -------------------------------- ### Prompt - Abstract Base Class Source: https://context7.com/drizzle-team/hanji/llms.txt The foundational abstract class for all interactive CLI prompts. Subclasses must implement `render(status)` and `result()`. It also provides an `on()` method for subscribing to lifecycle events. ```APIDOC ## `Prompt` — Abstract Base Class for Interactive Prompts The foundational abstract class all interactive CLI prompts extend. Subclasses must implement `render(status)` to return the string drawn to the terminal and `result()` to return the final typed value when the user submits. The class provides an `on()` method for subscribing to `attach`, `detach`, and `input` lifecycle events. ```typescript import color from "kleur"; import { Prompt, ITerminal, render, SelectState } from "hanji"; interface Item { label: string; value: string } class Select extends Prompt<{ index: number; value: string }> { private readonly data: SelectState; constructor(items: string[]) { super(); // Hide cursor when prompt becomes active this.on("attach", (terminal: ITerminal) => terminal.toggleCursor("hide")); this.data = new SelectState( items.map((it) => ({ label: it, value: `${it}-value` })) ); // Wire arrow-key navigation to this prompt's layout cycle this.data.bind(this); } render(status: "idle" | "submitted" | "aborted"): string { if (status === "submitted" || status === "aborted") return ""; return this.data.items .map((it, idx) => idx === this.data.selectedIdx ? color.green(`❯ ${it.label}`) : ` ${it.label}` ) .join("\n"); } result() { return { index: this.data.selectedIdx, value: this.data.items[this.data.selectedIdx]!.value, }; } } const { status, data } = await render(new Select(["Alice", "Bob", "Carol"])); if (status === "aborted") { console.log("User cancelled"); process.exit(0); } console.log(data); // { index: 1, value: 'Bob-value' } ``` ``` -------------------------------- ### Display Async Task Progress with TaskView and renderWithTask Source: https://context7.com/drizzle-team/hanji/llms.txt Use TaskView as a base class for rendering task statuses and renderWithTask to manage the display and lifecycle of asynchronous operations. It clears the terminal on success or shows an error on failure. ```typescript import { TaskView, renderWithTask } from "hanji"; import spinners from "cli-spinners"; class SpinnerTask extends TaskView { private frame = 0; private readonly label: string; constructor(label: string) { super(); this.on("attach", () => { const interval = setInterval(() => { this.frame = (this.frame + 1) % spinners.dots.frames.length; this.requestLayout(); }, spinners.dots.interval); this.on("detach", () => clearInterval(interval)); }); this.label = label; } render (status: T, meta?: T extends "rejected" ? Error : any ): string { if (status === "pending") { return `${spinners.dots.frames[this.frame]} ${this.label}…`; } if (status === "done") return `✔ ${this.label} complete`; if (status === "rejected") return `✘ ${this.label} failed: ${meta?.message}`; return ""; } } // Usage: the spinner runs while the DB migration resolves const result = await renderWithTask( new SpinnerTask("Running migrations"), runMigrations() // your Promise ); // Terminal shows: ✔ Running migrations complete console.log(result); // MigrationResult value ``` -------------------------------- ### Create Deferred Promises with `deferred()` Source: https://context7.com/drizzle-team/hanji/llms.txt The `deferred()` utility creates a promise with exposed `resolve` and `reject` functions. This is useful for settling a promise from outside its constructor or an async context, such as bridging external events like signals to promise resolution. ```typescript import { deferred } from "hanji"; async function waitForSignal(): Promise { const { promise, resolve, reject } = deferred(); process.once("SIGUSR1", () => resolve("SIGUSR1 received")); process.once("SIGUSR2", () => reject(new Error("SIGUSR2 abort"))); return promise; } try { const signal = await waitForSignal(); console.log(signal); // "SIGUSR1 received" } catch (err) { console.error(err.message); // "SIGUSR2 abort" } ``` -------------------------------- ### Handle Ctrl+C with onTerminate Source: https://context7.com/drizzle-team/hanji/llms.txt Register a custom callback for Ctrl+C events using onTerminate. This allows for graceful exit messages and manual process termination instead of the default behavior. ```typescript import { onTerminate, render } from "hanji"; import { ReadStream, WriteStream } from "tty"; onTerminate((stdin: ReadStream, stdout: WriteStream) => { stdout.write("\n⚠ Aborted — no changes were made.\n"); process.exit(0); }); // Now any prompt rendered afterwards respects the handler: const { status, data } = await render(myPrompt); ``` -------------------------------- ### deferred() — Promise Deferred Utility Source: https://context7.com/drizzle-team/hanji/llms.txt Creates a deferred promise, providing an object with `promise`, `resolve`, and `reject` properties. This utility allows external code to control the settlement of a promise. ```APIDOC ## `deferred()` — Promise Deferred Utility Creates a deferred promise — an object exposing `{ promise, resolve, reject }` — allowing external code to settle a promise from outside the constructor or async context. Used internally by `Terminal` to bridge keypress events to the `render()` return value. ```typescript import { deferred } from "hanji"; async function waitForSignal(): Promise { const { promise, resolve, reject } = deferred(); process.once("SIGUSR1", () => resolve("SIGUSR1 received")); process.once("SIGUSR2", () => reject(new Error("SIGUSR2 abort"))); return promise; } try { const signal = await waitForSignal(); console.log(signal); // "SIGUSR1 received" } catch (err) { console.error(err.message); // "SIGUSR2 abort" } ``` ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.