https://github.com/gingerbeardman/didact
Control your BenQ RD280UG from the command line
https://github.com/gingerbeardman/didact
benq ddc display-control macos-app menu-bar-app rd280
Last synced: 13 days ago
JSON representation
Control your BenQ RD280UG from the command line
- Host: GitHub
- URL: https://github.com/gingerbeardman/didact
- Owner: gingerbeardman
- License: other
- Created: 2026-06-13T21:58:09.000Z (20 days ago)
- Default Branch: main
- Last Pushed: 2026-06-16T22:31:55.000Z (17 days ago)
- Last Synced: 2026-06-17T00:19:01.253Z (17 days ago)
- Topics: benq, ddc, display-control, macos-app, menu-bar-app, rd280
- Language: Swift
- Homepage: https://www.gingerbeardman.com/apps/
- Size: 271 KB
- Stars: 4
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Didact (previously BtnQ)
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 vs BenQ Display Pilot 2
| | Didact | Display Pilot 2 |
|-----------|:----------:|:---------------:|
| Download | **517 KB** | 404 MB |
| Installed | **578 KB** | 936 MB |
…about **800× smaller to download** and **over 1,600× smaller installed**, for the
controls you actually use.
## Requirements
- 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.
## How it works
- **DDC transport** is a vendored copy of [AppleSiliconDDC](https://github.com/waydabber/AppleSiliconDDC)
(MIT). The private CoreDisplay/IOKit symbols it needs are declared in
`AppleSiliconDDCBridge.swift` via `@_silgen_name`, so the whole library is just
two Swift files — no bridging header, no SPM dependency. `CoreDisplay.framework`
is linked via `OTHER_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](https://github.com/iurev/bebenqli),
a Linux TUI for the same panel.
## Adding support for another monitor
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**).
### Config format
```jsonc
{
"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 }
]
}
```
**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`.
## Debugging — dump DDC values
`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
```
## Thanks
Didact is built directly on these two projects:
- **[AppleSiliconDDC](https://github.com/waydabber/AppleSiliconDDC)** by [@waydabber](https://github.com/waydabber) (MIT) — the DDC/CI transport, vendored into Didact.
- **[bebenqli](https://github.com/iurev/bebenqli)** by [@iurev](https://github.com/iurev) (MIT) — the BenQ RD280UG DDC/VCP map and the baseline-sweep discovery behind Listen mode.
## License
Didact is released under the [MIT License](LICENSE). Both vendored/ported
components above are also MIT-licensed; their notices are reproduced in the
[`LICENSE`](LICENSE) file.