diff --git a/app/ed.py b/app/ed.py new file mode 100644 index 0000000..c3a5072 --- /dev/null +++ b/app/ed.py @@ -0,0 +1,167 @@ +# 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()