diff options
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot')
11 files changed, 3900 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/compositor.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/compositor.go new file mode 100644 index 0000000..9bd5f0e --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/compositor.go @@ -0,0 +1,694 @@ +package screenshot + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc" + "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management" + 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 + CompositorScroll + CompositorMiracle +) + +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") + scrollSocket := os.Getenv("SCROLLSOCK") + miracleSocket := os.Getenv("MIRACLESOCK") + + switch { + case niriSocket != "": + if _, err := os.Stat(niriSocket); err == nil { + detectedCompositor = CompositorNiri + return detectedCompositor + } + case scrollSocket != "": + if _, err := os.Stat(scrollSocket); err == nil { + detectedCompositor = CompositorScroll + return detectedCompositor + } + case miracleSocket != "": + if _, err := os.Stat(miracleSocket); err == nil { + detectedCompositor = CompositorMiracle + 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 + OutputX int32 + OutputY int32 + OutputTransform int32 +} + +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 "" +} + +func getScrollFocusedMonitor() string { + output, err := exec.Command("scrollmsg", "-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 "" +} + +func getMiracleFocusedMonitor() string { + output, err := exec.Command("miraclemsg", "-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 CompositorScroll: + return getScrollFocusedMonitor() + case CompositorMiracle: + return getMiracleFocusedMonitor() + case CompositorNiri: + return getNiriFocusedMonitor() + case CompositorDWL: + return getDWLFocusedMonitor() + } + return "" +} + +type outputInfo struct { + x, y int32 + scale float64 + transform int32 +} + +func getAllOutputInfos() map[string]*outputInfo { + display, err := client.Connect("") + if err != nil { + return nil + } + ctx := display.Context() + defer ctx.Close() + + registry, err := display.GetRegistry() + if err != nil { + return nil + } + + var outputManager *wlr_output_management.ZwlrOutputManagerV1 + + registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) { + if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName { + mgr := wlr_output_management.NewZwlrOutputManagerV1(ctx) + version := e.Version + if version > 4 { + version = 4 + } + if err := registry.Bind(e.Name, e.Interface, version, mgr); err == nil { + outputManager = mgr + } + } + }) + + if err := wlhelpers.Roundtrip(display, ctx); err != nil { + return nil + } + + if outputManager == nil { + return nil + } + + type headState struct { + name string + x, y int32 + scale float64 + transform int32 + } + heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState) + done := false + + outputManager.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) { + state := &headState{} + heads[e.Head] = state + e.Head.SetNameHandler(func(ne wlr_output_management.ZwlrOutputHeadV1NameEvent) { + state.name = ne.Name + }) + e.Head.SetPositionHandler(func(pe wlr_output_management.ZwlrOutputHeadV1PositionEvent) { + state.x = pe.X + state.y = pe.Y + }) + e.Head.SetScaleHandler(func(se wlr_output_management.ZwlrOutputHeadV1ScaleEvent) { + state.scale = se.Scale + }) + e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) { + state.transform = te.Transform + }) + }) + outputManager.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) { + done = true + }) + + for !done { + if err := ctx.Dispatch(); err != nil { + return nil + } + } + + result := make(map[string]*outputInfo, len(heads)) + for _, state := range heads { + if state.name == "" { + continue + } + result[state.name] = &outputInfo{ + x: state.x, + y: state.y, + scale: state.scale, + transform: state.transform, + } + } + return result +} + +func getOutputInfo(outputName string) (*outputInfo, bool) { + infos := getAllOutputInfos() + if infos == nil { + return nil, false + } + info, ok := infos[outputName] + return info, ok +} + +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 + } + + geom := &WindowGeometry{ + X: state.x, + Y: state.y, + Width: state.w, + Height: state.h, + Output: state.name, + Scale: scale, + } + + if info, ok := getOutputInfo(state.name); ok { + geom.OutputX = info.x + geom.OutputY = info.y + geom.OutputTransform = info.transform + } + + return geom, nil + } + + return nil, fmt.Errorf("no active output found") +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/encode.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/encode.go new file mode 100644 index 0000000..8b40dca --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/encode.go @@ -0,0 +1,172 @@ +package screenshot + +import ( + "bufio" + "fmt" + "image" + "image/jpeg" + "image/png" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/log" + "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" +) + +func BufferToImage(buf *ShmBuffer) *image.RGBA { + return BufferToImageWithFormat(buf, uint32(FormatARGB8888)) +} + +func BufferToImageWithFormat(buf *ShmBuffer, format uint32) *image.RGBA { + img := image.NewRGBA(image.Rect(0, 0, buf.Width, buf.Height)) + data := buf.Data() + + var swapRB bool + switch format { + case uint32(FormatABGR8888), uint32(FormatXBGR8888): + swapRB = false + default: + swapRB = true + } + + for y := 0; y < buf.Height; y++ { + srcOff := y * buf.Stride + dstOff := y * img.Stride + for x := 0; x < buf.Width; x++ { + si := srcOff + x*4 + di := dstOff + x*4 + if si+3 >= len(data) || di+3 >= len(img.Pix) { + continue + } + if swapRB { + img.Pix[di+0] = data[si+2] + img.Pix[di+1] = data[si+1] + img.Pix[di+2] = data[si+0] + } else { + img.Pix[di+0] = data[si+0] + img.Pix[di+1] = data[si+1] + img.Pix[di+2] = data[si+2] + } + img.Pix[di+3] = 255 + } + } + return img +} + +func EncodePNG(w io.Writer, img image.Image) error { + enc := png.Encoder{CompressionLevel: png.BestSpeed} + return enc.Encode(w, img) +} + +func EncodeJPEG(w io.Writer, img image.Image, quality int) error { + return jpeg.Encode(w, img, &jpeg.Options{Quality: quality}) +} + +func EncodePPM(w io.Writer, img *image.RGBA) error { + bw := bufio.NewWriter(w) + bounds := img.Bounds() + if _, err := fmt.Fprintf(bw, "P6\n%d %d\n255\n", bounds.Dx(), bounds.Dy()); err != nil { + return err + } + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + off := (y-bounds.Min.Y)*img.Stride + (x-bounds.Min.X)*4 + if err := bw.WriteByte(img.Pix[off+0]); err != nil { + return err + } + if err := bw.WriteByte(img.Pix[off+1]); err != nil { + return err + } + if err := bw.WriteByte(img.Pix[off+2]); err != nil { + return err + } + } + } + return bw.Flush() +} + +func GenerateFilename(format Format) string { + t := time.Now() + ext := "png" + switch format { + case FormatJPEG: + ext = "jpg" + case FormatPPM: + ext = "ppm" + } + return fmt.Sprintf("screenshot_%s.%s", t.Format("2006-01-02_15-04-05"), ext) +} + +func GetOutputDir() string { + if dir := os.Getenv("DMS_SCREENSHOT_DIR"); dir != "" { + return dir + } + + if xdgPics := getXDGPicturesDir(); xdgPics != "" { + screenshotDir := filepath.Join(xdgPics, "Screenshots") + if err := os.MkdirAll(screenshotDir, 0o755); err == nil { + return screenshotDir + } + return xdgPics + } + + if home := os.Getenv("HOME"); home != "" { + return home + } + return "." +} + +func getXDGPicturesDir() string { + userConfigDir, err := os.UserConfigDir() + if err != nil { + log.Error("failed to get user config dir", "err", err) + return "" + } + userDirsFile := filepath.Join(userConfigDir, "user-dirs.dirs") + data, err := os.ReadFile(userDirsFile) + if err != nil { + return "" + } + + for _, line := range strings.Split(string(data), "\n") { + if len(line) == 0 || line[0] == '#' { + continue + } + const prefix = "XDG_PICTURES_DIR=" + if !strings.HasPrefix(line, prefix) { + continue + } + path := strings.Trim(line[len(prefix):], "\"") + expanded, err := utils.ExpandPath(path) + if err != nil { + return "" + } + return expanded + } + return "" +} + +func WriteToFile(buf *ShmBuffer, path string, format Format, quality int) error { + return WriteToFileWithFormat(buf, path, format, quality, uint32(FormatARGB8888)) +} + +func WriteToFileWithFormat(buf *ShmBuffer, path string, format Format, quality int, pixelFormat uint32) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + + img := BufferToImageWithFormat(buf, pixelFormat) + switch format { + case FormatJPEG: + return EncodeJPEG(f, img, quality) + case FormatPPM: + return EncodePPM(f, img) + default: + return EncodePNG(f, img) + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/notify.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/notify.go new file mode 100644 index 0000000..9633b42 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/notify.go @@ -0,0 +1,180 @@ +package screenshot + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "syscall" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/log" + "github.com/godbus/dbus/v5" +) + +const ( + notifyDest = "org.freedesktop.Notifications" + notifyPath = "/org/freedesktop/Notifications" + notifyInterface = "org.freedesktop.Notifications" +) + +type NotifyResult struct { + FilePath string + Clipboard bool + ImageData []byte + Width int + Height int +} + +func SendNotification(result NotifyResult) { + conn, err := dbus.SessionBus() + if err != nil { + log.Debug("dbus session failed", "err", err) + return + } + + var actions []string + if result.FilePath != "" { + actions = []string{"default", "Open"} + } + + hints := map[string]dbus.Variant{} + if len(result.ImageData) > 0 && result.Width > 0 && result.Height > 0 { + rowstride := result.Width * 3 + hints["image_data"] = dbus.MakeVariant(struct { + Width int32 + Height int32 + Rowstride int32 + HasAlpha bool + BitsPerSample int32 + Channels int32 + Data []byte + }{ + Width: int32(result.Width), + Height: int32(result.Height), + Rowstride: int32(rowstride), + HasAlpha: false, + BitsPerSample: 8, + Channels: 3, + Data: result.ImageData, + }) + } else if result.FilePath != "" { + hints["image_path"] = dbus.MakeVariant(result.FilePath) + } + + summary := "Screenshot captured" + body := "" + if result.Clipboard && result.FilePath != "" { + body = fmt.Sprintf("Copied to clipboard\n%s", filepath.Base(result.FilePath)) + } else if result.Clipboard { + body = "Copied to clipboard" + } else if result.FilePath != "" { + body = filepath.Base(result.FilePath) + } + + obj := conn.Object(notifyDest, notifyPath) + call := obj.Call( + notifyInterface+".Notify", + 0, + "DMS", + uint32(0), + "", + summary, + body, + actions, + hints, + int32(5000), + ) + + if call.Err != nil { + log.Debug("notify call failed", "err", call.Err) + return + } + + var notificationID uint32 + if err := call.Store(¬ificationID); err != nil { + log.Debug("failed to get notification id", "err", err) + return + } + + if len(actions) == 0 || result.FilePath == "" { + return + } + + spawnActionListener(notificationID, result.FilePath) +} + +func spawnActionListener(notificationID uint32, filePath string) { + exe, err := os.Executable() + if err != nil { + log.Debug("failed to get executable", "err", err) + return + } + + cmd := exec.Command(exe, "notify-action", fmt.Sprintf("%d", notificationID), filePath) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, + } + cmd.Start() +} + +func RunNotifyActionListener(args []string) { + if len(args) < 2 { + return + } + + notificationID, err := strconv.ParseUint(args[0], 10, 32) + if err != nil { + return + } + + filePath := args[1] + + conn, err := dbus.SessionBus() + if err != nil { + return + } + + if err := conn.AddMatchSignal( + dbus.WithMatchObjectPath(notifyPath), + dbus.WithMatchInterface(notifyInterface), + ); err != nil { + return + } + + signals := make(chan *dbus.Signal, 10) + conn.Signal(signals) + + for sig := range signals { + switch sig.Name { + case notifyInterface + ".ActionInvoked": + if len(sig.Body) < 2 { + continue + } + id, ok := sig.Body[0].(uint32) + if !ok || id != uint32(notificationID) { + continue + } + openFile(filePath) + return + + case notifyInterface + ".NotificationClosed": + if len(sig.Body) < 1 { + continue + } + id, ok := sig.Body[0].(uint32) + if !ok || id != uint32(notificationID) { + continue + } + return + } + } +} + +func openFile(filePath string) { + cmd := exec.Command("xdg-open", filePath) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, + } + cmd.Start() +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/region.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/region.go new file mode 100644 index 0000000..f296434 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/region.go @@ -0,0 +1,859 @@ +package screenshot + +import ( + "fmt" + "sync" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/log" + "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/keyboard_shortcuts_inhibit" + "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_layer_shell" + "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy" + "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter" + wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client" + "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" +) + +type SelectionState struct { + hasSelection bool // There's a selection to display (pre-loaded or user-drawn) + dragging bool // User is actively drawing a new selection + surface *OutputSurface // Surface where selection was made + // Surface-local logical coordinates (from pointer events) + anchorX float64 + anchorY float64 + currentX float64 + currentY float64 +} + +type RenderSlot struct { + shm *ShmBuffer + pool *client.ShmPool + wlBuf *client.Buffer + busy bool +} + +type OutputSurface struct { + output *WaylandOutput + wlSurface *client.Surface + layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1 + viewport *wp_viewporter.WpViewport + screenBuf *ShmBuffer + screenBufNoCursor *ShmBuffer + screenFormat uint32 + logicalW int + logicalH int + configured bool + yInverted bool + + // Triple-buffered render slots + slots [3]*RenderSlot + slotsReady bool +} + +type PreCapture struct { + screenBuf *ShmBuffer + screenBufNoCursor *ShmBuffer + format uint32 + yInverted bool +} + +type RegionSelector struct { + screenshoter *Screenshoter + + display *client.Display + registry *client.Registry + ctx *client.Context + + compositor *client.Compositor + shm *client.Shm + seat *client.Seat + pointer *client.Pointer + keyboard *client.Keyboard + layerShell *wlr_layer_shell.ZwlrLayerShellV1 + screencopy *wlr_screencopy.ZwlrScreencopyManagerV1 + viewporter *wp_viewporter.WpViewporter + + shortcutsInhibitMgr *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1 + shortcutsInhibitor *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1 + + outputs map[uint32]*WaylandOutput + outputsMu sync.Mutex + preCapture map[*WaylandOutput]*PreCapture + + surfaces []*OutputSurface + activeSurface *OutputSurface + + // Cursor surface for crosshair + cursorSurface *client.Surface + cursorBuffer *ShmBuffer + cursorWlBuf *client.Buffer + cursorPool *client.ShmPool + + selection SelectionState + pointerX float64 + pointerY float64 + preSelect Region + showCapturedCursor bool + shiftHeld bool + + running bool + cancelled bool + result Region + + capturedBuffer *ShmBuffer + capturedRegion Region +} + +func NewRegionSelector(s *Screenshoter) *RegionSelector { + return &RegionSelector{ + screenshoter: s, + outputs: make(map[uint32]*WaylandOutput), + preCapture: make(map[*WaylandOutput]*PreCapture), + showCapturedCursor: s.config.Cursor == CursorOn, + } +} + +func (r *RegionSelector) Run() (*CaptureResult, bool, error) { + if r.screenshoter != nil && r.screenshoter.config.Reset { + r.preSelect = Region{} + } else { + r.preSelect = GetLastRegion() + } + + if err := r.connect(); err != nil { + return nil, false, fmt.Errorf("wayland connect: %w", err) + } + defer r.cleanup() + + if err := r.setupRegistry(); err != nil { + return nil, false, fmt.Errorf("registry setup: %w", err) + } + + if err := r.roundtrip(); err != nil { + return nil, false, fmt.Errorf("roundtrip after registry: %w", err) + } + + switch { + case r.screencopy == nil: + return nil, false, fmt.Errorf("compositor does not support wlr-screencopy-unstable-v1") + case r.layerShell == nil: + return nil, false, fmt.Errorf("compositor does not support wlr-layer-shell-unstable-v1") + case r.seat == nil: + return nil, false, fmt.Errorf("no seat available") + case r.compositor == nil: + return nil, false, fmt.Errorf("compositor not available") + case r.shm == nil: + return nil, false, fmt.Errorf("wl_shm not available") + case len(r.outputs) == 0: + return nil, false, fmt.Errorf("no outputs available") + } + + if err := r.roundtrip(); err != nil { + return nil, false, fmt.Errorf("roundtrip after protocol check: %w", err) + } + + if err := r.preCaptureAllOutputs(); err != nil { + return nil, false, fmt.Errorf("pre-capture: %w", err) + } + + if err := r.createSurfaces(); err != nil { + return nil, false, fmt.Errorf("create surfaces: %w", err) + } + + _ = r.createCursor() + + if err := r.roundtrip(); err != nil { + return nil, false, fmt.Errorf("roundtrip after surfaces: %w", err) + } + + r.running = true + for r.running { + if err := r.ctx.Dispatch(); err != nil { + return nil, false, fmt.Errorf("dispatch: %w", err) + } + } + + if r.cancelled || r.capturedBuffer == nil { + return nil, r.cancelled, nil + } + + yInverted := false + var format uint32 + if r.selection.surface != nil { + yInverted = r.selection.surface.yInverted + format = r.selection.surface.screenFormat + } + + return &CaptureResult{ + Buffer: r.capturedBuffer, + Region: r.result, + YInverted: yInverted, + Format: format, + }, false, nil +} + +func (r *RegionSelector) connect() error { + display, err := client.Connect("") + if err != nil { + return err + } + r.display = display + r.ctx = display.Context() + return nil +} + +func (r *RegionSelector) roundtrip() error { + return wlhelpers.Roundtrip(r.display, r.ctx) +} + +func (r *RegionSelector) setupRegistry() error { + registry, err := r.display.GetRegistry() + if err != nil { + return err + } + r.registry = registry + + registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) { + r.handleGlobal(e) + }) + + registry.SetGlobalRemoveHandler(func(e client.RegistryGlobalRemoveEvent) { + r.outputsMu.Lock() + delete(r.outputs, e.Name) + r.outputsMu.Unlock() + }) + + return nil +} + +func (r *RegionSelector) handleGlobal(e client.RegistryGlobalEvent) { + switch e.Interface { + case client.CompositorInterfaceName: + comp := client.NewCompositor(r.ctx) + if err := r.registry.Bind(e.Name, e.Interface, e.Version, comp); err == nil { + r.compositor = comp + } + + case client.ShmInterfaceName: + shm := client.NewShm(r.ctx) + if err := r.registry.Bind(e.Name, e.Interface, e.Version, shm); err == nil { + r.shm = shm + } + + case client.SeatInterfaceName: + seat := client.NewSeat(r.ctx) + if err := r.registry.Bind(e.Name, e.Interface, e.Version, seat); err == nil { + r.seat = seat + r.setupInput() + } + + case client.OutputInterfaceName: + output := client.NewOutput(r.ctx) + version := e.Version + if version > 4 { + version = 4 + } + if err := r.registry.Bind(e.Name, e.Interface, version, output); err == nil { + r.outputsMu.Lock() + r.outputs[e.Name] = &WaylandOutput{ + wlOutput: output, + globalName: e.Name, + scale: 1, + fractionalScale: 1.0, + } + r.outputsMu.Unlock() + r.setupOutputHandlers(e.Name, output) + } + + case wlr_layer_shell.ZwlrLayerShellV1InterfaceName: + ls := wlr_layer_shell.NewZwlrLayerShellV1(r.ctx) + version := e.Version + if version > 4 { + version = 4 + } + if err := r.registry.Bind(e.Name, e.Interface, version, ls); err == nil { + r.layerShell = ls + } + + case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName: + sc := wlr_screencopy.NewZwlrScreencopyManagerV1(r.ctx) + version := e.Version + if version > 3 { + version = 3 + } + if err := r.registry.Bind(e.Name, e.Interface, version, sc); err == nil { + r.screencopy = sc + } + + case wp_viewporter.WpViewporterInterfaceName: + vp := wp_viewporter.NewWpViewporter(r.ctx) + if err := r.registry.Bind(e.Name, e.Interface, e.Version, vp); err == nil { + r.viewporter = vp + } + + case keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1InterfaceName: + mgr := keyboard_shortcuts_inhibit.NewZwpKeyboardShortcutsInhibitManagerV1(r.ctx) + if err := r.registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil { + r.shortcutsInhibitMgr = mgr + } + } +} + +func (r *RegionSelector) setupOutputHandlers(name uint32, output *client.Output) { + output.SetGeometryHandler(func(e client.OutputGeometryEvent) { + r.outputsMu.Lock() + if o, ok := r.outputs[name]; ok { + o.x = e.X + o.y = e.Y + o.transform = int32(e.Transform) + } + r.outputsMu.Unlock() + }) + + output.SetModeHandler(func(e client.OutputModeEvent) { + if e.Flags&uint32(client.OutputModeCurrent) == 0 { + return + } + r.outputsMu.Lock() + if o, ok := r.outputs[name]; ok { + o.width = e.Width + o.height = e.Height + } + r.outputsMu.Unlock() + }) + + output.SetScaleHandler(func(e client.OutputScaleEvent) { + r.outputsMu.Lock() + if o, ok := r.outputs[name]; ok { + o.scale = e.Factor + o.fractionalScale = float64(e.Factor) + } + r.outputsMu.Unlock() + }) + + output.SetNameHandler(func(e client.OutputNameEvent) { + r.outputsMu.Lock() + if o, ok := r.outputs[name]; ok { + o.name = e.Name + } + r.outputsMu.Unlock() + }) +} + +func (r *RegionSelector) preCaptureAllOutputs() error { + r.outputsMu.Lock() + outputs := make([]*WaylandOutput, 0, len(r.outputs)) + for _, o := range r.outputs { + outputs = append(outputs, o) + } + r.outputsMu.Unlock() + + pending := len(outputs) * 2 + done := make(chan struct{}, pending) + + for _, output := range outputs { + pc := &PreCapture{} + r.preCapture[output] = pc + + r.preCaptureOutput(output, pc, true, func() { done <- struct{}{} }) + r.preCaptureOutput(output, pc, false, func() { done <- struct{}{} }) + } + + for i := 0; i < pending; i++ { + if err := r.ctx.Dispatch(); err != nil { + return err + } + select { + case <-done: + default: + i-- + } + } + return nil +} + +func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture, withCursor bool, onReady func()) { + cursor := int32(0) + if withCursor { + cursor = 1 + } + + frame, err := r.screencopy.CaptureOutput(cursor, output.wlOutput) + if err != nil { + log.Error("screencopy capture failed", "err", err) + onReady() + return + } + + var capturedBuf *ShmBuffer + + var capturedFormat PixelFormat + frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) { + capturedFormat = PixelFormat(e.Format) + bpp := capturedFormat.BytesPerPixel() + if int(e.Stride) < int(e.Width)*bpp { + log.Error("invalid stride from compositor", "stride", e.Stride, "width", e.Width, "bpp", bpp) + return + } + buf, err := CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride)) + if err != nil { + log.Error("create screen buffer failed", "err", err) + return + } + + capturedBuf = buf + buf.Format = capturedFormat + + pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size())) + if err != nil { + log.Error("create shm pool failed", "err", err) + return + } + + wlBuf, err := pool.CreateBuffer(0, int32(buf.Width), int32(buf.Height), int32(buf.Stride), e.Format) + if err != nil { + log.Error("create wl_buffer failed", "err", err) + pool.Destroy() + return + } + + if err := frame.Copy(wlBuf); err != nil { + log.Error("frame copy failed", "err", err) + } + pool.Destroy() + }) + + frame.SetFlagsHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FlagsEvent) { + if withCursor { + pc.yInverted = (e.Flags & 1) != 0 + } + }) + + frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) { + frame.Destroy() + + if capturedBuf == nil { + onReady() + return + } + + if capturedFormat.Is24Bit() { + converted, newFormat, err := capturedBuf.ConvertTo32Bit(capturedFormat) + if err != nil { + log.Error("convert 24-bit to 32-bit failed", "err", err) + } else if converted != capturedBuf { + capturedBuf.Close() + capturedBuf = converted + capturedFormat = newFormat + } + } + + pc.format = uint32(capturedFormat) + + if pc.yInverted { + capturedBuf.FlipVertical() + pc.yInverted = false + } + + if output.transform != TransformNormal { + invTransform := InverseTransform(output.transform) + transformed, err := capturedBuf.ApplyTransform(invTransform) + if err != nil { + log.Error("apply transform failed", "err", err) + } else if transformed != capturedBuf { + capturedBuf.Close() + capturedBuf = transformed + } + } + + if withCursor { + pc.screenBuf = capturedBuf + } else { + pc.screenBufNoCursor = capturedBuf + } + + onReady() + }) + + frame.SetFailedHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FailedEvent) { + log.Error("screencopy failed") + frame.Destroy() + onReady() + }) +} + +func (r *RegionSelector) createSurfaces() error { + r.outputsMu.Lock() + outputs := make([]*WaylandOutput, 0, len(r.outputs)) + for _, o := range r.outputs { + outputs = append(outputs, o) + } + r.outputsMu.Unlock() + + for _, output := range outputs { + os, err := r.createOutputSurface(output) + if err != nil { + return fmt.Errorf("output %s: %w", output.name, err) + } + r.surfaces = append(r.surfaces, os) + } + + return nil +} + +func (r *RegionSelector) createCursor() error { + const size = 24 + const hotspot = size / 2 + + surface, err := r.compositor.CreateSurface() + if err != nil { + return fmt.Errorf("create cursor surface: %w", err) + } + r.cursorSurface = surface + + buf, err := CreateShmBuffer(size, size, size*4) + if err != nil { + return fmt.Errorf("create cursor buffer: %w", err) + } + r.cursorBuffer = buf + + // Draw crosshair + data := buf.Data() + for y := 0; y < size; y++ { + for x := 0; x < size; x++ { + off := (y*size + x) * 4 + // Vertical line + if x >= hotspot-1 && x <= hotspot && y >= 2 && y < size-2 { + data[off+0] = 255 // B + data[off+1] = 255 // G + data[off+2] = 255 // R + data[off+3] = 255 // A + continue + } + // Horizontal line + if y >= hotspot-1 && y <= hotspot && x >= 2 && x < size-2 { + data[off+0] = 255 + data[off+1] = 255 + data[off+2] = 255 + data[off+3] = 255 + continue + } + // Transparent + data[off+0] = 0 + data[off+1] = 0 + data[off+2] = 0 + data[off+3] = 0 + } + } + + pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size())) + if err != nil { + return fmt.Errorf("create cursor pool: %w", err) + } + r.cursorPool = pool + + wlBuf, err := pool.CreateBuffer(0, size, size, size*4, uint32(FormatARGB8888)) + if err != nil { + return fmt.Errorf("create cursor wl_buffer: %w", err) + } + r.cursorWlBuf = wlBuf + + if err := surface.Attach(wlBuf, 0, 0); err != nil { + return fmt.Errorf("attach cursor: %w", err) + } + if err := surface.Damage(0, 0, size, size); err != nil { + return fmt.Errorf("damage cursor: %w", err) + } + if err := surface.Commit(); err != nil { + return fmt.Errorf("commit cursor: %w", err) + } + + return nil +} + +func (r *RegionSelector) createOutputSurface(output *WaylandOutput) (*OutputSurface, error) { + surface, err := r.compositor.CreateSurface() + if err != nil { + return nil, fmt.Errorf("create surface: %w", err) + } + + layerSurf, err := r.layerShell.GetLayerSurface( + surface, + output.wlOutput, + uint32(wlr_layer_shell.ZwlrLayerShellV1LayerOverlay), + "dms-screenshot", + ) + if err != nil { + return nil, fmt.Errorf("get layer surface: %w", err) + } + + os := &OutputSurface{ + output: output, + wlSurface: surface, + layerSurf: layerSurf, + } + + if r.viewporter != nil { + vp, err := r.viewporter.GetViewport(surface) + if err == nil { + os.viewport = vp + } + } + + if err := layerSurf.SetAnchor( + uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorTop) | + uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorBottom) | + uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorLeft) | + uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorRight), + ); err != nil { + return nil, fmt.Errorf("set anchor: %w", err) + } + if err := layerSurf.SetExclusiveZone(-1); err != nil { + return nil, fmt.Errorf("set exclusive zone: %w", err) + } + if err := layerSurf.SetKeyboardInteractivity(uint32(wlr_layer_shell.ZwlrLayerSurfaceV1KeyboardInteractivityExclusive)); err != nil { + return nil, fmt.Errorf("set keyboard interactivity: %w", err) + } + + layerSurf.SetConfigureHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ConfigureEvent) { + if err := layerSurf.AckConfigure(e.Serial); err != nil { + log.Error("ack configure failed", "err", err) + return + } + os.logicalW = int(e.Width) + os.logicalH = int(e.Height) + os.configured = true + r.captureForSurface(os) + r.ensureShortcutsInhibitor(os) + }) + + layerSurf.SetClosedHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ClosedEvent) { + r.running = false + r.cancelled = true + }) + + if err := surface.Commit(); err != nil { + return nil, fmt.Errorf("surface commit: %w", err) + } + + return os, nil +} + +func (r *RegionSelector) ensureShortcutsInhibitor(os *OutputSurface) { + if r.shortcutsInhibitMgr == nil || r.seat == nil || r.shortcutsInhibitor != nil { + return + } + inhibitor, err := r.shortcutsInhibitMgr.InhibitShortcuts(os.wlSurface, r.seat) + if err == nil { + r.shortcutsInhibitor = inhibitor + } +} + +func (r *RegionSelector) captureForSurface(os *OutputSurface) { + pc := r.preCapture[os.output] + if pc == nil { + return + } + + os.screenBuf = pc.screenBuf + os.screenBufNoCursor = pc.screenBufNoCursor + os.screenFormat = pc.format + os.yInverted = pc.yInverted + + if os.logicalW > 0 && os.screenBuf != nil { + os.output.fractionalScale = float64(os.screenBuf.Width) / float64(os.logicalW) + } + + r.initRenderBuffer(os) + r.applyPreSelection(os) + r.redrawSurface(os) +} + +func (r *RegionSelector) initRenderBuffer(os *OutputSurface) { + if os.screenBuf == nil { + return + } + + for i := 0; i < 3; i++ { + slot := &RenderSlot{} + + buf, err := CreateShmBuffer(os.screenBuf.Width, os.screenBuf.Height, os.screenBuf.Stride) + if err != nil { + log.Error("create render slot buffer failed", "err", err) + return + } + slot.shm = buf + + pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size())) + if err != nil { + log.Error("create render slot pool failed", "err", err) + buf.Close() + return + } + slot.pool = pool + + wlBuf, err := pool.CreateBuffer(0, int32(buf.Width), int32(buf.Height), int32(buf.Stride), os.screenFormat) + if err != nil { + log.Error("create render slot wl_buffer failed", "err", err) + pool.Destroy() + buf.Close() + return + } + slot.wlBuf = wlBuf + + slotRef := slot + wlBuf.SetReleaseHandler(func(e client.BufferReleaseEvent) { + slotRef.busy = false + }) + + os.slots[i] = slot + } + os.slotsReady = true +} + +func (os *OutputSurface) acquireFreeSlot() *RenderSlot { + for _, slot := range os.slots { + if slot != nil && !slot.busy { + return slot + } + } + return nil +} + +func (r *RegionSelector) applyPreSelection(os *OutputSurface) { + if r.preSelect.IsEmpty() || os.screenBuf == nil || r.selection.hasSelection { + return + } + + if r.preSelect.Output != "" && r.preSelect.Output != os.output.name { + return + } + + scaleX := float64(os.logicalW) / float64(os.screenBuf.Width) + scaleY := float64(os.logicalH) / float64(os.screenBuf.Height) + + x1 := float64(r.preSelect.X-os.output.x) * scaleX + y1 := float64(r.preSelect.Y-os.output.y) * scaleY + x2 := float64(r.preSelect.X-os.output.x+r.preSelect.Width) * scaleX + y2 := float64(r.preSelect.Y-os.output.y+r.preSelect.Height) * scaleY + + r.selection.hasSelection = true + r.selection.dragging = false + r.selection.surface = os + r.selection.anchorX = x1 + r.selection.anchorY = y1 + r.selection.currentX = x2 + r.selection.currentY = y2 + r.activeSurface = os +} + +func (r *RegionSelector) getSourceBuffer(os *OutputSurface) *ShmBuffer { + if !r.showCapturedCursor && os.screenBufNoCursor != nil { + return os.screenBufNoCursor + } + return os.screenBuf +} + +func (r *RegionSelector) redrawSurface(os *OutputSurface) { + srcBuf := r.getSourceBuffer(os) + if srcBuf == nil || !os.slotsReady { + return + } + + slot := os.acquireFreeSlot() + if slot == nil { + return + } + + slot.shm.CopyFrom(srcBuf) + + // Draw overlay (dimming + selection) into this slot + r.drawOverlay(os, slot.shm) + + if os.viewport != nil { + _ = os.wlSurface.SetBufferScale(1) + _ = os.viewport.SetSource(0, 0, float64(slot.shm.Width), float64(slot.shm.Height)) + _ = os.viewport.SetDestination(int32(os.logicalW), int32(os.logicalH)) + } else { + bufferScale := os.output.scale + if bufferScale <= 0 { + bufferScale = 1 + } + _ = os.wlSurface.SetBufferScale(bufferScale) + } + + _ = os.wlSurface.Attach(slot.wlBuf, 0, 0) + _ = os.wlSurface.Damage(0, 0, int32(os.logicalW), int32(os.logicalH)) + _ = os.wlSurface.Commit() + + // Mark this slot as busy until compositor releases it + slot.busy = true +} + +func (r *RegionSelector) cleanup() { + if r.cursorWlBuf != nil { + r.cursorWlBuf.Destroy() + } + if r.cursorPool != nil { + r.cursorPool.Destroy() + } + if r.cursorSurface != nil { + r.cursorSurface.Destroy() + } + if r.cursorBuffer != nil { + r.cursorBuffer.Close() + } + + for _, os := range r.surfaces { + for _, slot := range os.slots { + if slot == nil { + continue + } + if slot.wlBuf != nil { + slot.wlBuf.Destroy() + } + if slot.pool != nil { + slot.pool.Destroy() + } + if slot.shm != nil { + slot.shm.Close() + } + } + if os.viewport != nil { + os.viewport.Destroy() + } + if os.layerSurf != nil { + os.layerSurf.Destroy() + } + if os.wlSurface != nil { + os.wlSurface.Destroy() + } + if os.screenBuf != nil { + os.screenBuf.Close() + } + if os.screenBufNoCursor != nil { + os.screenBufNoCursor.Close() + } + } + + if r.shortcutsInhibitor != nil { + _ = r.shortcutsInhibitor.Destroy() + } + if r.shortcutsInhibitMgr != nil { + _ = r.shortcutsInhibitMgr.Destroy() + } + if r.viewporter != nil { + r.viewporter.Destroy() + } + if r.screencopy != nil { + r.screencopy.Destroy() + } + if r.pointer != nil { + r.pointer.Release() + } + if r.keyboard != nil { + r.keyboard.Release() + } + if r.display != nil { + r.ctx.Close() + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/region_input.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/region_input.go new file mode 100644 index 0000000..e243443 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/region_input.go @@ -0,0 +1,274 @@ +package screenshot + +import ( + "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" +) + +func (r *RegionSelector) setupInput() { + if r.seat == nil { + return + } + + r.seat.SetCapabilitiesHandler(func(e client.SeatCapabilitiesEvent) { + if e.Capabilities&uint32(client.SeatCapabilityPointer) != 0 && r.pointer == nil { + if pointer, err := r.seat.GetPointer(); err == nil { + r.pointer = pointer + r.setupPointerHandlers() + } + } + if e.Capabilities&uint32(client.SeatCapabilityKeyboard) != 0 && r.keyboard == nil { + if keyboard, err := r.seat.GetKeyboard(); err == nil { + r.keyboard = keyboard + r.setupKeyboardHandlers() + } + } + }) +} + +func (r *RegionSelector) setupPointerHandlers() { + r.pointer.SetEnterHandler(func(e client.PointerEnterEvent) { + if r.cursorSurface != nil { + _ = r.pointer.SetCursor(e.Serial, r.cursorSurface, 12, 12) + } + + r.activeSurface = nil + for _, os := range r.surfaces { + if os.wlSurface.ID() == e.Surface.ID() { + r.activeSurface = os + break + } + } + + r.pointerX = e.SurfaceX + r.pointerY = e.SurfaceY + }) + + r.pointer.SetMotionHandler(func(e client.PointerMotionEvent) { + if r.activeSurface == nil { + return + } + + r.pointerX = e.SurfaceX + r.pointerY = e.SurfaceY + + if !r.selection.dragging { + return + } + + curX, curY := e.SurfaceX, e.SurfaceY + if r.shiftHeld { + dx := curX - r.selection.anchorX + dy := curY - r.selection.anchorY + adx, ady := dx, dy + if adx < 0 { + adx = -adx + } + if ady < 0 { + ady = -ady + } + size := adx + if ady > adx { + size = ady + } + if dx < 0 { + curX = r.selection.anchorX - size + } else { + curX = r.selection.anchorX + size + } + if dy < 0 { + curY = r.selection.anchorY - size + } else { + curY = r.selection.anchorY + size + } + } + + r.selection.currentX = curX + r.selection.currentY = curY + for _, os := range r.surfaces { + r.redrawSurface(os) + } + }) + + r.pointer.SetButtonHandler(func(e client.PointerButtonEvent) { + if r.activeSurface == nil { + return + } + + switch e.Button { + case 0x110: // BTN_LEFT + switch e.State { + case 1: // pressed + r.preSelect = Region{} + r.selection.hasSelection = true + r.selection.dragging = true + r.selection.surface = r.activeSurface + r.selection.anchorX = r.pointerX + r.selection.anchorY = r.pointerY + r.selection.currentX = r.pointerX + r.selection.currentY = r.pointerY + for _, os := range r.surfaces { + r.redrawSurface(os) + } + case 0: // released + r.selection.dragging = false + for _, os := range r.surfaces { + r.redrawSurface(os) + } + if r.screenshoter != nil && r.screenshoter.config.NoConfirm && r.selection.hasSelection { + r.finishSelection() + } + } + default: + r.cancelled = true + r.running = false + } + }) +} + +func (r *RegionSelector) setupKeyboardHandlers() { + r.keyboard.SetModifiersHandler(func(e client.KeyboardModifiersEvent) { + r.shiftHeld = e.ModsDepressed&1 != 0 + }) + + r.keyboard.SetKeyHandler(func(e client.KeyboardKeyEvent) { + if e.State != 1 { + return + } + + switch e.Key { + case 1: + r.cancelled = true + r.running = false + case 25: + r.showCapturedCursor = !r.showCapturedCursor + for _, os := range r.surfaces { + r.redrawSurface(os) + } + case 28, 57, 96: + if r.selection.hasSelection { + r.finishSelection() + } + } + }) +} + +func (r *RegionSelector) finishSelection() { + if r.selection.surface == nil { + r.running = false + return + } + + os := r.selection.surface + srcBuf := r.getSourceBuffer(os) + if srcBuf == nil { + r.running = false + return + } + + x1, y1 := r.selection.anchorX, r.selection.anchorY + x2, y2 := r.selection.currentX, r.selection.currentY + + if x1 > x2 { + x1, x2 = x2, x1 + } + if y1 > y2 { + y1, y2 = y2, y1 + } + + scaleX, scaleY := 1.0, 1.0 + if os.logicalW > 0 { + scaleX = float64(srcBuf.Width) / float64(os.logicalW) + scaleY = float64(srcBuf.Height) / float64(os.logicalH) + } + + bx1 := int(x1 * scaleX) + by1 := int(y1 * scaleY) + bx2 := int(x2 * scaleX) + by2 := int(y2 * scaleY) + + // Clamp to buffer bounds + if bx1 < 0 { + bx1 = 0 + } + if by1 < 0 { + by1 = 0 + } + if bx2 > srcBuf.Width { + bx2 = srcBuf.Width + } + if by2 > srcBuf.Height { + by2 = srcBuf.Height + } + + w, h := bx2-bx1+1, by2-by1+1 + if r.shiftHeld && w != h { + if w < h { + h = w + } else { + w = h + } + } + if w < 1 { + w = 1 + } + if h < 1 { + h = 1 + } + + // Create cropped buffer and copy pixels directly + cropped, err := CreateShmBuffer(w, h, w*4) + if err != nil { + r.running = false + return + } + + srcData := srcBuf.Data() + dstData := cropped.Data() + for y := 0; y < h; y++ { + srcY := by1 + y + if os.yInverted { + srcY = srcBuf.Height - 1 - (by1 + y) + } + if srcY < 0 || srcY >= srcBuf.Height { + continue + } + dstY := y + if os.yInverted { + dstY = h - 1 - y + } + for x := 0; x < w; x++ { + srcX := bx1 + x + if srcX < 0 || srcX >= srcBuf.Width { + continue + } + si := srcY*srcBuf.Stride + srcX*4 + di := dstY*cropped.Stride + x*4 + if si+3 < len(srcData) && di+3 < len(dstData) { + dstData[di+0] = srcData[si+0] + dstData[di+1] = srcData[si+1] + dstData[di+2] = srcData[si+2] + dstData[di+3] = srcData[si+3] + } + } + } + + r.capturedBuffer = cropped + r.capturedRegion = Region{ + X: int32(bx1), + Y: int32(by1), + Width: int32(w), + Height: int32(h), + Output: os.output.name, + } + + // Also store for "last region" feature with global coords + r.result = Region{ + X: int32(bx1) + os.output.x, + Y: int32(by1) + os.output.y, + Width: int32(w), + Height: int32(h), + Output: os.output.name, + } + + r.running = false +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/region_render.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/region_render.go new file mode 100644 index 0000000..4071f8a --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/region_render.go @@ -0,0 +1,326 @@ +package screenshot + +import "fmt" + +var fontGlyphs = map[rune][12]uint8{ + '0': {0x3C, 0x66, 0x66, 0x6E, 0x76, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00}, + '1': {0x18, 0x38, 0x78, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00, 0x00}, + '2': {0x3C, 0x66, 0x66, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x66, 0x7E, 0x00, 0x00}, + '3': {0x3C, 0x66, 0x06, 0x06, 0x1C, 0x06, 0x06, 0x06, 0x66, 0x3C, 0x00, 0x00}, + '4': {0x0C, 0x1C, 0x3C, 0x6C, 0xCC, 0xCC, 0xFE, 0x0C, 0x0C, 0x1E, 0x00, 0x00}, + '5': {0x7E, 0x60, 0x60, 0x60, 0x7C, 0x06, 0x06, 0x06, 0x66, 0x3C, 0x00, 0x00}, + '6': {0x1C, 0x30, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00}, + '7': {0x7E, 0x66, 0x06, 0x06, 0x0C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00}, + '8': {0x3C, 0x66, 0x66, 0x66, 0x3C, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00}, + '9': {0x3C, 0x66, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x06, 0x0C, 0x38, 0x00, 0x00}, + 'x': {0x00, 0x00, 0x00, 0x66, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0x66, 0x00, 0x00}, + 'E': {0x7E, 0x60, 0x60, 0x60, 0x7C, 0x60, 0x60, 0x60, 0x60, 0x7E, 0x00, 0x00}, + 'P': {0x7C, 0x66, 0x66, 0x66, 0x7C, 0x60, 0x60, 0x60, 0x60, 0x60, 0x00, 0x00}, + 'S': {0x3C, 0x66, 0x60, 0x60, 0x3C, 0x06, 0x06, 0x06, 0x66, 0x3C, 0x00, 0x00}, + 'a': {0x00, 0x00, 0x00, 0x3C, 0x06, 0x3E, 0x66, 0x66, 0x66, 0x3E, 0x00, 0x00}, + 'c': {0x00, 0x00, 0x00, 0x3C, 0x66, 0x60, 0x60, 0x60, 0x66, 0x3C, 0x00, 0x00}, + 'd': {0x00, 0x00, 0x06, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x66, 0x3E, 0x00, 0x00}, + 'e': {0x00, 0x00, 0x00, 0x3C, 0x66, 0x66, 0x7E, 0x60, 0x60, 0x3C, 0x00, 0x00}, + 'h': {0x00, 0x60, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00}, + 'i': {0x00, 0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00}, + 'n': {0x00, 0x00, 0x00, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00}, + 'o': {0x00, 0x00, 0x00, 0x3C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00}, + 'p': {0x00, 0x00, 0x00, 0x7C, 0x66, 0x66, 0x66, 0x7C, 0x60, 0x60, 0x00, 0x00}, + 'r': {0x00, 0x00, 0x00, 0x6E, 0x76, 0x60, 0x60, 0x60, 0x60, 0x60, 0x00, 0x00}, + 's': {0x00, 0x00, 0x00, 0x3E, 0x60, 0x60, 0x3C, 0x06, 0x06, 0x7C, 0x00, 0x00}, + 't': {0x00, 0x18, 0x18, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x0E, 0x00, 0x00}, + 'u': {0x00, 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3E, 0x00, 0x00}, + 'w': {0x00, 0x00, 0x00, 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00, 0x00}, + 'l': {0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00}, + ' ': {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + ':': {0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00}, + '/': {0x00, 0x02, 0x06, 0x0C, 0x18, 0x18, 0x30, 0x60, 0x40, 0x00, 0x00, 0x00}, + '[': {0x3C, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3C, 0x00, 0x00}, + ']': {0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x3C, 0x00, 0x00}, +} + +type OverlayStyle struct { + BackgroundR, BackgroundG, BackgroundB, BackgroundA uint8 + TextR, TextG, TextB uint8 + AccentR, AccentG, AccentB uint8 +} + +var DefaultOverlayStyle = OverlayStyle{ + BackgroundR: 30, BackgroundG: 30, BackgroundB: 30, BackgroundA: 220, + TextR: 255, TextG: 255, TextB: 255, + AccentR: 100, AccentG: 180, AccentB: 255, +} + +func (r *RegionSelector) drawOverlay(os *OutputSurface, renderBuf *ShmBuffer) { + data := renderBuf.Data() + stride := renderBuf.Stride + w, h := renderBuf.Width, renderBuf.Height + format := os.screenFormat + + // Dim the entire buffer + for y := 0; y < h; y++ { + off := y * stride + for x := 0; x < w; x++ { + i := off + x*4 + if i+3 >= len(data) { + continue + } + data[i+0] = uint8(int(data[i+0]) * 3 / 5) + data[i+1] = uint8(int(data[i+1]) * 3 / 5) + data[i+2] = uint8(int(data[i+2]) * 3 / 5) + } + } + + r.drawHUD(data, stride, w, h, format) + + if !r.selection.hasSelection || r.selection.surface != os { + return + } + + scaleX := float64(w) / float64(os.logicalW) + scaleY := float64(h) / float64(os.logicalH) + + bx1 := int(r.selection.anchorX * scaleX) + by1 := int(r.selection.anchorY * scaleY) + bx2 := int(r.selection.currentX * scaleX) + by2 := int(r.selection.currentY * scaleY) + + if bx1 > bx2 { + bx1, bx2 = bx2, bx1 + } + if by1 > by2 { + by1, by2 = by2, by1 + } + + bx1 = clamp(bx1, 0, w-1) + by1 = clamp(by1, 0, h-1) + bx2 = clamp(bx2, 0, w-1) + by2 = clamp(by2, 0, h-1) + + srcBuf := r.getSourceBuffer(os) + srcData := srcBuf.Data() + for y := by1; y <= by2; y++ { + rowOff := y * stride + for x := bx1; x <= bx2; x++ { + si := y*srcBuf.Stride + x*4 + di := rowOff + x*4 + if si+3 >= len(srcData) || di+3 >= len(data) { + continue + } + data[di+0] = srcData[si+0] + data[di+1] = srcData[si+1] + data[di+2] = srcData[si+2] + data[di+3] = srcData[si+3] + } + } + + selW, selH := bx2-bx1+1, by2-by1+1 + if r.shiftHeld && selW != selH { + if selW < selH { + selH = selW + } else { + selW = selH + } + } + r.drawBorder(data, stride, w, h, bx1, by1, selW, selH, format) + r.drawDimensions(data, stride, w, h, bx1, by1, selW, selH, format) +} + +func (r *RegionSelector) drawHUD(data []byte, stride, bufW, bufH int, format uint32) { + if r.selection.dragging { + return + } + + style := LoadOverlayStyle() + const charW, charH, padding, itemSpacing = 8, 12, 12, 24 + + cursorLabel := "hide" + if !r.showCapturedCursor { + cursorLabel = "show" + } + captureKey := "Space/Enter" + if r.screenshoter != nil && r.screenshoter.config.NoConfirm { + captureKey = "Drag+Release" + } + + items := []struct{ key, desc string }{ + {captureKey, "capture"}, + {"P", cursorLabel + " cursor"}, + {"Esc", "cancel"}, + } + + totalW := 0 + for i, item := range items { + totalW += len(item.key)*(charW+1) + 4 + len(item.desc)*(charW+1) + if i < len(items)-1 { + totalW += itemSpacing + } + } + + hudW := totalW + padding*2 + hudH := charH + padding*2 + hudX := (bufW - hudW) / 2 + hudY := bufH - hudH - 20 + + r.fillRect(data, stride, bufW, bufH, hudX, hudY, hudW, hudH, + style.BackgroundR, style.BackgroundG, style.BackgroundB, style.BackgroundA, format) + + tx, ty := hudX+padding, hudY+padding + for i, item := range items { + r.drawText(data, stride, bufW, bufH, tx, ty, item.key, + style.AccentR, style.AccentG, style.AccentB, format) + tx += len(item.key) * (charW + 1) + + r.drawText(data, stride, bufW, bufH, tx, ty, " "+item.desc, + style.TextR, style.TextG, style.TextB, format) + tx += (1 + len(item.desc)) * (charW + 1) + + if i < len(items)-1 { + tx += itemSpacing + } + } +} + +func (r *RegionSelector) drawBorder(data []byte, stride, bufW, bufH, x, y, w, h int, format uint32) { + const thickness = 2 + for i := 0; i < thickness; i++ { + r.drawHLine(data, stride, bufW, bufH, x-i, y-i, w+2*i, format) + r.drawHLine(data, stride, bufW, bufH, x-i, y+h+i-1, w+2*i, format) + r.drawVLine(data, stride, bufW, bufH, x-i, y-i, h+2*i, format) + r.drawVLine(data, stride, bufW, bufH, x+w+i-1, y-i, h+2*i, format) + } +} + +func (r *RegionSelector) drawHLine(data []byte, stride, bufW, bufH, x, y, length int, _ uint32) { + if y < 0 || y >= bufH { + return + } + rowOff := y * stride + for i := 0; i < length; i++ { + px := x + i + if px < 0 || px >= bufW { + continue + } + off := rowOff + px*4 + if off+3 >= len(data) { + continue + } + data[off], data[off+1], data[off+2], data[off+3] = 255, 255, 255, 255 + } +} + +func (r *RegionSelector) drawVLine(data []byte, stride, bufW, bufH, x, y, length int, _ uint32) { + if x < 0 || x >= bufW { + return + } + for i := 0; i < length; i++ { + py := y + i + if py < 0 || py >= bufH { + continue + } + off := py*stride + x*4 + if off+3 >= len(data) { + continue + } + data[off], data[off+1], data[off+2], data[off+3] = 255, 255, 255, 255 + } +} + +func (r *RegionSelector) drawDimensions(data []byte, stride, bufW, bufH, x, y, w, h int, format uint32) { + text := fmt.Sprintf("%dx%d", w, h) + + const charW, charH = 8, 12 + textW := len(text) * (charW + 1) + textH := charH + + tx := x + (w-textW)/2 + ty := y + h + 8 + + if ty+textH > bufH { + ty = y - textH - 8 + } + tx = clamp(tx, 0, bufW-textW) + + r.fillRect(data, stride, bufW, bufH, tx-4, ty-2, textW+8, textH+4, 0, 0, 0, 200, format) + r.drawText(data, stride, bufW, bufH, tx, ty, text, 255, 255, 255, format) +} + +func (r *RegionSelector) fillRect(data []byte, stride, bufW, bufH, x, y, w, h int, cr, cg, cb, ca uint8, format uint32) { + alpha := float64(ca) / 255.0 + invAlpha := 1.0 - alpha + + c0, c2 := cb, cr + if format == uint32(FormatABGR8888) || format == uint32(FormatXBGR8888) { + c0, c2 = cr, cb + } + + for py := y; py < y+h && py < bufH; py++ { + if py < 0 { + continue + } + for px := x; px < x+w && px < bufW; px++ { + if px < 0 { + continue + } + off := py*stride + px*4 + if off+3 >= len(data) { + continue + } + data[off+0] = uint8(float64(data[off+0])*invAlpha + float64(c0)*alpha) + data[off+1] = uint8(float64(data[off+1])*invAlpha + float64(cg)*alpha) + data[off+2] = uint8(float64(data[off+2])*invAlpha + float64(c2)*alpha) + data[off+3] = 255 + } + } +} + +func (r *RegionSelector) drawText(data []byte, stride, bufW, bufH, x, y int, text string, cr, cg, cb uint8, format uint32) { + for i, ch := range text { + r.drawChar(data, stride, bufW, bufH, x+i*9, y, ch, cr, cg, cb, format) + } +} + +func (r *RegionSelector) drawChar(data []byte, stride, bufW, bufH, x, y int, ch rune, cr, cg, cb uint8, format uint32) { + glyph, ok := fontGlyphs[ch] + if !ok { + return + } + + c0, c2 := cb, cr + if format == uint32(FormatABGR8888) || format == uint32(FormatXBGR8888) { + c0, c2 = cr, cb + } + + for row := 0; row < 12; row++ { + py := y + row + if py < 0 || py >= bufH { + continue + } + bits := glyph[row] + for col := 0; col < 8; col++ { + if (bits & (1 << (7 - col))) == 0 { + continue + } + px := x + col + if px < 0 || px >= bufW { + continue + } + off := py*stride + px*4 + if off+3 >= len(data) { + continue + } + data[off], data[off+1], data[off+2], data[off+3] = c0, cg, c2, 255 + } + } +} + +func clamp(v, lo, hi int) int { + switch { + case v < lo: + return lo + case v > hi: + return hi + default: + return v + } +} 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 +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/shm.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/shm.go new file mode 100644 index 0000000..a178cf0 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/shm.go @@ -0,0 +1,35 @@ +package screenshot + +import "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm" + +type PixelFormat = shm.PixelFormat + +const ( + FormatARGB8888 = shm.FormatARGB8888 + FormatXRGB8888 = shm.FormatXRGB8888 + FormatABGR8888 = shm.FormatABGR8888 + FormatXBGR8888 = shm.FormatXBGR8888 + FormatRGB888 = shm.FormatRGB888 + FormatBGR888 = shm.FormatBGR888 +) + +const ( + TransformNormal = shm.TransformNormal + Transform90 = shm.Transform90 + Transform180 = shm.Transform180 + Transform270 = shm.Transform270 + TransformFlipped = shm.TransformFlipped + TransformFlipped90 = shm.TransformFlipped90 + TransformFlipped180 = shm.TransformFlipped180 + TransformFlipped270 = shm.TransformFlipped270 +) + +type ShmBuffer = shm.Buffer + +func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) { + return shm.CreateBuffer(width, height, stride) +} + +func InverseTransform(transform int32) int32 { + return shm.InverseTransform(transform) +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/state.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/state.go new file mode 100644 index 0000000..0ba6c63 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/state.go @@ -0,0 +1,65 @@ +package screenshot + +import ( + "encoding/json" + "os" + "path" + "path/filepath" +) + +type PersistentState struct { + LastRegion Region `json:"last_region"` +} + +func getStateFilePath() string { + cacheDir, err := os.UserCacheDir() + if err != nil { + cacheDir = path.Join(os.Getenv("HOME"), ".cache") + } + return filepath.Join(cacheDir, "dms", "screenshot-state.json") +} + +func LoadState() (*PersistentState, error) { + path := getStateFilePath() + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return &PersistentState{}, nil + } + return nil, err + } + + var state PersistentState + if err := json.Unmarshal(data, &state); err != nil { + return &PersistentState{}, nil + } + return &state, nil +} + +func SaveState(state *PersistentState) error { + path := getStateFilePath() + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0o755); err != nil { + return err + } + + data, err := json.MarshalIndent(state, "", " ") + if err != nil { + return err + } + return os.WriteFile(path, data, 0o644) +} + +func GetLastRegion() Region { + state, err := LoadState() + if err != nil { + return Region{} + } + return state.LastRegion +} + +func SaveLastRegion(r Region) error { + state, _ := LoadState() + state.LastRegion = r + return SaveState(state) +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/theme.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/theme.go new file mode 100644 index 0000000..a2bd5f6 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/theme.go @@ -0,0 +1,125 @@ +package screenshot + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/log" + "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" +) + +type ThemeColors struct { + Background string `json:"surface"` + OnSurface string `json:"on_surface"` + Primary string `json:"primary"` +} + +type ColorScheme struct { + Dark ThemeColors `json:"dark"` + Light ThemeColors `json:"light"` +} + +type ColorsFile struct { + Colors ColorScheme `json:"colors"` +} + +var cachedStyle *OverlayStyle + +func LoadOverlayStyle() OverlayStyle { + if cachedStyle != nil { + return *cachedStyle + } + + style := DefaultOverlayStyle + colors := loadColorsFile() + if colors == nil { + cachedStyle = &style + return style + } + + theme := &colors.Dark + if isLightMode() { + theme = &colors.Light + } + + if bg, ok := parseHexColor(theme.Background); ok { + style.BackgroundR, style.BackgroundG, style.BackgroundB = bg[0], bg[1], bg[2] + } + if text, ok := parseHexColor(theme.OnSurface); ok { + style.TextR, style.TextG, style.TextB = text[0], text[1], text[2] + } + if accent, ok := parseHexColor(theme.Primary); ok { + style.AccentR, style.AccentG, style.AccentB = accent[0], accent[1], accent[2] + } + + cachedStyle = &style + return style +} + +func loadColorsFile() *ColorScheme { + path := getColorsFilePath() + data, err := os.ReadFile(path) + if err != nil { + return nil + } + + var file ColorsFile + if err := json.Unmarshal(data, &file); err != nil { + return nil + } + + return &file.Colors +} + +func getColorsFilePath() string { + cacheDir, err := os.UserCacheDir() + if err != nil { + log.Error("Failed to get user cache dir", "err", err) + return "" + } + return filepath.Join(cacheDir, "DankMaterialShell", "dms-colors.json") +} + +func isLightMode() bool { + scheme, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme") + if err != nil { + return false + } + + switch scheme { + case "'prefer-light'", "'default'": + return true + } + return false +} + +func parseHexColor(hex string) ([3]uint8, bool) { + hex = strings.TrimPrefix(hex, "#") + if len(hex) != 6 { + return [3]uint8{}, false + } + + var r, g, b uint8 + for i, ptr := range []*uint8{&r, &g, &b} { + val := 0 + for j := 0; j < 2; j++ { + c := hex[i*2+j] + val *= 16 + switch { + case c >= '0' && c <= '9': + val += int(c - '0') + case c >= 'a' && c <= 'f': + val += int(c - 'a' + 10) + case c >= 'A' && c <= 'F': + val += int(c - 'A' + 10) + default: + return [3]uint8{}, false + } + } + *ptr = uint8(val) + } + + return [3]uint8{r, g, b}, true +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/types.go new file mode 100644 index 0000000..00cbe1c --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/screenshot/types.go @@ -0,0 +1,81 @@ +package screenshot + +type Mode int + +const ( + ModeRegion Mode = iota + ModeWindow + ModeFullScreen + ModeAllScreens + ModeOutput + ModeLastRegion +) + +type Format int + +const ( + FormatPNG Format = iota + FormatJPEG + FormatPPM +) + +type CursorMode int + +const ( + CursorOff CursorMode = iota + CursorOn +) + +type Region struct { + X int32 `json:"x"` + Y int32 `json:"y"` + Width int32 `json:"width"` + Height int32 `json:"height"` + Output string `json:"output,omitempty"` +} + +func (r Region) IsEmpty() bool { + return r.Width <= 0 || r.Height <= 0 +} + +type Output struct { + Name string + X, Y int32 + Width int32 + Height int32 + Scale int32 + FractionalScale float64 + Transform int32 +} + +type Config struct { + Mode Mode + OutputName string + Cursor CursorMode + NoConfirm bool + Reset bool + Format Format + Quality int + OutputDir string + Filename string + Clipboard bool + SaveFile bool + Notify bool + Stdout bool +} + +func DefaultConfig() Config { + return Config{ + Mode: ModeRegion, + Cursor: CursorOff, + NoConfirm: false, + Reset: false, + Format: FormatPNG, + Quality: 90, + OutputDir: "", + Filename: "", + Clipboard: true, + SaveFile: true, + Notify: true, + } +} |