wl_clipboard.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. // SPDX-FileCopyrightText: JingMatrix https://github.com/JingMatrix
  2. //
  3. // SPDX-License-Identifier: GPL-2.0-or-later
  4. 'use strict';
  5. const Gio = imports.gi.Gio;
  6. const GjsPrivate = imports.gi.GjsPrivate;
  7. const GLib = imports.gi.GLib;
  8. const GObject = imports.gi.GObject;
  9. // laucher for wl-clipboard
  10. const launcher = new Gio.SubprocessLauncher({
  11. flags: Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_MERGE,
  12. });
  13. /*
  14. * DBus Interface Info
  15. */
  16. const DBUS_NAME = 'org.gnome.Shell.Extensions.GSConnect.Clipboard';
  17. const DBUS_PATH = '/org/gnome/Shell/Extensions/GSConnect/Clipboard';
  18. const DBUS_NODE = Gio.DBusNodeInfo.new_for_xml(`
  19. <node>
  20. <interface name="org.gnome.Shell.Extensions.GSConnect.Clipboard">
  21. <!-- Methods -->
  22. <method name="GetMimetypes">
  23. <arg direction="out" type="as" name="mimetypes"/>
  24. </method>
  25. <method name="GetText">
  26. <arg direction="out" type="s" name="text"/>
  27. </method>
  28. <method name="SetText">
  29. <arg direction="in" type="s" name="text"/>
  30. </method>
  31. <!-- Signals -->
  32. <signal name="OwnerChange"/>
  33. </interface>
  34. </node>
  35. `);
  36. const DBUS_INFO = DBUS_NODE.lookup_interface(DBUS_NAME);
  37. /*
  38. * Text Mimetypes
  39. */
  40. const TEXT_MIMETYPES = [
  41. 'text/plain;charset=utf-8',
  42. 'UTF8_STRING',
  43. 'text/plain',
  44. 'STRING',
  45. ];
  46. /* GSConnectClipboardPortal:
  47. *
  48. * A simple clipboard portal, especially useful on Wayland where GtkClipboard
  49. * doesn't work in the background.
  50. */
  51. var Clipboard = GObject.registerClass(
  52. {
  53. GTypeName: 'GSConnectShellClipboard',
  54. },
  55. class GSConnectShellClipboard extends GjsPrivate.DBusImplementation {
  56. _init() {
  57. super._init({
  58. g_interface_info: DBUS_INFO,
  59. });
  60. this._transferring = false;
  61. this.watcher = Gio.Subprocess.new([
  62. 'wl-paste',
  63. '-w',
  64. 'dbus-send',
  65. DBUS_PATH,
  66. '--dest=' + DBUS_NAME,
  67. DBUS_NAME + '.OwnerChange',
  68. ], Gio.SubprocessFlags.NONE);
  69. // Prepare DBus interface
  70. this._handleMethodCallId = this.connect(
  71. 'handle-method-call',
  72. this._onHandleMethodCall.bind(this)
  73. );
  74. this._nameId = Gio.DBus.own_name(
  75. Gio.BusType.SESSION,
  76. DBUS_NAME,
  77. Gio.BusNameOwnerFlags.NONE,
  78. this._onBusAcquired.bind(this),
  79. null,
  80. this._onNameLost.bind(this)
  81. );
  82. }
  83. _onBusAcquired(connection, name) {
  84. try {
  85. this.export(connection, DBUS_PATH);
  86. } catch (e) {
  87. logError(e);
  88. }
  89. }
  90. _onNameLost(connection, name) {
  91. try {
  92. this.unexport();
  93. } catch (e) {
  94. logError(e);
  95. }
  96. }
  97. async _onHandleMethodCall(iface, name, parameters, invocation) {
  98. let retval;
  99. try {
  100. const args = parameters.recursiveUnpack();
  101. retval = await this[name](...args);
  102. } catch (e) {
  103. if (e instanceof GLib.Error) {
  104. invocation.return_gerror(e);
  105. } else {
  106. if (!e.name.includes('.'))
  107. e.name = `org.gnome.gjs.JSError.${e.name}`;
  108. invocation.return_dbus_error(e.name, e.message);
  109. }
  110. return;
  111. }
  112. if (retval === undefined)
  113. retval = new GLib.Variant('()', []);
  114. try {
  115. if (!(retval instanceof GLib.Variant)) {
  116. const args = DBUS_INFO.lookup_method(name).out_args;
  117. retval = new GLib.Variant(
  118. `(${args.map((arg) => arg.signature).join('')})`,
  119. args.length === 1 ? [retval] : retval
  120. );
  121. }
  122. invocation.return_value(retval);
  123. // Without a response, the client will wait for timeout
  124. } catch (e) {
  125. invocation.return_dbus_error(
  126. 'org.gnome.gjs.JSError.ValueError',
  127. 'Service implementation returned an incorrect value type'
  128. );
  129. }
  130. }
  131. /**
  132. * Get the available mimetypes of the current clipboard content
  133. *
  134. * @return {Promise<string[]>} A list of mime-types
  135. */
  136. GetMimetypes() {
  137. return new Promise((resolve, reject) => {
  138. const proc = launcher.spawnv([
  139. 'wl-paste',
  140. '--list-types',
  141. '-n',
  142. ]);
  143. proc.communicate_utf8_async(null, null, (proc, res) => {
  144. try {
  145. const [, stdout, stderr] =
  146. proc.communicate_utf8_finish(res);
  147. if (proc.get_successful())
  148. resolve(stdout.trim().split('\n'));
  149. else
  150. logError(stderr);
  151. } catch (e) {
  152. reject(e);
  153. }
  154. });
  155. });
  156. }
  157. /**
  158. * Get the text content of the clipboard
  159. *
  160. * @return {Promise<string>} Text content of the clipboard
  161. */
  162. GetText() {
  163. return new Promise((resolve, reject) => {
  164. this.GetMimetypes().then((mimetypes) => {
  165. const mimetype = TEXT_MIMETYPES.find((type) =>
  166. mimetypes.includes(type)
  167. );
  168. if (mimetype !== undefined) {
  169. const proc = launcher.spawnv(['wl-paste', '-n']);
  170. proc.communicate_utf8_async(null, null, (proc, res) => {
  171. try {
  172. const [, stdout, stderr] =
  173. proc.communicate_utf8_finish(res);
  174. if (proc.get_successful())
  175. resolve(stdout);
  176. else
  177. logError(stderr);
  178. } catch (e) {
  179. reject(e);
  180. }
  181. });
  182. } else {
  183. reject(new Error('text not available'));
  184. }
  185. });
  186. });
  187. }
  188. /**
  189. * Set the text content of the clipboard
  190. *
  191. * @param {string} text - text content to set
  192. * @return {Promise} A promise for the operation
  193. */
  194. SetText(text) {
  195. return new Promise((resolve, reject) => {
  196. try {
  197. if (typeof text !== 'string') {
  198. throw new Gio.DBusError({
  199. code: Gio.DBusError.INVALID_ARGS,
  200. message: 'expected string',
  201. });
  202. }
  203. launcher.spawnv(['wl-copy', text]);
  204. resolve();
  205. } catch (e) {
  206. reject(e);
  207. }
  208. });
  209. }
  210. destroy() {
  211. if (this._nameId > 0) {
  212. Gio.bus_unown_name(this._nameId);
  213. this._nameId = 0;
  214. }
  215. if (this._handleMethodCallId > 0) {
  216. this.disconnect(this._handleMethodCallId);
  217. this._handleMethodCallId = 0;
  218. this.unexport();
  219. }
  220. if (this.watcher)
  221. this.watcher.force_exit();
  222. }
  223. }
  224. );
  225. var _portal = null;
  226. var _portalId = 0;
  227. /**
  228. * Watch for the service to start and export the clipboard portal when it does.
  229. */
  230. function watchService() {
  231. if (_portalId > 0)
  232. return;
  233. _portalId = Gio.bus_watch_name(
  234. Gio.BusType.SESSION,
  235. 'org.gnome.Shell.Extensions.GSConnect',
  236. Gio.BusNameWatcherFlags.NONE,
  237. () => {
  238. if (_portal === null)
  239. _portal = new Clipboard();
  240. },
  241. () => {
  242. if (_portal !== null) {
  243. _portal.destroy();
  244. _portal = null;
  245. }
  246. }
  247. );
  248. }
  249. /**
  250. * Stop watching the service and export the portal if currently running.
  251. */
  252. function unwatchService() {
  253. if (_portalId > 0) {
  254. Gio.bus_unwatch_name(_portalId);
  255. _portalId = 0;
  256. }
  257. }
  258. // vim:tabstop=2:shiftwidth=2:expandtab