@ 261101.7

261026.49 · 14d2659ed0efc7a2eead879efc66cfde50fdd1c5

README holds what www is, usage, install and a list of docs

The README had grown to 5,700 words. A Notes section of 3,000 words held paragraphs of over 700, and the opening line did not say who www is for. It now says what www reads and for whom, then gives the usage with a few examples, install and one line per doc: 541 words.

The rest moved unchanged into topic files in doc/: options, servers and locks, pages, code pages, search and controls, safety, speed, install and development. A check found every moved sentence in a doc file. The one README sentence doc/history.md lacked, on nested repos, joined it.

TestPageBudget read the README as its large doc. It now joins the README and every doc file, 56 KB against 33 KB before, which renders in 2.4 ms.

reading declarations

README.md

@@ -2,6 +2,8 @@  www reads a directory in the browser: Markdown as pages, code as highlighted source, a repo with its history. It runs on your machine and needs no build step. +It is for anyone who keeps notes, docs and code in plain files and wants to read them as linked pages, without pushing them anywhere first. A repo on GitHub, GitLab or Codeberg reads the same way once www has fetched it.+ It is fast. A page from a running server takes one to three milliseconds, a server starts in about 40 ms, and a test in `make test` fails any page that takes over five.  ## Usage@@ -20,139 +22,38 @@ www -h | --help www -V | --version ``` -Run `www` in a directory. It prints the tree's URL, opens the browser and writes a lockfile named after the endpoint, such as `127.31.1.1-8080.lock`.-`cat` the lockfile to see the URL, root, PID and start time.-Delete the lockfile to stop serving the tree. The Delete lock button on the tree's pages does the same.-The URL is all it prints, so `url=$(www --no-open)` works. Running `www` again in a served directory prints the same URL and starts nothing.--Trees on one address share one server. The first `www` starts it in the background, and later ones hand their tree to it and return.-When something other than your www on that address holds the port, `www` fails at once with `E98` and names the listeners there.-Delete lock and Kill server answer 410 Gone, with a page that names each tree stopped, its lock removed or kept, and whether the server serves on.-The server exits when its last lock is gone. SIGTERM stops it at once and removes every lock but a persistent `.LOCK`, as does the Kill server button on the index at `/`.-A running server keeps the binary it started with. `www --restart` moves every server's trees to the current binary, and `make install` and `make install-deb` run it for you; open tabs reload onto the new binary, and it says how many did. It moves only the servers its own binary started, so the package and a build from `make install` leave each other's servers alone; it names each server it leaves, and `-f` takes them too. `make install-deb` passes `-f`, since it removes the `make install` link. A binary without the capability for port 80, such as the one from `make install`, leaves a port-80 server on its old binary and says so.--The index at `/` reads the same on every server. It lists each running server in the order of their addresses: the address and the binary it runs. Each server's trees follow, with the lock's path on hover. With `--debug`, each server also says when its binary was built, from which commit and when the server started, and each tree when its lock was written; these serve debugging, so they are hidden otherwise. The server you are reading from says "this page", and only its trees carry a Delete lock button, since a server takes a stop only from its own pages; another server's address links to its own index. The package's binary and the one `make install` links are listed under No server when they run none, so both installs always show. A warning appears under a server whose binary changed on disk after it started, with the command that moves it to the new one.-To reach the server by name, add a line such as `127.31.1.1 www` to `/etc/hosts`. Type the name with its slash, eg `www/`, or the browser searches for it.--- `-a ADDR`, `--addr=ADDR`: serve on another loopback address, an IP or IP:PORT; the port defaults to 80 where www may bind it, else 8080-- `--share[=HOST]`: share the tree on the default route's address, or on the address or name given beyond loopback, with an optional port, such as `192.168.1.5`, `0.0.0.0` or `mate.peer.home`; see [sharing](doc/sharing.md)-- `--secret`: serve a shared tree under the token in `WWW_SECRET`, or a random one, so only those holding the link reach it-- `--foreground`: when no server runs on the address yet, serve in the foreground until Ctrl-C-- `--no-open`: print the URL without opening a browser-- `--no-bug`: hide the bug button at the foot of each page and refuse reports; `--restart` passes it to the new servers when given-- `-p`, `--persist`: write a persistent lock, such as `127.31.1.1-8080.LOCK`, so the tree is served again after a reboot; see [Serving after reboot](#serving-after-reboot)-- `--debug`: show what serves debugging: above each page's header, the build stamp, see [debugging](doc/debugging.md); on the index, each server's build and start time and each lock's time. `make run` sets it; `--restart` passes it to the new servers when given, and leaves it off otherwise-- `--cache=SIZE`: memory a new server may spend on cached git answers, eg `64M`; `0` caches nothing. The default is `32M`, and `--restart` goes back to it-- `--disk=SIZE`: disk a new server may fill with online repos, eg `4G`; it drops the repos read longest ago, but not one that another server is fetching, since every server shares the cache. The default is `2G`, and `--restart` goes back to it-- `--restart`: replace every server this binary started with one running it now, serving the same trees, and print each tree's URL; an address with persistent trees and no server gets one, when this binary served it last; with `-a` or `--share`, only the server on that address, whichever binary runs it; with a directory, it also serves that directory there and opens it in the browser unless a page of it was open, and says which-- `-f`, `--force`: with `--restart`, take the servers and persistent trees of other binaries too-- `-l`, `--list`: print each served tree's directory, URL and binary as keyval, on every address; with `-a` or `--share`, on that address only; with a directory, only the trees at or below it. A directory that holds no served tree prints nothing; one that does not exist fails with `E2`-- `--kill`: stop every server and print the trees it served, as `--list` does; with `-a` or `--share`, only the server on that address-- `--commit=REV`: print as keyval the commit REV names in the repo of DIR, or of the current directory: its full hash, its stardate and its subject. REV is a stardate, eg `261024.4`, a hash or any name git takes, so `git show $(www --commit=261024.4 | sed -n 's/^hash //p')` shows a commit seen on a page. A day and x, eg `261026.x`, works while one commit alone holds it. A commit or repo that is not there fails with `E2`-- `-V`, `--version`: print when the binary was built and the commit it came from, as the stamp says it, eg `www built 2026-10-21 12:49, from 1aa282c+modified`-- `-h`, `--help`: print the options--Options follow GNU style. Short ones combine, as in `-pa 127.31.1.2`, and options may follow the directory. Optional values go joined, as in `--share=192.168.1.5`; with a space the address would be read as the directory, and www says so. `-f` and `-n` mean force and dry run in every tool of the author's, so `--foreground` and `--no-open` have no short form. The help text is written in docopt notation, except that docopt has no way to write the optional `=HOST`. `make test` checks it with docopt-ng, run through `uv`, and skips the check where `uv` is missing.--`--list` and `--kill` print [keyval](https://unixfile.github.io/keyval/): three lines per tree, sorted, with every index the same width. The binary line is missing for a server that started before www recorded it.+Run `www` in a directory. It prints the tree's URL, opens the browser and writes a lockfile named after the endpoint, such as `127.31.1.1-8080.lock`. Delete the lockfile to stop serving the tree. Trees on one address share one server, which exits when its last lock is gone.  ```-tree.0.binary /usr/bin/www-tree.0.dir /home/t/keep/notes-tree.0.url http://127.31.1.1/keep/notes/-tree.1.binary /home/t/scratch/github.com/unixfile/www/build/www-tree.1.dir /home/t/scratch/github.com/unixfile/www-tree.1.url http://127.31.1.1:8080/scratch/github.com/unixfile/www/+www                         serve this directory and open it+www -p ~/notes              serve ~/notes, and again after a reboot+www --share=192.168.1.5     serve this directory to the LAN+www -l                      list every served tree as keyval+www --restart               move every server to the current binary ``` -Two variables set the default addresses. `--help` shows each value and where it comes from, and an error from a bad value names its variable.--- `WWW_SHARE`: the address or name for a bare `--share`, eg `mate.peer.home`, in a start and in `--kill`, `--restart` and `--list`. Without it a bare `--share` takes the default route's address-- `WWW_ADDR`: the loopback address for a start without `-a`. `--list`, `--kill` and `--restart` without an address still cover every server--A third sets how a start opens its URL. `WWW_OPEN` holds the command, split at spaces, with `%s` where the URL goes, or the URL appended when no `%s` is given. `WWW_OPEN='brave-browser --app=%s'` opens a tree in an app window and `WWW_OPEN=firefox` in Firefox. It applies to loopback starts only; a share opens through `xdg-open`, as every start does without the variable.--A fourth holds a share's token. `WWW_SECRET` gives `--secret` a token of letters, digits, `-` and `_` instead of a random one. The token never goes on the command line, where `ps` shows it to every local user and the shell keeps it in its history, so `--secret=token` fails with `E22`. `--help` says whether the variable is set but never shows its value.--## Serving after reboot--`www -p` writes a lock in capitals, such as `127.31.1.1-8080.LOCK`. It holds no PID, and it outlives its server: Kill server, `--kill`, SIGTERM and a reboot all stop the server but leave the `.LOCK` in place. The next server on that address serves the tree again, and so does `www --restart`, which starts a server for every address whose persistent trees have none. Deleting the `.LOCK`, or Delete lock on the tree's pages, ends the tree for good. `www -p` in a tree served by a plain `.lock` moves it to a `.LOCK`, and a plain `www` in a tree that holds a `.LOCK` keeps it.--A systemd user unit runs `www --restart` at login. The package puts `www.service` in `/usr/lib/systemd/user/`; enable it with `systemctl --user enable www`. `make install` puts `www-dev.service` in `~/.config/systemd/user/`; enable it with `systemctl --user enable www-dev`. Both ship disabled. They serve from login to logout, and linger stays off. Each brings back only the trees its own binary served last, so with both installs both units run.-The package also ships a system template, `www@.service`, which serves a user's trees from boot, before login, with the capability for port 80. Enable it with `sudo systemctl enable www@<user>`. While the template or the package's user unit runs for a user, the other stands aside, since both run one binary on the same trees. `www-dev` runs beside either.--## Debugging--A faint `www bug` button at the foot of every page files a numbered bug in www itself, not in the tree you are reading. The report holds a snapshot of what you saw, the server's state and an optional comment, in `csi/bugs/` of the checkout a `make build` binary came from, else in `~/.local/state/www/bugs/`, and in the log. `--debug` adds a build stamp above each page. See [doc/debugging.md](doc/debugging.md).--## Online repos--The server on your default address also serves 127.31.N.2, where a URL names a repo on a forge, eg `http://127.31.1.2/github.com/junegunn/fzf/`. The first visit fetches it into `~/.cache/www/` while a page shows each step, and www then serves it as it serves a checkout. A path that names no repo, eg `/github.com/junegunn`, lists the repos there. Two ways lead there from any page: paste a repo's URL into the search box, eg `github.com/junegunn/fzf`, and Enter opens it; and a doc's link to a repo on GitHub, GitLab, Codeberg or Bitbucket, or to a file in one, carries a small www mark that opens it there. See [online repos](doc/online-repos.md).--## Sharing--`www --share` serves the tree to other machines on the LAN from a server of its own. It serves only what other users could read, hides dot entries such as `.git` and shows nothing of the machine around the tree. `--secret` puts the tree under a random token. See [doc/sharing.md](doc/sharing.md).--## History--Add `?log` to a path for its git history, newest first: one line per commit, which opens to the full message and the diff. A tracked directory's listing opens it from its first entry, `<(git log)`, and a tracked doc or code page from a line at its top. The line says current, or changed when the page differs from its last commit. Hover names that commit by stardate and subject, eg 261024.4, the fourth commit that day on the main line, or 261026.x while it waits for a push, with hash and time. On an old version the line names its commit instead, and at its right end commit opens that commit's page and current leads back to the page as it is. A click on that line opens the timeline, a menu of every version of the page. A commit page shows its stardate and full hash as plain text, to select and give to git, then its diff for that path, with a link to all its files, and then its semantic diff: the declarations it changed, renamed or moved, each opening its own diff, and the changed tokens marked within the diff. Rows in listings, search results, the timeline and histories open wherever on the row you click. browse inside a commit's fold opens the page as it was then: `@` and a commit's hash on the tree's name, eg `www@5430ac3/README.md`, serve the whole tree as of that commit, docs, code and listings alike, and links within it stay in that version. Shares show it without trailers, and only when other users could read the repo. A page whose history lies in a nested repo, or behind a link, lists its commits but no versions, since the tree's versions do not hold it. See [doc/history.md](doc/history.md).--## Log--www logs every run, server event and page request to the systemd journal: `journalctl --user -t www`. Without a journal it writes `~/.local/state/www/log`. See [doc/log.md](doc/log.md).+Add `?log` to any page's URL for its history. Open `http://127.31.1.2/github.com/junegunn/fzf/` to read a repo from GitHub.  ## Install -www needs git.-`make install` links `build/www` into `~/.local/bin`.-`make deb` builds a Debian package in `build/`, such as `www_0~git20261015223930.20f02fd_amd64.deb`, versioned by the last commit's UTC time and hash. It depends on git. For another architecture, pass `ARCH`, eg `make deb ARCH=arm64`.-`make install-deb` builds the package while sudo asks for the password, then installs it with `sudo apt install`, which pulls in git. Debian's needrestart lists the services that use old libraries after the install instead of asking which to restart; none of them is www. It first removes the `~/.local/bin` link from `make install`, which would otherwise shadow the package's binary. Run it again to replace an installed build, even of the same commit. `make uninstall-deb` removes the package.--To try a change without installing it, `make run` builds `build/www` and the syntax tree helper and serves this directory on port 8080 with `--debug`, beside an installed server on port 80. A later run replaces the server on 8080 with the new build, serving the same trees, and open pages go on under it, so edit and `make run` again. It opens the browser only when no page of this directory was open on the old server, and says which happened. A hidden tab holds no connection, so it does not count and gets a second tab. `www --kill -a 127.31.N.1:8080` stops it.--`make bench` times what a reader waits for: a search across a generated tree of 4,000 files and 24 MB, and a code page's first view per language: lexing alone, the whole page, and the page with scope boxes. `BENCH` picks benchmarks by pattern and `COUNT` repeats them, eg `make bench BENCH=Search COUNT=10` for benchstat. Python, JavaScript and shell are timed on argparse.py, jquery.js and bash_completion from Debian's packages and skipped where those are not installed. Warm pages have no benchmark, since `TestPageBudget` in `make test` holds them under 5 ms.--The program's name lives in the file `WORKING_NAME`, since www is a working name until a final one is chosen. The binary, its paths, environment variables, journal tag and URLs, the Makefile and the Debian package all take it from there, and a test fails on any other copy outside prose and `go.mod`.--The package grants `/usr/bin/www` one capability, `cap_net_bind_service`, so it can bind port 80 while running as the user. `getcap /usr/bin/www` shows it. With it www serves on port 80 and URLs carry no port; without it, as after `make install`, the port is 8080. www also takes port 80 where the machine lets every user bind low ports.--## Notes--Each user owns 127.31.N.0/24 with N = uid − 1000 and serves on 127.31.N.1 by default, port 80 or 8080 as above, and online repos on 127.31.N.2. Uids from 1000 to 1255 fit; others need `--addr`. `-a` takes only loopback addresses; `--share` takes the others.-The addresses keep users out of each other's way, not out of each other's trees. Loopback is open to every local account, so on a machine shared with other users, they can reach your server. Serve nothing there you would not show them.--URLs start with the served directory's path relative to home, eg `http://127.31.1.1:8080/scratch/github.com/unixfile/webstack/index.md`, so a URL names its tree. A directory outside home uses its absolute path.-Below the prefix, URLs mirror the file tree, so relative links in the docs work unchanged. An absolute link such as `/x.md` points outside the prefix and gives 404, on a page of its own that names the trees nearest it, or every tree when none is near. A path the tree lacks gives 404 and names the whole URL; the digits turn like a coin before they stop. Below, the page says where git saw the path go: the commit that deleted or moved it, with a link to its last version, and the names close to it in the nearest directory that exists. A `.md` file renders as a page, a directory as a listing of its names, a text file as a highlighted code page with linkable lines, and anything else raw. Add `?raw` to any URL for the file itself. A tree inside another served tree answers for its own paths.-The box under the header searches what is below it as you type, ignoring case. It holds the keyboard, so typing on any page searches, wherever focus is. On a listing it searches the directory: names that hold the query first, then lines in text files, with the directory's own entries at full strength and what lies in its subdirectories after a gap, dimmed. A line result opens a code page at its line. Any other page filters itself: a code page keeps its matching lines with their numbers, a doc its matching blocks, a history its matching commits, with each match marked, and Enter jumps to the first match in the whole page. Below, after a gap and dimmed, follow the directory's results without the page's own file. The query stays in the URL as `?q=`, so Back from a result or a reload shows the results again. Hidden files are searched too, but not the stores machines write, such as `.git`, `.venv` or `.cache`, nor files over the code page limit; a search stops after half a second and says so, and a share searches only what it serves. Escape clears the query and brings the page back. Up and Down move a highlight through a listing's entries, or through a search's matches and results, and Enter opens it; a match opens at its place in the whole page. Escape on an empty query goes up, eg from a file to its directory or from a commit to its history, and highlights the page it left, so Enter goes back. Left and Right move the cursor in the box and never navigate. With nothing to walk, Up, Down and Space scroll the page. The highlight survives a reload. On a touch screen the box takes the keyboard only when tapped, since it would raise the on-screen keyboard. Shift+Tab in the box enters the header at the crumb Escape goes to, and Tab goes on from there to raw on a doc and Delete lock. Escape on a focused link or button returns to the box. Beside the box, ⧉ copies the page's text: what lies below the box, without www's own lines and links or line numbers, so a code page copies as its file and a listing as its names. A doc copies its Markdown, and lines a range marks, eg `#L3-L26`, copy alone. What a search hides stays out. Beside it, ⤴ copies a link to what is selected: selected or marked lines on a code page, pinned to the commit when the file is clean, eg `@261024.22/server.go#L10-L12`; other selected text as a link that scrolls to it and marks it, `#:~:text=`; with nothing selected, the page. The link carries the page's own address, so a share's link opens on other machines and a loopback one only here. On the web address it is the forge's own link at the commit shown, eg `https://github.com/junegunn/fzf/blob/<commit>/main.go#L10-L12`, which opens in any browser. The search row, box and buttons alike, sticks to the top of the window as the page scrolls, so all three stay at hand while reading, and a jump to a line stops below it. The box, its buttons, raw, the stop button and a code page's views fade while nothing needs them, and keep their space; the search row then lets the text scroll through it. They show while the page sits at its top, since the gaps they leave there look odd. Once the first line scrolls under the search row, they show only while the query holds a character, while the mouse moves over the column, while keyboard focus sits on one of them or a crumb, and when the page scrolls up. Once shown, they fade only 2 s after the last of these ends, never at once. The crumbs, the header's status and the bug button stay. A code page anchors lines as GitHub does, so `main.go#L42` lands on line 42 and highlights it. Clicking a line's number highlights it and puts its address in the address bar; clicking the number again clears both. A range such as `main.go#L10-L20` highlights those lines. Bare URLs in a doc become links. A relative link in a doc whose target is missing gets a dotted red underline and names the missing path on hover. A link that climbs out of the tree is not checked, since another tree may answer there.--The browser remembers the pages you open; the server keeps no such list. A search shows the best five that match its words first, under recent, from any tree on the address. Each visit counts and recent visits weigh more, as in zoxide. A reload is no visit. Each address is an origin of its own, so each visitor to a share sees only the pages that visitor opened, and online repos keep a list apart from the trees.--Below a doc, linked from names each doc in the tree that links to it, by any relative spelling. The list arrives after the page, since the server walks the tree for it: a few milliseconds in a repo, about 250 ms across all of `~/scratch`, 77,000 files. The server keeps each doc's links and reads a doc again only when its size or time changes. It reads a new tree's links in the background when the tree joins.--A server keeps git's answers so most pages run no git at all: which repo each directory lies in, the histories pages show and the commits they open, which never change and are kept by their full hash. It reads HEAD from the repo's files, and a history is kept under the commit HEAD names, so a new commit misses the cache and nothing shown goes stale. A `.git` that appears or goes makes the server ask git again. `--cache` sets the memory for it.-The cache holds only what someone has visited, with one exception. When a tree joins, the server asks git in the background for what its listing and its README show, since those open first. The log says how long that took.-In a large repo git can take a second for a history, because it reads every commit back to the ones that touched the path. When it takes over 100 ms, the history page says so and names `git commit-graph write --reachable --changed-paths`, which builds git's index of the paths each commit changed and makes such histories several times faster. www never writes to the repo itself.--Open pages reload when what they show changes. A doc takes its new version in place, without a reload, and each block that changed fills with the highlight colour and fades over three seconds, so the eye finds the change after a save. A code page reloads when its file is written, a listing when a name in it changes, any page in a repo when its HEAD moves, and the index when a tree joins or leaves. A page whose file or tree is gone waits two seconds for it to return, then reloads to say so. A page outlives its server. After three seconds without it, a line over the page says the server is gone and since when, and the page keeps trying each second. When a new server answers, eg after `make run` or `--restart`, the page takes its version from it and a line says new server, fading over three seconds. A restart takes about 60 ms, so its new server answers before the gone line shows. Each page holds a server-sent events stream while its tab is visible; a hidden tab lets go, since a browser opens at most six connections to one host, and checks again when shown. The server watches the directory of each open page and its repo's git directory through inotify, since editors save by renaming a new file over the old one, and polls every second where inotify fails.--Docs follow CommonMark with GitHub's extensions, rendered by goldmark: tables, strikethrough, task lists, bare URLs as links, footnotes and definition lists. Wiki links work as relative links do: `[[name]]` opens `name.md` in the doc's directory, `[[sub/name]]` opens `sub/name.md`, and a name with an extension, eg `[[main.go]]`, keeps it. `[[name|label]]` shows the label and `[[name#heading]]` opens at the heading. Lines join into a paragraph, and a blank line starts the next. A line that starts with a dash begins a list even right after text. Straight quotes and dashes turn typographic. A fenced block marked `dot` or `graphviz` becomes a diagram, drawn by Graphviz's `dot` when it is installed and kept by the hash of its source; without `dot`, or when `dot` rejects the graph, the block shows as code under a line that says why. `dot` may not read image files the graph names. Code in a fenced block with a known language is highlighted by chroma, as are code pages, whose language follows the file name. Go is lexed by Go's own scanner from the standard library instead, about 150 times faster, with the same colours. Python, JavaScript and shell have lexers of www's own, which follow chroma's rules and give every byte the class chroma gives it; chroma took 180 ms to lex argparse.py, 2,633 lines, and they take 2 ms. A whole page of it renders in about 5 ms, and jquery.js, 10,907 lines, in 28 ms instead of 400. Other languages go through chroma, at about 70 µs a line. The server keeps each coloured page by its content's hash, within 16 MB, so a later view takes a few milliseconds. A code page's views, flat, scopes and tree, and raw sit in a row of their own above the code's top right corner and follow the page down as it scrolls. Each keeps its place; the view shown is plain text, not a link. Add `?scopes` to a code page, or follow scopes, and it draws each block of code that spans lines as a sunken box behind its text, nested as deep as the code goes, with deeper boxes fainter. A block is a bracket pair whose ends sit on different lines, or in Python the lines indented under a line that ends with a colon. Brackets and strings within a line, around something, are smaller boxes inside it, down to the innermost, much as vim's `ci(` and `ci"` see them; a string that spans lines is a block. Fills alternate inward between a light shade and the page's colour, so depth shows without the page darkening. The boxes lie on a layer of their own, so no character moves and copied text is unchanged. A box is a link to its lines. The box under the pointer is tinted, and a click marks it and the boxes in it and puts its lines in the address, eg `#L482-L520`; a click on the marked box clears both. When the file is committed and unchanged since, the address also names that commit's version, eg `@261024.22/server.go?scopes#L3-L26`, so the link keeps pointing at the same lines after the file changes. Only the address moves: the page's links, eg raw and tree, still open the current file, and a live reload goes back to it. In the same corner, flat goes back to the plain page. Add `?tree`, or follow tree, for the syntax tree: one row per named node, with its field in its parent, its kind, its text on one line and a link to its lines, folded so the file's top-level declarations show first. A helper parses it with tree-sitter: `www-syntax`, which `make syntax` builds beside www and which needs a C compiler. It knows Go, Python, JavaScript, TypeScript, Rust, C, C++, shell, Java, C#, JSON, HTML, CSS and Ruby. Without the helper, or for another language, the page says so above the plain code. A first view of a 1,000-line file takes about 60 ms; the server keeps the tree, so a later view takes a few. A JSON file has one more view, gron, at the left end of that row. `?gron` shows the file as one line per node, each a JavaScript assignment such as `json.tags[0] = "a";`, so every value reads and searches with its full path. Keys keep the file's order and values show as written. A file that is not valid JSON shows the plain code under a line that names where parsing stopped. Every gap between blocks is the same, whether the block is a paragraph, list, table or code. Headings sit close to their text.--www refuses to serve the home directory and every directory above it, such as `/home` and `/`. A tree exposes everything below it, and home holds `~/.ssh`, `~/.gnupg` and every token file.-Symlinks inside the tree are followed, absolute ones included. A link that leads out of the tree is followed only into a directory that holds a file named `.www-follow`, or below one, and only when other users could read the target. A file needs the others-read bit and a directory others-read and others-execute, checked from the marker's directory down. So nobody reads through www what `cat` would not show them.-A store of private files linked into repos, such as lord's `~/.local/share/lord`, opens with one marker in its root. Secrets kept there at mode 600 stay refused. Opening a refused link shows the link, its target and the reason, and the listing marks it refused with the same on hover.-A loopback server answers only to loopback addresses, `localhost` and names that `/etc/hosts` maps to loopback. Its Content-Security-Policy forbids every script but www's own, which reloads pages, marks changed blocks, walks listings by arrow and opens history folds, and which it allows by hash. Raw HTML in a doc passes through, so a README in a cloned repo could otherwise run code on this origin.-www runs no code a tree supplies. It reads files and runs git with every option that would start a program named in the repo's config turned off: the pager, textconv, external diff, signature checks, fsmonitor and fetching.--Each endpoint has a registry directory such as `~/.local/state/www/127.31.1.1-8080/` with a symlink to each lock its server holds. A joining `www` adds a link, and the server sees it through inotify, writes the lock and serves the tree. Where inotify fails the server polls every second. An flock on the directory keeps a tree from joining a server on its way out. `$XDG_STATE_HOME` replaces `~/.local/state` when set.--A lock left by a crash is detected on the next start through its PID and the kernel boot ID, then removed. A new server also clears what a crashed server left in the registry.--The stylesheet is www's own. It began as md2html-default's, whose light palette it keeps, and the page colours sit as variables on `:root`. A page turns dark when the OS asks for it through `prefers-color-scheme`, with a dark palette and syntax colours of its own; it is a first draft. The browser loads it once: its URL names a hash of its content, eg `/_www/style.7b94a08e7731.css`, so it is kept for a year, and a changed stylesheet comes under a new URL.-Pages load no fonts from the network. STIX Two Text, the serif, comes from the machine when installed and otherwise from the copy built into www, which the browser caches for a year. The mono face is Iosevka where installed, else the system monospace.-The header names a tree by its base name and shows the full path on hover. Every tree page's breadcrumbs start at `index`, which links `/`. The header, listings and www's own messages are mono; documents keep the serif. Mono text shows no ligatures, so `<=` stays two characters in any font.-Every page keeps one width, 58ch of the body font, and keeps space for a scrollbar, so the header holds still from page to page. Code is half the size of the body text, never under 11px, so about 115 columns fit; longer lines scroll.--Add `*.*.*.*-*.lock` and `*.*.*.*-*.LOCK` to `~/.config/git/ignore` so locks stay out of `git status`.+www needs git. `make install` links `build/www` into `~/.local/bin`, and serves on port 8080. `make install-deb` builds and installs a Debian package, whose binary may bind port 80, so URLs carry no port.++## Documentation++- [Options and environment](doc/options.md): every option, the keyval output and the variables that set defaults+- [Servers and locks](doc/servers.md): how trees join a server, the index at `/`, addresses, restarts and serving after reboot+- [Pages](doc/pages.md): URLs, how docs render, links, live reload and the look+- [Code pages](doc/code-pages.md): highlighting, line links, scope boxes, the syntax tree and gron+- [Search and controls](doc/search.md): the search box, keys, the copy buttons and when controls fade+- [History](doc/history.md): histories, commits, stardates, the timeline and old versions of a tree+- [Online repos](doc/online-repos.md): reading a forge's repos on the second address+- [Sharing](doc/sharing.md): serving a tree to other machines+- [Safety](doc/safety.md): what www refuses to serve, follow or run+- [Speed](doc/speed.md): what www caches and how it is timed+- [Log](doc/log.md): what www writes to the journal+- [Debugging](doc/debugging.md): the bug button and the build stamp+- [Install](doc/install.md): the Debian package, port 80 and locks in git+- [Development](doc/development.md): `make run`, the working name and the usage check  ## Licence 

