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