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