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 userContext = { __proto__: modules };
16 var _userContext = newContext(userContext);
18 var EVAL_ERROR = "__dactyl_eval_error";
19 var EVAL_RESULT = "__dactyl_eval_result";
20 var EVAL_STRING = "__dactyl_eval_string";
22 var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
25 // cheap attempt at compatibility
26 let prop = { get: deprecated("dactyl", function liberator() dactyl) };
27 Object.defineProperty(window, "liberator", prop);
28 Object.defineProperty(modules, "liberator", prop);
31 this.modules = modules;
33 util.addObserver(this);
35 this.commands["dactyl.help"] = function (event) {
36 let elem = event.originalTarget;
37 dactyl.help(elem.getAttribute("tag") || elem.textContent);
39 this.commands["dactyl.restart"] = function (event) {
43 styles.registerSheet("resource://dactyl-skin/dactyl.css");
46 cleanup: function () {
48 delete window.liberator;
50 styles.unregisterSheet("resource://dactyl-skin/dactyl.css");
53 destroy: function () {
54 autocommands.trigger("LeavePre", {});
55 dactyl.triggerObserver("shutdown", null);
56 util.dump("All dactyl modules destroyed\n");
57 autocommands.trigger("Leave", {});
61 "dactyl-cleanup": function dactyl_cleanup() {
62 let modules = dactyl.modules;
64 for (let mod in values(modules.moduleList.reverse())) {
67 this.trapErrors("cleanup", mod);
69 this.trapErrors("destroy", mod);
72 for (let mod in values(modules.ownPropertyValues.reverse()))
73 if (mod instanceof Class && "INIT" in mod && "cleanup" in mod.INIT)
74 this.trapErrors(mod.cleanup, mod, dactyl, modules, window);
76 for (let name in values(Object.getOwnPropertyNames(modules).reverse()))
81 modules.__proto__ = {};
85 /** @property {string} The name of the current user profile. */
86 profileName: Class.memoize(function () {
87 // NOTE: services.profile.selectedProfile.name doesn't return
88 // what you might expect. It returns the last _actively_ selected
89 // profile (i.e. via the Profile Manager or -P option) rather than the
90 // current profile. These will differ if the current process was run
91 // without explicitly selecting a profile.
93 let dir = services.directory.get("ProfD", Ci.nsIFile);
94 for (let prof in iter(services.profile.profiles))
95 if (prof.QueryInterface(Ci.nsIToolkitProfile).rootDir.path === dir.path)
101 * @property {number} The current main mode.
102 * @see modes#mainModes
104 mode: deprecated("modes.main", {
105 get: function mode() modes.main,
106 set: function mode(val) modes.main = val
109 get menuItems() Dactyl.getMenuItems(),
114 NEW_BACKGROUND_TAB: "background-tab",
115 NEW_WINDOW: "window",
118 forceNewWindow: false,
120 version: deprecated("config.version", { get: function version() config.version }),
123 * @property {Object} The map of command-line options. These are
124 * specified in the argument to the host application's -{config.name}
125 * option. E.g. $ firefox -pentadactyl '+u=/tmp/rcfile ++noplugin'
127 * +u RCFILE Use RCFILE instead of .pentadactylrc.
128 * ++noplugin Don't load plugins.
129 * These two can be specified multiple times:
130 * ++cmd CMD Execute an Ex command before initialization.
131 * +c CMD Execute an Ex command after initialization.
133 commandLineOptions: {
134 /** @property Whether plugin loading should be prevented. */
136 /** @property An RC file to use rather than the default. */
138 /** @property An Ex command to run before any initialization is performed. */
140 /** @property An Ex command to run after all initialization has been performed. */
144 registerObserver: function registerObserver(type, callback, weak) {
145 if (!(type in this._observers))
146 this._observers[type] = [];
147 this._observers[type].push(weak ? Cu.getWeakReference(callback) : { get: function () callback });
150 registerObservers: function registerObservers(obj, prop) {
151 for (let [signal, func] in Iterator(obj[prop || "signals"]))
152 this.registerObserver(signal, obj.closure(func), false);
155 unregisterObserver: function unregisterObserver(type, callback) {
156 if (type in this._observers)
157 this._observers[type] = this._observers[type].filter(function (c) c.get() != callback);
160 // TODO: "zoom": if the zoom value of the current buffer changed
161 applyTriggerObserver: function triggerObserver(type, args) {
162 if (type in this._observers)
163 this._observers[type] = this._observers[type].filter(function (callback) {
164 if (callback.get()) {
167 callback.get().apply(null, args);
169 catch (e if e.message == "can't wrap XML objects") {
171 callback.get().apply(null, [String(args[0])].concat(args.slice(1)));
175 dactyl.reportError(e);
182 triggerObserver: function triggerObserver(type) {
183 return this.applyTriggerObserver(type, Array.slice(arguments, 1));
186 addUsageCommand: function (params) {
187 let name = commands.add(params.name, params.description,
189 let results = array(params.iterate(args))
190 .sort(function (a, b) String.localeCompare(a.name, b.name));
192 let filters = args.map(function (arg) RegExp("\\b" + util.regexp.escape(arg) + "\\b", "i"));
194 results = results.filter(function (item) filters.every(function (re) re.test(item.name + " " + item.description)));
196 commandline.commandOutput(
197 template.usage(results, params.format));
201 completer: function (context, args) {
202 context.keys.text = util.identity;
203 context.keys.description = function () seen[this.text] + " matching items";
205 context.completions = array(item.description.toLowerCase().split(/[()\s]+/)
206 for (item in params.iterate(args)))
207 .flatten().filter(function (w) /^\w[\w-_']+$/.test(w))
209 seen[k] = (seen[k] || 0) + 1;
213 options: params.options || []
217 this.indices[params.index] = function () {
218 let results = array((params.iterateIndex || params.iterate).call(params, commands.get(name).newArgs()))
219 .array.sort(function (a, b) String.localeCompare(a.name, b.name));
221 let tags = services["dactyl:"].HELP_TAGS;
222 for (let obj in values(results)) {
223 let res = dactyl.generateHelp(obj, null, null, true);
224 if (!set.has(tags, obj.helpTag))
225 res[1].@tag = obj.helpTag;
233 * Triggers the application bell to notify the user of an error. The
234 * bell may be either audible or visual depending on the value of the
235 * 'visualbell' option.
238 this.triggerObserver("beep");
239 if (options["visualbell"]) {
241 bell: document.getElementById("dactyl-bell"),
242 strut: document.getElementById("dactyl-bell-strut")
244 XML.ignoreWhitespace = true;
246 util.overlayWindow(window, {
249 <window id={document.documentElement.id} xmlns={XUL}>
250 <hbox style="display: none" highlight="Bell" id="dactyl-bell" key="bell"/>
254 <window id={document.documentElement.id} xmlns={XUL}>
255 <hbox style="display: none" highlight="Bell" id="dactyl-bell-strut" key="strut"/>
260 elems.bell.style.height = window.innerHeight + "px";
261 elems.strut.style.marginBottom = -window.innerHeight + "px";
262 elems.strut.style.display = elems.bell.style.display = "";
264 util.timeout(function () { elems.strut.style.display = elems.bell.style.display = "none"; }, 20);
267 let soundService = Cc["@mozilla.org/sound;1"].getService(Ci.nsISound);
273 * Reads a string from the system clipboard.
275 * This is same as Firefox's readFromClipboard function, but is needed for
276 * apps like Thunderbird which do not provide it.
280 clipboardRead: function clipboardRead(getClipboard) {
282 const clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
283 const transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
285 transferable.addDataFlavor("text/unicode");
287 let source = clipboard[getClipboard || !clipboard.supportsSelectionClipboard() ?
288 "kGlobalClipboard" : "kSelectionClipboard"];
289 clipboard.getData(transferable, source);
291 let str = {}, len = {};
292 transferable.getTransferData("text/unicode", str, len);
295 return str.value.QueryInterface(Ci.nsISupportsString)
296 .data.substr(0, len.value / 2);
303 * Copies a string to the system clipboard. If *verbose* is specified the
304 * copied string is also echoed to the command line.
306 * @param {string} str
307 * @param {boolean} verbose
309 clipboardWrite: function clipboardWrite(str, verbose) {
310 const clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
311 clipboardHelper.copyString(str);
314 let message = { message: "Yanked " + str };
316 message.domains = [util.newURI(str).host];
319 dactyl.echomsg(message);
323 dump: deprecated("util.dump",
324 { get: function dump() util.closure.dump }),
325 dumpStack: deprecated("util.dumpStack",
326 { get: function dumpStack() util.closure.dumpStack }),
329 * Outputs a plain message to the command line.
331 * @param {string} str The message to output.
332 * @param {number} flags These control the multi-line message behavior.
333 * See {@link CommandLine#echo}.
335 echo: function echo(str, flags) {
336 commandline.echo(str, commandline.HL_NORMAL, flags);
340 * Outputs an error message to the command line.
342 * @param {string} str The message to output.
343 * @param {number} flags These control the multi-line message behavior.
344 * See {@link CommandLine#echo}.
346 echoerr: function echoerr(str, flags) {
347 flags |= commandline.APPEND_TO_MESSAGES;
349 if (isinstance(str, ["Error", "Exception"]))
350 dactyl.reportError(str);
351 if (isObject(str) && "echoerr" in str)
353 else if (isinstance(str, ["Error", FailedAssertion]) && str.fileName)
354 str = <>{str.fileName.replace(/^.* -> /, "")}: {str.lineNumber}: {str}</>;
356 if (options["errorbells"])
359 commandline.echo(str, commandline.HL_ERRORMSG, flags);
363 * Outputs a warning message to the command line.
365 * @param {string} str The message to output.
366 * @param {number} flags These control the multi-line message behavior.
367 * See {@link CommandLine#echo}.
369 warn: function warn(str, flags) {
370 commandline.echo(str, "WarningMsg", flags | commandline.APPEND_TO_MESSAGES);
373 // TODO: add proper level constants
375 * Outputs an information message to the command line.
377 * @param {string} str The message to output.
378 * @param {number} verbosity The messages log level (0 - 15). Only
379 * messages with verbosity less than or equal to the value of the
380 * *verbosity* option will be output.
381 * @param {number} flags These control the multi-line message behavior.
382 * See {@link CommandLine#echo}.
384 echomsg: function echomsg(str, verbosity, flags) {
385 if (verbosity == null)
386 verbosity = 0; // verbosity level is exclusionary
388 if (options["verbose"] >= verbosity)
389 commandline.echo(str, commandline.HL_INFOMSG,
390 flags | commandline.APPEND_TO_MESSAGES);
394 * Loads and executes the script referenced by *uri* in the scope of the
397 * @param {string} uri The URI of the script to load. Should be a local
398 * chrome:, file:, or resource: URL.
399 * @param {Object} context The context object into which the script
402 loadScript: function (uri, context) {
403 JSMLoader.loadSubScript(uri, context, File.defaultEncoding);
406 userEval: function (str, context, fileName, lineNumber) {
408 if (jsmodules.__proto__ != window)
409 str = "with (window) { with (modules) { (this.eval || eval)(" + str.quote() + ") } }";
411 let info = contexts.context;
412 if (fileName == null)
413 if (info && info.file[0] !== "[")
414 ({ file: fileName, line: lineNumber, context: ctxt }) = info;
416 if (!context && fileName && fileName[0] !== "[")
417 context = _userContext || ctxt;
419 if (isinstance(context, ["Sandbox"]))
420 return Cu.evalInSandbox(str, context, "1.8", fileName, lineNumber);
424 context = userContext || ctxt;
426 context[EVAL_ERROR] = null;
427 context[EVAL_STRING] = str;
428 context[EVAL_RESULT] = null;
429 this.loadScript("resource://dactyl-content/eval.js", context);
430 if (context[EVAL_ERROR]) {
432 context[EVAL_ERROR].fileName = info.file;
433 context[EVAL_ERROR].lineNumber += info.line;
436 throw context[EVAL_ERROR];
438 return context[EVAL_RESULT];
441 delete context[EVAL_ERROR];
442 delete context[EVAL_RESULT];
443 delete context[EVAL_STRING];
448 * Acts like the Function builtin, but the code executes in the
449 * userContext global.
451 userFunc: function () {
452 return this.userEval(
453 "(function userFunction(" + Array.slice(arguments, 0, -1).join(", ") + ")" +
454 " { " + arguments[arguments.length - 1] + " })");
458 * Execute an Ex command string. E.g. ":zoom 300".
460 * @param {string} str The command to execute.
461 * @param {Object} modifiers Any modifiers to be passed to
462 * {@link Command#action}.
463 * @param {boolean} silent Whether the command should be echoed on the
466 execute: function (str, modifiers, silent) {
467 // skip comments and blank lines
468 if (/^\s*("|$)/.test(str))
471 modifiers = modifiers || {};
474 commands.lastCommand = str.replace(/^\s*:\s*/, "");
476 for (let [command, args] in commands.parseCommands(str.replace(/^'(.*)'$/, "$1"))) {
477 if (command === null)
478 throw FailedAssertion(_("dactyl.notCommand", config.appName, args.commandString));
480 res = res && command.execute(args, modifiers);
485 focus: function focus(elem, flags) {
486 flags = flags || services.focus.FLAG_BYMOUSE;
488 if (elem instanceof Document)
489 elem = elem.defaultView;
490 if (elem instanceof Element)
491 services.focus.setFocus(elem, flags);
492 else if (elem instanceof Window)
493 services.focus.focusedWindow = elem;
502 * Focuses the content window.
504 * @param {boolean} clearFocusedElement Remove focus from any focused
507 focusContent: function focusContent(clearFocusedElement) {
508 if (window != services.focus.activeWindow)
511 let win = document.commandDispatcher.focusedWindow;
512 let elem = config.mainWidget || content;
514 // TODO: make more generic
516 if (this.has("mail") && !config.isComposeWindow) {
517 let i = gDBView.selection.currentIndex;
518 if (i == -1 && gDBView.rowCount >= 0)
520 gDBView.selection.select(i);
523 let frame = buffer.focusedFrame;
524 if (frame && frame.top == content && !Editor.getEditor(frame))
530 if (clearFocusedElement) {
531 if (dactyl.focusedElement)
532 dactyl.focusedElement.blur();
533 if (win && Editor.getEditor(win)) {
534 this.withSavedValues(["ignoreFocus"], function _focusContent() {
535 this.ignoreFocus = true;
536 if (win.frameElement)
537 win.frameElement.blur();
539 if (content.document.activeElement instanceof HTMLIFrameElement)
540 content.document.activeElement.blur();
545 if (elem instanceof Window && Editor.getEditor(elem))
548 if (elem && elem != dactyl.focusedElement)
552 /** @property {Element} The currently focused element. */
553 get focusedElement() services.focus.getFocusedElementForWindow(window, true, {}),
554 set focusedElement(elem) dactyl.focus(elem),
557 * Returns whether this Dactyl extension supports *feature*.
559 * @param {string} feature The feature name.
562 has: function (feature) set.has(config.features, feature),
565 * Returns the URL of the specified help *topic* if it exists.
567 * @param {string} topic The help topic to look up.
568 * @param {boolean} consolidated Whether to search the consolidated help page.
571 findHelp: function (topic, consolidated) {
572 if (!consolidated && topic in services["dactyl:"].FILE_MAP)
574 let items = completion._runCompleter("help", topic, null, !!consolidated).items;
575 let partialMatch = null;
577 function format(item) item.description + "#" + encodeURIComponent(item.text);
579 for (let [i, item] in Iterator(items)) {
580 if (item.text == topic)
582 else if (!partialMatch && topic)
587 return format(partialMatch);
594 initDocument: function initDocument(doc) {
596 if (doc.location.protocol === "dactyl:") {
608 * Initialize the help system.
610 initHelp: function (force) {
611 if (force || !this.helpInitialized) {
612 if ("noscriptOverlay" in window) {
613 noscriptOverlay.safeAllow("chrome-data:", true, false);
614 noscriptOverlay.safeAllow("dactyl:", true, false);
617 // Find help and overlay files with the given name.
618 let findHelpFile = function findHelpFile(file) {
620 for (let [, namespace] in Iterator(namespaces)) {
621 let url = ["dactyl://", namespace, "/", file, ".xml"].join("");
622 let res = util.httpGet(url);
624 if (res.responseXML.documentElement.localName == "document")
626 if (res.responseXML.documentElement.localName == "overlay")
627 overlayMap[file] = url;
628 result.push(res.responseXML);
633 // Find the tags in the document.
634 let addTags = function addTags(file, doc) {
635 for (let elem in util.evaluateXPath("//@tag|//dactyl:tags/text()|//dactyl:tag/text()", doc))
636 for (let tag in values((elem.value || elem.textContent).split(/\s+/)))
640 let namespaces = ["locale-local", "locale"];
641 services["dactyl:"].init({});
643 let tagMap = services["dactyl:"].HELP_TAGS;
644 let fileMap = services["dactyl:"].FILE_MAP;
645 let overlayMap = services["dactyl:"].OVERLAY_MAP;
647 // Scrape the list of help files from all.xml
648 // Manually process main and overlay files, since XSLTProcessor and
649 // XMLHttpRequest don't allow access to chrome documents.
650 tagMap["all"] = tagMap["all.xml"] = "all";
651 tagMap["versions"] = tagMap["versions.xml"] = "versions";
652 let files = findHelpFile("all").map(function (doc)
653 [f.value for (f in util.evaluateXPath("//dactyl:include/@href", doc))]);
655 // Scrape the tags from the rest of the help files.
656 array.flatten(files).forEach(function (file) {
657 tagMap[file + ".xml"] = file;
658 findHelpFile(file).forEach(function (doc) {
663 // Process plugin help entries.
664 XML.ignoreWhiteSpace = XML.prettyPrinting = false;
667 for (let [, context] in Iterator(plugins.contexts))
668 if (context && context.INFO instanceof XML) {
669 let info = context.INFO;
670 if (info.*.@lang.length()) {
671 let lang = config.bestLocale(String(a) for each (a in info.*.@lang));
673 info.* = info.*.(function::attribute("lang").length() == 0 || @lang == lang);
675 for each (let elem in info.NS::info)
676 for each (let attr in ["@name", "@summary", "@href"])
677 if (elem[attr].length())
678 info[attr] = elem[attr];
680 body += <h2 xmlns={NS.uri} tag={context.INFO.@name + '-plugin'}>{context.INFO.@summary}</h2> +
685 '<?xml version="1.0"?>\n' +
686 '<?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>\n' +
687 '<!DOCTYPE document SYSTEM "resource://dactyl-content/dactyl.dtd">\n' +
688 unescape(encodeURI( // UTF-8 handling hack.
690 name="plugins" title={config.appName + " Plugins"}>
691 <h1 tag="using-plugins">Using Plugins</h1>
695 </document>.toXMLString()));
696 fileMap["plugins"] = function () ['text/xml;charset=UTF-8', help];
698 fileMap["versions"] = function () {
699 let NEWS = util.httpGet(config.addon.getResourceURI("NEWS").spec,
700 { mimeType: "text/plain;charset=UTF-8" })
703 let re = util.regexp(<![CDATA[
704 ^ (?P<comment> \s* # .*\n)
707 (?P<char> [-•*+]) \ //
709 (?: \2\ \ .*\n | \s*\n)* )
713 (?:[^-•*+\s] | [-•*+]\S)
718 | (?: ^ [^\S\n]* \n) +
721 let betas = util.regexp(/\[(b\d)\]/, "gx");
723 let beta = array(betas.iterate(NEWS))
724 .map(function (m) m[1]).uniq().slice(-1)[0];
726 default xml namespace = NS;
727 function rec(text, level, li) {
729 let list, space, i = 0;
731 for (let match in re.iterate(text)) {
734 else if (match.char) {
738 li.* += rec(match.content.replace(RegExp("^" + match.space, "gm"), ""), level + 1, li)
741 else if (match.par) {
742 let [, par, tags] = /([^]*?)\s*((?:\[[^\]]+\])*)\n*$/.exec(match.par);
744 tags = array(betas.iterate(tags)).map(function (m) m[1]);
746 let group = !tags.length ? "" :
747 !tags.some(function (t) t == beta) ? "HelpNewsOld" : "HelpNewsNew";
749 li.@highlight = group;
754 if (level == 0 && /^.*:\n$/.test(match.par))
755 res += <h2>{template.linkifyHelp(par.slice(0, -1), true)}</h2>;
757 let [, a, b] = /^(IMPORTANT:?)?([^]*)/.exec(par);
758 res += <p highlight={group + " HelpNews"}>{
760 <hl key="HelpNewsTag">{tags.join(" ")}</hl>
762 a ? <hl key="HelpWarning">{a}</hl> : ""
764 template.linkifyHelp(b, true)
770 for each (let attr in res..@highlight) {
771 attr.parent().@NS::highlight = attr;
772 delete attr.parent().@highlight;
777 let body = rec(NEWS, 0);
778 for each (let li in body..li) {
779 let list = li..li.(@NS::highlight == "HelpNewsOld");
780 if (list.length() && list.length() == li..li.(@NS::highlight != "").length()) {
781 for each (let li in list)
782 li.@NS::highlight = "";
783 li.@NS::highlight = "HelpNewsOld";
787 XML.prettyPrinting = XML.ignoreWhitespace = false;
788 return ["application/xml",
789 '<?xml version="1.0"?>\n' +
790 '<?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>\n' +
791 '<!DOCTYPE document SYSTEM "resource://dactyl-content/dactyl.dtd">\n' +
792 unescape(encodeURI( // UTF-8 handling hack.
793 <document xmlns={NS} xmlns:dactyl={NS}
794 name="versions" title={config.appName + " Versions"}>
795 <h1 tag="versions news NEWS">{config.appName} Versions</h1>
799 </document>.toXMLString()))
802 addTags("versions", util.httpGet("dactyl://help/versions").responseXML);
803 addTags("plugins", util.httpGet("dactyl://help/plugins").responseXML);
805 default xml namespace = NS;
807 overlayMap["index"] = ['text/xml;charset=UTF-8',
808 '<?xml version="1.0"?>\n' +
809 '<overlay xmlns="' + NS + '">\n' +
810 unescape(encodeURI( // UTF-8 handling hack.
811 template.map(dactyl.indices, function ([name, iter])
812 <dl insertafter={name + "-index"}>{
813 template.map(iter(), util.identity)
814 }</dl>, <>{"\n\n"}</>))) +
817 addTags("index", util.httpGet("dactyl://help-overlay/index").responseXML);
819 this.helpInitialized = true;
823 stringifyXML: function (xml) {
824 XML.prettyPrinting = false;
825 XML.ignoreWhitespace = false;
826 return UTF8(xml.toXMLString());
829 exportHelp: JavaScript.setCompleter(function (path) {
830 const FILE = io.File(path);
831 const PATH = FILE.leafName.replace(/\..*/, "") + "/";
832 const TIME = Date.now();
834 if (!FILE.exists() && (/\/$/.test(path) && !/\./.test(FILE.leafName)))
835 FILE.create(FILE.DIRECTORY_TYPE, octal(755));
838 if (FILE.isDirectory()) {
839 var addDataEntry = function addDataEntry(file, data) FILE.child(file).write(data);
840 var addURIEntry = function addURIEntry(file, uri) addDataEntry(file, util.httpGet(uri).responseText);
843 var zip = services.ZipWriter();
844 zip.open(FILE, File.MODE_CREATE | File.MODE_WRONLY | File.MODE_TRUNCATE);
846 addURIEntry = function addURIEntry(file, uri)
847 zip.addEntryChannel(PATH + file, TIME, 9,
848 services.io.newChannel(uri, null, null), false);
849 addDataEntry = function addDataEntry(file, data) // Unideal to an extreme.
850 addURIEntry(file, "data:text/plain;charset=UTF-8," + encodeURI(data));
853 let empty = set("area base basefont br col frame hr img input isindex link meta param"
856 switch(node.nodeType) {
857 case Node.ELEMENT_NODE:
858 if (isinstance(node, [HTMLBaseElement]))
861 data.push("<"); data.push(node.localName);
862 if (node instanceof HTMLHtmlElement)
863 data.push(" xmlns=" + XHTML.uri.quote());
865 for (let { name, value } in array.iterValues(node.attributes)) {
866 if (name == "dactyl:highlight") {
867 set.add(styles, value);
869 value = "hl-" + value;
871 if (name == "href") {
873 if (value.indexOf("dactyl://help-tag/") == 0) {
874 let uri = services.io.newChannel(value, null, null).originalURI;
875 value = uri.spec == value ? "javascript:;" : uri.path.substr(1);
877 if (!/^#|[\/](#|$)|^[a-z]+:/.test(value))
878 value = value.replace(/(#|$)/, ".xhtml$1");
880 if (name == "src" && value.indexOf(":") > 0) {
881 chromeFiles[value] = value.replace(/.*\//, "");
882 value = value.replace(/.*\//, "");
887 data.push(<>{value}</>.toXMLString());
890 if (node.localName in empty)
894 if (node instanceof HTMLHeadElement)
895 data.push(<link rel="stylesheet" type="text/css" href="help.css"/>.toXMLString());
896 Array.map(node.childNodes, fix);
897 data.push("</"); data.push(node.localName); data.push(">");
901 data.push(<>{node.textContent}</>.toXMLString());
905 let chromeFiles = {};
907 for (let [file, ] in Iterator(services["dactyl:"].FILE_MAP)) {
908 dactyl.open("dactyl://help/" + file);
909 dactyl.modules.events.waitForPageLoad();
911 '<?xml version="1.0" encoding="UTF-8"?>\n',
912 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"\n',
913 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'
915 fix(content.document.documentElement);
916 addDataEntry(file + ".xhtml", data.join(""));
919 let data = [h for (h in highlight) if (set.has(styles, h.class) || /^Help/.test(h.class))]
920 .map(function (h) h.selector
921 .replace(/^\[.*?=(.*?)\]/, ".hl-$1")
922 .replace(/html\|/g, "") + "\t" + "{" + h.cssText + "}")
924 addDataEntry("help.css", data.replace(/chrome:[^ ")]+\//g, ""));
926 addDataEntry("tag-map.json", JSON.stringify(services["dactyl:"].HELP_TAGS));
928 let m, re = /(chrome:[^ ");]+\/)([^ ");]+)/g;
929 while ((m = re.exec(data)))
930 chromeFiles[m[0]] = m[2];
932 for (let [uri, leaf] in Iterator(chromeFiles))
933 addURIEntry(leaf, uri);
937 }, [function (context, args) completion.file(context)]),
940 * Generates a help entry and returns it as a string.
942 * @param {Command|Map|Option} obj A dactyl *Command*, *Map* or *Option*
944 * @param {XMLList} extraHelp Extra help text beyond the description.
947 generateHelp: function generateHelp(obj, extraHelp, str, specOnly) {
948 default xml namespace = "";
951 link = tag = spec = util.identity;
954 if (obj instanceof Command) {
955 link = function (cmd) <ex>{cmd}</ex>;
956 args = obj.parseArgs("", CompletionContext(str || ""));
957 spec = function (cmd) cmd + (obj.bang ? <oa>!</oa> : <></>);
959 else if (obj instanceof Map) {
960 spec = function (map) obj.count ? <><oa>count</oa>{map}</> : <>{map}</>;
961 link = function (map) {
962 let [, mode, name, extra] = /^(?:(.)_)?(?:<([^>]+)>)?(.*)$/.exec(map);
963 let k = <k>{extra}</k>;
971 else if (obj instanceof Option) {
972 link = function (opt, name) <o>{name}</o>;
975 XML.prettyPrinting = false;
976 XML.ignoreWhitespace = false;
977 default xml namespace = NS;
979 // E4X has its warts.
984 <dt>{link(obj.helpTag || obj.name, obj.name)}</dt> <dd>{
985 template.linkifyHelp(obj.description ? obj.description.replace(/\.$/, "") : "", true)
988 return res.elements();
992 <tags>{template.map(obj.names.slice().reverse(), tag, " ")}</tags>
994 spec(template.highlightRegexp((obj.specs || obj.names)[0],
996 function (m, n0) <oa>{n0}</oa>))
999 <type>{obj.type}</type>
1000 <default>{obj.stringDefaultValue}</default></>}
1002 obj.description ? br + <p>{template.linkifyHelp(obj.description.replace(/\.?$/, "."), true)}</p> : "" }{
1003 extraHelp ? br + extraHelp : "" }{
1004 !(extraHelp || obj.description) ? br + <p>Sorry, no help available.</p> : "" }
1009 res.item.description.* += br +
1010 let (br = br + <> </>)
1011 <><dl>{ br + template.map(ary, function ([a, b]) <><dt>{a}</dt> <dd>{b}</dd></>, br) }
1017 add(completion._runCompleter(obj.completer, "", null, args).items
1018 .map(function (i) [i.text, i.description]));
1020 if (obj.options && obj.options.some(function (o) o.description))
1021 add(obj.options.filter(function (o) o.description)
1025 o.names.length == 1 ? "" :
1027 template.map(o.names.slice(1), function (n) <em>{n}</em>, <>, </>)
1031 return res.*.toXMLString()
1032 .replace(' xmlns="' + NS + '"', "", "g")
1033 .replace(/^ {12}|[ \t]+$/gm, "")
1034 .replace(/^\s*\n|\n\s*$/g, "") + "\n";
1038 * Opens the help page containing the specified *topic* if it exists.
1040 * @param {string} topic The help topic to open.
1041 * @param {boolean} consolidated Whether to use the consolidated help page.
1043 help: function (topic, consolidated) {
1046 let helpFile = consolidated ? "all" : options["helpfile"];
1048 if (helpFile in services["dactyl:"].FILE_MAP)
1049 dactyl.open("dactyl://help/" + helpFile, { from: "help" });
1051 dactyl.echomsg(_("help.noFile", helpFile.quote()));
1055 let page = this.findHelp(topic, consolidated);
1056 dactyl.assert(page != null, _("help.noTopic", topic));
1058 dactyl.open("dactyl://help/" + page, { from: "help" });
1062 * The map of global variables.
1064 * These are set and accessed with the "g:" prefix.
1066 _globalVariables: {},
1067 globalVariables: deprecated("the options system", {
1068 get: function globalVariables() this._globalVariables
1071 loadPlugins: function (args, force) {
1072 function sourceDirectory(dir) {
1073 dactyl.assert(dir.isReadable(), _("io.notReadable", dir.path));
1075 dactyl.log(_("dactyl.sourcingPlugins", dir.path), 3);
1077 let loadplugins = options.get("loadplugins");
1079 loadplugins = { __proto__: loadplugins, value: args.map(Option.parseRegexp) }
1081 dir.readDirectory(true).forEach(function (file) {
1082 if (file.isFile() && loadplugins.getKey(file.path) && !(!force && file.path in dactyl.pluginFiles)) {
1084 io.source(file.path);
1085 dactyl.pluginFiles[file.path] = true;
1088 dactyl.reportError(e);
1091 else if (file.isDirectory())
1092 sourceDirectory(file);
1096 let dirs = io.getRuntimeDirectories("plugins");
1098 if (dirs.length == 0) {
1099 dactyl.log(_("dactyl.noPluginDir"), 3);
1104 _("plugin.searchingForIn",
1105 ("plugins/**/*.{js," + config.fileExtension + "}").quote(),
1106 [dir.path.replace(/.plugins$/, "") for ([, dir] in Iterator(dirs))]
1107 .join(",").quote()),
1110 dirs.forEach(function (dir) {
1111 dactyl.echomsg(_("plugin.searchingFor", (dir.path + "/**/*.{js," + config.fileExtension + "}").quote()), 3);
1112 sourceDirectory(dir);
1116 // TODO: add proper level constants
1118 * Logs a message to the JavaScript error console. Each message has an
1119 * associated log level. Only messages with a log level less than or equal
1120 * to *level* will be printed. If *msg* is an object, it is pretty printed.
1122 * @param {string|Object} msg The message to print.
1123 * @param {number} level The logging level 0 - 15.
1125 log: function (msg, level) {
1126 let verbose = localPrefs.get("loglevel", 0);
1128 if (!level || level <= verbose) {
1129 if (isObject(msg) && !isinstance(msg, _))
1130 msg = util.objectToString(msg, false);
1132 services.console.logStringMessage(config.name + ": " + msg);
1136 onClick: function onClick(event) {
1137 if (event.originalTarget instanceof Element) {
1138 let command = event.originalTarget.getAttributeNS(NS, "command");
1139 if (command && event.button == 0) {
1140 event.preventDefault();
1142 if (dactyl.commands[command])
1143 dactyl.withSavedValues(["forceNewTab"], function () {
1144 dactyl.forceNewTab = event.ctrlKey || event.shiftKey || event.button == 1;
1145 dactyl.commands[command](event);
1151 onExecute: function onExecute(event) {
1152 let cmd = event.originalTarget.getAttribute("dactyl-execute");
1153 commands.execute(cmd, null, false, null,
1154 { file: "[Command Line]", line: 1 });
1158 * Opens one or more URLs. Returns true when load was initiated, or
1161 * @param {string|Array} urls A representation of the URLs to open. May be
1162 * either a string, which will be passed to
1163 * {@see Dactyl#parseURLs}, or an array in the same format as
1164 * would be returned by the same.
1165 * @param {object} params A set of parameters specifying how to open the
1166 * URLs. The following properties are recognized:
1168 * • background If true, new tabs are opened in the background.
1170 * • from The designation of the opener, as appears in
1171 * 'activate' and 'newtab' options. If present,
1172 * the newtab option provides the default 'where'
1173 * parameter, and the value of the 'activate'
1174 * parameter is inverted if 'background' is true.
1176 * • where One of CURRENT_TAB, NEW_TAB, or NEW_WINDOW
1178 * As a deprecated special case, the where parameter may be provided
1179 * by itself, in which case it is transformed into { where: params }.
1181 * @param {boolean} force Don't prompt whether to open more than 20
1183 * @returns {boolean}
1185 open: function (urls, params, force) {
1186 if (typeof urls == "string")
1187 urls = dactyl.parseURLs(urls);
1189 if (urls.length > prefs.get("browser.tabs.maxOpenBeforeWarn", 20) && !force)
1190 return commandline.input("This will open " + urls.length + " new tabs. Would you like to continue? (yes/[no]) ",
1192 if (resp && resp.match(/^y(es)?$/i))
1193 dactyl.open(urls, params, true);
1196 params = params || {};
1197 if (isString(params))
1198 params = { where: params };
1201 for (let [opt, flag] in Iterator({ replace: "REPLACE_HISTORY", hide: "BYPASS_HISTORY" }))
1202 flags |= params[opt] && Ci.nsIWebNavigation["LOAD_FLAGS_" + flag];
1204 let where = params.where || dactyl.CURRENT_TAB;
1205 let background = ("background" in params) ? params.background
1206 : params.where == dactyl.NEW_BACKGROUND_TAB;
1208 if (params.from && dactyl.has("tabs")) {
1209 if (!params.where && options.get("newtab").has(params.from))
1210 where = dactyl.NEW_TAB;
1211 background ^= !options.get("activate").has(params.from);
1214 if (urls.length == 0)
1217 let browser = config.tabbrowser;
1218 function open(urls, where) {
1220 let url = Array.concat(urls)[0];
1221 let postdata = Array.concat(urls)[1];
1223 // decide where to load the first url
1226 case dactyl.NEW_TAB:
1227 if (!dactyl.has("tabs"))
1228 return open(urls, dactyl.NEW_WINDOW);
1230 return prefs.withContext(function () {
1231 prefs.set("browser.tabs.loadInBackground", true);
1232 return browser.loadOneTab(url, null, null, postdata, background).linkedBrowser.contentDocument;
1235 case dactyl.NEW_WINDOW:
1236 let win = window.openDialog(document.documentURI, "_blank", "chrome,all,dialog=no");
1237 util.waitFor(function () win.document.readyState === "complete");
1238 browser = win.dactyl && win.dactyl.modules.config.tabbrowser || win.getBrowser();
1240 case dactyl.CURRENT_TAB:
1241 browser.loadURIWithFlags(url, flags, null, null, postdata);
1242 return browser.contentWindow;
1246 // Unfortunately, failed page loads throw exceptions and
1247 // cause a lot of unwanted noise. This solution means that
1248 // any genuine errors go unreported.
1251 if (dactyl.forceNewTab)
1252 where = dactyl.NEW_TAB;
1253 else if (dactyl.forceNewWindow)
1254 where = dactyl.NEW_WINDOW;
1256 where = dactyl.CURRENT_TAB;
1258 return urls.map(function (url) {
1259 let res = open(url, where);
1260 where = dactyl.NEW_TAB;
1267 * Returns an array of URLs parsed from *str*.
1269 * Given a string like 'google bla, www.osnews.com' return an array
1270 * ['www.google.com/search?q=bla', 'www.osnews.com']
1272 * @param {string} str
1273 * @returns {string[]}
1275 parseURLs: function parseURLs(str) {
1278 if (options["urlseparator"])
1279 urls = util.splitLiteral(str, util.regexp("\\s*" + options["urlseparator"] + "\\s*"));
1283 return urls.map(function (url) {
1286 if (/^(\.{0,2}|~)(\/|$)/.test(url)) {
1288 // Try to find a matching file.
1289 let file = io.File(url);
1290 if (file.exists() && file.isReadable())
1291 return services.io.newFileURI(file).spec;
1296 // If it starts with a valid protocol, pass it through.
1297 let proto = /^([-\w]+):/.exec(url);
1298 if (proto && "@mozilla.org/network/protocol;1?name=" + proto[1] in Cc)
1299 return url.replace(/\s+/g, "");
1301 // Check for a matching search keyword.
1302 let searchURL = this.has("bookmarks") && bookmarks.getSearchURL(url, false);
1306 // If it looks like URL-ish (foo.com/bar), let Gecko figure it out.
1307 if (this.urlish.test(url) || !this.has("bookmarks"))
1308 return util.createURI(url).spec;
1310 // Pass it off to the default search engine or, failing
1311 // that, let Gecko deal with it as is.
1312 return bookmarks.getSearchURL(url, true) || util.createURI(url).spec;
1315 stringToURLArray: deprecated("dactyl.parseURLs", "parseURLs"),
1316 urlish: Class.memoize(function () util.regexp(<![CDATA[
1318 <domain>+ (:\d+)? (/ .*) |
1320 <domain>+ \. [a-z0-9]+ |
1324 domain: util.regexp(String.replace(<![CDATA[
1326 U0000-U002c // U002d-U002e --.
1329 U003a-U0040 // U0041-U005a a-z
1330 U005b-U0060 // U0061-U007a A-Z
1333 ]]>, /U/g, "\\u"), "x")
1338 get plugins() plugins,
1340 setNodeVisible: function setNodeVisible(node, visible) {
1341 if (window.setToolbarVisibility && node.localName == "toolbar")
1342 window.setToolbarVisibility(node, visible);
1344 node.collapsed = !visible;
1347 confirmQuit: function confirmQuit()
1348 prefs.withContext(function () {
1349 prefs.set("browser.warnOnQuit", false);
1350 return window.canQuitApplication();
1354 * Quit the host application, no matter how many tabs/windows are open.
1356 * @param {boolean} saveSession If true the current session will be
1357 * saved and restored when the host application is restarted.
1358 * @param {boolean} force Forcibly quit irrespective of whether all
1359 * windows could be closed individually.
1361 quit: function (saveSession, force) {
1362 if (!force && !this.confirmQuit())
1365 let pref = "browser.startup.page";
1368 prefs.safeSet(pref, 3);
1369 if (!saveSession && prefs.get(pref) >= 2)
1370 prefs.safeSet(pref, 1);
1372 services.appStartup.quit(Ci.nsIAppStartup[force ? "eForceQuit" : "eAttemptQuit"]);
1376 * Restart the host application.
1378 restart: function () {
1379 if (!this.confirmQuit())
1382 services.appStartup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
1385 get assert() util.assert,
1388 * Traps errors in the called function, possibly reporting them.
1390 * @param {function} func The function to call
1391 * @param {object} self The 'this' object for the function.
1393 trapErrors: function trapErrors(func, self) {
1397 return func.apply(self || this, Array.slice(arguments, 2));
1400 dactyl.reportError(e, true);
1406 * Reports an error to both the console and the host application's
1409 * @param {Object} error The error object.
1411 reportError: function reportError(error, echo) {
1412 if (error instanceof FailedAssertion && error.noTrace || error.message === "Interrupted") {
1413 let context = contexts.context;
1414 let prefix = context ? context.file + ":" + context.line + ": " : "";
1415 if (error.message && error.message.indexOf(prefix) !== 0)
1416 error.message = prefix + error.message;
1419 dactyl.echoerr(template.linkifyHelp(error.message));
1424 util.reportError(error);
1427 if (error.result == Cr.NS_BINDING_ABORTED)
1430 dactyl.echoerr(error, commandline.FORCE_SINGLELINE);
1432 util.reportError(error);
1436 * Parses a Dactyl command-line string i.e. the value of the
1437 * -dactyl command-line option.
1439 * @param {string} cmdline The string to parse for command-line
1442 * @see Commands#parseArgs
1444 parseCommandLine: function (cmdline) {
1446 return commands.get("rehash").parseArgs(cmdline);
1449 dactyl.reportError(e, true);
1454 wrapCallback: function (callback, self) {
1455 self = self || this;
1456 let save = ["forceNewTab", "forceNewWindow"];
1457 let saved = save.map(function (p) dactyl[p]);
1458 return function wrappedCallback() {
1459 let args = arguments;
1460 return dactyl.withSavedValues(save, function () {
1461 saved.forEach(function (p, i) dactyl[save[i]] = p);
1463 return callback.apply(self, args);
1466 dactyl.reportError(e, true);
1473 * @property {Window[]} Returns an array of all the host application's
1476 get windows() [win for (win in iter(services.windowMediator.getEnumerator("navigator:browser")))],
1479 // initially hide all GUI elements, they are later restored unless the user
1480 // has :set go= or something similar in his config
1481 hideGUI: function () {
1482 let guioptions = config.guioptions;
1483 for (let option in guioptions) {
1484 guioptions[option].forEach(function (elem) {
1486 document.getElementById(elem).collapsed = true;
1494 getMenuItems: function () {
1495 function addChildren(node, parent) {
1496 for (let [, item] in Iterator(node.childNodes)) {
1497 if (item.childNodes.length == 0 && item.localName == "menuitem"
1498 && !/rdf:http:/.test(item.getAttribute("label"))) { // FIXME
1499 item.fullMenuPath = parent + item.getAttribute("label");
1504 if (item.localName == "menu")
1505 path += item.getAttribute("label") + ".";
1506 addChildren(item, path);
1512 addChildren(document.getElementById(config.guioptions["m"][1]), "");
1516 events: function () {
1517 events.listen(window, "click", dactyl.closure.onClick, true);
1518 events.listen(window, "dactyl.execute", dactyl.closure.onExecute, true);
1520 // Only general options are added here, which are valid for all Dactyl extensions
1521 options: function () {
1522 options.add(["errorbells", "eb"],
1523 "Ring the bell when an error message is displayed",
1526 options.add(["exrc", "ex"],
1527 "Enable automatic sourcing of an RC file in the current directory at startup",
1530 options.add(["fullscreen", "fs"],
1531 "Show the current window fullscreen",
1533 setter: function (value) window.fullScreen = value,
1534 getter: function () window.fullScreen
1540 c: ["Always show the command line, even when empty"],
1541 C: ["Always show the command line outside of the status line"],
1542 M: ["Always show messages outside of the status line"]
1544 setter: function (opts) {
1545 if (loaded.commandline)
1546 commandline.widgets.updateVisibility();
1551 s: ["Status bar", [statusline.statusBar.id]]
1552 }, config.guioptions),
1553 setter: function (opts) {
1554 for (let [opt, [, ids]] in Iterator(this.opts)) {
1555 ids.map(function (id) document.getElementById(id))
1556 .forEach(function (elem) {
1558 dactyl.setNodeVisible(elem, opts.indexOf(opt) >= 0);
1565 r: ["Right Scrollbar", "vertical"],
1566 l: ["Left Scrollbar", "vertical"],
1567 b: ["Bottom Scrollbar", "horizontal"]
1569 setter: function (opts) {
1570 let dir = ["horizontal", "vertical"].filter(
1571 function (dir) !Array.some(opts,
1572 function (o) this.opts[o] && this.opts[o][1] == dir, this),
1574 let class_ = dir.map(function (dir) "html|html > xul|scrollbar[orient=" + dir + "]");
1576 styles.system.add("scrollbar", "*",
1577 class_.length ? class_.join(", ") + " { visibility: collapse !important; }" : "",
1580 prefs.safeSet("layout.scrollbar.side", opts.indexOf("l") >= 0 ? 3 : 2,
1581 "See 'guioptions' scrollbar flags.");
1583 validator: function (opts) Option.validIf(!(opts.indexOf("l") >= 0 && opts.indexOf("r") >= 0),
1584 UTF8("Only one of ‘l’ or ‘r’ allowed"))
1589 n: ["Tab number", highlight.selector("TabNumber")],
1590 N: ["Tab number over icon", highlight.selector("TabIconNumber")]
1592 setter: function (opts) {
1593 let classes = [v[1] for ([k, v] in Iterator(this.opts)) if (opts.indexOf(k) < 0)];
1595 styles.system.add("taboptions", "chrome://*",
1596 classes.length ? classes.join(",") + "{ display: none; }" : "");
1598 if (!dactyl.has("Gecko2")) {
1599 tabs.tabBinding.enabled = Array.some(opts, function (k) k in this.opts, this);
1600 tabs.updateTabCount();
1602 if (config.tabbrowser.tabContainer._positionPinnedTabs)
1603 config.tabbrowser.tabContainer._positionPinnedTabs();
1606 validator: function (opts) dactyl.has("Gecko2") ||
1607 Option.validIf(!/[nN]/.test(opts), "Tab numbering not available in this " + config.host + " version")
1610 ].filter(function (group) !group.feature || dactyl.has(group.feature));
1612 options.add(["guioptions", "go"],
1613 "Show or hide certain GUI elements like the menu or toolbar",
1614 "charlist", config.defaults.guioptions || "", {
1617 cleanupValue: config.cleanups.guioptions ||
1618 "r" + [k for ([k, v] in iter(groups[1].opts))
1619 if (!document.getElementById(v[1][0]).collapsed)].join(""),
1621 values: array(groups).map(function (g) [[k, v[0]] for ([k, v] in Iterator(g.opts))]).flatten(),
1623 setter: function (value) {
1624 for (let group in values(groups))
1625 group.setter(value);
1626 events.checkFocus();
1629 validator: function (val) Option.validateCompleter.call(this, val) &&
1630 groups.every(function (g) !g.validator || g.validator(val))
1633 options.add(["helpfile", "hf"],
1634 "Name of the main help file",
1637 options.add(["loadplugins", "lpl"],
1638 "A regexp list that defines which plugins are loaded at startup and via :loadplugins",
1639 "regexplist", "'\\.(js|" + config.fileExtension + ")$'");
1641 options.add(["titlestring"],
1642 "The string shown at the end of the window title",
1643 "string", config.defaults.titlestring || config.host,
1645 setter: function (value) {
1646 let win = document.documentElement;
1647 function updateTitle(old, current) {
1648 document.title = document.title.replace(RegExp("(.*)" + util.regexp.escape(old)), "$1" + current);
1651 if (services.has("privateBrowsing")) {
1652 let oldValue = win.getAttribute("titlemodifier_normal");
1653 let suffix = win.getAttribute("titlemodifier_privatebrowsing").substr(oldValue.length);
1655 win.setAttribute("titlemodifier_normal", value);
1656 win.setAttribute("titlemodifier_privatebrowsing", value + suffix);
1658 if (services.privateBrowsing.privateBrowsingEnabled) {
1659 updateTitle(oldValue + suffix, value + suffix);
1664 updateTitle(win.getAttribute("titlemodifier"), value);
1665 win.setAttribute("titlemodifier", value);
1671 options.add(["urlseparator", "urlsep", "us"],
1672 "The regular expression used to separate multiple URLs in :open and friends",
1674 { validator: function (value) RegExp(value) });
1676 options.add(["verbose", "vbs"],
1677 "Define which info messages are displayed",
1679 { validator: function (value) Option.validIf(value >= 0 && value <= 15, "Value must be between 0 and 15") });
1681 options.add(["visualbell", "vb"],
1682 "Use visual bell instead of beeping on errors",
1685 setter: function (value) {
1686 prefs.safeSet("accessibility.typeaheadfind.enablesound", !value,
1687 "See 'visualbell' option");
1693 mappings: function () {
1694 mappings.add([modes.MAIN], ["<F1>"],
1695 "Open the introductory help page",
1696 function () { dactyl.help(); });
1698 mappings.add([modes.MAIN], ["<A-F1>"],
1699 "Open the single, consolidated help page",
1700 function () { ex.helpall(); });
1702 if (dactyl.has("session"))
1703 mappings.add([modes.NORMAL], ["ZQ"],
1704 "Quit and don't save the session",
1705 function () { dactyl.quit(false); });
1707 mappings.add([modes.NORMAL], ["ZZ"],
1708 "Quit and save the session",
1709 function () { dactyl.quit(true); });
1712 commands: function () {
1713 commands.add(["dia[log]"],
1714 "Open a " + config.appName + " dialog",
1716 let dialog = args[0];
1718 dactyl.assert(dialog in config.dialogs,
1719 _("error.invalidArgument", dialog));
1720 dactyl.assert(!config.dialogs[dialog][2] || config.dialogs[dialog][2](),
1721 _("dialog.notAvailable", dialog));
1723 config.dialogs[dialog][1]();
1726 dactyl.echoerr(_("error.cantOpen", dialog.quote(), e.message || e));
1731 completer: function (context) {
1732 context.ignoreCase = true;
1733 completion.dialog(context);
1737 commands.add(["em[enu]"],
1738 "Execute the specified menu item from the command line",
1740 let arg = args[0] || "";
1741 let items = Dactyl.getMenuItems();
1743 dactyl.assert(items.some(function (i) i.fullMenuPath == arg),
1744 _("emenu.notFound", arg));
1746 for (let [, item] in Iterator(items)) {
1747 if (item.fullMenuPath == arg)
1752 completer: function (context) completion.menuItem(context),
1756 commands.add(["exe[cute]"],
1757 "Execute the argument as an Ex command",
1760 let cmd = dactyl.userEval(args[0] || "");
1761 dactyl.execute(cmd || "", null, true);
1767 completer: function (context) completion.javascript(context),
1774 description: "Open the introductory help page"
1777 description: "Open the single consolidated help page"
1779 ].forEach(function (command) {
1780 let consolidated = command.name == "helpa[ll]";
1782 commands.add([command.name],
1783 command.description,
1785 dactyl.assert(!args.bang, _("help.dontPanic"));
1786 dactyl.help(args.literalArg, consolidated);
1790 completer: function (context) completion.help(context, consolidated),
1795 commands.add(["loadplugins", "lpl"],
1796 "Load all plugins immediately",
1798 dactyl.loadPlugins(args.length ? args : null, args.bang);
1805 serialize: function () [
1808 literalArg: options["loadplugins"].join(" ")
1813 commands.add(["norm[al]"],
1814 "Execute Normal mode commands",
1815 function (args) { events.feedkeys(args[0], args.bang, false, modes.NORMAL); },
1822 commands.add(["q[uit]"],
1823 dactyl.has("tabs") ? "Quit current tab" : "Quit application",
1825 if (dactyl.has("tabs") && tabs.remove(tabs.getTab(), 1, false))
1827 else if (dactyl.windows.length > 1)
1830 dactyl.quit(false, args.bang);
1836 commands.add(["reh[ash]"],
1837 "Reload the " + config.appName + " add-on",
1840 JSMLoader.rehashCmd = args.trailing; // Hack.
1849 description: "The initialization file to execute at startup",
1850 type: CommandOption.STRING
1853 names: ["++noplugin"],
1854 description: "Do not automatically load plugins"
1858 description: "Ex commands to execute prior to initialization",
1859 type: CommandOption.STRING,
1864 description: "Ex commands to execute after initialization",
1865 type: CommandOption.STRING,
1871 commands.add(["res[tart]"],
1872 "Force " + config.appName + " to restart",
1873 function () { dactyl.restart(); });
1875 function findToolbar(name) util.evaluateXPath(
1876 "//*[@toolbarname=" + util.escapeString(name, "'") + "]",
1877 document).snapshotItem(0);
1879 var toolbox = document.getElementById("navigator-toolbox");
1881 let hidden = function hidden(elem) (elem.getAttribute("autohide") || elem.getAttribute("collapsed")) == "true";
1883 let toolbarCommand = function (names, desc, action, filter) {
1884 commands.add(names, desc,
1886 let toolbar = findToolbar(args[0] || "");
1887 dactyl.assert(toolbar, _("error.invalidArgument"));
1889 events.checkFocus();
1892 completer: function (context) {
1893 completion.toolbar(context);
1895 context.filters.push(filter);
1901 toolbarCommand(["toolbars[how]", "tbs[how]"], "Show the named toolbar",
1902 function (toolbar) dactyl.setNodeVisible(toolbar, true),
1903 function ({ item }) hidden(item));
1904 toolbarCommand(["toolbarh[ide]", "tbh[ide]"], "Hide the named toolbar",
1905 function (toolbar) dactyl.setNodeVisible(toolbar, false),
1906 function ({ item }) !hidden(item));
1907 toolbarCommand(["toolbart[oggle]", "tbt[oggle]"], "Toggle the named toolbar",
1908 function (toolbar) dactyl.setNodeVisible(toolbar, hidden(toolbar)));
1911 commands.add(["time"],
1912 "Profile a piece of code or run a command multiple times",
1914 let count = args.count;
1915 let special = args.bang;
1916 args = args[0] || "";
1919 var method = function () commands.execute(args, null, true);
1921 method = dactyl.userFunc(args);
1925 let each, eachUnits, totalUnits;
1928 for (let i in util.interruptibleRange(0, count, 500)) {
1929 let now = Date.now();
1931 total += Date.now() - now;
1937 if (total / count >= 100) {
1938 each = total / 1000.0 / count;
1942 each = total / count;
1947 total = total / 1000.0;
1951 totalUnits = "msec";
1953 commandline.commandOutput(
1955 <tr highlight="Title" align="left">
1956 <th colspan="3">Code execution summary</th>
1958 <tr><td>  Executed:</td><td align="right"><span class="times-executed">{count}</span></td><td>times</td></tr>
1959 <tr><td>  Average time:</td><td align="right"><span class="time-average">{each.toFixed(2)}</span></td><td>{eachUnits}</td></tr>
1960 <tr><td>  Total time:</td><td align="right"><span class="time-total">{total.toFixed(2)}</span></td><td>{totalUnits}</td></tr>
1964 let beforeTime = Date.now();
1970 let afterTime = Date.now();
1972 if (afterTime - beforeTime >= 100)
1973 dactyl.echo(_("time.total", ((afterTime - beforeTime) / 1000.0).toFixed(2) + " sec"));
1975 dactyl.echo(_("time.total", (afterTime - beforeTime) + " msec"));
1984 completer: function (context) {
1985 if (/^:/.test(context.filter))
1986 return completion.ex(context);
1988 return completion.javascript(context);
1996 commands.add(["verb[ose]"],
1997 "Execute a command with 'verbose' set",
1999 let vbs = options.get("verbose");
2000 let value = vbs.value;
2001 let setFrom = vbs.setFrom;
2004 vbs.set(args.count || 1);
2006 dactyl.execute(args[0] || "", null, true);
2010 vbs.setFrom = setFrom;
2014 completer: function (context) completion.ex(context),
2020 commands.add(["ve[rsion]"],
2021 "Show version information",
2024 dactyl.open("about:");
2026 commandline.commandOutput(<>
2027 {config.appName} {config.version} running on:<br/>{navigator.userAgent}
2036 completion: function () {
2037 completion.dialog = function dialog(context) {
2038 context.title = ["Dialog"];
2039 context.filters.push(function ({ item }) !item[2] || item[2]());
2040 context.completions = [[k, v[0], v[2]] for ([k, v] in Iterator(config.dialogs))];
2043 completion.help = function help(context, consolidated) {
2045 context.title = ["Help"];
2046 context.anchored = false;
2047 context.completions = services["dactyl:"].HELP_TAGS;
2049 context.keys = { text: 0, description: function () "all" };
2052 completion.menuItem = function menuItem(context) {
2053 context.title = ["Menu Path", "Label"];
2054 context.anchored = false;
2055 context.keys = { text: "fullMenuPath", description: function (item) item.getAttribute("label") };
2056 context.completions = dactyl.menuItems;
2059 var toolbox = document.getElementById("navigator-toolbox");
2060 completion.toolbar = function toolbar(context) {
2061 context.title = ["Toolbar"];
2062 context.keys = { text: function (item) item.getAttribute("toolbarname"), description: function () "" };
2063 context.completions = util.evaluateXPath("//*[@toolbarname]", document);
2066 completion.window = function window(context) {
2067 context.title = ["Window", "Title"];
2068 context.keys = { text: function (win) dactyl.windows.indexOf(win) + 1, description: function (win) win.document.title };
2069 context.completions = dactyl.windows;
2073 dactyl.triggerObserver("load");
2075 dactyl.log(_("dactyl.modulesLoaded"), 3);
2077 dactyl.timeout(function () {
2079 var args = JSMLoader.commandlineArgs || services.commandLineHandler.optionValue;
2081 args = dactyl.parseCommandLine(args);
2084 dactyl.commandLineOptions.rcFile = args["+u"];
2085 dactyl.commandLineOptions.noPlugins = "++noplugin" in args;
2086 dactyl.commandLineOptions.postCommands = args["+c"];
2087 dactyl.commandLineOptions.preCommands = args["++cmd"];
2088 util.dump("Processing command-line option: " + args.string);
2092 dactyl.echoerr(_("dactyl.parsingCommandLine", e));
2095 dactyl.log(_("dactyl.commandlineOpts", util.objectToString(dactyl.commandLineOptions)), 3);
2097 // first time intro message
2098 const firstTime = "extensions." + config.name + ".firsttime";
2099 if (prefs.get(firstTime, true)) {
2100 dactyl.timeout(function () {
2101 this.withSavedValues(["forceNewTab"], function () {
2102 this.forceNewTab = true;
2104 prefs.set(firstTime, false);
2109 // TODO: we should have some class where all this guioptions stuff fits well
2110 // Dactyl.hideGUI();
2112 if (dactyl.userEval("typeof document", null, "test.js") === "undefined")
2113 jsmodules.__proto__ = XPCSafeJSObjectWrapper(window);
2115 if (dactyl.commandLineOptions.preCommands)
2116 dactyl.commandLineOptions.preCommands.forEach(function (cmd) {
2117 dactyl.execute(cmd);
2120 // finally, read the RC file and source plugins
2121 let init = services.environment.get(config.idName + "_INIT");
2122 let rcFile = io.getRCFile("~");
2125 if (dactyl.commandLineOptions.rcFile) {
2126 let filename = dactyl.commandLineOptions.rcFile;
2127 if (!/^(NONE|NORC)$/.test(filename))
2128 io.source(io.File(filename).path, { group: contexts.user });
2132 dactyl.execute(init);
2135 io.source(rcFile.path, { group: contexts.user });
2136 services.environment.set("MY_" + config.idName + "RC", rcFile.path);
2139 dactyl.log(_("dactyl.noRCFile"), 3);
2142 if (options["exrc"] && !dactyl.commandLineOptions.rcFile) {
2143 let localRCFile = io.getRCFile(io.cwd);
2144 if (localRCFile && !localRCFile.equals(rcFile))
2145 io.source(localRCFile.path, { group: contexts.user });
2149 if (dactyl.commandLineOptions.rcFile == "NONE" || dactyl.commandLineOptions.noPlugins)
2150 options["loadplugins"] = [];
2152 if (options["loadplugins"])
2153 dactyl.loadPlugins();
2156 dactyl.reportError(e, true);
2159 // after sourcing the initialization files, this function will set
2160 // all gui options to their default values, if they have not been
2161 // set before by any RC file
2162 for (let option in values(options.needInit))
2165 if (dactyl.commandLineOptions.postCommands)
2166 dactyl.commandLineOptions.postCommands.forEach(function (cmd) {
2167 dactyl.execute(cmd);
2170 if (JSMLoader.rehashCmd)
2171 dactyl.execute(JSMLoader.rehashCmd);
2172 JSMLoader.rehashCmd = null;
2174 dactyl.fullyInitialized = true;
2175 dactyl.triggerObserver("enter", null);
2176 autocommands.trigger("Enter", {});
2179 statusline.update();
2180 dactyl.log(_("dactyl.initialized", config.appName), 0);
2181 dactyl.initialized = true;
2185 // vim: set fdm=marker sw=4 ts=4 et: