The Windows edition of Local Route Launcher is not a port. It is the same argument made twice, in two languages that agree about almost nothing except where a window goes.
Version 1.2.0 was the first release with both. Most of the work on the second one was not code. It was deciding which sentences both builds had to make true, then finding the places where they quietly stopped being true.
The specification is shorter than the feature list
Strip the platforms away and the tool promises eight things:
- Accept a proxy endpoint, check that it is well formed, and remember it.
- Launch one named application with that endpoint scoped to that launch only.
- Leave the system proxy, DNS and routing configuration alone.
- Report three separate connection checks, each saying what it measured.
- Ask the application to close, and never force it.
- Stay reachable from the menu bar or the notification area when the window is gone.
- Switch between English and Simplified Chinese immediately, and remember the choice.
- Look deliberate in light and dark.
That is the whole product. Written as a list, it fits on one screen, and every item is a sentence a user can verify. Written as a codebase, it is two applications that happen to share a name.
Why not one runtime
The obvious alternative is a single codebase in a webview shell. I did not take it, and the reason is not taste.
Nearly every operation here is an operating-system operation: enumerate processes, read their arguments, inspect a PID-scoped connection table, ask another application to quit, place an icon in a system tray, install per user without administrator rights. A webview would supply the window and then hand all of that back to native code through a bridge. The download would grow by an order of magnitude for a utility whose window is usually closed.
So the choice was between one implementation with a bridge and two implementations without one. I chose the second and accepted the cost: every behaviour is written twice, and the two copies will drift unless something makes them agree.
What actually got shared
No code crossed over. What crossed over was wording, rules and order of operations:
- Validation rules. The same definition of a usable endpoint on both sides, including the cases that should be refused rather than corrected.
- The labels of the three checks. Identical in both builds, because a user comparing screenshots of two platforms should not have to work out whether “tunnel” means the same thing.
- The structure of the language catalogue. Separate files, but the same keys in the same order, so a missing string is visible rather than silent.
- One version number. Both platforms read the same
VERSIONfile, which is the subject of another note. - The release checklist. The same steps, in the same order, with two platform-specific branches.
That is a thinner kind of sharing than a shared library, and it is the kind that survives. A shared abstraction would have to model process inspection and tray icons; the model would be wrong on one platform within a release, and then both builds would inherit the error.
The parts that refused to be the same
Process inspection. On macOS the launcher reads process arguments and socket state directly. On Windows it combines PowerShell and CIM queries with a native, PID-scoped TCP table. The intent is identical; the mechanics have nothing in common. Paths and user settings are passed as an argument vector and never interpolated into a command string, because a path with a space or a quote in it is not an edge case on Windows — it is Tuesday.
Environment inheritance. Windows environment blocks are case-insensitive and macOS ones are not. A variable written as http_proxy and read as HTTP_PROXY is one key on one platform and two keys on the other. Getting this wrong produces a launcher that works on the machine it was written on and mysteriously does not on someone else's.
Asking an app to close. macOS goes through Launch Services; Windows posts a close request to the main window. Neither build terminates the process. Killing another application is how you lose someone's unsaved conversation, and a utility that can do that is not trustworthy with a single setting.
The tray. An AppKit status item and a WinForms notification icon hosted inside WPF look similar and share nothing.
Settings storage. macOS keeps the existing preferences domain, which also survived a product rename. Windows writes atomic JSON under the user's local application data. Both install per user, neither needs administrator rights, neither registers a service, neither starts at login.
Testing something that needs an account you do not have
The uncomfortable constraint: the application under test is a third-party desktop app that requires a signed-in account, and CI has no account, no display, and nobody to click anything. The way around it is a fixture — a separate, deliberately trivial application built for validation only, carrying the same executable name the launcher looks for. It is never bundled into an installer. Tests install the launcher to a path containing spaces, start the fixture through it, and check what the fixture actually received: arguments, environment, duplicate-launch rejection, graceful shutdown. Then they cover the four language and appearance combinations, closing and reopening the window, and uninstalling.
The macOS side took a different route for the same reason. The core checks are compiled into small executables with swiftc and run directly, with no test framework in between. The Windows core project builds and tests without WPF at all, because endpoint validation and preference writes do not need a window.
What none of this proves is that a real signed-in account works. The release notes say so in the same paragraph as the list of checks, which is the only honest place to put it.
Parity is a list of sentences, not a diff
Two builds will never be identical, and pretending otherwise produces a feature matrix nobody reads. What can be identical is the list at the top of this page. When a platform cannot honour an item, the page for that platform says which one and why, in the same place the user is already looking.
Two implementations cost more than one. For a tool whose entire purpose is to touch as little of the machine as possible, it was also the only way to keep it small.