#!/usr/bin/env python import copy import sys import tty import termios NORMAL = 0 EDIT = 1 META = 2 UP = 1000 DOWN = 1001 LEFT = 1002 RIGHT = 1003 class Editor(): def __init__(self, f): lines = [l.replace("\n", "") for l in f] self.buffer = Buffer(lines) self.cursor = Cursor(0, 0) self.mode = NORMAL self.history = [] def set_raw(self): fd = sys.stdin.fileno() self.old_settings = termios.tcgetattr(fd) tty.setraw(fd) def unset_raw(self): fd = sys.stdin.fileno() termios.tcsetattr(fd, termios.TCSADRAIN, self.old_settings) def run(self): self.running = True self.set_raw() while self.running: self.render() if self.mode == NORMAL: self.handle_input() if self.mode == EDIT: self.handle_edit() if self.mode == META: self.handle_meta() self.unset_raw() def render(self): ANSI.clear_screen() ANSI.move_cursor(0, 0) self.buffer.render() ANSI.move_cursor(self.cursor.row, self.cursor.col) def handle_meta(self): char = sys.stdin.read(1) if char == 'q': self.running = False def handle_edit(self): char = self.read_input() if char == chr(27): # ESC self.mode = NORMAL elif char == chr(127) and self.cursor.col: # BACK self.save_snapshot() self.buffer = self.buffer.delete(self.cursor) self.cursor = self.cursor.left(self.buffer) elif char == '\r': self.save_snapshot() self.buffer = self.buffer.split_line(self.cursor) self.cursor = Cursor(self.cursor.row+1, 0) elif char == UP: self.cursor = self.cursor.up(self.buffer) elif char == DOWN: self.cursor = self.cursor.down(self.buffer) elif char == LEFT: self.cursor = self.cursor.left(self.buffer) elif char == RIGHT: self.cursor = self.cursor.right(self.buffer) else: self.save_snapshot() self.buffer = self.buffer.insert(char, self.cursor) self.cursor = self.cursor.right(self.buffer) def read_input(self): char = sys.stdin.read(1) if char == '\x1b': nchar = sys.stdin.read(1) if nchar != '[': return char char = sys.stdin.read(1) if char == 'A': return UP if char == 'B': return DOWN if char == 'C': return RIGHT if char == 'D': return LEFT return char def handle_input(self): char = self.read_input() if char == ':': self.mode = META elif char == 'w' or char == UP: self.cursor = self.cursor.up(self.buffer) elif char == 's' or char == DOWN: self.cursor = self.cursor.down(self.buffer) elif char == 'a' or char == LEFT: self.cursor = self.cursor.left(self.buffer) elif char == 'd' or char == RIGHT: self.cursor = self.cursor.right(self.buffer) elif char == 'u': self.restore_snapshot() elif char == 'e': self.mode = EDIT def save_snapshot(self): self.history.append([self.buffer, self.cursor]) def restore_snapshot(self): if len(self.history): self.buffer, self.cursor = self.history.pop() class ANSI(): @staticmethod def ansi(s): return "\x1b[" + s @staticmethod def clear_screen(): sys.stdout.write(ANSI.ansi("2J")) @staticmethod def move_cursor(x, y): sys.stdout.write(ANSI.ansi("{};{}H".format(x+1, y+1))) class Buffer(): def __init__(self, lines): self.lines = lines def render(self): for line in self.lines: sys.stdout.write(line + "\r\n") def line_count(self): return len(self.lines) def line_length(self, x): return len(self.lines[x]) def insert(self, char, cursor): lines = copy.copy(self.lines) line = lines[cursor.row] lines[cursor.row] = line[:cursor.col] + char + line[cursor.col:] return Buffer(lines) def delete(self, cursor): lines = copy.copy(self.lines) line = lines[cursor.row] lines[cursor.row] = line[:cursor.col-1] + line[cursor.col:] return Buffer(lines) def split_line(self, cursor): lines = copy.copy(self.lines) line = lines[cursor.row] before, after = line[:cursor.col], line[cursor.col:] lines[cursor.row] = before lines.insert(cursor.row+1, after) return Buffer(lines) class Cursor(): def __init__(self, row=0, col=0): self.row = row self.col = col def up(self, buf): return Cursor(self.row-1, self.col).clamp(buf) def down(self, buf): return Cursor(self.row+1, self.col).clamp(buf) def left(self, buf): return Cursor(self.row, self.col-1).clamp(buf) def right(self, buf): return Cursor(self.row, self.col+1).clamp(buf) def clamp(self, buf): row = max(0, min(self.row, buf.line_count()-1)) col = max(0, min(self.col, buf.line_length(row))) return Cursor(row, col) with open(sys.argv[1]) as f: Editor(f).run()