Skip to content

A page that will not load no longer strands you - #126

Merged
PathGao merged 3 commits into
mainfrom
fix/a-page-that-will-not-load-no-longer-strands-you
Aug 31, 2026
Merged

A page that will not load no longer strands you#126
PathGao merged 3 commits into
mainfrom
fix/a-page-that-will-not-load-no-longer-strands-you

Conversation

@PathGao

@PathGao PathGao commented Aug 31, 2026

Copy link
Copy Markdown
Owner

What this is

Two faults, both found in one sitting while running the registry's own entries as wallpapers.

Nine entries come out of sites/, each one tried and each one failing there rather than failing review: a dead room URL (Douyu), a page that loads and draws nothing (WebClock), a page slow enough to hit the load timeout more often than not (Polish TV Clock), a WebGL scene that is almost entirely black unless something stirs it and nothing stirs a wallpaper, two clocks whose CSS makes them transparent — which on a wallpaper is a second layer over the desktop rather than a clock on a background — a login sheet that comes up transparent and cannot be got through (Google Calendar), an interactive drink picker, and Gitstalk, whose URL carried the maintainer's own username so that every user's wallpaper showed the maintainer's GitHub activity. Reasons are in sites/NOT-INCLUDED.md, next to the platforms that were never added, so a re-submission has something to beat.

And the panel stops disabling a display's column while a page is on its way to it.

Mechanism

The column. DisplayPanel put .disabled(column.isLoading) over the whole column, with a 0.45 veil over most of it. The comment argued that every control there would be aimed at a display already on its way somewhere, and that a load "lasts a few seconds and lets go by itself, so nothing has to be exempt from it". The second half is what was wrong. WallpaperScene.loadTimeout is thirty seconds, and a page that never answers holds the column for all of it — so the control that would take the display off the page that is stuck, the website chooser, was disabled along with everything that had a reason to be. The one page you most want to leave was the one page you could not leave from here. Adding a different site from the gallery still worked, because that window is not this column, so the way out of a stuck load was a different window entirely. That is how this was noticed.

Nothing was relying on the blanket disable. Every control that must not act while the page and the website disagree already says so itself, and is the reason hasPage exists: Crop and Mute both ask it, and it is false for the whole of a swap because hasLoadedItsWebsite compares the page against the website the display is heading for. Both also refuse the case inside beginCropSelection and toggleSound, for the keyboard paths that never touch a button. Browsing Mode deliberately does not ask it, and its own comment already argued for this change — a failed load is the moment a user most wants to reach the page by hand. The arrows and the mode ask canRotate. What is left is the two choosers and the power switch, all three of which are requests to go somewhere else, which is what a stuck load makes urgent rather than premature.

Choosing during a load was already safe underneath, which is why this is a deletion and not a mechanism. loadBySwapping cancels pendingLoad, replaces pendingWebView synchronously, and its defer clears that only when it is still the same object — so an abandoned load cannot report over the newer one, and cannot clear the newer one's busy flag. Its three Task.isCancelled checks all return before adopt, and the timeout task is cancelled by its own defer. The plain path re-enters WKWebView, which drops the previous navigation itself.

The spinner beside the chooser is unchanged and is now the only thing a load does to the panel.

The registry. Three places pointed at entries that can be deleted, which is the shape where two things answer the same question and nothing makes them agree. The first-launch line in Add Website typed one site's address into the field — the catalogue's answer written out a second time in Swift — so the suggestion outlived the entry. It is now a description of the range, in the README's own words, with nothing left to keep in step. sites/README.md built its worked example out of helvetictoc.yml, which this PR deletes; it uses a surviving entry. CANDIDATES.md still recommended the fluid simulation on the grounds that "left alone it is colour in motion", which is exactly what it was removed for not being.

Same shape in the tests: SiteCatalogTests asserted published().count == 38, the registry's size stated a second time in a place nothing updates. Every site added or removed failed that test while saying nothing about the decode failure it exists to catch. It now compares against the number of YAML files in sites, which is the invariant it wanted — a generator that drops entries breaks it, and adding a site does not.

Scope

Left alone: the gallery's Add/Added button. It reads "is this address in your list", so deleting a website offers it again and editing a website's URL offers the original again, but editing only its CSS, zoom or interval leaves the row stuck on "Added" with no way to get the recommended settings back. The obvious fix — compare the installed website against the entry and offer "Added" only when they match — is wrong here, and bilibili-live.yml is why: its URL is half-finished on purpose, meant to have a room appended. Every user who completes it would read as drift from the entry. Worth solving, not worth guessing at in this PR.

Left alone: two app faults these entries exposed. Google Calendar's sign-in sheet comes up transparent over the wallpaper and Browsing Mode cannot be reached to get through it — requiresLogin is not enough for that page, and removing the entry hides the fault rather than fixing it. And what should show through a page whose CSS makes it transparent is an open question; two entries were removed for landing on the wrong side of it, and nothing here decides it.

