#!/usr/bin/env python3 import curses import pickle import queue import re import socket import sys import threading class RPEditor: def __init__(self, filename=None): self.filename = filename self.lines = [""] self.cursor_y = 0 self.cursor_x = 0 self.mode = "normal" self.command = "" self.stdscr = None self.running = False self.thread = None self.socket_thread = None self.prev_key = None self.clipboard = "" self.undo_stack = [] self.redo_stack = [] self.selection_start = None self.selection_end = None self.max_undo = 100 self.lock = threading.RLock() self.client_sock, self.server_sock = socket.socketpair() self.command_queue = queue.Queue() if filename: self.load_file() def load_file(self): try: with open(self.filename) as f: self.lines = f.read().splitlines() if not self.lines: self.lines = [""] except: self.lines = [""] def _save_file(self): with self.lock: if self.filename: with open(self.filename, "w") as f: f.write("\n".join(self.lines)) def save_file(self): self.client_sock.send(pickle.dumps({"command": "save_file"})) def start(self): self.running = True self.socket_thread = threading.Thread(target=self.socket_listener) self.socket_thread.start() self.thread = threading.Thread(target=self.run) self.thread.start() def stop(self): self.client_sock.send(pickle.dumps({"command": "stop"})) self.running = False if self.stdscr: curses.endwin() if self.thread: self.thread.join() if self.socket_thread: self.socket_thread.join() self.client_sock.close() self.server_sock.close() def run(self): curses.wrapper(self.main_loop) def main_loop(self, stdscr): self.stdscr = stdscr curses.curs_set(1) self.stdscr.keypad(True) while self.running: with self.lock: self.draw() try: while True: command = self.command_queue.get_nowait() with self.lock: self.execute_command(command) except queue.Empty: pass key = self.stdscr.getch() with self.lock: self.handle_key(key) def draw(self): self.stdscr.clear() height, width = self.stdscr.getmaxyx() for i, line in enumerate(self.lines): if i < height - 1: self.stdscr.addstr(i, 0, line[:width]) status = f"{self.mode.upper()} | {self.filename or 'untitled'} | {self.cursor_y+1}:{self.cursor_x+1}" self.stdscr.addstr(height - 1, 0, status[:width]) if self.mode == "command": self.stdscr.addstr(height - 1, 0, self.command[:width]) self.stdscr.move(self.cursor_y, min(self.cursor_x, width - 1)) self.stdscr.refresh() def handle_key(self, key): if self.mode == "normal": self.handle_normal(key) elif self.mode == "insert": self.handle_insert(key) elif self.mode == "command": self.handle_command(key) def handle_normal(self, key): if key == ord("h") or key == curses.KEY_LEFT: self.move_cursor(0, -1) elif key == ord("j") or key == curses.KEY_DOWN: self.move_cursor(1, 0) elif key == ord("k") or key == curses.KEY_UP: self.move_cursor(-1, 0) elif key == ord("l") or key == curses.KEY_RIGHT: self.move_cursor(0, 1) elif key == ord("i"): self.mode = "insert" elif key == ord(":"): self.mode = "command" self.command = ":" elif key == ord("x"): self._delete_char() elif key == ord("a"): self.cursor_x += 1 self.mode = "insert" elif key == ord("A"): self.cursor_x = len(self.lines[self.cursor_y]) self.mode = "insert" elif key == ord("o"): self._insert_line(self.cursor_y + 1, "") self.cursor_y += 1 self.cursor_x = 0 self.mode = "insert" elif key == ord("O"): self._insert_line(self.cursor_y, "") self.cursor_x = 0 self.mode = "insert" elif key == ord("d") and self.prev_key == ord("d"): self.clipboard = self.lines[self.cursor_y] self._delete_line(self.cursor_y) if self.cursor_y >= len(self.lines): self.cursor_y = len(self.lines) - 1 self.cursor_x = 0 elif key == ord("y") and self.prev_key == ord("y"): self.clipboard = self.lines[self.cursor_y] elif key == ord("p"): self._insert_line(self.cursor_y + 1, self.clipboard) self.cursor_y += 1 self.cursor_x = 0 elif key == ord("P"): self._insert_line(self.cursor_y, self.clipboard) self.cursor_x = 0 elif key == ord("w"): line = self.lines[self.cursor_y] i = self.cursor_x while i < len(line) and not line[i].isalnum(): i += 1 while i < len(line) and line[i].isalnum(): i += 1 self.cursor_x = i elif key == ord("b"): line = self.lines[self.cursor_y] i = self.cursor_x - 1 while i >= 0 and not line[i].isalnum(): i -= 1 while i >= 0 and line[i].isalnum(): i -= 1 self.cursor_x = i + 1 elif key == ord("0"): self.cursor_x = 0 elif key == ord("$"): self.cursor_x = len(self.lines[self.cursor_y]) elif key == ord("g"): if self.prev_key == ord("g"): self.cursor_y = 0 self.cursor_x = 0 elif key == ord("G"): self.cursor_y = len(self.lines) - 1 self.cursor_x = 0 elif key == ord("u"): self.undo() elif key == ord("r") and self.prev_key == 18: self.redo() self.prev_key = key def handle_insert(self, key): if key == 27: self.mode = "normal" if self.cursor_x > 0: self.cursor_x -= 1 elif key == 10: self._split_line() elif key == curses.KEY_BACKSPACE or key == 127: self._backspace() elif 32 <= key <= 126: char = chr(key) self._insert_char(char) def handle_command(self, key): if key == 10: cmd = self.command[1:] if cmd == "q" or cmd == "q!": self.running = False elif cmd == "w": self._save_file() elif ( cmd == "wq" or cmd == "wq!" or cmd == "x" or cmd == "xq" or cmd == "x!" ): self._save_file() self.running = False elif cmd.startswith("w "): self.filename = cmd[2:] self._save_file() elif cmd == "wq": self._save_file() self.running = False self.mode = "normal" self.command = "" elif key == 27: self.mode = "normal" self.command = "" elif key == curses.KEY_BACKSPACE or key == 127: if len(self.command) > 1: self.command = self.command[:-1] elif 32 <= key <= 126: self.command += chr(key) def move_cursor(self, dy, dx): new_y = self.cursor_y + dy new_x = self.cursor_x + dx if 0 <= new_y < len(self.lines): self.cursor_y = new_y self.cursor_x = max(0, min(new_x, len(self.lines[self.cursor_y]))) def save_state(self): with self.lock: state = { "lines": list(self.lines), "cursor_y": self.cursor_y, "cursor_x": self.cursor_x, } self.undo_stack.append(state) if len(self.undo_stack) > self.max_undo: self.undo_stack.pop(0) self.redo_stack.clear() def undo(self): with self.lock: if self.undo_stack: current_state = { "lines": list(self.lines), "cursor_y": self.cursor_y, "cursor_x": self.cursor_x, } self.redo_stack.append(current_state) state = self.undo_stack.pop() self.lines = state["lines"] self.cursor_y = state["cursor_y"] self.cursor_x = state["cursor_x"] def redo(self): with self.lock: if self.redo_stack: current_state = { "lines": list(self.lines), "cursor_y": self.cursor_y, "cursor_x": self.cursor_x, } self.undo_stack.append(current_state) state = self.redo_stack.pop() self.lines = state["lines"] self.cursor_y = state["cursor_y"] self.cursor_x = state["cursor_x"] def _insert_text(self, text): self.save_state() lines = text.split("\n") if len(lines) == 1: self.lines[self.cursor_y] = ( self.lines[self.cursor_y][: self.cursor_x] + text + self.lines[self.cursor_y][self.cursor_x :] ) self.cursor_x += len(text) else: first = self.lines[self.cursor_y][: self.cursor_x] + lines[0] last = lines[-1] + self.lines[self.cursor_y][self.cursor_x :] self.lines[self.cursor_y] = first for i in range(1, len(lines) - 1): self.lines.insert(self.cursor_y + i, lines[i]) self.lines.insert(self.cursor_y + len(lines) - 1, last) self.cursor_y += len(lines) - 1 self.cursor_x = len(lines[-1]) def insert_text(self, text): self.client_sock.send(pickle.dumps({"command": "insert_text", "text": text})) def _delete_char(self): self.save_state() if self.cursor_x < len(self.lines[self.cursor_y]): self.lines[self.cursor_y] = ( self.lines[self.cursor_y][: self.cursor_x] + self.lines[self.cursor_y][self.cursor_x + 1 :] ) def delete_char(self): self.client_sock.send(pickle.dumps({"command": "delete_char"})) def _insert_char(self, char): self.lines[self.cursor_y] = ( self.lines[self.cursor_y][: self.cursor_x] + char + self.lines[self.cursor_y][self.cursor_x :] ) self.cursor_x += 1 def _split_line(self): line = self.lines[self.cursor_y] self.lines[self.cursor_y] = line[: self.cursor_x] self.lines.insert(self.cursor_y + 1, line[self.cursor_x :]) self.cursor_y += 1 self.cursor_x = 0 def _backspace(self): if self.cursor_x > 0: self.lines[self.cursor_y] = ( self.lines[self.cursor_y][: self.cursor_x - 1] + self.lines[self.cursor_y][self.cursor_x :] ) self.cursor_x -= 1 elif self.cursor_y > 0: prev_len = len(self.lines[self.cursor_y - 1]) self.lines[self.cursor_y - 1] += self.lines[self.cursor_y] del self.lines[self.cursor_y] self.cursor_y -= 1 self.cursor_x = prev_len def _insert_line(self, line_num, text): self.save_state() line_num = max(0, min(line_num, len(self.lines))) self.lines.insert(line_num, text) def _delete_line(self, line_num): self.save_state() if 0 <= line_num < len(self.lines): if len(self.lines) > 1: del self.lines[line_num] else: self.lines = [""] def _set_text(self, text): self.save_state() self.lines = text.splitlines() if text else [""] self.cursor_y = 0 self.cursor_x = 0 def set_text(self, text): self.client_sock.send(pickle.dumps({"command": "set_text", "text": text})) def _goto_line(self, line_num): line_num = max(0, min(line_num, len(self.lines) - 1)) self.cursor_y = line_num self.cursor_x = 0 def goto_line(self, line_num): self.client_sock.send( pickle.dumps({"command": "goto_line", "line_num": line_num}) ) def get_text(self): self.client_sock.send(pickle.dumps({"command": "get_text"})) try: return pickle.loads(self.client_sock.recv(4096)) except: return "" def get_cursor(self): self.client_sock.send(pickle.dumps({"command": "get_cursor"})) try: return pickle.loads(self.client_sock.recv(4096)) except: return (0, 0) def get_file_info(self): self.client_sock.send(pickle.dumps({"command": "get_file_info"})) try: return pickle.loads(self.client_sock.recv(4096)) except: return {} def socket_listener(self): while self.running: try: data = self.server_sock.recv(4096) if not data: break command = pickle.loads(data) self.command_queue.put(command) except OSError: break def execute_command(self, command): cmd = command.get("command") if cmd == "insert_text": self._insert_text(command["text"]) elif cmd == "delete_char": self._delete_char() elif cmd == "save_file": self._save_file() elif cmd == "set_text": self._set_text(command["text"]) elif cmd == "goto_line": self._goto_line(command["line_num"]) elif cmd == "get_text": result = "\n".join(self.lines) try: self.server_sock.send(pickle.dumps(result)) except: pass elif cmd == "get_cursor": result = (self.cursor_y, self.cursor_x) try: self.server_sock.send(pickle.dumps(result)) except: pass elif cmd == "get_file_info": result = { "filename": self.filename, "lines": len(self.lines), "cursor": (self.cursor_y, self.cursor_x), "mode": self.mode, } try: self.server_sock.send(pickle.dumps(result)) except: pass elif cmd == "stop": self.running = False def move_cursor_to(self, y, x): with self.lock: self.cursor_y = max(0, min(y, len(self.lines) - 1)) self.cursor_x = max(0, min(x, len(self.lines[self.cursor_y]))) def get_line(self, line_num): with self.lock: if 0 <= line_num < len(self.lines): return self.lines[line_num] return None def get_lines(self, start, end): with self.lock: start = max(0, start) end = min(end, len(self.lines)) return self.lines[start:end] def insert_at_line(self, line_num, text): with self.lock: self.save_state() line_num = max(0, min(line_num, len(self.lines))) self.lines.insert(line_num, text) def delete_lines(self, start, end): with self.lock: self.save_state() start = max(0, start) end = min(end, len(self.lines)) if start < end: del self.lines[start:end] if not self.lines: self.lines = [""] def replace_text(self, start_line, start_col, end_line, end_col, new_text): with self.lock: self.save_state() if start_line == end_line: line = self.lines[start_line] self.lines[start_line] = line[:start_col] + new_text + line[end_col:] else: first_part = self.lines[start_line][:start_col] last_part = self.lines[end_line][end_col:] new_lines = new_text.split("\n") self.lines[start_line] = first_part + new_lines[0] del self.lines[start_line + 1 : end_line + 1] for i, new_line in enumerate(new_lines[1:], 1): self.lines.insert(start_line + i, new_line) if len(new_lines) > 1: self.lines[start_line + len(new_lines) - 1] += last_part else: self.lines[start_line] += last_part def search(self, pattern, start_line=0): with self.lock: results = [] for i in range(start_line, len(self.lines)): matches = re.finditer(pattern, self.lines[i]) for match in matches: results.append((i, match.start(), match.end())) return results def replace_all(self, search_text, replace_text): with self.lock: self.save_state() for i in range(len(self.lines)): self.lines[i] = self.lines[i].replace(search_text, replace_text) def select_range(self, start_line, start_col, end_line, end_col): with self.lock: self.selection_start = (start_line, start_col) self.selection_end = (end_line, end_col) def get_selection(self): with self.lock: if not self.selection_start or not self.selection_end: return "" sl, sc = self.selection_start el, ec = self.selection_end if sl == el: return self.lines[sl][sc:ec] result = [self.lines[sl][sc:]] for i in range(sl + 1, el): result.append(self.lines[i]) result.append(self.lines[el][:ec]) return "\n".join(result) def delete_selection(self): with self.lock: if not self.selection_start or not self.selection_end: return self.save_state() sl, sc = self.selection_start el, ec = self.selection_end self.replace_text(sl, sc, el, ec, "") self.selection_start = None self.selection_end = None def apply_search_replace_block(self, search_block, replace_block): with self.lock: self.save_state() search_lines = search_block.splitlines() replace_lines = replace_block.splitlines() for i in range(len(self.lines) - len(search_lines) + 1): match = True for j, search_line in enumerate(search_lines): if self.lines[i + j].strip() != search_line.strip(): match = False break if match: indent = len(self.lines[i]) - len(self.lines[i].lstrip()) indented_replace = [" " * indent + line for line in replace_lines] self.lines[i : i + len(search_lines)] = indented_replace return True return False def apply_diff(self, diff_text): with self.lock: self.save_state() lines = diff_text.split("\n") for line in lines: if line.startswith("@@"): match = re.search(r"@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@", line) if match: start_line = int(match.group(1)) - 1 elif line.startswith("-"): if start_line < len(self.lines): del self.lines[start_line] elif line.startswith("+"): self.lines.insert(start_line, line[1:]) start_line += 1 def get_context(self, line_num, context_lines=3): with self.lock: start = max(0, line_num - context_lines) end = min(len(self.lines), line_num + context_lines + 1) return self.get_lines(start, end) def count_lines(self): with self.lock: return len(self.lines) def close(self): self.running = False self.stop() if self.thread: self.thread.join() def main(): filename = sys.argv[1] if len(sys.argv) > 1 else None editor = RPEditor(filename) editor.start() editor.thread.join() if __name__ == "__main__": main()