{"_id":"@benevbright/read-multiline","_rev":"8-d7b54ef057f20df295129ee36cabac88","name":"@benevbright/read-multiline","dist-tags":{"latest":"0.3.3-beta.1"},"versions":{"0.3.2":{"name":"@benevbright/read-multiline","version":"0.3.2","keywords":["cli","input","multiline","readline","stdin","terminal"],"author":"","license":"MIT","_id":"@benevbright/read-multiline@0.3.2","maintainers":[{"name":"benevbright","email":"benevbright@gmail.com"}],"homepage":"https://github.com/benevbright/read-multiline#readme","bugs":{"url":"https://github.com/benevbright/read-multiline/issues"},"dist":{"shasum":"468344a9d0f2aaee935a5161221c688aced71317","tarball":"https://registry.npmjs.org/@benevbright/read-multiline/-/read-multiline-0.3.2.tgz","fileCount":28,"integrity":"sha512-byD5+UCQlAjbD8lwyBcACwNUv4WKIgeW+gBaJjOkCPfxkYKrObFK7eiFWV9oOH7xjoyq+3n/G/kZ8NRTHiOjQg==","signatures":[{"sig":"MEUCIQCVCfjmKVLlBL0pSPG1InfEm1ngwt7Uc7gQy/lAIM0JuQIgCaDJTnVUfMOOpH6Jj+FKKY8+2LS31s44ytgOJo+krTU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":120502},"main":"./dist/index.js","pnpm":{"onlyBuiltDependencies":["esbuild","lefthook"]},"type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"9c442e1e6f85fcdb4c090e8179ebb26d59a0d7f1","scripts":{"knip":"knip","lint":"oxlint -c oxlint.json src","test":"vitest run","build":"tsc","format":"oxfmt src","prepare":"lefthook install","publint":"publint","release":"pnpm run build && changeset publish","lint:fix":"oxlint -c oxlint.json --fix src","changeset":"changeset","fix:oxfmt":"oxfmt","typecheck":"tsc --noEmit","fix:oxlint":"oxlint -c oxlint.json --fix","test:watch":"vitest","format:check":"oxfmt --check src","prepublishOnly":"pnpm run build","organize-imports":"organize-imports-cli tsconfig.json"},"_npmUser":{"name":"benevbright","email":"benevbright@gmail.com"},"repository":{"url":"git+https://github.com/benevbright/read-multiline.git","type":"git"},"_npmVersion":"10.9.2","description":"Simple multi-line input reader for Node.js terminals","directories":{},"_nodeVersion":"22.16.0","_hasShrinkwrap":false,"packageManager":"pnpm@10.33.0","devDependencies":{"knip":"6.3.1","oxfmt":"0.44.0","shiki":"4.0.2","oxlint":"1.59.0","vitest":"4.1.4","graphql":"16.13.2","publint":"0.3.18","lefthook":"2.1.5","typescript":"6.0.2","@types/node":"25.5.2","sql-highlight":"6.1.0","@clack/prompts":"1.2.0","@changesets/cli":"2.30.0","@xterm/headless":"6.0.0","@inquirer/prompts":"8.4.1","organize-imports-cli":"0.10.0","graphql-language-service-parser":"1.10.4"},"_npmOperationalInternal":{"tmp":"tmp/read-multiline_0.3.2_1776021696117_0.7575605900708686","host":"s3://npm-registry-packages-npm-production"}},"0.3.3":{"name":"@benevbright/read-multiline","version":"0.3.3","keywords":["cli","input","multiline","readline","stdin","terminal"],"author":"","license":"MIT","_id":"@benevbright/read-multiline@0.3.3","maintainers":[{"name":"benevbright","email":"benevbright@gmail.com"}],"homepage":"https://github.com/benevbright/read-multiline#readme","bugs":{"url":"https://github.com/benevbright/read-multiline/issues"},"dist":{"shasum":"d3c450c9fe908c18cdeb9937074426437887412e","tarball":"https://registry.npmjs.org/@benevbright/read-multiline/-/read-multiline-0.3.3.tgz","fileCount":28,"integrity":"sha512-KuiZs7pZmpaP19KszfedlwWYuc4qhO/TJs5Swz2ppNm4mF59yUKZZ+gD3bShYXj+C55grgAs88HWjg5u+gMMLg==","signatures":[{"sig":"MEQCIAqZRIN/hieS3jMveIRbqj0LO2snvECvWtFySLa+hHXrAiADKpLkDsrYhZOg02oeI/ZbwF5fcyBtEWoeW4rpdvlKpw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":120676},"main":"./dist/index.js","pnpm":{"onlyBuiltDependencies":["esbuild","lefthook"]},"type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"c0f647f8c31770d3514de56dd68a49280f20ab30","scripts":{"knip":"knip","lint":"oxlint -c oxlint.json src","test":"vitest run","build":"tsc","format":"oxfmt src","prepare":"lefthook install","publint":"publint","release":"pnpm run build && changeset publish","lint:fix":"oxlint -c oxlint.json --fix src","changeset":"changeset","fix:oxfmt":"oxfmt","typecheck":"tsc --noEmit","fix:oxlint":"oxlint -c oxlint.json --fix","test:watch":"vitest","format:check":"oxfmt --check src","prepublishOnly":"pnpm run build","organize-imports":"organize-imports-cli tsconfig.json"},"_npmUser":{"name":"benevbright","email":"benevbright@gmail.com"},"repository":{"url":"git+https://github.com/benevbright/read-multiline.git","type":"git"},"_npmVersion":"10.9.2","description":"Simple multi-line input reader for Node.js terminals","directories":{},"_nodeVersion":"22.16.0","_hasShrinkwrap":false,"packageManager":"pnpm@10.33.0","devDependencies":{"knip":"6.3.1","oxfmt":"0.44.0","shiki":"4.0.2","oxlint":"1.59.0","vitest":"4.1.4","graphql":"16.13.2","publint":"0.3.18","lefthook":"2.1.5","typescript":"6.0.2","@types/node":"25.5.2","sql-highlight":"6.1.0","@clack/prompts":"1.2.0","@changesets/cli":"2.30.0","@xterm/headless":"6.0.0","@inquirer/prompts":"8.4.1","organize-imports-cli":"0.10.0","graphql-language-service-parser":"1.10.4"},"_npmOperationalInternal":{"tmp":"tmp/read-multiline_0.3.3_1776023732089_0.3658285597988227","host":"s3://npm-registry-packages-npm-production"}},"0.3.1-beta.0":{"name":"@benevbright/read-multiline","version":"0.3.1-beta.0","keywords":["cli","input","multiline","readline","stdin","terminal"],"author":"","license":"MIT","_id":"@benevbright/read-multiline@0.3.1-beta.0","maintainers":[{"name":"benevbright","email":"benevbright@gmail.com"}],"homepage":"https://github.com/benevbright/read-multiline#readme","bugs":{"url":"https://github.com/benevbright/read-multiline/issues"},"dist":{"shasum":"04a46194663ace650665d131a4a59f10401419d1","tarball":"https://registry.npmjs.org/@benevbright/read-multiline/-/read-multiline-0.3.1-beta.0.tgz","fileCount":28,"integrity":"sha512-BdiQSeG9e+XHAueHNCHnbqm6dXRJsiHZi2pT3PLcHClUoDRCOv9P+KCutyVY3PDx/n9jUn0Q958fIpFIuIzQ8A==","signatures":[{"sig":"MEUCIQD0tFxtRDfoydTUWjpccwYp7mkQrOeFcDfeH/K9H/ny+AIgOituS9d18UZ8evbjyMsDvzRsQrFcLilaQZ6+A/zCqFo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":123316},"main":"./dist/index.js","pnpm":{"onlyBuiltDependencies":["esbuild","lefthook"]},"type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"a781aae7f34aa9f33f905ef2dbb67b7ae24f1dd9","scripts":{"knip":"knip","lint":"oxlint -c oxlint.json src","test":"vitest run","build":"tsc","format":"oxfmt src","prepare":"lefthook install","publint":"publint","release":"pnpm run build && changeset publish","lint:fix":"oxlint -c oxlint.json --fix src","changeset":"changeset","fix:oxfmt":"oxfmt","typecheck":"tsc --noEmit","fix:oxlint":"oxlint -c oxlint.json --fix","test:watch":"vitest","format:check":"oxfmt --check src","prepublishOnly":"pnpm run build","organize-imports":"organize-imports-cli tsconfig.json"},"_npmUser":{"name":"benevbright","email":"benevbright@gmail.com"},"repository":{"url":"git+https://github.com/benevbright/read-multiline.git","type":"git"},"_npmVersion":"10.9.2","description":"Simple multi-line input reader for Node.js terminals","directories":{},"_nodeVersion":"22.16.0","_hasShrinkwrap":false,"packageManager":"pnpm@10.33.0","devDependencies":{"knip":"6.3.1","oxfmt":"0.44.0","shiki":"4.0.2","oxlint":"1.59.0","vitest":"4.1.4","graphql":"16.13.2","publint":"0.3.18","lefthook":"2.1.5","typescript":"6.0.2","@types/node":"25.6.0","sql-highlight":"6.1.0","@clack/prompts":"1.2.0","@changesets/cli":"2.30.0","@xterm/headless":"6.0.0","@inquirer/prompts":"8.4.1","organize-imports-cli":"0.10.0","graphql-language-service-parser":"1.10.4"},"_npmOperationalInternal":{"tmp":"tmp/read-multiline_0.3.1-beta.0_1776062954145_0.2519000884584319","host":"s3://npm-registry-packages-npm-production"}},"0.3.3-beta.0":{"name":"@benevbright/read-multiline","version":"0.3.3-beta.0","keywords":["cli","input","multiline","readline","stdin","terminal"],"author":"","license":"MIT","_id":"@benevbright/read-multiline@0.3.3-beta.0","maintainers":[{"name":"benevbright","email":"benevbright@gmail.com"}],"homepage":"https://github.com/toiroakr/read-multiline#readme","bugs":{"url":"https://github.com/toiroakr/read-multiline/issues"},"dist":{"shasum":"0ccaf6bd01fec15666330c1e69c3ad0092da1370","tarball":"https://registry.npmjs.org/@benevbright/read-multiline/-/read-multiline-0.3.3-beta.0.tgz","fileCount":28,"integrity":"sha512-rRS1drcdFKuQV48CS829NLDGQ61bg6xv6B7MY9mHn422aAKvYOjBWB/aWkpInu1ho9ofhdaQuveLRJQKhSVlaQ==","signatures":[{"sig":"MEUCIQDRpXd2ihS0wvT8t8fd68uh9GGOJZKNOrFsb9vV+5HJgQIgffoZUe5qoFmaHAPltR61L7xuBpoIuzW+CmIPdLwOx0s=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":130498},"main":"./dist/index.js","pnpm":{"onlyBuiltDependencies":["esbuild","lefthook"]},"type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"4911261e26c0d9d95e66b7630de80310cf29bda7","scripts":{"knip":"knip","lint":"oxlint -c oxlint.json src","test":"vitest run","build":"tsc","format":"oxfmt src","prepare":"lefthook install","publint":"publint","release":"pnpm run build && changeset publish","lint:fix":"oxlint -c oxlint.json --fix src","changeset":"changeset","fix:oxfmt":"oxfmt","typecheck":"tsc --noEmit","fix:oxlint":"oxlint -c oxlint.json --fix","test:watch":"vitest","format:check":"oxfmt --check src","prepublishOnly":"pnpm run build","organize-imports":"organize-imports-cli tsconfig.json"},"_npmUser":{"name":"benevbright","email":"benevbright@gmail.com"},"repository":{"url":"git+https://github.com/toiroakr/read-multiline.git","type":"git"},"_npmVersion":"10.9.2","description":"Simple multi-line input reader for Node.js terminals","directories":{},"_nodeVersion":"22.16.0","_hasShrinkwrap":false,"packageManager":"pnpm@10.33.1","devDependencies":{"knip":"6.6.1","oxfmt":"0.46.0","shiki":"4.0.2","oxlint":"1.61.0","vitest":"4.1.5","graphql":"16.13.2","publint":"0.3.18","lefthook":"2.1.6","typescript":"6.0.3","@types/node":"25.6.0","sql-highlight":"6.1.0","@clack/prompts":"1.2.0","@changesets/cli":"2.31.0","@xterm/headless":"6.0.0","@inquirer/prompts":"8.4.2","organize-imports-cli":"0.10.0","graphql-language-service-parser":"1.10.4"},"_npmOperationalInternal":{"tmp":"tmp/read-multiline_0.3.3-beta.0_1777386553702_0.31517205429570017","host":"s3://npm-registry-packages-npm-production"}},"0.3.3-beta.1":{"name":"@benevbright/read-multiline","version":"0.3.3-beta.1","description":"Simple multi-line input reader for Node.js terminals","keywords":["cli","input","multiline","readline","stdin","terminal"],"license":"MIT","author":"","repository":{"type":"git","url":"git+https://github.com/toiroakr/read-multiline.git"},"type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"build":"tsc","test":"vitest run","test:watch":"vitest","typecheck":"tsc --noEmit","lint":"oxlint -c oxlint.json src","lint:fix":"oxlint -c oxlint.json --fix src","knip":"knip","format":"oxfmt src","format:check":"oxfmt --check src","organize-imports":"organize-imports-cli tsconfig.json","fix:oxlint":"oxlint -c oxlint.json --fix","fix:oxfmt":"oxfmt","prepare":"lefthook install","publint":"publint","prepublishOnly":"pnpm run build","changeset":"changeset","release":"pnpm run build && changeset publish"},"devDependencies":{"@changesets/cli":"2.31.0","@clack/prompts":"1.2.0","@inquirer/prompts":"8.4.2","@types/node":"25.6.0","@xterm/headless":"6.0.0","graphql":"16.13.2","graphql-language-service-parser":"1.10.4","knip":"6.6.1","lefthook":"2.1.6","organize-imports-cli":"0.10.0","oxfmt":"0.46.0","oxlint":"1.61.0","publint":"0.3.18","shiki":"4.0.2","sql-highlight":"6.1.0","typescript":"6.0.3","vitest":"4.1.5"},"packageManager":"pnpm@10.33.1","pnpm":{"onlyBuiltDependencies":["esbuild","lefthook"]},"_id":"@benevbright/read-multiline@0.3.3-beta.1","gitHead":"f719e4e123732403f45d02cf7726d2fe85967e5c","bugs":{"url":"https://github.com/toiroakr/read-multiline/issues"},"homepage":"https://github.com/toiroakr/read-multiline#readme","_nodeVersion":"22.16.0","_npmVersion":"10.9.2","dist":{"integrity":"sha512-SIoOtnES7uR7k+wyxzjkpVZ0p4RIX0sDqlTeB0N7fH9JzWpgAdnzQqhIwtt3nzkZapALLUfVLhXA5eeQ+9cRaA==","shasum":"2ff46c0219e6f24c7040cafae1034d6fa07b8965","tarball":"https://registry.npmjs.org/@benevbright/read-multiline/-/read-multiline-0.3.3-beta.1.tgz","fileCount":28,"unpackedSize":130553,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIEbDaaGoLpCwM9IXB1+ewKr7kZVod6KLXdW8XcpaHdJvAiEAtzbbS0Jvb8srcO8dWqcBi940Xax2+QQ491vgCPZA2pw="}]},"_npmUser":{"name":"benevbright","email":"benevbright@gmail.com"},"directories":{},"maintainers":[{"name":"benevbright","email":"benevbright@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/read-multiline_0.3.3-beta.1_1777387928622_0.8868060711385406"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-12T19:21:35.960Z","modified":"2026-04-28T14:52:08.950Z","0.3.0":"2026-04-12T19:08:29.378Z","0.3.2":"2026-04-12T19:21:36.254Z","0.3.3":"2026-04-12T19:55:32.230Z","0.3.1-beta.0":"2026-04-13T06:49:14.280Z","0.3.3-beta.0":"2026-04-28T14:29:13.943Z","0.3.3-beta.1":"2026-04-28T14:52:08.841Z"},"bugs":{"url":"https://github.com/toiroakr/read-multiline/issues"},"license":"MIT","homepage":"https://github.com/toiroakr/read-multiline#readme","keywords":["cli","input","multiline","readline","stdin","terminal"],"repository":{"type":"git","url":"git+https://github.com/toiroakr/read-multiline.git"},"description":"Simple multi-line input reader for Node.js terminals","maintainers":[{"name":"benevbright","email":"benevbright@gmail.com"}],"readme":"# read-multiline\n\nSimple multi-line input reader for Node.js terminals. Solves the limitation of Node.js's built-in `readline` module which only supports single-line input.\n\n## Features\n\n- **Enter** to submit, **Shift+Enter** / **Ctrl+J** to insert newlines (swappable)\n- Arrow key cursor navigation across lines\n- **Alt+Arrow** for word jumping / history, **Ctrl+Arrow** / **Cmd+Arrow** for line/buffer jumping\n- **Delete**, **Ctrl+U**, **Ctrl+K** for forward delete and line editing\n- **Ctrl+W** to delete previous word\n- **Ctrl+Z** / **Ctrl+Y** for undo/redo\n- **Ctrl+L** to clear screen and redraw\n- Full-width (CJK) character support with correct cursor positioning\n- Bracketed paste mode for multi-line paste\n- Input history navigation (Up/Down at boundaries, Alt+Up/Down, Ctrl+P/N, PageUp/PageDown)\n- File-based persistent history with atomic (tmp+rename) save and per-entry `shouldPersist` filter\n- Initial value pre-population\n- Validation with debounced live feedback\n- Max lines / max character length enforcement\n- Terminal resize (SIGWINCH) handling\n- **Ctrl+C** / **Ctrl+D** handling\n- Non-TTY (pipe) input support\n- Theme/style system with state-dependent styling\n- Built-in presets for `@inquirer/prompts` and `@clack/prompts`\n- Optional `inlinePrompt` to render the prompt and the first input line on the same terminal line\n- `createPrompt()` factory for reusable shared configuration\n- Zero dependencies\n\nBest experience with terminals supporting the [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/) (kitty, iTerm2, WezTerm, Ghostty, foot, etc.). **Ctrl+J** always inserts a newline regardless of settings, serving as a universal fallback in all terminals.\n\n## Install\n\n```bash\npnpm add @toiroakr/read-multiline\n```\n\n## Usage\n\n```typescript\nimport { readMultiline } from \"@toiroakr/read-multiline\";\n\nconst [value, error] = await readMultiline(\"Enter your message:\", {\n  history: { filePath: \"./history.json\" },\n  maxLines: 10,\n  maxLength: 1000,\n  validate: (v) => (v.trim() === \"\" ? \"Input cannot be empty\" : undefined),\n});\n\nif (error) {\n  if (error.kind === \"cancel\") console.log(\"Cancelled\");\n  else if (error.kind === \"eof\") console.log(\"EOF\");\n} else {\n  console.log(\"You entered:\", value);\n}\n```\n\n### With presets\n\n```typescript\nimport { createPrompt, presets } from \"@toiroakr/read-multiline\";\n\n// inquirer-style prompt\nconst askInquirer = createPrompt(presets.inquirer);\nconst [name] = await askInquirer(\"What is your name?\");\nconst [bio] = await askInquirer(\"Tell me about yourself:\");\n\n// clack-style prompt\nconst askClack = createPrompt(presets.clack);\nconst [input] = await askClack(\"Enter some text:\");\n```\n\n## API\n\n### `readMultiline(prompt, options?): Promise<ReadMultilineResult>`\n\nReturns a `ReadMultilineResult` tuple:\n\n- `[string, null]` on success (submitted input)\n- `[string, { kind: \"cancel\", message: \"Input cancelled\" }]` on Ctrl+C (includes partial input)\n- `[string, { kind: \"eof\", message: \"EOF received on empty input\" }]` on Ctrl+D with empty input\n\n| Parameter | Type     | Description                                   |\n| --------- | -------- | --------------------------------------------- |\n| `prompt`  | `string` | Prompt message on the header line above input |\n\n| Option                   | Type                                             | Default          | Description                                                                                             |\n| ------------------------ | ------------------------------------------------ | ---------------- | ------------------------------------------------------------------------------------------------------- |\n| `prefix`                 | `Stateful<string>`                               | `\"> \"`           | Prefix before the prompt message. Can be state-dependent                                                |\n| `linePrefix`             | `Stateful<string>`                               | same as `prefix` | Prefix for each input line. Can be state-dependent                                                      |\n| `theme`                  | `PromptTheme`                                    | `undefined`      | Theme for styling prompt elements                                                                       |\n| `input`                  | `TTYInput`                                       | `process.stdin`  | Input stream                                                                                            |\n| `output`                 | `WritableStream`                                 | `process.stdout` | Output stream                                                                                           |\n| `initialValue`           | `string`                                         | `undefined`      | Pre-populate the input                                                                                  |\n| `history`                | `string[] \\| HistoryOptions`                     | `[]`             | History entries or file-based persistent history                                                        |\n| `historyArrowNavigation` | `\"single\" \\| \"double\" \\| \"disabled\"`             | `\"single\"`       | How Up/Down interacts with history at boundaries                                                        |\n| `maxLines`               | `number`                                         | `undefined`      | Maximum number of lines                                                                                 |\n| `maxLength`              | `number`                                         | `undefined`      | Maximum total character count                                                                           |\n| `validate`               | `(value: string) => string \\| undefined \\| null` | `undefined`      | Validation function (return error message to reject)                                                    |\n| `validateDebounceMs`     | `number`                                         | `300`            | Debounce interval for live validation                                                                   |\n| `preferNewlineOnEnter`   | `boolean`                                        | `false`          | `true`: Enter=newline, `false`: Enter=submit                                                            |\n| `disabledKeys`           | `ModifiedEnterKey[]`                             | `[]`             | Key combos to disable                                                                                   |\n| `clearAfterSubmit`       | `boolean`                                        | `true`           | **Deprecated.** Clear input from terminal after submit. Use `theme.submitRender` instead                |\n| `footer`                 | `string`                                         | `undefined`      | Fixed footer text below the editor                                                                      |\n| `helpFooter`             | `boolean \\| HelpFooterDisplayOptions`            | `true`           | Auto-generated key bindings help footer                                                                 |\n| `inlinePrompt`           | `boolean`                                        | `false`          | Render the prompt header and the first input line on the same line. See [Inline prompt](#inline-prompt) |\n\n### Layout\n\nThe prompt renders as two visual areas: a **header line** and **input lines**.\n\n```\n[prefix][prompt]        ← header line (no input text here)\n[linePrefix][line 1]    ← all input lines use linePrefix\n[linePrefix][line 2]\n```\n\nWhen `prompt` is empty and `prefix` is empty, no header line is shown.\n\n#### Inline prompt\n\nWith `inlinePrompt: true`, the prompt header and the first input line share a single terminal line. Subsequent lines (from `Shift+Enter` / `Ctrl+J`) still use `linePrefix`:\n\n```\n[prefix][prompt][line 1]     ← header and first input on the same line\n[linePrefix][line 2]         ← subsequent lines keep the normal linePrefix\n```\n\nInline mode concatenates `prefix + prompt + input` with no implicit separator — include any desired trailing space in the prompt text. Combine with a `Stateful` `prefix` and `theme.submitRender: \"preserve\"` to get an inline prompt that transitions its prefix on submit. Subsequent lines (inserted via `Shift+Enter` / `Ctrl+J`) are prefixed with `linePrefix`:\n\n```typescript\nawait readMultiline(\"Bio: \", {\n  inlinePrompt: true,\n  prefix: { pending: \"> \", submitted: \"✔ \" },\n  linePrefix: \"  \",\n  theme: { submitRender: \"preserve\" },\n});\n\n// Before typing:\n//   > Bio:\n//\n// While editing (after Shift+Enter between lines):\n//   > Bio: Hello, I'm Tom.\n//     I like TypeScript.\n//\n// After submit:\n//   ✔ Bio: Hello, I'm Tom.\n//     I like TypeScript.\n```\n\n`inlinePrompt` requires the prompt header to render on a single terminal line. If `prefix` or `prompt` (including any `Stateful` prefix variant) contains a newline, `readMultiline` throws at call time. See `examples/inline-prompt.ts` for a full runnable demo.\n\n### `Stateful<T>`\n\nOptions like `prefix`, `linePrefix`, and theme styles accept a `Stateful<T>` value — either a plain value or an object with per-state values:\n\n```typescript\n// Plain value (same in all states)\nprefix: \"> \"\n\n// State-dependent values\nprefix: {\n  pending: \"? \",      // while editing\n  submitted: \"✔ \",    // after submission\n  cancelled: \"✘ \",    // after Ctrl+C (optional, defaults to pending)\n  error: \"! \",        // on validation error (optional, defaults to pending)\n}\n```\n\n### `PromptTheme`\n\n| Property       | Type                        | Description                                            |\n| -------------- | --------------------------- | ------------------------------------------------------ |\n| `prefix`       | `Stateful<StyleTextFormat>` | Style for the prefix text                              |\n| `linePrefix`   | `Stateful<StyleTextFormat>` | Style for the line prefix text                         |\n| `prompt`       | `StyleTextFormat`           | Style for the prompt message                           |\n| `input`        | `StyleTextFormat`           | Style for user input text while editing                |\n| `answer`       | `StyleTextFormat`           | Style for the answer text after submission             |\n| `cancelAnswer` | `StyleTextFormat`           | Style for the answer text after cancellation           |\n| `submitRender` | `\"clear\" \\| \"preserve\"`     | How to render after submission (default: `\"clear\"`)    |\n| `cancelRender` | `\"clear\" \\| \"preserve\"`     | How to render after Ctrl+C or EOF (default: `\"clear\"`) |\n| `error`        | `StyleTextFormat`           | Style for validation error messages                    |\n| `success`      | `StyleTextFormat`           | Style for validation success messages                  |\n| `footer`       | `StyleTextFormat`           | Style for footer text                                  |\n\n`StyleTextFormat` is the format parameter of Node.js's `util.styleText()` — e.g. `\"bold\"`, `\"red\"`, `\"cyan\"`, `[\"strikethrough\", \"dim\"]`.\n\n### `createPrompt(shared)`\n\nCreate a reusable prompt function with shared configuration. Per-call options are shallow-merged over the shared config.\n\n```typescript\nimport { createPrompt, presets } from \"@toiroakr/read-multiline\";\n\nconst ask = createPrompt(presets.inquirer);\nconst [name] = await ask(\"Name:\");\nconst [email] = await ask(\"Email:\");\n```\n\n### Presets\n\n#### `presets.inquirer`\n\nMimics `@inquirer/prompts` visual style:\n\n```\n? Enter name:          (pending)\n  John\n\n✔ Enter name:          (submitted)\n  John\n```\n\n- Blue `?` prefix → green `✔` on submit\n- Bold prompt, cyan answer\n- Inline help footer: **Enter** submit • **Shift+Enter** newline\n- `submitRender: \"preserve\"`, `cancelRender: \"preserve\"`\n\n#### `presets.clack`\n\nMimics `@clack/prompts` visual style:\n\n```\n│                      (pending)\n◆  Enter name:\n│  John\n\n│                      (submitted)\n◇  Enter name:\n│  John\n```\n\n- Cyan `◆` → green `◇` on submit, red `■` on cancel, yellow `▲` on error\n- Gray guide bar, dim answer, strikethrough+dim cancel answer\n- `submitRender: \"preserve\"`, `cancelRender: \"preserve\"`\n\n> **Note:** Ctrl+J (0x0A) always inserts a newline regardless of `preferNewlineOnEnter`.\n> When `preferNewlineOnEnter: true` is set but the kitty keyboard protocol is not supported,\n> the option automatically falls back to `false` to ensure submit (Enter) and newline (Ctrl+J) are always available.\n\n### Key Bindings\n\nThe following table shows all key bindings and their availability across terminal types.\n\n**Legend:** \"All\" = works in all terminals, \"Kitty\" = requires [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/)\n\n#### Submit / Newline\n\n`preferNewlineOnEnter` (default `false`) swaps the role of Enter and modified Enter keys. Ctrl+J always inserts a newline regardless of this setting.\n\n| Key         | Action (`preferNewlineOnEnter: false`) | Action (`true`) | Terminal      |\n| ----------- | -------------------------------------- | --------------- | ------------- |\n| Enter       | Submit                                 | Newline         | All           |\n| Shift+Enter | Newline                                | Submit          | Kitty         |\n| Ctrl+Enter  | Newline                                | Submit          | Kitty         |\n| Cmd+Enter   | Newline                                | Submit          | Kitty (macOS) |\n| Alt+Enter   | Newline                                | Submit          | All \\*        |\n| Ctrl+J      | Newline                                | Newline         | All           |\n\n\\* Alt+Enter requires \"Use Option as Meta key\" on some macOS terminals.\n\n#### Editing\n\n| Key                                         | Action                                   | Terminal |\n| ------------------------------------------- | ---------------------------------------- | -------- |\n| Backspace                                   | Delete character backward (merges lines) | All      |\n| Delete                                      | Delete character forward (merges lines)  | All      |\n| Ctrl+U                                      | Delete to line start                     | All      |\n| Ctrl+K                                      | Delete to line end                       | All      |\n| Ctrl+W                                      | Delete previous word                     | All      |\n| Ctrl+Z / Cmd+Z                              | Undo                                     | All \\*\\* |\n| Ctrl+Y / Ctrl+Shift+Z / Cmd+Shift+Z / Cmd+Y | Redo                                     | All \\*\\* |\n| Ctrl+L                                      | Clear screen and redraw                  | All      |\n\n\\*\\* Ctrl+Z/Y work in all terminals. Cmd+Z/Y and Ctrl+Shift+Z require kitty protocol.\n\n#### Cursor Movement\n\n| Key                                  | Action                                     | Terminal      |\n| ------------------------------------ | ------------------------------------------ | ------------- |\n| Left / Right                         | Move cursor (crosses line boundaries)      | All           |\n| Up / Down                            | Move between lines (history at boundaries) | All           |\n| Alt+Left / Alt+Right                 | Word jump                                  | All           |\n| Alt+Up / Alt+Down                    | History prev / next                        | All           |\n| Ctrl+P / Ctrl+N                      | History prev / next                        | All           |\n| PageUp / PageDown                    | History prev / next                        | All           |\n| Ctrl+Left / Ctrl+Right               | Line start / end                           | All           |\n| Ctrl+Up / Ctrl+Down                  | Buffer start / end                         | All           |\n| Option+Left / Option+Right (ESC+b/f) | Word jump                                  | All (macOS)   |\n| Cmd+Left / Cmd+Right                 | Line start / end                           | Kitty (macOS) |\n| Ctrl+A / Ctrl+E                      | Line start / end                           | All           |\n| Cmd+Up / Cmd+Down                    | Buffer start / end                         | Kitty (macOS) |\n| Home / End                           | Line start / end                           | All           |\n\n#### Control\n\n| Key    | Action                                                                          | Terminal |\n| ------ | ------------------------------------------------------------------------------- | -------- |\n| Ctrl+C | Cancel (returns `[input, { kind: \"cancel\", message }]`)                         | All      |\n| Ctrl+D | Delete at cursor, or EOF if empty (returns `[input, { kind: \"eof\", message }]`) | All      |\n\n### Disabling Keys\n\nUse `disabledKeys` to ignore specific key combinations:\n\n```typescript\n// Disable Ctrl+J (e.g., if it conflicts with your app)\nawait readMultiline(\"\", { disabledKeys: [\"ctrl+j\"] });\n\n// Only allow Shift+Enter and Ctrl+J as newline\nawait readMultiline(\"\", { disabledKeys: [\"ctrl+enter\", \"cmd+enter\", \"alt+enter\"] });\n```\n\nValid values: `\"shift+enter\"`, `\"ctrl+enter\"`, `\"cmd+enter\"`, `\"alt+enter\"`, `\"ctrl+j\"`\n\n### History\n\nPass an array for in-memory history, or a `HistoryOptions` object for file-based persistence:\n\n```typescript\n// In-memory history\nawait readMultiline(\"\", { history: [\"previous input\"] });\n\n// File-based persistent history\nawait readMultiline(\"\", {\n  history: { filePath: \"~/.myapp/history.json\", maxEntries: 50 },\n});\n```\n\n| Option          | Type                         | Default     | Description                                                                                        |\n| --------------- | ---------------------------- | ----------- | -------------------------------------------------------------------------------------------------- |\n| `filePath`      | `string`                     | (required)  | JSON file path for persistence                                                                     |\n| `maxEntries`    | `number`                     | `100`       | Maximum entries to keep                                                                            |\n| `shouldPersist` | `(value: string) => boolean` | `undefined` | Predicate that returns `false` to skip persisting a submitted value (it still resolves as success) |\n\nThe file is loaded at startup and updated after each submit via a read-modify-write cycle: the current file is re-read, the new entry appended, and the result written through a sibling temp file plus `fs.rename`. This way readers never observe a partial JSON document, and entries appended by concurrent sessions after this session started are preserved. Errors are silently ignored; the parent directory is created automatically.\n\nUse `shouldPersist` to accept values that validate successfully but shouldn't be recalled via history — for example, REPL meta commands or empty submissions:\n\n```typescript\nawait readMultiline(\"> \", {\n  history: {\n    filePath: \"~/.myapp/history.json\",\n    shouldPersist: (value) => value.trim() !== \"\" && !value.startsWith(\"\\\\\"),\n  },\n});\n```\n\n#### `historyArrowNavigation`\n\nControls how Up/Down arrow keys interact with history at boundaries:\n\n- `\"single\"` (default): at boundary, one press navigates history\n- `\"double\"`: at boundary, two consecutive presses navigate history\n- `\"disabled\"`: Up/Down never triggers history — use dedicated keys (Alt+Up/Down, Ctrl+P/N, PageUp/PageDown) instead\n\n### Footer\n\nUse `footer` for custom text, `helpFooter` for auto-generated key bindings help:\n\n```typescript\n// Auto-generated help footer (detects terminal capabilities)\nawait readMultiline(\"\", { helpFooter: true });\n\n// Customized help footer\nawait readMultiline(\"\", {\n  helpFooter: {\n    items: [\"submit\", \"newline\", \"undo\"], // Choose actions and order (default: [\"submit\", \"newline\", \"undo\", \"cancel\", \"eof\"])\n    maxKeysPerAction: 3, // Show up to 3 key alternatives per action (default: 2)\n    maxLines: 1, // Limit to 1 line (default: unlimited)\n    style: \"dim\", // Overall style (default: \"dim\", or none when separator is set)\n    keyStyle: \"bold\", // Style for key labels\n    actionStyle: \"dim\", // Style for action descriptions\n    separator: \" • \", // Inline layout with separator (default: grid layout)\n  },\n});\n\n// Custom footer + help footer together\nawait readMultiline(\"\", {\n  footer: \"Type your message below\",\n  helpFooter: true,\n});\n```\n\n`helpFooter` automatically detects [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/) support and only shows keys available in the current terminal. The `preferNewlineOnEnter` and `disabledKeys` options are inherited, and terminal width is auto-calculated.\n\n### Validation\n\nWhen a `validate` function is provided:\n\n1. On submit, the input is validated. If validation fails, a red error message appears below the input and submission is blocked.\n2. After the first validation failure, validation runs on every change (debounced) with live feedback: red for errors, green \"OK\" when valid.\n3. When a theme with error visual state is configured (e.g. `presets.clack`), the prefix and line prefix switch to their error-state appearance during validation errors.\n\n### Limits\n\nWhen `maxLines` or `maxLength` is set, input beyond the limit is silently blocked and a red error message appears below the input.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}