Skip to content

Commit e0b49e5

Browse files
committed
update spinner to actually spin
1 parent 96104af commit e0b49e5

File tree

2 files changed

+192
-118
lines changed

2 files changed

+192
-118
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clack/prompts": patch
3+
---
4+
5+
Update spinner so it actually spins

packages/prompts/src/index.ts

Lines changed: 187 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,150 +1,219 @@
11
import { State } from "@clack/core";
22
import { TextPrompt, SelectPrompt, ConfirmPrompt, block } from "@clack/core";
3-
import color from 'picocolors';
4-
import { cursor, erase } from 'sisteransi';
3+
import color from "picocolors";
4+
import { cursor, erase } from "sisteransi";
55

66
export { isCancel } from "@clack/core";
77

88
const symbol = (state: State) => {
9-
switch (state) {
10-
case 'initial':
11-
case 'active': return color.cyan('●')
12-
case 'cancel': return color.red('■')
13-
case 'error': return color.yellow('▲')
14-
case 'submit': return color.green('✔')
15-
}
16-
}
9+
switch (state) {
10+
case "initial":
11+
case "active":
12+
return color.cyan("●");
13+
case "cancel":
14+
return color.red("■");
15+
case "error":
16+
return color.yellow("▲");
17+
case "submit":
18+
return color.green("✔");
19+
}
20+
};
1721

18-
const barStart = '┌';
19-
const bar = '│';
20-
const barEnd = '└';
22+
const barStart = "┌";
23+
const bar = "│";
24+
const barEnd = "└";
2125

2226
export interface TextOptions {
23-
message: string;
24-
placeholder?: string;
25-
validate?: ((value: string) => string | void);
27+
message: string;
28+
placeholder?: string;
29+
validate?: (value: string) => string | void;
2630
}
2731
export const text = (opts: TextOptions) => {
28-
return new TextPrompt({
29-
validate: opts.validate,
30-
render() {
31-
const title = `${color.gray(bar)}\n${symbol(this.state)} ${opts.message}\n`;
32-
const placeholder = opts.placeholder ? color.inverse(opts.placeholder[0]) + color.dim(opts.placeholder.slice(1)) : color.inverse(color.hidden('_'));
33-
const value = !this.value ? placeholder : this.valueWithCursor;
32+
return new TextPrompt({
33+
validate: opts.validate,
34+
render() {
35+
const title = `${color.gray(bar)}\n${symbol(this.state)} ${
36+
opts.message
37+
}\n`;
38+
const placeholder = opts.placeholder
39+
? color.inverse(opts.placeholder[0]) +
40+
color.dim(opts.placeholder.slice(1))
41+
: color.inverse(color.hidden("_"));
42+
const value = !this.value ? placeholder : this.valueWithCursor;
3443

35-
switch (this.state) {
36-
case 'error': return `${title.trim()}\n${color.yellow(bar)} ${value}\n${color.yellow(barEnd)} ${color.yellow(this.error)}\n`;
37-
case 'submit': return `${title}${color.gray(bar)} ${color.dim(this.value)}`;
38-
case 'cancel': return `${title}${color.gray(bar)} ${color.strikethrough(color.dim(this.value))}${this.value.trim() ? '\n' + color.gray(bar) : ''}`;
39-
default: return `${title}${color.cyan(bar)} ${value}\n${color.cyan(barEnd)}\n`;
40-
}
41-
}
42-
}).prompt();
43-
}
44+
switch (this.state) {
45+
case "error":
46+
return `${title.trim()}\n${color.yellow(
47+
bar
48+
)} ${value}\n${color.yellow(barEnd)} ${color.yellow(this.error)}\n`;
49+
case "submit":
50+
return `${title}${color.gray(bar)} ${color.dim(this.value)}`;
51+
case "cancel":
52+
return `${title}${color.gray(bar)} ${color.strikethrough(
53+
color.dim(this.value)
54+
)}${this.value.trim() ? "\n" + color.gray(bar) : ""}`;
55+
default:
56+
return `${title}${color.cyan(bar)} ${value}\n${color.cyan(
57+
barEnd
58+
)}\n`;
59+
}
60+
},
61+
}).prompt();
62+
};
4463

