From e606a76a86374c5669b0b909947e08cddde1f37c Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 8 Dec 2025 09:39:42 -0500 Subject: [PATCH] screenshot: add screenshot-window support for DWL/MangoWC --- core/cmd/dms/commands_screenshot.go | 7 +- core/internal/screenshot/compositor.go | 289 ++++++++++++++++++++++++- core/internal/screenshot/screenshot.go | 164 +++++++++++++- 3 files changed, 451 insertions(+), 9 deletions(-) diff --git a/core/cmd/dms/commands_screenshot.go b/core/cmd/dms/commands_screenshot.go index ffbd56a1..f8660b7a 100644 --- a/core/cmd/dms/commands_screenshot.go +++ b/core/cmd/dms/commands_screenshot.go @@ -35,7 +35,7 @@ Modes: full - Capture the focused output all - Capture all outputs combined output - Capture a specific output by name - window - Capture the focused window (Hyprland only) + window - Capture the focused window (Hyprland/DWL) last - Capture the last selected region Output format (--format): @@ -91,9 +91,8 @@ If no previous region exists, falls back to interactive selection.`, var ssWindowCmd = &cobra.Command{ Use: "window", Short: "Capture the focused window", - Long: `Capture the currently focused window. -Currently only supported on Hyprland.`, - Run: runScreenshotWindow, + Long: `Capture the currently focused window. Supported on Hyprland and DWL.`, + Run: runScreenshotWindow, } var ssListCmd = &cobra.Command{ diff --git a/core/internal/screenshot/compositor.go b/core/internal/screenshot/compositor.go index 38913d99..d1e94995 100644 --- a/core/internal/screenshot/compositor.go +++ b/core/internal/screenshot/compositor.go @@ -5,6 +5,10 @@ import ( "fmt" "os" "os/exec" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc" + wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client" + "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" ) type Compositor int @@ -44,10 +48,42 @@ func DetectCompositor() Compositor { return detectedCompositor } + if detectDWLProtocol() { + detectedCompositor = CompositorDWL + return detectedCompositor + } + detectedCompositor = CompositorUnknown return detectedCompositor } +func detectDWLProtocol() bool { + display, err := client.Connect("") + if err != nil { + return false + } + ctx := display.Context() + defer ctx.Close() + + registry, err := display.GetRegistry() + if err != nil { + return false + } + + found := false + registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) { + if e.Interface == dwl_ipc.ZdwlIpcManagerV2InterfaceName { + found = true + } + }) + + if err := wlhelpers.Roundtrip(display, ctx); err != nil { + return false + } + + return found +} + func SetCompositorDWL() { detectedCompositor = CompositorDWL } @@ -57,14 +93,18 @@ type WindowGeometry struct { Y int32 Width int32 Height int32 + Output string + Scale float64 } func GetActiveWindow() (*WindowGeometry, error) { switch DetectCompositor() { case CompositorHyprland: return getHyprlandActiveWindow() + case CompositorDWL: + return getDWLActiveWindow() default: - return nil, fmt.Errorf("window capture requires Hyprland") + return nil, fmt.Errorf("window capture requires Hyprland or DWL") } } @@ -220,7 +260,112 @@ func SetDWLActiveOutput(name string) { } func getDWLFocusedMonitor() string { - return dwlActiveOutput + if dwlActiveOutput != "" { + return dwlActiveOutput + } + return queryDWLActiveOutput() +} + +func queryDWLActiveOutput() string { + display, err := client.Connect("") + if err != nil { + return "" + } + ctx := display.Context() + defer ctx.Close() + + registry, err := display.GetRegistry() + if err != nil { + return "" + } + + var dwlManager *dwl_ipc.ZdwlIpcManagerV2 + outputs := make(map[uint32]*client.Output) + + registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) { + switch e.Interface { + case dwl_ipc.ZdwlIpcManagerV2InterfaceName: + mgr := dwl_ipc.NewZdwlIpcManagerV2(ctx) + if err := registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil { + dwlManager = mgr + } + case client.OutputInterfaceName: + out := client.NewOutput(ctx) + version := e.Version + if version > 4 { + version = 4 + } + if err := registry.Bind(e.Name, e.Interface, version, out); err == nil { + outputs[e.Name] = out + } + } + }) + + if err := wlhelpers.Roundtrip(display, ctx); err != nil { + return "" + } + + if dwlManager == nil || len(outputs) == 0 { + return "" + } + + outputNames := make(map[uint32]string) + for name, out := range outputs { + n := name + out.SetNameHandler(func(e client.OutputNameEvent) { + outputNames[n] = e.Name + }) + } + + if err := wlhelpers.Roundtrip(display, ctx); err != nil { + return "" + } + + type outputState struct { + name string + active bool + gotFrame bool + } + states := make(map[uint32]*outputState) + + for name, out := range outputs { + dwlOut, err := dwlManager.GetOutput(out) + if err != nil { + continue + } + state := &outputState{name: outputNames[name]} + states[name] = state + + dwlOut.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) { + state.active = e.Active != 0 + }) + dwlOut.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) { + state.gotFrame = true + }) + } + + allFramesReceived := func() bool { + for _, s := range states { + if !s.gotFrame { + return false + } + } + return true + } + + for !allFramesReceived() { + if err := ctx.Dispatch(); err != nil { + return "" + } + } + + for _, state := range states { + if state.active { + return state.name + } + } + + return "" } func GetFocusedMonitor() string { @@ -236,3 +381,143 @@ func GetFocusedMonitor() string { } return "" } + +func getDWLActiveWindow() (*WindowGeometry, error) { + display, err := client.Connect("") + if err != nil { + return nil, fmt.Errorf("connect: %w", err) + } + ctx := display.Context() + defer ctx.Close() + + registry, err := display.GetRegistry() + if err != nil { + return nil, fmt.Errorf("get registry: %w", err) + } + + var dwlManager *dwl_ipc.ZdwlIpcManagerV2 + outputs := make(map[uint32]*client.Output) + + registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) { + switch e.Interface { + case dwl_ipc.ZdwlIpcManagerV2InterfaceName: + mgr := dwl_ipc.NewZdwlIpcManagerV2(ctx) + if err := registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil { + dwlManager = mgr + } + case client.OutputInterfaceName: + out := client.NewOutput(ctx) + version := e.Version + if version > 4 { + version = 4 + } + if err := registry.Bind(e.Name, e.Interface, version, out); err == nil { + outputs[e.Name] = out + } + } + }) + + if err := wlhelpers.Roundtrip(display, ctx); err != nil { + return nil, fmt.Errorf("roundtrip: %w", err) + } + + if dwlManager == nil { + return nil, fmt.Errorf("dwl_ipc_manager not available") + } + + if len(outputs) == 0 { + return nil, fmt.Errorf("no outputs found") + } + + outputNames := make(map[uint32]string) + for name, out := range outputs { + n := name + out.SetNameHandler(func(e client.OutputNameEvent) { + outputNames[n] = e.Name + }) + } + + if err := wlhelpers.Roundtrip(display, ctx); err != nil { + return nil, fmt.Errorf("roundtrip: %w", err) + } + + type dwlOutputState struct { + output *dwl_ipc.ZdwlIpcOutputV2 + name string + active bool + x, y int32 + w, h int32 + scalefactor uint32 + gotFrame bool + } + + dwlOutputs := make(map[uint32]*dwlOutputState) + for name, out := range outputs { + dwlOut, err := dwlManager.GetOutput(out) + if err != nil { + continue + } + state := &dwlOutputState{output: dwlOut, name: outputNames[name]} + dwlOutputs[name] = state + + dwlOut.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) { + state.active = e.Active != 0 + }) + dwlOut.SetXHandler(func(e dwl_ipc.ZdwlIpcOutputV2XEvent) { + state.x = e.X + }) + dwlOut.SetYHandler(func(e dwl_ipc.ZdwlIpcOutputV2YEvent) { + state.y = e.Y + }) + dwlOut.SetWidthHandler(func(e dwl_ipc.ZdwlIpcOutputV2WidthEvent) { + state.w = e.Width + }) + dwlOut.SetHeightHandler(func(e dwl_ipc.ZdwlIpcOutputV2HeightEvent) { + state.h = e.Height + }) + dwlOut.SetScalefactorHandler(func(e dwl_ipc.ZdwlIpcOutputV2ScalefactorEvent) { + state.scalefactor = e.Scalefactor + }) + dwlOut.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) { + state.gotFrame = true + }) + } + + allFramesReceived := func() bool { + for _, s := range dwlOutputs { + if !s.gotFrame { + return false + } + } + return true + } + + for !allFramesReceived() { + if err := ctx.Dispatch(); err != nil { + return nil, fmt.Errorf("dispatch: %w", err) + } + } + + for _, state := range dwlOutputs { + if !state.active { + continue + } + if state.w <= 0 || state.h <= 0 { + return nil, fmt.Errorf("no active window") + } + scale := float64(state.scalefactor) / 100.0 + if scale <= 0 { + scale = 1.0 + } + return &WindowGeometry{ + X: state.x, + Y: state.y, + Width: state.w, + Height: state.h, + Output: state.name, + Scale: scale, + }, nil + } + + return nil, fmt.Errorf("no active output found") +} diff --git a/core/internal/screenshot/screenshot.go b/core/internal/screenshot/screenshot.go index b8e909bc..86f85431 100644 --- a/core/internal/screenshot/screenshot.go +++ b/core/internal/screenshot/screenshot.go @@ -135,16 +135,138 @@ func (s *Screenshoter) captureWindow() (*CaptureResult, error) { Height: geom.Height, } - output := s.findOutputForRegion(region) + 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") } - if DetectCompositor() == CompositorHyprland { + switch DetectCompositor() { + case CompositorHyprland: return s.captureAndCrop(output, region) + case CompositorDWL: + return s.captureDWLWindow(output, region, geom.Scale) + default: + return s.captureRegionOnOutput(output, region) + } +} + +func (s *Screenshoter) captureDWLWindow(output *WaylandOutput, region Region, dwlScale float64) (*CaptureResult, error) { + result, err := s.captureWholeOutput(output) + if err != nil { + return nil, err } - return s.captureRegionOnOutput(output, region) + scale := dwlScale + if scale <= 0 { + scale = float64(result.Buffer.Width) / float64(output.width) + } + if scale <= 0 { + scale = 1.0 + } + + localX := int(float64(region.X) * scale) + localY := int(float64(region.Y) * scale) + if localX >= result.Buffer.Width { + localX = localX % result.Buffer.Width + } + if localY >= result.Buffer.Height { + localY = localY % result.Buffer.Height + } + + w := int(float64(region.Width) * scale) + h := int(float64(region.Height) * scale) + + if localY+h > result.Buffer.Height && h <= result.Buffer.Height { + localY = result.Buffer.Height - h + if localY < 0 { + localY = 0 + } + } + if localX+w > result.Buffer.Width && w <= result.Buffer.Width { + localX = result.Buffer.Width - w + if localX < 0 { + localX = 0 + } + } + + 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) { @@ -457,6 +579,31 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio 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(0) if s.config.IncludeCursor { cursor = 1 @@ -557,6 +704,17 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1, }, 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()