[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.

refactor: extract unicode/fallback to top-level

Nate Moore (Feb 16, 2023, 8:37 AM -0600) ce87e0f4 31a4c49e

+70 -57
+70 -57
packages/prompts/src/index.ts
··· 7 7 export { isCancel } from "@clack/core"; 8 8 9 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('╯', '+'); 10 30 11 31 const symbol = (state: State) => { 12 32 switch (state) { 13 33 case "initial": 14 34 case "active": 15 - return color.cyan(unicode ? "●" : "$"); 35 + return color.cyan(S_STEP_ACTIVE); 16 36 case "cancel": 17 - return color.red(unicode ? "■" : "x"); 37 + return color.red(S_STEP_CANCEL); 18 38 case "error": 19 - return color.yellow(unicode ? "▲" : "x"); 39 + return color.yellow(S_STEP_ERROR); 20 40 case "submit": 21 - return color.green(unicode ? "○" : "$"); 41 + return color.green(S_STEP_SUBMIT); 22 42 } 23 43 }; 24 - 25 - const barStart = unicode ? "┌" : "|"; 26 - const bar = unicode ? "│" : "|"; 27 - const barEnd = unicode ? "└" : "|"; 28 44 29 45 export interface TextOptions { 30 46 message: string; ··· 38 54 placeholder: opts.placeholder, 39 55 initialValue: opts.initialValue, 40 56 render() { 41 - const title = `${color.gray(bar)}\n${symbol(this.state)} ${ 57 + const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${ 42 58 opts.message 43 59 }\n`; 44 60 const placeholder = opts.placeholder ··· 50 66 switch (this.state) { 51 67 case "error": 52 68 return `${title.trim()}\n${color.yellow( 53 - bar 54 - )} ${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`; 55 71 case "submit": 56 - return `${title}${color.gray(bar)} ${color.dim(this.value)}`; 72 + return `${title}${color.gray(S_BAR)} ${color.dim(this.value)}`; 57 73 case "cancel": 58 - return `${title}${color.gray(bar)} ${color.strikethrough( 74 + return `${title}${color.gray(S_BAR)} ${color.strikethrough( 59 75 color.dim(this.value) 60 - )}${this.value.trim() ? "\n" + color.gray(bar) : ""}`; 76 + )}${this.value.trim() ? "\n" + color.gray(S_BAR) : ""}`; 61 77 default: 62 - return `${title}${color.cyan(bar)} ${value}\n${color.cyan( 63 - barEnd 78 + return `${title}${color.cyan(S_BAR)} ${value}\n${color.cyan( 79 + S_BAR_END 64 80 )}\n`; 65 81 } 66 82 }, ··· 81 97 inactive, 82 98 initialValue: opts.initialValue ?? true, 83 99 render() { 84 - const title = `${color.gray(bar)}\n${symbol(this.state)} ${ 100 + const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${ 85 101 opts.message 86 102 }\n`; 87 103 const value = this.value ? active : inactive; 88 104 89 105 switch (this.state) { 90 106 case "submit": 91 - return `${title}${color.gray(bar)} ${color.dim(value)}`; 107 + return `${title}${color.gray(S_BAR)} ${color.dim(value)}`; 92 108 case "cancel": 93 - return `${title}${color.gray(bar)} ${color.strikethrough( 109 + return `${title}${color.gray(S_BAR)} ${color.strikethrough( 94 110 color.dim(value) 95 - )}\n${color.gray(bar)}`; 111 + )}\n${color.gray(S_BAR)}`; 96 112 default: { 97 - return `${title}${color.cyan(bar)} ${ 113 + return `${title}${color.cyan(S_BAR)} ${ 98 114 this.value 99 - ? `${color.green(unicode ? "●" : ">")} ${active}` 100 - : `${color.dim(unicode ? "○" : " ")} ${color.dim(active)}` 115 + ? `${color.green(S_RADIO_ACTIVE)} ${active}` 116 + : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(active)}` 101 117 } ${color.dim("/")} ${ 102 118 !this.value 103 - ? `${color.green(unicode ? "●" : ">")} ${inactive}` 104 - : `${color.dim(unicode ? "○" : " ")} ${color.dim(inactive)}` 105 - }\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`; 106 122 } 107 123 } 108 124 }, ··· 128 144 ) => { 129 145 const label = option.label ?? option.value; 130 146 if (state === "active") { 131 - return `${color.green(unicode ? "●" : '>')} ${label} ${ 147 + return `${color.green(S_RADIO_ACTIVE)} ${label} ${ 132 148 option.hint ? color.dim(`(${option.hint})`) : "" 133 149 }`; 134 150 } else if (state === "selected") { ··· 136 152 } else if (state === "cancelled") { 137 153 return `${color.strikethrough(color.dim(label))}`; 138 154 } 139 - return `${color.dim(unicode ? "○" : '-')} ${color.dim(label)}`; 155 + return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`; 140 156 }; 141 157 142 158 return new SelectPrompt({ 143 159 options: opts.options, 144 160 initialValue: opts.initialValue, 145 161 render() { 146 - const title = `${color.gray(bar)}\n${symbol(this.state)} ${ 162 + const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${ 147 163 opts.message 148 164 }\n`; 149 165 150 166 switch (this.state) { 151 167 case "submit": 152 - return `${title}${color.gray(bar)} ${opt( 168 + return `${title}${color.gray(S_BAR)} ${opt( 153 169 this.options[this.cursor], 154 170 "selected" 155 171 )}`; 156 172 case "cancel": 157 - return `${title}${color.gray(bar)} ${opt( 173 + return `${title}${color.gray(S_BAR)} ${opt( 158 174 this.options[this.cursor], 159 175 "cancelled" 160 - )}\n${color.gray(bar)}`; 176 + )}\n${color.gray(S_BAR)}`; 161 177 default: { 162 - return `${title}${color.cyan(bar)} ${this.options 178 + return `${title}${color.cyan(S_BAR)} ${this.options 163 179 .map((option, i) => 164 180 opt(option, i === this.cursor ? "active" : "inactive") 165 181 ) 166 - .join(`\n${color.cyan(bar)} `)}\n${color.cyan(barEnd)}\n`; 182 + .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; 167 183 } 168 184 } 169 185 }, ··· 174 190 const opt = (option: Options[number], state: 'inactive' | 'active' | 'selected' | 'active-selected' | 'submitted' | 'cancelled') => { 175 191 const label = option.label ?? option.value; 176 192 if (state === 'active') { 177 - return `${color.cyan(unicode ? '◻' : ">")} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ''}` 193 + return `${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ''}` 178 194 } else if (state === 'selected') { 179 - return `${color.green(unicode ? '◼' : "+")} ${color.dim(label)}` 195 + return `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}` 180 196 } else if (state === 'cancelled') { 181 197 return `${color.strikethrough(color.dim(label))}`; 182 198 } else if (state === 'active-selected') { 183 - return `${color.green(unicode ? '◼' : "+")} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ''}` 199 + return `${color.green(S_CHECKBOX_SELECTED)} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ''}` 184 200 } else if (state === 'submitted') { 185 201 return `${color.dim(label)}`; 186 202 } 187 - return `${color.dim(unicode ? '◻' : "-")} ${color.dim(label)}`; 203 + return `${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`; 188 204 } 189 205 190 206 return new MultiSelectPrompt({ 191 207 options: opts.options, 192 208 initialValue: opts.initialValue, 193 209 render() { 194 - let title = `${color.gray(bar)}\n${symbol(this.state)} ${opts.message}\n`; 210 + let title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; 195 211 196 212 switch (this.state) { 197 213 case 'submit': { 198 214 const selectedOptions = this.options.filter(option => this.selectedValues.some(selectedValue => selectedValue === option.value as any)); 199 - return `${title}${color.gray(bar)} ${selectedOptions.map((option, i) => opt(option, 'submitted')).join(color.dim(", "))}`; 215 + return `${title}${color.gray(S_BAR)} ${selectedOptions.map((option, i) => opt(option, 'submitted')).join(color.dim(", "))}`; 200 216 }; 201 217 case 'cancel': { 202 218 const selectedOptions = this.options.filter(option => this.selectedValues.some(selectedValue => selectedValue === option.value as any)); 203 219 const label = selectedOptions.map((option, i) => opt(option, 'cancelled')).join(color.dim(", ")); 204 - return `${title}${color.gray(bar)} ${label.trim() ? `${label}\n${color.gray(bar)}` : ''}` 220 + return `${title}${color.gray(S_BAR)} ${label.trim() ? `${label}\n${color.gray(S_BAR)}` : ''}` 205 221 }; 206 222 case 'error': { 207 - const footer = this.error.split('\n').map((ln, i) => i === 0 ? `${color.yellow(barEnd)} ${color.yellow(ln)}` : ` ${ln}`).join('\n'); 208 - return `${title}${color.yellow(bar)} ${this.options.map((option, i) => { 223 + const footer = this.error.split('\n').map((ln, i) => i === 0 ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` : ` ${ln}`).join('\n'); 224 + return `${title}${color.yellow(S_BAR)} ${this.options.map((option, i) => { 209 225 const isOptionSelected = this.selectedValues.includes(option.value as any); 210 226 const isOptionHovered = i === this.cursor; 211 227 if(isOptionHovered && isOptionSelected) { ··· 215 231 return opt(option, 'selected'); 216 232 } 217 233 return opt(option, isOptionHovered ? 'active' : 'inactive'); 218 - }).join(`\n${color.yellow(bar)} `)}\n${footer}\n`; 234 + }).join(`\n${color.yellow(S_BAR)} `)}\n${footer}\n`; 219 235 } 220 236 default: { 221 - return `${title}${color.cyan(bar)} ${this.options.map((option, i) => { 237 + return `${title}${color.cyan(S_BAR)} ${this.options.map((option, i) => { 222 238 const isOptionSelected = this.selectedValues.includes(option.value as any); 223 239 const isOptionHovered = i === this.cursor; 224 240 if(isOptionHovered && isOptionSelected) { ··· 228 244 return opt(option, 'selected'); 229 245 } 230 246 return opt(option, isOptionHovered ? 'active' : 'inactive'); 231 - }).join(`\n${color.cyan(bar)} `)}\n${color.cyan(barEnd)}\n`; 247 + }).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; 232 248 } 233 249 } 234 250 } ··· 242 258 ln = strip(ln); 243 259 return ln.length > sum ? ln.length : sum 244 260 }, 0) + 2; 245 - const msg = lines.map((ln) => `${color.gray(bar)} ${color.dim(ln)}${' '.repeat(len - strip(ln).length)}${color.gray(bar)}`).join('\n'); 246 - process.stdout.write(`${color.gray(bar)}\n${color.green(unicode ? '○' : '$')} ${color.reset(title)} ${color.gray((unicode ? '─' : '-').repeat(len - title.length - 1) + (unicode ? '╮' : '+'))}\n${msg}\n${color.gray((unicode ? '├' : '+') + (unicode ? '─' : '-').repeat(len + 2) + (unicode ? '╯' : '+'))}\n`); 261 + const msg = lines.map((ln) => `${color.gray(S_BAR)} ${color.dim(ln)}${' '.repeat(len - strip(ln).length)}${color.gray(S_BAR)}`).join('\n'); 262 + 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`); 247 263 }; 248 264 249 265 export const cancel = (message = "") => { 250 - process.stdout.write(`${color.gray(barEnd)} ${color.red(message)}\n\n`); 266 + process.stdout.write(`${color.gray(S_BAR_END)} ${color.red(message)}\n\n`); 251 267 }; 252 268 253 269 export const intro = (title = "") => { 254 - process.stdout.write(`${color.gray(barStart)} ${title}\n`); 270 + process.stdout.write(`${color.gray(S_BAR_START)} ${title}\n`); 255 271 }; 256 272 257 273 export const outro = (message = "") => { 258 274 process.stdout.write( 259 - `${color.gray(bar)}\n${color.gray(barEnd)} ${message}\n\n` 275 + `${color.gray(S_BAR)}\n${color.gray(S_BAR_END)} ${message}\n\n` 260 276 ); 261 277 }; 262 278 263 - const arc = unicode ? [ 264 - '◒', '◐', '◓', '◑' 265 - ] : ['.','o','O','0'] 279 + const frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•','o','O','0'] 266 280 267 281 export const spinner = () => { 268 282 let unblock: () => void; 269 283 let loop: NodeJS.Timer; 270 - const frames = arc; 271 284 const delay = unicode ? 80 : 120; 272 285 return { 273 286 start(message = "") { 274 287 message = message.replace(/\.?\.?\.$/, ""); 275 288 unblock = block(); 276 289 process.stdout.write( 277 - `${color.gray(bar)}\n${color.magenta("○")} ${message}\n` 290 + `${color.gray(S_BAR)}\n${color.magenta("○")} ${message}\n` 278 291 ); 279 292 let i = 0; 280 293 let dot = 0; ··· 293 306 process.stdout.write(erase.down(2)); 294 307 clearInterval(loop); 295 308 process.stdout.write( 296 - `${color.gray(bar)}\n${color.green(unicode ? "○" : '$')} ${message}\n` 309 + `${color.gray(S_BAR)}\n${color.green(S_STEP_SUBMIT)} ${message}\n` 297 310 ); 298 311 unblock(); 299 312 },