LGUG2Z / komorebi

A tiling window manager for Windows 🍉
https://lgug2z.github.io/komorebi/
Other
9.36k stars 188 forks source link

[FEAT]: Use stable physical monitor identifiers for the multimonitor case #612

Closed EBNull closed 8 months ago

EBNull commented 10 months ago

Hi, I noticed issues like #364, #275, and thought I could add some additional context.

I think your solution in #275 will work most of the time (as I think it relies on GDI's virtual screen coordinates), though if you want to uniquely identify physical monitors, I have some pointers for you.

I was working on a map-virtual-to-physical monitor problem in the past and really found only the EDID was persistent and unique enough to identify monitors. I wrote up my journey at https://gist.github.com/EBNull/d65bacceefc58f5f1d728a66039807d2 .

In short, using a combination of GDI, SetupDi, EDD_GET_DEVICE_INTERFACE_NAME, and a little bit of EDID decoding, you can map these around.

This is probably more in-depth than needed (since komorebi is really exclusively in the "virtual screen" layer of abstraction) since you now remember monitors by virtual position, but I thought you might appreciate the additional context.

And thanks for referencing my gist in https://github.com/LGUG2Z/komorebi/commit/e04ba0e033c4515095d1290019b21bec7a644204 ! It was really neat to stumble across this project, read a few commits and issues, and find my old gist helping someone out.

LGUG2Z commented 10 months ago

Thanks so much for sharing this, and for the gist about programmatically changing focus!

I have started slowly working on assigning EDIDs to monitors in komorebi if anyone wants to follow along on YouTube: https://www.youtube.com/watch?v=N_IL53wYcXs

EBNull commented 10 months ago

Thanks for recording! Watching that reminded me of the similar quagmire I found myself in clicking through those docs - I forgot how bad it was.

I added some small notes to the linked commit above.

General thoughts:

EBNull commented 10 months ago

I started writing more here, but then I figured, eh, have some code. I assume you share the same username on gitlab, so, see https://gitlab.com/ebnull/hudctl/-/blob/master/monman/collectmoninfo_windows.go and https://gitlab.com/ebnull/hudctl/-/blob/master/monman/monitors_windows.go .

EBNull commented 10 months ago

Additional pointers (in no specific order):

My own code, "augmented" structures and a combination function:

type DisplayMonitorInfoAugmented struct {
    DisplayMonitorInfo gowin32.DisplayMonitorInfo
    MonitorInfoEx      *win32.MONITORINFOEXW
}

type PhysicalMonitorInfoAugmented struct {
    DisplayDevices   []win32.AdapterMonitorDisplayDevices
    PhysicalMonitors *win32.PhysicalMonitorArray
}

type CollectedWindowsAdapterInfo struct {
    DisplayMonitorInfoAugmented
    PhysicalMonitorInfoAugmented
}

func CollectWindowsAdapterAndMonitorInfo() ([]CollectedWindowsAdapterInfo, error) {
    dmmap := map[string]gowin32.DisplayDevice{}
    for _, dd := range gowin32.GetAllDisplayDevices() {

        dmmap[dd.DeviceName] = dd

    }
    var err error
    ret := []CollectedWindowsAdapterInfo{}

    for _, dm := range gowin32.GetAllDisplayMonitors() {
        cmi := CollectedWindowsAdapterInfo{DisplayMonitorInfoAugmented: DisplayMonitorInfoAugmented{DisplayMonitorInfo: dm}}

        cmi.MonitorInfoEx, err = win32.GetMonitorInfo(windows.Handle(dm.Handle))
        if err != nil {
            return nil, err
        }

        cmi.PhysicalMonitors, err = win32.GetPhysicalMonitorsFromHMONITOR(windows.Handle(dm.Handle))
        if err != nil {
            return nil, err
        }
        cmi.DisplayDevices = win32.GetDisplayDevicesFromAdapterDeviceName(cmi.MonitorInfoEx.Device())

        ret = append(ret, cmi)
    }

    return ret, nil
}

Usage of the data to get the EDIDs:

// +build windows

package monman

import (
    "fmt"
    "github.com/davecgh/go-spew/spew"
    "gitlab.com/ebnull/hudctl/monman/win32"
    "strings"
    //"golang.org/x/sys/windows/registry"
)

const (
    DISPLAY_DEVICE_ACTIVE           = 0x00000001
    DISPLAY_DEVICE_PRIMARY_DEVICE   = 0x00000004
    DISPLAY_DEVICE_MIRRORING_DRIVER = 0x00000008
    DISPLAY_DEVICE_VGA_COMPATIBLE   = 0x00000010
    DISPLAY_DEVICE_REMOVABLE        = 0x00000020
    DISPLAY_DEVICE_MODESPRUNED      = 0x08000000
)

func init() {
    MonMan = &WindowsMonitorManager{}
}

type WindowsMonitorManager struct{}

