commit 8a77883

delthas  ·  2025-09-24 10:10:25 +0000 UTC
parent efec872
Add /SCREENSHOT to take and upload screenshots
7 files changed,  +101, -29
M app.go
M app.go
+15, -3
 1@@ -1,6 +1,7 @@
 2 package senpai
 3 
 4 import (
 5+	"context"
 6 	"crypto/tls"
 7 	"errors"
 8 	"fmt"
 9@@ -28,7 +29,6 @@ import (
10 	"unicode/utf8"
11 
12 	"git.sr.ht/~rockorager/vaxis"
13-	"golang.org/x/net/context"
14 	"golang.org/x/net/proxy"
15 
16 	"git.sr.ht/~delthas/senpai/events"
17@@ -223,7 +223,7 @@ func NewApp(cfg Config) (app *App, err error) {
18 		return
19 	}
20 
21-	ui.NotifyStart(func(ev *ui.NotifyEvent) {
22+	ui.DBusStart(func(ev any) {
23 		app.postEvent(event{
24 			src:     "*",
25 			content: ev,
26@@ -248,7 +248,7 @@ func (app *App) Close() {
27 	for _, session := range app.sessions {
28 		session.Close()
29 	}
30-	ui.NotifyStop()
31+	ui.DBusStop()
32 	app.closing.Store(true)
33 	go func() {
34 		// drain remaining events
35@@ -594,6 +594,18 @@ func (app *App) handleUIEvent(ev interface{}) bool {
36 		app.win.SetColorTheme(ev.Mode)
37 	case *ui.NotifyEvent:
38 		app.win.JumpBufferNetwork(ev.NetID, ev.Buffer)
39+	case *ui.ScreenshotEvent:
40+		if err := commandDoUpload(app, []string{ev.Path}); err != nil {
41+			netID, buffer := app.win.CurrentBuffer()
42+			app.win.AddLine(netID, buffer, ui.Line{
43+				At:        time.Now(),
44+				Head:      "!!",
45+				HeadColor: ui.ColorRed,
46+				Notify:    ui.NotifyUnread,
47+				Body:      ui.PlainSprintf("SCREENSHOT: %s", err),
48+			})
49+			break
50+		}
51 	case statusLine:
52 		app.addStatusLine(ev.netID, ev.line)
53 	case *events.EventClickNick:
+13, -1
 1@@ -1,6 +1,7 @@
 2 package senpai
 3 
 4 import (
 5+	"context"
 6 	"errors"
 7 	"fmt"
 8 	"net/url"
 9@@ -13,7 +14,6 @@ import (
10 
11 	"git.sr.ht/~rockorager/vaxis"
12 	"github.com/delthas/go-libnp"
13-	"golang.org/x/net/context"
14 
15 	"git.sr.ht/~delthas/senpai/irc"
16 	"git.sr.ht/~delthas/senpai/ui"
17@@ -83,6 +83,11 @@ func init() {
18 			Desc:      "upload a local file to the bouncer",
19 			Handle:    commandDoUpload,
20 		},
21+		"SCREENSHOT": {
22+			AllowHome: true,
23+			Desc:      "take and upload a screenshot to the bouncer",
24+			Handle:    commandDoScreenshot,
25+		},
26 		"MSG": {
27 			AllowHome: true,
28 			MinArgs:   2,
29@@ -541,6 +546,13 @@ func commandDoUpload(app *App, args []string) (err error) {
30 	return nil
31 }
32 
33+func commandDoScreenshot(app *App, args []string) (err error) {
34+	if app.cfg.Transient || !app.cfg.LocalIntegrations {
35+		return fmt.Errorf("usage of SCREENSHOT is disabled")
36+	}
37+	return ui.Screenshot()
38+}
39+
40 func commandDoMsg(app *App, args []string) (err error) {
41 	target := args[0]
42 	content := args[1]
+6, -0
 1@@ -230,6 +230,12 @@ _name_ is matched case-insensitively.  It can be one of the following:
 2 *UPLOAD* <file path>
 3 	Upload a local file to the bouncer.
 4 
 5+*SCREENSHOT*
 6+	Take and upload a screenshot to the bouncer.
 7+
 8+	Requires support for D-Bus and xdg-desktop-portal (e.g. Gnome, or
 9+	xdg-desktop-portal-wlr on sway).
10+
11 *QUOTE* <raw message>
12 	Send _raw message_ verbatim.
13 
R ui/notify_fallback.go => ui/dbus_fallback.go
+6, -3
 1@@ -1,5 +1,4 @@
 2 //go:build !linux
 3-// +build !linux
 4 
 5 package ui
 6 
 7@@ -10,6 +9,10 @@ func (ui *UI) notify(target NotifyEvent, title, content string) int {
 8 
 9 func notifyClose(id int) {}
10 
11-func NotifyStart(f func(event *NotifyEvent)) {}
12+func Screenshot() error {
13+	return fmt.Errorf("failed to take screenshot: D-Bus is disabled")
14+}
15+
16+func DBusStart(callback func(any)) {}
17 
18-func NotifyStop() {}
19+func DBusStop() {}
R ui/notify_linux.go => ui/dbus_linux.go
+52, -16
  1@@ -1,17 +1,19 @@
  2 //go:build linux
  3-// +build linux
  4 
  5 package ui
  6 
  7 import (
  8+	"fmt"
  9+	"net/url"
 10 	"sync"
 11 
 12 	"github.com/godbus/dbus/v5"
 13 )
 14 
 15-var notificationsLock sync.Mutex
 16-var notifications = make(map[int]*NotifyEvent)
 17 var dbusConn *dbus.Conn
 18+var dbusLock sync.Mutex
 19+
 20+var notifications = make(map[int]*NotifyEvent)
 21 
 22 func notifyDBus(title, content string) int {
 23 	conn, err := dbus.SessionBus()
 24@@ -38,9 +40,9 @@ func (ui *UI) notify(target NotifyEvent, title, content string) int {
 25 	if ui.config.LocalIntegrations {
 26 		id := notifyDBus(title, content)
 27 		if id > 0 {
 28-			notificationsLock.Lock()
 29+			dbusLock.Lock()
 30 			notifications[id] = &target
 31-			notificationsLock.Unlock()
 32+			dbusLock.Unlock()
 33 			return id
 34 		}
 35 	}
 36@@ -58,7 +60,23 @@ func notifyClose(id int) {
 37 	obj.Call("org.freedesktop.Notifications.CloseNotification", 0, uint32(id))
 38 }
 39 
 40-func NotifyStart(opened func(*NotifyEvent)) {
 41+func Screenshot() error {
 42+	conn, err := dbus.SessionBus()
 43+	if err != nil {
 44+		return fmt.Errorf("failed to connect to D-Bus: %v", err)
 45+	}
 46+	obj := conn.Object("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop")
 47+	c := obj.Call("org.freedesktop.portal.Screenshot.Screenshot", 0, "", map[string]dbus.Variant{
 48+		"modal":       dbus.MakeVariant(true),
 49+		"interactive": dbus.MakeVariant(true),
 50+	})
 51+	if c.Err != nil {
 52+		return fmt.Errorf("failed to take screenshot: %v", c.Err)
 53+	}
 54+	return nil
 55+}
 56+
 57+func DBusStart(callback func(any)) {
 58 	conn, err := dbus.SessionBus()
 59 	if err != nil {
 60 		return
 61@@ -70,37 +88,55 @@ func NotifyStart(opened func(*NotifyEvent)) {
 62 	); err != nil {
 63 		return
 64 	}
 65+	if err := conn.AddMatchSignal(
 66+		dbus.WithMatchObjectPath("/org/freedesktop/portal/desktop"),
 67+		dbus.WithMatchInterface("org.freedesktop.portal.Request"),
 68+		dbus.WithMatchMember("Response"),
 69+	); err != nil {
 70+		return
 71+	}
 72 	c := make(chan *dbus.Signal, 64)
 73 	conn.Signal(c)
 74-	notificationsLock.Lock()
 75+	dbusLock.Lock()
 76 	dbusConn = conn
 77-	notificationsLock.Unlock()
 78+	dbusLock.Unlock()
 79 	go func() {
 80 		for v := range c {
 81 			switch v.Name {
 82 			case "org.freedesktop.Notifications.NotificationClosed":
 83 				id := int(v.Body[0].(uint32))
 84-				notificationsLock.Lock()
 85+				dbusLock.Lock()
 86 				delete(notifications, id)
 87-				notificationsLock.Unlock()
 88+				dbusLock.Unlock()
 89 			case "org.freedesktop.Notifications.ActionInvoked":
 90 				id := int(v.Body[0].(uint32))
 91-				notificationsLock.Lock()
 92+				dbusLock.Lock()
 93 				target, ok := notifications[id]
 94-				notificationsLock.Unlock()
 95+				dbusLock.Unlock()
 96 				if ok {
 97-					opened(target)
 98+					callback(target)
 99+				}
100+			case "org.freedesktop.portal.Request.Response":
101+				status := v.Body[0].(uint32)
102+				results := v.Body[1].(map[string]dbus.Variant)
103+				if status == 0 /* success */ {
104+					uri := results["uri"].Value().(string)
105+					if u, err := url.Parse(uri); err == nil && u.Scheme == "file" {
106+						callback(&ScreenshotEvent{
107+							Path: u.Path,
108+						})
109+					}
110 				}
111 			}
112 		}
113 	}()
114 }
115 
116-func NotifyStop() {
117-	notificationsLock.Lock()
118+func DBusStop() {
119+	dbusLock.Lock()
120 	c := dbusConn
121 	dbusConn = nil
122-	notificationsLock.Unlock()
123+	dbusLock.Unlock()
124 	if c != nil {
125 		c.Close()
126 	}
+0, -6
1@@ -1,6 +0,0 @@
2-package ui
3-
4-type NotifyEvent struct {
5-	NetID  string
6-	Buffer string
7-}
+9, -0
 1@@ -47,6 +47,15 @@ type Vaxis struct {
 2 	window vaxis.Window
 3 }
 4 
 5+type NotifyEvent struct {
 6+	NetID  string
 7+	Buffer string
 8+}
 9+
10+type ScreenshotEvent struct {
11+	Path string
12+}
13+
14 type clickEvent struct {
15 	xb    int
16 	xe    int