import curses from buffer import Buffer, Cursor, StableCursor, EditError KEY_NAME = {-1: 'KEY_IDLE'} class CAttr: pass def setup_screen(scr): for name, value in curses.__dict__.items(): if name.startswith('KEY_'): KEY_NAME[value] = name for c in range(32): if 1 <= c <= 26: keyname = 'KEY_CTRL_%s' % chr(64+c) else: keyname = 'KEY_%03d' % c KEY_NAME.setdefault(c, keyname) curses.start_color() try: curses.use_default_colors() # Python >= 2.4 only fg = -1 bg = -1 except AttributeError: fg = curses.COLOR_WHITE bg = curses.COLOR_BLACK curses.init_pair(1, curses.COLOR_BLUE, bg) CAttr.normal = curses.color_pair(0) CAttr.blue = curses.color_pair(1) | curses.A_BOLD CAttr.cursors = [] n = 3 for name, c1, c2 in [('blue', curses.COLOR_WHITE, curses.COLOR_BLUE), ('red', curses.COLOR_WHITE, curses.COLOR_RED), ('green', curses.COLOR_WHITE, curses.COLOR_GREEN), ('cyan', curses.COLOR_BLACK, curses.COLOR_CYAN), ('yellow', curses.COLOR_BLACK, curses.COLOR_YELLOW), ('magenta', curses.COLOR_WHITE, curses.COLOR_MAGENTA), ('red', curses.COLOR_YELLOW, curses.COLOR_RED)]: curses.init_pair(n, c1, c2) CAttr.cursors.append(curses.color_pair(n)) n += 1 CAttr.statusline_error = CAttr.cursors[-1] | curses.A_BOLD def cursorattr(nb): return CAttr.cursors[nb % len(CAttr.cursors)] class UnhandledKey(Exception): pass class CloseEditor(Exception): pass class EditWindow: show_missing_lines = True def __init__(self, buffer, geometry=(0, 0, 1, 1)): self.buffer = buffer self.geometry = geometry # (x, y, width, height) self.cursor = Cursor(buffer) self.firstchar = StableCursor(buffer) self.cutlines = '' self.last_action = None def display(self, scr, allow_incomplete_line=False): if self.cursor.pos < self.firstchar.pos: self.recenter() self.firstchar.to_line_start() visiblecursors = [] for cur in self.buffer.getcursors(): if cur is not self.cursor and cur.color is not None: visiblecursors.append((cur, cursorattr(cur.color))) cur = self.cursor visiblecursors.append((cur, cursorattr(cur.color))) wx, wy, stdscr_x, stdscr_y = self.geometry xstart = self.firstchar.pos lines = self.buffer.iterlines(xstart) nextline = [] empty_line = ' ' * stdscr_x result = [] def computenextline(): attributes = {} try: if not nextline: nextline.append(lines.next()) except StopIteration: nextline.append('') line = nextline.pop() if len(line) >= stdscr_x: nextline.append(line[stdscr_x-1:]) line = line[:stdscr_x-1] + '\\' attributes[stdscr_x-1] = CAttr.blue xend = xstart + stdscr_x-1 else: xend = xstart + len(line) if line.endswith('\n'): line = line[:-1] else: if self.show_missing_lines: attributes[len(line)] = CAttr.blue line += '\\' xend += 1 for cursor, attr in visiblecursors: if xstart <= cursor.pos < xend: attributes[cursor.pos-xstart] = attr line = line.ljust(stdscr_x) result.append((line, attributes)) return xend for y in range(stdscr_y): xstart = computenextline() while self.cursor.pos >= xstart: if allow_incomplete_line: result.pop(0) xstart = computenextline() else: self.recenter() self.display(scr, allow_incomplete_line=True) return # drawing starts here for y in range(stdscr_y): line, attributes = result[y] attributes = attributes.items() attributes.sort() x0 = 0 for x, attr in attributes: if x > x0: scr.addstr(wy+y, wx+x0, line[x0:x], CAttr.normal) scr.addstr(wy+y, wx+x, line[x], attr) x0 = x+1 if stdscr_x > x0: scr.addstr(wy+y, wx+x0, line[x0:], CAttr.normal) def recenter(self): self.firstchar.pos = self.cursor.pos try: count = self.geometry[3] // 2 while count > 0: p = self.firstchar.pos self.firstchar.move_up() distance = p - self.firstchar.pos count -= (distance // (self.geometry[2]-1) + 1) except EditError: pass def run(self, scr, keyreader): forcecolumn = None while True: self.display(scr) prev_pos = self.cursor.pos prev_column = self.cursor.column prev_version = self.buffer.version c = keyreader() self.dispatchkey(c) if self.last_action == self.KEY_IDLE: continue if (self.buffer.version != prev_version or self.cursor.pos == prev_pos): forcecolumn = None else: curcol = self.cursor.column linelength = len(self.cursor.readline().rstrip('\n')) if forcecolumn is None: if curcol == linelength < prev_column: forcecolumn = prev_column elif curcol == prev_column: if forcecolumn <= linelength: self.cursor.pos += forcecolumn - curcol forcecolumn = None else: self.cursor.pos += linelength - curcol elif curcol == linelength < forcecolumn: pass else: forcecolumn = None if self.cursor.pos != prev_pos: self.buffer.notify() def dispatchkey(self, c): action = None try: if c == 127: c = curses.KEY_BACKSPACE if 32 <= c < 256: action = self.insert self.insert(chr(c)) else: try: action = getattr(self, KEY_NAME[c]) except KeyError: raise EditError("unknown key code (%d)" % c) except AttributeError: raise UnhandledKey(KEY_NAME[c]) else: action() finally: self.last_action = action def insert(self, text, move_cursor=True): pos = self.cursor.pos self.buffer.insert(pos, text) if not move_cursor: self.cursor.pos = pos self.buffer.notify() # ____________________________________________________________ def KEY_IDLE(self): pass TABSIZE = 4 def KEY_CTRL_I(self): # tab t = self.TABSIZE self.insert(' ' * (t - self.cursor.column%t)) def KEY_CTRL_J(self): # enter # keep the indentation of the current line line = self.cursor.readline() i = 0 while i < len(line) and line[i] in ' \t': i += 1 self.insert('\n' + line[:i]) def KEY_CTRL_K(self): self.cursor.to_line_end() end = self.cursor.pos if end < len(self.buffer): end += 1 self.cursor.to_line_start() start = self.cursor.pos if self.last_action != self.KEY_CTRL_K: self.cutlines = '' self.cutlines += self.buffer[start:end] self.buffer.delete(start, end-start) def KEY_CTRL_U(self): self.insert(self.cutlines) def KEY_CTRL_X(self): if self.last_action != self.KEY_CTRL_X: raise CloseEditor('quit') else: raise CloseEditor('force') def KEY_BACKSPACE(self): n = 1 t = self.TABSIZE col = self.cursor.column if col % t == 0: linestart = self.cursor.readline()[:col] if linestart.isspace() and linestart[col-t:] == ' '*t: n = t # dedent by TABSIZE characters self.buffer.delete(self.cursor.pos-n, n) def KEY_LEFT(self): self.cursor.pos -= 1 def KEY_RIGHT(self): self.cursor.pos += 1 def KEY_UP(self): self.cursor.move_up() def KEY_DOWN(self): self.cursor.move_down() def KEY_HOME(self): self.cursor.to_line_start() def KEY_END(self): self.cursor.to_line_end() def KEY_DC(self): # delete character self.buffer.delete(self.cursor.pos, 1) def KEY_NPAGE(self): pagesize = max(1, self.geometry[3]-2) try: self.cursor.move_down(pagesize) self.firstchar.move_down(pagesize) except EditError: pass def KEY_PPAGE(self): pagesize = max(1, self.geometry[3]-2) try: self.cursor.move_up(pagesize) self.firstchar.move_up(pagesize) except EditError: pass