~ to show Consolehelp or hit TAB for a list of commands. Press
Ctrl-C to hide the console.
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.
Type ~ to activate the shell we will be building in this tutorial.
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;
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);
};
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.