@inquirer/select 4.3.4 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -95,15 +95,14 @@ const answer = await select({
95
95
 
96
96
  ## Options
97
97
 
98
- | Property | Type | Required | Description |
99
- | ------------ | ---------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
100
- | message | `string` | yes | The question to ask |
101
- | choices | `Choice[]` | yes | List of the available choices. |
102
- | default | `string` | no | Defines in front of which item the cursor will initially appear. When omitted, the cursor will appear on the first selectable item. |
103
- | pageSize | `number` | no | By default, lists of choice longer than 7 will be paginated. Use this option to control how many choices will appear on the screen at once. |
104
- | loop | `boolean` | no | Defaults to `true`. When set to `false`, the cursor will be constrained to the top and bottom of the choice list without looping. |
105
- | instructions | `{ navigation: string; pager: string; }` | no | Defines the help tip content. |
106
- | theme | [See Theming](#Theming) | no | Customize look of the prompt. |
98
+ | Property | Type | Required | Description |
99
+ | -------- | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
100
+ | message | `string` | yes | The question to ask |
101
+ | choices | `Choice[]` | yes | List of the available choices. |
102
+ | default | `string` | no | Defines in front of which item the cursor will initially appear. When omitted, the cursor will appear on the first selectable item. |
103
+ | pageSize | `number` | no | By default, lists of choice longer than 7 will be paginated. Use this option to control how many choices will appear on the screen at once. |
104
+ | loop | `boolean` | no | Defaults to `true`. When set to `false`, the cursor will be constrained to the top and bottom of the choice list without looping. |
105
+ | theme | [See Theming](#Theming) | no | Customize look of the prompt. |
107
106
 
108
107
  `Separator` objects can be used in the `choices` array to render non-selectable lines in the choice list. By default it'll render a line, but you can provide the text as argument (`new Separator('-- Dependencies --')`). This option is often used to add labels to groups within long list of options.
109
108
 
@@ -150,20 +149,34 @@ type Theme = {
150
149
  highlight: (text: string) => string;
151
150
  description: (text: string) => string;
152
151
  disabled: (text: string) => string;
152
+ keysHelpTip: (keys: [key: string, action: string][]) => string | undefined;
153
153
  };
154
154
  icon: {
155
155
  cursor: string;
156
156
  };
157
- helpMode: 'always' | 'never' | 'auto';
158
157
  indexMode: 'hidden' | 'number';
159
158
  };
160
159
  ```
161
160
 
162
- ### `theme.helpMode`
161
+ ### `theme.style.keysHelpTip`
163
162
 
164
- - `auto` (default): Hide the help tips after an interaction occurs.
165
- - `always`: The help tips will always show and never hide.
166
- - `never`: The help tips will never show.
163
+ This function allows you to customize the keyboard shortcuts help tip displayed below the prompt. It receives an array of key-action pairs and should return a formatted string. You can also hook here to localize the labels to different languages.
164
+
165
+ It can also returns `undefined` to hide the help tip entirely. This is the replacement for the deprecated theme option `helpMode: 'never'`.
166
+
167
+ ```js
168
+ theme: {
169
+ style: {
170
+ keysHelpTip: (keys) => {
171
+ // Return undefined to hide the help tip completely.
172
+ return undefined;
173
+
174
+ // Or customize the formatting. Or localize the labels.
175
+ return keys.map(([key, action]) => `${key}: ${action}`).join(' | ');
176
+ };
177
+ }
178
+ }
179
+ ```
167
180
 
168
181
  ### `theme.indexMode`
169
182
 
@@ -1,4 +1,4 @@
1
- import { Separator, type Theme } from '@inquirer/core';
1
+ import { Separator, type Theme, type Keybinding } from '@inquirer/core';
2
2
  import type { PartialDeep } from '@inquirer/type';
3
3
  type SelectTheme = {
4
4
  icon: {
@@ -7,9 +7,12 @@ type SelectTheme = {
7
7
  style: {
8
8
  disabled: (text: string) => string;
9
9
  description: (text: string) => string;
10
+ keysHelpTip: (keys: [key: string, action: string][]) => string | undefined;
10
11
  };
12
+ /** @deprecated Use theme.style.keysHelpTip instead */
11
13
  helpMode: 'always' | 'never' | 'auto';
12
14
  indexMode: 'hidden' | 'number';
15
+ keybindings: ReadonlyArray<Keybinding>;
13
16
  };
14
17
  type Choice<Value> = {
15
18
  value: Value;
@@ -13,9 +13,13 @@ const selectTheme = {
13
13
  style: {
14
14
  disabled: (text) => yoctocolors_cjs_1.default.dim(`- ${text}`),
15
15
  description: (text) => yoctocolors_cjs_1.default.cyan(text),
16
+ keysHelpTip: (keys) => keys
17
+ .map(([key, action]) => `${yoctocolors_cjs_1.default.bold(key)} ${yoctocolors_cjs_1.default.dim(action)}`)
18
+ .join(yoctocolors_cjs_1.default.dim(' • ')),
16
19
  },
17
- helpMode: 'auto',
20
+ helpMode: 'always',
18
21
  indexMode: 'hidden',
22
+ keybindings: [],
19
23
  };
20
24
  function isSelectable(item) {
21
25
  return !core_1.Separator.isSeparator(item) && !item.disabled;
@@ -47,11 +51,14 @@ function normalizeChoices(choices) {
47
51
  }
48
52
  exports.default = (0, core_1.createPrompt)((config, done) => {
49
53
  const { loop = true, pageSize = 7 } = config;
50
- const firstRender = (0, core_1.useRef)(true);
51
54
  const theme = (0, core_1.makeTheme)(selectTheme, config.theme);
55
+ const { keybindings } = theme;
52
56
  const [status, setStatus] = (0, core_1.useState)('idle');
53
57
  const prefix = (0, core_1.usePrefix)({ status, theme });
54
58
  const searchTimeoutRef = (0, core_1.useRef)();
59
+ // Vim keybindings (j/k) conflict with typing those letters in search,
60
+ // so search must be disabled when vim bindings are enabled
61
+ const searchEnabled = !keybindings.includes('vim');
55
62
  const items = (0, core_1.useMemo)(() => normalizeChoices(config.choices), [config.choices]);
56
63
  const bounds = (0, core_1.useMemo)(() => {
57
64
  const first = items.findIndex(isSelectable);
@@ -75,12 +82,12 @@ exports.default = (0, core_1.createPrompt)((config, done) => {
75
82
  setStatus('done');
76
83
  done(selectedChoice.value);
77
84
  }
78
- else if ((0, core_1.isUpKey)(key) || (0, core_1.isDownKey)(key)) {
85
+ else if ((0, core_1.isUpKey)(key, keybindings) || (0, core_1.isDownKey)(key, keybindings)) {
79
86
  rl.clearLine(0);
80
87
  if (loop ||
81
- ((0, core_1.isUpKey)(key) && active !== bounds.first) ||
82
- ((0, core_1.isDownKey)(key) && active !== bounds.last)) {
83
- const offset = (0, core_1.isUpKey)(key) ? -1 : 1;
88
+ ((0, core_1.isUpKey)(key, keybindings) && active !== bounds.first) ||
89
+ ((0, core_1.isDownKey)(key, keybindings) && active !== bounds.last)) {
90
+ const offset = (0, core_1.isUpKey)(key, keybindings) ? -1 : 1;
84
91
  let next = active;
85
92
  do {
86
93
  next = (next + offset + items.length) % items.length;
@@ -109,8 +116,7 @@ exports.default = (0, core_1.createPrompt)((config, done) => {
109
116
  else if ((0, core_1.isBackspaceKey)(key)) {
110
117
  rl.clearLine(0);
111
118
  }
112
- else {
113
- // Default to search
119
+ else if (searchEnabled) {
114
120
  const searchTerm = rl.line.toLowerCase();
115
121
  const matchIndex = items.findIndex((item) => {
116
122
  if (core_1.Separator.isSeparator(item) || !isSelectable(item))
@@ -129,16 +135,20 @@ exports.default = (0, core_1.createPrompt)((config, done) => {
129
135
  clearTimeout(searchTimeoutRef.current);
130
136
  }, []);
131
137
  const message = theme.style.message(config.message, status);
132
- let helpTipTop = '';
133
- let helpTipBottom = '';
134
- if (theme.helpMode === 'always' ||
135
- (theme.helpMode === 'auto' && firstRender.current)) {
136
- firstRender.current = false;
137
- if (items.length > pageSize) {
138
- helpTipBottom = `\n${theme.style.help(`(${config.instructions?.pager ?? 'Use arrow keys to reveal more choices'})`)}`;
138
+ let helpLine;
139
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
140
+ if (theme.helpMode !== 'never') {
141
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
142
+ if (config.instructions) {
143
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
144
+ const { pager, navigation } = config.instructions;
145
+ helpLine = theme.style.help(items.length > pageSize ? pager : navigation);
139
146
  }
140
147
  else {
141
- helpTipTop = theme.style.help(`(${config.instructions?.navigation ?? 'Use arrow keys'})`);
148
+ helpLine = theme.style.keysHelpTip([
149
+ ['↑↓', 'navigate'],
150
+ ['⏎', 'select'],
151
+ ]);
142
152
  }
143
153
  }
144
154
  let separatorCount = 0;
@@ -163,12 +173,22 @@ exports.default = (0, core_1.createPrompt)((config, done) => {
163
173
  loop,
164
174
  });
165
175
  if (status === 'done') {
166
- return `${prefix} ${message} ${theme.style.answer(selectedChoice.short)}`;
176
+ return [prefix, message, theme.style.answer(selectedChoice.short)]
177
+ .filter(Boolean)
178
+ .join(' ');
167
179
  }
168
- const choiceDescription = selectedChoice.description
169
- ? `\n${theme.style.description(selectedChoice.description)}`
170
- : ``;
171
- return `${[prefix, message, helpTipTop].filter(Boolean).join(' ')}\n${page}${helpTipBottom}${choiceDescription}${ansi_1.cursorHide}`;
180
+ const { description } = selectedChoice;
181
+ const lines = [
182
+ [prefix, message].filter(Boolean).join(' '),
183
+ page,
184
+ ' ',
185
+ description ? theme.style.description(description) : '',
186
+ helpLine,
187
+ ]
188
+ .filter(Boolean)
189
+ .join('\n')
190
+ .trimEnd();
191
+ return `${lines}${ansi_1.cursorHide}`;
172
192
  });
173
193
  var core_2 = require("@inquirer/core");
174
194
  Object.defineProperty(exports, "Separator", { enumerable: true, get: function () { return core_2.Separator; } });
@@ -1,4 +1,4 @@
1
- import { Separator, type Theme } from '@inquirer/core';
1
+ import { Separator, type Theme, type Keybinding } from '@inquirer/core';
2
2
  import type { PartialDeep } from '@inquirer/type';
3
3
  type SelectTheme = {
4
4
  icon: {
@@ -7,9 +7,12 @@ type SelectTheme = {
7
7
  style: {
8
8
  disabled: (text: string) => string;
9
9
  description: (text: string) => string;
10
+ keysHelpTip: (keys: [key: string, action: string][]) => string | undefined;
10
11
  };
12
+ /** @deprecated Use theme.style.keysHelpTip instead */
11
13
  helpMode: 'always' | 'never' | 'auto';
12
14
  indexMode: 'hidden' | 'number';
15
+ keybindings: ReadonlyArray<Keybinding>;
13
16
  };
14
17
  type Choice<Value> = {
15
18
  value: Value;
package/dist/esm/index.js CHANGED
@@ -7,9 +7,13 @@ const selectTheme = {
7
7
  style: {
8
8
  disabled: (text) => colors.dim(`- ${text}`),
9
9
  description: (text) => colors.cyan(text),
10
+ keysHelpTip: (keys) => keys
11
+ .map(([key, action]) => `${colors.bold(key)} ${colors.dim(action)}`)
12
+ .join(colors.dim(' • ')),
10
13
  },
11
- helpMode: 'auto',
14
+ helpMode: 'always',
12
15
  indexMode: 'hidden',
16
+ keybindings: [],
13
17
  };
14
18
  function isSelectable(item) {
15
19
  return !Separator.isSeparator(item) && !item.disabled;
@@ -41,11 +45,14 @@ function normalizeChoices(choices) {
41
45
  }
42
46
  export default createPrompt((config, done) => {
43
47
  const { loop = true, pageSize = 7 } = config;
44
- const firstRender = useRef(true);
45
48
  const theme = makeTheme(selectTheme, config.theme);
49
+ const { keybindings } = theme;
46
50
  const [status, setStatus] = useState('idle');
47
51
  const prefix = usePrefix({ status, theme });
48
52
  const searchTimeoutRef = useRef();
53
+ // Vim keybindings (j/k) conflict with typing those letters in search,
54
+ // so search must be disabled when vim bindings are enabled
55
+ const searchEnabled = !keybindings.includes('vim');
49
56
  const items = useMemo(() => normalizeChoices(config.choices), [config.choices]);
50
57
  const bounds = useMemo(() => {
51
58
  const first = items.findIndex(isSelectable);
@@ -69,12 +76,12 @@ export default createPrompt((config, done) => {
69
76
  setStatus('done');
70
77
  done(selectedChoice.value);
71
78
  }
72
- else if (isUpKey(key) || isDownKey(key)) {
79
+ else if (isUpKey(key, keybindings) || isDownKey(key, keybindings)) {
73
80
  rl.clearLine(0);
74
81
  if (loop ||
75
- (isUpKey(key) && active !== bounds.first) ||
76
- (isDownKey(key) && active !== bounds.last)) {
77
- const offset = isUpKey(key) ? -1 : 1;
82
+ (isUpKey(key, keybindings) && active !== bounds.first) ||
83
+ (isDownKey(key, keybindings) && active !== bounds.last)) {
84
+ const offset = isUpKey(key, keybindings) ? -1 : 1;
78
85
  let next = active;
79
86
  do {
80
87
  next = (next + offset + items.length) % items.length;
@@ -103,8 +110,7 @@ export default createPrompt((config, done) => {
103
110
  else if (isBackspaceKey(key)) {
104
111
  rl.clearLine(0);
105
112
  }
106
- else {
107
- // Default to search
113
+ else if (searchEnabled) {
108
114
  const searchTerm = rl.line.toLowerCase();
109
115
  const matchIndex = items.findIndex((item) => {
110
116
  if (Separator.isSeparator(item) || !isSelectable(item))
@@ -123,16 +129,20 @@ export default createPrompt((config, done) => {
123
129
  clearTimeout(searchTimeoutRef.current);
124
130
  }, []);
125
131
  const message = theme.style.message(config.message, status);
126
- let helpTipTop = '';
127
- let helpTipBottom = '';
128
- if (theme.helpMode === 'always' ||
129
- (theme.helpMode === 'auto' && firstRender.current)) {
130
- firstRender.current = false;
131
- if (items.length > pageSize) {
132
- helpTipBottom = `\n${theme.style.help(`(${config.instructions?.pager ?? 'Use arrow keys to reveal more choices'})`)}`;
132
+ let helpLine;
133
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
134
+ if (theme.helpMode !== 'never') {
135
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
136
+ if (config.instructions) {
137
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
138
+ const { pager, navigation } = config.instructions;
139
+ helpLine = theme.style.help(items.length > pageSize ? pager : navigation);
133
140
  }
134
141
  else {
135
- helpTipTop = theme.style.help(`(${config.instructions?.navigation ?? 'Use arrow keys'})`);
142
+ helpLine = theme.style.keysHelpTip([
143
+ ['↑↓', 'navigate'],
144
+ ['⏎', 'select'],
145
+ ]);
136
146
  }
137
147
  }
138
148
  let separatorCount = 0;
@@ -157,11 +167,21 @@ export default createPrompt((config, done) => {
157
167
  loop,
158
168
  });
159
169
  if (status === 'done') {
160
- return `${prefix} ${message} ${theme.style.answer(selectedChoice.short)}`;
170
+ return [prefix, message, theme.style.answer(selectedChoice.short)]
171
+ .filter(Boolean)
172
+ .join(' ');
161
173
  }
162
- const choiceDescription = selectedChoice.description
163
- ? `\n${theme.style.description(selectedChoice.description)}`
164
- : ``;
165
- return `${[prefix, message, helpTipTop].filter(Boolean).join(' ')}\n${page}${helpTipBottom}${choiceDescription}${cursorHide}`;
174
+ const { description } = selectedChoice;
175
+ const lines = [
176
+ [prefix, message].filter(Boolean).join(' '),
177
+ page,
178
+ ' ',
179
+ description ? theme.style.description(description) : '',
180
+ helpLine,
181
+ ]
182
+ .filter(Boolean)
183
+ .join('\n')
184
+ .trimEnd();
185
+ return `${lines}${cursorHide}`;
166
186
  });
167
187
  export { Separator } from '@inquirer/core';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inquirer/select",
3
- "version": "4.3.4",
3
+ "version": "4.4.0",
4
4
  "description": "Inquirer select/list prompt",
5
5
  "keywords": [
6
6
  "answer",
@@ -74,15 +74,15 @@
74
74
  "tsc": "tshy"
75
75
  },
76
76
  "dependencies": {
77
- "@inquirer/ansi": "^1.0.0",
78
- "@inquirer/core": "^10.2.2",
79
- "@inquirer/figures": "^1.0.13",
80
- "@inquirer/type": "^3.0.8",
77
+ "@inquirer/ansi": "^1.0.1",
78
+ "@inquirer/core": "^10.3.0",
79
+ "@inquirer/figures": "^1.0.14",
80
+ "@inquirer/type": "^3.0.9",
81
81
  "yoctocolors-cjs": "^2.1.2"
82
82
  },
83
83
  "devDependencies": {
84
84
  "@arethetypeswrong/cli": "^0.18.2",
85
- "@inquirer/testing": "^2.1.50",
85
+ "@inquirer/testing": "^2.1.51",
86
86
  "tshy": "^3.0.2"
87
87
  },
88
88
  "engines": {
@@ -108,5 +108,5 @@
108
108
  "optional": true
109
109
  }
110
110
  },
111
- "gitHead": "3fdf43342deb468ee41d698e407d8800700e08dc"
111
+ "gitHead": "87cb01e67a25983bdaf0d74a7685915c0afb5f23"
112
112
  }