Skip to content
On this page
字数:2.8k 字
预计:16 分钟
阅读量:

Ascii art 字体推荐

作者:winches
更新于:6 个月前

网站地址:ASCII Art(AA)生成器

fancy65 𝒩𝑒𝓍𝓉𝒰𝐼

fancy66 𝓝𝒆𝔁𝓽𝓤𝓘

handwriting1 𝒩𝑒𝓍𝓉𝒰𝐼

white_square 🄽🄴🅇🅃🅄🄸

Pacifico https://bjornlu.com/https://fonts.google.com/specimen/Pacifico

关于 node-cli 的指针移动,输出控制等

仓库:ansi-escapes

代码核心:

js
/**
 * This module provides a collection of ANSI escape sequences for manipulating the cursor and performing various terminal operations.
 * It includes functions for moving the cursor, erasing lines and screens, scrolling, and more.
 *
 * @module ansiEscapes
 */

import process from 'node:process'

// Constants for ANSI escape sequences
const ESC = '\u001B['
const OSC = '\u001B]'
const BEL = '\u0007'
const SEP = ';'

// Check if running in a browser environment
const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'

// Check if running in Apple Terminal
const isTerminalApp = !isBrowser && process.env.TERM_PROGRAM === 'Apple_Terminal'

// Check if running on Windows
const isWindows = !isBrowser && process.platform === 'win32'

// Define the `cwdFunction` based on the environment
const cwdFunction = isBrowser
  ? () => {
      throw new Error('`process.cwd()` only works in Node.js, not the browser.')
    }
  : process.cwd

// Create an object to hold the ANSI escape sequence functions
const ansiEscapes = {}

/**
 * Moves the cursor to the specified position.
 *
 * @param {number} x - The column position (1-based index).
 * @param {number} [y] - The row position (1-based index). If not provided, the cursor will only move horizontally.
 * @returns {string} The ANSI escape sequence for moving the cursor to the specified position.
 * @throws {TypeError} If `x` is not a number.
 */
ansiEscapes.cursorTo = (x, y) => {
  if (typeof x !== 'number')
    throw new TypeError('The `x` argument is required')

  if (typeof y !== 'number')
    return `${ESC + (x + 1)}G`

  return `${ESC + (y + 1) + SEP + (x + 1)}H`
}

/**
 * Moves the cursor by the specified amount in the horizontal and vertical directions.
 *
 * @param {number} x - The number of columns to move the cursor. Positive values move the cursor to the right, negative values move it to the left.
 * @param {number} y - The number of rows to move the cursor. Positive values move the cursor down, negative values move it up.
 * @returns {string} The ANSI escape sequence for moving the cursor by the specified amount.
 * @throws {TypeError} If `x` is not a number.
 */
ansiEscapes.cursorMove = (x, y) => {
  if (typeof x !== 'number')
    throw new TypeError('The `x` argument is required')

  let returnValue = ''

  if (x < 0)
    returnValue += `${ESC + (-x)}D`
  else if (x > 0)
    returnValue += `${ESC + x}C`

  if (y < 0)
    returnValue += `${ESC + (-y)}A`
  else if (y > 0)
    returnValue += `${ESC + y}B`

  return returnValue
}

/**
 * Moves the cursor up by the specified number of rows.
 *
 * @param {number} [count=1] - The number of rows to move the cursor up.
 * @returns {string} The ANSI escape sequence for moving the cursor up.
 */
ansiEscapes.cursorUp = (count = 1) => `${ESC + count}A`

/**
 * Moves the cursor down by the specified number of rows.
 *
 * @param {number} [count=1] - The number of rows to move the cursor down.
 * @returns {string} The ANSI escape sequence for moving the cursor down.
 */
ansiEscapes.cursorDown = (count = 1) => `${ESC + count}B`

/**
 * Moves the cursor forward by the specified number of columns.
 *
 * @param {number} [count=1] - The number of columns to move the cursor forward.
 * @returns {string} The ANSI escape sequence for moving the cursor forward.
 */
ansiEscapes.cursorForward = (count = 1) => `${ESC + count}C`

/**
 * Moves the cursor backward by the specified number of columns.
 *
 * @param {number} [count=1] - The number of columns to move the cursor backward.
 * @returns {string} The ANSI escape sequence for moving the cursor backward.
 */
ansiEscapes.cursorBackward = (count = 1) => `${ESC + count}D`

/**
 * Moves the cursor to the beginning of the current line.
 *
 * @returns {string} The ANSI escape sequence for moving the cursor to the beginning of the line.
 */
ansiEscapes.cursorLeft = `${ESC}G`

/**
 * Saves the current cursor position.
 *
 * @returns {string} The ANSI escape sequence for saving the cursor position.
 */
ansiEscapes.cursorSavePosition = isTerminalApp ? '\u001B7' : `${ESC}s`

/**
 * Restores the previously saved cursor position.
 *
 * @returns {string} The ANSI escape sequence for restoring the cursor position.
 */
ansiEscapes.cursorRestorePosition = isTerminalApp ? '\u001B8' : `${ESC}u`

/**
 * Gets the current cursor position.
 *
 * @returns {string} The ANSI escape sequence for getting the cursor position.
 */
ansiEscapes.cursorGetPosition = `${ESC}6n`

/**
 * Moves the cursor to the beginning of the next line.
 *
 * @returns {string} The ANSI escape sequence for moving the cursor to the beginning of the next line.
 */
ansiEscapes.cursorNextLine = `${ESC}E`

/**
 * Moves the cursor to the beginning of the previous line.
 *
 * @returns {string} The ANSI escape sequence for moving the cursor to the beginning of the previous line.
 */
ansiEscapes.cursorPrevLine = `${ESC}F`

/**
 * Hides the cursor.
 *
 * @returns {string} The ANSI escape sequence for hiding the cursor.
 */
ansiEscapes.cursorHide = `${ESC}?25l`

/**
 * Shows the cursor.
 *
 * @returns {string} The ANSI escape sequence for showing the cursor.
 */
ansiEscapes.cursorShow = `${ESC}?25h`

/**
 * Erases the specified number of lines.
 *
 * @param {number} count - The number of lines to erase.
 * @returns {string} The ANSI escape sequence for erasing the lines.
 */
ansiEscapes.eraseLines = (count) => {
  let clear = ''

  for (let i = 0; i < count; i++)
    clear += ansiEscapes.eraseLine + (i < count - 1 ? ansiEscapes.cursorUp() : '')

  if (count)
    clear += ansiEscapes.cursorLeft

  return clear
}

/**
 * Erases from the current cursor position to the end of the line.
 *
 * @returns {string} The ANSI escape sequence for erasing the end of the line.
 */
ansiEscapes.eraseEndLine = `${ESC}K`

/**
 * Erases from the beginning of the line to the current cursor position.
 *
 * @returns {string} The ANSI escape sequence for erasing the start of the line.
 */
ansiEscapes.eraseStartLine = `${ESC}1K`

/**
 * Erases the entire line.
 *
 * @returns {string} The ANSI escape sequence for erasing the line.
 */
ansiEscapes.eraseLine = `${ESC}2K`

/**
 * Erases from the current cursor position to the end of the screen.
 *
 * @returns {string} The ANSI escape sequence for erasing the end of the screen.
 */
ansiEscapes.eraseDown = `${ESC}J`

/**
 * Erases from the beginning of the screen to the current cursor position.
 *
 * @returns {string} The ANSI escape sequence for erasing the start of the screen.
 */
ansiEscapes.eraseUp = `${ESC}1J`

/**
 * Erases the entire screen.
 *
 * @returns {string} The ANSI escape sequence for erasing the screen.
 */
ansiEscapes.eraseScreen = `${ESC}2J`

/**
 * Scrolls the screen up by the specified number of lines.
 *
 * @param {number} count - The number of lines to scroll up.
 * @returns {string} The ANSI escape sequence for scrolling the screen up.
 */
ansiEscapes.scrollUp = `${ESC}S`

/**
 * Scrolls the screen down by the specified number of lines.
 *
 * @param {number} count - The number of lines to scroll down.
 * @returns {string} The ANSI escape sequence for scrolling the screen down.
 */
