Fork JQuery Terminal Emulator on GitHub

Examples

JSON-RPC with authentication

See demo in action. (If you want to copy code from examples click “toogle highlight” first)

Javascript code:

jQuery(function($) {
    $('#term').terminal("json-rpc-service-demo.php", {
        login: true,
        greetings: "You are authenticated"});
});

PHP code (in rpc_demo.php):

<?php
require('json_rpc.php');
 
class Demo {
  static $login_documentation = "return auth token";
  public function login($user, $passwd) {
    if (strcmp($user, 'demo') == 0 &&
        strcmp($passwd, 'demo') == 0) {
      // If you need to handle more than one user you can
      // create new token and save it in database
      return md5($user . ":" . $passwd);
    } else {
      throw new Exception("Wrong Password");
    }
  }
 
  static $ls_documentation = "list directory if token is" .
     " valid";
  public function ls($token, $path) {
    if (strcmp(md5("demo:demo"), $token) == 0) {
      if (preg_match("/\.\./", $path)) {
        throw new Exception("No directory traversal Dude");
      }
      $base = preg_replace("/(.*\/).*/", "$1",
                           $_SERVER["SCRIPT_FILENAME"]);
      $path = $base . ($path[0] != '/' ? "/" : "") . $path;
      $dir = opendir($path);
      while($name = readdir($dir)) {
        $fname = $path."/".$name;
        if (!is_dir($name) && !is_dir($fname)) {
          $list[] = $name;
        }
      }
      closedir($dir);
      return $list;
    } else {
      throw new Exception("Access Denied");
    }
  }
 
  static $whoami_documentation = "return user information";
  public function whoami() {
    return array(
        "user-agent" => $_SERVER["HTTP_USER_AGENT"],
        "your ip" => $_SERVER['REMOTE_ADDR'],
        "referer" => $_SERVER["HTTP_REFERER"],
        "request uri" => $_SERVER["REQUEST_URI"]);
  }
}
 
handle_json_rpc(new Demo());
?>

NOTE: If you use json_rpc.php file (which handle json-rpc) from the package you have always help function which display all methods or documentation strings if you provide them.

If you want secure login you should generate random token in login JSON-RPC function, and store it in database.
For example: md5(time()). You can also use SSL.

See demo in action. login is "demo" and password is "demo". Available command are "ls", "whoami", "help" and "help [rpc-method]"

Hint: if you want full access to the shell you can pass all commands (through AJAX/JSON-RPC) to php passthru function or create CGI script that will call the shell (Some hosting services block access to the shell from php but not from cgi script). You can also implement "cd" bash functionality by storing current path in variable and pass that variable with every command send to the server, you can implement dynamic prompt using the same variable.

Simple AJAX example

If you for some reason don't want to use JSON-RPC you can create interpreter that will echo ajax responses and simple php script.

$(function() {
    $('body').terminal(function(command, term) {
        term.pause();
        $.post('script.php', {command: command}).then(function(response) {
            term.echo(response).resume();
        });
    }, {
        greetings: 'Simple php example',
        onBlur: function() {
            return false;
        }
    });
});

From version 1.0.0 you can simplify that code using:

$(function() {
    $('body').terminal(function(command, term) {
        return $.post('script.php', {command: command});
    }, {
        greetings: 'Simple php example',
        onBlur: function() {
            return false;
        }
    });
});

NOTE: if you return a promise from interpreter it will call pause, wait for the response, then echo the response when it arrive and call resume.

<?php

if (isset($_POST['command'])) {
    echo "you typed '" . $_POST['command'] . "'.";
}

You can use different server side language instead of php.

Autocomplete

Adding autocomplete to terminal is simple use complete option with array or function as in api documentation or true value if you use JSON-RPC with system.describe or object as interpreter.

You can also create custom completion, for instance add, menu with items that you can click on that's added on keypress, From version 0.12.0 of the terminal there are two new api methods complete and before_cursor that simplify the code.

