A tiny macOS menu-bar app to control an external monitor over DDC/CI — no settings window, no ~1 GB control panel, just standard macOS menus.
Initially built for the BenQ RD280UG (which it ships with a profile for), Didact now works toward supporting any DDC/CI monitor: a built-in wizard detects the standard controls and learns the rest, and every monitor-specific detail lives in a shareable JSON profile — so new monitors need no code changes.
| Didact | Display Pilot 2 | |
|---|---|---|
| Download | 338 KB | 404 MB |
| Installed | 360 KB | 936 MB |
…about 1,200× smaller to download and over 2,600× smaller installed, for the controls you actually use.
- Apple Silicon Mac (DDC is done via
IOAVService, Apple-Silicon only). - A monitor connected over DisplayPort / USB-C / HDMI with DDC/CI enabled.
- macOS 13.0 or later.
- DDC transport is a vendored copy of AppleSiliconDDC
(MIT). The private CoreDisplay/IOKit symbols it needs are declared in
AppleSiliconDDCBridge.swiftvia@_silgen_name, so the whole library is just two Swift files — no bridging header, no SPM dependency.CoreDisplay.frameworkis linked viaOTHER_LDFLAGS = -framework CoreDisplay. - The app is not sandboxed (
ENABLE_APP_SANDBOX = NO): DDC needs raw IOKit access, which the App Sandbox forbids. - The DDC/VCP map was ported from bebenqli, a Linux TUI for the same panel.
Drop a JSON file into either:
- the app bundle (
Didact/Monitors/), or ~/Library/Application Support/Didact/Monitors/(no rebuild — use Didact ▸ Open Monitors Folder, then Reload Configs).
Field reference
| Field | Applies to | Meaning |
|---|---|---|
kind |
all | group, section, range, cycle, or toggle |
label |
all | Menu text |
vcp |
range/cycle/toggle | VCP feature code, hexadecimal (e.g. "60", "d9") |
min/max/step |
range | Slider bounds (decimal); step defaults to 1 |
options |
cycle | List of { value, label } |
onValue/offValue |
toggle | Values written for on/off |
channel |
range/cycle/toggle | High byte written for 16-bit multiplexed registers (e.g. Moon Halo on d9) |
readChannels |
range/cycle/toggle | High byte(s) that identify this control's value on read (a multiplexed read returns only the last-touched channel). Defaults to channel. |
noRead |
any control | Value can't be read back; Didact remembers the last value you set |
noVerify |
any control | Monitor reports a bogus value after a write |
Values: a JSON number is decimal; a JSON string is hexadecimal
("0x30" or "30"). vcp is always hex.
Multiplexed registers: some BenQ features share one VCP code, selected by the
high byte. Moon Halo brightness and colour temperature both live on d9
(channel: "0x01" and channel: "0x07"); Didact writes (channel << 8) | value.
Tools/dump.sh compiles a small CLI from the app's own DDC code and prints the
current value of every candidate VCP code, decoded against the config:
./Tools/dump.sh # uses Didact/Monitors/BenQ-RD280UG.json
./Tools/dump.sh path/to/other.json # decode against another config
Didact is built directly on these two projects:
- AppleSiliconDDC by @waydabber (MIT) — the DDC/CI transport, vendored into Didact.
- bebenqli by @iurev (MIT) — the BenQ RD280UG DDC/VCP map and the baseline-sweep discovery behind Listen mode.
Didact is released under the MIT License. Both vendored/ported
components above are also MIT-licensed; their notices are reproduced in the
LICENSE file.


{ "name": "BenQ RD280UG", // shown in the menu "match": ["RD280U", "RD280UG"], // case-insensitive substrings of the display's product name "controls": [ { "kind": "group", "label": "Image" }, { "kind": "section", "label": "Night Protection" }, // Slider. min/max/step are decimal. { "kind": "range", "label": "Brightness", "vcp": "10", "min": 0, "max": 100 }, // Pick-one. Each option value mirrors the DDC docs (hex string) or a decimal number. { "kind": "cycle", "label": "Source", "vcp": "60", "options": [ { "value": "0x0f", "label": "DisplayPort" }, { "value": "0x11", "label": "HDMI" }, { "value": "0x13", "label": "USB-C" } ] }, // On/off. { "kind": "toggle", "label": "Auto Brightness", "vcp": "e2", "onValue": 255, "offValue": 0 } ] }