1/**
2 * ANSI escape sequences can be used to style text in a tty or perform other
3 * interactions. this file contains many constants for escape codes, as well as
4 * functions for ones that take an argument, such as `cursorUp`.
5 *
6 * note that this file only produces escape sequences, and does not yet feature
7 * detection or fallback code.
8 *
9 * @module ansi
10 */
11
12/** resets all ansi styles */
13export const reset: string = `\x1b[0m`;
14
15/**
16 * puts the text in bold. can be combined with `dim` and colors.
17 * reset with `resetWeight`.
18 */
19export const bold: string = `\x1b[1m`;
20/**
21 * puts the text a more faded color, dimming it. can be combined with `bold` to
22 * produce bold-dim text. dim can be combined with any color to make a dimmed
23 * version of that color. reset with `resetWeight`.
24 */
25export const dim: string = `\x1b[2m`;
26/** resets `bold` and `dim` */
27export const resetWeight: string = `\x1b[22m`;
28
29/** resets foreground color */
30export const fgReset: string = `\x1b[39m`;
31/** sets the foreground color to black. on light mode terminals, this color may
32 * be visible. */
33export const fgBlack: string = `\x1b[30m`;
34/** sets the foreground color to red */
35export const fgRed: string = `\x1b[31m`;
36/** sets the foreground color to green */
37export const fgGreen: string = `\x1b[32m`;
38/** sets the foreground color to yellow */
39export const fgYellow: string = `\x1b[33m`;
40/** sets the foreground color to blue */
41export const fgBlue: string = `\x1b[34m`;
42/** sets the foreground color to magenta */
43export const fgMagenta: string = `\x1b[35m`;
44/** sets the foreground color to cyan */
45export const fgCyan: string = `\x1b[36m`;
46/**
47 * sets the foreground color to (dark) white / grey / gray. this color is
48 * brighter than `fgBrightBlack`. consider `dim` for greyed out text and
49 * `fgReset` to set the default foreground color.
50 */
51export const fgWhite: string = `\x1b[37m`;
52/**
53 * sets the foreground color to bright black / grey / gray. consider `dim` for
54 * greyed out text.
55 */
56export const fgBrightBlack: string = `\x1b[90m`;
57/** sets the foreground color to bright red */
58export const fgBrightRed: string = `\x1b[91m`;
59/** sets the foreground color to bright green */
60export const fgBrightGreen: string = `\x1b[92m`;
61/** sets the foreground color to bright yellow */
62export const fgBrightYellow: string = `\x1b[93m`;
63/** sets the foreground color to bright blue */
64export const fgBrightBlue: string = `\x1b[94m`;
65/** sets the foreground color to bright magenta */
66export const fgBrightMagenta: string = `\x1b[95m`;
67/** sets the foreground color to bright cyan */
68export const fgBrightCyan: string = `\x1b[96m`;
69/**
70 * sets the foreground color to bright white. consider `dim` for greyed out
71 * text and `fgReset` to set the default foreground color.
72 */
73export const fgBrightWhite: string = `\x1b[97m`;
74/** sets the foreground color to a color in the 256 color palette. */
75export function fg256(n: number): string {
76 return `\x1b[38;5;${n}m`;
77}
78/** sets the foreground color to an rgb color. */
79export function fgRgb(r: number, g: number, b: number): string {
80 return `\x1b[38;2;${r};${g};${b}m`;
81}
82
83/** resets the cell background color. */
84export const bgReset: string = `\x1b[49m`;
85/** sets the cell background to black. consider `bgReset` to clear the background. */
86export const bgBlack: string = `\x1b[40m`;
87/** sets the cell background to red. */
88export const bgRed: string = `\x1b[41m`;
89/** sets the cell background to green. */
90export const bgGreen: string = `\x1b[42m`;
91/** sets the cell background to yellow. */
92export const bgYellow: string = `\x1b[43m`;
93/** sets the cell background to blue. */
94export const bgBlue: string = `\x1b[44m`;
95/** sets the cell background to magenta. */
96export const bgMagenta: string = `\x1b[45m`;
97/** sets the cell background to cyan. */
98export const bgCyan: string = `\x1b[46m`;
99/**
100 * sets the cell background to (dark) white / grey / gray. this color is
101 * brighter than `bgBrightBlack`.
102 */
103export const bgWhite: string = `\x1b[47m`;
104/**
105 * sets the cell background to bright black. consider `bgReset` to clear the
106 * background.
107 */
108export const bgBrightBlack: string = `\x1b[100m`;
109/** sets the cell background to bright red. */
110export const bgBrightRed: string = `\x1b[101m`;
111/** sets the cell background to bright green. */
112export const bgBrightGreen: string = `\x1b[102m`;
113/** sets the cell background to bright yellow. */
114export const bgBrightYellow: string = `\x1b[103m`;
115/** sets the cell background to bright blue. */
116export const bgBrightBlue: string = `\x1b[104m`;
117/** sets the cell background to bright magenta. */
118export const bgBrightMagenta: string = `\x1b[105m`;
119/** sets the cell background to bright cyan. */
120export const bgBrightCyan: string = `\x1b[106m`;
121/** sets the cell background to bright white. */
122export const bgBrightWhite: string = `\x1b[107m`;
123/** sets the background color to a color in the 256 color palette. */
124export function bg256(n: number): string {
125 return `\x1b[48;5;${n}m`;
126}
127/** sets the background color to an rgb color. */
128export function bgRgb(r: number, g: number, b: number): string {
129 return `\x1b[48;2;${r};${g};${b}m`;
130}
131
132// cursor management
133
134/** save the position of the cursor, restored with {@linkcode cursorRestore}. */
135export const cursorSave: string = "\x1b7";
136/** restore the position of the cursor, saved with {@linkcode cursorSave}. */
137export const cursorRestore: string = "\x1b8";
138
139/** move the cursor up `n` units. */
140export function cursorUp(n: number): string {
141 return n ? `\x1b[${n}A` : "";
142}
143/** move the cursor down `n` units. */
144export function cursorDown(n: number): string {
145 return n ? `\x1b[${n}B` : "";
146}
147/** move the cursor right `n` units. */
148export function cursorRight(n: number): string {
149 return n ? `\x1b[${n}C` : "";
150}
151/** move the cursor right `n` units. */
152export function cursorLeft(n: number): string {
153 return n ? `\x1b[${n}D` : "";
154}
155/** move the cursor to the start of it's current line. */
156export const startOfLine: string = "\r";
157/** move the cursor to the start of the next line. */
158export const startOfNextLine: string = "\n";
159
160// erasure
161/** clear from and including the current cell right to the end of the line. */
162export const clearToEndOfLine: string = "\x1b[K";
163/** clear from and including the current cell left to the start of the line. */
164export const clearToStartOfLine: string = "\x1b[1K";
165/** clear the entire line that the cursor is on without moving it. */
166export const clearFullLine: string = "\x1b[2K";
167/** clear from and including the current cell to the end of the screen. */
168export const clearToEndOfScreen: string = "\x1b[0J";
169
170/**
171 * Begin Synchronized Output
172 * https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036
173 */
174export const syncStart: string = "\x1b[?2026h";
175/** End Synchronized Output */
176export const syncEnd: string = "\x1b[?2026l";
177
178/**
179 * Apply ANSI styles to text. `code` is a concatenation of zero or more styles,
180 * which will be cleared as required.
181 */
182export function style(code: string, text: string): string {
183 code = mergeStyles(code);
184 if (!code) return text;
185 const fg = /[\[;][39]/.test(code);
186 const bg = /[\[;](?:4|10)/.test(code);
187 const bold = /[\[;](?:1)[;m]/.test(code);
188 const dim = /[\[;](?:2)[;m]/.test(code);
189 return code + text
190 + mergeStyles(
191 (fg ? fgReset : "")
192 + (bg ? bgReset : "")
193 + (bold ? resetWeight : "")
194 + (dim ? resetWeight : ""),
195 );
196}
197
198/**
199 * Given a string of multiple style ansi codes, combines all of them.
200 * Must be used on a string of only ansi style codes.
201 */
202export function mergeStyles(code: string): string {
203 let styles = [];
204 while (code.length) {
205 if (!code.startsWith("\x1b[")) {
206 throw new Error("invalid style string: " + JSON.stringify(code));
207 }
208 const end = code.indexOf("m");
209 if (end === -1) {
210 throw new Error("invalid style string: " + JSON.stringify(code));
211 }
212 styles.push(code.slice(2, end));
213 code = code.slice(end + 1);
214 }
215 return `\x1b[${styles.join(";")}m`;
216}
217
218/**
219 * Convert a string with ansi text into a string where all the codes have been
220 * turned into their original names.
221 */
222export function debugAnsi(text: string): string {
223 // deno-fmt-ignore
224 const verbaitim: (keyof typeof self)[] = [
225 "fgCyan",
226 "fgBlue",
227 "bgReset",
228 "bgBlack",
229 "bgRed",
230 "bgGreen",
231 "bgYellow",
232 "bgBlue",
233 "bgMagenta",
234 "bgCyan",
235 "bgWhite",
236 "bgBrightBlack",
237 "bgBrightRed",
238 "bgBrightGreen",
239 "bgBrightYellow",
240 "bgBrightBlue",
241 "bgBrightMagenta",
242 "bgBrightCyan",
243 "bgBrightWhite",
244 "fgReset",
245 "fgBlack",
246 "fgRed",
247 "fgGreen",
248 "fgYellow",
249 "fgMagenta",
250 "fgWhite",
251 "fgBrightBlack",
252 "fgBrightRed",
253 "fgBrightGreen",
254 "fgBrightYellow",
255 "fgBrightBlue",
256 "fgBrightMagenta",
257 "fgBrightCyan",
258 "fgBrightWhite",
259 "clearToEndOfLine",
260 "clearToStartOfLine",
261 "clearFullLine",
262 "clearToEndOfScreen",
263 "syncStart",
264 "syncEnd",
265 "bold",
266 "dim",
267 "resetWeight",
268 "cursorSave",
269 "cursorRestore",
270 ];
271 for (const key of verbaitim) {
272 text = text.replaceAll(
273 String(self[key]),
274 style(key.includes("cursor") ? fgBlue : fgCyan, `\${${key}}`),
275 );
276 }
277 text = text.replace(/\x1b\[(\d+)A/g, style(fgYellow, "$${cursorUp($1)}"));
278 text = text.replace(/\x1b\[(\d+)B/g, style(fgYellow, "$${cursorDown($1)}"));
279 text = text.replace(/\x1b\[(\d+)C/g, style(fgYellow, "$${cursorRight($1)}"));
280 text = text.replace(/\x1b\[(\d+)D/g, style(fgYellow, "$${cursorLeft($1)}"));
281 text = text.replace(/\n/g, style(fgRed, "\\n"));
282 text = text.replace(/\r/g, style(fgMagenta, "\\r"));
283 return text;
284}
285
286/** Measure the visible length of a string, ignoring ansi codes. */
287export function widthInTerminal(str: string): number {
288 segmenter ??= new Intl.Segmenter();
289 let width = 0;
290 if (str.includes("\x1b")) str = str.replace(ansiEscapes, "");
291 for (let i = 0, { length } = str; i < length; i += 1) {
292 const code = str.charCodeAt(i);
293 if (code < 32) continue;
294 if (code < 127) {
295 width += 1;
296 continue;
297 }
298 // the remainder of the string is measured with the segmenter, which
299 // handles ascii correctly via segmentWidth (controls are zero-width).
300 for (const { segment } of segmenter.segment(str.slice(i))) {
301 width += segmentWidth(segment);
302 }
303 return width;
304 }
305 return width;
306}
307
308/**
309 * Like `str.slice(0, columns)`, but respecting width in a terminal.
310 * preserves ansi escape sequences.
311 */
312export function trimForTerminal(str: string, columns: number): string {
313 segmenter ??= new Intl.Segmenter();
314 let width = 0;
315 let ansi = false;
316 for (let i = 0, { length } = str; i < length; i += 1) {
317 const code = str.charCodeAt(i);
318 if (code < 32) {
319 if (code === 0x1b) {
320 const esc = str.slice(i).match(ansiEscapes);
321 if (esc) i += esc[0].length - 1, ansi = true;
322 }
323 continue;
324 }
325 if (code < 127) {
326 width += 1;
327 if (width >= columns) {
328 const result = str.slice(0, i + 1);
329 return ansi ? result + "\x1b[0m" : result;
330 }
331 continue;
332 }
333 // non-ascii path
334 let out = str.slice(0, i);
335 str = str.slice(i);
336 const segments = segmenter.segment(str);
337 {
338 const { segment } = UNWRAP(segments.containing(0));
339 width += segmentWidth(segment);
340 if (width > columns) return ansi ? out + "\x1b[0m" : out;
341 out += segment;
342 i = segment.length;
343 }
344 for ({ length } = str; i < length; i += 1) {
345 const code = str.charCodeAt(i);
346 if (code === 0x1b) {
347 const esc = str.slice(i).match(ansiEscapes);
348 if (esc) {
349 i += esc[0].length - 1, ansi = true;
350 out += esc[0];
351 continue;
352 }
353 }
354 const { segment } = UNWRAP(segments.containing(i));
355 width += segmentWidth(segment);
356 if (width > columns) return ansi ? out + "\x1b[0m" : out;
357 out += segment;
358 }
359 return out;
360 }
361 return str;
362}
363
364// derived from sindresorhus's wonderful `string-width`
365// https://github.com/sindresorhus/string-width/blob/42e7b697393a9b9ff732f9bd3ab1db87c3c208b2/index.js
366// https://github.com/sindresorhus/get-east-asian-width/blob/6aee3824b5e2ade910a38ed93cf9d89b7b5a8e5b/index.js
367// MIT license
368let segmenter: Intl.Segmenter | null = null;
369const ansiEscapes =
370 /[\u001B\u009B][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d\/\#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d\/\#&.:=?%@~_]*)*)?(?:\u0007|\u001B\u005C|\u009C))|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
371const zeroWidthClusterRegex = /^(?:\p{Default_Ignorable_Code_Point}|\p{Control}|\p{Mark}|\p{Surrogate})+$/v;
372const leadingNonPrintingRegex = /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+/v;
373const rgiEmojiRegex = /^\p{RGI_Emoji}$/v;
374
375/** remove all ansi escape sequences */
376export function strip(text: string): string {
377 return text.replace(ansiEscapes, "");
378}
379
380function segmentWidth(segment: string) {
381 if (zeroWidthClusterRegex.test(segment)) return 0;
382 if (rgiEmojiRegex.test(segment)) return 2;
383
384 // deno-fmt-ignore
385 const list = [
386 0x3000,
387 0x231A,
388 0x231B,
389 0x2329,
390 0x232A,
391 0x23F0,
392 0x23F3,
393 0x25FD,
394 0x25FE,
395 0x2614,
396 0x2615,
397 0x267F,
398 0x2693,
399 0x26A1,
400 0x26AA,
401 0x26AB,
402 0x26BD,
403 0x26BE,
404 0x26C4,
405 0x26C5,
406 0x26CE,
407 0x26D4,
408 0x26EA,
409 0x26F2,
410 0x26F3,
411 0x26F5,
412 0x26FA,
413 0x26FD,
414 0x2705,
415 0x270A,
416 0x270B,
417 0x2728,
418 0x274C,
419 0x274E,
420 0x2757,
421 0x27B0,
422 0x27BF,
423 0x2B1B,
424 0x2B1C,
425 0x2B50,
426 0x2B55,
427 0x1AFFD,
428 0x1AFFE,
429 0x1B132,
430 0x1B155,
431 0x1F004,
432 0x1F0CF,
433 0x1F18E,
434 0x1F250,
435 0x1F251,
436 0x1F3F4,
437 0x1F440,
438 0x1F57A,
439 0x1F595,
440 0x1F596,
441 0x1F5A4,
442 0x1F6CC,
443 0x1F6EB,
444 0x1F6EC,
445 0x1F7F0,
446 0x1FAC8,
447 ];
448 const x = segment.replace(leadingNonPrintingRegex, "").codePointAt(0) ?? 0;
449 return 1 + +(
450 // full
451 x >= 0xFF01 && x <= 0xFF60 || x >= 0xFFE0 && x <= 0xFFE6
452 // wide
453 || x >= 0x1100 && x <= 0x115F || x >= 0x23E9 && x <= 0x23EC
454 || x >= 0x2630 && x <= 0x2637 || x >= 0x2648 && x <= 0x2653
455 || x >= 0x268A && x <= 0x268F || x >= 0x2753 && x <= 0x2755
456 || x >= 0x2795 && x <= 0x2797 || x >= 0x2E80 && x <= 0x2E99
457 || x >= 0x2E9B && x <= 0x2EF3 || x >= 0x2F00 && x <= 0x2FD5
458 || x >= 0x2FF0 && x <= 0x2FFF || x >= 0x3001 && x <= 0x303E
459 || x >= 0x3041 && x <= 0x3096 || x >= 0x3099 && x <= 0x30FF
460 || x >= 0x3105 && x <= 0x312F || x >= 0x3131 && x <= 0x318E
461 || x >= 0x3190 && x <= 0x31E5 || x >= 0x31EF && x <= 0x321E
462 || x >= 0x3220 && x <= 0x3247 || x >= 0x3250 && x <= 0xA48C
463 || x >= 0xA490 && x <= 0xA4C6 || x >= 0xA960 && x <= 0xA97C
464 || x >= 0xAC00 && x <= 0xD7A3 || x >= 0xF900 && x <= 0xFAFF
465 || x >= 0xFE10 && x <= 0xFE19 || x >= 0xFE30 && x <= 0xFE52
466 || x >= 0xFE54 && x <= 0xFE66 || x >= 0xFE68 && x <= 0xFE6B
467 || x >= 0x16FE0 && x <= 0x16FE4 || x >= 0x16FF0 && x <= 0x16FF6
468 || x >= 0x17000 && x <= 0x18CD5 || x >= 0x18CFF && x <= 0x18D1E
469 || x >= 0x18D80 && x <= 0x18DF2 || x >= 0x1AFF0 && x <= 0x1AFF3
470 || x >= 0x1AFF5 && x <= 0x1AFFB || x >= 0x1B150 && x <= 0x1B152
471 || x >= 0x1B164 && x <= 0x1B167 || x >= 0x1B170 && x <= 0x1B2FB
472 || x >= 0x1D300 && x <= 0x1D356 || x >= 0x1D360 && x <= 0x1D376
473 || x >= 0x1F191 && x <= 0x1F19A || x >= 0x1F200 && x <= 0x1F202
474 || x >= 0x1F210 && x <= 0x1F23B || x >= 0x1F240 && x <= 0x1F248
475 || x >= 0x1F260 && x <= 0x1F265 || x >= 0x1F300 && x <= 0x1F320
476 || x >= 0x1F32D && x <= 0x1F335 || x >= 0x1F337 && x <= 0x1F37C
477 || x >= 0x1F37E && x <= 0x1F393 || x >= 0x1F3A0 && x <= 0x1F3CA
478 || x >= 0x1F3CF && x <= 0x1F3D3 || x >= 0x1F3E0 && x <= 0x1F3F0
479 || x >= 0x1F3F8 && x <= 0x1F43E || x >= 0x1F442 && x <= 0x1F4FC
480 || x >= 0x1F4FF && x <= 0x1F53D || x >= 0x1F54B && x <= 0x1F54E
481 || x >= 0x1F550 && x <= 0x1F567 || x >= 0x1F5FB && x <= 0x1F64F
482 || x >= 0x1F680 && x <= 0x1F6C5 || x >= 0x1F6D0 && x <= 0x1F6D2
483 || x >= 0x1F6D5 && x <= 0x1F6D8 || x >= 0x1F6DC && x <= 0x1F6DF
484 || x >= 0x1F6F4 && x <= 0x1F6FC || x >= 0x1F7E0 && x <= 0x1F7EB
485 || x >= 0x1F90C && x <= 0x1F93A || x >= 0x1F93C && x <= 0x1F945
486 || x >= 0x1F947 && x <= 0x1F9FF || x >= 0x1FA70 && x <= 0x1FA7C
487 || x >= 0x1FA80 && x <= 0x1FA8A || x >= 0x1FA8E && x <= 0x1FAC6
488 || x >= 0x1FACD && x <= 0x1FADC || x >= 0x1FADF && x <= 0x1FAEA
489 || x >= 0x1FAEF && x <= 0x1FAF8 || x >= 0x20000 && x <= 0x2FFFD
490 || x >= 0x30000 && x <= 0x3FFFD || x >= 0x1B000 && x <= 0x1B122
491 || list.includes(x)
492 );
493}
494
495import { UNWRAP } from "../assert.ts";
496import * as self from "./ansi.ts";