通过网络来剪贴
2020-03-29
周溱
我前文提到,我基本只用两个应用,一个是ssh到远程主机上的emacs, 另一个是本地的浏览器。另外,我目前常常用的一个数字设备是平板电脑。这种工作环境就带来一个问题,就是如何在这两个平台下剪贴文字,主要是从emacs向本地浏览器剪贴。你当然可以用android系统剪贴板,但有两个问题:
- 在终端窗口选择大量文字,尤其是超过一屏幕的文字相当困难,而且常常换行,空白等无法精确传递。而换行和空白对于我常常需要剪贴的源代码片段来说相当重要。
- 在一个平板电脑上,你没有精确的鼠标,只有手指。终端里文字通常还会相当小,我的笨手指无法有效地选择文字区间。
当你足够痛苦的时候,就是需要创造力的时候了。
现有方案
当然,有这个痛苦的不止我一个。苹果刚刚推出的iPad pro主要推的创新点就是自带track pad,能解决精确选中文字的问题。iPad pro确实诱惑很大,可惜799美元的价格让我望而却步,这个价格可以买不错的笔记本电脑了。相比之下,我日常用的平板售价仅79美元,花十倍的价钱解决技术问题并不是我的风格。
emacs平台下现有附加组件webpaste,可以让你用键盘选择要选择的文字区间,然后发到网上,以便于别人或你自己再用浏览器下载。但这个组件的目的是在聊天室快速传递大片文字同时不过分刷屏的,并不很适合我个人使用。第一它依赖第三方网上服务,第二到了网页上还要选中剪贴一次,虽然比终端窗口方便一点,但还是麻烦。
我的方案
我的灵感来自webpaste。我其实需要一个个性化的网上服务,能够暂时存储文字信息,而且能够快速选中剪贴。公网服务器我有,我日常使用emacs的主机就是。但我不想在上面安装任何服务端软件,毕竟日常维护和安全隐患都是需要考虑的。通盘考虑后我把问题分解成三个部分:
- 一个命令行工具读入要剪贴的文字,归档存入特定目录,并生成html索引以便查询。
- 一个网页脚本通过前述索引方便选中剪贴归档的文字片段。
- 一个emacs脚本来把选中的文字传给命令行脚本,并绑定快捷键。
命令行工具
对于处理文件并生成网页的命令行小工具来说,最适合的就是perl了。整个工具一共只需50行,包括注释。这里全文抄录如下。
#! /usr/bin/perl -w
#
# organize text from stdin to files so they can be pasted in browser
use strict;
use warnings;
use v5.14;
use File::Slurp;
use Template;
# configs
# max items to keep. oldest is removed
our $MAX_ITEMS = 5;
our $SCRATCH_DIR = $ENV{'HOME'} . '/public_html/scratch';
my $text = read_file(\*STDIN);
# let's assume we do not call this script every second
my $name = time();
my $text_size = length($text);
write_file("$SCRATCH_DIR/$name" . ".txt", $text);
my %scratches;
opendir(my $dh, $SCRATCH_DIR) || die "cannnot open folder";
while(my $file = readdir $dh) {
next unless (-f "$SCRATCH_DIR/$file");
next unless ($file =~ /\.txt$/);
my ($dev,$ino,$mode,$nlink,$uid,$gid, $rdev, $size, $atime, $mtime) =
stat("$SCRATCH_DIR/$file");
$scratches{$file} = {
mtime => $mtime,
size => $size,
};
}
closedir $dh;
my @sorted = sort {$scratches{$b}->{'mtime'} <=> $scratches{$a}->{'mtime'}} keys(%scratches);
for(my $i = $MAX_ITEMS; $i < scalar(@sorted); $i++) {
my $file = $sorted[$i];
unlink("$SCRATCH_DIR/$file");
delete($scratches{$file});
}
@sorted = @sorted[0..$MAX_ITEMS - 1] if (scalar(@sorted) > $MAX_ITEMS);
my $tt = Template->new({
ABSOLUTE => 1,
});
$tt->process("$SCRATCH_DIR/index.tt2",
{ list => \@sorted,
data => \%scratches },
"$SCRATCH_DIR/index.html") || die $tt->error(), "\n";
say "Copied $text_size byte to easypaste"
网页脚本
从网页上可以运行脚本,根据索引自动下载选中文字片段,并粘贴到系统剪贴板上。网页加载的时候自动下载选中最新的一份,用户可以多按两下按钮来选中之前的几份。这份javascript也不长,全文抄录如下:
var last = null;
function fixAllItem() {
document.querySelectorAll("li.scratch-item").forEach(item => {
var mtime = new Date(parseInt(item.getAttribute("data-mtime"))*1000);
item.querySelector("span.scratch-mtime").textContent = mtime.toLocaleString();
item.querySelector("button.scratch-button").addEventListener("click", (e) => {
loadContent(e.target.parentElement);
});
});
}
function loadContent(item) {
var file = item.getAttribute("data-name");
if (last == item) {
// loaded
document.querySelector("#target-text").select();
document.execCommand("copy");
item.querySelector("button.scratch-button").textContent = "Load";
last = null;
return;
}
if (last != null) {
last.querySelector("button.scratch-button").textContent = "Load";
}
fetch(file)
.then((res) => {
return res.text();
})
.then((text) => {
document.querySelector("#target-text").textContent = text;
last = item;
last.querySelector("button.scratch-button").textContent = "Copy";
});
}
document.addEventListener("DOMContentLoaded", function() {
fixAllItem();
loadContent(document.querySelector("li.scratch-item"));
})
emacs脚本
最后,我需要从emacs内调用命令行工具,把文字传递进去,并绑定热键。我一共做了两个热键绑定,一个传送当前高亮区间,一个传送已经剪贴到emacs内部剪贴板内的文字。利用emacs现成函数,一小段lisp代码即可:
;;; send current region to easypaste
(defun my-easypaste-region (&optional b e)
(interactive "r")
(shell-command-on-region b e "easypaste"))
;;; send top of kill ring to easypaste
(defun my-easypaste-kill ()
(interactive)
(shell-command-on-region (current-kill 0) nil "easypaste"))
(global-set-key "\C-cpr" 'my-easypaste-region)
(global-set-key "\C-cpk" 'my-easypaste-kill)
总结
三个小功能模块,我一共用了三种不同的编程语言,如果加上html是四种。每段程序都很短。最终我用最经济的方法实现了我需要的功能。
这个工具目前只负责从emacs向浏览器之间粘贴,但我只使用网络主机上的emacs,而浏览器在所有平台都可运行,我实际上可以方便从我的网络主机向所有平台搬运文本数据。反过来,即从本地平台向网络主机emacs剪贴现在我还只能依赖系统粘贴板,目前我还还并不觉得特别麻烦,留待以后再改善吧。