var ul;
var cmd;
var empty = {
    options: [],
    args: []
};
var commands = {
    'get-command': {
        options: ['name', 'age', 'description', 'address'],
        args: ['clear']
    },
    'git': {
        args: ['commit', 'push', 'pull'],
        options: ['amend', 'hard', 'version', 'help']
    },
    'get-name': empty,
    'get-age': empty,
    'get-money': empty
};
var ul;
var term = $('body').terminal($.noop, {
    onInit: function(term) {
        var wrapper = term.cmd().find('.cursor').wrap('<span/>').parent()
            .addClass('cmd-wrapper');
        ul = $('<ul></ul>').appendTo(wrapper);
        ul.on('click', 'li', function() {
            term.insert($(this).text());
            ul.empty();
        });
    },
    keydown: function(e) {
        var term = this;
        // setTimeout because terminal is adding characters in keypress
        // we use keydown because we need to prevent default action for
        // tab and still execute custom code
        setTimeout(function() {
            ul.empty();
            var command = term.get_command();
            var name = command.match(/^([^\s]*)/)[0];
            if (name) {
                var word = term.before_cursor(true);
                var regex = new RegExp('^' + $.terminal.escape_regex(word));
                var list;
                if (name == word) {
                    list = Object.keys(commands);
                } else if (command.match(/\s/)) {
                    if (commands[name]) {
                        if (word.match(/^--/)) {
                            list = commands[name].options.map(function(option) {
                                return '--' + option;
                            });
                        } else {
                            list = commands[name].args;
                        }
                    }
                }
                if (word.length >= 2 && list) {
                    var matched = [];
                    for (var i=list.length; i--;) {
                        if (regex.test(list[i])) {
                            matched.push(list[i]);
                        }
                    }
                    var insert = false;
                    if (e.which == 9) {
                        insert = term.complete(matched);
                    }
                    if (matched.length && !insert) {
                        ul.hide();
                        for (var i=0; i<matched.length; ++i) {
                            var str = matched[i].replace(regex, '');
                            $('<li>' + str + '</li>').appendTo(ul);
                        }
                        ul.show();
                    }
                }
            }
        }, 0);
        if (e.which == 9) {
            return false;
        }
    },
    onBlur: function() {
        return false;
    }
});

See demo in action.

CSRF

Example that add CSRF Protection to the terminal:

jQuery(function($) {
    var CSRF_HEADER = "X-CSRF-TOKEN";
    var csrfToken;
    $('<div/>').appendTo('body').terminal("test.php", {
        request: function(jxhr, request) {
            if (csrfToken) {
                jxhr.setRequestHeader(CSRF_HEADER, csrfToken);
            }
        },
        response: function(jxhr, response) {
            if (!response.error) {
                csrfToken = jxhr.getResponseHeader(CSRF_HEADER);
            }
        },
        width: 600,
        height: 480,
    });
});

SQL Syntax highlighter

Here is example to how to add syntax highlighting for mysql keywords

// mysql keywords
var uppercase = [
    'ACCESSIBLE', 'ADD', 'ALL', 'ALTER', 'ANALYZE', 'AND', 'AS', 'ASC',
    'ASENSITIVE', 'BEFORE', 'BETWEEN', 'BIGINT', 'BINARY', 'BLOB',
    'BOTH', 'BY', 'CALL', 'CASCADE', 'CASE', 'CHANGE', 'CHAR',
    'CHARACTER', 'CHECK', 'COLLATE', 'COLUMN', 'CONDITION',
    'CONSTRAINT', 'CONTINUE', 'CONVERT', 'CREATE', 'CROSS',
    'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP', 'CURRENT_USER',
    'CURSOR', 'DATABASE', 'DATABASES', 'DAY_HOUR', 'DAY_MICROSECOND',
    'DAY_MINUTE', 'DAY_SECOND', 'DEC', 'DECIMAL', 'DECLARE', 'DEFAULT',
    'DELAYED', 'DELETE', 'DESC', 'DESCRIBE', 'DETERMINISTIC',
    'DISTINCT', 'DISTINCTROW', 'DIV', 'DOUBLE', 'DROP', 'DUAL', 'EACH',
    'ELSE', 'ELSEIF', 'ENCLOSED', 'ESCAPED', 'EXISTS', 'EXIT',
    'EXPLAIN', 'FALSE', 'FETCH', 'FLOAT', 'FLOAT4', 'FLOAT8', 'FOR',
    'FORCE', 'FOREIGN', 'FROM', 'FULLTEXT', 'GRANT', 'GROUP', 'HAVING',
    'HIGH_PRIORITY', 'HOUR_MICROSECOND', 'HOUR_MINUTE', 'HOUR_SECOND',
    'IF', 'IGNORE', 'IN', 'INDEX', 'INFILE', 'INNER', 'INOUT',
    'INSENSITIVE', 'INSERT', 'INT', 'INT1', 'INT2', 'INT3', 'INT4',
    'INT8', 'INTEGER', 'INTERVAL', 'INTO', 'IS', 'ITERATE', 'JOIN',
    'KEY', 'KEYS', 'KILL', 'LEADING', 'LEAVE', 'LEFT', 'LIKE', 'LIMIT',
    'LINEAR', 'LINES', 'LOAD', 'LOCALTIME', 'LOCALTIMESTAMP', 'LOCK',
    'LONG', 'LONGBLOB', 'LONGTEXT', 'LOOP', 'LOW_PRIORITY',
    'MASTER_SSL_VERIFY_SERVER_CERT', 'MATCH', 'MEDIUMBLOB', 'MEDIUMINT',
    'MEDIUMTEXT', 'MIDDLEINT', 'MINUTE_MICROSECOND', 'MINUTE_SECOND',
    'MOD', 'MODIFIES', 'NATURAL', 'NOT', 'NO_WRITE_TO_BINLOG', 'NULL',
    'NUMERIC', 'ON', 'OPTIMIZE', 'OPTION', 'OPTIONALLY', 'OR', 'ORDER',
    'OUT', 'OUTER', 'OUTFILE', 'PRECISION', 'PRIMARY', 'PROCEDURE',
    'PURGE', 'RANGE', 'READ', 'READS', 'READ_WRITE', 'REAL',
    'REFERENCES', 'REGEXP', 'RELEASE', 'RENAME', 'REPEAT', 'REPLACE',
    'REQUIRE', 'RESTRICT', 'RETURN', 'REVOKE', 'RIGHT', 'RLIKE',
    'SCHEMA', 'SCHEMAS', 'SECOND_MICROSECOND', 'SELECT', 'SENSITIVE',
    'SEPARATOR', 'SET', 'SHOW', 'SMALLINT', 'SPATIAL', 'SPECIFIC',
    'SQL', 'SQLEXCEPTION', 'SQLSTATE', 'SQLWARNING', 'SQL_BIG_RESULT',
    'SQL_CALC_FOUND_ROWS', 'SQL_SMALL_RESULT', 'SSL', 'STARTING',
    'STRAIGHT_JOIN', 'TABLE', 'TERMINATED', 'THEN', 'TINYBLOB',
    'TINYINT', 'TINYTEXT', 'TO', 'TRAILING', 'TRIGGER', 'TRUE', 'UNDO',
    'UNION', 'UNIQUE', 'UNLOCK', 'UNSIGNED', 'UPDATE', 'USAGE', 'USE',
    'USING', 'UTC_DATE', 'UTC_TIME', 'UTC_TIMESTAMP', 'VALUES',
    'VARBINARY', 'VARCHAR', 'VARCHARACTER', 'VARYING', 'WHEN', 'WHERE',
    'WHILE', 'WITH', 'WRITE', 'XOR', 'YEAR_MONTH', 'ZEROFILL'];