ansiEscapes.scrollDown = `${ESC}T`

/**
 * Clears the screen.
 *
 * @returns {string} The ANSI escape sequence for clearing the screen.
 */
ansiEscapes.clearScreen = '\u001Bc'

/**
 * Clears the terminal.
 *
 * @returns {string} The ANSI escape sequence for clearing the terminal.
 */
ansiEscapes.clearTerminal = isWindows
  ? `${ansiEscapes.eraseScreen}${ESC}0f`
  : `${ansiEscapes.eraseScreen}${ESC}3J${ESC}H`

/**
 * Enters the alternative screen buffer.
 *
 * @returns {string} The ANSI escape sequence for entering the alternative screen buffer.
 */
ansiEscapes.enterAlternativeScreen = `${ESC}?1049h`

/**
 * Exits the alternative screen buffer.
 *
 * @returns {string} The ANSI escape sequence for exiting the alternative screen buffer.
 */
ansiEscapes.exitAlternativeScreen = `${ESC}?1049l`

/**
 * Produces a beep sound.
 *
 * @returns {string} The ANSI escape sequence for producing a beep sound.
 */
ansiEscapes.beep = BEL

/**
 * Creates a hyperlink in the terminal.
 *
 * @param {string} text - The text to display as the hyperlink.
 * @param {string} url - The URL to open when the hyperlink is clicked.
 * @returns {string} The ANSI escape sequence for creating a hyperlink.
 */
ansiEscapes.link = (text, url) => [
  OSC,
  '8',
  SEP,
  SEP,
  url,
  BEL,
  text,
  OSC,
  '8',
  SEP,
  SEP,
  BEL,
].join('')

/**
 * Displays an image in the terminal.
 *
 * @param {Buffer} buffer - The image data as a Buffer.
 * @param {Object} [options={}] - The options for displaying the image.
 * @param {number} [options.width] - The width of the image in pixels.
 * @param {number} [options.height] - The height of the image in pixels.
 * @param {boolean} [options.preserveAspectRatio=true] - Whether to preserve the aspect ratio of the image.
 * @returns {string} The ANSI escape sequence for displaying the image.
 */
ansiEscapes.image = (buffer, options = {}) => {
  let returnValue = `${OSC}1337;File=inline=1`

  if (options.width)
    returnValue += `;width=${options.width}`

  if (options.height)
    returnValue += `;height=${options.height}`

  if (options.preserveAspectRatio === false)
    returnValue += ';preserveAspectRatio=0'

  return `${returnValue}:${buffer.toString('base64')}${BEL}`
}

// Export the ansiEscapes object
export default ansiEscapes

slice-ansi

仓库:slice-ansi

js
import ansiStyles from 'ansi-styles'
import isFullwidthCodePoint from 'is-fullwidth-code-point'

// \x1b and \x9b
const ESCAPES = new Set([27, 155])

const CODE_POINT_0 = '0'.codePointAt(0)
const CODE_POINT_9 = '9'.codePointAt(0)

// e.g [39m
const endCodesSet = new Set()

// e.g [31m => [39m
const endCodesMap = new Map()
for (const [start, end] of ansiStyles.codes) {
  endCodesSet.add(ansiStyles.color.ansi(end))
  endCodesMap.set(ansiStyles.color.ansi(start), ansiStyles.color.ansi(end))
}

function getEndCode(code) {
  if (endCodesSet.has(code))
    return code

  if (endCodesMap.has(code))
    return endCodesMap.get(code)

  code = code.slice(2)
  if (code.includes(';'))
    code = `${code[0]}0`

  const returnValue = ansiStyles.codes.get(Number.parseInt(code, 10))
  if (returnValue)
    return ansiStyles.color.ansi(returnValue)

  return ansiStyles.reset.open
}

function findNumberIndex(string) {
  for (let index = 0; index < string.length; index++) {
    const codePoint = string.codePointAt(index)
    if (codePoint >= CODE_POINT_0 && codePoint <= CODE_POINT_9)
      return index

  }

  return -1
}

