[READ-ONLY] Mirror of https://github.com/bombshell-dev/clack. Effortlessly build beautiful command-line apps bomb.sh/docs/clack/basics/getting-started/
cli command-line command-line-app node prompt prompts
5

Configure Feed

Select the types of activity you want to include in your feed.

Refactor globalAliases (#218)

authored by

Nate Moore and committed by
GitHub
(Dec 19, 2024, 2:20 PM -0600) a83d2f88 f5603a31

+135 -67
+25
.changeset/kind-llamas-beam.md
··· 1 + --- 2 + "@clack/prompts": minor 3 + "@clack/core": minor 4 + --- 5 + 6 + Adds a new `updateSettings()` function to support new global keybindings. 7 + 8 + `updateSettings()` accepts an `aliases` object that maps custom keys to an action (`up | down | left | right | space | enter | cancel`). 9 + 10 + ```ts 11 + import { updateSettings } from "@clack/prompts"; 12 + 13 + // Support custom keybindings 14 + updateSettings({ 15 + aliases: { 16 + w: "up", 17 + a: "left", 18 + s: "down", 19 + d: "right", 20 + }, 21 + }); 22 + ``` 23 + 24 + > [!WARNING] 25 + > In order to enforce consistent, user-friendly defaults across the ecosystem, `updateSettings` does not support disabling Clack's default keybindings.
+14
.changeset/quiet-actors-wink.md
··· 1 + --- 2 + "@clack/prompts": minor 3 + "@clack/core": minor 4 + --- 5 + 6 + Updates default keybindings to support Vim motion shortcuts and map the `escape` key to cancel (`ctrl+c`). 7 + 8 + | alias | action | 9 + |------- |-------- | 10 + | `k` | up | 11 + | `l` | right | 12 + | `j` | down | 13 + | `h` | left | 14 + | `esc` | cancel |
+5 -2
packages/core/src/index.ts
··· 1 + export type { ClackState as State } from './types'; 2 + export type { ClackSettings } from './utils/settings'; 3 + 1 4 export { default as ConfirmPrompt } from './prompts/confirm'; 2 5 export { default as GroupMultiSelectPrompt } from './prompts/group-multiselect'; 3 6 export { default as MultiSelectPrompt } from './prompts/multi-select'; ··· 6 9 export { default as SelectPrompt } from './prompts/select'; 7 10 export { default as SelectKeyPrompt } from './prompts/select-key'; 8 11 export { default as TextPrompt } from './prompts/text'; 9 - export type { ClackState as State } from './types'; 10 - export { block, isCancel, setGlobalAliases } from './utils'; 12 + export { block, isCancel } from './utils'; 13 + export { updateSettings } from './utils/settings';
+11 -8
packages/core/src/prompts/prompt.ts
··· 5 5 import { cursor, erase } from 'sisteransi'; 6 6 import wrap from 'wrap-ansi'; 7 7 8 - import { ALIASES, CANCEL_SYMBOL, KEYS, diffLines, hasAliasKey, setRawMode } from '../utils'; 8 + import { CANCEL_SYMBOL, diffLines, isActionKey, setRawMode, settings } from '../utils'; 9 9 10 - import type { ClackEvents, ClackState, InferSetType } from '../types'; 10 + import type { ClackEvents, ClackState } from '../types'; 11 + import type { Action } from '../utils'; 11 12 12 13 export interface PromptOptions<Self extends Prompt> { 13 14 render(this: Omit<Self, 'prompt'>): string | undefined; ··· 181 182 if (this.state === 'error') { 182 183 this.state = 'active'; 183 184 } 184 - if (key?.name && !this._track && ALIASES.has(key.name)) { 185 - this.emit('cursor', ALIASES.get(key.name)); 186 - } 187 - if (key?.name && KEYS.has(key.name as InferSetType<typeof KEYS>)) { 188 - this.emit('cursor', key.name as InferSetType<typeof KEYS>); 185 + if (key?.name) { 186 + if (!this._track && settings.aliases.has(key.name)) { 187 + this.emit('cursor', settings.aliases.get(key.name)); 188 + } 189 + if (settings.actions.has(key.name as Action)) { 190 + this.emit('cursor', key.name as Action); 191 + } 189 192 } 190 193 if (char && (char.toLowerCase() === 'y' || char.toLowerCase() === 'n')) { 191 194 this.emit('confirm', char.toLowerCase() === 'y'); ··· 214 217 } 215 218 } 216 219 217 - if (hasAliasKey([char, key?.name, key?.sequence], 'cancel')) { 220 + if (isActionKey([char, key?.name, key?.sequence], 'cancel')) { 218 221 this.state = 'cancel'; 219 222 } 220 223 if (this.state === 'submit' || this.state === 'cancel') {
+2 -4
packages/core/src/types.ts
··· 1 - import type { KEYS } from './utils'; 2 - 3 - export type InferSetType<T> = T extends Set<infer U> ? U : never; 1 + import type { Action } from "./utils/settings"; 4 2 5 3 /** 6 4 * The state of the prompt ··· 16 14 cancel: (value?: any) => void; 17 15 submit: (value?: any) => void; 18 16 error: (value?: any) => void; 19 - cursor: (key?: InferSetType<typeof KEYS>) => void; 17 + cursor: (key?: Action) => void; 20 18 key: (key?: string) => void; 21 19 value: (value?: string) => void; 22 20 confirm: (value?: boolean) => void;
-49
packages/core/src/utils/aliases.ts
··· 1 - import type { InferSetType } from '../types'; 2 - 3 - const DEFAULT_KEYS = ['up', 'down', 'left', 'right', 'space', 'enter', 'cancel'] as const; 4 - export const KEYS = new Set(DEFAULT_KEYS); 5 - 6 - export const ALIASES = new Map<string, InferSetType<typeof KEYS>>([ 7 - ['k', 'up'], 8 - ['j', 'down'], 9 - ['h', 'left'], 10 - ['l', 'right'], 11 - ['\x03', 'cancel'], 12 - ]); 13 - 14 - /** 15 - * Set custom global aliases for the default keys - This will not overwrite existing aliases just add new ones 16 - * 17 - * @param aliases - A map of aliases to keys 18 - * @default 19 - * new Map([['k', 'up'], ['j', 'down'], ['h', 'left'], ['l', 'right'], ['\x03', 'cancel'],]) 20 - */ 21 - export function setGlobalAliases(alias: Array<[string, InferSetType<typeof KEYS>]>) { 22 - for (const [newAlias, key] of alias) { 23 - if (!ALIASES.has(newAlias)) { 24 - ALIASES.set(newAlias, key); 25 - } 26 - } 27 - } 28 - 29 - /** 30 - * Check if a key is an alias for a default key 31 - * @param key - The key to check for 32 - * @param type - The type of key to check for 33 - * @returns boolean 34 - */ 35 - export function hasAliasKey( 36 - key: string | Array<string | undefined>, 37 - type: InferSetType<typeof KEYS> 38 - ) { 39 - if (typeof key === 'string') { 40 - return ALIASES.has(key) && ALIASES.get(key) === type; 41 - } 42 - 43 - return key 44 - .map((n) => { 45 - if (n !== undefined && ALIASES.has(n) && ALIASES.get(n) === type) return true; 46 - return false; 47 - }) 48 - .includes(true); 49 - }
+3 -3
packages/core/src/utils/index.ts
··· 3 3 import * as readline from 'node:readline'; 4 4 import type { Readable } from 'node:stream'; 5 5 import { cursor } from 'sisteransi'; 6 - import { hasAliasKey } from './aliases'; 6 + import { isActionKey } from './settings'; 7 7 8 - export * from './aliases'; 9 8 export * from './string'; 9 + export * from './settings'; 10 10 11 11 const isWindows = globalThis.process.platform.startsWith('win'); 12 12 ··· 39 39 40 40 const clear = (data: Buffer, { name, sequence }: Key) => { 41 41 const str = String(data); 42 - if (hasAliasKey([str, name, sequence], 'cancel')) { 42 + if (isActionKey([str, name, sequence], 'cancel')) { 43 43 if (hideCursor) output.write(cursor.show); 44 44 process.exit(0); 45 45 return;
+73
packages/core/src/utils/settings.ts
··· 1 + const actions = ['up', 'down', 'left', 'right', 'space', 'enter', 'cancel'] as const; 2 + export type Action = (typeof actions)[number]; 3 + 4 + /** Global settings for Clack programs, stored in memory */ 5 + interface InternalClackSettings { 6 + actions: Set<Action>; 7 + aliases: Map<string, Action>; 8 + } 9 + 10 + export const settings: InternalClackSettings = { 11 + actions: new Set(actions), 12 + aliases: new Map<string, Action>([ 13 + // vim support 14 + ['k', 'up'], 15 + ['j', 'down'], 16 + ['h', 'left'], 17 + ['l', 'right'], 18 + ['\x03', 'cancel'], 19 + // opinionated defaults! 20 + ['escape', 'cancel'], 21 + ]), 22 + }; 23 + 24 + export interface ClackSettings { 25 + /** 26 + * Set custom global aliases for the default actions. 27 + * This will not overwrite existing aliases, it will only add new ones! 28 + * 29 + * @param aliases - An object that maps aliases to actions 30 + * @default { k: 'up', j: 'down', h: 'left', l: 'right', '\x03': 'cancel', 'escape': 'cancel' } 31 + */ 32 + aliases: Record<string, Action>; 33 + } 34 + 35 + export function updateSettings(updates: ClackSettings) { 36 + for (const _key in updates) { 37 + const key = _key as keyof ClackSettings; 38 + if (!Object.hasOwn(updates, key)) continue; 39 + const value = updates[key]; 40 + 41 + switch (key) { 42 + case 'aliases': { 43 + for (const alias in value) { 44 + if (!Object.hasOwn(value, alias)) continue; 45 + if (!settings.aliases.has(alias)) { 46 + settings.aliases.set(alias, value[alias]); 47 + } 48 + } 49 + break; 50 + } 51 + } 52 + } 53 + } 54 + 55 + /** 56 + * Check if a key is an alias for a default action 57 + * @param key - The raw key which might match to an action 58 + * @param action - The action to match 59 + * @returns boolean 60 + */ 61 + export function isActionKey(key: string | Array<string | undefined>, action: Action) { 62 + if (typeof key === 'string') { 63 + return settings.aliases.get(key) === action; 64 + } 65 + 66 + for (const value of key) { 67 + if (value === undefined) continue; 68 + if (isActionKey(value, action)) { 69 + return true; 70 + } 71 + } 72 + return false; 73 + }
+2 -1
packages/prompts/src/index.ts
··· 14 14 import color from 'picocolors'; 15 15 import { cursor, erase } from 'sisteransi'; 16 16 17 - export { isCancel, setGlobalAliases } from '@clack/core'; 17 + export { isCancel } from '@clack/core'; 18 + export { updateSettings, type ClackSettings } from '@clack/core'; 18 19 19 20 const unicode = isUnicodeSupported(); 20 21 const s = (c: string, fallback: string) => (unicode ? c : fallback);