var keywords = uppercase.concat(uppercase.map(function(keyword) {
    return keyword.toLowerCase();
}));
$.terminal.defaults.formatters.push(function(string) {
    return string.split(/((?:\s|&nbsp;)+)/).map(function(string) {
        if (keywords.indexOf(string) != -1) {
            return '[[b;white;]' + string + ']';
        } else {
            return string;
        }
    }).join('');
});

If you want to add formatting for different sql command and not for main interpterer you can use stack of formatters. It require version >=1.0 that introduce extra option for interpreter. The example will work for any number of nested interpreters even you call push new in your mysql command.

// this regex will allow mixed case like SeLect
var re = new RegExp('^(' + uppercase.join('|') + ')$', 'i');
function mysql_formatter(string) {
    return string.split(/((?:\s|&nbsp;)+)/).map(function(string) {
        if (re.test(string)) {
            return '[[b;white;]' + string + ']';
        } else {
            return string;
        }
    }).join('');
}
var formatters = [$.terminal.defaults.formatters];
$('body').terminal(function(command, term) {
    if (command.match(/^\s*mysql\s*$/)) {
        term.push(function(query) {
            term.echo('executing ' + query, {formatters: false});
        }, {
            prompt: 'mysql> ',
            name: 'mysql',
            // extra property saved in interpreter
            formatters: [mysql_formatter],
            completion: keywords
        });
    }
}, {
    onPush: function(before, after) {
        $.terminal.defaults.formatters = after.formatters || [];
        formatters.push($.terminal.defaults.formatters);
    },
    onPop: function(before, after) {
        formatters.pop();
        if (formatters.length > 0) {
            $.terminal.defaults.formatters = formatters[formatters.length-1];
        }
    }
});

Quake like terminal

See demo.

Below is code for small plugin called tilda.

(function($) {
    $.fn.tilda = function(eval, options) {
        if ($('body').data('tilda')) {
            return $('body').data('tilda').terminal;
        }
        this.addClass('tilda');
        options = options || {};
        eval = eval || function(command, term) {
            term.echo("you don't set eval for tilda");
        };
        var settings = {
            prompt: 'tilda> ',
            name: 'tilda',
            height: 100,
            enabled: false,
            greetings: 'Quake like console',
            keypress: function(e) {
                if (e.which == 96) {
                    return false;
                }
            }
        };
        if (options) {
            $.extend(settings, options);
        }
        this.append('<div class="td"></div>');
        var self = this;
        self.terminal = this.find('.td').terminal(eval,
                                               settings);
        var focus = false;
        $(document.documentElement).keypress(function(e) {
            if (e.charCode == 96) {
                self.slideToggle('fast');
                self.terminal.command_line.set('');
                self.terminal.focus(focus = !focus);
            }
        });
        $('body').data('tilda', this);
        this.hide();
        return self;
    };
})(jQuery);

