ADR 0004: State-aware desktop setup launcher
- Status: Deferred (2026-07-17; originally Proposed 2026-07-10)
- Date: 2026-07-10
- Deciders: Matt
- Related issues:
mdo-u79,mdo-gfw,mdo-bs3.4
Deferral (v0.6)
Full integration-state management is deferred. v0.6 ships only a cheap owned-registration check; the full manager described below would add cross-platform lifecycle complexity without evidence that it solves a common user problem.
What v0.6 actually ships (see mdo-bs3.4.1):
- Setup asks one question of the platform: is mdo's own handler
registration present? On Windows that is the mdo-owned
mdo.mdProgID open command underHKCU; on Linux it is the mdo-ownedmdo.desktopentry in the XDG applications directory. - When it is present, setup says the integration is already installed and offers exactly two outcomes: leave it unchanged, or reinstall (an idempotent rewrite of mdo-owned resources).
- The check does not infer partial, stale, system-wide, or effective-default states, and a failed query is treated as "not detected", falling back to the ordinary first-time flow.
Everything else in this document — the IntegrationReport state model,
handler-health classification, repair flows, launcher lifecycle status,
effective-default queries, binary-removal guidance, and the associated test
matrix — is the deferred full manager. It is retained below as design
reference for a future decision, not as an approved plan. In particular, this
ADR does not authorize comprehensive partial-state recovery: the minimal v0.6
check must not grow repair or remnant-cleanup behavior without a new decision
reopening this ADR.
Context
mdo-setup currently opens the first-run terminal tour. The tour always asks
whether to install file-manager integration, even when that integration is
already installed. Linux also ships no visible desktop entry for mdo-setup:
the only entry created by integration setup is the deliberately hidden
mdo.desktop file handler. Windows packages expose mdo-setup.exe as a
command, but do not consistently create a Start menu shortcut.
The setup surface should remain useful after onboarding. It should report the current integration state, offer only actions that make sense in that state, remove integration cleanly, and explain how to remove the binaries without trying to delete the running executable.
Recommendation
Build the state-aware setup flow, but treat it as a small integration manager, not as a general uninstaller. The value is strongest on Linux, where a visible application-menu entry fixes the current discoverability gap. The same state model is worthwhile on Windows and prevents repeat runs from presenting a misleading install prompt.
Keep the persistent setup launcher separate from the file-handler registration. Removing “Open as HTML” must not remove the launcher. A full cleanup action may remove both, then print the appropriate binary-removal commands; it must not delete or replace running binaries.
State model
Expose a platform-neutral report rather than only a three-value enum:
IntegrationReport {
availability: NotInstalled | OpenWith | Default,
launcher: Missing | Installed | Stale,
details: platform-specific observations and warnings,
}
availability drives the normal prompt. launcher and details preserve
partial, stale, and unqueryable states without pretending that they are a
healthy OpenWith installation. An I/O or platform-query failure is an error,
not NotInstalled.
In implementation, model handler health explicitly (Healthy, Partial, or
Stale) so a malformed registration can retain its observed availability
without being presented as ready to use.
The contextual flow remains one decision per run:
| Current state | Primary choices |
|---|---|
NotInstalled | Install for Open With; install and request default; keep unchanged |
OpenWith | Keep; request/set default; remove integration |
Default | Keep; remove integration |
| Partial or stale | Repair; remove remnants; keep unchanged |
“Complete cleanup” is a secondary, explicitly named action. It removes the file-handler registration, launcher registration, and mdo-owned icon, then prints binary-removal guidance.
Linux design
The handler is installed when
$XDG_DATA_HOME/applications/mdo.desktop exists, is a valid desktop entry,
and its Exec target is usable. A missing or stale target is a partial state,
not an installed state.
Query the defaults with:
xdg-mime query default text/markdown
xdg-mime query default text/x-markdown
Report Default only when mdo.desktop is the effective default for every
Markdown MIME type that resolves on the host. A split result is a partial
state and should offer repair. If xdg-mime is absent or fails, preserve the
known installed state but report that the default is unknown; do not silently
classify it as non-default. This matches the freedesktop lookup model, where
defaults can come from multiple desktop-specific and XDG configuration/data
locations rather than only the file currently edited by mdo. See the
MIME Applications specification.
Install a second file,
$XDG_DATA_HOME/applications/mdo-setup.desktop, with NoDisplay=false,
Terminal=false, no MimeType, and an absolute, desktop-entry-escaped Exec
path to mdo-setup. The existing mdo.desktop remains NoDisplay=true and is
the only entry associated with Markdown MIME types.
Do not let normal integration removal break the persistent launcher's icon. Either give the launcher a separately owned icon or retain the shared icon until both handler and launcher registrations have been removed.
The launcher installation vehicle is deliberately layered:
install.shinstalls or refreshes the per-user launcher after copying the binaries. This is the primary Linux release path and fixes discoverability immediately.- Homebrew installs a launcher template as package data.
mdo-setupinstalls the per-user launcher when first run, because a formula should not mutate a particular user's home directory during package installation. cargo installhas no post-install hook. Document that users can runmdo-setuponce from a shell; successful direct setup invocations idempotently repair/register the launcher. The hosted installer uses the internal--register-launcher-onlymode after copying the binaries.
The fallback cannot solve first-launch discovery for Cargo/Homebrew users; the
documentation must say that Linux users initially run mdo-setup from a shell
unless their installer created the application-menu entry. It must not imply
that double-clicking a bare extensionless binary is reliable.
Windows design
Registration is OpenWith only when the mdo.md ProgID, its open command,
and the .md\OpenWithProgids value all exist and point to a usable mdo handler.
Missing pieces or a command that targets a missing executable are partial or
stale and should offer repair.
Determine Default by querying the effective .md association through the
Windows association API, not merely by checking the mdo-owned HKCU keys.
Windows merges machine and per-user class registrations, and the user's
effective default is distinct from an application's ProgID registration. The
read-only UserChoice\ProgId value may be a fallback observation, but mdo must
never write UserChoice or assume it can reproduce Windows' protected data.
The user must choose defaults through Windows UI; mdo should register itself as a
candidate and open the appropriate Default Apps/“Open with” UI rather than
writing UserChoice. This follows Microsoft's
Default Programs guidance
and HKCR merge behavior.
Create a per-user Start menu shortcut named “mdo Setup” that targets
mdo-setup.exe. Scoop should declare it with the manifest's shortcuts field.
Other portable install paths, including Cargo and the current WinGet portable
manifest, should use an idempotent launcher-registration fallback; a successful
direct setup launch should repair the shortcut. This is a future Windows
implementation detail, not a command exposed by the current release. If WinGet
cannot express the shortcut for the portable package, retain this fallback rather
than changing installer technology solely for the launcher.
The hosted Windows installer script should register the shortcut after copying the binaries, just as the hosted Linux installer registers its desktop entry.
Uninstall removes only keys and values owned by mdo, the installed icon, and
the mdo Setup shortcut. It must not delete the .md mapping itself or overwrite
another application's default. Microsoft's file-type guidance likewise says
to remove owned ProgIDs while leaving file-type mappings alone when ownership
may have changed; see
How to register a file type.
Binary removal guidance
Do not guess one definitive install method from the executable path. Prefer a small install receipt written by packaging/setup when available. Without a receipt, print labeled possibilities:
cargo uninstall mdo-cli
brew uninstall mdo
winget uninstall Maphew.Mdo
scoop uninstall mdo
rm -f ~/.local/bin/mdo ~/.local/bin/mdo-open ~/.local/bin/mdo-setup
Only display commands relevant to the current OS and detected tools/path. The setup process does not execute them.
Package-manager removal may bypass mdo-setup and leave per-user handler state
behind. Add uninstall hooks where the package format supports them; otherwise
show a prominent pre-uninstall cleanup command in package notes and tolerate
stale registrations if users remove binaries first.
Implementation boundaries and tests
Keep status inspection and state transitions in file_manager; keep terminal,
dialog, and shortcut/desktop-launch behavior in mdo-setup. Launcher
registration should have explicit install, status, and remove functions so
integration removal cannot accidentally remove it.
Before shipping, cover:
- Linux missing, healthy, stale, split-default, and failed-
xdg-mimestates using isolated XDG directories and a fake command runner. - Desktop entry quoting,
NoDisplay, MIME ownership, idempotent repair, and removal that preserves unrelatedmimeapps.listentries. - Windows complete, partial, stale, and effective-default states using a
registry/association-query abstraction; verify that no
UserChoicevalue is written. - Start menu shortcut install/repair/removal and paths containing spaces.
- Every UX state transition, including declining an action and complete cleanup.
- Packaging checks that
install.shcreates the Linux launcher and Scoop declares the Windows shortcut.
Consequences
The setup launcher becomes a durable control surface and Linux gains a real application-menu entry. The handler and launcher lifecycles remain independent, and default-app changes respect each platform's ownership rules.
The cost is a richer status result, platform query abstractions, and packaging work in addition to the prompt change. Partial and unknown states must be tested explicitly; collapsing them into the three happy-path labels would make the UI simple at the expense of correctness.