SchOS/docs/Website/docs.js
Nexoniarz 1c1b0c9570 tty: six virtual terminals, and the plumbing that makes them terminals
/dev/tty1..tty6, Alt+F1..F6 or `chvt N`. Each is an independent terminal --
its own screen, cursor, line discipline, foreground process group and
session. Two shells on two terminals are unrelated processes that share a
keyboard and a screen.

What made it possible is a backing store. The console drew straight into the
framebuffer and scrolled by moving pixels, which works exactly as long as
nothing else wants the screen: there was nowhere to put terminal 2 while
terminal 1 was visible, and nothing to repaint terminal 1 from. Each terminal
owns a grid of cells now and the framebuffer shows whichever grid is active,
so vt_switch() is set-index, repaint, return -- nothing is saved on the way
out because the outgoing grid has been current all along.

The rule the layer is built on: a terminal does not know whether it is
visible. Writes land in the grid, the cursor moves, the grid scrolls, all
unconditionally; drawing is one question asked at the bottom of vt.c. Every
alternative spreads "am I on screen" through the escape parser, and then a
terminal switched away from mid-sequence comes back wrong.

Everything that was a single global is per-terminal now: tty_fg_pgrp (deleted),
termios, the input ring, the escape parser, the UTF-8 decoder, and the cursor
-- which also stops being pixels in gfx.c and becomes cells in the terminal.
TIOCSCTTY was `return 0` and now records proc->ctty, which is what /dev/tty
resolves through; /dev/tty was an alias for /dev/console with the same minor,
and they are different questions.

kbd.c names Alt+F1..F6 as KEY_ALT_F1+n, above 0xFF on purpose -- every byte
value is already a character or a UTF-8 continuation, and a chord colliding
with one would be typed into the terminal instead of switching away from it.
What the chord means is decided in console.c, where the terminals are.

VT_ACTIVATE / VT_WAITACTIVE / VT_GETSTATE / VT_OPENQRY, Linux's numbers.
The argument to ACTIVATE is by value, not a pointer -- same shape as TCFLSH.

Two bugs found by screenshotting the result rather than reading the log:

  - zsh emits 0x0F to leave alt-charset mode, because that is what the `linux`
    terminfo says rmacs is, and the console drew CP437 glyph 0x0F: a sun, in
    the prompt, on every line. A terminal never draws a C0 control character.
  - SGR 7 was unimplemented, so zsh's partial-line marker rendered as a bare
    '#' instead of a reversed block. Reverse video swaps the colours at draw
    time, not at store time, so turning it off restores what was underneath.

Verified by screendump: tty1 and tty2 show different screens, and switching
back repaints tty1's earlier content from its grid. schfs-check passes.

docs/architecture/terminals.md, including what it does not do -- no
scrollback, no reflow, no VT_PROCESS mode for a display server to veto a
switch, which Schcom will need before it owns Alt+F7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-13 00:08:00 +02:00

363 lines
14 KiB
JavaScript

