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