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