func (wmm *WindowsMonitorManager) GetMonitors() ([]BasicMonitor, error) {
    ret := []BasicMonitor{}

    seen := map[string]struct{}{}

    cmis, err := CollectWindowsAdapterAndMonitorInfo()
    if err != nil {
        return ret, err
    }
    for _, cmi := range cmis {
        for _, ddinfo := range cmi.DisplayDevices {
            if ddinfo.Monitor == nil {
                // Enabled monitor but not configured?
                spew.Dump(ddinfo)
                // TODO: Expose this issue
                continue
            }
            if DISPLAY_DEVICE_ACTIVE&ddinfo.Monitor.StateFlags == 0 {
                continue
            }
            if ddinfo.Monitor.DeviceName == "" {
                continue
            }
            _, ok := seen[ddinfo.Monitor.DeviceName]
            if ok {
                continue
            }
            seen[ddinfo.Monitor.DeviceName] = struct{}{}
            regPath, edidBytes, err := GetMonitorRegPathAndEdid(ddinfo.MonitorDeviceInterfaceName)
            if err != nil {
                return ret, err
            }
            regPath = strings.Replace(regPath, "\\REGISTRY\\MACHINE", "HKEY_LOCAL_MACHINE", 1)
            vd, err := NewVendorDataFromEdid(edidBytes)
            if err != nil {
                return ret, err
            }
            hd := HostData{
                PhysicalPath:           ddinfo.Monitor.DeviceName,
                LogicalPath:            ddinfo.MonitorDeviceInterfaceName,
                LogicalMonitorIdentity: fmt.Sprintf("%s (%s%04X) %s", vd.Name, vd.ManufacturerId, vd.ProductCode, vd.SerialNumber),
            }
            mon := WindowsMonitor{HostData: hd, VendorData: vd, RegPath: regPath, DisplayMonitorInfoAugmented: cmi.DisplayMonitorInfoAugmented, PhysicalMonitors: cmi.PhysicalMonitors, DDInfo: ddinfo}
            ret = append(ret, mon)
        }
    }

    //fmt.Printf("hMonitor %d @ (%d, %d) [%s] - %s\n", i, m.Rectangle.Left, m.Rectangle.Top, mie, pma.PhysicalMonitor)
    //pma.Close()

    return ret, nil
}

type WindowsMonitor struct {
    HostData
    VendorData
    RegPath                     string
    DisplayMonitorInfoAugmented DisplayMonitorInfoAugmented
    PhysicalMonitors            *win32.PhysicalMonitorArray
    DDInfo                      win32.AdapterMonitorDisplayDevices
}

func (wm WindowsMonitor) GetHostData() HostData {
    return wm.HostData
}

func (wm WindowsMonitor) GetVendorData() VendorData {
    return wm.VendorData
}

EDID stuff:

package monman

import (
    "fmt"
    "gitlab.com/ebnull/hudctl/monman/win32"
    //"github.com/davecgh/go-spew/spew"
    //"github.com/winlabs/gowin32"
    //w "github.com/winlabs/gowin32/wrappers"
    "golang.org/x/sys/windows/registry"
    "strings"
)

func GetMonitorRegPathAndEdid(monitorPath string) (string, []byte, error) {
    monitorPath = strings.ToLower(monitorPath)

    //fmt.Printf("Looking for %s\n", spew.Sdump(monitorPath))

    hDevInfo, err := win32.SetupDiGetClassDevsEx(win32.GUID_DEVINTERFACE_MONITOR, "", 0, win32.DIGCF_DEVICEINTERFACE, 0, "")
    if err != nil {
        return "", []byte{}, err
    }
    //fmt.Printf("Got hDevInfo: %#v\n", hDevInfo)

    devpath := ""
    i := 0
    for i = 0; true; i++ {
        difd, err := win32.SetupDiEnumDeviceInterfaces(hDevInfo, 0, win32.GUID_DEVINTERFACE_MONITOR, i)
        if difd == nil && err == nil {
            break
        }
        //fmt.Printf(spew.Sdump(difd))
        if err != nil {
            return "", []byte{}, err
        }

        devpath, err = win32.SetupDiGetDeviceInterfaceDetail(hDevInfo, difd)
        if err != nil {
            return "", []byte{}, err
        }
        if strings.ToLower(devpath) == monitorPath {
            break
        }
    }
    if strings.ToLower(devpath) != monitorPath {
        return "", []byte{}, fmt.Errorf("Could not find %s in SetupApi", monitorPath)
    }

    did, err := win32.SetupDiEnumDeviceInfo(hDevInfo, i)
    if err != nil {
        return "", []byte{}, err
    }
    hKey, err := win32.SetupDiOpenDevRegKey(hDevInfo, did, win32.DICS_FLAG_GLOBAL, 0, win32.DIREG_DEV, win32.KEY_READ)
    if err != nil {
        return "", []byte{}, err
    }
    regkey := registry.Key(hKey)
    edidBytes, _, err := regkey.GetBinaryValue("EDID")
    if err != nil {
        return "", []byte{}, err
    }

    keyName, err := win32.NtQueryKeyName(hKey)
    if err != nil {
        return "", []byte{}, err
    }

    return keyName, edidBytes, err
}
gazpachoking commented 8 months ago

Oh, this seems related to my question here https://github.com/LGUG2Z/komorebi/discussions/657, but I'm not sure if the solution implemented is applicable to my case? Is there a way now I can pin a given monitor workspace to a specific monitor when I change which monitors are plugged in to my laptop?