4564
export interface ConfirmOptions {
46-
message: string;
47-
active?: string;
48-
inactive?: string;
49-
initialValue?: boolean;
65+
message: string;
66+
active?: string;
67+
inactive?: string;
68+
initialValue?: boolean;
5069
}
5170
export const confirm = (opts: ConfirmOptions) => {
52-
const active = opts.active ?? 'Yes';
53-
const inactive = opts.inactive ?? 'No';
54-
return new ConfirmPrompt({
55-
active,
56-
inactive,
57-
initialValue: opts.initialValue ?? true,
58-
render() {
59-
const title = `${color.gray(bar)}\n${symbol(this.state)} ${opts.message}\n`;
60-
const value = this.value ? active : inactive;
71+
const active = opts.active ?? "Yes";
72+
const inactive = opts.inactive ?? "No";
73+
return new ConfirmPrompt({
74+
active,
75+
inactive,
76+
initialValue: opts.initialValue ?? true,
77+
render() {
78+
const title = `${color.gray(bar)}\n${symbol(this.state)} ${
79+
opts.message
80+
}\n`;
81+
const value = this.value ? active : inactive;
6182

62-
switch (this.state) {
63-
case 'submit': return `${title}${color.gray(bar)} ${color.dim(value)}`;
64-
case 'cancel': return `${title}${color.gray(bar)} ${color.strikethrough(color.dim(value))}\n${color.gray(bar)}`;
65-
default: {
66-
return `${title}${color.cyan(bar)} ${this.value ? `${color.green('◼')} ${active}` : `${color.dim('◻')} ${color.dim(active)}`} ${color.dim('/')} ${!this.value ? `${color.green('◼')} ${inactive}` : `${color.dim('◻')} ${color.dim(inactive)}`}\n${color.cyan(barEnd)}\n`;
67-
}
68-
}
83+
switch (this.state) {
84+
case "submit":
85+
return `${title}${color.gray(bar)} ${color.dim(value)}`;
86+
case "cancel":
87+
return `${title}${color.gray(bar)} ${color.strikethrough(
88+
color.dim(value)
89+
)}\n${color.gray(bar)}`;
90+
default: {
91+
return `${title}${color.cyan(bar)} ${
92+
this.value
93+
? `${color.green("◼")} ${active}`
94+
: `${color.dim("◻")} ${color.dim(active)}`
95+
} ${color.dim("/")} ${
96+
!this.value
97+
? `${color.green("◼")} ${inactive}`
98+
: `${color.dim("◻")} ${color.dim(inactive)}`
99+
}\n${color.cyan(barEnd)}\n`;
69100
}
70-
}).prompt();
71-
}
101+
}
102+
},
103+
}).prompt();
104+
};
72105

