168 lines
4.7 KiB
Python
168 lines
4.7 KiB
Python
# A simple line-oriented text editor for MicroPython/CircuitPython
|
|
# implementing the POSIX 'ed' subset
|
|
# Usage: from app.ed import edit
|
|
# Created by Luxferre in 2025, released into public domain
|
|
|
|
from deck.input import input
|
|
|
|
class Ed:
|
|
def __init__(self, filename=None):
|
|
self.filename = filename
|
|
self.buffer = []
|
|
self.current_line = 0 # 1-indexed for the user, 0 means empty or before first line
|
|
self.dirty = False
|
|
self.last_error = ""
|
|
|
|
if filename:
|
|
try:
|
|
with open(filename, "r") as f:
|
|
self.buffer = [line.rstrip("\n\r") for line in f.readlines()]
|
|
self.current_line = len(self.buffer)
|
|
print(len("".join(self.buffer)) + len(self.buffer)) # Print byte count
|
|
except OSError:
|
|
print(f"cannot open input file {filename}")
|
|
|
|
def _parse_address(self, addr_str):
|
|
"""Simple address parser for line numbers, '.', and '$'."""
|
|
if not addr_str:
|
|
return self.current_line
|
|
if addr_str == ".":
|
|
return self.current_line
|
|
if addr_str == "$":
|
|
return len(self.buffer)
|
|
try:
|
|
return int(addr_str)
|
|
except ValueError:
|
|
return None
|
|
|
|
def _get_range(self, addr_range):
|
|
"""Parses ranges like '1,5' or '%'."""
|
|
if not addr_range:
|
|
return self.current_line, self.current_line
|
|
if addr_range == ",":
|
|
return 1, len(self.buffer)
|
|
if addr_range == "%":
|
|
return 1, len(self.buffer)
|
|
|
|
parts = addr_range.split(",")
|
|
if len(parts) == 1:
|
|
addr = self._parse_address(parts[0])
|
|
return addr, addr
|
|
else:
|
|
return self._parse_address(parts[0]), self._parse_address(parts[1])
|
|
|
|
def run(self):
|
|
while True:
|
|
try:
|
|
cmd_line = input(":").strip()
|
|
except EOFError:
|
|
break
|
|
|
|
if not cmd_line:
|
|
# A null command (just newline) prints the current line
|
|
if self.buffer:
|
|
print(self.buffer[self.current_line - 1])
|
|
continue
|
|
|
|
# Basic command regex-like split
|
|
# Format: [address]command[parameters]
|
|
cmd_char = ""
|
|
addr_part = ""
|
|
for i, char in enumerate(cmd_line):
|
|
if char.isalpha():
|
|
addr_part = cmd_line[:i]
|
|
cmd_char = char
|
|
params = cmd_line[i+1:].strip()
|
|
break
|
|
else:
|
|
# If no alpha command found, try interpreting as address only
|
|
addr = self._parse_address(cmd_line)
|
|
if addr is not None and 0 <= addr <= len(self.buffer):
|
|
self.current_line = addr
|
|
if self.current_line > 0:
|
|
print(self.buffer[self.current_line - 1])
|
|
continue
|
|
else:
|
|
print("?")
|
|
continue
|
|
|
|
try:
|
|
start, end = self._get_range(addr_part)
|
|
|
|
if cmd_char == 'q':
|
|
if self.dirty and params != "!":
|
|
print("?")
|
|
self.dirty = False # Standard ed behavior: second q forces exit
|
|
continue
|
|
break
|
|
|
|
elif cmd_char == 'p' or cmd_char == 'n':
|
|
if not self.buffer:
|
|
print("?")
|
|
continue
|
|
start = max(1, start)
|
|
end = min(len(self.buffer), end)
|
|
for i in range(start - 1, end):
|
|
if cmd_char == 'n':
|
|
print(f"{i + 1}\t{self.buffer[i]}")
|
|
else:
|
|
print(self.buffer[i])
|
|
self.current_line = end
|
|
|
|
elif cmd_char == 'a':
|
|
idx = start
|
|
while True:
|
|
line = input("")
|
|
if line == ".":
|
|
break
|
|
self.buffer.insert(idx, line)
|
|
idx += 1
|
|
self.dirty = True
|
|
self.current_line = idx
|
|
|
|
elif cmd_char == 'i':
|
|
idx = max(0, start - 1)
|
|
while True:
|
|
line = input("")
|
|
if line == ".":
|
|
break
|
|
self.buffer.insert(idx, line)
|
|
idx += 1
|
|
self.dirty = True
|
|
self.current_line = idx
|
|
|
|
elif cmd_char == 'd':
|
|
if not self.buffer:
|
|
print("?")
|
|
continue
|
|
del self.buffer[start-1 : end]
|
|
self.current_line = min(start, len(self.buffer))
|
|
self.dirty = True
|
|
|
|
elif cmd_char == 'w':
|
|
target = params if params else self.filename
|
|
if not target:
|
|
print("?")
|
|
continue
|
|
with open(target, "w") as f:
|
|
for line in self.buffer:
|
|
f.write(line + "\n")
|
|
print(len("".join(self.buffer)) + len(self.buffer))
|
|
self.dirty = False
|
|
self.filename = target
|
|
|
|
elif cmd_char == 'f':
|
|
if params:
|
|
self.filename = params
|
|
print(self.filename if self.filename else "No current filename")
|
|
|
|
else:
|
|
print("?")
|
|
|
|
except Exception:
|
|
print("?")
|
|
|
|
def edit(fname):
|
|
editor = Ed(fname)
|
|
editor.run()
|