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