/ editor.py
editor.py
1 import curses 2 import sys 3 4 RIGHT_KEYS = set("qwertasdfgzxcvb12345`~!@#$%^&*()-_=+\\|") 5 LEFT_KEYS = set("yuiophjklnm67890[]{};:'\",.<>/?") 6 7 OPCODES = { 8 "00000001": "set [1] to value of address [2]", 9 "00000010": "read from user 1 bit into [1]", 10 "00000011": "write [1]", 11 "00000100": "set [1] to value of [2]", 12 "00010100": "add [2] + [3] and place it in [1]", 13 "00011100": "multiply [2] * [3] and place it in [1]", 14 "00011000": "subtract [2] - [3] and place it in [1]", 15 "00011010": "divide [2] / [3] and place it in [1]", 16 "00100001": "if [0] == [1] goto [2]", 17 "00100010": "if [0] >= [1] goto [2]", 18 "00100011": "if [0] <= [1] goto [2]", 19 "00100110": "if [0] > [1] goto [2]", 20 "00100111": "if [0] < [1] goto [2]", 21 "00100101": "if [0] != [1] goto [2]", 22 "00001111": "run from memory [1] with args memory[2:]", 23 "00001010": "load from persistent byte persistent[1] to memory[2]", 24 "00010101": "write to persistent memory memory[1] -> persistent[memory[2]]", 25 "01000000": "goto line [1] and run from there", 26 "01011111": "thread: start line [0], end line [1], time [2]" 27 } 28 29 30 def prompt_filename(stdscr, prompt): 31 curses.echo() 32 stdscr.addstr(curses.LINES - 2, 0, prompt) 33 stdscr.clrtoeol() 34 filename = stdscr.getstr(curses.LINES - 2, len(prompt)).decode('utf-8') 35 curses.noecho() 36 return filename.strip() 37 38 39 def find_opcode_start(line, cursor_x): 40 bit_positions = [i for i, c in enumerate(line) if c in '01'] 41 if not bit_positions: 42 return None 43 44 for i, pos in enumerate(bit_positions): 45 if pos >= cursor_x: 46 bit_index = i 47 break 48 else: 49 bit_index = len(bit_positions) - 1 50 51 start_bit = (bit_index // 8) * 8 52 if start_bit + 1 > len(bit_positions): 53 return None 54 return bit_positions[start_bit] 55 56 57 def get_bits_from(line, start, count=8): 58 bits = [] 59 i = start 60 while i < len(line) and len(bits) < count: 61 if line[i] in '01': 62 bits.append(line[i]) 63 i += 1 64 return ''.join(bits) 65 66 67 def is_backspace(key): 68 return key in ('\b', '\x7f', '\x08') or key == curses.KEY_BACKSPACE 69 70 71 def main(stdscr, filename=None): 72 curses.curs_set(1) 73 stdscr.clear() 74 text = [''] 75 y, x = 0, 0 76 77 def save_file(fname): 78 with open(fname, 'w') as f: 79 f.write('\n'.join(text)) 80 81 def load_file(fname): 82 nonlocal text 83 try: 84 with open(fname, 'r') as f: 85 lines = f.read().splitlines() 86 text = lines if lines else [''] 87 except: 88 text = [''] 89 90 if filename: 91 load_file(filename) 92 93 while True: 94 stdscr.clear() 95 height, width = stdscr.getmaxyx() 96 right_width = 40 97 text_width = width - right_width - 1 98 99 status = f"File: {filename or 'Untitled'} - [s] Save [l] Load [q] Quit" 100 stdscr.addstr(height - 1, 0, status[:width - 1], curses.A_REVERSE) 101 102 # Draw text with opcode highlighting 103 for idx, line in enumerate(text[:height - 2]): 104 start_idx = end_idx = None 105 if idx == y: 106 start_idx = find_opcode_start(line, x) 107 if start_idx is not None: 108 end_idx = start_idx + 8 + line[start_idx:start_idx+8].count(' ') 109 110 for i, c in enumerate(line[:text_width]): 111 attr = curses.A_NORMAL 112 if start_idx is not None and start_idx <= i < start_idx + 16: 113 if c in '01': 114 attr = curses.A_REVERSE 115 stdscr.addch(idx, i, c, attr) 116 117 # Opcode panel 118 stdscr.addstr(0, text_width + 1, "Current Opcode:", curses.A_UNDERLINE) 119 current_line = text[y] if y < len(text) else "" 120 opcode_start = find_opcode_start(current_line, x) 121 if opcode_start is not None: 122 bits = get_bits_from(current_line, opcode_start, 8) 123 if bits: 124 if len(bits) == 8 and bits in OPCODES: 125 stdscr.addstr(1, text_width + 1, f"{bits}: {OPCODES[bits]}", curses.A_BOLD) 126 else: 127 stdscr.addstr(1, text_width + 1, f"{bits}: (partial)", curses.A_DIM) 128 129 stdscr.addstr(3, text_width + 1, "Next possible opcodes:", curses.A_UNDERLINE) 130 matches = [f"{k}: {v}" for k, v in OPCODES.items() if k.startswith(bits) and k != bits] 131 if matches: 132 for i, match in enumerate(matches[:height - 6]): 133 stdscr.addstr(4 + i, text_width + 1, match[:right_width]) 134 else: 135 stdscr.addstr(4, text_width + 1, "No matching opcodes.", curses.A_DIM) 136 else: 137 stdscr.addstr(1, text_width + 1, "No bits at cursor", curses.A_DIM) 138 else: 139 stdscr.addstr(1, text_width + 1, "Move to opcode bits.", curses.A_DIM) 140 141 stdscr.move(y, x) 142 stdscr.refresh() 143 144 key = stdscr.get_wch() 145 146 # ==== Key Handling ==== 147 if isinstance(key, str): 148 if key.lower() == 'q': 149 break 150 elif key.lower() == 's': 151 if not filename: 152 filename = prompt_filename(stdscr, "Save as: ") 153 if filename: 154 save_file(filename) 155 elif key.lower() == 'l': 156 filename = prompt_filename(stdscr, "Load file: ") 157 if filename: 158 load_file(filename) 159 y, x = 0, 0 160 elif is_backspace(key): 161 if x > 0: 162 text[y] = text[y][:x - 1] + text[y][x:] 163 x -= 1 164 elif y > 0: 165 prev_len = len(text[y - 1]) 166 text[y - 1] += text[y] 167 del text[y] 168 y -= 1 169 x = prev_len 170 elif key == '\n': 171 line = text[y] 172 text[y] = line[:x] 173 text.insert(y + 1, line[x:]) 174 y += 1 175 x = 0 176 elif key == ' ': 177 text[y] = text[y][:x] + ' ' + text[y][x:] 178 x += 1 179 elif key.lower() in LEFT_KEYS: 180 text[y] = text[y][:x] + '0' + text[y][x:] 181 x += 1 182 elif key.lower() in RIGHT_KEYS: 183 text[y] = text[y][:x] + '1' + text[y][x:] 184 x += 1 185 elif key in ['0', '1']: 186 text[y] = text[y][:x] + key + text[y][x:] 187 x += 1 188 189 elif key == curses.KEY_BACKSPACE: 190 if x > 0: 191 text[y] = text[y][:x - 1] + text[y][x:] 192 x -= 1 193 elif y > 0: 194 prev_len = len(text[y - 1]) 195 text[y - 1] += text[y] 196 del text[y] 197 y -= 1 198 x = prev_len 199 elif key == curses.KEY_LEFT: 200 if x > 0: 201 x -= 1 202 elif y > 0: 203 y -= 1 204 x = len(text[y]) 205 elif key == curses.KEY_RIGHT: 206 if x < len(text[y]): 207 x += 1 208 elif y + 1 < len(text): 209 y += 1 210 x = 0 211 elif key == curses.KEY_UP: 212 if y > 0: 213 y -= 1 214 x = min(x, len(text[y])) 215 elif key == curses.KEY_DOWN: 216 if y + 1 < len(text): 217 y += 1 218 x = min(x, len(text[y])) 219 220 221 if __name__ == '__main__': 222 filename = sys.argv[1] if len(sys.argv) > 1 else None 223 curses.wrapper(main, filename)