MIRROR: javascript for ๐Ÿœ's, a tiny runtime with big ambitions
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add nano(1) demo and dry up raw mode between process.c/tty.c

+512 -52
+489
examples/demo/nano.js
··· 1 + #!/usr/bin/env ant 2 + 3 + import fs from 'node:fs'; 4 + import path from 'node:path'; 5 + 6 + const { stdin, stdout } = process; 7 + let file = process.argv[2]; 8 + let highlight = file ? /\.(c|m)?(j|t)s$/.test(path.extname(file)) : false; 9 + 10 + let lines = ['']; 11 + let modified = false; 12 + 13 + let row = 0; 14 + let col = 0; 15 + 16 + let scrollY = 0; 17 + let scrollX = 0; 18 + 19 + let message = ''; 20 + let msgTimer = null; 21 + 22 + let mode = 'edit'; 23 + let prompt = ''; 24 + let promptCb = null; 25 + let promptBuf = ''; 26 + let promptKeys = null; 27 + let searchTerm = ''; 28 + 29 + if (file) { 30 + try { 31 + const content = fs.readFileSync(file, 'utf8'); 32 + lines = content.split('\n'); 33 + if (lines[lines.length - 1] === '') lines.pop(); 34 + if (!lines.length) lines = ['']; 35 + setMessage(`[ Read ${lines.length} line${lines.length !== 1 ? 's' : ''} ]`); 36 + } catch { 37 + setMessage('[ New File ]'); 38 + } 39 + } else { 40 + setMessage('[ New File ]'); 41 + } 42 + 43 + function setMessage(msg) { 44 + if (msgTimer) clearTimeout(msgTimer); 45 + message = msg; 46 + msgTimer = setTimeout(() => { 47 + message = ''; 48 + draw(); 49 + }, 1500); 50 + } 51 + 52 + function rows() { 53 + return stdout.rows - 4; 54 + } 55 + 56 + function cols() { 57 + return stdout.columns; 58 + } 59 + 60 + function clamp() { 61 + if (row < 0) row = 0; 62 + if (row >= lines.length) row = lines.length - 1; 63 + if (col < 0) col = 0; 64 + if (col > lines[row].length) col = lines[row].length; 65 + if (row < scrollY) scrollY = row; 66 + if (row >= scrollY + rows()) scrollY = row - rows() + 1; 67 + if (col < scrollX) scrollX = col; 68 + if (col >= scrollX + cols()) scrollX = col - cols() + 1; 69 + } 70 + 71 + function shortcutRow(shortcuts, w, totalCols) { 72 + const cols = totalCols || shortcuts.length; 73 + const cellW = Math.floor(w / cols); 74 + let out = ''; 75 + 76 + for (let i = 0; i < shortcuts.length; i++) { 77 + const [key, label] = shortcuts[i]; 78 + if (!key && !label) { 79 + out += ' '.repeat(cellW); 80 + continue; 81 + } 82 + const cell = `\x1b[7m${key}\x1b[0m ${label}`; 83 + const padLen = cellW - key.length - label.length - 1; 84 + out += cell + (padLen > 0 ? ' '.repeat(padLen) : ''); 85 + } 86 + 87 + return out; 88 + } 89 + 90 + function draw() { 91 + const h = rows(); 92 + const w = cols(); 93 + let out = '\x1b[H'; 94 + 95 + const titleL = ` ANT nano ${Ant.version.match(/^\d+\.\d+\.\d+/)[0]}`; 96 + const titleC = file || 'New Buffer'; 97 + const titleR = modified ? 'Modified' : ''; 98 + 99 + const cpad = Math.max(0, Math.floor((w - titleC.length) / 2) - titleL.length); 100 + const rpad = Math.max(0, w - titleL.length - cpad - titleC.length - titleR.length); 101 + out += `\x1b[7m${titleL}${' '.repeat(cpad)}${titleC}${' '.repeat(rpad)}${titleR}\x1b[0m`; 102 + 103 + const visible = lines.slice(scrollY, scrollY + h); 104 + const raw = visible.map(l => l.slice(scrollX, scrollX + w)); 105 + let body; 106 + 107 + if (highlight) { 108 + const joined = raw.join('\n'); 109 + body = Ant.highlight(joined).split('\n'); 110 + } else { 111 + body = raw; 112 + } 113 + 114 + if (searchTerm) { 115 + const visRow = row - scrollY; 116 + const matchCol = col - scrollX; 117 + const matchEnd = matchCol + searchTerm.length; 118 + if (visRow >= 0 && visRow < body.length && body[visRow] && matchCol >= 0) { 119 + const rendered = body[visRow]; 120 + let result = ''; 121 + let visIdx = 0; 122 + let inHL = false; 123 + for (let bi = 0; bi < rendered.length; ) { 124 + if (rendered[bi] === '\x1b') { 125 + const end = rendered.indexOf('m', bi); 126 + if (end !== -1) { 127 + result += rendered.slice(bi, end + 1); 128 + bi = end + 1; 129 + continue; 130 + } 131 + } 132 + if (visIdx === matchCol && !inHL) { 133 + result += '\x1b[43;30m'; 134 + inHL = true; 135 + } 136 + if (visIdx === matchEnd && inHL) { 137 + result += '\x1b[0m'; 138 + inHL = false; 139 + } 140 + result += rendered[bi++]; 141 + visIdx++; 142 + } 143 + if (inHL) result += '\x1b[0m'; 144 + body[visRow] = result; 145 + } 146 + } 147 + 148 + for (let i = 0; i < h; i++) { 149 + out += `\x1b[${i + 2};1H\x1b[2K`; 150 + out += body[i] || ''; 151 + } 152 + 153 + const statusRow = h + 2; 154 + out += `\x1b[${statusRow};1H\x1b[2K`; 155 + 156 + if (mode === 'yesno') { 157 + const q = 'Save modified buffer?'; 158 + out += `\x1b[7m${q}${' '.repeat(Math.max(0, w - q.length))}\x1b[0m`; 159 + } else if (mode === 'prompt') { 160 + const promptText = prompt + promptBuf; 161 + out += `\x1b[7m${promptText}${' '.repeat(Math.max(0, w - promptText.length))}\x1b[0m`; 162 + } else if (message) { 163 + const pad = Math.max(0, Math.floor((w - message.length) / 2)); 164 + out += ' '.repeat(pad) + `\x1b[7m${message}\x1b[0m`; 165 + } 166 + 167 + let row1, row2; 168 + 169 + if ((mode === 'prompt' || mode === 'yesno') && promptKeys) { 170 + row1 = promptKeys[0]; 171 + row2 = promptKeys[1]; 172 + } else { 173 + row1 = [ 174 + ['^G', 'Help'], 175 + ['^X', 'Exit'], 176 + ['^O', 'Write'], 177 + ['^W', 'Search'], 178 + ['^K', 'Cut'], 179 + ['^U', 'Paste'] 180 + ]; 181 + row2 = [ 182 + ['^A', 'Home'], 183 + ['^E', 'End'], 184 + ['^\\', 'Replace'], 185 + ['^T', 'Execute'], 186 + ['^J', 'Justify'], 187 + ['^C', 'Location'] 188 + ]; 189 + } 190 + 191 + out += `\x1b[${h + 3};1H\x1b[2K`; 192 + out += shortcutRow(row1, w); 193 + out += `\x1b[${h + 4};1H\x1b[2K`; 194 + out += shortcutRow(row2, w); 195 + 196 + if (mode === 'yesno') { 197 + out += `\x1b[${statusRow};${'Save modified buffer?'.length + 2}H`; 198 + } else if (mode === 'prompt') { 199 + out += `\x1b[${statusRow};${prompt.length + promptBuf.length + 1}H`; 200 + } else { 201 + const cy = row - scrollY + 2; 202 + const cx = col - scrollX + 1; 203 + out += `\x1b[${cy};${cx}H`; 204 + } 205 + 206 + stdout.write(out); 207 + } 208 + 209 + let cutBuf = []; 210 + 211 + const writePromptKeys = [ 212 + [ 213 + ['^G', 'Help'], 214 + ['M-D', 'DOS Format'], 215 + ['M-A', 'Append'], 216 + ['M-B', 'Backup File'], 217 + ['^T', 'Browse'] 218 + ], 219 + [ 220 + ['^C', 'Cancel'], 221 + ['M-M', 'Mac Format'], 222 + ['M-P', 'Prepend'], 223 + ['^Q', 'Discard buffer'], 224 + ['', ''] 225 + ] 226 + ]; 227 + 228 + function handleYesNo(buf) { 229 + const ch = buf[0]; 230 + if (ch === 0x79 || ch === 0x59) { 231 + mode = 'edit'; 232 + promptKeys = null; 233 + if (file) { 234 + save(file); 235 + quit(); 236 + return; 237 + } 238 + startPrompt( 239 + 'Write to File: ', 240 + name => { 241 + if (!name) { 242 + setMessage('[ No file name given ]'); 243 + return; 244 + } 245 + save(name); 246 + quit(); 247 + }, 248 + writePromptKeys 249 + ); 250 + } else if (ch === 0x6e || ch === 0x4e) { 251 + mode = 'edit'; 252 + promptKeys = null; 253 + quit(); 254 + } else if (ch === 0x03) { 255 + mode = 'edit'; 256 + promptKeys = null; 257 + setMessage('[ Cancelled ]'); 258 + } 259 + } 260 + 261 + function handleKey(buf) { 262 + if (mode === 'yesno') { 263 + handleYesNo(buf); 264 + return; 265 + } 266 + 267 + if (mode === 'prompt') { 268 + handlePrompt(buf); 269 + return; 270 + } 271 + 272 + message = ''; 273 + const ch = buf[0]; 274 + 275 + if (ch === 0x1b && buf[1] === 0x5b) { 276 + const code = buf[2]; 277 + if (code === 0x41) row--; 278 + else if (code === 0x42) row++; 279 + else if (code === 0x43) col++; 280 + else if (code === 0x44) col--; 281 + else if (code === 0x48) col = 0; 282 + else if (code === 0x46) col = lines[row].length; 283 + else if (code === 0x35 && buf[3] === 0x7e) { 284 + row -= rows(); 285 + } else if (code === 0x36 && buf[3] === 0x7e) { 286 + row += rows(); 287 + } 288 + clamp(); 289 + return; 290 + } 291 + 292 + if (ch === 0x18) { 293 + if (modified) { 294 + mode = 'yesno'; 295 + promptKeys = [ 296 + [ 297 + ['Y', 'Yes'], 298 + ['', ''], 299 + ['', ''], 300 + ['', ''], 301 + ['', ''], 302 + ['', ''] 303 + ], 304 + [ 305 + ['N', 'No'], 306 + ['', ''], 307 + ['^C', 'Cancel'], 308 + ['', ''], 309 + ['', ''], 310 + ['', ''] 311 + ] 312 + ]; 313 + } else quit(); 314 + return; 315 + } 316 + 317 + if (ch === 0x0f) { 318 + startPrompt( 319 + 'Write to File: ', 320 + name => { 321 + const target = name || file; 322 + if (!target) { 323 + setMessage('[ No file name given ]'); 324 + return; 325 + } 326 + save(target); 327 + setMessage(`[ Wrote ${lines.length} line${lines.length !== 1 ? 's' : ''} ]`); 328 + }, 329 + writePromptKeys 330 + ); 331 + return; 332 + } 333 + 334 + if (ch === 0x17) { 335 + startPrompt( 336 + 'Search: ', 337 + term => { 338 + if (term) searchTerm = term; 339 + if (!searchTerm) return; 340 + for (let i = row; i < lines.length; i++) { 341 + const idx = lines[i].indexOf(searchTerm, i === row ? col + 1 : 0); 342 + if (idx !== -1) { 343 + row = i; 344 + col = idx; 345 + clamp(); 346 + return; 347 + } 348 + } 349 + for (let i = 0; i <= row; i++) { 350 + const idx = lines[i].indexOf(searchTerm); 351 + if (idx !== -1) { 352 + row = i; 353 + col = idx; 354 + clamp(); 355 + return; 356 + } 357 + } 358 + setMessage(`[ "${searchTerm}" not found ]`); 359 + }, 360 + null, 361 + searchTerm 362 + ); 363 + return; 364 + } 365 + 366 + if (ch === 0x0b) { 367 + cutBuf = [lines[row]]; 368 + lines.splice(row, 1); 369 + if (!lines.length) lines = ['']; 370 + modified = true; 371 + clamp(); 372 + return; 373 + } 374 + 375 + if (ch === 0x15) { 376 + if (cutBuf.length) { 377 + lines.splice(row, 0, ...cutBuf); 378 + modified = true; 379 + clamp(); 380 + } 381 + return; 382 + } 383 + 384 + if (ch === 0x01) { 385 + col = 0; 386 + return; 387 + } 388 + if (ch === 0x05) { 389 + col = lines[row].length; 390 + return; 391 + } 392 + 393 + if (ch === 0x7f) { 394 + if (col > 0) { 395 + lines[row] = lines[row].slice(0, col - 1) + lines[row].slice(col); 396 + col--; 397 + } else if (row > 0) { 398 + col = lines[row - 1].length; 399 + lines[row - 1] += lines[row]; 400 + lines.splice(row, 1); 401 + row--; 402 + } 403 + modified = true; 404 + clamp(); 405 + return; 406 + } 407 + 408 + if (ch === 0x0d) { 409 + const rest = lines[row].slice(col); 410 + lines[row] = lines[row].slice(0, col); 411 + lines.splice(row + 1, 0, rest); 412 + row++; 413 + col = 0; 414 + modified = true; 415 + clamp(); 416 + return; 417 + } 418 + 419 + if (ch === 0x09 || ch >= 0x20) { 420 + const c = ch === 0x09 ? '\t' : String.fromCharCode(ch); 421 + lines[row] = lines[row].slice(0, col) + c + lines[row].slice(col); 422 + col++; 423 + modified = true; 424 + clamp(); 425 + return; 426 + } 427 + } 428 + 429 + function startPrompt(msg, cb, keys, prefill) { 430 + mode = 'prompt'; 431 + prompt = msg; 432 + promptBuf = prefill || ''; 433 + promptCb = cb; 434 + promptKeys = keys || null; 435 + } 436 + 437 + function handlePrompt(buf) { 438 + const ch = buf[0]; 439 + if (ch === 0x0d) { 440 + mode = 'edit'; 441 + promptKeys = null; 442 + const cb = promptCb; 443 + promptCb = null; 444 + cb(promptBuf); 445 + return; 446 + } 447 + if (ch === 0x03) { 448 + mode = 'edit'; 449 + promptCb = null; 450 + promptKeys = null; 451 + setMessage('[ Cancelled ]'); 452 + return; 453 + } 454 + if (ch === 0x1b) return; 455 + if (ch === 0x7f) { 456 + promptBuf = promptBuf.slice(0, -1); 457 + return; 458 + } 459 + if (ch >= 0x20) { 460 + promptBuf += String.fromCharCode(ch); 461 + } 462 + } 463 + 464 + function save(target) { 465 + file = target; 466 + highlight = /\.(c|m)?(j|t)s$/.test(path.extname(file)); 467 + fs.writeFileSync(file, lines.join('\n') + '\n'); 468 + modified = false; 469 + } 470 + 471 + function quit() { 472 + stdin.setRawMode(false); 473 + stdout.write('\x1b[?1049l'); 474 + stdout.write('\x1b[?25h'); 475 + process.exit(0); 476 + } 477 + 478 + stdout.write('\x1b[?1049h'); 479 + stdout.write('\x1b[?25h'); 480 + stdin.setRawMode(true); 481 + stdin.resume(); 482 + draw(); 483 + 484 + stdin.on('data', buf => { 485 + handleKey(buf); 486 + draw(); 487 + }); 488 + 489 + stdout.on('resize', () => draw());
+3
include/modules/tty.h
··· 6 6 void init_tty_module(void); 7 7 ant_value_t tty_library(ant_t *js); 8 8 9 + bool tty_set_raw_mode(int fd, bool enable); 10 + bool tty_is_raw_mode(int fd); 11 + 9 12 #endif
+4 -26
src/modules/process.c
··· 38 38 #include "gc/modules.h" 39 39 40 40 #include "modules/process.h" 41 + #include "modules/tty.h" 41 42 #include "modules/symbol.h" 42 43 #include "modules/buffer.h" 43 44 #include "modules/napi.h" ··· 95 96 static uint64_t process_start_time = 0; 96 97 97 98 #ifndef _WIN32 98 - static struct termios stdin_saved_termios; 99 - static bool stdin_raw_mode = false; 100 99 static uv_signal_t sigwinch_handle; 101 100 static bool sigwinch_initialized = false; 102 101 #endif ··· 624 623 if (cols) *cols = out_cols; 625 624 } 626 625 627 - #ifndef _WIN32 628 626 static bool stdin_set_raw_mode(bool enable) { 629 627 if (!stdin_is_tty()) return false; 630 - if (enable) { 631 - if (stdin_raw_mode) return true; 632 - if (tcgetattr(STDIN_FILENO, &stdin_saved_termios) == -1) return false; 633 - struct termios raw = stdin_saved_termios; 634 - raw.c_lflag &= ~(ICANON | ECHO | ISIG); 635 - raw.c_cc[VMIN] = 1; 636 - raw.c_cc[VTIME] = 0; 637 - if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) == -1) return false; 638 - stdin_raw_mode = true; 639 - return true; 640 - } 641 - if (!stdin_raw_mode) return true; 642 - if (tcsetattr(STDIN_FILENO, TCSANOW, &stdin_saved_termios) == -1) return false; 643 - stdin_raw_mode = false; 644 - return true; 628 + return tty_set_raw_mode(STDIN_FILENO, enable); 645 629 } 646 - #else 647 - static bool stdin_set_raw_mode(bool enable) { 648 - (void)enable; 649 - return false; 650 - } 651 - #endif 652 630 653 631 static void stdin_alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) { 654 632 (void)handle; ··· 680 658 uv_loop_t *loop = uv_default_loop(); 681 659 if (uv_tty_init(loop, &stdin_state.tty, STDIN_FILENO, 1) != 0) return; 682 660 #ifndef _WIN32 683 - uv_tty_set_mode(&stdin_state.tty, stdin_raw_mode ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); 661 + uv_tty_set_mode(&stdin_state.tty, tty_is_raw_mode(STDIN_FILENO) ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); 684 662 #endif 685 663 stdin_state.tty.data = NULL; 686 664 stdin_state.tty_initialized = true; 687 665 } else { 688 666 #ifndef _WIN32 689 - uv_tty_set_mode(&stdin_state.tty, stdin_raw_mode ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); 667 + uv_tty_set_mode(&stdin_state.tty, tty_is_raw_mode(STDIN_FILENO) ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); 690 668 #endif 691 669 } 692 670 stdin_state.reading = true;
+16 -26
src/modules/tty.c
··· 266 266 } raw_state = { .fd = -1, .active = false }; 267 267 #endif 268 268 269 - static bool set_raw_mode_fd(int fd, bool enable) { 269 + bool tty_set_raw_mode(int fd, bool enable) { 270 270 #ifdef _WIN32 271 271 intptr_t os_handle = _get_osfhandle(fd); 272 272 if (os_handle == -1) return false; ··· 296 296 raw.c_iflag &= ~(IXON | ICRNL); 297 297 raw.c_cc[VMIN] = 1; 298 298 raw.c_cc[VTIME] = 0; 299 - 299 + #ifdef VDISCARD 300 + raw.c_cc[VDISCARD] = _POSIX_VDISABLE; 301 + #endif 302 + #ifdef VLNEXT 303 + raw.c_cc[VLNEXT] = _POSIX_VDISABLE; 304 + #endif 300 305 if (tcsetattr(fd, TCSANOW, &raw) == -1) return false; 301 306 raw_state.fd = fd; 302 307 raw_state.saved = saved; ··· 309 314 raw_state.fd = -1; 310 315 raw_state.active = false; 311 316 return true; 317 + #endif 318 + } 319 + 320 + bool tty_is_raw_mode(int fd) { 321 + #ifdef _WIN32 322 + return false; 323 + #else 324 + return raw_state.active && raw_state.fd == fd; 312 325 #endif 313 326 } 314 327 ··· 548 561 } 549 562 550 563 bool enable = nargs > 0 ? js_truthy(js, args[0]) : true; 551 - 552 - ant_value_t native_fn = js_get(js, this_obj, "__antNativeSetRawMode"); 553 - if (is_callable(native_fn)) { 554 - ant_value_t call_args[1]; 555 - int call_nargs = 0; 556 - if (nargs > 0) { 557 - call_args[0] = args[0]; 558 - call_nargs = 1; 559 - } 560 - ant_value_t result = sv_vm_call( 561 - js->vm, js, native_fn, this_obj, 562 - call_nargs > 0 ? call_args : NULL, call_nargs, 563 - NULL, false 564 - ); 565 - if (vtype(result) == T_ERR) return result; 566 - js_set(js, this_obj, "isRaw", js_bool(js_truthy(js, result) && enable)); 567 - return this_obj; 568 - } 569 - 570 564 int fd = stream_fd_from_this(js, ANT_STDIN_FD); 571 - if (!set_raw_mode_fd(fd, enable)) { 565 + if (!tty_set_raw_mode(fd, enable)) { 572 566 return js_mkerr_typed(js, JS_ERR_GENERIC, "Failed to set raw mode for fd %d", fd); 573 567 } 574 568 js_set(js, this_obj, "isRaw", js_bool(enable)); ··· 691 685 692 686 ant_value_t stdin_proto = js_get_proto(js, stdin_obj); 693 687 if (is_special_object(stdin_proto)) { 694 - ant_value_t native_set_raw = js_get(js, stdin_proto, "setRawMode"); 695 - if (is_callable(native_set_raw)) { 696 - js_set(js, stdin_proto, "__antNativeSetRawMode", native_set_raw); 697 - } 698 688 js_set_proto_init(stdin_proto, stream_readable_prototype(js)); 699 689 setup_readstream_proto(js, stdin_proto); 700 690 }