Background
I’ve mentioned VS Code dev containers on this blog before and like using them from WSL.
I’m also a fan of azbrowse for working with Azure resources from the terminal, and lately have found myself running azbrowse from within a dev container for various reasons.
There are several features in azbrowse that copy data to the clipboard, and when run from WSL it detects that and copies to the Windows clipboard, which is convenient. When run from a dev container, the experience isn’t so good (a polite way of saying that it doesn’t work).
In this post I’ll take a tour through the setup I am using to enable applications running in dev containers to copy content onto my Windows clipboard. If you just want the final code then skip to the end, otherwise keep reading…
Pre-requisites
Before we dive in, this post assumes that you have:
- WSL 2
- Docker Desktop for Windows configured for WSL 2 integration
- Visual Studio Code (VS Code)
- VS Code Remote-Containers extension
Copying to the clipboard from WSL
Windows has shipped a utility called clip.exe for a long time. It’s a handy little utility that reads from stdin and copies the contents to the clipboard. For example, running the following snippet in PowerShell will copy Hello
to your clipboard:
echo "Hello" | clip.exe
Even better, WSL 2 (by default) allows us to call Windows apps from within WSL. So, running the following snippet in bash will copy Hello from bash
to your Windows clipboard from inside WSL:
echo "Hello from bash" | clip.exe
Copying to the clipboard from a container
Now that we have a way to write to the clipboard, we need a way to invoke it from a dev container. The first part of this is to have something listening in WSL and forwarding on to clip.exe
. For this, we can use socat
(run sudo apt install socat
if you don’t have it installed). There are many options to choose from with socat
, but here we will instruct it to listen on a TCP port and execute clip.exe
passing the incoming traffic. Run the following in bash:
socat tcp-listen:8121,fork,bind=0.0.0.0 EXEC:'clip.exe'
In this example, socat
is listening on port 8121
(I have no real reason for picking this port - change it as you see fit!).
With socat
listening on a TCP port, the next step is to send it some text via TCP and we can make use of socat
again. Run the following command in another bash window:
echo "hello via socat" | socat - tcp:localhost:8121
This command pipes hello via scoat
into the socat
command to send it via TCP. The previous socat
command is listening and forwards the text to clip.exe
resulting in hello via socat
being placed onto your Windows clipboard.
Since the goal is to enable copying to the clipboard from a VS Code dev container, the next step is to test in a container. With the socat
listener still running, Run the following command to launch an ubuntu container:
docker run --rm -it ubuntu bash
Next, install socat
in the container and run a slight variation of the socat
command to send a value to the listener:
apt-get update && apt install -y socat
echo "hello from a container" | socat - tcp:host.docker.internal:8121
In this example, we use the host.docker.internal address in the container, which is a special domain name that Docker Desktop for Windows sets up, and which maps to the IP address for the host. This provides us a way to communicate back to the socat
listener we have running, and after running the last snippet in the docker container you will have hello from a container
on your Windows clipboard!
Faking xclip/xsel
As cool as it is to be able to send data from a container to the Windows clipboard with a custom command, the goal is to redirect content that applications in a container write to the clipboard so that it ends up on the Windows clipboard.
For this step I took inspiration from this repo which fakes out xsel
/xclip
- commonly used utilities for putting text on the clipboard.
To set up the content for this section, run the following in the container:
mkdir /cliptest
cd /cliptest
echo -e "for i in \"\$@\"\ndo\n case \"\$i\" in\n (-i|--input|-in)\n tee <&0 | socat - tcp:host.docker.internal:8121\n exit 0\n ;;\n esac\ndone" > clip.sh
chmod +x clip.sh
ln -s clip.sh xsel
ln -s clip.sh xclip
export PATH=/cliptest:$PATH
The above snippet creates /cliptest/clip.sh
with the following content (and makes it executable):
for i in "$@"
do
case "$i" in
(-i|--input|-in)
tee <&0 | socat - tcp:host.docker.internal:8121
exit 0
;;
esac
done
When this script is run, it checks for the -i
/--input
/-in
flags that are used with xsel
/xclip
to specify input to copy to the clipboard. If it finds them then it copies the input to socat
as in our previous example (to send to the socat
listener).
You can test that this is working by running the following in the container:
echo "hello from clip.sh" | ./clip.sh -i
The earlier snippet that created clip.sh
also created symbolic links to it named xsel
and xclip
. It also added the folder that these symlinks are in to the PATH
, so that running the following command will copy hello from fake xsel
to the Windows clipboard from inside the container.
echo "hello from fake xsel" | xsel --input
With the fake implementation in place, any process using xsel
/xclip
to copy content to the clipboard from within the container will actually be sending it via our socat
relay to the Windows clipboard!
Now all that remains is to make sure that any dev containers you run have the fake xsel
/xclip
installed…
Automagic installation in a dev container
There’s a handy feature in dev containers that supports dotfiles repositories. With this dotfiles support, you can configure VS Code so that whenever it builds a dev container, it clones your repository inside it and runs a script from it.
We can use this to have a repository with the clip.sh
script and symbolic links, and scripts to add update the PATH
in .bashrc
.
Final solution
So, to put all this together we need to do two things:
- update the host to launch the
socat
listener - configure VS Code to clone and use the dotfiles repo
Launching the socat
listener
To set up the socat
listener in WSL, add the following to your ~/.bashrc
file:
if [[ $(command -v socat > /dev/null; echo $?) == 0 ]]; then
# Start up the socat forwarder to clip.exe
ALREADY_RUNNING=$(ps -auxww | grep -q "[l]isten:8121"; echo $?)
if [[ $ALREADY_RUNNING != "0" ]]; then
echo "Starting clipboard relay..."
(setsid socat tcp-listen:8121,fork,bind=0.0.0.0 EXEC:'clip.exe' &) > /dev/null 2>&1
else
echo "Clipboard relay already running"
fi
fi
This snippet has some additional checks and configuration compared to the earlier example (such as using setsid
). For details on this, see the post on Forwarding SSH Agent requests from WSL to Windows which covers the same steps for ensuring that the listener continues running etc.
Configuring VS Code dev container to use dotfiles
To automatically add the clipboard support when a dev container is built, configure the dotfiles repositories to be https://github.com/stuartleeks/wsl-dev-container-clipboard-dotfiles (or add the example code there to your own dotfiles repository).
If you are configuring via the VS Code “Prefernces: Open Settings (JSON)” command, the following configuration options will configure the dotfiles using the example repo:
"remote.containers.dotfiles.installCommand": "~/dotfiles/install.sh",
"remote.containers.dotfiles.repository": "https://github.com/stuartleeks/wsl-dev-container-clipboard-dotfiles",
"remote.containers.dotfiles.targetPath": "~/dotfiles",
Conclusion
That’s all for this time! I hope you found it useful and/or interesting, whether as a solution or as a starting point. This solution works well for me, but take it and customise it to suit you. Or just take the techniques and use them for something altogether different 😃.
P.S. If you liked this, you may also like my book “WSL 2: Tips, Tricks and Techniques” which covers tips for working with WSL 2, Windows Terminal, VS Code dev containers and more https://wsl.tips/book :-)