mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-07 22:18:30 -04:00
screenshot: initial support for scroll capture mode
fixes #2871 port 1.5
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
package screenshot
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
const niriScreenshotTimeout = 5 * time.Second
|
||||
|
||||
// CaptureNiriWindowImage captures the focused window through niri's
|
||||
// ScreenshotWindow action; niri replies before writing the file, so a second
|
||||
// event-stream connection waits for ScreenshotCaptured. niri also copies the
|
||||
// capture to its own clipboard, which cannot be disabled.
|
||||
func CaptureNiriWindowImage(showPointer bool) (image.Image, error) {
|
||||
socket := os.Getenv("NIRI_SOCKET")
|
||||
if socket == "" {
|
||||
return nil, fmt.Errorf("NIRI_SOCKET not set")
|
||||
}
|
||||
|
||||
dir := os.Getenv("XDG_RUNTIME_DIR")
|
||||
if dir == "" {
|
||||
dir = os.TempDir()
|
||||
}
|
||||
path := filepath.Join(dir, fmt.Sprintf("dms-window-%d.png", os.Getpid()))
|
||||
|
||||
events, err := subscribeNiriEvents(socket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer events.Close()
|
||||
|
||||
if err := requestNiriWindowScreenshot(socket, path, showPointer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(path)
|
||||
|
||||
if err := awaitNiriScreenshot(events, path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open niri screenshot: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
img, err := png.Decode(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode niri screenshot: %w", err)
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
func subscribeNiriEvents(socket string) (net.Conn, error) {
|
||||
conn, err := net.DialTimeout("unix", socket, 2*time.Second)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect niri socket: %w", err)
|
||||
}
|
||||
_ = conn.SetDeadline(time.Now().Add(niriScreenshotTimeout))
|
||||
|
||||
if _, err := conn.Write([]byte("\"EventStream\"\n")); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("subscribe niri events: %w", err)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func awaitNiriScreenshot(events net.Conn, path string) error {
|
||||
scanner := bufio.NewScanner(events)
|
||||
scanner.Buffer(make([]byte, 0, 64<<10), 1<<20)
|
||||
|
||||
for scanner.Scan() {
|
||||
var event struct {
|
||||
ScreenshotCaptured *struct {
|
||||
Path string `json:"path"`
|
||||
} `json:"ScreenshotCaptured"`
|
||||
}
|
||||
if json.Unmarshal(scanner.Bytes(), &event) != nil {
|
||||
continue
|
||||
}
|
||||
if event.ScreenshotCaptured != nil && event.ScreenshotCaptured.Path == path {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("await niri screenshot: %w", err)
|
||||
}
|
||||
return fmt.Errorf("niri event stream closed before screenshot completed")
|
||||
}
|
||||
|
||||
func requestNiriWindowScreenshot(socket, path string, showPointer bool) error {
|
||||
conn, err := net.DialTimeout("unix", socket, 2*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect niri socket: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(3 * time.Second))
|
||||
|
||||
request := map[string]any{
|
||||
"Action": map[string]any{
|
||||
"ScreenshotWindow": map[string]any{
|
||||
"id": nil,
|
||||
"write_to_disk": true,
|
||||
"show_pointer": showPointer,
|
||||
"path": path,
|
||||
},
|
||||
},
|
||||
}
|
||||
payload, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := conn.Write(append(payload, '\n')); err != nil {
|
||||
return fmt.Errorf("niri request: %w", err)
|
||||
}
|
||||
|
||||
line, err := bufio.NewReader(conn).ReadBytes('\n')
|
||||
if err != nil {
|
||||
return fmt.Errorf("niri reply: %w", err)
|
||||
}
|
||||
|
||||
var reply map[string]json.RawMessage
|
||||
if err := json.Unmarshal(line, &reply); err != nil {
|
||||
return fmt.Errorf("parse niri reply: %w", err)
|
||||
}
|
||||
if raw, ok := reply["Err"]; ok {
|
||||
var msg string
|
||||
_ = json.Unmarshal(raw, &msg)
|
||||
return fmt.Errorf("niri screenshot: %s", msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Screenshoter) captureNiriWindow() (*CaptureResult, error) {
|
||||
img, err := CaptureNiriWindowImage(s.config.Cursor == CursorOn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf, err := ImageToBuffer(img)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scale := 1.0
|
||||
if output := s.findOutputByName(GetFocusedMonitor()); output != nil {
|
||||
scale = output.effectiveScale()
|
||||
}
|
||||
|
||||
return &CaptureResult{
|
||||
Buffer: buf,
|
||||
YInverted: false,
|
||||
Format: uint32(FormatARGB8888),
|
||||
Scale: scale,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user