]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/config.jsm
Import 1.0 supporting Firefox up to 14.*
[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 this.lazyRequire("styles", ["Styles"]);
23
24 function AboutHandler() {}
25 AboutHandler.prototype = {
26     get classDescription() "About " + config.appName + " Page",
27
28     classID: Components.ID("81495d80-89ee-4c36-a88d-ea7c4e5ac63f"),
29
30     get contractID() services.ABOUT + config.name,
31
32     QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
33
34     newChannel: function (uri) {
35         let channel = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService)
36                           .newChannel("dactyl://content/about.xul", null, null);
37         channel.originalURI = uri;
38         return channel;
39     },
40
41     getURIFlags: function (uri) Ci.nsIAboutModule.ALLOW_SCRIPT,
42 };
43 var ConfigBase = Class("ConfigBase", {
44     /**
45      * Called on dactyl startup to allow for any arbitrary application-specific
46      * initialization code. Must call superclass's init function.
47      */
48     init: function init() {
49         this.loadConfig();
50
51         this.features.push = deprecated("Set.add", function push(feature) Set.add(this, feature));
52         if (this.haveGecko("2b"))
53             Set.add(this.features, "Gecko2");
54
55         JSMLoader.registerFactory(JSMLoader.Factory(AboutHandler));
56         JSMLoader.registerFactory(JSMLoader.Factory(
57             Protocol("dactyl", "{9c8f2530-51c8-4d41-b356-319e0b155c44}",
58                      "resource://dactyl-content/")));
59
60         this.timeout(function () {
61             cache.register("config.dtd", function () util.makeDTD(config.dtd));
62         });
63
64         services["dactyl:"].pages["dtd"] = function () [null, cache.get("config.dtd")];
65
66         update(services["dactyl:"].providers, {
67             "locale": function (uri, path) LocaleChannel("dactyl-locale", config.locale, path, uri),
68             "locale-local": function (uri, path) LocaleChannel("dactyl-local-locale", config.locale, path, uri)
69         });
70     },
71
72     get prefs() localPrefs,
73
74     get has() Set.has(this.features),
75
76     configFiles: [
77         "resource://dactyl-common/config.json",
78         "resource://dactyl-local/config.json"
79     ],
80
81     configs: Class.Memoize(function () this.configFiles.map(function (url) JSON.parse(File.readURL(url)))),
82
83     loadConfig: function loadConfig(documentURL) {
84
85         for each (let config in this.configs) {
86             if (documentURL)
87                 config = config.overlays && config.overlays[documentURL] || {};
88
89             for (let [name, value] in Iterator(config)) {
90                 let prop = util.camelCase(name);
91
92                 if (isArray(this[prop]))
93                     this[prop] = [].concat(this[prop], value);
94                 else if (isObject(this[prop])) {
95                     if (isArray(value))
96                         value = Set(value);
97
98                     this[prop] = update({}, this[prop],
99                                         iter([util.camelCase(k), value[k]]
100                                              for (k in value)).toObject());
101                 }
102                 else
103                     this[prop] = value;
104             }
105         }
106     },
107
108     modules: {
109         global: ["addons",
110                  "base",
111                  "io",
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(<![CDATA[
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);
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 topic="command-line">command line</link>,
427         "tag.status-line":  <link topic="status-line">status line</link>,
428         "mode.command-line": <link topic="command-line-mode">Command Line</link>,
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 = <e4x xmlns={XUL} xmlns:dactyl={NS}>
456                     <menupopup id="viewSidebarMenu"/>
457                     <broadcasterset id="mainBroadcasterSet"/>
458             </e4x>;
459             for each (let [id, [name, key, uri]] in Iterator(this.sidebars)) {
460                 append.XUL::menupopup[0].* +=
461                         <menuitem observes={"pentadactyl-" + id + "Sidebar"} label={name} accesskey={key} xmlns={XUL}/>;
462                 append.XUL::broadcasterset[0].* +=
463                         <broadcaster id={"pentadactyl-" + id + "Sidebar"}
464                             autoCheck="false" type="checkbox" group="sidebar"
465                             sidebartitle={name} sidebarurl={uri}
466                             oncommand="toggleSidebar(this.id || this.observes);" xmlns={XUL}/>;
467             }
468
469             util.overlayWindow(window, { append: append.elements() });
470         },
471
472         get window() window,
473
474         get document() document,
475
476         ids: Class.Update({
477             get commandContainer() document.documentElement.id
478         }),
479
480         browser: Class.Memoize(function () window.gBrowser),
481         tabbrowser: Class.Memoize(function () window.gBrowser),
482
483         get browserModes() [modules.modes.NORMAL],
484
485         /**
486          * @property {string} The ID of the application's main XUL window.
487          */
488         mainWindowId: document.documentElement.id,
489
490         /**
491          * @property {number} The height (px) that is available to the output
492          *     window.
493          */
494         get outputHeight() this.browser.mPanelContainer.boxObject.height,
495
496         tabStrip: Class.Memoize(function () document.getElementById("TabsToolbar") || this.tabbrowser.mTabContainer),
497     }),
498
499     /**
500      * @property {Object} A mapping of names and descriptions
501      *     of the autocommands available in this application. Primarily used
502      *     for completion results.
503      */
504     autocommands: {},
505
506     /**
507      * @property {Object} A map of :command-complete option values to completer
508      *     function names.
509      */
510     completers: {},
511
512     /**
513      * @property {Object} Application specific defaults for option values. The
514      *     property names must be the options' canonical names, and the values
515      *     must be strings as entered via :set.
516      */
517     optionDefaults: {},
518
519     cleanups: {},
520
521     /**
522      * @property {Object} A map of dialogs available via the
523      *      :dialog command. Property names map dialog names to an array
524      *      with the following elements:
525      *  [0] description - A description of the dialog, used in
526      *                    command completion results for :dialog.
527      *  [1] action - The function executed by :dialog.
528      *  [2] test - Function which returns true if the dialog is available in
529      *      the current window. @optional
530      */
531     dialogs: {},
532
533     /**
534      * @property {set} A list of features available in this
535      *    application. Used extensively in feature test macros. Use
536      *    dactyl.has(feature) to check for a feature's presence
537      *    in this array.
538      */
539     features: {},
540
541     /**
542      * @property {string} The file extension used for command script files.
543      *     This is the name string sans "dactyl".
544      */
545     get fileExtension() this.name.slice(0, -6),
546
547     guioptions: {},
548
549     /**
550      * @property {string} The name of the application that hosts the
551      *     extension. E.g., "Firefox" or "XULRunner".
552      */
553     host: null,
554
555     /**
556      * @property {string} The name of the extension.
557      *    Required.
558      */
559     name: null,
560
561     /**
562      * @property {[string]} A list of extra scripts in the dactyl or
563      *    application namespaces which should be loaded before dactyl
564      *    initialization.
565      */
566     scripts: [],
567
568     sidebars: {},
569
570     /**
571      * @property {string} The leaf name of any temp files created by
572      *     {@link io.createTempFile}.
573      */
574     get tempFile() this.name + ".txt",
575
576     /**
577      * @constant
578      * @property {string} The default highlighting rules.
579      * See {@link Highlights#loadCSS} for details.
580      */
581     CSS: Class.Memoize(function () File.readURL("resource://dactyl-skin/global-styles.css")),
582
583     helpCSS: Class.Memoize(function () File.readURL("resource://dactyl-skin/help-styles.css"))
584 }, {
585 });
586 JSMLoader.loadSubScript("resource://dactyl-local-content/config.js", this);
587
588 config.INIT = update(Object.create(config.INIT), config.INIT, {
589     init: function init(dactyl, modules, window) {
590         init.superapply(this, arguments);
591
592         let img = window.Image();
593         img.src = this.logo || "resource://dactyl-local-content/logo.png";
594         img.onload = util.wrapCallback(function () {
595             highlight.loadCSS(<>{"!Logo  {"}
596                      display:    inline-block;
597                      background: url({img.src});
598                      width:      {img.width}px;
599                      height:     {img.height}px;
600                  {"}"}</>);
601             img = null;
602         });
603     },
604
605     load: function load(dactyl, modules, window) {
606         load.superapply(this, arguments);
607
608         this.timeout(function () {
609             if (this.branch && this.branch !== "default" &&
610                     modules.yes_i_know_i_should_not_report_errors_in_these_branches_thanks.indexOf(this.branch) === -1)
611                 dactyl.warn(_("warn.notDefaultBranch", config.appName, this.branch));
612         }, 1000);
613     }
614 });
615
616 endModule();
617
618 // catch(e){ if (typeof e === "string") e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
619
620 // vim: set fdm=marker sw=4 sts=4 et ft=javascript: