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.
106 lines
2.8 KiB
Go
106 lines
2.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Profile is one saved autorandr layout: the monitors it is for, by output and EDID fingerprint.
|
|
type Profile struct {
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
Monitors map[string]string `json:"monitors"`
|
|
Detected bool `json:"detected,omitempty"`
|
|
Current bool `json:"current,omitempty"`
|
|
}
|
|
|
|
// profileName is what a profile may be called: it becomes a directory name.
|
|
var profileName = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`)
|
|
|
|
// ProfileDirs are where autorandr keeps profiles, the account's first (autorandr(1)).
|
|
func ProfileDirs(home string) []string {
|
|
cfg := os.Getenv("XDG_CONFIG_HOME")
|
|
if cfg == "" {
|
|
cfg = filepath.Join(home, ".config")
|
|
}
|
|
return []string{filepath.Join(cfg, "autorandr"), "/etc/xdg/autorandr"}
|
|
}
|
|
|
|
// ReadProfiles lists the profiles in dirs; a name in an earlier directory hides a later one, as in
|
|
// autorandr itself. Each profile's monitors come from its `setup` file: `<output> <edid>` per line.
|
|
func ReadProfiles(dirs []string) []Profile {
|
|
seen := map[string]bool{}
|
|
var out []Profile
|
|
for _, dir := range dirs {
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, e := range entries {
|
|
if !e.IsDir() || seen[e.Name()] {
|
|
continue
|
|
}
|
|
setup := filepath.Join(dir, e.Name(), "setup")
|
|
f, err := os.Open(setup)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
p := Profile{Name: e.Name(), Path: filepath.Join(dir, e.Name()), Monitors: map[string]string{}}
|
|
sc := bufio.NewScanner(f)
|
|
for sc.Scan() {
|
|
fields := strings.Fields(sc.Text())
|
|
if len(fields) == 2 {
|
|
p.Monitors[fields[0]] = shortEDID(fields[1])
|
|
}
|
|
}
|
|
f.Close()
|
|
seen[e.Name()] = true
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out
|
|
}
|
|
|
|
// shortEDID is the fingerprint DecodeEDID gives for the same monitor, so a profile's monitors and the
|
|
// connected ones are compared by one value.
|
|
func shortEDID(hexEDID string) string {
|
|
b := make([]byte, 0, len(hexEDID)/2)
|
|
for i := 0; i+1 < len(hexEDID); i += 2 {
|
|
var v byte
|
|
for _, c := range hexEDID[i : i+2] {
|
|
v <<= 4
|
|
switch {
|
|
case c >= '0' && c <= '9':
|
|
v |= byte(c - '0')
|
|
case c >= 'a' && c <= 'f':
|
|
v |= byte(c - 'a' + 10)
|
|
case c >= 'A' && c <= 'F':
|
|
v |= byte(c - 'A' + 10)
|
|
default:
|
|
return hexEDID
|
|
}
|
|
}
|
|
b = append(b, v)
|
|
}
|
|
if id := DecodeEDID(b); id != nil {
|
|
return id.Fingerprint
|
|
}
|
|
return hexEDID
|
|
}
|
|
|
|
// names reads autorandr's one-name-per-line answers (--detected, --current).
|
|
func names(text string) map[string]bool {
|
|
out := map[string]bool{}
|
|
for _, l := range strings.Split(text, "\n") {
|
|
if l = strings.TrimSpace(l); l != "" {
|
|
out[strings.Fields(l)[0]] = true
|
|
}
|
|
}
|
|
return out
|
|
}
|