Files
mesh-catalog/modules/xorg/cmd/xorg/tools.go
T
jochen b2c170acda 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.
2026-10-04 13:21:47 +02:00

603 lines
16 KiB
Go

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, ", "))
}