├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── package.json ├── project.clj ├── resources ├── react-devtools │ └── injected │ │ └── GlobalHook.js ├── scripts │ └── env.js └── stylesheets │ └── app.css ├── src-dev └── flense_nw │ └── repl.clj └── src └── flense_nw ├── app.cljs ├── cli.cljs ├── env.cljs ├── error.cljs └── keymap.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | *jar 3 | /lib/ 4 | /node_modules/ 5 | /classes/ 6 | /out/ 7 | /target/ 8 | /vendor/ 9 | .lein-deps-sum 10 | .lein-repl-history 11 | .lein-plugins/ 12 | .nrepl* 13 | *.iml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Max Kreminski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flense-nw 2 | 3 | flense-nw is a Clojure code editor app written using [nw.js](http://nwjs.io/) and [Flense](https://github.com/mkremins/flense). Essentially, flense-nw wraps an instance of the baseline Flense editor component in an imitation of the traditional text editor interface, providing functionality like file I/O, configurable keybinds, and a way to enter text commands. 4 | 5 | Want to see it in action? Check out this short [video demo](https://www.youtube.com/watch?v=Vh9AymZsqEk) of the basic editing functionality. 6 | 7 | ## Building 8 | 9 | flense-nw runs on [nw.js](http://nwjs.io/). You'll also need [npm](https://www.npmjs.org/) to install some of the dependencies and [Leiningen](http://leiningen.org/) to compile the ClojureScript source. 10 | 11 | For the time being, flense-nw builds against the latest snapshot version of Flense. It's recommended that you check out the [Flense repo](https://github.com/mkremins/flense) and `lein install` it in your local repository before attempting to build flense-nw. 12 | 13 | ```bash 14 | cd path/to/flense 15 | lein cljsbuild once 16 | npm install 17 | path/to/nwjs . 18 | ``` 19 | 20 | This will build flense-nw from source and launch it as a standalone GUI app. 21 | 22 | ### Development tips 23 | 24 | #### Running flense editor directly from a browser 25 | 26 | This can be handy for quick tests or when using some bleeding-edge developer tools not available under NW.JS. 27 | Just keep in mind that some non-essential editor functionality can be broken in this environment. For example opening/saving 28 | files from local filesystem depends on node.js libraries embedded by NW.JS. 29 | 30 | I personally run simple HTTP server this way: 31 | 32 | ```bash 33 | cd path/to/flense-nw 34 | python -m SimpleHTTPServer 35 | ``` 36 | 37 | #### ClojureScript REPL 38 | 39 | ```bash 40 | lein repl 41 | ``` 42 | 43 | Next, require the `repl` namespace and boot the Clojurescript repl: 44 | 45 | ```clojure 46 | (require '[flense-nw.repl :as repl]) 47 | (repl/repl!) 48 | ``` 49 | 50 | This will start a Websocket repl using [Weasel](https://github.com/tomjakubowski/weasel). When you reload flense-nw application, it should automatically connect to Weasel and anything you type at the repl will start evaluating. 51 | 52 | ```clojure 53 | (in-ns 'flense-nw.app) 54 | app-state 55 | ``` 56 | 57 | Should print current app-state: 58 | 59 | # 60 | 61 | #### Using React Development Tools with NW.JS 62 | 63 | Flense uses Facebook's [React.js](https://github.com/facebook/react) library (via David Nolen's [Om](https://github.com/swannodette/om)). The React team offers a useful [React Developer Tools](https://github.com/facebook/react-devtools) (RDT) Chrome extension for inspecting and debugging React components (it integrates into Chrome's dev tools). However, flense-nw runs in [nw.js](https://github.com/nwjs/nw.js), and RDT cannot be easily installed into nw.js itself. 64 | 65 | A solution is to use standalone devtools (frontend) in a standalone Chrome with RDT extension installed and instruct devtools to connect to our remote backend (our nw.js context running inside Flense-nw). It is easily doable, but it requires a special setup: 66 | 67 | ##### Preparation 68 | 69 | * launch `/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --no-first-run --user-data-dir=~/temp/chrome-dev-profile` 70 | * install RDT 71 | 72 | ##### Development workflow 73 | 74 | 1. run nw.js instance with remote debugging enabled: 75 | 76 | cd path/to/flense-nw 77 | path/to/nwjs --remote-debugging-port=9222 . 78 | 79 | 2. launch `/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --no-first-run --user-data-dir=~/temp/chrome-dev-profile` 80 | 3. in Chrome navigate to [http://localhost:9222/json](http://localhost:9222/json) 81 | => you should see a websocket url for remote context running in your nw.js from step #1 (note: sometimes you have to do a second refresh to see devtoolsFrontendUrl): 82 | 83 | [ { 84 | "description": "", 85 | "devtoolsFrontendUrl": "/devtools/devtools.html?ws=localhost:9222/devtools/page/BDFB0179-D7E4-6A27-6AD4-D7039548FDCB", 86 | "id": "BDFB0179-D7E4-6A27-6AD4-D7039548FDCB", 87 | "title": "index.html", 88 | "type": "page", 89 | "url": "file:///Users/darwin/code/flense-dev/flense-nw/index.html", 90 | "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/BDFB0179-D7E4-6A27-6AD4-D7039548FDCB" 91 | } ] 92 | 93 | 4. in Chrome navigate to devtoolsFrontendUrl where you replace `/devtools/devtools.html` with `chrome-devtools://devtools/bundled/devtools.html` (kudos to Paul Irish for the solution) 94 | 95 | example: `chrome-devtools://devtools/bundled/devtools.html?ws=localhost:9222/devtools/page/BDFB0179-D7E4-6A27-6AD4-D7039548FDCB` 96 | 97 | Voila! Now you should have a debug session estabilished between your devtools in Chrome (devtools frontend) and your Flense-nw application (devtools backend). 98 | 99 | Last tested with Chrome Canary 42.0.2283.5 and RDT 0.12.1. 100 | 101 | ## License 102 | 103 | [MIT License](http://opensource.org/licenses/MIT). Hack away. 104 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Flense", 3 | "main": "index.html", 4 | "window": { 5 | "width": 900, 6 | "height": 700 7 | }, 8 | "dependencies": { 9 | "mkdirp": "0.4.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject mkremins/flense-nw "0.0-SNAPSHOT" 2 | :dependencies 3 | [[org.clojure/clojure "1.6.0"] 4 | [org.clojure/clojurescript "0.0-3126"] 5 | [org.clojure/core.async "0.1.346.0-17112a-alpha"] 6 | [org.omcljs/om "0.8.8"] 7 | [prismatic/om-tools "0.3.11"] 8 | [spellhouse/phalanges "0.1.6"] 9 | [mkremins/decodn "0.0-SNAPSHOT"] 10 | [mkremins/flense "0.0-SNAPSHOT"] 11 | [mkremins/fs "0.3.0"] 12 | [com.binaryage/devtools "0.0-SNAPSHOT"] 13 | [weasel "0.6.0"] 14 | [com.cemerick/piggieback "0.1.5"]] 15 | 16 | :plugins 17 | [[lein-cljsbuild "1.0.4"]] 18 | 19 | :repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]} 20 | :source-paths ["src", "src-dev"] ; note: "src" must be included to prevent https://github.com/mfikes/weasel-src-paths 21 | 22 | :cljsbuild 23 | {:builds 24 | [{:source-paths ["src"] 25 | :compiler {:main flense-nw.app 26 | :output-to "target/flense.js" 27 | :output-dir "target" 28 | :source-map true 29 | :optimizations :none}}]}) 30 | -------------------------------------------------------------------------------- /resources/react-devtools/injected/GlobalHook.js: -------------------------------------------------------------------------------- 1 | // Inject a `__REACT_DEVTOOLS_GLOBAL_HOOK__` global so that React can detect that the 2 | // devtools are installed (and skip its suggestion to install the devtools). 3 | var js = ( 4 | "Object.defineProperty(" + 5 | "window, '__REACT_DEVTOOLS_GLOBAL_HOOK__', {value: {" + 6 | "inject: function(runtime) { this._reactRuntime = runtime; }," + 7 | "getSelectedInstance: null," + 8 | "Overlay: null" + 9 | "}}" + 10 | ")" 11 | ); 12 | 13 | // This script runs before the element is created, so we add the script 14 | // to instead. 15 | if (!window["__REACT_DEVTOOLS_GLOBAL_HOOK__"]) { 16 | var script = document.createElement('script'); 17 | script.textContent = js; 18 | document.documentElement.appendChild(script); 19 | script.parentNode.removeChild(script); 20 | } 21 | -------------------------------------------------------------------------------- /resources/scripts/env.js: -------------------------------------------------------------------------------- 1 | function isNodePresent() { 2 | return (typeof process == "object"); 3 | } 4 | 5 | if (!isNodePresent()) { 6 | // mute require calls when running under browser 7 | window.require = function(name) { 8 | if (name!="React") 9 | console.info("A require call to '" + name + "' is not available under raw browser environment."); 10 | return {}; 11 | } 12 | } -------------------------------------------------------------------------------- /resources/stylesheets/app.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Source+Code+Pro); 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | body { 9 | font-family: 'Source Code Pro'; 10 | font-size: 14px; 11 | line-height: 1.4; 12 | } 13 | 14 | .tab-bar { 15 | background: #eee; 16 | } 17 | 18 | .tab { 19 | display: inline-block; 20 | font-family: 'Lucida Grande', Verdana, sans-serif; 21 | font-size: 12px; 22 | padding: 0.5rem 1rem; 23 | min-width: 80px; 24 | } 25 | 26 | .tab.selected { 27 | background: #fff; 28 | } 29 | 30 | .tab-content { 31 | margin-left: 5%; 32 | margin-top: 40px; 33 | } 34 | 35 | .flense { 36 | margin-bottom: 80px; 37 | } 38 | 39 | #cli { 40 | border: none; 41 | border-top: 1px solid #ccc; 42 | bottom: 0; 43 | font-family: inherit; 44 | font-size: inherit; 45 | left: 0; 46 | outline: none; 47 | padding: 1rem; 48 | position: fixed; 49 | width: 100%; 50 | } 51 | 52 | #error-bar { 53 | -webkit-backface-visibility: hidden; 54 | background: rgb(200,0,0); 55 | border-bottom-left-radius: 0.25rem; 56 | color: white; 57 | padding: 0.5rem; 58 | position: fixed; 59 | right: 0; 60 | top: 0; 61 | } 62 | 63 | #error-bar.visible { 64 | opacity: 1; 65 | } 66 | 67 | #error-bar.hidden { 68 | opacity: 0; 69 | transition: opacity 2s ease-out; 70 | } 71 | 72 | #error-bar:empty { 73 | visibility: hidden; 74 | } 75 | -------------------------------------------------------------------------------- /src-dev/flense_nw/repl.clj: -------------------------------------------------------------------------------- 1 | (ns flense-nw.repl 2 | (:require [cemerick.piggieback :as piggieback] 3 | [weasel.repl.websocket :as weasel])) 4 | 5 | (defn repl! 6 | "Starts a Clojurescript repl." 7 | [] 8 | (piggieback/cljs-repl :repl-env (weasel/repl-env :ip "0.0.0.0" :port 9001))) 9 | -------------------------------------------------------------------------------- /src/flense_nw/app.cljs: -------------------------------------------------------------------------------- 1 | (ns flense-nw.app 2 | (:require [cljs.core.async :as async :refer [document 31 | '[(defn greet [name] (str "Hello, " name "!"))])}]})) 32 | 33 | (def ^:private error-chan (async/chan)) 34 | 35 | (defn raise! 36 | "Display error message `mparts` to the user in the popover error bar." 37 | [& mparts] 38 | (async/put! error-chan (apply str mparts))) 39 | 40 | (defn open! 41 | "Load the source file at `fpath` and open the loaded document in a new tab." 42 | [fpath] 43 | (let [tree (-> fpath fs/slurp decodn/read-document model/annotate-paths) 44 | tabs (conj (:tabs @app-state) 45 | {:name fpath :document {:path [0] :tree tree}})] 46 | (reset! app-state {:selected-tab (dec (count tabs)) :tabs tabs}))) 47 | 48 | (defn perform! [action] 49 | (swap! app-state update-in [:tabs (:selected-tab @app-state) :document] 50 | (flense/perform action))) 51 | 52 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 53 | ;; text commands 54 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 55 | 56 | (defmulti handle-command (fn [command & _] command)) 57 | 58 | (defmethod handle-command :default [command & _] 59 | (raise! "Invalid command \"" command \")) 60 | 61 | (defmethod handle-command "open" [_ & args] 62 | (if-let [fpath (first args)] 63 | (open! fpath) 64 | (raise! "Must specify a filepath to open"))) 65 | 66 | (defmethod handle-command "rename" [_ & args] 67 | (if-let [new-name (first args)] 68 | (perform! #(clojure/rename-symbol % new-name)) 69 | (raise! "Must specify a new name to use"))) 70 | 71 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 72 | ;; keybinds 73 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 74 | 75 | (defn- handle-keydown [ev] 76 | (let [keyset (phalanges/key-set ev)] 77 | (if (= keyset #{:ctrl :x}) 78 | (do (.preventDefault ev) 79 | (.. js/document (getElementById "cli") focus)) 80 | (when-let [action (keymap keyset)] 81 | (.preventDefault ev) 82 | (perform! action))))) 83 | 84 | (def legal-char? 85 | (let [uppers (map (comp js/String.fromCharCode (partial + 65)) (range 26)) 86 | lowers (map (comp js/String.fromCharCode (partial + 97)) (range 26)) 87 | digits (map str (range 10)) 88 | puncts [\. \! \? \$ \% \& \+ \- \* \/ \= \< \> \_ \: \' \\ \|]] 89 | (set (concat uppers lowers digits puncts)))) 90 | 91 | (defn- handle-keypress [ev] 92 | (let [c (phalanges/key-char ev)] 93 | (when (legal-char? c) 94 | (.preventDefault ev) 95 | (perform! (partial text/insert c))))) 96 | 97 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 98 | ;; application setup and wiring 99 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 100 | 101 | (defcomponent tabs [data owner opts] 102 | (render [_] 103 | (let [{:keys [selected-tab tabs]} data] 104 | (dom/div {:class "tabs"} 105 | (dom/div {:class "tab-bar"} 106 | (for [i (range (count tabs))] 107 | (dom/div {:class (str "tab" (when (= i selected-tab) " selected")) 108 | :on-click #(om/update! data :selected-tab i)} 109 | (:name (nth tabs i))))) 110 | (dom/div {:class "tab-content"} 111 | (for [i (range (count tabs)) 112 | :let [{:keys [document]} (nth tabs i)]] 113 | (dom/div {:style {:display (if (= i selected-tab) "block" "none")}} 114 | (om/build flense/editor document {:opts opts})))))))) 115 | 116 | (defn init [] 117 | (let [command-chan (async/chan)] 118 | (om/root tabs app-state 119 | {:target (.getElementById js/document "editor-parent")}) 120 | (om/root cli-view nil 121 | {:target (.getElementById js/document "cli-parent") 122 | :shared {:command-chan command-chan}}) 123 | (om/root error-bar-view nil 124 | {:target (.getElementById js/document "error-bar-parent") 125 | :shared {:error-chan error-chan}}) 126 | (go-loop [] 127 | (let [[command & args] (