Merge pull request 'xorg: list the server's windows, and cut a window's screenshot from the screen (hq issue 345)' (#154) from fix/xorg-windows-and-window-screenshot into main

This commit was merged in pull request #154.
This commit is contained in:
2026-10-09 10:51:01 +00:00
8 changed files with 601 additions and 13 deletions
+19 -3
View File
@@ -9,7 +9,7 @@ The X display server as a module (novox/hq ADR 0208, research 026, to-be 42 phas
- **Gated by the capability `seat`**, which the host reports for a machine with a graphics device
and a connected display. See *Blockers* for why not `graphical-session`.
- **Packages:** `xorg-server`, `xorg-xinit`, `xorg-xrandr`, `xorg-xset`, `xorg-xrdb`, `xorg-xinput`,
`xorg-setxkbmap`, `xorg-xwd` and `autorandr`. The GPU's driver is not here. It follows the machine,
`xorg-setxkbmap`, `xorg-xwd`, `xorg-xwininfo` and `autorandr`. The GPU's driver is not here. It follows the machine,
so it belongs to that machine model's hardware module.
## What it owns
@@ -71,7 +71,8 @@ which is a cpp directive error under plain `xrdb -merge` but is skipped here.
| `xorg_input_devices` | r | the input devices, and each pointer's libinput settings |
| `xorg_input_set` | a | tap, tap drag, natural scroll, disable while typing, left handed, middle emulation, enabled, acceleration, by device id or name |
| `xorg_keyboard` | r/a | the XKB map; set layout, variant, model, options |
| `xorg_screenshot` | d | a PNG of the screen, one output or one window, inside the home (default `~/Pictures/Screenshots/`) |
| `xorg_screenshot` | d | a PNG of the screen, one output or one window, inside the home (default `~/Pictures/Screenshots/`); with `inline`, the PNG in the answer as base64 (at most 700 KiB; a larger one is halved in size until it fits, and `inline_scale` says so). A window that is not viewable is refused |
| `xorg_windows` | r | the windows as the X server knows them, managed or not: id, name, instance and class, size and place; with a class or name filter (or `detail`), each one's depth (`argb`), visual, map state, override-redirect, types, states, role and own opacity |
| `xorg_x_log` | r | the X server's log for the running session: version, start, errors (and warnings) |
All answers are structured. Each tool that changes the running server says how long the change lasts.
@@ -90,7 +91,22 @@ The bus it hands on is the user manager's (`unix:path=/run/user/<uid>/bus`). Wit
answers `{"error": "no-graphical-session", "reason": …, "looked": […]}`.
The screenshot needs no screenshot program. `xwd` dumps the screen, and the module turns the dump
into a PNG itself.
into a PNG itself. **A window is cut from the dump of the whole screen** at the place `xwininfo` gives,
never dumped by its id: `xwd -id` asks the server for the window's colormap entries, which a window of
a 32-bit (alpha) visual refused with `BadColor (X_QueryColors)` on a workstation (2026-10-09), and the
cut shows the window as the compositor shows it, which is what a question about a window that looks
wrong needs. A window under another one shows what covers it, and a window that is not viewable (unmapped, or on a
workspace not shown) is refused rather than photographed as whatever is at its place. `inline` answers
the image itself, for a session that cannot read the machine's files: a tool's answer crosses the bus,
whose largest message is NATS's default of 1 MiB (the `nats` module sets no other), so the PNG is held to
700 KiB (about 935 KiB as base64) and halved in size until it fits.
`xorg_windows` exists because a window manager lists only the windows it manages, and says nothing a
compositor goes by. A window of depth 32 has an alpha channel: the compositor blends it with what is
behind it, so one whose program drew nothing looks transparent rather than black. Its details take
two commands per window (`xwininfo -all`, and `xprop` for the role and the window's own opacity), so
they are read for at most 40 windows. `xprop` is the `picom` module's package; without it the role and
opacity are left out.
## What it improves
+21 -2
View File
@@ -143,16 +143,35 @@ func tools(d desktop.Desk) []stdio.Tool {
},
{
Name: "xorg_screenshot",
Description: "Take a screenshot to a PNG file: the whole screen, one output, or one window by its X id. " +
Description: "Take a screenshot to a PNG file: the whole screen, one output, or one window by its X id " +
"(cut from the screen as the compositor shows it, so a see-through window shows what is behind it). " +
"Written under the account's home (default ~/Pictures/Screenshots/screenshot-<time>.png). Answers " +
"the path, size and bytes, never the image.",
"the path, size and bytes; with inline, also the PNG itself as base64 (at most 700 KiB, halved in size until it " +
"fits; inline_scale says so). A window that is not viewable (unmapped, or on a workspace not shown) is refused.",
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)"),
"inline": desktop.Flag("also answer the PNG as base64, at most 700 KiB, for a session that cannot read the file (default false)"),
}),
Run: call(x.screenshot),
},
{
Name: "xorg_windows",
Description: "The windows on the X screen as the server knows them, managed or not (dialogs, tooltips, " +
"windows no window manager lists): id, name, instance and class, size and absolute place, level in " +
"the tree. With class or name (regular expressions), or detail, also each window's depth (argb: a " +
"32-bit visual a compositor blends with what is behind it), visual class, map state, override-redirect, " +
"window types and states, role and own opacity — for at most 40 windows. Without a filter, only " +
"windows with a class, unless all.",
Input: desktop.Schema(map[string]any{
"class": desktop.Str("only windows whose class or instance matches this regular expression"),
"name": desktop.Str("only windows whose name (title) matches this regular expression"),
"detail": desktop.Flag("read each window's details (default false; true when class or name is given)"),
"all": desktop.Flag("without a filter, also windows without a class (default false)"),
}),
Run: call(x.windows),
},
{
Name: "xorg_x_log",
Description: "The X server's log for the running session (or the newest one): its version, when it " +
+1 -1
View File
@@ -111,7 +111,7 @@ func TestItInstallsTheServerItsStartAndTheToolsItsVerbsRun(t *testing.T) {
}
}
sort.Strings(got)
want := "autorandr,xorg-server,xorg-setxkbmap,xorg-xinit,xorg-xinput,xorg-xrandr,xorg-xrdb,xorg-xset,xorg-xwd"
want := "autorandr,xorg-server,xorg-setxkbmap,xorg-xinit,xorg-xinput,xorg-xrandr,xorg-xrdb,xorg-xset,xorg-xwd,xorg-xwininfo"
if strings.Join(got, ",") != want {
t.Fatalf("packages: %v", got)
}
+79 -6
View File
@@ -3,8 +3,10 @@ package main
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"os"
@@ -494,7 +496,34 @@ func (x xorg) keyboard(ctx context.Context, a desktop.Args) (any, error) {
var windowID = regexp.MustCompile(`^(0x[0-9a-fA-F]{1,8}|[0-9]{1,10})$`)
// mostInline is the largest PNG a screenshot carries in its answer. A tool's answer crosses the bus,
// whose largest message is 1 MiB; 700 KiB is about 935 KiB as base64, which leaves room for the rest.
const mostInline = 700 << 10
// shrink averages each by×by block of an image into one pixel.
func shrink(img image.Image, by int) *image.RGBA {
b := img.Bounds()
out := image.NewRGBA(image.Rect(0, 0, b.Dx()/by, b.Dy()/by))
for y := 0; y < out.Bounds().Dy(); y++ {
for x := 0; x < out.Bounds().Dx(); x++ {
var r, g, bl, n uint32
for dy := 0; dy < by; dy++ {
for dx := 0; dx < by; dx++ {
cr, cg, cb, _ := img.At(b.Min.X+x*by+dx, b.Min.Y+y*by+dy).RGBA()
r, g, bl, n = r+cr, g+cg, bl+cb, n+1
}
}
out.SetRGBA(x, y, color.RGBA{uint8(r / n >> 8), uint8(g / n >> 8), uint8(bl / n >> 8), 0xff})
}
}
return out
}
func (x xorg) screenshot(ctx context.Context, a desktop.Args) (any, error) {
inline, _, err := a.Bool("inline")
if err != nil {
return nil, err
}
stamp := time.Now().Format("20060102-150405")
path, err := desktop.InHome(a.Opt("path", "~/Pictures/Screenshots/screenshot-"+stamp+".png"))
if err != nil {
@@ -514,15 +543,28 @@ func (x xorg) screenshot(ctx context.Context, a desktop.Args) (any, error) {
}
dump.Close()
defer os.Remove(dump.Name())
args := []string{"-silent", "-out", dump.Name()}
// A window is cut from the whole screen, not dumped by its id: what the screen shows is what the
// compositor made of it (a window with an alpha channel is blended with what is behind it), and xwd
// asks the server for a window's colormap entries, which a window of a 32-bit visual can refuse
// with BadColor (X_QueryColors). The cut is the window's frame-less area at its absolute place.
var cut *image4
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")
g, err := x.windowGeometry(ctx, env, w)
if err != nil {
return nil, err
}
// A window that is not viewable is not on the screen: its rectangle would show what is there
// instead, which is a picture of the wrong thing.
if g.Mapped != "" && g.Mapped != "IsViewable" {
return nil, fmt.Errorf("window %s is %s, not on the screen (IsUnMapped: not shown; IsUnviewable: "+
"its parent is not shown, e.g. on another workspace): bring it to the screen first", w, g.Mapped)
}
cut = &g
}
args := []string{"-silent", "-out", dump.Name(), "-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")
@@ -538,7 +580,15 @@ func (x xorg) screenshot(ctx context.Context, a desktop.Args) (any, error) {
return nil, fmt.Errorf("the dump xwd wrote: %w", err)
}
var shot image.Image = img
if name := a.Opt("output", ""); name != "" {
if cut != nil {
r := image.Rect(cut.X, cut.Y, cut.X+cut.W, cut.Y+cut.H).Intersect(img.Bounds())
if r.Empty() {
return nil, fmt.Errorf("the window is not on the screen (%dx%d at %d,%d)", cut.W, cut.H, cut.X, cut.Y)
}
crop := image.NewRGBA(image.Rect(0, 0, r.Dx(), r.Dy()))
draw.Draw(crop, crop.Bounds(), img, r.Min, draw.Src)
shot = crop
} else if name := a.Opt("output", ""); name != "" {
_, outs, _, err := x.query(ctx)
if err != nil {
return nil, err
@@ -568,7 +618,30 @@ func (x xorg) screenshot(ctx context.Context, a desktop.Args) (any, error) {
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
answer := map[string]any{"session": s.Display, "path": path, "width": b.Dx(), "height": b.Dy(), "bytes": buf.Len()}
if inline {
// For a session that cannot read the machine's files: the image itself, within the bus's bound.
// An image larger than mostInline is halved until it fits, and the answer says by how much; the
// file keeps the full size.
small, scale := buf.Bytes(), 1
for len(small) > mostInline && shot.Bounds().Dx()/(scale*2) >= 64 {
scale *= 2
var b bytes.Buffer
if err := png.Encode(&b, shrink(shot, scale)); err != nil {
return nil, err
}
small = b.Bytes()
}
if len(small) > mostInline {
answer["inline"] = fmt.Sprintf("not included: %d bytes even at 1/%d; take one output or one window", len(small), scale)
} else {
answer["png_base64"] = base64.StdEncoding.EncodeToString(small)
if scale > 1 {
answer["inline_scale"] = fmt.Sprintf("1/%d of the file's size, to fit the bus", scale)
}
}
}
return answer, nil
}
func (x xorg) xlog(ctx context.Context, a desktop.Args) (any, error) {
+252
View File
@@ -0,0 +1,252 @@
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
}
+222
View File
@@ -0,0 +1,222 @@
package main
import (
"bytes"
"context"
"encoding/base64"
"image/color"
"image/png"
"os"
"strings"
"testing"
"xorg/internal/desktop"
)
// The tree and the details as xwininfo printed them on a workstation (2026-10-09), with a calendar
// reminder of the mail client open: a dialog of a 32-bit visual.
const treeText = `
xwininfo: Window id: 0x3e6 (the root window) "i3"
Root window id: 0x3e6 (the root window) "i3"
Parent window id: 0x0 (none)
160 children:
0x1800002 "Dunst": ("Dunst" "Dunst") 259x39+2291+50 +2291+50
0x2a00e04 "Betterbird": ("eu.betterbird.Betterbird" "eu.betterbird.Betterbird") 1x1+0+0 +0+0
2 children:
0x2a00e08 (has no name): () 1x1+-1+-1 +-1+-1
0x600090 "[i3 con] floating_con around 0x5594": () 602x325+979+556 +979+556
1 child:
0x2a00221 "1 Reminder": ("Calendar" "eu.betterbird.Betterbird") 600x300+1+24 +980+581
0x2a00222 "say "hi": there": ("Mail" "eu.betterbird.Betterbird") 10x10+-100+-100 +-100+-100
`
const detailText = `
xwininfo: Window id: 0x2a00221 "1 Reminder"
Absolute upper-left X: 980
Absolute upper-left Y: 581
Width: 600
Height: 300
Depth: 32
Visual: 0x87
Visual Class: TrueColor
Map State: IsViewable
Override Redirect State: no
Someone wants these events:
KeyPress
Window manager hints:
Client accepts input or input focus: Yes
Window type:
Dialog
Window state:
Focused
Process id: 37147 on host h
Frame extents: 0, 0, 0, 0
Normal window size hints:
`
func TestTheTreeIsReadWithClassesPlacesAndLevels(t *testing.T) {
got := ParseTree(treeText)
if len(got) != 6 {
t.Fatalf("%d windows: %+v", len(got), got)
}
r := got[4]
if r.ID != "0x2a00221" || r.Name != "1 Reminder" || r.Instance != "Calendar" || r.Class != "eu.betterbird.Betterbird" ||
r.Width != 600 || r.Height != 300 || r.X != 980 || r.Y != 581 || r.Level != 2 {
t.Fatalf("%+v", r)
}
if got[2].ID != "0x2a00e08" || got[2].Class != "" || got[2].X != -1 || got[2].Level != 2 {
t.Fatalf("a nameless child at a negative place: %+v", got[2])
}
if got[5].Name != `say "hi": there` || got[5].X != -100 || got[5].Level != 1 {
t.Fatalf("a name with quotes and a colon: %+v", got[5])
}
}
func TestTheDetailsSayDepthTypeStateRoleAndOpacity(t *testing.T) {
w := Window{ID: "0x2a00221"}
ParseDetails(&w, detailText)
ParseWindowProps(&w, "WM_WINDOW_ROLE(STRING) = \"AlarmWindow\"\n_NET_WM_WINDOW_OPACITY(CARDINAL) = 2147483647\n")
if w.Depth != 32 || w.ARGB == nil || !*w.ARGB || w.Visual != "TrueColor" || w.Mapped != "IsViewable" ||
w.OverrideRedir == nil || *w.OverrideRedir || strings.Join(w.Types, ",") != "Dialog" ||
strings.Join(w.States, ",") != "Focused" || w.Role != "AlarmWindow" || w.Opacity == nil || *w.Opacity != 0.5 {
t.Fatalf("%+v", w)
}
plain := Window{}
ParseWindowProps(&plain, "WM_WINDOW_ROLE: not found.\n_NET_WM_WINDOW_OPACITY: not found.\n")
if plain.Opacity != nil || plain.Role != "" {
t.Fatalf("no opacity of its own is none, not 0: %+v", plain)
}
}
func TestWindowsFiltersByClassAndReadsTheDetailsOfWhatItShows(t *testing.T) {
f := &fakeDesk{answers: map[string]desktop.Result{
"xwininfo -root -tree": {Stdout: treeText},
"xwininfo -id 0x2a00e04 -all": {Stdout: " Depth: 32\n Map State: IsUnMapped\n"},
"xwininfo -id 0x2a00221 -all": {Stdout: detailText},
"xwininfo -id 0x2a00222 -all": {Code: 1, Stderr: "X Error: BadWindow"},
"xprop -id 0x2a00221 WM_WINDOW_ROLE _NET_WM_WINDOW_OPACITY": {Stdout: "WM_WINDOW_ROLE(STRING) = \"AlarmWindow\"\n"},
}}
got, err := byName(t, f.desk(true), "xorg_windows")(map[string]any{"class": "(?i)betterbird"})
if err != nil {
t.Fatal(err)
}
ws := got.(map[string]any)["windows"].([]Window)
if len(ws) != 3 || ws[1].Role != "AlarmWindow" || ws[1].Depth != 32 || ws[2].DetailProblems == "" {
t.Fatalf("%+v", ws)
}
f2 := &fakeDesk{answers: map[string]desktop.Result{"xwininfo -root -tree": {Stdout: treeText}}}
got, err = byName(t, f2.desk(true), "xorg_windows")(map[string]any{})
if err != nil {
t.Fatal(err)
}
if n := got.(map[string]any)["count"]; n != 4 || len(f2.ran) != 1 {
t.Fatalf("without a filter: the windows with a class, and no details read: %v %v", n, f2.ran)
}
}
func TestAWindowsScreenshotIsCutFromTheScreenNotDumpedByItsID(t *testing.T) {
// The fake xwd writes a 4x3 screen whose pixel (x, y) is (x*10, y*10, 7).
home := t.TempDir()
t.Setenv("MESH_OPERATOR_HOME", home)
t.Setenv("HOME", home)
f := &fakeDesk{answers: map[string]desktop.Result{
"xwininfo -id 0x2a00221": {Stdout: " Absolute upper-left X: 1\n Absolute upper-left Y: 1\n Width: 2\n Height: 2\n"},
}}
d := f.desk(true)
run := d.Run
d.Run = func(ctx context.Context, env []string, stdin []byte, name string, args ...string) desktop.Result {
if name == "xwd" {
for i, a := range args {
if a == "-id" {
t.Fatal("xwd is never asked for one window")
}
if a == "-out" {
os.WriteFile(args[i+1], xwdOf(4, 3, true, func(x, y int) (uint8, uint8, uint8) { return uint8(x * 10), uint8(y * 10), 7 }), 0o644)
}
}
return desktop.Result{}
}
return run(ctx, env, stdin, name, args...)
}
got, err := byName(t, d, "xorg_screenshot")(map[string]any{"window": "0x2a00221", "path": "~/shot.png", "inline": true})
if err != nil {
t.Fatal(err)
}
a := got.(map[string]any)
if a["width"] != 2 || a["height"] != 2 {
t.Fatalf("%v", a)
}
raw, err := base64.StdEncoding.DecodeString(a["png_base64"].(string))
if err != nil {
t.Fatal(err)
}
img, err := png.Decode(bytes.NewReader(raw))
if err != nil {
t.Fatal(err)
}
if c := color.RGBAModel.Convert(img.At(0, 0)).(color.RGBA); c != (color.RGBA{10, 10, 7, 0xff}) {
t.Fatalf("the cut starts at the window's corner: %v", c)
}
if _, err := ParseGeometry("Width: 0\n"); err == nil {
t.Fatal("no geometry is refused")
}
}
// screenFake is a desk whose xwd writes a w×h screen of noise and whose xwininfo
// answers one window with the given map state.
func screenFake(t *testing.T, w, h int, state string) desktop.Desk {
f := &fakeDesk{answers: map[string]desktop.Result{
"xwininfo -id 0x2a00221": {Stdout: " Absolute upper-left X: 1\n Absolute upper-left Y: 1\n Width: 2\n Height: 2\n Map State: " + state + "\n"},
}}
d := f.desk(true)
run := d.Run
d.Run = func(ctx context.Context, env []string, stdin []byte, name string, args ...string) desktop.Result {
if name == "xwd" {
for i, a := range args {
if a == "-out" {
// Noise, which a PNG cannot compress: the worst case for the bound.
os.WriteFile(args[i+1], xwdOf(w, h, true, func(x, y int) (uint8, uint8, uint8) {
n := uint32(x)*2654435761 ^ uint32(y)*2246822519
n ^= n >> 13
n *= 3266489917
return uint8(n), uint8(n >> 8), uint8(n >> 16)
}), 0o644)
}
}
return desktop.Result{}
}
return run(ctx, env, stdin, name, args...)
}
return d
}
func TestAWindowThatIsNotViewableIsNotPhotographed(t *testing.T) {
home := t.TempDir()
t.Setenv("MESH_OPERATOR_HOME", home)
for _, state := range []string{"IsUnMapped", "IsUnviewable"} {
_, err := byName(t, screenFake(t, 4, 3, state), "xorg_screenshot")(map[string]any{"window": "0x2a00221", "path": "~/s.png"})
if err == nil || !strings.Contains(err.Error(), state) {
t.Errorf("%s: %v", state, err)
}
}
}
func TestAnInlineImageFitsTheBusAndIsScaledDownWhenItWouldNot(t *testing.T) {
home := t.TempDir()
t.Setenv("MESH_OPERATOR_HOME", home)
got, err := byName(t, screenFake(t, 800, 600, "IsViewable"), "xorg_screenshot")(map[string]any{"path": "~/s.png", "inline": true})
if err != nil {
t.Fatal(err)
}
a := got.(map[string]any)
b64, _ := a["png_base64"].(string)
if b64 == "" || len(b64) > 1<<20-64<<10 {
t.Fatalf("an inline image within the bus's 1 MiB, with room for the answer: %d bytes; %v", len(b64), a["inline"])
}
if a["bytes"].(int) > mostInline && a["inline_scale"] == nil {
t.Fatalf("a file larger than the bound is answered scaled down, and says so: %v", a["inline_scale"])
}
}
+1 -1
View File
@@ -298,7 +298,7 @@ func TestEveryToolThatNeedsTheScreenSaysPlainlyThatThereIsNoSession(t *testing.T
{"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},
{"xorg_screenshot", nil}, {"xorg_windows", nil},
} {
_, err := byName(t, d, call.tool)(orEmpty(call.args))
if !desktop.IsNoSession(err) {
+6
View File
@@ -29,6 +29,7 @@
"xorg_input_set",
"xorg_keyboard",
"xorg_screenshot",
"xorg_windows",
"xorg_x_log"
],
"resources": [
@@ -72,6 +73,11 @@
"type": "package",
"package": "xorg-xwd"
},
{
"id": "package-xorg-xwininfo",
"type": "package",
"package": "xorg-xwininfo"
},
{
"id": "package-autorandr",
"type": "package",