python -m http.server 8000Then open http://localhost:8000/#home. There is no build step: GitHub Pages serves these files verbatim, so what you see locally is what ships.
Useful URLs while developing:
| URL | What it does |
|---|---|
/#home |
Normal |
/?wm=selftest#home |
Runs the pure layout assertions and logs pass/fail to the console |
/#home with JavaScript disabled |
The stacked fallback used when the window manager cannot run |
npm ci # once: Playwright and the generators (development tooling only)
npm test # node: tree maths, contract, parity, site data, theme, static security
npm run test:browser # playwright; PW_CHANNEL=msedge to run in Edge instead of bundled Chromium| Suite (repository root) | Covers |
|---|---|
test/wm-tree.test.js |
The pure tree and rect maths — every direction × layout combination, seam-free tiling, focus bookkeeping, validate reconciliation |
test/wm-extras.test.js |
Container focus, marks, swaps and the floating setters |
test/wm-commands.test.js |
The i3-msg parser, dispatch, and directional focus |
test/wm-contract.test.js |
Structure and accessibility invariants asserted against the source, byte budgets, cache tokens |
test/shared-constants.test.js |
Copies that cannot be imports held to their originals: 404.html's routes, the service's and j3w1ctl's collections, the renderers' link schemes, both packages' Playwright |
test/site.test.js |
content/site.json: validation, escaping, derived counts, no copies of the identity in the scripts |
test/theme.test.js |
The theme pin, the generated block, and that no colour literal survives outside it |
test/security-static.test.js |
Credentials, the j3w1ctl cache key and mutation locks |
test/public-photo-viewer.test.js |
The photo viewer's route-neutral close |
test/browser/wm.spec.js |
The real thing in a browser: tiling geometry, keys, tabs, drag, shell, boot, mobile |
test/browser/prerender.spec.js |
The two content renderers byte for byte, the entry pages against the desktop, the 404 rescue |
test/browser-fixture-server.mjs serves the repository with a synthetic content index and a fake
auth service (npm --prefix services/j3w1ctl-auth run test:browser-fixture starts it on its own);
the backend's own Playwright spec imports the same fixture, and both configs share
playwright.shared.mjs. The backend suite stays under services/j3w1ctl-auth/.
.github/workflows/ci.yml runs all of it on every push.
The tree and layout modules are pure and DOM-free specifically so they can be tested in node without
a browser or a DOM shim — tree-extras.js and commands.js too. Keep them that way; session.js
creates its media queries lazily for the same reason.
npm run generate writes every committed artifact that is derived from something else, and
npm run check (run by CI) fails if any is stale:
| Artifact | Derived from | Generator |
|---|---|---|
the @generated-site blocks in index.html (head, JSON-LD, the home terminal and folders, projects, elsewhere, about) and in wiki/index.html and 404.html (shared head, page bar and footer) |
content/site.json, and the page shell in services/j3w1ctl-auth/src/site-pages.js |
scripts/lib/site.mjs |
assets/fonts/*.woff2, the @generated-fonts block in site.css, the font preload tokens |
the pinned Nerd Fonts and Noto CJK sources, and every icon and Han character found in the sources | scripts/lib/fonts.mjs |
the 37 legacy aliases and the canonical pass-through tokens in the @generated-theme block in site.css |
vendor/j3w1-theme/exports/tokens.css, verified by theme.lock.json |
scripts/lib/theme.mjs |
the modulepreload list in index.html |
the static import graph from site.js and public-content.js |
scripts/lib/preloads.mjs |
assets/data/content-index.json, writing/**, photography/**, books/**, sitemap.xml, feed.xml |
content/** |
services/j3w1ctl-auth/src/generate.js |
Adding an icon, a Chinese label, a static import or a project is therefore a two-step change: edit
the source, then npm run generate and commit what it wrote. Editing inside a generated block by
hand fails npm run check with the block's name; the site generator also fails when the page
generator's constants, site.webmanifest or the theme's canvas disagree with content/site.json.
The site blocks are written first so the font subsetter sees their glyphs. npm run icons re-renders the raster icons and
the social card (committed, checked by shape rather than byte).
The site consumes a release of j3w1/theme; it does not follow that
repository's branch. The explicit updater is the only theme-integration command that uses the
network:
npm run update-theme -- v1.1.0 # tag or full 40-character commit; never a branch
npm run generate
npm run bump-cache-tokenThe updater resolves a tag to its commit, reads every upstream input at that immutable revision,
checks the published digest, and only then rewrites theme.lock.json and the vendored CSS. A failed
fetch or validation leaves the existing pin alone. npm run generate and npm run check read the
committed copy, verify its digest again, and never fetch theme data. Review and commit the lock,
vendored export, generated block, and shared cache-token changes together.
test/wm-contract.test.js caps the sizes of the core modules, the whole assets/js/wm directory,
the font faces, and — the one that governs first paint — the eager graph: every module a chain
of static imports reaches from boot.js. The greeter, the power sequences, touch, the self test and
every application except the shell are loaded on demand and do not count against it. Raise a cap
deliberately, in the same commit as the feature that needs it, with the reason in the test.
Add an entry to the bindings array in wm/keys.js:
{ keys: "g", description: "do the thing", test: (event) => event.key === "g", run: () => wm.doTheThing() }The help dialog is generated from wm.bindings(); update the tables in docs/wm-usage.md and
wiki/index.html by hand (the contract test checks the keys they document exist). Add a matching
command in wm/commands.js — a handler in HANDLERS and an entry in commandList — so the action
is reachable without a keyboard and from i3-msg in the terminal; see the gesture-parity rule in
wm-accessibility.md. Binding modes are tables in keys.js with their i3
prompts in the table beside them.
Before choosing a key: bare letters are the primary scheme, Alt+letter is an accepted alias.
Alt+arrow is browser history and must stay unbound. Bare Space must stay unbound or space-to-
scroll breaks inside terminal buffers and readers.
- Write
wm/apps/<name>.jsexporting a factory that takes{ body, statusline, title, wm, close }and returns{ destroy(), focus?() }. - Register it in
wm/apps/index.jswith a title, a window class, and status line labels.
It is then automatically launchable from the launcher (exec <name>) and the shell.
Two rules for application output:
- Write text with
textContent, neverinnerHTML. Markdown bodies go through the existing restricted-AST renderer (renderAstfromcontent-renderer.js), which is the only sanctioned path from content to DOM. - Never invent a value. If the browser does not expose something, print
unknownor omit the row. See §7 of wm-architecture.md.
Authored windows are declared in index.html and listed in wm/defaults.js. Both must agree —
wm-contract.test.js fails otherwise. A window needs:
<article class="window pane <app-class>"
data-wm-window="<id>" data-wm-title="<tab label>"
tabindex="0" aria-label="<accessible name>">Never create, move, clone, or remove an authored window at runtime. See §1 of wm-architecture.md for why.
Sizes that vary by viewport live in CSS custom properties (--gap, --wm-tab-height,
--wm-stack-row) and are read by the renderer, so the stylesheet stays the single source of truth.
Do not hard-code a pixel size in layout.js that a breakpoint needs to change.
Every asset of the public shell (site.css, desktop.css, and every script under assets/js/,
dynamic imports included) shares one ?v= token, bumped as a unit:
npm run bump-cache-token # today's date, or the next letter suffix if already today
npm run bump-cache-token -- --check # list every token in use; fails on a mixed shared setThe script rewrites index.html, wiki/index.html, admin/index.html, 404.html,
services/j3w1ctl-auth/src/site-pages.js and assets/js/**. Run npm run generate after it (the
entry pages carry the page generator's token), and redeploy the j3w1ctl service before the next
browser publish. It never touches content-renderer.js, photo-viewer.js, or admin/* targets —
those are versioned independently and only when they change — nor font URLs, which
npm run generate content-hashes. scripts/lib/cache-tokens.mjs is the single definition of the
shared set, and the contract test uses the same definition.
The public wiki is wiki/index.html, served at https://j3w1.github.io/wiki/. It is a hand-
authored page; its head, bar and footer are the generated page shell it shares with 404.html and
the entry pages, and its styles are the body.page rules in site.css.
It is deliberately part of the site rather than a GitHub wiki: GitHub only creates a repository's wiki git remote after the first page is made in the web UI and there is no API for that first page, so a GitHub wiki could never be created or kept current from here.
The site points at it from six places, all asserted by wm-contract.test.js:
- the file manager's Places → Wiki entry (the i3bar tray is for status, not destinations),
- the
?help dialog, - the
6:elsewherelink list, - the empty-workspace hint,
- the launcher (
open wiki) and the shell (wiki).
When you change a keybinding, update wiki/index.html too — the contract test checks that the keys
it documents still exist in keys.js, but it cannot check that new ones were added.
If you ever enable one, create the first page at https://github.com/j3w1/j3w1.github.io/wiki, then:
git clone https://github.com/j3w1/j3w1.github.io.wiki.git ../j3w1.wiki
Copy-Item docs/wm-usage.md ../j3w1.wiki/Home.md
Copy-Item docs/wm-architecture.md ../j3w1.wiki/Architecture.md
Copy-Item docs/wm-accessibility.md ../j3w1.wiki/Accessibility.md
Copy-Item docs/wm-development.md ../j3w1.wiki/Development.md
cd ../j3w1.wiki
git add -A && git commit -m "docs: publish window manager documentation" && git push?wm=selftest— layout assertions in the console.localStorage.removeItem("j3w1.wm.layout")— forget the saved layout.Shift+R— restart in place.- If the desktop does not appear at all, check the console:
createWmis wrapped intry/catch, and the inline head script setsdata-wm="off"on any module load error or after an 8 s deadline, so a boot error renders the site as the static fallback. That is intentional, but it does mean a boot error looks like "the window manager just didn't load".
- The browser fixture serves its own content index. Photography slugs in browser tests are
fixture-photographs, not the published ones. - Flex trims leading whitespace in flex items. The shell prompt wraps its spans in a
.shell-promptelement for exactly this reason. [hidden]isdisplay: none !importantinsite.css. You cannot override it with adisplayrule; do not sethiddenon something you intend to show with CSS..no-jsandhtml:not(.wm-active)are different things. The first means "no JavaScript"; the second also covers a failed boot, and is the one most fallback rules should use. The stacked document pairs.no-jswithhtml[data-wm="off"]explicitly, insite.css.- A re-export must be two statements. The preload list and the eager-graph budget follow lines
that start with
import, soexport { X } from "…"escapes both; writeimport { X } from "…"; export { X };.
- A shallow clone makes every file look newly added. The cache-token ratchets compare a file's
last commit date against its
?v=token, so they need real history:actions/checkoutdefaults to depth 1 and grafts its one commit as a root commit, so every file looks added today and the ratchet demands a token dated now. Both CI jobs check out withfetch-depth: 0; anywhere else the ratchets skip themselves ongit rev-parse --is-shallow-repositoryrather than assert against a fabricated date. - Text measures differently on every platform. Do not tune a breakpoint to the last pixel of one machine's font metrics: no other host is obliged to reproduce them, and a layout left with a pixel of slack overflows where the glyphs are a hair wider. Give geometry headroom and assert the headroom rather than a one-pixel tolerance — the same reason sizes belong in the stylesheet under Changing geometry above.
- Never assert a value the host supplies.
navigator.hardwareConcurrency, and anything else the machine rather than the fixture decides, differs between here and a runner. Assert the shape of the reading —/^\d+ thr$/— unless the fixture controls the value. - Each package context needs the browser build its own resolved Playwright version expects. The
repository root and
services/j3w1ctl-authcarry independent lockfiles, so a dependency update can move one without the other. Installing Chromium through only one package leaves the other asking for an executable a clean runner never downloaded; a development machine hides it because earlier runs populated the Playwright cache.test/shared-constants.test.jsenforces that both lockfiles resolve the same build, so CI installs Chromium once. Share settings between the two configs throughplaywright.shared.mjs, never by importing one config from the other: that loads a second Playwright from the other package, which Playwright refuses. - Geometry lands on a
requestAnimationFrame. A measurement taken straight after the input that causes a relayout can still read the layout that was there before it. Wait for the rendering boundary the assertion actually depends on —awaitFullscreenwaits for the window to fill its layer before measuring.workers: 1andretries: 0make such a race a hard first-try failure, and a fast machine wins the race every time.