commit 1c785cc930aca52a8537e37851dccc8b346645f9
parent ff8e7f5c4e7ade715d7d92a1d1c9ba73b7a4a3c4
Author: Michael Neuper <michael@michaelneuper.com>
Date: Sun, 11 Oct 2026 22:57:11 +0200
rework log section
Diffstat:
| M | css/style.css | | | 173 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------- |
| M | index.html | | | 90 | ++++++++++++++++++++++++++++++++++++++----------------------------------------- |
| M | js/computer.js | | | 6 | ++++-- |
| A | js/gitlog.js | | | 226 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | js/main.js | | | 2 | ++ |
| M | js/org.js | | | 97 | ++++++++++++++++++++++++++++++++++++++++++++++--------------------------------- |
| M | js/shell.js | | | 12 | ++++++------ |
7 files changed, 460 insertions(+), 146 deletions(-)
diff --git a/css/style.css b/css/style.css
@@ -64,6 +64,7 @@
--hl: color-mix(in srgb, var(--fg) 10%, var(--bg));
--wash: color-mix(in srgb, var(--fg) 5%, transparent);
--led-off: color-mix(in srgb, var(--fg) 13%, var(--bg));
+ --violet: color-mix(in oklch, var(--blue) 50%, var(--magenta));
--c-hw: var(--yellow);
--c-sw: var(--blue);
@@ -1056,35 +1057,97 @@ input[type="range"]::-moz-range-thumb {
padding-right: 3ch;
}
-.ds-rev {
- min-width: 0;
+/* experience as git log --graph --color */
+
+.gitlog-cmd {
+ margin-bottom: 12px;
+ font-weight: 400;
+ color: var(--fg);
+ line-height: 1.5;
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
}
-.ds-rev th:first-child {
- width: 16ch;
- white-space: nowrap;
+.gitlog-cmd .prompt {
+ color: var(--green);
}
-.ds-rev td:nth-child(2) {
+.gitlog {
+ padding: 16px 18px;
+ overflow-x: auto;
+ border: 1px solid var(--rule);
+ font-size: 12px;
+ line-height: 1.7;
color: var(--fg);
}
-.ds-rev td p {
- margin: 0;
+.gl-line {
+ width: max-content;
+ white-space: pre;
+}
+
+/* one colour per branch */
+.gitlog .lm { color: var(--magenta); } /* main */
+.gitlog .lu { color: var(--green); } /* undergrad */
+.gitlog .lt { color: var(--yellow); } /* teaching-assistant */
+.gitlog .lb { color: var(--red); } /* bbd-software */
+.gitlog .li { color: var(--blue); } /* mwr-internship */
+
+.gl-tip {
+ color: var(--fg);
+ font-weight: 600;
}
-.ds-rev td ul {
- margin: 6px 0 0;
- padding-left: 2.5ch;
+.gl-refs {
+ color: var(--yellow);
+}
+
+.gl-head {
+ color: var(--cyan);
+ font-weight: 600;
}
-.ds-rev td li {
- margin-bottom: 3px;
+.gl-branch {
+ color: var(--green);
+ font-weight: 600;
+}
+
+.gl-author {
+ color: var(--blue);
+}
+
+.gl-subj {
+ padding: 0;
+ border: 0;
+ background: none;
+ color: inherit;
+ font: inherit;
+ white-space: pre;
+ text-decoration: underline dotted;
+ text-decoration-color: var(--faint);
+ text-underline-offset: 0.25em;
+}
+
+.gl-subj:hover,
+.gl-subj[aria-expanded="true"] {
+ text-decoration-style: solid;
+ text-decoration-color: currentColor;
+}
+
+.gl-body,
+.gl-desc {
color: var(--dim);
}
-.ds-rev td li::marker {
- color: var(--faint);
+/* before the script runs (or without it), descriptions show under their commit */
+.gl-desc {
+ display: block;
+ padding-left: 4ch;
+}
+
+.gitlog-hint {
+ margin: 8px 0 0;
+ font-size: 12px;
}
/* ───────────────────────────── memory map ───────────────────────────── */
@@ -1400,12 +1463,14 @@ input[type="range"]::-moz-range-thumb {
/* ───────────────────────────── posts: an Org buffer ───────────────────────────── */
.org {
- max-width: 820px;
+ max-width: 880px;
font-size: 13px;
+ line-height: 1.75;
}
.org-kw {
margin: 0;
+ font: inherit;
color: var(--dim);
}
@@ -1413,13 +1478,24 @@ input[type="range"]::-moz-range-thumb {
color: var(--fg);
}
-.org-kw .title {
- font: 400 1.6em/1.4 var(--serif);
+.org-title {
color: var(--fg);
+ font-weight: 600;
}
-.org-tree {
+.org-p {
+ max-width: 76ch;
margin: 20px 0 0;
+ font-size: 13px;
+ line-height: 1.75;
+}
+
+.org > .org-tree {
+ margin-top: 20px;
+}
+
+.org-tree {
+ margin: 0;
padding: 0;
list-style: none;
}
@@ -1428,7 +1504,6 @@ input[type="range"]::-moz-range-thumb {
display: flex;
align-items: baseline;
gap: 1ch;
- padding: 3px 0;
}
.org-h:focus-visible {
@@ -1436,42 +1511,27 @@ input[type="range"]::-moz-range-thumb {
background: var(--hl);
}
-.org-bullet {
+.org-stars {
flex: none;
- width: 2ch;
padding: 0;
border: 0;
background: none;
- color: var(--c, var(--blue));
- font: 400 13px/1 var(--mono);
- text-align: left;
+ color: inherit;
+ font: inherit;
}
.org-h1 > .org-h {
- margin-top: 10px;
- font: 400 21px/1.3 var(--serif);
-}
-
-.org-h1 > .org-h .org-bullet {
color: var(--blue);
-}
-
-.org-h1 > .org-h .count {
- font: 400 12px/1 var(--mono);
- color: var(--dim);
-}
-
-.org-h2 {
- padding-left: 3ch;
+ font-weight: 600;
}
.org-h2 > .org-h {
- font: 400 17px/1.4 var(--serif);
+ color: var(--magenta);
}
.org-h2 a {
- flex: 1;
min-width: 0;
+ color: inherit;
text-decoration: none;
}
@@ -1480,33 +1540,35 @@ input[type="range"]::-moz-range-thumb {
text-decoration-thickness: 1px;
}
+.org-h .count,
+.org-ellipsis {
+ color: var(--dim);
+ font-weight: 400;
+}
+
.org-tags {
flex: none;
margin-left: auto;
- font: 400 12px/1 var(--mono);
+ padding-left: 2ch;
color: var(--dim);
+ white-space: nowrap;
}
.org-tags .t {
- color: var(--c, var(--dim));
-}
-
-.org-ellipsis {
- color: var(--dim);
- font: 400 12px/1 var(--mono);
+ color: var(--tag, var(--dim));
}
.org-drawer {
- margin: 2px 0 6px 3ch;
+ margin: 0 0 6px 3ch;
+ overflow-x: auto;
color: var(--dim);
font-size: 12px;
line-height: 1.6;
white-space: pre;
- overflow-x: auto;
}
.org-drawer .k {
- color: var(--magenta);
+ color: var(--faint);
}
.org-drawer .ts {
@@ -2220,6 +2282,15 @@ input[type="range"]::-moz-range-thumb {
grid-template-columns: minmax(0, 1fr);
}
+ .gitlog {
+ padding: 12px;
+ font-size: 11.5px;
+ }
+
+ .gitlog-cmd {
+ font-size: 11.5px;
+ }
+
.org-tags {
display: none;
}
diff --git a/index.html b/index.html
@@ -30,6 +30,7 @@
<span>michael neuper</span>
</a>
<nav class="nav" id="nav" aria-label="Sections">
+ <a href="#top">computer</a>
<a href="#about">about</a>
<a href="#projects">projects</a>
<a href="#posts">posts</a>
@@ -119,51 +120,47 @@
</figure>
</div>
- <h3 class="ds-h">3 Revision history</h3>
- <div class="table-scroll">
- <table class="ds-table ds-rev">
- <thead>
- <tr><th scope="col">Date</th><th scope="col">Changes</th></tr>
- </thead>
- <tbody>
- <tr>
- <th scope="row"><time datetime="2026-06">June 2026</time></th>
- <td>Teaching assistant for computer architecture.</td>
- </tr>
- <tr>
- <th scope="row"><time datetime="2026-02">February 2026</time></th>
- <td>Teaching assistant for introductory computer science.</td>
- </tr>
- <tr>
- <th scope="row"><time datetime="2025-01">January 2025</time></th>
- <td>
- <p>Bursar at BBD Software, in its Bursary & Beyond Programme. Each year’s Vac Weeks put bursars in small teams to design, build and present a full-stack web application:</p>
- <ul>
- <li>2025: an online laser tag game that uses computer vision to detect and tag players as they play.</li>
- <li>2026: an online foosball game you steer with your phone’s gyroscope.</li>
- </ul>
- </td>
- </tr>
- <tr>
- <th scope="row"><time datetime="2024-12">December 2024</time></th>
- <td>
- <p>Cybersecurity consultant intern.</p>
- <ul>
- <li>Completed cybersecurity training with labs on TryHackMe.</li>
- <li>Assessed the security of a web application from end to end, documenting each vulnerability with its risk and how to fix it.</li>
- <li>Wrote a professional report and presented it, client-style, to a panel of industry professionals.</li>
- </ul>
- </td>
- </tr>
- <tr>
- <th scope="row"><time datetime="2024-01">January 2024</time></th>
- <td>Initial release. Started an undergraduate degree in computer science at Stellenbosch University.</td>
- </tr>
- </tbody>
- </table>
+ <!-- Experience, as the one-line git log that would print it: "<date> - (refs) <subject> <author>".
+ data-lanes colours each graph character by branch: m main, u undergrad, t teaching-assistant,
+ b bbd-software, i mwr-internship (the colours are set in css/style.css). data-cont is the graph that continues below a commit, and a
+ .gl-desc becomes its expandable description. data-branch colours the date; data-tip makes the
+ * bold for a branch that's still going; data-today swaps in the current month when the page loads.
+ Without JavaScript this shows as plain text. -->
+ <h3 class="ds-h gitlog-cmd"><span class="prompt" aria-hidden="true">$ </span>git log --graph --all --color \
+ --date=format:'%Y-%m' \
+ --format='%ad -%C(auto)%d%C(reset) %s %C(blue)<%an>%C(reset)'</h3>
+ <div class="gitlog" aria-label="Experience, newest first">
+<div class="gl-line" data-lanes=" b u t " data-cont="| | | | " data-cont-lanes="m b u t " data-branch="m" data-tip data-today>* | | | 2026-10 - (HEAD -> main)</div>
+<div class="gl-line" data-lanes="m u t " data-cont="| | | | " data-cont-lanes="m b u t " data-branch="b" data-tip>| * | | 2026-06 - (bbd-software) Vac Week 2026 <BBD Software><div class="gl-desc">Built an online foosball game: players steer the rods by
+tilting their phones, using the gyroscope. Designed and
+presented as a full-stack web app with a small team.</div></div>
+<div class="gl-line" data-lanes="m b u " data-cont="| | | | " data-cont-lanes="m b u t " data-branch="t" data-tip>| | | * 2026-06 - (teaching-assistant) Teach computer architecture as a TA <Stellenbosch University></div>
+<div class="gl-line" data-lanes="m b u t">| | | |</div>
+<div class="gl-line" data-lanes="m b u " data-cont="| | | | " data-cont-lanes="m b u t " data-branch="t">| | | * 2026-02 - Teach introductory computer science as a TA <Stellenbosch University></div>
+<div class="gl-line" data-lanes="m u t " data-cont="| | | | " data-cont-lanes="m b u t " data-branch="b">| * | | 2025-06 - Vac Week 2025 <BBD Software><div class="gl-desc">Built an online laser tag game: computer vision finds players
+in the camera feed and tags them. Designed and presented as a
+full-stack web app with a small team.</div></div>
+<div class="gl-line" data-lanes="m b u t">| | | |</div>
+<div class="gl-line" data-lanes="m u t " data-cont="| | | | " data-cont-lanes="m b u t " data-branch="b">| * | | 2025-01 - Join BBD Software as a bursar <BBD Software><div class="gl-desc">Part of the Bursary & Beyond Programme, which includes the
+yearly Vac Weeks.</div></div>
+<div class="gl-line" data-lanes="mb u t">|/ | |</div>
+<div class="gl-line" data-lanes="m u t">| | |</div>
+<div class="gl-line" data-lanes="mi u t">|\ | |</div>
+<div class="gl-line" data-lanes="m u t " data-cont="| | | | " data-cont-lanes="m i u t " data-branch="i">| * | | 2024-12 - (mwr-internship) Complete a cybersecurity consultant internship <MWR CyberSec><div class="gl-desc">- Completed cybersecurity training with labs on TryHackMe.
+- Assessed a web application end to end, documenting each
+ vulnerability with its risk and how to fix it.
+- Wrote a professional report and presented it, client-style,
+ to a panel of industry professionals.</div></div>
+<div class="gl-line" data-lanes="mi u t">|/ | |</div>
+<div class="gl-line" data-lanes="m ut">| |/</div>
+<div class="gl-line" data-lanes="m " data-cont="| | " data-cont-lanes="m u " data-branch="u" data-tip>| * 2024-01 - (undergrad) Start an undergraduate degree in computer science <Stellenbosch University></div>
+<div class="gl-line" data-lanes="m u">| /</div>
+<div class="gl-line" data-lanes="mu">|/</div>
+<div class="gl-line" data-lanes=" " data-cont=" " data-cont-lanes=" " data-branch="m">* 2005-06 - Initial commit <Michael Neuper></div>
</div>
+ <p class="gitlog-hint dim">Click an underlined commit message to read the rest of it.</p>
- <h3 class="ds-h">4 Ordering information</h3>
+ <h3 class="ds-h">3 Ordering information</h3>
<p class="prose">Interested in systems programming, low-level software and security work. If that’s what you do, <a href="#contact">get in touch</a>.</p>
</section>
@@ -218,10 +215,9 @@
<!-- ────────────────────────────── posts ────────────────────────────── -->
<section class="sec" id="posts" data-file="posts.org" data-mode="Org">
<p class="winbar"><span class="file">posts.org</span><span class="rule" aria-hidden="true"></span><span class="meta" data-org="meta"></span></p>
- <h2 class="h2">Posts</h2>
- <p class="lede">From my blog at michaelneuper.com, shown the way I see them: as an Org buffer. Click a bullet or press <kbd>TAB</kbd> on a heading to fold it, <kbd>S-TAB</kbd> to cycle the whole buffer.</p>
+ <p class="org-p">From my blog at michaelneuper.com, shown the way I read them. <kbd>TAB</kbd> on a heading folds it, <kbd>S-TAB</kbd> cycles the whole buffer, and clicking a heading’s stars does the same.</p>
- <!-- Edit posts here. data-tags are space separated; data-cat colours the bullet. -->
+ <!-- Edit posts here. data-tags are space separated; each tag gets its own colour (see TAG_COLORS in js/org.js). -->
<ol class="posts">
<li data-date="2024-11-22" data-tags="emacs org" data-cat="sw"><a href="https://michaelneuper.com/posts/how-i-use-org-roam-to-takes-notes-for-cs/">How I Use Org-Roam to Take Notes for CS</a></li>
<li data-date="2023-11-15" data-tags="emacs latex" data-cat="math"><a href="https://michaelneuper.com/posts/efficient-latex-editing-with-emacs/">Efficient LaTeX Editing With Emacs</a></li>
@@ -309,7 +305,7 @@
<span class="ml-flags" aria-hidden="true"></span>
<span class="ml-mode">Asm</span>
<span class="ml-enc" aria-hidden="true">LF UTF-8</span>
- <span class="ml-vc" aria-hidden="true">Git-master</span>
+ <span class="ml-vc" aria-hidden="true">master</span>
<span class="ml-time" aria-hidden="true"></span>
</div>
<div class="mini" hidden>
diff --git a/js/computer.js b/js/computer.js
@@ -11,6 +11,8 @@ import { defcommand, message, completingRead, setSegment } from "./shell.js";
// Control lines, in the order the control-word LEDs show them.
const LINES = ["HLT", "MI", "RI", "RO", "IO", "II", "AI", "AO", "EO", "SU", "BI", "OI", "CE", "CO", "J", "FI"];
const L = Object.fromEntries(LINES.map((n, i) => [n, 1 << (15 - i)]));
+// How the lines are printed on the board: the sum-out line is ΣO, as on the build.
+const LABEL = { EO: "ΣO" };
const OPS = { 0: "NOP", 1: "LDA", 2: "ADD", 3: "SUB", 4: "STA", 5: "LDI", 6: "JMP", 7: "JC", 8: "JZ", 14: "OUT", 15: "HLT" };
const CODE = Object.fromEntries(Object.entries(OPS).map(([k, v]) => [v, +k]));
@@ -161,7 +163,7 @@ export function initComputer() {
const head = el("div", "mod-head");
head.append(el("span", "mod-name", name));
for (const sig of sigs) {
- const t = el("span", "sig", sig);
+ const t = el("span", "sig", LABEL[sig] ?? sig);
t.dataset.sig = sig;
head.append(t);
}
@@ -293,7 +295,7 @@ export function initComputer() {
const cwBits = LINES.map((n) => {
const bit = el("span", "cw-bit");
const l = el("span", "led b");
- bit.append(l, el("span", "", n));
+ bit.append(l, el("span", "", LABEL[n] ?? n));
cwWrap.append(bit);
return [bit, l];
});
diff --git a/js/gitlog.js b/js/gitlog.js
@@ -0,0 +1,226 @@
+// Experience as one-line git log --graph output, the "git lg" way:
+// <date> - (refs) <subject> <author>
+// with the date standing in for the hash and the company as the author.
+//
+// The rows are plain text in index.html, so they read fine without JavaScript.
+// This colours the graph by branch (data-lanes), colours the date, refs and
+// author, and turns subjects that have a .gl-desc into buttons that expand it.
+// Lanes: m main, u undergrad, t teaching-assistant, b bbd-software, i mwr-internship.
+//
+// When the log is wider than its box (on a phone), it wraps the way a narrow
+// terminal would: at word boundaries, with each wrapped line starting with the
+// commit's lane pipes and a short hanging indent.
+
+import { $, $$ } from "./util.js";
+
+const LANE_CLASS = { m: "lm", u: "lu", t: "lt", b: "lb", i: "li" };
+const ROW = /^([|*/\\ ]*?)(\d{4}-\d{2}(?:-\d{2})?) - (?:\(([^)]*)\) ?)?(.*?)(?: ?<([^>]*)>)?$/;
+const HANG = 2;
+
+function el(tag, cls, text) {
+ const e = document.createElement(tag);
+ if (cls) e.className = cls;
+ if (text != null) e.textContent = text;
+ return e;
+}
+
+// The graph characters, coloured by lane and hidden from screen readers.
+// `tip` makes the commit's * bold.
+function graph(text, lanes = "", tip = false) {
+ const g = el("span", "gl-graph");
+ g.setAttribute("aria-hidden", "true");
+ [...text].forEach((ch, i) => {
+ const lane = LANE_CLASS[(lanes[i] || " ").trim()];
+ if (ch === "*" && tip) g.append(el("span", "gl-tip", ch));
+ else g.append(lane && ch.trim() ? el("span", lane, ch) : ch);
+ });
+ return g;
+}
+
+// The HEAD commit is dated with the month the page is viewed in.
+function today() {
+ const d = new Date();
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
+}
+
+function parse(div) {
+ const descEl = $(".gl-desc", div);
+ const body = descEl ? descEl.textContent.replace(/^\n+|\s+$/g, "").split("\n") : null;
+ descEl?.remove();
+ const text = div.textContent.replace(/\s+$/, "");
+ const m = ROW.exec(text);
+ const row = {
+ graph: m ? m[1] : text, lanes: div.dataset.lanes, cont: div.dataset.cont ?? "", contLanes: div.dataset.contLanes,
+ tip: "tip" in div.dataset, branch: div.dataset.branch,
+ };
+ if (!m) return row;
+ return {
+ ...row,
+ date: "today" in div.dataset ? today() : m[2],
+ refs: m[3] ? m[3].split(/,\s*/).map((r) => (r.startsWith("HEAD -> ") ? { head: true, name: r.slice(8) } : { name: r })) : [],
+ subject: m[4],
+ author: m[5],
+ body,
+ };
+}
+
+const refsText = (refs) => `(${refs.map((r) => (r.head ? "HEAD -> " : "") + r.name).join(", ")})`;
+
+// A commit line as words, so it can be wrapped. Each word knows how to draw itself.
+function words(row) {
+ const w = [{ text: row.date, cls: `gl-date ${LANE_CLASS[row.branch] ?? ""}` }, { text: "-" }];
+ if (row.refs.length) w.push({ text: refsText(row.refs), refs: row.refs });
+ if (row.subject) for (const s of row.subject.split(" ")) w.push({ text: s, subj: true });
+ if (row.author) w.push({ text: `<${row.author}>`, cls: "gl-author" }); // kept on one line
+ return w;
+}
+
+// Greedy word wrap. Returns lines of words; a word longer than the width gets a line to itself.
+function wrap(list, width, hang) {
+ const lines = [[]];
+ let used = 0;
+ for (const w of list) {
+ const line = lines[lines.length - 1];
+ const need = (line.length ? 1 : 0) + w.text.length;
+ if (line.length && used + need > width) {
+ lines.push([w]);
+ used = hang + w.text.length;
+ } else {
+ line.push(w);
+ used += need;
+ }
+ }
+ return lines;
+}
+
+// Re-flow a description to a width, keeping "- " bullets with a hanging indent.
+function reflow(body, width) {
+ const blocks = [];
+ for (const line of body) {
+ if (line.startsWith("- ") || !blocks.length || !line.startsWith(" ")) blocks.push(line.trim());
+ else blocks[blocks.length - 1] += ` ${line.trim()}`;
+ }
+ // plain paragraphs that were hard-wrapped become one block again
+ const merged = [];
+ for (const b of blocks) {
+ if (merged.length && !b.startsWith("- ") && !merged[merged.length - 1].startsWith("- ")) merged[merged.length - 1] += ` ${b}`;
+ else merged.push(b);
+ }
+ const out = [];
+ for (const b of merged) {
+ const bullet = b.startsWith("- ");
+ const lines = wrap(b.split(" ").map((text) => ({ text })), width, bullet ? 2 : 0);
+ lines.forEach((l, i) => out.push((i && bullet ? " " : "") + l.map((w) => w.text).join(" ")));
+ }
+ return out;
+}
+
+export function initGitlog() {
+ const host = $(".gitlog");
+ if (!host) return;
+ const rows = $$(":scope > .gl-line", host).map(parse);
+ const open = new Set();
+ let lastCols = 0;
+
+ function measureCols() {
+ const probe = el("span", "", "0".repeat(20));
+ probe.style.cssText = "position:absolute;visibility:hidden;white-space:pre";
+ host.append(probe);
+ const ch = probe.getBoundingClientRect().width / 20 || 7;
+ probe.remove();
+ const cs = getComputedStyle(host);
+ const inner = host.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
+ return Math.max(24, Math.floor(inner / ch));
+ }
+
+ function render(focusRow = -1) {
+ const cols = measureCols();
+ lastCols = cols;
+ const lineLen = (r) => r.graph.length + words(r).reduce((n, w, i) => n + (i ? 1 : 0) + w.text.length, 0);
+ const wide = rows.every((r) => !r.date || lineLen(r) <= cols);
+ const pipesOnly = (r) => r && !r.date && /^[| ]+$/.test(r.graph);
+ const frag = document.createDocumentFragment();
+
+ rows.forEach((row, n) => {
+ if (!row.date) {
+ const line = el("div", "gl-line");
+ line.append(graph(row.graph, row.lanes));
+ frag.append(line);
+ return;
+ }
+
+ const isOpen = open.has(n);
+ const lines = wrap(words(row), cols - row.graph.length, HANG);
+ let subjSeen = false;
+ lines.forEach((ws, li) => {
+ const line = el("div", "gl-line");
+ line.append(li ? graph(row.cont, row.contLanes) : graph(row.graph, row.lanes, row.tip));
+ if (li) line.append(" ".repeat(HANG));
+ // consecutive subject words on a line become one button (or span) so the underline is unbroken
+ for (let k = 0; k < ws.length; k++) {
+ if (k) line.append(" ");
+ const w = ws[k];
+ if (w.subj) {
+ let text = w.text;
+ while (ws[k + 1]?.subj) text += ` ${ws[++k].text}`;
+ if (row.body) {
+ const b = el("button", "gl-subj", text);
+ b.type = "button";
+ b.dataset.row = n;
+ b.setAttribute("aria-expanded", String(isOpen));
+ if (subjSeen) b.tabIndex = -1; // one tab stop per commit, even when its subject wraps
+ line.append(b);
+ } else {
+ line.append(el("span", "gl-text", text));
+ }
+ subjSeen = true;
+ } else if (w.refs) {
+ const refs = el("span", "gl-refs");
+ refs.append("(");
+ w.refs.forEach((r, i) => {
+ if (i) refs.append(", ");
+ if (r.head) refs.append(el("span", "gl-head", "HEAD -> "));
+ refs.append(el("span", "gl-branch", r.name));
+ });
+ refs.append(")");
+ line.append(refs);
+ } else {
+ line.append(w.cls ? el("span", w.cls, w.text) : w.text);
+ }
+ }
+ frag.append(line);
+ });
+
+ if (row.body && isOpen) {
+ const refsLen = row.refs.length ? refsText(row.refs).length + 1 : 0;
+ // under the subject when there's room; otherwise a little deeper than wrapped lines
+ const indent = wide ? row.date.length + 3 + refsLen : HANG + 2;
+ const text = wide ? row.body : reflow(row.body, cols - row.cont.length - indent);
+ const tail = pipesOnly(rows[n + 1]) ? [] : [""];
+ for (const t of [...text, ...tail]) {
+ const b = el("div", "gl-line gl-body");
+ b.append(graph(row.cont, row.contLanes), t ? " ".repeat(indent) + t : "");
+ frag.append(b);
+ }
+ }
+ });
+
+ host.replaceChildren(frag);
+ host.classList.add("is-built");
+ if (focusRow >= 0) host.querySelector(`.gl-subj[data-row="${focusRow}"]`)?.focus({ preventScroll: true });
+ }
+
+ host.addEventListener("click", (e) => {
+ const b = e.target.closest(".gl-subj");
+ if (!b) return;
+ const n = +b.dataset.row;
+ if (open.has(n)) open.delete(n);
+ else open.add(n);
+ render(n);
+ });
+
+ render();
+ new ResizeObserver(() => {
+ if (measureCols() !== lastCols) render();
+ }).observe(host);
+}
diff --git a/js/main.js b/js/main.js
@@ -2,6 +2,7 @@ import { fontsReady } from "./util.js";
import "./shell.js";
import { initComputer } from "./computer.js";
import { initDatasheet } from "./datasheet.js";
+import { initGitlog } from "./gitlog.js";
import { initMemmap } from "./memmap.js";
import { initOrg } from "./org.js";
import { initUart } from "./uart.js";
@@ -14,6 +15,7 @@ function safely(fn) {
safely(initComputer);
safely(initDatasheet);
+safely(initGitlog);
safely(initOrg);
safely(initUart);
safely(initCtf);
diff --git a/js/org.js b/js/org.js
@@ -1,20 +1,34 @@
-// Renders the posts list as an Org buffer: years as top-level headings, posts
-// under them with tags and a property drawer. TAB folds the heading at point,
-// S-TAB cycles the whole buffer through OVERVIEW, CONTENTS and SHOW ALL.
+// Renders the posts section as an Org buffer: keywords, a paragraph, years as
+// "*" headings and posts as "**" headings with tags and a property drawer.
+// TAB folds the heading at point; S-TAB cycles OVERVIEW, CONTENTS and SHOW ALL.
import { $, $$ } from "./util.js";
import { message } from "./shell.js";
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+// One colour per tag, like org-tag-faces. Add an entry when you add a tag.
+const TAG_COLORS = {
+ emacs: "var(--magenta)",
+ org: "var(--green)",
+ latex: "var(--cyan)",
+ python: "var(--yellow)",
+ maths: "var(--blue)",
+ linux: "var(--orange)",
+ security: "var(--red)",
+ c: "var(--violet)",
+ web: "var(--fg)",
+};
+
export function initOrg() {
const sec = $("#posts");
const src = sec && $(".posts", sec);
if (!src) return;
+ const intro = $(".org-p", sec);
const posts = $$("li", src).map((li) => {
const a = $("a", li);
- return { date: li.dataset.date, tags: (li.dataset.tags || "").split(/\s+/).filter(Boolean), cat: li.dataset.cat, title: a.textContent.trim(), href: a.href };
+ return { date: li.dataset.date, tags: (li.dataset.tags || "").split(/\s+/).filter(Boolean), title: a.textContent.trim(), href: a.href };
});
const years = [...new Set(posts.map((p) => p.date.slice(0, 4)))].sort().reverse();
@@ -26,49 +40,70 @@ export function initOrg() {
};
const org = el("div", "org");
- const kw = (k, v, cls = "v") => {
- const p = el("p", "org-kw", `#+${k}: `);
- p.append(el("span", cls, v));
+ const kw = (tag, key, value, cls) => {
+ const p = el(tag, "org-kw");
+ const k = el("span", "org-key", `#+${key}: `);
+ if (tag === "h2") k.setAttribute("aria-hidden", "true");
+ p.append(k, el("span", cls, value));
return p;
};
- org.append(kw("title", "Posts", "title"), kw("author", "Michael Neuper"), kw("startup", "content"));
+ org.append(kw("h2", "title", "Posts", "org-title"), kw("p", "author", "Michael Neuper", "v"), kw("p", "startup", "content", "v"));
+ if (intro) org.append(intro);
const tree = el("ul", "org-tree");
tree.setAttribute("role", "tree");
tree.setAttribute("aria-label", "Posts by year");
const headings = [];
+ function heading(li, level) {
+ const h = el("div", "org-h");
+ h.tabIndex = headings.length === 0 ? 0 : -1;
+ const stars = el("button", "org-stars", "*".repeat(level));
+ stars.type = "button";
+ stars.tabIndex = -1;
+ stars.setAttribute("aria-label", "Fold or unfold");
+ stars.addEventListener("click", () => { li._fold(!li._open()); h.focus({ preventScroll: true }); });
+ h.append(stars);
+ li.prepend(h);
+ headings.push(h);
+ return h;
+ }
+
for (const y of years) {
const items = posts.filter((p) => p.date.startsWith(y));
const li = el("li", "org-h1");
li.setAttribute("role", "treeitem");
const h = heading(li, 1);
- h.append(el("span", "", y), el("span", "count", `[${items.length}]`));
- const ell = el("span", "org-ellipsis", "…");
- ell.hidden = true;
+ h.append(el("span", "org-text", y), el("span", "count", `[${items.length}]`));
+ const ell = el("span", "org-ellipsis", "...");
h.append(ell);
+
const kids = el("ul", "org-tree");
kids.setAttribute("role", "group");
- kids.style.margin = "0";
for (const p of items) {
const pl = el("li", "org-h2");
pl.setAttribute("role", "treeitem");
- if (p.cat) pl.dataset.cat = p.cat;
const ph = heading(pl, 2);
- const a = el("a", "", p.title);
+ const a = el("a", "org-text", p.title);
a.href = p.href;
a.tabIndex = -1;
ph.append(a);
- const tags = el("span", "org-tags");
- tags.append(":");
- for (const t of p.tags) tags.append(el("span", "t", t), ":");
- ph.append(tags);
- const pell = el("span", "org-ellipsis", "…");
+ if (p.tags.length) {
+ const tags = el("span", "org-tags");
+ tags.append(":");
+ for (const t of p.tags) {
+ const tg = el("span", "t", t);
+ tg.style.setProperty("--tag", TAG_COLORS[t] ?? "var(--dim)");
+ tags.append(tg, ":");
+ }
+ ph.append(tags);
+ }
+ const pell = el("span", "org-ellipsis", "...");
ph.append(pell);
+
const d = new Date(`${p.date}T12:00:00Z`);
const drawer = el("div", "org-drawer");
drawer.innerHTML = `<span class="k">:PROPERTIES:</span>\n<span class="k">:DATE:</span> <span class="ts"><${p.date} ${DAYS[d.getUTCDay()]}></span>\n<span class="k">:URL:</span> <a href="${p.href}" tabindex="-1">${p.href.replace(/^https?:\/\//, "")}</a>\n<span class="k">:END:</span>`;
- drawer.hidden = true;
pl.append(drawer);
pl._fold = (open) => {
drawer.hidden = !open;
@@ -95,24 +130,6 @@ export function initOrg() {
const meta = $('[data-org="meta"]', sec);
if (meta) meta.textContent = `${posts.length} posts`;
- function heading(li, level) {
- const h = el("div", "org-h");
- h.tabIndex = headings.length === 0 ? 0 : -1;
- const bullet = el("button", "org-bullet", level === 1 ? "◉" : "○");
- bullet.type = "button";
- bullet.tabIndex = -1;
- bullet.setAttribute("aria-label", "Fold or unfold");
- bullet.addEventListener("click", () => { cycle(li); h.focus({ preventScroll: true }); });
- h.append(bullet);
- li.prepend(h);
- headings.push(h);
- return h;
- }
-
- function cycle(li) {
- li._fold(!li._open());
- }
-
// Global visibility cycling, like S-TAB in Org.
const STATES = ["OVERVIEW", "CONTENTS", "SHOW ALL"];
let global = 1;
@@ -139,13 +156,13 @@ export function initOrg() {
t.focus();
};
if (e.key === "Tab" && e.shiftKey) { e.preventDefault(); globalCycle(); }
- else if (e.key === "Tab") { e.preventDefault(); cycle(li); }
+ else if (e.key === "Tab") { e.preventDefault(); li._fold(!li._open()); }
else if (e.key === "ArrowDown" || e.key === "j") { e.preventDefault(); move(i + 1); }
else if (e.key === "ArrowUp" || e.key === "k") { e.preventDefault(); move(i - 1); }
else if (e.key === "Enter") {
const a = h.querySelector("a");
if (a) a.click();
- else cycle(li);
+ else li._fold(!li._open());
}
});
}
diff --git a/js/shell.js b/js/shell.js
@@ -363,18 +363,18 @@ help.addEventListener("keydown", (e) => { if (e.key === "q") help.close(); });
const KEYMAP = [
{ key: "SPC", desc: "find file", cmd: "find-file" },
{ key: ":", desc: "M-x", cmd: "execute-extended-command" },
- { key: "a", desc: "about", cmd: "goto-about" },
- { key: "p", desc: "projects", cmd: "goto-projects" },
- { key: "n", desc: "posts", cmd: "goto-posts" },
- { key: "m", desc: "mail me", cmd: "goto-contact" },
- { key: "r", desc: "refill coffee", cmd: "coffee-refill" },
- { key: "b", desc: "buy me a coffee", cmd: "buy-me-a-coffee" },
{ key: "c", desc: "+computer", map: [
{ key: "c", desc: "run or pause", cmd: "computer-run" },
{ key: "n", desc: "step one cycle", cmd: "computer-step" },
{ key: "r", desc: "reset", cmd: "computer-reset" },
{ key: "l", desc: "load program", cmd: "computer-load-program" },
] },
+ { key: "a", desc: "about", cmd: "goto-about" },
+ { key: "p", desc: "projects", cmd: "goto-projects" },
+ { key: "n", desc: "posts", cmd: "goto-posts" },
+ { key: "m", desc: "mail me", cmd: "goto-contact" },
+ { key: "r", desc: "refill coffee", cmd: "coffee-refill" },
+ { key: "b", desc: "buy me a coffee", cmd: "buy-me-a-coffee" },
{ key: "h", desc: "+help", map: [
{ key: "t", desc: "load-theme", cmd: "load-theme" },
{ key: "h", desc: "keys and commands", cmd: "help" },