shithub: drawterm

Download patch

ref: 9620904ebb8cf2eea22a1a81cb9bb5274dda710d
parent: a130d441722ac3f759d2d83b98eb6aef7e84f97e
author: Lorenzo Bivens <git@lorenzobivens.info>
date: Fri Aug 13 23:39:50 EDT 2021

Merging echoline's android and fbdev forks

--- /dev/null
+++ b/Make.android
@@ -1,0 +1,38 @@
+# Android
+SDKPREFIX=$(HOME)/Android/Sdk
+JAVA_HOME=/usr
+OBJS=lib/arm64-v8a/libdrawterm.so lib/armeabi-v7a/libdrawterm.so lib/x86/libdrawterm.so lib/x86_64/libdrawterm.so
+
+all: drawterm.apk
+
+clean:
+	rm -f *.apk lib/*/*.so
+
+lib/arm64-v8a/libdrawterm.so:
+	CONF=android-arm64 make -j5;
+	CONF=android-arm64 make clean;
+
+lib/armeabi-v7a/libdrawterm.so:
+	CONF=android-arm make -j5;
+	CONF=android-arm make clean;
+
+lib/x86/libdrawterm.so:
+	CONF=android-386 make -j5;
+	CONF=android-386 make clean;
+
+lib/x86_64/libdrawterm.so:
+	CONF=android-amd64 make -j5;
+	CONF=android-amd64 make clean;
+
+drawterm.apk: drawterm-signed.apk
+	$(SDKPREFIX)/build-tools/30.0.3/zipalign -v -f 4 $< $@
+
+drawterm-signed.apk: drawterm-unsigned.apk drawterm.keystore
+	$(JAVA_HOME)/bin/jarsigner -verbose -keystore ./drawterm.keystore -storepass glendarocks -keypass glendarocks -signedjar $@ $< drawtermKey
+
+drawterm-unsigned.apk: $(OBJS)
+	$(SDKPREFIX)/build-tools/30.0.3/aapt package -v -f -M gui-android/AndroidManifest.xml -S gui-android/res -I $(SDKPREFIX)/platforms/android-21/android.jar -F $@ gui-android/bin
+	$(SDKPREFIX)/build-tools/30.0.3/aapt add $@ $(OBJS)
+
+drawterm.keystore:
+	$(JAVA_HOME)/bin/keytool -genkeypair -validity 1000 -dname "CN=9front,O=Android,C=US" -keystore $@ -storepass glendarocks -keypass glendarocks -alias drawtermKey -keyalg RSA -v
--- /dev/null
+++ b/Make.android-386
@@ -1,0 +1,26 @@
+# Android
+SDKPREFIX=$(HOME)/Android/Sdk
+NDKPREFIX=$(SDKPREFIX)/ndk/21.1.6352462/toolchains/llvm/prebuilt/linux-x86_64/bin
+JAVA_HOME=/usr
+
+PTHREAD=-pthread
+AR=$(NDKPREFIX)/i686-linux-android-ar
+AS=$(NDKPREFIX)/i686-linux-android-as
+RANLIB=$(NDKPREFIX)/i686-linux-android-ranlib
+STRIP=$(NDKPREFIX)/i686-linux-android-strip
+CC=$(NDKPREFIX)/i686-linux-android21-clang
+CFLAGS=-Wall -Wno-missing-braces -ggdb -I$(ROOT) -I$(ROOT)/include -I$(ROOT)/kern -c -Dmain=dt_main -fPIC
+O=o
+OS=posix
+GUI=android
+LDADD=-ggdb -lm -shared -llog -landroid
+LDFLAGS=$(PTHREAD)
+TARG=lib/x86/libdrawterm.so
+AUDIO=none
+
+all: default
+
+libmachdep.a:
+	arch=386; \
+	(cd posix-$$arch &&  make)
+
--- /dev/null
+++ b/Make.android-amd64
@@ -1,0 +1,26 @@
+# Android
+SDKPREFIX=$(HOME)/Android/Sdk
+NDKPREFIX=$(SDKPREFIX)/ndk/21.1.6352462/toolchains/llvm/prebuilt/linux-x86_64/bin
+JAVA_HOME=/usr
+
+PTHREAD=-pthread
+AR=$(NDKPREFIX)/x86_64-linux-android-ar
+AS=$(NDKPREFIX)/x86_64-linux-android-as
+RANLIB=$(NDKPREFIX)/x86_64-linux-android-ranlib
+STRIP=$(NDKPREFIX)/x86_64-linux-android-strip
+CC=$(NDKPREFIX)/x86_64-linux-android21-clang
+CFLAGS=-Wall -Wno-missing-braces -ggdb -I$(ROOT) -I$(ROOT)/include -I$(ROOT)/kern -c -Dmain=dt_main -fPIC
+O=o
+OS=posix
+GUI=android
+LDADD=-ggdb -lm -shared -llog -landroid
+LDFLAGS=$(PTHREAD)
+TARG=lib/x86_64/libdrawterm.so
+AUDIO=none
+
+all: default
+
+libmachdep.a:
+	arch=amd64; \
+	(cd posix-$$arch &&  make)
+
--- /dev/null
+++ b/Make.android-arm
@@ -1,0 +1,26 @@
+# Android
+SDKPREFIX=$(HOME)/Android/Sdk
+NDKPREFIX=$(SDKPREFIX)/ndk/21.1.6352462/toolchains/llvm/prebuilt/linux-x86_64/bin
+JAVA_HOME=/usr
+
+PTHREAD=-pthread
+AR=$(NDKPREFIX)/arm-linux-androideabi-ar
+AS=$(NDKPREFIX)/arm-linux-androideabi-as
+RANLIB=$(NDKPREFIX)/arm-linux-androideabi-ranlib
+STRIP=$(NDKPREFIX)/arm-linux-androideabi-strip
+CC=$(NDKPREFIX)/armv7a-linux-androideabi21-clang
+CFLAGS=-Wall -Wno-missing-braces -ggdb -I$(ROOT) -I$(ROOT)/include -I$(ROOT)/kern -c -Dmain=dt_main -fPIC
+O=o
+OS=posix
+GUI=android
+LDADD=-ggdb -lm -shared -llog -landroid
+LDFLAGS=$(PTHREAD)
+TARG=lib/armeabi-v7a/libdrawterm.so
+AUDIO=none
+
+all: default
+
+libmachdep.a:
+	arch=arm; \
+	(cd posix-$$arch &&  make)
+
--- /dev/null
+++ b/Make.android-arm64
@@ -1,0 +1,26 @@
+# Android
+SDKPREFIX=$(HOME)/Android/Sdk
+NDKPREFIX=$(SDKPREFIX)/ndk/21.1.6352462/toolchains/llvm/prebuilt/linux-x86_64/bin
+JAVA_HOME=/usr
+
+PTHREAD=-pthread
+AR=$(NDKPREFIX)/aarch64-linux-android-ar
+AS=$(NDKPREFIX)/aarch64-linux-android-as
+RANLIB=$(NDKPREFIX)/aarch64-linux-android-ranlib
+STRIP=$(NDKPREFIX)/aarch64-linux-android-strip
+CC=$(NDKPREFIX)/aarch64-linux-android21-clang
+CFLAGS=-Wall -Wno-missing-braces -ggdb -I$(ROOT) -I$(ROOT)/include -I$(ROOT)/kern -c -Dmain=dt_main -fPIC
+O=o
+OS=posix
+GUI=android
+LDADD=-ggdb -lm -shared -llog -landroid
+LDFLAGS=$(PTHREAD)
+TARG=lib/arm64-v8a/libdrawterm.so
+AUDIO=none
+
+all: default
+
+libmachdep.a:
+	arch=arm64; \
+	(cd posix-$$arch &&  make)
+
--- /dev/null
+++ b/Make.fbdev
@@ -1,0 +1,22 @@
+# Unix
+#PTHREAD=	# for Mac
+PTHREAD=-pthread
+AR=ar
+AS=as
+RANLIB=ranlib
+CC=gcc
+CFLAGS=-Wall -Wno-missing-braces -ggdb -I$(ROOT) -I$(ROOT)/include -I$(ROOT)/kern -c -D_THREAD_SAFE $(PTHREAD) -O2
+O=o
+OS=posix
+GUI=fbdev
+LDADD=-ggdb -lm -lasound
+LDFLAGS=$(PTHREAD)
+TARG=drawterm
+# AUDIO=none
+AUDIO=alsa
+
+all: default
+
+libmachdep.a:
+	arch=`uname -m|sed 's/i.86/386/;s/Power Macintosh/power/; s/x86_64/amd64/; s/armv[567].*/arm/; s/aarch64/arm64/'`; \
+	(cd posix-$$arch &&  make)
--- a/README
+++ b/README
@@ -18,6 +18,17 @@
 
 To build on Mac OS X with Cocoa, run CONF=osx-cocoa make and "cp drawterm gui-cocoa/drawterm.app/".
 
+To build for Android, make sure Make.android* and gui-android/Makefile are correct for your build and target systems, then run make -f Make.android
+
+USAGE
+-------
+On Android the five checkboxes at the top represent the three mouse buttons and mousewheel, determining which "buttons" are clicked. The "kb" button toggles the soft keyboard.
+
+
+CAVEATS
+--------
+Be aware that right now on Android the login details are saved as a plaintext string if saved, and there is no secstore support.
+
 
 BINARIES
 ---------
