JVM 当中的剪贴板访问

起因是想要写一个 kotlin 的脚本,用于读取剪贴板中的内容并保存到文件当中。然而网上搜了一圈,java 和 kotlin 给出的方法都是通过 jdk 中的 awt 包来获取系统剪贴板。因为 kotlin 脚本还是跑的 jvm target,所以方法还是通用的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;

public class Test {

    public static void main(String[] args) throws IOException, UnsupportedFlavorException {
        var clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
        var transferable = clipboard.getContents(null);
        System.out.println(transferable.getTransferData(DataFlavor.stringFlavor));
    }
}
1
2
3
val toolkit = Toolkit.getDefaultToolkit()
val clipboard = toolkit.systemClipboard
val result = clipboard.getData(DataFlavor.stringFlavor) as String

这个方法用是能用,而且支持操作的数据类型还挺全,除了文本,还能读写图片。然而有一个缺点,就是使用时必须关掉 headless 选项。kotlin 默认是关掉的,需要显示声明配置 kotlin -Djava.awt.headless=false来禁用掉。

Headless mode is a system configuration in which the display device, keyboard, or mouse is lacking. Sounds unexpected, but actually you can perform different operations in this mode, even with graphic data.

禁用掉 headless 模式后是代码是可用,然而运行起来时会拉起一个 jre 程序,虽然这个拉起的程序没有界面,但是还是会强制跳转切换一次窗口,体感很差。于是只能另寻办法,选择使用 nodejs,最后找到了 clipboardy,一个简单好用的 npm 库,先实现了我的需求。后来细细看了下它的实现代码,是通过调用命令后的方式来实现访问系统剪贴板。然而调用系统命令的话,各个系统环境的命令都不同,如何兼顾可移植性,在不同的系统环境都能运行呢?解决的办法也很简单粗暴,手动为每个系统都写一个实现,并且针对如果某些系统没有这个操作系统剪贴板的命令程序,还在 npm 包里附带一个二进制作为 fallback。简单,但可用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import process from 'node:process';
import isWSL from 'is-wsl';
import termux from './lib/termux.js';
import linux from './lib/linux.js';
import macos from './lib/macos.js';
import windows from './lib/windows.js';

const platformLib = (() => {
	switch (process.platform) {
		case 'darwin':
			return macos;
		case 'win32':
			return windows;
		case 'android':
			if (process.env.PREFIX !== '/data/data/com.termux/files/usr') {
				throw new Error('You need to install Termux for this module to work on Android: https://termux.com');
			}

			return termux;
		default:
			// `process.platform === 'linux'` for WSL.
			if (isWSL) {
				return windows;
			}

			return linux;
	}
})();

于是很自然地借鉴这个思路,用 kotlin 也来实现一版。代码实现可见 clipboard-jvm,实现的思路也是通过不同的操作系统,调用不同的 shell 命令,访问与操作系统剪贴板。对于执行外部命令

使用起来也很简单

1
2
3
4
5
import org.yeungyeah.clipboard.Clipboard
val clipboard = Clipboard.getClipboard()
println(clipboard.get())
clipboard.set("Hello World")
println(clipboard.get())

原本想把依赖库发布到 maven central 仓库,这样脚本就可以一行直接使用。然而 maven 发布实在是麻烦,研究了几个小时,还是被一些相关的检查卡点了,只能够先 install 到本地,给自己先用。后续上传到 maven central 后再更新,也顺便更新一篇文章如何 deploy to maven central.

部署到 maven central 仓库后,就可以通过配置直接导入依赖使用了。比如在 java 项目,通过

1
2
3
4
5
<dependency>
  <groupId>io.github.yeung66</groupId>
  <artifactId>clipboard-jvm</artifactId>
  <version>1.0.0</version>
</dependency>

或者是在 kotlin script 中,直接

1
2
3
4
5
6
7
@file:DependsOn("io.github.yeung66:clipboard-jvm:1.0.0")

import org.yeungyeah.clipboard.Clipboard
val clipboard = Clipboard.getClipboard()
println(clipboard.get())
clipboard.set("Hello World")
println(clipboard.get())

起飞🛫️。不过只是测试了在macOS系统下面的使用,按理说其他系统也是可行的,如果有兴趣的可以试试玩玩。