# FILE: autoload/conque_term/conque.py {{{ # AUTHOR: Nico Raffo # WEBSITE: http://conque.googlecode.com # MODIFIED: 2010-11-15 # VERSION: 2.0, for Vim 7.0 # LICENSE: # Conque - Vim terminal/console emulator # Copyright (C) 2009-2010 Nico Raffo # # MIT License # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. }}} """ Vim terminal emulator. The Conque does two things. First, it handles communication between Vim and the terminal/console subprocess. For example, Vim uses the Conque.write() method to send input, and Conque.read() to update the terminal buffer. Second, the Conque class handles Unix terminal escape sequence parsing. """ import vim import re import math class Conque: # CLASS PROPERTIES {{{ # screen object screen = None # subprocess object proc = None # terminal dimensions and scrolling region columns = 80 # same as $COLUMNS lines = 24 # same as $LINES working_columns = 80 # can be changed by CSI ? 3 l/h working_lines = 24 # can be changed by CSI r # top/bottom of the scroll region top = 1 # relative to top of screen bottom = 24 # relative to top of screen # cursor position l = 1 # current cursor line c = 1 # current cursor column # autowrap mode autowrap = True # absolute coordinate mode absolute_coords = True # tabstop positions tabstops = [] # enable colors enable_colors = True # color changes color_changes = {} # color history color_history = {} # don't wrap table output unwrap_tables = True # wrap CUF/CUB around line breaks wrap_cursor = False # do we need to move the cursor? cursor_set = False # current character set, ascii or graphics character_set = 'ascii' # used for auto_read actions read_count = 0 # }}} # constructor def __init__(self): # {{{ self.screen = ConqueScreen() # }}} # start program and initialize this instance def open(self, command, options): # {{{ # int vars self.columns = vim.current.window.width self.lines = vim.current.window.height self.working_columns = vim.current.window.width self.working_lines = vim.current.window.height self.bottom = vim.current.window.height # init color self.enable_colors = options['color'] # init tabstops self.init_tabstops() # open command self.proc = ConqueSubprocess() self.proc.open(command, {'TERM': options['TERM'], 'CONQUE': '1', 'LINES': str(self.lines), 'COLUMNS': str(self.columns)}) # send window size signal, in case LINES/COLUMNS is ignored self.update_window_size(True) # }}} # write to pty def write(self, input, set_cursor=True, read=True): # {{{ # check if window size has changed if read: self.update_window_size() # write and read self.proc.write(input) # read output immediately if read: self.read(1, set_cursor) # }}} # convert latin-1 input into utf-8 # XXX - this is a hack, to be removed soon def write_latin1(self, input, set_cursor=True, read=True): # {{{ if CONQUE_PYTHON_VERSION == 2: try: input_unicode = input.decode('latin-1', 'ignore') self.write(input_unicode.encode('utf-8', 'ignore'), set_cursor, read) except: return else: self.write(input, set_cursor, read) # }}} # read from pty, and update buffer def read(self, timeout=1, set_cursor=True, return_output=False, update_buffer=True): # {{{ output = '' # this may not actually work try: # read from subprocess output = self.proc.read(timeout) # and strip null chars output = output.replace(chr(0), '') if output == '': return # for bufferless terminals if not update_buffer: return output chunks = CONQUE_SEQ_REGEX.split(output) # don't go through all the csi regex if length is one (no matches) if len(chunks) == 1: self.plain_text(chunks[0]) else: for s in chunks: if s == '': continue # Check for control character match {{{ if CONQUE_SEQ_REGEX_CTL.match(s[0]): nr = ord(s[0]) if nr in CONQUE_CTL: getattr(self, 'ctl_' + CONQUE_CTL[nr])() else: pass # }}} # check for escape sequence match {{{ elif CONQUE_SEQ_REGEX_CSI.match(s): if s[-1] in CONQUE_ESCAPE: csi = self.parse_csi(s[2:]) getattr(self, 'csi_' + CONQUE_ESCAPE[s[-1]])(csi) else: pass # }}} # check for title match {{{ elif CONQUE_SEQ_REGEX_TITLE.match(s): self.change_title(s[2], s[4:-1]) # }}} # check for hash match {{{ elif CONQUE_SEQ_REGEX_HASH.match(s): if s[-1] in CONQUE_ESCAPE_HASH: getattr(self, 'hash_' + CONQUE_ESCAPE_HASH[s[-1]])() else: pass # }}} # check for charset match {{{ elif CONQUE_SEQ_REGEX_CHAR.match(s): if s[-1] in CONQUE_ESCAPE_CHARSET: getattr(self, 'charset_' + CONQUE_ESCAPE_CHARSET[s[-1]])() else: pass # }}} # check for other escape match {{{ elif CONQUE_SEQ_REGEX_ESC.match(s): if s[-1] in CONQUE_ESCAPE_PLAIN: getattr(self, 'esc_' + CONQUE_ESCAPE_PLAIN[s[-1]])() else: pass # }}} # else process plain text {{{ else: self.plain_text(s) # }}} # check window size if set_cursor: self.screen.set_cursor(self.l, self.c) # we need to set the cursor position self.cursor_set = False vim.command('redraw') except: pass if return_output: if CONQUE_PYTHON_VERSION == 3: return output else: return output.encode(vim.eval('&encoding'), 'replace') # }}} # for polling def auto_read(self): # {{{ # check subprocess status, but not every time since it's CPU expensive if self.read_count == 10: if not self.proc.is_alive(): vim.command('call conque_term#get_instance().close()') return else: self.read_count = 0 self.read_count += 1 # read output self.read(1) # reset timer if self.c == 1: vim.command('call feedkeys("\\", "n")') else: vim.command('call feedkeys("\\", "n")') # stop here if cursor doesn't need to be moved if self.cursor_set: return # otherwise set cursor position try: self.set_cursor(self.l, self.c) except: pass self.cursor_set = True # }}} ############################################################################################### # Plain text # {{{ def plain_text(self, input): # translate input into correct character set if self.character_set == 'graphics': old_input = input input = '' for i in range(0, len(old_input)): chrd = ord(old_input[i]) try: if chrd > 255: input = input + old_input[i] else: input = input + unichr(CONQUE_GRAPHICS_SET[chrd]) except: pass current_line = self.screen[self.l] if len(current_line) < self.working_columns: current_line = current_line + ' ' * (self.c - len(current_line)) # if line is wider than screen if self.c + len(input) - 1 > self.working_columns: # Table formatting hack if self.unwrap_tables and CONQUE_TABLE_OUTPUT.match(input): self.screen[self.l] = current_line[:self.c - 1] + input + current_line[self.c + len(input) - 1:] self.apply_color(self.c, self.c + len(input)) self.c += len(input) return diff = self.c + len(input) - self.working_columns - 1 # if autowrap is enabled if self.autowrap: self.screen[self.l] = current_line[:self.c - 1] + input[:-1 * diff] self.apply_color(self.c, self.working_columns) self.ctl_nl() self.ctl_cr() remaining = input[-1 * diff:] self.plain_text(remaining) else: self.screen[self.l] = current_line[:self.c - 1] + input[:-1 * diff - 1] + input[-1] self.apply_color(self.c, self.working_columns) self.c = self.working_columns # no autowrap else: self.screen[self.l] = current_line[:self.c - 1] + input + current_line[self.c + len(input) - 1:] self.apply_color(self.c, self.c + len(input)) self.c += len(input) def apply_color(self, start, end, line=0): # stop here if coloration is disabled if not self.enable_colors: return # allow custom line nr to be passed if line: real_line = line else: real_line = self.screen.get_real_line(self.l) # check for previous overlapping coloration to_del = [] if real_line in self.color_history: for i in range(len(self.color_history[real_line])): syn = self.color_history[real_line][i] if syn['start'] >= start and syn['start'] < end: vim.command('syn clear ' + syn['name']) to_del.append(i) # outside if syn['end'] > end: self.exec_highlight(real_line, end, syn['end'], syn['highlight']) elif syn['end'] > start and syn['end'] <= end: vim.command('syn clear ' + syn['name']) to_del.append(i) # outside if syn['start'] < start: self.exec_highlight(real_line, syn['start'], start, syn['highlight']) if len(to_del) > 0: to_del.reverse() for di in to_del: del self.color_history[real_line][di] # if there are no new colors if len(self.color_changes) == 0: return highlight = '' for attr in self.color_changes.keys(): highlight = highlight + ' ' + attr + '=' + self.color_changes[attr] # execute the highlight self.exec_highlight(real_line, start, end, highlight) def exec_highlight(self, real_line, start, end, highlight): unique_key = str(self.proc.pid) syntax_name = 'EscapeSequenceAt_' + unique_key + '_' + str(self.l) + '_' + str(start) + '_' + str(len(self.color_history) + 1) syntax_options = ' contains=ALLBUT,ConqueString,MySQLString,MySQLKeyword oneline' syntax_region = 'syntax match ' + syntax_name + ' /\%' + str(real_line) + 'l\%>' + str(start - 1) + 'c.*\%<' + str(end + 1) + 'c/' + syntax_options syntax_highlight = 'highlight ' + syntax_name + highlight vim.command(syntax_region) vim.command(syntax_highlight) # add syntax name to history if not real_line in self.color_history: self.color_history[real_line] = [] self.color_history[real_line].append({'name': syntax_name, 'start': start, 'end': end, 'highlight': highlight}) # }}} ############################################################################################### # Control functions {{{ def ctl_nl(self): # if we're in a scrolling region, scroll instead of moving cursor down if self.lines != self.working_lines and self.l == self.bottom: del self.screen[self.top] self.screen.insert(self.bottom, '') elif self.l == self.bottom: self.screen.append('') else: self.l += 1 self.color_changes = {} def ctl_cr(self): self.c = 1 self.color_changes = {} def ctl_bs(self): if self.c > 1: self.c += -1 def ctl_soh(self): pass def ctl_stx(self): pass def ctl_bel(self): vim.command('call conque_term#bell()') def ctl_tab(self): # default tabstop location ts = self.working_columns # check set tabstops for i in range(self.c, len(self.tabstops)): if self.tabstops[i]: ts = i + 1 break self.c = ts def ctl_so(self): self.character_set = 'graphics' def ctl_si(self): self.character_set = 'ascii' # }}} ############################################################################################### # CSI functions {{{ def csi_font(self, csi): # {{{ if not self.enable_colors: return # defaults to 0 if len(csi['vals']) == 0: csi['vals'] = [0] # 256 xterm color foreground if len(csi['vals']) == 3 and csi['vals'][0] == 38 and csi['vals'][1] == 5: self.color_changes['ctermfg'] = str(csi['vals'][2]) self.color_changes['guifg'] = '#' + self.xterm_to_rgb(csi['vals'][2]) # 256 xterm color background elif len(csi['vals']) == 3 and csi['vals'][0] == 48 and csi['vals'][1] == 5: self.color_changes['ctermbg'] = str(csi['vals'][2]) self.color_changes['guibg'] = '#' + self.xterm_to_rgb(csi['vals'][2]) # 16 colors else: for val in csi['vals']: if val in CONQUE_FONT: # ignore starting normal colors if CONQUE_FONT[val]['normal'] and len(self.color_changes) == 0: continue # clear color changes elif CONQUE_FONT[val]['normal']: self.color_changes = {} # save these color attributes for next plain_text() call else: for attr in CONQUE_FONT[val]['attributes'].keys(): if attr in self.color_changes and (attr == 'cterm' or attr == 'gui'): self.color_changes[attr] += ',' + CONQUE_FONT[val]['attributes'][attr] else: self.color_changes[attr] = CONQUE_FONT[val]['attributes'][attr] # }}} def csi_clear_line(self, csi): # {{{ # this escape defaults to 0 if len(csi['vals']) == 0: csi['val'] = 0 # 0 means cursor right if csi['val'] == 0: self.screen[self.l] = self.screen[self.l][0:self.c - 1] # 1 means cursor left elif csi['val'] == 1: self.screen[self.l] = ' ' * (self.c) + self.screen[self.l][self.c:] # clear entire line elif csi['val'] == 2: self.screen[self.l] = '' # clear colors if csi['val'] == 2 or (csi['val'] == 0 and self.c == 1): real_line = self.screen.get_real_line(self.l) if real_line in self.color_history: for syn in self.color_history[real_line]: vim.command('syn clear ' + syn['name']) # }}} def csi_cursor_right(self, csi): # {{{ # we use 1 even if escape explicitly specifies 0 if csi['val'] == 0: csi['val'] = 1 if self.wrap_cursor and self.c + csi['val'] > self.working_columns: self.l += int(math.floor((self.c + csi['val']) / self.working_columns)) self.c = (self.c + csi['val']) % self.working_columns return self.c = self.bound(self.c + csi['val'], 1, self.working_columns) # }}} def csi_cursor_left(self, csi): # {{{ # we use 1 even if escape explicitly specifies 0 if csi['val'] == 0: csi['val'] = 1 if self.wrap_cursor and csi['val'] >= self.c: self.l += int(math.floor((self.c - csi['val']) / self.working_columns)) self.c = self.working_columns - (csi['val'] - self.c) % self.working_columns return self.c = self.bound(self.c - csi['val'], 1, self.working_columns) # }}} def csi_cursor_to_column(self, csi): # {{{ self.c = self.bound(csi['val'], 1, self.working_columns) # }}} def csi_cursor_up(self, csi): # {{{ self.l = self.bound(self.l - csi['val'], self.top, self.bottom) self.color_changes = {} # }}} def csi_cursor_down(self, csi): # {{{ self.l = self.bound(self.l + csi['val'], self.top, self.bottom) self.color_changes = {} # }}} def csi_clear_screen(self, csi): # {{{ # default to 0 if len(csi['vals']) == 0: csi['val'] = 0 # 2 == clear entire screen if csi['val'] == 2: self.l = 1 self.c = 1 self.screen.clear() # 0 == clear down elif csi['val'] == 0: for l in range(self.bound(self.l + 1, 1, self.lines), self.lines + 1): self.screen[l] = '' # clear end of current line self.csi_clear_line(self.parse_csi('K')) # 1 == clear up elif csi['val'] == 1: for l in range(1, self.bound(self.l, 1, self.lines + 1)): self.screen[l] = '' # clear beginning of current line self.csi_clear_line(self.parse_csi('1K')) # clear coloration if csi['val'] == 2 or csi['val'] == 0: real_line = self.screen.get_real_line(self.l) for line in self.color_history.keys(): if line >= real_line: for syn in self.color_history[line]: vim.command('syn clear ' + syn['name']) self.color_changes = {} # }}} def csi_delete_chars(self, csi): # {{{ self.screen[self.l] = self.screen[self.l][:self.c] + self.screen[self.l][self.c + csi['val']:] # }}} def csi_add_spaces(self, csi): # {{{ self.screen[self.l] = self.screen[self.l][: self.c - 1] + ' ' * csi['val'] + self.screen[self.l][self.c:] # }}} def csi_cursor(self, csi): # {{{ if len(csi['vals']) == 2: new_line = csi['vals'][0] new_col = csi['vals'][1] else: new_line = 1 new_col = 1 if self.absolute_coords: self.l = self.bound(new_line, 1, self.lines) else: self.l = self.bound(self.top + new_line - 1, self.top, self.bottom) self.c = self.bound(new_col, 1, self.working_columns) if self.c > len(self.screen[self.l]): self.screen[self.l] = self.screen[self.l] + ' ' * (self.c - len(self.screen[self.l])) # }}} def csi_set_coords(self, csi): # {{{ if len(csi['vals']) == 2: new_start = csi['vals'][0] new_end = csi['vals'][1] else: new_start = 1 new_end = vim.current.window.height self.top = new_start self.bottom = new_end self.working_lines = new_end - new_start + 1 # if cursor is outside scrolling region, reset it if self.l < self.top: self.l = self.top elif self.l > self.bottom: self.l = self.bottom self.color_changes = {} # }}} def csi_tab_clear(self, csi): # {{{ # this escape defaults to 0 if len(csi['vals']) == 0: csi['val'] = 0 if csi['val'] == 0: self.tabstops[self.c - 1] = False elif csi['val'] == 3: for i in range(0, self.columns + 1): self.tabstops[i] = False # }}} def csi_set(self, csi): # {{{ # 132 cols if csi['val'] == 3: self.csi_clear_screen(self.parse_csi('2J')) self.working_columns = 132 # relative_origin elif csi['val'] == 6: self.absolute_coords = False # set auto wrap elif csi['val'] == 7: self.autowrap = True self.color_changes = {} # }}} def csi_reset(self, csi): # {{{ # 80 cols if csi['val'] == 3: self.csi_clear_screen(self.parse_csi('2J')) self.working_columns = 80 # absolute origin elif csi['val'] == 6: self.absolute_coords = True # reset auto wrap elif csi['val'] == 7: self.autowrap = False self.color_changes = {} # }}} # }}} ############################################################################################### # ESC functions {{{ def esc_scroll_up(self): # {{{ self.ctl_nl() self.color_changes = {} # }}} def esc_next_line(self): # {{{ self.ctl_nl() self.c = 1 # }}} def esc_set_tab(self): # {{{ if self.c <= len(self.tabstops): self.tabstops[self.c - 1] = True # }}} def esc_scroll_down(self): # {{{ if self.l == self.top: del self.screen[self.bottom] self.screen.insert(self.top, '') else: self.l += -1 self.color_changes = {} # }}} # }}} ############################################################################################### # HASH functions {{{ def hash_screen_alignment_test(self): # {{{ self.csi_clear_screen(self.parse_csi('2J')) self.working_lines = self.lines for l in range(1, self.lines + 1): self.screen[l] = 'E' * self.working_columns # }}} # }}} ############################################################################################### # CHARSET functions {{{ def charset_us(self): self.character_set = 'ascii' def charset_uk(self): self.character_set = 'ascii' def charset_graphics(self): self.character_set = 'graphics' # }}} ############################################################################################### # Random stuff {{{ def set_cursor(self, line, col): self.screen.set_cursor(line, col) def change_title(self, key, val): if key == '0' or key == '2': vim.command('setlocal statusline=' + re.escape(val)) try: vim.command('set titlestring=' + re.escape(val)) except: pass def paste(self): input = vim.eval('@@') input = input.replace("\n", "\r") self.read(50) def paste_selection(self): input = vim.eval('@@') input = input.replace("\n", "\r") self.write(input) def update_window_size(self, force=False): # resize if needed if force or vim.current.window.width != self.columns or vim.current.window.height != self.lines: # reset all window size attributes to default self.columns = vim.current.window.width self.lines = vim.current.window.height self.working_columns = vim.current.window.width self.working_lines = vim.current.window.height self.bottom = vim.current.window.height # reset screen object attributes self.l = self.screen.reset_size(self.l) # reset tabstops self.init_tabstops() # signal process that screen size has changed self.proc.window_resize(self.lines, self.columns) def insert_enter(self): # check window size self.update_window_size() # we need to set the cursor position self.cursor_set = False def init_tabstops(self): for i in range(0, self.columns + 1): if i % 8 == 0: self.tabstops.append(True) else: self.tabstops.append(False) def idle(self): pass def resume(self): pass def close(self): self.proc.close() def abort(self): self.proc.signal(1) # }}} ############################################################################################### # Utility {{{ def parse_csi(self, s): # {{{ attr = {'key': s[-1], 'flag': '', 'val': 1, 'vals': []} if len(s) == 1: return attr full = s[0:-1] if full[0] == '?': full = full[1:] attr['flag'] = '?' if full != '': vals = full.split(';') for val in vals: val = re.sub("\D", "", val) if val != '': attr['vals'].append(int(val)) if len(attr['vals']) == 1: attr['val'] = int(attr['vals'][0]) return attr # }}} def bound(self, val, min, max): # {{{ if val > max: return max if val < min: return min return val # }}} def xterm_to_rgb(self, color_code): # {{{ if color_code < 16: ascii_colors = ['000000', 'CD0000', '00CD00', 'CDCD00', '0000EE', 'CD00CD', '00CDCD', 'E5E5E5', '7F7F7F', 'FF0000', '00FF00', 'FFFF00', '5C5CFF', 'FF00FF', '00FFFF', 'FFFFFF'] return ascii_colors[color_code] elif color_code < 232: cc = int(color_code) - 16 p1 = "%02x" % (math.floor(cc / 36) * (255 / 5)) p2 = "%02x" % (math.floor((cc % 36) / 6) * (255 / 5)) p3 = "%02x" % (math.floor(cc % 6) * (255 / 5)) return p1 + p2 + p3 else: grey_tone = "%02x" % math.floor((255 / 24) * (color_code - 232)) return grey_tone + grey_tone + grey_tone # }}} # }}} # vim:foldmethod=marker