mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 13:32:50 -05:00
screenshot: add screenshot-window support for DWL/MangoWC
This commit is contained in:
@@ -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{
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user