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>
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
11 default xml namespace = XHTML;
12 XML.ignoreWhitespace = false;
13 XML.prettyPrinting = false;
15 var EVAL_ERROR = "__dactyl_eval_error";
16 var EVAL_RESULT = "__dactyl_eval_result";
17 var EVAL_STRING = "__dactyl_eval_string";
19 var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
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);
28 this.modules = modules;
30 util.addObserver(this);
32 this.commands["dactyl.help"] = function (event) {
33 let elem = event.originalTarget;
34 dactyl.help(elem.getAttribute("tag") || elem.textContent);
36 this.commands["dactyl.restart"] = function (event) {
40 styles.registerSheet("resource://dactyl-skin/dactyl.css");
43 this.cleanups.push(util.overlayObject(window, {
44 focusAndSelectUrlBar: function focusAndSelectUrlBar() {
45 switch (options.get("strictfocus").getKey(document.documentURIObject || util.newURI(document.documentURI), "moderate")) {
47 if (!Events.isHidden(window.gURLBar, true))
48 return focusAndSelectUrlBar.superapply(this, arguments);
56 cleanup: function () {
57 for (let cleanup in values(this.cleanups))
61 delete window.liberator;
63 styles.unregisterSheet("resource://dactyl-skin/dactyl.css");
66 destroy: function () {
67 autocommands.trigger("LeavePre", {});
68 dactyl.triggerObserver("shutdown", null);
69 util.dump("All dactyl modules destroyed\n");
70 autocommands.trigger("Leave", {});
73 // initially hide all GUI elements, they are later restored unless the user
74 // has :set go= or something similar in his config
75 hideGUI: function () {
76 let guioptions = config.guioptions;
77 for (let option in guioptions) {
78 guioptions[option].forEach(function (elem) {
80 document.getElementById(elem).collapsed = true;
89 "dactyl-cleanup": function dactyl_cleanup(subject, reason) {
90 let modules = dactyl.modules;
92 for (let mod in values(modules.moduleList.reverse())) {
95 this.trapErrors("cleanup", mod, reason);
97 this.trapErrors("destroy", mod, reason);
100 for (let mod in values(modules.ownPropertyValues.reverse()))
101 if (mod instanceof Class && "INIT" in mod && "cleanup" in mod.INIT)
102 this.trapErrors(mod.cleanup, mod, dactyl, modules, window, reason);
104 for (let name in values(Object.getOwnPropertyNames(modules).reverse()))
106 delete modules[name];
109 modules.__proto__ = {};
113 /** @property {string} The name of the current user profile. */
114 profileName: Class.memoize(function () {
115 // NOTE: services.profile.selectedProfile.name doesn't return
116 // what you might expect. It returns the last _actively_ selected
117 // profile (i.e. via the Profile Manager or -P option) rather than the
118 // current profile. These will differ if the current process was run
119 // without explicitly selecting a profile.
121 let dir = services.directory.get("ProfD", Ci.nsIFile);
122 for (let prof in iter(services.profile.profiles))
123 if (prof.QueryInterface(Ci.nsIToolkitProfile).rootDir.path === dir.path)
129 * @property {Modes.Mode} The current main mode.
130 * @see modes#mainModes
132 mode: deprecated("modes.main", {
133 get: function mode() modes.main,
134 set: function mode(val) modes.main = val
138 function dispatch(node, name) {
139 let event = node.ownerDocument.createEvent("Events");
140 event.initEvent(name, false, false);
141 node.dispatchEvent(event);
144 function addChildren(node, parent) {
145 if (~["menu", "menupopup"].indexOf(node.localName) && node.children.length)
146 dispatch(node, "popupshowing");
148 for (let [, item] in Iterator(node.childNodes)) {
149 if (item.childNodes.length == 0 && item.localName == "menuitem"
151 && !/rdf:http:/.test(item.getAttribute("label"))) { // FIXME
152 item.dactylPath = parent + item.getAttribute("label");
157 if (item.localName == "menu")
158 path += item.getAttribute("label") + ".";
159 addChildren(item, path);
165 addChildren(document.getElementById(config.guioptions["m"][1]), "");
172 NEW_BACKGROUND_TAB: "background-tab",
173 NEW_WINDOW: "window",
176 forceNewWindow: false,
178 version: deprecated("config.version", { get: function version() config.version }),
181 * @property {Object} The map of command-line options. These are
182 * specified in the argument to the host application's -{config.name}
183 * option. E.g. $ firefox -pentadactyl '+u=/tmp/rcfile ++noplugin'
185 * +u RCFILE Use RCFILE instead of .pentadactylrc.
186 * ++noplugin Don't load plugins.
187 * These two can be specified multiple times:
188 * ++cmd CMD Execute an Ex command before initialization.
189 * +c CMD Execute an Ex command after initialization.
191 commandLineOptions: {
192 /** @property Whether plugin loading should be prevented. */
194 /** @property An RC file to use rather than the default. */
196 /** @property An Ex command to run before any initialization is performed. */
198 /** @property An Ex command to run after all initialization has been performed. */
202 registerObserver: function registerObserver(type, callback, weak) {
203 if (!(type in this._observers))
204 this._observers[type] = [];
205 this._observers[type].push(weak ? Cu.getWeakReference(callback) : { get: function () callback });
208 registerObservers: function registerObservers(obj, prop) {
209 for (let [signal, func] in Iterator(obj[prop || "signals"]))
210 this.registerObserver(signal, obj.closure(func), false);
213 unregisterObserver: function unregisterObserver(type, callback) {
214 if (type in this._observers)
215 this._observers[type] = this._observers[type].filter(function (c) c.get() != callback);
218 // TODO: "zoom": if the zoom value of the current buffer changed
219 applyTriggerObserver: function triggerObserver(type, args) {
220 if (type in this._observers)
221 this._observers[type] = this._observers[type].filter(function (callback) {
222 if (callback.get()) {
225 callback.get().apply(null, args);
227 catch (e if e.message == "can't wrap XML objects") {
229 callback.get().apply(null, [String(args[0])].concat(args.slice(1)));
233 dactyl.reportError(e);
240 triggerObserver: function triggerObserver(type) {
241 return this.applyTriggerObserver(type, Array.slice(arguments, 1));
244 addUsageCommand: function (params) {
245 function keys(item) (item.names || [item.name]).concat(item.description, item.columns || []);
247 let name = commands.add(params.name, params.description,
249 let results = array(params.iterate(args))
250 .sort(function (a, b) String.localeCompare(a.name, b.name));
252 let filters = args.map(function (arg) util.regexp("\\b" + util.regexp.escape(arg) + "\\b", "i"));
254 results = results.filter(function (item) filters.every(function (re) keys(item).some(re.closure.test)));
256 commandline.commandOutput(
257 template.usage(results, params.format));
261 completer: function (context, args) {
262 context.keys.text = util.identity;
263 context.keys.description = function () seen[this.text] + /*L*/" matching items";
265 context.completions = array(keys(item).join(" ").toLowerCase().split(/[()\s]+/)
266 for (item in params.iterate(args)))
267 .flatten().filter(function (w) /^\w[\w-_']+$/.test(w))
269 seen[k] = (seen[k] || 0) + 1;
273 options: params.options || []
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));
281 let tags = services["dactyl:"].HELP_TAGS;
282 for (let obj in values(results)) {
283 let res = dactyl.generateHelp(obj, null, null, true);
284 if (!Set.has(tags, obj.helpTag))
285 res[1].@tag = obj.helpTag;
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.
298 this.triggerObserver("beep");
299 if (options["visualbell"]) {
301 bell: document.getElementById("dactyl-bell"),
302 strut: document.getElementById("dactyl-bell-strut")
304 XML.ignoreWhitespace = true;
306 util.overlayWindow(window, {
309 <window id={document.documentElement.id} xmlns={XUL}>
310 <hbox style="display: none" highlight="Bell" id="dactyl-bell" key="bell"/>
314 <window id={document.documentElement.id} xmlns={XUL}>
315 <hbox style="display: none" highlight="Bell" id="dactyl-bell-strut" key="strut"/>
320 elems.bell.style.height = window.innerHeight + "px";
321 elems.strut.style.marginBottom = -window.innerHeight + "px";
322 elems.strut.style.display = elems.bell.style.display = "";
324 util.timeout(function () { elems.strut.style.display = elems.bell.style.display = "none"; }, 20);
327 let soundService = Cc["@mozilla.org/sound;1"].getService(Ci.nsISound);
333 * Reads a string from the system clipboard.
335 * This is same as Firefox's readFromClipboard function, but is needed for
336 * apps like Thunderbird which do not provide it.
340 clipboardRead: function clipboardRead(getClipboard) {
342 const clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
343 const transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
345 transferable.addDataFlavor("text/unicode");
347 let source = clipboard[getClipboard || !clipboard.supportsSelectionClipboard() ?
348 "kGlobalClipboard" : "kSelectionClipboard"];
349 clipboard.getData(transferable, source);
351 let str = {}, len = {};
352 transferable.getTransferData("text/unicode", str, len);
355 return str.value.QueryInterface(Ci.nsISupportsString)
356 .data.substr(0, len.value / 2);
363 * Copies a string to the system clipboard. If *verbose* is specified the
364 * copied string is also echoed to the command line.
366 * @param {string} str
367 * @param {boolean} verbose
369 clipboardWrite: function clipboardWrite(str, verbose) {
370 const clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
371 clipboardHelper.copyString(str);
374 let message = { message: _("dactyl.yank", str) };
376 message.domains = [util.newURI(str).host];
379 dactyl.echomsg(message);
383 dump: deprecated("util.dump",
384 { get: function dump() util.closure.dump }),
385 dumpStack: deprecated("util.dumpStack",
386 { get: function dumpStack() util.closure.dumpStack }),
389 * Outputs a plain message to the command line.
391 * @param {string} str The message to output.
392 * @param {number} flags These control the multi-line message behavior.
393 * See {@link CommandLine#echo}.
395 echo: function echo(str, flags) {
396 commandline.echo(str, commandline.HL_NORMAL, flags);
400 * Outputs an error message to the command line.
402 * @param {string} str The message to output.
403 * @param {number} flags These control the multi-line message behavior.
404 * See {@link CommandLine#echo}.
406 echoerr: function echoerr(str, flags) {
407 flags |= commandline.APPEND_TO_MESSAGES;
409 if (isinstance(str, ["DOMException", "Error", "Exception"]) || isinstance(str, ["XPCWrappedNative_NoHelper"]) && /^\[Exception/.test(str))
410 dactyl.reportError(str);
411 if (isObject(str) && "echoerr" in str)
413 else if (isinstance(str, ["Error", FailedAssertion]) && str.fileName)
414 str = <>{str.fileName.replace(/^.* -> /, "")}: {str.lineNumber}: {str}</>;
416 if (options["errorbells"])
419 commandline.echo(str, commandline.HL_ERRORMSG, flags);
423 * Outputs a warning message to the command line.
425 * @param {string} str The message to output.
426 * @param {number} flags These control the multi-line message behavior.
427 * See {@link CommandLine#echo}.
429 warn: function warn(str, flags) {
430 commandline.echo(str, "WarningMsg", flags | commandline.APPEND_TO_MESSAGES);
433 // TODO: add proper level constants
435 * Outputs an information message to the command line.
437 * @param {string} str The message to output.
438 * @param {number} verbosity The messages log level (0 - 15). Only
439 * messages with verbosity less than or equal to the value of the
440 * *verbosity* option will be output.
441 * @param {number} flags These control the multi-line message behavior.
442 * See {@link CommandLine#echo}.
444 echomsg: function echomsg(str, verbosity, flags) {
445 if (verbosity == null)
446 verbosity = 0; // verbosity level is exclusionary
448 if (options["verbose"] >= verbosity)
449 commandline.echo(str, commandline.HL_INFOMSG,
450 flags | commandline.APPEND_TO_MESSAGES);
454 * Loads and executes the script referenced by *uri* in the scope of the
457 * @param {string} uri The URI of the script to load. Should be a local
458 * chrome:, file:, or resource: URL.
459 * @param {Object} context The context object into which the script
462 loadScript: function (uri, context) {
463 JSMLoader.loadSubScript(uri, context, File.defaultEncoding);
466 userEval: function (str, context, fileName, lineNumber) {
468 if (jsmodules.__proto__ != window)
469 str = "with (window) { with (modules) { (this.eval || eval)(" + str.quote() + ") } }";
471 let info = contexts.context;
472 if (fileName == null)
473 if (info && info.file[0] !== "[")
474 ({ file: fileName, line: lineNumber, context: ctxt }) = info;
476 if (!context && fileName && fileName[0] !== "[")
477 context = ctxt || _userContext;
479 if (isinstance(context, ["Sandbox"]))
480 return Cu.evalInSandbox(str, context, "1.8", fileName, lineNumber);
484 context = userContext || ctxt;
486 context[EVAL_ERROR] = null;
487 context[EVAL_STRING] = str;
488 context[EVAL_RESULT] = null;
489 this.loadScript("resource://dactyl-content/eval.js", context);
490 if (context[EVAL_ERROR]) {
492 context[EVAL_ERROR].fileName = info.file;
493 context[EVAL_ERROR].lineNumber += info.line;
496 throw context[EVAL_ERROR];
498 return context[EVAL_RESULT];
501 delete context[EVAL_ERROR];
502 delete context[EVAL_RESULT];
503 delete context[EVAL_STRING];
508 * Acts like the Function builtin, but the code executes in the
509 * userContext global.
511 userFunc: function () {
512 return this.userEval(
513 "(function userFunction(" + Array.slice(arguments, 0, -1).join(", ") + ")" +
514 " { " + arguments[arguments.length - 1] + " })");
518 * Execute an Ex command string. E.g. ":zoom 300".
520 * @param {string} str The command to execute.
521 * @param {Object} modifiers Any modifiers to be passed to
522 * {@link Command#action}.
523 * @param {boolean} silent Whether the command should be echoed on the
526 execute: function (str, modifiers, silent) {
527 // skip comments and blank lines
528 if (/^\s*("|$)/.test(str))
531 modifiers = modifiers || {};
534 commands.lastCommand = str.replace(/^\s*:\s*/, "");
536 for (let [command, args] in commands.parseCommands(str.replace(/^'(.*)'$/, "$1"))) {
537 if (command === null)
538 throw FailedAssertion(_("dactyl.notCommand", config.appName, args.commandString));
540 res = res && command.execute(args, modifiers);
545 focus: function focus(elem, flags) {
546 flags = flags || services.focus.FLAG_BYMOUSE;
548 if (elem instanceof Document)
549 elem = elem.defaultView;
550 if (elem instanceof Element)
551 services.focus.setFocus(elem, flags);
552 else if (elem instanceof Window)
553 services.focus.focusedWindow = elem;
562 * Focuses the content window.
564 * @param {boolean} clearFocusedElement Remove focus from any focused
567 focusContent: function focusContent(clearFocusedElement) {
568 if (window != services.focus.activeWindow)
571 let win = document.commandDispatcher.focusedWindow;
572 let elem = config.mainWidget || content;
574 // TODO: make more generic
576 if (this.has("mail") && !config.isComposeWindow) {
577 let i = gDBView.selection.currentIndex;
578 if (i == -1 && gDBView.rowCount >= 0)
580 gDBView.selection.select(i);
583 let frame = buffer.focusedFrame;
584 if (frame && frame.top == content && !Editor.getEditor(frame))
590 if (clearFocusedElement) {
591 if (dactyl.focusedElement)
592 dactyl.focusedElement.blur();
593 if (win && Editor.getEditor(win)) {
594 this.withSavedValues(["ignoreFocus"], function _focusContent() {
595 this.ignoreFocus = true;
596 if (win.frameElement)
597 win.frameElement.blur();
599 if (content.document.activeElement instanceof HTMLIFrameElement)
600 content.document.activeElement.blur();
605 if (elem instanceof Window && Editor.getEditor(elem))
608 if (elem && elem != dactyl.focusedElement)
612 /** @property {Element} The currently focused element. */
613 get focusedElement() services.focus.getFocusedElementForWindow(window, true, {}),
614 set focusedElement(elem) dactyl.focus(elem),
617 * Returns whether this Dactyl extension supports *feature*.
619 * @param {string} feature The feature name.
622 has: function (feature) Set.has(config.features, feature),
625 * Returns the URL of the specified help *topic* if it exists.
627 * @param {string} topic The help topic to look up.
628 * @param {boolean} consolidated Whether to search the consolidated help page.
631 findHelp: function (topic, consolidated) {
632 if (!consolidated && topic in services["dactyl:"].FILE_MAP)
634 let items = completion._runCompleter("help", topic, null, !!consolidated).items;
635 let partialMatch = null;
637 function format(item) item.description + "#" + encodeURIComponent(item.text);
639 for (let [i, item] in Iterator(items)) {
640 if (item.text == topic)
642 else if (!partialMatch && topic)
647 return format(partialMatch);
654 initDocument: function initDocument(doc) {
656 if (doc.location.protocol === "dactyl:") {
668 * Initialize the help system.
670 initHelp: function (force) {
671 // Waits for the add-on to become available, if necessary.
675 if (force || !this.helpInitialized) {
676 if ("noscriptOverlay" in window) {
677 noscriptOverlay.safeAllow("chrome-data:", true, false);
678 noscriptOverlay.safeAllow("dactyl:", true, false);
681 // Find help and overlay files with the given name.
682 let findHelpFile = function findHelpFile(file) {
684 for (let [, namespace] in Iterator(namespaces)) {
685 let url = ["dactyl://", namespace, "/", file, ".xml"].join("");
686 let res = util.httpGet(url);
688 if (res.responseXML.documentElement.localName == "document")
690 if (res.responseXML.documentElement.localName == "overlay")
691 overlayMap[file] = url;
692 result.push(res.responseXML);
697 // Find the tags in the document.
698 let addTags = function addTags(file, doc) {
699 for (let elem in util.evaluateXPath("//@tag|//dactyl:tags/text()|//dactyl:tag/text()", doc))
700 for (let tag in values((elem.value || elem.textContent).split(/\s+/)))
704 let namespaces = ["locale-local", "locale"];
705 services["dactyl:"].init({});
707 let tagMap = services["dactyl:"].HELP_TAGS;
708 let fileMap = services["dactyl:"].FILE_MAP;
709 let overlayMap = services["dactyl:"].OVERLAY_MAP;
711 // Scrape the list of help files from all.xml
712 // Manually process main and overlay files, since XSLTProcessor and
713 // XMLHttpRequest don't allow access to chrome documents.
714 tagMap["all"] = tagMap["all.xml"] = "all";
715 tagMap["versions"] = tagMap["versions.xml"] = "versions";
716 let files = findHelpFile("all").map(function (doc)
717 [f.value for (f in util.evaluateXPath("//dactyl:include/@href", doc))]);
719 // Scrape the tags from the rest of the help files.
720 array.flatten(files).forEach(function (file) {
721 tagMap[file + ".xml"] = file;
722 findHelpFile(file).forEach(function (doc) {
727 // Process plugin help entries.
728 XML.ignoreWhiteSpace = XML.prettyPrinting = false;
731 for (let [, context] in Iterator(plugins.contexts))
733 let info = contexts.getDocs(context);
734 if (info instanceof XML) {
735 if (info.*.@lang.length()) {
736 let lang = config.bestLocale(String(a) for each (a in info.*.@lang));
738 info.* = info.*.(function::attribute("lang").length() == 0 || @lang == lang);
740 for each (let elem in info.NS::info)
741 for each (let attr in ["@name", "@summary", "@href"])
742 if (elem[attr].length())
743 info[attr] = elem[attr];
745 body += <h2 xmlns={NS.uri} tag={info.@name + '-plugin'}>{info.@summary}</h2> +
754 '<?xml version="1.0"?>\n' +
755 '<?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>\n' +
756 '<!DOCTYPE document SYSTEM "resource://dactyl-content/dactyl.dtd">\n' +
758 name="plugins" title={config.appName + " Plugins"}>
759 <h1 tag="using-plugins">{_("help.title.Using Plugins")}</h1>
763 </document>.toXMLString();
764 fileMap["plugins"] = function () ['text/xml;charset=UTF-8', help];
766 fileMap["versions"] = function () {
767 let NEWS = util.httpGet(config.addon.getResourceURI("NEWS").spec,
768 { mimeType: "text/plain;charset=UTF-8" })
771 let re = util.regexp(<![CDATA[
772 ^ (?P<comment> \s* # .*\n)
775 (?P<char> [-•*+]) \ //
777 (?: \2\ \ .*\n | \s*\n)* )
781 (?:[^-•*+\s] | [-•*+]\S)
786 | (?: ^ [^\S\n]* \n) +
789 let betas = util.regexp(/\[(b\d)\]/, "gx");
791 let beta = array(betas.iterate(NEWS))
792 .map(function (m) m[1]).uniq().slice(-1)[0];
794 default xml namespace = NS;
795 function rec(text, level, li) {
796 XML.ignoreWhitespace = XML.prettyPrinting = false;
799 let list, space, i = 0;
801 for (let match in re.iterate(text)) {
804 else if (match.char) {
808 li.* += rec(match.content.replace(RegExp("^" + match.space, "gm"), ""), level + 1, li);
811 else if (match.par) {
812 let [, par, tags] = /([^]*?)\s*((?:\[[^\]]+\])*)\n*$/.exec(match.par);
814 tags = array(betas.iterate(tags)).map(function (m) m[1]);
816 let group = !tags.length ? "" :
817 !tags.some(function (t) t == beta) ? "HelpNewsOld" : "HelpNewsNew";
819 li.@highlight = group;
824 if (level == 0 && /^.*:\n$/.test(match.par)) {
825 let text = par.slice(0, -1);
826 res += <h2 tag={"news-" + text}>{template.linkifyHelp(text, true)}</h2>;
829 let [, a, b] = /^(IMPORTANT:?)?([^]*)/.exec(par);
830 res += <p highlight={group + " HelpNews"}>{
832 <hl key="HelpNewsTag">{tags.join(" ")}</hl>
834 a ? <hl key="HelpWarning">{a}</hl> : ""
836 template.linkifyHelp(b, true)
842 for each (let attr in res..@highlight) {
843 attr.parent().@NS::highlight = attr;
844 delete attr.parent().@highlight;
849 XML.ignoreWhitespace = XML.prettyPrinting = false;
850 let body = rec(NEWS, 0);
851 for each (let li in body..li) {
852 let list = li..li.(@NS::highlight == "HelpNewsOld");
853 if (list.length() && list.length() == li..li.(@NS::highlight != "").length()) {
854 for each (let li in list)
855 li.@NS::highlight = "";
856 li.@NS::highlight = "HelpNewsOld";
860 return ["application/xml",
861 '<?xml version="1.0"?>\n' +
862 '<?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>\n' +
863 '<!DOCTYPE document SYSTEM "resource://dactyl-content/dactyl.dtd">\n' +
864 <document xmlns={NS} xmlns:dactyl={NS}
865 name="versions" title={config.appName + " Versions"}>
866 <h1 tag="versions news NEWS">{config.appName} Versions</h1>
870 </document>.toXMLString()
873 addTags("versions", util.httpGet("dactyl://help/versions").responseXML);
874 addTags("plugins", util.httpGet("dactyl://help/plugins").responseXML);
876 default xml namespace = NS;
878 overlayMap["index"] = ['text/xml;charset=UTF-8',
879 '<?xml version="1.0"?>\n' +
880 <overlay xmlns={NS}>{
881 template.map(dactyl.indices, function ([name, iter])
882 <dl insertafter={name + "-index"}>{
883 template.map(iter(), util.identity)
884 }</dl>, <>{"\n\n"}</>)
886 addTags("index", util.httpGet("dactyl://help-overlay/index").responseXML);
888 overlayMap["gui"] = ['text/xml;charset=UTF-8',
889 '<?xml version="1.0"?>\n' +
891 <dl insertafter="dialog-list">{
892 template.map(config.dialogs, function ([name, val])
893 (!val[2] || val[2]())
894 ? <><dt>{name}</dt><dd>{val[0]}</dd></>
901 this.helpInitialized = true;
905 stringifyXML: function (xml) {
906 XML.prettyPrinting = false;
907 XML.ignoreWhitespace = false;
908 return UTF8(xml.toXMLString());
911 exportHelp: JavaScript.setCompleter(function (path) {
912 const FILE = io.File(path);
913 const PATH = FILE.leafName.replace(/\..*/, "") + "/";
914 const TIME = Date.now();
916 if (!FILE.exists() && (/\/$/.test(path) && !/\./.test(FILE.leafName)))
917 FILE.create(FILE.DIRECTORY_TYPE, octal(755));
920 if (FILE.isDirectory()) {
921 var addDataEntry = function addDataEntry(file, data) FILE.child(file).write(data);
922 var addURIEntry = function addURIEntry(file, uri) addDataEntry(file, util.httpGet(uri).responseText);
925 var zip = services.ZipWriter();
926 zip.open(FILE, File.MODE_CREATE | File.MODE_WRONLY | File.MODE_TRUNCATE);
928 addURIEntry = function addURIEntry(file, uri)
929 zip.addEntryChannel(PATH + file, TIME, 9,
930 services.io.newChannel(uri, null, null), false);
931 addDataEntry = function addDataEntry(file, data) // Unideal to an extreme.
932 addURIEntry(file, "data:text/plain;charset=UTF-8," + encodeURI(data));
935 let empty = Set("area base basefont br col frame hr img input isindex link meta param"
938 switch(node.nodeType) {
939 case Node.ELEMENT_NODE:
940 if (isinstance(node, [HTMLBaseElement]))
943 data.push("<"); data.push(node.localName);
944 if (node instanceof HTMLHtmlElement)
945 data.push(" xmlns=" + XHTML.uri.quote(),
946 " xmlns:dactyl=" + NS.uri.quote());
948 for (let { name, value } in array.iterValues(node.attributes)) {
949 if (name == "dactyl:highlight") {
950 Set.add(styles, value);
952 value = "hl-" + value;
954 if (name == "href") {
955 value = node.href || value;
956 if (value.indexOf("dactyl://help-tag/") == 0) {
957 let uri = services.io.newChannel(value, null, null).originalURI;
958 value = uri.spec == value ? "javascript:;" : uri.path.substr(1);
960 if (!/^#|[\/](#|$)|^[a-z]+:/.test(value))
961 value = value.replace(/(#|$)/, ".xhtml$1");
963 if (name == "src" && value.indexOf(":") > 0) {
964 chromeFiles[value] = value.replace(/.*\//, "");
965 value = value.replace(/.*\//, "");
968 data.push(" ", name, '="',
969 <>{value}</>.toXMLString().replace(/"/g, """),
972 if (node.localName in empty)
976 if (node instanceof HTMLHeadElement)
977 data.push(<link rel="stylesheet" type="text/css" href="help.css"/>.toXMLString());
978 Array.map(node.childNodes, fix);
979 data.push("</", node.localName, ">");
983 data.push(<>{node.textContent}</>.toXMLString());
987 let chromeFiles = {};
989 for (let [file, ] in Iterator(services["dactyl:"].FILE_MAP)) {
990 let url = "dactyl://help/" + file;
992 util.waitFor(function () content.location.href == url && buffer.loaded
993 && content.document.documentElement instanceof HTMLHtmlElement,
995 events.waitForPageLoad();
997 '<?xml version="1.0" encoding="UTF-8"?>\n',
998 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"\n',
999 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'
1001 fix(content.document.documentElement);
1002 addDataEntry(file + ".xhtml", data.join(""));
1005 let data = [h for (h in highlight) if (Set.has(styles, h.class) || /^Help/.test(h.class))]
1006 .map(function (h) h.selector
1007 .replace(/^\[.*?=(.*?)\]/, ".hl-$1")
1008 .replace(/html\|/g, "") + "\t" + "{" + h.cssText + "}")
1010 addDataEntry("help.css", data.replace(/chrome:[^ ")]+\//g, ""));
1012 addDataEntry("tag-map.json", JSON.stringify(services["dactyl:"].HELP_TAGS));
1014 let m, re = /(chrome:[^ ");]+\/)([^ ");]+)/g;
1015 while ((m = re.exec(data)))
1016 chromeFiles[m[0]] = m[2];
1018 for (let [uri, leaf] in Iterator(chromeFiles))
1019 addURIEntry(leaf, uri);
1023 }, [function (context, args) completion.file(context)]),
1026 * Generates a help entry and returns it as a string.
1028 * @param {Command|Map|Option} obj A dactyl *Command*, *Map* or *Option*
1030 * @param {XMLList} extraHelp Extra help text beyond the description.
1033 generateHelp: function generateHelp(obj, extraHelp, str, specOnly) {
1034 default xml namespace = "";
1036 let link, tag, spec;
1037 link = tag = spec = util.identity;
1040 if (obj instanceof Command) {
1041 link = function (cmd) <ex>{cmd}</ex>;
1042 args = obj.parseArgs("", CompletionContext(str || ""));
1043 spec = function (cmd) <>{
1044 obj.count ? <oa>count</oa> : <></>
1048 obj.bang ? <oa>!</oa> : <></>
1051 else if (obj instanceof Map) {
1052 spec = function (map) obj.count ? <><oa>count</oa>{map}</> : <>{map}</>;
1053 link = function (map) {
1054 let [, mode, name, extra] = /^(?:(.)_)?(?:<([^>]+)>)?(.*)$/.exec(map);
1055 let k = <k>{extra}</k>;
1063 else if (obj instanceof Option) {
1064 tag = spec = function (name) <>'{name}'</>;
1065 link = function (opt, name) <o>{name}</o>;
1066 args = { value: "", values: [] };
1069 XML.prettyPrinting = false;
1070 XML.ignoreWhitespace = false;
1071 default xml namespace = NS;
1073 // E4X has its warts.
1078 <dt>{link(obj.helpTag || obj.name, obj.name)}</dt> <dd>{
1079 template.linkifyHelp(obj.description ? obj.description.replace(/\.$/, "") : "", true)
1082 return res.elements();
1086 <tags>{template.map(obj.names.slice().reverse(), tag, " ")}</tags>
1088 spec(template.highlightRegexp((obj.specs || obj.names)[0],
1090 function (m, n0) <oa>{n0}</oa>))
1093 <type>{obj.type}</type>
1094 <default>{obj.stringDefaultValue}</default></>}
1096 obj.description ? br + <p>{template.linkifyHelp(obj.description.replace(/\.?$/, "."), true)}</p> : "" }{
1097 extraHelp ? br + extraHelp : "" }{
1098 !(extraHelp || obj.description) ? br + <p><!--L-->Sorry, no help available.</p> : "" }
1103 res.item.description.* += br +
1104 let (br = br + <> </>)
1105 <><dl>{ br + template.map(ary, function ([a, b]) <><dt>{a}</dt> <dd>{b}</dd></>, br) }
1111 add(completion._runCompleter(obj.closure.completer, "", null, args).items
1112 .map(function (i) [i.text, i.description]));
1114 if (obj.options && obj.options.some(function (o) o.description))
1115 add(obj.options.filter(function (o) o.description)
1119 o.names.length == 1 ? "" :
1121 template.map(o.names.slice(1), function (n) <em>{n}</em>, <>, </>)
1125 return res.*.toXMLString()
1126 .replace(' xmlns="' + NS + '"', "", "g")
1127 .replace(/^ {12}|[ \t]+$/gm, "")
1128 .replace(/^\s*\n|\n\s*$/g, "") + "\n";
1132 * Opens the help page containing the specified *topic* if it exists.
1134 * @param {string} topic The help topic to open.
1135 * @param {boolean} consolidated Whether to use the consolidated help page.
1137 help: function (topic, consolidated) {
1140 let helpFile = consolidated ? "all" : options["helpfile"];
1142 if (helpFile in services["dactyl:"].FILE_MAP)
1143 dactyl.open("dactyl://help/" + helpFile, { from: "help" });
1145 dactyl.echomsg(_("help.noFile", helpFile.quote()));
1149 let page = this.findHelp(topic, consolidated);
1150 dactyl.assert(page != null, _("help.noTopic", topic));
1152 dactyl.open("dactyl://help/" + page, { from: "help" });
1156 * The map of global variables.
1158 * These are set and accessed with the "g:" prefix.
1160 _globalVariables: {},
1161 globalVariables: deprecated(_("deprecated.for.theOptionsSystem"), {
1162 get: function globalVariables() this._globalVariables
1165 loadPlugins: function (args, force) {
1166 function sourceDirectory(dir) {
1167 dactyl.assert(dir.isReadable(), _("io.notReadable", dir.path));
1169 dactyl.log(_("dactyl.sourcingPlugins", dir.path), 3);
1171 let loadplugins = options.get("loadplugins");
1173 loadplugins = { __proto__: loadplugins, value: args.map(Option.parseRegexp) };
1175 dir.readDirectory(true).forEach(function (file) {
1176 if (file.isFile() && loadplugins.getKey(file.path)
1177 && !(!force && file.path in dactyl.pluginFiles && dactyl.pluginFiles[file.path] >= file.lastModifiedTime)) {
1179 io.source(file.path);
1180 dactyl.pluginFiles[file.path] = file.lastModifiedTime;
1183 dactyl.reportError(e);
1186 else if (file.isDirectory())
1187 sourceDirectory(file);
1191 let dirs = io.getRuntimeDirectories("plugins");
1193 if (dirs.length == 0) {
1194 dactyl.log(_("dactyl.noPluginDir"), 3);
1199 _("plugin.searchingForIn",
1200 ("plugins/**/*.{js," + config.fileExtension + "}").quote(),
1201 [dir.path.replace(/.plugins$/, "") for ([, dir] in Iterator(dirs))]
1202 .join(",").quote()),
1205 dirs.forEach(function (dir) {
1206 dactyl.echomsg(_("plugin.searchingFor", (dir.path + "/**/*.{js," + config.fileExtension + "}").quote()), 3);
1207 sourceDirectory(dir);
1211 // TODO: add proper level constants
1213 * Logs a message to the JavaScript error console. Each message has an
1214 * associated log level. Only messages with a log level less than or equal
1215 * to *level* will be printed. If *msg* is an object, it is pretty printed.
1217 * @param {string|Object} msg The message to print.
1218 * @param {number} level The logging level 0 - 15.
1220 log: function (msg, level) {
1221 let verbose = localPrefs.get("loglevel", 0);
1223 if (!level || level <= verbose) {
1224 if (isObject(msg) && !isinstance(msg, _))
1225 msg = util.objectToString(msg, false);
1227 services.console.logStringMessage(config.name + ": " + msg);
1231 onClick: function onClick(event) {
1232 if (event.originalTarget instanceof Element) {
1233 let command = event.originalTarget.getAttributeNS(NS, "command");
1234 if (command && event.button == 0) {
1235 event.preventDefault();
1237 if (dactyl.commands[command])
1238 dactyl.withSavedValues(["forceNewTab"], function () {
1239 dactyl.forceNewTab = event.ctrlKey || event.shiftKey || event.button == 1;
1240 dactyl.commands[command](event);
1246 onExecute: function onExecute(event) {
1247 let cmd = event.originalTarget.getAttribute("dactyl-execute");
1248 commands.execute(cmd, null, false, null,
1249 { file: /*L*/"[Command Line]", line: 1 });
1253 * Opens one or more URLs. Returns true when load was initiated, or
1256 * @param {string|Array} urls A representation of the URLs to open. May be
1257 * either a string, which will be passed to
1258 * {@see Dactyl#parseURLs}, or an array in the same format as
1259 * would be returned by the same.
1260 * @param {object} params A set of parameters specifying how to open the
1261 * URLs. The following properties are recognized:
1263 * • background If true, new tabs are opened in the background.
1265 * • from The designation of the opener, as appears in
1266 * 'activate' and 'newtab' options. If present,
1267 * the newtab option provides the default 'where'
1268 * parameter, and the value of the 'activate'
1269 * parameter is inverted if 'background' is true.
1271 * • where One of CURRENT_TAB, NEW_TAB, or NEW_WINDOW
1273 * As a deprecated special case, the where parameter may be provided
1274 * by itself, in which case it is transformed into { where: params }.
1276 * @param {boolean} force Don't prompt whether to open more than 20
1278 * @returns {boolean}
1280 open: function (urls, params, force) {
1281 if (typeof urls == "string")
1282 urls = dactyl.parseURLs(urls);
1284 if (urls.length > prefs.get("browser.tabs.maxOpenBeforeWarn", 20) && !force)
1285 return commandline.input(_("dactyl.prompt.openMany", urls.length) + " ",
1287 if (resp && resp.match(/^y(es)?$/i))
1288 dactyl.open(urls, params, true);
1291 params = params || {};
1292 if (isString(params))
1293 params = { where: params };
1296 for (let [opt, flag] in Iterator({ replace: "REPLACE_HISTORY", hide: "BYPASS_HISTORY" }))
1297 flags |= params[opt] && Ci.nsIWebNavigation["LOAD_FLAGS_" + flag];
1299 let where = params.where || dactyl.CURRENT_TAB;
1300 let background = ("background" in params) ? params.background
1301 : params.where == dactyl.NEW_BACKGROUND_TAB;
1303 if (params.from && dactyl.has("tabs")) {
1304 if (!params.where && options.get("newtab").has(params.from))
1305 where = dactyl.NEW_TAB;
1306 background ^= !options.get("activate").has(params.from);
1309 if (urls.length == 0)
1312 let browser = config.tabbrowser;
1313 function open(urls, where) {
1315 let url = Array.concat(urls)[0];
1316 let postdata = Array.concat(urls)[1];
1318 // decide where to load the first url
1321 case dactyl.NEW_TAB:
1322 if (!dactyl.has("tabs"))
1323 return open(urls, dactyl.NEW_WINDOW);
1325 return prefs.withContext(function () {
1326 prefs.set("browser.tabs.loadInBackground", true);
1327 return browser.loadOneTab(url, null, null, postdata, background).linkedBrowser.contentDocument;
1330 case dactyl.NEW_WINDOW:
1331 let win = window.openDialog(document.documentURI, "_blank", "chrome,all,dialog=no");
1332 util.waitFor(function () win.document.readyState === "complete");
1333 browser = win.dactyl && win.dactyl.modules.config.tabbrowser || win.getBrowser();
1335 case dactyl.CURRENT_TAB:
1336 browser.loadURIWithFlags(url, flags, null, null, postdata);
1337 return browser.contentWindow;
1341 // Unfortunately, failed page loads throw exceptions and
1342 // cause a lot of unwanted noise. This solution means that
1343 // any genuine errors go unreported.
1346 if (dactyl.forceNewTab)
1347 where = dactyl.NEW_TAB;
1348 else if (dactyl.forceNewWindow)
1349 where = dactyl.NEW_WINDOW;
1351 where = dactyl.CURRENT_TAB;
1353 return urls.map(function (url) {
1354 let res = open(url, where);
1355 where = dactyl.NEW_TAB;
1362 * Returns an array of URLs parsed from *str*.
1364 * Given a string like 'google bla, www.osnews.com' return an array
1365 * ['www.google.com/search?q=bla', 'www.osnews.com']
1367 * @param {string} str
1368 * @returns {[string]}
1370 parseURLs: function parseURLs(str) {
1373 if (options["urlseparator"])
1374 urls = util.splitLiteral(str, util.regexp("\\s*" + options["urlseparator"] + "\\s*"));
1378 return urls.map(function (url) {
1381 if (/^(\.{0,2}|~)(\/|$)/.test(url) || util.OS.isWindows && /^[a-z]:/i.test(url)) {
1383 // Try to find a matching file.
1384 let file = io.File(url);
1385 if (file.exists() && file.isReadable())
1386 return services.io.newFileURI(file).spec;
1391 // If it starts with a valid protocol, pass it through.
1392 let proto = /^([-\w]+):/.exec(url);
1393 if (proto && "@mozilla.org/network/protocol;1?name=" + proto[1] in Cc)
1396 // Check for a matching search keyword.
1397 let searchURL = this.has("bookmarks") && bookmarks.getSearchURL(url, false);
1401 // If it looks like URL-ish (foo.com/bar), let Gecko figure it out.
1402 if (this.urlish.test(url) || !this.has("bookmarks"))
1403 return util.createURI(url).spec;
1405 // Pass it off to the default search engine or, failing
1406 // that, let Gecko deal with it as is.
1407 return bookmarks.getSearchURL(url, true) || util.createURI(url).spec;
1410 stringToURLArray: deprecated("dactyl.parseURLs", "parseURLs"),
1411 urlish: Class.memoize(function () util.regexp(<![CDATA[
1413 <domain>+ (:\d+)? (/ .*) |
1415 <domain>+ \. [a-z0-9]+ |
1419 domain: util.regexp(String.replace(<![CDATA[
1421 U0000-U002c // U002d-U002e --.
1424 U003a-U0040 // U0041-U005a a-z
1425 U005b-U0060 // U0061-U007a A-Z
1428 ]]>, /U/g, "\\u"), "x")
1433 get plugins() plugins,
1435 setNodeVisible: function setNodeVisible(node, visible) {
1436 if (window.setToolbarVisibility && node.localName == "toolbar")
1437 window.setToolbarVisibility(node, visible);
1439 node.collapsed = !visible;
1442 confirmQuit: function confirmQuit()
1443 prefs.withContext(function () {
1444 prefs.set("browser.warnOnQuit", false);
1445 return window.canQuitApplication();
1449 * Quit the host application, no matter how many tabs/windows are open.
1451 * @param {boolean} saveSession If true the current session will be
1452 * saved and restored when the host application is restarted.
1453 * @param {boolean} force Forcibly quit irrespective of whether all
1454 * windows could be closed individually.
1456 quit: function (saveSession, force) {
1457 if (!force && !this.confirmQuit())
1460 let pref = "browser.startup.page";
1463 prefs.safeSet(pref, 3);
1464 if (!saveSession && prefs.get(pref) >= 2)
1465 prefs.safeSet(pref, 1);
1467 services.appStartup.quit(Ci.nsIAppStartup[force ? "eForceQuit" : "eAttemptQuit"]);
1471 * Restart the host application.
1473 restart: function () {
1474 if (!this.confirmQuit())
1477 services.appStartup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
1480 get assert() util.assert,
1483 * Traps errors in the called function, possibly reporting them.
1485 * @param {function} func The function to call
1486 * @param {object} self The 'this' object for the function.
1488 trapErrors: function trapErrors(func, self) {
1492 return func.apply(self || this, Array.slice(arguments, 2));
1495 dactyl.reportError(e, true);
1501 * Reports an error to both the console and the host application's
1504 * @param {Object} error The error object.
1506 reportError: function reportError(error, echo) {
1507 if (error instanceof FailedAssertion && error.noTrace || error.message === "Interrupted") {
1508 let context = contexts.context;
1509 let prefix = context ? context.file + ":" + context.line + ": " : "";
1510 if (error.message && error.message.indexOf(prefix) !== 0)
1511 error.message = prefix + error.message;
1514 dactyl.echoerr(template.linkifyHelp(error.message));
1519 util.reportError(error);
1522 if (error.result == Cr.NS_BINDING_ABORTED)
1525 dactyl.echoerr(error, commandline.FORCE_SINGLELINE);
1527 util.reportError(error);
1531 * Parses a Dactyl command-line string i.e. the value of the
1532 * -dactyl command-line option.
1534 * @param {string} cmdline The string to parse for command-line
1537 * @see Commands#parseArgs
1539 parseCommandLine: function (cmdline) {
1541 return commands.get("rehash").parseArgs(cmdline);
1544 dactyl.reportError(e, true);
1549 wrapCallback: function (callback, self) {
1550 self = self || this;
1551 let save = ["forceNewTab", "forceNewWindow"];
1552 let saved = save.map(function (p) dactyl[p]);
1553 return function wrappedCallback() {
1554 let args = arguments;
1555 return dactyl.withSavedValues(save, function () {
1556 saved.forEach(function (p, i) dactyl[save[i]] = p);
1558 return callback.apply(self, args);
1561 dactyl.reportError(e, true);
1568 * @property {[Window]} Returns an array of all the host application's
1571 get windows() [win for (win in iter(services.windowMediator.getEnumerator("navigator:browser"))) if (win.dactyl)],
1574 toolbarHidden: function hidden(elem) (elem.getAttribute("autohide") || elem.getAttribute("collapsed")) == "true"
1576 events: function () {
1577 events.listen(window, "click", dactyl.closure.onClick, true);
1578 events.listen(window, "dactyl.execute", dactyl.closure.onExecute, true);
1580 // Only general options are added here, which are valid for all Dactyl extensions
1581 options: function () {
1582 options.add(["errorbells", "eb"],
1583 "Ring the bell when an error message is displayed",
1586 options.add(["exrc", "ex"],
1587 "Enable automatic sourcing of an RC file in the current directory at startup",
1590 options.add(["fullscreen", "fs"],
1591 "Show the current window fullscreen",
1593 setter: function (value) window.fullScreen = value,
1594 getter: function () window.fullScreen
1600 c: ["Always show the command line, even when empty"],
1601 C: ["Always show the command line outside of the status line"],
1602 M: ["Always show messages outside of the status line"]
1604 setter: function (opts) {
1605 if (loaded.commandline || ~opts.indexOf("c"))
1606 commandline.widgets.updateVisibility();
1611 s: ["Status bar", [statusline.statusBar.id]]
1612 }, config.guioptions),
1613 setter: function (opts) {
1614 for (let [opt, [, ids]] in Iterator(this.opts)) {
1615 ids.map(function (id) document.getElementById(id))
1616 .forEach(function (elem) {
1618 dactyl.setNodeVisible(elem, opts.indexOf(opt) >= 0);
1625 r: ["Right Scrollbar", "vertical"],
1626 l: ["Left Scrollbar", "vertical"],
1627 b: ["Bottom Scrollbar", "horizontal"]
1629 setter: function (opts) {
1630 let dir = ["horizontal", "vertical"].filter(
1631 function (dir) !Array.some(opts,
1632 function (o) this.opts[o] && this.opts[o][1] == dir, this),
1634 let class_ = dir.map(function (dir) "html|html > xul|scrollbar[orient=" + dir + "]");
1636 styles.system.add("scrollbar", "*",
1637 class_.length ? class_.join(", ") + " { visibility: collapse !important; }" : "",
1640 prefs.safeSet("layout.scrollbar.side", opts.indexOf("l") >= 0 ? 3 : 2,
1641 _("option.guioptions.safeSet"));
1643 validator: function (opts) Option.validIf(!(opts.indexOf("l") >= 0 && opts.indexOf("r") >= 0),
1644 UTF8("Only one of ‘l’ or ‘r’ allowed"))
1649 n: ["Tab number", highlight.selector("TabNumber")],
1650 N: ["Tab number over icon", highlight.selector("TabIconNumber")]
1652 setter: function (opts) {
1653 let classes = [v[1] for ([k, v] in Iterator(this.opts)) if (opts.indexOf(k) < 0)];
1655 styles.system.add("taboptions", "chrome://*",
1656 classes.length ? classes.join(",") + "{ display: none; }" : "");
1658 if (!dactyl.has("Gecko2")) {
1659 tabs.tabBinding.enabled = Array.some(opts, function (k) k in this.opts, this);
1660 tabs.updateTabCount();
1662 if (config.tabbrowser.tabContainer._positionPinnedTabs)
1663 config.tabbrowser.tabContainer._positionPinnedTabs();
1666 validator: function (opts) dactyl.has("Gecko2") ||
1667 Option.validIf(!/[nN]/.test(opts), "Tab numbering not available in this " + config.host + " version")
1670 ].filter(function (group) !group.feature || dactyl.has(group.feature));
1672 options.add(["guioptions", "go"],
1673 "Show or hide certain GUI elements like the menu or toolbar",
1674 "charlist", config.defaults.guioptions || "", {
1677 cleanupValue: config.cleanups.guioptions ||
1678 "r" + [k for ([k, v] in iter(groups[1].opts))
1679 if (!Dactyl.toolbarHidden(document.getElementById(v[1][0])))].join(""),
1681 values: array(groups).map(function (g) [[k, v[0]] for ([k, v] in Iterator(g.opts))]).flatten(),
1683 setter: function (value) {
1684 for (let group in values(groups))
1685 group.setter(value);
1686 events.checkFocus();
1689 validator: function (val) Option.validateCompleter.call(this, val) &&
1690 groups.every(function (g) !g.validator || g.validator(val))
1693 options.add(["helpfile", "hf"],
1694 "Name of the main help file",
1697 options.add(["loadplugins", "lpl"],
1698 "A regexp list that defines which plugins are loaded at startup and via :loadplugins",
1699 "regexplist", "'\\.(js|" + config.fileExtension + ")$'");
1701 options.add(["titlestring"],
1702 "The string shown at the end of the window title",
1703 "string", config.defaults.titlestring || config.host,
1705 setter: function (value) {
1706 let win = document.documentElement;
1707 function updateTitle(old, current) {
1708 document.title = document.title.replace(RegExp("(.*)" + util.regexp.escape(old)), "$1" + current);
1711 if (services.has("privateBrowsing")) {
1712 let oldValue = win.getAttribute("titlemodifier_normal");
1713 let suffix = win.getAttribute("titlemodifier_privatebrowsing").substr(oldValue.length);
1715 win.setAttribute("titlemodifier_normal", value);
1716 win.setAttribute("titlemodifier_privatebrowsing", value + suffix);
1718 if (services.privateBrowsing.privateBrowsingEnabled) {
1719 updateTitle(oldValue + suffix, value + suffix);
1724 updateTitle(win.getAttribute("titlemodifier"), value);
1725 win.setAttribute("titlemodifier", value);
1731 options.add(["urlseparator", "urlsep", "us"],
1732 "The regular expression used to separate multiple URLs in :open and friends",
1734 { validator: function (value) RegExp(value) });
1736 options.add(["verbose", "vbs"],
1737 "Define which info messages are displayed",
1739 { validator: function (value) Option.validIf(value >= 0 && value <= 15, "Value must be between 0 and 15") });
1741 options.add(["visualbell", "vb"],
1742 "Use visual bell instead of beeping on errors",
1745 setter: function (value) {
1746 prefs.safeSet("accessibility.typeaheadfind.enablesound", !value,
1747 _("option.visualbell.safeSet"));
1753 mappings: function () {
1754 mappings.add([modes.MAIN], ["<open-help>", "<F1>"],
1755 "Open the introductory help page",
1756 function () { dactyl.help(); });
1758 mappings.add([modes.MAIN], ["<open-single-help>", "<A-F1>"],
1759 "Open the single, consolidated help page",
1760 function () { ex.helpall(); });
1762 if (dactyl.has("session"))
1763 mappings.add([modes.NORMAL], ["ZQ"],
1764 "Quit and don't save the session",
1765 function () { dactyl.quit(false); });
1767 mappings.add([modes.NORMAL], ["ZZ"],
1768 "Quit and save the session",
1769 function () { dactyl.quit(true); });
1772 commands: function () {
1773 commands.add(["dia[log]"],
1774 "Open a " + config.appName + " dialog",
1776 let dialog = args[0];
1778 dactyl.assert(dialog in config.dialogs,
1779 _("error.invalidArgument", dialog));
1780 dactyl.assert(!config.dialogs[dialog][2] || config.dialogs[dialog][2](),
1781 _("dialog.notAvailable", dialog));
1783 config.dialogs[dialog][1]();
1786 dactyl.echoerr(_("error.cantOpen", dialog.quote(), e.message || e));
1790 completer: function (context) {
1791 context.ignoreCase = true;
1792 completion.dialog(context);
1796 commands.add(["em[enu]"],
1797 "Execute the specified menu item from the command line",
1799 let arg = args[0] || "";
1800 let items = dactyl.menuItems;
1802 dactyl.assert(items.some(function (i) i.dactylPath == arg),
1803 _("emenu.notFound", arg));
1805 for (let [, item] in Iterator(items)) {
1806 if (item.dactylPath == arg) {
1807 dactyl.assert(!item.disabled, _("error.disabled", item.dactylPath));
1813 completer: function (context) completion.menuItem(context),
1817 commands.add(["exe[cute]"],
1818 "Execute the argument as an Ex command",
1821 let cmd = dactyl.userEval(args[0] || "");
1822 dactyl.execute(cmd || "", null, true);
1828 completer: function (context) completion.javascript(context),
1835 description: "Open the introductory help page"
1838 description: "Open the single consolidated help page"
1840 ].forEach(function (command) {
1841 let consolidated = command.name == "helpa[ll]";
1843 commands.add([command.name],
1844 command.description,
1846 dactyl.assert(!args.bang, _("help.dontPanic"));
1847 dactyl.help(args.literalArg, consolidated);
1851 completer: function (context) completion.help(context, consolidated),
1856 commands.add(["loadplugins", "lpl"],
1857 "Load all or matching plugins",
1859 dactyl.loadPlugins(args.length ? args : null, args.bang);
1866 serialize: function () [
1869 literalArg: options["loadplugins"].join(" ")
1874 commands.add(["norm[al]"],
1875 "Execute Normal mode commands",
1876 function (args) { events.feedkeys(args[0], args.bang, false, modes.NORMAL); },
1883 commands.add(["exit", "x"],
1884 "Quit " + config.appName,
1886 dactyl.quit(false, args.bang);
1892 commands.add(["q[uit]"],
1893 dactyl.has("tabs") ? "Quit current tab" : "Quit application",
1895 if (dactyl.has("tabs") && tabs.remove(tabs.getTab(), 1, false))
1897 else if (dactyl.windows.length > 1)
1900 dactyl.quit(false, args.bang);
1906 commands.add(["reh[ash]"],
1907 "Reload the " + config.appName + " add-on",
1910 storage.session.rehashCmd = args.trailing; // Hack.
1915 argCount: "0", // FIXME
1919 description: "The initialization file to execute at startup",
1920 type: CommandOption.STRING
1923 names: ["++noplugin"],
1924 description: "Do not automatically load plugins"
1928 description: "Ex commands to execute prior to initialization",
1929 type: CommandOption.STRING,
1934 description: "Ex commands to execute after initialization",
1935 type: CommandOption.STRING,
1941 commands.add(["res[tart]"],
1942 "Force " + config.appName + " to restart",
1943 function () { dactyl.restart(); },
1946 function findToolbar(name) util.evaluateXPath(
1947 "//*[@toolbarname=" + util.escapeString(name, "'") + " or " +
1948 "@toolbarname=" + util.escapeString(name.trim(), "'") + "]",
1949 document).snapshotItem(0);
1951 var toolbox = document.getElementById("navigator-toolbox");
1953 let toolbarCommand = function (names, desc, action, filter) {
1954 commands.add(names, desc,
1956 let toolbar = findToolbar(args[0] || "");
1957 dactyl.assert(toolbar, _("error.invalidArgument"));
1959 events.checkFocus();
1962 completer: function (context) {
1963 completion.toolbar(context);
1965 context.filters.push(filter);
1971 toolbarCommand(["toolbars[how]", "tbs[how]"], "Show the named toolbar",
1972 function (toolbar) dactyl.setNodeVisible(toolbar, true),
1973 function ({ item }) Dactyl.toolbarHidden(item));
1974 toolbarCommand(["toolbarh[ide]", "tbh[ide]"], "Hide the named toolbar",
1975 function (toolbar) dactyl.setNodeVisible(toolbar, false),
1976 function ({ item }) !Dactyl.toolbarHidden(item));
1977 toolbarCommand(["toolbart[oggle]", "tbt[oggle]"], "Toggle the named toolbar",
1978 function (toolbar) dactyl.setNodeVisible(toolbar, Dactyl.toolbarHidden(toolbar)));
1981 commands.add(["time"],
1982 "Profile a piece of code or run a command multiple times",
1984 let count = args.count;
1985 let special = args.bang;
1986 args = args[0] || "";
1989 var func = function () commands.execute(args, null, false);
1991 func = dactyl.userFunc(args);
1995 let each, eachUnits, totalUnits;
1998 for (let i in util.interruptibleRange(0, count, 500)) {
1999 let now = Date.now();
2001 total += Date.now() - now;
2007 if (total / count >= 100) {
2008 each = total / 1000.0 / count;
2012 each = total / count;
2017 total = total / 1000.0;
2021 totalUnits = "msec";
2023 commandline.commandOutput(
2025 <tr highlight="Title" align="left">
2026 <th colspan="3">{_("title.Code execution summary")}</th>
2028 <tr><td>  {_("title.Executed")}:</td><td align="right"><span class="times-executed">{count}</span></td><td><!--L-->times</td></tr>
2029 <tr><td>  {_("title.Average time")}:</td><td align="right"><span class="time-average">{each.toFixed(2)}</span></td><td>{eachUnits}</td></tr>
2030 <tr><td>  {_("title.Total time")}:</td><td align="right"><span class="time-total">{total.toFixed(2)}</span></td><td>{totalUnits}</td></tr>
2034 let beforeTime = Date.now();
2040 let afterTime = Date.now();
2042 if (afterTime - beforeTime >= 100)
2043 dactyl.echo(_("time.total", ((afterTime - beforeTime) / 1000.0).toFixed(2) + " sec"));
2045 dactyl.echo(_("time.total", (afterTime - beforeTime) + " msec"));
2054 completer: function (context) {
2055 if (/^:/.test(context.filter))
2056 return completion.ex(context);
2058 return completion.javascript(context);
2066 commands.add(["verb[ose]"],
2067 "Execute a command with 'verbose' set",
2069 let vbs = options.get("verbose");
2070 let value = vbs.value;
2071 let setFrom = vbs.setFrom;
2074 vbs.set(args.count || 1);
2076 dactyl.execute(args[0] || "", null, true);
2080 vbs.setFrom = setFrom;
2084 completer: function (context) completion.ex(context),
2090 commands.add(["ve[rsion]"],
2091 "Show version information",
2094 dactyl.open("about:");
2096 commandline.commandOutput(<>
2097 {config.appName} {config.version} running on:<br/>{navigator.userAgent}
2106 completion: function () {
2107 completion.dialog = function dialog(context) {
2108 context.title = ["Dialog"];
2109 context.filters.push(function ({ item }) !item[2] || item[2]());
2110 context.completions = [[k, v[0], v[2]] for ([k, v] in Iterator(config.dialogs))];
2113 completion.help = function help(context, consolidated) {
2115 context.title = ["Help"];
2116 context.anchored = false;
2117 context.completions = services["dactyl:"].HELP_TAGS;
2119 context.keys = { text: 0, description: function () "all" };
2122 completion.menuItem = function menuItem(context) {
2123 context.title = ["Menu Path", "Label"];
2124 context.anchored = false;
2127 description: function (item) item.getAttribute("label"),
2128 highlight: function (item) item.disabled ? "Disabled" : ""
2130 context.generate = function () dactyl.menuItems;
2133 var toolbox = document.getElementById("navigator-toolbox");
2134 completion.toolbar = function toolbar(context) {
2135 context.title = ["Toolbar"];
2136 context.keys = { text: function (item) item.getAttribute("toolbarname"), description: function () "" };
2137 context.completions = util.evaluateXPath("//*[@toolbarname]", document);
2140 completion.window = function window(context) {
2141 context.title = ["Window", "Title"];
2142 context.keys = { text: function (win) dactyl.windows.indexOf(win) + 1, description: function (win) win.document.title };
2143 context.completions = dactyl.windows;
2147 dactyl.triggerObserver("load");
2149 dactyl.log(_("dactyl.modulesLoaded"), 3);
2151 dactyl.timeout(function () {
2153 var args = storage.session.commandlineArgs || services.commandLineHandler.optionValue;
2155 args = dactyl.parseCommandLine(args);
2158 dactyl.commandLineOptions.rcFile = args["+u"];
2159 dactyl.commandLineOptions.noPlugins = "++noplugin" in args;
2160 dactyl.commandLineOptions.postCommands = args["+c"];
2161 dactyl.commandLineOptions.preCommands = args["++cmd"];
2162 util.dump("Processing command-line option: " + args.string);
2166 dactyl.echoerr(_("dactyl.parsingCommandLine", e));
2169 dactyl.log(_("dactyl.commandlineOpts", util.objectToString(dactyl.commandLineOptions)), 3);
2171 // first time intro message
2172 const firstTime = "extensions." + config.name + ".firsttime";
2173 if (prefs.get(firstTime, true)) {
2174 dactyl.timeout(function () {
2175 this.withSavedValues(["forceNewTab"], function () {
2176 this.forceNewTab = true;
2178 prefs.set(firstTime, false);
2183 // TODO: we should have some class where all this guioptions stuff fits well
2184 // dactyl.hideGUI();
2186 if (dactyl.userEval("typeof document", null, "test.js") === "undefined")
2187 jsmodules.__proto__ = XPCSafeJSObjectWrapper(window);
2189 if (dactyl.commandLineOptions.preCommands)
2190 dactyl.commandLineOptions.preCommands.forEach(function (cmd) {
2191 dactyl.execute(cmd);
2194 // finally, read the RC file and source plugins
2195 let init = services.environment.get(config.idName + "_INIT");
2196 let rcFile = io.getRCFile("~");
2199 if (dactyl.commandLineOptions.rcFile) {
2200 let filename = dactyl.commandLineOptions.rcFile;
2201 if (!/^(NONE|NORC)$/.test(filename))
2202 io.source(io.File(filename).path, { group: contexts.user });
2206 dactyl.execute(init);
2209 io.source(rcFile.path, { group: contexts.user });
2210 services.environment.set("MY_" + config.idName + "RC", rcFile.path);
2213 dactyl.log(_("dactyl.noRCFile"), 3);
2216 if (options["exrc"] && !dactyl.commandLineOptions.rcFile) {
2217 let localRCFile = io.getRCFile(io.cwd);
2218 if (localRCFile && !localRCFile.equals(rcFile))
2219 io.source(localRCFile.path, { group: contexts.user });
2223 if (dactyl.commandLineOptions.rcFile == "NONE" || dactyl.commandLineOptions.noPlugins)
2224 options["loadplugins"] = [];
2226 if (options["loadplugins"])
2227 dactyl.loadPlugins();
2230 dactyl.reportError(e, true);
2233 // after sourcing the initialization files, this function will set
2234 // all gui options to their default values, if they have not been
2235 // set before by any RC file
2236 for (let option in values(options.needInit))
2239 if (dactyl.commandLineOptions.postCommands)
2240 dactyl.commandLineOptions.postCommands.forEach(function (cmd) {
2241 dactyl.execute(cmd);
2244 if (storage.session.rehashCmd)
2245 dactyl.execute(storage.session.rehashCmd);
2246 storage.session.rehashCmd = null;
2248 dactyl.fullyInitialized = true;
2249 dactyl.triggerObserver("enter", null);
2250 autocommands.trigger("Enter", {});
2253 statusline.update();
2254 dactyl.log(_("dactyl.initialized", config.appName), 0);
2255 dactyl.initialized = true;
2259 // vim: set fdm=marker sw=4 ts=4 et: