npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

clickprompt

v1.0.0

Published

Mouse-enabled interactive prompts for the terminal. Click to select. Drag to reorder. Drop-in for inquirer and prompts. Zero dependencies. ESM + CJS.

Readme

clickprompt

Mouse-enabled interactive prompts for the terminal. Click to select, scroll to navigate, drag to reorder. Zero runtime dependencies. ESM + CJS.

npm bundle CI types license

npm install clickprompt

Most CLI prompt libraries ignore the mouse. clickprompt reads SGR mouse events (modes ?1000 / ?1002 / ?1006) so users can click rows, scroll through long lists, and drag items to reorder them. Keyboard always works too — mouse is additive.

Node 18+. Works in iTerm2, Terminal.app, Windows Terminal, VS Code's terminal, Alacritty, Kitty, WezTerm, GNOME Terminal, Konsole, tmux, GNU screen, and modern Windows conhost. Legacy cmd.exe and CI environments are auto-detected and fall back to keyboard.


Prompt lineup

| Prompt | Mouse | Notes | |---|---|---| | reorder | click + drag | Drag-to-reorder; optional per-row action chips | | select | click + wheel | Single choice | | multiselect | click + wheel | Multiple choice with min/max | | autocomplete | click + wheel | Type to filter | | confirm | click [Yes] / [No] | Y/N buttons | | toggle | click on/off | Boolean switch | | text | — | Line input + readline editing | | password | — | Masked text | | number | — | Numeric input with min/max/float | | spinner | — | Long-running task indicator | | form | (inherits) | Sequential multi-step wizard |

Decorations: intro, outro, note, log, logSuccess, logWarning, logError for clack-style session framing.

Quick start

import {
  intro, outro,
  text, select, multiselect, confirm, autocomplete, spinner,
} from "clickprompt";

intro("create-app");

const name = await text({
  message: "Project name",
  placeholder: "my-app",
  validate: (v) => v.length >= 3 || "At least 3 characters",
});

const framework = await select({
  message: "Framework",
  options: [
    { label: "Next.js",   value: "next",      hint: "React, full stack" },
    { label: "Astro",     value: "astro",     hint: "Content + islands" },
    { label: "SvelteKit", value: "sveltekit", hint: "Fast + small" },
  ],
});

const features = await multiselect({
  message: "Add features",
  options: [
    { label: "TypeScript",        value: "ts" },
    { label: "ESLint + Prettier", value: "lint" },
    { label: "Tailwind CSS",      value: "tw" },
    { label: "Vitest",            value: "test" },
  ],
  defaultSelected: ["ts", "lint"],
});

const region = await autocomplete({
  message: "Region",
  options: regions, // { label, value, hint? }[]
});

const ok = await confirm({ message: "Create project?", initial: true });

if (ok) {
  const s = spinner();
  s.start("Generating");
  await scaffold({ name, framework, features, region });
  s.stop("Done", true);
}

outro(`cd ${name} && npm install`);

Drop-in adapters

- import { input, select, confirm, checkbox, password, number } from "@inquirer/prompts";
+ import { input, select, confirm, checkbox, password, number } from "clickprompt/inquirer";
- import prompts from "prompts";
+ import prompts from "clickprompt/prompts";
  • clickprompt/inquirer exports: input, select, checkbox, confirm, password, number. The transformer option on input() is unsupported and throws explicitly — every other common option is covered.
  • clickprompt/prompts accepts terkelg's question types: text, password, invisible, number, confirm, select, multiselect, autocomplete, toggle. onCancel / onSubmit callbacks behave the same way (truthy return value stops the chain).

Controls

| Action | Mouse | Keyboard | |---|---|---| | Move highlight | Wheel scroll | ↑ / ↓ | | Select / toggle | Click | Enter / Space | | Page jump | — | PgUp / PgDn | | Jump to top/bottom | — | Home / End | | Select all (multiselect) | — | a | | Filter (autocomplete) | — | type freely; Backspace, Ctrl-U, Ctrl-W | | Cancel | — | Esc / Ctrl-C | | Cursor moves (text) | — | ← / →, Home / End, Ctrl-A / Ctrl-E | | Delete (text) | — | Backspace, Delete, Ctrl-D, Ctrl-U, Ctrl-W, Ctrl-K |

API reference

reorder(config) → Promise<T[] | { value, action }[]>

interface ReorderConfig<T> {
  message: string;
  options?: ReorderOption<T>[];   // alias: `items`
  items?: ReorderOption<T>[];
  pageSize?: number;              // default 10
  hideHint?: boolean;
  actions?: ReorderAction[];      // optional per-row action chips
}