/* SPDX-License-Identifier: Apache-2.0
*
* The documentation reader.
*
* Renders the project's Markdown in the site's own theme, so the docs can be
* read without leaving the page and without a build step. No dependency: the
* parser below handles exactly what docs/ actually uses -- headings, fenced
* code, tables, lists, quotes, and inline marks. Pulling in a 40 KB Markdown
* library to render 29 files would outweigh every other asset here combined.
*
* Everything is escaped before any tag is emitted. The only HTML that reaches
* the page is HTML this file wrote.
*/
(function () {
"use strict";
/* ---- the index ---------------------------------------------------- */
var DOCS = [
{ group: "Start here", items: [
["README.md", "Overview"],
["plan/1.0.md", "The 1.0 definition"],
["licensing.md", "Licensing"]
]},
{ group: "Architecture", items: [
["architecture/README.md", "The system, layer by layer"],
["architecture/boot.md", "Boot"],
["architecture/kernel.md", "Kernel"],
["architecture/syscalls.md", "Syscalls"],
["architecture/filesystem.md", "The VFS"],
["architecture/schfs.md", "SchFS"],
["architecture/terminals.md", "Virtual terminals"],
["architecture/devices.md", "Device model"],
["architecture/usb.md", "USB"],
["architecture/graphics.md", "Graphics"],
["architecture/networking.md", "Networking"],
["architecture/init.md", "init"]
]},
{ group: "Guides", items: [
["guides/building.md", "Building"],
["guides/testing.md", "Testing"],
["guides/layout.md", "Tree layout"],
["guides/adding-a-driver.md", "Adding a driver"]
]},
{ group: "Plan", items: [
["plan/README.md", "What plan/ is for"],
["plan/roadmap.md", "Roadmap"],
["plan/foundations.md", "Foundations"],
["plan/init.md", "schinit"],
["plan/wayland.md", "Wayland"],
["plan/hybrid.md", "Hybrid kernel"],
["plan/schapi.md", "SchAPI"]
]},
{ group: "History", items: [
["history/README.md", "Why history/ exists"],
["history/kernel.md", "Kernel"],
["history/schfs.md", "SchFS"],
["history/moving-to-linux.md", "Moving to Linux"]
]},
{ group: "Branding", items: [
["branding/README.md", "The three marks"]
]}
];
/* ---- helpers ------------------------------------------------------- */
function esc(s) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
/* Resolve a doc-relative link against the doc it appeared in, so
"../history/schfs.md" inside architecture/ lands on history/schfs.md. */
function resolve(from, href) {
var base = from.split("/").slice(0, -1);
var parts = href.split("/");
for (var i = 0; i < parts.length; i++) {
if (parts[i] === "." || parts[i] === "") continue;
if (parts[i] === "..") base.pop();
else base.push(parts[i]);
}
return base.join("/");
}
/* encodeURIComponent turns every "/" into %2F, which works but makes a
shared link unreadable. Slashes are legal in a query value, so keep them. */
function docHref(p) {
return "?doc=" + encodeURIComponent(p).replace(/%2F/g, "/");
}
function slug(s) {
return s.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-");
}
/* ---- inline marks --------------------------------------------------- */
/* U+0001 as the placeholder sentinel: it cannot occur in a Markdown source
file, so a code span lifted out here can never be confused with document
text when it is put back. */
var SENTINEL = "\u0001";
function inline(s, from) {
/* Code spans come out first and go back last, so nothing inside a backtick
pair is mistaken for emphasis or for a link. */
var spans = [];
s = s.replace(/`([^`]+)`/g, function (_, code) {
spans.push("<code>" + esc(code) + "</code>");
return SENTINEL + (spans.length - 1) + SENTINEL;
});
s = esc(s);
s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)[^)]*\)/g, function (_, alt, src) {
return '<img src="' + encodeURI(src) + '" alt="' + alt + '">';
});
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)[^)]*\)/g, function (_, text, href) {
if (/^https?:/.test(href))
return '<a href="' + encodeURI(href) + '" target="_blank" rel="noopener">' + text + "</a>";
if (href.charAt(0) === "#")
return '<a href="' + encodeURI(href) + '">' + text + "</a>";
if (/\.md($|#)/.test(href)) {
var cut = href.indexOf("#");
var hash = cut >= 0 ? href.slice(cut) : "";
var path = cut >= 0 ? href.slice(0, cut) : href;
return '<a href="' + docHref(resolve(from, path)) + hash + '">' + text + "</a>";
}
/* Anything else is a file in the repo rather than a document. Link at it
relative to docs/, which is where this page's ../ points. */
return '<a href="../' + encodeURI(resolve(from, href)) + '">' + text + "</a>";
});
s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
s = s.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
var restore = new RegExp(SENTINEL + "(\\d+)" + SENTINEL, "g");
return s.replace(restore, function (_, n) { return spans[+n]; });
}
/* ---- blocks --------------------------------------------------------- */
function render(md, from, headings) {
md = md.replace(/\r\n?/g, "\n").replace(/<!--[\s\S]*?-->/g, "");
var L = md.split("\n"), out = [], i = 0, m;
function listBlock(ordered) {
var tag = ordered ? "ol" : "ul", items = [];
while (i < L.length) {
var li = L[i].match(ordered ? /^\s*\d+\.\s+(.*)$/ : /^\s*[-*+]\s+(.*)$/);
if (!li) {
/* an indented continuation line belongs to the item above it */
if (items.length && /^\s+\S/.test(L[i])) {
items[items.length - 1] += " " + L[i].trim();
i++;
continue;
}
break;
}
items.push(li[1]);
i++;
}
out.push("<" + tag + ">");
items.forEach(function (t) { out.push("<li>" + inline(t, from) + "</li>"); });
out.push("</" + tag + ">");
}
function cells(row) {
return row.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|")
.map(function (c) { return c.trim(); });
}
while (i < L.length) {
var line = L[i];
if (!line.trim()) { i++; continue; }
/* fenced code */
if (/^\s*```/.test(line)) {
var lang = line.replace(/^\s*```/, "").trim(), buf = [];
i++;
while (i < L.length && !/^\s*```/.test(L[i])) buf.push(L[i++]);
i++;
out.push('<pre class="code"' + (lang ? ' data-lang="' + esc(lang) + '"' : "") +
"><code>" + esc(buf.join("\n")) + "</code></pre>");
continue;
}
/* table: a pipe row followed by a separator row */
if (/^\s*\|/.test(line) && /^\s*\|?[\s:|-]+$/.test(L[i + 1] || "") && /-/.test(L[i + 1] || "")) {
var head = cells(line);
i += 2;
out.push('<div class="tablewrap"><table><thead><tr>');
head.forEach(function (c) { out.push("<th>" + inline(c, from) + "</th>"); });
out.push("</tr></thead><tbody>");
while (i < L.length && /^\s*\|/.test(L[i])) {
out.push("<tr>");
cells(L[i++]).forEach(function (c) { out.push("<td>" + inline(c, from) + "</td>"); });
out.push("</tr>");
}
out.push("</tbody></table></div>");
continue;
}
/* heading */
if ((m = line.match(/^(#{1,6})\s+(.*?)\s*#*$/))) {
var lvl = m[1].length, text = m[2], id = slug(text);
if (lvl <= 3) headings.push({ level: lvl, text: text, id: id });
out.push("<h" + lvl + ' id="' + id + '">' + inline(text, from) + "</h" + lvl + ">");
i++;
continue;
}
/* rule */
if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) { out.push("<hr>"); i++; continue; }
/* blockquote */
if (/^\s*>/.test(line)) {
var q = [];
while (i < L.length && /^\s*>/.test(L[i])) q.push(L[i++].replace(/^\s*>\s?/, ""));
out.push("<blockquote>" + render(q.join("\n"), from, []) + "</blockquote>");
continue;
}
if (/^\s*[-*+]\s+/.test(line)) { listBlock(false); continue; }
if (/^\s*\d+\.\s+/.test(line)) { listBlock(true); continue; }
/* paragraph: runs to the next blank line or block opener */
var para = [];
while (i < L.length && L[i].trim() &&
!/^\s*(```|>|#{1,6}\s|[-*+]\s|\d+\.\s|\|)/.test(L[i]) &&
!/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(L[i])) {
para.push(L[i++]);
}
if (para.length) out.push("<p>" + inline(para.join(" "), from) + "</p>");
else i++; /* a block opener we do not handle: never spin on it */
}
return out.join("\n");
}
/* ---- the page ------------------------------------------------------- */
var nav = document.getElementById("docnav");
var article = document.getElementById("doc");
var tocEl = document.getElementById("toc");
function buildNav() {
DOCS.forEach(function (g) {
var h = document.createElement("p");
h.className = "navgroup";
h.textContent = g.group;
nav.appendChild(h);
var ul = document.createElement("ul");
g.items.forEach(function (it) {
var li = document.createElement("li");
var a = document.createElement("a");
a.href = docHref(it[0]);
a.textContent = it[1];
a.dataset.path = it[0];
li.appendChild(a);
ul.appendChild(li);
});
nav.appendChild(ul);
});
}
function markCurrent(path) {
Array.prototype.forEach.call(nav.querySelectorAll("a"), function (a) {
a.classList.toggle("on", a.dataset.path === path);
});
}
function buildToc(headings) {
tocEl.innerHTML = "";
var real = headings.filter(function (h) { return h.level >= 2; });
if (real.length < 2) { tocEl.hidden = true; return; }
tocEl.hidden = false;
var t = document.createElement("p");
t.className = "navgroup";
t.textContent = "On this page";
tocEl.appendChild(t);
var ul = document.createElement("ul");
real.forEach(function (h) {
var li = document.createElement("li");
li.className = "lvl" + h.level;
var a = document.createElement("a");
a.href = "#" + h.id;
a.textContent = h.text;
li.appendChild(a);
ul.appendChild(li);
});
tocEl.appendChild(ul);
}
function fail(title, body) {
article.innerHTML = "<h1>" + title + "</h1>" + body;
tocEl.hidden = true;
}
function load(path) {
markCurrent(path);
document.title = "Docs — SchOS";
article.setAttribute("aria-busy", "true");
fetch("../" + path).then(function (r) {
if (!r.ok) throw new Error(r.status + " " + r.statusText);
return r.text();
}).then(function (text) {
var headings = [];
article.innerHTML = render(text, path, headings);
buildToc(headings);
article.removeAttribute("aria-busy");
if (headings.length) document.title = headings[0].text + " — SchOS docs";
var hash = location.hash.slice(1);
var target = hash && document.getElementById(hash);
if (target) target.scrollIntoView();
else window.scrollTo(0, 0);
}).catch(function (e) {
article.removeAttribute("aria-busy");
/* The common case, and worth naming precisely: browsers refuse fetch()
on file:// for the same reason they refuse it cross-origin, so this
one page needs a server even though nothing here is dynamic. */
if (location.protocol === "file:") {
fail("This page needs a local server",
"<p>Browsers block <code>fetch()</code> on <code>file://</code> URLs, so the " +
"reader cannot load the Markdown. The rest of the site works from a file; " +
"this page does not. Serve the repository root:</p>" +
'<pre class="code"><code>cd /home/nexoniarz/Dokumenty/Projects/SchOS\n' +
"python3 -m http.server 8000</code></pre>" +
"<p>Then open <code>http://localhost:8000/docs/Website/docs.html</code>.</p>" +
'<p>Or read this one directly: <a href="../' + esc(path) + '">' + esc(path) + "</a></p>");
} else {
fail("Could not load that document",
"<p><code>" + esc(path) + "</code> — " + esc(e.message) + "</p>" +
'<p><a href="?doc=README.md">Back to the overview</a></p>');
}
});
}
function pathFromUrl() {
var m = /[?&]doc=([^&#]+)/.exec(location.search);
var p = m ? decodeURIComponent(m[1]) : "README.md";
/* Only ever load something that is in the index above. */
var known = DOCS.some(function (g) {
return g.items.some(function (it) { return it[0] === p; });
});
return known ? p : "README.md";
}
buildNav();
load(pathFromUrl());
/* Intercept in-page navigation so the reader does not reload its own shell. */
document.addEventListener("click", function (ev) {
var a = ev.target.closest ? ev.target.closest("a") : null;
if (!a || ev.metaKey || ev.ctrlKey || ev.shiftKey || a.target) return;
var href = a.getAttribute("href") || "";
if (href.indexOf("?doc=") !== 0) return;
ev.preventDefault();
history.pushState(null, "", href);
load(pathFromUrl());
});
window.addEventListener("popstate", function () { load(pathFromUrl()); });
})();