logo一言堂

通过网络来剪贴

前文提到,我基本只用两个应用,一个是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剪贴现在我还只能依赖系统粘贴板,目前我还还并不觉得特别麻烦,留待以后再改善吧。