interface ReorderOption<T> {
  label: string;
  value: T;
  hint?: string;
  locked?: boolean;               // can't be grabbed; blocks drag-through
  action?: string;                // initial action name (must match actions[])
}

Returns items in their new order. With actions, returns { value, action }[] instead of T[] so you also get each row's tag.

Controls:

  • Mouse: press to grab, drag to preview the drop position, release to commit. Items don't shuffle during the drag — only on release.
  • Keyboard: Space grabs/drops, ↑/↓ (or PgUp/PgDn) moves the grabbed item, Home/End jumps to the first/last unlocked slot, r resets to the original order, Enter confirms, Esc releases a grab (or cancels if nothing is held).

Actions example (git-rebase-style):

const plan = await reorder({
  message: "Rebase plan",
  items: commits.map((c) => ({ label: `${c.sha.slice(0, 7)} ${c.subject}`, value: c.sha })),
  actions: [
    { key: "p", name: "pick",   color: "green" },
    { key: "s", name: "squash", color: "yellow" },
    { key: "f", name: "fixup",  color: "yellow" },
    { key: "r", name: "reword", color: "cyan" },
    { key: "e", name: "edit",   color: "magenta" },
    { key: "d", name: "drop",   color: "red", strike: true },
  ],
});
// → [{ value: "abc1234", action: "pick" }, { value: "def5678", action: "squash" }, ...]

When actions[0].key collides with r (the default reset key), the action takes precedence — only configs without an r action keep the reset binding.

Pure helpers (exported for tests and custom widgets):

import { reorderArray, reorderArrayWithIndex } from "clickprompt";

reorderArray(["a", "b", "c", "d"], 0, 2);
// → ["b", "c", "a", "d"]

reorderArrayWithIndex(["a", "b", "c"], 0, 2, (x) => x === "b");
// → { result: ["a", "b", "c"], newIndex: 0 }  // locked "b" clamped the move

select(config) → Promise<T>

interface SelectConfig<T> {
  message: string;
  options: SelectOption<T>[];     // { label, value, hint?, disabled? }
  pageSize?: number;              // default 10
  initial?: number;               // default 0
  loop?: boolean;                 // default true
  hideHint?: boolean;             // default false
}

multiselect(config) → Promise<T[]>

All of select's options plus:

{
  defaultSelected?: T[];   // pre-checked values (matched with ===)
  min?: number;            // default 0
  max?: number;            // default Infinity
}

a toggles all (selects up to max, or clears if anything is selected). Submitting fewer than min or more than max shows an inline error instead of resolving.

text(config) → Promise<string>

