diff --git a/js/web/example.js b/js/web/example.js new file mode 100644 index 0000000..7607455 --- /dev/null +++ b/js/web/example.js @@ -0,0 +1,264 @@ +/*------------------------------------------------------------------------* + * Copyright 2013 Arne F. Claassen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *-------------------------------------------------------------------------*/ +(function(root, $, _) { + Josh.Example = (function(root, $, _) { + + // Enable console debugging, when Josh.Debug is set and there is a console object on the document root. + var _console = (Josh.Debug && root.console) ? root.console : { + log: function() { + } + }; + + // Setup of Shell + // -------------- + + // build the *fake* directory structure used to illustrate path commands and completions. + var treeroot = buildTree(); + + // Create `History` and `KillRing` by hand since we will use the `KillRing` for an example command. + var history = Josh.History(); + var killring = new Josh.KillRing(); + + // Create the `ReadLine` instance by hand so that we can provide it our `KillRing`. Since the shell needs to share + // the `History` object with `ReadLine` and `Shell` isn't getting to create `ReadLine` automatically as it usually does + // we need to pass in `History` into `ReadLine` as well. + var readline = new Josh.ReadLine({history: history, killring: killring, console: _console }); + + // Finally, create the `Shell`. + var shell = Josh.Shell({readline: readline, history: history, console: _console}); + + + // Create *killring* command + // ------------------------- + + // Setup the `Underscore` template for displaying items in the `KillRing`. + var killringItemTemplate = _.template("
<% _.each(items, function(item, i) { %>
<%- i %> <%- item %>
<% }); %>
") + + // Create a the command `killring` which will display all text currently in the `KillRing`, by attaching + // a handler to the `Shell`. + shell.setCommandHandler("killring", { + + // We don't implement any completion for the `killring` command, so we only provide an `exec` handler, and no `completion` handler. + exec: function(cmd, args, callback) { + + // `killring` takes one optional argument **-c** which clears the killring (just like **history -c**). + if(args[0] == "-c") { + killring.clear(); + + // The callback of an `exec` handler expects the html to display as result of executing the command. Clearing the + // killing has no output, so we just call the callback and exit the handler. + callback(); + return; + } + + // Return the output of feeding all items from the killring into our template. + callback(killringItemTemplate({items: killring.items()})); + } + }); + + // Setup PathHandler + // ----------------- + + // `PathHandler` is a mix-in for `Shell` to provide provide the standard unix `ls`, `pwd` and `cd` commands, as well + // as standard *bash*-style path tab-completion. It expects a `Shell` instance as its first argument so that it can + // attach its command handlers to the shell as well as overrride the default handler to support completion of path's + // starting with `.` or `/` without a leading command. + var pathhandler = new Josh.PathHandler(shell, {console: _console}); + + // `PathHandler` operates on path nodes which are expected to be objects with the 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. PathHandler does not modify + // these nodes, so any additional state your implementation requires can be attached to the nodes and be relied on + // being part of the node when received by the handling methods you implement. + // + // The pathhandler expects to be initialized with the current *directory*, i.e. a path node. + pathhandler.current = treeroot; + + // `PathHandler` requires two method, `getNode` and `getChildNodes`, to be provided in order to operate. + // + // `getNode` gets called with *path* string. This string is completely opaque to `PathHandler`, i.e. constructs such + // as `.` and `..` are an implementation detail. `PathHandler` does assume that the path separator is `/`. `getNode` + // is called anytime the pathhandler has a path and need to determine what if any node exists at that path which happens + // during path completion as well as `cd` and `ls` execution. + 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; + _console.log('start: ' + start.path + ', parts: ' + JSON.stringify(parts)); + return findNode(start, parts, callback); + }; + + // `getChildNodes` is used by path completion to determine the possible completion candidates. Path completion first + // determines the node for the given path, looking for the nearest `/` in case if the given path does not return a + // node via `getNode`. For our example, we've attached the child node objects directly to the node 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 can be found. + pathhandler.getChildNodes = function(node, callback) { + _console.log("children for " + node.name); + callback(node.childnodes); + }; + + // `findNode` is called recursively from `getNode` with the current node and remaining path already split into + // segments. It then simply resolves the node for the next segment in `parts` to a node, including relative + // references like `.` and `..`. In implementations that let you explore an hierarchy on a server, this function + // would live on the server side and be called remotely via `getNode`. + 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); + } + + // Setup Document Behavior + // ----------------------- + + // Activation and display behavior happens at document ready time. + $(root).ready(function() { + + // 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 `Shell` display model relies on a 'panel' to contain a 'view'. + // The 'panel' acts as the view-port, i.e. the visible portion of the shell content, while the 'view' is appended + // to and scrolled up as new content is added. + var $consolePanel = $('#shell-panel'); + + // attach readline to the shell panel + readline.attach($consolePanel.get(0)); + $consolePanel.focus(); + + // We use **jquery-ui**'s `resizable` to let us drag the bottom edge of the console up and down. + $consolePanel.resizable({ handles: "s"}); + }); + + // We attach the various objects we've created here to `Josh.Instance` purely so they can be inspected via a + // javascript console. This is not required for the functionality of the example. + Josh.Instance = { + Tree: treeroot, + Shell: shell, + PathHandler: pathhandler, + KillRing: killring + }; + + // This code builds our *fake* directory structure. Since most real applications of `Josh` would not keep their + // entire hierarchy in memory, but instead make callbacks to a server to find nodes and node children, the details + // of this function are of little interest. + function buildTree() { + var fs = { + bin: {}, + boot: {}, + dev: {}, + etc: { + default: {}, + 'rc.d': {}, + sysconfig: {}, + x11: {} + }, + home: { + bob: { + video: { + 'firefly.m4v': {} + }, + videos: { + 'Arrested Development': { + 's1e1.m4v': {} + }, + 'Better Off Ted': { + 's1e1.m4v': {} + } + } + }, + jane: {} + }, + lib: {}, + 'lost+found': {}, + misc: {}, + mnt: { + cdrom: {}, + sysimage: {} + }, + net: {}, + opt: {}, + proc: {}, + root: {}, + sbin: {}, + usr: { + x11: {}, + bin: {}, + include: {}, + lib: {}, + local: {}, + man: {}, + sbin: {}, + share: { + doc: {} + }, + src: {} + }, + var: { + lib: {}, + lock: {}, + run: {}, + log: { + httpd: { + access_log: {}, + error_log: {} + }, + 'boot.log': {}, + cron: {}, + messages: {} + } + } + }; + + function build(parent, node) { + parent.childnodes = _.map(_.pairs(node), function(pair) { + var child = { + name: pair[0], + path: parent.path + "/" + pair[0], + parent: parent + }; + build(child, pair[1]); + return child; + }); + parent.children = _.keys(node); + return parent; + } + var tree = build({name: "", path: ""}, fs); + tree.path = '/'; + return tree; + } + })(root, $, _); +})(this, $, _);