See demo.

Terminal in jQuery UI Dialog

Bellow is small plugin dterm.

(function($) {
    $.extend_if_has = function(desc, source, array) {
        for (var i=array.length;i--;) {
            if (typeof source[array[i]] != 'undefined') {
                desc[array[i]] = source[array[i]];
            }
        }
        return desc;
    };
    $.fn.dterm = function(interpeter, options) {
        var defaults = Object.keys($.terminal.defaults);
        var op = $.extend_if_has({}, options, defaults);
 
        var term = this.append('<div/>').
              terminal(interpeter, op);
        if (!options.title) {
            options.title = 'JQuery Terminal Emulator';
        }
        if (options.logoutOnClose) {
            options.close = function(e, ui) {
                term.logout();
                term.clear();
            };
        } else {
            options.close = function(e, ui) {
                term.focus(false);
            };
        }
        var self = this;
        if (window.IntersectionObserver) {
            var visibility_observer = new IntersectionObserver(function() {
                if (self.is(':visible')) {
                    terminal.enable().resize();
                } else {
                    self.disable();
                }
            }, {
                root: document.body
            });
            visibility_observer.observe(terminal[0]);
        }
        this.dialog($.extend({}, options, {
            resizeStop: function() {
                var content = self.find('.ui-dialog-content');
                terminal.resize(content.width(), content.height());
            },
            open: function(event, ui) {
                if (!window.IntersectionObserver) {
                    setTimeout(function() {
                        terminal.enable().resize();
                    }, 100);
                }
                if (typeof options.open == 'function') {
                    options.open(event, ui);
                }
            },
            show: 'fade',
            closeOnEscape: false
        }));
        self.terminal = terminal;
        return self;
    };
})(jQuery);

Demo Scheme interpreter inside JQuery UI Dialog.

Click on button to with scheme interpreter inside UI Dialog.

Hint: you can use JQuery from scheme. There is defined $ function and functions for all jquery object methods, they names start with coma and they always return jquery object so you can do chaining.

NOTE: you should include jQuery Terminal css file after jQuery UI one otherwise you will have white text in terminal, insided of gray.

Interpreter allow to use multiline expressions. When you type not finished S-Expresion it change the prompt with set_prompt, contatenate current command with previous not finished expression and when you close last parentises end press enter it evaluate whole expression.

If you want to call:

$("body").css("background-color", "black");

use

(.css ($ "body") "background-color" "black")

To attach event you can use lambda expressions.

(.click ($ ".terminal") (lambda () (display "click")))

this will attach click event to terminal.

Multiple interpreters

All interpreters are stored on the stack which which you can manipulate with terminal methods pop an push.

See demo.

In belowed code there are defied three commands:

jQuery(function($) {
  $('html').terminal(function(cmd, term) {
    if (cmd == 'help') {
      term.echo("available commands are mysql, js, test");
    } else if (cmd == 'test'){
      term.push(function(cmd, term) {
        if (command == 'help') {
          term.echo('type "ping" it will display "pong"');
        } else if (cmd == 'ping') {
          term.echo('pong');
        } else {
          term.echo('unknown command "' + cmd + '"');
        }
      }, {
        prompt: 'test> ',
        name: 'test'});
    } else if (command == "js") {
      term.push(function(command, term) {
        var result = window.eval(command);
        if (result != undefined) {
          term.echo(String(result));
        }
      }, {
        name: 'js',
        prompt: 'js> '});
      } else if (command == 'mysql') {
        term.push(function(command, term) {
          term.pause();
          //$.jrpc is helper function which
          //creates json-rpc request
          $.jrpc("mysql-rpc-demo.php",
            "query",
            [command],
            function(data) {
              term.resume();
              if (data.error) {
                if (data.error.error && data.error.error.message) {
                  term.error(data.error.error.message); // php error
                } else {
                  term.error(data.error.message); // json rpc error
                }
              } else {
                if (typeof data.result == 'boolean') {
                  term.echo(data.result ?
                            'success' :
                            'fail');
                } else {
                  var len = data.result.length;
                  for(var i=0;i<len; ++i) {
                    term.echo(data.result[i].join(' | '));
                  }
                }
              }
            },
            function(xhr, status, error) {
              term.error('[AJAX] ' + status +
                         ' - Server reponse is: \n' +
                         xhr.responseText);
                         term.resume();
                   }); // rpc call
          }, {
            greetings: "This is example of using mysql"+
              " from terminal\n you are allowed to exe"+
              "cute: select, insert, update and delete"+
              " from/to table:\n   table test(integer_"+
              "value integer, varchar_value varchar(255))",
            prompt: "mysql> "});
          } else {
            term.echo("unknow command " + command);
          }
        }, {
          greetings: "multiply terminals demo use help"+
                " to see available commands"
       });});

