A lightweight cross-platform desktop app that switches monitor video inputs via DDC/CI. It lives in the system tray and wraps ddcutil (Linux), then uses m1ddc with optional BetterDisplay fallback on macOS.
- Switch all monitors simultaneously (parallel goroutines)
- Switch each monitor individually via submenus
- Dynamic tray menu built from configuration
- Auto-detects OS to use the correct backend (
ddcutil/m1ddc) - On macOS, falls back to BetterDisplay CLI when
m1ddccannot confirm input switch - Logs to
~/.config/monitor-switch/monitor-switch.log
sudo apt install ddcutilTo run without sudo, add your user to the i2c group:
sudo usermod -aG i2c $USER
# Log out and back inbrew install m1ddcOptional (recommended when some displays do not react to m1ddc input):
- Install BetterDisplay in
/Applications
Requires Go 1.21+ and a C compiler (CGO is needed by the system tray library).
sudo apt install gcc libayatana-appindicator3-devgit clone https://github.com/arumata/monitor-switch.git && cd monitor-switch
go build -o monitor-switch .Note: The
systraylibrary uses CGO to call native OS APIs. Cross-compiling Linux → macOS (or vice versa) is not possible without a C cross-compiler. Each binary must be built on the target platform.
# On a Linux machine:
go build -o monitor-switch-linux-amd64 .
# On macOS (Apple Silicon):
go build -o monitor-switch-darwin-arm64 .The repository includes .github/workflows/build.yml which builds both binaries
on native runners (Ubuntu + macOS). Triggered by pushing a v* tag or manually via workflow_dispatch.
Copy config.json.example to config.json and edit it. The app searches for config.json in this order:
- Next to the executable
- Current working directory
~/.config/monitor-switch/config.json
{
"profiles": {
"mac_profile_name": "Mac",
"pc_profile_name": "PC"
},
"monitors": [
{
"name": "Gigabyte M27Q P",
"linux_identifier": "--bus=10",
"mac_identifier": "1",
"inputs": {
"pc": "0x0f",
"mac": "0x1b"
}
},
{
"name": "Gigabyte G27F",
"linux_identifier": "--bus=8",
"mac_identifier": "2",
"betterdisplay_identifier": "UUID=EF93549E-FD0C-4F9B-8EC5-26FCA463CAFD",
"inputs": {
"pc": "0x0f",
"mac": "18"
}
}
]
}| Field | Description |
|---|---|
profiles.mac_profile_name |
Display name for the Mac profile (shown in menu) |
profiles.pc_profile_name |
Display name for the PC profile (shown in menu) |
monitors[].name |
Monitor display name |
monitors[].linux_identifier |
Arguments for ddcutil (e.g. --bus=10) |
monitors[].mac_identifier |
Display selector for m1ddc (e.g. 1 or uuid=<UUID>) |
monitors[].betterdisplay_identifier |
Optional BetterDisplay selector used as macOS fallback (e.g. UUID=<UUID>) |
monitors[].inputs.pc |
Input source code for PC (hex for ddcutil, decimal for m1ddc) |
monitors[].inputs.mac |
Input source code for Mac (18, 0x12, alt:145) |
On macOS, the app first tries m1ddc, then falls back to BetterDisplay CLI if m1ddc fails or does not confirm the requested input.
Fallback selection uses:
monitors[].betterdisplay_identifier(if set)- otherwise
monitors[].mac_identifier - otherwise
nameLike=<monitors[].name>
If betterdisplay_identifier is set for a monitor, BetterDisplay is preferred first for that monitor (with m1ddc used as backup).
ddcutil detectddcutil capabilities --bus=<BUS_ID> | grep -A20 "Feature: 60"./monitor-switchGenerate config.json for the current machine (detected via m1ddc) and exit:
./monitor-switch --autogen-configOptional flags:
--config-path=/path/to/config.json— output path (default:./config.json)--force— overwrite existing config file
Generated monitor entries include mac_identifier (uuid=...) and betterdisplay_identifier (UUID=...), set inputs.mac to the currently detected input, and set inputs.pc to a reasonable default guess (15 or 18).
When BetterDisplay is installed, auto-generation prefers BetterDisplay CLI for input detection (more reliable on displays where m1ddc get input is noisy).
The app appears in the system tray. Menu structure:
- Switch ALL to Mac — switches all monitors in parallel
- Switch ALL to PC — switches all monitors in parallel
- Per-monitor submenus for individual switching
- Open Config — opens the config directory
- Quit — exits the application
monitor-switch/
├── main.go # Entry point, system tray logic
├── config/
│ └── config.go # JSON config parsing
├── commander/
│ └── commander.go # DDC/CI execution (ddcutil, m1ddc + BetterDisplay fallback)
├── icon/
│ ├── icon.go # Embedded tray icon (go:embed)
│ ├── icon.png # PNG icon for the tray
│ └── gen/
│ └── main.go # Icon generator
├── config.json.example # Example configuration
├── .github/workflows/
│ └── build.yml # CI: builds on Linux + macOS
├── go.mod
└── go.sum
MIT