Modified bootstrap.py to add files recursively in client dir, added bootstrap.py function to update UI config.js file based on app! docs in the couch, added some scaffolding to allow ui apps to just pull in config.js to get user config to use in an app.
authorJames Burke <jrburke@gmail.com>
Thu, 09 Apr 2009 14:48:21 -0700
changeset 172 5a6eeafe560e06f5cde2f6aa6e39c4526ae0dadc
parent 171 a12663e8f7985cc40f890183b9d744a306b38469
child 173 2d2dbca3135c05270ef9f34dba9737462a0a064b
push id3
push userjrburke@gmail.com
push dateThu, 09 Apr 2009 21:57:38 +0000
Modified bootstrap.py to add files recursively in client dir, added bootstrap.py function to update UI config.js file based on app! docs in the couch, added some scaffolding to allow ui apps to just pull in config.js to get user config to use in an app.
.hgignore
client/config.js
client/couch.xd.js
client/inflow/index.html
client/raindrop.xd.js
client/rd_list/index.html
schema/apps/all/all-map.js
schema/uiext/all/all-map.js
server/python/raindrop/bootstrap.py
server/python/run-raindrop.py
--- a/.hgignore
+++ b/.hgignore
@@ -1,7 +1,8 @@
 syntax:regexp
 \.egg
 \.egg-info
 \.pyc
 \.swp
+\.DS_Store
 ^server/python/build/
 ^nbproject/
