diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..33ba735 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,54 @@ +# basic + +Minimal black-and-white Hugo theme for text-first blogs. Light/dark/auto, no JS +dependencies. The theme itself lives at repo root; `exampleSite/` is the demo +site used for local dev. + +## Commands + +- `pnpm dev` — hugo server on `exampleSite/` with drafts, live reload +- `pnpm build` — minified production build of `exampleSite/` +- `node assets/js/console.js` — run the console self-check (asserts at bottom of file) + +Hugo min version 0.116.0. Assets go through Hugo Pipes (Minify + Fingerprint), +see `layouts/partials/console.html`. + +## Layout + +- `layouts/` — templates (`_default/`, `partials/`). `baseof.html` is the shell. +- `assets/css/main.css` — the whole stylesheet. One `--accent` var tints hovers. +- `assets/js/console.js` — the floating terminal (below). +- `assets/figlet/heading.flf` — figfont for the console banner. + +## Console + +Floating terminal, progressively enhanced — page works with JS off (launcher + +window start `hidden`, JS unhides). All logic is in one file, `assets/js/console.js`: + +- `run(input, ctx)` is a **pure** function returning `{ lines, clear?, close?, theme?, music?, volume? }`. + Keep it side-effect-free so the `node console.js` self-check can exercise it. +- Browser wiring at the bottom reads those signals (applies theme, plays music, etc). +- 8-bit soundtrack is synthesised live with WebAudio — no audio files. + +### Command style — all commands MUST follow this + +Multi-action commands are **subcommand-based**. Bare command prints a usage +block; a bad subcommand/arg prints a one-line `usage:` hint. Pattern (see +`theme` and `music`): + +``` +Usage: ... + + + +Subcommands: + + : + : +``` + +- Align the `:` in subcommand and `help` lines. +- Bad arg → single line: `usage: `. +- Add every new command to the `help` list (aligned `name : description`). +- Add an `assert` to the self-check block for each new branch, then run + `node assets/js/console.js`. diff --git a/assets/css/main.css b/assets/css/main.css index 162e5e6..0d6fb8e 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -137,6 +137,111 @@ main, .page-head h1 { margin: 0 0 0.5rem; font-size: 1.6rem; letter-spacing: -0.01em; } .page-intro { color: var(--muted); max-width: 46ch; } +/* ---- console (floating terminal) ---- */ +.console-launch { + position: fixed; + right: 1rem; + bottom: 1rem; + z-index: 50; + font-family: var(--mono); + font-size: 0.8rem; + padding: 0.5em 0.9em; + background: var(--bg); + color: var(--fg); + border: 1px solid var(--border); + border-radius: 999px; + cursor: pointer; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12); +} +.console-launch:hover, +.console-launch:focus-visible { color: var(--accent); border-color: var(--accent); } + +.console { + position: fixed; + right: 1rem; + bottom: 1rem; + z-index: 51; + /* roughly an 80x24 terminal */ + width: min(84ch, calc(100vw - 2rem)); + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + box-shadow: 0 10px 34px rgba(0, 0, 0, 0.2); + overflow: hidden; + font-family: var(--mono); + font-size: 0.82rem; + display: flex; + flex-direction: column; + height: min(90vh, 36rem); +} +/* the display:flex above beats the UA [hidden] rule, so hide explicitly */ +.console[hidden] { display: none; } +/* docked: pinned to the bottom quarter of the viewport */ +.console--docked { + inset: auto 0 0 0; + width: 100%; + height: 25vh; + max-height: 25vh; + border-radius: 0; + border-inline: 0; + border-bottom: 0; +} +.console__bar { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem 0.75rem; + border-bottom: 1px solid var(--border); + cursor: move; + user-select: none; + touch-action: none; +} +.console__actions { margin-left: auto; display: flex; gap: 0.15rem; } +.console__btn { + background: none; + border: 0; + padding: 0 0.25em; + color: var(--muted); + font: inherit; + font-size: 1rem; + line-height: 1; + cursor: pointer; +} +.console__btn:hover, +.console__btn:focus-visible { color: var(--accent); } +.console__title { color: var(--muted); font-size: 0.75rem; } +.console__screen { + padding: 0.75rem; + cursor: text; + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; +} +.console__log { margin: 0; flex: 1; min-height: 0; overflow: auto; } +.console__line { display: block; white-space: pre-wrap; color: var(--fg); } +.console__line--cmd { color: var(--muted); } +.console__banner-art { + margin: 0 0 0.5rem; + white-space: pre; + line-height: 1.02; + color: var(--accent); + overflow-x: auto; +} +.console__form { display: flex; align-items: baseline; gap: 0.5em; margin-top: 0.25rem; } +.console__prompt { color: var(--accent); } +.console__input { + flex: 1; + min-width: 0; + background: none; + border: 0; + padding: 0; + color: inherit; + font: inherit; + caret-color: var(--accent); +} +.console__input:focus { outline: none; } + /* ---- topics (taxonomy list) ---- */ .topics { display: flex; diff --git a/assets/figlet/heading.flf b/assets/figlet/heading.flf new file mode 100644 index 0000000..7a446f5 --- /dev/null +++ b/assets/figlet/heading.flf @@ -0,0 +1,434 @@ +flf2a$ 4 4 99 0 20 +Bulbhead by Jef Poskanzer, 23jun94 +Figlet release 2.0 -- August 5, 1993 + ____ __ __ __ ____ _ _ ____ __ ____ +( _ \( )( )( ) ( _ \( )_( )( ___) /__\ ( _ \ + ) _ < )(__)( )(__ ) _ < ) _ ( )___) /(__)\ )(_) ) +(____/(______)(____)(____/(_) (_)(____)(__)(__)(____/ + +Update February 12, 2002 by Markus Gebhard markus@jave.de + Added german umlauts + +Explanation of first line: +flf2 - "magic number" for file identification +a - should always be `a', for now +$ - the "hardblank" -- prints as a blank, but can't be smushed +4 - height of a character +4 - height of a character, not including descenders +99 - max line length (excluding comment lines) + a fudge factor +0 - default smushmode for this font (like "-m 0" on command line) +13 - number of comment lines + +$$@ +$$@ +$$@ +$$@@ +/\@ +)(@ +\/@ +()@@ +||@ + @ + @ + @@ + | | @ +-|-|-@ +-|-|-@ + | | @@ + _|_ @ +/ |_)@ +\_| \@ +(_|_/@@ + _ _ @ +(_)/ ) @ + / /_ @ + (_/(_)@@ + _ @ + ( ) @ + /_\/@ +(__/\@@ +/@ + @ + @ + @@ + _ @ + / )@ +( ( @ + \_)@@ + _ @ +( \ @ + ) )@ +(_/ @@ + @ +\|/@ +/|\@ + @@ + _ @ + _| |_ @ +(_ _)@ + |_| @@ + @ + @ +()@ +/ @@ + @ + ___ @ +(___)@ + @@ + @ + @ + @ +()@@ + _ @ + / )@ + / / @ +(_/ @@ + ___ @ + / _ \ @ +( (_) )@ + \___/ @@ + __ @ + / )@ + )( @ + (__)@@ + ___ @ +(__ \ @ + / _/ @ +(____)@@ + ___ @ +(__ )@ + (_ \@ +(___/@@ + __ @ + /. | @ +(_ _)@ + (_) @@ + ___ @ +| __)@ +|__ \@ +(___/@@ + _ @ + / ) @ +/ _ \@ +\___/@@ + ___ @ +(__ )@ + / / @ +(_/ @@ + ___ @ +( _ )@ +/ _ \@ +\___/@@ + ___ @ +/ _ \@ +\_ /@ + (_/ @@ + @ +()@ + @ +()@@ +()@ + @ +()@ +/ @@ + __@ + / /@ +< < @ + \_\@@ + ___ @ +(___)@ + ___ @ +(___)@@ +__ @ +\ \ @ + > >@ +/_/ @@ + ___ @ +(__ )@ + (_/ @ + (_) @@ + __ @ + / \@ +| ()/@ + \__ @@ + __ @ + /__\ @ + /(__)\ @ +(__)(__)@@ + ____ @ +( _ \@ + ) _ <@ +(____/@@ + ___ @ + / __)@ +( (__ @ + \___)@@ + ____ @ +( _ \ @ + )(_) )@ +(____/ @@ + ____ @ +( ___)@ + )__) @ +(____)@@ + ____ @ +( ___)@ + )__) @ +(__) @@ + ___ @ + / __)@ +( (_-.@ + \___/@@ + _ _ @ +( )_( )@ + ) _ ( @ +(_) (_)@@ + ____ @ +(_ _)@ + _)(_ @ +(____)@@ + ____ @ + (_ _)@ +.-_)( @ +\____) @@ + _ _ @ +( )/ )@ + ) ( @ +(_)\_)@@ + __ @ +( ) @ + )(__ @ +(____)@@ + __ __ @ +( \/ )@ + ) ( @ +(_/\/\_)@@ + _ _ @ +( \( )@ + ) ( @ +(_)\_)@@ + _____ @ +( _ )@ + )(_)( @ +(_____)@@ + ____ @ +( _ \@ + )___/@ +(__) @@ + _____ @ +( _ )@ + )(_)( @ +(___/\\@@ + ____ @ +( _ \@ + ) /@ +(_)\_)@@ + ___ @ +/ __)@ +\__ \@ +(___/@@ + ____ @ +(_ _)@ + )( @ + (__) @@ + __ __ @ +( )( )@ + )(__)( @ +(______)@@ + _ _ @ +( \/ )@ + \ / @ + \/ @@ + _ _ @ +( \/\/ )@ + ) ( @ +(__/\__)@@ + _ _ @ +( \/ )@ + ) ( @ +(_/\_)@@ + _ _ @ +( \/ )@ + \ / @ + (__) @@ + ____ @ +(_ )@ + / /_ @ +(____)@@ + __@ +| @ +| @ +|__@@ + _ @ +( \ @ + \ \ @ + \_)@@ +__ @ + |@ + |@ +__|@@ + / \ @ +(_^_)@ + @ + @@ + @ + @ + ___ @ +(___)@@ +\@ + @ + @ + @@ + __ @ + /__\ @ + /(__)\ @ +(__)(__)@@ + ____ @ +( _ \@ + ) _ <@ +(____/@@ + ___ @ + / __)@ +( (__ @ + \___)@@ + ____ @ +( _ \ @ + )(_) )@ +(____/ @@ + ____ @ +( ___)@ + )__) @ +(____)@@ + ____ @ +( ___)@ + )__) @ +(__) @@ + ___ @ + / __)@ +( (_-.@ + \___/@@ + _ _ @ +( )_( )@ + ) _ ( @ +(_) (_)@@ + ____ @ +(_ _)@ + _)(_ @ +(____)@@ + ____ @ + (_ _)@ +.-_)( @ +\____) @@ + _ _ @ +( )/ )@ + ) ( @ +(_)\_)@@ + __ @ +( ) @ + )(__ @ +(____)@@ + __ __ @ +( \/ )@ + ) ( @ +(_/\/\_)@@ + _ _ @ +( \( )@ + ) ( @ +(_)\_)@@ + _____ @ +( _ )@ + )(_)( @ +(_____)@@ + ____ @ +( _ \@ + )___/@ +(__) @@ + _____ @ +( _ )@ + )(_)( @ +(___/\\@@ + ____ @ +( _ \@ + ) /@ +(_)\_)@@ + ___ @ +/ __)@ +\__ \@ +(___/@@ + ____ @ +(_ _)@ + )( @ + (__) @@ + __ __ @ +( )( )@ + )(__)( @ +(______)@@ + _ _ @ +( \/ )@ + \ / @ + \/ @@ + _ _ @ +( \/\/ )@ + ) ( @ +(__/\__)@@ + _ _ @ +( \/ )@ + ) ( @ +(_/\_)@@ + _ _ @ +( \/ )@ + \ / @ + (__) @@ + ____ @ +(_ )@ + / /_ @ +(____)@@ + ,-@ +_| @ + | @ + `-@@ +/\@ +||@ +||@ +\/@@ +-. @ + |_@ + | @ +-' @@ + @ +/\/@ + @ + @@ + (_)(_) @ + /__\ @ + /(__)\ @ +(__)(__)@@ +(_)_(_)@ +( _ )@ + )(_)( @ +(_____)@@ + (_)(_) @ +( )( )@ + )(__)( @ +(______)@@ + (_)(_) @ + /__\ @ + /(__)\ @ +(__)(__)@@ +(_)_(_)@ +( _ )@ + )(_)( @ +(_____)@@ + (_)(_) @ +( )( )@ + )(__)( @ +(______)@@ + ___ @ +/ _ )@ +| _ \@ +| __/@@ +160 NO-BREAK SPACE + $@ + $@ + $@ + $@@ diff --git a/assets/js/console.js b/assets/js/console.js new file mode 100644 index 0000000..bd89e6e --- /dev/null +++ b/assets/js/console.js @@ -0,0 +1,324 @@ +/* basic — a small floating terminal. No deps. Hidden until the launcher opens it; + the page works fine without it. */ +(function () { + "use strict"; + + // ---- pure core (side-effect-free so `node console.js` can self-check) ---- + + function parse(input) { + var parts = String(input).trim().split(/\s+/); + return { name: (parts[0] || "").toLowerCase(), args: parts.slice(1) }; + } + + // Minimal FIGfont parser + renderer (full-width, no smushing). No deps. + function parseFig(txt) { + var lines = String(txt).split("\n"); + var head = lines[0].split(" "); + var hard = head[0].slice(-1); // hardblank char (last of signature) + var height = parseInt(head[1], 10); + var comment = parseInt(head[5], 10); + if (!(height > 0)) return null; + var map = {}, idx = 1 + comment; + for (var c = 32; c <= 126; c++) { + var glyph = []; + for (var r = 0; r < height; r++) { + var line = (lines[idx++] || "").replace(/\r$/, ""); + var end = line.slice(-1); + if (end) line = line.replace(new RegExp(end.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "+$"), ""); + glyph.push(line.split(hard).join(" ")); + } + map[c] = glyph; + } + return { height: height, map: map }; + } + + function banner(text, fig) { + if (!fig) return [String(text)]; + var rows = []; + for (var r = 0; r < fig.height; r++) rows.push(""); + var s = String(text); + for (var i = 0; i < s.length; i++) { + var g = fig.map[s.charCodeAt(i)] || fig.map[32]; + for (var r2 = 0; r2 < fig.height; r2++) rows[r2] += g[r2] || ""; + } + // drop fully-blank leading/trailing rows some fonts pad with + while (rows.length && !rows[0].trim()) rows.shift(); + while (rows.length && !rows[rows.length - 1].trim()) rows.pop(); + return rows; + } + + // run(input, ctx) -> { lines:[{text}], clear?, close?, theme? }. ctx.history: string[] + function run(input, ctx) { + var cmd = parse(input); + var history = (ctx && ctx.history) || []; + + switch (cmd.name) { + case "": + return { lines: [] }; + + case "help": + return { lines: [ + { text: "commands" }, + { text: " theme : changes the theme of the site" }, + { text: " music : plays an 8-bit soundtrack" }, + { text: " history : shows command history" }, + { text: " clear : clears the screen" }, + { text: " close : closes the console" }, + { text: " help : this message" } + ] }; + + case "theme": { + var sub = (cmd.args[0] || "").toLowerCase(); + var modes = ["light", "dark", "auto"]; + if (sub === "list") { + return { lines: modes.map(function (m) { return { text: " " + m }; }) }; + } + if (sub === "set") { + var mode = (cmd.args[1] || "").toLowerCase(); + if (modes.indexOf(mode) === -1) return { lines: [{ text: "usage: theme set " }] }; + return { lines: [{ text: "theme set to " + mode + "." }], theme: mode }; + } + return { lines: [ + { text: "Usage: theme ..." }, + { text: "" }, + { text: " Changes the theme of the site" }, + { text: "" }, + { text: "Subcommands:" }, + { text: "" }, + { text: " list : List available themes" }, + { text: " set : Set the theme" } + ] }; + } + + case "history": + if (!history.length) return { lines: [{ text: "no history yet." }] }; + return { lines: history.map(function (h, i) { + return { text: (i + 1 < 10 ? " " : "") + (i + 1) + " " + h }; + }) }; + + case "music": { + var sub = (cmd.args[0] || "").toLowerCase(); + if (sub === "play") return { lines: [], music: "on" }; + if (sub === "stop") return { lines: [], music: "off" }; + if (sub === "volume") { + var v = parseInt(cmd.args[1], 10); + if (!(v >= 0 && v <= 10)) return { lines: [{ text: "usage: music volume <0-10>" }] }; + return { lines: [{ text: "volume set to " + v + "." }], volume: v }; + } + return { lines: [ + { text: "Usage: music ..." }, + { text: "" }, + { text: " Play some tunes" }, + { text: "" }, + { text: "Subcommands:" }, + { text: "" }, + { text: " play : Play music" }, + { text: " stop : Stop music" }, + { text: " volume : Set music volume [0-10]" } + ] }; + } + + case "clear": + return { lines: [], clear: true }; + + case "close": + return { lines: [{ text: "bye." }], close: true }; + + default: + return { lines: [{ text: cmd.name + ": command not found. Type `help`." }] }; + } + } + + // ---- browser wiring ---- + + if (typeof document !== "undefined") { + var launch = document.querySelector(".console-launch"); + var root = document.querySelector(".console"); + if (launch && root) { + var bar = root.querySelector(".console__bar"); + var log = root.querySelector(".console__log"); + var form = root.querySelector(".console__form"); + var input = root.querySelector(".console__input"); + var expandBtn = root.querySelector(".console__expand"); + var closeBtn = root.querySelector(".console__close"); + var cmdHistory = []; // local — do not shadow window.history + + var write = function (line, kind) { + var el = document.createElement("div"); + el.className = "console__line" + (kind ? " console__line--" + kind : ""); + el.textContent = line.text; + log.appendChild(el); + }; + + // ---- 8-bit soundtrack: calm C-major pentatonic, WebAudio, no assets ---- + // 32 eighth-notes. midi note, 0 = rest. Sparse melody that rings. + var chiptune = (function () { + var lead = [72, 0, 76, 0, 79, 0, 76, 0, 74, 0, 77, 0, 81, 0, 79, 0, + 72, 0, 76, 0, 84, 0, 79, 0, 81, 0, 79, 0, 76, 0, 74, 0]; + var bass = [48, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, + 41, 0, 0, 0, 0, 0, 0, 0, 43, 0, 0, 0, 0, 0, 0, 0]; + var spn = 60 / 84 / 2; // seconds per eighth-note at a gentle 84bpm + var ctx, master, timer, nextTime, step, playing = false, vol = 0.5; + var freq = function (m) { return 440 * Math.pow(2, (m - 69) / 12); }; + var voice = function (m, t, dur, type, peak) { // peak: per-note gain (distinct from master vol) + if (!m) return; + var o = ctx.createOscillator(), g = ctx.createGain(); + o.type = type; o.frequency.value = freq(m); + g.gain.setValueAtTime(0.0001, t); + g.gain.exponentialRampToValueAtTime(peak, t + 0.04); // soft attack + g.gain.exponentialRampToValueAtTime(0.0001, t + dur); + o.connect(g); g.connect(master); o.start(t); o.stop(t + dur); + }; + var tick = function () { + while (nextTime < ctx.currentTime + 0.12) { + voice(lead[step], nextTime, spn * 2.2, "triangle", 0.14); // let it ring + voice(bass[step], nextTime, spn * 7, "sine", 0.12); // slow pad + step = (step + 1) % lead.length; + nextTime += spn; + } + }; + return { + // Resolves once audio is actually running; rejects if the browser blocks it. + on: function () { + if (playing) return Promise.resolve(); + if (!ctx) { + ctx = new (window.AudioContext || window.webkitAudioContext)(); + master = ctx.createGain(); master.gain.value = vol; master.connect(ctx.destination); + } + playing = true; step = 0; + return Promise.resolve(ctx.resume()).then(function () { + nextTime = ctx.currentTime + 0.05; + tick(); timer = setInterval(tick, 25); + }, function (err) { playing = false; throw err; }); + }, + off: function () { playing = false; clearInterval(timer); }, + setVolume: function (n) { vol = n / 10; if (master) master.gain.value = vol; } + }; + })(); + + var applyTheme = function (mode) { + if (mode === "auto") { localStorage.removeItem("theme"); delete document.documentElement.dataset.theme; } + else { localStorage.setItem("theme", mode); document.documentElement.dataset.theme = mode; } + }; + + var open = function () { + root.hidden = false; + launch.setAttribute("aria-expanded", "true"); + input.focus(); + }; + var close = function () { + chiptune.off(); + root.hidden = true; + root.classList.remove("console--docked"); + root.style.left = root.style.top = root.style.right = root.style.bottom = ""; + expandBtn.setAttribute("aria-pressed", "false"); + launch.setAttribute("aria-expanded", "false"); + launch.focus(); + }; + var toggleDock = function () { + var docked = root.classList.toggle("console--docked"); + if (docked) { root.style.left = root.style.top = root.style.right = root.style.bottom = ""; } + expandBtn.setAttribute("aria-pressed", docked ? "true" : "false"); + input.focus(); + }; + + launch.addEventListener("click", open); + closeBtn.addEventListener("click", close); + expandBtn.addEventListener("click", toggleDock); + + document.addEventListener("keydown", function (e) { + if (e.key === "Escape" && !root.hidden) close(); + }); + + form.addEventListener("submit", function (e) { + e.preventDefault(); + var value = input.value; + write({ text: "$ " + value }, "cmd"); + if (value.trim()) cmdHistory.push(value.trim()); + var res = run(value, { history: cmdHistory }); + if (res.clear) log.innerHTML = ""; + res.lines.forEach(function (l) { write(l); }); + if (res.theme) applyTheme(res.theme); + if (res.music === "on") { + chiptune.on().then( + function () { write({ text: "♪ music on" }); log.scrollTop = log.scrollHeight; }, + function () { write({ text: "music blocked by the browser — interact with the page and retry." }); log.scrollTop = log.scrollHeight; } + ); + } + if (res.music === "off") { chiptune.off(); write({ text: "music off" }); } + if (res.volume != null) chiptune.setVolume(res.volume); + input.value = ""; + log.scrollTop = log.scrollHeight; + if (res.close) close(); + }); + + root.querySelector(".console__screen").addEventListener("click", function () { input.focus(); }); + + // Drag by the title bar (skipped while docked). + bar.addEventListener("pointerdown", function (e) { + if (e.target.closest(".console__btn") || root.classList.contains("console--docked")) return; + var rect = root.getBoundingClientRect(); + var dx = e.clientX - rect.left, dy = e.clientY - rect.top; + var move = function (ev) { + var x = Math.max(0, Math.min(window.innerWidth - rect.width, ev.clientX - dx)); + var y = Math.max(0, Math.min(window.innerHeight - rect.height, ev.clientY - dy)); + root.style.left = x + "px"; root.style.top = y + "px"; + root.style.right = root.style.bottom = "auto"; + }; + var up = function () { + document.removeEventListener("pointermove", move); + document.removeEventListener("pointerup", up); + }; + document.addEventListener("pointermove", move); + document.addEventListener("pointerup", up); + }); + + write({ text: "\n" }); + write({ text: "type `help` to see available commands" }); + launch.hidden = false; + + // Banner: fetch the figfont (a cached, fingerprinted asset — not inlined + // base64 per page) and render it above the welcome text once it arrives. + if (root.dataset.fontSrc) { + fetch(root.dataset.fontSrc).then(function (r) { return r.text(); }).then(function (txt) { + var fig = null; + try { fig = parseFig(txt); } catch (e) { fig = null; } + var art = document.createElement("pre"); + art.className = "console__banner-art"; + art.setAttribute("aria-hidden", "true"); // decorative ASCII — keep it out of the aria-live log + art.textContent = banner(root.dataset.title || "", fig).join("\n"); + log.insertBefore(art, log.firstChild); + }).catch(function () {}); // no banner if the font can't load — page still works + } + } + } + + // ---- self-check: `node assets/js/console.js` ---- + + if (typeof module !== "undefined" && require.main === module) { + var assert = require("assert"); + assert.strictEqual(run("", {}).lines.length, 0); + assert.ok(/theme/.test(run("help", {}).lines[1].text)); + assert.strictEqual(run("theme set dark", {}).theme, "dark"); + assert.ok(/usage: theme set/.test(run("theme set purple", {}).lines[0].text)); + assert.ok(/dark/.test(run("theme list", {}).lines[1].text)); + assert.ok(/Subcommands/.test(run("theme", {}).lines[4].text)); + assert.strictEqual(run("music play", {}).music, "on"); + assert.strictEqual(run("music stop", {}).music, "off"); + assert.strictEqual(run("music volume 7", {}).volume, 7); + assert.ok(/0-10/.test(run("music volume 99", {}).lines[0].text)); + assert.ok(/Subcommands/.test(run("music", {}).lines[4].text)); + assert.strictEqual(run("clear", {}).clear, true); + assert.strictEqual(run("close", {}).close, true); + assert.ok(/no history/.test(run("history", { history: [] }).lines[0].text)); + assert.ok(/theme dark/.test(run("history", { history: ["theme dark"] }).lines[0].text)); + assert.ok(/not found/.test(run("wat", {}).lines[0].text)); + assert.deepStrictEqual(banner("x", null), ["x"], "no font -> plain title"); + var flf = require("fs").readFileSync(__dirname + "/../figlet/heading.flf", "utf8"); + var fig = parseFig(flf); + assert.ok(fig && fig.height > 0, "figfont parses"); + var b = banner("basic", fig); + assert.ok(b.length > 0 && b.some(function (r) { return /\S/.test(r); }), "banner renders glyphs"); + console.log("console.js self-check passed\n" + b.join("\n")); + } +})(); diff --git a/layouts/index.html b/layouts/index.html index df23419..65160bb 100644 --- a/layouts/index.html +++ b/layouts/index.html @@ -2,6 +2,7 @@ {{/* No h1 — the site title in the header already carries it. Keep the intro only. */}} {{ $intro := or .Content site.Params.description }} {{ with $intro }}
{{ . }}
{{ end }} + {{ partial "console.html" . }} {{ partial "topics.html" . }} {{ partial "post-list.html" (where site.RegularPages "Type" "posts") }} {{ end }} diff --git a/layouts/partials/console.html b/layouts/partials/console.html new file mode 100644 index 0000000..7c8ae76 --- /dev/null +++ b/layouts/partials/console.html @@ -0,0 +1,25 @@ +{{/* Floating terminal. Launcher + window both stay hidden until JS enhances them, + so the page is unaffected without JS. */}} + + +{{ $font := resources.Get "figlet/heading.flf" | resources.Fingerprint }} + + +{{ $js := resources.Get "js/console.js" | resources.Minify | resources.Fingerprint }} +