73106
interface Option {
74-
value: any;
75-
label?: string;
76-
hint?: string;
107+
value: any;
108+
label?: string;
109+
hint?: string;
77110
}
78111
export interface SelectOptions<Options extends Option[]> {
79-
message: string;
80-
options: Options;
81-
initialValue?: Options[number]['value'];
112+
message: string;
113+
options: Options;
114+
initialValue?: Options[number]["value"];
82115
}
83-
export const select = <Options extends Option[]>(opts: SelectOptions<Options>) => {
84-
const opt = (option: Options[number], state: 'inactive' | 'active' | 'selected' | 'cancelled') => {
85-
const label = option.label ?? option.value;
86-
if (state === 'active') {
87-
return `${color.green('◼')} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ''}`
88-
} else if (state === 'selected') {
89-
return `${color.dim(label)}`
90-
} else if (state === 'cancelled') {
91-
return `${color.strikethrough(color.dim(label))}`
92-
}
93-
return `${color.dim('◻')} ${color.dim(label)}`;
116+
export const select = <Options extends Option[]>(
117+
opts: SelectOptions<Options>
118+
) => {
119+
const opt = (
120+
option: Options[number],
121+
state: "inactive" | "active" | "selected" | "cancelled"
122+
) => {
123+
const label = option.label ?? option.value;
124+
if (state === "active") {
125+
return `${color.green("◼")} ${label} ${
126+
option.hint ? color.dim(`(${option.hint})`) : ""
127+
}`;
128+
} else if (state === "selected") {
129+
return `${color.dim(label)}`;
130+
} else if (state === "cancelled") {
131+
return `${color.strikethrough(color.dim(label))}`;
94132
}
133+
return `${color.dim("◻")} ${color.dim(label)}`;
134+
};
95135

96-
return new SelectPrompt({
97-
options: opts.options,
98-
initialValue: opts.initialValue,
99-
render() {
100-
const title = `${color.gray(bar)}\n${symbol(this.state)} ${opts.message}\n`;
136+
return new SelectPrompt({
137+
options: opts.options,
138+
initialValue: opts.initialValue,
139+
render() {
140+
const title = `${color.gray(bar)}\n${symbol(this.state)} ${
141+
opts.message
142+
}\n`;
101143

102-
switch (this.state) {
103-
case 'submit': return `${title}${color.gray(bar)} ${opt(this.options[this.cursor], 'selected')}`;
104-
case 'cancel': return `${title}${color.gray(bar)} ${opt(this.options[this.cursor], 'cancelled')}\n${color.gray(bar)}`;
105-
default: {
106-
return `${title}${color.cyan(bar)} ${this.options.map((option, i) => opt(option, i === this.cursor ? 'active' : 'inactive')).join(`\n${color.cyan(bar)} `)}\n${color.cyan(barEnd)}\n`;
107-
}
108-
}
144+
switch (this.state) {
145+
case "submit":
146+
return `${title}${color.gray(bar)} ${opt(
147+
this.options[this.cursor],
148+
"selected"
149+
)}`;
150+
case "cancel":
151+
return `${title}${color.gray(bar)} ${opt(
152+
this.options[this.cursor],
153+
"cancelled"
154+
)}\n${color.gray(bar)}`;
155+
default: {
156+
return `${title}${color.cyan(bar)} ${this.options
157+
.map((option, i) =>
158+
opt(option, i === this.cursor ? "active" : "inactive")
159+
)
160+
.join(`\n${color.cyan(bar)} `)}\n${color.cyan(barEnd)}\n`;
109161
}
110-
}).prompt();
111-
}
162+
}
163+
},
164+
}).prompt();
165+
};
112166

113-
export const cancel = (message = '') => {
114-
process.stdout.write(`${color.gray(barEnd)} ${color.red(message)}\n\n`);
115-
}
167+
export const cancel = (message = "") => {
168+
process.stdout.write(`${color.gray(barEnd)} ${color.red(message)}\n\n`);
169+
};
116170

117-
export const intro = (title = '') => {
118-
process.stdout.write(`${color.gray(barStart)} ${title}\n`);
119-
}
171+
export const intro = (title = "") => {
172+
process.stdout.write(`${color.gray(barStart)} ${title}\n`);
173+
};
120174

121-
export const outro = (message = '') => {
122-
process.stdout.write(`${color.gray(bar)}\n${color.gray(barEnd)} ${color.green(message)}\n\n`);
123-
}
175+
export const outro = (message = "") => {
176+
process.stdout.write(
177+
`${color.gray(bar)}\n${color.gray(barEnd)} ${color.green(message)}\n\n`
178+
);
179+
};
124180

125-
export const spinner = () => {
126-
let unblock: () => void;
127-
let loop: NodeJS.Timer;
128-
return {
129-
start(message = '') {
130-
message = message.replace(/\.\.\.$/, '');
131-
unblock = block();
132-
process.stdout.write(`${color.gray(bar)}\n${color.magenta('◆')} ${message}\n`);
133-
let i = 0;
134-
loop = setInterval(() => {
135-
process.stdout.write(cursor.move(-999, -2));
136-
process.stdout.write(erase.down(2))
137-
process.stdout.write(`${color.gray(bar)}\n${color.magenta('◆')} ${message}${i ? '.'.repeat(i) : ''}\n`);
138-
i = i > 2 ? 0 : i + 1;
139-
}, 300)
140-
},
141-
stop(message = '') {
142-
process.stdout.write(cursor.move(-999, -2));
143-
process.stdout.write(erase.down(2))
144-
clearInterval(loop);
145-
process.stdout.write(`${color.gray(bar)}\n${color.gray('◆')} ${message}\n`);
146-
unblock();
147-
}
148-
}
181+
const arc = [
182+
'◒', '◒', '◐', '◐', '◓', '◓', '◑', '◑'
183+
]
149184

150-
}
185+
export const spinner = () => {
186+
let unblock: () => void;
187+
let loop: NodeJS.Timer;
188+
const frames = arc;
189+
const delay = 120;
190+
return {
191+
start(message = "") {
192+
message = message.replace(/\.?\.?\.$/, "");
193+
unblock = block();
194+
process.stdout.write(
195+
`${color.gray(bar)}\n${color.magenta("○")} ${message}\n`
196+
);
197+
let i = 0;
198+
let dot = 0;
199+
loop = setInterval(() => {
200+
let frame = frames[i];
201+
dot = i % 2 === 0 ? i / 2 : dot;
202+
process.stdout.write(cursor.move(-999, -1));
203+
process.stdout.write(
204+
`${color.magenta(frame)} ${message}${dot > 0 ? '.'.repeat(dot) : ''} \n`
205+
);
206+
i = i > frames.length - 2 ? 0 : i + 1;
207+
}, delay);
208+
},
209+
stop(message = "") {
210+
process.stdout.write(cursor.move(-999, -2));
211+
process.stdout.write(erase.down(2));
212+
clearInterval(loop);
213+
process.stdout.write(
214+
`${color.gray(bar)}\n${color.gray("○")} ${message}\n`
215+
);
216+
unblock();
217+
},
218+
};
219+
};

0 commit comments

Comments
 (0)