Monorepo for Aesthetic.Computer aesthetic.computer
at main 296 lines 9.5 kB view raw
1// split.mjs — Side-by-side PTY split view for AC native 2// Left pane: system.pty (default: claude) 3// Right pane: system.pty2 (default: /bin/sh) 4// Ctrl+N: toggle focus | Ctrl+W: exit to prompt 5 6let leftGrid = null, rightGrid = null; 7let leftCols = 0, rightCols = 0; 8let rows = 0; 9const cellW = 6, cellH = 10; 10const CELL_SIZE = 10; 11let activePane = 0; // 0=left, 1=right 12let shiftHeld = false, ctrlHeld = false, altHeld = false; 13let cursorBlink = 0; 14let leftStarted = false, rightStarted = false; 15let leftLastExitCode = -1, rightLastExitCode = -1; 16let leftCmd = "/bin/claude", rightCmd = "/bin/sh"; 17 18// ANSI 16-color palette 19const COLORS = [ 20 [0, 0, 0], [170, 0, 0], [0, 170, 0], [170, 85, 0], 21 [0, 0, 170], [170, 0, 170], [0, 170, 170], [170, 170, 170], 22 [85, 85, 85], [255, 85, 85], [85, 255, 85], [255, 255, 85], 23 [85, 85, 255], [255, 85, 255], [85, 255, 255], [255, 255, 255], 24]; 25const DEFAULT_FG = [170, 170, 170]; 26const DEFAULT_BG = [0, 0, 0]; 27 28function fgColor(idx, bold, r, g, b) { 29 if (idx === 255) return [r, g, b]; 30 if (idx === 16) return bold ? COLORS[15] : DEFAULT_FG; 31 if (idx >= 0 && idx < 8 && bold) return COLORS[idx + 8]; 32 if (idx >= 0 && idx < 16) return COLORS[idx]; 33 return DEFAULT_FG; 34} 35 36function bgColor(idx, r, g, b) { 37 if (idx === 255) return [r, g, b]; 38 if (idx === 16) return DEFAULT_BG; 39 if (idx >= 0 && idx < 16) return COLORS[idx]; 40 return DEFAULT_BG; 41} 42 43const UNICODE_MAP = { 44 0x2500:"-",0x2502:"|",0x250C:"+",0x2510:"+",0x2514:"+",0x2518:"+", 45 0x251C:"+",0x2524:"+",0x252C:"+",0x2534:"+",0x253C:"+", 46 0x2550:"=",0x2551:"|",0x2554:"+",0x2557:"+",0x255A:"+",0x255D:"+", 47 0x2560:"+",0x2563:"+",0x2566:"+",0x2569:"+",0x256C:"+", 48 0x256D:"+",0x256E:"+",0x256F:"+",0x2570:"+", 49 0x2588:"#",0x2591:".",0x2592:":",0x2593:"#", 50 0x2190:"<",0x2191:"^",0x2192:">",0x2193:"v", 51 0x2022:"*",0x25CF:"*",0x25A0:"#",0x25B2:"^",0x25B6:">",0x25BC:"v", 52 0x2714:"+",0x2715:"x",0x2713:"+",0x2717:"x", 53 0x2026:"...",0x2018:"'",0x2019:"'",0x201C:'"',0x201D:'"', 54 0x2013:"-",0x2014:"--", 55 0x280B:"|",0x2819:"/",0x2838:"-",0x2830:"\\", 56 0x2826:"|",0x2807:"/",0x280E:"-",0x2821:"\\", 57}; 58 59function charReplace(ch) { 60 if (ch >= 32 && ch < 127) return String.fromCharCode(ch); 61 const mapped = UNICODE_MAP[ch]; 62 if (mapped) return mapped; 63 if (ch >= 0x2800 && ch <= 0x28FF) return "."; 64 if (ch >= 0x2500 && ch <= 0x257F) return "+"; 65 if (ch >= 0x2580 && ch <= 0x259F) return "#"; 66 if (ch >= 0x25A0 && ch <= 0x25FF) return "*"; 67 if (ch >= 0x2700 && ch <= 0x27BF) return "*"; 68 if (ch >= 0x1F000) return " "; 69 if (ch > 127) return " "; 70 return null; 71} 72 73function cmdToArgs(cmd) { 74 // "/bin/claude" → ["/bin/claude", "claude"] 75 // "/bin/sh" → ["/bin/sh", "sh"] 76 const name = cmd.split("/").pop(); 77 return [cmd, name]; 78} 79 80function boot({ system, screen, params }) { 81 // params[0] = left cmd name (e.g. "claude"), params[1] = right cmd name 82 const lname = (params && params[0]) || "claude"; 83 const rname = (params && params[1]) || "sh"; 84 leftCmd = lname.startsWith("/") ? lname : "/bin/" + lname; 85 rightCmd = rname.startsWith("/") ? rname : "/bin/" + rname; 86 87 const halfW = Math.floor((screen.width - 1) / 2); 88 leftCols = Math.floor(halfW / cellW); 89 rightCols = Math.floor((screen.width - halfW - 1) / cellW); 90 rows = Math.floor(screen.height / cellH); 91 92 const [lcmd, larg] = cmdToArgs(leftCmd); 93 const [rcmd, rarg] = cmdToArgs(rightCmd); 94 system.pty.spawn(lcmd, [larg], leftCols, rows); 95 system.pty2.spawn(rcmd, [rarg], rightCols, rows); 96 leftStarted = rightStarted = true; 97 activePane = 0; 98} 99 100function renderPane(pty, grid, xOff, paneCols, paneRows, isActive, ink, box, write) { 101 const T = __theme.update(); 102 103 if (!pty.active) { 104 ink(T.fgDim, T.fgDim, T.fgDim); 105 write("terminal exited", { x: xOff + 4, y: 4, font: 1 }); 106 return; 107 } 108 109 if (!grid) return; 110 111 const useCols = pty.cols || paneCols; 112 const useRows = pty.rows || paneRows; 113 114 for (let y = 0; y < useRows; y++) { 115 for (let x = 0; x < useCols; x++) { 116 const i = (y * useCols + x) * CELL_SIZE; 117 const ch = grid[i]; 118 const fg = grid[i + 1]; 119 const bg = grid[i + 2]; 120 const bold = grid[i + 3]; 121 const fg_r = grid[i + 4], fg_g = grid[i + 5], fg_b = grid[i + 6]; 122 const bg_r = grid[i + 7], bg_g = grid[i + 8], bg_b = grid[i + 9]; 123 124 const px = xOff + x * cellW; 125 const py = y * cellH; 126 127 const [br, bg2, bb] = bgColor(bg, bg_r, bg_g, bg_b); 128 if (br !== 0 || bg2 !== 0 || bb !== 0) { 129 ink(br, bg2, bb); 130 box(px, py, cellW, cellH); 131 } 132 if (ch > 32) { 133 const rep = charReplace(ch); 134 if (rep) { 135 const [fr, fg2, fb] = fgColor(fg, bold, fg_r, fg_g, fg_b); 136 ink(fr, fg2, fb); 137 write(rep, { x: px, y: py, font: 1 }); 138 } 139 } 140 } 141 } 142 143 // Blinking cursor — only when PTY reports cursor visible (?25h) 144 if (isActive && pty.cursorVisible !== false && Math.floor(cursorBlink / 30) % 2 === 0) { 145 const cx = xOff + (pty.cursorX || 0) * cellW; 146 const cy = (pty.cursorY || 0) * cellH; 147 ink(T.fg, T.fg, T.fg, 180); 148 box(cx, cy, cellW, cellH); 149 } 150} 151 152function paint({ wipe, ink, box, write, system, screen }) { 153 const T = __theme.update(); 154 const w = screen.width, h = screen.height; 155 wipe(T.bgDim[0], T.bgDim[1], T.bgDim[2]); 156 cursorBlink++; 157 158 const halfW = Math.floor((w - 1) / 2); 159 160 // Cache grids 161 if (system.pty.grid) leftGrid = system.pty.grid; 162 if (system.pty2.grid) rightGrid = system.pty2.grid; 163 164 // Resize check — left 165 const newLeftCols = Math.floor(halfW / cellW); 166 const newRightCols = Math.floor((w - halfW - 1) / cellW); 167 const newRows = Math.floor(h / cellH); 168 if (newLeftCols !== leftCols || newRows !== rows) { 169 leftCols = newLeftCols; rows = newRows; 170 if (leftCols > 0 && rows > 0) system.pty.resize(leftCols, rows); 171 } 172 if (newRightCols !== rightCols) { 173 rightCols = newRightCols; 174 if (rightCols > 0 && rows > 0) system.pty2.resize(rightCols, rows); 175 } 176 177 // Render left pane 178 renderPane(system.pty, leftGrid, 0, leftCols, rows, activePane === 0, 179 ink, box, write); 180 181 // Divider 182 const divX = halfW; 183 ink(activePane === 0 ? 80 : 40, activePane === 0 ? 160 : 80, activePane === 1 ? 160 : 80); 184 box(divX, 0, 1, h); 185 186 // Render right pane 187 renderPane(system.pty2, rightGrid, halfW + 1, rightCols, rows, activePane === 1, 188 ink, box, write); 189 190 // Status bar hint (bottom of divider) 191 ink(60, 60, 70); 192 write("^N:swap ^W:exit", { x: divX - 44, y: h - 10, font: 1 }); 193} 194 195const SHIFT_MAP = { 196 "1":"!","2":"@","3":"#","4":"$","5":"%", 197 "6":"^","7":"&","8":"*","9":"(","0":")", 198 "-":"_","=":"+","[":"{","]":"}","\\":"|", 199 ";":":","'":'"',",":"<",".":">","/":"?","`":"~", 200}; 201 202function keyToAnsi(key) { 203 if (key === "arrowup") return "\x1b[A"; 204 if (key === "arrowdown") return "\x1b[B"; 205 if (key === "arrowright") return "\x1b[C"; 206 if (key === "arrowleft") return "\x1b[D"; 207 if (key === "home") return "\x1b[H"; 208 if (key === "end") return "\x1b[F"; 209 if (key === "pageup") return "\x1b[5~"; 210 if (key === "pagedown") return "\x1b[6~"; 211 if (key === "insert") return "\x1b[2~"; 212 if (key === "delete") return "\x1b[3~"; 213 if (key === "enter" || key === "return") return "\r"; 214 if (key === "backspace") return "\x7f"; 215 if (key === "tab") return "\t"; 216 if (key === "escape") return "\x1b"; 217 if (key === "f1") return "\x1bOP"; 218 if (key === "f2") return "\x1bOQ"; 219 if (key === "f3") return "\x1bOR"; 220 if (key === "f4") return "\x1bOS"; 221 if (key === "f5") return "\x1b[15~"; 222 if (key === "f6") return "\x1b[17~"; 223 if (key === "f7") return "\x1b[18~"; 224 if (key === "f8") return "\x1b[19~"; 225 if (key === "f9") return "\x1b[20~"; 226 if (key === "f10") return "\x1b[21~"; 227 if (key === "f11") return "\x1b[23~"; 228 if (key === "f12") return "\x1b[24~"; 229 return null; 230} 231 232function act({ event: e, system }) { 233 if (e.is("keyboard:down:shift")) { shiftHeld = true; return; } 234 if (e.is("keyboard:up:shift")) { shiftHeld = false; return; } 235 if (e.is("keyboard:down:control")) { ctrlHeld = true; return; } 236 if (e.is("keyboard:up:control")) { ctrlHeld = false; return; } 237 if (e.is("keyboard:down:alt")) { altHeld = true; return; } 238 if (e.is("keyboard:up:alt")) { altHeld = false; return; } 239 240 if (!e.is("keyboard:down")) return; 241 const key = e.key; 242 if (!key) return; 243 244 // Ctrl+N — toggle active pane 245 if (ctrlHeld && key === "n") { 246 activePane = activePane === 0 ? 1 : 0; 247 return; 248 } 249 250 // Ctrl+W — exit to prompt 251 if (ctrlHeld && key === "w") { 252 system.jump("prompt"); 253 return; 254 } 255 256 const activePty = activePane === 0 ? system.pty : system.pty2; 257 258 // Ctrl+key → control character 259 if (ctrlHeld && key.length === 1) { 260 const code = key.toLowerCase().charCodeAt(0); 261 if (code >= 97 && code <= 122) { 262 activePty.write(String.fromCharCode(code - 96)); 263 return; 264 } 265 } 266 267 const ansi = keyToAnsi(key); 268 if (ansi) { 269 activePty.write(ansi); 270 cursorBlink = 0; 271 return; 272 } 273 274 if (key === "space") { 275 activePty.write(" "); 276 cursorBlink = 0; 277 return; 278 } 279 280 if (key.length === 1) { 281 let ch = key; 282 if (shiftHeld) { 283 if (SHIFT_MAP[ch]) ch = SHIFT_MAP[ch]; 284 else ch = ch.toUpperCase(); 285 } 286 activePty.write(ch); 287 cursorBlink = 0; 288 } 289} 290 291function leave({ system }) { 292 if (system.pty.active) system.pty.kill(); 293 if (system.pty2.active) system.pty2.kill(); 294} 295 296export { boot, paint, act, leave };