Skip to content

FBD 1.7.0

Choose a tag to compare

@FisiFla FisiFla released this 14 Oct 23:01
· 106 commits to main since this release

BetterDisplay-parity work agreed at the Phase 3 gate. Scope, evidence and the
deliberate exclusions are in docs/plans/betterdisplay-parity-scope.md,
docs/plans/fbd-gap-matrix.md and docs/plans/fbd-parity-progress.md.

Added

  • A live DPCD read over AUX: fbdcli dpcd <display>. Row 24's read path — IODPDeviceReadDPCD,
    the AUX route to a DisplayPort sink's registers where DSC, link training and tiling are described —
    reached through a runtime-resolved IOKit shim that declares only the read. Its write-side
    neighbours (IODPDeviceWriteDPCD, the retrain and set calls) resolve on this macOS and are
    deliberately not declared: a wrong write there can wedge the only attached display. The command
    reads a block and hands it to the decoder report --dpcd already uses, and a failure reports which
    step declined rather than one message for three causes. Verified as far as this machine allows: the
    entry points resolve, the argument validation and the decode path are exercised, and against the
    built-in panel it declines at service discovery, because FBD's registry walk is tuned for external
    displays. Proving a read end-to-end needs an external DisplayPort display, which this Mac has
    none of. A guessed fallback for the built-in panel was written, measured, and removed rather than
    shipped unverified.
  • The HUD can use the system's glass. FBD's HUD is a vibrancy view in .withinWindow mode,
    which samples the window's own content — the SwiftUI content and nothing else — so it draws a
    flat panel rather than sampling the desktop behind it. The OS's glass (NSGlassEffectView,
    macOS 26+) is now a third HUD background; the default is untouched, so an untouched install keeps
    the look it has always had. Captured on macOS 27: it renders correctly, and the difference is
    subtle — which matches the Crisp review's own note that no glass material can hold a HUD's
    backdrop on this OS family. Two measured facts shaped it: only contentView is guaranteed to sit
    inside the glass (so the SwiftUI content goes there, not in as a subview), and .behindWindow is
    not a substitute for .withinWindow here.
  • FBD speaks 简体中文 and 繁體中文 — starting with the panel. Crisp ships en · 简体中文 · 繁體中文
    and FBD shipped English only. The mechanism is deliberately the small one: three
    .lproj/Localizable.strings beside the icon in Sources/FBD/Resources, copied into the bundle by
    make app, which is where macOS looks for them — no SwiftPM resource bundle, and no code
    change
    , because SwiftUI already resolves Text("Display Mode") against the app bundle. The
    225 strings now cover the panel, the Settings pages and the filters/automation views. That last
    round turned up the real defect rather than a missing translation: SettingsRow,
    SettingsSliderRow and disclosureSection took title: String, and Text(String) is verbatim
    — SwiftUI only looks a key up through a LocalizedStringKey — so every settings title and every
    section header, the panel's included, was invisible to these files no matter how many translations
    were added. All call sites pass literals, so the parameters became LocalizedStringKey and the
    titles started resolving: verified live in the app's accessibility tree, where "Connect all" now
    reads 全部连接. A test stops the three files drifting: the languages must agree on every key, and
    every key must still appear in the source, since a renamed string would otherwise silently stop
    resolving and look like "not translated yet" rather than broken. The Settings sidebar went the same
    way: it rendered Text(item.rawValue), a String, so its labels were verbatim too, and SettingsPage
    now spells its cases out in a switch returning LocalizedStringKey — which keeps every key a
    literal the test can verify (236 keys each). What the drift test cannot reach is an interpolated
    string: SwiftUI turns Text("\(nits) nits") into the format key %lld nits, which never appears
    verbatim in the source, so the "every key still exists" check would reject an entry for it. That is
    now fixed rather than accepted: the check understands format keys and verifies their literal
    segments, so the interpolated strings are localised too — %lld nits, %lld Hz, %lld display%@
    and %lldpx, 240 keys per language. The Chinese forms drop the English plural, which is why a
    translation may omit a placeholder. What remains English is numbers with no words in them, and the
    automation model's own label/token — those stay English on purpose, because FBD_EVENT_LABEL
    hands them to shell rules as a script contract, so the UI maps those cases itself. Two things worth
    recording — Sources/FBD/Resources is excluded from the SwiftPM target (which is why Info.plist
    lives there and is copied by hand), so the .lproj directories have to be copied by the Makefile;
    and CFBundleLocalizations must not sit alongside real .lproj directories, because it made
    macOS report every language twice, a signal that sends the next person hunting a nested copy that
    is not there.
  • The DDC input menu marks the input in effect. It marked nothing until now: reading VCP 0x60 was
    a blocking DDC read, so the panel deliberately refused to guess and its title said "Choose…". The
    read now rides in the same batched read as contrast/volume/mute — one more feature on the
    display's own queue, off the main actor — so the menu shows a checkmark on the current input, its
    title names it, and where a monitor refuses to answer it falls back to the last input set rather
    than claiming knowledge it does not have. The Double → UInt16 conversion is a tested helper
    (DDC.inputSource(from:)) rather than an inline cast in a view, where an out-of-range answer would
    have trapped.
  • A Dark Mode toggle, completing Night Shift + True Tone. Crisp has all three system toggles and
    FBD had two. The mechanism is Crisp's: SkyLight's appearance theme
    (SLSGetAppearanceThemeLegacy / SLSSetAppearanceThemeLegacy / SLSSetAppearanceThemeNotifying),
    resolved at runtime through a C shim like every other private surface, so a Mac without those
    symbols degrades to "unavailable" rather than crashing — and the notifying variant is preferred so
    the switch crossfades the way Control Center's does. Available as fbdcli dark-mode [on|off]
    (bare, it reports) and as a toggle beside True Tone in Settings. Deliberately not stored in
    Settings: it is the system's switch and another app can flip it, so the UI reads it live rather
    than remembering a value that could go stale.
  • The resolution list shows the sizes you actually switch between. The panel's Resolutions
    section offered every mode inline — 132 of them on this machine — while reaching one through
    the ⋯ submenu cost three clicks, so neither route was good. It now shows one row per logical
    size at the current mode's aspect ratio, HiDPI preferred, with the mode in effect always on
    the list, and Show all N modes one click away. CuratedModes is pure, so what counts as
    worth showing is unit-tested: one row per size, the current mode owns its own row, variants
    win elsewhere, other aspect ratios wait behind "show all", and truncation never drops the
    mode that is on screen.
  • Configuration protection covers rotation and the colour profile.
    ProtectedProperties offered six of BetterDisplay's nine properties; it offers eight now.
    Neither new one can be read — there is no readable rotation and no readable current
    colour profile — so both are restored from the value FBD itself last applied, recorded as
    it applies it; one set by another tool is invisible, which the type says plainly. The
    legacy default is deliberately unchanged, so upgrading never begins re-applying more state
    than before: widening remains an explicit choice.
  • A saved network device can be attached to a display. The device then follows that
    display's volume when the brightness/volume keys drive it, so a TV or AVR stops being
    something reachable only through fbdcli tv. Attachments are keyed by the display's
    identity, not its id, so a reconnect keeps them; fbdcli config device attach|detach|attachments manages them, and a device deleted afterwards is reported as
    dangling rather than silently resolving to nothing. Philips sets are the one exception,
    and say so: pairing needs a PIN read off the TV and that pairing does not outlive the
    controller instance, so a fire-and-forget volume send cannot complete it.
  • Manual controls and a notification action for automation. A rule can be scoped to
    manual — a control you invoke yourself, via the new Run button in Settings →
    Automation or fbdcli automation run — and a notify action posts a user
    notification, which runs in-process rather than through a shell. ON and OFF are two
    manual rules, and a shell control that prints its state already records that output in
    the run log, which is how a state-query control is built. Notification permission is
    asked for only once a notification rule exists, never at first launch, and
    fbdcli automation run refuses a notification rule outright — the CLI is a bare binary
    with no bundle identity or authorisation, so a request from there would be accepted and
    then dropped, and reporting success would be a lie.
  • fbdcli report [file] — an exportable display report. A detailed, paste-ready block
    per display (identity, kind, geometry, current mode with its colour encoding, mode count
    and refresh rates, control paths, brightness, XDR/HDR state) under a header carrying the
    FBD and macOS versions — the macOS version because display behaviour differs per release.
    Prints, or writes to a file for attaching to a bug report. The formatting lives in
    DisplayReport in FBDCore and is locked by format tests, so it cannot drift silently.
  • The HTTP API covers four more of the parity settings. POST /api/config/connections runs the connection planner through the same guarded
    disconnect chokepoint the Settings buttons use; POST /api/config/protection sets a
    display's protected-property selection (the display may be a number or the string
    /api/displays returned); POST /api/config/device lists, adds or removes a saved
    TV/AVR; and POST /api/config/favourite lists resolution favourites, records the
    current mode into a slot, or recalls one. Each validates its input and names the valid
    values when it refuses, and a favourite whose mode is gone is reported rather than
    approximated.
  • Three Shortcuts actions for the parity surfaces. Set Lock Dim, Set Keyboard Control Target and Sync Display Group join the existing display actions, so the
    new features are scriptable from Shortcuts as well as from fbdcli and the app.
    Sync Display Group runs the same tested planner as the Settings page's "Sync now",
    and each action validates its input and names the valid values when it refuses.
  • The HTTP API covers the parity settings. GET /api/config reports the new
    settings — lock dim, the keyboard control target (with the valid values), the
    brightness model and configuration protection; POST /api/config/lockdim and
    POST /api/config/keytarget set them. Both writes take effect without a restart,
    because each is read back at use time: lock dim re-reads its policy on every lock
    event, and the media-key path reads the control target on every press. Both go
    through Settings, the same single write path the app and fbdcli share. The
    existing token gate and method gate apply unchanged, and an omitted
    targetPercent keeps the stored level rather than resetting it.
  • A saved network-device registry. FBD already had working LG webOS, Samsung,
    Philips and Yamaha controllers, but reaching one meant repeating the brand and
    host on every single command and nothing remembered a device between runs.
    NetworkDeviceRegistry stores them (name, brand, host, optional Yamaha zone)
    through the single settings write path; fbdcli config device list|add|remove
    manages them; and fbdcli tv <name> … addresses one by name. A host that could
    redirect the request — a scheme, path, credentials, query, or a :port suffix
    — is refused when the device is added rather than stored for later.
  • fbdcli config completed: group-sync <group> <0-100>,
    connections connect-all|disconnect-all-but-main|swap-identical, and
    model <key> [value] — the last of which also makes each brightness-model
    value settable on its own instead of only as one opaque blob.
  • Per-property configuration protection. Protection was one opaque blob
    (mode + brightness + preset). It now protects six properties independently —
    resolution, refresh rate, preset, HDR mode, main status and brightness — each
    selectable per display (ProtectedProperties, the Displays page, or
    fbdcli config protect <id> <properties>). The default stays the old set, so
    an existing install behaves exactly as before until the user widens it, and a
    property the display cannot report is never forced.
  • Display-group synchronisation and activation policy. Groups gained an
    enable flag, one of five activation policies, sync source/target member
    filters, an optional nits-matching mode, and a pure GroupSyncPlanner that
    reuses BrightnessSync for the nits maths. Managed on the new Groups page and
    via fbdcli config group-policy|group-enabled.
  • Resolution favourites. Five recall slots per display, recorded and applied
    from the Displays page or fbdcli config favourite <id> save|apply|list. A
    slot is recalled exactly, then by an equivalent mode, otherwise it reports
    itself unavailable rather than applying a wrong mode.
  • Mode-list breadth and a refresh-rate list. ModeFilterOptions plus a
    deduplicated descending refreshRates(from:). The unexposed (NTSC) timings
    BetterDisplay lists are not faked: docs/PRIVATE_APIS.md records that the
    private queries behind them need a live WindowServer connection and crash a
    process without one, so the gap is documented.
  • A tunable brightness model. BrightnessPreferences exposes the combined
    curve: hardware min/max nits, HDR peak SDR/HDR nits, a re-defined 0 % software
    level, a minimum hardware level, the software-dimming switchover point, the
    upscaling maximum, whether hardware control is preferred, and what to assume
    about stored control values at startup. BrightnessCurve is the pure
    mapping; every field is clamped, inverted bounds are repaired, and a
    non-finite value falls back to its default rather than to the ceiling.
  • Startup safe mode. Holding ⇧ Shift at launch — or FBD_SAFE_MODE=1 —
    skips stored EDID overrides and configuration-protection restore for the
    session. Fail-closed: it only ever suppresses an apply, and says so.
  • Lock dim. Dims to a chosen level while the screen is locked or the
    screensaver is up, and restores the recorded brightness on unlock. The state
    machine never dims blind, is idempotent on a repeated lock, and always has a
    restore path.
  • Keyboard control target. The brightness and volume keys can act on the
    screen under the pointer, the focused screen, every suitable screen, or the
    display whose name matches the current audio device (exact or partial match),
    with a deterministic fallback so a key press is never silently swallowed.
  • Connection management. ConnectionPlanner produces ordered
    connect/disconnect/swap steps, executed through the existing guarded
    DisconnectController chokepoint. It never emits a built-in-display disable
    unless the explicit override is set, and refuses outright when there is no
    main display.
  • fbdcli config — one command for the settings above (show, protect,
    favourite, lockdim, lockdim-target, keytarget, group-policy,
    group-enabled), running locally against the shared defaults with no second
    write path. Unknown property names, targets and on/off words are refused
    rather than silently ignored.
  • A sidebar Settings layout. Settings was a two-tab form; it is now a
    sidebar of Displays · Groups · Application · Menu · Keyboard ·
    About with long pages, and the panel grows to 760×860 for it.
  • Keyboard shortcuts on the panel's own controls. The settings gear takes ⌘,
    and the tools footer ⌘⇧V (Virtual Screens), ⌘⇧G (Groups), ⌘⇧P (PiP), ⌘U
    (Check for Updates) and ⌘Q (Quit FBD). The panel's controls previously carried
    no key equivalents at all — ⌘, only duplicated the one the status menu already
    had. This works because the panel can take key focus: its style mask carries
    both .titled and .nonactivatingPanel.
  • fbdcli ambient-light <display> [on|off]. Reads or sets the Apple panel's
    ambient-light auto-brightness. The seam existed and was reachable from the
    panel's More menu but had no CLI surface (gap-matrix row #12). A bare read
    doubles as the capability probe — a display with no ambient-light sensor is
    reported unavailable and exits 2 — and a set is confirmed by reading the state
    back rather than trusting a status code.
  • The status item's icon is now a choice, and it can hide itself when there is
    nothing to control.
    Settings → Menu → Status item offers a curated set of SF
    Symbols (each measured to resolve on macOS 27 — the list is closed rather than
    free text, because a name that fails to resolve would leave a menu-bar app with
    no button), and an option to hide the icon while no display other than the
    built-in one is attached. That option is refused unless a Dock icon is enabled:
    hiding the item with no other way in would strand the user, so the policy fails
    closed instead. Choosing an icon that cannot be resolved falls back to the
    default rather than leaving the button blank.
  • fbdcli config identity and config transfer. Which key a display's
    stored settings are filed under is now a choice — vendor+model+serial (the
    default), vendor+model, serial alone, or the display's name — and one display's
    settings can be copied onto another. The default reproduces the historical key
    byte for byte, so an existing install keeps every per-display setting attached;
    switching is an explicit step and the transfer is separate rather than a silent
    re-key. The copy handles both storage shapes: the per-display key families and
    the network-device attachment, which lives inside one shared blob and would
    otherwise be left pointing at nothing. It refuses to overwrite settings the
    destination already has unless --overwrite is passed.
  • A DPCD/DSC decoder, driven by fbdcli report --dpcd <hex>. The decoders for
    the DisplayPort Configuration Data block — revision, link rate and lane
    capability, per-lane training status, the DSC capability block, and HDCP's
    BSTATUS — are pure, and report --dpcd decodes a dump you supply, optionally
    with an address prefix (68028:00), because HDCP sits 400 kB away from the
    capability block. Reading DPCD from the hardware is not built: the private
    getters are located and symbol-verified, but their signatures are unknown, and
    calling one speculatively against a machine's only attached display is not a
    risk worth taking. A dump is treated as a set of ranges, and a register that was
    never read reports "not read" rather than zero — those are different claims, and
    a report that conflates them is worse than no report.
  • Two sibling projects reviewed, and the first of their ideas taken on. Two
    free alternatives, FreeDisplay and Crisp, were read for what FBD lacks; both are
    MIT (Crisp ships a LICENSE, FreeDisplay declares MIT in its README but ships no
    licence file). The capability comparison, the licences, and Crisp's Apple-grounded
    UI doctrine are recorded in
    docs/plans/reference-projects-freedisplay-crisp.md — FBD is ahead on automation,
    network devices, streaming and EDID override, and behind on scaling finesse,
    named presets, localisation and a few cheap system controls.
  • The panel's chrome no longer stacks a material on glass. The header, the
    settings header and the tools footer sat on .ultraThinMaterial inside a panel
    whose backdrop is already a frosted layer. They now use a flat labelColor shade
    (FBDTheme.chromeBandOpacity), so the panel holds one material rather than four —
    and three live blurs no longer re-sample the same backdrop on every redraw.
  • Keep the Mac awake. A toggle in Settings → Menu → System holds a
    NoDisplaySleepAssertion while FBD runs, so a long transfer, render or
    presentation does not end with the display asleep. It is app-side by necessity:
    the assertion belongs to the process that holds it, so a fbdcli invocation
    would drop it on exit — which is why there is no command-line switch for it.
  • Several PiP windows at once. Capture sessions are now keyed by their source,
    so a second display's PiP opens a second window instead of closing the first, and
    re-asking for a source replaces only that source's window. The app keeps one
    window per display, which is one window per source. fbdcli pip takes several
    sources in one invocation (pip 1 --window 1234), which is the only way to open
    two at once on a single-display Mac. Verified live: two windows coexisted and both
    tore down cleanly when the CLI's stdin closed.
  • A virtual screen can be associated with real displays, so an arrangement is
    recalled when it attaches.
    fbdcli config vscreen associate <screen-id> <display-id>… records the arrangement the displays have at that moment. They are
    named by identity, so the association outlives the reconnect that reassigns display
    ids — the reason it is worth storing at all. Recall goes through the same
    CGConfigureDisplayOrigin path layout protection uses, so there is one place that
    knows how to talk to the display layer, and it fails closed: if any associated
    display is absent, nothing moves, because a partial arrangement would shuffle the
    displays that are present into positions chosen for a layout that is not.
    Mirroring is deliberately not implemented and has no stored flag — ADR-0001 records
    that a virtual screen cannot be addressed as a display, so a flag nothing could
    apply would be the dead configuration this codebase has had to remove twice.
  • GUI scale matching — and the physical-size data it needed. CGDisplayScreenSize
    reports a real panel size (344×222 mm for this machine's built-in display), so the row
    carried as blocked on "reliable physical sizes" was blocked on nobody having called
    it. Display now exposes that size and the GUI scale it implies — logical points per
    millimetre, which is what determines how large the interface actually looks — and
    UIScaleMatching picks, for each display, the mode whose scale is closest to a
    reference. Two things are deliberate: distance is relative, so 5→6 and 6→5 are
    equally far apart, and the choice fails closed — if no mode gets within tolerance
    nothing changes, because moving every window on a display without achieving the match
    is worse than leaving it alone. fbdcli config uiscale reports each display's scale
    and what it would do, and toggles the setting; it reports rather than applies, since a
    resolution change moves every open window.

Changed

  • The panel is one surface now: rows are bands, not cards. The three rounded cards that floated
    inside the panel — the display row, Virtual Screens and Display Groups — each carried their own
    fill and a hairline border, which is the glass-on-glass stacking the chrome fix was about, one
    level down. They are full-width bands of Color.primary.opacity(0.035) (fbdPanelBand())
    separated by hairlines, so the expanded detail region sits in the panel's surface rather than on
    a card inside it. fbdCard() remains for Settings, where a group genuinely is its own surface.
    Checked against the rendered panel rather than the source: no rounded corners remain on rows, and
    the expanded detail reads as part of the band.
  • The brightness/volume HUD is configurable. Position (top centre/left/right, bottom
    centre), a continuous or a segmented bar, a dark HUD or one that follows the system
    appearance, a shadow toggle, and a corner-radius slider. The defaults reproduce the
    previous appearance exactly, so an untouched install is unchanged, and every value is
    clamped — a stored radius cannot clip the content. The HUD's content now carries its own
    foreground, which also fixes unreadable text on the dark HUD in light mode.
  • Dock visibility rules. A menu-bar app is an accessory by default; it can now be given
    a permanent Dock icon, or a brief one at launch so it is findable in the Dock just after
    it opens.
  • The menu-bar panel is flattened. Each display is now one compact row: icon, name,
    capability badges, current mode, the options menu and the power button on the title
    line, with the brightness slider and its nits value inline beneath it. Filters, DDC/CI
    and the resolution/XDR sections moved behind a single More controls disclosure.
    Before this, one display filled most of the 650pt panel, so a multi-display Mac could
    not see its own displays at once — the one thing the panel exists for. Nothing became
    unreachable: the ⋯ menu already covered mode, refresh, colour, presets, favourites,
    mirror, PiP, rotation, disable and Open in Settings, and the folded content is unfolded
    in Settings → Displays.
  • Settings.debugSummary now includes the lock-dim, keyboard-target and
    brightness-model settings.
  • fbdcli help documents config.

Fixed

  • "Prefer hardware brightness" and the curve's dimming zone now work, and a brightness read now
    agrees with the screen.
    All three needed the same thing: FBD applied software brightness as an
    overlay but neither tracked nor reported it, so a read returned the backlight value while the
    screen showed something dimmer — the read-back-disagrees-with-the-screen bug
    CombinedBrightness documents itself as having fixed once already. The dim is now tracked per
    display and reported by get; the overlay gives the brightness route its own dim source
    beside the dim-to-black/lock one, combined as the darkest of the two so releasing either leaves
    the other in force. On that footing preferHardwareControl == false delivers brightness as a
    dim over the backlight — falling back to the hardware when the request is brighter than the
    backlight, because a dim can only ever lower what the screen shows — and the curve's
    minHardwarePercent / softwareZeroPercent ramp the hardware and dim the rest, releasing the
    dim once the route climbs above the ceiling. The dim is taken only where it outlives the call, so
    a one-shot fbdcli invocation keeps the hardware route rather than accepting a write that would
    vanish. The shipped profile takes neither route, so a default install behaves exactly as before.
  • Four brightness-model sliders shaped nothing; two of them do now. The combined-curve
    model (BrightnessCurve) had no caller at all — XDRBoostPlanner computes its own linear
    map — so the "Upscaling maximum" and "Software dimming switchover" sliders in Settings were
    inert. upscaleMaxPercent now caps how far an upscale may reach, on the native nits target
    as well as the software-boost factor, and dimmingSwitchoverPercent moves where that boost
    ramps. Both take effect only when the profile carries them, so a shipped profile behaves
    exactly as before. The other two knobs — "Minimum hardware level" and "Redefine 0% software
    brightness" — belong to the curve's dimming zone, which needs software-brightness state FBD
    does not track; get does not report an overlay dim, so honouring them would make read-back
    disagree with the screen, which is precisely what CombinedBrightness documents itself as
    fixing. The row records that rather than half-wiring it.
  • The startup control-value policy works now. Settings offered "assume last used / write
    to device / read from device" and nothing read it: FBD had no store of per-display control
    values for it to act on, so a visible picker could not do anything. DisplayController
    now records the brightness it sets, per display identity, and launch reads it back,
    re-writes it, or leaves the device alone as asked. Absence is stored as absent rather than
    0, because 0 is a legitimate brightness — and the default policy touches nothing, so a
    default install behaves exactly as before. Verified live: with write, a stored 0.4
    reached the panel (38.6% after backlight granularity); with read, a deliberately wrong
    0.99 was corrected to the device's 0.3898 without changing the device; with assume,
    nothing happened at all.
  • "Allow unsafe modes" did nothing; now it does. The setting was written by the
    Settings page and read by nobody — every mode list came straight from the raw list — so
    the switch changed nothing while appearing to work. The offered list now runs through
    ModeFilterOptions (Display.offeredModes) in the panel's Display Mode menu, the
    per-display sections and fbdcli modes, which also reports how many modes it hid. Every
    other filter rule is deliberately left permissive: standard's view hides the scaled
    ("mixed") resolutions macOS itself provides — six on this machine — so wiring that view
    in wholesale would have silently removed resolutions people could pick. That regression
    is what the live before/after count caught, and there is a test for it now.
  • Settings → Back discarded a resized panel. Going back restored a hard-coded
    460×650, so a panel the user had widened snapped back to 460pt wide (the saved
    frame on this author's machine is 535pt). The size the panel had when Settings
    opened is now remembered and restored on the way back, clamped to the panel's
    minimum, so a deliberate resize survives the round trip.

Verification

  • swift build --disable-sandbox — clean, zero warnings.
  • swift test --disable-sandbox — 868 tests, 0 failures (FBDCoreTests 732,
    including the 266 test seams added by the parity work; FBDCLIParserTests 136).
  • make ui-smoke — 5/5 against the rebuilt app.
  • CLI round-trips verified against the live app for every config subcommand,
    including the refusal paths.

Not in this change

  • The menu-bar quick menu was not restructured to mirror BetterDisplay's
    grouped layout; only Settings was. Recorded as open.
  • The HTTP API stops short of group sync apply: it is reachable from fbdcli and the
    app UI, but exposing it needs more than a route. Group sync matches brightness in nits,
    which means the executor's DisplayControlling seam would need hardwareCeilingNits
    added, otherwise the endpoint would silently skip the luminance matching the CLI does.
  • No new private API was introduced, so docs/PRIVATE_APIS.md does not change.