]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/config.jsm
Import r6976 from upstream hg supporting Firefox up to 25.*
[dactyl.git] / common / modules / config.jsm
1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2013 Kris Maglione <maglione.k@gmail.com>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 "use strict";
8
9 let global = this;
10 defineModule("config", {
11     exports: ["ConfigBase", "Config", "config"],
12     require: ["dom", "io", "protocol", "services", "util", "template"]
13 });
14
15 lazyRequire("addons", ["AddonManager"]);
16 lazyRequire("cache", ["cache"]);
17 lazyRequire("highlight", ["highlight"]);
18 lazyRequire("messages", ["_"]);
19 lazyRequire("prefs", ["localPrefs", "prefs"]);
20 lazyRequire("storage", ["storage", "File"]);
21 lazyRequire("styles", ["Styles"]);
22
23 function AboutHandler() {}
24 AboutHandler.prototype = {
25     get classDescription() "About " + config.appName + " Page",
26
27     classID: Components.ID("81495d80-89ee-4c36-a88d-ea7c4e5ac63f"),
28
29     get contractID() services.ABOUT + config.name,
30
31     QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
32
33     newChannel: function (uri) {
34         let channel = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService)
35                           .newChannel("dactyl://content/about.xul", null, null);
36         channel.originalURI = uri;
37         return channel;
38     },
39
40     getURIFlags: function (uri) Ci.nsIAboutModule.ALLOW_SCRIPT
41 };
42 var ConfigBase = Class("ConfigBase", {
43     /**
44      * Called on dactyl startup to allow for any arbitrary application-specific
45      * initialization code. Must call superclass's init function.
46      */
47     init: function init() {
48         this.loadConfig();
49
50         this.features.push = deprecated("Set.add", function push(feature) Set.add(this, feature));
51         if (this.haveGecko("2b"))
52             Set.add(this.features, "Gecko2");
53
54         JSMLoader.registerFactory(JSMLoader.Factory(AboutHandler));
55         JSMLoader.registerFactory(JSMLoader.Factory(
56             Protocol("dactyl", "{9c8f2530-51c8-4d41-b356-319e0b155c44}",
57                      "resource://dactyl-content/")));
58
59         this.timeout(function () {
60             cache.register("config.dtd", () => util.makeDTD(config.dtd));
61         });
62
63         services["dactyl:"].pages["dtd"] = () => [null, cache.get("config.dtd")];
64
65         update(services["dactyl:"].providers, {
66             "locale": function (uri, path) LocaleChannel("dactyl-locale", config.locale, path, uri),
67             "locale-local": function (uri, path) LocaleChannel("dactyl-local-locale", config.locale, path, uri)
68         });
69     },
70
71     get prefs() localPrefs,
72
73     get has() Set.has(this.features),
74
75     configFiles: [
76         "resource://dactyl-common/config.json",
77         "resource://dactyl-local/config.json"
78     ],
79
80     configs: Class.Memoize(function () this.configFiles.map(url => JSON.parse(File.readURL(url)))),
81
82     loadConfig: function loadConfig(documentURL) {
83
84         for each (let config in this.configs) {
85             if (documentURL)
86                 config = config.overlays && config.overlays[documentURL] || {};
87
88             for (let [name, value] in Iterator(config)) {
89                 let prop = util.camelCase(name);
90
91                 if (isArray(this[prop]))
92                     this[prop] = [].concat(this[prop], value);
93                 else if (isObject(this[prop])) {
94                     if (isArray(value))
95                         value = Set(value);
96
97                     this[prop] = update({}, this[prop],
98                                         iter([util.camelCase(k), value[k]]
99                                              for (k in value)).toObject());
100                 }
101                 else
102                     this[prop] = value;
103             }
104         }
105     },
106
107     modules: {
108         global: ["addons",
109                  "base",
110                  "io",
111                  ["bookmarkcache", "bookmarkcache"],
112                  "buffer",
113                  "cache",
114                  "commands",
115                  "completion",
116                  "config",
117                  "contexts",
118                  "dom",
119                  "downloads",
120                  "finder",
121                  "help",
122                  "highlight",
123                  "javascript",
124                  "main",
125                  "messages",
126                  "options",
127                  "overlay",
128                  "prefs",
129                  "protocol",
130                  "sanitizer",
131                  "services",
132                  "storage",
133                  "styles",
134                  "template",
135                  "util"],
136
137         window: ["dactyl",
138                  "modes",
139                  "commandline",
140                  "abbreviations",
141                  "autocommands",
142                  "editor",
143                  "events",
144                  "hints",
145                  "key-processors",
146                  "mappings",
147                  "marks",
148                  "mow",
149                  "statusline"]
150     },
151
152     loadStyles: function loadStyles(force) {
153         highlight.styleableChrome = this.styleableChrome;
154
155         highlight.loadCSS(this.CSS.replace(/__MSG_(.*?)__/g,
156                                            (m0, m1) => _(m1)));
157         highlight.loadCSS(this.helpCSS.replace(/__MSG_(.*?)__/g,
158                                                (m0, m1) => _(m1)));
159
160         if (!this.haveGecko("2b"))
161             highlight.loadCSS(literal(/*
162                 !TabNumber               font-weight: bold; margin: 0px; padding-right: .8ex;
163                 !TabIconNumber  {
164                     font-weight: bold;
165                     color: white;
166                     text-align: center;
167                     text-shadow: black -1px 0 1px, black 0 1px 1px, black 1px 0 1px, black 0 -1px 1px;
168                 }
169             */));
170
171         let hl = highlight.set("Find", "");
172         hl.onChange = function () {
173             function hex(val) ("#" + util.regexp.iterate(/\d+/g, val)
174                                          .map(num => ("0" + Number(num).toString(16)).slice(-2))
175                                          .join("")
176                               ).slice(0, 7);
177
178             let elem = services.appShell.hiddenDOMWindow.document.createElement("div");
179             elem.style.cssText = this.cssText;
180
181             let keys = iter(Styles.propertyIter(this.cssText)).map(p => p.name).toArray();
182             let bg = keys.some(bind("test", /^background/));
183             let fg = keys.indexOf("color") >= 0;
184
185             let style = DOM(elem).style;
186             prefs[bg ? "safeSet" : "safeReset"]("ui.textHighlightBackground", hex(style.backgroundColor));
187             prefs[fg ? "safeSet" : "safeReset"]("ui.textHighlightForeground", hex(style.color));
188         };
189     },
190
191     get addonID() this.name + "@dactyl.googlecode.com",
192
193     addon: Class.Memoize(function () {
194         return (JSMLoader.bootstrap || {}).addon ||
195                     AddonManager.getAddonByID(this.addonID);
196     }),
197
198     get styleableChrome() Object.keys(this.overlays),
199
200     /**
201      * The current application locale.
202      */
203     appLocale: Class.Memoize(() => services.chromeRegistry.getSelectedLocale("global")),
204
205     /**
206      * The current dactyl locale.
207      */
208     locale: Class.Memoize(function () this.bestLocale(this.locales)),
209
210     /**
211      * The current application locale.
212      */
213     locales: Class.Memoize(function () {
214         // TODO: Merge with completion.file code.
215         function getDir(str) str.match(/^(?:.*[\/\\])?/)[0];
216
217         let uri = "resource://dactyl-locale/";
218         let jar = io.isJarURL(uri);
219         if (jar) {
220             let prefix = getDir(jar.JAREntry);
221             var res = iter(s.slice(prefix.length).replace(/\/.*/, "")
222                            for (s in io.listJar(jar.JARFile, prefix)))
223                         .toArray();
224         }
225         else {
226             res = array(f.leafName
227                         // Fails on FF3: for (f in util.getFile(uri).iterDirectory())
228                         for (f in values(util.getFile(uri).readDirectory()))
229                         if (f.isDirectory())).array;
230         }
231
232         let exists = function exists(pkg) services["resource:"].hasSubstitution("dactyl-locale-" + pkg);
233
234         return array.uniq([this.appLocale, this.appLocale.replace(/-.*/, "")]
235                             .filter(exists)
236                             .concat(res));
237     }),
238
239     /**
240      * Returns the best locale match to the current locale from a list
241      * of available locales.
242      *
243      * @param {[string]} list A list of available locales
244      * @returns {string}
245      */
246     bestLocale: function (list) {
247         return values([this.appLocale, this.appLocale.replace(/-.*/, ""),
248                        "en", "en-US", list[0]])
249             .nth(Set.has(Set(list)), 0);
250     },
251
252     /**
253      * A list of all known registered chrome and resource packages.
254      */
255     get chromePackages() {
256         // Horrible hack.
257         let res = {};
258         function process(manifest) {
259             for each (let line in manifest.split(/\n+/)) {
260                 let match = /^\s*(content|skin|locale|resource)\s+([^\s#]+)\s/.exec(line);
261                 if (match)
262                     res[match[2]] = true;
263             }
264         }
265         function processJar(file) {
266             let jar = services.ZipReader(file.file);
267             if (jar)
268                 try {
269                     if (jar.hasEntry("chrome.manifest"))
270                         process(File.readStream(jar.getInputStream("chrome.manifest")));
271                 }
272                 finally {
273                     jar.close();
274                 }
275         }
276
277         for each (let dir in ["UChrm", "AChrom"]) {
278             dir = File(services.directory.get(dir, Ci.nsIFile));
279             if (dir.exists() && dir.isDirectory())
280                 for (let file in dir.iterDirectory())
281                     if (/\.manifest$/.test(file.leafName))
282                         process(file.read());
283
284             dir = File(dir.parent);
285             if (dir.exists() && dir.isDirectory())
286                 for (let file in dir.iterDirectory())
287                     if (/\.jar$/.test(file.leafName))
288                         processJar(file);
289
290             dir = dir.child("extensions");
291             if (dir.exists() && dir.isDirectory())
292                 for (let ext in dir.iterDirectory()) {
293                     if (/\.xpi$/.test(ext.leafName))
294                         processJar(ext);
295                     else {
296                         if (ext.isFile())
297                             ext = File(ext.read().replace(/\n*$/, ""));
298                         let mf = ext.child("chrome.manifest");
299                         if (mf.exists())
300                             process(mf.read());
301                     }
302                 }
303         }
304         return Object.keys(res).sort();
305     },
306
307     /**
308      * Returns true if the current Gecko runtime is of the given version
309      * or greater.
310      *
311      * @param {string} min The minimum required version. @optional
312      * @param {string} max The maximum required version. @optional
313      * @returns {boolean}
314      */
315     haveGecko: function (min, max) let ({ compare } = services.versionCompare,
316                                         { platformVersion } = services.runtime)
317         (min == null || compare(platformVersion, min) >= 0) &&
318         (max == null || compare(platformVersion, max) < 0),
319
320     /** Dactyl's notion of the current operating system platform. */
321     OS: memoize({
322         _arch: services.runtime.OS,
323         /**
324          * @property {string} The normalised name of the OS. This is one of
325          *     "Windows", "Mac OS X" or "Unix".
326          */
327         get name() this.isWindows ? "Windows" : this.isMacOSX ? "Mac OS X" : "Unix",
328         /** @property {boolean} True if the OS is Windows. */
329         get isWindows() this._arch == "WINNT",
330         /** @property {boolean} True if the OS is Mac OS X. */
331         get isMacOSX() this._arch == "Darwin",
332         /** @property {boolean} True if the OS is some other *nix variant. */
333         get isUnix() !this.isWindows,
334         /** @property {RegExp} A RegExp which matches illegal characters in path components. */
335         get illegalCharacters() this.isWindows ? /[<>:"/\\|?*\x00-\x1f]/g : /[\/\x00]/g,
336
337         get pathListSep() this.isWindows ? ";" : ":"
338     }),
339
340     /**
341      * @property {string} The pathname of the VCS repository clone's root
342      *     directory if the application is running from one via an extension
343      *     proxy file.
344      */
345     VCSPath: Class.Memoize(function () {
346         if (/pre$/.test(this.addon.version)) {
347             let uri = util.newURI(this.addon.getResourceURI("").spec + "../.hg");
348             if (uri instanceof Ci.nsIFileURL &&
349                     uri.file.exists() &&
350                     io.pathSearch("hg"))
351                 return uri.file.parent.path;
352         }
353         return null;
354     }),
355
356     /**
357      * @property {string} The name of the VCS branch that the application is
358      *     running from if using an extension proxy file or was built from if
359      *     installed as an XPI.
360      */
361     branch: Class.Memoize(function () {
362         if (this.VCSPath)
363             return io.system(["hg", "-R", this.VCSPath, "branch"]).output;
364         return (/pre-hg\d+-(\S*)/.exec(this.version) || [])[1];
365     }),
366
367     /** @property {string} The name of the current user profile. */
368     profileName: Class.Memoize(function () {
369         // NOTE: services.profile.selectedProfile.name doesn't return
370         // what you might expect. It returns the last _actively_ selected
371         // profile (i.e. via the Profile Manager or -P option) rather than the
372         // current profile. These will differ if the current process was run
373         // without explicitly selecting a profile.
374
375         let dir = services.directory.get("ProfD", Ci.nsIFile);
376         for (let prof in iter(services.profile.profiles))
377             if (prof.QueryInterface(Ci.nsIToolkitProfile).rootDir.path === dir.path)
378                 return prof.name;
379         return "unknown";
380     }),
381
382     /** @property {string} The Dactyl version string. */
383     version: Class.Memoize(function () {
384         if (this.VCSPath)
385             return io.system(["hg", "-R", this.VCSPath, "log", "-r.",
386                               "--template=hg{rev}-{branch}"]).output;
387
388         return this.addon.version;
389     }),
390
391     buildDate: Class.Memoize(function () {
392         if (this.VCSPath)
393             return io.system(["hg", "-R", this.VCSPath, "log", "-r.",
394                               "--template={date|isodate}"]).output;
395         if ("@DATE@" !== "@" + "DATE@")
396             return _("dactyl.created", "@DATE@");
397     }),
398
399     get fileExt() this.name.slice(0, -6),
400
401     dtd: Class.Memoize(function ()
402         iter(this.dtdExtra,
403              (["dactyl." + k, v] for ([k, v] in iter(config.dtdDactyl))),
404              (["dactyl." + s, config[s]] for each (s in config.dtdStrings)))
405             .toObject()),
406
407     dtdDactyl: memoize({
408         get name() config.name,
409         get home() "http://5digits.org/",
410         get apphome() this.home + this.name,
411         code: "http://code.google.com/p/dactyl/",
412         get issues() this.home + "bug/" + this.name,
413         get plugins() "http://5digits.org/" + this.name + "/plugins",
414         get faq() this.home + this.name + "/faq",
415
416         "list.mailto": Class.Memoize(() => config.name + "@googlegroups.com"),
417         "list.href": Class.Memoize(() => "http://groups.google.com/group/" + config.name),
418
419         "hg.latest": Class.Memoize(function () this.code + "source/browse/"), // XXX
420         "irc": "irc://irc.oftc.net/#pentadactyl"
421     }),
422
423     dtdExtra: {
424         "xmlns.dactyl": "http://vimperator.org/namespaces/liberator",
425         "xmlns.html":   "http://www.w3.org/1999/xhtml",
426         "xmlns.xul":    "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
427
428         "tag.command-line": ["link", { xmlns: "dactyl", topic: "command-line" }, "command line"],
429         "tag.status-line":  ["link", { xmlns: "dactyl", topic: "status-line" }, "status line"],
430         "mode.command-line": ["link", { xmlns: "dactyl", topic: "command-line-mode" }, "Command Line"]
431     },
432
433     dtdStrings: [
434         "appName",
435         "fileExt",
436         "host",
437         "hostbin",
438         "idName",
439         "name",
440         "version"
441     ],
442
443     helpStyles: /^(Help|StatusLine|REPL)|^(Boolean|Dense|Indicator|MoreMsg|Number|Object|Logo|Key(word)?|String)$/,
444     styleHelp: function styleHelp() {
445         if (!this.helpStyled) {
446             for (let k in keys(highlight.loaded))
447                 if (this.helpStyles.test(k))
448                     highlight.loaded[k] = true;
449         }
450         this.helpCSS = true;
451     },
452
453     Local: function Local(dactyl, modules, { document, window }) ({
454         init: function init() {
455             this.loadConfig(document.documentURI);
456
457             let append = [
458                     ["menupopup", { id: "viewSidebarMenu", xmlns: "xul" }],
459                     ["broadcasterset", { id: "mainBroadcasterSet", xmlns: "xul" }]];
460
461             for each (let [id, [name, key, uri]] in Iterator(this.sidebars)) {
462                 append[0].push(
463                         ["menuitem", { observes: "pentadactyl-" + id + "Sidebar", label: name,
464                                        accesskey: key }]);
465                 append[1].push(
466                         ["broadcaster", { id: "pentadactyl-" + id + "Sidebar", autoCheck: "false",
467                                           type: "checkbox", group: "sidebar", sidebartitle: name,
468                                           sidebarurl: uri,
469                                           oncommand: "toggleSidebar(this.id || this.observes);" }]);
470             }
471
472             util.overlayWindow(window, { append: append });
473         },
474
475         get window() window,
476
477         get document() document,
478
479         ids: Class.Update({
480             get commandContainer() document.documentElement.id
481         }),
482
483         browser: Class.Memoize(() => window.gBrowser),
484         tabbrowser: Class.Memoize(() => window.gBrowser),
485
486         get browserModes() [modules.modes.NORMAL],
487
488         /**
489          * @property {string} The ID of the application's main XUL window.
490          */
491         mainWindowId: document.documentElement.id,
492
493         /**
494          * @property {number} The height (px) that is available to the output
495          *     window.
496          */
497         get outputHeight() this.browser.mPanelContainer.boxObject.height,
498
499         tabStrip: Class.Memoize(function () document.getElementById("TabsToolbar") || this.tabbrowser.mTabContainer)
500     }),
501
502     /**
503      * @property {Object} A mapping of names and descriptions
504      *     of the autocommands available in this application. Primarily used
505      *     for completion results.
506      */
507     autocommands: {},
508
509     /**
510      * @property {Object} A map of :command-complete option values to completer
511      *     function names.
512      */
513     completers: {},
514
515     /**
516      * @property {Object} Application specific defaults for option values. The
517      *     property names must be the options' canonical names, and the values
518      *     must be strings as entered via :set.
519      */
520     optionDefaults: {},
521
522     cleanups: {},
523
524     /**
525      * @property {Object} A map of dialogs available via the
526      *      :dialog command. Property names map dialog names to an array
527      *      with the following elements:
528      *  [0] description - A description of the dialog, used in
529      *                    command completion results for :dialog.
530      *  [1] action - The function executed by :dialog.
531      *  [2] test - Function which returns true if the dialog is available in
532      *      the current window. @optional
533      */
534     dialogs: {},
535
536     /**
537      * @property {set} A list of features available in this
538      *    application. Used extensively in feature test macros. Use
539      *    dactyl.has(feature) to check for a feature's presence
540      *    in this array.
541      */
542     features: {},
543
544     /**
545      * @property {string} The file extension used for command script files.
546      *     This is the name string sans "dactyl".
547      */
548     get fileExtension() this.name.slice(0, -6),
549
550     guioptions: {},
551
552     /**
553      * @property {string} The name of the application that hosts the
554      *     extension. E.g., "Firefox" or "XULRunner".
555      */
556     host: null,
557
558     /**
559      * @property {string} The name of the extension.
560      *    Required.
561      */
562     name: null,
563
564     /**
565      * @property {[string]} A list of extra scripts in the dactyl or
566      *    application namespaces which should be loaded before dactyl
567      *    initialization.
568      */
569     scripts: [],
570
571     sidebars: {},
572
573     /**
574      * @constant
575      * @property {string} The default highlighting rules.
576      * See {@link Highlights#loadCSS} for details.
577      */
578     CSS: Class.Memoize(() => File.readURL("resource://dactyl-skin/global-styles.css")),
579
580     helpCSS: Class.Memoize(() => File.readURL("resource://dactyl-skin/help-styles.css"))
581 }, {
582 });
583 JSMLoader.loadSubScript("resource://dactyl-local-content/config.js", this);
584
585 config.INIT = update(Object.create(config.INIT), config.INIT, {
586     init: function init(dactyl, modules, window) {
587         init.superapply(this, arguments);
588
589         let img = window.Image();
590         img.src = this.logo || "resource://dactyl-local-content/logo.png";
591         img.onload = util.wrapCallback(function () {
592             highlight.loadCSS(literal(/*
593                 !Logo  {
594                      display:    inline-block;
595                      background: url({src});
596                      width:      {width}px;
597                      height:     {height}px;
598                 }
599             */).replace(/\{(.*?)\}/g, (m, m1) => img[m1]));
600             img = null;
601         });
602     },
603
604     load: function load(dactyl, modules, window) {
605         load.superapply(this, arguments);
606
607         this.timeout(function () {
608             if (this.branch && this.branch !== "default" &&
609                     modules.yes_i_know_i_should_not_report_errors_in_these_branches_thanks.indexOf(this.branch) === -1)
610                 dactyl.warn(_("warn.notDefaultBranch", config.appName, this.branch));
611         }, 1000);
612     }
613 });
614
615 endModule();
616
617 // catch(e){ if (typeof e === "string") e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
618
619 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: