Files
2018-01-03 14:18:06 +01:00

707 lines
26 KiB
Python

import sys, gc
class Editor:
KEYMAP = {
"\x1b[A" : 0x0b,
"\x1b[B" : 0x0d,
"\x1b[D" : 0x1f,
"\x1b[C" : 0x1e,
"\x1b[H" : 0x10,
"\x1bOH" : 0x10,
"\x1b[1~": 0x10,
"\x1b[F" : 0x03,
"\x1bOF" : 0x03,
"\x1b[4~": 0x03,
"\x1b[5~": 0xfff1,
"\x1b[6~": 0xfff2,
"\x03" : 0x04,
"\r" : 0x0a,
"\x7f" : 0x08,
"\x1b[3~": 0x7f,
"\x1b[Z" : 0x15,
"\x19" : 0x18,
"\x08" : 0x12,
"\x12" : 0x12,
"\x11" : 0x11,
"\n" : 0x0a,
"\x13" : 0x13,
"\x06" : 0x06,
"\x0e" : 0x0e,
"\x07" : 0x07,
"\x05" : 0x05,
"\x1a" : 0x1a,
"\x09" : 0x09,
"\x15" : 0x15,
"\x18" : 0x18,
"\x16" : 0x16,
"\x04" : 0x04,
"\x0c" : 0x0c,
"\x14" : 0x14,
"\x02" : 0x02,
"\x01" : 0x01,
"\x17" : 0x17,
"\x0f" : 0x0f,
"\x1b[1;5H": 0x14,
"\x1b[1;5F": 0x02,
"\x1b[3;5~": 0x18,
"\x0b" : 0xfffd,
"\x1b[M" : 0x1b,
}
yank_buffer = []
find_pattern = ""
case = "n"
replc_pattern = ""
def __init__(self, tab_size, undo_limit):
self.top_line = self.cur_line = self.row = self.col = self.margin = 0
self.tab_size = tab_size
self.changed = ""
self.message = self.fname = ""
self.content = [""]
self.undo = []
self.undo_limit = max(undo_limit, 0)
self.undo_zero = 0
self.autoindent = "y"
self.mark = None
self.write_tabs = "n"
def wr(self, s):
sys.stdout.write(s)
def rd_any(self):
return False
def rd(self):
while True:
try: return sys.stdin.read(1)
except KeyboardInterrupt: return '\x03'
def goto(self, row, col):
self.wr("\x1b[{};{}H".format(row + 1, col + 1))
def clear_to_eol(self):
self.wr("\x1b[0K")
def cursor(self, onoff):
self.wr("\x1b[?25h" if onoff else "\x1b[?25l")
def hilite(self, mode):
if mode == 1:
#self.wr("\x1b[1;47m") #white background
self.wr("\x1b[1m")
elif mode == 2:
self.wr("\x1b[43m") # yellow
else:
self.wr("\x1b[0m")
def mouse_reporting(self, onoff):
self.wr('\x1b[?9h' if onoff else '\x1b[?9l')
def scroll_region(self, stop):
self.wr('\x1b[1;{}r'.format(stop) if stop else '\x1b[r')
def scroll_up(self, scrolling):
Editor.scrbuf[scrolling:] = Editor.scrbuf[:-scrolling]
Editor.scrbuf[:scrolling] = [''] * scrolling
self.goto(0, 0)
self.wr("\x1bM" * scrolling)
def scroll_down(self, scrolling):
Editor.scrbuf[:-scrolling] = Editor.scrbuf[scrolling:]
Editor.scrbuf[-scrolling:] = [''] * scrolling
self.goto(Editor.height - 1, 0)
self.wr("\x1bD " * scrolling)
def get_screen_size(self):
self.wr('\x1b[999;999H\x1b[6n')
pos = ''
char = self.rd()
while char != 'R':
pos += char
char = self.rd()
return [int(i, 10) for i in pos.lstrip("\n\x1b[").split(';')]
def redraw(self, flag):
self.cursor(False)
Editor.height, Editor.width = self.get_screen_size()
Editor.height -= 1
Editor.scrbuf = [(False,"\x00")] * Editor.height
self.row = min(Editor.height - 1, self.row)
self.scroll_region(Editor.height)
self.mouse_reporting(True)
if sys.implementation.name == "micropython":
gc.collect()
if flag: self.message = "{} Bytes Memory available".format(gc.mem_free())
def get_input(self):
while True:
in_buffer = self.rd()
if in_buffer == '\x1b':
while True:
in_buffer += self.rd()
c = in_buffer[-1]
if c == '~' or (c.isalpha() and c != 'O'):
break
if in_buffer in self.KEYMAP:
c = self.KEYMAP[in_buffer]
if c != 0x1b:
return c, ""
else:
mouse_fct = ord((self.rd()))
mouse_x = ord(self.rd()) - 33
mouse_y = ord(self.rd()) - 33
if mouse_fct == 0x61:
return 0x1d, ""
elif mouse_fct == 0x60:
return 0x1c, ""
else:
return 0x1b, [mouse_x, mouse_y, mouse_fct]
elif ord(in_buffer[0]) >= 32:
return 0, in_buffer
def display_window(self):
self.cur_line = min(self.total_lines - 1, max(self.cur_line, 0))
self.col = max(0, min(self.col, len(self.content[self.cur_line])))
if self.col >= Editor.width + self.margin:
self.margin = self.col - Editor.width + (Editor.width >> 2)
elif self.col < self.margin:
self.margin = max(self.col - (Editor.width >> 2), 0)
if not (self.top_line <= self.cur_line < self.top_line + Editor.height):
self.top_line = max(self.cur_line - self.row, 0)
self.row = self.cur_line - self.top_line
self.cursor(False)
i = self.top_line
for c in range(Editor.height):
if i == self.total_lines:
if Editor.scrbuf[c] != (False,''):
self.goto(c, 0)
self.clear_to_eol()
Editor.scrbuf[c] = (False,'')
else:
l = (self.mark != None and (
(self.mark <= i <= self.cur_line) or (self.cur_line <= i <= self.mark)),
self.content[i][self.margin:self.margin + Editor.width])
if l != Editor.scrbuf[c]:
self.goto(c, 0)
if l[0]: self.hilite(2)
self.wr(l[1])
if len(l[1]) < Editor.width:
self.clear_to_eol()
if l[0]: self.hilite(0)
Editor.scrbuf[c] = l
i += 1
self.goto(Editor.height, 0)
self.hilite(1)
self.wr("{}{} Row: {}/{} Col: {} {}".format(
self.changed, self.fname, self.cur_line + 1, self.total_lines,
self.col + 1, self.message)[:self.width - 1])
self.clear_to_eol()
self.hilite(0)
self.goto(self.row, self.col - self.margin)
self.cursor(True)
def spaces(self, line, pos = None):
return (len(line) - len(line.lstrip(" ")) if pos == None else
len(line[:pos]) - len(line[:pos].rstrip(" ")))
def line_range(self):
return ((self.mark, self.cur_line + 1) if self.mark < self.cur_line else
(self.cur_line, self.mark + 1))
def line_edit(self, prompt, default):
push_msg = lambda msg: self.wr(msg + "\b" * len(msg))
self.goto(Editor.height, 0)
self.hilite(1)
self.wr(prompt)
self.wr(default)
self.clear_to_eol()
res = default
pos = len(res)
while True:
key, char = self.get_input()
if key in (0x0a, 0x09):
self.hilite(0)
return res
elif key == 0x11:
self.hilite(0)
return None
elif key == 0x1f:
if pos > 0:
self.wr("\b")
pos -= 1
elif key == 0x1e:
if pos < len(res):
self.wr(res[pos])
pos += 1
elif key == 0x10:
self.wr("\b" * pos)
pos = 0
elif key == 0x03:
self.wr(res[pos:])
pos = len(res)
elif key == 0x7f:
if pos < len(res):
res = res[:pos] + res[pos+1:]
push_msg(res[pos:] + ' ')
elif key == 0x08:
if pos > 0:
res = res[:pos-1] + res[pos:]
self.wr("\b")
pos -= 1
push_msg(res[pos:] + ' ')
elif key == 0x16:
if Editor.yank_buffer:
self.wr('\b' * pos + ' ' * len(res) + '\b' * len(res))
res = Editor.yank_buffer[0].strip()[:Editor.width - len(prompt) - 2]
self.wr(res)
pos = len(res)
elif key == 0:
if len(prompt) + len(res) < self.width - 2:
res = res[:pos] + char + res[pos:]
self.wr(res[pos])
pos += len(char)
push_msg(res[pos:])
def find_in_file(self, pattern, col, end):
try: from ure import compile
except: from re import compile
Editor.find_pattern = pattern
if Editor.case != "y":
pattern = pattern.lower()
try:
rex = compile(pattern)
except:
self.message = "Invalid pattern: " + pattern
return -1
scol = col
for line in range(self.cur_line, end):
l = self.content[line]
if Editor.case != "y":
l = l.lower()
ecol = 1 if pattern[0] == '^' else len(l) + 1
for i in range(scol, ecol):
match = rex.match(l[i:])
if match:
self.col = i
self.cur_line = line
return len(match.group(0))
scol = 0
else:
self.message = pattern + " not found"
return -1
def undo_add(self, lnum, text, key, span = 1):
self.changed = '*'
if self.undo_limit > 0 and (
len(self.undo) == 0 or key == 0 or self.undo[-1][3] != key or self.undo[-1][0] != lnum):
if len(self.undo) >= self.undo_limit:
del self.undo[0]
self.undo_zero -= 1
self.undo.append([lnum, span, text, key, self.col])
def delete_lines(self, yank):
lrange = self.line_range()
if yank:
Editor.yank_buffer = self.content[lrange[0]:lrange[1]]
self.undo_add(lrange[0], self.content[lrange[0]:lrange[1]], 0, 0)
del self.content[lrange[0]:lrange[1]]
if self.content == []:
self.content = [""]
self.undo[-1][1] = 1
self.total_lines = len(self.content)
self.cur_line = lrange[0]
self.mark = None
def handle_edit_keys(self, key, char):
l = self.content[self.cur_line]
if key == 0x0d:
if self.cur_line < self.total_lines - 1:
self.cur_line += 1
if self.cur_line == self.top_line + Editor.height:
self.scroll_down(1)
elif key == 0x0b:
if self.cur_line > 0:
self.cur_line -= 1
if self.cur_line < self.top_line:
self.scroll_up(1)
elif key == 0x1f:
if self.col == 0 and self.cur_line > 0:
self.cur_line -= 1
self.col = len(self.content[self.cur_line])
if self.cur_line < self.top_line:
self.scroll_up(1)
else:
self.col -= 1
elif key == 0x1e:
if self.col >= len(l) and self.cur_line < self.total_lines - 1:
self.col = 0
self.cur_line += 1
if self.cur_line == self.top_line + Editor.height:
self.scroll_down(1)
else:
self.col += 1
elif key == 0x7f:
if self.mark != None:
self.delete_lines(False)
elif self.col < len(l):
self.undo_add(self.cur_line, [l], 0x7f)
self.content[self.cur_line] = l[:self.col] + l[self.col + 1:]
elif (self.cur_line + 1) < self.total_lines:
self.undo_add(self.cur_line, [l, self.content[self.cur_line + 1]], 0)
self.content[self.cur_line] = l + self.content.pop(self.cur_line + 1)
self.total_lines -= 1
elif key == 0x08:
if self.mark != None:
self.delete_lines(False)
elif self.col > 0:
self.undo_add(self.cur_line, [l], 0x08)
self.content[self.cur_line] = l[:self.col - 1] + l[self.col:]
self.col -= 1
elif self.cur_line > 0:
self.undo_add(self.cur_line - 1, [self.content[self.cur_line - 1], l], 0)
self.col = len(self.content[self.cur_line - 1])
self.content[self.cur_line - 1] += self.content.pop(self.cur_line)
self.cur_line -= 1
self.total_lines -= 1
elif key == 0:
self.mark = None
self.undo_add(self.cur_line, [l], 0x20 if char == " " else 0x41)
self.content[self.cur_line] = l[:self.col] + char + l[self.col:]
self.col += len(char)
elif key == 0x10:
ni = self.spaces(l)
self.col = ni if self.col != ni else 0
elif key == 0x03:
self.col = len(l)
elif key == 0xfff1:
self.cur_line -= Editor.height
elif key == 0xfff2:
self.cur_line += Editor.height
elif key == 0x06:
pat = self.line_edit("Find: ", Editor.find_pattern)
if pat:
self.find_in_file(pat, self.col, self.total_lines)
self.row = Editor.height >> 1
elif key == 0x0e:
if Editor.find_pattern:
self.find_in_file(Editor.find_pattern, self.col + 1, self.total_lines)
self.row = Editor.height >> 1
elif key == 0x07:
line = self.line_edit("Goto Line: ", "")
if line:
self.cur_line = int(line) - 1
self.row = Editor.height >> 1
elif key == 0x14:
self.cur_line = 0
elif key == 0x02:
self.cur_line = self.total_lines - 1
self.row = Editor.height - 1
elif key == 0x01:
pat = self.line_edit("Case Sensitive Search {}, Autoindent {}"
", Tab Size {}, Write Tabs {}"
": ".format(Editor.case, self.autoindent
, self.tab_size, self.write_tabs
), "")
try:
res = [i.strip().lower() for i in pat.split(",")]
if res[0]: Editor.case = 'y' if res[0][0] == 'y' else 'n'
if res[1]: self.autoindent = 'y' if res[1][0] == 'y' else 'n'
if res[2]: self.tab_size = int(res[2])
if res[3]: self.write_tabs = 'y' if res[3][0] == 'y' else 'n'
except:
pass
elif key == 0x1b:
if char[1] < Editor.height:
self.col = char[0] + self.margin
self.cur_line = char[1] + self.top_line
if char[2] in (0x22, 0x30):
self.mark = self.cur_line if self.mark == None else None
elif key == 0x1c:
if self.top_line > 0:
self.top_line = max(self.top_line - 3, 0)
self.cur_line = min(self.cur_line, self.top_line + Editor.height - 1)
self.scroll_up(3)
elif key == 0x1d:
if self.top_line + Editor.height < self.total_lines:
self.top_line = min(self.top_line + 3, self.total_lines - 1)
self.cur_line = max(self.cur_line, self.top_line)
self.scroll_down(3)
elif key == 0xfffd:
if self.col < len(l):
opening = "([{<"
closing = ")]}>"
level = 0
pos = self.col
srch = l[pos]
i = opening.find(srch)
if i >= 0:
pos += 1
match = closing[i]
for i in range(self.cur_line, self.total_lines):
for c in range(pos, len(self.content[i])):
if self.content[i][c] == match:
if level == 0:
self.cur_line, self.col = i, c
return True
else:
level -= 1
elif self.content[i][c] == srch:
level += 1
pos = 0
else:
i = closing.find(srch)
if i >= 0:
pos -= 1
match = opening[i]
for i in range(self.cur_line, -1, -1):
for c in range(pos, -1, -1):
if self.content[i][c] == match:
if level == 0:
self.cur_line, self.col = i, c
return True
else:
level -= 1
elif self.content[i][c] == srch:
level += 1
if i > 0:
pos = len(self.content[i - 1]) - 1
elif key == 0x0c:
self.mark = self.cur_line if self.mark == None else None
elif key == 0x0a:
self.mark = None
self.undo_add(self.cur_line, [l], 0, 2)
self.content[self.cur_line] = l[:self.col]
ni = 0
if self.autoindent == "y":
ni = min(self.spaces(l), self.col)
self.cur_line += 1
self.content[self.cur_line:self.cur_line] = [' ' * ni + l[self.col:]]
self.total_lines += 1
self.col = ni
elif key == 0x09:
if self.mark == None:
ni = self.tab_size - self.col % self.tab_size
self.undo_add(self.cur_line, [l], 0x09)
self.content[self.cur_line] = l[:self.col] + ' ' * ni + l[self.col:]
self.col += ni
else:
lrange = self.line_range()
self.undo_add(lrange[0], self.content[lrange[0]:lrange[1]], 0xfffe, lrange[1] - lrange[0])
for i in range(lrange[0],lrange[1]):
if len(self.content[i]) > 0:
self.content[i] = ' ' * (self.tab_size - self.spaces(self.content[i]) % self.tab_size) + self.content[i]
elif key == 0x15:
if self.mark == None:
ni = min((self.col - 1) % self.tab_size + 1, self.spaces(l, self.col))
if ni > 0:
self.undo_add(self.cur_line, [l], 0x15)
self.content[self.cur_line] = l[:self.col - ni] + l[self.col:]
self.col -= ni
else:
lrange = self.line_range()
self.undo_add(lrange[0], self.content[lrange[0]:lrange[1]], 0xffff, lrange[1] - lrange[0])
for i in range(lrange[0],lrange[1]):
ns = self.spaces(self.content[i])
if ns > 0:
self.content[i] = self.content[i][(ns - 1) % self.tab_size + 1:]
elif key == 0x12:
count = 0
pat = self.line_edit("Replace: ", Editor.find_pattern)
if pat:
rpat = self.line_edit("With: ", Editor.replc_pattern)
if rpat != None:
Editor.replc_pattern = rpat
q = ''
cur_line = self.cur_line
if self.mark != None:
(self.cur_line, end_line) = self.line_range()
self.col = 0
else:
end_line = self.total_lines
self.message = "Replace (yes/No/all/quit) ? "
while True:
ni = self.find_in_file(pat, self.col, end_line)
if ni >= 0:
if q != 'a':
self.display_window()
key, char = self.get_input()
q = char.lower()
if q == 'q' or key == 0x11:
break
elif q in ('a','y'):
self.undo_add(self.cur_line, [self.content[self.cur_line]], 0)
self.content[self.cur_line] = self.content[self.cur_line][:self.col] + rpat + self.content[self.cur_line][self.col + ni:]
self.col += len(rpat)
count += 1
else:
self.col += 1
if self.col >= len(self.content[self.cur_line]):
self.cur_line += 1
self.col = 0
else:
break
self.cur_line = cur_line
self.message = "'{}' replaced {} times".format(pat, count)
elif key == 0x18:
if self.mark != None: self.delete_lines(True)
elif key == 0x04:
if self.mark != None:
lrange = self.line_range()
Editor.yank_buffer = self.content[lrange[0]:lrange[1]]
self.mark = None
elif key == 0x16:
if Editor.yank_buffer:
if self.mark != None: self.delete_lines(False)
self.undo_add(self.cur_line, None, 0, -len(Editor.yank_buffer))
self.content[self.cur_line:self.cur_line] = Editor.yank_buffer
self.total_lines += len(Editor.yank_buffer)
elif key == 0x13:
fname = self.line_edit("Save File: ", self.fname)
if fname:
self.put_file(fname)
self.changed = ''
self.undo_zero = len(self.undo)
self.fname = fname
elif key == 0x1a:
if len(self.undo) > 0:
action = self.undo.pop(-1)
if not action[3] in (0xfffe, 0xffff):
self.cur_line = action[0]
self.col = action[4]
if action[1] >= 0:
if action[0] < self.total_lines:
self.content[action[0]:action[0] + action[1]] = action[2]
else:
self.content += action[2]
else:
del self.content[action[0]:action[0] - action[1]]
self.total_lines = len(self.content)
if len(self.undo) == self.undo_zero: self.changed = ''
self.mark = None
elif key == 0x05:
self.redraw(True)
def edit_loop(self):
if not self.content:
self.content = [""]
self.total_lines = len(self.content)
self.redraw(self.message == "")
while True:
if not self.rd_any():
self.display_window()
key, char = self.get_input()
self.message = ''
if key == 0x11:
if self.changed:
res = self.line_edit("Content changed! Quit without saving (y/N)? ", "N")
if not res or res[0].upper() != 'Y':
continue
self.scroll_region(0)
self.mouse_reporting(False)
self.goto(Editor.height, 0)
self.clear_to_eol()
self.undo = []
return key
elif key in (0x17, 0x0f):
return key
else:
self.handle_edit_keys(key, char)
def packtabs(self, s):
try: from uio import StringIO
except: from _io import StringIO
sb = StringIO()
for i in range(0, len(s), 8):
c = s[i:i + 8]
cr = c.rstrip(" ")
if (len(c) - len(cr)) > 1:
sb.write(cr + "\t")
else: sb.write(c)
return sb.getvalue()
def get_file(self, fname):
from os import listdir
try: from uos import stat
except: from os import stat
if not fname:
fname = self.line_edit("Open file: ", "")
if fname:
self.fname = fname
if fname in ('.', '..') or (stat(fname)[0] & 0x4000):
self.content = ["Directory '{}'".format(fname), ""] + sorted(listdir(fname))
else:
if True:
with open(fname) as f:
self.content = f.readlines()
Editor.tab_seen = 'n'
for i, l in enumerate(self.content):
self.content[i] = expandtabs(l.rstrip('\r\n\t '))
self.write_tabs = Editor.tab_seen
def put_file(self, fname):
if True:
from uos import remove, rename
tmpfile = fname + ".pyetmp"
with open(tmpfile, "w") as f:
for l in self.content:
if self.write_tabs == 'y':
f.write(self.packtabs(l) + '\n')
else:
f.write(l + '\n')
try: remove(fname)
except: pass
rename(tmpfile, fname)
def expandtabs(s):
try: from uio import StringIO
except: from _io import StringIO
if '\t' in s:
Editor.tab_seen = 'y'
sb = StringIO()
pos = 0
for c in s:
if c == '\t':
sb.write(" " * (8 - pos % 8))
pos += 8 - pos % 8
else:
sb.write(c)
pos += 1
return sb.getvalue()
else:
return s
def pye(*content, tab_size = 4, undo = 50, device = 0, baud = 115200):
gc.collect()
slot = [Editor(tab_size, undo)]
index = 0
if content:
for f in content:
if index: slot.append(Editor(tab_size, undo))
if type(f) == str and f:
try:
slot[index].get_file(f)
except Exception as err:
slot[index].message = "{!r}".format(err)
elif type(f) == list and len(f) > 0 and type(f[0]) == str:
slot[index].content = f
index += 1
while True:
try:
index %= len(slot)
key = slot[index].edit_loop()
if key == 0x11:
if len(slot) == 1:
break
del slot[index]
elif key == 0x0f:
slot.append(Editor(tab_size, undo))
index = len(slot) - 1
slot[index].get_file(None)
elif key == 0x17:
index += 1
except Exception as err:
slot[index].message = "{!r}".format(err)
Editor.yank_buffer = []
return slot[0].content if (slot[0].fname == "") else slot[0].fname