Skip to content

Commit

Permalink
Refactor globalAliases (#218)
Browse files Browse the repository at this point in the history
  • Loading branch information
natemoo-re authored Dec 19, 2024
1 parent f5603a3 commit a83d2f8
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 67 deletions.
25 changes: 25 additions & 0 deletions .changeset/kind-llamas-beam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
"@clack/prompts": minor
"@clack/core": minor
---

Adds a new `updateSettings()` function to support new global keybindings.

`updateSettings()` accepts an `aliases` object that maps custom keys to an action (`up | down | left | right | space | enter | cancel`).

```ts
import { updateSettings } from "@clack/prompts";

// Support custom keybindings
updateSettings({
aliases: {
w: "up",
a: "left",
s: "down",
d: "right",
},
});
```

> [!WARNING]
> In order to enforce consistent, user-friendly defaults across the ecosystem, `updateSettings` does not support disabling Clack's default keybindings.
14 changes: 14 additions & 0 deletions .changeset/quiet-actors-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@clack/prompts": minor
"@clack/core": minor
---

Updates default keybindings to support Vim motion shortcuts and map the `escape` key to cancel (`ctrl+c`).

| alias | action |
|------- |-------- |
| `k` | up |
| `l` | right |
| `j` | down |
| `h` | left |
| `esc` | cancel |
7 changes: 5 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export type { ClackState as State } from './types';
export type { ClackSettings } from './utils/settings';

export { default as ConfirmPrompt } from './prompts/confirm';
export { default as GroupMultiSelectPrompt } from './prompts/group-multiselect';
export { default as MultiSelectPrompt } from './prompts/multi-select';
Expand All @@ -6,5 +9,5 @@ export { default as Prompt } from './prompts/prompt';
export { default as SelectPrompt } from './prompts/select';
export { default as SelectKeyPrompt } from './prompts/select-key';
export { default as TextPrompt } from './prompts/text';
export type { ClackState as State } from './types';
export { block, isCancel, setGlobalAliases } from './utils';
export { block, isCancel } from './utils';
export { updateSettings } from './utils/settings';
19 changes: 11 additions & 8 deletions packages/core/src/prompts/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { WriteStream } from 'node:tty';
import { cursor, erase } from 'sisteransi';
import wrap from 'wrap-ansi';

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

import type { ClackEvents, ClackState, InferSetType } from '../types';
import type { ClackEvents, ClackState } from '../types';
import type { Action } from '../utils';

export interface PromptOptions<Self extends Prompt> {
render(this: Omit<Self, 'prompt'>): string | undefined;
Expand Down Expand Up @@ -181,11 +182,13 @@ export default class Prompt {
if (this.state === 'error') {
this.state = 'active';
}
if (key?.name && !this._track && ALIASES.has(key.name)) {
this.emit('cursor', ALIASES.get(key.name));
}
if (key?.name && KEYS.has(key.name as InferSetType<typeof KEYS>)) {
this.emit('cursor', key.name as InferSetType<typeof KEYS>);
if (key?.name) {
if (!this._track && settings.aliases.has(key.name)) {
this.emit('cursor', settings.aliases.get(key.name));
}
if (settings.actions.has(key.name as Action)) {
this.emit('cursor', key.name as Action);
}
}
if (char && (char.toLowerCase() === 'y' || char.toLowerCase() === 'n')) {
this.emit('confirm', char.toLowerCase() === 'y');
Expand Down Expand Up @@ -214,7 +217,7 @@ export default class Prompt {
}
}

if (hasAliasKey([char, key?.name, key?.sequence], 'cancel')) {
if (isActionKey([char, key?.name, key?.sequence], 'cancel')) {
this.state = 'cancel';
}
if (this.state === 'submit' || this.state === 'cancel') {
Expand Down
6 changes: 2 additions & 4 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import type { KEYS } from './utils';

export type InferSetType<T> = T extends Set<infer U> ? U : never;
import type { Action } from "./utils/settings";

/**
* The state of the prompt
Expand All @@ -16,7 +14,7 @@ export interface ClackEvents {
cancel: (value?: any) => void;
submit: (value?: any) => void;
error: (value?: any) => void;
cursor: (key?: InferSetType<typeof KEYS>) => void;
cursor: (key?: Action) => void;
key: (key?: string) => void;
value: (value?: string) => void;
confirm: (value?: boolean) => void;
Expand Down
49 changes: 0 additions & 49 deletions packages/core/src/utils/aliases.ts

This file was deleted.

6 changes: 3 additions & 3 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import type { Key } from 'node:readline';
import * as readline from 'node:readline';
import type { Readable } from 'node:stream';
import { cursor } from 'sisteransi';
import { hasAliasKey } from './aliases';
import { isActionKey } from './settings';

export * from './aliases';
export * from './string';
export * from './settings';

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

Expand Down Expand Up @@ -39,7 +39,7 @@ export function block({

const clear = (data: Buffer, { name, sequence }: Key) => {
const str = String(data);
if (hasAliasKey([str, name, sequence], 'cancel')) {
if (isActionKey([str, name, sequence], 'cancel')) {
if (hideCursor) output.write(cursor.show);
process.exit(0);
return;
Expand Down
73 changes: 73 additions & 0 deletions packages/core/src/utils/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const actions = ['up', 'down', 'left', 'right', 'space', 'enter', 'cancel'] as const;
export type Action = (typeof actions)[number];

/** Global settings for Clack programs, stored in memory */
interface InternalClackSettings {
actions: Set<Action>;
aliases: Map<string, Action>;
}

export const settings: InternalClackSettings = {
actions: new Set(actions),
aliases: new Map<string, Action>([
// vim support
['k', 'up'],
['j', 'down'],
['h', 'left'],
['l', 'right'],
['\x03', 'cancel'],
// opinionated defaults!
['escape', 'cancel'],
]),
};

export interface ClackSettings {
/**
* Set custom global aliases for the default actions.
* This will not overwrite existing aliases, it will only add new ones!
*
* @param aliases - An object that maps aliases to actions
* @default { k: 'up', j: 'down', h: 'left', l: 'right', '\x03': 'cancel', 'escape': 'cancel' }
*/
aliases: Record<string, Action>;
}

export function updateSettings(updates: ClackSettings) {
for (const _key in updates) {
const key = _key as keyof ClackSettings;
if (!Object.hasOwn(updates, key)) continue;
const value = updates[key];

switch (key) {
case 'aliases': {
for (const alias in value) {
if (!Object.hasOwn(value, alias)) continue;
if (!settings.aliases.has(alias)) {
settings.aliases.set(alias, value[alias]);
}
}
break;
}
}
}
}

/**
* Check if a key is an alias for a default action
* @param key - The raw key which might match to an action
* @param action - The action to match
* @returns boolean
*/
export function isActionKey(key: string | Array<string | undefined>, action: Action) {
if (typeof key === 'string') {
return settings.aliases.get(key) === action;
}

for (const value of key) {
if (value === undefined) continue;
if (isActionKey(value, action)) {
return true;
}
}
return false;
}
3 changes: 2 additions & 1 deletion packages/prompts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import isUnicodeSupported from 'is-unicode-supported';
import color from 'picocolors';
import { cursor, erase } from 'sisteransi';

export { isCancel, setGlobalAliases } from '@clack/core';
export { isCancel } from '@clack/core';
export { updateSettings, type ClackSettings } from '@clack/core';

const unicode = isUnicodeSupported();
const s = (c: string, fallback: string) => (unicode ? c : fallback);
Expand Down

0 comments on commit a83d2f8

Please sign in to comment.