xorg: the display server holds node-display-server and writes the session's start (hq ADR 0208)

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.
This commit is contained in:
jochen
2026-10-04 13:21:47 +02:00
parent b8982b4a7c
commit b2c170acda
22 changed files with 3974 additions and 0 deletions
+232
View File
@@ -0,0 +1,232 @@
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
}
+137
View File
@@ -0,0 +1,137 @@
package main
import (
"bufio"
"regexp"
"strconv"
"strings"
)
// Device is one input device the X server knows.
type Device struct {
ID int `json:"id"`
Name string `json:"name"`
Role string `json:"role"` // master or slave
Kind string `json:"kind"` // pointer or keyboard
Settings map[string]any `json:"settings,omitempty"`
}
var deviceLine = regexp.MustCompile(`^[^A-Za-z0-9]*(.+?)\s+id=(\d+)\s+\[(master|slave|floating)\s+(pointer|keyboard)?`)
// ParseDevices reads `xinput list`, the tree's drawing characters stripped.
func ParseDevices(text string) []Device {
var out []Device
sc := bufio.NewScanner(strings.NewReader(text))
for sc.Scan() {
m := deviceLine.FindStringSubmatch(sc.Text())
if m == nil {
continue
}
id, _ := strconv.Atoi(m[2])
kind := m[4]
if kind == "" {
kind = "floating"
}
out = append(out, Device{ID: id, Name: strings.TrimSpace(m[1]), Role: m[3], Kind: kind})
}
return out
}
// Knob is one setting input-set changes, and the libinput property that holds it.
type Knob struct {
Arg, Property, Kind string // Kind: bool or float
}
// Knobs are what input-set may change, by libinput's own property names (libinput(4)).
var Knobs = []Knob{
{"enabled", "Device Enabled", "bool"},
{"tap", "libinput Tapping Enabled", "bool"},
{"tap_drag", "libinput Tapping Drag Enabled", "bool"},
{"natural_scroll", "libinput Natural Scrolling Enabled", "bool"},
{"disable_while_typing", "libinput Disable While Typing Enabled", "bool"},
{"left_handed", "libinput Left Handed Enabled", "bool"},
{"middle_emulation", "libinput Middle Emulation Enabled", "bool"},
{"accel_speed", "libinput Accel Speed", "float"},
}
var propLine = regexp.MustCompile(`^\s+(.+?) \(\d+\):\s*(.*)$`)
// ParseProps reads `xinput list-props` into the settings Knobs name, as booleans and numbers. A
// device without a property (a keyboard has no tapping) simply lacks the setting.
func ParseProps(text string) map[string]any {
byProp := map[string]string{}
sc := bufio.NewScanner(strings.NewReader(text))
for sc.Scan() {
if m := propLine.FindStringSubmatch(sc.Text()); m != nil {
byProp[m[1]] = strings.TrimSpace(m[2])
}
}
out := map[string]any{}
for _, k := range Knobs {
v, ok := byProp[k.Property]
if !ok {
continue
}
first := strings.TrimSpace(strings.Split(v, ",")[0])
switch k.Kind {
case "bool":
out[k.Arg] = first == "1"
case "float":
if f, err := strconv.ParseFloat(first, 64); err == nil {
out[k.Arg] = f
}
}
}
if v, ok := byProp["libinput Accel Profile Enabled"]; ok {
profiles := []string{"adaptive", "flat", "custom"}
for i, bit := range strings.Split(v, ",") {
if strings.TrimSpace(bit) == "1" && i < len(profiles) {
out["accel_profile"] = profiles[i]
}
}
}
return out
}
// Keyboard is the X keyboard map as setxkbmap reports it.
type Keyboard struct {
Rules string `json:"rules,omitempty"`
Model string `json:"model,omitempty"`
Layout string `json:"layout,omitempty"`
Variant string `json:"variant,omitempty"`
Options []string `json:"options"`
}
// ParseKeyboard reads `setxkbmap -query`.
func ParseKeyboard(text string) Keyboard {
k := Keyboard{Options: []string{}}
sc := bufio.NewScanner(strings.NewReader(text))
for sc.Scan() {
key, value, ok := strings.Cut(sc.Text(), ":")
if !ok {
continue
}
value = strings.TrimSpace(value)
switch strings.TrimSpace(key) {
case "rules":
k.Rules = value
case "model":
k.Model = value
case "layout":
k.Layout = value
case "variant":
k.Variant = value
case "options":
for _, o := range strings.Split(value, ",") {
if o = strings.TrimSpace(o); o != "" {
k.Options = append(k.Options, o)
}
}
}
}
return k
}
// xkbName is what setxkbmap accepts as a layout, variant, model or option: letters, digits and the
// punctuation XKB names use. Anything else is refused before it reaches a command line.
var xkbName = regexp.MustCompile(`^[A-Za-z0-9_:+(),.-]+$`)
+105
View File
@@ -0,0 +1,105 @@
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
}
+167
View File
@@ -0,0 +1,167 @@
// xorg's tools (novox/hq ADR 0208, research 026/05): node-display-server's verbs `displays` and
// `layout`, and the module's own tools for one output's mode, the primary output, DPI, input devices,
// the keyboard map, a screenshot and the server's log.
//
// Every tool that touches the screen acts in the operator's running session, which it finds through
// internal/desktop: the node's runtime has none of its own. With no session, the answer says so as
// structured data. A tool that changes the running server changes it until the next login; the
// answer says what makes it last.
package main
import (
"context"
"fmt"
"os"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"xorg/internal/desktop"
)
// The session's holders this module prefers as the session's own process.
var sessionHolders = []string{"i3", "sway", "openbox", "bspwm", "awesome", "xmonad"}
func main() {
if err := stdio.Serve("", tools(desktop.Machine(sessionHolders...))); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
// call bounds one tool call below the runtime's 30 s limit.
func call(run func(ctx context.Context, a desktop.Args) (any, error)) func(map[string]any) (any, error) {
return func(args map[string]any) (any, error) {
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
return run(ctx, desktop.Args(args))
}
}
func tools(d desktop.Desk) []stdio.Tool {
x := xorg{d: d}
return []stdio.Tool{
{
Name: "node-display-server.displays",
Description: "The X screen and every output: connected or not, primary, where it sits, its rotation, " +
"physical size and DPI, the monitor's identity from its EDID (manufacturer, product, serial, name, " +
"fingerprint), the current and preferred mode, and every mode with its rates. Disconnected outputs " +
"are listed by name only unless all is true. Also the autorandr profile in force, when one matches.",
Input: desktop.Schema(map[string]any{
"all": desktop.Flag("list disconnected outputs in full (default false)"),
"modes": desktop.Flag("list every mode of each output (default true)"),
}),
Run: call(x.displays),
},
{
Name: "node-display-server.layout",
Description: "Monitor layout profiles (autorandr), keyed by the connected monitors' identities, so one " +
"profile needs no machine's name. list: every profile with the monitors it is for, which match " +
"the monitors connected now and which is in force. save: the current arrangement under a name " +
"(replace true to overwrite). apply: load one. The profiles are the operator's data, never the mesh's.",
Input: desktop.Schema(map[string]any{
"action": desktop.Enum("list, save or apply", "list", "save", "apply"),
"name": desktop.Str("the profile to save or apply"),
"replace": desktop.Flag("save over an existing profile of that name (default false)"),
}, "action"),
Run: call(x.layout),
},
{
Name: "xorg_set_mode",
Description: "Change one output now, through xrandr: its mode (WxH, or auto for the preferred one), rate, " +
"rotation, scale, position (x/y, or beside another output), on or off, primary. dry_run answers " +
"the command without running it. Lasts until the next login or hotplug: save a layout profile to keep it.",
Input: desktop.Schema(map[string]any{
"output": desktop.Str("the output, as displays names it (e.g. eDP-1)"),
"mode": desktop.Str("WxH, or auto for the monitor's preferred mode"),
"rate": desktop.Num("refresh rate in Hz"),
"rotate": desktop.Enum("rotation", "normal", "left", "right", "inverted"),
"scale": desktop.Num("scale factor, 0.25 to 4 (1 = none)"),
"x": desktop.Int("left edge on the screen, in pixels"),
"y": desktop.Int("top edge on the screen, in pixels"),
"relation": desktop.Enum("place it beside another output instead of at x/y", "right-of", "left-of", "above", "below", "same-as"),
"of": desktop.Str("the output the relation is to"),
"off": desktop.Flag("switch the output off"),
"primary": desktop.Flag("make it the primary output"),
"dry_run": desktop.Flag("answer the command, run nothing"),
}, "output"),
Run: call(x.setMode),
},
{
Name: "xorg_primary",
Description: "Which output is primary (the one the bar's tray and new windows prefer); with output, make it primary now.",
Input: desktop.Schema(map[string]any{"output": desktop.Str("the output to make primary (optional)")}),
Run: call(x.primary),
},
{
Name: "xorg_dpi",
Description: "The DPI X programs are told (Xft.dpi) beside each monitor's physical DPI. With value, set " +
"Xft.dpi and the screen's DPI now: programs started after it use it. The module's resources file " +
"sets it again at the next login (96 until it is a setting).",
Input: desktop.Schema(map[string]any{"value": desktop.Int("dots per inch, 48 to 480 (optional)")}),
Run: call(x.dpi),
},
{
Name: "xorg_input_devices",
Description: "The input devices the X server knows: id, name, pointer or keyboard, master or slave, and " +
"for each pointer its libinput settings (tap, natural scroll, acceleration, disable while typing, " +
"left handed, enabled).",
Input: desktop.Schema(map[string]any{"name": desktop.Str("only devices whose name contains this (optional)")}),
Run: call(x.inputDevices),
},
{
Name: "xorg_input_set",
Description: "Change libinput settings of a device now: tap, tap_drag, natural_scroll, disable_while_typing, " +
"left_handed, middle_emulation, enabled (true/false), accel_speed (-1 to 1). The device is an id or a " +
"name; a name applies to every device of that name that has the setting. Lasts until the device " +
"reconnects or the next login.",
Input: desktop.Schema(map[string]any{
"device": desktop.Str("the device's id or exact name"),
"tap": desktop.Flag("tap to click"),
"tap_drag": desktop.Flag("tap and drag"),
"natural_scroll": desktop.Flag("natural (reversed) scrolling"),
"disable_while_typing": desktop.Flag("ignore the touchpad while typing"),
"left_handed": desktop.Flag("swap the buttons"),
"middle_emulation": desktop.Flag("both buttons together are the middle one"),
"enabled": desktop.Flag("the device takes input at all"),
"accel_speed": desktop.Num("pointer acceleration, -1 to 1"),
}, "device"),
Run: call(x.inputSet),
},
{
Name: "xorg_keyboard",
Description: "The X keyboard map: rules, model, layout, variant and options. With layout, variant, model " +
"or options, set them now (options replace the current ones; an empty list clears them). Lasts " +
"until the next login.",
Input: desktop.Schema(map[string]any{
"layout": desktop.Str("e.g. us, be, us,be"),
"variant": desktop.Str("e.g. intl"),
"model": desktop.Str("e.g. pc105"),
"options": desktop.List("e.g. [\"caps:escape\", \"compose:ralt\"]"),
}),
Run: call(x.keyboard),
},
{
Name: "xorg_screenshot",
Description: "Take a screenshot to a PNG file: the whole screen, one output, or one window by its X id. " +
"Written under the account's home (default ~/Pictures/Screenshots/screenshot-<time>.png). Answers " +
"the path, size and bytes, never the image.",
Input: desktop.Schema(map[string]any{
"output": desktop.Str("only this output (optional)"),
"window": desktop.Str("only this window, by its X id, e.g. 0x3a00007 (optional)"),
"path": desktop.Str("where to write it, inside the home (optional)"),
}),
Run: call(x.screenshot),
},
{
Name: "xorg_x_log",
Description: "The X server's log for the running session (or the newest one): its version, when it " +
"started, and its errors — with warnings when asked — newest last.",
Input: desktop.Schema(map[string]any{
"warnings": desktop.Flag("include warnings (default false)"),
"limit": desktop.Int("at most this many lines (default 100, at most 1000)"),
}),
Run: call(x.xlog),
},
}
}
+243
View File
@@ -0,0 +1,243 @@
package main
// xorg's shape (novox/hq ADR 0208): it holds node-display-server and provides the display with the
// machine's reach; it writes the session's start as a block at the start of ~/.xinitrc in the order
// §5 gives, and the resources file every module contributes to. The block is also run, with the
// programs it calls replaced by recorders, to prove what it does rather than what it says.
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"testing"
)
type manifest struct {
Module string `json:"module"`
Capabilities []string `json:"capabilities"`
Provides []map[string]any `json:"provides"`
Requires []string `json:"requires"`
Claims []struct {
Name string `json:"name"`
Scope string `json:"scope"`
Serves []string `json:"serves"`
} `json:"claims"`
Seats any `json:"seats"`
Tools []string `json:"tools"`
Environment map[string]any `json:"environment"`
Shell []map[string]any `json:"shell"`
Resources []map[string]any `json:"resources"`
Build struct {
Artifacts []map[string]any `json:"artifacts"`
} `json:"build"`
}
func readManifest(t *testing.T) manifest {
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
if err != nil {
t.Fatal(err)
}
var m manifest
dec := json.NewDecoder(strings.NewReader(string(raw)))
if err := dec.Decode(&m); err != nil {
t.Fatal(err)
}
return m
}
func (m manifest) resource(t *testing.T, id string) map[string]any {
for _, r := range m.Resources {
if r["id"] == id {
return r
}
}
t.Fatalf("no resource %s", id)
return nil
}
func TestItHoldsTheDisplayServerSeatAndServesItsVerbs(t *testing.T) {
m := readManifest(t)
if m.Seats != nil || len(m.Claims) != 1 || m.Claims[0].Name != "node-display-server" || m.Claims[0].Scope != "node" {
t.Fatalf("claims: %+v", m.Claims)
}
if strings.Join(m.Claims[0].Serves, ",") != "displays,layout" {
t.Fatalf("serves: %v", m.Claims[0].Serves)
}
served := map[string]bool{}
for _, tool := range tools((&fakeDesk{}).desk(false)) {
served[tool.Name] = true
}
for _, v := range m.Claims[0].Serves {
if !served["node-display-server."+v] {
t.Errorf("the bundle does not serve the seat's %s", v)
}
}
own := 0
for name := range served {
if !strings.HasPrefix(name, "node-display-server.") {
own++
}
}
for _, name := range m.Tools {
if !served[name] || !strings.HasPrefix(name, "xorg_") {
t.Errorf("the manifest lists %s, which the bundle does not serve as its own", name)
}
}
if own != len(m.Tools) {
t.Errorf("the bundle serves %d tools of its own, the manifest lists %d", own, len(m.Tools))
}
}
func TestTheDisplayIsProvidedOnThisMachineOnlyAndGatedByASeatOfHardware(t *testing.T) {
m := readManifest(t)
if len(m.Provides) != 1 || m.Provides[0]["name"] != "x11-display" || m.Provides[0]["reach"] != "machine" {
t.Fatalf("provides: %v", m.Provides)
}
caps := strings.Join(m.Capabilities, ",")
if caps != "package-manager,seat" {
t.Fatalf("capabilities %s: seat is the hardware a display server needs; graphical-session is whether one runs now", caps)
}
}
func TestItInstallsTheServerItsStartAndTheToolsItsVerbsRun(t *testing.T) {
m := readManifest(t)
var got []string
for _, r := range m.Resources {
if r["type"] == "package" {
got = append(got, r["package"].(string))
}
}
sort.Strings(got)
want := "autorandr,xorg-server,xorg-setxkbmap,xorg-xinit,xorg-xinput,xorg-xrandr,xorg-xrdb,xorg-xset,xorg-xwd"
if strings.Join(got, ",") != want {
t.Fatalf("packages: %v", got)
}
}
func TestTheResourcesFileHoldsTheDefaultsAndTheThreeSlots(t *testing.T) {
r := readManifest(t).resource(t, "xresources")
c := r["content"].(string)
if r["path"] != "${machine:account-home}/.config/xorg/xresources" || r["into"] != nil {
t.Fatalf("an owned file: %v", r)
}
first, normal, last := strings.Index(c, "${shell:xresources:first}"), strings.Index(c, "${shell:xresources:normal}"), strings.Index(c, "${shell:xresources:last}")
if !(first > strings.Index(c, "Xft.dpi: 96") && first < normal && normal < last) {
t.Fatal("the defaults, then first, normal and last")
}
for _, line := range strings.Split(c, "\n") {
if strings.HasPrefix(line, "#") {
t.Errorf("merged without cpp, a # line is skipped: %q", line)
}
}
}
func TestTheSessionStartIsABlockAtTheStartInTheOrderTheDecisionGives(t *testing.T) {
r := readManifest(t).resource(t, "xinitrc")
if r["path"] != "${machine:account-home}/.xinitrc" || r["into"] != "block" || r["at"] != "start" {
t.Fatalf("%v", r)
}
c := r["content"].(string)
order := []string{
". \"${machine:account-home}/.config/mesh/environment.sh\"",
"dbus-update-activation-environment --systemd $mesh_words",
"xrdb -nocpp -merge \"${machine:account-home}/.config/xorg/xresources\"",
"autorandr --change",
"${shell:xinitrc:first}",
"${shell:xinitrc:normal}",
". \"$HOME/.xinitrc.local\"",
"${shell:xinitrc:last}",
}
at := -1
for _, step := range order {
i := strings.Index(c, step)
if i <= at {
t.Fatalf("%q is not after the step before it", step)
}
at = i
if strings.Count(c, step) != 1 {
t.Fatalf("%q appears more than once", step)
}
}
var code []string
for _, line := range strings.Split(c, "\n") {
if !strings.HasPrefix(strings.TrimSpace(line), "#") {
code = append(code, line)
}
}
lines := strings.Join(code, "\n")
if strings.Contains(lines, "--all") || strings.Contains(lines, "exec ") || strings.Contains(lines, "dbus-launch") {
t.Fatal("never --all, no exec of the block's own (the session's is contributed), no second bus")
}
if strings.Contains(c, "hal/env") || strings.Contains(c, "gnome-keyring") {
t.Fatal("the predecessor's secrets file and the keyring are not the display server's")
}
}
// render fills the block's placeholders as the controller would, with one contribution per slot.
func render(c, home string, slots map[string]string) string {
c = strings.ReplaceAll(c, "${machine:account-home}", home)
for _, slot := range []string{"first", "normal", "last"} {
code := ""
if s, ok := slots[slot]; ok {
code = "# a-module\n" + s + "\n"
}
c = strings.ReplaceAll(c, "${shell:xinitrc:"+slot+"}", code)
}
return c
}
func TestTheBlockRunsTheEnvironmentTheImportTheResourcesTheSlotsAndEndsInTheSession(t *testing.T) {
sh, err := exec.LookPath("sh")
if err != nil {
t.Skip("no sh")
}
home := t.TempDir()
bin := filepath.Join(home, "bin")
log := filepath.Join(home, "log")
os.MkdirAll(filepath.Join(home, ".config", "mesh"), 0o755)
os.MkdirAll(bin, 0o755)
os.WriteFile(filepath.Join(home, ".config", "mesh", "environment.sh"),
[]byte("export XDG_CURRENT_DESKTOP=i3\nexport GTK_THEME=Adwaita:dark\n"), 0o644)
os.WriteFile(filepath.Join(home, ".xinitrc.local"), []byte("echo local >> \""+log+"\"\n"), 0o644)
for _, prog := range []string{"dbus-update-activation-environment", "xrdb", "autorandr", "the-session"} {
os.WriteFile(filepath.Join(bin, prog), []byte("#!/bin/sh\necho \""+prog+" $*\" >> \""+log+"\"\n"), 0o755)
}
block := render(readManifest(t).resource(t, "xinitrc")["content"].(string), home, map[string]string{
"normal": "echo normal >> \"" + log + "\"",
"last": "exec the-session",
})
file := "#!/bin/sh\n# BEGIN mesh xorg.xinitrc\n" + block + "# END mesh xorg.xinitrc\n" +
"export SECRET_TOKEN=x\necho operator-below >> \"" + log + "\"\nexec i3\n"
os.WriteFile(filepath.Join(home, ".xinitrc"), []byte(file), 0o755)
os.WriteFile(filepath.Join(home, ".Xresources"), []byte("XTerm*saveLines: 1\n"), 0o644)
cmd := exec.Command(sh, filepath.Join(home, ".xinitrc"))
cmd.Env = []string{"HOME=" + home, "PATH=" + bin + ":/usr/bin:/bin", "DISPLAY=:7", "XAUTHORITY=" + home + "/.Xauthority",
"XDG_SESSION_TYPE=x11", "DBUS_SESSION_BUS_ADDRESS=unix:path=/nowhere", "SECRET_KEY=y"}
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("%v: %s", err, out)
}
raw, _ := os.ReadFile(log)
lines := strings.Split(strings.TrimSpace(string(raw)), "\n")
want := []string{
"dbus-update-activation-environment --systemd DISPLAY XAUTHORITY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP GTK_THEME",
"xrdb -nocpp -merge " + home + "/.config/xorg/xresources",
"xrdb -merge " + home + "/.Xresources",
"autorandr --change",
"normal",
"local",
"the-session",
}
if strings.Join(lines, "\n") != strings.Join(want, "\n") {
t.Fatalf("ran:\n%s\nwant:\n%s", strings.Join(lines, "\n"), strings.Join(want, "\n"))
}
}
func TestTheBundleIsGoBuiltForArch(t *testing.T) {
a := readManifest(t).Build.Artifacts
if len(a) != 1 || a[0]["language"] != "go" || a[0]["system"] != "arch" || a[0]["from"] != "cmd/xorg" || a[0]["binary"] != "xorg" {
t.Fatalf("%v", a)
}
}
+3
View File
@@ -0,0 +1,3 @@
rules: evdev
model: pc105
layout: us
+25
View File
@@ -0,0 +1,25 @@
⎡ Virtual core pointer id=2 [master pointer (3)]
⎜ ↳ Virtual core XTEST pointer id=4 [slave pointer (2)]
⎜ ↳ Logitech MX Master 3 id=7 [slave pointer (2)]
⎜ ↳ Logitech MX Master 3 id=8 [slave pointer (2)]
⎜ ↳ Logitech MX Keys id=9 [slave pointer (2)]
⎜ ↳ Razer Razer Basilisk V3 Pro id=10 [slave pointer (2)]
⎜ ↳ Razer Razer Basilisk V3 Pro id=11 [slave pointer (2)]
⎜ ↳ ITE Tech. Inc. ITE Device(8910) id=13 [slave pointer (2)]
⎜ ↳ ASUP1208:00 093A:3011 Mouse id=15 [slave pointer (2)]
⎜ ↳ ASUP1208:00 093A:3011 Touchpad id=16 [slave pointer (2)]
⎜ ↳ ydotoold virtual device id=20 [slave pointer (2)]
⎣ Virtual core keyboard id=3 [master keyboard (2)]
↳ Virtual core XTEST keyboard id=5 [slave keyboard (3)]
↳ Video Bus id=6 [slave keyboard (3)]
↳ Razer Razer Basilisk V3 Pro id=12 [slave keyboard (3)]
↳ Video Bus id=14 [slave keyboard (3)]
↳ Power Button id=17 [slave keyboard (3)]
↳ Sleep Button id=18 [slave keyboard (3)]
↳ Asus WMI hotkeys id=19 [slave keyboard (3)]
↳ Logitech MX Master 3 id=21 [slave keyboard (3)]
↳ Logitech MX Master 3 id=22 [slave keyboard (3)]
↳ Logitech MX Keys id=23 [slave keyboard (3)]
↳ Razer Razer Basilisk V3 Pro id=24 [slave keyboard (3)]
↳ ITE Tech. Inc. ITE Device(8910) id=25 [slave keyboard (3)]
↳ ydotoold virtual device id=26 [slave keyboard (3)]
+48
View File
@@ -0,0 +1,48 @@
Device 'ASUP1208:00 093A:3011 Touchpad':
Device Enabled (187): 1
Coordinate Transformation Matrix (189): 1.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 1.000000
libinput Tapping Enabled (347): 1
libinput Tapping Enabled Default (348): 0
libinput Tapping Drag Enabled (349): 1
libinput Tapping Drag Enabled Default (350): 1
libinput Tapping Drag Lock Enabled (351): 0
libinput Tapping Drag Lock Enabled Default (352): 0
libinput Tapping Button Mapping Enabled (353): 1, 0
libinput Tapping Button Mapping Default (354): 1, 0
libinput Natural Scrolling Enabled (318): 1
libinput Natural Scrolling Enabled Default (319): 0
libinput Disable While Typing Enabled (355): 1
libinput Disable While Typing Enabled Default (356): 1
libinput Scroll Methods Available (320): 1, 1, 0
libinput Scroll Method Enabled (321): 1, 0, 0
libinput Scroll Method Enabled Default (322): 1, 0, 0
libinput Click Methods Available (357): 1, 1
libinput Click Method Enabled (358): 1, 0
libinput Click Method Enabled Default (359): 1, 0
libinput Clickfinger Button Mapping Enabled (360): 1, 0
libinput Clickfinger Button Mapping Default (361): 1, 0
libinput Middle Emulation Enabled (327): 0
libinput Middle Emulation Enabled Default (328): 0
libinput Accel Speed (329): 0.150000
libinput Accel Speed Default (330): 0.000000
libinput Accel Profiles Available (331): 1, 1, 1
libinput Accel Profile Enabled (332): 1, 0, 0
libinput Accel Profile Enabled Default (333): 1, 0, 0
libinput Accel Custom Fallback Points (334): <no items>
libinput Accel Custom Fallback Step (335): 0.000000
libinput Accel Custom Motion Points (336): <no items>
libinput Accel Custom Motion Step (337): 0.000000
libinput Accel Custom Scroll Points (338): <no items>
libinput Accel Custom Scroll Step (339): 0.000000
libinput Left Handed Enabled (340): 0
libinput Left Handed Enabled Default (341): 0
libinput Send Events Modes Available (303): 1, 1
libinput Send Events Mode Enabled (304): 0, 0
libinput Send Events Mode Enabled Default (305): 0, 0
Device Node (306): "/dev/input/event20"
Device Product ID (307): 2362, 12305
libinput Drag Lock Buttons (342): <no items>
libinput Horizontal Scroll Enabled (343): 1
libinput Scrolling Pixel Distance (344): 15
libinput Scrolling Pixel Distance Default (345): 15
libinput High Resolution Wheel Scroll Enabled (346): 1
+437
View File
@@ -0,0 +1,437 @@
Screen 0: minimum 320 x 200, current 2880 x 1800, maximum 16384 x 16384
eDP-1 connected primary 2880x1800+0+0 (normal left inverted right x axis y axis) 302mm x 189mm
EDID:
00ffffffffffff004c839c4100000000
00210104b51e137803cfd1ae513eb623
0b505400000001010101010101010101
010101010101cbfe4064b00838772008
88002ebd1000001b000000fd003078da
da420100000000000000000000fe0053
44430a202020202020202020000000fc
0041544e413430435530352d30200127
702079020020000c4c83009c41000000
00002100260009040000000000500000
220014e7f309853f0b63001f00070007
071700070007002b000c270030770000
2700303b000081001f731a0000030330
7800a07402600278000000008de30580
00e60605017460022e00061810405ed0
6000000000000000000000000000fc90
panel_type: OLED
supported: unknown, OLED
HDCP Content Type: HDCP Type0
supported: HDCP Type0, HDCP Type1
Content Protection: Undesired
supported: Undesired, Desired, Enabled
vrr_capable: 1
range: (0, 1)
Colorspace: Default
supported: Default, BT709_YCC, opRGB, BT2020_RGB, BT2020_YCC
max bpc: 16
range: (8, 16)
underscan vborder: 0
range: (0, 128)
underscan hborder: 0
range: (0, 128)
underscan: off
supported: off, on, auto
scaling mode: None
supported: None, Full, Center, Full aspect
link-status: Good
supported: Good, Bad
CTM: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
CONNECTOR_ID: 383
supported: 383
non-desktop: 0
range: (0, 1)
2880x1800 60.00*+ 120.00 +
2880x1620 59.96 59.97
2560x1600 59.99 59.97
2560x1440 59.99 59.99 59.96 59.95
2048x1536 85.00 75.00 60.00
1920x1440 85.00 75.00 60.00
1856x1392 75.00 60.01
1792x1344 75.00 60.01
2048x1152 59.99 59.98 59.90 59.91
1920x1200 120.00 59.88 59.95
1920x1080 120.00 60.01 59.97 59.96 59.93
1600x1200 120.00 85.00 75.00 70.00 65.00 60.00
1680x1050 120.00 59.95 59.88
1400x1050 74.76 59.98
1600x900 59.99 59.94 59.95 59.82
1280x1024 120.00 85.02 75.02 60.02
1440x900 120.00
1400x900 59.96 59.88
1280x960 85.00 60.00
1440x810 60.00 59.97
1368x768 59.88 59.85
1280x800 120.00 59.99 59.97 59.81 59.91
1152x864 75.00
1280x720 120.00 60.00 59.99 59.86 59.74
1024x768 120.00 85.00 75.05 60.04 85.00 75.03 70.07 60.00
1024x768i 86.96
960x720 85.00 75.00 60.00
928x696 75.00 60.05
896x672 75.05 60.01
1024x576 59.95 59.96 59.90 59.82
960x600 59.93 60.00
832x624 74.55
960x540 59.96 59.99 59.63 59.82
800x600 120.00 85.00 75.00 70.00 65.00 60.00 85.14 72.19 75.00 60.32 56.25
840x525 60.01 59.88
864x486 59.92 59.57
700x525 74.76 59.98
800x450 59.95 59.82
640x512 85.02 75.02 60.02
700x450 59.96 59.88
640x480 120.00 85.09 60.00 85.01 72.81 75.00 59.94
720x405 59.51 58.99
720x400 85.04
684x384 59.88 59.85
640x400 59.88 59.98 85.08
576x432 75.00
640x360 59.86 59.83 59.84 59.32
640x350 85.08
512x384 85.00 75.03 70.07 60.00
512x384i 87.06
512x288 60.00 59.92
416x312 74.66
480x270 59.63 59.82
400x300 85.27 72.19 75.12 60.32 56.34
432x243 59.92 59.57
320x240 85.18 72.81 75.00 60.05
360x202 59.51 59.13
360x200 85.04
320x200 85.27
320x180 59.84 59.32
320x175 85.27
DP-1 disconnected (normal left inverted right x axis y axis)
subconnector: Unknown
supported: Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native
HDCP Content Type: HDCP Type0
supported: HDCP Type0, HDCP Type1
Content Protection: Undesired
supported: Undesired, Desired, Enabled
vrr_capable: 0
range: (0, 1)
Colorspace: Default
supported: Default, BT709_YCC, opRGB, BT2020_RGB, BT2020_YCC
max bpc: 16
range: (8, 16)
underscan vborder: 0
range: (0, 128)
underscan hborder: 0
range: (0, 128)
underscan: off
supported: off, on, auto
Broadcast RGB: Automatic
supported: Automatic, Full, Limited 16:235
scaling mode: None
supported: None, Full, Center, Full aspect
link-status: Good
supported: Good, Bad
CTM: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
CONNECTOR_ID: 391
supported: 391
non-desktop: 0
range: (0, 1)
DP-2 disconnected (normal left inverted right x axis y axis)
subconnector: Unknown
supported: Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native
HDCP Content Type: HDCP Type0
supported: HDCP Type0, HDCP Type1
Content Protection: Undesired
supported: Undesired, Desired, Enabled
vrr_capable: 0
range: (0, 1)
Colorspace: Default
supported: Default, BT709_YCC, opRGB, BT2020_RGB, BT2020_YCC
max bpc: 16
range: (8, 16)
underscan vborder: 0
range: (0, 128)
underscan hborder: 0
range: (0, 128)
underscan: off
supported: off, on, auto
Broadcast RGB: Automatic
supported: Automatic, Full, Limited 16:235
scaling mode: None
supported: None, Full, Center, Full aspect
link-status: Good
supported: Good, Bad
CTM: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
CONNECTOR_ID: 398
supported: 398
non-desktop: 0
range: (0, 1)
DP-3 disconnected (normal left inverted right x axis y axis)
subconnector: Unknown
supported: Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native
HDCP Content Type: HDCP Type0
supported: HDCP Type0, HDCP Type1
Content Protection: Undesired
supported: Undesired, Desired, Enabled
vrr_capable: 0
range: (0, 1)
Colorspace: Default
supported: Default, BT709_YCC, opRGB, BT2020_RGB, BT2020_YCC
max bpc: 16
range: (8, 16)
underscan vborder: 0
range: (0, 128)
underscan hborder: 0
range: (0, 128)
underscan: off
supported: off, on, auto
Broadcast RGB: Automatic
supported: Automatic, Full, Limited 16:235
scaling mode: None
supported: None, Full, Center, Full aspect
link-status: Good
supported: Good, Bad
CTM: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
CONNECTOR_ID: 405
supported: 405
non-desktop: 0
range: (0, 1)
DP-4 disconnected (normal left inverted right x axis y axis)
subconnector: Unknown
supported: Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native
HDCP Content Type: HDCP Type0
supported: HDCP Type0, HDCP Type1
Content Protection: Undesired
supported: Undesired, Desired, Enabled
vrr_capable: 0
range: (0, 1)
Colorspace: Default
supported: Default, BT709_YCC, opRGB, BT2020_RGB, BT2020_YCC
max bpc: 16
range: (8, 16)
underscan vborder: 0
range: (0, 128)
underscan hborder: 0
range: (0, 128)
underscan: off
supported: off, on, auto
Broadcast RGB: Automatic
supported: Automatic, Full, Limited 16:235
scaling mode: None
supported: None, Full, Center, Full aspect
link-status: Good
supported: Good, Bad
CTM: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
CONNECTOR_ID: 411
supported: 411
non-desktop: 0
range: (0, 1)
DP-5 disconnected (normal left inverted right x axis y axis)
subconnector: Unknown
supported: Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native
HDCP Content Type: HDCP Type0
supported: HDCP Type0, HDCP Type1
Content Protection: Undesired
supported: Undesired, Desired, Enabled
vrr_capable: 0
range: (0, 1)
Colorspace: Default
supported: Default, BT709_YCC, opRGB, BT2020_RGB, BT2020_YCC
max bpc: 16
range: (8, 16)
underscan vborder: 0
range: (0, 128)
underscan hborder: 0
range: (0, 128)
underscan: off
supported: off, on, auto
Broadcast RGB: Automatic
supported: Automatic, Full, Limited 16:235
scaling mode: None
supported: None, Full, Center, Full aspect
link-status: Good
supported: Good, Bad
CTM: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
CONNECTOR_ID: 417
supported: 417
non-desktop: 0
range: (0, 1)
DP-6 disconnected (normal left inverted right x axis y axis)
subconnector: Unknown
supported: Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native
HDCP Content Type: HDCP Type0
supported: HDCP Type0, HDCP Type1
Content Protection: Undesired
supported: Undesired, Desired, Enabled
vrr_capable: 0
range: (0, 1)
Colorspace: Default
supported: Default, BT709_YCC, opRGB, BT2020_RGB, BT2020_YCC
max bpc: 16
range: (8, 16)
underscan vborder: 0
range: (0, 128)
underscan hborder: 0
range: (0, 128)
underscan: off
supported: off, on, auto
Broadcast RGB: Automatic
supported: Automatic, Full, Limited 16:235
scaling mode: None
supported: None, Full, Center, Full aspect
link-status: Good
supported: Good, Bad
CTM: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
CONNECTOR_ID: 423
supported: 423
non-desktop: 0
range: (0, 1)
DP-7 disconnected (normal left inverted right x axis y axis)
subconnector: Unknown
supported: Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native
HDCP Content Type: HDCP Type0
supported: HDCP Type0, HDCP Type1
Content Protection: Undesired
supported: Undesired, Desired, Enabled
vrr_capable: 0
range: (0, 1)
Colorspace: Default
supported: Default, BT709_YCC, opRGB, BT2020_RGB, BT2020_YCC
max bpc: 16
range: (8, 16)
underscan vborder: 0
range: (0, 128)
underscan hborder: 0
range: (0, 128)
underscan: off
supported: off, on, auto
Broadcast RGB: Automatic
supported: Automatic, Full, Limited 16:235
scaling mode: None
supported: None, Full, Center, Full aspect
link-status: Good
supported: Good, Bad
CTM: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
CONNECTOR_ID: 429
supported: 429
non-desktop: 0
range: (0, 1)
DP-8 disconnected (normal left inverted right x axis y axis)
subconnector: Unknown
supported: Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native
HDCP Content Type: HDCP Type0
supported: HDCP Type0, HDCP Type1
Content Protection: Undesired
supported: Undesired, Desired, Enabled
vrr_capable: 0
range: (0, 1)
Colorspace: Default
supported: Default, BT709_YCC, opRGB, BT2020_RGB, BT2020_YCC
max bpc: 16
range: (8, 16)
underscan vborder: 0
range: (0, 128)
underscan hborder: 0
range: (0, 128)
underscan: off
supported: off, on, auto
Broadcast RGB: Automatic
supported: Automatic, Full, Limited 16:235
scaling mode: None
supported: None, Full, Center, Full aspect
link-status: Good
supported: Good, Bad
CTM: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
CONNECTOR_ID: 435
supported: 435
non-desktop: 0
range: (0, 1)
DP-1-0 disconnected (normal left inverted right x axis y axis)
PRIME Synchronization: 1
supported: 0, 1
CTM: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
CscMatrix: 65536 0 0 0 0 65536 0 0 0 0 65536 0
BorderDimensions: 4
supported: 4
Border: 0 0 0 0
range: (0, 65535)
SignalFormat: DisplayPort
supported: DisplayPort
ConnectorType: DisplayPort
ConnectorNumber: 0
_ConnectorLocation: 0
non-desktop: 0
supported: 0, 1
DP-1-1 disconnected (normal left inverted right x axis y axis)
PRIME Synchronization: 1
supported: 0, 1
CTM: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
CscMatrix: 65536 0 0 0 0 65536 0 0 0 0 65536 0
BorderDimensions: 4
supported: 4
Border: 0 0 0 0
range: (0, 65535)
SignalFormat: TMDS
supported: TMDS
ConnectorType: DisplayPort
ConnectorNumber: 0
_ConnectorLocation: 0
non-desktop: 0
supported: 0, 1
HDMI-1-0 disconnected (normal left inverted right x axis y axis)
PRIME Synchronization: 1
supported: 0, 1
CTM: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
CscMatrix: 65536 0 0 0 0 65536 0 0 0 0 65536 0
BorderDimensions: 4
supported: 4
Border: 0 0 0 0
range: (0, 65535)
SignalFormat: TMDS
supported: TMDS
ConnectorType: HDMI
ConnectorNumber: 2
_ConnectorLocation: 2
non-desktop: 0
supported: 0, 1
DP-1-2 disconnected (normal left inverted right x axis y axis)
PRIME Synchronization: 1
supported: 0, 1
CTM: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
CscMatrix: 65536 0 0 0 0 65536 0 0 0 0 65536 0
BorderDimensions: 4
supported: 4
Border: 0 0 0 0
range: (0, 65535)
SignalFormat: DisplayPort
supported: DisplayPort
ConnectorType: Panel
ConnectorNumber: 3
_ConnectorLocation: 3
non-desktop: 0
supported: 0, 1
+602
View File
@@ -0,0 +1,602 @@
package main
import (
"bytes"
"context"
"fmt"
"image"
"image/draw"
"image/png"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"
"xorg/internal/desktop"
)
type xorg struct{ d desktop.Desk }
// restored is what every answer that changed the running server says about lasting.
const untilLogin = "until the next login: the session's start applies the declared defaults and the matching layout profile again"
var outputName = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`)
func (x xorg) query(ctx context.Context) (Screen, []Output, *desktop.Session, error) {
res, s, err := x.d.InSession(ctx, "xrandr", "--prop")
if err != nil {
return Screen{}, nil, nil, err
}
if !res.OK() {
return Screen{}, nil, s, res.Err()
}
screen, outs := ParseXrandr(res.Stdout)
return screen, outs, s, nil
}
func (x xorg) displays(ctx context.Context, a desktop.Args) (any, error) {
all, _, err := a.Bool("all")
if err != nil {
return nil, err
}
modes, given, err := a.Bool("modes")
if err != nil {
return nil, err
}
if !given {
modes = true
}
screen, outs, s, err := x.query(ctx)
if err != nil {
return nil, err
}
var shown []Output
var off []string
for _, o := range outs {
if !o.Connected && !all {
off = append(off, o.Name)
continue
}
if !modes {
o.Modes = nil
}
shown = append(shown, o)
}
answer := map[string]any{"session": s.Display, "screen": screen, "outputs": shown, "disconnected": off}
if r := x.d.Run(ctx, s.Env(x.d.Base), nil, "autorandr", "--current"); r.OK() {
answer["profile"] = firstName(r.Stdout)
}
return answer, nil
}
func firstName(text string) string {
for n := range names(text) {
return n
}
return ""
}
func (x xorg) layout(ctx context.Context, a desktop.Args) (any, error) {
action, err := a.OneOf("action", "", "list", "save", "apply")
if err != nil {
return nil, err
}
dirs := ProfileDirs(desktop.Home())
if action == "list" {
profiles := ReadProfiles(dirs)
answer := map[string]any{"profiles": profiles, "where": dirs[0]}
s, err := x.d.Find()
if err != nil {
answer["session"] = nil
answer["note"] = "no graphical session: which profile matches the monitors connected is not known"
return answer, nil
}
env := s.Env(x.d.Base)
det := x.d.Run(ctx, env, nil, "autorandr", "--detected")
if det.Code == 127 {
return nil, fmt.Errorf("autorandr is not installed here; the xorg module's package resource installs it")
}
detected, current := names(det.Stdout), names(x.d.Run(ctx, env, nil, "autorandr", "--current").Stdout)
for i := range profiles {
profiles[i].Detected, profiles[i].Current = detected[profiles[i].Name], current[profiles[i].Name]
}
answer["profiles"] = profiles
return answer, nil
}
name, err := a.Text("name")
if err != nil {
return nil, err
}
if !profileName.MatchString(name) {
return nil, fmt.Errorf("a profile name is letters, digits, dot, dash and underscore, up to 64")
}
args := []string{"--load", name}
if action == "save" {
replace, _, err := a.Bool("replace")
if err != nil {
return nil, err
}
args = []string{"--save", name}
if replace {
args = append(args, "--force")
}
}
res, s, err := x.d.InSession(ctx, "autorandr", args...)
if err != nil {
return nil, err
}
if res.Code == 127 {
return nil, fmt.Errorf("autorandr is not installed here; the xorg module's package resource installs it")
}
if !res.OK() {
return nil, res.Err()
}
answer := map[string]any{"action": action, "name": name, "session": s.Display, "said": strings.TrimSpace(res.Stdout + res.Stderr)}
if action == "save" {
answer["kept"] = "in " + dirs[0] + ": the operator's data; the session's start applies it whenever these monitors are connected"
}
return answer, nil
}
// ModeArgs is the xrandr command line set-mode asks for, refused before anything runs when it is not
// one xrandr would take.
func ModeArgs(a desktop.Args) ([]string, error) {
output, err := a.Text("output")
if err != nil {
return nil, err
}
if !outputName.MatchString(output) {
return nil, fmt.Errorf("%q is not an output name", output)
}
args := []string{"--output", output}
off, _, err := a.Bool("off")
if err != nil {
return nil, err
}
if off {
return append(args, "--off"), nil
}
switch mode := a.Opt("mode", ""); {
case mode == "auto":
args = append(args, "--auto")
case mode != "":
if !regexp.MustCompile(`^\d{2,5}x\d{2,5}i?$`).MatchString(mode) {
return nil, fmt.Errorf("mode is WxH, e.g. 2560x1440, or auto")
}
args = append(args, "--mode", mode)
}
if rate, given, err := a.Number("rate"); err != nil {
return nil, err
} else if given {
if rate < 1 || rate > 1000 {
return nil, fmt.Errorf("rate is in Hz, 1 to 1000")
}
args = append(args, "--rate", strconv.FormatFloat(rate, 'f', 2, 64))
}
if a.Has("rotate") {
r, err := a.OneOf("rotate", "", "normal", "left", "right", "inverted")
if err != nil {
return nil, err
}
args = append(args, "--rotate", r)
}
if scale, given, err := a.Number("scale"); err != nil {
return nil, err
} else if given {
if scale < 0.25 || scale > 4 {
return nil, fmt.Errorf("scale is from 0.25 to 4")
}
f := strconv.FormatFloat(scale, 'f', -1, 64)
args = append(args, "--scale", f+"x"+f)
}
if a.Has("relation") {
rel, err := a.OneOf("relation", "", "right-of", "left-of", "above", "below", "same-as")
if err != nil {
return nil, err
}
of, err := a.Text("of")
if err != nil || !outputName.MatchString(of) {
return nil, fmt.Errorf("relation needs of: the output it is beside")
}
args = append(args, "--"+rel, of)
} else if a.Has("x") || a.Has("y") {
px, err := a.Whole("x", 0, -32768, 32768)
if err != nil {
return nil, err
}
py, err := a.Whole("y", 0, -32768, 32768)
if err != nil {
return nil, err
}
args = append(args, "--pos", fmt.Sprintf("%dx%d", px, py))
}
if p, _, err := a.Bool("primary"); err != nil {
return nil, err
} else if p {
args = append(args, "--primary")
}
if len(args) == 2 {
return nil, fmt.Errorf("nothing to change: give a mode, rate, rotate, scale, position, off or primary")
}
return args, nil
}
func (x xorg) setMode(ctx context.Context, a desktop.Args) (any, error) {
args, err := ModeArgs(a)
if err != nil {
return nil, err
}
if dry, _, _ := a.Bool("dry_run"); dry {
return map[string]any{"dry_run": true, "command": append([]string{"xrandr"}, args...)}, nil
}
res, s, err := x.d.InSession(ctx, "xrandr", args...)
if err != nil {
return nil, err
}
if !res.OK() {
return nil, res.Err()
}
_, outs, _, _ := x.query(ctx)
var now *Output
for i := range outs {
if outs[i].Name == args[1] {
o := outs[i]
o.Modes = nil
now = &o
}
}
return map[string]any{"session": s.Display, "command": res.Command, "output": now, "lasts": untilLogin}, nil
}
func (x xorg) primary(ctx context.Context, a desktop.Args) (any, error) {
if name := a.Opt("output", ""); name != "" {
if !outputName.MatchString(name) {
return nil, fmt.Errorf("%q is not an output name", name)
}
res, _, err := x.d.InSession(ctx, "xrandr", "--output", name, "--primary")
if err != nil {
return nil, err
}
if !res.OK() {
return nil, res.Err()
}
}
_, outs, s, err := x.query(ctx)
if err != nil {
return nil, err
}
answer := map[string]any{"session": s.Display, "primary": nil}
for _, o := range outs {
if o.Primary {
answer["primary"] = o.Name
}
}
if a.Has("output") {
answer["lasts"] = untilLogin
}
return answer, nil
}
// ResourceValue is one resource's value from `xrdb -query` output.
func ResourceValue(query, name string) string {
for _, l := range strings.Split(query, "\n") {
if k, v, ok := strings.Cut(l, ":"); ok && strings.TrimSpace(k) == name {
return strings.TrimSpace(v)
}
}
return ""
}
func (x xorg) dpi(ctx context.Context, a desktop.Args) (any, error) {
value, err := a.Whole("value", 0, 48, 480)
if err != nil {
return nil, err
}
s, err := x.d.Find()
if err != nil {
return nil, err
}
env := s.Env(x.d.Base)
if value != 0 {
v := strconv.Itoa(value)
if r := x.d.Run(ctx, env, []byte("Xft.dpi: "+v+"\n"), "xrdb", "-nocpp", "-merge", "-"); !r.OK() {
return nil, r.Err()
}
if r := x.d.Run(ctx, env, nil, "xrandr", "--dpi", v); !r.OK() {
return nil, r.Err()
}
}
q := x.d.Run(ctx, env, nil, "xrdb", "-query")
if !q.OK() {
return nil, q.Err()
}
_, outs, _, err := x.query(ctx)
if err != nil {
return nil, err
}
physical := map[string]float64{}
for _, o := range outs {
if o.DPI > 0 {
physical[o.Name] = o.DPI
}
}
answer := map[string]any{"session": s.Display, "xft_dpi": ResourceValue(q.Stdout, "Xft.dpi"), "physical": physical}
if value != 0 {
answer["set"] = value
answer["lasts"] = "for programs started from now on, " + untilLogin
}
return answer, nil
}
func (x xorg) devices(ctx context.Context, env []string) ([]Device, error) {
r := x.d.Run(ctx, env, nil, "xinput", "list")
if !r.OK() {
return nil, r.Err()
}
return ParseDevices(r.Stdout), nil
}
func (x xorg) inputDevices(ctx context.Context, a desktop.Args) (any, error) {
s, err := x.d.Find()
if err != nil {
return nil, err
}
env := s.Env(x.d.Base)
all, err := x.devices(ctx, env)
if err != nil {
return nil, err
}
filter := strings.ToLower(a.Opt("name", ""))
var out []Device
for _, dev := range all {
if filter != "" && !strings.Contains(strings.ToLower(dev.Name), filter) {
continue
}
if dev.Role == "slave" && dev.Kind == "pointer" && !strings.Contains(dev.Name, "XTEST") {
if r := x.d.Run(ctx, env, nil, "xinput", "list-props", strconv.Itoa(dev.ID)); r.OK() {
dev.Settings = ParseProps(r.Stdout)
}
}
out = append(out, dev)
}
return map[string]any{"session": s.Display, "devices": out}, nil
}
func (x xorg) inputSet(ctx context.Context, a desktop.Args) (any, error) {
which, err := a.Text("device")
if err != nil {
return nil, err
}
type change struct {
knob Knob
value string
}
var changes []change
for _, k := range Knobs {
if !a.Has(k.Arg) {
continue
}
switch k.Kind {
case "bool":
b, _, err := a.Bool(k.Arg)
if err != nil {
return nil, err
}
changes = append(changes, change{k, map[bool]string{true: "1", false: "0"}[b]})
case "float":
f, _, err := a.Number(k.Arg)
if err != nil {
return nil, err
}
if f < -1 || f > 1 {
return nil, fmt.Errorf("%s is from -1 to 1", k.Arg)
}
changes = append(changes, change{k, strconv.FormatFloat(f, 'f', 3, 64)})
}
}
if len(changes) == 0 {
return nil, fmt.Errorf("nothing to change: name at least one setting")
}
s, err := x.d.Find()
if err != nil {
return nil, err
}
env := s.Env(x.d.Base)
all, err := x.devices(ctx, env)
if err != nil {
return nil, err
}
var ids []Device
for _, dev := range all {
if dev.Role == "slave" && (strconv.Itoa(dev.ID) == which || dev.Name == which) {
ids = append(ids, dev)
}
}
if len(ids) == 0 {
return nil, fmt.Errorf("no input device is %q: xorg_input_devices lists them", which)
}
type outcome struct {
ID int `json:"id"`
Name string `json:"name"`
Set []string `json:"set"`
Skipped []string `json:"skipped,omitempty"`
Now map[string]any `json:"now"`
}
var done []outcome
for _, dev := range ids {
props := x.d.Run(ctx, env, nil, "xinput", "list-props", strconv.Itoa(dev.ID))
have := ParseProps(props.Stdout)
o := outcome{ID: dev.ID, Name: dev.Name, Set: []string{}}
for _, c := range changes {
if _, ok := have[c.knob.Arg]; !ok {
o.Skipped = append(o.Skipped, c.knob.Arg)
continue
}
r := x.d.Run(ctx, env, nil, "xinput", "set-prop", strconv.Itoa(dev.ID), c.knob.Property, c.value)
if !r.OK() {
return nil, r.Err()
}
o.Set = append(o.Set, c.knob.Arg)
}
after := x.d.Run(ctx, env, nil, "xinput", "list-props", strconv.Itoa(dev.ID))
o.Now = ParseProps(after.Stdout)
done = append(done, o)
}
return map[string]any{"session": s.Display, "devices": done, "lasts": "until the device reconnects or the next login"}, nil
}
func (x xorg) keyboard(ctx context.Context, a desktop.Args) (any, error) {
s, err := x.d.Find()
if err != nil {
return nil, err
}
env := s.Env(x.d.Base)
var args []string
for _, k := range []string{"layout", "variant", "model"} {
if v := a.Opt(k, ""); v != "" {
if !xkbName.MatchString(v) {
return nil, fmt.Errorf("%s %q is not an XKB name", k, v)
}
args = append(args, "-"+k, v)
}
}
if a.Has("options") {
opts, err := a.Strings("options")
if err != nil {
return nil, err
}
args = append(args, "-option", "")
for _, o := range opts {
if !xkbName.MatchString(o) {
return nil, fmt.Errorf("option %q is not an XKB name", o)
}
args = append(args, "-option", o)
}
}
if len(args) > 0 {
if r := x.d.Run(ctx, env, nil, "setxkbmap", args...); !r.OK() {
return nil, r.Err()
}
}
q := x.d.Run(ctx, env, nil, "setxkbmap", "-query")
if !q.OK() {
return nil, q.Err()
}
answer := map[string]any{"session": s.Display, "keyboard": ParseKeyboard(q.Stdout)}
if len(args) > 0 {
answer["lasts"] = untilLogin
}
return answer, nil
}
var windowID = regexp.MustCompile(`^(0x[0-9a-fA-F]{1,8}|[0-9]{1,10})$`)
func (x xorg) screenshot(ctx context.Context, a desktop.Args) (any, error) {
stamp := time.Now().Format("20060102-150405")
path, err := desktop.InHome(a.Opt("path", "~/Pictures/Screenshots/screenshot-"+stamp+".png"))
if err != nil {
return nil, err
}
if !strings.HasSuffix(strings.ToLower(path), ".png") {
return nil, fmt.Errorf("the screenshot is a PNG: the path ends in .png")
}
s, err := x.d.Find()
if err != nil {
return nil, err
}
env := s.Env(x.d.Base)
dump, err := os.CreateTemp("", "xorg-screenshot-*.xwd")
if err != nil {
return nil, err
}
dump.Close()
defer os.Remove(dump.Name())
args := []string{"-silent", "-out", dump.Name()}
if w := a.Opt("window", ""); w != "" {
if !windowID.MatchString(w) {
return nil, fmt.Errorf("window is an X window id, e.g. 0x3a00007")
}
args = append(args, "-id", w)
} else {
args = append(args, "-root")
}
if r := x.d.Run(ctx, env, nil, "xwd", args...); !r.OK() {
if r.Code == 127 {
return nil, fmt.Errorf("xwd is not installed here; the xorg module's package resource (xorg-xwd) installs it")
}
return nil, r.Err()
}
raw, err := os.ReadFile(dump.Name())
if err != nil {
return nil, err
}
img, err := DecodeXWD(raw)
if err != nil {
return nil, fmt.Errorf("the dump xwd wrote: %w", err)
}
var shot image.Image = img
if name := a.Opt("output", ""); name != "" {
_, outs, _, err := x.query(ctx)
if err != nil {
return nil, err
}
var g *Geometry
for _, o := range outs {
if o.Name == name {
g = o.Geometry
}
}
if g == nil {
return nil, fmt.Errorf("output %q is not on: displays lists the outputs in use", name)
}
r := image.Rect(g.X, g.Y, g.X+g.Width, g.Y+g.Height).Intersect(img.Bounds())
crop := image.NewRGBA(image.Rect(0, 0, r.Dx(), r.Dy()))
draw.Draw(crop, crop.Bounds(), img, r.Min, draw.Src)
shot = crop
}
var buf bytes.Buffer
if err := png.Encode(&buf, shot); err != nil {
return nil, err
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return nil, err
}
if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil {
return nil, err
}
b := shot.Bounds()
return map[string]any{"session": s.Display, "path": path, "width": b.Dx(), "height": b.Dy(), "bytes": buf.Len()}, nil
}
func (x xorg) xlog(ctx context.Context, a desktop.Args) (any, error) {
warnings, _, err := a.Bool("warnings")
if err != nil {
return nil, err
}
limit, err := a.Whole("limit", 100, 1, 1000)
if err != nil {
return nil, err
}
display := -1
if s, err := x.d.Find(); err == nil {
if n, ok := desktop.DisplayNumber(s.Display); ok {
display = n
}
}
files := XLogFiles(display, desktop.Home())
if display >= 0 {
files = append(files, XLogFiles(-1, desktop.Home())...)
}
for _, f := range files {
raw, err := os.ReadFile(f)
if err != nil {
continue
}
return ParseXLog(f, string(raw), warnings, limit), nil
}
sort.Strings(files)
return nil, fmt.Errorf("no X server log is readable here; looked at %s", strings.Join(files, ", "))
}
+103
View File
@@ -0,0 +1,103 @@
package main
import (
"bufio"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
)
// LogLine is one marked line of the X server's log.
type LogLine struct {
At float64 `json:"at_seconds"`
Level string `json:"level"`
Text string `json:"text"`
}
// XLog is what x-log answers.
type XLog struct {
File string `json:"file"`
Server string `json:"server,omitempty"`
Started string `json:"started,omitempty"`
Errors int `json:"errors"`
Warnings int `json:"warnings"`
Lines []LogLine `json:"lines"`
Cut bool `json:"cut,omitempty"`
}
var (
logMark = regexp.MustCompile(`^\[\s*([0-9.]+)\] \((EE|WW)\) (.*)$`)
logStart = regexp.MustCompile(`\(==\) Log file: "[^"]*", Time: (.+)$`)
)
// ParseXLog reads an Xorg log: the server's version, when it started, and its errors and (when
// asked) warnings, the last `limit` of them. The legend line that explains the markers is not one.
func ParseXLog(file, text string, warnings bool, limit int) XLog {
out := XLog{File: file, Lines: []LogLine{}}
sc := bufio.NewScanner(strings.NewReader(text))
sc.Buffer(make([]byte, 1<<20), 1<<22)
for sc.Scan() {
line := sc.Text()
if out.Server == "" && strings.HasPrefix(strings.TrimSpace(line), "X.Org X Server ") {
out.Server = strings.TrimSpace(line)
}
if m := logStart.FindStringSubmatch(line); m != nil && out.Started == "" {
out.Started = m[1]
}
m := logMark.FindStringSubmatch(line)
if m == nil {
continue
}
at, _ := strconv.ParseFloat(m[1], 64)
level := map[string]string{"EE": "error", "WW": "warning"}[m[2]]
if level == "error" {
out.Errors++
} else {
out.Warnings++
if !warnings {
continue
}
}
out.Lines = append(out.Lines, LogLine{At: at, Level: level, Text: m[3]})
}
if limit > 0 && len(out.Lines) > limit {
out.Lines = out.Lines[len(out.Lines)-limit:]
out.Cut = true
}
return out
}
// XLogFiles are where an X server on display n logs, best first: a root server's log, then a
// rootless one's under the account's data directory. With no display, every log found, newest first.
func XLogFiles(display int, home string) []string {
dirs := []string{"/var/log", filepath.Join(home, ".local/share/xorg")}
if display >= 0 {
var out []string
for _, d := range dirs {
out = append(out, filepath.Join(d, "Xorg."+strconv.Itoa(display)+".log"))
}
return out
}
type found struct {
path string
mod int64
}
var all []found
for _, d := range dirs {
matches, _ := filepath.Glob(filepath.Join(d, "Xorg.*.log"))
for _, p := range matches {
if info, err := os.Stat(p); err == nil {
all = append(all, found{p, info.ModTime().UnixNano()})
}
}
}
sort.Slice(all, func(i, j int) bool { return all[i].mod > all[j].mod })
var out []string
for _, f := range all {
out = append(out, f.path)
}
return out
}
+338
View File
@@ -0,0 +1,338 @@
package main
import (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
"encoding/json"
"image/color"
"os"
"path/filepath"
"strings"
"testing"
"xorg/internal/desktop"
)
func fixture(t *testing.T, name string) string {
b, err := os.ReadFile(filepath.Join("testdata", name))
if err != nil {
t.Fatal(err)
}
return string(b)
}
// fakeDesk answers commands from a table keyed by the command line, and records what ran.
type fakeDesk struct {
answers map[string]desktop.Result
ran []string
env [][]string
}
func (f *fakeDesk) desk(session bool) desktop.Desk {
return desktop.Desk{
Find: func() (*desktop.Session, error) {
if !session {
return nil, &desktop.NoSession{Reason: "no process of uid 1000 carries a display"}
}
return &desktop.Session{UID: 1000, Display: ":1", Type: "x11", XAuthority: "/home/op/.Xauthority", RuntimeDir: "/run/user/1000"}, nil
},
Run: func(_ context.Context, env []string, _ []byte, name string, args ...string) desktop.Result {
line := strings.Join(append([]string{name}, args...), " ")
f.ran = append(f.ran, line)
f.env = append(f.env, env)
if r, ok := f.answers[line]; ok {
r.Command = append([]string{name}, args...)
return r
}
return desktop.Result{Command: append([]string{name}, args...), Code: 127, Stderr: "not in the table: " + line}
},
Base: []string{"PATH=/usr/bin"},
}
}
func byName(t *testing.T, d desktop.Desk, name string) func(map[string]any) (any, error) {
for _, tool := range tools(d) {
if tool.Name == name {
return tool.Run
}
}
t.Fatalf("no tool %s", name)
return nil
}
func TestTheOutputsAndTheMonitorsIdentityAreReadFromXrandr(t *testing.T) {
screen, outs := ParseXrandr(fixture(t, "xrandr-prop.txt"))
if screen.Width != 2880 || screen.Height != 1800 || screen.MaxWidth != 16384 {
t.Fatalf("screen: %+v", screen)
}
if len(outs) != 13 {
t.Fatalf("%d outputs", len(outs))
}
e := outs[0]
if e.Name != "eDP-1" || !e.Connected || !e.Primary || !e.Enabled || e.Rotation != "normal" || e.WidthMM != 302 {
t.Fatalf("eDP-1: %+v", e)
}
if *e.Geometry != (Geometry{2880, 1800, 0, 0}) || e.DPI != 242.2 {
t.Fatalf("geometry and DPI: %+v %v", e.Geometry, e.DPI)
}
if e.Current == nil || e.Current.Rate != 60 || e.Preferred == nil || e.Preferred.Size != "2880x1800" || e.Preferred.Rate != 60 {
t.Fatalf("current %+v preferred %+v", e.Current, e.Preferred)
}
if e.Monitor == nil || e.Monitor.Manufacturer != "SDC" || e.Monitor.Product != "419C" || e.Monitor.Name != "ATNA40CU05-0" || len(e.Monitor.Fingerprint) != 12 {
t.Fatalf("identity: %+v", e.Monitor)
}
if len(e.Modes) < 20 || e.Modes[0].Size != "2880x1800" || len(e.Modes[0].Rates) != 2 {
t.Fatalf("modes: %+v", e.Modes[:2])
}
if outs[1].Connected || outs[1].Enabled || outs[1].Name != "DP-1" {
t.Fatalf("a disconnected output: %+v", outs[1])
}
}
func TestARotatedSecondMonitorIsReadWithItsPlaceAndDensity(t *testing.T) {
text := "Screen 0: minimum 8 x 8, current 4000 x 2560, maximum 32767 x 32767\n" +
"DP-2 connected 1440x2560+2560+0 left (normal left inverted right x axis y axis) 597mm x 336mm\n" +
" 2560x1440 59.95 + 74.97*\n"
_, outs := ParseXrandr(text)
o := outs[0]
if o.Rotation != "left" || o.Geometry.X != 2560 || o.Current.Rate != 74.97 || o.Preferred.Rate != 59.95 {
t.Fatalf("%+v %+v %+v", o, o.Current, o.Preferred)
}
if o.DPI < 108 || o.DPI > 110 {
t.Fatalf("a rotated output's density is its long side over its long side: %v", o.DPI)
}
}
func TestSetModeBuildsOnlyCommandsXrandrTakes(t *testing.T) {
args, err := ModeArgs(desktop.Args{"output": "DP-2", "mode": "2560x1440", "rate": 144.0, "rotate": "left", "scale": 1.5, "relation": "right-of", "of": "eDP-1", "primary": true})
want := "--output DP-2 --mode 2560x1440 --rate 144.00 --rotate left --scale 1.5x1.5 --right-of eDP-1 --primary"
if err != nil || strings.Join(args, " ") != want {
t.Fatalf("%v %v", args, err)
}
if args, _ := ModeArgs(desktop.Args{"output": "HDMI-1", "off": true, "mode": "1x1"}); strings.Join(args, " ") != "--output HDMI-1 --off" {
t.Fatalf("off wins: %v", args)
}
if args, _ := ModeArgs(desktop.Args{"output": "DP-1", "x": 1920.0, "y": 0.0}); strings.Join(args, " ") != "--output DP-1 --pos 1920x0" {
t.Fatalf("position: %v", args)
}
for _, bad := range []desktop.Args{
{"output": "DP-1; rm -rf ~"}, {"output": "DP-1"}, {"output": "DP-1", "mode": "big"},
{"output": "DP-1", "scale": 9.0}, {"output": "DP-1", "relation": "right-of"}, {"output": "DP-1", "rotate": "sideways"},
} {
if _, err := ModeArgs(bad); err == nil {
t.Errorf("accepted %v", bad)
}
}
}
func TestDryRunRunsNothing(t *testing.T) {
f := &fakeDesk{}
got, err := byName(t, f.desk(true), "xorg_set_mode")(map[string]any{"output": "eDP-1", "mode": "auto", "dry_run": true})
if err != nil || len(f.ran) != 0 || !strings.Contains(asJSON(got), `"--auto"`) {
t.Fatalf("%v %v %v", got, err, f.ran)
}
}
func TestInputDevicesAndTheirLibinputSettingsAreRead(t *testing.T) {
devs := ParseDevices(fixture(t, "xinput-list.txt"))
if len(devs) != 25 {
t.Fatalf("%d devices", len(devs))
}
var pad *Device
for i := range devs {
if devs[i].ID == 16 {
pad = &devs[i]
}
}
if pad == nil || pad.Name != "ASUP1208:00 093A:3011 Touchpad" || pad.Role != "slave" || pad.Kind != "pointer" {
t.Fatalf("%+v", pad)
}
if devs[0].Name != "Virtual core pointer" || devs[0].Role != "master" {
t.Fatalf("the tree's drawing is stripped: %+v", devs[0])
}
s := ParseProps(fixture(t, "xinput-props-16.txt"))
if s["tap"] != true || s["natural_scroll"] != true || s["accel_speed"] != 0.15 || s["accel_profile"] != "adaptive" || s["left_handed"] != false {
t.Fatalf("%v", s)
}
}
func TestInputSetChangesOnlyWhatADeviceHasAndSaysHowLongItLasts(t *testing.T) {
props := fixture(t, "xinput-props-16.txt")
f := &fakeDesk{answers: map[string]desktop.Result{
"xinput list": {Stdout: fixture(t, "xinput-list.txt")},
"xinput list-props 16": {Stdout: props},
"xinput list-props 15": {Stdout: "Device 'Mouse':\n\tDevice Enabled (187):\t1\n"},
"xinput set-prop 16 libinput Tapping Enabled 0": {},
}}
got, err := byName(t, f.desk(true), "xorg_input_set")(map[string]any{"device": "16", "tap": false})
if err != nil || !strings.Contains(asJSON(got), `"set":["tap"]`) || !strings.Contains(asJSON(got), "lasts") {
t.Fatalf("%s %v", asJSON(got), err)
}
if _, err := byName(t, f.desk(true), "xorg_input_set")(map[string]any{"device": "16"}); err == nil {
t.Fatal("nothing to change is refused")
}
if _, err := byName(t, f.desk(true), "xorg_input_set")(map[string]any{"device": "16", "accel_speed": 3.0}); err == nil {
t.Fatal("accel_speed out of range")
}
got, err = byName(t, f.desk(true), "xorg_input_set")(map[string]any{"device": "15", "tap": true})
if err != nil || !strings.Contains(asJSON(got), `"skipped":["tap"]`) {
t.Fatalf("a mouse has no tapping: %s %v", asJSON(got), err)
}
}
func TestTheKeyboardMapIsReadAndSetOnlyWithXKBNames(t *testing.T) {
k := ParseKeyboard(fixture(t, "setxkbmap.txt") + "options: caps:escape,compose:ralt\n")
if k.Layout != "us" || k.Model != "pc105" || len(k.Options) != 2 {
t.Fatalf("%+v", k)
}
f := &fakeDesk{answers: map[string]desktop.Result{
"setxkbmap -layout us,be -option -option caps:escape": {},
"setxkbmap -query": {Stdout: fixture(t, "setxkbmap.txt")},
}}
if _, err := byName(t, f.desk(true), "xorg_keyboard")(map[string]any{"layout": "us,be", "options": []any{"caps:escape"}}); err != nil {
t.Fatal(err, f.ran)
}
if _, err := byName(t, f.desk(true), "xorg_keyboard")(map[string]any{"layout": "us; xterm"}); err == nil {
t.Fatal("a layout that is not an XKB name")
}
}
// xwdOf writes a w×h dump of 32-bit pixels in the given byte order, coloured by colourAt.
func xwdOf(w, h int, lsb bool, colourAt func(x, y int) (r, g, b uint8)) []byte {
var buf bytes.Buffer
name := []byte("xwdump\x00\x00")
header := []uint32{uint32(100 + len(name)), 7, 2, 24, uint32(w), uint32(h), 0, 0, 32, 0, 32, 32, uint32(w * 4), 4,
0xff0000, 0x00ff00, 0x0000ff, 8, 256, 0, uint32(w), uint32(h), 0, 0, 0}
if lsb {
header[7] = 0
} else {
header[7] = 1
}
for _, v := range header {
binary.Write(&buf, binary.BigEndian, v)
}
buf.Write(name)
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
r, g, b := colourAt(x, y)
p := uint32(r)<<16 | uint32(g)<<8 | uint32(b)
if lsb {
binary.Write(&buf, binary.LittleEndian, p)
} else {
binary.Write(&buf, binary.BigEndian, p)
}
}
}
return buf.Bytes()
}
func TestAWindowDumpBecomesThePictureItHolds(t *testing.T) {
paint := func(x, y int) (uint8, uint8, uint8) { return uint8(x * 40), uint8(y * 60), 0xde }
for _, lsb := range []bool{true, false} {
img, err := DecodeXWD(xwdOf(5, 3, lsb, paint))
if err != nil {
t.Fatal(err)
}
if img.Bounds().Dx() != 5 || img.Bounds().Dy() != 3 {
t.Fatalf("bounds %v", img.Bounds())
}
if got := img.RGBAAt(4, 2); got != (color.RGBA{160, 120, 0xde, 0xff}) {
t.Fatalf("lsb=%v: %v", lsb, got)
}
}
dump := xwdOf(2, 2, true, paint)
if _, err := DecodeXWD(dump[:len(dump)-3]); err == nil {
t.Fatal("a cut dump is refused")
}
binary.BigEndian.PutUint32(dump[4:], 6)
if _, err := DecodeXWD(dump); err == nil {
t.Fatal("another file version is refused")
}
}
func TestTheServersLogAnswersItsErrorsAndNotTheLegend(t *testing.T) {
text := "[ 16.118] \nX.Org X Server 1.21.1.24\n[ 16.118] Markers: (--) probed,\n" +
"[ 16.118] \t(WW) warning, (EE) error, (NI) not implemented, (??) unknown.\n" +
"[ 16.118] (==) Log file: \"/var/log/Xorg.1.log\", Time: Fri Oct 2 13:08:45 2026\n" +
"[ 16.123] (WW) The directory \"/usr/share/fonts/misc\" does not exist.\n" +
"[ 16.181] (EE) Failed to load module \"ati\" (module does not exist, 0)\n" +
"[ 16.184] (EE) Failed to load module \"nouveau\" (module does not exist, 0)\n"
got := ParseXLog("/var/log/Xorg.1.log", text, false, 1)
if got.Server != "X.Org X Server 1.21.1.24" || got.Started != "Fri Oct 2 13:08:45 2026" || got.Errors != 2 || got.Warnings != 1 {
t.Fatalf("%+v", got)
}
if len(got.Lines) != 1 || !got.Cut || !strings.Contains(got.Lines[0].Text, "nouveau") || got.Lines[0].At != 16.184 {
t.Fatalf("the newest error, cut to the limit: %+v", got.Lines)
}
if all := ParseXLog("", text, true, 0); len(all.Lines) != 3 {
t.Fatalf("with warnings: %+v", all.Lines)
}
}
func TestProfilesAreReadWithTheMonitorsTheyAreFor(t *testing.T) {
dir := t.TempDir()
_, outs := ParseXrandr(fixture(t, "xrandr-prop.txt"))
os.MkdirAll(filepath.Join(dir, "laptop"), 0o755)
os.WriteFile(filepath.Join(dir, "laptop", "setup"), []byte("eDP-1 "+hex.EncodeToString(outs[0].edid)+"\n"), 0o644)
os.MkdirAll(filepath.Join(dir, "empty"), 0o755) // no setup: not a profile
got := ReadProfiles([]string{dir, filepath.Join(dir, "missing")})
if len(got) != 1 || got[0].Name != "laptop" || got[0].Monitors["eDP-1"] != outs[0].Monitor.Fingerprint {
t.Fatalf("%+v", got)
}
if !profileName.MatchString("home-office_2") || profileName.MatchString("../x") || profileName.MatchString("") {
t.Fatal("profile names")
}
}
func TestEveryToolThatNeedsTheScreenSaysPlainlyThatThereIsNoSession(t *testing.T) {
f := &fakeDesk{}
d := f.desk(false)
for _, call := range []struct {
tool string
args map[string]any
}{
{"node-display-server.displays", nil},
{"node-display-server.layout", map[string]any{"action": "apply", "name": "home"}},
{"xorg_set_mode", map[string]any{"output": "eDP-1", "mode": "auto"}},
{"xorg_primary", nil}, {"xorg_dpi", nil}, {"xorg_input_devices", nil},
{"xorg_input_set", map[string]any{"device": "16", "tap": true}}, {"xorg_keyboard", nil},
{"xorg_screenshot", nil},
} {
_, err := byName(t, d, call.tool)(orEmpty(call.args))
if !desktop.IsNoSession(err) {
t.Errorf("%s: %v", call.tool, err)
}
}
got, err := byName(t, d, "node-display-server.layout")(map[string]any{"action": "list"})
if err != nil || !strings.Contains(asJSON(got), "no graphical session") {
t.Fatalf("listing profiles needs no session, and says what it could not tell: %v %v", got, err)
}
if len(f.ran) != 0 {
t.Fatalf("nothing ran without a session: %v", f.ran)
}
}
func TestACommandRunsWithTheSessionsDisplay(t *testing.T) {
f := &fakeDesk{answers: map[string]desktop.Result{"xrandr --prop": {Stdout: fixture(t, "xrandr-prop.txt")}}}
if _, err := byName(t, f.desk(true), "node-display-server.displays")(map[string]any{}); err != nil {
t.Fatal(err)
}
env := strings.Join(f.env[0], " ")
if !strings.Contains(env, "DISPLAY=:1") || !strings.Contains(env, "XAUTHORITY=/home/op/.Xauthority") {
t.Fatalf("%s", env)
}
}
func orEmpty(m map[string]any) map[string]any {
if m == nil {
return map[string]any{}
}
return m
}
func asJSON(v any) string {
b, _ := json.Marshal(v)
return string(b)
}
+83
View File
@@ -0,0 +1,83 @@
package main
import (
"encoding/binary"
"errors"
"fmt"
"image"
"image/color"
"math/bits"
)
// DecodeXWD reads an X window dump as `xwd` writes it (XWDFile.h, file version 7): the screenshot
// tool's one dependency is xwd, from the X apps the display server already ships, and the PNG is made
// here — no screenshot program, no image library.
//
// Only what an X server dumps in practice is read: a ZPixmap of 24 or 32 bits per pixel with a
// TrueColor or DirectColor visual, in either byte order. Anything else is refused by name.
func DecodeXWD(b []byte) (*image.RGBA, error) {
const headerWords = 25
if len(b) < headerWords*4 {
return nil, errors.New("too short for an xwd header")
}
h := make([]uint32, headerWords)
for i := range h {
h[i] = binary.BigEndian.Uint32(b[i*4:])
}
headerSize, version, format := h[0], h[1], h[2]
width, height := int(h[4]), int(h[5])
byteOrder, bpp, bytesPerLine, class := h[7], int(h[11]), int(h[12]), h[13]
red, green, blue := h[14], h[15], h[16]
ncolors := int(h[19])
switch {
case version != 7:
return nil, fmt.Errorf("xwd file version %d; 7 is read", version)
case format != 2:
return nil, fmt.Errorf("pixmap format %d; ZPixmap (2) is read", format)
case bpp != 24 && bpp != 32:
return nil, fmt.Errorf("%d bits per pixel; 24 and 32 are read", bpp)
case class != 4 && class != 5:
return nil, fmt.Errorf("visual class %d; TrueColor and DirectColor are read", class)
case width <= 0 || height <= 0 || width > 32768 || height > 32768:
return nil, fmt.Errorf("a %dx%d image", width, height)
case bytesPerLine < width*bpp/8:
return nil, fmt.Errorf("%d bytes per line for %d pixels", bytesPerLine, width)
}
start := int(headerSize) + ncolors*12
if start < 0 || len(b) < start+bytesPerLine*height {
return nil, fmt.Errorf("the image data is cut short: %d bytes, %d needed", len(b), start+bytesPerLine*height)
}
pixels := b[start:]
step := bpp / 8
channel := func(p, mask uint32) uint8 {
if mask == 0 {
return 0
}
shift := bits.TrailingZeros32(mask)
width := bits.OnesCount32(mask)
v := (p & mask) >> shift
if width >= 8 {
return uint8(v >> (width - 8))
}
return uint8(v << (8 - width))
}
img := image.NewRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
row := pixels[y*bytesPerLine:]
for x := 0; x < width; x++ {
px := row[x*step : x*step+step]
var p uint32
if byteOrder == 0 { // LSBFirst
for i := step - 1; i >= 0; i-- {
p = p<<8 | uint32(px[i])
}
} else {
for i := 0; i < step; i++ {
p = p<<8 | uint32(px[i])
}
}
img.SetRGBA(x, y, color.RGBA{channel(p, red), channel(p, green), channel(p, blue), 0xff})
}
}
return img, nil
}