├── server ├── Makefile └── server.coffee ├── .gitignore ├── chrome-extension ├── icons │ ├── icon-128.png │ ├── icon-16.png │ ├── icon-19.png │ ├── icon-38.png │ ├── icon-48.png │ └── icon-512.png ├── manifest.json ├── options.css ├── common.coffee ├── background.coffee ├── foreground.coffee ├── options.coffee └── options.html ├── package.json ├── LICENSE ├── Makefile └── README.md /server/Makefile: -------------------------------------------------------------------------------- 1 | 2 | run-server: 3 | cd ..; make $@ 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | text-aid-too.zip 3 | node_modules 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /chrome-extension/icons/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/text-aid-too/HEAD/chrome-extension/icons/icon-128.png -------------------------------------------------------------------------------- /chrome-extension/icons/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/text-aid-too/HEAD/chrome-extension/icons/icon-16.png -------------------------------------------------------------------------------- /chrome-extension/icons/icon-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/text-aid-too/HEAD/chrome-extension/icons/icon-19.png -------------------------------------------------------------------------------- /chrome-extension/icons/icon-38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/text-aid-too/HEAD/chrome-extension/icons/icon-38.png -------------------------------------------------------------------------------- /chrome-extension/icons/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/text-aid-too/HEAD/chrome-extension/icons/icon-48.png -------------------------------------------------------------------------------- /chrome-extension/icons/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/text-aid-too/HEAD/chrome-extension/icons/icon-512.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "text-aid-too", 3 | "version": "1.1.7", 4 | "author": "Stephen Blott ", 5 | "description": "Edit web inputs (including on GMail) with your favourite native text editor; and (experimentally) use markdown.", 6 | "homepage": "https://github.com/smblott-github/text-aid-too", 7 | "repository": { "type": "git", "url": "https://github.com/smblott-github/text-aid-too" }, 8 | "licenses" : [ {"type" : "MIT" } ], 9 | "engines" : { "node" : ">=0.4" }, 10 | "keywords": [ "text", "aid", "extension", "chrome", "edit", "vim" ], 11 | "dependencies": { 12 | "ws" : "~0.4.31", 13 | "optimist" : "~0.6.1", 14 | "watchr": "~2.4.13", 15 | "coffee-script": "~1.9.3", 16 | "markdown": "~0.5.0", 17 | "html": "~0.0.10" 18 | }, 19 | "bin": { "text-aid-too": "server/server.coffee" } 20 | } 21 | -------------------------------------------------------------------------------- /chrome-extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Text-Aid-Too", 4 | "version": "1.1.1", 5 | "description": "Edit web inputs (including on GMail) with your favourite native text editor.", 6 | 7 | "background": { "scripts": [ "common.js", "background.js" ] }, 8 | "options_page": "options.html", 9 | 10 | "content_scripts": [ { 11 | "matches": [ "" ], 12 | "js": [ "common.js", "foreground.js" ], 13 | "run_at": "document_idle", 14 | "all_frames": true } ], 15 | 16 | "icons": { "16": "icons/icon-16.png", 17 | "48": "icons/icon-48.png", 18 | "128": "icons/icon-128.png" }, 19 | 20 | "page_action": { 21 | "default_icon": { 22 | "19": "icons/icon-19.png", 23 | "38": "icons/icon-38.png" 24 | }, 25 | "default_title": "Text-Aid-Too" }, 26 | 27 | "permissions": [ "storage" ] 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Stephen Blott 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /chrome-extension/options.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | font: 14px "DejaVu Sans", "Arial", sans-serif; 4 | color: #303942; 5 | margin: 0 auto; 6 | } 7 | 8 | div#wrapper { 9 | width: 700px; 10 | margin-left: 35px; 11 | } 12 | 13 | header { 14 | font-size: 18px; 15 | font-weight: normal; 16 | border-bottom: 1px solid #eee; 17 | padding: 20px 0 15px 0; 18 | width: 100%; 19 | } 20 | 21 | #options { 22 | width: 100%; 23 | } 24 | 25 | ul { 26 | list-style-type: none; 27 | margin: 0; 28 | } 29 | 30 | li { 31 | padding: 5px; 32 | } 33 | 34 | input[type="text"], input[type="number"], textarea { 35 | border: 1px solid #bfbfbf; 36 | border-radius: 2px; 37 | color: #444; 38 | font: inherit; 39 | padding: 3px; 40 | width: 100%; 41 | } 42 | 43 | .optionName { 44 | max-width: 65px; 45 | } 46 | 47 | input, textarea { 48 | box-sizing: border-box; 49 | } 50 | 51 | button:focus, input[type="text"]:focus, textarea:focus { 52 | -webkit-transition: border-color 200ms; 53 | border-color: #4d90fe; 54 | outline: none; 55 | } 56 | 57 | pre, code, .code, #status { 58 | font-family: Consolas, "Liberation Mono", Courier, monospace; 59 | background: #eeeeee; 60 | } 61 | 62 | #statusWrapper, #defaultKey { 63 | float: right; 64 | } 65 | -------------------------------------------------------------------------------- /chrome-extension/common.coffee: -------------------------------------------------------------------------------- 1 | 2 | Common = 3 | 4 | # Default values. 5 | default: 6 | key: 7 | altKey: false 8 | ctrlKey: true 9 | shiftKey: false 10 | keyCode: 186 # ";" 11 | 12 | port: "9293" 13 | secret: "BETTER-FIX-ME-ON-THE-OPTIONS-PAGE" 14 | 15 | # Give objects (including elements) distinct identities. 16 | identity: do -> 17 | identities = [] 18 | getId: (obj) -> 19 | index = identities.indexOf obj 20 | if index < 0 21 | index = identities.length 22 | identities.push obj 23 | index 24 | getObj: (id) -> identities[id] 25 | 26 | # Convenience wrapper for setTimeout (with the arguments around the other way). 27 | setTimeout: (ms, func) -> setTimeout func, ms 28 | 29 | # Like Nodejs's nextTick. 30 | nextTick: (func) -> @setTimeout 0, func 31 | 32 | # Extend an object with additional properties. 33 | extend: (hash1, hash2) -> 34 | hash1[key] = value for own key, value of hash2 35 | hash1 36 | 37 | chromeStoreKey: "klbcooigafjpbiahdjccmajnaehomajc" 38 | 39 | isChromeStoreVersion: do -> 40 | 0 == chrome.extension.getURL("").indexOf "chrome-extension://klbcooigafjpbiahdjccmajnaehomajc" 41 | 42 | log: (args...) -> 43 | console.log args... unless @isChromeStoreVersion 44 | 45 | root = exports ? window 46 | root.Common = Common 47 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | build: 3 | coffee -c ./chrome-extension/*.coffee ./server/server.coffee 4 | 5 | auto: 6 | coffee -w -c . 7 | 8 | install: 9 | sudo npm install -g . 10 | 11 | # Run a test version of the server. It uses a different port from the default 12 | # port, so it doesn't conflict with the live server. Note that you'll have to 13 | # set the port and the secret below within the extension. 14 | run-server: 15 | TEXT_AID_TOO_SECRET=hul8quahJ4eeL1Ib \ 16 | TEXT_AID_TOO_EDITOR="urxvt -T textaid -geometry 90x25-50+15 -e vim" \ 17 | coffee server/server.coffee --port 9294 --markdown 18 | 19 | help-text: 20 | coffee server/server.coffee -h 21 | 22 | # This target is probably of interest to smblott only. 23 | pack-extension: 24 | $(MAKE) build 25 | google-chrome \ 26 | --pack-extension=$(PWD)/chrome-extension \ 27 | --pack-extension-key="$(HOME)/local/sbenv/ssh/text-aid-too.pem" 28 | ls -l chrome-extension.crx 29 | mv -v chrome-extension.crx $(HOME)/storage/google-drive/Extensions/text-aid-too.crx 30 | 31 | .PHONY: build auto pack-extension pack run-server install help-text publish 32 | 33 | # For Chrome Store. 34 | pack: 35 | $(MAKE) build 36 | zip -r text-aid-too.zip chrome-extension \ 37 | -x '*'.coffee \ 38 | -x chrome-extension/icons/icon-512.png 39 | 40 | # For npm. 41 | publish: 42 | $(MAKE) build 43 | npm publish 44 | -------------------------------------------------------------------------------- /chrome-extension/background.coffee: -------------------------------------------------------------------------------- 1 | 2 | getOrSet = (key, value, callback = null) -> 3 | chrome.storage.sync.get key, (items) -> 4 | unless chrome.runtime.lastError 5 | if items[key]? 6 | callback? items[key] 7 | else 8 | obj = {} 9 | obj[key] = value 10 | chrome.storage.sync.set obj 11 | callback? value 12 | 13 | for key in [ "key", "port", "secret" ] 14 | getOrSet key, Common.default[key] 15 | 16 | launchEdit = (request) -> 17 | Common.log request 18 | 19 | getOrSet "port", Common.default.port, (port) -> 20 | port = parseInt port 21 | url = "ws://localhost:#{port}/" 22 | 23 | getOrSet "secret", Common.default.secret, (secret) -> 24 | request.secret = secret 25 | Common.log "send: #{request.tabId} #{request.id} #{url} #{secret} length=#{request.text?.length}" 26 | 27 | socket = new WebSocket url 28 | socket.onerror = socket.onclose = -> 29 | Common.log " done: #{request.tabId} #{request.id} #{url} #{secret}" 30 | socket.close() 31 | 32 | socket.onopen = -> 33 | socket.send JSON.stringify request 34 | 35 | socket.onmessage = (message) -> 36 | response = JSON.parse message.data 37 | Common.log " recv: #{request.tabId} #{request.id} #{url} #{secret} length=#{response.text?.length}" 38 | chrome.tabs.sendMessage response.tabId, response 39 | false # We will not be calling sendResponse. 40 | 41 | launchPing = (request, sender, sendResponse) -> 42 | getOrSet "port", Common.default.port, (port) -> 43 | port = parseInt port 44 | url = "ws://localhost:#{port}/" 45 | 46 | getOrSet "secret", Common.default.secret, (secret) -> 47 | request.secret = secret 48 | Common.log "ping..." 49 | 50 | exit = (isUp) -> 51 | socket.onerror = socket.onclose = socket.onmessage = null 52 | Common.log " #{isUp}" 53 | sendResponse { isUp } 54 | socket.close() 55 | 56 | socket = new WebSocket url 57 | socket.onerror = socket.onclose = -> exit false 58 | socket.onopen = -> socket.send JSON.stringify request 59 | 60 | socket.onmessage = (message) -> 61 | response = JSON.parse message.data 62 | exit response.isOk 63 | 64 | true # We *will* be calling sendResponse. 65 | 66 | updateIcon = (request, sender) -> 67 | Common.log "icon", request.showing 68 | if request.showing 69 | chrome.pageAction.show sender.tab.id 70 | else 71 | chrome.pageAction.hide sender.tab.id 72 | false # We will not be calling sendResponse. 73 | 74 | handlers = 75 | edit: launchEdit 76 | ping: launchPing 77 | icon: updateIcon 78 | 79 | chrome.runtime.onMessage.addListener do -> 80 | extensionVersion = chrome.runtime.getManifest().version 81 | 82 | (request, sender, sendResponse) -> 83 | console.log extensionVersion 84 | Common.log "request", request.name, handlers[request.name]? 85 | if sender.tab?.id? 86 | Common.extend request, 87 | tabId: sender.tab.id 88 | url: sender.tab.url 89 | isChromeStoreVersion: Common.isChromeStoreVersion 90 | extensionVersion: extensionVersion 91 | handlers[request.name]? request, sender, sendResponse 92 | else 93 | false 94 | -------------------------------------------------------------------------------- /chrome-extension/foreground.coffee: -------------------------------------------------------------------------------- 1 | 2 | frame = 1 + Math.floor 999999999 * Math.random() 3 | 4 | # Tests whether this text-aid-too's trigger event. 5 | isTriggerEvent = do -> 6 | properties = [ "altKey", "ctrlKey", "shiftKey", "keyCode" ] 7 | 8 | (key, event) -> 9 | for property in properties 10 | return false unless event[property] == key[property] 11 | true 12 | 13 | contentCache = {} 14 | 15 | getElementContent = (element, id) -> 16 | if contentCache[id]? 17 | contentCache[id] 18 | else if element.isContentEditable 19 | element.innerHTML 20 | else 21 | element.value 22 | 23 | setElementContent = (element, request) -> 24 | contentCache[request.id] = request.originalText if request.originalText? 25 | if element.isContentEditable 26 | element.innerHTML = request.text 27 | else 28 | element.value = request.text 29 | 30 | clearElementContent = (event) -> 31 | if element = getElement() 32 | id = Common.identity.getId element 33 | delete contentCache[id] if id? and contentCache[id]? 34 | true 35 | 36 | # Send a request to edit text. 37 | editElement = (element) -> 38 | id = Common.identity.getId element 39 | chrome.runtime.sendMessage 40 | name: "edit" 41 | text: getElementContent element, id 42 | isContentEditable: element.isContentEditable 43 | id: id 44 | frame: frame 45 | 46 | # Receive edited text. 47 | chrome.runtime.onMessage.addListener (request, sender) -> 48 | if request.frame == frame and element = Common.identity.getObj request.id 49 | setElementContent element, request 50 | false 51 | 52 | # Returns the active element (if it is editable) or null. 53 | getElement = do -> 54 | nonEditableInputs = [ "radio", "checkbox" ] 55 | editableNodeNames = [ "textarea" ] 56 | 57 | (element = document.activeElement) -> 58 | nodeName = element.nodeName?.toLowerCase() 59 | return element if false or 60 | element.isContentEditable or 61 | (nodeName? and nodeName == "input" and element.type not in nonEditableInputs) or 62 | (nodeName? and nodeName in editableNodeNames) 63 | null 64 | 65 | installListener = (element, event, callback) -> 66 | element.addEventListener event, callback, true 67 | 68 | chrome.storage.sync.get "key", (items) -> 69 | unless chrome.runtime.lastError 70 | key = items.key 71 | 72 | # This is the main keyboard-event listener. We check on every keydown because some sites (notably 73 | # Google's Inbox) change the content-editable flag on the fly. 74 | installListener window, "keydown", (event) -> 75 | maintainIcon() 76 | if isTriggerEvent(key, event) and element = getElement() 77 | Common.log "keyboard hit, element:", element 78 | event.preventDefault() 79 | event.stopImmediatePropagation() 80 | editElement element 81 | return false 82 | true 83 | 84 | chrome.storage.onChanged.addListener (changes, area) => 85 | if area == "sync" and changes.key?.newValue? 86 | key = changes.key.newValue 87 | 88 | maintainIcon = do -> 89 | showing = false 90 | -> 91 | changed = false 92 | if not showing and getElement() 93 | showing = true 94 | changed = true 95 | else if showing and not getElement() 96 | showing = false 97 | changed = true 98 | if changed 99 | chrome.runtime.sendMessage name: "icon", showing: showing 100 | Common.log "icon:", showing 101 | true 102 | 103 | for event in [ "focus", "blur" ] 104 | installListener window, event, maintainIcon 105 | 106 | installListener window, "keypress", clearElementContent 107 | 108 | -------------------------------------------------------------------------------- /chrome-extension/options.coffee: -------------------------------------------------------------------------------- 1 | 2 | $ = (id) -> document.getElementById id 3 | 4 | document.addEventListener "DOMContentLoaded", -> 5 | versionElement = $("version") 6 | versionElement.textContent = chrome.runtime.getManifest().version 7 | 8 | portElement = $("port") 9 | secretElement = $("secret") 10 | commandElement = $("command") 11 | 12 | escape = (str) -> 13 | str = str.replace /\\/g, "\\\\" 14 | str = str.replace /"/g, "\\\"" 15 | 16 | maintainServerCommand = -> 17 | command = "\n" 18 | command += " # Set your editor, something like...\n" 19 | command += " export TEXT_AID_TOO_EDITOR=\"gvim -f\"\n\n" 20 | command += " export TEXT_AID_TOO_SECRET=\"#{escape secretElement.value.trim()}\"\n\n" if secretElement.value.trim() 21 | command += 22 | if portElement.value.trim() == Common.default.port 23 | " text-aid-too\n" 24 | else 25 | " text-aid-too --port #{portElement.value.trim()}\n" 26 | command += "\n" 27 | commandElement.textContent = command 28 | 29 | chrome.storage.sync.get [ "port", "secret" ], (items) -> 30 | unless chrome.runtime.lastError 31 | port.value = items.port if items.port? 32 | secret.value = items.secret if items.secret? 33 | maintainServerCommand() 34 | 35 | for element in [ portElement, secretElement ] 36 | do (element) -> 37 | element.addEventListener "change", -> 38 | value = element.value.trim() 39 | 40 | # Ensure that port is a number. 41 | if element == portElement and not /^[1-9][0-9]*$/.test value 42 | element.value = value = Common.default.port 43 | 44 | obj = {} 45 | obj[element.id] = value 46 | chrome.storage.sync.set obj 47 | maintainServerCommand() 48 | 49 | element.addEventListener "keydown", (event) -> 50 | element.blur() if event.keyCode == 27 51 | 52 | element.addEventListener "change", maintainServerCommand 53 | 54 | chrome.storage.sync.get "key", (items) -> 55 | unless chrome.runtime.lastError 56 | batchUpdate = false 57 | keys = [ "ctrlKey", "altKey", "shiftKey" ] 58 | key = items.key 59 | 60 | $("keyCode").textContent = key.keyCode 61 | $("setKey").value = "Set keyboard shortcut" 62 | 63 | saveKey = -> 64 | newKey = {} 65 | newKey[k] = $(k).checked for k in keys 66 | newKey.keyCode = parseInt $("keyCode").textContent 67 | chrome.storage.sync.set key: newKey 68 | 69 | for k in keys 70 | do (k) -> 71 | $(k).checked = key[k] 72 | $(k).addEventListener "change", -> 73 | saveKey() unless batchUpdate 74 | 75 | $("setKey").addEventListener "click", -> 76 | $("setKey").disabled = true 77 | $("setKey").value = "Type your keyboard shortcut..." 78 | 79 | cancel = -> 80 | window.removeEventListener "keydown", keydown 81 | window.removeEventListener "keyup", keyup 82 | $("setKey").disabled = false 83 | $("setKey").value = "Set keyboard shortcut" 84 | 85 | window.addEventListener "keydown", keydown = (event) -> 86 | return if event.repeat 87 | if event.keyCode not in [16..18] # Ctrl, Alt and Shift. 88 | batchUpdate = true 89 | $(k).checked = event[k] for k in keys 90 | $("keyCode").textContent = event.keyCode 91 | saveKey() 92 | batchUpdate = false 93 | cancel() 94 | 95 | window.addEventListener "keyup", keyup = -> cancel() 96 | 97 | maintainStatus = do -> 98 | status = $("status") 99 | messageTexts = true: "connected", false: "cannot connect" 100 | messageColours = true: "Green", false: "Red" 101 | -> 102 | chrome.runtime.sendMessage { name: "ping" }, (response) -> 103 | status.textContent = messageTexts[response.isUp] 104 | status.style.color = messageColours[response.isUp] 105 | Common.setTimeout 1000, maintainStatus 106 | 107 | maintainStatus() 108 | 109 | -------------------------------------------------------------------------------- /chrome-extension/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Text-Aid-Too Options 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 |
Text-Aid-Too Options
13 |

