Get Paid Support

Examples

JSON-RPC with Simple 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 or session.
For example: md5(time()). You can also use SSL to make it more secured.

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

JSON-RPC with JWT authentication

See demo in action.

The full code is available on GitHub

Javascript code:

function interceptor(req, res) {
    if (res.error && res.error.message === 'Access token expired') {
         // TODO: remove this echo
         this.echo('Token expired: refreshing ...');
         this.pause();
         return new Promise(resolve => {
             $.jrpc('service.php', 'refresh', [], data => {
                 const re = /Refresh token expired/;
                 if (data.error && data.error.message.match(re)) {
                     this.logout();
                     return resolve({...res, error: data.error});
                 }
                 const token = data.result;
                 this.set_token(token);
                 req.params[0] = token;
                 const { method, params } = req;
                 $.jrpc('service.php', method, params, message => {
                     this.resume();
                     resolve(message);
                 });
             });
         });
     }
}
let term;
$(function() {
    term = $('#term').terminal(['service.php', {
        refresh() {
            // this command will manually referesh the token
            // you don't need this command, it's only for demo purpose
            return new Promise(resolve => {
                $.jrpc('service.php', 'refresh', [], (message) => {
                    this.set_token(message.result);
                    resolve();
                });
            });
        }
    }], {
         login: true,
         completion: true,
         describe: false,
         greetings: 'Welcome to JWT demo',
         rpc: interceptor
    });
});

see PHP Code

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'
    });
});

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'
    });
});

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.

From version 2.8.0 repo now includes the file that will add menu autocomplete automatically.

<script src="https://unpkg.com/jquery.terminal/js/autocomplete_menu.js"></script>
        

The file will monkey patch the terminal and create new option for terminal. To use menu autocomplete you just need this:

$('body').terminal(function(command) {
}, {
    autocompleteMenu: true,
    completion: ['foo', 'bar', 'baz']
});

The complition options is the same, it can call callback function 2 argument, or return a promise. The value need to be array of strings.

Below is first, original code that shows example, how to create autocomplete menu.

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;
        }
    }
});

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,
    });
});

Note that this will break if you Open the app in more than one tab. To fix the issue you can use my other library sysend.js and share the token.

jQuery(function($) {
    var CSRF_HEADER = "X-CSRF-TOKEN";
    var csrfToken;
    sysend.on('csrfToken', function(token) {
        csrfToken = token;
    });
    $('<div/>').appendTo('body').terminal("test.php", {
        request: function(jxhr, request) {
            if (csrfToken) {
                jxhr.setRequestHeader(CSRF_HEADER, csrfToken);
            }
            sysend.broadcast('csrfToken', 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: "multiple 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
https://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

NOTE: in version 2.24.0 typing animation was added to the library. No animate all you have to do is:

term.typing('echo', 100, 'Hello', function() {  });
term.typing('prompt', 100, 'name: ', function() {
});
        

The function also return a promise so you can use return value instead of a callback function.

Someone else asked 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;
            }
        }
    });
});

Here you can find the same code refactored a bit with modern JavaScript.

Spinners animation

Spinner animations from sindresorhus/cli-spinners.