bench_test.go

@@ -64,7 +64,7 @@ func BenchmarkSearch(b *testing.B) { // alone, then the whole page, whose difference is the page writer, and // the page with scopes, whose difference is the scope boxes. Go and // CSS come from this repo, CSS through chroma; Python, JavaScript and-// shell from Debian's packages, the files the README times, skipped where+// shell from Debian's packages, the files doc/speed.md names, skipped where // they are not installed. func BenchmarkCodePage(b *testing.B) { 	pythons, _ := filepath.Glob("/usr/lib/python3*/argparse.py")

budget_test.go

@@ -14,22 +14,33 @@ import ( const pageBudget = 5 * time.Millisecond  // Every kind of page renders within pageBudget once its git answers are-// cached: a doc and a code page the size of www's own README and-// render.go, a listing, histories and a commit's diff. Each takes the-// median of 15 runs, so one slow run does not fail it.+// cached: a doc of www's README and every file in doc/ joined, a code+// page of render.go, a listing, histories and a commit's diff. Each takes+// the median of 15 runs, so one slow run does not fail it. func TestPageBudget(t *testing.T) { 	if raceOn { 		t.Skip("the race detector slows every page past the budget") 	} 	needGit(t) 	dir := t.TempDir()-	for _, name := range []string{"README.md", "render.go"} {+	docs, err := filepath.Glob("doc/*.md")+	if err != nil {+		t.Fatal(err)+	}+	var readme []byte+	for _, name := range append([]string{"README.md"}, docs...) { 		b, err := os.ReadFile(name) 		if err != nil { 			t.Fatal(err) 		}-		os.WriteFile(filepath.Join(dir, name), b, 0o644)+		readme = append(append(readme, b...), '\n')+	}+	os.WriteFile(filepath.Join(dir, "README.md"), readme, 0o644)+	b, err := os.ReadFile("render.go")+	if err != nil {+		t.Fatal(err) 	}+	os.WriteFile(filepath.Join(dir, "render.go"), b, 0o644) 	if out, err := exec.Command("git", "init", "-q", dir).CombinedOutput(); err != nil { 		t.Fatalf("git init: %v\n%s", err, out) 	}

doc/code-pages.md

new file mode 100644@@ -0,0 +1,13 @@+# Code pages++Code in a fenced block with a known language is highlighted by chroma, as are code pages, whose language follows the file name. Go is lexed by Go's own scanner from the standard library instead, about 150 times faster, with the same colours. Python, JavaScript and shell have lexers of www's own, which follow chroma's rules and give every byte the class chroma gives it; chroma took 180 ms to lex argparse.py, 2,633 lines, and they take 2 ms. A whole page of it renders in about 5 ms, and jquery.js, 10,907 lines, in 28 ms instead of 400. Other languages go through chroma, at about 70 µs a line. The server keeps each coloured page by its content's hash, within 16 MB, so a later view takes a few milliseconds.++A code page anchors lines as GitHub does, so `main.go#L42` lands on line 42 and highlights it. Clicking a line's number highlights it and puts its address in the address bar; clicking the number again clears both. A range such as `main.go#L10-L20` highlights those lines.++A code page's views, flat, scopes and tree, and raw sit in a row of their own above the code's top right corner and follow the page down as it scrolls. Each keeps its place; the view shown is plain text, not a link.++Add `?scopes` to a code page, or follow scopes, and it draws each block of code that spans lines as a sunken box behind its text, nested as deep as the code goes, with deeper boxes fainter. A block is a bracket pair whose ends sit on different lines, or in Python the lines indented under a line that ends with a colon. Brackets and strings within a line, around something, are smaller boxes inside it, down to the innermost, much as vim's `ci(` and `ci"` see them; a string that spans lines is a block. Fills alternate inward between a light shade and the page's colour, so depth shows without the page darkening. The boxes lie on a layer of their own, so no character moves and copied text is unchanged. A box is a link to its lines. The box under the pointer is tinted, and a click marks it and the boxes in it and puts its lines in the address, eg `#L482-L520`; a click on the marked box clears both. When the file is committed and unchanged since, the address also names that commit's version, eg `@261024.22/server.go?scopes#L3-L26`, so the link keeps pointing at the same lines after the file changes. Only the address moves: the page's links, eg raw and tree, still open the current file, and a live reload goes back to it. In the same corner, flat goes back to the plain page.++Add `?tree`, or follow tree, for the syntax tree: one row per named node, with its field in its parent, its kind, its text on one line and a link to its lines, folded so the file's top-level declarations show first. A helper parses it with tree-sitter: `www-syntax`, which `make syntax` builds beside www and which needs a C compiler. It knows Go, Python, JavaScript, TypeScript, Rust, C, C++, shell, Java, C#, JSON, HTML, CSS and Ruby. Without the helper, or for another language, the page says so above the plain code. A first view of a 1,000-line file takes about 60 ms; the server keeps the tree, so a later view takes a few.++A JSON file has one more view, gron, at the left end of that row. `?gron` shows the file as one line per node, each a JavaScript assignment such as `json.tags[0] = "a";`, so every value reads and searches with its full path. Keys keep the file's order and values show as written. A file that is not valid JSON shows the plain code under a line that names where parsing stopped.

doc/development.md

new file mode 100644@@ -0,0 +1,7 @@+# Development++To try a change without installing it, `make run` builds `build/www` and the syntax tree helper and serves this directory on port 8080 with `--debug`, beside an installed server on port 80. A later run replaces the server on 8080 with the new build, serving the same trees, and open pages go on under it, so edit and `make run` again. It opens the browser only when no page of this directory was open on the old server, and says which happened. A hidden tab holds no connection, so it does not count and gets a second tab. `www --kill -a 127.31.N.1:8080` stops it.++The program's name lives in the file `WORKING_NAME`, since www is a working name until a final one is chosen. The binary, its paths, environment variables, journal tag and URLs, the Makefile and the Debian package all take it from there, and a test fails on any other copy outside prose and `go.mod`.++The help text is written in docopt notation. `make test` checks it with docopt-ng, run through `uv`, and skips the check where `uv` is missing.

doc/history.md

@@ -12,4 +12,4 @@ www runs `git log`, `git show` and `git rev-parse` in the directory of what the A shared server shows history and diffs as loopback does, with four differences. History appears only when other users could read the repo's git directory, its `HEAD`, `objects` and `refs`; nobody reads through www what git run as another user would not show them. Each message loses its trailer block, which carries emails and session links in lines such as `Signed-off-by:` and `Claude-Session:`. As in git, the last paragraph is a trailer block when a quarter of its lines are `Key: value` lines, so a cherry-pick note or a wrapped trailer does not keep it. A page in a repo the share keeps private never reloads on a commit. Diffs leave out files whose names begin with a dot, as listings do. A share never shows git's errors, which name paths on the machine, and says only `No git history here.` No history page shows authors, on a share or on loopback. -Each commit in a history has a browse link, inside its fold, to the page as it was in that commit. `@` and a commit's hash on a tree's name serve the tree as of that commit, eg `/scratch/www@5430ac3/` for its root or `/scratch/www@5430ac3/doc/history.md` for one doc: the version qualifies the tree, so it sits on the name, not below it as an entry would. A tree served at `/` has no name, so its versions sit below it, eg `/@5430ac3/`. The address below the tree, eg `/scratch/www/@5430ac3/`, is the old form and redirects. A stardate works the same, eg `www@261024.4/`, and a hash of a commit with a stardate redirects to it. A day alone, eg `www@261024/`, names no commit and says so, though its six digits would also read as a hash. Docs, code pages, listings and search work there as on the tree itself, read from git rather than the disk, and relative links stay in the same version. Any length of hash from four characters up works and redirects to git's short form. The top line names the commit that last changed the page as of that version, stardate first as in histories, and opens the same timeline with that version marked. At the line's right end, commit opens the commit's page, and current leads to the page as it is, as does the timeline's first row. The breadcrumbs and the tab title carry the hash, eg `www @ 5430ac3`. A version never changes, so it has no live reload, no Delete lock button and no path to copy, and its docs list no backlinks. www's script still runs there, so keys, the search box and line marks work as on the tree. A tree that holds an entry named like a version, eg a directory `@cafe`, serves that entry at `/scratch/www/@cafe/`, and the old form does not redirect there. Shares and the web address have no versions.+Each commit in a history has a browse link, inside its fold, to the page as it was in that commit. `@` and a commit's hash on a tree's name serve the tree as of that commit, eg `/scratch/www@5430ac3/` for its root or `/scratch/www@5430ac3/doc/history.md` for one doc: the version qualifies the tree, so it sits on the name, not below it as an entry would. A tree served at `/` has no name, so its versions sit below it, eg `/@5430ac3/`. The address below the tree, eg `/scratch/www/@5430ac3/`, is the old form and redirects. A stardate works the same, eg `www@261024.4/`, and a hash of a commit with a stardate redirects to it. A day alone, eg `www@261024/`, names no commit and says so, though its six digits would also read as a hash. Docs, code pages, listings and search work there as on the tree itself, read from git rather than the disk, and relative links stay in the same version. Any length of hash from four characters up works and redirects to git's short form. The top line names the commit that last changed the page as of that version, stardate first as in histories, and opens the same timeline with that version marked. At the line's right end, commit opens the commit's page, and current leads to the page as it is, as does the timeline's first row. The breadcrumbs and the tab title carry the hash, eg `www @ 5430ac3`. A version never changes, so it has no live reload, no Delete lock button and no path to copy, and its docs list no backlinks. www's script still runs there, so keys, the search box and line marks work as on the tree. A tree that holds an entry named like a version, eg a directory `@cafe`, serves that entry at `/scratch/www/@cafe/`, and the old form does not redirect there. Shares and the web address have no versions. A page whose history lies in a nested repo, or behind a link, lists its commits but no versions, since the tree's versions do not hold it.

doc/install.md

new file mode 100644@@ -0,0 +1,10 @@+# Install++www needs git.+`make install` links `build/www` into `~/.local/bin`.+`make deb` builds a Debian package in `build/`, such as `www_0~git20261015223930.20f02fd_amd64.deb`, versioned by the last commit's UTC time and hash. It depends on git. For another architecture, pass `ARCH`, eg `make deb ARCH=arm64`.+`make install-deb` builds the package while sudo asks for the password, then installs it with `sudo apt install`, which pulls in git. Debian's needrestart lists the services that use old libraries after the install instead of asking which to restart; none of them is www. It first removes the `~/.local/bin` link from `make install`, which would otherwise shadow the package's binary. Run it again to replace an installed build, even of the same commit. `make uninstall-deb` removes the package.++The package grants `/usr/bin/www` one capability, `cap_net_bind_service`, so it can bind port 80 while running as the user. `getcap /usr/bin/www` shows it. With it www serves on port 80 and URLs carry no port; without it, as after `make install`, the port is 8080. www also takes port 80 where the machine lets every user bind low ports.++Add `*.*.*.*-*.lock` and `*.*.*.*-*.LOCK` to `~/.config/git/ignore` so locks stay out of `git status`.

doc/options.md

new file mode 100644@@ -0,0 +1,43 @@+# Options and environment++`www --help` prints the options in short. Here is each in full.++- `-a ADDR`, `--addr=ADDR`: serve on another loopback address, an IP or IP:PORT; the port defaults to 80 where www may bind it, else 8080+- `--share[=HOST]`: share the tree on the default route's address, or on the address or name given beyond loopback, with an optional port, such as `192.168.1.5`, `0.0.0.0` or `mate.peer.home`; see `doc/sharing.md`+- `--secret`: serve a shared tree under the token in `WWW_SECRET`, or a random one, so only those holding the link reach it+- `--foreground`: when no server runs on the address yet, serve in the foreground until Ctrl-C+- `--no-open`: print the URL without opening a browser+- `--no-bug`: hide the bug button at the foot of each page and refuse reports; `--restart` passes it to the new servers when given+- `-p`, `--persist`: write a persistent lock, such as `127.31.1.1-8080.LOCK`, so the tree is served again after a reboot; see `doc/servers.md`+- `--debug`: show what serves debugging: above each page's header, the build stamp, see `doc/debugging.md`; on the index, each server's build and start time and each lock's time. `make run` sets it; `--restart` passes it to the new servers when given, and leaves it off otherwise+- `--cache=SIZE`: memory a new server may spend on cached git answers, eg `64M`; `0` caches nothing. The default is `32M`, and `--restart` goes back to it+- `--disk=SIZE`: disk a new server may fill with online repos, eg `4G`; it drops the repos read longest ago, but not one that another server is fetching, since every server shares the cache. The default is `2G`, and `--restart` goes back to it+- `--restart`: replace every server this binary started with one running it now, serving the same trees, and print each tree's URL; an address with persistent trees and no server gets one, when this binary served it last; with `-a` or `--share`, only the server on that address, whichever binary runs it; with a directory, it also serves that directory there and opens it in the browser unless a page of it was open, and says which+- `-f`, `--force`: with `--restart`, take the servers and persistent trees of other binaries too+- `-l`, `--list`: print each served tree's directory, URL and binary as keyval, on every address; with `-a` or `--share`, on that address only; with a directory, only the trees at or below it. A directory that holds no served tree prints nothing; one that does not exist fails with `E2`+- `--kill`: stop every server and print the trees it served, as `--list` does; with `-a` or `--share`, only the server on that address+- `--commit=REV`: print as keyval the commit REV names in the repo of DIR, or of the current directory: its full hash, its stardate and its subject. REV is a stardate, eg `261024.4`, a hash or any name git takes, so `git show $(www --commit=261024.4 | sed -n 's/^hash //p')` shows a commit seen on a page. A day and x, eg `261026.x`, works while one commit alone holds it. A commit or repo that is not there fails with `E2`+- `-V`, `--version`: print when the binary was built and the commit it came from, as the stamp says it, eg `www built 2026-10-21 12:49, from 1aa282c+modified`+- `-h`, `--help`: print the options++Options follow GNU style. Short ones combine, as in `-pa 127.31.1.2`, and options may follow the directory. Optional values go joined, as in `--share=192.168.1.5`; with a space the address would be read as the directory, and www says so. `-f` and `-n` mean force and dry run in every tool of the author's, so `--foreground` and `--no-open` have no short form. The help text is written in docopt notation, except that docopt has no way to write the optional `=HOST`.++`--list` and `--kill` print [keyval](https://unixfile.github.io/keyval/): three lines per tree, sorted, with every index the same width. The binary line is missing for a server that started before www recorded it.++```+tree.0.binary /usr/bin/www+tree.0.dir /home/t/keep/notes+tree.0.url http://127.31.1.1/keep/notes/+tree.1.binary /home/t/scratch/github.com/unixfile/www/build/www+tree.1.dir /home/t/scratch/github.com/unixfile/www+tree.1.url http://127.31.1.1:8080/scratch/github.com/unixfile/www/+```++Two variables set the default addresses. `--help` shows each value and where it comes from, and an error from a bad value names its variable.++- `WWW_SHARE`: the address or name for a bare `--share`, eg `mate.peer.home`, in a start and in `--kill`, `--restart` and `--list`. Without it a bare `--share` takes the default route's address+- `WWW_ADDR`: the loopback address for a start without `-a`. `--list`, `--kill` and `--restart` without an address still cover every server++A third sets how a start opens its URL. `WWW_OPEN` holds the command, split at spaces, with `%s` where the URL goes, or the URL appended when no `%s` is given. `WWW_OPEN='brave-browser --app=%s'` opens a tree in an app window and `WWW_OPEN=firefox` in Firefox. It applies to loopback starts only; a share opens through `xdg-open`, as every start does without the variable.++A fourth holds a share's token. `WWW_SECRET` gives `--secret` a token of letters, digits, `-` and `_` instead of a random one. The token never goes on the command line, where `ps` shows it to every local user and the shell keeps it in its history, so `--secret=token` fails with `E22`. `--help` says whether the variable is set but never shows its value.

doc/pages.md

new file mode 100644@@ -0,0 +1,29 @@+# Pages++URLs start with the served directory's path relative to home, eg `http://127.31.1.1:8080/scratch/github.com/unixfile/webstack/index.md`, so a URL names its tree. A directory outside home uses its absolute path.+Below the prefix, URLs mirror the file tree, so relative links in the docs work unchanged. An absolute link such as `/x.md` points outside the prefix and gives 404, on a page of its own that names the trees nearest it, or every tree when none is near. A path the tree lacks gives 404 and names the whole URL; the digits turn like a coin before they stop. Below, the page says where git saw the path go: the commit that deleted or moved it, with a link to its last version, and the names close to it in the nearest directory that exists. A `.md` file renders as a page, a directory as a listing of its names, a text file as a highlighted code page with linkable lines, and anything else raw. Add `?raw` to any URL for the file itself. A tree inside another served tree answers for its own paths.++Bare URLs in a doc become links. A relative link in a doc whose target is missing gets a dotted red underline and names the missing path on hover. A link that climbs out of the tree is not checked, since another tree may answer there.++## Docs++Docs follow CommonMark with GitHub's extensions, rendered by goldmark: tables, strikethrough, task lists, bare URLs as links, footnotes and definition lists. Wiki links work as relative links do: `[[name]]` opens `name.md` in the doc's directory, `[[sub/name]]` opens `sub/name.md`, and a name with an extension, eg `[[main.go]]`, keeps it. `[[name|label]]` shows the label and `[[name#heading]]` opens at the heading. Lines join into a paragraph, and a blank line starts the next. A line that starts with a dash begins a list even right after text. Straight quotes and dashes turn typographic. A fenced block marked `dot` or `graphviz` becomes a diagram, drawn by Graphviz's `dot` when it is installed and kept by the hash of its source; without `dot`, or when `dot` rejects the graph, the block shows as code under a line that says why. `dot` may not read image files the graph names.++Every gap between blocks is the same, whether the block is a paragraph, list, table or code. Headings sit close to their text.++## Linked from and recent pages++Below a doc, linked from names each doc in the tree that links to it, by any relative spelling. The list arrives after the page, since the server walks the tree for it: a few milliseconds in a repo, about 250 ms across all of `~/scratch`, 77,000 files. The server keeps each doc's links and reads a doc again only when its size or time changes. It reads a new tree's links in the background when the tree joins.++The browser remembers the pages you open; the server keeps no such list. A search shows the best five that match its words first, under recent, from any tree on the address. Each visit counts and recent visits weigh more, as in zoxide. A reload is no visit. Each address is an origin of its own, so each visitor to a share sees only the pages that visitor opened, and online repos keep a list apart from the trees.++## Live reload++Open pages reload when what they show changes. A doc takes its new version in place, without a reload, and each block that changed fills with the highlight colour and fades over three seconds, so the eye finds the change after a save. A code page reloads when its file is written, a listing when a name in it changes, any page in a repo when its HEAD moves, and the index when a tree joins or leaves. A page whose file or tree is gone waits two seconds for it to return, then reloads to say so. A page outlives its server. After three seconds without it, a line over the page says the server is gone and since when, and the page keeps trying each second. When a new server answers, eg after `make run` or `--restart`, the page takes its version from it and a line says new server, fading over three seconds. A restart takes about 60 ms, so its new server answers before the gone line shows. Each page holds a server-sent events stream while its tab is visible; a hidden tab lets go, since a browser opens at most six connections to one host, and checks again when shown. The server watches the directory of each open page and its repo's git directory through inotify, since editors save by renaming a new file over the old one, and polls every second where inotify fails.++## Look++The stylesheet is www's own. It began as md2html-default's, whose light palette it keeps, and the page colours sit as variables on `:root`. A page turns dark when the OS asks for it through `prefers-color-scheme`, with a dark palette and syntax colours of its own; it is a first draft. The browser loads it once: its URL names a hash of its content, eg `/_www/style.7b94a08e7731.css`, so it is kept for a year, and a changed stylesheet comes under a new URL.+Pages load no fonts from the network. STIX Two Text, the serif, comes from the machine when installed and otherwise from the copy built into www, which the browser caches for a year. The mono face is Iosevka where installed, else the system monospace.+The header names a tree by its base name and shows the full path on hover. Every tree page's breadcrumbs start at `index`, which links `/`. The header, listings and www's own messages are mono; documents keep the serif. Mono text shows no ligatures, so `<=` stays two characters in any font.+Every page keeps one width, 58ch of the body font, and keeps space for a scrollbar, so the header holds still from page to page. Code is half the size of the body text, never under 11px, so about 115 columns fit; longer lines scroll.

doc/safety.md

new file mode 100644@@ -0,0 +1,9 @@+# Safety++www refuses to serve the home directory and every directory above it, such as `/home` and `/`. A tree exposes everything below it, and home holds `~/.ssh`, `~/.gnupg` and every token file.+Symlinks inside the tree are followed, absolute ones included. A link that leads out of the tree is followed only into a directory that holds a file named `.www-follow`, or below one, and only when other users could read the target. A file needs the others-read bit and a directory others-read and others-execute, checked from the marker's directory down. So nobody reads through www what `cat` would not show them.+A store of private files linked into repos, such as lord's `~/.local/share/lord`, opens with one marker in its root. Secrets kept there at mode 600 stay refused. Opening a refused link shows the link, its target and the reason, and the listing marks it refused with the same on hover.+A loopback server answers only to loopback addresses, `localhost` and names that `/etc/hosts` maps to loopback. Its Content-Security-Policy forbids every script but www's own, which reloads pages, marks changed blocks, walks listings by arrow and opens history folds, and which it allows by hash. Raw HTML in a doc passes through, so a README in a cloned repo could otherwise run code on this origin.+www runs no code a tree supplies. It reads files and runs git with every option that would start a program named in the repo's config turned off: the pager, textconv, external diff, signature checks, fsmonitor and fetching.++The addresses keep users out of each other's way, not out of each other's trees. Loopback is open to every local account, so on a machine shared with other users, they can reach your server. Serve nothing there you would not show them.

doc/search.md

new file mode 100644@@ -0,0 +1,9 @@+# Search and controls++The box under the header searches what is below it as you type, ignoring case. It holds the keyboard, so typing on any page searches, wherever focus is. On a listing it searches the directory: names that hold the query first, then lines in text files, with the directory's own entries at full strength and what lies in its subdirectories after a gap, dimmed. A line result opens a code page at its line. Any other page filters itself: a code page keeps its matching lines with their numbers, a doc its matching blocks, a history its matching commits, with each match marked, and Enter jumps to the first match in the whole page. Below, after a gap and dimmed, follow the directory's results without the page's own file. The query stays in the URL as `?q=`, so Back from a result or a reload shows the results again. Hidden files are searched too, but not the stores machines write, such as `.git`, `.venv` or `.cache`, nor files over the code page limit; a search stops after half a second and says so, and a share searches only what it serves.++Escape clears the query and brings the page back. Up and Down move a highlight through a listing's entries, or through a search's matches and results, and Enter opens it; a match opens at its place in the whole page. Escape on an empty query goes up, eg from a file to its directory or from a commit to its history, and highlights the page it left, so Enter goes back. Left and Right move the cursor in the box and never navigate. With nothing to walk, Up, Down and Space scroll the page. The highlight survives a reload. On a touch screen the box takes the keyboard only when tapped, since it would raise the on-screen keyboard. Shift+Tab in the box enters the header at the crumb Escape goes to, and Tab goes on from there to raw on a doc and Delete lock. Escape on a focused link or button returns to the box.++Beside the box, ⧉ copies the page's text: what lies below the box, without www's own lines and links or line numbers, so a code page copies as its file and a listing as its names. A doc copies its Markdown, and lines a range marks, eg `#L3-L26`, copy alone. What a search hides stays out. Beside it, ⤴ copies a link to what is selected: selected or marked lines on a code page, pinned to the commit when the file is clean, eg `@261024.22/server.go#L10-L12`; other selected text as a link that scrolls to it and marks it, `#:~:text=`; with nothing selected, the page. The link carries the page's own address, so a share's link opens on other machines and a loopback one only here. On the web address it is the forge's own link at the commit shown, eg `https://github.com/junegunn/fzf/blob/<commit>/main.go#L10-L12`, which opens in any browser.++The search row, box and buttons alike, sticks to the top of the window as the page scrolls, so all three stay at hand while reading, and a jump to a line stops below it. The box, its buttons, raw, the stop button and a code page's views fade while nothing needs them, and keep their space; the search row then lets the text scroll through it. They show while the page sits at its top, since the gaps they leave there look odd. Once the first line scrolls under the search row, they show only while the query holds a character, while the mouse moves over the column, while keyboard focus sits on one of them or a crumb, and when the page scrolls up. Once shown, they fade only 2 s after the last of these ends, never at once. The crumbs, the header's status and the bug button stay.

doc/servers.md

new file mode 100644@@ -0,0 +1,32 @@+# Servers and locks++Run `www` in a directory. It prints the tree's URL, opens the browser and writes a lockfile named after the endpoint, such as `127.31.1.1-8080.lock`.+`cat` the lockfile to see the URL, root, PID and start time.+Delete the lockfile to stop serving the tree. The Delete lock button on the tree's pages does the same.+The URL is all it prints, so `url=$(www --no-open)` works. Running `www` again in a served directory prints the same URL and starts nothing.++Trees on one address share one server. The first `www` starts it in the background, and later ones hand their tree to it and return.+When something other than your www on that address holds the port, `www` fails at once with `E98` and names the listeners there.+Delete lock and Kill server answer 410 Gone, with a page that names each tree stopped, its lock removed or kept, and whether the server serves on.+The server exits when its last lock is gone. SIGTERM stops it at once and removes every lock but a persistent `.LOCK`, as does the Kill server button on the index at `/`.+A running server keeps the binary it started with. `www --restart` moves every server's trees to the current binary, and `make install` and `make install-deb` run it for you; open tabs reload onto the new binary, and it says how many did. It moves only the servers its own binary started, so the package and a build from `make install` leave each other's servers alone; it names each server it leaves, and `-f` takes them too. `make install-deb` passes `-f`, since it removes the `make install` link. A binary without the capability for port 80, such as the one from `make install`, leaves a port-80 server on its old binary and says so.++The index at `/` reads the same on every server. It lists each running server in the order of their addresses: the address and the binary it runs. Each server's trees follow, with the lock's path on hover. With `--debug`, each server also says when its binary was built, from which commit and when the server started, and each tree when its lock was written; these serve debugging, so they are hidden otherwise. The server you are reading from says "this page", and only its trees carry a Delete lock button, since a server takes a stop only from its own pages; another server's address links to its own index. The package's binary and the one `make install` links are listed under No server when they run none, so both installs always show. A warning appears under a server whose binary changed on disk after it started, with the command that moves it to the new one.+To reach the server by name, add a line such as `127.31.1.1 www` to `/etc/hosts`. Type the name with its slash, eg `www/`, or the browser searches for it.++## Addresses++Each user owns 127.31.N.0/24 with N = uid − 1000 and serves on 127.31.N.1 by default, port 80 where www may bind it, else 8080, and online repos on 127.31.N.2. Uids from 1000 to 1255 fit; others need `--addr`. `-a` takes only loopback addresses; `--share` takes the others.++## Serving after reboot++`www -p` writes a lock in capitals, such as `127.31.1.1-8080.LOCK`. It holds no PID, and it outlives its server: Kill server, `--kill`, SIGTERM and a reboot all stop the server but leave the `.LOCK` in place. The next server on that address serves the tree again, and so does `www --restart`, which starts a server for every address whose persistent trees have none. Deleting the `.LOCK`, or Delete lock on the tree's pages, ends the tree for good. `www -p` in a tree served by a plain `.lock` moves it to a `.LOCK`, and a plain `www` in a tree that holds a `.LOCK` keeps it.++A systemd user unit runs `www --restart` at login. The package puts `www.service` in `/usr/lib/systemd/user/`; enable it with `systemctl --user enable www`. `make install` puts `www-dev.service` in `~/.config/systemd/user/`; enable it with `systemctl --user enable www-dev`. Both ship disabled. They serve from login to logout, and linger stays off. Each brings back only the trees its own binary served last, so with both installs both units run.+The package also ships a system template, `www@.service`, which serves a user's trees from boot, before login, with the capability for port 80. Enable it with `sudo systemctl enable www@<user>`. While the template or the package's user unit runs for a user, the other stands aside, since both run one binary on the same trees. `www-dev` runs beside either.++## Registry++Each endpoint has a registry directory such as `~/.local/state/www/127.31.1.1-8080/` with a symlink to each lock its server holds. A joining `www` adds a link, and the server sees it through inotify, writes the lock and serves the tree. Where inotify fails the server polls every second. An flock on the directory keeps a tree from joining a server on its way out. `$XDG_STATE_HOME` replaces `~/.local/state` when set.++A lock left by a crash is detected on the next start through its PID and the kernel boot ID, then removed. A new server also clears what a crashed server left in the registry.

doc/speed.md

new file mode 100644@@ -0,0 +1,9 @@+# Speed++www is fast. A page from a running server takes one to three milliseconds, a server starts in about 40 ms, and a test in `make test` fails any page that takes over five.++A server keeps git's answers so most pages run no git at all: which repo each directory lies in, the histories pages show and the commits they open, which never change and are kept by their full hash. It reads HEAD from the repo's files, and a history is kept under the commit HEAD names, so a new commit misses the cache and nothing shown goes stale. A `.git` that appears or goes makes the server ask git again. `--cache` sets the memory for it.+The cache holds only what someone has visited, with one exception. When a tree joins, the server asks git in the background for what its listing and its README show, since those open first. The log says how long that took.+In a large repo git can take a second for a history, because it reads every commit back to the ones that touched the path. When it takes over 100 ms, the history page says so and names `git commit-graph write --reachable --changed-paths`, which builds git's index of the paths each commit changed and makes such histories several times faster. www never writes to the repo itself.++`make bench` times what a reader waits for: a search across a generated tree of 4,000 files and 24 MB, and a code page's first view per language: lexing alone, the whole page, and the page with scope boxes. `BENCH` picks benchmarks by pattern and `COUNT` repeats them, eg `make bench BENCH=Search COUNT=10` for benchstat. Python, JavaScript and shell are timed on argparse.py, jquery.js and bash_completion from Debian's packages and skipped where those are not installed. Warm pages have no benchmark, since `TestPageBudget` in `make test` holds them under 5 ms.