function parseAnsiCode(string, offset) {
  string = string.slice(offset, offset + 19)
  const startIndex = findNumberIndex(string)
  if (startIndex !== -1) {
    let endIndex = string.indexOf('m', startIndex)
    if (endIndex === -1)
      endIndex = string.length

    return string.slice(0, endIndex + 1)
  }
}

function tokenize(string, endCharacter = Number.POSITIVE_INFINITY) {
  const returnValue = []

  let index = 0
  let visibleCount = 0
  while (index < string.length) {
    const codePoint = string.codePointAt(index)

    if (ESCAPES.has(codePoint)) {
      const code = parseAnsiCode(string, index)
      if (code) {
        returnValue.push({
          type: 'ansi',
          code,
          endCode: getEndCode(code),
        })
        index += code.length
        continue
      }
    }

    const isFullWidth = isFullwidthCodePoint(codePoint)
    const character = String.fromCodePoint(codePoint)

    returnValue.push({
      type: 'character',
      value: character,
      isFullWidth,
    })

    index += character.length
    visibleCount += isFullWidth ? 2 : character.length

    if (visibleCount >= endCharacter)
      break

  }

  return returnValue
}

function reduceAnsiCodes(codes) {
  let returnValue = []

  for (const code of codes) {
    if (code.code === ansiStyles.reset.open) {
      // Reset code, disable all codes
      returnValue = []
    }
    else if (endCodesSet.has(code.code)) {
      // This is an end code, disable all matching start codes
      returnValue = returnValue.filter(returnValueCode => returnValueCode.endCode !== code.code)
    }
    else {
      // This is a start code. Disable all styles this "overrides", then enable it
      returnValue = returnValue.filter(returnValueCode => returnValueCode.endCode !== code.endCode)
      returnValue.push(code)
    }
  }

  return returnValue
}

function undoAnsiCodes(codes) {
  const reduced = reduceAnsiCodes(codes)
  const endCodes = reduced.map(({ endCode }) => endCode)
  return endCodes.reverse().join('')
}

export default function sliceAnsi(string, start, end) {
  const tokens = tokenize(string, end)
  let activeCodes = []
  let position = 0
  let returnValue = ''
  let include = false

  for (const token of tokens) {
    if (end !== undefined && position >= end)
      break

    if (token.type === 'ansi') {
      activeCodes.push(token)
      if (include)
        returnValue += token.code

    }
    else {
      // Character
      if (!include && position >= start) {
        include = true
        // Simplify active codes
        activeCodes = reduceAnsiCodes(activeCodes)
        returnValue = activeCodes.map(({ code }) => code).join('')
      }

      if (include)
        returnValue += token.value

      position += token.isFullWidth ? 2 : token.value.length
    }
  }

  // Disable active codes at the end
  returnValue += undoAnsiCodes(activeCodes)
  return returnValue
}

ansi-regex / strip-ansi

js
export default function ansiRegex({ onlyFirst = false } = {}) {
  const pattern = [
    '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
    '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))'
  ].join('|')

  return new RegExp(pattern, onlyFirst ? undefined : 'g')
}

ansi-styles

仓库地址:ansi-styles

js
const ANSI_BACKGROUND_OFFSET = 10

const wrapAnsi16 = (offset = 0) => code => `\u001B[${code + offset}m`

const wrapAnsi256 = (offset = 0) => code => `\u001B[${38 + offset};5;${code}m`

const wrapAnsi16m = (offset = 0) => (red, green, blue) => `\u001B[${38 + offset};2;${red};${green};${blue}m`

