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