From fbd5c87dbf563ea897f1e7274fddf839170a652e Mon Sep 17 00:00:00 2001 From: Johan Jongsma Date: Sun, 1 Feb 2026 08:04:10 +0000 Subject: [PATCH] Initial commit --- .gitignore | 5 ++ README.md | 53 ++++++++++++++ com.inou.screenshot-server.plist | 20 ++++++ main.go | 117 +++++++++++++++++++++++++++++++ 4 files changed, 195 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 com.inou.screenshot-server.plist create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d82971 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.env +*.log +.DS_Store + diff --git a/README.md b/README.md new file mode 100644 index 0000000..1774eb0 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# Screenshot Server + +Tiny HTTP server that serves the latest screenshot from ~/Desktop, then deletes it. + +## Build (on Mac) + +```bash +cd /path/to/screenshot-server +go build -o screenshot-server +``` + +## Install as service + +```bash +# Copy binary +sudo cp screenshot-server /usr/local/bin/ + +# Install launchd service +cp com.inou.screenshot-server.plist ~/Library/LaunchAgents/ + +# Start it +launchctl load ~/Library/LaunchAgents/com.inou.screenshot-server.plist + +# Check status +launchctl list | grep screenshot +``` + +## Usage + +Take a screenshot (Cmd+Shift+4), then: + +```bash +curl http://192.168.1.14:9123/screenshot -o screenshot.png +``` + +## Endpoints + +- `GET /screenshot` - Returns latest screenshot PNG, then deletes it +- `GET /health` - Returns "ok" + +## Uninstall + +```bash +launchctl unload ~/Library/LaunchAgents/com.inou.screenshot-server.plist +rm ~/Library/LaunchAgents/com.inou.screenshot-server.plist +sudo rm /usr/local/bin/screenshot-server +``` + +## Logs + +```bash +tail -f /tmp/screenshot-server.log +``` diff --git a/com.inou.screenshot-server.plist b/com.inou.screenshot-server.plist new file mode 100644 index 0000000..9602dac --- /dev/null +++ b/com.inou.screenshot-server.plist @@ -0,0 +1,20 @@ + + + + + Label + com.inou.screenshot-server + ProgramArguments + + /usr/local/bin/screenshot-server + + RunAtLoad + + KeepAlive + + StandardOutPath + /tmp/screenshot-server.log + StandardErrorPath + /tmp/screenshot-server.log + + diff --git a/main.go b/main.go new file mode 100644 index 0000000..d94768d --- /dev/null +++ b/main.go @@ -0,0 +1,117 @@ +// 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" + "path/filepath" + "sort" + "strings" +) + +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") +} + +func findLatestScreenshot(desktop string) (string, error) { + entries, err := os.ReadDir(desktop) + if err != nil { + return "", err + } + + var screenshots []os.DirEntry + for _, e := range entries { + name := e.Name() + if strings.HasPrefix(name, "Screenshot") && strings.HasSuffix(name, ".png") { + screenshots = append(screenshots, e) + } + } + + if len(screenshots) == 0 { + return "", fmt.Errorf("no screenshots found") + } + + // Sort by modification time (newest first) + sort.Slice(screenshots, func(i, j int) bool { + infoI, _ := screenshots[i].Info() + infoJ, _ := screenshots[j].Info() + return infoI.ModTime().After(infoJ.ModTime()) + }) + + return filepath.Join(desktop, screenshots[0].Name()), nil +} + +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 + } + + // Open and serve + 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 + } + + // Delete after successful send + f.Close() + if err := os.Remove(path); err != nil { + log.Println("Warning: could not delete:", err) + } else { + log.Println("Served and deleted:", filename) + } +} + +func healthHandler(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("ok")) +} + +func main() { + http.HandleFunc("/screenshot", screenshotHandler) + 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(" GET /health - health check") + + if err := http.ListenAndServe(port, nil); err != nil { + log.Fatal(err) + } +}