diff --git a/butterfly/static/coffees/backsel.coffee b/butterfly/static/coffees/backsel.coffee
new file mode 100644
index 0000000..7744030
--- /dev/null
+++ b/butterfly/static/coffees/backsel.coffee
@@ -0,0 +1,24 @@
+state =
+ x: null
+ y: null
+
+document.addEventListener 'keydown', (e) ->
+ if e.shiftKey and (37 <= e.keyCode <= 40)
+ if state.y == null
+ state.y = term.ybase + term.y
+ if e.keyCode == 38
+ state.y--
+ if state.y < term.ybase
+ state.y = term.ybase
+ else if e.keyCode == 40
+ state.y++
+ if state.y > term.ybase + term.y
+ state.y = term.ybase + term.y
+
+ term.emit('data', ' \x0b\x15')
+ if state.y != term.ybase + term.y
+ term.emit('data', term.grabText(0, term.cols - 1, state.y, state.y).replace('\n', ''))
+ e.stopPropagation()
+ return false
+ else
+ state.x = state.y = null
diff --git a/butterfly/static/coffees/main.coffee b/butterfly/static/coffees/main.coffee
index 004f3be..1d78c5c 100644
--- a/butterfly/static/coffees/main.coffee
+++ b/butterfly/static/coffees/main.coffee
@@ -1,74 +1,7 @@
-try
- document.createEvent("TouchEvent")
- virtual_input = true
-catch e
- virtual_input = false
-
term = ws = null
cols = rows = null
quit = false
-if virtual_input
- ctrl = false
- alt = false
- first = true
- virtual_input = document.createElement 'input'
- virtual_input.type = 'password'
- virtual_input.style.position = 'fixed'
- virtual_input.style.top = 0
- virtual_input.style.left = 0
- virtual_input.style.border = 'none'
- virtual_input.style.outline = 'none'
- virtual_input.style.opacity = 0
- virtual_input.value = '0'
- document.body.appendChild virtual_input
-
- virtual_input.addEventListener 'blur', ->
- setTimeout((=> @focus()), 10)
-
- addEventListener 'click', ->
- virtual_input.focus()
-
- addEventListener 'touchstart', (e) ->
- if e.touches.length == 1
- ctrl = true
- else if e.touches.length == 2
- ctrl = false
- alt = true
- else if e.touches.length == 3
- ctrl = true
- alt = true
-
- virtual_input.addEventListener 'keydown', (e) ->
- term.keyDown(e)
- return true
-
- virtual_input.addEventListener 'input', (e) ->
- len = @value.length
-
- if len == 0
- e.keyCode = 8
- term.keyDown e
- @value = '0'
- return true
-
- e.keyCode = @value.charAt(1).charCodeAt(0)
-
- if (ctrl or alt) and not first
- e.keyCode = @value.charAt(1).charCodeAt(0)
- e.ctrlKey = ctrl
- e.altKey = alt
- if e.keyCode >= 97 && e.keyCode <= 122
- e.keyCode -= 32
- term.keyDown e
- @value = '0'
- ctrl = alt = false
- return true
-
- term.keyPress e
- first = false
- @value = '0'
- true
$ = document.querySelectorAll.bind(document)
@@ -80,7 +13,7 @@ ws.onopen = ->
term = new Terminal(
visualBell: 100
screenKeys: true
- scrollback: -1
+ scrollback: 100000
)
term.on "data", (data) ->
ws.send 'SH|' + data
@@ -93,6 +26,9 @@ ws.onopen = ->
resize()
+ws.onerror = -> console.log "WebSocket error", arguments
+ws.onmessage = (e) -> term.write e.data
+
ws.onclose = ->
if term
term.destroy()
@@ -100,10 +36,6 @@ ws.onclose = ->
quit = true
open('','_self').close()
-ws.onerror = -> console.log "WebSocket error", arguments
-ws.onmessage = (e) ->
- term.write event.data
-
addEventListener 'beforeunload', ->
if not quit
'This will exit the terminal session'
@@ -133,3 +65,23 @@ addEventListener 'resize', resize = ->
div.style.height = eh + 'px'
ws.send "RS|#{cols},#{rows}"
+
+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"
+
+
+cbench = (n=100000000) ->
+ rnd = ''
+ while rnd.length < n
+ rnd += "\x1b[#{30 + parseInt(Math.random() * 20)}m"
+ rnd += Math.random().toString(36).substring(2)
+
+ t0 = (new Date()).getTime()
+ term.write rnd
+ console.log "#{n} chars + colors in #{(new Date()).getTime() - t0} ms"
diff --git a/butterfly/static/coffees/term.coffee b/butterfly/static/coffees/term.coffee
new file mode 100644
index 0000000..9935454
--- /dev/null
+++ b/butterfly/static/coffees/term.coffee
@@ -0,0 +1,1067 @@
+
+
+class Terminal
+ constructor: ->
+ @cols = 80
+ @rows = 24
+ @scrollback = 100000
+
+ @defAttr = (0 << 18) | (257 << 9) | (256 << 0)
+ @curAttr = @defAttr
+
+
+ @sendFocus = false
+
+ eraseAttr: ->
+ (@defAttr & ~0x1ff) | (@curAttr & 0x1ff)
+
+ focus: ->
+ @send('\x1b[I') if @sendFocus
+ @showCursor()
+
+ blur: ->
+ @cursorState = 1
+ @refresh(@y, @y)
+ @send('\x1b[O') if @sendFocus
+
+ open: (parent) ->
+ @parent = parent or @parent
+ throw new Error('Terminal requires a parent element') unless @parent
+
+ # Global elements
+ @context = @parent.ownerDocument.defaultView
+ @document = @parent.ownerDocument
+ @body = @document.getElementsByTagName('body')[0]
+
+ # Main terminal element
+ @element = @document.createElement('div')
+ @element.className = 'terminal focus'
+ @element.style.outline = 'none'
+ @element.setAttribute('tabindex', 0)
+
+ # Terminal lines
+ @children = [];
+ for i in [0..rows]
+ div = @document.createElement('div')
+ @element.appendChild(div)
+ @children.push(div)
+
+ @parent.appendChild(@element);
+
+ # Draw screen
+ @refresh 0, @rows - 1
+
+ @focus()
+ @startBlink()
+
+
+ destroy: ->
+ @readable = false
+ @writable = false
+ @write = -> 0
+
+ @element.parentNode?.removeChild(@element)
+
+
+ refresh: (start, end) ->
+ if end - start >= @rows / 2
+ parent = @element.parentNode
+ parent?.removeChild @element
+
+ width = @cols
+ y = start
+
+ if end >= @lines.length
+ @log "`end` is too large. Most likely a bad CSR."
+ end = @lines.length - 1
+
+ while y <= end
+ row = y + @ydisp
+ 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 += "" if attr isnt @defAttr
+ if data isnt @defAttr
+ classes = []
+ out += "> 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 += "" 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 += "&"
+ when "<"
+ out += "<"
+ when ">"
+ out += ">"
+ else
+ if ch <= " "
+ out += " "
+ else
+ i++ if isWide(ch)
+ out += ch
+ out += "" if i is x
+ attr = data
+ i++
+ out += "" 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: ->
+ 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)
+
+
+ scroll: ->
+ if ++@ybase is @scrollback
+ @ybase = @ybase / 2 | 0
+ @lines = @lines.slice(-(@ybase + @rows) + 1)
+
+ @ydisp = @ybase
+
+ # last line
+ row = @ybase + @rows - 1
+
+ # subtract the bottom scroll region
+ row -= @rows - 1 - @scrollBottom
+ if row is @lines.length
+ # potential optimization:
+ # pushing is faster than splicing
+ # when they amount to the same
+ # behavior.
+ @lines.push @blankLine()
+ else
+ # add our new line
+ @lines.splice row, 0, @blankLine()
+
+ if @scrollTop isnt 0
+ if @ybase isnt 0
+ @ybase--
+ @ydisp = @ybase
+ @lines.splice @ybase + @scrollTop, 1
+
+ # this.maxRange();
+ @updateRange @scrollTop
+ @updateRange @scrollBottom
+
+ scrollDisp: (disp) ->
+ @ydisp += disp
+ if @ydisp > @ybase
+ @ydisp = @ybase
+
+ else
+ @ydisp = 0 if @ydisp < 0
+
+ @refresh 0, @rows - 1
+
+ write: (data) ->
+ @refreshStart = @y
+ @refreshEnd = @y
+
+ if @ybase isnt @ydisp
+ @ydisp = @ybase
+ @maxRange()
+
+ i = 0
+ l = data.length
+ while i < l
+ ch = data[i]
+ switch @state
+ when normal
+ switch ch
+
+ # '\a'
+ when "\u0007"
+ @bell()
+
+ # '\n', '\v', '\f'
+ when "\n", "\u000b", "\u000c"
+ @x = 0 if @convertEol
+
+ @y++
+ if @y > @scrollBottom
+ @y--
+ @scroll()
+
+ # '\r'
+ when "\r"
+ @x = 0
+
+ # '\b'
+ when "\b"
+ @x-- if @x > 0
+
+ # '\t'
+ when "\t"
+ @x = @nextStop()
+
+ # shift out
+ when "\u000e"
+ @setgLevel 1
+
+ # shift in
+ when "\u000f"
+ @setgLevel 0
+
+ # '\e'
+ when "\u001b"
+ @state = escaped
+
+ else
+ # ' '
+ if ch >= " "
+ ch = @charset[ch] if @charset and @charset[ch]
+ if @x >= @cols
+ @x = 0
+ @y++
+ if @y > @scrollBottom
+ @y--
+ @scroll()
+ @lines[@y + @ybase][@x] = [this.curAttr, ch]
+ @x++
+ @updateRange @y
+ if isWide(ch)
+ j = @y + @ybase
+ if @cols < 2 or @x >= @cols
+ @lines[j][@x - 1] = [this.curAttr, " "]
+ break
+
+ @lines[j][@x] = [this.curAttr, " "]
+ @x++
+
+ when escaped
+ switch ch
+ # ESC [ Control Sequence Introducer ( CSI is 0x9b).
+ when "["
+ @params = []
+ @currentParam = 0
+ @state = csi
+
+ # ESC ] Operating System Command ( OSC is 0x9d).
+ when "]"
+ @params = []
+ @currentParam = 0
+ @state = osc
+
+ # ESC P Device Control String ( DCS is 0x90).
+ when "P"
+ @params = []
+ @currentParam = 0
+ @state = dcs
+
+ # ESC _ Application Program Command ( APC is 0x9f).
+ when "_"
+ @state = ignore
+
+ # ESC ^ Privacy Message ( PM is 0x9e).
+ when "^"
+ @state = ignore
+
+ # ESC c Full Reset (RIS).
+ when "c"
+ @reset()
+
+ # ESC E Next Line ( NEL is 0x85).
+ # ESC D Index ( IND is 0x84).
+ when "E"
+ @x = 0
+ when "D"
+ @index()
+
+ # ESC M Reverse Index ( RI is 0x8d).
+ when "M"
+ @reverseIndex()
+
+ # ESC % Select default/utf-8 character set.
+ # @ = default, G = utf-8
+ when "%"
+
+ #this.charset = null;
+ @setgLevel 0
+ @setgCharset 0, Terminal.charsets.US
+ @state = normal
+ i++
+
+ # ESC (,),*,+,-,. Designate G0-G2 Character Set.
+ # <-- this seems to get all the attention
+ when "(", ")" , "*" , "+" , "-" , "."
+ switch ch
+ when "("
+ @gcharset = 0
+ when ")"
+ @gcharset = 1
+ when "*"
+ @gcharset = 2
+ when "+"
+ @gcharset = 3
+ when "-"
+ @gcharset = 1
+ when "."
+ @gcharset = 2
+ @state = charset
+
+ # Designate G3 Character Set (VT300).
+ # A = ISO Latin-1 Supplemental.
+ # Not implemented.
+ when "/"
+ @gcharset = 3
+ @state = charset
+ i--
+
+ # ESC N
+ # Single Shift Select of G2 Character Set
+ # ( SS2 is 0x8e). This affects next character only.
+
+ # ESC O
+ # Single Shift Select of G3 Character Set
+ # ( SS3 is 0x8f). This affects next character only.
+ when "N", "O"
+ break
+
+ # ESC n
+ # Invoke the G2 Character Set as GL (LS2).
+ when "n"
+ @setgLevel 2
+
+ # ESC o
+ # Invoke the G3 Character Set as GL (LS3).
+ when "o"
+ @setgLevel 3
+
+ # ESC |
+ # Invoke the G3 Character Set as GR (LS3R).
+ when "|"
+ @setgLevel 3
+
+ # ESC }
+ # Invoke the G2 Character Set as GR (LS2R).
+ when "}"
+ @setgLevel 2
+
+ # ESC ~
+ # Invoke the G1 Character Set as GR (LS1R).
+ when "~"
+ @setgLevel 1
+
+ # ESC 7 Save Cursor (DECSC).
+ when "7"
+ @saveCursor()
+ @state = normal
+
+ # ESC 8 Restore Cursor (DECRC).
+ when "8"
+ @restoreCursor()
+ @state = normal
+
+ # ESC # 3 DEC line height/width
+ when "#"
+ @state = normal
+ i++
+
+ # ESC H Tab Set (HTS is 0x88).
+ when "H"
+ @tabSet()
+
+ # ESC = Application Keypad (DECPAM).
+ when "="
+ @log "Serial port requested application keypad."
+ @applicationKeypad = true
+ @state = normal
+
+ # ESC > Normal Keypad (DECPNM).
+ when ">"
+ @log "Switching back to normal keypad."
+ @applicationKeypad = false
+ @state = normal
+ else
+ @state = normal
+ @error "Unknown ESC control: %s.", ch
+ when charset
+ switch ch
+ when "0" # DEC Special Character and Line Drawing Set.
+ cs = Terminal.charsets.SCLD
+ when "A" # UK
+ cs = Terminal.charsets.UK
+ when "B" # United States (USASCII).
+ cs = Terminal.charsets.US
+ when "4" # Dutch
+ cs = Terminal.charsets.Dutch
+ # Finnish
+ when "C", "5"
+ cs = Terminal.charsets.Finnish
+ when "R" # French
+ cs = Terminal.charsets.French
+ when "Q" # FrenchCanadian
+ cs = Terminal.charsets.FrenchCanadian
+ when "K" # German
+ cs = Terminal.charsets.German
+ when "Y" # Italian
+ cs = Terminal.charsets.Italian
+ # NorwegianDanish
+ when "E", "6"
+ cs = Terminal.charsets.NorwegianDanish
+ when "Z" # Spanish
+ cs = Terminal.charsets.Spanish
+ # Swedish
+ when "H", "7"
+ cs = Terminal.charsets.Swedish
+ when "=" # Swiss
+ cs = Terminal.charsets.Swiss
+ when "/" # ISOLatin (actually /A)
+ cs = Terminal.charsets.ISOLatin
+ i++
+ else # Default
+ cs = Terminal.charsets.US
+ @setgCharset @gcharset, cs
+ @gcharset = null
+ @state = normal
+ when osc
+
+ # OSC Ps ; Pt ST
+ # OSC Ps ; Pt BEL
+ # Set Text Parameters.
+ if ch is "\u001b" or ch is "\u0007"
+ i++ if ch is "\u001b"
+ @params.push @currentParam
+ switch @params[0]
+ when 0, 1 , 2
+ if @params[1]
+ @title = @params[1] + " - ƸӜƷ butterfly"
+ @handleTitle @title
+
+ when 99
+ # Custom escape to produce raw html
+ html = "
" + @params[1] + "
"
+ @lines[@y + @ybase][@x] = [
+ this.curAttr
+ html
+ ]
+ line = 0
+
+ while line < @get_html_height_in_lines(html) - 1
+ @y++
+ if @y > @scrollBottom
+ @y--
+ @scroll()
+ line++
+ @updateRange @y
+
+ # reset colors
+ @params = []
+ @currentParam = 0
+ @state = normal
+ else
+ unless @params.length
+ if ch >= "0" and ch <= "9"
+ @currentParam = @currentParam * 10 + ch.charCodeAt(0) - 48
+ else if ch is ";"
+ @params.push @currentParam
+ @currentParam = ""
+ else
+ @currentParam += ch
+
+ when csi
+ # '?', '>', '!'
+ if ch is "?" or ch is ">" or ch is "!"
+ @prefix = ch
+ break
+
+ # 0 - 9
+ if ch >= "0" and ch <= "9"
+ @currentParam = @currentParam * 10 + ch.charCodeAt(0) - 48
+ break
+
+ # '$', '"', ' ', '\''
+ if ch is "$" or ch is "\"" or ch is " " or ch is "'"
+ @postfix = ch
+ break
+ @params.push @currentParam
+ @currentParam = 0
+
+ # ';'
+ break if ch is ";"
+ @state = normal
+ switch ch
+ # CSI Ps A
+ # Cursor Up Ps Times (default = 1) (CUU).
+ when "A"
+ @cursorUp @params
+
+ # CSI Ps B
+ # Cursor Down Ps Times (default = 1) (CUD).
+ when "B"
+ @cursorDown @params
+
+ # CSI Ps C
+ # Cursor Forward Ps Times (default = 1) (CUF).
+ when "C"
+ @cursorForward @params
+
+ # CSI Ps D
+ # Cursor Backward Ps Times (default = 1) (CUB).
+ when "D"
+ @cursorBackward @params
+
+ # CSI Ps ; Ps H
+ # Cursor Position [row;column] (default = [1,1]) (CUP).
+ when "H"
+ @cursorPos @params
+
+ # CSI Ps J Erase in Display (ED).
+ when "J"
+ @eraseInDisplay @params
+
+ # CSI Ps K Erase in Line (EL).
+ when "K"
+ @eraseInLine @params
+
+ # CSI Pm m Character Attributes (SGR).
+ when "m"
+ @charAttributes @params unless @prefix
+
+ # CSI Ps n Device Status Report (DSR).
+ when "n"
+ @deviceStatus @params unless @prefix
+
+ # CSI Ps @
+ # Insert Ps (Blank) Character(s) (default = 1) (ICH).
+ when "@"
+ @insertChars @params
+
+ # CSI Ps E
+ # Cursor Next Line Ps Times (default = 1) (CNL).
+ when "E"
+ @cursorNextLine @params
+
+ # CSI Ps F
+ # Cursor Preceding Line Ps Times (default = 1) (CNL).
+ when "F"
+ @cursorPrecedingLine @params
+
+ # CSI Ps G
+ # Cursor Character Absolute [column] (default = [row,1]) (CHA).
+ when "G"
+ @cursorCharAbsolute @params
+
+ # CSI Ps L
+ # Insert Ps Line(s) (default = 1) (IL).
+ when "L"
+ @insertLines @params
+
+ # CSI Ps M
+ # Delete Ps Line(s) (default = 1) (DL).
+ when "M"
+ @deleteLines @params
+
+ # CSI Ps P
+ # Delete Ps Character(s) (default = 1) (DCH).
+ when "P"
+ @deleteChars @params
+
+ # CSI Ps X
+ # Erase Ps Character(s) (default = 1) (ECH).
+ when "X"
+ @eraseChars @params
+
+ # CSI Pm ` Character Position Absolute
+ # [column] (default = [row,1]) (HPA).
+ when "`"
+ @charPosAbsolute @params
+
+ # 141 61 a * HPR -
+ # Horizontal Position Relative
+ when "a"
+ @HPositionRelative @params
+
+ # CSI P s c
+ # Send Device Attributes (Primary DA).
+ # CSI > P s c
+ # Send Device Attributes (Secondary DA)
+ when "c"
+ @sendDeviceAttributes @params
+
+ # CSI Pm d
+ # Line Position Absolute [row] (default = [1,column]) (VPA).
+ when "d"
+ @linePosAbsolute @params
+
+ # 145 65 e * VPR - Vertical Position Relative
+ when "e"
+ @VPositionRelative @params
+
+ # CSI Ps ; Ps f
+ # Horizontal and Vertical Position [row;column] (default =
+ # [1,1]) (HVP).
+ when "f"
+ @HVPosition @params
+
+ # CSI Pm h Set Mode (SM).
+ # CSI ? Pm h - mouse escape codes, cursor escape codes
+ when "h"
+ @setMode @params
+
+ # CSI Pm l Reset Mode (RM).
+ # CSI ? Pm l
+ when "l"
+ @resetMode @params
+
+ # CSI Ps ; Ps r
+ # Set Scrolling Region [top;bottom] (default = full size of win-
+ # dow) (DECSTBM).
+ # CSI ? Pm r
+ when "r"
+ @setScrollRegion @params
+
+ # CSI s
+ # Save cursor (ANSI.SYS).
+ when "s"
+ @saveCursor @params
+
+ # CSI u
+ # Restore cursor (ANSI.SYS).
+ when "u"
+ @restoreCursor @params
+
+ # CSI Ps I
+ # Cursor Forward Tabulation Ps tab stops (default = 1) (CHT).
+ when "I"
+ @cursorForwardTab @params
+
+ # CSI Ps S Scroll up Ps lines (default = 1) (SU).
+ when "S"
+ @scrollUp @params
+
+ # CSI Ps T Scroll down Ps lines (default = 1) (SD).
+ # CSI Ps ; Ps ; Ps ; Ps ; Ps T
+ # CSI > Ps; Ps T
+ when "T"
+ @scrollDown @params if @params.length < 2 and not @prefix
+
+ # CSI Ps Z
+ # Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).
+ when "Z"
+ @cursorBackwardTab @params
+
+ # CSI Ps b Repeat the preceding graphic character Ps times (REP).
+ when "b"
+ @repeatPrecedingCharacter @params
+
+ # CSI Ps g Tab Clear (TBC).
+ when "g"
+ @tabClear @params
+
+ # CSI > Ps p Set pointer mode.
+ # CSI ! p Soft terminal reset (DECSTR).
+ # CSI Ps$ p
+ # Request ANSI mode (DECRQM).
+ # CSI ? Ps$ p
+ # Request DEC private mode (DECRQM).
+ # CSI Ps ; Ps " p
+ when "p"
+ switch @prefix
+
+ # case '>':
+ # this.setPointerMode(this.params);
+ # break;
+ when "!"
+ @softReset @params
+
+ else
+ @error "Unknown CSI code: %s.", ch
+ @prefix = ""
+ @postfix = ""
+ when dcs
+ if ch is "\u001b" or ch is "\u0007"
+ i++ if ch is "\u001b"
+ switch @prefix
+ # User-Defined Keys (DECUDK).
+ when ""
+ break
+
+ # Request Status String (DECRQSS).
+ # test: echo -e '\eP$q"p\e\\'
+ when "$q"
+ pt = @currentParam
+ valid = false
+ switch pt
+
+ # DECSCA
+ when "\"q"
+ pt = "0\"q"
+
+ # DECSCL
+ when "\"p"
+ pt = "61\"p"
+
+ # DECSTBM
+ when "r"
+ pt = "" + (@scrollTop + 1) + ";" + (@scrollBottom + 1) + "r"
+
+ # SGR
+ when "m"
+ pt = "0m"
+
+ else
+ @error "Unknown DCS Pt: %s.", pt
+ pt = ""
+
+ @send "\u001bP" + +valid + "$r" + pt + "\u001b\\"
+
+ # Set Termcap/Terminfo Data (xterm, experimental).
+ when "+p"
+ break
+ # Request Termcap/Terminfo String (xterm, experimental)
+ # Regular xterm does not even respond to this sequence.
+ # This can cause a small glitch in vim.
+ # test: echo -ne '\eP+q6b64\e\\'
+ when "+q"
+ pt = @currentParam
+ valid = false
+ @send "\u001bP" + +valid + "+r" + pt + "\u001b\\"
+
+ else
+ @error "Unknown DCS prefix: %s.", @prefix
+
+ @currentParam = 0
+ @prefix = ""
+ @state = normal
+
+ else unless @currentParam
+ if not @prefix and ch isnt "$" and ch isnt "+"
+ @currentParam = ch
+ else if @prefix.length is 2
+ @currentParam = ch
+ else
+ @prefix += ch
+ else
+ @currentParam += ch
+
+ when ignore
+ # For PM and APC.
+ if ch is "\u001b" or ch is "\u0007"
+ i++ if ch is "\u001b"
+ @state = normal
+ i++
+ @updateRange @y
+ @refresh @refreshStart, @refreshEnd
+
+ writeln: (data) ->
+ @write "#{data}\r\n"
+
+ 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
+ # TODO
+ # if ev.shiftKey and ev.keyCode is 45
+ # @emit "paste"
+ # return true
+ # if ev.ctrlKey and ev.keyCode is 45
+ # @emit "copy"
+ # return true
+
+ # 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
+ return cancel(ev)
+
+ if @skipNextKey
+ @skipNextKey = false
+ return true
+
+ switch ev.keyCode
+ # backspace
+ when 8
+ key = if ev.altKey then "\u001b" else ""
+ if ev.shiftKey
+ key += "\x08" # ^H
+ break
+ key += "\x7f" # ^?
+
+ # tab
+ when 9
+ if ev.shiftKey
+ key = "\u001b[Z"
+ break
+ key = "\t"
+
+ # return/enter
+ when 13
+ key = "\r"
+
+ # escape
+ when 27
+ key = "\u001b"
+
+ # left-arrow
+ when 37
+ if @applicationCursor
+ key = "\u001bOD" # SS3 as ^[O for 7-bit
+ #key = '\x8fD'; // SS3 as 0x8f for 8-bit
+ break
+ return true if ev.shiftKey
+ key = "\u001b[D"
+
+ # right-arrow
+ when 39
+ if @applicationCursor
+ key = "\u001bOC"
+ break
+ return true if ev.shiftKey
+ key = "\u001b[C"
+
+ # up-arrow
+ when 38
+ if @applicationCursor
+ key = "\u001bOA"
+ break
+ if ev.ctrlKey
+ @scrollDisp -1
+ return cancel(ev)
+ else if ev.shiftKey
+ return true
+ else
+ key = "\u001b[A"
+
+ # down-arrow
+ when 40
+ if @applicationCursor
+ key = "\u001bOB"
+ break
+ if ev.ctrlKey
+ @scrollDisp 1
+ return cancel(ev)
+ else if ev.shiftKey
+ return true
+ else
+ key = "\u001b[B"
+
+ # delete
+ when 46
+ key = "\u001b[3~"
+
+ # insert
+ when 45
+ key = "\u001b[2~"
+
+ # home
+ when 36
+ if @applicationKeypad
+ key = "\u001bOH"
+ break
+ key = "\u001bOH"
+
+ # end
+ when 35
+ if @applicationKeypad
+ key = "\u001bOF"
+ break
+ key = "\u001bOF"
+
+ # page up
+ when 33
+ if ev.shiftKey
+ @scrollDisp -(@rows - 1)
+ return cancel(ev)
+ else
+ key = "\u001b[5~"
+
+ # page down
+ when 34
+ if ev.shiftKey
+ @scrollDisp @rows - 1
+ return cancel(ev)
+ else
+ key = "\u001b[6~"
+
+ # F1
+ when 112
+ key = "\u001bOP"
+
+ # F2
+ when 113
+ key = "\u001bOQ"
+
+ # F3
+ when 114
+ key = "\u001bOR"
+
+ # F4
+ when 115
+ key = "\u001bOS"
+
+ # F5
+ when 116
+ key = "\u001b[15~"
+
+ # F6
+ when 117
+ key = "\u001b[17~"
+
+ # F7
+ when 118
+ key = "\u001b[18~"
+
+ # F8
+ when 119
+ key = "\u001b[19~"
+
+ # F9
+ when 120
+ key = "\u001b[20~"
+
+ # F10
+ when 121
+ key = "\u001b[21~"
+
+ # F11
+ when 122
+ key = "\u001b[23~"
+
+ # F12
+ when 123
+ key = "\u001b[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 (->
+ self.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 = "\u001b" + String.fromCharCode(ev.keyCode + 32)
+ else if ev.keyCode is 192
+ key = "\u001b`"
+ else
+ key = "\u001b" + (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)
+
+ # TODO
+ # @emit "keydown", ev
+ # @emit "key", key, ev
+ @showCursor()
+ cancel ev
diff --git a/butterfly/static/coffees/virtual_input.coffee b/butterfly/static/coffees/virtual_input.coffee
new file mode 100644
index 0000000..4d64a18
--- /dev/null
+++ b/butterfly/static/coffees/virtual_input.coffee
@@ -0,0 +1,68 @@
+try
+ document.createEvent("TouchEvent")
+ virtual_input = true
+catch e
+ virtual_input = false
+
+
+if virtual_input
+ ctrl = false
+ alt = false
+ first = true
+ virtual_input = document.createElement 'input'
+ virtual_input.type = 'password'
+ virtual_input.style.position = 'fixed'
+ virtual_input.style.top = 0
+ virtual_input.style.left = 0
+ virtual_input.style.border = 'none'
+ virtual_input.style.outline = 'none'
+ virtual_input.style.opacity = 0
+ virtual_input.value = '0'
+ document.body.appendChild virtual_input
+
+ virtual_input.addEventListener 'blur', ->
+ setTimeout((=> @focus()), 10)
+
+ addEventListener 'click', ->
+ virtual_input.focus()
+
+ addEventListener 'touchstart', (e) ->
+ if e.touches.length == 1
+ ctrl = true
+ else if e.touches.length == 2
+ ctrl = false
+ alt = true
+ else if e.touches.length == 3
+ ctrl = true
+ alt = true
+
+ virtual_input.addEventListener 'keydown', (e) ->
+ term.keyDown(e)
+ return true
+
+ virtual_input.addEventListener 'input', (e) ->
+ len = @value.length
+
+ if len == 0
+ e.keyCode = 8
+ term.keyDown e
+ @value = '0'
+ return true
+
+ e.keyCode = @value.charAt(1).charCodeAt(0)
+
+ if (ctrl or alt) and not first
+ e.keyCode = @value.charAt(1).charCodeAt(0)
+ e.ctrlKey = ctrl
+ e.altKey = alt
+ if e.keyCode >= 97 && e.keyCode <= 122
+ e.keyCode -= 32
+ term.keyDown e
+ @value = '0'
+ ctrl = alt = false
+ return true
+
+ term.keyPress e
+ first = false
+ @value = '0'
+ true
diff --git a/butterfly/static/javascripts/main.js b/butterfly/static/javascripts/main.js
index a852308..6a8f797 100644
--- a/butterfly/static/javascripts/main.js
+++ b/butterfly/static/javascripts/main.js
@@ -1,5 +1,114 @@
// Generated by CoffeeScript 1.6.3
-var $, alt, cols, ctrl, e, first, quit, resize, rows, term, virtual_input, ws, ws_url;
+var $, alt, bench, cbench, cols, ctrl, e, first, quit, resize, rows, state, term, virtual_input, ws, ws_url;
+
+term = ws = null;
+
+cols = rows = null;
+
+quit = false;
+
+$ = document.querySelectorAll.bind(document);
+
+ws_url = 'ws://' + document.location.host + '/ws' + location.pathname;
+
+ws = new WebSocket(ws_url);
+
+ws.onopen = function() {
+ console.log("WebSocket open", arguments);
+ term = new Terminal({
+ visualBell: 100,
+ screenKeys: true,
+ scrollback: 100000
+ });
+ term.on("data", function(data) {
+ return ws.send('SH|' + data);
+ });
+ term.on("title", function(title) {
+ return document.title = title;
+ });
+ term.open($('main')[0]);
+ $('.terminal')[0].style = '';
+ return resize();
+};
+
+ws.onerror = function() {
+ return console.log("WebSocket error", arguments);
+};
+
+ws.onmessage = function(e) {
+ return term.write(e.data);
+};
+
+ws.onclose = function() {
+ if (term) {
+ term.destroy();
+ }
+ console.log("WebSocket closed", arguments);
+ quit = true;
+ return open('', '_self').close();
+};
+
+addEventListener('beforeunload', function() {
+ if (!quit) {
+ return 'This will exit the terminal session';
+ }
+});
+
+addEventListener('resize', resize = function() {
+ var div, eh, ew, fake_term, fake_term_div, fake_term_line, main, main_bb, _i, _len, _ref;
+ main = $('main')[0];
+ fake_term = document.createElement('div');
+ fake_term.className = 'terminal test';
+ fake_term_div = document.createElement('div');
+ fake_term_line = document.createElement('span');
+ fake_term_line.textContent = '0123456789';
+ fake_term_div.appendChild(fake_term_line);
+ fake_term.appendChild(fake_term_div);
+ main.appendChild(fake_term);
+ ew = fake_term_line.getBoundingClientRect().width;
+ eh = fake_term_div.getBoundingClientRect().height;
+ main.removeChild(fake_term);
+ main_bb = main.getBoundingClientRect();
+ cols = Math.floor(10 * main_bb.width / ew) - 1;
+ rows = Math.floor(main_bb.height / eh);
+ console.log("Computed " + cols + " cols and " + rows + " rows from ", main_bb, ew, eh);
+ term.resize(cols, rows);
+ _ref = $('.terminal div');
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ div = _ref[_i];
+ div.style.height = eh + 'px';
+ }
+ return ws.send("RS|" + cols + "," + rows);
+});
+
+bench = function(n) {
+ var rnd, t0;
+ if (n == null) {
+ n = 100000000;
+ }
+ rnd = '';
+ while (rnd.length < n) {
+ rnd += Math.random().toString(36).substring(2);
+ }
+ t0 = (new Date()).getTime();
+ term.write(rnd);
+ return console.log("" + n + " chars in " + ((new Date()).getTime() - t0) + " ms");
+};
+
+cbench = function(n) {
+ var rnd, t0;
+ if (n == null) {
+ n = 100000000;
+ }
+ rnd = '';
+ while (rnd.length < n) {
+ rnd += "\x1b[" + (30 + parseInt(Math.random() * 20)) + "m";
+ rnd += Math.random().toString(36).substring(2);
+ }
+ t0 = (new Date()).getTime();
+ term.write(rnd);
+ return console.log("" + n + " chars + colors in " + ((new Date()).getTime() - t0) + " ms");
+};
try {
document.createEvent("TouchEvent");
@@ -9,12 +118,6 @@ try {
virtual_input = false;
}
-term = ws = null;
-
-cols = rows = null;
-
-quit = false;
-
if (virtual_input) {
ctrl = false;
alt = false;
@@ -82,76 +185,35 @@ if (virtual_input) {
});
}
-$ = document.querySelectorAll.bind(document);
-
-ws_url = 'ws://' + document.location.host + '/ws' + location.pathname;
-
-ws = new WebSocket(ws_url);
-
-ws.onopen = function() {
- console.log("WebSocket open", arguments);
- term = new Terminal({
- visualBell: 100,
- screenKeys: true,
- scrollback: -1
- });
- term.on("data", function(data) {
- return ws.send('SH|' + data);
- });
- term.on("title", function(title) {
- return document.title = title;
- });
- term.open($('main')[0]);
- $('.terminal')[0].style = '';
- return resize();
+state = {
+ x: null,
+ y: null
};
-ws.onclose = function() {
- if (term) {
- term.destroy();
- }
- console.log("WebSocket closed", arguments);
- quit = true;
- return open('', '_self').close();
-};
-
-ws.onerror = function() {
- return console.log("WebSocket error", arguments);
-};
-
-ws.onmessage = function(e) {
- return term.write(event.data);
-};
-
-addEventListener('beforeunload', function() {
- if (!quit) {
- return 'This will exit the terminal session';
+document.addEventListener('keydown', function(e) {
+ var _ref;
+ if (e.shiftKey && ((37 <= (_ref = e.keyCode) && _ref <= 40))) {
+ if (state.y === null) {
+ state.y = term.ybase + term.y;
+ }
+ if (e.keyCode === 38) {
+ state.y--;
+ if (state.y < term.ybase) {
+ state.y = term.ybase;
+ }
+ } else if (e.keyCode === 40) {
+ state.y++;
+ if (state.y > term.ybase + term.y) {
+ state.y = term.ybase + term.y;
+ }
+ }
+ term.emit('data', ' \x0b\x15');
+ if (state.y !== term.ybase + term.y) {
+ term.emit('data', term.grabText(0, term.cols - 1, state.y, state.y).replace('\n', ''));
+ }
+ e.stopPropagation();
+ return false;
+ } else {
+ return state.x = state.y = null;
}
});
-
-addEventListener('resize', resize = function() {
- var div, eh, ew, fake_term, fake_term_div, fake_term_line, main, main_bb, _i, _len, _ref;
- main = $('main')[0];
- fake_term = document.createElement('div');
- fake_term.className = 'terminal test';
- fake_term_div = document.createElement('div');
- fake_term_line = document.createElement('span');
- fake_term_line.textContent = '0123456789';
- fake_term_div.appendChild(fake_term_line);
- fake_term.appendChild(fake_term_div);
- main.appendChild(fake_term);
- ew = fake_term_line.getBoundingClientRect().width;
- eh = fake_term_div.getBoundingClientRect().height;
- main.removeChild(fake_term);
- main_bb = main.getBoundingClientRect();
- cols = Math.floor(10 * main_bb.width / ew) - 1;
- rows = Math.floor(main_bb.height / eh);
- console.log("Computed " + cols + " cols and " + rows + " rows from ", main_bb, ew, eh);
- term.resize(cols, rows);
- _ref = $('.terminal div');
- for (_i = 0, _len = _ref.length; _i < _len; _i++) {
- div = _ref[_i];
- div.style.height = eh + 'px';
- }
- return ws.send("RS|" + cols + "," + rows);
-});
diff --git a/butterfly/utils.py b/butterfly/utils.py
index cc98c7e..df6c32a 100644
--- a/butterfly/utils.py
+++ b/butterfly/utils.py
@@ -107,7 +107,7 @@ def get_env(inode):
if '=' in keyval:
key, val = keyval.split('=', 1)
env[key] = val
- return env
+ return env
class Socket(object):