Repository navigation
Releases: FisiFla/FBD
Release list
FBD 2.1.4
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
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.mdexplains why the Keychain item's
access control list is not widened to avoid the prompt.
FBD 2.1.2
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, andSecItemCopyMatchingblocks 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
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 indocs/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'savailableis true,enabled
reads back andsetEnabled:round-trips — off reads back off, on reads back on. The claim came
from a note in Lunar'sCBTrueToneClient.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.mdgained 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 -lintaccepts 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
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 carriesnullplus 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|codexwrites an agent-readySKILL.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;--printwrites 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-displaydisconnect/reconnectaction (withremember), ahidpiaction
(install,remove,status) and agamma-flooraction (enabled, optionalthreshold).
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> reconnectand
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-tokenno longer prints the API token to stdout. It now explains how to obtain
it;--revealprints 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 thefbd://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.mdaudits 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.shis 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-testadds one
set-and-read-back round trip per feature with the original value restored afterwards,
--jsonprints the machine-readable evidence, and--ledgerappends a timestamped
evidence block todocs/PRIVATE_APIS.md. With no DDC display attached, every check is
skipped and the script exits 0 (issue #26).
FBD 2.0.0
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 newSettingsPane/SettingsGroupcontainers
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
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). IOAVServiceCreateWithServiceandIOAVServiceCopyEDIDwere 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-syncexited before its debounced brightness writes landed, so it reported success without changing anything.fbdcli ddc-testnow 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
lodpiinstead ofsdr, which is an HDR term. - EDID overrides are disabled on Apple Silicon.
IOAVServiceSetVirtualEDIDModewas 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
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 — withAll Modesas the only second level. The Virtual Screens
page keepsReconnect 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, orFBD_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
stateConfigProtectionControlleralready 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 anApply Display PresetApp Intent. The store reuses the resolution-favourite
shape — one JSON array per display underdisplayPresets.<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, somainStatusis
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 —fbdcliprints askipped:line, the panel says so in its notice, and Settings names them
in the row.The extraction that made it possible is
DisplayStateSnapshot: the privateSavedStatestruct
ConfigProtectionControllerwrote 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
(CGGetLastMouseDeltais 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
CursorContainmentControllerrefuses 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
IOAVServiceI2C calls,
0x84 0x03/0x82 0x01packets, 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;
--directinside automation payloads is preserved;helplistsenableon 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 bridgedNSString(the name) andNSArray(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).fbdclimay show a one-time Keychain prompt. - The local API rejects browser requests (non-loopback
Hostor anyOrigin), 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
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 decoderreport --dpcdalready 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
.withinWindowmode,
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: onlycontentViewis guaranteed to sit
inside the glass (so the SwiftUI content goes there, not in as a subview), and.behindWindowis
not a substitute for.withinWindowhere. - 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.stringsbeside the icon inSources/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 resolvesText("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,
SettingsSliderRowanddisclosureSectiontooktitle: String, andText(String)is verbatim
— SwiftUI only looks a key up through aLocalizedStringKey— 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 becameLocalizedStringKeyand 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 renderedText(item.rawValue), a String, so its labels were verbatim too, andSettingsPage
now spells its cases out in aswitchreturningLocalizedStringKey— which keeps every key a
literal the test can verify (236 keys each). What the drift test cannot reach is an interpolated
string: SwiftUI turnsText("\(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 ownlabel/token— those stay English on purpose, becauseFBD_EVENT_LABEL
hands them to shell rules as a script contract, so the UI maps those cases itself. Two things worth
recording —Sources/FBD/Resourcesisexcluded from the SwiftPM target (which is why Info.plist
lives there and is copied by hand), so the.lprojdirectories have to be copied by the Makefile;
andCFBundleLocalizationsmust not sit alongside real.lprojdirectories, 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. TheDouble → UInt16conversion 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 asfbdcli 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.CuratedModesis 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.
ProtectedPropertiesoffered 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 throughfbdcli tv. Attachments are keyed by the display's
identity, not its id, so a reconnect keeps them;fbdcli config device attach|detach|attachmentsmanages 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 orfbdcli automation run— and anotifyaction 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 runrefuses 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
DisplayReportin FBDCore and is locked by format tests, so it cannot drift silently.- **The HTTP API covers four more of t...
FBD 1.6.0
⚠️ This build is UNSIGNEDAd-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 runxattr -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, soappcast.xmlis 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 0xNNrather 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 bareImage(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.animationSpringwas a
280ms spring withdampingFraction 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
FBDThemetokens, value-identically.
Fixed
Read capabilitiesfroze 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 Protectionmisrepresented 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 Settingspromised 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 Displayopened 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.ymllooked for the wrong CHANGELOG heading. It searched for the
tag form while headings are written without thev— 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 asToggle("")/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.
AreduceMotion ? .opacity : .opacityternary had identical branches, so it
did nothing while appearing to handle the setting.