zsh/vim/autoload/conque_term/conque.py

976 lines
28 KiB
Python

# FILE: autoload/conque_term/conque.py {{{
# AUTHOR: Nico Raffo <nicoraffo@gmail.com>
# 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("\<right>\<left>", "n")')
else:
vim.command('call feedkeys("\<left>\<right>", "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