@jakubmazanec/cli 0.1.0-11dd4a3
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/LICENSE.md +840 -0
- package/README.md +38 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +3 -0
- package/dist/constants.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +4 -0
- package/dist/main.js.map +1 -0
- package/dist/program/Command.d.ts +42 -0
- package/dist/program/Command.js +64 -0
- package/dist/program/Command.js.map +1 -0
- package/dist/program/CommandAction.d.ts +12 -0
- package/dist/program/CommandAction.js +3 -0
- package/dist/program/CommandAction.js.map +1 -0
- package/dist/program/CommandArguments.d.ts +5 -0
- package/dist/program/CommandArguments.js +3 -0
- package/dist/program/CommandArguments.js.map +1 -0
- package/dist/program/CommandOptions.d.ts +11 -0
- package/dist/program/CommandOptions.js +3 -0
- package/dist/program/CommandOptions.js.map +1 -0
- package/dist/program/Program.d.ts +40 -0
- package/dist/program/Program.js +105 -0
- package/dist/program/Program.js.map +1 -0
- package/dist/program/ProgramDetail.d.ts +9 -0
- package/dist/program/ProgramDetail.js +5 -0
- package/dist/program/ProgramDetail.js.map +1 -0
- package/dist/program/ProgramError.d.ts +4 -0
- package/dist/program/ProgramError.js +8 -0
- package/dist/program/ProgramError.js.map +1 -0
- package/dist/program/ProgramOptions.d.ts +13 -0
- package/dist/program/ProgramOptions.js +5 -0
- package/dist/program/ProgramOptions.js.map +1 -0
- package/dist/program/ProgramStreams.d.ts +9 -0
- package/dist/program/ProgramStreams.js +5 -0
- package/dist/program/ProgramStreams.js.map +1 -0
- package/dist/program/getProgramDetail.d.ts +6 -0
- package/dist/program/getProgramDetail.js +27 -0
- package/dist/program/getProgramDetail.js.map +1 -0
- package/dist/program.d.ts +10 -0
- package/dist/program.js +12 -0
- package/dist/program.js.map +1 -0
- package/dist/ui/ErrorDetail.d.ts +12 -0
- package/dist/ui/ErrorDetail.js +5 -0
- package/dist/ui/ErrorDetail.js.map +1 -0
- package/dist/ui/Failure.d.ts +15 -0
- package/dist/ui/Failure.js +65 -0
- package/dist/ui/Failure.js.map +1 -0
- package/dist/ui/Header.d.ts +15 -0
- package/dist/ui/Header.js +26 -0
- package/dist/ui/Header.js.map +1 -0
- package/dist/ui/Help.d.ts +13 -0
- package/dist/ui/Help.js +64 -0
- package/dist/ui/Help.js.map +1 -0
- package/dist/ui/Spinner.d.ts +4 -0
- package/dist/ui/Spinner.js +39 -0
- package/dist/ui/Spinner.js.map +1 -0
- package/dist/ui/colors.d.ts +10 -0
- package/dist/ui/colors.js +12 -0
- package/dist/ui/colors.js.map +1 -0
- package/dist/ui/form/Form.d.ts +9 -0
- package/dist/ui/form/Form.js +3 -0
- package/dist/ui/form/Form.js.map +1 -0
- package/dist/ui/form/FormConfig.d.ts +8 -0
- package/dist/ui/form/FormConfig.js +3 -0
- package/dist/ui/form/FormConfig.js.map +1 -0
- package/dist/ui/form/FormProps.d.ts +40 -0
- package/dist/ui/form/FormProps.js +3 -0
- package/dist/ui/form/FormProps.js.map +1 -0
- package/dist/ui/form/FormResult.d.ts +6 -0
- package/dist/ui/form/FormResult.js +3 -0
- package/dist/ui/form/FormResult.js.map +1 -0
- package/dist/ui/form/FormRow.d.ts +30 -0
- package/dist/ui/form/FormRow.js +59 -0
- package/dist/ui/form/FormRow.js.map +1 -0
- package/dist/ui/form/FormRowConfig.d.ts +14 -0
- package/dist/ui/form/FormRowConfig.js +5 -0
- package/dist/ui/form/FormRowConfig.js.map +1 -0
- package/dist/ui/form/FormRowsConfig.d.ts +5 -0
- package/dist/ui/form/FormRowsConfig.js +3 -0
- package/dist/ui/form/FormRowsConfig.js.map +1 -0
- package/dist/ui/form/FormValue.d.ts +9 -0
- package/dist/ui/form/FormValue.js +3 -0
- package/dist/ui/form/FormValue.js.map +1 -0
- package/dist/ui/form/FormValues.d.ts +8 -0
- package/dist/ui/form/FormValues.js +3 -0
- package/dist/ui/form/FormValues.js.map +1 -0
- package/dist/ui/form/SelectInput.d.ts +29 -0
- package/dist/ui/form/SelectInput.js +123 -0
- package/dist/ui/form/SelectInput.js.map +1 -0
- package/dist/ui/form/SelectInputItem.d.ts +9 -0
- package/dist/ui/form/SelectInputItem.js +5 -0
- package/dist/ui/form/SelectInputItem.js.map +1 -0
- package/dist/ui/form/TextInput.d.ts +30 -0
- package/dist/ui/form/TextInput.js +155 -0
- package/dist/ui/form/TextInput.js.map +1 -0
- package/dist/ui/form/createForm.d.ts +9 -0
- package/dist/ui/form/createForm.js +72 -0
- package/dist/ui/form/createForm.js.map +1 -0
- package/dist/ui/form.d.ts +13 -0
- package/dist/ui/form.js +15 -0
- package/dist/ui/form.js.map +1 -0
- package/dist/ui/getErrorDetail.d.ts +5 -0
- package/dist/ui/getErrorDetail.js +63 -0
- package/dist/ui/getErrorDetail.js.map +1 -0
- package/dist/ui/icons.d.ts +9 -0
- package/dist/ui/icons.js +13 -0
- package/dist/ui/icons.js.map +1 -0
- package/dist/ui/internals/Wrapper.d.ts +12 -0
- package/dist/ui/internals/Wrapper.js +26 -0
- package/dist/ui/internals/Wrapper.js.map +1 -0
- package/dist/ui/internals/programContext.d.ts +9 -0
- package/dist/ui/internals/programContext.js +6 -0
- package/dist/ui/internals/programContext.js.map +1 -0
- package/dist/ui/internals/rotate.d.ts +8 -0
- package/dist/ui/internals/rotate.js +17 -0
- package/dist/ui/internals/rotate.js.map +1 -0
- package/dist/ui/internals.d.ts +3 -0
- package/dist/ui/internals.js +5 -0
- package/dist/ui/internals.js.map +1 -0
- package/dist/ui/useProgram.d.ts +7 -0
- package/dist/ui/useProgram.js +24 -0
- package/dist/ui/useProgram.js.map +1 -0
- package/dist/ui.d.ts +10 -0
- package/dist/ui.js +12 -0
- package/dist/ui.js.map +1 -0
- package/package.json +61 -0
- package/src/constants.ts +1 -0
- package/src/main.ts +2 -0
- package/src/program/Command.ts +121 -0
- package/src/program/CommandAction.ts +23 -0
- package/src/program/CommandArguments.ts +9 -0
- package/src/program/CommandOptions.ts +17 -0
- package/src/program/Program.ts +136 -0
- package/src/program/ProgramDetail.ts +9 -0
- package/src/program/ProgramError.ts +9 -0
- package/src/program/ProgramOptions.ts +16 -0
- package/src/program/ProgramStreams.ts +8 -0
- package/src/program/getProgramDetail.ts +38 -0
- package/src/program.ts +10 -0
- package/src/ui/ErrorDetail.ts +12 -0
- package/src/ui/Failure.tsx +59 -0
- package/src/ui/Header.tsx +37 -0
- package/src/ui/Help.tsx +68 -0
- package/src/ui/Spinner.tsx +30 -0
- package/src/ui/colors.ts +13 -0
- package/src/ui/form/Form.ts +9 -0
- package/src/ui/form/FormConfig.ts +9 -0
- package/src/ui/form/FormProps.ts +67 -0
- package/src/ui/form/FormResult.ts +8 -0
- package/src/ui/form/FormRow.tsx +119 -0
- package/src/ui/form/FormRowConfig.ts +17 -0
- package/src/ui/form/FormRowsConfig.ts +6 -0
- package/src/ui/form/FormValue.ts +12 -0
- package/src/ui/form/FormValues.ts +9 -0
- package/src/ui/form/SelectInput.tsx +165 -0
- package/src/ui/form/SelectInputItem.ts +10 -0
- package/src/ui/form/TextInput.tsx +228 -0
- package/src/ui/form/createForm.tsx +89 -0
- package/src/ui/form.ts +13 -0
- package/src/ui/getErrorDetail.ts +73 -0
- package/src/ui/icons.ts +13 -0
- package/src/ui/internals/Wrapper.tsx +34 -0
- package/src/ui/internals/programContext.ts +12 -0
- package/src/ui/internals/rotate.ts +12 -0
- package/src/ui/internals.ts +3 -0
- package/src/ui/useProgram.ts +24 -0
- 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
|
+
}
|
package/src/ui/Help.tsx
ADDED
|
@@ -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
|
+
}
|
package/src/ui/colors.ts
ADDED
|
@@ -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,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,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,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
|
+
}
|