async = require 'async' fs = require 'fs' class Terminal # public: constructor: (width, height) -> @width = 1 @height = 1 @text = [] @attributes = [] @x = 0 @y = 0 @a = 0 # cursor attributes @partial = '' @resize width, height resize: (width, height) -> # FIXME: write a version that retains some of the data @width = width @height = height @text = [] @attributes = [] for y in [0...height] @text[y] = [] @attributes[y] = [] for x in [0...width] @text[y].push ' ' @attributes[y].push 0 # pass data from stdout update: (data) -> return unless data?.length > 0 if @partial.length > 0 data = @partial + data @partial = '' parts = data.split(/\x1b\[/) if parts.length > 1 if -1 is @escape_sequence_length parts[parts.length - 1] @partial = parts.pop() if parts.length > 0 for i in [0...parts.length] if i is 0 @update_text parts[i] else @update_sequence_then_text parts[i] return clear_rest_of_line: -> for i in [@x...@width] @text[@y][i] = ' ' @attributes[@y][i] = @a add_new_line: -> # clear top line for i in [0...@width] @text[0][i] = ' ' @attributes[0][i] = 0 # move (newly cleared) top line to the bottom tmp = @text.shift() @text.push(tmp) tmp = @attributes.shift() @attributes.push(tmp) # slide cursor up with rest of text @y -= 1 wrap_to_next_line: -> if @y is @height - 1 @add_new_line() @y += 1 @x = 0 # str has no escape sequences update_text: (str) -> return unless str.length > 0 for c in str switch c when '\t' # tab @update_text " ".substr(@x % 8) when '\x07' # bell false when '\x0d' # cr @x = 0 when '\x08' # backspace if @x > 0 @x -= 1 @text[@y][@x] = ' ' # should this set the attribute too? when '\x0a', '\x0b' # lf, vertical tab (same thing) @wrap_to_next_line() else @text[@y][@x] = c @attributes[@y][@x] = @a @x += 1 if @x is @width @wrap_to_next_line() return set_attribute_bits: (mask, value) -> @a = (@a & ~mask) | value csi_m: default: "0", go: -> for i in arguments fixed = i while fixed[0] is '0' fixed = fixed.substr 1 switch fixed when '' @set_attribute_bits 0xffffff, 0 when '1' # bold @set_attribute_bits 0x10000, 0x10000 when '4' # underline @set_attribute_bits 0x20000, 0x20000 when '5' # blink @set_attribute_bits 0x40000, 0x40000 when '8' # invisible @set_attribute_bits 0x80000, 0x80000 when '22' # not bold... according to a page @set_attribute_bits 0x10000, 0 when '21' # ... though this would make more sense for "not bold" @set_attribute_bits 0x10000, 0 when '24' # not underline @set_attribute_bits 0x20000, 0 when '25' # not blink @set_attribute_bits 0x40000, 0 when '28' # not invisible @set_attribute_bits 0x80000, 0 when '30' # fg black @set_attribute_bits 0xff, 0x00 when '31' # fg red @set_attribute_bits 0xff, 0x01 when '32' # fg green @set_attribute_bits 0xff, 0x02 when '33' # fg yellow @set_attribute_bits 0xff, 0x03 when '34' # fg blue @set_attribute_bits 0xff, 0x04 when '35' # fg magenta @set_attribute_bits 0xff, 0x05 when '36' # fg cyan @set_attribute_bits 0xff, 0x06 when '37', '39' # fg white (39 is default) @set_attribute_bits 0xff, 0x07 when '40' # bg black @set_attribute_bits 0xff00, 0x0000 when '41' # bg red @set_attribute_bits 0xff00, 0x0100 when '42' # bg green @set_attribute_bits 0xff00, 0x0200 when '43' # bg yellow @set_attribute_bits 0xff00, 0x0300 when '44' # bg blue @set_attribute_bits 0xff00, 0x0400 when '45' # bg magenta @set_attribute_bits 0xff00, 0x0500 when '46' # bg cyan @set_attribute_bits 0xff00, 0x0600 when '47' # bg white @set_attribute_bits 0xff00, 0x0700 when '49' # bg default @set_attribute_bits 0xff00, 0x0000 else # if we don't recognize the style, go back to default @set_attribute_bits 0xffffff, 0 return # str is the whole escape sequence (minus the esc[ prefix) update_sequence: (str) -> command = @["csi_#{str.substr str.length - 1}"] return unless command? args = str.substr(0, str.length - 1).split ';' for i in [0...args.length] if args[i] is '' args[i] = command.default command.go.call this, args... update_sequence_then_text: (str) -> len = @escape_sequence_length str if len is -1 console.log "couldn't find escape sequence here: #{str.substr 0, 25}" @update_text "ESC[" + str else @update_sequence str.substr 0, len @update_text str.substr len escape_sequence_length: (str) -> parts = str.match(/^[0-9;?]{0,25}./) return -1 unless parts? return parts[0].length exports.new = (width, height) -> return new Terminal width, height