index.js 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. import { createPrompt, useState, useKeypress, usePrefix, isEnterKey, isBackspaceKey, makeTheme, } from '@inquirer/core';
  2. function isStepOf(value, step, min) {
  3. const valuePow = value * Math.pow(10, 6);
  4. const stepPow = step * Math.pow(10, 6);
  5. const minPow = min * Math.pow(10, 6);
  6. return (valuePow - (Number.isFinite(min) ? minPow : 0)) % stepPow === 0;
  7. }
  8. function validateNumber(value, { min, max, step, }) {
  9. if (value == null || Number.isNaN(value)) {
  10. return false;
  11. }
  12. else if (value < min || value > max) {
  13. return `Value must be between ${min} and ${max}`;
  14. }
  15. else if (step !== 'any' && !isStepOf(value, step, min)) {
  16. return `Value must be a multiple of ${step}${Number.isFinite(min) ? ` starting from ${min}` : ''}`;
  17. }
  18. return true;
  19. }
  20. export default createPrompt((config, done) => {
  21. const { validate = () => true, min = -Infinity, max = Infinity, step = 1, required = false, } = config;
  22. const theme = makeTheme(config.theme);
  23. const [status, setStatus] = useState('idle');
  24. const [value, setValue] = useState(''); // store the input value as string and convert to number on "Enter"
  25. // Ignore default if not valid.
  26. const validDefault = validateNumber(config.default, { min, max, step }) === true
  27. ? config.default?.toString()
  28. : undefined;
  29. const [defaultValue = '', setDefaultValue] = useState(validDefault);
  30. const [errorMsg, setError] = useState();
  31. const prefix = usePrefix({ status, theme });
  32. useKeypress(async (key, rl) => {
  33. // Ignore keypress while our prompt is doing other processing.
  34. if (status !== 'idle') {
  35. return;
  36. }
  37. if (isEnterKey(key)) {
  38. const input = value || defaultValue;
  39. const answer = input === '' ? undefined : Number(input);
  40. setStatus('loading');
  41. let isValid = true;
  42. if (required || answer != null) {
  43. isValid = validateNumber(answer, { min, max, step });
  44. }
  45. if (isValid === true) {
  46. isValid = await validate(answer);
  47. }
  48. if (isValid === true) {
  49. setValue(String(answer ?? ''));
  50. setStatus('done');
  51. done(answer);
  52. }
  53. else {
  54. // Reset the readline line value to the previous value. On line event, the value
  55. // get cleared, forcing the user to re-enter the value instead of fixing it.
  56. rl.write(value);
  57. setError(isValid || 'You must provide a valid numeric value');
  58. setStatus('idle');
  59. }
  60. }
  61. else if (isBackspaceKey(key) && !value) {
  62. setDefaultValue(undefined);
  63. }
  64. else if (key.name === 'tab' && !value) {
  65. setDefaultValue(undefined);
  66. rl.clearLine(0); // Remove the tab character.
  67. rl.write(defaultValue);
  68. setValue(defaultValue);
  69. }
  70. else {
  71. setValue(rl.line);
  72. setError(undefined);
  73. }
  74. });
  75. const message = theme.style.message(config.message, status);
  76. let formattedValue = value;
  77. if (status === 'done') {
  78. formattedValue = theme.style.answer(value);
  79. }
  80. let defaultStr;
  81. if (defaultValue && status !== 'done' && !value) {
  82. defaultStr = theme.style.defaultAnswer(defaultValue);
  83. }
  84. let error = '';
  85. if (errorMsg) {
  86. error = theme.style.error(errorMsg);
  87. }
  88. return [
  89. [prefix, message, defaultStr, formattedValue]
  90. .filter((v) => v !== undefined)
  91. .join(' '),
  92. error,
  93. ];
  94. });