{
  message: string;
  initial?: string;
  placeholder?: string;
  validate?: (value: string) => true | string | null | Promise<...>;
  mask?: string;           // used internally by password()
}
  • Supports surrogate-pair-safe cursor movement (emoji don't get cut in half).
  • Recognises pasted multi-byte chunks and inserts every printable codepoint at once (raw mode delivers a whole paste as one data event).
  • Async validate is awaited; the input is ignored while validation is in flight to keep the displayed value in sync with the submitted value.
  • Mouse mode is forced OFF for text-based prompts so the terminal's native click-drag text selection (copy/paste) still works.

password(config) → Promise<string>

Thin wrapper around text with masking.

{
  message: string;
  initial?: string;
  mask?: string;           // default "•"; pass "" for fully hidden
  validate?: Validator;
}

number(config) → Promise<number>

{
  message: string;
  initial?: number;
  min?: number;            // default -Infinity
  max?: number;            // default  Infinity
  float?: boolean;         // default true
  placeholder?: string;
}

Throws on construction if max < min. parseInt (integer mode) or Number (float mode); whitespace-only input is rejected before parsing so it can't sneak through as 0.

confirm(config) → Promise<boolean>

{
  message: string;
  initial?: boolean;       // default true (Yes highlighted)
  yesLabel?: string;       // default "Yes"
  noLabel?: string;        // default "No"
}

Mouse: click [ Yes ] or [ No ]. Keyboard: ←/→ or Tab/Shift-Tab to move highlight, y/n to commit immediately, Enter to confirm the current highlight, Esc/Ctrl-C/Ctrl-D to cancel.

toggle(config) → Promise<boolean>

{
  message: string;
  initial?: boolean;       // default false
  onLabel?: string;        // default "on"
  offLabel?: string;       // default "off"
}

Mouse: click either side. Keyboard: ←/→ to move, Tab/Shift-Tab/Space to flip, y sets on, n sets off, Enter confirms.

autocomplete(config) → Promise<T>

{
  message: string;
  options: SelectOption<T>[];
  pageSize?: number;                                    // default 8
  initial?: number;
  placeholder?: string;
  filter?: (query, option) => number | false | null;   // higher score = better
}

Default filter scores exact match (1000), prefix (500), substring (100), and in-order fuzzy match (10). A custom filter that throws is treated as "no match" rather than crashing the prompt.

spinner() → Spinner

interface Spinner {
  start(message?: string): void;
  message(message: string): void;
  stop(finalMessage?: string, success?: boolean): void;  // false → ✖ icon
  isActive(): boolean;
}

Process-exit safe: registers its own exit / uncaughtException / SIGINT handlers so the cursor is restored even if the program dies mid-spin.

form(steps, config?) → Promise<Record<string, T>>

Sequential multi-step wizard. Each step is awaited in order:

import { form, text, select, confirm } from "clickprompt";

const result = await form({
  name:      text({ message: "Project name" }),
  framework: select({ message: "Framework", options: [...] }),
  confirm:   confirm({ message: "Create?" }),
}, {
  intro: "create-app",
  outro: "All set",
  showStep: true,
});
// result: { name: "...", framework: "...", confirm: true }

Steps can be thunks that receive earlier answers:

await form({
  name: text({ message: "Name" }),
  greet: ({ name }) => confirm({ message: `Hi ${name} — proceed?` }),
});

Cancelling any step rejects the whole form with CancelledError. Partial answers are not returned; chain forms manually if you need rollback.

This works because every prompt function returns a LazyPrompt — a thenable that defers terminal setup until first await. Multiple prompts inside a form({...}) literal therefore don't race for stdin.

Decorations

import { intro, outro, note, log, logSuccess, logWarning, logError } from "clickprompt";

intro("create-app");
log("Reading config…");
note("This overwrites your config file.", "Heads up");
logWarning("Network is slow");
logSuccess("Wrote 12 files");
logError("Build failed");
outro("Done");

Every decoration accepts an optional WriteStream as the last argument so output can be routed somewhere other than process.stdout (test harnesses, multiplexed terminals).

Cancellation

Every prompt rejects with CancelledError on Esc / Ctrl-C:

import { select, CancelledError } from "clickprompt";

try {
  const value = await select({ ... });
} catch (err) {
  if (err instanceof CancelledError) {
    console.log("User cancelled");
    process.exit(130);
  }
  throw err;
}

For programmatic cancellation, pass an AbortSignal in the prompt config:

const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 5000);

await select({ ...config, signal: ctrl.signal });

Pre-aborted signals are detected before any terminal state is touched, so you don't have to worry about cleaning up after a cancelled-before-start prompt.

Shared runtime options

Every prompt config extends PromptRuntimeOpts:

interface PromptRuntimeOpts {
  useMouse?: boolean;          // force mouse on/off (auto-detect by default)
  signal?: AbortSignal;
  hideCursor?: boolean;        // default true (except text-based prompts)
  output?: WriteStream;        // default process.stdout
  input?: NodeJS.ReadStream;   // default process.stdin
}

Low-level building blocks

For custom widgets:

import {
  // Mouse
  parseMouse,           // (data) → MouseEvent | null
  parseMouseAll,        // (data) → MouseEvent[]
  stripMouse,           // remove mouse seqs, keep keys
  MouseBuffer,          // stateful, split-safe parser
  // Terminal control
  enableMouse,          // optional { hover: true } for ?1003
  disableMouse,
  hideCursor,
  showCursor,
  clearLines,
  queryCursorPosition,  // → { row, col } via DSR (ESC[6n)
  getTermSize,
  // Detection
  detectTerminal,       // → TerminalInfo
  getMouseHint,         // hint string for tmux/screen, or null
  // Keys
  parseKey,             // (data) → KeyEvent | null
  // String width (zero-dep replacement for string-width / strip-ansi)
  visualWidth,          // counts terminal columns
  styledWidth,          // visualWidth(stripAnsi(s))
  stripAnsi,            // strip CSI escape sequences
} from "clickprompt";

MouseBuffer matters when mouse sequences split across stdin chunks (SSH / slow links). Each prompt allocates its own:

const buf = new MouseBuffer();
process.stdin.on("data", (chunk) => {
  for (const ev of buf.feed(chunk)) {
    // mouse event
  }
  const keyBytes = buf.consumeRemaining();
  // pass keyBytes to parseKey()
});

visualWidth handles Hangul / CJK / fullwidth forms, ZWJ-joined emoji clusters (👨‍👩‍👧, 👨‍💻), regional indicator pairs (🇰🇷), skin tone modifiers (👋🏻), VS16-promoted emoji (☎️ vs ☎), and VS15 (text presentation).

detectTerminal()

