261028.6 · 1c2621768fcded2901c30c32065ef031f6860ddd
README and docs read in short paragraphs under topic headings
Long runs of joined lines rendered as one block, up to 587 words in history.md. The content stays; paragraphs split by topic, sections gain headings, and true lists become bullets. README opens with one line on what www reads.
reading declarations
@@ -1,10 +1,12 @@ # www -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.+www reads directories and repos in the browser: Markdown as pages, code as code, history beside both. -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 for anyone who keeps notes, docs and code in plain files and wants to read them as linked pages. It runs on your machine. Nothing needs a build step, and nothing needs a push first. -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.+A repo on GitHub, GitLab or Codeberg reads the same way, once www has fetched it.++It is fast. A page takes one to three milliseconds and a server starts in about 40 ms. `make test` fails any page that takes over 5 ms. ## Usage @@ -24,7 +26,9 @@ 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`. Delete the lockfile to stop serving the tree. Trees on one address share one server, which exits when its last lock is gone.+Run `www` in a directory. It prints the tree's URL and opens the browser.++It also writes a lockfile named after the endpoint, eg `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. ``` www serve this directory and open it@@ -36,11 +40,17 @@ www --public --disk=6G -a 127.0.0.1:8080 serve online repos to the web, behind a proxy ``` -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.+Add `?log` to any page's URL for its history.++To read a repo from GitHub, open `http://127.31.1.2/github.com/junegunn/fzf/`. ## Install -www needs git. `make install` links `build/www` into `~/.local/bin`, and serves on port 8080. `make install-deb` builds and installs the Debian packages, www and its syntax tree helper. The package's binary may bind port 80, so URLs carry no port.+www needs git.++`make install` links `build/www` into `~/.local/bin` and serves on port 8080.++`make install-deb` builds and installs two Debian packages: www and its syntax tree helper. The package's binary may bind port 80, so URLs carry no port. ## Documentation @@ -61,5 +71,8 @@ www needs git. `make install` links `build/www` into `~/.local/bin`, and serves ## Licence -MIT, see `LICENSE`. The embedded subsets of STIX Two Text and Iosevka are under the SIL Open Font License, in `assets/fonts/STIXTwoText-OFL.txt` and `assets/fonts/Iosevka-OFL.txt`.-The binary links in three Go modules, all under the MIT licence: goldmark by Yusuke Inuzuka, chroma by Alec Thomas and regexp2 by Doug Clark, which ports parts of Microsoft's dotnet/corefx. The package's copyright file carries their notices.+MIT, see `LICENSE`.++The embedded fonts are subsets of STIX Two Text and Iosevka, under the SIL Open Font License: see `assets/fonts/STIXTwoText-OFL.txt` and `assets/fonts/Iosevka-OFL.txt`.++The binary links in three Go modules under the MIT licence: goldmark by Yusuke Inuzuka, chroma by Alec Thomas and regexp2 by Doug Clark. regexp2 ports parts of Microsoft's dotnet/corefx. The package's copyright file carries their notices.
@@ -1,13 +1,51 @@ # 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 under 2 ms. A whole page of it renders in about 3 ms, and jquery.js, 10,907 lines, in about 20 ms instead of 400. A Go page of 1,400 lines takes 2 ms. 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.+Code in a fenced block with a known language is highlighted by chroma, as are code pages, whose language follows the file name. -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.+## Highlighting speed -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.+Go is lexed by Go's own scanner from the standard library instead, about 150 times faster, with the same colours. A Go page of 1,400 lines takes 2 ms. -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.+Python, JavaScript and shell have lexers of www's own. They 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 under 2 ms. A whole page of it renders in about 3 ms, and jquery.js, 10,907 lines, in about 20 ms instead of 400. -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 the package of that name installs and `make syntax` builds beside www with 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,400-line file takes about 30 ms and sends 1.2 MB; the server keeps the tree, so a later view takes a few milliseconds.+Other languages go through chroma, at about 70 µs a line. -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.+The server keeps each coloured page by its content's hash, within 16 MB, so a later view takes a few milliseconds.++## Line links++A code page anchors lines as GitHub does, so `main.go#L42` lands on line 42 and highlights it. A range such as `main.go#L10-L20` highlights those lines.++Clicking a line's number highlights it and puts its address in the address bar. Clicking the number again clears both.++## Views++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. The row follows the page down as it scrolls. Each view keeps its place; the view shown is plain text, not a link.++## Scopes++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. Boxes nest as deep as the code goes, and deeper boxes are 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. 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`. The link then 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.++## Syntax tree++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. It is folded so the file's top-level declarations show first.++A helper parses it with tree-sitter: `www-syntax`, which the package of that name installs and `make syntax` builds beside www with 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,400-line file takes about 30 ms and sends 1.2 MB. The server keeps the tree, so a later view takes a few milliseconds.++## Gron++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.
@@ -2,7 +2,9 @@ ## Bug reports -A small, faint `www bug` button sits at the foot of every page. It reports a bug in www and the server, not in the tree you are reading. A click files a numbered bug with the page attached, and the button says which, eg `bug 7 filed`. Text selected on the page goes in as the note. A field then opens beside the button for a comment: Enter adds it, Escape or clicking away leaves the bug without one.+A small, faint `www bug` button sits at the foot of every page. It reports a bug in www and the server, not in the tree you are reading.++A click files a numbered bug with the page attached, and the button says which, eg `bug 7 filed`. Text selected on the page goes in as the note. A field then opens beside the button for a comment: Enter adds it, Escape or clicking away leaves the bug without one. Each report lands in `csi/bugs/` of the checkout the binary was built from, when `make build` built it there and that checkout has csi, so the bugs sit with www's own project. Otherwise, as for the package, it lands in `~/.local/state/www/bugs/`, or below `$XDG_STATE_HOME` when set: @@ -21,4 +23,6 @@ bug 7 comment: "after a restart" ## Build stamp -With `--debug`, each page says when its binary was built and how many seconds ago, the commit it was built from and how long the page took to render, eg `built 12:49, 108 s ago, from 1aa282c+modified, rendered in 3.3 ms`. It sits small and faint above the header, so a glance tells which build served the page. `+modified` marks a build from uncommitted code. www's script counts the seconds up while the page is open. Shares leave the stamp out.+With `--debug`, each page says when its binary was built and how many seconds ago, the commit it was built from and how long the page took to render, eg `built 12:49, 108 s ago, from 1aa282c+modified, rendered in 3.3 ms`. It sits small and faint above the header, so a glance tells which build served the page.++`+modified` marks a build from uncommitted code. www's script counts the seconds up while the page is open. Shares leave the stamp out.
@@ -1,9 +1,19 @@ # 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.+To try a change without installing it, run `make run`. It builds `build/www` and the syntax tree helper and serves this directory on port 8080 with `--debug`, beside an installed server on port 80. -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`.+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 working name++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. A test fails on any other copy outside prose and `go.mod`.++## Usage check 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. -`make fonts` cuts the embedded fonts from STIX Two Text 2.13 and Iosevka 34.9.1, pinned by SHA-256 in `assets/fonts/subset.py`, and writes `assets/fonts.css`. It needs the network and `uv`; its output is committed, so a build needs neither.+## Fonts++`make fonts` cuts the embedded fonts from STIX Two Text 2.13 and Iosevka 34.9.1, pinned by SHA-256 in `assets/fonts/subset.py`, and writes `assets/fonts.css`. It needs the network and `uv`. Its output is committed, so a build needs neither.
@@ -1,15 +1,95 @@ # History -Add `?log` to a path for its git history: the commits that touched it, newest first, one line each with its stardate and subject. The stardate leads, so the stardates line up, and the version menu orders its rows the same way. A line opens to the commit's full message and its diff, limited to the path; the diff loads when the line first opens. A commit with no message shows its day alone. Each commit shows its stardate, eg 261024.4, the fourth commit of 24 Oct 2026 on the repo's main line: the first-parent line of origin's main or master, else of the branch origin's HEAD names, eg origin/trunk, else of the local main or master. A commit a merge brought in is named after the merge, eg 261024.4.2. A commit on the local main or master that origin's line has not reached yet shows its day and x, eg 261026.x: its number comes with the push, so a rebase before it renames nothing. Several commits can share a day and x, so a history or the timeline adds each one's short hash, eg 261026.x 6376dd4. Its version's address uses the hash. A commit on no main line, eg on another branch, shows its short hash. Hover shows the hash and the exact time. A row is one target: a click anywhere on it opens or closes the fold. Inside, the full hash links the commit's own page and browse links its version. A directory's history covers everything below it. A listing whose directory has commits opens its history from its first entry, `<(git log)`. A doc or code page with commits opens with a line that says where the reader is: current when the page matches its last commit, changed when it has edits since. Hover names the commit by stardate and subject. www compares git's id for the file with the one HEAD keeps, which takes microseconds, and asks `git status` only when they differ. A ▸ leads the line, the browser's own sign that it opens, and turns to ▾ while open. A click anywhere on the line opens the timeline, a menu of every version of the page, newest first, then the history; `?timeline` serves it as a page. The version shown is marked. The newest row leads to the current page and says current, or, when the page has uncommitted changes, a row of its own above it says so. The menu loads when the pointer first passes over the line, so it opens full. Up and Down walk it, Enter opens a version, and Escape or a click outside closes it. On a share, where there are no versions, its rows open the commits. A page outside a repo shows git's complaint.-A history lists the 500 newest commits. Bodies wrapped by hand join into paragraphs; a list item or an indented line keeps its own line.-A commit's hash in a history's fold, or commit at the right end of a version's top line, opens the commit on a page of its own: its stardate and full hash as plain text, to select and give to git, then its message and its diff, limited to the file or directory the history was for, with a link to all the files it changed in the tree. Added lines sit on a green tint and removed ones on a red. The diff stops at 1 MiB. From the terminal, `www --commit=261024.4` prints a commit's full hash, stardate and subject as keyval.+Add `?log` to a path for its git history: the commits that touched it, newest first, one line each with its stardate and subject. A directory's history covers everything below it. A history lists the 500 newest commits. -The diff shows at once. The semantic diff follows a moment later: above the diff, each file's declarations the commit added, removed, changed, renamed, moved or only reformatted, eg `changed tree.serveSearch` or `moved Away from x.go`. Each name opens a page of that declaration alone: what the commit did to it, its diff from its old self, which a rename or a move between files follows, and links to its lines before and after where the tree has versions. Within the diff, the tokens that changed in a changed line sit on a stronger tint, so a renamed argument marks the name alone; a run of lines that shares under half its tokens with the lines it replaced is left unmarked. Declarations come from the syntax helper, `make syntax`, and the token marks from the lexers, so without the helper the page says so and keeps the marks. A first view of a commit that touches 12 files took about 190 ms; the server keeps the result by commit, so a later view takes a few milliseconds and shows it with the page.-www runs `git log`, `git show` and `git rev-parse` in the directory of what the page shows, after links resolve. A cloned repo's `.git/config` can name programs for git to run, so www turns off external diff, textconv, signature checks and fetches, and drops `GIT_` variables from its environment. git's own `safe.directory` check refuses repos that other users own.+A listing whose directory has commits opens its history from its first entry, `<(git log)`. A page outside a repo shows git's complaint. -## On shares+## A history's lines++The stardate leads each line, so the stardates line up, and the version menu orders its rows the same way. A commit with no message shows its day alone. Hover shows the hash and the exact time.++A line opens to the commit's full message and its diff, limited to the path. The diff loads when the line first opens. Bodies wrapped by hand join into paragraphs; a list item or an indented line keeps its own line.++A row is one target: a click anywhere on it opens or closes the fold. Inside, the full hash links the commit's own page and browse links its version.++## Stardates++Each commit shows its stardate, eg 261024.4, the fourth commit of 24 Oct 2026 on the repo's main line. The main line is the first-parent line of origin's main or master, else of the branch origin's HEAD names, eg origin/trunk, else of the local main or master.++A commit a merge brought in is named after the merge, eg 261024.4.2.++A commit on the local main or master that origin's line has not reached yet shows its day and x, eg 261026.x. Its number comes with the push, so a rebase before it renames nothing. Several commits can share a day and x, so a history or the timeline adds each one's short hash, eg 261026.x 6376dd4. Its version's address uses the hash.++A commit on no main line, eg on another branch, shows its short hash.++## Where the reader is++A doc or code page with commits opens with a line that says where the reader is: current when the page matches its last commit, changed when it has edits since. Hover names the commit by stardate and subject.++www compares git's id for the file with the one HEAD keeps, which takes microseconds, and asks `git status` only when they differ.++## The timeline++A ▸ leads the line, the browser's own sign that it opens, and turns to ▾ while open. A click anywhere on the line opens the timeline: a menu of every version of the page, newest first, then the history. `?timeline` serves it as a page.++The version shown is marked. The newest row leads to the current page and says current. When the page has uncommitted changes, a row of its own above it says so.++The menu loads when the pointer first passes over the line, so it opens full. Up and Down walk it, Enter opens a version, and Escape or a click outside closes it. On a share, where there are no versions, its rows open the commits.++## Commit pages++A commit's hash in a history's fold, or commit at the right end of a version's top line, opens the commit on a page of its own. It shows the stardate and full hash as plain text, to select and give to git, then the message and the diff. The diff is limited to the file or directory the history was for, with a link to all the files the commit changed in the tree.++Added lines sit on a green tint and removed ones on a red. The diff stops at 1 MiB.++From the terminal, `www --commit=261024.4` prints a commit's full hash, stardate and subject as keyval.++## Semantic diffs++The diff shows at once. The semantic diff follows a moment later, above the diff: each file's declarations the commit added, removed, changed, renamed, moved or only reformatted, eg `changed tree.serveSearch` or `moved Away from x.go`.++Each name opens a page of that declaration alone. It shows what the commit did to it and its diff from its old self, which a rename or a move between files follows. It links to its lines before and after where the tree has versions.++Within the diff, the tokens that changed in a changed line sit on a stronger tint, so a renamed argument marks the name alone. A run of lines that shares under half its tokens with the lines it replaced is left unmarked.++Declarations come from the syntax helper, `make syntax`, and the token marks from the lexers. Without the helper the page says so and keeps the marks.++A first view of a commit that touches 12 files took about 190 ms. The server keeps the result by commit, so a later view takes a few milliseconds and shows it with the page.++## Git's safety++www runs `git log`, `git show` and `git rev-parse` in the directory of what the page shows, after links resolve.++A cloned repo's `.git/config` can name programs for git to run. So www turns off external diff, textconv, signature checks and fetches, and drops `GIT_` variables from its environment. git's own `safe.directory` check refuses repos that other users own. -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. 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.+## On shares++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.`++## Old versions of a tree++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 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.++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. Any length of hash from four characters up works and redirects to git's short form.++Docs, code pages, listings and search work there as on the tree itself, read from git rather than the disk. Relative links stay in the same version. www's script still runs there, so keys, the search box and line marks work as on the tree.++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.++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.
@@ -1,11 +1,25 @@ # Install www needs git.+ `make install` links `build/www` into `~/.local/bin`.++## Debian packages+ `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 deb-syntax` builds `www-syntax`, the syntax tree helper, as a package of its own beside it, since the helper needs cgo and libc while www is static. The main package recommends it and the helper depends on www, each at the same version, because www reads the helper's output. For another architecture it needs a C cross compiler, eg `make deb-syntax ARCH=arm64 CC=aarch64-linux-gnu-gcc`.-`make install-deb` builds both packages while sudo asks for the password, then installs them 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 both. -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.+`make install-deb` builds both packages while sudo asks for the password, then installs them with `sudo apt install`, which pulls in git. 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 both.++Debian's needrestart lists the services that use old libraries after the install instead of asking which to restart. None of them is www.++## Port 80++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.++## Locks in git Add `*.*.*.*-*.lock` and `*.*.*.*-*.LOCK` to `~/.config/git/ignore` so locks stay out of `git status`.
@@ -11,16 +11,21 @@ journalctl --user -t www -p err ``` Each entry carries the endpoint it concerns in `WWW_ENDPOINT` and its priority in `PRIORITY`: 3 for faults, 6 for the rest. The journal also holds what explains a vanished server, such as a suspend, an OOM kill or a logout.+ Where no journal socket exists, entries go to `~/.local/state/www/log` instead, or below `$XDG_STATE_HOME` when set. Its lines hold time, PID, endpoint, priority and the message. The file is mode 600 and rotates once to `log.1` at 1 MB. `www --help` names the place in use. ## What is logged -Every run of `www`, with its arguments, working directory and binary, and its outcome when it fails. A run that joins a server or finds its tree already served says so.-Each server's start with its binary and commit, eg `/usr/bin/www 54cca2a`, and `+modified` for a build from uncommitted code. A stale binary is the first suspect when a page looks wrong.-Each tree served and withdrawn, with the cause: a deleted lock, a Delete lock or Kill server button, or a signal. Then the server's stop and exit.-Stale locks and registry links cleared after a crash, and the SIGTERM that `--kill` and `--restart` send.-Every page request, with method, path, status and time taken. Live reload streams, the stylesheet, fonts and the favicon come with every page and are left out. A shared server adds the visitor's address.-Bug reports from the bug button at each page's foot, as one line starting `bug N:` with the page's URL, the note, the window, the browser, the build and the snapshot's path, and each comment as `bug N comment:`.-Faults: render and git failures, refused links, inotify falling back to polling, and what net/http reports. A detached server's stderr goes to the same place, so a panic is kept.+- Every run of `www`, with its arguments, working directory and binary, and its outcome when it fails. A run that joins a server or finds its tree already served says so.+- Each server's start with its binary and commit, eg `/usr/bin/www 54cca2a`, and `+modified` for a build from uncommitted code. A stale binary is the first suspect when a page looks wrong.+- Each tree served and withdrawn, with the cause: a deleted lock, a Delete lock or Kill server button, or a signal. Then the server's stop and exit.+- Stale locks and registry links cleared after a crash, and the SIGTERM that `--kill` and `--restart` send.+- Every page request, with method, path, status and time taken. Live reload streams, the stylesheet, fonts and the favicon come with every page and are left out. A shared server adds the visitor's address.+- Bug reports from the bug button at each page's foot, as one line starting `bug N:` with the page's URL, the note, the window, the browser, the build and the snapshot's path, and each comment as `bug N comment:`.+- Faults: render and git failures, refused links, inotify falling back to polling, and what net/http reports. A detached server's stderr goes to the same place, so a panic is kept.++## Privacy++A secret share's token never reaches the log. Paths do, and on shares so do visitors' addresses. -A secret share's token never reaches the log. Paths do, and on shares so do visitors' addresses. Your user journal is readable by you and by members of `adm` and `systemd-journal`.+Your user journal is readable by you and by members of `adm` and `systemd-journal`.
@@ -1,16 +1,22 @@ # Online repos -The server on your default address also listens on the address after it, 127.31.N.2, on the same port. There a URL names a repo on a forge by its host and path, eg `http://127.31.1.2/github.com/junegunn/fzf/README.md`. The first visit fetches the repo into a cache, and from then on www serves it as it serves a checkout: docs, code, listings, search and history. Home and the web never collide, since each has an address of its own.+The server on your default address also listens on the address after it, 127.31.N.2, on the same port. There a URL names a repo on a forge by its host and path, eg `http://127.31.1.2/github.com/junegunn/fzf/README.md`. The first visit fetches the repo into a cache. From then on www serves it as it serves a checkout: docs, code, listings, search and history. -The index at `/` on the default address links the web address. The web address's own `/` lists the repos in the cache, each with its size and when it was last read, and how much of the disk limit they fill, and links back to the trees on the default address.+Home and the web never collide, since each has an address of its own.++The index at `/` on the default address links the web address. The web address's own `/` lists the repos in the cache, each with its size and when it was last read. It says how much of the disk limit they fill, and links back to the trees on the default address. ## Ways in The index at `/` names the web address, but a reader seldom sees the index, so two ways in lie where a reader already is. -A link in a doc to a repo on GitHub, GitLab, Codeberg or Bitbucket, or to a file or directory in one, is followed by a small www, raised like a footnote mark. It opens the same repo or file on the web address, and its title names the repo. The forge's own link stays as it was. A link to a forge page that is not a file, eg an issue or an account, gets no mark. A file link names a branch or commit, but the web address serves the commit it fetched, so the mark opens the file as it is there. The copy button leaves the mark out.+A link in a doc to a repo on GitHub, GitLab, Codeberg or Bitbucket, or to a file or directory in one, is followed by a small www, raised like a footnote mark. It opens the same repo or file on the web address, and its title names the repo. The forge's own link stays as it was.++A link to a forge page that is not a file, eg an issue or an account, gets no mark. A file link names a branch or commit, but the web address serves the commit it fetched, so the mark opens the file as it is there. The copy button leaves the mark out.++A forge URL typed or pasted into the search box, with or without `https://`, adds a result above the others, under "online". Enter opens it, since it comes first. -A forge URL typed or pasted into the search box, with or without `https://`, adds a result above the others, under "online". Enter opens it, since it comes first. A URL to any other host, eg `https://git.suckless.org/dwm`, gets no result there; paste it after the web address instead, scheme and all, eg `http://127.31.1.2/https://git.suckless.org/dwm`. The web address drops the scheme, and turns a forge's view of a file, eg `/blob/master/README.md`, into the file.+A URL to any other host, eg `https://git.suckless.org/dwm`, gets no result there; paste it after the web address instead, scheme and all, eg `http://127.31.1.2/https://git.suckless.org/dwm`. The web address drops the scheme, and turns a forge's view of a file, eg `/blob/master/README.md`, into the file. Both appear wherever a server serves the web address: on its default address and on the web address itself. A share offers neither. @@ -26,7 +32,9 @@ When another website links to a repo not yet cached, the browser marks the reque ## The cache -The cache lives in `~/.cache/www/`, or `$XDG_CACHE_HOME/www/`, one directory per repo named by its host and path. Each holds a bare clone without file contents, in `.git`, and one worktree of the commit served, such as `.wt-b412a450538e`, which the link `.tree` names. The worktree's checkout fetches every file of that commit in one request. Older file versions arrive when a page needs them: a commit's diff fetches the blobs it shows, which takes about half a second on GitHub. A version fetches a directory's files in one request, the first time its listing or one of its files opens, so its first page takes about the same.+The cache lives in `~/.cache/www/`, or `$XDG_CACHE_HOME/www/`, one directory per repo named by its host and path. Each holds a bare clone without file contents, in `.git`, and one worktree of the commit served, such as `.wt-b412a450538e`, which the link `.tree` names.++The worktree's checkout fetches every file of that commit in one request. Older file versions arrive when a page needs them: a commit's diff fetches the blobs it shows, which takes about half a second on GitHub. A version fetches a directory's files in one request, the first time its listing or one of its files opens, so its first page takes about the same. `--disk=SIZE` sets the cache's limit, 2G by default. When a fetch takes it over, www deletes the repos read longest ago until it fits. A repo larger than the limit on its own goes too, and its page says so. @@ -34,7 +42,9 @@ Delete the directory any time; www fetches what it needs again. ## Staying current -A visit to a repo fetched over five minutes ago serves the cached commit at once and fetches in the background. When the branch moved, www builds the new commit's worktree beside the old one and swaps the link, so no page sees a half-updated tree, and open pages reload onto the new commit. The header of every page in the repo names the commit shown and when it was fetched, eg `@ 52502e6 · fetched 23:40`, as a version's crumbs name a commit; the hash links the page in that commit's version, which stays put after the next fetch. When a fetch fails, the page keeps the commit it has and says the fetch failed.+A visit to a repo fetched over five minutes ago serves the cached commit at once and fetches in the background. When the branch moved, www builds the new commit's worktree beside the old one and swaps the link, so no page sees a half-updated tree. Open pages reload onto the new commit.++The header of every page in the repo names the commit shown and when it was fetched, eg `@ 52502e6 · fetched 23:40`, as a version's crumbs name a commit; the hash links the page in that commit's version, which stays put after the next fetch. When a fetch fails, the page keeps the commit it has and says the fetch failed. A fetch takes the branches and tags. It leaves out GitHub's pull request refs, which would fill a large repo's cache with commits nobody asked for. @@ -58,9 +68,17 @@ The listing names its source and the sources that failed first, so a partial lis `www --public --disk=SIZE -a ADDR` serves online repos to anyone who reaches the address, eg through Caddy in front of it. It serves no tree from the machine and no other address, and it runs in the foreground for a service manager. `--share=HOST` binds a public address directly instead. -A visitor learns nothing of the machine or of what others read. `/` says only how to read a repo. A listing that no forge or web frontend gives is a 404, never the cache. Pages write the cache's paths from `cache/`, git's errors included. No page has a stop or bug button, nothing opens a browser, and any Host header is answered, since nothing local is there to protect.+A visitor learns nothing of the machine or of what others read. `/` says only how to read a repo. A listing that no forge or web frontend gives is a 404, never the cache. Pages write the cache's paths from `cache/`, git's errors included. ++No page has a stop or bug button, nothing opens a browser, and any Host header is answered, since nothing local is there to protect.++`--disk` is required, since any visitor can make www clone.++The limits aim at a bot that makes www clone without end, not at a reader. A repo already in the cache opens at any time. A visit to an uncached path starts a read: a probe, a clone or a listing.++One address runs one read at a time and starts at most 20 within an hour. Over that it gets a 429 with `Retry-After`, and the log names the address. An IPv6 address counts by its /64. Behind a proxy on loopback, eg Caddy, the visitor is the last address in `X-Forwarded-For`. -`--disk` is required, since any visitor can make www clone. The limits aim at a bot that makes www clone without end, not at a reader. A repo already in the cache opens at any time. A visit to an uncached path starts a read: a probe, a clone or a listing. One address runs one read at a time and starts at most 20 within an hour; over that it gets a 429 with `Retry-After`, and the log names the address. An IPv6 address counts by its /64. Behind a proxy on loopback, eg Caddy, the visitor is the last address in `X-Forwarded-For`. At most four clones run at once, and a fifth waits on its reading page.+At most four clones run at once, and a fifth waits on its reading page. One leak remains: a repo already in the cache opens at once, and one that is not shows the reading page first.
@@ -1,29 +1,81 @@ # 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.+Below the prefix, URLs mirror the file tree, so relative links in the docs work unchanged. A tree inside another served tree answers for its own paths.++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.++## Missing pages++An absolute link such as `/x.md` points outside the prefix and gives 404. Its page 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. It also names the files with names close to it in the nearest directory that exists.++## Links++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.+Docs follow CommonMark with GitHub's extensions, rendered by goldmark: tables, strikethrough, task lists, bare URLs as links, footnotes and definition lists.++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.++Wiki links work as relative links do. `[[name]]` opens `name.md` in the doc's directory and `[[sub/name]]` opens `sub/name.md`. A name with an extension, eg `[[main.go]]`, keeps it. `[[name|label]]` shows the label and `[[name#heading]]` opens at the heading.++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+## Linked from++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.++## Recent pages++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. -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.+Each visit counts and recent visits weigh more, as in zoxide. A reload is no visit. -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.+Each address is an origin of its own, so each visitor to a share sees only the pages that visitor opened. 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.+Open pages reload when what they show changes.++A doc takes its new version in place, without a reload. 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. It 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, and Iosevka, the mono, come from the machine when installed and otherwise from subsets built into www, which the browser caches for a year. Each face has a core file for Latin text and www's own signs, about 25 KB for the serif and 15 KB for the mono, so a plain page fetches about 40 KB of font. A second file per face holds Greek, Cyrillic and symbols, and the browser fetches it only for a page that uses them. Text shows at once in a fallback font and switches when the file arrives.-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.+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. The dark palette is a first draft.++The browser loads the stylesheet 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.++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.++## Fonts++Pages load no fonts from the network. STIX Two Text, the serif, and Iosevka, the mono, come from the machine when installed. Otherwise they come from subsets built into www, which the browser caches for a year.++Each face has a core file for Latin text and www's own signs, about 25 KB for the serif and 15 KB for the mono, so a plain page fetches about 40 KB of font. A second file per face holds Greek, Cyrillic and symbols, and the browser fetches it only for a page that uses them.++Text shows at once in a fallback font and switches when the file arrives.
@@ -1,9 +1,29 @@ # 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.+## Symlinks++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. The listing marks it refused, with the same on hover.++## Browsers++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 it allows by hash. www's script reloads pages, marks changed blocks, walks listings by arrow and opens history folds. Raw HTML in a doc passes through, so a README in a cloned repo could otherwise run code on this origin.++## Code in a tree++www runs no code a tree supplies. It reads files and runs git with every option turned off that would start a program named in the repo's config: the pager, textconv, external diff, signature checks, fsmonitor and fetching.++## Other users++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.
@@ -1,9 +1,59 @@ # 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, a syntax tree the rows whose field, kind or text holds the query, with their folds opened, and each match is 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.+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. -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.+## What a search finds -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.+On a listing it searches the directory: names that hold the query first, then lines in text files. The directory's own entries show at full strength, and what lies in its subdirectories follows after a gap, dimmed. A line result opens a code page at its line. -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.+Any other page filters itself, and each match is marked:++- a code page keeps its matching lines with their numbers+- a doc keeps its matching blocks+- a history keeps its matching commits+- a syntax tree keeps the rows whose field, kind or text holds the query, with their folds opened++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. A share searches only what it serves.++## Keys++Escape clears the query and brings the page back. Escape on an empty query goes up, eg from a file to its directory or from a commit to its history. It highlights the page it left, so Enter goes 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. The highlight survives a reload.++Left and Right move the cursor in the box and never navigate. With nothing to walk, Up, Down and Space scroll the page.++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.++On a touch screen the box takes the keyboard only when tapped, since it would raise the on-screen keyboard.++## Copy buttons++Beside the box, ⧉ copies the page's text: what lies below the box, without www's own lines and links or line numbers. 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.++## When controls show++The search row, box and buttons alike, sticks to the top of the window as the page scrolls. All three stay at hand while reading, and a jump to a line stops below the row.++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. The crumbs, the header's status and the bug button stay.++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+- when the page scrolls up++Once shown, they fade only 2 s after the last of these ends, never at once.
@@ -1,32 +1,72 @@ # 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.+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.+ 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. +Delete the lockfile to stop serving the tree. The Delete lock button on the tree's pages does the same.++## One server per address+ 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.+Delete lock and Kill server answer 410 Gone. The page names each tree stopped, its lock removed or kept, and whether the server serves on.++## Restarts++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++The index at `/` reads the same on every server. It lists each running server in the order of their addresses, with the address and the binary it runs. Each server's trees follow, with the lock's path on hover.++The server you are reading from says "this page". 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.++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.+ 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.+Each user owns 127.31.N.0/24 with N = uid − 1000. www serves on 127.31.N.1 by default, on port 80 where it 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.+`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. 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.+## Systemd units++A systemd user unit runs `www --restart` at login. Both ship disabled. They serve from login to logout, and linger stays off.++- 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`.++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.+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. `$XDG_STATE_HOME` replaces `~/.local/state` when set.++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. 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.
@@ -8,7 +8,9 @@ The printed URL carries the address's name when its reverse lookup resolves back to it, so a share on a WireGuard address with its own DNS prints `http://mate.peer.home/notes/`. Otherwise it carries the address. The port defaults to 80 where www may bind it, else 8080, as on loopback. `-a` takes only loopback addresses, so a typo there cannot share a tree. -A shared address gets a server of its own, apart from the loopback one. More trees join it with the same `--share` address. The lock is named after the address, such as `192.168.1.5-80.lock`, and deleting it stops the share. `www --kill --share` stops every tree on that address. `www --restart --share` moves the trees to a new binary. `www -l` lists shared trees with the rest.+A shared address gets a server of its own, apart from the loopback one. More trees join it with the same `--share` address. The lock is named after the address, such as `192.168.1.5-80.lock`, and deleting it stops the share.++`www --kill --share` stops every tree on that address. `www --restart --share` moves the trees to a new binary. `www -l` lists shared trees with the rest. On `0.0.0.0` the printed URL carries the machine's name, since the server answers on every address. `0.0.0.0` claims its port on every address, so it cannot bind a port that any listener holds, the loopback www included. www then fails with `E98` and names the listeners on that port. Share on one address, or give `0.0.0.0` another port. @@ -16,7 +18,13 @@ On `0.0.0.0` the printed URL carries the machine's name, since the server answer A shared tree mounts at its base name, eg `http://192.168.1.69/notes/`. The URL does not reveal where the tree lives on the machine. -The server serves only what other users on the machine could read. A file needs the others-read bit, and every directory from the tree's root down to it needs others-read and others-execute. Links out of the tree follow the `.www-follow` rule as on loopback. Names that begin with a dot are neither listed nor served, at any depth, nor are links that lead to them inside the tree: `.git` holds the history and every deleted file. History pages show commit messages alone, without trailers, and only when other users could read the repo. Everything else in the tree is served, so share a directory that holds only what you mean to share.+The server serves only what other users on the machine could read. A file needs the others-read bit, and every directory from the tree's root down to it needs others-read and others-execute. Links out of the tree follow the `.www-follow` rule as on loopback.++Names that begin with a dot are neither listed nor served, at any depth, nor are links that lead to them inside the tree: `.git` holds the history and every deleted file.++History pages show commit messages alone, without trailers, and only when other users could read the repo.++Everything else in the tree is served, so share a directory that holds only what you mean to share. A refused entry shows as refused, without the reason, since the reason names paths outside the tree. The index at `/` lists the shared trees by name. It shows no lock paths, no other addresses and no buttons. No page has a Delete lock or Kill server button, so no visitor can stop the server.
@@ -1,9 +1,27 @@ # 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.+www is fast. A page from a running server takes one to three milliseconds and a server starts in about 40 ms. A test in `make test` fails any page that takes over 5 ms.++## Git's answers++A server keeps git's answers, so most pages run no git at all. It keeps which repo each directory lies in, the histories pages show and the commits they open. Commits never change and are kept by their full hash. `--cache` sets the memory for all of it.++The server reads HEAD from the repo's files, and a history is kept under the commit HEAD names. A new commit misses the cache, so nothing shown goes stale. A `.git` that appears or goes makes the server ask git again. -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.+## Slow histories++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`. That builds git's index of the paths each commit changed and makes such histories several times faster. www never writes to the repo itself.++## Benchmarks++`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. Each language is timed three ways: 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.