Skip to content

Commit a83d2f8

Browse files
authored
Refactor globalAliases (#218)
1 parent f5603a3 commit a83d2f8

File tree

9 files changed

+135
-67
lines changed

9 files changed

+135
-67
lines changed

.changeset/kind-llamas-beam.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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.

.changeset/quiet-actors-wink.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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 |

packages/core/src/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
export type { ClackState as State } from './types';
2+
export type { ClackSettings } from './utils/settings';
3+
14
export { default as ConfirmPrompt } from './prompts/confirm';
25
export { default as GroupMultiSelectPrompt } from './prompts/group-multiselect';
36
export { default as MultiSelectPrompt } from './prompts/multi-select';
@@ -6,5 +9,5 @@ export { default as Prompt } from './prompts/prompt';
69
export { default as SelectPrompt } from './prompts/select';
710
export { default as SelectKeyPrompt } from './prompts/select-key';
811
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';

packages/core/src/prompts/prompt.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import { WriteStream } from 'node:tty';
55
import { cursor, erase } from 'sisteransi';
66
import wrap from 'wrap-ansi';
77

8-
import { ALIASES, CANCEL_SYMBOL, KEYS, diffLines, hasAliasKey, setRawMode } from '../utils';
8+
import { CANCEL_SYMBOL, diffLines, isActionKey, setRawMode, settings } from '../utils';
99

10-
import type { ClackEvents, ClackState, InferSetType } from '../types';
10+
import type { ClackEvents, ClackState } from '../types';
11+
import type { Action } from '../utils';
1112

1213
export interface PromptOptions<Self extends Prompt> {
1314
render(this: Omit<Self, 'prompt'>): string | undefined;
@@ -181,11 +182,13 @@ export default class Prompt {
181182
if (this.state === 'error') {
182183
this.state = 'active';
183184
}
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+
}
189192
}
190193
if (char && (char.toLowerCase() === 'y' || char.toLowerCase() === 'n')) {
191194
this.emit('confirm', char.toLowerCase() === 'y');
@@ -214,7 +217,7 @@ export default class Prompt {
214217
}
215218
}
216219

217-
if (hasAliasKey([char, key?.name, key?.sequence], 'cancel')) {
220+
if (isActionKey([char, key?.name, key?.sequence], 'cancel')) {
218221
this.state = 'cancel';
219222
}
220223
if (this.state === 'submit' || this.state === 'cancel') {

packages/core/src/types.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
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";
42

53
/**
64
* The state of the prompt
@@ -16,7 +14,7 @@ export interface ClackEvents {
1614
cancel: (value?: any) => void;
1715
submit: (value?: any) => void;
1816
error: (value?: any) => void;
19-
cursor: (key?: InferSetType<typeof KEYS>) => void;
17+
cursor: (key?: Action) => void;
2018
key: (key?: string) => void;
2119
value: (value?: string) => void;
2220
confirm: (value?: boolean) => void;

packages/core/src/utils/aliases.ts

Lines changed: 0 additions & 49 deletions
This file was deleted.

packages/core/src/utils/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import type { Key } from 'node:readline';
33
import * as readline from 'node:readline';
44
import type { Readable } from 'node:stream';
55
import { cursor } from 'sisteransi';
6-
import { hasAliasKey } from './aliases';
6+
import { isActionKey } from './settings';
77

8-
export * from './aliases';
98
export * from './string';
9+
export * from './settings';
1010

1111
const isWindows = globalThis.process.platform.startsWith('win');
1212

@@ -39,7 +39,7 @@ export function block({
3939

4040
const clear = (data: Buffer, { name, sequence }: Key) => {
4141
const str = String(data);
42-
if (hasAliasKey([str, name, sequence], 'cancel')) {
42+
if (isActionKey([str, name, sequence], 'cancel')) {
4343
if (hideCursor) output.write(cursor.show);
4444
process.exit(0);
4545
return;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
}

packages/prompts/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import isUnicodeSupported from 'is-unicode-supported';
1414
import color from 'picocolors';
1515
import { cursor, erase } from 'sisteransi';
1616

17-
export { isCancel, setGlobalAliases } from '@clack/core';
17+
export { isCancel } from '@clack/core';
18+
export { updateSettings, type ClackSettings } from '@clack/core';
1819

1920
const unicode = isUnicodeSupported();
2021
const s = (c: string, fallback: string) => (unicode ? c : fallback);

0 commit comments

Comments
 (0)