If you want to display ascii table like real mysql command, take a look at asci_table function in leash project, it use wcwidth to calcuate the width of the characters but if you don't care about chenese characters you can replace it with string.length.

PHP code for mysql service:

<?php
require('json_rpc.php');
 
$conn = mysql_connect('localhost', 'user', 'password');
mysql_select_db('database');
 
class MysqlDemo {
  public function query($query) {
    if (preg_match("/create|drop/", $query)) {
      throw new Exception("Sorry you are not allowed to ".
                          "execute '" . $query . "'");
    }
    if (!preg_match("/(select.*from *test|insert *into *".
                    "test.*|delete *from *test|update *t".
                    "est)/", $query)) {
      throw new Exception("Sorry you can't execute '" .
                          $query . "' you are only allow".
                          "ed to select, insert, delete ".
                          "or update 'test' table");
    }
    if ($res = mysql_query($query)) {
      if ($res === true) {
        return true;
      }
      if (mysql_num_rows($res) > 0) {
        while ($row = mysql_fetch_row($res)) {
          $result[] = $row;
        }
        return $result;
      } else {
        return array();
      }
    } else {
      throw new Exception("MySQL Error: ".mysql_error());
    }
  }
}
 
handle_json_rpc(new MysqlDemo());
?>

See demo.

Star Wars Animation

This is Star Wars ASCIIMation created by Simon Jansen
http://www.asciimation.co.nz/

$(function() {
    var frames = [];
    var LINES_PER_FRAME = 14;
    var DELAY = 67;
    //star_wars is array of lines from 'js/star_wars.js'
    var lines = star_wars.length;
    for (var i=0; i<lines; i+=LINES_PER_FRAME) {
        frames.push(star_wars.slice(i, i+LINES_PER_FRAME));
    }
    var stop = false;
    //to show greetings after clearing the terminal
    function greetings(term) {
        term.echo('STAR WARS ASCIIMACTION\n'+
                  'Simon Jansen (C) 1997 - 2008\n'+
                  'www.asciimation.co.nz\n\n'+
                  'type "play" to start animation, '+
                  'press CTRL+D to stop');
    }
    function play(term, delay) {
        var i = 0;
        var next_delay;
        if (delay == undefined) {
            delay = DELAY;
        }
        function display() {
            if (i == frames.length) {
                i = 0;
            }
            term.clear();
            if (frames[i][0].match(/[0-9]+/)) {
                next_delay = frames[i][0] * delay;
            } else {
                next_delay = delay;
            }
            term.echo(frames[i++].slice(1).join('\n')+'\n');
            if (!stop) {
                setTimeout(display, next_delay);
            } else {
                term.clear();
                greetings(term);
                i = 0;
            }
        }
        display();
    }

    $('#starwarsterm').terminal(function(command, term){
        if (command == 'play') {
            term.pause();
            stop = false;
            play(term);
        }
    }, {
        width: 500,
        height: 230,
        prompt: 'starwars> ',
        greetings: null,
        onInit: function(term) {
            greetings(term);
        },
        keypress: function(e, term) {
            if (e.which == 100 && e.ctrlKey) {
                stop = true;
                term.resume();
                return false;
            }
        }
    });
});

Ask before executing a command

Someone ask me how to create, command that ask users before executing, and here is the code, it will keep asking until eather yes or no will be entered (or short y/n).

$('#term').terminal(function(command, term) {
    if (command == 'foo') {
        var history = term.history();
        history.disable();
        term.push(function(command) {
            if (command.match(/^(y|yes)$/i)) {
                term.echo('execute your command here');
                term.pop();
                history.enable();
            } else if (command.match(/^(n|no)$/i)) {
                term.pop();
                history.enable();
            }
        }, {
            prompt: 'Are you sure? '
        });
    }
});

Animation that emulate user typing

Someone else aks if it's posible to create animation like user typing. Here is the code that emulate user typing on initialization of the terminal and before every ajax call, which can finish after animation.

$(function() {
    var anim = false;
    function typed(finish_typing) {
        return function(term, message, delay, finish) {
            anim = true;
            var prompt = term.get_prompt();
            var c = 0;
            if (message.length > 0) {
                term.set_prompt('');
                var new_prompt = '';
                var interval = setInterval(function() {
                    var chr = $.terminal.substring(message, c, c+1);
                    new_prompt += chr;
                    term.set_prompt(new_prompt);
                    c++;
                    if (c == length(message)) {
                        clearInterval(interval);
                        // execute in next interval
                        setTimeout(function() {
                            // swap command with prompt
                            finish_typing(term, message, prompt);
                            anim = false
                            finish && finish();
                        }, delay);
                    }
                }, delay);
            }
        };
    }
    function length(string) {
        string = $.terminal.strip(string);
        return $('<span>' + string + '</span>').text().length;
    }
    var typed_prompt = typed(function(term, message, prompt) {
        term.set_prompt(message + ' ');
    });
    var typed_message = typed(function(term, message, prompt) {
        term.echo(message)
        term.set_prompt(prompt);
    });

    $('body').terminal(function(cmd, term) {
        var finish = false;
        var msg = "Wait I'm executing ajax call";
        term.set_prompt('> ');
        typed_message(term, msg, 200, function() {
            finish = true;
        });
        var args = {command: cmd};
        $.get('commands.php', args, function(result) {
            (function wait() {
                if (finish) {
                    term.echo(result);
                } else {
                    setTimeout(wait, 500);
                }
            })();
        });
    }, {
        name: 'xxx',
        greetings: null,
        width: 500,
        height: 300,
        onInit: function(term) {
            // first question
            var msg = "Wellcome to my terminal";
            typed_message(term, msg, 200, function() {
                typed_prompt(term, "what's your name:", 100);
            });
        },
        keydown: function(e) {
            //disable keyboard when animating
            if (anim) {
                return false;
            }
        }
    });
});

