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