Skip to content

Latest commit

 

History

History
259 lines (204 loc) · 14.9 KB

wm-development.md

File metadata and controls

259 lines (204 loc) · 14.9 KB

Working on the window manager

Run the site

python -m http.server 8000

Then 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

Run the tests

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.

Generated files

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).

Updating the theme pin

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-token

The 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.

Byte budgets

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.

Adding a keybinding

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.

Adding an application

  1. Write wm/apps/<name>.js exporting a factory that takes { body, statusline, title, wm, close } and returns { destroy(), focus?() }.
  2. Register it in wm/apps/index.js with 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, never innerHTML. Markdown bodies go through the existing restricted-AST renderer (renderAst from content-renderer.js), which is the only sanctioned path from content to DOM.
  • Never invent a value. If the browser does not expose something, print unknown or omit the row. See §7 of wm-architecture.md.

Adding or moving a window

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.

Changing geometry

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.

Cache busting

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 set

The 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 wiki

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:elsewhere link 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.

Optional: mirroring to a GitHub wiki

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

Debugging

  • ?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: createWm is wrapped in try/catch, and the inline head script sets data-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".

Things that will bite you

  • 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-prompt element for exactly this reason.
  • [hidden] is display: none !important in site.css. You cannot override it with a display rule; do not set hidden on something you intend to show with CSS.
  • .no-js and html: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-js with html[data-wm="off"] explicitly, in site.css.
  • A re-export must be two statements. The preload list and the eager-graph budget follow lines that start with import, so export { X } from "…" escapes both; write import { X } from "…"; export { X };.

On the CI runner

  • 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/checkout defaults 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 with fetch-depth: 0; anywhere else the ratchets skip themselves on git rev-parse --is-shallow-repository rather 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-auth carry 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.js enforces that both lockfiles resolve the same build, so CI installs Chromium once. Share settings between the two configs through playwright.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 — awaitFullscreen waits for the window to fill its layer before measuring. workers: 1 and retries: 0 make such a race a hard first-try failure, and a fast machine wins the race every time.