xorg: refuse a window that is not on the screen, and keep an inline screenshot within the bus (review of #154)
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

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.
This commit is contained in:
2026-10-09 12:23:51 +02:00
parent 52a4827983
commit 4fe8958dda
5 changed files with 120 additions and 13 deletions
+47 -6
View File
@@ -6,6 +6,7 @@ import (
"encoding/base64"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"os"
@@ -495,8 +496,28 @@ 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.
const mostInline = 4 << 20
// 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")
@@ -535,6 +556,12 @@ func (x xorg) screenshot(ctx context.Context, a desktop.Args) (any, error) {
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"}
@@ -593,11 +620,25 @@ func (x xorg) screenshot(ctx context.Context, a desktop.Args) (any, error) {
b := shot.Bounds()
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 a bound.
if buf.Len() > mostInline {
answer["inline"] = fmt.Sprintf("not included: %d bytes is more than %d; take one output or one window", buf.Len(), mostInline)
// 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(buf.Bytes())
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