mirror of https://github.com/gdamore/tcell.git
284 lines
6.9 KiB
JavaScript
284 lines
6.9 KiB
JavaScript
// Copyright 2024 The TCell Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use file except in compliance with the License.
|
|
// You may obtain a copy of the license at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
const wasmFilePath = "main.wasm";
|
|
const term = document.getElementById("terminal");
|
|
var width = 80;
|
|
var height = 24;
|
|
const beepAudio = new Audio("beep.wav");
|
|
|
|
var cx = -1;
|
|
var cy = -1;
|
|
var cursorClass = "cursor-blinking-block";
|
|
var cursorColor = "";
|
|
|
|
var content; // {data: row[height], dirty: bool}
|
|
// row = {data: element[width], previous: span}
|
|
// dirty/[previous being null] indicates if previous (or entire terminal) needs to be recalculated.
|
|
// dirty is true/null if terminal/previous need to be re-calculated/shown
|
|
|
|
function initialize() {
|
|
resize(width, height); // initialize content
|
|
show(); // then show the screen
|
|
}
|
|
|
|
function resize(w, h) {
|
|
width = w;
|
|
height = h;
|
|
content = { data: new Array(height), dirty: true };
|
|
for (let i = 0; i < height; i++) {
|
|
content.data[i] = { data: new Array(width), previous: null };
|
|
}
|
|
|
|
clearScreen();
|
|
}
|
|
|
|
function clearScreen(fg, bg) {
|
|
if (fg) {
|
|
term.style.color = intToHex(fg);
|
|
}
|
|
if (bg) {
|
|
term.style.backgroundColor = intToHex(bg);
|
|
}
|
|
|
|
content.dirty = true;
|
|
for (let i = 0; i < height; i++) {
|
|
content.data[i].previous = null; // we set the row to be recalculated later
|
|
for (let j = 0; j < width; j++) {
|
|
content.data[i].data[j] = document.createTextNode(" "); // set the entire row to spaces.
|
|
}
|
|
}
|
|
}
|
|
|
|
function drawCell(x, y, s, fg, bg, attrs, us, uc) {
|
|
var span = document.createElement("span");
|
|
var use = false;
|
|
|
|
if ((attrs & (1 << 2)) != 0) {
|
|
// reverse video
|
|
var temp = bg;
|
|
bg = fg;
|
|
fg = temp;
|
|
use = true;
|
|
}
|
|
if (fg != -1) {
|
|
span.style.color = intToHex(fg);
|
|
use = true;
|
|
}
|
|
if (bg != -1) {
|
|
span.style.backgroundColor = intToHex(bg);
|
|
use = true;
|
|
}
|
|
|
|
// NB: these has to be updated if Attrs.go changes
|
|
if (attrs != 0) {
|
|
use = true;
|
|
if ((attrs & 1) != 0) {
|
|
span.classList.add("bold");
|
|
}
|
|
if ((attrs & (1 << 4)) != 0) {
|
|
span.classList.add("dim");
|
|
}
|
|
if ((attrs & (1 << 5)) != 0) {
|
|
span.classList.add("italic");
|
|
}
|
|
if ((attrs & (1 << 6)) != 0) {
|
|
span.classList.add("strikethrough");
|
|
}
|
|
}
|
|
if (us != 0) {
|
|
use = true;
|
|
if (us == 1) {
|
|
span.classList.add("underline");
|
|
} else if (us == 2) {
|
|
span.classList.add("double_underline");
|
|
} else if (us == 3) {
|
|
span.classList.add("curly_underline");
|
|
} else if (us == 4) {
|
|
span.classList.add("dotted_underline");
|
|
} else if (us == 5) {
|
|
span.classList.add("dashed_underline");
|
|
}
|
|
if (uc != -1) {
|
|
span.style.textDecorationColor = intToHex(uc);
|
|
}
|
|
}
|
|
|
|
if ((attrs & (1 << 1)) != 0) {
|
|
var blink = document.createElement("span");
|
|
blink.classList.add("blink");
|
|
var textnode = document.createTextNode(s);
|
|
blink.appendChild(textnode);
|
|
span.appendChild(blink);
|
|
} else {
|
|
var textnode = document.createTextNode(s);
|
|
span.appendChild(textnode);
|
|
}
|
|
|
|
content.dirty = true; // invalidate terminal- new cell
|
|
content.data[y].previous = null; // invalidate row- new row
|
|
content.data[y].data[x] = use ? span : textnode;
|
|
}
|
|
|
|
function show() {
|
|
if (!content.dirty) {
|
|
return; // no new draws; no need to update
|
|
}
|
|
|
|
displayCursor();
|
|
|
|
term.innerHTML = "";
|
|
content.data.forEach((row) => {
|
|
if (row.previous == null) {
|
|
row.previous = document.createElement("span");
|
|
row.data.forEach((c) => {
|
|
row.previous.appendChild(c);
|
|
});
|
|
row.previous.appendChild(document.createTextNode("\n"));
|
|
}
|
|
term.appendChild(row.previous);
|
|
});
|
|
|
|
content.dirty = false;
|
|
}
|
|
|
|
function showCursor(x, y) {
|
|
content.dirty = true;
|
|
|
|
if (!(cx < 0 || cy < 0)) {
|
|
// if original position is a valid cursor position
|
|
content.data[cy].previous = null;
|
|
if (content.data[cy].data[cx].classList) {
|
|
content.data[cy].data[cx].classList.remove(cursorClass);
|
|
}
|
|
}
|
|
|
|
cx = x;
|
|
cy = y;
|
|
}
|
|
|
|
function displayCursor() {
|
|
content.dirty = true;
|
|
|
|
if (!(cx < 0 || cy < 0)) {
|
|
// if new position is a valid cursor position
|
|
content.data[cy].previous = null;
|
|
|
|
if (!content.data[cy].data[cx].classList) {
|
|
var span = document.createElement("span");
|
|
span.appendChild(content.data[cy].data[cx]);
|
|
content.data[cy].data[cx] = span;
|
|
}
|
|
|
|
if (cursorColor != "") {
|
|
term.style.setProperty("--cursor-color", cursorColor);
|
|
} else {
|
|
term.style.setProperty("--cursor-color", "lightgrey");
|
|
}
|
|
|
|
content.data[cy].data[cx].classList.add(cursorClass);
|
|
}
|
|
}
|
|
|
|
function setCursorStyle(newClass, newColor) {
|
|
if (newClass == cursorClass && newColor == cursorColor) {
|
|
return;
|
|
}
|
|
|
|
if (!(cx < 0 || cy < 0)) {
|
|
// mark cursor row as dirty; new class has been applied to (cx, cy)
|
|
content.dirty = true;
|
|
content.data[cy].previous = null;
|
|
|
|
if (content.data[cy].data[cx].classList) {
|
|
content.data[cy].data[cx].classList.remove(cursorClass);
|
|
}
|
|
|
|
// adding the new class will be dealt with when displayCursor() is called
|
|
}
|
|
|
|
cursorClass = newClass;
|
|
cursorColor = newColor;
|
|
}
|
|
|
|
function beep() {
|
|
beepAudio.currentTime = 0;
|
|
beepAudio.play();
|
|
}
|
|
|
|
function setTitle(title) {
|
|
document.title = title;
|
|
}
|
|
|
|
function intToHex(n) {
|
|
return "#" + n.toString(16).padStart(6, "0");
|
|
}
|
|
|
|
initialize();
|
|
|
|
let fontwidth = term.clientWidth / width;
|
|
let fontheight = term.clientHeight / height;
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
onKeyEvent(e.key, e.shiftKey, e.altKey, e.ctrlKey, e.metaKey);
|
|
});
|
|
|
|
term.addEventListener("click", (e) => {
|
|
onMouseClick(
|
|
Math.min((e.offsetX / fontwidth) | 0, width - 1),
|
|
Math.min((e.offsetY / fontheight) | 0, height - 1),
|
|
e.which,
|
|
e.shiftKey,
|
|
e.altKey,
|
|
e.ctrlKey
|
|
);
|
|
});
|
|
|
|
term.addEventListener("mousemove", (e) => {
|
|
onMouseMove(
|
|
Math.min((e.offsetX / fontwidth) | 0, width - 1),
|
|
Math.min((e.offsetY / fontheight) | 0, height - 1),
|
|
e.which,
|
|
e.shiftKey,
|
|
e.altKey,
|
|
e.ctrlKey
|
|
);
|
|
});
|
|
|
|
term.addEventListener("focus", (e) => {
|
|
onFocus(true);
|
|
});
|
|
|
|
term.addEventListener("blur", (e) => {
|
|
onFocus(false);
|
|
});
|
|
term.tabIndex = 0;
|
|
|
|
document.addEventListener("paste", (e) => {
|
|
e.preventDefault();
|
|
var text = (e.originalEvent || e).clipboardData.getData("text/plain");
|
|
onPaste(true);
|
|
for (let i = 0; i < text.length; i++) {
|
|
onKeyEvent(text.charAt(i), false, false, false, false);
|
|
}
|
|
onPaste(false);
|
|
});
|
|
|
|
const go = new Go();
|
|
WebAssembly.instantiateStreaming(fetch(wasmFilePath), go.importObject).then(
|
|
(result) => {
|
|
go.run(result.instance);
|
|
}
|
|
);
|