mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-06 05:25:41 -05:00
Compare commits
19 Commits
ddda87c5a7
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ce78e7134 | ||
|
|
9ebfab2e78 | ||
|
|
833d245251 | ||
|
|
00d3024143 | ||
|
|
aedeab8a6a | ||
|
|
4d39169eb8 | ||
|
|
2ddc448150 | ||
|
|
f9a6b4ce2c | ||
|
|
22b2b69413 | ||
|
|
7f11632ea6 | ||
|
|
c0b4d5e2c2 | ||
|
|
2c23d0249c | ||
|
|
c3233fbf61 | ||
|
|
ecfc8e208c | ||
|
|
52d5e21fc4 | ||
|
|
6d0c56554f | ||
|
|
844e91dc9e | ||
|
|
1f00b5f577 | ||
|
|
2c48458384 |
@@ -472,5 +472,7 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
greeterCmd,
|
greeterCmd,
|
||||||
setupCmd,
|
setupCmd,
|
||||||
colorCmd,
|
colorCmd,
|
||||||
|
screenshotCmd,
|
||||||
|
notifyActionCmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
377
core/cmd/dms/commands_screenshot.go
Normal file
377
core/cmd/dms/commands_screenshot.go
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ssOutputName string
|
||||||
|
ssIncludeCursor bool
|
||||||
|
ssFormat string
|
||||||
|
ssQuality int
|
||||||
|
ssOutputDir string
|
||||||
|
ssFilename string
|
||||||
|
ssNoClipboard bool
|
||||||
|
ssNoFile bool
|
||||||
|
ssNoNotify bool
|
||||||
|
ssStdout bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var screenshotCmd = &cobra.Command{
|
||||||
|
Use: "screenshot",
|
||||||
|
Short: "Capture screenshots",
|
||||||
|
Long: `Capture screenshots from Wayland displays.
|
||||||
|
|
||||||
|
Modes:
|
||||||
|
region - Select a region interactively (default)
|
||||||
|
full - Capture the focused output
|
||||||
|
all - Capture all outputs combined
|
||||||
|
output - Capture a specific output by name
|
||||||
|
window - Capture the focused window (Hyprland only)
|
||||||
|
last - Capture the last selected region
|
||||||
|
|
||||||
|
Output format (--format):
|
||||||
|
png - PNG format (default)
|
||||||
|
jpg/jpeg - JPEG format
|
||||||
|
ppm - PPM format
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dms screenshot # Region select, save file + clipboard
|
||||||
|
dms screenshot full # Full screen of focused output
|
||||||
|
dms screenshot all # All screens combined
|
||||||
|
dms screenshot output -o DP-1 # Specific output
|
||||||
|
dms screenshot window # Focused window (Hyprland)
|
||||||
|
dms screenshot last # Last region (pre-selected)
|
||||||
|
dms screenshot --no-clipboard # Save file only
|
||||||
|
dms screenshot --no-file # Clipboard only
|
||||||
|
dms screenshot --cursor # Include cursor
|
||||||
|
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssRegionCmd = &cobra.Command{
|
||||||
|
Use: "region",
|
||||||
|
Short: "Select a region interactively",
|
||||||
|
Run: runScreenshotRegion,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssFullCmd = &cobra.Command{
|
||||||
|
Use: "full",
|
||||||
|
Short: "Capture the focused output",
|
||||||
|
Run: runScreenshotFull,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssAllCmd = &cobra.Command{
|
||||||
|
Use: "all",
|
||||||
|
Short: "Capture all outputs combined",
|
||||||
|
Run: runScreenshotAll,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssOutputCmd = &cobra.Command{
|
||||||
|
Use: "output",
|
||||||
|
Short: "Capture a specific output",
|
||||||
|
Run: runScreenshotOutput,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssLastCmd = &cobra.Command{
|
||||||
|
Use: "last",
|
||||||
|
Short: "Capture the last selected region",
|
||||||
|
Long: `Capture the previously selected region without interactive selection.
|
||||||
|
If no previous region exists, falls back to interactive selection.`,
|
||||||
|
Run: runScreenshotLast,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssWindowCmd = &cobra.Command{
|
||||||
|
Use: "window",
|
||||||
|
Short: "Capture the focused window",
|
||||||
|
Long: `Capture the currently focused window.
|
||||||
|
Currently only supported on Hyprland.`,
|
||||||
|
Run: runScreenshotWindow,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List available outputs",
|
||||||
|
Run: runScreenshotList,
|
||||||
|
}
|
||||||
|
|
||||||
|
var notifyActionCmd = &cobra.Command{
|
||||||
|
Use: "notify-action",
|
||||||
|
Hidden: true,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
screenshot.RunNotifyActionListener(args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssIncludeCursor, "cursor", false, "Include cursor in screenshot")
|
||||||
|
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
|
||||||
|
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
|
||||||
|
screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory")
|
||||||
|
screenshotCmd.PersistentFlags().StringVar(&ssFilename, "filename", "", "Output filename (auto-generated if empty)")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
|
||||||
|
|
||||||
|
screenshotCmd.AddCommand(ssRegionCmd)
|
||||||
|
screenshotCmd.AddCommand(ssFullCmd)
|
||||||
|
screenshotCmd.AddCommand(ssAllCmd)
|
||||||
|
screenshotCmd.AddCommand(ssOutputCmd)
|
||||||
|
screenshotCmd.AddCommand(ssLastCmd)
|
||||||
|
screenshotCmd.AddCommand(ssWindowCmd)
|
||||||
|
screenshotCmd.AddCommand(ssListCmd)
|
||||||
|
|
||||||
|
screenshotCmd.Run = runScreenshotRegion
|
||||||
|
}
|
||||||
|
|
||||||
|
func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
||||||
|
config := screenshot.DefaultConfig()
|
||||||
|
config.Mode = mode
|
||||||
|
config.OutputName = ssOutputName
|
||||||
|
config.IncludeCursor = ssIncludeCursor
|
||||||
|
config.Clipboard = !ssNoClipboard
|
||||||
|
config.SaveFile = !ssNoFile
|
||||||
|
config.Notify = !ssNoNotify
|
||||||
|
config.Stdout = ssStdout
|
||||||
|
|
||||||
|
if ssOutputDir != "" {
|
||||||
|
config.OutputDir = ssOutputDir
|
||||||
|
}
|
||||||
|
if ssFilename != "" {
|
||||||
|
config.Filename = ssFilename
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(ssFormat) {
|
||||||
|
case "jpg", "jpeg":
|
||||||
|
config.Format = screenshot.FormatJPEG
|
||||||
|
case "ppm":
|
||||||
|
config.Format = screenshot.FormatPPM
|
||||||
|
default:
|
||||||
|
config.Format = screenshot.FormatPNG
|
||||||
|
}
|
||||||
|
|
||||||
|
if ssQuality < 1 {
|
||||||
|
ssQuality = 1
|
||||||
|
}
|
||||||
|
if ssQuality > 100 {
|
||||||
|
ssQuality = 100
|
||||||
|
}
|
||||||
|
config.Quality = ssQuality
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshot(config screenshot.Config) {
|
||||||
|
sc := screenshot.New(config)
|
||||||
|
result, err := sc.Run()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result == nil {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer result.Buffer.Close()
|
||||||
|
|
||||||
|
if result.YInverted {
|
||||||
|
result.Buffer.FlipVertical()
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Stdout {
|
||||||
|
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error writing to stdout: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var filePath string
|
||||||
|
|
||||||
|
if config.SaveFile {
|
||||||
|
outputDir := config.OutputDir
|
||||||
|
if outputDir == "" {
|
||||||
|
outputDir = screenshot.GetOutputDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := config.Filename
|
||||||
|
if filename == "" {
|
||||||
|
filename = screenshot.GenerateFilename(config.Format)
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath = filepath.Join(outputDir, filename)
|
||||||
|
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Clipboard {
|
||||||
|
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error copying to clipboard: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if !config.SaveFile {
|
||||||
|
fmt.Println("Copied to clipboard")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Notify {
|
||||||
|
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format)
|
||||||
|
screenshot.SendNotification(screenshot.NotifyResult{
|
||||||
|
FilePath: filePath,
|
||||||
|
Clipboard: config.Clipboard,
|
||||||
|
ImageData: thumbData,
|
||||||
|
Width: thumbW,
|
||||||
|
Height: thumbH,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
|
||||||
|
var mimeType string
|
||||||
|
var data bytes.Buffer
|
||||||
|
|
||||||
|
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case screenshot.FormatJPEG:
|
||||||
|
mimeType = "image/jpeg"
|
||||||
|
if err := screenshot.EncodeJPEG(&data, img, quality); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
mimeType = "image/png"
|
||||||
|
if err := screenshot.EncodePNG(&data, img); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("wl-copy", "--type", mimeType)
|
||||||
|
cmd.Stdin = &data
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
|
||||||
|
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case screenshot.FormatJPEG:
|
||||||
|
return screenshot.EncodeJPEG(os.Stdout, img, quality)
|
||||||
|
default:
|
||||||
|
return screenshot.EncodePNG(os.Stdout, img)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat uint32) ([]byte, int, int) {
|
||||||
|
srcW, srcH := buf.Width, buf.Height
|
||||||
|
scale := 1.0
|
||||||
|
if srcW > maxSize || srcH > maxSize {
|
||||||
|
if srcW > srcH {
|
||||||
|
scale = float64(maxSize) / float64(srcW)
|
||||||
|
} else {
|
||||||
|
scale = float64(maxSize) / float64(srcH)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dstW := int(float64(srcW) * scale)
|
||||||
|
dstH := int(float64(srcH) * scale)
|
||||||
|
if dstW < 1 {
|
||||||
|
dstW = 1
|
||||||
|
}
|
||||||
|
if dstH < 1 {
|
||||||
|
dstH = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
data := buf.Data()
|
||||||
|
rgb := make([]byte, dstW*dstH*3)
|
||||||
|
swapRB := pixelFormat == uint32(screenshot.FormatARGB8888) || pixelFormat == uint32(screenshot.FormatXRGB8888) || pixelFormat == 0
|
||||||
|
|
||||||
|
for y := 0; y < dstH; y++ {
|
||||||
|
srcY := int(float64(y) / scale)
|
||||||
|
if srcY >= srcH {
|
||||||
|
srcY = srcH - 1
|
||||||
|
}
|
||||||
|
for x := 0; x < dstW; x++ {
|
||||||
|
srcX := int(float64(x) / scale)
|
||||||
|
if srcX >= srcW {
|
||||||
|
srcX = srcW - 1
|
||||||
|
}
|
||||||
|
si := srcY*buf.Stride + srcX*4
|
||||||
|
di := (y*dstW + x) * 3
|
||||||
|
if si+2 < len(data) {
|
||||||
|
if swapRB {
|
||||||
|
rgb[di+0] = data[si+2]
|
||||||
|
rgb[di+1] = data[si+1]
|
||||||
|
rgb[di+2] = data[si+0]
|
||||||
|
} else {
|
||||||
|
rgb[di+0] = data[si+0]
|
||||||
|
rgb[di+1] = data[si+1]
|
||||||
|
rgb[di+2] = data[si+2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rgb, dstW, dstH
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotRegion(cmd *cobra.Command, args []string) {
|
||||||
|
config := getScreenshotConfig(screenshot.ModeRegion)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotFull(cmd *cobra.Command, args []string) {
|
||||||
|
config := getScreenshotConfig(screenshot.ModeFullScreen)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotAll(cmd *cobra.Command, args []string) {
|
||||||
|
config := getScreenshotConfig(screenshot.ModeAllScreens)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotOutput(cmd *cobra.Command, args []string) {
|
||||||
|
if ssOutputName == "" && len(args) > 0 {
|
||||||
|
ssOutputName = args[0]
|
||||||
|
}
|
||||||
|
if ssOutputName == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error: output name required (use -o or provide as argument)")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
config := getScreenshotConfig(screenshot.ModeOutput)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotLast(cmd *cobra.Command, args []string) {
|
||||||
|
config := getScreenshotConfig(screenshot.ModeLastRegion)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotWindow(cmd *cobra.Command, args []string) {
|
||||||
|
config := getScreenshotConfig(screenshot.ModeWindow)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotList(cmd *cobra.Command, args []string) {
|
||||||
|
outputs, err := screenshot.ListOutputs()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range outputs {
|
||||||
|
fmt.Printf("%s: %dx%d+%d+%d (scale: %d)\n",
|
||||||
|
o.Name, o.Width, o.Height, o.X, o.Y, o.Scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_layer_shell"
|
"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/wlr_screencopy"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter"
|
"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"
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,15 +34,19 @@ type Output struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LayerSurface struct {
|
type LayerSurface struct {
|
||||||
output *Output
|
output *Output
|
||||||
state *SurfaceState
|
state *SurfaceState
|
||||||
wlSurface *client.Surface
|
wlSurface *client.Surface
|
||||||
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
|
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
|
||||||
viewport *wp_viewporter.WpViewport
|
viewport *wp_viewporter.WpViewport
|
||||||
wlPool *client.ShmPool
|
wlPool *client.ShmPool
|
||||||
wlBuffer *client.Buffer
|
wlBuffer *client.Buffer
|
||||||
configured bool
|
bufferBusy bool
|
||||||
hidden bool
|
oldPool *client.ShmPool
|
||||||
|
oldBuffer *client.Buffer
|
||||||
|
scopyBuffer *client.Buffer
|
||||||
|
configured bool
|
||||||
|
hidden bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Picker struct {
|
type Picker struct {
|
||||||
@@ -165,26 +170,7 @@ func (p *Picker) connect() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Picker) roundtrip() error {
|
func (p *Picker) roundtrip() error {
|
||||||
callback, err := p.display.Sync()
|
return wlhelpers.Roundtrip(p.display, p.ctx)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
callback.SetDoneHandler(func(e client.CallbackDoneEvent) {
|
|
||||||
close(done)
|
|
||||||
})
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
if err := p.ctx.Dispatch(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Picker) setupRegistry() error {
|
func (p *Picker) setupRegistry() error {
|
||||||
@@ -481,6 +467,12 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ls.scopyBuffer != nil {
|
||||||
|
ls.scopyBuffer.Destroy()
|
||||||
|
}
|
||||||
|
ls.scopyBuffer = wlBuffer
|
||||||
|
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {})
|
||||||
|
|
||||||
if err := frame.Copy(wlBuffer); err != nil {
|
if err := frame.Copy(wlBuffer); err != nil {
|
||||||
log.Error("failed to copy frame", "err", err)
|
log.Error("failed to copy frame", "err", err)
|
||||||
}
|
}
|
||||||
@@ -507,7 +499,6 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
|
|||||||
func (p *Picker) redrawSurface(ls *LayerSurface) {
|
func (p *Picker) redrawSurface(ls *LayerSurface) {
|
||||||
var renderBuf *ShmBuffer
|
var renderBuf *ShmBuffer
|
||||||
if ls.hidden {
|
if ls.hidden {
|
||||||
// When hidden, just show the screenshot without overlay
|
|
||||||
renderBuf = ls.state.RedrawScreenOnly()
|
renderBuf = ls.state.RedrawScreenOnly()
|
||||||
} else {
|
} else {
|
||||||
renderBuf = ls.state.Redraw()
|
renderBuf = ls.state.Redraw()
|
||||||
@@ -516,27 +507,38 @@ func (p *Picker) redrawSurface(ls *LayerSurface) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ls.wlPool != nil {
|
if ls.oldBuffer != nil {
|
||||||
ls.wlPool.Destroy()
|
ls.oldBuffer.Destroy()
|
||||||
ls.wlPool = nil
|
ls.oldBuffer = nil
|
||||||
}
|
}
|
||||||
if ls.wlBuffer != nil {
|
if ls.oldPool != nil {
|
||||||
ls.wlBuffer.Destroy()
|
ls.oldPool.Destroy()
|
||||||
ls.wlBuffer = nil
|
ls.oldPool = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ls.oldPool = ls.wlPool
|
||||||
|
ls.oldBuffer = ls.wlBuffer
|
||||||
|
ls.wlPool = nil
|
||||||
|
ls.wlBuffer = nil
|
||||||
|
|
||||||
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
|
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ls.wlPool = pool
|
ls.wlPool = pool
|
||||||
|
|
||||||
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(FormatARGB8888))
|
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ls.wlBuffer = wlBuffer
|
ls.wlBuffer = wlBuffer
|
||||||
|
|
||||||
|
lsRef := ls
|
||||||
|
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
|
||||||
|
lsRef.bufferBusy = false
|
||||||
|
})
|
||||||
|
ls.bufferBusy = true
|
||||||
|
|
||||||
logicalW, logicalH := ls.state.LogicalSize()
|
logicalW, logicalH := ls.state.LogicalSize()
|
||||||
if logicalW == 0 || logicalH == 0 {
|
if logicalW == 0 || logicalH == 0 {
|
||||||
logicalW = int(ls.output.width)
|
logicalW = int(ls.output.width)
|
||||||
@@ -551,30 +553,13 @@ func (p *Picker) redrawSurface(ls *LayerSurface) {
|
|||||||
if ls.viewport != nil {
|
if ls.viewport != nil {
|
||||||
srcW := float64(renderBuf.Width) / float64(scale)
|
srcW := float64(renderBuf.Width) / float64(scale)
|
||||||
srcH := float64(renderBuf.Height) / float64(scale)
|
srcH := float64(renderBuf.Height) / float64(scale)
|
||||||
if err := ls.viewport.SetSource(0, 0, srcW, srcH); err != nil {
|
_ = ls.viewport.SetSource(0, 0, srcW, srcH)
|
||||||
log.Warn("failed to set viewport source", "err", err)
|
_ = ls.viewport.SetDestination(int32(logicalW), int32(logicalH))
|
||||||
}
|
|
||||||
if err := ls.viewport.SetDestination(int32(logicalW), int32(logicalH)); err != nil {
|
|
||||||
log.Warn("failed to set viewport destination", "err", err)
|
|
||||||
}
|
|
||||||
if err := ls.wlSurface.SetBufferScale(scale); err != nil {
|
|
||||||
log.Warn("failed to set buffer scale", "err", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := ls.wlSurface.SetBufferScale(scale); err != nil {
|
|
||||||
log.Warn("failed to set buffer scale", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ls.wlSurface.Attach(wlBuffer, 0, 0); err != nil {
|
|
||||||
log.Warn("failed to attach buffer", "err", err)
|
|
||||||
}
|
|
||||||
if err := ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH)); err != nil {
|
|
||||||
log.Warn("failed to damage surface", "err", err)
|
|
||||||
}
|
|
||||||
if err := ls.wlSurface.Commit(); err != nil {
|
|
||||||
log.Warn("failed to commit surface", "err", err)
|
|
||||||
}
|
}
|
||||||
|
_ = ls.wlSurface.SetBufferScale(scale)
|
||||||
|
_ = ls.wlSurface.Attach(wlBuffer, 0, 0)
|
||||||
|
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
|
||||||
|
_ = ls.wlSurface.Commit()
|
||||||
|
|
||||||
ls.state.SwapBuffers()
|
ls.state.SwapBuffers()
|
||||||
}
|
}
|
||||||
@@ -617,9 +602,14 @@ func (p *Picker) setupPointerHandlers() {
|
|||||||
log.Debug("failed to hide cursor", "err", err)
|
log.Debug("failed to hide cursor", "err", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e.Surface == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
p.activeSurface = nil
|
p.activeSurface = nil
|
||||||
|
surfaceID := e.Surface.ID()
|
||||||
for _, ls := range p.surfaces {
|
for _, ls := range p.surfaces {
|
||||||
if ls.wlSurface.ID() == e.Surface.ID() {
|
if ls.wlSurface.ID() == surfaceID {
|
||||||
p.activeSurface = ls
|
p.activeSurface = ls
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -628,7 +618,6 @@ func (p *Picker) setupPointerHandlers() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If surface was hidden, mark it as visible again
|
|
||||||
if p.activeSurface.hidden {
|
if p.activeSurface.hidden {
|
||||||
p.activeSurface.hidden = false
|
p.activeSurface.hidden = false
|
||||||
}
|
}
|
||||||
@@ -638,8 +627,12 @@ func (p *Picker) setupPointerHandlers() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
|
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
|
||||||
|
if e.Surface == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
surfaceID := e.Surface.ID()
|
||||||
for _, ls := range p.surfaces {
|
for _, ls := range p.surfaces {
|
||||||
if ls.wlSurface.ID() == e.Surface.ID() {
|
if ls.wlSurface.ID() == surfaceID {
|
||||||
p.hideSurface(ls)
|
p.hideSurface(ls)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -672,6 +665,15 @@ func (p *Picker) setupKeyboardHandlers() {
|
|||||||
|
|
||||||
func (p *Picker) cleanup() {
|
func (p *Picker) cleanup() {
|
||||||
for _, ls := range p.surfaces {
|
for _, ls := range p.surfaces {
|
||||||
|
if ls.scopyBuffer != nil {
|
||||||
|
ls.scopyBuffer.Destroy()
|
||||||
|
}
|
||||||
|
if ls.oldBuffer != nil {
|
||||||
|
ls.oldBuffer.Destroy()
|
||||||
|
}
|
||||||
|
if ls.oldPool != nil {
|
||||||
|
ls.oldPool.Destroy()
|
||||||
|
}
|
||||||
if ls.wlBuffer != nil {
|
if ls.wlBuffer != nil {
|
||||||
ls.wlBuffer.Destroy()
|
ls.wlBuffer.Destroy()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,93 +1,40 @@
|
|||||||
package colorpicker
|
package colorpicker
|
||||||
|
|
||||||
import (
|
import "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
type ShmBuffer = shm.Buffer
|
||||||
)
|
|
||||||
|
|
||||||
type ShmBuffer struct {
|
|
||||||
fd int
|
|
||||||
data []byte
|
|
||||||
size int
|
|
||||||
Width int
|
|
||||||
Height int
|
|
||||||
Stride int
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
|
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
|
||||||
size := stride * height
|
return shm.CreateBuffer(width, height, stride)
|
||||||
|
|
||||||
fd, err := unix.MemfdCreate("dms-colorpicker", 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create memfd: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := unix.Ftruncate(fd, int64(size)); err != nil {
|
|
||||||
unix.Close(fd)
|
|
||||||
return nil, fmt.Errorf("ftruncate failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := unix.Mmap(fd, 0, size, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
|
|
||||||
if err != nil {
|
|
||||||
unix.Close(fd)
|
|
||||||
return nil, fmt.Errorf("mmap failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ShmBuffer{
|
|
||||||
fd: fd,
|
|
||||||
data: data,
|
|
||||||
size: size,
|
|
||||||
Width: width,
|
|
||||||
Height: height,
|
|
||||||
Stride: stride,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShmBuffer) Fd() int {
|
func GetPixelColor(buf *ShmBuffer, x, y int) Color {
|
||||||
return s.fd
|
return GetPixelColorWithFormat(buf, x, y, FormatARGB8888)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShmBuffer) Size() int {
|
func GetPixelColorWithFormat(buf *ShmBuffer, x, y int, format PixelFormat) Color {
|
||||||
return s.size
|
if x < 0 || x >= buf.Width || y < 0 || y >= buf.Height {
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ShmBuffer) Data() []byte {
|
|
||||||
return s.data
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ShmBuffer) GetPixel(x, y int) Color {
|
|
||||||
if x < 0 || x >= s.Width || y < 0 || y >= s.Height {
|
|
||||||
return Color{}
|
return Color{}
|
||||||
}
|
}
|
||||||
|
|
||||||
offset := y*s.Stride + x*4
|
data := buf.Data()
|
||||||
|
offset := y*buf.Stride + x*4
|
||||||
if offset+3 >= len(s.data) {
|
if offset+3 >= len(data) {
|
||||||
return Color{}
|
return Color{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if format == FormatABGR8888 || format == FormatXBGR8888 {
|
||||||
|
return Color{
|
||||||
|
R: data[offset],
|
||||||
|
G: data[offset+1],
|
||||||
|
B: data[offset+2],
|
||||||
|
A: data[offset+3],
|
||||||
|
}
|
||||||
|
}
|
||||||
return Color{
|
return Color{
|
||||||
B: s.data[offset],
|
B: data[offset],
|
||||||
G: s.data[offset+1],
|
G: data[offset+1],
|
||||||
R: s.data[offset+2],
|
R: data[offset+2],
|
||||||
A: s.data[offset+3],
|
A: data[offset+3],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShmBuffer) Close() error {
|
|
||||||
var firstErr error
|
|
||||||
if s.data != nil {
|
|
||||||
if err := unix.Munmap(s.data); err != nil && firstErr == nil {
|
|
||||||
firstErr = fmt.Errorf("munmap failed: %w", err)
|
|
||||||
}
|
|
||||||
s.data = nil
|
|
||||||
}
|
|
||||||
if s.fd >= 0 {
|
|
||||||
if err := unix.Close(s.fd); err != nil && firstErr == nil {
|
|
||||||
firstErr = fmt.Errorf("close fd failed: %w", err)
|
|
||||||
}
|
|
||||||
s.fd = -1
|
|
||||||
}
|
|
||||||
return firstErr
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,15 +4,17 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PixelFormat uint32
|
type PixelFormat = shm.PixelFormat
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FormatARGB8888 PixelFormat = 0
|
FormatARGB8888 = shm.FormatARGB8888
|
||||||
FormatXRGB8888 PixelFormat = 1
|
FormatXRGB8888 = shm.FormatXRGB8888
|
||||||
FormatABGR8888 PixelFormat = 0x34324241
|
FormatABGR8888 = shm.FormatABGR8888
|
||||||
FormatXBGR8888 PixelFormat = 0x34324258
|
FormatXBGR8888 = shm.FormatXBGR8888
|
||||||
)
|
)
|
||||||
|
|
||||||
type SurfaceState struct {
|
type SurfaceState struct {
|
||||||
@@ -98,6 +100,12 @@ func (s *SurfaceState) ScreenBuffer() *ShmBuffer {
|
|||||||
return s.screenBuf
|
return s.screenBuf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SurfaceState) ScreenFormat() PixelFormat {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return s.screenFormat
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SurfaceState) OnScreencopyFlags(flags uint32) {
|
func (s *SurfaceState) OnScreencopyFlags(flags uint32) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.yInverted = (flags & 1) != 0
|
s.yInverted = (flags & 1) != 0
|
||||||
@@ -253,7 +261,7 @@ func (s *SurfaceState) Redraw() *ShmBuffer {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
copy(dst.data, s.screenBuf.data)
|
dst.CopyFrom(s.screenBuf)
|
||||||
|
|
||||||
px := int(math.Round(float64(s.pointerX) * s.scaleX))
|
px := int(math.Round(float64(s.pointerX) * s.scaleX))
|
||||||
py := int(math.Round(float64(s.pointerY) * s.scaleY))
|
py := int(math.Round(float64(s.pointerY) * s.scaleY))
|
||||||
@@ -261,15 +269,15 @@ func (s *SurfaceState) Redraw() *ShmBuffer {
|
|||||||
px = clamp(px, 0, dst.Width-1)
|
px = clamp(px, 0, dst.Width-1)
|
||||||
py = clamp(py, 0, dst.Height-1)
|
py = clamp(py, 0, dst.Height-1)
|
||||||
|
|
||||||
picked := s.screenBuf.GetPixel(px, py)
|
picked := GetPixelColorWithFormat(s.screenBuf, px, py, s.screenFormat)
|
||||||
|
|
||||||
drawMagnifier(
|
drawMagnifier(
|
||||||
dst.data, dst.Stride, dst.Width, dst.Height,
|
dst.Data(), dst.Stride, dst.Width, dst.Height,
|
||||||
s.screenBuf.data, s.screenBuf.Stride, s.screenBuf.Width, s.screenBuf.Height,
|
s.screenBuf.Data(), s.screenBuf.Stride, s.screenBuf.Width, s.screenBuf.Height,
|
||||||
px, py, picked,
|
px, py, picked,
|
||||||
)
|
)
|
||||||
|
|
||||||
drawColorPreview(dst.data, dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase)
|
drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase)
|
||||||
|
|
||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
@@ -289,7 +297,7 @@ func (s *SurfaceState) RedrawScreenOnly() *ShmBuffer {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
copy(dst.data, s.screenBuf.data)
|
dst.CopyFrom(s.screenBuf)
|
||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +319,7 @@ func (s *SurfaceState) PickColor() (Color, bool) {
|
|||||||
sy = s.screenBuf.Height - 1 - sy
|
sy = s.screenBuf.Height - 1 - sy
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.screenBuf.GetPixel(sx, sy), true
|
return GetPixelColorWithFormat(s.screenBuf, sx, sy, s.screenFormat), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SurfaceState) Destroy() {
|
func (s *SurfaceState) Destroy() {
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
|
|||||||
dependencies = append(dependencies, a.detectWindowManager(wm))
|
dependencies = append(dependencies, a.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, a.detectQuickshell())
|
dependencies = append(dependencies, a.detectQuickshell())
|
||||||
dependencies = append(dependencies, a.detectXDGPortal())
|
dependencies = append(dependencies, a.detectXDGPortal())
|
||||||
dependencies = append(dependencies, a.detectPolkitAgent())
|
|
||||||
dependencies = append(dependencies, a.detectAccountsService())
|
dependencies = append(dependencies, a.detectAccountsService())
|
||||||
|
|
||||||
// Hyprland-specific tools
|
// Hyprland-specific tools
|
||||||
@@ -107,7 +106,6 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
|
|||||||
// Base detections (common across distros)
|
// Base detections (common across distros)
|
||||||
dependencies = append(dependencies, a.detectMatugen())
|
dependencies = append(dependencies, a.detectMatugen())
|
||||||
dependencies = append(dependencies, a.detectDgop())
|
dependencies = append(dependencies, a.detectDgop())
|
||||||
dependencies = append(dependencies, a.detectHyprpicker())
|
|
||||||
dependencies = append(dependencies, a.detectClipboardTools()...)
|
dependencies = append(dependencies, a.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
@@ -127,20 +125,6 @@ func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) detectPolkitAgent() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if a.packageInstalled("mate-polkit") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "mate-polkit",
|
|
||||||
Status: status,
|
|
||||||
Description: "PolicyKit authentication agent",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ArchDistribution) detectAccountsService() deps.Dependency {
|
func (a *ArchDistribution) detectAccountsService() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
status := deps.StatusMissing
|
||||||
if a.packageInstalled("accountsservice") {
|
if a.packageInstalled("accountsservice") {
|
||||||
@@ -178,18 +162,13 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
|||||||
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeSystem},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
packages["hyprland"] = a.getHyprlandMapping(variants["hyprland"])
|
packages["hyprland"] = a.getHyprlandMapping(variants["hyprland"])
|
||||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
|
||||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
|
||||||
packages["hyprctl"] = a.getHyprlandMapping(variants["hyprland"])
|
packages["hyprctl"] = a.getHyprlandMapping(variants["hyprland"])
|
||||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
|
||||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
packages["niri"] = a.getNiriMapping(variants["niri"])
|
packages["niri"] = a.getNiriMapping(variants["niri"])
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, d.detectWindowManager(wm))
|
dependencies = append(dependencies, d.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, d.detectQuickshell())
|
dependencies = append(dependencies, d.detectQuickshell())
|
||||||
dependencies = append(dependencies, d.detectXDGPortal())
|
dependencies = append(dependencies, d.detectXDGPortal())
|
||||||
dependencies = append(dependencies, d.detectPolkitAgent())
|
|
||||||
dependencies = append(dependencies, d.detectAccountsService())
|
dependencies = append(dependencies, d.detectAccountsService())
|
||||||
|
|
||||||
if wm == deps.WindowManagerNiri {
|
if wm == deps.WindowManagerNiri {
|
||||||
@@ -89,20 +88,6 @@ func (d *DebianDistribution) detectXDGPortal() deps.Dependency {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) detectPolkitAgent() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if d.packageInstalled("mate-polkit") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "mate-polkit",
|
|
||||||
Status: status,
|
|
||||||
Description: "PolicyKit authentication agent",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency {
|
func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
status := deps.StatusMissing
|
||||||
if d.commandExists("xwayland-satellite") {
|
if d.commandExists("xwayland-satellite") {
|
||||||
@@ -149,7 +134,6 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
|
|
||||||
// DMS packages from OBS with variant support
|
// DMS packages from OBS with variant support
|
||||||
@@ -158,9 +142,7 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"cliphist": {Name: "cliphist", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"cliphist": {Name: "cliphist", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
|
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
// Keep ghostty as manual (no OBS package yet)
|
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if wm == deps.WindowManagerNiri {
|
if wm == deps.WindowManagerNiri {
|
||||||
@@ -664,30 +646,6 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) installGhosttyDebian(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
d.log("Installing Ghostty using Debian installer script...")
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.1,
|
|
||||||
Step: "Running Ghostty Debian installer...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: "curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh | sudo bash",
|
|
||||||
LogOutput: "Installing Ghostty using pre-built Debian package",
|
|
||||||
}
|
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh)\"")
|
|
||||||
|
|
||||||
if err := d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.9); err != nil {
|
|
||||||
return fmt.Errorf("failed to install Ghostty: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.log("Ghostty installed successfully using Debian installer")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -697,10 +655,6 @@ func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages
|
|||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
switch pkg {
|
switch pkg {
|
||||||
case "ghostty":
|
|
||||||
if err := d.installGhosttyDebian(ctx, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to install ghostty: %w", err)
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
|
if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, f.detectWindowManager(wm))
|
dependencies = append(dependencies, f.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, f.detectQuickshell())
|
dependencies = append(dependencies, f.detectQuickshell())
|
||||||
dependencies = append(dependencies, f.detectXDGPortal())
|
dependencies = append(dependencies, f.detectXDGPortal())
|
||||||
dependencies = append(dependencies, f.detectPolkitAgent())
|
|
||||||
dependencies = append(dependencies, f.detectAccountsService())
|
dependencies = append(dependencies, f.detectAccountsService())
|
||||||
|
|
||||||
// Hyprland-specific tools
|
// Hyprland-specific tools
|
||||||
@@ -92,7 +91,6 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
// Base detections (common across distros)
|
// Base detections (common across distros)
|
||||||
dependencies = append(dependencies, f.detectMatugen())
|
dependencies = append(dependencies, f.detectMatugen())
|
||||||
dependencies = append(dependencies, f.detectDgop())
|
dependencies = append(dependencies, f.detectDgop())
|
||||||
dependencies = append(dependencies, f.detectHyprpicker())
|
|
||||||
dependencies = append(dependencies, f.detectClipboardTools()...)
|
dependencies = append(dependencies, f.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
@@ -112,20 +110,6 @@ func (f *FedoraDistribution) detectXDGPortal() deps.Dependency {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) detectPolkitAgent() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if f.packageInstalled("mate-polkit") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "mate-polkit",
|
|
||||||
Status: status,
|
|
||||||
Description: "PolicyKit authentication agent",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FedoraDistribution) packageInstalled(pkg string) bool {
|
func (f *FedoraDistribution) packageInstalled(pkg string) bool {
|
||||||
cmd := exec.Command("rpm", "-q", pkg)
|
cmd := exec.Command("rpm", "-q", pkg)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
@@ -145,9 +129,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
"hyprpicker": f.getHyprpickerMapping(variants["hyprland"]),
|
|
||||||
|
|
||||||
// COPR packages
|
// COPR packages
|
||||||
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
||||||
@@ -160,10 +142,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
packages["hyprland"] = f.getHyprlandMapping(variants["hyprland"])
|
packages["hyprland"] = f.getHyprlandMapping(variants["hyprland"])
|
||||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
|
||||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
|
||||||
packages["hyprctl"] = f.getHyprlandMapping(variants["hyprland"])
|
packages["hyprctl"] = f.getHyprlandMapping(variants["hyprland"])
|
||||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
|
||||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
packages["niri"] = f.getNiriMapping(variants["niri"])
|
packages["niri"] = f.getNiriMapping(variants["niri"])
|
||||||
@@ -194,13 +173,6 @@ func (f *FedoraDistribution) getHyprlandMapping(variant deps.PackageVariant) Pac
|
|||||||
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) getHyprpickerMapping(variant deps.PackageVariant) PackageMapping {
|
|
||||||
if variant == deps.VariantGit {
|
|
||||||
return PackageMapping{Name: "hyprpicker-git", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
|
||||||
}
|
|
||||||
return PackageMapping{Name: "hyprpicker", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
if variant == deps.VariantGit {
|
if variant == deps.VariantGit {
|
||||||
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
|
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
|
|
||||||
dependencies = append(dependencies, g.detectMatugen())
|
dependencies = append(dependencies, g.detectMatugen())
|
||||||
dependencies = append(dependencies, g.detectDgop())
|
dependencies = append(dependencies, g.detectDgop())
|
||||||
dependencies = append(dependencies, g.detectHyprpicker())
|
|
||||||
dependencies = append(dependencies, g.detectClipboardTools()...)
|
dependencies = append(dependencies, g.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
@@ -190,7 +189,6 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
|
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
|
||||||
"mate-polkit": {Name: "mate-extra/mate-polkit", Repository: RepoTypeSystem},
|
"mate-polkit": {Name: "mate-extra/mate-polkit", Repository: RepoTypeSystem},
|
||||||
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
|
||||||
"hyprpicker": g.getHyprpickerMapping(variants["hyprland"]),
|
|
||||||
|
|
||||||
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
|
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
|
||||||
"qtdeclarative": {Name: "dev-qt/qtdeclarative", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"},
|
"qtdeclarative": {Name: "dev-qt/qtdeclarative", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"},
|
||||||
@@ -207,10 +205,7 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
packages["hyprland"] = g.getHyprlandMapping(variants["hyprland"])
|
packages["hyprland"] = g.getHyprlandMapping(variants["hyprland"])
|
||||||
packages["grim"] = PackageMapping{Name: "gui-apps/grim", Repository: RepoTypeSystem}
|
|
||||||
packages["slurp"] = PackageMapping{Name: "gui-apps/slurp", Repository: RepoTypeSystem}
|
|
||||||
packages["hyprctl"] = g.getHyprlandMapping(variants["hyprland"])
|
packages["hyprctl"] = g.getHyprlandMapping(variants["hyprland"])
|
||||||
packages["grimblast"] = PackageMapping{Name: "gui-wm/hyprland-contrib", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}
|
|
||||||
packages["jq"] = PackageMapping{Name: "app-misc/jq", Repository: RepoTypeSystem}
|
packages["jq"] = PackageMapping{Name: "app-misc/jq", Repository: RepoTypeSystem}
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
packages["niri"] = g.getNiriMapping(variants["niri"])
|
packages["niri"] = g.getNiriMapping(variants["niri"])
|
||||||
@@ -236,10 +231,6 @@ func (g *GentooDistribution) getHyprlandMapping(variant deps.PackageVariant) Pac
|
|||||||
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: archKeyword}
|
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: archKeyword}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) getHyprpickerMapping(_ deps.PackageVariant) PackageMapping {
|
|
||||||
return PackageMapping{Name: "gui-apps/hyprpicker", Repository: RepoTypeGURU, AcceptKeywords: g.getArchKeyword()}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping {
|
func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping {
|
||||||
return PackageMapping{Name: "gui-wm/niri", Repository: RepoTypeGURU, UseFlags: "dbus screencast", AcceptKeywords: g.getArchKeyword()}
|
return PackageMapping{Name: "gui-wm/niri", Repository: RepoTypeGURU, UseFlags: "dbus screencast", AcceptKeywords: g.getArchKeyword()}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
|
|||||||
dependencies = append(dependencies, o.detectWindowManager(wm))
|
dependencies = append(dependencies, o.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, o.detectQuickshell())
|
dependencies = append(dependencies, o.detectQuickshell())
|
||||||
dependencies = append(dependencies, o.detectXDGPortal())
|
dependencies = append(dependencies, o.detectXDGPortal())
|
||||||
dependencies = append(dependencies, o.detectPolkitAgent())
|
|
||||||
dependencies = append(dependencies, o.detectAccountsService())
|
dependencies = append(dependencies, o.detectAccountsService())
|
||||||
|
|
||||||
// Hyprland-specific tools
|
// Hyprland-specific tools
|
||||||
@@ -101,20 +100,6 @@ func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) detectPolkitAgent() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if o.packageInstalled("mate-polkit") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "mate-polkit",
|
|
||||||
Status: status,
|
|
||||||
Description: "PolicyKit authentication agent",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
|
func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
|
||||||
cmd := exec.Command("rpm", "-q", pkg)
|
cmd := exec.Command("rpm", "-q", pkg)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
@@ -134,7 +119,6 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
||||||
|
|
||||||
@@ -148,10 +132,7 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
||||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
|
||||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
|
||||||
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
||||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
|
||||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
// Niri stable has native package support on openSUSE
|
// Niri stable has native package support on openSUSE
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ package distros
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
@@ -66,7 +64,6 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, u.detectWindowManager(wm))
|
dependencies = append(dependencies, u.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, u.detectQuickshell())
|
dependencies = append(dependencies, u.detectQuickshell())
|
||||||
dependencies = append(dependencies, u.detectXDGPortal())
|
dependencies = append(dependencies, u.detectXDGPortal())
|
||||||
dependencies = append(dependencies, u.detectPolkitAgent())
|
|
||||||
dependencies = append(dependencies, u.detectAccountsService())
|
dependencies = append(dependencies, u.detectAccountsService())
|
||||||
|
|
||||||
// Hyprland-specific tools
|
// Hyprland-specific tools
|
||||||
@@ -101,20 +98,6 @@ func (u *UbuntuDistribution) detectXDGPortal() deps.Dependency {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) detectPolkitAgent() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if u.packageInstalled("mate-polkit") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "mate-polkit",
|
|
||||||
Status: status,
|
|
||||||
Description: "PolicyKit authentication agent",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency {
|
func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
status := deps.StatusMissing
|
||||||
if u.commandExists("xwayland-satellite") {
|
if u.commandExists("xwayland-satellite") {
|
||||||
@@ -161,7 +144,6 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
|
|
||||||
// DMS packages from PPAs
|
// DMS packages from PPAs
|
||||||
@@ -170,19 +152,14 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"cliphist": {Name: "cliphist", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"cliphist": {Name: "cliphist", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
|
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
// Keep ghostty as manual (no PPA available)
|
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
// Use the cppiber PPA for Hyprland
|
// Use the cppiber PPA for Hyprland
|
||||||
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
|
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
|
||||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
|
||||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
|
||||||
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
|
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
|
||||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
|
||||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
niriVariant := variants["niri"]
|
niriVariant := variants["niri"]
|
||||||
@@ -577,10 +554,6 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
buildDeps["libxcb1-dev"] = true
|
buildDeps["libxcb1-dev"] = true
|
||||||
buildDeps["libpipewire-0.3-dev"] = true
|
buildDeps["libpipewire-0.3-dev"] = true
|
||||||
buildDeps["libpam0g-dev"] = true
|
buildDeps["libpam0g-dev"] = true
|
||||||
case "ghostty":
|
|
||||||
buildDeps["curl"] = true
|
|
||||||
buildDeps["libgtk-4-dev"] = true
|
|
||||||
buildDeps["libadwaita-1-dev"] = true
|
|
||||||
case "matugen":
|
case "matugen":
|
||||||
buildDeps["curl"] = true
|
buildDeps["curl"] = true
|
||||||
case "cliphist":
|
case "cliphist":
|
||||||
@@ -594,10 +567,6 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
if err := u.installRust(ctx, sudoPassword, progressChan); err != nil {
|
if err := u.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install Rust: %w", err)
|
return fmt.Errorf("failed to install Rust: %w", err)
|
||||||
}
|
}
|
||||||
case "ghostty":
|
|
||||||
if err := u.installZig(ctx, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to install Zig: %w", err)
|
|
||||||
}
|
|
||||||
case "cliphist", "dgop":
|
case "cliphist", "dgop":
|
||||||
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
|
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install Go: %w", err)
|
return fmt.Errorf("failed to install Go: %w", err)
|
||||||
@@ -661,40 +630,6 @@ func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword strin
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installZig(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
if u.commandExists("zig") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
homeDir, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
|
||||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
zigUrl := "https://ziglang.org/download/0.11.0/zig-linux-x86_64-0.11.0.tar.xz"
|
|
||||||
zigTmp := filepath.Join(cacheDir, "zig.tar.xz")
|
|
||||||
|
|
||||||
downloadCmd := exec.CommandContext(ctx, "curl", "-L", zigUrl, "-o", zigTmp)
|
|
||||||
if err := u.runWithProgress(downloadCmd, progressChan, PhaseSystemPackages, 0.84, 0.85); err != nil {
|
|
||||||
return fmt.Errorf("failed to download Zig: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
extractCmd := ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
fmt.Sprintf("tar -xf %s -C /opt/", zigTmp))
|
|
||||||
if err := u.runWithProgress(extractCmd, progressChan, PhaseSystemPackages, 0.85, 0.86); err != nil {
|
|
||||||
return fmt.Errorf("failed to extract Zig: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
linkCmd := ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"ln -sf /opt/zig-linux-x86_64-0.11.0/zig /usr/local/bin/zig")
|
|
||||||
return u.runWithProgress(linkCmd, progressChan, PhaseSystemPackages, 0.86, 0.87)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
if u.commandExists("go") {
|
if u.commandExists("go") {
|
||||||
return nil
|
return nil
|
||||||
@@ -742,30 +677,6 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installGhosttyUbuntu(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
u.log("Installing Ghostty using Ubuntu installer script...")
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.1,
|
|
||||||
Step: "Running Ghostty Ubuntu installer...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: "curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh | sudo bash",
|
|
||||||
LogOutput: "Installing Ghostty using pre-built Ubuntu package",
|
|
||||||
}
|
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh)\"")
|
|
||||||
|
|
||||||
if err := u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.9); err != nil {
|
|
||||||
return fmt.Errorf("failed to install Ghostty: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
u.log("Ghostty installed successfully using Ubuntu installer")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -775,10 +686,6 @@ func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages
|
|||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
switch pkg {
|
switch pkg {
|
||||||
case "ghostty":
|
|
||||||
if err := u.installGhosttyUbuntu(ctx, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to install ghostty: %w", err)
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
if err := u.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
|
if err := u.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
||||||
|
|||||||
69
core/internal/screenshot/compositor.go
Normal file
69
core/internal/screenshot/compositor.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Compositor int
|
||||||
|
|
||||||
|
const (
|
||||||
|
CompositorUnknown Compositor = iota
|
||||||
|
CompositorHyprland
|
||||||
|
)
|
||||||
|
|
||||||
|
func DetectCompositor() Compositor {
|
||||||
|
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
|
||||||
|
return CompositorHyprland
|
||||||
|
}
|
||||||
|
return CompositorUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowGeometry struct {
|
||||||
|
X int32
|
||||||
|
Y int32
|
||||||
|
Width int32
|
||||||
|
Height int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetActiveWindow() (*WindowGeometry, error) {
|
||||||
|
compositor := DetectCompositor()
|
||||||
|
|
||||||
|
switch compositor {
|
||||||
|
case CompositorHyprland:
|
||||||
|
return getHyprlandActiveWindow()
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("window capture requires Hyprland (other compositors not yet supported)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type hyprlandWindow struct {
|
||||||
|
At [2]int32 `json:"at"`
|
||||||
|
Size [2]int32 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHyprlandActiveWindow() (*WindowGeometry, error) {
|
||||||
|
cmd := exec.Command("hyprctl", "-j", "activewindow")
|
||||||
|
output, err := cmd.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
|
||||||
|
}
|
||||||
197
core/internal/screenshot/encode.go
Normal file
197
core/internal/screenshot/encode.go
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
swapRB := format == uint32(FormatARGB8888) || format == uint32(FormatXRGB8888) || format == 0
|
||||||
|
|
||||||
|
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, 0755); err == nil {
|
||||||
|
return screenshotDir
|
||||||
|
}
|
||||||
|
return xdgPics
|
||||||
|
}
|
||||||
|
|
||||||
|
if home := os.Getenv("HOME"); home != "" {
|
||||||
|
return home
|
||||||
|
}
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
|
||||||
|
func getXDGPicturesDir() string {
|
||||||
|
configDir := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
if configDir == "" {
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
if home == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
configDir = filepath.Join(home, ".config")
|
||||||
|
}
|
||||||
|
|
||||||
|
userDirsFile := filepath.Join(configDir, "user-dirs.dirs")
|
||||||
|
data, err := os.ReadFile(userDirsFile)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range splitLines(string(data)) {
|
||||||
|
if len(line) == 0 || line[0] == '#' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const prefix = "XDG_PICTURES_DIR="
|
||||||
|
if len(line) > len(prefix) && line[:len(prefix)] == prefix {
|
||||||
|
path := line[len(prefix):]
|
||||||
|
path = trimQuotes(path)
|
||||||
|
path = expandHome(path)
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitLines(s string) []string {
|
||||||
|
var lines []string
|
||||||
|
start := 0
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if s[i] == '\n' {
|
||||||
|
lines = append(lines, s[start:i])
|
||||||
|
start = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start < len(s) {
|
||||||
|
lines = append(lines, s[start:])
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimQuotes(s string) string {
|
||||||
|
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
|
||||||
|
return s[1 : len(s)-1]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandHome(path string) string {
|
||||||
|
if len(path) >= 5 && path[:5] == "$HOME" {
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
return home + path[5:]
|
||||||
|
}
|
||||||
|
if len(path) >= 1 && path[0] == '~' {
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
return home + path[1:]
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
180
core/internal/screenshot/notify.go
Normal file
180
core/internal/screenshot/notify.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
805
core/internal/screenshot/region.go
Normal file
805
core/internal/screenshot/region.go
Normal file
@@ -0,0 +1,805 @@
|
|||||||
|
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: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) Run() (*CaptureResult, bool, error) {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
|
||||||
|
buf, err := CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("create screen buffer failed", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if withCursor {
|
||||||
|
pc.screenBuf = buf
|
||||||
|
pc.format = e.Format
|
||||||
|
} else {
|
||||||
|
pc.screenBufNoCursor = buf
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Attach and commit (viewport only needs to be set once, but it's cheap)
|
||||||
|
scale := os.output.scale
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.viewport != nil {
|
||||||
|
srcW := float64(slot.shm.Width) / float64(scale)
|
||||||
|
srcH := float64(slot.shm.Height) / float64(scale)
|
||||||
|
_ = os.viewport.SetSource(0, 0, srcW, srcH)
|
||||||
|
_ = os.viewport.SetDestination(int32(os.logicalW), int32(os.logicalH))
|
||||||
|
}
|
||||||
|
_ = os.wlSurface.SetBufferScale(scale)
|
||||||
|
|
||||||
|
_ = 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
264
core/internal/screenshot/region_input.go
Normal file
264
core/internal/screenshot/region_input.go
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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:
|
||||||
|
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 srcY >= srcBuf.Height {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for x := 0; x < w; x++ {
|
||||||
|
srcX := bx1 + x
|
||||||
|
if srcX >= srcBuf.Width {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
si := srcY*srcBuf.Stride + srcX*4
|
||||||
|
di := y*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
|
||||||
|
}
|
||||||
322
core/internal/screenshot/region_render.go
Normal file
322
core/internal/screenshot/region_render.go
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
items := []struct{ key, desc string }{
|
||||||
|
{"Space/Enter", "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
|
||||||
|
}
|
||||||
|
}
|
||||||
613
core/internal/screenshot/screenshot.go
Normal file
613
core/internal/screenshot/screenshot.go
Normal file
@@ -0,0 +1,613 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"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
|
||||||
|
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) {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
output := s.findOutputForRegion(region)
|
||||||
|
if output == nil {
|
||||||
|
return nil, fmt.Errorf("could not find output for window")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.captureRegionOnOutput(output, region)
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
var minX, minY, maxX, maxY int32
|
||||||
|
first := true
|
||||||
|
|
||||||
|
for _, o := range s.outputs {
|
||||||
|
outputs = append(outputs, o)
|
||||||
|
right := o.x + o.width
|
||||||
|
bottom := o.y + o.height
|
||||||
|
|
||||||
|
if first {
|
||||||
|
minX, minY = o.x, o.y
|
||||||
|
maxX, maxY = right, bottom
|
||||||
|
first = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.x < minX {
|
||||||
|
minX = o.x
|
||||||
|
}
|
||||||
|
if o.y < minY {
|
||||||
|
minY = o.y
|
||||||
|
}
|
||||||
|
if right > maxX {
|
||||||
|
maxX = right
|
||||||
|
}
|
||||||
|
if bottom > maxY {
|
||||||
|
maxY = bottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.outputsMu.Unlock()
|
||||||
|
|
||||||
|
if len(outputs) == 0 {
|
||||||
|
return nil, fmt.Errorf("no outputs available")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(outputs) == 1 {
|
||||||
|
return s.captureWholeOutput(outputs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
totalW := maxX - minX
|
||||||
|
totalH := maxY - minY
|
||||||
|
|
||||||
|
compositeStride := int(totalW) * 4
|
||||||
|
composite, err := CreateShmBuffer(int(totalW), int(totalH), compositeStride)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create composite buffer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
composite.Clear()
|
||||||
|
|
||||||
|
var format uint32
|
||||||
|
for _, output := range outputs {
|
||||||
|
result, err := s.captureWholeOutput(output)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("failed to capture output", "name", output.name, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if format == 0 {
|
||||||
|
format = result.Format
|
||||||
|
}
|
||||||
|
s.blitBuffer(composite, result.Buffer, int(output.x-minX), int(output.y-minY), result.YInverted)
|
||||||
|
result.Buffer.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CaptureResult{
|
||||||
|
Buffer: composite,
|
||||||
|
Region: Region{X: minX, Y: minY, Width: totalW, Height: totalH},
|
||||||
|
Format: format,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted bool) {
|
||||||
|
srcData := src.Data()
|
||||||
|
dstData := dst.Data()
|
||||||
|
|
||||||
|
for srcY := 0; srcY < src.Height; srcY++ {
|
||||||
|
actualSrcY := srcY
|
||||||
|
if yInverted {
|
||||||
|
actualSrcY = src.Height - 1 - srcY
|
||||||
|
}
|
||||||
|
|
||||||
|
dy := dstY + srcY
|
||||||
|
if dy < 0 || dy >= dst.Height {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
srcRowOff := actualSrcY * src.Stride
|
||||||
|
dstRowOff := dy * dst.Stride
|
||||||
|
|
||||||
|
for srcX := 0; srcX < src.Width; srcX++ {
|
||||||
|
dx := dstX + srcX
|
||||||
|
if dx < 0 || dx >= dst.Width {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
si := srcRowOff + srcX*4
|
||||||
|
di := dstRowOff + dx*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(0)
|
||||||
|
if s.config.IncludeCursor {
|
||||||
|
cursor = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
frame, err := s.screencopy.CaptureOutput(cursor, output.wlOutput)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("capture output: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.processFrame(frame, Region{
|
||||||
|
X: output.x,
|
||||||
|
Y: output.y,
|
||||||
|
Width: output.width,
|
||||||
|
Height: output.height,
|
||||||
|
Output: output.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
|
||||||
|
localX := region.X - output.x
|
||||||
|
localY := region.Y - output.y
|
||||||
|
|
||||||
|
cursor := int32(0)
|
||||||
|
if s.config.IncludeCursor {
|
||||||
|
cursor = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
frame, err := s.screencopy.CaptureOutputRegion(
|
||||||
|
cursor,
|
||||||
|
output.wlOutput,
|
||||||
|
localX, localY,
|
||||||
|
region.Width, region.Height,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("capture region: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.processFrame(frame, region)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1, region Region) (*CaptureResult, error) {
|
||||||
|
var buf *ShmBuffer
|
||||||
|
var format PixelFormat
|
||||||
|
var yInverted bool
|
||||||
|
ready := false
|
||||||
|
failed := false
|
||||||
|
|
||||||
|
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
format = PixelFormat(e.Format)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.Destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
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 failed {
|
||||||
|
if buf != nil {
|
||||||
|
buf.Close()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("frame capture failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CaptureResult{
|
||||||
|
Buffer: buf,
|
||||||
|
Region: region,
|
||||||
|
YInverted: yInverted,
|
||||||
|
Format: uint32(format),
|
||||||
|
}, 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 {
|
||||||
|
if cx >= o.x && cx < o.x+o.width && cy >= o.y && cy < o.y+o.height {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range s.outputs {
|
||||||
|
if region.X >= o.x && region.X < o.x+o.width &&
|
||||||
|
region.Y >= o.y && region.Y < o.y+o.height {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) findFocusedOutput() *WaylandOutput {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
|
||||||
|
result := make([]Output, 0, len(sc.outputs))
|
||||||
|
for _, o := range sc.outputs {
|
||||||
|
result = append(result, Output{
|
||||||
|
Name: o.name,
|
||||||
|
X: o.x,
|
||||||
|
Y: o.y,
|
||||||
|
Width: o.width,
|
||||||
|
Height: o.height,
|
||||||
|
Scale: o.scale,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
18
core/internal/screenshot/shm.go
Normal file
18
core/internal/screenshot/shm.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
type ShmBuffer = shm.Buffer
|
||||||
|
|
||||||
|
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
|
||||||
|
return shm.CreateBuffer(width, height, stride)
|
||||||
|
}
|
||||||
65
core/internal/screenshot/state.go
Normal file
65
core/internal/screenshot/state.go
Normal file
@@ -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, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(state, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(path, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
127
core/internal/screenshot/theme.go
Normal file
127
core/internal/screenshot/theme.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 := os.Getenv("XDG_CACHE_HOME")
|
||||||
|
if cacheDir == "" {
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
if home == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
cacheDir = filepath.Join(home, ".cache")
|
||||||
|
}
|
||||||
|
return filepath.Join(cacheDir, "DankMaterialShell", "dms-colors.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLightMode() bool {
|
||||||
|
out, err := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "color-scheme").Output()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
scheme := strings.TrimSpace(string(out))
|
||||||
|
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
|
||||||
|
}
|
||||||
68
core/internal/screenshot/types.go
Normal file
68
core/internal/screenshot/types.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
type Mode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModeRegion Mode = iota
|
||||||
|
ModeWindow
|
||||||
|
ModeFullScreen
|
||||||
|
ModeAllScreens
|
||||||
|
ModeOutput
|
||||||
|
ModeLastRegion
|
||||||
|
)
|
||||||
|
|
||||||
|
type Format int
|
||||||
|
|
||||||
|
const (
|
||||||
|
FormatPNG Format = iota
|
||||||
|
FormatJPEG
|
||||||
|
FormatPPM
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Mode Mode
|
||||||
|
OutputName string
|
||||||
|
IncludeCursor bool
|
||||||
|
Format Format
|
||||||
|
Quality int
|
||||||
|
OutputDir string
|
||||||
|
Filename string
|
||||||
|
Clipboard bool
|
||||||
|
SaveFile bool
|
||||||
|
Notify bool
|
||||||
|
Stdout bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultConfig() Config {
|
||||||
|
return Config{
|
||||||
|
Mode: ModeRegion,
|
||||||
|
IncludeCursor: false,
|
||||||
|
Format: FormatPNG,
|
||||||
|
Quality: 90,
|
||||||
|
OutputDir: "",
|
||||||
|
Filename: "",
|
||||||
|
Clipboard: true,
|
||||||
|
SaveFile: true,
|
||||||
|
Notify: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,10 @@ func (b *DDCBackend) scanI2CDevices() error {
|
|||||||
return b.scanI2CDevicesInternal(false)
|
return b.scanI2CDevicesInternal(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *DDCBackend) ForceRescan() error {
|
||||||
|
return b.scanI2CDevicesInternal(true)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *DDCBackend) scanI2CDevicesInternal(force bool) error {
|
func (b *DDCBackend) scanI2CDevicesInternal(force bool) error {
|
||||||
b.scanMutex.Lock()
|
b.scanMutex.Lock()
|
||||||
defer b.scanMutex.Unlock()
|
defer b.scanMutex.Unlock()
|
||||||
@@ -64,10 +68,6 @@ func (b *DDCBackend) scanI2CDevicesInternal(force bool) error {
|
|||||||
activeBuses[i] = true
|
activeBuses[i] = true
|
||||||
id := fmt.Sprintf("ddc:i2c-%d", i)
|
id := fmt.Sprintf("ddc:i2c-%d", i)
|
||||||
|
|
||||||
if _, exists := b.devices.Load(id); exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
dev, err := b.probeDDCDevice(i)
|
dev, err := b.probeDDCDevice(i)
|
||||||
if err != nil || dev == nil {
|
if err != nil || dev == nil {
|
||||||
continue
|
continue
|
||||||
@@ -261,8 +261,16 @@ func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) er
|
|||||||
|
|
||||||
busPath := fmt.Sprintf("/dev/i2c-%d", dev.bus)
|
busPath := fmt.Sprintf("/dev/i2c-%d", dev.bus)
|
||||||
|
|
||||||
|
if _, err := os.Stat(busPath); os.IsNotExist(err) {
|
||||||
|
b.devices.Delete(id)
|
||||||
|
log.Debugf("removed stale DDC device %s (bus no longer exists)", id)
|
||||||
|
return fmt.Errorf("device disconnected: %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
fd, err := syscall.Open(busPath, syscall.O_RDWR, 0)
|
fd, err := syscall.Open(busPath, syscall.O_RDWR, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
b.devices.Delete(id)
|
||||||
|
log.Debugf("removed DDC device %s (open failed: %v)", id, err)
|
||||||
return fmt.Errorf("open i2c device: %w", err)
|
return fmt.Errorf("open i2c device: %w", err)
|
||||||
}
|
}
|
||||||
defer syscall.Close(fd)
|
defer syscall.Close(fd)
|
||||||
|
|||||||
@@ -89,6 +89,13 @@ func (m *Manager) initDDC() {
|
|||||||
|
|
||||||
func (m *Manager) Rescan() {
|
func (m *Manager) Rescan() {
|
||||||
log.Debug("Rescanning brightness devices...")
|
log.Debug("Rescanning brightness devices...")
|
||||||
|
|
||||||
|
if m.ddcReady && m.ddcBackend != nil {
|
||||||
|
if err := m.ddcBackend.ForceRescan(); err != nil {
|
||||||
|
log.Debugf("DDC force rescan failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m.updateState()
|
m.updateState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,18 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/pilebones/go-udev/netlink"
|
"github.com/pilebones/go-udev/netlink"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UdevMonitor struct {
|
type UdevMonitor struct {
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
|
rescanMutex sync.Mutex
|
||||||
|
rescanTimer *time.Timer
|
||||||
|
rescanPending bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUdevMonitor(manager *Manager) *UdevMonitor {
|
func NewUdevMonitor(manager *Manager) *UdevMonitor {
|
||||||
@@ -34,10 +39,8 @@ func (m *UdevMonitor) run(manager *Manager) {
|
|||||||
matcher := &netlink.RuleDefinitions{
|
matcher := &netlink.RuleDefinitions{
|
||||||
Rules: []netlink.RuleDefinition{
|
Rules: []netlink.RuleDefinition{
|
||||||
{Env: map[string]string{"SUBSYSTEM": "backlight"}},
|
{Env: map[string]string{"SUBSYSTEM": "backlight"}},
|
||||||
// ! TODO: most drivers dont emit this for leds?
|
{Env: map[string]string{"SUBSYSTEM": "drm"}},
|
||||||
// ! inotify brightness_hw_changed works, but thn some devices dont do that...
|
{Env: map[string]string{"SUBSYSTEM": "i2c"}},
|
||||||
// ! So for now the GUI just shows OSDs for leds, without reflecting actual HW value
|
|
||||||
// {Env: map[string]string{"SUBSYSTEM": "leds"}},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if err := matcher.Compile(); err != nil {
|
if err := matcher.Compile(); err != nil {
|
||||||
@@ -49,7 +52,7 @@ func (m *UdevMonitor) run(manager *Manager) {
|
|||||||
errs := make(chan error)
|
errs := make(chan error)
|
||||||
conn.Monitor(events, errs, matcher)
|
conn.Monitor(events, errs, matcher)
|
||||||
|
|
||||||
log.Info("Udev monitor started for backlight/leds events")
|
log.Info("Udev monitor started for backlight/drm/i2c events")
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -75,11 +78,54 @@ func (m *UdevMonitor) handleEvent(manager *Manager, event netlink.UEvent) {
|
|||||||
sysname := filepath.Base(devpath)
|
sysname := filepath.Base(devpath)
|
||||||
action := string(event.Action)
|
action := string(event.Action)
|
||||||
|
|
||||||
|
switch subsystem {
|
||||||
|
case "drm", "i2c":
|
||||||
|
m.handleDisplayEvent(manager, action, subsystem, sysname)
|
||||||
|
case "backlight":
|
||||||
|
m.handleBacklightEvent(manager, action, sysname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UdevMonitor) handleDisplayEvent(manager *Manager, action, subsystem, sysname string) {
|
||||||
|
switch action {
|
||||||
|
case "add", "remove", "change":
|
||||||
|
log.Debugf("Udev %s event: %s:%s - queueing DDC rescan", action, subsystem, sysname)
|
||||||
|
m.debouncedRescan(manager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UdevMonitor) debouncedRescan(manager *Manager) {
|
||||||
|
m.rescanMutex.Lock()
|
||||||
|
defer m.rescanMutex.Unlock()
|
||||||
|
|
||||||
|
m.rescanPending = true
|
||||||
|
|
||||||
|
if m.rescanTimer != nil {
|
||||||
|
m.rescanTimer.Reset(2 * time.Second)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.rescanTimer = time.AfterFunc(2*time.Second, func() {
|
||||||
|
m.rescanMutex.Lock()
|
||||||
|
pending := m.rescanPending
|
||||||
|
m.rescanPending = false
|
||||||
|
m.rescanMutex.Unlock()
|
||||||
|
|
||||||
|
if !pending {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("Executing debounced DDC rescan")
|
||||||
|
manager.Rescan()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UdevMonitor) handleBacklightEvent(manager *Manager, action, sysname string) {
|
||||||
switch action {
|
switch action {
|
||||||
case "change":
|
case "change":
|
||||||
m.handleChange(manager, subsystem, sysname)
|
m.handleChange(manager, "backlight", sysname)
|
||||||
case "add", "remove":
|
case "add", "remove":
|
||||||
log.Debugf("Udev %s event: %s:%s - triggering rescan", action, subsystem, sysname)
|
log.Debugf("Udev %s event: backlight:%s - triggering rescan", action, sysname)
|
||||||
manager.Rescan()
|
manager.Rescan()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
core/internal/wayland/client/helpers.go
Normal file
26
core/internal/wayland/client/helpers.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
|
||||||
|
func Roundtrip(display *wlclient.Display, ctx *wlclient.Context) error {
|
||||||
|
callback, err := display.Sync()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
callback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
|
||||||
|
close(done)
|
||||||
|
})
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
if err := ctx.Dispatch(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
139
core/internal/wayland/shm/buffer.go
Normal file
139
core/internal/wayland/shm/buffer.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package shm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PixelFormat uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
FormatARGB8888 PixelFormat = 0
|
||||||
|
FormatXRGB8888 PixelFormat = 1
|
||||||
|
FormatABGR8888 PixelFormat = 0x34324241
|
||||||
|
FormatXBGR8888 PixelFormat = 0x34324258
|
||||||
|
)
|
||||||
|
|
||||||
|
type Buffer struct {
|
||||||
|
fd int
|
||||||
|
data []byte
|
||||||
|
size int
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
Stride int
|
||||||
|
Format PixelFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateBuffer(width, height, stride int) (*Buffer, error) {
|
||||||
|
size := stride * height
|
||||||
|
|
||||||
|
fd, err := unix.MemfdCreate("dms-shm", 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("memfd_create: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := unix.Ftruncate(fd, int64(size)); err != nil {
|
||||||
|
unix.Close(fd)
|
||||||
|
return nil, fmt.Errorf("ftruncate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := unix.Mmap(fd, 0, size, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
|
||||||
|
if err != nil {
|
||||||
|
unix.Close(fd)
|
||||||
|
return nil, fmt.Errorf("mmap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Buffer{
|
||||||
|
fd: fd,
|
||||||
|
data: data,
|
||||||
|
size: size,
|
||||||
|
Width: width,
|
||||||
|
Height: height,
|
||||||
|
Stride: stride,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) Fd() int { return b.fd }
|
||||||
|
func (b *Buffer) Size() int { return b.size }
|
||||||
|
func (b *Buffer) Data() []byte { return b.data }
|
||||||
|
|
||||||
|
func (b *Buffer) Close() error {
|
||||||
|
var firstErr error
|
||||||
|
|
||||||
|
if b.data != nil {
|
||||||
|
if err := unix.Munmap(b.data); err != nil && firstErr == nil {
|
||||||
|
firstErr = fmt.Errorf("munmap: %w", err)
|
||||||
|
}
|
||||||
|
b.data = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.fd >= 0 {
|
||||||
|
if err := unix.Close(b.fd); err != nil && firstErr == nil {
|
||||||
|
firstErr = fmt.Errorf("close: %w", err)
|
||||||
|
}
|
||||||
|
b.fd = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) GetPixelRGBA(x, y int) (r, g, b2, a uint8) {
|
||||||
|
if x < 0 || x >= b.Width || y < 0 || y >= b.Height {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
off := y*b.Stride + x*4
|
||||||
|
if off+3 >= len(b.data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.data[off+2], b.data[off+1], b.data[off], b.data[off+3]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) GetPixelBGRA(x, y int) (b2, g, r, a uint8) {
|
||||||
|
if x < 0 || x >= b.Width || y < 0 || y >= b.Height {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
off := y*b.Stride + x*4
|
||||||
|
if off+3 >= len(b.data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.data[off], b.data[off+1], b.data[off+2], b.data[off+3]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) ConvertBGRAtoRGBA() {
|
||||||
|
for y := 0; y < b.Height; y++ {
|
||||||
|
rowOff := y * b.Stride
|
||||||
|
for x := 0; x < b.Width; x++ {
|
||||||
|
off := rowOff + x*4
|
||||||
|
if off+3 >= len(b.data) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.data[off], b.data[off+2] = b.data[off+2], b.data[off]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) FlipVertical() {
|
||||||
|
tmp := make([]byte, b.Stride)
|
||||||
|
for y := 0; y < b.Height/2; y++ {
|
||||||
|
topOff := y * b.Stride
|
||||||
|
botOff := (b.Height - 1 - y) * b.Stride
|
||||||
|
copy(tmp, b.data[topOff:topOff+b.Stride])
|
||||||
|
copy(b.data[topOff:topOff+b.Stride], b.data[botOff:botOff+b.Stride])
|
||||||
|
copy(b.data[botOff:botOff+b.Stride], tmp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) Clear() {
|
||||||
|
for i := range b.data {
|
||||||
|
b.data[i] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) CopyFrom(src *Buffer) {
|
||||||
|
copy(b.data, src.data)
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
}: let
|
}: let
|
||||||
cfg = config.programs.dankMaterialShell;
|
cfg = config.programs.dankMaterialShell;
|
||||||
in {
|
in {
|
||||||
qmlPath = "${dmsPkgs.dankMaterialShell}/etc/xdg/quickshell/dms";
|
qmlPath = "${dmsPkgs.dms-shell}/share/quickshell/dms";
|
||||||
|
|
||||||
packages =
|
packages =
|
||||||
[
|
[
|
||||||
@@ -19,7 +19,7 @@ in {
|
|||||||
pkgs.libsForQt5.qt5ct
|
pkgs.libsForQt5.qt5ct
|
||||||
pkgs.kdePackages.qt6ct
|
pkgs.kdePackages.qt6ct
|
||||||
|
|
||||||
dmsPkgs.dmsCli
|
dmsPkgs.dms-shell
|
||||||
]
|
]
|
||||||
++ lib.optional cfg.enableSystemMonitoring dmsPkgs.dgop
|
++ lib.optional cfg.enableSystemMonitoring dmsPkgs.dgop
|
||||||
++ lib.optionals cfg.enableClipboard [pkgs.cliphist pkgs.wl-clipboard]
|
++ lib.optionals cfg.enableClipboard [pkgs.cliphist pkgs.wl-clipboard]
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"--command"
|
"--command"
|
||||||
cfg.compositor.name
|
cfg.compositor.name
|
||||||
"-p"
|
"-p"
|
||||||
"${dmsPkgs.dankMaterialShell}/etc/xdg/quickshell/dms"
|
"${dmsPkgs.dms-shell}/share/quickshell/dms"
|
||||||
]
|
]
|
||||||
++ lib.optionals (cfg.compositor.customConfig != "") [
|
++ lib.optionals (cfg.compositor.customConfig != "") [
|
||||||
"-C"
|
"-C"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ in {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Service = {
|
Service = {
|
||||||
ExecStart = lib.getExe dmsPkgs.dmsCli + " run --session";
|
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,6 +89,6 @@ in {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
home.packages = common.packages ++ [dmsPkgs.dankMaterialShell];
|
home.packages = common.packages;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ in {
|
|||||||
|
|
||||||
config = lib.mkIf cfg.enable
|
config = lib.mkIf cfg.enable
|
||||||
{
|
{
|
||||||
environment.etc."xdg/quickshell/dms".source = "${dmsPkgs.dankMaterialShell}/etc/xdg/quickshell/dms";
|
environment.etc."xdg/quickshell/dms".source = "${dmsPkgs.dms-shell}/share/quickshell/dms";
|
||||||
|
|
||||||
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
|
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
|
||||||
description = "DankMaterialShell";
|
description = "DankMaterialShell";
|
||||||
@@ -26,11 +26,11 @@ in {
|
|||||||
restartTriggers = lib.optional cfg.systemd.restartIfChanged common.qmlPath;
|
restartTriggers = lib.optional cfg.systemd.restartIfChanged common.qmlPath;
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = lib.getExe dmsPkgs.dmsCli + " run --session";
|
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
environment.systemPackages = [cfg.quickshell.package dmsPkgs.dankMaterialShell] ++ common.packages;
|
environment.systemPackages = [cfg.quickshell.package] ++ common.packages;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
75
flake.nix
75
flake.nix
@@ -20,7 +20,7 @@
|
|||||||
system: fn system nixpkgs.legacyPackages.${system}
|
system: fn system nixpkgs.legacyPackages.${system}
|
||||||
);
|
);
|
||||||
buildDmsPkgs = pkgs: {
|
buildDmsPkgs = pkgs: {
|
||||||
inherit (self.packages.${pkgs.stdenv.hostPlatform.system}) dmsCli dankMaterialShell;
|
dms-shell = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||||
dgop = dgop.packages.${pkgs.stdenv.hostPlatform.system}.dgop;
|
dgop = dgop.packages.${pkgs.stdenv.hostPlatform.system}.dgop;
|
||||||
};
|
};
|
||||||
mkModuleWithDmsPkgs = path: args @ {pkgs, ...}: {
|
mkModuleWithDmsPkgs = path: args @ {pkgs, ...}: {
|
||||||
@@ -46,10 +46,11 @@
|
|||||||
+ "_"
|
+ "_"
|
||||||
+ (self.shortRev or "dirty");
|
+ (self.shortRev or "dirty");
|
||||||
in {
|
in {
|
||||||
dmsCli = pkgs.buildGoModule (finalAttrs: {
|
dms-shell = pkgs.buildGoModule (let
|
||||||
|
rootSrc = ./.;
|
||||||
|
in {
|
||||||
inherit version;
|
inherit version;
|
||||||
|
pname = "dms-shell";
|
||||||
pname = "dmsCli";
|
|
||||||
src = ./core;
|
src = ./core;
|
||||||
vendorHash = "sha256-2PCqiW4frxME8IlmwWH5ktznhd/G1bah5Ae4dp0HPTQ=";
|
vendorHash = "sha256-2PCqiW4frxME8IlmwWH5ktznhd/G1bah5Ae4dp0HPTQ=";
|
||||||
|
|
||||||
@@ -58,50 +59,56 @@
|
|||||||
ldflags = [
|
ldflags = [
|
||||||
"-s"
|
"-s"
|
||||||
"-w"
|
"-w"
|
||||||
"-X main.Version=${finalAttrs.version}"
|
"-X main.Version=${version}"
|
||||||
];
|
];
|
||||||
|
|
||||||
nativeBuildInputs = [pkgs.installShellFiles];
|
nativeBuildInputs = [
|
||||||
|
pkgs.installShellFiles
|
||||||
|
pkgs.makeWrapper
|
||||||
|
];
|
||||||
|
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
|
mkdir -p $out/share/quickshell/dms
|
||||||
|
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
||||||
|
|
||||||
|
chmod u+w $out/share/quickshell/dms/VERSION
|
||||||
|
echo "${version}" > $out/share/quickshell/dms/VERSION
|
||||||
|
|
||||||
|
# Install desktop file and icon
|
||||||
|
install -D ${rootSrc}/assets/dms-open.desktop \
|
||||||
|
$out/share/applications/dms-open.desktop
|
||||||
|
install -D ${rootSrc}/core/assets/danklogo.svg \
|
||||||
|
$out/share/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
|
wrapProgram $out/bin/dms --add-flags "-c $out/share/quickshell/dms"
|
||||||
|
|
||||||
|
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
||||||
|
$out/lib/systemd/user/dms.service
|
||||||
|
|
||||||
|
substituteInPlace $out/lib/systemd/user/dms.service \
|
||||||
|
--replace-fail /usr/bin/dms $out/bin/dms \
|
||||||
|
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
|
||||||
|
|
||||||
|
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
|
||||||
|
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
|
||||||
|
|
||||||
installShellCompletion --cmd dms \
|
installShellCompletion --cmd dms \
|
||||||
--bash <($out/bin/dms completion bash) \
|
--bash <($out/bin/dms completion bash) \
|
||||||
--fish <($out/bin/dms completion fish ) \
|
--fish <($out/bin/dms completion fish) \
|
||||||
--zsh <($out/bin/dms completion zsh)
|
--zsh <($out/bin/dms completion zsh)
|
||||||
'';
|
'';
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
description = "DankMaterialShell Command Line Interface";
|
description = "Desktop shell for wayland compositors built with Quickshell & GO";
|
||||||
homepage = "https://github.com/AvengeMedia/danklinux";
|
homepage = "https://danklinux.com";
|
||||||
mainProgram = "dms";
|
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
|
||||||
license = pkgs.lib.licenses.mit;
|
license = pkgs.lib.licenses.mit;
|
||||||
platforms = pkgs.lib.platforms.unix;
|
mainProgram = "dms";
|
||||||
|
platforms = pkgs.lib.platforms.linux;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
dankMaterialShell = pkgs.stdenvNoCC.mkDerivation {
|
default = self.packages.${system}.dms-shell;
|
||||||
inherit version;
|
|
||||||
|
|
||||||
pname = "dankMaterialShell";
|
|
||||||
src = ./quickshell;
|
|
||||||
installPhase = ''
|
|
||||||
mkdir -p $out/etc/xdg/quickshell
|
|
||||||
cp -r ./ $out/etc/xdg/quickshell/dms
|
|
||||||
|
|
||||||
# Create DMS Version file
|
|
||||||
echo "${version}" > $out/etc/xdg/quickshell/dms/VERSION
|
|
||||||
|
|
||||||
# Install desktop file
|
|
||||||
mkdir -p $out/share/applications
|
|
||||||
cp ${./assets/dms-open.desktop} $out/share/applications/dms-open.desktop
|
|
||||||
|
|
||||||
# Install icon
|
|
||||||
mkdir -p $out/share/icons/hicolor/scalable/apps
|
|
||||||
cp ${./core/assets/danklogo.svg} $out/share/icons/hicolor/scalable/apps/danklogo.svg
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
default = self.packages.${system}.dmsCli;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,15 @@ Singleton {
|
|||||||
const currentOSD = currentOSDsByScreen[screenName];
|
const currentOSD = currentOSDsByScreen[screenName];
|
||||||
|
|
||||||
if (currentOSD && currentOSD !== osd) {
|
if (currentOSD && currentOSD !== osd) {
|
||||||
currentOSD.hide();
|
if (typeof currentOSD.hide === "function") {
|
||||||
|
try {
|
||||||
|
currentOSD.hide();
|
||||||
|
} catch (e) {
|
||||||
|
currentOSDsByScreen[screenName] = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentOSDsByScreen[screenName] = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentOSDsByScreen[screenName] = osd;
|
currentOSDsByScreen[screenName] = osd;
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ Singleton {
|
|||||||
property bool controlCenterShowNetworkIcon: true
|
property bool controlCenterShowNetworkIcon: true
|
||||||
property bool controlCenterShowBluetoothIcon: true
|
property bool controlCenterShowBluetoothIcon: true
|
||||||
property bool controlCenterShowAudioIcon: true
|
property bool controlCenterShowAudioIcon: true
|
||||||
property bool controlCenterShowVpnIcon: false
|
property bool controlCenterShowVpnIcon: true
|
||||||
property bool controlCenterShowBrightnessIcon: false
|
property bool controlCenterShowBrightnessIcon: false
|
||||||
property bool controlCenterShowMicIcon: false
|
property bool controlCenterShowMicIcon: false
|
||||||
property bool controlCenterShowBatteryIcon: false
|
property bool controlCenterShowBatteryIcon: false
|
||||||
@@ -318,7 +318,7 @@ Singleton {
|
|||||||
property bool osdAudioOutputEnabled: true
|
property bool osdAudioOutputEnabled: true
|
||||||
|
|
||||||
property bool powerActionConfirm: true
|
property bool powerActionConfirm: true
|
||||||
property int powerActionHoldDuration: 1
|
property real powerActionHoldDuration: 0.5
|
||||||
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
||||||
property string powerMenuDefaultAction: "logout"
|
property string powerMenuDefaultAction: "logout"
|
||||||
property bool powerMenuGridLayout: false
|
property bool powerMenuGridLayout: false
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ var SPEC = {
|
|||||||
controlCenterShowNetworkIcon: { def: true },
|
controlCenterShowNetworkIcon: { def: true },
|
||||||
controlCenterShowBluetoothIcon: { def: true },
|
controlCenterShowBluetoothIcon: { def: true },
|
||||||
controlCenterShowAudioIcon: { def: true },
|
controlCenterShowAudioIcon: { def: true },
|
||||||
controlCenterShowVpnIcon: { def: false },
|
controlCenterShowVpnIcon: { def: true },
|
||||||
controlCenterShowBrightnessIcon: { def: false },
|
controlCenterShowBrightnessIcon: { def: false },
|
||||||
controlCenterShowMicIcon: { def: false },
|
controlCenterShowMicIcon: { def: false },
|
||||||
controlCenterShowBatteryIcon: { def: false },
|
controlCenterShowBatteryIcon: { def: false },
|
||||||
@@ -217,7 +217,7 @@ var SPEC = {
|
|||||||
osdAudioOutputEnabled: { def: true },
|
osdAudioOutputEnabled: { def: true },
|
||||||
|
|
||||||
powerActionConfirm: { def: true },
|
powerActionConfirm: { def: true },
|
||||||
powerActionHoldDuration: { def: 1 },
|
powerActionHoldDuration: { def: 0.5 },
|
||||||
powerMenuActions: { def: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] },
|
powerMenuActions: { def: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] },
|
||||||
powerMenuDefaultAction: { def: "logout" },
|
powerMenuDefaultAction: { def: "logout" },
|
||||||
powerMenuGridLayout: { def: false },
|
powerMenuGridLayout: { def: false },
|
||||||
|
|||||||
@@ -787,12 +787,18 @@ DankModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
readonly property int remainingSeconds: Math.ceil(SettingsData.powerActionHoldDuration * (1 - root.holdProgress))
|
readonly property real totalMs: SettingsData.powerActionHoldDuration * 1000
|
||||||
|
readonly property int remainingMs: Math.ceil(totalMs * (1 - root.holdProgress))
|
||||||
text: {
|
text: {
|
||||||
if (root.showHoldHint)
|
if (root.showHoldHint)
|
||||||
return I18n.tr("Hold longer to confirm");
|
return I18n.tr("Hold longer to confirm");
|
||||||
if (root.holdProgress > 0)
|
if (root.holdProgress > 0) {
|
||||||
return I18n.tr("Hold to confirm (%1s)").arg(remainingSeconds);
|
if (totalMs < 1000)
|
||||||
|
return I18n.tr("Hold to confirm (%1 ms)").arg(remainingMs);
|
||||||
|
return I18n.tr("Hold to confirm (%1s)").arg(Math.ceil(remainingMs / 1000));
|
||||||
|
}
|
||||||
|
if (totalMs < 1000)
|
||||||
|
return I18n.tr("Hold to confirm (%1 ms)").arg(totalMs);
|
||||||
return I18n.tr("Hold to confirm (%1s)").arg(SettingsData.powerActionHoldDuration);
|
return I18n.tr("Hold to confirm (%1s)").arg(SettingsData.powerActionHoldDuration);
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ Item {
|
|||||||
id: barBorderShape
|
id: barBorderShape
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
preferredRendererType: Shape.CurveRenderer
|
preferredRendererType: Shape.CurveRenderer
|
||||||
visible: (barConfig?.borderEnabled ?? false) && !barWindow.hasMaximizedToplevel
|
visible: barConfig?.borderEnabled ?? false
|
||||||
|
|
||||||
ShapePath {
|
ShapePath {
|
||||||
fillColor: "transparent"
|
fillColor: "transparent"
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ Item {
|
|||||||
property var itemData: modelData
|
property var itemData: modelData
|
||||||
readonly property real itemSpacing: root.widgetSpacing
|
readonly property real itemSpacing: root.widgetSpacing
|
||||||
|
|
||||||
width: widgetLoader.item ? widgetLoader.item.width : 0
|
width: root.isVertical ? root.width : (widgetLoader.item ? widgetLoader.item.width : 0)
|
||||||
height: widgetLoader.item ? widgetLoader.item.height : 0
|
height: widgetLoader.item ? widgetLoader.item.height : 0
|
||||||
|
|
||||||
readonly property bool active: widgetLoader.active
|
readonly property bool active: widgetLoader.active
|
||||||
|
|||||||
@@ -302,7 +302,8 @@ Item {
|
|||||||
"vpn": vpnComponent,
|
"vpn": vpnComponent,
|
||||||
"notepadButton": notepadButtonComponent,
|
"notepadButton": notepadButtonComponent,
|
||||||
"colorPicker": colorPickerComponent,
|
"colorPicker": colorPickerComponent,
|
||||||
"systemUpdate": systemUpdateComponent
|
"systemUpdate": systemUpdateComponent,
|
||||||
|
"powerMenuButton": powerMenuButtonComponent
|
||||||
};
|
};
|
||||||
|
|
||||||
let pluginMap = PluginService.getWidgetComponents();
|
let pluginMap = PluginService.getWidgetComponents();
|
||||||
@@ -314,36 +315,37 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property var allComponents: ({
|
readonly property var allComponents: ({
|
||||||
"launcherButtonComponent": launcherButtonComponent,
|
"launcherButtonComponent": launcherButtonComponent,
|
||||||
"workspaceSwitcherComponent": workspaceSwitcherComponent,
|
"workspaceSwitcherComponent": workspaceSwitcherComponent,
|
||||||
"focusedWindowComponent": focusedWindowComponent,
|
"focusedWindowComponent": focusedWindowComponent,
|
||||||
"runningAppsComponent": runningAppsComponent,
|
"runningAppsComponent": runningAppsComponent,
|
||||||
"clockComponent": clockComponent,
|
"clockComponent": clockComponent,
|
||||||
"mediaComponent": mediaComponent,
|
"mediaComponent": mediaComponent,
|
||||||
"weatherComponent": weatherComponent,
|
"weatherComponent": weatherComponent,
|
||||||
"systemTrayComponent": systemTrayComponent,
|
"systemTrayComponent": systemTrayComponent,
|
||||||
"privacyIndicatorComponent": privacyIndicatorComponent,
|
"privacyIndicatorComponent": privacyIndicatorComponent,
|
||||||
"clipboardComponent": clipboardComponent,
|
"clipboardComponent": clipboardComponent,
|
||||||
"cpuUsageComponent": cpuUsageComponent,
|
"cpuUsageComponent": cpuUsageComponent,
|
||||||
"memUsageComponent": memUsageComponent,
|
"memUsageComponent": memUsageComponent,
|
||||||
"diskUsageComponent": diskUsageComponent,
|
"diskUsageComponent": diskUsageComponent,
|
||||||
"cpuTempComponent": cpuTempComponent,
|
"cpuTempComponent": cpuTempComponent,
|
||||||
"gpuTempComponent": gpuTempComponent,
|
"gpuTempComponent": gpuTempComponent,
|
||||||
"notificationButtonComponent": notificationButtonComponent,
|
"notificationButtonComponent": notificationButtonComponent,
|
||||||
"batteryComponent": batteryComponent,
|
"batteryComponent": batteryComponent,
|
||||||
"layoutComponent": layoutComponent,
|
"layoutComponent": layoutComponent,
|
||||||
"controlCenterButtonComponent": controlCenterButtonComponent,
|
"controlCenterButtonComponent": controlCenterButtonComponent,
|
||||||
"capsLockIndicatorComponent": capsLockIndicatorComponent,
|
"capsLockIndicatorComponent": capsLockIndicatorComponent,
|
||||||
"idleInhibitorComponent": idleInhibitorComponent,
|
"idleInhibitorComponent": idleInhibitorComponent,
|
||||||
"spacerComponent": spacerComponent,
|
"spacerComponent": spacerComponent,
|
||||||
"separatorComponent": separatorComponent,
|
"separatorComponent": separatorComponent,
|
||||||
"networkComponent": networkComponent,
|
"networkComponent": networkComponent,
|
||||||
"keyboardLayoutNameComponent": keyboardLayoutNameComponent,
|
"keyboardLayoutNameComponent": keyboardLayoutNameComponent,
|
||||||
"vpnComponent": vpnComponent,
|
"vpnComponent": vpnComponent,
|
||||||
"notepadButtonComponent": notepadButtonComponent,
|
"notepadButtonComponent": notepadButtonComponent,
|
||||||
"colorPickerComponent": colorPickerComponent,
|
"colorPickerComponent": colorPickerComponent,
|
||||||
"systemUpdateComponent": systemUpdateComponent
|
"systemUpdateComponent": systemUpdateComponent,
|
||||||
})
|
"powerMenuButtonComponent": powerMenuButtonComponent
|
||||||
|
})
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: stackContainer
|
id: stackContainer
|
||||||
@@ -532,7 +534,27 @@ Item {
|
|||||||
section: topBarContent.getWidgetSection(parent)
|
section: topBarContent.getWidgetSection(parent)
|
||||||
parentScreen: barWindow.screen
|
parentScreen: barWindow.screen
|
||||||
onClicked: {
|
onClicked: {
|
||||||
clipboardHistoryModalPopup.toggle();
|
clipboardHistoryModalPopup.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: powerMenuButtonComponent
|
||||||
|
|
||||||
|
PowerMenuButton {
|
||||||
|
widgetThickness: barWindow.widgetThickness
|
||||||
|
barThickness: barWindow.effectiveBarThickness
|
||||||
|
axis: barWindow.axis
|
||||||
|
section: topBarContent.getWidgetSection(parent)
|
||||||
|
parentScreen: barWindow.screen
|
||||||
|
onClicked: {
|
||||||
|
if (powerMenuModalLoader) {
|
||||||
|
powerMenuModalLoader.active = true
|
||||||
|
if (powerMenuModalLoader.item) {
|
||||||
|
powerMenuModalLoader.item.openCentered()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,7 +237,8 @@ Loader {
|
|||||||
"notepadButton": components.notepadButtonComponent,
|
"notepadButton": components.notepadButtonComponent,
|
||||||
"colorPicker": components.colorPickerComponent,
|
"colorPicker": components.colorPickerComponent,
|
||||||
"systemUpdate": components.systemUpdateComponent,
|
"systemUpdate": components.systemUpdateComponent,
|
||||||
"layout": components.layoutComponent
|
"layout": components.layoutComponent,
|
||||||
|
"powerMenuButton": components.powerMenuButtonComponent
|
||||||
};
|
};
|
||||||
|
|
||||||
if (componentMap[widgetId]) {
|
if (componentMap[widgetId]) {
|
||||||
|
|||||||
24
quickshell/Modules/DankBar/Widgets/PowerMenuButton.qml
Normal file
24
quickshell/Modules/DankBar/Widgets/PowerMenuButton.qml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modules.Plugins
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
BasePill {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool isActive: false
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Item {
|
||||||
|
implicitWidth: root.widgetThickness - root.horizontalPadding * 2
|
||||||
|
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "power_settings_new"
|
||||||
|
size: Theme.barIconSize(root.barThickness)
|
||||||
|
color: Theme.widgetIconColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -780,12 +780,18 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
readonly property int remainingSeconds: Math.ceil(SettingsData.powerActionHoldDuration * (1 - root.holdProgress))
|
readonly property real totalMs: SettingsData.powerActionHoldDuration * 1000
|
||||||
|
readonly property int remainingMs: Math.ceil(totalMs * (1 - root.holdProgress))
|
||||||
text: {
|
text: {
|
||||||
if (root.showHoldHint)
|
if (root.showHoldHint)
|
||||||
return I18n.tr("Hold longer to confirm");
|
return I18n.tr("Hold longer to confirm");
|
||||||
if (root.holdProgress > 0)
|
if (root.holdProgress > 0) {
|
||||||
return I18n.tr("Hold to confirm (%1s)").arg(remainingSeconds);
|
if (totalMs < 1000)
|
||||||
|
return I18n.tr("Hold to confirm (%1 ms)").arg(remainingMs);
|
||||||
|
return I18n.tr("Hold to confirm (%1s)").arg(Math.ceil(remainingMs / 1000));
|
||||||
|
}
|
||||||
|
if (totalMs < 1000)
|
||||||
|
return I18n.tr("Hold to confirm (%1 ms)").arg(totalMs);
|
||||||
return I18n.tr("Hold to confirm (%1s)").arg(SettingsData.powerActionHoldDuration);
|
return I18n.tr("Hold to confirm (%1s)").arg(SettingsData.powerActionHoldDuration);
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
|||||||
@@ -905,4 +905,4 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -408,15 +408,27 @@ Item {
|
|||||||
onToggled: checked => SettingsData.set("powerActionConfirm", checked)
|
onToggled: checked => SettingsData.set("powerActionConfirm", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsSliderRow {
|
SettingsDropdownRow {
|
||||||
|
id: holdDurationDropdown
|
||||||
|
property var durationOptions: ["250 ms", "500 ms", "750 ms", "1 second", "2 seconds", "3 seconds", "5 seconds", "10 seconds"]
|
||||||
|
property var durationValues: [0.25, 0.5, 0.75, 1, 2, 3, 5, 10]
|
||||||
|
|
||||||
text: I18n.tr("Hold Duration")
|
text: I18n.tr("Hold Duration")
|
||||||
description: I18n.tr("How long to hold the button to confirm the action")
|
options: durationOptions
|
||||||
minimum: 1
|
|
||||||
maximum: 10
|
|
||||||
unit: "s"
|
|
||||||
visible: SettingsData.powerActionConfirm
|
visible: SettingsData.powerActionConfirm
|
||||||
value: SettingsData.powerActionHoldDuration
|
|
||||||
onSliderValueChanged: newValue => SettingsData.set("powerActionHoldDuration", newValue)
|
Component.onCompleted: {
|
||||||
|
const currentDuration = SettingsData.powerActionHoldDuration;
|
||||||
|
const index = durationValues.indexOf(currentDuration);
|
||||||
|
currentValue = index >= 0 ? durationOptions[index] : "500 ms";
|
||||||
|
}
|
||||||
|
|
||||||
|
onValueChanged: value => {
|
||||||
|
const index = durationOptions.indexOf(value);
|
||||||
|
if (index < 0)
|
||||||
|
return;
|
||||||
|
SettingsData.set("powerActionHoldDuration", durationValues[index]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -239,7 +239,14 @@ Item {
|
|||||||
"description": I18n.tr("Check for system updates"),
|
"description": I18n.tr("Check for system updates"),
|
||||||
"icon": "update",
|
"icon": "update",
|
||||||
"enabled": SystemUpdateService.distributionSupported
|
"enabled": SystemUpdateService.distributionSupported
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"id": "powerMenuButton",
|
||||||
|
"text": I18n.tr("Power"),
|
||||||
|
"description": I18n.tr("Display the power system menu"),
|
||||||
|
"icon": "power_settings_new",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
var allPluginVariants = PluginService.getAllPluginVariants();
|
var allPluginVariants = PluginService.getAllPluginVariants();
|
||||||
|
|||||||
@@ -752,15 +752,28 @@ Singleton {
|
|||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: screenChangeRescanTimer
|
id: screenChangeRescanTimer
|
||||||
|
property int rescanAttempt: 0
|
||||||
interval: 3000
|
interval: 3000
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: rescanDevices()
|
onTriggered: {
|
||||||
|
rescanDevices();
|
||||||
|
rescanAttempt++;
|
||||||
|
if (rescanAttempt < 3) {
|
||||||
|
interval = rescanAttempt === 1 ? 5000 : 8000;
|
||||||
|
restart();
|
||||||
|
} else {
|
||||||
|
rescanAttempt = 0;
|
||||||
|
interval = 3000;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: Quickshell
|
target: Quickshell
|
||||||
|
|
||||||
function onScreensChanged() {
|
function onScreensChanged() {
|
||||||
|
screenChangeRescanTimer.rescanAttempt = 0;
|
||||||
|
screenChangeRescanTimer.interval = 3000;
|
||||||
screenChangeRescanTimer.restart();
|
screenChangeRescanTimer.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ Item {
|
|||||||
signal popoutClosed
|
signal popoutClosed
|
||||||
signal backgroundClicked
|
signal backgroundClicked
|
||||||
|
|
||||||
|
property var _lastOpenedScreen: null
|
||||||
|
|
||||||
property int effectiveBarPosition: 0
|
property int effectiveBarPosition: 0
|
||||||
property real effectiveBarBottomGap: 0
|
property real effectiveBarBottomGap: 0
|
||||||
|
|
||||||
@@ -100,9 +102,17 @@ Item {
|
|||||||
if (!screen)
|
if (!screen)
|
||||||
return;
|
return;
|
||||||
closeTimer.stop();
|
closeTimer.stop();
|
||||||
|
|
||||||
|
if (_lastOpenedScreen !== null && _lastOpenedScreen !== screen) {
|
||||||
|
contentWindow.visible = false;
|
||||||
|
if (useBackgroundWindow)
|
||||||
|
backgroundWindow.visible = false;
|
||||||
|
}
|
||||||
|
_lastOpenedScreen = screen;
|
||||||
|
|
||||||
shouldBeVisible = true;
|
shouldBeVisible = true;
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
if (shouldBeVisible) {
|
if (shouldBeVisible && screen) {
|
||||||
if (useBackgroundWindow)
|
if (useBackgroundWindow)
|
||||||
backgroundWindow.visible = true;
|
backgroundWindow.visible = true;
|
||||||
contentWindow.visible = true;
|
contentWindow.visible = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user