[READ-ONLY] Mirror of https://github.com/bombshell-dev/clack. Effortlessly build beautiful command-line apps bomb.sh/docs/clack/basics/getting-started/
cli command-line command-line-app node prompt prompts
5

Configure Feed

Select the types of activity you want to include in your feed.

feat: add ascii fallbacks (#24)

authored by

Nate Moore and committed by
GitHub
(Feb 16, 2023, 8:45 AM -0600) d89e67ac e1053245

+93 -60
+5
.changeset/sharp-badgers-hug.md
··· 1 + --- 2 + "@clack/prompts": minor 3 + --- 4 + 5 + Improved Windows/non-unicode support
+6 -2
packages/prompts/package.json
··· 57 57 "sisteransi": "^1.0.5" 58 58 }, 59 59 "devDependencies": { 60 - "unbuild": "^1.1.1" 61 - } 60 + "unbuild": "^1.1.1", 61 + "is-unicode-supported": "^1.3.0" 62 + }, 63 + "bundledDependencies": [ 64 + "is-unicode-supported" 65 + ] 62 66 }
+75 -58
packages/prompts/src/index.ts
··· 2 2 import { MultiSelectPrompt, TextPrompt, SelectPrompt, ConfirmPrompt, block } from "@clack/core"; 3 3 import color from "picocolors"; 4 4 import { cursor, erase } from "sisteransi"; 5 + import isUnicodeSupported from "is-unicode-supported"; 6 + 5 7 export { isCancel } from "@clack/core"; 6 8 9 + const unicode = isUnicodeSupported(); 10 + const s = (c: string, fallback: string) => unicode ? c : fallback; 11 + const S_STEP_ACTIVE = s("◆", "*"); 12 + const S_STEP_CANCEL = s("■", "x"); 13 + const S_STEP_ERROR = s("▲", "x"); 14 + const S_STEP_SUBMIT = s("◇", "o"); 15 + 16 + const S_BAR_START = s("┌", "T"); 17 + const S_BAR = s("│", "|"); 18 + const S_BAR_END = s("└", "—"); 19 + 20 + const S_RADIO_ACTIVE = s("●", ">"); 21 + const S_RADIO_INACTIVE = s("○", " "); 22 + const S_CHECKBOX_ACTIVE = s("◻", "[•]"); 23 + const S_CHECKBOX_SELECTED = s("◼", "[+]"); 24 + const S_CHECKBOX_INACTIVE = s("◻", "[ ]"); 25 + 26 + const S_BAR_H = s('─', '-'); 27 + const S_CORNER_TOP_RIGHT = s('╮', '+'); 28 + const S_CONNECT_LEFT = s('├', '+'); 29 + const S_CORNER_BOTTOM_RIGHT = s('╯', '+'); 30 + 7 31 const symbol = (state: State) => { 8 32 switch (state) { 9 33 case "initial": 10 34 case "active": 11 - return color.cyan("●"); 35 + return color.cyan(S_STEP_ACTIVE); 12 36 case "cancel": 13 - return color.red("■"); 37 + return color.red(S_STEP_CANCEL); 14 38 case "error": 15 - return color.yellow("▲"); 39 + return color.yellow(S_STEP_ERROR); 16 40 case "submit": 17 - return color.green("○"); 41 + return color.green(S_STEP_SUBMIT); 18 42 } 19 43 }; 20 44 21 - const barStart = "┌"; 22 - const bar = "│"; 23 - const barEnd = "└"; 24 - 25 45 export interface TextOptions { 26 46 message: string; 27 47 placeholder?: string; ··· 34 54 placeholder: opts.placeholder, 35 55 initialValue: opts.initialValue, 36 56 render() { 37 - const title = `${color.gray(bar)}\n${symbol(this.state)} ${ 57 + const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${ 38 58 opts.message 39 59 }\n`; 40 60 const placeholder = opts.placeholder ··· 46 66 switch (this.state) { 47 67 case "error": 48 68 return `${title.trim()}\n${color.yellow( 49 - bar 50 - )} ${value}\n${color.yellow(barEnd)} ${color.yellow(this.error)}\n`; 69 + S_BAR 70 + )} ${value}\n${color.yellow(S_BAR_END)} ${color.yellow(this.error)}\n`; 51 71 case "submit": 52 - return `${title}${color.gray(bar)} ${color.dim(this.value)}`; 72 + return `${title}${color.gray(S_BAR)} ${color.dim(this.value)}`; 53 73 case "cancel": 54 - return `${title}${color.gray(bar)} ${color.strikethrough( 74 + return `${title}${color.gray(S_BAR)} ${color.strikethrough( 55 75 color.dim(this.value) 56 - )}${this.value.trim() ? "\n" + color.gray(bar) : ""}`; 76 + )}${this.value.trim() ? "\n" + color.gray(S_BAR) : ""}`; 57 77 default: 58 - return `${title}${color.cyan(bar)} ${value}\n${color.cyan( 59 - barEnd 78 + return `${title}${color.cyan(S_BAR)} ${value}\n${color.cyan( 79 + S_BAR_END 60 80 )}\n`; 61 81 } 62 82 }, ··· 77 97 inactive, 78 98 initialValue: opts.initialValue ?? true, 79 99 render() { 80 - const title = `${color.gray(bar)}\n${symbol(this.state)} ${ 100 + const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${ 81 101 opts.message 82 102 }\n`; 83 103 const value = this.value ? active : inactive; 84 104 85 105 switch (this.state) { 86 106 case "submit": 87 - return `${title}${color.gray(bar)} ${color.dim(value)}`; 107 + return `${title}${color.gray(S_BAR)} ${color.dim(value)}`; 88 108 case "cancel": 89 - return `${title}${color.gray(bar)} ${color.strikethrough( 109 + return `${title}${color.gray(S_BAR)} ${color.strikethrough( 90 110 color.dim(value) 91 - )}\n${color.gray(bar)}`; 111 + )}\n${color.gray(S_BAR)}`; 92 112 default: { 93 - return `${title}${color.cyan(bar)} ${ 113 + return `${title}${color.cyan(S_BAR)} ${ 94 114 this.value 95 - ? `${color.green("●")} ${active}` 96 - : `${color.dim("○")} ${color.dim(active)}` 115 + ? `${color.green(S_RADIO_ACTIVE)} ${active}` 116 + : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(active)}` 97 117 } ${color.dim("/")} ${ 98 118 !this.value 99 - ? `${color.green("●")} ${inactive}` 100 - : `${color.dim("○")} ${color.dim(inactive)}` 101 - }\n${color.cyan(barEnd)}\n`; 119 + ? `${color.green(S_RADIO_ACTIVE)} ${inactive}` 120 + : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(inactive)}` 121 + }\n${color.cyan(S_BAR_END)}\n`; 102 122 } 103 123 } 104 124 }, ··· 133 153 ) => { 134 154 const label = option.label ?? String(option.value); 135 155 if (state === "active") { 136 - return `${color.green("●")} ${label} ${ 156 + return `${color.green(S_RADIO_ACTIVE)} ${label} ${ 137 157 option.hint ? color.dim(`(${option.hint})`) : "" 138 158 }`; 139 159 } else if (state === "selected") { ··· 141 161 } else if (state === "cancelled") { 142 162 return `${color.strikethrough(color.dim(label))}`; 143 163 } 144 - return `${color.dim("○")} ${color.dim(label)}`; 164 + return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`; 145 165 }; 146 166 147 167 return new SelectPrompt({ 148 168 options: opts.options, 149 169 initialValue: opts.initialValue, 150 170 render() { 151 - const title = `${color.gray(bar)}\n${symbol(this.state)} ${ 171 + const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${ 152 172 opts.message 153 173 }\n`; 154 174 155 175 switch (this.state) { 156 176 case "submit": 157 - return `${title}${color.gray(bar)} ${opt( 177 + return `${title}${color.gray(S_BAR)} ${opt( 158 178 this.options[this.cursor], 159 179 "selected" 160 180 )}`; 161 181 case "cancel": 162 - return `${title}${color.gray(bar)} ${opt( 182 + return `${title}${color.gray(S_BAR)} ${opt( 163 183 this.options[this.cursor], 164 184 "cancelled" 165 - )}\n${color.gray(bar)}`; 185 + )}\n${color.gray(S_BAR)}`; 166 186 default: { 167 - return `${title}${color.cyan(bar)} ${this.options 187 + return `${title}${color.cyan(S_BAR)} ${this.options 168 188 .map((option, i) => 169 189 opt(option, i === this.cursor ? "active" : "inactive") 170 190 ) 171 - .join(`\n${color.cyan(bar)} `)}\n${color.cyan(barEnd)}\n`; 191 + .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; 172 192 } 173 193 } 174 194 }, ··· 179 199 const opt = (option: Options[number], state: 'inactive' | 'active' | 'selected' | 'active-selected' | 'submitted' | 'cancelled') => { 180 200 const label = option.label ?? String(option.value); 181 201 if (state === 'active') { 182 - return `${color.cyan('◻')} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ''}` 202 + return `${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ''}` 183 203 } else if (state === 'selected') { 184 - return `${color.green('◼')} ${color.dim(label)}` 204 + return `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}` 185 205 } else if (state === 'cancelled') { 186 206 return `${color.strikethrough(color.dim(label))}`; 187 207 } else if (state === 'active-selected') { 188 - return `${color.green('◼')} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ''}` 208 + return `${color.green(S_CHECKBOX_SELECTED)} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ''}` 189 209 } else if (state === 'submitted') { 190 210 return `${color.dim(label)}`; 191 211 } 192 - return `${color.dim('◻')} ${color.dim(label)}`; 212 + return `${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`; 193 213 } 194 214 195 215 return new MultiSelectPrompt({ ··· 197 217 initialValue: opts.initialValue, 198 218 cursorAt: opts.cursorAt, 199 219 render() { 200 - let title = `${color.gray(bar)}\n${symbol(this.state)} ${opts.message}\n`; 220 + let title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; 201 221 202 222 switch (this.state) { 203 223 case 'submit': { 204 224 const selectedOptions = this.options.filter(option => this.selectedValues.some(selectedValue => selectedValue === option.value as any)); 205 - return `${title}${color.gray(bar)} ${selectedOptions.map((option, i) => opt(option, 'submitted')).join(color.dim(", "))}`; 225 + return `${title}${color.gray(S_BAR)} ${selectedOptions.map((option, i) => opt(option, 'submitted')).join(color.dim(", "))}`; 206 226 }; 207 227 case 'cancel': { 208 228 const selectedOptions = this.options.filter(option => this.selectedValues.some(selectedValue => selectedValue === option.value as any)); 209 229 const label = selectedOptions.map((option, i) => opt(option, 'cancelled')).join(color.dim(", ")); 210 - return `${title}${color.gray(bar)} ${label.trim() ? `${label}\n${color.gray(bar)}` : ''}` 230 + return `${title}${color.gray(S_BAR)} ${label.trim() ? `${label}\n${color.gray(S_BAR)}` : ''}` 211 231 }; 212 232 case 'error': { 213 - const footer = this.error.split('\n').map((ln, i) => i === 0 ? `${color.yellow(barEnd)} ${color.yellow(ln)}` : ` ${ln}`).join('\n'); 214 - return `${title}${color.yellow(bar)} ${this.options.map((option, i) => { 233 + const footer = this.error.split('\n').map((ln, i) => i === 0 ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` : ` ${ln}`).join('\n'); 234 + return `${title}${color.yellow(S_BAR)} ${this.options.map((option, i) => { 215 235 const isOptionSelected = this.selectedValues.includes(option.value as any); 216 236 const isOptionHovered = i === this.cursor; 217 237 if(isOptionHovered && isOptionSelected) { ··· 221 241 return opt(option, 'selected'); 222 242 } 223 243 return opt(option, isOptionHovered ? 'active' : 'inactive'); 224 - }).join(`\n${color.yellow(bar)} `)}\n${footer}\n`; 244 + }).join(`\n${color.yellow(S_BAR)} `)}\n${footer}\n`; 225 245 } 226 246 default: { 227 - return `${title}${color.cyan(bar)} ${this.options.map((option, i) => { 247 + return `${title}${color.cyan(S_BAR)} ${this.options.map((option, i) => { 228 248 const isOptionSelected = this.selectedValues.includes(option.value as any); 229 249 const isOptionHovered = i === this.cursor; 230 250 if(isOptionHovered && isOptionSelected) { ··· 234 254 return opt(option, 'selected'); 235 255 } 236 256 return opt(option, isOptionHovered ? 'active' : 'inactive'); 237 - }).join(`\n${color.cyan(bar)} `)}\n${color.cyan(barEnd)}\n`; 257 + }).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; 238 258 } 239 259 } 240 260 } ··· 248 268 ln = strip(ln); 249 269 return ln.length > sum ? ln.length : sum 250 270 }, 0) + 2; 251 - const msg = lines.map((ln) => `${color.gray(bar)} ${color.dim(ln)}${' '.repeat(len - strip(ln).length)}${color.gray(bar)}`).join('\n'); 252 - process.stdout.write(`${color.gray(bar)}\n${color.green('○')} ${color.reset(title)} ${color.gray('─'.repeat(len - title.length - 1) + '╮')}\n${msg}\n${color.gray('├' + '─'.repeat(len + 2) + '╯')}\n`); 271 + const msg = lines.map((ln) => `${color.gray(S_BAR)} ${color.dim(ln)}${' '.repeat(len - strip(ln).length)}${color.gray(S_BAR)}`).join('\n'); 272 + process.stdout.write(`${color.gray(S_BAR)}\n${color.green(S_STEP_SUBMIT)} ${color.reset(title)} ${color.gray(S_BAR_H.repeat(len - title.length - 1) + S_CORNER_TOP_RIGHT)}\n${msg}\n${color.gray(S_CONNECT_LEFT + S_BAR_H.repeat(len + 2) + S_CORNER_BOTTOM_RIGHT)}\n`); 253 273 }; 254 274 255 275 export const cancel = (message = "") => { 256 - process.stdout.write(`${color.gray(barEnd)} ${color.red(message)}\n\n`); 276 + process.stdout.write(`${color.gray(S_BAR_END)} ${color.red(message)}\n\n`); 257 277 }; 258 278 259 279 export const intro = (title = "") => { 260 - process.stdout.write(`${color.gray(barStart)} ${title}\n`); 280 + process.stdout.write(`${color.gray(S_BAR_START)} ${title}\n`); 261 281 }; 262 282 263 283 export const outro = (message = "") => { 264 284 process.stdout.write( 265 - `${color.gray(bar)}\n${color.gray(barEnd)} ${message}\n\n` 285 + `${color.gray(S_BAR)}\n${color.gray(S_BAR_END)} ${message}\n\n` 266 286 ); 267 287 }; 268 288 269 - const arc = [ 270 - '◒', '◐', '◓', '◑' 271 - ] 289 + const frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•','o','O','0'] 272 290 273 291 export const spinner = () => { 274 292 let unblock: () => void; 275 293 let loop: NodeJS.Timer; 276 - const frames = arc; 277 - const delay = 80; 294 + const delay = unicode ? 80 : 120; 278 295 return { 279 296 start(message = "") { 280 297 message = message.replace(/\.?\.?\.$/, ""); 281 298 unblock = block(); 282 299 process.stdout.write( 283 - `${color.gray(bar)}\n${color.magenta("○")} ${message}\n` 300 + `${color.gray(S_BAR)}\n${color.magenta("○")} ${message}\n` 284 301 ); 285 302 let i = 0; 286 303 let dot = 0; ··· 299 316 process.stdout.write(erase.down(2)); 300 317 clearInterval(loop); 301 318 process.stdout.write( 302 - `${color.gray(bar)}\n${color.green("○")} ${message}\n` 319 + `${color.gray(S_BAR)}\n${color.green(S_STEP_SUBMIT)} ${message}\n` 303 320 ); 304 321 unblock(); 305 322 },
+7
pnpm-lock.yaml
··· 43 43 packages/prompts: 44 44 specifiers: 45 45 '@clack/core': workspace:^0.1.3 46 + is-unicode-supported: ^1.3.0 46 47 picocolors: ^1.0.0 47 48 sisteransi: ^1.0.5 48 49 unbuild: ^1.1.1 ··· 51 52 picocolors: 1.0.0 52 53 sisteransi: 1.0.5 53 54 devDependencies: 55 + is-unicode-supported: 1.3.0 54 56 unbuild: 1.1.1 55 57 56 58 packages: ··· 1990 1992 for-each: 0.3.3 1991 1993 gopd: 1.0.1 1992 1994 has-tostringtag: 1.0.0 1995 + dev: true 1996 + 1997 + /is-unicode-supported/1.3.0: 1998 + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} 1999 + engines: {node: '>=12'} 1993 2000 dev: true 1994 2001 1995 2002 /is-weakref/1.0.2: