320 lines
9.1 KiB
Go
320 lines
9.1 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type screenCaptureSession struct {
|
|
capture func() (image.Image, error)
|
|
close func()
|
|
}
|
|
|
|
func (s *screenCaptureSession) Capture() (image.Image, error) {
|
|
return s.capture()
|
|
}
|
|
|
|
func (s *screenCaptureSession) Close() {
|
|
if s.close != nil {
|
|
s.close()
|
|
}
|
|
}
|
|
|
|
// captureScreenRegion opens the native desktop region selector and returns the
|
|
// selected pixels. The command is deliberately kept outside the web build and
|
|
// uses tools commonly available on Ubuntu desktops.
|
|
func captureScreenRegion() (image.Image, error) {
|
|
var attempts []error
|
|
if !strings.Contains(strings.ToLower(os.Getenv("XDG_CURRENT_DESKTOP")), "gnome") {
|
|
if _, err := exec.LookPath("grim"); err == nil {
|
|
if _, err := exec.LookPath("slurp"); err == nil {
|
|
img, captureErr := captureWaylandRegion()
|
|
if captureErr == nil {
|
|
return img, nil
|
|
}
|
|
attempts = append(attempts, fmt.Errorf("grim/slurp: %w", captureErr))
|
|
}
|
|
}
|
|
}
|
|
|
|
commands := []struct {
|
|
name string
|
|
args func(string) []string
|
|
}{
|
|
{name: "gnome-screenshot", args: func(path string) []string { return []string{"-a", "-f", path} }},
|
|
{name: "scrot", args: func(path string) []string { return []string{"-s", path} }},
|
|
{name: "import", args: func(path string) []string { return []string{path} }},
|
|
}
|
|
|
|
for _, candidate := range commands {
|
|
if _, err := exec.LookPath(candidate.name); err != nil {
|
|
continue
|
|
}
|
|
img, err := captureToFile(candidate.name, candidate.args)
|
|
if err == nil {
|
|
return img, nil
|
|
}
|
|
attempts = append(attempts, fmt.Errorf("%s: %w", candidate.name, err))
|
|
}
|
|
|
|
if _, err := exec.LookPath("flameshot"); err == nil {
|
|
img, err := captureToFile("flameshot", func(path string) []string {
|
|
return []string{"gui", "--path", path}
|
|
})
|
|
if err != nil {
|
|
attempts = append(attempts, fmt.Errorf("flameshot: %w", err))
|
|
} else {
|
|
return img, nil
|
|
}
|
|
}
|
|
|
|
if len(attempts) > 0 {
|
|
return nil, fmt.Errorf("snímání oblasti selhalo: %w", errors.Join(attempts...))
|
|
}
|
|
return nil, errors.New("nenalezen nástroj pro výběr oblasti (grim+slurp, gnome-screenshot, scrot, import nebo flameshot)")
|
|
}
|
|
|
|
// openScreenCaptureSession selects a persistent backend once and returns a
|
|
// capture function that can be called repeatedly without reopening the picker.
|
|
func openScreenCaptureSession() (*screenCaptureSession, error) {
|
|
var attempts []error
|
|
isGNOME := strings.Contains(strings.ToLower(os.Getenv("XDG_CURRENT_DESKTOP")), "gnome")
|
|
|
|
if _, err := exec.LookPath("flameshot"); err == nil {
|
|
session, sessionErr := openFlameshotSession()
|
|
if sessionErr == nil {
|
|
return session, nil
|
|
}
|
|
attempts = append(attempts, fmt.Errorf("flameshot: %w", sessionErr))
|
|
}
|
|
|
|
if !isGNOME {
|
|
if _, err := exec.LookPath("grim"); err == nil {
|
|
if _, err := exec.LookPath("slurp"); err == nil {
|
|
selection, selectErr := selectWaylandRegion()
|
|
if selectErr == nil {
|
|
session := &screenCaptureSession{
|
|
capture: func() (image.Image, error) {
|
|
return captureWaylandSelection(selection)
|
|
},
|
|
}
|
|
if _, captureErr := session.Capture(); captureErr == nil {
|
|
return session, nil
|
|
} else {
|
|
attempts = append(attempts, fmt.Errorf("grim/slurp: %w", captureErr))
|
|
}
|
|
}
|
|
if selectErr != nil {
|
|
attempts = append(attempts, fmt.Errorf("grim/slurp: %w", selectErr))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if _, err := exec.LookPath("slop"); err == nil {
|
|
if _, err := exec.LookPath("maim"); err == nil {
|
|
selection, selectErr := selectX11Region()
|
|
if selectErr == nil {
|
|
session := &screenCaptureSession{
|
|
capture: func() (image.Image, error) {
|
|
return captureToFile("maim", func(path string) []string {
|
|
return []string{"-g", selection, path}
|
|
})
|
|
},
|
|
}
|
|
if _, captureErr := session.Capture(); captureErr == nil {
|
|
return session, nil
|
|
} else {
|
|
attempts = append(attempts, fmt.Errorf("slop/maim: %w", captureErr))
|
|
}
|
|
}
|
|
if selectErr != nil {
|
|
attempts = append(attempts, fmt.Errorf("slop/maim: %w", selectErr))
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(attempts) > 0 {
|
|
return nil, fmt.Errorf("živé snímání oblasti selhalo: %w", errors.Join(attempts...))
|
|
}
|
|
if isGNOME {
|
|
return nil, errors.New("GNOME Wayland nepovoluje živé snímání libovolné oblasti přes gnome-screenshot; pro živé sledování nainstalujte Flameshot s podporou --last-region. Jednorázové snímání oblasti zůstává dostupné.")
|
|
}
|
|
return nil, errors.New("nenalezen nástroj pro živé snímání vybrané oblasti (Flameshot, grim+slurp nebo slop+maim)")
|
|
}
|
|
|
|
func openFlameshotSession() (*screenCaptureSession, error) {
|
|
selection, err := selectFlameshotRegion()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("výběr oblasti: %w", err)
|
|
}
|
|
session := &screenCaptureSession{
|
|
capture: func() (image.Image, error) {
|
|
return captureFlameshotSelection(selection)
|
|
},
|
|
}
|
|
if _, err := session.Capture(); err != nil {
|
|
return nil, fmt.Errorf("opakované snímání oblasti: %w", err)
|
|
}
|
|
return session, nil
|
|
}
|
|
|
|
func selectFlameshotRegion() (string, error) {
|
|
var stderr bytes.Buffer
|
|
command := exec.Command("flameshot", "gui", "--print-geometry", "--accept-on-select")
|
|
command.Stderr = &stderr
|
|
data, err := command.Output()
|
|
if err != nil {
|
|
if detail := strings.TrimSpace(stderr.String()); detail != "" {
|
|
return "", fmt.Errorf("%w (%s)", err, detail)
|
|
}
|
|
return "", err
|
|
}
|
|
return parseFlameshotRegion(string(data))
|
|
}
|
|
|
|
func captureFlameshotSelection(selection string) (image.Image, error) {
|
|
return captureToFile("flameshot", func(path string) []string {
|
|
return []string{"screen", "--region", selection, "--path", path}
|
|
})
|
|
}
|
|
|
|
func parseFlameshotRegion(output string) (string, error) {
|
|
value := strings.TrimSpace(output)
|
|
var width, height, x, y int
|
|
if n, _ := fmt.Sscanf(value, "%dx%d+%d+%d", &width, &height, &x, &y); n != 4 {
|
|
values := strings.Fields(value)
|
|
if len(values) != 4 {
|
|
return "", errors.New("oblast nebyla vybrána")
|
|
}
|
|
var err error
|
|
if width, err = strconv.Atoi(values[0]); err != nil {
|
|
return "", fmt.Errorf("neplatná oblast Flameshotu: %q", value)
|
|
}
|
|
if height, err = strconv.Atoi(values[1]); err != nil {
|
|
return "", fmt.Errorf("neplatná oblast Flameshotu: %q", value)
|
|
}
|
|
if x, err = strconv.Atoi(values[2]); err != nil {
|
|
return "", fmt.Errorf("neplatná oblast Flameshotu: %q", value)
|
|
}
|
|
if y, err = strconv.Atoi(values[3]); err != nil {
|
|
return "", fmt.Errorf("neplatná oblast Flameshotu: %q", value)
|
|
}
|
|
}
|
|
if width <= 0 || height <= 0 {
|
|
return "", fmt.Errorf("neplatná oblast Flameshotu: %q", value)
|
|
}
|
|
return fmt.Sprintf("%dx%d+%d+%d", width, height, x, y), nil
|
|
}
|
|
|
|
func captureWaylandRegion() (image.Image, error) {
|
|
selection, err := selectWaylandRegion()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return captureWaylandSelection(selection)
|
|
}
|
|
|
|
func selectWaylandRegion() (string, error) {
|
|
var stderr bytes.Buffer
|
|
slurp := exec.Command("slurp")
|
|
slurp.Stderr = &stderr
|
|
area, err := slurp.Output()
|
|
if err != nil {
|
|
if detail := strings.TrimSpace(stderr.String()); detail != "" {
|
|
return "", fmt.Errorf("výběr oblasti: %w (%s)", err, detail)
|
|
}
|
|
return "", fmt.Errorf("výběr oblasti: %w", err)
|
|
}
|
|
selection := strings.TrimSpace(string(area))
|
|
if selection == "" {
|
|
return "", errors.New("oblast nebyla vybrána")
|
|
}
|
|
return selection, nil
|
|
}
|
|
|
|
func captureWaylandSelection(selection string) (image.Image, error) {
|
|
var stderr bytes.Buffer
|
|
tmp, err := os.CreateTemp("", "fckeuspy-screen-*.png")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
path := tmp.Name()
|
|
if err := tmp.Close(); err != nil {
|
|
_ = os.Remove(path)
|
|
return nil, err
|
|
}
|
|
defer os.Remove(path)
|
|
|
|
stderr.Reset()
|
|
grim := exec.Command("grim", "-g", selection, path)
|
|
grim.Stderr = &stderr
|
|
if err := grim.Run(); err != nil {
|
|
if detail := strings.TrimSpace(stderr.String()); detail != "" {
|
|
return nil, fmt.Errorf("snímek oblasti: %w (%s)", err, detail)
|
|
}
|
|
return nil, fmt.Errorf("snímek oblasti: %w", err)
|
|
}
|
|
return decodeImageFile(path)
|
|
}
|
|
|
|
func selectX11Region() (string, error) {
|
|
var stderr bytes.Buffer
|
|
slop := exec.Command("slop", "-f", "%g")
|
|
slop.Stderr = &stderr
|
|
area, err := slop.Output()
|
|
if err != nil {
|
|
if detail := strings.TrimSpace(stderr.String()); detail != "" {
|
|
return "", fmt.Errorf("výběr oblasti: %w (%s)", err, detail)
|
|
}
|
|
return "", fmt.Errorf("výběr oblasti: %w", err)
|
|
}
|
|
selection := strings.TrimSpace(string(area))
|
|
if selection == "" {
|
|
return "", errors.New("oblast nebyla vybrána")
|
|
}
|
|
return selection, nil
|
|
}
|
|
|
|
func captureToFile(name string, args func(string) []string) (image.Image, error) {
|
|
tmp, err := os.CreateTemp("", "fckeuspy-screen-*.png")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
path := tmp.Name()
|
|
if err := tmp.Close(); err != nil {
|
|
_ = os.Remove(path)
|
|
return nil, err
|
|
}
|
|
defer os.Remove(path)
|
|
|
|
if err := exec.Command(name, args(path)...).Run(); err != nil {
|
|
return nil, err
|
|
}
|
|
return decodeImageFile(path)
|
|
}
|
|
|
|
func decodeImageFile(path string) (image.Image, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
data, err := io.ReadAll(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
img, _, err := image.Decode(bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return img, nil
|
|
}
|