From c5e52aafdb64806f7a86a24b4548b1e69c2e3163 Mon Sep 17 00:00:00 2001 From: hellerve Date: Fri, 11 May 2018 17:10:22 +0200 Subject: [PATCH] initial --- README.md | 5 ++ t.py | 191 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 README.md create mode 100755 t.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..7670b7b --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# t + +Yet another simple terminal editor, based on [this +screenshot](https://www.destroyallsoftware.com/screencasts/catalog/text-editor-from-scratch) +by Gary Bernhardt, ported to Python and with multimodality added. diff --git a/t.py b/t.py new file mode 100755 index 0000000..82fbb23 --- /dev/null +++ b/t.py @@ -0,0 +1,191 @@ +#!/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()