An open-source JavaScript terminal emulator library, that works in your browser and Node.js.
ls
, cd
, head
, cat
, echo
, rm
)Install with npm
or with yarn
.
npm install javascript-terminal --save
yarn add javascript-terminal
Create a new instance of the terminal emulator:
const emulator = new Terminal.Emulator();
const emulatorState = Terminal.EmulatorState.createEmpty();
In all examples below it is assumed that emulator
and emulatorState
have been created.
Once you've created the emulator
and emulatorState
, you can run commands! emulator.execute
is used to run a command string from the user, and it returns the new emulator state.
const commandStr = 'ls';
const plugins = [];
const newEmulatorState = emulator.execute(emulatorState, commandStr, plugins)
In the example above, newEmulatorState
now contains the updated emulator state after the side-effects of running the ls
command string.
You can then see the updated outputs using:
newEmulatorState.getOutputs()
Putting everything together you now have enough knowledge to build a simple terminal emulator in Node.js! Check out the demo code, or keep reading for more advanced features.
The terminal can autocomplete user input using a partial command, filename or folder name.
Call emulator.autocomplete
with the emulatorState
and a partially completed string to get autocompleted text. If no autocompletion can be made, the original string is returned.
const partialString = 'ech';
emulator.autocomplete(emulatorState, partialString); // autocompletes to 'echo'
The history of the terminal can be navigated using a keyboard.
Create History Keyboard Plugin To enable history iteration, create the history keyboard plugin.
const historyKeyboardPlugin = new Terminal.HistoryKeyboardPlugin(emulatorState);
Updating History Keyboard Plugin
When running a command, provide the history keyboard plugin in the call to emulator.execute
:
const commandStr = 'ls'; // commandStr contains the user input
emulator.execute(emulatorState, commandStr, [historyKeyboardPlugin]);
This ensures that the history keyboard plugin has the latest state required for history iteration.
Iteration
When the user presses the up key call completeUp()
.
historyKeyboardPlugin.completeUp() // returns string from history stack
When the user presses the down key call completeDown()
.
historyKeyboardPlugin.completeDown() // returns string from history stack
The emulator state encapsulates the stateful elements of the emulator: the file system, command mapping, history, outputs and environment variables.
The default emulator state is created with createEmpty()
.
const emulatorState = Terminal.EmulatorState.createEmpty();
Elements of the emulator state can be accessed and updated during runtime using the getters and setters:
getFileSystem()
and setFileSystem(newFileSystem)
getEnvVariables()
and setEnvVariables(newEnvVariables)
getHistory()
and setHistory(newHistory)
getOutputs()
and setOutputs(newOutputs)
getCommandMapping()
and setCommandMapping(newCommandMapping)
Using a setter returns a new instance of the emulator state.
For example:
const defaultState = Terminal.EmulatorState.createEmpty();
const defaultOutputs = defaultState.getOutputs();
const newOutputs = Terminal.Outputs.addRecord(
defaultOutputs, Terminal.OutputFactory.makeTextOutput('added output')
);
const emulatorState = defaultState.setOutputs(newOutputs);
Notice how setting the new outputs with setOutputs
returns new emulatorState
by updating the old defaultState
.
To create emulator state with customised elements (e.g. a custom file system), use a JavaScript object with create()
which conforms to the following object schema:
const emulatorState = Terminal.EmulatorState.create({
'fs': customFileSystem,
'environmentVariables': customEnvVariables,
'history': customHistory,
'outputs': customOutputs,
'commandMapping': customCommandMapping
})
The JavaScript object only needs to have the keys of the element you wish to adjust. Default elements will be used as a fallback. For example, if you only need a custom file system your code would look like this:
const emulatorState = Terminal.EmulatorState.create({
'fs': customFileSystem
})
Examples of creating and modifying custom elements follow.
Terminal.FileSystem
allows the creation of an in-memory file system with create
using a JavaScript object.
Files and directories should be represented using a JavaScript object. File objects are differentiated from directories by use of a content
key.
canModify: false
in the object can be used to prevent modification of the file system object (such as deletion or renaming) of the file/directory and its children files/directories (if any).
const customFileSystem = Terminal.FileSystem.create({
'/home': { },
'/home/README': {content: 'This is a text file', canModify: false},
'/home/nested/directory/file': {content: 'End of nested directory!'}
});
// Using a custom operation
const isHomeDefined = Terminal.DirOp.hasDirectory(customFileSystem, '/home')
The created file system (in customFileSystem
) can be read or modified using a DirOp
or FileOp
(see the source code for JSDoc comments explaining each operation).
The command mapping can be used to add a new command to the terminal.
Here is an example of adding a command to print the contents of the arguments:
const customCommandMapping = Terminal.CommandMapping.create({
...Terminal.defaultCommandMapping,
'print': {
'function': (state, opts) => {
const input = opts.join(' ');
return {
output: OutputFactory.makeTextOutput(input)
};
},
'optDef': {}
}
});
For more advanced source code examples of how commands are defined view the commands
directory.
History contains a list of previously run commands. The list can be displayed to the user using the history
command.
const customHistory = Terminal.History.create(['a', 'b']);
Outputs contains a list which contains all emulator output including text output, error output and command headers. These outputs are intended to be visible to the user.
If a command has output, it will be appended to the outputs list.
const textOutput = Terminal.OutputFactory.makeTextOutput(
`This is an example of adding an output to display to the user without running any command.`
);
const customOutputs = Terminal.Outputs.create([textOutput]);
Environment variables can be accessed inside a command's code or by the user (for example, by executing the echo
command).
const defaultEnvVariables = Terminal.EnvironmentVariables.create();
const customEnvVariables = Terminal.EnvironmentVariables.setEnvironmentVariable(
defaultEnvVariables, 'CUSTOM_ENV_VARIABLE', 'this is the value'
);
This library does not prescribe a method for displaying terminal output or the user interface, so I've provided examples in Node.js, pure JavaScript/HTML/CSS and with React/JavaScript/HTML/CSS:
/demo-cli
directory for an example of usage in Node.js/demo-web
directory for an example of usage in plain HTML and JavaScriptFirst, make sure you have Node.js, Yarn and Git installed.
Now, fork and clone repo and install the dependencies.
git clone https://github.com/rohanchandra/javascript-terminal.git
cd javascript-terminal/
yarn install
yarn build
- creates a development and production build of the library in lib
yarn test
- run testsyarn test:min
- run tests with summary reportsyarn test:coverage
- shows test coverage statsyarn artifact:coverage-report
- creates HTML test coverage report in .nyc_output
yarn cli
- demo of using the emulator library in a Node.js command-line interface (this requires you have built the library with yarn build
)Copyright 2018 Rohan Chandra
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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.