/**
* # Terminal Riot Tag
*
* Provides a pretend commandline interface, capable of displaying output from a
* "shell" javascript class. Multiple terminal tags can be used on a page, using
* the same shell class or different ones - each terminal will work
* independently.
*
* ## Defaults
*
* The default prompt is `'$'`. There is no default the welcome message. The
* default shell performs no actions: pressing enter will simply move the cursor
* to the next prompt line.
*
* ## Usage
*
*
*
*
*
*
*
*
*
* ## Dependencies
*
* - riot.js (http://riotjs.com/)
* - he.js (https://github.com/mathiasbynens/he) for HTML Entity conversion.
*
* ## Making A Shell Class
*
* The shell class must be defined before the tag is mounted. Shells can keep
* prompt and welcome message settings, and must define a `run()` function.
* Here is the structure of a minimal shell that does nothing:
*
* // Contents of myshell.js
* function() myshell() {
* this.prompt = ''
* this.welcome = ''
* this.run = function(input, terminal) {
* return ''
* }
* }
*
* // Shell class used by Terminal Riot Tag in index.html
*
*
* The `run()` function can take 2 parameters. The first will contain user input
* from the command line. The second will contain the terminal object itself -
* this will allow the shell access to all of the terminal's functions, mainly
* through its 2 child tags, `terminal.display` and
* `terminal.commandline`. Refer those tags below for capabilities.
*
* A shell can, at its most basic operation, return a string of text or HTML,
* and this will be appended to the display.
*
* ## Apps
*
* A shell can also return an object that will temporarily process all input,
* until the object returns control back to the shell. This can be thought of as
* the shell returning an "app" object.
*
* An "app" object should implement a `run()` function, just like the shell
* class. It should also have exit conditions under which it will return control
* to the shell by calling `terminal.returnToShell()`.
*
* {
* 'run': function(input, terminal) {
* terminal.commandline.hidePrompt()
* terminal.display.add('Hello! inputs will now come to this app.')
* if (input == 'exit') {
* terminal.display.add('Bye! Returning control to the shell.')
* terminal.commandline.showPrompt()
* terminal.returnToShell()
* }
* }
* }
*
* An "app" is an object, so can keep state information for a current run with
* `this`. For more persistent saving of information, it is also possible to
* save data to `terminal` or to `terminal.shell`.
*/
// Create a new shell with the class name given to the terminal tag.
var shell = { 'run': function() { return ''; } }
this.shell = window[opts.shell] ? new window[opts.shell] : shell
this.active = this.shell
this.welcome = this.shell.welcome || opts.welcome
this.prompt = this.shell.prompt || opts.prompt
this.display = this.tags.display
this.commandline = this.tags.commandline
/**
* How to process a command:
*
* - Make the input safe by transforming html entities.
* - Keep the last command line on display before other output.
* - Based on shell response, append output, or give control to an app object.
* - Update the display.
*/
var self = this
this.commandline.on('cmdEntered', function(prompt, input) {
input = he.encode(input)
self.display.add(prompt + input + '\n')
var response = self.active.run(input, self)
if (typeof response === 'string') {
self.display.add(response)
}
if (typeof response === 'object' && typeof response.run === 'function') {
self.active = response
response.run(input, self)
}
self.update()
})
/**
* Make the shell the active process.
*
* For app objects to return control to the shell when finished.
*/
returnToShell() {
this.active = this.shell
}
add(text) {
if (text) {
text = this.preserveWhiteSpace(text)
this.output.push({ 'content': text })
}
}
set(text) {
if (text) {
this.clear()
this.add(text)
}
}
clear() {
this.output.length = 0
// This is needed for subsequent changes to `output` to display properly.
this.update()
}
hide() {
this.saved = this.output.splice(0, this.output.length)
this.clear()
}
restore() {
if (this.saved.length > 0) {
this.clear()
this.output = this.saved.splice(0, this.saved.length)
}
}
preserveWhiteSpace(text) {
text = text.replace(/(?:\r\n|\r|\n)/g, '
')
// Search for tags or whitespace. Escape whitespace, leave tags.
text = text.replace(/<[^<]+>|( )/g, function(match, group1) {
if (group1 == " ") { return ' ' }
return match
})
return text
}
this.output = []
this.add(opts.welcome)
this.prompt = opts.prompt || '$ '
this.visible = true
this.on('mount', function() {
document.getElementsByName('command')[0].focus()
})
process() {
var prompt = this.visible ? this.prompt : '';
this.trigger('cmdEntered', prompt, this.command.value)
this.command.value = ''
}
setPrompt(value) {
this.prompt = value
// `write()` is called to actually update the `raw` tag's html.
this.tags.lhs.write(value)
}
hidePrompt() {
this.visible = false
}
showPrompt() {
this.visible = true
}
// Initialise HTML from tag options and expose `write()` for updating.
write(text) {
this.root.innerHTML = text
}
this.write(opts.content)