// SPDX-FileCopyrightText: JingMatrix https://github.com/JingMatrix // // 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'; // laucher for wl-clipboard const launcher = new Gio.SubprocessLauncher({ flags: Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_MERGE, }); /* * 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(` `); 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() { super._init({ g_interface_info: DBUS_INFO, }); this._transferring = false; this.watcher = Gio.Subprocess.new([ 'wl-paste', '-w', 'dbus-send', DBUS_PATH, '--dest=' + DBUS_NAME, DBUS_NAME + '.OwnerChange', ], Gio.SubprocessFlags.NONE); // 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) ); } _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} A list of mime-types */ GetMimetypes() { return new Promise((resolve, reject) => { const proc = launcher.spawnv([ 'wl-paste', '--list-types', '-n', ]); proc.communicate_utf8_async(null, null, (proc, res) => { try { const [, stdout, stderr] = proc.communicate_utf8_finish(res); if (proc.get_successful()) resolve(stdout.trim().split('\n')); else logError(stderr); } catch (e) { reject(e); } }); }); } /** * Get the text content of the clipboard * * @return {Promise} Text content of the clipboard */ GetText() { return new Promise((resolve, reject) => { this.GetMimetypes().then((mimetypes) => { const mimetype = TEXT_MIMETYPES.find((type) => mimetypes.includes(type) ); if (mimetype !== undefined) { const proc = launcher.spawnv(['wl-paste', '-n']); proc.communicate_utf8_async(null, null, (proc, res) => { try { const [, stdout, stderr] = proc.communicate_utf8_finish(res); if (proc.get_successful()) resolve(stdout); else logError(stderr); } 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', }); } launcher.spawnv(['wl-copy', text]); resolve(); } catch (e) { reject(e); } }); } destroy() { 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(); } if (this.watcher) this.watcher.force_exit(); } } ); let _portal = null; let _portalId = 0; /** * Watch for the service to start and export the clipboard portal when it does. */ export function watchService() { 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; } } // vim:tabstop=2:shiftwidth=2:expandtab