diff --git a/core/cmd/dms/commands_screenshot.go b/core/cmd/dms/commands_screenshot.go index 4362e478..ffbd56a1 100644 --- a/core/cmd/dms/commands_screenshot.go +++ b/core/cmd/dms/commands_screenshot.go @@ -35,6 +35,7 @@ Modes: full - Capture the focused output all - Capture all outputs combined output - Capture a specific output by name + window - Capture the focused window (Hyprland only) last - Capture the last selected region Output format (--format): @@ -47,6 +48,7 @@ Examples: 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 @@ -86,6 +88,14 @@ 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", @@ -117,6 +127,7 @@ func init() { screenshotCmd.AddCommand(ssAllCmd) screenshotCmd.AddCommand(ssOutputCmd) screenshotCmd.AddCommand(ssLastCmd) + screenshotCmd.AddCommand(ssWindowCmd) screenshotCmd.AddCommand(ssListCmd) screenshotCmd.Run = runScreenshotRegion @@ -347,6 +358,11 @@ func runScreenshotLast(cmd *cobra.Command, args []string) { 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 { diff --git a/core/internal/screenshot/compositor.go b/core/internal/screenshot/compositor.go new file mode 100644 index 00000000..6c06cd18 --- /dev/null +++ b/core/internal/screenshot/compositor.go @@ -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 +} diff --git a/core/internal/screenshot/screenshot.go b/core/internal/screenshot/screenshot.go index 324e3495..87f94593 100644 --- a/core/internal/screenshot/screenshot.go +++ b/core/internal/screenshot/screenshot.go @@ -77,6 +77,8 @@ func (s *Screenshoter) Run() (*CaptureResult, error) { return s.captureLastRegion() case ModeRegion: return s.captureRegion() + case ModeWindow: + return s.captureWindow() case ModeOutput: return s.captureOutput(s.config.OutputName) case ModeFullScreen: @@ -119,6 +121,27 @@ func (s *Screenshoter) captureRegion() (*CaptureResult, error) { 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 {