Not left alone, because it had to be: one line of indentation in DisplayPanel.picker. chooser's first argument sat flush against the left margin, which put the three arguments under it out of alignment with it. CI's lint job is swiftlint lint --strict, so those three counted as violations and exited 2 — every pull request against main was red on a file it had not touched, this one included. Its own commit, whitespace only, and repo-wide strict lint is clean after it. Worth knowing separately: the last three runs on main failed every job, not only this one, so there may be more to it than the indentation.

Left alone: clock and calendar in sites/schema.json. Both tags now have no entries, but the schema is the vocabulary a contributor may use, not an inventory of what is currently in the directory. A clock that works is a wanted contribution.

Verification

macOS 26.6.2, Apple silicon.

swift test: 222 tests in 49 suites, all passing. Xcode build clean. swiftlint clean on every file this changes; the three warnings noted above are on main. Tools/check-strings.py against the build: 269 strings, every one in the catalogue and shown by something — the two removed strings are gone and the new one is reached. Tools/validate-sites.py: 29 entries, 0 problems, and the eight featured ranks are the same eight in the same order.

The new guardrail was checked the only way a guardrail is worth anything: .disabled(column.isLoading) was put back on modeButtons and the test went red, naming the second reader. Restored, green.

What was not verified by hand: the panel change itself. It was not run. No display was driven, no page was made to hang, no chooser was clicked mid-load. The reasoning above is read off the source, not observed — the cancel path in particular is an argument about pendingWebView identity and Task.isCancelled, not a measurement. What a person should do before trusting it: add a URL that never answers, pick it on one display, and while the spinner is up, use that same column's chooser to pick something else. The stuck load should be abandoned, the new page should arrive, and the column should never have been grey. Worth doing on two displays, since the argument for disabling was about one column speaking for another.

PathGao added 3 commits September 1, 2026 06:36
Each was run as a wallpaper and failed there: a dead link, a page that draws
nothing, a transparent page that is a second layer over the desktop rather than
a clock, a URL carrying the maintainer's own username. Reasons are in
sites/NOT-INCLUDED.md so a re-submission has something to beat.

Three copies of the registry pointed at entries that no longer exist. The
first-launch line in Add Website typed one site's address into the field, so
the suggestion outlived the entry; it is now a description of the range, with
nothing to keep in step. sites/README.md built its worked example out of a file
that has been deleted; it uses a surviving one. CANDIDATES.md still recommended
the fluid simulation on the grounds it is colour in motion, which is what it
was removed for not being.

The published-catalogue test asserted a count of 38, so every site added or
removed failed it while saying nothing about decoding. It now compares against
the number of YAML files in sites/, which is the invariant it wanted.

29 entries, 8 featured, ranks unchanged. 221 tests pass.
The panel disabled a display's whole column while a page was on its way, on the
argument that the controls would be aimed at a display already going somewhere,
and that a load 'lasts a few seconds and lets go by itself, so nothing has to be
exempt from it'. The second half is false. A page that never answers holds the
column for the full thirty-second load timeout, and the website chooser — the
one control that takes the display off the page that is stuck — was disabled
along with everything else. The only way out was to add a different site from
the gallery, which is another window.

Nothing depended on the blanket disable. Crop and Mute ask hasPage, which is
false for the whole of a swap because hasLoadedItsWebsite compares the page
against the website the display is heading for; both also refuse the case
themselves, for the keyboard paths. Browsing Mode deliberately does not ask it,
and its own comment argued for this change already. The arrows and the mode ask
canRotate. What is left is the two choosers and the power switch, all of which
are requests to go elsewhere.

Choosing during a load was already safe underneath: loadBySwapping cancels
pendingLoad, replaces pendingWebView, and its defer clears that only when it is
still the same object, so an abandoned load cannot report over the newer one.
The plain path re-enters WKWebView, which drops the previous navigation itself.

The spinner beside the chooser is unchanged and is now the only thing a load
does to the panel. A test holds that: one reader of column.isLoading in the
file. Verified by putting the disable back and watching it fail.

222 tests pass.
One line in DisplayPanel.picker sat flush against the left margin, which put the
three arguments under it out of alignment with it. swiftlint lint --strict is
CI's lint job, and it counted three violations and exited 2, so every pull
request against main was red on a file it had not touched.

Whitespace only. Repo-wide strict lint is clean after it.
@PathGao
PathGao enabled auto-merge (squash) August 31, 2026 22:54
@PathGao
PathGao merged commit f62e8fc into main Aug 31, 2026
5 checks passed
@PathGao
PathGao deleted the fix/a-page-that-will-not-load-no-longer-strands-you branch August 31, 2026 22:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant