]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/content/dactyl.js
Import 1.0rc1 supporting Firefox up to 11.*
[dactyl.git] / common / content / dactyl.js
index 1a244ff641412240d89ec6c7c4726bdcd993529e..dc9df88be5fd42c2adfc36014e01c939a3d56bb8 100644 (file)
@@ -4,7 +4,7 @@
 //
 // This work is licensed for reuse under an MIT license. Details are
 // given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
 
 /** @scope modules */
 
@@ -29,10 +29,6 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         this._observers = {};
         util.addObserver(this);
 
-        this.commands["dactyl.help"] = function (event) {
-            let elem = event.originalTarget;
-            dactyl.help(elem.getAttribute("tag") || elem.textContent);
-        };
         this.commands["dactyl.restart"] = function (event) {
             dactyl.restart();
         };
@@ -40,7 +36,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         styles.registerSheet("resource://dactyl-skin/dactyl.css");
 
         this.cleanups = [];
-        this.cleanups.push(util.overlayObject(window, {
+        this.cleanups.push(overlay.overlayObject(window, {
             focusAndSelectUrlBar: function focusAndSelectUrlBar() {
                 switch (options.get("strictfocus").getKey(document.documentURIObject || util.newURI(document.documentURI), "moderate")) {
                 case "laissez-faire":
@@ -60,10 +56,16 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         delete window.dactyl;
         delete window.liberator;
 
+        // Prevents box ordering bugs after our stylesheet is removed.
+        styles.system.add("cleanup-sheet", config.styleableChrome, <![CDATA[
+            #TabsToolbar tab { display: none; }
+        ]]>);
         styles.unregisterSheet("resource://dactyl-skin/dactyl.css");
+        DOM('#TabsToolbar tab', document).style.display;
     },
 
     destroy: function () {
+        this.observe.unregister();
         autocommands.trigger("LeavePre", {});
         dactyl.triggerObserver("shutdown", null);
         util.dump("All dactyl modules destroyed\n");
@@ -97,9 +99,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                     this.trapErrors("destroy", mod, reason);
             }
 
-            for (let mod in values(modules.ownPropertyValues.reverse()))
-                if (mod instanceof Class && "INIT" in mod && "cleanup" in mod.INIT)
-                    this.trapErrors(mod.cleanup, mod, dactyl, modules, window, reason);
+            modules.moduleManager.initDependencies("cleanup");
 
             for (let name in values(Object.getOwnPropertyNames(modules).reverse()))
                 try {
@@ -110,20 +110,14 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         }
     },
 
-    /** @property {string} The name of the current user profile. */
-    profileName: Class.memoize(function () {
-        // NOTE: services.profile.selectedProfile.name doesn't return
-        // what you might expect. It returns the last _actively_ selected
-        // profile (i.e. via the Profile Manager or -P option) rather than the
-        // current profile. These will differ if the current process was run
-        // without explicitly selecting a profile.
-
-        let dir = services.directory.get("ProfD", Ci.nsIFile);
-        for (let prof in iter(services.profile.profiles))
-            if (prof.QueryInterface(Ci.nsIToolkitProfile).rootDir.path === dir.path)
-                return prof.name;
-        return "unknown";
-    }),
+    signals: {
+        "io.source": function ioSource(context, file, modTime) {
+            if (context.INFO)
+                help.flush("help/plugins.xml", modTime);
+        }
+    },
+
+    profileName: deprecated("config.profileName", { get: function profileName() config.profileName }),
 
     /**
      * @property {Modes.Mode} The current main mode.
@@ -134,29 +128,27 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         set: function mode(val) modes.main = val
     }),
 
-    get menuItems() {
-        function dispatch(node, name) {
-            let event = node.ownerDocument.createEvent("Events");
-            event.initEvent(name, false, false);
-            node.dispatchEvent(event);
-        }
-
+    getMenuItems: function getMenuItems(targetPath) {
         function addChildren(node, parent) {
+            DOM(node).createContents();
+
             if (~["menu", "menupopup"].indexOf(node.localName) && node.children.length)
-                dispatch(node, "popupshowing");
+                DOM(node).popupshowing({ bubbles: false });
 
             for (let [, item] in Iterator(node.childNodes)) {
                 if (item.childNodes.length == 0 && item.localName == "menuitem"
                     && !item.hidden
                     && !/rdf:http:/.test(item.getAttribute("label"))) { // FIXME
                     item.dactylPath = parent + item.getAttribute("label");
-                    items.push(item);
+                    if (!targetPath || targetPath.indexOf(item.dactylPath) == 0)
+                        items.push(item);
                 }
                 else {
                     let path = parent;
                     if (item.localName == "menu")
                         path += item.getAttribute("label") + ".";
-                    addChildren(item, path);
+                    if (!targetPath || targetPath.indexOf(path) == 0)
+                        addChildren(item, path);
                 }
             }
         }
@@ -166,14 +158,24 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         return items;
     },
 
+    get menuItems() this.getMenuItems(),
+
     // Global constants
     CURRENT_TAB: "here",
     NEW_TAB: "tab",
     NEW_BACKGROUND_TAB: "background-tab",
     NEW_WINDOW: "window",
 
-    forceNewTab: false,
-    forceNewWindow: false,
+    forceBackground: null,
+    forceTarget: null,
+
+    get forceOpen() ({ background: this.forceBackground,
+                       target: this.forceTarget }),
+    set forceOpen(val) {
+        for (let [k, v] in Iterator({ background: "forceBackground", target: "forceTarget" }))
+            if (k in val)
+                this[v] = val[k];
+    },
 
     version: deprecated("config.version", { get: function version() config.version }),
 
@@ -202,7 +204,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
     registerObserver: function registerObserver(type, callback, weak) {
         if (!(type in this._observers))
             this._observers[type] = [];
-        this._observers[type].push(weak ? Cu.getWeakReference(callback) : { get: function () callback });
+        this._observers[type].push(weak ? util.weakReference(callback) : { get: function () callback });
     },
 
     registerObservers: function registerObservers(obj, prop) {
@@ -215,7 +217,6 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             this._observers[type] = this._observers[type].filter(function (c) c.get() != callback);
     },
 
-    // TODO: "zoom": if the zoom value of the current buffer changed
     applyTriggerObserver: function triggerObserver(type, args) {
         if (type in this._observers)
             this._observers[type] = this._observers[type].filter(function (callback) {
@@ -249,7 +250,8 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                 let results = array(params.iterate(args))
                     .sort(function (a, b) String.localeCompare(a.name, b.name));
 
-                let filters = args.map(function (arg) util.regexp("\\b" + util.regexp.escape(arg) + "\\b", "i"));
+                let filters = args.map(function (arg) let (re = util.regexp.escape(arg))
+                                        util.regexp("\\b" + re + "\\b|(?:^|[()\\s])" + re + "(?:$|[()\\s])", "i"));
                 if (filters.length)
                     results = results.filter(function (item) filters.every(function (re) keys(item).some(re.closure.test)));
 
@@ -261,10 +263,11 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                 completer: function (context, args) {
                     context.keys.text = util.identity;
                     context.keys.description = function () seen[this.text] + /*L*/" matching items";
+                    context.ignoreCase = true;
                     let seen = {};
                     context.completions = array(keys(item).join(" ").toLowerCase().split(/[()\s]+/)
                                                 for (item in params.iterate(args)))
-                        .flatten().filter(function (w) /^\w[\w-_']+$/.test(w))
+                        .flatten()
                         .map(function (k) {
                             seen[k] = (seen[k] || 0) + 1;
                             return k;
@@ -278,10 +281,10 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                 let results = array((params.iterateIndex || params.iterate).call(params, commands.get(name).newArgs()))
                         .array.sort(function (a, b) String.localeCompare(a.name, b.name));
 
-                let tags = services["dactyl:"].HELP_TAGS;
+                let haveTag = Set.has(help.tags);
                 for (let obj in values(results)) {
                     let res = dactyl.generateHelp(obj, null, null, true);
-                    if (!Set.has(tags, obj.helpTag))
+                    if (!haveTag(obj.helpTag))
                         res[1].@tag = obj.helpTag;
 
                     yield res;
@@ -303,7 +306,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             };
             XML.ignoreWhitespace = true;
             if (!elems.bell)
-                util.overlayWindow(window, {
+                overlay.overlayWindow(window, {
                     objects: elems,
                     prepend: <>
                         <window id={document.documentElement.id} xmlns={XUL}>
@@ -335,16 +338,20 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
      * This is same as Firefox's readFromClipboard function, but is needed for
      * apps like Thunderbird which do not provide it.
      *
+     * @param {string} which Which clipboard to write to. Either
+     *     "global" or "selection". If not provided, both clipboards are
+     *     updated.
+     *     @optional
      * @returns {string}
      */
-    clipboardRead: function clipboardRead(getClipboard) {
+    clipboardRead: function clipboardRead(which) {
         try {
-            const clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
-            const transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
+            const { clipboard } = services;
 
+            let transferable = services.Transferable();
             transferable.addDataFlavor("text/unicode");
 
-            let source = clipboard[getClipboard || !clipboard.supportsSelectionClipboard() ?
+            let source = clipboard[which == "global" || !clipboard.supportsSelectionClipboard() ?
                                    "kGlobalClipboard" : "kSelectionClipboard"];
             clipboard.getData(transferable, source);
 
@@ -363,12 +370,19 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
      * Copies a string to the system clipboard. If *verbose* is specified the
      * copied string is also echoed to the command line.
      *
-     * @param {string} str
-     * @param {boolean} verbose
+     * @param {string} str The string to write.
+     * @param {boolean} verbose If true, the user is notified of the copied data.
+     * @param {string} which Which clipboard to write to. Either
+     *     "global" or "selection". If not provided, both clipboards are
+     *     updated.
+     *     @optional
      */
-    clipboardWrite: function clipboardWrite(str, verbose) {
-        const clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
-        clipboardHelper.copyString(str);
+    clipboardWrite: function clipboardWrite(str, verbose, which) {
+        if (which == null || which == "selection" && !services.clipboard.supportsSelectionClipboard())
+            services.clipboardHelper.copyString(str);
+        else
+            services.clipboardHelper.copyStringToClipboard(str,
+                services.clipboard["k" + util.capitalize(which) + "Clipboard"]);
 
         if (verbose) {
             let message = { message: _("dactyl.yank", str) };
@@ -406,8 +420,10 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
     echoerr: function echoerr(str, flags) {
         flags |= commandline.APPEND_TO_MESSAGES;
 
-        if (isinstance(str, ["DOMException", "Error", "Exception"]) || isinstance(str, ["XPCWrappedNative_NoHelper"]) && /^\[Exception/.test(str))
+        if (isinstance(str, ["DOMException", "Error", "Exception", ErrorBase])
+                || isinstance(str, ["XPCWrappedNative_NoHelper"]) && /^\[Exception/.test(str))
             dactyl.reportError(str);
+
         if (isObject(str) && "echoerr" in str)
             str = str.echoerr;
         else if (isinstance(str, ["Error", FailedAssertion]) && str.fileName)
@@ -465,43 +481,50 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
 
     userEval: function (str, context, fileName, lineNumber) {
         let ctxt;
-        if (jsmodules.__proto__ != window)
+        if (jsmodules.__proto__ != window && jsmodules.__proto__ != XPCNativeWrapper(window) &&
+                jsmodules.isPrototypeOf(context))
             str = "with (window) { with (modules) { (this.eval || eval)(" + str.quote() + ") } }";
 
         let info = contexts.context;
         if (fileName == null)
-            if (info && info.file[0] !== "[")
+            if (info)
                 ({ file: fileName, line: lineNumber, context: ctxt }) = info;
 
-        if (!context && fileName && fileName[0] !== "[")
+        if (fileName && fileName[0] == "[")
+            fileName = "dactyl://command-line/";
+        else if (!context)
             context = ctxt || _userContext;
 
         if (isinstance(context, ["Sandbox"]))
             return Cu.evalInSandbox(str, context, "1.8", fileName, lineNumber);
-        else
-            try {
-                if (!context)
-                    context = userContext || ctxt;
-
-                context[EVAL_ERROR] = null;
-                context[EVAL_STRING] = str;
-                context[EVAL_RESULT] = null;
-                this.loadScript("resource://dactyl-content/eval.js", context);
-                if (context[EVAL_ERROR]) {
-                    try {
-                        context[EVAL_ERROR].fileName = info.file;
-                        context[EVAL_ERROR].lineNumber += info.line;
-                    }
-                    catch (e) {}
-                    throw context[EVAL_ERROR];
+
+        if (!context)
+            context = userContext || ctxt;
+
+        if (services.has("dactyl") && services.dactyl.evalInContext)
+            return services.dactyl.evalInContext(str, context, fileName, lineNumber);
+
+        try {
+            context[EVAL_ERROR] = null;
+            context[EVAL_STRING] = str;
+            context[EVAL_RESULT] = null;
+
+            this.loadScript("resource://dactyl-content/eval.js", context);
+            if (context[EVAL_ERROR]) {
+                try {
+                    context[EVAL_ERROR].fileName = info.file;
+                    context[EVAL_ERROR].lineNumber += info.line;
                 }
-                return context[EVAL_RESULT];
-            }
-            finally {
-                delete context[EVAL_ERROR];
-                delete context[EVAL_RESULT];
-                delete context[EVAL_STRING];
+                catch (e) {}
+                throw context[EVAL_ERROR];
             }
+            return context[EVAL_RESULT];
+        }
+        finally {
+            delete context[EVAL_ERROR];
+            delete context[EVAL_RESULT];
+            delete context[EVAL_STRING];
+        }
     },
 
     /**
@@ -543,19 +566,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
     },
 
     focus: function focus(elem, flags) {
-        flags = flags || services.focus.FLAG_BYMOUSE;
-        try {
-            if (elem instanceof Document)
-                elem = elem.defaultView;
-            if (elem instanceof Element)
-                services.focus.setFocus(elem, flags);
-            else if (elem instanceof Window)
-                services.focus.focusedWindow = elem;
-        }
-        catch (e) {
-            util.dump(elem);
-            util.reportError(e);
-        }
+        DOM(elem).focus(flags);
     },
 
     /**
@@ -621,33 +632,6 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
      */
     has: function (feature) Set.has(config.features, feature),
 
-    /**
-     * Returns the URL of the specified help *topic* if it exists.
-     *
-     * @param {string} topic The help topic to look up.
-     * @param {boolean} consolidated Whether to search the consolidated help page.
-     * @returns {string}
-     */
-    findHelp: function (topic, consolidated) {
-        if (!consolidated && topic in services["dactyl:"].FILE_MAP)
-            return topic;
-        let items = completion._runCompleter("help", topic, null, !!consolidated).items;
-        let partialMatch = null;
-
-        function format(item) item.description + "#" + encodeURIComponent(item.text);
-
-        for (let [i, item] in Iterator(items)) {
-            if (item.text == topic)
-                return format(item);
-            else if (!partialMatch && topic)
-                partialMatch = item;
-        }
-
-        if (partialMatch)
-            return format(partialMatch);
-        return null;
-    },
-
     /**
      * @private
      */
@@ -663,243 +647,18 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         }
     },
 
+    help: deprecated("help.help", { get: function help() modules.help.closure.help }),
+    findHelp: deprecated("help.findHelp", { get: function findHelp() help.closure.findHelp }),
+
     /**
      * @private
      * Initialize the help system.
      */
-    initHelp: function (force) {
-        // Waits for the add-on to become available, if necessary.
-        config.addon;
-        config.version;
-
-        if (force || !this.helpInitialized) {
-            if ("noscriptOverlay" in window) {
-                noscriptOverlay.safeAllow("chrome-data:", true, false);
-                noscriptOverlay.safeAllow("dactyl:", true, false);
-            }
-
-            // Find help and overlay files with the given name.
-            let findHelpFile = function findHelpFile(file) {
-                let result = [];
-                for (let [, namespace] in Iterator(namespaces)) {
-                    let url = ["dactyl://", namespace, "/", file, ".xml"].join("");
-                    let res = util.httpGet(url);
-                    if (res) {
-                        if (res.responseXML.documentElement.localName == "document")
-                            fileMap[file] = url;
-                        if (res.responseXML.documentElement.localName == "overlay")
-                            overlayMap[file] = url;
-                        result.push(res.responseXML);
-                    }
-                }
-                return result;
-            };
-            // Find the tags in the document.
-            let addTags = function addTags(file, doc) {
-                for (let elem in util.evaluateXPath("//@tag|//dactyl:tags/text()|//dactyl:tag/text()", doc))
-                    for (let tag in values((elem.value || elem.textContent).split(/\s+/)))
-                        tagMap[tag] = file;
-            };
-
-            let namespaces = ["locale-local", "locale"];
-            services["dactyl:"].init({});
-
-            let tagMap = services["dactyl:"].HELP_TAGS;
-            let fileMap = services["dactyl:"].FILE_MAP;
-            let overlayMap = services["dactyl:"].OVERLAY_MAP;
-
-            // Scrape the list of help files from all.xml
-            // Manually process main and overlay files, since XSLTProcessor and
-            // XMLHttpRequest don't allow access to chrome documents.
-            tagMap["all"] = tagMap["all.xml"] = "all";
-            tagMap["versions"] = tagMap["versions.xml"] = "versions";
-            let files = findHelpFile("all").map(function (doc)
-                    [f.value for (f in util.evaluateXPath("//dactyl:include/@href", doc))]);
-
-            // Scrape the tags from the rest of the help files.
-            array.flatten(files).forEach(function (file) {
-                tagMap[file + ".xml"] = file;
-                findHelpFile(file).forEach(function (doc) {
-                    addTags(file, doc);
-                });
-            });
-
-            // Process plugin help entries.
-            XML.ignoreWhiteSpace = XML.prettyPrinting = false;
-
-            let body = XML();
-            for (let [, context] in Iterator(plugins.contexts))
-                try {
-                    let info = contexts.getDocs(context);
-                    if (info instanceof XML) {
-                        if (info.*.@lang.length()) {
-                            let lang = config.bestLocale(String(a) for each (a in info.*.@lang));
-
-                            info.* = info.*.(function::attribute("lang").length() == 0 || @lang == lang);
-
-                            for each (let elem in info.NS::info)
-                                for each (let attr in ["@name", "@summary", "@href"])
-                                    if (elem[attr].length())
-                                        info[attr] = elem[attr];
-                        }
-                        body += <h2 xmlns={NS.uri} tag={info.@name + '-plugin'}>{info.@summary}</h2> +
-                            info;
-                    }
-                }
-                catch (e) {
-                    util.reportError(e);
-                }
+    initHelp: function initHelp() {
+        if ("noscriptOverlay" in window)
+            noscriptOverlay.safeAllow("dactyl:", true, false);
 
-            let help =
-                '<?xml version="1.0"?>\n' +
-                '<?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>\n' +
-                '<!DOCTYPE document SYSTEM "resource://dactyl-content/dactyl.dtd">\n' +
-                <document xmlns={NS}
-                    name="plugins" title={config.appName + " Plugins"}>
-                    <h1 tag="using-plugins">{_("help.title.Using Plugins")}</h1>
-                    <toc start="2"/>
-
-                    {body}
-                </document>.toXMLString();
-            fileMap["plugins"] = function () ['text/xml;charset=UTF-8', help];
-
-            fileMap["versions"] = function () {
-                let NEWS = util.httpGet(config.addon.getResourceURI("NEWS").spec,
-                                        { mimeType: "text/plain;charset=UTF-8" })
-                               .responseText;
-
-                let re = util.regexp(<![CDATA[
-                      ^ (?P<comment> \s* # .*\n)
-
-                    | ^ (?P<space> \s*)
-                        (?P<char>  [-•*+]) \ //
-                      (?P<content> .*\n
-                         (?: \2\ \ .*\n | \s*\n)* )
-
-                    | (?P<par>
-                          (?: ^ [^\S\n]*
-                              (?:[^-•*+\s] | [-•*+]\S)
-                              .*\n
-                          )+
-                      )
-
-                    | (?: ^ [^\S\n]* \n) +
-                ]]>, "gmxy");
-
-                let betas = util.regexp(/\[(b\d)\]/, "gx");
-
-                let beta = array(betas.iterate(NEWS))
-                            .map(function (m) m[1]).uniq().slice(-1)[0];
-
-                default xml namespace = NS;
-                function rec(text, level, li) {
-                    XML.ignoreWhitespace = XML.prettyPrinting = false;
-
-                    let res = <></>;
-                    let list, space, i = 0;
-
-                    for (let match in re.iterate(text)) {
-                        if (match.comment)
-                            continue;
-                        else if (match.char) {
-                            if (!list)
-                                res += list = <ul/>;
-                            let li = <li/>;
-                            li.* += rec(match.content.replace(RegExp("^" + match.space, "gm"), ""), level + 1, li);
-                            list.* += li;
-                        }
-                        else if (match.par) {
-                            let [, par, tags] = /([^]*?)\s*((?:\[[^\]]+\])*)\n*$/.exec(match.par);
-                            let t = tags;
-                            tags = array(betas.iterate(tags)).map(function (m) m[1]);
-
-                            let group = !tags.length                       ? "" :
-                                        !tags.some(function (t) t == beta) ? "HelpNewsOld" : "HelpNewsNew";
-                            if (i === 0 && li) {
-                                li.@highlight = group;
-                                group = "";
-                            }
-
-                            list = null;
-                            if (level == 0 && /^.*:\n$/.test(match.par)) {
-                                let text = par.slice(0, -1);
-                                res += <h2 tag={"news-" + text}>{template.linkifyHelp(text, true)}</h2>;
-                            }
-                            else {
-                                let [, a, b] = /^(IMPORTANT:?)?([^]*)/.exec(par);
-                                res += <p highlight={group + " HelpNews"}>{
-                                    !tags.length ? "" :
-                                    <hl key="HelpNewsTag">{tags.join(" ")}</hl>
-                                }{
-                                    a ? <hl key="HelpWarning">{a}</hl> : ""
-                                }{
-                                    template.linkifyHelp(b, true)
-                                }</p>;
-                            }
-                        }
-                        i++;
-                    }
-                    for each (let attr in res..@highlight) {
-                        attr.parent().@NS::highlight = attr;
-                        delete attr.parent().@highlight;
-                    }
-                    return res;
-                }
-
-                XML.ignoreWhitespace = XML.prettyPrinting = false;
-                let body = rec(NEWS, 0);
-                for each (let li in body..li) {
-                    let list = li..li.(@NS::highlight == "HelpNewsOld");
-                    if (list.length() && list.length() == li..li.(@NS::highlight != "").length()) {
-                        for each (let li in list)
-                            li.@NS::highlight = "";
-                        li.@NS::highlight = "HelpNewsOld";
-                    }
-                }
-
-                return ["application/xml",
-                    '<?xml version="1.0"?>\n' +
-                    '<?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>\n' +
-                    '<!DOCTYPE document SYSTEM "resource://dactyl-content/dactyl.dtd">\n' +
-                    <document xmlns={NS} xmlns:dactyl={NS}
-                        name="versions" title={config.appName + " Versions"}>
-                        <h1 tag="versions news NEWS">{config.appName} Versions</h1>
-                        <toc start="2"/>
-
-                        {body}
-                    </document>.toXMLString()
-                ];
-            }
-            addTags("versions", util.httpGet("dactyl://help/versions").responseXML);
-            addTags("plugins", util.httpGet("dactyl://help/plugins").responseXML);
-
-            default xml namespace = NS;
-
-            overlayMap["index"] = ['text/xml;charset=UTF-8',
-                '<?xml version="1.0"?>\n' +
-                <overlay xmlns={NS}>{
-                template.map(dactyl.indices, function ([name, iter])
-                    <dl insertafter={name + "-index"}>{
-                        template.map(iter(), util.identity)
-                    }</dl>, <>{"\n\n"}</>)
-                }</overlay>];
-            addTags("index", util.httpGet("dactyl://help-overlay/index").responseXML);
-
-            overlayMap["gui"] = ['text/xml;charset=UTF-8',
-                '<?xml version="1.0"?>\n' +
-                <overlay xmlns={NS}>
-                    <dl insertafter="dialog-list">{
-                    template.map(config.dialogs, function ([name, val])
-                        (!val[2] || val[2]())
-                            ? <><dt>{name}</dt><dd>{val[0]}</dd></>
-                            : undefined,
-                        <>{"\n"}</>)
-                    }</dl>
-                </overlay>];
-
-
-            this.helpInitialized = true;
-        }
+        help.initialize();
     },
 
     stringifyXML: function (xml) {
@@ -908,120 +667,6 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         return UTF8(xml.toXMLString());
     },
 
-    exportHelp: JavaScript.setCompleter(function (path) {
-        const FILE = io.File(path);
-        const PATH = FILE.leafName.replace(/\..*/, "") + "/";
-        const TIME = Date.now();
-
-        if (!FILE.exists() && (/\/$/.test(path) && !/\./.test(FILE.leafName)))
-            FILE.create(FILE.DIRECTORY_TYPE, octal(755));
-
-        dactyl.initHelp();
-        if (FILE.isDirectory()) {
-            var addDataEntry = function addDataEntry(file, data) FILE.child(file).write(data);
-            var addURIEntry  = function addURIEntry(file, uri) addDataEntry(file, util.httpGet(uri).responseText);
-        }
-        else {
-            var zip = services.ZipWriter();
-            zip.open(FILE, File.MODE_CREATE | File.MODE_WRONLY | File.MODE_TRUNCATE);
-
-            addURIEntry = function addURIEntry(file, uri)
-                zip.addEntryChannel(PATH + file, TIME, 9,
-                    services.io.newChannel(uri, null, null), false);
-            addDataEntry = function addDataEntry(file, data) // Unideal to an extreme.
-                addURIEntry(file, "data:text/plain;charset=UTF-8," + encodeURI(data));
-        }
-
-        let empty = Set("area base basefont br col frame hr img input isindex link meta param"
-                            .split(" "));
-        function fix(node) {
-            switch(node.nodeType) {
-            case Node.ELEMENT_NODE:
-                if (isinstance(node, [HTMLBaseElement]))
-                    return;
-
-                data.push("<"); data.push(node.localName);
-                if (node instanceof HTMLHtmlElement)
-                    data.push(" xmlns=" + XHTML.uri.quote(),
-                              " xmlns:dactyl=" + NS.uri.quote());
-
-                for (let { name, value } in array.iterValues(node.attributes)) {
-                    if (name == "dactyl:highlight") {
-                        Set.add(styles, value);
-                        name = "class";
-                        value = "hl-" + value;
-                    }
-                    if (name == "href") {
-                        value = node.href || value;
-                        if (value.indexOf("dactyl://help-tag/") == 0) {
-                            let uri = services.io.newChannel(value, null, null).originalURI;
-                            value = uri.spec == value ? "javascript:;" : uri.path.substr(1);
-                        }
-                        if (!/^#|[\/](#|$)|^[a-z]+:/.test(value))
-                            value = value.replace(/(#|$)/, ".xhtml$1");
-                    }
-                    if (name == "src" && value.indexOf(":") > 0) {
-                        chromeFiles[value] = value.replace(/.*\//, "");
-                        value = value.replace(/.*\//, "");
-                    }
-
-                    data.push(" ", name, '="',
-                              <>{value}</>.toXMLString().replace(/"/g, "&quot;"),
-                              '"');
-                }
-                if (node.localName in empty)
-                    data.push(" />");
-                else {
-                    data.push(">");
-                    if (node instanceof HTMLHeadElement)
-                        data.push(<link rel="stylesheet" type="text/css" href="help.css"/>.toXMLString());
-                    Array.map(node.childNodes, fix);
-                    data.push("</", node.localName, ">");
-                }
-                break;
-            case Node.TEXT_NODE:
-                data.push(<>{node.textContent}</>.toXMLString());
-            }
-        }
-
-        let chromeFiles = {};
-        let styles = {};
-        for (let [file, ] in Iterator(services["dactyl:"].FILE_MAP)) {
-            let url = "dactyl://help/" + file;
-            dactyl.open(url);
-            util.waitFor(function () content.location.href == url && buffer.loaded
-                            && content.document.documentElement instanceof HTMLHtmlElement,
-                         15000);
-            events.waitForPageLoad();
-            var data = [
-                '<?xml version="1.0" encoding="UTF-8"?>\n',
-                '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"\n',
-                '          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'
-            ];
-            fix(content.document.documentElement);
-            addDataEntry(file + ".xhtml", data.join(""));
-        }
-
-        let data = [h for (h in highlight) if (Set.has(styles, h.class) || /^Help/.test(h.class))]
-            .map(function (h) h.selector
-                               .replace(/^\[.*?=(.*?)\]/, ".hl-$1")
-                               .replace(/html\|/g, "") + "\t" + "{" + h.cssText + "}")
-            .join("\n");
-        addDataEntry("help.css", data.replace(/chrome:[^ ")]+\//g, ""));
-
-        addDataEntry("tag-map.json", JSON.stringify(services["dactyl:"].HELP_TAGS));
-
-        let m, re = /(chrome:[^ ");]+\/)([^ ");]+)/g;
-        while ((m = re.exec(data)))
-            chromeFiles[m[0]] = m[2];
-
-        for (let [uri, leaf] in Iterator(chromeFiles))
-            addURIEntry(leaf, uri);
-
-        if (zip)
-            zip.close();
-    }, [function (context, args) completion.file(context)]),
-
     /**
      * Generates a help entry and returns it as a string.
      *
@@ -1040,6 +685,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         if (obj instanceof Command) {
             link = function (cmd) <ex>{cmd}</ex>;
             args = obj.parseArgs("", CompletionContext(str || ""));
+            tag  = function (cmd) <>:{cmd}</>;
             spec = function (cmd) <>{
                     obj.count ? <oa>count</oa> : <></>
                 }{
@@ -1050,6 +696,9 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         }
         else if (obj instanceof Map) {
             spec = function (map) obj.count ? <><oa>count</oa>{map}</> : <>{map}</>;
+            tag = function (map) <>{
+                    let (c = obj.modes[0].char) c ? c + "_" : ""
+                }{ map }</>;
             link = function (map) {
                 let [, mode, name, extra] = /^(?:(.)_)?(?:<([^>]+)>)?(.*)$/.exec(map);
                 let k = <k>{extra}</k>;
@@ -1061,7 +710,8 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             };
         }
         else if (obj instanceof Option) {
-            tag = spec = function (name) <>'{name}'</>;
+            spec = function () template.map(obj.names, tag, " ");
+            tag = function (name) <>'{name}'</>;
             link = function (opt, name) <o>{name}</o>;
             args = { value: "", values: [] };
         }
@@ -1075,7 +725,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                     </>;
 
         let res = <res>
-                <dt>{link(obj.helpTag || obj.name, obj.name)}</dt> <dd>{
+                <dt>{link(obj.helpTag || tag(obj.name), obj.name)}</dt> <dd>{
                     template.linkifyHelp(obj.description ? obj.description.replace(/\.$/, "") : "", true)
                 }</dd></res>;
         if (specOnly)
@@ -1084,10 +734,11 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         res.* += <>
             <item>
                 <tags>{template.map(obj.names.slice().reverse(), tag, " ")}</tags>
-                <spec>{
-                    spec(template.highlightRegexp((obj.specs || obj.names)[0],
-                                                  /\[(.*?)\]/g,
-                                                  function (m, n0) <oa>{n0}</oa>))
+                <spec>{let (name = (obj.specs || obj.names)[0])
+                          spec(template.highlightRegexp(tag(name),
+                               /\[(.*?)\]/g,
+                               function (m, n0) <oa>{n0}</oa>),
+                               name)
                 }</spec>{
                 !obj.type ? "" : <>
                 <type>{obj.type}</type>
@@ -1128,30 +779,6 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                   .replace(/^\s*\n|\n\s*$/g, "") + "\n";
     },
 
-    /**
-     * Opens the help page containing the specified *topic* if it exists.
-     *
-     * @param {string} topic The help topic to open.
-     * @param {boolean} consolidated Whether to use the consolidated help page.
-     */
-    help: function (topic, consolidated) {
-        dactyl.initHelp();
-        if (!topic) {
-            let helpFile = consolidated ? "all" : options["helpfile"];
-
-            if (helpFile in services["dactyl:"].FILE_MAP)
-                dactyl.open("dactyl://help/" + helpFile, { from: "help" });
-            else
-                dactyl.echomsg(_("help.noFile", helpFile.quote()));
-            return;
-        }
-
-        let page = this.findHelp(topic, consolidated);
-        dactyl.assert(page != null, _("help.noTopic", topic));
-
-        dactyl.open("dactyl://help/" + page, { from: "help" });
-    },
-
     /**
      * The map of global variables.
      *
@@ -1173,7 +800,9 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                 loadplugins = { __proto__: loadplugins, value: args.map(Option.parseRegexp) };
 
             dir.readDirectory(true).forEach(function (file) {
-                if (file.isFile() && loadplugins.getKey(file.path)
+                if (file.leafName[0] == ".")
+                    ;
+                else if (file.isFile() && loadplugins.getKey(file.path)
                         && !(!force && file.path in dactyl.pluginFiles && dactyl.pluginFiles[file.path] >= file.lastModifiedTime)) {
                     try {
                         io.source(file.path);
@@ -1218,7 +847,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
      * @param {number} level The logging level 0 - 15.
      */
     log: function (msg, level) {
-        let verbose = localPrefs.get("loglevel", 0);
+        let verbose = config.prefs.get("loglevel", 0);
 
         if (!level || level <= verbose) {
             if (isObject(msg) && !isinstance(msg, _))
@@ -1228,35 +857,41 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         }
     },
 
-    onClick: function onClick(event) {
-        if (event.originalTarget instanceof Element) {
-            let command = event.originalTarget.getAttributeNS(NS, "command");
-            if (command && event.button == 0) {
-                event.preventDefault();
+    events: {
+        click: function onClick(event) {
+            let elem = event.originalTarget;
 
-                if (dactyl.commands[command])
-                    dactyl.withSavedValues(["forceNewTab"], function () {
-                        dactyl.forceNewTab = event.ctrlKey || event.shiftKey || event.button == 1;
-                        dactyl.commands[command](event);
-                    });
+            if (elem instanceof Element && services.security.isSystemPrincipal(elem.nodePrincipal)) {
+                let command = elem.getAttributeNS(NS, "command");
+                if (command && event.button == 0) {
+                    event.preventDefault();
+
+                    if (dactyl.commands[command])
+                        dactyl.withSavedValues(["forceTarget"], function () {
+                            if (event.ctrlKey || event.shiftKey || event.button == 1)
+                                dactyl.forceTarget = dactyl.NEW_TAB;
+                            dactyl.commands[command](event);
+                        });
+                }
             }
-        }
-    },
+        },
 
-    onExecute: function onExecute(event) {
-        let cmd = event.originalTarget.getAttribute("dactyl-execute");
-        commands.execute(cmd, null, false, null,
-                         { file: /*L*/"[Command Line]", line: 1 });
+        "dactyl.execute": function onExecute(event) {
+            let cmd = event.originalTarget.getAttribute("dactyl-execute");
+            commands.execute(cmd, null, false, null,
+                             { file: /*L*/"[Command Line]", line: 1 });
+        }
     },
 
     /**
      * Opens one or more URLs. Returns true when load was initiated, or
      * false on error.
      *
-     * @param {string|Array} urls A representation of the URLs to open. May be
+     * @param {string|object|Array} urls A representation of the URLs to open. May be
      *     either a string, which will be passed to
-     *     {@see Dactyl#parseURLs}, or an array in the same format as
-     *     would be returned by the same.
+     *     {@link Dactyl#parseURLs}, an array in the same format as
+     *     would be returned by the same, or an object as returned by
+     *     {@link DOM#formData}.
      * @param {object} params A set of parameters specifying how to open the
      *     URLs. The following properties are recognized:
      *
@@ -1297,8 +932,9 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             flags |= params[opt] && Ci.nsIWebNavigation["LOAD_FLAGS_" + flag];
 
         let where = params.where || dactyl.CURRENT_TAB;
-        let background = ("background" in params) ? params.background
-                                                  : params.where == dactyl.NEW_BACKGROUND_TAB;
+        let background = dactyl.forceBackground != null ? dactyl.forceBackground :
+                         ("background" in params)       ? params.background
+                                                        : params.where == dactyl.NEW_BACKGROUND_TAB;
 
         if (params.from && dactyl.has("tabs")) {
             if (!params.where && options.get("newtab").has(params.from))
@@ -1310,21 +946,23 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             return;
 
         let browser = config.tabbrowser;
-        function open(urls, where) {
+        function open(loc, where) {
             try {
-                let url = Array.concat(urls)[0];
-                let postdata = Array.concat(urls)[1];
+                if (isArray(loc))
+                    loc = { url: loc[0], postData: loc[1] };
+                else if (isString(loc))
+                    loc = { url: loc };
 
                 // decide where to load the first url
                 switch (where) {
 
                 case dactyl.NEW_TAB:
                     if (!dactyl.has("tabs"))
-                        return open(urls, dactyl.NEW_WINDOW);
+                        return open(loc, dactyl.NEW_WINDOW);
 
                     return prefs.withContext(function () {
                         prefs.set("browser.tabs.loadInBackground", true);
-                        return browser.loadOneTab(url, null, null, postdata, background).linkedBrowser.contentDocument;
+                        return browser.loadOneTab(loc.url, null, null, loc.postData, background).linkedBrowser.contentDocument;
                     });
 
                 case dactyl.NEW_WINDOW:
@@ -1333,7 +971,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                     browser = win.dactyl && win.dactyl.modules.config.tabbrowser || win.getBrowser();
                     // FALLTHROUGH
                 case dactyl.CURRENT_TAB:
-                    browser.loadURIWithFlags(url, flags, null, null, postdata);
+                    browser.loadURIWithFlags(loc.url, flags, null, null, loc.postData);
                     return browser.contentWindow;
                 }
             }
@@ -1343,10 +981,8 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             // any genuine errors go unreported.
         }
 
-        if (dactyl.forceNewTab)
-            where = dactyl.NEW_TAB;
-        else if (dactyl.forceNewWindow)
-            where = dactyl.NEW_WINDOW;
+        if (dactyl.forceTarget)
+            where = dactyl.forceTarget;
         else if (!where)
             where = dactyl.CURRENT_TAB;
 
@@ -1378,7 +1014,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         return urls.map(function (url) {
             url = url.trim();
 
-            if (/^(\.{0,2}|~)(\/|$)/.test(url) || util.OS.isWindows && /^[a-z]:/i.test(url)) {
+            if (/^(\.{0,2}|~)(\/|$)/.test(url) || config.OS.isWindows && /^[a-z]:/i.test(url)) {
                 try {
                     // Try to find a matching file.
                     let file = io.File(url);
@@ -1390,7 +1026,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
 
             // If it starts with a valid protocol, pass it through.
             let proto = /^([-\w]+):/.exec(url);
-            if (proto && "@mozilla.org/network/protocol;1?name=" + proto[1] in Cc)
+            if (proto && services.PROTOCOL + proto[1] in Cc)
                 return url;
 
             // Check for a matching search keyword.
@@ -1408,7 +1044,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         }, this);
     },
     stringToURLArray: deprecated("dactyl.parseURLs", "parseURLs"),
-    urlish: Class.memoize(function () util.regexp(<![CDATA[
+    urlish: Class.Memoize(function () util.regexp(<![CDATA[
             ^ (
                 <domain>+ (:\d+)? (/ .*) |
                 <domain>+ (:\d+) |
@@ -1470,10 +1106,12 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
     /**
      * Restart the host application.
      */
-    restart: function () {
+    restart: function (args) {
         if (!this.confirmQuit())
             return;
 
+        config.prefs.set("commandline-args", args);
+
         services.appStartup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
     },
 
@@ -1492,7 +1130,12 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             return func.apply(self || this, Array.slice(arguments, 2));
         }
         catch (e) {
-            dactyl.reportError(e, true);
+            try {
+                dactyl.reportError(e, true);
+            }
+            catch (e) {
+                util.reportError(e);
+            }
             return e;
         }
     },
@@ -1507,7 +1150,8 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         if (error instanceof FailedAssertion && error.noTrace || error.message === "Interrupted") {
             let context = contexts.context;
             let prefix = context ? context.file + ":" + context.line + ": " : "";
-            if (error.message && error.message.indexOf(prefix) !== 0)
+            if (error.message && error.message.indexOf(prefix) !== 0 &&
+                    prefix != "[Command Line]:1: ")
                 error.message = prefix + error.message;
 
             if (error.message)
@@ -1519,8 +1163,10 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                 util.reportError(error);
             return;
         }
+
         if (error.result == Cr.NS_BINDING_ABORTED)
             return;
+
         if (echo)
             dactyl.echoerr(error, commandline.FORCE_SINGLELINE);
         else
@@ -1545,10 +1191,9 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             return [];
         }
     },
-
     wrapCallback: function (callback, self) {
         self = self || this;
-        let save = ["forceNewTab", "forceNewWindow"];
+        let save = ["forceOpen"];
         let saved = save.map(function (p) dactyl[p]);
         return function wrappedCallback() {
             let args = arguments;
@@ -1573,9 +1218,90 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
 }, {
     toolbarHidden: function hidden(elem) (elem.getAttribute("autohide") || elem.getAttribute("collapsed")) == "true"
 }, {
+    cache: function () {
+        cache.register("help/plugins.xml", function () {
+            // Process plugin help entries.
+            XML.ignoreWhiteSpace = XML.prettyPrinting = false;
+
+            let body = XML();
+            for (let [, context] in Iterator(plugins.contexts))
+                try {
+                    let info = contexts.getDocs(context);
+                    if (info instanceof XML) {
+                        if (info.*.@lang.length()) {
+                            let lang = config.bestLocale(String(a) for each (a in info.*.@lang));
+
+                            info.* = info.*.(function::attribute("lang").length() == 0 || @lang == lang);
+
+                            for each (let elem in info.NS::info)
+                                for (let attr in values(["@name", "@summary", "@href"]))
+                                    if (elem[attr].length())
+                                        info[attr] = elem[attr];
+                        }
+                        body += <h2 xmlns={NS.uri} tag={info.@name + '-plugin'}>{info.@summary}</h2> +
+                            info;
+                    }
+                }
+                catch (e) {
+                    util.reportError(e);
+                }
+
+            return '<?xml version="1.0"?>\n' +
+                   '<?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>\n' +
+                   '<!DOCTYPE document SYSTEM "resource://dactyl-content/dactyl.dtd">\n' +
+                   <document xmlns={NS}
+                       name="plugins" title={config.appName + " Plugins"}>
+                       <h1 tag="using-plugins">{_("help.title.Using Plugins")}</h1>
+                       <toc start="2"/>
+
+                       {body}
+                   </document>.toXMLString();
+        });
+
+        cache.register("help/index.xml", function () {
+            default xml namespace = NS;
+
+            return '<?xml version="1.0"?>\n' +
+                   <overlay xmlns={NS}>{
+                   template.map(dactyl.indices, function ([name, iter])
+                       <dl insertafter={name + "-index"}>{
+                           template.map(iter(), util.identity)
+                       }</dl>, <>{"\n\n"}</>)
+                   }</overlay>;
+        });
+
+        cache.register("help/gui.xml", function () {
+            default xml namespace = NS;
+
+            return '<?xml version="1.0"?>\n' +
+                   <overlay xmlns={NS}>
+                       <dl insertafter="dialog-list">{
+                       template.map(config.dialogs, function ([name, val])
+                           (!val[2] || val[2]())
+                               ? <><dt>{name}</dt><dd>{val[0]}</dd></>
+                               : undefined,
+                           <>{"\n"}</>)
+                       }</dl>
+                   </overlay>;
+        });
+
+        cache.register("help/privacy.xml", function () {
+            default xml namespace = NS;
+
+            return '<?xml version="1.0"?>\n' +
+                   <overlay xmlns={NS}>
+                       <dl insertafter="sanitize-items">{
+                       template.map(options.get("sanitizeitems").values
+                           .sort(function (a, b) String.localeCompare(a.name, b.name)),
+                           function ({ name, description })
+                           <><dt>{name}</dt><dd>{template.linkifyHelp(description, true)}</dd></>,
+                           <>{"\n"}</>)
+                       }</dl>
+                   </overlay>;
+        });
+    },
     events: function () {
-        events.listen(window, "click", dactyl.closure.onClick, true);
-        events.listen(window, "dactyl.execute", dactyl.closure.onExecute, true);
+        events.listen(window, dactyl, "events", true);
     },
     // Only general options are added here, which are valid for all Dactyl extensions
     options: function () {
@@ -1671,12 +1397,12 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
 
         options.add(["guioptions", "go"],
             "Show or hide certain GUI elements like the menu or toolbar",
-            "charlist", config.defaults.guioptions || "", {
+            "charlist", "", {
 
                 // FIXME: cleanup
                 cleanupValue: config.cleanups.guioptions ||
-                    "r" + [k for ([k, v] in iter(groups[1].opts))
-                           if (!Dactyl.toolbarHidden(document.getElementById(v[1][0])))].join(""),
+                    "rb" + [k for ([k, v] in iter(groups[1].opts))
+                            if (!Dactyl.toolbarHidden(document.getElementById(v[1][0])))].join(""),
 
                 values: array(groups).map(function (g) [[k, v[0]] for ([k, v] in Iterator(g.opts))]).flatten(),
 
@@ -1700,12 +1426,15 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
 
         options.add(["titlestring"],
             "The string shown at the end of the window title",
-            "string", config.defaults.titlestring || config.host,
+            "string", config.host,
             {
                 setter: function (value) {
                     let win = document.documentElement;
                     function updateTitle(old, current) {
-                        document.title = document.title.replace(RegExp("(.*)" + util.regexp.escape(old)), "$1" + current);
+                        if (config.browser.updateTitlebar)
+                            config.browser.updateTitlebar();
+                        else
+                            document.title = document.title.replace(RegExp("(.*)" + util.regexp.escape(old)), "$1" + current);
                     }
 
                     if (services.has("privateBrowsing")) {
@@ -1744,21 +1473,13 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             {
                 setter: function (value) {
                     prefs.safeSet("accessibility.typeaheadfind.enablesound", !value,
-                                  _("option.visualbell.safeSet"));
+                                  _("option.safeSet", "visualbell"));
                     return value;
                 }
             });
     },
 
     mappings: function () {
-        mappings.add([modes.MAIN], ["<open-help>", "<F1>"],
-            "Open the introductory help page",
-            function () { dactyl.help(); });
-
-        mappings.add([modes.MAIN], ["<open-single-help>", "<A-F1>"],
-            "Open the single, consolidated help page",
-            function () { ex.helpall(); });
-
         if (dactyl.has("session"))
             mappings.add([modes.NORMAL], ["ZQ"],
                 "Quit and don't save the session",
@@ -1797,7 +1518,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             "Execute the specified menu item from the command line",
             function (args) {
                 let arg = args[0] || "";
-                let items = dactyl.menuItems;
+                let items = dactyl.getMenuItems(arg);
 
                 dactyl.assert(items.some(function (i) i.dactylPath == arg),
                               _("emenu.notFound", arg));
@@ -1829,30 +1550,6 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                 literal: 0
             });
 
-        [
-            {
-                name: "h[elp]",
-                description: "Open the introductory help page"
-            }, {
-                name: "helpa[ll]",
-                description: "Open the single consolidated help page"
-            }
-        ].forEach(function (command) {
-            let consolidated = command.name == "helpa[ll]";
-
-            commands.add([command.name],
-                command.description,
-                function (args) {
-                    dactyl.assert(!args.bang, _("help.dontPanic"));
-                    dactyl.help(args.literalArg, consolidated);
-                }, {
-                    argCount: "?",
-                    bang: true,
-                    completer: function (context) completion.help(context, consolidated),
-                    literal: 0
-                });
-        });
-
         commands.add(["loadplugins", "lpl"],
             "Load all or matching plugins",
             function (args) {
@@ -1903,47 +1600,66 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                 bang: true
             });
 
+        let startupOptions = [
+            {
+                names: ["+u"],
+                description: "The initialization file to execute at startup",
+                type: CommandOption.STRING
+            },
+            {
+                names: ["++noplugin"],
+                description: "Do not automatically load plugins"
+            },
+            {
+                names: ["++cmd"],
+                description: "Ex commands to execute prior to initialization",
+                type: CommandOption.STRING,
+                multiple: true
+            },
+            {
+                names: ["+c"],
+                description: "Ex commands to execute after initialization",
+                type: CommandOption.STRING,
+                multiple: true
+            },
+            {
+                names: ["+purgecaches"],
+                description: "Purge " + config.appName + " caches at startup",
+                type: CommandOption.NOARG
+            }
+        ];
+
         commands.add(["reh[ash]"],
             "Reload the " + config.appName + " add-on",
             function (args) {
                 if (args.trailing)
                     storage.session.rehashCmd = args.trailing; // Hack.
                 args.break = true;
+
+                if (args["+purgecaches"])
+                    cache.flush();
+
                 util.rehash(args);
             },
             {
                 argCount: "0", // FIXME
-                options: [
-                    {
-                        names: ["+u"],
-                        description: "The initialization file to execute at startup",
-                        type: CommandOption.STRING
-                    },
-                    {
-                        names: ["++noplugin"],
-                        description: "Do not automatically load plugins"
-                    },
-                    {
-                        names: ["++cmd"],
-                        description: "Ex commands to execute prior to initialization",
-                        type: CommandOption.STRING,
-                        multiple: true
-                    },
-                    {
-                        names: ["+c"],
-                        description: "Ex commands to execute after initialization",
-                        type: CommandOption.STRING,
-                        multiple: true
-                    }
-                ]
+                options: startupOptions
             });
 
         commands.add(["res[tart]"],
-            "Force " + config.appName + " to restart",
-            function () { dactyl.restart(); },
-            { argCount: "0" });
+            "Force " + config.host + " to restart",
+            function (args) {
+                if (args["+purgecaches"])
+                    cache.flush();
 
-        function findToolbar(name) util.evaluateXPath(
+                dactyl.restart(args.string);
+            },
+            {
+                argCount: "0",
+                options: startupOptions
+            });
+
+        function findToolbar(name) DOM.XPath(
             "//*[@toolbarname=" + util.escapeString(name, "'") + " or " +
                 "@toolbarname=" + util.escapeString(name.trim(), "'") + "]",
             document).snapshotItem(0);
@@ -2092,10 +1808,14 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             function (args) {
                 if (args.bang)
                     dactyl.open("about:");
-                else
-                    commandline.commandOutput(<>
-                        {config.appName} {config.version} running on:<br/>{navigator.userAgent}
-                    </>);
+                else {
+                    let date = config.buildDate;
+                    date = date ? " (" + date + ")" : "";
+
+                    commandline.commandOutput(
+                        <div>{config.appName} {config.version}{date} running on: </div> +
+                        <div>{navigator.userAgent}</div>)
+                }
             }, {
                 argCount: "0",
                 bang: true
@@ -2110,15 +1830,6 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             context.completions = [[k, v[0], v[2]] for ([k, v] in Iterator(config.dialogs))];
         };
 
-        completion.help = function help(context, consolidated) {
-            dactyl.initHelp();
-            context.title = ["Help"];
-            context.anchored = false;
-            context.completions = services["dactyl:"].HELP_TAGS;
-            if (consolidated)
-                context.keys = { text: 0, description: function () "all" };
-        };
-
         completion.menuItem = function menuItem(context) {
             context.title = ["Menu Path", "Label"];
             context.anchored = false;
@@ -2134,7 +1845,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         completion.toolbar = function toolbar(context) {
             context.title = ["Toolbar"];
             context.keys = { text: function (item) item.getAttribute("toolbarname"), description: function () "" };
-            context.completions = util.evaluateXPath("//*[@toolbarname]", document);
+            context.completions = DOM.XPath("//*[@toolbarname]", document);
         };
 
         completion.window = function window(context) {
@@ -2148,9 +1859,17 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
 
         dactyl.log(_("dactyl.modulesLoaded"), 3);
 
+        userContext.DOM = Class("DOM", DOM, { init: function DOM_(sel, ctxt) DOM(sel, ctxt || buffer.focusedFrame.document) });
+        userContext.$ = modules.userContext.DOM;
+
         dactyl.timeout(function () {
             try {
-                var args = storage.session.commandlineArgs || services.commandLineHandler.optionValue;
+                var args = config.prefs.get("commandline-args")
+                        || storage.session.commandlineArgs
+                        || services.commandLineHandler.optionValue;
+
+                config.prefs.reset("commandline-args");
+
                 if (isString(args))
                     args = dactyl.parseCommandLine(args);
 
@@ -2168,17 +1887,14 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
 
             dactyl.log(_("dactyl.commandlineOpts", util.objectToString(dactyl.commandLineOptions)), 3);
 
-            // first time intro message
-            const firstTime = "extensions." + config.name + ".firsttime";
-            if (prefs.get(firstTime, true)) {
+            if (config.prefs.get("first-run", true))
                 dactyl.timeout(function () {
-                    this.withSavedValues(["forceNewTab"], function () {
-                        this.forceNewTab = true;
-                        this.help();
-                        prefs.set(firstTime, false);
+                    config.prefs.set("first-run", false);
+                    this.withSavedValues(["forceTarget"], function () {
+                        this.forceTarget = dactyl.NEW_TAB;
+                        help.help();
                     });
                 }, 1000);
-            }
 
             // TODO: we should have some class where all this guioptions stuff fits well
             // dactyl.hideGUI();