const styles = {
  modifier: {
    reset: [0, 0],
    // 21 isn't widely supported and 22 does the same thing
    bold: [1, 22],
    dim: [2, 22],
    italic: [3, 23],
    underline: [4, 24],
    overline: [53, 55],
    inverse: [7, 27],
    hidden: [8, 28],
    strikethrough: [9, 29],
  },
  color: {
    black: [30, 39],
    red: [31, 39],
    green: [32, 39],
    yellow: [33, 39],
    blue: [34, 39],
    magenta: [35, 39],
    cyan: [36, 39],
    white: [37, 39],

    // Bright color
    blackBright: [90, 39],
    gray: [90, 39], // Alias of `blackBright`
    grey: [90, 39], // Alias of `blackBright`
    redBright: [91, 39],
    greenBright: [92, 39],
    yellowBright: [93, 39],
    blueBright: [94, 39],
    magentaBright: [95, 39],
    cyanBright: [96, 39],
    whiteBright: [97, 39],
  },
  bgColor: {
    bgBlack: [40, 49],
    bgRed: [41, 49],
    bgGreen: [42, 49],
    bgYellow: [43, 49],
    bgBlue: [44, 49],
    bgMagenta: [45, 49],
    bgCyan: [46, 49],
    bgWhite: [47, 49],

    // Bright color
    bgBlackBright: [100, 49],
    bgGray: [100, 49], // Alias of `bgBlackBright`
    bgGrey: [100, 49], // Alias of `bgBlackBright`
    bgRedBright: [101, 49],
    bgGreenBright: [102, 49],
    bgYellowBright: [103, 49],
    bgBlueBright: [104, 49],
    bgMagentaBright: [105, 49],
    bgCyanBright: [106, 49],
    bgWhiteBright: [107, 49],
  },
}

export const modifierNames = Object.keys(styles.modifier)
export const foregroundColorNames = Object.keys(styles.color)
export const backgroundColorNames = Object.keys(styles.bgColor)
export const colorNames = [...foregroundColorNames, ...backgroundColorNames]

function assembleStyles() {
  const codes = new Map()

  for (const [groupName, group] of Object.entries(styles)) {
    for (const [styleName, style] of Object.entries(group)) {
      styles[styleName] = {
        open: `\u001B[${style[0]}m`,
        close: `\u001B[${style[1]}m`,
      }

      group[styleName] = styles[styleName]

      codes.set(style[0], style[1])
    }

    Object.defineProperty(styles, groupName, {
      value: group,
      enumerable: false,
    })
  }

  Object.defineProperty(styles, 'codes', {
    value: codes,
    enumerable: false,
  })

  styles.color.close = '\u001B[39m'
  styles.bgColor.close = '\u001B[49m'

  styles.color.ansi = wrapAnsi16()
  styles.color.ansi256 = wrapAnsi256()
  styles.color.ansi16m = wrapAnsi16m()
  styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET)
  styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET)
  styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET)

  // From https://github.com/Qix-/color-convert/blob/3f0e0d4e92e235796ccb17f6e85c72094a651f49/conversions.js
  Object.defineProperties(styles, {
    rgbToAnsi256: {
      value: (red, green, blue) => {
        // We use the extended greyscale palette here, with the exception of
        // black and white. normal palette only has 4 greyscale shades.
        if (red === green && green === blue) {
          if (red < 8)
            return 16

          if (red > 248)
            return 231

          return Math.round(((red - 8) / 247) * 24) + 232
        }

        return 16
        + (36 * Math.round(red / 255 * 5))
        + (6 * Math.round(green / 255 * 5))
        + Math.round(blue / 255 * 5)
      },
      enumerable: false,
    },
    hexToRgb: {
      value: (hex) => {
        const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16))
        if (!matches)
          return [0, 0, 0]

        let [colorString] = matches

        if (colorString.length === 3)
          colorString = [...colorString].map(character => character + character).join('')

        const integer = Number.parseInt(colorString, 16)

        return [

          (integer >> 16) & 0xFF,
          (integer >> 8) & 0xFF,
          integer & 0xFF,
        ]
      },
      enumerable: false,
    },
    hexToAnsi256: {
      value: hex => styles.rgbToAnsi256(...styles.hexToRgb(hex)),
      enumerable: false,
    },
    ansi256ToAnsi: {
      value: (code) => {
        if (code < 8)
          return 30 + code

        if (code < 16)
          return 90 + (code - 8)

        let red
        let green
        let blue

        if (code >= 232) {
          red = (((code - 232) * 10) + 8) / 255
          green = red
          blue = red
        }
        else {
          code -= 16

          const remainder = code % 36

          red = Math.floor(code / 36) / 5
          green = Math.floor(remainder / 6) / 5
          blue = (remainder % 6) / 5
        }

        const value = Math.max(red, green, blue) * 2

        if (value === 0)
          return 30

        let result = 30 + ((Math.round(blue) << 2) | (Math.round(green) << 1) | Math.round(red))

        if (value === 2)
          result += 60

        return result
      },
      enumerable: false,
    },
    rgbToAnsi: {
      value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)),
      enumerable: false,
    },
    hexToAnsi: {
      value: hex => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)),
      enumerable: false,
    },
  })

  return styles
}