--- /dev/null
+++ b/gui-android/AndroidManifest.xml
@@ -1,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="org.echoline.drawterm">
+
+    <supports-screens android:largeScreens="true"
+        android:normalScreens="true" android:smallScreens="true"
+        android:anyDensity="true" />
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+        <activity
+            android:name=".MainActivity"
+            android:label="@string/app_name"
+            android:windowSoftInputMode="stateUnchanged|adjustNothing">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <!--<uses-permission android:name="android.permission.SET_DEBUG_APP"/>-->
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.CAMERA"/>
+</manifest>
--- /dev/null
+++ b/gui-android/Makefile
@@ -1,0 +1,23 @@
+ROOT=..
+include ../Make.config
+LIB=libgui.a
+
+OFILES=\
+	cpp/android.$O\
+	cpp/native-lib.$O\
+	cpp/devandroid.$O\
+
+default: $(LIB) gen/org/echoline/drawterm/R.java bin/classes.dex
+$(LIB): $(OFILES)
+	$(AR) r $(LIB) $(OFILES)
+	$(RANLIB) $(LIB)
+
+gen/org/echoline/drawterm/R.java: $(shell find res/ -type f)
+	$(SDKPREFIX)/build-tools/30.0.3/aapt package -f -m -M AndroidManifest.xml -I $(SDKPREFIX)/platforms/android-21/android.jar -S res/ -J gen
+
+bin/classes.dex: obj/org/echoline/drawterm/MainActivity.class obj/org/echoline/drawterm/DrawTermThread.class obj/org/echoline/drawterm/MySurfaceView.class
+	$(SDKPREFIX)/build-tools/30.0.3/dx --dex --verbose --output=$@ obj/
+
+obj/org/echoline/drawterm/%.class: java/org/echoline/drawterm/%.java
+	$(JAVA_HOME)/bin/javac -d obj/ -classpath $(SDKPREFIX)/platforms/android-21/android.jar -sourcepath java java/org/echoline/drawterm/$*.java gen/org/echoline/drawterm/R.java
+
--- /dev/null
+++ b/gui-android/cpp/android.c
@@ -1,0 +1,230 @@
+#include <jni.h>
+#include <android/native_window.h>
+#include <android/log.h>
+
+#include "u.h"
+#include "lib.h"
+#include "dat.h"
+#include "fns.h"
+
+#include <draw.h>
+#include <memdraw.h>
+#include <keyboard.h>
+#include <cursor.h>
+#include "screen.h"
+
+Memimage *gscreen = nil;
+extern int screenWidth;
+extern int screenHeight;
+extern ANativeWindow *window;
+extern jobject mainActivityObj;
+extern JavaVM *jvm;
+
+char*
+clipread(void)
+{
+	char *ret;
+	const char *s;
+	JNIEnv *env;
+	jint rs = (*jvm)->AttachCurrentThread(jvm, &env, NULL);
+	if (rs != JNI_OK) {
+		__android_log_print(ANDROID_LOG_WARN, "drawterm", "AttachCurrentThread returned error: %d", rs);
+		return strdup("");
+	}
+	jclass clazz = (*env)->GetObjectClass(env, mainActivityObj);
+	jmethodID methodID = (*env)->GetMethodID(env, clazz, "getClipBoard", "()Ljava/lang/String;");
+        jstring str = (jstring)(*env)->CallObjectMethod(env, mainActivityObj, methodID);
+	s = (*env)->GetStringUTFChars(env, str, NULL);
+	ret = strdup(s);
+	(*env)->ReleaseStringUTFChars(env, str, s);
+	(*jvm)->DetachCurrentThread(jvm);
+	return ret;
+}
+
+int
+clipwrite(char *buf)
+{
+	JNIEnv *env;
+	jint rs = (*jvm)->GetEnv(jvm, (void**)&env, JNI_VERSION_1_6);
+	if(rs != JNI_OK) {
+		__android_log_print(ANDROID_LOG_WARN, "drawterm", "GetEnv returned error: %d", rs);
+		return 0;
+	}
+	jclass clazz = (*env)->GetObjectClass(env, mainActivityObj);
+	jmethodID methodID = (*env)->GetMethodID(env, clazz, "setClipBoard", "(Ljava/lang/String;)V");
+        jstring str = (*env)->NewStringUTF(env, buf);
+	(*env)->CallVoidMethod(env, mainActivityObj, methodID, str);
+	return 0;
+}
+
+void
+show_notification(char *buf)
+{
+	JNIEnv *env;
+	jint rs = (*jvm)->AttachCurrentThread(jvm, &env, NULL);
+	if(rs != JNI_OK) {
+		__android_log_print(ANDROID_LOG_WARN, "drawterm", "AttachCurrentThread returned error: %d", rs);
+		return;
+	}
+	jclass clazz = (*env)->GetObjectClass(env, mainActivityObj);
+	jmethodID methodID = (*env)->GetMethodID(env, clazz, "showNotification", "(Ljava/lang/String;)V");
+        jstring str = (*env)->NewStringUTF(env, buf);
+	(*env)->CallVoidMethod(env, mainActivityObj, methodID, str);
+	(*jvm)->DetachCurrentThread(jvm);
+	return;
+}
+
+int
+num_cameras()
+{
+	JNIEnv *env;
+	int n;
+	jint rs = (*jvm)->GetEnv(jvm, (void**)&env, JNI_VERSION_1_6);
+	if(rs != JNI_OK) {
+		__android_log_print(ANDROID_LOG_WARN, "drawterm", "GetEnv returned error: %d", rs);
+		return 0;
+	}
+	jclass clazz = (*env)->GetObjectClass(env, mainActivityObj);
+	jmethodID methodID = (*env)->GetMethodID(env, clazz, "numCameras", "()I");
+	n = (*env)->CallIntMethod(env, mainActivityObj, methodID);
+	return n;
+}
+
+void
+take_picture(int id)
+{
+	JNIEnv *env;
+	jint rs = (*jvm)->AttachCurrentThread(jvm, &env, NULL);
+	if(rs != JNI_OK) {
+		__android_log_print(ANDROID_LOG_WARN, "drawterm", "AttachCurrentThread returned error: %d", rs);
+		return;
+	}
+	jclass clazz = (*env)->GetObjectClass(env, mainActivityObj);
+	jmethodID methodID = (*env)->GetMethodID(env, clazz, "takePicture", "(I)V");
+	(*env)->CallVoidMethod(env, mainActivityObj, methodID, id);
+	(*jvm)->DetachCurrentThread(jvm);
+	return;
+}
+
+void
+setcolor(ulong i, ulong r, ulong g, ulong b)
+{
+	return;
+}
+
+void
+getcolor(ulong v, ulong *r, ulong *g, ulong *b)
+{
+	*r = (v>>16)&0xFF;
+	*g = (v>>8)&0xFF;
+	*b = v&0xFF;
+}
+
+void
+flushmemscreen(Rectangle r)
+{
+	ANativeWindow_Buffer buffer;
+	uint8_t *pixels;
+	int x, y, o, b;
+	ARect bounds;
+
+	if (window == NULL)
+		return;
+
+	memset(&buffer, 0, sizeof(buffer));
+
+	bounds.left = r.min.x;
+	bounds.top = r.min.y;
+	bounds.right = r.max.x;
+	bounds.bottom = r.max.y;
+
+	if (ANativeWindow_lock(window, &buffer, &bounds) != 0) {
+		__android_log_print(ANDROID_LOG_WARN, "drawterm", "Unable to lock window buffer");
+		return;
+	}
+
+	r.min.x = bounds.left;
+	r.min.y = bounds.top;
+	r.max.x = bounds.right;
+	r.max.y = bounds.bottom;
+
+	pixels = (uint8_t*)buffer.bits;
+	for (y = r.min.y; y < r.max.y; y++)
+		for (x = r.min.x; x < r.max.x; x++) {
+			o = (y * screenWidth + x) * 4;
+			b = (y * buffer.stride + x) * 4;
+			pixels[b+3] = 0xFF;
+			pixels[b+2] = gscreen->data->bdata[o+0];
+			pixels[b+1] = gscreen->data->bdata[o+1];
+			pixels[b+0] = gscreen->data->bdata[o+2];
+		}
+
+	if (ANativeWindow_unlockAndPost(window) != 0) {
+		__android_log_print(ANDROID_LOG_WARN, "drawterm", "Unable to unlock and post window buffer");
+	}
+	return;
+}
+
+void
+screeninit(void)
+{
+	Rectangle r = Rect(0,0,screenWidth,screenHeight);
+	memimageinit();
+	screensize(r, XRGB32);
+	if (gscreen == nil)
+		panic("screensize failed");
+	gscreen->clipr = r;
+	terminit();
+	qlock(&drawlock);
+	flushmemscreen(r);
+	qunlock(&drawlock);
+	return;
+}
+
+void
+screensize(Rectangle r, ulong chan)
+{
+	Memimage *mi;
+
+	mi = allocmemimage(r, chan);
+	if (mi == nil)
+		return;
+
+	if (gscreen != nil)
+		freememimage(gscreen);
+
+	gscreen = mi;
+	gscreen->clipr = ZR;
+}
+
+Memdata*
+attachscreen(Rectangle *r, ulong *chan, int *depth, int *width, int *softscreen)
+{
+	*r = gscreen->clipr;
+	*depth = gscreen->depth;
+	*chan = gscreen->chan;
+	*width = gscreen->width;
+	*softscreen = 1;
+
+	gscreen->data->ref++;
+	return gscreen->data;
+}
+
+void
+setcursor(void)
+{
+	return;
+}
+
+void
+mouseset(Point xy)
+{
+	return;
+}
+
+void
+guimain(void)
+{
+	cpubody();
+}
+
--- /dev/null
+++ b/gui-android/cpp/devandroid.c
@@ -1,0 +1,248 @@
+#include	"u.h"
+#include	"lib.h"
+#include	"dat.h"
+#include	"fns.h"
+#include	"error.h"
+
+#include <android/log.h>
+#include <android/sensor.h>
+
+void show_notification(char *buf);
+void take_picture(int id);
+int num_cameras();
+
+int Ncameras = 0;
+
+uchar *cambuf = nil;
+int camlen;
+
+ASensorManager *sensorManager = NULL;
+
+enum
+{
+	Qdir		= 0,
+	Qcam		= 1,
+	Qaccel		= 2,
+	Qcompass	= 4,
+	Qnotification	= 6,
+};
+#define QID(p, c, y) 	(((p)<<16) | ((c)<<4) | (y))
+
+static void androidinit(void);
+
+static void
+androidinit(void)
+{
+	sensorManager = ASensorManager_getInstance();
+
+	Ncameras = num_cameras();
+}
+
+static Chan*
+androidattach(char *param)
+{
+	Chan *c;
+
+	c = devattach('N', param);
+	c->qid.path = QID(0, 0, Qdir);
+	c->qid.type = QTDIR;
+	c->qid.vers = 0;
+
+	return c;
+}
+
+static int
+androidgen(Chan *c, char *n, Dirtab *d, int nd, int s, Dir *dp)
+{
+	Qid q;
+
+	if (s == DEVDOTDOT) {
+		mkqid(&q, Qdir, 0, QTDIR);
+		devdir(c, q, "#N", 0, eve, 0555, dp);
+		return 1;
+	}
+	if (s < Ncameras) {
+		sprintf(up->genbuf, "cam%d.jpg", s);
+		mkqid(&q, (s << 16) | Qcam, 0, QTFILE);
+		devdir(c, q, up->genbuf, 0, eve, 0444, dp);
+		return 1;
+	}
+	if (s == Ncameras) {
+		sprintf(up->genbuf, "accel");
+		mkqid(&q, Qaccel, 0, QTFILE);
+		devdir(c, q, up->genbuf, 0, eve, 0444, dp);
+		return 1;
+	}
+	if (s == (Ncameras+1)) {
+		sprintf(up->genbuf, "compass");
+		mkqid(&q, Qcompass, 0, QTFILE);
+		devdir(c, q, up->genbuf, 0, eve, 0444, dp);
+		return 1;
+	}
+	if (s == (Ncameras+2)) {
+		sprintf(up->genbuf, "notification");
+		mkqid(&q, Qnotification, 0, QTFILE);
+		devdir(c, q, up->genbuf, 0, eve, 0222, dp);
+		return 1;
+	}
+	return -1;
+}
+
+static Walkqid*
+androidwalk(Chan *c, Chan *nc, char **name, int nname)
+{
+	return devwalk(c, nc, name, nname, 0, 0, androidgen);
+}
+
+static int
+androidstat(Chan *c, uchar *db, int n)
+{
+	return devstat(c, db, n, 0, 0, androidgen);
+}
+
+static Chan*
+androidopen(Chan *c, int omode)
+{
+	p9_uvlong s;
+
+	c = devopen(c, omode, 0, 0, androidgen);
+
+	if (c->qid.path & Qcam) {
+		s = c->qid.path >> 16;
+		take_picture(s);
+	}
+	c->mode = openmode(omode);
+	c->flag |= COPEN;
+	c->offset = 0;
+	c->iounit = 8192;
+
+	return c;
+}
+
+static void
+androidclose(Chan *c)
+{
+	if (c->qid.path & Qcam && cambuf != nil) {
+		free(cambuf);
+		cambuf = nil;
+	}
+}
+
+static long
+androidread(Chan *c, void *v, long n, vlong off)
+{
+	char *a = v;
+	long l;
+	const ASensor *sensor;
+	ASensorEventQueue *queue = NULL;
+	ASensorEvent data;
+
+	switch((ulong)c->qid.path & 0xF) {
+		default:
+			error(Eperm);
+			return -1;
+
+		case Qcam:
+			while(cambuf == nil)
+				usleep(10 * 1000);
+
+			l = camlen - off;
+			if (l > n)
+				l = n;
+
+			if (l > 0)
+				memcpy(a, cambuf + off, l);
+
+			return l;
+		case Qaccel:
+			queue = ASensorManager_createEventQueue(sensorManager, ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS), 1, NULL, NULL);
+			if (queue == NULL)
+				return 0;
+			sensor = ASensorManager_getDefaultSensor(sensorManager, ASENSOR_TYPE_ACCELEROMETER);
+			if (sensor == NULL) {
+				ASensorManager_destroyEventQueue(sensorManager, queue);
+				return 0;
+			}
+			if (ASensorEventQueue_enableSensor(queue, sensor)) {
+				ASensorEventQueue_disableSensor(queue, sensor);
+				ASensorManager_destroyEventQueue(sensorManager, queue);
+				return 0;
+			}
+			l = 0;
+			if (ALooper_pollAll(1000, NULL, NULL, NULL) == 1) {
+				if (ASensorEventQueue_getEvents(queue, &data, 1)) {
+					l = snprint(a, n, "%11f %11f %11f\n", data.vector.x, data.vector.y, data.vector.z);
+				}
+			}
+			ASensorEventQueue_disableSensor(queue, sensor);
+			ASensorManager_destroyEventQueue(sensorManager, queue);
+			return l;
+		case Qcompass:
+			queue = ASensorManager_createEventQueue(sensorManager, ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS), 1, NULL, NULL);
+			if (queue == NULL)
+				return 0;
+			sensor = ASensorManager_getDefaultSensor(sensorManager, ASENSOR_TYPE_MAGNETIC_FIELD);
+			if (sensor == NULL) {
+				ASensorManager_destroyEventQueue(sensorManager, queue);
+				return 0;
+			}
+			if (ASensorEventQueue_enableSensor(queue, sensor)) {
+				ASensorEventQueue_disableSensor(queue, sensor);
+				ASensorManager_destroyEventQueue(sensorManager, queue);
+				return 0;
+			}
+			l = 0;
+			if (ALooper_pollAll(1000, NULL, NULL, NULL) == 1) {
+				if (ASensorEventQueue_getEvents(queue, &data, 1)) {
+					l = snprint(a, n, "%11f %11f %11f\n", data.vector.x, data.vector.y, data.vector.z);
+				}
+			}
+			ASensorEventQueue_disableSensor(queue, sensor);
+			ASensorManager_destroyEventQueue(sensorManager, queue);
+			return l;
+		case Qdir:
+			return devdirread(c, a, n, 0, 0, androidgen);
+	}
+}
+
+static long
+androidwrite(Chan *c, void *vp, long n, vlong off)
+{
+	char *a = vp;
+	char *str;
+
+	switch((ulong)c->qid.path) {
+		case Qnotification:
+			str = malloc(n+1);
+			memcpy(str, a, n);
+			str[n] = '\0';
+			show_notification(str);
+			free(str);
+			return n;
+		default:
+			error(Eperm);
+			break;
+	}
+	return -1;
+}
+
+Dev androiddevtab = {
+	'N',
+	"android",
+
+	devreset,
+	androidinit,
+	devshutdown,
+	androidattach,
+	androidwalk,
+	androidstat,
+	androidopen,
+	devcreate,
+	androidclose,
+	androidread,
+	devbread,
+	androidwrite,
+	devbwrite,
+	devremove,
+	devwstat,
+};
--- /dev/null
+++ b/gui-android/cpp/native-lib.c
@@ -1,0 +1,173 @@
+#include <jni.h>
+#include <android/native_window.h>
+#include <android/native_window_jni.h>
+#include <android/log.h>
+#include "u.h"
+#include "lib.h"
+#include "dat.h"
+#include "fns.h"
+#include "error.h"
+#include <draw.h>
+#include <string.h>
+#include <keyboard.h>
+
+void absmousetrack(int, int, int, ulong);
+ulong ticks(void);
+int dt_main(int, char**);
+int screenWidth;
+int screenHeight;
+Point mousept = {0, 0};
+int buttons = 0;
+float ws = 1;
+float hs = 1;
+extern char *snarfbuf;
+int mPaused = 0;
+ANativeWindow *window = NULL;
+jobject mainActivityObj;
+JavaVM *jvm;
+void flushmemscreen(Rectangle r);
+extern uchar *cambuf;
+extern int camlen;
+
+JNIEXPORT void JNICALL
+Java_org_echoline_drawterm_MainActivity_setObject(
+        JNIEnv *env,
+        jobject obj) {
+    mainActivityObj = (*env)->NewGlobalRef(env, obj);
+    jint rs = (*env)->GetJavaVM(env, &jvm);
+    assert(rs == JNI_OK);
+}
+
+JNIEXPORT void JNICALL
+Java_org_echoline_drawterm_MainActivity_keyDown(
+        JNIEnv *env,
+        jobject obj,
+        jint c) {
+    kbdkey(c, 1);
+}
+
+JNIEXPORT void JNICALL
+Java_org_echoline_drawterm_MainActivity_keyUp(
+        JNIEnv *env,
+        jobject obj,
+        jint c) {
+    kbdkey(c, 0);
+}
+
+JNIEXPORT void JNICALL
+Java_org_echoline_drawterm_MainActivity_setPass(
+        JNIEnv *env,
+        jobject obj,
+        jstring str) {
+    setenv("PASS", (char*)(*env)->GetStringUTFChars(env, str, 0), 1);
+}
+
+JNIEXPORT void JNICALL
+Java_org_echoline_drawterm_MainActivity_setWidth(
+        JNIEnv *env,
+        jobject obj,
+        jint width) {
+    screenWidth = width;
+}
+
+JNIEXPORT void JNICALL
+Java_org_echoline_drawterm_MainActivity_setHeight(
+        JNIEnv *env,
+        jobject obj,
+        jint height) {
+    screenHeight = height;
+}
+
+JNIEXPORT void JNICALL
+Java_org_echoline_drawterm_MainActivity_setWidthScale(
+        JNIEnv *env,
+        jobject obj,
+        jfloat s) {
+    ws = s;
+}
+
+JNIEXPORT void JNICALL
+Java_org_echoline_drawterm_MainActivity_setHeightScale(
+        JNIEnv *env,
+        jobject obj,
+        jfloat s) {
+    hs = s;
+}
+
+JNIEXPORT jint JNICALL
+Java_org_echoline_drawterm_MainActivity_dtmain(
+        JNIEnv *env,
+        jobject obj,
+        jobjectArray argv) {
+    int i, ret;
+    char **args = (char **) malloc(((*env)->GetArrayLength(env, argv)+1) * sizeof(char *));
+
+    for (i = 0; i < (*env)->GetArrayLength(env, argv); i++) {
+        jobject str = (jobject) (*env)->GetObjectArrayElement(env, argv, i);
+        args[i] = strdup((char*)(*env)->GetStringUTFChars(env, (jstring)str, 0));
+    }
+    args[(*env)->GetArrayLength(env, argv)] = NULL;
+
+    ret = dt_main(i, args);
+
+    for (i = 0; args[i] != NULL; i++) {
+        free(args[i]);
+    }
+    free(args);
+
+    return ret;
+}
+
+JNIEXPORT void JNICALL
+Java_org_echoline_drawterm_MainActivity_setMouse(
+        JNIEnv *env,
+        jobject obj,
+        jintArray args) {
+    jboolean isCopy;
+    jint *data;
+    if ((*env)->GetArrayLength(env, args) < 3)
+        return;
+    data = (*env)->GetIntArrayElements(env, args, &isCopy);
+    mousept.x = (int)(data[0] / ws);
+    mousept.y = (int)(data[1] / hs);
+    buttons = data[2];
+    (*env)->ReleaseIntArrayElements(env, args, data, 0);
+    absmousetrack(mousept.x, mousept.y, buttons, ticks());
+}
+
+JNIEXPORT void JNICALL
+Java_org_echoline_drawterm_MainActivity_setDTSurface(
+	JNIEnv* jenv,
+	jobject obj,
+	jobject surface) {
+    if (surface != NULL) {
+        window = ANativeWindow_fromSurface(jenv, surface);
+	ANativeWindow_setBuffersGeometry(window, screenWidth, screenHeight,
+		AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM);
+	flushmemscreen(Rect(0, 0, screenWidth, screenHeight));
+    } else if (window != NULL) {
+        ANativeWindow_release(window);
+	window = NULL;
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_org_echoline_drawterm_MainActivity_exitDT(
+	JNIEnv* jenv,
+	jobject obj) {
+    exit(0);
+}
+
+JNIEXPORT void JNICALL
+Java_org_echoline_drawterm_MainActivity_sendPicture(
+	JNIEnv* env,
+	jobject obj,
+	jbyteArray array) {
+    jint len = (*env)->GetArrayLength(env, array);
+    jbyte *bytes = (*env)->GetByteArrayElements(env, array, NULL);
+    camlen = len;
+    cambuf = malloc(camlen);
+    memcpy(cambuf, bytes, camlen);
+    (*env)->ReleaseByteArrayElements(env, array, bytes, 0);
+}
+
--- /dev/null
+++ b/gui-android/java/org/echoline/drawterm/DrawTermThread.java
@@ -1,0 +1,32 @@
+package org.echoline.drawterm;
+
+/**
+ * Created by eli on 12/4/17.
+ */
+
+public class DrawTermThread extends Thread {
+	private MainActivity m;
+	private String p;
+	private String []args;
+
+	public DrawTermThread(String []args, String p, MainActivity m) {
+		this.m = m;
+		this.p = p;
+		this.args = args;
+	}
+
+	@Override
+	public void run() {
+		if (p != null && !p.equals(""))
+			m.setPass(p);
+		m.dtmain(args);
+		m.runOnUiThread(new Runnable() {
+			@Override
+			public void run() {
+				m.exitDT();
+				m.setContentView(R.layout.server_main);
+				m.populateServers(m);
+			}
+		});
+	}
+}
--- /dev/null
+++ b/gui-android/java/org/echoline/drawterm/MainActivity.java
@@ -1,0 +1,437 @@
+package org.echoline.drawterm;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.os.Bundle;
+import android.os.Environment;
+
+import android.app.Activity;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.WindowManager;
+import android.view.Surface;
+import android.view.inputmethod.InputMethodManager;
+import android.view.KeyEvent;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.TotalCaptureResult;
+import android.media.Image;
+import android.media.ImageReader;
+import android.graphics.ImageFormat;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import java.io.File;
+import java.util.Map;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.ArrayList;
+
+public class MainActivity extends Activity {
+	private Map<String, ?> map;
+	private MainActivity mainActivity;
+	private boolean dtrunning = false;
+	private DrawTermThread dthread;
+	private int notificationId;
+	private CameraDevice cameraDevice = null;
+	private byte []jpegBytes;
+
+	static {
+		System.loadLibrary("drawterm");
+	}
+
+	public void showNotification(String text) {
+		Notification.Builder builder = new Notification.Builder(this)
+			.setDefaults(Notification.DEFAULT_SOUND)
+			.setSmallIcon(R.drawable.ic_small)
+			.setContentText(text)
+			.setStyle(new Notification.BigTextStyle().bigText(text))
+			.setPriority(Notification.PRIORITY_DEFAULT);
+
+		((NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE)).notify(notificationId, builder.build());
+		notificationId++;
+	}
+
+	public int numCameras() {
+		try {
+			return ((CameraManager)getSystemService(Context.CAMERA_SERVICE)).getCameraIdList().length;
+		} catch (CameraAccessException e) {
+			Log.w("drawterm", e.toString());
+			return 0;
+		}
+	}
+
+	public void takePicture(int id) {
+		try {
+			HandlerThread mBackgroundThread = new HandlerThread("Camera Background");
+			mBackgroundThread.start();
+			Handler mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
+			CameraManager manager = (CameraManager)getSystemService(Context.CAMERA_SERVICE);
+			String []cameraIdList = manager.getCameraIdList();
+			manager.openCamera(cameraIdList[id], new CameraDevice.StateCallback() {
+				public void onOpened(CameraDevice device) {
+					cameraDevice = device;
+				}
+				public void onDisconnected(CameraDevice device) {
+					if (cameraDevice != null)
+						cameraDevice.close();
+					cameraDevice = null;
+				}
+				public void onError(CameraDevice device, int error) {
+					if (cameraDevice != null)
+						cameraDevice.close();
+					cameraDevice = null;
+				}
+			}, mBackgroundHandler);
+			ImageReader reader = ImageReader.newInstance(640, 480, ImageFormat.JPEG, 1);
+			CaptureRequest.Builder captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG);
+			captureBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
+			captureBuilder.set(CaptureRequest.CONTROL_AWB_MODE, CameraMetadata.CONTROL_AWB_MODE_AUTO);
+			captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON);
+			captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getWindowManager().getDefaultDisplay().getRotation());
+			captureBuilder.addTarget(reader.getSurface());
+			reader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
+				public void onImageAvailable(ImageReader reader) {
+					Image image = null;
+					try {
+						image = reader.acquireLatestImage();
+						ByteBuffer buffer = image.getPlanes()[0].getBuffer();
+						jpegBytes = new byte[buffer.capacity()];
+						buffer.get(jpegBytes);
+					} catch (Exception e) {
+						Log.w("drawterm", e.toString());
+					} finally {
+						if (image != null) {
+							image.close();
+						}
+					}
+				}
+			}, mBackgroundHandler);
+			List<Surface> outputSurfaces = new ArrayList<Surface>(1);
+			outputSurfaces.add(reader.getSurface());
+			cameraDevice.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() {
+				public void onConfigured(CameraCaptureSession session) {
+					try {
+						List<CaptureRequest> captureRequests = new ArrayList<CaptureRequest>(10);
+						for (int i = 0; i < 10; i++)
+							captureRequests.add(captureBuilder.build());
+						session.captureBurst(captureRequests, new CameraCaptureSession.CaptureCallback() {
+							public void onCaptureSequenceCompleted(CameraCaptureSession session, int sequenceId, long frameNumber) {
+								try {
+									sendPicture(jpegBytes);
+									mBackgroundThread.quitSafely();
+									mBackgroundThread.join();
+								} catch (Exception e) {
+									Log.w("drawterm", e.toString());
+								}
+							}
+						}, mBackgroundHandler);
+					} catch (CameraAccessException e) {
+						e.printStackTrace();
+					}
+				}
+				public void onConfigureFailed(CameraCaptureSession session) {
+				}
+			}, mBackgroundHandler);
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+	}
+
+	public void serverView(View v) {
+		setContentView(R.layout.server_main);
+		serverButtons();
+
+		String s = (String)map.get(((TextView)v).getText().toString());
+		String []a = s.split("\007");
+
+		((EditText)findViewById(R.id.cpuServer)).setText((String)a[0]);
+		((EditText)findViewById(R.id.authServer)).setText((String)a[1]);
+		((EditText)findViewById(R.id.userName)).setText((String)a[2]);
+		if (a.length > 3)
+			((EditText)findViewById(R.id.passWord)).setText((String)a[3]);
+	}
+
+	public void populateServers(Context context) {
+		ListView ll = (ListView)findViewById(R.id.servers);
+		ArrayAdapter<String> la = new ArrayAdapter<String>(this, R.layout.item_main);
+		SharedPreferences settings = getSharedPreferences("DrawtermPrefs", 0);
+		map = (Map<String, ?>)settings.getAll();
+		String key;
+		Object []keys = map.keySet().toArray();
+		for (int i = 0; i < keys.length; i++) {
+			key = (String)keys[i];
+			la.add(key);
+		}
+		ll.setAdapter(la);
+
+		setDTSurface(null);
+		dtrunning = false;
+	}
+
+	public void runDrawterm(String []args, String pass) {
+		Resources res = getResources();
+		DisplayMetrics dm = res.getDisplayMetrics();
+
+		int wp = dm.widthPixels;
+		int hp = dm.heightPixels;
+
+		setContentView(R.layout.drawterm_main);
+
+		Button kbutton = (Button)findViewById(R.id.keyboardToggle);
+		kbutton.setOnClickListener(new View.OnClickListener() {
+			@Override
+			public void onClick(final View view) {
+				InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
+				imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0);
+			}
+		});
+
+		int rid = res.getIdentifier("navigation_bar_height", "dimen", "android");
+		if (rid > 0) {
+			hp -= res.getDimensionPixelSize(rid);
+		}
+		LinearLayout ll = (LinearLayout)findViewById(R.id.dtButtons);
+		hp -= ll.getHeight();
+
+		int w = (int)(wp * (160.0/dm.xdpi));
+		int h = (int)(hp * (160.0/dm.ydpi));
+		float ws = (float)wp/w;
+		float hs = (float)hp/h;
+		// only scale up
+		if (ws < 1) {
+			ws = 1;
+			w = wp;
+		}
+		if (hs < 1) {
+			hs = 1;
+			h = hp;
+		}
+
+		MySurfaceView mView = new MySurfaceView(mainActivity, w, h, ws, hs);
+		mView.getHolder().setFixedSize(w, h);
+
+		LinearLayout l = (LinearLayout)findViewById(R.id.dlayout);
+		l.addView(mView, 1, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT));
+
+		dthread = new DrawTermThread(args, pass, mainActivity);
+		dthread.start();
+
+		dtrunning = true;
+	}
+
+	public void serverButtons() {
+		Button button = (Button)findViewById(R.id.save);
+		button.setOnClickListener(new View.OnClickListener() {
+			@Override
+			public void onClick(View v) {
+				String cpu = ((EditText)findViewById(R.id.cpuServer)).getText().toString();
+				String auth = ((EditText)findViewById(R.id.authServer)).getText().toString();
+				String user = ((EditText)findViewById(R.id.userName)).getText().toString();
+				String pass = ((EditText)findViewById(R.id.passWord)).getText().toString();
+
+				SharedPreferences settings = getSharedPreferences("DrawtermPrefs", 0);
+				SharedPreferences.Editor editor = settings.edit();
+				editor.putString(user + "@" + cpu + " (auth="  + auth + ")", cpu + "\007" + auth + "\007" + user + "\007" + pass);
+				editor.commit();
+			}
+		});
+
+		button = (Button) findViewById(R.id.connect);
+		button.setOnClickListener(new View.OnClickListener() {
+			@Override
+			public void onClick(final View view) {
+				String cpu = ((EditText)findViewById(R.id.cpuServer)).getText().toString();
+				String auth = ((EditText)findViewById(R.id.authServer)).getText().toString();
+				String user = ((EditText)findViewById(R.id.userName)).getText().toString();
+				String pass = ((EditText)findViewById(R.id.passWord)).getText().toString();
+
+				String args[] = {"drawterm", "-p", "-h", cpu, "-a", auth, "-u", user};
+				runDrawterm(args, pass);
+			}
+		});
+	}
+
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+		getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+		mainActivity = this;
+		setObject();
+		setContentView(R.layout.activity_main);
+		populateServers(this);
+
+		View fab = findViewById(R.id.fab);
+		fab.setOnClickListener(new View.OnClickListener() {
+			@Override
+			public void onClick(View v) {
+				setContentView(R.layout.server_main);
+				serverButtons();
+			}
+		});
+	}
+
+	@Override
+	public boolean dispatchKeyEvent(KeyEvent event)
+	{
+		if (!dtrunning) {
+			return super.dispatchKeyEvent(event);
+		}
+
+		int k = event.getUnicodeChar();
+		if (k == 0) {
+			k = event.getDisplayLabel();
+			if (k >= 'A' && k <= 'Z')
+				k |= 0x20;
+		}
+		String chars = event.getCharacters();
+		if (k == 0 && chars != null) {
+			for (int i = 0; i < chars.length(); i++) {
+				k = chars.codePointAt(i);
+				keyDown(k);
+				keyUp(k);
+			}
+			return true;
+		}
+
+		if (k == 0) switch (event.getKeyCode()) {
+		case KeyEvent.KEYCODE_DEL:
+			k = 0x0008;
+			break;
+		case KeyEvent.KEYCODE_FORWARD_DEL:
+			k = 0x007F;
+			break;
+		case KeyEvent.KEYCODE_ESCAPE:
+			k = 0x001B;
+			break;
+		case KeyEvent.KEYCODE_MOVE_HOME:
+			k = 0xF00D;
+			break;
+		case KeyEvent.KEYCODE_MOVE_END:
+			k = 0xF018;
+			break;
+		case KeyEvent.KEYCODE_PAGE_UP:
+			k = 0xF00F;
+			break;
+		case KeyEvent.KEYCODE_PAGE_DOWN:
+			k = 0xF013;
+			break;
+		case KeyEvent.KEYCODE_INSERT:
+			k = 0xF014;
+			break;
+		case KeyEvent.KEYCODE_SYSRQ:
+			k = 0xF010;
+			break;
+		case KeyEvent.KEYCODE_DPAD_UP:
+			k = 0xF00E;
+			break;
+		case KeyEvent.KEYCODE_DPAD_LEFT:
+			k = 0xF011;
+			break;
+		case KeyEvent.KEYCODE_DPAD_RIGHT:
+			k = 0xF012;
+			break;
+		case KeyEvent.KEYCODE_DPAD_DOWN:
+			k = 0xF800;
+			break;
+		}
+
+		if (k == 0)
+			return true;
+
+		if (event.isCtrlPressed()) {
+			keyDown(0xF017);
+		}
+		if (event.isAltPressed() && k < 128) {
+			keyDown(0xF015);
+		}
+
+		if (event.getAction() == KeyEvent.ACTION_DOWN) {
+			keyDown(k);
+		}
+		else if (event.getAction() == KeyEvent.ACTION_UP) {
+			keyUp(k);
+		}
+
+		if (event.isCtrlPressed()) {
+			keyUp(0xF017);
+		}
+		if (event.isAltPressed() && k < 128) {
+			keyUp(0xF015);
+		}
+
+		return true;
+	}
+
+	@Override
+	public void onBackPressed()
+	{
+	}
+
+	@Override
+	public void onDestroy()
+	{
+		setDTSurface(null);
+		dtrunning = false;
+		exitDT();
+		super.onDestroy();
+	}
+
+	public void setClipBoard(String str) {
+		ClipboardManager cm = (ClipboardManager)getApplicationContext().getSystemService(Context.CLIPBOARD_SERVICE);
+		if (cm != null) {
+			ClipData cd = ClipData.newPlainText(null, str);
+			cm.setPrimaryClip(cd);
+		}
+	}
+
+	public String getClipBoard() {
+		ClipboardManager cm = (ClipboardManager)getApplicationContext().getSystemService(Context.CLIPBOARD_SERVICE);
+		if (cm != null) {
+			ClipData cd = cm.getPrimaryClip();
+			if (cd != null)
+				return (String)(cd.getItemAt(0).coerceToText(mainActivity.getApplicationContext()).toString());
+		}
+		return "";
+	}
+
+	public native void dtmain(Object[] args);
+	public native void setPass(String arg);
+	public native void setWidth(int arg);
+	public native void setHeight(int arg);
+	public native void setWidthScale(float arg);
+	public native void setHeightScale(float arg);
+	public native void setDTSurface(Surface surface);
+	public native void setMouse(int[] args);
+	public native void setObject();
+	public native void keyDown(int c);
+	public native void keyUp(int c);
+	public native void exitDT();
+	public native void sendPicture(byte[] array);
+}
--- /dev/null
+++ b/gui-android/java/org/echoline/drawterm/MySurfaceView.java
@@ -1,0 +1,91 @@
+package org.echoline.drawterm;
+
+import android.util.Log;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.SystemClock;
+import android.view.MotionEvent;
+import android.view.SurfaceView;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.EditText;
+
+import java.nio.ByteBuffer;
+import java.nio.IntBuffer;
+import java.security.spec.ECField;
+
+/**
+ * Created by eli on 12/3/17.
+ */
+public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
+	private int screenWidth, screenHeight;
+	private MainActivity mainActivity;
+	private float ws, hs;
+
+	public MySurfaceView(Context context, int w, int h, float ws, float hs) {
+		super(context);
+		screenHeight = h;
+		screenWidth = w;
+		this.ws = ws;
+		this.hs = hs;
+		mainActivity = (MainActivity)context;
+		mainActivity.setWidth(screenWidth);
+		mainActivity.setHeight(screenHeight);
+		mainActivity.setWidthScale(ws);
+		mainActivity.setHeightScale(hs);
+		setWillNotDraw(true);
+
+		getHolder().addCallback(this);
+
+		setOnTouchListener(new View.OnTouchListener() {
+			private int[] mouse = new int[3];
+
+			@Override
+			public boolean onTouch(View v, MotionEvent event) {
+				CheckBox left = (CheckBox)mainActivity.findViewById(R.id.mouseLeft);
+				CheckBox middle = (CheckBox)mainActivity.findViewById(R.id.mouseMiddle);
+				CheckBox right = (CheckBox)mainActivity.findViewById(R.id.mouseRight);
+				CheckBox up = (CheckBox)mainActivity.findViewById(R.id.mouseUp);
+				CheckBox down = (CheckBox)mainActivity.findViewById(R.id.mouseDown);
+				int buttons = (left.isChecked()? 1: 0) |
+								(middle.isChecked()? 2: 0) |
+								(right.isChecked()? 4: 0) |
+								(up.isChecked()? 8: 0) |
+								(down.isChecked()? 16: 0);
+				if (event.getAction() == MotionEvent.ACTION_DOWN) {
+					mouse[0] = Math.round(event.getX());
+					mouse[1] = Math.round(event.getY());
+					mouse[2] = buttons;
+					mainActivity.setMouse(mouse);
+				} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
+					mouse[0] = Math.round(event.getX());
+					mouse[1] = Math.round(event.getY());
+					mouse[2] = buttons;
+					mainActivity.setMouse(mouse);
+				} else if (event.getAction() == MotionEvent.ACTION_UP) {
+					mouse[0] = Math.round(event.getX());
+					mouse[1] = Math.round(event.getY());
+					mouse[2] = 0;
+					mainActivity.setMouse(mouse);
+				}
+				return true;
+			}
+		});
+	}
+
+	@Override
+	public void surfaceCreated(SurfaceHolder holder) {
+		mainActivity.setDTSurface(holder.getSurface());
+	}
+
+	@Override
+	public void surfaceChanged(SurfaceHolder holder, int w, int h, int format) {
+	}
+
+	@Override
+	public void surfaceDestroyed(SurfaceHolder holder) {
+		mainActivity.setDTSurface(null);
+	}
+}
--- /dev/null
+++ b/gui-android/res/layout/activity_main.xml
@@ -1,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.widget.FrameLayout
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent"
+	tools:context="org.echoline.drawterm.MainActivity">
+
+	<include layout="@layout/content_main" />
+
+	<LinearLayout
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_gravity="bottom|end">
+
+		<Button
+			android:id="@+id/fab"
+			android:text="add server"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"/>
+	</LinearLayout>
+
+</android.widget.FrameLayout>
--- /dev/null
+++ b/gui-android/res/layout/content_main.xml
@@ -1,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.widget.FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="org.echoline.drawterm.MainActivity"
+    tools:showIn="@layout/activity_main">
+    <ListView
+        android:id="@+id/servers"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+    </ListView>
+</android.widget.FrameLayout>
--- /dev/null
+++ b/gui-android/res/layout/drawterm_main.xml
@@ -1,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/dlayout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    tools:context="org.echoline.drawterm.MainActivity">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom"
+        android:gravity="center_horizontal"
+        android:orientation="horizontal"
+        android:id="@+id/dtButtons">
+
+        <CheckBox
+            android:id="@+id/mouseLeft"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent" />
+        <CheckBox
+            android:id="@+id/mouseMiddle"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent" />
+        <CheckBox
+            android:id="@+id/mouseRight"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent" />
+        <CheckBox
+            android:id="@+id/mouseUp"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent" />
+        <CheckBox
+            android:id="@+id/mouseDown"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent" />
+        <Button
+            android:id="@+id/keyboardToggle"
+            android:text="kb"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+    </LinearLayout>
+</LinearLayout>
--- /dev/null
+++ b/gui-android/res/layout/item_main.xml
@@ -1,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent" android:layout_height="match_parent"
+    android:padding="10dp"
+    android:textSize="16sp"
+    android:onClick="serverView">
+</TextView>
+
--- /dev/null
+++ b/gui-android/res/layout/server_main.xml
@@ -1,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.widget.FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="org.echoline.drawterm.MainActivity">
+
+    <LinearLayout
+        android:layout_height="match_parent"
+        android:layout_width="match_parent"
+        android:orientation="vertical">
+
+        <EditText
+            android:id="@+id/cpuServer"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:ems="10"
+            android:hint="CPU" />
+        <EditText
+            android:id="@+id/authServer"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:ems="10"
+            android:hint="Auth" />
+        <EditText
+            android:id="@+id/userName"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:ems="10"
+            android:hint="Username" />
+
+        <EditText
+            android:id="@+id/passWord"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:ems="10"
+            android:password="true"
+            android:hint="Password"/>
+
+        <Button
+            android:id="@+id/save"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Save" />
+
+        <Button
+            android:id="@+id/connect"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Connect" />
+
+    </LinearLayout>
+
+</android.widget.FrameLayout>
--- /dev/null
+++ b/gui-android/res/values/colors.xml
@@ -1,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#3F51B5</color>
+    <color name="colorPrimaryDark">#303F9F</color>
+    <color name="colorAccent">#3FB551</color>
+</resources>
--- /dev/null
+++ b/gui-android/res/values/strings.xml
@@ -1,0 +1,3 @@
+<resources>
+    <string name="app_name">Drawterm</string>
+</resources>
--- /dev/null
+++ b/gui-android/res/values/styles.xml
@@ -1,0 +1,8 @@
+<resources>
+
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="android:Theme.NoTitleBar">
+        <!-- Customize your theme here. -->
+    </style>
+
+</resources>
--- /dev/null
+++ b/gui-fbdev/Makefile
@@ -1,0 +1,12 @@
+ROOT=..
+include ../Make.config
+LIB=libgui.a
+
+OFILES=\
+	fbdev.$O\
+
+default: $(LIB)
+$(LIB): $(OFILES)
+	$(AR) r $(LIB) $(OFILES)
+	$(RANLIB) $(LIB)
+
--- /dev/null
+++ b/gui-fbdev/fbdev.c
@@ -1,0 +1,673 @@
+#include "u.h"
+#include "lib.h"
+#include "dat.h"
+#include "fns.h"
+#include "error.h"
+
+#include <draw.h>
+#include <memdraw.h>
+#include <keyboard.h>
+#include <cursor.h>
+#include "screen.h"
+
+#undef long
+#undef ulong
+
+#include <linux/fb.h>
+#include <linux/input.h>
+
+uchar*		fbp;
+Memimage*	screenimage;
+Memimage*	backbuf;
+Rectangle	screenr;
+char*		snarfbuf;
+struct fb_fix_screeninfo finfo;
+struct fb_var_screeninfo vinfo;
+int		*eventfds = NULL;
+int		neventfds;
+Point		mousexy;
+char		shift_state;
+int		ttyfd;
+char*		tty;
+char		hidden;
+int		devicesfd;
+ulong		chan;
+int		depth;
+
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <limits.h>
+
+#include <fcntl.h>
+#include <termios.h>
+#include <poll.h>
+#include <sys/ioctl.h>
+#include <linux/keyboard.h>
+#include <signal.h>
+
+#include <termios.h>
+
+#define ulong p9_ulong
+
+int code2key[] = {
+	Kesc, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\x08',
+	'\x09', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\n',
+	Kctl, 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '`', Kshift,
+	'\\', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', Kshift, '*', Kalt,
+	' ', Kcaps, KF|1, KF|2, KF|3, KF|4, KF|5, KF|6, KF|7, KF|8, KF|9, KF|10,
+	Knum, Kscroll,
+	'7', '8', '9', '-', '4', '5', '6', '+', '1', '2', '3', '0', '.',
+};
+
+int code2key_shift[] = {
+	Kesc, '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', '\x08',
+	'\x09', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '\n',
+	Kctl, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '\"', '~', Kshift,
+	'|', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', Kshift, '*', Kalt,
+	' ', Kcaps, KF|1, KF|2, KF|3, KF|4, KF|5, KF|6, KF|7, KF|8, KF|9, KF|10,
+	Knum, Kscroll,
+	'7', '8', '9', '-', '4', '5', '6', '+', '1', '2', '3', '0', '.',
+};
+
+Memimage *gscreen;
+char *snarfbuf = nil;
+
+int onevent(struct input_event*);
+void termctl(uint32_t o, int or);
+void ctrlc(int sig);
+
+void
+_fbput(Memimage *m, Rectangle r) {
+	int y;
+
+	for (y = r.min.y; y < r.max.y; y++){
+		long loc = y * finfo.line_length + r.min.x * depth;
+		void *ptr = m->data->bdata + y * m->width * 4 + r.min.x * depth;
+
+		memcpy(fbp + loc, ptr, Dx(r) * depth);
+	}
+}
+
+Memimage*
+fbattach(int fbdevidx)
+{
+	Rectangle r;
+	char devname[64];
+	size_t size;
+	int fd;
+
+	/*
+	 * Connect to /dev/fb0
+	 */
+	snprintf(devname, sizeof(devname) - 1, "/dev/fb%d", fbdevidx);
+	if ((fd = open(devname, O_RDWR)) < 0)
+		goto err;
+
+	if (ioctl(fd, FBIOGET_VSCREENINFO, &(vinfo)) < 0)
+		goto err;
+
+	switch (vinfo.bits_per_pixel) {
+	case 32:
+		chan = XRGB32;
+		depth = 4;
+		break;
+	case 16:
+		chan = RGB16;
+		depth = 2;
+		break;
+	default:
+		goto err;
+	}
+
+	if (ioctl(fd, FBIOGET_FSCREENINFO, &(finfo)) < 0)
+		goto err;
+
+	size = vinfo.yres_virtual * finfo.line_length;
+	if ((fbp = mmap(0, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, (off_t)0)) < 0)
+		goto err;
+	/*
+	 * Figure out underlying screen format.
+	 */
+	r = Rect(0, 0, vinfo.xres_virtual, vinfo.yres_virtual);
+
+	screenr = r;
+
+	screenimage = allocmemimage(r, chan);
+	backbuf = allocmemimage(r, chan);
+	return backbuf;
+
+err:
+	return nil;
+}
+
+int
+eventattach()
+{
+	char eventfile[PATH_MAX] = "";
+	char line[PATH_MAX];
+	FILE *devices;
+	char *ptr;
+
+	neventfds = 0;
+	devices = fopen("/proc/bus/input/devices", "r");
+	if (devices == NULL)
+		return -1;
+	while (fgets(line, sizeof(line)-1, devices) != NULL)
+		if (line[0] == 'H') {
+			ptr = strstr(line, "event");
+			if (ptr == NULL)
+				continue;
+			ptr[strcspn(ptr, " \r\n")] = '\0';
+			snprintf(eventfile, sizeof(eventfile)-1, "/dev/input/%s", ptr);
+			neventfds++;
+			eventfds = realloc(eventfds, neventfds * sizeof(int));
+			eventfds[neventfds-1] = open(eventfile, O_RDONLY);
+			if (eventfds[neventfds-1] < 0)
+				neventfds--;
+		}
+	fclose(devices);
+
+	if (neventfds == 0)
+		return -1;
+	return 1;
+}
+
+void
+flushmemscreen(Rectangle r)
+{
+	int x, y, i;
+	Point p;
+	long fbloc;
+	int x2, y2;
+
+	if (rectclip(&r, screenimage->r) == 0)
+		return;
+	if (Dx(r) == 0 || Dy(r) == 0)
+		return;
+
+	assert(!canqlock(&drawlock));
+
+	memimagedraw(screenimage, r, backbuf, r.min, nil, r.min, S);
+
+	if (hidden != 0)
+		return;
+
+	p = mousexy;
+
+	// draw cursor
+	for (x = 0; x < 16; x++) {
+		x2 = x + cursor.offset.x;
+
+		if ((p.x + x2) < 0)
+			continue;
+
+		if ((p.x + x2) >= screenimage->r.max.x)
+			break;
+
+		for (y = 0; y < 16; y++) {
+			y2 = y + cursor.offset.y;
+
+			if ((p.y + y2) < 0)
+				continue;
+
+			if ((p.y + y2) >= screenimage->r.max.y)
+				break;
+
+			i = y * 2 + x / 8;
+			fbloc = ((p.y+y2) * screenimage->r.max.x + (p.x+x2)) * depth;
+
+			if (cursor.clr[i] & (128 >> (x % 8))) {
+				switch (depth) {
+				case 2:
+					*((uint16_t*)(screenimage->data->bdata + fbloc)) = 0xFFFF;
+					break;
+				case 4:
+					*((uint32_t*)(screenimage->data->bdata + fbloc)) = 0xFFFFFFFF;
+					break;
+				}
+			}
+
+			if (cursor.set[i] & (128 >> (x % 8))) {
+				switch (depth) {
+				case 2:
+					*((uint16_t*)(screenimage->data->bdata + fbloc)) = 0x0000;
+					break;
+				case 4:
+					*((uint32_t*)(screenimage->data->bdata + fbloc)) = 0xFF000000;
+					break;
+				}
+			}
+		}
+	}
+
+	_fbput(screenimage, r);
+}
+
+static void
+fbproc(void *v)
+{
+	struct input_event data;
+	char buf[32];
+	struct pollfd *pfd;
+	int r;
+	int ioctlarg;
+
+	pfd = calloc(3, sizeof(struct pollfd));
+	pfd[0].fd = ttyfd; // for virtual console switches
+	pfd[0].events = POLLPRI;
+	pfd[1].fd = 0; // stdin goes to nowhere
+	pfd[1].events = POLLIN;
+	pfd[2].fd = open("/proc/bus/input/devices", O_RDONLY); // input hotplug
+	if (pfd[2].fd < 0)
+		panic("cannot open /proc/bus/input/devices: %r");
+	pfd[2].events = POLLIN;
+
+TOP:
+	while(read(pfd[2].fd, buf, 31) > 0);
+
+	pfd = realloc(pfd, sizeof(struct pollfd) * (neventfds + 3));
+	for (r = 0; r < neventfds; r++) {
+		pfd[r+3].fd = eventfds[r];
+		pfd[r+3].events = POLLIN;
+	}
+
+	for(;;) {
+		shift_state = 6;
+		if (ioctl(0, TIOCLINUX, &shift_state) < 0)
+			panic("ioctl TIOCLINUX 6: %r");
+
+		r = poll(pfd, 3+neventfds, -1);
+		if (r < 0)
+			oserror();
+		if (pfd[0].revents & POLLPRI) {
+			if ((r = read(ttyfd, buf, 31)) <= 0)
+				panic("ttyfd read: %r");
+			buf[r] = '\0';
+			if (strcmp(buf, tty) == 0) {
+				hidden = 0;
+				printf("\e[?25l");
+				fflush(stdout);
+				qlock(&drawlock);
+				flushmemscreen(gscreen->clipr);
+				qunlock(&drawlock);
+			}
+			else
+				hidden = 1;
+			close(ttyfd);
+			ttyfd = open("/sys/class/tty/tty0/active", O_RDONLY);
+			if (ttyfd < 0)
+				panic("cannot open tty active fd: %r");
+			pfd[0].fd = ttyfd;
+			read(ttyfd, buf, 0);
+		}
+		if (pfd[1].revents & POLLIN)
+			read(pfd[1].fd, buf, 31);
+		if (pfd[2].revents & POLLIN) {
+			for (r = 0; r < neventfds; r++)
+				close(eventfds[r]);
+			if(eventattach() < 0) {
+				panic("cannot open event files: %r");
+			}
+			goto TOP;
+		}
+		for (r = 0; r < neventfds; r++)
+			if (pfd[r+3].revents & POLLIN) {
+				if (read(pfd[r+3].fd, &data, sizeof(data)) != sizeof(data))
+					panic("eventfd read: %r");
+				if (onevent(&data) == 0) {
+					ioctlarg = 15;
+					if (ioctl(0, TIOCLINUX, &ioctlarg) != 0) {
+						ioctlarg = 4;
+						ioctl(0, TIOCLINUX, &ioctlarg);
+						qlock(&drawlock);
+						flushmemscreen(gscreen->clipr);
+						qunlock(&drawlock);
+					} else {
+						write(1, "\033[9;30]", 7);
+					}
+				}
+			}
+	}
+
+	printf("\e[?25h");
+	fflush(stdout);
+	termctl(ECHO, 1);
+	free(pfd);
+}
+
+void
+screensize(Rectangle r, ulong chan)
+{
+	gscreen = backbuf;
+	gscreen->clipr = ZR;
+}
+
+void
+screeninit(void)
+{
+	int r;
+	char buf[1];
+
+	// set up terminal
+	printf("\e[?25l");
+	fflush(stdout);
+	termctl(~(ICANON|ECHO), 0);
+	signal(SIGINT, ctrlc);
+
+	memimageinit();
+
+	// tty switching
+	ttyfd = open("/sys/class/tty/tty0/active", O_RDONLY);
+	if (ttyfd >= 0) {
+		tty = malloc(32);
+		r = read(ttyfd, tty, 31);
+		if (r >= 0)
+			tty[r] = '\0';
+		else
+			tty[0] = '\0';
+		close(ttyfd);
+		ttyfd = open("/sys/class/tty/tty0/active", O_RDONLY);
+	}
+	if (ttyfd < 0)
+		panic("cannot open tty active fd: %r");
+	read(ttyfd, buf, 0);
+	hidden = 0;
+
+	if(fbattach(0) == nil) {
+		panic("cannot open framebuffer: %r");
+	}
+
+	if(eventattach() < 0) {
+		panic("cannot open event files: %r");
+	}
+
+	screensize(screenr, chan);
+	if (gscreen == nil)
+		panic("screensize failed");
+
+	gscreen->clipr = screenr;
+	kproc("fbdev", fbproc, nil);
+
+	terminit();
+
+	qlock(&drawlock);
+	flushmemscreen(gscreen->clipr);
+	qunlock(&drawlock);
+}
+
+Memdata*
+attachscreen(Rectangle *r, ulong *chan, int *depth, int *width, int *softscreen)
+{
+	*r = gscreen->clipr;
+	*chan = gscreen->chan;
+	*depth = gscreen->depth;
+	*width = gscreen->width;
+	*softscreen = 1;
+
+	gscreen->data->ref++;
+	return gscreen->data;
+}
+
+void
+getcolor(ulong i, ulong *r, ulong *g, ulong *b)
+{
+	ulong v;
+	
+	v = cmap2rgb(i);
+	*r = (v>>16)&0xFF;
+	*g = (v>>8)&0xFF;
+	*b = v&0xFF;
+}
+
+void
+setcolor(ulong i, ulong r, ulong g, ulong b)
+{
+	/* no-op */
+	return;
+}
+
+char*
+clipread(void)
+{
+	if(snarfbuf)
+		return strdup(snarfbuf);
+	return nil;
+}
+
+int
+clipwrite(char *buf)
+{
+	if(snarfbuf)
+		free(snarfbuf);
+	snarfbuf = strdup(buf);
+	return 0;
+}
+
+void
+guimain(void)
+{
+	cpubody();
+}
+
+void
+termctl(uint32_t o, int or)
+{
+	struct termios t;
+
+	tcgetattr(0, &t);
+	if (or)
+		t.c_lflag |= o;
+	else
+		t.c_lflag &= o;
+	tcsetattr(0, TCSANOW, &t);
+}
+
+void
+ctrlc(int sig) {
+}
+
+int
+onevent(struct input_event *data)
+{
+	Rectangle old, new;
+	ulong msec;
+	static int buttons;
+	static Point coord;
+	static char touched;
+	static Point startmousept;
+	static Point startpt;
+	int key;
+	static ulong lastmsec = 0;
+
+	if (hidden != 0)
+		return -1;
+
+	msec = ticks();
+
+	old.min = mousexy;
+	old.max = addpt(old.min, Pt(16, 16));
+
+	buttons &= ~0x18;
+
+	switch(data->type) {
+	case 3:
+		switch(data->code) {
+		case 0:
+			coord.x = data->value;
+			break;
+		case 1:
+			coord.y = data->value;
+			break;
+		case 0x18:
+		case 0x1c:
+			if (data->value == 0)
+				touched = 0;
+			else if (data->value > 24) {
+				touched = 1;
+				startmousept = coord;
+				startpt = mousexy;
+			}
+			break;
+		default:
+			return -1;
+		}
+		if (touched)
+			mousexy = addpt(startpt, divpt(subpt(coord, startmousept), 4));
+		break;
+	case 2:
+		switch(data->code) {
+		case 0:
+			mousexy.x += data->value;
+			break;
+		case 1:
+			mousexy.y += data->value;
+			break;
+		case 8:
+			buttons |= data->value == 1? 8: 16;
+			break;
+		default:
+			return -1;
+		}
+		break;
+	case 1:
+		switch(data->code) {
+		case 0x110:
+			if (data->value == 1)
+				buttons |= 1;
+			else
+				buttons &= ~1;
+			break;
+		case 0x111:
+			if (data->value == 1)
+				buttons |= shift_state & (1 << KG_SHIFT)? 2: 4;
+			else
+				buttons &= ~(shift_state & (1 << KG_SHIFT)? 2: 4);
+			break;
+		case 0x112:
+			if (data->value == 1)
+				buttons |= 2;
+			else
+				buttons &= ~2;
+			break;
+		default:
+			if (hidden)
+				return 0;
+			if (data->code > 0 && data->code <= nelem(code2key)) {
+				if (shift_state & (1 << KG_SHIFT))
+					key = code2key_shift[data->code-1];
+				else
+					key = code2key[data->code-1];
+				if (key == Kshift)
+					return -1;
+				kbdkey(key, data->value);
+				return 0;
+			}
+			switch(data->code) {
+			case 87:
+				kbdkey(KF|11, data->value);
+				break;
+			case 88:
+				kbdkey(KF|12, data->value);
+				break;
+			case 96:
+				kbdkey('\n', data->value);
+				break;
+			case 97:
+				kbdkey(Kctl, data->value);
+				break;
+			case 98:
+				kbdkey('/', data->value);
+				break;
+			case 100:
+				kbdkey(Kalt, data->value);
+				break;
+			case 102:
+				kbdkey(Khome, data->value);
+				break;
+			case 103:
+				kbdkey(Kup, data->value);
+				break;
+			case 104:
+				kbdkey(Kpgup, data->value);
+				break;
+			case 105:
+				kbdkey(Kleft, data->value);
+				break;
+			case 106:
+				kbdkey(Kright, data->value);
+				break;
+			case 107:
+				kbdkey(Kend, data->value);
+				break;
+			case 108:
+				kbdkey(Kdown, data->value);
+				break;
+			case 109:
+				kbdkey(Kpgdown, data->value);
+				break;
+			case 110:
+				kbdkey(Kins, data->value);
+				break;
+			case 111:
+				kbdkey(Kdel, data->value);
+				break;
+			}
+			return 0;
+		}
+		break;
+	default:
+		return -1;
+	}
+
+	if (mousexy.x < screenimage->r.min.x)
+		mousexy.x = screenimage->r.min.x;
+	if (mousexy.y < screenimage->r.min.y)
+		mousexy.y = screenimage->r.min.y;
+	if (mousexy.x > screenimage->r.max.x)
+		mousexy.x = screenimage->r.max.x;
+	if (mousexy.y > screenimage->r.max.y)
+		mousexy.y = screenimage->r.max.y;
+	
+	new.min = mousexy;
+	new.max = addpt(new.min, Pt(16, 16)); // size of cursor bitmap
+
+	combinerect(&new, old);
+	new.min = subpt(new.min, Pt(16, 16)); // to encompass any cursor->offset
+
+	qlock(&drawlock);
+	flushmemscreen(new);
+	qunlock(&drawlock);
+
+	if ((msec - lastmsec) < 10)
+		if (data->type != 1)
+			return 0;
+
+	lastmsec = msec;
+
+	absmousetrack(mousexy.x, mousexy.y, buttons, msec);
+
+	return 0;
+}
+
+void
+mouseset(Point p)
+{
+	qlock(&drawlock);
+	mousexy = p;
+	flushmemscreen(screenr);
+	qunlock(&drawlock);
+}
+
+void
+setcursor(void)
+{
+	qlock(&drawlock);
+	flushmemscreen(screenr);
+	qunlock(&drawlock);
+}
+
+void
+titlewrite(char* buf)
+{
+}
+
--- /dev/null
+++ b/kern/devaudio-alsa.c
@@ -1,0 +1,108 @@
+/*
+ * ALSA
+ */
+#include <alsa/asoundlib.h>
+#include	"u.h"
+#include	"lib.h"
+#include	"dat.h"
+#include	"fns.h"
+#include	"error.h"
+#include	"devaudio.h"
+
+enum
+{
+	Channels = 2,
+	Rate = 44100,
+	Bits = 16,
+};
+
+static snd_pcm_t *playback;
+static snd_pcm_t *capture;
+static int speed = Rate;
+
+/* maybe this should return -1 instead of sysfatal */
+void
+audiodevopen(void)
+{
+	if(snd_pcm_open(&playback, "default", SND_PCM_STREAM_PLAYBACK, 0) < 0)
+		error("snd_pcm_open playback");
+
+	if(snd_pcm_set_params(playback, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED, 2, speed, 1, 500000) < 0)
+		error("snd_pcm_set_params playback");
+
+	if(snd_pcm_prepare(playback) < 0)
+		error("snd_pcm_prepare playback");
+
+	if(snd_pcm_open(&capture, "default", SND_PCM_STREAM_CAPTURE, 0) < 0)
+		error("snd_pcm_open capture");
+
+	if(snd_pcm_set_params(capture, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED, 2, speed, 1, 500000) < 0)
+		error("snd_pcm_set_params capture");
+
+	if(snd_pcm_prepare(capture) < 0)
+		error("snd_pcm_prepare capture");
+}
+
+void
+audiodevclose(void)
+{
+	snd_pcm_drain(playback);
+	snd_pcm_close(playback);
+
+	snd_pcm_close(capture);
+}
+
+void
+audiodevsetvol(int what, int left, int right)
+{
+	if(what == Vspeed){
+		speed = left;
+		return;
+	}
+}
+
+void
+audiodevgetvol(int what, int *left, int *right)
+{
+	if(what == Vspeed){
+		*left = *right = speed;
+		return;
+	}
+
+	*left = *right = 100;
+}
+
+int
+audiodevwrite(void *v, int n)
+{
+	snd_pcm_sframes_t frames;
+	int tot, m;
+
+	for(tot = 0; tot < n; tot += m){
+		do {
+			frames = snd_pcm_writei(playback, v+tot, (n-tot)/4);
+		} while(frames == -EAGAIN);
+		if (frames < 0)
+			frames = snd_pcm_recover(playback, frames, 0);
+		if (frames < 0)
+			error((char*)snd_strerror(frames));
+		m = frames*4;
+	}
+
+	return tot;
+}
+
+int
+audiodevread(void *v, int n)
+{
+	snd_pcm_sframes_t frames;
+
+	do {
+		frames = snd_pcm_readi(capture, v, n/4);
+	} while(frames == -EAGAIN);
+
+	if (frames < 0)
+		error((char*)snd_strerror(frames));
+
+	return frames*4;
+}
--- a/main.c
+++ b/main.c
@@ -54,6 +54,7 @@
 	if(bind("#U", "/root", MREPL) < 0)
 		panic("bind #U: %r");
 	bind("#A", "/dev", MAFTER);
+	bind("#N", "/dev", MAFTER);
 	bind("#C", "/", MAFTER);
 
 	if(open("/dev/cons", OREAD) != 0)