commit 900b075e3aa03890f1538d97558920fc2bb9ae59
Author: crs <cschoeneman@gmail.com>
Date:   Sun May 13 11:40:29 2001 +0000

    initial revision of synergy.  currently semi-supports X windows
    on unix, but client screens don't simulate events other than
    mouse move.  also not supporting clipboard at all yet and the
    main app is just a temporary framework to test with.  must
    clean up protocol and communication.

diff --git a/BasicTypes.h b/BasicTypes.h
new file mode 100644
index 00000000..8dfce8c2
--- /dev/null
+++ b/BasicTypes.h
@@ -0,0 +1,33 @@
+#ifndef BASICTYPES_H
+#define BASICTYPES_H
+
+#if defined(__linux__)
+
+#define CONFIG_PLATFORM_LINUX
+#define CONFIG_TYPES_X11
+
+#include <stdint.h>
+
+typedef int8_t			SInt8;
+typedef int16_t			SInt16;
+typedef int32_t			SInt32;
+typedef int64_t			SInt64;
+
+typedef uint8_t			UInt8;
+typedef uint16_t		UInt16;
+typedef uint32_t		UInt32;
+typedef uint64_t		UInt64;
+
+#else
+
+#error unsupported platform
+
+#endif
+
+#ifndef NULL
+#define NULL 0
+#endif
+
+#endif
+
+
diff --git a/CClient.cpp b/CClient.cpp
new file mode 100644
index 00000000..65030c99
--- /dev/null
+++ b/CClient.cpp
@@ -0,0 +1,287 @@
+#include "CClient.h"
+#include "CString.h"
+#include "TMethodJob.h"
+#include "IScreen.h"
+#include "ISocket.h"
+#include "CMessageSocket.h"
+#include "CSocketFactory.h"
+#include "IEventQueue.h"
+#include "CEvent.h"
+#include "CTrace.h"
+#include <assert.h>
+
+//
+// CClient
+//
+
+CClient::CClient(IScreen* screen) : m_screen(screen),
+								m_socket(NULL)
+{
+	assert(m_screen != NULL);
+	assert(!m_screen->getName().empty());
+}
+
+CClient::~CClient()
+{
+	assert(m_socket == NULL);
+}
+
+void					CClient::run(const CString& hostname)
+{
+	assert(m_socket == NULL);
+
+	try {
+		// create socket and
+		m_socket = CSOCKETFACTORY->create();
+		m_socket->setWriteJob(new TMethodJob<CClient>(this,
+												&CClient::onConnect));
+		TRACE(("connecting to %s...", hostname.c_str()));
+		m_socket->connect(hostname, 40001); // CProtocol::kDefaultPort
+
+		bool m_done = false;	// FIXME
+
+		IEventQueue* queue = CEQ;
+		while (!m_done) {
+			// wait for connection, network messages, and events
+			queue->wait(-1.0);
+
+			// handle events
+			while (!queue->isEmpty()) {
+				// get the next event
+				CEvent event;
+				queue->pop(&event);
+
+				// handle it
+				switch (event.m_any.m_type) {
+				  case CEventBase::kScreenSize: {
+					sendScreenSize();
+					break;
+				  }
+
+				  case CEventBase::kNull:
+				  case CEventBase::kKeyDown:
+				  case CEventBase::kKeyRepeat:
+				  case CEventBase::kKeyUp:
+				  case CEventBase::kMouseDown:
+				  case CEventBase::kMouseUp:
+				  case CEventBase::kMouseMove:
+				  case CEventBase::kMouseWheel:
+					// FIXME -- other cases
+					break;
+				}
+			}
+		}
+
+		delete m_socket;
+		m_socket = NULL;
+	}
+
+	catch (...) {
+		delete m_socket;
+		m_socket = NULL;
+		throw;
+	}
+}
+
+void					CClient::onConnect()
+{
+	TRACE(("connected"));
+
+	// say hello
+	const CString name(m_screen->getName());
+	char buf[512];
+	memcpy(buf, "SYNERGY\000\001", 9);
+	buf[9] = static_cast<char>(name.length());
+	memcpy(buf + 10, name.c_str(), name.length());
+	m_socket->write(buf, 10 + name.length());
+
+	// handle messages
+	m_socket->setWriteJob(NULL);
+	m_socket = new CMessageSocket(m_socket);
+	m_socket->setReadJob(new TMethodJob<CClient>(this, &CClient::onRead));
+}
+
+void					CClient::onRead()
+{
+	char buf[512];
+	SInt32 n = m_socket->read(buf, sizeof(buf));
+	if (n == -1) {
+		// disconnect
+		TRACE(("hangup"));
+	}
+	else if (n > 0) {
+		TRACE(("msg: 0x%02x length %d", buf[0], n));
+		switch (buf[0]) {
+		  case '\002':
+			TRACE(("  open"));
+
+			// open the screen
+			m_screen->open(buf[1] != 0);
+
+			// send initial size
+			sendScreenSize();
+			break;
+
+		  case '\003':
+			TRACE(("  close"));
+			m_screen->close();
+			break;
+
+		  case '\004': {
+			const SInt32 x = static_cast<SInt32>(
+								(static_cast<UInt32>(buf[1]) << 24) +
+								(static_cast<UInt32>(buf[2]) << 16) +
+								(static_cast<UInt32>(buf[3]) << 8) +
+								(static_cast<UInt32>(buf[4])     ));
+			const SInt32 y = static_cast<SInt32>(
+								(static_cast<UInt32>(buf[5]) << 24) +
+								(static_cast<UInt32>(buf[6]) << 16) +
+								(static_cast<UInt32>(buf[7]) << 8) +
+								(static_cast<UInt32>(buf[8])     ));
+			TRACE(("  enter: %d,%d", x, y));
+			m_screen->enterScreen(x, y);
+			break;
+		  }
+
+		  case '\005':
+			TRACE(("  leave"));
+			m_screen->leaveScreen();
+			break;
+
+		  case '\007': {
+			const KeyID k = static_cast<KeyID>(
+								(static_cast<UInt32>(buf[1]) << 24) +
+								(static_cast<UInt32>(buf[2]) << 16) +
+								(static_cast<UInt32>(buf[3]) << 8) +
+								(static_cast<UInt32>(buf[4])     ));
+			TRACE(("  key down: %d", k));
+			m_screen->onKeyDown(k);
+			break;
+		  }
+
+		  case '\010': {
+			const KeyID k = static_cast<KeyID>(
+								(static_cast<UInt32>(buf[1]) << 24) +
+								(static_cast<UInt32>(buf[2]) << 16) +
+								(static_cast<UInt32>(buf[3]) << 8) +
+								(static_cast<UInt32>(buf[4])     ));
+			const SInt32 n = static_cast<SInt32>(
+								(static_cast<UInt32>(buf[5]) << 24) +
+								(static_cast<UInt32>(buf[6]) << 16) +
+								(static_cast<UInt32>(buf[7]) << 8) +
+								(static_cast<UInt32>(buf[8])     ));
+			TRACE(("  key repeat: %d x%d", k, n));
+			m_screen->onKeyRepeat(k, n);
+			break;
+		  }
+
+		  case '\011': {
+			const KeyID k = static_cast<KeyID>(
+								(static_cast<UInt32>(buf[1]) << 24) +
+								(static_cast<UInt32>(buf[2]) << 16) +
+								(static_cast<UInt32>(buf[3]) << 8) +
+								(static_cast<UInt32>(buf[4])     ));
+			TRACE(("  key up: %d", k));
+			m_screen->onKeyUp(k);
+			break;
+		  }
+
+		  case '\012': {
+			const KeyToggleMask m = static_cast<KeyToggleMask>(
+								(static_cast<UInt32>(buf[1]) << 8) +
+								(static_cast<UInt32>(buf[2])     ));
+			TRACE(("  key toggle: 0x%04x", m));
+			m_screen->onKeyToggle(m);
+			break;
+		  }
+
+		  case '\013': {
+			const ButtonID b = static_cast<ButtonID>(
+								static_cast<UInt32>(buf[1]));
+			TRACE(("  mouse down: %d", b));
+			m_screen->onMouseDown(b);
+			break;
+		  }
+
+		  case '\014': {
+			const ButtonID b = static_cast<ButtonID>(
+								static_cast<UInt32>(buf[1]));
+			TRACE(("  mouse up: %d", b));
+			m_screen->onMouseUp(b);
+			break;
+		  }
+
+		  case '\015': {
+			const SInt32 x = static_cast<SInt32>(
+								(static_cast<UInt32>(buf[1]) << 24) +
+								(static_cast<UInt32>(buf[2]) << 16) +
+								(static_cast<UInt32>(buf[3]) << 8) +
+								(static_cast<UInt32>(buf[4])     ));
+			const SInt32 y = static_cast<SInt32>(
+								(static_cast<UInt32>(buf[5]) << 24) +
+								(static_cast<UInt32>(buf[6]) << 16) +
+								(static_cast<UInt32>(buf[7]) << 8) +
+								(static_cast<UInt32>(buf[8])     ));
+			TRACE(("  mouse move: %d,%d", x, y));
+			m_screen->onMouseMove(x, y);
+			break;
+		  }
+
+		  case '\016': {
+			const SInt32 n = static_cast<SInt32>(
+								(static_cast<UInt32>(buf[1]) << 24) +
+								(static_cast<UInt32>(buf[2]) << 16) +
+								(static_cast<UInt32>(buf[3]) << 8) +
+								(static_cast<UInt32>(buf[4])     ));
+			TRACE(("  mouse wheel: %d", n));
+			m_screen->onMouseWheel(n);
+			break;
+		  }
+
+		  case '\017': {
+			TRACE(("  screen saver: %s", buf[1] ? "on" : "off"));
+			m_screen->onScreenSaver(buf[1] != 0);
+			break;
+		  }
+
+		  case '\020': {
+			const SInt32 x = static_cast<SInt32>(
+								(static_cast<UInt32>(buf[1]) << 24) +
+								(static_cast<UInt32>(buf[2]) << 16) +
+								(static_cast<UInt32>(buf[3]) << 8) +
+								(static_cast<UInt32>(buf[4])     ));
+			const SInt32 y = static_cast<SInt32>(
+								(static_cast<UInt32>(buf[5]) << 24) +
+								(static_cast<UInt32>(buf[6]) << 16) +
+								(static_cast<UInt32>(buf[7]) << 8) +
+								(static_cast<UInt32>(buf[8])     ));
+			TRACE(("  warp: %d,%d", x, y));
+			m_screen->warpCursor(x, y);
+			break;
+		  }
+
+		  default:
+			TRACE(("  unknown message"));
+		}
+	}
+}
+
+void					CClient::sendScreenSize()
+{
+	// get the size
+	SInt32 w, h;
+	m_screen->getSize(&w, &h);
+
+	// send it
+	char buf[9];
+	memcpy(buf, "\201", 1);
+	buf[1] = static_cast<char>((w >> 24) & 0xff);
+	buf[2] = static_cast<char>((w >> 16) & 0xff);
+	buf[3] = static_cast<char>((w >> 8) & 0xff);
+	buf[4] = static_cast<char>(w & 0xff);
+	buf[5] = static_cast<char>((h >> 24) & 0xff);
+	buf[6] = static_cast<char>((h >> 16) & 0xff);
+	buf[7] = static_cast<char>((h >> 8) & 0xff);
+	buf[8] = static_cast<char>(h & 0xff);
+	m_socket->write(buf, sizeof(buf));
+}
diff --git a/CClient.h b/CClient.h
new file mode 100644
index 00000000..c99cb364
--- /dev/null
+++ b/CClient.h
@@ -0,0 +1,28 @@
+#ifndef CCLIENT_H
+#define CCLIENT_H
+
+#include "IClient.h"
+
+class IScreen;
+class ISocket;
+
+class CClient : public IClient {
+  public:
+	CClient(IScreen* screen);
+	virtual ~CClient();
+
+	// IClient overrides
+	virtual void		run(const CString& hostname);
+
+  private:
+	void				onConnect();
+	void				onRead();
+
+	void				sendScreenSize();
+
+  private:
+	IScreen*			m_screen;
+	ISocket*			m_socket;
+};
+
+#endif
diff --git a/CEvent.h b/CEvent.h
new file mode 100644
index 00000000..452aa4e6
--- /dev/null
+++ b/CEvent.h
@@ -0,0 +1,58 @@
+#ifndef CEVENT_H
+#define CEVENT_H
+
+#include "BasicTypes.h"
+#include "KeyTypes.h"
+#include "MouseTypes.h"
+
+class ISocket;
+
+class CEventBase {
+  public:
+	enum EType {
+		kNull,
+		kKeyDown,
+		kKeyRepeat,
+		kKeyUp,
+		kMouseDown,
+		kMouseUp,
+		kMouseMove,
+		kMouseWheel,
+		kScreenSize
+	};
+
+	EType				m_type;
+};
+
+class CEventKey : public CEventBase {
+  public:
+	KeyID				m_key;
+	SInt32				m_count;
+};
+
+class CEventMouse : public CEventBase {
+  public:
+	ButtonID			m_button;
+	SInt32				m_x;		// or wheel delta
+	SInt32				m_y;
+};
+
+class CEventSize : public CEventBase {
+  public:
+	SInt32				m_w;
+	SInt32				m_h;
+};
+
+class CEvent {
+  public:
+	union {
+	  public:
+		CEventBase		m_any;
+		CEventKey		m_key;
+		CEventMouse		m_mouse;
+		CEventSize		m_size;
+	};
+};
+
+#endif
+
diff --git a/CEventQueue.cpp b/CEventQueue.cpp
new file mode 100644
index 00000000..0cef9742
--- /dev/null
+++ b/CEventQueue.cpp
@@ -0,0 +1,80 @@
+#include "CEventQueue.h"
+
+//
+// IEventQueue
+//
+
+IEventQueue*			IEventQueue::s_instance = NULL;
+
+IEventQueue::IEventQueue()
+{
+	assert(s_instance == NULL);
+	s_instance = this;
+}
+
+IEventQueue::~IEventQueue()
+{
+	s_instance = NULL;
+}
+
+IEventQueue*			IEventQueue::getInstance()
+{
+	return s_instance;
+}
+
+
+//
+// CEventQueue
+//
+
+CEventQueue::CEventQueue()
+{
+	// do nothing
+}
+
+CEventQueue::~CEventQueue()
+{
+	// do nothing
+}
+
+void					CEventQueue::pop(CEvent* event)
+{
+	assert(event != NULL);
+
+	// wait for an event
+	while (isEmpty())
+		wait(-1.0);
+
+	// lock the queue, extract an event, then unlock
+	lock();
+	*event = m_queue.front();
+	m_queue.pop_front();
+	unlock();
+}
+
+void					CEventQueue::push(const CEvent* event)
+{
+	// put the event at the end of the queue and signal that the queue
+	// is not empty
+	lock();
+	m_queue.push_back(*event);
+	signalNotEmpty();
+	unlock();
+}
+
+bool					CEventQueue::isEmpty()
+{
+	lock();
+	bool e = m_queue.empty();
+	unlock();
+
+	// if queue is empty then poll to see if more stuff is ready to go
+	// on the queue and check again if the queue is empty.
+	if (e) {
+		wait(0.0);
+		lock();
+		e = m_queue.empty();
+		unlock();
+	}
+	return e;
+}
diff --git a/CEventQueue.h b/CEventQueue.h
new file mode 100644
index 00000000..de6a853d
--- /dev/null
+++ b/CEventQueue.h
@@ -0,0 +1,36 @@
+#ifndef CEVENTQUEUE_H
+#define CEVENTQUEUE_H
+
+#include "IEventQueue.h"
+#include "CEvent.h"
+#include <list>
+
+class CEventQueue : public IEventQueue {
+  public:
+	CEventQueue();
+	virtual ~CEventQueue();
+
+	// IEventQueue overrides
+	virtual void		wait(double timeout) = 0;
+	virtual void		pop(CEvent*);
+	virtual void		push(const CEvent*);
+	virtual bool		isEmpty();
+
+  protected:
+	// signal the queue not-empty condition.  this should cause wait()
+	// to stop waiting.
+	virtual void		signalNotEmpty() = 0;
+
+	// lock the queue mutex
+	virtual void		lock() = 0;
+
+	// unlock the queue mutex
+	virtual void		unlock() = 0;
+
+  private:
+	typedef std::list<CEvent> List;
+
+	List				m_queue;
+};
+
+#endif
diff --git a/CMessageSocket.cpp b/CMessageSocket.cpp
new file mode 100644
index 00000000..c89c0db1
--- /dev/null
+++ b/CMessageSocket.cpp
@@ -0,0 +1,153 @@
+#include "CMessageSocket.h"
+#include "TMethodJob.h"
+#include "CTrace.h"
+#include <assert.h>
+#include <string.h>
+
+//
+// CMessageSocket
+//
+
+CMessageSocket::CMessageSocket(ISocket* socket) :
+								m_socket(socket),
+								m_buffer(NULL),
+								m_size(0),
+								m_capacity(0),
+								m_msgSize(0)
+{
+	m_socket->setReadJob(new TMethodJob<CMessageSocket>(this,
+								&CMessageSocket::readJobCB));
+}
+
+CMessageSocket::~CMessageSocket()
+{
+	delete m_socket;
+	delete[] m_buffer;
+}
+
+void					CMessageSocket::setWriteJob(IJob* adoptedJob)
+{
+	CSocket::setWriteJob(adoptedJob);
+	if (adoptedJob != NULL)
+		m_socket->setWriteJob(new TMethodJob<CMessageSocket>(this,
+								&CMessageSocket::writeJobCB));
+	else
+		m_socket->setWriteJob(NULL);
+}
+
+void					CMessageSocket::connect(const CString&, UInt16)
+{
+	assert(0 && "connect() illegal on CMessageSocket");
+}
+
+void					CMessageSocket::listen(const CString&, UInt16)
+{
+	assert(0 && "listen() illegal on CMessageSocket");
+}
+
+ISocket*				CMessageSocket::accept()
+{
+	assert(0 && "accept() illegal on CMessageSocket");
+	return NULL;
+}
+
+SInt32					CMessageSocket::read(void* buffer, SInt32 n)
+{
+	// if we don't have an entire message yet then read more data
+	if (m_size == 0 || m_size < m_msgSize) {
+		doRead();
+	}
+
+	// if we don't have a whole message yet then return 0
+	if (m_size < m_msgSize)
+		return 0;
+
+	// how many bytes should we return?
+	if (m_msgSize - 2 < n)
+		n = m_msgSize - 2;
+
+	// copy data
+	// FIXME -- should have method for retrieving size of next message
+	::memcpy(buffer, m_buffer + 2, n);
+
+	// discard returned message
+	::memmove(m_buffer, m_buffer + m_msgSize, m_size - m_msgSize);
+	m_size   -= m_msgSize;
+	m_msgSize = 0;
+
+	// get next message size
+	if (m_size >= 2) {
+		m_msgSize = static_cast<SInt32>(
+								(static_cast<UInt32>(m_buffer[1]) << 8) +
+								(static_cast<UInt32>(m_buffer[2])     ));
+		TRACE(("  next message size: %d", m_msgSize));
+	}
+
+	return n;
+}
+
+void					CMessageSocket::write(const void* buffer, SInt32 n)
+{
+	// FIXME -- no fixed size buffers
+	char tmp[512];
+	assert(n < (SInt32)sizeof(tmp) - 2);
+	::memcpy(tmp + 2, buffer, n);
+	n += 2;
+	tmp[0] = static_cast<char>((n >> 8) & 0xff);
+	tmp[1] = static_cast<char>(n & 0xff);
+	m_socket->write(tmp, n);
+}
+
+SInt32					CMessageSocket::doRead()
+{
+	// if read buffer is full then grow it
+	if (m_size == m_capacity) {
+		// compute new capacity and allocate space
+		SInt32 newCapacity = (m_capacity < 256) ? 256 : 2 * m_capacity;
+		UInt8* newBuffer = new UInt8[newCapacity];
+
+		// cut over
+		::memcpy(newBuffer, m_buffer, m_size);
+		delete[] m_buffer;
+		m_buffer   = newBuffer;
+		m_capacity = newCapacity;
+	}
+
+	// read as much data as possible
+	const SInt32 numRead = m_socket->read(m_buffer + m_size,
+												m_capacity - m_size);
+	TRACE(("socket %p read %d bytes", this, numRead));
+
+	// hangup is a special case.  if buffer isn't empty then we'll
+	// discard the partial message.
+	if (numRead == -1)
+		return numRead;
+
+	// get next message size
+	if (m_size < 2 && m_size + numRead >= 2) {
+		m_msgSize = static_cast<SInt32>(
+								(static_cast<UInt32>(m_buffer[0]) << 8) +
+								(static_cast<UInt32>(m_buffer[1])     ));
+		TRACE(("  next message size: %d", m_msgSize));
+	}
+
+	m_size += numRead;
+	return numRead;
+}
+
+void					CMessageSocket::readJobCB()
+{
+	if (doRead() == -1) {
+		// remote side hungup.  don't check for readability anymore.
+		m_socket->setReadJob(NULL);
+	}
+	else if (m_size > 0 && m_size >= m_msgSize) {
+		TRACE(("  message ready"));
+		runReadJob();
+	}
+}
+
+void					CMessageSocket::writeJobCB()
+{
+	runWriteJob();
+}
diff --git a/CMessageSocket.h b/CMessageSocket.h
new file mode 100644
index 00000000..b199c3ec
--- /dev/null
+++ b/CMessageSocket.h
@@ -0,0 +1,33 @@
+#ifndef CMESSAGESOCKET_H
+#define CMESSAGESOCKET_H
+
+#include "CSocket.h"
+
+class CMessageSocket : public CSocket {
+  public:
+	CMessageSocket(ISocket* adoptedSocket);
+	virtual ~CMessageSocket();
+
+	// ISocket overrides
+	// connect(), listen(), and accept() may not be called.
+	virtual void		setWriteJob(IJob* adoptedJob);
+	virtual void		connect(const CString& hostname, UInt16 port);
+	virtual void		listen(const CString& hostname, UInt16 port);
+	virtual ISocket*	accept();
+	virtual SInt32		read(void* buffer, SInt32 numBytes);
+	virtual void		write(const void* buffer, SInt32 numBytes);
+
+  private:
+	SInt32				doRead();
+	virtual void		readJobCB();
+	virtual void		writeJobCB();
+
+  private:
+	ISocket*			m_socket;
+	UInt8*				m_buffer;
+	SInt32				m_size;
+	SInt32				m_capacity;
+	SInt32				m_msgSize;
+};
+
+#endif
diff --git a/CProtocol.h b/CProtocol.h
new file mode 100644
index 00000000..83659fd4
--- /dev/null
+++ b/CProtocol.h
@@ -0,0 +1,20 @@
+#ifndef CPROTOCOL_H
+#define CPROTOCOL_H
+
+#include "BasicTypes.h"
+
+class CProtocol {
+  public:
+	CProtocol();
+	virtual ~CProtocol();
+
+	// manipulators
+
+
+	// accessors
+
+	void				ReadMessage(ISocket*, CMessage&) const;
+	void				WriteMessage(ISocket*, const CMessage&) const;
+};
+
+#endif
diff --git a/CScreenProxy.cpp b/CScreenProxy.cpp
new file mode 100644
index 00000000..d287107d
--- /dev/null
+++ b/CScreenProxy.cpp
@@ -0,0 +1,234 @@
+#include "CScreenProxy.h"
+#include "ISocket.h"
+#include "CMessageSocket.h"
+#include "TMethodJob.h"
+#include "CTrace.h"
+//
+// CScreenProxy
+//
+
+CScreenProxy::CScreenProxy(const CString& name, ISocket* socket) :
+								m_name(name),
+								m_socket(socket),
+								m_w(0), m_h(0)
+{
+	assert(!m_name.empty());
+	assert(m_socket != NULL);
+
+	m_socket = new CMessageSocket(m_socket);
+	m_socket->setReadJob(new TMethodJob<CScreenProxy>(this,
+								&CScreenProxy::onRead));
+}
+
+CScreenProxy::~CScreenProxy()
+{
+	delete m_socket;
+}
+
+void					CScreenProxy::open(bool isPrimary)
+{
+	char buf[2];
+	memcpy(buf, "\002", 1);
+	buf[1] = static_cast<char>(isPrimary ? 1 : 0);
+	m_socket->write(buf, sizeof(buf));
+}
+
+void					CScreenProxy::close()
+{
+	char buf[1];
+	memcpy(buf, "\003", 1);
+	m_socket->write(buf, sizeof(buf));
+}
+
+void					CScreenProxy::enterScreen(SInt32 x, SInt32 y)
+{
+	char buf[9];
+	memcpy(buf, "\004", 1);
+	buf[1] = static_cast<char>((x >> 24) & 0xff);
+	buf[2] = static_cast<char>((x >> 16) & 0xff);
+	buf[3] = static_cast<char>((x >> 8) & 0xff);
+	buf[4] = static_cast<char>(x & 0xff);
+	buf[5] = static_cast<char>((y >> 24) & 0xff);
+	buf[6] = static_cast<char>((y >> 16) & 0xff);
+	buf[7] = static_cast<char>((y >> 8) & 0xff);
+	buf[8] = static_cast<char>(y & 0xff);
+	m_socket->write(buf, sizeof(buf));
+}
+
+void					CScreenProxy::leaveScreen()
+{
+	char buf[1];
+	memcpy(buf, "\005", 1);
+	m_socket->write(buf, sizeof(buf));
+}
+
+void					CScreenProxy::warpCursor(SInt32 x, SInt32 y)
+{
+	char buf[9];
+	memcpy(buf, "\020", 1);
+	buf[1] = static_cast<char>((x >> 24) & 0xff);
+	buf[2] = static_cast<char>((x >> 16) & 0xff);
+	buf[3] = static_cast<char>((x >> 8) & 0xff);
+	buf[4] = static_cast<char>(x & 0xff);
+	buf[5] = static_cast<char>((y >> 24) & 0xff);
+	buf[6] = static_cast<char>((y >> 16) & 0xff);
+	buf[7] = static_cast<char>((y >> 8) & 0xff);
+	buf[8] = static_cast<char>(y & 0xff);
+	m_socket->write(buf, sizeof(buf));
+}
+
+void					CScreenProxy::setClipboard(const IClipboard*)
+{
+	// FIXME
+}
+
+void					CScreenProxy::onKeyDown(KeyID k)
+{
+	char buf[5];
+	memcpy(buf, "\007", 1);
+	buf[1] = static_cast<char>((k >> 24) & 0xff);
+	buf[2] = static_cast<char>((k >> 16) & 0xff);
+	buf[3] = static_cast<char>((k >> 8) & 0xff);
+	buf[4] = static_cast<char>(k & 0xff);
+	m_socket->write(buf, sizeof(buf));
+}
+
+void					CScreenProxy::onKeyRepeat(KeyID k, SInt32 n)
+{
+	char buf[9];
+	memcpy(buf, "\010", 1);
+	buf[1] = static_cast<char>((k >> 24) & 0xff);
+	buf[2] = static_cast<char>((k >> 16) & 0xff);
+	buf[3] = static_cast<char>((k >> 8) & 0xff);
+	buf[4] = static_cast<char>(k & 0xff);
+	buf[5] = static_cast<char>((n >> 24) & 0xff);
+	buf[6] = static_cast<char>((n >> 16) & 0xff);
+	buf[7] = static_cast<char>((n >> 8) & 0xff);
+	buf[8] = static_cast<char>(n & 0xff);
+	m_socket->write(buf, sizeof(buf));
+}
+
+void					CScreenProxy::onKeyUp(KeyID k)
+{
+	char buf[5];
+	memcpy(buf, "\011", 1);
+	buf[1] = static_cast<char>((k >> 24) & 0xff);
+	buf[2] = static_cast<char>((k >> 16) & 0xff);
+	buf[3] = static_cast<char>((k >> 8) & 0xff);
+	buf[4] = static_cast<char>(k & 0xff);
+	m_socket->write(buf, sizeof(buf));
+}
+
+void					CScreenProxy::onKeyToggle(KeyToggleMask m)
+{
+	char buf[3];
+	memcpy(buf, "\012", 1);
+	buf[1] = static_cast<char>((m >> 8) & 0xff);
+	buf[2] = static_cast<char>(m & 0xff);
+	m_socket->write(buf, sizeof(buf));
+}
+
+void					CScreenProxy::onMouseDown(ButtonID b)
+{
+	char buf[2];
+	memcpy(buf, "\013", 1);
+	buf[1] = static_cast<char>(b & 0xff);
+	m_socket->write(buf, sizeof(buf));
+}
+
+void					CScreenProxy::onMouseUp(ButtonID b)
+{
+	char buf[2];
+	memcpy(buf, "\014", 1);
+	buf[1] = static_cast<char>(b & 0xff);
+	m_socket->write(buf, sizeof(buf));
+}
+
+void					CScreenProxy::onMouseMove(SInt32 x, SInt32 y)
+{
+	char buf[9];
+	memcpy(buf, "\015", 1);
+	buf[1] = static_cast<char>((x >> 24) & 0xff);
+	buf[2] = static_cast<char>((x >> 16) & 0xff);
+	buf[3] = static_cast<char>((x >> 8) & 0xff);
+	buf[4] = static_cast<char>(x & 0xff);
+	buf[5] = static_cast<char>((y >> 24) & 0xff);
+	buf[6] = static_cast<char>((y >> 16) & 0xff);
+	buf[7] = static_cast<char>((y >> 8) & 0xff);
+	buf[8] = static_cast<char>(y & 0xff);
+	m_socket->write(buf, sizeof(buf));
+}
+
+void					CScreenProxy::onMouseWheel(SInt32 n)
+{
+	char buf[5];
+	memcpy(buf, "\016", 1);
+	buf[1] = static_cast<char>((n >> 24) & 0xff);
+	buf[2] = static_cast<char>((n >> 16) & 0xff);
+	buf[3] = static_cast<char>((n >> 8) & 0xff);
+	buf[4] = static_cast<char>(n & 0xff);
+	m_socket->write(buf, sizeof(buf));
+}
+
+void					CScreenProxy::onScreenSaver(bool show)
+{
+	char buf[2];
+	memcpy(buf, "\017", 1);
+	buf[1] = show ? 1 : 0;
+	m_socket->write(buf, sizeof(buf));
+}
+
+void					CScreenProxy::onClipboardChanged()
+{
+	// FIXME
+}
+
+CString					CScreenProxy::getName() const
+{
+	return m_name;
+}
+
+void					CScreenProxy::getSize(SInt32* w, SInt32* h) const
+{
+	assert(w != NULL);
+	assert(h != NULL);
+
+	*w = m_w;
+	*h = m_h;
+}
+
+void					CScreenProxy::getClipboard(IClipboard*) const
+{
+	// FIXME
+}
+
+void					CScreenProxy::onRead()
+{
+	char buf[512];
+	SInt32 n = m_socket->read(buf, sizeof(buf));
+	if (n == -1) {
+		// FIXME -- disconnect
+		TRACE(("hangup"));
+	}
+	else if (n > 0) {
+		switch (buf[0]) {
+		  case '\201':
+			m_w = static_cast<SInt32>(
+								(static_cast<UInt32>(buf[1]) << 24) +
+								(static_cast<UInt32>(buf[2]) << 16) +
+								(static_cast<UInt32>(buf[3]) << 8) +
+								(static_cast<UInt32>(buf[4])     ));
+			m_h = static_cast<SInt32>(
+								(static_cast<UInt32>(buf[5]) << 24) +
+								(static_cast<UInt32>(buf[6]) << 16) +
+								(static_cast<UInt32>(buf[7]) << 8) +
+								(static_cast<UInt32>(buf[8])     ));
+			TRACE(("new size: %dx%d", m_w, m_h));
+			break;
+
+		  default:
+			TRACE(("unknown message: 0x%02x, %d bytes", buf[0], n));
+			break;
+		}
+	}
+}
diff --git a/CScreenProxy.h b/CScreenProxy.h
new file mode 100644
index 00000000..39786d82
--- /dev/null
+++ b/CScreenProxy.h
@@ -0,0 +1,43 @@
+#ifndef CSCREENPROXY_H
+#define CSCREENPROXY_H
+
+#include "IScreen.h"
+
+class ISocket;
+
+class CScreenProxy : public IScreen {
+  public:
+	CScreenProxy(const CString& name, ISocket*);
+	virtual ~CScreenProxy();
+
+	// IScreen overrides
+	virtual void		open(bool);
+	virtual void		close();
+	virtual void		enterScreen(SInt32 xAbsolute, SInt32 yAbsolute);
+	virtual void		leaveScreen();
+	virtual void		warpCursor(SInt32 xAbsolute, SInt32 yAbsolute);
+	virtual void		setClipboard(const IClipboard*);
+	virtual void		onScreenSaver(bool show);
+	virtual void		onKeyDown(KeyID);
+	virtual void		onKeyRepeat(KeyID, SInt32 count);
+	virtual void		onKeyUp(KeyID);
+	virtual void		onKeyToggle(KeyToggleMask);
+	virtual void		onMouseDown(ButtonID);
+	virtual void		onMouseUp(ButtonID);
+	virtual void		onMouseMove(SInt32 xAbsolute, SInt32 yAbsolute);
+	virtual void		onMouseWheel(SInt32 delta);
+	virtual void		onClipboardChanged();
+	virtual CString		getName() const;
+	virtual void		getSize(SInt32* width, SInt32* height) const;
+	virtual void		getClipboard(IClipboard*) const;
+
+  private:
+	void				onRead();
+
+  private:
+	CString				m_name;
+	ISocket*			m_socket;
+	SInt32				m_w, m_h;
+};
+
+#endif
diff --git a/CServer.cpp b/CServer.cpp
new file mode 100644
index 00000000..fb28810f
--- /dev/null
+++ b/CServer.cpp
@@ -0,0 +1,811 @@
+#include "CServer.h"
+#include "CEvent.h"
+#include "IEventQueue.h"
+#include "IScreen.h"
+#include "CScreenProxy.h"
+#include "ISocket.h"
+#include "CSocketFactory.h"
+#include "CMessageSocket.h"
+#include "TMethodJob.h"
+#include "CTrace.h"
+#include <assert.h>
+#include <string.h>
+#include <ctype.h>
+
+#if !defined(NDEBUG)
+static const char*		s_dirName[] = { "left", "right", "top", "bottom" };
+#endif
+
+//
+// CServerSocketJob
+//
+
+class CServerSocketJob : public IJob {
+  public:
+	typedef void (CServer::*ServerMethod)(ISocket*);
+
+	CServerSocketJob(CServer*, ServerMethod, ISocket*);
+	virtual ~CServerSocketJob();
+
+	// IJob overrides
+	virtual void		run();
+
+  private:
+	CServer*			m_server;
+	ServerMethod		m_method;
+	ISocket*			m_socket;
+};
+
+CServerSocketJob::CServerSocketJob(CServer* server,
+								ServerMethod method, ISocket* socket) :
+								m_server(server),
+								m_method(method),
+								m_socket(socket)
+{
+	// do nothing
+}
+
+CServerSocketJob::~CServerSocketJob()
+{
+	// do nothing
+}
+
+void					CServerSocketJob::run()
+{
+	(m_server->*m_method)(m_socket);
+}
+
+
+//
+// CServer
+//
+
+class XServerScreenExists {	// FIXME
+  public:
+	XServerScreenExists(const CString&) { }
+};
+
+// the width/height of the zone on the edge of the local screen that
+// will provoke a switch to a neighboring screen.  this generally
+// shouldn't be changed because it effectively reduces the size of
+// the local screen's screen.
+// FIXME -- should get this from the local screen itself.  it may
+// need a slightly larger zone (to avoid virtual screens) or it may
+// be able to generate off-screen coordinates to provoke the switch
+// in which case the size can be zero.
+const SInt32			CServer::s_zoneSize = 1;
+
+CServer::CServer() : m_running(false), m_done(false),
+								m_localScreen(NULL),
+								m_activeScreen(NULL),
+								m_listenHost(),
+								// FIXME -- define kDefaultPort
+								m_listenPort(40001/*CProtocol::kDefaultPort*/),
+								m_listenSocket(NULL)
+{
+	// FIXME
+}
+
+CServer::~CServer()
+{
+	assert(m_listenSocket == NULL);
+
+	// FIXME
+}
+
+void					CServer::setListenPort(
+								const CString& hostname, UInt16 port)
+{
+	m_listenHost = hostname;
+	m_listenPort = port;
+}
+
+void					CServer::addLocalScreen(IScreen* screen)
+{
+	assert(screen != NULL);
+	assert(m_running == false);
+	assert(m_localScreen == NULL);
+
+	addScreen(screen->getName(), screen);
+	m_localScreen  = screen;
+	m_activeScreen = screen;
+
+	// open the screen as primary
+	screen->open(true);
+}
+
+void					CServer::addRemoteScreen(const CString& name)
+{
+	addScreen(name, NULL);
+}
+
+void					CServer::addScreen(const CString& name, IScreen* screen)
+{
+	assert(!name.empty());
+
+	// cannot add a screen multiple times
+	if (m_map.count(name) != 0)
+		throw XServerScreenExists(name);
+
+	// add entry for screen in the map
+	ScreenCell& cell = m_map[name];
+
+	// set the cell's screen
+	cell.m_screen = screen;
+}
+
+void					CServer::removeScreen(const CString& name)
+{
+	// screen must in map
+	assert(!name.empty());
+	assert(m_map.count(name) == 1);
+
+	// look up cell
+	ScreenCell& cell = m_map[name];
+
+	// if this is the local screen then there must not be any other
+	// screens and we must not be running.
+	assert(cell.m_screen != m_localScreen || (m_map.size() == 1 && !m_running));
+
+	// if this is the active screen then warp to the local screen, or
+	// set no active screen if this is the local screen.
+	if (cell.m_screen == m_localScreen) {
+		m_activeScreen = NULL;
+		m_localScreen  = NULL;
+	}
+	else if (cell.m_screen == m_activeScreen) {
+		setActiveScreen(m_localScreen);
+	}
+
+	// close the screen
+	if (cell.m_screen)
+		cell.m_screen->close();
+
+	// fix up map
+	if (!cell.m_neighbor[kLeft].empty()) {
+		assert(m_map.count(cell.m_neighbor[kLeft]) == 1);
+		m_map[cell.m_neighbor[kLeft]].m_neighbor[kRight] =
+											cell.m_neighbor[kRight];
+	}
+	if (!cell.m_neighbor[kRight].empty()) {
+		assert(m_map.count(cell.m_neighbor[kRight]) == 1);
+		m_map[cell.m_neighbor[kRight]].m_neighbor[kLeft] =
+											cell.m_neighbor[kLeft];
+	}
+	if (!cell.m_neighbor[kTop].empty()) {
+		assert(m_map.count(cell.m_neighbor[kTop]) == 1);
+		m_map[cell.m_neighbor[kTop]].m_neighbor[kBottom] =
+											cell.m_neighbor[kBottom];
+	}
+	if (!cell.m_neighbor[kBottom].empty()) {
+		assert(m_map.count(cell.m_neighbor[kBottom]) == 1);
+		m_map[cell.m_neighbor[kBottom]].m_neighbor[kTop] =
+											cell.m_neighbor[kTop];
+	}
+}
+
+void					CServer::connectEdge(
+								const CString& src, EDirection srcSide,
+								const CString& dst)
+{
+	// check input
+	assert(!src.empty());
+	assert(!dst.empty());
+	assert(srcSide >= kFirstDirection && srcSide <= kLastDirection);
+
+	// both screens must exist in map
+	assert(m_map.count(src) == 1);
+	assert(m_map.count(dst) == 1);
+
+	// look up map entry
+	ScreenCell& cell = m_map[src];
+
+	// set edge
+	cell.m_neighbor[srcSide] = dst;
+
+	TRACE(("connect %s:%s to %s", src.c_str(),
+								s_dirName[srcSide],
+								cell.m_neighbor[srcSide].c_str()));
+}
+
+void					CServer::disconnectEdge(
+								const CString& src, EDirection srcSide)
+{
+	// check input
+	assert(!src.empty());
+	assert(srcSide >= kFirstDirection && srcSide <= kLastDirection);
+	assert(m_map.count(src) == 1);
+
+	TRACE(("disconnect %s:%s from %s", src.c_str(),
+								s_dirName[srcSide],
+								m_map[src].m_neighbor[srcSide].c_str()));
+
+	// look up map entry
+	ScreenCell& cell = m_map[src];
+
+	// set edge
+	cell.m_neighbor[srcSide] = CString();
+}
+
+void					CServer::run()
+{
+	assert(m_running == false);
+	assert(m_activeScreen != NULL);
+	assert(m_activeScreen == m_localScreen);
+
+	// prepare socket to listen for remote screens
+	// FIXME -- need m_socketFactory (creates sockets of desired type)
+//	m_listenSocket = m_socketFactory->createSocket();
+m_listenSocket = CSOCKETFACTORY->create();
+	m_listenSocket->setReadJob(new TMethodJob<CServer>(this,
+										&CServer::newConnectionCB));
+	// FIXME -- keep retrying until this works (in case of FIN_WAIT).
+	// also, must clean up m_listenSocket if this method throws anywhere.
+	m_listenSocket->listen(m_listenHost, m_listenPort);
+
+	// now running
+	m_running = true;
+
+	// event loop
+	IEventQueue* queue = CEQ;
+	while (!m_done) {
+		// wait for new connections, network messages, and user events
+		queue->wait(-1.0);
+
+		// handle events
+		while (!queue->isEmpty()) {
+			// get the next event
+			CEvent event;
+			queue->pop(&event);
+
+			// handle it
+			switch (event.m_any.m_type) {
+			  case CEventBase::kNull:
+				// do nothing
+				break;
+
+			  case CEventBase::kKeyDown:
+			  case CEventBase::kKeyRepeat:
+			  case CEventBase::kKeyUp:
+				if (!onCommandKey(&event.m_key))
+					relayEvent(&event);
+				break;
+
+			  case CEventBase::kMouseDown:
+			  case CEventBase::kMouseUp:
+			  case CEventBase::kMouseWheel:
+				relayEvent(&event);
+				break;
+
+			  case CEventBase::kMouseMove:
+				if (m_localScreen == m_activeScreen)
+					onLocalMouseMove(event.m_mouse.m_x, event.m_mouse.m_y);
+				else
+					onRemoteMouseMove(event.m_mouse.m_x, event.m_mouse.m_y);
+				break;
+
+			  case CEventBase::kScreenSize:
+				// FIXME
+				break;
+			}
+		}
+	}
+
+	// reset
+	m_running = false;
+	m_done    = false;
+
+	// tell screens to shutdown
+	// FIXME
+
+	// close our socket
+	delete m_listenSocket;
+	m_listenSocket = NULL;
+}
+
+void					CServer::onClipboardChanged(IScreen*)
+{
+	// FIXME -- should take screen name not screen pointer
+	// FIXME
+}
+
+void					CServer::setActiveScreen(IScreen* screen)
+{
+	// FIXME -- should take screen name not screen pointer
+	assert(screen != NULL);
+	assert(m_map.count(screen->getName()) == 1);
+
+	// ignore if no change
+	if (m_activeScreen == screen)
+		return;
+
+	// get center of screen
+	SInt32 w, h;
+	screen->getSize(&w, &h);
+	w >>= 1;
+	h >>= 1;
+
+	// switch
+	switchScreen(screen, w, h);
+}
+
+IScreen*				CServer::getActiveScreen() const
+{
+	return m_activeScreen;
+}
+
+void					CServer::relayEvent(const CEvent* event)
+{
+	assert(event != NULL);
+	assert(m_activeScreen != NULL);
+
+	// ignore attempts to relay to the local screen
+	if (m_activeScreen == m_localScreen)
+		return;
+
+	// relay the event
+	switch (event->m_any.m_type) {
+	  case CEventBase::kNull:
+		// do nothing
+		break;
+
+	  case CEventBase::kKeyDown:
+		m_activeScreen->onKeyDown(event->m_key.m_key);
+		break;
+
+	  case CEventBase::kKeyRepeat:
+		m_activeScreen->onKeyRepeat(event->m_key.m_key, event->m_key.m_count);
+		break;
+
+	  case CEventBase::kKeyUp:
+		m_activeScreen->onKeyUp(event->m_key.m_key);
+		break;
+
+	  case CEventBase::kMouseDown:
+		m_activeScreen->onMouseDown(event->m_mouse.m_button);
+		break;
+
+	  case CEventBase::kMouseUp:
+		m_activeScreen->onMouseUp(event->m_mouse.m_button);
+		break;
+
+	  case CEventBase::kMouseWheel:
+		m_activeScreen->onMouseWheel(event->m_mouse.m_x);
+		break;
+
+	  case CEventBase::kMouseMove:
+		assert(0 && "kMouseMove relayed");
+		break;
+
+	  default:
+		assert(0 && "invalid event relayed");
+		break;
+	}
+}
+
+bool					CServer::onCommandKey(const CEventKey* /*keyEvent*/)
+{
+	// FIXME -- strip out command keys (e.g. lock to screen, warp, etc.)
+	return false;
+}
+
+void					CServer::onLocalMouseMove(SInt32 x, SInt32 y)
+{
+	assert(m_activeScreen == m_localScreen);
+
+	// ignore if locked to screen
+	if (isLockedToScreen())
+		return;
+
+	// get local screen's size
+	SInt32 w, h;
+	m_activeScreen->getSize(&w, &h);
+
+	// see if we should change screens
+	EDirection dir;
+	if (x < s_zoneSize) {
+		x -= s_zoneSize;
+		dir = kLeft;
+	}
+	else if (x >= w - s_zoneSize) {
+		x += s_zoneSize;
+		dir = kRight;
+	}
+	else if (y < s_zoneSize) {
+		y -= s_zoneSize;
+		dir = kTop;
+	}
+	else if (y >= h - s_zoneSize) {
+		y += s_zoneSize;
+		dir = kBottom;
+	}
+	else {
+		// still on local screen
+		return;
+	}
+	TRACE(("leave %s on %s", m_activeScreen->getName().c_str(), s_dirName[dir]));
+
+	// get new screen.  if no screen in that direction then ignore move.
+	IScreen* newScreen = getNeighbor(m_activeScreen, dir, x, y);
+	if (newScreen == NULL)
+		return;
+
+	// remap position to account for resolution differences between screens
+	mapPosition(m_activeScreen, dir, newScreen, x, y);
+
+	// switch screen
+	switchScreen(newScreen, x, y);
+}
+
+void					CServer::onRemoteMouseMove(SInt32 dx, SInt32 dy)
+{
+	assert(m_activeScreen != NULL);
+	assert(m_activeScreen != m_localScreen);
+
+	// put mouse back in center of local screen's grab area
+// XXX	m_localScreen->warpToCenter();
+
+	// save old position
+	const SInt32 xOld = m_x;
+	const SInt32 yOld = m_y;
+
+	// accumulate mouse position
+	m_x += dx;
+	m_y += dy;
+
+	// get active screen's size
+	SInt32 w, h;
+	m_activeScreen->getSize(&w, &h);
+
+	// switch screens if mouse is outside screen and not locked to screen
+	IScreen* newScreen = NULL;
+	if (!isLockedToScreen()) {
+		// find direction of neighbor
+		EDirection dir;
+		if (m_x < 0)
+			dir = kLeft;
+		else if (m_x > w - 1)
+			dir = kRight;
+		else if (m_y < 0)
+			dir = kTop;
+		else if (m_y > h - 1)
+			dir = kBottom;
+		else
+			newScreen = m_activeScreen;
+
+		// get neighbor if we should switch
+		if (newScreen == NULL) {
+			TRACE(("leave %s on %s", m_activeScreen->getName().c_str(),
+								s_dirName[dir]));
+
+			SInt32 x = m_x, y = m_y;
+			newScreen = getNeighbor(m_activeScreen, dir, x, y);
+
+			// remap position to account for resolution differences
+			if (newScreen != NULL) {
+				m_x = x;
+				m_y = y;
+				mapPosition(m_activeScreen, dir, newScreen, m_x, m_y);
+			}
+			else {
+				if (m_x < 0)
+					m_x = 0;
+				else if (m_x > w - 1)
+					m_x = w - 1;
+				if (m_y < 0)
+					m_y = 0;
+				else if (m_y > h - 1)
+					m_y = h - 1;
+			}
+		}
+	}
+
+	// clamp mouse position if locked to screen
+	else {
+		TRACE(("clamp to %s", m_activeScreen->getName().c_str()));
+
+		if (m_x < 0)
+			m_x = 0;
+		else if (m_x > w - 1)
+			m_x = w - 1;
+		if (m_y < 0)
+			m_y = 0;
+		else if (m_y > h - 1)
+			m_y = h - 1;
+	}
+
+	// if on same screen then warp cursor
+	if (newScreen == NULL || newScreen == m_activeScreen) {
+		// ignore if clamped mouse didn't move
+		if (m_x != xOld || m_y != yOld) {
+			TRACE(("move on %s to %d,%d",
+								m_activeScreen->getName().c_str(), m_x, m_y));
+			m_activeScreen->onMouseMove(m_x, m_y);
+		}
+	}
+
+	// otherwise switch the screen
+	else {
+		switchScreen(newScreen, m_x, m_y);
+	}
+}
+
+bool					CServer::isLockedToScreen() const
+{
+	// FIXME
+	return false;
+}
+
+void					CServer::mapPosition(
+								const IScreen* src, EDirection srcSide,
+								const IScreen* dst, SInt32& x, SInt32& y) const
+{
+	assert(src != NULL);
+	assert(dst != NULL);
+	assert(srcSide >= kFirstDirection && srcSide <= kLastDirection);
+
+	// get sizes
+	SInt32 wSrc, hSrc, wDst, hDst;
+	src->getSize(&wSrc, &hSrc);
+	dst->getSize(&wDst, &hDst);
+
+	// remap
+	switch (srcSide) {
+	  case kLeft:
+	  case kRight:
+		assert(y >= 0 && y < hSrc);
+		y = static_cast<SInt32>(0.5 + y *
+								static_cast<double>(hDst - 1) / (hSrc - 1));
+		break;
+
+	  case kTop:
+	  case kBottom:
+		assert(x >= 0 && x < wSrc);
+		x = static_cast<SInt32>(0.5 + x *
+								static_cast<double>(wSrc - 1) / (wSrc - 1));
+		break;
+	}
+}
+
+IScreen*				CServer::getNeighbor(
+								const IScreen* src, EDirection dir) const
+{
+	// check input
+	assert(src != NULL);
+	assert(dir >= kFirstDirection && dir <= kLastDirection);
+	assert(m_map.count(src->getName()) == 1);
+
+	// look up source cell
+	ScreenMap::const_iterator index = m_map.find(src->getName());
+	do {
+		// look up name of neighbor
+		const ScreenCell& cell = index->second;
+		const CString dstName(cell.m_neighbor[dir]);
+
+		// if nothing in that direction then return NULL
+		if (dstName.empty())
+			return NULL;
+
+		// look up neighbor cell
+		assert(m_map.count(dstName) == 1);
+		index = m_map.find(dstName);
+
+		// if no screen pointer then can't go to that neighbor so keep
+		// searching in the same direction.
+#ifndef NDEBUG
+		if (index->second.m_screen == NULL)
+			TRACE(("skipping over unconnected screen %s", dstName.c_str()));
+#endif
+	} while (index->second.m_screen == NULL);
+
+	return index->second.m_screen;
+}
+
+IScreen*				CServer::getNeighbor(
+								const IScreen* src, EDirection srcSide,
+								SInt32& x, SInt32& y) const
+{
+	// given a position relative to src and which side of the screen we
+	// left, find the screen we should move onto and where.  if the
+	// position is sufficiently far from src then we may cross multiple
+	// screens.
+
+	// check input
+	assert(src != NULL);
+	assert(srcSide >= kFirstDirection && srcSide <= kLastDirection);
+
+	// get the first neighbor
+	IScreen* dst = getNeighbor(src, srcSide);
+	IScreen* lastGoodScreen = dst;
+
+	// get the original screen's size (needed for kRight and kBottom)
+	SInt32 w, h;
+	src->getSize(&w, &h);
+
+	// find destination screen, adjusting x or y (but not both)
+	switch (srcSide) {
+	  case kLeft:
+		while (dst) {
+			lastGoodScreen = dst;
+			lastGoodScreen->getSize(&w, &h);
+			x += w;
+			if (x >= 0)
+				break;
+			TRACE(("skipping over screen %s", dst->getName().c_str()));
+			dst = getNeighbor(lastGoodScreen, srcSide);
+		}
+		break;
+
+	  case kRight:
+		while (dst) {
+			lastGoodScreen = dst;
+			x -= w;
+			lastGoodScreen->getSize(&w, &h);
+			if (x < w)
+				break;
+			TRACE(("skipping over screen %s", dst->getName().c_str()));
+			dst = getNeighbor(lastGoodScreen, srcSide);
+		}
+		break;
+
+	  case kTop:
+		while (dst) {
+			lastGoodScreen = dst;
+			lastGoodScreen->getSize(&w, &h);
+			y += h;
+			if (y >= 0)
+				break;
+			TRACE(("skipping over screen %s", dst->getName().c_str()));
+			dst = getNeighbor(lastGoodScreen, srcSide);
+		}
+		break;
+
+	  case kBottom:
+		while (dst) {
+			lastGoodScreen = dst;
+			y -= h;
+			lastGoodScreen->getSize(&w, &h);
+			if (y < h)
+				break;
+			TRACE(("skipping over screen %s", dst->getName().c_str()));
+			dst = getNeighbor(lastGoodScreen, srcSide);
+		}
+		break;
+	}
+
+	// if entering local screen then be sure to move in far enough to
+	// avoid the switching zone.  if entering a side that doesn't have
+	// a neighbor (i.e. an asymmetrical side) then we don't need to
+	// move inwards because that side can't provoke a switch.
+	if (lastGoodScreen == m_localScreen) {
+		ScreenMap::const_iterator index = m_map.find(m_localScreen->getName());
+		const ScreenCell& cell = index->second;
+		switch (srcSide) {
+		  case kLeft:
+			if (!cell.m_neighbor[kRight].empty() && x > w - 1 - s_zoneSize)
+				x = w - 1 - s_zoneSize;
+			break;
+
+		  case kRight:
+			if (!cell.m_neighbor[kLeft].empty() && x < s_zoneSize)
+				x = s_zoneSize;
+			break;
+
+		  case kTop:
+			if (!cell.m_neighbor[kBottom].empty() && y > h - 1 - s_zoneSize)
+				y = h - 1 - s_zoneSize;
+			break;
+
+		  case kBottom:
+			if (!cell.m_neighbor[kTop].empty() && y < s_zoneSize)
+				y = s_zoneSize;
+			break;
+		}
+	}
+
+	return lastGoodScreen;
+}
+
+void					CServer::switchScreen(
+								IScreen* screen, SInt32 x, SInt32 y)
+{
+	assert(screen != NULL);
+	assert(m_running == true);
+	assert(m_activeScreen != NULL);
+#ifndef NDEBUG
+	{
+		SInt32 w, h;
+		screen->getSize(&w, &h);
+		assert(x >= 0 && y >= 0 && x < w && y < h);
+	}
+#endif
+
+	TRACE(("switch %s to %s at %d,%d", m_activeScreen->getName().c_str(),
+								screen->getName().c_str(), x, y));
+
+	// wrapping means leaving the active screen and entering it again.
+	// since that's a waste of time we skip that and just warp the
+	// mouse.
+	if (m_activeScreen != screen) {
+		// leave active screen
+		m_activeScreen->leaveScreen();
+
+		// cut over
+		m_activeScreen = screen;
+
+		// enter new screen
+		m_activeScreen->enterScreen(x, y);
+	}
+	else {
+		m_activeScreen->warpCursor(x, y);
+	}
+
+	// record new position
+	m_x = x;
+	m_y = y;
+}
+
+void					CServer::newConnectionCB()
+{
+	ISocket* socket = m_listenSocket->accept();
+	TRACE(("accepted socket %p", socket));
+	socket->setReadJob(new CServerSocketJob(this, &CServer::loginCB, socket));
+	m_logins.insert(socket);
+}
+
+void					CServer::loginCB(ISocket* socket)
+{
+	// FIXME -- no fixed size buffers
+	UInt8 buffer[512];
+	SInt32 n = socket->read(buffer, sizeof(buffer));
+	if (n == -1) {
+		TRACE(("socket %p disconnected", socket));
+		goto fail;
+	}
+	TRACE(("read %d bytes from socket %p", n, socket));
+	if (n <= 10) {
+		TRACE(("socket %p: bogus %d byte message; hanging up", socket, n));
+		goto fail;
+	}
+	if (n > 10) {
+		if (::memcmp(buffer, "SYNERGY\000\001", 9) != 0) {
+			TRACE(("socket %p: bad login", socket));
+			goto fail;
+		}
+
+		const SInt32 nameLen = static_cast<SInt32>(buffer[9]);
+		if (nameLen < 1 || nameLen > 64) {
+			TRACE(("socket %p: bad login name length %d", socket, nameLen));
+			goto fail;
+		}
+
+		for (SInt32 i = 0; i < nameLen; ++i)
+			if (!isalnum(buffer[10 + i])) {
+				TRACE(("socket %p: bad login name", socket));
+				goto fail;
+			}
+
+		CString name(reinterpret_cast<char*>(buffer + 10), nameLen);
+		const ScreenMap::iterator index = m_map.find(name);
+		if (index == m_map.end()) {
+			TRACE(("socket %p: unknown screen %s", socket, name.c_str()));
+			goto fail;
+		}
+		if (index->second.m_screen != NULL) {
+			TRACE(("socket %p: screen %s already connected",
+								socket, name.c_str()));
+			goto fail;
+		}
+
+		TRACE(("socket %p: login %s", socket, name.c_str()));
+		CScreenProxy* screen = new CScreenProxy(name, socket);
+		m_logins.erase(socket);
+		index->second.m_screen = screen;
+		index->second.m_screen->open(false);
+	}
+	return;
+
+fail:
+	m_logins.erase(socket);
+	delete socket;
+}
diff --git a/CServer.h b/CServer.h
new file mode 100644
index 00000000..b95fa7ef
--- /dev/null
+++ b/CServer.h
@@ -0,0 +1,103 @@
+#ifndef CSERVER_H
+#define CSERVER_H
+
+#include "IServer.h"
+#include "BasicTypes.h"
+#include "CString.h"
+#include <map>
+#include <set>
+
+class CEvent;
+class CEventKey;
+class IScreen;
+class ISocket;
+
+class CServer : public IServer {
+  public:
+	enum EDirection { kLeft, kRight, kTop, kBottom,
+						kFirstDirection = kLeft, kLastDirection = kBottom };
+
+	CServer();
+	virtual ~CServer();
+
+	// manipulators
+
+	// set the server's interface and port to listen for remote screens
+	void				setListenPort(const CString& hostname, UInt16 port);
+
+	// add local screen
+	void				addLocalScreen(IScreen*);
+
+	// add a remote screen
+	void				addRemoteScreen(const CString& name);
+
+	// remove a local or remote screen.  neighbors on opposite sides
+	// of this screen are made neighbors of each other.
+	void				removeScreen(const CString& name);
+
+	// connect/disconnect screen edges
+	void				connectEdge(const CString& src, EDirection srcSide,
+								const CString& dst);
+	void				disconnectEdge(const CString& src, EDirection srcSide);
+
+	// accessors
+
+
+	// IServer overrides
+	virtual void		run();
+	virtual void		onClipboardChanged(IScreen*);
+	virtual void		setActiveScreen(IScreen*);
+	virtual IScreen*	getActiveScreen() const;
+
+  protected:
+	virtual void		relayEvent(const CEvent* event);
+	virtual bool		onCommandKey(const CEventKey* keyEvent);
+	virtual void		onLocalMouseMove(SInt32 x, SInt32 y);
+	virtual void		onRemoteMouseMove(SInt32 dx, SInt32 dy);
+	virtual bool		isLockedToScreen() const;
+	virtual void		mapPosition(const IScreen* src, EDirection srcSide,
+								const IScreen* dst, SInt32& x, SInt32& y) const;
+	IScreen*			getNeighbor(const IScreen* src, EDirection) const;
+	IScreen*			getNeighbor(const IScreen* src, EDirection srcSide,
+								SInt32& x, SInt32& y) const;
+	void				switchScreen(IScreen* screen, SInt32 x, SInt32 y);
+
+  private:
+	void				addScreen(const CString&, IScreen*);
+	void				newConnectionCB();
+	void				loginCB(ISocket*);
+
+	struct ScreenCell {
+	  public:
+		ScreenCell() : m_screen(NULL) { }
+	  public:
+		IScreen*		m_screen;
+		CString			m_neighbor[kLastDirection - kFirstDirection + 1];
+	};
+
+  private:
+	typedef std::map<CString, ScreenCell> ScreenMap;
+	typedef std::set<ISocket*> SocketSet;
+
+	// main loop stuff
+	bool				m_running;
+	bool				m_done;
+
+	// screen tracking
+	IScreen*			m_localScreen;
+	IScreen*			m_activeScreen;
+	SInt32				m_x, m_y;
+	ScreenMap			m_map;
+
+	// listen socket stuff
+	CString				m_listenHost;
+	UInt16				m_listenPort;
+	ISocket*			m_listenSocket;
+
+	// login sockets
+	SocketSet			m_logins;
+
+	static const SInt32	s_zoneSize;
+};
+
+#endif
diff --git a/CSocket.cpp b/CSocket.cpp
new file mode 100644
index 00000000..311d87d0
--- /dev/null
+++ b/CSocket.cpp
@@ -0,0 +1,58 @@
+#include "CSocket.h"
+#include "IJob.h"
+
+//
+// CSocket
+//
+
+CSocket::CSocket() : m_readJob(NULL), m_writeJob(NULL)
+{
+	// do nothing
+}
+
+CSocket::~CSocket()
+{
+	delete m_readJob;
+	delete m_writeJob;
+}
+
+void					CSocket::setReadJob(IJob* adoptedJob)
+{
+	delete m_readJob;
+	m_readJob = adoptedJob;
+	onJobChanged();
+}
+
+void					CSocket::setWriteJob(IJob* adoptedJob)
+{
+	delete m_writeJob;
+	m_writeJob = adoptedJob;
+	onJobChanged();
+}
+
+void					CSocket::onJobChanged()
+{
+	// do nothing
+}
+
+void					CSocket::runReadJob()
+{
+	if (m_readJob)
+		m_readJob->run();
+}
+
+void					CSocket::runWriteJob()
+{
+	if (m_writeJob)
+		m_writeJob->run();
+}
+
+bool					CSocket::hasReadJob() const
+{
+	return (m_readJob != NULL);
+}
+
+bool					CSocket::hasWriteJob() const
+{
+	return (m_writeJob != NULL);
+}
diff --git a/CSocket.h b/CSocket.h
new file mode 100644
index 00000000..aa4cf9f8
--- /dev/null
+++ b/CSocket.h
@@ -0,0 +1,41 @@
+#ifndef CSOCKET_H
+#define CSOCKET_H
+
+#include "ISocket.h"
+
+class IJob;
+
+class CSocket : public ISocket {
+  public:
+	CSocket();
+	virtual ~CSocket();
+
+	// ISocket overrides
+	virtual void		setReadJob(IJob* adoptedJob);
+	virtual void		setWriteJob(IJob* adoptedJob);
+	virtual void		connect(const CString& hostname, UInt16 port) = 0;
+	virtual void		listen(const CString& hostname, UInt16 port) = 0;
+	virtual ISocket*	accept() = 0;
+	virtual SInt32		read(void* buffer, SInt32 numBytes) = 0;
+	virtual void		write(const void* buffer, SInt32 numBytes) = 0;
+
+  protected:
+	// called when the read or write job is changed.  default does nothing.
+	virtual void		onJobChanged();
+
+	// subclasses should call these at the appropriate time.  different
+	// platforms will arrange to detect these situations differently.
+	// does nothing if the respective job is NULL.
+	void				runReadJob();
+	void				runWriteJob();
+
+	// return true iff the socket has a read job or a write job
+	bool				hasReadJob() const;
+	bool				hasWriteJob() const;
+
+  private:
+	IJob*				m_readJob;
+	IJob*				m_writeJob;
+};
+
+#endif
diff --git a/CSocketFactory.cpp b/CSocketFactory.cpp
new file mode 100644
index 00000000..0995b897
--- /dev/null
+++ b/CSocketFactory.cpp
@@ -0,0 +1,31 @@
+#include "CSocketFactory.h"
+#include "BasicTypes.h"
+#include <assert.h>
+
+//
+// CSocketFactory
+//
+
+CSocketFactory*			CSocketFactory::s_instance = NULL;
+
+CSocketFactory::CSocketFactory()
+{
+	// do nothing
+}
+
+CSocketFactory::~CSocketFactory()
+{
+	// do nothing
+}
+
+void					CSocketFactory::setInstance(CSocketFactory* factory)
+{
+	delete s_instance;
+	s_instance = factory;
+}
+
+CSocketFactory*			CSocketFactory::getInstance()
+{
+	assert(s_instance != NULL);
+	return s_instance;
+}
diff --git a/CSocketFactory.h b/CSocketFactory.h
new file mode 100644
index 00000000..7a73d391
--- /dev/null
+++ b/CSocketFactory.h
@@ -0,0 +1,29 @@
+#ifndef CSOCKETFACTORY_H
+#define CSOCKETFACTORY_H
+
+#define CSOCKETFACTORY CSocketFactory::getInstance()
+
+class ISocket;
+
+class CSocketFactory {
+  public:
+	CSocketFactory();
+	virtual ~CSocketFactory();
+
+	// manipulators
+
+	static void			setInstance(CSocketFactory*);
+
+	// accessors
+
+	// create a socket
+	virtual ISocket*	create() const = 0;
+
+	// get the global instance
+	static CSocketFactory*	getInstance();
+
+  private:
+	static CSocketFactory*	s_instance;
+};
+
+#endif
diff --git a/CString.h b/CString.h
new file mode 100644
index 00000000..9b5667ac
--- /dev/null
+++ b/CString.h
@@ -0,0 +1,40 @@
+#ifndef CSTRING_H
+#define CSTRING_H
+
+#include <string>
+
+#ifndef CSTRING_DEF_CTOR
+#define CSTRING_ALLOC1
+#define CSTRING_ALLOC2
+#define CSTRING_DEF_CTOR CString() : _Myt() { }
+#endif
+
+// use to get appropriate type for string constants.  it depends on
+// the internal representation type of CString.
+#define _CS(_x) 		_x
+
+class CString : public std::string {
+  public:
+	typedef char _E;
+	typedef _E CharT;
+	typedef std::allocator<_E> _A;
+	typedef std::string _Myt;
+	typedef const_iterator _It;
+
+	// same constructors as base class
+	CSTRING_DEF_CTOR
+	CString(const _Myt& _X) : _Myt(_X) { }
+	CString(const _Myt& _X, size_type _P, size_type _M CSTRING_ALLOC1) :
+								_Myt(_X, _P, _M CSTRING_ALLOC2) { }
+	CString(const _E *_S, size_type _N CSTRING_ALLOC1) :
+								_Myt(_S, _N CSTRING_ALLOC2) { }
+	CString(const _E *_S CSTRING_ALLOC1) :
+								_Myt(_S CSTRING_ALLOC2) { }
+	CString(size_type _N, _E _C CSTRING_ALLOC1) :
+								_Myt(_N, _C CSTRING_ALLOC2) { }
+	CString(_It _F, _It _L CSTRING_ALLOC1) :
+								_Myt(_F, _L CSTRING_ALLOC2) { }
+};
+
+#endif
+
diff --git a/CTrace.cpp b/CTrace.cpp
new file mode 100644
index 00000000..e782616a
--- /dev/null
+++ b/CTrace.cpp
@@ -0,0 +1,16 @@
+#include "CTrace.h"
+#include <stdarg.h>
+#include <stdio.h>
+
+//
+// CTrace
+//
+
+void					CTrace::print(const char* fmt, ...)
+{
+	va_list args;
+	va_start(args, fmt);
+	vfprintf(stderr, fmt, args);
+	va_end(args);
+	fprintf(stderr, "\n");
+}
diff --git a/CTrace.h b/CTrace.h
new file mode 100644
index 00000000..34787ac9
--- /dev/null
+++ b/CTrace.h
@@ -0,0 +1,19 @@
+#ifndef CTRACE_H
+#define CTRACE_H
+
+class CTrace {
+  public:
+	static void			print(const char* fmt, ...);
+};
+
+#if defined(NDEBUG)
+
+#define TRACE(_X)
+
+#else // NDEBUG
+
+#define TRACE(_X)		CTrace::print ## _X
+
+#endif // NDEBUG
+
+#endif
diff --git a/CUnixEventQueue.cpp b/CUnixEventQueue.cpp
new file mode 100644
index 00000000..db685dca
--- /dev/null
+++ b/CUnixEventQueue.cpp
@@ -0,0 +1,141 @@
+#include "CUnixEventQueue.h"
+#include "IJob.h"
+#include <sys/time.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+//
+// CUnixEventQueue
+//
+
+CUnixEventQueue::CUnixEventQueue()
+{
+	// do nothing
+}
+
+CUnixEventQueue::~CUnixEventQueue()
+{
+	// clean up lists
+	clearList(m_readList);
+	clearList(m_writeList);
+}
+
+void					CUnixEventQueue::addFileDesc(int fd,
+								IJob* readJob, IJob* writeJob)
+{
+	assert(fd != -1);
+	assert(m_readList.count(fd) == 0 && m_writeList.count(fd) == 0);
+	assert(readJob != writeJob || readJob == NULL);
+
+	if (readJob)
+		m_readList[fd] = readJob;
+	if (writeJob)
+		m_writeList[fd] = writeJob;
+}
+
+void					CUnixEventQueue::removeFileDesc(int fd)
+{
+	assert(fd != -1);
+
+	// remove from lists
+	eraseList(m_readList, fd);
+	eraseList(m_writeList, fd);
+}
+
+void					CUnixEventQueue::wait(double timeout)
+{
+	// prepare sets
+	fd_set fdRead, fdWrite;
+	const int maxRead  = prepList(m_readList,  &fdRead);
+	const int maxWrite = prepList(m_writeList, &fdWrite);
+
+	// compute the larger of maxRead and maxWrite
+	const int fdMax = (maxRead > maxWrite) ? maxRead : maxWrite;
+	if (fdMax == -1)
+		return;
+
+	// prepare timeout
+	struct timeval* pTimeout = NULL;
+	struct timeval  sTimeout;
+	if (timeout >= 0.0) {
+		sTimeout.tv_sec  = static_cast<int>(timeout);
+		sTimeout.tv_usec = static_cast<int>(1000000.0 *
+								(timeout - sTimeout.tv_sec));
+		pTimeout = &sTimeout;
+	}
+
+	// wait
+	const int n = ::select(fdMax + 1, &fdRead, &fdWrite, NULL, pTimeout);
+
+	// return on error or if nothing to do
+	if (n <= 0)
+		return;
+
+	// invoke jobs
+	// note -- calling removeFileDesc() from a job is likely to crash the
+	// program because we expect all jobs with active file descriptors to
+	// persist for the duration of these loops.
+	int fd;
+	for (fd = 0; fd <= maxRead; ++fd)
+		if (FD_ISSET(fd, &fdRead)) {
+			assert(m_readList.count(fd) > 0);
+			assert(m_readList[fd] != NULL);
+			m_readList[fd]->run();
+		}
+	for (fd = 0; fd <= maxWrite; ++fd)
+		if (FD_ISSET(fd, &fdWrite)) {
+			assert(m_writeList.count(fd) > 0);
+			assert(m_writeList[fd] != NULL);
+			m_writeList[fd]->run();
+		}
+}
+
+void					CUnixEventQueue::lock()
+{
+	// do nothing
+}
+
+void					CUnixEventQueue::unlock()
+{
+	// do nothing
+}
+
+void					CUnixEventQueue::signalNotEmpty()
+{
+	// do nothing
+}
+
+void					CUnixEventQueue::eraseList(List& list, int fd) const
+{
+	List::iterator index = list.find(fd);
+	if (index != list.end()) {
+		delete index->second;
+		list.erase(index);
+	}
+}
+
+void					CUnixEventQueue::clearList(List& list) const
+{
+	for (List::const_iterator index = list.begin();
+								index != list.end(); ++index)
+		delete index->second;
+	list.clear();
+}
+
+int					CUnixEventQueue::prepList(
+								const List& list, void* vfdSet) const
+{
+	fd_set* fdSet = reinterpret_cast<fd_set*>(vfdSet);
+	FD_ZERO(fdSet);
+
+	int fdMax = -1;
+	for (List::const_iterator index = list.begin();
+								index != list.end(); ++index) {
+		const int fd = index->first;
+		FD_SET(fd, fdSet);
+		if (fd > fdMax)
+			fdMax = fd;
+	}
+
+	return fdMax;
+}
diff --git a/CUnixEventQueue.h b/CUnixEventQueue.h
new file mode 100644
index 00000000..217efbf4
--- /dev/null
+++ b/CUnixEventQueue.h
@@ -0,0 +1,54 @@
+#ifndef CUNIXEVENTQUEUE_H
+#define CUNIXEVENTQUEUE_H
+
+#include "CEventQueue.h"
+#include <map>
+
+#undef CEQ
+#define CEQ ((CUnixEventQueue*)CEventQueue::getInstance())
+
+class IJob;
+
+class CUnixEventQueue : public CEventQueue {
+  public:
+	CUnixEventQueue();
+	virtual ~CUnixEventQueue();
+
+	// manipulators
+
+	// add a file descriptor to wait on.  if adoptedReadJob is not NULL
+	// then it'll be called when the file descriptor is readable.  if
+	// adoptedWriteJob is not NULL then it will be called then the file
+	// descriptor is writable.  at least one job must not be NULL and
+	// the jobs may not be the same.  ownership of the jobs is assumed.
+	// the file descriptor must not have already been added or, if it
+	// was, it must have been removed.
+	void				addFileDesc(int fd,
+								IJob* adoptedReadJob, IJob* adoptedWriteJob);
+
+	// remove a file descriptor from the list being waited on.  the
+	// associated jobs are destroyed.  the file descriptor must have
+	// been added and not since removed.
+	void				removeFileDesc(int fd);
+
+	// IEventQueue overrides
+	virtual void		wait(double timeout);
+
+  protected:
+	// CEventQueue overrides
+	virtual void		lock();
+	virtual void		unlock();
+	virtual void		signalNotEmpty();
+
+  private:
+	typedef std::map<int, IJob*> List;
+	void				eraseList(List&, int fd) const;
+	void				clearList(List&) const;
+	int					prepList(const List&, void* fdSet) const;
+
+  private:
+	List				m_readList;
+	List				m_writeList;
+};
+
+#endif
diff --git a/CUnixTCPSocket.cpp b/CUnixTCPSocket.cpp
new file mode 100644
index 00000000..ebec593b
--- /dev/null
+++ b/CUnixTCPSocket.cpp
@@ -0,0 +1,262 @@
+#include "CUnixTCPSocket.h"
+#include "CUnixEventQueue.h"
+#include "CString.h"
+#include "TMethodJob.h"
+#include "XSocket.h"
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <netdb.h>
+#include <errno.h>
+#include <string.h>
+extern int h_errno;
+
+CUnixTCPSocket::CUnixTCPSocket() : m_fd(-1),
+								m_state(kNone),
+								m_addedJobs(false)
+{
+	// create socket
+	m_fd = ::socket(PF_INET, SOCK_STREAM, 0);
+	if (m_fd == -1)
+		throw XSocketCreate(::strerror(errno));
+
+	// make it non-blocking
+	int mode = ::fcntl(m_fd, F_GETFL, 0);
+	if (mode == -1 || ::fcntl(m_fd, F_SETFL, mode | O_NONBLOCK) == -1) {
+		::close(m_fd);
+		throw XSocketCreate(::strerror(errno));
+	}
+}
+
+CUnixTCPSocket::CUnixTCPSocket(int fd) : m_fd(fd),
+								m_state(kConnected),
+								m_addedJobs(false)
+{
+	assert(m_fd != -1);
+}
+
+CUnixTCPSocket::~CUnixTCPSocket()
+{
+	assert(m_fd != -1);
+
+	// unhook events
+	if (m_addedJobs)
+		CEQ->removeFileDesc(m_fd);
+
+	// drain socket
+	if (m_state == kConnected)
+		::shutdown(m_fd, 0);
+
+	// close socket
+	::close(m_fd);
+}
+
+void					CUnixTCPSocket::onJobChanged()
+{
+	// remove old jobs
+	if (m_addedJobs) {
+		CEQ->removeFileDesc(m_fd);
+		m_addedJobs = false;
+	}
+
+	// which jobs should we install?
+	bool doRead  = false;
+	bool doWrite = false;
+	switch (m_state) {
+	  case kNone:
+		return;
+
+	  case kConnecting:
+		doWrite = true;
+		break;
+
+	  case kConnected:
+		doRead  = hasReadJob();
+		doWrite = hasWriteJob();
+		break;
+
+	  case kListening:
+		doRead = true;
+		break;
+	}
+
+	// make jobs
+	IJob* readJob  = doRead  ? new TMethodJob<CUnixTCPSocket>(this,
+										&CUnixTCPSocket::readCB) : NULL;
+	IJob* writeJob = doWrite ? new TMethodJob<CUnixTCPSocket>(this,
+										&CUnixTCPSocket::writeCB) : NULL;
+
+	// install jobs
+	CEQ->addFileDesc(m_fd, readJob, writeJob);
+	m_addedJobs = true;
+}
+
+void					CUnixTCPSocket::readCB()
+{
+	runReadJob();
+}
+
+void					CUnixTCPSocket::writeCB()
+{
+	if (m_state == kConnecting) {
+		// now connected.  start watching for reads.
+		m_state = kConnected;
+		onJobChanged();
+	}
+	runWriteJob();
+}
+
+void					CUnixTCPSocket::connect(
+								const CString& hostname, UInt16 port)
+{
+	assert(m_fd != -1);
+	assert(m_state == kNone);
+
+	// hostname to address
+	struct hostent* hent = ::gethostbyname(hostname.c_str());
+	if (hent == NULL)
+		throw XSocketName(::hstrerror(h_errno));
+
+	// construct address
+	struct sockaddr_in addr;
+	assert(hent->h_addrtype == AF_INET);
+	assert(hent->h_length   == sizeof(addr.sin_addr));
+	addr.sin_family = hent->h_addrtype;
+	addr.sin_port   = htons(port);
+	::memcpy(&addr.sin_addr, hent->h_addr_list[0], hent->h_length);
+
+	// start connecting
+	if (::connect(m_fd, reinterpret_cast<struct sockaddr*>(&addr),
+											sizeof(addr)) == -1) {
+		if (errno != EINPROGRESS)
+			throw XSocketConnect(::strerror(errno));
+		m_state = kConnecting;
+	}
+	else {
+		m_state = kConnected;
+		runWriteJob();
+	}
+	onJobChanged();
+}
+
+void					CUnixTCPSocket::listen(
+								const CString& hostname, UInt16 port)
+{
+	assert(m_fd != -1);
+	assert(m_state == kNone);
+	assert(port != 0);
+
+	// construct address
+	struct sockaddr_in addr;
+	if (!hostname.empty()) {
+		// hostname to address
+		struct hostent* hent = ::gethostbyname(hostname.c_str());
+		if (hent == NULL)
+			throw XSocketName(::hstrerror(h_errno));
+
+		// fill in address
+		assert(hent->h_addrtype == AF_INET);
+		assert(hent->h_length   == sizeof(addr.sin_addr));
+		::memcpy(&addr.sin_addr, hent->h_addr_list[0], hent->h_length);
+	}
+	else {
+		// all addresses
+		addr.sin_addr.s_addr = htonl(INADDR_ANY);
+	}
+	addr.sin_family = AF_INET;
+	addr.sin_port   = htons(port);
+
+	// bind to address
+	if (::bind(m_fd, reinterpret_cast<struct sockaddr*>(&addr),
+											sizeof(addr)) == -1)
+		throw XSocketListen(::strerror(errno));
+
+	// start listening
+	if (::listen(m_fd, 3) == -1)
+		throw XSocketListen(::strerror(errno));
+	m_state = kListening;
+	onJobChanged();
+}
+
+ISocket*				CUnixTCPSocket::accept()
+{
+	assert(m_fd != -1);
+	assert(m_state == kListening);
+
+	for (;;) {
+		// wait for connection
+		fd_set fdset;
+		FD_ZERO(&fdset);
+		FD_SET(m_fd, &fdset);
+		::select(m_fd + 1, &fdset, NULL, NULL, NULL);
+
+		// accept connection
+		struct sockaddr addr;
+		socklen_t addrlen = sizeof(addr);
+		int fd = ::accept(m_fd, &addr, &addrlen);
+		if (fd == -1)
+			if (errno == EAGAIN)
+				continue;
+			else
+				throw XSocketAccept(::strerror(errno));
+
+		// return new socket object
+		return new CUnixTCPSocket(fd);
+	}
+}
+
+SInt32					CUnixTCPSocket::read(void* buffer, SInt32 numBytes)
+{
+	assert(m_fd != -1);
+	assert(m_state == kConnected);
+
+	const ssize_t n = ::read(m_fd, buffer, numBytes);
+	if (n == -1) {
+		// check for no data to read
+		if (errno == EAGAIN || errno == EINTR)
+			return 0;
+
+		// error
+		return -1;
+	}
+
+	// check for socket closed
+	if (n == 0)
+		return -1;
+
+	// return num bytes read
+	return n;
+}
+
+void					CUnixTCPSocket::write(
+								const void* buffer, SInt32 numBytes)
+{
+	const char* ptr = static_cast<const char*>(buffer);
+
+	while (numBytes > 0) {
+		// write more data
+		const ssize_t n = ::write(m_fd, ptr, numBytes);
+
+		// check for errors
+		if (n == -1) {
+			// wait if can't write data then try again
+			if (errno == EAGAIN || errno == EINTR) {
+				fd_set fdset;
+				FD_ZERO(&fdset);
+				FD_SET(m_fd, &fdset);
+				::select(m_fd + 1, NULL, &fdset, NULL, NULL);
+				continue;
+			}
+
+			// error
+			throw XSocketWrite(::strerror(errno));
+		}
+
+		// account for written data
+		ptr += n;
+		numBytes -= n;
+	}
+}
diff --git a/CUnixTCPSocket.h b/CUnixTCPSocket.h
new file mode 100644
index 00000000..ac8d0a34
--- /dev/null
+++ b/CUnixTCPSocket.h
@@ -0,0 +1,47 @@
+#ifndef CUNIXTCPSOCKET_H
+#define CUNIXTCPSOCKET_H
+
+#include "CSocket.h"
+#include "CSocketFactory.h"
+
+class CUnixTCPSocket : public CSocket {
+  public:
+	CUnixTCPSocket();
+	virtual ~CUnixTCPSocket();
+
+	// ISocket overrides
+	virtual void		connect(const CString& hostname, UInt16 port);
+	virtual void		listen(const CString& hostname, UInt16 port);
+	virtual ISocket*	accept();
+	virtual SInt32		read(void* buffer, SInt32 numBytes);
+	virtual void		write(const void* buffer, SInt32 numBytes);
+
+  protected:
+	// CSocket overrides
+	virtual void		onJobChanged();
+
+  private:
+	CUnixTCPSocket(int);
+
+	// callbacks for read/write events
+	void				readCB();
+	void				writeCB();
+
+  private:
+	enum EState { kNone, kConnecting, kConnected, kListening };
+	int					m_fd;
+	EState				m_state;
+	bool				m_addedJobs;
+};
+
+class CUnixTCPSocketFactory : public CSocketFactory {
+  public:
+	CUnixTCPSocketFactory() { }
+	virtual ~CUnixTCPSocketFactory() { }
+
+	// CSocketFactory overrides
+	virtual ISocket*	create() const
+								{ return new CUnixTCPSocket; }
+};
+
+#endif
diff --git a/CUnixXScreen.cpp b/CUnixXScreen.cpp
new file mode 100644
index 00000000..752a774d
--- /dev/null
+++ b/CUnixXScreen.cpp
@@ -0,0 +1,34 @@
+#include "CUnixXScreen.h"
+#include "CUnixEventQueue.h"
+#include "TMethodJob.h"
+#include <X11/X.h>
+
+//
+// CUnixXScreen
+//
+
+CUnixXScreen::CUnixXScreen(const CString& name) :
+								CXScreen(name)
+{
+	// do nothing
+}
+
+CUnixXScreen::~CUnixXScreen()
+{
+	// do nothing
+}
+
+void					CUnixXScreen::onOpen(bool)
+{
+	// register our X event	handler
+	CEQ->addFileDesc(ConnectionNumber(getDisplay()),
+								new TMethodJob<CUnixXScreen>(this,
+									&CUnixXScreen::onEvents), NULL);
+
+}
+
+void					CUnixXScreen::onClose()
+{
+	// unregister the X event handler
+	CEQ->removeFileDesc(ConnectionNumber(getDisplay()));
+}
diff --git a/CUnixXScreen.h b/CUnixXScreen.h
new file mode 100644
index 00000000..459eed8c
--- /dev/null
+++ b/CUnixXScreen.h
@@ -0,0 +1,16 @@
+#ifndef CUNIXXSCREEN_H
+#define CUNIXXSCREEN_H
+
+#include "CXScreen.h"
+
+class CUnixXScreen : public CXScreen {
+  public:
+	CUnixXScreen(const CString& name);
+	virtual ~CUnixXScreen();
+
+  protected:
+	virtual void		onOpen(bool isPrimary);
+	virtual void		onClose();
+};
+
+#endif
diff --git a/CXScreen.cpp b/CXScreen.cpp
new file mode 100644
index 00000000..bd683eb4
--- /dev/null
+++ b/CXScreen.cpp
@@ -0,0 +1,571 @@
+#include "CXScreen.h"
+#include "CEvent.h"
+#include "CEventQueue.h"
+#include <assert.h>
+#include <unistd.h>
+#include <X11/X.h>
+#include <X11/extensions/XTest.h>
+
+//
+// CXScreen
+//
+class XClientOpen { }; // FIXME
+
+CXScreen::CXScreen(const CString& name) :
+								m_name(name),
+								m_display(NULL),
+								m_primary(false),
+								m_w(0), m_h(0),
+								m_window(None),
+								m_active(false)
+{
+	// do nothing
+}
+
+CXScreen::~CXScreen()
+{
+	assert(m_display == NULL);
+}
+
+void					CXScreen::open(bool isPrimary)
+{
+	assert(m_display == NULL);
+
+	m_primary = isPrimary;
+
+	bool opened = false;
+	try {
+		// open the display
+		m_display = ::XOpenDisplay(NULL);	// FIXME -- allow non-default
+		if (m_display == NULL)
+			throw XClientOpen();
+
+		// hook up event handling
+		onOpen(m_primary);
+		opened = true;
+
+		// get default screen
+		m_screen = DefaultScreen(m_display);
+		Screen* screen = ScreenOfDisplay(m_display, m_screen);
+
+		// get screen size
+		m_w = WidthOfScreen(screen);
+		m_h = HeightOfScreen(screen);
+
+		// type specific operations
+		if (m_primary)
+			openPrimary();
+		else
+			openSecondary();
+	}
+	catch (...) {
+		if (opened)
+			onClose();
+
+		if (m_display != NULL) {
+			::XCloseDisplay(m_display);
+			m_display = NULL;
+		}
+
+		throw;
+	}
+}
+
+void					CXScreen::close()
+{
+	assert(m_display != NULL);
+
+	// type specific operations
+	if (m_primary)
+		closePrimary();
+	else
+		closeSecondary();
+
+	// unhook event handling
+	onClose();
+
+	// close the display
+	::XCloseDisplay(m_display);
+	m_display = NULL;
+}
+
+void					CXScreen::enterScreen(SInt32 x, SInt32 y)
+{
+	assert(m_display != NULL);
+
+	if (m_primary)
+		enterScreenPrimary(x, y);
+	else
+		enterScreenSecondary(x, y);
+}
+
+void					CXScreen::leaveScreen()
+{
+	assert(m_display != NULL);
+
+	if (m_primary)
+		leaveScreenPrimary();
+	else
+		leaveScreenSecondary();
+}
+
+void					CXScreen::warpCursor(SInt32 x, SInt32 y)
+{
+	assert(m_display != NULL);
+
+	// warp the mouse
+	Window root = RootWindow(m_display, m_screen);
+	::XWarpPointer(m_display, None, root, 0, 0, 0, 0, x, y);
+	::XSync(m_display, False);
+
+	// discard mouse events since we just added one we don't want
+	XEvent xevent;
+	while (::XCheckWindowEvent(m_display, m_window,
+								PointerMotionMask, &xevent))
+		; // do nothing
+}
+
+void					CXScreen::setClipboard(
+								const IClipboard* clipboard)
+{
+	assert(m_display != NULL);
+
+	if (m_primary)
+		setClipboardPrimary(clipboard);
+	else
+		setClipboardSecondary(clipboard);
+}
+
+void					CXScreen::onScreenSaver(bool show)
+{
+	assert(m_display != NULL);
+
+	if (m_primary)
+		onScreenSaverPrimary(show);
+	else
+		onScreenSaverSecondary(show);
+}
+
+void					CXScreen::onKeyDown(KeyID)
+{
+	assert(m_display != NULL);
+	assert(m_primary == false);
+
+	// FIXME
+}
+
+void					CXScreen::onKeyRepeat(KeyID, SInt32)
+{
+	assert(m_display != NULL);
+	assert(m_primary == false);
+
+	// FIXME
+}
+
+void					CXScreen::onKeyUp(KeyID)
+{
+	assert(m_display != NULL);
+	assert(m_primary == false);
+
+	// FIXME
+}
+
+void					CXScreen::onKeyToggle(KeyToggleMask)
+{
+	assert(m_display != NULL);
+	assert(m_primary == false);
+
+	// FIXME
+}
+
+void					CXScreen::onMouseDown(ButtonID)
+{
+	assert(m_display != NULL);
+	assert(m_primary == false);
+
+	// FIXME
+}
+
+void					CXScreen::onMouseUp(ButtonID)
+{
+	assert(m_display != NULL);
+	assert(m_primary == false);
+
+	// FIXME
+}
+
+void					CXScreen::onMouseMove(SInt32 x, SInt32 y)
+{
+	assert(m_display != NULL);
+	assert(m_primary == false);
+
+	XTestFakeMotionEvent(m_display, m_screen, x, y, CurrentTime);
+}
+
+void					CXScreen::onMouseWheel(SInt32)
+{
+	assert(m_display != NULL);
+	assert(m_primary == false);
+
+	// FIXME
+}
+
+void					CXScreen::onClipboardChanged()
+{
+	assert(m_display != NULL);
+	assert(m_primary == false);
+
+	// FIXME
+}
+
+CString					CXScreen::getName() const
+{
+	return m_name;
+}
+
+void					CXScreen::getSize(
+								SInt32* width, SInt32* height) const
+{
+	assert(m_display != NULL);
+	assert(width != NULL && height != NULL);
+
+	*width  = m_w;
+	*height = m_h;
+}
+
+void					CXScreen::getClipboard(
+								IClipboard* /*clipboard*/) const
+{
+	assert(m_display != NULL);
+
+	// FIXME
+}
+
+void					CXScreen::openPrimary()
+{
+	// get the root window
+	Window root = RootWindow(m_display, m_screen);
+
+	// create the grab window.  this window is used to capture user
+	// input when the user is focussed on another client.  don't let
+	// the window manager mess with it.
+	XSetWindowAttributes attr;
+	attr.event_mask            = PointerMotionMask |// PointerMotionHintMask |
+								ButtonPressMask | ButtonReleaseMask |
+								KeyPressMask | KeyReleaseMask |
+								KeymapStateMask;
+	attr.do_not_propagate_mask = 0;
+	attr.override_redirect     = True;
+	attr.cursor                = None;
+	m_window = ::XCreateWindow(m_display, root, 0, 0, m_w, m_h, 0, 0,
+								InputOnly, CopyFromParent,
+								CWDontPropagate | CWEventMask |
+								CWOverrideRedirect | CWCursor,
+								&attr);
+
+	// start watching for events on other windows
+	selectEvents(root);
+}
+
+void					CXScreen::closePrimary()
+{
+	assert(m_window != None);
+
+	// destroy window
+	::XDestroyWindow(m_display, m_window);
+	m_window = None;
+}
+
+void					CXScreen::enterScreenPrimary(SInt32 x, SInt32 y)
+{
+	assert(m_window != None);
+	assert(m_active == true);
+
+	// warp to requested location
+	::XWarpPointer(m_display, None, m_window, 0, 0, 0, 0, x, y);
+
+	// unmap the grab window.  this also ungrabs the mouse and keyboard.
+	::XUnmapWindow(m_display, m_window);
+
+	// remove all input events for grab window
+	XEvent event;
+	while (::XCheckWindowEvent(m_display, m_window,
+								PointerMotionMask |
+								ButtonPressMask | ButtonReleaseMask |
+								KeyPressMask | KeyReleaseMask |
+								KeymapStateMask,
+								&event))
+		; // do nothing
+
+	// not active anymore
+	m_active = false;
+}
+
+void					CXScreen::leaveScreenPrimary()
+{
+	assert(m_window != None);
+	assert(m_active == false);
+
+	// raise and show the input window
+	::XMapRaised(m_display, m_window);
+
+	// grab the mouse and keyboard.  keep trying until we get them.
+	// if we can't grab one after grabbing the other then ungrab
+	// and wait before retrying.
+	int result;
+	do {
+		// mouse first
+		do {
+			result = ::XGrabPointer(m_display, m_window, True, 0,
+								GrabModeAsync, GrabModeAsync,
+								m_window, None, CurrentTime);
+			assert(result != GrabNotViewable);
+			if (result != GrabSuccess)
+				::sleep(1);
+		} while (result != GrabSuccess);
+
+		// now the keyboard
+		result = ::XGrabKeyboard(m_display, m_window, True,
+								GrabModeAsync, GrabModeAsync, CurrentTime);
+		assert(result != GrabNotViewable);
+		if (result != GrabSuccess) {
+			::XUngrabPointer(m_display, CurrentTime);
+			::sleep(1);
+		}
+	} while (result != GrabSuccess);
+
+	// move the mouse to the center of grab window
+	warpCursor(m_w >> 1, m_h >> 1);
+
+	// local client now active
+	m_active = true;
+}
+
+void					CXScreen::setClipboardPrimary(
+								const IClipboard* /*clipboard*/)
+{
+	// FIXME
+}
+
+void					CXScreen::onScreenSaverPrimary(bool /*show*/)
+{
+	// FIXME
+}
+
+void					CXScreen::openSecondary()
+{
+	// verify the availability of the XTest extension
+	int majorOpcode, firstEvent, firstError;
+	if (!::XQueryExtension(m_display, XTestExtensionName,
+								&majorOpcode, &firstEvent, &firstError))
+		throw XClientOpen();
+
+	// become impervious to server grabs
+	XTestGrabControl(m_display, True);
+}
+
+void					CXScreen::closeSecondary()
+{
+	// no longer impervious to server grabs
+	XTestGrabControl(m_display, False);
+}
+
+void					CXScreen::enterScreenSecondary(
+								SInt32 x, SInt32 y)
+{
+	// FIXME
+}
+
+void					CXScreen::leaveScreenSecondary()
+{
+	// FIXME
+}
+
+void					CXScreen::setClipboardSecondary(
+								const IClipboard* /*clipboard*/)
+{
+	// FIXME
+}
+
+void					CXScreen::onScreenSaverSecondary(bool /*show*/)
+{
+	// FIXME
+}
+
+Display*				CXScreen::getDisplay() const
+{
+	return m_display;
+}
+
+void					CXScreen::onEvents()
+{
+	if (m_primary)
+		onPrimaryEvents();
+	else
+		onSecondaryEvents();
+}
+
+void					CXScreen::selectEvents(Window w) const
+{
+	// we want to track the mouse everywhere on the display.  to achieve
+	// that we select PointerMotionMask on every window.  we also select
+	// SubstructureNotifyMask in order to get CreateNotify events so we
+	// select events on new windows too.
+
+	// we don't want to adjust our grab window
+	if (w == m_window)
+		return;
+
+	// select events of interest
+	::XSelectInput(m_display, w, PointerMotionMask | SubstructureNotifyMask);
+
+	// recurse on child windows
+	Window rw, pw, *cw;
+	unsigned int nc;
+	if (::XQueryTree(m_display, w, &rw, &pw, &cw, &nc)) {
+		for (unsigned int i = 0; i < nc; ++i)
+			selectEvents(cw[i]);
+		::XFree(cw);
+	}
+}
+
+KeyID					CXScreen::mapKey(unsigned int keycode) const
+{
+	return keycode;
+}
+
+ButtonID				CXScreen::mapButton(unsigned int button) const
+{
+	switch (button) {
+	  case 1:
+		return kButtonLeft;
+
+	  case 2:
+		return kButtonMiddle;
+
+	  case 3:
+		return kButtonRight;
+	}
+
+	return kButtonNone;
+}
+
+void					CXScreen::onPrimaryEvents()
+{
+	while (XPending(m_display) > 0) {
+		XEvent xevent;
+		XNextEvent(m_display, &xevent);
+
+		switch (xevent.type) {
+		  case KeyPress: {
+			const KeyID key = mapKey(xevent.xkey.keycode);
+			if (key != kKeyNone) {
+				CEvent event;
+				event.m_key.m_type  = CEventBase::kKeyDown;
+				event.m_key.m_key   = key;
+				event.m_key.m_count = 0;
+				CEQ->push(&event);
+			}
+			break;
+		  }
+
+		  // FIXME -- simulate key repeat.  X sends press/release for
+		  // repeat.  must detect auto repeat and use kKeyRepeat.
+		  case KeyRelease: {
+			const KeyID key = mapKey(xevent.xkey.keycode);
+			if (key != kKeyNone) {
+				CEvent event;
+				event.m_key.m_type  = CEventBase::kKeyUp;
+				event.m_key.m_key   = key;
+				event.m_key.m_count = 0;
+				CEQ->push(&event);
+			}
+			break;
+		  }
+
+		  case ButtonPress: {
+			const ButtonID button = mapButton(xevent.xbutton.button);
+			if (button != kButtonNone) {
+				CEvent event;
+				event.m_mouse.m_type   = CEventBase::kMouseDown;
+				event.m_mouse.m_button = button;
+				event.m_mouse.m_x      = 0;
+				event.m_mouse.m_y      = 0;
+				CEQ->push(&event);
+			}
+			break;
+		  }
+
+		  case ButtonRelease: {
+			const ButtonID button = mapButton(xevent.xbutton.button);
+			if (button != kButtonNone) {
+				CEvent event;
+				event.m_mouse.m_type   = CEventBase::kMouseUp;
+				event.m_mouse.m_button = button;
+				event.m_mouse.m_x      = 0;
+				event.m_mouse.m_y      = 0;
+				CEQ->push(&event);
+			}
+			break;
+		  }
+
+		  case MotionNotify: {
+			CEvent event;
+			event.m_mouse.m_type   = CEventBase::kMouseMove;
+			event.m_mouse.m_button = kButtonNone;
+			if (!m_active) {
+				event.m_mouse.m_x = xevent.xmotion.x_root;
+				event.m_mouse.m_y = xevent.xmotion.y_root;
+			}
+			else {
+				// FIXME -- slurp up all remaining motion events?
+				// probably not since key strokes may go to wrong place.
+
+				// get mouse deltas
+				Window root, window;
+				int xRoot, yRoot, xWindow, yWindow;
+				unsigned int mask;
+				if (!::XQueryPointer(m_display, m_window, &root, &window,
+								&xRoot, &yRoot, &xWindow, &yWindow, &mask))
+					break;
+				event.m_mouse.m_x = xRoot - (m_w >> 1);
+				event.m_mouse.m_y = yRoot - (m_h >> 1);
+
+				// warp mouse back to center
+				warpCursor(m_w >> 1, m_h >> 1);
+			}
+			CEQ->push(&event);
+			break;
+		  }
+
+		  case CreateNotify:
+			// select events on new window
+			if (m_primary)
+				selectEvents(xevent.xcreatewindow.window);
+			break;
+
+/*
+		  case SelectionClear:
+			target->XXX(xevent.xselectionclear.);
+			break;
+
+		  case SelectionNotify:
+			target->XXX(xevent.xselection.);
+			break;
+
+		  case SelectionRequest:
+			target->XXX(xevent.xselectionrequest.);
+			break;
+*/
+		}
+	}
+}
+
+void					CXScreen::onSecondaryEvents()
+{
+	while (XPending(m_display) > 0) {
+		XEvent xevent;
+		XNextEvent(m_display, &xevent);
+		// FIXME
+	}
+}
diff --git a/CXScreen.h b/CXScreen.h
new file mode 100644
index 00000000..c2fb9b83
--- /dev/null
+++ b/CXScreen.h
@@ -0,0 +1,81 @@
+#ifndef CXSCREEN_H
+#define CXSCREEN_H
+
+#include "IScreen.h"
+#include <X11/Xlib.h>
+
+class CXScreen : public IScreen {
+  public:
+	CXScreen(const CString& name);
+	virtual ~CXScreen();
+
+	// IScreen overrides
+	virtual void		open(bool isPrimary);
+	virtual void		close();
+	virtual void		enterScreen(SInt32 x, SInt32 y);
+	virtual void		leaveScreen();
+	virtual void		warpCursor(SInt32 x, SInt32 y);
+	virtual void		setClipboard(const IClipboard*);
+	virtual void		onScreenSaver(bool);
+	virtual void		onKeyDown(KeyID);
+	virtual void		onKeyRepeat(KeyID, SInt32);
+	virtual void		onKeyUp(KeyID);
+	virtual void		onKeyToggle(KeyToggleMask);
+	virtual void		onMouseDown(ButtonID);
+	virtual void		onMouseUp(ButtonID);
+	virtual void		onMouseMove(SInt32, SInt32);
+	virtual void		onMouseWheel(SInt32);
+	virtual void		onClipboardChanged();
+	virtual CString		getName() const;
+	virtual void		getSize(SInt32* width, SInt32* height) const;
+	virtual void		getClipboard(IClipboard*) const;
+
+  protected:
+	// primary screen implementations
+	virtual void		openPrimary();
+	virtual void		closePrimary();
+	virtual void		enterScreenPrimary(SInt32 x, SInt32 y);
+	virtual void		leaveScreenPrimary();
+	virtual void		setClipboardPrimary(const IClipboard*);
+	virtual void		onScreenSaverPrimary(bool);
+
+	// secondary screen implementations
+	virtual void		openSecondary();
+	virtual void		closeSecondary();
+	virtual void		enterScreenSecondary(SInt32 x, SInt32 y);
+	virtual void		leaveScreenSecondary();
+	virtual void		setClipboardSecondary(const IClipboard*);
+	virtual void		onScreenSaverSecondary(bool);
+
+	// get the display
+	Display*			getDisplay() const;
+
+	// process X events from the display
+	void				onEvents();
+
+	// called by open() and close().  override to hook up and unhook the
+	// display connection to the event queue.  call onEvents() when events
+	// are available.
+	virtual void		onOpen(bool isPrimary) = 0;
+	virtual void		onClose() = 0;
+
+  private:
+	void				selectEvents(Window) const;
+	KeyID				mapKey(unsigned int keycode) const;
+	ButtonID			mapButton(unsigned int button) const;
+	void				onPrimaryEvents();
+	void				onSecondaryEvents();
+
+  private:
+	CString				m_name;
+	Display*			m_display;
+	int					m_screen;
+	bool				m_primary;
+	SInt32				m_w, m_h;
+
+	// stuff for primary screens
+	Window				m_window;
+	bool				m_active;
+};
+
+#endif
diff --git a/IClient.h b/IClient.h
new file mode 100644
index 00000000..02dd7129
--- /dev/null
+++ b/IClient.h
@@ -0,0 +1,17 @@
+#ifndef ICLIENT_H
+#define ICLIENT_H
+
+class CString;
+
+class IClient {
+  public:
+	IClient() { }
+	virtual ~IClient() { }
+
+	// manipulators
+
+	// connect to server and begin processing events
+	virtual void		run(const CString& hostname) = 0;
+};
+
+#endif
diff --git a/IClipboard.h b/IClipboard.h
new file mode 100644
index 00000000..e69de29b
diff --git a/IEventQueue.h b/IEventQueue.h
new file mode 100644
index 00000000..36609b0c
--- /dev/null
+++ b/IEventQueue.h
@@ -0,0 +1,45 @@
+#ifndef IEVENTQUEUE_H
+#define IEVENTQUEUE_H
+
+#define CEQ (IEventQueue::getInstance())
+
+class CEvent;
+
+class IEventQueue {
+  public:
+	IEventQueue();
+	virtual ~IEventQueue();
+
+	// note -- all of the methods in an IEventQueue subclass for a
+	// platform must be thread safe if it will be used by multiple
+	// threads simultaneously on that platform.
+
+	// manipulators
+
+	// wait up to timeout seconds for the queue to become not empty.
+	// as a side effect this can do the insertion of events.  if
+	// timeout < 0.0 then wait indefinitely.  it's possible for
+	// wait() to return prematurely so always call isEmpty() to
+	// see if there are any events.
+	virtual void		wait(double timeout) = 0;
+
+	// reads and removes the next event on the queue.  waits indefinitely
+	// for an event if the queue is empty.
+	virtual void		pop(CEvent*) = 0;
+
+	// push an event onto the queue
+	virtual void		push(const CEvent*) = 0;
+
+	// returns true if the queue is empty and wait() would block
+	virtual bool		isEmpty() = 0;
+
+	// accessors
+
+	// get the singleton event queue
+	static IEventQueue*	getInstance();
+
+  private:
+	static IEventQueue*	s_instance;
+};
+
+#endif
diff --git a/IJob.h b/IJob.h
new file mode 100644
index 00000000..381e29ad
--- /dev/null
+++ b/IJob.h
@@ -0,0 +1,14 @@
+#ifndef IJOB_H
+#define IJOB_H
+
+class IJob {
+  public:
+	IJob() { }
+	virtual ~IJob() { }
+
+	// manipulators
+
+	virtual void		run() = 0;
+};
+
+#endif
diff --git a/IScreen.h b/IScreen.h
new file mode 100644
index 00000000..a8427584
--- /dev/null
+++ b/IScreen.h
@@ -0,0 +1,132 @@
+#ifndef ISCREEN_H
+#define ISCREEN_H
+
+/*
+ * IScreen -- interface for display screens
+ *
+ * a screen encapsulates input and output devices, typically a mouse
+ * and keyboard for input and a graphical display for output.  one
+ * screen is designated as the primary screen.  only input from the
+ * primary screen's input devices is used.  other screens are secondary
+ * screens and they simulate input from their input devices but ignore
+ * any actual input.  a screen can be either a primary or a secondary
+ * but not both at the same time.  most methods behave differently
+ * depending on the screen type.
+ */
+
+#include "BasicTypes.h"
+#include "KeyTypes.h"
+#include "MouseTypes.h"
+#include "CString.h"
+
+class IClipboard;
+
+class IScreen {
+  public:
+	IScreen() { }
+	virtual ~IScreen() { }
+
+	// manipulators
+
+	// open/close screen.  these are where the client should do
+	// initialization and cleanup of the system's screen.  if isPrimary
+	// is true then this screen will be used (exclusively) as the
+	// primary screen, otherwise it will be used (exclusively) as a
+	// secondary screen.
+	//
+	// primary:
+	//   open():  open the screen and begin reporting input events to
+	// the event queue.  input events should be reported no matter
+	// where on the screen they occur but the screen should not
+	// interfere with the normal dispatching of events.  the screen
+	// should detect when the screen saver is activated.  if it can't
+	// do that it should disable the screen saver and start it itself
+	// after the appropriate duration of no input.
+	//
+	// secondary:
+	//   open():  open the screen, hide the cursor and disable the
+	// screen saver.  then wait for an enterScreen() or close(),
+	// reporting the following events: FIXME.
+	virtual void		open(bool isPrimary) = 0;
+	virtual void		close() = 0;
+
+	// enter/leave screen
+	//
+	// primary:
+	//   enterScreen():  the user has navigated back to the primary
+	// screen.  warp the cursor to the given coordinates, unhide the
+	// cursor and ungrab the input devices.  the screen must also
+	// detect and report (enqueue) input events.  for the primary
+	// screen, enterScreen() is only called after a leaveScreen().
+	//   leaveScreen():  the user has navigated off the primary screen.
+	// hide the cursor and grab exclusive access to the input devices.
+	// input events must be reported.
+	//
+	// secondary:
+	//   enterScreen():  the user has navigated to this secondary
+	// screen.  warp the cursor to the given coordinates and show it.
+	// prepare to simulate input events.
+	//   leaveScreen():  the user has navigated off this secondary
+	// screen.  clean up input event simulation.  hide the cursor.
+	virtual void		enterScreen(SInt32 xAbsolute, SInt32 yAbsolute) = 0;
+	virtual void		leaveScreen() = 0;
+
+	// warp the cursor to the given position
+	virtual void		warpCursor(SInt32 xAbsolute, SInt32 yAbsolute) = 0;
+
+	//
+	// clipboard operations
+	//
+
+	// set the screen's clipboard contents.  this is usually called
+	// soon after an enterScreen().
+	virtual void		setClipboard(const IClipboard*) = 0;
+
+	//
+	// screen saver operations
+	//
+
+	// show or hide the screen saver
+	virtual void		onScreenSaver(bool show) = 0;
+
+	//
+	// input simulation
+	//
+	// these methods must simulate the appropriate input event.
+	// these methods may only called on secondary screens.
+	//
+
+	// keyboard input
+	// onKeyToggle() sets the keyboard toggle key states (e.g. num lock).
+	virtual void		onKeyDown(KeyID) = 0;
+	virtual void		onKeyRepeat(KeyID, SInt32 count) = 0;
+	virtual void		onKeyUp(KeyID) = 0;
+	virtual void		onKeyToggle(KeyToggleMask) = 0;
+
+	// mouse input
+	virtual void		onMouseDown(ButtonID) = 0;
+	virtual void		onMouseUp(ButtonID) = 0;
+	virtual void		onMouseMove(SInt32 xAbsolute, SInt32 yAbsolute) = 0;
+	virtual void		onMouseWheel(SInt32 delta) = 0;
+
+	// clipboard input
+	// FIXME -- do we need this?
+	virtual void		onClipboardChanged() = 0;
+
+	// accessors
+
+	// get the screen's name.  all screens must have a name unique on
+	// the server they connect to.  the hostname is usually an
+	// appropriate name.
+	virtual CString		getName() const = 0;
+
+	// get the size of the screen
+	virtual void		getSize(SInt32* width, SInt32* height) const = 0;
+
+	// clipboard operations
+
+	// get the screen's clipboard contents
+	virtual void		getClipboard(IClipboard*) const = 0;
+};
+
+#endif
diff --git a/IServer.h b/IServer.h
new file mode 100644
index 00000000..2a041d64
--- /dev/null
+++ b/IServer.h
@@ -0,0 +1,29 @@
+#ifndef ISERVER_H
+#define ISERVER_H
+
+class IScreen;
+
+class IServer {
+  public:
+	IServer() { }
+	virtual ~IServer() { }
+
+	// manipulators
+
+	// run the server until terminated
+	virtual void		run() = 0;
+
+	// clipboard operations
+	virtual void		onClipboardChanged(IScreen*) = 0;
+
+	// enter the given screen, leaving the previous screen.  the cursor
+	// should be warped to the center of the screen.
+	virtual void		setActiveScreen(IScreen*) = 0;
+
+	// accessors
+
+	// get the screen that was last entered
+	virtual IScreen*	getActiveScreen() const = 0;
+};
+
+#endif
diff --git a/ISocket.h b/ISocket.h
new file mode 100644
index 00000000..1a46f5d9
--- /dev/null
+++ b/ISocket.h
@@ -0,0 +1,45 @@
+#ifndef ISOCKET_H
+#define ISOCKET_H
+
+#include "BasicTypes.h"
+
+class IJob;
+class CString;
+
+class ISocket {
+  public:
+	// d'tor closes the socket
+	ISocket() { }
+	virtual ~ISocket() { }
+
+	// manipulators
+
+	// set the job to invoke when the socket is readable or writable.
+	// a socket that has connected after a call to connect() becomes
+	// writable.  a socket that is ready to accept a connection after
+	// a call to listen() becomes readable.  the socket returned by
+	// accept() does not have any jobs assigned to it.
+	virtual void		setReadJob(IJob* adoptedJob) = 0;
+	virtual void		setWriteJob(IJob* adoptedJob) = 0;
+
+	// open/close.  connect() begins connecting to the given host but
+	// doesn't wait for the connection to complete.  listen() begins
+	// listening on the given interface and port;  if hostname is
+	// empty then listen on all interfaces.  accept() waits for a
+	// connection on the listening interface and returns a new
+	// socket for the connection.
+	virtual void		connect(const CString& hostname, UInt16 port) = 0;
+	virtual void		listen(const CString& hostname, UInt16 port) = 0;
+	virtual ISocket*	accept() = 0;
+
+	// read data from socket.  returns without waiting if not enough
+	// data is available.  returns the number of bytes actually read,
+	// which is zero if there were no bytes to read and -1 if the
+	// remote end of the socket has disconnected.
+	virtual SInt32		read(void* buffer, SInt32 numBytes) = 0;
+
+	// write data to socket.  waits until all data has been written.
+	virtual void		write(const void* buffer, SInt32 numBytes) = 0;
+};
+
+#endif
diff --git a/KeyTypes.h b/KeyTypes.h
new file mode 100644
index 00000000..c220a9fc
--- /dev/null
+++ b/KeyTypes.h
@@ -0,0 +1,18 @@
+#ifndef KEYTYPES_H
+#define KEYTYPES_H
+
+// type to hold a key identifier
+typedef UInt32			KeyID;
+
+// type to hold bitmask of keys that have toggle states
+typedef UInt16			KeyToggleMask;
+
+// toggle key bitmasks
+static const UInt32		KeyToggleShiftLock  = 0x0001;
+static const UInt32		KeyToggleNumLock    = 0x0002;
+static const UInt32		KeyToggleScrollLock = 0x0004;
+
+// key codes
+static const KeyID		kKeyNone = 0;
+
+#endif
diff --git a/Make-linux b/Make-linux
new file mode 100644
index 00000000..e78e2da4
--- /dev/null
+++ b/Make-linux
@@ -0,0 +1,103 @@
+#
+# empty define, used to terminate lists
+#
+NULL = 
+
+#
+# build tools
+#
+CXX       = /usr/bin/g++
+LD        = /usr/bin/ld
+MKDIR     = /bin/mkdir
+RM        = /bin/rm -f
+RMR       = /bin/rm -rf
+
+#
+# compiler options
+#
+CXXFLAGS  = $(LCXXFLAGS) $(GCXXFLAGS) $(CXXOPTIMIZER) $(MKDEPOPT)
+LCXXFLAGS = $(LCXXDEFS) $(LCXXINCS) $(LCXXOPTS)
+GCXXFLAGS = $(GCXXDEFS) $(GCXXINCS) $(GCXXOPTS)
+
+GCXXDEFS  = -D_POSIX_SOURCE
+GCXXINCS  = -I$(DEPTH)/include -I/usr/include -I/usr/X11R6/include
+GCXXOPTS  = -Wall -W -fexceptions -fno-rtti
+CXXOPTIMIZER = -g
+#CXXOPTIMIZER = -O2 -DNDEBUG
+
+#
+# linker options
+#
+LDFLAGS	  = $(LDOPTS) $(LDLIBS)
+LDOPTS	  = $(LLDOPTS) $(GLDOPTS)
+LDLIBS	  = $(LLDLIBS) $(GLDLIBS)
+
+GLDLIBS	  = -L/usr/X11R6/lib -lX11 -lXext -lXtst
+GLDOPTS	  =
+
+#
+# dependency generation stuff
+#
+MKDEP     = $(DEPTH)/tools/depconv
+MKDEPOPT  = -MD
+MKDEPPRE  =
+MKDEPPOST = $(MKDEP) -f $(MKDEPFILE) $*.d; $(RM) $*.d
+MKDEPFILE = Makedepend
+
+#
+# Convenience file list macros:
+#
+SOURCES   = $(CXXFILES)
+OBJECTS   = $(CXXFILES:.cpp=.o)
+
+#
+# stuff to clean
+#
+DIRT      = $(_FORCE) $(LDIRT) $(GDIRT)
+GDIRT     = *.[eoud] a.out core ar.tmp.* $(MKDEPFILE)
+
+#
+# always unsatisfied target
+#
+_FORCE    = $(COMMONPREF)_force
+
+#
+#
+# common rules
+#
+#
+
+#
+# default target.  makefiles must define a target named `targets'.
+#
+default: targets
+
+#
+# always unsatisfied target
+#
+$(_FORCE):
+
+#
+# cleaners
+#
+COMMONTARGETS = clean clobber
+$(COMMONPREF)clean: $(_FORCE)
+	$(RM) $(DIRT)
+
+$(COMMONPREF)clobber: clean $(_FORCE)
+	$(RM) $(TARGETS)
+
+#
+# implicit target rules
+#
+.SUFFIXES: .cpp .o
+
+.cpp.o:
+	$(MKDEPPRE)
+	$(CXX) $(CXXFLAGS) -c $<
+	$(MKDEPPOST)
+
+#
+# load dependencies
+#
+sinclude $(MKDEPFILE)
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..f8384949
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,38 @@
+DEPTH=.
+include Make-linux
+
+#
+# target files
+#
+TARGETS  = main
+
+#
+# source files
+#
+CXXFILES = 				\
+	XBase.cpp			\
+	CTrace.cpp			\
+	CEventQueue.cpp			\
+	CSocket.cpp			\
+	CMessageSocket.cpp		\
+	CSocketFactory.cpp		\
+	CServer.cpp			\
+	CClient.cpp			\
+	CScreenProxy.cpp		\
+	CXScreen.cpp			\
+	CUnixXScreen.cpp		\
+	CUnixTCPSocket.cpp		\
+	CUnixEventQueue.cpp		\
+	main.cpp			\
+	$(NULL)
+
+#
+# libraries we depend on
+#
+DEPLIBS  = \
+	$(NULL)
+
+targets: $(TARGETS)
+
+main: $(OBJECTS) $(DEPLIBS)
+	$(CXX) $(CXXFLAGS) -o $@ $(OBJECTS) $(LDFLAGS)
diff --git a/MouseTypes.h b/MouseTypes.h
new file mode 100644
index 00000000..ac908486
--- /dev/null
+++ b/MouseTypes.h
@@ -0,0 +1,13 @@
+#ifndef MOUSETYPES_H
+#define MOUSETYPES_H
+
+// type to hold mouse button identifier
+typedef UInt8			ButtonID;
+
+// mouse button identifiers
+static const ButtonID	kButtonNone   = 0;
+static const ButtonID	kButtonLeft   = 1;
+static const ButtonID	kButtonRight  = 2;
+static const ButtonID	kButtonMiddle = 3;
+
+#endif
diff --git a/TMethodJob.h b/TMethodJob.h
new file mode 100644
index 00000000..341e3491
--- /dev/null
+++ b/TMethodJob.h
@@ -0,0 +1,23 @@
+#ifndef TMETHODJOB_H
+#define	TMETHODJOB_H
+
+#include "IJob.h"
+
+template <class T>
+class TMethodJob : public IJob {
+  public:
+	typedef void (T::*Method)();
+
+	TMethodJob(T* object, Method method) :
+								m_object(object), m_method(method) { }
+	virtual ~TMethodJob() { }
+
+	// IJob overrides
+	virtual void		run() { (m_object->*m_method)(); }
+
+  private:
+	T*					m_object;
+	Method				m_method;
+};
+
+#endif
diff --git a/XBase.cpp b/XBase.cpp
new file mode 100644
index 00000000..df1bd475
--- /dev/null
+++ b/XBase.cpp
@@ -0,0 +1,40 @@
+#include "XBase.h"
+
+// win32 wants a const char* argument to std::exception c'tor
+#if CONFIG_PLATFORM_WIN32
+#define STDEXCEPTARG ""
+#endif
+
+// default to no argument
+#ifndef STDEXCEPTARG
+#define STDEXCEPTARG
+#endif
+
+//
+// XBase
+//
+
+XBase::XBase() : exception(STDEXCEPTARG)
+{
+	// do nothing
+}
+
+XBase::~XBase()
+{
+	// do nothing
+}
+
+const char*				XBase::what() const
+{
+	return getType();
+}
+
+const char*				XBase::getType() const
+{
+	return "XBase.h";
+}
+
+CString					XBase::format(const CString& fmt) const
+{
+	return fmt;
+}
diff --git a/XBase.h b/XBase.h
new file mode 100644
index 00000000..f2ed5162
--- /dev/null
+++ b/XBase.h
@@ -0,0 +1,31 @@
+#ifndef XBASE_H
+#define XBASE_H
+
+#include "CString.h"
+#include <exception>
+
+class XBase : public std::exception {
+  public:
+	XBase();
+	virtual ~XBase();
+
+	// accessors
+
+	// return the name of the exception type
+	virtual const char*	getType() const;
+
+	// format and return formatString by replacing positional
+	// arguments (%1, %2, etc.).  default returns formatString
+	// unchanged.  subclasses should document what positional
+	// arguments they replace.
+	virtual CString		format(const CString& formatString) const;
+
+	// std::exception overrides
+	virtual const char*	what() const;
+};
+
+#define XNAME(_n)											\
+  public:													\
+	virtual const char*	getType() const { return #_n; }
+
+#endif
diff --git a/XSocket.h b/XSocket.h
new file mode 100644
index 00000000..e8f775f2
--- /dev/null
+++ b/XSocket.h
@@ -0,0 +1,33 @@
+#ifndef XSOCKET_H
+#define XSOCKET_H
+
+#include "XBase.h"
+
+class XSocket : public XBase {
+  public:
+	// accessors
+
+	const char*			getMessage() const { return m_msg; }
+
+  protected:
+	XSocket(const char* msg) : m_msg(msg) { }
+
+  private:
+	const char*			m_msg;
+};
+
+#define XSOCKETDEF(_n)								\
+class _n : public XSocket {							\
+  public:											\
+	_n(const char* msg) : XSocket(msg) { }			\
+	XNAME(_n)										\
+};
+
+XSOCKETDEF(XSocketCreate)
+XSOCKETDEF(XSocketName)
+XSOCKETDEF(XSocketConnect)
+XSOCKETDEF(XSocketListen)
+XSOCKETDEF(XSocketAccept)
+XSOCKETDEF(XSocketWrite)
+
+#endif
diff --git a/main.cpp b/main.cpp
new file mode 100644
index 00000000..b879e1a1
--- /dev/null
+++ b/main.cpp
@@ -0,0 +1,114 @@
+#include <stdio.h>
+#include <X11/X.h>
+#include <X11/Xlib.h>
+
+#include "CServer.h"
+#include "CClient.h"
+#include "CUnixTCPSocket.h"
+#include "CUnixEventQueue.h"
+#include "CUnixXScreen.h"
+
+/*
+static void selectMotion(Display* dpy, Window w)
+{
+	// select events
+	XSelectInput(dpy, w, PointerMotionMask | SubstructureNotifyMask);
+
+	// recurse on child windows
+	Window rw, pw, *cw;
+	unsigned int nc;
+	if (XQueryTree(dpy, w, &rw, &pw, &cw, &nc)) {
+		for (unsigned int i = 0; i < nc; ++i)
+			selectMotion(dpy, cw[i]);
+		XFree(cw);
+	}
+}
+
+static void trackMouse(Display* dpy)
+{
+	// note -- this doesn't track the mouse when it's grabbed.  that's
+	// okay for synergy because we don't want to cross screens then.
+	selectMotion(dpy, DefaultRootWindow(dpy));
+	while (true) {
+		XEvent event;
+		XNextEvent(dpy, &event);
+		switch (event.type) {
+		  case MotionNotify:
+			fprintf(stderr, "mouse: %d,%d\n", event.xmotion.x_root, event.xmotion.y_root);
+			break;
+
+		  case CreateNotify:
+			selectMotion(dpy, event.xcreatewindow.window);
+			break;
+		}
+	}
+}
+
+static void checkLEDs(Display* dpy)
+{
+	XKeyboardState values;
+	XGetKeyboardControl(dpy, &values);
+
+	fprintf(stderr, "led (%08x): ", (unsigned int)values.led_mask);
+	for (int i = 0; i < 32; ++i)
+		fprintf(stderr, "%c", (values.led_mask & (1 << i)) ? 'O' : '.');
+	fprintf(stderr, "\n");
+
+	XKeyboardControl ctrl;
+	for (int i = 0; i < 32; i += 2) {
+		ctrl.led = i + 1;
+		ctrl.led_mode = LedModeOff;
+		XChangeKeyboardControl(dpy, KBLed | KBLedMode, &ctrl);
+		XSync(dpy, False);
+	}
+}
+*/
+
+int main(int argc, char** argv)
+{
+/*
+	printf("Hello world\n");
+
+	Display* dpy = XOpenDisplay(NULL);
+
+	checkLEDs(dpy);
+	trackMouse(dpy);
+
+	XCloseDisplay(dpy);
+*/
+
+	// install socket factory
+	CSocketFactory::setInstance(new CUnixTCPSocketFactory);
+
+	// create event queue
+	CUnixEventQueue eventQueue;
+
+	if (argc <= 1) {
+		// create server
+		CServer server;
+
+		// create clients
+		CUnixXScreen localScreen("audrey2");
+
+		// register clients
+		server.addLocalScreen(&localScreen);
+		server.addRemoteScreen("remote1");
+
+		// hook up edges
+		server.connectEdge("audrey2", CServer::kLeft, "remote1");
+		server.connectEdge("audrey2", CServer::kTop, "audrey2");
+		server.connectEdge("audrey2", CServer::kBottom, "audrey2");
+		server.connectEdge("remote1", CServer::kLeft, "audrey2");
+
+		// do it
+		server.run();
+	}
+	else {
+		// create client
+		CUnixXScreen screen("remote1");
+		CClient client(&screen);
+		client.run(argv[1]);
+	}
+
+	return 0;
+}
diff --git a/tools/depconv b/tools/depconv
new file mode 100755
index 00000000..7bccd466
--- /dev/null
+++ b/tools/depconv
@@ -0,0 +1,46 @@
+#!/bin/sh
+#
+# add dependency info from files on command line to $depfile
+
+depfile="Makedepends"
+
+dependencies=""
+targets="^$"
+
+# tmp directory
+if test -z "$TMP"; then TMP=/tmp; fi
+
+while [ -n "$*" ]; do
+    case "$1" in
+      -f)
+	depfile=$2
+	shift
+	shift
+	;;
+      -*)
+	echo "usage: $0 [-f <makefile>] <dependency-file> ..."
+	exit 1
+	;;
+      *)
+	break
+    esac
+done
+
+# collect all dependencies
+while [ -n "$*" ]; do
+    line=`cat $1 | sed -e 's/\\\\//g' | sed -e 's/ \/[^ ]*//g'`
+    target=`echo $line | sed -e 's/^\([^:]*\):.*/\1/'`
+    targets="$targets|^$target:"
+    dependencies="$dependencies$line\n"
+    shift
+done
+
+# add new dependencies to $depfile
+if [ -n "$targets" ]; then
+    if [ -r $depfile ]; then
+	(egrep -v $targets $depfile; echo -e -n $dependencies) > $TMP/dep$$
+	if [ $? -eq 0 ]; then mv $TMP/dep$$ $depfile; fi
+    else
+	echo -e -n $dependencies > $depfile
+    fi
+fi