The X server, its start and its tools as one module. It provides x11-display with the machine's reach, gated by the host's seat capability. It writes a block at the start of ~/.xinitrc: the account's environment, an explicit import into the user manager, the mesh's X resources merged without cpp, autorandr, the xinitrc slots, ~/.xinitrc.local, and the session's exec from the last slot. Its Go tools serve the seat's displays and layout (autorandr profiles keyed by EDID), and set-mode, primary, dpi, input devices and settings, keyboard, a screenshot (xwd decoded in Go) and the server's log. internal/desktop is how every desktop tool finds the operator's session from the runtime, which has none: from the session's own processes, reading only its words, confirmed with logind.
233 lines
6.6 KiB
Go
233 lines
6.6 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Screen is the X screen as xrandr reports it.
|
|
type Screen struct {
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
MaxWidth int `json:"max_width"`
|
|
MaxHeight int `json:"max_height"`
|
|
}
|
|
|
|
// Output is one connector: a monitor when connected.
|
|
type Output struct {
|
|
Name string `json:"name"`
|
|
Connected bool `json:"connected"`
|
|
Primary bool `json:"primary,omitempty"`
|
|
Enabled bool `json:"enabled"`
|
|
Geometry *Geometry `json:"geometry,omitempty"`
|
|
Rotation string `json:"rotation,omitempty"`
|
|
Reflect string `json:"reflect,omitempty"`
|
|
WidthMM int `json:"width_mm,omitempty"`
|
|
HeightMM int `json:"height_mm,omitempty"`
|
|
// DPI is the physical density of the current mode, from the size the monitor reports.
|
|
DPI float64 `json:"dpi,omitempty"`
|
|
Monitor *Identity `json:"monitor,omitempty"`
|
|
Current *ModeRate `json:"current,omitempty"`
|
|
Preferred *ModeRate `json:"preferred,omitempty"`
|
|
Modes []Mode `json:"modes,omitempty"`
|
|
|
|
edid []byte
|
|
}
|
|
|
|
// Geometry is where an output's picture sits on the screen.
|
|
type Geometry struct {
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
X int `json:"x"`
|
|
Y int `json:"y"`
|
|
}
|
|
|
|
// Mode is one resolution and the refresh rates it is offered at.
|
|
type Mode struct {
|
|
Size string `json:"size"`
|
|
Rates []float64 `json:"rates"`
|
|
}
|
|
|
|
// ModeRate is one resolution at one rate.
|
|
type ModeRate struct {
|
|
Size string `json:"size"`
|
|
Rate float64 `json:"rate"`
|
|
}
|
|
|
|
// Identity is who the monitor is, from its EDID: what an autorandr profile is keyed by, and a name a
|
|
// person recognises.
|
|
type Identity struct {
|
|
Manufacturer string `json:"manufacturer"`
|
|
Product string `json:"product"`
|
|
Serial string `json:"serial,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
Year int `json:"year,omitempty"`
|
|
// Fingerprint is the first 12 hex characters of the EDID's sha256: one monitor, whatever port.
|
|
Fingerprint string `json:"fingerprint"`
|
|
}
|
|
|
|
var (
|
|
screenLine = regexp.MustCompile(`^Screen \d+: minimum \d+ x \d+, current (\d+) x (\d+), maximum (\d+) x (\d+)`)
|
|
geometryRe = regexp.MustCompile(`^(\d+)x(\d+)\+(-?\d+)\+(-?\d+)$`)
|
|
sizeMM = regexp.MustCompile(`\)\s+(\d+)mm x (\d+)mm`)
|
|
)
|
|
|
|
// ParseXrandr reads `xrandr --prop` (or `--query`): the screen, and every output with its modes and,
|
|
// where the properties carry one, its EDID decoded.
|
|
func ParseXrandr(text string) (Screen, []Output) {
|
|
var screen Screen
|
|
var outs []Output
|
|
var cur *Output
|
|
inEDID := false
|
|
sc := bufio.NewScanner(strings.NewReader(text))
|
|
sc.Buffer(make([]byte, 1<<20), 1<<22)
|
|
for sc.Scan() {
|
|
line := sc.Text()
|
|
switch {
|
|
case strings.HasPrefix(line, "Screen "):
|
|
if m := screenLine.FindStringSubmatch(line); m != nil {
|
|
screen = Screen{atoi(m[1]), atoi(m[2]), atoi(m[3]), atoi(m[4])}
|
|
}
|
|
inEDID = false
|
|
case line != "" && line[0] != ' ' && line[0] != '\t':
|
|
outs = append(outs, parseOutputLine(line))
|
|
cur = &outs[len(outs)-1]
|
|
inEDID = false
|
|
case cur == nil:
|
|
case strings.HasPrefix(line, "\t\t"):
|
|
if inEDID {
|
|
if b, err := hex.DecodeString(strings.TrimSpace(line)); err == nil {
|
|
cur.edid = append(cur.edid, b...)
|
|
}
|
|
}
|
|
case strings.HasPrefix(line, "\t"):
|
|
inEDID = strings.HasPrefix(strings.TrimSpace(line), "EDID:")
|
|
case strings.HasPrefix(line, " "):
|
|
inEDID = false
|
|
parseModeLine(cur, line)
|
|
}
|
|
}
|
|
for i := range outs {
|
|
o := &outs[i]
|
|
if len(o.edid) >= 128 {
|
|
o.Monitor = DecodeEDID(o.edid)
|
|
}
|
|
if o.Geometry != nil && o.WidthMM > 0 {
|
|
w := o.Geometry.Width
|
|
if o.Rotation == "left" || o.Rotation == "right" {
|
|
w = o.Geometry.Height
|
|
}
|
|
o.DPI = float64(int(float64(w)/(float64(o.WidthMM)/25.4)*10+0.5)) / 10
|
|
}
|
|
}
|
|
return screen, outs
|
|
}
|
|
|
|
func parseOutputLine(line string) Output {
|
|
f := strings.Fields(line)
|
|
o := Output{Name: f[0], Connected: len(f) > 1 && f[1] == "connected"}
|
|
for _, tok := range f[2:] {
|
|
if strings.HasPrefix(tok, "(") {
|
|
break
|
|
}
|
|
switch {
|
|
case tok == "primary":
|
|
o.Primary = true
|
|
case geometryRe.MatchString(tok):
|
|
g := geometryRe.FindStringSubmatch(tok)
|
|
o.Geometry = &Geometry{atoi(g[1]), atoi(g[2]), atoi(g[3]), atoi(g[4])}
|
|
o.Enabled = true
|
|
case tok == "normal" || tok == "left" || tok == "inverted" || tok == "right":
|
|
o.Rotation = tok
|
|
case tok == "X" || tok == "Y" || tok == "and" || tok == "axis":
|
|
o.Reflect = strings.TrimSpace(o.Reflect + " " + tok)
|
|
}
|
|
}
|
|
if o.Enabled && o.Rotation == "" {
|
|
o.Rotation = "normal"
|
|
}
|
|
if m := sizeMM.FindStringSubmatch(line); m != nil {
|
|
o.WidthMM, o.HeightMM = atoi(m[1]), atoi(m[2])
|
|
}
|
|
return o
|
|
}
|
|
|
|
// parseModeLine reads ` 2880x1800 60.00*+ 120.00 +`: a rate marked * is in use, + preferred,
|
|
// and xrandr writes the + as a token of its own after a rate not in use. A panel may mark several
|
|
// rates preferred; the first is the monitor's own preference.
|
|
func parseModeLine(o *Output, line string) {
|
|
f := strings.Fields(line)
|
|
if len(f) == 0 || !strings.Contains(f[0], "x") {
|
|
return
|
|
}
|
|
mode := Mode{Size: f[0]}
|
|
for _, tok := range f[1:] {
|
|
if tok == "+" {
|
|
if n := len(mode.Rates); n > 0 && o.Preferred == nil {
|
|
o.Preferred = &ModeRate{mode.Size, mode.Rates[n-1]}
|
|
}
|
|
continue
|
|
}
|
|
current := strings.Contains(tok, "*")
|
|
preferred := strings.Contains(tok, "+")
|
|
rate, err := strconv.ParseFloat(strings.Trim(tok, "*+"), 64)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
mode.Rates = append(mode.Rates, rate)
|
|
if current {
|
|
o.Current = &ModeRate{mode.Size, rate}
|
|
}
|
|
if preferred && o.Preferred == nil {
|
|
o.Preferred = &ModeRate{mode.Size, rate}
|
|
}
|
|
}
|
|
o.Modes = append(o.Modes, mode)
|
|
}
|
|
|
|
// DecodeEDID reads the vendor block and the descriptors of an EDID.
|
|
func DecodeEDID(e []byte) *Identity {
|
|
if len(e) < 128 {
|
|
return nil
|
|
}
|
|
sum := sha256.Sum256(e)
|
|
id := &Identity{Fingerprint: hex.EncodeToString(sum[:])[:12]}
|
|
v := uint16(e[8])<<8 | uint16(e[9])
|
|
id.Manufacturer = string([]byte{
|
|
byte('A' - 1 + (v>>10)&0x1f), byte('A' - 1 + (v>>5)&0x1f), byte('A' - 1 + v&0x1f),
|
|
})
|
|
id.Product = fmt.Sprintf("%04X", uint16(e[10])|uint16(e[11])<<8)
|
|
if serial := uint32(e[12]) | uint32(e[13])<<8 | uint32(e[14])<<16 | uint32(e[15])<<24; serial != 0 {
|
|
id.Serial = strconv.FormatUint(uint64(serial), 10)
|
|
}
|
|
if e[17] != 0 {
|
|
id.Year = 1990 + int(e[17])
|
|
}
|
|
for _, at := range []int{54, 72, 90, 108} {
|
|
d := e[at : at+18]
|
|
if d[0] != 0 || d[1] != 0 || d[2] != 0 {
|
|
continue
|
|
}
|
|
text := strings.TrimSpace(strings.SplitN(string(d[5:18]), "\n", 2)[0])
|
|
switch d[3] {
|
|
case 0xfc:
|
|
id.Name = text
|
|
case 0xff:
|
|
if text != "" {
|
|
id.Serial = text
|
|
}
|
|
}
|
|
}
|
|
return id
|
|
}
|
|
|
|
func atoi(s string) int {
|
|
n, _ := strconv.Atoi(s)
|
|
return n
|
|
}
|