tcell/webfiles/tcell.js

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);
}
);