summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/screenshot.go
diff options
context:
space:
mode:
authorAlexanderCurl <alexc@alexc.hu>2026-04-18 17:46:06 +0100
committerAlexanderCurl <alexc@alexc.hu>2026-04-18 17:46:06 +0100
commit70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch)
treeab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/screenshot.go
parent5d94c0a7d44a2255b81815a52a7056a94a39842d (diff)
downloadRaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.tar.gz
RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.zip
Replaced file structures for themes in the correct format
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/screenshot.go')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/screenshot.go1089
1 files changed, 1089 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/screenshot.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/screenshot.go
new file mode 100644
index 0000000..42d3404
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/screenshot.go
@@ -0,0 +1,1089 @@
+package screenshot
+
+import (
+ "fmt"
+ "math"
+ "sync"
+
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
+ wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
+ "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
+)
+
+type WaylandOutput struct {
+ wlOutput *client.Output
+ globalName uint32
+ name string
+ x, y int32
+ width int32
+ height int32
+ scale int32
+ fractionalScale float64
+ transform int32
+}
+
+type CaptureResult struct {
+ Buffer *ShmBuffer
+ Region Region
+ YInverted bool
+ Format uint32
+}
+
+type Screenshoter struct {
+ config Config
+
+ display *client.Display
+ registry *client.Registry
+ ctx *client.Context
+
+ compositor *client.Compositor
+ shm *client.Shm
+ screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
+
+ outputs map[uint32]*WaylandOutput
+ outputsMu sync.Mutex
+}
+
+func New(config Config) *Screenshoter {
+ return &Screenshoter{
+ config: config,
+ outputs: make(map[uint32]*WaylandOutput),
+ }
+}
+
+func (s *Screenshoter) Run() (*CaptureResult, error) {
+ if err := s.connect(); err != nil {
+ return nil, fmt.Errorf("wayland connect: %w", err)
+ }
+ defer s.cleanup()
+
+ if err := s.setupRegistry(); err != nil {
+ return nil, fmt.Errorf("registry setup: %w", err)
+ }
+
+ if err := s.roundtrip(); err != nil {
+ return nil, fmt.Errorf("roundtrip: %w", err)
+ }
+
+ if s.screencopy == nil {
+ return nil, fmt.Errorf("compositor does not support wlr-screencopy-unstable-v1")
+ }
+
+ if err := s.roundtrip(); err != nil {
+ return nil, fmt.Errorf("roundtrip: %w", err)
+ }
+
+ switch s.config.Mode {
+ case ModeLastRegion:
+ return s.captureLastRegion()
+ case ModeRegion:
+ return s.captureRegion()
+ case ModeWindow:
+ return s.captureWindow()
+ case ModeOutput:
+ return s.captureOutput(s.config.OutputName)
+ case ModeFullScreen:
+ return s.captureFullScreen()
+ case ModeAllScreens:
+ return s.captureAllScreens()
+ default:
+ return s.captureRegion()
+ }
+}
+
+func (s *Screenshoter) captureLastRegion() (*CaptureResult, error) {
+ lastRegion := GetLastRegion()
+ if lastRegion.IsEmpty() {
+ return s.captureRegion()
+ }
+
+ output := s.findOutputForRegion(lastRegion)
+ if output == nil {
+ return s.captureRegion()
+ }
+
+ return s.captureRegionOnOutput(output, lastRegion)
+}
+
+func (s *Screenshoter) captureRegion() (*CaptureResult, error) {
+ if s.config.Reset {
+ if err := SaveLastRegion(Region{}); err != nil {
+ log.Debug("failed to reset last region", "err", err)
+ }
+ }
+
+ selector := NewRegionSelector(s)
+ result, cancelled, err := selector.Run()
+ if err != nil {
+ return nil, fmt.Errorf("region selection: %w", err)
+ }
+ if cancelled || result == nil {
+ return nil, nil
+ }
+
+ if err := SaveLastRegion(result.Region); err != nil {
+ log.Debug("failed to save last region", "err", err)
+ }
+
+ return result, nil
+}
+
+func (s *Screenshoter) captureWindow() (*CaptureResult, error) {
+ geom, err := GetActiveWindow()
+ if err != nil {
+ return nil, err
+ }
+
+ region := Region{
+ X: geom.X,
+ Y: geom.Y,
+ Width: geom.Width,
+ Height: geom.Height,
+ }
+
+ var output *WaylandOutput
+ if geom.Output != "" {
+ output = s.findOutputByName(geom.Output)
+ }
+ if output == nil {
+ output = s.findOutputForRegion(region)
+ }
+ if output == nil {
+ return nil, fmt.Errorf("could not find output for window")
+ }
+
+ switch DetectCompositor() {
+ case CompositorHyprland:
+ return s.captureAndCrop(output, region)
+ case CompositorDWL:
+ return s.captureDWLWindow(output, region, geom)
+ default:
+ return s.captureRegionOnOutput(output, region)
+ }
+}
+
+func (s *Screenshoter) captureDWLWindow(output *WaylandOutput, region Region, geom *WindowGeometry) (*CaptureResult, error) {
+ result, err := s.captureWholeOutput(output)
+ if err != nil {
+ return nil, err
+ }
+
+ scale := geom.Scale
+ if scale <= 0 || scale == 1.0 {
+ if output.fractionalScale > 1.0 {
+ scale = output.fractionalScale
+ }
+ }
+ if scale <= 0 {
+ scale = 1.0
+ }
+
+ localX := int(float64(region.X-geom.OutputX) * scale)
+ localY := int(float64(region.Y-geom.OutputY) * scale)
+ w := int(float64(region.Width) * scale)
+ h := int(float64(region.Height) * scale)
+
+ if localX < 0 {
+ w += localX
+ localX = 0
+ }
+ if localY < 0 {
+ h += localY
+ localY = 0
+ }
+ if localX+w > result.Buffer.Width {
+ w = result.Buffer.Width - localX
+ }
+ if localY+h > result.Buffer.Height {
+ h = result.Buffer.Height - localY
+ }
+
+ if w <= 0 || h <= 0 {
+ result.Buffer.Close()
+ return nil, fmt.Errorf("window not visible on output")
+ }
+
+ cropped, err := CreateShmBuffer(w, h, w*4)
+ if err != nil {
+ result.Buffer.Close()
+ return nil, fmt.Errorf("create crop buffer: %w", err)
+ }
+
+ srcData := result.Buffer.Data()
+ dstData := cropped.Data()
+
+ for y := 0; y < h; y++ {
+ srcY := localY + y
+ if result.YInverted {
+ srcY = result.Buffer.Height - 1 - (localY + y)
+ }
+ if srcY < 0 || srcY >= result.Buffer.Height {
+ continue
+ }
+
+ dstY := y
+ if result.YInverted {
+ dstY = h - 1 - y
+ }
+
+ for x := 0; x < w; x++ {
+ srcX := localX + x
+ if srcX < 0 || srcX >= result.Buffer.Width {
+ continue
+ }
+
+ si := srcY*result.Buffer.Stride + srcX*4
+ di := dstY*cropped.Stride + x*4
+
+ if si+3 >= len(srcData) || di+3 >= len(dstData) {
+ continue
+ }
+
+ dstData[di+0] = srcData[si+0]
+ dstData[di+1] = srcData[si+1]
+ dstData[di+2] = srcData[si+2]
+ dstData[di+3] = srcData[si+3]
+ }
+ }
+
+ result.Buffer.Close()
+ cropped.Format = PixelFormat(result.Format)
+
+ return &CaptureResult{
+ Buffer: cropped,
+ Region: region,
+ YInverted: false,
+ Format: result.Format,
+ }, nil
+}
+
+func (s *Screenshoter) captureFullScreen() (*CaptureResult, error) {
+ output := s.findFocusedOutput()
+ if output == nil {
+ s.outputsMu.Lock()
+ for _, o := range s.outputs {
+ output = o
+ break
+ }
+ s.outputsMu.Unlock()
+ }
+
+ if output == nil {
+ return nil, fmt.Errorf("no output available")
+ }
+
+ return s.captureWholeOutput(output)
+}
+
+func (s *Screenshoter) captureOutput(name string) (*CaptureResult, error) {
+ s.outputsMu.Lock()
+ var output *WaylandOutput
+ for _, o := range s.outputs {
+ if o.name == name {
+ output = o
+ break
+ }
+ }
+ s.outputsMu.Unlock()
+
+ if output == nil {
+ return nil, fmt.Errorf("output %q not found", name)
+ }
+
+ return s.captureWholeOutput(output)
+}
+
+func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
+ s.outputsMu.Lock()
+ outputs := make([]*WaylandOutput, 0, len(s.outputs))
+ for _, o := range s.outputs {
+ outputs = append(outputs, o)
+ }
+ s.outputsMu.Unlock()
+
+ if len(outputs) == 0 {
+ return nil, fmt.Errorf("no outputs available")
+ }
+ if len(outputs) == 1 {
+ return s.captureWholeOutput(outputs[0])
+ }
+
+ wlrInfos := getAllOutputInfos()
+
+ type pendingOutput struct {
+ result *CaptureResult
+ logX float64
+ logY float64
+ scale float64
+ }
+ var pending []pendingOutput
+ maxScale := 1.0
+
+ for _, output := range outputs {
+ result, err := s.captureWholeOutput(output)
+ if err != nil {
+ log.Warn("failed to capture output", "name", output.name, "err", err)
+ continue
+ }
+
+ logX, logY := float64(output.x), float64(output.y)
+ scale := float64(output.scale)
+
+ switch DetectCompositor() {
+ case CompositorHyprland:
+ if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
+ logX, logY = float64(hx), float64(hy)
+ }
+ if hs := GetHyprlandMonitorScale(output.name); hs > 0 {
+ scale = hs
+ }
+ default:
+ if wlrInfos != nil {
+ if info, ok := wlrInfos[output.name]; ok {
+ logX, logY = float64(info.x), float64(info.y)
+ if info.scale > 0 {
+ scale = info.scale
+ }
+ }
+ }
+ }
+
+ if scale <= 0 {
+ scale = 1.0
+ }
+
+ pending = append(pending, pendingOutput{result: result, logX: logX, logY: logY, scale: scale})
+ if scale > maxScale {
+ maxScale = scale
+ }
+ }
+
+ if len(pending) == 0 {
+ return nil, fmt.Errorf("failed to capture any outputs")
+ }
+ if len(pending) == 1 {
+ return pending[0].result, nil
+ }
+
+ type layoutEntry struct {
+ result *CaptureResult
+ canvasX int
+ canvasY int
+ canvasW int
+ canvasH int
+ }
+ entries := make([]layoutEntry, len(pending))
+ var minX, minY, maxX, maxY int
+
+ for i, p := range pending {
+ cx := int(math.Round(p.logX * maxScale))
+ cy := int(math.Round(p.logY * maxScale))
+ cw := int(math.Round(float64(p.result.Buffer.Width) * maxScale / p.scale))
+ ch := int(math.Round(float64(p.result.Buffer.Height) * maxScale / p.scale))
+
+ entries[i] = layoutEntry{result: p.result, canvasX: cx, canvasY: cy, canvasW: cw, canvasH: ch}
+
+ right := cx + cw
+ bottom := cy + ch
+ if i == 0 {
+ minX, minY, maxX, maxY = cx, cy, right, bottom
+ continue
+ }
+ if cx < minX {
+ minX = cx
+ }
+ if cy < minY {
+ minY = cy
+ }
+ if right > maxX {
+ maxX = right
+ }
+ if bottom > maxY {
+ maxY = bottom
+ }
+ }
+
+ totalW := maxX - minX
+ totalH := maxY - minY
+ composite, err := CreateShmBuffer(totalW, totalH, totalW*4)
+ if err != nil {
+ for _, e := range entries {
+ e.result.Buffer.Close()
+ }
+ return nil, fmt.Errorf("create composite buffer: %w", err)
+ }
+ composite.Clear()
+
+ var format uint32
+ for _, e := range entries {
+ if format == 0 {
+ format = e.result.Format
+ }
+ s.blitBufferScaled(composite, e.result.Buffer,
+ e.canvasX-minX, e.canvasY-minY, e.canvasW, e.canvasH,
+ e.result.YInverted)
+ e.result.Buffer.Close()
+ }
+
+ return &CaptureResult{
+ Buffer: composite,
+ Region: Region{X: int32(minX), Y: int32(minY), Width: int32(totalW), Height: int32(totalH)},
+ Format: format,
+ }, nil
+}
+
+func (s *Screenshoter) blitBufferScaled(dst, src *ShmBuffer, dstX, dstY, dstW, dstH int, yInverted bool) {
+ if dstW <= 0 || dstH <= 0 {
+ return
+ }
+
+ srcData := src.Data()
+ dstData := dst.Data()
+
+ for dy := 0; dy < dstH; dy++ {
+ canvasY := dstY + dy
+ if canvasY < 0 || canvasY >= dst.Height {
+ continue
+ }
+
+ srcY := dy * src.Height / dstH
+ if yInverted {
+ srcY = src.Height - 1 - srcY
+ }
+ if srcY < 0 || srcY >= src.Height {
+ continue
+ }
+
+ srcRowOff := srcY * src.Stride
+ dstRowOff := canvasY * dst.Stride
+
+ for dx := 0; dx < dstW; dx++ {
+ canvasX := dstX + dx
+ if canvasX < 0 || canvasX >= dst.Width {
+ continue
+ }
+
+ srcX := dx * src.Width / dstW
+ if srcX >= src.Width {
+ continue
+ }
+
+ si := srcRowOff + srcX*4
+ di := dstRowOff + canvasX*4
+
+ if si+3 >= len(srcData) || di+3 >= len(dstData) {
+ continue
+ }
+
+ dstData[di+0] = srcData[si+0]
+ dstData[di+1] = srcData[si+1]
+ dstData[di+2] = srcData[si+2]
+ dstData[di+3] = srcData[si+3]
+ }
+ }
+}
+
+func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult, error) {
+ cursor := int32(s.config.Cursor)
+
+ frame, err := s.screencopy.CaptureOutput(cursor, output.wlOutput)
+ if err != nil {
+ return nil, fmt.Errorf("capture output: %w", err)
+ }
+
+ result, err := s.processFrame(frame, Region{
+ X: output.x,
+ Y: output.y,
+ Width: output.width,
+ Height: output.height,
+ Output: output.name,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ if result.YInverted {
+ result.Buffer.FlipVertical()
+ result.YInverted = false
+ }
+
+ if output.transform == TransformNormal {
+ return result, nil
+ }
+
+ invTransform := InverseTransform(output.transform)
+ transformed, err := result.Buffer.ApplyTransform(invTransform)
+ if err != nil {
+ result.Buffer.Close()
+ return nil, fmt.Errorf("apply transform: %w", err)
+ }
+
+ if transformed != result.Buffer {
+ result.Buffer.Close()
+ result.Buffer = transformed
+ }
+
+ result.Region.Width = int32(transformed.Width)
+ result.Region.Height = int32(transformed.Height)
+
+ return result, nil
+}
+
+func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*CaptureResult, error) {
+ result, err := s.captureWholeOutput(output)
+ if err != nil {
+ return nil, err
+ }
+
+ outX, outY := output.x, output.y
+ scale := float64(output.scale)
+ if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
+ outX, outY = hx, hy
+ }
+ if s := GetHyprlandMonitorScale(output.name); s > 0 {
+ scale = s
+ }
+ if scale <= 0 {
+ scale = 1.0
+ }
+
+ localX := int(float64(region.X-outX) * scale)
+ localY := int(float64(region.Y-outY) * scale)
+ w := int(float64(region.Width) * scale)
+ h := int(float64(region.Height) * scale)
+
+ cropped, err := CreateShmBuffer(w, h, w*4)
+ if err != nil {
+ result.Buffer.Close()
+ return nil, fmt.Errorf("create crop buffer: %w", err)
+ }
+
+ srcData := result.Buffer.Data()
+ dstData := cropped.Data()
+
+ for y := 0; y < h; y++ {
+ srcY := localY + y
+ if result.YInverted {
+ srcY = result.Buffer.Height - 1 - (localY + y)
+ }
+ if srcY < 0 || srcY >= result.Buffer.Height {
+ continue
+ }
+
+ dstY := y
+ if result.YInverted {
+ dstY = h - 1 - y
+ }
+
+ for x := 0; x < w; x++ {
+ srcX := localX + x
+ if srcX < 0 || srcX >= result.Buffer.Width {
+ continue
+ }
+
+ si := srcY*result.Buffer.Stride + srcX*4
+ di := dstY*cropped.Stride + x*4
+
+ if si+3 >= len(srcData) || di+3 >= len(dstData) {
+ continue
+ }
+
+ dstData[di+0] = srcData[si+0]
+ dstData[di+1] = srcData[si+1]
+ dstData[di+2] = srcData[si+2]
+ dstData[di+3] = srcData[si+3]
+ }
+ }
+
+ result.Buffer.Close()
+ cropped.Format = PixelFormat(result.Format)
+
+ return &CaptureResult{
+ Buffer: cropped,
+ Region: region,
+ YInverted: false,
+ Format: result.Format,
+ }, nil
+}
+
+func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
+ if output.transform != TransformNormal {
+ return s.captureRegionOnTransformedOutput(output, region)
+ }
+
+ scale := output.fractionalScale
+ if scale <= 0 && DetectCompositor() == CompositorHyprland {
+ scale = GetHyprlandMonitorScale(output.name)
+ }
+ if scale <= 0 {
+ scale = float64(output.scale)
+ }
+ if scale <= 0 {
+ scale = 1.0
+ }
+
+ localX := int32(float64(region.X-output.x) * scale)
+ localY := int32(float64(region.Y-output.y) * scale)
+ w := int32(float64(region.Width) * scale)
+ h := int32(float64(region.Height) * scale)
+
+ if DetectCompositor() == CompositorDWL {
+ scaledOutW := int32(float64(output.width) * scale)
+ scaledOutH := int32(float64(output.height) * scale)
+ if localX >= scaledOutW {
+ localX = localX % scaledOutW
+ }
+ if localY >= scaledOutH {
+ localY = localY % scaledOutH
+ }
+ if localX+w > scaledOutW {
+ w = scaledOutW - localX
+ }
+ if localY+h > scaledOutH {
+ h = scaledOutH - localY
+ }
+ if localX < 0 {
+ w += localX
+ localX = 0
+ }
+ if localY < 0 {
+ h += localY
+ localY = 0
+ }
+ }
+
+ cursor := int32(s.config.Cursor)
+
+ frame, err := s.screencopy.CaptureOutputRegion(cursor, output.wlOutput, localX, localY, w, h)
+ if err != nil {
+ return nil, fmt.Errorf("capture region: %w", err)
+ }
+
+ return s.processFrame(frame, region)
+}
+
+func (s *Screenshoter) captureRegionOnTransformedOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
+ result, err := s.captureWholeOutput(output)
+ if err != nil {
+ return nil, err
+ }
+
+ scale := output.fractionalScale
+ if scale <= 0 && DetectCompositor() == CompositorHyprland {
+ scale = GetHyprlandMonitorScale(output.name)
+ }
+ if scale <= 0 {
+ scale = float64(output.scale)
+ }
+ if scale <= 0 {
+ scale = 1.0
+ }
+
+ localX := int(float64(region.X-output.x) * scale)
+ localY := int(float64(region.Y-output.y) * scale)
+ w := int(float64(region.Width) * scale)
+ h := int(float64(region.Height) * scale)
+
+ if localX < 0 {
+ w += localX
+ localX = 0
+ }
+ if localY < 0 {
+ h += localY
+ localY = 0
+ }
+ if localX+w > result.Buffer.Width {
+ w = result.Buffer.Width - localX
+ }
+ if localY+h > result.Buffer.Height {
+ h = result.Buffer.Height - localY
+ }
+
+ if w <= 0 || h <= 0 {
+ result.Buffer.Close()
+ return nil, fmt.Errorf("region not visible on output")
+ }
+
+ cropped, err := CreateShmBuffer(w, h, w*4)
+ if err != nil {
+ result.Buffer.Close()
+ return nil, fmt.Errorf("create crop buffer: %w", err)
+ }
+
+ srcData := result.Buffer.Data()
+ dstData := cropped.Data()
+
+ for y := 0; y < h; y++ {
+ srcOff := (localY+y)*result.Buffer.Stride + localX*4
+ dstOff := y * cropped.Stride
+ if srcOff+w*4 <= len(srcData) && dstOff+w*4 <= len(dstData) {
+ copy(dstData[dstOff:dstOff+w*4], srcData[srcOff:srcOff+w*4])
+ }
+ }
+
+ result.Buffer.Close()
+ cropped.Format = PixelFormat(result.Format)
+
+ return &CaptureResult{
+ Buffer: cropped,
+ Region: region,
+ YInverted: false,
+ Format: result.Format,
+ }, nil
+}
+
+func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1, region Region) (*CaptureResult, error) {
+ var buf *ShmBuffer
+ var pool *client.ShmPool
+ var wlBuf *client.Buffer
+ var format PixelFormat
+ var yInverted bool
+ ready := false
+ failed := false
+
+ frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
+ format = PixelFormat(e.Format)
+ bpp := format.BytesPerPixel()
+ if int(e.Stride) < int(e.Width)*bpp {
+ log.Error("invalid stride from compositor", "stride", e.Stride, "width", e.Width, "bpp", bpp)
+ return
+ }
+ var err error
+ buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
+ if err != nil {
+ log.Error("failed to create buffer", "err", err)
+ return
+ }
+ buf.Format = format
+ })
+
+ frame.SetFlagsHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FlagsEvent) {
+ yInverted = (e.Flags & 1) != 0
+ })
+
+ frame.SetBufferDoneHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferDoneEvent) {
+ if buf == nil {
+ return
+ }
+
+ var err error
+ pool, err = s.shm.CreatePool(buf.Fd(), int32(buf.Size()))
+ if err != nil {
+ log.Error("failed to create pool", "err", err)
+ return
+ }
+
+ wlBuf, err = pool.CreateBuffer(0, int32(buf.Width), int32(buf.Height), int32(buf.Stride), uint32(format))
+ if err != nil {
+ pool.Destroy()
+ pool = nil
+ log.Error("failed to create wl_buffer", "err", err)
+ return
+ }
+
+ if err := frame.Copy(wlBuf); err != nil {
+ log.Error("failed to copy frame", "err", err)
+ }
+ })
+
+ frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
+ ready = true
+ })
+
+ frame.SetFailedHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FailedEvent) {
+ failed = true
+ })
+
+ for !ready && !failed {
+ if err := s.ctx.Dispatch(); err != nil {
+ frame.Destroy()
+ return nil, fmt.Errorf("dispatch: %w", err)
+ }
+ }
+
+ frame.Destroy()
+ if wlBuf != nil {
+ wlBuf.Destroy()
+ }
+ if pool != nil {
+ pool.Destroy()
+ }
+
+ if failed {
+ if buf != nil {
+ buf.Close()
+ }
+ return nil, fmt.Errorf("frame capture failed")
+ }
+
+ if format.Is24Bit() {
+ converted, newFormat, err := buf.ConvertTo32Bit(format)
+ if err != nil {
+ buf.Close()
+ return nil, fmt.Errorf("convert 24-bit to 32-bit: %w", err)
+ }
+ if converted != buf {
+ buf.Close()
+ buf = converted
+ }
+ format = newFormat
+ }
+
+ return &CaptureResult{
+ Buffer: buf,
+ Region: region,
+ YInverted: yInverted,
+ Format: uint32(format),
+ }, nil
+}
+
+func (s *Screenshoter) findOutputByName(name string) *WaylandOutput {
+ s.outputsMu.Lock()
+ defer s.outputsMu.Unlock()
+ for _, o := range s.outputs {
+ if o.name == name {
+ return o
+ }
+ }
+ return nil
+}
+
+func (s *Screenshoter) findOutputForRegion(region Region) *WaylandOutput {
+ s.outputsMu.Lock()
+ defer s.outputsMu.Unlock()
+
+ cx := region.X + region.Width/2
+ cy := region.Y + region.Height/2
+
+ for _, o := range s.outputs {
+ x, y, w, h := o.x, o.y, o.width, o.height
+ if DetectCompositor() == CompositorHyprland {
+ if hx, hy, hw, hh, ok := GetHyprlandMonitorGeometry(o.name); ok {
+ x, y, w, h = hx, hy, hw, hh
+ }
+ }
+ if cx >= x && cx < x+w && cy >= y && cy < y+h {
+ return o
+ }
+ }
+
+ for _, o := range s.outputs {
+ x, y, w, h := o.x, o.y, o.width, o.height
+ if DetectCompositor() == CompositorHyprland {
+ if hx, hy, hw, hh, ok := GetHyprlandMonitorGeometry(o.name); ok {
+ x, y, w, h = hx, hy, hw, hh
+ }
+ }
+ if region.X >= x && region.X < x+w &&
+ region.Y >= y && region.Y < y+h {
+ return o
+ }
+ }
+
+ return nil
+}
+
+func (s *Screenshoter) findFocusedOutput() *WaylandOutput {
+ if mon := GetFocusedMonitor(); mon != "" {
+ s.outputsMu.Lock()
+ defer s.outputsMu.Unlock()
+ for _, o := range s.outputs {
+ if o.name == mon {
+ return o
+ }
+ }
+ }
+ s.outputsMu.Lock()
+ defer s.outputsMu.Unlock()
+ for _, o := range s.outputs {
+ return o
+ }
+ return nil
+}
+
+func (s *Screenshoter) connect() error {
+ display, err := client.Connect("")
+ if err != nil {
+ return err
+ }
+ s.display = display
+ s.ctx = display.Context()
+ return nil
+}
+
+func (s *Screenshoter) roundtrip() error {
+ return wlhelpers.Roundtrip(s.display, s.ctx)
+}
+
+func (s *Screenshoter) setupRegistry() error {
+ registry, err := s.display.GetRegistry()
+ if err != nil {
+ return err
+ }
+ s.registry = registry
+
+ registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
+ s.handleGlobal(e)
+ })
+
+ registry.SetGlobalRemoveHandler(func(e client.RegistryGlobalRemoveEvent) {
+ s.outputsMu.Lock()
+ delete(s.outputs, e.Name)
+ s.outputsMu.Unlock()
+ })
+
+ return nil
+}
+
+func (s *Screenshoter) handleGlobal(e client.RegistryGlobalEvent) {
+ switch e.Interface {
+ case client.CompositorInterfaceName:
+ comp := client.NewCompositor(s.ctx)
+ if err := s.registry.Bind(e.Name, e.Interface, e.Version, comp); err == nil {
+ s.compositor = comp
+ }
+
+ case client.ShmInterfaceName:
+ shm := client.NewShm(s.ctx)
+ if err := s.registry.Bind(e.Name, e.Interface, e.Version, shm); err == nil {
+ s.shm = shm
+ }
+
+ case client.OutputInterfaceName:
+ output := client.NewOutput(s.ctx)
+ version := e.Version
+ if version > 4 {
+ version = 4
+ }
+ if err := s.registry.Bind(e.Name, e.Interface, version, output); err == nil {
+ s.outputsMu.Lock()
+ s.outputs[e.Name] = &WaylandOutput{
+ wlOutput: output,
+ globalName: e.Name,
+ scale: 1,
+ fractionalScale: 1.0,
+ }
+ s.outputsMu.Unlock()
+ s.setupOutputHandlers(e.Name, output)
+ }
+
+ case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
+ sc := wlr_screencopy.NewZwlrScreencopyManagerV1(s.ctx)
+ version := e.Version
+ if version > 3 {
+ version = 3
+ }
+ if err := s.registry.Bind(e.Name, e.Interface, version, sc); err == nil {
+ s.screencopy = sc
+ }
+ }
+}
+
+func (s *Screenshoter) setupOutputHandlers(name uint32, output *client.Output) {
+ output.SetGeometryHandler(func(e client.OutputGeometryEvent) {
+ s.outputsMu.Lock()
+ if o, ok := s.outputs[name]; ok {
+ o.x, o.y = e.X, e.Y
+ o.transform = int32(e.Transform)
+ }
+ s.outputsMu.Unlock()
+ })
+
+ output.SetModeHandler(func(e client.OutputModeEvent) {
+ if e.Flags&uint32(client.OutputModeCurrent) == 0 {
+ return
+ }
+ s.outputsMu.Lock()
+ if o, ok := s.outputs[name]; ok {
+ o.width, o.height = e.Width, e.Height
+ }
+ s.outputsMu.Unlock()
+ })
+
+ output.SetScaleHandler(func(e client.OutputScaleEvent) {
+ s.outputsMu.Lock()
+ if o, ok := s.outputs[name]; ok {
+ o.scale = e.Factor
+ o.fractionalScale = float64(e.Factor)
+ }
+ s.outputsMu.Unlock()
+ })
+
+ output.SetNameHandler(func(e client.OutputNameEvent) {
+ s.outputsMu.Lock()
+ if o, ok := s.outputs[name]; ok {
+ o.name = e.Name
+ }
+ s.outputsMu.Unlock()
+ })
+}
+
+func (s *Screenshoter) cleanup() {
+ if s.screencopy != nil {
+ s.screencopy.Destroy()
+ }
+ if s.display != nil {
+ s.ctx.Close()
+ }
+}
+
+func (s *Screenshoter) GetOutputs() []*WaylandOutput {
+ s.outputsMu.Lock()
+ defer s.outputsMu.Unlock()
+ out := make([]*WaylandOutput, 0, len(s.outputs))
+ for _, o := range s.outputs {
+ out = append(out, o)
+ }
+ return out
+}
+
+func ListOutputs() ([]Output, error) {
+ sc := New(DefaultConfig())
+ if err := sc.connect(); err != nil {
+ return nil, err
+ }
+ defer sc.cleanup()
+
+ if err := sc.setupRegistry(); err != nil {
+ return nil, err
+ }
+ if err := sc.roundtrip(); err != nil {
+ return nil, err
+ }
+ if err := sc.roundtrip(); err != nil {
+ return nil, err
+ }
+
+ sc.outputsMu.Lock()
+ defer sc.outputsMu.Unlock()
+
+ compositor := DetectCompositor()
+ result := make([]Output, 0, len(sc.outputs))
+ for _, o := range sc.outputs {
+ out := Output{
+ Name: o.name,
+ X: o.x,
+ Y: o.y,
+ Width: o.width,
+ Height: o.height,
+ Scale: o.scale,
+ FractionalScale: o.fractionalScale,
+ Transform: o.transform,
+ }
+
+ switch compositor {
+ case CompositorHyprland:
+ if hx, hy, hw, hh, ok := GetHyprlandMonitorGeometry(o.name); ok {
+ out.X, out.Y = hx, hy
+ out.Width, out.Height = hw, hh
+ }
+ if s := GetHyprlandMonitorScale(o.name); s > 0 {
+ out.FractionalScale = s
+ }
+ }
+
+ result = append(result, out)
+ }
+ return result, nil
+}