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