├── .gitignore ├── .hgignore ├── .hgtags ├── LICENCE ├── README.md ├── bin └── schirm ├── cljs ├── README.md ├── doc │ └── intro.md ├── project.clj ├── resources │ └── public │ │ ├── css │ │ └── user.css │ │ ├── js │ │ ├── d3.js │ │ └── term.js │ │ └── simple.html ├── src │ ├── clj │ │ └── schirm_cljs │ │ │ └── core.clj │ └── cljs │ │ ├── schirm_cljs │ │ ├── copy_n_paste_hack.cljs │ │ ├── dom_utils.cljs │ │ ├── keys.cljs │ │ ├── screen-tests.cljs │ │ ├── screen.cljs │ │ ├── term.cljs │ │ └── word-select.cljs │ │ └── wb │ │ └── wb.cljs └── test │ └── schirm_cljs │ └── core_test.clj ├── doc ├── blockdiagram.dia ├── cross.svg ├── demo.gif ├── protocol.txt ├── recordschirmdemo.sh ├── rendermodel.dia ├── schirm-large.png ├── schirm-logo.png ├── schirm-small.png ├── schirm.png └── schirm.svg ├── schirm ├── __init__.py ├── __main__.py ├── browserscreen.py ├── chan │ ├── LICENCE │ ├── __init__.py │ └── chan.py ├── main.py ├── proxyconnection.py ├── pyte │ ├── AUTHORS │ ├── LICENSE │ ├── README │ ├── __init__.py │ ├── __main__.py │ ├── charsets.py │ ├── control.py │ ├── escape.py │ ├── graphics.py │ ├── modes.py │ ├── screens.py │ └── streams.py ├── resources │ ├── __init__.py │ ├── schirm.css │ ├── schirm.js │ ├── schirm.png │ ├── term.css │ ├── term.html │ ├── term.js │ └── user.css ├── termiframe.py ├── terminal.py ├── terminalio.py ├── termkey.py ├── termscreen.py ├── utils.py └── webkitwindow.py ├── setup.py └── support ├── bottleexample.py ├── codemirror-full.js ├── cpu_usage.py ├── iframedemo.py ├── schirmclient.py ├── schirmtable.py ├── sedit ├── sget ├── smysql ├── stree └── sview /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build 3 | dist 4 | 5 | cljs/target 6 | cljs/repl 7 | cljs/out 8 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | *.pyc 4 | build 5 | dist 6 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | cfdcb768ac20bfb212573ad1b7e01819f7239a67 0.1-alpha0 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Schirm 2 | ====== 3 | 4 | A Linux compatible terminal emulator which exposes additional HTML 5 | rendering modes to client programs. 6 | 7 | ![Demo](https://raw.githubusercontent.com/hoeck/schirm/master/doc/demo.gif) 8 | 9 | Installing 10 | ========== 11 | 12 | Requires Python 2.7 and PyQT4 (and ClojureScript for development). 13 | 14 | Get the source: 15 | 16 | $ git clone https://github.com/hoeck/schirm.git 17 | 18 | Optionally install xsel ( or 19 | `apt-get install xsel`) to be able to paste the current X 20 | selection using Shift-Insert. Middle-click selection works without 21 | additional programs. 22 | 23 | Run: 24 | 25 | $ cd schirm 26 | $ python -m schirm 27 | 28 | or install and then run it: 29 | 30 | $ python setup.py install 31 | $ schirm 32 | 33 | Runs all your favorite commandline applications, including: 34 | 35 | mc, htop, vim, grep --color ... 36 | 37 | Example Programs 38 | ================ 39 | 40 | See the demos in `support/` 41 | 42 | - they use the schirmclient.py lib to show HTML documents in the terminal 43 | 44 | - add them to your `PATH`: 45 | 46 | $ PATH="$PATH:/support" 47 | 48 | or invoke them with `/support/` 49 | 50 | - get and view some html: 51 | 52 | $ curl news.ycombinator.com | sview 53 | 54 | - preview a Markdown document: 55 | 56 | $ markdown README.md | sview 57 | 58 | - show a tree of a directory (uses d3.js, slow/crashing for large directories): 59 | 60 | $ stree . 61 | 62 | or 63 | 64 | $ stree --circle 65 | 66 | - view an image: 67 | 68 | $ sview schirm-logo.png 69 | 70 | with interactive rescaling buttons: 71 | 72 | $ sview -i schirm-logo.png 73 | 74 | - edit text using codemirror: 75 | 76 | $ sedit README.md 77 | 78 | Styling 79 | ======= 80 | 81 | Place your own styles into `~/.schirm/user.css` and restart the terminal. 82 | See `schirm/resources/user.css` for the default stylesheet. 83 | 84 | In the terminal window, right click for the context menu and select 85 | `inspect` to open the webkit devtools. Right-click on the very left of 86 | the terminal window to open a context menu containing the `reload` item 87 | to restart the terminal 88 | 89 | Client API 90 | ========== 91 | 92 | Escape-sequence based, works with every programming language which can write bytes to 93 | stdout. See ``support/schirmclient.py``. 94 | 95 | Missing Features/Defects 96 | ======================== 97 | 98 | - no terminal mouse click support 99 | - only 16 colors 100 | - only UTF-8 101 | - slow Application mode (e.g. fullscreen ncurses apps) 102 | - `cat <1G file>` will bring everything to a halt 103 | - emulation glitches (e.g. when resizing htop) 104 | 105 | Similar Programs 106 | ================ 107 | 108 | - : terminal emulator supporting HTML iframes and serving files, server written in Python 109 | - : terminal emulator allowing inline HTML and javascript 110 | - : not an emulator but a completely new terminal, built on WebKit and Node.js 111 | - : HTML5 Terminal emulator 112 | - : a complete browser terminal emulator with server 113 | - : nextgen Terminal Emulator of the Enlightment Project 114 | - : QT python terminal emulator 115 | - : beautiful VTXXX compatible terminal emulator library 116 | - (referenced by Fabrice Bellard on his tech details page. Bellard uses his own proprietary terminal emulator though) 117 | - ([actual terminal emulator here](https://raw.github.com/chjj/tty.js/master/static/term.js)) 118 | - (Frank Bi's unmaintained terminal emulator, outdated by now) 119 | - (from jor1k, the JavaScript OpenRISC emulator) 120 | - : built on electron, blends shell features (e.g. autocompletion) into the terminal, aims to be VT100 compatible 121 | - : nodejs, runs in chrome, VT100, web features such as image and html display 122 | - : electron, alpha, augments commands such as ls and git status, uses fish shell, special HTerminal protocol for extensions 123 | - : electron, typescriptm codemirror for displaying, solid emulation based on term.js 124 | - : terminal emulation in JS, Server or GUI in Java, extension via escape sequences 125 | - : electron, focus on emulation and plugin-like extensions 126 | - : electron, integrates with the actual shell to e.g. provide an autocompletion popup 127 | 128 | Licence 129 | ======= 130 | 131 | Copyright (C) 2012 Erik Soehnel 132 | 133 | Licenced under the GPLv3. 134 | 135 | Client library is BSD licenced. 136 | -------------------------------------------------------------------------------- /bin/schirm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | if __name__ == '__main__': 4 | from schirm.main import main 5 | main() 6 | -------------------------------------------------------------------------------- /cljs/README.md: -------------------------------------------------------------------------------- 1 | # schirm-cljs 2 | 3 | A Clojure library designed to ... well, that part is up to you. 4 | 5 | ## Usage 6 | 7 | ## Developing 8 | 9 | 10 | 11 | interactive development & browser repl 12 | 13 | 14 | $ cd resources/public 15 | $ python -m SimpleHTTPServer 16 | 17 | $ lein cljsbuild auto 18 | $ rlwrap lein trampoline cljsbuild repl-listen 19 | 20 | 21 | ## License 22 | 23 | -------------------------------------------------------------------------------- /cljs/doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to schirm-cljs 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/great-documentation/what-to-write/) 4 | -------------------------------------------------------------------------------- /cljs/project.clj: -------------------------------------------------------------------------------- 1 | (defproject schirm-cljs "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :url "http://example.com/FIXME" 4 | :license {:name "GNU GPLv3" 5 | :url "http://www.gnu.org/licenses/gpl-3.0.html"} 6 | 7 | ;; CLJ source code path 8 | :source-paths ["src/clj"] 9 | :dependencies [[org.clojure/clojure "1.5.1"] 10 | ;;[com.cemerick/clojurescript.test "0.0.4"] 11 | [core.async "0.1.0-SNAPSHOT"] 12 | ] 13 | 14 | ;; lein-cljsbuild plugin to build a CLJS project 15 | :plugins [[lein-cljsbuild "0.3.2"]] 16 | 17 | ;; cljsbuild options configuration 18 | :cljsbuild {:builds 19 | [{;; CLJS source code path 20 | :source-paths ["src/cljs"] 21 | 22 | ;; Google Closure (CLS) options configuration 23 | :compiler {;; CLS generated JS script filename 24 | :output-to "resources/public/js/term.js" 25 | 26 | ;; minimal JS optimization directive 27 | :optimizations :whitespace 28 | 29 | ;; generated JS code prettyfication 30 | :pretty-print true}}]}) -------------------------------------------------------------------------------- /cljs/resources/public/css/user.css: -------------------------------------------------------------------------------- 1 | /* color attribute styles 2 | * taken from shellinabox styles.css 3 | */ 4 | 5 | /* normal */ 6 | .f-default { } 7 | .f-default-reversed { color: #ffffff; } 8 | .f-red { color: #cd0000; } 9 | .f-green { color: #00cd00; } 10 | .f-brown { color: #cdcd00; } 11 | .f-blue { color: #0000ee; } 12 | .f-magenta { color: #cd00cd; } 13 | .f-cyan { color: #00cdcd; } 14 | .f-white { color: #e5e5e5; } 15 | 16 | /* bold/bright */ 17 | .bold.f-default { color: #7f7f7f; } 18 | .bold.f-red { color: #ff0000; } 19 | .bold.f-green { color: #00ff00; } 20 | .bold.f-brown { color: #e8e800; } 21 | .bold.f-blue { color: #5c5cff; } 22 | .bold.f-magenta { color: #ff00ff; } 23 | .bold.f-cyan { color: #00ffff; } 24 | .bold.f-white { color: #ffffff; } 25 | 26 | /* normal */ 27 | .b-default { } 28 | .b-default-reversed { background-color: #000000; } 29 | .b-black { background-color: #000000; } 30 | .b-red { background-color: #cd0000; } 31 | .b-green { background-color: #00cd00; } 32 | .b-brown { background-color: #cdcd00; } 33 | .b-blue { background-color: #0000aa; } 34 | .b-magenta { background-color: #cd00cd; } 35 | .b-cyan { background-color: #00cdcd; } 36 | .b-white { background-color: #e5e5e5; } 37 | 38 | /* other attrs */ 39 | 40 | .bold { 41 | font-weight: 700; 42 | } 43 | 44 | .italics { 45 | font-style: italic; 46 | } 47 | 48 | .underscore { 49 | text-decoration: underline; 50 | } 51 | 52 | .strikethrough { 53 | text-decoration: line-through; 54 | } 55 | 56 | .cursor { 57 | background-color: #ff00b0; 58 | } 59 | 60 | .cursor-inactive { 61 | border: solid 1px #ff00b0; 62 | display: inline-block; 63 | width: 7px; 64 | height: 14px; 65 | box-sizing: border-box; 66 | vertical-align:top; 67 | } 68 | 69 | /* min scrollbar */ 70 | 71 | body { 72 | overflow-y:scroll; 73 | } 74 | 75 | body::-webkit-scrollbar { 76 | width: 6px; 77 | } 78 | 79 | body::-webkit-scrollbar-track { 80 | -webkit-border-radius: 1px; 81 | border-radius: 1px; 82 | background: rgba(245,245,245,0.8); 83 | } 84 | 85 | body::-webkit-scrollbar-thumb { 86 | -webkit-border-radius: 1px; 87 | border-radius: 1px; 88 | background: rgba(180,180,180,0.8); 89 | 90 | body::-webkit-scrollbar-thumb:window-inactive { 91 | background: rgba(230,230,230,0.8); 92 | } 93 | 94 | /* Example: using different fonts: 95 | @font-face { 96 | font-family: BitstreamVeraSansMono; 97 | src: url('localfont/usr/share/fonts/truetype/ttf-bitstream-vera/VeraMono.ttf'); 98 | } 99 | 100 | @font-face { 101 | font-family: BitstreamVeraSansMono; 102 | font-weight: bold; 103 | src: url('localfont/usr/share/fonts/truetype/ttf-bitstream-vera/VeraMoBd.ttf'); 104 | } 105 | 106 | body { 107 | font-family: BitstreamVeraSansMono; 108 | color: #181818; 109 | background-color: #F9FFE9 110 | } 111 | */ 112 | -------------------------------------------------------------------------------- /cljs/resources/public/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple CLJS 6 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 54 | 55 | 56 | 57 |
58 | A-B-CD
59 | 012345678901234567890123456789012345678901234567890123456789012345678901234567890
60 |           10        20        30         40       50        60        70
61 | 
62 | 
63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /cljs/src/clj/schirm_cljs/core.clj: -------------------------------------------------------------------------------- 1 | (ns schirm-cljs.core) 2 | 3 | (defn foo 4 | "I don't do a whole lot." 5 | [x] 6 | (println x "Hello, World!")) 7 | -------------------------------------------------------------------------------- /cljs/src/cljs/schirm_cljs/copy_n_paste_hack.cljs: -------------------------------------------------------------------------------- 1 | (ns schirm-cljs.copy-n-paste-hack 2 | (:require [schirm-cljs.dom-utils :as dom-utils])) 3 | 4 | (defn key-paste 5 | "Invoke f with the pasted value. 6 | 7 | Use this function when a CTRL-V keypress happens." 8 | [f] 9 | (let [textarea (.createElement js/document "TEXTAREA") 10 | focused-element (.-activeElement js/document)] 11 | (-> js/document .-body (.appendChild textarea)) 12 | (.focus textarea) 13 | (.setTimeout js/window (fn [] 14 | (f (.-value textarea)) 15 | (.remove textarea) 16 | (.focus focused-element)) 17 | 0) 18 | false)) 19 | 20 | (defn setup-mouse-paste 21 | "Use a nearly invisible pixel-size textarea to be able to paste text. 22 | 23 | Set it up so that on any right click on a non-selected part of 24 | container, the browser 'input' contextmenu appears, with the 'paste' 25 | menuitem resulting in calling the paste callback function with the 26 | pasted value. 27 | 28 | Leave a 25px wide space on the left where the normal contextmenu 29 | appears to be able to access the 'reload' and 'inspect element' 30 | items." 31 | [container paste] 32 | (let [ta (dom-utils/create-element "textarea" {:class-name "paste"})] 33 | (-> js/document .-body (.appendChild ta)) 34 | ;; the container is focused when pressing any key or when the terminal receives any data 35 | (.addEventListener ta "blur" #(set! (-> ta .-style .-display) "none")) 36 | ;; pasting 37 | (.addEventListener ta "paste" (fn [e] 38 | (.setTimeout js/window 39 | (fn [] 40 | (paste (.-value ta)) 41 | (set! (.-value ta) "") 42 | (.focus container)) 43 | 0))) 44 | (.addEventListener container 45 | "mousedown" 46 | (fn [e] 47 | (let [x (.-x e) 48 | y (.-y e) 49 | sel (.getSelection js/document) 50 | sel-bounds (when-let [s (< 0 (.-rangeCount sel))] 51 | (-> sel (.getRangeAt 0) .getBoundingClientRect)) 52 | scroll-top (-> js/document .-body .-scrollTop) 53 | scroll-left (-> js/document .-body .-scrollLeft)] 54 | (when (and 55 | ;; right or middle click 56 | (or (= (.-button e) 2) 57 | (= (.-button e) 1)) 58 | ;; on the first 25 pixels, present the normal, non-input 59 | ;; contextmenu to allow accessing the 'reload' and 'inspect element' items 60 | (< 25 x) 61 | ;; not on a selection 62 | (if sel-bounds 63 | (or (not (< (.-left sel-bounds) x (.-right sel-bounds))) 64 | (not (< (.-top sel-bounds) y (.-bottom sel-bounds)))) 65 | true)) 66 | ;; display and move the pixel sized textarea to the current mouse position 67 | (set! (-> ta .-style .-display) "block") 68 | (set! (-> ta .-style .-top) (+ scroll-top y)) 69 | (set! (-> ta .-style .-left) (+ scroll-left x)) 70 | (.focus ta) 71 | (.preventDefault e))))))) 72 | -------------------------------------------------------------------------------- /cljs/src/cljs/schirm_cljs/dom_utils.cljs: -------------------------------------------------------------------------------- 1 | (ns schirm-cljs.dom-utils 2 | (:require [clojure.string :as string])) 3 | 4 | (defn min-fill 5 | "Return a string s of at least min-len, prepending fill-char characters if necessary" 6 | [s min-len fill-char] 7 | (let [fill (- min-len (count s))] 8 | (if (< 0 fill) 9 | (str (apply str (repeat fill (or fill-char \ ))) s) 10 | s))) 11 | 12 | (defn re-find-class 13 | "Match a regex against an elements classes and return the first match." 14 | [element re] 15 | (->> element .-classList array-seq (map #(second (re-matches re %))) (filter identity) first)) 16 | 17 | (defn element-at-pos 18 | "Use pos as the offset into the .textContents of children of parent. 19 | 20 | Return the element containing the pos as well as the local offset of pos." 21 | [parent pos] 22 | (let [children (-> parent .-children array-seq)] 23 | (loop [i 0 24 | lower 0] 25 | (let [ch (nth children i) 26 | upper (when ch (+ lower (-> ch .-textContent count)))] 27 | (cond (nil? ch) nil 28 | (<= upper pos) (recur (inc i) upper) 29 | :else [ch (- pos lower)]))))) 30 | 31 | (defn elements-between-pos 32 | "Like element-at-pos, but return all elements between start and end." 33 | [parent start end] 34 | (when (<= start end) 35 | (let [[start-element start-local] (element-at-pos parent start)] 36 | (if start-element 37 | (loop [e start-element 38 | e-end (+ (-> start-element .-textContent count) (- start start-local)) 39 | res [start-element]] 40 | (if (< end e-end) 41 | {:elements res :local-start start-local :local-end (- (-> e .-textContent count) (- e-end end))} 42 | (if-let [n (.-nextElementSibling e)] 43 | (recur n (+ e-end (-> n .-textContent count)) (conj res n)) 44 | {:elements res :local-start start-local :local-end (-> e .-textContent count)}))) 45 | {})))) 46 | 47 | (defn element-pos [element] 48 | (->> (iterate #(.-previousSibling %) element) 49 | (drop 1) 50 | (take-while identity) 51 | (map #(-> % .-textContent count)) 52 | (reduce + 0))) 53 | 54 | (defn lispify [s] 55 | (-> (string/replace s #"[A-Z]" "-$&") string/lower-case)) 56 | 57 | (defn camelcasify [s] 58 | (string/replace s #"-[A-z0-9]" #(string/upper-case (nth % 1)))) 59 | 60 | (defn create-element [type attrs] 61 | (let [e (.createElement js/document type)] 62 | (when-let [h (:inner-html attrs)] (set! (.-innerHTML e) h)) 63 | (when-let [t (:inner-text attrs)] (set! (.-innerText e) t)) 64 | (when-let [styles (:style attrs)] 65 | (doseq [[k v] styles] 66 | (aset (.-style e) (camelcasify (name k)) v))) 67 | (doseq [c (:class attrs)] 68 | (-> e .-classList (.add c))) 69 | (doseq [[k v] (filter #(not (contains? #{:inner-text :inner-html :style :class})) attrs)] 70 | (aset e (camelcasify (name k)) v)) 71 | e)) 72 | 73 | (defn create-element-from-string [string] 74 | (-> (create-element "div" {:inner-html string}) 75 | .-firstChild)) 76 | 77 | (defn char-size 78 | "Compute the size of an 'X' in element. 79 | 80 | Size includes margin, border and padding. Return a map with :height, 81 | :width and :gap keys. :gap describes the size of the space between 82 | two lines of text." 83 | [element] 84 | (let [specimen (create-element "span" {:inner-html "X"}) 85 | gap-specimen (create-element "span" {:inner-html "X
X"})] 86 | (.appendChild element specimen) 87 | (.appendChild element gap-specimen) 88 | (let [specimen-get-computed-style (fn [k] (-> js/window (.getComputedStyle specimen k) .-value (or 0))) 89 | specimen-frame-height (->> ["margin-top" "border-top" "border-bottom" "margin-bottom"] 90 | (map specimen-get-computed-style) 91 | (reduce +)) 92 | specimen-height (+ specimen-frame-height (.-offsetHeight specimen)) 93 | specimen-frame-width (->> ["margin-left" "border-left" "border-right" "margin-right"] 94 | (map specimen-get-computed-style) 95 | (reduce +)) 96 | specimen-width (+ specimen-frame-width (.-offsetWidth specimen)) 97 | ;; The size of the gap between two lines is required for an 98 | ;; accurate computation of the height of lines. It seems to 99 | ;; depend on the selected font. 100 | gap (- (+ (.-offsetHeight gap-specimen) 101 | specimen-frame-height) 102 | (* 2 specimen-height)) ;; ??? 103 | ] 104 | (.removeChild element specimen) 105 | (.removeChild element gap-specimen) 106 | {:width specimen-width 107 | :height specimen-height 108 | :gap gap}))) 109 | 110 | (defn scrollbar-size 111 | ;; determine the height/width of a horizontal/vertical scrollbar 112 | [] 113 | (let [div (create-element "div" 114 | {:style {:width "100px" 115 | :height "100px" 116 | :overflow-x "scroll" 117 | :overflow-y "scroll"}}) 118 | content (create-element "div" 119 | {:style {:width "200px" 120 | :height "200px"}})] 121 | (.appendChild div content) 122 | (-> js/document .-body (.appendChild div)) 123 | 124 | (let [h (- 100 (.-clientHeight div)) 125 | v (- 100 (.-clientWidth div))] 126 | (-> js/document .-body (.removeChild div)) 127 | {:vertical (if (< 0 v) v 0) 128 | :horizontal (if (< 0 h) h 0)}))) 129 | 130 | (defn select 131 | "Return the first DOM element matching selector. 132 | 133 | parent defaults to js/document when the first element of args is not 134 | a DOM element 135 | 136 | Example: (select parent 'div.container) 137 | (select parent 'div.container 'span)" 138 | [& args] 139 | (let [[element selector] 140 | (if (-> args first .-querySelector) 141 | [(first args) (rest args)] 142 | [js/document args])] 143 | (-> element (.querySelector (string/join \ (map name selector)))))) 144 | 145 | (defn document-ready 146 | "Execute f when the documents readyState changes to complete or is ready." 147 | [f] 148 | (if (= (.-readyState js/document) "complete") 149 | (f) 150 | (.addEventListener js/document "readystatechange" 151 | (fn [] (when (= (.-readyState js/document) "complete") 152 | (f)))))) 153 | 154 | (defn show 155 | "Show or hide the given DOM element." 156 | [element show] 157 | (set! (-> element .-style .-display) 158 | (if show "block" "none"))) 159 | -------------------------------------------------------------------------------- /cljs/src/cljs/schirm_cljs/keys.cljs: -------------------------------------------------------------------------------- 1 | (ns schirm-cljs.keys 2 | (:require [clojure.string :as string])) 3 | 4 | ;; map browser key codes to Gtk key names used in schirm 5 | ;; see termkey.py 6 | (def known-keys {33 "Page_Up", 7 | 34 "Page_Down", 8 | 35 "End", 9 | 36 "Home", 10 | 45 "Insert", 11 | 46 "Delete", 12 | 13 | 37 "Left", 14 | 38 "Up", 15 | 39 "Right", 16 | 40 "Down", 17 | 18 | 32 "Space", 19 | 8 "BackSpace", 20 | 9 "Tab", 21 | 13 "Enter", 22 | 27 "Esc", 23 | 24 | 112 "F1", 25 | 113 "F2", 26 | 114 "F3", 27 | 115 "F4", 28 | 116 "F5", 29 | 117 "F6", 30 | 118 "F7", 31 | 119 "F8", 32 | 120 "F9", 33 | 121 "F10", 34 | 122 "F11", 35 | 123 "F12"}) 36 | 37 | (defn get-key-chord [key] 38 | (->> [(when (:shift key) :shift), 39 | (when (:control key) :control), 40 | (when (:alt key ) :alt), 41 | (string/lower-case (when-let [k (or (:name key) 42 | (and (not= (:string key) "") (:string key)) 43 | (.fromCharCode js/String (:code key)))] (keyword k)))] 44 | (filter identity))) 45 | 46 | (defn handle-key-down [chords env key] 47 | (let [ascii-a 65 48 | ascii-z 90 49 | chord (get-key-chord key) 50 | handler (get chords chord) 51 | send-key (:send-key env)] 52 | (cond ;; key chords 53 | handler 54 | (handler env) 55 | ;; catch (control|alt)-* sequences 56 | (and (or (:control key) (:alt key)) 57 | (<= ascii-a (:code key) ascii-z)) 58 | (do (send-key (assoc key :name (.fromCharCode js/String (:code key)))) 59 | true) 60 | ;; special keys 61 | (:name key) 62 | (do (send-key key) 63 | true) 64 | :else false 65 | ))) 66 | 67 | (defn setup-window-key-handlers [element chords env] 68 | (let [key-down-processed (atom false) 69 | chords (into {} (map (fn [[k,v]] [(map string/lower-case k) v]) chords))] 70 | (set! (.-onkeydown element) 71 | (fn [e] 72 | (let [key {:name (get known-keys (.-keyCode e)) 73 | :code (.-keyCode e) 74 | :string "", 75 | :shift (.-shiftKey e) 76 | :alt (.-altKey e) 77 | :control (.-ctrlKey e)} 78 | processed (handle-key-down chords env key)] 79 | (reset! key-down-processed processed) 80 | (not processed)))) 81 | (set! (.-onkeypress element) 82 | (fn [e] 83 | (let [key {:name nil 84 | :string (.fromCharCode js/String (.-charCode e)), 85 | :shift (.-shiftKey e) 86 | :alt (.-altKey e) 87 | :control (.-ctrlKey e)}] 88 | (if (and (:string key) (not @key-down-processed) (not (handle-key-down chords env key))) 89 | (do ((:send-key env) key) 90 | true) 91 | false)))) 92 | (set! (.-onkeyup element) 93 | (fn [e] (reset! key-down-processed false))))) -------------------------------------------------------------------------------- /cljs/src/cljs/schirm_cljs/screen-tests.cljs: -------------------------------------------------------------------------------- 1 | (ns schirm-cljs.screen-tests 2 | (:require [clojure.string :as string] 3 | [schirm-cljs.screen :as screen] 4 | [schirm-cljs.dom-utils :as dom-utils])) 5 | 6 | (defn readable-styled-string [styled-string] 7 | (apply vector 8 | (:string styled-string) 9 | (sort (map keyword (remove empty? (-> styled-string :style screen/get-class-string (string/split \ ))))))) 10 | 11 | (defn into-styled-string [s & properties] 12 | (screen/StyledString. s (screen/get-style-from-classnames (map name properties)))) 13 | 14 | (defn line->readable [line] 15 | (->> line 16 | (map readable-styled-string) 17 | (into []))) 18 | 19 | (defn readable->line 20 | [readable-line] 21 | (vec (map #(apply into-styled-string %) readable-line))) 22 | 23 | (defn append-readable-line [parent line] 24 | (let [line-element (screen/create-line (readable->line line))] 25 | (.appendChild parent line-element) 26 | line-element)) 27 | 28 | (defn test-dom-line-op [line, expected, f] 29 | (when-not (dom-utils/select 'pre) (throw 'pre-node-is-missing)) 30 | (let [line-element (append-readable-line (dom-utils/select 'pre) line)] 31 | (f line-element) ;; modifies the DOM 32 | (let [a (screen/read-line line-element) 33 | b (readable->line expected)] 34 | (if (= a b) 35 | :pass 36 | {:line line 37 | :expected expected 38 | :got (-> line-element screen/read-line line->readable)})))) 39 | 40 | (defn run-tests [] 41 | (set! (.-innerText (dom-utils/select 'pre)) "") 42 | [ 43 | ;; insert 44 | 45 | (test-dom-line-op [] 46 | [["ABC"]] 47 | #(screen/line-insert % (into-styled-string "ABC") 0)) 48 | (test-dom-line-op [] 49 | [[" ABC"]] 50 | #(screen/line-insert % (into-styled-string "ABC") 3)) 51 | (test-dom-line-op [["A" :f-red]] 52 | [["AB" :f-red]] 53 | #(screen/line-insert % (into-styled-string "B" :f-red) 1)) 54 | (test-dom-line-op [["A" :f-red]] 55 | [["BA" :f-red]] 56 | #(screen/line-insert % (into-styled-string "B" :f-red) 0)) 57 | (test-dom-line-op [["A" :f-red]] 58 | [["A" :f-red] 59 | [" "] 60 | ["B" :f-red]] 61 | #(screen/line-insert % (into-styled-string "B" :f-red) 5)) 62 | (test-dom-line-op [["ABC" :f-red]] 63 | [["ABxC" :f-red]] 64 | #(screen/line-insert % (into-styled-string "x" :f-red) 2)) 65 | (test-dom-line-op [["ABC" :f-red]] 66 | [["AB" :f-red] ["xx" :f-blue :bold] ["C" :f-red]] 67 | #(screen/line-insert % (into-styled-string "xx" :f-blue :bold) 2)) 68 | (test-dom-line-op [["ABC" :f-red] ["DEF" :f-blue]] 69 | [["ABC" :f-red] ["xxDEF" :f-blue]] 70 | #(screen/line-insert % (into-styled-string "xx" :f-blue) 3)) 71 | 72 | (test-dom-line-op [["ABC" :f-red] ["DEF" :f-blue]] 73 | [["ABCxx" :f-red] ["DEF" :f-blue]] 74 | #(screen/line-insert % (into-styled-string "xx" :f-red) 3)) 75 | (test-dom-line-op [["ABC" :f-red] ["DEF" :f-blue]] 76 | [["ABC" :f-red] ["xxDEF" :f-blue]] 77 | #(screen/line-insert % (into-styled-string "xx" :f-blue) 3)) 78 | 79 | ;; remove 80 | 81 | (test-dom-line-op [["ABC" :f-blue] ["DEF" :f-green] ["GHI" :f-red]] 82 | [["AB" :f-blue] ["HI" :f-red]] 83 | #(screen/line-remove % 2 5)) 84 | 85 | (test-dom-line-op [["ABC" :f-blue] ["DEF" :f-red]] 86 | [["AB" :f-blue] ["F" :f-red]] 87 | #(screen/line-remove % 2 3)) 88 | 89 | (test-dom-line-op [["ABC" :f-blue] ["DEF" :f-red]] 90 | [["A" :f-blue]] 91 | #(screen/line-remove % 1 5)) 92 | 93 | (test-dom-line-op [["ABC" :f-blue] ["DEF" :f-red]] 94 | [["F" :f-red]] 95 | #(screen/line-remove % 0 5)) 96 | 97 | (test-dom-line-op [["A" :f-blue] ["B" :f-red] ["C" :f-blue]] 98 | [["AC" :f-blue]] 99 | #(screen/line-remove % 1 1)) 100 | 101 | (test-dom-line-op [["ABC" :f-blue] ["D" :f-red] ["E" :f-bold] ["FG" :f-blue]] 102 | [["ABCG" :f-blue]] 103 | #(screen/line-remove % 3 3)) 104 | 105 | (test-dom-line-op [["AB" :f-blue] ["C" :f-blue :cursor] ["DEF" :f-blue]] 106 | [["ABDEF" :f-blue]] 107 | #(screen/line-remove % 2 1)) 108 | 109 | (test-dom-line-op [["ABDEF" :f-blue]] 110 | [["ABCDEF" :f-blue]] 111 | #(screen/line-insert % (into-styled-string "C" :f-blue) 2)) 112 | 113 | ;; insert-overwrite 114 | 115 | (test-dom-line-op [["ABC" :f-blue] ["DEF" :f-red]] 116 | [["Axxx" :f-blue] ["EF" :f-red]] 117 | #(screen/line-insert-overwrite % (into-styled-string "xxx" :f-blue) 1)) 118 | 119 | (test-dom-line-op [["ABC" :f-blue] ["DEF" :f-red]] 120 | [["A" :f-blue] ["xxxEF" :f-red]] 121 | #(screen/line-insert-overwrite % (into-styled-string "xxx" :f-red) 1)) 122 | 123 | (test-dom-line-op [["foo:" :f-green :b-default] ["$ " :f-default :b-default]] 124 | [["foo:" :f-green :b-default] ["$ wxyz" :f-default :b-default]] 125 | #(do 126 | (screen/line-insert-overwrite % (into-styled-string "w" :f-default :b-default) 6) 127 | (screen/line-insert-overwrite % (into-styled-string "x" :f-default :b-default) 7) 128 | (screen/line-insert-overwrite % (into-styled-string "y" :f-default :b-default) 8) 129 | (screen/line-insert-overwrite % (into-styled-string "z" :f-default :b-default) 9))) 130 | 131 | (test-dom-line-op [["pre" :f-default :b-default] 132 | ["ABC" :f-blue :b-default] 133 | ["suf" :f-default :b-default]] 134 | [["pre" :f-default :b-default] 135 | ["A" :f-blue :b-default] 136 | ["+" :f-red :b-default] 137 | ["C" :f-blue :b-default] 138 | ["suf" :f-default :b-default]] 139 | (fn [line] (screen/line-insert-overwrite line (into-styled-string "+", :f-red) 4))) 140 | 141 | ;; cursor 142 | 143 | (test-dom-line-op [["ABCDEF" :f-blue]] 144 | [["AB" :f-blue] ["C" :f-blue :cursor] ["DEF" :f-blue]] 145 | #(screen/line-set-cursor % 2)) 146 | 147 | (test-dom-line-op [["112233 " :f-blue]] 148 | [["1122" :f-blue] ["3" :f-blue :cursor] ["3 " :f-blue]] 149 | #(do 150 | ;;(.log js/console "foo") 151 | (screen/line-set-cursor % 6) 152 | (screen/line-remove-cursor %) 153 | (screen/line-set-cursor % 5) 154 | (screen/line-remove-cursor %) 155 | (screen/line-set-cursor % 4))) 156 | 157 | (test-dom-line-op [["AB" :f-blue] ["C" :f-blue :cursor] ["DEF" :f-blue]] 158 | [["ABCDEF" :f-blue]] 159 | #(screen/line-remove-cursor %)) 160 | ]) 161 | -------------------------------------------------------------------------------- /cljs/src/cljs/schirm_cljs/term.cljs: -------------------------------------------------------------------------------- 1 | (ns schirm-cljs.term 2 | (:require [cljs.core.async :as async 3 | :refer [! chan close! sliding-buffer put! alts!]] 4 | 5 | [clojure.string :as string] 6 | 7 | [clojure.browser.repl :as repl] 8 | 9 | [schirm-cljs.screen-tests :as tests] 10 | 11 | [schirm-cljs.screen :as screen] 12 | [schirm-cljs.keys :as keys] 13 | [schirm-cljs.dom-utils :as dom-utils] 14 | [schirm-cljs.word-select :as word-select] 15 | [schirm-cljs.copy-n-paste-hack :as copy-n-paste-hack]) 16 | 17 | (:require-macros [cljs.core.async.macros :as m :refer [go alt!]])) 18 | 19 | ;; events -> chan 20 | ;; socket-messages -> chan 21 | 22 | (defn create-styled-string [string attrs] 23 | (screen/StyledString. string (apply screen/->CharacterStyle attrs))) 24 | 25 | (defn create-fragment-from-lines 26 | "Create a document fragment from a seq of seqs of raw segments. 27 | 28 | Raw segments are tuples of (string, class-string) forming the basic 29 | parts of a line." 30 | [lines] 31 | (let [fragment (.createDocumentFragment js/document)] 32 | (doseq [raw-segments lines] 33 | (let [line (.createElement js/document "div")] 34 | (doseq [[string class] (if (empty? raw-segments) 35 | [[" " ""]] ;; empty line 36 | raw-segments)] 37 | (let [segment (.createElement js/document "span")] 38 | (set! (.-className segment) class) 39 | (set! (.-textContent segment) string) 40 | (.appendChild line segment))) 41 | (.appendChild fragment line))) 42 | fragment)) 43 | 44 | (defn create-iframe [id] 45 | (let [scroll-size (dom-utils/scrollbar-size) 46 | uri (format "http://%s.localhost" id)] 47 | (dom-utils/create-element 48 | "iframe" 49 | {:style {:width "100%", 50 | :min-height (:vertical scroll-size) 51 | :height (:vertical scroll-size)} 52 | :src uri 53 | :id id}))) 54 | 55 | (def iframe-menu-thumb-svg " 56 | 57 | 59 | 60 | ") 61 | 62 | (defn invoke-screen-method [state scrollback-screen alt-screen msg] 63 | (let [[meth & args] msg 64 | screen (if (:alt-mode state) alt-screen scrollback-screen)] 65 | (case meth 66 | "set-line-origin" (do (apply screen/set-origin screen args) 67 | state) 68 | "reset" (do (screen/reset scrollback-screen (nth args 0)) 69 | (screen/reset alt-screen (nth args 0)) 70 | state) 71 | "resize" (do (screen/set-size scrollback-screen (nth args 0) (nth args 1)) 72 | (screen/set-size alt-screen (nth args 0) (nth args 1)) 73 | state) 74 | "insert" (let [[line, col, string, attrs] args 75 | ss (create-styled-string string attrs)] 76 | (screen/update-line screen 77 | line 78 | #(screen/line-insert % ss col)) 79 | state) 80 | "insert-overwrite" (let [[line, col, string, attrs] args 81 | ss (create-styled-string string attrs)] 82 | (screen/update-line screen 83 | line 84 | #(screen/line-insert-overwrite % ss col)) 85 | state) 86 | "remove" (let [[line, col, n] args] 87 | (screen/update-line screen 88 | line 89 | #(screen/line-remove % col n)) 90 | state) 91 | "insert-line" (do (screen/insert-line screen (screen/create-line [(screen/default-styled-string 1)]) (nth args 0)) 92 | state) 93 | "append-line" (do (screen/append-line screen (screen/create-line [(screen/default-styled-string 1)])) 94 | state) 95 | "append-many-lines" (let [lines (nth args 0 [])] 96 | (screen/append-line screen (create-fragment-from-lines lines)) 97 | state) 98 | "remove-line" (do (screen/remove-line screen (nth args 0)) 99 | state) 100 | "adjust" (do (screen/adjust screen) 101 | state) 102 | "cursor" (let [[line, col] args] 103 | (screen/set-cursor screen line col) 104 | state) 105 | "enter-alt-mode" (do (screen/show scrollback-screen false) 106 | (screen/show alt-screen true) 107 | (assoc state :alt-mode true)) 108 | "leave-alt-mode" (do (screen/show scrollback-screen true) 109 | (screen/show alt-screen false) 110 | (screen/reset alt-screen) 111 | (assoc state :alt-mode false)) 112 | 113 | "scrollback-cleanup" (do (screen/scrollback-cleanup screen (nth args 0)) 114 | state) 115 | "set-title" (do (set! (.-title js/document) (nth args 0)) 116 | state) 117 | 118 | ;; iframe 119 | "iframe-enter" (let [[iframe-id pos] args 120 | menu-thumb (dom-utils/create-element-from-string iframe-menu-thumb-svg) 121 | iframe (create-iframe iframe-id)] 122 | (screen/update-line screen pos 123 | (fn [l] 124 | (set! (.-innerHTML l) "") 125 | ;; make the line position:relative so the close div 126 | ;; appears on the upper left over the iframe 127 | (-> l .-classList (.add "iframe-line")) 128 | (-> l .-classList (.add "iframe-line-active")) 129 | (.appendChild l menu-thumb) 130 | (.appendChild l iframe) 131 | (.focus iframe))) 132 | (.addEventListener iframe "webkitTransitionEnd" #(screen/auto-scroll screen)) 133 | state) 134 | 135 | "iframe-leave" (do (doseq [l (array-seq (.querySelectorAll js/document ".iframe-line-active"))] 136 | (-> l .-classList (.remove "iframe-line-active"))) 137 | state) 138 | 139 | "iframe-resize" (let [[iframe-id height] args 140 | iframe (.getElementById js/document iframe-id) 141 | height-style (if (== height "fullscreen") "100%" (format "%spx" height))] 142 | (when iframe (-> iframe .-style .-height (set! height-style))) 143 | (.setTimeout js/window #(screen/auto-scroll screen)) 144 | state) 145 | 146 | "iframe-set-url" (let [[iframe-id url] args 147 | iframe (.getElementById js/document iframe-id)] 148 | (set! (.-src iframe) url) 149 | state) 150 | 151 | ;; debug 152 | "start-clojurescript-repl" (do (repl/connect "http://localhost:9000/repl") 153 | state) 154 | ))) 155 | 156 | (def chords {;; paste xselection 157 | [:shift :insert] 158 | (fn [{:keys [send]}] 159 | ;; paste the primary x selection using `xsel` 160 | (send {:name "paste_selection"})) 161 | [:control :v] 162 | (fn [{:keys [send]}] (copy-n-paste-hack/key-paste #(send {:name "paste_selection" :string %})) true) 163 | ;; scrolling 164 | [:shift :page_up] (fn [env] (screen/scroll :page-up) true) 165 | [:shift :page_down] (fn [env] (screen/scroll :page-down) true) 166 | [:shift :home] (fn [env] (screen/scroll :top) true) 167 | [:shift :end] (fn [env] (screen/scroll :bottom) true) 168 | ;; browsers have space and shift-space bound to scroll page down/up 169 | [:space] (fn [{:keys [send-key]}] (send-key {:string " "})) 170 | [:shift :space] (fn [{:keys [send-key]}] (send-key {:string " "})) 171 | ;; ignore F12 as this opens the browsers devtools 172 | [:F12] (fn [env] false) 173 | ;; zoom 174 | [:control :+] (fn [{:keys [send]}] (send {:name "zoom_increase"})) 175 | [:control :-] (fn [{:keys [send]}] (send {:name "zoom_decrease"})) 176 | [:control :0] (fn [{:keys [send]}] (send {:name "zoom_default"}))}) 177 | 178 | (defn setup-keys [send-chan send-callback] 179 | (let [send (fn [message] 180 | (put! send-chan (.stringify js/JSON (clj->js message))) 181 | ;; return true to indicate successful chord-handling 182 | true) 183 | send-key (fn [key] 184 | (when send-callback (send-callback)) 185 | (let [message {:name :keypress :key key}] 186 | (send message))) 187 | ;; an environment to implement key-chord actions 188 | env {:send-key send-key 189 | :send send}] 190 | (keys/setup-window-key-handlers js/window chords env))) 191 | 192 | (defn setup-screens [parent-element input-chan] 193 | (let [[scrollback-screen alt-screen :as screens] (screen/create-screens parent-element) 194 | state (atom {})] 195 | (screen/show alt-screen false) 196 | (go 197 | (loop [] 198 | (doseq [message (js (assoc new-size :name :resize))] 207 | (put! ws-send (.stringify js/JSON message))))] 208 | (set! (.-onresize js/window) resize-screen) 209 | (resize-screen))) 210 | 211 | (defn setup-word-select [screens] 212 | (doseq [s screens] 213 | (.addEventListener (.-element s) 214 | "dblclick" 215 | #(word-select/select-word % s)))) 216 | 217 | (defn setup-iframe-focus [] 218 | ;;; TODO: automatically focus active iframes 219 | (.addEventListener js/window "focus" (fn [] 220 | (when-let [e (-> js/document (.querySelector "iframe.focus"))] 221 | (-> e .-classList (.remove "focus"))))) 222 | (.addEventListener js/window "blur" (fn [] 223 | (.setTimeout js/window (fn [] 224 | (when (-> js/document .-activeElement .-tagName (= "IFRAME")) 225 | (-> js/document .-activeElement .-classList (.add "focus")))) 226 | 0)))) 227 | 228 | (defn setup-websocket [url in out] 229 | (let [ws (js/WebSocket. url)] 230 | (set! (.-onmessage ws) 231 | (fn [ev] 232 | (if (not= "" (.-data ev)) 233 | (put! out (.parse js/JSON (.-data ev)))))) 234 | (set! (.-onopen ws) 235 | #(go 236 | (loop [] 237 | (let [msg (js {:name "paste_selection" :string %}))))) 247 | 248 | (defn setup-iframe-menu-thumb 249 | "The iframe-menu-thumb provides control over applications running in frame mode. 250 | 251 | Analogous to control flow keyboard shortcuts, it is able to 'close' 252 | the iframe (making the app to leave frame mode) or to 'kill' the app 253 | by sending SIGINT (CTRL-C)." 254 | [ws-send screens] 255 | (let [a 1] 256 | (doseq [s screens] 257 | (.addEventListener (.-element s) 258 | "click" 259 | (fn [e] 260 | (let [target (-> e .-target) 261 | line? #(and (-> % .-classList) 262 | (-> % .-classList (.contains "iframe-line")))] 263 | ;; TODO: after qtwebkit update, check whether svg elements support .-classList! 264 | (when (and (or (-> target .-tagName (== "svg")) 265 | (-> target .-tagName (== "path"))) 266 | (or (-> target .-parentElement line?) 267 | (-> target .-parentElement .-parentElement line?) 268 | (-> target .-parentElement .-parentElement .-parentElement line?))) 269 | (let [message (clj->js {:name "iframe_request_close"})] 270 | (put! ws-send (.stringify js/JSON message)))))))))) 271 | 272 | (defn setup-terminal [] 273 | (let [ws-send (chan) 274 | ws-recv (chan) 275 | ws-url (format "ws://%s" (-> js/window .-location .-host)) 276 | container (dom-utils/select 'body) 277 | screens (setup-screens container ws-recv)] 278 | (setup-keys ws-send 279 | ;; enable auto-scroll when typing non-chord keys 280 | ;; == sending keystrokes to the terminal 281 | #(screen/auto-scroll (nth screens 0) true)) 282 | (setup-word-select screens) 283 | (setup-iframe-focus) 284 | (setup-iframe-menu-thumb ws-send screens) 285 | (setup-right-click container ws-send) 286 | (setup-resize container ws-send screens) 287 | (setup-websocket ws-url ws-send ws-recv))) 288 | 289 | (defn init [] 290 | (dom-utils/document-ready setup-terminal)) 291 | 292 | (defn tests [] 293 | (dom-utils/document-ready (fn [] 294 | (doseq [result (tests/run-tests)] 295 | (.log js/console (str result)))))) 296 | 297 | (init) 298 | -------------------------------------------------------------------------------- /cljs/src/cljs/schirm_cljs/word-select.cljs: -------------------------------------------------------------------------------- 1 | (ns schirm-cljs.word-select 2 | (:require [schirm-cljs.dom-utils :as dom-utils])) 3 | 4 | (defn select-in-line 5 | "Select a substring on a line." 6 | [line-element start-idx end-idx] 7 | (let [s (.getSelection js/document) 8 | r (.createRange js/document) 9 | [start-elem start-pos] (dom-utils/element-at-pos line-element start-idx) 10 | [end-elem end-pos] (dom-utils/element-at-pos line-element end-idx)] 11 | (if start-elem 12 | (.setStart r (-> start-elem .-childNodes (aget 0)) start-pos) 13 | (.setStartBefore r (.-firstChild line-element))) 14 | (if end-elem 15 | (.setEnd r (-> end-elem .-childNodes (aget 0)) end-pos) 16 | (.setEndAfter r (.-lastChild line-element))) 17 | (.removeAllRanges s) 18 | (.addRange s r))) 19 | 20 | (defn word-boundaries 21 | "Detect boundaries of the word at idx in string. 22 | 23 | Use char-regex to define which characters make up a word. 24 | Returns nil or {:start number, :end number} when no word can be found." 25 | [string, idx, char-regex] 26 | (when (nth string idx nil) 27 | (let [char-regex (if (nil? char-regex) 28 | #"[-,.\\/?%&#:_~A-Za-z0-9]" 29 | char-regex) 30 | find-word-boundary (fn [indexes] 31 | (->> indexes 32 | (map #(when (.test char-regex (nth string %)) %)) 33 | (take-while #(not (nil? %))) 34 | last))] 35 | (when (.test char-regex (nth string idx)) 36 | {:start (or (find-word-boundary (range idx -1 -1)) idx) 37 | :end (if-let [i (find-word-boundary (range idx (count string)))] 38 | (inc i) 39 | idx)})))) 40 | 41 | (defn select-word 42 | "Select the word on the line that has been double-clicked." 43 | [event screen] 44 | (when (or (-> event .-target .-tagName (= "SPAN")) 45 | (-> event .-target .-parentElement .-tagName (= "DIV"))) 46 | (let [line (-> event .-target .-parentElement) 47 | rel-pos (/ (.-clientX event) (.-offsetWidth line)) 48 | text (.-innerText line) 49 | idx (.round js/Math (* (.-width screen) rel-pos)) 50 | boundaries (word-boundaries text idx)] 51 | (when (and boundaries (<= (:start boundaries) (:end boundaries))) 52 | (select-in-line line (:start boundaries) (:end boundaries))))) 53 | true) 54 | -------------------------------------------------------------------------------- /cljs/src/cljs/wb/wb.cljs: -------------------------------------------------------------------------------- 1 | (ns wb.wb ;; workbench 2 | (:use [schirm-cljs.dom-utils :only [select]]) 3 | (:require [clojure.string :as string] 4 | 5 | [cljs.core.async :as async 6 | :refer [! chan close! sliding-buffer put! alts!]] 7 | 8 | [schirm-cljs.screen :as screen] 9 | [schirm-cljs.screen-tests :as screen-tests] 10 | [schirm-cljs.dom-utils :as dom-utils]) 11 | 12 | (:require-macros [cljs.core.async.macros :as m :refer [go alt!]])) 13 | 14 | (defn reload [] 15 | (let [delay-in-ms 100] 16 | (js/setTimeout (fn [] (-> js/window .-location .reload)) delay-in-ms) 17 | (symbol (format "reloading-in-%s-milliseconds" delay-in-ms)))) 18 | -------------------------------------------------------------------------------- /cljs/test/schirm_cljs/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns schirm-cljs.core-test 2 | (:require [clojure.test :refer :all] 3 | [schirm-cljs.core :refer :all])) 4 | 5 | (deftest a-test 6 | (testing "FIXME, I fail." 7 | (is (= 0 1)))) 8 | -------------------------------------------------------------------------------- /doc/blockdiagram.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoeck/schirm/1007c95283e3804e9b634f1510b6f4b0930b6018/doc/blockdiagram.dia -------------------------------------------------------------------------------- /doc/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 31 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /doc/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoeck/schirm/1007c95283e3804e9b634f1510b6f4b0930b6018/doc/demo.gif -------------------------------------------------------------------------------- /doc/protocol.txt: -------------------------------------------------------------------------------- 1 | 2 | # enter iframe mode 3 | CSI ? 5151 ; h 4 | 5 | # all writes go to the iframes root document 6 | 7 | # transmit resource 8 | SOS (start-of-string, ESC X) 9 |
10 | 11 | 12 | ST (string terminator ESC \) 13 | 14 | HEADER: 15 | HTTP, custom fields to name the resource, 16 | if the first char is a '{', the header is a JSON object literal 17 | 18 | x-schirm-message: 19 | x-schirm-resource: 20 | 21 | BODY: 22 | 23 | base64 encoded data 24 | 25 | # request end 26 | ST 27 | 28 | # leave iframe mode 29 | CSI ? 5151 l 30 | -------------------------------------------------------------------------------- /doc/recordschirmdemo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | NAME='demo' 4 | WINDOWID=$(xdotool selectwindow) 5 | 6 | # video friendly size 7 | xdotool windowsize $WINDOWID 480 129 8 | 9 | # record 10 | recordmydesktop --windowid=$WINDOWID --no-sound --no-cursor --no-wm-check --fps=10 --height=128 -y 1 -o $NAME.ogv 11 | 12 | # single images 13 | mplayer -ao null