interface TerminalInfo {
  isTTY: boolean;
  isCI: boolean;
  isLegacyWindowsCmd: boolean;   // pre-Win10 conhost
  canUseMouse: boolean;          // isTTY && !isCI && !isLegacyWindowsCmd
  isTmux: boolean;
  isScreen: boolean;
  isSSH: boolean;                // SSH_CLIENT / SSH_TTY / SSH_CONNECTION
  termProgram: string;
}

CI detection respects explicit opt-out (CI=false, CI=0). Windows 10+ conhost gets mouse enabled; only pre-Win10 falls back.

Terminal support

| Terminal | Click | Wheel | Notes | |---|---|---|---| | iTerm2 (macOS) | ✅ | ✅ | | | Terminal.app (macOS) | ✅ | ✅ | | | Windows Terminal | ✅ | ✅ | | | VS Code integrated terminal | ✅ | ✅ | | | Cursor / JetBrains terminals | ✅ | ✅ | | | Alacritty / Kitty / WezTerm | ✅ | ✅ | | | GNOME Terminal / Konsole | ✅ | ✅ | | | Windows conhost (Win 10+, powershell.exe) | ✅ | ✅ | clickprompt patches ENABLE_MOUSE_INPUT on first prompt via a one-time PowerShell pinvoke | | tmux | ✅ | ✅ | needs set -g mouse on | | GNU screen | ✅ | ✅ | needs mousetrack on | | Legacy Windows cmd.exe (Win 7 / 8) | — | — | auto-detected, keyboard-only | | CI / non-TTY | — | — | setupPrompt() throws a clear error |

Module system

clickprompt ships both ESM and CJS builds. Node picks the right one based on your project's type and how you import:

// ESM
import { select, reorder } from "clickprompt";
import { input } from "clickprompt/inquirer";

// CommonJS
const { select, reorder } = require("clickprompt");
const { input } = require("clickprompt/inquirer");

TypeScript types resolve identically in both. The exports map declares import / require / types conditions for ., ./inquirer, ./prompts, and ./package.json.

Stability

[email protected] follows semver:

  • patch (1.0.x) — bug fixes only, no API changes
  • minor (1.x.0) — additive: new prompts, options, exports. Existing code keeps working
  • major (2.0.0) — breaking changes, with at least one minor release of deprecation notice
{ "dependencies": { "clickprompt": "^1.0.0" } }

The 0.x line is no longer maintained.

Design principles

  1. Zero runtime dependencies. Ships ESM + CJS dual build.
  2. Mouse is additive. Every prompt works keyboard-only over SSH, in CI, anywhere a terminal is interactive.
  3. Drop-in friendly. API mirrors prompts and @inquirer/prompts.
  4. TypeScript-first. Strict types, full .d.ts output, no any in the public surface.
  5. Terminal-safe cleanup. Mouse mode, raw mode, cursor, and bracketed paste are always restored — on normal completion, on Esc/Ctrl-C, on SIGINT/SIGTERM, on uncaughtException, and on process.exit.

Known limitations

  • Legacy Windows cmd.exe (pre-Win10 conhost) has no SGR mouse support. Auto-detected; falls back to keyboard.
  • tmux / GNU screen need their own mouse config (set -g mouse on / mousetrack on) before clicks reach the inner process. getMouseHint() returns a one-line reminder when these are detected.
  • Non-TTY environments (CI, redirected stdin/stdout, headless containers) cannot run any prompt — setupPrompt() throws. Gate with detectTerminal().isTTY first.
  • reorder mouse drag smoothness is rate-limited by the terminal's motion event throttling, not by us. On very slow links the drag indicator may feel chunky; the final order is still correct.
  • text() / password() paste filters out control characters (\n, \r, etc.) since these are single-line inputs. To collect multi-line input use a different mechanism.
  • String width edge cases: we follow the Unicode spec (Hangul, CJK, Emoji_Presentation, ZWJ-joined clusters, regional indicator pairs, VS16-promoted emoji, skin-tone modifiers). Some older terminals render those at different widths regardless — that's a font/locale issue, not a bug here.
  • Inquirer adapter: input()'s transformer option is intentionally unsupported and throws explicitly, since clickprompt renders the input verbatim.

Tests

npm test          # 195 unit tests, native node:test runner
npm run test:cjs  # 13 CJS smoke checks

The suite covers mouse SGR parsing (including split-across-chunks via MouseBuffer), key normalization, list cursor helpers, surrogate-pair-safe text editing, paste handling, reorder lock-barrier movement, duplicate-safe index tracking, multiselect min/max validation timing, async-validator abort safety, runtime overshoot/scroll behavior, listener-leak prevention, signal/abort handling, Number.isNaN clamping, visual width (CJK / emoji / ZWJ / regional indicators / VS16), and the adapter shapes (inquirer, terkelg prompts).

Contributing

See CONTRIBUTING.md.

License

MIT © Donggyu