1import Gio from "gi://Gio";
2import GLib from "gi://GLib";
3import Meta from "gi://Meta";
4import Shell from "gi://Shell";
5import St from "gi://St";
6import { Extension } from "resource:///org/gnome/shell/extensions/extension.js";
7import * as Main from "resource:///org/gnome/shell/ui/main.js";
8import { CaptureService, isWaylandSession } from "./captureService.js";
9import { CaptureFeedback } from "./captureFeedback.js";
10
11const NAME = "org.gnome.Shell.Extensions.T3SnapShot";
12const PATH = "/org/gnome/Shell/Extensions/T3SnapShot";
13const XML = `<node><interface name="${NAME}">
14 <property name="Version" type="u" access="read"/>
15 <method name="Capture">
16 <arg name="png" type="ay" direction="out"/>
17 <arg name="metadata" type="s" direction="out"/>
18 </method>
19 <method name="CaptureWithFeedback">
20 <arg name="flash" type="b" direction="in"/>
21 <arg name="animate" type="b" direction="in"/>
22 <arg name="png" type="ay" direction="out"/>
23 <arg name="metadata" type="s" direction="out"/>
24 <arg name="animationStarted" type="b" direction="out"/>
25 </method>
26 <method name="Activate"><arg name="title" type="s" direction="in"/></method>
27 <method name="Animate">
28 <arg name="x" type="d" direction="in"/>
29 <arg name="y" type="d" direction="in"/>
30 <arg name="width" type="d" direction="in"/>
31 <arg name="height" type="d" direction="in"/>
32 </method>
33</interface></node>`;
34
35function busIdentity(method, name, resultType) {
36 return new Promise((resolve, reject) => {
37 Gio.DBus.session.call(
38 "org.freedesktop.DBus",
39 "/org/freedesktop/DBus",
40 "org.freedesktop.DBus",
41 method,
42 new GLib.Variant("(s)", [name]),
43 new GLib.VariantType(resultType),
44 Gio.DBusCallFlags.NONE,
45 5_000,
46 null,
47 (connection, result) => {
48 try {
49 resolve(connection.call_finish(result).deepUnpack()[0]);
50 } catch (error) {
51 if (error.matches(Gio.DBusError, Gio.DBusError.NAME_HAS_NO_OWNER)) resolve(null);
52 else reject(error);
53 }
54 },
55 );
56 });
57}
58
59const getNameOwner = (name) => busIdentity("GetNameOwner", name, "(s)");
60const getProcessId = (sender) => busIdentity("GetConnectionUnixProcessID", sender, "(u)");
61
62function takeSnapshot(animate) {
63 // No await between reading identity and Shell taking its snapshot of the focused actor.
64 const window = global.display.focus_window;
65 if (!window || window.minimized || !window.get_compositor_private()) {
66 throw new Error("No active window is available for capture.");
67 }
68 const app = Shell.WindowTracker.get_default().get_window_app(window);
69 const bounds = window.get_frame_rect();
70 const bufferBounds = window.get_buffer_rect();
71 let content;
72 if (animate && St.Settings.get().enable_animations) {
73 try {
74 content = window.get_compositor_private().paint_to_content(null);
75 } catch (error) {
76 console.warn(`T3 capture preview unavailable: ${error.message}`);
77 }
78 }
79 const metadata = JSON.stringify({
80 title: window.get_title() ?? "",
81 appName: app?.get_name() ?? "",
82 appIdentifier: app?.get_id() ?? "",
83 processId: Math.max(0, window.get_pid()),
84 bounds: { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height },
85 });
86 const stream = Gio.MemoryOutputStream.new_resizable();
87 const screenshot = new Shell.Screenshot();
88 return new Promise((resolve, reject) => {
89 screenshot.screenshot_window(true, false, stream, (source, result) => {
90 try {
91 const [success] = source.screenshot_window_finish(result);
92 if (!success) throw new Error("GNOME could not capture the active window.");
93 stream.close(null);
94 if (stream.get_data_size() > 32 * 1024 * 1024)
95 throw new Error("The screenshot is too large.");
96 resolve({
97 png: stream.steal_as_bytes().get_data(),
98 metadata,
99 content,
100 bounds: { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height },
101 bufferBounds: {
102 x: bufferBounds.x,
103 y: bufferBounds.y,
104 width: bufferBounds.width,
105 height: bufferBounds.height,
106 },
107 });
108 } catch (error) {
109 reject(error);
110 } finally {
111 if (!stream.is_closed()) stream.close(null);
112 }
113 });
114 });
115}
116
117export default class T3SnapShotExtension extends Extension {
118 enable() {
119 this._feedback = new CaptureFeedback();
120 this._sessionChanged = Main.sessionMode.connect("updated", () => this._feedback.dispose());
121 this._monitorsChanged = Main.layoutManager.connect("monitors-changed", () =>
122 this._feedback.dispose(),
123 );
124 this._service = new CaptureService({
125 getNameOwner,
126 getProcessId,
127 beginFeedback: (...args) => this._feedback.begin(...args),
128 isAvailable: () =>
129 isWaylandSession(Meta) && !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter,
130 takeSnapshot,
131 });
132 this._object = Gio.DBusExportedObject.wrapJSObject(XML, this);
133 this._object.export(Gio.DBus.session, PATH);
134 this._owner = Gio.bus_own_name_on_connection(
135 Gio.DBus.session,
136 NAME,
137 Gio.BusNameOwnerFlags.NONE,
138 null,
139 null,
140 );
141 }
142
143 get Version() {
144 return 2;
145 }
146
147 CaptureAsync(_params, invocation) {
148 void this._service
149 .capture(invocation.get_sender())
150 .then(({ png, metadata }) =>
151 invocation.return_value(new GLib.Variant("(ays)", [png, metadata])),
152 )
153 .catch((error) => invocation.return_dbus_error(`${NAME}.Failed`, error.message));
154 }
155
156 CaptureWithFeedbackAsync([flash, animate], invocation) {
157 void this._service
158 .capture(invocation.get_sender(), { flash, animate })
159 .then(({ png, metadata, animationStarted }) =>
160 invocation.return_value(new GLib.Variant("(aysb)", [png, metadata, animationStarted])),
161 )
162 .catch((error) => invocation.return_dbus_error(`${NAME}.Failed`, error.message));
163 }
164
165 ActivateAsync([title], invocation) {
166 void this._feedback
167 .activate(invocation.get_sender(), title)
168 .then(() => invocation.return_value(null))
169 .catch((error) => invocation.return_dbus_error(`${NAME}.Failed`, error.message));
170 }
171
172 AnimateAsync([x, y, width, height], invocation) {
173 void this._feedback
174 .animate(invocation.get_sender(), { x, y, width, height })
175 .then(() => invocation.return_value(null))
176 .catch((error) => invocation.return_dbus_error(`${NAME}.Failed`, error.message));
177 }
178
179 disable() {
180 this._service?.disable();
181 this._feedback?.dispose();
182 if (this._sessionChanged) Main.sessionMode.disconnect(this._sessionChanged);
183 if (this._monitorsChanged) Main.layoutManager.disconnect(this._monitorsChanged);
184 this._sessionChanged = 0;
185 this._monitorsChanged = 0;
186 this._object?.unexport();
187 if (this._owner) Gio.bus_unown_name(this._owner);
188 this._service = null;
189 this._object = null;
190 this._owner = null;
191 }
192}