#!/usr/bin/env python3 import curses import threading import sys import os import re import socket import pickle import queue 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, 'r') 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': [line for line in 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': [line for line in 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': [line for line in 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()