initial
This commit is contained in:
5
README.md
Normal file
5
README.md
Normal file
@@ -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.
|
191
t.py
Executable file
191
t.py
Executable file
@@ -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()
|
Reference in New Issue
Block a user