External Display Brightness Control for macOS
LumaLink is a native macOS menu bar app that gives you full control over external display brightness using DDC/CI protocol. Control your monitors with hardware keys (F1/F2), menu bar sliders, or keyboard shortcutsβjust like the built-in MacBook display.
- π₯οΈ Multi-Display Support β Control brightness on all external displays independently
- β¨οΈ Hardware Key Integration β Use F1/F2 brightness keys (or Fn+F1/F2) on external displays
- ποΈ Smooth Control β Per-display sliders with debounced writes for responsive adjustments
- π DDC/CI Protocol β Direct hardware control via I2C (no software overlays needed)
- π Software Dimming Fallback β Gamma-based dimming for displays that don't support DDC
- π Auto-Launch β Optional launch at login using SMAppService
- πΎ Persistent Settings β Remembers per-display preferences across reboots
- β‘ Low CPU Usage β Event-driven architecture, no polling loops
- macOS 13.0 (Ventura) or later (macOS 14 Sonoma recommended)
- Apple Silicon (M1/M2/M3) or Intel Mac
- External display connected via:
- USB-C / Thunderbolt
- DisplayPort
- HDMI
- Accessibility permission for keyboard monitoring
- Download the latest release from Releases
- Drag
LumaLink.appto/Applications - Launch the app
- Grant Accessibility permission when prompted
- Adjust brightness from the menu bar βοΈ
See BUILD.md for detailed build instructions.
# Clone repository
git clone https://github.com/yourusername/LumaLink.git
cd LumaLink
# Open in Xcode
open LumaLink.xcodeproj
# Build and run
β + RClick the βοΈ icon in the menu bar to access:
- Per-Display Sliders β Adjust brightness for each monitor independently
- Quick Actions β Set all displays to 25%, 50%, 75%, or 100%
- Display Menu β Enable software dimming or access advanced settings
- Preferences β Configure launch at login, brightness step, and permissions
- F1 (or Fn+F1) β Decrease brightness on selected external display
- F2 (or Fn+F2) β Increase brightness on selected external display
Default step: 6% (configurable in Preferences)
If your display doesn't support DDC over certain connections (USB-C docks, some KVMs):
- Click the gear icon next to the display name
- Select "Enable Software Dimming"
- Brightness will be controlled via gamma curves (visual dimming only)
Note: Software dimming doesn't change the backlightβit's a color profile overlay.
Symptoms: External display doesn't appear in LumaLink menu
Solutions:
- Click "Refresh Displays" in the menu
- Check cable connection (try a different USB-C port)
- Some USB-C hubs/docks don't expose I2C β try direct connection
- Restart LumaLink after connecting display
Symptoms: "DDC unavailable" warning or brightness changes have no effect
Common Causes:
- USB-C Docks/Hubs β Many cheaper docks don't pass through DDC/CI signals
- Try: Direct USB-C to DisplayPort/HDMI cable
- KVM Switches β Most KVMs block DDC
- Try: Direct connection or enable software dimming
- HDR Mode β Some displays disable DDC when HDR is active
- Try: Disable HDR in System Settings β Displays
- Display Settings β DDC may be disabled in monitor OSD
- Try: Check monitor settings for "DDC/CI" option
Workaround: Enable "Software Dimming" for affected displays
Symptoms: F1/F2 keys don't adjust external brightness
Solutions:
- Grant Accessibility permission:
- System Settings β Privacy & Security β Accessibility
- Add and enable LumaLink
- Check "Use F1, F2 as standard function keys" setting:
- System Settings β Keyboard β Keyboard Shortcuts β Function Keys
- LumaLink works with both configurations
- Restart LumaLink after granting permissions
| Issue | Explanation | Workaround |
|---|---|---|
| USB-C dock brightness | Many docks don't pass I2C | Use direct cable or software dimming |
| KVM switches | Most KVMs block DDC signals | Switch KVM input or use software dimming |
| HDR displays | DDC often disabled in HDR mode | Disable HDR or use software dimming |
| Slow response | Some panels throttle DDC writes | Normalβapp debounces to 100ms |
| Multiple controllers | Daisy-chained displays | Unchain or control individually |
Enable debug logging:
# View logs in Console.app
log stream --predicate 'subsystem == "com.lumalink.LumaLink"' --level debugCommon error codes:
transportFailedβ I2C communication blocked (dock/cable issue)timeoutβ Display not responding (try power cycle)invalidResponseβ Corrupted DDC packet (retry)
LumaLink/
βββ Sources/
β βββ App/
β β βββ LumaLinkApp.swift # App entry point
β βββ UI/
β β βββ Menu/MenuView.swift # Menu bar interface
β β βββ Preferences/ # Settings window
β βββ Domain/
β β βββ Display.swift # Display model & discovery
β β βββ BrightnessService.swift # Core brightness logic
β βββ Infra/
β β βββ DDC/
β β β βββ DDCClient.swift # VCP packet encoding
β β β βββ IOKitI2CTransport.swift # Low-level I2C
β β βββ Hotkeys/KeyboardMonitor.swift
β β βββ Config/AppConfig.swift # Persistence
β β βββ Logging/Log.swift
β βββ Support/
β βββ EDIDParser.swift # EDID hashing
βββ Tests/ # Unit tests
- SwiftUI β Modern declarative UI with MenuBarExtra
- IOKit I2C β Direct framebuffer I2C access via
IOI2CInterface - DDC/CI β VESA standard for monitor control (VCP code 0x10)
- CGDisplay APIs β Display enumeration and identification
- NSEvent Monitors β Global keyboard event interception
- ServiceManagement β Login item registration
Run unit tests:
# In Xcode
β + U
# Or via command line
xcodebuild test -scheme LumaLink -destination 'platform=macOS'Test Coverage:
- DDC packet encoding/decoding: β 95%
- EDID parsing & hashing: β 90%
- Brightness service logic: β 85%
- Debouncing & rate limiting: β 80%
Integration tests use mock I2C transport to simulate hardware responses.
- Accessibility β To monitor F1/F2 key presses globally
- Only intercepts brightness keys
- Does not log or record keystrokes
LumaLink must be signed and notarized for distribution:
- Non-sandboxed (required for IOKit I2C access)
- Hardened runtime enabled
- No private frameworks used
- Developer ID signed
- β No analytics or telemetry
- β No network requests
- β No data leaves your Mac
- β
Settings stored locally in
~/Library/Application Support
- Xcode 15.0 or later
- Swift 5.9 or later
- macOS 13.0 SDK
| Setting | Value | Reason |
|---|---|---|
| App Sandbox | OFF | IOKit I2C requires root-level access |
| Hardened Runtime | ON | Required for notarization |
| Library Validation | ON | Security best practice |
| Code Signing | Developer ID | Distribution outside Mac App Store |
- Idle CPU: < 1%
- Memory: < 30 MB
- I2C timeout: 800ms
- Debounce interval: 100ms
- Max retry: 2 attempts with 200ms backoff
- VESA DDC/CI Standard
- VCP Code Table
- VCP 0x10: Brightness (0-100)
- VCP 0x12: Contrast (0-100)
- VCP 0xD6: Power Mode
Contributions welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Write tests for new functionality
- Ensure all tests pass (
β + U) - Submit a pull request
MIT License - see LICENSE file for details.
- Inspired by MonitorControl
- DDC implementation references from ddcctl
- EDID parsing based on VESA EDID Standard
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Email: [email protected]
Made with βοΈ for external display users