#!/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()