123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
- //
- // SPDX-License-Identifier: GPL-2.0-or-later
- import Gio from 'gi://Gio';
- import GjsPrivate from 'gi://GjsPrivate';
- import GLib from 'gi://GLib';
- import GObject from 'gi://GObject';
- import Meta from 'gi://Meta';
- /*
- * DBus Interface Info
- */
- const DBUS_NAME = 'org.gnome.Shell.Extensions.GSConnect.Clipboard';
- const DBUS_PATH = '/org/gnome/Shell/Extensions/GSConnect/Clipboard';
- const DBUS_NODE = Gio.DBusNodeInfo.new_for_xml(`
- <node>
- <interface name="org.gnome.Shell.Extensions.GSConnect.Clipboard">
- <!-- Methods -->
- <method name="GetMimetypes">
- <arg direction="out" type="as" name="mimetypes"/>
- </method>
- <method name="GetText">
- <arg direction="out" type="s" name="text"/>
- </method>
- <method name="SetText">
- <arg direction="in" type="s" name="text"/>
- </method>
- <method name="GetValue">
- <arg direction="in" type="s" name="mimetype"/>
- <arg direction="out" type="ay" name="value"/>
- </method>
- <method name="SetValue">
- <arg direction="in" type="ay" name="value"/>
- <arg direction="in" type="s" name="mimetype"/>
- </method>
- <!-- Signals -->
- <signal name="OwnerChange"/>
- </interface>
- </node>
- `);
- const DBUS_INFO = DBUS_NODE.lookup_interface(DBUS_NAME);
- /*
- * Text Mimetypes
- */
- const TEXT_MIMETYPES = [
- 'text/plain;charset=utf-8',
- 'UTF8_STRING',
- 'text/plain',
- 'STRING',
- ];
- /* GSConnectClipboardPortal:
- *
- * A simple clipboard portal, especially useful on Wayland where GtkClipboard
- * doesn't work in the background.
- */
- export const Clipboard = GObject.registerClass({
- GTypeName: 'GSConnectShellClipboard',
- }, class GSConnectShellClipboard extends GjsPrivate.DBusImplementation {
- _init(params = {}) {
- super._init({
- g_interface_info: DBUS_INFO,
- });
- this._transferring = false;
- // Watch global selection
- this._selection = global.display.get_selection();
- this._ownerChangedId = this._selection.connect(
- 'owner-changed',
- this._onOwnerChanged.bind(this)
- );
- // Prepare DBus interface
- this._handleMethodCallId = this.connect(
- 'handle-method-call',
- this._onHandleMethodCall.bind(this)
- );
- this._nameId = Gio.DBus.own_name(
- Gio.BusType.SESSION,
- DBUS_NAME,
- Gio.BusNameOwnerFlags.NONE,
- this._onBusAcquired.bind(this),
- null,
- this._onNameLost.bind(this)
- );
- }
- _onOwnerChanged(selection, type, source) {
- /* We're only interested in the standard clipboard */
- if (type !== Meta.SelectionType.SELECTION_CLIPBOARD)
- return;
- /* In Wayland an intermediate GMemoryOutputStream is used which triggers
- * a second ::owner-changed emission, so we need to ensure we ignore
- * that while the transfer is resolving.
- */
- if (this._transferring)
- return;
- this._transferring = true;
- /* We need to put our signal emission in an idle callback to ensure that
- * Mutter's internal calls have finished resolving in the loop, or else
- * we'll end up with the previous selection's content.
- */
- GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
- this.emit_signal('OwnerChange', null);
- this._transferring = false;
- return GLib.SOURCE_REMOVE;
- });
- }
- _onBusAcquired(connection, name) {
- try {
- this.export(connection, DBUS_PATH);
- } catch (e) {
- logError(e);
- }
- }
- _onNameLost(connection, name) {
- try {
- this.unexport();
- } catch (e) {
- logError(e);
- }
- }
- async _onHandleMethodCall(iface, name, parameters, invocation) {
- let retval;
- try {
- const args = parameters.recursiveUnpack();
- retval = await this[name](...args);
- } catch (e) {
- if (e instanceof GLib.Error) {
- invocation.return_gerror(e);
- } else {
- if (!e.name.includes('.'))
- e.name = `org.gnome.gjs.JSError.${e.name}`;
- invocation.return_dbus_error(e.name, e.message);
- }
- return;
- }
- if (retval === undefined)
- retval = new GLib.Variant('()', []);
- try {
- if (!(retval instanceof GLib.Variant)) {
- const args = DBUS_INFO.lookup_method(name).out_args;
- retval = new GLib.Variant(
- `(${args.map(arg => arg.signature).join('')})`,
- (args.length === 1) ? [retval] : retval
- );
- }
- invocation.return_value(retval);
- // Without a response, the client will wait for timeout
- } catch (e) {
- invocation.return_dbus_error(
- 'org.gnome.gjs.JSError.ValueError',
- 'Service implementation returned an incorrect value type'
- );
- }
- }
- /**
- * Get the available mimetypes of the current clipboard content
- *
- * @return {Promise<string[]>} A list of mime-types
- */
- GetMimetypes() {
- return new Promise((resolve, reject) => {
- try {
- const mimetypes = this._selection.get_mimetypes(
- Meta.SelectionType.SELECTION_CLIPBOARD
- );
- resolve(mimetypes);
- } catch (e) {
- reject(e);
- }
- });
- }
- /**
- * Get the text content of the clipboard
- *
- * @return {Promise<string>} Text content of the clipboard
- */
- GetText() {
- return new Promise((resolve, reject) => {
- const mimetypes = this._selection.get_mimetypes(
- Meta.SelectionType.SELECTION_CLIPBOARD);
- const mimetype = TEXT_MIMETYPES.find(type => mimetypes.includes(type));
- if (mimetype !== undefined) {
- const stream = Gio.MemoryOutputStream.new_resizable();
- this._selection.transfer_async(
- Meta.SelectionType.SELECTION_CLIPBOARD,
- mimetype, -1,
- stream, null,
- (selection, res) => {
- try {
- selection.transfer_finish(res);
- const bytes = stream.steal_as_bytes();
- const bytearray = bytes.get_data();
- resolve(new TextDecoder().decode(bytearray));
- } catch (e) {
- reject(e);
- }
- }
- );
- } else {
- reject(new Error('text not available'));
- }
- });
- }
- /**
- * Set the text content of the clipboard
- *
- * @param {string} text - text content to set
- * @return {Promise} A promise for the operation
- */
- SetText(text) {
- return new Promise((resolve, reject) => {
- try {
- if (typeof text !== 'string') {
- throw new Gio.DBusError({
- code: Gio.DBusError.INVALID_ARGS,
- message: 'expected string',
- });
- }
- const source = Meta.SelectionSourceMemory.new(
- 'text/plain;charset=utf-8', GLib.Bytes.new(text));
- this._selection.set_owner(
- Meta.SelectionType.SELECTION_CLIPBOARD, source);
- resolve();
- } catch (e) {
- reject(e);
- }
- });
- }
- /**
- * Get the content of the clipboard with the type @mimetype.
- *
- * @param {string} mimetype - the mimetype to request
- * @return {Promise<Uint8Array>} The content of the clipboard
- */
- GetValue(mimetype) {
- return new Promise((resolve, reject) => {
- const stream = Gio.MemoryOutputStream.new_resizable();
- this._selection.transfer_async(
- Meta.SelectionType.SELECTION_CLIPBOARD,
- mimetype, -1,
- stream, null,
- (selection, res) => {
- try {
- selection.transfer_finish(res);
- const bytes = stream.steal_as_bytes();
- resolve(bytes.get_data());
- } catch (e) {
- reject(e);
- }
- }
- );
- });
- }
- /**
- * Set the content of the clipboard to @value with the type @mimetype.
- *
- * @param {Uint8Array} value - the value to set
- * @param {string} mimetype - the mimetype of the value
- * @return {Promise} - A promise for the operation
- */
- SetValue(value, mimetype) {
- return new Promise((resolve, reject) => {
- try {
- const source = Meta.SelectionSourceMemory.new(mimetype,
- GLib.Bytes.new(value));
- this._selection.set_owner(
- Meta.SelectionType.SELECTION_CLIPBOARD, source);
- resolve();
- } catch (e) {
- reject(e);
- }
- });
- }
- destroy() {
- if (this._selection && this._ownerChangedId > 0) {
- this._selection.disconnect(this._ownerChangedId);
- this._ownerChangedId = 0;
- }
- if (this._nameId > 0) {
- Gio.bus_unown_name(this._nameId);
- this._nameId = 0;
- }
- if (this._handleMethodCallId > 0) {
- this.disconnect(this._handleMethodCallId);
- this._handleMethodCallId = 0;
- this.unexport();
- }
- }
- });
- let _portal = null;
- let _portalId = 0;
- /**
- * Watch for the service to start and export the clipboard portal when it does.
- */
- export function watchService() {
- if (GLib.getenv('XDG_SESSION_TYPE') !== 'wayland')
- return;
- if (_portalId > 0)
- return;
- _portalId = Gio.bus_watch_name(
- Gio.BusType.SESSION,
- 'org.gnome.Shell.Extensions.GSConnect',
- Gio.BusNameWatcherFlags.NONE,
- () => {
- if (_portal === null)
- _portal = new Clipboard();
- },
- () => {
- if (_portal !== null) {
- _portal.destroy();
- _portal = null;
- }
- }
- );
- }
- /**
- * Stop watching the service and export the portal if currently running.
- */
- export function unwatchService() {
- if (_portalId > 0) {
- Gio.bus_unwatch_name(_portalId);
- _portalId = 0;
- }
- }
|