├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------