// screenshot-server: Serves the latest screenshot from ~/Desktop, then deletes it // Run: go build -o screenshot-server && ./screenshot-server // Fetch: curl http://mac-ip:9123/screenshot -o screenshot.png package main import ( "fmt" "io" "log" "net/http" "os" "os/exec" "path/filepath" "regexp" "sort" "strconv" "strings" "time" ) const ( port = ":9123" desktopDir = "" // Will be set to ~/Desktop ) func getDesktopPath() string { home, err := os.UserHomeDir() if err != nil { log.Fatal("Cannot find home directory:", err) } return filepath.Join(home, "Desktop") } type screenshotFile struct { path string modTime time.Time } func findLatestScreenshot(desktop string) (string, error) { entries, err := os.ReadDir(desktop) if err != nil { return "", err } var screenshots []screenshotFile for _, e := range entries { name := e.Name() if strings.HasPrefix(name, "Screenshot") && strings.HasSuffix(name, ".png") { info, err := e.Info() if err != nil { continue } screenshots = append(screenshots, screenshotFile{ path: filepath.Join(desktop, name), modTime: info.ModTime(), }) } } if len(screenshots) == 0 { return "", fmt.Errorf("no screenshots found") } // Sort by modification time (newest first) sort.Slice(screenshots, func(i, j int) bool { return screenshots[i].modTime.After(screenshots[j].modTime) }) return screenshots[0].path, nil } func serveAndDelete(w http.ResponseWriter, path string) { f, err := os.Open(path) if err != nil { http.Error(w, "Cannot open file", http.StatusInternalServerError) return } defer f.Close() filename := filepath.Base(path) w.Header().Set("Content-Type", "image/png") w.Header().Set("X-Screenshot-Name", filename) _, err = io.Copy(w, f) if err != nil { log.Println("Error sending:", err) return } f.Close() if err := os.Remove(path); err != nil { log.Println("Warning: could not delete:", err) } else { log.Println("Served and deleted:", filename) } } func screenshotHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "GET only", http.StatusMethodNotAllowed) return } desktop := getDesktopPath() path, err := findLatestScreenshot(desktop) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) log.Println("No screenshot:", err) return } serveAndDelete(w, path) } func captureHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "POST only", http.StatusMethodNotAllowed) return } display := r.URL.Query().Get("display") if display == "" { display = "1" } tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("capture-%d.png", time.Now().UnixNano())) cmd := exec.Command("screencapture", "-x", "-D", display, tmpFile) if out, err := cmd.CombinedOutput(); err != nil { http.Error(w, fmt.Sprintf("capture failed: %v: %s", err, out), http.StatusInternalServerError) log.Printf("Capture failed (display %s): %v: %s", display, err, out) os.Remove(tmpFile) return } // Resize to 50% using sips if out, err := exec.Command("sips", "-g", "pixelWidth", tmpFile).Output(); err == nil { re := regexp.MustCompile(`pixelWidth:\s*(\d+)`) if m := re.FindSubmatch(out); m != nil { if w, _ := strconv.Atoi(string(m[1])); w > 0 { exec.Command("sips", "--resampleWidth", strconv.Itoa(w/2), tmpFile).Run() } } } log.Printf("Captured display %s", display) serveAndDelete(w, tmpFile) } func healthHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) } func main() { http.HandleFunc("/screenshot", screenshotHandler) http.HandleFunc("/capture", captureHandler) http.HandleFunc("/health", healthHandler) log.Printf("Screenshot server starting on %s", port) log.Printf("Desktop: %s", getDesktopPath()) log.Printf("Endpoints:") log.Printf(" GET /screenshot - fetch & delete latest screenshot") log.Printf(" POST /capture?display=N - take screenshot of display N") log.Printf(" GET /health - health check") if err := http.ListenAndServe(port, nil); err != nil { log.Fatal(err) } }