new file mode 100644
--- /dev/null
+++ b/client/config.js
@@ -0,0 +1,66 @@
+;(function(){
+  //Find raindrop location
+  var scripts = document.getElementsByTagName("script");
+  var i = scripts.length - 1;
+  var prefix = "";
+  while(i > -1){
+    var src = scripts[i].src;
+    if(src && src.indexOf("/raindrop/files/config") != -1){
+      prefix = src.split("/").slice(0, 3).join("/") + "/raindrop/files";
+    }
+    i--;
+  }
+
+  djConfig = {
+    require: ["raindrop", "couch", "dojox.io.proxy.xip"],
+    baseUrl: "./",
+    couchUrl: "http://127.0.0.1:5984",
+    //iframeProxyUrl: "http://127.0.0.1:5984/raindrop/files/xip_server.html",
+    
+    //If we only support postMessage browsers, then don't need client URL
+    //This makes the assumption the app always includes xip_client.html in the
+    //same directory as any app page that uses the couchdb API.
+    xipClientUrl: "./xip_client.html",
+    modulePaths: {
+      /*INSERT CONFIG HERE*/
+      //"dojox.io.proxy.xip": prefix + "/xip",
+      "raindrop": prefix + "/raindrop",
+      "couch": prefix + "/couch"
+    }
+  };
+  
+  //TODO: just doing this here in JS because my python fu is weak.
+  //Need to split off the html file name from the application paths.
+  //Also need to strip off domain if it matches the page we are currently
+  //on, to avoid xd loading of modules (makes for easier debugging). That
+  //part might need to be kept in JavaScript. Using http headers on the server
+  //will not give us the full picture.
+  var parts = location.href.split("/");
+  var frameworkNames = {
+    "raindrop": 1,
+    "couch": 1,
+    //"dojox.io.proxy.xip": 1
+  };
+  
+  for (var param in djConfig.modulePaths) {
+    var value = djConfig.modulePaths[param];
+    
+    //Pull off . files from path
+    value = value.split("/");
+    if (value[value.length - 1].indexOf(".") != -1) {
+      value.pop();
+    }
+
+    //Adjust path to be local, if not one of the framework values.
+    if (!frameworkNames[param]) {
+        if (value[0] == parts[0] && value[1] == parts[1] && value[2] == parts[2]){
+          value.splice(0, 3, "");
+        }
+    }
+
+    value = value.join("/");
+    djConfig.modulePaths[param] = value;
+  }
+  
+  document.write('<script src="http://ajax.googleapis.com/ajax/libs/dojo/1.3.0/dojo/dojo.xd.js.uncompressed.js"></script>');
+})();
new file mode 100644
--- /dev/null
+++ b/client/couch.xd.js
@@ -0,0 +1,229 @@
+//Modified from couchdb's jquery.couch.js file.
+
+//xdomain loading wrapper. Can be inserted by a build process, but manually doing it for now.
+window[(typeof (djConfig)!="undefined"&&djConfig.scopeMap&&djConfig.scopeMap[0][1])||"dojo"]._xdResourceLoaded(function(dojo, dijit, dojox){
+  return {
+  depends:[["provide","couch"]],defineResource:function(dojo, dijit, dojox){
+
+//Main module definition
+dojo.provide("couch");
+
+;(function(){
+  function _handle(response, ioArgs) {
+    //Used as the callback for XHR calls. Figure out what options method to call.
+    var options = ioArgs.args;
+    if (response instanceof Error) {
+      if (options.error) {
+        var xhr = ioArgs.xhr;
+        options.error(xhr.status, xhr.statusText, xhr.responseText);
+      } else {
+        alert("An error occurred retrieving the list of all databases: " + response);
+      }
+    } else {
+      if (options.beforeSuccess){
+        options.beforeSuccess(response, ioArgs);
+      }else if (options.success) {
+        options.success(response);
+      }
+    }
+  }
+  
+  function _call(type, url, options, beforeSuccess) {
+    //Basic fetch method used by all calls.
+    options = options || {};
+    type = type.toUpperCase();
+  
+    //Make a new options so we do not tamper with existing options object.
+    var options = dojo.delegate(options);
+    dojo.mixin(options, {
+      url: url,
+      handleAs: "json",
+      beforeSuccess: beforeSuccess,
+      headers: {
+        contentType: "application/json"
+      },
+      iframeProxyUrl: djConfig.iframeProxyUrl,
+      handle: _handle,
+    });
+
+    dojo.xhr(type, options, (options.postData || options.putData));
+  }
+
+  // Convert a options object to an url query string.
+  // ex: {key:'value',key2:'value2'} becomes '?key="value"&key2="value2"'
+  function encodeOptions(options) {
+    var buf = []
+    if (typeof(options) == "object" && options !== null) {
+      for (var name in options) {
+        if (name == "error" || name == "success") continue;
+        var value = options[name];
+        // keys will result in a POST, so we don't need them in our GET part
+        if (name == "keys") continue;
+        if (name == "key" || name == "startkey" || name == "endkey") {
+          value = toJSON(value);
+        }
+        buf.push(encodeURIComponent(name) + "=" + encodeURIComponent(value));
+      }
+    }
+    return buf.length ? "?" + buf.join("&") : "";
+  }
+
+  function toJSON(obj) {
+    return obj !== null ? dojo.toJson(obj) : null;
+  }
+
+
+  function applyExtensions(extensions, rows) {
+    //Function that will apply extensions to a set of rows.
+    //These are "perspective" extensions that are applied after
+    //a couchdb view is run.
+
+    //First, convert extension string names to actual functions.    
+    extensions = dojo.map(extensions, function (extension) {
+      return dojo.getObject(extension);
+    });
+
+    //All extensions must return true for the row to be returned.
+    return dojo.filter(rows, function(row, i, array) {
+      var result = true;
+      for (var i = 0; i < extensions.length; i++) {
+        if (!extensions[i](row, i, array)) {
+          result = false;
+          break;
+        }
+      }
+      return result;
+    });
+  }
+
+couch = {
+    allDbs: function(options) {
+      _call("GET", "/_all_dbs", options);
+    },
+
+    config: function(options) {
+      _call("GET", "/_config/", options);
+    },
+
+    db: function(name) {
+      return {
+        name: name,
+        uri: djConfig.couchUrl + "/" + encodeURIComponent(name) + "/",
+
+        compact: function(options) {
+          _call("POST", this.uri + "_compact", options, function(response, ioArgs) {
+            var options = ioArgs.args;
+            if (ioArgs.xhr.status == 202) {
+              if (options.success) {
+                options.success(resp);
+              }
+            } else {
+              return Error("Invalid status: " + ioArgs.xhr.status);
+            }
+          });
+        },
+        create: function(options) {
+          _call("PUT", this.uri + "_compact", options, function(response, ioArgs) {
+            var options = ioArgs.args;
+            if (ioArgs.xhr.status == 201) {
+              if (options.success) {
+                options.success(resp);
+              }
+            } else {
+              return Error("Invalid status: " + ioArgs.xhr.status);
+            }
+          });
+        },
+        drop: function(options) {
+          _call("DELETE", this.uri, options);
+        },
+        info: function(options) {
+          _call("GET", this.uri, options);
+        },
+        allDocs: function(options) {
+          if (options && options.keys) {
+            options = dojo.delegate(options || {});
+            options.postData = toJSON({keys: options.keys});
+            _call("POST", this.uri + "_all_docs" + encodeOptions(options), options);
+          } else {
+            _call("GET", this.uri + "_all_docs" + encodeOptions(options), options);
+          }
+        },
+        openDoc: function(docId, options) {
+          _call("GET", this.uri + encodeURIComponent(docId) + encodeOptions(options), options);
+        },
+        saveDoc: function(doc, options) {
+          if (doc._id === undefined) {
+            var method = "POST";
+            var uri = this.uri;
+          } else {
+            var method = "PUT";
+            var uri = this.uri  + encodeURIComponent(doc._id);
+          }
+          _call(method, uri, options, function(response, ioArgs) {
+              doc._id = resp.id;
+              doc._rev = resp.rev;
+              if (req.status == 201) {
+                if (options.success) {
+                  options.success(resp);
+                }                
+              } else {
+                return Error("Invalid status: " + ioArgs.xhr.status);              
+              }          
+          });
+        },
+        removeDoc: function(doc, options) {
+          _call("DELETE", this.uri + encodeURIComponent(doc._id) + encodeOptions({rev: doc._rev}), options);
+        },
+        query: function(mapFun, reduceFun, language, options) {
+          language = language || "javascript"
+          if (typeof(mapFun) != "string") {
+            mapFun = mapFun.toSource ? mapFun.toSource() : "(" + mapFun.toString() + ")";
+          }
+          var body = {language: language, map: mapFun};
+          if (reduceFun != null) {
+            if (typeof(reduceFun) != "string")
+              reduceFun = reduceFun.toSource ? reduceFun.toSource() : "(" + reduceFun.toString() + ")";
+            body.reduce = reduceFun;
+          }
+          
+          options = dojo.delegate(options || {});
+          options.postData = toJSON(body);
+
+          _call("POST", this.uri + "_slow_view" + encodeOptions(options), options);
+        },
+        view: function(name, options, extensions) {
+          if (options.keys) {
+            options = dojo.delegate(options || {});
+            options.postData = {keys: options.keys};            
+            _call("POST", this.uri + "_design/" + name + encodeOptions(options), options);
+          } else {
+            var beforeSuccess;
+            if(extensions) {
+              beforeSuccess = function(json){
+                json.rows = applyExtensions(extensions, json.rows);
+                if (options.success) {
+                  options.success(json);
+                }
+              };
+            }
+            _call("GET", this.uri + "_design/" + name + encodeOptions(options), options, beforeSuccess);
+          }
+        }
+      };
+    },
+
+    info: function(options) {
+      _call("GET", "/", options);
+    },
+
+    replicate: function(source, target, options) {
+      options = dojo.delegate(options || {});
+      options.postData = toJSON({source: source, target: target});
+      _call("POST", "/_replicate", options);
+    }
+
+  }
+})();
+
+}}});
new file mode 100644
--- /dev/null
+++ b/client/inflow/index.html
@@ -0,0 +1,144 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>raindrop</title>
+  <script src="http://127.0.0.1:5984/raindrop/files/config.js" charset="utf-8"></script>
+
+  <script type="text/javascript" charset="utf-8">
+    dojo.addOnLoad(function() {
+
+    });
+  </script>
+  <style type="text/css" media="screen, print">
+    #stories { list-style: none; padding: 0px; margin: 0px; }
+    .story  { margin: 1.6ex 0px; padding: 0px; max-width: 80ex; }
+
+    .topic { position: relative; }
+    div.photo { position: absolute; left: 0px; width: 50px; height: 50px; }
+    .close { position: absolute; right: 0px; text-decoration: none; font-size: x-small; font-weight: bold; visibility: hidden; padding: 1ex; }
+    .close:before { content: "["; }
+    .close:after { content: "]"; }
+    .story:hover .close { visibility: visible; color: GrayText; }
+    .close:hover { color: WindowText; }
+
+    .tools { position: absolute; right: 0px; font-size: x-small; visibility: hidden; }
+    .story:hover .tools { visibility: visible; }
+    .message { max-width: 70ex; margin-left: 50px; padding-left: 1em; }
+
+    div.photo { vertical-align: top; background-color: #efefef; border: 1px dashed gray;}
+    img.photo { width: 48px; height: 48px; }
+    .message { }
+
+    .from { text-decoration: none; font-weight: bold; }
+    .medium { font-size: small; }
+    .subject { font-weight: bold; }
+    .content , .timestamp { margin: 0.5ex 0px; }
+    .content { overflow: hidden; font-size: small; height: 6.3ex; font-size-adjust: 0.5; color: #333; }
+    .timestamp { color: GrayText; font-size: x-small; }
+
+
+    .reply { background-color: #f8f8f8; margin-left: 22px; position: relative; padding: 2px; margin-bottom: 1ex; }
+    .reply div.photo { width: 26px; height: 26px; left: 2px; }
+    .reply div.photo img.photo { width: 24px; height: 24px; }
+    .reply .message { margin-left: 26px; }
+    .reply .message .meta { font-size: small; }
+    .reply .message .meta .medium { font-size: x-small; }
+    .reply .message .content .subject { display: none; }
+
+    .reply.response input { visibility: hidden; }
+    .reply.response:hover input { visibility: visible; }
+
+  </style>
+</head>
+<body>
+
+  <ol id="stories">
+    <li class="story">
+      <div class="topic">
+        <a href="" class="close">X</a>
+        <div class="photo"><img class="photo" src="" /></div>
+        <div class="message">
+          <div class="header">
+            <span class="from"><a class="from" href="">Bryan Clark</a></span>
+            <span class="medium">&lt;clarkbw@gmail.com&gt;</span>
+          </div>
+          <div class="content">
+            <span class="subject">Subject of the message</span>
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Mauris elit mi, fermentum ac, ornare eu, pulvinar vel, mi. Duis rhoncus, leo eu ultrices tincidunt, orci neque pellentesque ligula, sit amet sollicitudin erat turpis id purus. Nunc semper neque. Integer ligula libero, fermentum ut, feugiat id, viverra at, sapien. Nullam pulvinar urna vitae arcu. Ut vitae urna ut dui imperdiet mattis. Sed ut justo eget justo iaculis consectetuer. Vestibulum ac nunc at velit rutrum consequat. Pellentesque imperdiet. Nullam eros leo, molestie vitae, pellentesque id, pharetra vel, magna. Phasellus non nisl vitae turpis tristique interdum. In hac habitasse platea dictumst. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris dictum, orci vel condimentum molestie, neque erat tincidunt ante, quis aliquet magna nibh et ante. Quisque ornare, diam non malesuada tincidunt, ligula lectus pellentesque elit, in faucibus nisi leo eu nulla. Etiam luctus
+          </div>
+          <div class="tools">
+            <a href="">reply</a> <a href="">forward</a>
+          </div>
+          <div class="timestamp">
+            about an hour ago
+          </div>
+        </div>
+      </div>
+    </li>
+
+    <li class="story">
+      <div class="topic">
+        <a href="" class="close">X</a>
+        <div class="photo"><img class="photo" src="" /></div>
+        <div class="message">
+          <div class="meta">
+            <span class="from"><a class="from" href="">David Ascher</a></span>
+            <span class="medium">&lt;davida@mozilla.org&gt;</span>
+          </div>
+          <div class="content">
+            <span class="subject">Subject of the message</span>
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Mauris elit mi, fermentum ac, ornare eu, pulvinar vel, mi. Duis rhoncus, leo eu ultrices tincidunt, orci neque pellentesque ligula, sit amet sollicitudin erat turpis id purus. Nunc semper neque. Integer ligula libero, fermentum ut, feugiat id, viverra at, sapien. Nullam pulvinar urna vitae arcu. Ut vitae urna ut dui imperdiet mattis. Sed ut justo eget justo iaculis consectetuer. Vestibulum ac nunc at velit rutrum consequat. Pellentesque imperdiet. Nullam eros leo, molestie vitae, pellentesque id, pharetra vel, magna. Phasellus non nisl vitae turpis tristique interdum. In hac habitasse platea dictumst. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris dictum, orci vel condimentum molestie, neque erat tincidunt ante, quis aliquet magna nibh et ante. Quisque ornare, diam non malesuada tincidunt, ligula lectus pellentesque elit, in faucibus nisi leo eu nulla. Etiam luctus
+          </div>
+
+          <div class="timestamp">
+            about 2 hours ago
+          </div>
+        </div>
+      </div>
+      <div class="reply">
+        <div class="photo"><img class="photo" src="" /></div>
+        <div class="message">
+          <div class="meta">
+            <span class="from"><a class="from" href="">David Ascher</a></span>
+            <span class="medium">&lt;davida@mozilla.org&gt;</span>
+          </div>
+          <div class="content">
+            <span class="subject">Subject of the message</span>
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Mauris elit mi, fermentum ac, ornare eu, pulvinar vel, mi. Duis rhoncus, leo eu ultrices tincidunt, orci neque pellentesque ligula, sit amet sollicitudin erat turpis id purus. Nunc semper neque. Integer ligula libero, fermentum ut, feugiat id, viverra at, sapien. Nullam pulvinar urna vitae arcu. Ut vitae urna ut dui imperdiet mattis. Sed ut justo eget justo iaculis consectetuer. Vestibulum ac nunc at velit rutrum consequat. Pellentesque imperdiet. Nullam eros leo, molestie vitae, pellentesque id, pharetra vel, magna. Phasellus non nisl vitae turpis tristique interdum. In hac habitasse platea dictumst. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris dictum, orci vel condimentum molestie, neque erat tincidunt ante, quis aliquet magna nibh et ante. Quisque ornare, diam non malesuada tincidunt, ligula lectus pellentesque elit, in faucibus nisi leo eu nulla. Etiam luctus
+          </div>
+
+          <div class="timestamp">
+            about 2 hours ago
+          </div>
+        </div>
+      </div>
+      <div class="reply">
+        <div class="photo"><img class="photo" src="" /></div>
+        <div class="message">
+          <div class="meta">
+            <span class="from"><a class="from" href="">David Ascher</a></span>
+            <span class="medium">&lt;davida@mozilla.org&gt;</span>
+          </div>
+          <div class="content">
+            <span class="subject">Subject of the message</span>
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Mauris elit mi, fermentum ac, ornare eu, pulvinar vel, mi. Duis rhoncus, leo eu ultrices tincidunt, orci neque pellentesque ligula, sit amet sollicitudin erat turpis id purus. Nunc semper neque. Integer ligula libero, fermentum ut, feugiat id, viverra at, sapien. Nullam pulvinar urna vitae arcu. Ut vitae urna ut dui imperdiet mattis. Sed ut justo eget justo iaculis consectetuer. Vestibulum ac nunc at velit rutrum consequat. Pellentesque imperdiet. Nullam eros leo, molestie vitae, pellentesque id, pharetra vel, magna. Phasellus non nisl vitae turpis tristique interdum. In hac habitasse platea dictumst. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris dictum, orci vel condimentum molestie, neque erat tincidunt ante, quis aliquet magna nibh et ante. Quisque ornare, diam non malesuada tincidunt, ligula lectus pellentesque elit, in faucibus nisi leo eu nulla. Etiam luctus
+          </div>
+          <div class="timestamp">
+            about 2 hours ago
+          </div>
+        </div>
+      </div>
+      <div class="reply response">
+        <div class="photo you"><img class="photo you" src="" /></div>
+        <div class="message">
+          <form>
+            <input style="float:right;" type="submit" value="Reply All"/>
+            <textarea style="width:80%;height:22px;"></textarea>
+          </form>
+        </div>
+      </div>
+    </li>
+  </ol>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/client/raindrop.xd.js
@@ -0,0 +1,97 @@
+//xdomain loading wrapper. Can be inserted by a build process, but manually doing it for now.
+window[(typeof (djConfig)!="undefined"&&djConfig.scopeMap&&djConfig.scopeMap[0][1])||"dojo"]._xdResourceLoaded(function(dojo, dijit, dojox){
+  return {
+  depends:[["provide","raindrop"], ["require","couch"]],defineResource:function(dojo, dijit, dojox){
+
+//Main module definition
+dojo.provide("raindrop");
+
+dojo.require("couch");
+
+/*
+This file provides some basic environment services running in raindrop.
+*/
+
+raindrop = {
+  onDocClick: function(evt) {
+    //summary: Handles doc clicks to see if we need to use a register protocol.
+    var node = evt.target;
+    var href = node.href;
+    
+    if (!href && node.nodeName.toLowerCase() == "img") {
+      //Small cheat to handle images that are hyperlinked.
+      //May need to revisit this for the long term.
+      href = node.parentNode.href;
+    }
+
+    if (href) {
+      href = href.split("#")[1];
+      if (href && href.indexOf("rd:") == 0) {
+        //Have a valid rd: protocol link.
+        href = href.split(":");
+        var protocol = href[1];
+
+        //Strip off rd: and protocol: for the final
+        //value to pass to protocol handler.
+        href.splice(0, 2);
+        var value = href.join(":");
+        
+        dojo.stopEvent(evt);
+
+        this.routeProtocol(protocol, value);
+      }
+    }
+  },
+  
+  routeProtocol: function(/*String*/protocol, /*String*/value) {
+    //summary: Handles loading of the protocols and routing the call to the right handler.
+    //Fetch the protocols
+    if (!this.protocols) {
+      this.protocols = {};
+      couch.db("extensions").view("all/protocol", {
+        include_docs: true,
+        group : false,
+        success: dojo.hitch(this, function(json) {
+          dojo.forEach(json.rows, function(row) {
+            var val = row.value;
+            this.protocols[val.protocol] = {
+              handler: val.handler
+            };
+          }, this);
+          this.callProtocol(protocol, value);
+        })
+      });
+    } else {
+      this.callProtocol(protocol, value);
+    }
+  },
+
+  callProtocol: function(/*String*/protocol, /*String*/value) {
+    //summary: Choose the right protocol extension to process the value.
+    protocol = this.protocols[protocol];
+    if (protocol) {
+      //Get the module to load. Assumption is anything but the last .part
+      //is the module name. The last .part is the method to call on the module.
+      var module = protocol.handler.split(".");
+      module.pop();
+
+      //Dynamically load protocol handler on demand. If already loaded,
+      //the code in the dojo.addOnLoad will execute immediately.
+      dojo["require"](module.join("."));
+      dojo.addOnLoad(function() {
+        var handler = dojo.getObject(protocol.handler);
+        if (handler) {
+          handler(value);
+        }
+      });
+    }
+  }
+}
+
+dojo.addOnLoad(function(){
+  //Register an onclick handler on the body to handle "#rd:" protocol URLs.
+  dojo.connect(document.documentElement, "onclick", raindrop, "onDocClick");
+});
+
+
+}}});
new file mode 100644
--- /dev/null
+++ b/client/rd_list/index.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+  <script src="http://127.0.0.1:5984/raindrop/files/config.js" charset="utf-8"></script>
+  <script type="text/javascript" charset="utf-8">
+    dojo.addOnLoad(function(){
+      //http://127.0.0.1:5984/raindrop/_design/raindrop!apps!all/_view/all?limit=11
+      couch.db("raindrop").view("raindrop!apps!all/_view/all", {
+        include_docs: true,
+        group : false,
+        success: dojo.hitch(this, function(json) {
+          var html = "";
+          dojo.forEach(json.rows, function(row) {
+            var doc = row.doc;
+            if (doc._id != "app!app/rd_list") {
+              html += '<li><a href="' + doc.location + '">' + doc.name + '</a></li>';
+            }
+          });
+
+          dojo.place(html, dojo.query("ul")[0], "only");
+        })
+      });
+    });
+  </script>
+</head>
+
+<body>
+  <h1>Raindrop app listing</h1>
+  <ul></ul>
+</body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/schema/apps/all/all-map.js
@@ -0,0 +1,5 @@
+function(doc) {
+  if (doc._id.indexOf("app!app/") == 0) {
+    emit(doc._id, null);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/schema/uiext/all/all-map.js
@@ -0,0 +1,5 @@
+function(doc) {
+  if (doc._id.indexOf("app!") == 0 && doc.provide && doc.location) {
+    emit(doc.provide, doc.location);
+  }
+}
--- a/server/python/raindrop/bootstrap.py
+++ b/server/python/raindrop/bootstrap.py
@@ -59,60 +59,138 @@ def install_client_files(whateva, option
 
     def _open_not_exists(failure, *args, **kw):
         failure.trap(twisted.web.error.Error)
         if failure.value.status != '404': # not found.
             failure.raiseException()
         return {} # return an empty doc.
 
     def _maybe_update_doc(design_doc):
+        def _insert_file(path, couch_path, attachments, fp):
+            f = open(path, 'rb')
+            ct = mimetypes.guess_type(path)[0]
+            if ct is None and sys.platform=="win32":
+                # A very simplistic check in the windows registry.
+                import _winreg
+                try:
+                    k = _winreg.OpenKey(_winreg.HKEY_CLASSES_ROOT,
+                                        os.path.splitext(path)[1])
+                    ct = _winreg.QueryValueEx(k, "Content Type")[0]
+                except EnvironmentError:
+                    pass
+            assert ct, "can't guess the content type for '%s'" % filename
+            data = f.read()
+            fp.get_finger(path).update(data)
+            attachments[couch_path] = {
+                'content_type': ct,
+                'data': base64.b64encode(data)
+            }
+            f.close()
+
+        def _check_dir(client_dir, couch_path, attachments, fp):
+            for filename in os.listdir(client_dir):
+                path = os.path.join(client_dir, filename)
+                # Insert files if they do not start with a dot or
+                # end in a ~, those are probably temp editor files. 
+                if os.path.isfile(path) and \
+                   not filename.startswith(".") and \
+                   not filename.endswith("~"):
+                    _insert_file(path, couch_path + filename, attachments, fp)
+                elif os.path.isdir(path):
+                    new_couch_path = filename + "/"
+                    if couch_path:
+                        new_couch_path = couch_path + new_couch_path
+                    _check_dir(path, new_couch_path, attachments, fp)
+                logger.debug("filename '%s'", filename)
+
         fp = Fingerprinter()
         attachments = design_doc['_attachments'] = {}
         # we cannot go in a zipped egg...
         root_dir = path_part_nuke(model.__file__, 4)
         client_dir = os.path.join(root_dir, 'client')
         logger.debug("listing contents of '%s' to look for client files", client_dir)
 
-        for filename in os.listdir(client_dir):
-            path = os.path.join(client_dir, filename)
-            if os.path.isfile(path):
-                f = open(path, 'rb')
-                ct = mimetypes.guess_type(filename)[0]
-                if ct is None and sys.platform=="win32":
-                    # A very simplistic check in the windows registry.
-                    import _winreg
-                    try:
-                        k = _winreg.OpenKey(_winreg.HKEY_CLASSES_ROOT,
-                                            os.path.splitext(filename)[1])
-                        ct = _winreg.QueryValueEx(k, "Content Type")[0]
-                    except EnvironmentError:
-                        pass
-                assert ct, "can't guess the content type for '%s'" % filename
-                data = f.read()
-                fp.get_finger(filename).update(data)
-                attachments[filename] = {
-                    'content_type': ct,
-                    'data': base64.b64encode(data)
-                }
-                f.close()
-            logger.debug("filename '%s' (%s)", filename, ct)
+        # recursively go through directories, adding files.
+        _check_dir(client_dir, "", attachments, fp)
+
         new_prints = fp.get_prints()
         if options.force or design_doc.get('fingerprints') != new_prints:
             logger.info("client files are different - updating doc")
             design_doc['fingerprints'] = new_prints
             return d.saveDoc(design_doc, FILES_DOC)
         logger.debug("client files are identical - not updating doc")
         return None
 
     defrd = d.openDoc(FILES_DOC)
     defrd.addCallbacks(_opened_ok, _open_not_exists)
     defrd.addCallback(_maybe_update_doc)
     return defrd
 
 
+def update_apps(whateva):
+    """Updates the app config file using the latest app docs in the couch.
+       Should be run after a UI app/extension is added or removed from the couch.
+    """
+
+    print "update_apps"
+
+    db = get_db()
+
+    def _open_not_exists(failure, *args, **kw):
+        # If no files document, then error out immediately.
+        failure.trap(twisted.web.error.Error)
+        failure.raiseException()
+
+    def _load_config(doc):
+        # load app config from couch
+        dfd = db.openView("raindrop!uiext!all", "all")
+        dfd.addErrback(_open_not_exists)
+        dfd.addCallback(_insert_config, doc)
+        return dfd
+
+    def _insert_config(view_results, doc):
+        # we cannot go in a zipped egg...
+        root_dir = path_part_nuke(model.__file__, 4)
+        config_path = os.path.join(root_dir, 'client') + "/config.js"
+
+        # load config.js skeleton
+        f = open(config_path, 'rb')
+        data = f.read()
+        f.close()
+
+
+        #Inject the config into the config.js
+        config = ""
+        config += ",".join(
+           ["'%s': '%s'" % (
+              item["key"].replace("'", "\\'"), 
+              item["value"].replace("'", "\\'")
+           ) for item in view_results["rows"]]
+        )
+
+        for item in view_results["rows"]:
+            prefix = item["key"]
+            # get path put pull off any fragment ID or query string
+            path = item["value"].split("#")[0].split("?")[0]
+
+        data = data.replace('/*INSERT CONFIG HERE*/', config)
+
+        # save config.js in the files.
+        doc["_attachments"]["config.js"] = {
+            'content_type': "application/x-javascript; charset=UTF-8",
+            'data': base64.b64encode(data)
+        }
+
+        return db.saveDoc(doc, FILES_DOC)
+
+    defrd = db.openDoc(FILES_DOC)
+    defrd.addErrback(_open_not_exists)
+    defrd.addCallback(_load_config)
+    return defrd
+
 def install_accounts(whateva):
     db = get_db()
     config = get_config()
 
     def _opened_ok(doc):
         logger.info("account '%(_id)s' already exists, will be updating existing account",
                     doc)
         return doc
--- a/server/python/run-raindrop.py
+++ b/server/python/run-raindrop.py
@@ -265,16 +265,17 @@ def main():
         if db_created:
             return bootstrap.install_accounts(None)
     d.addCallback(model.fab_db
         ).addCallback(maybe_install_accounts
         )
     # Check if the files on the filesystem need updating.
     d.addCallback(bootstrap.install_client_files, options)
     d.addCallback(bootstrap.install_views, options)
+    d.addCallback(bootstrap.update_apps)
 
     # Now process the args specified.
     for i, arg in enumerate(args):
         try:
             func = all_args[arg]
         except KeyError:
             parser.error("Invalid command: " + arg)