Click or type ~ to show Console
Type help or hit TAB for a list of commands. Press Ctrl-C to hide the console.

Quake-style console with unix-style path handling

This tutorial shows implements a quake-style shell that drops down over your current page. Inside the shell a fake filesystem illustrates how Pathhandler.js can be used to provide standard unix commands ls, cd, pwd and filepath tab completion.

Try out the Console

Type ~ to activate the shell we will be building in this tutorial.

Hooking up PathHandler.js

PathHandler is a mix-in for Josh.Shell to provide provide the standard unix ls, pwd and cd commands, as well as standard bash-style path tab-completion. It expects a Josh.Shell instance as its first argument so that it can attach its command handlers to the shell as well as override the default handler to support completion of paths starting with . or / without a leading command.

var shell = Josh.Shell();
var pathhandler = new Josh.PathHandler(shell);

PathHandler operates on path nodes which are expected to be objects with a minimum structure of:

{
    name: 'localname',
    path: '/full/path/to/localname'
}

where name is the name of the node and path is the absolute path to the node. It does not modify these nodes, so any additional state your implementation requires can be attached to the nodes and be relied on as being part of the node when PathHandler provides one to your handler as an argument.

The pathhandler expects to be initialized with the current directory in the form pf a path node. For this example, we've build up a full tree of nodes providing a skeleton unix file system. Our nodes take the structure of:

{
    name: 'localname',
    path: '/full/path/to/localname',
    parent: {parent node},
    children: [{child nodes}]
}

This allows the example to simply navigate up and down a tree from any node. The tree is built by the helper builtTree() whose implementation is not of consequence to how PathHandler works and is therefore just assumed to exist, so that we can create the tree and assign its root as pathhandler.current.

var treeroot = buildTree();
pathhandler.current = treeroot;

Implementing PathHandler's required methods

PathHandler requires two method, getNode and getChildNodes, to be provided in order to operate.

getNode gets called with a path string. This string is completely opaque to PathHandler, i.e. supporting constructs such as . and .. are up to the implementor. PathHandler calls getNode anytime it has a path and needs to determine what if any node exists at that path. Thish happens during path completion as well as cd and ls execution. It simply provides the path and expects its callback to be called with a pathnode for that path or null. The only assumption about paths that it does have is that the path separator is /.

pathhandler.getNode = function(path, callback) {
  if(!path) {
    return callback(pathhandler.current);
  }
  var parts = _.filter(path.split('/'), function(x) {
    return x;
  });
  var start = ((path || '')[0] == '/') ? treeroot : pathhandler.current;
  return findNode(start, parts, callback);
};

For this example, no path always means the current node, otherwise we split the path into its components and walk the tree via a helper findNode from either the root or the current node depending on whether the path started with a /. findNode is specific to this implementation, since it can work on an in memory tree. In a service bound implementation the findNode logic would likely reside at the server and the callback called in the completion closure of an ajax call.

function findNode(current, parts, callback) {
  if(!parts || parts.length == 0) {
    return callback(current);
  }
  if(parts[0] == ".") {

  } else if(parts[0] == "..") {
    current = current.parent;
  } else {
    current = _.first(_.filter(current.childnodes, function(node) {
      return node.name == parts[0];
    }));
  }
  if(!current) {
    return callback();
  }
  return findNode(current, _.rest(parts), callback);
}

The second required method is getChildNodes and is used by path completion to determine the possible completion candidates. Path completion first determines the nearest resolvable node for the given path. It does this by first calling getNode on the current path and failing to get a node, looking for the nearest tail / to find a parent and use the trailing characters as the partial path to be completed against children found via getChildNodes. For our example, we've attached the child node objects directly to the pathnode object, so we can simply return it. Usually this would be used to call the server with the provided node's path or id so that the appropriate children could be retrieved.

pathhandler.getChildNodes = function(node, callback) { callback(node.childnodes); };

Setup Quake-console Behavior

The default name for the div the shell uses as its container is shell-panel, although that can be changed via the shell config parameter shell-panel-id. The Josh.Shell display model is based on a panel that defines the viewport of the console screen while the content is appended to a view that is contained inside the panel and continuously scrolled down. For the quake-style console, we want the panel to take up the width of the browser and overlay the page's top portion. For this we add the panel right after the body tag:

<div id="shell-panel">
  <div>Type <code>help</code> or hit <code>TAB</code> for a list of commands.
    Press <code>Ctrl-C</code> to hide the console.
  </div>
  <div id="shell-view"></div>
</div>

With css, we make sure this is initially invisible, is fixed in top position and has a high z-index so that it will always be on top.

We use jquery-ui's resizable so that the shell can be resized by dragging its bottom edge.

var $consolePanel = $('#shell-panel');
$consolePanel.resizable({ handles: "s"});

Next, we wire up a the keypress handler for shell activation, since Josh.ReadLine does not listen to keys until the shell is activated. This handler will only execute while the shell is inactive and trigger on ~, using jquery animaton to slide it down and give it focus.

$(document).keypress(function(event) {
  if(shell.isActive()) {
    return;
  }
  if(event.keyCode == 126) {
    event.preventDefault();
    shell.activate();
    $consolePanel.slideDown();
    $consolePanel.focus();
  }
});

Finally, we wire create a function to deactivate the shell, slide it back up and hide it and attach it to the shell's EOT (Ctrl-D on empty line) or a Cancel (Ctrl-C) signals: we deactivate the shell and hide the console.

function hideAndDeactivate() {
  shell.deactivate();
  $consolePanel.slideUp();
  $consolePanel.blur();
}
shell.onEOT(hideAndDeactivate);
shell.onCancel(hideAndDeactivate);

Now the Shell is ready to be activated and it's faked unix file system browsed, all with minimal custom code.