@jakubmazanec/cli 0.1.0-9029e49

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.
Files changed (167) hide show
  1. package/LICENSE.md +840 -0
  2. package/README.md +38 -0
  3. package/dist/constants.d.ts +1 -0
  4. package/dist/constants.js +3 -0
  5. package/dist/constants.js.map +1 -0
  6. package/dist/main.d.ts +2 -0
  7. package/dist/main.js +4 -0
  8. package/dist/main.js.map +1 -0
  9. package/dist/program/Command.d.ts +42 -0
  10. package/dist/program/Command.js +64 -0
  11. package/dist/program/Command.js.map +1 -0
  12. package/dist/program/CommandAction.d.ts +12 -0
  13. package/dist/program/CommandAction.js +3 -0
  14. package/dist/program/CommandAction.js.map +1 -0
  15. package/dist/program/CommandArguments.d.ts +5 -0
  16. package/dist/program/CommandArguments.js +3 -0
  17. package/dist/program/CommandArguments.js.map +1 -0
  18. package/dist/program/CommandOptions.d.ts +11 -0
  19. package/dist/program/CommandOptions.js +3 -0
  20. package/dist/program/CommandOptions.js.map +1 -0
  21. package/dist/program/Program.d.ts +40 -0
  22. package/dist/program/Program.js +105 -0
  23. package/dist/program/Program.js.map +1 -0
  24. package/dist/program/ProgramDetail.d.ts +9 -0
  25. package/dist/program/ProgramDetail.js +5 -0
  26. package/dist/program/ProgramDetail.js.map +1 -0
  27. package/dist/program/ProgramError.d.ts +4 -0
  28. package/dist/program/ProgramError.js +8 -0
  29. package/dist/program/ProgramError.js.map +1 -0
  30. package/dist/program/ProgramOptions.d.ts +13 -0
  31. package/dist/program/ProgramOptions.js +5 -0
  32. package/dist/program/ProgramOptions.js.map +1 -0
  33. package/dist/program/ProgramStreams.d.ts +9 -0
  34. package/dist/program/ProgramStreams.js +5 -0
  35. package/dist/program/ProgramStreams.js.map +1 -0
  36. package/dist/program/getProgramDetail.d.ts +6 -0
  37. package/dist/program/getProgramDetail.js +27 -0
  38. package/dist/program/getProgramDetail.js.map +1 -0
  39. package/dist/program.d.ts +10 -0
  40. package/dist/program.js +12 -0
  41. package/dist/program.js.map +1 -0
  42. package/dist/ui/ErrorDetail.d.ts +12 -0
  43. package/dist/ui/ErrorDetail.js +5 -0
  44. package/dist/ui/ErrorDetail.js.map +1 -0
  45. package/dist/ui/Failure.d.ts +15 -0
  46. package/dist/ui/Failure.js +65 -0
  47. package/dist/ui/Failure.js.map +1 -0
  48. package/dist/ui/Header.d.ts +15 -0
  49. package/dist/ui/Header.js +26 -0
  50. package/dist/ui/Header.js.map +1 -0
  51. package/dist/ui/Help.d.ts +13 -0
  52. package/dist/ui/Help.js +64 -0
  53. package/dist/ui/Help.js.map +1 -0
  54. package/dist/ui/Spinner.d.ts +4 -0
  55. package/dist/ui/Spinner.js +39 -0
  56. package/dist/ui/Spinner.js.map +1 -0
  57. package/dist/ui/colors.d.ts +10 -0
  58. package/dist/ui/colors.js +12 -0
  59. package/dist/ui/colors.js.map +1 -0
  60. package/dist/ui/form/Form.d.ts +9 -0
  61. package/dist/ui/form/Form.js +3 -0
  62. package/dist/ui/form/Form.js.map +1 -0
  63. package/dist/ui/form/FormConfig.d.ts +8 -0
  64. package/dist/ui/form/FormConfig.js +3 -0
  65. package/dist/ui/form/FormConfig.js.map +1 -0
  66. package/dist/ui/form/FormProps.d.ts +40 -0
  67. package/dist/ui/form/FormProps.js +3 -0
  68. package/dist/ui/form/FormProps.js.map +1 -0
  69. package/dist/ui/form/FormResult.d.ts +6 -0
  70. package/dist/ui/form/FormResult.js +3 -0
  71. package/dist/ui/form/FormResult.js.map +1 -0
  72. package/dist/ui/form/FormRow.d.ts +30 -0
  73. package/dist/ui/form/FormRow.js +59 -0
  74. package/dist/ui/form/FormRow.js.map +1 -0
  75. package/dist/ui/form/FormRowConfig.d.ts +14 -0
  76. package/dist/ui/form/FormRowConfig.js +5 -0
  77. package/dist/ui/form/FormRowConfig.js.map +1 -0
  78. package/dist/ui/form/FormRowsConfig.d.ts +5 -0
  79. package/dist/ui/form/FormRowsConfig.js +3 -0
  80. package/dist/ui/form/FormRowsConfig.js.map +1 -0
  81. package/dist/ui/form/FormValue.d.ts +9 -0
  82. package/dist/ui/form/FormValue.js +3 -0
  83. package/dist/ui/form/FormValue.js.map +1 -0
  84. package/dist/ui/form/FormValues.d.ts +8 -0
  85. package/dist/ui/form/FormValues.js +3 -0
  86. package/dist/ui/form/FormValues.js.map +1 -0
  87. package/dist/ui/form/SelectInput.d.ts +29 -0
  88. package/dist/ui/form/SelectInput.js +123 -0
  89. package/dist/ui/form/SelectInput.js.map +1 -0
  90. package/dist/ui/form/SelectInputItem.d.ts +9 -0
  91. package/dist/ui/form/SelectInputItem.js +5 -0
  92. package/dist/ui/form/SelectInputItem.js.map +1 -0
  93. package/dist/ui/form/TextInput.d.ts +30 -0
  94. package/dist/ui/form/TextInput.js +155 -0
  95. package/dist/ui/form/TextInput.js.map +1 -0
  96. package/dist/ui/form/createForm.d.ts +9 -0
  97. package/dist/ui/form/createForm.js +72 -0
  98. package/dist/ui/form/createForm.js.map +1 -0
  99. package/dist/ui/form.d.ts +13 -0
  100. package/dist/ui/form.js +15 -0
  101. package/dist/ui/form.js.map +1 -0
  102. package/dist/ui/getErrorDetail.d.ts +5 -0
  103. package/dist/ui/getErrorDetail.js +63 -0
  104. package/dist/ui/getErrorDetail.js.map +1 -0
  105. package/dist/ui/icons.d.ts +9 -0
  106. package/dist/ui/icons.js +13 -0
  107. package/dist/ui/icons.js.map +1 -0
  108. package/dist/ui/internals/Wrapper.d.ts +12 -0
  109. package/dist/ui/internals/Wrapper.js +26 -0
  110. package/dist/ui/internals/Wrapper.js.map +1 -0
  111. package/dist/ui/internals/programContext.d.ts +9 -0
  112. package/dist/ui/internals/programContext.js +6 -0
  113. package/dist/ui/internals/programContext.js.map +1 -0
  114. package/dist/ui/internals/rotate.d.ts +8 -0
  115. package/dist/ui/internals/rotate.js +17 -0
  116. package/dist/ui/internals/rotate.js.map +1 -0
  117. package/dist/ui/internals.d.ts +3 -0
  118. package/dist/ui/internals.js +5 -0
  119. package/dist/ui/internals.js.map +1 -0
  120. package/dist/ui/useProgram.d.ts +7 -0
  121. package/dist/ui/useProgram.js +24 -0
  122. package/dist/ui/useProgram.js.map +1 -0
  123. package/dist/ui.d.ts +10 -0
  124. package/dist/ui.js +12 -0
  125. package/dist/ui.js.map +1 -0
  126. package/package.json +61 -0
  127. package/src/constants.ts +1 -0
  128. package/src/main.ts +2 -0
  129. package/src/program/Command.ts +121 -0
  130. package/src/program/CommandAction.ts +23 -0
  131. package/src/program/CommandArguments.ts +9 -0
  132. package/src/program/CommandOptions.ts +17 -0
  133. package/src/program/Program.ts +136 -0
  134. package/src/program/ProgramDetail.ts +9 -0
  135. package/src/program/ProgramError.ts +9 -0
  136. package/src/program/ProgramOptions.ts +16 -0
  137. package/src/program/ProgramStreams.ts +8 -0
  138. package/src/program/getProgramDetail.ts +38 -0
  139. package/src/program.ts +10 -0
  140. package/src/ui/ErrorDetail.ts +12 -0
  141. package/src/ui/Failure.tsx +59 -0
  142. package/src/ui/Header.tsx +37 -0
  143. package/src/ui/Help.tsx +68 -0
  144. package/src/ui/Spinner.tsx +30 -0
  145. package/src/ui/colors.ts +13 -0
  146. package/src/ui/form/Form.ts +9 -0
  147. package/src/ui/form/FormConfig.ts +9 -0
  148. package/src/ui/form/FormProps.ts +67 -0
  149. package/src/ui/form/FormResult.ts +8 -0
  150. package/src/ui/form/FormRow.tsx +119 -0
  151. package/src/ui/form/FormRowConfig.ts +17 -0
  152. package/src/ui/form/FormRowsConfig.ts +6 -0
  153. package/src/ui/form/FormValue.ts +12 -0
  154. package/src/ui/form/FormValues.ts +9 -0
  155. package/src/ui/form/SelectInput.tsx +165 -0
  156. package/src/ui/form/SelectInputItem.ts +10 -0
  157. package/src/ui/form/TextInput.tsx +228 -0
  158. package/src/ui/form/createForm.tsx +89 -0
  159. package/src/ui/form.ts +13 -0
  160. package/src/ui/getErrorDetail.ts +73 -0
  161. package/src/ui/icons.ts +13 -0
  162. package/src/ui/internals/Wrapper.tsx +34 -0
  163. package/src/ui/internals/programContext.ts +12 -0
  164. package/src/ui/internals/rotate.ts +12 -0
  165. package/src/ui/internals.ts +3 -0
  166. package/src/ui/useProgram.ts +24 -0
  167. package/src/ui.ts +10 -0