Progress bar animation

You can test it by executing command `progress 30`.

Here is the code for progres bar animation:

jQuery(function($) {
    function progress(percent, width) {
        var size = Math.round(width*percent/100);
        var left = '', taken = '', i;
        for (i=size; i--;) {
            taken += '=';
        }
        if (taken.length > 0) {
            taken = taken.replace(/=$/, '>');
        }
        for (i=width-size; i--;) {
            left += ' ';
        }
        return '[' + taken + left + '] ' + percent + '%';
    }
    var animation = false;
    var timer;
    var prompt;
    var string;
    $('body').terminal(function(command, term) {
        var cmd = $.terminal.parse_command(command);
        if (cmd.name == 'progress') {
            var i = 0, size = cmd.args[0];
            prompt = term.get_prompt();
            string = progress(0, size);
            term.set_prompt(progress);
            animation = true;
            (function loop() {
                string = progress(i++, size);
                term.set_prompt(string);
                if (i < 100) {
                    timer = setTimeout(loop, 100);
                } else {
                    term.echo(progress(i, size) + ' [[b;green;]OK]')
                        .set_prompt(prompt);
                    animation = false
                }
            })();
        }
    }, {
        keydown: function(e, term) {
            if (animation) {
                if (e.which == 68 && e.ctrlKey) { // CTRL+D
                    clearTimeout(timer);
                    animation = false;
                    term.echo(string + ' [[b;red;]FAIL]')
                        .set_prompt(prompt);
                }
                return false;
            }
        }
    });
});

Spinners animation

Spinner animations from sindresorhus/cli-spinners.

$(function() {
    var url = 'https://rawgit.com/sindresorhus/cli-spinners/master/' +
              'spinners.json';
    $.getJSON(url, function(spinners) {
        var animation = false;
        var timer;
        var prompt;
        var spinner;
        var i;
        function start(term, spinner) {
            animation = true;
            i = 0;
            function set() {
                var text = spinner.frames[i++ % spinner.frames.length];
                term.set_prompt(text);
            };
            prompt = term.get_prompt();
            term.find('.cursor').hide();
            set();
            timer = setInterval(set, spinner.interval);
        }
        function stop(term, spinner) {
            setTimeout(function() {
                clearInterval(timer);
                var frame = spinner.frames[i % spinner.frames.length];
                term.set_prompt(prompt).echo(frame);
                animation = false;
                term.find('.cursor').show();
            }, 0);
        }
        $('#spinners .term').terminal({
            spinner: function(name) {
                spinner = spinners[name];
                if (!spinner) {
                    this.error('Spinner not found');
                } else {
                    this.echo('press CTRL+D to stop');
                    start(this, spinner);
                }
            },
            help: function() {
                var text = Object.keys(spinners).join('\t');
                this.echo('Available spinners: ' + text, {
                    keepWords: true
                });
            }
        }, {
            greetings: false,
            onInit: function(term) {
                term.echo('Spinners, type [[b;#fff;]help] to display '+
                          'available spinners or [[b;#fff;]spinner <n'+
                          'ame>] for animation', {
                    keepWords: true
                });
            },
            completion: true,
            keydown: function(e, term) {
                if (animation) {
                    if (e.which == 68 && e.ctrlKey) { // CTRL+D
                        stop(term, spinner);
                    }
                    return false;
                }
            }
        });
    });
});

Less bash command

Here is implementation of bash less command (not all commands implemented)

