Closed EBNull closed 8 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
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:
SetupDi
family gets you to the right keyI 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 .
Additional pointers (in no specific order):
GetAllDisplayDevices
- https://github.com/winlabs/gowin32/blob/0d265587d3c90d3dca04c4927c845b5334ade86f/sysinfo.go#L70-L92GetAllDisplayMonitors
- https://github.com/winlabs/gowin32/blob/0d265587d3c90d3dca04c4927c845b5334ade86f/sysinfo.go#L105C6-L119My 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
}
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?
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 ofEDID
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.