253 lines
6.8 KiB
Python
253 lines
6.8 KiB
Python
import dang as curses
|
|
import sys
|
|
import gc
|
|
|
|
class MaybeDisableReload:
|
|
def __enter__(self):
|
|
try:
|
|
from supervisor import runtime
|
|
except ImportError:
|
|
return
|
|
|
|
self._old_autoreload = runtime.autoreload
|
|
runtime.autoreload = False
|
|
|
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
try:
|
|
from supervisor import runtime
|
|
except ImportError:
|
|
return
|
|
|
|
runtime.autoreload = self._old_autoreload
|
|
|
|
def gc_mem_free_hint():
|
|
if hasattr(gc, 'mem_free'):
|
|
gc.collect()
|
|
return f" | free: {gc.mem_free()}"
|
|
return ""
|
|
|
|
def readonly():
|
|
try:
|
|
import storage
|
|
except ImportError:
|
|
return False
|
|
|
|
return storage.getmount('/').readonly
|
|
|
|
class Buffer:
|
|
def __init__(self, lines):
|
|
self.lines = lines
|
|
|
|
def __len__(self):
|
|
return len(self.lines)
|
|
|
|
def __getitem__(self, index):
|
|
return self.lines[index]
|
|
|
|
@property
|
|
def bottom(self):
|
|
return len(self) - 1
|
|
|
|
def insert(self, cursor, string):
|
|
row, col = cursor.row, cursor.col
|
|
current = self.lines.pop(row)
|
|
new = current[:col] + string + current[col:]
|
|
self.lines.insert(row, new)
|
|
|
|
def split(self, cursor):
|
|
row, col = cursor.row, cursor.col
|
|
current = self.lines.pop(row)
|
|
self.lines.insert(row, current[:col])
|
|
self.lines.insert(row + 1, current[col:])
|
|
|
|
def delete(self, cursor):
|
|
row, col = cursor.row, cursor.col
|
|
if (row, col) < (self.bottom, len(self[row])):
|
|
current = self.lines.pop(row)
|
|
if col < len(self[row]):
|
|
new = current[:col] + current[col + 1:]
|
|
self.lines.insert(row, new)
|
|
else:
|
|
next = self.lines.pop(row)
|
|
new = current + next
|
|
self.lines.insert(row, new)
|
|
|
|
|
|
def clamp(x, lower, upper):
|
|
if x < lower:
|
|
return lower
|
|
if x > upper:
|
|
return upper
|
|
return x
|
|
|
|
|
|
class Cursor:
|
|
def __init__(self, row=0, col=0, col_hint=None):
|
|
self.row = row
|
|
self._col = col
|
|
self._col_hint = col if col_hint is None else col_hint
|
|
|
|
@property
|
|
def col(self):
|
|
return self._col
|
|
|
|
@col.setter
|
|
def col(self, col):
|
|
self._col = col
|
|
self._col_hint = col
|
|
|
|
def _clamp_col(self, buffer):
|
|
self._col = min(self._col_hint, len(buffer[self.row]))
|
|
|
|
def up(self, buffer):
|
|
if self.row > 0:
|
|
self.row -= 1
|
|
self._clamp_col(buffer)
|
|
|
|
def down(self, buffer):
|
|
if self.row < len(buffer) - 1:
|
|
self.row += 1
|
|
self._clamp_col(buffer)
|
|
|
|
def left(self, buffer):
|
|
if self.col > 0:
|
|
self.col -= 1
|
|
elif self.row > 0:
|
|
self.row -= 1
|
|
self.col = len(buffer[self.row])
|
|
|
|
def right(self, buffer):
|
|
if self.col < len(buffer[self.row]):
|
|
self.col += 1
|
|
elif self.row < len(buffer) - 1:
|
|
self.row += 1
|
|
self.col = 0
|
|
|
|
def end(self, buffer):
|
|
self.col = len(buffer[self.row])
|
|
|
|
class Window:
|
|
def __init__(self, n_rows, n_cols, row=0, col=0):
|
|
self.n_rows = n_rows
|
|
self.n_cols = n_cols
|
|
self.row = row
|
|
self.col = col
|
|
|
|
@property
|
|
def bottom(self):
|
|
return self.row + self.n_rows - 1
|
|
|
|
def up(self, cursor):
|
|
if cursor.row == self.row - 1 and self.row > 0:
|
|
self.row -= 1
|
|
|
|
def down(self, buffer, cursor):
|
|
if cursor.row == self.bottom + 1 and self.bottom < len(buffer) - 1:
|
|
self.row += 1
|
|
|
|
def horizontal_scroll(self, cursor, left_margin=5, right_margin=2):
|
|
n_pages = cursor.col // (self.n_cols - right_margin)
|
|
self.col = max(n_pages * self.n_cols - right_margin - left_margin, 0)
|
|
|
|
def translate(self, cursor):
|
|
return cursor.row - self.row, cursor.col - self.col
|
|
|
|
|
|
def left(window, buffer, cursor):
|
|
cursor.left(buffer)
|
|
window.up(cursor)
|
|
window.horizontal_scroll(cursor)
|
|
|
|
def right(window, buffer, cursor):
|
|
cursor.right(buffer)
|
|
window.down(buffer, cursor)
|
|
window.horizontal_scroll(cursor)
|
|
|
|
def home(window, buffer, cursor):
|
|
cursor.col = 0
|
|
window.horizontal_scroll(cursor)
|
|
|
|
def end(window, buffer, cursor):
|
|
cursor.end(buffer)
|
|
window.horizontal_scroll(cursor)
|
|
|
|
def editor(stdscr, filename):
|
|
with open(filename) as f:
|
|
buffer = Buffer(f.read().splitlines())
|
|
|
|
window = Window(curses.LINES - 1, curses.COLS - 1)
|
|
cursor = Cursor()
|
|
|
|
stdscr.erase()
|
|
|
|
img = [''] * window.n_rows
|
|
def setline(row, line):
|
|
if img[row] == line:
|
|
return
|
|
img[row] = line
|
|
stdscr.addstr(row, 0, line)
|
|
|
|
while True:
|
|
for row, line in enumerate(buffer[window.row:window.row + window.n_rows-1]):
|
|
if row == cursor.row - window.row and window.col > 0:
|
|
line = "«" + line[window.col + 1:]
|
|
if len(line) > window.n_cols:
|
|
line = line[:window.n_cols - 1] + "»"
|
|
line += ' ' * (window.n_cols - len(line))
|
|
setline(row, line)
|
|
|
|
row = window.n_rows - 1
|
|
if readonly():
|
|
line = f"{filename:12} (readonly) | ^C: quit{gc_mem_free_hint()}"
|
|
else:
|
|
line = f"{filename:12} | ^X: write & exit | ^C: quit w/o save{gc_mem_free_hint()}"
|
|
setline(row, line)
|
|
|
|
stdscr.move(*window.translate(cursor))
|
|
|
|
k = stdscr.getkey()
|
|
if len(k) == 1 and ' ' <= k <= '~':
|
|
buffer.insert(cursor, k)
|
|
for _ in k:
|
|
right(window, buffer, cursor)
|
|
elif k == "\x18" and not readonly: # ctrl-x
|
|
with open(filename, "w") as f:
|
|
for row in buffer:
|
|
f.write(row)
|
|
return
|
|
elif k == "KEY_HOME":
|
|
home(window, buffer, cursor)
|
|
elif k == "KEY_END":
|
|
end(window, buffer, cursor)
|
|
elif k == "KEY_LEFT":
|
|
left(window, buffer, cursor)
|
|
elif k == "KEY_DOWN":
|
|
cursor.down(buffer)
|
|
window.down(buffer, cursor)
|
|
window.horizontal_scroll(cursor)
|
|
elif k == "KEY_UP":
|
|
cursor.up(buffer)
|
|
window.up(cursor)
|
|
window.horizontal_scroll(cursor)
|
|
elif k == "KEY_RIGHT":
|
|
right(window, buffer, cursor)
|
|
elif k == "\n":
|
|
buffer.split(cursor)
|
|
right(window, buffer, cursor)
|
|
elif k in ("KEY_DELETE", "\x04"):
|
|
buffer.delete(cursor)
|
|
elif k in ("KEY_BACKSPACE", "\x7f"):
|
|
if (cursor.row, cursor.col) > (0, 0):
|
|
left(window, buffer, cursor)
|
|
buffer.delete(cursor)
|
|
|
|
def edit(filename):
|
|
with MaybeDisableReload():
|
|
curses.wrapper(editor, filename)
|
|
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("filename")
|
|
args = parser.parse_args()
|
|
edit(filename)
|