var resize = [];
$('<SELECTOR>').terminal(function(command, term) {
  if (command.match(/ *less +[^ ]+/)) {
    term.pause();
    $.ajax({
      // leading and trailing spaces and keep those inside argument
      url: command.replace(/^\s+|\s+$/g, '').
        replace(/^ */, '').split(/(\s+)/).slice(2).join(''),
      method: 'GET',
      dataType: 'text',
      success: function(source) {
        term.resume();
        var export_data = term.export_view();
        var less = true;
        source = source.replace(/&/g, '&amp;').
          replace(/\[/g, '&#91;').
          replace(/\]/g, '&#93;');
        var cols = term.cols();
        var rows = term.rows();
        resize = [];
        var lines = source.split('\n');
        resize.push(function() {
          if (less) {
            cols = term.cols();
            rows = term.rows();
            print();
          }
        });
        var pos = 0;
        function print() {
          term.clear();
          term.echo(lines.slice(pos, pos+rows-1).join('\n'));
        }
        print();
        term.push($.noop, {
          keydown: function(e) {
            if (term.get_prompt() !== '/') {
              if (e.which == 191) {
                term.set_prompt('/');
              } else if (e.which === 38) { //up
                if (pos > 0) {
                  --pos;
                  print();
                }
              } else if (e.which === 40) { //down
                if (pos < lines.length-1) {
                  ++pos;
                  print();
                }
              } else if (e.which === 34) { // Page up
                pos += rows;
                if (pos > lines.length-1-rows) {
                  pos = lines.length-1-rows;
                }
                print();
              } else if (e.which === 33) { // page down
                pos -= rows;
                if (pos < 0) {
                  pos = 0;
                }
                print();
              } else if (e.which == 81) { //Q
                less = false;
                term.pop().import_view(export_data);
              }
              return false;
            } else {
              if (e.which === 8 && term.get_command() === '') {
                term.set_prompt(':');
              } else if (e.which == 13) {
                var command = term.get_command();
                // basic search find only first
                // instance and don't mark the result
                if (command.length > 0) {
                  var regex = new RegExp(command);
                  for (var i=0; i<lines.length; ++i) {
                    if (regex.test(lines[i])) {
                      pos = i;
                      print();
                      term.set_command('');
                      break;
                    }
                  }
                  term.set_command('');
                  term.set_prompt(':');
                }
                return false;
              }
            }
          },
          prompt: ':'
        });
      }
    });
  }
}, {
  onResize: function(term) {
    for (var i=resize.length;i--;) {
      resize[i](term);
    }
  }
});

Improved less command with syntax highlighting can be found in this pen.

Bash history commands

If you want to add bash history commands like !!, !$ or !* here is the code:

$('body').terminal(function(command, term) {
    var cmd = $.terminal.parse_command(command);
    if (command.match(/![*$]|\s*!!(:p)?\s*$|\s*!(.*)/)) {
        var new_command;
        var history = term.history();
        var last = $.terminal.parse_command(history.last());
        var match = command.match(/\s*!(?![!$*])(.*)/);
        if (match) {
            var re = new RegExp($.terminal.escape_regex(match[1]));
            var history_data = history.data();
            for (var i=history_data.length; i--;) {
                if (re.test(history_data[i])) {
                    new_command = history_data[i];
                    break;
                }
            }
            if (!new_command) {
                var msg = $.terminal.defaults.strings.commandNotFound;
                term.error(sprintf(msg, $.terminal.escape_brackets(match[1])));
            }
        } else if (command.match(/![*$]/)) {
            if (last.args.length) {
                var last_arg = last.args[last.args.length-1];
                new_command = command.replace(/!\$/g, last_arg);
            }
            new_command = new_command.replace(/!\*/g, last.rest);
        } else if (command.match(/\s*!!(:p)?/)) {
            new_command = last.command;
        }
        if (new_command) {
            term.echo(new_command);
        }
        if (!command.match(/![*$!]:p/)) {
            if (new_command) {
                term.exec(new_command, true);
            }
        }
    } else if (cmd.name == 'echo') {
        term.echo(cmd.rest);
    }
}, {
    // we need to disable history for bash history commands
    historyFilter: function(command) {
        return !command.match(/![*$]|\s*!!(:p)?\s*$|\s*!(.*)/);
    }
});

Smooth CSS3 cursor animation

From version 0.8 terminal use CSS animation for blinking so you can change it without touching JavaScript code.

Here is different looking cursor blinking animation that can be use with terminal.

@keyframes blink {
    50% {
        color: #000;
        background: #0c0;
        -webkit-box-shadow: 0 0 5px rgba(0,100,0,50);
        box-shadow: 0 0 5px rgba(0,100,0,50);
    }
}
@-webkit-keyframes blink {
    50% {
        color: #000;
        background: #0c0;
        -webkit-box-shadow: 0 0 5px rgba(0,100,0,50);
        box-shadow: 0 0 5px rgba(0,100,0,50);
    }
}
@-ms-keyframes blink {
    50% {
        color: #000;
        background: #0c0;
        -webkit-box-shadow: 0 0 5px rgba(0,100,0,50);
        box-shadow: 0 0 5px rgba(0,100,0,50);
    }
}
@-moz-keyframes blink {
    50% {
        color: #000;
        background: #0c0;
        -webkit-box-shadow: 0 0 5px rgba(0,100,0,50);
        box-shadow: 0 0 5px rgba(0,100,0,50);
    }
}
.terminal {
    --background: #000;
    --color: #0c0;
    text-shadow: 0 0 3px rgba(0,100,0,50);
}
.cmd .cursor.blink {
    -webkit-animation: 1s blink infinite;
    animation: 1s blink infinite;
    -webkit-box-shadow: 0 0 0 rgba(0,100,0,50);
    box-shadow: 0 0 0 rgba(0,100,0,50);
    border: none;
    margin: 0;
}

