Skip to content

Commit

Permalink
feat: improve create-tnf's style
Browse files Browse the repository at this point in the history
  • Loading branch information
sorrycc committed Dec 25, 2024
1 parent 9260ed2 commit 4483be0
Show file tree
Hide file tree
Showing 21 changed files with 1,805 additions and 70 deletions.
2 changes: 1 addition & 1 deletion create-tnf/.fatherrc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig } from 'father';

export default defineConfig({
cjs: {
esm: {
input: 'src',
output: 'dist',
},
Expand Down
2 changes: 1 addition & 1 deletion create-tnf/bin/create-tnf.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env node

require('../dist/cli');
import('../dist/cli.js');
8 changes: 6 additions & 2 deletions create-tnf/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"type": "git",
"url": "https://github.com/umijs/tnf"
},
"type": "module",
"bin": {
"create-tnf": "bin/create-tnf.js"
},
Expand All @@ -22,9 +23,12 @@
"templates"
],
"dependencies": {
"@clack/prompts": "^0.7.0",
"@clack/prompts": "^0.9.0",
"yargs-parser": "^21.1.1",
"is-unicode-supported": "^1.3.0",
"picocolors": "^1.1.1",
"yargs-parser": "^21.1.1"
"sisteransi": "^1.0.5",
"wrap-ansi": "^8.1.0"
},
"devDependencies": {
"@types/yargs-parser": "^21.0.3",
Expand Down
3 changes: 3 additions & 0 deletions create-tnf/src/clack/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @clack/core and @clack/prompt

Forked from https://github.com/sveltejs/cli .
10 changes: 10 additions & 0 deletions create-tnf/src/clack/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export { default as ConfirmPrompt } from './src/prompts/confirm.js';
export { default as GroupMultiSelectPrompt } from './src/prompts/group-multiselect.js';
export { default as MultiSelectPrompt } from './src/prompts/multi-select.js';
export { default as PasswordPrompt } from './src/prompts/password.js';
export { default as Prompt, isCancel } from './src/prompts/prompt.js';
export type { State } from './src/prompts/prompt.js';
export { default as SelectPrompt } from './src/prompts/select.js';
export { default as SelectKeyPrompt } from './src/prompts/select-key.js';
export { default as TextPrompt } from './src/prompts/text.js';
export { block } from './src/utils.js';
37 changes: 37 additions & 0 deletions create-tnf/src/clack/core/src/prompts/confirm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { cursor } from 'sisteransi';
import Prompt, { type PromptOptions } from './prompt.js';

interface ConfirmOptions extends PromptOptions<ConfirmPrompt> {
active: string;
inactive: string;
initialValue?: boolean;
}
export default class ConfirmPrompt extends Prompt {
get cursor(): 0 | 1 {
return this.value ? 0 : 1;
}

private get _value() {
return this.cursor === 0;
}

constructor(opts: ConfirmOptions) {
super(opts, false);
this.value = opts.initialValue ? true : false;

this.on('value', () => {
this.value = this._value;
});

this.on('confirm', (confirm) => {
this.output.write(cursor.move(0, -1));
this.value = confirm;
this.state = 'submit';
this.close();
});

this.on('cursor', () => {
this.value = !this.value;
});
}
}
96 changes: 96 additions & 0 deletions create-tnf/src/clack/core/src/prompts/group-multiselect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import Prompt, { type PromptOptions } from './prompt.js';

interface GroupMultiSelectOptions<T extends { value: any }>
extends PromptOptions<GroupMultiSelectPrompt<T>> {
options: Record<string, T[]>;
initialValues?: Array<T['value']>;
required?: boolean;
cursorAt?: T['value'];
selectableGroups?: boolean;
}
export default class GroupMultiSelectPrompt<
T extends { value: any },
> extends Prompt {
options: Array<T & { group: string | boolean }>;
cursor: number = 0;
#selectableGroups: boolean;

getGroupItems(group: string): T[] {
return this.options.filter((o) => o.group === group);
}

isGroupSelected(group: string): boolean {
const items = this.getGroupItems(group);
return (
this.#selectableGroups && items.every((i) => this.value.includes(i.value))
);
}

private toggleValue() {
const item = this.options[this.cursor]!;
if (item.group === true) {
const group = item.value;
const groupedItems = this.getGroupItems(group);
if (this.isGroupSelected(group)) {
this.value = this.value.filter(
(v: string) => groupedItems.findIndex((i) => i.value === v) === -1,
);
} else {
this.value = [...this.value, ...groupedItems.map((i) => i.value)];
}
this.value = Array.from(new Set(this.value));
} else {
const selected = this.value.includes(item.value);
this.value = selected
? this.value.filter((v: T['value']) => v !== item.value)
: [...this.value, item.value];
}
}

constructor(opts: GroupMultiSelectOptions<T>) {
super(opts, false);
const { options } = opts;
this.#selectableGroups = opts.selectableGroups ?? true;
this.options = Object.entries(options).flatMap(([key, option]) => [
{ value: key, group: true, label: key },
...option.map((opt) => ({ ...opt, group: key })),
]) as any;
this.value = [...(opts.initialValues ?? [])];
this.cursor = Math.max(
this.options.findIndex(({ value }) => value === opts.cursorAt),
this.#selectableGroups ? 0 : 1,
);

this.on('cursor', (key) => {
switch (key) {
case 'left':
case 'up':
this.cursor =
this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
if (
!this.#selectableGroups &&
this.options[this.cursor]!.group === true
) {
this.cursor =
this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
}
break;
case 'down':
case 'right':
this.cursor =
this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
if (
!this.#selectableGroups &&
this.options[this.cursor]!.group === true
) {
this.cursor =
this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
}
break;
case 'space':
this.toggleValue();
break;
}
});
}
}
65 changes: 65 additions & 0 deletions create-tnf/src/clack/core/src/prompts/multi-select.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Prompt, { type PromptOptions } from './prompt.js';

interface MultiSelectOptions<T extends { value: any }>
extends PromptOptions<MultiSelectPrompt<T>> {
options: T[];
initialValues?: Array<T['value']>;
required?: boolean;
cursorAt?: T['value'];
}
export default class MultiSelectPrompt<
T extends { value: any },
> extends Prompt {
options: T[];
cursor: number = 0;

private get _value() {
return this.options[this.cursor]!.value;
}

private toggleAll() {
const allSelected = this.value.length === this.options.length;
this.value = allSelected ? [] : this.options.map((v) => v.value);
}

private toggleValue() {
const selected = this.value.includes(this._value);
this.value = selected
? this.value.filter((value: T['value']) => value !== this._value)
: [...this.value, this._value];
}

constructor(opts: MultiSelectOptions<T>) {
super(opts, false);

this.options = opts.options;
this.value = [...(opts.initialValues ?? [])];
this.cursor = Math.max(
this.options.findIndex(({ value }) => value === opts.cursorAt),
0,
);
this.on('key', (char) => {
if (char === 'a') {
this.toggleAll();
}
});

this.on('cursor', (key) => {
switch (key) {
case 'left':
case 'up':
this.cursor =
this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
break;
case 'down':
case 'right':
this.cursor =
this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
break;
case 'space':
this.toggleValue();
break;
}
});
}
}
33 changes: 33 additions & 0 deletions create-tnf/src/clack/core/src/prompts/password.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import color from 'picocolors';
import Prompt, { type PromptOptions } from './prompt.js';

interface PasswordOptions extends PromptOptions<PasswordPrompt> {
mask?: string;
}
export default class PasswordPrompt extends Prompt {
valueWithCursor = '';
private _mask = '•';
get cursor(): number {
return this._cursor;
}
get masked(): string {
return this.value.replaceAll(/./g, this._mask);
}
constructor({ mask, ...opts }: PasswordOptions) {
super(opts);
this._mask = mask ?? '•';

this.on('finalize', () => {
this.valueWithCursor = this.masked;
});
this.on('value', () => {
if (this.cursor >= this.value.length) {
this.valueWithCursor = `${this.masked}${color.inverse(color.hidden('_'))}`;
} else {
const s1 = this.masked.slice(0, this.cursor);
const s2 = this.masked.slice(this.cursor);
this.valueWithCursor = `${s1}${color.inverse(s2[0])}${s2.slice(1)}`;
}
});
}
}
Loading

0 comments on commit 4483be0

Please sign in to comment.