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