Monorepo for Aesthetic.Computer
aesthetic.computer
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 };