Well that was fun. (This web worker version is 3x slower but a bit smoother. Sadly it doesn't work in firefox because of this bug: https://bugzilla.mozilla.org/show_bug.cgi?id=504553)

This commit is contained in:
Florian Mounier
2014-02-14 16:04:45 +01:00
parent a96950c303
commit c42c59a1a9
5 changed files with 4974 additions and 1911 deletions

View File

@@ -15,65 +15,786 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
worker = new Worker('/static/javascripts/worker.js')
worker.addEventListener 'message', (e) ->
switch e.data.cmd
when 'refresh'
frontterm.refresh e.data.lines, e.data.start, e.data.end
cols = rows = null
quit = false
$ = document.querySelectorAll.bind(document)
send = (data) ->
ws.send 'S' + data
worker.postMessage
cmd: 'data'
data: data
ctl = (type, args...) ->
params = args.join(',')
if type == 'Resize'
ws.send 'R' + params
worker.postMessage 'R' + params
# ws.send 'R' + params
ws_url = 'ws://' + document.location.host + '/ws' + location.pathname
ws = new WebSocket ws_url
ws.addEventListener 'open', ->
console.log "WebSocket open", arguments
ws.send 'R' + term.cols + ',' + term.rows
if location.hash
setTimeout ->
ws.send 'S' + location.hash.slice(1) + '\n'
, 100
ws.addEventListener 'error', ->
console.log "WebSocket error", arguments
ws.addEventListener 'message', (e) ->
setTimeout ->
term.write e.data
, 1
ws.addEventListener 'close', ->
console.log "WebSocket closed", arguments
quit = true
open('','_self').close()
term = new Terminal $('#wrapper')[0], send, ctl
addEventListener 'beforeunload', ->
if not quit
'This will exit the terminal session'
bench = (n=100000000) ->
rnd = ''
while rnd.length < n
rnd += Math.random().toString(36).substring(2)
t0 = (new Date()).getTime()
term.write rnd
console.log "#{n} chars in #{(new Date()).getTime() - t0} ms"
cancel = (ev) ->
ev.preventDefault() if ev.preventDefault
ev.stopPropagation() if ev.stopPropagation
ev.cancelBubble = true
false
cbench = (n=100000000) ->
rnd = ''
while rnd.length < n
rnd += "\x1b[#{30 + parseInt(Math.random() * 20)}m"
rnd += Math.random().toString(36).substring(2)
class FrontTerminal
constructor: (@parent) ->
# Global elements
@context = @parent.ownerDocument.defaultView
@document = @parent.ownerDocument
@body = @document.getElementsByTagName('body')[0]
t0 = (new Date()).getTime()
term.write rnd
console.log "#{n} chars + colors in #{(new Date()).getTime() - t0} ms"
# Main terminal element
@element = @document.createElement('div')
@element.className = 'terminal focus'
@element.style.outline = 'none'
@element.setAttribute 'tabindex', 0
@parent.appendChild(@element)
# Adding one line to compute char size
div = @document.createElement('div')
div.className = 'line'
@element.appendChild(div)
@children = [div]
@compute_char_size()
div.style.height = @char_size.height + 'px'
term_size = @parent.getBoundingClientRect()
@cols = Math.floor(term_size.width / @char_size.width) - 1 # ?
@rows = Math.floor(term_size.height / @char_size.height)
i = @rows - 1
while i--
div = @document.createElement('div')
div.style.height = @char_size.height + 'px'
div.className = 'line'
@element.appendChild(div)
@children.push(div)
@visualBell = 100
@cursorHidden = false
@queue = ''
# stream
@defAttr = (0 << 18) | (257 << 9) | (256 << 0)
@skipNextKey = null
@cursorState = 0
# Draw screen
# @refresh 0, @rows - 1
@focus()
@startBlink()
addEventListener 'keydown', @keyDown.bind(@)
addEventListener 'keypress', @keyPress.bind(@)
addEventListener 'focus', @focus.bind(@)
addEventListener 'blur', @blur.bind(@)
addEventListener 'paste', @paste.bind(@)
addEventListener 'resize', @resize.bind(@)
# Horrible Firefox paste workaround
if typeof InstallTrigger isnt "undefined"
@element.contentEditable = 'true'
@element.addEventListener "mouseup", (ev) =>
sel = getSelection().getRangeAt(0)
if sel.startOffset is sel.endOffset
getSelection().removeAllRanges()
@initmouse()
compute_char_size: ->
test_span = document.createElement('span')
test_span.textContent = '0123456789'
@children[0].appendChild(test_span)
@char_size =
width: test_span.getBoundingClientRect().width / 10
height: @children[0].getBoundingClientRect().height
@children[0].removeChild(test_span)
focus: ->
@send('\x1b[I') if @sendFocus
@showCursor()
@element.classList.add('focus')
@element.classList.remove('blur')
blur: ->
@cursorState = 1
# @refresh(@y, @y)
@send('\x1b[O') if @sendFocus
@element.classList.add('blur')
@element.classList.remove('focus')
paste: (ev) ->
if ev.clipboardData
@send ev.clipboardData.getData('text/plain')
else if @context.clipboardData
@send @context.clipboardData.getData('Text')
cancel(ev)
# XTerm mouse events
# http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
# To better understand these
# the xterm code is very helpful:
# Relevant files:
# button.c, charproc.c, misc.c
# Relevant functions in xterm/button.c:
# BtnCode, EmitButtonCode, EditorButton, SendMousePosition
initmouse: ->
pressed = 32
# mouseup, mousedown, mousewheel
# left click: ^[[M 3<^[[M#3<
# mousewheel up: ^[[M`3>
sendButton = (ev) =>
# get the xterm-style button
button = getButton(ev)
# get mouse coordinates
pos = getCoords(ev)
return unless pos
sendEvent button, pos
switch ev.type
when "mousedown"
pressed = button
when "mouseup"
# keep it at the left
# button, just in case.
pressed = 32
# motion example of a left click:
# ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7<
sendMove = (ev) =>
button = pressed
pos = getCoords(ev)
return unless pos
# buttons marked as motions
# are incremented by 32
button += 32
sendEvent button, pos
# encode button and
# position to characters
encode = (data, ch) =>
unless @utfMouse
return data.push(0) if ch is 255
ch = 127 if ch > 127
data.push ch
else
return data.push(0) if ch is 2047
if ch < 127
data.push ch
else
ch = 2047 if ch > 2047
data.push 0xC0 | (ch >> 6)
data.push 0x80 | (ch & 0x3F)
# send a mouse event:
# regular/utf8: ^[[M Cb Cx Cy
# urxvt: ^[[ Cb ; Cx ; Cy M
# sgr: ^[[ Cb ; Cx ; Cy M/m
# vt300: ^[[ 24(1/3/5)~ [ Cx , Cy ] \r
# locator: CSI P e ; P b ; P r ; P c ; P p & w
sendEvent = (button, pos) =>
if @urxvtMouse
pos.x -= 32
pos.y -= 32
pos.x++
pos.y++
@send "\x1b[" + button + ";" + pos.x + ";" + pos.y + "M"
return
if @sgrMouse
pos.x -= 32
pos.y -= 32
@send "\x1b[<" + (if (button & 3) is 3 then button & ~3 else button) + ";" + pos.x + ";" + pos.y + (if (button & 3) is 3 then "m" else "M")
return
data = []
encode data, button
encode data, pos.x
encode data, pos.y
@send "\x1b[M" + String.fromCharCode.apply(String, data)
getButton = (ev) =>
# two low bits:
# 0 = left
# 1 = middle
# 2 = right
# 3 = release
# wheel up/down:
# 1, and 2 - with 64 added
switch ev.type
when "mousedown"
button = if ev.button? then +ev.button else (if ev.which? then ev.which - 1 else null)
when "mouseup"
button = 3
when "wheel"
button = if ev.deltaY < 0 then 64 else 65
# next three bits are the modifiers:
# 4 = shift, 8 = meta, 16 = control
shift = if ev.shiftKey then 4 else 0
meta = if ev.metaKey then 8 else 0
ctrl = if ev.ctrlKey then 16 else 0
mod = shift | meta | ctrl
# no mods
if @vt200Mouse
# ctrl only
mod &= ctrl
else
mod = 0 unless @normalMouse
# increment to SP
(32 + (mod << 2)) + button
# mouse coordinates measured in cols/rows
getCoords = (ev) =>
x = ev.pageX
y = ev.pageY
# should probably check offsetParent
# but this is more portable
el = @element
while el and el isnt @document.documentElement
x -= el.offsetLeft
y -= el.offsetTop
el = if "offsetParent" of el then el.offsetParent else el.parentNode
# convert to cols/rows
w = @element.clientWidth
h = @element.clientHeight
x = Math.ceil((x / w) * @cols)
y = Math.ceil((y / h) * @rows)
# be sure to avoid sending
# bad positions to the program
x = 0 if x < 0
x = @cols if x > @cols
y = 0 if y < 0
y = @rows if y > @rows
# xterm sends raw bytes and
# starts at 32 (SP) for each.
x += 32
y += 32
x: x
y: y
type: ev.type
addEventListener "mousedown", (ev) =>
return unless @mouseEvents
# send the button
sendButton ev
# fix for odd bug
#if (@vt200Mouse && !@normalMouse) {
if @vt200Mouse
sendButton
__proto__: ev
type: "mouseup"
return cancel(ev)
addEventListener "mousemove", sendMove.bind(this) if @normalMouse
# x10 compatibility mode can't send button releases
unless @x10Mouse
addEventListener "mouseup", up = (ev) =>
sendButton ev
removeEventListener "mousemove", sendMove if @normalMouse
removeEventListener "mouseup", up
cancel ev
cancel ev
addEventListener "wheel", (ev) =>
if @mouseEvents
return if @x10Mouse
sendButton ev
else
return if @applicationKeypad
@scrollDisp if ev.deltaY > 0 then 5 else -5
cancel ev
refresh: (lines, start, end) ->
if end - start >= @rows / 3
parent = @element.parentNode
parent?.removeChild @element
width = @cols
y = start
if end >= lines.length
end = lines.length - 1
while y <= end
row = y
line = lines[row]
out = ""
if y is @y and (@ydisp is @ybase or @selectMode) and not @cursorHidden
x = @x
else
x = -Infinity
attr = @defAttr
i = 0
while i < width
data = line[i][0]
ch = line[i][1]
if data isnt attr
out += "</span>" if attr isnt @defAttr
if data isnt @defAttr
classes = []
out += "<span "
bg = data & 0x1ff
fg = (data >> 9) & 0x1ff
flags = data >> 18
# bold
classes.push "bold" if flags & 1
# underline
classes.push "underline" if flags & 2
# blink
classes.push "blink" if flags & 4
# inverse
classes.push "reverse-video" if flags & 8
# invisible
classes.push "invisible" if flags & 16
fg += 8 if flags & 1 and fg < 8
classes.push "bg-color-" + bg
classes.push "fg-color-" + fg
out += "class=\""
out += classes.join(" ")
out += "\">"
out += "<span class=\"" + (if @cursorState then "reverse-video " else "") + "cursor\">" if i is x
# This is a temporary dirty hack for raw html insertion
if ch.length > 1
out += ch
else
switch ch
when "&"
out += "&amp;"
when "<"
out += "&lt;"
when ">"
out += "&gt;"
else
if ch <= " "
out += "&nbsp;"
else
i++ if "\uff00" < ch < "\uffef"
out += ch
out += "</span>" if i is x
attr = data
i++
out += "</span>" if attr isnt @defAttr
@children[y].innerHTML = out
y++
parent?.appendChild @element
_cursorBlink: ->
@cursorState ^= 1
cursor = @element.querySelector(".cursor")
return unless cursor
if cursor.classList.contains("reverse-video")
cursor.classList.remove "reverse-video"
else
cursor.classList.add "reverse-video"
showCursor: ->
return
# unless @cursorState
# @cursorState = 1
# @refresh @y, @y
startBlink: ->
return unless @cursorBlink
@_blinker = => @_cursorBlink()
@_blink = setInterval(@_blinker, 500)
refreshBlink: ->
return unless @cursorBlink
clearInterval @_blink
@_blink = setInterval(@_blinker, 500)
scrollDisp: (disp) ->
@ydisp += disp
if @ydisp > @ybase
@ydisp = @ybase
else
@ydisp = 0 if @ydisp < 0
# @refresh 0, @rows - 1
keyDown: (ev) ->
# Key Resources:
# https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
# Don't handle modifiers alone
return true if ev.keyCode > 15 and ev.keyCode < 19
# Handle shift insert and ctrl insert copy/paste usefull for typematrix keyboard
return true if (ev.shiftKey or ev.ctrlKey) and ev.keyCode is 45
# Alt-z works as an escape to relay the following keys to the browser.
# usefull to trigger browser shortcuts, i.e.: Alt+Z F5 to reload
# May be redundant with keyPrefix
if ev.altKey and ev.keyCode is 90 and not @skipNextKey
@skipNextKey = true
@element.classList.add('skip')
return cancel(ev)
if @skipNextKey
@skipNextKey = false
@element.classList.remove('skip')
return true
switch ev.keyCode
# backspace
when 8
key = if ev.altKey then "\x1b" else ""
if ev.shiftKey
key += "\x08" # ^H
break
key += "\x7f" # ^?
# tab
when 9
if ev.shiftKey
key = "\x1b[Z"
break
key = "\t"
# return/enter
when 13
key = "\r"
# escape
when 27
key = "\x1b"
# left-arrow
when 37
if @applicationCursor
key = "\x1bOD" # SS3 as ^[O for 7-bit
#key = '\x8fD'; // SS3 as 0x8f for 8-bit
break
return true if ev.shiftKey
key = "\x1b[D"
# right-arrow
when 39
if @applicationCursor
key = "\x1bOC"
break
return true if ev.shiftKey
key = "\x1b[C"
# up-arrow
when 38
if @applicationCursor
key = "\x1bOA"
break
if ev.ctrlKey
@scrollDisp -1
return cancel(ev)
else if ev.shiftKey
return true
else
key = "\x1b[A"
# down-arrow
when 40
if @applicationCursor
key = "\x1bOB"
break
if ev.ctrlKey
@scrollDisp 1
return cancel(ev)
else if ev.shiftKey
return true
else
key = "\x1b[B"
# delete
when 46
key = "\x1b[3~"
# insert
when 45
key = "\x1b[2~"
# home
when 36
if @applicationKeypad
key = "\x1bOH"
break
key = "\x1bOH"
# end
when 35
if @applicationKeypad
key = "\x1bOF"
break
key = "\x1bOF"
# page up
when 33
if ev.shiftKey
@scrollDisp -(@rows - 1)
return cancel(ev)
else
key = "\x1b[5~"
# page down
when 34
if ev.shiftKey
@scrollDisp @rows - 1
return cancel(ev)
else
key = "\x1b[6~"
# F1
when 112
key = "\x1bOP"
# F2
when 113
key = "\x1bOQ"
# F3
when 114
key = "\x1bOR"
# F4
when 115
key = "\x1bOS"
# F5
when 116
key = "\x1b[15~"
# F6
when 117
key = "\x1b[17~"
# F7
when 118
key = "\x1b[18~"
# F8
when 119
key = "\x1b[19~"
# F9
when 120
key = "\x1b[20~"
# F10
when 121
key = "\x1b[21~"
# F11
when 122
key = "\x1b[23~"
# F12
when 123
key = "\x1b[24~"
else
# a-z and space
if ev.ctrlKey
if ev.keyCode >= 65 and ev.keyCode <= 90
# Ctrl-A
if @screenKeys
if not @prefixMode and not @selectMode and ev.keyCode is 65
@enterPrefix()
return cancel(ev)
# Ctrl-V
if @prefixMode and ev.keyCode is 86
@leavePrefix()
return
# Ctrl-C
if (@prefixMode or @selectMode) and ev.keyCode is 67
if @visualMode
setTimeout (=>
@leaveVisual()
return
), 1
return
key = String.fromCharCode(ev.keyCode - 64)
else if ev.keyCode is 32
# NUL
key = String.fromCharCode(0)
else if ev.keyCode >= 51 and ev.keyCode <= 55
# escape, file sep, group sep, record sep, unit sep
key = String.fromCharCode(ev.keyCode - 51 + 27)
else if ev.keyCode is 56
# delete
key = String.fromCharCode(127)
else if ev.keyCode is 219
# ^[ - escape
key = String.fromCharCode(27)
# ^] - group sep
else
key = String.fromCharCode(29) if ev.keyCode is 221
else if ev.altKey
if ev.keyCode >= 65 and ev.keyCode <= 90
key = "\x1b" + String.fromCharCode(ev.keyCode + 32)
else if ev.keyCode is 192
key = "\x1b`"
else
key = "\x1b" + (ev.keyCode - 48) if ev.keyCode >= 48 and ev.keyCode <= 57
if ev.keyCode >= 37 and ev.keyCode <= 40
if ev.ctrlKey
key = key.slice(0, -1) + "1;5" + key.slice(-1)
else if ev.altKey
key = key.slice(0, -1) + "1;3" + key.slice(-1)
else key = key.slice(0, -1) + "1;4" + key.slice(-1) if ev.shiftKey
return true unless key
if @prefixMode
@leavePrefix()
return cancel(ev)
if @selectMode
@keySelect ev, key
return cancel(ev)
@showCursor()
@handler(key)
cancel ev
keyPress: (ev) ->
if @skipNextKey is false
@skipNextKey = null
return true
cancel ev
if ev.charCode
key = ev.charCode
else unless ev.which?
key = ev.keyCode
else if ev.which isnt 0 and ev.charCode isnt 0
key = ev.which
else
return false
return false if not key or ev.ctrlKey or ev.altKey or ev.metaKey
key = String.fromCharCode(key)
@showCursor()
@handler key
false
handler: (data) ->
worker.postMessage
cmd: 'data',
data: data
send: (data) ->
unless @queue
setTimeout (=>
@handler @queue
@queue = ""
return
), 1
@queue += data
bell: ->
return unless @visualBell
@element.classList.add "bell"
setTimeout (=>
@element.classList.remove "bell"
), @visualBell
resize: ->
old_cols = @cols
old_rows = @rows
term_size = @parent.getBoundingClientRect()
@cols = Math.floor(term_size.width / @char_size.width) - 1 # ?
@rows = Math.floor(term_size.height / @char_size.height)
if old_cols == @cols and old_rows == @rows
return
@ctl 'Resize', @cols, @rows
# resize rows
j = old_rows
if j < @rows
el = @element
while j++ < @rows
if @children.length < @rows
line = @document.createElement("div")
line.className = 'line'
line.style.height = @char_size.height + 'px'
el.appendChild line
@children.push line
else if j > @rows
while j-- > @rows
if @children.length > @rows
el = @children.pop()
continue unless el
el.parentNode.removeChild el
get_html_height_in_lines: (html) ->
temp_node = document.createElement("div")
temp_node.innerHTML = html
@element.appendChild temp_node
html_height = temp_node.getBoundingClientRect().height
@element.removeChild temp_node
Math.ceil(html_height / @char_size.height)
frontterm = new FrontTerminal $('#wrapper')[0], send, ctl
worker.postMessage
cmd: 'init'
cols: frontterm.cols
rows: frontterm.rows
wsurl: 'ws://' + document.location.host + '/ws' + location.pathname

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2
dev.py
View File

@@ -8,7 +8,7 @@ import shlex
commands = [
'coffee -wcb -j butterfly/static/javascripts/main.js ' +
'butterfly/static/coffees/term.coffee ' +
# 'butterfly/static/coffees/term.coffee ' +
'butterfly/static/coffees/backsel.coffee ' +
'butterfly/static/coffees/virtual_input.coffee ' +
'butterfly/static/coffees/main.coffee ',