Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Monitor Switch — Software KVM Switch

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.

Features

  • 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 m1ddc cannot confirm input switch
  • Logs to ~/.config/monitor-switch/monitor-switch.log

Prerequisites

Linux (Kubuntu / KDE Plasma)

sudo apt install ddcutil

To run without sudo, add your user to the i2c group:

sudo usermod -aG i2c $USER
# Log out and back in

macOS (Apple Silicon)

brew install m1ddc

Optional (recommended when some displays do not react to m1ddc input):

Building from Source

Requires Go 1.21+ and a C compiler (CGO is needed by the system tray library).

Linux build dependencies

sudo apt install gcc libayatana-appindicator3-dev

Build

git clone https://github.com/arumata/monitor-switch.git && cd monitor-switch
go build -o monitor-switch .

Cross-compilation

Note: The systray library 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 .

Automated builds (GitHub Actions)

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.

Configuration

Copy config.json.example to config.json and edit it. The app searches for config.json in this order:

  1. Next to the executable
  2. Current working directory
  3. ~/.config/monitor-switch/config.json

Example 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"
      }
    }
  ]
}

Configuration fields

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:

  1. monitors[].betterdisplay_identifier (if set)
  2. otherwise monitors[].mac_identifier
  3. otherwise nameLike=<monitors[].name>

If betterdisplay_identifier is set for a monitor, BetterDisplay is preferred first for that monitor (with m1ddc used as backup).

Finding Bus IDs on Linux

ddcutil detect

Finding input source codes

ddcutil capabilities --bus=<BUS_ID> | grep -A20 "Feature: 60"

Usage

./monitor-switch

Auto-generate config (macOS)

Generate config.json for the current machine (detected via m1ddc) and exit:

./monitor-switch --autogen-config

Optional 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

Project Structure

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

License

MIT

About

Cross-platform Software KVM Switch — switch monitor inputs via system tray (ddcutil/m1ddc)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages