Files
t-deckard/app/ed.py
T

242 lines
6.8 KiB
Python
Raw Normal View History

2025-12-21 11:13:39 +02:00
# A simple line-oriented text editor for MicroPython/CircuitPython
2025-12-22 09:05:35 +02:00
# implementing a POSIX 'ed' subset
# Usage: from app.ed import edit, view
2025-12-21 11:13:39 +02:00
# Created by Luxferre in 2025, released into public domain
from deck.input import input
2025-12-21 15:04:06 +02:00
from deck.pager import print_paged
from deck.runtime import runcode
2025-12-21 11:13:39 +02:00
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:
2025-12-21 11:15:26 +02:00
cmd_line = input(":").strip()
2025-12-21 11:13:39 +02:00
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 != "!":
2025-12-21 11:38:31 +02:00
print("Unsaved changes, q once again to force exit")
2025-12-21 11:13:39 +02:00
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':
2025-12-22 11:08:13 +02:00
print(f"{(i+1):<5} {self.buffer[i]}")
2025-12-21 11:13:39 +02:00
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
2025-12-21 11:24:51 +02:00
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
2025-12-21 11:13:39 +02:00
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
2025-12-21 11:24:51 +02:00
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:
2025-12-21 11:36:08 +02:00
print("Invalid s/old/new syntax")
2025-12-21 11:24:51 +02:00
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
2025-12-21 11:13:39 +02:00
elif cmd_char == 'w':
target = params if params else self.filename
if not target:
2025-12-21 11:36:08 +02:00
print("No target file to write to")
2025-12-21 11:13:39 +02:00
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")
elif cmd_char == 'x': # execute current buffer range as Python code
code = '\n'.join(self.buffer[start-1:end])
print(f'Running lines {start} to {end} as Python code...')
runcode(code)
2025-12-21 11:13:39 +02:00
else:
2025-12-21 11:36:08 +02:00
print("Unknown command")
2025-12-21 11:13:39 +02:00
2025-12-21 11:36:08 +02:00
except Exception as e:
print(f"Exception occurred: {e}")
2025-12-21 11:13:39 +02:00
def edit(fname):
editor = Ed(fname)
editor.run()
2025-12-21 15:04:06 +02:00
2025-12-26 09:59:30 +02:00
def edbuf(bufstr=''):
editor = Ed()
2025-12-26 09:59:30 +02:00
editor.buffer = bufstr.split('\n')
editor.current_line = len(editor.buffer)
editor.run()
return '\n'.join(editor.buffer)
2025-12-26 09:59:30 +02:00
def edurl(url):
from deck.http import http_fetch
status, text = http_fetch(url)
return edbuf(text)
def viewbuf(text, lno=False):
if lno:
buf = text.split('\n')
for i in range(0, len(buf) - 1):
buf[i] = f'{(i+1):<5} {buf[i]}'
text = '\n'.join(buf)
print_paged(text)
2025-12-22 11:08:13 +02:00
def view(fname, lno=False):
2025-12-21 15:04:06 +02:00
try:
with open(fname, 'r') as f:
text = f.read()
2025-12-26 09:59:30 +02:00
viewbuf(text, lno)
2025-12-21 15:04:06 +02:00
except:
print('Error opening the input file!')
2025-12-26 09:59:30 +02:00
def viewurl(url, lno=False):
from deck.http import http_fetch
status, text = http_fetch(url)
viewbuf(text, lno)