Skip to content
MoreDisplays

Type to search.

Development log

Not a changelog. Nothing has been released, so there are no versions to list. These are dated milestones, and the ones with something proven on real hardware say so.

Subscribe by RSS

The update check now happens on its own, once a day

The app reads one small file from this site once a day and says so in the menu bar when there is a new version. Nothing is downloaded or installed without being asked, the version can be skipped, and the whole thing can be switched off.

The updater has been in the app since 1.2, and it only ever ran when somebody chose Check for Updates. That is a fine design for anybody who thinks to look, and no design at all for everybody else: this app is distributed outside the App Store, so nothing on the machine was ever going to mention that a fix existed.

It now looks by itself, once a day, and the menu bar item says Update to 1.6… when it finds something. That is the entire change on the outside.

What did not change

The three steps and the gaps between them. The daily look reads one JSON file, the same one this site’s download page is built from, and stops there. Nothing is fetched, nothing is verified, nothing is replaced. Taking the update is still two deliberate clicks, and a downloaded build still has to satisfy its size, the published checksum, and macOS itself on notarisation and team before it goes anywhere near Applications.

A background check that reached for a two megabyte disk image on somebody’s tethered connection would be a check they were right to resent.

The three ways out

Switch it off. Check for updates automatically is in Settings, on the General tab, next to the button. It is read every time the check is about to run, so turning it off stops the next one rather than the one after a relaunch.

Skip a version. The offer carries a Skip button. It silences the daily check for that one version and nothing else. Being offered 1.6 again after declining it is nagging; being offered 1.7 afterwards is the point.

Never leave it running. A copy that is not running makes no requests at all.

Quieter than it sounds

The check is silent unless it finds something, and a check that cannot reach the site is written to the log and forgotten. A site that was briefly unreachable at three in the morning is not something to greet anybody with, and an error waiting in a window the next morning about a question nobody asked is worse than no answer.

There is no alert, no notification and no window brought forward. A display utility that interrupts somebody to talk about itself has misjudged how important it is.

How the project asks for money

Voluntary. Nothing for thirty days, then a reminder once a month, and a licence key stops the asking. Stopping the asking is the only thing a key does.

Nothing in the app is locked, limited or switched off, and that is not changing. What is new is that it asks to be paid for.

Nothing at all for the first thirty days: somebody deciding whether this is useful should not be interrupted while deciding. After that, once every thirty days. Not per launch and not per week, on the grounds that a reminder which appears constantly is not a reminder, it is a reason to stop using the software.

A key ends the asking permanently, and that is the only thing a key does. No feature is behind one, nothing expires, no limit is lifted.

The policy is the part most likely to be got wrong in a way nobody notices, so it is a pure function with tests on it, down to the detail that a clock which has gone backwards does not earn anybody a second reminder for travelling east.

Keys are checked on your own Mac against a number inside the app. No account, no activation, no server, and nothing to know about who entered one. Anybody who wants to forge a key can read the secret out of the binary, which is true of every offline check ever written; this one is a receipt rather than a lock, and its job is to tell a real key from a mistyped one.

They are written in Crockford’s base32, without I, L, O or U, so nothing in a key can be misread as a digit when it is read down a telephone.

Where it is

It arrived in version 1.2. The reminder appears once, thirty days in, and the Support tab in Settings carries the addresses, the suggested amount and the field a key is typed into. Ten euro is the suggestion; nothing checks whether anything arrived, and nothing can.

Verified on hardware

A privileged service, shipped but not yet serving

The app now carries a launchd service that runs as root from boot, installed from a switch in Settings and reported by mdctl status. It does not present a display at the login window yet.

The login window is the one screen this app has never been able to cover. It runs in your user session, so before anybody logs in it is not running.

Closing that gap needs a service running as root from boot, and the app now ships one. It lives at Contents/MacOS inside the bundle with its launchd description at Contents/Library/LaunchDaemons, which is where SMAppService insists on finding it. Shipping it inside the app is what keeps uninstalling honest: drag the app to the trash, plus one unregister, rather than a hunt through /Library.

It was proven on hardware first. A LaunchDaemon can create a virtual display, and it can also select a mode on one, which was the open question: CoreGraphics builds its mode tables from reconfiguration events delivered to a process running an application event loop, and a daemon does not have one. If that had failed, every display at the login window would have arrived at the wrong resolution. So the service checks itself, by creating a display through the same code the app uses and asking the machine whether it is the size that was asked for.

What it does not do

Serve. The last step is missing: the service starts, logs and exits, and no display appears at the login window. There is nothing to gain from switching it on in this version, and the page about it says so at the top rather than at the bottom.

What is already worth having

Consent asked in the app’s own words before macOS asks in its own, so a request to approve something running as root does not arrive unexplained. Declining as a real answer that is remembered. And a service: line in mdctl status, because this service exists for a Mac reached over the network, where “have a look in System Settings” is advice nobody at the far end can take.

The app can update itself, in three deliberate steps

Checking, downloading and installing are three separate actions with gaps between them, and a downloaded build has to satisfy a checksum and macOS before it goes anywhere near Applications.

Until now the only way to hear about a new version was the feed on this site. There is now an updater in the app, and no third party framework behind it.

Three steps, and the gaps between them are the design. Checking asks this site what it is offering and names what it found, downloading nothing. Downloading fetches the image and verifies it, and stops there. Installing replaces the running app and restarts into it, which the window says before you press it. Two tests hold those gaps open: that checking downloads nothing, and that a download which does not match never becomes ready to install.

What a build has to satisfy

Three checks, in this order. The size, because a wrong size is cheap to notice and means the rest are about the wrong file. The checksum this site published, derived from the image rather than typed by anyone. And then the signature: macOS must agree the build is notarised and signed by this team.

The third is the one that matters. Bytes matching a checksum served from the same place as the bytes prove nothing on their own. That check is what makes a hijacked website a broken update rather than a compromised Mac.

An update that needs a newer macOS than this Mac runs is reported as exactly that, rather than hidden. “There is an update you cannot have, and why” is a better answer than pretending to be current.

Where it is

It arrived in version 1.2. At the time nothing was checked in the background at all: the check happened when you chose Check for Updates from the menu bar item, and at no other time. That changed later: the app now also looks once a day, which can be switched off.

The download is a zip, and the release is one command

The app itself, stapled and zipped, is now the button people press. The disk image stays for the in-app updater, and both are cut from the same signed artefact by a single command.

A disk image has one practical weakness as a web download: almost no web server ships a MIME type for .dmg, so it goes out as a generic byte stream and the browser is left to invent a filename. A zip does not have that problem anywhere.

So the zip is now the button, and it is the app itself rather than an archive of a disk image. Unzip, drag to Applications, done.

Both, and why

The disk image has not gone. The updater inside the app mounts what it downloads, so the address in this site’s release.json still points at the image; changing that would break updating for everyone who already has a copy. The two are cut from the same artefact: the zip is made by mounting the notarised image and taking the app out of it, so they cannot be different builds.

One detail that had to be got right. Notarisation staples its ticket to the disk image, not to the app inside it, and an app taken out of the image therefore has no ticket of its own. It still opens, but only on a Mac that can reach Apple to ask. The app is stapled on the way into the zip, so a first launch works with no network.

One command

make publish now goes from the working tree to a directory ready to upload: it clears the last build, signs and notarises a new image, derives every fact this site states from that image, builds the site and leaves the zips ready.

The reason it exists is a failure with no symptoms. A disk image built before the last source change is signed, notarised, mounts, and passes every check, while missing the change it was cut for. It happened twice in one afternoon. The site’s release step now refuses an image older than the newest source file, so the mistake is caught by a machine rather than remembered by a person.

The group library travels between Macs

Groups sync over your own iCloud, on by default, with dated local backups and a restore that backs up first.

Someone with two Macs almost always wants the same groups on both, so sharing is on by default.

What travels

The groups themselves, and the record of which ones were deleted. Nothing else. Which group is active and every setting stay on the machine they belong to, because a rack Mac and a laptop want the same library and emphatically not the same active group.

How two machines agree

Every group carries when it last changed, and the newer copy wins. Reordering the sidebar does not count as a change: order is a property of the list, not of the group.

Deletions are recorded rather than simply performed. An absence cannot travel, so without a record a group deleted on one Mac reappears from the other forever.

Backups

Dated copies of the whole configuration, listed with when each was taken and how many groups it holds. Restoring takes a backup of the current state first, so restoring the wrong one is itself recoverable. They are plain files with a timestamp in the name, because a backup format only this app can open is a backup nobody trusts.

Sync and backups has the detail.

Verified on hardware

A hardware smoke test for the private interface

make smoke creates a real 5120x1440 virtual display, confirms it is active and unmirrored, and releases it.

Virtual displays come from an interface Apple does not document and can change without warning. The only honest way to know whether it still works on a given machine and a given macOS is to try it.

make smoke creates one 5120 by 1440 virtual display, confirms it appears in the active display list unmirrored at the right resolution, holds it for five seconds, releases it and confirms it is gone. It never runs in CI, because it needs real hardware.

Run it on any new machine, and again after every macOS update. It is the fastest way to find out whether an update broke the interface, and it answers that question in about ten seconds rather than after an afternoon of confusion.

Verified on hardware

Switching groups verified on real hardware

A group was applied on a real Mac, a second group replaced it with no gap, and deactivating left the machine exactly as it started.

The first end-to-end proof that the core feature works outside a test harness.

A group was applied through the running app. Its virtual display appeared at the requested resolution, at the position its layout specified, and the main display did not move. Switching straight to a second group replaced that display with the second group’s: one command, and no intermediate state with nothing on screen. Deactivating left the machine exactly as it started.

Still unverified on hardware

Sleep and wake, logout and login, connecting or disconnecting a monitor while a group is active, a cold boot with activate-on-launch, deleting the active group, a shortcut another app already owns, and a crash while virtual displays exist.

That list is recorded in the known issues on the support page, rather than left to be remembered.

Packaging that refuses to lie about what it produced

make dmg signs, notarises and packages, and says loudly when it has produced a local test build instead of a release.

The release path exists, and it is deliberately noisy about what it just made.

Without a signing identity and a notary profile it still builds a disk image, and it says clearly that the result is a local test build. A packaging step that quietly produces an unsigned artefact named like a release is how an unsigned release gets shipped.

Signing also refuses when the provisioning profile grants something other than what the app asks for. That check exists because the iCloud entitlement is restricted: it counts only when the bundle embeds a profile granting exactly it, and a mismatch would produce a build where sharing silently never works.

The app icon is generated from a script rather than checked in as an opaque binary, so the shapes live in a diff that can be reviewed.

Following along

The RSS feed is the only notification this project offers. There is no newsletter and no account, and the repository stays private until there is something to release.