Dotfiles_ArchAP/tools/wlthumbs/wayland.go

333 lines
10 KiB
Go

package main
import (
"fmt"
"log"
"strings"
"github.com/mil-ad/wlthumbs/proto"
"github.com/rajveermalviya/go-wayland/wayland/client"
"golang.org/x/sys/unix"
)
type toplevel struct {
handle *proto.ExtForeignToplevelHandle
id, title, appID string
closed, done bool
}
type output struct {
wl *client.Output
name string
make_, model string
done bool
}
// source is a normalized capture target (window or monitor). captureOne fills
// the trailing fields with the captured frame.
type source struct {
typ string // "window" | "output"
id string
app string
title string
cap *proto.ExtImageCaptureSource
raw []byte // captured pixels (nil until/unless capture succeeds)
w, h uint32
transform uint32
swapRB bool
thumb string // path to the written PNG (empty if capture/encode failed)
}
// app holds the Wayland connection and the globals/objects we bind on it.
type app struct {
display *client.Display
registry *client.Registry
shm *client.Shm
copyMgr *proto.ExtImageCopyCaptureManager
toplevelSrcMgr *proto.ExtForeignToplevelImageCaptureSourceManager
outputSrcMgr *proto.ExtOutputImageCaptureSourceManager
toplevelList *proto.ExtForeignToplevelList
toplevels []*toplevel
outputs []*output
}
// connect opens the Wayland connection, binds the globals we need, and verifies
// the compositor supports the required capture protocols.
func (a *app) connect() error {
display, err := client.Connect("")
if err != nil {
return fmt.Errorf("connect to wayland: %w", err)
}
a.display = display
display.SetErrorHandler(func(e client.DisplayErrorEvent) {
log.Printf("display error: object=%d code=%d %q", e.ObjectId, e.Code, e.Message)
})
registry, err := display.GetRegistry()
if err != nil {
return fmt.Errorf("get registry: %w", err)
}
a.registry = registry
registry.SetGlobalHandler(a.handleGlobal)
a.roundtrip() // register globals
a.roundtrip() // deliver their initial events
var missing []string
if a.shm == nil {
missing = append(missing, "wl_shm")
}
if a.copyMgr == nil {
missing = append(missing, "ext_image_copy_capture_manager_v1")
}
if a.toplevelSrcMgr == nil {
missing = append(missing, "ext_foreign_toplevel_image_capture_source_manager_v1")
}
if len(missing) > 0 {
return fmt.Errorf("compositor is missing required protocols: %s", strings.Join(missing, ", "))
}
return nil
}
func (a *app) ctx() *client.Context { return a.display.Context() }
// bind is a spec-correct wl_registry.bind. go-wayland's Registry.Bind writes the
// string-length prefix as the *padded* length instead of len+1; older libwayland
// tolerated it, but newer libwayland rejects it with "invalid arguments". The
// proxy is already registered by its New* constructor, so we only marshal and
// send the request here.
func (a *app) bind(e client.RegistryGlobalEvent, p client.Proxy) {
iface := e.Interface
strLen := len(iface) + 1 // includes the null terminator
pad := strLen
if pad%4 != 0 {
pad += 4 - pad%4
}
const opcode = 0
bufLen := 8 + 4 + (4 + pad) + 4 + 4
buf := make([]byte, bufLen)
client.PutUint32(buf[0:4], a.registry.ID())
client.PutUint32(buf[4:8], uint32(bufLen<<16|opcode))
client.PutUint32(buf[8:12], e.Name)
client.PutString(buf[12:12+4+pad], iface, strLen)
o := 12 + 4 + pad
client.PutUint32(buf[o:o+4], e.Version)
client.PutUint32(buf[o+4:o+8], p.ID())
if err := a.ctx().WriteMsg(buf, nil); err != nil {
log.Fatalf("bind %s: %v", iface, err)
}
}
func (a *app) handleGlobal(e client.RegistryGlobalEvent) {
switch e.Interface {
case "wl_shm":
a.shm = client.NewShm(a.ctx())
a.bind(e, a.shm)
case "ext_image_copy_capture_manager_v1":
a.copyMgr = proto.NewExtImageCopyCaptureManager(a.ctx())
a.bind(e, a.copyMgr)
case "ext_foreign_toplevel_image_capture_source_manager_v1":
a.toplevelSrcMgr = proto.NewExtForeignToplevelImageCaptureSourceManager(a.ctx())
a.bind(e, a.toplevelSrcMgr)
case "ext_output_image_capture_source_manager_v1":
a.outputSrcMgr = proto.NewExtOutputImageCaptureSourceManager(a.ctx())
a.bind(e, a.outputSrcMgr)
case "ext_foreign_toplevel_list_v1":
a.toplevelList = proto.NewExtForeignToplevelList(a.ctx())
a.bind(e, a.toplevelList)
a.toplevelList.SetToplevelHandler(a.handleToplevel)
case "wl_output":
o := &output{wl: client.NewOutput(a.ctx())}
a.bind(e, o.wl)
a.outputs = append(a.outputs, o)
o.wl.SetGeometryHandler(func(e client.OutputGeometryEvent) { o.make_, o.model = e.Make, e.Model })
o.wl.SetNameHandler(func(e client.OutputNameEvent) { o.name = e.Name })
o.wl.SetDoneHandler(func(client.OutputDoneEvent) { o.done = true })
}
}
func (a *app) handleToplevel(e proto.ExtForeignToplevelListToplevelEvent) {
t := &toplevel{handle: e.Toplevel}
a.toplevels = append(a.toplevels, t)
t.handle.SetIdentifierHandler(func(e proto.ExtForeignToplevelHandleIdentifierEvent) { t.id = e.Identifier })
t.handle.SetTitleHandler(func(e proto.ExtForeignToplevelHandleTitleEvent) { t.title = e.Title })
t.handle.SetAppIdHandler(func(e proto.ExtForeignToplevelHandleAppIdEvent) { t.appID = e.AppId })
t.handle.SetClosedHandler(func(proto.ExtForeignToplevelHandleClosedEvent) { t.closed = true })
t.handle.SetDoneHandler(func(proto.ExtForeignToplevelHandleDoneEvent) { t.done = true })
}
// enumerate collects toplevels and outputs, round-tripping until each has
// delivered its full initial state (a "done" event) and no new ones appear.
func (a *app) enumerate() {
const maxRounds = 20 // safety bound; converges in 1-2 in practice
for range maxRounds {
before := len(a.toplevels)
a.roundtrip()
stable := len(a.toplevels) == before
allDone := true
for _, t := range a.toplevels {
if !t.done && !t.closed {
allDone = false
}
}
for _, o := range a.outputs {
if !o.done {
allDone = false
}
}
if stable && allDone {
return
}
}
}
// roundtrip flushes the connection and dispatches events until the compositor
// answers a sync callback, i.e. until everything queued so far is processed.
func (a *app) roundtrip() {
cb, err := a.display.Sync()
if err != nil {
log.Fatalf("sync: %v", err)
}
defer cb.Destroy()
done := false
cb.SetDoneHandler(func(client.CallbackDoneEvent) { done = true })
for !done {
if err := a.dispatch(); err != nil {
log.Fatalf("dispatch: %v", err)
}
}
}
// dispatch processes one event, tolerating events addressed to objects we have
// already destroyed. The compositor may have queued events for a session/frame
// before it saw our destroy request; go-wayland reports those as "unable find
// sender" but has still consumed the message, so it is safe to continue. A real
// connection failure (EOF, broken pipe) is returned so callers can abort.
func (a *app) dispatch() error {
err := a.ctx().Dispatch()
if err != nil && strings.Contains(err.Error(), "unable find sender") {
return nil
}
return err
}
// captureOne grabs a single frame for one source into an shm buffer and stores
// the raw pixels (and dimensions/transform/format) on the source. It returns
// false if the compositor could not satisfy the capture. All Wayland and mmap
// resources for this source are released before returning.
func (a *app) captureOne(s *source) bool {
session, err := a.copyMgr.CreateSession(s.cap, 0)
if err != nil {
log.Printf("create session %q: %v", s.id, err)
return false
}
defer session.Destroy()
defer s.cap.Destroy()
var w, h uint32
var formats []uint32
sessionDone := false
session.SetBufferSizeHandler(func(e proto.ExtImageCopyCaptureSessionBufferSizeEvent) { w, h = e.Width, e.Height })
session.SetShmFormatHandler(func(e proto.ExtImageCopyCaptureSessionShmFormatEvent) { formats = append(formats, e.Format) })
session.SetDoneHandler(func(proto.ExtImageCopyCaptureSessionDoneEvent) { sessionDone = true })
session.SetStoppedHandler(func(proto.ExtImageCopyCaptureSessionStoppedEvent) { sessionDone = true })
a.roundtrip()
if !sessionDone || w == 0 || h == 0 {
log.Printf("no buffer constraints for %q", s.id)
return false
}
format, swapRB, ok := chooseFormat(formats)
if !ok {
log.Printf("no supported shm format for %q (got %v)", s.id, formats)
return false
}
stride := int(w) * 4
size := stride * int(h)
fd, err := shmFile(size)
if err != nil {
log.Printf("shm file for %q: %v", s.id, err)
return false
}
defer unix.Close(fd)
data, err := unix.Mmap(fd, 0, size, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
if err != nil {
log.Printf("mmap for %q: %v", s.id, err)
return false
}
defer unix.Munmap(data)
pool, err := a.shm.CreatePool(fd, int32(size))
if err != nil {
log.Printf("create pool for %q: %v", s.id, err)
return false
}
defer pool.Destroy()
buf, err := pool.CreateBuffer(0, int32(w), int32(h), int32(stride), uint32(format))
if err != nil {
log.Printf("create buffer for %q: %v", s.id, err)
return false
}
defer buf.Destroy()
frame, err := session.CreateFrame()
if err != nil {
log.Printf("create frame for %q: %v", s.id, err)
return false
}
defer frame.Destroy()
var tf uint32
ready, failed := false, false
frame.SetTransformHandler(func(e proto.ExtImageCopyCaptureFrameTransformEvent) { tf = e.Transform })
frame.SetReadyHandler(func(proto.ExtImageCopyCaptureFrameReadyEvent) { ready = true })
frame.SetFailedHandler(func(e proto.ExtImageCopyCaptureFrameFailedEvent) {
failed = true
log.Printf("capture failed for %q (reason=%d)", s.id, e.Reason)
})
if err := frame.AttachBuffer(buf); err != nil {
log.Printf("attach buffer for %q: %v", s.id, err)
return false
}
frame.DamageBuffer(0, 0, int32(w), int32(h))
if err := frame.Capture(); err != nil {
log.Printf("capture for %q: %v", s.id, err)
return false
}
for !ready && !failed {
if err := a.dispatch(); err != nil {
log.Printf("dispatch during capture %q: %v", s.id, err)
return false
}
}
if failed {
return false
}
// Copy the pixels out of the mapping before the deferred Munmap runs.
s.raw = make([]byte, size)
copy(s.raw, data)
s.w, s.h, s.transform, s.swapRB = w, h, tf, swapRB
return true
}
// shmFile returns an fd backed by an anonymous in-memory file of the given size.
func shmFile(size int) (int, error) {
fd, err := unix.MemfdCreate("wlthumbs", unix.MFD_CLOEXEC)
if err != nil {
return -1, fmt.Errorf("memfd_create: %w", err)
}
if err := unix.Ftruncate(fd, int64(size)); err != nil {
unix.Close(fd)
return -1, fmt.Errorf("ftruncate: %w", err)
}
return fd, nil
}