14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 52 | 53 |
Port
Secret
Keyboard shortcut 25 | Control 26 | [ The default is Ctrl-; ] 27 |
32 | Alt 33 |
38 | Shift 39 |
44 |  keyCode 45 |
50 | 51 |
54 | 55 |
56 |
57 | 58 |

59 | Server set up: Server status: ? 60 |

    61 |
    
     62 |         
63 |

64 | 65 |

66 | Explanation: 67 |

    68 |
  • 69 | This extension does nothing on its own; you also need to install and run the 70 | text-aid-too server. See the project's home page 72 | for details. 73 |
  • 74 |
  • 75 | There's no need to save your changes; just use Escape to blur the inputs above. 76 | Option values are synchronised between Chrome instances. 77 |
  • 78 | 79 |
  • 80 | Set Port to the port number used by the server. The default is 9293. 81 |
  • 82 | 83 |
  • 84 | Set Secret to a secret value which you provide to the server (optional, but 85 | recommended). If the server uses a 86 | secret, then it ignores requests which do not include that secret. 87 | See here for 88 | details. 89 |
  • 90 | 91 |
  • 92 | The default keyboard shortcut is Ctrl-;, but you can set your own shortcut 93 | above. 94 |
  • 95 | 96 |
  • 97 | For more information, see the project's 98 | home page. 99 | Please report issues 100 | here. 101 |
  • 102 | 103 |
  • 104 | Version . 105 |
  • 106 |
