mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
524 lines
11 KiB
Go
524 lines
11 KiB
Go
package screenshot
|
|
|
|
import (
|
|
"encoding/json"
|
|
"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
|
|
|
|
const (
|
|
CompositorUnknown Compositor = iota
|
|
CompositorHyprland
|
|
CompositorSway
|
|
CompositorNiri
|
|
CompositorDWL
|
|
)
|
|
|
|
var detectedCompositor Compositor = -1
|
|
|
|
func DetectCompositor() Compositor {
|
|
if detectedCompositor >= 0 {
|
|
return detectedCompositor
|
|
}
|
|
|
|
hyprlandSig := os.Getenv("HYPRLAND_INSTANCE_SIGNATURE")
|
|
niriSocket := os.Getenv("NIRI_SOCKET")
|
|
swaySocket := os.Getenv("SWAYSOCK")
|
|
|
|
switch {
|
|
case niriSocket != "":
|
|
if _, err := os.Stat(niriSocket); err == nil {
|
|
detectedCompositor = CompositorNiri
|
|
return detectedCompositor
|
|
}
|
|
case swaySocket != "":
|
|
if _, err := os.Stat(swaySocket); err == nil {
|
|
detectedCompositor = CompositorSway
|
|
return detectedCompositor
|
|
}
|
|
case hyprlandSig != "":
|
|
detectedCompositor = CompositorHyprland
|
|
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
|
|
}
|
|
|
|
type WindowGeometry struct {
|
|
X int32
|
|
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 or DWL")
|
|
}
|
|
}
|
|
|
|
type hyprlandWindow struct {
|
|
At [2]int32 `json:"at"`
|
|
Size [2]int32 `json:"size"`
|
|
}
|
|
|
|
func getHyprlandActiveWindow() (*WindowGeometry, error) {
|
|
output, err := exec.Command("hyprctl", "-j", "activewindow").Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hyprctl activewindow: %w", err)
|
|
}
|
|
|
|
var win hyprlandWindow
|
|
if err := json.Unmarshal(output, &win); err != nil {
|
|
return nil, fmt.Errorf("parse activewindow: %w", err)
|
|
}
|
|
|
|
if win.Size[0] <= 0 || win.Size[1] <= 0 {
|
|
return nil, fmt.Errorf("no active window")
|
|
}
|
|
|
|
return &WindowGeometry{
|
|
X: win.At[0],
|
|
Y: win.At[1],
|
|
Width: win.Size[0],
|
|
Height: win.Size[1],
|
|
}, nil
|
|
}
|
|
|
|
type hyprlandMonitor struct {
|
|
Name string `json:"name"`
|
|
X int32 `json:"x"`
|
|
Y int32 `json:"y"`
|
|
Width int32 `json:"width"`
|
|
Height int32 `json:"height"`
|
|
Scale float64 `json:"scale"`
|
|
Focused bool `json:"focused"`
|
|
}
|
|
|
|
func GetHyprlandMonitorScale(name string) float64 {
|
|
output, err := exec.Command("hyprctl", "-j", "monitors").Output()
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
|
|
var monitors []hyprlandMonitor
|
|
if err := json.Unmarshal(output, &monitors); err != nil {
|
|
return 0
|
|
}
|
|
|
|
for _, m := range monitors {
|
|
if m.Name == name {
|
|
return m.Scale
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func getHyprlandFocusedMonitor() string {
|
|
output, err := exec.Command("hyprctl", "-j", "monitors").Output()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
var monitors []hyprlandMonitor
|
|
if err := json.Unmarshal(output, &monitors); err != nil {
|
|
return ""
|
|
}
|
|
|
|
for _, m := range monitors {
|
|
if m.Focused {
|
|
return m.Name
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func GetHyprlandMonitorGeometry(name string) (x, y, w, h int32, ok bool) {
|
|
output, err := exec.Command("hyprctl", "-j", "monitors").Output()
|
|
if err != nil {
|
|
return 0, 0, 0, 0, false
|
|
}
|
|
|
|
var monitors []hyprlandMonitor
|
|
if err := json.Unmarshal(output, &monitors); err != nil {
|
|
return 0, 0, 0, 0, false
|
|
}
|
|
|
|
for _, m := range monitors {
|
|
if m.Name == name {
|
|
logicalW := int32(float64(m.Width) / m.Scale)
|
|
logicalH := int32(float64(m.Height) / m.Scale)
|
|
return m.X, m.Y, logicalW, logicalH, true
|
|
}
|
|
}
|
|
return 0, 0, 0, 0, false
|
|
}
|
|
|
|
type swayWorkspace struct {
|
|
Output string `json:"output"`
|
|
Focused bool `json:"focused"`
|
|
}
|
|
|
|
func getSwayFocusedMonitor() string {
|
|
output, err := exec.Command("swaymsg", "-t", "get_workspaces").Output()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
var workspaces []swayWorkspace
|
|
if err := json.Unmarshal(output, &workspaces); err != nil {
|
|
return ""
|
|
}
|
|
|
|
for _, ws := range workspaces {
|
|
if ws.Focused {
|
|
return ws.Output
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type niriWorkspace struct {
|
|
Output string `json:"output"`
|
|
IsFocused bool `json:"is_focused"`
|
|
}
|
|
|
|
func getNiriFocusedMonitor() string {
|
|
output, err := exec.Command("niri", "msg", "-j", "workspaces").Output()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
var workspaces []niriWorkspace
|
|
if err := json.Unmarshal(output, &workspaces); err != nil {
|
|
return ""
|
|
}
|
|
|
|
for _, ws := range workspaces {
|
|
if ws.IsFocused {
|
|
return ws.Output
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
var dwlActiveOutput string
|
|
|
|
func SetDWLActiveOutput(name string) {
|
|
dwlActiveOutput = name
|
|
}
|
|
|
|
func getDWLFocusedMonitor() string {
|
|
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 {
|
|
switch DetectCompositor() {
|
|
case CompositorHyprland:
|
|
return getHyprlandFocusedMonitor()
|
|
case CompositorSway:
|
|
return getSwayFocusedMonitor()
|
|
case CompositorNiri:
|
|
return getNiriFocusedMonitor()
|
|
case CompositorDWL:
|
|
return getDWLFocusedMonitor()
|
|
}
|
|
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")
|
|
}
|