const TXBUF = 0; const RXBUF = 32; const TXHEAD = 64; const TXTAIL = 65; const RXHEAD = 66; const RXTAIL = 67; const TXBUF_SIZE = 32; const RXBUF_SIZE = 32; const PERIPHS_SIZE = 68; const POLL_INTERVAL_MS = 20; const COLS = 80; const ROWS = 36; const TAB_WIDTH = 8; const CURSOR_IDLE_TIME_MS = 1000; class Emulator { constructor() { this.mem = new WebAssembly.Memory({ initial: 1, maximum: 1, shared: true }); this.mem_u8 = new Uint8Array(this.mem.buffer); for (let i = 0; i < PERIPHS_SIZE; ++i) this.mem_u8[i] = 0; this.decoder = new TextDecoder('utf-8'); this.encoder = new TextEncoder('utf-8'); this.output = document.getElementById('output'); this.rx_queue = []; this.timer = setInterval(() => this.poll(), POLL_INTERVAL_MS); this.grid = Array.from( { length: ROWS }, () => new Array(COLS).fill(' ')); this.cursor = { x: 0, y: 0 }; this.range = { start: { x: 0, y: 0 }, end: { x: 0, y: 0 } }; this.idle_timer = null; document.addEventListener('keydown', (e) => this.handle_keydown(e)); this.flush_output(); document.getElementById('cursor').classList.add('blinking'); this.worker = new Worker('boot.js'); this.worker.postMessage(this.mem); } poll() { const txhead = Atomics.load(this.mem_u8, TXHEAD); const txtail = Atomics.load(this.mem_u8, TXTAIL); if (txhead != txtail) this.handle_tx_data(txhead, txtail); if (this.rx_queue.length != 0) { const rxhead = Atomics.load(this.mem_u8, RXHEAD); const rxtail = Atomics.load(this.mem_u8, RXTAIL); if (this.fifo_next(rxtail) != rxhead) this.handle_rx_data(rxhead, rxtail); } } fifo_next(idx) { return (idx + 1) & 0x1f; } handle_tx_data(head, tail) { const data = []; do { data.push(this.mem_u8[TXBUF + head]); head = this.fifo_next(head); } while (head != tail); Atomics.store(this.mem_u8, TXHEAD, head); this.print(this.decoder.decode(new Uint8Array(data))); } handle_rx_data(head, tail) { do { this.mem_u8[RXBUF + tail] = this.rx_queue.shift(); tail = this.fifo_next(tail); } while (this.fifo_next(tail) != head && this.rx_queue.length != 0); Atomics.store(this.mem_u8, RXTAIL, tail); } print(str) { for (const cp of str) { switch (cp) { case '\n': do this.print_character(' '); while (this.range.start.x != 0); break; case '\t': do this.print_character(' '); while (this.range.start.x % TAB_WIDTH != 0); break; default: this.print_character(cp); break; } } this.flush_output(); } print_character(c) { this.shift_up(this.range.start); this.grid[this.range.start.y][this.range.start.x] = c; this.cursor = this.next_cell(this.cursor); this.range.start = this.next_cell(this.range.start); } handle_keydown(e) { if (e.key.length == 1 && !e.ctrlKey && !e.altKey && !e.metaKey) { this.shift_up(this.cursor); this.grid[this.cursor.y][this.cursor.x] = e.key; this.cursor_move(1, 0); } else if (e.key == 'Enter') { this.cursor.y = this.range.end.y + 1; this.cursor.x = 0; this.submit_line(); Object.assign(this.range.start, this.cursor); Object.assign(this.range.end, this.cursor); } else if (e.key == 'ArrowLeft' || e.key == 'b' && e.ctrlKey) { this.cursor_move(-1, 0); } else if (e.key == 'ArrowRight' || e.key == 'f' && e.ctrlKey) { this.cursor_move(1, 0); } else if (e.key == 'ArrowUp' || e.key == 'p' && e.ctrlKey) { this.cursor_move(0, -1); } else if (e.key == 'ArrowDown' || e.key == 'n' && e.ctrlKey) { this.cursor_move(0, 1); } else if (e.key == 'Backspace') { if (this.cursor_move(-1, 0)) this.shift_down(this.cursor); } else if (e.key == 'Delete' || e.key == 'd' && e.ctrlKey) { this.shift_down(this.cursor); } else if (e.key == 'Home' || e.key == 'a' && e.ctrlKey) { Object.assign(this.cursor, this.range.start); } else if (e.key == 'End' || e.key == 'e' && e.ctrlKey) { Object.assign(this.cursor, this.range.end); } else { return; } e.preventDefault(); this.flush_output(); document.getElementById('cursor').classList.remove('blinking'); if (this.idle_timer) clearTimeout(this.idle_timer); this.idle_timer = setTimeout(() => { document.getElementById('cursor').classList.add('blinking'); }, CURSOR_IDLE_TIME_MS); } submit_line() { let line = ''; let cell = this.range.start; while (cell.x != this.range.end.x && cell.y != this.range.y) { line += this.grid[cell.y][cell.x]; cell = this.next_cell(cell); } line += '\n'; for (let cu of this.encoder.encode(line)) { this.rx_queue.push(cu); } } cursor_move(dx, dy) { let x = this.cursor.x + dx; let y = this.cursor.y + dy; if (x < 0) { --y; x += COLS; } else if (x >= COLS) { ++y; x -= COLS; } if (this.range.start.y > y || this.range.end.y < y) return false; if (y == this.range.start.y && this.range.start.x > x) return false; if (y == this.range.end.y && this.range.end.x < x) return false; this.cursor.x = x; this.cursor.y = y; return true; } prev_cell(cell) { if (cell.x > 0) return { x: cell.x - 1, y: cell.y }; else if (cell.y > 0) return { x: COLS - 1, y: cell.y - 1 }; else return null; } next_cell(cell) { if (cell.x < COLS - 1) return { x: cell.x + 1, y: cell.y }; else if (cell.y < ROWS - 1) return { x: 0, y: cell.y + 1 }; else return null; } shift_up(start) { let cell = { x: this.range.end.x, y: this.range.end.y }; while (cell.x != start.x || cell.y != start.y) { let prev = this.prev_cell(cell); this.grid[cell.y][cell.x] = this.grid[prev.y][prev.x]; cell = prev; } this.range.end = this.next_cell(this.range.end); } shift_down(start) { let cell = { x: start.x, y: start.y }; while (cell.x != this.range.end.x || cell.y != this.range.end.y) { let next = this.next_cell(cell); this.grid[cell.y][cell.x] = this.grid[next.y][next.x]; cell = next; } this.grid[this.range.end.y][this.range.end.x] = ' '; this.range.end = this.prev_cell(this.range.end); } flush_output() { const html = this.grid.map((row, y) => { return row.map((c, x) => { const ec = this.html_escape(c); if (x == this.cursor.x && y == this.cursor.y) return '' + ec + ''; else return ec; }).join(''); }).join('\n'); this.output.innerHTML = html; } html_escape(c) { switch (c) { case '"': return '"'; case "'": return '''; case '&': return '&'; case '<': return '<'; case '>': return '>'; default: return c; } } } window.addEventListener('DOMContentLoaded', () => { document.getElementById('output').innerText = ''; window.emu = new Emulator(); });