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
+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-}
M
ui/ui.go
+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