# A simple line-oriented text editor for MicroPython/CircuitPython # implementing the POSIX 'ed' subset # Usage: from app.ed import edit, unlock_rootfs, lock_rootfs # Created by Luxferre in 2025, released into public domain from deck.input import input MCU_STORAGE = False try: import storage MCU_STORAGE = True except: pass 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("Unsaved changes, q once again to force exit") 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 == 'c': if not self.buffer: print("?") continue del self.buffer[start-1 : end] idx = 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 == 's': if not self.buffer or not params: print("?") continue # Simple s/old/new/ parser sep = params[0] parts = params.split(sep) if len(parts) < 3: print("Invalid s/old/new syntax") continue old, new = parts[1], parts[2] start = max(1, start) end = min(len(self.buffer), end) for i in range(start - 1, end): if old in self.buffer[i]: self.buffer[i] = self.buffer[i].replace(old, new) self.dirty = True self.current_line = end elif cmd_char == 'w': target = params if params else self.filename if not target: print("No target file to write to") 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("Unknown command") except Exception as e: print(f"Exception occurred: {e}") def unlock_rootfs(): if MCU_STORAGE: storage.remount("/", readonly=False) def lock_rootfs(): if MCU_STORAGE: storage.remount("/", readonly=True) def edit(fname): editor = Ed(fname) editor.run()