2011-05-23

gnu screen xwindows clipboard integration

Part of the reason that I'm writing this blog post is to make sure I properly configured sourcecode syntax highlighting for my blog.

I created a project with utilities for authoring blog posts in reStructuredText syntax so I can write my blog articles in reStructuredText syntax using vim, preview as html with a simple vim command, and translate to html for publication.

Not too long ago, I tinkered with a very simple program to integrate the gnu screen buffer and xwindows CLIPBOARD. I ended up rewriting it in lua, perl and c as an experiment to see the relative performance differences. I pasted the 3 versions below to verify that syntax highlighting looks good with all 3 languages.

There were other examples of screen buffer <-> xwindows clipboard integration on the net, but none of them worked for me. To get it working, I had to add a 10ms sleep after copying /tmp/screen_exchange to my xwindows clipboard with xsel -bi. I'm not sure why the sleep is necessary, but I could not get it to work consistently without at least a 10ms sleep. Eventually I will peek into the screen and xsel code to better understand the apparent race condition, but for now working around the problem with a 10ms sleep is fine.

So now I can enter Copy Mode in screen, mark the start of my selection as usual with the space bar, and press . to close the selection. The final . exits Copy Mode and copies the screen buffer to my xwindows clipboard with a single keypress.

For reference, here is my ~/.screenrc:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
startup_message off
hardstatus alwayslastline

### Support for "tabs" in the screen status line:
hardstatus string '%{= kG}[ %H ][  %{= kw}%-w%{= bk}%n*%t%{= kw}%+w %= %{g}][%{B} %{W}%c %{g}]'
shelltitle " "

### In copy mode, map '.' to copy the selection to the Xwindows clipboard:
# explanation:
#
#   stuff ' '               -- this enters a space character in your terminal,
#                              effectively ending Copy Mode and putting your
#                              selection in the screen paste buffer
#
#   writebuf                -- Writes the screen paste buffer out to a file
#                              (default is /tmp/screen-exchange )
#
#   exec 'screen_buff_copy' -- executes screen_buff_copy, which is a program
#                              that will read /tmp/screen_buff_copy and write
#                              it to the Xwindows CLIPBOARD with xsel -bi
#
bindkey -m . eval "stuff ' '" "writebuf" "exec '/home/greg/scripts/screen/screen_buff_copy'"

During my initial troubleshooting, I decided to put the screen_exchange -> xsel step into an external screen_buff_copy program so I could invoke the program outside of screen to minimize the moving parts. Once I discovered the sleep workaround, I decided to try to make my screen_buff_copy program as lightweight as possible because it executes every time I want to copy text to my clipboard. I settled on lua, perl and c for my implementation trials because they have less startup overhead for short programs than, e.g., java or python.

I first wrote the program in lua:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/lua

-- NOTE: the socket does not ship as part of the lua std libs
-- You have to install it separately.
require('io')
require('socket')

pipe = io.popen("/usr/bin/xsel -bi", "w")
fp = io.open("/tmp/screen-exchange")

-- write file contents to pipe
pipe:write(fp:read("*all"))

-- close pipe and file pointer
pipe:close()
fp:close()

-- There is some race condition in the entire screen slurping/exec/whatever
-- process. Without a sleep, it does not work consistently. There is also a lag
-- of about 1 second in the stuff, writebuf and exec steps before this script is
-- properly executed and the time that the clipboard is actually set

-- sleep for 0.01 seconds:
socket.select(nil, nil, 0.01)

I was not thrilled that I had to load a library that is not part of the std libs (socket), and I quickly rewrote it in perl, curious to see perl's relative performance.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/perl

#
#   This is very very marginally more lightweight than the lua version,
#   and one nice thing about it is that it uses all perl builtins without
#   requiring external libs (in lua's case, the external socket lib is required
#   for sleep)
#

open(F, "/tmp/screen-exchange");
open(P, "| xsel -bi");

print P <F>;

close F;
close P;

### Sleep (needed due to weird race condition)
my $sleep_seconds = 0.01;
select(undef, undef, undef, $sleep_seconds );

And then I decided to write it in C just to see if the overhead was perceptibly lower. Interestingly, it really was not. The C version is not faster than the perl version, but it requires about twice as many lines of code.

 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
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <unistd.h> /* for usleep */

/*************************************************************
*
*       screen_buff_copy.c
*
*       It is totally unnecessary to do this in C. The performance
*       is almost indistinguishable from the perl version.
*
*************************************************************/

/* Sleep for 10 milliseconds (seems like the min needed for reliability) */
#define MILLISECOND_IN_USEC     1000
#define USLEEP_TIME             MILLISECOND_IN_USEC * 10

#define BUFF_SIZE               80

int main(int argc, char **argv) {
    FILE    *pipe;
    FILE    *fp;
    size_t  num_read; /* number of items read */

    char    buff[BUFF_SIZE];

    pipe = popen("/usr/bin/xsel -bi", "w");
    fp = fopen("/tmp/screen-exchange", "r");

    while( (num_read = fread(buff, sizeof(char), sizeof(buff), fp)) > 0 ) {
        fwrite(buff, sizeof(char), num_read, pipe);
    }

    fflush(pipe);
    fclose(fp);
    pclose(pipe);

    /* Yes, we need to sleep */
    usleep(USLEEP_TIME);

    return(0);
}

A simple benchmark program (another opportunity for syntax higlighting another lang):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/bin/sh

ITER=100
sbc=screen_buff_copy

for p in ./$sbc ./${sbc}.pl ./${sbc}.lua; do
    echo "Timing $ITER iterations of ${p}: "
    time for n in `seq $ITER`; do $p; done
    echo
done

And the results -- perl and c are basically identical. Most of the time is spent in the usleep anyway. Perl 5's super-low startup latency for short scripts is amazing (ditto with lua, although lua takes a tiny hit here when it loads its external socket library).

Timing 100 iterations
  c perl lua
real 0m2.304s 0m2.263s 0m2.596s
user 0m0.608s 0m0.512s 0m0.692s
sys 0m0.444s 0m0.332s 0m0.432s