v0.4.0 b6be3a8

edid.go RawHistoryFolder

1package drm
2
3import (
4	"bufio"
5	"math"
6	"os"
7	"path/filepath"
8	"strings"
9)
10
11// Monitor identifies the display attached to a connector.
12type Monitor struct {
13	Make, Model, Serial string
14	Chromaticity        Chromaticity
15	BitsPerColor        int // 0 when unspecified
16	HDR                 HDRMetadata
17}
18
19// Chromaticity is the EDID base block's 10-bit CIE xy coordinates.
20type Chromaticity struct {
21	RedX, RedY, GreenX, GreenY, BlueX, BlueY, WhiteX, WhiteY float64
22}
23
24// HDRMetadata is the CTA-861 HDR static metadata and BT.2020 colorimetry.
25type HDRMetadata struct {
26	SDR, PQ, HLG, StaticType1, BT2020RGB, BT2020YCC bool
27	MaxLuminance, MaxFrameAverage, MinLuminance     float64 // cd/m²; zero when unspecified
28}
29
30// readMonitor parses the connector's EDID from sysfs. Missing data yields "Unknown".
31func readMonitor(card, connector string) Monitor {
32	data, err := os.ReadFile(filepath.Join("/sys/class/drm", filepath.Base(card)+"-"+connector, "edid"))
33	if err != nil {
34		return Monitor{Make: "Unknown", Model: "Unknown"}
35	}
36	return parseEDID(data)
37}
38
39func parseEDID(d []byte) Monitor {
40	m := Monitor{Make: "Unknown", Model: "Unknown"}
41	if len(d) < 128 || string(d[:8]) != "\x00\xff\xff\xff\xff\xff\xff\x00" {
42		return m
43	}
44	id := uint16(d[8])<<8 | uint16(d[9])
45	pnp := string([]byte{byte(id>>10&0x1f) + '@', byte(id>>5&0x1f) + '@', byte(id&0x1f) + '@'})
46	m.Make = pnpName(pnp)
47	if d[20]&0x80 != 0 {
48		m.BitsPerColor = []int{0, 6, 8, 10, 12, 14, 16, 0}[d[20]>>4&7]
49	}
50	xy := func(hi, lo byte) float64 { return float64(uint16(hi)<<2|uint16(lo)) / 1024 }
51	m.Chromaticity = Chromaticity{
52		RedX: xy(d[27], d[25]>>6), RedY: xy(d[28], d[25]>>4&3),
53		GreenX: xy(d[29], d[25]>>2&3), GreenY: xy(d[30], d[25]&3),
54		BlueX: xy(d[31], d[26]>>6), BlueY: xy(d[32], d[26]>>4&3),
55		WhiteX: xy(d[33], d[26]>>2&3), WhiteY: xy(d[34], d[26]&3),
56	}
57	for i := 0; i < int(d[126]) && (i+2)*128 <= len(d); i++ {
58		m.HDR.parseCTA(d[(i+1)*128 : (i+2)*128])
59	}
60	// Display descriptors: 0xFC name, 0xFF serial.
61	for off := 54; off+18 <= 126; off += 18 {
62		b := d[off : off+18]
63		if b[0] != 0 || b[1] != 0 {
64			continue
65		}
66		text := strings.TrimRight(strings.SplitN(string(b[5:18]), "\n", 2)[0], " ")
67		switch b[3] {
68		case 0xfc:
69			m.Model = text
70		case 0xff:
71			m.Serial = text
72		}
73	}
74	return m
75}
76
77// parseCTA skips malformed data blocks rather than reading into the DTD area.
78func (h *HDRMetadata) parseCTA(b []byte) {
79	if len(b) < 128 || b[0] != 0x02 || b[2] < 4 || b[2] > 127 {
80		return
81	}
82	for off := 4; off < int(b[2]); {
83		size := int(b[off] & 0x1f)
84		end := off + 1 + size
85		if end > int(b[2]) {
86			return
87		}
88		if b[off]>>5 == 7 && size >= 2 {
89			v := b[off+1 : end]
90			switch v[0] {
91			case 5: // colorimetry: BT2020RGB bit 7, BT2020YCC bit 6
92				h.BT2020RGB = h.BT2020RGB || v[1]&0x80 != 0
93				h.BT2020YCC = h.BT2020YCC || v[1]&0x40 != 0
94			case 6: // HDR static metadata: EOTF, descriptors, luminances
95				if len(v) < 3 {
96					break
97				}
98				h.SDR = h.SDR || v[1]&1 != 0
99				h.PQ = h.PQ || v[1]&4 != 0
100				h.HLG = h.HLG || v[1]&8 != 0
101				h.StaticType1 = h.StaticType1 || v[2]&1 != 0
102				if len(v) > 3 && v[3] != 0 {
103					h.MaxLuminance = 50 * math.Exp2(float64(v[3])/32)
104				}
105				if len(v) > 4 && v[4] != 0 {
106					h.MaxFrameAverage = 50 * math.Exp2(float64(v[4])/32)
107				}
108				if len(v) > 5 && h.MaxLuminance != 0 {
109					h.MinLuminance = h.MaxLuminance * math.Pow(float64(v[5])/255, 2) / 100
110				}
111			}
112		}
113		off = end
114	}
115}
116
117// pnpName maps a PNP id to a vendor name via hwdata, falling back to the id.
118func pnpName(id string) string {
119	f, err := os.Open("/usr/share/hwdata/pnp.ids")
120	if err != nil {
121		return id
122	}
123	defer f.Close()
124	s := bufio.NewScanner(f)
125	for s.Scan() {
126		if name, ok := strings.CutPrefix(s.Text(), id+"\t"); ok {
127			return name
128		}
129	}
130	return id
131}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close