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 }