]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/config.jsm
9e5b7c0c59ea3526d4c79afcd5c0f769de8d2eee
[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-2012 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", function () util.makeDTD(config.dtd));
61         });
62
63         services["dactyl:"].pages["dtd"] = function () [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(function (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, function (m0, m1) _(m1)));
156         highlight.loadCSS(this.helpCSS.replace(/__MSG_(.*?)__/g, function (m0, m1) _(m1)));
157
158         if (!this.haveGecko("2b"))
159             highlight.loadCSS(literal(/*
160                 !TabNumber               font-weight: bold; margin: 0px; padding-right: .8ex;
161                 !TabIconNumber  {
162                     font-weight: bold;
163                     color: white;
164                     text-align: center;
165                     text-shadow: black -1px 0 1px, black 0 1px 1px, black 1px 0 1px, black 0 -1px 1px;
166                 }
167             */));
168
169         let hl = highlight.set("Find", "");
170         hl.onChange = function () {
171             function hex(val) ("#" + util.regexp.iterate(/\d+/g, val)
172                                          .map(function (num) ("0" + Number(num).toString(16)).slice(-2))
173                                          .join("")
174                               ).slice(0, 7);
175
176             let elem = services.appShell.hiddenDOMWindow.document.createElement("div");
177             elem.style.cssText = this.cssText;
178
179             let keys = iter(Styles.propertyIter(this.cssText)).map(function (p) p.name).toArray();
180             let bg = keys.some(bind("test", /^background/));
181             let fg = keys.indexOf("color") >= 0;
182
183             let style = DOM(elem).style;
184             prefs[bg ? "safeSet" : "safeReset"]("ui.textHighlightBackground", hex(style.backgroundColor));
185             prefs[fg ? "safeSet" : "safeReset"]("ui.textHighlightForeground", hex(style.color));
186         };
187     },
188
189     get addonID() this.name + "@dactyl.googlecode.com",
190
191     addon: Class.Memoize(function () {
192         return (JSMLoader.bootstrap || {}).addon ||
193                     AddonManager.getAddonByID(this.addonID);
194     }),
195
196     get styleableChrome() Object.keys(this.overlays),
197
198     /**
199      * The current application locale.
200      */
201     appLocale: Class.Memoize(function () services.chromeRegistry.getSelectedLocale("global")),
202
203     /**
204      * The current dactyl locale.
205      */
206     locale: Class.Memoize(function () this.bestLocale(this.locales)),
207
208     /**
209      * The current application locale.
210      */
211     locales: Class.Memoize(function () {
212         // TODO: Merge with completion.file code.
213         function getDir(str) str.match(/^(?:.*[\/\\])?/)[0];
214
215         let uri = "resource://dactyl-locale/";
216         let jar = io.isJarURL(uri);
217         if (jar) {
218             let prefix = getDir(jar.JAREntry);
219             var res = iter(s.slice(prefix.length).replace(/\/.*/, "")
220                            for (s in io.listJar(jar.JARFile, prefix)))
221                         .toArray();
222         }
223         else {
224             res = array(f.leafName
225                         // Fails on FF3: for (f in util.getFile(uri).iterDirectory())
226                         for (f in values(util.getFile(uri).readDirectory()))
227                         if (f.isDirectory())).array;
228         }
229
230         let exists = function exists(pkg) services["resource:"].hasSubstitution("dactyl-locale-" + pkg);
231
232         return array.uniq([this.appLocale, this.appLocale.replace(/-.*/, "")]
233                             .filter(exists)
234                             .concat(res));
235     }),
236
237     /**
238      * Returns the best locale match to the current locale from a list
239      * of available locales.
240      *
241      * @param {[string]} list A list of available locales
242      * @returns {string}
243      */
244     bestLocale: function (list) {
245         return values([this.appLocale, this.appLocale.replace(/-.*/, ""),
246                        "en", "en-US", list[0]])
247             .nth(Set.has(Set(list)), 0);
248     },
249
250     /**
251      * A list of all known registered chrome and resource packages.
252      */
253     get chromePackages() {
254         // Horrible hack.
255         let res = {};
256         function process(manifest) {
257             for each (let line in manifest.split(/\n+/)) {
258                 let match = /^\s*(content|skin|locale|resource)\s+([^\s#]+)\s/.exec(line);
259                 if (match)
260                     res[match[2]] = true;
261             }
262         }
263         function processJar(file) {
264             let jar = services.ZipReader(file.file);
265             if (jar)
266                 try {
267                     if (jar.hasEntry("chrome.manifest"))
268                         process(File.readStream(jar.getInputStream("chrome.manifest")));
269                 }
270                 finally {
271                     jar.close();
272                 }
273         }
274
275         for each (let dir in ["UChrm", "AChrom"]) {
276             dir = File(services.directory.get(dir, Ci.nsIFile));
277             if (dir.exists() && dir.isDirectory())
278                 for (let file in dir.iterDirectory())
279                     if (/\.manifest$/.test(file.leafName))
280                         process(file.read());
281
282             dir = File(dir.parent);
283             if (dir.exists() && dir.isDirectory())
284                 for (let file in dir.iterDirectory())
285                     if (/\.jar$/.test(file.leafName))
286                         processJar(file);
287
288             dir = dir.child("extensions");
289             if (dir.exists() && dir.isDirectory())
290                 for (let ext in dir.iterDirectory()) {
291                     if (/\.xpi$/.test(ext.leafName))
292                         processJar(ext);
293                     else {
294                         if (ext.isFile())
295                             ext = File(ext.read().replace(/\n*$/, ""));
296                         let mf = ext.child("chrome.manifest");
297                         if (mf.exists())
298                             process(mf.read());
299                     }
300                 }
301         }
302         return Object.keys(res).sort();
303     },
304
305     /**
306      * Returns true if the current Gecko runtime is of the given version
307      * or greater.
308      *
309      * @param {string} min The minimum required version. @optional
310      * @param {string} max The maximum required version. @optional
311      * @returns {boolean}
312      */
313     haveGecko: function (min, max) let ({ compare } = services.versionCompare,
314                                         { platformVersion } = services.runtime)
315         (min == null || compare(platformVersion, min) >= 0) &&
316         (max == null || compare(platformVersion, max) < 0),
317
318     /** Dactyl's notion of the current operating system platform. */
319     OS: memoize({
320         _arch: services.runtime.OS,
321         /**
322          * @property {string} The normalised name of the OS. This is one of
323          *     "Windows", "Mac OS X" or "Unix".
324          */
325         get name() this.isWindows ? "Windows" : this.isMacOSX ? "Mac OS X" : "Unix",
326         /** @property {boolean} True if the OS is Windows. */
327         get isWindows() this._arch == "WINNT",
328         /** @property {boolean} True if the OS is Mac OS X. */
329         get isMacOSX() this._arch == "Darwin",
330         /** @property {boolean} True if the OS is some other *nix variant. */
331         get isUnix() !this.isWindows,
332         /** @property {RegExp} A RegExp which matches illegal characters in path components. */
333         get illegalCharacters() this.isWindows ? /[<>:"/\\|?*\x00-\x1f]/g : /[\/\x00]/g,
334
335         get pathListSep() this.isWindows ? ";" : ":"
336     }),
337
338     /**
339      * @property {string} The pathname of the VCS repository clone's root
340      *     directory if the application is running from one via an extension
341      *     proxy file.
342      */
343     VCSPath: Class.Memoize(function () {
344         if (/pre$/.test(this.addon.version)) {
345             let uri = util.newURI(this.addon.getResourceURI("").spec + "../.hg");
346             if (uri instanceof Ci.nsIFileURL &&
347                     uri.file.exists() &&
348                     io.pathSearch("hg"))
349                 return uri.file.parent.path;
350         }
351         return null;
352     }),
353
354     /**
355      * @property {string} The name of the VCS branch that the application is
356      *     running from if using an extension proxy file or was built from if
357      *     installed as an XPI.
358      */
359     branch: Class.Memoize(function () {
360         if (this.VCSPath)
361             return io.system(["hg", "-R", this.VCSPath, "branch"]).output;
362         return (/pre-hg\d+-(\S*)/.exec(this.version) || [])[1];
363     }),
364
365     /** @property {string} The name of the current user profile. */
366     profileName: Class.Memoize(function () {
367         // NOTE: services.profile.selectedProfile.name doesn't return
368         // what you might expect. It returns the last _actively_ selected
369         // profile (i.e. via the Profile Manager or -P option) rather than the
370         // current profile. These will differ if the current process was run
371         // without explicitly selecting a profile.
372
373         let dir = services.directory.get("ProfD", Ci.nsIFile);
374         for (let prof in iter(services.profile.profiles))
375             if (prof.QueryInterface(Ci.nsIToolkitProfile).rootDir.path === dir.path)
376                 return prof.name;
377         return "unknown";
378     }),
379
380     /** @property {string} The Dactyl version string. */
381     version: Class.Memoize(function () {
382         if (this.VCSPath)
383             return io.system(["hg", "-R", this.VCSPath, "log", "-r.",
384                               "--template=hg{rev}-{branch}"]).output;
385
386         return this.addon.version;
387     }),
388
389     buildDate: Class.Memoize(function () {
390         if (this.VCSPath)
391             return io.system(["hg", "-R", this.VCSPath, "log", "-r.",
392                               "--template={date|isodate}"]).output;
393         if ("@DATE@" !== "@" + "DATE@")
394             return _("dactyl.created", "@DATE@");
395     }),
396
397     get fileExt() this.name.slice(0, -6),
398
399     dtd: Class.Memoize(function ()
400         iter(this.dtdExtra,
401              (["dactyl." + k, v] for ([k, v] in iter(config.dtdDactyl))),
402              (["dactyl." + s, config[s]] for each (s in config.dtdStrings)))
403             .toObject()),
404
405     dtdDactyl: memoize({
406         get name() config.name,
407         get home() "http://5digits.org/",
408         get apphome() this.home + this.name,
409         code: "http://code.google.com/p/dactyl/",
410         get issues() this.home + "bug/" + this.name,
411         get plugins() "http://5digits.org/" + this.name + "/plugins",
412         get faq() this.home + this.name + "/faq",
413
414         "list.mailto": Class.Memoize(function () config.name + "@googlegroups.com"),
415         "list.href": Class.Memoize(function () "http://groups.google.com/group/" + config.name),
416
417         "hg.latest": Class.Memoize(function () this.code + "source/browse/"), // XXX
418         "irc": "irc://irc.oftc.net/#pentadactyl"
419     }),
420
421     dtdExtra: {
422         "xmlns.dactyl": "http://vimperator.org/namespaces/liberator",
423         "xmlns.html":   "http://www.w3.org/1999/xhtml",
424         "xmlns.xul":    "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
425
426         "tag.command-line": ["link", { xmlns: "dactyl", topic: "command-line" }, "command line"],
427         "tag.status-line":  ["link", { xmlns: "dactyl", topic: "status-line" }, "status line"],
428         "mode.command-line": ["link", { xmlns: "dactyl", topic: "command-line-mode" }, "Command Line"]
429     },
430
431     dtdStrings: [
432         "appName",
433         "fileExt",
434         "host",
435         "hostbin",
436         "idName",
437         "name",
438         "version"
439     ],
440
441     helpStyles: /^(Help|StatusLine|REPL)|^(Boolean|Dense|Indicator|MoreMsg|Number|Object|Logo|Key(word)?|String)$/,
442     styleHelp: function styleHelp() {
443         if (!this.helpStyled) {
444             for (let k in keys(highlight.loaded))
445                 if (this.helpStyles.test(k))
446                     highlight.loaded[k] = true;
447         }
448         this.helpCSS = true;
449     },
450
451     Local: function Local(dactyl, modules, { document, window }) ({
452         init: function init() {
453             this.loadConfig(document.documentURI);
454
455             let append = [
456                     ["menupopup", { id: "viewSidebarMenu", xmlns: "xul" }],
457                     ["broadcasterset", { id: "mainBroadcasterSet", xmlns: "xul" }]];
458
459             for each (let [id, [name, key, uri]] in Iterator(this.sidebars)) {
460                 append[0].push(
461                         ["menuitem", { observes: "pentadactyl-" + id + "Sidebar", label: name,
462                                        accesskey: key }]);
463                 append[1].push(
464                         ["broadcaster", { id: "pentadactyl-" + id + "Sidebar", autoCheck: "false",
465                                           type: "checkbox", group: "sidebar", sidebartitle: name,
466                                           sidebarurl: uri,
467                                           oncommand: "toggleSidebar(this.id || this.observes);" }]);
468             }
469
470             util.overlayWindow(window, { append: append });
471         },
472
473         get window() window,
474
475         get document() document,
476
477         ids: Class.Update({
478             get commandContainer() document.documentElement.id
479         }),
480
481         browser: Class.Memoize(function () window.gBrowser),
482         tabbrowser: Class.Memoize(function () window.gBrowser),
483
484         get browserModes() [modules.modes.NORMAL],
485
486         /**
487          * @property {string} The ID of the application's main XUL window.
488          */
489         mainWindowId: document.documentElement.id,
490
491         /**
492          * @property {number} The height (px) that is available to the output
493          *     window.
494          */
495         get outputHeight() this.browser.mPanelContainer.boxObject.height,
496
497         tabStrip: Class.Memoize(function () document.getElementById("TabsToolbar") || this.tabbrowser.mTabContainer)
498     }),
499
500     /**
501      * @property {Object} A mapping of names and descriptions
502      *     of the autocommands available in this application. Primarily used
503      *     for completion results.
504      */
505     autocommands: {},
506
507     /**
508      * @property {Object} A map of :command-complete option values to completer
509      *     function names.
510      */
511     completers: {},
512
513     /**
514      * @property {Object} Application specific defaults for option values. The
515      *     property names must be the options' canonical names, and the values
516      *     must be strings as entered via :set.
517      */
518     optionDefaults: {},
519
520     cleanups: {},
521
522     /**
523      * @property {Object} A map of dialogs available via the
524      *      :dialog command. Property names map dialog names to an array
525      *      with the following elements:
526      *  [0] description - A description of the dialog, used in
527      *                    command completion results for :dialog.
528      *  [1] action - The function executed by :dialog.
529      *  [2] test - Function which returns true if the dialog is available in
530      *      the current window. @optional
531      */
532     dialogs: {},
533
534     /**
535      * @property {set} A list of features available in this
536      *    application. Used extensively in feature test macros. Use
537      *    dactyl.has(feature) to check for a feature's presence
538      *    in this array.
539      */
540     features: {},
541
542     /**
543      * @property {string} The file extension used for command script files.
544      *     This is the name string sans "dactyl".
545      */
546     get fileExtension() this.name.slice(0, -6),
547
548     guioptions: {},
549
550     /**
551      * @property {string} The name of the application that hosts the
552      *     extension. E.g., "Firefox" or "XULRunner".
553      */
554     host: null,
555
556     /**
557      * @property {string} The name of the extension.
558      *    Required.
559      */
560     name: null,
561
562     /**
563      * @property {[string]} A list of extra scripts in the dactyl or
564      *    application namespaces which should be loaded before dactyl
565      *    initialization.
566      */
567     scripts: [],
568
569     sidebars: {},
570
571     /**
572      * @constant
573      * @property {string} The default highlighting rules.
574      * See {@link Highlights#loadCSS} for details.
575      */
576     CSS: Class.Memoize(function () File.readURL("resource://dactyl-skin/global-styles.css")),
577
578     helpCSS: Class.Memoize(function () File.readURL("resource://dactyl-skin/help-styles.css"))
579 }, {
580 });
581 JSMLoader.loadSubScript("resource://dactyl-local-content/config.js", this);
582
583 config.INIT = update(Object.create(config.INIT), config.INIT, {
584     init: function init(dactyl, modules, window) {
585         init.superapply(this, arguments);
586
587         let img = window.Image();
588         img.src = this.logo || "resource://dactyl-local-content/logo.png";
589         img.onload = util.wrapCallback(function () {
590             highlight.loadCSS(literal(/*
591                 !Logo  {
592                      display:    inline-block;
593                      background: url({src});
594                      width:      {width}px;
595                      height:     {height}px;
596                 }
597             */).replace(/\{(.*?)\}/g, function (m, m1) img[m1]));
598             img = null;
599         });
600     },
601
602     load: function load(dactyl, modules, window) {
603         load.superapply(this, arguments);
604
605         this.timeout(function () {
606             if (this.branch && this.branch !== "default" &&
607                     modules.yes_i_know_i_should_not_report_errors_in_these_branches_thanks.indexOf(this.branch) === -1)
608                 dactyl.warn(_("warn.notDefaultBranch", config.appName, this.branch));
609         }, 1000);
610     }
611 });
612
613 endModule();
614
615 // catch(e){ if (typeof e === "string") e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
616
617 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: