]> git.donarmstrong.com Git - dactyl.git/blob - common/content/dactyl.js
Import 1.0b7.1 supporting Firefox up to 8.*
[dactyl.git] / common / content / dactyl.js
1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 "use strict";
8
9 /** @scope modules */
10
11 default xml namespace = XHTML;
12 XML.ignoreWhitespace = false;
13 XML.prettyPrinting = false;
14
15 var EVAL_ERROR = "__dactyl_eval_error";
16 var EVAL_RESULT = "__dactyl_eval_result";
17 var EVAL_STRING = "__dactyl_eval_string";
18
19 var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
20     init: function () {
21         window.dactyl = this;
22         // cheap attempt at compatibility
23         let prop = { get: deprecated("dactyl", function liberator() dactyl) };
24         Object.defineProperty(window, "liberator", prop);
25         Object.defineProperty(modules, "liberator", prop);
26         this.commands = {};
27         this.indices = {};
28         this.modules = modules;
29         this._observers = {};
30         util.addObserver(this);
31
32         this.commands["dactyl.help"] = function (event) {
33             let elem = event.originalTarget;
34             dactyl.help(elem.getAttribute("tag") || elem.textContent);
35         };
36         this.commands["dactyl.restart"] = function (event) {
37             dactyl.restart();
38         };
39
40         styles.registerSheet("resource://dactyl-skin/dactyl.css");
41
42         this.cleanups = [];
43         this.cleanups.push(util.overlayObject(window, {
44             focusAndSelectUrlBar: function focusAndSelectUrlBar() {
45                 switch (options.get("strictfocus").getKey(document.documentURIObject || util.newURI(document.documentURI), "moderate")) {
46                 case "laissez-faire":
47                     if (!Events.isHidden(window.gURLBar, true))
48                         return focusAndSelectUrlBar.superapply(this, arguments);
49                 default:
50                     // Evil. Ignore.
51                 }
52             }
53         }));
54     },
55
56     cleanup: function () {
57         for (let cleanup in values(this.cleanups))
58             cleanup.call(this);
59
60         delete window.dactyl;
61         delete window.liberator;
62
63         styles.unregisterSheet("resource://dactyl-skin/dactyl.css");
64     },
65
66     destroy: function () {
67         autocommands.trigger("LeavePre", {});
68         dactyl.triggerObserver("shutdown", null);
69         util.dump("All dactyl modules destroyed\n");
70         autocommands.trigger("Leave", {});
71     },
72
73     // initially hide all GUI elements, they are later restored unless the user
74     // has :set go= or something similar in his config
75     hideGUI: function () {
76         let guioptions = config.guioptions;
77         for (let option in guioptions) {
78             guioptions[option].forEach(function (elem) {
79                 try {
80                     document.getElementById(elem).collapsed = true;
81                 }
82                 catch (e) {}
83             });
84         }
85     },
86
87
88     observers: {
89         "dactyl-cleanup": function dactyl_cleanup(subject, reason) {
90             let modules = dactyl.modules;
91
92             for (let mod in values(modules.moduleList.reverse())) {
93                 mod.stale = true;
94                 if ("cleanup" in mod)
95                     this.trapErrors("cleanup", mod, reason);
96                 if ("destroy" in mod)
97                     this.trapErrors("destroy", mod, reason);
98             }
99
100             for (let mod in values(modules.ownPropertyValues.reverse()))
101                 if (mod instanceof Class && "INIT" in mod && "cleanup" in mod.INIT)
102                     this.trapErrors(mod.cleanup, mod, dactyl, modules, window, reason);
103
104             for (let name in values(Object.getOwnPropertyNames(modules).reverse()))
105                 try {
106                     delete modules[name];
107                 }
108                 catch (e) {}
109             modules.__proto__ = {};
110         }
111     },
112
113     /** @property {string} The name of the current user profile. */
114     profileName: Class.memoize(function () {
115         // NOTE: services.profile.selectedProfile.name doesn't return
116         // what you might expect. It returns the last _actively_ selected
117         // profile (i.e. via the Profile Manager or -P option) rather than the
118         // current profile. These will differ if the current process was run
119         // without explicitly selecting a profile.
120
121         let dir = services.directory.get("ProfD", Ci.nsIFile);
122         for (let prof in iter(services.profile.profiles))
123             if (prof.QueryInterface(Ci.nsIToolkitProfile).rootDir.path === dir.path)
124                 return prof.name;
125         return "unknown";
126     }),
127
128     /**
129      * @property {Modes.Mode} The current main mode.
130      * @see modes#mainModes
131      */
132     mode: deprecated("modes.main", {
133         get: function mode() modes.main,
134         set: function mode(val) modes.main = val
135     }),
136
137     get menuItems() {
138         function dispatch(node, name) {
139             let event = node.ownerDocument.createEvent("Events");
140             event.initEvent(name, false, false);
141             node.dispatchEvent(event);
142         }
143
144         function addChildren(node, parent) {
145             if (~["menu", "menupopup"].indexOf(node.localName) && node.children.length)
146                 dispatch(node, "popupshowing");
147
148             for (let [, item] in Iterator(node.childNodes)) {
149                 if (item.childNodes.length == 0 && item.localName == "menuitem"
150                     && !item.hidden
151                     && !/rdf:http:/.test(item.getAttribute("label"))) { // FIXME
152                     item.dactylPath = parent + item.getAttribute("label");
153                     items.push(item);
154                 }
155                 else {
156                     let path = parent;
157                     if (item.localName == "menu")
158                         path += item.getAttribute("label") + ".";
159                     addChildren(item, path);
160                 }
161             }
162         }
163
164         let items = [];
165         addChildren(document.getElementById(config.guioptions["m"][1]), "");
166         return items;
167     },
168
169     // Global constants
170     CURRENT_TAB: "here",
171     NEW_TAB: "tab",
172     NEW_BACKGROUND_TAB: "background-tab",
173     NEW_WINDOW: "window",
174
175     forceNewTab: false,
176     forceNewWindow: false,
177
178     version: deprecated("config.version", { get: function version() config.version }),
179
180     /**
181      * @property {Object} The map of command-line options. These are
182      *     specified in the argument to the host application's -{config.name}
183      *     option. E.g. $ firefox -pentadactyl '+u=/tmp/rcfile ++noplugin'
184      *     Supported options:
185      *         +u RCFILE   Use RCFILE instead of .pentadactylrc.
186      *         ++noplugin  Don't load plugins.
187      *     These two can be specified multiple times:
188      *         ++cmd CMD   Execute an Ex command before initialization.
189      *         +c CMD      Execute an Ex command after initialization.
190      */
191     commandLineOptions: {
192         /** @property Whether plugin loading should be prevented. */
193         noPlugins: false,
194         /** @property An RC file to use rather than the default. */
195         rcFile: null,
196         /** @property An Ex command to run before any initialization is performed. */
197         preCommands: null,
198         /** @property An Ex command to run after all initialization has been performed. */
199         postCommands: null
200     },
201
202     registerObserver: function registerObserver(type, callback, weak) {
203         if (!(type in this._observers))
204             this._observers[type] = [];
205         this._observers[type].push(weak ? Cu.getWeakReference(callback) : { get: function () callback });
206     },
207
208     registerObservers: function registerObservers(obj, prop) {
209         for (let [signal, func] in Iterator(obj[prop || "signals"]))
210             this.registerObserver(signal, obj.closure(func), false);
211     },
212
213     unregisterObserver: function unregisterObserver(type, callback) {
214         if (type in this._observers)
215             this._observers[type] = this._observers[type].filter(function (c) c.get() != callback);
216     },
217
218     // TODO: "zoom": if the zoom value of the current buffer changed
219     applyTriggerObserver: function triggerObserver(type, args) {
220         if (type in this._observers)
221             this._observers[type] = this._observers[type].filter(function (callback) {
222                 if (callback.get()) {
223                     try {
224                         try {
225                             callback.get().apply(null, args);
226                         }
227                         catch (e if e.message == "can't wrap XML objects") {
228                             // Horrible kludge.
229                             callback.get().apply(null, [String(args[0])].concat(args.slice(1)));
230                         }
231                     }
232                     catch (e) {
233                         dactyl.reportError(e);
234                     }
235                     return true;
236                 }
237             });
238     },
239
240     triggerObserver: function triggerObserver(type) {
241         return this.applyTriggerObserver(type, Array.slice(arguments, 1));
242     },
243
244     addUsageCommand: function (params) {
245         function keys(item) (item.names || [item.name]).concat(item.description, item.columns || []);
246
247         let name = commands.add(params.name, params.description,
248             function (args) {
249                 let results = array(params.iterate(args))
250                     .sort(function (a, b) String.localeCompare(a.name, b.name));
251
252                 let filters = args.map(function (arg) util.regexp("\\b" + util.regexp.escape(arg) + "\\b", "i"));
253                 if (filters.length)
254                     results = results.filter(function (item) filters.every(function (re) keys(item).some(re.closure.test)));
255
256                 commandline.commandOutput(
257                     template.usage(results, params.format));
258             },
259             {
260                 argCount: "*",
261                 completer: function (context, args) {
262                     context.keys.text = util.identity;
263                     context.keys.description = function () seen[this.text] + /*L*/" matching items";
264                     let seen = {};
265                     context.completions = array(keys(item).join(" ").toLowerCase().split(/[()\s]+/)
266                                                 for (item in params.iterate(args)))
267                         .flatten().filter(function (w) /^\w[\w-_']+$/.test(w))
268                         .map(function (k) {
269                             seen[k] = (seen[k] || 0) + 1;
270                             return k;
271                         }).uniq();
272                 },
273                 options: params.options || []
274             });
275
276         if (params.index)
277             this.indices[params.index] = function () {
278                 let results = array((params.iterateIndex || params.iterate).call(params, commands.get(name).newArgs()))
279                         .array.sort(function (a, b) String.localeCompare(a.name, b.name));
280
281                 let tags = services["dactyl:"].HELP_TAGS;
282                 for (let obj in values(results)) {
283                     let res = dactyl.generateHelp(obj, null, null, true);
284                     if (!Set.has(tags, obj.helpTag))
285                         res[1].@tag = obj.helpTag;
286
287                     yield res;
288                 }
289             };
290     },
291
292     /**
293      * Triggers the application bell to notify the user of an error. The
294      * bell may be either audible or visual depending on the value of the
295      * 'visualbell' option.
296      */
297     beep: function () {
298         this.triggerObserver("beep");
299         if (options["visualbell"]) {
300             let elems = {
301                 bell: document.getElementById("dactyl-bell"),
302                 strut: document.getElementById("dactyl-bell-strut")
303             };
304             XML.ignoreWhitespace = true;
305             if (!elems.bell)
306                 util.overlayWindow(window, {
307                     objects: elems,
308                     prepend: <>
309                         <window id={document.documentElement.id} xmlns={XUL}>
310                             <hbox style="display: none" highlight="Bell" id="dactyl-bell" key="bell"/>
311                         </window>
312                     </>,
313                     append: <>
314                         <window id={document.documentElement.id} xmlns={XUL}>
315                             <hbox style="display: none" highlight="Bell" id="dactyl-bell-strut" key="strut"/>
316                         </window>
317                     </>
318                 }, elems);
319
320             elems.bell.style.height = window.innerHeight + "px";
321             elems.strut.style.marginBottom = -window.innerHeight + "px";
322             elems.strut.style.display = elems.bell.style.display = "";
323
324             util.timeout(function () { elems.strut.style.display = elems.bell.style.display = "none"; }, 20);
325         }
326         else {
327             let soundService = Cc["@mozilla.org/sound;1"].getService(Ci.nsISound);
328             soundService.beep();
329         }
330     },
331
332     /**
333      * Reads a string from the system clipboard.
334      *
335      * This is same as Firefox's readFromClipboard function, but is needed for
336      * apps like Thunderbird which do not provide it.
337      *
338      * @returns {string}
339      */
340     clipboardRead: function clipboardRead(getClipboard) {
341         try {
342             const clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
343             const transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
344
345             transferable.addDataFlavor("text/unicode");
346
347             let source = clipboard[getClipboard || !clipboard.supportsSelectionClipboard() ?
348                                    "kGlobalClipboard" : "kSelectionClipboard"];
349             clipboard.getData(transferable, source);
350
351             let str = {}, len = {};
352             transferable.getTransferData("text/unicode", str, len);
353
354             if (str)
355                 return str.value.QueryInterface(Ci.nsISupportsString)
356                           .data.substr(0, len.value / 2);
357         }
358         catch (e) {}
359         return null;
360     },
361
362     /**
363      * Copies a string to the system clipboard. If *verbose* is specified the
364      * copied string is also echoed to the command line.
365      *
366      * @param {string} str
367      * @param {boolean} verbose
368      */
369     clipboardWrite: function clipboardWrite(str, verbose) {
370         const clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
371         clipboardHelper.copyString(str);
372
373         if (verbose) {
374             let message = { message: _("dactyl.yank", str) };
375             try {
376                 message.domains = [util.newURI(str).host];
377             }
378             catch (e) {};
379             dactyl.echomsg(message);
380         }
381     },
382
383     dump: deprecated("util.dump",
384                      { get: function dump() util.closure.dump }),
385     dumpStack: deprecated("util.dumpStack",
386                           { get: function dumpStack() util.closure.dumpStack }),
387
388     /**
389      * Outputs a plain message to the command line.
390      *
391      * @param {string} str The message to output.
392      * @param {number} flags These control the multi-line message behavior.
393      *     See {@link CommandLine#echo}.
394      */
395     echo: function echo(str, flags) {
396         commandline.echo(str, commandline.HL_NORMAL, flags);
397     },
398
399     /**
400      * Outputs an error message to the command line.
401      *
402      * @param {string} str The message to output.
403      * @param {number} flags These control the multi-line message behavior.
404      *     See {@link CommandLine#echo}.
405      */
406     echoerr: function echoerr(str, flags) {
407         flags |= commandline.APPEND_TO_MESSAGES;
408
409         if (isinstance(str, ["DOMException", "Error", "Exception"]) || isinstance(str, ["XPCWrappedNative_NoHelper"]) && /^\[Exception/.test(str))
410             dactyl.reportError(str);
411         if (isObject(str) && "echoerr" in str)
412             str = str.echoerr;
413         else if (isinstance(str, ["Error", FailedAssertion]) && str.fileName)
414             str = <>{str.fileName.replace(/^.* -> /, "")}: {str.lineNumber}: {str}</>;
415
416         if (options["errorbells"])
417             dactyl.beep();
418
419         commandline.echo(str, commandline.HL_ERRORMSG, flags);
420     },
421
422     /**
423      * Outputs a warning message to the command line.
424      *
425      * @param {string} str The message to output.
426      * @param {number} flags These control the multi-line message behavior.
427      *     See {@link CommandLine#echo}.
428      */
429     warn: function warn(str, flags) {
430         commandline.echo(str, "WarningMsg", flags | commandline.APPEND_TO_MESSAGES);
431     },
432
433     // TODO: add proper level constants
434     /**
435      * Outputs an information message to the command line.
436      *
437      * @param {string} str The message to output.
438      * @param {number} verbosity The messages log level (0 - 15). Only
439      *     messages with verbosity less than or equal to the value of the
440      *     *verbosity* option will be output.
441      * @param {number} flags These control the multi-line message behavior.
442      *     See {@link CommandLine#echo}.
443      */
444     echomsg: function echomsg(str, verbosity, flags) {
445         if (verbosity == null)
446             verbosity = 0; // verbosity level is exclusionary
447
448         if (options["verbose"] >= verbosity)
449             commandline.echo(str, commandline.HL_INFOMSG,
450                              flags | commandline.APPEND_TO_MESSAGES);
451     },
452
453     /**
454      * Loads and executes the script referenced by *uri* in the scope of the
455      * *context* object.
456      *
457      * @param {string} uri The URI of the script to load. Should be a local
458      *     chrome:, file:, or resource: URL.
459      * @param {Object} context The context object into which the script
460      *     should be loaded.
461      */
462     loadScript: function (uri, context) {
463         JSMLoader.loadSubScript(uri, context, File.defaultEncoding);
464     },
465
466     userEval: function (str, context, fileName, lineNumber) {
467         let ctxt;
468         if (jsmodules.__proto__ != window)
469             str = "with (window) { with (modules) { (this.eval || eval)(" + str.quote() + ") } }";
470
471         let info = contexts.context;
472         if (fileName == null)
473             if (info && info.file[0] !== "[")
474                 ({ file: fileName, line: lineNumber, context: ctxt }) = info;
475
476         if (!context && fileName && fileName[0] !== "[")
477             context = ctxt || _userContext;
478
479         if (isinstance(context, ["Sandbox"]))
480             return Cu.evalInSandbox(str, context, "1.8", fileName, lineNumber);
481         else
482             try {
483                 if (!context)
484                     context = userContext || ctxt;
485
486                 context[EVAL_ERROR] = null;
487                 context[EVAL_STRING] = str;
488                 context[EVAL_RESULT] = null;
489                 this.loadScript("resource://dactyl-content/eval.js", context);
490                 if (context[EVAL_ERROR]) {
491                     try {
492                         context[EVAL_ERROR].fileName = info.file;
493                         context[EVAL_ERROR].lineNumber += info.line;
494                     }
495                     catch (e) {}
496                     throw context[EVAL_ERROR];
497                 }
498                 return context[EVAL_RESULT];
499             }
500             finally {
501                 delete context[EVAL_ERROR];
502                 delete context[EVAL_RESULT];
503                 delete context[EVAL_STRING];
504             }
505     },
506
507     /**
508      * Acts like the Function builtin, but the code executes in the
509      * userContext global.
510      */
511     userFunc: function () {
512         return this.userEval(
513             "(function userFunction(" + Array.slice(arguments, 0, -1).join(", ") + ")" +
514             " { " + arguments[arguments.length - 1] + " })");
515     },
516
517     /**
518      * Execute an Ex command string. E.g. ":zoom 300".
519      *
520      * @param {string} str The command to execute.
521      * @param {Object} modifiers Any modifiers to be passed to
522      *     {@link Command#action}.
523      * @param {boolean} silent Whether the command should be echoed on the
524      *     command line.
525      */
526     execute: function (str, modifiers, silent) {
527         // skip comments and blank lines
528         if (/^\s*("|$)/.test(str))
529             return;
530
531         modifiers = modifiers || {};
532
533         if (!silent)
534             commands.lastCommand = str.replace(/^\s*:\s*/, "");
535         let res = true;
536         for (let [command, args] in commands.parseCommands(str.replace(/^'(.*)'$/, "$1"))) {
537             if (command === null)
538                 throw FailedAssertion(_("dactyl.notCommand", config.appName, args.commandString));
539
540             res = res && command.execute(args, modifiers);
541         }
542         return res;
543     },
544
545     focus: function focus(elem, flags) {
546         flags = flags || services.focus.FLAG_BYMOUSE;
547         try {
548             if (elem instanceof Document)
549                 elem = elem.defaultView;
550             if (elem instanceof Element)
551                 services.focus.setFocus(elem, flags);
552             else if (elem instanceof Window)
553                 services.focus.focusedWindow = elem;
554         }
555         catch (e) {
556             util.dump(elem);
557             util.reportError(e);
558         }
559     },
560
561     /**
562      * Focuses the content window.
563      *
564      * @param {boolean} clearFocusedElement Remove focus from any focused
565      *     element.
566      */
567     focusContent: function focusContent(clearFocusedElement) {
568         if (window != services.focus.activeWindow)
569             return;
570
571         let win = document.commandDispatcher.focusedWindow;
572         let elem = config.mainWidget || content;
573
574         // TODO: make more generic
575         try {
576             if (this.has("mail") && !config.isComposeWindow) {
577                 let i = gDBView.selection.currentIndex;
578                 if (i == -1 && gDBView.rowCount >= 0)
579                     i = 0;
580                 gDBView.selection.select(i);
581             }
582             else {
583                 let frame = buffer.focusedFrame;
584                 if (frame && frame.top == content && !Editor.getEditor(frame))
585                     elem = frame;
586             }
587         }
588         catch (e) {}
589
590         if (clearFocusedElement) {
591             if (dactyl.focusedElement)
592                 dactyl.focusedElement.blur();
593             if (win && Editor.getEditor(win)) {
594                 this.withSavedValues(["ignoreFocus"], function _focusContent() {
595                     this.ignoreFocus = true;
596                     if (win.frameElement)
597                         win.frameElement.blur();
598                     // Grr.
599                     if (content.document.activeElement instanceof HTMLIFrameElement)
600                         content.document.activeElement.blur();
601                 });
602             }
603         }
604
605         if (elem instanceof Window && Editor.getEditor(elem))
606             elem = window;
607
608         if (elem && elem != dactyl.focusedElement)
609             dactyl.focus(elem);
610      },
611
612     /** @property {Element} The currently focused element. */
613     get focusedElement() services.focus.getFocusedElementForWindow(window, true, {}),
614     set focusedElement(elem) dactyl.focus(elem),
615
616     /**
617      * Returns whether this Dactyl extension supports *feature*.
618      *
619      * @param {string} feature The feature name.
620      * @returns {boolean}
621      */
622     has: function (feature) Set.has(config.features, feature),
623
624     /**
625      * Returns the URL of the specified help *topic* if it exists.
626      *
627      * @param {string} topic The help topic to look up.
628      * @param {boolean} consolidated Whether to search the consolidated help page.
629      * @returns {string}
630      */
631     findHelp: function (topic, consolidated) {
632         if (!consolidated && topic in services["dactyl:"].FILE_MAP)
633             return topic;
634         let items = completion._runCompleter("help", topic, null, !!consolidated).items;
635         let partialMatch = null;
636
637         function format(item) item.description + "#" + encodeURIComponent(item.text);
638
639         for (let [i, item] in Iterator(items)) {
640             if (item.text == topic)
641                 return format(item);
642             else if (!partialMatch && topic)
643                 partialMatch = item;
644         }
645
646         if (partialMatch)
647             return format(partialMatch);
648         return null;
649     },
650
651     /**
652      * @private
653      */
654     initDocument: function initDocument(doc) {
655         try {
656             if (doc.location.protocol === "dactyl:") {
657                 dactyl.initHelp();
658                 config.styleHelp();
659             }
660         }
661         catch (e) {
662             util.reportError(e);
663         }
664     },
665
666     /**
667      * @private
668      * Initialize the help system.
669      */
670     initHelp: function (force) {
671         // Waits for the add-on to become available, if necessary.
672         config.addon;
673         config.version;
674
675         if (force || !this.helpInitialized) {
676             if ("noscriptOverlay" in window) {
677                 noscriptOverlay.safeAllow("chrome-data:", true, false);
678                 noscriptOverlay.safeAllow("dactyl:", true, false);
679             }
680
681             // Find help and overlay files with the given name.
682             let findHelpFile = function findHelpFile(file) {
683                 let result = [];
684                 for (let [, namespace] in Iterator(namespaces)) {
685                     let url = ["dactyl://", namespace, "/", file, ".xml"].join("");
686                     let res = util.httpGet(url);
687                     if (res) {
688                         if (res.responseXML.documentElement.localName == "document")
689                             fileMap[file] = url;
690                         if (res.responseXML.documentElement.localName == "overlay")
691                             overlayMap[file] = url;
692                         result.push(res.responseXML);
693                     }
694                 }
695                 return result;
696             };
697             // Find the tags in the document.
698             let addTags = function addTags(file, doc) {
699                 for (let elem in util.evaluateXPath("//@tag|//dactyl:tags/text()|//dactyl:tag/text()", doc))
700                     for (let tag in values((elem.value || elem.textContent).split(/\s+/)))
701                         tagMap[tag] = file;
702             };
703
704             let namespaces = ["locale-local", "locale"];
705             services["dactyl:"].init({});
706
707             let tagMap = services["dactyl:"].HELP_TAGS;
708             let fileMap = services["dactyl:"].FILE_MAP;
709             let overlayMap = services["dactyl:"].OVERLAY_MAP;
710
711             // Scrape the list of help files from all.xml
712             // Manually process main and overlay files, since XSLTProcessor and
713             // XMLHttpRequest don't allow access to chrome documents.
714             tagMap["all"] = tagMap["all.xml"] = "all";
715             tagMap["versions"] = tagMap["versions.xml"] = "versions";
716             let files = findHelpFile("all").map(function (doc)
717                     [f.value for (f in util.evaluateXPath("//dactyl:include/@href", doc))]);
718
719             // Scrape the tags from the rest of the help files.
720             array.flatten(files).forEach(function (file) {
721                 tagMap[file + ".xml"] = file;
722                 findHelpFile(file).forEach(function (doc) {
723                     addTags(file, doc);
724                 });
725             });
726
727             // Process plugin help entries.
728             XML.ignoreWhiteSpace = XML.prettyPrinting = false;
729
730             let body = XML();
731             for (let [, context] in Iterator(plugins.contexts))
732                 try {
733                     let info = contexts.getDocs(context);
734                     if (info instanceof XML) {
735                         if (info.*.@lang.length()) {
736                             let lang = config.bestLocale(String(a) for each (a in info.*.@lang));
737
738                             info.* = info.*.(function::attribute("lang").length() == 0 || @lang == lang);
739
740                             for each (let elem in info.NS::info)
741                                 for each (let attr in ["@name", "@summary", "@href"])
742                                     if (elem[attr].length())
743                                         info[attr] = elem[attr];
744                         }
745                         body += <h2 xmlns={NS.uri} tag={info.@name + '-plugin'}>{info.@summary}</h2> +
746                             info;
747                     }
748                 }
749                 catch (e) {
750                     util.reportError(e);
751                 }
752
753             let help =
754                 '<?xml version="1.0"?>\n' +
755                 '<?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>\n' +
756                 '<!DOCTYPE document SYSTEM "resource://dactyl-content/dactyl.dtd">\n' +
757                 <document xmlns={NS}
758                     name="plugins" title={config.appName + " Plugins"}>
759                     <h1 tag="using-plugins">{_("help.title.Using Plugins")}</h1>
760                     <toc start="2"/>
761
762                     {body}
763                 </document>.toXMLString();
764             fileMap["plugins"] = function () ['text/xml;charset=UTF-8', help];
765
766             fileMap["versions"] = function () {
767                 let NEWS = util.httpGet(config.addon.getResourceURI("NEWS").spec,
768                                         { mimeType: "text/plain;charset=UTF-8" })
769                                .responseText;
770
771                 let re = util.regexp(<![CDATA[
772                       ^ (?P<comment> \s* # .*\n)
773
774                     | ^ (?P<space> \s*)
775                         (?P<char>  [-•*+]) \ //
776                       (?P<content> .*\n
777                          (?: \2\ \ .*\n | \s*\n)* )
778
779                     | (?P<par>
780                           (?: ^ [^\S\n]*
781                               (?:[^-•*+\s] | [-•*+]\S)
782                               .*\n
783                           )+
784                       )
785
786                     | (?: ^ [^\S\n]* \n) +
787                 ]]>, "gmxy");
788
789                 let betas = util.regexp(/\[(b\d)\]/, "gx");
790
791                 let beta = array(betas.iterate(NEWS))
792                             .map(function (m) m[1]).uniq().slice(-1)[0];
793
794                 default xml namespace = NS;
795                 function rec(text, level, li) {
796                     XML.ignoreWhitespace = XML.prettyPrinting = false;
797
798                     let res = <></>;
799                     let list, space, i = 0;
800
801                     for (let match in re.iterate(text)) {
802                         if (match.comment)
803                             continue;
804                         else if (match.char) {
805                             if (!list)
806                                 res += list = <ul/>;
807                             let li = <li/>;
808                             li.* += rec(match.content.replace(RegExp("^" + match.space, "gm"), ""), level + 1, li);
809                             list.* += li;
810                         }
811                         else if (match.par) {
812                             let [, par, tags] = /([^]*?)\s*((?:\[[^\]]+\])*)\n*$/.exec(match.par);
813                             let t = tags;
814                             tags = array(betas.iterate(tags)).map(function (m) m[1]);
815
816                             let group = !tags.length                       ? "" :
817                                         !tags.some(function (t) t == beta) ? "HelpNewsOld" : "HelpNewsNew";
818                             if (i === 0 && li) {
819                                 li.@highlight = group;
820                                 group = "";
821                             }
822
823                             list = null;
824                             if (level == 0 && /^.*:\n$/.test(match.par)) {
825                                 let text = par.slice(0, -1);
826                                 res += <h2 tag={"news-" + text}>{template.linkifyHelp(text, true)}</h2>;
827                             }
828                             else {
829                                 let [, a, b] = /^(IMPORTANT:?)?([^]*)/.exec(par);
830                                 res += <p highlight={group + " HelpNews"}>{
831                                     !tags.length ? "" :
832                                     <hl key="HelpNewsTag">{tags.join(" ")}</hl>
833                                 }{
834                                     a ? <hl key="HelpWarning">{a}</hl> : ""
835                                 }{
836                                     template.linkifyHelp(b, true)
837                                 }</p>;
838                             }
839                         }
840                         i++;
841                     }
842                     for each (let attr in res..@highlight) {
843                         attr.parent().@NS::highlight = attr;
844                         delete attr.parent().@highlight;
845                     }
846                     return res;
847                 }
848
849                 XML.ignoreWhitespace = XML.prettyPrinting = false;
850                 let body = rec(NEWS, 0);
851                 for each (let li in body..li) {
852                     let list = li..li.(@NS::highlight == "HelpNewsOld");
853                     if (list.length() && list.length() == li..li.(@NS::highlight != "").length()) {
854                         for each (let li in list)
855                             li.@NS::highlight = "";
856                         li.@NS::highlight = "HelpNewsOld";
857                     }
858                 }
859
860                 return ["application/xml",
861                     '<?xml version="1.0"?>\n' +
862                     '<?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>\n' +
863                     '<!DOCTYPE document SYSTEM "resource://dactyl-content/dactyl.dtd">\n' +
864                     <document xmlns={NS} xmlns:dactyl={NS}
865                         name="versions" title={config.appName + " Versions"}>
866                         <h1 tag="versions news NEWS">{config.appName} Versions</h1>
867                         <toc start="2"/>
868
869                         {body}
870                     </document>.toXMLString()
871                 ];
872             }
873             addTags("versions", util.httpGet("dactyl://help/versions").responseXML);
874             addTags("plugins", util.httpGet("dactyl://help/plugins").responseXML);
875
876             default xml namespace = NS;
877
878             overlayMap["index"] = ['text/xml;charset=UTF-8',
879                 '<?xml version="1.0"?>\n' +
880                 <overlay xmlns={NS}>{
881                 template.map(dactyl.indices, function ([name, iter])
882                     <dl insertafter={name + "-index"}>{
883                         template.map(iter(), util.identity)
884                     }</dl>, <>{"\n\n"}</>)
885                 }</overlay>];
886             addTags("index", util.httpGet("dactyl://help-overlay/index").responseXML);
887
888             overlayMap["gui"] = ['text/xml;charset=UTF-8',
889                 '<?xml version="1.0"?>\n' +
890                 <overlay xmlns={NS}>
891                     <dl insertafter="dialog-list">{
892                     template.map(config.dialogs, function ([name, val])
893                         (!val[2] || val[2]())
894                             ? <><dt>{name}</dt><dd>{val[0]}</dd></>
895                             : undefined,
896                         <>{"\n"}</>)
897                     }</dl>
898                 </overlay>];
899
900
901             this.helpInitialized = true;
902         }
903     },
904
905     stringifyXML: function (xml) {
906         XML.prettyPrinting = false;
907         XML.ignoreWhitespace = false;
908         return UTF8(xml.toXMLString());
909     },
910
911     exportHelp: JavaScript.setCompleter(function (path) {
912         const FILE = io.File(path);
913         const PATH = FILE.leafName.replace(/\..*/, "") + "/";
914         const TIME = Date.now();
915
916         if (!FILE.exists() && (/\/$/.test(path) && !/\./.test(FILE.leafName)))
917             FILE.create(FILE.DIRECTORY_TYPE, octal(755));
918
919         dactyl.initHelp();
920         if (FILE.isDirectory()) {
921             var addDataEntry = function addDataEntry(file, data) FILE.child(file).write(data);
922             var addURIEntry  = function addURIEntry(file, uri) addDataEntry(file, util.httpGet(uri).responseText);
923         }
924         else {
925             var zip = services.ZipWriter();
926             zip.open(FILE, File.MODE_CREATE | File.MODE_WRONLY | File.MODE_TRUNCATE);
927
928             addURIEntry = function addURIEntry(file, uri)
929                 zip.addEntryChannel(PATH + file, TIME, 9,
930                     services.io.newChannel(uri, null, null), false);
931             addDataEntry = function addDataEntry(file, data) // Unideal to an extreme.
932                 addURIEntry(file, "data:text/plain;charset=UTF-8," + encodeURI(data));
933         }
934
935         let empty = Set("area base basefont br col frame hr img input isindex link meta param"
936                             .split(" "));
937         function fix(node) {
938             switch(node.nodeType) {
939             case Node.ELEMENT_NODE:
940                 if (isinstance(node, [HTMLBaseElement]))
941                     return;
942
943                 data.push("<"); data.push(node.localName);
944                 if (node instanceof HTMLHtmlElement)
945                     data.push(" xmlns=" + XHTML.uri.quote(),
946                               " xmlns:dactyl=" + NS.uri.quote());
947
948                 for (let { name, value } in array.iterValues(node.attributes)) {
949                     if (name == "dactyl:highlight") {
950                         Set.add(styles, value);
951                         name = "class";
952                         value = "hl-" + value;
953                     }
954                     if (name == "href") {
955                         value = node.href || value;
956                         if (value.indexOf("dactyl://help-tag/") == 0) {
957                             let uri = services.io.newChannel(value, null, null).originalURI;
958                             value = uri.spec == value ? "javascript:;" : uri.path.substr(1);
959                         }
960                         if (!/^#|[\/](#|$)|^[a-z]+:/.test(value))
961                             value = value.replace(/(#|$)/, ".xhtml$1");
962                     }
963                     if (name == "src" && value.indexOf(":") > 0) {
964                         chromeFiles[value] = value.replace(/.*\//, "");
965                         value = value.replace(/.*\//, "");
966                     }
967
968                     data.push(" ", name, '="',
969                               <>{value}</>.toXMLString().replace(/"/g, "&quot;"),
970                               '"');
971                 }
972                 if (node.localName in empty)
973                     data.push(" />");
974                 else {
975                     data.push(">");
976                     if (node instanceof HTMLHeadElement)
977                         data.push(<link rel="stylesheet" type="text/css" href="help.css"/>.toXMLString());
978                     Array.map(node.childNodes, fix);
979                     data.push("</", node.localName, ">");
980                 }
981                 break;
982             case Node.TEXT_NODE:
983                 data.push(<>{node.textContent}</>.toXMLString());
984             }
985         }
986
987         let chromeFiles = {};
988         let styles = {};
989         for (let [file, ] in Iterator(services["dactyl:"].FILE_MAP)) {
990             let url = "dactyl://help/" + file;
991             dactyl.open(url);
992             util.waitFor(function () content.location.href == url && buffer.loaded
993                             && content.document.documentElement instanceof HTMLHtmlElement,
994                          15000);
995             events.waitForPageLoad();
996             var data = [
997                 '<?xml version="1.0" encoding="UTF-8"?>\n',
998                 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"\n',
999                 '          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'
1000             ];
1001             fix(content.document.documentElement);
1002             addDataEntry(file + ".xhtml", data.join(""));
1003         }
1004
1005         let data = [h for (h in highlight) if (Set.has(styles, h.class) || /^Help/.test(h.class))]
1006             .map(function (h) h.selector
1007                                .replace(/^\[.*?=(.*?)\]/, ".hl-$1")
1008                                .replace(/html\|/g, "") + "\t" + "{" + h.cssText + "}")
1009             .join("\n");
1010         addDataEntry("help.css", data.replace(/chrome:[^ ")]+\//g, ""));
1011
1012         addDataEntry("tag-map.json", JSON.stringify(services["dactyl:"].HELP_TAGS));
1013
1014         let m, re = /(chrome:[^ ");]+\/)([^ ");]+)/g;
1015         while ((m = re.exec(data)))
1016             chromeFiles[m[0]] = m[2];
1017
1018         for (let [uri, leaf] in Iterator(chromeFiles))
1019             addURIEntry(leaf, uri);
1020
1021         if (zip)
1022             zip.close();
1023     }, [function (context, args) completion.file(context)]),
1024
1025     /**
1026      * Generates a help entry and returns it as a string.
1027      *
1028      * @param {Command|Map|Option} obj A dactyl *Command*, *Map* or *Option*
1029      *     object
1030      * @param {XMLList} extraHelp Extra help text beyond the description.
1031      * @returns {string}
1032      */
1033     generateHelp: function generateHelp(obj, extraHelp, str, specOnly) {
1034         default xml namespace = "";
1035
1036         let link, tag, spec;
1037         link = tag = spec = util.identity;
1038         let args = null;
1039
1040         if (obj instanceof Command) {
1041             link = function (cmd) <ex>{cmd}</ex>;
1042             args = obj.parseArgs("", CompletionContext(str || ""));
1043             spec = function (cmd) <>{
1044                     obj.count ? <oa>count</oa> : <></>
1045                 }{
1046                     cmd
1047                 }{
1048                     obj.bang ? <oa>!</oa> : <></>
1049                 }</>;
1050         }
1051         else if (obj instanceof Map) {
1052             spec = function (map) obj.count ? <><oa>count</oa>{map}</> : <>{map}</>;
1053             link = function (map) {
1054                 let [, mode, name, extra] = /^(?:(.)_)?(?:<([^>]+)>)?(.*)$/.exec(map);
1055                 let k = <k>{extra}</k>;
1056                 if (name)
1057                     k.@name = name;
1058                 if (mode)
1059                     k.@mode = mode;
1060                 return k;
1061             };
1062         }
1063         else if (obj instanceof Option) {
1064             tag = spec = function (name) <>'{name}'</>;
1065             link = function (opt, name) <o>{name}</o>;
1066             args = { value: "", values: [] };
1067         }
1068
1069         XML.prettyPrinting = false;
1070         XML.ignoreWhitespace = false;
1071         default xml namespace = NS;
1072
1073         // E4X has its warts.
1074         let br = <>
1075                     </>;
1076
1077         let res = <res>
1078                 <dt>{link(obj.helpTag || obj.name, obj.name)}</dt> <dd>{
1079                     template.linkifyHelp(obj.description ? obj.description.replace(/\.$/, "") : "", true)
1080                 }</dd></res>;
1081         if (specOnly)
1082             return res.elements();
1083
1084         res.* += <>
1085             <item>
1086                 <tags>{template.map(obj.names.slice().reverse(), tag, " ")}</tags>
1087                 <spec>{
1088                     spec(template.highlightRegexp((obj.specs || obj.names)[0],
1089                                                   /\[(.*?)\]/g,
1090                                                   function (m, n0) <oa>{n0}</oa>))
1091                 }</spec>{
1092                 !obj.type ? "" : <>
1093                 <type>{obj.type}</type>
1094                 <default>{obj.stringDefaultValue}</default></>}
1095                 <description>{
1096                     obj.description ? br + <p>{template.linkifyHelp(obj.description.replace(/\.?$/, "."), true)}</p> : "" }{
1097                         extraHelp ? br + extraHelp : "" }{
1098                         !(extraHelp || obj.description) ? br + <p><!--L-->Sorry, no help available.</p> : "" }
1099                 </description>
1100             </item></>;
1101
1102         function add(ary) {
1103             res.item.description.* += br +
1104                 let (br = br + <>    </>)
1105                     <><dl>{ br + template.map(ary, function ([a, b]) <><dt>{a}</dt> <dd>{b}</dd></>, br) }
1106                     </dl>
1107                 </>;
1108         }
1109
1110         if (obj.completer)
1111             add(completion._runCompleter(obj.closure.completer, "", null, args).items
1112                           .map(function (i) [i.text, i.description]));
1113
1114         if (obj.options && obj.options.some(function (o) o.description))
1115             add(obj.options.filter(function (o) o.description)
1116                    .map(function (o) [
1117                         o.names[0],
1118                         <>{o.description}{
1119                             o.names.length == 1 ? "" :
1120                                 <> (short name: {
1121                                     template.map(o.names.slice(1), function (n) <em>{n}</em>, <>, </>)
1122                                 })</>
1123                         }</>
1124                     ]));
1125         return res.*.toXMLString()
1126                   .replace(' xmlns="' + NS + '"', "", "g")
1127                   .replace(/^ {12}|[ \t]+$/gm, "")
1128                   .replace(/^\s*\n|\n\s*$/g, "") + "\n";
1129     },
1130
1131     /**
1132      * Opens the help page containing the specified *topic* if it exists.
1133      *
1134      * @param {string} topic The help topic to open.
1135      * @param {boolean} consolidated Whether to use the consolidated help page.
1136      */
1137     help: function (topic, consolidated) {
1138         dactyl.initHelp();
1139         if (!topic) {
1140             let helpFile = consolidated ? "all" : options["helpfile"];
1141
1142             if (helpFile in services["dactyl:"].FILE_MAP)
1143                 dactyl.open("dactyl://help/" + helpFile, { from: "help" });
1144             else
1145                 dactyl.echomsg(_("help.noFile", helpFile.quote()));
1146             return;
1147         }
1148
1149         let page = this.findHelp(topic, consolidated);
1150         dactyl.assert(page != null, _("help.noTopic", topic));
1151
1152         dactyl.open("dactyl://help/" + page, { from: "help" });
1153     },
1154
1155     /**
1156      * The map of global variables.
1157      *
1158      * These are set and accessed with the "g:" prefix.
1159      */
1160     _globalVariables: {},
1161     globalVariables: deprecated(_("deprecated.for.theOptionsSystem"), {
1162         get: function globalVariables() this._globalVariables
1163     }),
1164
1165     loadPlugins: function (args, force) {
1166         function sourceDirectory(dir) {
1167             dactyl.assert(dir.isReadable(), _("io.notReadable", dir.path));
1168
1169             dactyl.log(_("dactyl.sourcingPlugins", dir.path), 3);
1170
1171             let loadplugins = options.get("loadplugins");
1172             if (args)
1173                 loadplugins = { __proto__: loadplugins, value: args.map(Option.parseRegexp) };
1174
1175             dir.readDirectory(true).forEach(function (file) {
1176                 if (file.isFile() && loadplugins.getKey(file.path)
1177                         && !(!force && file.path in dactyl.pluginFiles && dactyl.pluginFiles[file.path] >= file.lastModifiedTime)) {
1178                     try {
1179                         io.source(file.path);
1180                         dactyl.pluginFiles[file.path] = file.lastModifiedTime;
1181                     }
1182                     catch (e) {
1183                         dactyl.reportError(e);
1184                     }
1185                 }
1186                 else if (file.isDirectory())
1187                     sourceDirectory(file);
1188             });
1189         }
1190
1191         let dirs = io.getRuntimeDirectories("plugins");
1192
1193         if (dirs.length == 0) {
1194             dactyl.log(_("dactyl.noPluginDir"), 3);
1195             return;
1196         }
1197
1198         dactyl.echomsg(
1199             _("plugin.searchingForIn",
1200                 ("plugins/**/*.{js," + config.fileExtension + "}").quote(),
1201                 [dir.path.replace(/.plugins$/, "") for ([, dir] in Iterator(dirs))]
1202                     .join(",").quote()),
1203             2);
1204
1205         dirs.forEach(function (dir) {
1206             dactyl.echomsg(_("plugin.searchingFor", (dir.path + "/**/*.{js," + config.fileExtension + "}").quote()), 3);
1207             sourceDirectory(dir);
1208         });
1209     },
1210
1211     // TODO: add proper level constants
1212     /**
1213      * Logs a message to the JavaScript error console. Each message has an
1214      * associated log level. Only messages with a log level less than or equal
1215      * to *level* will be printed. If *msg* is an object, it is pretty printed.
1216      *
1217      * @param {string|Object} msg The message to print.
1218      * @param {number} level The logging level 0 - 15.
1219      */
1220     log: function (msg, level) {
1221         let verbose = localPrefs.get("loglevel", 0);
1222
1223         if (!level || level <= verbose) {
1224             if (isObject(msg) && !isinstance(msg, _))
1225                 msg = util.objectToString(msg, false);
1226
1227             services.console.logStringMessage(config.name + ": " + msg);
1228         }
1229     },
1230
1231     onClick: function onClick(event) {
1232         if (event.originalTarget instanceof Element) {
1233             let command = event.originalTarget.getAttributeNS(NS, "command");
1234             if (command && event.button == 0) {
1235                 event.preventDefault();
1236
1237                 if (dactyl.commands[command])
1238                     dactyl.withSavedValues(["forceNewTab"], function () {
1239                         dactyl.forceNewTab = event.ctrlKey || event.shiftKey || event.button == 1;
1240                         dactyl.commands[command](event);
1241                     });
1242             }
1243         }
1244     },
1245
1246     onExecute: function onExecute(event) {
1247         let cmd = event.originalTarget.getAttribute("dactyl-execute");
1248         commands.execute(cmd, null, false, null,
1249                          { file: /*L*/"[Command Line]", line: 1 });
1250     },
1251
1252     /**
1253      * Opens one or more URLs. Returns true when load was initiated, or
1254      * false on error.
1255      *
1256      * @param {string|Array} urls A representation of the URLs to open. May be
1257      *     either a string, which will be passed to
1258      *     {@see Dactyl#parseURLs}, or an array in the same format as
1259      *     would be returned by the same.
1260      * @param {object} params A set of parameters specifying how to open the
1261      *     URLs. The following properties are recognized:
1262      *
1263      *      â€¢ background   If true, new tabs are opened in the background.
1264      *
1265      *      â€¢ from         The designation of the opener, as appears in
1266      *                     'activate' and 'newtab' options. If present,
1267      *                     the newtab option provides the default 'where'
1268      *                     parameter, and the value of the 'activate'
1269      *                     parameter is inverted if 'background' is true.
1270      *
1271      *      â€¢ where        One of CURRENT_TAB, NEW_TAB, or NEW_WINDOW
1272      *
1273      *      As a deprecated special case, the where parameter may be provided
1274      *      by itself, in which case it is transformed into { where: params }.
1275      *
1276      * @param {boolean} force Don't prompt whether to open more than 20
1277      *     tabs.
1278      * @returns {boolean}
1279      */
1280     open: function (urls, params, force) {
1281         if (typeof urls == "string")
1282             urls = dactyl.parseURLs(urls);
1283
1284         if (urls.length > prefs.get("browser.tabs.maxOpenBeforeWarn", 20) && !force)
1285             return commandline.input(_("dactyl.prompt.openMany", urls.length) + " ",
1286                 function (resp) {
1287                     if (resp && resp.match(/^y(es)?$/i))
1288                         dactyl.open(urls, params, true);
1289                 });
1290
1291         params = params || {};
1292         if (isString(params))
1293             params = { where: params };
1294
1295         let flags = 0;
1296         for (let [opt, flag] in Iterator({ replace: "REPLACE_HISTORY", hide: "BYPASS_HISTORY" }))
1297             flags |= params[opt] && Ci.nsIWebNavigation["LOAD_FLAGS_" + flag];
1298
1299         let where = params.where || dactyl.CURRENT_TAB;
1300         let background = ("background" in params) ? params.background
1301                                                   : params.where == dactyl.NEW_BACKGROUND_TAB;
1302
1303         if (params.from && dactyl.has("tabs")) {
1304             if (!params.where && options.get("newtab").has(params.from))
1305                 where = dactyl.NEW_TAB;
1306             background ^= !options.get("activate").has(params.from);
1307         }
1308
1309         if (urls.length == 0)
1310             return;
1311
1312         let browser = config.tabbrowser;
1313         function open(urls, where) {
1314             try {
1315                 let url = Array.concat(urls)[0];
1316                 let postdata = Array.concat(urls)[1];
1317
1318                 // decide where to load the first url
1319                 switch (where) {
1320
1321                 case dactyl.NEW_TAB:
1322                     if (!dactyl.has("tabs"))
1323                         return open(urls, dactyl.NEW_WINDOW);
1324
1325                     return prefs.withContext(function () {
1326                         prefs.set("browser.tabs.loadInBackground", true);
1327                         return browser.loadOneTab(url, null, null, postdata, background).linkedBrowser.contentDocument;
1328                     });
1329
1330                 case dactyl.NEW_WINDOW:
1331                     let win = window.openDialog(document.documentURI, "_blank", "chrome,all,dialog=no");
1332                     util.waitFor(function () win.document.readyState === "complete");
1333                     browser = win.dactyl && win.dactyl.modules.config.tabbrowser || win.getBrowser();
1334                     // FALLTHROUGH
1335                 case dactyl.CURRENT_TAB:
1336                     browser.loadURIWithFlags(url, flags, null, null, postdata);
1337                     return browser.contentWindow;
1338                 }
1339             }
1340             catch (e) {}
1341             // Unfortunately, failed page loads throw exceptions and
1342             // cause a lot of unwanted noise. This solution means that
1343             // any genuine errors go unreported.
1344         }
1345
1346         if (dactyl.forceNewTab)
1347             where = dactyl.NEW_TAB;
1348         else if (dactyl.forceNewWindow)
1349             where = dactyl.NEW_WINDOW;
1350         else if (!where)
1351             where = dactyl.CURRENT_TAB;
1352
1353         return urls.map(function (url) {
1354             let res = open(url, where);
1355             where = dactyl.NEW_TAB;
1356             background = true;
1357             return res;
1358         });
1359     },
1360
1361     /**
1362      * Returns an array of URLs parsed from *str*.
1363      *
1364      * Given a string like 'google bla, www.osnews.com' return an array
1365      * ['www.google.com/search?q=bla', 'www.osnews.com']
1366      *
1367      * @param {string} str
1368      * @returns {[string]}
1369      */
1370     parseURLs: function parseURLs(str) {
1371         let urls;
1372
1373         if (options["urlseparator"])
1374             urls = util.splitLiteral(str, util.regexp("\\s*" + options["urlseparator"] + "\\s*"));
1375         else
1376             urls = [str];
1377
1378         return urls.map(function (url) {
1379             url = url.trim();
1380
1381             if (/^(\.{0,2}|~)(\/|$)/.test(url) || util.OS.isWindows && /^[a-z]:/i.test(url)) {
1382                 try {
1383                     // Try to find a matching file.
1384                     let file = io.File(url);
1385                     if (file.exists() && file.isReadable())
1386                         return services.io.newFileURI(file).spec;
1387                 }
1388                 catch (e) {}
1389             }
1390
1391             // If it starts with a valid protocol, pass it through.
1392             let proto = /^([-\w]+):/.exec(url);
1393             if (proto && "@mozilla.org/network/protocol;1?name=" + proto[1] in Cc)
1394                 return url;
1395
1396             // Check for a matching search keyword.
1397             let searchURL = this.has("bookmarks") && bookmarks.getSearchURL(url, false);
1398             if (searchURL)
1399                 return searchURL;
1400
1401             // If it looks like URL-ish (foo.com/bar), let Gecko figure it out.
1402             if (this.urlish.test(url) || !this.has("bookmarks"))
1403                 return util.createURI(url).spec;
1404
1405             // Pass it off to the default search engine or, failing
1406             // that, let Gecko deal with it as is.
1407             return bookmarks.getSearchURL(url, true) || util.createURI(url).spec;
1408         }, this);
1409     },
1410     stringToURLArray: deprecated("dactyl.parseURLs", "parseURLs"),
1411     urlish: Class.memoize(function () util.regexp(<![CDATA[
1412             ^ (
1413                 <domain>+ (:\d+)? (/ .*) |
1414                 <domain>+ (:\d+) |
1415                 <domain>+ \. [a-z0-9]+ |
1416                 localhost
1417             ) $
1418         ]]>, "ix", {
1419         domain: util.regexp(String.replace(<![CDATA[
1420             [^
1421                 U0000-U002c // U002d-U002e --.
1422                 U002f       // /
1423                             // U0030-U0039 0-9
1424                 U003a-U0040 // U0041-U005a a-z
1425                 U005b-U0060 // U0061-U007a A-Z
1426                 U007b-U007f
1427             ]
1428         ]]>, /U/g, "\\u"), "x")
1429     })),
1430
1431     pluginFiles: {},
1432
1433     get plugins() plugins,
1434
1435     setNodeVisible: function setNodeVisible(node, visible) {
1436         if (window.setToolbarVisibility && node.localName == "toolbar")
1437             window.setToolbarVisibility(node, visible);
1438         else
1439             node.collapsed = !visible;
1440     },
1441
1442     confirmQuit: function confirmQuit()
1443         prefs.withContext(function () {
1444             prefs.set("browser.warnOnQuit", false);
1445             return window.canQuitApplication();
1446         }),
1447
1448     /**
1449      * Quit the host application, no matter how many tabs/windows are open.
1450      *
1451      * @param {boolean} saveSession If true the current session will be
1452      *     saved and restored when the host application is restarted.
1453      * @param {boolean} force Forcibly quit irrespective of whether all
1454      *    windows could be closed individually.
1455      */
1456     quit: function (saveSession, force) {
1457         if (!force && !this.confirmQuit())
1458             return;
1459
1460         let pref = "browser.startup.page";
1461         prefs.save(pref);
1462         if (saveSession)
1463             prefs.safeSet(pref, 3);
1464         if (!saveSession && prefs.get(pref) >= 2)
1465             prefs.safeSet(pref, 1);
1466
1467         services.appStartup.quit(Ci.nsIAppStartup[force ? "eForceQuit" : "eAttemptQuit"]);
1468     },
1469
1470     /**
1471      * Restart the host application.
1472      */
1473     restart: function () {
1474         if (!this.confirmQuit())
1475             return;
1476
1477         services.appStartup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
1478     },
1479
1480     get assert() util.assert,
1481
1482     /**
1483      * Traps errors in the called function, possibly reporting them.
1484      *
1485      * @param {function} func The function to call
1486      * @param {object} self The 'this' object for the function.
1487      */
1488     trapErrors: function trapErrors(func, self) {
1489         try {
1490             if (isString(func))
1491                 func = self[func];
1492             return func.apply(self || this, Array.slice(arguments, 2));
1493         }
1494         catch (e) {
1495             dactyl.reportError(e, true);
1496             return e;
1497         }
1498     },
1499
1500     /**
1501      * Reports an error to both the console and the host application's
1502      * Error Console.
1503      *
1504      * @param {Object} error The error object.
1505      */
1506     reportError: function reportError(error, echo) {
1507         if (error instanceof FailedAssertion && error.noTrace || error.message === "Interrupted") {
1508             let context = contexts.context;
1509             let prefix = context ? context.file + ":" + context.line + ": " : "";
1510             if (error.message && error.message.indexOf(prefix) !== 0)
1511                 error.message = prefix + error.message;
1512
1513             if (error.message)
1514                 dactyl.echoerr(template.linkifyHelp(error.message));
1515             else
1516                 dactyl.beep();
1517
1518             if (!error.noTrace)
1519                 util.reportError(error);
1520             return;
1521         }
1522         if (error.result == Cr.NS_BINDING_ABORTED)
1523             return;
1524         if (echo)
1525             dactyl.echoerr(error, commandline.FORCE_SINGLELINE);
1526         else
1527             util.reportError(error);
1528     },
1529
1530     /**
1531      * Parses a Dactyl command-line string i.e. the value of the
1532      * -dactyl command-line option.
1533      *
1534      * @param {string} cmdline The string to parse for command-line
1535      *     options.
1536      * @returns {Object}
1537      * @see Commands#parseArgs
1538      */
1539     parseCommandLine: function (cmdline) {
1540         try {
1541             return commands.get("rehash").parseArgs(cmdline);
1542         }
1543         catch (e) {
1544             dactyl.reportError(e, true);
1545             return [];
1546         }
1547     },
1548
1549     wrapCallback: function (callback, self) {
1550         self = self || this;
1551         let save = ["forceNewTab", "forceNewWindow"];
1552         let saved = save.map(function (p) dactyl[p]);
1553         return function wrappedCallback() {
1554             let args = arguments;
1555             return dactyl.withSavedValues(save, function () {
1556                 saved.forEach(function (p, i) dactyl[save[i]] = p);
1557                 try {
1558                     return callback.apply(self, args);
1559                 }
1560                 catch (e) {
1561                     dactyl.reportError(e, true);
1562                 }
1563             });
1564         }
1565     },
1566
1567     /**
1568      * @property {[Window]} Returns an array of all the host application's
1569      *     open windows.
1570      */
1571     get windows() [win for (win in iter(services.windowMediator.getEnumerator("navigator:browser"))) if (win.dactyl)],
1572
1573 }, {
1574     toolbarHidden: function hidden(elem) (elem.getAttribute("autohide") || elem.getAttribute("collapsed")) == "true"
1575 }, {
1576     events: function () {
1577         events.listen(window, "click", dactyl.closure.onClick, true);
1578         events.listen(window, "dactyl.execute", dactyl.closure.onExecute, true);
1579     },
1580     // Only general options are added here, which are valid for all Dactyl extensions
1581     options: function () {
1582         options.add(["errorbells", "eb"],
1583             "Ring the bell when an error message is displayed",
1584             "boolean", false);
1585
1586         options.add(["exrc", "ex"],
1587             "Enable automatic sourcing of an RC file in the current directory at startup",
1588             "boolean", false);
1589
1590         options.add(["fullscreen", "fs"],
1591             "Show the current window fullscreen",
1592             "boolean", false, {
1593                 setter: function (value) window.fullScreen = value,
1594                 getter: function () window.fullScreen
1595             });
1596
1597         const groups = [
1598             {
1599                 opts: {
1600                     c: ["Always show the command line, even when empty"],
1601                     C: ["Always show the command line outside of the status line"],
1602                     M: ["Always show messages outside of the status line"]
1603                 },
1604                 setter: function (opts) {
1605                     if (loaded.commandline || ~opts.indexOf("c"))
1606                         commandline.widgets.updateVisibility();
1607                 }
1608             },
1609             {
1610                 opts: update({
1611                     s: ["Status bar", [statusline.statusBar.id]]
1612                 }, config.guioptions),
1613                 setter: function (opts) {
1614                     for (let [opt, [, ids]] in Iterator(this.opts)) {
1615                         ids.map(function (id) document.getElementById(id))
1616                            .forEach(function (elem) {
1617                             if (elem)
1618                                 dactyl.setNodeVisible(elem, opts.indexOf(opt) >= 0);
1619                         });
1620                     }
1621                 }
1622             },
1623             {
1624                 opts: {
1625                     r: ["Right Scrollbar", "vertical"],
1626                     l: ["Left Scrollbar", "vertical"],
1627                     b: ["Bottom Scrollbar", "horizontal"]
1628                 },
1629                 setter: function (opts) {
1630                     let dir = ["horizontal", "vertical"].filter(
1631                         function (dir) !Array.some(opts,
1632                             function (o) this.opts[o] && this.opts[o][1] == dir, this),
1633                         this);
1634                     let class_ = dir.map(function (dir) "html|html > xul|scrollbar[orient=" + dir + "]");
1635
1636                     styles.system.add("scrollbar", "*",
1637                                       class_.length ? class_.join(", ") + " { visibility: collapse !important; }" : "",
1638                                       true);
1639
1640                     prefs.safeSet("layout.scrollbar.side", opts.indexOf("l") >= 0 ? 3 : 2,
1641                                   _("option.guioptions.safeSet"));
1642                 },
1643                 validator: function (opts) Option.validIf(!(opts.indexOf("l") >= 0 && opts.indexOf("r") >= 0),
1644                                                           UTF8("Only one of â€˜l’ or â€˜r’ allowed"))
1645             },
1646             {
1647                 feature: "tabs",
1648                 opts: {
1649                     n: ["Tab number", highlight.selector("TabNumber")],
1650                     N: ["Tab number over icon", highlight.selector("TabIconNumber")]
1651                 },
1652                 setter: function (opts) {
1653                     let classes = [v[1] for ([k, v] in Iterator(this.opts)) if (opts.indexOf(k) < 0)];
1654
1655                     styles.system.add("taboptions", "chrome://*",
1656                                       classes.length ? classes.join(",") + "{ display: none; }" : "");
1657
1658                     if (!dactyl.has("Gecko2")) {
1659                         tabs.tabBinding.enabled = Array.some(opts, function (k) k in this.opts, this);
1660                         tabs.updateTabCount();
1661                     }
1662                     if (config.tabbrowser.tabContainer._positionPinnedTabs)
1663                         config.tabbrowser.tabContainer._positionPinnedTabs();
1664                 },
1665                 /*
1666                 validator: function (opts) dactyl.has("Gecko2") ||
1667                     Option.validIf(!/[nN]/.test(opts), "Tab numbering not available in this " + config.host + " version")
1668                  */
1669             }
1670         ].filter(function (group) !group.feature || dactyl.has(group.feature));
1671
1672         options.add(["guioptions", "go"],
1673             "Show or hide certain GUI elements like the menu or toolbar",
1674             "charlist", config.defaults.guioptions || "", {
1675
1676                 // FIXME: cleanup
1677                 cleanupValue: config.cleanups.guioptions ||
1678                     "r" + [k for ([k, v] in iter(groups[1].opts))
1679                            if (!Dactyl.toolbarHidden(document.getElementById(v[1][0])))].join(""),
1680
1681                 values: array(groups).map(function (g) [[k, v[0]] for ([k, v] in Iterator(g.opts))]).flatten(),
1682
1683                 setter: function (value) {
1684                     for (let group in values(groups))
1685                         group.setter(value);
1686                     events.checkFocus();
1687                     return value;
1688                 },
1689                 validator: function (val) Option.validateCompleter.call(this, val) &&
1690                         groups.every(function (g) !g.validator || g.validator(val))
1691             });
1692
1693         options.add(["helpfile", "hf"],
1694             "Name of the main help file",
1695             "string", "intro");
1696
1697         options.add(["loadplugins", "lpl"],
1698             "A regexp list that defines which plugins are loaded at startup and via :loadplugins",
1699             "regexplist", "'\\.(js|" + config.fileExtension + ")$'");
1700
1701         options.add(["titlestring"],
1702             "The string shown at the end of the window title",
1703             "string", config.defaults.titlestring || config.host,
1704             {
1705                 setter: function (value) {
1706                     let win = document.documentElement;
1707                     function updateTitle(old, current) {
1708                         document.title = document.title.replace(RegExp("(.*)" + util.regexp.escape(old)), "$1" + current);
1709                     }
1710
1711                     if (services.has("privateBrowsing")) {
1712                         let oldValue = win.getAttribute("titlemodifier_normal");
1713                         let suffix = win.getAttribute("titlemodifier_privatebrowsing").substr(oldValue.length);
1714
1715                         win.setAttribute("titlemodifier_normal", value);
1716                         win.setAttribute("titlemodifier_privatebrowsing", value + suffix);
1717
1718                         if (services.privateBrowsing.privateBrowsingEnabled) {
1719                             updateTitle(oldValue + suffix, value + suffix);
1720                             return value;
1721                         }
1722                     }
1723
1724                     updateTitle(win.getAttribute("titlemodifier"), value);
1725                     win.setAttribute("titlemodifier", value);
1726
1727                     return value;
1728                 }
1729             });
1730
1731         options.add(["urlseparator", "urlsep", "us"],
1732             "The regular expression used to separate multiple URLs in :open and friends",
1733             "string", " \\| ",
1734             { validator: function (value) RegExp(value) });
1735
1736         options.add(["verbose", "vbs"],
1737             "Define which info messages are displayed",
1738             "number", 1,
1739             { validator: function (value) Option.validIf(value >= 0 && value <= 15, "Value must be between 0 and 15") });
1740
1741         options.add(["visualbell", "vb"],
1742             "Use visual bell instead of beeping on errors",
1743             "boolean", false,
1744             {
1745                 setter: function (value) {
1746                     prefs.safeSet("accessibility.typeaheadfind.enablesound", !value,
1747                                   _("option.visualbell.safeSet"));
1748                     return value;
1749                 }
1750             });
1751     },
1752
1753     mappings: function () {
1754         mappings.add([modes.MAIN], ["<open-help>", "<F1>"],
1755             "Open the introductory help page",
1756             function () { dactyl.help(); });
1757
1758         mappings.add([modes.MAIN], ["<open-single-help>", "<A-F1>"],
1759             "Open the single, consolidated help page",
1760             function () { ex.helpall(); });
1761
1762         if (dactyl.has("session"))
1763             mappings.add([modes.NORMAL], ["ZQ"],
1764                 "Quit and don't save the session",
1765                 function () { dactyl.quit(false); });
1766
1767         mappings.add([modes.NORMAL], ["ZZ"],
1768             "Quit and save the session",
1769             function () { dactyl.quit(true); });
1770     },
1771
1772     commands: function () {
1773         commands.add(["dia[log]"],
1774             "Open a " + config.appName + " dialog",
1775             function (args) {
1776                 let dialog = args[0];
1777
1778                 dactyl.assert(dialog in config.dialogs,
1779                               _("error.invalidArgument", dialog));
1780                 dactyl.assert(!config.dialogs[dialog][2] || config.dialogs[dialog][2](),
1781                               _("dialog.notAvailable", dialog));
1782                 try {
1783                     config.dialogs[dialog][1]();
1784                 }
1785                 catch (e) {
1786                     dactyl.echoerr(_("error.cantOpen", dialog.quote(), e.message || e));
1787                 }
1788             }, {
1789                 argCount: "1",
1790                 completer: function (context) {
1791                     context.ignoreCase = true;
1792                     completion.dialog(context);
1793                 }
1794             });
1795
1796         commands.add(["em[enu]"],
1797             "Execute the specified menu item from the command line",
1798             function (args) {
1799                 let arg = args[0] || "";
1800                 let items = dactyl.menuItems;
1801
1802                 dactyl.assert(items.some(function (i) i.dactylPath == arg),
1803                               _("emenu.notFound", arg));
1804
1805                 for (let [, item] in Iterator(items)) {
1806                     if (item.dactylPath == arg) {
1807                         dactyl.assert(!item.disabled, _("error.disabled", item.dactylPath));
1808                         item.doCommand();
1809                     }
1810                 }
1811             }, {
1812                 argCount: "1",
1813                 completer: function (context) completion.menuItem(context),
1814                 literal: 0
1815             });
1816
1817         commands.add(["exe[cute]"],
1818             "Execute the argument as an Ex command",
1819             function (args) {
1820                 try {
1821                     let cmd = dactyl.userEval(args[0] || "");
1822                     dactyl.execute(cmd || "", null, true);
1823                 }
1824                 catch (e) {
1825                     dactyl.echoerr(e);
1826                 }
1827             }, {
1828                 completer: function (context) completion.javascript(context),
1829                 literal: 0
1830             });
1831
1832         [
1833             {
1834                 name: "h[elp]",
1835                 description: "Open the introductory help page"
1836             }, {
1837                 name: "helpa[ll]",
1838                 description: "Open the single consolidated help page"
1839             }
1840         ].forEach(function (command) {
1841             let consolidated = command.name == "helpa[ll]";
1842
1843             commands.add([command.name],
1844                 command.description,
1845                 function (args) {
1846                     dactyl.assert(!args.bang, _("help.dontPanic"));
1847                     dactyl.help(args.literalArg, consolidated);
1848                 }, {
1849                     argCount: "?",
1850                     bang: true,
1851                     completer: function (context) completion.help(context, consolidated),
1852                     literal: 0
1853                 });
1854         });
1855
1856         commands.add(["loadplugins", "lpl"],
1857             "Load all or matching plugins",
1858             function (args) {
1859                 dactyl.loadPlugins(args.length ? args : null, args.bang);
1860             },
1861             {
1862                 argCount: "*",
1863                 bang: true,
1864                 keepQuotes: true,
1865                 serialGroup: 10,
1866                 serialize: function () [
1867                     {
1868                         command: this.name,
1869                         literalArg: options["loadplugins"].join(" ")
1870                     }
1871                 ]
1872             });
1873
1874         commands.add(["norm[al]"],
1875             "Execute Normal mode commands",
1876             function (args) { events.feedkeys(args[0], args.bang, false, modes.NORMAL); },
1877             {
1878                 argCount: "1",
1879                 bang: true,
1880                 literal: 0
1881             });
1882
1883         commands.add(["exit", "x"],
1884             "Quit " + config.appName,
1885             function (args) {
1886                 dactyl.quit(false, args.bang);
1887             }, {
1888                 argCount: "0",
1889                 bang: true
1890             });
1891
1892         commands.add(["q[uit]"],
1893             dactyl.has("tabs") ? "Quit current tab" : "Quit application",
1894             function (args) {
1895                 if (dactyl.has("tabs") && tabs.remove(tabs.getTab(), 1, false))
1896                     return;
1897                 else if (dactyl.windows.length > 1)
1898                     window.close();
1899                 else
1900                     dactyl.quit(false, args.bang);
1901             }, {
1902                 argCount: "0",
1903                 bang: true
1904             });
1905
1906         commands.add(["reh[ash]"],
1907             "Reload the " + config.appName + " add-on",
1908             function (args) {
1909                 if (args.trailing)
1910                     storage.session.rehashCmd = args.trailing; // Hack.
1911                 args.break = true;
1912                 util.rehash(args);
1913             },
1914             {
1915                 argCount: "0", // FIXME
1916                 options: [
1917                     {
1918                         names: ["+u"],
1919                         description: "The initialization file to execute at startup",
1920                         type: CommandOption.STRING
1921                     },
1922                     {
1923                         names: ["++noplugin"],
1924                         description: "Do not automatically load plugins"
1925                     },
1926                     {
1927                         names: ["++cmd"],
1928                         description: "Ex commands to execute prior to initialization",
1929                         type: CommandOption.STRING,
1930                         multiple: true
1931                     },
1932                     {
1933                         names: ["+c"],
1934                         description: "Ex commands to execute after initialization",
1935                         type: CommandOption.STRING,
1936                         multiple: true
1937                     }
1938                 ]
1939             });
1940
1941         commands.add(["res[tart]"],
1942             "Force " + config.appName + " to restart",
1943             function () { dactyl.restart(); },
1944             { argCount: "0" });
1945
1946         function findToolbar(name) util.evaluateXPath(
1947             "//*[@toolbarname=" + util.escapeString(name, "'") + " or " +
1948                 "@toolbarname=" + util.escapeString(name.trim(), "'") + "]",
1949             document).snapshotItem(0);
1950
1951         var toolbox = document.getElementById("navigator-toolbox");
1952         if (toolbox) {
1953             let toolbarCommand = function (names, desc, action, filter) {
1954                 commands.add(names, desc,
1955                     function (args) {
1956                         let toolbar = findToolbar(args[0] || "");
1957                         dactyl.assert(toolbar, _("error.invalidArgument"));
1958                         action(toolbar);
1959                         events.checkFocus();
1960                     }, {
1961                         argCount: "1",
1962                         completer: function (context) {
1963                             completion.toolbar(context);
1964                             if (filter)
1965                                 context.filters.push(filter);
1966                         },
1967                         literal: 0
1968                     });
1969             };
1970
1971             toolbarCommand(["toolbars[how]", "tbs[how]"], "Show the named toolbar",
1972                 function (toolbar) dactyl.setNodeVisible(toolbar, true),
1973                 function ({ item }) Dactyl.toolbarHidden(item));
1974             toolbarCommand(["toolbarh[ide]", "tbh[ide]"], "Hide the named toolbar",
1975                 function (toolbar) dactyl.setNodeVisible(toolbar, false),
1976                 function ({ item }) !Dactyl.toolbarHidden(item));
1977             toolbarCommand(["toolbart[oggle]", "tbt[oggle]"], "Toggle the named toolbar",
1978                 function (toolbar) dactyl.setNodeVisible(toolbar, Dactyl.toolbarHidden(toolbar)));
1979         }
1980
1981         commands.add(["time"],
1982             "Profile a piece of code or run a command multiple times",
1983             function (args) {
1984                 let count = args.count;
1985                 let special = args.bang;
1986                 args = args[0] || "";
1987
1988                 if (args[0] == ":")
1989                     var func = function () commands.execute(args, null, false);
1990                 else
1991                     func = dactyl.userFunc(args);
1992
1993                 try {
1994                     if (count > 1) {
1995                         let each, eachUnits, totalUnits;
1996                         let total = 0;
1997
1998                         for (let i in util.interruptibleRange(0, count, 500)) {
1999                             let now = Date.now();
2000                             func();
2001                             total += Date.now() - now;
2002                         }
2003
2004                         if (special)
2005                             return;
2006
2007                         if (total / count >= 100) {
2008                             each = total / 1000.0 / count;
2009                             eachUnits = "sec";
2010                         }
2011                         else {
2012                             each = total / count;
2013                             eachUnits = "msec";
2014                         }
2015
2016                         if (total >= 100) {
2017                             total = total / 1000.0;
2018                             totalUnits = "sec";
2019                         }
2020                         else
2021                             totalUnits = "msec";
2022
2023                         commandline.commandOutput(
2024                                 <table>
2025                                     <tr highlight="Title" align="left">
2026                                         <th colspan="3">{_("title.Code execution summary")}</th>
2027                                     </tr>
2028                                     <tr><td>&#xa0;&#xa0;{_("title.Executed")}:</td><td align="right"><span class="times-executed">{count}</span></td><td><!--L-->times</td></tr>
2029                                     <tr><td>&#xa0;&#xa0;{_("title.Average time")}:</td><td align="right"><span class="time-average">{each.toFixed(2)}</span></td><td>{eachUnits}</td></tr>
2030                                     <tr><td>&#xa0;&#xa0;{_("title.Total time")}:</td><td align="right"><span class="time-total">{total.toFixed(2)}</span></td><td>{totalUnits}</td></tr>
2031                                 </table>);
2032                     }
2033                     else {
2034                         let beforeTime = Date.now();
2035                         func();
2036
2037                         if (special)
2038                             return;
2039
2040                         let afterTime = Date.now();
2041
2042                         if (afterTime - beforeTime >= 100)
2043                             dactyl.echo(_("time.total", ((afterTime - beforeTime) / 1000.0).toFixed(2) + " sec"));
2044                         else
2045                             dactyl.echo(_("time.total", (afterTime - beforeTime) + " msec"));
2046                     }
2047                 }
2048                 catch (e) {
2049                     dactyl.echoerr(e);
2050                 }
2051             }, {
2052                 argCount: "1",
2053                 bang: true,
2054                 completer: function (context) {
2055                     if (/^:/.test(context.filter))
2056                         return completion.ex(context);
2057                     else
2058                         return completion.javascript(context);
2059                 },
2060                 count: true,
2061                 hereDoc: true,
2062                 literal: 0,
2063                 subCommand: 0
2064             });
2065
2066         commands.add(["verb[ose]"],
2067             "Execute a command with 'verbose' set",
2068             function (args) {
2069                 let vbs = options.get("verbose");
2070                 let value = vbs.value;
2071                 let setFrom = vbs.setFrom;
2072
2073                 try {
2074                     vbs.set(args.count || 1);
2075                     vbs.setFrom = null;
2076                     dactyl.execute(args[0] || "", null, true);
2077                 }
2078                 finally {
2079                     vbs.set(value);
2080                     vbs.setFrom = setFrom;
2081                 }
2082             }, {
2083                 argCount: "1",
2084                 completer: function (context) completion.ex(context),
2085                 count: true,
2086                 literal: 0,
2087                 subCommand: 0
2088             });
2089
2090         commands.add(["ve[rsion]"],
2091             "Show version information",
2092             function (args) {
2093                 if (args.bang)
2094                     dactyl.open("about:");
2095                 else
2096                     commandline.commandOutput(<>
2097                         {config.appName} {config.version} running on:<br/>{navigator.userAgent}
2098                     </>);
2099             }, {
2100                 argCount: "0",
2101                 bang: true
2102             });
2103
2104     },
2105
2106     completion: function () {
2107         completion.dialog = function dialog(context) {
2108             context.title = ["Dialog"];
2109             context.filters.push(function ({ item }) !item[2] || item[2]());
2110             context.completions = [[k, v[0], v[2]] for ([k, v] in Iterator(config.dialogs))];
2111         };
2112
2113         completion.help = function help(context, consolidated) {
2114             dactyl.initHelp();
2115             context.title = ["Help"];
2116             context.anchored = false;
2117             context.completions = services["dactyl:"].HELP_TAGS;
2118             if (consolidated)
2119                 context.keys = { text: 0, description: function () "all" };
2120         };
2121
2122         completion.menuItem = function menuItem(context) {
2123             context.title = ["Menu Path", "Label"];
2124             context.anchored = false;
2125             context.keys = {
2126                 text: "dactylPath",
2127                 description: function (item) item.getAttribute("label"),
2128                 highlight: function (item) item.disabled ? "Disabled" : ""
2129             };
2130             context.generate = function () dactyl.menuItems;
2131         };
2132
2133         var toolbox = document.getElementById("navigator-toolbox");
2134         completion.toolbar = function toolbar(context) {
2135             context.title = ["Toolbar"];
2136             context.keys = { text: function (item) item.getAttribute("toolbarname"), description: function () "" };
2137             context.completions = util.evaluateXPath("//*[@toolbarname]", document);
2138         };
2139
2140         completion.window = function window(context) {
2141             context.title = ["Window", "Title"];
2142             context.keys = { text: function (win) dactyl.windows.indexOf(win) + 1, description: function (win) win.document.title };
2143             context.completions = dactyl.windows;
2144         };
2145     },
2146     load: function () {
2147         dactyl.triggerObserver("load");
2148
2149         dactyl.log(_("dactyl.modulesLoaded"), 3);
2150
2151         dactyl.timeout(function () {
2152             try {
2153                 var args = storage.session.commandlineArgs || services.commandLineHandler.optionValue;
2154                 if (isString(args))
2155                     args = dactyl.parseCommandLine(args);
2156
2157                 if (args) {
2158                     dactyl.commandLineOptions.rcFile = args["+u"];
2159                     dactyl.commandLineOptions.noPlugins = "++noplugin" in args;
2160                     dactyl.commandLineOptions.postCommands = args["+c"];
2161                     dactyl.commandLineOptions.preCommands = args["++cmd"];
2162                     util.dump("Processing command-line option: " + args.string);
2163                 }
2164             }
2165             catch (e) {
2166                 dactyl.echoerr(_("dactyl.parsingCommandLine", e));
2167             }
2168
2169             dactyl.log(_("dactyl.commandlineOpts", util.objectToString(dactyl.commandLineOptions)), 3);
2170
2171             // first time intro message
2172             const firstTime = "extensions." + config.name + ".firsttime";
2173             if (prefs.get(firstTime, true)) {
2174                 dactyl.timeout(function () {
2175                     this.withSavedValues(["forceNewTab"], function () {
2176                         this.forceNewTab = true;
2177                         this.help();
2178                         prefs.set(firstTime, false);
2179                     });
2180                 }, 1000);
2181             }
2182
2183             // TODO: we should have some class where all this guioptions stuff fits well
2184             // dactyl.hideGUI();
2185
2186             if (dactyl.userEval("typeof document", null, "test.js") === "undefined")
2187                 jsmodules.__proto__ = XPCSafeJSObjectWrapper(window);
2188
2189             if (dactyl.commandLineOptions.preCommands)
2190                 dactyl.commandLineOptions.preCommands.forEach(function (cmd) {
2191                     dactyl.execute(cmd);
2192                 });
2193
2194             // finally, read the RC file and source plugins
2195             let init = services.environment.get(config.idName + "_INIT");
2196             let rcFile = io.getRCFile("~");
2197
2198             try {
2199                 if (dactyl.commandLineOptions.rcFile) {
2200                     let filename = dactyl.commandLineOptions.rcFile;
2201                     if (!/^(NONE|NORC)$/.test(filename))
2202                         io.source(io.File(filename).path, { group: contexts.user });
2203                 }
2204                 else {
2205                     if (init)
2206                         dactyl.execute(init);
2207                     else {
2208                         if (rcFile) {
2209                             io.source(rcFile.path, { group: contexts.user });
2210                             services.environment.set("MY_" + config.idName + "RC", rcFile.path);
2211                         }
2212                         else
2213                             dactyl.log(_("dactyl.noRCFile"), 3);
2214                     }
2215
2216                     if (options["exrc"] && !dactyl.commandLineOptions.rcFile) {
2217                         let localRCFile = io.getRCFile(io.cwd);
2218                         if (localRCFile && !localRCFile.equals(rcFile))
2219                             io.source(localRCFile.path, { group: contexts.user });
2220                     }
2221                 }
2222
2223                 if (dactyl.commandLineOptions.rcFile == "NONE" || dactyl.commandLineOptions.noPlugins)
2224                     options["loadplugins"] = [];
2225
2226                 if (options["loadplugins"])
2227                     dactyl.loadPlugins();
2228             }
2229             catch (e) {
2230                 dactyl.reportError(e, true);
2231             }
2232
2233             // after sourcing the initialization files, this function will set
2234             // all gui options to their default values, if they have not been
2235             // set before by any RC file
2236             for (let option in values(options.needInit))
2237                 option.initValue();
2238
2239             if (dactyl.commandLineOptions.postCommands)
2240                 dactyl.commandLineOptions.postCommands.forEach(function (cmd) {
2241                     dactyl.execute(cmd);
2242                 });
2243
2244             if (storage.session.rehashCmd)
2245                 dactyl.execute(storage.session.rehashCmd);
2246             storage.session.rehashCmd = null;
2247
2248             dactyl.fullyInitialized = true;
2249             dactyl.triggerObserver("enter", null);
2250             autocommands.trigger("Enter", {});
2251         }, 100);
2252
2253         statusline.update();
2254         dactyl.log(_("dactyl.initialized", config.appName), 0);
2255         dactyl.initialized = true;
2256     }
2257 });
2258
2259 // vim: set fdm=marker sw=4 ts=4 et: