Skip to content

Releases: FisiFla/FBD

FBD 2.1.4

Choose a tag to compare

@github-actions github-actions released this 22 Oct 13:43

UNSIGNED BUILD — not notarised; right-click → Open on first launch.

Changed

  • The Application page now groups the HUD settings together, right under "General". The "Custom
    OSD" switch — which turns the brightness HUD off — sat in the "Integrations" group next to the
    HTTP API, below the fold of the settings window. It now opens a group of its own, "On-screen
    display", with the switch and the HUD's position, bar, background, shadow and corner radius; the
    Dock icon options moved to a "Dock" group next to it.

FBD 2.1.3

Choose a tag to compare

@github-actions github-actions released this 22 Oct 13:19

UNSIGNED BUILD — not notarised; right-click → Open on first launch.

Fixed

  • The brightness slider drew a blue ring around itself. The slider's focusable view is the whole
    row — both sun icons included — so macOS's keyboard-focus effect was drawn as a border around
    the control instead of as a focus indicator. The thumb already grows when the control has the
    focus, which is the indicator this design uses; the system's ring is switched off.
  • The brightness HUD popped up for changes FBD did not make. Every brightness change FBD observed
    was announced, including the ones the hardware made on its own — the brightness keys, the
    ambient-light sensor, another application — so the pill appeared on its own, often. The update
    now carries where it came from, and only FBD's own changes are announced. The HUD stays for the
    writes FBD performs, because macOS shows no indicator of its own for a brightness change an
    application makes.

Documentation

  • The install notes now say what an unsigned update costs: macOS treats the new binary as a new
    app, so Screen Recording is requested again and the login Keychain asks for its password once
    more (the app stays responsive while it does). SECURITY.md explains why the Keychain item's
    access control list is not widened to avoid the prompt.

FBD 2.1.2

Choose a tag to compare

@github-actions github-actions released this 20 Oct 14:46

UNSIGNED BUILD — not notarised; right-click → Open on first launch.

Fixed

  • The app could freeze solid when macOS asked for Keychain access. Reading the HTTP API token
    touches the login Keychain, and SecItemCopyMatching blocks on the modal "allow access?"
    prompt when the running binary is not in the item's access control list — which is exactly what
    happens after an update, because an unsigned build's identity is its code hash. The read ran on
    the main actor, so the prompt froze the menu bar, the settings window and every HTTP request
    (they all wait for the main actor) until it was answered. The token is now read off the main
    actor, cached, and served from memory afterwards; the prompt can still appear once per unsigned
    update, but the app stays usable while it is on screen.

FBD 2.1.1

Choose a tag to compare

@github-actions github-actions released this 20 Oct 14:34

UNSIGNED BUILD — not notarised; right-click → Open on first launch.

Changed

  • The feature matrix now says what was measured rather than what was inherited. Every cell that
    reads live-verified was measured on this machine (M1 Max, macOS 27.0.1, built-in XDR panel,
    2026-10-20): the brightness write, the resolution write, Night Shift, the display presets, the
    software overlay, the OSD, the PiP enumeration, True Tone and the HiDPI install script. The
    evidence lives in docs/PRIVATE_APIS.md, which also lists what this machine cannot show:
    DDC/CI on an external monitor, EDID on the built-in panel, the below-floor blend's DDC floor,
    the three display-connection behaviours, the privileged HiDPI write, and the app's own Screen
    Recording grant.

Fixed

  • The full-screen overlay could latch on a blank white screen. The capture excluded the overlay
    by window id, but that id was matched against a snapshot of the window list taken microseconds
    after the overlay had been ordered front — when the overlay was not in the snapshot yet, the
    capture included the overlay itself, and the overlay then rendered its own output for ever. The
    overlay windows now sit below the screen-capture shielding level instead of on it, the capture
    waits (briefly) for the overlay to be known and falls back to excluding the application, the
    window stays invisible until a real frame has been drawn, and a session that never receives one
    is torn down and reported instead of painting.
  • The below-floor brightness blend never engaged. A request below the panel's floor was served by
    the software-dim route, which released the blend and left the screen at the floor while the
    slider said otherwise. The blend now decides before the dim route does, and only one software
    dim can be in force per display.
  • The blend's read-back reported the floor instead of the position that was asked for. It decided
    "is the blend pinning the hardware?" by asking the hardware whether it sat at the threshold — a
    condition that is also true of the normal case, and that a read which has not caught up with the
    write yet fails. The read-back now follows the software factor, the one piece of state that says
    the blend is in force: 5 % in, 5 % out.
  • Turning a full-screen filter off left its parameters behind, so the next composition restored
    the old cast and a stale software boost kept the screen brighter than the brightness slider
    said. "Off" now clears the user's filter parameters; the boost and the blend keep their own
    state, because they belong to the brightness route.
  • The display model could claim an XDR upscale that was not on screen. The "is a software boost
    active?" question was answered with the model's own flag — the one the boost itself writes — so
    the flag could never clear itself once the session that owned it was gone, and the brightness
    read-back reported a brightness the screen was not showing. The live overlay session decides
    now.
  • The feature matrix said the True Tone toggle degrades on macOS 27. It does not: measured on
    macOS 27.0.1 (built-in XDR panel, 2026-10-20), CBTrueToneClient's available is true, enabled
    reads back and setEnabled: round-trips — off reads back off, on reads back on. The claim came
    from a note in Lunar's CBTrueToneClient.h (not controllable from third parties since macOS
    26.3) that was never measured here. The matrix, the private-API ledger and the controller's
    comment now say what was measured; the macOS 26 cell is marked unmeasured instead of degraded.
  • The matrix said EDID is available on macOS 27. Measured: the built-in panel exposes none
    (fbdcli edid export 1 → "no EDID available"), which the private-API ledger already said.
    EDID now has its own row, and external displays stay unverified.
  • The matrix said the XDR native preset rewrite works on macOS 26. It is refused on macOS 26 and
    later — the controller's own self-test detects the rejected write — so both columns now point
    at the software boost and the footnote says why.
  • fbdcli xdr <id> <nits> printed "upscaling enabled" when the write had in fact fallen back to
    the software boost. The boost lives in an overlay, and an overlay does not outlive a one-shot
    command, so the brightening was gone the moment the CLI exited. The command now says that
    instead of claiming a lasting change, and it names the read-back.

Documentation

  • docs/PRIVATE_APIS.md gained a "Live-verified on this machine — 2026-10-20" section: what was
    measured, the two measured facts that change a claim elsewhere (the True Tone note inherited
    from Lunar, and the macOS 26+ refusal of the XDR preset rewrite), and what this machine cannot
    show.
  • A new test runs the generated HiDPI override install script against a temporary root: the plist
    is written and carries the ladder, plutil -lint accepts it, a repeated install is
    byte-identical and leaves no staging file behind, and the removal takes exactly that file away.

FBD 2.1.0

Choose a tag to compare

@github-actions github-actions released this 20 Oct 12:22

UNSIGNED BUILD — not notarised; right-click → Open on first launch.

Added

  • fbdcli report [<id>][file] [--json] is a paste-ready report for a bug report: the display's
    identity (masked), its geometry and mode, the control paths, the EDID, the panel facts and the
    sink's DPCD — revision, max link rate, lanes, capability bandwidth, DSC and HDCP. A fact is
    either a value or absent with a reason, never a number standing in for a measurement: the
    fields whose source cannot be read safely on this macOS (the active link bandwidth and rate,
    the DSC-in-use flag, the tiling, the connection-mode keys) are reported as unavailable with
    the reason, and the JSON carries null plus the reason next to it. Serials are masked and
    home-directory paths are shortened, on the same precedent as the masked settings dump
    (issue #19).
  • fbdcli skill install claude|codex writes an agent-ready SKILL.md: the command grammar, the
    token model (the token is never echoed), a read-back command for every state-changing command
    and the write contract. The document is compiled in and version-stamped, so a re-install updates
    it cleanly; --print writes exactly the file's bytes to stdout (issue #31).
  • The capabilities the other issues added are now reachable from all three surfaces. The HTTP API
    grew a per-display disconnect/reconnect action (with remember), a hidpi action
    (install, remove, status) and a gamma-floor action (enabled, optional threshold).
    Five new Shortcuts actions do the same through the controller seam the HTTP API drives: toggle
    HDR mode, set the extra-brightness target, switch the input source, disconnect or reconnect a
    display (remembering it if asked), and apply an image adjustment. The per-display page shows a
    "Brightness blend" row and a "Connection" section — hidden rather than greyed where they do
    not apply — and states what each of them needs. The CLI grew the matching commands:
    fbdcli hidpi <id> install|remove|status [--preview], fbdcli gamma-floor <id> [on|off] [percent], fbdcli display <id> disconnect [--remember], fbdcli display <id> reconnect and
    fbdcli display --list-remembered. Every one of them follows the write contract below: a lost
    reply says the command may have applied and exits 75, a refused write names the read-back
    command (issue #28).
  • A display can now be kept disconnected across reboots: FBD remembers the choice and
    re-disconnects the display when it comes back, until the user re-enables it. The memory is
    filed per display identity, so it survives a restart (issue #30).
  • The built-in display can be turned off while an external display is attached and the lid is
    open. It is a setting, never a silent action, and the existing base-M3 guard still refuses
    the whole path before any call is made: on those Macs the built-in display may not come
    back without a reboot, so nothing is attempted (issue #30).
  • An external display can follow the built-in panel's auto-brightness: the ambient sensor's
    value is mapped through the nits ceilings and written through the usual brightness path, so
    it inherits the write cooldown and the coalescing instead of racing the slider (issue #30).
  • A monitor's brightness scale rarely reaches black, so the bottom of the brightness slider
    was a dead zone: the knob moved, the screen did not. The new below-floor blend pins the
    hardware at a threshold and lets the software layer carry the rest as a multiplicative dim,
    so the screen reaches what was asked for instead of stopping at the panel's floor. The
    curve is continuous at the handoff and monotonic across it, the read-back reports the
    position that was asked for rather than the floor, and in HDR the whole range goes to the
    software layer — the DDC scale says nothing about the panel's luminance there. Off by
    default, per display, and it needs Screen Recording permission (issue #29).
  • A per-display HiDPI smooth scaling section: FBD generates a dense scaled-mode ladder
    (the native size stepped down in 16-point increments, the aspect held, down to half the
    native size) and installs it as a display override, so macOS offers scaled resolutions
    between the panel's native size and half of it instead of a single HiDPI step. The ladder
    is written to the display's own override folder and the display is re-enumerated
    afterwards. The section is hidden on displays that cannot carry an override, and it says
    plainly whether macOS will ask for an administrator password — it only says so when a
    prompt is really coming (issue #27).
  • DDC/CI replies are validated before any of their numbers are used: a reply has to carry the
    DDC/CI signature, echo the VCP code that was asked for, report no error, and pass its
    checksum. A monitor that acknowledges an I2C read but answers with stale EDID bytes or a NULL
    frame no longer poisons the brightness scale (issue #25).
  • A display whose DDC controller stops answering has its reads quarantined after six
    consecutive failures — or after a single attempt that takes longer than three seconds, which
    means the channel is not listening at all. Quarantined reads fail fast instead of hanging for
    the full timeout; writes are unaffected (issue #25).
  • DDC/CI reads are cached for five seconds and paced like writes, so a display is not asked
    more often than its controller can answer (issue #25).

Changed

  • A write whose reply is lost now says so instead of inviting a blind retry: the CLI reports
    "the command may have applied", names the command that reads the state back, and exits 75
    (EX_TEMPFAIL). A refused write exits 2. Every state-changing command — routed and direct —
    was audited for retry-inviting wording (issue #31).
  • The software boost no longer overwrites the display's own picture settings: the overlay
    keeps the user's filter and composes the boost into it, so applying and removing a boost
    leaves the screen as it was (issue #29).
  • fbdcli auth-token no longer prints the API token to stdout. It now explains how to obtain
    it; --reveal prints the token explicitly. The token grants full control of the user's
    displays, so it is treated as a secret (issue #32).
  • The local API token comparison now has a single implementation (APIToken) shared by the
    HTTP router and the fbd:// URL handler, instead of two copies that had to be kept in
    sync (issue #32).
  • Enabling or disabling a display and switching its mode now pause DDC traffic and wait for
    the transactions already in flight to finish. The window server waits behind an in-flight
    I2C transaction, and freezes the whole Mac with it (issue #25).

Fixed

  • The HTTP server parsed every request's headers twice — once per received TCP segment. The
    header block is now parsed exactly once per request (issue #32).
  • A DDC/CI write no longer leaves a stale cached read behind: the cache entry is dropped as
    soon as the write is accepted, so the next read asks the display rather than the cache
    (issue #25).

Documentation

  • ADR 0005 records why the HiDPI override is a command model rather than a script: the
    ladder, the path and the idempotency check are pure and unit-tested, and only the
    executor knows about privilege escalation — so the privileged helper (issue #23) replaces
    one type instead of the feature (issue #27).
  • docs/ddc-equivalence.md audits FBD's DDC/CI transport against didriksg/Crisp v1.7.1,
    component by component: the private symbol, the wire framing, the reply validation, the read
    cache, the read quarantine, the write cadence, the bus contention and the display↔channel
    matching. Each divergence is either aligned — with the commit that aligned it — or recorded
    with the reason the equivalence still holds. The claim it supports is code-verified
    equivalence
    , never hardware verification (issue #24).
  • scripts/ddc-smoke.sh is a non-destructive DDC/CI smoke harness: it enumerates the
    displays through the app's own matching layer, then per display parses the EDID, parses the
    capabilities string and reads brightness, contrast and volume. --write-test adds one
    set-and-read-back round trip per feature with the original value restored afterwards,
    --json prints the machine-readable evidence, and --ledger appends a timestamped
    evidence block to docs/PRIVATE_APIS.md. With no DDC display attached, every check is
    skipped and the script exits 0 (issue #26).

FBD 2.0.0

Choose a tag to compare

@github-actions github-actions released this 16 Oct 18:15

UNSIGNED BUILD — not notarised; right-click → Open on first launch.

Changed

  • Settings is redesigned in the popover's design language: glass cards, round icon chips,
    a large page title, and a sidebar with a tinted icon per page. Every page (Displays, Groups,
    Application, Menu, Keyboard, About) uses the new SettingsPane / SettingsGroup containers
    in place of the stock grouped form.

Fixed

  • Opening a display's page in the popover paused for a beat before the slide started: the whole
    page was laid out in the transition's first frame. The mode lists now appear first and the
    heavier sections (filters, monitor controls, display actions, more) follow once the slide ends.

FBD 1.8.1

Choose a tag to compare

@github-actions github-actions released this 16 Oct 17:21

UNSIGNED BUILD — not signed with a Developer ID and not notarized.

[1.8.1] - 2026-10-16

Fixed

  • External displays on Apple Silicon were never matched to their AV service, so DDC/CI and
    EDID reads could not work on any monitor. The service is now paired by EDID UUID and
    registry order (verified on hardware: the EDID reads back over I2C).
  • IOAVServiceCreateWithService and IOAVServiceCopyEDID were declared with the wrong
    signatures (missing allocator; missing out-parameter).
  • A display that has an AV service but never answers DDC/CI (most TVs) is no longer offered
    DDC controls after its first refresh.
  • fbdcli config group-sync exited before its debounced brightness writes landed, so it reported success without changing anything.
  • fbdcli ddc-test now reports the AV service and the DDC/CI reply separately.

Changed

  • Scaled modes the system reports without the HiDPI flag (seen on an LG TV, "looks like
    1920x1080" on a 4K backing) are now treated as HiDPI. The CLI labels non-HiDPI modes
    lodpi instead of sdr, which is an HDR term.
  • EDID overrides are disabled on Apple Silicon. IOAVServiceSetVirtualEDIDMode was declared
    with the wrong arguments (disassembly shows a 32-bit mode plus raw EDID bytes), and its mode
    values are undocumented, so applying an override now fails instead of guessing.

FBD 1.8.0

Choose a tag to compare

@github-actions github-actions released this 16 Oct 13:58

UNSIGNED BUILD — not signed with a Developer ID and not notarized.
Gatekeeper will block it; see the README Install section.

[1.8.0] - 2026-10-16

Changed

  • Redesigned the menu-bar panel (Control Center style, Liquid Glass on macOS 26+); Settings now
    opens in its own window.
    The panel is a borderless popover sized to its content (340 pt wide,
    at most 85% of the screen tall, then it scrolls). The front page has each display's brightness
    (and DDC volume) slider, quick toggles for Dark Mode, Night Shift, True Tone and Keep Awake,
    presets, and rows for virtual screens and display groups. Every other per-display control the old
    panel had is on that display's page, one click away — resolution, refresh rate, XDR / HDR, colour
    profile, EDID, filters, monitor controls and display actions, plus what the old ⋯ menu held: HiDPI,
    Auto Brightness, resolution favourites, presets (apply and save), mirroring, Picture in Picture,
    streaming and Show in Settings — with All Modes as the only second level. The Virtual Screens
    page keeps Reconnect All. Selections are
    flat checkmark lists, and every label is plain language, localised in English, Simplified and
    Traditional Chinese. On macOS 26+ the backdrop, toggles, page header, small buttons and slider
    thumb use Liquid Glass, and the HUD defaults to glass (a saved choice wins). Earlier macOS
    versions, or FBD_DISABLE_GLASS=1, get the same UI with materials. CI and release builds now
    require Xcode 26+.

Added

  • Named display presets: a display's whole configuration saved under a name, with an icon.
    FBD could recall a resolution (config favourite) and restore a set of protected properties on
    reconnect, but it could not name and re-apply a display's whole configuration — which is what
    BetterDisplay's presets and the Crisp review's feature list both call for. A preset records the
    state ConfigProtectionController already knew how to capture — mode, refresh rate, brightness,
    the XDR preset, HDR mode, rotation and the colour profile — under a name the user chooses and one
    of a closed set of twelve SF Symbols, closed for the reason the menu-bar icon's set is: a symbol
    name that fails to resolve would draw a blank row. Three surfaces, plus Shortcuts:
    fbdcli config preset <id> list|save|apply|delete, a per-display Display presets section in
    Settings → Displays (icon, name, the properties it carries, apply and delete), a quick apply menu
    in the panel, and an Apply Display Preset App Intent. The store reuses the resolution-favourite
    shape — one JSON array per display under displayPresets.<identity>, the key dropped when the
    last preset goes — refuses a name already in use on that display, and applies through the same
    path reconnect-restore uses.

    Two choices are deliberate. A preset does not carry main-display status by default: applying
    one must not silently move the menu bar out from under another display, so mainStatus is
    restored only when it is asked for explicitly. And apply reports what it could not restore: a
    preset can name properties a display cannot accept (brightness on a panel with no brightness path,
    the XDR preset on a display that is not XDR-capable), and counting those as applied would be a
    lie — fbdcli prints a skipped: line, the panel says so in its notice, and Settings names them
    in the row.

    The extraction that made it possible is DisplayStateSnapshot: the private SavedState struct
    ConfigProtectionController wrote and read, made public. The stored blob is byte-identical, so
    a configuration saved by an older FBD still restores.

  • The pointer can cross between displays whose edges do not line up. macOS stops the pointer at an
    edge no other display covers, so a short display beside a tall one — or one lowered out of line —
    traps it in the gap between them. Settings.edgeCrossingEnabled (Settings → Displays, or
    fbdcli layout edge-crossing [on|off]) carries it over instead: FBD watches pointer movement and,
    when a push reaches an edge a neighbour's facing edge continues, lands the pointer just inside that
    neighbour at the height it was pushed at.

    Three things were measured rather than assumed, and each changed the design. The pointer cannot be
    polled for its direction:
    macOS clamps it to the displays' bounds, so a pointer pinned at an edge
    reports zero movement between samples — the push has to come from the event's own hardware delta
    (CGGetLastMouseDelta is gone from the Swift SDK). Warping needs no permission, and neither does
    a mouse-moved monitor
    — only key events need Accessibility, which is the trade
    CursorContainmentController refuses for an event tap and this feature does not have to make. And
    a warp holds hardware input back for a quarter of a second by default, which reads as the pointer
    sticking, so the suppression interval is zeroed before each warp.

    It fails closed. A neighbouring display that does not actually meet the edge means nothing happens —
    a deliberate gap is respected, and the geometry refuses to move a pointer macOS would have crossed by
    itself. It is inert with one display, and if the monitor never fires it simply does nothing: it can
    never fight the user for the pointer. Off by default, like everything else in FBD that moves it.

Fixed

  • DDC/CI now uses the framing Apple Silicon monitors expect (5-argument IOAVService I2C calls,
    0x84 0x03 / 0x82 0x01 packets, XOR checksum), matching the reference implementation. Brightness,
    contrast, volume, input and mute over DDC are rewritten accordingly; not yet verified on hardware (#7).
  • Mute media key no longer only unmutes; DDC slider drags send the latest value instead of queueing
    every step; the DDC service cache is refreshed on display changes; EDID product code and serial
    are read little-endian.
  • XDR upscaling above the hardware ceiling falls back to the software boost when the native preset
    write is refused (macOS 27); ending a boost no longer removes the dim or rounded-corner overlays.
  • A built-in display disabled by FBD, and a lock-dimmed brightness, are restored after a crash or quit.
  • fbdcli: a bad token or timeout is an error instead of silently driving the hardware directly;
    --direct inside automation payloads is preserved; help lists enable on its own line; zsh
    completion covers every command.
  • Creating a virtual screen no longer crashes FBD. The SkyLight calls that build a virtual
    display were handed pointers to a bridged NSString (the name) and NSArray (the optional modes)
    that were temporaries: Swift released them as soon as the pointer was taken, while SkyLight kept
    using it, so creating a screen from the app crashed. Both objects are now kept alive for the whole
    call.

Security

  • The HTTP API token and LG TV pairing key moved from preferences to the Keychain (migrated on first
    run). fbdcli may show a one-time Keychain prompt.
  • The local API rejects browser requests (non-loopback Host or any Origin), sends no CORS headers,
    caps concurrent connections and no longer falls back to a guessable token.
  • FBD_DISPLAY_NAME (from the monitor's EDID) is sanitised before it reaches automation scripts.

FBD 1.7.0

Choose a tag to compare

@FisiFla FisiFla released this 14 Oct 23:01

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 t...
Read more

FBD 1.6.0

Choose a tag to compare

@FisiFla FisiFla released this 04 Oct 07:45

⚠️ This build is UNSIGNED

Ad-hoc signed (TeamIdentifier not set) — this project has no Apple Developer ID
certificate yet. Two visible consequences:

  • Gatekeeper will refuse to open it. After unzipping: right-click FBD.app
    → Open → Open, or run xattr -dr com.apple.quarantine FBD.app
  • Sparkle auto-update is not available for this build. The feed can only be
    signed from a Developer ID–signed archive, so appcast.xml is unchanged and
    this release will not reach existing installs automatically.

A signed + notarized build is tracked in #6.

Build: universal (arm64 + x86_64) · Asset: FBD.zip
SHA-256: 38b625f9d5f07b7ea80aad90c0cec09adf5d9aa1dd027c52c2f643df03813c68


[1.6.0] — 2026-10-04

Added

  • A feedback layer. There was no loading or error state anywhere in the UI
    (grep -rn ProgressView Sources/FBD/ returned nothing).
    • FBDStatus + FBDStatusLine: an inline working/failed/succeeded line with a
      single combined VoiceOver announcement.
    • FBDNotice + FBDNoticeBanner: an app-level surface for menu-initiated
      actions, which need one because a menu dismisses on selection and a status
      line inside it would never be read.
    • The DDC panel shows a spinner while probing, then a checkmark or an explicit
      "No reply from the display".
  • DDC inputs are offered by name. When a display reports which inputs it has
    (the VCP 0x60 value list in its capabilities reply), the panel offers those by
    name — "HDMI 1", "DisplayPort 2" — and shows anything outside the MCCS table as
    Input 0xNN rather than attaching a plausible-looking wrong label. Only values
    that the display itself reports are offered, and the raw field remains as a
    manual override.
  • FBDCard — the display-card surface, previously copied byte-identically into
    four views.
  • fbdIconButton() — a 28pt hit target for icon-only controls, applied to seven
    of them. A bare Image(systemName:) in a borderless button is only as large as
    its glyph (roughly 12–16pt), which is easy to miss deliberately.

Changed

  • A design-review pass over the whole UI, driven by an Impeccable critique
    (24/40 on Nielsen's heuristics, run as two isolated assessments: a design
    review and a technical/accessibility evidence pass).
  • Motion now matches the product register. FBDTheme.animationSpring was a
    280ms spring with dampingFraction 0.7 — i.e. bounce — where a tool the user
    is mid-task with should use 150–250ms eased and no bounce. Renamed to
    animationPanel, since it no longer described itself correctly.
  • 32 spacing/padding literals routed through FBDTheme tokens, value-identically.

Fixed

  • Read capabilities froze the UI while it ran. The DDC probe blocks (a
    settle sleep plus up to three I2C reads) and its only caller was @MainActor,
    so the main thread stalled for the whole probe. A spinner would have been a
    lie, because SwiftUI cannot draw while that thread is blocked — so the read now
    runs on the per-display queue and reports back on the main actor. The same
    blocking applied to the panel's contrast/volume/mute read-back.
  • Controls that silently did nothing now say so. The DDC panel's input
    "Apply" returned early on an invalid value with no feedback of any kind; it is
    disabled until the field holds a usable value, with help text naming the range.
    Rotation and mirror/unmirror results were discarded, so a refused action closed
    the menu and looked dead; both now surface.
  • Configuration Protection misrepresented its own scope. It was a
    per-display menu toggle over one global flag, and enabling it saved only the
    display whose menu it was flipped from — leaving every other display apparently
    protected with nothing stored. It lives in Settings now, beside Layout
    protection, and snapshots every online display.
  • Show in Settings promised navigation it never performed. It posted no
    payload and Settings had no target-display concept, so it landed on the
    Overview page showing nothing about the display. It now opens the Per-Display
    tab and scrolls to that display.
  • Mirror Display opened empty on a single-display Mac, because its targets
    are other online displays. It is disabled with a reason instead.
  • A display with no DDC/CI now says so rather than silently omitting the
    panel, which read as a bug rather than as an absent capability.
  • The arrangement grid's display names were unreadable in light mode — 9pt
    white on a 25%-opacity accent fill, roughly 1.5:1. Non-dragging tiles use
    .primary, which adapts to the appearance.
  • release.yml looked for the wrong CHANGELOG heading. It searched for the
    tag form while headings are written without the v — so every release after
    1.4.0 would have shipped with empty release notes. The search is now anchored to
    the start of a line, because an unanchored pattern also matches a passing
    mention of a heading inside prose and then the notes begin mid-sentence.

Accessibility

  • Four controls had no accessible name. The automation rule toggle and its
    three pickers were built as Toggle("") / Picker("") with .labelsHidden()
    — which hides a label visually but still exposes it, so an empty one left
    VoiceOver with nothing to announce. They now carry real hidden names, and the
    rule's remove button has an accessible label alongside its tooltip.
  • Reduce Motion was bypassed in two places. The footer's expanders animated
    regardless of the setting, in a file that already guards four other animations.
    A reduceMotion ? .opacity : .opacity ternary had identical branches, so it
    did nothing while appearing to handle the setting.

[1.5.0] — 2026-10-03