his article presents the Python shared clipboard—a little utility that allows different machines connected over a LAN to share the contents of their clipboards. While building it, you might learn something about cross-platform development in Python: how to access the clipboard on different platforms through various APIs, and some sound software design.
Here’s a quick scenario that illustrates the basics. Suppose you’re running both a Mac and a Windows machine. You select and copy the text “The shared clipboard is awesome!” from within a text editor on the Mac (see Figure 1). Having done that, you can now paste that exact same text into an application on a separate Windows machine (see Figure 2). The shared clipboard works in the background, making clipboard interactions between machines as simple as copying and pasting between applications on the same machine.
A shared clipboard is particularly useful for people that work on multiple machines at the same time. For example, I typically work with between two and three physical machines running Windows XP, Vista, Linux, and Mac OS X Leopard; some of those OSs run in virtual machines (VMs).
I often look up something on the web or in a file and need to paste it in some application on another machine. Before I developed the shared clipboard I had several inconvenient options: I could switch to the target machine and look up the content again; I could copy the content on one machine and send an IM or email to the other machine, open the email or IM client, copy the content, and paste it in its final destination; or I could create a file containing the content on a shared network drive, save it, and then open the file from the other machine. All these options take extra time, but using the shared clipboard to copy and paste across machines takes essentially no more time than a copy and paste operation on a single machine.
It important to understand that SharedClipboard is designed to help a single user copy and paste between multiple machines, not to share information between users. Different users will want different content in their clipboards. Sharing the clipboard will cause them to overwrite each other’s clipboard contents, perhaps causing people to lose data before they had a chance to paste it.
How Does It Work?
The shared clipboard is based on a very simple idea: you keep a little program called SharedClipboard running on each physical machine from which you wish to share a clipboard. This program monitors the machine’s clipboard and a special shared file. For example, suppose you had three machines: A, B, and C. Whenever you copy some text into the clipboard of machine A, the SharedClipboard instance running on that machine overwrites the contents of the shared file. Immediately, the SharedClipboard instances on the other two machines (B and C) that monitor the shared file will detect that its content has changed—and will copy the file’s contents into their own local clipboards, so it’s ready to be pasted.
To prevent infinite loops that repeatedly write the same content to the clipboard and the file, SharedClipboard checks whether the clipboard contents are identical to the contents of the shared file, before modifying it; if they are, it doesn’t alter the file. Race conditions are ignored. If you were to simultaneously copy some data into the clipboard of more than one machine (you need to be quick!), one of the machines will grab the shared file first, modify it, and then the other machine will open and modify the file. It may not be clear exactly which machine’s clipboard content will end up in the shared file and available to all machines, but such contention won’t break the SharedClipboard itself. In practice, such contention is all but impossible for a single user.
SharedClipboard and Virtual Machines
The shared clipboard works through a single instance when you’re running virtual machines, because VMs typically let you share the clipboard of the host OS. In other words, even if you’re running multiple VMs with different operating systems, they can all share the physical machine’s clipboard. Therefore, running SharedClipboard on any one of the virtual machines or on the host OS lets all benefit from it.
SharedClipboard in a Multiuser Environment
As described so far, the shared clipboard is designed to be used by a single user. But what if you have multiple users on a single LAN, each controlling several machines, and they all wish (naturally) to use the shared clipboard? No problem. The shared file that each instance of SharedClipboard uses is a command-line argument to the program. Each user should launch a SharedClipboard instance on each of their physical machines, passing the same shared filename to each instance.
They won’t be able to share data with each other through their SharedClipboard instances, but as long as different users use different shared files (typically, that file would reside on one of their machines or a network share) they will not overwrite each other’s clipboards. The size of the shared file for each user is always the size of the last copied content, so it doesn’t grow over time and won’t clog up the drive.
Shared Clipboard Design
The shared clipboard design is very simple. It needs to work with different clipboard APIs on different platforms, but the main algorithm is the same. The program employs a simple generic clipboard interface consisting of the following four operations:
Shared Clipboard Implementation
You can see the full code for the main module (SharedClipboard.py) in Listing 1, but here’s how it works. First, it imports some standard library modules (os, sys, time). The sys.platform attribute lets Python detect which platform the program is running on. Based on the platform, it imports all the attributes from the appropriate module using the from X import * notation. That makes all the interface methods available (openClipboard, closeClipboard, getClipboardData, setClipboardData).
import os import sys import time if sys.platform == 'darwin': use_carbon=True if use_carbon: from MacSharedClipboard import * else: from CarbonSharedClipboard import * elif sys.platform == 'win32': from WindowsSharedClipboard import *
Note that if the platform is ‘darwin’ (Mac OS X), the module must make one further decision based the value of the use_carbon Boolean flag.
The monitorClipboard function contains the main loop. It initializes the variable prev_data to an empty string and starts a loop (I don’t want to say infinite loop, because I have personally terminated this loop many times, so I know for a fact it is not infinite). Next, it opens the clipboard using by calling the openClipboard() method. The actual implantation code for openClipboard comes from one of the pre-selected platform-specific modules. If the openClipboard call fails, the code raises an exception—although the program simply catches the exception quietly and the code continues to the next loop iteration, which sleeps for a second and then attempts to open the clipboard again. That’s because it’s likely that some other program had the clipboard open temporarily. There is no need to panic and exit.
def monitorClipboard(clipboard_file): prev_data = '' while (True): time.sleep(1) try: openClipboard() except: print 'OpenClipboard() failed' continue
If the clipboard opens successfully the code enters two nested try blocks. The external try-finally block just ensures that the program will call closeClipboard() if openClipboard() succeeded. The inner try-except block handles any exceptions raised during the main operation.
try: try: ... except Exception, e: print e pass finally: closeClipboard()
|Author’s Note: The nested try blocks are necessary for backward compatibility with older version of Python. Python 2.5 introduced an improvement to the exception-handling syntax (along with many other improvements) so now you could write the following equivalent construct:
This new syntax is much nicer because you save a potentially confusing nested block and a whole level of indentation. You can read more about Python 2.5 in my three-part series (see the Related Resources section of this article in the left column for links).
The core of the SharedClipboard program first checks whether something new has been placed in the clipboard. If so, it writes the clipboard data to the shared file, and updates the prev_data variable which keeps the last-written data so it can compare that to the clipboard contents during the next iteration. This practice prevents the program from writing the same content repeatedly if it just sits in the clipboard because the user forgot to paste it.
If there is nothing new in the clipboard, it then checks the shared file for something new—perhaps the clipboard contents from a different machine has been written to the shared file since the last check. If there is something new in the file copies that content to the local clipboard, once again updating the prev_data variable with the latest content.
data = getClipboardData() if data and data != prev_data: open(clipboard_file, 'w').write(data) print 'writing %s to file' % data prev_data = data else: data = open(clipboard_file, 'r').read() if data != prev_data: setClipboardData(data) print 'putting %s in clipboard' % data prev_data = data
You might think from looking at the preceding code that you could just place the line prev_data = data outside of the if-else block, because it appears to be duplicated in both the if and the else portions of the block. But you can’t—there is a case when there is nothing in the clipboard (just after you paste the clipboard contents) and if you externalize this line then it will result in the shared file’s contents being put into the clipboard repeatedly.
That leaves only the “main” function, which is pretty standard stuff, so you only need to read about it if you are new to Python. Python doesn’t really have a main function. Any code that’s not inside a function or a class definition gets executed when the hosting module is executed or imported (see the sidebar “Why Doesn’t SharedClipboard Use a Clipboard Class” for more about Python and classes). However, it is common practice to have some conditional code running, such as this block:
This code executes only when the module is executed directly and not when it is imported. The SharedClipboard program follows this convention and immediately calls a main() function. Note that this convention is not required. Most Python programmers simply write their “main” code immediately following the check for __name__==’__main__’. The main() function verifies that the arguments (sys.argv) contain a filename (for the shared file) and launches monitorClipboard. It could be a little more thorough and check that the file exists, is writable, and that the SharedClipboard has sufficient permissions to access it for writing, but as I wrote it sheerly as a utility for my own private use, I kept it lightweight. If such an issue should arise, the shared clipboard will just not work—and I’ll notice when I try to paste something I copied on a different machine. For debugging purposes, the following error will be printed on the SharedClipboard console window (which I keep minimized) for me to stare at.
def main(): usage = """Usage: python SharedClipboard.py
The filename should refer to a writable existing file. The file should be on a shared location visible and (writable) to all the shared clipboard instances on all machines. """ if len(sys.argv) != 2 or not os.path.isfile(sys.argv): print usage sys.exit(1) clipboard_file = sys.argv monitorClipboard(clipboard_file) if __name__=='__main__': main()
Windows Shared Clipboard
The Windows shared clipboard module (WindowsSharedClipboard.py, shown in Listing 2), is implemented on top of the win32clipboard module of the Win32 extensions for Python. There is a one-to-one relationship between the abstract clipboard interface and the win32clipboard interface, so each method implementation requires only a line or two; win32clipboard does the heavy lifting.
First, the code imports the win32clipboard module. The openClipboard() and closeClipboard() just call the corresponding win32clipboard methods. The closeClipboard() method also swallows any exception after printing it to the console. There is nothing it can do to recover if the error is a permanent condition. It handles the exception because in the main loop the call to closeClipboard() is not wrapped with a try-except block (it’s in the outer try-finally block).
import win32clipboard def openClipboard(): win32clipboard.OpenClipboard() def closeClipboard(): try: win32clipboard.CloseClipboard() except Exception, e: print e pass
The getClipboardData() function checks if there is text content in the clipboard and, if so, returns it (or returns None if there isn’t). Any exceptions propagate up, and get caught by the try-except block in the main loop.
def getClipboardData(): if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_TEXT): return win32clipboard.GetClipboardData() else: return None
The setClipboardData() function is even simpler. It just empties the clipboard and puts the new content into it (as text). Again, exceptions propagate upward by design.
def setClipboardData(data): win32clipboard.EmptyClipboard() win32clipboard.SetClipboardData(win32clipboard.CF_TEXT, data)
|Author’s Note: the win32clipboard module is not part of the standard Python distribution on Windows. You can get it (along with lots of other comprehensive Python bindings to Win32 APIs) as part of the Win32 extensions project on SourceForge. Alternatively, the module is also bundled with the ActivePython distribution for Windows.|
Implementation for Mac OS X Leopard (Carbon)
There are actually two separate modules for the Mac shared clipboard because the Carbon module kept crashing on Mac OS Tiger. The pbcopy/pbpaste-based module should work on any Mac OS X version. On Leopard, the Carbon-based module works fine.
The CarbonSharedClipboard.py module (see Listing 3) implements the clipboard interface on top of the Mac’s native Carbon API, providing clipboard (or rather “pasteboard” as it is called on the Mac) access.
The module begins with a couple of imports statements from Carbon.Scrap, which corresponds more or less to the clipboard on Windows. It also imports the MacOS to be able to access the MacOs.Error exception type. Note that openClipboard() and closeClipboard() do nothing here.
from Carbon.Scrap import (GetCurrentScrap, ClearCurrentScrap) import MacOS def openClipboard(): pass # no-op on the mac def closeClipboard(): pass # no-op on the mac
This implementation of the getClipboardData() function has a try-except block to catch exceptions. It gets the current scrap and tries to return any data associated with the ‘TEXT’ flavor, which is simply the text content of the Mac pasteboard. If it fails, the module raises an exception. If no text content exists in the clipboard the getScrapFlavorData call raises a -102 MacOS.Error. Because that’s common, in this case the code just returns an empty string. However, for any other error, it re-raises the exception, which then propagates to the main loop for handling.
def getClipboardData(): try: scrap = GetCurrentScrap() return scrap.GetScrapFlavorData(flavorType='TEXT') except MacOS.Error, e: if e != -102: # -102 == noTypeErr raise return ''
The setClipboardData() function is much more straightforward: clear the current scrap, get it, put the text into the pasteboard, and you’re done.
def setClipboardData(data): ClearCurrentScrap() scrap = GetCurrentScrap() scrap.PutScrapFlavor(flavorType='TEXT', 0, text)
Implementation for All Mac OS X Versions
The other Mac module (MacSharedClipboard.py, in Listing 4) implements the clipboard interface on top of two command-line programs called pbcopy (which copies text into the clipboard) and pbpaste (which pastes whatever text is in the clipboard). The prefix “pb” stands for “pasteboard,” the Mac term for clipboard.
This module begins by importing the subprocess module—a standard Python library module (new in Python 2.4) used to launch and interact with external processes. It is very handy for a variety of tasks. Again, the openClipboard() and closeClipboard() functions are empty:
import subprocess def openClipboard(): pass # no-op on the mac def closeClipboard(): pass # no-op on the mac
The getClipboardData() function launches the pbpaste command line tool and intercepts its standard output. It waits for the tool to finish running, reads the data and returns it. You know the drill about exceptions by now.
def getClipboardData(): p = subprocess.Popen(['pbpaste'], stdout=subprocess.PIPE) retcode = p.wait() data = p.stdout.read() return data
In this module, the setClipboardData() function is essentially a mirror image of getClipboardData(). It launches the pbcopy tool, intercepts the standard input, writes the data to the pasteboard, closes the standard input and wait for the tool to finish running.
def setClipboardData(data): p = subprocess.Popen(['pbcopy'], stdin=subprocess.PIPE) p.stdin.write(data) p.stdin.close() retcode = p.wait()
Possible Extensions to SharedClipboard
After you’ve implemented the basic SharedClipboard discussed in this article, you might consider extending the utility by giving it additional capabilities, such as:
- Adding Linux Support: If you run Linux in a VM hosted on a Mac or Windows box you can simply run SharedClipboard on the host OS. However, it makes a lot of sense to extend the program to support Linux for pure Linux users. To do so will require supporting at least GNOME and KDE, the most popular Linux windows managers. You can access Gnome’s clipboard using pygtk and KDE’s clipboard using pyqt. Another option is to require that the target machines have xsel or xclip, which work very much like pbcopy/pbpaste and implement Linux clipboard support on top of those.
- Adding Internet Connectivity: This is not quite as straightforward as adding a new host OS, because it will require writing a little web application that functions similarly to the shared file. It will also require some changes to SharedClipboard.py. Instead of reading and writing to a file, an Internet version would get content from the web application and post data to the web application when a user puts something new into the clipboard. Note that—depending on your connection—the latency may be noticeable when using a web-shared clipboard; but otherwise, the user experience should stay the same; copy here and paste there!
- Adding Non-Text Support: Currently, SharedClipboard supports only shared text, and adding support for non-text types, such as images, is non-trivial. The image clipboard formats differ between different operating systems, so if you put a copied image into the shared clipboard on a Mac it is very unlikely you will be able to paste it on Windows (and vice versa). One approach would to translate the image in the clipboard from its native clipboard format to some neutral format that can be read on every machine (e.g. a PIL image) when writing to the shared file, and when reading from the shared file translating from the neutral format to some appropriate native clipboard image format before placing the file contents in the clipboard.
Whether you decide to extend it or leave it as is, SharedClipboard is a convenient little tool that can improve productivity for people working with multiple physical machines. You’ve toured its current design and implementation, and seen some ideas for further development. If you think of some entirely new capability for this utility, let me know.