$(function() {
    var url = 'https://unpkg.com/cli-spinners/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)

From version 1.16.0 included in the page in less.js file and use less as jQuery plugin on terminal instance.

$('<SELECTOR>').terminal({
    less: function(url) {
        $.get(url).then(text => {
            this.less(text);
        });
    }
});

Here is old example:

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.

Pipe operator

From version 2.4.0 there is file pipe.js include in npm and unpkg that have pipe operator. ALl you need is to include the file and you can use new pipe option to enable pipe.

var count = 0;
var term = $('body').terminal({
    echo: function(string) {
        return new Promise(function(resolve) {
            term.echo(string);
            setTimeout(resolve, 1000);
        });
    },
    read: function() {
        return term.read('').then(function(string) {
            term.echo('read[' +(++count)+']: ' + string);
        });
    }
}, {
    pipe: true
});

Pipe depend on read and echo that can be used as stdin and stdout in real terminal to combine the commands

You can execute echo foo | read or even echo foo | read | read it will also work if pipe is in string like this echo "pipe |" | read (it will also work if you escape quotes or pipe)

you can test it on 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.

@-webkit-keyframes terminal-smooth {
    0%, 100% {
        left: 0;
        color: #0c0;
        color: var(--original-color, #aaa);
        background-color: #000;
        background-color: var(--background, #000);
        -webkit-box-shadow: none;
        box-shadow: none;
        border: none;
        padding: 0;
        margin: 0;
    }
    50% {
        left: 0;
        color: #000;
        background: #0c0;
        -webkit-box-shadow: 0 0 5px var(--color, #aaa);
        box-shadow: 0 0 5px var(--color, #aaa);
        border: none;
        padding: 0;
        margin: 0;
        border-bottom: 2px solid transparent;
    }
}
@-moz-keyframes terminal-smooth {
    0%, 100% {
        left: 0;
        color: #0c0;
        color: var(--original-color, #aaa);
        background-color: #000;
        background-color: var(--background, #000);
        -moz-box-shadow: none;
        box-shadow: none;
        border: none;
        padding: 0;
        margin: 0;
    }
    50% {
        left: 0;
        color: #000;
        background: #0c0;
        -mox-box-shadow: 0 0 5px var(--color, #aaa);
        box-shadow: 0 0 5px var(--color, #aaa);
        border: none;
        padding: 0;
        margin: 0;
        border-bottom: 2px solid transparent;
    }
}
@keyframes terminal-smooth {
    0%, 100% {
        left: 0;
        color: #0c0;
        color: var(--original-color, #aaa);
        background-color: #000;
        background-color: var(--background, #000);
        -webkit-box-shadow: none;
        box-shadow: none;
        border: none;
        padding: 0;
        margin: 0;
    }
    50% {
        left: 0;
        color: #000;
        background: #0c0;
        -webkit-box-shadow: 0 0 5px var(--color, #aaa);
        box-shadow: 0 0 5px var(--color, #aaa);
        border: none;
        padding: 0;
        margin: 0;
        border-bottom: 2px solid transparent;
    }
}
#css-cursor .terminal {
    --background: #000;
    --color: #0c0;
    --animation: terminal-smooth;
    text-shadow: 0 0 3px rgba(0,100,0,0.5);
}
/* below can be removed in version >= 2.1.0 */
#css-cursor .cmd .cursor-line {
    overflow: visible;
}

Using Virtual Keyboard with Terminal

There were problems with terminal on touch devices (it now works without problems). 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. Current version works with native keyboard (tested on Android and iPhone) if you find issues on the phone please add an issue.

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.

If you want something lightweight you can check jsh.php, it's single file php shell.

Vintage and OS Fake Terminals

See also this Vintage Screen Effects Collection on CodePen for inspiration.

404 Error Page

To see 404 page (page not found error) just open any non exisitng page like /404. You will have few commands like:

NOTE: if you want to share the link to 404 page with any command (e.g. on Twitter), best way is to use URL shortener like tinyurl.com. Because somtimes the links got broken, brackets in URL can be confusing.

The error page has also terminal based chat, come and say hi, if you stay a little longer you can find some other people. If you leave the page open you will get notication when someonne will write something (sound and favicon will update). Type help to see available commands.

Emoji

Inspired by a comment I've created a demo of emoji in terminal. From version 2.1.0 all needed files are included with the package, you need to include emoji.css and emoji.js and use this code that use iamcal/emoji-data

You can type emoji as unicode characters, limited number of ASCII emoticons, or full name of emoji (the one like :smiley:)

$.get('https://unpkg.com/emoji-datasource').then($.terminal.emoji);

If you want to have tab completion with emoji names, simply get names from JSON data and use it in completion, check codepen demo for details.

Create Settings object from questions

Here is example of list of questions that are ask to the user to fill out. User can confirm that the options he set are correct, if not he can correct his options. The code support:

This can be used as equivalent of the normal html form.

This solution is based on Leash (visible when installing the app). And came up from this issue on github.

var mask = ['root_password', 'password'];
var settings = {};
var questions = [
    {
        name: "root_password",
        text: 'Enter your administration password',
        prompt: "root password: "
    },
    {
        name: "server",
        text: "Type your server name"
    },
    {
        name: "username",
        text: "Your normal username"
    },
    {
        name: "home",
        text: "Home directory"
    },
    {
        name: 'guest',
        text: 'Allow guest sessions (Y)es/(N)o',
        boolean: true
    },
    {
        name: 'sudo',
        text: 'Execute sudo for user accounts (Y)es/(N)o',
        boolean: true
    },
    {
        name: "password"
    }
];
function ask_questions(step) {
    var question = questions[step];
    if (question) {
        if (question.text) {
            term.echo('[[b;#fff;]' + question.text + ']');
        }
        var show_mask = mask.indexOf(question.name) != -1;
        term.push(function(command) {
            if (show_mask) {
                term.set_mask(false);
            }
            if (question.boolean) {
                var value;
                if (command.match(/^Y(es)?/i)) {
                    value = true;
                } else if (command.match(/^N(o)?/i)) {
                    value = false;
                }
                if (typeof value != 'undefined') {
                    settings[question.name] = value;
                    term.pop();
                    ask_questions(step+1);
                }
            } else {
                settings[question.name] = command;
                term.pop();
                ask_questions(step+1);
            }
        }, {
            prompt: question.prompt || question.name + ": "
        });
        // set command and mask need to called after push
        // otherwise they will not work
        if (show_mask) {
            term.set_mask(true);
        }
        if (typeof settings[question.name] != 'undefined') {
            if (typeof settings[question.name] == 'boolean') {
                term.set_command(settings[question.name] ? 'y' : 'n');
            } else {
                term.set_command(settings[question.name]);
            }
        }
    } else {
        finish();
    }
}
function finish() {
    term.echo('Your settings:');
    var str = Object.keys(settings).map(function(key) {
        var value = settings[key];
        if (mask.indexOf(key) != -1) {
           // mask everything except first and last character
            var split = value.match(/^(.)(.*)(.)$/, '*');
            value = split[1] + split[2].replace(/./g, '*') + split[3];
        }
        return '[[b;#fff;]' + key + ']: ' + value;
    }).join('\n');
    term.echo(str);
    term.push(function(command) {
        if (command.match(/^y$/i)) {
            term.echo(JSON.stringify(settings));
            term.pop().history().enable();
        } else if (command.match(/^n$/i)) {
            term.pop();
            ask_questions(0);
        }
    }, {
        prompt: 'Are those correct (y|n): '
    });
}
term.history().disable();
ask_questions(0);
        

You can also use new forms feature.

You can also buy access to base application for Vintage Quiz Terminal App on Gumroad.

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://unpkg.com/jquery.terminal@2.1.2/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.

ReactJS Terminal

Here is example us ReactJS Component with two way data binding. It require version 1.11.0 (to be released, right now on devel branch). The change that was required is to allow to call set_command with silient option to not call onCommandChange event that was causing infinite update on render.

Sorry no color, To add highlighting to jsx I need to update library for sytanx highliting.

class Terminal extends React.Component {
  componentDidMount() {
    var {interpreter, command, ...options} = this.props;
    this.terminal = $(this.node).terminal(interpreter, options);
  }
  componentWillUnmount() {
    this.terminal.destroy();
  }
  isCommandControlled() {
    return this.props.command != undefined;
  }
  render() {
    if (this.terminal && this.isCommandControlled()) {
      this.terminal.set_command(this.props.command, true);
    }
    return (
      <div ref={(node) => this.node = node}></div>
    );
  }
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {command: ''};
  }
  update(command) {
    this.setState({
      command
    });
  }
  exec() {
    this.terminal.exec(this.state.command);
    this.update('');
  }
  render() {
    let command = this.state.command;
    return (
      <div>
        <Terminal
          interpreter={
             (command, term) => {
                 term.echo('you typed ' + command);
             }
          }
          height={100}
          command={command}
          onInit={(term) => {this.terminal = term}}
          onCommandChange={(command) => this.update(command)}
          greetings="React Terminal"/>
        <input value={command}
               onChange={(event) => this.update(event.target.value)}
          onKeyDown={(e) => { if (e.which == 13) { this.exec(); }}}/>
        <div>{command}</div>
      </div>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('app')
);
        

You can see the demo on Codepen.

There is also inline-console library with different API.

There is also a way to render React application inside terminal, see demo.

And here is demo of ReactJS application with useImperativeHandler hook.

Electron Terminal

Here is repoitory for basic Electron Application with jQuery Terminal, for when you want to create native command line application or game that will work on Windows, MacOSX or GNU/Linux

Balancing parenthesis

If you type closing parenthesis, it will jump to matched open one.

var position;
var timer;
$('body').terminal($.noop, {
    name: 'parenthesis',
    greetings: "start typing parenthesis",
    keydown: function() {
        if (position) {
            this.set_position(position);
            position = false;
        }
    },
    keypress: function(e) {
        var term = this;
        if (e.key == ')') {
            setTimeout(function() {
                position = term.get_position();
                var command = term.before_cursor();
                var count = 1;
                var close_pos = position - 1;
                var c;
                while (count > 0) {
                    c = command[--close_pos];
                    if (!c) {
                        return;
                    }
                    if (c === '(') {
                        count--;
                    } else if (c == ')') {
                        count++;
                    }
                }
                if (c == '(') {
                    clearTimeout(timer);
                    setTimeout(function() {
                        term.set_position(close_pos);
                        timer = setTimeout(function() {
                            term.set_position(position)
                            position = false;
                        }, 200);
                    }, 0);
                }
            }, 0);
        } else {
            position = false;
        }
    }
});

If you want to support case where parenthesis are inside strings (it shouldn't take that one into account) then you can use this code (taken from my lisp interpteter lips, link in multiline example)

// copy from LIPS source code
var pre_parse_re = /("(?:\\[\S\s]|[^"])*"|\/(?! )[^\/\\]*(?:\\[\S\s][^\/\\]*)*\/[gimy]*(?=\s|\(|\)|$)|;.*)/g;
var tokens_re = /("(?:\\[\S\s]|[^"])*"|\/(?! )[^\/\\]*(?:\\[\S\s][^\/\\]*)*\/[gimy]*(?=\s|\(|\)|$)|\(|\)|'|"(?:\\[\S\s]|[^"])+|(?:\\[\S\s]|[^"])*"|;.*|(?:[-+]?(?:(?:\.[0-9]+|[0-9]+\.[0-9]+)(?:[eE][-+]?[0-9]+)?)|[0-9]+\.)[0-9]|\.|,@|,|`|[^(\s)]+)/gim;
// ----------------------------------------------------------------------
function tokenize(str) {
    var count = 0;
    var offset = 0;
    var tokens = [];
    str.split(pre_parse_re).filter(Boolean).forEach(function(string) {
        if (string.match(pre_parse_re)) {
            if (!string.match(/^;/)) {
                var col = (string.split(/\n/), [""]).pop().length;
                tokens.push({
                    token: string,
                    col,
                    offset: count + offset,
                    line: offset
                });
                count += string.length;
            }
            offset += (string.match("\n") || []).length;
            return;
        }
        string.split('\n').filter(Boolean).forEach(function(line, i) {
            var col = 0;
            line.split(tokens_re).filter(Boolean).forEach(function(token) {
                var line = i + offset;
                var result = {
                    col,
                    line,
                    token,
                    offset: count + line
                };
                col += token.length;
                count += token.length;
                tokens.push(result);
            });
        });
    });
    return tokens;
}
var term = $('body').terminal($.noop, {
    name: 'lips',
    greetings: false,
    // below is the code for parenthesis matching (jumping)
    keydown: function() {
        if (position) {
            term.set_position(position);
            position = false;
        }
    },
    keypress: function(e) {
        var term = this;
        if (e.key == ')') {
            setTimeout(function() {
                position = term.get_position();
                var command = term.before_cursor();
                var len = command.split(/\n/)[0].length;
                var tokens = tokenize(command);
                var count = 1;
                var token;
                var i = tokens.length - 1;
                while (count > 0) {
                    token = tokens[--i];
                    if (!token) {
                        return;
                    }
                    if (token.token === '(') {
                        count--;
                    } else if (token.token == ')') {
                        count++;
                    }
                }
                if (token.token == '(') {
                    clearTimeout(timer);
                    setTimeout(function() {
                        term.set_position(token.offset);
                        timer = setTimeout(function() {
                            term.set_position(position)
                            position = false;
                        }, 200);
                    }, 0);
                }
            }, 0);
        } else {
            position = false;
        }
    }
});
        

The code will tokenize strings and it will not split on parenthesis that are inside regular expressions or strings.

Multiline input

Here is code that can be use together with parenthesis balancing:

// balancing function code is different than previous example
// because we are counting whole command
function balance(code) {
    // tokenize from previous example
    var tokens = tokenize(code);
    var count = 0;
    var token;
    var i = tokens.length;
    while ((token = tokens[--i])) {
        if (token.token === '(') {
            count++
        } else if (token.token == ')') {
            count--;
        }
    }
    return count;
}
var term = $('#multiline .term').terminal(function(command) {
    console.log({command});
}, {
    keymap: {
        ENTER: function(e, original) {
            if (balance(this.get_command()) === 0) {
                original();
            } else {
                this.insert('\n');
            }
        }
    }
});

My Scheme based Lisp interpreter also have basic auto indent.

Rouge like game

If you want to create game like Rouge (game where characters that are elements of environmet are just ASCII characters, more info on Wikipedia), you can use rot.js framework, here is base that you can be used to create real game. It have random generated levels and you collect gold. If you create nice game don't forget to share. The game don't renders on terminal but on canvas, but when interacting it look like part of the terminal.

Browser confirm replacement

Here is jQuery plugin that can be used as replacement for native browser function confirm:

$.fn.confirm = async function(message) {
    var term = $(this).terminal();
    const response = await new Promise(function(resolve) {
        term.push(function(command) {
            if (command.match(/Y(es)?/i)) {
                resolve(true);
            } else if (command.match(/N(o)?/i)) {
                resolve(false);
            }
        }, {
            prompt: message
        });
    });
    term.pop();
    return response;
};
        

It use new async await syntax that will not work in IE, for this browser you can use this code that only use Promises or like in code below using jQuery Deferred:

$.fn.confirm = function(message) {
    var term = $(this).terminal();
    var deferred = new $.Deferred();
    term.push(function(command) {
        if (command.match(/^Y(es)?$/i)) {
            deferred.resolve(true);
        } else if (command.match(/^N(o)?$/i)) {
            deferred.resolve(false);
        }
        if (command.match(/^(Y(es)?|N(o)?)$/i)) {
            term.pop();
        }
    }, {
        prompt: message
    });
    return deferred.promise();
};
        

To use the plugin you can use this code:

term.confirm('Are you sure? Y/N ').then(function(confirm) {
   if (confirm) {
      console.log('User confirm');
   } else {
      console.log("User didn't confirm");
   }
});
        

Echo without newline

This was requested few times and I've finally created monkey patch for echo command.

From version 2.8.0 repo contain file (the file require version 2.8.0 that added new API method echo_command). If you include the file you will have new option in echo newline (default is true).

<script src="https://unpkg.com/jquery.terminal/js/echo_newline.js"></script>
$('body').terminal({
    foo: async function() {
        this.echo('.', {newline: false});
        await sleep(1000);
        this.echo('.', {newline: false});
        await sleep(1000);
        this.echo('.', {newline: false});
    }
});

Below is code for original example that was adding echo, it was buggy. The one in repo is much better, and it have unit test coverage.

It will not be added to the library though. It will only work with string prompts, functions will require more work.

var last;
var term = $('body').terminal($.noop, {
    echoCommand: false,
    keymap: {
        'ENTER': function(e, original) {
            var str;
            if (!last) {
                str = this.get_prompt() + this.get_command();
            } else {
                var str = last + prompt + this.get_command();
                last = '';
            }
            this.echo(str);
            original(e);
        }
    },
    prompt: '>>> '
});
var prompt = term.get_prompt();
(function(echo) {
    term.echo = function(arg, options) {
        var settings = $.extend({
            newline: true
        }, options);
        if (!prompt) {
            prompt = this.get_prompt();
        }
        if (settings.newline === false) {
            // this probably can be simplify because terminal handle
            // newlines in prompt
            last += arg;
            arg += this.get_prompt();
            var arr = arg.split('\n');
            var last_line;
            if (arr.length === 1) {
                last_line = arg;
            } else {
                echo(arr.slice(0, -1).join('\n'), options);
                last_line = arr[arr.length - 1];
            }
            this.set_prompt(last_line);
        } else {
            if (prompt) {
                this.set_prompt(prompt);
            }
            if (last) {
                echo(last + arg, options);
            } else {
                echo(arg, options);
            }
            last = '';
            prompt = '';
        }
    };
})(term.echo);
        

ANSI artwork

From version 2.0.0 that fixed unix_formatting you can view ANSI artwork on Terminal.

First you need to convert the artwork to UTF-8 on Linux you can use iconv command

iconv -f CP437 -t UTF-8 < artwork.ans
          

You can find ANSI artwork in Fuel Magazine (files with .ans extension) or in google.

Here is codepen demo that display few artworks from fuel. It looks best on Linux and Window 10. Windows 7 and lowwer have broken implementation of Unicode so they don't look good. On MacOS it also don't look good (because of how Unicode characters are rendered) but better than on Windows 7.

Figlet ASCII art fonts

Figlet is a unix utility, that allows to render text using ASCII art fonts.

Example:

    _______       __     __     ____
   / ____(_)___ _/ /__  / /_   / __ \___  ____ ___  ____
  / /_  / / __ `/ / _ \/ __/  / / / / _ \/ __ `__ \/ __ \
 / __/ / / /_/ / /  __/ /_   / /_/ /  __/ / / / / / /_/ /
/_/   /_/\__, /_/\___/\__/  /_____/\___/_/ /_/ /_/\____/
        /____/
          

There is also library in JavaScript on npm, that implement same algorithm (and reuse the fonts). You can use that library from webpack or from unpgk.com (or different CDN), to render text or greetings on the terminal.

With this you can have greetings similar to default one (but that was created by hand).

You also don't need to care about escaping special characters.

To see how it will look like in terminal, see this Codepen Demo.

FontAwesome Icons

From version 2.16.0 the library have improvements for handling FontAwesome icons, in output and command line.

Here is demo of using FontAwesome icons with jQuery Terminal.

Mobile demos

Terminal work out of the box on mobile (latest version tested mostly on Android).

To use terminal on mobile you just need standard viewport meta tag

<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
          

Demos that can be tested on mobile (default examples that are in git repository on GitHub)

NOTE: some examples may show old solution to full screen terminal.

$('body').terminal(function() {
}, {
    onBlur: function() {
        return false;
    }
});
        

If you use onBlur like this, your terminal will not work on mobile. This is because on Mobile (require by the platform) keyboard can't be in focus when you open any website. To activate the keyboard, you need disabled terminal, so you can tap on it to enable and show virtual keyboard.

Codepen demos

Here is a link to a list of various codepen demos.

In the wild