From f18d36f6c2693ae299eb736cb6f1e9dac307ab53 Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 15 Jul 2026 09:17:24 -0400 Subject: [PATCH] screenshot: add --json flag to capture metadata fixes #2852 port 1.5 --- core/cmd/dms/commands_screenshot.go | 81 ++++++++++++++++++++++---- core/internal/screenshot/region.go | 5 ++ core/internal/screenshot/screenshot.go | 49 +++++++++------- 3 files changed, 103 insertions(+), 32 deletions(-) diff --git a/core/cmd/dms/commands_screenshot.go b/core/cmd/dms/commands_screenshot.go index 262fc59b1..93a35943c 100644 --- a/core/cmd/dms/commands_screenshot.go +++ b/core/cmd/dms/commands_screenshot.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "encoding/json" "fmt" "os" "os/exec" @@ -27,8 +28,19 @@ var ( ssNoConfirm bool ssReset bool ssStdout bool + ssJSON bool ) +type screenshotMetadata struct { + Status string `json:"status"` + Path string `json:"path,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + Scale float64 `json:"scale,omitempty"` + Mime string `json:"mime,omitempty"` + Error string `json:"error,omitempty"` +} + var screenshotCmd = &cobra.Command{ Use: "screenshot", Short: "Capture screenshots", @@ -59,7 +71,8 @@ Examples: dms screenshot --no-file # Clipboard only dms screenshot --no-confirm # Region capture on mouse release dms screenshot --cursor=on # Include cursor - dms screenshot -f jpg -q 85 # JPEG with quality 85`, + dms screenshot -f jpg -q 85 # JPEG with quality 85 + dms screenshot --json # Print capture metadata as JSON`, } var ssRegionCmd = &cobra.Command{ @@ -128,6 +141,7 @@ func init() { screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation") screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing") screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)") + screenshotCmd.PersistentFlags().BoolVar(&ssJSON, "json", false, "Print capture metadata as JSON") screenshotCmd.AddCommand(ssRegionCmd) screenshotCmd.AddCommand(ssFullCmd) @@ -203,7 +217,36 @@ func setPopoutScreenshotMode(begin bool) { _ = exec.Command("qs", cmdArgs...).Run() } +func writeScreenshotJSON(meta screenshotMetadata) { + _ = json.NewEncoder(os.Stdout).Encode(meta) +} + +func exitScreenshotError(context string, err error) { + if ssJSON { + writeScreenshotJSON(screenshotMetadata{Status: "error", Error: err.Error()}) + os.Exit(1) + } + fmt.Fprintf(os.Stderr, "Error%s: %v\n", context, err) + os.Exit(1) +} + +func formatMime(format screenshot.Format) string { + switch format { + case screenshot.FormatJPEG: + return "image/jpeg" + case screenshot.FormatPPM: + return "image/x-portable-pixmap" + default: + return "image/png" + } +} + func runScreenshot(config screenshot.Config) { + if ssJSON && config.Stdout { + fmt.Fprintln(os.Stderr, "Error: --json cannot be combined with --stdout") + os.Exit(1) + } + // Region select needs the keyboard; drop popout grabs for its duration. result, err := func() (*screenshot.CaptureResult, error) { interactive := config.Mode == screenshot.ModeRegion || config.Mode == screenshot.ModeLastRegion @@ -215,11 +258,13 @@ func runScreenshot(config screenshot.Config) { }() if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) + exitScreenshotError("", err) } if result == nil { + if ssJSON { + writeScreenshotJSON(screenshotMetadata{Status: "aborted", Error: "User cancelled selection"}) + } os.Exit(0) } @@ -231,8 +276,7 @@ func runScreenshot(config screenshot.Config) { 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) + exitScreenshotError(" writing to stdout", err) } return } @@ -252,22 +296,37 @@ func runScreenshot(config screenshot.Config) { 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) + exitScreenshotError(" writing file", err) + } + if !ssJSON { + fmt.Println(filePath) } - 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) + exitScreenshotError(" copying to clipboard", err) } - if !config.SaveFile { + if !ssJSON && !config.SaveFile { fmt.Println("Copied to clipboard") } } + if ssJSON { + scale := result.Scale + if scale <= 0 { + scale = 1.0 + } + writeScreenshotJSON(screenshotMetadata{ + Status: "success", + Path: filePath, + Width: result.Buffer.Width, + Height: result.Buffer.Height, + Scale: scale, + Mime: formatMime(config.Format), + }) + } + if config.Notify { thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format) screenshot.SendNotification(screenshot.NotifyResult{ diff --git a/core/internal/screenshot/region.go b/core/internal/screenshot/region.go index f29643458..c8c392938 100644 --- a/core/internal/screenshot/region.go +++ b/core/internal/screenshot/region.go @@ -178,9 +178,13 @@ func (r *RegionSelector) Run() (*CaptureResult, bool, error) { yInverted := false var format uint32 + scale := 1.0 if r.selection.surface != nil { yInverted = r.selection.surface.yInverted format = r.selection.surface.screenFormat + if s := r.selection.surface.output.fractionalScale; s > 0 { + scale = s + } } return &CaptureResult{ @@ -188,6 +192,7 @@ func (r *RegionSelector) Run() (*CaptureResult, bool, error) { Region: r.result, YInverted: yInverted, Format: format, + Scale: scale, }, false, nil } diff --git a/core/internal/screenshot/screenshot.go b/core/internal/screenshot/screenshot.go index 43253a21a..cdf7fd44a 100644 --- a/core/internal/screenshot/screenshot.go +++ b/core/internal/screenshot/screenshot.go @@ -28,6 +28,21 @@ type CaptureResult struct { Region Region YInverted bool Format uint32 + Scale float64 +} + +func (o *WaylandOutput) effectiveScale() float64 { + scale := o.fractionalScale + if scale <= 0 && DetectCompositor() == CompositorHyprland { + scale = GetHyprlandMonitorScale(o.name) + } + if scale <= 0 { + scale = float64(o.scale) + } + if scale <= 0 { + return 1.0 + } + return scale } type Screenshoter struct { @@ -255,6 +270,7 @@ func (s *Screenshoter) captureMangoWindow(output *WaylandOutput, region Region, Region: region, YInverted: false, Format: result.Format, + Scale: scale, }, nil } @@ -430,6 +446,7 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) { Buffer: composite, Region: Region{X: int32(minX), Y: int32(minY), Width: int32(totalW), Height: int32(totalH)}, Format: format, + Scale: maxScale, }, nil } @@ -502,6 +519,7 @@ func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult if err != nil { return nil, err } + result.Scale = output.effectiveScale() if result.YInverted { result.Buffer.FlipVertical() @@ -604,6 +622,7 @@ func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*Ca Region: region, YInverted: false, Format: result.Format, + Scale: scale, }, nil } @@ -612,16 +631,7 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio return s.captureRegionOnTransformedOutput(output, region) } - scale := output.fractionalScale - if scale <= 0 && DetectCompositor() == CompositorHyprland { - scale = GetHyprlandMonitorScale(output.name) - } - if scale <= 0 { - scale = float64(output.scale) - } - if scale <= 0 { - scale = 1.0 - } + scale := output.effectiveScale() localX := int32(float64(region.X-output.x) * scale) localY := int32(float64(region.Y-output.y) * scale) @@ -660,7 +670,12 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio return nil, fmt.Errorf("capture region: %w", err) } - return s.processFrame(frame, region) + result, err := s.processFrame(frame, region) + if err != nil { + return nil, err + } + result.Scale = scale + return result, nil } func (s *Screenshoter) captureRegionOnTransformedOutput(output *WaylandOutput, region Region) (*CaptureResult, error) { @@ -669,16 +684,7 @@ func (s *Screenshoter) captureRegionOnTransformedOutput(output *WaylandOutput, r return nil, err } - scale := output.fractionalScale - if scale <= 0 && DetectCompositor() == CompositorHyprland { - scale = GetHyprlandMonitorScale(output.name) - } - if scale <= 0 { - scale = float64(output.scale) - } - if scale <= 0 { - scale = 1.0 - } + scale := output.effectiveScale() localX := int(float64(region.X-output.x) * scale) localY := int(float64(region.Y-output.y) * scale) @@ -730,6 +736,7 @@ func (s *Screenshoter) captureRegionOnTransformedOutput(output *WaylandOutput, r Region: region, YInverted: false, Format: result.Format, + Scale: scale, }, nil }