107 |

108 | 109 |

110 | Credits: 111 |

119 |

120 | 121 |
122 | 123 | 124 | -------------------------------------------------------------------------------- /server/server.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | 3 | # Required modules: 4 | # npm install watchr 5 | # npm install optimist 6 | # npm install ws 7 | # npm install markdown 8 | # npm install html 9 | # npm install coffee-script 10 | 11 | # Set the environment variable below, and the server will refuse to serve clients who don't know the secret. 12 | secret = process.env.TEXT_AID_TOO_SECRET 13 | 14 | for module in [ 15 | # The first of these must be installed via "npm". 16 | "watchr" 17 | "optimist" 18 | "ws" 19 | "markdown" 20 | "html" 21 | # These are standard. 22 | "os" 23 | "fs" 24 | "path" 25 | "child_process" 26 | ] 27 | try 28 | global[module] = require module 29 | catch 30 | console.log "ERROR\n#{module} is not available: sudo npm install -g #{module}" 31 | process.exit 1 32 | 33 | config = 34 | port: "9293" 35 | host: "localhost" 36 | editor: "gvim -f" 37 | 38 | defaultEditor = 39 | if process.env.TEXT_AID_TOO_EDITOR 40 | process.env.TEXT_AID_TOO_EDITOR 41 | else 42 | config.editor 43 | 44 | pjson = require path.join "..", "package.json" 45 | version = pjson.version 46 | 47 | helpText = 48 | """ 49 | Usage: 50 | text-aid-too [--port PORT] [--editor EDITOR-COMMAND] [--markdown] 51 | 52 | Example: 53 | export TEXT_AID_TOO_EDITOR="gvim -f" 54 | TEXT_AID_TOO_SECRET=hul8quahJ4eeL1Ib text-aid-too --port 9293 55 | 56 | Markdown (experimental): 57 | With the "--markdown" flag, text-aid-too tries to find non-HTML 58 | paragraphs in HTML texts and parses them as markdown. This only 59 | applies to texts from contentEditable elements (e.g. the GMail 60 | compose window). 61 | 62 | Environment variables: 63 | TEXT_AID_TOO_EDITOR: the editor command to use. 64 | TEXT_AID_TOO_SECRET: the shared secret; set this in the extension too. 65 | 66 | Version: #{version} 67 | """ 68 | 69 | args = optimist.usage(helpText) 70 | .alias("h", "help") 71 | .default("port", config.port) 72 | .default("editor", defaultEditor) 73 | .default("markdown", false) 74 | .argv 75 | 76 | if args.help 77 | optimist.showHelp() 78 | process.exit(0) 79 | 80 | console.log """ 81 | server ws://#{config.host}:#{args.port} 82 | secret #{if secret? then secret else ''} 83 | editor #{args.editor} 84 | version #{version} 85 | """ 86 | 87 | WSS = ws.Server 88 | wss = new WSS port: args.port, host: config.host 89 | wss.on "connection", (ws) -> ws.on "message", handler ws 90 | 91 | getEditCommand = (filename) -> 92 | command = if 0 <= args.editor.indexOf "%s" then args.editor.replace "%s", filename else "#{args.editor} #{filename}" 93 | console.log "exec:", command 94 | command 95 | 96 | handler = (ws) -> (message) -> 97 | request = JSON.parse message 98 | 99 | onExit = [] 100 | onExit.push -> ws.close() 101 | exit = (continuation = null) -> 102 | callback() for callback in onExit.reverse() 103 | onExit = [] 104 | continuation?() 105 | 106 | if secret? and 0 < secret.length 107 | unless request.secret? and request.secret == secret 108 | console.log """ 109 | mismatched or invalid secret; aborting request: 110 | required secret: #{secret} 111 | received secret: #{request.secret} 112 | """ 113 | return exit() 114 | 115 | sendResponse = (response, continuation = null) -> 116 | response.serverVersion = version 117 | ws.send JSON.stringify response 118 | continuation?() 119 | 120 | handlers = 121 | ping: -> 122 | console.log "ping: ok" 123 | request.isOk = true 124 | sendResponse request, exit 125 | 126 | edit: -> 127 | username = process.env.USER ? "unknown" 128 | directory = process.env.TMPDIR ? os.tmpdir() 129 | timestamp = process.hrtime().join "-" 130 | suffix = if request.isContentEditable then "html" else "txt" 131 | filename = path.join directory, "#{username}-text-aid-too-#{timestamp}.#{suffix}" 132 | 133 | console.log "edit:", filename 134 | onExit.push -> console.log " done:", filename 135 | 136 | fs.writeFile filename, (request.originalText ? request.text), (error) -> 137 | return exit() if error 138 | onExit.push -> fs.unlink filename, -> 139 | 140 | sendText = (continuation = null) -> 141 | fs.readFile filename, "utf8", (error, data) -> 142 | return exit() if error 143 | console.log " send: #{filename} [#{data.length}]" 144 | data = data.replace /\n$/, "" 145 | request.text = request.originalText = data 146 | request.text = formatMarkdown data if request.isContentEditable and args.markdown 147 | sendResponse request, continuation 148 | 149 | monitor = watchr.watch 150 | path: filename 151 | listener: sendText 152 | # This is only used for the "watch" method. 153 | catchupDelay: 400 154 | # Unfortunately, the "watch" method isn't reliable. So we're actually using the "watchFile" method 155 | # instead. See https://github.com/bevry/watchr/issues/33. 156 | preferredMethods: [ 'watchFile', 'watch' ] 157 | interval: 500 158 | onExit.push -> monitor.close() 159 | 160 | child = child_process.exec getEditCommand filename 161 | child.on "exit", (error) -> 162 | if error then exit() else sendText exit 163 | 164 | if handlers[request.name]? 165 | handlers[request.name]() 166 | else 167 | console.log "error; unknown request:", request 168 | 169 | markdownToHtml = (text) -> 170 | try 171 | html.prettyPrint markdown.markdown.toHTML text 172 | catch 173 | text 174 | 175 | # This is best-effort markdown handling. Paragraphs are separated by "\n\n". We collect together as many 176 | # paragraphs which don't seem to contain HTML as we can, and process them as markdown. Everything else just gets 177 | # passed through. 178 | formatMarkdown = (text) -> 179 | [ output, texts, input ] = [ [], [], text.split("\n\n").reverse() ] 180 | 181 | flushMarkdown = -> 182 | if 0 < texts.length 183 | output.push markdownToHtml texts.join "\n\n" 184 | texts = [] 185 | 186 | while 0 < input.length 187 | paragraph = input.pop() 188 | if /<\/?[a-zA-Z]+/.test paragraph 189 | flushMarkdown() 190 | output.push paragraph 191 | else 192 | texts.push paragraph 193 | 194 | flushMarkdown() 195 | output.join "\n\n" 196 | 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Text-Aid-Too 2 | 3 | *Text-aid-too* is a variation on the *text-aid* theme: it allows you to edit 4 | web inputs in your native text editor, such as Vim or Emacs. It's a 5 | combination of a 6 | [Chrome extension](https://chrome.google.com/webstore/detail/text-aid-too/klbcooigafjpbiahdjccmajnaehomajc) and a 7 | [server](https://www.npmjs.com/package/text-aid-too). 8 | 9 | *But Text-aid-too is different:* 10 | - In addition to traditional HTML inputs, it also works for `contentEditable` 11 | inputs (such as the GMail compose window). 12 | - It updates the input's contents whenever the file is written/saved, so you 13 | can preview your changes as you go along. 14 | - In `contentEditable` inputs (e.g. on Gmail) you can optionally use Markdown 15 | mark up, so you can write rich text GMail messages in Markdown (experimental, 16 | see [below](https://github.com/smblott-github/text-aid-too#markdown)). 17 | - The temporary file name has the appropriate extension for the input type (`.txt` 18 | or `.html`, as appropriate). Therefore, your text editor can detect the file 19 | type appropriately. 20 | - It checks inputs dynamically, so it works on sites (such as Google's Inbox) 21 | which toggle the `contentEditable` status on-the-fly. 22 | 23 | The default keyboard shortcut is ``, but you can set your own keyboard 24 | shortcut on the extension's options page. 25 | 26 | ### Screenshot 27 | 28 | ![Screenshot](https://cloud.githubusercontent.com/assets/2641335/8124943/cd7c5ffe-10d8-11e5-8403-e14d18dc482d.png) 29 | 30 | ### Installation 31 | 32 | #### Prerequisites 33 | 34 | You'll need [nodejs](https://nodejs.org/) and [Coffeescript](http://coffeescript.org/) (`sudo npm install -g coffee-script`). 35 | 36 | #### The Easy Way 37 | 38 | 1. Install the [extension](https://chrome.google.com/webstore/detail/klbcooigafjpbiahdjccmajnaehomajc) from the Chrome Store. 39 | 1. Install the server (and its dependencies): 40 | 41 | `sudo npm install -g text-aid-too` 42 | 43 | 1. Configure the port and shared secret on the extension's options page 44 | (optional, but required if you want to use a non-default port or a shared 45 | secret). 46 | 47 | 48 | Then, launch the server; which might be something like... 49 | 50 | export TEXT_AID_TOO_SECRET="" 51 | export TEXT_AID_TOO_EDITOR="gvim -f" 52 | 53 | # Use the default port (9293)... 54 | text-aid-too 55 | 56 | # Or... 57 | text-aid-too --port 9294 58 | 59 | ##### Important 60 | 61 | - The editor command must not fork and exit. Its process must remain until the 62 | editor is closed. For example, don't set the editor to `gvim` (which forks), 63 | set it to `gvim -f` which runs in the foreground (or the equivalent for your 64 | favourite editor) instead. 65 | 66 | - If you get an error regarding the "d-bus daemon not running", then see [this post](https://github.com/smblott-github/text-aid-too/issues/5). 67 | 68 | - *Text-aid-too* will not work with other *text-aid* servers. Those use HTTP, 69 | whereas *Text-aid-too* uses its own web-socket based protocol. This allows 70 | it to update the input's contents on-the-fly (that is, on file write). 71 | 72 | #### Automatically run as a background service 73 | 74 | On GNU/Linux, Text-Aid-Too can be packaged in a systemd user service, so it is 75 | automatically started as a background process when you log in. 76 | 77 | Create the unit file, and customize with your favorite editor: 78 | 79 | mkdir -p ~/.config/systemd/user 80 | cat >~/.config/systemd/user/text-aid-too.service <