ceecbb6af4
General - 配置界面支持配置对象存储桶 - 快捷键进入截图 - 一键上传截图 & 复制截图链接 MacOS - 状态栏指示器和应用图标
146 lines
5.0 KiB
Go
146 lines
5.0 KiB
Go
//go:build darwin
|
||
|
||
package screencapture
|
||
|
||
import (
|
||
"fmt"
|
||
"image"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// macCapturer shells out to /usr/sbin/screencapture, which is bundled with
|
||
// macOS, requires no cgo, and is fully compatible with macOS 15/26 SDKs
|
||
// where CoreGraphics's display-capture API has been removed.
|
||
//
|
||
// Trade-off: a tiny process-spawn cost per capture (~30 ms). Acceptable for
|
||
// an interactive screenshot tool — users cannot perceive it.
|
||
type macCapturer struct{}
|
||
|
||
// New returns a macOS-native Capturer.
|
||
func New() Capturer { return &macCapturer{} }
|
||
|
||
// VirtualBounds returns the union of all displays measured in CSS pixels
|
||
// (i.e. logical, not Retina physical pixels). We query system_profiler via
|
||
// AppleScript-free shell utilities to avoid extra cgo dependencies.
|
||
//
|
||
// For simplicity we currently return the main display only; this matches
|
||
// what the WebView/window can actually paint without a separate transparent
|
||
// window per display. Multi-monitor support is a follow-up spec.
|
||
func (c *macCapturer) VirtualBounds() image.Rectangle {
|
||
out, err := exec.Command(
|
||
"system_profiler", "SPDisplaysDataType",
|
||
).Output()
|
||
if err == nil {
|
||
// Look for "Resolution: 3024 x 1964" lines and take the first one.
|
||
for _, line := range strings.Split(string(out), "\n") {
|
||
line = strings.TrimSpace(line)
|
||
if !strings.HasPrefix(line, "Resolution:") {
|
||
continue
|
||
}
|
||
parts := strings.Fields(line)
|
||
// Expected pattern: ["Resolution:", "3024", "x", "1964", ...]
|
||
if len(parts) >= 4 {
|
||
w, errW := strconv.Atoi(parts[1])
|
||
h, errH := strconv.Atoi(parts[3])
|
||
if errW == nil && errH == nil {
|
||
// Resolution reported is physical; convert to CSS px by
|
||
// halving for Retina (assume 2× when w >= 2560).
|
||
if w >= 2560 {
|
||
w /= 2
|
||
h /= 2
|
||
}
|
||
return image.Rect(0, 0, w, h)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// Conservative fallback used by ~all modern Macs.
|
||
return image.Rect(0, 0, 1440, 900)
|
||
}
|
||
|
||
// CaptureRegion grabs the requested rectangle via the OS tool and returns
|
||
// the resulting PNG bytes.
|
||
//
|
||
// Rect coords here are logical screen coordinates, matching macOS
|
||
// screencapture's -R contract and the CSS-point coordinates reported by the
|
||
// transparent overlay.
|
||
func (c *macCapturer) CaptureRegion(rect image.Rectangle) ([]byte, error) {
|
||
if rect.Dx() <= 0 || rect.Dy() <= 0 {
|
||
return nil, fmt.Errorf("invalid capture rectangle: %v", rect)
|
||
}
|
||
|
||
tmp := filepath.Join(os.TempDir(), fmt.Sprintf("snapgo-%d.png", time.Now().UnixNano()))
|
||
defer os.Remove(tmp)
|
||
|
||
region := fmt.Sprintf("%d,%d,%d,%d",
|
||
rect.Min.X, rect.Min.Y, rect.Dx(), rect.Dy(),
|
||
)
|
||
|
||
// -x: silent (no shutter sound)
|
||
// -R: rectangle
|
||
// -t png
|
||
cmd := exec.Command("/usr/sbin/screencapture", "-x", "-R", region, "-t", "png", tmp)
|
||
if out, err := cmd.CombinedOutput(); err != nil {
|
||
return nil, fmt.Errorf("screencapture: %w (%s)", err, strings.TrimSpace(string(out)))
|
||
}
|
||
data, err := os.ReadFile(tmp)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("read screencapture output: %w", err)
|
||
}
|
||
return data, nil
|
||
}
|
||
|
||
// CaptureInteractive launches `screencapture -i` which presents the macOS
|
||
// native region selector (the same crosshair that Cmd+Shift+4 produces).
|
||
//
|
||
// Design rationale:
|
||
// - The native picker is pixel-perfect across Retina / external displays
|
||
// and supports multi-monitor out of the box.
|
||
// - It also handles Esc / right-click to cancel without requiring us to
|
||
// run a transparent overlay window — which previously caused the main
|
||
// window to flash a grey full-screen and trap the user.
|
||
// - When the user cancels, screencapture exits 0 but writes no file; we
|
||
// detect that case via os.Stat and return ErrCancelled.
|
||
func (c *macCapturer) CaptureInteractive() ([]byte, error) {
|
||
tmp := filepath.Join(os.TempDir(), fmt.Sprintf("snapgo-i-%d.png", time.Now().UnixNano()))
|
||
defer os.Remove(tmp)
|
||
|
||
// -i: interactive region selection
|
||
// -x: silent (suppress the camera shutter sound)
|
||
// -t png: png output
|
||
cmd := exec.Command("/usr/sbin/screencapture", "-i", "-x", "-t", "png", tmp)
|
||
if out, err := cmd.CombinedOutput(); err != nil {
|
||
return nil, fmt.Errorf("screencapture -i: %w (%s)", err, strings.TrimSpace(string(out)))
|
||
}
|
||
|
||
// User cancelled (Esc / right-click): tool exits 0 with no file written.
|
||
info, err := os.Stat(tmp)
|
||
if err != nil || info.Size() == 0 {
|
||
return nil, ErrCancelled
|
||
}
|
||
return os.ReadFile(tmp)
|
||
}
|
||
|
||
// CaptureFullScreen runs `screencapture -x` to grab the primary display in
|
||
// a single shot. We use -m so only the main display is captured even if
|
||
// external monitors are attached — matches the "primary screen only" UX
|
||
// chosen for the in-app overlay.
|
||
func (c *macCapturer) CaptureFullScreen() ([]byte, error) {
|
||
tmp := filepath.Join(os.TempDir(), fmt.Sprintf("snapgo-full-%d.png", time.Now().UnixNano()))
|
||
defer os.Remove(tmp)
|
||
|
||
// -x: silent
|
||
// -m: main display only
|
||
// -t png: png output
|
||
cmd := exec.Command("/usr/sbin/screencapture", "-x", "-m", "-t", "png", tmp)
|
||
if out, err := cmd.CombinedOutput(); err != nil {
|
||
return nil, fmt.Errorf("screencapture -x -m: %w (%s)", err, strings.TrimSpace(string(out)))
|
||
}
|
||
return os.ReadFile(tmp)
|
||
}
|