const ansiStyles = assembleStyles()

export default ansiStyles

更定制化的颜色输出

js
import styles from 'ansi-styles'

styles.color.ansi(styles.rgbToAnsi(100, 200, 15)) // RGB to 16 color ansi foreground code
styles.bgColor.ansi(styles.hexToAnsi('#C0FFEE')) // HEX to 16 color ansi foreground code

styles.color.ansi256(styles.rgbToAnsi256(100, 200, 15)) // RGB to 256 color ansi foreground code
styles.bgColor.ansi256(styles.hexToAnsi256('#C0FFEE')) // HEX to 256 color ansi foreground code

styles.color.ansi16m(100, 200, 15) // RGB to 16 million color foreground code
styles.bgColor.ansi16m(...styles.hexToRgb('#C0FFEE')) // Hex (RGB) to 16 million color foreground code

log-update

仓库地址:log-update

js
// 导入所需的模块
import process from 'node:process'

// 用于处理系统和环境相关的信息
import ansiEscapes from 'ansi-escapes'

// 用于生成ANSI转义码
import cliCursor from 'cli-cursor'

// 用于控制命令行中的光标显示/隐藏
import wrapAnsi from 'wrap-ansi'

// 用于在指定宽度处换行ANSI字符串
import sliceAnsi from 'slice-ansi'

// 用于切割ANSI字符串
import stripAnsi from 'strip-ansi' // 用于移除ANSI字符串中的ANSI转义码

// 默认的终端高度
const defaultTerminalHeight = 24

// 获取终端的宽度,默认为80
const getWidth = ({ columns = 80 }) => columns

// 将文本适应到终端的高度,截掉多余的无法展示的部分
function fitToTerminalHeight(stream, text) {
  const terminalHeight = stream.rows ?? defaultTerminalHeight
  const lines = text.split('\n')
  const toRemove = Math.max(0, lines.length - terminalHeight)
  return toRemove ? sliceAnsi(text, stripAnsi(lines.slice(0, toRemove).join('\n')).length + 1) : text
}

// 创建一个更新日志的函数
export function createLogUpdate(stream, { showCursor = false } = {}) {
  let previousLineCount = 0
  let previousWidth = getWidth(stream)
  let previousOutput = ''

  // 重置函数
  const reset = () => {
    previousOutput = ''
    previousWidth = getWidth(stream)
    previousLineCount = 0
  }

  // 渲染函数
  const render = (...arguments_) => {
    if (!showCursor)
      cliCursor.hide()

    let output = fitToTerminalHeight(stream, `${arguments_.join(' ')}\n`)
    const width = getWidth(stream)

    if (output === previousOutput && previousWidth === width)
      return

    previousOutput = output
    previousWidth = width
    output = wrapAnsi(output, width, { trim: false, hard: true, wordWrap: false })

    // 清除之前的行数,然后输出新的内容
    stream.write(ansiEscapes.eraseLines(previousLineCount) + output)
    previousLineCount = output.split('\n').length
  }

  // 清除函数
  render.clear = () => {
    stream.write(ansiEscapes.eraseLines(previousLineCount))
    reset()
  }

  // 完成函数
  render.done = () => {
    reset()
    if (!showCursor)
      cliCursor.show()

  }

  return render
}

// 创建一个更新stdout的日志函数
const logUpdate = createLogUpdate(process.stdout)
export default logUpdate

// 创建一个更新stderr的日志函数
export const logUpdateStderr = createLogUpdate(process.stderr)

Made with ❤️