diff --git a/cli/deno.json b/cli/deno.json index 722276e6ec35..e7618c4a3b12 100644 --- a/cli/deno.json +++ b/cli/deno.json @@ -5,6 +5,8 @@ ".": "./mod.ts", "./parse-args": "./parse_args.ts", "./prompt-secret": "./prompt_secret.ts", + "./unstable-ansi": "./unstable_ansi.ts", + "./unstable-get-cursor": "./unstable_get_cursor.ts", "./unstable-progress-bar": "./unstable_progress_bar.ts", "./unstable-progress-bar-stream": "./unstable_progress_bar_stream.ts", "./unstable-prompt-select": "./unstable_prompt_select.ts", diff --git a/cli/unstable_ansi.ts b/cli/unstable_ansi.ts new file mode 100644 index 000000000000..7719979c8b36 --- /dev/null +++ b/cli/unstable_ansi.ts @@ -0,0 +1,1003 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +/** + * Ansi is a class with static methods and properties that returns various Ansi + * Escape Sequences. This class is not an exhaustive list of what is possible + * with Ansi Escape Sequences, nor does it guarantee that every code will work + * in every terminal. The only way to guarantee that only one code will work in + * a particular terminal, is to check for yourself. Calling these methods and + * properties does not automatically change the terminal settings. Only once + * they are passed to stdout or stderr will they take effect. + * + * These codes were based off the + * [xterm reference](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html). + * + * @example Basic Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log(Ansi.doubleHeightTop + "Hello World!"); + * console.log(Ansi.doubleHeightBottom + "Hello World!"); + * ``` + */ +export class Ansi { + /** + * Causes content on the current line to enlarge, showing only the top half + * of characters with each character taking up two columns. Can be used in + * combination with {@linkcode Ansi.doubleHeightBottom} on the next line to + * make text appear twice as big. + * + * @returns string The ANSI escape code for double-height top. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log(Ansi.doubleHeightTop + "Hello World!"); + * console.log(Ansi.doubleHeightBottom + "Hello World!"); + * ``` + */ + static get doubleHeightTop(): string { + return "\x1b#3"; + } + + /** + * Causes content on the current line to enlarge, showing only the bottom + * half of the characters with each character taking up two columns. Can be + * used in combination with {@linkcode Ansi.doubleHeightTop} on the previous + * line to make text appear twice as big. + * + * @returns string The ANSI escape code for double-height bottom. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log(Ansi.doubleHeightTop + "Hello World!"); + * console.log(Ansi.doubleHeightBottom + "Hello World!"); + * ``` + */ + static get doubleHeightBottom(): string { + return "\x1b#4"; + } + + /** + * Causes content on the current line to shrink down to a single column, + * essentially reverting the effects of {@linkcode Ansi.doubleHeightTop}, + * {@linkcode Ansi.doubleHeightBottom}, or {@linkcode Ansi.doubleWidth}. + * + * @returns string The ANSI escape code for single-width. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log(Ansi.doubleHeightTop + "Hello World!"); + * console.log(Ansi.doubleHeightBottom + "Hello World!"); + * await delay(1000); + * console.log( + * Ansi.moveCursorUpStart(2) + + * Ansi.deleteLines(1) + + * Ansi.singleWidth, + * ); + * ``` + */ + static get singleWidth(): string { + return "\x1b#5"; + } + + /** + * Causes content on the current line to stretch out, with each character + * taking up two columns. Can be reverted with {@linkcode Ansi.singleWidth}. + * + * @returns string The ANSI escape code for double-width. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log(Ansi.doubleWidth + "Hello World!"); + * ``` + */ + static get doubleWidth(): string { + return "\x1b#6"; + } + + /** + * Saves current cursor position that can later be restored with + * {@linkcode Ansi.restoreCursorPosition}. + * + * @returns string The ANSI escape code for saving cursor position. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log( + * Ansi.saveCursorPosition + + * Ansi.setCursorPosition(Deno.consoleSize().rows, 1) + + * Ansi.eraseLine + + * "Hello World!" + + * Ansi.restoreCursorPosition, + * ); + * ``` + */ + static get saveCursorPosition(): string { + return "\x1b7"; + } + + /** + * Restores cursor position that was earlier saved with + * {@linkcode Ansi.saveCursorPosition}. + * + * @returns string The ANSI escape code for restoring cursor position. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log( + * Ansi.saveCursorPosition + + * Ansi.setCursorPosition(Deno.consoleSize().rows) + + * Ansi.eraseLine + + * "Hello World!" + + * Ansi.restoreCursorPosition, + * ); + * ``` + */ + static get restoreCursorPosition(): string { + return "\x1b8"; + } + + /** + * This is a full reset of the terminal, reverting it back to its original + * default settings, clearing the screen, resetting modes, colors, character + * sets and more. Essentially making the terminal behave as if it were just + * started by the user. This command is very disruptive to the user. Also see + * {@linkcode Ansi.softReset}. + * + * @returns string The ANSI escape code for hard resetting the terminal. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log(Ansi.hideCursor); + * await delay(5000); + * console.log(Ansi.hardReset); + * ``` + */ + static get hardReset(): string { + return "\x1bc"; + } + + /** + * This command resets many settings to their initial state without fully + * reinitializing the terminal like {@linkcode Ansi.hardReset}. It preserves + * things like cursor position and display content, but clears modes, + * character sets, etc. Should probably be called when exiting the program. + * + * @returns string The ANSI escape code for soft resetting the terminal. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log(Ansi.hideCursor); + * await delay(5000); + * console.log(Ansi.softReset); + * ``` + */ + static get softReset(): string { + return "\x1b[!p"; + } + + /** + * Inserts `x` spaces at the cursor position. Shifting existing line content + * to the right. Cursor position does not change. Characters that exit the + * display are discarded. + * + * @param x The number of spaces to insert. + * @returns string The ANSI escape code for inserting spaces. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello World!"); + * await delay(1000); + * console.log( + * Ansi.moveCursorUp() + + * Ansi.setCursorColumn(6) + + * Ansi.insertSpace(10), + * ); + * ``` + */ + static insertSpace(x = 1): string { + return `\x1b[${x}@`; + } + + /** + * Deletes `x` characters at cursor position and to the right. Shifting line + * content left. + * + * @param x The number of characters to delete. + * @returns string The ANSI escape code for deleting characters. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello World!"); + * await delay(1000); + * console.log( + * Ansi.moveCursorUpStart() + + * Ansi.deleteCharacters(5) + + * "Bye", + * ); + * ``` + */ + static deleteCharacters(x = 1): string { + return `\x1b[${x}P`; + } + + /** + * Erases `x` characters at cursor position and to the right. + * + * @param x The number of characters to erase. + * @returns string The ANSI escape code for erasing characters. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello World!"); + * await delay(1000); + * console.log( + * Ansi.moveCursorUp() + + * Ansi.setCursorColumn(7) + + * Ansi.eraseCharacters(6) + + * "Bob!", + * ); + * ``` + */ + static eraseCharacters(x = 1): string { + return `\x1b[${x}X`; + } + + /** + * Inserts `x` lines at cursor position. Shifting current line and below + * down. Cursor position does not change. Characters that exit the display + * are discarded. + * + * @param x The number of lines to insert. + * @returns string The ANSI escape code for inserting lines. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello\nWorld!"); + * await delay(1000); + * console.log( + * Ansi.moveCursorUpStart() + + * Ansi.insertLines() + + * "and Goodbye", + * ); + * ``` + */ + static insertLines(x = 1): string { + return `\x1b[${x}L`; + } + + /** + * Deletes `x` lines at cursor position. Shifting below lines up. + * + * @param x The number of lines to delete. + * @returns string The ANSI escape code for deleting lines. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello\nWorld!"); + * await delay(1000); + * console.log(Ansi.moveCursorUpStart() + Ansi.deleteLines()); + * await delay(1000); + * console.log("and Goodbye!"); + * ``` + */ + static deleteLines(x = 1): string { + return `\x1b[${x}M`; + } + + /** + * Moves cursor position up `x` lines or up to the top margin. + * + * @param x The number of lines to move up. + * @returns string The ANSI escape code for moving the cursor up. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello\nWorld!"); + * await delay(1000); + * console.log(Ansi.moveCursorUp(2) + "Goodbye"); + * ``` + */ + static moveCursorUp(x = 1): string { + return `\x1b[${x}A`; + } + + /** + * Moves cursor position `x` lines up or up to the top of the margin, and to + * the beginning of that line. + * + * @param x The number of lines to move up. + * @returns string The ANSI escape code for moving the cursor up and to the + * beginning of the line. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello\nWorld!"); + * await delay(1000); + * console.log(Ansi.moveCursorUpStart(2) + "Goodbye"); + * ``` + */ + static moveCursorUpStart(x = 1): string { + return `\x1b[${x}F`; + } + + /** + * Moves cursor position down `x` lines or up to the bottom margin. + * + * @param x The number of lines to move down. + * @returns string The ANSI escape code for moving the cursor down. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello\nWorld!"); + * await delay(1000); + * console.log( + * Ansi.moveCursorUpStart(2) + + * "Goodbye" + + * Ansi.moveCursorDown() + + * Ansi.setCursorColumn() + + * Ansi.eraseLine + + * "Bob!", + * ); + * ``` + */ + static moveCursorDown(x = 1): string { + return `\x1b[${x}B`; + } + + /** + * Moves cursor position `x` lines down or up to the bottom margin, and to + * the beginning of that line. + * + * @param x The number of lines to move down. + * @returns string The ANSI escape code for moving the cursor down and to the + * beginning of the line. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello\nWorld!"); + * await delay(1000); + * console.log( + * Ansi.moveCursorUpStart(2) + + * "Goodbye" + + * Ansi.moveCursorDownStart() + + * Ansi.eraseLine + + * "Bob!", + * ); + * ``` + */ + static moveCursorDownStart(x = 1): string { + return `\x1b[${x}E`; + } + + /** + * Moves cursor position `x` columns right or up to the right margin. + * + * @param x The number of columns to move right. + * @returns string The ANSI escape code for moving the cursor right. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log(Ansi.moveCursorRight(2) + "Hello World!"); + * ``` + */ + static moveCursorRight(x = 1): string { + return `\x1b[${x}C`; + } + + /** + * Moves cursor position `x` tab stops right or up to the right margin. + * + * @param x The number of tab stops to move right. + * @returns string The ANSI escape code for moving the cursor right every tab + * stop. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log(Ansi.moveCursorRightTab() + "Hello World!"); + * ``` + */ + static moveCursorRightTab(x = 1): string { + return `\x1b[${x}I`; + } + + /** + * Moves cursor position `x` columns left or up to the left margin. + * + * @param x The number of columns to move left. + * @returns string The ANSI escape code for moving the cursor left. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log( + * Ansi.moveCursorRight(4) + + * Ansi.moveCursorLeft(2) + + * "Hello World!", + * ); + * ``` + */ + static moveCursorLeft(x = 1): string { + return `\x1b[${x}D`; + } + + /** + * Moves cursor position `x` tab stops left or up to the left margin. + * + * @param x The number of tab stops to move left. + * @returns string The ANSI escape code for moving the cursor left every tab + * stop. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log( + * Ansi.moveCursorRightTab(2) + + * Ansi.moveCursorLeftTab() + + * "Hello World!", + * ); + * ``` + */ + static moveCursorLeftTab(x = 1): string { + return `\x1b[${x}Z`; + } + + /** + * Sets cursor position to column `x` or up to the sides of the margins. + * Columns begin at `1` not `0`. + * + * @param x The column to move to. + * @returns string The ANSI escape code for setting the cursor column. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello World!"); + * await delay(1000); + * console.log( + * Ansi.moveCursorUp() + + * Ansi.setCursorColumn(7) + + * "and Goodbye!", + * ); + * ``` + */ + static setCursorColumn(x = 1): string { + return `\x1b[${x}G`; + } + + /** + * Sets cursor position to line `x` or down to the bottom of the margin. + * Lines begin at `1` not `0`. + * + * @param x The line to move to. + * @returns string The ANSI escape code for setting the cursor line. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log(Ansi.setCursorLine() + Ansi.eraseLine + "Hello World!"); + * ``` + */ + static setCursorLine(x = 1): string { + return `\x1b[${x}d`; + } + + /** + * Sets cursor position to `x` line and `y` column or up to the margin. Lines + * and columns begin at `1` not `0`. + * + * @param x The line to move to. + * @param y The column to move to. + * @returns string The ANSI escape code for setting the cursor position. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log( + * Ansi.setCursorPosition(5, 2) + + * Ansi.eraseLine + + * "Hello World!", + * ); + * ``` + */ + static setCursorPosition(x = 1, y = 1): string { + return `\x1b[${x};${y}H`; + } + + /** + * Erases line content to the right of cursor position. + * + * @returns string The ANSI escape code for erasing the line content to the + * right of the cursor. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello World!"); + * await delay(1000); + * console.log( + * Ansi.moveCursorUp() + + * Ansi.setCursorColumn(7) + + * Ansi.eraseLineAfterCursor, + * ); + * ``` + */ + static get eraseLineAfterCursor(): string { + return "\x1b[0K"; + } + + /** + * Erases line content to the left of cursor position. + * + * @returns string The ANSI escape code for erasing the line content to the + * left of the cursor. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello World!"); + * await delay(1000); + * console.log( + * Ansi.moveCursorUp() + + * Ansi.setCursorColumn(7) + + * Ansi.eraseLineBeforeCursor, + * ); + * ``` + */ + static get eraseLineBeforeCursor(): string { + return "\x1b[1K"; + } + + /** + * Erases entire line content. + * + * @returns string The ANSI escape code for erasing the entire line content. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello World!"); + * await delay(1000); + * console.log(Ansi.moveCursorUp() + Ansi.eraseLine); + * ``` + */ + static get eraseLine(): string { + return "\x1b[2K"; + } + + /** + * Erases content of lines below cursor position and content to the right on + * the same line as cursor. + * + * @returns string The ANSI escape code for erasing the content after the + * cursor. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello World!"); + * await delay(1000); + * console.log( + * Ansi.moveCursorUp() + + * Ansi.setCursorColumn(7) + + * Ansi.eraseDisplayAfterCursor, + * ); + * ``` + */ + static get eraseDisplayAfterCursor(): string { + return "\x1b[0J"; + } + + /** + * Erases content of lines above cursor position and content to the left on + * the same line as cursor. + * + * @returns string The ANSI escape code for erasing the content before the + * cursor. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello World!"); + * await delay(1000); + * console.log( + * Ansi.moveCursorUp() + + * Ansi.setCursorColumn(7) + + * Ansi.eraseDisplayBeforeCursor, + * ); + * ``` + */ + static get eraseDisplayBeforeCursor(): string { + return "\x1b[1J"; + } + + /** + * Erases all content. + * + * @returns string The ANSI escape code for erasing the entire display. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello World!"); + * await delay(1000); + * console.log(Ansi.eraseDisplay); + * ``` + */ + static get eraseDisplay(): string { + return "\x1b[2J"; + } + + /** + * Shifts content within the scrollable region up `x` lines, inserting blank + * lines at the bottom of the scrollable region. + * + * @param x The number of lines content should be shifted up. + * @returns string The ANSI escape code for shifting content up. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log(Ansi.shiftUpAndInsert()); + * ``` + */ + static shiftUpAndInsert(x = 1): string { + return `\x1b[${x}S`; + } + + /** + * Shifts content within the scrollable region down `x` lines, inserting + * blank lines at the top of the scrollable region. + * + * @param x The number of lines content should be shifted down. + * @returns string The ANSI escape code for shifting content down. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log(Ansi.shiftDownAndInsert(3)); + * ``` + */ + static shiftDownAndInsert(x = 1): string { + return `\x1b[${x}T`; + } + + /** + * Repeats last graphic character printed `x` times at cursor position. + * + * @param x The number of times the last character printed should be repeated. + * @returns string The ANSI escape code for repeating the last printed + * character. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello World!" + Ansi.repeatLastCharacter(4)); + * ``` + */ + static repeatLastCharacter(x = 1): string { + return `\x1b[${x}b`; + } + + /** + * Causes existing characters to the right of the cursor position to shift + * right as new characters are written. Opposite of + * {@linkcode Ansi.replaceMode}. + * + * @returns string The ANSI escape code for entering insert mode. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello World!"); + * await delay(1000); + * console.log( + * Ansi.insertMode + + * Ansi.moveCursorUp() + + * Ansi.setCursorColumn(7) + + * "and Goodbye " + + * Ansi.replaceMode, + * ); + * ``` + */ + static get insertMode(): string { + return "\x1b[4h"; + } + + /** + * Causes existing characters to be overwritten at the cursor position by new + * characters. See also {@linkcode Ansi.insertMode}. + * + * @returns string The ANSI escape code for entering replace mode. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log("Hello World!"); + * await delay(1000); + * console.log( + * Ansi.insertMode + + * Ansi.moveCursorUp() + + * Ansi.setCursorColumn(7) + + * "and Goodbye " + + * Ansi.replaceMode, + * ); + * ``` + */ + static get replaceMode(): string { + return "\x1b[4l"; + } + + /** + * Causes top and bottom margins to shrink to scrollable region (See + * {@linkcode Ansi.setScrollableRegion}) preventing the cursor from moving + * to the lines outside it. + * + * @returns string The ANSI escape code for enabling origin mode. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log( + * Ansi.eraseDisplay + + * Ansi.setCursorPosition() + + * "Hello World" + + * Ansi.setScrollableRegion(2) + + * Ansi.enableOriginMode, + * ); + * await delay(1000); + * console.log(Ansi.setCursorPosition() + "Bye World!"); + * ``` + */ + static get enableOriginMode(): string { + return "\x1b[?6h"; + } + + /** + * Causes the top and bottom margins to enlarge to the user's display. See + * {@linkcode Ansi.enableOriginMode}. + * + * @returns string The ANSI escape code for disabling origin mode. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log( + * Ansi.eraseDisplay + + * Ansi.setCursorPosition() + + * "Hello World" + + * Ansi.setScrollableRegion(2) + + * Ansi.enableOriginMode, + * ); + * await delay(1000); + * console.log(Ansi.setCursorPosition() + "Bye World!"); + * await delay(1000); + * console.log( + * Ansi.disableOriginMode + + * Ansi.setCursorPosition() + + * Ansi.eraseLine + + * "Hi World!", + * ); + * ``` + */ + static get disableOriginMode(): string { + return "\x1b[?6l"; + } + + /** + * Causes cursor to automatically move to the next line when it hits the + * end of the current line to continue writing. See also + * {@linkcode Ansi.disableAutoWrap}. + * + * @returns string The ANSI escape code to enable auto-wrap. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log(Ansi.enableAutoWrap + "A" + "h".repeat(500)); + * ``` + */ + static get enableAutoWrap(): string { + return "\x1b[?7h"; + } + + /** + * Causes cursor to remain on the same line when it hits the end of the + * current line. See also {@linkcode Ansi.enableAutoWrap}. + * + * @returns string The ANSI escape code to disable auto-wrap. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log(Ansi.disableAutoWrap + "A" + "h".repeat(500)); + * ``` + */ + static get disableAutoWrap(): string { + return "\x1b[?7l"; + } + + /** + * Sets the cursor animation style. + * + * @param x The cursor style to set. + * @returns string The ANSI escape code to set the cursor style. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log(Ansi.setCursorStyle(CursorStyle.BlinkingUnderline)); + * ``` + */ + static setCursorStyle(x: CursorStyle): string { + return `\x1b[${x} q`; + } + + /** + * Causes cursor position to be visible to the user. See also + * {@linkcode Ansi.hideCursor}. + * + * @returns string The ANSI escape code to show the cursor. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log(Ansi.hideCursor); + * await delay(5000); + * console.log(Ansi.showCursor); + * ``` + */ + static get showCursor(): string { + return "\x1b[?25h"; + } + + /** + * Causes cursor position to be hidden from the user. See also + * {@linkcode Ansi.showCursor}. + * + * @returns string The ANSI escape code to hide the cursor. + * + * @example Usage + * ```ts ignore + * import { delay } from "@std/async/delay"; + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log(Ansi.hideCursor); + * await delay(5000); + * console.log(Ansi.showCursor); + * ``` + */ + static get hideCursor(): string { + return "\x1b[?25l"; + } + + /** + * Sets the scrollable region of the display. Allowing either or both the top + * and bottom lines to not have their content moved when the scrolling region + * is updated. `x` is the top line of the scrollable region. `y` is the + * bottom line of the scrollable region. + * + * @param x The top line of the scrollable region. + * @param y The bottom line of the scrollable region. + * @returns string The ANSI escape code to set the scrollable region. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log( + * Ansi.eraseDisplay + + * Ansi.setScrollableRegion(3, 10), + * ); + * setInterval(() => console.log(Math.random()), 1000); + * ``` + */ + static setScrollableRegion(x = 1, y?: number): string { + return `\x1b[${x}${y == undefined ? "" : `;${y}`}r`; + } +} + +/** + * CursorStyle is a const enum used to set the value in + * {@linkcode Ansi.setCursorStyle}. + * + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable_ansi"; + * + * console.log(Ansi.setCursorStyle(CursorStyle.BlinkingUnderline)); + * ``` + */ +export const enum CursorStyle { + Default = 0, + BlinkingBlock = 1, + SteadyBlock = 2, + BlinkingUnderline = 3, + SteadyUnderline = 4, + BlinkingBar = 5, + SteadyBar = 6, +} diff --git a/cli/unstable_get_cursor.ts b/cli/unstable_get_cursor.ts new file mode 100644 index 000000000000..16341d4e1b5b --- /dev/null +++ b/cli/unstable_get_cursor.ts @@ -0,0 +1,78 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +/** + * This function asks the terminal for the cursor's current position. + * Information entered by the user will be discarded while requesting the cursor + * position. The cursor position may have also moved in this time due to the + * user entering information. Due to the sync nature of this function, it is + * not recommended to use this function frequently. Instead + * {@linkcode Ansi.saveCursorPosition} and + * {@linkcode Ansi.restoreCursorPosition} should be used when possible. + * + * @returns The cursor position or null if the terminal is not a TTY. + * @example Usage + * ```ts ignore + * import { Ansi } from "@std/cli/unstable-ansi"; + * import { askForCursorPositionSync } from "@std/cli/unstable-get-cursor"; + * + * const { column, row } = askForCursorPositionSync()!; + * await Deno.stderr.write(new TextEncoder().encode( + * Ansi.setCursorPosition() + + * "Hello World!" + + * Ansi.setCursorPosition(row, column), + * )); + * ``` + */ +export function askForCursorPositionSync(): + | { row: number; column: number } + | null { + if (!Deno.stdin.isTerminal() || !Deno.stderr.isTerminal()) { + return null; + } + Deno.stdin.setRaw(true, { cbreak: true }); + Deno.stderr.writeSync(Uint8Array.from([0x1b, 0x5b, 0x36, 0x6e])); // \x1b[6n + const buffer = new Uint8Array(1024); + while (true) { + const x = Deno.stdin.readSync(buffer.subarray(32)); + const csi = findCSI(buffer.subarray(0, 32 + x!)); + if (csi?.final === 82) { // R + Deno.stdin.setRaw(false); + const [row, column] = new TextDecoder() + .decode(csi.parameters) + .split(";") + .map((x) => parseInt(x)); + return { row: row!, column: column! }; + } + buffer.set(buffer.subarray(buffer.length - 32)); + } +} + +function findCSI( + buffer: Uint8Array, +): { parameters: Uint8Array; intermediates: Uint8Array; final: number } | null { + for (let i = 0; i < buffer.length - 1; ++i) { + if (buffer[i] === 0x1b && buffer[i + 1] === 0x5b) { + i += 2; + const parameters = getRange(buffer.subarray(i), 0x30, 0x3f); + i += parameters.length; + const intermediates = getRange(buffer.subarray(i), 0x20, 0x2f); + i += intermediates.length; + const final = buffer[i]; + if (final != undefined && 0x40 <= final && final <= 0x7e) { + return { parameters, intermediates, final }; + } + } + } + return null; +} + +function getRange(buffer: Uint8Array, min: number, max: number) { + let i = 0; + for (; i < buffer.length; ++i) { + const byte = buffer[i]!; + if (byte < min || max < byte) { + return buffer.subarray(0, i); + } + } + return buffer.subarray(0, i); +}