@@ -0,0 +1,59 @@
1
+ import {Box, Text, type BoxProps} from 'ink';
2
+
3
+ import * as colors from './colors.js';
4
+ import {type ErrorDetail} from './ErrorDetail.js';
5
+ import {getErrorDetail} from './getErrorDetail.js';
6
+ import {Header} from './Header.js';
7
+ import * as icons from './icons.js';
8
+
9
+ /**
10
+ * {@linkcode Failure} component props.
11
+ */
12
+ export type FailureProps = BoxProps & {
13
+ /** Error that describes a failure state. Function {@linkcode getErrorDetail} is used for getting all the information from the error; you can use it to implement own retrieval logic and pass the result as `errorDetail` prop. */
14
+ error: Error;
15
+
16
+ /** Information about the failure state. Overrides information retireved from `error` prop. */
17
+ errorDetail?: ErrorDetail;
18
+ };
19
+
20
+ /**
21
+ * A component that renders a failure state.
22
+ */
23
+ export function Failure({error, errorDetail, ...boxProps}: FailureProps) {
24
+ let {label, description, stack, cause} = errorDetail ?? getErrorDetail(error);
25
+
26
+ return (
27
+ <Box {...boxProps} flexDirection="column">
28
+ <Header icon={icons.error}>
29
+ <Text color={colors.highlighted}>{label}</Text>
30
+ </Header>
31
+
32
+ {description ? (
33
+ <Box flexDirection="column" marginLeft={2}>
34
+ <Text color={colors.failure}>{description}</Text>
35
+ </Box>
36
+ ) : null}
37
+
38
+ {stack ? (
39
+ <Box flexDirection="column" marginTop={1} marginLeft={2}>
40
+ <Text color={colors.muted}>{stack}</Text>
41
+ </Box>
42
+ ) : null}
43
+
44
+ {cause ? (
45
+ <>
46
+ <Box flexDirection="column" marginTop={1} marginLeft={2}>
47
+ <Text color={colors.muted}>{cause.description}</Text>
48
+ </Box>
49
+
50
+ {cause.stack ? (
51
+ <Box flexDirection="column" marginTop={1} marginLeft={2}>
52
+ <Text color={colors.muted}>{cause.stack}</Text>
53
+ </Box>
54
+ ) : null}
55
+ </>
56
+ ) : null}
57
+ </Box>
58
+ );
59
+ }
@@ -0,0 +1,37 @@
1
+ import {Box, type BoxProps, Text} from 'ink';
2
+ import {type PropsWithChildren} from 'react';
3
+
4
+ import {Spinner} from './Spinner.js';
5
+
6
+ /**
7
+ * {@linkcode Header} component props.
8
+ */
9
+ export type HeaderProps = BoxProps & {
10
+ /** Icon (recommended is 1 character) to render before the header itself. */
11
+ icon?: string;
12
+
13
+ /** Show spinner instead of the icon. */
14
+ showSpinner?: boolean;
15
+ };
16
+
17
+ /**
18
+ * Header component.
19
+ */
20
+ export function Header({
21
+ children,
22
+ icon,
23
+ showSpinner = false,
24
+ ...boxProps
25
+ }: PropsWithChildren<HeaderProps>) {
26
+ return (
27
+ <Box {...boxProps}>
28
+ {icon || showSpinner ? (
29
+ <Box marginRight={1}>
30
+ {showSpinner ? <Spinner /> : null}
31
+ {!showSpinner && icon ? <Text>{icon}</Text> : null}
32
+ </Box>
33
+ ) : null}
34
+ <Box>{children}</Box>
35
+ </Box>
36
+ );
37
+ }
@@ -0,0 +1,68 @@
1
+ import chalk from 'chalk';
2
+ import {Box, Text, type BoxProps} from 'ink';
3
+
4
+ import * as colors from './colors.js';
5
+ import {Header} from './Header.js';
6
+ import * as icons from './icons.js';
7
+ import {type Program} from '../program.js';
8
+
9
+ /**
10
+ * {@linkcode Help} component props.
11
+ */
12
+ export type HelpProps = BoxProps & {
13
+ /** {@linkcode Program} instance. */
14
+ program: Program;
15
+ };
16
+
17
+ /**
18
+ * A component that renders help information.
19
+ */
20
+ export function Help({program, ...boxProps}: HelpProps) {
21
+ return (
22
+ <Box {...boxProps} flexDirection="column">
23
+ <Text>{`${chalk[colors.notice](program.name ?? '?')} ${chalk[colors.muted](
24
+ program.version ?? '?',
25
+ )}`}</Text>
26
+ <Text color={colors.body}>{program.description}</Text>
27
+
28
+ {program.commands.length ? (
29
+ <>
30
+ <Header icon={icons.info} marginTop={1}>
31
+ <Text color={colors.highlighted}>Usage</Text>
32
+ </Header>
33
+ {program.commands.map((command) => {
34
+ let line = `${chalk[colors.muted](program.bin ?? '?')} ${command.name}`;
35
+
36
+ if (command.parameters) {
37
+ if (command.parameters.some((parameterConfig) => !!parameterConfig.required)) {
38
+ line += ` ${chalk[colors.muted]('<parameters>')}`;
39
+ } else {
40
+ line += ` ${chalk[colors.muted]('[<parameters>]')}`;
41
+ }
42
+ }
43
+
44
+ if (command.options) {
45
+ if (
46
+ Object.entries(command.options).some(([, optionConfig]) => !!optionConfig.required)
47
+ ) {
48
+ line += ` ${chalk[colors.muted]('<options>')}`;
49
+ } else {
50
+ line += ` ${chalk[colors.muted]('[<options>]')}`;
51
+ }
52
+ }
53
+
54
+ if (command.description) {
55
+ line += ` ${command.description}`;
56
+ }
57
+
58
+ return (
59
+ <Box key={command.name} marginLeft={2} flexDirection="column">
60
+ <Text>{line}</Text>
61
+ </Box>
62
+ );
63
+ })}
64
+ </>
65
+ ) : null}
66
+ </Box>
67
+ );
68
+ }
@@ -0,0 +1,30 @@
1
+ import {Text} from 'ink';
2
+ import {useState, useEffect} from 'react';
3
+
4
+ const SPINNER = {
5
+ interval: 80,
6
+ frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
7
+ };
8
+
9
+ /**
10
+ * Spinner component.
11
+ */
12
+ export function Spinner() {
13
+ let [frame, setFrame] = useState(0);
14
+
15
+ useEffect(() => {
16
+ let timer = setInterval(() => {
17
+ setFrame((previousFrame) => {
18
+ let isLastFrame = previousFrame === SPINNER.frames.length - 1;
19
+
20
+ return isLastFrame ? 0 : previousFrame + 1;
21
+ });
22
+ }, SPINNER.interval);
23
+
24
+ return () => {
25
+ clearInterval(timer);
26
+ };
27
+ }, []);
28
+
29
+ return <Text>{SPINNER.frames[frame]}</Text>;
30
+ }
@@ -0,0 +1,13 @@
1
+ export const body = 'white';
2
+ export const highlighted = 'whiteBright';
3
+ export const muted = 'gray';
4
+
5
+ export const unselected = 'gray';
6
+ export const selected = 'greenBright';
7
+
8
+ export const notice = 'magenta';
9
+
10
+ export const info = 'cyan';
11
+ export const success = 'green';
12
+ export const warning = 'yellow';
13
+ export const failure = 'red';
@@ -0,0 +1,9 @@
1
+ import {type FC} from 'react';
2
+
3
+ import {type FormConfig} from './FormConfig.js';
4
+ import {type FormProps} from './FormProps.js';
5
+
6
+ /**
7
+ * Form component.
8
+ */
9
+ export type Form<C extends FormConfig> = FC<FormProps<C>> & {config: C};
@@ -0,0 +1,9 @@
1
+ import {type FormRowsConfig} from './FormRowsConfig.js';
2
+
3
+ /**
4
+ * Form configuration.
5
+ */
6
+ export type FormConfig = {
7
+ /** Form rows configuration. */
8
+ rows: FormRowsConfig;
9
+ };
@@ -0,0 +1,67 @@
1
+ import {type BoxProps} from 'ink';
2
+
3
+ import {type FormConfig} from './FormConfig.js';
4
+ import {type FormRowConfig} from './FormRowConfig.js';
5
+ import {type FormValues} from './FormValues.js';
6
+
7
+ /** A type that creates string literal union type with names of rows that need some props. */
8
+ type FormRowWithPropsNames<C extends FormConfig> = {
9
+ [P in keyof C['rows']]: C['rows'][P]['type'] extends 'text'
10
+ ? never
11
+ : C['rows'][P]['type'] extends 'confirm'
12
+ ? never
13
+ : C['rows'][P]['type'] extends 'select'
14
+ ? P
15
+ : never;
16
+ }[keyof C['rows']];
17
+
18
+ type FormPropsRow<C extends FormRowConfig> = C['type'] extends 'text'
19
+ ? never
20
+ : C['type'] extends 'confirm'
21
+ ? never
22
+ : C['type'] extends 'select'
23
+ ? {
24
+ choices: Array<{value: string; label: string}>;
25
+ }
26
+ : never;
27
+
28
+ type FormPropsRows<C extends FormConfig> = Pick<
29
+ {
30
+ [P in keyof C['rows']]: FormPropsRow<C['rows'][P]>;
31
+ },
32
+ FormRowWithPropsNames<C>
33
+ >;
34
+
35
+ /**
36
+ * Form component props.
37
+ */
38
+ export type FormProps<C extends FormConfig> = FormPropsRows<C> extends Record<
39
+ number | string | symbol,
40
+ never
41
+ >
42
+ ? BoxProps & {
43
+ /** Props for each row. */
44
+ rows?: null | undefined;
45
+
46
+ /** Form values */
47
+ values: FormValues<C>;
48
+
49
+ /** Handler function that is called when value changes. */
50
+ onChange?: ((formResult: FormValues<C>) => void) | undefined;
51
+
52
+ /** Handler function that is called when the value is accepted. */
53
+ onComplete: (formResult: FormValues<C>) => void;
54
+ }
55
+ : BoxProps & {
56
+ /** Props for each row. */
57
+ rows: FormPropsRows<C>;
58
+
59
+ /** Form values */
60
+ values: FormValues<C>;
61
+
62
+ /** Handler function that is called when value changes. */
63
+ onChange?: ((formResult: FormValues<C>) => void) | undefined;
64
+
65
+ /** Handler function that is called when the value is accepted. */
66
+ onComplete: (formResult: FormValues<C>) => void;
67
+ };
@@ -0,0 +1,8 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any -- needed */
2
+ import {type Form} from './Form.js';
3
+ import {type FormValues} from './FormValues.js';
4
+
5
+ /**
6
+ * Form result. It is the value of all form rows after the last one is accepted.
7
+ */
8
+ export type FormResult<F extends Pick<Form<any>, 'config'>> = FormValues<F['config']>;
@@ -0,0 +1,119 @@
1
+ import {type BoxProps} from 'ink';
2
+ import {useCallback} from 'react';
3
+
4
+ import {type FormConfig} from './FormConfig.js';
5
+ import {type FormProps} from './FormProps.js';
6
+ import {type FormRowConfig} from './FormRowConfig.js';
7
+ import {SelectInput} from './SelectInput.js';
8
+ import {TextInput} from './TextInput.js';
9
+
10
+ /**
11
+ * Form row component props.
12
+ */
13
+ type FormRowProps<C extends FormConfig> = BoxProps & {
14
+ /** Form row configuration. */
15
+ config: FormRowConfig;
16
+
17
+ // TODO: Should all rows props be passed to each row?
18
+ /** Props of all form rows. */
19
+ rows: FormProps<C>['rows'] | null;
20
+
21
+ /** Row name. */
22
+ rowName: keyof C['rows'];
23
+
24
+ /** Value. */
25
+ value: unknown;
26
+
27
+ /** Should component listen to Listen to user's input? Useful in case there are multiple input components at the same time and input must be "routed" to a specific component. */
28
+ focus: boolean;
29
+
30
+ /** Handler function that is called when value changes. */
31
+ onChange: (value: unknown, inputName: keyof C['rows']) => void;
32
+
33
+ /** Handler function that is called when the value is accepted. */
34
+ onComplete: (value: unknown, inputName: keyof C['rows']) => void;
35
+
36
+ /** Indicates whether value has been accepted. Component in the row then doesn't accept more input. */
37
+ isComplete: boolean;
38
+ };
39
+
40
+ /**
41
+ * Form row component.
42
+ */
43
+ export function FormRow<C extends FormConfig>({
44
+ rowName,
45
+ config,
46
+ rows,
47
+ value,
48
+ focus,
49
+ isComplete,
50
+ onChange,
51
+ onComplete,
52
+ ...boxProps
53
+ }: FormRowProps<C>) {
54
+ // TODO: Each input component handles values and events differently; the APIs should be adjusted.
55
+ let handleChange = useCallback(
56
+ (newValue: unknown) => {
57
+ onChange(newValue, rowName);
58
+ },
59
+ [rowName, onChange],
60
+ );
61
+
62
+ let handleFinish = useCallback(
63
+ (newValue: unknown) => {
64
+ onComplete(newValue, rowName);
65
+ },
66
+ [rowName, onComplete],
67
+ );
68
+
69
+ let handleSelectFinish = useCallback(
70
+ (item: {value: unknown}) => {
71
+ onComplete(item.value, rowName);
72
+ },
73
+ [rowName, onComplete],
74
+ );
75
+
76
+ switch (config.type) {
77
+ case 'text': {
78
+ return (
79
+ <TextInput
80
+ key={String(rowName)}
81
+ label={config.label ?? ''}
82
+ validate={config.validate}
83
+ value={value as string}
84
+ focus={focus}
85
+ isComplete={isComplete}
86
+ onChange={handleChange}
87
+ onSubmit={handleFinish}
88
+ {...boxProps}
89
+ />
90
+ );
91
+ }
92
+ case 'select': {
93
+ return (
94
+ <SelectInput
95
+ key={String(rowName)}
96
+ label={config.label ?? ''}
97
+ items={
98
+ rows
99
+ ? (
100
+ (
101
+ rows as {
102
+ [key: string]: unknown;
103
+ }
104
+ )[rowName as string] as {choices: Array<{value: string; label: string}>}
105
+ ).choices
106
+ : []
107
+ }
108
+ focus={focus}
109
+ isComplete={isComplete}
110
+ onSelect={handleSelectFinish}
111
+ {...boxProps}
112
+ />
113
+ );
114
+ }
115
+ default: {
116
+ return null;
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Form row configuration.
3
+ */
4
+ export type FormRowConfig =
5
+ | {
6
+ type: 'confirm';
7
+ label?: string;
8
+ }
9
+ | {
10
+ type: 'select';
11
+ label?: string;
12
+ }
13
+ | {
14
+ type: 'text';
15
+ label?: string;
16
+ validate?: (value: string) => void;
17
+ };
@@ -0,0 +1,6 @@
1
+ import {type FormRowConfig} from './FormRowConfig.js';
2
+
3
+ /**
4
+ * Form rows configuration.
5
+ */
6
+ export type FormRowsConfig = Record<string, FormRowConfig>;
@@ -0,0 +1,12 @@
1
+ import {type FormRowConfig} from './FormRowConfig.js';
2
+
3
+ export type StringFormValue = {value: string; isComplete: boolean};
4
+
5
+ /**
6
+ * Form value.
7
+ */
8
+ export type FormValue<C extends FormRowConfig> = C['type'] extends 'text'
9
+ ? StringFormValue
10
+ : C['type'] extends 'select'
11
+ ? StringFormValue
12
+ : never;
@@ -0,0 +1,9 @@
1
+ import {type FormConfig} from './FormConfig.js';
2
+ import {type FormValue} from './FormValue.js';
3
+
4
+ /**
5
+ * Form values.
6
+ */
7
+ export type FormValues<C extends FormConfig> = {
8
+ [P in keyof C['rows']]: FormValue<C['rows'][P]>;
9
+ };
@@ -0,0 +1,165 @@
1
+ import {Box, type BoxProps, Text, useInput, type Key} from 'ink';
2
+ import _ from 'lodash';
3
+ import {useState, useEffect, useRef, useCallback} from 'react';
4
+
5
+ import {type SelectInputItem} from './SelectInputItem.js';
6
+ import * as colors from '../colors.js';
7
+ import * as icons from '../icons.js';
8
+ import {rotate} from '../internals.js';
9
+
10
+ /**
11
+ * Select input component props.
12
+ */
13
+ type SelectInputProps = BoxProps & {
14
+ /** Text to display as a label. */
15
+ label: string;
16
+
17
+ /** Items to display in a list. */
18
+ items: SelectInputItem[];
19
+
20
+ /** Should component listen to Listen to user's input? Useful in case there are multiple input components at the same time and input must be "routed" to a specific component. */
21
+ focus?: boolean | undefined;
22
+
23
+ /** Index of initially selected item. */
24
+ initialIndex?: number | undefined;
25
+
26
+ /** Number of items to display. */
27
+ limit?: number | undefined;
28
+
29
+ /** Handler function that is called when item is selected. */
30
+ onSelect?: ((item: SelectInputItem) => void) | undefined;
31
+
32
+ /** Handler function that is called when item is highlighted. */
33
+ onHighlight?: ((item: SelectInputItem) => void) | undefined;
34
+
35
+ /** Indicates whether value has been accepted. Component then doesn't accept more input. */
36
+ isComplete?: boolean | undefined;
37
+ };
38
+
39
+ /**
40
+ * Select input component.
41
+ */
42
+ export function SelectInput({
43
+ label,
44
+ items = [],
45
+ focus = true,
46
+ initialIndex = 0,
47
+ limit: customLimit,
48
+ onSelect,
49
+ onHighlight,
50
+ marginTop = 0,
51
+ isComplete,
52
+ ...boxProps
53
+ }: SelectInputProps): JSX.Element {
54
+ let [rotateIndex, setRotateIndex] = useState(0);
55
+ let [selectedIndex, setSelectedIndex] = useState(initialIndex);
56
+ let hasLimit = typeof customLimit === 'number' && items.length > customLimit;
57
+ let limit = hasLimit ? Math.min(customLimit as number, items.length) : items.length;
58
+ let previousItems = useRef<SelectInputItem[]>(items);
59
+
60
+ useEffect(() => {
61
+ if (
62
+ !_.isEqual(
63
+ previousItems.current.map((item) => item.value),
64
+ items.map((item) => item.value),
65
+ )
66
+ ) {
67
+ setRotateIndex(0);
68
+ setSelectedIndex(0);
69
+ }
70
+
71
+ previousItems.current = items;
72
+ }, [items]);
73
+
74
+ let inputHandler = useCallback(
75
+ (input: string, key: Key) => {
76
+ if (input === 'k' || key.upArrow) {
77
+ let lastIndex = (hasLimit ? limit : items.length) - 1;
78
+ let atFirstIndex = selectedIndex === 0;
79
+ let nextIndex = hasLimit ? selectedIndex : lastIndex;
80
+ let nextRotateIndex = atFirstIndex ? rotateIndex + 1 : rotateIndex;
81
+ let nextSelectedIndex = atFirstIndex ? nextIndex : selectedIndex - 1;
82
+
83
+ setRotateIndex(nextRotateIndex);
84
+ setSelectedIndex(nextSelectedIndex);
85
+
86
+ let slicedItems = hasLimit ? rotate(items, nextRotateIndex).slice(0, limit) : items;
87
+
88
+ if (typeof onHighlight === 'function') {
89
+ onHighlight(slicedItems[nextSelectedIndex] as SelectInputItem);
90
+ }
91
+ }
92
+
93
+ if (input === 'j' || key.downArrow) {
94
+ let isAtLastIndex = selectedIndex === (hasLimit ? limit : items.length) - 1;
95
+ let nextIndex = hasLimit ? selectedIndex : 0;
96
+ let nextRotateIndex = isAtLastIndex ? rotateIndex - 1 : rotateIndex;
97
+ let nextSelectedIndex = isAtLastIndex ? nextIndex : selectedIndex + 1;
98
+
99
+ setRotateIndex(nextRotateIndex);
100
+ setSelectedIndex(nextSelectedIndex);
101
+
102
+ let slicedItems = hasLimit ? rotate(items, nextRotateIndex).slice(0, limit) : items;
103
+
104
+ if (typeof onHighlight === 'function') {
105
+ onHighlight(slicedItems[nextSelectedIndex] as SelectInputItem);
106
+ }
107
+ }
108
+
109
+ if (key.return) {
110
+ let slicedItems = hasLimit ? rotate(items, rotateIndex).slice(0, limit) : items;
111
+
112
+ if (typeof onSelect === 'function') {
113
+ onSelect(slicedItems[selectedIndex] as SelectInputItem);
114
+ }
115
+ }
116
+ },
117
+ [hasLimit, limit, rotateIndex, selectedIndex, items, onSelect, onHighlight],
118
+ );
119
+
120
+ useInput(inputHandler, {isActive: focus});
121
+
122
+ let visibleItem: SelectInputItem[] = hasLimit
123
+ ? rotate(items, rotateIndex).slice(0, limit)
124
+ : items;
125
+
126
+ return (
127
+ <Box {...boxProps}>
128
+ <Box marginRight={1}>
129
+ <Text>{isComplete ? icons.success : icons.question}</Text>
130
+ </Box>
131
+
132
+ <Box flexDirection="column">
133
+ <Box marginRight={1}>
134
+ <Text color={colors.highlighted}>{label}</Text>
135
+ </Box>
136
+ {isComplete ? (
137
+ <Text color={colors.body}>
138
+ {items[selectedIndex]?.label ?? String(items[selectedIndex]?.value)}
139
+ </Text>
140
+ ) : (
141
+ <Box flexDirection="column">
142
+ {visibleItem.map((item, index) => {
143
+ let isSelected = index === selectedIndex;
144
+
145
+ return (
146
+ <Box key={item.value}>
147
+ <Box marginRight={1}>
148
+ {isSelected ? (
149
+ <Text color={colors.selected}>{icons.selectedCircle}</Text>
150
+ ) : (
151
+ <Text color={colors.unselected}>{icons.unselectedCircle}</Text>
152
+ )}
153
+ </Box>
154
+ <Text
155
+ color={isSelected ? colors.highlighted : colors.muted}
156
+ >{`${item.label}`}</Text>
157
+ </Box>
158
+ );
159
+ })}
160
+ </Box>
161
+ )}
162
+ </Box>
163
+ </Box>
164
+ );
165
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Select input item.
3
+ */
4
+ export type SelectInputItem = {
5
+ /** Text to display as a label. */
6
+ label: string;
7
+
8
+ /** Value. */
9
+ value: string;
10
+ };