├── LICENSE ├── README.md └── ghost-text-server.tcl /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Fredrik Alstromer 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ghost Text VIm 2 | 3 | A small server script allowing you to use GVIm with Ghost Text Chrome 4 | (or Firefox) extension. It's a standalone script, and will launch new 5 | instances of GVIm each time. It's implemented in TCL because, reasons, 6 | and requires TCL version at least 8.6. It also requires sha1, json, and 7 | json::write. On Debian and Ubuntu these are available in the tcllib 8 | package. 9 | 10 | The integration from VIm to Chrome is fairly robust, and will update on 11 | each change as soon as you get back to normal mode. In the other 12 | direction is a bit more shaky, and will only work if VIm is in normal 13 | mode. If you change the text in chrome while in insert mode in VIm, 14 | you'll get a bunch of crap buffer. This is due to the fact that we can 15 | only replace content by sending keys to VIm (and TCL has issues passing 16 | `` to the remote commands) rather than using remote expressions. 17 | 18 | Anyway, I wrote this for me, drop me a line if you have any issues and I 19 | might invest some more time into making it more user friendly. 20 | -------------------------------------------------------------------------------- /ghost-text-server.tcl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tclsh 2 | 3 | package require Tcl 8.6 4 | package require try 5 | package require cmdline 1.5 6 | 7 | package require sha1 8 | package require json 9 | package require json::write 10 | 11 | set PORT 4001 12 | set WSGUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 13 | 14 | set verbose no 15 | set debug no 16 | 17 | set options { 18 | {v "Enable verbose output"} 19 | {d "Enable debug output"} 20 | {L.arg "gvim" "Use this command to launch vim"} 21 | {R.arg "gvim" "Use this command for remote commands"} 22 | } 23 | 24 | try { 25 | array set params [::cmdline::getoptions argv $options] 26 | set verbose $params(v) 27 | set debug $params(d) 28 | set remote $params(R) 29 | set launcher $params(L) 30 | } trap {CMDLINE USAGE} {msg o} { 31 | puts $msg 32 | exit 1 33 | } 34 | 35 | proc vim-send {name msg} { 36 | if {$::debug} { puts "vim-send $name $msg" } 37 | exec $::remote --servername $name --remote-send $msg 38 | } 39 | proc vim-expr {name expr} { 40 | if {$::debug} { puts "vim-expr $name $expr" } 41 | exec $::remote --servername $name --remote-expr $expr 42 | } 43 | proc vim-launch {name} { 44 | if {$::debug} { puts "vim-launch $name" } 45 | exec $::launcher -c "set buftype=nofile" -c "set bufhidden=hide" -c "set noswapfile" --servername $name & 46 | } 47 | 48 | proc launch-editor {name} { 49 | vim-launch $name 50 | 51 | while {[incr attempts] < 10 && [catch {vim-expr $name changenr()}]} { 52 | after 250 53 | } 54 | } 55 | 56 | set quit 0 57 | 58 | proc xor {payload mask} { 59 | set len [string length $payload] 60 | # binary scan i only reads in multiples of 4, so pad the string 61 | binary scan "$payload..." i* payload 62 | string range [ 63 | binary format i* [lmap x $payload {expr {$x ^ $mask}}] 64 | ] 0 $len-1 65 | } 66 | 67 | proc stringify {dictVal} { 68 | foreach {k v} $dictVal { 69 | lappend a "[json::write::string $k]:[json::write::string $v]" 70 | } 71 | return "{[join $a ,]}" 72 | } 73 | 74 | proc every {ms body {last {}}} { 75 | global every 76 | if {$ms == "cancel"} { 77 | if {[info exists every($body)]} { 78 | after cancel $every($body) 79 | unset every($body) 80 | } 81 | } else { 82 | if {[info exists every($body)]} { 83 | after cancel $every($body) 84 | } 85 | if {[set last [eval $body $last]] != "end"} { 86 | set every($body) [after $ms every $ms [list $body] $last] 87 | } 88 | } 89 | } 90 | 91 | proc send {chan packet} { 92 | puts -nonewline $chan $packet 93 | flush $chan 94 | } 95 | 96 | proc onclose {chan} { 97 | if {$::debug} { puts "onclose $chan" } 98 | if {[catch { 99 | if {![eof $chan]} { 100 | set packet [binary format H2c 88 0]; # fin, close, zero unmasked length 101 | send $chan $packet 102 | } 103 | } exc]} { 104 | puts "Exception sending 'close': $exc" 105 | } 106 | 107 | every cancel [list refresh $chan] 108 | if {$::verbose} { 109 | set addr "unknown" 110 | set port "0" 111 | catch { lassign [chan configure $chan -peername] addr host port } 112 | puts "WebSocket $addr:$port disconnected." 113 | } 114 | close $chan 115 | 116 | if {[catch { 117 | vim-send $chan {:q} 118 | } exc]} { 119 | puts "Exception while closing VIm: $exc" 120 | } 121 | } 122 | 123 | # http://wiki.tcl.tk/515 124 | proc u2a {s} { 125 | set res "" 126 | foreach i [split $s ""] { 127 | scan $i %c c 128 | if {$c<128} {append res $i} else {append res \\u[format %04.4x $c]} 129 | } 130 | set res 131 | } ;#RS 132 | 133 | proc refresh {chan {change {}}} { 134 | if {[catch { 135 | set nchange [vim-expr $chan changenr()] 136 | if {$nchange != $change} { 137 | if {"n" != [vim-expr $chan mode()]} { 138 | # we're editing, wait for the edit to finish. 139 | set nchange $change 140 | } else { 141 | set buf [vim-expr $chan {getline(1,'$')}] 142 | } 143 | } 144 | } exc]} { 145 | puts "Exception querying VIm: $exc" 146 | onclose $chan 147 | return end 148 | } elseif {$nchange != $change} { 149 | if {$::verbose} { puts "Update detected, sending..." } 150 | set change $nchange 151 | set buf [u2a [stringify [list text $buf]]] 152 | set len [string bytelength $buf] 153 | if {$len >= 65536} { 154 | set blen [binary format cII 127 0 $len] 155 | } elseif {$len >= 126} { 156 | set blen [binary format cS 126 $len] 157 | } else { 158 | set blen [binary format c $len] 159 | } 160 | # rfc6455 5.1 ... A server MUST NOT mask any frames 161 | if {$::debug} { puts "Sending: $len $buf" } 162 | set packet "[binary format H2 81]$blen$buf" 163 | send $chan $packet 164 | flush $chan 165 | } 166 | return $change 167 | } 168 | 169 | proc onmessage {chan} { 170 | if {[catch { 171 | if {[eof $chan]} { 172 | onclose $chan 173 | } else { 174 | set mask "" 175 | set masked 0 176 | set len 0 177 | set preamble [read $chan 1] 178 | set masklen [read $chan 1] 179 | binary scan $preamble b4 flags 180 | binary scan $preamble h opcode 181 | binary scan $masklen B masked 182 | binary scan $masklen c len 183 | if {$len < 0} {set len [expr {$len & 127}]} 184 | 185 | if {$len == 126} { 186 | # 16 bit length. 187 | binary scan [read $chan 2] S len 188 | } elseif {$len == 127} { 189 | binary scan [read $chan 8] II over len 190 | if {$over != 0 || $len < 0} { 191 | puts "Oversized packet; closing. ($over:$len)" 192 | onclose $chan 193 | return 194 | } 195 | } 196 | # rfc6455 5.1 ... A client MUST mask all frames 197 | if {$masked} {binary scan [read $chan 4] i mask} {onclose $chan; return} 198 | if {$len < 0} {set len [expr {65536 + $len}]} 199 | if {$::debug} { 200 | puts "FLAGS:$flags OPCODE:$opcode MASKED:$masked LEN:$len MASK:$mask" 201 | } 202 | 203 | set payload [xor [read $chan $len] $mask] 204 | # puts $payload 205 | 206 | switch $opcode { 207 | 1 { 208 | # text 209 | set payload [encoding convertfrom utf-8 $payload] 210 | if {$::debug} { puts "received text frame: $payload" } 211 | set msg [json::json2dict $payload] 212 | set lines [dict get $msg text] 213 | set escaped [string map { 214 | "\\" "\\\\" 215 | "\"" "\\\"" 216 | } $lines] 217 | set txt "\[\"[join [split $escaped "\n"] {","}]\"\]" 218 | lassign [vim-expr $chan getpos('.')] bnum lnum col 219 | # tcl can't have '<' at the begging of an exec-parameter, so 220 | # we can't send first... 221 | vim-send $chan {:%d _} 222 | vim-expr $chan "append(0,$txt)" 223 | # there's always one empty line after deleting them all, remove it. 224 | vim-send $chan {:$d _} 225 | vim-expr $chan "cursor($lnum,$col)" 226 | # enable file type detection 227 | vim-send $chan {:filetype detect} 228 | 229 | every 1000 [list refresh $chan] [vim-expr $chan changenr()] 230 | } 231 | 8 { 232 | # close 233 | onclose $chan 234 | } 235 | 9 { 236 | ; # ping 237 | puts "Ping? Pong." 238 | set packet [binary format H2c 8a 0]; # fin, pong, zero unmasked length 239 | send $chan $packet 240 | } 241 | a { 242 | # pong 243 | puts "Pong." 244 | } 245 | 246 | default { 247 | puts "Unknown opcode: $opcode" 248 | } 249 | } 250 | } 251 | } exc]} { 252 | puts "Caught exception: $exc" 253 | } 254 | } 255 | 256 | proc sl {chan {msg {}}} { 257 | # puts $msg 258 | puts $chan "$msg" 259 | } 260 | 261 | proc accept {chan addr port} { 262 | global PORT WSGUID 263 | fconfigure $chan -translation crlf 264 | while {![eof $chan]} { 265 | set l [string trim [gets $chan]] 266 | if {[string equal $l ""]} break 267 | set h [lindex $l 0] 268 | set v [lreplace $l 0 0] 269 | set rq($h) $v 270 | } 271 | 272 | if {[info exists rq(Upgrade:)] && [string equal websocket $rq(Upgrade:)]} { 273 | if {$::verbose} { puts "WebSocket $addr:$port connected." } 274 | if {$::debug} { puts "WebSocket [array get rq Sec-*]" } 275 | sl $chan "HTTP/1.1 101 Switching Protocols" 276 | sl $chan "Date: [clock format [clock seconds] -format {%a, %d %h %Y %T %Z} -timezone GMT]" 277 | sl $chan "Server: ..." 278 | sl $chan "Upgrade: $rq(Upgrade:)" 279 | sl $chan "Connection: Upgrade" 280 | sl $chan "Sec-WebSocket-Accept: [ 281 | binary encode base64 [ 282 | binary decode hex [sha1::sha1 "$rq(Sec-WebSocket-Key:)$WSGUID"] 283 | ] 284 | ]" 285 | sl $chan 286 | flush $chan 287 | fconfigure $chan -translation binary -blocking 0 288 | 289 | launch-editor $chan 290 | 291 | fileevent $chan readable [list onmessage $chan] 292 | } else { 293 | if {$::verbose} { puts "HTTP $addr:$port connected." } 294 | # don't know why we do this extra request, just keep using the same port. 295 | set payload [subst {{"WebSocketPort":$PORT,"ProtocolVersion":1}}] 296 | sl $chan "HTTP/1.1 200 Ok" 297 | sl $chan "Date: [clock format [clock seconds] -format {%a, %d %h %Y %T %Z} -timezone GMT]" 298 | sl $chan "Server: ..." 299 | sl $chan "Content-Type: application/json" 300 | sl $chan "Connection: close" 301 | # sl $chan "Content-Length: [string length $payload]" 302 | sl $chan 303 | sl $chan $payload 304 | sl $chan 305 | close $chan 306 | if {$::verbose} { puts "HTTP $addr:$port disconnected." } 307 | } 308 | } 309 | 310 | if {$::verbose} { puts "Listening on $PORT"; } 311 | socket -server accept -myaddr localhost $PORT 312 | vwait quit 313 | --------------------------------------------------------------------------------