~
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.