Implement a nice terminal emulator on the JS side
This commit is contained in:
240
emu.js
240
emu.js
@@ -11,6 +11,12 @@ 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({
|
||||
@@ -23,12 +29,26 @@ class Emulator {
|
||||
this.mem_u8[i] = 0;
|
||||
|
||||
this.decoder = new TextDecoder('utf-8');
|
||||
this.encoder = new TextEncoder('utf-8');
|
||||
|
||||
this.output = document.getElementById('output');
|
||||
this.input = document.getElementById('input');
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -36,21 +56,213 @@ class Emulator {
|
||||
poll() {
|
||||
const txhead = Atomics.load(this.mem_u8, TXHEAD);
|
||||
const txtail = Atomics.load(this.mem_u8, TXTAIL);
|
||||
if (txhead !== txtail)
|
||||
this.handle_txdata(txhead, 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);
|
||||
}
|
||||
}
|
||||
|
||||
handle_txdata(head, tail) {
|
||||
const data = [];
|
||||
let i = head;
|
||||
do {
|
||||
data.push(this.mem_u8[TXBUF + i]);
|
||||
i = (i + 1) % TXBUF_SIZE;
|
||||
} while (i !== tail);
|
||||
Atomics.store(this.mem_u8, TXHEAD, tail);
|
||||
fifo_next(idx) {
|
||||
return (idx + 1) & 0x1f;
|
||||
}
|
||||
|
||||
const str = this.decoder.decode(new Uint8Array(data));
|
||||
this.output.innerText += str;
|
||||
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 '<span id="cursor">' + ec + '</span>';
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user