Using Virtual Keyboard with Terminal

There are problems with terminal on touch devices. I've found a project Keyboard that create virtual keyboard using jQuery UI. I've created a demo of working terminal with keyboard. The code still need tweeks to work full screen.

See demo

Since keyboard is not working on mobile, this demo is left for historical reason and because it's just one example of the use of terminal.

Using History API for commands

As a response for this issue on github I came up with a way to keep every command response in history using HTML5 History API, so you can click back and forward buttons and it will show you previous and next commands.

$(function() {
    var save_state = [];
    var terminal = $('#term').terminal(function(command, term) {
        var cmd = $.terminal.split_command(command);
        var url;
        if (cmd.name == 'open') {
            term.pause();
            // open html and display it on terminal as it is
            url = cmd.args[0];
            $.get(url, function(result) {
                term.echo(result, {raw:true}).resume();
                save_state.push(term.export_view());
                history.pushState(save_state.length-1, null, url);
            }, 'text');
        } else {
            // store all other commands
            save_state.push(term.export_view());
            url = '/' + cmd.name + '/' + cmd.args.join('/');
            history.pushState(save_state.length-1, null, url);
        }
    });
    save_state.push(terminal.export_view()); // save initial state
    $(window).on('popstate', function(e) {
        if (save_state.length) {
            terminal.import_view(save_state[history.state || 0]);
        }
    });
});

Each command after it finish need to call this:

save_state.push(term.export_view());
history.pushState(save_state.length-1, null, '<NEW URL>');

So it keep current view of the terminal (after the command finishes) in save_state array and index in push state (I've try to put whole view in history.state but it didn't work). On back/forward buttons click it will get that value from array and restore the view of the terminal.

Version 0.9.0 introduced similar API but using url hash. To enable it use historyState option and to execute hash on load use execHash option.

Shell

You can also check my project LEASH - Browser Shell you will have shell without need to install anything on the server (so you don't need root access), it use lot of features of jQuery terminal, like better less command or python interpreter.

You can also use cordova application that use leash to have access to android shell.

Vintage an OS Like Terminals

Emoji

Inspired by a comment I've created a demo of emoji in terminal. The demo use devel branch because there was problem with moving cursor when formatting change length of the text like with emoji that one space for text like :smile:

The code is creating css style based on iamcal/emoji-data

var base = 'https://raw.githubusercontent.com/iamcal/emoji-data/master/img-emojione-64/';
$.get('https://rawgit.com/iamcal/emoji-data/eb2246bb9263cba4e04e1497d635925ef59bd143/emoji.json').then(function(list) {
    var style = $('<style>');
    var text = {};
    var names = [];
    list.forEach(function(emoji) {
        var rule = '.emoji.' + emoji.short_name + '{' +
            'background-image: url(' + base + emoji.image + ');' +
            '}';
        style.html(style.html() + rule + '\n');
        text[emoji.text] = emoji.short_name;
        names.push(emoji.short_name);
    });
    var re = new RegExp('(' + Object.keys(text).map(function(text) {
        return $.terminal.escape_regex(text);
    }).join('|') + ')', 'g');
    style.appendTo('head');
    $.terminal.defaults.formatters.push(
        function(string) {
            return string.replace(/:([^:]+):/g, function(_, name) {
                if (names.indexOf(name) === -1) {
                    return _;
                }
                return '[[;;;emoji ' + name + '] ]';
            }).replace(re, function(name) {
                return '[[;;;emoji ' + text[name] + '] ]';
            });
        }
    );
});
.emoji {
    width: 16px;
    height: 16px;
    background-size: cover;
    display: inline-block;
}

Terminal Widget

Inspired by adsense code, I've create small js file that you can include on your page to load jQuery Termianl, so you don't need to include jQuery or any other files, just one js file.

<!DOCTYPE html>
<html>
  <body>
    <script>
      (terminals = window.terminals || []).push([
        'body', function(command, term) {}
      ]);
    </script>
    <style>
     body {
         min-height: 100vh;
         margin: 0;
     }
    </style>
    <script src="https://cdn.rawgit.com/jcubic/jquery.
                 terminal/master/js/terminal.widget.js"></script>
  </body>
</html>
        

The spec for array is the same as arguments to terminal but with selector as first element, so second is function string or object or array, and the third are the options.

In the wild