├── .gitmodules ├── LICENSE ├── README.md ├── build ├── app │ ├── atom │ │ ├── ace.rule │ │ ├── app.js │ │ ├── bootstrap.js │ │ ├── forge │ │ ├── icons │ │ │ ├── icon-128.png │ │ │ └── icon-196.png │ │ ├── index.html │ │ ├── package.json │ │ ├── regenerator-runtime.js │ │ ├── src.rule │ │ ├── style.css │ │ └── tedit-embedded.css │ ├── chrome │ │ ├── ace.rule │ │ ├── background.js │ │ ├── bootstrap.js │ │ ├── guess-generators.js │ │ ├── icons │ │ │ ├── icon-128.png │ │ │ └── icon-196.png │ │ ├── index.html │ │ ├── manifest.json │ │ ├── regenerator-runtime.js │ │ ├── src.rule │ │ ├── style.css │ │ └── tedit-embedded.css │ ├── fxos │ │ ├── ace.rule │ │ ├── bootstrap.js │ │ ├── forge │ │ ├── icons │ │ │ ├── icon-moz-128.png │ │ │ └── icon-moz-60.png │ │ ├── index.html │ │ ├── manifest.webapp │ │ ├── src.rule │ │ ├── style.css │ │ └── tedit-embedded.css │ └── winrt │ │ └── package.appxmanifest ├── minimal │ ├── css │ │ ├── codemirror.css │ │ ├── main.css │ │ ├── notebook-dark.css │ │ ├── notebook.css │ │ └── wallpaper.jpg │ ├── index.html │ └── main.js ├── ui │ ├── icons.css │ ├── index.html │ ├── loader.js │ ├── modules │ └── src └── web │ ├── ace │ ├── icons │ ├── icon-128.png │ └── icon-196.png │ ├── index.html │ ├── manifest.appcache │ ├── regenerator-runtime.js │ ├── styles.css │ ├── tedit-old.js │ └── tedit.js ├── dat.json ├── lib ├── bodec.js ├── culvert.js ├── git-sha1.js └── pako.js ├── shared ├── bootstrap.js ├── icon-128.png ├── icon-196.png ├── icon-moz-128.png ├── icon-moz-60.png ├── style.css └── tedit-embedded.css ├── src-minimal ├── apps │ └── config.js ├── data │ └── config.js ├── extensions │ └── text-editor.js ├── main.js └── ui │ ├── app-window.js │ ├── code-mirror-editor.js │ ├── desktop.js │ └── drag-helper.js ├── src-ui ├── locker.js ├── main.js ├── style.css └── tree.js └── src ├── backends-atom.js ├── backends-chrome.js ├── backends-fxos.js ├── backends.js ├── backends ├── chrome-fs.js ├── encrypt-repo.js ├── github-clone.js ├── github.js ├── indexed-db.js ├── local-storage.js ├── repo-common.js └── websql.js ├── data ├── document.js ├── fs.js ├── hooks.js ├── importfs.js ├── load-module.js ├── publisher.js └── rescape.js ├── extra-modes └── mode-jackl.js ├── main-chrome.js ├── main-fxos.js ├── main-web.js ├── prefs-chrome.js ├── prefs.js ├── runtimes-atom.js ├── runtimes-chrome.js ├── runtimes-fxos.js ├── runtimes.js ├── runtimes ├── edit-hook.js ├── file-export.js ├── http-server-atom.js ├── http-server.js ├── local-eval.js ├── local-exec.js └── local-export.js └── ui ├── applytheme.js ├── context-menu.js ├── dialog.js ├── editor.js ├── elements.js ├── global-keys.js ├── notify.js ├── row.js ├── slider.js ├── tree.js └── zoom.js /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/git-sha1"] 2 | path = lib/git-sha1 3 | url = git@github.com:creationix/git-sha1.git 4 | [submodule "lib/js-git"] 5 | path = lib/js-git 6 | url = git@github.com:creationix/js-git.git 7 | [submodule "lib/mine"] 8 | path = lib/mine 9 | url = git@github.com:creationix/mine.git 10 | [submodule "lib/dombuilder"] 11 | path = lib/dombuilder 12 | url = git@github.com:creationix/dombuilder.git 13 | [submodule "lib/pathjoin"] 14 | path = lib/pathjoin 15 | url = git@github.com:creationix/pathjoin.git 16 | [submodule "lib/bodec"] 17 | path = lib/bodec 18 | url = git@github.com:creationix/bodec.git 19 | [submodule "lib/carallel"] 20 | path = lib/carallel 21 | url = git@github.com:creationix/carallel.git 22 | [submodule "lib/simple-mime"] 23 | path = lib/simple-mime 24 | url = git@github.com:creationix/simple-mime.git 25 | [submodule "lib/ace-builds"] 26 | path = lib/ace-builds 27 | url = git@github.com:ajaxorg/ace-builds.git 28 | [submodule "filters"] 29 | path = filters 30 | url = git@github.com:creationix/my-filters.git 31 | [submodule "lib/http-codec"] 32 | url = git@github.com:creationix/http-codec.git 33 | ref = refs/heads/master 34 | path = lib/http-codec 35 | [submodule "lib/jon-parse"] 36 | url = git@github.com:creationix/jon-parse.git 37 | ref = refs/heads/master 38 | path = lib/jon-parse 39 | [submodule "lib/git-tree"] 40 | url = git@github.com:creationix/git-tree.git 41 | ref = refs/heads/master 42 | path = lib/git-tree 43 | [submodule "lib/pako"] 44 | url = git@github.com:nodeca/pako.git 45 | ref = refs/heads/master 46 | path = lib/pako 47 | [submodule "lib/js-github"] 48 | url = git@github.com:creationix/js-github.git 49 | ref = refs/heads/master 50 | path = lib/js-github 51 | [submodule "lib/git-chrome-fs"] 52 | path = lib/git-chrome-fs 53 | url = git@github.com:creationix/git-chrome-fs.git 54 | [submodule "lib/forge-bin"] 55 | url = git@github.com:creationix/forge-bin.git 56 | ref = refs/heads/master 57 | path = lib/forge-bin 58 | [submodule "lib/css"] 59 | url = git@github.com:reworkcss/css.git 60 | ref = refs/heads/master 61 | path = lib/css 62 | [submodule "lib/tedit-regenerator"] 63 | url = git@github.com:creationix/tedit-regenerator.git 64 | ref = refs/heads/master 65 | prefix = hxgkk2pm-xunob4 66 | path = lib/tedit-regenerator 67 | [submodule "lib/gen-run"] 68 | url = git@github.com:creationix/gen-run.git 69 | ref = refs/heads/master 70 | prefix = hxgl6kdw-1g44jf 71 | path = lib/gen-run 72 | [submodule "lib/codemirror"] 73 | path = lib/codemirror 74 | url = git://github.com/marijnh/codemirror.git 75 | [submodule "lib/cm-notebook-theme"] 76 | path = lib/cm-notebook-theme 77 | url = git://github.com/creationix/cm-notebook-theme.git 78 | [submodule "lib/domchanger"] 79 | url = git@github.com:creationix/domchanger.git 80 | ref = refs/heads/master 81 | prefix = hyan5444-pdgcm6 82 | path = lib/domchanger 83 | [submodule "lib/cm-jackl-mode"] 84 | url = git@github.com:creationix/cm-jackl-mode.git 85 | ref = refs/heads/master 86 | prefix = hyan8noi-kafv26 87 | path = lib/cm-jackl-mode 88 | [submodule "lib/cm-jon-mode"] 89 | url = git@github.com:creationix/cm-jon-mode.git 90 | ref = refs/heads/master 91 | prefix = hyanahkj-1cg9rnm 92 | path = lib/cm-jon-mode 93 | [submodule "lib/culvert"] 94 | url = git@github.com:creationix/culvert.git 95 | ref = refs/heads/master 96 | prefix = hyhoryto-jekx0x 97 | path = lib/culvert 98 | [submodule "lib/wheaty-cjs-bundler"] 99 | path = lib/wheaty-cjs-bundler 100 | url = git@github.com:creationix/wheaty-cjs-bundler.git 101 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Tim Caswell 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 11 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 12 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 13 | THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | tedit-app 2 | ========= 3 | 4 | Tedit is a git based development environment. When I say git based I mean you 5 | don't edit files on disk. You edit git databases directly. Visually it looks 6 | much like a traditional editor complete with file tree and editor pane. Under 7 | the hood, you are browsing the git database graph and creating new nodes and 8 | updating the root reference whenever you make a change. 9 | 10 | The purpose of Tedit is to create a development platform that makes programming 11 | JavaScript easy and more accessable. It runs great on ChromeBooks and soon 12 | there will be a hosted web version that runs on mobile browsers on tablets. 13 | 14 | Install at the Chrome [Web Store](https://chrome.google.com/webstore/detail/tedit-development-environ/ooekdijbnbbjdfjocaiflnjgoohnblgf) 15 | 16 | 17 | ![Tedit Screenshot](http://creationix.com/tedit-0.1.12-1.png) 18 | 19 | ## Hacking on Tedit 20 | 21 | So you decided you want to help me build this awesome tool. That's great. 22 | 23 | First, Tedit is a self-hosting compiler / editor / platform. This means you 24 | need Tedit to build Tedit. Go get the chrome store version if you haven't 25 | already. 26 | 27 | Visual walkthrough: 28 | 29 | - If you don't have a github token handy, create a new one at 30 | - Launch the [pre-built version of Tedit](https://chrome.google.com/webstore/detail/tedit-development-environ/ooekdijbnbbjdfjocaiflnjgoohnblgf) and using the context menu (right-click) in the 31 | empty pane to the left, select "Clone Github Repo" 32 | - Enter `creationix/tedit` (or your fork if you want write access) in the first field and paste your token in the last. 33 | - Right-Click on the `build/chrome/app` folder in the new tree and select "Live Export to Disk". 34 | - Select a parent folder (I usually do Desktop) and a name for the target (I like `tedit`). 35 | - Watch the save icon spin while it exports the files to disk. 36 | - When done, open Chrome to , enable developer mode, and add the exported folder as an unpacked extension. 37 | - Launch the generated version of tedit. I recommend changing the color of this second version using `Control+B` to tell them apart. 38 | -------------------------------------------------------------------------------- /build/app/atom/ace.rule: -------------------------------------------------------------------------------- 1 | { 2 | program: "smart-copy" 3 | 4 | input: "lib/ace-builds/src-min-noconflict" 5 | rules: [ 6 | -- We are only interested in .js files. 7 | ["\\.js$", true] 8 | -- The snippets are turned off, no sense installing them. 9 | ["(^|/)snippets($|/)", false] 10 | -- Remove some ace modules that don't justify their size to usefulness ratio. 11 | ["-xquery\\.js$", false] 12 | ["-jsoniq\\.js$", false] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /build/app/atom/app.js: -------------------------------------------------------------------------------- 1 | var app = require('app'); 2 | var BrowserWindow = require('browser-window'); 3 | 4 | var mainWindow = null; 5 | 6 | app.on('window-all-closed', function() { 7 | app.quit(); 8 | }); 9 | 10 | app.on('ready', function() { 11 | mainWindow = new BrowserWindow({width: 800, height: 600}); 12 | 13 | mainWindow.loadUrl('file://' + __dirname + '/index.html'); 14 | 15 | mainWindow.on('closed', function() { 16 | mainWindow = null; 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /build/app/atom/bootstrap.js: -------------------------------------------------------------------------------- 1 | ../../../shared/bootstrap.js -------------------------------------------------------------------------------- /build/app/atom/forge: -------------------------------------------------------------------------------- 1 | ../../../lib/forge-bin -------------------------------------------------------------------------------- /build/app/atom/icons/icon-128.png: -------------------------------------------------------------------------------- 1 | ../../../../shared/icon-128.png -------------------------------------------------------------------------------- /build/app/atom/icons/icon-196.png: -------------------------------------------------------------------------------- 1 | ../../../../shared/icon-196.png -------------------------------------------------------------------------------- /build/app/atom/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tedit App 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /build/app/atom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tedit", 3 | "productName": "Tedit Development Environment", 4 | "version": "0.2.17", 5 | "description": "An offline enabled development environment that works directly on js-git databases", 6 | "main": "app.js", 7 | "icons": { 8 | "196": "icons/icon-196.png", 9 | "128": "icons/icon-128.png" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /build/app/atom/regenerator-runtime.js: -------------------------------------------------------------------------------- 1 | ../../../lib/tedit-regenerator/runtime.js -------------------------------------------------------------------------------- /build/app/atom/src.rule: -------------------------------------------------------------------------------- 1 | { 2 | program: "js-compiler" 3 | 4 | mappings: { 5 | -- Map to the main tedit source files 6 | main.js: "src/main-web.js" 7 | prefs.js: "src/prefs-web.js" 8 | backends: "src/backends" 9 | backends.js: "src/backends-atom.js" 10 | runtimes: "src/runtimes" 11 | runtimes.js: "src/runtimes-atom.js" 12 | data: "src/data" 13 | ui: "src/ui" 14 | 15 | -- Map to the external dependencies 16 | http-codec.js: "lib/http-codec/http-codec.js" 17 | bodec.js: "lib/bodec/bodec.js" 18 | carallel.js: "lib/carallel/carallel.js" 19 | css-parse.js: "lib/css/lib/parse/index.js" 20 | dombuilder.js: "lib/dombuilder/dombuilder.js" 21 | gen-run.js: "lib/gen-run/run.js" 22 | git-sha1.js: "lib/git-sha1/git-sha1.js" 23 | git-tree.js: "lib/git-tree/git-tree.js" 24 | jon-parse.js: "lib/jon-parse/jon-parse.js" 25 | js-git: "lib/js-git" 26 | js-github: "lib/js-github" 27 | mine.js: "lib/mine/mine.js" 28 | pako: "lib/pako/lib" 29 | pathjoin.js: "lib/pathjoin/pathjoin.js" 30 | simple-mime.js: "lib/simple-mime/simple-mime.js" 31 | regenerator.js: "lib/tedit-regenerator/regenerator-bundle.js" 32 | } 33 | } -------------------------------------------------------------------------------- /build/app/atom/style.css: -------------------------------------------------------------------------------- 1 | ../../../shared/style.css -------------------------------------------------------------------------------- /build/app/atom/tedit-embedded.css: -------------------------------------------------------------------------------- 1 | ../../../shared/tedit-embedded.css -------------------------------------------------------------------------------- /build/app/chrome/ace.rule: -------------------------------------------------------------------------------- 1 | { 2 | program: "smart-copy" 3 | 4 | input: "lib/ace-builds/src-min-noconflict" 5 | rules: [ 6 | -- We are only interested in .js files. 7 | ["\\.js$", true] 8 | -- The snippets are turned off, no sense installing them. 9 | ["(^|/)snippets($|/)", false] 10 | -- Remove some ace modules that don't justify their size to usefulness ratio. 11 | ["-xquery\\.js$", false] 12 | ["-jsoniq\\.js$", false] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /build/app/chrome/background.js: -------------------------------------------------------------------------------- 1 | /*global chrome*/ 2 | chrome.app.runtime.onLaunched.addListener(function() { 3 | chrome.app.window.create('/index.html', { 4 | id: "tedit", 5 | frame: "none", 6 | width: 950, 7 | height: 550 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /build/app/chrome/bootstrap.js: -------------------------------------------------------------------------------- 1 | ../../../shared/bootstrap.js -------------------------------------------------------------------------------- /build/app/chrome/guess-generators.js: -------------------------------------------------------------------------------- 1 | window.hasGenerators = (function*(){yield true;})().next().value; 2 | -------------------------------------------------------------------------------- /build/app/chrome/icons/icon-128.png: -------------------------------------------------------------------------------- 1 | ../../../../shared/icon-128.png -------------------------------------------------------------------------------- /build/app/chrome/icons/icon-196.png: -------------------------------------------------------------------------------- 1 | ../../../../shared/icon-196.png -------------------------------------------------------------------------------- /build/app/chrome/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tedit App 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /build/app/chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tedit Development Environment", 3 | "short_name": "Tedit", 4 | "version": "0.2.21", 5 | "manifest_version": 2, 6 | "minimum_chrome_version": "31", 7 | "description": "An offline enabled development environment that works directly on js-git databases", 8 | "offline_enabled": true, 9 | "app": { 10 | "background": { 11 | "scripts": ["background.js"] 12 | } 13 | }, 14 | "permissions": [ 15 | "storage", 16 | "unlimitedStorage", 17 | {"fileSystem": ["write", "retainEntries", "directory"]} 18 | ], 19 | "sockets": { 20 | "tcpServer" : {"listen":":*"} 21 | }, 22 | "icons": { 23 | "196": "icons/icon-196.png", 24 | "128": "icons/icon-128.png" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /build/app/chrome/regenerator-runtime.js: -------------------------------------------------------------------------------- 1 | ../../../lib/tedit-regenerator/runtime.js -------------------------------------------------------------------------------- /build/app/chrome/src.rule: -------------------------------------------------------------------------------- 1 | { 2 | program: "js-compiler" 3 | 4 | mappings: { 5 | -- Map to the main tedit source files 6 | main.js: "src/main-chrome.js" 7 | prefs.js: "src/prefs-chrome.js" 8 | backends: "src/backends" 9 | backends.js: "src/backends-chrome.js" 10 | runtimes: "src/runtimes" 11 | runtimes.js: "src/runtimes-chrome.js" 12 | data: "src/data" 13 | ui: "src/ui" 14 | 15 | -- Map to the external dependencies 16 | bodec.js: "lib/bodec/bodec.js" 17 | carallel.js: "lib/carallel/carallel.js" 18 | css-parse.js: "lib/css/lib/parse/index.js" 19 | css: "lib/css" 20 | dombuilder.js: "lib/dombuilder/dombuilder.js" 21 | git-chrome-fs: "lib/git-chrome-fs" 22 | gen-run.js: "lib/gen-run/run.js" 23 | git-sha1.js: "lib/git-sha1/git-sha1.js" 24 | git-tree.js: "lib/git-tree/git-tree.js" 25 | http-codec.js: "lib/http-codec/http-codec.js" 26 | jon-parse.js: "lib/jon-parse/jon-parse.js" 27 | js-git: "lib/js-git" 28 | js-github: "lib/js-github" 29 | mine.js: "lib/mine/mine.js" 30 | pako: "lib/pako" 31 | pako.js: "lib/pako.js" 32 | pathjoin.js: "lib/pathjoin/pathjoin.js" 33 | simple-mime.js: "lib/simple-mime/simple-mime.js" 34 | regenerator.js: "lib/tedit-regenerator/regenerator-bundle.js" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /build/app/chrome/style.css: -------------------------------------------------------------------------------- 1 | ../../../shared/style.css -------------------------------------------------------------------------------- /build/app/chrome/tedit-embedded.css: -------------------------------------------------------------------------------- 1 | ../../../shared/tedit-embedded.css -------------------------------------------------------------------------------- /build/app/fxos/ace.rule: -------------------------------------------------------------------------------- 1 | { 2 | program: "smart-copy" 3 | 4 | input: "lib/ace-builds/src-min-noconflict" 5 | rules: [ 6 | -- We are only interested in .js files. 7 | ["\\.js$", true] 8 | -- The snippets are turned off, no sense installing them. 9 | ["(^|/)snippets($|/)", false] 10 | -- Remove some ace modules that don't justify their size to usefulness ratio. 11 | ["-xquery\\.js$", false] 12 | ["-jsoniq\\.js$", false] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /build/app/fxos/bootstrap.js: -------------------------------------------------------------------------------- 1 | ../../../shared/bootstrap.js -------------------------------------------------------------------------------- /build/app/fxos/forge: -------------------------------------------------------------------------------- 1 | ../../../lib/forge-bin -------------------------------------------------------------------------------- /build/app/fxos/icons/icon-moz-128.png: -------------------------------------------------------------------------------- 1 | ../../../../shared/icon-moz-128.png -------------------------------------------------------------------------------- /build/app/fxos/icons/icon-moz-60.png: -------------------------------------------------------------------------------- 1 | ../../../../shared/icon-moz-60.png -------------------------------------------------------------------------------- /build/app/fxos/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tedit App 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /build/app/fxos/manifest.webapp: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tedit Development Environment", 3 | "developer": { 4 | "name": "Tim Caswell", 5 | "url": "http://creationix.com/" 6 | }, 7 | "default_locale": "en", 8 | "version": "0.2.16", 9 | "type": "privileged", 10 | "description": "An offline enabled development environment that works directly on js-git databases", 11 | "launch_path": "/index.html", 12 | "icons": { 13 | "60": "/icons/icon-moz-60.png", 14 | "128": "/icons/icon-moz-128.png" 15 | }, 16 | "permissions": { 17 | "tcp-socket": { 18 | "description": "Required for communicate with remote git servers" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /build/app/fxos/src.rule: -------------------------------------------------------------------------------- 1 | { 2 | program: "js-compiler" 3 | 4 | mappings: { 5 | -- Map to the main tedit source files 6 | main.js: "src/main-fxos.js" 7 | prefs.js: "src/prefs-web.js" 8 | backends: "src/backends" 9 | backends.js: "src/backends-fxos.js" 10 | runtimes: "src/runtimes" 11 | runtimes.js: "src/runtimes-fxos.js" 12 | data: "src/data" 13 | ui: "src/ui" 14 | 15 | -- Map to the external dependencies 16 | http-codec.js: "lib/http-codec/http-codec.js" 17 | bodec.js: "lib/bodec/bodec.js" 18 | carallel.js: "lib/carallel/carallel.js" 19 | css-parse.js: "lib/css/lib/parse/index.js" 20 | dombuilder.js: "lib/dombuilder/dombuilder.js" 21 | git-sha1.js: "lib/git-sha1/git-sha1.js" 22 | git-tree.js: "lib/git-tree/git-tree.js" 23 | jon-parse.js: "lib/jon-parse/jon-parse.js" 24 | js-git: "lib/js-git" 25 | js-github: "lib/js-github" 26 | mine.js: "lib/mine/mine.js" 27 | pako: "lib/pako/lib" 28 | pathjoin.js: "lib/pathjoin/pathjoin.js" 29 | simple-mime.js: "lib/simple-mime/simple-mime.js" 30 | } 31 | } -------------------------------------------------------------------------------- /build/app/fxos/style.css: -------------------------------------------------------------------------------- 1 | ../../../shared/style.css -------------------------------------------------------------------------------- /build/app/fxos/tedit-embedded.css: -------------------------------------------------------------------------------- 1 | ../../../shared/tedit-embedded.css -------------------------------------------------------------------------------- /build/app/winrt/package.appxmanifest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creationix/tedit/35e76c468e7c799e6b40f41b5a106c3c35ef7afb/build/app/winrt/package.appxmanifest -------------------------------------------------------------------------------- /build/minimal/css/codemirror.css: -------------------------------------------------------------------------------- 1 | ../../../lib/codemirror/lib/codemirror.css -------------------------------------------------------------------------------- /build/minimal/css/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | touch-action: none; 4 | } 5 | body { 6 | background-color: #666; 7 | margin: 0; 8 | background: url(wallpaper.jpg); 9 | background-repeat: no-repeat; 10 | background-position: center center; 11 | background-size: cover; 12 | overflow: hidden; 13 | } 14 | 15 | /* make notebook themes transparent */ 16 | /*.cm-s-notebook.CodeMirror {background-color:rgba(254, 252, 238, 0.8)}*/ 17 | /*.cm-s-notebook .CodeMirror-activeline-background,*/ 18 | /*.cm-s-notebook .CodeMirror-selected {background: rgba(255,255,255,0.2)}*/ 19 | /*.cm-s-notebook.CodeMirror-focused .CodeMirror-selected {background: rgba(255,255,255,0.7)}*/ 20 | 21 | /*.cm-s-notebook-dark.CodeMirror {background-color:rgba(31, 29, 27, 0.8)}*/ 22 | /*.cm-s-notebook-dark .CodeMirror-activeline-background,*/ 23 | /*.cm-s-notebook-dark .CodeMirror-selected {background: rgba(0,0,0,0.2)}*/ 24 | /*.cm-s-notebook-dark.CodeMirror-focused .CodeMirror-selected {background: rgba(0,0,0,0.7)}*/ 25 | 26 | .CodeMirror { 27 | font-size: 14pt; 28 | } 29 | 30 | .window { 31 | position: absolute; 32 | overflow: hidden; 33 | } 34 | .window.maximized { 35 | position: static; 36 | } 37 | .window.focused { 38 | z-index: 10; 39 | } 40 | 41 | .window.light { 42 | box-shadow: 0 0 7px rgba(0,0,0,0.5); 43 | } 44 | .window.dark { 45 | box-shadow: 0 0 7px rgba(255,255,255,0.5); 46 | } 47 | 48 | .CodeMirror { 49 | position: absolute; 50 | top: 0; 51 | bottom: 0; 52 | left: 0; 53 | right: 0; 54 | height: auto; 55 | width: auto; 56 | } 57 | 58 | .content { 59 | position: absolute; 60 | overflow: auto; 61 | top: 42px; 62 | left: 10px; 63 | right: 10px; 64 | bottom: 10px; 65 | } 66 | .light .title-bar, .light .close-box, .light .max-box { 67 | background-color: #fff; 68 | color: #000; 69 | } 70 | .dark .title-bar, .dark .close-box, .dark .max-box { 71 | background-color: #000; 72 | color: #fff; 73 | } 74 | .title-bar { 75 | touch-action: none; 76 | -webkit-user-select: none; 77 | user-select: none; 78 | position: absolute; 79 | overflow: hidden; 80 | left: 10px; 81 | right: 74px; 82 | text-align: center; 83 | font-family: sans-serif; 84 | font-size: 12pt; 85 | text-overflow: ellipsis; 86 | cursor: move; 87 | } 88 | .close-box { 89 | right: 10px; 90 | } 91 | .max-box { 92 | right: 42px; 93 | } 94 | .max-box, .close-box { 95 | font-size: 12pt; 96 | -webkit-user-select: none; 97 | user-select: none; 98 | position: absolute; 99 | overflow: hidden; 100 | width: 32px; 101 | text-align: center; 102 | cursor: pointer; 103 | } 104 | .max-box, .close-box, .title-bar { 105 | line-height: 32px; 106 | top: 10px; 107 | height: 32px; 108 | opacity: 0.5; 109 | } 110 | 111 | .focused .max-box, .focused .close-box, .focused .title-bar { 112 | opacity: 0.9; 113 | } 114 | .close-box:hover, .max-box:hover { 115 | background-color: #888; 116 | } 117 | .resize { 118 | touch-action: none; 119 | position: absolute; 120 | } 121 | .light .resize:hover { 122 | background-color: rgba(0,0,0,0.2); 123 | } 124 | .dark .resize:hover { 125 | background-color: rgba(255,255,255,0.2); 126 | } 127 | .resize.n, .resize.ne, .resize.nw, .resize.s, .resize.se, .resize.sw { 128 | height: 10px; 129 | } 130 | .resize.n, .resize.ne, .resize.nw { 131 | top: 0; 132 | } 133 | .resize.s, .resize.se, .resize.sw { 134 | bottom: 0; 135 | } 136 | .resize.ne, .resize.e, .resize.se, .resize.sw, .resize.w, .resize.nw { 137 | width: 10px; 138 | } 139 | .resize.ne, .resize.e, .resize.se { 140 | right: 0; 141 | } 142 | .resize.sw, .resize.w, .resize.nw { 143 | left: 0; 144 | } 145 | .resize.n, .resize.s { 146 | left: 10px; 147 | right: 10px; 148 | } 149 | .resize.w, .resize.e { 150 | top: 10px; 151 | bottom: 10px; 152 | } 153 | .resize.n {cursor: n-resize} 154 | .resize.ne{cursor: ne-resize} 155 | .resize.e {cursor: e-resize} 156 | .resize.se{cursor: se-resize} 157 | .resize.s {cursor: s-resize} 158 | .resize.sw{cursor: sw-resize} 159 | .resize.w{cursor: w-resize} 160 | .resize.nw{cursor: nw-resize} 161 | 162 | ::-webkit-scrollbar { 163 | background-color: rgba(100,100,100,0.05); 164 | width: 1em; 165 | height: 1em; 166 | } 167 | .ace_editor ::-webkit-scrollbar, .ace_editor ::-webkit-scrollbar-thumb { 168 | font-size: 16px; 169 | } 170 | ::-webkit-scrollbar-thumb { 171 | border: solid 3px transparent; 172 | box-shadow: inset 0 0 0 .1em rgba(128, 128, 128, 0.2), inset 0 0 .7em .2em rgba(128, 128, 128, 0.2); 173 | } 174 | ::-webkit-scrollbar-thumb:hover { 175 | box-shadow: inset 0 0 0 .1em rgba(128, 128, 128, 0.8), inset 0 0 .7em .2em rgba(128, 128, 128, 0.8); 176 | } 177 | -------------------------------------------------------------------------------- /build/minimal/css/notebook-dark.css: -------------------------------------------------------------------------------- 1 | ../../../lib/cm-notebook-theme/notebook-dark.css -------------------------------------------------------------------------------- /build/minimal/css/notebook.css: -------------------------------------------------------------------------------- 1 | ../../../lib/cm-notebook-theme/notebook.css -------------------------------------------------------------------------------- /build/minimal/css/wallpaper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creationix/tedit/35e76c468e7c799e6b40f41b5a106c3c35ef7afb/build/minimal/css/wallpaper.jpg -------------------------------------------------------------------------------- /build/minimal/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Creative Space 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /build/minimal/main.js: -------------------------------------------------------------------------------- 1 | #!js 2 | 3 | var pathJoin = require('path').join; 4 | var bodec = require('bodec'); 5 | var mine = require('mine'); 6 | var modes = require('js-git/lib/modes'); 7 | 8 | function wrapper(name) { 9 | var modules = {}; 10 | var defs = {/*DEFS*/}; 11 | window.require = require; 12 | require(/*MAIN*/); 13 | function require(filename) { 14 | var module = modules[filename]; 15 | if (module) return module.exports; 16 | module = modules[filename] = {exports:{}}; 17 | var dirname = filename.substring(0, filename.lastIndexOf("/")); 18 | var def = defs[filename]; 19 | if (!def) throw new Error("No such module: " + filename); 20 | def(module, module.exports, dirname, filename); 21 | return module.exports; 22 | } 23 | } 24 | 25 | module.exports = function* (pathToEntry) { 26 | 27 | var started = {}; 28 | var js = ""; 29 | var main = "src-minimal/main.js"; 30 | 31 | yield* load(main); 32 | 33 | var code = wrapper.toString().replace("/*MAIN*/", JSON.stringify(main)).split("/*DEFS*/"); 34 | 35 | js = "(" + code[0] + js + code[1] + "());\n"; 36 | 37 | return [200, {"Content-Type":"application/javascript"}, js]; 38 | 39 | function* load(path) { 40 | if (started[path]) return; 41 | started[path] = true; 42 | var meta = yield* pathToEntry(path); 43 | if (!meta) throw new Error("No such file: " + path); 44 | var blob = yield meta.repo.loadAs("blob", meta.hash); 45 | var code = bodec.toUnicode(blob); 46 | var deps = mine(code); 47 | var base = pathJoin(path, ".."); 48 | for (var i = deps.length - 1; i >= 0; --i) { 49 | var dep = deps[i]; 50 | var depName = dep.name; 51 | if (depName[0] === ".") { 52 | depName = yield* findLocal(pathJoin(base, depName)); 53 | } 54 | else { 55 | depName = yield* findModule(base, depName); 56 | } 57 | if (depName) { 58 | yield* load(depName); 59 | var offset = dep.offset; 60 | code = code.substring(0, offset) + 61 | depName + 62 | code.substring(offset + dep.name.length); 63 | } 64 | } 65 | js += JSON.stringify(path) + 66 | ": function (module, exports, __dirname, __filename) {\n" + 67 | code + "\n},\n"; 68 | } 69 | 70 | function* findLocal(path) { 71 | var meta = yield* pathToEntry(path); 72 | if (meta) { 73 | // Exact match! Happy days. 74 | if (modes.isFile(meta.mode)) return path; 75 | if (meta.mode !== modes.tree) return; 76 | // Maybe it's a module with a package.json? 77 | var pkgPath = pathJoin(path, "package.json"); 78 | meta = yield* pathToEntry(pkgPath); 79 | if (meta && modes.isFile(meta.mode)) { 80 | var json = yield meta.repo.loadAs("text", meta.hash); 81 | var pkgInfo = JSON.parse(json); 82 | if (pkgInfo.main) { 83 | return yield* findLocal(pathJoin(path, pkgInfo.main)); 84 | } 85 | } 86 | var idxPath = pathJoin(path, "index.js"); 87 | meta = yield* pathToEntry(idxPath); 88 | if (meta && modes.isFile(meta.mode)) return idxPath; 89 | } 90 | // Maybe they forgot the extension? 91 | path = path + ".js"; 92 | meta = yield* pathToEntry(path); 93 | if (meta && modes.isFile(meta.mode)) return path; 94 | } 95 | 96 | function* findModule(base, name) { 97 | return (yield* findLocal(pathJoin("src", name))) || 98 | (yield* findLocal(pathJoin("lib", name))); 99 | } 100 | 101 | }; 102 | -------------------------------------------------------------------------------- /build/ui/icons.css: -------------------------------------------------------------------------------- 1 | ../../shared/tedit-embedded.css -------------------------------------------------------------------------------- /build/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tedit 6 | 7 | 8 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /build/ui/loader.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load", function () { 2 | "use strict"; 3 | var defs = {}; 4 | var modules = {}; 5 | var blobUrls = {}; 6 | var loading = {}; 7 | 8 | var initQueue = null; 9 | function initRegenerator(callback) { 10 | if (initQueue) return initQueue.push(callback); 11 | var left = 2; 12 | var done = false; 13 | initQueue = [callback]; 14 | callback = function () { 15 | var queue = initQueue; 16 | initQueue = null; 17 | for (var i = 0; i < queue.length; i++) { 18 | queue[i].apply(this, arguments); 19 | } 20 | }; 21 | 22 | console.log("No native generator support detected, loading regenerator compiler and runtime"); 23 | loadScript("modules/tedit-regenerator/regenerator-bundle.js", function (err, fn) { 24 | if (err) { 25 | done = true; 26 | return callback(err); 27 | } 28 | var exports = {}; 29 | var module = {exports:exports}; 30 | fn(module, exports); 31 | window.regenerator = module.exports; 32 | if (--left) return; 33 | done = true; 34 | callback(); 35 | }, true); 36 | loadScript("modules/tedit-regenerator/runtime.js", function (err, fn) { 37 | if (err) { 38 | done = true; 39 | return callback(err); 40 | } 41 | fn(); 42 | if (--left) return; 43 | done = true; 44 | callback(); 45 | }, true); 46 | } 47 | 48 | requireBase(".").async("./src/main.js", function (err) { 49 | if (err) throw err; 50 | }); 51 | 52 | function resolve(base, name) { 53 | var url = name[0] === "." ? pathJoin(base, name) : pathJoin("modules", name); 54 | if (!/\.js$/i.test(url)) url += ".js"; 55 | return url; 56 | } 57 | 58 | function requireBase(base) { 59 | requireSync.async = requireAsync; 60 | return requireSync; 61 | 62 | function requireSync(name) { 63 | var url = resolve(base, name); 64 | if (url in modules) return modules[url].exports; 65 | var fn = defs[url]; 66 | if (!fn) throw new Error("no such module: " + url); 67 | var module = modules[url] = {exports:{}}; 68 | return exec(url, module, fn); 69 | } 70 | 71 | function requireAsync(name, callback) { 72 | if (!callback) return requireAsync.bind(null, name); 73 | var url = resolve(base, name); 74 | if (url in modules) return callback(null, modules[url].exports); 75 | loadScript(url, function (err, fn) { 76 | if (url in modules) return callback(null, modules[url].exports); 77 | var module = modules[url] = {exports:{}}; 78 | if (!fn) throw new Error("No such module: " + url); 79 | if (err) return callback(err); 80 | exec(url, module, fn, callback); 81 | }); 82 | } 83 | 84 | function exec(url, module, fn, callback) { 85 | var index = url.lastIndexOf("/"); 86 | var dirname = url.substring(0, index); 87 | var filename = url.substring(index + 1); 88 | if (fn.constructor === Function) { 89 | fn(requireBase(dirname), dirname, filename, module, module.exports); 90 | if (callback) callback(null, module.exports); 91 | } 92 | else { 93 | run(fn(requireBase(dirname), dirname, filename, module, module.exports), function (err, ret) { 94 | if (err) { 95 | if (callback) return callback(err); 96 | throw err; 97 | } 98 | if (ret !== undefined) module.exports = ret; 99 | if (callback) return callback(null, module.exports); 100 | }); 101 | } 102 | return module.exports; 103 | } 104 | } 105 | 106 | function loadScript(url, callback, raw) { 107 | if (defs[url]) return callback(null, defs[url]); 108 | 109 | // Guard against concurrent requests for the same resource 110 | var callbacks = loading[url]; 111 | if (callbacks) return callbacks.push(callback); 112 | callbacks = loading[url] = [callback]; 113 | callback = function () { 114 | delete loading[url]; 115 | for (var i = 0; i < callbacks.length; i++) { 116 | callbacks[i].apply(this, arguments); 117 | } 118 | }; 119 | 120 | readUrl(url, function (err, js) { 121 | var id = "code://" + url; 122 | var blobUrl, tag; 123 | if (js === undefined) return callback(err); 124 | if (raw) { 125 | js = "(function (module, exports) {" + js + "})"; 126 | return inject(); 127 | } 128 | var needgen = /\byield\b/.test(js); 129 | js = "(function" + (needgen ? "*" : "") + 130 | " (require, __dirname, __filename, module, exports) {" + js + "})"; 131 | var deps = mine(js); 132 | if (!deps.length) return inject(); 133 | return preload(url, deps, inject); 134 | 135 | function inject(err) { 136 | if (err) return callback(err); 137 | if (needgen && !window.hasgens && !window.regenerator) { 138 | return initRegenerator(inject); 139 | } 140 | js = "window[" + JSON.stringify(id) + "]" + js + ";\n"; 141 | if (needgen && window.regenerator) { 142 | var index = url.lastIndexOf("/"); 143 | js = window.regenerator(js, { 144 | sourceFileName: url.substring(index + 1), 145 | sourceRoot: url.substring(0, index) 146 | }); 147 | } 148 | 149 | window[id] = define; 150 | var blob = new Blob([js], {type : 'application/javascript'}); 151 | blobUrl = URL.createObjectURL(blob); 152 | tag = document.createElement("script"); 153 | tag.setAttribute("charset", "utf-8"); 154 | tag.setAttribute("async", "async"); 155 | tag.setAttribute("src", blobUrl); 156 | document.head.appendChild(tag); 157 | } 158 | 159 | function define(fn) { 160 | if (blobUrls[id]) { 161 | URL.revokeObjectURL(blobUrls[id]); 162 | } 163 | blobUrls[id] = blobUrl; 164 | delete window[id]; 165 | document.head.removeChild(tag); 166 | if (!raw) defs[url] = fn; 167 | callback(null, fn); 168 | } 169 | 170 | }); 171 | } 172 | 173 | function preload(url, deps, callback) { 174 | var index = url.lastIndexOf("/"); 175 | var base = url.substring(0, index); 176 | var left = deps.length; 177 | var done = false; 178 | deps.forEach(function (dep) { 179 | if (done) return; 180 | loadScript(resolve(base, dep.name), function (err) { 181 | if (done) return; 182 | if (err) { 183 | done = true; 184 | return callback(err); 185 | } 186 | if (!--left) { 187 | done = true; 188 | return callback(); 189 | } 190 | }); 191 | }); 192 | } 193 | 194 | function readUrl(url, callback) { 195 | var done = false; 196 | var xhr = new XMLHttpRequest(); 197 | xhr.open("GET", url, true); 198 | xhr.timeout = 2000; 199 | xhr.ontimeout = onTimeout; 200 | xhr.onreadystatechange = onReadyStateChange; 201 | return xhr.send(); 202 | 203 | function onReadyStateChange() { 204 | if (done) return; 205 | if (xhr.readyState !== 4) return; 206 | // Give onTimeout a chance to run first if that's the reason status is 0. 207 | if (!xhr.status) { 208 | xhr.status = -1; 209 | return setTimeout(onReadyStateChange, 0); 210 | } 211 | done = true; 212 | if (xhr.status < 200 || xhr.status >= 500) { 213 | return callback(new Error("Invalid status code for " + url + ": " + xhr.status)); 214 | } 215 | if (xhr.status > 400) { 216 | return callback(); 217 | } 218 | return callback(null, xhr.responseText); 219 | } 220 | 221 | function onTimeout() { 222 | if (done) return; 223 | done = true; 224 | return callback(new Error("Timeout requesting " + url)); 225 | } 226 | } 227 | 228 | // from path-join, but inlines for easy use. 229 | 230 | // Joins path segments. Preserves initial "/" and resolves ".." and "." 231 | // Does not support using ".." to go above/outside the root. 232 | // This means that join("foo", "../../bar") will not resolve to "../bar" 233 | function pathJoin(/* path segments */) { 234 | // Split the inputs into a list of path commands. 235 | var parts = []; 236 | for (var i = 0, l = arguments.length; i < l; i++) { 237 | if (arguments[i] === undefined || arguments[i] === null) continue; 238 | parts = parts.concat(arguments[i].split("/")); 239 | } 240 | // Interpret the path commands to get the new resolved path. 241 | var newParts = []; 242 | for (i = 0, l = parts.length; i < l; i++) { 243 | var part = parts[i]; 244 | // Remove leading and trailing slashes 245 | // Also remove "." segments 246 | if (!part || part === ".") continue; 247 | // Interpret ".." to pop the last segment 248 | if (part === "..") newParts.pop(); 249 | // Push new path segments. 250 | else newParts.push(part); 251 | } 252 | // Preserve the initial slash if there was one. 253 | if (parts[0] === "") newParts.unshift(""); 254 | // Turn back into a single string path. 255 | return newParts.join("/") || (newParts.length ? "/" : "."); 256 | } 257 | 258 | // from gen-run, but inlined for easy use. 259 | function run(generator, callback) { 260 | var iterator; 261 | if (typeof generator === "function") { 262 | // Pass in resume for no-wrap function calls 263 | iterator = generator(resume); 264 | } 265 | else if (typeof generator === "object") { 266 | // Oterwise, assume they gave us the iterator directly. 267 | iterator = generator; 268 | } 269 | else { 270 | throw new TypeError("Expected generator or iterator and got " + typeof generator); 271 | } 272 | 273 | var data = null, yielded = false; 274 | 275 | var next = callback ? nextSafe : nextPlain; 276 | 277 | next(); 278 | check(); 279 | 280 | function nextSafe(err, item) { 281 | var n; 282 | try { 283 | n = (err ? iterator.throw(err) : iterator.next(item)); 284 | if (!n.done) { 285 | if (n.value) start(n.value); 286 | yielded = true; 287 | return; 288 | } 289 | } 290 | catch (err) { 291 | return callback(err); 292 | } 293 | return callback(null, n.value); 294 | } 295 | 296 | function nextPlain(err, item) { 297 | var cont = (err ? iterator.throw(err) : iterator.next(item)).value; 298 | if (cont) start(cont); 299 | yielded = true; 300 | } 301 | 302 | function start(cont) { 303 | // Pass in resume to continuables if one was yielded. 304 | if (typeof cont === "function") return cont(resume()); 305 | // If an array of continuables is yielded, run in parallel 306 | if (Array.isArray(cont)) { 307 | for (var i = 0, l = cont.length; i < l; ++i) { 308 | if (typeof cont[i] !== "function") return; 309 | } 310 | return parallel(cont, resume()); 311 | } 312 | // Also run hash of continuables in parallel, but name results. 313 | if (typeof cont === "object" && Object.getPrototypeOf(cont) === Object.prototype) { 314 | var keys = Object.keys(cont); 315 | for (var i = 0, l = keys.length; i < l; ++i) { 316 | if (typeof cont[keys[i]] !== "function") return; 317 | } 318 | return parallelNamed(keys, cont, resume()); 319 | } 320 | } 321 | 322 | function resume() { 323 | var done = false; 324 | return function () { 325 | if (done) return; 326 | done = true; 327 | data = arguments; 328 | check(); 329 | }; 330 | } 331 | 332 | function check() { 333 | while (data && yielded) { 334 | var err = data[0]; 335 | var item = data[1]; 336 | data = null; 337 | yielded = false; 338 | next(err, item); 339 | yielded = true; 340 | } 341 | } 342 | 343 | } 344 | 345 | function parallel(array, callback) { 346 | var length = array.length; 347 | var left = length; 348 | var results = new Array(length); 349 | var done = false; 350 | return array.forEach(function (cont, i) { 351 | cont(function (err, result) { 352 | if (done) return; 353 | if (err) { 354 | done = true; 355 | return callback(err); 356 | } 357 | results[i] = result; 358 | if (--left) return; 359 | done = true; 360 | return callback(null, results); 361 | }); 362 | }); 363 | } 364 | 365 | function parallelNamed(keys, obj, callback) { 366 | var length = keys.length; 367 | var left = length; 368 | var results = {}; 369 | var done = false; 370 | return keys.forEach(function (key) { 371 | var cont = obj[key]; 372 | results[key] = null; 373 | cont(function (err, result) { 374 | if (done) return; 375 | if (err) { 376 | done = true; 377 | return callback(err); 378 | } 379 | results[key] = result; 380 | if (--left) return; 381 | done = true; 382 | return callback(null, results); 383 | }); 384 | }); 385 | } 386 | 387 | // From creationix/mine, but inlined for easy use 388 | 389 | // Mine a string for require calls and export the module names 390 | // Extract all require calls using a proper state-machine parser. 391 | function mine(js) { 392 | js = "" + js; 393 | var names = []; 394 | var state = 0; 395 | var ident; 396 | var quote; 397 | var name; 398 | var start; 399 | 400 | var isIdent = /[a-z0-9_.$]/i; 401 | var isWhitespace = /[ \r\n\t]/; 402 | 403 | function $start(char) { 404 | if (char === "/") { 405 | return $slash; 406 | } 407 | if (char === "'" || char === '"') { 408 | quote = char; 409 | return $string; 410 | } 411 | if (isIdent.test(char)) { 412 | ident = char; 413 | return $ident; 414 | } 415 | return $start; 416 | } 417 | 418 | function $ident(char) { 419 | if (isIdent.test(char)) { 420 | ident += char; 421 | return $ident; 422 | } 423 | if (char === "(" && ident === "require") { 424 | ident = undefined; 425 | return $call; 426 | } else { 427 | if (isWhitespace.test(char)){ 428 | if (ident !== 'yield' && ident !== 'return'){ 429 | return $ident; 430 | } 431 | } 432 | } 433 | return $start(char); 434 | } 435 | 436 | function $call(char) { 437 | if (isWhitespace.test(char)) return $call; 438 | if (char === "'" || char === '"') { 439 | quote = char; 440 | name = ""; 441 | start = i + 1; 442 | return $name; 443 | } 444 | return $start(char); 445 | } 446 | 447 | function $name(char) { 448 | if (char === quote) { 449 | return $close; 450 | } 451 | if (char === "\\") { 452 | return $nameEscape; 453 | } 454 | name += char; 455 | return $name; 456 | } 457 | 458 | function $nameEscape(char) { 459 | if (char === "\\") { 460 | name += char; 461 | } else { 462 | name += JSON.parse('"\\' + char + '"'); 463 | } 464 | return $name; 465 | } 466 | 467 | function $close(char) { 468 | if (isWhitespace.test(char)) return $close; 469 | if (char === ")" || char === ',') { 470 | names.push({ 471 | name: name, 472 | offset: start 473 | }); 474 | } 475 | name = undefined; 476 | return $start(char); 477 | } 478 | 479 | function $string(char) { 480 | if (char === "\\") { 481 | return $escape; 482 | } 483 | if (char === quote) { 484 | return $start; 485 | } 486 | return $string; 487 | } 488 | 489 | function $escape() { 490 | return $string; 491 | } 492 | 493 | function $slash(char) { 494 | if (char === "/") return $lineComment; 495 | if (char === "*") return $multilineComment; 496 | return $start(char); 497 | } 498 | 499 | function $lineComment(char) { 500 | if (char === "\r" || char === "\n") return $start; 501 | return $lineComment; 502 | } 503 | 504 | function $multilineComment(char) { 505 | if (char === "*") return $multilineEnding; 506 | return $multilineComment; 507 | } 508 | 509 | function $multilineEnding(char) { 510 | if (char === "/") return $start; 511 | if (char === "*") return $multilineEnding; 512 | return $multilineComment; 513 | } 514 | 515 | state = $start; 516 | for (var i = 0, l = js.length; i < l; i++) { 517 | state = state(js[i]); 518 | } 519 | return names; 520 | } 521 | 522 | }); -------------------------------------------------------------------------------- /build/ui/modules: -------------------------------------------------------------------------------- 1 | ../../lib -------------------------------------------------------------------------------- /build/ui/src: -------------------------------------------------------------------------------- 1 | ../../src-ui -------------------------------------------------------------------------------- /build/web/ace: -------------------------------------------------------------------------------- 1 | ../../lib/ace-builds/src-min-noconflict -------------------------------------------------------------------------------- /build/web/icons/icon-128.png: -------------------------------------------------------------------------------- 1 | ../../../shared/icon-128.png -------------------------------------------------------------------------------- /build/web/icons/icon-196.png: -------------------------------------------------------------------------------- 1 | ../../../shared/icon-196.png -------------------------------------------------------------------------------- /build/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tedit App 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /build/web/manifest.appcache: -------------------------------------------------------------------------------- 1 | #!js 2 | "use strict"; 3 | var pathJoin = require('path').join; 4 | var modes = require('js-git/lib/modes'); 5 | var acePath = "lib/ace-builds/src-min-noconflict"; 6 | 7 | var names = [ 8 | "index.html", 9 | "styles.css", 10 | "tedit.js", 11 | "regenerator-runtime.js", 12 | "icons/icon-128.png", 13 | "icons/icon-196.png", 14 | ]; 15 | 16 | module.exports = function* (pathToEntry, url) { 17 | var entry, i, l; 18 | var txt = "CACHE MANIFEST\n"; 19 | for (i = 0, l = names.length; i < l; ++i) { 20 | var name = names[i]; 21 | entry = yield* pathToEntry(pathJoin(__dirname, name)); 22 | if (!entry || !entry.hash) txt += "#" + name + "\n"; 23 | else txt += name + "#" + entry.hash + "\n"; 24 | } 25 | var meta = yield* pathToEntry(acePath); 26 | var tree = yield meta.repo.loadAs("tree", meta.hash); 27 | var keys = Object.keys(tree); 28 | for (i = 0, l = keys.length; i < l; ++i) { 29 | var key = keys[i]; 30 | entry = tree[key]; 31 | // Skip folders and xquery stuff (it's huge) 32 | if (entry.mode !== modes.blob || /xquery/.test(key)) { 33 | continue; 34 | } 35 | txt += "ace/" + key + "#" + entry.hash + "\n"; 36 | } 37 | 38 | txt += "NETWORK:\n*\n"; 39 | 40 | return [200, 41 | { "Content-Type": "text/cache-manifest" }, 42 | txt 43 | ]; 44 | 45 | }; 46 | -------------------------------------------------------------------------------- /build/web/regenerator-runtime.js: -------------------------------------------------------------------------------- 1 | ../../lib/tedit-regenerator/runtime.js -------------------------------------------------------------------------------- /build/web/styles.css: -------------------------------------------------------------------------------- 1 | #!js 2 | 3 | var pathJoin = require('path').join; 4 | 5 | module.exports = function* (pathToEntry) { 6 | var files = [ 7 | "../../shared/tedit-embedded.css", 8 | "../../shared/style.css", 9 | ]; 10 | var css = ""; 11 | for (var i = 0, l = files.length; i < l; ++i) { 12 | var path = pathJoin(__dirname, files[i]); 13 | var entry = yield* pathToEntry(path); 14 | css += "\n/* " + path + " */\n" + 15 | (yield entry.repo.loadAs("text", entry.hash)); 16 | } 17 | 18 | return [200, { 19 | "Content-Type": "text/css; charset=utf-8", 20 | }, css]; 21 | 22 | }; -------------------------------------------------------------------------------- /build/web/tedit-old.js: -------------------------------------------------------------------------------- 1 | #!js 2 | 3 | 4 | var pathJoin = require('path').join; 5 | var bodec = require('bodec'); 6 | var mine = require('mine'); 7 | var modes = require('js-git/lib/modes'); 8 | 9 | function wrapper(name) { 10 | var modules = {}; 11 | var defs = {/*DEFS*/}; 12 | window.require = require; 13 | require(/*MAIN*/); 14 | function require(filename) { 15 | var module = modules[filename]; 16 | if (module) return module.exports; 17 | module = modules[filename] = {exports:{}}; 18 | var dirname = filename.substring(0, filename.lastIndexOf("/")); 19 | var def = defs[filename]; 20 | if (!def) throw new Error("No such module: " + filename); 21 | def(module, module.exports, dirname, filename); 22 | return module.exports; 23 | } 24 | } 25 | 26 | module.exports = function* (pathToEntry) { 27 | 28 | var started = {}; 29 | var js = ""; 30 | var main = "src/main-web.js"; 31 | 32 | yield* load(main); 33 | 34 | js = "(" + 35 | wrapper.toString() 36 | .replace("/*DEFS*/", js) 37 | .replace("/*MAIN*/", JSON.stringify(main)) + 38 | "());\n"; 39 | 40 | return [200, {"Content-Type":"application/javascript"}, js]; 41 | 42 | function* load(path) { 43 | if (started[path]) return; 44 | started[path] = true; 45 | var meta = yield* pathToEntry(path); 46 | if (!meta) throw new Error("No such file: " + path); 47 | var blob = yield meta.repo.loadAs("blob", meta.hash); 48 | var code = bodec.toUnicode(blob); 49 | var deps = mine(code); 50 | var base = pathJoin(path, ".."); 51 | for (var i = deps.length - 1; i >= 0; --i) { 52 | var dep = deps[i]; 53 | var depName = dep.name; 54 | if (depName[0] === ".") { 55 | depName = yield* findLocal(pathJoin(base, depName)); 56 | } 57 | else { 58 | depName = yield* findModule(base, depName); 59 | } 60 | if (depName) { 61 | yield* load(depName); 62 | var offset = dep.offset; 63 | code = code.substring(0, offset) + 64 | depName + 65 | code.substring(offset + dep.name.length); 66 | } 67 | } 68 | js += JSON.stringify(path) + 69 | ": function (module, exports, __dirname, __filename) {" + 70 | code + "},\n"; 71 | } 72 | 73 | function* findLocal(path) { 74 | var meta = yield* pathToEntry(path); 75 | if (meta) { 76 | // Exact match! Happy days. 77 | if (modes.isFile(meta.mode)) return path; 78 | if (meta.mode !== modes.tree) return; 79 | // Maybe it's a module with a package.json? 80 | var pkgPath = pathJoin(path, "package.json"); 81 | meta = yield* pathToEntry(pkgPath); 82 | if (meta && modes.isFile(meta.mode)) { 83 | var json = yield meta.repo.loadAs("text", meta.hash); 84 | var pkgInfo = JSON.parse(json); 85 | if (pkgInfo.main) { 86 | return yield* findLocal(pathJoin(path, pkgInfo.main)); 87 | } 88 | } 89 | var idxPath = pathJoin(path, "index.js"); 90 | meta = yield* pathToEntry(idxPath); 91 | if (meta && modes.isFile(meta.mode)) return idxPath; 92 | } 93 | // Maybe they forgot the extension? 94 | path = path + ".js"; 95 | meta = yield* pathToEntry(path); 96 | if (meta && modes.isFile(meta.mode)) return path; 97 | } 98 | 99 | function* findModule(base, name) { 100 | return (yield* findLocal(pathJoin("src", name))) || 101 | (yield* findLocal(pathJoin("lib", name))); 102 | } 103 | 104 | }; 105 | -------------------------------------------------------------------------------- /build/web/tedit.js: -------------------------------------------------------------------------------- 1 | #!js 2 | 3 | module.exports = require('../../lib/wheaty-cjs-bundler/bundler.js')("src/main-web.js", ["src","lib"]); 4 | -------------------------------------------------------------------------------- /dat.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Tedit", 3 | "description": "An experimental IDE in the browser with Git implemented in JS", 4 | "app": { "name": "tedit" } 5 | } 6 | -------------------------------------------------------------------------------- /lib/bodec.js: -------------------------------------------------------------------------------- 1 | bodec/bodec.js -------------------------------------------------------------------------------- /lib/culvert.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./culvert/channel.js'); -------------------------------------------------------------------------------- /lib/git-sha1.js: -------------------------------------------------------------------------------- 1 | git-sha1/git-sha1.js -------------------------------------------------------------------------------- /lib/pako.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./pako/index.js'); -------------------------------------------------------------------------------- /shared/bootstrap.js: -------------------------------------------------------------------------------- 1 | // Tiny AMD loader that auto-loads src/main.js 2 | (function () { 3 | "use strict"; 4 | var modules = {}; 5 | var defs = {}; 6 | var ready = {}; 7 | var pending = {}; 8 | var scripts = {}; 9 | var oldRequire = typeof require === "function" && require; 10 | window.oldRequire = oldRequire; 11 | window.define = define; 12 | window.require = requireSync; 13 | window.requireAsync = requireAsync; 14 | window.defs = defs; 15 | document.body.textContent = ""; 16 | 17 | modules["forge.js"] = window.forge; 18 | ready["forge.js"] = true; 19 | 20 | requireAsync("main.js"); 21 | 22 | function requireAsync(name, callback) { 23 | if (!(/\.[^\/]+$/.test(name))) name += ".js"; 24 | load(name, function () { 25 | var module = requireSync(name); 26 | if (callback) callback(module); 27 | }, {}); 28 | } 29 | 30 | function requireSync(name) { 31 | if (!(/\.[^\/]+$/.test(name))) name += ".js"; 32 | if (name in modules) return modules[name].exports; 33 | var exports = {}; 34 | if (!(name in defs)) { 35 | if (oldRequire) return oldRequire(name); 36 | throw new Error("Unknown module " + name); 37 | } 38 | var module = modules[name] = {exports:exports}; 39 | if (defs[name].fn(module, exports) !== undefined) throw new Error("Use `module.exports = value`, not `return value`"); 40 | return module.exports; 41 | } 42 | 43 | // Make sure a module and all it's deps are defined. 44 | function load(name, callback, chain) { 45 | if (oldRequire && name === "remote.js") { 46 | return callback(); 47 | } 48 | // If it's flagged ready, it's ready 49 | if (ready[name]) return callback(); 50 | 51 | // If there is something going on wait for it to finish. 52 | if (name in pending) return pending[name].push(callback); 53 | // If the module isn't downloaded yet, start it. 54 | if (!(name in defs)) return download(name, callback); 55 | if (chain[name]) return callback(); 56 | chain[name] = true; 57 | var def = defs[name]; 58 | var missing = def.deps.filter(function (depName) { 59 | return !ready[depName]; 60 | }); 61 | var left = missing.length; 62 | if (!left) { 63 | ready[name] = true; 64 | return callback(); 65 | } 66 | return missing.forEach(function (depName) { 67 | load(depName, onDepLoad, chain); 68 | }); 69 | 70 | function onDepLoad() { 71 | if (!--left) return load(name, callback, chain); 72 | } 73 | } 74 | 75 | function download(name, callback) { 76 | var script = document.createElement("script"); 77 | script.setAttribute("charset", "utf-8"); 78 | script.setAttribute("src", "src/" + name); 79 | script.setAttribute("async", true); 80 | script.addEventListener("error", function () { 81 | define(name, [], function () { 82 | throw new Error("Unable to load " + name); 83 | }); 84 | }); 85 | scripts[name] = script; 86 | pending[name] = [callback]; 87 | document.head.appendChild(script); 88 | } 89 | 90 | function define(name, deps, fn) { 91 | var script = scripts[name]; 92 | if (!script) throw new Error("Name mismatch for " + name); 93 | delete scripts[name]; 94 | document.head.removeChild(script); 95 | defs[name] = { 96 | deps: deps, 97 | fn: fn 98 | }; 99 | flush(name); 100 | } 101 | 102 | function flush(name) { 103 | var list = pending[name]; 104 | delete pending[name]; 105 | for (var i = 0, l = list.length; i < l; i++) { 106 | load(name, list[i], {}); 107 | } 108 | } 109 | 110 | })(); 111 | -------------------------------------------------------------------------------- /shared/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creationix/tedit/35e76c468e7c799e6b40f41b5a106c3c35ef7afb/shared/icon-128.png -------------------------------------------------------------------------------- /shared/icon-196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creationix/tedit/35e76c468e7c799e6b40f41b5a106c3c35ef7afb/shared/icon-196.png -------------------------------------------------------------------------------- /shared/icon-moz-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creationix/tedit/35e76c468e7c799e6b40f41b5a106c3c35ef7afb/shared/icon-moz-128.png -------------------------------------------------------------------------------- /shared/icon-moz-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creationix/tedit/35e76c468e7c799e6b40f41b5a106c3c35ef7afb/shared/icon-moz-60.png -------------------------------------------------------------------------------- /shared/style.css: -------------------------------------------------------------------------------- 1 | 2 | *, 3 | *:before, 4 | *:after { 5 | box-sizing: border-box; 6 | } 7 | body { 8 | margin: 0; 9 | font-family: sans-serif; 10 | display: -webkit-flex; 11 | display: flex; 12 | -webkit-align-items: center; 13 | align-items: center; 14 | -webkit-justify-content: center; 15 | justify-content: center; 16 | } 17 | 18 | ul.tree { 19 | user-select: none; 20 | padding: 0.1em 0.1em 2em 0.1em; 21 | margin: 0; 22 | font-family: Ubuntu, sans-serif; 23 | } 24 | .tree, .tree ul { 25 | margin: 0; 26 | padding: 0; 27 | line-height: 1.4em; 28 | list-style-type: none; 29 | } 30 | .tree ul { 31 | padding-left: 0.7em; 32 | } 33 | .tree li { 34 | white-space: nowrap; 35 | } 36 | .tree .row { 37 | margin-left: -10em; 38 | padding-left: 10em; 39 | margin-right: -10em; 40 | padding-right: 10em; 41 | cursor: pointer; 42 | } 43 | .tree .tight { 44 | margin: 0 -2px 0 -4px; 45 | } 46 | .theme-light .tree.blur .row:hover { 47 | box-shadow: inset 0 0 0.3em rgba(0,0,0,0.5); 48 | } 49 | .theme-dark .tree.blur .row:hover { 50 | box-shadow: inset 0 0 0.3em rgba(255,255,255,0.5); 51 | } 52 | @-webkit-keyframes pulse-dark { 53 | 0% { 54 | box-shadow: inset 0 0 0.2em rgba(255,255,255,0.5); 55 | } 56 | 100% { 57 | box-shadow: inset 0 0 0.4em rgba(255,255,255,0.5); 58 | } 59 | } 60 | @-webkit-keyframes pulse-light { 61 | 0% { 62 | box-shadow: inset 0 0 0.2em rgba(0,0,0,0.5); 63 | } 64 | 100% { 65 | box-shadow: inset 0 0 0.4em rgba(0,0,0,0.5); 66 | } 67 | } 68 | 69 | .theme-light .tree .row.selected { 70 | box-shadow: inset 0 0 0.3em rgba(0,0,0,0.5); 71 | -webkit-animation: pulse-light 0.5s infinite alternate; 72 | background-color: rgba(0,0,0,0.05); 73 | } 74 | .theme-dark .tree .row.selected { 75 | box-shadow: inset 0 0 0.3em rgba(255,255,255,0.5); 76 | -webkit-animation: pulse-dark 0.5s infinite alternate; 77 | background-color: rgba(255,255,255,0.05); 78 | } 79 | 80 | 81 | 82 | .theme-light .tree .row.active { 83 | 84 | background-color: rgba(0,0,0,0.1); 85 | } 86 | .theme-dark .tree .row.active { 87 | background-color: rgba(255,255,255,0.1); 88 | } 89 | 90 | ::-webkit-scrollbar { 91 | background-color: rgba(100,100,100,0.05); 92 | width: 1em; 93 | height: 1em; 94 | } 95 | .ace_editor ::-webkit-scrollbar, .ace_editor ::-webkit-scrollbar-thumb { 96 | font-size: 16px; 97 | } 98 | ::-webkit-scrollbar-thumb { 99 | border: solid 3px transparent; 100 | box-shadow: inset 0 0 0 .1em rgba(128, 128, 128, 0.2), inset 0 0 .7em .2em rgba(128, 128, 128, 0.2); 101 | } 102 | ::-webkit-scrollbar-thumb:hover { 103 | box-shadow: inset 0 0 0 .1em rgba(128, 128, 128, 0.8), inset 0 0 .7em .2em rgba(128, 128, 128, 0.8); 104 | } 105 | 106 | .wrap { 107 | position: absolute; 108 | top: 2px; 109 | bottom: 2px; 110 | left: 2px; 111 | right: 2px; 112 | } 113 | body, .tree, .titlebar, .main, .main .editor, .preview, .dragger, .image { 114 | position: absolute; 115 | top: 0; 116 | left: 0; 117 | right: 0; 118 | bottom: 0; 119 | height: 100%; 120 | width: 100%; 121 | overflow: hidden; 122 | } 123 | .tree { 124 | right: auto; 125 | width: 200px; 126 | overflow-y: auto; 127 | overflow-x: hidden; 128 | } 129 | .main { 130 | left: 200px; 131 | top: 2em; 132 | height: auto; 133 | width: auto; 134 | } 135 | .titlebar { 136 | -webkit-app-region: drag; 137 | bottom: auto; 138 | left: 200px; 139 | width: auto; 140 | font-size: 1em; 141 | line-height: 2em; 142 | height: 2em; 143 | text-align: left; 144 | padding-left: 1em; 145 | } 146 | .titlebar .fade { 147 | opacity: 0.6; 148 | font-size: 0.8em; 149 | margin-right: 0.2em; 150 | } 151 | .closebox { 152 | -webkit-app-region: no-drag; 153 | position: absolute; 154 | right: 0.25em; 155 | top: 0; 156 | height: 1.5em; 157 | width: 2.75em; 158 | line-height: 1.5em; 159 | text-align: center; 160 | color: #fff; 161 | background-color: #933; 162 | cursor: pointer; 163 | } 164 | .closebox:hover { 165 | background-color: #e11; 166 | } 167 | 168 | .popup { 169 | z-index: 100; 170 | position: absolute; 171 | bottom: 0; 172 | left: 0; 173 | right: 0; 174 | height: 2em; 175 | line-height: 2em; 176 | text-align: center; 177 | font-weight: bold; 178 | } 179 | .theme-light .popup { 180 | color: #000; 181 | text-shadow: 0 0 0.25em #fff; 182 | background-color: rgba(255,255,255,0.5); 183 | } 184 | .theme-dark .popup { 185 | color: #fff; 186 | text-shadow: 0 0 0.25em #000; 187 | background-color: rgba(0,0,0,0.5); 188 | } 189 | 190 | .ace_gutter, .dragger { 191 | cursor: ew-resize !important; 192 | } 193 | .preview { 194 | background-color: #cf6969; 195 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QsEFCs5qJx92gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAK0lEQVQ4y2M0MjL6z4AHGBsb45NmYGKgEIwaMBgMYCEUz2fPnh0NxOFvAADQeAVQbGocpgAAAABJRU5ErkJggg=='); 196 | } 197 | 198 | .preview .dragger { 199 | width: 2em; 200 | right: auto; 201 | } 202 | .preview .image { 203 | left: 2em; 204 | width: auto; 205 | background-position: center; 206 | } 207 | .preview .zoom { 208 | background-repeat: no-repeat; 209 | background-size: contain; 210 | } 211 | 212 | .dialog { 213 | box-shadow: 0 0 .6em rgba(0,0,0,0.7); 214 | z-index: 12; 215 | display: -webkit-flex; 216 | display: flex; 217 | -webkit-flex-direction: column; 218 | flex-direction: column; 219 | font-size: 1em; 220 | border: 1px solid rgba(128,128,128,0.5); 221 | 222 | } 223 | 224 | .theme-light .dialog { 225 | background-color: #bbb; 226 | color: #000; 227 | } 228 | 229 | .theme-dark .dialog { 230 | background-color: #000; 231 | color: #bbb; 232 | } 233 | 234 | .dialog .title { 235 | line-height: 2em; 236 | height: 2em; 237 | display: -webkit-flex; 238 | display: flex; 239 | } 240 | .dialog .title .content { 241 | text-align: center; 242 | -webkit-flex: 1; 243 | flex: 1; 244 | padding: 0 .6em; 245 | } 246 | .dialog .title .closebox { 247 | position: static; 248 | } 249 | .dialog .body { 250 | padding: 0.5em 0.5em 0 0.5em; 251 | } 252 | 253 | .input { 254 | display: -webkit-flex; 255 | display: flex; 256 | margin-bottom: 0.5em; 257 | } 258 | 259 | .input input { 260 | font-size: 1em; 261 | line-height: 1em; 262 | } 263 | 264 | .input-field { 265 | -webkit-flex: 1; 266 | flex: 1; 267 | } 268 | 269 | .input-field:not(:first-child) { 270 | border-left: 0; 271 | } 272 | 273 | .input-field:not(:last-child) { 274 | border-right: 0; 275 | } 276 | 277 | .input-item { 278 | font: inherit; 279 | font-weight: 400; 280 | } 281 | 282 | .input-field,.input-item { 283 | border: 1px solid rgba(120,120,120,0.8); 284 | padding: .5em .75em; 285 | margin-left: 0; 286 | margin-right: 0; 287 | } 288 | 289 | .shield { 290 | position: absolute; 291 | z-index: 11; 292 | top: 0; 293 | left: 0; 294 | right: 0; 295 | bottom: 0; 296 | } 297 | ul.contextMenu { 298 | position: absolute; 299 | z-index: 12; 300 | margin: 0; 301 | padding: 0.2em 0; 302 | list-style-type: none; 303 | font-size: 0.9em; 304 | box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.2); 305 | } 306 | .theme-dark ul.contextMenu { 307 | background-color: #000; 308 | color: #ddd; 309 | border: 1px solid rgba(255,255,255,0.5); 310 | } 311 | .theme-light ul.contextMenu { 312 | background-color: #fff; 313 | color: #333; 314 | border: 1px solid rgba(0,0,0,0.5); 315 | } 316 | ul.contextMenu li { 317 | padding: 0.4em 1em 0.4em 0.4em; 318 | cursor: pointer; 319 | } 320 | ul.contextMenu li i { 321 | margin-right: 0.4em; 322 | } 323 | ul.contextMenu li:hover { 324 | background-color: #78CF8A; 325 | color: #000; 326 | } 327 | ul.contextMenu li.disabled, 328 | ul.contextMenu li.sep { 329 | height: inherit; 330 | line-height: inherit; 331 | background-color: inherit; 332 | color: inherit; 333 | cursor: inherit; 334 | opacity: 0.3; 335 | } 336 | ul.contextMenu li.sep { 337 | padding: 0; 338 | } 339 | ul.contextMenu li.sep hr { 340 | margin: 0.2em 0.4em; 341 | height: 0; 342 | border: 0; 343 | height: 1px; 344 | } 345 | .theme-dark ul.contextMenu li.sep hr{ 346 | background-color: #fff; 347 | } 348 | .theme-light ul.contextMenu li.sep hr{ 349 | background-color: #000; 350 | } 351 | 352 | .theme-dark .wrap { 353 | color: #fff; 354 | background-color: #111; 355 | } 356 | .theme-light .wrap { 357 | color: #111; 358 | background-color: #f8f8f8; 359 | } 360 | 361 | .tree .row.dirty { 362 | font-style: italic; 363 | } 364 | .tree .row.staged { 365 | font-weight: bold; 366 | } 367 | -------------------------------------------------------------------------------- /src-minimal/apps/config.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creationix/tedit/35e76c468e7c799e6b40f41b5a106c3c35ef7afb/src-minimal/apps/config.js -------------------------------------------------------------------------------- /src-minimal/data/config.js: -------------------------------------------------------------------------------- 1 | var jonParse = require('jon-parse'); 2 | 3 | var configText = localStorage.getItem("config-file"); 4 | 5 | if (!configText) { 6 | configText = "{\n" + 7 | " -- Set global theme:\n" + 8 | " -- true for light theme, false for dark\n" + 9 | " lightTheme: true\n" + 10 | "}\n"; 11 | } 12 | 13 | var config = jonParse(configText); 14 | 15 | var listeners = []; 16 | 17 | module.exports = { 18 | get: getSource, 19 | set: setSource, 20 | on: addListener, 21 | off: removeListener, 22 | }; 23 | 24 | function getSource() { 25 | return configText; 26 | } 27 | 28 | function setSource(text) { 29 | if (text === configText) return; 30 | var data = jonParse(text); 31 | if (data.constructor !== Object.prototype) { 32 | throw new Error("Config file must export an object"); 33 | } 34 | config = data; 35 | localStorage.setItem("config-file", text); 36 | configText = text; 37 | listeners.forEach(notify); 38 | } 39 | 40 | function notify(fn) { 41 | fn(config); 42 | } 43 | 44 | function addListener(fn) { 45 | listeners.push(fn); 46 | fn(config); 47 | } 48 | 49 | function removeListener(fn) { 50 | listeners.splice(listeners.indexOf(fn)); 51 | } -------------------------------------------------------------------------------- /src-minimal/extensions/text-editor.js: -------------------------------------------------------------------------------- 1 | require('t-core') -------------------------------------------------------------------------------- /src-minimal/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | var domChanger = require('domchanger'); 5 | var Desktop = require('./ui/desktop'); 6 | 7 | var desktop = domChanger(Desktop, document.body, {handleEvent: handleEvent}); 8 | 9 | 10 | function handleEvent() { 11 | console.log(arguments); 12 | } 13 | 14 | require('./data/config').on(function (config) { 15 | desktop.update(config); 16 | }); 17 | 18 | 19 | // var xhr; 20 | // var run = require('gen-run'); 21 | // var modes = require('js-git/lib/modes'); 22 | 23 | // var mounts = []; 24 | 25 | // function addMount(path, repo) { 26 | // var i = find(path); 27 | // if (mounts[i] && mounts[i].path === path) { 28 | // throw new Error("Mount already at " + path); 29 | // } 30 | // var data = { 31 | // path: path, 32 | // repo: repo 33 | // }; 34 | // i++; 35 | // if (i === mounts.length) mounts.push(data); 36 | // else mounts.splice(i, 0, data); 37 | // } 38 | 39 | // // Find the index of path 40 | // // If not found, return the index of the mount that would contain path. 41 | // function find(path) { 42 | // for (var i = 0; i < mounts.length; i++) { 43 | 44 | // } 45 | // var max = mounts.length - 1; 46 | // var min = 0; 47 | // // return; 48 | // while (min <= max) { 49 | // var i = ((max + min) / 2) | 0; 50 | // var mount = mounts[i]; 51 | // if (mount.path === path) return i; 52 | // if (mount.path < path) max = i - 1; 53 | // else min = i + 1; 54 | // } 55 | // return min; 56 | // } 57 | 58 | // addMount("") 59 | // addMount("foo/bar") 60 | // addMount("foo") 61 | // addMount("foo/bar/baz") 62 | // addMount("what") 63 | // addMount("and") 64 | 65 | // console.log("mounts", mounts); 66 | 67 | // function removeMount(path) { 68 | // for (var i = 0; i < mounts.length; i++) { 69 | // if (path > mounts[i].path) break; 70 | // } 71 | // } 72 | 73 | 74 | 75 | // run(function* () { 76 | // var token, login; 77 | // do { 78 | // token = localStorage.getItem("token"); 79 | // if (!token) { 80 | // token = window.prompt("Enter github token"); 81 | // if (!token) throw new Error("Aborted"); 82 | // localStorage.setItem("token", token); 83 | // } 84 | // xhr = require('js-github/lib/xhr')("", token); 85 | // login = localStorage.getItem("login"); 86 | // if (!login) { 87 | // var result = yield xhr("GET", "/user"); 88 | // login = result.body && result.body.login; 89 | // if (login) { 90 | // localStorage.setItem("login", login); 91 | // } 92 | // else { 93 | // localStorage.setItem("token", (token = "")); 94 | // } 95 | // } 96 | // } while (!token || !login); 97 | 98 | // var repo = {root:""}; 99 | // require('js-github/mixins/github-db')(repo, login + "/desktop", token); 100 | // require('js-git/mixins/path-to-entry')(repo); 101 | // require('js-git/mixins/create-tree')(repo); 102 | // require('js-git/mixins/formats')(repo); 103 | // mounts[""] = repo; 104 | 105 | // var tree = yield* readTree(""); 106 | // console.log(tree); 107 | // }); 108 | 109 | // function findRepo(path) { 110 | // var length = 0, root = ""; 111 | // var repo = mounts[path]; 112 | // if (repo) return repo; 113 | // var names = Object.keys(mounts); 114 | // for (var i = 0; i < names.length; i++) { 115 | // var name = names[i]; 116 | // if (name.length > length && path.length > name.length && 117 | // path.substring(0, name.length) === name && 118 | // path[name.length] === "/") { 119 | // root = name; 120 | // length = name.length; 121 | // } 122 | // } 123 | // return mounts[root]; 124 | // } 125 | 126 | // function* readTree(path) { 127 | // var repo = findRepo(path); 128 | // console.log(repo); 129 | // var commitHash = yield repo.readRef("refs/heads/master"); 130 | // var commit = yield repo.loadAs("commit", commitHash); 131 | // var tree = yield repo.loadAs("tree", commit.tree); 132 | // console.log(tree); 133 | 134 | // } 135 | 136 | // VFS interface 137 | // Get an entry from the filesystem 138 | // {hash,mode,repo}, or just {repo} if the path doesn't exist. 139 | // read(path) => entry 140 | // Write a new entry to the filesystem. Non-blocking 141 | // Here entry is just {hash,mode}, empty for delete. 142 | // write(path, entry) 143 | // Wait for all writes to complete before unblocking. 144 | // flush() => 145 | // Get the js-git repo instance for a given path 146 | 147 | 148 | // Mounts interface 149 | // mount(path, repo) 150 | // 151 | 152 | // Extensions interface 153 | // -------------------------------------------------------------------------------- /src-minimal/ui/app-window.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var drag = require('./drag-helper'); 4 | 5 | module.exports = AppWindow; 6 | 7 | function AppWindow(emit, refresh) { 8 | var width, height, left, top; 9 | var maximized = false; 10 | var id; 11 | 12 | var northProps = drag(north); 13 | var northEastProps = drag(northEast); 14 | var eastProps = drag(east); 15 | var southEastProps = drag(southEast); 16 | var southProps = drag(south); 17 | var southWestProps = drag(southWest); 18 | var westProps = drag(west); 19 | var northWestProps = drag(northWest); 20 | var titleBarProps = drag(titleBar); 21 | 22 | return { render: render }; 23 | 24 | function render(windowWidth, windowHeight, isDark, props, child) { 25 | id = props.id; 26 | 27 | if (width === undefined) { 28 | width = (windowWidth / 2) | 0; 29 | height = (windowHeight / 2) | 0; 30 | left = ((windowWidth - width) / 2) | 0; 31 | top = ((windowHeight - height) / 2) | 0; 32 | } 33 | 34 | // Manually run constraints that edges must be inside desktop and 35 | // window must be at least 200x100 36 | var right = left + width; 37 | if (right < 10) right = 10; 38 | if (left > windowWidth - 10) left = windowWidth - 10; 39 | var mid = ((left + right) / 2) | 0; 40 | if (mid < ((windowWidth / 2) | 0)) { 41 | if (right < left + 200) right = left + 200; 42 | width = right - left; 43 | if (width > windowWidth) { 44 | left += width - windowWidth; 45 | width = windowWidth; 46 | } 47 | } 48 | else { 49 | if (left > right - 200) left = right - 200; 50 | width = right - left; 51 | if (width > windowWidth) width = windowWidth; 52 | } 53 | 54 | var bottom = top + height; 55 | if (bottom < 10) bottom = 10; 56 | if (top > windowHeight - 10) top = windowHeight - 10; 57 | mid = ((top + bottom) / 2) | 0; 58 | if (mid < ((windowHeight / 2) | 0)) { 59 | if (bottom < top + 100) bottom = top + 100; 60 | height = bottom - top; 61 | if (height > windowHeight) { 62 | top += height - windowHeight; 63 | height = windowHeight; 64 | } 65 | } 66 | else { 67 | if (top > bottom - 100) top = bottom - 100; 68 | height = bottom - top; 69 | if (height > windowHeight) height = windowHeight; 70 | } 71 | 72 | var style = maximized ? { 73 | top: "-10px", 74 | left: "-10px", 75 | right: "-10px", 76 | bottom: "-10px" 77 | } : { 78 | width: width + "px", 79 | height: height + "px", 80 | transform: "translate3d(" + left + "px," + top + "px,0)", 81 | webkitTransform: "translate3d(" + left + "px," + top + "px,0)", 82 | }; 83 | var classes = [isDark ? "dark" : "light"]; 84 | if (props.focused) classes.push("focused"); 85 | return [".window", { 86 | onmousedown: onAnyClick, ontouchstart: onAnyClick, 87 | style: style, class: classes.join(" ") 88 | }, 89 | ["article.content", child], 90 | [".resize.n", northProps], 91 | [".resize.ne", northEastProps], 92 | [".resize.e", eastProps], 93 | [".resize.se", southEastProps], 94 | [".resize.s", southProps], 95 | [".resize.sw", southWestProps], 96 | [".resize.w", westProps], 97 | [".resize.nw", northWestProps], 98 | [".title-bar", titleBarProps, props.title], 99 | [".max-box", {onclick:onMaxClick}, maximized ? "▼" : "▲"], 100 | [".close-box", {onclick:onCloseClick},"✖"], 101 | ]; 102 | } 103 | 104 | function onAnyClick(evt) { 105 | emit("focus", id); 106 | } 107 | 108 | function onMaxClick(evt) { 109 | evt.stopPropagation(); 110 | maximized = !maximized; 111 | refresh(); 112 | emit("focus", id); 113 | } 114 | 115 | function onCloseClick(evt) { 116 | evt.stopPropagation(); 117 | emit("destroy", id); 118 | } 119 | 120 | function north(dx, dy) { 121 | height -= dy; 122 | top += dy; 123 | refresh(); 124 | } 125 | function northEast(dx, dy) { 126 | height -= dy; 127 | top += dy; 128 | width += dx; 129 | refresh(); 130 | } 131 | function east(dx, dy) { 132 | width += dx; 133 | refresh(); 134 | } 135 | function southEast(dx, dy) { 136 | height += dy; 137 | width += dx; 138 | refresh(); 139 | } 140 | function south(dx, dy) { 141 | height += dy; 142 | refresh(); 143 | } 144 | function southWest(dx, dy) { 145 | height += dy; 146 | width -= dx; 147 | left += dx; 148 | refresh(); 149 | } 150 | function west(dx, dy) { 151 | width -= dx; 152 | left += dx; 153 | refresh(); 154 | } 155 | function northWest(dx, dy) { 156 | height -= dy; 157 | top += dy; 158 | width -= dx; 159 | left += dx; 160 | refresh(); 161 | } 162 | function titleBar(dx, dy) { 163 | top += dy; 164 | left += dx; 165 | refresh(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src-minimal/ui/code-mirror-editor.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var CodeMirror = require('codemirror/lib/codemirror'); 4 | require("codemirror/addon/edit/closebrackets"); 5 | require("codemirror/addon/comment/comment"); 6 | require("codemirror/keymap/sublime"); 7 | require("codemirror/addon/hint/anyword-hint"); 8 | require("codemirror/addon/hint/show-hint"); 9 | require('cm-jackl-mode'); 10 | require('cm-jon-mode'); 11 | 12 | module.exports = CodeMirrorEditor; 13 | 14 | function CodeMirrorEditor(emit) { 15 | var id; 16 | var code, mode, theme; 17 | var el; 18 | var cm = new CodeMirror(function (root) { 19 | el = root; 20 | }, { 21 | keyMap: "sublime", 22 | // lineNumbers: true, 23 | rulers: [{ column: 80 }], 24 | autoCloseBrackets: true, 25 | matchBrackets: true, 26 | showCursorWhenSelecting: true, 27 | styleActiveLine: true, 28 | }); 29 | setTimeout(function () { 30 | cm.refresh(); 31 | }, 0); 32 | 33 | cm.on("focus", function () { 34 | emit("focus", id); 35 | }); 36 | 37 | var replacements = { 38 | "lambda": "λ", 39 | "*": "×", 40 | "/": "÷", 41 | "<=": "≤", 42 | ">=": "≥", 43 | "!=": "≠", 44 | }; 45 | 46 | cm.on("change", function (cm, change) { 47 | if (mode !== "jackl" || change.text[0] !== " ") return; 48 | var type = cm.getTokenTypeAt(change.from); 49 | if (type !== "operator" && type !== "builtin") return; 50 | var token = cm.getTokenAt(change.from, true); 51 | var replacement = replacements[token.string]; 52 | if (!replacement) return; 53 | var line = change.to.line; 54 | cm.replaceRange(replacement, { 55 | ch: token.start, 56 | line: line 57 | }, { 58 | ch: token.end, 59 | line: line 60 | }); 61 | }); 62 | 63 | return { render: render }; 64 | 65 | function render(isDark, props) { 66 | id = props.id; 67 | var newTheme = isDark ? "notebook-dark" : "notebook"; 68 | if (newTheme !== theme) { 69 | theme = newTheme; 70 | cm.setOption("theme", theme); 71 | } 72 | if (props.mode !== mode) { 73 | mode = props.mode; 74 | cm.setOption("mode", mode); 75 | } 76 | if (props.code !== code) { 77 | code = props.code; 78 | cm.setValue(code); 79 | } 80 | if (props.focused && !cm.hasFocus()) { 81 | cm.focus(); 82 | } 83 | return el; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src-minimal/ui/desktop.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var AppWindow = require('./app-window'); 4 | var CodeMirrorEditor = require('./code-mirror-editor'); 5 | 6 | var jkl = '-- Inverted question mark!\n(macro (¿ no yes cond)\n [[:? cond yes no]]\n)\n\n-- Sample output for 3x3 maze\n\n-- ██████████████\n-- ██ ██ ██\n-- ██ ██████ ██\n-- ██ ██\n-- ██ ██ ██████\n-- ██ ██ ██\n-- ██████████████\n\n(def width 30)\n(def height 30)\n(def size (× width height))\n\n-- Cells point to parent\n(def cells (map (i size) [i null]))\n\n-- Walls flag right and bottom\n(def walls (map (i size) [true true]))\n\n-- Define the sequence of index and right/left\n(def ww (- width 1))\n(def hh (- height 1))\n(def sequence (shuffle (concat\n (map (i size)\n (if (< (% i width) ww) [true i])\n )\n (map (i size)\n (if (< (÷ i width) hh) [false i])\n )\n)))\n\n-- Find the root of a set cell -> cell\n(def (find-root cell)\n (? (. cell 1) (find-root (. cell 1)) cell)\n)\n\n(for (item sequence)\n (def i (. item 1))\n (def root (find-root (. cells i)))\n (def other (find-root (. cells (+ i (? (. item 0) 1 width)))))\n (if (≠ (. root 0) (. other 0))\n (. root 1 other)\n (. (. walls i) (? (. item 0) 0 1) false)\n )\n)\n\n(def w (× width 2))\n(def h (× height 2))\n(join "\\n" (map (y (+ h 1))\n (join "" (map (x (+ w 1))\n (¿ " " "██" (or\n -- Four outer edges are always true\n (= x 0) (= y 0) (= x w) (= y h)\n -- Inner cells are more complicated\n (? (% y 2)\n (? (% x 2)\n -- cell middle\n false\n -- cell right\n (. (. walls (+ (÷ (- x 1) 2) (× (÷ y 2) width))) 0)\n )\n (? (% x 2)\n -- cell bottom\n (. (. walls (+ (÷ x 2) (× (÷ (- y 1) 2) width))) 1)\n -- cell corner\n true\n )\n )\n ))\n ))\n))\n'; 7 | 8 | var config = require('../data/config'); 9 | module.exports = Desktop; 10 | 11 | function Desktop(emit, refresh) { 12 | var isDark = false; 13 | window.addEventListener("keydown", onKeyDown); 14 | window.addEventListener("resize", onResize); 15 | var width = window.innerWidth; 16 | var height = window.innerHeight; 17 | var windows = [ 18 | { id: genId(), 19 | title: "config.jon", code: config.get(), mode: "jon" }, 20 | { id: genId(), 21 | title: "bananas/samples/maze.jkl", code: jkl, mode: "jackl" }, 22 | ]; 23 | 24 | return { 25 | render: render, 26 | on: { 27 | destroy: onWindowDestroy, 28 | focus: onWindowFocus 29 | } 30 | }; 31 | 32 | function findWindow(id) { 33 | for (var i = 0; i < windows.length; i++) { 34 | if (windows[i].id === id) return i; 35 | } 36 | throw new Error("Invalid window id: " + id); 37 | } 38 | 39 | function onWindowDestroy(id) { 40 | windows.splice(findWindow(id), 1); 41 | refresh(); 42 | } 43 | 44 | function onWindowFocus(id) { 45 | var dirty = false; 46 | for (var i = 0; i < windows.length; i++) { 47 | var window = windows[i]; 48 | var focused = window.id === id; 49 | if (focused !== window.focused) { 50 | window.focused = focused; 51 | dirty = true; 52 | } 53 | windows[i].focused = windows[i].id === id; 54 | } 55 | if (dirty) refresh(); 56 | } 57 | 58 | function onResize(evt) { 59 | var newWidth = window.innerWidth; 60 | var newHeight = window.innerHeight; 61 | if (newWidth !== width || newHeight !== height) { 62 | width = newWidth; 63 | height = newHeight; 64 | refresh(); 65 | } 66 | } 67 | 68 | function onKeyDown(evt) { 69 | var mod = (evt.ctrlKey ? 1 : 0) | 70 | (evt.shiftKey ? 2 : 0) | 71 | (evt.altKey ? 4 : 0) | 72 | (evt.metaKey ? 8 : 0); 73 | if (mod === 1 && evt.keyCode === 66) { 74 | // Control-B - toggle theme 75 | evt.preventDefault(); 76 | isDark = !isDark; 77 | refresh(); 78 | } 79 | } 80 | 81 | function render() { 82 | return windows.map(function (props) { 83 | var ui = [AppWindow, width, height, isDark, props, 84 | [CodeMirrorEditor, isDark, props] 85 | ]; 86 | ui.key = props.id; 87 | return ui; 88 | }); 89 | } 90 | } 91 | 92 | 93 | function genId() { 94 | return Date.now().toString(36) + (Math.random() * 0x100000000).toString(36); 95 | } -------------------------------------------------------------------------------- /src-minimal/ui/drag-helper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var dragging = {}; 3 | 4 | // Given a move handler, call it while dragging with delta-x and delta-y 5 | // Supports mouse-events, touch-events and pointer-events. 6 | // Returns a map of event handlers for use in domchanger 7 | // Usage: drag(onMove(dx, dy){}) -> props 8 | module.exports = drag; 9 | 10 | var usePointer = !!window.PointerEvent; 11 | 12 | // Need global events for up and move since the target often changes. 13 | if (usePointer) { 14 | window.addEventListener("pointerup", onPointerUp); 15 | window.addEventListener("pointermove", onPointerMove); 16 | } 17 | else { 18 | window.addEventListener("mouseup", onMouseUp); 19 | window.addEventListener("mousemove", onMouseMove); 20 | window.addEventListener("touchend", onTouchEnd); 21 | window.addEventListener("touchmove", onTouchMove); 22 | } 23 | 24 | function drag(fn) { 25 | return usePointer ? { 26 | onpointerdown: onPointerDown 27 | } : { 28 | onmousedown: onMouseDown, 29 | ontouchstart: onTouchStart 30 | }; 31 | 32 | function onPointerDown(evt) { 33 | var id = evt.pointerId; 34 | if (dragging[id]) return; 35 | evt.preventDefault(); 36 | start(id, evt.clientX, evt.clientY, fn); 37 | } 38 | 39 | function onTouchStart(evt) { 40 | var found = false; 41 | for (var i = 0; i < evt.changedTouches.length; i++) { 42 | var touch = evt.changedTouches[i]; 43 | var id = touch.identifier; 44 | if (!dragging[id]) { 45 | found = true; 46 | start(id, touch.clientX, touch.clientY, fn); 47 | } 48 | } 49 | if (found) { 50 | evt.preventDefault(); 51 | } 52 | } 53 | 54 | function onMouseDown(evt) { 55 | if (dragging.mouse) return; 56 | evt.preventDefault(); 57 | start("mouse", evt.clientX, evt.clientY, fn); 58 | } 59 | } 60 | 61 | function onPointerMove(evt) { 62 | var id = evt.pointerId; 63 | if (!dragging[id]) return; 64 | evt.preventDefault(); 65 | evt.stopPropagation(); 66 | move(id, evt.clientX, evt.clientY); 67 | } 68 | 69 | function onPointerUp(evt) { 70 | var id = evt.pointerId; 71 | if (!dragging[id]) return; 72 | evt.preventDefault(); 73 | evt.stopPropagation(); 74 | stop(id); 75 | } 76 | 77 | function onTouchMove(evt) { 78 | var found = false; 79 | for (var i = 0; i < evt.changedTouches.length; i++) { 80 | var touch = evt.changedTouches[i]; 81 | var id = touch.identifier; 82 | if (dragging[id]) { 83 | found = true; 84 | move(id, touch.clientX, touch.clientY); 85 | } 86 | } 87 | if (found) { 88 | evt.preventDefault(); 89 | evt.stopPropagation(); 90 | } 91 | } 92 | 93 | function onTouchEnd(evt) { 94 | var found = false; 95 | for (var i = 0; i < evt.changedTouches.length; i++) { 96 | var touch = evt.changedTouches[i]; 97 | var id = touch.identifier; 98 | if (dragging[id]) { 99 | found = true; 100 | stop(id); 101 | } 102 | } 103 | if (found) { 104 | evt.preventDefault(); 105 | evt.stopPropagation(); 106 | } 107 | } 108 | 109 | function onMouseMove(evt) { 110 | if (!dragging.mouse) return; 111 | evt.preventDefault(); 112 | evt.stopPropagation(); 113 | move("mouse", evt.clientX, evt.clientY); 114 | } 115 | 116 | function onMouseUp(evt) { 117 | if (!dragging.mouse) return; 118 | evt.preventDefault(); 119 | evt.stopPropagation(); 120 | stop("mouse"); 121 | } 122 | 123 | 124 | function start(id, x, y, fn) { 125 | dragging[id] = { 126 | x: x, 127 | y: y, 128 | fn: fn 129 | }; 130 | } 131 | 132 | function move(id, x, y) { 133 | var data = dragging[id]; 134 | data.fn(x - data.x, y - data.y); 135 | data.x = x; 136 | data.y = y; 137 | } 138 | 139 | function stop(id) { 140 | dragging[id] = null; 141 | } 142 | -------------------------------------------------------------------------------- /src-ui/locker.js: -------------------------------------------------------------------------------- 1 | var modes = require('js-git/lib/modes'); 2 | 3 | module.exports = function (repo, rootHash, onRootChange) { 4 | var writing = false; 5 | var writeLocks = {}; 6 | var readLocks = {}; 7 | var queue = []; 8 | 9 | return begin; 10 | 11 | function* begin(prefix, writable) { 12 | // Check and normalize inputs 13 | if (typeof prefix !== "string") { 14 | throw new TypeError("prefix must be string"); 15 | } 16 | prefix = normalizePath(prefix); 17 | prefix = prefix ? prefix + "/" : prefix; 18 | writable = !!writable; 19 | 20 | // Wait for lock acquisition 21 | var lock = yield* getLock(prefix, writable); 22 | 23 | var alive = true; 24 | var edits = {}; 25 | var cache = {}; 26 | 27 | // Read the current state of the prefix 28 | var entry = yield repo.pathToEntry(rootHash, prefix); 29 | if (entry.mode !== modes.tree) { 30 | throw new Error("No such tree " + prefix); 31 | } 32 | entry.repo = repo; 33 | cache[""] = entry; 34 | 35 | // Build and return the external API 36 | var api = {end: end, read: read, getRepo: getRepo}; 37 | if (writable) api.write = write; 38 | return api; 39 | 40 | function* pathToEntry(path) { 41 | console.log("p2e", path) 42 | var entry = cache[path]; 43 | if (!entry) { 44 | var index = path.lastIndexOf("/"); 45 | if (index < 0) return; 46 | var parent = yield* pathToEntry(path.substring(0, index)); 47 | if (parent.mode !== modes.tree) return; 48 | var tree = entry.tree || (yield repo.loadAs("tree", parent.hash)); 49 | entry = tree[path.substring(index + 1)]; 50 | if (!entry) return {last: parent}; 51 | cache[path] = entry; 52 | if (!entry.repo) entry.repo = repo; 53 | } 54 | if (entry.mode === modes.tree && !entry.tree) { 55 | entry.tree = yield entry.repo.loadAs("tree", entry.hash); 56 | } 57 | return entry; 58 | } 59 | 60 | // The user calls this when they wish to end the transaction and release the lock. 61 | function* end() { 62 | if (!alive) throw new Error("Transaction closed"); 63 | alive = false; 64 | throw new Error("TODO: flush writes"); 65 | yield* releaseLock(lock); 66 | } 67 | 68 | function* read(path) { 69 | path = check(path); 70 | var entry = yield* pathToEntry(path); 71 | if (entry.mode) return entry; 72 | } 73 | 74 | function* getRepo(path) { 75 | path = check(path); 76 | var entry = yield* pathToEntry(path); 77 | if (entry.last) entry = entry.last; 78 | return entry.repo; 79 | } 80 | 81 | function* write(path, entry) { 82 | path = check(path); 83 | var oldEntry = yield* pathToEntry(path); 84 | if (!oldEntry.mode && oldEntry.last.mode !== modes.tree) { 85 | throw new Error("Can't create path " + path); 86 | } 87 | edits[path] = entry; 88 | 89 | throw new Error("TODO: Implement write"); 90 | } 91 | 92 | function check(path) { 93 | if (!alive) throw new Error("Transaction closed"); 94 | path = normalizePath(path); 95 | if (path + "/" !== prefix && path.substring(0, prefix.length) !== prefix) { 96 | throw new Error("Path " + path + " outside prefix " + prefix); 97 | } 98 | return path.substring(prefix.length); 99 | } 100 | } 101 | 102 | function* getLock(prefix, writable) { 103 | return { 104 | prefix: prefix, 105 | writable: writable 106 | }; 107 | return yield function (fn) { 108 | queue.push({prefix: prefix, writable: writable, fn: fn}); 109 | }; 110 | } 111 | 112 | function* releaseLock(lock) { 113 | // throw new Error("TODO: release lock"); 114 | } 115 | 116 | }; 117 | 118 | 119 | function normalizePath(path) { 120 | return path.split("/").filter(Boolean).join("/"); 121 | } 122 | 123 | /* 124 | // Get a writable lock to the www folder and it's contents 125 | // No other writable locks will be granted for this section. 126 | var op = yield* fs.startWrite("www"); 127 | op.writeFile("www/index.html", "..."); 128 | op.writeFile("www/style.html", "..."); 129 | // Reading is also allowed from a read lock 130 | yield* op.readEntry(".gitmodules"); 131 | // You can even read yet-to-be-written changes 132 | yield* op.readFile("www/style.html"); 133 | // Close the lock, returns the new tree hash for the requested root. 134 | yield* op.close(); 135 | 136 | // Get a readable lock, there can be many concurrent reads, but only one 137 | // concurrent write. 138 | var op = yield* fs.startRead("www/css"); 139 | // Read the tree entries 140 | yield* op.readTree("www/css"); 141 | // Release the lock when you're done so writes can happen. 142 | yield* op.close(); 143 | 144 | // // Read ops 145 | // yield* op.readEntry(path) 146 | // yield* op.readFile(path) 147 | // yield* op.readTree(path) 148 | // // Write ops 149 | // op.writeEntry(path, entry) 150 | // op.writeFile(path, contents) 151 | // op.deleteEntry(path) 152 | // op.moveEntry(oldPath, newPath) 153 | // // close flushing any writes and releasing lock 154 | // yield* op.close() 155 | */ 156 | -------------------------------------------------------------------------------- /src-ui/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var repo = {}; 4 | yield require('js-git/mixins/indexed-db').init; 5 | require('js-git/mixins/indexed-db')(repo, "test.git"); 6 | require('js-git/mixins/path-to-entry')(repo); 7 | require('js-git/mixins/create-tree')(repo); 8 | // require('js-git/mixins/walkers')(repo); 9 | // require('js-git/mixins/pack-ops')(repo); 10 | // require('js-git/mixins/delay')(repo, 100); 11 | require('js-git/mixins/formats')(repo); 12 | 13 | var modes = require('js-git/lib/modes'); 14 | 15 | // var domChanger = require('domchanger/domchanger.js'); 16 | // var Tree = require('./tree'); 17 | 18 | // domChanger(Tree, document.body).update(repo, "test.git"); 19 | 20 | var head = yield repo.readRef("refs/heads/master"); 21 | var commit = yield repo.loadAs("commit", head); 22 | 23 | var begin = require('./locker')(repo, commit.tree); 24 | 25 | var op = yield* begin("www"); 26 | var entry = yield* op.read("www"); 27 | console.log(entry); 28 | yield* op.end(); 29 | op = yield* begin("", true); 30 | var repo = yield* op.getRepo("www/greeting.txt"); 31 | yield* op.write("www/greeting.txt", { 32 | mode: modes.blob, 33 | hash: yield repo.saveAs("blob", "Hello World") 34 | }); 35 | -------------------------------------------------------------------------------- /src-ui/style.css: -------------------------------------------------------------------------------- 1 | 2 | ul.tree { 3 | -moz-user-select: none; 4 | user-select: none; 5 | padding: 0.1em 0.1em 2em 0.1em; 6 | margin: 0; 7 | font-family: Ubuntu, sans-serif; 8 | } 9 | .tree, .tree ul { 10 | margin: 0; 11 | padding: 0; 12 | line-height: 1.4em; 13 | list-style-type: none; 14 | } 15 | .tree ul { 16 | padding-left: 0.7em; 17 | } 18 | .tree li { 19 | white-space: nowrap; 20 | } 21 | .tree .row { 22 | margin-left: -10em; 23 | padding-left: 10em; 24 | margin-right: -10em; 25 | padding-right: 10em; 26 | cursor: pointer; 27 | } 28 | .tree .tight { 29 | margin: 0 -2px 0 -4px; 30 | } 31 | .theme-light .row:hover { 32 | box-shadow: inset 0 0 0.3em rgba(0,0,0,0.5); 33 | } 34 | .theme-dark .row:hover { 35 | box-shadow: inset 0 0 0.3em rgba(255,255,255,0.5); 36 | } 37 | .theme-dark { 38 | background-color: #333; 39 | color: #eee; 40 | } 41 | -------------------------------------------------------------------------------- /src-ui/tree.js: -------------------------------------------------------------------------------- 1 | var modes = require('js-git/lib/modes'); 2 | var defer = require('js-git/lib/defer'); 3 | var run = require('gen-run/run.js'); 4 | 5 | module.exports = Tree; 6 | 7 | function Tree(emit, refresh) { 8 | var root = {}; 9 | var repo, name; 10 | 11 | run(function* () { 12 | yield defer; 13 | var head = yield repo.readRef("refs/heads/master"); 14 | if (!head) { 15 | console.log("No repo found, creating a new one"); 16 | head = yield repo.saveAs("commit", { 17 | tree: yield repo.saveAs("tree", {}), 18 | author: { 19 | name: "AutoInit", 20 | email: "js-git@creationix.com" 21 | }, 22 | message: "Auto-init empty repo" 23 | }); 24 | yield repo.updateRef("refs/heads/master", head); 25 | } 26 | var commit = yield repo.loadAs("commit", head); 27 | root.mode = modes.tree; 28 | root.hash = commit.tree; 29 | refresh(); 30 | }); 31 | 32 | return { 33 | render: render, 34 | on: { 35 | click: onChildClick 36 | } 37 | }; 38 | 39 | function render(newRepo, newName) { 40 | repo = newRepo; 41 | name = newName; 42 | return [ 43 | ["form", {onsubmit: onSubmit}, 44 | ["input", {name: "path", placeholder: "path", required: true}], 45 | ["input", {type: "submit", "value": "Create File"}], 46 | ], 47 | ["ul.tree", 48 | [Row, name, root] 49 | ] 50 | ]; 51 | } 52 | 53 | function onSubmit(evt) { 54 | /*jshint validthis:true*/ 55 | evt.preventDefault(); 56 | var entries = [{ mode: modes.blob, content: "", path: this.path.value }]; 57 | entries.base = root.hash; 58 | run(function* () { 59 | root.hash = yield repo.createTree(entries); 60 | yield repo.updateRef("refs/heads/master", yield repo.saveAs("commit", { 61 | tree: root.hash, 62 | author: { 63 | name: "JS-Git", 64 | email: "js-git@creationix.com" 65 | }, 66 | message: "Auto commit" 67 | })); 68 | yield* cleanup(root); 69 | }); 70 | } 71 | 72 | function onChildClick(path, node) { 73 | run(function* () { 74 | node.treeHash = node.hash; 75 | node.tree = yield repo.loadAs("tree", node.hash); 76 | node.busy = false; 77 | node.open = true; 78 | refresh(); 79 | }); 80 | } 81 | 82 | function* cleanup(node) { 83 | if (node.mode !== modes.tree) return; 84 | if (node.hash !== node.treeHash) { 85 | node.treeHash = node.hash; 86 | node.tree = yield repo.loadAs("tree", node.hash); 87 | refresh(); 88 | var names = Object.keys(node.tree); 89 | for (var i = 0; i < names.length; i++) { 90 | var name = names[i]; 91 | var child = node.tree[name]; 92 | if (child.mode === modes.tree) { 93 | yield* cleanup(child); 94 | } 95 | } 96 | } 97 | } 98 | } 99 | 100 | function Row(emit, refresh) { 101 | var path, node; 102 | 103 | return {render: render}; 104 | 105 | function render(newPath, newNode) { 106 | path = newPath; 107 | node = newNode; 108 | var icon = 109 | node.busy ? "icon-spin1 animate-spin" : 110 | node.mode === modes.sym ? "icon-link" : 111 | node.mode === modes.file ? "icon-doc" : 112 | node.mode === modes.exec ? "icon-cog" : 113 | node.mode === modes.commit ? "icon-fork" : 114 | node.open ? "icon-folder-open" : "icon-folder"; 115 | var title = modes.toType(node.mode) + " " + node.hash; 116 | var name = path.substring(path.lastIndexOf("/") + 1); 117 | var ui = ["li", 118 | ["div.row", {onclick: onClick}, 119 | ["i", {class: icon, title: title}], 120 | ["span", {title:path}, name], 121 | ] 122 | ]; 123 | if (node.open) { 124 | var ul = ["ul"]; 125 | ui.push(ul); 126 | var names = Object.keys(node.tree); 127 | for (var i = 0; i < names.length; i++) { 128 | var childName = names[i]; 129 | ul.push([Row, join(path, childName), node.tree[childName]]); 130 | } 131 | } 132 | return ui; 133 | } 134 | 135 | function onClick(evt) { 136 | evt.preventDefault(); 137 | evt.stopPropagation(); 138 | if (node.mode !== modes.tree) return; 139 | if (node.tree) { 140 | node.open = !node.open; 141 | refresh(); 142 | } 143 | else { 144 | node.busy = true; 145 | refresh(); 146 | emit("click", path, node); 147 | } 148 | } 149 | } 150 | 151 | function join(base, name) { 152 | return base ? base + "/" + name : name; 153 | } 154 | -------------------------------------------------------------------------------- /src/backends-atom.js: -------------------------------------------------------------------------------- 1 | var backends = module.exports = []; 2 | backends.push(require('backends/github-clone')); 3 | backends.push(require('backends/indexed-db')); 4 | -------------------------------------------------------------------------------- /src/backends-chrome.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('backends/github-clone'), 3 | require('backends/chrome-fs'), 4 | require('backends/indexed-db'), 5 | ]; 6 | 7 | //foo -------------------------------------------------------------------------------- /src/backends-fxos.js: -------------------------------------------------------------------------------- 1 | var backends = module.exports = []; 2 | backends.push(require('backends/github')); 3 | backends.push(require('backends/indexed-db')); 4 | -------------------------------------------------------------------------------- /src/backends.js: -------------------------------------------------------------------------------- 1 | var backends = module.exports = []; 2 | backends.push(require('backends/github-clone')); 3 | if (window.indexedDB) { 4 | backends.push(require('backends/indexed-db')); 5 | } 6 | else if (window.openDatabase) { 7 | backends.push(require('backends/websql')); 8 | } 9 | else if (window.localStorage) { 10 | // backends.push(require('backends/local-storage')); 11 | // } 12 | 13 | // else { 14 | console.warn("No persistance can be used on this platform"); 15 | // backends.push(require('backends/mem-storage')); 16 | } 17 | -------------------------------------------------------------------------------- /src/backends/chrome-fs.js: -------------------------------------------------------------------------------- 1 | var modes = require('js-git/lib/modes'); 2 | var chrome = window.chrome; 3 | 4 | exports.menuItem = { 5 | icon: "git", 6 | label: "Mount Local Repo", 7 | action: mountBareRepo 8 | }; 9 | 10 | function mountBareRepo(row) { 11 | var fs = require('data/fs'); 12 | chrome.fileSystem.chooseEntry({ type: "openDirectory"}, function (dir) { 13 | if (!dir) return; 14 | var name = dir.name; 15 | dir.getDirectory(".git", {}, function (result) { 16 | dir = result; 17 | go(); 18 | }, go); 19 | function go() { 20 | require('ui/tree').makeUnique(row, name, modes.commit, function (path) { 21 | var entry = chrome.fileSystem.retainEntry(dir); 22 | row.call(path, fs.addRepo, { entry: entry }); 23 | }); 24 | } 25 | }); 26 | } 27 | 28 | exports.createRepo = function (config) { 29 | if (!config.entry) return; 30 | var repo = {}; 31 | 32 | require('git-chrome-fs/mixins/fs-db')(repo, config.entry); 33 | 34 | require('./repo-common')(repo); 35 | 36 | return repo; 37 | }; 38 | -------------------------------------------------------------------------------- /src/backends/encrypt-repo.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var forge = window.forge;//require('forge'); 3 | var bodec = require('bodec'); 4 | var defer = require('js-git/lib/defer'); 5 | var prefs = require('prefs'); 6 | 7 | module.exports = function (storage, passphrase) { 8 | 9 | require('js-git/mixins/path-to-entry')(storage); 10 | require('js-git/mixins/mem-cache')(storage); 11 | require('js-git/mixins/create-tree')(storage); 12 | require('js-git/mixins/formats')(storage); 13 | 14 | // Derive a 32 bit key from the passphrase 15 | var key = forge.pkcs5.pbkdf2(passphrase, 'kodeforkids', 16000, 32); 16 | 17 | var repo = {}; 18 | var fs = require('js-git/lib/git-fs')(storage, { 19 | shouldEncrypt: function (path) { 20 | // We only want to encrypt the actual blobs 21 | // Everything else can be plaintext. 22 | return path.split("/").filter(Boolean)[0] === "objects"; 23 | }, 24 | encrypt: function (plain) { 25 | var iv = forge.random.getBytesSync(16); 26 | var cipher = forge.cipher.createCipher('AES-CBC', key); 27 | cipher.start({iv: iv}); 28 | var raw = bodec.toRaw(plain); 29 | cipher.update(forge.util.createBuffer(raw)); 30 | cipher.finish(); 31 | var encrypted = cipher.output.bytes(); 32 | return bodec.fromRaw(iv + encrypted); 33 | }, 34 | decrypt: function (encrypted) { 35 | var decipher = forge.cipher.createDecipher('AES-CBC', key); 36 | var iv = bodec.toRaw(encrypted, 0, 16); 37 | encrypted = bodec.toRaw(encrypted, 16); 38 | decipher.start({iv: iv}); 39 | decipher.update(forge.util.createBuffer(encrypted)); 40 | decipher.finish(); 41 | return bodec.fromRaw(decipher.output.bytes()); 42 | }, 43 | getRootTree: function (callback) { 44 | 45 | if (rootTree) { 46 | callback(null, rootTree); 47 | callback = null; 48 | if (Date.now() - rootTime < 1000) return; 49 | } 50 | storage.readRef("refs/heads/master", function (err, hash) { 51 | if (!hash) return callback(err); 52 | storage.loadAs("commit", hash, function (err, commit) { 53 | if (!commit) return callback(err); 54 | rootTree = commit.tree; 55 | rootTime = Date.now(); 56 | if (callback) callback(null, commit.tree); 57 | }); 58 | }); 59 | }, 60 | setRootTree: function (hash, callback) { 61 | rootTree = hash; 62 | rootTime = Date.now(); 63 | defer(saveRoot); 64 | callback(); 65 | } 66 | }); 67 | 68 | var rootTree; 69 | var rootTime; 70 | var saving, savedRoot; 71 | function saveRoot() { 72 | if (saving || savedRoot === rootTree) return; 73 | saving = rootTree; 74 | storage.saveAs("commit", { 75 | tree: rootTree, 76 | author: { 77 | name: prefs.get("userName", "JS-Git"), 78 | email: prefs.get("userEmail", "js-git@creationix.com") 79 | }, 80 | message: "Auto commit to update fs image" 81 | }, function (err, hash) { 82 | if (!hash) return onDone(err); 83 | storage.updateRef("refs/heads/master", hash, function (err) { 84 | onDone(err); 85 | }, true); 86 | 87 | function onDone(err) { 88 | if (!err) savedRoot = saving; 89 | saving = false; 90 | if (err) throw err; 91 | } 92 | }); 93 | } 94 | 95 | require('js-git/mixins/fs-db')(repo, fs); 96 | 97 | return repo; 98 | 99 | }; -------------------------------------------------------------------------------- /src/backends/github-clone.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var modes = require('js-git/lib/modes'); 4 | var prefs = require('prefs'); 5 | 6 | exports.menuItem = { 7 | icon: "download-cloud", 8 | label: "Clone Github Repo", 9 | action: addGithubClone 10 | }; 11 | 12 | exports.createRepo = function (config) { 13 | if (!config.github) return; 14 | if (!config.url) throw new Error("Missing url in github config"); 15 | var githubToken = prefs.get("githubToken", ""); 16 | if (!githubToken) throw new Error("Missing github access token"); 17 | var remote = {}; 18 | var githubName = getGithubName(config.url); 19 | require('js-github/mixins/github-db')(remote, githubName, githubToken); 20 | require('js-git/mixins/read-combiner')(remote); 21 | 22 | if (config.passphrase) { 23 | remote = require('./encrypt-repo')(remote, config.passphrase); 24 | } 25 | 26 | var repo = {}; 27 | if (!config.prefix) { 28 | config.prefix = Date.now().toString(36) + "-" + (Math.random() * 0x100000000).toString(36); 29 | } 30 | require('js-git/mixins/indexed-db')(repo, config.prefix); 31 | prefs.save(); 32 | 33 | require('js-git/mixins/sync')(repo, remote); 34 | require('js-git/mixins/fall-through')(repo, remote); 35 | 36 | require('./repo-common')(repo); 37 | 38 | return repo; 39 | }; 40 | 41 | function getGithubName(url) { 42 | var match = url.match(/github.com[:\/](.*?)(?:\.git)?$/); 43 | if (!match) throw new Error("Url is not github repo: " + url); 44 | return match[1]; 45 | } 46 | 47 | function addGithubClone(row) { 48 | var prefs = require('prefs'); 49 | var githubToken = prefs.get("githubToken", ""); 50 | var dialog = require('ui/dialog'); 51 | 52 | dialog.multiEntry("Mount Github Repo", [ 53 | {name: "path", placeholder: "user/name", required:true}, 54 | {name: "ref", placeholder: "refs/heads/master"}, 55 | {name: "name", placeholder: "localname"}, 56 | {name: "passphrase", placeholder: "encryption passphrase"}, 57 | {name: "token", placeholder: "Enter github auth token", required:true, value: githubToken} 58 | ], function (result) { 59 | if (!result) return; 60 | if (result.token !== githubToken) { 61 | prefs.set("githubToken", result.token); 62 | } 63 | var url = result.path; 64 | // Assume github if user/name combo is given 65 | if (/^[^\/:@]+\/[^\/:@]+$/.test(url)) { 66 | url = "git@github.com:" + url + ".git"; 67 | } 68 | var fs = require('data/fs'); 69 | var name = result.name || result.path.match(/[^\/]*$/)[0]; 70 | var ref = result.ref || "refs/heads/master"; 71 | var config = { 72 | url: url, 73 | ref: ref, 74 | github: true 75 | }; 76 | if (result.passphrase) config.passphrase = result.passphrase; 77 | require('ui/tree').makeUnique(row, name, modes.commit, function (path) { 78 | row.call(path, fs.addRepo, config); 79 | }); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /src/backends/github.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var modes = require('js-git/lib/modes'); 4 | 5 | exports.menuItem = { 6 | icon: "github", 7 | label: "Mount Github Repo", 8 | action: addGithubMount 9 | }; 10 | 11 | exports.createRepo = function (config) { 12 | if (!config.github) return; 13 | var repo = {}; 14 | if (!config.url) throw new Error("Missing url in github config"); 15 | var githubName = getGithubName(config.url); 16 | var prefs = require('prefs'); 17 | var githubToken = prefs.get("githubToken", ""); 18 | if (!githubToken) throw new Error("Missing github access token"); 19 | require('js-github/mixins/github-db')(repo, githubName, githubToken); 20 | 21 | if (config.passphrase) { 22 | repo = require('./encrypt-repo')(repo, config.passphrase); 23 | } 24 | 25 | // Cache github objects locally in indexeddb 26 | if (window.indexedDB) { 27 | require('js-git/mixins/add-cache')(repo, require('js-git/mixins/indexed-db')); 28 | } 29 | else { 30 | require('js-git/mixins/add-cache')(repo, require('js-git/mixins/websql-db')); 31 | } 32 | 33 | require('./repo-common')(repo); 34 | 35 | return repo; 36 | }; 37 | 38 | function getGithubName(url) { 39 | var match = url.match(/github.com[:\/](.*?)(?:\.git)?$/); 40 | if (!match) throw new Error("Url is not github repo: " + url); 41 | return match[1]; 42 | } 43 | 44 | function addGithubMount(row) { 45 | var prefs = require('prefs'); 46 | var githubToken = prefs.get("githubToken", ""); 47 | var dialog = require('ui/dialog'); 48 | 49 | dialog.multiEntry("Mount Github Repo", [ 50 | {name: "path", placeholder: "user/name", required:true}, 51 | {name: "ref", placeholder: "refs/heads/master"}, 52 | {name: "name", placeholder: "localname"}, 53 | {name: "passphrase", placeholder: "encryption passphrase"}, 54 | {name: "token", placeholder: "Enter github auth token", required:true, value: githubToken} 55 | ], function (result) { 56 | if (!result) return; 57 | if (result.token !== githubToken) { 58 | prefs.set("githubToken", result.token); 59 | } 60 | var url = result.path; 61 | // Assume github if user/name combo is given 62 | if (/^[^\/:@]+\/[^\/:@]+$/.test(url)) { 63 | url = "git@github.com:" + url + ".git"; 64 | } 65 | var fs = require('data/fs'); 66 | var name = result.name || result.path.match(/[^\/]*$/)[0]; 67 | var ref = result.ref || "refs/heads/master"; 68 | var config = { 69 | url: url, 70 | ref: ref, 71 | github: true 72 | }; 73 | if (result.passphrase) config.passphrase = result.passphrase; 74 | require('ui/tree').makeUnique(row, name, modes.commit, function (path) { 75 | row.call(path, fs.addRepo, config); 76 | }); 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /src/backends/indexed-db.js: -------------------------------------------------------------------------------- 1 | var prefs = require('prefs'); 2 | 3 | 4 | exports.init = require('js-git/mixins/indexed-db').init; 5 | 6 | exports.createRepo = function (config) { 7 | var repo = {}; 8 | if (!config.prefix) { 9 | config.prefix = Date.now().toString(36) + "-" + (Math.random() * 0x100000000).toString(36); 10 | prefs.save(); 11 | } 12 | require('js-git/mixins/indexed-db')(repo, config.prefix); 13 | prefs.save(); 14 | 15 | require('./repo-common')(repo); 16 | 17 | return repo; 18 | }; 19 | -------------------------------------------------------------------------------- /src/backends/local-storage.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creationix/tedit/35e76c468e7c799e6b40f41b5a106c3c35ef7afb/src/backends/local-storage.js -------------------------------------------------------------------------------- /src/backends/repo-common.js: -------------------------------------------------------------------------------- 1 | module.exports = function (repo) { 2 | require('js-git/mixins/create-tree')(repo); 3 | 4 | // Cache everything except blobs over 100 bytes in memory. 5 | require('js-git/mixins/mem-cache')(repo); 6 | 7 | // Combine concurrent read requests for the same hash 8 | require('js-git/mixins/read-combiner')(repo); 9 | 10 | require('js-git/mixins/walkers')(repo); 11 | 12 | // Add in value formatting niceties. Also adds text and array types. 13 | require('js-git/mixins/formats')(repo); 14 | }; 15 | -------------------------------------------------------------------------------- /src/backends/websql.js: -------------------------------------------------------------------------------- 1 | var prefs = require('prefs'); 2 | 3 | exports.init = require('js-git/mixins/websql-db').init; 4 | 5 | exports.createRepo = function (config) { 6 | var repo = {}; 7 | if (!config.prefix) { 8 | config.prefix = Date.now().toString(36) + "-" + (Math.random() * 0x100000000).toString(36); 9 | prefs.save(); 10 | } 11 | require('js-git/mixins/websql-db')(repo, config.prefix); 12 | prefs.save(); 13 | 14 | require('./repo-common')(repo); 15 | 16 | return repo; 17 | }; 18 | -------------------------------------------------------------------------------- /src/data/document.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /*global ace*/ 3 | 4 | var modelist = ace.require('ace/ext/modelist'); 5 | 6 | var whitespace = ace.require('ace/ext/whitespace'); 7 | 8 | var editor = require('ui/editor'); 9 | var binary = require('bodec'); 10 | var modes = require('js-git/lib/modes'); 11 | var recent = []; 12 | var recentIndex = 0; 13 | var current; 14 | 15 | var imageTypes = { 16 | png: "image/png", 17 | gif: "image/gif", 18 | jpg: "image/jpeg", 19 | jpeg: "image/jpeg", 20 | svg: "image/svg+xml", 21 | }; 22 | 23 | // JSHint Options for JavaScript files 24 | var hintOptions = [{ 25 | unused: true, 26 | undef: true, 27 | esnext: true, 28 | browser: true, 29 | node: true, 30 | onevar: false, 31 | passfail: false, 32 | maxerr: 100, 33 | multistr: true, 34 | globalstrict: true 35 | }]; 36 | 37 | module.exports = setDoc; 38 | setDoc.next = next; 39 | setDoc.reset = reset; 40 | 41 | function setDoc(row, body) { 42 | if (!row) return editor.setDoc(); 43 | var doc = row.doc || (row.doc = { save: save }); 44 | doc.row = row; 45 | 46 | var ext = row.path.match(/[^.]*$/)[0].toLowerCase(); 47 | var imageType = imageTypes[ext]; 48 | if (imageType) { 49 | if (doc.session) delete doc.session; 50 | if (doc.hash !== row.hash) { 51 | var blob = new Blob([body], { type: imageType }); 52 | doc.url = window.URL.createObjectURL(blob); 53 | } 54 | } 55 | else { 56 | var code = binary.toUnicode(body); 57 | if (doc.url) delete doc.url; 58 | if (!doc.session) { 59 | doc.session = ace.createEditSession(code); 60 | doc.code = code; 61 | doc.session.setTabSize(2); 62 | whitespace.detectIndentation(doc.session); 63 | doc.mode = 0; 64 | } 65 | else if (doc.code !== code) { 66 | doc.session.setValue(code, 1); 67 | doc.code = code; 68 | whitespace.detectIndentation(doc.session); 69 | } 70 | if (doc.mode !== row.mode) { 71 | var aceMode = 72 | /\.rule/.test(row.path) ? "ace/mode/jack" : 73 | /\.gitmodules/.test(row.path) ? "ace/mode/ini" : 74 | /\.webapp/.test(row.path) ? "ace/mode/json" : 75 | row.mode === modes.sym ? "ace/mode/text" : 76 | modelist.getModeForPath(row.path).mode; 77 | doc.session.setMode(aceMode, function () { 78 | if (aceMode !== "ace/mode/javascript") return; 79 | doc.session.$worker.call("setOptions", hintOptions); 80 | }); 81 | doc.mode = row.mode; 82 | } 83 | 84 | } 85 | doc.hash = row.hash; 86 | 87 | current = doc; 88 | reset(); 89 | editor.setDoc(doc); 90 | 91 | function save(text) { 92 | if (text === doc.code) return; 93 | doc.code = text; 94 | setDoc.updateDoc(row, binary.fromUnicode(text)); 95 | } 96 | } 97 | 98 | function next() { 99 | if (!recent.length) return; 100 | recentIndex = (recentIndex + 1) % recent.length; 101 | current = recent[recentIndex]; 102 | editor.setDoc(current); 103 | setDoc.setActive(current.row.path); 104 | } 105 | 106 | function reset() { 107 | if (!current) return; 108 | // Put current at the front of the recent list. 109 | var index = recent.indexOf(current); 110 | if (index >= 0) recent.splice(index, 1); 111 | recent.unshift(current); 112 | recentIndex = 0; 113 | } 114 | -------------------------------------------------------------------------------- /src/data/fs.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var backends = require('backends'); 4 | var prefs = require('prefs'); 5 | // Data for repos is keyed by path. The root config is keyed by "". 6 | // Live js-git instances by path 7 | var repos = {}; 8 | // Config data by path 9 | var configs = prefs.get("configs", {}); 10 | // Store the hash to the current root node 11 | var rootHash = prefs.get("rootHash"); 12 | 13 | module.exports = require('git-tree')({ 14 | configs: configs, 15 | repos: repos, 16 | getRootHash: function () { return rootHash; }, 17 | setRootHash: function (hash) { 18 | rootHash = hash; 19 | prefs.set("rootHash", hash); 20 | }, 21 | saveConfig: prefs.save, 22 | createRepo: createRepo, 23 | }); 24 | module.exports.repos = repos; 25 | module.exports.configs = configs; 26 | 27 | // Create a repo instance from a config 28 | function createRepo(config) { 29 | for (var i = 0, l = backends.length; i < l; i++) { 30 | var repo = backends[i].createRepo(config); 31 | if (repo) return repo; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/data/hooks.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creationix/tedit/35e76c468e7c799e6b40f41b5a106c3c35ef7afb/src/data/hooks.js -------------------------------------------------------------------------------- /src/data/importfs.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var modes = require('js-git/lib/modes'); 4 | var ignores = importEntry.ignores = [".git", "node_modules"]; 5 | 6 | module.exports = importEntry; 7 | 8 | function importEntry(repo, entry, callback) { 9 | repo.importStatus = "Loading " + entry.fullPath; 10 | if (entry.isDirectory) return importDirectory(repo, entry, callback); 11 | if (entry.isFile) return importFile(repo, entry, callback); 12 | console.error("UNKNOWN TYPE", entry); 13 | } 14 | 15 | function importFile(repo, entry, callback) { 16 | var reader = new FileReader(); 17 | reader.onloadend = function() { 18 | repo.saveAs("blob", this.result, function (err, hash) { 19 | if (err) return callback(err); 20 | callback(null, hash, entry.name); 21 | }); 22 | }; 23 | entry.file(function (file) { 24 | reader.readAsArrayBuffer(file); 25 | }); 26 | } 27 | 28 | // Import a tree and callback the root hash. 29 | function importDirectory(repo, dirEntry, callback) { 30 | var reader = dirEntry.createReader(); 31 | 32 | // Build a tree 33 | var tree = []; 34 | var treeOffset = 0; 35 | var item; 36 | 37 | // Loop over result chunks 38 | var entries; 39 | var length; 40 | var index; 41 | return reader.readEntries(onEntries, onError); 42 | 43 | function onEntries(results) { 44 | length = results.length; 45 | if (!length) { 46 | return repo.saveAs("tree", tree, function (err, hash) { 47 | if (err) return callback(err); 48 | callback(null, hash, dirEntry.name); 49 | }); 50 | } 51 | entries = results; 52 | index = 0; 53 | loadNext(); 54 | } 55 | 56 | function loadNext() { 57 | if (index >= length) { 58 | return reader.readEntries(onEntries, onError); 59 | } 60 | var result = entries[index++]; 61 | if (ignores.indexOf(result.name) >= 0) { 62 | return loadNext(); 63 | } 64 | item = tree[treeOffset++] = { 65 | name: result.name, 66 | mode: result.isDirectory ? modes.tree : modes.blob 67 | }; 68 | importEntry(repo, result, onImport); 69 | } 70 | 71 | function onImport(err, hash) { 72 | if (err) throw err; 73 | item.hash = hash; 74 | loadNext(); 75 | } 76 | 77 | function onError() { 78 | console.log(arguments); 79 | throw new Error("ERROR"); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/data/load-module.js: -------------------------------------------------------------------------------- 1 | /*global defs, modules, self*/ 2 | var mine = require('mine'); 3 | 4 | // Compile cjs formatted code in a web worker and return 5 | module.exports = function (code) { 6 | var js = []; 7 | mine(code).forEach(function (dep) { 8 | var name = dep.name; 9 | if (!(/\.[^\/]+$/.test(name))) name += ".js"; 10 | var def = defs[name]; 11 | if (def.deps.length) throw new Error("Complex deps " + name); 12 | var fn = def.fn; 13 | js.push("defs[" + JSON.stringify(dep.name) + "] = " + fn.toString() + ";"); 14 | }); 15 | js.push("defs.main = function (module, exports) {\n" + code + "\n};"); 16 | if (js.length) { 17 | js.unshift("var require = _require;"); 18 | js.unshift(_require.toString()); 19 | js.unshift("var defs = {}, modules = {};"); 20 | } 21 | js.push(genRPC.toString()); 22 | js.push("var rpc = genRPC(self, require('main'));"); 23 | js = js.join("\n\n"); 24 | 25 | var blob = new Blob([js], { type: "application/javascript" }); 26 | var blobURL = window.URL.createObjectURL(blob); 27 | var worker = new Worker(blobURL); 28 | return genRPC(worker); 29 | }; 30 | 31 | function _require(name) { 32 | if (name in modules) return modules[name]; 33 | if (!(name in defs)) throw new Error("Invalid require " + name); 34 | var exports = {}; 35 | var module = modules[name] = { exports: exports }; 36 | defs[name](module, exports); 37 | modules[name] = module.exports; 38 | return module.exports; 39 | } 40 | 41 | // mini RPC system to communicate with the worker. 42 | // This code is shared by both sides. 43 | // Serialized functions don't support return values or `this`, use callbacks. 44 | function genRPC(worker, main) { 45 | var nextId = 1; 46 | var functions = []; 47 | var callbacks = {}; 48 | // var me = self === worker ? "worker" : "master"; 49 | 50 | worker.onmessage = onmessage; 51 | 52 | return function request() { 53 | send(0, arguments); 54 | }; 55 | 56 | function send(id, args) { 57 | var transfers = []; 58 | var message = [id, Array.prototype.map.call(args, function (arg, i) { 59 | return freeze(arg, i < args.length - 1, transfers); 60 | }, transfers)]; 61 | // console.log(me + " out " + JSON.stringify(message)); 62 | try { 63 | worker.postMessage(message, transfers); 64 | } 65 | catch (err) { 66 | console.error("Problem posting " + JSON.stringify({ 67 | message: message, 68 | transferLengths: transfers.map(function (item) { return item.byteLength; }) 69 | })); 70 | throw err; 71 | } 72 | } 73 | 74 | // Freeze functions in a message by turning them into numbered tokens 75 | function freeze(value, permanent, transfers) { 76 | var type = typeof value; 77 | if (type === "function") { 78 | if (permanent) { 79 | var index = functions.indexOf(value); 80 | if (index < 0) { 81 | index = functions.length; 82 | functions.push(value); 83 | } 84 | return {$: -1 - index}; 85 | } 86 | var id = (nextId++).toString(36); 87 | callbacks[id] = value; 88 | return {$:id}; 89 | } 90 | if (Array.isArray(value)) { 91 | return value.map(freeze); 92 | } 93 | if (value && type === "object") { 94 | if (value instanceof Error) { 95 | return {$e: value.toString()}; 96 | } 97 | if (value.constructor.name === "Uint8Array") { 98 | if (value.length) transfers.push(value.buffer); 99 | return value; 100 | } 101 | var object = {}; 102 | for (var key in value) { 103 | object[key] = freeze(value[key], false, transfers); 104 | } 105 | return object; 106 | } 107 | return value; 108 | } 109 | 110 | function onmessage(evt) { 111 | // console.log(me + " in " + JSON.stringify(evt.data)); 112 | var id = parseInt(evt.data[0], 36); 113 | var args = thaw(evt.data[1]); 114 | var fn; 115 | if (id < 0) fn = functions[-1 - id]; 116 | else if (id > 0) { 117 | fn = callbacks[evt.data[0]]; 118 | delete callbacks[id]; 119 | } 120 | else fn = main; 121 | if (!fn) throw new Error("Missing callback " + evt.data[0]); 122 | fn.apply(null, args); 123 | } 124 | 125 | // Turn numbered tokens into proxy functions that call the remote side 126 | function thaw(value) { 127 | if (Array.isArray(value)) { 128 | return value.map(thaw); 129 | } 130 | if (value && typeof value === "object") { 131 | if (value.constructor.name === "Uint8Array") { 132 | return value; 133 | } 134 | if (value.$e) { 135 | return new Error(value.$e); 136 | } 137 | if (value.$) { 138 | return proxy(value.$); 139 | } 140 | var object = {}; 141 | for (var name in value) { 142 | object[name] = thaw(value[name]); 143 | } 144 | return object; 145 | } 146 | return value; 147 | } 148 | 149 | function proxy(id) { 150 | return function proxyFunction() { 151 | send(id, arguments); 152 | }; 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /src/data/publisher.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var binary = require('bodec'); 4 | var pathJoin = require('pathjoin'); 5 | var loadModule = require('./load-module'); 6 | var modes = require('js-git/lib/modes'); 7 | var sha1 = require('git-sha1'); 8 | 9 | // readPath accepts a path and outputs {mode,hash,root,[mime],fetch} 10 | module.exports = function (readPath, settings) { 11 | 12 | var codeHashes = {}; 13 | var filters = {}; 14 | 15 | 16 | return servePath; 17 | 18 | function servePath(path, callback) { 19 | if (!callback) return servePath.bind(null, path); 20 | // console.log("servePath", path); 21 | return readPath(path, bake, callback); 22 | } 23 | 24 | function bake(req, callback) { 25 | req.ruleHash = sha1(JSON.stringify(req)); 26 | // console.log("BAKE", { 27 | // req: req, 28 | // settings: settings 29 | // }); 30 | if (!settings.filters) { 31 | // TODO: serve rule file as static file. 32 | return callback(null, {mode:modes.file,hash:"TODO:servefile",fetch:function (callback) { 33 | callback(null, binary.fromUnicode("TODO:servefile")); 34 | }}); 35 | } 36 | 37 | var codeHash; 38 | var codePath = pathJoin(settings.filters, req.program + ".js"); 39 | return servePath(codePath, onCodeEntry); 40 | 41 | function onCodeEntry(err, entry) { 42 | if (err) return callback(err); 43 | if (!entry.hash) return callback(new Error("Missing filter " + req.name)); 44 | req.codeHash = codeHash = entry.hash; 45 | // If the code hasn't changed, reuse the existing compiled worker. 46 | if (codeHashes[req.program] === codeHash) { 47 | return filters[req.program](servePath, req, callback); 48 | } 49 | return entry.fetch(onCode); 50 | } 51 | 52 | function onCode(err, blob) { 53 | if (err) return callback(err); 54 | var code; 55 | try { code = binary.toUnicode(blob); } 56 | catch (err) { return callback(err); } 57 | console.log("Compiling filter " + req.program); 58 | var module = loadModule(code); 59 | if (typeof module !== "function") { 60 | return callback(new Error(req.program + " exports was not a function")); 61 | } 62 | filters[req.program] = module; 63 | codeHashes[req.program] = codeHash; 64 | filters[req.program](servePath, req, callback); 65 | } 66 | } 67 | 68 | }; 69 | -------------------------------------------------------------------------------- /src/data/rescape.js: -------------------------------------------------------------------------------- 1 | module.exports = rescape; 2 | 3 | // Escape a string for inclusion in a regular expression. 4 | function rescape(string) { 5 | return string.replace(/([.?*+^$[\]\\(){}|])/g, "\\$1") ; 6 | } 7 | -------------------------------------------------------------------------------- /src/main-chrome.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require('carallel')([ 4 | // Initialize the subsystems in parallel for fast boot 5 | require('prefs').init, 6 | require('js-git/mixins/indexed-db').init 7 | ], function (err) { 8 | if (err) throw err; 9 | // Load the main GUI components 10 | require('ui/editor'); 11 | require('ui/slider'); 12 | require('ui/global-keys'); 13 | }); 14 | -------------------------------------------------------------------------------- /src/main-fxos.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var backends = require('backends'); 3 | 4 | // Initialize the subsystems in parallel for fast boot 5 | var setup = backends.map(function (backend) { 6 | return backend.init; 7 | }).filter(Boolean); 8 | 9 | require('carallel')(setup, function (err) { 10 | if (err) throw err; 11 | // Load the main GUI components 12 | require('ui/editor'); 13 | require('ui/slider'); 14 | require('ui/global-keys'); 15 | }); 16 | -------------------------------------------------------------------------------- /src/main-web.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var backends = require('./backends.js'); 3 | 4 | // Initialize the subsystems in parallel for fast boot 5 | var setup = backends.map(function (backend) { 6 | return backend.init; 7 | }).filter(Boolean); 8 | 9 | require('carallel')(setup, function (err) { 10 | if (err) throw err; 11 | // Load the main GUI components 12 | require('./ui/editor.js'); 13 | require('./ui/slider.js'); 14 | require('./ui/global-keys.js'); 15 | }); 16 | 17 | // Reload the page when appcache detects an update. 18 | window.addEventListener("load", function () { 19 | window.applicationCache.addEventListener('updateready', function() { 20 | if (window.applicationCache.status == window.applicationCache.UPDATEREADY) { 21 | window.location.reload(); 22 | } 23 | }, false); 24 | }, false); 25 | -------------------------------------------------------------------------------- /src/prefs-chrome.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /*global chrome*/ 3 | 4 | var defer = require('js-git/lib/defer'); 5 | var storage = chrome.storage.local; 6 | var prefs; 7 | var dirty = false; 8 | var saving = false; 9 | module.exports = { init: init, get: get, set: set, save: save, clearSync: clearSync }; 10 | 11 | function init(callback) { 12 | storage.get("prefs", function (items) { 13 | prefs = items.prefs || {}; 14 | // This is deferred to workaround chrome error reporting issues. 15 | defer(callback); 16 | }); 17 | } 18 | 19 | function get(name, fallback) { 20 | if (name in prefs) return prefs[name]; 21 | prefs[name] = fallback; 22 | return fallback; 23 | } 24 | 25 | function set(name, value) { 26 | // console.log(name, value); 27 | if (typeof value !== "object" && prefs[name] === value) return; 28 | prefs[name] = value; 29 | save(); 30 | return value; 31 | } 32 | 33 | function save() { 34 | if (!saving) { 35 | saving = true; 36 | storage.set({prefs:prefs}, onSave); 37 | } 38 | else dirty = true; 39 | } 40 | 41 | function onSave() { 42 | if (chrome.runtime.lastError) console.error(chrome.runtime.lastError.message); 43 | setTimeout(reset, 1000); 44 | } 45 | 46 | function reset() { 47 | saving = false; 48 | if (!dirty) return; 49 | dirty = false; 50 | save(); 51 | } 52 | 53 | function clearSync(names, callback) { 54 | names.forEach(function (name) { 55 | console.warn("Clearing", name); 56 | delete prefs[name]; 57 | }); 58 | storage.set({prefs:prefs}, callback); 59 | } -------------------------------------------------------------------------------- /src/prefs.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var json = localStorage.getItem("prefs"); 4 | var prefs; 5 | try { 6 | prefs = json && JSON.parse(json); 7 | } 8 | catch (err) { 9 | } 10 | prefs = prefs || {}; 11 | 12 | module.exports = { get: get, set: set, save: save, clearSync: clearSync }; 13 | 14 | function get(name, fallback) { 15 | return prefs[name] || (prefs[name] = fallback); 16 | } 17 | 18 | function set(name, value) { 19 | prefs[name] = value; 20 | save(); 21 | return value; 22 | } 23 | 24 | function save() { 25 | localStorage.setItem("prefs", JSON.stringify(prefs)); 26 | } 27 | 28 | function clearSync(names, callback) { 29 | localStorage.clear(); 30 | callback(); 31 | } -------------------------------------------------------------------------------- /src/runtimes-atom.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('runtimes/local-export'), 3 | require('runtimes/local-exec'), 4 | require('runtimes/local-eval'), 5 | require('runtimes/http-server-atom'), 6 | ]; 7 | -------------------------------------------------------------------------------- /src/runtimes-chrome.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('runtimes/local-export'), 3 | require('runtimes/local-exec'), 4 | require('runtimes/local-eval'), 5 | require('runtimes/file-export'), 6 | require('runtimes/http-server'), 7 | ]; 8 | -------------------------------------------------------------------------------- /src/runtimes-fxos.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('runtimes/local-export'), 3 | require('runtimes/local-exec') 4 | ]; 5 | -------------------------------------------------------------------------------- /src/runtimes.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('runtimes/local-export'), 3 | require('runtimes/local-exec'), 4 | require('runtimes/local-eval'), 5 | ]; 6 | -------------------------------------------------------------------------------- /src/runtimes/edit-hook.js: -------------------------------------------------------------------------------- 1 | var fs = require('data/fs'); 2 | var prefs = require('prefs'); 3 | var hookConfigs = prefs.get("hookConfigs", {}); 4 | var hooks = require('data/hooks'); 5 | 6 | module.exports = editHook; 7 | function editHook(row, dialogFn, action) { 8 | row.call(fs.readEntry, function (entry) { 9 | var config = hookConfigs[row.path] || { 10 | entry: prefs.get("defaultExportEntry"), 11 | source: row.path, 12 | port: 8080, 13 | filters: entry.root + "/filters", 14 | name: row.path.substring(row.path.lastIndexOf("/") + 1) 15 | }; 16 | dialogFn(config, function (settings) { 17 | if (!settings) return; 18 | hookConfigs[row.path] = settings; 19 | if (settings.entry) prefs.set("defaultExportEntry", settings.entry); 20 | hooks[row.path] = action(row, settings); 21 | prefs.save(); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /src/runtimes/file-export.js: -------------------------------------------------------------------------------- 1 | /*global chrome*/ 2 | "use strict"; 3 | 4 | var dialog = require('ui/dialog'); 5 | var fileSystem = chrome.fileSystem; 6 | var readPath = require('data/fs').readPath; 7 | var publisher = require('data/publisher'); 8 | var notify = require('ui/notify'); 9 | var modes = require('js-git/lib/modes'); 10 | var editHook = require('./edit-hook'); 11 | 12 | var memory = {}; 13 | 14 | exports.menuItem = { 15 | icon: "hdd", 16 | label: "Live Export to Disk", 17 | action: pushExport 18 | }; 19 | 20 | function pushExport(row) { 21 | editHook(row, exportConfigDialog, addExportHook); 22 | } 23 | 24 | 25 | function addExportHook(row, settings) { 26 | var rootEntry; 27 | var servePath = publisher(readPath, settings); 28 | var dirty = null; 29 | row.pulse++; 30 | fileSystem.restoreEntry(settings.entry, function (entry) { 31 | if (!entry) row.fail(new Error("Failed to restore entry")); 32 | rootEntry = entry; 33 | row.pulse--; 34 | hook(); 35 | }); 36 | 37 | return hook; 38 | 39 | function hook() { 40 | if (!rootEntry) return; 41 | // If it's busy doing an export, put the row in the dirty queue 42 | if (queue) { 43 | dirty = row; 44 | return; 45 | } 46 | row.exportPath = rootEntry.fullPath + "/" + settings.name; 47 | 48 | // Mark the process as busy 49 | row.pulse++; 50 | queue = []; 51 | 52 | pending = 0; 53 | enqueue(exportPath, settings.source, settings.name, rootEntry); 54 | onSuccess(); 55 | } 56 | 57 | function enqueue(fn) { 58 | var args = Array.prototype.slice.call(arguments, 1); 59 | queue.push({fn:fn,args:args}); 60 | } 61 | 62 | var queue, checking, pending, sync; 63 | function onSuccess() { 64 | if (checking) { 65 | sync = true; 66 | return; 67 | } 68 | checking = true; 69 | while (queue.length) { 70 | var next = queue.shift(); 71 | sync = false; 72 | next.fn.apply(null, next.args); 73 | if (!sync) break; 74 | } 75 | checking = false; 76 | if (!pending) onDone(); 77 | } 78 | 79 | function onDone() { 80 | row.pulse--; 81 | queue = null; 82 | notify("Finished Export to " + rootEntry.fullPath + "/" + settings.name); 83 | // If there was a pending request, run it now. 84 | if (dirty) { 85 | var newrow = dirty; 86 | dirty = null; 87 | hook(newrow); 88 | } 89 | } 90 | 91 | function onError(err) { 92 | row.pulse--; 93 | notify("Export Failed"); 94 | row.fail(err); 95 | } 96 | 97 | function exportPath(path, name, dir) { 98 | pending++; 99 | return servePath(path, onEntry); 100 | 101 | function onEntry(err, entry) { 102 | pending--; 103 | if (!entry) return onError(err || new Error("Can't find " + path)); 104 | return exportEntry(path, name, dir, entry); 105 | } 106 | } 107 | 108 | function exportEntry(path, name, dir, entry) { 109 | var hash = memory[path]; 110 | // Always walk trees because there might be symlinks under them that point 111 | // to changed content without the tree's content actually changing. 112 | if (entry.mode === modes.tree) return exportTree(path, name, dir, entry); 113 | // If the hashes match, it means we've already exported this version of this path. 114 | if (hash && entry.hash === hash) { 115 | // console.log("Skipping", path, hash); 116 | return onSuccess(); 117 | } 118 | notify("Exporting " + path + "..."); 119 | // console.log("Exporting", path, hash); 120 | 121 | // Mark this as being saved. 122 | memory[path] = entry.hash; 123 | pending++; 124 | entry.fetch(onBody); 125 | 126 | function onBody(err, body) { 127 | pending--; 128 | if (body === undefined) return onError(err || new Error("Problem fetching response body")); 129 | return exportFile(name, dir, body); 130 | } 131 | } 132 | 133 | function exportTree(path, name, dir, entry) { 134 | // Create the directoy 135 | pending++; 136 | entry.fetch(function (err, tree) { 137 | if (err) return onError(err); 138 | dir.getDirectory(name, {create: true}, onDir, onError); 139 | function onDir(dirEntry) { 140 | pending--; 141 | // Export it's children. 142 | exportChildren(path, tree, dirEntry); 143 | } 144 | }); 145 | 146 | } 147 | 148 | function exportChildren(base, tree, dir) { 149 | Object.keys(tree).forEach(function (name) { 150 | enqueue(exportPath, base + "/" + name, name, dir); 151 | }); 152 | onSuccess(); 153 | } 154 | 155 | function exportFile(name, dir, body) { 156 | // Flag for onWriteEnd to know state 157 | var truncated = false; 158 | 159 | // Create the file 160 | pending++; 161 | dir.getFile(name, {create:true}, onFile, onError); 162 | 163 | // Create a writer for the file 164 | function onFile(file) { 165 | file.createWriter(onWriter, onError); 166 | } 167 | 168 | // Setup the writer and start the write 169 | function onWriter(fileWriter) { 170 | fileWriter.onwriteend = onWriteEnd; 171 | fileWriter.onerror = onError; 172 | fileWriter.write(new Blob([body])); 173 | } 174 | 175 | // This gets called twice. The first calls truncate and then comes back. 176 | function onWriteEnd() { 177 | if (truncated) { 178 | pending--; 179 | return onSuccess(); 180 | } 181 | truncated = true; 182 | // Trim any extra data leftover from a previous version of the file. 183 | this.truncate(this.position); 184 | } 185 | } 186 | } 187 | 188 | 189 | function exportConfigDialog(config, callback) { 190 | var entry; 191 | var $ = dialog("Export Config", [ 192 | ["form", {onsubmit: submit}, 193 | ["label", {"for": "target"}, "Target Parent Folder"], 194 | [".input", 195 | ["input.input-field$target", { 196 | name: "target", 197 | onclick: chooseFolder, 198 | onkeyup: reset, 199 | required: true 200 | }], 201 | ["button.input-item", {onclick: chooseFolder}, "Choose..."] 202 | ], 203 | ["label", {"for": "name"}, "Target Name"], 204 | [".input", 205 | ["input.input-field$name", { 206 | name: "name", 207 | value: config.name, 208 | required: true 209 | }], 210 | ], 211 | ["label", {"for": "source"}, "Source Path"], 212 | [".input", 213 | ["input.input-field$source", { 214 | name: "source", 215 | value: config.source, 216 | required: true 217 | }], 218 | ], 219 | ["label", {"for": "filters"}, "Filters Path"], 220 | [".input", 221 | ["input.input-field$filters", { 222 | name: "filters", 223 | value: config.filters, 224 | }], 225 | ["input.input-item$submit", {type:"submit",value:"OK"}] 226 | ] 227 | ] 228 | ], onCancel); 229 | 230 | if (config.entry) { 231 | return fileSystem.isRestorable(config.entry, onCheck); 232 | } 233 | return reset(); 234 | 235 | function onCheck(isRestorable) { 236 | if (!isRestorable) { 237 | delete config.entry; 238 | return reset(); 239 | } 240 | return fileSystem.restoreEntry(config.entry, onEntry); 241 | } 242 | 243 | function onEntry(result) { 244 | if (result) entry = result; 245 | return reset(); 246 | } 247 | 248 | function onCancel(evt) { 249 | nullify(evt); 250 | $.close(); 251 | callback(); 252 | } 253 | 254 | function reset() { 255 | $.target.value = entry && entry.fullPath || ""; 256 | } 257 | 258 | function submit(evt) { 259 | nullify(evt); 260 | 261 | config.source = $.source.value; 262 | config.name = $.name.value; 263 | config.filters = $.filters.value; 264 | config.entry = fileSystem.retainEntry(entry); 265 | $.close(); 266 | callback(config); 267 | } 268 | 269 | function chooseFolder(evt) { 270 | nullify(evt); 271 | return fileSystem.chooseEntry({ type: "openDirectory"}, onEntry); 272 | } 273 | 274 | } 275 | 276 | function nullify(evt) { 277 | if (!evt) return; 278 | evt.stopPropagation(); 279 | evt.preventDefault(); 280 | } 281 | -------------------------------------------------------------------------------- /src/runtimes/http-server-atom.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var editHook = require('./edit-hook'); 3 | var dialog = require('ui/dialog'); 4 | var readPath = require('data/fs').readPath; 5 | var publisher = require('data/publisher'); 6 | var notify = require('ui/notify'); 7 | var codec = require('http-codec').server; 8 | var bodec = require('bodec'); 9 | var getMime = require('simple-mime')("text/plain"); 10 | var pathJoin = require('pathjoin'); 11 | var modes = require('js-git/lib/modes'); 12 | 13 | var remote = require('remote'); 14 | var net = remote.require('net'); 15 | 16 | exports.menuItem = { 17 | icon: "globe", 18 | label: "Serve Over HTTP", 19 | action: pullServe 20 | }; 21 | 22 | function pullServe(row) { 23 | editHook(row, serveConfigDialog, addServeHook); 24 | } 25 | 26 | function nullify(evt) { 27 | if (!evt) return; 28 | evt.stopPropagation(); 29 | evt.preventDefault(); 30 | } 31 | 32 | function serveConfigDialog(config, callback) { 33 | var $ = dialog("Serve Config", [ 34 | ["form", {onsubmit: submit}, 35 | ["label", {"for": "port"}, "Local HTTP Port"], 36 | [".input", 37 | ["input.input-field$port", { 38 | name: "port", 39 | value: config.port, 40 | required: true 41 | }], 42 | ["input.input-item$public", { 43 | type: "checkbox", 44 | name: "public", 45 | checked: !!config.public, 46 | title: "Make this available to others on your local network?" 47 | }], 48 | ], 49 | ["label", {"for": "source"}, "Source Path"], 50 | [".input", 51 | ["input.input-field$source", { 52 | name: "source", 53 | value: config.source, 54 | required: true 55 | }], 56 | ], 57 | ["label", {"for": "filters"}, "Filters Path"], 58 | [".input", 59 | ["input.input-field$filters", { 60 | name: "filters", 61 | value: config.filters, 62 | }], 63 | ["input.input-item$submit", {type:"submit",value:"OK"}] 64 | ] 65 | ] 66 | ], onCancel); 67 | 68 | function onCancel(evt) { 69 | nullify(evt); 70 | $.close(); 71 | callback(); 72 | } 73 | 74 | function submit(evt) { 75 | nullify(evt); 76 | 77 | config.port = parseInt($.port.value, 10); 78 | config.source = $.source.value; 79 | config.public = $.public.checked; 80 | config.filters = $.filters.value; 81 | $.close(); 82 | callback(config); 83 | } 84 | 85 | } 86 | 87 | 88 | function addServeHook(row, settings) { 89 | var rootHash; 90 | var servePath = publisher(readPath, settings); 91 | 92 | var server = net.createServer(onConnection); 93 | var ip = settings.public ? "0.0.0.0" : "127.0.0.1"; 94 | server.listen(settings.port, ip, onListen); 95 | 96 | return hook; 97 | 98 | // TODO: add proper error checking all over this file 99 | 100 | function onListen() { 101 | 102 | // Look up the local port to show the user 103 | var info = server.address(); 104 | var address = info.address === "0.0.0.0" ? "localhost" : info.address; 105 | notify("Local Server at http://" + address + ":" + info.port + "/"); 106 | row.serverPort = info.port; 107 | } 108 | 109 | function onConnection(client) { 110 | 111 | var decode = codec.decoder(onItem); 112 | var encode = codec.encoder(onOut); 113 | 114 | notify("TCP connection from " + client.remoteAddress + ":" + client.remotePort); 115 | client.on("data", onData); 116 | 117 | function onData(data) { 118 | decode(new Uint8Array(data)); 119 | } 120 | 121 | function onOut(data) { 122 | if (data) { 123 | var raw = bodec.toRaw(data); 124 | client.write(raw, "binary"); 125 | } 126 | else { 127 | client.end(); 128 | } 129 | } 130 | 131 | function onItem(item) { 132 | if (!item.method) return; // TODO: handle request bodies 133 | 134 | // Ensure the request is either HEAD or GET by rejecting everything else 135 | var head = item.method === "HEAD"; 136 | if (!head && item.method !== "GET") { 137 | return respond(405, [ 138 | ["Allow", "HEAD,GET"] 139 | ], ""); 140 | } 141 | 142 | var pathname = item.path.split("?")[0]; 143 | 144 | // Normalize the path to work with publisher system 145 | var path = pathJoin(settings.source, pathname); 146 | 147 | // Put headers in lowercased object for quick access 148 | var headers = {}; 149 | item.headers.forEach(function (pair) { 150 | headers[pair[0].toLowerCase()] = pair[1]; 151 | }); 152 | 153 | var etag = headers['if-none-match']; 154 | serve(); 155 | 156 | function serve() { 157 | row.pulse++; 158 | servePath(path, function (err, result) { 159 | row.pulse--; 160 | try { onServe(err, result); } 161 | catch (err) { row.fail(err); } 162 | }); 163 | } 164 | 165 | function onServe(err, result) { 166 | 167 | if (err) return error(err); 168 | 169 | if (!(result && result.hash)) { 170 | return respond(404, [], pathname + " not found"); 171 | } 172 | 173 | if (result.hash && result.hash === etag) { 174 | // etag matches, no change 175 | return respond(304, [ 176 | ["Etag", result.hash] 177 | ], ""); 178 | } 179 | 180 | if (result.mode === modes.tree) { 181 | // Tell the browser to redirect if they forgot the trailing slash on a tree. 182 | if (pathname[pathname.length - 1] !== "/") { 183 | return respond(301, [ 184 | ["Location", pathname + "/"] 185 | ], ""); 186 | } 187 | return result.fetch(function (err, tree) { 188 | if (err) return error(err); 189 | // Do an internal redirect if an index.html exists 190 | if (tree["index.html"]) { 191 | path += "/index.html"; 192 | return serve(); 193 | } 194 | var accept = headers.accept; 195 | if (/text\/html/.test(accept)) { 196 | return respond(200, [ 197 | ["Etag", result.hash], 198 | ["Content-Type", "text/html; charset=utf-8"] 199 | ], formatTree(tree) + "\n"); 200 | } 201 | // Otherwise send the raw JSON 202 | return respond(200, [ 203 | ["Etag", result.hash], 204 | ["Content-Type", "application/json; charset=utf-8"] 205 | ], JSON.stringify(tree) + "\n"); 206 | }); 207 | } 208 | 209 | result.fetch(function (err, body) { 210 | if (err) return error(err); 211 | var resHeaders = [ 212 | ["Etag", result.hash], 213 | ["Content-Type", result.mime || getMime(path)] 214 | ]; 215 | body = new Uint8Array(body); 216 | respond(200, resHeaders, body); 217 | }); 218 | } 219 | 220 | function error(err) { 221 | respond(500, [], err.stack); 222 | row.fail(err); 223 | } 224 | 225 | 226 | function respond(code, headers, body) { 227 | // Log the request 228 | notify(item.method + " " + pathname + " " + code); 229 | 230 | if (typeof body === "string") body = bodec.fromUnicode(body); 231 | var contentType, contentLength; 232 | headers.forEach(function (pair) { 233 | var key = pair[0].toLowerCase(); 234 | if (key === "content-type") contentType = pair[1]; 235 | else if (key === "content-length") contentLength = pair[1]; 236 | }); 237 | if (typeof body === "string") { 238 | body = bodec.fromUnicode(body); 239 | } 240 | if (!contentType) headers.push(["Content-Type", "text/plain; charset=utf-8"]); 241 | if (!contentLength) headers.push(["Content-Length", body.length]); 242 | encode({ 243 | code: code, 244 | headers: headers 245 | }); 246 | encode(body); 247 | encode(); 248 | } 249 | } 250 | } 251 | 252 | function hook(newHash) { 253 | if (newHash === rootHash) return; 254 | rootHash = newHash; 255 | if (server) return; 256 | // TODO: maybe invalidate some caches if needed? 257 | } 258 | 259 | } 260 | 261 | function formatTree(tree) { 262 | return "
    \n " + Object.keys(tree).map(function (name) { 263 | var escaped = name.replace(/&/g, '&') 264 | .replace(/"/g, '"') 265 | .replace(//g, '>'); 267 | var entry = tree[name]; 268 | return '
  • ' + escaped + "" + 269 | " - " + entry.mode.toString(8) + 270 | " - " + entry.hash + "
  • "; 271 | }).join("\n ") + "\n
"; 272 | } 273 | 274 | function noop() {} 275 | -------------------------------------------------------------------------------- /src/runtimes/http-server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var editHook = require('./edit-hook'); 3 | var dialog = require('ui/dialog'); 4 | var readPath = require('data/fs').readPath; 5 | var publisher = require('data/publisher'); 6 | var notify = require('ui/notify'); 7 | var codec = require('http-codec').server; 8 | var tcpServer = window.chrome.sockets.tcpServer; 9 | var tcp = window.chrome.sockets.tcp; 10 | var bodec = require('bodec'); 11 | var getMime = require('simple-mime')("text/plain"); 12 | var pathJoin = require('pathjoin'); 13 | var modes = require('js-git/lib/modes'); 14 | 15 | exports.menuItem = { 16 | icon: "globe", 17 | label: "Serve Over HTTP", 18 | action: pullServe 19 | }; 20 | 21 | function pullServe(row) { 22 | editHook(row, serveConfigDialog, addServeHook); 23 | } 24 | 25 | function nullify(evt) { 26 | if (!evt) return; 27 | evt.stopPropagation(); 28 | evt.preventDefault(); 29 | } 30 | 31 | function serveConfigDialog(config, callback) { 32 | var $ = dialog("Serve Config", [ 33 | ["form", {onsubmit: submit}, 34 | ["label", {"for": "port"}, "Local HTTP Port"], 35 | [".input", 36 | ["input.input-field$port", { 37 | name: "port", 38 | value: config.port, 39 | required: true 40 | }], 41 | ["input.input-item$public", { 42 | type: "checkbox", 43 | name: "public", 44 | checked: !!config.public, 45 | title: "Make this available to others on your local network?" 46 | }], 47 | ], 48 | ["label", {"for": "source"}, "Source Path"], 49 | [".input", 50 | ["input.input-field$source", { 51 | name: "source", 52 | value: config.source, 53 | required: true 54 | }], 55 | ], 56 | ["label", {"for": "filters"}, "Filters Path"], 57 | [".input", 58 | ["input.input-field$filters", { 59 | name: "filters", 60 | value: config.filters, 61 | }], 62 | ["input.input-item$submit", {type:"submit",value:"OK"}] 63 | ] 64 | ] 65 | ], onCancel); 66 | 67 | function onCancel(evt) { 68 | nullify(evt); 69 | $.close(); 70 | callback(); 71 | } 72 | 73 | function submit(evt) { 74 | nullify(evt); 75 | 76 | config.port = parseInt($.port.value, 10); 77 | config.source = $.source.value; 78 | config.public = $.public.checked; 79 | config.filters = $.filters.value; 80 | $.close(); 81 | callback(config); 82 | } 83 | 84 | } 85 | 86 | 87 | function addServeHook(row, settings) { 88 | var serverId, rootHash; 89 | var servePath = publisher(readPath, settings); 90 | 91 | tcpServer.create({}, onCreate); 92 | 93 | return hook; 94 | 95 | function onCreate(createInfo) { 96 | serverId = createInfo.socketId; 97 | var ip = settings.public ? "0.0.0.0" : "127.0.0.1"; 98 | tcpServer.listen(serverId, ip, settings.port, onListen); 99 | } 100 | 101 | // TODO: add proper error checking all over this file 102 | 103 | function onListen(result) { 104 | if (result < 0) console.warn("Negative result to listen", result); 105 | // Look up the local port to show the user 106 | tcpServer.getInfo(serverId, function (info) { 107 | // Show the user a globe icon with port information. 108 | var address = info.localAddress === "0.0.0.0" ? "localhost" : info.localAddress; 109 | notify("Local Server at http://" + address + ":" + info.localPort + "/"); 110 | row.serverPort = info.localPort; 111 | }); 112 | tcpServer.onAccept.addListener(onAccept); 113 | } 114 | 115 | function onAccept(info) { 116 | if (info.socketId !== serverId) return; 117 | 118 | var clientId = info.clientSocketId; 119 | var decode = codec.decoder(onItem); 120 | var encode = codec.encoder(onOut); 121 | 122 | 123 | tcp.getInfo(clientId, function (info) { 124 | notify("TCP connection from " + info.peerAddress + ":" + info.peerPort); 125 | tcp.onReceive.addListener(onReceive); 126 | tcp.setPaused(clientId, false); 127 | }); 128 | 129 | function onReceive(info) { 130 | if (info.socketId !== clientId) return; 131 | decode(new Uint8Array(info.data)); 132 | } 133 | 134 | function onOut(binary) { 135 | if (binary) { 136 | tcp.send(clientId, binary.buffer, noop); 137 | } 138 | else { 139 | tcp.close(clientId, noop); 140 | } 141 | } 142 | 143 | function onItem(item) { 144 | if (!item.method) return; // TODO: handle request bodies 145 | 146 | // Ensure the request is either HEAD or GET by rejecting everything else 147 | var head = item.method === "HEAD"; 148 | if (!head && item.method !== "GET") { 149 | return respond(405, [ 150 | ["Allow", "HEAD,GET"] 151 | ], ""); 152 | } 153 | 154 | var pathname = item.path.split("?")[0]; 155 | 156 | // Normalize the path to work with publisher system 157 | var path = pathJoin(settings.source, pathname); 158 | 159 | // Put headers in lowercased object for quick access 160 | var headers = {}; 161 | item.headers.forEach(function (pair) { 162 | headers[pair[0].toLowerCase()] = pair[1]; 163 | }); 164 | 165 | var etag = headers['if-none-match']; 166 | serve(); 167 | 168 | function serve() { 169 | row.pulse++; 170 | servePath(path, function (err, result) { 171 | row.pulse--; 172 | try { onServe(err, result); } 173 | catch (err) { row.fail(err); } 174 | }); 175 | } 176 | 177 | function onServe(err, result) { 178 | 179 | if (err) return error(err); 180 | 181 | if (!(result && result.hash)) { 182 | return respond(404, [], pathname + " not found"); 183 | } 184 | 185 | if (result.hash && result.hash === etag) { 186 | // etag matches, no change 187 | return respond(304, [ 188 | ["Etag", result.hash] 189 | ], ""); 190 | } 191 | 192 | if (result.mode === modes.tree) { 193 | // Tell the browser to redirect if they forgot the trailing slash on a tree. 194 | if (pathname[pathname.length - 1] !== "/") { 195 | return respond(301, [ 196 | ["Location", pathname + "/"] 197 | ], ""); 198 | } 199 | return result.fetch(function (err, tree) { 200 | if (err) return error(err); 201 | // Do an internal redirect if an index.html exists 202 | if (tree["index.html"]) { 203 | path += "/index.html"; 204 | return serve(); 205 | } 206 | var accept = headers.accept; 207 | if (/text\/html/.test(accept)) { 208 | return respond(200, [ 209 | ["Etag", result.hash], 210 | ["Content-Type", "text/html; charset=utf-8"] 211 | ], formatTree(tree) + "\n"); 212 | } 213 | // Otherwise send the raw JSON 214 | return respond(200, [ 215 | ["Etag", result.hash], 216 | ["Content-Type", "application/json; charset=utf-8"] 217 | ], JSON.stringify(tree) + "\n"); 218 | }); 219 | } 220 | 221 | result.fetch(function (err, body) { 222 | if (err) return error(err); 223 | var resHeaders = [ 224 | ["Etag", result.hash], 225 | ["Content-Type", result.mime || getMime(path)] 226 | ]; 227 | body = new Uint8Array(body); 228 | respond(200, resHeaders, body); 229 | }); 230 | } 231 | 232 | function error(err) { 233 | respond(500, [], err.stack); 234 | row.fail(err); 235 | } 236 | 237 | 238 | function respond(code, headers, body) { 239 | // Log the request 240 | notify(item.method + " " + pathname + " " + code); 241 | 242 | if (typeof body === "string") body = bodec.fromUnicode(body); 243 | var contentType, contentLength; 244 | headers.forEach(function (pair) { 245 | var key = pair[0].toLowerCase(); 246 | if (key === "content-type") contentType = pair[1]; 247 | else if (key === "content-length") contentLength = pair[1]; 248 | }); 249 | if (typeof body === "string") { 250 | body = bodec.fromUnicode(body); 251 | } 252 | if (!contentType) headers.push(["Content-Type", "text/plain; charset=utf-8"]); 253 | if (!contentLength) headers.push(["Content-Length", body.length]); 254 | encode({ 255 | code: code, 256 | headers: headers 257 | }); 258 | encode(body); 259 | encode(); 260 | } 261 | } 262 | } 263 | 264 | function hook(newHash) { 265 | if (newHash === rootHash) return; 266 | rootHash = newHash; 267 | if (!serverId) return; 268 | // TODO: maybe invalidate some caches if needed? 269 | } 270 | 271 | } 272 | 273 | function formatTree(tree) { 274 | return "
    \n " + Object.keys(tree).map(function (name) { 275 | var escaped = name.replace(/&/g, '&') 276 | .replace(/"/g, '"') 277 | .replace(//g, '>'); 279 | var entry = tree[name]; 280 | return '
  • ' + escaped + "" + 281 | " - " + entry.mode.toString(8) + 282 | " - " + entry.hash + "
  • "; 283 | }).join("\n ") + "\n
"; 284 | } 285 | 286 | function noop() {} 287 | -------------------------------------------------------------------------------- /src/runtimes/local-eval.js: -------------------------------------------------------------------------------- 1 | 2 | exports.menuItem = { 3 | icon: "asterisk", 4 | label: "Eval in Main", 5 | action: execFile 6 | }; 7 | 8 | var defer = require('js-git/lib/defer'); 9 | var editor = require('ui/editor'); 10 | var tree = require('ui/tree'); 11 | var mine = require('mine'); 12 | var fs = require('data/fs'); 13 | var pathJoin = require('pathjoin'); 14 | var bodec = require('bodec'); 15 | var run = require('gen-run'); 16 | 17 | function execFile(row) { 18 | 19 | var modules = {}; 20 | var defs = {}; 21 | var loading = {}; 22 | 23 | tree.activateDoc(row, true, function () { 24 | var js = editor.getText(); 25 | var path = "vfs:/" + row.path; 26 | load(path, js, function (err) { 27 | if (err) row.fail(err); 28 | defer(function () { 29 | try { fakeRequire(path); } 30 | catch (err) { row.fail(err); } 31 | }); 32 | }); 33 | }); 34 | 35 | function fakeRequire(path) { 36 | if (path in modules) { 37 | return modules[path].exports; 38 | } 39 | if (path in defs) { 40 | var exports = {}; 41 | var module = modules[path] = { exports: exports }; 42 | var filename = path.substring(5); 43 | var dirname = pathJoin(filename, ".."); 44 | defs[path](run, fakeRequire, module, exports, dirname, filename); 45 | return module.exports; 46 | } 47 | return require(path); 48 | } 49 | 50 | function load(path, js, callback, force) { 51 | if (!force && (defs[path] || modules[path] || loading[path])) return callback(); 52 | loading[path] = true; 53 | if (js === null || js === undefined) { 54 | return fs.readBlob(path.substring(4), function (err, entry) { 55 | if (err || !entry || !entry.hash) { 56 | return callback(err || new Error("Missing file " + path)); 57 | } 58 | return load(path, bodec.toUnicode(entry.blob), callback, true); 59 | }); 60 | } 61 | var left = 1; 62 | mine(js).reverse().forEach(function (dep) { 63 | var len = dep.name.length; 64 | if (!/\.js$/.test(dep.name)) { 65 | dep.name += ".js"; 66 | } 67 | if (dep.name[0] === ".") { 68 | dep.name = pathJoin(path, "..", dep.name); 69 | left++; 70 | load(dep.name, null, check); 71 | } 72 | js = js.substring(0, dep.offset) + dep.name + js.substring(dep.offset + len); 73 | }); 74 | // Allow yield and yield* in any script body! 75 | js = "run(function* () {" + js + "});"; 76 | if (!window.hasGenerators) { 77 | var regenerator = require('tedit-regenerator/regenerator-bundle'); 78 | // var regenerator = require('regenerator'); 79 | js = regenerator(js); 80 | } 81 | var url = URL.createObjectURL(new Blob([ 82 | "window[" + JSON.stringify(path) + "](function (run, require, module, exports, __dirname, __filename) {" + js + "});\n" 83 | ], {type: 'application/javascript'})); 84 | var old = window.path; 85 | if (old) URL.revokeObjectURL(old); 86 | window[path] = function (fn) { 87 | document.head.removeChild(tag); 88 | window[path] = url; 89 | defs[path] = fn; 90 | check(); 91 | }; 92 | var tag = document.createElement('script'); 93 | tag.setAttribute("charset", "utf-8"); 94 | tag.setAttribute("src", url); 95 | document.head.appendChild(tag); 96 | 97 | var done; 98 | 99 | function check(err) { 100 | if (done) return; 101 | if (err) { 102 | done = true; 103 | return callback(err); 104 | } 105 | 106 | if (!--left) { 107 | done = true; 108 | return callback(); 109 | } 110 | } 111 | 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/runtimes/local-exec.js: -------------------------------------------------------------------------------- 1 | var mine = require('mine'); 2 | var fs = require('data/fs'); 3 | var bodec = require('bodec'); 4 | 5 | exports.menuItem = { 6 | icon: "cog", 7 | label: "Run in Worker", 8 | combo: 1, //Control 9 | keyCode: 13, // Enter 10 | action: execFile 11 | }; 12 | 13 | var editor = require('ui/editor'); 14 | var tree = require('ui/tree'); 15 | var config = window.ace.require('ace/config'); 16 | var urls = {}; 17 | 18 | function jsToUrl(path, js) { 19 | var url = urls[path]; 20 | if (url) window.URL.revokeObjectURL(url); 21 | var blob = new Blob([js], { type: "application/javascript" }); 22 | return (urls[path] = window.URL.createObjectURL(blob)); 23 | } 24 | 25 | 26 | var requireJs = "\n" + (function require(url) { 27 | /*global self*/ 28 | "use strict"; 29 | if (!require.cache) { 30 | require.id = 0; 31 | require.callbacks = {}; 32 | self.onmessage = function (evt) { 33 | var message = JSON.parse(evt.data); 34 | var callback = require.callbacks[message.id]; 35 | delete require.callbacks[message.id]; 36 | callback(message.error, message.result); 37 | }; 38 | require.cache = {fs: {exports: { 39 | readFile: function readFile(path, callback) { 40 | if (!callback) return readFile.bind(null, path); 41 | var id = require.id++; 42 | self.postMessage(JSON.stringify({id:id,path:path})); 43 | require.callbacks[id] = callback; 44 | } 45 | }}}; 46 | require.aliases = { 47 | "gen-run": "https://raw.githubusercontent.com/creationix/gen-run/master/run.js", 48 | "bodec": "https://raw.githubusercontent.com/creationix/bodec/master/bodec.js", 49 | }; 50 | } 51 | if (require.aliases[url]) url = require.aliases[url]; 52 | var module = require.cache[url]; 53 | if (module) return module.exports; 54 | module = self.module = require.cache[url] = { exports: {} }; 55 | self.importScripts(url); 56 | return module.exports; 57 | }).toString(); 58 | 59 | 60 | function execFile(row) { 61 | tree.activateDoc(row, true, function () { 62 | var js = editor.getText(); 63 | js += requireJs; 64 | var blobURL = jsToUrl(row.path, js); 65 | var worker = new Worker(blobURL); 66 | worker.onmessage = function (evt) { 67 | var message = JSON.parse(evt.data); 68 | fs.readBlob(message.path, function (error, result) { 69 | if (result && result.blob) result = bodec.toUnicode(result.blob); 70 | worker.postMessage(JSON.stringify({ 71 | id: message.id, 72 | error: error, 73 | result: result 74 | })); 75 | }); 76 | }; 77 | worker.onerror = function (error) { 78 | var row = error.lineno - 1; 79 | var column = error.colno - 1; 80 | editor.moveCursorTo(row, column); 81 | editor.getSession().setAnnotations([{ 82 | row: row, 83 | column: column, 84 | text: error.message, 85 | type: "error" 86 | }]); 87 | config.loadModule("ace/ext/error_marker", function(module) { 88 | module.showErrorMarker(editor, 1); 89 | }); 90 | }; 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /src/runtimes/local-export.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var tree = require('ui/tree'); 4 | var carallel = require('carallel'); 5 | var pathJoin = require('pathjoin'); 6 | var dialog = require('ui/dialog'); 7 | var defer = require('js-git/lib/defer'); 8 | var fs = require('data/fs'); 9 | var readPath = fs.readPath; 10 | var publisher = require('data/publisher'); 11 | var notify = require('ui/notify'); 12 | var modes = require('js-git/lib/modes'); 13 | var editHook = require('./edit-hook'); 14 | 15 | var memory = {}; 16 | 17 | exports.menuItem = { 18 | icon: "folder-open", 19 | label: "Live Export to VFS", 20 | action: pushExport 21 | }; 22 | 23 | function pushExport(row) { 24 | editHook(row, exportConfigDialog, addExportHook); 25 | } 26 | 27 | function addExportHook(row, settings) { 28 | console.warn("addExportHook", row, settings); 29 | var servePath = publisher(readPath, settings); 30 | var dirty = null; 31 | var toWrite = null; 32 | defer(hook); 33 | 34 | return hook; 35 | 36 | function hook() { 37 | // If it's busy doing an export, put the row in the dirty slot 38 | if (toWrite) { 39 | dirty = row; 40 | return; 41 | } 42 | row.exportPath = settings.name; 43 | 44 | // Mark the process as busy 45 | row.pulse++; 46 | toWrite = {}; 47 | var old = fs.configs[""].current; 48 | 49 | doExport(settings.source, settings.name, function (err) { 50 | row.pulse--; 51 | if (err) { 52 | toWrite = null; 53 | notify("Export Failed"); 54 | row.fail(err); 55 | } 56 | else if (old !== fs.configs[""].current) { 57 | notify("Finished Export to " + settings.name); 58 | tree.reload(); 59 | } 60 | 61 | // If there was a pending request, run it now. 62 | if (dirty) { 63 | var newrow = dirty; 64 | dirty = null; 65 | hook(newrow); 66 | } 67 | }); 68 | } 69 | 70 | function doExport(source, target, callback) { 71 | exportPath(source, target, function (err) { 72 | if (err) return callback(err); 73 | var paths = Object.keys(toWrite); 74 | if (!paths.length) return callback(); 75 | 76 | notify("Updating trees..."); 77 | carallel(paths.map(function (path) { 78 | return fs.writeEntry(path, toWrite[path]); 79 | }), callback); 80 | toWrite = null; 81 | 82 | }); 83 | } 84 | 85 | function exportPath(source, target, callback) { 86 | if (!callback) return exportPath.bind(null, source, target); 87 | servePath(source, function (err, entry) { 88 | if (!entry) return callback(err || new Error("Can't find " + source)); 89 | // Always walk trees because there might be symlinks under them that point 90 | // to changed content without the tree's content actually changing. 91 | if (entry.mode === modes.tree) { 92 | return exportTree(source, target, entry, callback); 93 | } 94 | // Skip already exported files 95 | var hash = memory[source]; 96 | if (hash && entry.hash === hash) { 97 | return callback(); 98 | } 99 | exportFile(source, target, entry, callback); 100 | }); 101 | } 102 | 103 | function exportTree(source, target, entry, callback) { 104 | entry.fetch(function (err, tree) { 105 | if (err) return callback(err); 106 | var names = Object.keys(tree); 107 | serial(names.map(function (name) { 108 | return exportPath(pathJoin(source, name), pathJoin(target, name)); 109 | }), 2, callback); 110 | }); 111 | } 112 | 113 | function exportFile(source, target, entry, callback) { 114 | notify("Reading " + source + "..."); 115 | entry.fetch(onBody); 116 | fs.readRepo(target, onRepo); 117 | var body, repo; 118 | function onBody(err, result) { 119 | if (!result) return callback(err || new Error("Missing body")); 120 | body = result; 121 | if (repo) add(); 122 | } 123 | function onRepo(err, result) { 124 | if (!result) return callback(err || new Error("Missing repo")); 125 | repo = result; 126 | if (body) add(); 127 | } 128 | 129 | function add() { 130 | notify("Writing " + target + "..."); 131 | repo.saveAs("blob", body, function (err, hash) { 132 | if (!hash) return callback(err || new Error("Problem saving")); 133 | // record as being saved 134 | memory[source] = entry.hash; 135 | toWrite[target] = { 136 | mode: modes.blob, 137 | hash: hash 138 | }; 139 | callback(); 140 | }); 141 | } 142 | } 143 | 144 | } 145 | 146 | function exportConfigDialog(config, callback) { 147 | var $ = dialog("Export Config", [ 148 | ["form", {onsubmit: submit}, 149 | ["label", {"for": "name"}, "Target Path"], 150 | [".input", 151 | ["input.input-field$name", { 152 | name: "name", 153 | value: config.name, 154 | required: true 155 | }], 156 | ], 157 | ["label", {"for": "source"}, "Source Path"], 158 | [".input", 159 | ["input.input-field$source", { 160 | name: "source", 161 | value: config.source, 162 | required: true 163 | }], 164 | ], 165 | ["label", {"for": "filters"}, "Filters Path"], 166 | [".input", 167 | ["input.input-field$filters", { 168 | name: "filters", 169 | value: config.filters, 170 | }], 171 | ["input.input-item$submit", {type:"submit",value:"OK"}] 172 | ] 173 | ] 174 | ], onCancel); 175 | 176 | 177 | function onCancel(evt) { 178 | nullify(evt); 179 | $.close(); 180 | callback(); 181 | } 182 | 183 | function submit(evt) { 184 | nullify(evt); 185 | 186 | config.source = $.source.value; 187 | config.name = $.name.value; 188 | config.filters = $.filters.value; 189 | $.close(); 190 | callback(config); 191 | } 192 | 193 | } 194 | 195 | function nullify(evt) { 196 | if (!evt) return; 197 | evt.stopPropagation(); 198 | evt.preventDefault(); 199 | } 200 | 201 | function serial(actions, num, callback) { 202 | var i = 0, l = actions.length; 203 | var left = l; 204 | var results = new Array(l); 205 | if (!left) return callback(); 206 | var done = false; 207 | for (var j = 0; j < num; j++) { 208 | check(); 209 | } 210 | 211 | function check() { 212 | if (done) return; 213 | if (i < l) load(i++); 214 | } 215 | 216 | function load(index) { 217 | actions[index](function (err, result) { 218 | results[index] = result; 219 | left--; 220 | if (done) return; 221 | if (err || !left) { 222 | done = true; 223 | return callback(err, result); 224 | } 225 | check(); 226 | }); 227 | } 228 | } -------------------------------------------------------------------------------- /src/ui/applytheme.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /*global ace*/ 3 | 4 | var parseCss = require('css/lib/parse/index'); 5 | 6 | var template = { 7 | ".tree, div#ace_settingsmenu": { 8 | "color": [".ace_text-layer", ""], 9 | "background-color": [".ace_text-layer", ""], 10 | "background": [".ace_text-layer", ""], 11 | "background-image": [".ace_text-layer", ""] 12 | }, 13 | ".tree .icon-attention": { 14 | "color": "#f72;" 15 | }, 16 | ".tree .icon-fork": { 17 | "color": [".ace_type", ".ace_punctuation.ace_operator", ".ace_punctuation", ".ace_keyword"] 18 | }, 19 | ".tree .icon-folder-open, .tree .icon-folder": { 20 | "color": [".ace_comment"] 21 | }, 22 | ".tree .icon-doc": { 23 | "color": [".ace_string"] 24 | }, 25 | ".tree .icon-link": { 26 | "color": [".ace_keyword.ace_operator", ".ace_keyword"] 27 | }, 28 | ".tree .icon-cog": { 29 | "color": [".ace_variable"] 30 | }, 31 | ".tree span": { 32 | "color": [".ace_identifier"] 33 | }, 34 | ".titlebar, body": { 35 | "color": [".ace_gutter"], 36 | "background-color": [".ace_gutter"], 37 | "background": [".ace_gutter"], 38 | "background-image": [".ace_gutter"], 39 | }, 40 | ".titlebar, .dialog .title, .input-item, .dragger": { 41 | "background-color": [".ace_gutter"], 42 | "background": [".ace_gutter"], 43 | "background-image": [".ace_text-layer", ".ace_gutter"], 44 | "color": [""] 45 | }, 46 | ".input-item[type=submit], .input-field[type=submit]": { 47 | "color": [".ace_type", ".ace_punctuation.ace_operator", ".ace_punctuation", ".ace_keyword", ""] 48 | }, 49 | ".input-field": { 50 | "background-color": [""], 51 | "color": [""] 52 | } 53 | 54 | }; 55 | 56 | var tag; 57 | 58 | module.exports = function (theme) { 59 | var aceTheme = ace.require(theme.theme); 60 | if (!theme) return {}; 61 | var cssClass = aceTheme.cssClass; 62 | var prefix = new RegExp("^." + cssClass + " "); 63 | var rules = {}; 64 | try { 65 | var aceCss = aceTheme.cssText; 66 | 67 | parseCss(aceCss).stylesheet.rules.forEach(function (rule) { 68 | if (rule.type !== "rule") return; 69 | var declarations = {}; 70 | rule.declarations.forEach(function (declaration) { 71 | if (declaration.type !== "declaration") return; 72 | declarations[declaration.property] = declaration.value; 73 | }); 74 | rule.selectors.forEach(function (selector) { 75 | if (selector === "." + cssClass) selector += " "; 76 | if (!prefix.test(selector)) return; 77 | var name = selector.replace(prefix, ""); 78 | rules[name] = declarations; 79 | }); 80 | }); 81 | } 82 | catch (err) { 83 | console.log(aceTheme.cssText); 84 | console.error(theme.theme, err.toString()); 85 | return {}; 86 | } 87 | 88 | // console.log(rules); 89 | var css = Object.keys(template).map(function (name) { 90 | var props = template[name]; 91 | var contents = Object.keys(props).map(function (key) { 92 | var values = props[key]; 93 | if (!Array.isArray(values)) return " " + key + ":" + values + ";"; 94 | 95 | for (var i = 0, l = values.length; i < l; i++) { 96 | var option = values[i]; 97 | var rule = rules[option]; 98 | if (rule && rule[key]) { 99 | // console.log("Matched", [name, key, option, rule[key]]); 100 | return " " + key + ":" + rule[key] + ";"; 101 | } 102 | } 103 | if (i === l) { 104 | // console.warn("Not Matched", [name, key]); 105 | return ""; 106 | } 107 | 108 | }).join("\n").trim(); 109 | if (!contents) return; 110 | return name + "{\n " + contents + "\n}"; 111 | }).join("\n"); 112 | 113 | if (tag) document.head.removeChild(tag); 114 | tag = document.createElement("style"); 115 | tag.setAttribute("data-theme", theme.theme); 116 | tag.textContent = css; 117 | document.head.appendChild(tag); 118 | }; 119 | -------------------------------------------------------------------------------- /src/ui/context-menu.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var domBuilder = require('dombuilder'); 4 | 5 | module.exports = ContextMenu; 6 | 7 | function ContextMenu(evt, node, items) { 8 | if (!(this instanceof ContextMenu)) return new ContextMenu(evt, node, items); 9 | var $ = {}; 10 | 11 | var css = { left: evt.pageX + "px" }; 12 | if (evt.pageY < window.innerHeight / 2) { 13 | css.top = evt.pageY + "px"; 14 | } 15 | else { 16 | css.bottom = (window.innerHeight - evt.pageY) + "px"; 17 | } 18 | var attrs = { css: css }; 19 | document.body.appendChild(domBuilder([ 20 | [".shield$shield", {onclick: closeMenu, oncontextmenu: closeMenu}], 21 | ["ul.contextMenu$ul", attrs, items.map(function (item) { 22 | if (item.sep) return ["li.sep", ["hr"]]; 23 | var attrs = {}; 24 | if (item.action) { 25 | attrs.onclick = function (evt) { 26 | closeMenu(evt); 27 | item.action(node); 28 | }; 29 | } 30 | else { 31 | attrs.class = "disabled"; 32 | } 33 | return ["li", attrs, 34 | ["i", {class: "icon-" + item.icon}], 35 | item.label 36 | ]; 37 | })], 38 | ], $)); 39 | 40 | this.close = closeMenu; 41 | function closeMenu(evt) { 42 | evt.preventDefault(); 43 | evt.stopPropagation(); 44 | document.body.removeChild($.ul); 45 | document.body.removeChild($.shield); 46 | $ = null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ui/dialog.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var domBuilder = require('dombuilder'); 4 | 5 | dialog.alert = alertDialog; 6 | dialog.prompt = promptDialog; 7 | dialog.confirm = confirmDialog; 8 | dialog.multiEntry = multiEntryDialog; 9 | 10 | module.exports = dialog; 11 | 12 | function nullify(evt) { 13 | if (!evt) return; 14 | evt.stopPropagation(); 15 | evt.preventDefault(); 16 | } 17 | 18 | function dialog(title, contents, onCancel) { 19 | var $ = { close: closeDialog }; 20 | document.body.appendChild(domBuilder([ 21 | [".shield$shield", {onclick: cancel, oncontextmenu: cancel}], 22 | [".dialog$dialog", 23 | [".title", 24 | [".content", title], 25 | [".closebox",{onclick: cancel}, "×"] 26 | ], 27 | [".body", contents] 28 | ] 29 | ], $)); 30 | $.cancel = cancel; 31 | dialog.close = closeDialog; 32 | return $; 33 | 34 | function cancel(evt) { 35 | nullify(evt); 36 | onCancel(); 37 | } 38 | 39 | function closeDialog() { 40 | delete dialog.close; 41 | document.body.removeChild($.shield); 42 | document.body.removeChild($.dialog); 43 | } 44 | } 45 | 46 | function alertDialog(title, message, callback) { 47 | var $ = dialog(title, ["p", message], onCancel); 48 | return $; 49 | 50 | function onCancel() { 51 | $.close(); 52 | if (callback) callback(); 53 | } 54 | } 55 | 56 | function promptDialog(prompt, value, callback) { 57 | var $ = dialog(prompt, [ 58 | ["form", {onsubmit: submit}, 59 | [".input", 60 | ["input.input-field$input", {value:value,required:true}], 61 | ["input.input-item", {type:"submit",value:"OK"}] 62 | ] 63 | ] 64 | ], onCancel); 65 | $.input.focus(); 66 | return $; 67 | 68 | function onCancel() { 69 | $.close(); 70 | callback(); 71 | } 72 | 73 | function submit(evt) { 74 | nullify(evt); 75 | $.close(); 76 | callback($.input.value); 77 | } 78 | } 79 | 80 | function multiEntryDialog(title, entries, callback) { 81 | var $ = dialog(title, ["form$form", {onsubmit: submit}, 82 | entries.map(function (entry, i) { 83 | var row = [".input", 84 | ["input.input-field", entry], 85 | ]; 86 | if (i === entries.length - 1) { 87 | row.push(["input.input-item", {type:"submit",value:"OK"}]); 88 | } 89 | return row; 90 | }) 91 | ], onCancel); 92 | $.form.elements[0].focus(); 93 | 94 | function onCancel() { 95 | $.close(); 96 | callback(); 97 | } 98 | 99 | function submit(evt) { 100 | nullify(evt); 101 | var result = {}; 102 | entries.forEach(function (entry, i) { 103 | var value = $.form.elements[i].value; 104 | result[entry.name] = value; 105 | }); 106 | $.close(); 107 | callback(result); 108 | } 109 | 110 | } 111 | 112 | function confirmDialog(question, callback) { 113 | var $ = dialog("Confirm", [ 114 | ["p", question], 115 | ["form", {onsubmit: submit}, 116 | [".input", 117 | ["input.input-field$yes", {type:"submit",value:"Yes"}], 118 | ["input.input-item", {type:"button",value:"No",onclick:onCancel}] 119 | ] 120 | ] 121 | ], onCancel); 122 | $.yes.focus(); 123 | return $; 124 | 125 | function onCancel(evt) { 126 | nullify(evt); 127 | $.close(); 128 | callback(); 129 | } 130 | 131 | function submit(evt) { 132 | nullify(evt); 133 | $.close(); 134 | callback(true); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/ui/editor.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | /*global ace*/ 4 | 5 | var domBuilder = require('dombuilder'); 6 | 7 | var notify = require('./notify'); 8 | var $ = require('./elements'); 9 | var prefs = require('prefs'); 10 | var zoom = require('./zoom'); 11 | 12 | ace.require("ace/ext/language_tools"); // Trigger the extension. 13 | var whitespace = ace.require('ace/ext/whitespace'); 14 | var themes = ace.require('ace/ext/themelist').themesByName; 15 | var themeNames = Object.keys(ace.require('ace/ext/themelist').themesByName); 16 | var themeIndex = prefs.get("themeIndex", themeNames.indexOf("idle_fingers")); 17 | 18 | // Put sample content and liven the editor 19 | 20 | var code = jack.toString().substr(20); 21 | code = code.substr(0, code.length - 4); 22 | code = code.split("\n").map(function (line) { return line.substr(2); }).join("\n"); 23 | 24 | var editor = ace.edit($.editor); 25 | editor.setTheme = setTheme; 26 | editor.prevTheme = prevTheme; 27 | editor.nextTheme = nextTheme; 28 | editor.focused = false; 29 | 30 | // Turn on autocompletion 31 | editor.setOptions({ 32 | enableBasicAutocompletion: true, 33 | // enableSnippets: true 34 | }); 35 | 36 | zoom(onZoom); 37 | 38 | 39 | // Use Tab for autocomplete 40 | function shouldComplete(editor) { 41 | if (editor.getSelectedText()) { 42 | return false; 43 | } 44 | var session = editor.getSession(); 45 | var doc = session.getDocument(); 46 | var pos = editor.getCursorPosition(); 47 | 48 | var line = doc.getLine(pos.row); 49 | return ace.require("ace/autocomplete/util").retrievePrecedingIdentifier(line, pos.column); 50 | } 51 | editor.commands.addCommand({ 52 | name: "completeOrIndent", 53 | bindKey: "Tab", 54 | exec: function(editor) { 55 | if (shouldComplete(editor)) { 56 | editor.execCommand("startAutocomplete"); 57 | } else { 58 | editor.indent(); 59 | } 60 | } 61 | }); 62 | 63 | editor.updatePath = function (doc) { 64 | if (doc !== currentDoc) return; 65 | updateTitle(doc.row.path); 66 | }; 67 | 68 | setTheme(themeNames[themeIndex], true); 69 | editor.on("blur", function () { 70 | editor.focused = false; 71 | if (currentDoc && currentDoc.save) save(); 72 | }); 73 | editor.on("focus", function () { 74 | editor.focused = true; 75 | }); 76 | editor.on("change", function () { 77 | if (currentDoc && currentDoc.onChange) currentDoc.onChange(currentDoc.session.getValue()); 78 | }); 79 | editor.commands.addCommand({ 80 | name: 'save', 81 | bindKey: {win: 'Ctrl-S', mac: 'Command-S'}, 82 | exec: function() { 83 | if (currentDoc && currentDoc.save) save(); 84 | }, 85 | readOnly: false 86 | }); 87 | 88 | editor.saveToGit = save; 89 | function save() { 90 | 91 | if (!currentDoc.session) return; 92 | // Trim trailing whitespace. 93 | var doc = currentDoc.session.getDocument(); 94 | var lines = doc.getAllLines(); 95 | for (var i = 0, l = lines.length; i < l; i++) { 96 | var line = lines[i]; 97 | var index = line.search(/\s+$/); 98 | if (index >= 0) doc.removeInLine(i, index, line.length); 99 | } 100 | 101 | // Remove extra trailing blank lines 102 | while (--i >= 0) if (/\S/.test(lines[i])) break; 103 | if (i < l - 2) doc.removeLines(i + 2, l - 1); 104 | 105 | currentDoc.save(currentDoc.session.getValue()); 106 | } 107 | 108 | var textMode = true; 109 | var currentDoc = null; 110 | 111 | var fallback = { 112 | session: ace.createEditSession(code, "ace/mode/jack"), 113 | row: {path: "Tedit"} 114 | }; 115 | whitespace.detectIndentation(fallback.session); 116 | 117 | $.image.addEventListener("click", function (evt) { 118 | evt.stopPropagation(); 119 | evt.preventDefault(); 120 | if (currentDoc) { 121 | currentDoc.tiled = !currentDoc.tiled; 122 | updateImage(); 123 | } 124 | }, false); 125 | 126 | editor.getText = function () { 127 | return currentDoc.session.getValue(); 128 | }; 129 | 130 | editor.setDoc = function (doc) { 131 | if (!doc) doc = fallback; 132 | currentDoc = doc; 133 | 134 | if (doc.session) { 135 | editor.setSession(doc.session); 136 | } 137 | updateTitle(doc.row.path); 138 | 139 | if (doc.url) { 140 | // This is an image url. 141 | if (textMode) { 142 | textMode = false; 143 | $.preview.style.display = "block"; 144 | $.editor.style.display = "none"; 145 | } 146 | return updateImage(); 147 | } 148 | if (!textMode) { 149 | textMode = true; 150 | $.preview.style.display = "none"; 151 | $.editor.style.display = "block"; 152 | } 153 | }; 154 | 155 | function updateTitle(path) { 156 | var index = path.lastIndexOf("/"); 157 | $.titlebar.textContent = ""; 158 | $.titlebar.appendChild(domBuilder([ 159 | ["span.fade", path.substr(0, index + 1)], 160 | ["span", path.substr(index + 1)], 161 | ])); 162 | } 163 | 164 | editor.setDoc(); 165 | 166 | function onZoom(scale) { 167 | editor.setFontSize(16 * scale); 168 | } 169 | 170 | function updateImage() { 171 | var img = currentDoc; 172 | $.image.style.backgroundImage = "url(" + img.url + ")"; 173 | if (img.tiled) $.image.classList.remove("zoom"); 174 | else $.image.classList.add("zoom"); 175 | } 176 | 177 | function setTheme(name, quiet) { 178 | var theme = themes[name]; 179 | document.body.setAttribute("class", "theme-" + (theme.isDark ? "dark" : "light")); 180 | editor.renderer.setTheme(theme.theme, function () { 181 | require('./applytheme')(theme); 182 | if (!quiet) notify(theme.caption); 183 | }); 184 | } 185 | 186 | function nextTheme() { 187 | themeIndex = (themeIndex + 1) % themeNames.length; 188 | prefs.set("themeIndex", themeIndex); 189 | setTheme(themeNames[themeIndex]); 190 | } 191 | 192 | function prevTheme() { 193 | themeIndex = (themeIndex - 1); 194 | if (themeIndex < 0) themeIndex += themeNames.length; 195 | prefs.set("themeIndex", themeIndex); 196 | setTheme(themeNames[themeIndex]); 197 | } 198 | 199 | module.exports = editor; 200 | 201 | }()); 202 | 203 | function jack() {/* 204 | [ "Welcome to the Tedit Workspace" 205 | "This Development Environment is under heavy development" ] 206 | -- This file is written in Jack, a new language for kids! 207 | vars Global-Controls, File-Tree-Controls, Mouse-and-Touch-Controls 208 | 209 | "Right-Click in the area to the left to create a new repo" 210 | 211 | Global-Controls = { 212 | Control-Shift-R: "Reload the app" 213 | Control-Plus: "Increase font size" 214 | Control-Minus: "Decrease font size" 215 | Control-B: "Apply next Theme" 216 | Control-Shift-B: "Apply previous Theme" 217 | Control-E: "Toggle focus between editor and file tree" 218 | Control-N: "Create new file relative to current or selected" 219 | Alt-Tilde: "Switch between recently opened files" 220 | -- If you manually close the tree by dragging, toggle remembers this. 221 | } 222 | 223 | File-Tree-Controls = { 224 | Up: "Move the selection up" 225 | Down: "Move the selection down" 226 | Left: "Close the current folder or move to parent folder" 227 | Right: "Open the current folder or move to first child" 228 | Home: "Jump to top of list" 229 | End: "Jump to end of list" 230 | Page-Up: "Go up 10 times" 231 | Page-Down: "Go down 10 times" 232 | When-on-folder: { 233 | Space-or-Enter: "Toggle folder open and close" 234 | } 235 | When-on-file: { 236 | Enter: "Open file and move focus to editor" 237 | Space: "Open file, but keep focus" 238 | } 239 | } 240 | 241 | Mouse-and-Touch-Controls = { 242 | Drag-Titlebar: "Move the window" 243 | Drag-Gutter: "Resize Panes" 244 | Click-Directory: "Toggle open/close on directory" 245 | Click-File: "Select file" 246 | Click-Selected-File: "Open file and focus on Editor" 247 | Click-Activated-File: "Deactivate file" 248 | } 249 | 250 | */} 251 | -------------------------------------------------------------------------------- /src/ui/elements.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /*global chrome*/ 3 | var isChrome = window.chrome && window.chrome.app && window.chrome.app.window; 4 | 5 | // Create the main UI 6 | var domBuilder = require('dombuilder'); 7 | // Hook for global zoom keybindings 8 | require('./zoom')(onZoom); 9 | 10 | var $ = {}; 11 | document.body.appendChild(domBuilder([ 12 | [".wrap", 13 | ["ul.tree.blur$tree"], 14 | [".main$main", 15 | [".editor$editor"], 16 | [".preview$preview", {css: {display:"none"}}, 17 | [".dragger$dragger"], 18 | [".image$image"] 19 | ] 20 | ], 21 | [".titlebar$titlebar"], 22 | ], 23 | isChrome ? [".closebox$closebox", {onclick: closeWindow}, "×"]: [], 24 | ], $)); 25 | 26 | 27 | module.exports = $; 28 | 29 | function onZoom(scale) { 30 | document.body.style.fontSize = (scale * 16) + "px"; 31 | } 32 | 33 | function closeWindow() { 34 | chrome.app.window.current().close(); 35 | } 36 | -------------------------------------------------------------------------------- /src/ui/global-keys.js: -------------------------------------------------------------------------------- 1 | /*global chrome*/ 2 | "use strict"; 3 | 4 | var zoom = require('./zoom'); 5 | var editor = require('./editor'); 6 | var tree = require('./tree'); 7 | var dialog = require('./dialog'); 8 | var doc = require('data/document'); 9 | var pending; 10 | 11 | var extras = []; 12 | exports.register = function (combo, keyCode, handler) { 13 | extras.push({ 14 | combo: combo, 15 | keyCode: keyCode, 16 | handler: handler 17 | }); 18 | }; 19 | 20 | window.addEventListener("keydown", onDown, true); 21 | window.addEventListener("keyup", onUp, true); 22 | window.addEventListener("keypress", onPress, true); 23 | function onDown(evt) { 24 | // Combine all combinations into number from 0 to 15. 25 | var combo = 26 | (evt.ctrlKey ? 1 : 0) | 27 | (evt.shiftKey ? 2 : 0) | 28 | (evt.altKey ? 4 : 0) | 29 | (evt.metaKey ? 8 : 0); 30 | 31 | // Ctrl-0 32 | if (combo === 1 && evt.keyCode === 48) zoom.reset(); 33 | // Ctrl-"+" 34 | else if (combo === 1 && evt.keyCode === 187) zoom.bigger(); 35 | // Ctrl-"-" 36 | else if (combo === 1 && evt.keyCode === 189) zoom.smaller(); 37 | 38 | // Ctrl-Shift-R 39 | else if (combo === 3 && evt.keyCode === 82) chrome.runtime.reload(); 40 | 41 | // Ctrl-B 42 | else if (combo === 1 && evt.keyCode === 66) editor.nextTheme(); 43 | // Ctrl-Shift-B 44 | else if (combo === 3 && evt.keyCode === 66) editor.prevTheme(); 45 | 46 | else if (dialog.close) { 47 | // Esc closes a dialog 48 | if (combo === 0 && evt.keyCode === 27) dialog.close(); 49 | else return; 50 | } 51 | // Alt+` switches between documents 52 | else if (combo === 4 && evt.keyCode === 192) { 53 | pending = true; 54 | doc.next(); 55 | } 56 | // Control-E Toggles Tree 57 | else if (combo === 1 && evt.keyCode === 69) tree.toggle(); 58 | // Control-N Create new file 59 | else if (combo === 1 && evt.keyCode === 78) tree.newFile(); 60 | else if (!tree.isFocused()) { 61 | extras.forEach(function (extra) { 62 | if (extra.combo === combo && extra.keyCode === evt.keyCode) { 63 | extra.handler(); 64 | } 65 | }); 66 | return; 67 | } 68 | else if (combo === 0 && evt.keyCode === 33) tree.pageUp(); 69 | else if (combo === 0 && evt.keyCode === 34) tree.pageDown(); 70 | else if (combo === 0 && evt.keyCode === 35) tree.end(); 71 | else if (combo === 0 && evt.keyCode === 36) tree.home(); 72 | else if (combo === 0 && evt.keyCode === 37) tree.left(); 73 | else if (combo === 0 && evt.keyCode === 38) tree.up(); 74 | else if (combo === 0 && evt.keyCode === 39) tree.right(); 75 | else if (combo === 0 && evt.keyCode === 40) tree.down(); 76 | else if (combo === 0 && evt.keyCode === 27) tree.cancel(); 77 | else if (combo === 0 && evt.keyCode === 8) tree.backspace(); 78 | else if (combo === 0 && evt.keyCode === 13) { // Enter 79 | tree.activate(); 80 | } 81 | else return; 82 | evt.preventDefault(); 83 | evt.stopPropagation(); 84 | } 85 | function onPress(evt) { 86 | if (dialog.close || !tree.isFocused()) return; 87 | evt.preventDefault(); 88 | evt.stopPropagation(); 89 | tree.onChar(evt.charCode); 90 | } 91 | function onUp(evt) { 92 | if (evt.keyCode === 18) { 93 | if (pending) { 94 | pending = false; 95 | doc.reset(); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/ui/notify.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var domBuilder = require('dombuilder'); 4 | var popup = domBuilder([".popup"]); 5 | var timeout = null; 6 | hide(); 7 | document.body.appendChild(popup); 8 | 9 | module.exports = function (message) { 10 | popup.textContent = message; 11 | console.info(message); 12 | if (timeout) clearTimeout(timeout); 13 | else show(); 14 | timeout = setTimeout(hide, 1000); 15 | }; 16 | 17 | function hide() { 18 | popup.style.display = "none"; 19 | timeout = null; 20 | } 21 | 22 | function show() { 23 | popup.style.display = "block"; 24 | } 25 | -------------------------------------------------------------------------------- /src/ui/row.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // This module handles rendering for rows in the file tree 3 | // It exports an object with data-bound properties that control the UI 4 | 5 | var domBuilder = require('dombuilder'); 6 | var modes = require('js-git/lib/modes'); 7 | var defer = require('js-git/lib/defer'); 8 | 9 | module.exports = makeRow; 10 | 11 | // This represents trees, commits, files, and symlinks. 12 | // Any of it's properties can be read or written and auto-updates the UI 13 | function makeRow(path, mode, hash) { 14 | if (typeof path !== "string") throw new TypeError("path must be a string"); 15 | if (typeof mode !== "number") throw new TypeError("mode must be a number"); 16 | var errorMessage = "", 17 | title, 18 | treeHash, 19 | pulse = 0, 20 | exportPath, 21 | serverPort, 22 | open = false, 23 | ahead = 0, 24 | behind = 0, 25 | busy = 0, 26 | active = false, 27 | selected = false, 28 | dirty = false, 29 | staged = false; 30 | var $ = {}; 31 | var children; 32 | var row = { 33 | el: domBuilder(["li$el", ["$row", ["i$icon"], ["span$span"]]], $), 34 | get title () { return title; }, 35 | set title( value) { 36 | title = value; 37 | updatePath(); 38 | }, 39 | get pulse () { return pulse; }, 40 | set pulse( value) { 41 | pulse = value; 42 | updateIcon(); 43 | }, 44 | get exportPath () { return exportPath; }, 45 | set exportPath( value) { 46 | exportPath = value; 47 | updateIcon(); 48 | }, 49 | get serverPort () { return serverPort; }, 50 | set serverPort( value) { 51 | serverPort = value; 52 | updateIcon(); 53 | }, 54 | get path() { return path; }, 55 | set path(newPath) { 56 | path = newPath; 57 | updatePath(); 58 | }, 59 | get mode() { return mode; }, 60 | set mode(value) { 61 | mode = value; 62 | updateIcon(); 63 | updateUl(); 64 | }, 65 | get ahead() { return ahead; }, 66 | set ahead(value) { 67 | ahead = value; 68 | updateIcon(); 69 | }, 70 | get behind() { return behind; }, 71 | set behind(value) { 72 | behind = value; 73 | updateIcon(); 74 | }, 75 | get open() { return open; }, 76 | set open(value) { 77 | open = value; 78 | updateIcon(); 79 | }, 80 | get busy() { return busy; }, 81 | set busy(isBusy) { 82 | busy = isBusy; 83 | updateIcon(); 84 | }, 85 | get active() { return active; }, 86 | set active(value) { 87 | active = value; 88 | updateRow(); 89 | }, 90 | get selected() { return selected; }, 91 | set selected(value) { 92 | selected = value; 93 | updateRow(); 94 | }, 95 | get dirty() { return dirty; }, 96 | set dirty(value) { 97 | dirty = value; 98 | updateRow(); 99 | }, 100 | get staged() { return staged; }, 101 | set staged(value) { 102 | staged = value; 103 | updateRow(); 104 | }, 105 | get hash() { return hash; }, 106 | set hash(value) { 107 | hash = value; 108 | updateIcon(); 109 | }, 110 | get treeHash() { return treeHash; }, 111 | set treeHash(value) { 112 | treeHash = value; 113 | updateIcon(); 114 | }, 115 | get errorMessage() { return errorMessage; }, 116 | set errorMessage(value) { 117 | errorMessage = value; 118 | updateIcon(); 119 | }, 120 | get hasChildren() { 121 | return open && children.length; 122 | }, 123 | addChild: addChild, 124 | removeChild: removeChild, 125 | removeChildren: removeChildren, 126 | // Boilerplate helper to automate fs calls for a row 127 | call: call, 128 | fail: fail, 129 | }; 130 | row.rowEl = $.row; 131 | $.el.js = row; 132 | updateAll(); 133 | return row; 134 | 135 | function updateIcon() { 136 | var value = 137 | (errorMessage ? "icon-attention" : 138 | busy ? "icon-spin1 animate-spin" : 139 | mode === modes.sym ? "icon-link" : 140 | mode === modes.file ? "icon-doc" : 141 | mode === modes.exec ? "icon-cog" : 142 | mode === modes.commit ? "icon-fork" : 143 | open ? "icon-folder-open" : "icon-folder") + 144 | (mode === modes.commit ? " tight" : ""); 145 | $.icon.setAttribute("class", value); 146 | var title = modes.toType(mode) + " " + hash; 147 | if (errorMessage) title += "\n" + errorMessage; 148 | $.icon.setAttribute("title", title); 149 | if (mode !== modes.commit) { 150 | if ($.folder) { 151 | $.row.removeChild($.folder); 152 | delete $.folder; 153 | } 154 | } 155 | else { 156 | if (!$.folder) { 157 | $.row.insertBefore(domBuilder(["i$folder"], $), $.icon); 158 | } 159 | $.folder.setAttribute("class", "icon-folder" + (open ? "-open" : "")); 160 | $.folder.setAttribute("title", "tree " + treeHash); 161 | } 162 | if (ahead) { 163 | if (!$.ahead) { 164 | $.row.appendChild(domBuilder(["i$ahead"], $)); 165 | $.ahead.setAttribute("class", "icon-upload-cloud"); 166 | } 167 | $.ahead.setAttribute("title", ahead + " commit" + (ahead === 1 ? "" : "s") + " ahead"); 168 | } 169 | else if ($.ahead) { 170 | $.row.removeChild($.ahead); 171 | delete $.ahead; 172 | } 173 | if (behind) { 174 | if (!$.behind) { 175 | $.row.appendChild(domBuilder(["i$behind"], $)); 176 | $.behind.setAttribute("class", "icon-download-cloud"); 177 | } 178 | $.behind.setAttribute("title", behind + " commit" + (behind === 1 ? "" : "s") + " behind"); 179 | } 180 | else if ($.behind) { 181 | $.row.removeChild($.behind); 182 | delete $.behind; 183 | } 184 | if (serverPort) { 185 | if (!$.server) { 186 | $.row.appendChild(domBuilder(["i$server"], $)); 187 | $.server.setAttribute("class", "icon-globe" + (pulse ? " animate-spin" : "")); 188 | } 189 | $.server.setAttribute("title", "Serving on port " + serverPort); 190 | } 191 | else if ($.server) { 192 | $.row.removeChild($.server); 193 | delete $.server; 194 | } 195 | if (exportPath) { 196 | if (!$.export) { 197 | $.row.appendChild(domBuilder(["i$export"], $)); 198 | } 199 | $.export.setAttribute("title", "Live Exporting to " + exportPath); 200 | $.export.setAttribute("class", "icon-floppy" + (pulse ? " animate-spin" : "")); 201 | } 202 | else if ($.export) { 203 | $.row.removeChild($.export); 204 | delete $.export; 205 | } 206 | } 207 | 208 | function updatePath() { 209 | // Update the UI to show the short-name 210 | var name = path.substring(path.lastIndexOf("/") + 1); 211 | $.span.textContent = name || "Tedit WorkSpace"; 212 | $.span.setAttribute("title", title || path); 213 | } 214 | 215 | function updateRow() { 216 | var classes = ["row"]; 217 | if (dirty) classes.push("dirty"); 218 | if (staged) classes.push("staged"); 219 | if (active) classes.push("active"); 220 | if (selected) classes.push("selected"); 221 | $.row.setAttribute("class", classes.join(" ")); 222 | } 223 | 224 | function updateUl() { 225 | if (modes.isBlob(mode)) { 226 | if ($.ul) { 227 | $.el.removeChild($.ul); 228 | delete $.ul; 229 | children = null; 230 | } 231 | } 232 | else if (!$.ul) { 233 | $.el.appendChild(domBuilder(["ul$ul"], $)); 234 | children = []; 235 | } 236 | } 237 | 238 | function updateAll() { 239 | updateIcon(); 240 | updatePath(); 241 | updateRow(); 242 | updateUl(); 243 | } 244 | 245 | function addChild(child) { 246 | if (!$.ul) throw new Error("Not Container"); 247 | var other; 248 | // Sort children by path 249 | for (var i = 0, l = children.length; i < l; i++) { 250 | other = children[i]; 251 | if (other.path > child.path) break; 252 | } 253 | if (i === l) { 254 | $.ul.appendChild(child.el); 255 | children.push(child); 256 | } 257 | else { 258 | $.ul.insertBefore(child.el, children[i].el); 259 | children.splice(i, 0, child); 260 | } 261 | return child; 262 | } 263 | 264 | function removeChild(child) { 265 | if (!$.ul) throw new Error("Not Container"); 266 | var index = children.indexOf(child); 267 | if (index < 0) throw new Error("Child not found"); 268 | children.splice(index, 1); 269 | $.ul.removeChild(child.el); 270 | return child; 271 | } 272 | 273 | function removeChildren() { 274 | if (!children) return; 275 | children.length = 0; 276 | while ($.ul.firstChild) $.ul.removeChild($.ul.firstChild); 277 | } 278 | 279 | function after(child) { 280 | var index = children.indexOf(child); 281 | if (index < children.length - 1) { 282 | return children[index + 1]; 283 | } 284 | throw "TODO: Implement after jump"; 285 | } 286 | 287 | function before(child) { 288 | var index = children.indexOf(child); 289 | if (index > 1) return children[index - 1]; 290 | throw "TODO: Implement before jump"; 291 | } 292 | 293 | // First arg is function to call. 294 | // row path is injected as first argument in function 295 | // If first arg is a string, it's used at the path instead. 296 | // callback is intercepted with error stripping/checking callback 297 | // callback gets rest of original callback args. 298 | function call(fn) { 299 | var args = Array.prototype.slice.call(arguments, 1); 300 | // Strip original callback from end if it's there. 301 | var cb = (typeof args[args.length - 1] === "function") && args.pop(); 302 | // Prepend path at front of args 303 | if (typeof fn !== "function") { 304 | var newPath = fn; 305 | fn = args[0]; 306 | args[0] = newPath; 307 | } 308 | else { 309 | args.unshift(path); 310 | } 311 | // console.log(path, "+", arguments); 312 | // Append new callback at end 313 | args.push(callback); 314 | // Callbacks may be called sync, we need to detect that. 315 | var sync = null; 316 | try { fn.apply(null, args); } 317 | catch (err) { 318 | defer(function () { fail(err); }); 319 | } 320 | if (sync === null) { 321 | sync = false; 322 | // If we make it this far, we're waiting for an async action to complete. 323 | row.busy++; 324 | } 325 | 326 | function callback(err) { 327 | if (sync === null) sync = true; 328 | else row.busy--; 329 | // If there was an async error, report it 330 | if (err) fail(err); 331 | if (!cb) return; 332 | // Otherwise call the original callback with the rest of the args. 333 | var args = Array.prototype.slice.call(arguments, 1); 334 | try { cb.apply(null, args); } 335 | catch (err) { fail(err); } 336 | } 337 | } 338 | 339 | function fail(err) { 340 | row.errorMessage = err.toString(); 341 | throw err; 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/ui/slider.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require('./editor'); 4 | 5 | var $ = require("./elements"); 6 | var prefs = require('prefs'); 7 | 8 | var position = null; 9 | var isTouch = false; 10 | var size = prefs.get("slider", 200); 11 | var innerWidth; 12 | 13 | innerWidth = window.innerWidth; 14 | slide(size); 15 | var gutter = document.querySelector(".ace_gutter"); 16 | var dragger = document.querySelector(".dragger"); 17 | 18 | window.addEventListener("resize", onResize); 19 | gutter.addEventListener("mousedown", onStart, true); 20 | gutter.addEventListener("touchstart", onStart, true); 21 | dragger.addEventListener("mousedown", onStart, true); 22 | dragger.addEventListener("touchstart", onStart, true); 23 | 24 | require('./zoom')(onZoom); 25 | 26 | function onZoom(scale, oldScale) { 27 | slide(Math.round(size / oldScale * scale)); 28 | } 29 | 30 | function onResize() { 31 | innerWidth = window.innerWidth; 32 | slide(size); 33 | } 34 | 35 | function onStart(evt) { 36 | if (position !== null) return; 37 | evt.preventDefault(); 38 | evt.stopPropagation(); 39 | if (evt.touches) { 40 | evt = evt.touches[0]; 41 | isTouch = true; 42 | } 43 | else { 44 | isTouch = false; 45 | } 46 | position = evt.clientX; 47 | if (isTouch) { 48 | window.addEventListener("touchmove", onMove, true); 49 | window.addEventListener('touchend', onEnd, true); 50 | } 51 | else { 52 | window.addEventListener("mousemove", onMove, true); 53 | window.addEventListener('mouseup', onEnd, true); 54 | } 55 | } 56 | 57 | function onMove(evt) { 58 | evt.preventDefault(); 59 | evt.stopPropagation(); 60 | if (evt.touches) evt = evt.touches[0]; 61 | var delta = evt.clientX - position; 62 | position = evt.clientX; 63 | size += delta; 64 | slide(size); 65 | } 66 | 67 | function onEnd() { 68 | if (isTouch) { 69 | window.removeEventListener("touchmove", onMove, true); 70 | window.removeEventListener('touchend', onEnd, true); 71 | } 72 | else { 73 | window.removeEventListener("mousemove", onMove, true); 74 | window.removeEventListener('mouseup', onEnd, true); 75 | } 76 | position = null; 77 | isTouch = null; 78 | } 79 | 80 | function slide(x) { 81 | size = x; 82 | if (size < 0) size = 0; 83 | if (size > innerWidth - 42) size = innerWidth - 42; 84 | prefs.set("slider", size); 85 | $.tree.style.width = size + "px"; 86 | $.titlebar.style.left = size + "px"; 87 | $.main.style.left = size + "px"; 88 | } 89 | 90 | module.exports = { 91 | get size() { 92 | return size; 93 | }, 94 | set size(value) { 95 | slide(value); 96 | } 97 | }; 98 | -------------------------------------------------------------------------------- /src/ui/zoom.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var notify = require('./notify'); 4 | var prefs = require('prefs'); 5 | var zooms = [ 6 | 25, 33, 50, 67, 75, 90, 100, 110, 120, 125, 150, 175, 200, 250, 300, 400, 500 7 | ]; 8 | var zoomIndex = prefs.get("zoomIndex", zooms.indexOf(100)); 9 | var oldIndex = zoomIndex; 10 | 11 | var handlers = []; 12 | 13 | onZoom.bigger = bigger; 14 | onZoom.smaller = smaller; 15 | onZoom.reset = reset; 16 | module.exports = onZoom; 17 | 18 | function bigger() { 19 | if (zoomIndex < zooms.length - 1) zoomIndex++; 20 | zoom(); 21 | } 22 | 23 | function smaller() { 24 | if (zoomIndex > 0) zoomIndex--; 25 | zoom(); 26 | } 27 | 28 | function reset() { 29 | zoomIndex = zooms.indexOf(100); 30 | zoom(); 31 | } 32 | 33 | function onZoom(callback) { 34 | handlers.push(callback); 35 | callback(zooms[zoomIndex] / 100, zooms[oldIndex] / 100); 36 | } 37 | 38 | function zoom() { 39 | if (zoomIndex === oldIndex) return; 40 | var percent = zooms[zoomIndex]; 41 | var scale = percent / 100; 42 | var oldScale = oldIndex && zooms[oldIndex] / 100; 43 | oldIndex = zoomIndex; 44 | handlers.forEach(function (handler) { 45 | handler(scale, oldScale); 46 | }); 47 | prefs.set("zoomIndex", zoomIndex); 48 | notify(percent + "% zoom"); 49 | } 50 | --------------------------------------------------------------------------------