├── .npmrc ├── .eslintignore ├── docs └── assets │ ├── images │ ├── icons.png │ ├── icons@2x.png │ ├── widgets.png │ └── widgets@2x.png │ └── js │ ├── search.json │ └── main.js ├── examples ├── write.ts ├── viewport.ts ├── position.ts ├── colors.ts ├── display.ts ├── erase.ts ├── lucky.ts ├── spin.ts ├── smart-render.ts ├── curl.ts └── video.ts ├── src ├── canvas │ ├── CanvasOptions.ts │ └── Canvas.ts ├── cell │ ├── DisplayOptions.ts │ ├── CellOptions.ts │ ├── DisplayModes.ts │ └── Cell.ts ├── color │ ├── HEXRegex.ts │ ├── RGBRegex.ts │ ├── Color.ts │ └── NamedColors.ts └── encodeToVT100.ts ├── tsconfig.eslint.json ├── typedoc.json ├── .editorconfig ├── .travis.yml ├── jest.config.js ├── .vscode ├── settings.json └── launch.json ├── tsconfig.json ├── LICENSE ├── .eslintrc.js ├── package.json ├── .gitignore ├── README.md └── test ├── Color.spec.ts ├── Cell.spec.ts └── Canvas.spec.ts /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | docs 4 | node_modules 5 | -------------------------------------------------------------------------------- /docs/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghaiklor/terminal-canvas/HEAD/docs/assets/images/icons.png -------------------------------------------------------------------------------- /docs/assets/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghaiklor/terminal-canvas/HEAD/docs/assets/images/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/images/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghaiklor/terminal-canvas/HEAD/docs/assets/images/widgets.png -------------------------------------------------------------------------------- /docs/assets/images/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghaiklor/terminal-canvas/HEAD/docs/assets/images/widgets@2x.png -------------------------------------------------------------------------------- /examples/write.ts: -------------------------------------------------------------------------------- 1 | import { Canvas } from '..'; 2 | 3 | const canvas = Canvas.create().reset(); 4 | 5 | canvas.write('HELLO').flush(); 6 | -------------------------------------------------------------------------------- /src/canvas/CanvasOptions.ts: -------------------------------------------------------------------------------- 1 | import { WriteStream } from 'tty'; 2 | 3 | export interface ICanvasOptions { 4 | stream: WriteStream 5 | width: number 6 | height: number 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "include": [ 7 | "**/*.*" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "includeVersion": true, 3 | "mode": "file", 4 | "name": "terminal-canvas", 5 | "out": "docs", 6 | "readme": "README.md", 7 | "theme": "minimal" 8 | } 9 | -------------------------------------------------------------------------------- /src/cell/DisplayOptions.ts: -------------------------------------------------------------------------------- 1 | export interface IDisplayOptions { 2 | bold: boolean 3 | dim: boolean 4 | underlined: boolean 5 | blink: boolean 6 | reverse: boolean 7 | hidden: boolean 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/color/HEXRegex.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Regular expression for capturing HEX channels. 3 | * E.g. "#FFFFFF" 4 | */ 5 | export const HEX_REGEX = /#(?[0-9A-F]{2})(?[0-9A-F]{2})(?[0-9A-F]{2})/iu; 6 | -------------------------------------------------------------------------------- /src/color/RGBRegex.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Regular expression for capturing RGB channels. 3 | * E.g. "rgb(0, 0, 0)" 4 | */ 5 | export const RGB_REGEX = /rgb\((?\d{1,3}), (?\d{1,3}), (?\d{1,3})\)/iu; 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - lts/* 5 | addons: 6 | apt: 7 | packages: 8 | - libasound2-dev 9 | script: 10 | - npm run all 11 | after_success: 12 | - bash <(curl -s https://codecov.io/bash) 13 | -------------------------------------------------------------------------------- /examples/viewport.ts: -------------------------------------------------------------------------------- 1 | import { Canvas } from '..'; 2 | 3 | const canvas = Canvas.create({ width: 5 }).reset(); 4 | 5 | // It will print only 34567 because you have negative X coordinate and width equal to 5 6 | canvas.moveTo(-2, 1).write('1234567890').flush(); 7 | -------------------------------------------------------------------------------- /src/cell/CellOptions.ts: -------------------------------------------------------------------------------- 1 | import { IColor } from '../color/Color'; 2 | import { IDisplayOptions } from './DisplayOptions'; 3 | 4 | export interface ICellOptions { 5 | x: number 6 | y: number 7 | background: IColor 8 | foreground: IColor 9 | display: Partial 10 | } 11 | -------------------------------------------------------------------------------- /src/encodeToVT100.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts string with control code to VT100 control sequence. 3 | * 4 | * @param {String} code Control code that you want to encode 5 | * @returns {String} Returns VT100 control sequence 6 | */ 7 | export function encodeToVT100 (code: string): string { 8 | return `\u001b${code}`; 9 | } 10 | -------------------------------------------------------------------------------- /src/cell/DisplayModes.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable sort-keys */ 2 | 3 | export const DISPLAY_MODES = { 4 | RESET_ALL: 0, 5 | BOLD: 1, 6 | DIM: 2, 7 | UNDERLINED: 4, 8 | BLINK: 5, 9 | REVERSE: 7, 10 | HIDDEN: 8, 11 | RESET_BOLD: 21, 12 | RESET_DIM: 22, 13 | RESET_UNDERLINED: 24, 14 | RESET_BLINK: 25, 15 | RESET_REVERSE: 27, 16 | RESET_HIDDEN: 28, 17 | }; 18 | -------------------------------------------------------------------------------- /examples/position.ts: -------------------------------------------------------------------------------- 1 | import { Canvas } from '..'; 2 | 3 | const canvas = Canvas.create().reset(); 4 | 5 | canvas 6 | .moveTo(15, 5) 7 | .write('moveTo') 8 | .moveBy(15, 5) 9 | .write('moveBy') 10 | .down(5) 11 | .write('down') 12 | .up(10) 13 | .write('up') 14 | .left(20) 15 | .write('left') 16 | .right(40) 17 | .write('right') 18 | .down(10) 19 | .flush(); 20 | -------------------------------------------------------------------------------- /examples/colors.ts: -------------------------------------------------------------------------------- 1 | import { Canvas } from '..'; 2 | import { NAMED_COLORS } from '../src/color/NamedColors'; 3 | 4 | const canvas = Canvas.create().reset(); 5 | const COLORS = Array.from(NAMED_COLORS.keys()); 6 | 7 | for (let y = 0; y < process.stdout.rows; y += 1) { 8 | for (let x = 0; x < process.stdout.columns; x += 1) { 9 | canvas.moveTo(x, y).background(COLORS[(y + x) % (COLORS.length - 1)]).write(' '); 10 | } 11 | } 12 | 13 | canvas.flush(); 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | collectCoverage: true, 4 | coverageDirectory: 'coverage', 5 | coverageProvider: 'v8', 6 | coverageReporters: ['json', 'text', 'lcov', 'clover'], 7 | errorOnDeprecated: true, 8 | maxWorkers: '80%', 9 | preset: 'ts-jest', 10 | resetMocks: true, 11 | restoreMocks: true, 12 | testEnvironment: 'node', 13 | testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'], 14 | verbose: true, 15 | }; 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Astley", 4 | "BORO", 5 | "Codecov", 6 | "MAUVELOUS", 7 | "Obrezkov", 8 | "RAZZLE", 9 | "REBECCA", 10 | "Touhou", 11 | "WUZZY", 12 | "kittik", 13 | "lcov", 14 | "paren", 15 | "performant", 16 | "postversion", 17 | "preversion", 18 | "rawvideo", 19 | "readonly", 20 | "tsbuildinfo", 21 | "typedoc", 22 | "webm", 23 | "youtube", 24 | "ytdl" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/display.ts: -------------------------------------------------------------------------------- 1 | import { Canvas } from '..'; 2 | 3 | const canvas = Canvas.create().reset(); 4 | 5 | canvas 6 | .bold() 7 | .write('BOLD') 8 | .bold(false) 9 | .moveBy(-4, 1) 10 | .dim() 11 | .write('DIM') 12 | .dim(false) 13 | .moveBy(-3, 1) 14 | .underlined() 15 | .write('UNDERLINED') 16 | .underlined(false) 17 | .moveBy(-10, 1) 18 | .blink() 19 | .write('BLINK') 20 | .blink(false) 21 | .moveBy(-5, 1) 22 | .reverse() 23 | .write('REVERSE') 24 | .reverse(false) 25 | .moveBy(-7, 1) 26 | .hidden() 27 | .write('HIDDEN') 28 | .hidden(false) 29 | .flush(); 30 | -------------------------------------------------------------------------------- /examples/erase.ts: -------------------------------------------------------------------------------- 1 | import { Canvas } from '..'; 2 | 3 | const canvas = Canvas.create().reset().hideCursor(); 4 | 5 | for (let y = 0; y < process.stdout.rows; y += 1) { 6 | canvas.moveTo(0, y).write('E'.repeat(process.stdout.columns)); 7 | } 8 | 9 | canvas.moveTo(process.stdout.columns / 2, process.stdout.rows / 2).flush(); 10 | 11 | setTimeout(() => canvas.eraseToStart().flush(), 1000); 12 | setTimeout(() => canvas.eraseToEnd().flush(), 2000); 13 | setTimeout(() => canvas.eraseToUp().flush(), 3000); 14 | setTimeout(() => canvas.eraseToDown().flush(), 4000); 15 | setTimeout(() => canvas.eraseScreen().flush().showCursor(), 5000); 16 | -------------------------------------------------------------------------------- /examples/lucky.ts: -------------------------------------------------------------------------------- 1 | import { Canvas } from '..'; 2 | 3 | const canvas = Canvas.create().reset(); 4 | const colors = ['red', 'cyan', 'yellow', 'green', 'blue']; 5 | const text = 'Always after me lucky charms.'; 6 | 7 | let offset = 0; 8 | 9 | setInterval(() => { 10 | let y = 0; 11 | let dy = 1; 12 | 13 | for (let i = 0; i < 40; i += 1) { 14 | const color = colors[(i + offset) % colors.length]; 15 | const char = text[(i + offset) % text.length]; 16 | 17 | canvas.moveBy(1, dy).foreground(color).write(char); 18 | 19 | y += dy; 20 | 21 | if (y <= 0 || y >= 5) dy *= -1; 22 | } 23 | 24 | canvas.moveTo(0, 0).flush(); 25 | offset += 1; 26 | }, 150); 27 | -------------------------------------------------------------------------------- /examples/spin.ts: -------------------------------------------------------------------------------- 1 | import { Canvas } from '..'; 2 | 3 | const canvas = Canvas.create().reset(); 4 | const radius = 10; 5 | const colors = ['red', 'yellow', 'green', 'dark_cyan', 'blue', 'magenta']; 6 | 7 | let points: Array<[number, number]> = []; 8 | let theta = 0; 9 | 10 | setInterval(() => { 11 | const x = 2 + (radius + Math.cos(theta) * radius) * 2; 12 | const y = 2 + radius + Math.sin(theta) * radius; 13 | 14 | points.unshift([x, y]); 15 | points.forEach((point, i) => { 16 | canvas.moveTo(point[0], point[1]); 17 | canvas.background(colors[Math.floor(i / 12)]).write(' ').flush(); 18 | }); 19 | 20 | points = points.slice(0, 12 * colors.length - 1); 21 | theta += Math.PI / 40; 22 | }, 30); 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "declaration": true, 5 | "declarationMap": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "incremental": true, 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitAny": true, 13 | "noImplicitReturns": true, 14 | "noImplicitThis": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "outDir": "dist", 18 | "removeComments": true, 19 | "rootDir": "src", 20 | "sourceMap": true, 21 | "strict": true, 22 | "strictBindCallApply": true, 23 | "strictFunctionTypes": true, 24 | "strictNullChecks": true, 25 | "strictPropertyInitialization": true, 26 | "target": "ES2019", 27 | }, 28 | "include": [ 29 | "src/**/*.*" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /examples/smart-render.ts: -------------------------------------------------------------------------------- 1 | import { Canvas } from '..'; 2 | 3 | const canvas = Canvas.create().reset(); 4 | 5 | let x = 0; 6 | let y = 0; 7 | 8 | setInterval(() => { 9 | canvas 10 | .moveTo(1, 0) 11 | .background('white') 12 | .foreground('black') 13 | .write('This example shows how fast builds difference between two frames'); 14 | 15 | canvas 16 | .moveTo(x, y) 17 | .background('yellow') 18 | .foreground('black') 19 | .write('ABCDEFGHIJKLMNOP'); 20 | 21 | canvas 22 | .moveTo(x * 2, y) 23 | .background('yellow') 24 | .foreground('black') 25 | .write('ABCDEFGHIJKLMNOP'); 26 | 27 | canvas 28 | .moveTo(x * 3, y) 29 | .background('yellow') 30 | .foreground('black') 31 | .write('ABCDEFGHIJKLMNOP'); 32 | 33 | canvas.flush(); 34 | canvas.erase(x, y, x + 16, y); 35 | canvas.erase(x * 2, y, x * 2 + 16, y); 36 | canvas.erase(x * 3, y, x * 3 + 16, y); 37 | 38 | x = x > process.stdout.columns ? 0 : x + 1; 39 | y = y > process.stdout.rows ? 0 : y + 1; 40 | }, 40); 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-2020 Eugene Obrezkov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the \"Software\"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest All", 11 | "program": "${workspaceFolder}/node_modules/.bin/jest", 12 | "args": [ 13 | "--runInBand", 14 | "--coverage", 15 | "false" 16 | ], 17 | "console": "integratedTerminal", 18 | "internalConsoleOptions": "neverOpen", 19 | "disableOptimisticBPs": true, 20 | "windows": { 21 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 22 | } 23 | }, 24 | { 25 | "type": "node", 26 | "request": "launch", 27 | "name": "Jest Current File", 28 | "program": "${workspaceFolder}/node_modules/.bin/jest", 29 | "args": [ 30 | "${fileBasenameNoExtension}", 31 | "--config", 32 | "jest.config.js", 33 | "--coverage", 34 | "false" 35 | ], 36 | "console": "integratedTerminal", 37 | "internalConsoleOptions": "neverOpen", 38 | "disableOptimisticBPs": true, 39 | "windows": { 40 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 41 | } 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true, 5 | }, 6 | extends: [ 7 | 'eslint:all', 8 | 'plugin:@typescript-eslint/all', 9 | 'plugin:import/typescript', 10 | 'plugin:jest/all', 11 | 'plugin:node/recommended', 12 | 'plugin:promise/recommended', 13 | 'standard-with-typescript', 14 | ], 15 | globals: { 16 | Atomics: 'readonly', // eslint-disable-line @typescript-eslint/naming-convention 17 | SharedArrayBuffer: 'readonly', // eslint-disable-line @typescript-eslint/naming-convention 18 | }, 19 | parser: '@typescript-eslint/parser', 20 | parserOptions: { 21 | ecmaVersion: 2019, 22 | sourceType: 'module', 23 | project: 'tsconfig.eslint.json', 24 | }, 25 | plugins: [ 26 | '@typescript-eslint', 27 | 'import', 28 | 'jest', 29 | 'node', 30 | 'promise', 31 | 'standard', 32 | ], 33 | rules: { 34 | '@typescript-eslint/indent': ['error', 2], 35 | '@typescript-eslint/no-magic-numbers': ['off'], 36 | '@typescript-eslint/prefer-readonly-parameter-types': ['off'], 37 | '@typescript-eslint/quotes': ['error', 'single'], 38 | '@typescript-eslint/semi': ['error', 'always'], 39 | '@typescript-eslint/typedef': ['off'], 40 | 'array-bracket-newline': ['error', 'consistent'], 41 | 'array-element-newline': ['error', 'consistent'], 42 | 'comma-dangle': ['error', 'always-multiline'], 43 | 'func-style': ['off'], 44 | 'function-call-argument-newline': ['error', 'consistent'], 45 | 'function-paren-newline': ['error', 'consistent'], 46 | 'id-length': ['error', { exceptions: ['r', 'g', 'b', 'x', 'y', '_', 'i'] }], 47 | indent: ['error', 2], 48 | 'max-len': ['error', 120], 49 | 'max-lines': ['off'], 50 | 'max-lines-per-function': ['off'], 51 | 'max-params': ['error', { max: 4 }], 52 | 'max-statements': ['off'], 53 | 'multiline-ternary': ['error', 'always-multiline'], 54 | 'newline-per-chained-call': ['error', { ignoreChainWithDepth: 3 }], 55 | 'no-constructor-return': ['off'], 56 | 'no-ternary': ['off'], 57 | 'node/no-missing-import': ['error', { tryExtensions: ['.js', '.ts', '.json', '.node'] }], 58 | 'node/no-unsupported-features/es-syntax': ['error', { ignores: ['modules'] }], 59 | quotes: ['error', 'single'], 60 | semi: ['error', 'always'], 61 | 'sort-keys': ['error', 'asc', { minKeys: 5, natural: true }], 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "terminal-canvas", 3 | "version": "3.1.2", 4 | "description": "Manipulate the cursor in your terminal via high-performant, low-level, canvas-like API", 5 | "main": "dist/canvas/Canvas.js", 6 | "types": "dist/canvas/Canvas.d.ts", 7 | "license": "MIT", 8 | "homepage": "https://github.com/ghaiklor/terminal-canvas#readme", 9 | "keywords": [ 10 | "terminal", 11 | "shell", 12 | "cursor", 13 | "canvas", 14 | "renderer", 15 | "render" 16 | ], 17 | "files": [ 18 | "dist" 19 | ], 20 | "directories": { 21 | "doc": "docs", 22 | "example": "examples", 23 | "lib": "dist", 24 | "test": "test" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/ghaiklor/terminal-canvas/issues", 28 | "email": "ghaiklor@gmail.com" 29 | }, 30 | "author": { 31 | "name": "Eugene Obrezkov", 32 | "email": "ghaiklor@gmail.com", 33 | "url": "https://ghaiklor.com" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/ghaiklor/terminal-canvas.git" 38 | }, 39 | "engines": { 40 | "node": ">=12.0.0" 41 | }, 42 | "scripts": { 43 | "all": "npm run clean && npm run build && npm run test && npm run lint", 44 | "build": "tsc", 45 | "clean": "rimraf coverage dist tsconfig.tsbuildinfo", 46 | "commit": "git cz", 47 | "docs": "typedoc src", 48 | "lint": "eslint --fix --ext .js,.ts .", 49 | "postversion": "git push && git push --tags", 50 | "prepare": "npm run build", 51 | "prepublishOnly": "npm run all", 52 | "preversion": "npm run all", 53 | "test": "jest", 54 | "version": "npm run docs && git add ." 55 | }, 56 | "devDependencies": { 57 | "@types/fluent-ffmpeg": "2.1.14", 58 | "@types/jest": "26.0.5", 59 | "@types/node": "14.0.24", 60 | "@types/stream-throttle": "0.1.0", 61 | "@typescript-eslint/eslint-plugin": "3.7.0", 62 | "@typescript-eslint/parser": "3.7.0", 63 | "chop-stream": "0.0.2", 64 | "eslint": "7.5.0", 65 | "eslint-config-standard": "14.1.1", 66 | "eslint-config-standard-with-typescript": "18.0.2", 67 | "eslint-plugin-import": "2.22.0", 68 | "eslint-plugin-jest": "23.18.0", 69 | "eslint-plugin-node": "11.1.0", 70 | "eslint-plugin-promise": "4.2.1", 71 | "eslint-plugin-standard": "4.0.1", 72 | "fluent-ffmpeg": "2.1.2", 73 | "git-cz": "4.7.0", 74 | "jest": "29.7.0", 75 | "rimraf": "3.0.2", 76 | "speaker": "0.5.2", 77 | "stream-throttle": "0.1.3", 78 | "ts-jest": "26.1.3", 79 | "typedoc": "0.23.21", 80 | "typescript": "3.9.7", 81 | "ytdl-core": "3.1.2" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # Icon must end with two \r 7 | Icon 8 | 9 | # Thumbnails 10 | ._* 11 | 12 | # Files that might appear in the root of a volume 13 | .DocumentRevisions-V100 14 | .fseventsd 15 | .Spotlight-V100 16 | .TemporaryItems 17 | .Trashes 18 | .VolumeIcon.icns 19 | .com.apple.timemachine.donotpresent 20 | 21 | # Directories potentially created on remote AFP share 22 | .AppleDB 23 | .AppleDesktop 24 | Network Trash Folder 25 | Temporary Items 26 | .apdisk 27 | 28 | # Logs 29 | logs 30 | *.log 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | lerna-debug.log* 35 | 36 | # Diagnostic reports (https://nodejs.org/api/report.html) 37 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 38 | 39 | # Runtime data 40 | pids 41 | *.pid 42 | *.seed 43 | *.pid.lock 44 | 45 | # Directory for instrumented libs generated by jscoverage/JSCover 46 | lib-cov 47 | 48 | # Coverage directory used by tools like istanbul 49 | coverage 50 | *.lcov 51 | 52 | # nyc test coverage 53 | .nyc_output 54 | 55 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 56 | .grunt 57 | 58 | # Bower dependency directory (https://bower.io/) 59 | bower_components 60 | 61 | # node-waf configuration 62 | .lock-wscript 63 | 64 | # Compiled binary addons (https://nodejs.org/api/addons.html) 65 | build/Release 66 | 67 | # Dependency directories 68 | node_modules/ 69 | jspm_packages/ 70 | 71 | # TypeScript v1 declaration files 72 | typings/ 73 | 74 | # TypeScript cache 75 | *.tsbuildinfo 76 | 77 | # Optional npm cache directory 78 | .npm 79 | 80 | # Optional eslint cache 81 | .eslintcache 82 | 83 | # Microbundle cache 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | .node_repl_history 91 | 92 | # Output of 'npm pack' 93 | *.tgz 94 | 95 | # Yarn Integrity file 96 | .yarn-integrity 97 | 98 | # dotenv environment variables file 99 | .env 100 | .env.test 101 | 102 | # parcel-bundler cache (https://parceljs.org/) 103 | .cache 104 | 105 | # Next.js build output 106 | .next 107 | 108 | # Nuxt.js build / generate output 109 | .nuxt 110 | dist 111 | 112 | # Gatsby files 113 | .cache/ 114 | 115 | # vuepress build output 116 | .vuepress/dist 117 | 118 | # Serverless directories 119 | .serverless/ 120 | 121 | # FuseBox cache 122 | .fusebox/ 123 | 124 | # DynamoDB Local files 125 | .dynamodb/ 126 | 127 | # TernJS port file 128 | .tern-port 129 | 130 | # Stores VSCode versions used for testing VSCode extensions 131 | .vscode-test 132 | 133 | # VisualStudioCode 134 | .vscode/* 135 | !.vscode/settings.json 136 | !.vscode/tasks.json 137 | !.vscode/launch.json 138 | !.vscode/extensions.json 139 | *.code-workspace 140 | 141 | # Ignore all local history of files 142 | .history 143 | -------------------------------------------------------------------------------- /examples/curl.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | /* eslint-disable @typescript-eslint/no-misused-promises */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 4 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 5 | /* eslint-disable @typescript-eslint/prefer-ts-expect-error */ 6 | 7 | import { Canvas } from '..'; 8 | // @ts-ignore 9 | import ChopStream from 'chop-stream'; 10 | import { Throttle } from 'stream-throttle'; 11 | import { createServer } from 'http'; 12 | import ffmpeg from 'fluent-ffmpeg'; 13 | import ytdl from 'ytdl-core'; 14 | 15 | const PORT = process.env.PORT ?? 8080; 16 | const CHARACTERS = ' .,:;i1tfLCG08@'.split(''); 17 | 18 | createServer(async (request, response) => { 19 | const url = (request.headers['x-youtube-url'] ?? 'https://www.youtube.com/watch?v=Hiqn1Ur32AE') as string; 20 | const useColors = (request.headers['x-use-colors'] ?? 'false') === 'true'; 21 | const width = parseInt((request.headers['x-viewport-width'] ?? '100') as string, 10); 22 | const height = parseInt((request.headers['x-viewport-height'] ?? '100') as string, 10); 23 | 24 | // @ts-ignore 25 | const canvas = Canvas.create({ stream: response, height, width }).reset(); 26 | const info = await ytdl.getInfo(url); 27 | 28 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 29 | const video = info.formats.find( 30 | (format) => format.quality === 'tiny' && format.container === 'webm' && typeof format.audioChannels === 'undefined', 31 | )!; 32 | 33 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 34 | const videoSize = { width: video.width!, height: video.height! }!; 35 | const scale = Math.min(canvas.width / videoSize.width, canvas.height / videoSize.height); 36 | const frameWidth = Math.floor(videoSize.width * scale); 37 | const frameHeight = Math.floor(videoSize.height * scale); 38 | const frameSize = frameWidth * frameHeight * 3; 39 | 40 | ffmpeg(video.url) 41 | .format('rawvideo') 42 | .videoFilters([ 43 | { filter: 'fps', options: 30 }, 44 | { filter: 'scale', options: `${frameWidth}:${frameHeight}` }, 45 | ]) 46 | .outputOptions('-pix_fmt', 'rgb24') 47 | .outputOptions('-update', '1') 48 | .on('start', () => canvas.saveScreen().reset()) 49 | .on('end', () => canvas.restoreScreen()) 50 | .pipe(new Throttle({ rate: frameSize * 30 })) 51 | .pipe(new ChopStream(frameSize)) 52 | .on('data', (frameData: number[]) => { 53 | if (useColors) { 54 | for (let y = 0; y < frameHeight; y += 1) { 55 | for (let x = 0; x < frameWidth; x += 1) { 56 | const offset = (y * frameWidth + x) * 3; 57 | const r = frameData[offset]; 58 | const g = frameData[offset + 1]; 59 | const b = frameData[offset + 2]; 60 | 61 | canvas 62 | .moveTo(x + (canvas.width / 2 - frameWidth / 2), y + (canvas.height / 2 - frameHeight / 2)) 63 | .background(`rgb(${r}, ${g}, ${b})`) 64 | .write(' '); 65 | } 66 | } 67 | } else { 68 | const contrastFactor = 2.95; 69 | 70 | for (let y = 0; y < frameHeight; y += 1) { 71 | for (let x = 0; x < frameWidth; x += 1) { 72 | const offset = (y * frameWidth + x) * 3; 73 | const r = Math.max(0, Math.min(contrastFactor * (frameData[offset] - 128) + 128, 255)); 74 | const g = Math.max(0, Math.min(contrastFactor * (frameData[offset + 1] - 128) + 128, 255)); 75 | const b = Math.max(0, Math.min(contrastFactor * (frameData[offset + 2] - 128) + 128, 255)); 76 | const brightness = 1 - (0.299 * r + 0.587 * g + 0.114 * b) / 255; 77 | 78 | canvas 79 | .moveTo(x + (canvas.width / 2 - frameWidth / 2), y + (canvas.height / 2 - frameHeight / 2)) 80 | .write(CHARACTERS[Math.round(brightness * 14)]); 81 | } 82 | } 83 | } 84 | 85 | canvas.flush(); 86 | }); 87 | }).listen(PORT); 88 | -------------------------------------------------------------------------------- /examples/video.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 4 | /* eslint-disable @typescript-eslint/prefer-ts-expect-error */ 5 | 6 | import ytdl, { videoInfo } from 'ytdl-core'; 7 | import { Canvas } from '..'; 8 | // @ts-ignore 9 | import ChopStream from 'chop-stream'; 10 | import Speaker from 'speaker'; 11 | import { Throttle } from 'stream-throttle'; 12 | import ffmpeg from 'fluent-ffmpeg'; 13 | 14 | const YOUTUBE_URL = process.env.YOUTUBE_URL ?? 'https://www.youtube.com/watch?v=Hiqn1Ur32AE'; 15 | const CHARACTERS = ' .,:;i1tfLCG08@'.split(''); 16 | const canvas = Canvas.create().reset(); 17 | 18 | function playVideo (info: videoInfo): void { 19 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 20 | const video = info.formats.find( 21 | (format) => format.quality === 'tiny' && format.container === 'webm' && typeof format.audioChannels === 'undefined', 22 | )!; 23 | 24 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 25 | const videoSize = { width: video.width!, height: video.height! }!; 26 | const scale = Math.min(canvas.width / videoSize.width, canvas.height / videoSize.height); 27 | const frameWidth = Math.floor(videoSize.width * scale); 28 | const frameHeight = Math.floor(videoSize.height * scale); 29 | const frameSize = frameWidth * frameHeight * 3; 30 | 31 | ffmpeg(video.url) 32 | .format('rawvideo') 33 | .videoFilters([ 34 | { filter: 'fps', options: 30 }, 35 | { filter: 'scale', options: `${frameWidth}:${frameHeight}` }, 36 | ]) 37 | .outputOptions('-pix_fmt', 'rgb24') 38 | .outputOptions('-update', '1') 39 | .on('start', () => canvas.saveScreen().reset()) 40 | .on('end', () => canvas.restoreScreen()) 41 | .pipe(new Throttle({ rate: frameSize * 30 })) 42 | .pipe(new ChopStream(frameSize)) 43 | .on('data', (frameData: number[]) => { 44 | if (process.env.USE_COLOR === 'true') { 45 | for (let y = 0; y < frameHeight; y += 1) { 46 | for (let x = 0; x < frameWidth; x += 1) { 47 | const offset = (y * frameWidth + x) * 3; 48 | const r = frameData[offset]; 49 | const g = frameData[offset + 1]; 50 | const b = frameData[offset + 2]; 51 | 52 | canvas 53 | .moveTo(x + (canvas.width / 2 - frameWidth / 2), y + (canvas.height / 2 - frameHeight / 2)) 54 | .background(`rgb(${r}, ${g}, ${b})`) 55 | .write(' '); 56 | } 57 | } 58 | } else { 59 | const contrastFactor = 2.95; 60 | 61 | for (let y = 0; y < frameHeight; y += 1) { 62 | for (let x = 0; x < frameWidth; x += 1) { 63 | const offset = (y * frameWidth + x) * 3; 64 | const r = Math.max(0, Math.min(contrastFactor * (frameData[offset] - 128) + 128, 255)); 65 | const g = Math.max(0, Math.min(contrastFactor * (frameData[offset + 1] - 128) + 128, 255)); 66 | const b = Math.max(0, Math.min(contrastFactor * (frameData[offset + 2] - 128) + 128, 255)); 67 | const brightness = 1 - (0.299 * r + 0.587 * g + 0.114 * b) / 255; 68 | 69 | canvas 70 | .moveTo(x + (canvas.width / 2 - frameWidth / 2), y + (canvas.height / 2 - frameHeight / 2)) 71 | .write(CHARACTERS[Math.round(brightness * 14)]); 72 | } 73 | } 74 | } 75 | 76 | canvas.flush(); 77 | }); 78 | } 79 | 80 | function playAudio (info: videoInfo): NodeJS.WritableStream { 81 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 82 | const audio = info.formats.find( 83 | (format) => format.quality === 'tiny' && format.container === 'webm' && format.audioChannels === 2, 84 | )!; 85 | 86 | const speaker = new Speaker({ channels: 2, sampleRate: 44100 }); 87 | 88 | return ffmpeg(audio.url) 89 | .noVideo() 90 | .audioCodec('pcm_s16le') 91 | .format('s16le') 92 | .pipe(speaker); 93 | } 94 | 95 | (async () => { 96 | const info = await ytdl.getInfo(YOUTUBE_URL); 97 | 98 | playVideo(info); 99 | playAudio(info); 100 | })().catch((error) => process.stderr.write(error)); 101 | 102 | process.on('SIGTERM', () => canvas.restoreScreen()); 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terminal-canvas 2 | 3 | ![Travis (.com)](https://img.shields.io/travis/com/ghaiklor/terminal-canvas?style=for-the-badge) 4 | ![Codecov](https://img.shields.io/codecov/c/gh/ghaiklor/terminal-canvas?style=for-the-badge) 5 | ![npm](https://img.shields.io/npm/dt/terminal-canvas?style=for-the-badge) 6 | 7 | [![GitHub Follow](https://img.shields.io/github/followers/ghaiklor.svg?label=Follow&style=social)](https://github.com/ghaiklor) 8 | [![Twitter Follow](https://img.shields.io/twitter/follow/ghaiklor.svg?label=Follow&style=social)](https://twitter.com/ghaiklor) 9 | 10 | Manipulate the cursor in your terminal via high-performant, low-level, canvas-like API. 11 | 12 | Entirely written with TypeScript, terminal-canvas exposes features that you can use for rendering in terminal. 13 | High-performance algorithms and optimizations allow to render only cells which have been changed. 14 | Just look at the demo videos below to see, what you can do with terminal-canvas :smiley: 15 | 16 | ## Demo 17 | 18 | | Touhou - Bad Apple (ASCII Art) | Touhou - Bad Apple (Colorful Edition) | Doom (Colorful Edition) | 19 | | :------------------------------------: | :------------------------------------: | :---------------------: | 20 | | [![1][Touhou ASCII P]][Touhou ASCII V] | [![1][Touhou Color P]][Touhou Color V] | [![1][Doom P]][Doom V] | 21 | 22 | ## Getting Started 23 | 24 | Install it via npm: 25 | 26 | ```shell 27 | npm install terminal-canvas 28 | ``` 29 | 30 | Include in your project: 31 | 32 | ```javascript 33 | const Canvas = require('terminal-canvas'); 34 | const canvas = new Canvas(); 35 | 36 | canvas.moveTo(10, 10).write('Hello, world').flush(); 37 | ``` 38 | 39 | ## Examples 40 | 41 | A lot of examples are available to you [here](./examples) 42 | 43 | ## API Reference 44 | 45 | API Reference is accessible [here](https://ghaiklor.github.io/terminal-canvas/) 46 | 47 | ## How terminal-canvas works 48 | 49 | ### Control Sequences 50 | 51 | terminal-canvas uses VT100 compatible control sequences to manipulate the cursor in the terminal. 52 | 53 | You can find a lot of useful information about that here: 54 | 55 | - [Terminal codes (ANSI/VT100) introduction](http://wiki.bash-hackers.org/scripting/terminalcodes) 56 | - [ANSI/VT100 Terminal Control Escape Sequences](http://www.termsys.demon.co.uk/vtansi.htm) 57 | - [Colors and formatting (ANSI/VT100 Control sequences)](http://misc.flogisoft.com/bash/tip_colors_and_formatting) 58 | - [Xterm Control Sequences](http://www.x.org/docs/xterm/ctlseqs.pdf) 59 | - [CONSOLE_CODES (man page)](http://man7.org/linux/man-pages/man4/console_codes.4.html) 60 | 61 | The next thing which terminal-canvas does is wrap those control codes, so you are able to call JavaScript methods. 62 | 63 | ### Virtual Terminal 64 | 65 | The first releases of the terminal-canvas (which was kittik-cursor) were based on real-time updating terminal cursor, when you are calling some method on the canvas. 66 | 67 | This caused performance issues. 68 | So I decided to create my own wrapper around terminal cells. 69 | 70 | Each real cell in the terminal has a wrapper (class Cell in the src folder). 71 | The main problem, which Cell resolves, is to render each cell independently from another cells. 72 | So I can grab any cell at any coordinate and render it independently from others. 73 | 74 | It works the following way. 75 | Each Cell has style settings and position in the real terminal. 76 | When you are converting Cell to the control sequences, it concatenates the following sequences: 77 | 78 | 1. Convert cell position to control sequence 79 | 2. Convert foreground and background color to control sequence 80 | 3. Convert display settings to control sequences 81 | 4. Pre-pend the cell char with sequences above 82 | 5. Reset all display settings to default 83 | 84 | That way, each cell wrapped in own control sequences that can be flushed at any moment. 85 | 86 | ### Difference between two frames 87 | 88 | The last thing I did, was update only cells that really changed. 89 | 90 | The algorithm is simple. 91 | 92 | When you are writing to the canvas, all write operations mark virtual cells as modified cells. 93 | After some time, you decide to flush changes. When flush() method is called it does 2 things: 94 | 95 | 1. Iterate through all cells and find only cells with modified marker; 96 | 2. Convert modified cell to control sequence and compare that sequence with the sequence that was used at the previous frame; 97 | 3. If they are not equal, store new control sequence and write to stream, otherwise, ignore it 98 | 99 | That's how I made it possible to render videos in the terminal at 30 FPS. 100 | 101 | BTW, if I remove Throttle stream, I'm getting 120 FPS :smiley: 102 | 103 | ## License 104 | 105 | [MIT](./LICENSE) 106 | 107 | [Touhou ASCII P]: https://img.youtube.com/vi/aYK8wFMiRP0/0.jpg 108 | [Touhou ASCII V]: https://www.youtube.com/watch?v=aYK8wFMiRP0 109 | [Touhou Color P]: https://img.youtube.com/vi/wgMm0wOZ8MM/0.jpg 110 | [Touhou Color V]: https://www.youtube.com/watch?v=wgMm0wOZ8MM 111 | [Doom P]: https://img.youtube.com/vi/Y5q_HDkt8nA/0.jpg 112 | [Doom V]: https://www.youtube.com/watch?v=Y5q_HDkt8nA 113 | -------------------------------------------------------------------------------- /test/Color.spec.ts: -------------------------------------------------------------------------------- 1 | import { Color } from '../src/color/Color'; 2 | 3 | describe('color', () => { 4 | it('should properly create Color instance from named color', () => { 5 | expect.hasAssertions(); 6 | 7 | const color = new Color('black'); 8 | 9 | expect(color.r).toStrictEqual(0); 10 | expect(color.g).toStrictEqual(0); 11 | expect(color.b).toStrictEqual(0); 12 | }); 13 | 14 | it('should properly create Color instance from RGB color', () => { 15 | expect.hasAssertions(); 16 | 17 | const color = new Color('rgb(0, 100, 200)'); 18 | 19 | expect(color.r).toStrictEqual(0); 20 | expect(color.g).toStrictEqual(100); 21 | expect(color.b).toStrictEqual(200); 22 | }); 23 | 24 | it('should properly create Color instance from HEX color', () => { 25 | expect.hasAssertions(); 26 | 27 | const color = new Color('#001020'); 28 | 29 | expect(color.r).toStrictEqual(0); 30 | expect(color.g).toStrictEqual(16); 31 | expect(color.b).toStrictEqual(32); 32 | }); 33 | 34 | it('should properly throw exception if color is not parsed', () => { 35 | expect.hasAssertions(); 36 | expect(() => new Color('false color')).toThrow(new Error('Color false color can\'t be parsed')); 37 | }); 38 | 39 | it('should properly get/set red channel', () => { 40 | expect.hasAssertions(); 41 | 42 | const color = new Color('black'); 43 | 44 | expect(color.getR()).toStrictEqual(0); 45 | expect(color.setR(20)).toBeInstanceOf(Color); 46 | expect(color.getR()).toStrictEqual(20); 47 | expect(color.setR(-50)).toBeInstanceOf(Color); 48 | expect(color.getR()).toStrictEqual(0); 49 | expect(color.setR(500)).toBeInstanceOf(Color); 50 | expect(color.getR()).toStrictEqual(255); 51 | }); 52 | 53 | it('should properly get/set green channel', () => { 54 | expect.hasAssertions(); 55 | 56 | const color = new Color('black'); 57 | 58 | expect(color.getG()).toStrictEqual(0); 59 | expect(color.setG(20)).toBeInstanceOf(Color); 60 | expect(color.getG()).toStrictEqual(20); 61 | expect(color.setG(-50)).toBeInstanceOf(Color); 62 | expect(color.getG()).toStrictEqual(0); 63 | expect(color.setG(500)).toBeInstanceOf(Color); 64 | expect(color.getG()).toStrictEqual(255); 65 | }); 66 | 67 | it('should properly get/set blue channel', () => { 68 | expect.hasAssertions(); 69 | 70 | const color = new Color('black'); 71 | 72 | expect(color.getB()).toStrictEqual(0); 73 | expect(color.setB(20)).toBeInstanceOf(Color); 74 | expect(color.getB()).toStrictEqual(20); 75 | expect(color.setB(-50)).toBeInstanceOf(Color); 76 | expect(color.getB()).toStrictEqual(0); 77 | expect(color.setB(500)).toBeInstanceOf(Color); 78 | expect(color.getB()).toStrictEqual(255); 79 | }); 80 | 81 | it('should properly return RGB object', () => { 82 | expect.hasAssertions(); 83 | 84 | const color = new Color('black'); 85 | 86 | expect(color.toRgb()).toStrictEqual({ r: 0, g: 0, b: 0 }); 87 | expect(color.setR(10)).toBeInstanceOf(Color); 88 | expect(color.setG(20)).toBeInstanceOf(Color); 89 | expect(color.setB(30)).toBeInstanceOf(Color); 90 | expect(color.toRgb()).toStrictEqual({ r: 10, g: 20, b: 30 }); 91 | }); 92 | 93 | it('should properly return HEX string', () => { 94 | expect.hasAssertions(); 95 | 96 | const color = new Color('black'); 97 | 98 | expect(color.toHex()).toStrictEqual('#000000'); 99 | expect(color.setR(16)).toBeInstanceOf(Color); 100 | expect(color.setG(32)).toBeInstanceOf(Color); 101 | expect(color.setB(48)).toBeInstanceOf(Color); 102 | expect(color.toHex()).toStrictEqual('#102030'); 103 | }); 104 | 105 | it('should properly check if color is named', () => { 106 | expect.hasAssertions(); 107 | expect(Color.isNamed('black')).toBe(true); 108 | expect(Color.isNamed('BlAcK')).toBe(true); 109 | expect(Color.isNamed('BLACK')).toBe(true); 110 | expect(Color.isNamed('False Color')).toBe(false); 111 | }); 112 | 113 | it('should properly check if color in RGB', () => { 114 | expect.hasAssertions(); 115 | expect(Color.isRgb('rgb(0, 10, 20)')).toBe(true); 116 | expect(Color.isRgb('RgB(0, 10, 50)')).toBe(true); 117 | expect(Color.isRgb('False Color')).toBe(false); 118 | }); 119 | 120 | it('should properly check if color in HEX', () => { 121 | expect.hasAssertions(); 122 | expect(Color.isHex('#001020')).toBe(true); 123 | expect(Color.isHex('#AABBCC')).toBe(true); 124 | expect(Color.isHex('#aaBbdD')).toBe(true); 125 | expect(Color.isHex('False Color')).toBe(false); 126 | }); 127 | 128 | it('should properly create color from RGB', () => { 129 | expect.hasAssertions(); 130 | 131 | const color = Color.fromRgb('rgb(10, 20, 30)'); 132 | 133 | expect(color.r).toStrictEqual(10); 134 | expect(color.g).toStrictEqual(20); 135 | expect(color.b).toStrictEqual(30); 136 | }); 137 | 138 | it('should properly throw an error if RGB pattern is incorrect', () => { 139 | expect.hasAssertions(); 140 | expect(() => Color.fromRgb('wrong rgb')).toThrow('Unrecognized RGB pattern: wrong rgb'); 141 | }); 142 | 143 | it('should properly create color from HEX', () => { 144 | expect.hasAssertions(); 145 | 146 | const color = Color.fromHex('#102030'); 147 | 148 | expect(color.r).toStrictEqual(16); 149 | expect(color.g).toStrictEqual(32); 150 | expect(color.b).toStrictEqual(48); 151 | }); 152 | 153 | it('should properly throw an error if HEX pattern is incorrect', () => { 154 | expect.hasAssertions(); 155 | expect(() => Color.fromHex('wrong hex')).toThrow('Unrecognized HEX pattern: wrong hex'); 156 | }); 157 | 158 | it('should properly create Color instance from static create()', () => { 159 | expect.hasAssertions(); 160 | 161 | const color = Color.create('black'); 162 | 163 | expect(color.r).toStrictEqual(0); 164 | expect(color.g).toStrictEqual(0); 165 | expect(color.b).toStrictEqual(0); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /src/color/Color.ts: -------------------------------------------------------------------------------- 1 | import { HEX_REGEX } from './HEXRegex'; 2 | import { NAMED_COLORS } from './NamedColors'; 3 | import { RGB_REGEX } from './RGBRegex'; 4 | 5 | export interface IColor { 6 | r: number 7 | g: number 8 | b: number 9 | } 10 | 11 | /** 12 | * Color class responsible for converting colors between rgb and hex. 13 | * 14 | * @since 2.0.0 15 | */ 16 | export class Color implements IColor { 17 | public r = 0; 18 | public g = 0; 19 | public b = 0; 20 | 21 | /** 22 | * Create new Color instance. 23 | * You can use different formats of color: named, rgb or hex. 24 | * Class will try to parse your provided color, otherwise throws an error. 25 | * 26 | * @constructor 27 | * @param {String|IColor} color String with named color, rgb, hex or object with {r, g, b} properties 28 | * @param {Number} color.r Red channel 29 | * @param {Number} color.g Green channel 30 | * @param {Number} color.b Blue channel 31 | * 32 | * @example 33 | * Color.create('black'); 34 | * Color.create('rgb(0, 10, 20)'); 35 | * Color.create('#AABBCC'); 36 | * Color.create({r: 0, g: 10, b: 20}); 37 | */ 38 | public constructor (color: string | IColor) { 39 | if (typeof color === 'string') { 40 | const hex = NAMED_COLORS.get(color.toUpperCase()); 41 | if (typeof hex !== 'undefined') { 42 | return Color.fromHex(hex); 43 | } 44 | 45 | if (Color.isRgb(color)) return Color.fromRgb(color); 46 | if (Color.isHex(color)) return Color.fromHex(color); 47 | 48 | throw new Error(`Color ${color} can't be parsed`); 49 | } else { 50 | this.setR(color.r); 51 | this.setG(color.g); 52 | this.setB(color.b); 53 | } 54 | } 55 | 56 | /** 57 | * Check if provided color is named color. 58 | * 59 | * @static 60 | * @param {String} color 61 | * @returns {Boolean} 62 | */ 63 | public static isNamed (color: string): boolean { 64 | return NAMED_COLORS.has(color.toUpperCase()); 65 | } 66 | 67 | /** 68 | * Check if provided color written in RGB representation. 69 | * 70 | * @static 71 | * @param {String} rgb RGB color 72 | * @returns {Boolean} 73 | */ 74 | public static isRgb (rgb: string): boolean { 75 | return RGB_REGEX.test(rgb); 76 | } 77 | 78 | /** 79 | * Check if provided color written in HEX representation. 80 | * 81 | * @static 82 | * @param {String} hex HEX color 83 | * @returns {Boolean} 84 | */ 85 | public static isHex (hex: string): boolean { 86 | return HEX_REGEX.test(hex); 87 | } 88 | 89 | /** 90 | * Parse RGB color and return Color instance. 91 | * 92 | * @static 93 | * @param {String} rgb RGB color 94 | * @returns {Color} 95 | */ 96 | public static fromRgb (rgb: string): Color { 97 | const match = RGB_REGEX.exec(rgb); 98 | if (match === null || typeof match.groups === 'undefined') { 99 | throw new Error(`Unrecognized RGB pattern: ${rgb}`); 100 | } 101 | 102 | const { red, green, blue } = match.groups; 103 | return this.create({ 104 | r: parseInt(red, 10), 105 | g: parseInt(green, 10), 106 | b: parseInt(blue, 10), 107 | }); 108 | } 109 | 110 | /** 111 | * Parse HEX color and return Color instance. 112 | * 113 | * @static 114 | * @param {String} hex HEX color 115 | * @returns {Color} 116 | */ 117 | public static fromHex (hex: string): Color { 118 | const match = HEX_REGEX.exec(hex); 119 | if (match === null || typeof match.groups === 'undefined') { 120 | throw new Error(`Unrecognized HEX pattern: ${hex}`); 121 | } 122 | 123 | const { red, green, blue } = match.groups; 124 | return this.create({ 125 | r: parseInt(red, 16), 126 | g: parseInt(green, 16), 127 | b: parseInt(blue, 16), 128 | }); 129 | } 130 | 131 | /** 132 | * Wrapper around `new Color()`. 133 | * 134 | * @static 135 | * @returns {Color} 136 | */ 137 | public static create (color: string | IColor): Color { 138 | return new this(color); 139 | } 140 | 141 | /** 142 | * Get rounded value of red channel. 143 | * 144 | * @returns {Number} 145 | */ 146 | public getR (): number { 147 | return Math.round(this.r); 148 | } 149 | 150 | /** 151 | * Set clamped value of red channel. 152 | * 153 | * @param {Number} value 154 | * @returns {Color} 155 | */ 156 | public setR (value: number): Color { 157 | this.r = Math.max(0, Math.min(value, 255)); 158 | return this; 159 | } 160 | 161 | /** 162 | * Get rounded value of green channel. 163 | * 164 | * @returns {Number} 165 | */ 166 | public getG (): number { 167 | return Math.round(this.g); 168 | } 169 | 170 | /** 171 | * Set clamped value of green channel. 172 | * 173 | * @param {Number} value 174 | * @returns {Color} 175 | */ 176 | public setG (value: number): Color { 177 | this.g = Math.max(0, Math.min(value, 255)); 178 | return this; 179 | } 180 | 181 | /** 182 | * Get rounded value of blue channel. 183 | * 184 | * @returns {Number} 185 | */ 186 | public getB (): number { 187 | return Math.round(this.b); 188 | } 189 | 190 | /** 191 | * Set clamped value of blue channel. 192 | * 193 | * @param {Number} value 194 | * @returns {Color} 195 | */ 196 | public setB (value: number): Color { 197 | this.b = Math.max(0, Math.min(value, 255)); 198 | return this; 199 | } 200 | 201 | /** 202 | * Convert color to RGB representation. 203 | * 204 | * @returns {{r: Number, g: Number, b: Number}} 205 | */ 206 | public toRgb (): IColor { 207 | return { r: this.getR(), g: this.getG(), b: this.getB() }; 208 | } 209 | 210 | /** 211 | * Convert color to HEX representation. 212 | * 213 | * @returns {String} 214 | */ 215 | public toHex (): string { 216 | const red = this.getR().toString(16).padStart(2, '0'); 217 | const green = this.getG().toString(16).padStart(2, '0'); 218 | const blue = this.getB().toString(16).padStart(2, '0'); 219 | 220 | return `#${[red, green, blue].join('')}`; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/color/NamedColors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Dictionary of all available named colors in terminal-canvas. 3 | */ 4 | export const NAMED_COLORS = new Map([ 5 | ['ALICE_BLUE', '#F0F8FF'], 6 | ['ALMOND', '#EFDECD'], 7 | ['ANTIQUE_BRASS', '#CD9575'], 8 | ['ANTIQUE_WHITE', '#FAEBD7'], 9 | ['APRICOT', '#FDD9B5'], 10 | ['AQUA', '#00FFFF'], 11 | ['AQUAMARINE', '#78DBE2'], 12 | ['ASPARAGUS', '#87A96B'], 13 | ['ATOMIC_TANGERINE', '#FFA474'], 14 | ['AZURE', '#F0FFFF'], 15 | ['BANANA_MANIA', '#FAE7B5'], 16 | ['BEAVER', '#9F8170'], 17 | ['BEIGE', '#F5F5DC'], 18 | ['BISQUE', '#FFE4C4'], 19 | ['BITTERSWEET', '#FD7C6E'], 20 | ['BLACK', '#000000'], 21 | ['BLANCHED_ALMOND', '#FFEBCD'], 22 | ['BLIZZARD_BLUE', '#ACE5EE'], 23 | ['BLUE_BELL', '#A2A2D0'], 24 | ['BLUE_GRAY', '#6699CC'], 25 | ['BLUE_GREEN', '#0D98BA'], 26 | ['BLUE_VIOLET', '#7366BD'], 27 | ['BLUE', '#1F75FE'], 28 | ['BLUSH', '#DE5D83'], 29 | ['BRICK_RED', '#CB4154'], 30 | ['BROWN', '#B4674D'], 31 | ['BURLY_WOOD', '#DEB887'], 32 | ['BURNT_ORANGE', '#FF7F49'], 33 | ['BURNT_SIENNA', '#EA7E5D'], 34 | ['CADET_BLUE', '#B0B7C6'], 35 | ['CANARY', '#FFFF99'], 36 | ['CARIBBEAN_GREEN', '#1CD3A2'], 37 | ['CARNATION_PINK', '#FFAACC'], 38 | ['CERISE', '#DD4492'], 39 | ['CERULEAN', '#1DACD6'], 40 | ['CHARTREUSE', '#7FFF00'], 41 | ['CHESTNUT', '#BC5D58'], 42 | ['CHOCOLATE', '#D2691E'], 43 | ['COPPER', '#DD9475'], 44 | ['CORAL', '#FF7F50'], 45 | ['CORN_FLOWER_BLUE', '#6495ED'], 46 | ['CORN_SILK', '#FFF8DC'], 47 | ['CORNFLOWER', '#9ACEEB'], 48 | ['COTTON_CANDY', '#FFBCD9'], 49 | ['CRIMSON', '#DC143C'], 50 | ['CYAN', '#00FFFF'], 51 | ['DANDELION', '#FDDB6D'], 52 | ['DARK_BLUE', '#00008B'], 53 | ['DARK_CYAN', '#008B8B'], 54 | ['DARK_GOLDEN_ROD', '#B8860B'], 55 | ['DARK_GRAY', '#A9A9A9'], 56 | ['DARK_GREEN', '#006400'], 57 | ['DARK_GREY', '#A9A9A9'], 58 | ['DARK_KHAKI', '#BDB76B'], 59 | ['DARK_MAGENTA', '#8B008B'], 60 | ['DARK_OLIVE_GREEN', '#556B2F'], 61 | ['DARK_ORANGE', '#FF8C00'], 62 | ['DARK_ORCHID', '#9932CC'], 63 | ['DARK_RED', '#8B0000'], 64 | ['DARK_SALMON', '#E9967A'], 65 | ['DARK_SEA_GREEN', '#8FBC8F'], 66 | ['DARK_SLATE_BLUE', '#483D8B'], 67 | ['DARK_SLATE_GRAY', '#2F4F4F'], 68 | ['DARK_SLATE_GREY', '#2F4F4F'], 69 | ['DARK_TURQUOISE', '#00CED1'], 70 | ['DARK_VIOLET', '#9400D3'], 71 | ['DEEP_PINK', '#FF1493'], 72 | ['DEEP_SKY_BLUE', '#00BFFF'], 73 | ['DENIM', '#2B6CC4'], 74 | ['DESERT_SAND', '#EFCDB8'], 75 | ['DIM_GRAY', '#696969'], 76 | ['DIM_GREY', '#696969'], 77 | ['DODGER_BLUE', '#1E90FF'], 78 | ['EGGPLANT', '#6E5160'], 79 | ['ELECTRIC_LIME', '#CEFF1D'], 80 | ['FERN', '#71BC78'], 81 | ['FIREBRICK', '#B22222'], 82 | ['FLORAL_WHITE', '#FFFAF0'], 83 | ['FOREST_GREEN', '#6DAE81'], 84 | ['FUCHSIA', '#C364C5'], 85 | ['FUZZY_WUZZY', '#CC6666'], 86 | ['GAINS_BORO', '#DCDCDC'], 87 | ['GHOST_WHITE', '#F8F8FF'], 88 | ['GOLD', '#E7C697'], 89 | ['GOLDENROD', '#FCD975'], 90 | ['GRANNY_SMITH_APPLE', '#A8E4A0'], 91 | ['GRAY', '#95918C'], 92 | ['GREEN_BLUE', '#1164B4'], 93 | ['GREEN_YELLOW', '#F0E891'], 94 | ['GREEN', '#1CAC78'], 95 | ['GREY', '#808080'], 96 | ['HONEYDEW', '#F0FFF0'], 97 | ['HOT_MAGENTA', '#FF1DCE'], 98 | ['HOT_PINK', '#FF69B4'], 99 | ['INCHWORM', '#B2EC5D'], 100 | ['INDIAN_RED', '#CD5C5C'], 101 | ['INDIGO', '#5D76CB'], 102 | ['IVORY', '#FFFFF0'], 103 | ['JAZZ_BERRY_JAM', '#CA3767'], 104 | ['JUNGLE_GREEN', '#3BB08F'], 105 | ['KHAKI', '#F0E68C'], 106 | ['LASER_LEMON', '#FEFE22'], 107 | ['LAVENDER_BLUSH', '#FFF0F5'], 108 | ['LAVENDER', '#FCB4D5'], 109 | ['LAWN_GREEN', '#7CFC00'], 110 | ['LEMON_CHIFFON', '#FFFACD'], 111 | ['LEMON_YELLOW', '#FFF44F'], 112 | ['LIGHT_BLUE', '#ADD8E6'], 113 | ['LIGHT_CORAL', '#F08080'], 114 | ['LIGHT_CYAN', '#E0FFFF'], 115 | ['LIGHT_GOLDENROD_YELLOW', '#FAFAD2'], 116 | ['LIGHT_GRAY', '#D3D3D3'], 117 | ['LIGHT_GREEN', '#90EE90'], 118 | ['LIGHT_GREY', '#D3D3D3'], 119 | ['LIGHT_PINK', '#FFB6C1'], 120 | ['LIGHT_SALMON', '#FFA07A'], 121 | ['LIGHT_SEA_GREEN', '#20B2AA'], 122 | ['LIGHT_SKY_BLUE', '#87CEFA'], 123 | ['LIGHT_SLATE_GRAY', '#778899'], 124 | ['LIGHT_SLATE_GREY', '#778899'], 125 | ['LIGHT_STEEL_BLUE', '#B0C4DE'], 126 | ['LIGHT_YELLOW', '#FFFFE0'], 127 | ['LIME_GREEN', '#32CD32'], 128 | ['LIME', '#00FF00'], 129 | ['LINEN', '#FAF0E6'], 130 | ['MACARONI_AND_CHEESE', '#FFBD88'], 131 | ['MAGENTA', '#F664AF'], 132 | ['MAGIC_MINT', '#AAF0D1'], 133 | ['MAHOGANY', '#CD4A4C'], 134 | ['MAIZE', '#EDD19C'], 135 | ['MANATEE', '#979AAA'], 136 | ['MANGO_TANGO', '#FF8243'], 137 | ['MAROON', '#C8385A'], 138 | ['MAUVELOUS', '#EF98AA'], 139 | ['MEDIUM_AQUAMARINE', '#66CDAA'], 140 | ['MEDIUM_BLUE', '#0000CD'], 141 | ['MEDIUM_ORCHID', '#BA55D3'], 142 | ['MEDIUM_PURPLE', '#9370DB'], 143 | ['MEDIUM_SEA_GREEN', '#3CB371'], 144 | ['MEDIUM_SLATE_BLUE', '#7B68EE'], 145 | ['MEDIUM_SPRING_GREEN', '#00FA9A'], 146 | ['MEDIUM_TURQUOISE', '#48D1CC'], 147 | ['MEDIUM_VIOLET_RED', '#C71585'], 148 | ['MELON', '#FDBCB4'], 149 | ['MIDNIGHT_BLUE', '#1A4876'], 150 | ['MINT_CREAM', '#F5FFFA'], 151 | ['MISTY_ROSE', '#FFE4E1'], 152 | ['MOCCASIN', '#FFE4B5'], 153 | ['MOUNTAIN_MEADOW', '#30BA8F'], 154 | ['MULBERRY', '#C54B8C'], 155 | ['NAVAJO_WHITE', '#FFDEAD'], 156 | ['NAVY_BLUE', '#1974D2'], 157 | ['NAVY', '#000080'], 158 | ['NEON_CARROT', '#FFA343'], 159 | ['OLD_LACE', '#FDF5E6'], 160 | ['OLIVE_DRAB', '#6B8E23'], 161 | ['OLIVE_GREEN', '#BAB86C'], 162 | ['OLIVE', '#808000'], 163 | ['ORANGE_RED', '#FF2B2B'], 164 | ['ORANGE_YELLOW', '#F8D568'], 165 | ['ORANGE', '#FF7538'], 166 | ['ORCHID', '#E6A8D7'], 167 | ['OUTER_SPACE', '#414A4C'], 168 | ['OUTRAGEOUS_ORANGE', '#FF6E4A'], 169 | ['PACIFIC_BLUE', '#1CA9C9'], 170 | ['PALE_GOLDENROD', '#EEE8AA'], 171 | ['PALE_GREEN', '#98FB98'], 172 | ['PALE_TURQUOISE', '#AFEEEE'], 173 | ['PALE_VIOLET_RED', '#DB7093'], 174 | ['PAPAYA_WHIP', '#FFEFD5'], 175 | ['PEACH_PUFF', '#FFDAB9'], 176 | ['PEACH', '#FFCFAB'], 177 | ['PERIWINKLE', '#C5D0E6'], 178 | ['PERU', '#CD853F'], 179 | ['PIGGY_PINK', '#FDDDE6'], 180 | ['PINE_GREEN', '#158078'], 181 | ['PINK_FLAMINGO', '#FC74FD'], 182 | ['PINK_SHERBET', '#F78FA7'], 183 | ['PINK', '#FFC0CB'], 184 | ['PLUM', '#8E4585'], 185 | ['POWDER_BLUE', '#B0E0E6'], 186 | ['PURPLE_HEART', '#7442C8'], 187 | ['PURPLE_MOUNTAINS_MAJESTY', '#9D81BA'], 188 | ['PURPLE_PIZZAZZ', '#FE4EDA'], 189 | ['PURPLE', '#800080'], 190 | ['RADICAL_RED', '#FF496C'], 191 | ['RAW_SIENNA', '#D68A59'], 192 | ['RAW_UMBER', '#714B23'], 193 | ['RAZZLE_DAZZLE_ROSE', '#FF48D0'], 194 | ['RAZZMATAZZ', '#E3256B'], 195 | ['REBECCA_PURPLE', '#663399'], 196 | ['RED_ORANGE', '#FF5349'], 197 | ['RED_VIOLET', '#C0448F'], 198 | ['RED', '#EE204D'], 199 | ['ROBINS_EGG_BLUE', '#1FCECB'], 200 | ['ROSY_BROWN', '#BC8F8F'], 201 | ['ROYAL_BLUE', '#4169E1'], 202 | ['ROYAL_PURPLE', '#7851A9'], 203 | ['SADDLE_BROWN', '#8B4513'], 204 | ['SALMON', '#FF9BAA'], 205 | ['SANDY_BROWN', '#F4A460'], 206 | ['SCARLET', '#FC2847'], 207 | ['SCREAMING_GREEN', '#76FF7A'], 208 | ['SEA_GREEN', '#9FE2BF'], 209 | ['SEASHELL', '#FFF5EE'], 210 | ['SEPIA', '#A5694F'], 211 | ['SHADOW', '#8A795D'], 212 | ['SHAMROCK', '#45CEA2'], 213 | ['SHOCKING_PINK', '#FB7EFD'], 214 | ['SIENNA', '#A0522D'], 215 | ['SILVER', '#CDC5C2'], 216 | ['SKY_BLUE', '#80DAEB'], 217 | ['SLATE_BLUE', '#6A5ACD'], 218 | ['SLATE_GRAY', '#708090'], 219 | ['SLATE_GREY', '#708090'], 220 | ['SNOW', '#FFFAFA'], 221 | ['SPRING_GREEN', '#ECEABE'], 222 | ['STEEL_BLUE', '#4682B4'], 223 | ['SUNGLOW', '#FFCF48'], 224 | ['SUNSET_ORANGE', '#FD5E53'], 225 | ['TAN', '#FAA76C'], 226 | ['TEAL_BLUE', '#18A7B5'], 227 | ['TEAL', '#008080'], 228 | ['THISTLE', '#EBC7DF'], 229 | ['TICKLE_ME_PINK', '#FC89AC'], 230 | ['TIMBER_WOLF', '#DBD7D2'], 231 | ['TOMATO', '#FF6347'], 232 | ['TROPICAL_RAIN_FOREST', '#17806D'], 233 | ['TUMBLEWEED', '#DEAA88'], 234 | ['TURQUOISE_BLUE', '#77DDE7'], 235 | ['TURQUOISE', '#40E0D0'], 236 | ['UN_MELLOW_YELLOW', '#FFFF66'], 237 | ['VIOLET_BLUE', '#324AB2'], 238 | ['VIOLET_PURPLE', '#926EAE'], 239 | ['VIOLET_RED', '#F75394'], 240 | ['VIOLET', '#EE82EE'], 241 | ['VIVID_TANGERINE', '#FFA089'], 242 | ['VIVID_VIOLET', '#8F509D'], 243 | ['WHEAT', '#F5DEB3'], 244 | ['WHITE_SMOKE', '#F5F5F5'], 245 | ['WHITE', '#FFFFFF'], 246 | ['WILD_BLUE_YONDER', '#A2ADD0'], 247 | ['WILD_STRAWBERRY', '#FF43A4'], 248 | ['WILD_WATERMELON', '#FC6C85'], 249 | ['WISTERIA', '#CDA4DE'], 250 | ['YELLOW_GREEN', '#C5E384'], 251 | ['YELLOW_ORANGE', '#FFAE42'], 252 | ['YELLOW', '#FCE883'], 253 | ]); 254 | -------------------------------------------------------------------------------- /src/cell/Cell.ts: -------------------------------------------------------------------------------- 1 | import { DISPLAY_MODES } from './DisplayModes'; 2 | import { ICellOptions } from './CellOptions'; 3 | import { IColor } from '../color/Color'; 4 | import { IDisplayOptions } from './DisplayOptions'; 5 | import { encodeToVT100 } from '../encodeToVT100'; 6 | 7 | /** 8 | * Wrapper for one cell in the terminal. 9 | * It is used for converting abstract configuration of the cell to the real control sequence. 10 | * 11 | * @since 2.0.0 12 | */ 13 | export class Cell implements ICellOptions { 14 | public isModified = false; 15 | public char = ' '; 16 | public x = 0; 17 | public y = 0; 18 | public background: IColor = { r: -1, g: -1, b: -1 }; 19 | public foreground: IColor = { r: -1, g: -1, b: -1 }; 20 | public display: IDisplayOptions = { 21 | blink: false, 22 | bold: false, 23 | dim: false, 24 | hidden: false, 25 | reverse: false, 26 | underlined: false, 27 | }; 28 | 29 | /** 30 | * Create Cell instance which are able to convert itself to ASCII control sequence. 31 | * 32 | * @constructor 33 | * @param {String} char Char that you want to wrap with control sequence 34 | * @param {Object} [options] Options object where you can set additional style to char 35 | * @param {Number} [options.x] X coordinate 36 | * @param {Number} [options.y] Y coordinate 37 | * @param {Object} [options.background] Background color, fill with -1 if you don't want to use background 38 | * @param {Number} [options.background.r] Red channel 39 | * @param {Number} [options.background.g] Green channel 40 | * @param {Number} [options.background.b] Blue channel 41 | * @param {Object} [options.foreground] Foreground color, fill with -1 if you don't want to use foreground 42 | * @param {Number} [options.foreground.r] Red channel 43 | * @param {Number} [options.foreground.g] Green channel 44 | * @param {Number} [options.foreground.b] Blue channel 45 | * @param {Object} [options.display] Object with display modes 46 | * @param {Boolean} [options.display.bold] Bold style 47 | * @param {Boolean} [options.display.dim] Dim style 48 | * @param {Boolean} [options.display.underlined] Underlined style 49 | * @param {Boolean} [options.display.blink] Blink style 50 | * @param {Boolean} [options.display.reverse] Reverse style 51 | * @param {Boolean} [options.display.hidden] Hidden style 52 | */ 53 | public constructor (char: string, options?: Partial) { 54 | this.setChar(char); 55 | 56 | if (typeof options?.x !== 'undefined') { 57 | this.setX(options.x); 58 | } 59 | 60 | if (typeof options?.y !== 'undefined') { 61 | this.setY(options.y); 62 | } 63 | 64 | if (typeof options?.background !== 'undefined') { 65 | this.setBackground(options.background.r, options.background.g, options.background.b); 66 | } 67 | 68 | if (typeof options?.foreground !== 'undefined') { 69 | this.setForeground(options.foreground.r, options.foreground.g, options.foreground.b); 70 | } 71 | 72 | if (typeof options?.display !== 'undefined') { 73 | this.setDisplay(options.display); 74 | } 75 | } 76 | 77 | /** 78 | * Wrapper around `new Cell()`. 79 | * 80 | * @static 81 | * @returns {Cell} 82 | */ 83 | public static create (char: string, options?: Partial): Cell { 84 | return new this(char, options); 85 | } 86 | 87 | /** 88 | * Returns current character in the cell. 89 | * 90 | * @returns {String} 91 | */ 92 | public getChar (): string { 93 | return this.char; 94 | } 95 | 96 | /** 97 | * Updates the cell with the newly specified character. 98 | * 99 | * @param {String} char Char to update in the cell 100 | */ 101 | public setChar (char: string): Cell { 102 | this.char = char.slice(0, 1); 103 | return this; 104 | } 105 | 106 | /** 107 | * Get X coordinate of the cell. 108 | * 109 | * @returns {Number} 110 | */ 111 | public getX (): number { 112 | return this.x; 113 | } 114 | 115 | /** 116 | * Set X coordinate. 117 | * 118 | * @param {Number} x X coordinate of the cell 119 | */ 120 | public setX (x: number): Cell { 121 | this.x = Math.floor(x); 122 | return this; 123 | } 124 | 125 | /** 126 | * Get Y coordinate of the cell. 127 | * 128 | * @returns {Number} 129 | */ 130 | public getY (): number { 131 | return this.y; 132 | } 133 | 134 | /** 135 | * Set Y coordinate. 136 | * 137 | * @param {Number} y Y coordinate of the cell 138 | */ 139 | public setY (y: number): Cell { 140 | this.y = Math.floor(y); 141 | return this; 142 | } 143 | 144 | /** 145 | * Get current background options of the cell. 146 | * 147 | * @returns {IColor} 148 | */ 149 | public getBackground (): IColor { 150 | return this.background; 151 | } 152 | 153 | /** 154 | * Set a new background for the cell. 155 | * 156 | * @param {IColor} background Color to set on the background of the cell 157 | */ 158 | public setBackground (r: number, g: number, b: number): Cell { 159 | this.background = { r, g, b }; 160 | return this; 161 | } 162 | 163 | /** 164 | * Reset background for the cell. 165 | * 166 | * @returns {Cell} 167 | */ 168 | public resetBackground (): Cell { 169 | this.background = { r: -1, g: -1, b: -1 }; 170 | return this; 171 | } 172 | 173 | /** 174 | * Get current foreground options of the cell. 175 | * 176 | * @returns {IColor} 177 | */ 178 | public getForeground (): IColor { 179 | return this.foreground; 180 | } 181 | 182 | /** 183 | * Set a new foreground for the cell. 184 | * 185 | * @param {IColor} foreground Color to set on the foreground of the cell 186 | */ 187 | public setForeground (r: number, g: number, b: number): Cell { 188 | this.foreground = { r, g, b }; 189 | return this; 190 | } 191 | 192 | /** 193 | * Resets foreground for the cell. 194 | * 195 | * @returns {Cell} 196 | */ 197 | public resetForeground (): Cell { 198 | this.foreground = { r: -1, g: -1, b: -1 }; 199 | return this; 200 | } 201 | 202 | /** 203 | * Get display characteristics of the cell. 204 | * 205 | * @returns {IDisplayOptions} 206 | */ 207 | public getDisplay (): IDisplayOptions { 208 | return this.display; 209 | } 210 | 211 | /** 212 | * Updates display characteristics of the cell. 213 | * 214 | * @param {IDisplayOptions} display Options for the display characteristics of the character in cell 215 | */ 216 | public setDisplay (display: Partial): Cell { 217 | this.display = { 218 | blink: typeof display.blink === 'undefined' ? false : display.blink, 219 | bold: typeof display.bold === 'undefined' ? false : display.bold, 220 | dim: typeof display.dim === 'undefined' ? false : display.dim, 221 | hidden: typeof display.hidden === 'undefined' ? false : display.hidden, 222 | reverse: typeof display.reverse === 'undefined' ? false : display.reverse, 223 | underlined: typeof display.underlined === 'undefined' ? false : display.underlined, 224 | }; 225 | 226 | return this; 227 | } 228 | 229 | /** 230 | * Resets display characteristics for the cell. 231 | * 232 | * @returns {Cell} 233 | */ 234 | public resetDisplay (): Cell { 235 | this.display = { 236 | blink: false, 237 | bold: false, 238 | dim: false, 239 | hidden: false, 240 | reverse: false, 241 | underlined: false, 242 | }; 243 | 244 | return this; 245 | } 246 | 247 | /** 248 | * Reset all display settings for the cell. 249 | * It resets char, background, foreground and display mode. 250 | * 251 | * @returns {Cell} 252 | */ 253 | public reset (): Cell { 254 | this.setChar(' '); 255 | this.resetBackground(); 256 | this.resetForeground(); 257 | this.resetDisplay(); 258 | this.isModified = true; 259 | 260 | return this; 261 | } 262 | 263 | /** 264 | * Convert cell to VT100 control sequence. 265 | * 266 | * @returns {String} 267 | */ 268 | public toString (): string { 269 | const { background, foreground, char, y, x } = this; 270 | const { bold, dim, underlined, blink, reverse, hidden } = this.display; 271 | 272 | return ( 273 | encodeToVT100(`[${y + 1};${x + 1}f`) + 274 | (background.r > -1 ? encodeToVT100(`[48;2;${background.r};${background.g};${background.b}m`) : '') + 275 | (foreground.r > -1 ? encodeToVT100(`[38;2;${foreground.r};${foreground.g};${foreground.b}m`) : '') + 276 | (bold ? encodeToVT100(`[${DISPLAY_MODES.BOLD}m`) : '') + 277 | (dim ? encodeToVT100(`[${DISPLAY_MODES.DIM}m`) : '') + 278 | (underlined ? encodeToVT100(`[${DISPLAY_MODES.UNDERLINED}m`) : '') + 279 | (blink ? encodeToVT100(`[${DISPLAY_MODES.BLINK}m`) : '') + 280 | (reverse ? encodeToVT100(`[${DISPLAY_MODES.REVERSE}m`) : '') + 281 | (hidden ? encodeToVT100(`[${DISPLAY_MODES.HIDDEN}m`) : '') + 282 | char + 283 | encodeToVT100(`[${DISPLAY_MODES.RESET_ALL}m`) 284 | ); 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /test/Cell.spec.ts: -------------------------------------------------------------------------------- 1 | import { Cell } from '../src/cell/Cell'; 2 | 3 | describe('cell', () => { 4 | it('should properly create cell with default arguments', () => { 5 | expect.hasAssertions(); 6 | 7 | const cell = new Cell(' '); 8 | 9 | expect(cell.char).toStrictEqual(' '); 10 | expect(cell.x).toStrictEqual(0); 11 | expect(cell.y).toStrictEqual(0); 12 | expect(cell.background).toStrictEqual({ r: -1, g: -1, b: -1 }); 13 | expect(cell.foreground).toStrictEqual({ r: -1, g: -1, b: -1 }); 14 | expect(cell.display).toStrictEqual({ 15 | blink: false, 16 | bold: false, 17 | dim: false, 18 | hidden: false, 19 | reverse: false, 20 | underlined: false, 21 | }); 22 | }); 23 | 24 | it('should properly create cell with custom arguments', () => { 25 | expect.hasAssertions(); 26 | 27 | const cell = new Cell('s', { 28 | background: { r: 1, g: 2, b: 3 }, 29 | display: { bold: true }, 30 | foreground: { r: 4, g: 5, b: 6 }, 31 | x: 10, 32 | y: 20, 33 | }); 34 | 35 | expect(cell.char).toStrictEqual('s'); 36 | expect(cell.x).toStrictEqual(10); 37 | expect(cell.y).toStrictEqual(20); 38 | expect(cell.background).toStrictEqual({ r: 1, g: 2, b: 3 }); 39 | expect(cell.foreground).toStrictEqual({ r: 4, g: 5, b: 6 }); 40 | expect(cell.display).toStrictEqual({ 41 | blink: false, 42 | bold: true, 43 | dim: false, 44 | hidden: false, 45 | reverse: false, 46 | underlined: false, 47 | }); 48 | }); 49 | 50 | it('should properly create cell with custom X argument', () => { 51 | expect.hasAssertions(); 52 | 53 | const cell = new Cell('s', { x: 10 }); 54 | 55 | expect(cell.char).toStrictEqual('s'); 56 | expect(cell.x).toStrictEqual(10); 57 | expect(cell.y).toStrictEqual(0); 58 | }); 59 | 60 | it('should properly create cell with custom Y argument', () => { 61 | expect.hasAssertions(); 62 | 63 | const cell = new Cell('s', { y: 10 }); 64 | 65 | expect(cell.char).toStrictEqual('s'); 66 | expect(cell.x).toStrictEqual(0); 67 | expect(cell.y).toStrictEqual(10); 68 | }); 69 | 70 | it('should properly get/set char', () => { 71 | expect.hasAssertions(); 72 | 73 | const cell = new Cell(' '); 74 | 75 | expect(cell.getChar()).toStrictEqual(' '); 76 | expect(cell.setChar('t')).toBeInstanceOf(Cell); 77 | expect(cell.getChar()).toStrictEqual('t'); 78 | expect(cell.setChar('long text')).toBeInstanceOf(Cell); 79 | expect(cell.getChar()).toStrictEqual('l'); 80 | }); 81 | 82 | it('should properly get/set X coordinate', () => { 83 | expect.hasAssertions(); 84 | 85 | const cell = new Cell(' '); 86 | 87 | expect(cell.getX()).toStrictEqual(0); 88 | expect(cell.setX(40)).toBeInstanceOf(Cell); 89 | expect(cell.getX()).toStrictEqual(40); 90 | expect(cell.setX(50.99)).toBeInstanceOf(Cell); 91 | expect(cell.getX()).toStrictEqual(50); 92 | }); 93 | 94 | it('should properly get/set Y coordinate', () => { 95 | expect.hasAssertions(); 96 | 97 | const cell = new Cell(' '); 98 | 99 | expect(cell.getY()).toStrictEqual(0); 100 | expect(cell.setY(40)).toBeInstanceOf(Cell); 101 | expect(cell.getY()).toStrictEqual(40); 102 | expect(cell.setY(50.99)).toBeInstanceOf(Cell); 103 | expect(cell.getY()).toStrictEqual(50); 104 | }); 105 | 106 | it('should properly get/set background color', () => { 107 | expect.hasAssertions(); 108 | 109 | const cell = new Cell(' '); 110 | 111 | expect(cell.getBackground()).toStrictEqual({ r: -1, g: -1, b: -1 }); 112 | expect(cell.setBackground(0, 100, 200)).toBeInstanceOf(Cell); 113 | expect(cell.getBackground()).toStrictEqual({ r: 0, g: 100, b: 200 }); 114 | expect(cell.resetBackground()).toBeInstanceOf(Cell); 115 | expect(cell.getBackground()).toStrictEqual({ r: -1, g: -1, b: -1 }); 116 | }); 117 | 118 | it('should properly get/set foreground color', () => { 119 | expect.hasAssertions(); 120 | 121 | const cell = new Cell(' '); 122 | 123 | expect(cell.getForeground()).toStrictEqual({ r: -1, g: -1, b: -1 }); 124 | expect(cell.setForeground(0, 100, 200)).toBeInstanceOf(Cell); 125 | expect(cell.getForeground()).toStrictEqual({ r: 0, g: 100, b: 200 }); 126 | expect(cell.resetForeground()).toBeInstanceOf(Cell); 127 | expect(cell.getForeground()).toStrictEqual({ r: -1, g: -1, b: -1 }); 128 | }); 129 | 130 | it('should properly get/set display modes', () => { 131 | expect.hasAssertions(); 132 | 133 | const cell = new Cell(' '); 134 | 135 | expect(cell.getDisplay()).toStrictEqual({ 136 | blink: false, 137 | bold: false, 138 | dim: false, 139 | hidden: false, 140 | reverse: false, 141 | underlined: false, 142 | }); 143 | 144 | expect(cell.setDisplay({ 145 | blink: false, 146 | bold: true, 147 | dim: false, 148 | hidden: false, 149 | reverse: false, 150 | underlined: true, 151 | })).toBeInstanceOf(Cell); 152 | 153 | expect(cell.getDisplay()).toStrictEqual({ 154 | blink: false, 155 | bold: true, 156 | dim: false, 157 | hidden: false, 158 | reverse: false, 159 | underlined: true, 160 | }); 161 | 162 | expect(cell.resetDisplay()).toBeInstanceOf(Cell); 163 | expect(cell.getDisplay()).toStrictEqual({ 164 | blink: false, 165 | bold: false, 166 | dim: false, 167 | hidden: false, 168 | reverse: false, 169 | underlined: false, 170 | }); 171 | }); 172 | 173 | it('should properly set display options if display is an empty object', () => { 174 | expect.hasAssertions(); 175 | 176 | const cell = new Cell('s', { display: {} }); 177 | 178 | expect(cell.display).toStrictEqual({ 179 | blink: false, 180 | bold: false, 181 | dim: false, 182 | hidden: false, 183 | reverse: false, 184 | underlined: false, 185 | }); 186 | }); 187 | 188 | it('should properly reset the cell contents and display settings', () => { 189 | expect.hasAssertions(); 190 | 191 | const cell = new Cell('s', { 192 | background: { r: 1, g: 2, b: 3 }, 193 | display: { blink: true, bold: true, dim: true, hidden: true, reverse: true, underlined: true }, 194 | foreground: { r: 4, g: 5, b: 6 }, 195 | x: 10, 196 | y: 20, 197 | }); 198 | 199 | expect(cell.char).toStrictEqual('s'); 200 | expect(cell.x).toStrictEqual(10); 201 | expect(cell.y).toStrictEqual(20); 202 | expect(cell.background).toStrictEqual({ r: 1, g: 2, b: 3 }); 203 | expect(cell.foreground).toStrictEqual({ r: 4, g: 5, b: 6 }); 204 | expect(cell.display).toStrictEqual({ 205 | blink: true, 206 | bold: true, 207 | dim: true, 208 | hidden: true, 209 | reverse: true, 210 | underlined: true, 211 | }); 212 | 213 | expect(cell.reset()).toBeInstanceOf(Cell); 214 | expect(cell.char).toStrictEqual(' '); 215 | expect(cell.x).toStrictEqual(10); 216 | expect(cell.y).toStrictEqual(20); 217 | expect(cell.background).toStrictEqual({ r: -1, g: -1, b: -1 }); 218 | expect(cell.foreground).toStrictEqual({ r: -1, g: -1, b: -1 }); 219 | expect(cell.display).toStrictEqual({ 220 | blink: false, 221 | bold: false, 222 | dim: false, 223 | hidden: false, 224 | reverse: false, 225 | underlined: false, 226 | }); 227 | }); 228 | 229 | it('should properly convert Cell into ASCII sequence', () => { 230 | expect.hasAssertions(); 231 | 232 | const cell = new Cell(' '); 233 | 234 | expect(cell.toString()).toStrictEqual('\u001b[1;1f \u001b[0m'); 235 | expect(cell.setX(20)).toBeInstanceOf(Cell); 236 | expect(cell.toString()).toStrictEqual('\u001b[1;21f \u001b[0m'); 237 | expect(cell.setY(10)).toBeInstanceOf(Cell); 238 | expect(cell.toString()).toStrictEqual('\u001b[11;21f \u001b[0m'); 239 | expect(cell.setBackground(0, 100, 200)).toBeInstanceOf(Cell); 240 | expect(cell.toString()).toStrictEqual('\u001b[11;21f\u001b[48;2;0;100;200m \u001b[0m'); 241 | expect(cell.setForeground(200, 100, 0)).toBeInstanceOf(Cell); 242 | expect(cell.toString()).toStrictEqual('\u001b[11;21f\u001b[48;2;0;100;200m\u001b[38;2;200;100;0m \u001b[0m'); 243 | expect(cell.setDisplay({ 244 | blink: true, 245 | bold: true, 246 | dim: true, 247 | hidden: true, 248 | reverse: true, 249 | underlined: true, 250 | })).toBeInstanceOf(Cell); 251 | 252 | // eslint-disable-next-line max-len 253 | expect(cell.toString()).toStrictEqual('\u001b[11;21f\u001b[48;2;0;100;200m\u001b[38;2;200;100;0m\u001b[1m\u001b[2m\u001b[4m\u001b[5m\u001b[7m\u001b[8m \u001b[0m'); 254 | }); 255 | 256 | it('should properly create Cell instance from static create()', () => { 257 | expect.hasAssertions(); 258 | 259 | const cell = Cell.create('s', { x: 10, y: 20 }); 260 | 261 | expect(cell.char).toStrictEqual('s'); 262 | expect(cell.x).toStrictEqual(10); 263 | expect(cell.y).toStrictEqual(20); 264 | expect(cell.background).toStrictEqual({ r: -1, g: -1, b: -1 }); 265 | expect(cell.foreground).toStrictEqual({ r: -1, g: -1, b: -1 }); 266 | expect(cell.display).toStrictEqual({ 267 | blink: false, 268 | bold: false, 269 | dim: false, 270 | hidden: false, 271 | reverse: false, 272 | underlined: false, 273 | }); 274 | }); 275 | }); 276 | -------------------------------------------------------------------------------- /src/canvas/Canvas.ts: -------------------------------------------------------------------------------- 1 | import { Color, IColor } from '../color/Color'; 2 | import { Cell } from '../cell/Cell'; 3 | import { ICanvasOptions } from './CanvasOptions'; 4 | import { IDisplayOptions } from '../cell/DisplayOptions'; 5 | import { WriteStream } from 'tty'; 6 | import { encodeToVT100 } from '../encodeToVT100'; 7 | 8 | /** 9 | * Canvas implements low-level API to terminal control codes. 10 | * 11 | * @see http://www.termsys.demon.co.uk/vtansi.htm 12 | * @see http://misc.flogisoft.com/bash/tip_colors_and_formatting 13 | * @see http://man7.org/linux/man-pages/man4/console_codes.4.html 14 | * @see http://www.x.org/docs/xterm/ctlseqs.pdf 15 | * @see http://wiki.bash-hackers.org/scripting/terminalcodes 16 | * @since 1.0.0 17 | */ 18 | export class Canvas implements ICanvasOptions { 19 | public readonly cells: Cell[]; 20 | public lastFrame: string[]; 21 | public stream: WriteStream = process.stdout; 22 | public width: number; 23 | public height: number; 24 | public cursorX = 0; 25 | public cursorY = 0; 26 | public cursorBackground: IColor = { r: -1, g: -1, b: -1 }; 27 | public cursorForeground: IColor = { r: -1, g: -1, b: -1 }; 28 | public cursorDisplay: IDisplayOptions = { 29 | blink: false, 30 | bold: false, 31 | dim: false, 32 | hidden: false, 33 | reverse: false, 34 | underlined: false, 35 | }; 36 | 37 | /** 38 | * Creates canvas that writes direct to `stdout` by default. 39 | * You can override destination stream with another Writable stream. 40 | * Also, you can specify custom width and height of viewport where cursor will render the frame. 41 | * 42 | * @constructor 43 | * @param {Object} [options] 44 | * @param {Stream} [options.stream=process.stdout] Writable stream 45 | * @param {Number} [options.width=stream.columns] Number of columns (width) 46 | * @param {Number} [options.height=stream.rows] Number of rows (height) 47 | * @example 48 | * Canvas.create({stream: fs.createWriteStream(), width: 20, height: 20}); 49 | */ 50 | public constructor (options?: Partial) { 51 | if (typeof options?.stream !== 'undefined') { 52 | this.stream = options.stream; 53 | } 54 | 55 | this.width = this.stream.columns; 56 | if (typeof options?.width !== 'undefined') { 57 | this.width = options.width; 58 | } 59 | 60 | this.height = this.stream.rows; 61 | if (typeof options?.height !== 'undefined') { 62 | this.height = options.height; 63 | } 64 | 65 | this.cells = Array 66 | .from({ length: this.width * this.height }) 67 | .map((_, index) => new Cell(' ', { x: this.getXYFromPointer(index)[0], y: this.getXYFromPointer(index)[1] })); 68 | 69 | this.lastFrame = Array.from({ length: this.width * this.height }).fill(''); 70 | } 71 | 72 | /** 73 | * Wrapper around `new Canvas()`. 74 | * 75 | * @static 76 | * @returns {Canvas} 77 | */ 78 | public static create (options?: Partial): Canvas { 79 | return new this(options); 80 | } 81 | 82 | /** 83 | * Write to the buffer. 84 | * It doesn't apply immediately, but stores in virtual terminal that represented as array of {@link Cell} instances. 85 | * For applying changes, you need to call {@link flush} method. 86 | * 87 | * @param {String} data Data to write to the terminal 88 | * @returns {Canvas} 89 | * @example 90 | * canvas.write('Hello, world').flush(); 91 | */ 92 | public write (data: string): Canvas { 93 | const { width, height } = this; 94 | const background = this.cursorBackground; 95 | const foreground = this.cursorForeground; 96 | const display = this.cursorDisplay; 97 | 98 | for (const char of data) { 99 | const x = this.cursorX; 100 | const y = this.cursorY; 101 | const pointer = this.getPointerFromXY(x, y); 102 | 103 | if (x >= 0 && x < width && y >= 0 && y < height) { 104 | const cell = this.cells[pointer]; 105 | 106 | cell.setChar(char); 107 | cell.setX(x); 108 | cell.setY(y); 109 | cell.setBackground(background.r, background.g, background.b); 110 | cell.setForeground(foreground.r, foreground.g, foreground.b); 111 | cell.setDisplay(display); 112 | cell.isModified = true; 113 | } 114 | 115 | this.cursorX += 1; 116 | } 117 | 118 | return this; 119 | } 120 | 121 | /** 122 | * Flush changes to the real terminal, taking only modified cells. 123 | * Firstly, we get modified cells that have been affected by {@link write} method. 124 | * Secondly, we compare these modified cells with the last frame. 125 | * If cell has changes that doesn't equal to the cell from the last frame - write to the stream. 126 | * 127 | * @returns {Canvas} 128 | */ 129 | public flush (): Canvas { 130 | let payload = ''; 131 | 132 | for (let i = 0; i < this.cells.length; i += 1) { 133 | const cell = this.cells[i]; 134 | 135 | if (cell.isModified) { 136 | cell.isModified = false; 137 | const cellSeq = cell.toString(); 138 | 139 | if (cellSeq !== this.lastFrame[i]) { 140 | this.lastFrame[i] = cellSeq; 141 | payload += cellSeq; 142 | } 143 | } 144 | } 145 | 146 | this.stream.write(payload); 147 | 148 | return this; 149 | } 150 | 151 | /** 152 | * Get index of the virtual terminal representation from (x, y) coordinates. 153 | * 154 | * @param {Number} [x] X coordinate on the terminal 155 | * @param {Number} [y] Y coordinate on the terminal 156 | * @returns {Number} Returns index in the buffer array 157 | * @example 158 | * canvas.getPointerFromXY(0, 0); // returns 0 159 | * canvas.getPointerFromXY(); // x and y in this case is current position of the cursor 160 | */ 161 | public getPointerFromXY (x: number = this.cursorX, y: number = this.cursorY): number { 162 | return y * this.width + x; 163 | } 164 | 165 | /** 166 | * Get (x, y) coordinate from the virtual terminal pointer. 167 | * 168 | * @param {Number} index Index in the buffer 169 | * @returns {Array} Returns an array [x, y] 170 | * @example 171 | * canvas.getXYFromPointer(0); // returns [0, 0] 172 | */ 173 | public getXYFromPointer (index: number): [number, number] { 174 | return [index - Math.floor(index / this.width) * this.width, Math.floor(index / this.width)]; 175 | } 176 | 177 | /** 178 | * Move the cursor up. 179 | * 180 | * @param {Number} [y=1] 181 | * @returns {Canvas} 182 | * @example 183 | * canvas.up(); // moves cursor up by one cell 184 | * canvas.up(5); // moves cursor up by five cells 185 | */ 186 | public up (y = 1): Canvas { 187 | this.cursorY -= Math.floor(y); 188 | return this; 189 | } 190 | 191 | /** 192 | * Move the cursor down. 193 | * 194 | * @param {Number} [y=1] 195 | * @returns {Canvas} 196 | * @example 197 | * canvas.down(); // moves cursor down by one cell 198 | * canvas.down(5); // moves cursor down by five cells 199 | */ 200 | public down (y = 1): Canvas { 201 | this.cursorY += Math.floor(y); 202 | return this; 203 | } 204 | 205 | /** 206 | * Move the cursor right. 207 | * 208 | * @param {Number} [x=1] 209 | * @returns {Canvas} 210 | * @example 211 | * canvas.right(); // moves cursor right by one cell 212 | * canvas.right(5); // moves cursor right by five cells 213 | */ 214 | public right (x = 1): Canvas { 215 | this.cursorX += Math.floor(x); 216 | return this; 217 | } 218 | 219 | /** 220 | * Move the cursor left. 221 | * 222 | * @param {Number} [x=1] 223 | * @returns {Canvas} 224 | * @example 225 | * canvas.left(); // moves cursor left by one cell 226 | * canvas.left(5); // moves cursor left by five cells 227 | */ 228 | public left (x = 1): Canvas { 229 | this.cursorX -= Math.floor(x); 230 | return this; 231 | } 232 | 233 | /** 234 | * Move the cursor position relative to the current coordinates. 235 | * 236 | * @param {Number} x Offset by X coordinate 237 | * @param {Number} y Offset by Y coordinate 238 | * @returns {Canvas} 239 | * @example 240 | * canvas.moveBy(5, 5); // moves cursor to the right and down by five cells 241 | */ 242 | public moveBy (x: number, y: number): Canvas { 243 | if (x < 0) this.left(-x); 244 | if (x > 0) this.right(x); 245 | 246 | if (y < 0) this.up(-y); 247 | if (y > 0) this.down(y); 248 | 249 | return this; 250 | } 251 | 252 | /** 253 | * Set the cursor position by absolute coordinates. 254 | * 255 | * @param {Number} x X coordinate 256 | * @param {Number} y Y coordinate 257 | * @returns {Canvas} 258 | * @example 259 | * canvas.moveTo(10, 10); // moves cursor to the (10, 10) coordinate 260 | */ 261 | public moveTo (x: number, y: number): Canvas { 262 | this.cursorX = Math.floor(x); 263 | this.cursorY = Math.floor(y); 264 | 265 | return this; 266 | } 267 | 268 | /** 269 | * Set the foreground color. 270 | * This color is used when text is rendering. 271 | * 272 | * @param {String} color Color name, rgb, hex or none if you want to disable foreground filling 273 | * @returns {Canvas} 274 | * @example 275 | * canvas.foreground('white'); 276 | * canvas.foreground('#000000'); 277 | * canvas.foreground('rgb(255, 255, 255)'); 278 | * canvas.foreground('none'); // disables foreground filling (will be used default filling) 279 | */ 280 | public foreground (color: string): Canvas { 281 | this.cursorForeground = color === 'none' ? { r: -1, g: -1, b: -1 } : Color.create(color).toRgb(); 282 | return this; 283 | } 284 | 285 | /** 286 | * Set the background color. 287 | * This color is used for filling the whole cell in the TTY. 288 | * 289 | * @param {String} color Color name, rgb, hex or none if you want to disable background filling 290 | * @returns {Canvas} 291 | * @example 292 | * canvas.background('white'); 293 | * canvas.background('#000000'); 294 | * canvas.background('rgb(255, 255, 255)'); 295 | * canvas.background('none'); // disables background filling (will be used default filling) 296 | */ 297 | public background (color: string): Canvas { 298 | this.cursorBackground = color === 'none' ? { r: -1, g: -1, b: -1 } : Color.create(color).toRgb(); 299 | return this; 300 | } 301 | 302 | /** 303 | * Toggle bold display mode. 304 | * 305 | * @param {Boolean} [isBold=true] If false, disables bold mode 306 | * @returns {Canvas} 307 | * @example 308 | * canvas.bold(); // enable bold mode 309 | * canvas.bold(false); // disable bold mode 310 | */ 311 | public bold (isBold = true): Canvas { 312 | this.cursorDisplay.bold = isBold; 313 | return this; 314 | } 315 | 316 | /** 317 | * Toggle dim display mode. 318 | * 319 | * @param {Boolean} [isDim=true] If false, disables dim mode 320 | * @returns {Canvas} 321 | * @example 322 | * canvas.dim(); // enable dim mode 323 | * canvas.dim(false); // disable dim mode 324 | */ 325 | public dim (isDim = true): Canvas { 326 | this.cursorDisplay.dim = isDim; 327 | return this; 328 | } 329 | 330 | /** 331 | * Toggle underlined display mode. 332 | * 333 | * @param {Boolean} [isUnderlined=true] If false, disables underlined mode 334 | * @returns {Canvas} 335 | * @example 336 | * canvas.underlined(); // enable underlined mode 337 | * canvas.underlined(false); // disable underlined mode 338 | */ 339 | public underlined (isUnderlined = true): Canvas { 340 | this.cursorDisplay.underlined = isUnderlined; 341 | return this; 342 | } 343 | 344 | /** 345 | * Toggle blink display mode. 346 | * 347 | * @param {Boolean} [isBlink=true] If false, disables blink mode 348 | * @returns {Canvas} 349 | * @example 350 | * canvas.blink(); // enable blink mode 351 | * canvas.blink(false); // disable blink mode 352 | */ 353 | public blink (isBlink = true): Canvas { 354 | this.cursorDisplay.blink = isBlink; 355 | return this; 356 | } 357 | 358 | /** 359 | * Toggle reverse display mode. 360 | * 361 | * @param {Boolean} [isReverse=true] If false, disables reverse display mode 362 | * @returns {Canvas} 363 | * @example 364 | * canvas.reverse(); // enable reverse mode 365 | * canvas.reverse(false); // disable reverse mode 366 | */ 367 | public reverse (isReverse = true): Canvas { 368 | this.cursorDisplay.reverse = isReverse; 369 | return this; 370 | } 371 | 372 | /** 373 | * Toggle hidden display mode. 374 | * 375 | * @param {Boolean} [isHidden=true] If false, disables hidden display mode 376 | * @returns {Canvas} 377 | * @example 378 | * canvas.hidden(); // enable hidden mode 379 | * canvas.hidden(false); // disable hidden mode 380 | */ 381 | public hidden (isHidden = true): Canvas { 382 | this.cursorDisplay.hidden = isHidden; 383 | return this; 384 | } 385 | 386 | /** 387 | * Erase the specified region. 388 | * The region describes the rectangle shape which need to erase. 389 | * 390 | * @param {Number} x1 391 | * @param {Number} y1 392 | * @param {Number} x2 393 | * @param {Number} y2 394 | * @returns {Canvas} 395 | * @example 396 | * canvas.erase(0, 0, 5, 5); 397 | */ 398 | public erase (x1: number, y1: number, x2: number, y2: number): Canvas { 399 | for (let y = y1; y <= y2; y += 1) { 400 | for (let x = x1; x <= x2; x += 1) { 401 | const pointer = this.getPointerFromXY(x, y); 402 | this.cells[pointer]?.reset(); 403 | } 404 | } 405 | 406 | return this; 407 | } 408 | 409 | /** 410 | * Erase from current position to end of the line. 411 | * 412 | * @returns {Canvas} 413 | * @example 414 | * canvas.eraseToEnd(); 415 | */ 416 | public eraseToEnd (): Canvas { 417 | return this.erase(this.cursorX, this.cursorY, this.width - 1, this.cursorY); 418 | } 419 | 420 | /** 421 | * Erase from current position to start of the line. 422 | * 423 | * @returns {Canvas} 424 | * @example 425 | * canvas.eraseToStart(); 426 | */ 427 | public eraseToStart (): Canvas { 428 | return this.erase(0, this.cursorY, this.cursorX, this.cursorY); 429 | } 430 | 431 | /** 432 | * Erase from current line to down. 433 | * 434 | * @returns {Canvas} 435 | * @example 436 | * canvas.eraseToDown(); 437 | */ 438 | public eraseToDown (): Canvas { 439 | return this.erase(0, this.cursorY, this.width - 1, this.height - 1); 440 | } 441 | 442 | /** 443 | * Erase from current line to up. 444 | * 445 | * @returns {Canvas} 446 | * @example 447 | * canvas.eraseToUp(); 448 | */ 449 | public eraseToUp (): Canvas { 450 | return this.erase(0, 0, this.width - 1, this.cursorY); 451 | } 452 | 453 | /** 454 | * Erase current line. 455 | * 456 | * @returns {Canvas} 457 | * @example 458 | * canvas.eraseLine(); 459 | */ 460 | public eraseLine (): Canvas { 461 | return this.erase(0, this.cursorY, this.width - 1, this.cursorY); 462 | } 463 | 464 | /** 465 | * Erase the entire screen. 466 | * 467 | * @returns {Canvas} 468 | * @example 469 | * canvas.eraseScreen(); 470 | */ 471 | public eraseScreen (): Canvas { 472 | return this.erase(0, 0, this.width - 1, this.height - 1); 473 | } 474 | 475 | /** 476 | * Save current terminal state into the buffer. 477 | * Applies immediately without calling {@link flush} method. 478 | * 479 | * @returns {Canvas} 480 | * @example 481 | * canvas.saveScreen(); 482 | */ 483 | public saveScreen (): Canvas { 484 | this.stream.write(encodeToVT100('[?47h')); 485 | return this; 486 | } 487 | 488 | /** 489 | * Restore terminal state from the buffer. 490 | * Applies immediately without calling {@link flush}. 491 | * 492 | * @returns {Canvas} 493 | * @example 494 | * canvas.restoreScreen(); 495 | */ 496 | public restoreScreen (): Canvas { 497 | this.stream.write(encodeToVT100('[?47l')); 498 | return this; 499 | } 500 | 501 | /** 502 | * Set the terminal cursor invisible. 503 | * Applies immediately without calling {@link flush}. 504 | * 505 | * @returns {Canvas} 506 | * @example 507 | * canvas.hideCursor(); 508 | */ 509 | public hideCursor (): Canvas { 510 | this.stream.write(encodeToVT100('[?25l')); 511 | return this; 512 | } 513 | 514 | /** 515 | * Set the terminal cursor visible. 516 | * Applies immediately without calling {@link flush}. 517 | * 518 | * @returns {Canvas} 519 | * @example 520 | * canvas.showCursor(); 521 | */ 522 | public showCursor (): Canvas { 523 | this.stream.write(encodeToVT100('[?25h')); 524 | return this; 525 | } 526 | 527 | /** 528 | * Reset all terminal settings. 529 | * Applies immediately without calling {@link flush}. 530 | * 531 | * @returns {Canvas} 532 | * @example 533 | * canvas.reset(); 534 | */ 535 | public reset (): Canvas { 536 | this.stream.write(encodeToVT100('c')); 537 | return this; 538 | } 539 | } 540 | -------------------------------------------------------------------------------- /test/Canvas.spec.ts: -------------------------------------------------------------------------------- 1 | import { Canvas } from '../src/canvas/Canvas'; 2 | 3 | describe('canvas', () => { 4 | it('should properly initialize with default arguments', () => { 5 | expect.hasAssertions(); 6 | 7 | const canvas = new Canvas(); 8 | 9 | expect(canvas).toBeInstanceOf(Canvas); 10 | expect(canvas.stream).toStrictEqual(process.stdout); 11 | expect(canvas.width).toStrictEqual(process.stdout.columns); 12 | expect(canvas.height).toStrictEqual(process.stdout.rows); 13 | expect(canvas.cursorX).toStrictEqual(0); 14 | expect(canvas.cursorY).toStrictEqual(0); 15 | expect(canvas.cursorBackground).toStrictEqual({ r: -1, g: -1, b: -1 }); 16 | expect(canvas.cursorForeground).toStrictEqual({ r: -1, g: -1, b: -1 }); 17 | expect(canvas.cursorDisplay.bold).toBe(false); 18 | expect(canvas.cursorDisplay.dim).toBe(false); 19 | expect(canvas.cursorDisplay.underlined).toBe(false); 20 | expect(canvas.cursorDisplay.blink).toBe(false); 21 | expect(canvas.cursorDisplay.reverse).toBe(false); 22 | expect(canvas.cursorDisplay.hidden).toBe(false); 23 | }); 24 | 25 | it('should properly initialize with custom arguments', () => { 26 | expect.hasAssertions(); 27 | 28 | const canvas = new Canvas({ stream: process.stdout, width: 10, height: 20 }); 29 | 30 | expect(canvas).toBeInstanceOf(Canvas); 31 | expect(canvas.stream).toStrictEqual(process.stdout); 32 | expect(canvas.width).toStrictEqual(10); 33 | expect(canvas.height).toStrictEqual(20); 34 | expect(canvas.cursorX).toStrictEqual(0); 35 | expect(canvas.cursorY).toStrictEqual(0); 36 | expect(canvas.cursorBackground).toStrictEqual({ r: -1, g: -1, b: -1 }); 37 | expect(canvas.cursorForeground).toStrictEqual({ r: -1, g: -1, b: -1 }); 38 | expect(canvas.cursorDisplay.bold).toBe(false); 39 | expect(canvas.cursorDisplay.dim).toBe(false); 40 | expect(canvas.cursorDisplay.underlined).toBe(false); 41 | expect(canvas.cursorDisplay.blink).toBe(false); 42 | expect(canvas.cursorDisplay.reverse).toBe(false); 43 | expect(canvas.cursorDisplay.hidden).toBe(false); 44 | expect(canvas.cells).toHaveLength(10 * 20); 45 | }); 46 | 47 | it('should properly initialize with custom width argument', () => { 48 | expect.hasAssertions(); 49 | 50 | const canvas = new Canvas({ width: 10 }); 51 | 52 | expect(canvas.stream).toStrictEqual(process.stdout); 53 | expect(canvas.width).toStrictEqual(10); 54 | expect(canvas.height).toStrictEqual(process.stdout.rows); 55 | }); 56 | 57 | it('should properly initialize with custom height argument', () => { 58 | expect.hasAssertions(); 59 | 60 | const canvas = new Canvas({ height: 10 }); 61 | 62 | expect(canvas.stream).toStrictEqual(process.stdout); 63 | expect(canvas.width).toStrictEqual(process.stdout.columns); 64 | expect(canvas.height).toStrictEqual(10); 65 | }); 66 | 67 | it('should properly initialize the coordinates for the cells', () => { 68 | expect.hasAssertions(); 69 | 70 | const canvas = new Canvas({ width: 2, height: 2 }); 71 | 72 | expect(canvas.cells[0].x).toBe(0); 73 | expect(canvas.cells[0].y).toBe(0); 74 | 75 | expect(canvas.cells[1].x).toBe(1); 76 | expect(canvas.cells[1].y).toBe(0); 77 | 78 | expect(canvas.cells[2].x).toBe(0); 79 | expect(canvas.cells[2].y).toBe(1); 80 | 81 | expect(canvas.cells[3].x).toBe(1); 82 | expect(canvas.cells[3].y).toBe(1); 83 | }); 84 | 85 | it('should properly write to the canvas', () => { 86 | expect.hasAssertions(); 87 | 88 | const canvas = new Canvas({ width: 20, height: 10 }); 89 | expect(canvas.cells[0].getChar()).toStrictEqual(' '); 90 | 91 | canvas.write('test'); 92 | expect(canvas.cells[0].toString()).toStrictEqual('\u001b[1;1ft\u001b[0m'); 93 | expect(canvas.cells[1].toString()).toStrictEqual('\u001b[1;2fe\u001b[0m'); 94 | expect(canvas.cells[2].toString()).toStrictEqual('\u001b[1;3fs\u001b[0m'); 95 | expect(canvas.cells[3].toString()).toStrictEqual('\u001b[1;4ft\u001b[0m'); 96 | }); 97 | 98 | it('should properly ignore write if out of the bounding box', () => { 99 | expect.hasAssertions(); 100 | 101 | const canvas = new Canvas({ width: 20, height: 10 }); 102 | expect(canvas.cells[0].getChar()).toStrictEqual(' '); 103 | 104 | canvas.write('test'); 105 | expect(canvas.cells[0].toString()).toStrictEqual('\u001b[1;1ft\u001b[0m'); 106 | expect(canvas.cells[1].toString()).toStrictEqual('\u001b[1;2fe\u001b[0m'); 107 | expect(canvas.cells[2].toString()).toStrictEqual('\u001b[1;3fs\u001b[0m'); 108 | expect(canvas.cells[3].toString()).toStrictEqual('\u001b[1;4ft\u001b[0m'); 109 | 110 | canvas.moveTo(-5, -5).write('do not print'); 111 | expect(canvas.cells[0].toString()).toStrictEqual('\u001b[1;1ft\u001b[0m'); 112 | expect(canvas.cells[1].toString()).toStrictEqual('\u001b[1;2fe\u001b[0m'); 113 | expect(canvas.cells[2].toString()).toStrictEqual('\u001b[1;3fs\u001b[0m'); 114 | expect(canvas.cells[3].toString()).toStrictEqual('\u001b[1;4ft\u001b[0m'); 115 | expect(canvas.cells[4].getChar()).toStrictEqual(' '); 116 | }); 117 | 118 | it('should properly flush the buffer into the stream', () => { 119 | expect.hasAssertions(); 120 | 121 | const canvas = new Canvas({ width: 20, height: 10 }); 122 | const spy = jest.spyOn(process.stdout, 'write'); 123 | 124 | expect(canvas.write('test')).toBeInstanceOf(Canvas); 125 | expect(canvas.flush()).toBeInstanceOf(Canvas); 126 | expect(canvas.moveTo(0, 0).write('1234')).toBeInstanceOf(Canvas); 127 | expect(canvas.flush()).toBeInstanceOf(Canvas); 128 | expect(spy.mock.calls).toHaveLength(2); 129 | }); 130 | 131 | it('should properly skip the flush when changes the same', () => { 132 | expect.hasAssertions(); 133 | 134 | const canvas = new Canvas({ width: 20, height: 10 }); 135 | const spy = jest.spyOn(process.stdout, 'write'); 136 | 137 | expect(canvas.write('test')).toBeInstanceOf(Canvas); 138 | expect(canvas.flush()).toBeInstanceOf(Canvas); 139 | expect(canvas.moveTo(0, 0).write('test')).toBeInstanceOf(Canvas); 140 | expect(canvas.flush()).toBeInstanceOf(Canvas); 141 | expect(spy.mock.calls).toHaveLength(2); 142 | }); 143 | 144 | it('should properly calculate buffer pointer', () => { 145 | expect.hasAssertions(); 146 | 147 | const canvas = new Canvas({ width: 20, height: 10 }); 148 | 149 | expect(canvas.getPointerFromXY()).toStrictEqual(0); 150 | expect(canvas.moveTo(10, 10)).toBeInstanceOf(Canvas); 151 | expect(canvas.getPointerFromXY()).toStrictEqual(10 * 20 + 10); 152 | expect(canvas.getPointerFromXY(20, 20)).toStrictEqual(20 * 20 + 20); 153 | }); 154 | 155 | it('should properly calculate coordinates from buffer pointer', () => { 156 | expect.hasAssertions(); 157 | 158 | const canvas = new Canvas({ width: 20, height: 10 }); 159 | 160 | expect(canvas.getXYFromPointer(0)).toStrictEqual([0, 0]); 161 | expect(canvas.getXYFromPointer(1)).toStrictEqual([1, 0]); 162 | expect(canvas.getXYFromPointer(10)).toStrictEqual([10, 0]); 163 | expect(canvas.getXYFromPointer(200)).toStrictEqual([ 164 | 200 - Math.floor(200 / canvas.width) * canvas.width, Math.floor(200 / canvas.width), 165 | ]); 166 | }); 167 | 168 | it('should properly move cursor up with default arguments', () => { 169 | expect.hasAssertions(); 170 | 171 | const canvas = new Canvas({ width: 20, height: 10 }); 172 | 173 | expect(canvas.cursorY).toStrictEqual(0); 174 | expect(canvas.up()).toBeInstanceOf(Canvas); 175 | expect(canvas.cursorY).toStrictEqual(-1); 176 | }); 177 | 178 | it('should properly move cursor up with custom arguments', () => { 179 | expect.hasAssertions(); 180 | 181 | const canvas = new Canvas({ width: 20, height: 10 }); 182 | 183 | expect(canvas.cursorY).toStrictEqual(0); 184 | expect(canvas.up(5)).toBeInstanceOf(Canvas); 185 | expect(canvas.cursorY).toStrictEqual(-5); 186 | }); 187 | 188 | it('should properly move cursor down with default arguments', () => { 189 | expect.hasAssertions(); 190 | 191 | const canvas = new Canvas({ width: 20, height: 10 }); 192 | 193 | expect(canvas.cursorY).toStrictEqual(0); 194 | expect(canvas.down()).toBeInstanceOf(Canvas); 195 | expect(canvas.cursorY).toStrictEqual(1); 196 | }); 197 | 198 | it('should properly move cursor down with custom arguments', () => { 199 | expect.hasAssertions(); 200 | 201 | const canvas = new Canvas({ width: 20, height: 10 }); 202 | 203 | expect(canvas.cursorY).toStrictEqual(0); 204 | expect(canvas.down(5)).toBeInstanceOf(Canvas); 205 | expect(canvas.cursorY).toStrictEqual(5); 206 | }); 207 | 208 | it('should properly move cursor right with default arguments', () => { 209 | expect.hasAssertions(); 210 | 211 | const canvas = new Canvas({ width: 20, height: 10 }); 212 | 213 | expect(canvas.cursorX).toStrictEqual(0); 214 | expect(canvas.right()).toBeInstanceOf(Canvas); 215 | expect(canvas.cursorX).toStrictEqual(1); 216 | }); 217 | 218 | it('should properly move cursor right with custom arguments', () => { 219 | expect.hasAssertions(); 220 | 221 | const canvas = new Canvas({ width: 20, height: 10 }); 222 | 223 | expect(canvas.cursorX).toStrictEqual(0); 224 | expect(canvas.right(5)).toBeInstanceOf(Canvas); 225 | expect(canvas.cursorX).toStrictEqual(5); 226 | }); 227 | 228 | it('should properly move cursor left with default arguments', () => { 229 | expect.hasAssertions(); 230 | 231 | const canvas = new Canvas({ width: 20, height: 10 }); 232 | 233 | expect(canvas.cursorX).toStrictEqual(0); 234 | expect(canvas.left()).toBeInstanceOf(Canvas); 235 | expect(canvas.cursorX).toStrictEqual(-1); 236 | }); 237 | 238 | it('should properly move cursor left with custom arguments', () => { 239 | expect.hasAssertions(); 240 | 241 | const canvas = new Canvas({ width: 20, height: 10 }); 242 | 243 | expect(canvas.cursorX).toStrictEqual(0); 244 | expect(canvas.left(5)).toBeInstanceOf(Canvas); 245 | expect(canvas.cursorX).toStrictEqual(-5); 246 | }); 247 | 248 | it('should properly set relative position of cursor', () => { 249 | expect.hasAssertions(); 250 | 251 | const canvas = new Canvas({ width: 20, height: 10 }); 252 | const rightSpy = jest.spyOn(canvas, 'right'); 253 | const downSpy = jest.spyOn(canvas, 'down'); 254 | const leftSpy = jest.spyOn(canvas, 'left'); 255 | const upSpy = jest.spyOn(canvas, 'up'); 256 | 257 | expect(canvas.moveBy(10, 20)).toBeInstanceOf(Canvas); 258 | expect(canvas.moveBy(-5, -10)).toBeInstanceOf(Canvas); 259 | expect(rightSpy.mock.calls[0][0]).toStrictEqual(10); 260 | expect(downSpy.mock.calls[0][0]).toStrictEqual(20); 261 | expect(leftSpy.mock.calls[0][0]).toStrictEqual(5); 262 | expect(upSpy.mock.calls[0][0]).toStrictEqual(10); 263 | }); 264 | 265 | it('should properly set absolute position of cursor', () => { 266 | expect.hasAssertions(); 267 | 268 | const canvas = new Canvas({ width: 20, height: 10 }); 269 | 270 | expect(canvas.cursorX).toStrictEqual(0); 271 | expect(canvas.cursorY).toStrictEqual(0); 272 | expect(canvas.moveTo(5, 10)).toBeInstanceOf(Canvas); 273 | expect(canvas.cursorX).toStrictEqual(5); 274 | expect(canvas.cursorY).toStrictEqual(10); 275 | }); 276 | 277 | it('should properly change foreground color', () => { 278 | expect.hasAssertions(); 279 | 280 | const canvas = new Canvas({ width: 20, height: 10 }); 281 | 282 | expect(canvas.cursorForeground).toStrictEqual({ r: -1, g: -1, b: -1 }); 283 | expect(canvas.foreground('white')).toBeInstanceOf(Canvas); 284 | expect(canvas.cursorForeground).toStrictEqual({ r: 255, g: 255, b: 255 }); 285 | expect(canvas.foreground('none')).toBeInstanceOf(Canvas); 286 | expect(canvas.cursorForeground).toStrictEqual({ r: -1, g: -1, b: -1 }); 287 | }); 288 | 289 | it('should properly change background color', () => { 290 | expect.hasAssertions(); 291 | 292 | const canvas = new Canvas({ width: 20, height: 10 }); 293 | 294 | expect(canvas.cursorBackground).toStrictEqual({ r: -1, g: -1, b: -1 }); 295 | expect(canvas.background('black')).toBeInstanceOf(Canvas); 296 | expect(canvas.cursorBackground).toStrictEqual({ r: 0, g: 0, b: 0 }); 297 | expect(canvas.background('none')).toBeInstanceOf(Canvas); 298 | expect(canvas.cursorBackground).toStrictEqual({ r: -1, g: -1, b: -1 }); 299 | }); 300 | 301 | it('should properly toggle bold mode', () => { 302 | expect.hasAssertions(); 303 | 304 | const canvas = new Canvas({ width: 20, height: 10 }); 305 | 306 | expect(canvas.bold()).toBeInstanceOf(Canvas); 307 | expect(canvas.cursorDisplay.bold).toBe(true); 308 | expect(canvas.bold(false)).toBeInstanceOf(Canvas); 309 | expect(canvas.cursorDisplay.bold).toBe(false); 310 | }); 311 | 312 | it('should properly toggle dim mode', () => { 313 | expect.hasAssertions(); 314 | 315 | const canvas = new Canvas({ width: 20, height: 10 }); 316 | 317 | expect(canvas.dim()).toBeInstanceOf(Canvas); 318 | expect(canvas.cursorDisplay.dim).toBe(true); 319 | expect(canvas.dim(false)).toBeInstanceOf(Canvas); 320 | expect(canvas.cursorDisplay.dim).toBe(false); 321 | }); 322 | 323 | it('should properly toggle underlined mode', () => { 324 | expect.hasAssertions(); 325 | 326 | const canvas = new Canvas({ width: 20, height: 10 }); 327 | 328 | expect(canvas.underlined()).toBeInstanceOf(Canvas); 329 | expect(canvas.cursorDisplay.underlined).toBe(true); 330 | expect(canvas.underlined(false)).toBeInstanceOf(Canvas); 331 | expect(canvas.cursorDisplay.underlined).toBe(false); 332 | }); 333 | 334 | it('should properly toggle blink mode', () => { 335 | expect.hasAssertions(); 336 | 337 | const canvas = new Canvas({ width: 20, height: 10 }); 338 | 339 | expect(canvas.blink()).toBeInstanceOf(Canvas); 340 | expect(canvas.cursorDisplay.blink).toBe(true); 341 | expect(canvas.blink(false)).toBeInstanceOf(Canvas); 342 | expect(canvas.cursorDisplay.blink).toBe(false); 343 | }); 344 | 345 | it('should properly toggle reverse mode', () => { 346 | expect.hasAssertions(); 347 | 348 | const canvas = new Canvas({ width: 20, height: 10 }); 349 | 350 | expect(canvas.reverse()).toBeInstanceOf(Canvas); 351 | expect(canvas.cursorDisplay.reverse).toBe(true); 352 | expect(canvas.reverse(false)).toBeInstanceOf(Canvas); 353 | expect(canvas.cursorDisplay.reverse).toBe(false); 354 | }); 355 | 356 | it('should properly toggle hidden mode', () => { 357 | expect.hasAssertions(); 358 | 359 | const canvas = new Canvas({ width: 20, height: 10 }); 360 | 361 | expect(canvas.hidden()).toBeInstanceOf(Canvas); 362 | expect(canvas.cursorDisplay.hidden).toBe(true); 363 | expect(canvas.hidden(false)).toBeInstanceOf(Canvas); 364 | expect(canvas.cursorDisplay.hidden).toBe(false); 365 | }); 366 | 367 | it('should properly erase the specified region', () => { 368 | expect.hasAssertions(); 369 | 370 | const canvas = new Canvas({ width: 20, height: 10 }); 371 | const spy = jest.spyOn(canvas, 'getPointerFromXY').mockImplementation(() => 0); 372 | 373 | expect(canvas.moveTo(5, 5).erase(0, 0, 5, 5)).toBeInstanceOf(Canvas); 374 | expect(spy.mock.calls).toHaveLength(36); 375 | }); 376 | 377 | it('should properly ignore reset method on the cell if out of boundaries', () => { 378 | expect.hasAssertions(); 379 | 380 | const canvas = new Canvas({ width: 20, height: 10 }); 381 | expect(canvas.erase(0, 0, 30, 30)).toBeInstanceOf(Canvas); 382 | }); 383 | 384 | it('should properly erase from current position to the end of line', () => { 385 | expect.hasAssertions(); 386 | 387 | const canvas = new Canvas({ width: 20, height: 10 }); 388 | const spy = jest.spyOn(canvas, 'erase').mockImplementation(() => canvas); 389 | 390 | expect(canvas.moveTo(5, 5).eraseToEnd()).toBeInstanceOf(Canvas); 391 | expect(spy.mock.calls[0]).toMatchObject([5, 5, 20 - 1, 5]); 392 | }); 393 | 394 | it('should properly erase from current position to the start of line', () => { 395 | expect.hasAssertions(); 396 | 397 | const canvas = new Canvas({ width: 20, height: 10 }); 398 | const spy = jest.spyOn(canvas, 'erase').mockImplementation(() => canvas); 399 | 400 | expect(canvas.moveTo(5, 5).eraseToStart()).toBeInstanceOf(Canvas); 401 | expect(spy.mock.calls[0]).toMatchObject([0, 5, 5, 5]); 402 | }); 403 | 404 | it('should properly erase from current line to down', () => { 405 | expect.hasAssertions(); 406 | 407 | const canvas = new Canvas({ width: 20, height: 10 }); 408 | const spy = jest.spyOn(canvas, 'erase').mockImplementation(() => canvas); 409 | 410 | expect(canvas.moveTo(5, 5).eraseToDown()).toBeInstanceOf(Canvas); 411 | expect(spy.mock.calls[0]).toMatchObject([0, 5, 20 - 1, 10 - 1]); 412 | }); 413 | 414 | it('should properly erase from current line to up', () => { 415 | expect.hasAssertions(); 416 | 417 | const canvas = new Canvas({ width: 20, height: 10 }); 418 | const spy = jest.spyOn(canvas, 'erase').mockImplementation(() => canvas); 419 | 420 | expect(canvas.moveTo(5, 5).eraseToUp()).toBeInstanceOf(Canvas); 421 | expect(spy.mock.calls[0]).toMatchObject([0, 0, 20 - 1, 5]); 422 | }); 423 | 424 | it('should properly erase the current line', () => { 425 | expect.hasAssertions(); 426 | 427 | const canvas = new Canvas({ width: 20, height: 10 }); 428 | const spy = jest.spyOn(canvas, 'erase').mockImplementation(() => canvas); 429 | 430 | expect(canvas.moveTo(5, 5).eraseLine()).toBeInstanceOf(Canvas); 431 | expect(spy.mock.calls[0]).toMatchObject([0, 5, 20 - 1, 5]); 432 | }); 433 | 434 | it('should properly erase the entire screen', () => { 435 | expect.hasAssertions(); 436 | 437 | const canvas = new Canvas({ width: 20, height: 10 }); 438 | const spy = jest.spyOn(canvas, 'erase').mockImplementation(() => canvas); 439 | 440 | expect(canvas.eraseScreen()).toBeInstanceOf(Canvas); 441 | expect(spy.mock.calls[0]).toMatchObject([0, 0, 20 - 1, 10 - 1]); 442 | }); 443 | 444 | it('should properly save the screen contents', () => { 445 | expect.hasAssertions(); 446 | 447 | const canvas = new Canvas({ width: 20, height: 10 }); 448 | const spy = jest.spyOn(process.stdout, 'write'); 449 | 450 | expect(canvas.saveScreen()).toBeInstanceOf(Canvas); 451 | expect(spy.mock.calls).toHaveLength(1); 452 | expect(spy.mock.calls[0][0]).toStrictEqual('\u001b[?47h'); 453 | }); 454 | 455 | it('should properly restore the screen contents', () => { 456 | expect.hasAssertions(); 457 | 458 | const canvas = new Canvas({ width: 20, height: 10 }); 459 | const spy = jest.spyOn(process.stdout, 'write'); 460 | 461 | expect(canvas.restoreScreen()).toBeInstanceOf(Canvas); 462 | expect(spy.mock.calls).toHaveLength(1); 463 | expect(spy.mock.calls[0][0]).toStrictEqual('\u001b[?47l'); 464 | }); 465 | 466 | it('should properly hide the cursor', () => { 467 | expect.hasAssertions(); 468 | 469 | const canvas = new Canvas({ width: 20, height: 10 }); 470 | const spy = jest.spyOn(process.stdout, 'write'); 471 | 472 | expect(canvas.hideCursor()).toBeInstanceOf(Canvas); 473 | expect(spy.mock.calls).toHaveLength(1); 474 | expect(spy.mock.calls[0][0]).toStrictEqual('\u001b[?25l'); 475 | }); 476 | 477 | it('should properly show the cursor', () => { 478 | expect.hasAssertions(); 479 | 480 | const canvas = new Canvas({ width: 20, height: 10 }); 481 | const spy = jest.spyOn(process.stdout, 'write'); 482 | 483 | expect(canvas.showCursor()).toBeInstanceOf(Canvas); 484 | expect(spy.mock.calls).toHaveLength(1); 485 | expect(spy.mock.calls[0][0]).toStrictEqual('\u001b[?25h'); 486 | }); 487 | 488 | it('should properly reset the TTY state', () => { 489 | expect.hasAssertions(); 490 | 491 | const canvas = new Canvas({ width: 20, height: 10 }); 492 | const spy = jest.spyOn(process.stdout, 'write'); 493 | 494 | expect(canvas.reset()).toBeInstanceOf(Canvas); 495 | expect(spy.mock.calls).toHaveLength(1); 496 | expect(spy.mock.calls[0][0]).toStrictEqual('\u001bc'); 497 | }); 498 | 499 | it('should properly create new instance from static create()', () => { 500 | expect.hasAssertions(); 501 | 502 | const canvas = Canvas.create(); 503 | 504 | expect(canvas).toBeInstanceOf(Canvas); 505 | expect(canvas.stream).toStrictEqual(process.stdout); 506 | expect(canvas.width).toStrictEqual(process.stdout.columns); 507 | expect(canvas.height).toStrictEqual(process.stdout.rows); 508 | expect(canvas.cursorX).toStrictEqual(0); 509 | expect(canvas.cursorY).toStrictEqual(0); 510 | expect(canvas.cursorBackground).toStrictEqual({ r: -1, g: -1, b: -1 }); 511 | expect(canvas.cursorForeground).toStrictEqual({ r: -1, g: -1, b: -1 }); 512 | expect(canvas.cursorDisplay.bold).toBe(false); 513 | expect(canvas.cursorDisplay.dim).toBe(false); 514 | expect(canvas.cursorDisplay.underlined).toBe(false); 515 | expect(canvas.cursorDisplay.blink).toBe(false); 516 | expect(canvas.cursorDisplay.reverse).toBe(false); 517 | expect(canvas.cursorDisplay.hidden).toBe(false); 518 | }); 519 | }); 520 | -------------------------------------------------------------------------------- /docs/assets/js/search.json: -------------------------------------------------------------------------------- 1 | {"kinds":{"32":"Variable","64":"Function","128":"Class","256":"Interface","512":"Constructor","1024":"Property","2048":"Method","2097152":"Object literal"},"rows":[{"id":0,"kind":32,"name":"HEX_REGEX","url":"globals.html#hex_regex","classes":"tsd-kind-variable"},{"id":1,"kind":32,"name":"NAMED_COLORS","url":"globals.html#named_colors","classes":"tsd-kind-variable"},{"id":2,"kind":32,"name":"RGB_REGEX","url":"globals.html#rgb_regex","classes":"tsd-kind-variable"},{"id":3,"kind":256,"name":"IColor","url":"interfaces/icolor.html","classes":"tsd-kind-interface"},{"id":4,"kind":1024,"name":"r","url":"interfaces/icolor.html#r","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"IColor"},{"id":5,"kind":1024,"name":"g","url":"interfaces/icolor.html#g","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"IColor"},{"id":6,"kind":1024,"name":"b","url":"interfaces/icolor.html#b","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"IColor"},{"id":7,"kind":128,"name":"Color","url":"classes/color.html","classes":"tsd-kind-class"},{"id":8,"kind":1024,"name":"r","url":"classes/color.html#r","classes":"tsd-kind-property tsd-parent-kind-class","parent":"Color"},{"id":9,"kind":1024,"name":"g","url":"classes/color.html#g","classes":"tsd-kind-property tsd-parent-kind-class","parent":"Color"},{"id":10,"kind":1024,"name":"b","url":"classes/color.html#b","classes":"tsd-kind-property tsd-parent-kind-class","parent":"Color"},{"id":11,"kind":512,"name":"constructor","url":"classes/color.html#constructor","classes":"tsd-kind-constructor tsd-parent-kind-class","parent":"Color"},{"id":12,"kind":2048,"name":"isNamed","url":"classes/color.html#isnamed","classes":"tsd-kind-method tsd-parent-kind-class tsd-is-static","parent":"Color"},{"id":13,"kind":2048,"name":"isRgb","url":"classes/color.html#isrgb","classes":"tsd-kind-method tsd-parent-kind-class tsd-is-static","parent":"Color"},{"id":14,"kind":2048,"name":"isHex","url":"classes/color.html#ishex","classes":"tsd-kind-method tsd-parent-kind-class tsd-is-static","parent":"Color"},{"id":15,"kind":2048,"name":"fromRgb","url":"classes/color.html#fromrgb","classes":"tsd-kind-method tsd-parent-kind-class tsd-is-static","parent":"Color"},{"id":16,"kind":2048,"name":"fromHex","url":"classes/color.html#fromhex","classes":"tsd-kind-method tsd-parent-kind-class tsd-is-static","parent":"Color"},{"id":17,"kind":2048,"name":"create","url":"classes/color.html#create","classes":"tsd-kind-method tsd-parent-kind-class tsd-is-static","parent":"Color"},{"id":18,"kind":2048,"name":"getR","url":"classes/color.html#getr","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Color"},{"id":19,"kind":2048,"name":"setR","url":"classes/color.html#setr","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Color"},{"id":20,"kind":2048,"name":"getG","url":"classes/color.html#getg","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Color"},{"id":21,"kind":2048,"name":"setG","url":"classes/color.html#setg","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Color"},{"id":22,"kind":2048,"name":"getB","url":"classes/color.html#getb","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Color"},{"id":23,"kind":2048,"name":"setB","url":"classes/color.html#setb","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Color"},{"id":24,"kind":2048,"name":"toRgb","url":"classes/color.html#torgb","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Color"},{"id":25,"kind":2048,"name":"toHex","url":"classes/color.html#tohex","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Color"},{"id":26,"kind":2097152,"name":"DISPLAY_MODES","url":"globals.html#display_modes","classes":"tsd-kind-object-literal"},{"id":27,"kind":32,"name":"RESET_ALL","url":"globals.html#display_modes.reset_all","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"DISPLAY_MODES"},{"id":28,"kind":32,"name":"BOLD","url":"globals.html#display_modes.bold","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"DISPLAY_MODES"},{"id":29,"kind":32,"name":"DIM","url":"globals.html#display_modes.dim","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"DISPLAY_MODES"},{"id":30,"kind":32,"name":"UNDERLINED","url":"globals.html#display_modes.underlined","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"DISPLAY_MODES"},{"id":31,"kind":32,"name":"BLINK","url":"globals.html#display_modes.blink","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"DISPLAY_MODES"},{"id":32,"kind":32,"name":"REVERSE","url":"globals.html#display_modes.reverse","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"DISPLAY_MODES"},{"id":33,"kind":32,"name":"HIDDEN","url":"globals.html#display_modes.hidden","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"DISPLAY_MODES"},{"id":34,"kind":32,"name":"RESET_BOLD","url":"globals.html#display_modes.reset_bold","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"DISPLAY_MODES"},{"id":35,"kind":32,"name":"RESET_DIM","url":"globals.html#display_modes.reset_dim","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"DISPLAY_MODES"},{"id":36,"kind":32,"name":"RESET_UNDERLINED","url":"globals.html#display_modes.reset_underlined","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"DISPLAY_MODES"},{"id":37,"kind":32,"name":"RESET_BLINK","url":"globals.html#display_modes.reset_blink","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"DISPLAY_MODES"},{"id":38,"kind":32,"name":"RESET_REVERSE","url":"globals.html#display_modes.reset_reverse","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"DISPLAY_MODES"},{"id":39,"kind":32,"name":"RESET_HIDDEN","url":"globals.html#display_modes.reset_hidden","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"DISPLAY_MODES"},{"id":40,"kind":256,"name":"IDisplayOptions","url":"interfaces/idisplayoptions.html","classes":"tsd-kind-interface"},{"id":41,"kind":1024,"name":"bold","url":"interfaces/idisplayoptions.html#bold","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"IDisplayOptions"},{"id":42,"kind":1024,"name":"dim","url":"interfaces/idisplayoptions.html#dim","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"IDisplayOptions"},{"id":43,"kind":1024,"name":"underlined","url":"interfaces/idisplayoptions.html#underlined","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"IDisplayOptions"},{"id":44,"kind":1024,"name":"blink","url":"interfaces/idisplayoptions.html#blink","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"IDisplayOptions"},{"id":45,"kind":1024,"name":"reverse","url":"interfaces/idisplayoptions.html#reverse","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"IDisplayOptions"},{"id":46,"kind":1024,"name":"hidden","url":"interfaces/idisplayoptions.html#hidden","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"IDisplayOptions"},{"id":47,"kind":256,"name":"ICellOptions","url":"interfaces/icelloptions.html","classes":"tsd-kind-interface"},{"id":48,"kind":1024,"name":"x","url":"interfaces/icelloptions.html#x","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"ICellOptions"},{"id":49,"kind":1024,"name":"y","url":"interfaces/icelloptions.html#y","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"ICellOptions"},{"id":50,"kind":1024,"name":"background","url":"interfaces/icelloptions.html#background","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"ICellOptions"},{"id":51,"kind":1024,"name":"foreground","url":"interfaces/icelloptions.html#foreground","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"ICellOptions"},{"id":52,"kind":1024,"name":"display","url":"interfaces/icelloptions.html#display","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"ICellOptions"},{"id":53,"kind":64,"name":"encodeToVT100","url":"globals.html#encodetovt100","classes":"tsd-kind-function"},{"id":54,"kind":128,"name":"Cell","url":"classes/cell.html","classes":"tsd-kind-class"},{"id":55,"kind":1024,"name":"isModified","url":"classes/cell.html#ismodified","classes":"tsd-kind-property tsd-parent-kind-class","parent":"Cell"},{"id":56,"kind":1024,"name":"char","url":"classes/cell.html#char","classes":"tsd-kind-property tsd-parent-kind-class","parent":"Cell"},{"id":57,"kind":1024,"name":"x","url":"classes/cell.html#x","classes":"tsd-kind-property tsd-parent-kind-class","parent":"Cell"},{"id":58,"kind":1024,"name":"y","url":"classes/cell.html#y","classes":"tsd-kind-property tsd-parent-kind-class","parent":"Cell"},{"id":59,"kind":2097152,"name":"background","url":"classes/cell.html#background","classes":"tsd-kind-object-literal tsd-parent-kind-class","parent":"Cell"},{"id":60,"kind":32,"name":"r","url":"classes/cell.html#background.r","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Cell.background"},{"id":61,"kind":32,"name":"g","url":"classes/cell.html#background.g","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Cell.background"},{"id":62,"kind":32,"name":"b","url":"classes/cell.html#background.b","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Cell.background"},{"id":63,"kind":2097152,"name":"foreground","url":"classes/cell.html#foreground","classes":"tsd-kind-object-literal tsd-parent-kind-class","parent":"Cell"},{"id":64,"kind":32,"name":"r","url":"classes/cell.html#foreground.r-1","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Cell.foreground"},{"id":65,"kind":32,"name":"g","url":"classes/cell.html#foreground.g-1","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Cell.foreground"},{"id":66,"kind":32,"name":"b","url":"classes/cell.html#foreground.b-1","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Cell.foreground"},{"id":67,"kind":2097152,"name":"display","url":"classes/cell.html#display","classes":"tsd-kind-object-literal tsd-parent-kind-class","parent":"Cell"},{"id":68,"kind":32,"name":"blink","url":"classes/cell.html#display.blink","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Cell.display"},{"id":69,"kind":32,"name":"bold","url":"classes/cell.html#display.bold","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Cell.display"},{"id":70,"kind":32,"name":"dim","url":"classes/cell.html#display.dim","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Cell.display"},{"id":71,"kind":32,"name":"hidden","url":"classes/cell.html#display.hidden","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Cell.display"},{"id":72,"kind":32,"name":"reverse","url":"classes/cell.html#display.reverse","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Cell.display"},{"id":73,"kind":32,"name":"underlined","url":"classes/cell.html#display.underlined","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Cell.display"},{"id":74,"kind":512,"name":"constructor","url":"classes/cell.html#constructor","classes":"tsd-kind-constructor tsd-parent-kind-class","parent":"Cell"},{"id":75,"kind":2048,"name":"create","url":"classes/cell.html#create","classes":"tsd-kind-method tsd-parent-kind-class tsd-is-static","parent":"Cell"},{"id":76,"kind":2048,"name":"getChar","url":"classes/cell.html#getchar","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Cell"},{"id":77,"kind":2048,"name":"setChar","url":"classes/cell.html#setchar","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Cell"},{"id":78,"kind":2048,"name":"getX","url":"classes/cell.html#getx","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Cell"},{"id":79,"kind":2048,"name":"setX","url":"classes/cell.html#setx","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Cell"},{"id":80,"kind":2048,"name":"getY","url":"classes/cell.html#gety","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Cell"},{"id":81,"kind":2048,"name":"setY","url":"classes/cell.html#sety","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Cell"},{"id":82,"kind":2048,"name":"getBackground","url":"classes/cell.html#getbackground","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Cell"},{"id":83,"kind":2048,"name":"setBackground","url":"classes/cell.html#setbackground","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Cell"},{"id":84,"kind":2048,"name":"resetBackground","url":"classes/cell.html#resetbackground","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Cell"},{"id":85,"kind":2048,"name":"getForeground","url":"classes/cell.html#getforeground","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Cell"},{"id":86,"kind":2048,"name":"setForeground","url":"classes/cell.html#setforeground","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Cell"},{"id":87,"kind":2048,"name":"resetForeground","url":"classes/cell.html#resetforeground","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Cell"},{"id":88,"kind":2048,"name":"getDisplay","url":"classes/cell.html#getdisplay","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Cell"},{"id":89,"kind":2048,"name":"setDisplay","url":"classes/cell.html#setdisplay","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Cell"},{"id":90,"kind":2048,"name":"resetDisplay","url":"classes/cell.html#resetdisplay","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Cell"},{"id":91,"kind":2048,"name":"reset","url":"classes/cell.html#reset","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Cell"},{"id":92,"kind":2048,"name":"toString","url":"classes/cell.html#tostring","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Cell"},{"id":93,"kind":256,"name":"ICanvasOptions","url":"interfaces/icanvasoptions.html","classes":"tsd-kind-interface"},{"id":94,"kind":1024,"name":"stream","url":"interfaces/icanvasoptions.html#stream","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"ICanvasOptions"},{"id":95,"kind":1024,"name":"width","url":"interfaces/icanvasoptions.html#width","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"ICanvasOptions"},{"id":96,"kind":1024,"name":"height","url":"interfaces/icanvasoptions.html#height","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"ICanvasOptions"},{"id":97,"kind":128,"name":"Canvas","url":"classes/canvas.html","classes":"tsd-kind-class"},{"id":98,"kind":1024,"name":"cells","url":"classes/canvas.html#cells","classes":"tsd-kind-property tsd-parent-kind-class","parent":"Canvas"},{"id":99,"kind":1024,"name":"lastFrame","url":"classes/canvas.html#lastframe","classes":"tsd-kind-property tsd-parent-kind-class","parent":"Canvas"},{"id":100,"kind":1024,"name":"stream","url":"classes/canvas.html#stream","classes":"tsd-kind-property tsd-parent-kind-class","parent":"Canvas"},{"id":101,"kind":1024,"name":"width","url":"classes/canvas.html#width","classes":"tsd-kind-property tsd-parent-kind-class","parent":"Canvas"},{"id":102,"kind":1024,"name":"height","url":"classes/canvas.html#height","classes":"tsd-kind-property tsd-parent-kind-class","parent":"Canvas"},{"id":103,"kind":1024,"name":"cursorX","url":"classes/canvas.html#cursorx","classes":"tsd-kind-property tsd-parent-kind-class","parent":"Canvas"},{"id":104,"kind":1024,"name":"cursorY","url":"classes/canvas.html#cursory","classes":"tsd-kind-property tsd-parent-kind-class","parent":"Canvas"},{"id":105,"kind":2097152,"name":"cursorBackground","url":"classes/canvas.html#cursorbackground","classes":"tsd-kind-object-literal tsd-parent-kind-class","parent":"Canvas"},{"id":106,"kind":32,"name":"r","url":"classes/canvas.html#cursorbackground.r","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Canvas.cursorBackground"},{"id":107,"kind":32,"name":"g","url":"classes/canvas.html#cursorbackground.g","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Canvas.cursorBackground"},{"id":108,"kind":32,"name":"b","url":"classes/canvas.html#cursorbackground.b","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Canvas.cursorBackground"},{"id":109,"kind":2097152,"name":"cursorForeground","url":"classes/canvas.html#cursorforeground","classes":"tsd-kind-object-literal tsd-parent-kind-class","parent":"Canvas"},{"id":110,"kind":32,"name":"r","url":"classes/canvas.html#cursorforeground.r-1","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Canvas.cursorForeground"},{"id":111,"kind":32,"name":"g","url":"classes/canvas.html#cursorforeground.g-1","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Canvas.cursorForeground"},{"id":112,"kind":32,"name":"b","url":"classes/canvas.html#cursorforeground.b-1","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Canvas.cursorForeground"},{"id":113,"kind":2097152,"name":"cursorDisplay","url":"classes/canvas.html#cursordisplay","classes":"tsd-kind-object-literal tsd-parent-kind-class","parent":"Canvas"},{"id":114,"kind":32,"name":"blink","url":"classes/canvas.html#cursordisplay.blink-1","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Canvas.cursorDisplay"},{"id":115,"kind":32,"name":"bold","url":"classes/canvas.html#cursordisplay.bold-1","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Canvas.cursorDisplay"},{"id":116,"kind":32,"name":"dim","url":"classes/canvas.html#cursordisplay.dim-1","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Canvas.cursorDisplay"},{"id":117,"kind":32,"name":"hidden","url":"classes/canvas.html#cursordisplay.hidden-1","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Canvas.cursorDisplay"},{"id":118,"kind":32,"name":"reverse","url":"classes/canvas.html#cursordisplay.reverse-1","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Canvas.cursorDisplay"},{"id":119,"kind":32,"name":"underlined","url":"classes/canvas.html#cursordisplay.underlined-1","classes":"tsd-kind-variable tsd-parent-kind-object-literal","parent":"Canvas.cursorDisplay"},{"id":120,"kind":512,"name":"constructor","url":"classes/canvas.html#constructor","classes":"tsd-kind-constructor tsd-parent-kind-class","parent":"Canvas"},{"id":121,"kind":2048,"name":"create","url":"classes/canvas.html#create","classes":"tsd-kind-method tsd-parent-kind-class tsd-is-static","parent":"Canvas"},{"id":122,"kind":2048,"name":"write","url":"classes/canvas.html#write","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":123,"kind":2048,"name":"flush","url":"classes/canvas.html#flush","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":124,"kind":2048,"name":"getPointerFromXY","url":"classes/canvas.html#getpointerfromxy","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":125,"kind":2048,"name":"getXYFromPointer","url":"classes/canvas.html#getxyfrompointer","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":126,"kind":2048,"name":"up","url":"classes/canvas.html#up","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":127,"kind":2048,"name":"down","url":"classes/canvas.html#down","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":128,"kind":2048,"name":"right","url":"classes/canvas.html#right","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":129,"kind":2048,"name":"left","url":"classes/canvas.html#left","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":130,"kind":2048,"name":"moveBy","url":"classes/canvas.html#moveby","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":131,"kind":2048,"name":"moveTo","url":"classes/canvas.html#moveto","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":132,"kind":2048,"name":"foreground","url":"classes/canvas.html#foreground","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":133,"kind":2048,"name":"background","url":"classes/canvas.html#background","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":134,"kind":2048,"name":"bold","url":"classes/canvas.html#bold","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":135,"kind":2048,"name":"dim","url":"classes/canvas.html#dim","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":136,"kind":2048,"name":"underlined","url":"classes/canvas.html#underlined","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":137,"kind":2048,"name":"blink","url":"classes/canvas.html#blink","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":138,"kind":2048,"name":"reverse","url":"classes/canvas.html#reverse","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":139,"kind":2048,"name":"hidden","url":"classes/canvas.html#hidden","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":140,"kind":2048,"name":"erase","url":"classes/canvas.html#erase","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":141,"kind":2048,"name":"eraseToEnd","url":"classes/canvas.html#erasetoend","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":142,"kind":2048,"name":"eraseToStart","url":"classes/canvas.html#erasetostart","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":143,"kind":2048,"name":"eraseToDown","url":"classes/canvas.html#erasetodown","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":144,"kind":2048,"name":"eraseToUp","url":"classes/canvas.html#erasetoup","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":145,"kind":2048,"name":"eraseLine","url":"classes/canvas.html#eraseline","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":146,"kind":2048,"name":"eraseScreen","url":"classes/canvas.html#erasescreen","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":147,"kind":2048,"name":"saveScreen","url":"classes/canvas.html#savescreen","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":148,"kind":2048,"name":"restoreScreen","url":"classes/canvas.html#restorescreen","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":149,"kind":2048,"name":"hideCursor","url":"classes/canvas.html#hidecursor","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":150,"kind":2048,"name":"showCursor","url":"classes/canvas.html#showcursor","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"},{"id":151,"kind":2048,"name":"reset","url":"classes/canvas.html#reset","classes":"tsd-kind-method tsd-parent-kind-class","parent":"Canvas"}],"index":{"version":"2.3.8","fields":["name","parent"],"fieldVectors":[["name/0",[0,46.25]],["parent/0",[]],["name/1",[1,46.25]],["parent/1",[]],["name/2",[2,46.25]],["parent/2",[]],["name/3",[3,35.264]],["parent/3",[]],["name/4",[4,31.586]],["parent/4",[3,3.407]],["name/5",[5,31.586]],["parent/5",[3,3.407]],["name/6",[6,31.586]],["parent/6",[3,3.407]],["name/7",[7,20.6]],["parent/7",[]],["name/8",[4,31.586]],["parent/8",[7,1.99]],["name/9",[5,31.586]],["parent/9",[7,1.99]],["name/10",[6,31.586]],["parent/10",[7,1.99]],["name/11",[8,37.777]],["parent/11",[7,1.99]],["name/12",[9,46.25]],["parent/12",[7,1.99]],["name/13",[10,46.25]],["parent/13",[7,1.99]],["name/14",[11,46.25]],["parent/14",[7,1.99]],["name/15",[12,46.25]],["parent/15",[7,1.99]],["name/16",[13,46.25]],["parent/16",[7,1.99]],["name/17",[14,37.777]],["parent/17",[7,1.99]],["name/18",[15,46.25]],["parent/18",[7,1.99]],["name/19",[16,46.25]],["parent/19",[7,1.99]],["name/20",[17,46.25]],["parent/20",[7,1.99]],["name/21",[18,46.25]],["parent/21",[7,1.99]],["name/22",[19,46.25]],["parent/22",[7,1.99]],["name/23",[20,46.25]],["parent/23",[7,1.99]],["name/24",[21,46.25]],["parent/24",[7,1.99]],["name/25",[22,46.25]],["parent/25",[7,1.99]],["name/26",[23,23.563]],["parent/26",[]],["name/27",[24,46.25]],["parent/27",[23,2.276]],["name/28",[25,33.257]],["parent/28",[23,2.276]],["name/29",[26,33.257]],["parent/29",[23,2.276]],["name/30",[27,33.257]],["parent/30",[23,2.276]],["name/31",[28,33.257]],["parent/31",[23,2.276]],["name/32",[29,33.257]],["parent/32",[23,2.276]],["name/33",[30,33.257]],["parent/33",[23,2.276]],["name/34",[31,46.25]],["parent/34",[23,2.276]],["name/35",[32,46.25]],["parent/35",[23,2.276]],["name/36",[33,46.25]],["parent/36",[23,2.276]],["name/37",[34,46.25]],["parent/37",[23,2.276]],["name/38",[35,46.25]],["parent/38",[23,2.276]],["name/39",[36,46.25]],["parent/39",[23,2.276]],["name/40",[37,30.155]],["parent/40",[]],["name/41",[25,33.257]],["parent/41",[37,2.913]],["name/42",[26,33.257]],["parent/42",[37,2.913]],["name/43",[27,33.257]],["parent/43",[37,2.913]],["name/44",[28,33.257]],["parent/44",[37,2.913]],["name/45",[29,33.257]],["parent/45",[37,2.913]],["name/46",[30,33.257]],["parent/46",[37,2.913]],["name/47",[38,31.586]],["parent/47",[]],["name/48",[39,41.141]],["parent/48",[38,3.052]],["name/49",[40,41.141]],["parent/49",[38,3.052]],["name/50",[41,37.777]],["parent/50",[38,3.052]],["name/51",[42,37.777]],["parent/51",[38,3.052]],["name/52",[43,41.141]],["parent/52",[38,3.052]],["name/53",[44,46.25]],["parent/53",[]],["name/54",[45,17.163]],["parent/54",[]],["name/55",[46,46.25]],["parent/55",[45,1.658]],["name/56",[47,46.25]],["parent/56",[45,1.658]],["name/57",[39,41.141]],["parent/57",[45,1.658]],["name/58",[40,41.141]],["parent/58",[45,1.658]],["name/59",[41,37.777]],["parent/59",[45,1.658]],["name/60",[4,31.586]],["parent/60",[48,3.65]],["name/61",[5,31.586]],["parent/61",[48,3.65]],["name/62",[6,31.586]],["parent/62",[48,3.65]],["name/63",[42,37.777]],["parent/63",[45,1.658]],["name/64",[4,31.586]],["parent/64",[49,3.65]],["name/65",[5,31.586]],["parent/65",[49,3.65]],["name/66",[6,31.586]],["parent/66",[49,3.65]],["name/67",[43,41.141]],["parent/67",[45,1.658]],["name/68",[28,33.257]],["parent/68",[50,3.052]],["name/69",[25,33.257]],["parent/69",[50,3.052]],["name/70",[26,33.257]],["parent/70",[50,3.052]],["name/71",[30,33.257]],["parent/71",[50,3.052]],["name/72",[29,33.257]],["parent/72",[50,3.052]],["name/73",[27,33.257]],["parent/73",[50,3.052]],["name/74",[8,37.777]],["parent/74",[45,1.658]],["name/75",[14,37.777]],["parent/75",[45,1.658]],["name/76",[51,46.25]],["parent/76",[45,1.658]],["name/77",[52,46.25]],["parent/77",[45,1.658]],["name/78",[53,46.25]],["parent/78",[45,1.658]],["name/79",[54,46.25]],["parent/79",[45,1.658]],["name/80",[55,46.25]],["parent/80",[45,1.658]],["name/81",[56,46.25]],["parent/81",[45,1.658]],["name/82",[57,46.25]],["parent/82",[45,1.658]],["name/83",[58,46.25]],["parent/83",[45,1.658]],["name/84",[59,46.25]],["parent/84",[45,1.658]],["name/85",[60,46.25]],["parent/85",[45,1.658]],["name/86",[61,46.25]],["parent/86",[45,1.658]],["name/87",[62,46.25]],["parent/87",[45,1.658]],["name/88",[63,46.25]],["parent/88",[45,1.658]],["name/89",[64,46.25]],["parent/89",[45,1.658]],["name/90",[65,46.25]],["parent/90",[45,1.658]],["name/91",[66,41.141]],["parent/91",[45,1.658]],["name/92",[67,46.25]],["parent/92",[45,1.658]],["name/93",[68,35.264]],["parent/93",[]],["name/94",[69,41.141]],["parent/94",[68,3.407]],["name/95",[70,41.141]],["parent/95",[68,3.407]],["name/96",[71,41.141]],["parent/96",[68,3.407]],["name/97",[72,12.577]],["parent/97",[]],["name/98",[73,46.25]],["parent/98",[72,1.215]],["name/99",[74,46.25]],["parent/99",[72,1.215]],["name/100",[69,41.141]],["parent/100",[72,1.215]],["name/101",[70,41.141]],["parent/101",[72,1.215]],["name/102",[71,41.141]],["parent/102",[72,1.215]],["name/103",[75,46.25]],["parent/103",[72,1.215]],["name/104",[76,46.25]],["parent/104",[72,1.215]],["name/105",[77,46.25]],["parent/105",[72,1.215]],["name/106",[4,31.586]],["parent/106",[78,3.65]],["name/107",[5,31.586]],["parent/107",[78,3.65]],["name/108",[6,31.586]],["parent/108",[78,3.65]],["name/109",[79,46.25]],["parent/109",[72,1.215]],["name/110",[4,31.586]],["parent/110",[80,3.65]],["name/111",[5,31.586]],["parent/111",[80,3.65]],["name/112",[6,31.586]],["parent/112",[80,3.65]],["name/113",[81,46.25]],["parent/113",[72,1.215]],["name/114",[28,33.257]],["parent/114",[82,3.052]],["name/115",[25,33.257]],["parent/115",[82,3.052]],["name/116",[26,33.257]],["parent/116",[82,3.052]],["name/117",[30,33.257]],["parent/117",[82,3.052]],["name/118",[29,33.257]],["parent/118",[82,3.052]],["name/119",[27,33.257]],["parent/119",[82,3.052]],["name/120",[8,37.777]],["parent/120",[72,1.215]],["name/121",[14,37.777]],["parent/121",[72,1.215]],["name/122",[83,46.25]],["parent/122",[72,1.215]],["name/123",[84,46.25]],["parent/123",[72,1.215]],["name/124",[85,46.25]],["parent/124",[72,1.215]],["name/125",[86,46.25]],["parent/125",[72,1.215]],["name/126",[87,46.25]],["parent/126",[72,1.215]],["name/127",[88,46.25]],["parent/127",[72,1.215]],["name/128",[89,46.25]],["parent/128",[72,1.215]],["name/129",[90,46.25]],["parent/129",[72,1.215]],["name/130",[91,46.25]],["parent/130",[72,1.215]],["name/131",[92,46.25]],["parent/131",[72,1.215]],["name/132",[42,37.777]],["parent/132",[72,1.215]],["name/133",[41,37.777]],["parent/133",[72,1.215]],["name/134",[25,33.257]],["parent/134",[72,1.215]],["name/135",[26,33.257]],["parent/135",[72,1.215]],["name/136",[27,33.257]],["parent/136",[72,1.215]],["name/137",[28,33.257]],["parent/137",[72,1.215]],["name/138",[29,33.257]],["parent/138",[72,1.215]],["name/139",[30,33.257]],["parent/139",[72,1.215]],["name/140",[93,46.25]],["parent/140",[72,1.215]],["name/141",[94,46.25]],["parent/141",[72,1.215]],["name/142",[95,46.25]],["parent/142",[72,1.215]],["name/143",[96,46.25]],["parent/143",[72,1.215]],["name/144",[97,46.25]],["parent/144",[72,1.215]],["name/145",[98,46.25]],["parent/145",[72,1.215]],["name/146",[99,46.25]],["parent/146",[72,1.215]],["name/147",[100,46.25]],["parent/147",[72,1.215]],["name/148",[101,46.25]],["parent/148",[72,1.215]],["name/149",[102,46.25]],["parent/149",[72,1.215]],["name/150",[103,46.25]],["parent/150",[72,1.215]],["name/151",[66,41.141]],["parent/151",[72,1.215]]],"invertedIndex":[["b",{"_index":6,"name":{"6":{},"10":{},"62":{},"66":{},"108":{},"112":{}},"parent":{}}],["background",{"_index":41,"name":{"50":{},"59":{},"133":{}},"parent":{}}],["blink",{"_index":28,"name":{"31":{},"44":{},"68":{},"114":{},"137":{}},"parent":{}}],["bold",{"_index":25,"name":{"28":{},"41":{},"69":{},"115":{},"134":{}},"parent":{}}],["canvas",{"_index":72,"name":{"97":{}},"parent":{"98":{},"99":{},"100":{},"101":{},"102":{},"103":{},"104":{},"105":{},"109":{},"113":{},"120":{},"121":{},"122":{},"123":{},"124":{},"125":{},"126":{},"127":{},"128":{},"129":{},"130":{},"131":{},"132":{},"133":{},"134":{},"135":{},"136":{},"137":{},"138":{},"139":{},"140":{},"141":{},"142":{},"143":{},"144":{},"145":{},"146":{},"147":{},"148":{},"149":{},"150":{},"151":{}}}],["canvas.cursorbackground",{"_index":78,"name":{},"parent":{"106":{},"107":{},"108":{}}}],["canvas.cursordisplay",{"_index":82,"name":{},"parent":{"114":{},"115":{},"116":{},"117":{},"118":{},"119":{}}}],["canvas.cursorforeground",{"_index":80,"name":{},"parent":{"110":{},"111":{},"112":{}}}],["cell",{"_index":45,"name":{"54":{}},"parent":{"55":{},"56":{},"57":{},"58":{},"59":{},"63":{},"67":{},"74":{},"75":{},"76":{},"77":{},"78":{},"79":{},"80":{},"81":{},"82":{},"83":{},"84":{},"85":{},"86":{},"87":{},"88":{},"89":{},"90":{},"91":{},"92":{}}}],["cell.background",{"_index":48,"name":{},"parent":{"60":{},"61":{},"62":{}}}],["cell.display",{"_index":50,"name":{},"parent":{"68":{},"69":{},"70":{},"71":{},"72":{},"73":{}}}],["cell.foreground",{"_index":49,"name":{},"parent":{"64":{},"65":{},"66":{}}}],["cells",{"_index":73,"name":{"98":{}},"parent":{}}],["char",{"_index":47,"name":{"56":{}},"parent":{}}],["color",{"_index":7,"name":{"7":{}},"parent":{"8":{},"9":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{}}}],["constructor",{"_index":8,"name":{"11":{},"74":{},"120":{}},"parent":{}}],["create",{"_index":14,"name":{"17":{},"75":{},"121":{}},"parent":{}}],["cursorbackground",{"_index":77,"name":{"105":{}},"parent":{}}],["cursordisplay",{"_index":81,"name":{"113":{}},"parent":{}}],["cursorforeground",{"_index":79,"name":{"109":{}},"parent":{}}],["cursorx",{"_index":75,"name":{"103":{}},"parent":{}}],["cursory",{"_index":76,"name":{"104":{}},"parent":{}}],["dim",{"_index":26,"name":{"29":{},"42":{},"70":{},"116":{},"135":{}},"parent":{}}],["display",{"_index":43,"name":{"52":{},"67":{}},"parent":{}}],["display_modes",{"_index":23,"name":{"26":{}},"parent":{"27":{},"28":{},"29":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{}}}],["down",{"_index":88,"name":{"127":{}},"parent":{}}],["encodetovt100",{"_index":44,"name":{"53":{}},"parent":{}}],["erase",{"_index":93,"name":{"140":{}},"parent":{}}],["eraseline",{"_index":98,"name":{"145":{}},"parent":{}}],["erasescreen",{"_index":99,"name":{"146":{}},"parent":{}}],["erasetodown",{"_index":96,"name":{"143":{}},"parent":{}}],["erasetoend",{"_index":94,"name":{"141":{}},"parent":{}}],["erasetostart",{"_index":95,"name":{"142":{}},"parent":{}}],["erasetoup",{"_index":97,"name":{"144":{}},"parent":{}}],["flush",{"_index":84,"name":{"123":{}},"parent":{}}],["foreground",{"_index":42,"name":{"51":{},"63":{},"132":{}},"parent":{}}],["fromhex",{"_index":13,"name":{"16":{}},"parent":{}}],["fromrgb",{"_index":12,"name":{"15":{}},"parent":{}}],["g",{"_index":5,"name":{"5":{},"9":{},"61":{},"65":{},"107":{},"111":{}},"parent":{}}],["getb",{"_index":19,"name":{"22":{}},"parent":{}}],["getbackground",{"_index":57,"name":{"82":{}},"parent":{}}],["getchar",{"_index":51,"name":{"76":{}},"parent":{}}],["getdisplay",{"_index":63,"name":{"88":{}},"parent":{}}],["getforeground",{"_index":60,"name":{"85":{}},"parent":{}}],["getg",{"_index":17,"name":{"20":{}},"parent":{}}],["getpointerfromxy",{"_index":85,"name":{"124":{}},"parent":{}}],["getr",{"_index":15,"name":{"18":{}},"parent":{}}],["getx",{"_index":53,"name":{"78":{}},"parent":{}}],["getxyfrompointer",{"_index":86,"name":{"125":{}},"parent":{}}],["gety",{"_index":55,"name":{"80":{}},"parent":{}}],["height",{"_index":71,"name":{"96":{},"102":{}},"parent":{}}],["hex_regex",{"_index":0,"name":{"0":{}},"parent":{}}],["hidden",{"_index":30,"name":{"33":{},"46":{},"71":{},"117":{},"139":{}},"parent":{}}],["hidecursor",{"_index":102,"name":{"149":{}},"parent":{}}],["icanvasoptions",{"_index":68,"name":{"93":{}},"parent":{"94":{},"95":{},"96":{}}}],["icelloptions",{"_index":38,"name":{"47":{}},"parent":{"48":{},"49":{},"50":{},"51":{},"52":{}}}],["icolor",{"_index":3,"name":{"3":{}},"parent":{"4":{},"5":{},"6":{}}}],["idisplayoptions",{"_index":37,"name":{"40":{}},"parent":{"41":{},"42":{},"43":{},"44":{},"45":{},"46":{}}}],["ishex",{"_index":11,"name":{"14":{}},"parent":{}}],["ismodified",{"_index":46,"name":{"55":{}},"parent":{}}],["isnamed",{"_index":9,"name":{"12":{}},"parent":{}}],["isrgb",{"_index":10,"name":{"13":{}},"parent":{}}],["lastframe",{"_index":74,"name":{"99":{}},"parent":{}}],["left",{"_index":90,"name":{"129":{}},"parent":{}}],["moveby",{"_index":91,"name":{"130":{}},"parent":{}}],["moveto",{"_index":92,"name":{"131":{}},"parent":{}}],["named_colors",{"_index":1,"name":{"1":{}},"parent":{}}],["r",{"_index":4,"name":{"4":{},"8":{},"60":{},"64":{},"106":{},"110":{}},"parent":{}}],["reset",{"_index":66,"name":{"91":{},"151":{}},"parent":{}}],["reset_all",{"_index":24,"name":{"27":{}},"parent":{}}],["reset_blink",{"_index":34,"name":{"37":{}},"parent":{}}],["reset_bold",{"_index":31,"name":{"34":{}},"parent":{}}],["reset_dim",{"_index":32,"name":{"35":{}},"parent":{}}],["reset_hidden",{"_index":36,"name":{"39":{}},"parent":{}}],["reset_reverse",{"_index":35,"name":{"38":{}},"parent":{}}],["reset_underlined",{"_index":33,"name":{"36":{}},"parent":{}}],["resetbackground",{"_index":59,"name":{"84":{}},"parent":{}}],["resetdisplay",{"_index":65,"name":{"90":{}},"parent":{}}],["resetforeground",{"_index":62,"name":{"87":{}},"parent":{}}],["restorescreen",{"_index":101,"name":{"148":{}},"parent":{}}],["reverse",{"_index":29,"name":{"32":{},"45":{},"72":{},"118":{},"138":{}},"parent":{}}],["rgb_regex",{"_index":2,"name":{"2":{}},"parent":{}}],["right",{"_index":89,"name":{"128":{}},"parent":{}}],["savescreen",{"_index":100,"name":{"147":{}},"parent":{}}],["setb",{"_index":20,"name":{"23":{}},"parent":{}}],["setbackground",{"_index":58,"name":{"83":{}},"parent":{}}],["setchar",{"_index":52,"name":{"77":{}},"parent":{}}],["setdisplay",{"_index":64,"name":{"89":{}},"parent":{}}],["setforeground",{"_index":61,"name":{"86":{}},"parent":{}}],["setg",{"_index":18,"name":{"21":{}},"parent":{}}],["setr",{"_index":16,"name":{"19":{}},"parent":{}}],["setx",{"_index":54,"name":{"79":{}},"parent":{}}],["sety",{"_index":56,"name":{"81":{}},"parent":{}}],["showcursor",{"_index":103,"name":{"150":{}},"parent":{}}],["stream",{"_index":69,"name":{"94":{},"100":{}},"parent":{}}],["tohex",{"_index":22,"name":{"25":{}},"parent":{}}],["torgb",{"_index":21,"name":{"24":{}},"parent":{}}],["tostring",{"_index":67,"name":{"92":{}},"parent":{}}],["underlined",{"_index":27,"name":{"30":{},"43":{},"73":{},"119":{},"136":{}},"parent":{}}],["up",{"_index":87,"name":{"126":{}},"parent":{}}],["width",{"_index":70,"name":{"95":{},"101":{}},"parent":{}}],["write",{"_index":83,"name":{"122":{}},"parent":{}}],["x",{"_index":39,"name":{"48":{},"57":{}},"parent":{}}],["y",{"_index":40,"name":{"49":{},"58":{}},"parent":{}}]],"pipeline":[]}} -------------------------------------------------------------------------------- /docs/assets/js/main.js: -------------------------------------------------------------------------------- 1 | !function(){var e=function(t){var r=new e.Builder;return r.pipeline.add(e.trimmer,e.stopWordFilter,e.stemmer),r.searchPipeline.add(e.stemmer),t.call(r,r),r.build()};e.version="2.3.7",e.utils={},e.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),e.utils.asString=function(e){return null==e?"":e.toString()},e.utils.clone=function(e){if(null==e)return e;for(var t=Object.create(null),r=Object.keys(e),i=0;i=this.length)return e.QueryLexer.EOS;var t=this.str.charAt(this.pos);return this.pos+=1,t},e.QueryLexer.prototype.width=function(){return this.pos-this.start},e.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},e.QueryLexer.prototype.backup=function(){this.pos-=1},e.QueryLexer.prototype.acceptDigitRun=function(){for(var t,r;47<(r=(t=this.next()).charCodeAt(0))&&r<58;);t!=e.QueryLexer.EOS&&this.backup()},e.QueryLexer.prototype.more=function(){return this.pos=this.scrollTop||0===this.scrollTop,isShown!==this.showToolbar&&(this.toolbar.classList.toggle("tsd-page-toolbar--hide"),this.secondaryNav.classList.toggle("tsd-navigation--toolbar-hide")),this.lastY=this.scrollTop},Viewport}(typedoc.EventTarget);typedoc.Viewport=Viewport,typedoc.registerService(Viewport,"viewport")}(typedoc||(typedoc={})),function(typedoc){function Component(options){this.el=options.el}typedoc.Component=Component}(typedoc||(typedoc={})),function(typedoc){typedoc.pointerDown="mousedown",typedoc.pointerMove="mousemove",typedoc.pointerUp="mouseup",typedoc.pointerDownPosition={x:0,y:0},typedoc.preventNextClick=!1,typedoc.isPointerDown=!1,typedoc.isPointerTouch=!1,typedoc.hasPointerMoved=!1,typedoc.isMobile=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),document.documentElement.classList.add(typedoc.isMobile?"is-mobile":"not-mobile"),typedoc.isMobile&&"ontouchstart"in document.documentElement&&(typedoc.isPointerTouch=!0,typedoc.pointerDown="touchstart",typedoc.pointerMove="touchmove",typedoc.pointerUp="touchend"),document.addEventListener(typedoc.pointerDown,function(e){typedoc.isPointerDown=!0,typedoc.hasPointerMoved=!1;var t="touchstart"==typedoc.pointerDown?e.targetTouches[0]:e;typedoc.pointerDownPosition.y=t.pageY||0,typedoc.pointerDownPosition.x=t.pageX||0}),document.addEventListener(typedoc.pointerMove,function(e){if(typedoc.isPointerDown&&!typedoc.hasPointerMoved){var t="touchstart"==typedoc.pointerDown?e.targetTouches[0]:e,x=typedoc.pointerDownPosition.x-(t.pageX||0),y=typedoc.pointerDownPosition.y-(t.pageY||0);typedoc.hasPointerMoved=10scrollTop;)index-=1;for(;index"+match+""}),parent=row.parent||"";(parent=parent.replace(new RegExp(this.query,"i"),function(match){return""+match+""}))&&(name=''+parent+"."+name);var item=document.createElement("li");item.classList.value=row.classes,item.innerHTML='\n '+name+"\n ",this.results.appendChild(item)}}},Search.prototype.setLoadingState=function(value){this.loadingState!=value&&(this.el.classList.remove(SearchLoadingState[this.loadingState].toLowerCase()),this.loadingState=value,this.el.classList.add(SearchLoadingState[this.loadingState].toLowerCase()),this.updateResults())},Search.prototype.setHasFocus=function(value){this.hasFocus!=value&&(this.hasFocus=value,this.el.classList.toggle("has-focus"),value?(this.setQuery(""),this.field.value=""):this.field.value=this.query)},Search.prototype.setQuery=function(value){this.query=value.trim(),this.updateResults()},Search.prototype.setCurrentResult=function(dir){var current=this.results.querySelector(".current");if(current){var rel=1==dir?current.nextElementSibling:current.previousElementSibling;rel&&(current.classList.remove("current"),rel.classList.add("current"))}else(current=this.results.querySelector(1==dir?"li:first-child":"li:last-child"))&¤t.classList.add("current")},Search.prototype.gotoCurrentResult=function(){var current=this.results.querySelector(".current");if(current||(current=this.results.querySelector("li:first-child")),current){var link=current.querySelector("a");link&&(window.location.href=link.href),this.field.blur()}},Search.prototype.bindEvents=function(){var _this=this;this.results.addEventListener("mousedown",function(){_this.resultClicked=!0}),this.results.addEventListener("mouseup",function(){_this.resultClicked=!1,_this.setHasFocus(!1)}),this.field.addEventListener("focusin",function(){_this.setHasFocus(!0),_this.loadIndex()}),this.field.addEventListener("focusout",function(){_this.resultClicked?_this.resultClicked=!1:setTimeout(function(){return _this.setHasFocus(!1)},100)}),this.field.addEventListener("input",function(){_this.setQuery(_this.field.value)}),this.field.addEventListener("keydown",function(e){13==e.keyCode||27==e.keyCode||38==e.keyCode||40==e.keyCode?(_this.preventPress=!0,e.preventDefault(),13==e.keyCode?_this.gotoCurrentResult():27==e.keyCode?_this.field.blur():38==e.keyCode?_this.setCurrentResult(-1):40==e.keyCode&&_this.setCurrentResult(1)):_this.preventPress=!1}),this.field.addEventListener("keypress",function(e){_this.preventPress&&e.preventDefault()}),document.body.addEventListener("keydown",function(e){e.altKey||e.ctrlKey||e.metaKey||!_this.hasFocus&&47this.groups.length-1&&(index=this.groups.length-1),this.index!=index){var to=this.groups[index];if(-1