midi-harbor/specs/010-graphical-interface/research.md
James Coleman ebe6302baf build(gui): update libcosmic to master 03d7dcb832bc
- The interface moves from 87ab8179 to libcosmic master of 2026-09-24, which brings its newer winit and accesskit forks and upstream's menu, context-menu and maximized-window fixes.
- The accesskit_winit build failure newer libcosmic has on Windows does not reach this build: macOS and Windows disable libcosmic's default features, a11y with them, so no accesskit crate compiles there. Enabling a11y on Windows would need a patched accesskit, as the pin's comment notes.
- The lock keeps gpu-allocator on windows 0.62.2, the version wgpu-hal 28.0.1 passes it; the update had re-resolved it to an older windows, breaking the Windows build with mismatched Direct3D types, and a later cargo update can do so again.
2026-09-29 14:35:56 -05:00

167 lines
10 KiB
Markdown

# Research: Graphical Interface
Entries from the research log, under their original numbers. Findings marked **VERIFIED** were
proven in this repository; **ASSUMED** ones rest on the literature.
---
## R-001: Can libcosmic build and run on macOS?
**Decision**: Yes. Use libcosmic for the optional GUI on both macOS and Linux.
**Status**: **VERIFIED** empirically. A spike crate depending on `libcosmic` with
`default-features = false, features = ["winit", "wgpu", "tokio", "multi-window"]` compiled cleanly
on macOS 15.6 / Apple Silicon, and a real `cosmic::Application` implementing `view`/`update` linked
and **launched a window that stayed alive**. This was the single largest risk in the project and it
is now retired.
**Rationale**: libcosmic's `Cargo.toml` already carries explicit `cfg` handling that excludes
macOS from the Linux-only dependency set (`freedesktop-icons`, `freedesktop-desktop-entry`,
`zbus`, `ashpd`, `cctk`, `cosmic-config`'s dbus path) and substitutes `phf`-based bundled icons on
non-Unix and macOS targets. The default feature set (`wayland`, `x11`, `dbus-config`, `a11y`) is
Linux-oriented and must be turned off on macOS.
**Consequences for the plan**:
- Feature selection must be target-conditional in `Cargo.toml`:
- macOS: `default-features = false`, `features = ["winit", "wgpu", "tokio", "multi-window"]`
- Linux: the default feature set, plus `wayland` and `x11`.
- libcosmic is pinned by git revision, not a crates.io version. It publishes as `1.0.0` from
`github.com/pop-os/libcosmic`; the spike used revision `87ab8179`. Pin an exact revision and bump
deliberately — this is a fast-moving dependency and the macOS path is not covered by its CI.
- Icon rendering on macOS uses bundled icons rather than a freedesktop icon theme, so the GUI must
not assume a system icon theme is present.
**Alternatives considered**: `iced` directly (libcosmic is built on it) — rejected because the user
specified libcosmic and the spike proved it viable. `egui` — rejected for the same reason.
**Bumped to `03d7dcb832bc`** (libcosmic master of 2026-09-24, nine commits after `87ab8179`) on
2026-09-29:
- The Windows failure the owner's notes (`veris/docs/libcosmic.md`) describe, `accesskit_winit`
passing `&Window` where winit now has a trait, is still there at accesskit `6c20249`, but it does
not reach this build: macOS and Windows turn libcosmic's default features off, `a11y` with them,
so no accesskit crate is compiled there. Linux builds `accesskit_winit` with its Unix backend,
which is unaffected. Enabling `a11y` on Windows would need the notes' `[patch]` of
`wash2/accesskit` to a fork with the fix, rebased onto the accesskit revision libcosmic then
uses.
- `cargo update -p libcosmic` re-resolved `gpu-allocator`'s range for the `windows` crate to 0.57,
then 0.61, which the newer accesskit and `ble-peripheral-rust` bring in. `wgpu-hal` 28.0.1 passes
it `windows` 0.62 types, so the Windows build failed with mismatched `ID3D12Device` types. The
lock file names `windows` 0.62.2 for `gpu-allocator`, as it did before the bump; check it after
any `cargo update`, which may move it again.
- The window at `03d7dcb832bc`: on macOS it opened with its menus; on Linux the AppImage's window
rendered on X11 in XFCE; the Windows build passed its tests on the test machine, but the window
was not seen there, the machine's desktop being in use.
- libcosmic's X11 attributes are still overwritten by its Wayland ones
(`iced/winit/src/conversion.rs`), so the X11 window class still comes from `argv[0]` (R-104).
---
## R-025: What building the window found that no test had
**Status**: **VERIFIED** against a running daemon on macOS (2026-09-20).
The graphical interface was the last client to be written, and writing it surfaced four defects
that the command line had been quietly tolerating. All four were in the daemon, not the window.
**Enabling a device did not reopen it.** `open_present_devices` was reached only from device
refresh, so a device switched off and on again stayed shut until the next hot-plug or restart,
while still listing as attached and accepting routes. Reconcile now brings hardware up alongside
virtual ports. Verified by sending notes through a device before and after a disable/enable cycle.
**Disabling a device called the virtual port teardown.** A device belongs to the system; only the
ports opened onto it are ours to release. It now closes rather than destroys.
**Monitoring a switched-off endpoint waited forever.** A runtime entry exists even for a disabled
endpoint, so the "is not running" check never fired and a user sat watching a stream that could
never carry anything. The handle, not the entry, decides.
**The endpoint list had no order.** Configured endpoints load first and discovered hardware is
appended, so the same machine listed its endpoints differently after a restart. This was found the
hard way: a toggle clicked in the window landed on a different row than the one read, because the
list reordered between the two. `ListEndpoints` now sorts by kind, then name, then identifier.
**Event kinds were Debug-formatted.** `format!("{:?}", kind)` put `EndpointStateChanged` on the
wire where the contract documents `endpoint_state_changed`, and made every Rust variant rename a
silent contract change. The names are now written out and tested for shape.
**The lesson**: each of these was invisible from the command line, which lists once and exits. A
client that redraws every second and offers a control for each row exercises the daemon in a way
no single command does.
---
## R-026: The window is a client, and recovers like one
**Status**: **VERIFIED** (2026-09-20). Daemon killed and restarted with the window open.
The interface polls a snapshot once a second rather than holding `WatchState` open. The streams
exist so a client does not miss an event; a window redrawing every second misses nothing a person
can see, and polling means a daemon restart needs no reconnect logic — a failed refresh drops the
channel and the next tick reconnects.
Killing the daemon replaces the whole view with what to do about it, rather than leaving a stale
list beside an error, which would suggest those endpoints were still being managed. Restarting the
daemon restored the window with no user action.
The presentation logic is pure functions over generated types, tested without a window or a
daemon: 24 tests cover the wording, including that the two kinds of down never read the same, that
guidance appears only where a user can act, that our own drops are never reported as network loss,
and that attached hardware does not read as a connection.
---
## R-078: The redesigned window, and what it asks of the daemon
**Status**: **DECIDED** (2026-09-23), for Phase 12. Decided by the owner, working through a
libcosmic design study in `crates/gui/examples/designs/`.
The first window was a nav bar over flat lists, with forms built into the pages and a kind named
only by a faint word under each endpoint. The owner compared five layouts drawn in libcosmic,
each navigable through every area, and chose the list layout, then refined it. Several of the
refinements are not wording: they change what a port, a route and a network session are, so they
reach the daemon, the configuration file and the contract.
Decisions on the window:
- **A list per kind.** Endpoints are listed in one section per kind: virtual ports, network
ports, Bluetooth devices, USB and hardware, and the IAC buses and Apple network sessions macOS
provides. Each kind Midi Harbor makes ends with its own add action.
- **A docked side panel.** Clicking an endpoint opens its details beside the list, docked rather
laid over it so a dialog can open without closing it. Edit, Delete, Disconnect and Forget live
there, not on the rows. Changing page closes it.
- **One dialog to add and edit.** Adding and editing the same thing share one modal dialog,
filled in when editing.
- **Routes switch on and off on their row only**, never in a dialog.
- **Pages:** Endpoints, Routes, Bluetooth, Activity, Monitor and Settings. There is no network
page: network ports are endpoints, the machines a network port can reach are in its panel, and
the machines this one knows, with whether each is let in without asking, are in Settings.
- **No direction shown** for endpoints that always carry MIDI both ways. Hardware shows its MIDI
In and MIDI Out counts, as Audio MIDI Setup does, with its maker and model.
Decisions that reach the daemon:
- **"Network port", not "session".** RTP-MIDI calls it a session, and so do Apple and rtpMIDI,
but from the user's side it is a port that reaches the network. It is labelled "Network port ·
RTP-MIDI", its UDP number is "UDP port", and the dialog says Apple calls it a network session.
The CLI's `session` commands and the configuration's `network_session` kind are still accepted.
- **Connector counts for virtual ports.** A virtual port has a number of MIDI In connectors and a
number of MIDI Out connectors, as the IAC Driver's buses do, each at least 1 and at most 16. A
count above one shows to other applications as numbered ports, and a route names one
connector. This replaces the in-only and out-only directions, which FR-002 never allowed: an
existing port of either becomes one of each.
- **An automatic virtual port for a network port**, on by default. Other applications see the
network port as a MIDI port of the same name, joined to it both ways, the way macOS presents
its own network sessions. Off, the network port only carries devices over the network. The
port is the network port's own: renamed and removed with it, and never listed on its own.
- **Two-way routes.** One route can carry MIDI both ways between two endpoints that both send
and receive, instead of two routes.
- **A network port's machines.** Its panel shows the name other machines see (the Bonjour name,
`local_name`), and one list of machines: those in it, each with its address, latency and its
own Disconnect, then those it could connect to, each with Connect, then a way to connect by
name, address and port, the port defaulting to 5004. Connecting a second machine while one is
connected carries it beside the first, which needs the daemon to invite it.
Each decision that reaches the daemon is a task in Phase 12, and each contract change there is
additive.