├── .eslintrc.js ├── .gitignore ├── README.md ├── demo-cli.js ├── examples ├── base16-default-dark.css ├── demo-cli.js ├── head.txt ├── index.html ├── styles.css └── terminal.css ├── package-lock.json └── package.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'es6': true 5 | }, 6 | 'extends': 'eslint:recommended', 7 | 'globals': { 8 | 'Atomics': 'readonly', 9 | 'SharedArrayBuffer': 'readonly' 10 | }, 11 | 'parserOptions': { 12 | 'ecmaVersion': 2018 13 | }, 14 | 'rules': { 15 | 'accessor-pairs': 'error', 16 | 'array-bracket-newline': 'error', 17 | 'array-bracket-spacing': 'error', 18 | 'array-callback-return': 'error', 19 | 'array-element-newline': 'error', 20 | 'arrow-body-style': 'error', 21 | 'arrow-parens': [ 22 | 'error', 23 | 'as-needed' 24 | ], 25 | 'arrow-spacing': [ 26 | 'error', 27 | { 28 | 'after': true, 29 | 'before': true 30 | } 31 | ], 32 | 'block-scoped-var': 'error', 33 | 'block-spacing': 'error', 34 | 'brace-style': 'error', 35 | 'callback-return': 'error', 36 | 'camelcase': 'error', 37 | 'capitalized-comments': 'error', 38 | 'class-methods-use-this': 'off', 39 | 'comma-dangle': 'error', 40 | 'comma-spacing': [ 41 | 'error', 42 | { 43 | 'after': true, 44 | 'before': false 45 | } 46 | ], 47 | 'comma-style': 'error', 48 | 'complexity': 'error', 49 | 'computed-property-spacing': [ 50 | 'error', 51 | 'never' 52 | ], 53 | 'consistent-return': 'error', 54 | 'consistent-this': 'error', 55 | 'curly': 'off', 56 | 'default-case': 'error', 57 | 'dot-location': 'error', 58 | 'dot-notation': 'error', 59 | 'eol-last': 'error', 60 | 'eqeqeq': 'error', 61 | 'func-call-spacing': 'error', 62 | 'func-name-matching': 'error', 63 | 'func-names': 'error', 64 | 'func-style': 'error', 65 | 'function-paren-newline': 'error', 66 | 'generator-star-spacing': 'error', 67 | 'global-require': 'error', 68 | 'guard-for-in': 'off', 69 | 'handle-callback-err': 'error', 70 | 'id-blacklist': 'error', 71 | 'id-length': 'error', 72 | 'id-match': 'error', 73 | 'implicit-arrow-linebreak': [ 74 | 'error', 75 | 'beside' 76 | ], 77 | 'indent': 'off', 78 | 'indent-legacy': 'off', 79 | 'init-declarations': 'error', 80 | 'jsx-quotes': 'error', 81 | 'key-spacing': 'error', 82 | 'keyword-spacing': 'error', 83 | 'line-comment-position': 'error', 84 | 'linebreak-style': [ 85 | 'error', 86 | 'unix' 87 | ], 88 | 'lines-around-comment': 'error', 89 | 'lines-around-directive': 'error', 90 | 'lines-between-class-members': [ 91 | 'error', 92 | 'always' 93 | ], 94 | 'max-classes-per-file': 'error', 95 | 'max-depth': 'error', 96 | 'max-len': 'off', 97 | 'max-lines': 'error', 98 | 'max-lines-per-function': 'error', 99 | 'max-nested-callbacks': 'error', 100 | 'max-params': 'error', 101 | 'max-statements': 'off', 102 | 'max-statements-per-line': 'error', 103 | 'multiline-comment-style': 'error', 104 | 'multiline-ternary': 'error', 105 | 'new-cap': 'error', 106 | 'new-parens': 'error', 107 | 'newline-after-var': 'off', 108 | 'newline-before-return': 'error', 109 | 'newline-per-chained-call': 'error', 110 | 'no-alert': 'error', 111 | 'no-array-constructor': 'error', 112 | 'no-async-promise-executor': 'error', 113 | 'no-await-in-loop': 'off', 114 | 'no-bitwise': 'error', 115 | 'no-buffer-constructor': 'error', 116 | 'no-caller': 'error', 117 | 'no-catch-shadow': 'error', 118 | 'no-confusing-arrow': 'error', 119 | 'no-continue': 'error', 120 | 'no-div-regex': 'error', 121 | 'no-duplicate-imports': 'error', 122 | 'no-else-return': 'error', 123 | 'no-empty-function': 'error', 124 | 'no-eq-null': 'error', 125 | 'no-eval': 'error', 126 | 'no-extend-native': 'error', 127 | 'no-extra-bind': 'error', 128 | 'no-extra-label': 'error', 129 | 'no-extra-parens': 'off', 130 | 'no-floating-decimal': 'error', 131 | 'no-implicit-coercion': 'error', 132 | 'no-implicit-globals': 'error', 133 | 'no-implied-eval': 'error', 134 | 'no-inline-comments': 'error', 135 | 'no-invalid-this': 'error', 136 | 'no-iterator': 'error', 137 | 'no-label-var': 'error', 138 | 'no-labels': 'error', 139 | 'no-lone-blocks': 'error', 140 | 'no-lonely-if': 'error', 141 | 'no-loop-func': 'error', 142 | 'no-magic-numbers': 'off', 143 | 'no-misleading-character-class': 'error', 144 | 'no-mixed-operators': [ 145 | 'error', 146 | { 147 | 'allowSamePrecedence': true 148 | } 149 | ], 150 | 'no-mixed-requires': 'error', 151 | 'no-multi-assign': 'error', 152 | 'no-multi-spaces': 'error', 153 | 'no-multi-str': 'error', 154 | 'no-multiple-empty-lines': 'error', 155 | 'no-native-reassign': 'error', 156 | 'no-negated-condition': 'error', 157 | 'no-negated-in-lhs': 'error', 158 | 'no-nested-ternary': 'error', 159 | 'no-new': 'error', 160 | 'no-new-func': 'error', 161 | 'no-new-object': 'error', 162 | 'no-new-require': 'error', 163 | 'no-new-wrappers': 'error', 164 | 'no-octal-escape': 'error', 165 | 'no-param-reassign': 'error', 166 | 'no-path-concat': 'error', 167 | 'no-plusplus': 'error', 168 | 'no-process-env': 'error', 169 | 'no-process-exit': 'error', 170 | 'no-proto': 'error', 171 | 'no-prototype-builtins': 'error', 172 | 'no-restricted-globals': 'error', 173 | 'no-restricted-imports': 'error', 174 | 'no-restricted-modules': 'error', 175 | 'no-restricted-properties': 'error', 176 | 'no-restricted-syntax': 'error', 177 | 'no-return-assign': 'error', 178 | 'no-return-await': 'error', 179 | 'no-script-url': 'error', 180 | 'no-self-compare': 'error', 181 | 'no-sequences': 'error', 182 | 'no-shadow': 'error', 183 | 'no-shadow-restricted-names': 'error', 184 | 'no-spaced-func': 'error', 185 | 'no-sync': 'error', 186 | 'no-tabs': 'error', 187 | 'no-template-curly-in-string': 'error', 188 | 'no-ternary': 'error', 189 | 'no-throw-literal': 'error', 190 | 'no-trailing-spaces': 'error', 191 | 'no-undef-init': 'error', 192 | 'no-undefined': 'error', 193 | 'no-underscore-dangle': 'error', 194 | 'no-unmodified-loop-condition': 'error', 195 | 'no-unneeded-ternary': 'error', 196 | 'no-unused-expressions': 'error', 197 | 'no-use-before-define': 'error', 198 | 'no-useless-call': 'error', 199 | 'no-useless-catch': 'error', 200 | 'no-useless-computed-key': 'error', 201 | 'no-useless-concat': 'error', 202 | 'no-useless-constructor': 'error', 203 | 'no-useless-rename': 'error', 204 | 'no-useless-return': 'error', 205 | 'no-var': 'error', 206 | 'no-void': 'error', 207 | 'no-warning-comments': 'error', 208 | 'no-whitespace-before-property': 'error', 209 | 'no-with': 'error', 210 | 'nonblock-statement-body-position': 'error', 211 | 'object-curly-newline': 'error', 212 | 'object-curly-spacing': 'error', 213 | 'object-property-newline': 'error', 214 | 'object-shorthand': 'error', 215 | 'one-var': 'off', 216 | 'one-var-declaration-per-line': 'error', 217 | 'operator-assignment': [ 218 | 'error', 219 | 'always' 220 | ], 221 | 'operator-linebreak': 'error', 222 | 'padded-blocks': 'off', 223 | 'padding-line-between-statements': 'error', 224 | 'prefer-arrow-callback': 'error', 225 | 'prefer-const': 'error', 226 | 'prefer-destructuring': 'error', 227 | 'prefer-named-capture-group': 'error', 228 | 'prefer-numeric-literals': 'error', 229 | 'prefer-object-spread': 'error', 230 | 'prefer-promise-reject-errors': 'error', 231 | 'prefer-reflect': 'error', 232 | 'prefer-rest-params': 'error', 233 | 'prefer-spread': 'error', 234 | 'prefer-template': 'error', 235 | 'quote-props': 'error', 236 | 'quotes': [ 237 | 'error', 238 | 'single' 239 | ], 240 | 'radix': 'error', 241 | 'require-atomic-updates': 'error', 242 | 'require-await': 'error', 243 | 'require-jsdoc': 'error', 244 | 'require-unicode-regexp': 'error', 245 | 'rest-spread-spacing': 'error', 246 | 'semi': 'off', 247 | 'semi-spacing': 'error', 248 | 'semi-style': [ 249 | 'error', 250 | 'last' 251 | ], 252 | 'sort-imports': 'error', 253 | 'sort-keys': 'error', 254 | 'sort-vars': 'error', 255 | 'space-before-blocks': 'error', 256 | 'space-before-function-paren': 'off', 257 | 'space-in-parens': [ 258 | 'error', 259 | 'never' 260 | ], 261 | 'space-infix-ops': 'error', 262 | 'space-unary-ops': 'error', 263 | 'spaced-comment': 'error', 264 | 'strict': 'error', 265 | 'switch-colon-spacing': 'error', 266 | 'symbol-description': 'error', 267 | 'template-curly-spacing': 'error', 268 | 'template-tag-spacing': 'error', 269 | 'unicode-bom': [ 270 | 'error', 271 | 'never' 272 | ], 273 | 'valid-jsdoc': 'error', 274 | 'vars-on-top': 'error', 275 | 'wrap-iife': 'error', 276 | 'wrap-regex': 'error', 277 | 'yield-star-spacing': 'error', 278 | 'yoda': 'error' 279 | } 280 | }; 281 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # demo-cli 2 | 3 | Easily create demos of interacting with CLI-based things. See [https://demo-cli.dev](https://demo-cli.dev) for examples. 4 | 5 | ### Super quick example: 6 | 7 | ```javascript 8 | 9 |
10 | 11 | 23 | ``` 24 | 25 | ## Installation 26 | 27 | `npm install @gammons/demo-cli` 28 | 29 | demo-cli has no JS dependencies. 30 | 31 | You can use any CSS you like to emulate the terminal, or you can use `terminal.css` included in the `examples/` folder. 32 | 33 | ## Notes / caveats 34 | 35 | Fair warning: `demo-cli` does no input sanitization whatsoever. 36 | 37 | ## Is it good? 38 | 39 | Yes. Yes it is. 40 | -------------------------------------------------------------------------------- /demo-cli.js: -------------------------------------------------------------------------------- 1 | export default class DemoCLI { 2 | 3 | /** 4 | * Creates a new DemoCLI object. 5 | * @constructor 6 | * @param {string} containerName - the name of the element to inject the CLI into, e.g. "#cli" 7 | * @param {Object} options - the options to use. 8 | * @param {string} [options.cursor = '▋'] - The cursor character to use. 9 | * @param {string} [options.prompt = '➜ '] - The prompt string to use. 10 | */ 11 | constructor(containerName, options = {}) { 12 | this.container = document.querySelector(containerName) 13 | this.cursor = options.cursor || '▋' 14 | this.prompt = options.prompt || '➜ ' 15 | 16 | this.reset() 17 | } 18 | 19 | /** 20 | * Resets the terminal. Equivalent to `clear` or CTRL+L. 21 | * @returns {undefined} 22 | */ 23 | reset() { 24 | this.container.innerHTML = '' 25 | } 26 | 27 | /** 28 | * Print a string to the terminal. Prints the string inside of a element. 29 | * @param {string} string - the string to print. 30 | * @param {Object} options - the options to use. 31 | * @param {string} className - the className to set for the element. Can be used to control colors, fonts, etc. 32 | * @returns {undefined} 33 | */ 34 | print(string, options = {}) { 35 | const span = document.createElement('span'); 36 | if (options.className) span.className = options.className 37 | 38 | for (const prop in options) { 39 | span.setAttribute(prop, options[prop]) 40 | } 41 | 42 | span.innerHTML = string 43 | this.container.appendChild(span) 44 | } 45 | 46 | /** 47 | * Print a string to the terminal and hit the enter key. Prints the string inside of a element. 48 | * @param {string} string - the string to print. 49 | * @param {Object} options - the options to use. 50 | * @param {string} className - the className to set for the element. Can be used to control colors, fonts, etc. 51 | * @returns {undefined} 52 | */ 53 | println(string, options = {}) { 54 | this.print(string, options) 55 | this.enterKey() 56 | } 57 | 58 | /** 59 | * Print the prompt to the terminal. 60 | * @param {Object} options - the options to use. 61 | * @param {string} className - the className to set for the element. Can be used to control colors, fonts, etc. 62 | * @returns {undefined} 63 | */ 64 | printPrompt(options = {}) { 65 | this.print(this.prompt, options) 66 | this.printCursor() 67 | } 68 | 69 | /** 70 | * Print a carraige retun and start a new line. 71 | * @returns {undefined} 72 | */ 73 | enterKey() { 74 | this.container.appendChild(document.createElement('br')) 75 | } 76 | 77 | /** 78 | * Explicitly print the cursor to the terminal. 79 | * @param {Object} options - the options to use. 80 | * @param {string} className - the className to set for the element. Can be used to control colors, fonts, etc. 81 | * @returns {undefined} 82 | */ 83 | printCursor(options = {}) { 84 | this.removeCursor() 85 | 86 | options['data-cli-cursor'] = this.cursor 87 | this.print('', options) 88 | } 89 | 90 | /** 91 | * Remove the cursor from the terminal 92 | * @returns {undefined} 93 | */ 94 | removeCursor() { 95 | const cursors = this.container.querySelectorAll('[data-cli-cursor]') 96 | 97 | for (const el of cursors) { 98 | el.removeAttribute('data-cli-cursor') 99 | } 100 | } 101 | 102 | /** 103 | * Wait a specified time 104 | * @param {Integer} time - the time to wait in ms. 105 | * @returns {Promise} a promise that resolves in the time specified 106 | */ 107 | wait(time) { 108 | return new Promise(resolve => setTimeout(resolve, time)) 109 | } 110 | 111 | /** 112 | * Simulate typing characters to the screen 113 | * @param {string} string - the string to type. 114 | * @param {Object} options - the options to use. 115 | * @param {Integer} [options.delay=60] - the delay in ms between typed characters. 116 | * @param {Boolean} [options.random=false] - Delay a random amount of time between characters 117 | * @param {Float} [options.delayVariability=0.3] - If using `random`, this is the % increase or decrease to delay by. 118 | * @returns {undefined} 119 | */ 120 | async type(string, options = {}) { 121 | this.removeCursor() 122 | 123 | let delayMin = options.delay || 60 124 | let delayMax = options.delay || 60 125 | const delayVariability = options.delayVariability || 0.3 126 | 127 | if (options.random) { 128 | delayMin = options.delay - (options.delay * delayVariability) 129 | delayMax = options.delay + (options.delay * delayVariability) 130 | } 131 | 132 | const span = document.createElement('span') 133 | span.setAttribute('data-cli-cursor', this.cursor) 134 | 135 | this.container.appendChild(span) 136 | 137 | for (const char of string) { 138 | const delay = Math.floor(Math.random() * (delayMax - delayMin + 1)) + delayMin 139 | await this.wait(delay) 140 | span.textContent += char 141 | } 142 | span.removeAttribute('data-cli-cursor') 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /examples/base16-default-dark.css: -------------------------------------------------------------------------------- 1 | .base00 { color: #181818; } 2 | .base01 { color: #282828; } 3 | .base02 { color: #383838; } 4 | .base03 { color: #585858; } 5 | .base04 { color: #b8b8b8; } 6 | .base05 { color: #d8d8d8; } 7 | .base06 { color: #e8e8e8; } 8 | .base07 { color: #f8f8f8; } 9 | .base08 { color: #ab4642; } 10 | .base09 { color: #dc9656; } 11 | .base0A { color: #f7ca88; } 12 | .base0B { color: #a1b56c; } 13 | .base0C { color: #86c1b9; } 14 | .base0D { color: #7cafc2; } 15 | .base0E { color: #ba8baf; } 16 | .base0F { color: #a16946; } 17 | 18 | .base00-background { background-color: #181818; } 19 | .base01-background { background-color: #282828; } 20 | .base02-background { background-color: #383838; } 21 | .base03-background { background-color: #585858; } 22 | .base04-background { background-color: #b8b8b8; } 23 | .base05-background { background-color: #d8d8d8; } 24 | .base06-background { background-color: #e8e8e8; } 25 | .base07-background { background-color: #f8f8f8; } 26 | .base08-background { background-color: #ab4642; } 27 | .base09-background { background-color: #dc9656; } 28 | .base0A-background { background-color: #f7ca88; } 29 | .base0B-background { background-color: #a1b56c; } 30 | .base0C-background { background-color: #86c1b9; } 31 | .base0D-background { background-color: #7cafc2; } 32 | .base0E-background { background-color: #ba8baf; } 33 | .base0F-background { background-color: #a16946; } 34 | -------------------------------------------------------------------------------- /examples/demo-cli.js: -------------------------------------------------------------------------------- 1 | ../demo-cli.js -------------------------------------------------------------------------------- /examples/head.txt: -------------------------------------------------------------------------------- 1 | HEAD(1) User Commands HEAD(1) 2 | 3 | NAME 4 | head - output the first part of files 5 | 6 | SYNOPSIS 7 | head [OPTION]... [FILE]... 8 | 9 | DESCRIPTION 10 | Print the first 10 lines of each FILE to standard output. With more than one FILE, 11 | precede each with a header giving the file name. 12 | 13 | With no FILE, or when FILE is -, read standard input. 14 | 15 | Mandatory arguments to long options are mandatory for short options too. 16 | 17 | -c, --bytes=[-]NUM 18 | print the first NUM bytes of each file; with the leading '-', print all but the 19 | last NUM bytes of each file 20 | 21 | -n, --lines=[-]NUM 22 | print the first NUM lines instead of the first 10; with the leading '-', print 23 | all but the last NUM lines of each file 24 | 25 | -q, --quiet, --silent 26 | never print headers giving file names 27 | 28 | -v, --verbose 29 | always print headers giving file names 30 | 31 | -z, --zero-terminated 32 | line delimiter is NUL, not newline 33 | 34 | --help display this help and exit 35 | 36 | --version 37 | output version information and exit 38 | 39 | NUM may have a multiplier suffix: b 512, kB 1000, K 1024, MB 1000*1000, M 1024*1024, 40 | GB 1000*1000*1000, G 1024*1024*1024, and so on for T, P, E, Z, Y. 41 | 42 | AUTHOR 43 | Written by David MacKenzie and Jim Meyering. 44 | 45 | REPORTING BUGS 46 | GNU coreutils online help:demo-cli
is a simple javascript class that allows you to create neat-o canned demos of CLI-based tools.
35 | import DemoCLI from "./demo-cli.js" 36 | 37 | const cli = new DemoCLI("#simple") 38 | 39 | const h = (async () => { 40 | cli.printPrompt() 41 | await cli.type('echo "testing this"') 42 | cli.enterKey() 43 | cli.println("testing this") 44 | cli.printPrompt() 45 | })() 46 |47 |
73 | import DemoCLI from "./demo-cli.js" 74 | 75 | const cli = new DemoCLI("#cli") 76 | cli.printPrompt() 77 | cli.print("print ") 78 | cli.print("on same line ") 79 | cli.enterKey() 80 | 81 | let colorNum = 1 82 | for(let x = 0; x <= 16; x++) { 83 | cli.printPrompt() 84 | cli.println("colors", {className: `base0${x.toString(16)}`}) 85 | colorNum++ 86 | } 87 | 88 | cli.println("println") 89 | cli.println("println") 90 | cli.printPrompt({className: "base08"}) 91 |92 |
121 | import DemoCLI from "./demo-cli.js" 122 | const t = new DemoCLI("#typing-cli") 123 | const f = (async () => { 124 | while (true) { 125 | t.printPrompt() 126 | await t.type("here I am typing", {delay: 60, random: true}) 127 | t.enterKey() 128 | t.printPrompt() 129 | 130 | await t.wait(2000) 131 | 132 | await t.type("here I am typing more stuff") 133 | t.enterKey() 134 | t.printPrompt() 135 | 136 | await t.type("going to reset here") 137 | await t.wait(2000) 138 | t.enterKey() 139 | t.reset() 140 | } 141 | })() 142 |143 |