├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .gitlab-ci.yml ├── .nvmrc ├── .prettierrc.js ├── .storybook ├── config.js └── webpack.config.js ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── OutputList.js ├── PromptSymbol.js ├── ReactTerminal.js ├── ReactTerminalStateless.js ├── TerminalContainer.js ├── index.js ├── input │ ├── CommandInput.js │ ├── StyledForm.js │ └── StyledInput.js ├── output │ ├── HeaderOutput.js │ ├── OutputContainer.js │ ├── TextCommandWrapper.js │ ├── TextErrorOutput.js │ ├── TextErrorWrapper.js │ ├── TextOutput.js │ └── index.js └── themes │ ├── default.js │ ├── dye.js │ ├── ember.js │ ├── forest.js │ ├── hacker.js │ ├── index.js │ ├── light.js │ ├── magpie.js │ └── sea.js ├── stories ├── ReactTerminal.stories.js ├── ReactTerminalStateless.stories.js ├── ReactTerminal_output.stories.js ├── ReactTerminal_prompt.stories.js ├── ReactTerminal_state.stories.js └── ReactTerminal_themes.stories.js ├── test ├── ReactTerminal.spec.js ├── ReactTerminalStateless.spec.js └── library.spec.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/env", 4 | "@babel/react" 5 | ], 6 | "plugins": [ 7 | "add-module-exports", 8 | "@babel/plugin-proposal-object-rest-spread", 9 | "@babel/plugin-proposal-class-properties" 10 | ] 11 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = LF 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "env": { 4 | "browser": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | 9 | "extends": [ 10 | "plugin:react/recommended" 11 | ], 12 | 13 | "globals": { 14 | "document": false, 15 | "escape": false, 16 | "navigator": false, 17 | "unescape": false, 18 | "window": false, 19 | "describe": true, 20 | "before": true, 21 | "it": true, 22 | "expect": true, 23 | "sinon": true, 24 | "beforeEach": true 25 | }, 26 | 27 | "parser": "babel-eslint", 28 | 29 | "plugins": [ 30 | 31 | ], 32 | 33 | "rules": { 34 | "block-scoped-var": 2, 35 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 36 | "camelcase": [2, { "properties": "always" }], 37 | "comma-dangle": [2, "never"], 38 | "comma-spacing": [2, { "before": false, "after": true }], 39 | "comma-style": [2, "last"], 40 | "complexity": 0, 41 | "consistent-return": 2, 42 | "consistent-this": 0, 43 | "curly": [2, "multi-line"], 44 | "default-case": 0, 45 | "dot-location": [2, "property"], 46 | "dot-notation": 0, 47 | "eol-last": 2, 48 | "eqeqeq": [2, "allow-null"], 49 | "func-names": 0, 50 | "func-style": 0, 51 | "generator-star-spacing": [2, "both"], 52 | "guard-for-in": 0, 53 | "handle-callback-err": [2, "^(err|error|anySpecificError)$" ], 54 | "indent": [2, 2, { "SwitchCase": 1 }], 55 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 56 | "keyword-spacing": [2, {"before": true, "after": true}], 57 | "linebreak-style": 0, 58 | "max-depth": 0, 59 | "max-len": [2, 120, 4], 60 | "max-nested-callbacks": 0, 61 | "max-params": 0, 62 | "max-statements": 0, 63 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 64 | "newline-after-var": [2, "always"], 65 | "new-parens": 2, 66 | "no-alert": 0, 67 | "no-array-constructor": 2, 68 | "no-bitwise": 0, 69 | "no-caller": 2, 70 | "no-catch-shadow": 0, 71 | "no-cond-assign": 2, 72 | "no-console": 0, 73 | "no-constant-condition": 0, 74 | "no-continue": 0, 75 | "no-control-regex": 2, 76 | "no-debugger": 2, 77 | "no-delete-var": 2, 78 | "no-div-regex": 0, 79 | "no-dupe-args": 2, 80 | "no-dupe-keys": 2, 81 | "no-duplicate-case": 2, 82 | "no-else-return": 2, 83 | "no-empty": 0, 84 | "no-empty-character-class": 2, 85 | "no-eq-null": 0, 86 | "no-eval": 2, 87 | "no-ex-assign": 2, 88 | "no-extend-native": 2, 89 | "no-extra-bind": 2, 90 | "no-extra-boolean-cast": 2, 91 | "no-extra-parens": 0, 92 | "no-extra-semi": 0, 93 | "no-extra-strict": 0, 94 | "no-fallthrough": 2, 95 | "no-floating-decimal": 2, 96 | "no-func-assign": 2, 97 | "no-implied-eval": 2, 98 | "no-inline-comments": 0, 99 | "no-inner-declarations": [2, "functions"], 100 | "no-invalid-regexp": 2, 101 | "no-irregular-whitespace": 2, 102 | "no-iterator": 2, 103 | "no-label-var": 2, 104 | "no-labels": 2, 105 | "no-lone-blocks": 0, 106 | "no-lonely-if": 0, 107 | "no-loop-func": 0, 108 | "no-mixed-requires": 0, 109 | "no-mixed-spaces-and-tabs": [2, false], 110 | "no-multi-spaces": 2, 111 | "no-multi-str": 2, 112 | "no-multiple-empty-lines": [2, { "max": 1 }], 113 | "no-native-reassign": 2, 114 | "no-negated-in-lhs": 2, 115 | "no-nested-ternary": 0, 116 | "no-new": 2, 117 | "no-new-func": 2, 118 | "no-new-object": 2, 119 | "no-new-require": 2, 120 | "no-new-wrappers": 2, 121 | "no-obj-calls": 2, 122 | "no-octal": 2, 123 | "no-octal-escape": 2, 124 | "no-path-concat": 0, 125 | "no-plusplus": 0, 126 | "no-process-env": 0, 127 | "no-process-exit": 0, 128 | "no-proto": 2, 129 | "no-redeclare": 2, 130 | "no-regex-spaces": 2, 131 | "no-reserved-keys": 0, 132 | "no-restricted-modules": 0, 133 | "no-return-assign": 2, 134 | "no-script-url": 0, 135 | "no-self-compare": 2, 136 | "no-sequences": 2, 137 | "no-shadow": 0, 138 | "no-shadow-restricted-names": 2, 139 | "no-spaced-func": 2, 140 | "no-sparse-arrays": 2, 141 | "no-sync": 0, 142 | "no-ternary": 0, 143 | "no-throw-literal": 2, 144 | "no-trailing-spaces": 2, 145 | "no-undef": 2, 146 | "no-undef-init": 2, 147 | "no-undefined": 0, 148 | "no-underscore-dangle": 0, 149 | "no-unneeded-ternary": 2, 150 | "no-unreachable": 2, 151 | "no-unused-expressions": 0, 152 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 153 | "no-use-before-define": 2, 154 | "no-var": 0, 155 | "no-void": 0, 156 | "no-warning-comments": 0, 157 | "no-with": 2, 158 | "one-var": 0, 159 | "operator-assignment": 0, 160 | "operator-linebreak": [2, "after"], 161 | "padded-blocks": 0, 162 | "quote-props": 0, 163 | "quotes": [2, "single", "avoid-escape"], 164 | "radix": 2, 165 | "semi": [2, "always"], 166 | "semi-spacing": 0, 167 | "sort-vars": 0, 168 | "space-before-blocks": [2, "always"], 169 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 170 | "space-in-brackets": 0, 171 | "space-in-parens": [2, "never"], 172 | "space-infix-ops": 2, 173 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 174 | "spaced-comment": [2, "always"], 175 | "strict": 0, 176 | "use-isnan": 2, 177 | "valid-jsdoc": 0, 178 | "valid-typeof": 2, 179 | "vars-on-top": 2, 180 | "wrap-iife": [2, "any"], 181 | "wrap-regex": 0, 182 | "yoda": [2, "never"] 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | coverage 4 | .storybook_output 5 | .nyc_output 6 | *.log 7 | docs 8 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:8 2 | 3 | stages: 4 | - build 5 | - test 6 | - deploy 7 | 8 | before_script: 9 | - yarn install 10 | 11 | # Build stage 12 | build: 13 | script: 14 | - yarn build 15 | artifacts: 16 | paths: 17 | - lib/ 18 | 19 | # Test stage 20 | test: 21 | script: 22 | - yarn build 23 | - yarn test:coverage 24 | artifacts: 25 | paths: 26 | - coverage/ 27 | 28 | # Deploy stage 29 | pages: 30 | stage: deploy 31 | script: 32 | - mkdir public 33 | # Test coverage 34 | - yarn run artifact:test-coverage 35 | - mv coverage/ public/coverage 36 | # React Storybook 37 | - yarn run artifact:storybook 38 | - mv .storybook_output/ public/storybook 39 | artifacts: 40 | paths: 41 | - public 42 | expire_in: 30 days 43 | only: 44 | - master 45 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v6.10 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true 3 | }; 4 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | // automatically import all files ending in *.stories.js 4 | const req = require.context('../stories', true, /.stories.js$/); 5 | function loadStories() { 6 | req.keys().forEach(filename => req(filename)); 7 | } 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | resolve: { 5 | modules: [path.resolve(__dirname, '../src'), 'node_modules'], 6 | extensions: ['.json', '.js'] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "7" 5 | - "6" 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.5.0 2 | - Use Styled Components as peer dependencies and do not bundle them (#29: https://github.com/rohanchandra/react-terminal-component/pull/29) 3 | 4 | ## 1.4.1 5 | - Allow space before/after prompt symbol 6 | - Fix scroll semantics -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Rohan Chandra 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](https://user-images.githubusercontent.com/816965/38487336-1d193960-3c23-11e8-8da6-9575b0eac3e9.png) 2 | 3 | # React Terminal Component 4 | > React component that renders a terminal emulator 5 | 6 | [Demo](https://rohanchandra.gitlab.io/react-terminal-component/storybook/) 7 | 8 | ![screenshot](https://user-images.githubusercontent.com/816965/38465790-8d461534-3b63-11e8-850c-c3cd76ef5048.gif) 9 | 10 | React Terminal Component is a customizable React component backed by a [JavaScript terminal emulator](https://github.com/rohanchandra/javascript-terminal). 11 | 12 | Some of cool features of this React component are: 13 | 14 | * Emulator themes 15 | * In-memory file system 16 | * Built-in commands like `ls`, `cd`, `head`, `cat`, `echo`, `rm` and more 17 | * Autocompletion of terminal commands 18 | * Keyboard navigation of past commands using up and down arrow key 19 | 20 | ## Installation 21 | Install with `npm` or with `yarn`. 22 | 23 | ```shell 24 | npm install react-terminal-component javascript-terminal --save 25 | ``` 26 | 27 | ```shell 28 | yarn add react-terminal-component javascript-terminal 29 | ``` 30 | 31 | ## Usage 32 | 33 | ### React 34 | ```javascript 35 | import React, { Component } from 'react'; 36 | import ReactTerminal from 'react-terminal-component'; 37 | 38 | class App extends Component { 39 | render() { 40 | return ( 41 |
42 | 43 |
44 | ); 45 | } 46 | } 47 | ``` 48 | 49 | ### Props 50 | 51 | All of these props apply to both `ReactTerminal` and `ReactTerminalStateless`. 52 | 53 | #### emulatorState 54 | 55 | * [Code examples](https://github.com/rohanchandra/react-terminal-component/blob/master/stories/ReactTerminal_state.stories.js) 56 | * [Demo](https://rohanchandra.gitlab.io/react-terminal-component/storybook/?selectedKind=ReactTerminal%20-%20Emulator%20State) 57 | 58 | Emulator state is created using the [JavaScript terminal emulator library](https://github.com/rohanchandra/javascript-terminal) and contains: 59 | 60 | * the file system, 61 | * command mapping, 62 | * history, 63 | * command outputs, and 64 | * environment variables 65 | 66 | The `emulatorState` prop allows you to provide a custom emulator state. 67 | 68 | See the [library documentation](https://github.com/rohanchandra/javascript-terminal#emulator-state) for information on creating the emulator state, or view the [code examples](https://github.com/rohanchandra/react-terminal-component/blob/master/stories/ReactTerminal_state.stories.js) to get started. 69 | 70 | #### theme 71 | 72 | * [Code examples](https://github.com/rohanchandra/react-terminal-component/blob/master/stories/ReactTerminal_themes.stories.js) 73 | * [Demo](https://rohanchandra.gitlab.io/react-terminal-component/storybook/?selectedKind=ReactTerminal%20-%20Themes) 74 | 75 | The `theme` prop accepts an object from `ReactThemes`. The themes current available are: 76 | 77 | * `ReactThemes.magpie` 78 | * `ReactThemes.ember` 79 | * `ReactThemes.dye` 80 | * `ReactThemes.forest` 81 | * `ReactThemes.hacker` 82 | * `ReactThemes.sea` 83 | * `ReactThemes.light` 84 | 85 | To import `ReactThemes` use the following code: 86 | 87 | ``` 88 | import { ReactThemes } from 'react-terminal-component'; 89 | ``` 90 | 91 | Alternatively, you can specify your own theme with an object like this: 92 | 93 | ``` 94 | 106 | ``` 107 | 108 | #### promptSymbol 109 | 110 | * [Code examples](https://github.com/rohanchandra/react-terminal-component/blob/master/stories/ReactTerminal_prompt.stories.js) 111 | * [Demo](https://rohanchandra.gitlab.io/react-terminal-component/storybook/?selectedKind=ReactTerminal%20-%20Prompt%20symbol) 112 | 113 | The `promptSymbol` prop accepts a string to be displayed in command headers and the input field. 114 | 115 | #### outputRenderers 116 | 117 | * [Code example](https://github.com/rohanchandra/react-terminal-component/blob/master/stories/ReactTerminal_output.stories.js) 118 | * [Demo](https://rohanchandra.gitlab.io/react-terminal-component/storybook/?selectedKind=ReactTerminal%20-%20OutputRenderer) 119 | 120 | The `outputRenderers` prop allows you to create new ways of displaying terminal output. See the [code example](https://github.com/rohanchandra/react-terminal-component/blob/master/stories/ReactTerminal_output.stories.js), which creates a new type of output (output with a white background). 121 | 122 | The default renderers are accessible in `ReactOutputRenderers`. 123 | 124 | ``` 125 | import { ReactOutputRenderers } from 'react-terminal-component'; 126 | ``` 127 | 128 | #### acceptInput 129 | 130 | The `acceptInput` prop is a Boolean value, defaulting to true. When disabled, the input field is removed. This may be useful in conjunction with `ReactTerminalStateless` if you're managing state externally and simulating long-running commands. 131 | 132 | * [Code examples](https://github.com/rohanchandra/react-terminal-component/blob/master/stories/ReactTerminal.stories.js) 133 | * [Demo](https://rohanchandra.gitlab.io/react-terminal-component/storybook/?selectedKind=ReactTerminal&selectedStory=with%20acceptInput%3Dfalse) 134 | 135 | #### autoFocus 136 | 137 | The `autoFocus` prop defaults to true and causes the input field to gain focus when the component is first mounted and whenever the component is updated i.e. if props change. This prop can work in conjunction with `clickToFocus`. 138 | 139 | * [Code examples](https://github.com/rohanchandra/react-terminal-component/blob/master/stories/ReactTerminal.stories.js) 140 | * [Demo](https://rohanchandra.gitlab.io/react-terminal-component/storybook/?selectedKind=ReactTerminal&selectedStory=with%20autoFocus%3Dfalse) 141 | 142 | #### clickToFocus 143 | 144 | The `clickToFocus` prop is a Boolean value, defaulting to false. When enabled, clicking anywhere within the terminal will shift focus to the input field. 145 | 146 | * [Code examples](https://github.com/rohanchandra/react-terminal-component/blob/master/stories/ReactTerminal.stories.js) 147 | * [Demo](https://rohanchandra.gitlab.io/react-terminal-component/storybook/?selectedKind=ReactTerminal&selectedStory=with%20clickToFocus) 148 | 149 | ### Managing state externally 150 | 151 | The `ReactTerminal` component allows the initial values of `emulatorState` and `inputStr` to be specified, but thereafter the component handles the state internally. In some cases, you may need to manage the state externally from the `react-terminal-component` module. 152 | 153 | You can use `ReactTerminalStateless` to control the state of `emulatorState` and `inputStr`, but you must also supply the `onInputChange` and `onStateChange` props. You'll need to use `javascript-terminal` in order to modify the `emulatorState`. 154 | 155 | This is a simple component which handles the terminal state. You could adapt this idea to make the state be handled by Redux and to modify the state asynchronously, not in direct response to a command within the terminal. 156 | 157 | ```javascript 158 | import React, { Component } from 'react'; 159 | import {ReactTerminalStateless} from 'react-terminal-component'; 160 | 161 | class App extends Component { 162 | constructor() { 163 | super(); 164 | this.state = { 165 | emulatorState: EmulatorState.createEmpty(), 166 | inputStr: 'initial value' 167 | }; 168 | } 169 | 170 | render() { 171 | return ( 172 | this.setState({inputStr})} 176 | onStateChange={(emulatorState) => this.setState({emulatorState})} 177 | /> 178 | ); 179 | } 180 | } 181 | ``` 182 | 183 | ## Building 184 | 185 | ### Set-up 186 | 187 | First, make sure you have [Node.js](https://nodejs.org/en/download/), [Yarn](https://yarnpkg.com/en/docs/install) and [Git](https://git-scm.com/downloads) installed. 188 | 189 | Now, fork and clone repo and install the dependencies. 190 | 191 | ```shell 192 | git clone https://github.com/rohanchandra/react-terminal-component.git 193 | cd react-terminal-component/ 194 | yarn install 195 | ``` 196 | 197 | ### Scripts 198 | 199 | #### Build scripts 200 | * `yarn build` - creates a production build of the library in `lib` 201 | * `yarn dev` - creates a development build of the library and runs a watcher 202 | 203 | #### Test scripts 204 | * `yarn test` - run tests 205 | * `yarn test:min` - run tests with summary reports 206 | * `yarn test:coverage` - shows test coverage stats 207 | 208 | #### Artifacts 209 | * `yarn artifact:coverage-report` - creates HTML test coverage report in `.nyc_output` 210 | * `yarn artifact:storybook` - emulator demos 211 | 212 | ## Potential uses 213 | 214 | Some ideas for using React Terminal Component in your next project: 215 | 216 | * **Games**: Create a command-line based game, playable in the browser 217 | * **Education**: Teach popular *NIX commands sandboxed in the browser (no important files accidentally removed with `rm -r`!) 218 | * **Personal website**: Make your personal website or web resume a command-line interface 219 | * **Demos**: Create mock commands in JavaScript for your CLI app, and let users try out commands in their browser with simulated output 220 | 221 | ## License 222 | 223 | Copyright 2018 Rohan Chandra 224 | 225 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 226 | 227 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 228 | 229 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 230 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-terminal-component", 3 | "version": "1.5.0", 4 | "description": "React component that renders a terminal emulator", 5 | "main": "lib/react-terminal-component.js", 6 | "scripts": { 7 | "build": "webpack --mode development && webpack --mode production", 8 | "dev": "webpack --progress --colors --watch --mode development", 9 | "test": "cross-env NODE_PATH=./src mocha --require @babel/register --colors './test/**/*.spec.js'", 10 | "test:min": "yarn run test --reporter min", 11 | "test:coverage": "nyc yarn run test", 12 | "storybook": "start-storybook -p 6006", 13 | "artifact:test-coverage": "nyc --reporter=html yarn run test", 14 | "artifact:storybook": "build-storybook -c .storybook -o .storybook_output", 15 | "build-storybook": "build-storybook" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/rohanchandra/react-terminal-component.git" 20 | }, 21 | "keywords": [ 22 | "terminal", 23 | "emulation" 24 | ], 25 | "author": "Rohan Chandra", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/rohanchandra/react-terminal-component/issues" 29 | }, 30 | "homepage": "https://github.com/rohanchandra/react-terminal-component", 31 | "nyc": { 32 | "exclude": [ 33 | "test", 34 | "lib" 35 | ] 36 | }, 37 | "devDependencies": { 38 | "@babel/cli": "^7.4.4", 39 | "@babel/core": "^7.4.5", 40 | "@babel/plugin-proposal-class-properties": "^7.4.4", 41 | "@babel/plugin-proposal-object-rest-spread": "^7.4.4", 42 | "@babel/register": "^7.4.4", 43 | "@storybook/addon-actions": "^5.1.9", 44 | "@storybook/addon-links": "^5.1.9", 45 | "@storybook/addons": "^5.1.9", 46 | "@storybook/react": "^5.1.9", 47 | "babel-eslint": "^10.0.2", 48 | "babel-loader": "^8.0.6", 49 | "babel-plugin-add-module-exports": "^1.0.2", 50 | "babel-preset-env": "^1.7.0", 51 | "babel-preset-react": "^6.24.1", 52 | "chai": "^4.2.0", 53 | "cross-env": "^5.2.0", 54 | "enzyme": "^3.10.0", 55 | "enzyme-adapter-react-16": "^1.14.0", 56 | "eslint": "^5.16.0", 57 | "eslint-loader": "^2.1.2", 58 | "eslint-plugin-react": "^7.13.0", 59 | "mocha": "^6.1.4", 60 | "nyc": "^14.1.1", 61 | "styled-components": "^5.2.1", 62 | "webpack": "^4.35.0", 63 | "webpack-cli": "^3.3.4", 64 | "yargs": "^13.2.4" 65 | }, 66 | "dependencies": { 67 | "javascript-terminal": "^1.0.3", 68 | "prop-types": "^15.7.2" 69 | }, 70 | "peerDependencies": { 71 | "react": "^15.0.0 || ^16.0.0", 72 | "react-dom": "^15.0.0 || ^16.0.0", 73 | "styled-components": "^5.0.0" 74 | }, 75 | "files": [ 76 | "lib/react-terminal-component.js", 77 | "lib/react-terminal-component.min.js" 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /src/OutputList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const OutputList = ({ outputRenderers, outputs, terminalId, ...outputRendererProps }) => ( 5 |
6 | {outputs.map((output, index) => { 7 | const type = output.get('type'); 8 | const content = output.get('content'); 9 | 10 | if (!outputRenderers.hasOwnProperty(type)) { 11 | throw new Error(`No output renderer set for ${type} in outputRenderers`); 12 | } 13 | 14 | const OutputComponent = outputRenderers[type]; 15 | 16 | return ( 17 | 21 | ); 22 | })} 23 |
24 | ); 25 | 26 | OutputList.propTypes = { 27 | outputRenderers: PropTypes.object.isRequired, 28 | outputs: PropTypes.object.isRequired, 29 | terminalId: PropTypes.string, 30 | }; 31 | 32 | export default OutputList; 33 | -------------------------------------------------------------------------------- /src/PromptSymbol.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const PromptSymbol = styled.span` 4 | margin-right: 0.25em; 5 | white-space: pre; 6 | color: ${({ theme }) => theme.promptSymbolColor}; 7 | `; 8 | 9 | export default PromptSymbol; 10 | -------------------------------------------------------------------------------- /src/ReactTerminal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ReactTerminalStateless from 'ReactTerminalStateless'; 4 | import TerminalStateless from './ReactTerminalStateless'; 5 | 6 | class Terminal extends Component { 7 | constructor({emulatorState, inputStr}) { 8 | super(); 9 | 10 | this.state = { 11 | emulatorState, 12 | inputStr 13 | }; 14 | } 15 | 16 | _init(props) { 17 | const {emulatorState, inputStr} = props; 18 | 19 | this.setState({ 20 | emulatorState, 21 | inputStr 22 | }); 23 | } 24 | 25 | componentDidMount() { 26 | this._init(this.props); 27 | } 28 | 29 | componentWillReceiveProps(nextProps) { 30 | if (nextProps) { 31 | this._init(nextProps); 32 | } 33 | } 34 | 35 | _onInputChange = (inputStr) => { 36 | this.setState({inputStr}); 37 | } 38 | 39 | _onStateChange = (emulatorState) => { 40 | this.setState({emulatorState, inputStr: ''}); 41 | } 42 | 43 | render() { 44 | // eslint-disable-next-line no-unused-vars 45 | const {emulatorState: removedEmulatorState, inputStr: removedInputStr, ...otherProps} = this.props; 46 | const {emulatorState, inputStr} = this.state; 47 | 48 | // We're using the spread operator to pass along all props to the child componentm 49 | // except for emulatorState and inputStr which must come from the state. 50 | return ( 51 | 58 | ); 59 | } 60 | }; 61 | 62 | Terminal.propTypes = { 63 | ...TerminalStateless.commonPropTypes, 64 | emulatorState: PropTypes.object, 65 | inputStr: PropTypes.string 66 | }; 67 | 68 | Terminal.defaultProps = { 69 | ...TerminalStateless.defaultProps 70 | }; 71 | 72 | export default Terminal; 73 | -------------------------------------------------------------------------------- /src/ReactTerminalStateless.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import { ThemeProvider } from 'styled-components'; 4 | import { 5 | Emulator, HistoryKeyboardPlugin, EmulatorState 6 | } from 'javascript-terminal'; 7 | import defaultTheme from 'themes/default'; 8 | import CommandInput from 'input/CommandInput'; 9 | import OutputList from 'OutputList'; 10 | import TerminalContainer from 'TerminalContainer'; 11 | import defaultRenderers from 'output'; 12 | 13 | class TerminalStateless extends Component { 14 | constructor({emulatorState}) { 15 | super(); 16 | 17 | this.emulator = new Emulator(); 18 | this.historyKeyboardPlugin = new HistoryKeyboardPlugin(emulatorState); 19 | this.plugins = [this.historyKeyboardPlugin]; 20 | this.inputRef = null; 21 | this.containerRef = null; 22 | this.dragStart = {}; 23 | this.dragging = false; 24 | } 25 | 26 | focus() { 27 | if (this.inputRef) { 28 | this.inputRef.focus(); 29 | } 30 | } 31 | 32 | scrollOutput() { 33 | this.containerRef.scrollTop = this.containerRef.scrollHeight; 34 | } 35 | 36 | componentDidUpdate() { 37 | const {autoFocus} = this.props; 38 | 39 | this.scrollOutput(); 40 | 41 | if (autoFocus) { 42 | this.focus(); 43 | } 44 | } 45 | 46 | _submitInput = (commandStr) => { 47 | const {onStateChange, emulatorState} = this.props; 48 | const newState = this.emulator.execute( 49 | emulatorState, commandStr, this.plugins 50 | ); 51 | 52 | onStateChange(newState); 53 | } 54 | 55 | _setInput(inputStr) { 56 | const {onInputChange} = this.props; 57 | 58 | onInputChange(inputStr); 59 | } 60 | 61 | _onInputKeyDownEvent = (e) => { 62 | switch (e.key) { 63 | case 'ArrowUp': 64 | e.preventDefault(); 65 | this._setInput(this.historyKeyboardPlugin.completeUp()); 66 | break; 67 | 68 | case 'ArrowDown': 69 | e.preventDefault(); 70 | this._setInput(this.historyKeyboardPlugin.completeDown()); 71 | break; 72 | 73 | case 'Tab': 74 | e.preventDefault(); 75 | 76 | const autoCompletedStr = this.emulator.autocomplete( 77 | this.props.emulatorState, this.props.inputStr 78 | ); 79 | 80 | this._setInput(autoCompletedStr); 81 | break; 82 | } 83 | } 84 | 85 | _onClick = () => { 86 | if (this.inputRef && !this.dragging) { 87 | this.scrollOutput(); 88 | this.inputRef.focus(); 89 | } 90 | }; 91 | 92 | _onMouseDown = (e) => { 93 | this.dragging = false; 94 | this.dragStart = { 95 | x: e.screenX, 96 | y: e.screenY 97 | }; 98 | } 99 | 100 | _onMouseUp = (e) => { 101 | if (this.dragStart.x === e.screenX && this.dragStart.y === e.screenY) { 102 | this.dragging = false; 103 | } else { 104 | // For the next 100ms consider any click event to be part of this drag. 105 | this.dragging = true; 106 | setTimeout(() => { this.isDragging = false; }, 100, this); 107 | } 108 | } 109 | 110 | render() { 111 | const { 112 | acceptInput, autoFocus, clickToFocus, emulatorState, inputStr, theme, promptSymbol, outputRenderers, terminalId 113 | } = this.props; 114 | let inputControl, focusProps; 115 | 116 | if (!emulatorState) { 117 | return null; 118 | } 119 | 120 | if (clickToFocus) { 121 | focusProps = { 122 | onClick: this._onClick, 123 | onMouseDown: this._onMouseDown, 124 | onMouseUp: this._onMouseUp 125 | }; 126 | } 127 | 128 | if (acceptInput) { 129 | inputControl = ( 130 | { this.inputRef = ref; }} 132 | autoFocus={autoFocus} 133 | promptSymbol={promptSymbol} 134 | value={inputStr} 135 | onSubmit={this._submitInput} 136 | onKeyDown={this._onInputKeyDownEvent} 137 | onChange={(e) => this._setInput(e.target.value)} 138 | /> 139 | ); 140 | } 141 | 142 | return ( 143 | 144 | { this.containerRef = ref; }} 147 | {...focusProps} 148 | > 149 | 155 | {inputControl} 156 | 157 | 158 | ); 159 | } 160 | }; 161 | 162 | // These props are shared with ReactTerminal. 163 | TerminalStateless.commonPropTypes = { 164 | acceptInput: PropTypes.bool, 165 | autoFocus: PropTypes.bool, 166 | clickToFocus: PropTypes.bool, 167 | outputRenderers: PropTypes.object, 168 | promptSymbol: PropTypes.string, 169 | terminalId: PropTypes.string, 170 | theme: PropTypes.object 171 | }; 172 | 173 | TerminalStateless.propTypes = { 174 | ...TerminalStateless.commonPropTypes, 175 | emulatorState: PropTypes.object.isRequired, 176 | inputStr: PropTypes.string.isRequired, 177 | onInputChange: PropTypes.func.isRequired, 178 | onStateChange: PropTypes.func.isRequired 179 | }; 180 | 181 | TerminalStateless.defaultProps = { 182 | acceptInput: true, 183 | autoFocus: true, 184 | clickToFocus: false, 185 | emulatorState: EmulatorState.createEmpty(), 186 | inputStr: '', 187 | outputRenderers: defaultRenderers, 188 | promptSymbol: '$', 189 | terminalId: 'terminal01', 190 | theme: defaultTheme 191 | }; 192 | 193 | export default TerminalStateless; 194 | -------------------------------------------------------------------------------- /src/TerminalContainer.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const TerminalContainer = styled.div` 4 | & > :last-child { 5 | padding-bottom: ${({ theme }) => theme.spacing}; 6 | } 7 | height: ${({ theme }) => theme.height}; 8 | width: ${({ theme }) => theme.width}; 9 | line-height: 1.2em; 10 | padding: ${({ theme }) => theme.spacing}; 11 | overflow-y: scroll; 12 | color: ${({ theme }) => theme.outputColor}; 13 | background: ${({ theme }) => theme.background}; 14 | font-family: monospace; 15 | font-size: ${({ theme }) => theme.fontSize}; 16 | `; 17 | 18 | export default TerminalContainer; 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ReactTerminal from 'ReactTerminal'; 2 | import ReactTerminalStateless from 'ReactTerminalStateless'; 3 | import ReactThemes from 'themes'; 4 | import ReactOutputRenderers from 'output'; 5 | 6 | export default ReactTerminal; 7 | 8 | export { 9 | ReactTerminal, ReactTerminalStateless, ReactThemes, ReactOutputRenderers 10 | }; 11 | -------------------------------------------------------------------------------- /src/input/CommandInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import StyledInput from 'input/StyledInput'; 3 | import StyledForm from 'input/StyledForm'; 4 | import PromptSymbol from 'PromptSymbol'; 5 | import PropTypes from 'prop-types'; 6 | 7 | class CommandInput extends Component { 8 | focus() { 9 | this.input.focus(); 10 | } 11 | 12 | render() { 13 | const { autoFocus, promptSymbol, value, onChange, onSubmit, onKeyDown } = this.props; 14 | 15 | return ( 16 |
17 | { 20 | e.preventDefault(); 21 | onSubmit(this.input.value); 22 | }} 23 | > 24 | {promptSymbol} 25 | { 28 | e.persist(); 29 | onChange(e); 30 | }} 31 | value={value} 32 | ref={ref => (this.input = ref)} 33 | /> 34 | 35 |
36 | ); 37 | } 38 | }; 39 | 40 | CommandInput.propTypes = { 41 | autoFocus: PropTypes.bool.isRequired, 42 | onSubmit: PropTypes.func.isRequired, 43 | onChange: PropTypes.func.isRequired, 44 | onKeyDown: PropTypes.func.isRequired, 45 | promptSymbol: PropTypes.string.isRequired, 46 | value: PropTypes.string.isRequired 47 | }; 48 | 49 | export default CommandInput; 50 | -------------------------------------------------------------------------------- /src/input/StyledForm.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const StyledForm = styled.form` 4 | display: flex; 5 | `; 6 | 7 | export default StyledForm; 8 | -------------------------------------------------------------------------------- /src/input/StyledInput.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const StyledInput = styled.input` 4 | flex: 1; 5 | border: 0; 6 | box-sizing: border-box; 7 | outline: none; 8 | color: ${({ theme }) => theme.commandColor}; 9 | background: ${({ theme }) => theme.background}; 10 | font-size: 1em; 11 | font-family: monospace; 12 | padding: 0; 13 | `; 14 | 15 | export default StyledInput; 16 | -------------------------------------------------------------------------------- /src/output/HeaderOutput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import PromptSymbol from 'PromptSymbol'; 4 | import OutputContainer from 'output/OutputContainer'; 5 | import TextCommandWrapper from 'output/TextCommandWrapper'; 6 | 7 | const HeaderOutput = ({ content, promptSymbol }) => ( 8 | 9 | {promptSymbol}{content.command} 10 | 11 | ); 12 | 13 | HeaderOutput.propTypes = { 14 | content: PropTypes.object.isRequired, 15 | promptSymbol: PropTypes.string.isRequired 16 | }; 17 | 18 | export default HeaderOutput; 19 | -------------------------------------------------------------------------------- /src/output/OutputContainer.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const OutputContainer = styled.div` 4 | white-space: pre; 5 | `; 6 | 7 | export default OutputContainer; 8 | -------------------------------------------------------------------------------- /src/output/TextCommandWrapper.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const TextCommandWrapper = styled.span` 4 | color: ${({ theme }) => theme.commandColor}; 5 | `; 6 | 7 | export default TextCommandWrapper; 8 | -------------------------------------------------------------------------------- /src/output/TextErrorOutput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import OutputContainer from 'output/OutputContainer'; 4 | import TextErrorWrapper from 'output/TextErrorWrapper'; 5 | 6 | const TextErrorOutput = ({ content }) => ( 7 | 8 | {content} 9 | 10 | ); 11 | 12 | TextErrorOutput.propTypes = { 13 | content: PropTypes.string.isRequired 14 | }; 15 | 16 | export default TextErrorOutput; 17 | -------------------------------------------------------------------------------- /src/output/TextErrorWrapper.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const TextErrorWrapper = styled.div` 4 | color: ${({ theme }) => theme.errorOutputColor}; 5 | `; 6 | 7 | export default TextErrorWrapper; 8 | -------------------------------------------------------------------------------- /src/output/TextOutput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import OutputContainer from 'output/OutputContainer'; 4 | 5 | const TextOutput = ({ content }) => ( 6 | {content} 7 | ); 8 | 9 | TextOutput.propTypes = { 10 | content: PropTypes.string.isRequired 11 | }; 12 | 13 | export default TextOutput; 14 | -------------------------------------------------------------------------------- /src/output/index.js: -------------------------------------------------------------------------------- 1 | import { OutputType } from 'javascript-terminal'; 2 | import TextOutput from 'output/TextOutput'; 3 | import TextErrorOutput from 'output/TextErrorOutput'; 4 | import HeaderOutput from 'output/HeaderOutput'; 5 | 6 | export default { 7 | [OutputType.TEXT_OUTPUT_TYPE]: TextOutput, 8 | [OutputType.TEXT_ERROR_OUTPUT_TYPE]: TextErrorOutput, 9 | [OutputType.HEADER_OUTPUT_TYPE]: HeaderOutput 10 | }; 11 | -------------------------------------------------------------------------------- /src/themes/default.js: -------------------------------------------------------------------------------- 1 | const defaultColors = { 2 | background: '#141313', 3 | promptSymbolColor: '#6effe6', 4 | commandColor: '#fcfcfc', 5 | outputColor: '#fcfcfc', 6 | errorOutputColor: '#ff89bd' 7 | }; 8 | 9 | export default { 10 | ...defaultColors, 11 | fontSize: '1.1rem', 12 | spacing: '1%', 13 | fontFamily: 'monospace', 14 | height: '50vh', 15 | width: '100%' 16 | }; 17 | -------------------------------------------------------------------------------- /src/themes/dye.js: -------------------------------------------------------------------------------- 1 | import defaultTheme from 'themes/default'; 2 | 3 | export default { 4 | ...defaultTheme, 5 | background: '#282a38', 6 | commandColor: '#acffe7', 7 | outputColor: '#fff', 8 | errorOutputColor: '#ff8e8e', 9 | promptSymbolColor: '#f8aeff' 10 | }; 11 | -------------------------------------------------------------------------------- /src/themes/ember.js: -------------------------------------------------------------------------------- 1 | import defaultTheme from 'themes/default'; 2 | 3 | export default { 4 | ...defaultTheme, 5 | background: '#fff4d7', 6 | commandColor: '#bb8b41', 7 | outputColor: '#8b7159', 8 | errorOutputColor: '#b54828', 9 | promptSymbolColor: '#bb8b41' 10 | }; 11 | -------------------------------------------------------------------------------- /src/themes/forest.js: -------------------------------------------------------------------------------- 1 | import defaultTheme from 'themes/default'; 2 | 3 | export default { 4 | ...defaultTheme, 5 | background: '#2d4422', 6 | commandColor: '#ffcc60', 7 | outputColor: '#ffcc60', 8 | errorOutputColor: '#e5e67f', 9 | promptSymbolColor: '#ffad00' 10 | }; 11 | -------------------------------------------------------------------------------- /src/themes/hacker.js: -------------------------------------------------------------------------------- 1 | import defaultTheme from 'themes/default'; 2 | 3 | export default { 4 | ...defaultTheme, 5 | background: '#181e19', 6 | commandColor: '#a3ffb0', 7 | outputColor: '#a3ffb0', 8 | errorOutputColor: '#ffa1a1', 9 | promptSymbolColor: '#a3ffb0' 10 | }; 11 | -------------------------------------------------------------------------------- /src/themes/index.js: -------------------------------------------------------------------------------- 1 | import magpie from './magpie'; 2 | import light from './light'; 3 | import ember from './ember'; 4 | import dye from './dye'; 5 | import forest from './forest'; 6 | import hacker from './hacker'; 7 | import sea from './sea'; 8 | import defaultTheme from './default'; 9 | 10 | export default { 11 | magpie, 12 | light, 13 | ember, 14 | dye, 15 | forest, 16 | hacker, 17 | sea, 18 | default: defaultTheme 19 | }; 20 | -------------------------------------------------------------------------------- /src/themes/light.js: -------------------------------------------------------------------------------- 1 | import defaultTheme from 'themes/default'; 2 | 3 | export default { 4 | ...defaultTheme, 5 | background: 'white', 6 | commandColor: 'black', 7 | outputColor: 'black', 8 | errorOutputColor: '#e856ee', 9 | promptSymbolColor: '#9156ff' 10 | }; 11 | -------------------------------------------------------------------------------- /src/themes/magpie.js: -------------------------------------------------------------------------------- 1 | import defaultTheme from 'themes/default'; 2 | 3 | export default { 4 | ...defaultTheme, 5 | background: 'black', 6 | commandColor: 'white', 7 | outputColor: 'white', 8 | errorOutputColor: '#ff8383', 9 | promptSymbolColor: 'white' 10 | }; 11 | -------------------------------------------------------------------------------- /src/themes/sea.js: -------------------------------------------------------------------------------- 1 | import defaultTheme from 'themes/default'; 2 | 3 | export default { 4 | ...defaultTheme, 5 | background: '#0e2933', 6 | commandColor: '#6efeff', 7 | outputColor: '#cefffa', 8 | errorOutputColor: '#b6a6ff', 9 | promptSymbolColor: '#6efeff' 10 | }; 11 | -------------------------------------------------------------------------------- /stories/ReactTerminal.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | EmulatorState, OutputFactory, Outputs 4 | } from 'javascript-terminal'; 5 | import { storiesOf } from '@storybook/react'; 6 | import ReactTerminal from 'ReactTerminal'; 7 | 8 | storiesOf('ReactTerminal', module) 9 | .add('with default config', () => ( 10 | 11 | )) 12 | .add('with clickToFocus', () => ( 13 | 14 | )) 15 | .add('with acceptInput=false', () => { 16 | const defaultState = EmulatorState.createEmpty(); 17 | const defaultOutputs = defaultState.getOutputs(); 18 | 19 | const newOutputs = Outputs.addRecord( 20 | defaultOutputs, OutputFactory.makeTextOutput('This terminal is read-only.') 21 | ); 22 | const emulatorState = defaultState.setOutputs(newOutputs); 23 | 24 | return ; 25 | }) 26 | .add('with autoFocus=false', () => ( 27 | 28 | )); 29 | 30 | -------------------------------------------------------------------------------- /stories/ReactTerminalStateless.stories.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import {EmulatorState} from 'javascript-terminal'; 4 | import ReactTerminalStateless from 'ReactTerminalStateless'; 5 | 6 | class StatelessTerminalWrapper extends Component { 7 | constructor() { 8 | super(); 9 | this.state = { 10 | emulatorState: EmulatorState.createEmpty(), 11 | inputStr: 'initial val' 12 | }; 13 | } 14 | 15 | render() { 16 | return ( 17 | this.setState({inputStr})} 21 | onStateChange={(emulatorState) => this.setState({emulatorState, inputStr: ''})} 22 | /> 23 | ); 24 | } 25 | }; 26 | 27 | storiesOf('ReactTerminalStateless', module) 28 | .add('with simple wrapper', () => ); 29 | -------------------------------------------------------------------------------- /stories/ReactTerminal_output.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import ReactTerminal from 'ReactTerminal'; 5 | import ReactOutputRenderers from 'output'; 6 | import { 7 | CommandMapping, OutputFactory, EmulatorState, 8 | defaultCommandMapping 9 | } from 'javascript-terminal'; 10 | 11 | const PAPER_TYPE = 'paper'; 12 | 13 | const paperStyles = { 14 | backgroundColor: 'white', 15 | color: 'black', 16 | fontFamily: 'sans-serif', 17 | padding: '1em', 18 | margin: '1em 0', 19 | borderRadius: '0.2em' 20 | }; 21 | 22 | const PaperOutput = ({ content }) => ( 23 |
24 |

{content.title}

25 | 26 | {content.body} 27 |
28 | ); 29 | 30 | const createPaperRecord = (title, body) => { 31 | return new OutputFactory.OutputRecord({ 32 | type: PAPER_TYPE, 33 | content: { 34 | title, 35 | body 36 | } 37 | }); 38 | }; 39 | 40 | storiesOf('ReactTerminal - OutputRenderer', module) 41 | .add('with new command and renderer', () => { 42 | // Add a print command that outputs a PaperRecord 43 | const customState = EmulatorState.create({ 44 | 'commandMapping': CommandMapping.create({ 45 | ...defaultCommandMapping, 46 | 'print': { 47 | 'function': (state, opts) => { 48 | const userInput = opts.join(' '); 49 | 50 | return { 51 | output: createPaperRecord('A custom renderer', userInput) 52 | }; 53 | }, 54 | 'optDef': {} 55 | } 56 | }) 57 | }); 58 | 59 | // Add rendering support for the PaperRecord using PaperOutput as the renderer 60 | return ( 61 | 68 | ); 69 | }); 70 | -------------------------------------------------------------------------------- /stories/ReactTerminal_prompt.stories.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import ReactTerminal from 'ReactTerminal'; 5 | 6 | storiesOf('ReactTerminal - Prompt symbol', module) 7 | .add('with text symbol', () => ( 8 | 9 | )) 10 | .add('with emoji symbol', () => ( 11 | 12 | )); 13 | -------------------------------------------------------------------------------- /stories/ReactTerminal_state.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import ReactTerminal from 'ReactTerminal'; 5 | import { 6 | EmulatorState, OutputFactory, CommandMapping, 7 | EnvironmentVariables, FileSystem, History, 8 | Outputs, defaultCommandMapping 9 | } from 'javascript-terminal'; 10 | 11 | storiesOf('ReactTerminal - Emulator State', module) 12 | .add('with custom file system', () => { 13 | const customState = EmulatorState.create({ 14 | 'fs': FileSystem.create({ 15 | '/home': { }, 16 | '/home/README': {content: 'This is a text file'}, 17 | '/home/nested/directory': {}, 18 | '/home/nested/directory/file': {content: 'End of nested directory!'} 19 | }) 20 | }); 21 | 22 | return ( 23 | 26 | ); 27 | }) 28 | .add('with existing outputs', () => { 29 | const defaultState = EmulatorState.createEmpty(); 30 | const defaultOutputs = defaultState.getOutputs(); 31 | 32 | const newOutputs = Outputs.addRecord( 33 | defaultOutputs, OutputFactory.makeTextOutput( 34 | `This is an example of using output to display a message 35 | to the user before any commands are run.` 36 | ) 37 | ); 38 | const emulatorState = defaultState.setOutputs(newOutputs); 39 | 40 | return ( 41 | 44 | ); 45 | }) 46 | .add('with history', () => { 47 | const defaultState = EmulatorState.createEmpty(); 48 | const defaultHistory = defaultState.getHistory(); 49 | 50 | const newHistory = History.recordCommand(defaultHistory, 'history'); 51 | const emulatorState = defaultState.setHistory(newHistory); 52 | 53 | return ( 54 | 57 | ); 58 | }) 59 | .add('with new environment variable', () => { 60 | const defaultState = EmulatorState.createEmpty(); 61 | const defaultEnvVariables = defaultState.getEnvVariables(); 62 | 63 | const newEnvVariables = EnvironmentVariables.setEnvironmentVariable( 64 | defaultEnvVariables, 'CUSTOM_ENV_VARIABLE', 'this is the value' 65 | ); 66 | const emulatorState = defaultState.setEnvVariables(newEnvVariables); 67 | 68 | return ( 69 | 72 | ); 73 | }) 74 | .add('with new command', () => { 75 | const customState = EmulatorState.create({ 76 | 'commandMapping': CommandMapping.create({ 77 | ...defaultCommandMapping, 78 | 'print': { 79 | 'function': (state, opts) => { 80 | const input = opts.join(' '); 81 | 82 | return { 83 | output: OutputFactory.makeTextOutput(input) 84 | }; 85 | }, 86 | 'optDef': {} 87 | } 88 | }) 89 | }); 90 | 91 | return ( 92 | 95 | ); 96 | }); 97 | -------------------------------------------------------------------------------- /stories/ReactTerminal_themes.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import ReactTerminal from 'ReactTerminal'; 5 | import ReactThemes from 'themes'; 6 | 7 | const demoThemeProps = { 8 | inputStr: 'echo command text looks like this' 9 | }; 10 | 11 | storiesOf('ReactTerminal - Themes', module) 12 | .add('with default theme', () => ( 13 | 14 | )) 15 | .add('with custom height and width', () => ( 16 | 17 | )) 18 | .add('with Magpie theme', () => ( 19 | 20 | )) 21 | .add('with Ember theme', () => ( 22 | 23 | )) 24 | .add('with Dye theme', () => ( 25 | 26 | )) 27 | .add('with Forest theme', () => ( 28 | 29 | )) 30 | .add('with Hacker theme', () => ( 31 | 32 | )) 33 | .add('with Sea theme', () => ( 34 | 35 | )) 36 | .add('with Light theme', () => ( 37 | 38 | )); 39 | -------------------------------------------------------------------------------- /test/ReactTerminal.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import React from 'react'; 3 | import { configure, shallow } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import ReactTerminal from 'ReactTerminal'; 6 | import ReactTerminalStateless from 'ReactTerminalStateless'; 7 | 8 | describe('ReactTerminal', () => { 9 | configure({ 10 | adapter: new Adapter() // React adapter for enzyme 11 | }); 12 | 13 | it('should render', () => { 14 | const container = shallow(); 15 | 16 | chai.assert.isDefined(container); 17 | }); 18 | 19 | it('should have ReactTerminalStateless component', () => { 20 | const container = shallow(); 21 | 22 | chai.expect(container.find(ReactTerminalStateless).length).to.equal(1); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/ReactTerminalStateless.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import React from 'react'; 3 | import { configure, shallow } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import {EmulatorState} from 'javascript-terminal'; 6 | import ReactTerminalStateless from 'ReactTerminalStateless'; 7 | 8 | describe('ReactTerminalStateless', () => { 9 | const props = { 10 | inputStr: '', 11 | emulatorState: EmulatorState.createEmpty(), 12 | onInputChange: () => {}, 13 | onStateChange: () => {} 14 | }; 15 | 16 | configure({ 17 | adapter: new Adapter() // React adapter for enzyme 18 | }); 19 | 20 | it('should render', () => { 21 | const container = shallow(); 22 | 23 | chai.assert.isDefined(container); 24 | }); 25 | 26 | it('should have .terminalContainer CSS class', () => { 27 | const container = shallow(); 28 | 29 | chai.expect(container.find('.terminalContainer').length).to.equal(1); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/library.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | 3 | import * as Terminal from '../lib/react-terminal-component.js'; 4 | 5 | describe('Given the Terminal library', () => { 6 | it('should define all API functions', () => { 7 | chai.assert.isDefined(Terminal.ReactTerminal); 8 | chai.assert.isDefined(Terminal.ReactThemes); 9 | chai.assert.isDefined(Terminal.ReactOutputRenderers); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* global __dirname, require, module*/ 2 | 3 | const webpack = require('webpack'); 4 | const path = require('path'); 5 | const { argv: args } = require('yargs'); 6 | 7 | const isProd = args.mode === 'production'; 8 | 9 | let plugins = [ 10 | new webpack.NamedModulesPlugin() 11 | ]; 12 | 13 | const libraryName = 'react-terminal-component'; 14 | 15 | const config = { 16 | entry: { 17 | [libraryName]: [path.join(__dirname, 'src/index.js')] 18 | }, 19 | devtool: 'source-map', 20 | output: { 21 | path: path.join(__dirname, 'lib'), 22 | filename: isProd ? '[name].min.js' : '[name].js', 23 | library: libraryName, 24 | libraryTarget: 'umd', 25 | umdNamedDefine: true, 26 | // Required to create single build for Node and web targets 27 | // FIXME: https://github.com/webpack/webpack/issues/6522 28 | globalObject: 'this' 29 | }, 30 | module: { 31 | rules: [{ 32 | test: /\.js$/, 33 | exclude: /node_modules/, 34 | use: { 35 | loader: 'babel-loader' 36 | } 37 | }] 38 | }, 39 | resolve: { 40 | modules: [path.resolve('./node_modules'), path.resolve('./src')], 41 | extensions: ['.json', '.js'] 42 | }, 43 | plugins: plugins, 44 | externals: { 45 | react: 'react', 46 | 'react-dom': 'react-dom', 47 | 'styled-components': 'styled-components' 48 | } 49 | }; 50 | 51 | module.exports = config; 52 | --------------------------------------------------------------------------------