Skip to content

Instantly share code, notes, and snippets.

@ewollesen
Created October 1, 2018 20:17
Show Gist options
  • Save ewollesen/1529d20b4ad8726f61e1144a7540f32c to your computer and use it in GitHub Desktop.
Save ewollesen/1529d20b4ad8726f61e1144a7540f32c to your computer and use it in GitHub Desktop.
Parses the EDID info reported by xrandr to print names for monitors
// Copyright (C) 2018 Eric Wollesen <ericw at xmtp dot net>
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// display-names prints the names of the connected displays.
//
// It reads the names from EDID information, which it obtains via calling out to
// xrandr(1).
package main
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"log"
"os/exec"
"regexp"
"strings"
)
func main() {
err := Main()
if err != nil {
log.Fatal(err)
}
}
func Main() (err error) {
displays, err := readEDIDBlocks()
if err != nil {
return err
}
for _, info := range displays {
vn, err := info.vendorName()
if err != nil {
return err
}
name := vn + " " + info.displayName()
if strings.TrimSpace(name) == "" {
name = "Undefined"
}
fmt.Printf("%s\t%t\t%s\n", info.Output, info.Enabled, name)
}
return nil
}
var (
connectedRe = regexp.MustCompile(`^.* connected `)
enabledRe = regexp.MustCompile(`^.* connected [^(]`)
hexRe = regexp.MustCompile(`^\s*[0-9a-fA-F]+`)
)
type displayInfo struct {
Enabled bool
Name string
Output string
EDID []byte
}
var letters = []string{
"", // 0
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", // 1-13
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", // 14-26
}
func (di *displayInfo) vendorName() (string, error) {
// big-endian 16-bit value made up of three 5-bit letters: 00001=A, 00010=B,
// ... 11010=Z. E.g. 24 4d = 0 01001 00010 01101 = "IBM".
i := int16(0)
err := binary.Read(bytes.NewReader(di.EDID[8:10]), binary.BigEndian, &i)
if err != nil {
return "", fmt.Errorf("reading big-endian vendor name: %s", err)
}
first := (i >> 10) & 0x001f
second := (i >> 5) & 0x001f
third := i & 0x001f
return fmt.Sprintf(
"%s%s%s", letters[first], letters[second], letters[third]), nil
}
// displayName reads the value of the display name EDID descriptor
func (di *displayInfo) displayName() string {
name := make([]byte, modelIdLen)
done:
// https://en.wikipedia.org/wiki/Extended_Display_Identification_Data#EDID_1.4_data_format
// Iterate over the descriptors, looking for "display name" descriptors.
for i := desc1Addr; i < (desc1Addr + numDescs*descLen); i += descLen {
if bytes.HasPrefix(di.EDID[i:], displayNameHeader) {
modelIdStart := i + len(displayNameHeader)
for j := modelIdStart; j < modelIdStart+modelIdLen; j++ {
if di.EDID[j] == lineFeed {
break done
} else {
name = append(name, di.EDID[j])
}
}
}
}
return string(bytes.Trim(name, "\x00"))
}
// readEDIDBlocks parses EDID blocks from xrandr output.
//
// xrandr prints the blocks hex-encoded, so readEDIDBlocks also decodes them.
func readEDIDBlocks() (info []*displayInfo, err error) {
cmd := exec.Command("xrandr", "--props")
stdout := bytes.Buffer{}
cmd.Stdout = &stdout
err = cmd.Run()
if err != nil {
return nil, fmt.Errorf("running xrandr: %s", err)
}
scanner := bufio.NewScanner(&stdout)
edidHexData := ""
output := ""
enabled := false
inEdid := false
for scanner.Scan() {
line := scanner.Text()
if connectedRe.MatchString(line) {
fields := strings.Fields(line)
output = fields[0]
enabled = enabledRe.MatchString(line)
}
if inEdid && hexRe.MatchString(line) {
edidHexData += strings.TrimSpace(line)
continue
}
if strings.TrimSpace(line) == "EDID:" {
inEdid = true
continue
}
if inEdid {
inEdid = false
edidData, err := hex.DecodeString(edidHexData)
if err != nil {
return nil, fmt.Errorf("hex decoding edid: %s", err)
}
info = append(info, &displayInfo{
Enabled: enabled,
Output: output,
EDID: edidData,
})
edidHexData = ""
output = ""
enabled = false
continue
}
}
return info, nil
}
const (
desc1Addr = 0x36 // 54 decimal
descLen = 0x12 // 18 decimal
numDescs = 4
modelIdLen = 13
lineFeed = 0x0a
)
var (
displayNameHeader = []byte{0, 0, 0, 0xfc, 0}
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment