Files
mesh-catalog/modules/xorg/cmd/xorg/windows.go
T
jschoubben 4fe8958dda
mesh/merge-gate pass: builds xorg → g14, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
xorg: refuse a window that is not on the screen, and keep an inline screenshot within the bus (review of #154)
A window that is unmapped or on a hidden workspace was cut from the screen anyway, which shows what is
there instead of it. An inline image of up to 4 MiB could not cross the bus, whose largest message is
1 MiB; it is now held to 700 KiB and halved in size until it fits, saying so.
2026-10-09 12:23:51 +02:00

253 lines
8.4 KiB
Go

package main
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"xorg/internal/desktop"
)
// The windows on the X screen, as the server knows them (xwininfo): what a window manager's own list
// (node-display-session.windows) leaves out — a window it does not manage, a dialog, a tooltip, a
// window that is mapped but drawn by nobody — and what a compositor goes by: the window's depth (32
// bits is a visual with an alpha channel, which a compositor blends with what is behind it), its type,
// whether it is mapped, and its own opacity.
//
// The tree comes from one `xwininfo -root -tree`. A window's details take one `xwininfo -all` and one
// `xprop` each, so they are read only for the windows asked about, and at most mostDetailed of them.
const mostDetailed = 40
// Window is one window of the tree, and its details when they were read.
type Window struct {
ID string `json:"window"`
Name string `json:"name,omitempty"`
Instance string `json:"instance,omitempty"`
Class string `json:"class,omitempty"`
Width int `json:"width"`
Height int `json:"height"`
X int `json:"x"`
Y int `json:"y"`
// Level is the window's depth in the tree: 1 is a child of the root (a frame or an unmanaged window).
Level int `json:"level"`
// The details (detail true, or a class asked for).
Depth int `json:"depth,omitempty"`
ARGB *bool `json:"argb,omitempty"`
Visual string `json:"visual_class,omitempty"`
Mapped string `json:"map_state,omitempty"`
OverrideRedir *bool `json:"override_redirect,omitempty"`
Types []string `json:"types,omitempty"`
States []string `json:"states,omitempty"`
Role string `json:"role,omitempty"`
Opacity *float64 `json:"opacity,omitempty"`
DetailProblems string `json:"detail_problem,omitempty"`
}
// treeLine is one line of `xwininfo -root -tree`: the id, the name (or "(has no name)"), the class
// pair when the window has one, the size and position relative to the parent, and the absolute one.
var treeLine = regexp.MustCompile(`^(\s*)(0x[0-9a-fA-F]+) (?:\(has no name\)|"(.*)"): \((?:"([^"]*)" "([^"]*)")?\)\s+(\d+)x(\d+)[+-]-?\d+[+-]-?\d+\s+\+?(-?\d+)\+?(-?\d+)\s*$`)
// ParseTree reads the windows of `xwininfo -root -tree`, in the server's stacking order per parent.
func ParseTree(text string) []Window {
var out []Window
for _, line := range strings.Split(text, "\n") {
m := treeLine.FindStringSubmatch(line)
if m == nil {
continue
}
w := Window{ID: m[2], Name: m[3], Instance: m[4], Class: m[5]}
w.Width, _ = strconv.Atoi(m[6])
w.Height, _ = strconv.Atoi(m[7])
w.X, _ = strconv.Atoi(m[8])
w.Y, _ = strconv.Atoi(m[9])
// xwininfo indents a child of the root by 5 spaces and each further level by 3.
w.Level = 1 + (len(m[1])-5)/3
out = append(out, w)
}
return out
}
// ParseDetails adds what `xwininfo -id <w> -all` says about one window.
func ParseDetails(w *Window, text string) {
section := ""
for _, raw := range strings.Split(text, "\n") {
line := strings.TrimSpace(raw)
indent := len(raw) - len(strings.TrimLeft(raw, " "))
switch {
case strings.HasPrefix(line, "Depth:"):
w.Depth, _ = strconv.Atoi(strings.TrimSpace(strings.TrimPrefix(line, "Depth:")))
argb := w.Depth == 32
w.ARGB = &argb
section = ""
case strings.HasPrefix(line, "Visual Class:"):
w.Visual = strings.TrimSpace(strings.TrimPrefix(line, "Visual Class:"))
case strings.HasPrefix(line, "Map State:"):
w.Mapped = strings.TrimSpace(strings.TrimPrefix(line, "Map State:"))
case strings.HasPrefix(line, "Override Redirect State:"):
v := strings.TrimSpace(strings.TrimPrefix(line, "Override Redirect State:")) == "yes"
w.OverrideRedir = &v
case line == "Window type:":
section = "type"
case line == "Window state:":
section = "state"
case line == "":
section = ""
case section != "" && indent >= 10:
if section == "type" {
w.Types = append(w.Types, line)
} else {
w.States = append(w.States, line)
}
default:
if indent < 10 {
section = ""
}
}
}
}
// opacityLine and roleLine are the two properties read with xprop.
var (
opacityLine = regexp.MustCompile(`^_NET_WM_WINDOW_OPACITY\(CARDINAL\) = (\d+)`)
roleLine = regexp.MustCompile(`^WM_WINDOW_ROLE\(STRING\) = "(.*)"`)
)
// ParseWindowProps adds the window's role and its own opacity (0 to 1) from xprop.
func ParseWindowProps(w *Window, text string) {
for _, line := range strings.Split(text, "\n") {
line = strings.TrimSpace(line)
if m := opacityLine.FindStringSubmatch(line); m != nil {
if n, err := strconv.ParseUint(m[1], 10, 32); err == nil {
v := float64(n) / float64(0xffffffff)
v = float64(int(v*1000+0.5)) / 1000
w.Opacity = &v
}
}
if m := roleLine.FindStringSubmatch(line); m != nil {
w.Role = m[1]
}
}
}
func (x xorg) windows(ctx context.Context, a desktop.Args) (any, error) {
classExpr := a.Opt("class", "")
nameExpr := a.Opt("name", "")
detail, _, err := a.Bool("detail")
if err != nil {
return nil, err
}
all, _, err := a.Bool("all")
if err != nil {
return nil, err
}
var classRe, nameRe *regexp.Regexp
if classExpr != "" {
if classRe, err = regexp.Compile(classExpr); err != nil {
return nil, fmt.Errorf("class is a regular expression: %w", err)
}
}
if nameExpr != "" {
if nameRe, err = regexp.Compile(nameExpr); err != nil {
return nil, fmt.Errorf("name is a regular expression: %w", err)
}
}
res, s, err := x.d.InSession(ctx, "xwininfo", "-root", "-tree")
if err != nil {
return nil, err
}
if !res.OK() {
if res.Code == 127 {
return nil, fmt.Errorf("xwininfo is not installed here; the xorg module's package resource (xorg-xwininfo) installs it")
}
return nil, res.Err()
}
tree := ParseTree(res.Stdout)
var shown []Window
for _, w := range tree {
switch {
case classRe != nil && !(classRe.MatchString(w.Class) || classRe.MatchString(w.Instance)):
continue
case nameRe != nil && !nameRe.MatchString(w.Name):
continue
case classRe == nil && nameRe == nil && !all && w.Class == "":
// Without a filter, the windows a program owns: those with a class.
continue
}
shown = append(shown, w)
}
detailed := 0
if detail || classRe != nil || nameRe != nil {
env := s.Env(x.d.Base)
for i := range shown {
if detailed == mostDetailed {
break
}
detailed++
w := &shown[i]
info := x.d.Run(ctx, env, nil, "xwininfo", "-id", w.ID, "-all")
if !info.OK() {
// A window can go between the tree and its details.
w.DetailProblems = strings.TrimSpace(info.Stderr)
continue
}
ParseDetails(w, info.Stdout)
if props := x.d.Run(ctx, env, nil, "xprop", "-id", w.ID, "WM_WINDOW_ROLE", "_NET_WM_WINDOW_OPACITY"); props.OK() {
ParseWindowProps(w, props.Stdout)
}
}
}
answer := map[string]any{"session": s.Display, "windows": shown, "count": len(shown), "in_tree": len(tree)}
if detailed < len(shown) && (detail || classRe != nil || nameRe != nil) {
answer["detailed"] = fmt.Sprintf("the first %d of %d; narrow class or name for the rest", detailed, len(shown))
}
return answer, nil
}
// windowGeometry is a window's absolute position and size, from `xwininfo -id`.
func (x xorg) windowGeometry(ctx context.Context, env []string, id string) (image4, error) {
r := x.d.Run(ctx, env, nil, "xwininfo", "-id", id)
if !r.OK() {
if r.Code == 127 {
return image4{}, fmt.Errorf("xwininfo is not installed here; the xorg module's package resource (xorg-xwininfo) installs it")
}
return image4{}, r.Err()
}
return ParseGeometry(r.Stdout)
}
// image4 is a rectangle on the screen, and the window's map state there.
type image4 struct {
X, Y, W, H int
Mapped string
}
// ParseGeometry reads the absolute corner and the size from `xwininfo -id`.
func ParseGeometry(text string) (image4, error) {
var g image4
found := 0
for _, raw := range strings.Split(text, "\n") {
line := strings.TrimSpace(raw)
for prefix, into := range map[string]*int{
"Absolute upper-left X:": &g.X, "Absolute upper-left Y:": &g.Y, "Width:": &g.W, "Height:": &g.H,
} {
if strings.HasPrefix(line, "Map State:") {
g.Mapped = strings.TrimSpace(strings.TrimPrefix(line, "Map State:"))
}
if strings.HasPrefix(line, prefix) {
if n, err := strconv.Atoi(strings.TrimSpace(strings.TrimPrefix(line, prefix))); err == nil {
*into = n
found++
}
}
}
}
if found < 4 || g.W <= 0 || g.H <= 0 {
return image4{}, fmt.Errorf("xwininfo gave no geometry for the window")
}
return g, nil
}