]> git.neil.brown.name Git - plato.git/commitdiff
Add lots of stuff I had been keeping elsewhere
authorNeilBrown <neilb@suse.de>
Wed, 26 Dec 2012 23:14:56 +0000 (10:14 +1100)
committerNeilBrown <neilb@suse.de>
Wed, 26 Dec 2012 23:14:56 +0000 (10:14 +1100)
64 files changed:
alarm/alarm.c [new file with mode: 0644]
alarm/cal.c [new file with mode: 0644]
alarm/listsel.c [new file with mode: 0644]
alarm/listsel.h [new file with mode: 0644]
alarm/wkalrm.c [new file with mode: 0644]
contacts/contactdb.py [new file with mode: 0644]
contacts/contacts.py [new file with mode: 0644]
gps-utils/gpstime [new file with mode: 0755]
gps-utils/gpstz [new file with mode: 0755]
gps-utils/ubxgen [new file with mode: 0755]
gsm/BUG [new file with mode: 0644]
gsm/TOFIX [new file with mode: 0644]
gsm/atchan.py [new file with mode: 0644]
gsm/gsm-carriers.py [new file with mode: 0755]
gsm/gsm-data.py [new file with mode: 0644]
gsm/gsm-getsms.py [new file with mode: 0644]
gsm/gsm-sms.py [new file with mode: 0644]
gsm/gsmd-old [new file with mode: 0755]
gsm/gsmd.py [new file with mode: 0644]
gsm/notes [new file with mode: 0644]
icons/tapinput-dextr.png [new file with mode: 0644]
lib/tap3 [new file with mode: 0644]
lib/tapboard.py
lib/tapboard_dextr.py [new file with mode: 0644]
lib/wmctrl.py [new file with mode: 0644]
netman/dnsmasq.conf [new file with mode: 0644]
netman/interfaces [new file with mode: 0644]
netman/netman.py [new file with mode: 0644]
netman/sysctl.conf [new file with mode: 0644]
netman/usbnet [new file with mode: 0644]
netman/wifi-udhcpc.script [new file with mode: 0755]
netman/wifinet [new file with mode: 0644]
petrol/petrol.py [new file with mode: 0644]
plato/cmd.py [new file with mode: 0644]
plato/grouptypes.py [new file with mode: 0644]
plato/plato.py [new file with mode: 0644]
plato/plato_gsm.py [new file with mode: 0644]
plato/plato_internal.py [new file with mode: 0644]
plato/plato_settings.py [new file with mode: 0644]
plato/plato_sms.py [new file with mode: 0644]
plato/window_group.py [new file with mode: 0644]
scribble/Sample-Pages/1 [new file with mode: 0755]
scribble/Sample-Pages/2 [new file with mode: 0755]
scribble/Sample-Pages/3 [new file with mode: 0755]
scribble/scribble.desktop [new file with mode: 0644]
scribble/scribble.png [new file with mode: 0644]
scribble/scribble.py [new file with mode: 0755]
scribble/scribble/Makefile [new file with mode: 0644]
scribble/scribble/Sample-Pages/1 [new file with mode: 0755]
scribble/scribble/Sample-Pages/2 [new file with mode: 0755]
scribble/scribble/Sample-Pages/3 [new file with mode: 0755]
scribble/scribble/scribble.desktop [new file with mode: 0644]
scribble/scribble/scribble.png [new file with mode: 0644]
scribble/scribble/scribble.py [new file with mode: 0755]
shop/shop.py [new file with mode: 0755]
sms/exesms [new file with mode: 0644]
sms/sendsms.py [new file with mode: 0755]
sms/storesms.py [new file with mode: 0644]
sound/list.h [new file with mode: 0644]
sound/notes [new file with mode: 0644]
sound/sound.c [new file with mode: 0644]
utils/dialer.py [new file with mode: 0644]
utils/tapinput-dextr.py [new file with mode: 0644]
utils/tapinput.py

diff --git a/alarm/alarm.c b/alarm/alarm.c
new file mode 100644 (file)
index 0000000..c5b9403
--- /dev/null
@@ -0,0 +1,275 @@
+
+/*
+ * generate alarm messages as required.
+ * Alarm information is stored in /data/alarms
+ * format is:
+ *   date:recurrence:message
+ *
+ * date is yyyy-mm-dd-hh-mm-ss
+ *  recurrence is currently nndays
+ * message is any text
+ *
+ * When the time comes, a txt message is generated
+ *
+ * We use dnotify to watch the file
+ *
+ * We never report any event that is before the timestamp
+ * on the file - that guards against replaying lots of
+ * events if the clock gets set backwards.
+ */
+
+#define _XOPEN_SOURCE
+#define _BSD_SOURCE
+#define _GNU_SOURCE
+#include <unistd.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <fcntl.h>
+#include <time.h>
+#include <string.h>
+#include <stdio.h>
+#include <sys/ioctl.h>
+#include <linux/rtc.h>
+#include <sys/dir.h>
+#include <event.h>
+#include "libsus.h"
+
+struct alarm_ev {
+       struct alarm_ev *next;
+       time_t when;
+       long recur;
+       char *mesg;
+};
+
+void alarm_free(struct alarm_ev *ev)
+{
+       while (ev) {
+               struct alarm_ev *n = ev->next;
+               free(ev->mesg);
+               free(ev);
+               ev = n;
+       }
+}
+
+void update_event(struct alarm_ev *ev, time_t now)
+{
+       /* make sure 'when' is in the future if possible */
+       while (ev->when < now && ev->recur > 0)
+               ev->when += ev->recur;
+}
+
+struct alarm_ev *event_parse(char *line)
+{
+       struct alarm_ev *rv;
+       char *recur, *mesg;
+       struct tm tm;
+       long rsec;
+       recur = strchr(line, ':');
+       if (!recur)
+               return NULL;
+       *recur++ = 0;
+       mesg = strchr(recur, ':');
+       if (!mesg)
+               return NULL;
+       *mesg++ = 0;
+       line = strptime(line, "%Y-%m-%d-%H-%M-%S", &tm);
+       if (!line)
+               return NULL;
+       rsec = atoi(recur);
+       if (rsec < 0)
+               return NULL;
+       rv = malloc(sizeof(*rv));
+       rv->next = NULL;
+       tm.tm_isdst = -1;
+       rv->when = mktime(&tm);
+       rv->recur = rsec;
+       rv->mesg = strdup(mesg);
+       return rv;
+}
+
+struct alarm_ev *event_load_soonest(char *file, time_t now)
+{
+       /* load the file and return only the soonest event at or after now */
+       char line[2048];
+       struct stat stb;
+       struct alarm_ev *rv = NULL, *ev;
+       FILE *f;
+
+       f = fopen(file, "r");
+       if (!f)
+               return NULL;
+       if (fstat(fileno(f), &stb) == 0 &&
+           stb.st_mtime > now)
+               now = stb.st_mtime;
+
+       while (fgets(line, sizeof(line), f) != NULL) {
+               char *eol = line + strlen(line);
+               if (eol > line && eol[-1] == '\n')
+                       eol[-1] = 0;
+
+               ev = event_parse(line);
+               if (!ev)
+                       continue;
+               update_event(ev, now);
+               if (ev->when < now) {
+                       alarm_free(ev);
+                       continue;
+               }
+               if (rv == NULL) {
+                       rv = ev;
+                       continue;
+               }
+               if (rv->when < ev->when) {
+                       alarm_free(ev);
+                       continue;
+               }
+               alarm_free(rv);
+               rv = ev;
+       }
+       fclose(f);
+       return rv;
+}
+
+struct alarm_ev *event_load_soonest_dir(char *dir, time_t now)
+{
+       DIR *d = opendir(dir);
+       struct dirent *de;
+       struct alarm_ev *rv = NULL;
+
+       if (!d)
+               return NULL;
+       while ((de = readdir(d)) != NULL) {
+               char *p = NULL;
+               struct alarm_ev *e;
+               if (de->d_ino == 0)
+                       continue;
+               if (de->d_name[0] == '.')
+                       continue;
+               asprintf(&p, "%s/%s", dir, de->d_name);
+               e = event_load_soonest(p, now);
+               free(p);
+               if (e == NULL)
+                       ;
+               else if (rv == NULL)
+                       rv = e;
+               else if (rv->when < e->when)
+                       alarm_free(e);
+               else {
+                       alarm_free(rv);
+                       rv = e;
+               }
+       }
+       closedir(d);
+       return rv;
+}
+
+void event_deliver(struct alarm_ev *ev)
+{
+       char line[2048];
+       char file[1024];
+       struct tm *tm;
+       char *z;
+       char *m;
+       int f;
+       static time_t last_ev;
+       time_t now;
+
+       if (!ev->mesg || !ev->mesg[0])
+               return;
+       time(&now);
+       if (now <= last_ev)
+               now = last_ev + 1;
+       last_ev = now;
+       tm = gmtime(&now);
+       strftime(line, 2048, "%Y%m%d-%H%M%S", tm);
+       tm = localtime(&ev->when);
+       strftime(line+strlen(line), 1024, " ALARM %Y%m%d-%H%M%S", tm);
+       z = line + strlen(line);
+       sprintf(z, "%+03d alarm ", tm->tm_gmtoff/60/15);
+
+       z = line + strlen(line);
+       m = ev->mesg;
+
+       while (*m) {
+               switch (*m) {
+               default:
+                       *z++ = *m;
+                       break;
+               case ' ':
+               case '\t':
+               case '\n':
+                       sprintf(z, "%%%02x", *m);
+                       z += 3;
+                       break;
+               }
+               m++;
+       }
+       *z++ = '\n';
+       *z = 0;
+
+       sprintf(file, "/data/SMS/%.6s", line);
+       f = open(file, O_WRONLY | O_APPEND | O_CREAT, 0600);
+       if (f >= 0) {
+               write(f, line, strlen(line));
+               close(f);
+               f = open("/data/SMS/newmesg", O_WRONLY | O_APPEND | O_CREAT,
+                        0600);
+               if (f >= 0) {
+                       line[16] = '\n';
+                       write(f, line, 17);
+                       close(f);
+               }
+               f = open("/var/run/alert/alarm", O_WRONLY | O_CREAT, 0600);
+               if (f) {
+                       write(f, "new\n", 4);
+                       close(f);
+               }
+               /* Abort any current suspend cycle */
+               suspend_abort(-1);
+       }
+}
+
+
+time_t now;
+struct event *wkev;
+int efd;
+static void check_alarms(int fd, short ev, void *vp)
+{
+       struct alarm_ev *evt = event_load_soonest_dir("/data/alarms", now);
+       if (wkev && !vp)
+               wakealarm_destroy(wkev);
+       wkev = NULL;
+       while ((evt = event_load_soonest_dir("/data/alarms", now)) != NULL
+              && evt->when <= time(0)) {
+               event_deliver(evt);
+               now = evt->when + 1;
+               alarm_free(evt);
+       }
+       if (!evt)
+               return;
+       wkev = wakealarm_set(evt->when, check_alarms, (void*)1);
+       alarm_free(evt);
+       fcntl(efd, F_NOTIFY, DN_MODIFY|DN_CREATE|DN_RENAME);
+}
+
+main(int argc, char *argv[])
+{
+       struct event ev;
+       sigset_t set;
+
+       efd = open("/data/alarms", O_DIRECTORY|O_RDONLY);
+       if (efd < 0)
+               exit(1);
+
+       event_init();
+       signal_set(&ev, SIGIO, check_alarms, NULL);
+       signal_add(&ev, NULL);
+
+       fcntl(efd, F_NOTIFY, DN_MODIFY|DN_CREATE|DN_RENAME);
+
+       time(&now);
+       check_alarms(0, 0, NULL);
+
+       event_loop(0);
+       exit(0);
+}
diff --git a/alarm/cal.c b/alarm/cal.c
new file mode 100644 (file)
index 0000000..3eeff79
--- /dev/null
@@ -0,0 +1,1309 @@
+#define _XOPEN_SOURCE
+#define _GNU_SOURCE
+
+#include <unistd.h>
+#include <stdlib.h>
+
+#include <string.h>
+#include <malloc.h>
+#include <math.h>
+#include <time.h>
+
+#include <gtk/gtk.h>
+
+#include "listsel.h"
+
+struct event {
+       struct event *next;
+       time_t when, first;
+       long recur;
+       char *mesg;
+};
+
+
+char *days[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
+
+GtkWidget *calblist[42], *month, *date_display, *date_time_display;
+GtkWidget *window, *clockw, *cal, *reasonw, *timerw;
+GtkWidget *browse_buttons, *event_buttons, *move_buttons, *move_event;
+GtkWidget *timer_display, *time_display;
+GtkWidget *today_btn, *undelete_btn;
+GtkWidget *once_btn, *daily_btn, *weekly_btn, *freq_buttons;
+GtkWidget *timers_list;
+GtkTextBuffer *reason_buffer;
+time_t dlist[42], chosen_date;
+int prev_mday, hour, minute;
+int freq;
+
+struct event *evlist, *active_event;
+struct event *deleted_event;
+struct list_entry_text *alarm_entries;
+time_t alarm_date = 0;
+int evcnt = -1;
+int moving = 0;
+struct sellist *alarm_selector;
+
+char *filename = NULL;
+
+PangoLayout *layout;
+GdkGC *colour = NULL;
+
+int size(struct list_entry *i, int *width, int*height)
+{
+       PangoRectangle ink, log;
+       struct list_entry_text *item = (void*)i;
+
+       if (i->height) {
+               *width = item->true_width;
+               *height = i->height;
+               return 0;
+       }
+       pango_layout_set_text(layout, item->text, -1);
+       pango_layout_get_extents(layout, &ink, &log);
+       *width = log.width / PANGO_SCALE;
+       *height = log.height / PANGO_SCALE;
+       item->true_width = i->width = *width;
+       i->height = *height;
+       return 0;
+}
+
+int render(struct list_entry *i, int selected, GtkWidget *d)
+{
+       PangoRectangle ink, log;
+       struct list_entry_text *item = (void*)i;
+       int x;
+       GdkColor col;
+
+       pango_layout_set_text(layout, item->text, -1);
+
+       if (colour == NULL) {
+               colour = gdk_gc_new(gtk_widget_get_window(d));
+               gdk_color_parse("purple", &col);
+               gdk_gc_set_rgb_fg_color(colour, &col);
+       }
+       if (selected) {
+               gdk_color_parse("pink", &col);
+               gdk_gc_set_rgb_fg_color(colour, &col);
+               gdk_draw_rectangle(gtk_widget_get_window(d),
+                                  colour, TRUE,
+                                  item->head.x, item->head.y,
+                                  item->head.width, item->head.height);
+               gdk_color_parse("purple", &col);
+               gdk_gc_set_rgb_fg_color(colour, &col);
+       }
+#if 0
+       x = (i->width - item->true_width)/2;
+       if (x < 0)
+               x = 0;
+#else
+       x = 0;
+#endif
+       gdk_draw_layout(gtk_widget_get_window(d),
+                       colour,
+                       item->head.x+x, item->head.y, layout);
+       return 0;
+}
+
+
+
+void set_cal(time_t then);
+
+void set_date(GtkButton *button, int num)
+{
+       set_cal(dlist[num]);
+}
+
+void prev_year(GtkButton *button)
+{
+       set_cal(chosen_date - 365*24*3600);
+}
+void next_year(GtkButton *button)
+{
+       set_cal(chosen_date + 365*24*3600);
+}
+
+void finish(void)
+{
+       gtk_main_quit();
+}
+
+void set_time(GtkButton *button, int num)
+{
+       struct tm now;
+       time_t then;
+       char buf[100];
+       int hr24, hr12;
+       char m = 'A';
+       if (num >= 100)
+               hour = num/100;
+       else
+               minute = num;
+
+       hr24 = hour;
+       if (hour == 24)
+               hr24 = 0;
+       hr12 = hour;
+       if (hr12 > 12) {
+               hr12 -= 12;
+               m = 'P';
+       }
+       if (hr12 == 12)
+               m = 'P' + 'A' - m;
+
+       then = chosen_date + hour * 3600 + minute * 60;
+       localtime_r(&then, &now);
+       strftime(buf, sizeof(buf), "%a, %d %B %Y, %H:%M", &now);
+       gtk_label_set_text(GTK_LABEL(date_display), buf);
+       gtk_label_set_text(GTK_LABEL(date_time_display), buf);
+#if 0
+       sprintf(buf, "%02d:%02dhrs\n%2d:%02d%cM", hr24, minute,
+               hr12, minute, m);
+       gtk_label_set_text(GTK_LABEL(time_label), buf);
+#endif
+}
+
+GtkWidget *add_button(GtkWidget **blist, char *name,
+               PangoFontDescription *fd,
+               GCallback handle)
+{
+       GtkWidget *l, *b;
+       if (*blist == NULL) {
+               *blist = gtk_hbox_new(TRUE, 0);
+               gtk_widget_show(*blist);
+               gtk_widget_set_size_request(*blist, -1, 80);
+       }
+       
+       l = *blist;
+
+       b = gtk_button_new_with_label(name);
+       gtk_widget_show(b);
+       pango_font_description_set_size(fd, 12 * PANGO_SCALE);
+       gtk_widget_modify_font(GTK_BIN(b)->child, fd);
+       gtk_container_add(GTK_CONTAINER(l), b);
+       g_signal_connect((gpointer)b, "clicked", handle, NULL);
+       return b;
+}
+int delay = 0;
+int show_time(void *d);
+int timer_tick = -1;
+void add_time(int mins);
+
+void load_timers(void);
+void select_timer(void)
+{
+       gtk_container_remove(GTK_CONTAINER(window), cal);
+       show_time(NULL);
+       delay = 0;
+       add_time(0);
+       load_timers();
+       gtk_container_add(GTK_CONTAINER(window), timerw);
+       timer_tick = g_timeout_add(1000, show_time, NULL);
+}
+
+void events_select(void)
+{
+       /* the selected event becomes current and can be moved */
+       if (!active_event)
+               return;
+       set_cal(active_event->when);
+}
+
+void move_confirm(void)
+{
+       struct tm now;
+       time_t then;
+       char buf[100];
+
+       if (active_event) {
+               then = active_event->when;
+               localtime_r(&then, &now);
+               hour = now.tm_hour;
+               minute = now.tm_min;
+       } else
+               hour = minute = 0;
+       then = chosen_date + hour * 3600 + minute * 60;
+       localtime_r(&then, &now);
+       strftime(buf, sizeof(buf), "%a, %d %B %Y, %H:%M", &now);
+       gtk_label_set_text(GTK_LABEL(date_display), buf);
+       gtk_label_set_text(GTK_LABEL(date_time_display), buf);
+
+       gtk_container_remove(GTK_CONTAINER(window), cal);
+       gtk_container_add(GTK_CONTAINER(window), clockw);
+}
+
+void update_freq(void);
+void events_move(void)
+{
+       if (!active_event)
+               return;
+
+       moving = 1;
+       freq = active_event->recur;
+       set_cal(active_event->when);
+
+       gtk_widget_hide(event_buttons);
+       gtk_widget_show(move_buttons);
+       update_freq();
+       gtk_widget_show(freq_buttons);
+
+       gtk_widget_hide(alarm_selector->drawing);
+       gtk_widget_show(move_event);
+
+       gtk_text_buffer_set_text(reason_buffer, active_event->mesg, -1);
+}
+
+void events_new(void)
+{
+       moving = 2;
+       freq = 0;
+       gtk_widget_hide(event_buttons);
+       gtk_widget_hide(browse_buttons);
+       gtk_widget_show(move_buttons);
+       update_freq();
+       gtk_widget_show(freq_buttons);
+
+       gtk_widget_hide(alarm_selector->drawing);
+       gtk_widget_show(move_event);
+       gtk_text_buffer_set_text(reason_buffer, "", -1);
+}
+
+void move_abort(void)
+{
+       gtk_widget_hide(move_buttons);
+       gtk_widget_hide(freq_buttons);
+       if (active_event) {
+               gtk_widget_hide(browse_buttons);
+               gtk_widget_show(event_buttons);
+       } else {
+               gtk_widget_hide(event_buttons);
+               gtk_widget_show(browse_buttons);
+       }
+
+       gtk_widget_show(alarm_selector->drawing);
+       gtk_widget_hide(move_event);
+       moving = 0;
+       if (active_event)
+               set_cal(active_event->when);
+       else
+               set_cal(chosen_date);
+
+}
+
+void cal_today(void)
+{
+       /* jump to today */
+       time_t now;
+       time(&now);
+       set_cal(now);
+       if (deleted_event) {
+               gtk_widget_hide(today_btn);
+               gtk_widget_show(undelete_btn);
+       }
+}
+
+void cal_restore(void)
+{
+       gtk_container_remove(GTK_CONTAINER(window), clockw);
+       gtk_container_add(GTK_CONTAINER(window), cal);
+}
+
+void reason_confirm(void);
+void clock_confirm(void)
+{
+       gtk_container_remove(GTK_CONTAINER(window), clockw);
+       gtk_container_add(GTK_CONTAINER(window), reasonw);
+       reason_confirm();
+}
+
+void update_freq(void)
+{
+       if (freq == 0)
+               gtk_label_set_markup(GTK_LABEL(GTK_BIN(once_btn)->child), "<span background=\"pink\">Once</span>");
+       else
+               gtk_label_set_markup(GTK_LABEL(GTK_BIN(once_btn)->child), "(Once)");
+
+       if (freq == 1*24*3600)
+               gtk_label_set_markup(GTK_LABEL(GTK_BIN(daily_btn)->child), "<span background=\"pink\">Daily</span>");
+       else if (freq == 2*24*3600)
+               gtk_label_set_markup(GTK_LABEL(GTK_BIN(daily_btn)->child), "<span background=\"pink\">2-Daily</span>");
+       else if (freq == 3*24*3600)
+               gtk_label_set_markup(GTK_LABEL(GTK_BIN(daily_btn)->child), "<span background=\"pink\">3-Daily</span>");
+       else
+               gtk_label_set_markup(GTK_LABEL(GTK_BIN(daily_btn)->child), "(Daily)");
+
+       if (freq == 7*24*3600)
+               gtk_label_set_markup(GTK_LABEL(GTK_BIN(weekly_btn)->child), "<span background=\"pink\">Weekly</span>");
+       else if (freq == 14*24*3600)
+               gtk_label_set_markup(GTK_LABEL(GTK_BIN(weekly_btn)->child), "<span background=\"pink\">Fortnightly</span>");
+       else
+               gtk_label_set_markup(GTK_LABEL(GTK_BIN(weekly_btn)->child), "(Weekly)");
+}
+void set_once(void)
+{
+       freq = 0;
+       update_freq();
+}
+void set_weekly(void)
+{
+       if (freq == 7 * 24 * 3600)
+               freq = 14 *24 * 3600;
+       else
+               freq = 7 * 24 * 3600;
+       update_freq();
+}
+void set_daily(void)
+{
+       if (freq == 1 * 24 * 3600)
+               freq = 2 * 24 * 3600;
+       else if (freq == 2 *24 * 3600)
+               freq = 3 * 24 * 3600;
+       else
+               freq = 1 * 24 * 3600;
+       update_freq();
+}
+
+/********************************************************************/
+
+
+void event_free(struct event *ev)
+{
+       while (ev) {
+               struct event *n = ev->next;
+               free(ev->mesg);
+               free(ev);
+               ev = n;
+       }
+}
+
+void update_event(struct event *ev, time_t now)
+{
+       /* make sure 'when' is in the future if possible */
+       ev->when = ev->first;
+       while (ev->when < now && ev->recur > 0)
+               ev->when += ev->recur;
+}
+
+void update_events(struct event *ev, time_t now)
+{
+       while (ev) {
+               update_event(ev, now);
+               ev = ev->next;
+       }
+}
+
+void sort_events(struct event **evtp)
+{
+       /* sort events list in *evtp by ->when
+        * use merge sort
+        */
+       int cnt=0;
+
+       struct event *ev[2];
+       ev[0] = *evtp;
+       ev[1] = NULL;
+
+       do {
+               struct event **evp[2], *e[2];
+               int current = 0;
+               time_t prev = 0;
+               int next = 0;
+               cnt++;
+               evp[0] = &ev[0];
+               evp[1] = &ev[1];
+               e[0] = ev[0];
+               e[1] = ev[1];
+
+               /* take least of e[0] and e[1]
+                * if it is larger than prev, add to
+                * evp[current], else swap current then add
+                */
+               while (e[0] || e[1]) {
+                       if (e[next] == NULL ||
+                           (e[1-next] != NULL && 
+                            !((prev <= e[1-next]->when)
+                              ^(e[1-next]->when <= e[next]->when)
+                              ^(e[next]->when <= prev)))
+                               )
+                               next = 1 - next;
+
+                       if (e[next]->when < prev)
+                               current = 1 - current;
+                       prev = e[next]->when;
+                       *evp[current] = e[next];
+                       evp[current] = &e[next]->next;
+                       e[next] = e[next]->next;
+               }
+               *evp[0] = NULL;
+               *evp[1] = NULL;
+       } while (ev[0] && ev[1]);
+       if (ev[0])
+               *evtp = ev[0];
+       else
+               *evtp = ev[1];
+}
+
+struct event *event_parse(char *line)
+{
+       struct event *rv;
+       char *recur, *mesg;
+       struct tm tm;
+       long rsec;
+       recur = strchr(line, ':');
+       if (!recur)
+               return NULL;
+       *recur++ = 0;
+       mesg = strchr(recur, ':');
+       if (!mesg)
+               return NULL;
+       *mesg++ = 0;
+       line = strptime(line, "%Y-%m-%d-%H-%M-%S", &tm);
+       if (!line)
+               return NULL;
+       rsec = atoi(recur);
+       if (rsec < 0)
+               return NULL;
+       rv = malloc(sizeof(*rv));
+       rv->next = NULL;
+       tm.tm_isdst = -1;
+       rv->when = mktime(&tm);
+       rv->first = rv->when;
+       rv->recur = rsec;
+       rv->mesg = strdup(mesg);
+       return rv;
+}
+
+struct event *event_load_all(char *file)
+{
+       /* load the file and return a linked list */
+       char line[2048];
+       struct event *rv = NULL, *ev;
+       FILE *f;
+
+       f = fopen(file, "r");
+       if (!f)
+               return NULL;
+       while (fgets(line, sizeof(line), f) != NULL) {
+               char *eol = line + strlen(line);
+               if (eol > line && eol[-1] == '\n')
+                       eol[-1] = 0;
+
+               ev = event_parse(line);
+               if (!ev)
+                       continue;
+               ev->next = rv;
+               rv = ev;
+       }
+       return rv;
+}
+
+void event_save_all(char *file, struct event *list)
+{
+       FILE *f;
+       char *tmp = strdup(file);
+       char *c;
+
+       c = tmp + strlen(tmp);
+       while (c > tmp && c[-1] != '/')
+               c--;
+       if (*c) *c = '.';
+
+       f = fopen(tmp, "w");
+       while (list) {
+               struct event *ev = list;
+               struct tm *tm;
+               char buf[100];
+               list = ev->next;
+
+               tm = localtime(&ev->first);
+               strftime(buf, sizeof(buf), "%Y-%m-%d-%H-%M-%S", tm);
+               fprintf(f, "%s:%d:%s\n", buf, ev->recur, ev->mesg);
+       }
+       fflush(f);
+       fsync(fileno(f));
+       fclose(f);
+       rename(tmp, file);
+}
+
+
+void load_timers(void)
+{
+       time_t now = time(0);
+       struct event *timers = event_load_all("/data/alarms/timer");
+       struct event *t;
+       char buf[1024];
+       int len = 0;
+
+       update_events(timers, now);
+       sort_events(&timers);
+       strcpy(buf,"");
+       for (t = timers ; t; t = t->next) {
+               struct tm *tm;
+               if (t->when < now)
+                       continue;
+               tm = localtime(&t->when);
+               strftime(buf+len, sizeof(buf)-len, "%H:%M\n", tm);
+               len += strlen(buf+len);
+       }
+       event_free(timers);
+       gtk_label_set_text(GTK_LABEL(timers_list), buf);
+}
+
+
+struct list_entry *alarms_item(void *list, int n)
+{
+       struct event *ev;
+
+       if (alarm_date != chosen_date) {
+               int i;
+               struct tm *tm, today;
+
+               if (evlist == NULL) {
+                       filename = "/home/neilb/ALARMS";
+                       evlist = event_load_all(filename);
+                       if (evlist == NULL) {
+                               filename = "/data/alarms/cal";
+                               evlist = event_load_all(filename);
+                       }
+               }
+               update_events(evlist, chosen_date);
+               sort_events(&evlist);
+               evcnt = 0;
+               ev = evlist;
+               while (ev && ev->when < chosen_date)
+                       ev = ev->next;
+               for ( ; ev ; ev = ev->next)
+                       evcnt++;
+               free(alarm_entries);
+               alarm_entries = calloc(evcnt, sizeof(alarm_entries[0]));
+               ev = evlist;
+               while (ev && ev->when < chosen_date)
+                       ev = ev->next;
+               tm = localtime(&chosen_date);
+               today = *tm;
+               for (i=0; ev ; i++, ev = ev->next) {
+                       char buf[50], o, c, r;
+                       struct tm *tm = localtime(&ev->when);
+                       if (tm->tm_mday == today.tm_mday &&
+                           tm->tm_mon == today.tm_mon &&
+                           tm->tm_year == today.tm_year)
+                               strftime(buf, 50, "%H:%M\t", tm);
+                       else if (tm->tm_year == today.tm_year ||
+                                (tm->tm_year == today.tm_year + 1 &&
+                                 tm->tm_mon < today.tm_mon)
+                               )
+                               strftime(buf, 50, "%b%d\t", tm);
+                       else
+                               strftime(buf, 50, "%Y\t", tm);
+
+                       o = ' ';c=' ';
+                       if (ev->when < time(0)) {
+                               o = '(';
+                               c = ')';
+                       }
+                       if (ev->recur == 0)
+                               r = ' ';
+                       else if (ev->recur == 3600*24*1)
+                               r = '1'; /* daily */
+                       else if (ev->recur == 3600*24*2)
+                               r = '2'; /* 2-daily */
+                       else if (ev->recur == 3600*24*3)
+                               r = '3'; /* 3-daily */
+                       else if (ev->recur == 3600*24*7)
+                               r = '+'; /* weekly */
+                       else if (ev->recur == 3600*24*14)
+                               r = '*'; /* fortnightly */
+                       else
+                               r = '#'; /* other period */
+                       asprintf(&alarm_entries[i].text, "%c%c %s %s%c",
+                                o, r,
+                                buf, ev->mesg, c);
+                       alarm_entries[i].bg = "white";
+                       alarm_entries[i].fg = "blue";
+                       alarm_entries[i].underline = 0;
+               }
+               alarm_date = chosen_date;
+       }
+       if (n >= evcnt)
+               return NULL;
+
+       return &alarm_entries[n].head;
+}
+
+
+void events_delete(void)
+{
+       struct event **evp;
+       if (!active_event)
+               return;
+
+       for (evp = &evlist; *evp; evp = &(*evp)->next) {
+               if (*evp != active_event)
+                       continue;
+               *evp = active_event->next;
+               break;
+       }
+       if (deleted_event) {
+               free(deleted_event->mesg);
+               free(deleted_event);
+       }
+       deleted_event = active_event;
+       active_event = NULL;
+       event_save_all(filename, evlist);
+       alarm_date = 0;
+       move_abort();
+}
+
+void events_undelete(void)
+{
+       if (!deleted_event)
+               return;
+       active_event = deleted_event;
+       deleted_event = NULL;
+       active_event->next = evlist;
+       evlist = active_event;
+       event_save_all(filename, evlist);
+       alarm_date = 0;
+       move_abort();
+}
+
+void alarms_selected(void *list, int s)
+{
+       struct event *e;
+
+
+       if (s < 0 || s >= evcnt)
+               return;
+       e = evlist;
+       while (e && e->when < chosen_date)
+               e = e->next;
+       while (s && e) {
+               s--;
+               e = e->next;
+       }
+       active_event = e;
+
+       gtk_widget_hide(browse_buttons);
+       gtk_widget_show(event_buttons);
+}
+
+struct list_handlers alarms_han = {
+       .getitem = alarms_item,
+       .get_size = size,
+       .render = render,
+       .selected = alarms_selected,
+};
+
+
+GtkWidget *create_cal_window(void)
+{
+       GtkWidget *v, *t, *l;
+       GtkWidget *h;
+       GtkWidget *blist = NULL;
+       int i,r,c;
+       PangoFontDescription *desc;
+       struct sellist *sl;
+
+       desc = pango_font_description_new();
+       pango_font_description_set_size(desc, 11 * PANGO_SCALE);
+
+       v = gtk_vbox_new(FALSE, 0);
+       gtk_widget_show(v);
+
+       h = gtk_hbox_new(FALSE, 0);
+       gtk_container_add_with_properties(GTK_CONTAINER(v), h, "expand", 0, NULL);
+       gtk_widget_show(h);
+
+       l = gtk_button_new_with_label(" << ");
+       gtk_widget_modify_font(GTK_BIN(l)->child, desc);
+       gtk_button_set_relief(GTK_BUTTON(l), GTK_RELIEF_NONE);
+       g_signal_connect((gpointer)l, "clicked",
+                        G_CALLBACK(prev_year), NULL);
+       gtk_container_add(GTK_CONTAINER(h), l);
+       gtk_widget_show(l);
+       gtk_widget_set(l, "can-focus", 0, NULL);
+
+       l = gtk_label_new("Month 9999");
+       gtk_widget_modify_font(l, desc);
+       gtk_container_add(GTK_CONTAINER(h), l);
+       gtk_widget_show(l);
+       month = l;
+
+
+       l = gtk_button_new_with_label(" >> ");
+       gtk_widget_modify_font(GTK_BIN(l)->child, desc);
+       gtk_button_set_relief(GTK_BUTTON(l), GTK_RELIEF_NONE);
+       g_signal_connect((gpointer)l, "clicked",
+                        G_CALLBACK(next_year), NULL);
+       gtk_container_add(GTK_CONTAINER(h), l);
+       gtk_widget_show(l);
+       gtk_widget_set(l, "can-focus", 0, NULL);
+
+
+       t = gtk_table_new(7, 7, FALSE);
+       gtk_widget_show(t);
+
+       gtk_container_add_with_properties(GTK_CONTAINER(v), t, "expand", 0, NULL);
+
+       for (i=0; i<7; i++) {
+               l = gtk_label_new(days[i]);
+               gtk_widget_modify_font(l, desc);
+               gtk_widget_show(l);
+               gtk_table_attach(GTK_TABLE(t), l, i, i+1, 0, 1, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0);
+       }
+
+       for (r=1; r<7; r++)
+               for (c=0; c<7; c++) {
+                       int p = (r-1)*7+c;
+                       l = gtk_button_new_with_label(" 99 ");
+                       gtk_widget_modify_font(GTK_BIN(l)->child, desc);
+                       gtk_button_set_relief(GTK_BUTTON(l), GTK_RELIEF_NONE);
+                       gtk_table_attach(GTK_TABLE(t), l, c,c+1, r,r+1, GTK_FILL, GTK_FILL, 0, 0);
+                       gtk_widget_show(l);
+                       g_signal_connect((gpointer)l, "clicked",
+                                        G_CALLBACK(set_date), (void*)(long)p);
+                       calblist[p] = l;
+                       dlist[p] = 0;
+               }
+
+       /* list of alarms from this date */
+       sl = listsel_new(NULL, &alarms_han);
+       pango_font_description_set_size(desc, 15 * PANGO_SCALE);
+       gtk_widget_modify_font(sl->drawing, desc);
+       gtk_container_add(GTK_CONTAINER(v), sl->drawing);
+       gtk_widget_show(sl->drawing);
+       alarm_selector = sl;
+
+       l = gtk_text_view_new_with_buffer(reason_buffer);
+       gtk_widget_modify_font(l, desc);
+       gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(l), GTK_WRAP_WORD_CHAR);
+       gtk_widget_hide(l);
+       gtk_container_add(GTK_CONTAINER(v), l);
+       move_event = l;
+
+
+       add_button(&blist, "timer", desc, select_timer);
+       add_button(&blist, "new", desc, events_new);
+       today_btn = add_button(&blist, "today", desc, cal_today);
+       undelete_btn = add_button(&blist, "undelete", desc, events_undelete);
+       gtk_widget_hide(undelete_btn);
+       gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0);
+       browse_buttons = blist;
+       blist = NULL;
+
+       add_button(&blist, "go to", desc, events_select);
+       add_button(&blist, "change", desc, events_move);
+       add_button(&blist, "delete", desc, events_delete);
+       gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0);
+       event_buttons = blist;
+       blist = NULL;
+
+       add_button(&blist, "confirm", desc, move_confirm);
+       add_button(&blist, "abort", desc, move_abort);
+       gtk_widget_hide(blist);
+       gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0);
+
+       move_buttons = blist;
+
+       blist = NULL;
+       once_btn = add_button(&blist, "Once", desc, set_once);
+       daily_btn = add_button(&blist, "(Daily)", desc, set_daily);
+       weekly_btn = add_button(&blist, "(Weekly)", desc, set_weekly);
+       gtk_widget_hide(blist);
+       gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0);
+       freq_buttons = blist;
+
+       return v;
+}
+
+void set_cal(time_t then)
+{
+       struct tm now, first, today, *tm;
+       int d, x;
+       time_t today_s;
+       char buf[400];
+
+       time(&today_s);
+       localtime_r(&today_s, &today);
+       localtime_r(&then, &now);
+
+       then -= now.tm_sec;
+       then -= now.tm_min*60;
+       then -= now.tm_hour*3600;
+       chosen_date = then;
+
+       localtime_r(&then, &now);
+
+       tm = localtime(&then);
+
+       strftime(buf, sizeof(buf), "%a, %d %B %Y", &now);
+       gtk_label_set_text(GTK_LABEL(date_display), buf);
+       
+       /* previous month */
+       while (tm->tm_mon == now.tm_mon) {
+               then -= 22*3600;
+               tm = localtime(&then);
+       }
+       /* Start of week */
+       while (tm->tm_wday != 0) {
+               then -= 22*3600;
+               tm = localtime(&then);
+       }
+       first = *tm;
+
+       if (abs(dlist[0] - then) > 48*3600) {
+               strftime(buf, 40, "%B %Y", &now);
+               gtk_label_set_text(GTK_LABEL(month), buf);
+       }
+
+       for (d=0; d<42; d++) {
+               char *bg = "", *fg = "black", *today_fg = "blue";
+
+               if (tm->tm_mon != now.tm_mon) {
+                       fg = "grey"; today_fg = "pink";
+               } else if (tm->tm_mday == now.tm_mday)
+                       bg = "background=\"green\"";
+
+               if (tm->tm_year == today.tm_year &&
+                   tm->tm_mon == today.tm_mon &&
+                   tm->tm_mday == today.tm_mday)
+                       fg = today_fg;
+
+               sprintf(buf, "<span %s foreground=\"%s\"> %02d </span>",
+                       bg, fg, tm->tm_mday);
+               if (abs(dlist[d] - then) > 48*3600 ||
+                   tm->tm_mday == now.tm_mday ||
+                   tm->tm_mday == prev_mday)
+                       gtk_label_set_markup(GTK_LABEL(GTK_BIN(calblist[d])->child), buf);
+               dlist[d] = then;
+               x = tm->tm_mday;
+               while (x == tm->tm_mday) {
+                       then += 22*3600;
+                       tm = localtime(&then);
+               }
+       }
+       prev_mday = now.tm_mday;
+
+       alarm_selector->selected = -1;
+       if (!moving) {
+               active_event = NULL;
+               gtk_widget_hide(event_buttons);
+               gtk_widget_show(browse_buttons);
+       }
+       g_signal_emit_by_name(alarm_selector->drawing, "configure-event",
+                             NULL, alarm_selector);
+
+
+       gtk_widget_show(today_btn);
+       gtk_widget_hide(undelete_btn);
+}
+
+void center_me(GtkWidget *label, void *xx, int pos)
+{
+       /* I have just been realised.  Find size and
+        * adjust position
+        */
+       GtkWidget *button = gtk_widget_get_parent(label);
+       GtkWidget *parent = gtk_widget_get_parent(button);
+       GtkAllocation alloc;
+       int x = pos / 10000;
+       int y = pos % 10000;
+
+       gtk_widget_get_allocation(button, &alloc);
+       printf("move %d %d/%d by %d/%d from %d/%d\n", pos, x, y,
+              alloc.width/2, alloc.height/2, alloc.x, alloc.y);
+       x -= alloc.width / 2;
+       y -= alloc.height / 2;
+       if (x != alloc.x || y != alloc.y) {
+       printf("move %d %d/%d by %d/%d from %d/%d\n", pos, x, y,
+              alloc.width/2, alloc.height/2, alloc.x, alloc.y);
+               gtk_fixed_move(GTK_FIXED(parent), button, x, y);
+       }
+}
+
+
+void add_nums(GtkWidget *f, int radius, int start, int end, int step,
+             int div, int scale,
+             char *colour, PangoFontDescription *desc)
+{
+       int i;
+       for (i=start; i<end; i+= step) {
+               char buf[400];
+               double a, s, c, r;
+               int x, y;
+               GtkWidget *l;
+               long scaled;
+               sprintf(buf, "<span background=\"%s\">%02d</span>", colour, i);
+               l = gtk_button_new_with_label(" 99 ");
+               gtk_widget_modify_font(GTK_BIN(l)->child, desc);
+               gtk_label_set_markup(GTK_LABEL(GTK_BIN(l)->child),
+                                    buf);
+               gtk_widget_show(l);
+               scaled = i * scale;
+               g_signal_connect((gpointer)l, "clicked",
+                                G_CALLBACK(set_time), (void*)scaled);
+               a = i * 2.0 * M_PI / div;
+               s = sin(a); c= cos(a);
+               r = (double)radius;
+               if (fabs(s) < fabs(c))
+                       r = r / fabs(c);
+               else
+                       r = r / fabs(s);
+               x = 210 + (int)(r * s) - r/18;
+               y = 210 - (int)(r * c) - r/18;
+               gtk_fixed_put(GTK_FIXED(f), l, x, y);
+       }
+}
+               
+GtkWidget *create_clock_window(void)
+{
+       PangoFontDescription *desc;
+       GtkWidget *f, *l, *v;
+       GtkWidget *blist = NULL;
+
+       desc = pango_font_description_new();
+       
+       v = gtk_vbox_new(FALSE, 0);
+       gtk_widget_show(v);
+
+       l = gtk_label_new(" Date "); /* Date for which time is being chosen */
+       pango_font_description_set_size(desc, 12 * PANGO_SCALE);
+       gtk_widget_modify_font(l, desc);
+       gtk_container_add_with_properties(GTK_CONTAINER(v), l, "expand", 0, NULL);
+       gtk_widget_show(l);
+       date_display = l;
+
+       f = gtk_fixed_new();
+       gtk_widget_show(f);
+       gtk_container_add(GTK_CONTAINER(v), f);
+
+       pango_font_description_set_size(desc, 17 * PANGO_SCALE);
+       add_nums(f, 200, 6, 18, 1, 12, 100, "light green", desc);
+       pango_font_description_set_size(desc, 15 * PANGO_SCALE);
+       add_nums(f, 140, 1, 6, 1, 12, 100, "light blue", desc);
+       add_nums(f, 140,18,25, 1, 12, 100, "light blue", desc);
+       pango_font_description_set_size(desc, 12 * PANGO_SCALE);
+       add_nums(f, 95, 0, 60, 5, 60, 1, "pink", desc);
+
+#if 0
+       l = gtk_label_new("09:00\n9:00AM");
+       hour = 9;
+       minute = 0;
+       pango_font_description_set_size(desc, 12 * PANGO_SCALE);
+       gtk_widget_modify_font(l, desc);
+       gtk_widget_show(l);
+       gtk_fixed_put(GTK_FIXED(f), l, 170,180);
+       time_label = l;
+#endif
+
+       add_button(&blist, "confirm", desc, clock_confirm);
+       add_button(&blist, "back", desc, cal_restore);
+       gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0);
+
+       return v;
+}
+
+char *reasons[] = {"awake", "go", "meet", "buy", "call", "doctor", "dentist", "ok", "thanks", "coming"};
+struct list_entry_text *reason_entries;
+int selected_reason = -1;
+int num_reasons;
+
+struct list_entry *item(void *list, int n)
+{
+       if (n < num_reasons)
+               return &reason_entries[n].head;
+       else
+               return NULL;
+}
+void selected(void *list, int n)
+{
+       selected_reason = n;
+}
+
+struct list_handlers reason_han =  {
+       .getitem = item,
+       .get_size = size,
+       .render = render,
+       .selected = selected,
+};
+
+void reason_back(void)
+{
+       gtk_container_remove(GTK_CONTAINER(window), reasonw);
+       gtk_container_add(GTK_CONTAINER(window), clockw);
+}
+
+void reason_confirm(void)
+{
+       struct event *ev;
+       GtkTextIter start, finish;
+
+       if (!active_event) {
+               ev = malloc(sizeof(*ev));
+               ev->next = evlist;
+               evlist = ev;
+       } else {
+               ev = active_event;
+               free(ev->mesg);
+       }
+       ev->recur = freq;
+       ev->when = ev->first = chosen_date + hour*3600 + minute*60;
+       gtk_text_buffer_get_bounds(reason_buffer, &start, &finish);
+       ev->mesg = gtk_text_buffer_get_text(reason_buffer,
+                                           &start, &finish, FALSE);
+       gtk_container_remove(GTK_CONTAINER(window), reasonw);
+       gtk_container_add(GTK_CONTAINER(window), cal);
+
+       event_save_all(filename, evlist);
+
+       active_event = ev;
+       alarm_date = 0; /* force refresh */
+       move_abort();
+}
+
+void reason_select(void)
+{
+       if (selected_reason < 0 ||
+           selected_reason >= num_reasons)
+               return;
+       gtk_text_buffer_delete_selection(reason_buffer, TRUE, TRUE);
+       gtk_text_buffer_insert_at_cursor(reason_buffer, " ", 1);
+       gtk_text_buffer_insert_at_cursor(reason_buffer, reasons[selected_reason],
+                                        strlen(reasons[selected_reason]));
+}
+
+GtkWidget *create_reason_window(void)
+{
+       /* The "reason" window allows the reason for the alarm
+        * to be set.
+        * It has:
+        *  a label to show date and time
+        *  a text editing widget containing the reason
+        *  a selectable list of canned reasons
+        *  buttons for: confirm select clear
+        *
+        * confirm adds the alarm, or not if the reason is clear
+        * select adds the selected canned reason to the editor
+        * clear clears the reason
+        */
+
+       PangoFontDescription *desc;
+       GtkWidget *v, *blist=NULL, *l;
+       struct sellist *sl;
+       int i, num;
+
+       desc = pango_font_description_new();
+       v = gtk_vbox_new(FALSE, 0);
+       gtk_widget_show(v);
+
+       l = gtk_label_new(" Date and Time ");
+       pango_font_description_set_size(desc, 10 * PANGO_SCALE);
+       gtk_widget_modify_font(l, desc);
+       gtk_widget_show(l);
+       gtk_container_add_with_properties(GTK_CONTAINER(v), l, "expand", 0, NULL);
+       date_time_display = l;
+
+       l = gtk_text_view_new_with_buffer(reason_buffer);
+       gtk_widget_modify_font(l, desc);
+       gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(l), GTK_WRAP_WORD_CHAR);
+       gtk_widget_show(l);
+       gtk_container_add(GTK_CONTAINER(v), l);
+       gtk_text_buffer_set_text(reason_buffer, "Sample Reason", -1);
+
+       num = sizeof(reasons)/sizeof(reasons[0]);
+       reason_entries = calloc(num+1, sizeof(reason_entries[0]));
+       for (i=0; i<num; i++) {
+               reason_entries[i].text = reasons[i];
+               reason_entries[i].bg = "white";
+               reason_entries[i].fg = "blue";
+               reason_entries[i].underline = 0;
+       }
+       num_reasons = num;
+
+       sl = listsel_new(NULL, &reason_han);
+       pango_font_description_set_size(desc, 15 * PANGO_SCALE);
+       gtk_widget_modify_font(sl->drawing, desc);
+       gtk_container_add(GTK_CONTAINER(v), sl->drawing);
+       gtk_widget_show(sl->drawing);
+
+       add_button(&blist, "confirm", desc, reason_confirm);
+       add_button(&blist, "select", desc, reason_select);
+       add_button(&blist, "back", desc, reason_back);
+       gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0);
+       return v;
+}
+
+void add_time(int mins)
+{
+       time_t now;
+       char buf[30];
+       struct tm *tm;
+       delay += mins;
+       if (delay <= 0)
+               delay = 0;
+       now = time(0) + (delay?:1)*60;
+       tm = localtime(&now);
+       sprintf(buf, "+%d:%02d - ",
+               delay/60, delay%60);
+       strftime(buf+strlen(buf),
+                sizeof(buf)-strlen(buf),
+                "%H:%M", tm);
+       gtk_label_set_text(GTK_LABEL(timer_display), buf);
+
+}
+void add_time_60(void) { add_time(60); }
+void add_time_10(void) { add_time(10); }
+void add_time_1(void) { add_time(1); }
+void del_time_60(void) { add_time(-60); }
+void del_time_10(void) { add_time(-10); }
+void del_time_1(void) { add_time(-1); }
+
+int show_time(void *d)
+{
+       time_t now;
+       char buf[30];
+       struct tm *tm;
+       now = time(0);
+       tm = localtime(&now);
+       strftime(buf, sizeof(buf), "%H:%M:%S", tm);
+       gtk_label_set_text(GTK_LABEL(time_display), buf);
+}
+
+void to_cal(void)
+{
+       gtk_container_remove(GTK_CONTAINER(window), timerw);
+       g_source_remove(timer_tick);
+       gtk_container_add(GTK_CONTAINER(window), cal);
+}
+void set_timer(void)
+{
+       FILE *f = fopen("/data/alarms/timer", "a");
+       char buf[100];
+       time_t now = time(0) + delay*60;
+       struct tm *tm = localtime(&now);
+
+       strftime(buf, sizeof(buf), "%Y-%m-%d-%H-%M-%S::TIMER\n", tm);
+       if (f) {
+               fputs(buf, f);
+               fclose(f);
+       }
+       to_cal();
+}
+void clear_timers(void)
+{
+       unlink("/data/alarms/timer");
+       to_cal();
+}
+
+GtkWidget *create_timer_window(void)
+{
+       /* The timer window lets you set a one-shot alarm
+        * some number of minutes in the future.
+        * There are buttons for +1hr +10 +1 and - all those
+        * The timer window also shows a large digital clock with seconds
+        */
+
+       PangoFontDescription *desc;
+       GtkWidget *v, *blist, *l;
+
+       desc = pango_font_description_new();
+       v = gtk_vbox_new(FALSE, 0);
+       gtk_widget_show(v);
+
+       l = gtk_label_new("  Timer ");
+       pango_font_description_set_size(desc, 25 * PANGO_SCALE);
+       gtk_widget_modify_font(l, desc);
+       gtk_widget_show(l);
+       gtk_container_add_with_properties(GTK_CONTAINER(v), l, "expand", 0, NULL);
+       
+       l = gtk_label_new(" 99:99:99 ");
+       pango_font_description_set_size(desc, 40 * PANGO_SCALE);
+       gtk_widget_modify_font(l, desc);
+       gtk_widget_show(l);
+       gtk_container_add_with_properties(GTK_CONTAINER(v), l, "expand", 0, NULL);
+       time_display = l;
+
+       l = gtk_label_new(" ");
+       pango_font_description_set_size(desc, 10 * PANGO_SCALE);
+       gtk_widget_modify_font(l, desc);
+       gtk_widget_show(l);
+       gtk_container_add_with_properties(GTK_CONTAINER(v), l, "expand", 1, NULL);
+       timers_list = l;
+
+
+       /* now from the bottom up */
+       blist = NULL;
+       pango_font_description_set_size(desc, 10 * PANGO_SCALE);
+       add_button(&blist, "Return", desc, to_cal);
+       add_button(&blist, "Set", desc, set_timer);
+       add_button(&blist, "Clear All", desc, clear_timers);
+       gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0);
+
+       blist = NULL;
+       pango_font_description_set_size(desc, 10 * PANGO_SCALE);
+       add_button(&blist, "-1hr", desc, del_time_60);
+       add_button(&blist, "-10m", desc, del_time_10);
+       add_button(&blist, "-1m", desc, del_time_1);
+       gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0);
+
+       
+       l = gtk_label_new("+1:34 - 99:99 ");
+       pango_font_description_set_size(desc, 15 * PANGO_SCALE);
+       gtk_widget_modify_font(l, desc);
+       gtk_widget_show(l);
+       
+       gtk_box_pack_end(GTK_BOX(v), l, FALSE, FALSE, 0);
+       timer_display = l;
+
+       blist = NULL;
+       pango_font_description_set_size(desc, 10 * PANGO_SCALE);
+       add_button(&blist, "+1hr", desc, add_time_60);
+       add_button(&blist, "+10m", desc, add_time_10);
+       add_button(&blist, "+1m", desc, add_time_1);
+       gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0);
+
+       return v;
+}      
+
+main(int argc, char *argv[])
+{
+       GtkSettings *set;
+
+       gtk_set_locale();
+       gtk_init_check(&argc, &argv);
+
+       set = gtk_settings_get_default();
+       gtk_settings_set_long_property(set, "gtk-xft-dpi", 151 * PANGO_SCALE, "code");
+
+
+       window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+       gtk_window_set_default_size(GTK_WINDOW(window), 480, 640);
+       {
+       PangoContext *context;
+       PangoFontDescription *desc;
+       desc = pango_font_description_new();
+
+       pango_font_description_set_size(desc, 12 * PANGO_SCALE);
+       gtk_widget_modify_font(window, desc);
+       context = gtk_widget_get_pango_context(window);
+       layout = pango_layout_new(context);
+       }
+
+       reason_buffer = gtk_text_buffer_new(NULL);
+       cal = create_cal_window();
+       clockw = create_clock_window();
+       reasonw = create_reason_window();
+       timerw = create_timer_window();
+       gtk_container_add(GTK_CONTAINER(window), cal);
+       g_signal_connect((gpointer)window, "destroy",
+                        G_CALLBACK(finish), NULL);
+       gtk_widget_show(window);
+       gtk_widget_ref(cal);
+       gtk_widget_ref(clockw);
+       gtk_widget_ref(reasonw);
+       gtk_widget_ref(timerw);
+       set_cal(time(0));
+
+
+
+       gtk_main();
+}
diff --git a/alarm/listsel.c b/alarm/listsel.c
new file mode 100644 (file)
index 0000000..d6e20f5
--- /dev/null
@@ -0,0 +1,579 @@
+/*
+ * selectable, auto-scrolling list widget
+ *
+ * We display a list of texts and allow one to be selected,
+ * normally with a tap.
+ * The selected text is highlighted and the list auto-scrolls to
+ * ensure it is not too close to the edge, so no other scroll
+ * function is needed.
+ *
+ * The list is defined by a function that takes a number
+ * and returns the element.
+ * This element will point to a table of functions that
+ * measure the size of the entry and render it.  Some standard
+ * functions are provided to help with this.
+ * Each element has space to store current size/location for
+ * tap lookups.
+ */
+#include <unistd.h>
+#include <stdlib.h>
+
+#include <string.h>
+#include <malloc.h>
+
+#include <gtk/gtk.h>
+
+#include "listsel.h"
+
+void calc_layout_no(struct sellist *list)
+{
+       /* choose an appropriate 'top', 'last' and cols
+        * set x,y,width,height on each entry in that range.
+        *
+        * Starting at 'sel' (or zero) we walk backwards
+        * towards 'top' assessing size.
+        * If we hit 'top' while in first 1/3 of display
+        * we target being just above bottom third
+        * If we don't reach 'top' while above bottom
+        * 1/3, then we target just below the top 1/3,
+        * having recorded where 'top' would be (When we first
+        * reached 1/3 from 'sel')
+        * Once we have chosen top, we move to end to find
+        * bottom.  If we hit end before bottom,
+        * we need to move top backwards if possible.
+        *
+        * So:
+        * - walk back from 'sel' until find
+        *    'top' or distance above sel exceeds 2/3
+        *    record location where distance was last < 1/3
+        * - if found top and distance < 1/3 and top not start,
+        *    step back while that is still < 1/3
+        * - elif distance exceeds 2/3, set top et al to the
+        *   found 1/3 position
+        * - move down from 'sel' until total distance is
+        *   height, or hit end.
+        * - if hit end, move top backwards while total distance
+        *   still less than height.
+        *
+        * columns add a complication as we don't know how
+        * many columns to use until we know the max width of
+        * the display choices.
+        * I think we assume cols based on selected entry,
+        * and as soon as we find something that decreases
+        * col count, start again from top.
+        *
+        * That doesn't work so well, particularly with multiple
+        * columns - we don't see the col-breaks properly.
+        * To see them, we really need to chose a firm starting
+        * place.
+        * So:
+        *  Start with 'top' at beginning and walk along.
+        *   If we hit end of list before bottom and top!=0,
+        *   walk backwards from end above bottom to set top.
+        *  If we find 'sel' after first 1/3 and before last - good.
+        *  If before first third, set 'sel' above last third
+        *  and walk back until find beginning or start, set top there.
+        *  If after last third, set 'sel' past first third,
+        *  walk back to top, set top there.
+        *
+        */
+       int top, last;
+       int sel;
+       int i;
+       int cols = 1, col, row;
+       struct list_entry *e;
+       int w, h;
+       int dist, pos, colwidth;
+       int altdist, alttop;
+
+       sel = list->selected;
+       if (sel < 0)
+               sel = 0;
+       e = list->han->getitem(list->list, sel);
+       if (e == NULL && sel > 0) {
+               sel = 0;
+               e = list->han->getitem(list->list, sel);
+       }
+       if (e == NULL)
+               /* list is empty */
+               return;
+
+       top = list->top;
+       if (top < 0)
+               top = 0;
+       if (top > sel)
+               top = sel;
+
+ retry_cols:
+       // did_scroll = 0;
+ retry_scroll:
+       /* lay things out from 'top' */
+       i = top; col = 0; row = 0;
+       while (col < cols) {
+               e = list->han->getitem(list->list, i);
+               if (e == NULL)
+                       break;
+               list->han->get_size(e, &w, &h);
+               e->x = col * list->width / cols;
+               e->y = row;
+               e->width = list->width / cols;
+               row += h;
+               if (row > list->height) {
+                       col++;
+                       e->x = col * list->width / cols;
+                       e->y = 0;
+                       row = h;
+               }
+               e->need_draw = 1;
+       }
+       
+       list->han->get_size(e, &w, &h);
+       cols = list->width / w;
+       if (cols == 0)
+               cols = 1;
+ col_retry:
+       dist = 0;
+       i = sel;
+       e = list->han->getitem(list->list, i);
+       list->han->get_size(e, &w, &h);
+       dist += h;
+
+       while (i > top && dist < (cols-1) * list->height + 2 * list->height/3) {
+               //printf("X i=%d dist=%d cols=%d lh=%d at=%d\n", i, dist, cols, list->height, alttop);
+               i--;
+               e = list->han->getitem(list->list, i);
+               list->han->get_size(e, &w, &h);
+               dist += h;
+               if (cols > 1 && w*cols > list->width) {
+                       cols --;
+                       goto col_retry;
+               }
+               if (dist * 3 < list->height) {
+                       alttop = i;
+                       altdist = dist;
+               }
+       }
+       if (i == top) {
+               if (dist*3 < list->height) {
+                       /* try to move to other end */
+                       while (i > 0 &&
+                              dist < (cols-1)*list->height * list->height*2/3) {
+                               i--;
+                               e = list->han->getitem(list->list, i);
+                               list->han->get_size(e, &w, &h);
+                               dist += h;
+                               if (cols > 1 && w*cols > list->width) {
+                                       cols--;
+                                       goto col_retry;
+                               }
+                               top = i;
+                       }
+               }
+       } else {
+               /* too near bottom */
+               top = alttop;
+               dist = altdist;
+       }
+
+       i = sel;
+       e = list->han->getitem(list->list, i);
+       list->han->get_size(e, &w, &h);
+       while (dist + h <= list->height * cols) {
+               dist += h;
+               i++;
+               //printf("Y i=%d dist=%d cols=%d\n", i, dist, cols);
+               e = list->han->getitem(list->list, i);
+               if (e == NULL)
+                       break;
+               list->han->get_size(e, &w, &h);
+               if (cols > 1 && w*cols > list->width) {
+                       cols --;
+                       goto col_retry;
+               }
+       }
+       if (e == NULL) {
+               /* near end, maybe move top up */
+               i = top;
+               while (i > 0) {
+                       i--;
+                       e = list->han->getitem(list->list, i);
+                       list->han->get_size(e, &w, &h);
+                       if (dist + h >= list->height * cols)
+                               break;
+                       dist += h;
+                       if (cols > 1 && w * cols > list->width) {
+                               cols--;
+                               goto col_retry;
+                       }
+                       top = i;
+               }
+       }
+
+       /* OK, we are going with 'top' and 'cols'. */
+       list->cols = cols;
+       list->top = top;
+       list->selected = sel;
+       /* set x,y,width,height for each
+        * width and height are set - adjust width and set x,y
+        */
+       col = 0;
+       pos = 0;
+       i = top;
+       colwidth = list->width / cols;
+       last = top;
+       while (col < cols) {
+               e = list->han->getitem(list->list, i);
+               if (e == NULL)
+                       break;
+               e->x = col * colwidth;
+               e->y = pos;
+               //printf("Set %d,%d  %d,%d\n", e->x, e->y, e->height, list->height);
+               pos += e->height;
+               if (pos > list->height) {
+                       pos = 0;
+                       col++;
+                       continue;
+               }
+               e->width = colwidth;
+               e->need_draw = 1;
+               last = i;
+               i++;
+       }
+       list->last = last;
+}
+
+void calc_layout(struct sellist *list)
+{
+       /* Choose 'top', and set 'last' and 'cols'
+        * so that 'selected' is visible in width/height,
+        * and is not in a 'bad' position.
+        * If 'sel' is in first column and ->y < height/3
+        * and 'top' is not zero, that is bad, we set top to zero.
+        * If 'sel' is in last column and ->Y > height/3
+        * and 'last' is not end of list, that is bad, we set
+        *  top to 'last'
+        * If 'sel' is before 'top', that is bad, set 'top' to 'sel'.
+        * If 'sel' is after 'last', that is bad, set 'top' to 'sel'.
+        */
+       int top = list->top;
+       int sel = list->selected;
+       int cols = 10000;
+       int col, row, pos, last;
+       struct list_entry *sele;
+       int did_jump = 0;
+
+ retry_cols:
+       /* make sure 'top' and 'sel' are valid */
+       top = list->top;
+       sel = list->selected;
+       did_jump = 0;
+       if (top < 0 || list->han->getitem(list->list, top) == NULL)
+               top = 0;
+       if (sel < 0 || list->han->getitem(list->list, sel) == NULL)
+               sel = 0;
+
+
+ retry:
+       //printf("try with top=%d cols=%d\n", top, cols);
+       pos = top; col=0; row=0; last= -1;
+       sele = NULL;
+       while(1) {
+               int w, h;
+               struct list_entry *e;
+
+               e = list->han->getitem(list->list, pos);
+               if (e == NULL)
+                       break;
+               if (pos == sel)
+                       sele = e;
+               list->han->get_size(e, &w, &h);
+               if (cols > 1 && w * cols > list->width) {
+                       /* too many columns */
+                       cols = list->width / w;
+                       if (cols <= 0)
+                               cols = 1;
+                       goto retry_cols;
+               }
+               e->x = col * list->width / cols;
+               e->y = row;
+               e->width = list->width / cols;
+               e->need_draw = 1;
+               row += h;
+               if (row > list->height && e->y > 0) {
+                       col ++;
+                       if (col == cols)
+                               break;
+                       e->x = col * list->width / cols;
+                       e->y = 0;
+                       row = h;
+               }
+               last = pos;
+               pos++;
+       }
+       /* Check if this is OK */
+       if (sele == NULL) {
+               //printf("no sel: %d %d\n", sel, top);
+               if (last <= top)
+                       goto out;
+               if (sel < top)
+                       top--;
+               else
+                       top++;
+               goto retry;
+       }
+       //printf("x=%d y=%d hi=%d lh=%d top=%d lw=%d cols=%d\n",
+       //       sele->x, sele->y, sele->height, list->height, top, list->width, cols);
+       if (sele->x == 0 && sele->y + sele->height < list->height / 3
+           && top > 0) {
+               if (did_jump)
+                       top --;
+               else {
+                       top = 0;
+                       did_jump = 1;
+               }
+               goto retry;
+       }
+       if (sele->x == (cols-1) * list->width / cols &&
+           sele->y + sele->height > list->height * 2 / 3 &&
+           col == cols) {
+               if (did_jump)
+                       top ++;
+               else {
+                       top = last;
+                       did_jump = 1;
+               }
+               goto retry;
+       }
+       if (col < cols && top > 0 &&
+           (col < cols - 1 || row < list->height / 2)) {
+               top --;
+               goto retry;
+       }
+ out:
+       list->top = top;
+       list->last = last;
+       list->cols = cols;
+       //printf("top=%d last=%d cols=%d\n", top, last, cols);
+}
+
+
+void configure(GtkWidget *draw, void *event, struct sellist *list)
+{
+       GtkAllocation alloc;
+
+       gtk_widget_get_allocation(draw, &alloc);
+#if 0
+       if (list->width == alloc.width &&
+           list->height == alloc.height)
+               return;
+#endif
+       list->width = alloc.width;
+       list->height = alloc.height;
+       calc_layout(list);
+       gtk_widget_queue_draw(draw);
+}
+
+void expose(GtkWidget *draw, GdkEventExpose *event, struct sellist *list)
+{
+       int i;
+       /* Draw all fields in the event->area */
+
+       for (i = list->top; i <= list->last; i++) {
+               struct list_entry *e = list->han->getitem(list->list, i);
+               if (e->x > event->area.x + event->area.width)
+                       /* to the right */
+                       continue;
+               if (e->x + e->width < event->area.x)
+                       /* to the left */
+                       continue;
+               if (e->y > event->area.y + event->area.height)
+                       /* below */
+                       continue;
+               if (e->y + e->height < event->area.y)
+                       /* above */
+                       continue;
+
+               e->need_draw = 1;
+               if (event->count == 0 && e->need_draw == 1) {
+                       e->need_draw = 0;
+                       list->han->render(e, i == list->selected, list->drawing);
+               }
+       }
+}
+
+void tap(GtkWidget *draw, GdkEventButton *event, struct sellist *list)
+{
+       int i;
+       for (i=list->top; i <= list->last; i++) {
+               struct list_entry *e = list->han->getitem(list->list, i);
+               if (event->x >= e->x &&
+                   event->x < e->x + e->width &&
+                   event->y >= e->y &&
+                   event->y < e->y + e->height)
+               {
+                       //printf("found item %d\n", i);
+                       list->selected = i;
+                       calc_layout(list);
+                       gtk_widget_queue_draw(list->drawing);
+                       list->han->selected(list, i);
+                       break;
+               }
+       }
+}
+
+void *listsel_new(void *list, struct list_handlers *han)
+{
+       struct sellist *rv;
+
+       rv = malloc(sizeof(*rv));
+       rv->list = list;
+       rv->han = han;
+       rv->drawing = gtk_drawing_area_new();
+       rv->top = 0;
+       rv->selected = -1;
+       rv->width = rv->height = 0;
+       rv->cols = 1;
+
+       g_signal_connect((gpointer)rv->drawing, "expose-event",
+                        G_CALLBACK(expose), rv);
+       g_signal_connect((gpointer)rv->drawing, "configure-event",
+                        G_CALLBACK(configure), rv);
+
+       gtk_widget_add_events(rv->drawing,
+                             GDK_BUTTON_PRESS_MASK|
+                             GDK_BUTTON_RELEASE_MASK);
+       g_signal_connect((gpointer)rv->drawing, "button_release_event",
+                        G_CALLBACK(tap), rv);
+
+       return rv;
+}
+
+
+#ifdef MAIN
+
+struct list_entry_text *entries;
+int entcnt;
+PangoFontDescription *fd;
+PangoLayout *layout;
+GdkGC *colour;
+
+/*
+ * gdk_color_parse("green", GdkColor *col);
+ * gdk_colormap_alloc_color(GdkColormap, GdkColor, writeable?, bestmatch?)
+ * gdk_colormap_get_system
+ *
+ * gdk_gc_new(drawable)
+ * gdk_gc_set_foreground(gc, color)
+ * gdk_gc_set_background(gc, color)
+ * gdk_gc_set_rgb_fg_color(gc, color)
+ */
+struct list_entry *item(void *list, int n)
+{
+       if (n < entcnt)
+               return &entries[n].head;
+       else
+               return NULL;
+}
+int size(struct list_entry *i, int *width, int*height)
+{
+       PangoRectangle ink, log;
+       struct list_entry_text *item = (void*)i;
+
+       if (i->height) {
+               *width = item->true_width;
+               *height = i->height;
+               return 0;
+       }
+       //printf("calc for %s\n", item->text);
+       pango_layout_set_text(layout, item->text, -1);
+       pango_layout_get_extents(layout, &ink, &log);
+       //printf("%s %d %d\n", item->text, log.width, log.height);
+       *width = log.width / PANGO_SCALE;
+       *height = log.height / PANGO_SCALE;
+       item->true_width = i->width = *width;
+       i->height = *height;
+       return 0;
+}
+
+int render(struct list_entry *i, int selected, GtkWidget *d)
+{
+       PangoRectangle ink, log;
+       struct list_entry_text *item = (void*)i;
+       int x;
+       GdkColor col;
+
+       pango_layout_set_text(layout, item->text, -1);
+
+       if (colour == NULL) {
+               colour = gdk_gc_new(gtk_widget_get_window(d));
+               gdk_color_parse("purple", &col);
+               gdk_gc_set_rgb_fg_color(colour, &col);
+       }
+       if (selected) {
+               gdk_color_parse("pink", &col);
+               gdk_gc_set_rgb_fg_color(colour, &col);
+               gdk_draw_rectangle(gtk_widget_get_window(d),
+                                  colour, TRUE,
+                                  item->head.x, item->head.y,
+                                  item->head.width, item->head.height);
+               gdk_color_parse("purple", &col);
+               gdk_gc_set_rgb_fg_color(colour, &col);
+       }
+       x = (i->width - item->true_width)/2;
+       if (x < 0)
+               x = 0;
+       gdk_draw_layout(gtk_widget_get_window(d),
+                       colour,
+                       item->head.x+x, item->head.y, layout);
+       return 0;
+}
+void selected(void *list, int n)
+{
+       printf("got %d\n", n);
+}
+
+struct list_handlers myhan =  {
+       .getitem = item,
+       .get_size = size,
+       .render = render,
+       .selected = selected,
+};
+
+main(int argc, char *argv[])
+{
+       int i;
+       GtkWidget *w;
+       struct sellist *l;
+       PangoContext *context;
+
+       entries = malloc(sizeof(*entries) * argc);
+       memset(entries, 0, sizeof(*entries)*argc);
+       for (i=1; i<argc; i++) {
+               entries[i-1].text = argv[i];
+               entries[i-1].bg = "white";
+               entries[i-1].fg = "blue";
+               entries[i-1].underline = 0;
+       }
+       entcnt = i-1;
+
+       gtk_set_locale();
+       gtk_init_check(&argc, &argv);
+
+       fd = pango_font_description_new();
+       pango_font_description_set_size(fd, 35 * PANGO_SCALE);
+
+       w = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+       gtk_window_set_default_size(GTK_WINDOW(w), 480, 640);
+
+       gtk_widget_modify_font(w, fd);
+       context = gtk_widget_get_pango_context(w);
+       layout = pango_layout_new(context);
+
+       l = listsel_new(NULL, &myhan);
+       gtk_container_add(GTK_CONTAINER(w), l->drawing);
+       gtk_widget_show(l->drawing);
+       gtk_widget_show(w);
+
+       gtk_main();
+}
+#endif
diff --git a/alarm/listsel.h b/alarm/listsel.h
new file mode 100644 (file)
index 0000000..0933deb
--- /dev/null
@@ -0,0 +1,39 @@
+
+struct sellist {
+       void *list;
+       struct list_handlers *han;
+       GtkWidget *drawing;
+
+       int top;        /* Index of first element displayed */
+       int selected;   /* Index of currently selected element */
+       int last;       /* Index of last displayed element */
+
+       int width, height; /* Pixel size of widget */
+       int cols;       /* Columns */
+};
+
+struct list_handlers {
+       struct list_entry *(*getitem)(void *list, int n);
+       int (*get_size)(struct list_entry *item, int *width, int *height);
+       int (*render)(struct list_entry *item, int selected,
+                     GtkWidget *d);
+       void (*selected)(void *list, int element);
+};
+
+struct list_entry {
+       int x, y, width, height;
+       int need_draw;
+};
+
+struct list_entry_text {
+       struct list_entry head;
+       char *text;
+       
+       int true_width;
+       char *bg, *fg;
+       int underline;
+};
+
+extern void *listsel_new(void *list, struct list_handlers *han);
+
+
diff --git a/alarm/wkalrm.c b/alarm/wkalrm.c
new file mode 100644 (file)
index 0000000..89add78
--- /dev/null
@@ -0,0 +1,244 @@
+/*
+ * wkalrm.c - Use the RTC alarm to wake us up
+ *
+ * Copyright (C) 2008 by OpenMoko, Inc.
+ * Written by Werner Almesberger <werner@openmoko.org>
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include <time.h>
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <linux/rtc.h>
+
+
+#define DEFAULT_RTC "/dev/rtc0"
+
+
+static const char *device = DEFAULT_RTC;
+static int fd;
+
+
+/* ----- Low-level wrappers ------------------------------------------------ */
+
+
+static void read_alarm(struct rtc_wkalrm *alarm)
+{
+       int res;
+
+       res = ioctl(fd, RTC_WKALM_RD, alarm);
+       if (res < 0) {
+               perror("ioctl(RTC_WKALM_RD)");
+               exit(1);
+       }
+}
+
+
+static void read_time(struct rtc_time *tm)
+{
+       int res;
+
+       res = ioctl(fd, RTC_RD_TIME, tm);
+       if (res < 0) {
+               perror("ioctl(RTC_RD_TIME)");
+               exit(1);
+       }
+}
+
+
+static void write_alarm(const struct rtc_wkalrm *alarm)
+{
+       int res;
+
+       res = ioctl(fd, RTC_WKALM_SET, alarm);
+       if (res < 0) {
+               perror("ioctl(RTC_WKALM_SET)");
+               exit(1);
+       }
+}
+
+
+/* ----- Date conversions -------------------------------------------------- */
+
+
+static void show_alarm(void)
+{
+       struct rtc_wkalrm alarm;
+       struct rtc_time tm;
+
+       read_time(&tm);
+       printf("time is %02d:%02d:%02d %04d-%02d-%02d\n",
+              tm.tm_hour, tm.tm_min, tm.tm_sec,
+              tm.tm_year+1900, tm.tm_mon+1,
+              tm.tm_mday);
+
+
+       read_alarm(&alarm);
+       if (!alarm.enabled)
+               printf("alarm disabled%s\n",
+                   alarm.pending ? " (pending)" : "");
+       else
+               printf("%02d:%02d:%02d %04d-%02d-%02d%s\n",
+                   alarm.time.tm_hour, alarm.time.tm_min, alarm.time.tm_sec,
+                   alarm.time.tm_year+1900, alarm.time.tm_mon+1,
+                   alarm.time.tm_mday,
+                   alarm.pending ? " (pending)" : "");
+}
+
+
+static void set_alarm_abs(const char *t, const char *day)
+{
+       fprintf(stderr, "not yet implemented :-)\n");
+       exit(1);
+}
+
+
+static void set_alarm_delta(time_t delta)
+{
+       struct rtc_wkalrm alarm;
+       struct tm tm, *tmp;
+       time_t t;
+
+       read_time(&alarm.time);
+       memset(&tm, 0, sizeof(tm));
+       tm.tm_sec = alarm.time.tm_sec;
+       tm.tm_min = alarm.time.tm_min;
+       tm.tm_hour = alarm.time.tm_hour;
+       tm.tm_mday = alarm.time.tm_mday;
+       tm.tm_mon = alarm.time.tm_mon;
+       tm.tm_year = alarm.time.tm_year;
+       tm.tm_isdst = -1;
+       t = mktime(&tm);
+       if (t == (time_t) -1) {
+               fprintf(stderr, "mktime: error\n");
+               exit(1);
+       }
+       t += delta;
+       tmp = localtime(&t);
+       if (!tmp) {
+               fprintf(stderr, "localtime_r: error\n");
+               exit(1);
+       }
+       alarm.time.tm_sec = tmp->tm_sec;
+       alarm.time.tm_min = tmp->tm_min;
+       alarm.time.tm_hour = tmp->tm_hour;
+       alarm.time.tm_mday = tmp->tm_mday;
+       alarm.time.tm_mon = tmp->tm_mon;
+       alarm.time.tm_year = tmp->tm_year;
+       alarm.enabled = 1;
+       write_alarm(&alarm);
+}
+
+
+static void set_alarm_rel(const char *delta)
+{
+       unsigned long n;
+       char *end;
+
+       n = strtoul(delta, &end, 10);
+       if (!strcmp(end, "d") || !strcmp(end, "day") || !strcmp(end, "days"))
+               n *= 24*3600;
+       else if (!strcmp(end, "h") || !strcmp(end, "hour") ||
+           !strcmp(end, "hours"))
+               n *= 3600;
+       else if (!strcmp(end, "m") || !strcmp(end, "min") ||
+           !strcmp(end, "mins"))
+               n *= 60;
+       else if (strcmp(end, "s") && strcmp(end, "sec") &&
+           strcmp(end, "secs")) {
+               fprintf(stderr, "invalid delta time \"%s\"\n", delta);
+               exit(1);
+       }
+       set_alarm_delta(n);
+}
+
+
+static void disable_alarm(void)
+{
+       struct rtc_wkalrm alarm;
+
+       read_alarm(&alarm);
+       alarm.enabled = 0;
+       write_alarm(&alarm);
+}
+
+
+static void set_alarm_24h(const char *t)
+{
+       fprintf(stderr, "not yet implemented :-)\n");
+       exit(1);
+}
+
+
+static void set_alarm(const char *when)
+{
+       if (*when == '+')
+               set_alarm_rel(when+1);
+       else
+               set_alarm_24h(when);
+}
+
+
+/* ----- Command line parsing ---------------------------------------------- */
+
+
+static void usage(const char *name)
+{
+       fprintf(stderr,
+"usage: %s [-d device]\n"
+"       %s [-d device] hh:mm[:ss] [[yyyy-]mm-dd]\n"
+"       %s [-d device] +Nunit\n\n"
+"  unit  is d[ay[s]], h[our[s]] m[in[s]], or s[ec[s]]\n\n"
+"  -d device  open the specified RTC device (default: %s)\n"
+    , name, name, name, DEFAULT_RTC);
+       exit(1);
+}
+
+
+int main(int argc, char **argv)
+{
+       int c;
+
+       while ((c = getopt(argc, argv, "d:")) != EOF)
+               switch (c) {
+               case 'd':
+                       device = optarg;
+                       break;
+               default:
+                       usage(*argv);
+               }
+
+       fd = open(device, O_RDWR);
+       if (fd < 0) {
+               perror(device);
+               exit(1);
+       }
+
+       switch (argc-optind) {
+       case 0:
+               show_alarm();
+               break;
+       case 1:
+               if (!strcmp(argv[optind], "off"))
+                       disable_alarm();
+               else
+                       set_alarm(argv[optind]);
+               break;
+       case 2:
+               set_alarm_abs(argv[optind], argv[optind+1]);
+               break;
+       default:
+               usage(*argv);
+       }
+       return 0;
+}
diff --git a/contacts/contactdb.py b/contacts/contactdb.py
new file mode 100644 (file)
index 0000000..9e20ccf
--- /dev/null
@@ -0,0 +1,157 @@
+
+#
+# Contacts DB interface for various programs
+# Currently only understand name/number and speed-dials
+# Separately we also provide access to log of incoming and
+# outgoing calls, mapped to names through the contacts DB.
+#
+# Need:
+#   list of "speed-dials".  These are ordered
+#   look up name/number from speed-dial
+#   map 'name' to entry - first match, with '.'s
+#   map 'number' to probable name
+#   load/save address book
+#   load /var/log/incoming and /var/log/outgoing
+#
+
+import os, time
+
+class entry:
+    def __init__(self, name, num = None):
+        # if 'num' is None, then 'name' contains "name;num;..."
+        if num == None:
+            a = name.strip().split(';')
+            if len(a) < 2:
+                raise ValueError;
+            name = a[0]
+            num = a[1]
+        self.name = name
+        self.num = num
+        self.speed = None
+        
+    def is_speed(self):
+        return len(self.name) == 1
+
+    def is_deleted(self):
+        return self.name[:8] == '!deleted'
+
+    def match_type(self, patn):
+        # patn might match:
+        # 1: start of name
+        # 2: start of word in name
+        # 3: somewhere in name
+        # 4: somewhere in num
+        p = patn.lower()
+        n = name.lower()
+        l = len(p)
+        if n[0:l] == p:
+            return 1
+        if n.find(' '+p) >= 0:
+            return 2
+        if n.find(p) >= 0:
+            return 3
+        if self.num.find(p):
+            return 4
+        return -1
+
+    def same_num(self, num):
+        l = len(num)
+        if l < 4:
+            # too short to be at all useful
+            return False
+        # 8 is enough to identify
+        if l > 8:
+            l = 8
+        return len(self.num) >= l and self.num[-l:] == num[-l:]
+
+    def __cmp__(self, other):
+        if self.speed and not other.speed:
+            return -1
+        if other.speed and not self.speed:
+            return 1
+        return cmp(self.name, other.name)
+
+class contacts:
+    def __init__(self):
+        try:
+            self.file = '/data/address-book'
+            self.load()
+        except:
+            self.file = '/home/neilb/home/mobile-numbers-jan-08'
+            self.load()
+
+    def load(self):
+        self.list = []
+        self.deleted = []
+        self.speed = {}
+        speed = {}
+        f = open(self.file)
+        for l in f:
+            e = entry(l)
+            if e.is_speed():
+                speed[e.num] = e.name
+            elif e.is_deleted():
+                self.deleted.append(e)
+            else:
+                self.list.append(e)
+        if speed:
+            for i in range(len(self.list)):
+                if self.list[i].name in speed:
+                    self.speed[speed[self.list[i].name]] = self.list[i]
+                    self.list[i].speed = speed[self.list[i].name]
+        self.resort()
+
+    def resort(self):
+        self.list.sort()
+        self.deleted.sort()
+
+    def save(self):
+        f = open(self.file + '.new', 'w')
+        for e in self.list:
+            f.write(e.name+';'+e.num+';\n')
+            if e.speed:
+                f.write(e.speed+';'+e.name+';\n')
+        for e in self.deleted:
+            f.write(e.name+';'+e.num+';\n')
+        f.close()
+        os.rename(self.file+'.new', self.file)
+
+    def add(self, name, num, speed = None):
+        c = entry(name, num)
+        self.list.append(c)
+        if speed:
+            c.speed = speed
+            self.speed[speed] = c
+        self.resort()
+
+    def undelete(self, ind):
+        e = self.deleted[ind]
+        del self.deleted[ind]
+        s = e.name
+        if s[:8] == '!deleted':
+            p = s.find('-')
+            if p <= 0:
+                p = 7
+            e.name = s[p+1:]
+        self.list.append(e)
+        self.resort()
+        if e.speed:
+            self.speed[e.speed] = e
+
+    def delete(self, ind):
+        e = self.list[ind]
+        del self.list[ind]
+        n = time.strftime('!deleted.%Y.%m.%d-') + e.name
+        e.name = n
+        self.deleted.append(e)
+        self.resort()
+
+    def find_num(self, num):
+        for e in self.list:
+            if e.same_num(num):
+                return e
+        return None
+
+if __name__ == "__main__":
+    c = contacts()
+    print c.speed.keys()
diff --git a/contacts/contacts.py b/contacts/contacts.py
new file mode 100644 (file)
index 0000000..5549848
--- /dev/null
@@ -0,0 +1,619 @@
+#!/usr/bin/env python
+
+#
+# Contacts manager
+#  Currently a 'contact' is a name, a number, and a speed-dial letter
+#
+# We have a content pane and a row of buttons at the bottom.
+# The content is either:
+#   - list of contact names - highlighted minipane at bottom with number
+#   - detailed contact info that can be editted (name, number, speed-pos)
+#
+# When list of contacts are displayed, typing a char adds that char to
+# a  substring and only contacts containing that substring are listed.
+# An extra entry at the start is given which matches exactly the substring
+# and can be used to create a new entry.
+# Alternately, the list can show only 'deleted' entries, still with substring
+# matching.  Colour is different in this case.
+#
+# Buttons for contact list are:
+#  When nothing selected (list_none):
+#     New ALL Undelete
+#  When contact selected (list_sel):
+#     Call SMS Open ALL
+#  When new-entry selected (list_new):
+#    Create ALL
+#  When viewing deleted entries and nothing or first is selected (del_none):
+#    Return
+#  When viewing deleted entries and one is selected (del_sel):
+#    Undelete Open Return
+#
+# When the detailed contact info is displayed all fields can be
+# edited and change background colour if they differ from stored value
+# Fields are: Name Number
+# Button for details are:
+#  When nothing is changed (edit_nil):
+#     Call SMS Close Delete
+#  When something is changed on new entry (edit_new)
+#     Discard  Create
+#  When something is changed on old entry (edit old)
+#     Restore Save Create
+#
+# 'delete' doesn't actually delete, but adds '!delete$DATE-' to the name which
+# causes most lookups to ignore the entry.
+#
+# TODO
+# - find way to properly reset 'current' pos after edit
+# - have a global 'state' object which other things refer to
+#   It has an 'updated' state which other objects can connect to
+# - save file after an update
+
+import gtk, pango, time, gobject, os
+from scrawl import Scrawl
+from listselect import ListSelect
+from contactdb import contacts
+
+def strip_prefix(num):
+    if num[:2] == "02":
+        return num[2:]
+    if num[:2] == "04":
+        return num[1:]
+    if num[:4] == "+612":
+        return num[4:]
+    if num[:4] == "+614":
+        return num[3:]
+    return num
+
+def match_num(num, str):
+    """ 'num' is a phone number, and 'str' is a search string
+    We want to allow matches that ignore the country/local prefix
+    which might be "02" or "+612" or something else in other countries.
+    But I'm not in other countries, so just strip those if present.
+    """
+    if num.find(str) >= 0:
+        return True
+    if "+612".find(str) == 0:
+        return True
+    if "02".find(str) == 0:
+        return True
+    return strip_prefix(num).find(strip_prefix(str)) == 0
+
+class Contacts(gtk.Window):
+    def __init__(self):
+        gtk.Window.__init__(self)
+        self.set_default_size(480,640)
+        self.set_title("Contacts")
+        self.connect('destroy', self.close_win)
+
+        self.current = None
+        self.timer = None
+        self.make_ui()
+        self.load_book()
+        self.show()
+        self.voice_cb = None
+        self.sms_cb = None
+        self.undeleting = False
+        self.watch_clip('contact-find')
+
+    def make_ui(self):
+        # UI consists of:
+        #   list of contacts -or-
+        #   editable field
+        #   -and-
+        #   variable list of buttons.
+        #
+        ctx = self.get_pango_context()
+        fd = ctx.get_font_description()
+        fd.set_absolute_size(35*pango.SCALE)
+        self.fd = fd
+
+        v = gtk.VBox(); v.show()
+        self.add(v)
+
+        s = self.listui()
+        self.lst = s
+        v.pack_start(s, expand=True)
+        s.show()
+        self.show()
+        self.sc.set_colour('red')
+
+        s = self.editui()
+        v.pack_start(s, expand = True)
+        s.hide()
+        self.ed = s
+
+        bv = gtk.VBox(); bv.show(); v.pack_start(bv, expand=False)
+        def hide_some(w):
+            for c in w.get_children():
+                c.hide()
+        bv.hide_some = lambda : hide_some(bv)
+        self.buttons = bv
+
+        b = self.buttonlist(bv)
+        self.button(b, 'New', self.open)
+        self.button(b, 'Undelete', self.undelete)
+        self.button(b, 'ALL', self.all)
+        self.list_none = b
+
+        b = self.buttonlist(bv)
+        self.button(b, 'Call', self.call)
+        self.button(b, 'SMS', self.sms)
+        self.button(b, 'Open', self.open)
+        self.button(b, 'Delete', self.delete)
+        self.list_sel = b
+
+        b = self.buttonlist(bv)
+        self.button(b, 'Create', self.open)
+        self.button(b, 'ALL', self.all)
+        self.list_new = b
+
+        b = self.buttonlist(bv)
+        self.button(b, 'Return', self.all)
+        self.del_none = b
+
+        b = self.buttonlist(bv)
+        self.button(b, 'Undelete', self.delete)
+        self.button(b, 'Open', self.open)
+        self.button(b, 'Return', self.all)
+        self.del_sel = b
+
+        b = self.buttonlist(bv)
+        self.button(b, 'Call', self.call)
+        self.button(b, 'SMS', self.sms)
+        self.button(b, 'Close', self.close)
+        self.button(b, 'Delete', self.delete)
+        self.edit_nil = b
+
+        b = self.buttonlist(bv)
+        self.button(b, 'Discard', self.close)
+        self.button(b, 'Create', self.create)
+        self.edit_new = b
+
+        b = self.buttonlist(bv)
+        self.button(b, 'Restore', self.open)
+        self.button(b, 'Save', self.save)
+        self.button(b, 'Create', self.create)
+        self.edit_old = b
+
+        self.list_none.show()
+
+    def listui(self):
+        s = ListSelect(markup=True); s.show()
+        s.set_format("normal","black", background="grey", selected="white")
+        s.set_format("deleted","red", background="grey", selected="white")
+        s.set_format("virtual","blue", background="grey", selected="white")
+        s.connect('selected', self.selected)
+        s.set_zoom(37)
+        self.clist = contact_list()
+        s.list = self.clist
+        s.list_changed()
+        self.sel = s
+        def gottap(p):
+            x,y = p
+            s.tap(x,y)
+        self.sc = Scrawl(s, self.gotsym, gottap, None, None)
+
+        s.set_events(s.get_events() | gtk.gdk.KEY_PRESS_MASK)
+        def key(list, ev):
+            if len(ev.string) == 1:
+                self.gotsym(ev.string)
+            elif ev.keyval == 65288:
+                self.gotsym('<BS>')
+            else:
+                #print ev.keyval, len(ev.string), ev.string
+                pass
+        s.connect('key_press_event', key)
+        s.set_flags(gtk.CAN_FOCUS)
+        s.grab_focus()
+
+        v = gtk.VBox(); v.show()
+        v.pack_start(s, expand=True)
+        l = gtk.Label('')
+        l.modify_font(self.fd)
+        self.number_label = l
+        v.pack_start(l, expand=False)
+        return v
+
+    def editui(self):
+        ui = gtk.VBox()
+
+        self.fields = {}
+        self.field(ui, 'Name')
+        self.field(ui, 'Number')
+        self.field(ui, 'Speed Number')
+        return ui
+
+    def field(self, v, lbl):
+        h = gtk.HBox(); h.show()
+        l = gtk.Label(lbl); l.show()
+        l.modify_font(self.fd)
+        h.pack_start(l, expand=False)
+        e = gtk.Entry(); e.show()
+        e.modify_font(self.fd)
+        h.pack_start(e, expand=True)
+        e.connect('changed', self.field_update, lbl)
+        v.pack_start(h, expand=True, fill=True)
+        self.fields[lbl] = e
+        return e
+
+    def buttonlist(self, v):
+        b = gtk.HBox()
+        b.set_homogeneous(True)
+        b.hide()
+        v.pack_end(b, expand=False)
+        return b
+
+    def button(self, bl, label, func):
+        b = gtk.Button()
+        if label[0:3] == "gtk" :
+            img = gtk.image_new_from_stock(label, self.isize)
+            img.show()
+            b.add(img)
+        else:
+            b.set_label(label)
+            b.child.modify_font(self.fd)
+        b.show()
+        b.connect('clicked', func)
+        b.set_focus_on_click(False)
+        bl.pack_start(b, expand = True)
+
+    def close_win(self, widget):
+        gtk.main_quit()
+
+    def selected(self, list, item):
+        self.buttons.hide_some()
+
+        if item == None:
+            item = -1
+        if self.undeleting:
+            if item < 0 or (item == 0 and self.clist.str != ''):
+                self.del_none.show()
+            else:
+                self.del_sel.show()
+                self.current = self.clist.getitem(item)
+        elif item < 0:
+            self.list_none.show()
+            self.number_label.hide()
+            self.current = None
+        elif item == 0 and self.clist.str != '':
+            self.list_new.show()
+            self.number_label.hide()
+            self.current = None
+        else:
+            self.list_sel.show()
+            i = self.clist[item]
+            self.current = self.clist.getitem(item)
+            if i == None:
+                self.number_label.hide()
+            else:
+                self.number_label.set_text(self.clist.list[self.current].num)
+                self.number_label.show()
+
+    def field_update(self, ev, fld):
+        if self.flds[fld] == self.fields[fld].get_text():
+            self.fields[fld].modify_base(gtk.STATE_NORMAL,None)
+        else:
+            self.fields[fld].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('yellow'))
+
+        same = True
+        for i in ['Name', 'Number', 'Speed Number']:
+            if self.fields[i].get_text() != self.flds[i]:
+                same = False
+        self.buttons.hide_some()
+        if self.current == None:
+            self.edit_new.show()
+        elif same:
+            self.edit_nil.show()
+        else:
+            self.edit_old.show()
+        pass
+
+    def load_book(self):
+        self.book = contacts()
+        self.clist.set_list(self.book.list)
+
+    def queue_save(self):
+        if self.timer:
+            gobject.source_remove(self.timer)
+        self.timer = gobject.timeout_add(15*1000, self.do_save)
+    def do_save(self):
+        if self.timer:
+            gobject.source_remove(self.timer)
+        self.timer = None
+        self.book.save()
+
+    def gotsym(self,sym):
+        if sym == '<BS>':
+            s = self.clist.str[:-1]
+        elif len(sym) > 1:
+            s = self.clist.str
+        elif ord(sym) >= 32:
+            s = self.clist.str + sym
+        else:
+            return
+        self.clist.set_str(s)
+        self.sel.list_changed()
+        self.sel.select(None)
+
+    def watch_clip(self, board):
+        self.cb = gtk.Clipboard(selection=board)
+        self.targets = [ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ]
+        self.got_clip(self.cb, None)
+        self.cb.set_with_data(self.targets,
+                              self.get_clip, self.got_clip, None)
+
+
+    def got_clip(self, clipb, data):
+        s = clipb.wait_for_text()
+        if not s:
+            print 'not s'
+            return
+        print 'got', s
+        self.clist.set_str(s)
+        self.lst.show()
+        self.sel.grab_focus()
+        self.ed.hide()
+        self.sel.list_changed()
+        self.sel.select(None)
+        self.cb.set_with_data(self.targets, self.get_clip, self.got_clip, None)
+        self.present()
+
+    def get_clip(self, sel, seldata, info, data):
+        sel.set_text("Contact Please")
+
+    def open(self, ev):
+        self.lst.hide()
+        self.ed.show()
+
+        self.buttons.hide_some()
+        if self.current == None:
+            self.edit_new.show()
+        else:
+            self.edit_nil.show()
+        self.flds = {}
+        self.flds['Name'] = ''
+        self.flds['Number'] = ''
+        self.flds['Speed Number'] = ''
+        if self.current != None:
+            current = self.clist.list[self.current]
+            self.flds['Name'] = current.name
+            self.flds['Number'] = current.num
+            self.flds['Speed Number'] = current.speed
+            if current.speed == None:
+                self.flds['Speed Number'] = ""
+        elif self.clist.str:
+            num = True
+            for d in self.clist.str:
+                if not d.isdigit() and d != '+':
+                    num = False
+            if num:
+                self.flds['Number'] = self.clist.str
+            else:
+                self.flds['Name'] = self.clist.str
+            
+        for f in ['Name', 'Number', 'Speed Number'] :
+            c = self.flds[f]
+            if not c:
+                c = ""
+            self.fields[f].set_text(c)
+            self.fields[f].modify_base(gtk.STATE_NORMAL,None)
+
+    def all(self, ev):
+        self.clist.set_str('')
+        self.undeleting = False
+        self.current = None
+        self.clist.set_list(self.book.list)
+        self.sel.select(None)
+        self.sel.list_changed()
+        pass
+    def create(self, ev):
+        self.save(None)
+    def save(self,ev):
+        name = self.fields['Name'].get_text()
+        num = self.fields['Number'].get_text()
+        speeds = self.fields['Speed Number'].get_text()
+        speed = speeds
+        if speed == "":
+            speed = None
+        if name == '' or name.find(';') >= 0:
+            self.fields['Name'].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('pink'))
+            return
+        if num == '' or num.find(';') >= 0:
+            self.fields['Number'].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('pink'))
+            return
+        if len(speeds) > 1 or speeds.find(';') >= 0:
+            self.fields['Speed Number'].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('pink'))
+            return
+        if self.current == None or ev == None:
+            self.book.add(name, num, speed)
+        else:
+            current = self.book.list[self.current]
+            current.name = name
+            current.num = num
+            current.speed = speed
+        self.flds['Name'] = name
+        self.flds['Number'] = num
+        self.flds['Speed Number'] = speeds
+        self.book.resort()
+        self.clist.set_list(self.book.list)
+        self.sel.list_changed()
+        self.close(ev)
+        self.queue_save()
+
+    def delete(self, ev):
+        if self.current != None:
+            if self.undeleting:
+                self.book.undelete(self.current)
+                self.clist.set_list(self.book.deleted)
+            else:
+                self.book.delete(self.current)
+                self.clist.set_list(self.book.list)
+            self.sel.list_changed()
+            self.close(ev)
+            self.queue_save()
+        pass
+
+    def undelete(self, ev):
+        self.undeleting = True
+        self.clist.set_str('')
+        self.clist.set_list(self.book.deleted)
+        self.current = None
+        self.sel.list_changed()
+        self.sel.select(None)
+        pass
+    def call(self, ev):
+        if not self.voice_cb:
+            self.voice_cb = gtk.Clipboard(selection='voice-dial')
+        self.call_str = self.clist.list[self.current].num
+        self.voice_cb.set_with_data([ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ],
+                                    self.call_getdata, self.call_cleardata, None)
+    def call_getdata(self, clipb, sel, info, data):
+        sel.set_text(self.call_str)
+    def call_cleardata(self, clipb, data):
+        self.call_str = ""
+
+    def sms(self, ev):
+        if not self.sms_cb:
+            self.sms_cb = gtk.Clipboard(selection='sms-new')
+        self.sms_str = self.clist.list[self.current].num
+        self.sms_cb.set_with_data([ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0)],
+                                  self.sms_get_data, self.sms_cleardata, None)
+    def sms_get_data(self, clipb, sel, info, data):
+        sel.set_text(self.sms_str)
+    def sms_cleardata(self, clipb, data):
+        self.sms_str = ""
+
+
+    def close(self, ev):
+        self.lst.show()
+        self.sel.grab_focus()
+        self.ed.hide()
+        if self.current != None and self.current >= len(self.clist):
+            self.current = None
+        self.selected(None, self.sel.selected)
+
+class contact_list:
+    def __init__(self):
+        self.set_list([])
+    def set_list(self, list):
+        self.list = list
+        self.match_start = []
+        self.match_word = []
+        self.match_any = []
+        self.match_str = ''
+        self.total = 0
+        self.str = ''
+
+    def set_str(self,str):
+        self.str = str
+
+    def recalc(self):
+        if self.str == self.match_str:
+            return
+        if self.match_str != '' and self.str[:len(self.match_str)] == self.match_str:
+            # str is a bit longer
+            self.recalc_quick()
+            return
+        self.match_start = []
+        self.match_word = []
+        self.match_any = []
+        self.match_str = self.str
+        s = self.str.lower()
+        l = len(self.str)
+        for i in range(len(self.list)):
+            name = self.list[i].name.lower()
+            if name[0:l] == s:
+                self.match_start.append(i)
+            elif name.find(' '+s) >= 0:
+                self.match_word.append(i)
+            elif name.find(s) >= 0 or match_num(self.list[i].num, s):
+                self.match_any.append(i)
+        self.total = len(self.match_start) + len(self.match_word) + len(self.match_any)
+
+    def recalc_quick(self):
+        # The string has been extended so we only look through what we already have
+        self.match_str = self.str
+        s = self.str.lower()
+        l = len(self.str)
+        
+        lst = self.match_start
+        self.match_start = []
+        for i in lst:
+            name = self.list[i].name.lower()
+            if name[0:l] == s:
+                self.match_start.append(i)
+            else:
+                self.match_word.append(i)
+        self.match_word.sort()
+        lst = self.match_word
+        self.match_word = []
+        for i in lst:
+            name = self.list[i].name.lower()
+            if name.find(' '+s) >= 0:
+                self.match_word.append(i)
+            else:
+                self.match_any.append(i)
+        self.match_any.sort()
+        lst = self.match_any
+        self.match_any = []
+        for i in lst:
+            name = self.list[i].name.lower()
+            if name.find(s) >= 0 or match_num(self.list[i].num, s):
+                self.match_any.append(i)
+        self.total = len(self.match_start) + len(self.match_word) + len(self.match_any)
+                    
+    def __len__(self):
+        self.recalc()
+        if self.str == '':
+            return len(self.list)
+        else:
+            return self.total + 1
+    def getitem(self, ind):
+        if ind < 0:
+            print '!!!!', ind
+        if self.str == '':
+            return ind
+
+        if ind == 0:
+            return -1
+        ind -= 1
+        if ind < len(self.match_start):
+            return self.match_start[ind]
+        ind -= len(self.match_start)
+        if ind < len(self.match_word):
+            return self.match_word[ind]
+        ind -= len(self.match_word)
+        if ind < len(self.match_any):
+            return self.match_any[ind]
+        return None
+
+    def __getitem__(self, ind):
+
+        i = self.getitem(ind)
+        if i < 0:
+            return (protect(self.str), 'virtual')
+        if i == None:
+            return None
+        s = self.list[i].name
+        n = self.list[i].num
+        s = protect(s)
+        n = protect(n)
+        if s[:8] == '!deleted':
+            p = s.find('-')
+            if p <= 0:
+                p = 7
+            return (s[p+1:],'deleted')
+        else:
+            return (s + '<span color="blue" size="15000">\n     '+ n + '</span>', 'normal')
+
+def protect(txt):
+    txt = txt.replace('&', '&amp;')
+    txt = txt.replace('<', '&lt;')
+    txt = txt.replace('>', '&gt;')
+    return txt
+
+if __name__ == "__main__":
+
+    c = Contacts()
+    gtk.main()
+    
diff --git a/gps-utils/gpstime b/gps-utils/gpstime
new file mode 100755 (executable)
index 0000000..c34291d
--- /dev/null
@@ -0,0 +1,33 @@
+#!/bin/bash
+
+
+gpspipe -r -n 30 | {
+sum=0
+cnt=0
+while IFS=, read x t d m y r;
+ do [ $x = '$GPZDA' ] || continue
+  [ -z "$t" ] && continue
+  now=`date +%s`
+  hr=${t:0:2}
+  mn=${t:2:2}
+  sc=${t:4:2}
+  
+  then=`date --utc --date="$y-$m-$d $hr:$mn:$sc" +%s`
+  sum=$[sum+then-now]
+  cnt=$[cnt+1]
+done
+if [ $cnt -gt 0 ]
+then
+  diff=$[sum/cnt]
+  if [ $diff -lt -4 -o $diff -gt 4 ]
+  then echo "Change by $diff seconds"
+    date --set "+$diff seconds"
+    hwclock -w
+  else
+    echo "Diff is $diff - no change"
+  fi
+else
+  echo "No time found"
+fi
+}
+
diff --git a/gps-utils/gpstz b/gps-utils/gpstz
new file mode 100755 (executable)
index 0000000..be7a405
--- /dev/null
@@ -0,0 +1,62 @@
+#!/bin/bash
+
+case $1 in
+    */* )
+      if cmp -s /etc/localtime /usr/share/zoneinfo/$1
+      then : localtime is OK
+      else : echo Copying to localtime 
+         cp /usr/share/zoneinfo/$1 /etc/localtime
+      fi
+      if [ `cat /etc/timezone` != $1 ]
+      then : echo Setting /etc/timezone
+           echo $1 > /etc/timezone
+      fi
+      exit 0
+   ;;
+   --list ) ;;
+   * ) echo >&2 Usage: gpstz [--list] zone/name
+       exit 1
+esac
+
+gpspipe -r -n 20 | grep GPGGA | while IFS=, read a tm lat NS long EW etc 
+ do
+    long=${long%.*} lat=${lat%.*}
+    case $NS in
+       N) lat=+$lat;;
+       S) lat=-$lat;;
+    esac
+    case $EW in
+       E) long=+$long ;;
+       W) long=-$long ;;
+    esac
+    # echo $lat $long
+    mind=9999999999
+    while read country loc tz desc
+    do
+      case $country in
+         \#* ) continue;;
+      esac
+      case $loc in
+         [-+][0-9][0-9][0-9][0-9][-+][0-9][0-9][0-9][0-9][0-9] )
+           tlat=${loc%??????}
+           tlat=${tlat#?}
+           tlat=${tlat#0}
+           tlat=${tlat#0}
+           tlat=${tlat#0}
+           tlat=${loc%??????????}$tlat
+            tlong=${loc#?????} 
+           slong=${tlong%?????}
+           tlong=${tlong#?}
+           tlong=${tlong#0}
+           tlong=${tlong#0}
+           tlong=$slong${tlong#0}
+       ;;
+         * ) continue
+      esac
+      # echo $tz at $tlat $tlong
+      x=$[long-tlong] y=$[lat-tlat]
+      d=$[x*x+y*y]
+      echo $d $tz
+    done < /usr/share/zoneinfo/zone.tab 
+    break 
+ done | sort -n | sed 10q
diff --git a/gps-utils/ubxgen b/gps-utils/ubxgen
new file mode 100755 (executable)
index 0000000..a4efe2d
--- /dev/null
@@ -0,0 +1,68 @@
+#!/usr/bin/python
+#
+# ubx packet generator
+#
+# v0.2
+#
+# Wilfried Klaebe <wk-openmoko@chaos.in-kiel.de>
+# NeilBrown <neilb@suse.de>
+#
+# Usage:
+#
+# ubxgen.py 06 13 04 00 01 00 00 00 > packet.ubx
+#
+# prepends 0xb5 0x62 header,
+# appends checksum,
+# outputs binary packet to stdout
+#
+# Numbers can be given in decimal with a suffix 'dN' where
+# 'N' is the number of bytes.  These are converted in little-endian
+# The value 'L' can be given which is th 2-byte length
+# of the rest of the message
+#
+# you can send the packet to GPS chip like this:
+#
+# cat packet.ubx > /dev/ttySAC1
+
+import sys
+import binascii
+
+cs0=0
+cs1=0
+
+sys.stdout.write("\xb5\x62")
+
+outbuf = []
+leng = None
+
+for d in sys.argv[1:]:
+    if d == 'L':
+        leng = len(outbuf)
+        outbuf.append(0)
+        outbuf.append(0)
+    elif 'd' in d:
+        p = d.index('d')
+        bytes = int(d[p+1:])
+        d = int(d[0:p])
+        while bytes > 0:
+            b = d % 256
+            d = int(d/256)
+            outbuf.append(b)
+            bytes -= 1
+    else:
+        c = binascii.unhexlify(d)
+        outbuf.append(ord(c))
+
+if leng != None:
+    l = len(outbuf) - (leng + 2)
+    outbuf[leng] = l % 256
+    outbuf[leng+1] = int(l/256)
+
+for c in outbuf:
+    sys.stdout.write(chr(c))
+    cs0 += c
+    cs0 &= 255
+    cs1 += cs0
+    cs1 &= 255
+
+sys.stdout.write(chr(cs0)+chr(cs1))
diff --git a/gsm/BUG b/gsm/BUG
new file mode 100644 (file)
index 0000000..8154013
--- /dev/null
+++ b/gsm/BUG
@@ -0,0 +1,18 @@
+Thu Aug 30 22:30:11 2012 state becomes flight
+Thu Aug 30 22:30:11 2012 check flightmode got 1
+Thu Aug 30 22:30:11 2012 advance flight chooses 0, 0
+Thu Aug 30 22:30:11 2012 send AT command +CFUN=0 2000
+Traceback (most recent call last):
+  File "/usr/local/bin/gsmd", line 859, in <module>
+    a = GsmD('/dev/ttyHS_Application')
+  File "/usr/local/bin/gsmd", line 648, in __init__
+    self.advance()
+  File "/usr/local/bin/gsmd", line 746, in advance
+    control[self.gstate][t].start(self)
+  File "/usr/local/bin/gsmd", line 106, in start
+    self.advance(channel)
+  File "/usr/local/bin/gsmd", line 170, in advance
+    channel.atcmd(at)
+  File "/usr/local/bin/atchan.py", line 114, in atcmd
+    self.sock.write('AT' + cmd + '\r')
+AttributeError: 'NoneType' object has no attribute 'write'
diff --git a/gsm/TOFIX b/gsm/TOFIX
new file mode 100644 (file)
index 0000000..bff8aba
--- /dev/null
+++ b/gsm/TOFIX
@@ -0,0 +1,36 @@
+Sun Dec 23 10:36:08 2012 send AT command +CPAS 2000
+Sun Dec 23 10:36:08 2012 received AT response +CPAS: 4
+Sun Dec 23 10:36:08 2012 call_status got +CPAS: 4
+Sun Dec 23 10:36:08 2012 s = 4
+Sun Dec 23 10:36:08 2012 received AT response OK
+Sun Dec 23 10:36:08 2012 call_status got OK
+Sun Dec 23 10:36:08 2012 advance on-call chooses 0, 2000
+Sun Dec 23 10:36:08 2012 Sleeping for 2.000000 seconds
+Sun Dec 23 10:36:08 2012 received AT response OK
+Sun Dec 23 10:36:08 2012 received AT response OK
+Sun Dec 23 10:36:08 2012 received AT response +CPAS: 4
+Sun Dec 23 10:36:08 2012 received AT response OK
+Sun Dec 23 10:36:09 2012 Check call got 0415836820
+Sun Dec 23 10:36:10 2012 Timer Fired
+Sun Dec 23 10:36:10 2012 advance on-call chooses 0, 5
+Sun Dec 23 10:36:10 2012 Sleeping for 0.005000 seconds
+Sun Dec 23 10:36:10 2012 Timer Fired
+Sun Dec 23 10:36:10 2012 advance on-call chooses 0, 5
+Sun Dec 23 10:36:10 2012 Sleeping for 0.005000 seconds
+Sun Dec 23 10:36:10 2012 Timer Fired
+Sun Dec 23 10:36:10 2012 advance on-call chooses 0, 4
+Sun Dec 23 10:36:10 2012 Sleeping for 0.004000 seconds
+Sun Dec 23 10:36:10 2012 Timer Fired
+Sun Dec 23 10:36:10 2012 advance on-call chooses 2, 0
+Sun Dec 23 10:36:10 2012 send AT command +CPAS 2000
+Sun Dec 23 10:36:10 2012 Timer Fired
+Sun Dec 23 10:36:10 2012 send AT command  2000
+Sun Dec 23 10:36:10 2012 Timer Fired
+Sun Dec 23 10:36:10 2012 send AT command  2000
+Sun Dec 23 10:36:10 2012 Timer Fired
+Sun Dec 23 10:36:10 2012 send AT command  2000
+Sun Dec 23 10:36:10 2012 received AT response +CPAS: 4
+Sun Dec 23 10:36:10 2012 received AT response OK
+Sun Dec 23 10:36:10 2012 call_status got OK
+Sun Dec 23 10:36:10 2012 send AT command +CPAS 2000
+:
diff --git a/gsm/atchan.py b/gsm/atchan.py
new file mode 100644 (file)
index 0000000..bc92b23
--- /dev/null
@@ -0,0 +1,208 @@
+
+#
+# Handle a connection to an AT device via /dev/ttyXX
+#
+# We directly support high level commands (reset_modem
+# etc) but don't know anything about AT commands - we just send them
+# through and hand back reply.  Replies also go via a callback
+# We also provide timeout support, but someone else needs to tell us
+# when to set a timeout, and when to clear it.
+#
+# This is usually subclassed by code with an agenda.
+
+import gobject, sys, os, time
+import termios
+from tracing import log
+from socket import *
+
+class AtChannel:
+    def __init__(self, path):
+        self.path = path
+        self.connected = False
+        self.watcher = None
+        self.sock = None
+        self.buf = ""
+        self.linelist = []
+
+        self.pending = False
+        self.timer = None
+
+    def disconnect(self):
+        if self.watcher:
+            gobject.source_remove(self.watcher)
+            self.watcher = None
+        if self.sock:
+            self.sock.close()
+            self.sock = None
+        self.connected = False
+
+    def connect(self):
+        if self.sock != None:
+            return
+        log("connect to", self.path)
+        s = open(self.path,"r+")
+        #s.setblocking(0)
+        fd = s.fileno()
+        attr = termios.tcgetattr(fd)
+        attr[3] = attr[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG
+        attr[6][termios.VMIN] = 0
+        attr[6][termios.VTIME] = 0
+        termios.tcsetattr(fd, termios.TCSANOW, attr)
+        
+        self.watcher = gobject.io_add_watch(s, gobject.IO_IN, self.readdata)
+        self.sock = s
+        self.connected = True
+
+    def readdata(self, io, arg):
+        try:
+            r = self.sock.read(1000)
+        except IOError:
+            # no data there really.
+            return True
+        if not r:
+            # pipe closed
+            if io != None:
+                self.getline(None)
+            return False
+        r = self.buf + r
+        ra = r.split('\n')
+        self.buf = ra[-1];
+        del ra[-1]
+        for ln in ra:
+            ln = ln.strip('\r')
+            self.getline(ln)
+        # FIXME this should be configurable
+        if self.buf == '> ':
+            self.getline(self.buf)
+            self.buf = ''
+        return True
+
+    def getline(self, line):
+        if line == None:
+            log("received EOF")
+            self.takeline(line)
+            if self.pending:
+                self.pending = False
+                gobject.source_remove(self.timer)
+                self.timer = None
+            return
+        if len(line):
+            log("received AT response", line)
+        if self.takeline(line):
+            if self.pending:
+                self.pending = False
+                gobject.source_remove(self.timer)
+                self.timer = None
+
+    def atcmd(self, cmd, timeout = 2000):
+        """
+        Send the command, preceeded by 'AT' and set a timeout.
+        self.takeline() should return True when the command
+        has been responded to, otherwise we will call
+        self.timedout() after the time.
+        """
+        self.set_timeout(timeout)
+        log("send AT command", cmd, timeout)
+        try:
+            self.sock.write('AT' + cmd + '\r')
+            self.sock.flush()
+        except IOError:
+            self.cancel_timeout()
+            self.set_timeout(10)
+
+    def timer_fired(self):
+        log("Timer Fired")
+        self.pending = False
+        self.timer = None
+        self.timedout()
+        return False
+    
+    def set_timeout(self, delay):
+        if self.pending:
+            raise ValueError
+        self.timer = gobject.timeout_add(delay, self.timer_fired)
+        self.pending = True
+
+    def cancel_timeout(self):
+        if self.pending:
+            gobject.source_remove(self.timer)
+            self.pending = False
+            
+    def abort_timeout(self):
+        if self.pending:
+            self.cancel_timeout()
+            self.set_timeout(0)
+
+    # these are likely to be over-ridden by a child class
+    def takeline(self, line):
+        self.linelist.append(line)
+
+    def wait_line(self, timeout):
+        self.cancel_timeout()
+        self.set_timeout(timeout)
+        if len(self.linelist) == 0:
+            self.readdata(None, None)
+        c = gobject.main_context_default()
+        while not self.linelist and self.pending:
+            c.iteration()
+        if self.linelist:
+            self.cancel_timeout()
+            l = self.linelist[0]
+            del self.linelist[0]
+            return l
+        else:
+            return None
+    def timedout(self):
+        pass
+
+
+    def chat(self, mesg, resp, timeout = 1000):
+        """
+        Send the message (if not 'None') and wait up to
+        'timeout' for one of the responses (regexp)
+        Return None on timeout, or number of response.
+        combined with an array of the messages received.
+        """
+        if mesg:
+            log("send command", mesg)
+            try:
+                self.sock.write(mesg + '\r\n')
+                self.sock.flush()
+            except error:
+                timeout = 10
+
+        conv = []
+        while True:
+            l = self.wait_line(timeout)
+            if l == None:
+                return (None, conv)
+            conv.append(l)
+            for i in range(len(resp)):
+                ptn = resp[i]
+                if type(ptn) == str:
+                    if ptn == l.strip():
+                        return (i, conv)
+                else:
+                    if resp[i].match(l):
+                        return (i, conv)
+
+    def chat1(self, mesg, resp, timeout=1000):
+        n,c = self.chat(mesg, resp, timeout = timeout)
+        return n
+
+
+def found(list, patn):
+    """
+    see if patn can be found in the list of strings
+    """
+    for l in list:
+        l = l.strip()
+        if type(patn) == str:
+            if l == patn:
+                return True
+        else:
+            p = patn.match(l)
+            if p:
+                return p
+    return False
+
diff --git a/gsm/gsm-carriers.py b/gsm/gsm-carriers.py
new file mode 100755 (executable)
index 0000000..ff54da8
--- /dev/null
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+
+# get list of currently available carriers from Option 3G
+#
+from atchan import AtChannel, found
+import sys, re
+
+chan = AtChannel(path='/dev/ttyHS_Control')
+chan.connect()
+
+if chan.chat1('ATE0', ['OK','ERROR']) != 0:
+       sys.exit(1)
+
+n,c = chan.chat('AT+COPS=?', ['OK','ERROR'], timeout=45000)
+if n == None and len(c) == 0:
+       # need to poke to get a response
+       n,c = chan.chat('', ['OK','ERROR'], timeout=10000)
+if n == None and len(c) == 0:
+       # need to poke to get a response
+       n,c = chan.chat('', ['OK','ERROR'], timeout=10000)
+
+if n != 0:
+       sys.exit(1)
+
+
+m = found(c, re.compile('^\+COPS: (.*)'))
+if m:
+       clist = re.findall('\((\d+),"([^"]*)","([^"]*)","([^"]*)",(\d+)\)', m.group(1))
+       for (mode, long, short, num, type) in clist:
+               print num, type, '"%s" "%s"' % (short, long)
+
diff --git a/gsm/gsm-data.py b/gsm/gsm-data.py
new file mode 100644 (file)
index 0000000..5816591
--- /dev/null
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+
+# establish data connection on Option 3G module.
+#
+# AT+CGDCONT=1,"IP","${APN}"
+# OK
+# AT_OWANCALL=1,1,1
+# OK
+# _OWANCALL: 1, 1
+# AT_OWANDATA?
+# _OWANDATA: 1, 10.6.177.182, 0.0.0.0, 211.29.132.12, 61.88.88.88, 0.0.0.0, 0.0.0.0,144000
+# ifconfig hso0 10.6.177.182
+# route add default dev hso0
+# echo nameserver 211.29.132.12 > /etc/resolv.conf
+# echo nameserver 61.88.88.88 >> /etc/resolv.conf
+#
+# ...but....
+# we might already be connected, and we might not know, and just
+# stopping/starting doesn't seem to do much - no notification.
+# so:
+# - first check status.  If up, configure and done
+# - if not, enable and wait.
+# - on timeout, check status again.
+
+#APN="exetel1"
+APN="INTERNET"
+import atchan, sys, re, os
+
+def check_status(chan):
+    n,c = chan.chat('AT_OWANDATA?', ['OK','ERROR'])
+    want = re.compile('_OWANDATA: 1, ([0-9.]+), [0-9.]+, ([0-9.]+), ([0-9.]+), [0-9.]+, [0-9.]+,\d+$')
+    if n == 0:
+        m = atchan.found(c, want)
+    if n != 0 or not m:
+        return False
+    return m
+
+def configure(m):
+    g = m.groups()
+    ip = g[0]
+    ns1= g[1]
+    ns2= g[2]
+    print ip, ns1, ns2
+    os.system("/sbin/ifconfig hso0 %s" % ip)
+    os.system('route add default dev hso0')
+    f = open("/etc/resolv.conf", "w")
+    f.write("nameserver %s\n" % ns1)
+    f.write("nameserver %s\n" % ns2)
+    f.close()
+
+def disconnect(chan):
+    n,c = chan.chat('AT_OWANCALL=1,0,0', ['OK','ERROR'])
+
+chan = atchan.AtChannel(path="/dev/ttyHS_Control")
+chan.connect()
+
+chan.chat1('ATE0', ['OK','ERROR'])
+if chan.chat1('AT+CGDCONT=1,"IP","%s"' % APN, ['OK','ERROR']) != 0:
+    print 'Could not set APN'
+    sys.exit(1)
+
+m = check_status(chan)
+
+if sys.argv[1] == 'status':
+    if m:
+        print "Active: ", m.groups()[0]
+    else:
+        print "inactive"
+    sys.exit(0)
+if sys.argv[1] == "off":
+    if m:
+        disconnect(chan)
+    else:
+        print 'DATA already disconnected'
+    os.system('route delete default dev hso0')
+    os.system('/sbin/ifconfig hso0 down')
+    sys.exit(0)
+
+if m:
+    print 'already active'
+    configure(m)
+    sys.exit(0)
+
+want = re.compile('^_OWANCALL: *\d+, *(\d*)$')
+
+if chan.chat1('AT_OWANCALL=1,1,1', ['OK','ERROR']) != 0:
+    print 'Could not start data connection'
+    sys.exit(1)
+l = chan.wait_line(10000)
+if l == None:
+    print 'No response for DATA connect'
+else:
+    m = want.match(l)
+    if m and m.groups()[0] == '1':
+        print 'Connected'
+    else:
+        print 'Connect failed'
+        sys.exit(1)
+
+m = check_status(chan)
+if m:
+    configure(m)
+else:
+    print 'Sorry, could not connect'
+    #disconnect(chan)
diff --git a/gsm/gsm-getsms.py b/gsm/gsm-getsms.py
new file mode 100644 (file)
index 0000000..01eaebc
--- /dev/null
@@ -0,0 +1,412 @@
+#!/usr/bin/env python
+# coding=UTF-8
+
+# Collect SMS messages from the GSM device.
+# We store a list of messages that are thought to be
+# in the SIM card: number from date
+#  e.g.   17 61403xxxxxx 09/02/17,20:28:36+44
+# As we read messages, if we find one that is not in that list,
+# we record it in the SMS store, then update the list
+#
+# An option can specify either 'new' or 'all.
+# 'new' will only ask for 'REC UNREAD' and so will be faster and so
+# is appropriate when we know that a new message has arrived.
+# 'all' reads all messages and so is appropriate for an occasional
+# 'sync' like when first turning the phone on.
+#
+# If we discover that the SMS card is more than half full, we
+# deleted the oldest messages.
+# We discover this by 'all' finding lots of messages, or 'new'
+# finding a message with a high index.
+# For now, we "know" that the SIM card can hold 30 messages.
+#
+# We need to be careful about long messages.  A multi-part message
+# looks like e.g.
+#+CMGL: 19,"REC UNREAD","61403xxxxxx",,"09/02/18,10:51:46+44",145,140
+#0500031C0201A8E8F41C949E83C2207B599E07B1DFEE33A85D9ECFC3E732888E0ED34165FCB85C26CF41747419344FBBCFEC32A85D9ECFC3E732889D6EA7E9A0F91B444787E9A024681C7683E6E532E88E0ED341E939485E1E97D3F6321914A683E8E832E84D4797E5A0B29B0C7ABB41ED3CC8282F9741F2BADB5D96BB40D7329B0D9AD3D36C36887E2FBBE9
+#+CMGL: 20,"REC UNREAD","61403xxxxxx",,"09/02/18,10:51:47+44",145,30
+#0500031C0202F2A0B71C347F83D8653ABD2C9F83E86FD0F90D72B95C2E17
+
+# what about:
+# 0281F001000081000019C9531BF4AED3E769721944479741F4F7DD0D4287D96C
+# 02 81F0 ??
+#       010000810000
+#                   19 (25 bytes)
+#                     C9531BF4AED3E769721944479741F4F7DD0D4287D96C
+#                     I'm outside the town hall
+# This is a saved message.
+#
+# If that was just hex you could use
+#  perl -e 'print pack("H*","050....")'
+# to print it.. but no...
+# Looks like it decodes as:
+# 05  - length of header, not including this byte
+# 00  - concatentated SMS with 8 bit ref number (08 means 16 bit ref number)
+# 03  - length of rest of header
+# 1C  - ref number for this concat-SMS
+# 02  - number of parts in this SMS
+# 01  - number of this part - counting starts from 1
+# A8E8F41C949E83C22.... message, 7 bits per char. so:
+#  A8  - 54 *2 + 0   54 == T           1010100 0     1010100
+# 0E8  - 68 *2 + 0   68 == h           1 1101000     1101000
+#  F4  -             69 == i           11 110100     1101001  1
+#  1C                73 == s           000 11100     1110011  11
+#  94                20 == space       1001 0100     0100000  000
+#  9E                69 == i           10011 110     1101001  1001
+#  83                73 == s           100000 11     1110011  10011
+#                    20 == space                    0100000   0100000
+
+# 153 characters in first message. 19*8 + 1
+# that uses 19*7+1 == 134 octets
+# There are 6 in the header so a total of 140
+# second message has 27 letters - 3*8+3
+# That requires 3*7+3 == 24 octets.  30 with the 6 octet header.
+
+# then there are VCARD messages that look lie e.g.
+#+CMGL: 2,"REC READ","61403xxxxxx",,"09/01/29,13:01:26+44",145,137
+#06050423F40000424547494E3A56434152440D0A56455253494F4E3A322E310D0A4E3A....0D0A454E443A56434152440D0A
+#which is
+#06050423F40000
+#then
+#BEGIN:VCARD
+#VERSION:2.1
+#N: ...
+#...
+#END:VCARD
+# The 06050423f40000
+# might decode like:
+#  06  - length of rest of header
+#  05  - magic code meaning 'user data'
+#  04  - length of rest of header...
+#  23  - 
+#  f4  -  destination port '23f4' means 'vcard'
+#  00  -   
+#  00  -  0000 is the origin port.
+#
+#in hex/ascii
+#
+# For now, ignore anything longer than the specified length.
+
+import os
+import suspend
+#os.environ['PYTRACE'] = '1'
+
+import atchan, sys, re
+from storesms import SMSmesg, SMSstore
+
+
+def load_mirror(filename):
+    # load an array of index address date
+    # from the file and store in a hash
+    rv = {}
+    try:
+        f = file(filename)
+    except IOError:
+        return rv
+    l = f.readline()
+    while l:
+        fields = l.strip().split(None, 1)
+        rv[fields[0]] = fields[1]
+        l = f.readline()
+    return rv
+
+def save_mirror(filename, hash):
+    n = filename + '.new'
+    f = open(n, 'w')
+    for i in hash:
+        f.write(i + ' ' + hash[i] + '\n')
+    f.close()
+    os.rename(n, filename)
+
+# GSM uses a 7-bit code that is not the same as ASCII...
+# -*- coding: utf8 -*- 
+gsm = (u"@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\x1bÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?"
+       u"¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑÜ`¿abcdefghijklmnopqrstuvwxyzäöñüà")
+ext = (u"````````````````````^```````````````````{}`````\\````````````[~]`"
+       u"|````````````````````````````````````€``````````````````````````")
+
+# Take a unicode string and produce a byte string for GSM
+def gsm_encode(plaintext):
+    res = ""
+    for c in plaintext:
+        idx = gsm.find(c);
+        if idx != -1:
+            res += chr(idx)
+            continue
+        idx = ext.find(c)
+        if idx != -1:
+            res += chr(27)
+            res += chr(idx)
+    return res
+# take a GSM byte string (7-in-8 conversion already done) and produce unicode
+def gsm_decode(code):
+    uni = u''
+    esc = False
+    for c in code:
+        n = ord(c)
+        if esc:
+            uni += ext[n]
+            esc = False
+        elif n == 27:
+            esc = True
+        else:
+            uni += gsm[n]
+    return uni
+
+
+def sms_decode(msg,pos = 1):
+    #msg is a 7-in-8 encoding of a longer message.
+    carry = 0
+    str = ''
+    while msg:
+        c = msg[0:2]
+        msg = msg[2:]
+        b = int(c, 16)
+        if pos == 0:
+            if carry:
+                str += chr(carry + (b&1)*64)
+                carry = 0
+            b /= 2
+        else:
+            b = (b << (pos-1)) | carry
+            carry = (b & 0xff80) >> 7
+            b &= 0x7f
+        if (b & 0x7f) != 0:
+            str += chr(b&0x7f)
+        pos = (pos+1) % 7
+    return gsm_decode(str)
+
+def sms_unicode_decode(msg):
+    # 2bytes unicode numbers - 4 syms each
+    m = u''
+    for i in range(len(msg)/4):
+        c = int(msg[i*4:i*4+4],16)
+        m += unichr(c)
+    return m
+
+def cvt_telnum(type, msg, len):
+    if type == '81' or type  == '91':
+        n = ''
+        for i in range(len):
+            n += msg[i + 1 - (i%2)*2]
+        if type == '91':
+            return '+' + n
+        else:
+            return n
+    if type == 'D0':
+        return sms_decode(msg)
+    return "?" + type + msg
+
+def cvt_date(msg):
+    #YYMMDDHHMMSSZZ -> 20YY/MM/DD HH:MM:SS+ZZ swapping letters
+    sep='0//,::+'
+    dt = '2'
+    for i in range(len(msg)/2):
+        dt += sep[i] + msg[i*2+1] + msg[i*2]
+    return dt
+
+
+def main():
+    mode = 'all'
+    for a in sys.argv[1:]:
+        if a == '-n':
+            mode = 'new'
+        else:
+            print "Unknown option:", a
+            sys.exit(1)
+
+    pth = None
+    for p in ['/data','/media/card','/var/tmp']:
+        if os.path.exists(os.path.join(p,'SMS')):
+            pth = p
+            break
+
+    if not pth:
+        print "Cannot find SMS directory"
+        sys.exit(1)
+
+    dir = os.path.join(pth, 'SMS')
+    print "Using ", dir
+    store = SMSstore(dir)
+
+    chan = atchan.AtChannel(path = '/dev/ttyHS_Control')
+    chan.connect()
+    
+    # consume any extra 'OK' that might be present
+    chan.chat1('', ['OK', 'ERROR']);
+    if chan.chat1('ATE0', ['OK', 'ERROR']) != 0:
+        sys.exit(1)
+
+    # get ID of SIM card
+    n,c = chan.chat('AT+CIMI', ['OK', 'ERROR'])
+    CIMI='unknown'
+    for l in c:
+        l = l.strip()
+        if re.match('^\d+$', l):
+            CIMI = l
+
+    mfile = os.path.join(dir, '.sim-mirror-'+CIMI)
+    #FIXME lock mirror file
+    mirror = load_mirror(mfile)
+
+    chan.chat('AT+CMGF=0', ['OK','ERROR'])
+    if mode == 'new':
+        chan.atcmd('+CMGL=0')
+    else:
+        chan.atcmd('+CMGL=4')
+
+    # reading the msg list might fail for some reason, so
+    # we always prime the mirror list with the previous version
+    # and only replace things, never assume they aren't there
+    # because we cannot see them
+    newmirror = mirror
+    mirror_seen = {}
+
+    l = ''
+    state = 'waiting'
+    msg = ''
+    # text format
+    #                            indx  state    from          name       date          type len
+    #+CMGL: 40,"REC READ","+61406022084",,"12/03/14,18:00:40+44"
+    # PDU MODE
+    #+CMGL: index, 0-4, unread read unsent sent all, ?? , byte len after header
+    #+CMGL: 3,1,,40
+    #07911614786007F0 040B911654999946F100002120217035534417CE729ACD02BDD7203A3AEC5ECF5D2062D9ED4ECF01
+    #                       61450000641F000012021207533544
+    # 07 is length (octets) of header (911614786007F0)
+    #  91 is SMSC number type; 81 is local? 91 is international D0 is textual D0456C915A6402 == EXETEL
+    #  1614786007F0 is the SMSC number: 61418706700
+    #
+    #   04 is "SMS-DELIVER" and some other details  44 == ?? 24??
+    #    0B is length of sende number (11)
+    #     91 is type as above
+    #      1654999946F1 is number: 61459999641 (F is padding)
+    #       00 is protocol id - always 0 ??
+    #        00 is Data coding scheme.  00 is 7-bit default
+    #         21202170355344 is stime stamp: 12/02/12 07:35:53+44
+    #          17 is length of body (23)
+    #           CE729ACD02BDD7203A3AEC5ECF5D2062D9ED4ECF01 is 7-in-8 message
+    #
+    # other coding schemes:
+    #  08 is 16 bit unicode
+    #  11 is VCARD: 06050400E20080 C2E231E9D459874129B1A170EA886176B9A1A016993A182D26B3E164B919AEA1283A893AEB3028253614
+    #         looks like 7-in-8 with a 7/8 header 
+    #      or can be just a message.
+    #     or ??? (message from AUST POST)
+    #  01 is much like 00 ??
+
+
+    want = re.compile('^\+CMGL: (\d+),(\d+),("[^"]*")?,(\d+)$')
+
+    found_one = False
+    while state == 'reading' or not (l[0:2] == 'OK' or l[0:5] == 'ERROR' or
+                                     l[0:10] == '+CMS ERROR'):
+        l = chan.wait_line(1000)
+        if l == None:
+            sys.exit(1)
+        print "Got (",state,")", l
+        if state == 'reading':
+            msg = l
+            if designation != '0' and designation != '1':
+                #send, not recv
+                state = 'waiting'
+                continue
+            if len(msg) >= msg_len:
+                state = 'waiting'
+                hlen = int(msg[0:2], 16)
+                hdr = msg[2:(2+hlen*2)]
+                # hdr is the sending number - don't care
+                msg = msg[2+hlen*2:]
+                # typ == 04 - SMS-DELIVER
+                typ = int(msg[0:2],16)
+                nlen = int(msg[2:4], 16)
+                ntype = msg[4:6]
+                numlen = (nlen + nlen % 2)
+                sender = cvt_telnum(ntype, msg[6:6+numlen], nlen)
+                msg = msg[6+numlen:]
+                proto = msg[0:2]
+                coding = msg[2:4]
+                date = cvt_date(msg[4:18])
+                bdy_len = int(msg[18:20], 16)
+                body = msg[20:]
+                ref = None; part = None
+
+                if body[0:6] == '050003':
+                    # concatenated message with 8bit ref number
+                    ref  = body[6:8]
+                    part = ( int(body[10:12],16), int(body[8:10], 16))
+                    if coding == '08':
+                        txt = sms_unicode_decode(body)
+                    else:
+                        txt = sms_decode(body[12:],0)
+                elif body[0:6] == '060504':
+                    # VCARD ??
+                    txt = sms_decode(body[14:])
+                elif coding == '00':
+                    txt = sms_decode(body)
+                elif coding == '11':
+                    txt = sms_decode(body)
+                elif coding == '01':
+                    txt = sms_decode(body)
+                elif coding == '08':
+                    txt = sms_unicode_decode(body)
+                else:
+                    print "ignoring", index, sender, date, body
+                    continue
+                if ref == None:
+                    print "found", index, sender, date, txt.encode('utf-8')
+                else:
+                    print "found", index, ref, part, sender, date, repr(txt)
+
+                if index in mirror and mirror[index] == date[2:] + ' ' + sender:
+                    print "Already have that one"
+                else:
+                    sms = SMSmesg(source='GSM', time=date[2:], sender=sender,
+                                  text = txt.encode('utf-8'), state = 'NEW',
+                                  ref= ref, part = part)
+                    store.store(sms)
+                    found_one = True
+                newmirror[index] = date[2:] + ' ' + sender
+                mirror_seen[index] = date[2:] + ' ' + sender
+        else:
+            m = want.match(l)
+            if m:
+                g = m.groups()
+                index = g[0]
+                designation = g[1]
+                msg_len = int(g[3], 10)
+                msg = ''
+                state = 'reading'
+
+    mirror = newmirror
+
+    if len(mirror) > 10:
+        rev = {}
+        dlist = []
+        for i in mirror:
+            rev[mirror[i]+' '+str(i)] = i
+            dlist.append(mirror[i]+' '+str(i))
+        dlist.sort()
+        for i in range(len(mirror) - 10):
+            dt=dlist[i]
+            ind = rev[dt]
+            print 'del', i, dt, ind
+            resp = chan.chat1('AT+CMGD=%s' % ind, ['OK', 'ERROR', '+CMS ERROR'],
+                              timeout=3000)
+            if resp == 0 or ind not in mirror_seen:
+                del mirror[ind]
+
+    save_mirror(mfile, mirror)
+    if found_one:
+        try:
+            f = open("/var/run/alert/sms", "w")
+            f.write("new")
+            f.close()
+            suspend.abort_cycle()
+        except:
+            pass
+    if mode == 'new' and not found_one:
+        sys.exit(1)
+    sys.exit(0)
+
+main()
diff --git a/gsm/gsm-sms.py b/gsm/gsm-sms.py
new file mode 100644 (file)
index 0000000..d410d92
--- /dev/null
@@ -0,0 +1,186 @@
+#!/usr/bin/env python
+
+# Send an SMS message using GSM.
+# Args are:
+#   sender - ignored
+#   recipient - phone number
+#   message - no newlines
+#
+# We simply connect to the GSM module,
+# check for a registration
+# and
+#   AT+CMGS="recipient"
+#   >  message
+#   >  control-Z
+#
+# Sending multipart sms messages:
+#    ref: http://www.developershome.com/sms/cmgsCommand4.asp
+# 1/ set PDU mode with AT+CMGF=0
+# 2/ split message into 153-char bundles
+# 3/ create messages as follows:
+#
+#  00   - this says we aren't providing an SMSC number
+#  41   - TPDU header - type is SMS-SUBMIT, user-data header present
+#  00   - please assign a message reference number
+#  xx   - length in digits of phone number
+#  91 for IDD, 81 for "don't know what sort of number this is"
+#  164030...  BCD phone number, nibble-swapped, pad with F at end if needed
+#  00   - protocol identifier??
+#  00   - encoding - 7 bit ascii
+#  XX   - length of message in septets
+# Then the message which starts with 7 septets of header that looks like 6 octets.
+#  05   -  length of rest of header
+#  00   -  multi-path with 1 byte id number
+#  03   -  length of rest
+#  idnumber - random id number
+#  parts    - number of parts
+#  this part - this part, starts from '1'
+#
+# then AT+CMGS=len-of-TPDU in octets
+#
+# TPDU header byte:
+# Structure: (n) = bits
+# +--------+----------+---------+-------+-------+--------+
+# | RP (1) | UDHI (1) | SRI (1) | X (1) | X (1) | MTI(2) |
+# +--------+----------+---------+-------+-------+--------+
+#      RP:
+#              Reply path
+#      UDHI:
+#              User Data Header Indicator = Does the UD contains a header
+#              0 : Only the Short Message
+#              1 : Beginning of UD containsheader information
+#      SRI:
+#              Status Report Indication.
+#              The SME (Short Message Entity) has requested a status report.
+#      MTI:
+#              00 for SMS-Deliver
+#               01 for SMS-SUBMIT
+
+
+import atchan, sys, re, random
+
+def encode_number(recipient):
+    # encoded number is
+    # number of digits
+    # 91 for international, 81 for local interpretation
+    # BCD digits, nibble-swapped, F padded.
+    if recipient[0] == '+':
+        type = '91'
+        recipient = recipient[1:]
+    else:
+        type = '81'
+    leng = '%02X' % len(recipient)
+    if len(recipient) % 2 == 1:
+        recipient += 'F'
+    swap = ''
+    while recipient:
+        swap += recipient[1] + recipient[0]
+        recipient = recipient[2:]
+    return leng + type + swap
+
+def code7(pad, mesg):
+    # Encode the message as 8 chars in 7 bytes.
+    # pad with 'pad' 0 bits at the start (low in the byte)
+    carry = 0
+    # we have 'pad' low bits stored low in 'carry'
+    code = ''
+    while mesg:
+        c = ord(mesg[0])
+        mesg = mesg[1:]
+        if pad + 7 >= 8:
+            # have a full byte
+            b = carry & ((1<<pad)-1)
+            b += (c << pad) & 255
+            code += '%02X' % b
+            pad -= 1
+            carry = c >> (7-pad)
+        else:
+            # not a full byte yet, just a septet
+            pad = 7
+            carry = c
+    if pad:
+        # a few bits still to emit
+        b = carry & ((1<<pad)-1)
+        code += '%02X' % b
+    return code
+
+def add_len(mesg):
+    return ('%02X' % (len(mesg)/2)) + mesg
+
+def send(chan, dest, mesg):
+    n,c = chan.chat('AT+CMGS=%d' % (len(mesg)/2), ['OK','ERROR','>'])
+    if n == 2:
+        n,c = chan.chat('%s%s\032' % (dest, mesg), ['OK','ERROR'], 10000)
+        if n == 0 and atchan.found(c, re.compile('^\+CMGS: \d+') ):
+            return True
+        # Sometimes don't get the +CMGS: status - strange
+        if n == 0:
+            return True
+    return False
+
+recipient = sys.argv[2]
+mesg = sys.argv[3]
+
+chan = atchan.AtChannel(path="/dev/ttyHS_Control")
+chan.connect()
+
+# clear any pending error status
+chan.chat1('ATE0', ['OK', 'ERROR'])
+if chan.chat1('ATE0', ['OK', 'ERROR']) != 0:
+    print 'system error - message not sent'
+    sys.exit(1)
+
+want = re.compile('^\+COPS:.*".+"')
+n,c =  chan.chat('AT+COPS?', ['OK', 'ERROR'], 2000 )
+if n != 0 or not atchan.found(c, want):
+    print 'No Service - message not sent'
+    sys.exit(1)
+
+# use PDU mode
+n,c = chan.chat('AT+CMGF=0', ['OK', 'ERROR'])
+if n != 0:
+    print 'Unknown error'
+    sys.exit(1)
+
+# SMSC header and TPDU header
+SMSC = '00' # No SMSC number given, use default
+##SMSC = '07911614910930F1' # This is my SMSC
+REF = '00'  # ask network to assign ref number
+phone_num = encode_number(recipient)
+proto = '00' # don't know what this means
+encode = '00' # 7 bit ascii
+if len(mesg) <= 160:
+    # single SMS mesg
+    m1 = code7(0,mesg)
+    m2 = '%02X'%len(mesg) + m1
+    coded = "01" + REF + phone_num + proto + encode + m2
+    if send(chan, SMSC, coded):
+        print "OK message sent"
+        sys.exit(0)
+    else:
+        print "ERROR message not sent"
+        sys.exit(1)
+
+elif len(mesg) <= 5 * 153:
+    # Multiple messsage
+    packets = (len(mesg) + 152) / 153
+    packet = 0
+    mesgid = random.getrandbits(8)
+    while len(mesg) > 0:
+        m = mesg[0:153]
+        mesg = mesg[153:]
+        id = mesgid
+        packet = packet + 1
+        UDH = add_len('00' + add_len('%02X%02X%02X'%(id, packets, packet)))
+        m1 = UDH + code7(1, m)
+        m2 = '%02X'%(7+len(m)) + m1
+        coded = "41" + REF + phone_num + proto + encode + m2
+        if not send(chan, SMSC, coded):
+            print "ERROR message not sent at part %d/%d" % (packet,packets)
+            sys.exit(1)
+    print 'OK message sent in %d parts' % packets
+    sys.exit(0)
+else:
+    print 'Message is too long'
+    sys.exit(1)
+
diff --git a/gsm/gsmd-old b/gsm/gsmd-old
new file mode 100755 (executable)
index 0000000..5a09f30
--- /dev/null
@@ -0,0 +1,867 @@
+#!/usr/bin/env python
+
+#
+# Calls can be made by writing a number to
+#  /run/gsm-state/call
+# Status get set call 'Calling' and then 'BUSY' or ''
+# Call can be answered by writing 'answer' to 'call'
+# or can be cancelled by writing ''.
+# During a call, chars can be written to
+#  /run/gsm-state/dtmf
+# to send tones.
+
+## FIXME
+# e.g. receive AT response +CREG: 1,"08A7","6E48"
+#  show that SIM is now ready
+# cope with /var/lock/suspend not existing yet
+#  define 'reset'
+
+import re, time, gobject, os
+from atchan import AtChannel
+import dnotify, suspend
+from tracing import log
+from subprocess import Popen
+
+def record(key, value):
+    f = open('/run/gsm-state/.new.' + key, 'w')
+    f.write(value)
+    f.close()
+    os.rename('/run/gsm-state/.new.' + key,
+              '/run/gsm-state/' + key)
+
+def recall(key):
+    try:
+        fd = open("/run/gsm-state/" + key)
+        l = fd.read(1000)
+        fd.close()
+    except IOError:
+        l = ""
+    return l.strip()
+
+def set_alert(key, value):
+    path = '/run/alert/' + key
+    if value == None:
+        try:
+            os.unlink(path)
+        except OSError:
+            pass
+    else:
+        try:
+            f = open(path, 'w')
+            f.write(value)
+            f.close()
+        except IOError:
+            pass
+
+def calllog(key, msg):
+    f = open('/var/log/' + key, 'a')
+    now = time.strftime("%Y-%m-%d %H:%M:%S")
+    f.write(now + ' ' + msg + "\n")
+    f.close()
+
+class Task:
+    def __init__(self, repeat):
+        self.repeat = repeat
+        pass
+    def start(self, channel):
+        # take the first action for this task
+        pass
+    def takeline(self, channel, line):
+        # a line has arrived that is presumably for us
+        pass
+    def timeout(self, channel):
+        # we asked for a timeout and got it
+        pass
+
+
+class AtAction(Task):
+    # An AtAction involves:
+    #   optional sending an AT command to check some value
+    #      matching the result against a string, possibly storing the value
+    #   if there is no match send some other AT command, probably to set a value
+    #
+    # States are 'init' 'checking', 'setting', 'done'
+    ok = re.compile("^OK")
+    busy = re.compile("\+CMS ERROR.*SIM busy")
+    not_ok = re.compile("^(ERROR|\+CM[SE] ERROR:)")
+    def __init__(self, check = None, ok = None, record = None, at = None,
+                 timeout=None, handle = None, repeat = None, arg = None,
+                 critical = True, noreply=None):
+        Task.__init__(self, repeat)
+        self.check = check
+        self.okstr = ok
+        if ok:
+            self.okre = re.compile(ok)
+        self.record = record
+        self.at = at
+        self.arg = arg
+        self.timeout_time = timeout
+        self.handle = handle
+        self.critical = critical
+        self.noreply = noreply
+
+    def start(self, channel):
+        channel.state['retries'] = 0
+        channel.state['stage'] = 'init'
+        self.advance(channel)
+
+    def takeline(self, channel, line):
+        if line == None:
+            channel.set_state('reset')
+            channel.advance()
+            return
+        m = self.ok.match(line)
+        if m:
+            channel.cancel_timeout()
+            if self.handle:
+                self.handle(channel, line, None)
+            return self.advance(channel)
+
+        if self.busy.match(line):
+            channel.cancel_timeout()
+            channel.set_timeout(5000)
+            return
+        if self.not_ok.match(line):
+            channel.cancel_timeout()
+            return self.timeout(channel)
+        
+        if channel.state['stage'] == 'checking':
+            m = self.okre.match(line)
+            if m:
+                channel.state['matched'] = True
+                if self.record:
+                    record(self.record[0], m.expand(self.record[1]))
+                if self.handle:
+                    self.handle(channel, line, m)
+                return
+                
+        if channel.state['stage'] == 'setting':
+            # didn't really expect anything here..
+            pass
+
+    def timeout(self, channel):
+        if channel.state['retries'] >= 5:
+            if self.critical:
+                channel.set_state('reset')
+            channel.advance()
+            return
+        channel.state['retries'] += 1
+        channel.state['stage'] = 'init'
+        channel.atcmd('')
+
+    def advance(self, channel):
+        st = channel.state['stage']
+        if st == 'init' and self.check:
+            channel.state['stage'] = 'checking'
+            if self.timeout_time:
+                channel.atcmd(self.check, timeout = self.timeout_time)
+            else:
+                channel.atcmd(self.check)
+        elif (st == 'init' or st == 'checking') and self.at and not 'matched' in channel.state:
+            channel.state['stage'] = 'setting'
+            at = self.at
+            if self.arg:
+                at = at % channel.args[self.arg]
+            if self.timeout_time:
+                channel.atcmd(at, timeout = self.timeout_time)
+            else:
+                channel.atcmd(at)
+            if self.noreply:
+                channel.cancel_timeout()
+                channel.advance()
+        else:
+            channel.advance()
+
+class PowerAction(Task):
+    # A PowerAction ensure that we have a connection to the modem
+    #  and sets the power on or off, or resets the modem
+    def __init__(self, cmd):
+        Task.__init__(self, None)
+        self.cmd = cmd
+
+    def start(self, channel):
+        if self.cmd == "on":
+            if not channel.connected:
+                channel.connect()
+            if not channel.altchan.connected:
+                channel.altchan.connect()
+            channel.check_flightmode()
+        elif self.cmd == "off":
+            record('carrier', '')
+            record('cell', '')
+            record('signal_strength','0/32')
+            channel.disconnect()
+            channel.altchan.disconnect()
+        elif self.cmd == 'reopen':
+            channel.disconnect()
+            channel.altchan.disconnect()
+            channel.connect()
+            channel.altchan.connect()
+        return channel.advance()
+
+def rel(handle):
+    handle.release()
+    log("did release")
+    return False
+
+class SuspendComplete(Task):
+    # This action simply allows suspend to continue
+    def __init__(self):
+        Task.__init__(self, None)
+
+    def start(self, channel):
+        if channel.suspend_pending:
+            channel.suspend_pending = False
+            log("queue release")
+            gobject.idle_add(rel, channel.suspend_handle)
+        return channel.advance()
+
+class ChangeStateAction(Task):
+    # This action changes to a new state, like a goto
+    def __init__(self, state):
+        Task.__init__(self, None)
+        self.newstate = state
+    def start(self, channel):
+        channel.set_state(self.newstate)
+        return channel.advance()
+
+class CheckSMS(Task):
+    def __init__(self):
+        Task.__init__(self, None)
+    def start(self, channel):
+        if channel.pending_sms:
+            channel.pending_sms = False
+            p = Popen('gsm-getsms -n', shell=True, close_fds = True)
+            ok = p.wait()
+        return channel.advance()
+
+class RouteVoice(Task):
+    def __init__(self, on):
+        Task.__init__(self, None)
+        self.request = on
+    def start(self, channel):
+        if self.request:
+            channel.sound_on = True
+            try:
+                f = open("/run/sound/00-voicecall","w")
+                f.close()
+            except:
+                pass
+            p = Popen('/usr/local/bin/gsm-voice-routing', close_fds = True)
+            log('Running gsm-voice-routing pid', p.pid)
+            channel.voice_route = p
+        elif channel.sound_on:
+            if channel.voice_route:
+                channel.voice_route.send_signal(15)
+                channel.voice_route.wait()
+                channel.voice_route = None
+            try:
+                os.unlink("/run/sound/00-voicecall")
+            except OSError:
+                pass
+            channel.sound_on = False
+        return channel.advance()
+
+class BlockSuspendAction(Task):
+    def __init__(self, enable):
+        Task.__init__(self, None)
+        self.enable = enable
+    def start(self, channel):
+        if self.enable:
+            channel.suspend_blocker.block()
+        channel.advance()
+
+        if not self.enable:
+            channel.suspend_blocker.unblock()
+
+
+class Async:
+    def __init__(self, msg, handle, handle_extra = None):
+        self.msg = msg
+        self.msgre = re.compile(msg)
+        self.handle = handle
+        self.handle_extra = handle_extra
+
+    def match(self, line):
+        return self.msgre.match(line)
+
+# async handlers...
+LAC=0
+CELLID=0
+cellnames={}
+def status_update(channel, line, m):
+    if m and m.groups()[3] != None:
+        global LAC, CELLID, cellnames
+        LAC = int(m.groups()[2],16)
+        CELLID = int(m.groups()[3],16)
+        record('cellid', "%04X %06X" % (LAC, CELLID));
+        if CELLID in cellnames:
+            record('cell', cellnames[CELLID])
+            log("That one is", cellnames[CELLID])
+
+def new_sms(channel, line, m):
+    if m:
+        channel.pending_sms = False
+        record('newsms', m.groups()[1])
+        p = Popen('gsm-getsms -n', shell=True, close_fds = True)
+        ok = p.wait()
+def maybe_sms(line, channel):
+    channel.pending_sms = True
+
+def sigstr(channel, line, m):
+    if m:
+        record('signal_strength', m.groups()[0] + '/32')
+
+global incoming_cell_id
+def cellid_update(channel, line, m):
+    # get something like +CBM: 1568,50,1,1,1
+    # don't know what that means, just collect the 'extra' line
+    # I think the '50' means 'this is a cell id'.  I should
+    # probably test for that.
+    #
+    # response can be multi-line
+    global incoming_cell_id
+    incoming_cell_id = ""
+
+def cellid_new(channel, line):
+    global CELLID, cellnames, incoming_cell_id
+    if not line:
+        # end of message
+        if incoming_cell_id:
+            l = re.sub('[^!-~]+',' ',incoming_cell_id)
+            if CELLID:
+                cellnames[CELLID] = l
+            record('cell', l)
+            return False
+    line = line.strip()
+    if incoming_cell_id:
+        incoming_cell_id += ' ' + line
+    else:
+        incoming_cell_id = line
+    return True
+
+incoming_num = None
+def incoming(channel, line, m):
+    global incoming_num
+    if incoming_num:
+        record('incoming', incoming_num)
+    else:
+        record('incoming', '-')
+    set_alert('ring', 'new')
+    if channel.gstate not in ['incoming', 'answer']:
+        calllog('incoming', '-call-')
+        channel.set_state('incoming')
+        record('status', 'INCOMING')
+        global cpas_zero_cnt
+        cpas_zero_cnt = 0
+
+def incoming_number(channel, line, m):
+    global incoming_num
+    if m:
+        num = m.groups()[0]
+        if incoming_num == None:
+            calllog('incoming', num);
+        incoming_num = num
+        record('incoming', incoming_num)
+
+def no_carrier(channel, line, m):
+    record('status', '')
+    record('call', '')
+    if channel.gstate != 'idle':
+        channel.set_state('idle')
+
+
+def busy(channel, line, m):
+    record('status', 'BUSY')
+    record('call', '')
+
+def ussd(channel, line, m):
+    pass
+
+cpas_zero_cnt = 0
+def call_status(channel, line, m):
+    global cpas_zero_cnt
+    global calling
+    log("call_status got", line)
+    if not m:
+        return
+    s = int(m.groups()[0])
+    log("s = %d" % s)
+    if s == 0:
+        if calling:
+            return
+        cpas_zero_cnt += 1
+        if cpas_zero_cnt <= 3:
+            return
+        # idle
+        global incoming_num
+        incoming_num = None
+        record('incoming', '')
+        if channel.gstate == 'incoming':
+            calllog('incoming','-end-')
+            record('status', '')
+        if channel.gstate != 'idle' and channel.gstate != 'suspend':
+            channel.set_state('idle')
+    cpas_zero_cnt = 0
+    calling = False
+    if s == 3:
+        # incoming call
+        if channel.gstate not in  ['incoming', 'answer']:
+            # strange ..
+            channel.set_state('incoming')
+            record('status', 'INCOMING')
+            set_alert('ring', 'new')
+            record('incoming', '-')
+    if s == 4:
+        # on a call
+        if channel.gstate != 'on-call' and channel.gstate != 'hangup':
+            channel.set_state('on-call')
+
+control = {}
+
+# For flight mode, we turn the power off.
+control['to_flight'] = [
+    AtAction(at='+CFUN=0'),
+    PowerAction('off'),
+    ChangeStateAction('flight'),
+    ]
+
+control['flight'] = [
+    SuspendComplete()
+    ]
+
+control['reset'] = [
+    # turning power off just kills everything!!!
+    PowerAction('reopen'),
+    #PowerAction('off'),
+    AtAction(at='E0', timeout=30000),
+    ChangeStateAction('init'),
+    ]
+
+# For suspend, we want power on, but no wakups for status or cellid
+control['suspend'] = [
+    AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status),
+    AtAction(check='+CFUN?', ok='\+CFUN: 1', at='+CFUN=1', timeout=10000),
+    CheckSMS(),
+    AtAction(at='+CNMI=1,1,0,0,0'),
+    AtAction(at='_OSQI=0'),
+    AtAction(at='_OEANT=0'),
+    AtAction(at='_OSSYS=0'),
+    AtAction(at='_OPONI=0'),
+    AtAction(at='+CREG=0'),
+    SuspendComplete()
+    ]
+
+control['listenerr'] = [
+    PowerAction('on'),
+    AtAction(at='V1E0'),
+    AtAction(at='+CMEE=2;+CRC=1')
+    ]
+control['init'] = [
+    BlockSuspendAction(True),
+    SuspendComplete(),
+    PowerAction('on'),
+    AtAction(at='V1E0'),
+    AtAction(at='+CMEE=2;+CRC=1'),
+    # Turn the device on.
+    AtAction(check='+CFUN?', ok='\+CFUN: 1', at='+CFUN=1', timeout=10000),
+    # Report carrier as long name
+    AtAction(at='+COPS=3,0'),
+    # register with a carrier
+    #AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS',
+    #         record=('carrier', '\\1'), timeout=10000),
+    AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS=0',
+             record=('carrier', '\\1'), timeout=10000, repeat=37000),
+    # text format for various messages such SMS
+    AtAction(check='+CMGF?', ok='\+CMGF: 0', at='+CMGF=0'),
+    # get location status updates
+    AtAction(at='+CREG=2'),
+    AtAction(check='+CREG?', ok='\+CREG: 2,(\d)(,"([^"]*)","([^"]*)")',
+             handle=status_update, timeout=4000),
+    # Enable collection of  Cell Info message
+    #AtAction(check='+CSCB?', ok='\+CSCB: 1,.*', at='+CSCB=1'),
+    #AtAction(at='+CSCB=0'),
+    AtAction(at='+CSCB=1', critical=False),
+    # Enable async reporting of TXT and Cell info messages
+    #AtAction(check='+CNMI?', ok='\+CNMI: 1,1,2,0,0', at='+CNMI=1,1,2,0,0'),
+    AtAction(at='+CNMI=1,0,0,0,0', critical=False),
+    AtAction(at='+CNMI=1,1,2,0,0', critical=False),
+    # Enable async reporting of signal strength
+    AtAction(at='_OSQI=1', critical=False),
+
+    # Enable reporting of Caller number id.
+    AtAction(check='+CLIP?', ok='\+CLIP: 1,[012]', at='+CLIP=1', timeout=10000,
+             critical = False),
+    AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status),
+    ChangeStateAction('idle')
+    ]
+
+control['idle'] = [
+    RouteVoice(False),
+    CheckSMS(),
+    BlockSuspendAction(False),
+    SuspendComplete(),
+    AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS=0',
+             record=('carrier', '\\1'), timeout=10000, repeat=37000),
+    # Make sure on GSM
+    AtAction(at='_OPSYS=3,2'),
+    # get signal string
+    AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)',
+             record=('signal_strength','\\1/32'), repeat=29000)
+    ]
+
+control['incoming'] = [
+    BlockSuspendAction(True),
+    SuspendComplete(),
+    AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status, repeat=500),
+    
+    # monitor signal strength
+    AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)',
+             record=('signal_strength','\\1/32'), repeat=30000)
+    ]
+
+control['answer'] = [
+    AtAction(at='A'),
+    RouteVoice(True),
+    ChangeStateAction('incoming')
+    ]
+
+control['call'] = [
+    AtAction(at='D%s;', arg='number'),
+    RouteVoice(True),
+    ChangeStateAction('on-call')
+    ]
+
+control['dtmf'] = [
+    AtAction(at='+VTS=%s', arg='dtmf', noreply=True),
+    ChangeStateAction('on-call')
+    ]
+
+control['hangup'] = [
+    AtAction(at='+CHUP'),
+    RouteVoice(False),
+    BlockSuspendAction(False),
+    ChangeStateAction('idle')
+    ]
+
+control['on-call'] = [
+    BlockSuspendAction(True),
+    SuspendComplete(),
+    AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status, repeat=2000),
+    
+    # get signal strength
+    AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)',
+             record=('signal_strength','\\1/32'), repeat=30000)
+    ]
+async = [
+    Async(msg='\+CREG: ([01])(,"([^"]*)","([^"]*)")?', handle=status_update),
+    Async(msg='\+CMTI: "([A-Z]+)",(\d+)', handle = new_sms),
+    Async(msg='\+CBM: \d+,\d+,\d+,\d+,\d+', handle=cellid_update,
+          handle_extra = cellid_new),
+    Async(msg='\+CRING: (.*)', handle = incoming),
+    Async(msg='RING', handle = incoming),
+    Async(msg='\+CLIP: "([^"]+)",[0-9,]*', handle = incoming_number),
+    Async(msg='NO CARRIER', handle = no_carrier),
+    Async(msg='BUSY', handle = busy),
+    Async(msg='\+CUSD: ([012])(,"(.*)"(,[0-9]+)?)?$', handle = ussd),
+    Async(msg='_OSIGQ: ([0-9]+),([0-9]*)$', handle = sigstr),
+
+    ]
+
+class GsmD(AtChannel):
+
+    # gsmd works like a state machine
+    # the high level states are: flight suspend idle incoming on-call
+    #   Note that the whole 'call-waiting' experience is not coverred here.
+    #     That needs to be handled by whoever answers calls and allows interaction
+    #     between user and phone system.
+    #
+    # Each state contains a list of tasks such as setting and
+    # checking config options and monitoring state (e.g. signal strength)
+    # Some tasks are single-shot and only need to complete each time the state is
+    # entered.  Others are repeating (such as status monitoring).
+    # We take the first task of the current list and execute it, or wait
+    # until one will be ready.
+    # Tasks themselves can be state machines, so we keep track of what 'stage'
+    # we are up to in the current task.
+    #
+    # The system is (naturally) event driven.  The main two events that we
+    # receive are:
+    # 'takeline' which presents one line of text from the GSM device, and
+    # 'timeout' which indicates that a timeout set when a command was sent has
+    # expired.
+    # Other events are:
+    #   'taskready'  when the time of the next pending task arrives.
+    #   'flight'     when the state of the 'flight mode' has changed
+    #   'suspend'    when a suspend has been requested.
+    #
+    # Each event does some event specific processing to modify the state,
+    # Then calls 'self.advance' to progress the state machine.
+    # When high level state changes are requested, any pending task is discarded.
+    #
+    # If a task detects an error (gsm device not responding properly) it might
+    # request a reset.  This involves sending a modem_reset command and then
+    # restarting the current state from the top.
+    # A task can also indicate:
+    #  The next stage to try
+    #  How long to wait before retrying (or None)
+    #
+
+    def __init__(self, path, altpath):
+        AtChannel.__init__(self, path = path)
+
+        self.extra = None
+        self.flightmode = True
+        self.state = None
+        self.args = {}
+        self.suspend_pending = False
+        self.pending_sms = False
+        self.sound_on = True
+        self.voice_route = None
+        self.tasknum = None
+        self.altpath = altpath
+        self.altchan = CarrierDetect(altpath, self)
+        self.gstate = None
+        self.nextstate = None
+        self.statechanged = False
+
+        record('carrier','')
+        record('cell','')
+        record('incoming','')
+        record('signal_strength','')
+        record('status', '')
+
+        # set the initial state
+        self.set_state('flight')
+
+       # Monitor other external events which affect us
+        d = dnotify.dir('/var/lib/misc/flightmode')
+        self.flightmode_watcher = d.watch('active', self.check_flightmode)
+        d = dnotify.dir('/run/gsm-state')
+        self.call_watcher = d.watch('call', self.check_call)
+        self.dtmf_watcher = d.watch('dtmf', self.check_dtmf)
+            
+        self.suspend_handle = suspend.monitor(self.do_suspend, self.do_resume)
+        self.suspend_blocker = suspend.blocker()
+
+
+        # Check the externally imposed state
+        self.check_flightmode(self.flightmode_watcher)
+
+        # and GO!
+        self.advance()
+
+    def check_call(self, f = None):
+        l = recall('call')
+        log("Check call got", l)
+        if l == "":
+            if self.nextstate not in ['hangup', 'idle']:
+                self.set_state('hangup')
+                record('status','')
+                record('incoming','')
+        elif l == 'answer':
+            if self.nextstate == 'incoming':
+                record('status', 'on-call')
+                record('incoming','')
+                set_alert('ring', None)
+                self.set_state('answer')
+        else:
+            if self.nextstate == 'idle':
+                global calling
+                calling = True
+                self.args['number'] = l
+                self.set_state('call')
+                record('status', 'on-call')
+
+    def check_dtmf(self, f = None):
+        l = recall('dtmf')
+        log("Check dtmf got", l)
+        if len(l):
+            self.args['dtmf'] = l
+            self.set_state('dtmf')
+            record('dtmf','')
+
+    def check_flightmode(self, f = None):
+        try:
+            fd = open("/var/lib/misc/flightmode/active")
+            l = fd.read(1)
+            fd.close()
+        except IOError:
+            l = ""
+        log("check flightmode got", len(l))
+        if len(l) == 0:
+            if self.flightmode:
+                self.flightmode = False
+                if self.suspend_handle.suspended:
+                    self.set_state('suspend')
+                else:
+                    self.set_state('init')
+        else:
+            if not self.flightmode:
+                self.flightmode = True
+                self.set_state('to_flight')
+
+    def do_suspend(self):
+        log("do suspend")
+        if self.nextstate == 'flight':
+            return True
+        self.suspend_pending = True
+        self.set_state('suspend')
+        return False
+
+    def do_resume(self):
+        log("do resume")
+        if self.nextstate == 'suspend':
+            self.set_state('init')
+    
+    def set_state(self, state):
+        # this happens asynchronously so we must be careful
+        # about changing things.  Just record the new state
+        # and abort any timeout
+        log("state should become", state)
+        self.nextstate = state
+        self.statechanged = True
+        self.abort_timeout()
+
+    def advance(self):
+        # 'advance' is called by a 'Task' when it has finished
+        # It may have called 'set_state' first either to report
+        # an error or to effect a regular state change
+        now = int(time.time()*1000)
+        if self.tasknum != None:
+            self.lastrun[self.tasknum] = now
+            self.tasknum = None
+        if self.statechanged:
+            # time to effect 'set_state' synchronously
+            self.statechanged = False
+            self.gstate = self.nextstate
+            log("state becomes", self.gstate)
+            n = len(control[self.gstate])
+            self.lastrun = n * [0]
+        (t, delay) = self.next_cmd()
+        log("advance %s chooses %d, %d" % (self.gstate, t, delay))
+        if delay:
+            log("Sleeping for %f seconds" % (delay/1000.0))
+            self.set_timeout(delay)
+        else:
+            self.tasknum = t
+            self.state = {}
+            control[self.gstate][t].start(self)
+        
+        
+    def takeline(self, line):
+
+        if self.extra:
+            # an async message is multi-line and we need to handle
+            # the extra line.
+            if not self.extra.handle_extra(self, line):
+                self.extra = None
+            return False
+
+        if line == None:
+            self.set_state('reset')
+            self.advance()
+        if not line:
+            return False
+
+        # Check for an async message
+        for m in async:
+            mt = m.match(line)
+            if mt:
+                m.handle(self, line, mt)
+                if m.handle_extra:
+                    self.extra = m
+                return False
+
+        # else pass it to the task
+        if self.tasknum != None:
+            control[self.gstate][self.tasknum].takeline(self, line)
+
+    def timedout(self):
+        if self.tasknum == None:
+            self.advance()
+        else:
+            control[self.gstate][self.tasknum].timeout(self)
+        
+    def next_cmd(self):
+        # Find a command to execute, or a delay
+        # return (cmd,time)
+        # cmd is an index into control[state],
+        # time is seconds until try something
+        mindelay = 60*60*1000
+        cs = control[self.gstate]
+        n = len(cs)
+        now = int(time.time()*1000)
+        for i in range(n):
+            if self.lastrun[i] == 0 or (cs[i].repeat and
+                                        self.lastrun[i] + cs[i].repeat <= now):
+                return (i, 0)
+            if cs[i].repeat:
+                delay = (self.lastrun[i] + cs[i].repeat) - now;
+                if delay < mindelay:
+                    mindelay = delay
+        return (0, mindelay)
+
+class CarrierDetect(AtChannel):
+    # on the hso modem in the GTA04, the 'NO CARRIER' signal
+    # arrives on the 'Modem' port, not on the 'Application' port.
+    # So we listen to the 'Modem' port, and report any
+    # 'NO CARRIER' we see - or indeed anything that we see.
+    def __init__(self, path, main):
+        AtChannel.__init__(self, path = path)
+        self.main = main
+
+    def takeline(self, line):
+        self.main.takeline(line)
+
+class SysfsWatcher:
+    # watch for changes on a sysfs file and report them
+    # We read the content, report that, wait for a change
+    # and report again
+    def __init__(self, path, action):
+        self.path = path
+        self.action = action
+        self.fd = open(path, "r")
+        self.watcher = gobject.io_add_watch(self.fd, gobject.IO_PRI, self.read)
+        self.read()
+
+    def read(self, *args):
+        self.fd.seek(0)
+        try:
+            r = self.fd.read(4096)
+        except IOerror:
+            return True
+        self.action(r)
+        return True
+
+try:
+    os.mkdir("/run/gsm-state")
+except:
+    pass
+
+calling = False
+a = GsmD('/dev/ttyHS_Application', '/dev/ttyHS_Modem')
+print "GsmD started"
+
+try:
+    f = open("/sys/class/gpio/gpio176/edge", "w")
+except IOError:
+    f = None
+if f:
+    f.write("rising")
+    f.close()
+    w = SysfsWatcher("/sys/class/gpio/gpio176/value",
+                     lambda l: maybe_sms(l, a))
+else:
+    import evdev
+    def check_evt(dc, mom, typ, code, val):
+        if typ == 1 and val == 1:
+            # keypress
+            maybe_sms("", a)
+    try:
+        f = evdev.EvDev("/dev/input/incoming", check_evt)
+    except:
+        f = None
+c = gobject.main_context_default()
+while True:
+    c.iteration()
diff --git a/gsm/gsmd.py b/gsm/gsmd.py
new file mode 100644 (file)
index 0000000..1d72506
--- /dev/null
@@ -0,0 +1,893 @@
+#!/usr/bin/env python
+
+#
+# Calls can be made by writing a number to
+#  /run/gsm-state/call
+# Status get set call 'Calling' and then 'BUSY' or ''
+# Call can be answered by writing 'answer' to 'call'
+# or can be cancelled by writing ''.
+# During a call, chars can be written to
+#  /run/gsm-state/dtmf
+# to send tones.
+
+## FIXME
+# e.g. receive AT response +CREG: 1,"08A7","6E48"
+#  show that SIM is now ready
+# cope with /var/lock/suspend not existing yet
+#  define 'reset'
+
+import re, time, gobject, os
+from atchan import AtChannel
+import dnotify, suspend
+from tracing import log
+from subprocess import Popen
+
+def record(key, value):
+    f = open('/run/gsm-state/.new.' + key, 'w')
+    f.write(value)
+    f.close()
+    os.rename('/run/gsm-state/.new.' + key,
+              '/run/gsm-state/' + key)
+
+def recall(key):
+    try:
+        fd = open("/run/gsm-state/" + key)
+        l = fd.read(1000)
+        fd.close()
+    except IOError:
+        l = ""
+    return l.strip()
+
+def set_alert(key, value):
+    path = '/run/alert/' + key
+    if value == None:
+        try:
+            os.unlink(path)
+        except OSError:
+            pass
+    else:
+        try:
+            f = open(path, 'w')
+            f.write(value)
+            f.close()
+        except IOError:
+            pass
+
+lastlog={}
+def calllog(key, msg):
+    f = open('/var/log/' + key, 'a')
+    now = time.strftime("%Y-%m-%d %H:%M:%S")
+    f.write(now + ' ' + msg + "\n")
+    f.close()
+    lastlog[key] = msg
+
+def calllog_end(key):
+    if key in lastlog:
+        calllog(key, '-end-')
+        del lastlog[key]
+
+class Task:
+    def __init__(self, repeat):
+        self.repeat = repeat
+        pass
+    def start(self, channel):
+        # take the first action for this task
+        pass
+    def takeline(self, channel, line):
+        # a line has arrived that is presumably for us
+        pass
+    def timeout(self, channel):
+        # we asked for a timeout and got it
+        pass
+
+class AtAction(Task):
+    # An AtAction involves:
+    #   optionally sending an AT command to check some value
+    #      matching the result against a string, possibly storing the value
+    #   if there is no match send some other AT command, probably to set a value
+    #
+    # States are 'init' 'checking', 'setting', 'done'
+    ok = re.compile("^OK")
+    busy = re.compile("\+CMS ERROR.*SIM busy")
+    not_ok = re.compile("^(ERROR|\+CM[SE] ERROR:)")
+    def __init__(self, check = None, ok = None, record = None, at = None,
+                 timeout=None, handle = None, repeat = None, arg = None,
+                 critical = True, noreply=None):
+        Task.__init__(self, repeat)
+        self.check = check
+        self.okstr = ok
+        if ok:
+            self.okre = re.compile(ok)
+        self.record = record
+        self.at = at
+        self.arg = arg
+        self.timeout_time = timeout
+        self.handle = handle
+        self.critical = critical
+        self.noreply = noreply
+
+    def start(self, channel):
+        channel.state['retries'] = 0
+        channel.state['stage'] = 'init'
+        self.advance(channel)
+
+    def takeline(self, channel, line):
+        if line == None:
+            channel.set_state('reset')
+            channel.advance()
+            return
+        m = self.ok.match(line)
+        if m:
+            channel.cancel_timeout()
+            if self.handle:
+                self.handle(channel, line, None)
+            return self.advance(channel)
+
+        if self.busy.match(line):
+            channel.cancel_timeout()
+            channel.set_timeout(5000)
+            return
+        if self.not_ok.match(line):
+            channel.cancel_timeout()
+            return self.timeout(channel)
+
+        if channel.state['stage'] == 'checking':
+            m = self.okre.match(line)
+            if m:
+                channel.state['matched'] = True
+                if self.record:
+                    record(self.record[0], m.expand(self.record[1]))
+                if self.handle:
+                    self.handle(channel, line, m)
+                return
+
+        if channel.state['stage'] == 'setting':
+            # didn't really expect anything here..
+            pass
+
+    def timeout(self, channel):
+        if channel.state['retries'] >= 5:
+            if self.critical:
+                channel.set_state('reset')
+            channel.advance()
+            return
+        channel.state['retries'] += 1
+        channel.state['stage'] = 'init'
+        channel.atcmd('')
+
+    def advance(self, channel):
+        st = channel.state['stage']
+        if st == 'init' and self.check:
+            channel.state['stage'] = 'checking'
+            if self.timeout_time:
+                channel.atcmd(self.check, timeout = self.timeout_time)
+            else:
+                channel.atcmd(self.check)
+        elif (st == 'init' or st == 'checking') and self.at and not 'matched' in channel.state:
+            channel.state['stage'] = 'setting'
+            at = self.at
+            if self.arg:
+                at = at % channel.args[self.arg]
+            if self.timeout_time:
+                channel.atcmd(at, timeout = self.timeout_time)
+            else:
+                channel.atcmd(at)
+            if self.noreply:
+                channel.cancel_timeout()
+                channel.advance()
+        else:
+            channel.advance()
+
+class PowerAction(Task):
+    # A PowerAction ensure that we have a connection to the modem
+    #  and sets the power on or off, or resets the modem
+    def __init__(self, cmd):
+        Task.__init__(self, None)
+        self.cmd = cmd
+
+    def start(self, channel):
+        if self.cmd == "on":
+            if not channel.connected:
+                channel.connect()
+            if not channel.altchan.connected:
+                channel.altchan.connect()
+            channel.check_flightmode()
+        elif self.cmd == "off":
+            record('carrier', '')
+            record('cell', '')
+            record('signal_strength','0/32')
+            channel.disconnect()
+            channel.altchan.disconnect()
+        elif self.cmd == 'reopen':
+            channel.disconnect()
+            channel.altchan.disconnect()
+            channel.connect()
+            channel.altchan.connect()
+        return channel.advance()
+
+class ChangeStateAction(Task):
+    # This action changes to a new state, like a goto
+    def __init__(self, state):
+        Task.__init__(self, None)
+        self.newstate = state
+    def start(self, channel):
+        if self.newstate:
+            state = self.newstate
+        elif channel.statechanged:
+            state = channel.nextstate
+            channel.statechanged = False
+        else:
+            state = None
+        if state:
+            channel.gstate = state
+            channel.tasknum = None
+            if not channel.statechanged:
+                channel.nextstate = state
+            log("ChangeStateAction chooses", channel.gstate)
+            n = len(control[channel.gstate])
+            channel.lastrun = n * [0]
+        return channel.advance()
+
+class CheckSMS(Task):
+    def __init__(self):
+        Task.__init__(self, None)
+    def start(self, channel):
+        if channel.pending_sms:
+            channel.pending_sms = False
+            p = Popen('gsm-getsms -n', shell=True, close_fds = True)
+            ok = p.wait()
+        return channel.advance()
+
+class RouteVoice(Task):
+    def __init__(self, on):
+        Task.__init__(self, None)
+        self.request = on
+    def start(self, channel):
+        if self.request:
+            channel.sound_on = True
+            try:
+                f = open("/run/sound/00-voicecall","w")
+                f.close()
+            except:
+                pass
+            p = Popen('/usr/local/bin/gsm-voice-routing', close_fds = True)
+            log('Running gsm-voice-routing pid', p.pid)
+            channel.voice_route = p
+        elif channel.sound_on:
+            if channel.voice_route:
+                channel.voice_route.send_signal(15)
+                channel.voice_route.wait()
+                channel.voice_route = None
+            try:
+                os.unlink("/run/sound/00-voicecall")
+            except OSError:
+                pass
+            channel.sound_on = False
+        return channel.advance()
+
+class BlockSuspendAction(Task):
+    def __init__(self, enable):
+        Task.__init__(self, None)
+        self.enable = enable
+    def start(self, channel):
+        print "BlockSuspendAction sets", self.enable
+        if self.enable:
+            channel.suspend_blocker.block()
+            # No point holding a pending suspend any more
+            if channel.suspend_pending:
+                channel.suspend_pending = False
+                print "BlockSuspendAction calls release"
+                suspend.abort_cycle()
+                channel.suspend_handle.release()
+        if not self.enable:
+            channel.suspend_blocker.unblock()
+
+        channel.advance()
+
+class Async:
+    def __init__(self, msg, handle, handle_extra = None):
+        self.msg = msg
+        self.msgre = re.compile(msg)
+        self.handle = handle
+        self.handle_extra = handle_extra
+
+    def match(self, line):
+        return self.msgre.match(line)
+
+# async handlers...
+LAC=0
+CELLID=0
+cellnames={}
+def status_update(channel, line, m):
+    if m and m.groups()[3] != None:
+        global LAC, CELLID, cellnames
+        LAC = int(m.groups()[2],16)
+        CELLID = int(m.groups()[3],16)
+        record('cellid', "%04X %06X" % (LAC, CELLID));
+        if CELLID in cellnames:
+            record('cell', cellnames[CELLID])
+            log("That one is", cellnames[CELLID])
+
+def new_sms(channel, line, m):
+    if m:
+        channel.pending_sms = False
+        record('newsms', m.groups()[1])
+        p = Popen('gsm-getsms -n', shell=True, close_fds = True)
+        ok = p.wait()
+
+def maybe_sms(line, channel):
+    channel.pending_sms = True
+
+def sigstr(channel, line, m):
+    if m:
+        record('signal_strength', m.groups()[0] + '/32')
+
+global incoming_cell_id
+def cellid_update(channel, line, m):
+    # get something like +CBM: 1568,50,1,1,1
+    # don't know what that means, just collect the 'extra' line
+    # I think the '50' means 'this is a cell id'.  I should
+    # probably test for that.
+    #
+    # response can be multi-line
+    global incoming_cell_id
+    incoming_cell_id = ""
+
+def cellid_new(channel, line):
+    global CELLID, cellnames, incoming_cell_id
+    if not line:
+        # end of message
+        if incoming_cell_id:
+            l = re.sub('[^!-~]+',' ',incoming_cell_id)
+            if CELLID:
+                cellnames[CELLID] = l
+            record('cell', l)
+            return False
+    line = line.strip()
+    if incoming_cell_id:
+        incoming_cell_id += ' ' + line
+    else:
+        incoming_cell_id = line
+    return True
+
+incoming_num = None
+def incoming(channel, line, m):
+    global incoming_num
+    if incoming_num:
+        record('incoming', incoming_num)
+    else:
+        record('incoming', '-')
+    set_alert('ring', 'new')
+    if channel.gstate not in ['on-call', 'incoming', 'answer']:
+        calllog('incoming', '-call-')
+        channel.set_state('incoming')
+        record('status', 'INCOMING')
+        global cpas_zero_cnt
+        cpas_zero_cnt = 0
+
+def incoming_number(channel, line, m):
+    global incoming_num
+    if m:
+        num = m.groups()[0]
+        if incoming_num == None:
+            calllog('incoming', num);
+        incoming_num = num
+        record('incoming', incoming_num)
+
+def no_carrier(channel, line, m):
+    record('status', '')
+    record('call', '')
+    if channel.gstate != 'idle':
+        channel.set_state('idle')
+
+def busy(channel, line, m):
+    record('status', 'BUSY')
+    record('call', '')
+
+def ussd(channel, line, m):
+    pass
+
+cpas_zero_cnt = 0
+def call_status(channel, line, m):
+    global cpas_zero_cnt
+    global calling
+    log("call_status got", line)
+    if not m:
+        return
+    s = int(m.groups()[0])
+    log("s = %d" % s)
+    if s == 0:
+        if calling:
+            return
+        cpas_zero_cnt += 1
+        if cpas_zero_cnt <= 3:
+            return
+        # idle
+        global incoming_num
+        incoming_num = None
+        record('incoming', '')
+        if channel.gstate in ['on-call','incoming','call']:
+            calllog_end('incoming')
+            calllog_end('outgoing')
+            record('status', '')
+        if channel.gstate != 'idle' and channel.gstate != 'suspend':
+            channel.set_state('idle')
+    cpas_zero_cnt = 0
+    calling = False
+    if s == 3:
+        # incoming call
+        if channel.gstate not in  ['incoming', 'answer']:
+            # strange ..
+            channel.set_state('incoming')
+            record('status', 'INCOMING')
+            set_alert('ring', 'new')
+            record('incoming', '-')
+    if s == 4:
+        # on a call - but could be just a data call, so don't do anything
+        #if channel.gstate != 'on-call' and channel.gstate != 'hangup':
+        #    channel.set_state('on-call')
+        pass
+
+control = {}
+
+# For flight mode, we turn the power off.
+control['flight'] = [
+    AtAction(at='+CFUN=0'),
+    PowerAction('off'),
+    BlockSuspendAction(False),
+    ]
+
+control['reset'] = [
+    # turning power off just kills everything!!!
+    PowerAction('reopen'),
+    #PowerAction('off'),
+    AtAction(at='E0', timeout=30000),
+    ChangeStateAction('init'),
+    ]
+
+# For suspend, we want power on, but no wakups for status or cellid
+control['suspend'] = [
+    AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status),
+    AtAction(check='+CFUN?', ok='\+CFUN: 1', at='+CFUN=1', timeout=10000),
+    CheckSMS(),
+    ChangeStateAction(None), # allow async state change
+    AtAction(at='+CNMI=1,1,0,0,0'),
+    AtAction(at='_OSQI=0'),
+    AtAction(at='_OEANT=0'),
+    AtAction(at='_OSSYS=0'),
+    AtAction(at='_OPONI=0'),
+    AtAction(at='+CREG=0'),
+    ]
+control['resume'] = [
+    BlockSuspendAction(True),
+    AtAction(at='+CNMI=1,1,2,0,0', critical=False),
+    AtAction(at='_OSQI=1', critical=False),
+    AtAction(at='+CREG=2'),
+    CheckSMS(),
+    ChangeStateAction(None),
+    ChangeStateAction('idle'),
+    ]
+
+control['listenerr'] = [
+    PowerAction('on'),
+    AtAction(at='V1E0'),
+    AtAction(at='+CMEE=2;+CRC=1')
+    ]
+control['init'] = [
+    BlockSuspendAction(True),
+    PowerAction('on'),
+    AtAction(at='V1E0'),
+    AtAction(at='+CMEE=2;+CRC=1'),
+    # Turn the device on.
+    AtAction(check='+CFUN?', ok='\+CFUN: 1', at='+CFUN=1', timeout=10000),
+    # Report carrier as long name
+    AtAction(at='+COPS=3,0'),
+    # register with a carrier
+    #AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS',
+    #         record=('carrier', '\\1'), timeout=10000),
+    AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS=0',
+             record=('carrier', '\\1'), timeout=10000),
+    # text format for various messages such SMS
+    AtAction(check='+CMGF?', ok='\+CMGF: 0', at='+CMGF=0'),
+    # get location status updates
+    AtAction(at='+CREG=2'),
+    AtAction(check='+CREG?', ok='\+CREG: 2,(\d)(,"([^"]*)","([^"]*)")',
+             handle=status_update, timeout=4000),
+    # Enable collection of  Cell Info message
+    #AtAction(check='+CSCB?', ok='\+CSCB: 1,.*', at='+CSCB=1'),
+    #AtAction(at='+CSCB=0'),
+    AtAction(at='+CSCB=1', critical=False),
+    # Enable async reporting of TXT and Cell info messages
+    #AtAction(check='+CNMI?', ok='\+CNMI: 1,1,2,0,0', at='+CNMI=1,1,2,0,0'),
+    AtAction(at='+CNMI=1,0,0,0,0', critical=False),
+    AtAction(at='+CNMI=1,1,2,0,0', critical=False),
+    # Enable async reporting of signal strength
+    AtAction(at='_OSQI=1', critical=False),
+
+    # Enable reporting of Caller number id.
+    AtAction(check='+CLIP?', ok='\+CLIP: 1,[012]', at='+CLIP=1', timeout=10000,
+             critical = False),
+    AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status),
+    ChangeStateAction('idle')
+    ]
+
+control['idle'] = [
+    RouteVoice(False),
+    CheckSMS(),
+    BlockSuspendAction(False),
+    AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS',
+             record=('carrier', '\\1'), timeout=10000),
+    #AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS=0',
+    #         record=('carrier', '\\1'), timeout=10000, repeat=37000),
+    # Make sure to use both 2G and 3G
+    AtAction(at='_OPSYS=3,2'),
+    # get signal string
+    AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)',
+             record=('signal_strength','\\1/32'), repeat=29000)
+    ]
+
+control['incoming'] = [
+    BlockSuspendAction(True),
+    AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status, repeat=500),
+
+    # monitor signal strength
+    AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)',
+             record=('signal_strength','\\1/32'), repeat=30000)
+    ]
+
+control['answer'] = [
+    AtAction(at='A'),
+    RouteVoice(True),
+    ChangeStateAction('on-call')
+    ]
+
+control['call'] = [
+    AtAction(at='D%s;', arg='number'),
+    RouteVoice(True),
+    ChangeStateAction('on-call')
+    ]
+
+control['dtmf'] = [
+    AtAction(at='+VTS=%s', arg='dtmf', noreply=True),
+    ChangeStateAction('on-call')
+    ]
+
+control['hangup'] = [
+    AtAction(at='+CHUP'),
+    ChangeStateAction('idle')
+    ]
+
+control['on-call'] = [
+    BlockSuspendAction(True),
+    AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status, repeat=2000),
+
+    # get signal strength
+    AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)',
+             record=('signal_strength','\\1/32'), repeat=30000)
+    ]
+
+async = [
+    Async(msg='\+CREG: ([01])(,"([^"]*)","([^"]*)")?', handle=status_update),
+    Async(msg='\+CMTI: "([A-Z]+)",(\d+)', handle = new_sms),
+    Async(msg='\+CBM: \d+,\d+,\d+,\d+,\d+', handle=cellid_update,
+          handle_extra = cellid_new),
+    Async(msg='\+CRING: (.*)', handle = incoming),
+    Async(msg='RING', handle = incoming),
+    Async(msg='\+CLIP: "([^"]+)",[0-9,]*', handle = incoming_number),
+    Async(msg='NO CARRIER', handle = no_carrier),
+    Async(msg='BUSY', handle = busy),
+    Async(msg='\+CUSD: ([012])(,"(.*)"(,[0-9]+)?)?$', handle = ussd),
+    Async(msg='_OSIGQ: ([0-9]+),([0-9]*)$', handle = sigstr),
+
+    ]
+
+class GsmD(AtChannel):
+
+    # gsmd works like a state machine
+    # the high level states are: flight suspend idle incoming on-call
+    #   Note that the whole 'call-waiting' experience is not coverred here.
+    #     That needs to be handled by whoever answers calls and allows interaction
+    #     between user and phone system.
+    #
+    # Each state contains a list of tasks such as setting and
+    # checking config options and monitoring state (e.g. signal strength)
+    # Some tasks are single-shot and only need to complete each time the state is
+    # entered.  Others are repeating (such as status monitoring).
+    # We take the first task of the current list and execute it, or wait
+    # until one will be ready.
+    # Tasks themselves can be state machines, so we keep track of what 'stage'
+    # we are up to in the current task.
+    #
+    # The system is (naturally) event driven.  The main two events that we
+    # receive are:
+    # 'takeline' which presents one line of text from the GSM device, and
+    # 'timeout' which indicates that a timeout set when a command was sent has
+    # expired.
+    # Other events are:
+    #   'taskready'  when the time of the next pending task arrives.
+    #   'flight'     when the state of the 'flight mode' has changed
+    #   'suspend'    when a suspend has been requested.
+    #
+    # Each event does some event specific processing to modify the state,
+    # Then calls 'self.advance' to progress the state machine.
+    # When high level state changes are requested, any pending task is discarded.
+    #
+    # If a task detects an error (gsm device not responding properly) it might
+    # request a reset.  This involves sending a modem_reset command and then
+    # restarting the current state from the top.
+    # A task can also indicate:
+    #  The next stage to try
+    #  How long to wait before retrying (or None)
+    #
+
+    def __init__(self, path, altpath):
+        AtChannel.__init__(self, path = path)
+
+        self.extra = None
+        self.flightmode = True
+        self.state = None
+        self.args = {}
+        self.suspend_pending = False
+        self.pending_sms = False
+        self.sound_on = True
+        self.voice_route = None
+        self.tasknum = None
+        self.altpath = altpath
+        self.altchan = CarrierDetect(altpath, self)
+        self.gstate = None
+        self.nextstate = None
+        self.statechanged = False
+
+        record('carrier','')
+        record('cell','')
+        record('incoming','')
+        record('signal_strength','')
+        record('status', '')
+
+        # set the initial state
+        self.set_state('flight')
+
+       # Monitor other external events which affect us
+        d = dnotify.dir('/var/lib/misc/flightmode')
+        self.flightmode_watcher = d.watch('active', self.check_flightmode)
+        d = dnotify.dir('/run/gsm-state')
+        self.call_watcher = d.watch('call', self.check_call)
+        self.dtmf_watcher = d.watch('dtmf', self.check_dtmf)
+
+        self.suspend_handle = suspend.monitor(self.do_suspend, self.do_resume)
+        self.suspend_blocker = suspend.blocker()
+
+        # Check the externally imposed state
+        self.check_flightmode(self.flightmode_watcher)
+
+        # and GO!
+        self.advance()
+
+    def check_call(self, f = None):
+        l = recall('call')
+        log("Check call got", l)
+        if l == "":
+            if self.nextstate not in ['hangup', 'idle']:
+                global incoming_num
+                incoming_num = None
+                self.set_state('hangup')
+                record('status','')
+                record('incoming','')
+                calllog_end('incoming')
+                calllog_end('outgoing')
+        elif l == 'answer':
+            if self.nextstate == 'incoming':
+                record('status', 'on-call')
+                record('incoming','')
+                set_alert('ring', None)
+                self.set_state('answer')
+        else:
+            if self.nextstate == 'idle':
+                global calling
+                calling = True
+                self.args['number'] = l
+                self.set_state('call')
+                calllog('outgoing',l)
+                record('status', 'on-call')
+
+    def check_dtmf(self, f = None):
+        l = recall('dtmf')
+        log("Check dtmf got", l)
+        if len(l):
+            self.args['dtmf'] = l
+            self.set_state('dtmf')
+            record('dtmf','')
+
+    def check_flightmode(self, f = None):
+        try:
+            fd = open("/var/lib/misc/flightmode/active")
+            l = fd.read(1)
+            fd.close()
+        except IOError:
+            l = ""
+        log("check flightmode got", len(l))
+        if len(l) == 0:
+            if self.flightmode:
+                self.flightmode = False
+                if self.suspend_handle.suspended:
+                    self.set_state('suspend')
+                else:
+                    self.set_state('init')
+        else:
+            if not self.flightmode:
+                self.flightmode = True
+                self.set_state('flight')
+
+    def do_suspend(self):
+        self.suspend_pending = True
+        if self.nextstate not in ['flight', 'resume']:
+            print "do suspend sets suspend"
+            self.set_state('suspend')
+        else:
+            print "do suspend avoids suspend"
+            self.abort_timeout()
+        return False
+
+    def do_resume(self):
+        if self.nextstate == 'suspend':
+            self.set_state('resume')
+
+    def set_state(self, state):
+        # this happens asynchronously so we must be careful
+        # about changing things.  Just record the new state
+        # and abort any timeout
+        log("state should become", state)
+        self.nextstate = state
+        self.statechanged = True
+        self.abort_timeout()
+
+    def advance(self):
+        # 'advance' is called by a 'Task' when it has finished
+        # It may have called 'set_state' first either to report
+        # an error or to effect a regular state change
+        now = int(time.time()*1000)
+        if self.tasknum != None:
+            self.lastrun[self.tasknum] = now
+            self.tasknum = None
+        (t, delay) = self.next_cmd()
+        log("advance %s chooses %d, %d" % (self.gstate, t, delay))
+        if delay and self.statechanged:
+            # time to effect 'set_state' synchronously
+            self.statechanged = False
+            self.gstate = self.nextstate
+            log("state becomes", self.gstate)
+            n = len(control[self.gstate])
+            self.lastrun = n * [0]
+            t, delay = self.next_cmd()
+
+        if delay and self.suspend_pending:
+            self.suspend_pending = False
+            print "advance calls release"
+            self.suspend_handle.release()
+
+        if delay:
+            log("Sleeping for %f seconds" % (delay/1000.0))
+            self.set_timeout(delay)
+        else:
+            self.tasknum = t
+            self.state = {}
+            control[self.gstate][t].start(self)
+
+    def takeline(self, line):
+
+        if self.extra:
+            # an async message is multi-line and we need to handle
+            # the extra line.
+            if not self.extra.handle_extra(self, line):
+                self.extra = None
+            return False
+
+        if line == None:
+            self.set_state('reset')
+            self.advance()
+        if not line:
+            return False
+
+        # Check for an async message
+        for m in async:
+            mt = m.match(line)
+            if mt:
+                m.handle(self, line, mt)
+                if m.handle_extra:
+                    self.extra = m
+                return False
+
+        # else pass it to the task
+        if self.tasknum != None:
+            control[self.gstate][self.tasknum].takeline(self, line)
+
+    def timedout(self):
+        if self.tasknum == None:
+            self.advance()
+        else:
+            control[self.gstate][self.tasknum].timeout(self)
+
+    def next_cmd(self):
+        # Find a command to execute, or a delay
+        # return (cmd,time)
+        # cmd is an index into control[state],
+        # time is seconds until try something
+        mindelay = 60*60*1000
+        if self.gstate == None:
+            return (0, mindelay)
+        cs = control[self.gstate]
+        n = len(cs)
+        now = int(time.time()*1000)
+        for i in range(n):
+            if self.lastrun[i] == 0 or (cs[i].repeat and
+                                        self.lastrun[i] + cs[i].repeat <= now):
+                return (i, 0)
+            if cs[i].repeat:
+                delay = (self.lastrun[i] + cs[i].repeat) - now;
+                if delay < mindelay:
+                    mindelay = delay
+        return (0, mindelay)
+
+class CarrierDetect(AtChannel):
+    # on the hso modem in the GTA04, the 'NO CARRIER' signal
+    # arrives on the 'Modem' port, not on the 'Application' port.
+    # So we listen to the 'Modem' port, and report any
+    # 'NO CARRIER' we see - or indeed anything that we see.
+    def __init__(self, path, main):
+        AtChannel.__init__(self, path = path)
+        self.main = main
+
+    def takeline(self, line):
+        self.main.takeline(line)
+
+class SysfsWatcher:
+    # watch for changes on a sysfs file and report them
+    # We read the content, report that, wait for a change
+    # and report again
+    def __init__(self, path, action):
+        self.path = path
+        self.action = action
+        self.fd = open(path, "r")
+        self.watcher = gobject.io_add_watch(self.fd, gobject.IO_PRI, self.read)
+        self.read()
+
+    def read(self, *args):
+        self.fd.seek(0)
+        try:
+            r = self.fd.read(4096)
+        except IOerror:
+            return True
+        self.action(r)
+        return True
+
+try:
+    os.mkdir("/run/gsm-state")
+except:
+    pass
+
+calling = False
+a = GsmD('/dev/ttyHS_Application', '/dev/ttyHS_Modem')
+print "GsmD started"
+
+try:
+    f = open("/sys/class/gpio/gpio176/edge", "w")
+except IOError:
+    f = None
+if f:
+    f.write("rising")
+    f.close()
+    w = SysfsWatcher("/sys/class/gpio/gpio176/value",
+                     lambda l: maybe_sms(l, a))
+else:
+    import evdev
+    def check_evt(dc, mom, typ, code, val):
+        if typ == 1 and val == 1:
+            # keypress
+            maybe_sms("", a)
+    try:
+        f = evdev.EvDev("/dev/input/incoming", check_evt)
+    except:
+        f = None
+c = gobject.main_context_default()
+while True:
+    c.iteration()
diff --git a/gsm/notes b/gsm/notes
new file mode 100644 (file)
index 0000000..dd77cea
--- /dev/null
+++ b/gsm/notes
@@ -0,0 +1,116 @@
+The state machine has become a mess and doesn't work.
+
+I regularly forget to call 'advance' and I'm not even sure where
+it should be called.
+
+Signals for file changes via dnotify come at arbitrary times and
+cause race problems.
+
+Some states need to progress fully before being changed and there
+is no mechanism to ensure that.
+
+So I need to make it all cleaner.  First I need to understand what we have.
+
+
+1/ timeouts
+
+
+2/ response from GSM module
+
+3/ async message from GSM module
+      record details
+      run gsm-getsms
+      set alerts
+
+4/ file changes
+    'flightmode'
+        check_flightmode -> suspend or init,  or to_flight
+    'call'
+        check_call ->
+              empty string might hang up
+              'answer' should answer if 'incoming'
+              'number' should go on-call
+        
+    'dtmf'
+        switch to 'dtmf' state which then goes back to on-call
+
+5/ suspend notification:
+    do_suspend :  state to suspend
+    do_resume  :  state to init
+
+states:
+
+ to_flight -> flight
+ reset
+ suspend
+ listenerr
+ init
+ idle
+ incoming
+ answer
+ call
+ dtmf
+ hangup
+ on-call
+
+Actions are:
+  AtAction   - this is only one with stages.
+  PowerAction
+  SuspendComplete - should  combine with BlockSuspendAction - DONE
+  ChangeStateAction
+  CheckSMS
+  RouteVoice
+  BlockSuspendAction
+
+
+We have a transition into a state, then being in a state.
+so to_flight and flight
+   call and on-call
+   answer and on-call
+   hangup and idle
+
+Sometimes a sequence of events must complete in order.  Sometimes they
+can be interrupted.  'suspend' performs a number of actions which
+might abort the suspend.  e.g. it checks call status.  Then it
+performs a number of actions that should complete.  So maybe have a
+'CheckState' action which is allowed to switch state if a request has
+been made.  This might be implicit at the end of every state, probably
+excepting states that have an explicit ChangeStateAction.
+
+The true states are:
+ idle
+ suspend
+ flight
+ incoming   - blocks suspend
+ on-call    - blocks suspend
+
+Transitions are:
+  init : ends in idle
+  to_flight
+  reset
+  answer: incoming -> on-call
+  hangup: incoming or on-call -> idle
+  dtmf: on-call -> on-call
+  call: idle -> on-call
+
+
+Async events check if there is an active task (channel.tasknum).
+If there is not, the  timer is reset for 'now'.
+This is done in an 'idle' handler to ensure single-threaded.
+So if tasknum == None, channel.cancel_timeout(), timer_fired()
+
+if 'advance' finds there is nothing to do but wait, it will allow
+suspend to complete if pending.
+
+-----------
+
+When we come out of suspend we call init and that somehow does a hangup.
+We need to have some wake-from-suspend path that is less intense.
+ - DONE
+
+2 probs:
+- set_state will interrupt a timeout which can interfere with AtAction
+   sub states.  That is bad.
+- resume is immediately replaced by 'suspend' which hangs around longer
+  than we want... I guess the cpas and cfun and checksms should protect
+  against staying in suspend... but how?
\ No newline at end of file
diff --git a/icons/tapinput-dextr.png b/icons/tapinput-dextr.png
new file mode 100644 (file)
index 0000000..3a8ff87
Binary files /dev/null and b/icons/tapinput-dextr.png differ
diff --git a/lib/tap3 b/lib/tap3
new file mode 100644 (file)
index 0000000..0385d8a
--- /dev/null
+++ b/lib/tap3
@@ -0,0 +1,31 @@
+
+Another possible tapboard:
+Based on Dextr
+
+1    '  A  B  C  D  ,   BS
+S    ?  E  F  G  H  .   DEL
+A    Z  I  J  K  L  M   RET
+SP   N  O  P  Q  R  S   SP
+SP   T  U  V  W  X  Y   SP
+
+
+-
+
++ - * { } < > / # [ ] = 
+@ $ % ^ & * ( ) ~ ` _ \ | 
+tap esc del home
+
+ !  @  $  %  ^  &  `
+ (  1  2  3  +  ~  )
+ [  4  5  6  -  _  ]
+ {  7  8  9  /  \  }
+ <  #  0  *  =  |  >
+
+Default is 'lower'
+drag left/right gives caps
+drag left/right then up/down does 'extra'
+'1' switches to 'number' mode until pressed again
+'A' switches to 'caps' and back
+
+
+Need esc, tab, and other function keyss
index 0583458e35c703af0b4490b2e499cb7a485c6dfd..530caa3c6de95fd214b37d52a1cc440530f20285 100644 (file)
@@ -91,7 +91,7 @@ class TapBoard(gtk.VBox):
         'hideme' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                   ())
         }
-    def __init__(self):
+    def __init__(self, mode='lower'):
         gtk.rc_parse_string("""
        style "tap-button-style" {
              GtkWidget::focus-padding = 0
@@ -163,7 +163,7 @@ class TapBoard(gtk.VBox):
         #   False with '-shift' for a single shift
         #   True with '-shift' for a locked shit
         self.image_mode = ''
-        self.mode = 'lower'
+        self.mode = mode
         self.shift = ''
         self.locked = None
         self.size = 0
diff --git a/lib/tapboard_dextr.py b/lib/tapboard_dextr.py
new file mode 100644 (file)
index 0000000..30694e6
--- /dev/null
@@ -0,0 +1,438 @@
+#!/usr/bin/env python
+
+# Copyright (C) 2011-2012 Neil Brown <neilb@suse.de>
+#
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License along
+#    with this program; if not, write to the Free Software Foundation, Inc.,
+#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+#
+# a library to draw a widget for tap-input of text
+#
+# Have a 4x10 array of buttons.  Some buttons enter a symbol,
+# others switch to a different array of buttons.
+# The 4 rows aren't the same, but vary like a qwerty keyboard.
+# Row1:  10 buttons
+# Row2:   9 buttons offset by half a button
+# Row3:  10 buttons just like Row1
+# Row4:   5 buttons each double-width
+#
+# vertial press/drag is passed to caller as movement.
+# press/hold is passed to caller as 'unmap'.
+# horizontal press/drag modifies the selected button
+#  on the first 3 rows, it shifts
+#  on NUM it goes straight to punctuation
+#
+# Different configs are:
+# lower-case alpha, with minimal punctuation
+# upper-case alpha, with different punctuation
+# numeric with phone and calculator punctuation
+# Remaining punctuation with some cursor control.
+#
+# Bottom row is normally:
+#   Shift NUM  SPC ENTER BackSpace
+# When 'shift' is pressed, the keyboard flips between
+#   upper/lower or numeric/punc
+# and bottom row maybe should become:
+#   lock control alt ... something.
+
+import gtk, pango, gobject
+
+keymap = {}
+
+keymap['lower'] = [
+    ['\'','a','b','c','d',','],
+    ['?', 'e','f','g','h','.'],
+    ['z', 'i','j','k','l','m'],
+    ['n', 'o','p','q','r','s'],
+    ['t', 'u','v','w','x','y']
+]
+keymap['caps'] = [
+    ['"', 'A','B','C','D',';'],
+    ['!', 'E','F','G','H',':'],
+    ['Z', 'I','J','K','L','M'],
+    ['N', 'O','P','Q','R','S'],
+    ['T', 'U','V','W','X','Y']
+]
+keymap['lower-shift'] = keymap['caps']
+keymap['lower-xtra'] = [
+    [' ',' ',' ',' ',' ',' '],
+    [' ','1','2','3','+',' '],
+    [' ','4','5','6','-',' '],
+    [' ','7','8','9',' ',' '],
+    [' ','*','0','#',' ',' ']
+]
+keymap['caps-xtra'] = keymap['lower-xtra']
+keymap['number'] = [
+    ['!','@','$','%','^','&','`'],
+    ['(','1','2','3','+','~',')'],
+    ['[','4','5','6','-','_',']'],
+    ['{','7','8','9','/','\\','}'],
+    ['<','#','0','*','=','|','>'],
+]
+    
+class TapBoard(gtk.VBox):
+    __gsignals__ = {
+        'key' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                 (gobject.TYPE_STRING,)),
+        'move': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                 (gobject.TYPE_INT, gobject.TYPE_INT)),
+        'hideme' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                  ())
+        }
+    def __init__(self):
+        gtk.rc_parse_string("""
+       style "tap-button-style" {
+             GtkWidget::focus-padding = 0
+             GtkWidget::focus-line-width = 1
+             xthickness = 1
+             ythickness = 0
+         }
+         widget "*.tap-button" style "tap-button-style"
+         """)
+
+        gtk.VBox.__init__(self)
+        self.keysize = 44
+        self.aspect = 1.4
+        self.width = int(10*self.keysize)
+        self.height = int(4*self.aspect*self.keysize)
+
+        self.dragx = None
+        self.dragy = None
+        self.moved = False
+        self.xmoved = False
+        self.xmin = 100000
+        self.xmax = 0
+
+        self.isize = gtk.icon_size_register("mine", 40, 40)
+
+        self.button_timeout = None
+
+        self.buttons = []
+
+        self.set_homogeneous(True)
+
+        for row in range(5):
+            h = gtk.HBox()
+            h.show()
+            self.add(h)
+            bl = []
+            h.set_homogeneous(True)
+            for col in range(8):
+                b = self.add_button(None, self.tap, (row,col), h)
+                bl.append(b)
+            self.buttons.append(bl)
+
+        # mode can be 'lower', 'upper', 'func' (later) or 'number'
+        self.image_mode = ''
+        self.mode = 'lower'
+        self.size = 0
+        self.connect("size-allocate", self.update_buttons)
+        self.connect("realize", self.update_buttons)
+
+
+    def add_button(self, label, click, arg, box, font = None):
+        if not label:
+            b = gtk.Button()
+            b.set_name("tap-button")
+        elif label[0:4] == 'gtk-':
+            img = gtk.image_new_from_stock(label, self.isize)
+            img.show()
+            b = gtk.Button()
+            b.add(img)
+        else:
+            b = gtk.Button(label)
+        b.show()
+        b.set_property('can-focus', False)
+        if font:
+            b.child.modify_font(font)
+        b.connect('button_press_event', self.press, arg)
+        b.connect('button_release_event', self.release, click, arg)
+        b.connect('motion_notify_event', self.motion)
+        b.add_events(gtk.gdk.POINTER_MOTION_MASK|
+                     gtk.gdk.POINTER_MOTION_HINT_MASK)
+
+        box.add(b)
+        return b
+
+    def update_buttons(self, *a):
+        if self.window == None:
+            return
+
+        alloc = self.buttons[0][0].get_allocation()
+        w = alloc.width; h = alloc.height
+        if w > h:
+            size = h
+        else:
+            size = w
+        size -= 6
+        if size <= 10 or size == self.size:
+            return
+        #print "update buttons", size
+        self.size = size
+
+        # For each button in 3x3 we need 10 images,
+        # one for initial state, and one for each of the new states
+        # So there are two fonts we want.
+        # First we make the initial images
+        fdsingle = pango.FontDescription('sans 10')
+        fdsingle.set_absolute_size(size / 1.1 * pango.SCALE)
+        fdword = pango.FontDescription('sans 10')
+        fdword.set_absolute_size(size / 2 * pango.SCALE)
+        fdxtra = pango.FontDescription('sans 10')
+        fdxtra.set_absolute_size(size/2.3 * pango.SCALE)
+
+        bg = self.get_style().bg_gc[gtk.STATE_NORMAL]
+        fg = self.get_style().fg_gc[gtk.STATE_NORMAL]
+        red = self.window.new_gc()
+        red.set_foreground(self.get_colormap().alloc_color(gtk.gdk.color_parse('red')))
+        blue = self.window.new_gc()
+        blue.set_foreground(self.get_colormap().alloc_color(gtk.gdk.color_parse('blue')))
+        base_images = {}
+        for mode in keymap.keys():
+            if mode[-5:] == 'xtra':
+                continue
+            base_images[mode] = 30*[None]
+            for row in range(5):
+                for col in range(6):
+                    if not self.buttons[row][col+1]:
+                        continue
+                    sym = keymap[mode][row][col]
+                    pm = gtk.gdk.Pixmap(self.window, size, size)
+                    pm.draw_rectangle(bg, True, 0, 0, size, size)
+                    if len(sym) == 1:
+                        self.modify_font(fdsingle)
+                    else:
+                        self.modify_font(fdword)
+                    layout = self.create_pango_layout(sym[0:3])
+                    (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+                    colour = fg
+                    if sym in "aeiouAEUIO0123456789":
+                        colour = blue
+                    pm.draw_layout(colour,
+                                   int(size/2 - ew/2),
+                                   int(size/2 - eh/2),
+                                   layout)
+                    if (mode+'-xtra') in keymap:
+                        self.modify_font(fdxtra)
+                        sym = keymap[mode+'-xtra'][row][col]
+                        layout = self.create_pango_layout(sym)
+                        (ink, (ex,ey,ew2,eh2)) = layout.get_pixel_extents()
+                        colour = fg
+                        if sym in "aeiouAEIOU0123456789":
+                            colour = blue
+                        pm.draw_layout(colour, int(size/2)+ew2,int(size/2)-eh2,layout)
+                    im = gtk.Image()
+                    im.set_from_pixmap(pm, None)
+                    base_images[mode][row*6+col] = im
+
+        base_images['common'] = 24*[None]
+        for sym in [ (0,0,"123"), (1,0,"ABC"), (2,0,"Func"),
+                     (0,7,"BkSp"), (1,7,"Ret"), (2,7,"Tab")]:
+            (r,c,s) = sym
+            pm = gtk.gdk.Pixmap(self.window, size, size)
+            pm.draw_rectangle(bg, True, 0, 0, size, size)
+            self.modify_font(fdword)
+            layout = self.create_pango_layout(s)
+            (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+            colour = red
+            pm.draw_layout(colour,
+                           int(size/2 - ew/2),
+                           int(size/2 - eh/2),
+                           layout)
+            im = gtk.Image()
+            im.set_from_pixmap(pm, None)
+            base_images['common'][r*8+c] = im
+
+        self.base_images = base_images
+            
+        fd = pango.FontDescription('sans 10')
+        fd.set_absolute_size(size / 1.5 * pango.SCALE)
+        self.modify_font(fd)
+        self.set_button_images()
+
+    def set_button_images(self):
+        mode = self.mode
+        if self.image_mode == mode:
+            return
+        for row in range(5):
+            if row < 3:
+                for col in [0,7]:
+                    b = self.buttons[row][col]
+                    b.set_image(self.base_images['common'][row*8+col])
+            for col in range(6):
+                b = self.buttons[row][col+1]
+                if not b:
+                    continue
+                im = self.base_images[mode][row*6+col]
+                if im:
+                    b.set_image(im)
+        self.image_mode = mode
+
+
+    def tap(self, rc, moved):
+        (row,col) = rc
+        m = self.mode
+        if col == 0 or col == 7:
+            # special buttons
+            if row >= 3:
+                sym = ' '
+            elif rc == (0,7):
+                sym = '\b'
+            elif rc == (1,7):
+                sym = '\n'
+            elif rc == (2,7):
+                sym = '\t'
+            elif rc == (0,0):
+                if m == 'number':
+                    self.mode = 'lower'
+                else:
+                    self.mode = 'number'
+                self.set_button_images()
+                return
+            elif rc == (1,0):
+                if m == 'caps':
+                    self.mode = 'lower'
+                else:
+                    self.mode = 'caps'
+                self.set_button_images()
+                return
+            else:
+                return
+        elif moved:
+            if moved == 2 and (self.mode + '-xtra') in keymap\
+                    and keymap[self.mode + '-xtra'][row][col-1] != ' ':
+                m = self.mode + '-xtra'
+            else:
+                m = self.mode + '-shift'
+            sym = keymap[m][row][col-1]
+        else:
+            sym = keymap[m][row][col-1]
+        self.emit('key', sym)
+
+    def press(self, widget, ev, arg):
+        self.dragx = int(ev.x_root)
+        self.dragy = int(ev.y_root)
+        self.moved = False
+        self.xmoved = False
+        self.xmin = self.dragx
+        self.xmax = self.dragx
+
+        # press-and-hold makes us disappear
+        if self.button_timeout:
+            gobject.source_remove(self.button_timeout)
+            self.button_timeout = None
+        self.button_timeout = gobject.timeout_add(500, self.disappear)
+
+    def release(self, widget, ev, click, arg):
+        dx = self.dragx
+        dy = self.dragy
+        y = int(ev.y_root)
+        self.dragx = None
+        self.dragy = None
+
+        if self.button_timeout:
+            gobject.source_remove(self.button_timeout)
+            self.button_timeout = None
+        if self.moved:
+            self.moved = False
+            # If we are half way off the screen, hide
+            root = gtk.gdk.get_default_root_window()
+            (rx,ry,rwidth,rheight,depth) = root.get_geometry()
+            (x,y,width,height,depth) = self.window.get_geometry()
+            xmid = int(x + width / 2)
+            ymid = int(y + height / 2)
+            if xmid < 0 or xmid > rwidth or \
+               ymid < 0 or ymid > rheight:
+                self.hide()
+        else:
+            if self.button_timeout:
+                gobject.source_remove(self.button_timeout)
+                self.button_timeout = None
+            if self.xmoved:
+                if self.xmin < dx and self.xmax > dx:
+                    click(arg, 2)
+                elif abs(y-dy) > 40:
+                    click(arg, 2)
+                else:
+                    click(arg, 1)
+            else:
+                click(arg, 0)
+            self.xmoved = False
+
+    def motion(self, widget, ev):
+        if self.dragx == None:
+            return
+        x = int(ev.x_root)
+        y = int(ev.y_root)
+
+        if (not self.xmoved and abs(y-self.dragy) > 40) or self.moved:
+            if not self.moved:
+                self.emit('move', 0, 0)
+            self.emit('move', x-self.dragx, y-self.dragy)
+            self.moved = True
+            if self.button_timeout:
+                gobject.source_remove(self.button_timeout)
+                self.button_timeout = None
+        if (not self.moved and abs(x-self.dragx) > 40) or self.xmoved:
+            self.xmoved = True
+            if x < self.xmin:
+                self.xmin = x
+            if x > self.xmax:
+                self.xmax = x
+            if self.button_timeout:
+                gobject.source_remove(self.button_timeout)
+                self.button_timeout = None
+        if ev.is_hint:
+            gtk.gdk.flush()
+            ev.window.get_pointer()
+
+    def disappear(self):
+        self.dragx = None
+        self.dragy = None
+        self.emit('hideme')
+
+    def do_buttons(self):
+        self.set_button_images()
+        self.button_timeout = None
+        return False
+
+    def set_buttons_soon(self):
+        if self.button_timeout:
+            gobject.source_remove(self.button_timeout)
+        self.button_timeout = gobject.timeout_add(500, self.do_buttons)
+
+
+if __name__ == "__main__" :
+    w = gtk.Window()
+    w.connect("destroy", lambda w: gtk.main_quit())
+    ti = TapBoard()
+    w.add(ti)
+    w.set_default_size(ti.width, ti.height)
+    root = gtk.gdk.get_default_root_window()
+    (x,y,width,height,depth) = root.get_geometry()
+    x = int((width-ti.width)/2)
+    y = int((height-ti.height)/2)
+    w.move(x,y)
+    def pkey(ti, str):
+        print 'key', str
+    ti.connect('key', pkey)
+    def hideme(ti):
+        print 'hidememe'
+        w.hide()
+    ti.connect('hideme', hideme)
+    ti.show()
+    w.show()
+
+    gtk.main()
+
diff --git a/lib/wmctrl.py b/lib/wmctrl.py
new file mode 100644 (file)
index 0000000..f0c8b0e
--- /dev/null
@@ -0,0 +1,163 @@
+
+#
+# manage a list of current windows and allow a selected
+# window to be raised.
+# I'm using Xlib for this, which doesn't have a built-in event
+# mechanism like gtk does in gobject.
+# So if you want to make sure property change notify events
+# get handled, you need to arrange that read events on
+# winlist.fd are passed to winlist.events.
+# e.g. gobject.io_add_watch(winlist.fd, gobject.IO_IN, winlist.events)
+#
+
+import Xlib.X
+import Xlib.display
+import Xlib.protocol.event
+
+class mywindow:
+    def __init__(self, win, name, pid, id, list):
+        self.id = id
+        self.win = win
+        self.name = name
+        self.pid = pid
+        self.list = list
+
+    def raise_win(self):
+        msg = Xlib.protocol.event.ClientMessage(window = self.win,
+                                                client_type = self.list.ACTIVE_WINDOW,
+                                                data = (32, [0,0,0,0,0])
+                                                )
+        msg.send_event = 1
+        mask = (Xlib.X.SubstructureRedirectMask | 
+                Xlib.X.SubstructureNotifyMask)
+        self.list.root.send_event(msg, event_mask = mask)
+        self.win.map()
+        self.win.raise_window()
+        #p = w.query_tree().parent
+        #if p:
+        #    p.map()
+        #    p.raise_window()
+        self.list.display.flush()
+
+    def close_win(self):
+        msg = Xlib.protocol.event.ClientMessage(window = self.win,
+                                                client_type = self.list.CLOSE_WINDOW,
+                                                data = (32, [0,0,0,0,0])
+                                                )
+        msg.send_event = 1
+        mask = (Xlib.X.SubstructureRedirectMask | 
+                Xlib.X.SubstructureNotifyMask)
+        self.list.root.send_event(msg, event_mask = mask)
+        self.list.display.flush()
+        
+class winlist:
+    def __init__(self, add_handle = None):
+        self.display = Xlib.display.Display()
+        self.root = self.display.screen().root
+        self.winfo = {}
+        self.windows = ()
+        self.WM_STRUT = self.display.intern_atom('_NET_WM_STRUT')
+        self.CARDINAL = self.display.intern_atom('CARDINAL')
+        self.ACTIVE_WINDOW = self.display.intern_atom('_NET_ACTIVE_WINDOW')
+        self.CLOSE_WINDOW = self.display.intern_atom('_NET_CLOSE_WINDOW')
+        self.NAME = self.display.intern_atom('WM_NAME')
+        self.STRING = self.display.intern_atom('STRING')
+        self.PID = self.display.intern_atom('_NET_WM_PID')
+        self.LIST = self.display.intern_atom('_NET_CLIENT_LIST_STACKING')
+        self.WINDOW = self.display.intern_atom('WINDOW')
+
+        self.fd = self.display.fileno()
+        self.change_handle = None
+        self.add_handle = add_handle
+        self.del_handle = None
+
+        self.root.change_attributes(event_mask = Xlib.X.PropertyChangeMask )
+        self.get_list()
+        
+
+    def add_win(self, id):
+        if id in self.winfo:
+            return self.winfo[id]
+        w = self.display.create_resource_object('window', id)
+        p = w.get_property(self.WM_STRUT, self.CARDINAL, 0, 100)
+        self.winfo[id] = None
+        if p:
+            return None
+        p = w.get_property(self.NAME, self.STRING, 0, 100)
+        if p and p.format == 8:
+            name = p.value
+            name = name.replace('&','&amp;')
+            name = name.replace('<','&lt;')
+            name = name.replace('>','&gt;')
+        else:
+            return None
+
+        p = w.get_property(self.PID, self.CARDINAL, 0, 100)
+        if p and p.format == 32:
+            pid = p.value[0]
+        else:
+            pid = 0
+
+        self.winfo[id] = mywindow(w, name, pid, id, self)
+
+        if self.add_handle:
+            self.add_handle(self.winfo[id])
+        return self.winfo[id]
+
+
+    def get_list(self):
+        l = self.root.get_property(self.LIST, self.WINDOW, 0, 100)
+        windows = []
+        for w in l.value:
+            if self.add_win(w):
+                windows.append(w)
+        self.windows = windows
+        self.clean_winfo()
+        if self.change_handle:
+            self.change_handle()
+
+    def clean_winfo(self):
+        togo = []
+        for w in self.winfo:
+            if w not in self.windows:
+                togo.append(w)
+        for w in togo:
+            del self.winfo[w]
+            if self.del_handle:
+                self.del_handle(w)
+
+    def events(self, *a):
+        i = self.display.pending_events()
+        while i > 0:
+            event = self.display.next_event()
+            self.handle_event(event)
+            i = i - 1
+        return True
+
+    def handle_event(self, event):
+        if event.atom != self.LIST:
+            return False
+        self.get_list()
+        return True
+
+    def top(self, num = 0):
+        if num > len(self.windows) or num < 0:
+            return None
+        return self.winfo[self.windows[-1-num]]
+
+    def on_change(self, func, add=None, delete=None):
+        self.change_handle = func
+        self.add_handle = add
+        self.del_handle = delete
+        
+
+if __name__ == '__main__':
+    w = winlist()
+    for i in w.winfo:
+        print i, w.winfo[i].name
+    while 1:
+        event = w.display.next_event()
+        if w.handle_event(event):
+            print "First is", w.top(1).name
+            w.top(1).raise_win()
+
diff --git a/netman/dnsmasq.conf b/netman/dnsmasq.conf
new file mode 100644 (file)
index 0000000..14d4b83
--- /dev/null
@@ -0,0 +1,3 @@
+dhcp-range=192.168.202.2,192.168.202.6,255.255.255.248,6h
+dhcp-range=192.168.202.10,192.168.202.14,255.255.255.248,6h
+dhcp-range=192.168.202.18,192.168.202.22,255.255.255.248,6h
diff --git a/netman/interfaces b/netman/interfaces
new file mode 100644 (file)
index 0000000..c74a992
--- /dev/null
@@ -0,0 +1,47 @@
+
+auto lo
+iface lo inet loopback
+
+auto usb0=usb0-p2p
+
+# usb0-client is used when no other network is available
+iface usb0-client inet static
+       pre-up rmmod g_ether || true
+       pre-up modprobe g_ether host_addr=56:88:91:5F:AF:81
+       address 192.168.202.1
+       netmask 255.255.255.248
+       network 192.168.202.0
+       gateway 192.168.202.2
+       post-down rmmod g_ether
+       up echo nameserver 192.168.1.3 >/etc/resolv.conf
+
+# usb0-p2p (peer-to-peer) is used when something else provides
+# the default route but we don't want the laptop to use us to get it.
+iface usb0-p2p inet static
+       pre-up rmmod g_ether || true
+       pre-up modprobe g_ether host_addr=56:88:91:5F:AF:81
+       address 192.168.202.1
+       netmask 255.255.255.248
+       network 192.168.202.0
+       post-down rmmod g_ether
+
+# usb0-hotspot is used when usb is used to tether to a hotspit.
+# Note it has a different host_addr so notebook knows to act differently
+# and use DHCP to get a route.
+iface usb0-hotspot inet static
+       pre-up rmmod g_ether || true
+       pre-up modprobe g_ether host_addr=56:88:91:5F:AF:82
+       address 192.168.202.1
+       netmask 255.255.255.248
+       network 192.168.202.0
+       post-down rmmod g_ether
+
+iface wlan0-hotspot inet static
+       address 192.168.202.9
+       netmask 255.255.255.248
+       network 192.168.202.8
+
+ifbase pan0-hostspot inet static
+       address 192.168.202.17
+       netmask 255.255.255.248
+       network 192.168.202.16
diff --git a/netman/netman.py b/netman/netman.py
new file mode 100644 (file)
index 0000000..6149cc0
--- /dev/null
@@ -0,0 +1,737 @@
+#!/usr/bin/env python
+
+#TODO
+# handle iptables masquerade directly so it can be tuned to the IP address
+# discovered.
+#      up iptables -t nat -A POSTROUTING -s 192.168.202.16/29 -j MASQUERADE
+#      down iptables -t nat -D POSTROUTING -s 192.168.202.16/29 -j MASQUERADE
+#  This should be done on any interface that is the 'hotspot'
+#DONE - rfkill unblock
+#DONE - kill children on exit
+#- add 3G support
+#  - connect
+#  - disconnect
+#  - poll for status
+#  - configure access point
+#- USB detect presence of connector?
+#- wifi:
+#  - report crypto status and strength
+#DONE  - extract 'id=' from CONNECTED to find current
+#DONE  - notice when wifi goes away
+#      CTRL-EVENT-DISCONNECTED
+#DONE  - if no dhcp response for a while, reassociate
+#  - config page to:
+#     list known and visible networks
+#     disble/enable, forget, set-password
+#     make sure to save config
+#     kill supplicant when done?
+#DONE  - ensure label is refreshed on different connect and disconnect stages.
+
+# Manage networks for the openmoko phoenux
+# There are 4 devices (unless I add VPN support)
+# USB, WIFI, WWAN, Bluetooth
+#
+# USB is normally on, though it can be turned off to
+# allow a different USB widget.
+# WIFI, WWAN, Bluetooth need to be explicitly enabled
+# for now at least
+# There is only one default route, and one DNS server
+# If WWAN is active, it provides route and DNS else
+# if WIFI is active and DHCP responds, it provides route and DNS
+# else if BT is active and DHCP responds, it provides route and DNS
+# else USB should provide route and DNS
+#
+# When we have a route, we provide DHCP to other interfaces
+# using dnsmasq, and provide masquarading to Internet.
+#
+# Main page shows each interface with status including
+#  IP address
+# One can be selected, and bottom buttons can be used to
+#   enable / disable, hotspot,  and configure
+# 'configure' goes to a new page, different for each interface.
+#
+# listen configures the interface to allow incoming...
+#
+# WWAN: configure allows APN to be set.  This is stored per SIM card.
+# WIFI: lists known and visible ESSIDs and show password which can be
+#    editted.  Also bottom buttons for 'activate' and 'forget' or 'allow'.
+# BT: lists visible peers which support networking and allows 'bind'
+#    or 'connect'
+# USB: ???
+#
+
+# WWAN in managed using atchan talking to the ttyHS_Control port
+#   We cannot hold it open, so poll status occasionally.
+#   ifconfig and route is managed directly.
+# WIFI is managed by talking to wpa_supplicant over a socket.
+#   ifconfig up/down might need to be done directly for power management
+#   udhcp is managed directly
+# BT ?? don't know yet
+# USB if managed with ifup/ifdown
+#
+# dnsmasq is stopped and restarted as needed with required options.
+#
+# Addresses:
+# This program doesn't know about specific IP addresses, that all
+# lives in config files.  However the plan is as follows:
+# Everything lives in a single class-C: 192.168.202.0/24
+# usb-server gets 6 addresses:  0/29   1 is me, 2-7 is you
+# usb-client gets same 6 addresses:  0/29   2 is you, 1 is me
+# wifi-hotspot gets 6 addresses: 8/29  9 is me, 10-14 are for clients
+# bt-server gets 6 addresses:  16/29   17 is me, 18-23 are for clients
+#
+# dnsmasq only responds to DHCP on 1, 9, 17
+#
+# only one of 3g, wifi, bt, usb-client can be active at at time
+# They set the server in resolv.conf, and set a default.
+# usb-hotspot, wifi-hotspot, bt-hotspot set up IP masqurading.
+# 
+#
+
+# so...
+# Each of 'usb', 'wifi', 'bt', can be in 'hotspot' mode at any time.
+# Of 'wifi', 'gsm', 'bt', 'usb', the first that is active and accessable
+# and not configured for 'hotspot' is configured as default route and others
+# that are not 'hotspot' are disabled - except 'usb' which becomes
+# 'p2p' for local traffic only.
+#
+# An interface can be in one of several states:
+#  - hotspot (not gsm) - active and providing access
+#  - disabled - explicitly disabled, not to be used
+#  - active - has found a connection, provides the default route
+#  - over-ridden - a higher-precedence interface is active
+#  - pending - no higher interface is active, but haven't found connection yet.
+#
+# When 'active' is released, we switch to 'disabled' and deconfig interface
+# When 'active' is pressed, we switch to 'pending' and rescan which might
+#    switch to 'over-ridden', or might start configuring, leading to 'active'
+# When 'hotspot' is pressed, we deconfig interface if up, then enable hotspot
+# When 'hotspot' is released, we deconfig and switch to 'pending'
+
+
+import gtk, pango
+from subprocess import Popen, PIPE
+import suspend
+
+class iface:
+    def __init__(self, parent):
+        self.state = 'disabled'
+        self.parent = parent
+        self.make_button()
+        self.can_hotspot = True
+        self.hotspot_net = None
+        self.hotspot = False
+        self.addr = 'No Address'
+        self.set_label()
+
+    def make_button(self):
+        self.button = gtk.ToggleButton(self.name)
+        self.button.set_alignment(0,0.5)
+        self.button.show()
+        self.button.connect('pressed', self.press)
+
+    def set_label(self):
+        self.addr = self.get_addr()
+        if self.state == 'disabled':
+            config = ''
+        else:
+            self.get_config()
+            config = ' (%s)' % self.config
+        if self.hotspot:
+            hs = 'hotspot '
+        else:
+            hs = ''
+        self.label = ('<span color="blue" size="30000">%s</span>\n<span size="12000">  %s%s\n%s</span>\n'
+                      % (self.name, hs, self.state, config))
+        if self.state == 'active' and not self.hotspot:
+            self.label = self.label + '<span size="12000">Gateway: %s</span>' % self.get_default()
+        self.button.child.set_markup(self.label)
+
+        if (self.hotspot and
+            self.addr != self.hotspot_net and
+            self.addr != 'No Address') :
+            if self.hotspot_net:
+                Popen(['iptables','-t','nat','-D','POSTROUTING','-s',
+                       self.hotspot_net,'-j','MASQUERADE']).wait()
+            self.hotspot_net = self.addr
+            Popen(['iptables','-t','nat','-A','POSTROUTING','-s',
+                   self.hotspot_net,'-j','MASQUERADE']).wait()
+        if not self.hotspot and self.hotspot_net != None:
+            Popen(['iptables','-t','nat','-D','POSTROUTING','-s',
+                   self.hotspot_net,'-j','MASQUERADE']).wait()
+            self.hotspot_net = None
+
+    def get_config(self):
+        self.config = '-'
+
+    def press(self, ev):
+        self.parent.select(self)
+
+    def get_addr(self):
+        p = Popen(['ip', 'add', 'show', 'dev', self.iface],
+                  stdout = PIPE, close_fds = True)
+        ip = 'No Address'
+        for l in p.stdout:
+            l = l.strip().split()
+            if l[0] == 'inet':
+                ip = l[1]
+        p.wait()
+        return ip
+
+    def get_default(self):
+        p = Popen(['ip', 'route', 'list', 'match', '128.0.0.1'],
+                  stdout = PIPE, close_fds = True)
+        route = 'none'
+        for l in p.stdout:
+            l = l.strip().split()
+            if l[0] == 'default' and l[1] == 'via':
+                route = l[2]
+        return route
+
+    def rfkill(self, state):
+        Popen(['rfkill', state, self.rfkill_name]).wait()
+
+    def shutdown(self):
+        pass
+
+    def config_widget(self):
+        return None
+
+from socket import *
+import os, gobject, time
+from listselect import ListSelect
+class WLAN_iface(iface):
+    # We run wpa_supplicant and udhcpc as needed, killing them when done
+    # For 'hotspot' we also run 'ifup' or 'ifdown'
+    # When wpa_supplicant is running, we connect to the socket to
+    # communicate and add new networks
+    # Some wpa_cli commands:
+    # ATTACH   -> results in 'OK'
+    # PING -> results in 'PONG'  (wpa_cli does this every 5 seconds)
+    # LIST_NETWORKS
+    # 0        LinuxRules      any     [CURRENT]
+    # 1        n-squared       any     
+    # 2        JesusIsHere     any     
+    # 3        TorchNet        any     
+    # 4        freedom any     
+    
+    #
+    # Some async responses:
+    # <3>CTRL-EVENT-BSS-ADDED 0 00:60:64:24:0a:22
+    # <3>CTRL-EVENT-BSS-ADDED 1 00:04:ed:1e:98:48
+    # <3>CTRL-EVENT-SCAN-RESULTS 
+    # <3>Trying to associate with 00:60:64:24:0a:22 (SSID='LinuxRules' freq=2427 MHz)
+    # <3>Associated with 00:60:64:24:0a:22
+    # <3>WPA: Key negotiation completed with 00:60:64:24:0a:22 [PTK=CCMP GTK=TKIP]
+    # <3>CTRL-EVENT-CONNECTED - Connection to 00:60:64:24:0a:22 completed (auth) [id=0 id_str=]
+    # <3>CTRL-EVENT-BSS-REMOVED 1 00:04:ed:1e:98:48
+    # <3>CTRL-EVENT-DISCONNECTED bssid=..... reason=0
+
+    def __init__(self, parent):
+        self.name = 'WLAN'
+        self.rfkill_name = 'wifi'
+        self.iface = 'wlan0'
+        self.ssid = 'unset'
+        self.supplicant = None
+        self.udhcpc = None
+        self.netlist = {}
+        self.scanlist = {}
+        self.configing = False
+        self.pending = None
+        self.to_send = []
+        iface.__init__(self, parent)
+        self.set_label()
+        self.cfg = None
+
+    def get_config(self):
+        self.config = self.addr + ' ESSID=%s' % self.ssid
+
+    def activate(self):
+        self.rfkill('unblock')
+        self.supplicant_up()
+
+    def shutdown(self):
+        self.udhcpc_close()
+        self.supplicant_close()
+        Popen('ifconfig wlan0 down', shell=True).wait()
+        self.rfkill('block')
+
+    def supplicant_up(self):
+        if self.supplicant:
+            return
+        self.supplicant = Popen(['wpa_supplicant','-i','wlan0',
+                                 '-c','/etc/wpa_supplicant.conf','-W'],
+                                close_fds = True)
+        try:
+            os.unlink('/run/wpa_ctrl_netman')
+        except:
+            pass
+        s = socket(AF_UNIX, SOCK_DGRAM)
+        s.bind('/run/wpa_ctrl_netman')
+        ok = False
+        while not ok:
+            try:
+                s.connect('/run/wpa_supplicant/wlan0')
+                ok = True
+            except:
+                time.sleep(0.1)
+        self.sock = s
+        self.watch = gobject.io_add_watch(s, gobject.IO_IN, self.recv)
+        self.request('ATTACH')
+
+    def restart_dhcp(self):
+        # run udhcpc -i wlan0 -f (in foreground)
+        # recognise message like
+        # Lease of 192.168.1.184 obtained, lease time 600
+
+        self.udhcpc_close()
+        env = os.environ.copy()
+        if self.hotspot:
+            env['NO_DEFAULT'] = 'yes'
+        self.udhcpc = Popen(['udhcpc','--interface=wlan0','--foreground',
+                             '--script=/etc/wifi-udhcpc.script'],
+                            close_fds = True,
+                            env = env,
+                            stdout = PIPE)
+        self.udhcpc_watch = gobject.io_add_watch(
+            self.udhcpc.stdout, gobject.IO_IN,
+            self.dhcp_read)
+
+    def dhcp_read(self, dir, foo):
+        if not self.udhcpc:
+            return False
+        m = self.udhcpc.stdout.readline()
+        if not m:
+            return False
+        l = m.strip().split()
+        print 'dhcp got', l
+        if len(l) >= 4 and l[0] == 'Lease' and l[3] == 'obtained,' and self.state != 'active':
+            self.state = 'active'
+            self.parent.update_active()
+            self.set_label()
+            if self.checker:
+                gobject.source_remove(self.checker)
+                self.checker = None
+        return True
+
+    def request(self, msg):
+        if self.pending:
+            self.to_send.append(msg)
+            return
+        self.pending = msg
+        self.sock.sendall(msg)
+        print 'sent request', msg
+
+    def recv(self, dir, foo):
+        try:
+            r = self.sock.recv(4096)
+        except error:
+            return False
+        if not r:
+            self.supplicant_close()
+            return False
+
+        if r[0] == '<':
+            self.async(r[r.find('>')+1:])
+            return True
+        elif self.pending == 'ATTACH':
+            # assume it is OK
+            self.request('LIST_NETWORKS')
+        elif self.pending == 'LIST_NETWORKS':
+            self.take_list(r)
+        elif self.pending == 'SCAN_RESULTS':
+            self.take_scan(r)
+
+        self.pending = None
+        if len(self.to_send) > 0:
+            self.request(self.to_send.pop(0))
+        return True
+
+    def udhcpc_close(self):
+        if self.udhcpc:
+            gobject.source_remove(self.udhcpc_watch)
+            self.udhcpc.terminate()
+            self.udhcpc.wait()
+            self.udhcpc = None
+            self.udhcpc_watch = None
+
+    def supplicant_close(self):
+        if self.supplicant:
+            self.supplicant.terminate()
+            gobject.source_remove(self.watch)
+            self.watch = None
+            self.supplicant.wait()
+            self.supplicant = None
+
+    def async(self, mesg):
+        print 'GOT ', mesg
+        print 'got (%s)' % mesg[:20]
+        if mesg[:20] == 'CTRL-EVENT-CONNECTED':
+            p = mesg.find('id=')
+            if p > 0:
+                id = mesg[p+3:].split()[0]
+                for ssid in self.netlist:
+                    if id == self.netlist[ssid]:
+                        self.ssid = ssid
+            self.set_label()
+            self.checker = gobject.timeout_add(30000, self.reassoc)
+            return self.restart_dhcp()
+        if mesg[:23] == 'CTRL-EVENT-DISCONNECTED':
+            if self.state == 'active':
+                self.state = 'pending'
+            self.udhcpc_close()
+            self.set_label()
+            self.parent.update_active()
+            return
+        if mesg[:23] == 'CTRL-EVENT-SCAN-RESULTS':
+            self.request('SCAN_RESULTS')
+            return
+        if mesg[:19] == 'Trying to associate':
+            l = mesg.split("'")
+            print 'len', len(l)
+            if len(l) > 1:
+                print 'ssid = ', l[1]
+                self.ssid = l[1]
+            return
+
+    def reassoc(self):
+        if self.state == 'active':
+            return False
+        self.request('REASSOCIATE')
+        self.checker = gobject.timeout_add(30000, self.reassoc)
+        return False
+
+    def take_list(self, mesg):
+        self.netlist = {}
+        for line in mesg.split('\n'):
+            if line != '' and line[0:7] != 'network':
+                l = line.split('\t')
+                self.netlist[l[1]] = l[0]
+        print 'netlist', self.netlist
+        self.fill_list()
+
+    def take_scan(self, mesg):
+        self.scanlist = {}
+        for line in mesg.split('\n'):
+            if line != '' and line[:5] != 'bssid':
+                l = line.split('\t')
+                self.scanlist[l[4]] = (l[2],l[3])
+        print 'scan', self.scanlist
+        self.fill_list()
+
+    def config_widget(self):
+        if self.cfg == None:
+            self.make_cfg()
+        self.fill_list()
+        self.configing = True
+        self.supplicant_up()
+        self.request('SCAN')
+        return self.cfg
+
+    def make_cfg(self):
+        # config widget is an entry, a ListSel, and a button row
+        cfg = gtk.VBox()
+        entry = gtk.Entry()
+        ls = ListSelect()
+        bb = gtk.HBox()
+
+        cfg.show()
+        cfg.pack_start(entry, expand = False)
+        entry.show()
+
+        cfg.pack_start(ls, expand = True)
+        ls.show()
+        ls.set_zoom(40)
+        cfg.pack_start(bb, expand = False)
+        bb.show()
+        bb.set_homogeneous(True)
+        bb.set_size_request(-1, 80)
+
+        entry.modify_font(self.parent.fd)
+        self.add_button(bb, 'Allow', self.allow)
+        self.add_button(bb, 'Forget', self.forget)
+        self.add_button(bb, 'Set Key', self.set_key)
+        self.add_button(bb, 'Done', self.done)
+
+        self.cfg = cfg
+        self.key_entry = entry
+        self.net_ls = ls
+
+    def add_button(self, bb, name, cmd):
+        btn = gtk.Button()
+        btn.set_label(name)
+        btn.child.modify_font(self.parent.fd)
+        btn.show()
+        bb.pack_start(btn, expand = True)
+        btn.connect('clicked', cmd)
+
+    def allow(self, x):
+        self.net_ls.reconfig(self.net_ls,1)
+        print 'alloc is', self.net_ls.get_allocation()
+        pass
+    def forget(self, x):
+        pass
+    def set_key(self, x):
+        pass
+    def done(self, x):
+        self.configing = False
+        self.parent.deconfig()
+
+    def fill_list(self):
+        if not self.configing:
+            return
+        l = [('none','green')]
+        for i in self.scanlist:
+            if i in self.netlist:
+                l.append((i, 'blue'))
+            else:
+                l.append((i, 'black'))
+        for i in self.netlist:
+            if i not in self.scanlist:
+                l.append((i, 'red'))
+        print 'filled list', l
+        self.net_ls.list = l
+        self.net_ls.list_changed()
+
+    def config_hotspot(self):
+        self.rfkill('unblock')
+        self.supplicant_up()
+            
+
+class WWAN_iface(iface):
+    def __init__(self, parent):
+        self.name = 'GSM/3G'
+        self.rfkill_name = 'wwan'
+        self.conf = None
+        self.iface = 'hso0'
+        self.APN = 'INTERNET'
+        iface.__init__(self, parent)
+        self.can_hotspot = False
+
+    def get_config(self):
+        self.config = self.addr + ' APN=%s' % self.APN
+
+    def activate(self):
+        self.rfkill('unblock')
+        Popen('/usr/local/bin/gsm-data up', shell=True).wait()
+        self.state = 'active'
+
+    def shutdown(self):
+        Popen('/usr/local/bin/gsm-data down', shell=True).wait()
+        self.rfkill('block')
+
+class BT_iface(iface):
+    def __init__(self, parent):
+        self.name = 'BT'
+        self.iface = 'pan0'
+        self.rfkill_name = 'bluetooth'
+        iface.__init__(self, parent)
+
+class USB_iface(iface):
+    def __init__(self, parent):
+        self.name = 'USB'
+        self.conf = 'p2p'
+        self.iface = 'usb0'
+        self.state = 'pending'
+        iface.__init__(self, parent)
+        self.set_label()
+
+    def get_config(self):
+        self.config = self.addr
+
+    def setconf(self, mode):
+        if self.conf == mode:
+            return
+        if mode:
+            cmd = 'ifdown --force usb0; ifup usb0=usb0-%s' % mode
+        else:
+            cmd = 'ifdown --force usb0'
+        print 'run command', cmd
+        Popen(cmd, shell=True).wait()
+        self.conf = mode
+
+    def shutdown(self):
+        # interpret this as 'switch to p2p mode', unless 'disabled'
+        if self.state == 'disabled':
+            self.setconf(None)
+        else:
+            self.setconf('p2p')
+
+    def activate(self):
+        # must set state to 'active' once it is - then maybe update_active
+        print 'activate usb'
+        self.setconf('client')
+        self.state = 'active'
+
+    def config_hotspot(self):
+        self.setconf('hotspot')
+        self.state = 'active'
+
+class Netman(gtk.Window):
+    # Main window has one radio button for each interface.
+    # Along bottom are 3 buttons: toggle 'enable', toggle 'hotspot', 'config'
+    # 
+    def __init__(self):
+        gtk.Window.__init__(self)
+        self.connect('destroy', self.close_application)
+        self.set_title('NetMan')
+
+        self.iface = [ WLAN_iface(self), WWAN_iface(self),
+                       BT_iface(self), USB_iface(self) ]
+
+        # 'v' fills the window, 'i' holds the interface, 'b' holds the buttons
+        v = gtk.VBox()
+        v.show()
+        self.main_window = v
+        self.add(v)
+        i = gtk.VBox()
+        i.show()
+        v.pack_start(i, expand = True)
+        i.set_homogeneous(True)
+
+        for iface in self.iface:
+            i.pack_start(iface.button, expand = True)
+
+        b = gtk.HBox()
+        b.show()
+        b.set_size_request(-1, 80)
+        b.set_homogeneous(True)
+        v.pack_end(b, expand = False)
+
+        ctx = self.get_pango_context()
+        fd = ctx.get_font_description()
+        fd.set_absolute_size(35*pango.SCALE)
+        self.fd = fd
+
+        self.blocker = suspend.blocker()
+
+        self.enable_btn = self.add_button(b, 'enabled', fd, True, self.enable)
+        self.hotspot_btn = self.add_button(b, 'hotspot', fd, True, self.hotspot)
+        self.add_button(b, 'config', fd, False, self.config)
+        self.selected = self.iface[0]
+        self.iface[0].button.set_active(True)
+
+        self.update_active()
+
+    def close_application(self, ev):
+        for i in self.iface:
+            i.shutdown()
+        gtk.main_quit()
+
+    def select(self, child):
+        for i in self.iface:
+            if i != child:
+                i.button.set_active(False)
+        self.selected = None
+        self.enable_btn.set_active(child.state != 'disabled')
+        self.hotspot_btn.set_inconsistent(not child.can_hotspot)
+        self.hotspot_btn.set_active(child.hotspot)
+        self.selected = child
+
+    def add_button(self, bb, name, fd, toggle, func):
+        if toggle:
+            btn = gtk.ToggleButton()
+        else:
+            btn = gtk.Button()
+        btn.set_label(name)
+        btn.child.modify_font(fd)
+        btn.show()
+        bb.pack_start(btn, expand = True)
+        btn.connect('clicked', func)
+        return btn
+
+    def enable(self, ev):
+        if not self.selected:
+            return
+        print 'active?', self.enable_btn.get_active()
+        if not self.enable_btn.get_active():
+            return self.disable()
+
+        if self.selected.state != 'disabled':
+            return
+        print 'enable %s, was %s' % (self.selected.name, self.selected.state)
+        self.selected.state = 'pending'
+        self.update_active()
+    def disable(self):
+        if self.selected.state == 'disabled':
+            return
+        self.selected.state = 'disabled'
+        self.selected.shutdown()
+        self.update_active()
+        self.selected.set_label()
+
+    def hotspot(self, ev):
+        if not self.selected:
+            return
+        if self.hotspot_btn.get_active():
+            # enable hotspot
+            s = self.selected
+            self.selected = None
+            self.enable_btn.set_active(True)
+            self.selected = s
+            s.hotspot = True
+            try:
+                f = open('/proc/sys/net/ipv4/ip_forward','w')
+                f.write('1')
+                f.close()
+            except:
+                pass
+            s.config_hotspot()
+            s.set_label()
+            self.blocker.block()
+        else:
+            # disable hotspot
+            self.selected.hotspot = False
+            self.selected.shutdown()
+            self.selected.set_label()
+            self.update_active()
+
+    def config(self, ev):
+        w = self.selected.config_widget()
+        if not w:
+            return
+        self.remove(self.main_window)
+        self.add(w)
+        w.show()
+
+    def deconfig(self):
+        self.remove(self.get_child())
+        self.add(self.main_window)
+
+    def update_active(self):
+        # try to activate the first available interface,
+        # and stop others
+        active_found = False
+        print 'update_active start'
+        hotspot = False
+        for i in self.iface:
+            print i.name, i.state
+            if i.hotspot:
+                hotspot = True
+            if i.state in ['disabled'] or i.hotspot:
+                continue
+            if active_found:
+                if i.state != 'over-ridden':
+                    i.shutdown()
+                    i.state = 'over-ridden'
+                    i.set_label()
+            elif i.state == 'active':
+                active_found = True
+            else:
+                if i.state == 'over-ridden':
+                    i.state = 'pending'
+                i.activate()
+                if i.state == 'active':
+                    active_found = True
+                i.set_label()
+        if not hotspot:
+            self.blocker.unblock()
+        print 'update_active end'
+
+if __name__ == '__main__':
+
+    n = Netman()
+    n.set_default_size(480, 640)
+    n.show()
+    gtk.main()
+
diff --git a/netman/sysctl.conf b/netman/sysctl.conf
new file mode 100644 (file)
index 0000000..c24ade9
--- /dev/null
@@ -0,0 +1,2 @@
+net.ipv4.ip_forward=1
+net.ipv6.conf.all.forwarding=1
diff --git a/netman/usbnet b/netman/usbnet
new file mode 100644 (file)
index 0000000..31ccd45
--- /dev/null
@@ -0,0 +1,8 @@
+#!/bin/sh
+ifconfig wlan0 down
+ifdown usb0
+if [ " $1" = " on" ]
+then
+  ifup usb0
+  ifconfig usb0
+fi
diff --git a/netman/wifi-udhcpc.script b/netman/wifi-udhcpc.script
new file mode 100755 (executable)
index 0000000..2c1a186
--- /dev/null
@@ -0,0 +1,72 @@
+#!/bin/sh
+# udhcpc script for netman - modified to recognised NO_DEFAULT env var
+# to mean that default route and DNS config should not be included.
+#
+# Busybox udhcpc dispatcher script. Copyright (C) 2009 by Axel Beckert.
+#
+# Based on the busybox example scripts and the old udhcp source
+# package default.* scripts.
+
+RESOLV_CONF="/etc/resolv.conf"
+
+echo udhcp script $*
+
+case $1 in
+    bound|renew)
+       [ -n "$broadcast" ] && BROADCAST="broadcast $broadcast"
+       [ -n "$subnet" ] && NETMASK="netmask $subnet"
+
+       /sbin/ifconfig $interface $ip $BROADCAST $NETMASK
+
+       if [ -z "$NO_DEFAULT" ]; then
+           if [ -n "$router" ]; then
+               echo "$0: Resetting default routes"
+               while /sbin/route del default gw 0.0.0.0 dev $interface; do :; done
+
+               metric=0
+               for i in $router; do
+                   /sbin/route add default gw $i dev $interface metric $metric
+                   metric=$(($metric + 1))
+               done
+           fi
+
+           # Update resolver configuration file
+           R=""
+           [ -n "$domain" ] && R="domain $domain
+"
+           for i in $dns; do
+               echo "$0: Adding DNS $i"
+               R="${R}nameserver $i
+"
+           done
+
+           if [ -x /sbin/resolvconf ]; then
+               echo -n "$R" | resolvconf -a "${interface}.udhcpc"
+           else
+               echo -n "$R" > "$RESOLV_CONF"
+           fi
+       fi
+       ;;
+
+    deconfig)
+       if [ -z "$NO_DEFAULT" ]; then
+           if [ -x /sbin/resolvconf ]; then
+               resolvconf -d "${interface}.udhcpc"
+           fi
+       fi
+       /sbin/ifconfig $interface 0.0.0.0
+       ;;
+
+    leasefail)
+       echo "$0: Lease failed: $message"
+       ;;
+
+    nak)
+       echo "$0: Received a NAK: $message"
+       ;;
+
+    *)
+       echo "$0: Unknown udhcpc command: $1";
+       exit 1;
+       ;;
+esac
diff --git a/netman/wifinet b/netman/wifinet
new file mode 100644 (file)
index 0000000..34b77d6
--- /dev/null
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+ifdown usb0
+ifconfig wlan0 down
+fuser -k /sbin/wpa_*
+rmmod libertas_sdio
+if [ " $1" = " up" ]
+then
+  modprobe libertas_sdio
+  rfkill unblock wifi
+  wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant.conf -W
+  wpa_cli -B -i wlan0 -a /root/wpa_updown
+  sleep 5
+  ifconfig wlan0
+  iwconfig wlan0
+fi
diff --git a/petrol/petrol.py b/petrol/petrol.py
new file mode 100644 (file)
index 0000000..d7d6d4d
--- /dev/null
@@ -0,0 +1,437 @@
+#!/usr/bin/env python
+
+#
+# Freerunner app to track petrol usage in new car.
+# We need to keep a log of entries.  Each entry:
+#
+#  date  kilometers litres whether-full  price-paid
+#
+# These are displayed with l/100K number and can get
+# overall l/100K and c/l between two points
+#
+# Must be able to edit old entries.
+#
+# So: 2 pages:
+#
+# 1/
+#   summary line: l/100K, c/l from selected to mark
+#   list of entries, scrollable and selectable
+#   buttons:  new, edit, mark/unmark
+#
+# 2/ Entry fields for a new entry
+#    date: default to 'today' with plus/minus button on either side
+#    kilometer - simple text entry
+#    litres  + 'fill' indicator
+#    c/l
+#    $
+#    keyboard in number mode
+#    Buttons:  Fill, not-fill, Discard, Save
+#
+# Should I be able to select between different vehicles?  Not now.
+
+import sys, os, time
+import pygtk, gtk, pango
+from listselect import ListSelect
+from tapboard import TapBoard
+
+
+class petrol_list:
+    def __init__(self, list):
+        self.list = list
+        self.mark = None
+    def set_list(self, list):
+        self.list = list
+    def set_mark(self, ind):
+        self.mark = ind
+
+    def __len__(self):
+        return len(self.list)
+    def __getitem__(self, ind):
+        i = self.list[ind]
+        dt = i[0]
+        tm = time.strptime(dt, '%Y-%m-%d')
+        dt = time.strftime('%d/%b/%Y', tm)
+        k = "%06d" % int(i[1])
+        l = "%06.2f" % float(i[2])
+        if len(i) > 3 and i[3] == "full":
+            f = "F"
+        else:
+            f = "-"
+        if len(i) > 4:
+            p = "$%06.2f" % float(i[4])
+        else:
+            p = "----.--"
+        str = "%s %s %s %s %s" % (dt, k, l, p, f)
+        if self.mark == ind:
+            type = 'mark'
+        else:
+            type = 'normal'
+        return (str, type)
+        
+
+
+class Petrol(gtk.Window):
+    def __init__(self, file):
+        gtk.Window.__init__(self)
+        self.connect("destroy",self.close_application)
+        self.set_title("Petrol")
+
+        ctx = self.get_pango_context()
+        fd = ctx.get_font_description()
+        fd.set_absolute_size(35*pango.SCALE)
+        vb = gtk.VBox(); self.add(vb); vb.show()
+        self.isize = gtk.icon_size_register("petrol", 40, 40)
+
+        self.listui = self.make_list_ui(fd)
+        self.editui = self.make_edit_ui(fd)
+        vb.add(self.listui); vb.add(self.editui)
+        self.listui.show()
+        self.editui.hide()
+
+        self.active_entry = None
+        self.colours={}
+
+        self.filename = file
+        self.load_file()
+        self.pl.set_list(self.list)
+
+    def close_application(self, ev):
+        gtk.main_quit()
+
+    def make_list_ui(self, fd):
+        ui = gtk.VBox()
+        l = gtk.Label("Petrol Usage")
+        l.modify_font(fd)
+        l.show(); ui.pack_start(l, expand=False)
+
+        h = gtk.HBox(); h.show(); ui.pack_start(h, expand=False)
+        h.set_homogeneous(True)
+        ui.usage_summary = gtk.Label("??.??? l/CKm")
+        ui.usage_summary.show()
+        ui.usage_summary.modify_font(fd)
+        h.add(ui.usage_summary)
+
+        ui.price_summary = gtk.Label("???.? c/l")
+        ui.price_summary.show()
+        ui.price_summary.modify_font(fd)
+        h.add(ui.price_summary)
+
+        ui.list = ListSelect()
+        ui.list.show()
+        ui.pack_start(ui.list, expand=True)
+        ui.list.set_format("normal","black", background="grey", selected="white")
+        ui.list.set_format("mark", "black", bullet=True,
+                           background="grey", selected="white")
+        ui.list.set_format("blank", "black", background="lightblue")
+        ui.list.connect('selected', self.selected)
+        self.pl = petrol_list([])
+        ui.list.list = self.pl
+        ui.list.set_zoom(34)
+
+        bbar = gtk.HBox(); bbar.show(); ui.pack_start(bbar, expand=False)
+        self.button(bbar, "New", fd, self.new)
+        self.button(bbar, "Edit", fd, self.edit)
+        self.button(bbar, "Mark", fd, self.mark)
+        return ui
+
+    def make_edit_ui(self, fd):
+        ui = gtk.VBox()
+
+        # title
+        l = gtk.Label("Petrol Event")
+        l.modify_font(fd)
+        l.show(); ui.pack_start(l, fill=False)
+
+        # date - with prev/next buttons.
+        h = gtk.HBox(); h.show(); ui.pack_start(h,fill=False)
+        self.button(h, gtk.STOCK_GO_BACK, fd, self.date_prev, expand=False)
+        l = gtk.Label("Today")
+        l.modify_font(fd)
+        l.show(); h.pack_start(l, expand=True)
+        self.button(h, gtk.STOCK_GO_FORWARD, fd, self.date_next, expand=False)
+        ui.date = l
+
+        # text entry for kilometers
+        h = gtk.HBox(); h.show(); ui.pack_start(h,fill=False)
+        e = self.entry(h, "Km:", fd, self.KM)
+        self.km_entry = e;
+
+        # text entry for  litres, with 'fill' indicator
+        h = gtk.HBox(); h.show(); ui.pack_start(h,fill=False)
+        e = self.entry(h, "litres:", fd, self.Litres)
+        self.l_entry = e;
+        l = gtk.Label("(full)")
+        l.modify_font(fd)
+        l.show(); h.pack_start(l, expand=False)
+        self.full_label = l
+
+        # text entry for cents/litre
+        h = gtk.HBox(); h.show(); ui.pack_start(h,fill=False)
+        e = self.entry(h, "cents/l:", fd, self.Cents)
+        self.cl_entry = e;
+
+        # text entry for price paid
+        h = gtk.HBox(); h.show(); ui.pack_start(h,fill=False)
+        e = self.entry(h, "Cost: $", fd, self.Cost)
+        self.cost_entry = e;
+
+        self.entry_priority = [self.l_entry, self.cl_entry]
+
+        # keyboard
+        t = TapBoard(); t.show()
+        ui.pack_start(t, fill=True)
+        t.connect('key', self.use_key)
+
+        # Buttons: fill/non-fill, Discard, Save
+        bbar = gtk.HBox(); bbar.show(); ui.pack_start(bbar, fill=False)
+        bbar.set_homogeneous(True)
+        self.fill_button = self.button(bbar, "non-Fill", fd, self.fill)
+        self.button(bbar, "Discard", fd, self.discard)
+        self.button(bbar, "Save", fd, self.save)
+        
+        return ui
+
+    def button(self, bar, label, fd, func, expand = True):
+        btn = gtk.Button()
+        if label[0:3] == "gtk" :
+            img = gtk.image_new_from_stock(label, self.isize)
+            img.show()
+            btn.add(img)
+        else:
+            btn.set_label(label)
+            btn.child.modify_font(fd)
+        btn.show()
+        bar.pack_start(btn, expand = expand)
+        btn.connect("clicked", func)
+        btn.set_focus_on_click(False)
+        return btn
+
+    def entry(self, bar, label, fd, func):
+        l = gtk.Label(label)
+        l.modify_font(fd)
+        l.show(); bar.pack_start(l, expand=False)
+        e = gtk.Entry(); e.show(); bar.pack_start(e, expand=True)
+        e.modify_font(fd)
+        e.set_events(e.get_events()|gtk.gdk.FOCUS_CHANGE_MASK)
+        e.connect('focus-in-event', self.focus)
+        e.connect('changed', func)
+        return e
+    def focus(self, ent, ev):
+        self.active_entry = ent
+        if (len(self.entry_priority) == 0 or
+            self.entry_priority[0] != ent):
+            # Make this entry the most recent
+            self.entry_priority = [ent] + self.entry_priority[:1]
+
+    def calc_other(self):
+        if len(self.entry_priority) != 2:
+            return
+        if self.l_entry not in self.entry_priority:
+            cl = self.check_entry(self.cl_entry)
+            cost = self.check_entry(self.cost_entry)
+            if cl != None and cost != None:
+                self.force_entry(self.l_entry, "%.2f" %(cost * 100.0 / cl))
+        if self.cl_entry not in self.entry_priority:
+            l = self.check_entry(self.l_entry)
+            cost = self.check_entry(self.cost_entry)
+            if l != None and l > 0 and cost != None:
+                self.force_entry(self.cl_entry, "%.2f" %(cost * 100.0 / l))
+        if self.cost_entry not in self.entry_priority:
+            cl = self.check_entry(self.cl_entry)
+            l = self.check_entry(self.l_entry)
+            if cl != None and l != None:
+                self.force_entry(self.cost_entry, "%.2f" %(cl * l / 100.0))
+
+    def force_entry(self, entry, val):
+        entry.set_text(val)
+        entry.modify_base(gtk.STATE_NORMAL, self.get_colour('yellow'))
+
+    def use_key(self, tb, str):
+        if not self.active_entry:
+            return
+        if str == '\b':
+            self.active_entry.emit('backspace')
+        elif str == '\n':
+            self.active_entry.emit('activate')
+        else:
+            self.active_entry.emit('insert-at-cursor', str)
+
+    def set_colour(self, name, col):
+        self.colours[name] = col
+    def get_colour(self, col):
+        # col is either a colour name, or a pre-set colour.
+        # so if it isn't in the list, add it
+        if col == None:
+            return None
+        if col not in self.colours:
+            self.set_colour(col, col)
+        if type(self.colours[col]) == str:
+            self.colours[col] = \
+                self.get_colormap().alloc_color(gtk.gdk.color_parse(self.colours[col]))
+        return self.colours[col]
+
+
+    def check_entry(self, entry):
+        txt = entry.get_text()
+        v = None
+        try:
+            if txt != "":
+                v = eval(txt)
+            colour = None
+        except:
+            colour = 'red'
+        entry.modify_base(gtk.STATE_NORMAL, self.get_colour(colour))
+        return v
+
+    def KM(self,entry):
+        v = self.check_entry(entry)
+    def Litres(self,entry):
+        self.calc_other()
+    def Cents(self,entry):
+        self.calc_other()
+    def Cost(self,entry):
+        self.calc_other()
+
+    def load_file(self):
+        # date:km:litre:full?:price
+        list = []
+        try:
+            f = open(self.filename)
+            l = f.readline()
+            while len(l) > 0:
+                l = l.strip()
+                w = l.split(':')
+                if len(w) >= 3:
+                    list.append(w)
+                l = f.readline()
+        except:
+            pass
+
+        list.sort(reverse=True)
+        self.list = list
+        self.curr = 0
+        self.mark = None
+
+    def save_file(self):
+        try:
+            f = open(self.filename + '.tmp', 'w')
+            for l in self.list:
+                f.write(':'.join(l) + '\n')
+            f.close()
+            os.rename(self.filename + '.tmp', self.filename)
+        except:
+            pass
+
+    def selected(self, ev, ind):
+        self.curr = ind
+        l = self.list[ind]
+        ind+= 1
+        ltr = float(l[2])
+        while ind < len(self.list) and \
+                (len(self.list[ind]) <= 3 or self.list[ind][3] != 'full'):
+            ltr += float(self.list[ind][2])
+            ind += 1
+        if ind >= len(self.list) or len(l) <= 3 or l[3] != 'full':
+            lckm = "??.??? l/CKm"
+        else:
+            km = float(l[1]) - float(self.list[ind][1])
+            lckm = "%6.3f l/CKm" % (ltr*100/km)
+        self.listui.usage_summary.set_text(lckm)
+
+        if len(l) >= 5 and float(l[2]) > 0:
+            cl = "%6.2f c/l" % (float(l[4])*100/float(l[2]))
+        else:
+            cl = "???.? c/l"
+        self.listui.price_summary.set_text(cl)
+
+    def new(self, ev):
+        self.curr = None
+        self.editui.date.set_text('Today')
+        self.km_entry.set_text('')
+        self.l_entry.set_text('')
+        self.full_label.set_text('(full)')
+        self.cl_entry.set_text('')
+        self.cost_entry.set_text('')
+        self.listui.hide()
+        self.editui.show()
+
+    def edit(self, ev):
+        if self.curr == None:
+            self.curr = 0
+        l = self.list[self.curr]
+        self.editui.date.set_text(time.strftime('%d/%b/%Y', time.strptime(l[0], "%Y-%m-%d")))
+        self.km_entry.set_text(l[1])
+        self.l_entry.set_text(l[2])
+        self.full_label.set_text('(full)' if l[3]=='full' else '(not full)')
+        self.cost_entry.set_text(l[4])
+        self.entry_priority=[self.l_entry, self.cost_entry];
+        self.calc_other()
+        self.listui.hide()
+        self.editui.show()
+
+    def mark(self, ev):
+        pass
+
+    def date_get(self):
+        x = self.editui.date.get_text()
+        try:
+            then = time.strptime(x, "%d/%b/%Y")
+        except ValueError:
+            then = time.localtime()
+        return then
+    def date_prev(self, ev):
+        tm = time.localtime(time.mktime(self.date_get()) - 12*3600)
+        self.editui.date.set_text(time.strftime("%d/%b/%Y", tm))
+
+    def date_next(self, ev):
+        t = time.mktime(self.date_get()) + 25*3600
+        if t > time.time():
+            t = time.time()
+        tm = time.localtime(t)
+        self.editui.date.set_text(time.strftime("%d/%b/%Y", tm))
+
+    def fill(self, ev):
+        if self.full_label.get_text() == "(full)":
+            self.full_label.set_text("(not full)")
+            self.fill_button.child.set_text("Fill")
+        else:
+            self.full_label.set_text("(full)")
+            self.fill_button.child.set_text("non-Fill")
+
+
+    def discard(self, ev):
+        self.listui.show()
+        self.editui.hide()
+        pass
+
+    def save(self, ev):
+        self.listui.show()
+        self.editui.hide()
+        date = time.strftime('%Y-%m-%d', self.date_get())
+        km = "%d" % self.check_entry(self.km_entry)
+        ltr = "%.2f" % self.check_entry(self.l_entry)
+        full = 'full' if self.full_label.get_text() == "(full)"  else 'notfull'
+        price = "%.2f" % self.check_entry(self.cost_entry)
+        if self.curr == None:
+            self.list.append([date,km,ltr,full,price])
+        else:
+            self.list[self.curr] = [date,km,ltr,full,price]
+        self.list.sort(reverse=True)
+        self.pl.set_list(self.list)
+        self.listui.list.list_changed()
+        if self.curr == None:
+            self.listui.list.select(0)
+            self.curr = 0
+        else:
+            self.listui.list.select(self.curr)
+        self.save_file()
+        self.selected(None, self.curr)
+
+
+if __name__ == "__main__":
+
+    p = Petrol("/data/RAV4")
+    p.set_default_size(480, 640)
+    p.show()
+    gtk.main()
+
diff --git a/plato/cmd.py b/plato/cmd.py
new file mode 100644 (file)
index 0000000..c841294
--- /dev/null
@@ -0,0 +1,215 @@
+#
+# Support for running commands from the Laucher
+#
+# Once a command is run, we watch for it to finish
+# and update status when it does.
+# We also maintain window list and for commands that appear
+# in windows, we associate the window with the command.
+# ShellTask() runs a command and captures output in a text buffer
+#  that can be displayed in a FingerScroll
+# WinTask() runs a command in a window
+
+import os,fcntl, gobject
+import pango
+from subprocess import Popen, PIPE
+from fingerscroll import FingerScroll
+
+class ShellTask:
+    # Normally there is one button : "Run"
+    # When this is pressed we create a 'FingerScroll' text buffer
+    # to hold the output.
+    # The button then becomes 'ReRun'
+    # When we get deselected, the buffer gets hidden and we get
+    # new button 'Display'
+    def __init__(self, name, line, owner):
+        self.format = "cmd"
+        self.append = False
+        self.is_full = True
+        # remove leading '!'
+        line = line[1:]
+        if line[0] == '-':
+            self.is_full = False
+            line = line[1:]
+        if line[0] == '+':
+            self.append = True
+            line = line[1:]
+        if name == None:
+            self.name = line
+        else:
+            self.name = name
+        self.cmd = line
+        self.buffer = None
+        self.displayed = False
+        self.job = None
+        self.owner = owner
+
+    def buttons(self):
+        if self.displayed:
+            if self.job:
+                return ['-','Kill']
+            else:
+                return ['ReRun', 'Close']
+        if self.buffer != None:
+            if self.job:
+                return ['-', 'Display']
+            else:
+                return ['Run', 'Display']
+        return ['Run']
+
+    def embedded(self):
+        if self.displayed:
+            return self.buffer
+        return None
+    def embed_full(self):
+        return self.is_full
+
+    def press(self, num):
+        if num == 1:
+            if self.displayed and self.job:
+                # must be a 'kill' request'
+                os.kill(self.job.pid, 15)
+                return
+            self.displayed = not self.displayed
+            self.owner.update(self, False)
+            return
+
+        if self.job:
+            return
+
+        if self.buffer == None:
+            self.buffer = FingerScroll()
+            self.buffer.show()
+            self.buffer.connect('hide', self.unmap_buff)
+
+            fd = pango.FontDescription('Monospace 10')
+            fd.set_absolute_size(15*pango.SCALE)
+            self.buffer.modify_font(fd)
+        self.buff = self.buffer.get_buffer()
+        if not self.append:
+            self.buff.delete(self.buff.get_start_iter(), self.buff.get_end_iter())
+        # run the program
+        self.job = Popen(self.cmd, shell=True, close_fds = True,
+                         stdout=PIPE, stderr=PIPE)
+
+        def set_noblock(f):
+            flg = fcntl.fcntl(f, fcntl.F_GETFL, 0)
+            fcntl.fcntl(f, fcntl.F_SETFL, flg | os.O_NONBLOCK)
+        set_noblock(self.job.stdout)
+        set_noblock(self.job.stderr)
+        self.wc = gobject.child_watch_add(self.job.pid, self.done)
+        self.wout = gobject.io_add_watch(self.job.stdout, gobject.IO_IN, self.read)
+        self.werr = gobject.io_add_watch(self.job.stderr, gobject.IO_IN, self.read)
+
+        self.displayed = True
+        
+    def read(self, f, dir):
+        l = f.read()
+        self.buff.insert(self.buff.get_end_iter(), l)
+        gobject.idle_add(self.adjust)
+        if l == "":
+            return False
+        return True
+
+    def adjust(self):
+        adj = self.buffer.vadj
+        adj.set_value(adj.upper - adj.page_size)
+
+    def done(self, *a):
+        self.read(self.job.stdout, None)
+        self.read(self.job.stderr, None)
+        gobject.source_remove(self.wout)
+        gobject.source_remove(self.werr)
+        self.job.stdout.close()
+        self.job.stderr.close()
+        self.job = None
+        self.owner.update(self, None)
+
+    def unmap_buff(self, widget):
+        if self.job == None:
+            self.displayed = False
+
+
+class WinTask:
+    # A WinTask runs a command and expects it to
+    # create a window.  This window can then be
+    # raised or closed, or the process killed.
+    # we find out about windows appearing
+    # by connecting to the 'new-window' signal in
+    # the owner
+    # When there is no process, button in "Run"
+    # When there is a process but no window,  "Kill"
+    # When there is process and window, "Raise", "Close", "Kill"
+    def __init__(self, name, window, line, owner):
+        self.name = name
+        self.job = None
+        self.win_id = None
+        self.win = None
+        self.window_name = window
+        self.cmd = line
+        self.embedded = lambda:None
+        self.owner = owner
+        owner.connect('new-window', self.new_win)
+        owner.connect('lost-window', self.lost_win)
+        owner.connect('request-window', self.request_win)
+
+    def buttons(self):
+        if not self.job:
+            return ['Run']
+        if not self.win_id:
+            return ['-','Kill']
+        return ['Raise','Close','Kill']
+
+    def get_format(self):
+        if self.job or self.win:
+            return "win"
+        else:
+            return "cmd"
+
+    def press(self, num):
+        if not self.job and not self.win:
+            if num != 0:
+                return
+            self.job = Popen(self.cmd, shell=True, close_fds = True)
+            self.wc = gobject.child_watch_add(self.job.pid, self.done)
+            return
+
+        if not self.win_id and self.job:
+            if num != 1:
+                return
+            os.kill(self.job.pid, 15)
+            return
+
+        # We have a win_id and a job
+        if num == 0:
+            self.win.raise_win()
+        elif num == 1:
+            self.win.close_win()
+        elif num == 2 and self.job:
+            os.kill(self.job.pid, 15)
+
+    def done(self, *a):
+        self.job = None
+        self.win_id = None
+        self.owner.update(self, None)
+
+    def new_win(self, source, win):
+        if self.job and self.job.pid == win.pid:
+            self.win_id = win.id
+            self.win = win
+        if self.window_name and self.window_name == win.name:
+            self.win_id = win.id
+            self.win = win
+        if self.win_id:
+            self.format = "win"
+        self.owner.update(self, None)
+
+    def lost_win(self, source, id):
+        if self.win_id == id:
+            self.win_id = None
+            self.win = None
+            self.format = "cmd"
+            self.owner.update(self, None)
+
+    def request_win(self, source, name):
+        if self.window_name == name:
+            self.press(0)
diff --git a/plato/grouptypes.py b/plato/grouptypes.py
new file mode 100644 (file)
index 0000000..1ebff72
--- /dev/null
@@ -0,0 +1,105 @@
+#
+# Generic code for group types
+#
+# A 'group' must provide:
+#
+# - 'parse' to take a line from the config file and interpret it.
+# - 'name' and 'format' which are present to ListSelect.
+#     'format' is normally 'group'.
+# - 'tasks' which provides a task list to display in the other
+#    ListSelect.
+#
+# A 'group' can generate the signal:
+# - 'new-task-list' to report that the task list has changed
+# ???
+#
+
+import cmd, re
+
+class IgnoreType:
+    def __init__(self, win, name):
+        self.name = name
+        self.format = 'group'
+        self.tasks = []
+
+    def parse(self, line):
+        pass
+
+class ListType:
+    """A ListType group parses a list of tasks out of
+    the config file and provides them as a static list.
+    Tasks can be:
+      !command   - run command and capture text output
+      (window)command - run command and expect it to appear as 'window'
+      command.command - run an internal command from the given module
+    In each case, arguments can follow and are treated as you would expect.
+    """
+
+    def __init__(self, win, name):
+        self.name = name
+        self.format = 'group'
+        self.tasks = []
+        self.win = win
+
+    def parse(self, line):
+        line = line.strip()
+        m = re.match('^([A-Za-z0-9_ ]+)=(.*)', line)
+        if m:
+            name = m.groups()[0].strip()
+            line = m.groups()[1].strip()
+        else:
+            name = None
+        if line[0] == '!':
+            self.parse_txt(name, line)
+        elif line[0] == '(':
+            self.parse_win(name, line)
+        else:
+            self.parse_internal(name, line)
+
+    def parse_txt(self, name, line):
+        task = cmd.ShellTask(name, line, self)
+        if task.name:
+            self.tasks.append(task)
+
+    def parse_win(self, name, line):
+        f = line[1:].split(')', 1)
+        if len(f) != 2:
+            return
+        if name == None:
+            name = f[0]
+        task = cmd.WinTask(name, f[0], f[1], self)
+        if task:
+            self.tasks.append(task)
+
+    def parse_internal(self, name, line):
+        # split in to words, honouring quotes
+        words = map(lambda a: a[0].strip('"')+a[1].strip("'")+a[2],
+                    re.findall('("[^"]*")?(\'[^\']*\')?([^ "\'][^ "\']*)? *',
+                               line))[:-1]
+        
+        cmd = words[0].split('.', 1)
+        if len(cmd) == 1:
+            words[0] = "internal."+cmd[0]
+            cmd = ['internal',cmd[0]]
+        if len(cmd) != 2:
+            return
+
+        exec "import plato_" + cmd[0]
+        fn = eval("plato_"+words[0])
+        if name == None:
+            name = cmd[0]
+        task = fn(name, self, *words[1:])
+        if task:
+            self.tasks.append(task)
+
+    def update(self, task, refresh):
+        if refresh:
+            self.win.select(self, task)
+        self.win.update(task)
+    def emit(self, *args):
+        self.win.emit(*args)
+
+    def connect(self, name, *cmd):
+        return self.win.connect(name, *cmd)
+    def queue_draw(self):
+        return self.win.queue_draw()
diff --git a/plato/plato.py b/plato/plato.py
new file mode 100644 (file)
index 0000000..de2293a
--- /dev/null
@@ -0,0 +1,564 @@
+#!/usr/bin/env python
+
+#TODO
+# aux button
+# calculator
+# sms
+#   entry shows number of new messages
+#   embed shows latest new message if there is one
+#   buttons allow 'read' or 'open'
+# phone calls
+#   number of missed calls, or incoming number
+#   embed shows call log, or tap board
+#   buttons allow 'call' or 'answer' or 'open' ....
+# embed calendar
+#   selected by 'date'
+#   tapping can select a date
+# run windowed program
+# monitor list of windows
+# address list
+#   this is a 'group' where 'tasks' are contacts
+
+
+# Launcher, version 2.
+# This module just defines the UI and plugin interface
+# All tasks etc go in loaded modules.
+#
+# The elements of the UI are:
+#
+#   ----------------------------------
+#   |  Text (Entry) box (one line)   |
+#   +--------------------------------+
+#   |                |               |
+#   |    group       |      task     |
+#   |   selection    |     selection |
+#   |    list        |      list     |
+#   |                |               |
+#   |                |               |
+#   +----------------+               |
+#   |   optional     |               |
+#   |   task-        |               |
+#   |     specific   |               |
+#   |   widget       +---------------|
+#   |                | secondary     |
+#   |                | button row    |
+#   +----------------+---------------+
+#   |         Main Button Row        |
+#   +--------------------------------+
+#
+# If the optional widget is present, then the Main Button Row
+# disappears and the secondary button row is used instead.
+#
+# The selection lists auto-scroll when an item near top or bottom
+# is selected.  Selecting an entry only updates other parts of
+# the UI and does not perform any significant action.
+# Actions are performed by buttons which appear in a button
+# row.
+# The optional widget can be anything provided by the group
+# or task, for example:
+#  - tap-board for entering text
+#  - calendar for selecting a date
+#  - display area for small messages (e.g. SMS)
+#  - preview during file selection
+#  - map of current location
+#
+# Entered text alway appears in the Text Entry box and
+# is made available to the current group and task
+# That object may use the text and may choose to delete it.
+# e.g.
+#   A calculator task might display an evaluation of the text
+#   A call task might offer to call the displayed number, and
+#     delete it when the call completes
+#   An address-book group might restrict displayed tasks
+#     to those which match the text
+#   A 'todo' task might set the text to match the task content,
+#     then update the content as the text changes.
+#
+# A config file describes the groups.
+# It is .platorc in $HOME
+# A group is enclosed in [] and can have an optional type:
+#  [group1]
+#  [Windows/winlist]
+# If no type is given the 'list' type is used.
+# this parses following lines to create a static list of tasks.
+# Other possible types are:
+#   winlist: list of windows on display
+#   contacts: list of contacts
+#   notifications: list of recent notifications (alarm/battery/whatever)
+#
+# the 'list' type is described in more detail in 'grouptypes'.
+#
+
+import sys, gtk, os, pango
+import gobject
+
+if __name__ == '__main__':
+    sys.path.insert(1, '/home/neilb/home/freerunner/lib')
+    sys.path.insert(1, '/home/neilb/home/freerunner/sms')
+    sys.path.insert(1, '/root/lib')
+
+from tapboard import TapBoard
+import grouptypes, listselect, scrawl
+from grouptypes import *
+from window_group import *
+from evdev import EvDev
+
+import plato_gsm
+
+class MakeList:
+    # This class is a wrapper on a 'tasklist' which presents an
+    # entry list that can be used by ListSelect.
+    # Each entry in the list is a name and a format
+    # The group can either contain a list of task: tasks
+    # or a function: get_task
+    # in the later case the length is reported as -1
+    #
+    # a task must provide a .name or a get_name function
+    # similarly a .format or a .get_format function
+    def __init__(self, group):
+        self.group = group
+
+    def __getitem__(self, ind):
+        i = self.get_item(ind)
+        if i == None:
+            return None
+        try:
+            name = i.name
+        except AttributeError:
+            name = i.get_name()
+        try:
+            format = i.format
+        except AttributeError:
+            format = i.get_format()
+        return (name, format)
+
+    def __len__(self):
+        try:
+            e = self.group.tasks
+            l = len(e)
+        except AttributeError:
+            l = 50000
+        return l
+
+    def get_item(self, ind):
+        try:
+            e = self.group
+            if ind >= 0 and ind < len(e):
+                i = e[ind]
+            else:
+                i = None
+        except AttributeError:
+            try:
+                e = self.group.tasks
+                if ind >= 0 and ind < len(e):
+                    i = e[ind]
+                else:
+                    i = None
+            except AttributeError:
+                i = self.group.get_task(ind)
+        return i
+
+
+class LaunchWin(gtk.Window):
+    __gsignals__ = {
+        'new-window' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                        (gobject.TYPE_PYOBJECT,)),
+        'lost-window' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                         (gobject.TYPE_INT,)),
+        'request-window' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                         (gobject.TYPE_STRING,)),
+        }
+    def __init__(self):
+        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
+        self.set_default_size(480, 640)
+        self.connect('destroy', lambda w: gtk.main_quit())
+
+        self.embeded_widget = None
+        self.embeded_full = False
+        self.hidden = True
+        self.next_group = 0
+        self.widgets = {}
+        self.buttons = []
+        self.create_ui()
+        self.load_config()
+        self.grouplist.select(0)
+
+    def create_ui(self):
+        # Create the UI framework, first the components
+
+        # The Entry
+        e1 = gtk.Entry()
+        e1.set_alignment(0.5) ; # Center text
+        e1.connect('changed', self.entry_changed)
+        e1.connect('backspace', self.entry_changed)
+        e1.show()
+        self.entry = e1
+
+        # Label for the entry
+        lbl = gtk.Label("Label")
+        lbl.hide()
+        self.label = lbl
+
+        # The text row: Entry fills, label doesn't
+        tr = gtk.HBox()
+        tr.pack_start(lbl, expand = False)
+        tr.pack_end(e1, expand = True)
+        tr.show()
+
+        # The group list
+        l1 = listselect.ListSelect(center = False, linescale = 1.3, markup = True)
+        l1.connect('selected', self.group_select)
+        # set appearance here
+        l1.set_colour('group','blue')
+        self.grouplist = l1
+        l1.set_zoom(35)
+        l1.show()
+
+        # The task list
+        l2 = listselect.ListSelect(center = True, linescale = 1.3, markup = True)
+        l2.connect('selected', self.task_select)
+        l2.set_colour('cmd', 'black')
+        l2.set_colour('win', 'blue')
+        l2.set_zoom(35)
+        # set appearance
+        self.tasklist = l2
+        l2.show()
+
+        # The embedded widget: provide a VBox as a place holder
+        v1 = gtk.VBox()
+        self.embed_box = v1
+
+        # The Main button box - buttons are added later
+        h1 = gtk.HBox(True)
+        self.main_buttons = h1
+        # HACK
+        h1.set_size_request(-1, 80)
+        h1.show()
+
+        # The Secondary button box
+        h2 = gtk.HBox(True)
+        h2.set_size_request(-1, 60)
+        self.secondary_buttons = h2
+
+        # Now make the two columns
+
+        v2 = gtk.VBox(True)
+        v2.pack_start(self.grouplist, expand = True)
+        v2.pack_end(self.embed_box, expand = False)
+        v2.show()
+
+        v3 = gtk.VBox()
+        v3.pack_start(self.tasklist, expand = True)
+        v3.pack_end(self.secondary_buttons, expand = False)
+        v3.show()
+
+        # and bind them together
+        h3 = gtk.HBox(True)
+        h3.pack_start(v2, expand=True)
+        h3.pack_end(v3, expand=True)
+        h3.show()
+
+        # A vbox to hold tr and main buttons
+        v3a = gtk.VBox()
+        v3a.show()
+        v3a.pack_start(tr, expand=False)
+        v3a.pack_end(h3, expand=True)
+        self.non_buttons = v3a
+
+        # a box for a 'full screen' embedded widget
+        v3b = gtk.VBox()
+        v3b.hide()
+        self.embed_full_box = v3b
+
+
+        # And now one big vbox to hold it all
+        v4 = gtk.VBox()
+        v4.pack_start(self.non_buttons, expand=True)
+        v4.pack_start(self.embed_full_box, expand=True)
+        v4.pack_end(self.main_buttons, expand=False)
+        v4.show()
+        self.add(v4)
+        self.show()
+
+        ## We want writing recognistion to work
+        ## over the whole middle section.  Only that
+        ## turns out to be too hard for my lowly gtk
+        ## skills.  So we do recognition separately
+        ## on each selection box only.
+        s1 = scrawl.Scrawl(l1, self.getsym, lambda p: l1.tap(p[0],p[1]))
+        s2 = scrawl.Scrawl(l2, self.getsym, lambda p: l2.tap(p[0],p[1]))
+        s1.set_colour('red')
+        s2.set_colour('red')
+
+
+        ctx = self.get_pango_context()
+        fd = ctx.get_font_description()
+        fd.set_absolute_size(30 * pango.SCALE)
+        self.button_font = fd;
+        self.entry.modify_font(fd)
+        self.label.modify_font(fd)
+
+    def load_config(self):
+        fname = os.path.join(os.environ['HOME'], ".platorc")
+        types = {}
+        types['ignore'] = IgnoreType
+        types['list' ] = ListType
+        types['winlist'] = WindowType
+        types['call_list']=plato_gsm.call_list
+        groups = []
+        f = open(fname)
+        gobj = None
+        for line in f:
+            l = line.strip()
+            if not l:
+                continue
+            if l[0] == '[':
+                l = l.strip('[]')
+                f = l.split('/', 1)
+                group = f[0]
+                if len(f) > 1:
+                    group_type = f[1]
+                else:
+                    group_type = "list"
+                if group_type in types:
+                    gobj = types[group_type](self, group)
+                else:
+                    gobj = types['ignore'](self, group)
+                groups.append(gobj)
+            elif gobj != None:
+                gobj.parse(l)
+        self.grouplist.list = MakeList(groups)
+        self.grouplist.list_changed()
+
+
+    def entry_changed(self, entry):
+        print "fixme", entry.get_text()
+
+    def group_select(self, list, item):
+        g = list.list.get_item(item)
+        self.tasklist.list = MakeList(g)
+        self.tasklist.list_changed()
+        self.tasklist.select(None)
+        if self.tasklist.list != None:
+            self.task_select(self.tasklist,
+                             self.tasklist.selected)
+
+    def set_group(self, group):
+        self.tasklist.list = MakeList(group)
+        self.tasklist.list_changed()
+        self.tasklist.select(None)
+        if self.tasklist.list != None:
+            self.task_select(self.tasklist,
+                             self.tasklist.selected)
+
+    def task_select(self, list, item):
+        if item == None:
+            self.set_buttons(None)
+            self.set_embed(None, None)
+        else:
+            task = self.tasklist.list.get_item(item)
+            if task:
+                self.set_buttons(task.buttons())
+                bed = task.embedded()
+                full = False
+                if bed != None and type(bed) != str:
+                    full = task.embed_full()
+                self.set_embed(bed, full)
+
+    def update(self, task):
+        if self.tasklist.selected != None and \
+           self.tasklist.list.get_item(self.tasklist.selected) == task:
+            self.set_buttons(task.buttons())
+            bed = task.embedded()
+            full = False
+            if bed != None and type(bed) != str:
+                full = task.embed_full()
+            self.set_embed(bed, full)
+
+    def select(self, group, task):
+        i = 0
+        gl = self.grouplist.list
+        while i < len(gl) and gl.get_item(i) != None:
+            if gl.get_item(i) == group:
+                self.grouplist.select(i)
+                break
+            i += 1
+        tl = self.tasklist.list
+        i = 0
+        while i < len(tl) and tl.get_item(i) != None:
+            if tl.get_item(i) == task:
+                self.tasklist.select(i)
+                self.present()
+                break
+            i += 1
+    
+    def set_buttons(self, list):
+        if not list:
+            # hide the button boxes
+            self.secondary_buttons.hide()
+            self.main_buttons.hide()
+            self.buttons = []
+            return
+        if self.same_buttons(list):
+            return
+
+        self.buttons = list
+        self.update_buttons(self.main_buttons)
+        self.update_buttons(self.secondary_buttons)
+        if self.embeded_widget and not self.embeded_full:
+            self.main_buttons.hide()
+            self.secondary_buttons.show()
+        else:
+            self.secondary_buttons.hide()
+            self.main_buttons.show()
+
+    def same_buttons(self, list):
+        if len(list) != len(self.buttons):
+            return False
+        for i in range(len(list)):
+            if list[i] != self.buttons[i]:
+                return False
+        return True
+
+    def update_buttons(self, box):
+        # make sure there are enough buttons
+        have = len(box.get_children())
+        need = len(self.buttons) - have
+        if need > 0:
+            for i in range(need):
+                b = gtk.Button("?")
+                b.child.modify_font(self.button_font)
+                b.set_property('can-focus', False)
+                box.add(b)
+                b.connect('clicked', self.button_pressed, have + i)
+            have += need
+        b = box.get_children()
+        # hide extra buttons
+        if need < 0:
+            for i in range(-need):
+                b[have-i-1].hide()
+        # update each button
+        for i in range(len(self.buttons)):
+            b[i].child.set_text(self.buttons[i])
+            b[i].show()
+
+    def button_pressed(self, widget, num):
+        self.hidden = True
+        self.tasklist.list.get_item(self.tasklist.selected).press(num)
+        self.task_select(self.tasklist,
+                         self.tasklist.selected)
+
+    def set_embed(self, widget, full):
+        if type(widget) == str:
+            widget, full = self.make_widget(widget)
+            
+        if widget == self.embeded_widget and full == self.embeded_full:
+            return
+        if self.embeded_widget:
+            self.embeded_widget.hide()
+            if self.embeded_full:
+                self.embed_full_box.remove(self.embeded_widget)
+            else:
+                self.embed_box.remove(self.embeded_widget)
+            self.embeded_widget = None
+        self.embeded_widget = widget
+        self.embeded_full = full
+        if widget and not full:
+            self.embed_box.add(widget)
+            widget.show()
+            self.main_buttons.hide()
+            self.embed_box.show()
+            self.non_buttons.show()
+            self.embed_full_box.hide()
+            if self.buttons:
+                self.secondary_buttons.show()
+        elif widget:
+            self.embed_full_box.add(widget)
+            widget.show()
+            self.embed_full_box.show()
+            self.non_buttons.hide()
+            if self.buttons:
+                self.main_buttons.show()
+        else:
+            self.embed_box.hide()
+            self.embed_full_box.hide()
+            self.non_buttons.show()
+            self.secondary_buttons.hide()
+            if self.buttons:
+                self.main_buttons.show()
+
+    def make_widget(self, name):
+        if name in self.widgets:
+            return self.widgets[name]
+        if name == "tapboard":
+            w = TapBoard()
+            def key(w, k):
+                if k == '\b':
+                    self.entry.emit('backspace')
+                elif k == 'Return':
+                    self.entry.emit('activate')
+                elif len(k) == 1:
+                    self.entry.emit('insert-at-cursor', k)
+            w.connect('key', key)
+            self.widgets[name] = (w, False)
+            return (w, False)
+        return None
+
+    def getsym(self, sym):
+        print "gotsym", sym
+        if sym == '<BS>':
+            self.entry.emit('backspace')
+        elif sym == '<newline>':
+            self.entry.emit('activate')
+        elif len(sym) == 1:
+            self.entry.emit('insert-at-cursor', sym)
+
+    def activate(self, *a):
+        # someone pressed a button or icon to get our
+        # attention.
+        # We raise the window and if nothing has happened recently,
+        # select first group
+        if self.hidden:
+            self.hidden = False
+            self.next_group = 0
+        else:
+            if (self.next_group > len(self.grouplist.list) or
+                self.grouplist.list[self.next_group] == None):
+                self.next_group = 0
+            self.grouplist.select(self.next_group)
+            self.next_group += 1
+            
+        self.present()
+
+    def ev_activate(self, down, moment, typ, code, value):
+        if typ != 1:
+            # not a key press
+            return
+        if code != 169 and code != 116:
+            # not AUX or Power
+            return
+        if value == 0:
+            # down press, wait for up
+            self.down_at = moment
+            return
+        self.activate()
+
+class LaunchIcon(gtk.StatusIcon):
+    def __init__(self, callback):
+        gtk.StatusIcon.__init__(self)
+        self.set_from_stock(gtk.STOCK_EXECUTE)
+        self.connect('activate', callback)
+
+
+if __name__ == '__main__':
+    sys.path.insert(1, '/home/neilb/home/freerunner/lib')
+    sys.path.insert(1, '/root/lib')
+    l = LaunchWin()
+    i = LaunchIcon(l.activate)
+    try:
+        aux = EvDev("/dev/input/aux", l.ev_activate)
+    except OSError:
+        aux = None
+
+    gtk.settings_get_default().set_long_property("gtk-cursor-blink", 0, "main")
+    gtk.main()
diff --git a/plato/plato_gsm.py b/plato/plato_gsm.py
new file mode 100644 (file)
index 0000000..dd3a88b
--- /dev/null
@@ -0,0 +1,484 @@
+
+#
+# plato plugins for mobile phone functionality.
+# carrier:
+#     displays current carrier
+#     button will search for possible carriers
+#     while search, button show how long search has progressed
+#     on succcess, get one button per carrier valid for 2 minutes
+#     When one is selected, write that to /var/run/gsm/request_carrier
+#
+# calls:
+#     display incoming phone number and when there is one, allows
+#     'answer' or 'reject', then '-' or 'hangup'
+#     When no call, buttons for 'dial' or 'contacts'
+#
+# newmsg:
+#     displays new message and allows more to be shown in embedded
+#     also allows sms reader to be run
+
+from plato_internal import file
+from subprocess import Popen, PIPE
+import os.path, fcntl, time, gobject, re, gtk
+
+def record(key, value):
+    f = open('/var/run/gsm-state/.new.' + key, 'w')
+    f.write(value)
+    f.close()
+    os.rename('/var/run/gsm-state/.new.' + key,
+              '/var/run/gsm-state/' + key)
+
+
+def recall(key):
+    try:
+        fd = open("/var/run/gsm-state/" + key)
+        l = fd.read(1000)
+        fd.close()
+    except IOError:
+        l = ""
+    return l.strip()
+
+class carrier(file):
+    def __init__(self, name, owner):
+        file.__init__(self, name, owner, "/var/run/gsm-state/carrier")
+        self.buttons = self.cbuttons
+
+        self.carriers = []
+        self.carrier_buttons = None
+        self.carrier_valid = 0
+        self.reader = None
+        self.job = None
+        self.job_start = 0
+        self.timeout = None
+
+    def cbuttons(self):
+        if self.job:
+            # waiting for carriers - can cancel
+            if self.timeout == None:
+                self.timeout = gobject.timeout_add(3000, self.change_button)
+            waited = time.time() - self.job_start
+            return [ 'Cancel after %d seconds' % waited]
+
+        if self.carrier_buttons and time.time() - self.carrier_valid < 120:
+            return self.carrier_buttons
+        self.carrier_buttons = None
+        if (os.path.exists('/var/run/gsm-state/request_carrier') and
+            os.path.getsize('/var/run/gsm-state/request_carrier') > 1):
+                return ['Search Carriers','Cancel Request']
+        return ['Search Carriers']
+
+    def change_button(self):
+        self.timeout = None
+        self.owner.update(self, None)
+
+    def press(self, ind):
+        if self.job:
+            if ind == 0:
+                self.job.terminate()
+            return
+
+        if self.carriers and self.carrier_buttons:
+            if ind < len(self.carriers):
+                num = self.carriers[ind][0]
+                record('request_carrier',num)
+                self.carrier_buttons = None
+            return
+
+        if ind == 1:
+            record('request_carrier', '')
+            return
+        if ind != 0:
+            return
+
+        # initiate search
+        # run gsm-carriers expecting output like:
+        # 50503 0 "voda AU" "vodafone AU"
+        # record output in carriers[] and short names in carrer_buttons
+        self.job = Popen("gsm-carriers", shell=True, close_fds=True,
+                         stdout = PIPE)
+        self.job_start = time.time()
+        gobject.child_watch_add(self.job.pid, self.done)
+        flg = fcntl.fcntl(self.job.stdout, fcntl.F_GETFL, 0)
+        fcntl.fcntl(self.job.stdout, fcntl.F_SETFL, flg | os.O_NONBLOCK)
+        self.buf = ''
+        self.reader = gobject.io_add_watch(self.job.stdout, gobject.IO_IN,
+                                           self.read)
+
+    def read(self, f, dir):
+        try:
+            l = f.read()
+        except IOError:
+            l = None
+        if l == None:
+            if self.job.poll() == None:
+                return True
+        elif l != '':
+            self.buf += l
+            return True
+        self.job.wait()
+        lines = self.buf.split('\n')
+        c=[]
+        b=[]
+        for line in lines:
+            words = re.findall('([^"][^ ]*|"[^"]*["]) *', line)
+            if len(words) == 4:
+                c.append(words)
+                b.append(words[2].strip('"'))
+        self.carriers = c
+        self.carrier_buttons = b
+        self.carrier_valid = time.time()
+        self.job = None
+        self.owner.update(self, None)
+        return False
+
+    def done(self, *a):
+        if not self.job:
+            return
+        self.job.wait()
+        #flg = fcntl.fcntl(self.job.stdout, fcntl.F_GETFL, 0)
+        #fcntl.fcntl(self.job.stdout, fcntl.F_SETFL, flg & ~os.O_NONBLOCK)
+        self.read(self.job.stdout, None)
+
+
+
+class calls(file):
+    # Monitor 'status' which can be
+    #   INCOMING BUSY on-call or empty
+    # INCOMING:
+    #   get caller from 'incoming' and display - via contacts if possibe
+    #        Buttons are 'answer','reject'
+    # BUSY:
+    #   display BUSY
+    #        Buttons are '-', 'cancel'
+    # on-call:
+    #   Buttons are 'hold', 'cancel'
+    # empty:
+    #   display (call logs)
+    #     Buttons are "Received" "Dialled" "Dialer" "Contacts"
+    def __init__(self, name, owner):
+        file.__init__(self, name, owner, '/var/run/gsm-state/status')
+        self.buttons = self.cbuttons
+        self.get_name = self.cgetname
+        self.status = ''
+        self.choose_logs = False
+        self.caller = None
+        self.dialer_win = None
+        self.contacts_win = None
+        self.dj = None
+        self.cj = None
+        # catch queue_draw from 'file'
+        self.owner = self
+        self.gowner = owner
+        owner.connect('new-window', self.new_win)
+        owner.connect('lost-window', self.lost_win)
+
+    def cbuttons(self):
+        if self.status == '' or self.status == '-':
+            if self.choose_logs:
+                return ['Call', "Recv'd\nCalls","Dialed\nCalls"]
+            else:
+                return ['Logs', "Dialer", "Contacts"]
+        if self.status == 'INCOMING':
+            return ['Answer','Reject']
+        if self.status == 'BUSY':
+            return ['-','Cancel']
+        if self.status == 'on-call':
+            return ['Hold','Cancel']
+        return []
+
+    def cgetname(self):
+        status = file.get_name(self)
+        if self.status != status:
+            self.status = status
+            print "update for", status
+            # was "status == 'INCOMING'" to raise on incoming calls
+            # but want Dailer to do that
+            self.gowner.update(self, False)
+        if self.status == 'INCOMING':
+            if self.caller == None:
+                self.caller = recall('incoming')
+                if self.caller == '':
+                    self.caller = '(private)'
+                else:
+                    contacts = contactdb.contacts()
+                    n = contacts.find_num(self.caller)
+                    if n:
+                        self.caller = n.name
+        else:
+            self.caller = None
+        if self.status == '' or self.status == '-':
+            if self.choose_logs:
+                return '(call logs)'
+            else:
+                return '(no call)'
+        if self.status == 'INCOMING':
+            return self.caller + ' calling'
+        if self.status == 'BUSY':
+            return 'BUSY'
+        if self.status == 'on-call':
+            return "On Call"
+        return '??'+self.status
+
+    def press(self, ind):
+        if self.status == '' or self.status == '-':
+            if self.choose_logs:
+                if ind == 0:
+                    self.choose_logs = False
+                elif ind == 1:
+                    self.gowner.win.set_group(call_list(self.gowner.win, '', True))
+                elif ind == 2:
+                    self.gowner.win.set_group(call_list(self.gowner.win, '', False))
+            else:
+                if ind == 0:
+                    self.choose_logs = True
+                elif ind == 1:
+                    if self.dialer_win:
+                        self.dialer_win.raise_win()
+                    elif not self.dj:
+                        self.dj = Popen("dialer", shell=True, close_fds = True)
+                        gobject.child_watch_add(self.dj.pid, self.djdone)
+                elif ind == 2:
+                    if self.contacts_win:
+                        self.contacts_win.raise_win()
+                    elif not self.cj:
+                        self.cj = Popen("contacts", shell=True, close_fds = True)
+                        gobject.child_watch_add(self.cj.pid, self.cjdone)
+            self.gowner.update(self, None)
+            return
+
+        if self.status == 'INCOMING':
+            if ind == 0:
+                record('call','answer')
+            elif ind == 1:
+                record('call','')
+            return
+
+        if ind == 0:
+            print "on hold??"
+            return
+        if ind == 1:
+            record('call','')
+
+    def queue_draw(self):
+        # file changed
+        self.cgetname()
+        self.gowner.queue_draw()
+
+    def djdone(self, *a):
+        self.dj.wait()
+        self.dj = None
+    def cjdone(self, *a):
+        self.cj.wait()
+        self.cj = None
+                
+    def new_win(self, source, win):
+        if win.name == "Dialer":
+            self.dialer_win = win
+        if win.name == "Contacts":
+            self.contacts_win = win
+
+    def lost_win(self, source, id):
+        if self.dialer_win and self.dialer_win.id == id:
+            self.dialer_win = None
+        if self.contacts_win and self.contacts_win.id == id:
+            self.contacts_win = None
+
+import contactdb
+
+contacts = None
+
+def friendly_time(tm, secs):
+    now = time.time()
+    if secs > now or secs < now - 7*24*3600:
+        return time.strftime("%Y-%m-%d %H:%M:%S", tm)
+    age = now - secs
+    if age < 12*3600:
+        return time.strftime("%H:%M:%S", tm)
+    return time.strftime("%a %H:%M:%S", tm)
+
+class corresp:
+    # This task represents a correspondant in the
+    # lists of dialled numbers and  received calls
+    # It displays the contact name/number and time/date
+    # in a friendly form
+    # If there is a number we provide a button to call-back
+    # and one to find contact
+    def __init__(self, name, owner, when):
+        global contacts
+        self.format="brown"
+        self.owner = owner
+        self.number = name
+        self.embedded = lambda:None
+        dt, tm = when
+        self.when = time.strptime(dt+'='+tm, '%Y-%m-%d=%H:%M:%S')
+        self.secs = time.mktime(self.when)
+        
+        if contacts == None:
+            contacts = contactdb.contacts()
+        if name == None:
+            self.contact = "-private-number-"
+        else:
+            n = contacts.find_num(name)
+            if n:
+                self.contact = n.name
+            else:
+                self.contact = name
+
+    def get_name(self):
+        w = friendly_time(self.when, self.secs)
+        return '<span size="xx-small">%s</span>\n<span size="xx-small">%s</span>' % (
+            self.contact, w)
+
+    def buttons(self):
+        if self.number:
+            return ['Call','Txt','Contacts']
+        return []
+
+    def press(self, ind):
+        if ind == 0:
+            record('call',self.number)
+            send_number('voice-dial', self.number)
+            self.owner.emit('request-window','Dialer')
+        if ind == 1:
+            send_number('sms-new', self.number)
+            self.owner.emit('request-window','SendSMS')
+        if ind == 2:
+            send_number('contact-find', self.number)
+            self.owner.emit('request-window','Contacts')
+
+sel_number = None
+clips = {}
+def clip_get_data(clip, sel, info, data):
+    global sel_number
+    sel.set_text(sel_number)
+def clip_clear_data(clip, data):
+    global sel_number
+    sel_number = None
+    c.set_with_data([(gtk.gdk.SELECTION_TYPE_STRING, 0, 0)],
+                    clip_get_data, clip_clear_data, None)
+
+def send_number(sel, num):
+    global sel_number, clips
+    sel_number = num
+    if sel not in clips:
+        clips[sel] = gtk.Clipboard(selection = sel)
+    c = clips[sel]
+    c.set_with_data([(gtk.gdk.SELECTION_TYPE_STRING, 0, 0)],
+                    clip_get_data, clip_clear_data, None)
+
+
+
+class call_list:
+    # A list of 'corresp' tasks taken from /var/log/incoming or 
+    # /var/log/outgoing
+    def __init__(self, win, name, incoming = None):
+        self.win = win
+        if incoming == None:
+            incoming = name == 'incoming'
+
+        if incoming:
+            self.list = incoming_list(self.update)
+            self.name = "Incoming Calls"
+        else:
+            self.list = outgoing_list(self.update)
+            self.name = "Outgoing Calls"
+        self.format = 'group'
+
+    def get_task(self, ind):
+        if ind >= len(self.list):
+            return None
+        start, end, num = self.list[ind]
+        return corresp(num, self, start)
+
+    def update(self):
+        self.win.queue_draw()
+    def emit(self, *a):
+        self.win.emit(*a)
+
+
+import dnotify
+class incoming_list:
+    # present as a list of received calls
+    # and notify whenever it changes
+    # Each entry is a triple (start, end, number)
+    # start and end are (date, time)
+    def __init__(self, notify, file='incoming'):
+        self.list = []
+        self.notify = notify
+        self.watch = dnotify.dir("/var/log")
+        self.fwatch = self.watch.watch(file, self.changed)
+        self.changed()
+    def __len__(self):
+        return len(self.list)
+    def __getitem__(self, ind):
+        return self.list[-1-ind]
+    def flush_one(self, start, end, number):
+        if start:
+            self.lst.append((start, end, number))
+    def changed(self):
+        self.lst = []
+
+        try:
+            f = open("/var/log/incoming")
+        except IOError:
+            f = []
+        start = None; end=None; number=None
+        for l in f:
+            w = l.split()
+            if len(w) != 3:
+                continue
+            if w[2] == '-call-':
+                self.flush_one(start, end, number)
+                start = (w[0], w[1])
+                number = None; end = None
+            elif w[2] == '-end-':
+                end = (w[0], w[1])
+                self.flush_one(start, end, number)
+                start = None; end = None; number = None
+            else:
+                number = w[2]
+                if not start:
+                    start = (w[0], w[1])
+        if number:
+            self.flush_one(start, end, number)
+        try:
+            f.close()
+        except AttributeError:
+            pass
+        self.list = self.lst
+        self.notify()
+
+class outgoing_list(incoming_list):
+    # present similar list for outgoing calls
+    def __init__(self, notify):
+        incoming_list.__init__(self, notify, file='outgoing')
+
+    def changed(self):
+        self.lst = []
+        try:
+            f = open("/var/log/outgoing")
+        except IOError:
+            f = []
+        start = None; end=None; number=None
+        for l in f:
+            w = l.split()
+            if len(w) != 3:
+                continue
+            if w[2] == '-end-':
+                end = (w[0], w[1])
+                self.flush_one(start, end, number)
+                start = None; end = None; number = None
+            else:
+                self.flush_one(start, end, number)
+                start = (w[0], w[1])
+                number = w[2]
+        if number:
+            self.flush_one(start, end, number)
+        try:
+            f.close()
+        except AttributeError:
+            pass
+        self.list = self.lst
+        self.notify()
+
+
diff --git a/plato/plato_internal.py b/plato/plato_internal.py
new file mode 100644 (file)
index 0000000..2e50cef
--- /dev/null
@@ -0,0 +1,176 @@
+#
+# internal commands for 'plato'
+# some of these affect plato directly, some are just
+# ad hoc simple things.
+
+import time as _time
+import gobject, gtk
+import dnotify, os
+
+class date:
+    def __init__(self, name, owner):
+        self.buttons = lambda:['Calendar']
+        self.embedded = lambda:None
+        self.format = 'cmd'
+        self.timeout = None
+        self.owner = owner
+
+    def press(self, ind):
+        if ind != 0:
+            return
+        self.gowner.emit('request-window','cal')
+
+
+    def get_name(self):
+        now = _time.time() 
+        if self.timeout == None:
+            next_hour = int(now/60/60)+1
+            self.timeout = gobject.timeout_add(
+                int (((next_hour*3600) - now) * 1000),
+                self.do_change)
+        return _time.strftime('%d-%b-%Y',
+                              _time.localtime(now))
+
+    def do_change(self):
+        self.timeout = None
+        if self.owner:
+            self.owner.queue_draw()
+
+
+class time:
+    def __init__(self, name, owner):
+        self.buttons = lambda:None
+        self.embedded = lambda:None
+        self.format = 'cmd'
+        self.timeout = None
+        self.owner = owner
+
+    def get_name(self):
+        now = _time.time() 
+        if self.timeout == None:
+            next_min = int(now/60)+1
+            self.timeout = gobject.timeout_add(
+                int (((next_min*60) - now) * 1000),
+                self.do_change)
+        return _time.strftime('%H:%M',
+                              _time.localtime(_time.time()))
+
+    def do_change(self):
+        self.timeout = None
+        if self.owner:
+            self.owner.queue_draw()
+
+zonelist = None
+def get_zone(ind):
+    global zonelist
+    if zonelist == None:
+        try:
+            f = open("/data/timezone_list")
+            l = f.readlines()
+        except IOError:
+            l = []
+        zonelist = map(lambda x:x.strip(), l)
+    if ind < 0 or ind >= len(zonelist):
+        return "UTC"
+    return zonelist[ind]
+
+class tz:
+    # Arg is either a timezone name or a number
+    # to index into a file containing a list of
+    # timezone names  '0' is first line
+    def __init__(self, name, owner, zone):
+        try:
+            ind = int(zone)
+            self.zone = get_zone(ind)
+        except ValueError:
+            self.zone = zone
+        self.buttons = lambda:None
+        self.embedded = lambda:None
+        self.format = 'darkgreen'
+        self.owner = owner
+
+    def get_name(self):
+        if 'TZ' in os.environ:
+            TZ = os.environ['TZ']
+        else:
+            TZ = None
+        os.environ['TZ'] = self.zone
+        _time.tzset()
+        now = _time.time()
+        tm = _time.strftime("%d-%b-%Y %H:%M", _time.localtime(now))
+
+        if TZ:
+            os.environ['TZ'] = TZ
+        else:
+            del(os.environ['TZ'])
+        _time.tzset()
+        return '<span size="10000">'+tm+"\n"+self.zone+'</span>'
+
+
+class file:
+    def __init__(self, name, owner, path):
+        self.buttons = lambda:None
+        self.embedded = lambda:None
+        self.path = path
+        self.format = 'cmd'
+        self.owner = owner
+        self.watch = None
+        self.fwatch = None
+
+    def get_name(self):
+        try:
+            f = open(self.path)
+            l = f.readline().strip()
+            f.close()
+        except IOError:
+            l = "-"
+        self.set_watch()
+
+        return l
+
+    def set_watch(self):
+        if self.watch == None:
+            try:
+                self.watch = dnotify.dir(os.path.dirname(self.path))
+            except OSError:
+                self.watch = None
+        if self.watch == None:
+            return
+        if self.fwatch == None:
+            self.fwatch = self.watch.watch(os.path.basename(self.path),
+                                           self.file_changed)
+
+    def file_changed(self, thing):
+        if self.fwatch:
+            self.fwatch.cancel()
+            self.fwatch = None
+        if self.owner:
+            self.owner.queue_draw()
+
+class quit:
+    def __init__(self, name, owner):
+        self.buttons = lambda:['Exit']
+        self.embedded = lambda:None
+        self.name = name
+        self.format = 'cmd'
+
+    def press(self, ind):
+        gtk.main_quit()
+
+class zoom:
+    def __init__(self, name, owner):
+        self.buttons = lambda:['group+','group-','task+','task-']
+        self.owner = owner
+        self.name = name
+        self.format = 'cmd'
+        self.embedded = lambda:None
+    def press(self, ind):
+        w = self.owner.win
+        if ind == 0:
+            w.grouplist.set_zoom(w.grouplist.zoom+1)
+        if ind == 1:
+            w.grouplist.set_zoom(w.grouplist.zoom-1)
+        if ind == 2:
+            w.tasklist.set_zoom(w.tasklist.zoom+1)
+        if ind == 3:
+            w.tasklist.set_zoom(w.tasklist.zoom-1)
diff --git a/plato/plato_settings.py b/plato/plato_settings.py
new file mode 100644 (file)
index 0000000..4a1e819
--- /dev/null
@@ -0,0 +1,34 @@
+import os, stat
+
+class alert:
+    def __init__(self, name, owner):
+        blist = []
+        for i in os.listdir("/etc/alert"):
+            if stat.S_ISDIR(os.lstat("/etc/alert/"+i)[0]):
+                blist.append(i)
+        self.blist = blist
+
+        self.embedded = lambda:None
+        self.format = 'cmd'
+        self.timeout = None
+        self.owner = owner
+        try:
+            self.mode = os.readlink("/etc/alert/normal")
+        except:
+            self.mode = '??'
+
+    def buttons(self):
+        return self.blist
+
+    def press(self, ind):
+        if ind < 0 or ind >= len(self.blist):
+            return
+        o = self.blist[ind]
+        os.unlink("/etc/alert/normal")
+        os.symlink(o, "/etc/alert/normal")
+        self.mode = o
+        self.owner.queue_draw()
+
+    def get_name(self):
+        return 'mode: ' + self.mode
+
diff --git a/plato/plato_sms.py b/plato/plato_sms.py
new file mode 100644 (file)
index 0000000..05ce4ba
--- /dev/null
@@ -0,0 +1,89 @@
+#
+# plato plugin for text messages
+#
+# 
+
+import gtk
+from plato_internal import file
+from fingerscroll import FingerScroll
+from storesms import SMSstore
+
+import contactdb
+contacts = None
+
+def protect(txt):
+    txt = txt.replace('&', '&amp;')
+    txt = txt.replace('<', '&lt;')
+    txt = txt.replace('>', '&gt;')
+    return txt
+
+class newmsg(file):
+    # display either (SMS) or recipient of last message
+    # button is "open" to load SendSMS
+    # embedded is most recent text message
+    def __init__(self, name, owner):
+        file.__init__(self, name, owner, "/data/SMS/newmesg")
+        self.owner = self
+        self.gowner = owner
+        self.get_name = self.mgetname
+        self.buttons = self.mbuttons
+        self.embedded = self.membedded
+        self.messages = 0
+        self.who_from = None
+        self.buffer = FingerScroll(gtk.WRAP_WORD_CHAR)
+        self.buffer.show()
+        self.buff = self.buffer.get_buffer()
+        self.store = SMSstore("/data/SMS")
+        self.last_txt = 0
+        global contacts
+        if contacts == None:
+            contacts = contactdb.contacts()
+        self.queue_draw()
+
+    def mgetname(self):
+        if self.messages == 0:
+            return '(SMS)'
+        if self.messages == 1:
+            return self.who_from
+        return '%s (+%d)' % (self.who_from, self.messages-1)
+
+    def mbuttons(self):
+        return ['Open']
+
+    def membedded(self):
+        if self.messages == 0:
+            return None
+        return self.buffer
+    def embed_full(self):
+        return False
+
+    def press(self, ind):
+        self.gowner.emit('request-window','SendSMS')
+
+    def set_display(self, mesg):
+        self.buff.delete(self.buff.get_start_iter(),
+                         self.buff.get_end_iter())
+        self.buff.insert(self.buff.get_end_iter(), mesg)
+
+    def queue_draw(self):
+        # newmesg has changed
+        # need to get new messages and set
+        # embedded, message, and from
+        #
+        (next, l) = self.store.lookup(None, 'NEW')
+        self.messages = len(l)
+        if len(l) >= 1:
+            self.set_display(l[0].text)
+            self.who_from = l[0].correspondent
+            global contacts
+            if self.who_from:
+                c = contacts.find_num(self.who_from)
+                if c:
+                    self.who_from = c.name
+            
+            if l[0].stamp > self.last_txt:
+                self.last_txt = l[0].stamp
+                self.gowner.update(self, True)
+        self.gowner.update(self, False)
+        self.gowner.queue_draw()
+        file.set_watch(self)
diff --git a/plato/window_group.py b/plato/window_group.py
new file mode 100644 (file)
index 0000000..fb55295
--- /dev/null
@@ -0,0 +1,66 @@
+#
+# A plato 'group' of windows.
+# This needs to be present for any window management
+# to work. i.e. a WinTask won't see the window unless this
+# was been activated.
+
+from wmctrl import *
+import gobject
+
+class Wwrap:
+    # Wrap a window from wmctrl as a Task
+    def __init__(self, w):
+        self.w = w
+        self.name = w.name
+        self.format = "blue"
+        self.embedded = lambda:None
+
+    def buttons(self):
+        if self.w.pid > 0:
+            return ['Raise', 'Close', 'Kill']
+        return ['Raise','Close']
+
+    def press(self, ind):
+        if ind == 0:
+            self.w.raise_win()
+            return
+        if ind == 1:
+            self.w.close_win()
+            return
+        if ind == 2:
+            if self.w.pid > 1:
+                os.kill(self.w.pid, 15)
+            return
+
+class WindowType:
+    def __init__(self, win, name):
+        self.owner = win
+        self.format = 'group'
+        self.name = name
+        self.buttons = lambda:None
+        self.embedded = lambda:None
+        self.ignore = []
+        self.list = winlist(add_handle = self.add)
+        self.current = {}
+        gobject.io_add_watch(self.list.fd, gobject.IO_IN, self.list.events)
+        self.list.on_change(self.change, self.add, self.delete)
+
+    def parse(self, line):
+        # any window names listed in config file are ignored
+        self.ignore.append(line)
+
+    def get_task(self, ind):
+        w = self.list.winfo
+        if ind >= len(w):
+            return None
+        return Wwrap(w[self.list.windows[ind]])
+
+    def change(self):
+        self.owner.queue_draw()
+
+    def add(self, window):
+        print "emit new window", window.name
+        self.owner.emit('new-window',window)
+
+    def delete(self, wid):
+        self.owner.emit('lost-window',wid)
diff --git a/scribble/Sample-Pages/1 b/scribble/Sample-Pages/1
new file mode 100755 (executable)
index 0000000..aea83a1
--- /dev/null
@@ -0,0 +1,19 @@
+"black":64,81:"Welcome to"
+"black":72,159:62,162:45,170:34,178:36,192:48,201:63,207:79,214:92,226:97,245:95,263:83,278:65,287:51,289:50,278
+"black":146,200:136,204:122,212:120,241:131,246:144,249:159,243:169,235
+"black":177,206:188,221:193,231:186,211:186,199:199,187:209,183:219,182:234,181:238,191
+"black":255,186:261,196:270,213
+"black":263,111:270,142:276,170:280,186:284,199:286,209:292,224:292,213:293,203:294,192:299,178:317,187:317,201:307,210:292,202
+"black":321,103:321,119:327,156:330,176:332,193:333,207:337,194:345,175:357,171:365,182:360,197:333,194
+"black":371,96:371,108:374,124:376,138:380,154:382,171:387,195
+"black":416,182:421,171:433,161:429,151:417,155:407,168:405,183:410,193:422,196:454,183
+"red":433,211:423,212:405,214:393,215:380,218:367,221:355,225:344,230:332,233:319,237:307,241:295,245:283,250:258,258:235,264:224,267:214,270:201,273:189,276:177,279:153,285:142,289:129,293:117,296:106,300:96,303:79,310:63,316
+"black":92,384:91,396:92,417:91,429:89,417:88,400:90,385:94,371:102,360:113,357:123,359:131,371:129,383:123,394:112,402:100,403
+"black":133,403:139,414:137,401:148,390:162,390:173,395
+"black":174,411:189,407:200,404:208,394:198,388:186,391:178,403:182,418:195,426:208,426:221,422
+"black":253,398:240,394:227,396:232,406:243,410:225,421
+"black":288,405:264,404:275,413:286,418:276,428:259,431:249,431
+"red":359,349:370,359:381,370:391,377:404,391:399,401:389,407:379,411:366,418:356,425:346,431
+"red":94,506:"to continue"
+"red":65,48:71,36:70,23:69,12:59,23
+"red":68,7:76,17:85,29
diff --git a/scribble/Sample-Pages/2 b/scribble/Sample-Pages/2
new file mode 100755 (executable)
index 0000000..bc60b71
--- /dev/null
@@ -0,0 +1,23 @@
+"black":31,42:"Here is a page number"
+"black":350,36:361,38:372,40:383,36:395,35:405,33:416,29:426,25:441,18:450,7:440,8:451,7:454,17:453,27
+"black":33,103:"Use"
+"black":202,82:192,80:180,90:168,101:158,106:171,118:189,129:199,135
+"black":241,113:"To go back"
+"black":92,161:106,168:116,174:128,183:140,193:134,204:124,214:113,225:103,234:93,242
+"black":176,198:"for next"
+"black":58,259:54,274:58,291:65,303:79,308:91,303:101,264:101,254
+"black":112,276:117,289:120,300:124,289:138,284:149,295
+"black":192,283:182,282:172,285:170,296:180,298:190,290:192,279:191,265:186,246:183,234:186,251:189,262:190,273:192,284:194,294:197,304
+"black":217,291:226,280:236,278:247,287:245,299:226,305:213,294:215,284:225,280:241,280
+"black":58,349:67,359:71,371:70,360:71,350:77,338:90,335:102,337:112,342
+"black":123,352:134,355:146,355:156,354:163,343:151,338:135,340:129,356:141,371:167,375:184,372
+"black":224,336:222,346:208,350:201,364:208,376:218,372:225,357:225,337:222,321:224,342:227,353:230,363:235,373
+"black":261,361:262,349:274,343:285,348:288,359:272,374:258,369:258,359:264,349
+"black":65,406:68,395:69,409:71,425:73,436
+"black":86,412:76,414:58,416:46,416
+"black":110,436:"add page"
+"black":96,499:84,499:73,500:63,500:53,502
+"black":121,510:"remove page"
+"black":365,503:375,500:378,510:368,514:360,504:371,503
+"black":402,510:402,500:408,510:397,515:390,504:407,499
+"black":429,507:440,504:444,516:430,515:422,503:435,502
diff --git a/scribble/Sample-Pages/3 b/scribble/Sample-Pages/3
new file mode 100755 (executable)
index 0000000..e3ddef8
--- /dev/null
@@ -0,0 +1,18 @@
+"black":117,45:122,57:108,51:96,48:82,53:70,62:57,74:47,86:41,100:40,117:41,128:48,146:56,156:66,165:78,171:110,177:133,169
+"black":158,119:"Clear page"
+"black":195,159:"Before removal"
+"black":83,235:84,224:80,250:79,270:78,289:78,319:79,330
+"black":165,229:149,223:128,220:105,218:60,213:46,213:36,213
+"black":120,308:130,303:143,298:156,306:154,320:139,329:129,326:127,308
+"black":186,297:175,305:168,315:178,318:189,315:199,304:201,321:203,332:169,367:162,357:165,344
+"black":241,304:229,298:219,306:216,319:230,325:241,320:251,310:251,299:252,310:257,340:256,362:250,376:236,379:224,372:221,361
+"black":271,227:271,240:274,255:275,271:277,291:278,308:279,318
+"black":299,322:309,320:328,305:318,300:303,313:333,335:361,322
+"black":219,392:222,409:224,426:226,437
+"black":273,405:261,398:217,395:204,394:193,393:179,394
+"black":249,426:261,424:271,424:268,413:252,418:247,431:263,439:285,441:297,441
+"black":295,424:311,427:322,434
+"black":324,420:312,441:302,455
+"black":354,373:349,388:350,400:351,414:353,427:354,444
+"black":380,414:366,405:355,401:343,400:332,398:322,397
+"black":66,492:"Tap to enable"
diff --git a/scribble/scribble.desktop b/scribble/scribble.desktop
new file mode 100644 (file)
index 0000000..94385cc
--- /dev/null
@@ -0,0 +1,12 @@
+[Desktop Entry]
+Name=Scribble pad
+Comment=Note pad for scibbles and note taking
+Encoding=UTF-8
+Version=1.0
+Type=Application
+Exec=scribble.py
+Icon=scribble
+Terminal=false
+Categories=GTK;Application;PIM;Office
+SingleInstance=true
+StartupNotify=true
diff --git a/scribble/scribble.png b/scribble/scribble.png
new file mode 100644 (file)
index 0000000..a5b9cbc
Binary files /dev/null and b/scribble/scribble.png differ
diff --git a/scribble/scribble.py b/scribble/scribble.py
new file mode 100755 (executable)
index 0000000..ecf1f9f
--- /dev/null
@@ -0,0 +1,1493 @@
+#!/usr/bin/env python
+
+
+# scribble - scribble pad designed for Neo Freerunner
+#
+# Copyright (C) 2008 Neil Brown <neil@brown.name>
+#
+#
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    The GNU General Public License, version 2, is available at
+#    http://www.fsf.org/licensing/licenses/info/GPLv2.html
+#    Or you can write to the Free Software Foundation, Inc., 51
+#    Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+#    Author: Neil Brown
+#    Email: <neil@brown.name>
+
+
+# TODO
+# - index page
+#     list of pages
+#     Buttons: select, delete, new
+# - bigger buttons - fewer
+#   Need:  undo, redo, colour, text/line
+#   When text is selected, these change to:
+#        align, join, make-title
+
+import pygtk
+import gtk
+import os
+import pango
+import gobject
+import time
+import listselect
+
+###########################################################
+# Writing recognistion code
+import math
+
+
+def LoadDict(dict):
+    # Upper case.
+    # Where they are like lowercase, we either double
+    # the last stroke (L, J, I) or draw backwards (S, Z, X)
+    # U V are a special case
+
+    dict.add('A', "R(4)6,8")
+    dict.add('B', "R(4)6,4.R(7)1,6")
+    dict.add('B', "R(4)6,4.L(4)2,8.R(7)1,6")
+    dict.add('B', "S(6)7,1.R(4)6,4.R(7)0,6")
+    dict.add('C', "R(4)8,2")
+    dict.add('D', "R(4)6,6")
+    dict.add('E', "L(1)2,8.L(7)2,8")
+    # double the stem for F
+    dict.add('F', "L(4)2,6.S(3)7,1")
+    dict.add('F', "S(1)5,3.S(3)1,7.S(3)7,1")
+
+    dict.add('G', "L(4)2,5.S(8)1,7")
+    dict.add('G', "L(4)2,5.R(8)6,8")
+    # FIXME I need better straight-curve alignment
+    dict.add('H', "S(3)1,7.R(7)6,8.S(5)7,1")
+    dict.add('H', "L(3)0,5.R(7)6,8.S(5)7,1")
+    # capital I is down/up
+    dict.add('I', "S(4)1,7.S(4)7,1")
+
+    # Capital J has a left/right tail
+    dict.add('J', "R(4)1,6.S(7)3,5")
+
+    dict.add('K', "L(4)0,2.R(4)6,6.L(4)2,8")
+
+    # Capital L, like J, doubles the foot
+    dict.add('L', "L(4)0,8.S(7)4,3")
+
+    dict.add('M', "R(3)6,5.R(5)3,8")
+    dict.add('M', "R(3)6,5.L(1)0,2.R(5)3,8")
+
+    dict.add('N', "R(3)6,8.L(5)0,2")
+
+    # Capital O is CW, but can be CCW in special dict
+    dict.add('O', "R(4)1,1", bot='0')
+
+    dict.add('P', "R(4)6,3")
+    dict.add('Q', "R(4)7,7.S(8)0,8")
+
+    dict.add('R', "R(4)6,4.S(8)0,8")
+
+    # S is drawn bottom to top.
+    dict.add('S', "L(7)6,1.R(1)7,2")
+
+    # Double the stem for capital T
+    dict.add('T', "R(4)0,8.S(5)7,1")
+
+    # U is L to R, V is R to L for now
+    dict.add('U', "L(4)0,2")
+    dict.add('V', "R(4)2,0")
+
+    dict.add('W', "R(5)2,3.L(7)8,6.R(3)5,0")
+    dict.add('W', "R(5)2,3.R(3)5,0")
+
+    dict.add('X', "R(4)6,0")
+
+    dict.add('Y',"L(1)0,2.R(5)4,6.S(5)6,2")
+    dict.add('Y',"L(1)0,2.S(5)2,7.S(5)7,2")
+
+    dict.add('Z', "R(4)8,2.L(4)6,0")
+
+    # Lower case
+    dict.add('a', "L(4)2,2.L(5)1,7")
+    dict.add('a', "L(4)2,2.L(5)0,8")
+    dict.add('a', "L(4)2,2.S(5)0,8")
+    dict.add('b', "S(3)1,7.R(7)6,3")
+    dict.add('c', "L(4)2,8", top='C')
+    dict.add('d', "L(4)5,2.S(5)1,7")
+    dict.add('d', "L(4)5,2.L(5)0,8")
+    dict.add('e', "S(4)3,5.L(4)5,8")
+    dict.add('e', "L(4)3,8")
+    dict.add('f', "L(4)2,6", top='F')
+    dict.add('f', "S(1)5,3.S(3)1,7", top='F')
+    dict.add('g', "L(1)2,2.R(4)1,6")
+    dict.add('h', "S(3)1,7.R(7)6,8")
+    dict.add('h', "L(3)0,5.R(7)6,8")
+    dict.add('i', "S(4)1,7", top='I', bot='1')
+    dict.add('j', "R(4)1,6", top='J')
+    dict.add('k', "L(3)0,5.L(7)2,8")
+    dict.add('k', "L(4)0,5.R(7)6,6.L(7)1,8")
+    dict.add('l', "L(4)0,8", top='L')
+    dict.add('l', "S(4)0,8", top='L')
+    dict.add('l', "S(3)1,7.S(7)3,5", top='L')
+    dict.add('m', "S(3)1,7.R(3)6,8.R(5)6,8")
+    dict.add('m', "L(3)0,2.R(3)6,8.R(5)6,8")
+    dict.add('n', "S(3)1,7.R(4)6,8")
+    dict.add('o', "L(4)1,1", top='O', bot='0')
+    dict.add('p', "S(3)1,7.R(4)6,3")
+    dict.add('q', "L(1)2,2.L(5)1,5")
+    dict.add('q', "L(1)2,2.S(5)1,7.R(8)6,2")
+    dict.add('q', "L(1)2,2.S(5)1,7.S(5)1,7")
+    # FIXME this double 1,7 is due to a gentle where the
+    # second looks like a line because it is narrow.??
+    dict.add('r', "S(3)1,7.R(4)6,2")
+    dict.add('s', "L(1)2,7.R(7)1,6", top='S', bot='5')
+    dict.add('s', "L(5)1,8.R(7)2,3", top='S', bot='5')
+    dict.add('t', "R(4)0,8", top='T', bot='7')
+    dict.add('t', "S(1)3,5.S(5)1,7", top='T', bot='7')
+    dict.add('u', "L(4)0,2.S(5)1,7")
+    dict.add('v', "L(4)0,2.L(2)0,2")
+    dict.add('w', "L(3)0,2.L(5)0,2", top='W')
+    dict.add('w', "L(3)0,5.R(7)6,8.L(5)3,2", top='W')
+    dict.add('w', "L(3)0,5.L(5)3,2", top='W')
+    dict.add('x', "L(4)0,6", top='X')
+    dict.add('y', "L(1)0,2.R(5)4,6", top='Y') # if curved
+    dict.add('y', "L(1)0,2.S(5)2,7", top='Y')
+    dict.add('z', "R(4)0,6.L(4)2,8", top='Z', bot='2')
+
+    # Digits
+    dict.add('0', "L(4)7,7")
+    dict.add('0', "R(4)7,7")
+    dict.add('1', "S(4)7,1")
+    dict.add('2', "R(4)0,6.S(7)3,5")
+    dict.add('2', "R(4)3,6.L(4)2,8")
+    dict.add('3', "R(1)0,6.R(7)1,6")
+    dict.add('4', "L(4)7,5")
+    dict.add('5', "L(1)2,6.R(7)0,3")
+    dict.add('5', "L(1)2,6.L(4)0,8.R(7)0,3")
+    dict.add('6', "L(4)2,3")
+    dict.add('7', "S(1)3,5.R(4)1,6")
+    dict.add('7', "R(4)0,6")
+    dict.add('7', "R(4)0,7")
+    dict.add('8', "L(4)2,8.R(4)4,2.L(3)6,1")
+    dict.add('8', "L(1)2,8.R(7)2,0.L(1)6,1")
+    dict.add('8', "L(0)2,6.R(7)0,1.L(2)6,0")
+    dict.add('8', "R(4)2,6.L(4)4,2.R(5)8,1")
+    dict.add('9', "L(1)2,2.S(5)1,7")
+
+    dict.add(' ', "S(4)3,5")
+    dict.add('<BS>', "S(4)5,3")
+    dict.add('-', "S(4)3,5.S(4)5,3")
+    dict.add('_', "S(4)3,5.S(4)5,3.S(4)3,5")
+    dict.add("<left>", "S(4)5,3.S(3)3,5")
+    dict.add("<right>","S(4)3,5.S(5)5,3")
+    dict.add("<newline>", "S(4)2,6")
+
+
+class DictSegment:
+    # Each segment has four elements:
+    #   direction: Right Straight Left (R=cw, L=ccw)
+    #   location: 0-8.
+    #   start: 0-8
+    #   finish: 0-8
+    # Segments match if the difference at each element
+    # is 0, 1, or 3 (RSL coded as 012)
+    # A difference of 1 required both to be same / 3
+    # On a match, return number of 0s
+    # On non-match, return -1
+    def __init__(self, str):
+        # D(L)S,R
+        # 0123456
+        self.e = [0,0,0,0]
+        if len(str) != 7:
+            raise ValueError
+        if str[1] != '(' or str[3] != ')' or str[5] != ',':
+            raise ValueError
+        if str[0] == 'R':
+            self.e[0] = 0
+        elif str[0] == 'L':
+            self.e[0] = 2
+        elif str[0] == 'S':
+            self.e[0] = 1
+        else:
+            raise ValueError
+
+        self.e[1] = int(str[2])
+        self.e[2] = int(str[4])
+        self.e[3] = int(str[6])
+
+    def match(self, other):
+        cnt = 0
+        for i in range(0,4):
+            diff = abs(self.e[i] - other.e[i])
+            if diff == 0:
+                cnt += 1
+            elif diff == 3:
+                pass
+            elif diff == 1 and (self.e[i]/3 == other.e[i]/3):
+                pass
+            else:
+                return -1
+        return cnt
+
+class DictPattern:
+    # A Dict Pattern is a list of segments.
+    # A parsed pattern matches a dict pattern if
+    # the are the same nubmer of segments and they
+    # all match.  The value of the match is the sum
+    # of the individual matches.
+    # A DictPattern is printers as segments joined by periods.
+    #
+    def __init__(self, str):
+        self.segs = map(DictSegment, str.split("."))
+    def match(self,other):
+        if len(self.segs) != len(other.segs):
+            return -1
+        cnt = 0
+        for i in range(0,len(self.segs)):
+            m = self.segs[i].match(other.segs[i])
+            if m < 0:
+                return m
+            cnt += m
+        return cnt
+
+
+class Dictionary:
+    # The dictionary hold all the pattern for symbols and
+    # performs lookup
+    # Each pattern in the directionary can be associated
+    # with  3 symbols.  One when drawing in middle of screen,
+    # one for top of screen, one for bottom.
+    # Often these will all be the same.
+    # This allows e.g. s and S to have the same pattern in different
+    # location on the touchscreen.
+    # A match requires a unique entry with a match that is better
+    # than any other entry.
+    #
+    def __init__(self):
+        self.dict = []
+    def add(self, sym, pat, top = None, bot = None):
+        if top == None: top = sym
+        if bot == None: bot = sym
+        self.dict.append((DictPattern(pat), sym, top, bot))
+
+    def _match(self, p):
+        max = -1
+        val = None
+        for (ptn, sym, top, bot) in self.dict:
+            cnt = ptn.match(p)
+            if cnt > max:
+                max = cnt
+                val = (sym, top, bot)
+            elif cnt == max:
+                val = None
+        return val
+
+    def match(self, str, pos = "mid"):
+        p = DictPattern(str)
+        m = self._match(p)
+        if m == None:
+            return m
+        (mid, top, bot) = self._match(p)
+        if pos == "top": return top
+        if pos == "bot": return bot
+        return mid
+
+
+class Point:
+    # This represents a point in the path and all the points leading
+    # up to it.  It allows us to find the direction and curvature from
+    # one point to another
+    # We store x,y, and sum/cnt of points so far
+    def __init__(self,x,y) :
+        self.xsum = x
+        self.ysum = y
+        self.x = x
+        self.y = y
+        self.cnt = 1
+
+    def copy(self):
+        n = Point(0,0)
+        n.xsum = self.xsum
+        n.ysum = self.ysum
+        n.x = self.x
+        n.y = self.y
+        n.cnt = self.cnt
+        return n
+
+    def add(self,x,y):
+        if self.x == x and self.y == y:
+            return
+        self.x = x
+        self.y = y
+        self.xsum += x
+        self.ysum += y
+        self.cnt += 1
+
+    def xlen(self,p):
+        return abs(self.x - p.x)
+    def ylen(self,p):
+        return abs(self.y - p.y)
+    def sqlen(self,p):
+        x = self.x - p.x
+        y = self.y - p.y
+        return x*x + y*y
+
+    def xdir(self,p):
+        if self.x > p.x:
+            return 1
+        if self.x < p.x:
+            return -1
+        return 0
+    def ydir(self,p):
+        if self.y > p.y:
+            return 1
+        if self.y < p.y:
+            return -1
+        return 0
+    def curve(self,p):
+        if self.cnt == p.cnt:
+            return 0
+        x1 = p.x ; y1 = p.y
+        (x2,y2) = self.meanpoint(p)
+        x3 = self.x; y3 = self.y
+
+        curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
+        curve = curve * 100 / ((y3-y1)*(y3-y1)
+                               + (x3-x1)*(x3-x1))
+        if curve > 6:
+            return 1
+        if curve < -6:
+            return -1
+        return 0
+
+    def Vcurve(self,p):
+        if self.cnt == p.cnt:
+            return 0
+        x1 = p.x ; y1 = p.y
+        (x2,y2) = self.meanpoint(p)
+        x3 = self.x; y3 = self.y
+
+        curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
+        curve = curve * 100 / ((y3-y1)*(y3-y1)
+                               + (x3-x1)*(x3-x1))
+        return curve
+
+    def meanpoint(self,p):
+        x = (self.xsum - p.xsum) / (self.cnt - p.cnt)
+        y = (self.ysum - p.ysum) / (self.cnt - p.cnt)
+        return (x,y)
+
+    def is_sharp(self,A,C):
+        # Measure the cosine at self between A and C
+        # as A and C could be curve, we take the mean point on
+        # self.A and self.C as the points to find cosine between
+        (ax,ay) = self.meanpoint(A)
+        (cx,cy) = self.meanpoint(C)
+        a = ax-self.x; b=ay-self.y
+        c = cx-self.x; d=cy-self.y
+        x = a*c + b*d
+        y = a*d - b*c
+        h = math.sqrt(x*x+y*y)
+        if h > 0:
+            cs = x*1000/h
+        else:
+            cs = 0
+        return (cs > 900)
+
+class BBox:
+    # a BBox records min/max x/y of some Points and
+    # can subsequently report row, column, pos of each point
+    # can also locate one bbox in another
+
+    def __init__(self, p):
+        self.minx = p.x
+        self.maxx = p.x
+        self.miny = p.y
+        self.maxy = p.y
+
+    def width(self):
+        return self.maxx - self.minx
+    def height(self):
+        return self.maxy - self.miny
+
+    def add(self, p):
+        if p.x > self.maxx:
+            self.maxx = p.x
+        if p.x < self.minx:
+            self.minx = p.x
+
+        if p.y > self.maxy:
+            self.maxy = p.y
+        if p.y < self.miny:
+            self.miny = p.y
+    def finish(self, div = 3):
+        # if aspect ratio is bad, we adjust max/min accordingly
+        # before setting [xy][12].  We don't change self.min/max
+        # as they are used to place stroke in bigger bbox.
+        # Normally divisions are at 1/3 and 2/3. They can be moved
+        # by setting div e.g. 2 = 1/2 and 1/2
+        (minx,miny,maxx,maxy) = (self.minx,self.miny,self.maxx,self.maxy)
+        if (maxx - minx) * 3 < (maxy - miny) * 2:
+            # too narrow
+            mid = int((maxx + minx)/2)
+            halfwidth = int ((maxy - miny)/3)
+            minx = mid - halfwidth
+            maxx = mid + halfwidth
+        if (maxy - miny) * 3 < (maxx - minx) * 2:
+            # too wide
+            mid = int((maxy + miny)/2)
+            halfheight = int ((maxx - minx)/3)
+            miny = mid - halfheight
+            maxy = mid + halfheight
+
+        div1 = div - 1
+        self.x1 = int((div1*minx + maxx)/div)
+        self.x2 = int((minx + div1*maxx)/div)
+        self.y1 = int((div1*miny + maxy)/div)
+        self.y2 = int((miny + div1*maxy)/div)
+
+    def row(self, p):
+        # 0, 1, 2 - top to bottom
+        if p.y <= self.y1:
+            return 0
+        if p.y < self.y2:
+            return 1
+        return 2
+    def col(self, p):
+        if p.x <= self.x1:
+            return 0
+        if p.x < self.x2:
+            return 1
+        return 2
+    def box(self, p):
+        # 0 to 9
+        return self.row(p) * 3 + self.col(p)
+
+    def relpos(self,b):
+        # b is a box within self.  find location 0-8
+        if b.maxx < self.x2 and b.minx < self.x1:
+            x = 0
+        elif b.minx > self.x1 and b.maxx > self.x2:
+            x = 2
+        else:
+            x = 1
+        if b.maxy < self.y2 and b.miny < self.y1:
+            y = 0
+        elif b.miny > self.y1 and b.maxy > self.y2:
+            y = 2
+        else:
+            y = 1
+        return y*3 + x
+
+
+def different(*args):
+    cur = 0
+    for i in args:
+        if cur != 0 and i != 0 and cur != i:
+            return True
+        if cur == 0:
+            cur = i
+    return False
+
+def maxcurve(*args):
+    for i in args:
+        if i != 0:
+            return i
+    return 0
+
+class PPath:
+    # a PPath refines a list of x,y points into a list of Points
+    # The Points mark out segments which end at significant Points
+    # such as inflections and reversals.
+
+    def __init__(self, x,y):
+
+        self.start = Point(x,y)
+        self.mid = Point(x,y)
+        self.curr = Point(x,y)
+        self.list = [ self.start ]
+
+    def add(self, x, y):
+        self.curr.add(x,y)
+
+        if ( (abs(self.mid.xdir(self.start) - self.curr.xdir(self.mid)) == 2) or
+             (abs(self.mid.ydir(self.start) - self.curr.ydir(self.mid)) == 2) or
+             (abs(self.curr.Vcurve(self.start))+2 < abs(self.mid.Vcurve(self.start)))):
+            pass
+        else:
+            self.mid = self.curr.copy()
+
+        if self.curr.xlen(self.mid) > 4 or self.curr.ylen(self.mid) > 4:
+            self.start = self.mid.copy()
+            self.list.append(self.start)
+            self.mid = self.curr.copy()
+
+    def close(self):
+        self.list.append(self.curr)
+
+    def get_sectlist(self):
+        if len(self.list) <= 2:
+            return [[0,self.list]]
+        l = []
+        A = self.list[0]
+        B = self.list[1]
+        s = [A,B]
+        curcurve = B.curve(A)
+        for C in self.list[2:]:
+            cabc = C.curve(A)
+            cab = B.curve(A)
+            cbc = C.curve(B)
+            if B.is_sharp(A,C) and not different(cabc, cab, cbc, curcurve):
+                # B is too pointy, must break here
+                l.append([curcurve, s])
+                s = [B, C]
+                curcurve = cbc
+            elif not different(cabc, cab, cbc, curcurve):
+                # all happy
+                s.append(C)
+                if curcurve == 0:
+                    curcurve = maxcurve(cab, cbc, cabc)
+            elif not different(cabc, cab, cbc)  :
+                # gentle inflection along AB
+                # was: AB goes in old and new section
+                # now: AB only in old section, but curcurve
+                #      preseved.
+                l.append([curcurve,s])
+                s = [A, B, C]
+                curcurve =maxcurve(cab, cbc, cabc)
+            else:
+                # Change of direction at B
+                l.append([curcurve,s])
+                s = [B, C]
+                curcurve = cbc
+
+            A = B
+            B = C
+        l.append([curcurve,s])
+
+        return l
+
+    def remove_shorts(self, bbox):
+        # in self.list, if a point is close to the previous point,
+        # remove it.
+        if len(self.list) <= 2:
+            return
+        w = bbox.width()/10
+        h = bbox.height()/10
+        n = [self.list[0]]
+        leng = w*h*2*2
+        for p in self.list[1:]:
+            l = p.sqlen(n[-1])
+            if l > leng:
+                n.append(p)
+        self.list = n
+
+    def text(self):
+        # OK, we have a list of points with curvature between.
+        # want to divide this into sections.
+        # for each 3 consectutive points ABC curve of ABC and AB and BC
+        # If all the same, they are all in a section.
+        # If not B starts a new section and the old ends on B or C...
+        BB = BBox(self.list[0])
+        for p in self.list:
+            BB.add(p)
+        BB.finish()
+        self.bbox = BB
+        self.remove_shorts(BB)
+        sectlist = self.get_sectlist()
+        t = ""
+        for c, s in sectlist:
+            if c > 0:
+                dr = "R"  # clockwise is to the Right
+            elif c < 0:
+                dr = "L"  # counterclockwise to the Left
+            else:
+                dr = "S"  # straight
+            bb = BBox(s[0])
+            for p in s:
+                bb.add(p)
+            bb.finish()
+            # If  all points are in some row or column, then
+            # line is S
+            rwdiff = False; cldiff = False
+            rw = bb.row(s[0]); cl=bb.col(s[0])
+            for p in s:
+                if bb.row(p) != rw: rwdiff = True
+                if bb.col(p) != cl: cldiff = True
+            if not rwdiff or not cldiff: dr = "S"
+
+            t1 = dr
+            t1 += "(%d)" % BB.relpos(bb)
+            t1 += "%d,%d" % (bb.box(s[0]), bb.box(s[-1]))
+            t += t1 + '.'
+        return t[:-1]
+
+
+
+def page_cmp(a,b):
+    if a.lower() < b.lower():
+        return -1
+    if a.lower() > b.lower():
+        return 1
+    if a < b:
+        return -1
+    if a > b:
+        return 1
+    return 0
+
+def inc_name(a):
+    l = len(a)
+    while l > 0 and a[l-1] >= '0' and a[l-1] <= '9':
+        l -= 1
+    # a[l:] is the last number
+    if l == len(a):
+        # there is no number
+        return a + ".1"
+    num = 0 + int(a[l:])
+    return a[0:l] + ("%d" % (num+1))
+
+class ScribblePad:
+
+    def __init__(self):
+        window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+        window.connect("destroy", self.close_application)
+        window.set_title("ScribblePad")
+        #window.set_size_request(480,640)
+        self.window = window
+
+        vb = gtk.VBox()
+        vb.show()
+        self.draw_box = vb
+
+        bar = gtk.HBox(True)
+        bar.set_size_request(-1, 60)
+        vb.pack_end(bar, expand=False)
+        bar.show()
+
+        l = gtk.Label('Page Name')
+        vb.pack_start(l, expand=False)
+        l.show()
+        self.name = l
+
+        page = gtk.DrawingArea()
+        page.set_size_request(480,540)
+        vb.add(page)
+        page.show()
+        ctx = page.get_pango_context()
+        fd = ctx.get_font_description()
+        fd.set_absolute_size(25*pango.SCALE)
+        page.modify_font(fd)
+
+        l.modify_font(fd)
+
+        dflt = gtk.widget_get_default_style()
+        fd = dflt.font_desc
+        fd.set_absolute_size(25*pango.SCALE)
+
+        self.pixbuf = None
+        self.width = 0
+        self.height = 0
+        self.need_redraw = True
+        self.zoomx = None; self.zoomy = None
+
+        # Now the widgets:
+
+        done = gtk.Button("Done"); done.show()
+        colbtn = gtk.Button("colour"); colbtn.show()
+        undo = gtk.Button('Undo') ; undo.show()
+        redo = gtk.Button('Redo') ; redo.show()
+
+        done.child.modify_font(fd)
+        colbtn.child.modify_font(fd)
+        undo.child.modify_font(fd)
+        redo.child.modify_font(fd)
+
+        bar.add(done)
+        bar.add(colbtn)
+        bar.add(undo)
+        bar.add(redo)
+
+        colbtn.connect("clicked", self.colour_change)
+        undo.connect("clicked", self.undo)
+        redo.connect("clicked", self.redo)
+        done.connect("clicked", self.done)
+
+        self.col_align = colbtn
+        self.undo_join = undo
+        self.redo_rename = redo
+
+        self.page = page
+        self.colbtn = colbtn
+        self.line = None
+        self.lines = []
+        self.hist = [] # undo history
+        self.selecting = False
+        self.textcurs = 0
+        self.textstr = None
+        self.textpos = None
+
+
+        page.connect("button_press_event", self.press)
+        page.connect("button_release_event", self.release)
+        page.connect("motion_notify_event", self.motion)
+        page.connect("expose-event", self.refresh)
+        page.connect("configure-event", self.reconfigure)
+        page.connect("key_press_event", self.type)
+        page.set_events(gtk.gdk.EXPOSURE_MASK
+                        | gtk.gdk.STRUCTURE_MASK
+                        | gtk.gdk.BUTTON_PRESS_MASK
+                        | gtk.gdk.BUTTON_RELEASE_MASK
+                        | gtk.gdk.KEY_PRESS_MASK
+                        | gtk.gdk.KEY_RELEASE_MASK
+                        | gtk.gdk.POINTER_MOTION_MASK
+                        | gtk.gdk.POINTER_MOTION_HINT_MASK)
+        page.set_property('can-focus', True)
+
+
+        # Now create the index window
+        # A listselect of all the pages, with a row of buttons:
+        #   open delete new undelete
+        ls = listselect.ListSelect(center = False)
+        self.listsel = ls
+
+        ls.connect('selected', self.page_select)
+        ls.set_colour('page','blue')
+        ls.set_zoom(38)
+        ls.show()
+        
+        vb = gtk.VBox()
+        vb.show()
+        self.list_box = vb
+
+        vb.add(ls)
+        bar = gtk.HBox(True); bar.show()
+        bar.set_size_request(-1, 60)
+        b= gtk.Button("Open"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.open_page)
+        b= gtk.Button("Del"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.del_page)
+        b= gtk.Button("New"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.new_page)
+        b= gtk.Button("Undelete"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.undelete_pages)
+
+        vb.pack_end(bar, expand=False)
+
+        window.add(self.draw_box)
+
+        window.set_default_size(480,640)
+
+        window.show()
+
+
+        if 'HOME' in os.environ:
+            home = os.environ['HOME']
+        else:
+            home = ""
+        if home == "" or home == "/":
+            home = "/home/root"
+        self.page_dir = home + '/Pages'
+        self.page_dir = '/data/Pages'
+        self.load_pages()
+
+        colourmap = page.get_colormap()
+        self.colourmap = {}
+        self.colnames = [ 'black', 'red', 'blue', 'green', 'purple', 'pink', 'yellow' ]
+        for col in self.colnames:
+            c = gtk.gdk.color_parse(col)
+            gc = page.window.new_gc()
+            gc.line_width = 2
+            gc.set_foreground(colourmap.alloc_color(c))
+            self.colourmap[col] = gc
+        self.reset_colour = False
+
+        self.colour_textmode = self.colourmap['blue']
+
+        self.colourname = "black"
+        self.colour = self.colourmap['black']
+        self.colbtn.child.set_text('black')
+        self.bg = page.get_style().bg_gc[gtk.STATE_NORMAL]
+
+        self.dict = Dictionary()
+        LoadDict(self.dict)
+        self.textstr = None
+
+
+        ctx = page.get_pango_context()
+        fd = ctx.get_font_description()
+        met = ctx.get_metrics(fd)
+        self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE
+        self.lineascent = met.get_ascent() / pango.SCALE
+
+        self.timeout = None
+
+        window.remove(self.draw_box)
+        window.add(self.list_box)
+
+
+    def close_application(self, widget):
+        self.save_page()
+        gtk.main_quit()
+
+    def load_pages(self):
+        try:
+            os.mkdir(self.page_dir)
+        except:
+            pass
+        self.names = os.listdir(self.page_dir)
+        if len(self.names) == 0:
+            self.names.append("1")
+        self.names.sort(page_cmp)
+        self.pages = {}
+        self.pagenum = 0
+        self.load_page()
+        self.update_list()
+        
+    def update_list(self):
+        l = []
+        for p in self.names:
+            l.append([p,'page'])
+        self.listsel.list = l
+        self.listsel.list_changed()
+        self.listsel.select(self.pagenum)
+        return
+
+    def page_select(self, list, item):
+        if item == None:
+            return
+        self.pagenum = item
+
+    def open_page(self, b):
+        self.load_page()
+        self.window.remove(self.list_box)
+        self.window.add(self.draw_box)
+
+    def done(self, b):
+        self.flush_text()
+        self.save_page()
+        self.window.remove(self.draw_box)
+        self.window.add(self.list_box)
+
+    def del_page(self, b):
+        pass
+
+    def new_page(self, b):
+        newname = self.choose_unique(self.names[self.pagenum])
+        self.names = self.names[0:self.pagenum+1] + [ newname ] + \
+                     self.names[self.pagenum+1:]
+        self.pagenum += 1;
+        self.update_list()
+        self.listsel.select(self.pagenum)
+
+        return
+
+    def undelete_pages(self, b):
+        pass
+        
+
+    def type(self, c, ev):
+        if ev.keyval == 65288:
+            self.add_sym('<BS>')
+        elif ev.string == '\r':
+            self.add_sym('<newline>')
+        else:
+            self.add_sym(ev.string)
+        
+    def press(self, c, ev):
+        # Start a new line
+        if self.timeout:
+            gobject.source_remove(self.timeout)
+            self.timeout = None
+        self.taptime = time.time()
+        self.movetext = None
+        c.grab_focus()
+        self.movetimeout = None
+        self.selecting = False
+        if self.selection:
+            self.selection = None
+            self.need_redraw = True
+            self.name_buttons()
+            self.redraw()
+
+        self.line = [ self.colourname, [int(ev.x), int(ev.y)] ]
+        return
+    def release(self, c, ev):
+        if self.movetimeout:
+            gobject.source_remove(self.movetimeout)
+            self.movetimeout = None
+            
+        if self.movetext:
+            self.movetext = None
+            self.line = None
+            self.need_redraw = True
+            self.redraw()
+            return
+        if self.line == None:
+            return
+
+        if self.selecting:
+            self.selecting = False
+            self.line = None
+            return
+        if self.timeout == None:
+            self.timeout = gobject.timeout_add(20*1000, self.tick)
+            
+        if len(self.line) == 2:
+            # just set a cursor
+            need_redraw = ( self.textstr != None)
+            oldpos = None
+            if not self.textstr:
+                oldpos = self.textpos
+            self.flush_text()
+            if need_redraw:
+                self.redraw()
+            (lineno,index) = self.find_text(self.line[1])
+            
+            if lineno == None:
+                # new text,
+                pos = self.align_text(self.line[1])
+                if oldpos and abs(pos[0]-oldpos[0]) < 40 and \
+                       abs(pos[1] - oldpos[1]) < 40:
+                    # turn of text mode
+                    self.flush_text()
+                    self.line = None
+                    self.need_redraw = True
+                    self.setlabel()
+                    return
+                self.textpos = pos
+                self.textstr = ""
+                self.textcurs = 0
+                # draw the cursor
+                self.draw_text(pos, self.colour, "", 0)
+                self.line = None
+            else:
+                # clicked inside an old text.
+                # shuffle it to the top, open it, edit.
+                ln = self.lines[lineno]
+                self.lines = self.lines[:lineno] + self.lines[lineno+1:]
+                self.textpos = ln[1]
+                self.textstr = ln[2]
+                if ln[0] in self.colourmap:
+                    self.colourname = ln[0]
+                else:
+                    self.colourname = "black"
+                self.colbtn.child.set_text(self.colourname)
+                self.colour = self.colourmap[self.colourname]
+                self.textcurs = index + 1
+                self.need_redraw = True
+                self.redraw()
+            self.setlabel()
+            self.line = None
+            return
+        if self.textstr != None:
+            sym = self.getsym()
+            if sym:
+                self.add_sym(sym)
+            else:
+                self.redraw()
+            self.line = None
+            self.reset_colour = True
+            return
+
+        self.lines.append(self.line)
+        self.line = None
+        self.reset_colour = True
+        self.need_redraw = True
+        return
+    def motion(self, c, ev):
+        if self.line:
+            if ev.is_hint:
+                x, y, state = ev.window.get_pointer()
+            else:
+                x = ev.x
+                y = ev.y
+            x = int(x)
+            y = int(y)
+            prev = self.line[-1]
+            if not self.movetext and abs(prev[0] - x) < 10 and abs(prev[1] - y) < 10:
+                return
+            if not self.movetext and  len(self.line) == 2 and time.time() - self.taptime > 0.5:
+                self.flush_text()
+                (lineno, index) = self.find_text(prev)
+                if lineno != None:
+                    self.movetext = self.lines[lineno]
+                    self.moveoffset = [prev[0]-self.movetext[1][0], prev[1]-self.movetext[1][1]]
+            if self.movetext:
+                self.movetext[1] = [x-self.moveoffset[0],y-self.moveoffset[1]]
+                self.need_redraw = True
+                self.redraw()
+                return
+
+            if self.movetimeout:
+                gobject.source_remove(self.movetimeout)
+            self.movetimeout = gobject.timeout_add(650, self.movetick)
+                
+            if self.textstr != None:
+                c.window.draw_line(self.colour_textmode, prev[0],prev[1],x,y)
+            else:
+                c.window.draw_line(self.colour, prev[0],prev[1],x,y)
+            self.line.append([x,y])
+        return
+
+    def movetick(self):
+        # longish pause while drawing
+        self.flush_text()
+        self.selecting = True
+        self.need_redraw = True
+        self.redraw()
+        
+    def tick(self):
+        # nothing for 20 seconds, flush the page
+        self.save_page()
+        gobject.source_remove(self.timeout)
+        self.timeout = None
+
+    def find_text(self, pos):
+        x = pos[0]; y = pos[1] + self.lineascent
+        for i in range(0, len(self.lines)):
+            p = self.lines[i]
+            if type(p[2]) != str:
+                continue
+            if x >= p[1][0] and y >= p[1][1] and y < p[1][1] + self.lineheight:
+                # could be this line - check more precisely
+                layout = self.page.create_pango_layout(p[2]+' ')
+                (ink, log) = layout.get_pixel_extents()
+                (ex,ey,ew,eh) = log
+                if x < p[1][0] + ex or x > p[1][0] + ex + ew  or \
+                   y < p[1][1] + ey or \
+                   y > p[1][1] + ey + self.lineheight :
+                    continue
+                # OK, it is in this one.  Find out where.
+                (index, gr) = layout.xy_to_index((x - p[1][0] - ex) * pango.SCALE,
+                                                 (y - p[1][1] - ey - self.lineheight) * pango.SCALE)
+                if index >= len(p[2]):
+                    index = len(p[2])-1
+                return (i, index)
+        return (None, None)
+
+    def align_text(self, pos):
+        # align pos to existing text.
+        # if pos is near one-line-past a previous text, move the exactly
+        # one-line-past
+        x = pos[0]; y = pos[1] + self.lineascent
+        for l in self.lines:
+            if type(l[2]) != str:
+                continue
+            if abs(x - l[1][0]) > self.lineheight:
+                continue
+            if abs(y - (l[1][1] + self.lineheight)) > self.lineheight:
+                continue
+            return [ l[1][0], l[1][1] + self.lineheight ]
+        return pos
+        
+    def flush_text(self):
+        if self.textstr == None:
+            self.textpos = None
+            return
+        self.setlabel()
+        if len(self.textstr) == 0:
+            self.textstr = None
+            self.textpos = None
+            return
+        l = [self.colourname, self.textpos, self.textstr]
+        self.lines.append(l)
+        self.need_redraw = True
+        self.textstr = None
+        self.textpos = None
+
+    def draw_text(self, pos, colour, str, cursor = None, drawable = None, selected=False):
+        if drawable == None:
+            drawable = self.page.window
+        layout = self.page.create_pango_layout(str)
+        if self.zoomx != None:
+            xs = 2; xo = -self.zoomx
+            ys = 2; yo = -self.zoomy
+        else:
+            xs = 1 ; xo = 0
+            ys = 1 ; yo = 0
+        (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+        if len(pos) == 2 or pos[2] != ew or pos[3] != eh:
+            while len(pos) > 2:
+                pos.pop()
+            pos.append(ew)
+            pos.append(eh)
+        if selected:
+            drawable.draw_rectangle(self.colourmap['yellow'], True,
+                                    ex+pos[0], ey+pos[1]-self.lineascent, ew, eh)
+        drawable.draw_layout(colour, pos[0]*xs+xo,
+                             (pos[1] - self.lineascent)*ys + yo,
+                             layout)
+        if cursor != None:
+            (strong,weak) = layout.get_cursor_pos(cursor)
+            (x,y,width,height) = strong
+            x = pos[0] + x / pango.SCALE
+            y = pos[1]
+            drawable.draw_line(self.colour_textmode,
+                               x*xs+xo,y*ys+yo, (x-3)*xs+xo, (y+3)*ys+yo)
+            drawable.draw_line(self.colour_textmode,
+                               x*xs+xo,y*ys+yo, (x+3)*xs+xo, (y+3)*ys+yo)
+
+    def add_sym(self, sym):
+        if self.textstr == None:
+            return
+        if sym == "<BS>":
+            if self.textcurs > 0:
+                self.textstr = self.textstr[0:self.textcurs-1]+ \
+                               self.textstr[self.textcurs:]
+                self.textcurs -= 1
+        elif sym == "<left>":
+            if self.textcurs > 0:
+                self.textcurs -= 1
+        elif sym == "<right>":
+            if self.textcurs < len(self.textstr):
+                self.textcurs += 1
+        elif sym == "<newline>":
+            tail = self.textstr[self.textcurs:]
+            self.textstr = self.textstr[:self.textcurs]
+            oldpos = self.textpos
+            self.flush_text()
+            self.textcurs = len(tail)
+            self.textstr = tail
+            self.textpos = [ oldpos[0], oldpos[1] +
+                             self.lineheight ]
+        else:
+            self.textstr = self.textstr[0:self.textcurs] + sym + \
+                           self.textstr[self.textcurs:]
+            self.textcurs += 1
+        self.redraw()
+
+
+    def getsym(self):
+        alloc = self.page.get_allocation()
+        pagebb = BBox(Point(0,0))
+        pagebb.add(Point(alloc.width, alloc.height))
+        pagebb.finish(div = 2)
+
+        p = PPath(self.line[1][0], self.line[1][1])
+        for pp in self.line[1:]:
+            p.add(pp[0], pp[1])
+        p.close()
+        patn = p.text()
+        pos = pagebb.relpos(p.bbox)
+        tpos = "mid"
+        if pos < 3:
+            tpos = "top"
+        if pos >= 6:
+            tpos = "bot"
+        sym = self.dict.match(patn, tpos)
+        if sym == None:
+            print "Failed to match pattern:", patn
+        return sym
+
+    def refresh(self, area, ev):
+        self.redraw()
+
+    def setlabel(self):
+        if self.textstr == None:
+            self.name.set_label('Page: ' + self.names[self.pagenum] + ' (draw)')
+        else:
+            self.name.set_label('Page: ' + self.names[self.pagenum] + ' (text)')
+
+    def name_buttons(self):
+        if self.selection:
+            self.col_align.child.set_text('Align')
+            self.undo_join.child.set_text('Join')
+            self.redo_rename.child.set_text('Rename')
+        else:
+            self.col_align.child.set_text(self.colourname)
+            self.undo_join.child.set_text('Undo')
+            self.redo_rename.child.set_text('Redo')
+        
+    def redraw(self):
+        self.setlabel()
+
+        if self.need_redraw:
+            self.draw_buf()
+        self.page.window.draw_drawable(self.bg, self.pixbuf, 0, 0, 0, 0,
+                                       self.width, self.height)
+
+        if self.textstr != None:
+            self.draw_text(self.textpos, self.colour, self.textstr,
+                           self.textcurs)
+
+        return
+    def draw_buf(self):
+        if self.pixbuf == None:
+            alloc = self.page.get_allocation()
+            self.pixbuf = gtk.gdk.Pixmap(self.page.window, alloc.width, alloc.height)
+            self.width = alloc.width
+            self.height = alloc.height
+
+        self.pixbuf.draw_rectangle(self.bg, True, 0, 0,
+                                   self.width, self.height)
+        if self.zoomx != None:
+            xs = 2; xo = -self.zoomx
+            ys = 2; yo = -self.zoomy
+        else:
+            xs = 1 ; xo = 0
+            ys = 1 ; yo = 0
+        self.selection = []
+        for l in self.lines:
+            if l[0] in self.colourmap:
+                col = self.colourmap[l[0]]
+            else:
+                col = self.colourmap['black']
+            st = l[1]
+            if type(l[2]) == list:
+                for p in l[2:]:
+                    self.pixbuf.draw_line(col, st[0]*xs + xo, st[1]*ys + yo,
+                                          p[0]*xs + xo,p[1]* ys + yo)
+                    st = p
+            if type(l[2]) == str:
+                # if this text is 'near' the current line, make it red
+                selected = False
+                if self.selecting and self.line and len(st) == 4:
+                    for p in self.line[1:]:
+                        if p[0] > st[0] and \
+                           p[0] < st[0] + st[2] and \
+                           p[1] > st[1] - self.lineascent and \
+                           p[1] < st[1] - self.lineascent + st[3]:
+                            selected = True
+                            break
+                if selected:
+                    self.selection.append(l)
+                self.draw_text(st, col, l[2], drawable = self.pixbuf,
+                               selected = selected)
+        self.need_redraw = False
+        self.name_buttons()
+        
+
+    def reconfigure(self, w, ev):
+        alloc = w.get_allocation()
+        if self.pixbuf == None:
+            return
+        if alloc.width != self.width or alloc.height != self.height:
+            self.pixbuf = None
+            self.need_redraw = True
+
+
+    def colour_change(self,t):
+        if self.selection:
+            # button is 'join' not 'colour'
+            return self.realign(t)
+        
+        if self.reset_colour and self.colourname != 'black':
+            next = 'black'
+        else:
+            next = 'black'
+            prev = ''
+            for c in self.colnames:
+                if self.colourname == prev:
+                    next = c
+                prev = c
+        self.reset_colour = False
+        self.colourname = next
+        self.colour = self.colourmap[next]
+        t.child.set_text(next)
+        if self.textstr:
+            self.draw_text(self.textpos, self.colour, self.textstr,
+                           self.textcurs)
+            
+        return
+    def text_change(self,t):
+        self.flush_text()
+        return
+    def undo(self,b):
+        if self.selection:
+            return self.join(b)
+        
+        if len(self.lines) == 0:
+            return
+        self.hist.append(self.lines.pop())
+        self.need_redraw = True
+        self.redraw()
+        return
+    def redo(self,b):
+        if self.selection:
+            self.rename(self.selection[0][2])
+            return
+        if len(self.hist) == 0:
+            return
+        self.lines.append(self.hist.pop())
+        self.need_redraw = True
+        self.redraw()
+        return
+    def choose_unique(self, newname):
+        while newname in self.names:
+            new2 = inc_name(newname)
+            if new2 not in self.names:
+                newname = new2
+            elif (newname + ".1") not in self.names:
+                newname = newname + ".1"
+            else:
+                newname = newname + ".0.1"
+
+        return newname
+    def delete(self,b):
+        # hack
+        if self.selection:
+            return self.join(b)
+        self.flush_text()
+        if len(self.names) <= 1:
+            return
+        if len(self.lines) > 0:
+            return
+        self.save_page()
+        nm = self.names[self.pagenum]
+        if nm in self.pages:
+            del self.pages[nm]
+        self.names = self.names[0:self.pagenum] + self.names[self.pagenum+1:]
+        if self.pagenum >= len(self.names):
+            self.pagenum -= 1
+        self.load_page()
+        self.need_redraw = True
+        self.redraw()
+
+        return
+
+    def cmplines(self, a,b):
+        pa = a[1]
+        pb = b[1]
+        if pa[1] != pb[1]:
+            return pa[1] - pb[1]
+        return pa[0] - pb[0]
+    
+    def realign(self, b):
+        self.selection.sort(self.cmplines)
+        x = self.selection[0][1][0]
+        y = self.selection[0][1][1]
+        for i in range(len(self.selection)):
+            self.selection[i][1][0] = x
+            self.selection[i][1][1] = y
+            y += self.lineheight
+        self.need_redraw = True
+        self.redraw()
+
+    def join(self, b):
+        self.selection.sort(self.cmplines)
+        txt = ""
+        for i in range(len(self.selection)):
+            if txt:
+                txt = txt + ' ' + self.selection[i][2]
+            else:
+                txt = self.selection[i][2]
+            self.selection[i][2] = None
+        self.selection[0][2] = txt
+        i = 0;
+        while i <  len(self.lines):
+            if len(self.lines[i]) > 2 and  self.lines[i][2] == None:
+                self.lines = self.lines[:i] + self.lines[i+1:]
+            else:
+                i += 1
+        self.need_redraw = True
+        self.redraw()
+        
+    
+    def rename(self, newname):
+        # Rename current page and rename the file
+        if self.names[self.pagenum] == newname:
+            return
+        self.save_page()
+        newname = self.choose_unique(newname)
+        oldpath = self.page_dir + "/" + self.names[self.pagenum]
+        newpath = self.page_dir + "/" + newname
+        try :
+            os.rename(oldpath, newpath)
+            self.names[self.pagenum] = newname
+            self.names.sort(page_cmp)
+            self.pagenum = self.names.index(newname)
+            self.setlabel()
+        except:
+            pass
+        self.update_list()
+
+    def setname(self,b):
+        if self.textstr:
+            if len(self.textstr) > 0:
+                self.rename(self.textstr)
+                
+    def clear(self,b):
+        while len(self.lines) > 0:
+            self.hist.append(self.lines.pop())
+        self.need_redraw = True
+        self.redraw()
+        return
+
+    def parseline(self, l):
+        # string in "", or num,num.  ':' separates words
+        words = l.strip().split(':')
+        line = []
+        for w in words:
+            if w[0] == '"':
+                w = w[1:-1]
+            elif w.find(',') >= 0:
+                n = w.find(',')
+                x = int(w[:n])
+                y = int(w[n+1:])
+                w = [x,y]
+            line.append(w)
+        return line
+
+    def load_page(self):
+        self.need_redraw = True
+        nm = self.names[self.pagenum]
+        if nm in self.pages:
+            self.lines = self.pages[nm]
+            return
+        self.lines = [];
+        try:
+            f = open(self.page_dir + "/" + self.names[self.pagenum], "r")
+        except:
+            f = None
+        if f:
+            l = f.readline()
+            while len(l) > 0:
+                self.lines.append(self.parseline(l))
+                l = f.readline()
+            f.close()
+        return
+
+    def save_page(self):
+        t = self.textstr; tc = self.textcurs
+        self.flush_text()
+        self.pages[self.names[self.pagenum]] = self.lines
+        tosave = self.lines
+        if t and len(t):
+            # restore the text
+            ln = self.lines[-1]
+            self.lines = self.lines[:-1]
+            self.textpos = ln[1]
+            self.textstr = ln[2]
+            self.textcurs = tc
+
+        fn = self.page_dir + "/" + self.names[self.pagenum]
+        if len(tosave) == 0:
+            try:
+                os.unlink(fn)
+            except:
+                pass
+            return
+        f = open(fn, "w")
+        for l in tosave:
+            start = True
+            if not l:
+                continue
+            for w in l:
+                if not start:
+                    f.write(":")
+                start = False
+                if isinstance(w, str):
+                    f.write('"%s"' % w)
+                elif isinstance(w, list):
+                    f.write("%d,%d" %( w[0],w[1]))
+            f.write("\n")
+        f.close()
+
+def main():
+    gtk.main()
+    return 0
+if __name__ == "__main__":
+    ScribblePad()
+    main()
diff --git a/scribble/scribble/Makefile b/scribble/scribble/Makefile
new file mode 100644 (file)
index 0000000..fb0db4e
--- /dev/null
@@ -0,0 +1,13 @@
+
+oldinstall:
+       cp scribble.py /usr/bin
+       chmod a+rx /usr/bin/scribble.py
+       cp scribble.desktop /usr/share/applications
+       chmod a+r /usr/share/applications/scribble.desktop
+       cp scribble.png /usr/share/pixmaps/scribble.png
+       chmod a+r /usr/share/pixmaps/scribble.png
+       if [ -d $$HOME/Pages ] ; then : ; else cp -r Sample-Pages $$HOME/Pages; fi
+
+install:
+       chmod +x scribble.py
+       $(CP) scribble.py $(DEST)/usr/local/bin/scribble
diff --git a/scribble/scribble/Sample-Pages/1 b/scribble/scribble/Sample-Pages/1
new file mode 100755 (executable)
index 0000000..aea83a1
--- /dev/null
@@ -0,0 +1,19 @@
+"black":64,81:"Welcome to"
+"black":72,159:62,162:45,170:34,178:36,192:48,201:63,207:79,214:92,226:97,245:95,263:83,278:65,287:51,289:50,278
+"black":146,200:136,204:122,212:120,241:131,246:144,249:159,243:169,235
+"black":177,206:188,221:193,231:186,211:186,199:199,187:209,183:219,182:234,181:238,191
+"black":255,186:261,196:270,213
+"black":263,111:270,142:276,170:280,186:284,199:286,209:292,224:292,213:293,203:294,192:299,178:317,187:317,201:307,210:292,202
+"black":321,103:321,119:327,156:330,176:332,193:333,207:337,194:345,175:357,171:365,182:360,197:333,194
+"black":371,96:371,108:374,124:376,138:380,154:382,171:387,195
+"black":416,182:421,171:433,161:429,151:417,155:407,168:405,183:410,193:422,196:454,183
+"red":433,211:423,212:405,214:393,215:380,218:367,221:355,225:344,230:332,233:319,237:307,241:295,245:283,250:258,258:235,264:224,267:214,270:201,273:189,276:177,279:153,285:142,289:129,293:117,296:106,300:96,303:79,310:63,316
+"black":92,384:91,396:92,417:91,429:89,417:88,400:90,385:94,371:102,360:113,357:123,359:131,371:129,383:123,394:112,402:100,403
+"black":133,403:139,414:137,401:148,390:162,390:173,395
+"black":174,411:189,407:200,404:208,394:198,388:186,391:178,403:182,418:195,426:208,426:221,422
+"black":253,398:240,394:227,396:232,406:243,410:225,421
+"black":288,405:264,404:275,413:286,418:276,428:259,431:249,431
+"red":359,349:370,359:381,370:391,377:404,391:399,401:389,407:379,411:366,418:356,425:346,431
+"red":94,506:"to continue"
+"red":65,48:71,36:70,23:69,12:59,23
+"red":68,7:76,17:85,29
diff --git a/scribble/scribble/Sample-Pages/2 b/scribble/scribble/Sample-Pages/2
new file mode 100755 (executable)
index 0000000..bc60b71
--- /dev/null
@@ -0,0 +1,23 @@
+"black":31,42:"Here is a page number"
+"black":350,36:361,38:372,40:383,36:395,35:405,33:416,29:426,25:441,18:450,7:440,8:451,7:454,17:453,27
+"black":33,103:"Use"
+"black":202,82:192,80:180,90:168,101:158,106:171,118:189,129:199,135
+"black":241,113:"To go back"
+"black":92,161:106,168:116,174:128,183:140,193:134,204:124,214:113,225:103,234:93,242
+"black":176,198:"for next"
+"black":58,259:54,274:58,291:65,303:79,308:91,303:101,264:101,254
+"black":112,276:117,289:120,300:124,289:138,284:149,295
+"black":192,283:182,282:172,285:170,296:180,298:190,290:192,279:191,265:186,246:183,234:186,251:189,262:190,273:192,284:194,294:197,304
+"black":217,291:226,280:236,278:247,287:245,299:226,305:213,294:215,284:225,280:241,280
+"black":58,349:67,359:71,371:70,360:71,350:77,338:90,335:102,337:112,342
+"black":123,352:134,355:146,355:156,354:163,343:151,338:135,340:129,356:141,371:167,375:184,372
+"black":224,336:222,346:208,350:201,364:208,376:218,372:225,357:225,337:222,321:224,342:227,353:230,363:235,373
+"black":261,361:262,349:274,343:285,348:288,359:272,374:258,369:258,359:264,349
+"black":65,406:68,395:69,409:71,425:73,436
+"black":86,412:76,414:58,416:46,416
+"black":110,436:"add page"
+"black":96,499:84,499:73,500:63,500:53,502
+"black":121,510:"remove page"
+"black":365,503:375,500:378,510:368,514:360,504:371,503
+"black":402,510:402,500:408,510:397,515:390,504:407,499
+"black":429,507:440,504:444,516:430,515:422,503:435,502
diff --git a/scribble/scribble/Sample-Pages/3 b/scribble/scribble/Sample-Pages/3
new file mode 100755 (executable)
index 0000000..e3ddef8
--- /dev/null
@@ -0,0 +1,18 @@
+"black":117,45:122,57:108,51:96,48:82,53:70,62:57,74:47,86:41,100:40,117:41,128:48,146:56,156:66,165:78,171:110,177:133,169
+"black":158,119:"Clear page"
+"black":195,159:"Before removal"
+"black":83,235:84,224:80,250:79,270:78,289:78,319:79,330
+"black":165,229:149,223:128,220:105,218:60,213:46,213:36,213
+"black":120,308:130,303:143,298:156,306:154,320:139,329:129,326:127,308
+"black":186,297:175,305:168,315:178,318:189,315:199,304:201,321:203,332:169,367:162,357:165,344
+"black":241,304:229,298:219,306:216,319:230,325:241,320:251,310:251,299:252,310:257,340:256,362:250,376:236,379:224,372:221,361
+"black":271,227:271,240:274,255:275,271:277,291:278,308:279,318
+"black":299,322:309,320:328,305:318,300:303,313:333,335:361,322
+"black":219,392:222,409:224,426:226,437
+"black":273,405:261,398:217,395:204,394:193,393:179,394
+"black":249,426:261,424:271,424:268,413:252,418:247,431:263,439:285,441:297,441
+"black":295,424:311,427:322,434
+"black":324,420:312,441:302,455
+"black":354,373:349,388:350,400:351,414:353,427:354,444
+"black":380,414:366,405:355,401:343,400:332,398:322,397
+"black":66,492:"Tap to enable"
diff --git a/scribble/scribble/scribble.desktop b/scribble/scribble/scribble.desktop
new file mode 100644 (file)
index 0000000..94385cc
--- /dev/null
@@ -0,0 +1,12 @@
+[Desktop Entry]
+Name=Scribble pad
+Comment=Note pad for scibbles and note taking
+Encoding=UTF-8
+Version=1.0
+Type=Application
+Exec=scribble.py
+Icon=scribble
+Terminal=false
+Categories=GTK;Application;PIM;Office
+SingleInstance=true
+StartupNotify=true
diff --git a/scribble/scribble/scribble.png b/scribble/scribble/scribble.png
new file mode 100644 (file)
index 0000000..a5b9cbc
Binary files /dev/null and b/scribble/scribble/scribble.png differ
diff --git a/scribble/scribble/scribble.py b/scribble/scribble/scribble.py
new file mode 100755 (executable)
index 0000000..ecf1f9f
--- /dev/null
@@ -0,0 +1,1493 @@
+#!/usr/bin/env python
+
+
+# scribble - scribble pad designed for Neo Freerunner
+#
+# Copyright (C) 2008 Neil Brown <neil@brown.name>
+#
+#
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    The GNU General Public License, version 2, is available at
+#    http://www.fsf.org/licensing/licenses/info/GPLv2.html
+#    Or you can write to the Free Software Foundation, Inc., 51
+#    Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+#    Author: Neil Brown
+#    Email: <neil@brown.name>
+
+
+# TODO
+# - index page
+#     list of pages
+#     Buttons: select, delete, new
+# - bigger buttons - fewer
+#   Need:  undo, redo, colour, text/line
+#   When text is selected, these change to:
+#        align, join, make-title
+
+import pygtk
+import gtk
+import os
+import pango
+import gobject
+import time
+import listselect
+
+###########################################################
+# Writing recognistion code
+import math
+
+
+def LoadDict(dict):
+    # Upper case.
+    # Where they are like lowercase, we either double
+    # the last stroke (L, J, I) or draw backwards (S, Z, X)
+    # U V are a special case
+
+    dict.add('A', "R(4)6,8")
+    dict.add('B', "R(4)6,4.R(7)1,6")
+    dict.add('B', "R(4)6,4.L(4)2,8.R(7)1,6")
+    dict.add('B', "S(6)7,1.R(4)6,4.R(7)0,6")
+    dict.add('C', "R(4)8,2")
+    dict.add('D', "R(4)6,6")
+    dict.add('E', "L(1)2,8.L(7)2,8")
+    # double the stem for F
+    dict.add('F', "L(4)2,6.S(3)7,1")
+    dict.add('F', "S(1)5,3.S(3)1,7.S(3)7,1")
+
+    dict.add('G', "L(4)2,5.S(8)1,7")
+    dict.add('G', "L(4)2,5.R(8)6,8")
+    # FIXME I need better straight-curve alignment
+    dict.add('H', "S(3)1,7.R(7)6,8.S(5)7,1")
+    dict.add('H', "L(3)0,5.R(7)6,8.S(5)7,1")
+    # capital I is down/up
+    dict.add('I', "S(4)1,7.S(4)7,1")
+
+    # Capital J has a left/right tail
+    dict.add('J', "R(4)1,6.S(7)3,5")
+
+    dict.add('K', "L(4)0,2.R(4)6,6.L(4)2,8")
+
+    # Capital L, like J, doubles the foot
+    dict.add('L', "L(4)0,8.S(7)4,3")
+
+    dict.add('M', "R(3)6,5.R(5)3,8")
+    dict.add('M', "R(3)6,5.L(1)0,2.R(5)3,8")
+
+    dict.add('N', "R(3)6,8.L(5)0,2")
+
+    # Capital O is CW, but can be CCW in special dict
+    dict.add('O', "R(4)1,1", bot='0')
+
+    dict.add('P', "R(4)6,3")
+    dict.add('Q', "R(4)7,7.S(8)0,8")
+
+    dict.add('R', "R(4)6,4.S(8)0,8")
+
+    # S is drawn bottom to top.
+    dict.add('S', "L(7)6,1.R(1)7,2")
+
+    # Double the stem for capital T
+    dict.add('T', "R(4)0,8.S(5)7,1")
+
+    # U is L to R, V is R to L for now
+    dict.add('U', "L(4)0,2")
+    dict.add('V', "R(4)2,0")
+
+    dict.add('W', "R(5)2,3.L(7)8,6.R(3)5,0")
+    dict.add('W', "R(5)2,3.R(3)5,0")
+
+    dict.add('X', "R(4)6,0")
+
+    dict.add('Y',"L(1)0,2.R(5)4,6.S(5)6,2")
+    dict.add('Y',"L(1)0,2.S(5)2,7.S(5)7,2")
+
+    dict.add('Z', "R(4)8,2.L(4)6,0")
+
+    # Lower case
+    dict.add('a', "L(4)2,2.L(5)1,7")
+    dict.add('a', "L(4)2,2.L(5)0,8")
+    dict.add('a', "L(4)2,2.S(5)0,8")
+    dict.add('b', "S(3)1,7.R(7)6,3")
+    dict.add('c', "L(4)2,8", top='C')
+    dict.add('d', "L(4)5,2.S(5)1,7")
+    dict.add('d', "L(4)5,2.L(5)0,8")
+    dict.add('e', "S(4)3,5.L(4)5,8")
+    dict.add('e', "L(4)3,8")
+    dict.add('f', "L(4)2,6", top='F')
+    dict.add('f', "S(1)5,3.S(3)1,7", top='F')
+    dict.add('g', "L(1)2,2.R(4)1,6")
+    dict.add('h', "S(3)1,7.R(7)6,8")
+    dict.add('h', "L(3)0,5.R(7)6,8")
+    dict.add('i', "S(4)1,7", top='I', bot='1')
+    dict.add('j', "R(4)1,6", top='J')
+    dict.add('k', "L(3)0,5.L(7)2,8")
+    dict.add('k', "L(4)0,5.R(7)6,6.L(7)1,8")
+    dict.add('l', "L(4)0,8", top='L')
+    dict.add('l', "S(4)0,8", top='L')
+    dict.add('l', "S(3)1,7.S(7)3,5", top='L')
+    dict.add('m', "S(3)1,7.R(3)6,8.R(5)6,8")
+    dict.add('m', "L(3)0,2.R(3)6,8.R(5)6,8")
+    dict.add('n', "S(3)1,7.R(4)6,8")
+    dict.add('o', "L(4)1,1", top='O', bot='0')
+    dict.add('p', "S(3)1,7.R(4)6,3")
+    dict.add('q', "L(1)2,2.L(5)1,5")
+    dict.add('q', "L(1)2,2.S(5)1,7.R(8)6,2")
+    dict.add('q', "L(1)2,2.S(5)1,7.S(5)1,7")
+    # FIXME this double 1,7 is due to a gentle where the
+    # second looks like a line because it is narrow.??
+    dict.add('r', "S(3)1,7.R(4)6,2")
+    dict.add('s', "L(1)2,7.R(7)1,6", top='S', bot='5')
+    dict.add('s', "L(5)1,8.R(7)2,3", top='S', bot='5')
+    dict.add('t', "R(4)0,8", top='T', bot='7')
+    dict.add('t', "S(1)3,5.S(5)1,7", top='T', bot='7')
+    dict.add('u', "L(4)0,2.S(5)1,7")
+    dict.add('v', "L(4)0,2.L(2)0,2")
+    dict.add('w', "L(3)0,2.L(5)0,2", top='W')
+    dict.add('w', "L(3)0,5.R(7)6,8.L(5)3,2", top='W')
+    dict.add('w', "L(3)0,5.L(5)3,2", top='W')
+    dict.add('x', "L(4)0,6", top='X')
+    dict.add('y', "L(1)0,2.R(5)4,6", top='Y') # if curved
+    dict.add('y', "L(1)0,2.S(5)2,7", top='Y')
+    dict.add('z', "R(4)0,6.L(4)2,8", top='Z', bot='2')
+
+    # Digits
+    dict.add('0', "L(4)7,7")
+    dict.add('0', "R(4)7,7")
+    dict.add('1', "S(4)7,1")
+    dict.add('2', "R(4)0,6.S(7)3,5")
+    dict.add('2', "R(4)3,6.L(4)2,8")
+    dict.add('3', "R(1)0,6.R(7)1,6")
+    dict.add('4', "L(4)7,5")
+    dict.add('5', "L(1)2,6.R(7)0,3")
+    dict.add('5', "L(1)2,6.L(4)0,8.R(7)0,3")
+    dict.add('6', "L(4)2,3")
+    dict.add('7', "S(1)3,5.R(4)1,6")
+    dict.add('7', "R(4)0,6")
+    dict.add('7', "R(4)0,7")
+    dict.add('8', "L(4)2,8.R(4)4,2.L(3)6,1")
+    dict.add('8', "L(1)2,8.R(7)2,0.L(1)6,1")
+    dict.add('8', "L(0)2,6.R(7)0,1.L(2)6,0")
+    dict.add('8', "R(4)2,6.L(4)4,2.R(5)8,1")
+    dict.add('9', "L(1)2,2.S(5)1,7")
+
+    dict.add(' ', "S(4)3,5")
+    dict.add('<BS>', "S(4)5,3")
+    dict.add('-', "S(4)3,5.S(4)5,3")
+    dict.add('_', "S(4)3,5.S(4)5,3.S(4)3,5")
+    dict.add("<left>", "S(4)5,3.S(3)3,5")
+    dict.add("<right>","S(4)3,5.S(5)5,3")
+    dict.add("<newline>", "S(4)2,6")
+
+
+class DictSegment:
+    # Each segment has four elements:
+    #   direction: Right Straight Left (R=cw, L=ccw)
+    #   location: 0-8.
+    #   start: 0-8
+    #   finish: 0-8
+    # Segments match if the difference at each element
+    # is 0, 1, or 3 (RSL coded as 012)
+    # A difference of 1 required both to be same / 3
+    # On a match, return number of 0s
+    # On non-match, return -1
+    def __init__(self, str):
+        # D(L)S,R
+        # 0123456
+        self.e = [0,0,0,0]
+        if len(str) != 7:
+            raise ValueError
+        if str[1] != '(' or str[3] != ')' or str[5] != ',':
+            raise ValueError
+        if str[0] == 'R':
+            self.e[0] = 0
+        elif str[0] == 'L':
+            self.e[0] = 2
+        elif str[0] == 'S':
+            self.e[0] = 1
+        else:
+            raise ValueError
+
+        self.e[1] = int(str[2])
+        self.e[2] = int(str[4])
+        self.e[3] = int(str[6])
+
+    def match(self, other):
+        cnt = 0
+        for i in range(0,4):
+            diff = abs(self.e[i] - other.e[i])
+            if diff == 0:
+                cnt += 1
+            elif diff == 3:
+                pass
+            elif diff == 1 and (self.e[i]/3 == other.e[i]/3):
+                pass
+            else:
+                return -1
+        return cnt
+
+class DictPattern:
+    # A Dict Pattern is a list of segments.
+    # A parsed pattern matches a dict pattern if
+    # the are the same nubmer of segments and they
+    # all match.  The value of the match is the sum
+    # of the individual matches.
+    # A DictPattern is printers as segments joined by periods.
+    #
+    def __init__(self, str):
+        self.segs = map(DictSegment, str.split("."))
+    def match(self,other):
+        if len(self.segs) != len(other.segs):
+            return -1
+        cnt = 0
+        for i in range(0,len(self.segs)):
+            m = self.segs[i].match(other.segs[i])
+            if m < 0:
+                return m
+            cnt += m
+        return cnt
+
+
+class Dictionary:
+    # The dictionary hold all the pattern for symbols and
+    # performs lookup
+    # Each pattern in the directionary can be associated
+    # with  3 symbols.  One when drawing in middle of screen,
+    # one for top of screen, one for bottom.
+    # Often these will all be the same.
+    # This allows e.g. s and S to have the same pattern in different
+    # location on the touchscreen.
+    # A match requires a unique entry with a match that is better
+    # than any other entry.
+    #
+    def __init__(self):
+        self.dict = []
+    def add(self, sym, pat, top = None, bot = None):
+        if top == None: top = sym
+        if bot == None: bot = sym
+        self.dict.append((DictPattern(pat), sym, top, bot))
+
+    def _match(self, p):
+        max = -1
+        val = None
+        for (ptn, sym, top, bot) in self.dict:
+            cnt = ptn.match(p)
+            if cnt > max:
+                max = cnt
+                val = (sym, top, bot)
+            elif cnt == max:
+                val = None
+        return val
+
+    def match(self, str, pos = "mid"):
+        p = DictPattern(str)
+        m = self._match(p)
+        if m == None:
+            return m
+        (mid, top, bot) = self._match(p)
+        if pos == "top": return top
+        if pos == "bot": return bot
+        return mid
+
+
+class Point:
+    # This represents a point in the path and all the points leading
+    # up to it.  It allows us to find the direction and curvature from
+    # one point to another
+    # We store x,y, and sum/cnt of points so far
+    def __init__(self,x,y) :
+        self.xsum = x
+        self.ysum = y
+        self.x = x
+        self.y = y
+        self.cnt = 1
+
+    def copy(self):
+        n = Point(0,0)
+        n.xsum = self.xsum
+        n.ysum = self.ysum
+        n.x = self.x
+        n.y = self.y
+        n.cnt = self.cnt
+        return n
+
+    def add(self,x,y):
+        if self.x == x and self.y == y:
+            return
+        self.x = x
+        self.y = y
+        self.xsum += x
+        self.ysum += y
+        self.cnt += 1
+
+    def xlen(self,p):
+        return abs(self.x - p.x)
+    def ylen(self,p):
+        return abs(self.y - p.y)
+    def sqlen(self,p):
+        x = self.x - p.x
+        y = self.y - p.y
+        return x*x + y*y
+
+    def xdir(self,p):
+        if self.x > p.x:
+            return 1
+        if self.x < p.x:
+            return -1
+        return 0
+    def ydir(self,p):
+        if self.y > p.y:
+            return 1
+        if self.y < p.y:
+            return -1
+        return 0
+    def curve(self,p):
+        if self.cnt == p.cnt:
+            return 0
+        x1 = p.x ; y1 = p.y
+        (x2,y2) = self.meanpoint(p)
+        x3 = self.x; y3 = self.y
+
+        curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
+        curve = curve * 100 / ((y3-y1)*(y3-y1)
+                               + (x3-x1)*(x3-x1))
+        if curve > 6:
+            return 1
+        if curve < -6:
+            return -1
+        return 0
+
+    def Vcurve(self,p):
+        if self.cnt == p.cnt:
+            return 0
+        x1 = p.x ; y1 = p.y
+        (x2,y2) = self.meanpoint(p)
+        x3 = self.x; y3 = self.y
+
+        curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
+        curve = curve * 100 / ((y3-y1)*(y3-y1)
+                               + (x3-x1)*(x3-x1))
+        return curve
+
+    def meanpoint(self,p):
+        x = (self.xsum - p.xsum) / (self.cnt - p.cnt)
+        y = (self.ysum - p.ysum) / (self.cnt - p.cnt)
+        return (x,y)
+
+    def is_sharp(self,A,C):
+        # Measure the cosine at self between A and C
+        # as A and C could be curve, we take the mean point on
+        # self.A and self.C as the points to find cosine between
+        (ax,ay) = self.meanpoint(A)
+        (cx,cy) = self.meanpoint(C)
+        a = ax-self.x; b=ay-self.y
+        c = cx-self.x; d=cy-self.y
+        x = a*c + b*d
+        y = a*d - b*c
+        h = math.sqrt(x*x+y*y)
+        if h > 0:
+            cs = x*1000/h
+        else:
+            cs = 0
+        return (cs > 900)
+
+class BBox:
+    # a BBox records min/max x/y of some Points and
+    # can subsequently report row, column, pos of each point
+    # can also locate one bbox in another
+
+    def __init__(self, p):
+        self.minx = p.x
+        self.maxx = p.x
+        self.miny = p.y
+        self.maxy = p.y
+
+    def width(self):
+        return self.maxx - self.minx
+    def height(self):
+        return self.maxy - self.miny
+
+    def add(self, p):
+        if p.x > self.maxx:
+            self.maxx = p.x
+        if p.x < self.minx:
+            self.minx = p.x
+
+        if p.y > self.maxy:
+            self.maxy = p.y
+        if p.y < self.miny:
+            self.miny = p.y
+    def finish(self, div = 3):
+        # if aspect ratio is bad, we adjust max/min accordingly
+        # before setting [xy][12].  We don't change self.min/max
+        # as they are used to place stroke in bigger bbox.
+        # Normally divisions are at 1/3 and 2/3. They can be moved
+        # by setting div e.g. 2 = 1/2 and 1/2
+        (minx,miny,maxx,maxy) = (self.minx,self.miny,self.maxx,self.maxy)
+        if (maxx - minx) * 3 < (maxy - miny) * 2:
+            # too narrow
+            mid = int((maxx + minx)/2)
+            halfwidth = int ((maxy - miny)/3)
+            minx = mid - halfwidth
+            maxx = mid + halfwidth
+        if (maxy - miny) * 3 < (maxx - minx) * 2:
+            # too wide
+            mid = int((maxy + miny)/2)
+            halfheight = int ((maxx - minx)/3)
+            miny = mid - halfheight
+            maxy = mid + halfheight
+
+        div1 = div - 1
+        self.x1 = int((div1*minx + maxx)/div)
+        self.x2 = int((minx + div1*maxx)/div)
+        self.y1 = int((div1*miny + maxy)/div)
+        self.y2 = int((miny + div1*maxy)/div)
+
+    def row(self, p):
+        # 0, 1, 2 - top to bottom
+        if p.y <= self.y1:
+            return 0
+        if p.y < self.y2:
+            return 1
+        return 2
+    def col(self, p):
+        if p.x <= self.x1:
+            return 0
+        if p.x < self.x2:
+            return 1
+        return 2
+    def box(self, p):
+        # 0 to 9
+        return self.row(p) * 3 + self.col(p)
+
+    def relpos(self,b):
+        # b is a box within self.  find location 0-8
+        if b.maxx < self.x2 and b.minx < self.x1:
+            x = 0
+        elif b.minx > self.x1 and b.maxx > self.x2:
+            x = 2
+        else:
+            x = 1
+        if b.maxy < self.y2 and b.miny < self.y1:
+            y = 0
+        elif b.miny > self.y1 and b.maxy > self.y2:
+            y = 2
+        else:
+            y = 1
+        return y*3 + x
+
+
+def different(*args):
+    cur = 0
+    for i in args:
+        if cur != 0 and i != 0 and cur != i:
+            return True
+        if cur == 0:
+            cur = i
+    return False
+
+def maxcurve(*args):
+    for i in args:
+        if i != 0:
+            return i
+    return 0
+
+class PPath:
+    # a PPath refines a list of x,y points into a list of Points
+    # The Points mark out segments which end at significant Points
+    # such as inflections and reversals.
+
+    def __init__(self, x,y):
+
+        self.start = Point(x,y)
+        self.mid = Point(x,y)
+        self.curr = Point(x,y)
+        self.list = [ self.start ]
+
+    def add(self, x, y):
+        self.curr.add(x,y)
+
+        if ( (abs(self.mid.xdir(self.start) - self.curr.xdir(self.mid)) == 2) or
+             (abs(self.mid.ydir(self.start) - self.curr.ydir(self.mid)) == 2) or
+             (abs(self.curr.Vcurve(self.start))+2 < abs(self.mid.Vcurve(self.start)))):
+            pass
+        else:
+            self.mid = self.curr.copy()
+
+        if self.curr.xlen(self.mid) > 4 or self.curr.ylen(self.mid) > 4:
+            self.start = self.mid.copy()
+            self.list.append(self.start)
+            self.mid = self.curr.copy()
+
+    def close(self):
+        self.list.append(self.curr)
+
+    def get_sectlist(self):
+        if len(self.list) <= 2:
+            return [[0,self.list]]
+        l = []
+        A = self.list[0]
+        B = self.list[1]
+        s = [A,B]
+        curcurve = B.curve(A)
+        for C in self.list[2:]:
+            cabc = C.curve(A)
+            cab = B.curve(A)
+            cbc = C.curve(B)
+            if B.is_sharp(A,C) and not different(cabc, cab, cbc, curcurve):
+                # B is too pointy, must break here
+                l.append([curcurve, s])
+                s = [B, C]
+                curcurve = cbc
+            elif not different(cabc, cab, cbc, curcurve):
+                # all happy
+                s.append(C)
+                if curcurve == 0:
+                    curcurve = maxcurve(cab, cbc, cabc)
+            elif not different(cabc, cab, cbc)  :
+                # gentle inflection along AB
+                # was: AB goes in old and new section
+                # now: AB only in old section, but curcurve
+                #      preseved.
+                l.append([curcurve,s])
+                s = [A, B, C]
+                curcurve =maxcurve(cab, cbc, cabc)
+            else:
+                # Change of direction at B
+                l.append([curcurve,s])
+                s = [B, C]
+                curcurve = cbc
+
+            A = B
+            B = C
+        l.append([curcurve,s])
+
+        return l
+
+    def remove_shorts(self, bbox):
+        # in self.list, if a point is close to the previous point,
+        # remove it.
+        if len(self.list) <= 2:
+            return
+        w = bbox.width()/10
+        h = bbox.height()/10
+        n = [self.list[0]]
+        leng = w*h*2*2
+        for p in self.list[1:]:
+            l = p.sqlen(n[-1])
+            if l > leng:
+                n.append(p)
+        self.list = n
+
+    def text(self):
+        # OK, we have a list of points with curvature between.
+        # want to divide this into sections.
+        # for each 3 consectutive points ABC curve of ABC and AB and BC
+        # If all the same, they are all in a section.
+        # If not B starts a new section and the old ends on B or C...
+        BB = BBox(self.list[0])
+        for p in self.list:
+            BB.add(p)
+        BB.finish()
+        self.bbox = BB
+        self.remove_shorts(BB)
+        sectlist = self.get_sectlist()
+        t = ""
+        for c, s in sectlist:
+            if c > 0:
+                dr = "R"  # clockwise is to the Right
+            elif c < 0:
+                dr = "L"  # counterclockwise to the Left
+            else:
+                dr = "S"  # straight
+            bb = BBox(s[0])
+            for p in s:
+                bb.add(p)
+            bb.finish()
+            # If  all points are in some row or column, then
+            # line is S
+            rwdiff = False; cldiff = False
+            rw = bb.row(s[0]); cl=bb.col(s[0])
+            for p in s:
+                if bb.row(p) != rw: rwdiff = True
+                if bb.col(p) != cl: cldiff = True
+            if not rwdiff or not cldiff: dr = "S"
+
+            t1 = dr
+            t1 += "(%d)" % BB.relpos(bb)
+            t1 += "%d,%d" % (bb.box(s[0]), bb.box(s[-1]))
+            t += t1 + '.'
+        return t[:-1]
+
+
+
+def page_cmp(a,b):
+    if a.lower() < b.lower():
+        return -1
+    if a.lower() > b.lower():
+        return 1
+    if a < b:
+        return -1
+    if a > b:
+        return 1
+    return 0
+
+def inc_name(a):
+    l = len(a)
+    while l > 0 and a[l-1] >= '0' and a[l-1] <= '9':
+        l -= 1
+    # a[l:] is the last number
+    if l == len(a):
+        # there is no number
+        return a + ".1"
+    num = 0 + int(a[l:])
+    return a[0:l] + ("%d" % (num+1))
+
+class ScribblePad:
+
+    def __init__(self):
+        window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+        window.connect("destroy", self.close_application)
+        window.set_title("ScribblePad")
+        #window.set_size_request(480,640)
+        self.window = window
+
+        vb = gtk.VBox()
+        vb.show()
+        self.draw_box = vb
+
+        bar = gtk.HBox(True)
+        bar.set_size_request(-1, 60)
+        vb.pack_end(bar, expand=False)
+        bar.show()
+
+        l = gtk.Label('Page Name')
+        vb.pack_start(l, expand=False)
+        l.show()
+        self.name = l
+
+        page = gtk.DrawingArea()
+        page.set_size_request(480,540)
+        vb.add(page)
+        page.show()
+        ctx = page.get_pango_context()
+        fd = ctx.get_font_description()
+        fd.set_absolute_size(25*pango.SCALE)
+        page.modify_font(fd)
+
+        l.modify_font(fd)
+
+        dflt = gtk.widget_get_default_style()
+        fd = dflt.font_desc
+        fd.set_absolute_size(25*pango.SCALE)
+
+        self.pixbuf = None
+        self.width = 0
+        self.height = 0
+        self.need_redraw = True
+        self.zoomx = None; self.zoomy = None
+
+        # Now the widgets:
+
+        done = gtk.Button("Done"); done.show()
+        colbtn = gtk.Button("colour"); colbtn.show()
+        undo = gtk.Button('Undo') ; undo.show()
+        redo = gtk.Button('Redo') ; redo.show()
+
+        done.child.modify_font(fd)
+        colbtn.child.modify_font(fd)
+        undo.child.modify_font(fd)
+        redo.child.modify_font(fd)
+
+        bar.add(done)
+        bar.add(colbtn)
+        bar.add(undo)
+        bar.add(redo)
+
+        colbtn.connect("clicked", self.colour_change)
+        undo.connect("clicked", self.undo)
+        redo.connect("clicked", self.redo)
+        done.connect("clicked", self.done)
+
+        self.col_align = colbtn
+        self.undo_join = undo
+        self.redo_rename = redo
+
+        self.page = page
+        self.colbtn = colbtn
+        self.line = None
+        self.lines = []
+        self.hist = [] # undo history
+        self.selecting = False
+        self.textcurs = 0
+        self.textstr = None
+        self.textpos = None
+
+
+        page.connect("button_press_event", self.press)
+        page.connect("button_release_event", self.release)
+        page.connect("motion_notify_event", self.motion)
+        page.connect("expose-event", self.refresh)
+        page.connect("configure-event", self.reconfigure)
+        page.connect("key_press_event", self.type)
+        page.set_events(gtk.gdk.EXPOSURE_MASK
+                        | gtk.gdk.STRUCTURE_MASK
+                        | gtk.gdk.BUTTON_PRESS_MASK
+                        | gtk.gdk.BUTTON_RELEASE_MASK
+                        | gtk.gdk.KEY_PRESS_MASK
+                        | gtk.gdk.KEY_RELEASE_MASK
+                        | gtk.gdk.POINTER_MOTION_MASK
+                        | gtk.gdk.POINTER_MOTION_HINT_MASK)
+        page.set_property('can-focus', True)
+
+
+        # Now create the index window
+        # A listselect of all the pages, with a row of buttons:
+        #   open delete new undelete
+        ls = listselect.ListSelect(center = False)
+        self.listsel = ls
+
+        ls.connect('selected', self.page_select)
+        ls.set_colour('page','blue')
+        ls.set_zoom(38)
+        ls.show()
+        
+        vb = gtk.VBox()
+        vb.show()
+        self.list_box = vb
+
+        vb.add(ls)
+        bar = gtk.HBox(True); bar.show()
+        bar.set_size_request(-1, 60)
+        b= gtk.Button("Open"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.open_page)
+        b= gtk.Button("Del"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.del_page)
+        b= gtk.Button("New"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.new_page)
+        b= gtk.Button("Undelete"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.undelete_pages)
+
+        vb.pack_end(bar, expand=False)
+
+        window.add(self.draw_box)
+
+        window.set_default_size(480,640)
+
+        window.show()
+
+
+        if 'HOME' in os.environ:
+            home = os.environ['HOME']
+        else:
+            home = ""
+        if home == "" or home == "/":
+            home = "/home/root"
+        self.page_dir = home + '/Pages'
+        self.page_dir = '/data/Pages'
+        self.load_pages()
+
+        colourmap = page.get_colormap()
+        self.colourmap = {}
+        self.colnames = [ 'black', 'red', 'blue', 'green', 'purple', 'pink', 'yellow' ]
+        for col in self.colnames:
+            c = gtk.gdk.color_parse(col)
+            gc = page.window.new_gc()
+            gc.line_width = 2
+            gc.set_foreground(colourmap.alloc_color(c))
+            self.colourmap[col] = gc
+        self.reset_colour = False
+
+        self.colour_textmode = self.colourmap['blue']
+
+        self.colourname = "black"
+        self.colour = self.colourmap['black']
+        self.colbtn.child.set_text('black')
+        self.bg = page.get_style().bg_gc[gtk.STATE_NORMAL]
+
+        self.dict = Dictionary()
+        LoadDict(self.dict)
+        self.textstr = None
+
+
+        ctx = page.get_pango_context()
+        fd = ctx.get_font_description()
+        met = ctx.get_metrics(fd)
+        self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE
+        self.lineascent = met.get_ascent() / pango.SCALE
+
+        self.timeout = None
+
+        window.remove(self.draw_box)
+        window.add(self.list_box)
+
+
+    def close_application(self, widget):
+        self.save_page()
+        gtk.main_quit()
+
+    def load_pages(self):
+        try:
+            os.mkdir(self.page_dir)
+        except:
+            pass
+        self.names = os.listdir(self.page_dir)
+        if len(self.names) == 0:
+            self.names.append("1")
+        self.names.sort(page_cmp)
+        self.pages = {}
+        self.pagenum = 0
+        self.load_page()
+        self.update_list()
+        
+    def update_list(self):
+        l = []
+        for p in self.names:
+            l.append([p,'page'])
+        self.listsel.list = l
+        self.listsel.list_changed()
+        self.listsel.select(self.pagenum)
+        return
+
+    def page_select(self, list, item):
+        if item == None:
+            return
+        self.pagenum = item
+
+    def open_page(self, b):
+        self.load_page()
+        self.window.remove(self.list_box)
+        self.window.add(self.draw_box)
+
+    def done(self, b):
+        self.flush_text()
+        self.save_page()
+        self.window.remove(self.draw_box)
+        self.window.add(self.list_box)
+
+    def del_page(self, b):
+        pass
+
+    def new_page(self, b):
+        newname = self.choose_unique(self.names[self.pagenum])
+        self.names = self.names[0:self.pagenum+1] + [ newname ] + \
+                     self.names[self.pagenum+1:]
+        self.pagenum += 1;
+        self.update_list()
+        self.listsel.select(self.pagenum)
+
+        return
+
+    def undelete_pages(self, b):
+        pass
+        
+
+    def type(self, c, ev):
+        if ev.keyval == 65288:
+            self.add_sym('<BS>')
+        elif ev.string == '\r':
+            self.add_sym('<newline>')
+        else:
+            self.add_sym(ev.string)
+        
+    def press(self, c, ev):
+        # Start a new line
+        if self.timeout:
+            gobject.source_remove(self.timeout)
+            self.timeout = None
+        self.taptime = time.time()
+        self.movetext = None
+        c.grab_focus()
+        self.movetimeout = None
+        self.selecting = False
+        if self.selection:
+            self.selection = None
+            self.need_redraw = True
+            self.name_buttons()
+            self.redraw()
+
+        self.line = [ self.colourname, [int(ev.x), int(ev.y)] ]
+        return
+    def release(self, c, ev):
+        if self.movetimeout:
+            gobject.source_remove(self.movetimeout)
+            self.movetimeout = None
+            
+        if self.movetext:
+            self.movetext = None
+            self.line = None
+            self.need_redraw = True
+            self.redraw()
+            return
+        if self.line == None:
+            return
+
+        if self.selecting:
+            self.selecting = False
+            self.line = None
+            return
+        if self.timeout == None:
+            self.timeout = gobject.timeout_add(20*1000, self.tick)
+            
+        if len(self.line) == 2:
+            # just set a cursor
+            need_redraw = ( self.textstr != None)
+            oldpos = None
+            if not self.textstr:
+                oldpos = self.textpos
+            self.flush_text()
+            if need_redraw:
+                self.redraw()
+            (lineno,index) = self.find_text(self.line[1])
+            
+            if lineno == None:
+                # new text,
+                pos = self.align_text(self.line[1])
+                if oldpos and abs(pos[0]-oldpos[0]) < 40 and \
+                       abs(pos[1] - oldpos[1]) < 40:
+                    # turn of text mode
+                    self.flush_text()
+                    self.line = None
+                    self.need_redraw = True
+                    self.setlabel()
+                    return
+                self.textpos = pos
+                self.textstr = ""
+                self.textcurs = 0
+                # draw the cursor
+                self.draw_text(pos, self.colour, "", 0)
+                self.line = None
+            else:
+                # clicked inside an old text.
+                # shuffle it to the top, open it, edit.
+                ln = self.lines[lineno]
+                self.lines = self.lines[:lineno] + self.lines[lineno+1:]
+                self.textpos = ln[1]
+                self.textstr = ln[2]
+                if ln[0] in self.colourmap:
+                    self.colourname = ln[0]
+                else:
+                    self.colourname = "black"
+                self.colbtn.child.set_text(self.colourname)
+                self.colour = self.colourmap[self.colourname]
+                self.textcurs = index + 1
+                self.need_redraw = True
+                self.redraw()
+            self.setlabel()
+            self.line = None
+            return
+        if self.textstr != None:
+            sym = self.getsym()
+            if sym:
+                self.add_sym(sym)
+            else:
+                self.redraw()
+            self.line = None
+            self.reset_colour = True
+            return
+
+        self.lines.append(self.line)
+        self.line = None
+        self.reset_colour = True
+        self.need_redraw = True
+        return
+    def motion(self, c, ev):
+        if self.line:
+            if ev.is_hint:
+                x, y, state = ev.window.get_pointer()
+            else:
+                x = ev.x
+                y = ev.y
+            x = int(x)
+            y = int(y)
+            prev = self.line[-1]
+            if not self.movetext and abs(prev[0] - x) < 10 and abs(prev[1] - y) < 10:
+                return
+            if not self.movetext and  len(self.line) == 2 and time.time() - self.taptime > 0.5:
+                self.flush_text()
+                (lineno, index) = self.find_text(prev)
+                if lineno != None:
+                    self.movetext = self.lines[lineno]
+                    self.moveoffset = [prev[0]-self.movetext[1][0], prev[1]-self.movetext[1][1]]
+            if self.movetext:
+                self.movetext[1] = [x-self.moveoffset[0],y-self.moveoffset[1]]
+                self.need_redraw = True
+                self.redraw()
+                return
+
+            if self.movetimeout:
+                gobject.source_remove(self.movetimeout)
+            self.movetimeout = gobject.timeout_add(650, self.movetick)
+                
+            if self.textstr != None:
+                c.window.draw_line(self.colour_textmode, prev[0],prev[1],x,y)
+            else:
+                c.window.draw_line(self.colour, prev[0],prev[1],x,y)
+            self.line.append([x,y])
+        return
+
+    def movetick(self):
+        # longish pause while drawing
+        self.flush_text()
+        self.selecting = True
+        self.need_redraw = True
+        self.redraw()
+        
+    def tick(self):
+        # nothing for 20 seconds, flush the page
+        self.save_page()
+        gobject.source_remove(self.timeout)
+        self.timeout = None
+
+    def find_text(self, pos):
+        x = pos[0]; y = pos[1] + self.lineascent
+        for i in range(0, len(self.lines)):
+            p = self.lines[i]
+            if type(p[2]) != str:
+                continue
+            if x >= p[1][0] and y >= p[1][1] and y < p[1][1] + self.lineheight:
+                # could be this line - check more precisely
+                layout = self.page.create_pango_layout(p[2]+' ')
+                (ink, log) = layout.get_pixel_extents()
+                (ex,ey,ew,eh) = log
+                if x < p[1][0] + ex or x > p[1][0] + ex + ew  or \
+                   y < p[1][1] + ey or \
+                   y > p[1][1] + ey + self.lineheight :
+                    continue
+                # OK, it is in this one.  Find out where.
+                (index, gr) = layout.xy_to_index((x - p[1][0] - ex) * pango.SCALE,
+                                                 (y - p[1][1] - ey - self.lineheight) * pango.SCALE)
+                if index >= len(p[2]):
+                    index = len(p[2])-1
+                return (i, index)
+        return (None, None)
+
+    def align_text(self, pos):
+        # align pos to existing text.
+        # if pos is near one-line-past a previous text, move the exactly
+        # one-line-past
+        x = pos[0]; y = pos[1] + self.lineascent
+        for l in self.lines:
+            if type(l[2]) != str:
+                continue
+            if abs(x - l[1][0]) > self.lineheight:
+                continue
+            if abs(y - (l[1][1] + self.lineheight)) > self.lineheight:
+                continue
+            return [ l[1][0], l[1][1] + self.lineheight ]
+        return pos
+        
+    def flush_text(self):
+        if self.textstr == None:
+            self.textpos = None
+            return
+        self.setlabel()
+        if len(self.textstr) == 0:
+            self.textstr = None
+            self.textpos = None
+            return
+        l = [self.colourname, self.textpos, self.textstr]
+        self.lines.append(l)
+        self.need_redraw = True
+        self.textstr = None
+        self.textpos = None
+
+    def draw_text(self, pos, colour, str, cursor = None, drawable = None, selected=False):
+        if drawable == None:
+            drawable = self.page.window
+        layout = self.page.create_pango_layout(str)
+        if self.zoomx != None:
+            xs = 2; xo = -self.zoomx
+            ys = 2; yo = -self.zoomy
+        else:
+            xs = 1 ; xo = 0
+            ys = 1 ; yo = 0
+        (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+        if len(pos) == 2 or pos[2] != ew or pos[3] != eh:
+            while len(pos) > 2:
+                pos.pop()
+            pos.append(ew)
+            pos.append(eh)
+        if selected:
+            drawable.draw_rectangle(self.colourmap['yellow'], True,
+                                    ex+pos[0], ey+pos[1]-self.lineascent, ew, eh)
+        drawable.draw_layout(colour, pos[0]*xs+xo,
+                             (pos[1] - self.lineascent)*ys + yo,
+                             layout)
+        if cursor != None:
+            (strong,weak) = layout.get_cursor_pos(cursor)
+            (x,y,width,height) = strong
+            x = pos[0] + x / pango.SCALE
+            y = pos[1]
+            drawable.draw_line(self.colour_textmode,
+                               x*xs+xo,y*ys+yo, (x-3)*xs+xo, (y+3)*ys+yo)
+            drawable.draw_line(self.colour_textmode,
+                               x*xs+xo,y*ys+yo, (x+3)*xs+xo, (y+3)*ys+yo)
+
+    def add_sym(self, sym):
+        if self.textstr == None:
+            return
+        if sym == "<BS>":
+            if self.textcurs > 0:
+                self.textstr = self.textstr[0:self.textcurs-1]+ \
+                               self.textstr[self.textcurs:]
+                self.textcurs -= 1
+        elif sym == "<left>":
+            if self.textcurs > 0:
+                self.textcurs -= 1
+        elif sym == "<right>":
+            if self.textcurs < len(self.textstr):
+                self.textcurs += 1
+        elif sym == "<newline>":
+            tail = self.textstr[self.textcurs:]
+            self.textstr = self.textstr[:self.textcurs]
+            oldpos = self.textpos
+            self.flush_text()
+            self.textcurs = len(tail)
+            self.textstr = tail
+            self.textpos = [ oldpos[0], oldpos[1] +
+                             self.lineheight ]
+        else:
+            self.textstr = self.textstr[0:self.textcurs] + sym + \
+                           self.textstr[self.textcurs:]
+            self.textcurs += 1
+        self.redraw()
+
+
+    def getsym(self):
+        alloc = self.page.get_allocation()
+        pagebb = BBox(Point(0,0))
+        pagebb.add(Point(alloc.width, alloc.height))
+        pagebb.finish(div = 2)
+
+        p = PPath(self.line[1][0], self.line[1][1])
+        for pp in self.line[1:]:
+            p.add(pp[0], pp[1])
+        p.close()
+        patn = p.text()
+        pos = pagebb.relpos(p.bbox)
+        tpos = "mid"
+        if pos < 3:
+            tpos = "top"
+        if pos >= 6:
+            tpos = "bot"
+        sym = self.dict.match(patn, tpos)
+        if sym == None:
+            print "Failed to match pattern:", patn
+        return sym
+
+    def refresh(self, area, ev):
+        self.redraw()
+
+    def setlabel(self):
+        if self.textstr == None:
+            self.name.set_label('Page: ' + self.names[self.pagenum] + ' (draw)')
+        else:
+            self.name.set_label('Page: ' + self.names[self.pagenum] + ' (text)')
+
+    def name_buttons(self):
+        if self.selection:
+            self.col_align.child.set_text('Align')
+            self.undo_join.child.set_text('Join')
+            self.redo_rename.child.set_text('Rename')
+        else:
+            self.col_align.child.set_text(self.colourname)
+            self.undo_join.child.set_text('Undo')
+            self.redo_rename.child.set_text('Redo')
+        
+    def redraw(self):
+        self.setlabel()
+
+        if self.need_redraw:
+            self.draw_buf()
+        self.page.window.draw_drawable(self.bg, self.pixbuf, 0, 0, 0, 0,
+                                       self.width, self.height)
+
+        if self.textstr != None:
+            self.draw_text(self.textpos, self.colour, self.textstr,
+                           self.textcurs)
+
+        return
+    def draw_buf(self):
+        if self.pixbuf == None:
+            alloc = self.page.get_allocation()
+            self.pixbuf = gtk.gdk.Pixmap(self.page.window, alloc.width, alloc.height)
+            self.width = alloc.width
+            self.height = alloc.height
+
+        self.pixbuf.draw_rectangle(self.bg, True, 0, 0,
+                                   self.width, self.height)
+        if self.zoomx != None:
+            xs = 2; xo = -self.zoomx
+            ys = 2; yo = -self.zoomy
+        else:
+            xs = 1 ; xo = 0
+            ys = 1 ; yo = 0
+        self.selection = []
+        for l in self.lines:
+            if l[0] in self.colourmap:
+                col = self.colourmap[l[0]]
+            else:
+                col = self.colourmap['black']
+            st = l[1]
+            if type(l[2]) == list:
+                for p in l[2:]:
+                    self.pixbuf.draw_line(col, st[0]*xs + xo, st[1]*ys + yo,
+                                          p[0]*xs + xo,p[1]* ys + yo)
+                    st = p
+            if type(l[2]) == str:
+                # if this text is 'near' the current line, make it red
+                selected = False
+                if self.selecting and self.line and len(st) == 4:
+                    for p in self.line[1:]:
+                        if p[0] > st[0] and \
+                           p[0] < st[0] + st[2] and \
+                           p[1] > st[1] - self.lineascent and \
+                           p[1] < st[1] - self.lineascent + st[3]:
+                            selected = True
+                            break
+                if selected:
+                    self.selection.append(l)
+                self.draw_text(st, col, l[2], drawable = self.pixbuf,
+                               selected = selected)
+        self.need_redraw = False
+        self.name_buttons()
+        
+
+    def reconfigure(self, w, ev):
+        alloc = w.get_allocation()
+        if self.pixbuf == None:
+            return
+        if alloc.width != self.width or alloc.height != self.height:
+            self.pixbuf = None
+            self.need_redraw = True
+
+
+    def colour_change(self,t):
+        if self.selection:
+            # button is 'join' not 'colour'
+            return self.realign(t)
+        
+        if self.reset_colour and self.colourname != 'black':
+            next = 'black'
+        else:
+            next = 'black'
+            prev = ''
+            for c in self.colnames:
+                if self.colourname == prev:
+                    next = c
+                prev = c
+        self.reset_colour = False
+        self.colourname = next
+        self.colour = self.colourmap[next]
+        t.child.set_text(next)
+        if self.textstr:
+            self.draw_text(self.textpos, self.colour, self.textstr,
+                           self.textcurs)
+            
+        return
+    def text_change(self,t):
+        self.flush_text()
+        return
+    def undo(self,b):
+        if self.selection:
+            return self.join(b)
+        
+        if len(self.lines) == 0:
+            return
+        self.hist.append(self.lines.pop())
+        self.need_redraw = True
+        self.redraw()
+        return
+    def redo(self,b):
+        if self.selection:
+            self.rename(self.selection[0][2])
+            return
+        if len(self.hist) == 0:
+            return
+        self.lines.append(self.hist.pop())
+        self.need_redraw = True
+        self.redraw()
+        return
+    def choose_unique(self, newname):
+        while newname in self.names:
+            new2 = inc_name(newname)
+            if new2 not in self.names:
+                newname = new2
+            elif (newname + ".1") not in self.names:
+                newname = newname + ".1"
+            else:
+                newname = newname + ".0.1"
+
+        return newname
+    def delete(self,b):
+        # hack
+        if self.selection:
+            return self.join(b)
+        self.flush_text()
+        if len(self.names) <= 1:
+            return
+        if len(self.lines) > 0:
+            return
+        self.save_page()
+        nm = self.names[self.pagenum]
+        if nm in self.pages:
+            del self.pages[nm]
+        self.names = self.names[0:self.pagenum] + self.names[self.pagenum+1:]
+        if self.pagenum >= len(self.names):
+            self.pagenum -= 1
+        self.load_page()
+        self.need_redraw = True
+        self.redraw()
+
+        return
+
+    def cmplines(self, a,b):
+        pa = a[1]
+        pb = b[1]
+        if pa[1] != pb[1]:
+            return pa[1] - pb[1]
+        return pa[0] - pb[0]
+    
+    def realign(self, b):
+        self.selection.sort(self.cmplines)
+        x = self.selection[0][1][0]
+        y = self.selection[0][1][1]
+        for i in range(len(self.selection)):
+            self.selection[i][1][0] = x
+            self.selection[i][1][1] = y
+            y += self.lineheight
+        self.need_redraw = True
+        self.redraw()
+
+    def join(self, b):
+        self.selection.sort(self.cmplines)
+        txt = ""
+        for i in range(len(self.selection)):
+            if txt:
+                txt = txt + ' ' + self.selection[i][2]
+            else:
+                txt = self.selection[i][2]
+            self.selection[i][2] = None
+        self.selection[0][2] = txt
+        i = 0;
+        while i <  len(self.lines):
+            if len(self.lines[i]) > 2 and  self.lines[i][2] == None:
+                self.lines = self.lines[:i] + self.lines[i+1:]
+            else:
+                i += 1
+        self.need_redraw = True
+        self.redraw()
+        
+    
+    def rename(self, newname):
+        # Rename current page and rename the file
+        if self.names[self.pagenum] == newname:
+            return
+        self.save_page()
+        newname = self.choose_unique(newname)
+        oldpath = self.page_dir + "/" + self.names[self.pagenum]
+        newpath = self.page_dir + "/" + newname
+        try :
+            os.rename(oldpath, newpath)
+            self.names[self.pagenum] = newname
+            self.names.sort(page_cmp)
+            self.pagenum = self.names.index(newname)
+            self.setlabel()
+        except:
+            pass
+        self.update_list()
+
+    def setname(self,b):
+        if self.textstr:
+            if len(self.textstr) > 0:
+                self.rename(self.textstr)
+                
+    def clear(self,b):
+        while len(self.lines) > 0:
+            self.hist.append(self.lines.pop())
+        self.need_redraw = True
+        self.redraw()
+        return
+
+    def parseline(self, l):
+        # string in "", or num,num.  ':' separates words
+        words = l.strip().split(':')
+        line = []
+        for w in words:
+            if w[0] == '"':
+                w = w[1:-1]
+            elif w.find(',') >= 0:
+                n = w.find(',')
+                x = int(w[:n])
+                y = int(w[n+1:])
+                w = [x,y]
+            line.append(w)
+        return line
+
+    def load_page(self):
+        self.need_redraw = True
+        nm = self.names[self.pagenum]
+        if nm in self.pages:
+            self.lines = self.pages[nm]
+            return
+        self.lines = [];
+        try:
+            f = open(self.page_dir + "/" + self.names[self.pagenum], "r")
+        except:
+            f = None
+        if f:
+            l = f.readline()
+            while len(l) > 0:
+                self.lines.append(self.parseline(l))
+                l = f.readline()
+            f.close()
+        return
+
+    def save_page(self):
+        t = self.textstr; tc = self.textcurs
+        self.flush_text()
+        self.pages[self.names[self.pagenum]] = self.lines
+        tosave = self.lines
+        if t and len(t):
+            # restore the text
+            ln = self.lines[-1]
+            self.lines = self.lines[:-1]
+            self.textpos = ln[1]
+            self.textstr = ln[2]
+            self.textcurs = tc
+
+        fn = self.page_dir + "/" + self.names[self.pagenum]
+        if len(tosave) == 0:
+            try:
+                os.unlink(fn)
+            except:
+                pass
+            return
+        f = open(fn, "w")
+        for l in tosave:
+            start = True
+            if not l:
+                continue
+            for w in l:
+                if not start:
+                    f.write(":")
+                start = False
+                if isinstance(w, str):
+                    f.write('"%s"' % w)
+                elif isinstance(w, list):
+                    f.write("%d,%d" %( w[0],w[1]))
+            f.write("\n")
+        f.close()
+
+def main():
+    gtk.main()
+    return 0
+if __name__ == "__main__":
+    ScribblePad()
+    main()
diff --git a/shop/shop.py b/shop/shop.py
new file mode 100755 (executable)
index 0000000..4e3cfc4
--- /dev/null
@@ -0,0 +1,2210 @@
+#!/usr/bin/env python
+
+#
+# TO FIX
+# - document
+# - use separate hand-writing code
+# - use separate list-select code
+
+
+import sys, os, time
+import pygtk, gtk, pango
+import gobject
+
+###########################################################
+# Writing recognistion code
+import math
+
+global place
+place = 0
+
+def LoadDict(dict):
+    # Upper case.
+    # Where they are like lowercase, we either double
+    # the last stroke (L, J, I) or draw backwards (S, Z, X)
+    # U V are a special case
+
+    dict.add('A', "R(4)6,8")
+    dict.add('B', "R(4)6,4.R(7)1,6")
+    dict.add('B', "R(4)6,4.L(4)2,8.R(7)1,6")
+    dict.add('B', "S(6)7,1.R(4)6,4.R(7)0,6")
+    dict.add('C', "R(4)8,2")
+    dict.add('D', "R(4)6,6")
+    dict.add('E', "L(1)2,8.L(7)2,8")
+    # double the stem for F
+    dict.add('F', "L(4)2,6.S(3)7,1")
+    dict.add('F', "S(1)5,3.S(3)1,7.S(3)7,1")
+
+    dict.add('G', "L(4)2,5.S(8)1,7")
+    dict.add('G', "L(4)2,5.R(8)6,8")
+    # FIXME I need better straight-curve alignment
+    dict.add('H', "S(3)1,7.R(7)6,8.S(5)7,1")
+    dict.add('H', "L(3)0,5.R(7)6,8.S(5)7,1")
+    # capital I is down/up
+    dict.add('I', "S(4)1,7.S(4)7,1")
+
+    # Capital J has a left/right tail
+    dict.add('J', "R(4)1,6.S(7)3,5")
+
+    dict.add('K', "L(4)0,2.R(4)6,6.L(4)2,8")
+
+    # Capital L, like J, doubles the foot
+    dict.add('L', "L(4)0,8.S(7)4,3")
+
+    dict.add('M', "R(3)6,5.R(5)3,8")
+    dict.add('M', "R(3)6,5.L(1)0,2.R(5)3,8")
+
+    dict.add('N', "R(3)6,8.L(5)0,2")
+
+    # Capital O is CW, but can be CCW in special dict
+    dict.add('O', "R(4)1,1", bot='0')
+
+    dict.add('P', "R(4)6,3")
+    dict.add('Q', "R(4)7,7.S(8)0,8")
+
+    dict.add('R', "R(4)6,4.S(8)0,8")
+
+    # S is drawn bottom to top.
+    dict.add('S', "L(7)6,1.R(1)7,2")
+
+    # Double the stem for capital T
+    dict.add('T', "R(4)0,8.S(5)7,1")
+
+    # U is L to R, V is R to L for now
+    dict.add('U', "L(4)0,2")
+    dict.add('V', "R(4)2,0")
+
+    dict.add('W', "R(5)2,3.L(7)8,6.R(3)5,0")
+    dict.add('W', "R(5)2,3.R(3)5,0")
+
+    dict.add('X', "R(4)6,0")
+
+    dict.add('Y',"L(1)0,2.R(5)4,6.S(5)6,2")
+    dict.add('Y',"L(1)0,2.S(5)2,7.S(5)7,2")
+
+    dict.add('Z', "R(4)8,2.L(4)6,0")
+
+    # Lower case
+    dict.add('a', "L(4)2,2.L(5)1,7")
+    dict.add('a', "L(4)2,2.L(5)0,8")
+    dict.add('a', "L(4)2,2.S(5)0,8")
+    dict.add('b', "S(3)1,7.R(7)6,3")
+    dict.add('c', "L(4)2,8", top='C')
+    dict.add('d', "L(4)5,2.S(5)1,7")
+    dict.add('d', "L(4)5,2.L(5)0,8")
+    dict.add('e', "S(4)3,5.L(4)5,8")
+    dict.add('e', "L(4)3,8")
+    dict.add('f', "L(4)2,6", top='F')
+    dict.add('f', "S(1)5,3.S(3)1,7", top='F')
+    dict.add('g', "L(1)2,2.R(4)1,6")
+    dict.add('h', "S(3)1,7.R(7)6,8")
+    dict.add('h', "L(3)0,5.R(7)6,8")
+    dict.add('i', "S(4)1,7", top='I', bot='1')
+    dict.add('j', "R(4)1,6", top='J')
+    dict.add('k', "L(3)0,5.L(7)2,8")
+    dict.add('k', "L(4)0,5.R(7)6,6.L(7)1,8")
+    dict.add('l', "L(4)0,8", top='L')
+    dict.add('l', "S(3)1,7.S(7)3,5", top='L')
+    dict.add('m', "S(3)1,7.R(3)6,8.R(5)6,8")
+    dict.add('m', "L(3)0,2.R(3)6,8.R(5)6,8")
+    dict.add('n', "S(3)1,7.R(4)6,8")
+    dict.add('o', "L(4)1,1", top='O', bot='0')
+    dict.add('p', "S(3)1,7.R(4)6,3")
+    dict.add('q', "L(1)2,2.L(5)1,5")
+    dict.add('q', "L(1)2,2.S(5)1,7.R(8)6,2")
+    dict.add('q', "L(1)2,2.S(5)1,7.S(5)1,7")
+    # FIXME this double 1,7 is due to a gentle where the
+    # second looks like a line because it is narrow.??
+    dict.add('r', "S(3)1,7.R(4)6,2")
+    dict.add('s', "L(1)2,7.R(7)1,6", top='S', bot='5')
+    dict.add('t', "R(4)0,8", top='T', bot='7')
+    dict.add('t', "S(1)3,5.S(5)1,7", top='T', bot='7')
+    dict.add('u', "L(4)0,2.S(5)1,7")
+    dict.add('v', "L(4)0,2.L(2)0,2")
+    dict.add('w', "L(3)0,2.L(5)0,2", top='W')
+    dict.add('w', "L(3)0,5.R(7)6,8.L(5)3,2", top='W')
+    dict.add('w', "L(3)0,5.L(5)3,2", top='W')
+    dict.add('x', "L(4)0,6", top='X')
+    dict.add('y', "L(1)0,2.R(5)4,6", top='Y') # if curved
+    dict.add('y', "L(1)0,2.S(5)2,7", top='Y')
+    dict.add('z', "R(4)0,6.L(4)2,8", top='Z', bot='2')
+
+    # Digits
+    dict.add('0', "L(4)7,7")
+    dict.add('0', "R(4)7,7")
+    dict.add('1', "S(4)7,1")
+    dict.add('2', "R(4)0,6.S(7)3,5")
+    dict.add('2', "R(4)3,6.L(4)2,8")
+    dict.add('3', "R(1)0,6.R(7)1,6")
+    dict.add('4', "L(4)7,5")
+    dict.add('5', "L(1)2,6.R(7)0,3")
+    dict.add('5', "L(1)2,6.L(4)0,8.R(7)0,3")
+    dict.add('6', "L(4)2,3")
+    dict.add('7', "S(1)3,5.R(4)1,6")
+    dict.add('7', "R(4)0,6")
+    dict.add('7', "R(4)0,7")
+    dict.add('8', "L(4)2,8.R(4)4,2.L(3)6,1")
+    dict.add('8', "L(1)2,8.R(7)2,0.L(1)6,1")
+    dict.add('8', "L(0)2,6.R(7)0,1.L(2)6,0")
+    dict.add('8', "R(4)2,6.L(4)4,2.R(5)8,1")
+    dict.add('9', "L(1)2,2.S(5)1,7")
+
+    dict.add(' ', "S(4)3,5")
+    dict.add('<BS>', "S(4)5,3")
+    dict.add('-', "S(4)3,5.S(4)5,3")
+    dict.add('_', "S(4)3,5.S(4)5,3.S(4)3,5")
+    dict.add("<left>", "S(4)5,3.S(3)3,5")
+    dict.add("<right>","S(4)3,5.S(5)5,3")
+    dict.add("<left>", "S(4)7,1.S(1)1,7") # "<up>"
+    dict.add("<right>","S(4)1,7.S(7)7,1") # "<down>"
+    dict.add("<newline>", "S(4)2,6")
+
+
+class DictSegment:
+    # Each segment has for elements:
+    #   direction: Right Straight Left (R=cw, L=ccw)
+    #   location: 0-8.
+    #   start: 0-8
+    #   finish: 0-8
+    # Segments match if there difference at each element
+    # is 0, 1, or 3 (RSL coded as 012)
+    # A difference of 1 required both to be same / 3
+    # On a match, return number of 0s
+    # On non-match, return -1
+    def __init__(self, str):
+        # D(L)S,R
+        # 0123456
+        self.e = [0,0,0,0]
+        if len(str) != 7:
+            raise ValueError
+        if str[1] != '(' or str[3] != ')' or str[5] != ',':
+            raise ValueError
+        if str[0] == 'R':
+            self.e[0] = 0
+        elif str[0] == 'L':
+            self.e[0] = 2
+        elif str[0] == 'S':
+            self.e[0] = 1
+        else:
+            raise ValueError
+
+        self.e[1] = int(str[2])
+        self.e[2] = int(str[4])
+        self.e[3] = int(str[6])
+
+    def match(self, other):
+        cnt = 0
+        for i in range(0,4):
+            diff = abs(self.e[i] - other.e[i])
+            if diff == 0:
+                cnt += 1
+            elif diff == 3:
+                pass
+            elif diff == 1 and (self.e[i]/3 == other.e[i]/3):
+                pass
+            else:
+                return -1
+        return cnt
+
+class DictPattern:
+    # A Dict Pattern is a list of segments.
+    # A parsed pattern matches a dict pattern if
+    # the are the same nubmer of segments and they
+    # all match.  The value of the match is the sum
+    # of the individual matches.
+    # A DictPattern is printers as segments joined by periods.
+    #
+    def __init__(self, str):
+        self.segs = map(DictSegment, str.split("."))
+    def match(self,other):
+        if len(self.segs) != len(other.segs):
+            return -1
+        cnt = 0
+        for i in range(0,len(self.segs)):
+            m = self.segs[i].match(other.segs[i])
+            if m < 0:
+                return m
+            cnt += m
+        return cnt
+
+
+class Dictionary:
+    # The dictionary hold all the pattern for symbols and
+    # performs lookup
+    # Each pattern in the directionary can be associated
+    # with  3 symbols.  One when drawing in middle of screen,
+    # one for top of screen, one for bottom.
+    # Often these will all be the same.
+    # This allows e.g. s and S to have the same pattern in different
+    # location on the touchscreen.
+    # A match requires a unique entry with a match that is better
+    # than any other entry.
+    #
+    def __init__(self):
+        self.dict = []
+    def add(self, sym, pat, top = None, bot = None):
+        if top == None: top = sym
+        if bot == None: bot = sym
+        self.dict.append((DictPattern(pat), sym, top, bot))
+
+    def _match(self, p):
+        max = -1
+        val = None
+        for (ptn, sym, top, bot) in self.dict:
+            cnt = ptn.match(p)
+            if cnt > max:
+                max = cnt
+                val = (sym, top, bot)
+            elif cnt == max:
+                val = None
+        return val
+
+    def match(self, str, pos = "mid"):
+        p = DictPattern(str)
+        m = self._match(p)
+        if m == None:
+            return m
+        (mid, top, bot) = self._match(p)
+        if pos == "top": return top
+        if pos == "bot": return bot
+        return mid
+
+
+class Point:
+    # This represents a point in the path and all the points leading
+    # up to it.  It allows us to find the direction and curvature from
+    # one point to another
+    # We store x,y, and sum/cnt of points so far
+    def __init__(self,x,y) :
+        self.xsum = x
+        self.ysum = y
+        self.x = x
+        self.y = y
+        self.cnt = 1
+
+    def copy(self):
+        n = Point(0,0)
+        n.xsum = self.xsum
+        n.ysum = self.ysum
+        n.x = self.x
+        n.y = self.y
+        n.cnt = self.cnt
+        return n
+
+    def add(self,x,y):
+        if self.x == x and self.y == y:
+            return
+        self.x = x
+        self.y = y
+        self.xsum += x
+        self.ysum += y
+        self.cnt += 1
+
+    def xlen(self,p):
+        return abs(self.x - p.x)
+    def ylen(self,p):
+        return abs(self.y - p.y)
+    def sqlen(self,p):
+        x = self.x - p.x
+        y = self.y - p.y
+        return x*x + y*y
+
+    def xdir(self,p):
+        if self.x > p.x:
+            return 1
+        if self.x < p.x:
+            return -1
+        return 0
+    def ydir(self,p):
+        if self.y > p.y:
+            return 1
+        if self.y < p.y:
+            return -1
+        return 0
+    def curve(self,p):
+        if self.cnt == p.cnt:
+            return 0
+        x1 = p.x ; y1 = p.y
+        (x2,y2) = self.meanpoint(p)
+        x3 = self.x; y3 = self.y
+
+        curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
+        curve = curve * 100 / ((y3-y1)*(y3-y1)
+                               + (x3-x1)*(x3-x1))
+        if curve > 6:
+            return 1
+        if curve < -6:
+            return -1
+        return 0
+
+    def Vcurve(self,p):
+        if self.cnt == p.cnt:
+            return 0
+        x1 = p.x ; y1 = p.y
+        (x2,y2) = self.meanpoint(p)
+        x3 = self.x; y3 = self.y
+
+        curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
+        curve = curve * 100 / ((y3-y1)*(y3-y1)
+                               + (x3-x1)*(x3-x1))
+        return curve
+
+    def meanpoint(self,p):
+        x = (self.xsum - p.xsum) / (self.cnt - p.cnt)
+        y = (self.ysum - p.ysum) / (self.cnt - p.cnt)
+        return (x,y)
+
+    def is_sharp(self,A,C):
+        # Measure the cosine at self between A and C
+        # as A and C could be curve, we take the mean point on
+        # self.A and self.C as the points to find cosine between
+        (ax,ay) = self.meanpoint(A)
+        (cx,cy) = self.meanpoint(C)
+        a = ax-self.x; b=ay-self.y
+        c = cx-self.x; d=cy-self.y
+        x = a*c + b*d
+        y = a*d - b*c
+        h = math.sqrt(x*x+y*y)
+        if h > 0:
+            cs = x*1000/h
+        else:
+            cs = 0
+        return (cs > 900)
+
+class BBox:
+    # a BBox records min/max x/y of some Points and
+    # can subsequently report row, column, pos of each point
+    # can also locate one bbox in another
+
+    def __init__(self, p):
+        self.minx = p.x
+        self.maxx = p.x
+        self.miny = p.y
+        self.maxy = p.y
+
+    def width(self):
+        return self.maxx - self.minx
+    def height(self):
+        return self.maxy - self.miny
+
+    def add(self, p):
+        if p.x > self.maxx:
+            self.maxx = p.x
+        if p.x < self.minx:
+            self.minx = p.x
+
+        if p.y > self.maxy:
+            self.maxy = p.y
+        if p.y < self.miny:
+            self.miny = p.y
+    def finish(self, div = 3):
+        # if aspect ratio is bad, we adjust max/min accordingly
+        # before setting [xy][12].  We don't change self.min/max
+        # as they are used to place stroke in bigger bbox.
+        # Normally divisions are at 1/3 and 2/3. They can be moved
+        # by setting div e.g. 2 = 1/2 and 1/2
+        (minx,miny,maxx,maxy) = (self.minx,self.miny,self.maxx,self.maxy)
+        if (maxx - minx) * 3 < (maxy - miny) * 2:
+            # too narrow
+            mid = int((maxx + minx)/2)
+            halfwidth = int ((maxy - miny)/3)
+            minx = mid - halfwidth
+            maxx = mid + halfwidth
+        if (maxy - miny) * 3 < (maxx - minx) * 2:
+            # too wide
+            mid = int((maxy + miny)/2)
+            halfheight = int ((maxx - minx)/3)
+            miny = mid - halfheight
+            maxy = mid + halfheight
+
+        div1 = div - 1
+        self.x1 = int((div1*minx + maxx)/div)
+        self.x2 = int((minx + div1*maxx)/div)
+        self.y1 = int((div1*miny + maxy)/div)
+        self.y2 = int((miny + div1*maxy)/div)
+
+    def row(self, p):
+        # 0, 1, 2 - top to bottom
+        if p.y <= self.y1:
+            return 0
+        if p.y < self.y2:
+            return 1
+        return 2
+    def col(self, p):
+        if p.x <= self.x1:
+            return 0
+        if p.x < self.x2:
+            return 1
+        return 2
+    def box(self, p):
+        # 0 to 9
+        return self.row(p) * 3 + self.col(p)
+
+    def relpos(self,b):
+        # b is a box within self.  find location 0-8
+        if b.maxx < self.x2 and b.minx < self.x1:
+            x = 0
+        elif b.minx > self.x1 and b.maxx > self.x2:
+            x = 2
+        else:
+            x = 1
+        if b.maxy < self.y2 and b.miny < self.y1:
+            y = 0
+        elif b.miny > self.y1 and b.maxy > self.y2:
+            y = 2
+        else:
+            y = 1
+        return y*3 + x
+
+
+def different(*args):
+    cur = 0
+    for i in args:
+        if cur != 0 and i != 0 and cur != i:
+            return True
+        if cur == 0:
+            cur = i
+    return False
+
+def maxcurve(*args):
+    for i in args:
+        if i != 0:
+            return i
+    return 0
+
+class PPath:
+    # a PPath refines a list of x,y points into a list of Points
+    # The Points mark out segments which end at significant Points
+    # such as inflections and reversals.
+
+    def __init__(self, x,y):
+
+        self.start = Point(x,y)
+        self.mid = Point(x,y)
+        self.curr = Point(x,y)
+        self.list = [ self.start ]
+
+    def add(self, x, y):
+        self.curr.add(x,y)
+
+        if ( (abs(self.mid.xdir(self.start) - self.curr.xdir(self.mid)) == 2) or
+             (abs(self.mid.ydir(self.start) - self.curr.ydir(self.mid)) == 2) or
+             (abs(self.curr.Vcurve(self.start))+2 < abs(self.mid.Vcurve(self.start)))):
+            pass
+        else:
+            self.mid = self.curr.copy()
+
+        if self.curr.xlen(self.mid) > 4 or self.curr.ylen(self.mid) > 4:
+            self.start = self.mid.copy()
+            self.list.append(self.start)
+            self.mid = self.curr.copy()
+
+    def close(self):
+        self.list.append(self.curr)
+
+    def get_sectlist(self):
+        if len(self.list) <= 2:
+            return [[0,self.list]]
+        l = []
+        A = self.list[0]
+        B = self.list[1]
+        s = [A,B]
+        curcurve = B.curve(A)
+        for C in self.list[2:]:
+            cabc = C.curve(A)
+            cab = B.curve(A)
+            cbc = C.curve(B)
+            if B.is_sharp(A,C) and not different(cabc, cab, cbc, curcurve):
+                # B is too pointy, must break here
+                l.append([curcurve, s])
+                s = [B, C]
+                curcurve = cbc
+            elif not different(cabc, cab, cbc, curcurve):
+                # all happy
+                s.append(C)
+                if curcurve == 0:
+                    curcurve = maxcurve(cab, cbc, cabc)
+            elif not different(cabc, cab, cbc)  :
+                # gentle inflection along AB
+                # was: AB goes in old and new section
+                # now: AB only in old section, but curcurve
+                #      preseved.
+                l.append([curcurve,s])
+                s = [A, B, C]
+                curcurve =maxcurve(cab, cbc, cabc)
+            else:
+                # Change of direction at B
+                l.append([curcurve,s])
+                s = [B, C]
+                curcurve = cbc
+
+            A = B
+            B = C
+        l.append([curcurve,s])
+
+        return l
+
+    def remove_shorts(self, bbox):
+        # in self.list, if a point is close to the previous point,
+        # remove it.
+        if len(self.list) <= 2:
+            return
+        w = bbox.width()/10
+        h = bbox.height()/10
+        n = [self.list[0]]
+        leng = w*h*2*2
+        for p in self.list[1:]:
+            l = p.sqlen(n[-1])
+            if l > leng:
+                n.append(p)
+        self.list = n
+
+    def text(self):
+        # OK, we have a list of points with curvature between.
+        # want to divide this into sections.
+        # for each 3 consectutive points ABC curve of ABC and AB and BC
+        # If all the same, they are all in a section.
+        # If not B starts a new section and the old ends on B or C...
+        BB = BBox(self.list[0])
+        for p in self.list:
+            BB.add(p)
+        BB.finish()
+        self.bbox = BB
+        self.remove_shorts(BB)
+        sectlist = self.get_sectlist()
+        t = ""
+        for c, s in sectlist:
+            if c > 0:
+                dr = "R"  # clockwise is to the Right
+            elif c < 0:
+                dr = "L"  # counterclockwise to the Left
+            else:
+                dr = "S"  # straight
+            bb = BBox(s[0])
+            for p in s:
+                bb.add(p)
+            bb.finish()
+            # If  all points are in some row or column, then
+            # line is S
+            rwdiff = False; cldiff = False
+            rw = bb.row(s[0]); cl=bb.col(s[0])
+            for p in s:
+                if bb.row(p) != rw: rwdiff = True
+                if bb.col(p) != cl: cldiff = True
+            if not rwdiff or not cldiff: dr = "S"
+
+            t1 = dr
+            t1 += "(%d)" % BB.relpos(bb)
+            t1 += "%d,%d" % (bb.box(s[0]), bb.box(s[-1]))
+            t += t1 + '.'
+        return t[:-1]
+
+
+
+
+
+class text_input:
+    def __init__(self, page, callout):
+
+        self.page = page
+        self.callout = callout
+        self.colour = None
+        self.line = None
+        self.dict = Dictionary()
+        LoadDict(self.dict)
+
+        page.connect("button_press_event", self.press)
+        page.connect("button_release_event", self.release)
+        page.connect("motion_notify_event", self.motion)
+        page.set_events(page.get_events()
+                        | gtk.gdk.BUTTON_PRESS_MASK
+                        | gtk.gdk.BUTTON_RELEASE_MASK
+                        | gtk.gdk.POINTER_MOTION_MASK
+                        | gtk.gdk.POINTER_MOTION_HINT_MASK)
+
+    def set_colour(self, col):
+        self.colour = col
+    
+    def press(self, c, ev):
+        # Start a new line
+        self.line = [ [int(ev.x), int(ev.y)] ]
+        return
+    def release(self, c, ev):
+        if self.line == None:
+            return
+        if len(self.line) == 1:
+            self.callout('click', ev)
+            self.line = None
+            return
+
+        sym = self.getsym()
+        if sym:
+            self.callout('sym', sym)
+        self.callout('redraw', None)
+        self.line = None
+        return
+
+    def motion(self, c, ev):
+        if self.line:
+            if ev.is_hint:
+                x, y, state = ev.window.get_pointer()
+            else:
+                x = ev.x
+                y = ev.y
+            x = int(x)
+            y = int(y)
+            prev = self.line[-1]
+            if abs(prev[0] - x) < 10 and abs(prev[1] - y) < 10:
+                return
+            if self.colour:
+                c.window.draw_line(self.colour, prev[0],prev[1],x,y)
+            self.line.append([x,y])
+        return
+
+    def getsym(self):
+        alloc = self.page.get_allocation()
+        pagebb = BBox(Point(0,0))
+        pagebb.add(Point(alloc.width, alloc.height))
+        pagebb.finish(div = 2)
+
+        p = PPath(self.line[1][0], self.line[1][1])
+        for pp in self.line[1:]:
+            p.add(pp[0], pp[1])
+        p.close()
+        patn = p.text()
+        pos = pagebb.relpos(p.bbox)
+        tpos = "mid"
+        if pos < 3:
+            tpos = "top"
+        if pos >= 6:
+            tpos = "bot"
+        sym = self.dict.match(patn, tpos)
+        if sym == None:
+            print "Failed to match pattern:", patn
+        return sym
+
+
+
+
+
+########################################################################
+
+
+def extend_array(ra, leng, val=None):
+    while len(ra) <= leng:
+        ra.append(val)
+           
+
+class Prod:
+    # A product that might be purchased
+    # These are stored in a list index by product number
+    def __init__(self, num, line):
+        # line is read from file, or string typed in for new
+        # product in which case it contains no comma.
+        # otherwise "Name,[R|I]{,Ln:m}"
+        self.num = num
+        words = line.split(',')
+        self.name = words[0]
+        self.regular = (len(words) > 1 and words[1] == 'R')
+        self.loc = []
+        for loc in words[2:]:
+            if len(loc) == 0:
+                continue
+            n = loc[1:].split(':')
+            pl = int(n[0])
+            lc = int(n[1])
+            extend_array(self.loc, pl, -1)
+            self.loc[pl] = lc
+
+    def format(self,f):
+        str = "I%d," % self.num
+        str += self.name + ','
+        if self.regular:
+            str += 'R'
+        else:
+            str += 'I'
+        for i in range(len(self.loc)):
+            if self.loc[i] >= 0:
+                str += ",L%d:%d"%(i, self.loc[i])
+        str += '\n'
+        f.write(str)
+
+
+class Purch:
+    # A purchase that could be made
+    # A list of these is the current shopping list.
+    def __init__(self,source):
+        # source is a string read from a file, or
+        # a product being added to the list.
+        if source.__class__ == Prod:
+            self.prod = source.num
+            self.state = 'X'
+            self.comment = ""
+        elif source.__class__ == str:
+            l = source.split(',', 2)
+            self.prod = int(l[0])
+            self.state = l[1]
+            self.comment = l[2]
+        else:
+            raise ValueError
+
+    def format(self, f):
+        str = '%d,%s,%s\n' % (self.prod, self.state, self.comment)
+        f.write(str)
+
+    def loc(self):
+        global place
+        p = products[self.prod]
+        if len(p.loc) <= place:
+            return -1
+        if p.loc[place] == None:
+            return -1
+        return p.loc[place]
+
+    def locord(self):
+        global place
+        p = products[self.prod]
+        if len(p.loc) <= place:
+            return -1
+        if p.loc[place] == -1 or p.loc[place] == None:
+            return -1
+        return locorder[place].index(p.loc[place])
+
+def purch_cmp(a,b):
+    pa = products[a.prod]
+    pb = products[b.prod]
+    la = a.locord()
+    lb = b.locord()
+
+    if la < lb:
+        return -1
+    if la > lb:
+        return 1
+    # same location
+    return cmp(pa.name, pb.name)
+
+
+def parse_places(l):
+    # P,n:name,...
+    w = l.split(',')
+    if w[0] != 'P':
+        return
+    for p in w[1:]:
+        w2 = p.split(':',1)
+        pos = int(w2[0])
+        extend_array(places, pos,0)
+        places[pos] = w2[1]
+
+def parse_locations(l):
+    # Ln,m:loc,m2:loc2,
+    w = l.split(',')
+    if w[0][0] != 'L':
+        return
+    lnum = int(w[0][1:])
+    loc = []
+    order = []
+    for l in w[1:]:
+        w2 = l.split(':',1)
+        pos = int(w2[0])
+        extend_array(loc, pos)
+        loc[pos] = w2[1]
+        order.append(pos)
+    extend_array(locations, lnum)
+    extend_array(locorder, lnum)
+    locations[lnum] = loc
+    locorder[lnum] = order
+
+def parse_item(l):
+    # In,rest
+    w = l.split(',',1)
+    if w[0][0] != 'I':
+        return
+    lnum = int(w[0][1:])
+    itm = Prod(lnum, w[1])
+    extend_array(products, lnum)
+    products[lnum] = itm
+        
+def load_table(f):
+    # read P L and I lines
+    l = f.readline()
+    while len(l) > 0:
+        l = l.strip()
+        if l[0] == 'P':
+            parse_places(l)
+        elif l[0] == 'L':
+            parse_locations(l)
+        elif l[0] == 'I':
+            parse_item(l)
+        l = f.readline()
+
+def save_table(name):
+    try:
+        f = open(name+".new", "w")
+    except:
+        return
+    f.write("P")
+    for i in range(len(places)):
+        f.write(",%d:%s" % (i, places[i]))
+    f.write("\n")
+
+    for i in range(len(places)):
+        f.write("L%d" % i)
+        for j in locorder[i]:
+            f.write(",%d:%s" % (j, locations[i][j]))
+        f.write("\n")
+    for p in products:
+        if p:
+            p.format(f)
+    f.close()
+    os.rename(name+".new", name)
+
+table_timeout = None
+def table_changed():
+    global table_timeout
+    if table_timeout:
+        gobject.source_remove(table_timeout)
+        table_timeout = None
+    table_timeout = gobject.timeout_add(15*1000, table_tick)
+
+def table_tick():
+    global table_timeout
+    if table_timeout:
+        gobject.source_remove(table_timeout)
+        table_timeout = None
+        save_table("Products")
+
+def load_list(f):
+    # Read item,state,comment from file to 'purch' list
+    l = f.readline()
+    while len(l) > 0:
+        l = l.strip()
+        purch.append(Purch(l))
+        l = f.readline()
+
+def save_list(name):
+    try:
+        f = open(name+".new", "w")
+    except:
+        return
+    for p in purch:
+        if p.state != 'X':
+            p.format(f)
+    f.close()
+    os.rename(name+".new", name)
+
+
+list_timeout = None
+def list_changed():
+    global list_timeout
+    if list_timeout:
+        gobject.source_remove(list_timeout)
+        list_timeout = None
+    list_timeout = gobject.timeout_add(15*1000, list_tick)
+
+def list_tick():
+    global list_timeout
+    if list_timeout:
+        gobject.source_remove(list_timeout)
+        list_timeout = None
+        save_list("Purchases")
+
+def merge_list(purch, prod):
+    # add to purch any products not already there
+    have = []
+    for p in purch:
+        extend_array(have, p.prod, False)
+        have[p.prod] = True
+    for p in prod:
+        if p and (p.num >= len(have) or not have[p.num]) :
+            purch.append(Purch(p))
+
+def locname(purch):
+    if purch.loc() < 0:
+        return "Unknown"
+    else:
+        return locations[place][purch.loc()]
+
+class PurchView:
+    # A PurchView is the view on the list of possible purchases.
+    # We draw the names in a DrawingArea
+    # When a name is tapped, we call-out to possibly update it.
+    # We get a callback when:
+    #   item state changes, so we need to change colour
+    #   list (or sort-order) changes so complete redraw is needed
+    #   zoom changes
+    #
+
+    def __init__(self, zoom, callout, entry):
+        p = gtk.DrawingArea()
+        p.show()
+        self.widget = p
+
+        fd = p.get_pango_context().get_font_description()
+        self.fd = fd
+
+        self.callout = callout
+        self.zoom = 0
+        self.set_zoom(zoom)
+        self.pixbuf = None
+        self.width = self.height = 0
+        self.need_redraw = True
+
+        self.colours = None
+
+        self.plist = None
+        self.search = None
+        self.current = None
+        self.gonext = False
+        self.top = None
+        self.all_headers = False
+
+        p.connect("expose-event", self.redraw)
+        p.connect("configure-event", self.reconfig)
+        
+        #p.connect("button_release_event", self.click)
+        p.set_events(gtk.gdk.EXPOSURE_MASK
+                     | gtk.gdk.STRUCTURE_MASK)
+
+        self.entry = entry
+        self.writing = text_input(p, self.stylus)
+
+    def stylus(self, cmd, info):
+        if cmd == "click":
+            self.click(None, info)
+            return
+        if cmd == "redraw":
+            self.widget.queue_draw()
+            return
+        if cmd == "sym":
+
+            if info == "<BS>":
+                self.entry.emit("backspace")
+            elif info == "<newline>":
+                self.entry.emit("activate")
+            else:
+                self.entry.emit("insert-at-cursor",info)
+            #print "Got Sym ", info
+
+    def add_col(self, sym, col):
+        c = gtk.gdk.color_parse(col)
+        gc = self.widget.window.new_gc()
+        gc.set_foreground(self.widget.get_colormap().alloc_color(c))
+        self.colours[sym] = gc
+
+    def set_zoom(self, zoom):
+        if zoom > 50:
+            zoom = 50
+        if zoom < 20:
+            zoom = 20
+        if zoom == self.zoom:
+            return
+        self.need_redraw = True
+        self.zoom = zoom
+        s = pango.SCALE
+        for i in range(zoom):
+            s = s * 11 / 10
+        self.fd.set_absolute_size(s)
+        self.widget.modify_font(self.fd)
+        met = self.widget.get_pango_context().get_metrics(self.fd)
+
+        self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE
+        self.lineascent = met.get_ascent() / pango.SCALE
+        self.widget.queue_draw()
+
+    def set_search(self, str):
+        self.search = str
+        self.need_redraw = True
+        self.widget.queue_draw()
+
+    def reconfig(self, w, ev):
+        alloc = w.get_allocation()
+        if not self.pixbuf:
+            return
+        if alloc.width != self.width or alloc.height != self.height:
+            self.pixbuf = None
+            self.need_redraw = True
+
+    def redraw(self, w, ev):
+        if self.colours == None:
+            self.colours = {}
+            self.add_col('N', "blue")  # need
+            self.add_col('F', "darkgreen") # found
+            self.add_col('C', "red")   # Cannot find
+            self.add_col('R', "orange")# Regular
+            self.add_col('X', "black") # No Need
+            self.add_col(' ', "white") # selected background
+            self.add_col('_', "black") # location separator
+            self.add_col('title', "cyan")
+            self.bg = self.widget.get_style().bg_gc[gtk.STATE_NORMAL]
+            self.writing.set_colour(self.colours['_'])
+
+            
+        if self.need_redraw:
+            self.draw_buf()
+
+        self.widget.window.draw_drawable(self.bg, self.pixbuf, 0, 0, 0, 0,
+                                         self.width, self.height)
+
+    def draw_buf(self):
+        self.need_redraw = False
+        p = self.widget
+        if self.pixbuf == None:
+            alloc = p.get_allocation()
+            self.pixbuf = gtk.gdk.Pixmap(p.window, alloc.width, alloc.height)
+            self.width = alloc.width
+            self.height = alloc.height
+        self.pixbuf.draw_rectangle(self.bg, True, 0, 0,
+                                   self.width, self.height)
+        
+        if self.plist == None:
+            # Empty list, say so.
+            layout = self.widget.create_pango_layout("List Is Empty")
+            self.cols = 1
+            (ink, log) = layout.get_pixel_extents()
+            (ex,ey,ew,eh) = log
+            self.pixbuf.draw_layout(self.colours['X'], (self.width-ew)/2,
+                                    (self.height-eh)/2,
+                                    layout)
+            return
+
+        # find max width and height
+        maxw = 1; maxh = 1; longest = "nothing"
+        curr = None; top = 0
+        visible = []
+        curloc = None
+        for p in self.plist:
+
+            if self.search == None and p.state == 'X':
+                # Don't normally show "noneed" entries
+                if p.prod == self.current and self.gonext:
+                    curr = len(visible)
+                continue
+            if self.search != None and products[p.prod].name.lower().find(self.search.lower()) < 0:
+                # doesn't contain search string
+                continue
+            while p.loc() != curloc:
+                if not self.all_headers:
+                    curloc = p.loc()
+                elif curloc == None:
+                    curloc = -1
+                else:
+                    if curloc < 0:
+                        i = -1
+                    else:
+                        i = locorder[place].index(curloc)
+
+                    if i < len(locorder[place]) - 1:
+                        curloc = locorder[place][i+1]
+                    else:
+                        break
+                if curloc < 0:
+                    locstr = "Unknown"
+                elif curloc < len(locations[place]):
+                    locstr = locations[place][curloc]
+                else:
+                    locstr = None
+
+                if locstr == None:
+                    break
+
+                if self.top == locstr:
+                    top = len(visible)
+                visible.append(locstr)
+
+                layout = self.widget.create_pango_layout(locstr)
+                (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+                if ew > maxw: maxw = ew; longest = products[p.prod].name
+                if eh > maxh: maxh = eh
+
+            if p.prod == self.top:
+                top = len(visible)
+            if curr != None and self.gonext and self.gonext == p.state:
+                self.gonext = False
+                self.current = p.prod
+                self.callout(p, 'auto')
+            if p.prod == self.current:
+                curr = len(visible)
+            visible.append(p)
+            layout = self.widget.create_pango_layout(products[p.prod].name)
+            (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+            if ew > maxw: maxw = ew; longest = products[p.prod].name
+            if eh > maxh: maxh = eh
+        # print "mw=%d mh=%d lh=%d la=%d" % (maxw, maxh, self.lineheight, self.lineascent)
+
+        if self.all_headers:
+            # any following headers with no items visible
+            while True:
+                if curloc == None:
+                    curloc = -1
+                else:
+                    if curloc < 0:
+                        i = -1
+                    else:
+                        i = locorder[place].index(curloc)
+
+                    if i < len(locorder[place]) - 1:
+                        curloc = locorder[place][i+1]
+                    else:
+                        break
+                if curloc < 0:
+                    locstr = "Unknown"
+                elif curloc < len(locations[place]):
+                    locstr = locations[place][curloc]
+                else:
+                    locstr = None
+
+                if locstr == None:
+                    break
+
+                if self.top == locstr:
+                    top = len(visible)
+                visible.append(locstr)
+
+                layout = self.widget.create_pango_layout(locstr)
+                (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+                if ew > maxw: maxw = ew; longest = products[p.prod].name
+                if eh > maxh: maxh = eh
+
+        self.gonext = False
+        truemaxw = maxw
+        maxw = int(maxw * 11/10)
+        if maxh > self.lineheight:
+            self.lineheight = maxh
+        # Find max rows/columns
+        rows = int(self.height / self.lineheight)
+        cols = int(self.width / maxw) 
+        if rows < 1 or cols < 1:
+            if self.zoom > 10:
+                self.set_zoom(self.zoom - 1)
+                self.need_redraw = True
+                self.widget.queue_draw()
+            return
+        #print "rows=%d cols=%s" % (rows,cols)
+        colw = int(self.width / cols)
+        offset = (colw - truemaxw)/2
+        self.offset = offset
+
+        # check 'curr' is in appropriate range and
+        # possibly adjust 'top'.  Then trim visible to top.
+        # Allow one blank line at the end.
+        cells = rows * cols
+        if cells >= len(visible):
+            # display everything
+            top = 0
+        elif curr != None:
+            # make sure curr is in good range
+            if curr - top < rows/3:
+                top = curr - (cells - rows/3)
+                if top < 0:
+                    top = 0
+            if (cells - (curr - top)) < rows/3:
+                top = curr - rows / 3
+            if len(visible) - top < cells-1:
+                top = len(visible) - (cells-1)
+            if top < 0:
+                top = 0
+        else:
+            if len(visible) - top < cells-1:
+                top = len(visible) - (cells-1)
+            if top < 0:
+                top = 0
+                
+        visible = visible[top:]
+        self.top = None
+
+        self.visible = visible
+        self.rows = rows
+        self.cols = cols
+
+        for r in range(rows):
+            for c in range(cols):
+                pos = c*rows+r
+                uline = False
+                if pos < len(visible):
+                    if type(visible[pos]) == str:
+                        strng = visible[pos]
+                        state = 'title'
+                        comment = False
+                        if self.top == None:
+                            self.top = visible[pos]
+                    else:
+                        strng = products[visible[pos].prod].name
+                        uline =  products[visible[pos].prod].regular
+                        state = visible[pos].state
+                        if self.top == None:
+                            self.top = visible[pos].prod
+                        comment = (not not visible[pos].comment)
+                else:
+                    break
+                layout = self.widget.create_pango_layout(strng)
+                if uline:
+                    a = pango.AttrList()
+                    a.insert(pango.AttrUnderline(pango.UNDERLINE_SINGLE,0,len(strng)))
+                    layout.set_attributes(a)
+                if curr != None and c*rows+r == curr - top:
+                    self.pixbuf.draw_rectangle(self.colours[' '], True,
+                                               c*colw, r*self.lineheight,
+                                               colw, self.lineheight)
+                if state == 'title':
+                    self.pixbuf.draw_rectangle(self.colours['_'], True,
+                                               c*colw+2, r*self.lineheight,
+                                               colw-4, self.lineheight)
+                    self.pixbuf.draw_rectangle(self.colours['_'], False,
+                                               c*colw+2, r*self.lineheight,
+                                               colw-4-1, self.lineheight-1)
+
+                if comment:
+                    if offset > self.lineheight * 0.8:
+                        w = int (self.lineheight * 0.8 * 0.9)
+                        o = (offset - w) / 2
+                    else:
+                        w = int(offset*0.9)
+                        o = int((offset-w)/2)
+                    vo = int((self.lineheight-w)/2)
+                    self.pixbuf.draw_rectangle(self.colours[state], True,
+                                               c * colw + o, r * self.lineheight + vo,
+                                               w, w)
+
+                    
+                self.pixbuf.draw_layout(self.colours[state],
+                                        c * colw + offset,
+                                        r * self.lineheight,
+                                        layout)
+
+
+    def click(self, w, ev):
+        cw = self.width / self.cols
+        rh = self.lineheight
+        col = int(ev.x/cw)
+        row = int(ev.y/rh)
+        cell = col * self.rows + row
+        cellpos = ev.x - col * cw
+        pos = 0
+        if cellpos < self.lineheight or cellpos < self.offset:
+            pos = -1
+        elif cellpos > cw - self.lineheight:
+            pos = 1
+        
+        if cell < len(self.visible) and type(self.visible[cell]) != str:
+            if pos == 0:
+                prod = self.visible[cell].prod
+                layout = self.widget.create_pango_layout(products[prod].name)
+                (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+                if cellpos > self.offset + ew:
+                    pos = 1
+            self.callout(self.visible[cell], pos)
+        if cell < len(self.visible) and type(self.visible[cell]) == str:
+            self.callout(self.visible[cell], 'heading')
+
+
+    def set_purch(self, purch):
+        if purch:
+            self.plist = purch
+        self.need_redraw = True
+        self.widget.queue_draw()
+
+    def select(self, item = None, gonext = False):
+        if gonext:
+            self.gonext = gonext
+        elif item != None:
+            self.current = item
+        self.need_redraw = True
+        self.widget.queue_draw()
+
+    def show_headers(self, all):
+        self.all_headers = all
+        self.need_redraw = True
+        self.widget.queue_draw()
+
+def swap_place(a,b):
+    places[a], places[b] = places[b], places[a]
+    locorder[a], locorder[b] = locorder[b], locorder[a]
+    locations[a], locations[b] = locations[b], locations[a]
+    for p in products:
+        if p:
+            extend_array(p.loc, a, -1)
+            extend_array(p.loc, b, -1)
+            p.loc[a], p.loc[b] = p.loc[b], p.loc[a]
+    table_changed()
+
+class ShoppingList:
+
+    def __init__(self):
+        window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+        window.connect("destroy", self.close_application)
+        window.set_title("Shopping List")
+
+        self.purch = None
+        self.isize = gtk.icon_size_register("mine", 40, 40)
+
+        # try to guess where user is so when he tried to change the
+        # location of something, we try 'here' first.
+        # Update this whenever we find something, or set a location.
+        # We clear to -1 when we change place
+        self.current_loc = -1
+
+        vb = gtk.VBox()
+        window.add(vb)
+        vb.show()
+
+        top = gtk.HBox()
+        top.set_size_request(-1,40)
+        vb.pack_start(top, expand=False)
+        top.show()
+
+        glob = gtk.HBox()
+        glob.set_size_request(-1,80)
+        glob.set_homogeneous(True)
+        vb.pack_end(glob, expand=False)
+        glob.show()
+        self.glob_control = glob
+
+        locedit = gtk.HBox()
+        locedit.set_size_request(-1, 80)
+        vb.pack_end(locedit, expand=False)
+        locedit.hide()
+        self.locedit = locedit
+
+        loc = gtk.HBox()
+        loc.set_size_request(-1,80)
+        vb.pack_end(loc, expand=False)
+        loc.hide()
+        self.loc = loc
+
+        placeb = gtk.HBox()
+        placeb.set_size_request(-1,80)
+        vb.pack_end(placeb, expand=False)
+        placeb.hide()
+        self.place = placeb
+
+        filemenu = gtk.HBox()
+        filemenu.set_homogeneous(True)
+        filemenu.set_size_request(-1,80)
+        vb.pack_end(filemenu, expand=False)
+        filemenu.hide()
+        self.filemenu = filemenu
+
+        curr = gtk.HBox()
+        curr.set_size_request(-1,80)
+        curr.set_homogeneous(True)
+        vb.pack_end(curr, expand=False)
+        curr.show()
+        self.curr = curr
+        self.mode = 'curr'
+
+        e = gtk.Entry()
+
+        l = PurchView(34, self.item_selected, e)
+        vb.add(l.widget)
+        self.lview = l
+
+        # multi use text-entry body
+        # used for search-string, comment-entry, item-entry/rename
+        #
+        ctx = e.get_pango_context()
+        fd = ctx.get_font_description()
+        fd.set_absolute_size(25*pango.SCALE)
+        e.modify_font(fd)
+        e.show()
+        top.add(e)
+        self.button(gtk.STOCK_OK, top, self.enter_change, expand = False)
+        self.entry = e
+        global XX
+        XX = e
+        self.entry_ignore = True
+        self.entry_mode = "comment"
+        e.connect("changed", self.entry_changed)
+        self.ecol_search = None
+        self.ecol_comment = None
+        self.ecol_item = None
+        self.ecol_loc = None
+
+        # global control buttons
+        self.button(gtk.STOCK_REFRESH, glob, self.choose_place)
+        self.button(gtk.STOCK_PREFERENCES, glob, self.show_controls)
+        self.search_toggle = self.button(gtk.STOCK_FIND, glob, self.toggle_search, toggle=True)
+        self.button(gtk.STOCK_ADD, glob, self.add_item)
+        self.button(gtk.STOCK_EDIT, glob, self.change_name)
+
+        # buttons to control current entry
+        self.button(gtk.STOCK_APPLY, curr, self.tick)
+        self.button(gtk.STOCK_CANCEL, curr, self.cross)
+        self.button(gtk.STOCK_JUMP_TO, curr, self.choose_loc)
+        self.button(gtk.STOCK_ZOOM_IN, curr, self.zoomin)
+        self.button(gtk.STOCK_ZOOM_OUT, curr, self.zoomout)
+
+        # buttons for whole-list operations
+        self.button(gtk.STOCK_SAVE_AS, filemenu, self.record)
+        self.button(gtk.STOCK_HOME, filemenu, self.reset)
+        a = self.button(gtk.STOCK_CONVERT, filemenu, self.add_regulars)
+        self.add_reg_clicked = False
+        a.connect('leave',self.clear_regulars)
+        self.save_button = self.button(gtk.STOCK_SAVE, filemenu, self.save)
+        self.revert_button = self.button(gtk.STOCK_REVERT_TO_SAVED, filemenu, self.revert_to_saved)
+        self.revert_button.hide()
+        self.button(gtk.STOCK_QUIT, filemenu, self.close_application)
+
+        # Buttons to change location of current entry
+        self.button(gtk.STOCK_GO_BACK, loc, self.prevloc, expand = False)
+        self.curr_loc = self.button("here", loc, self.curr_switch)
+        self.button(gtk.STOCK_GO_FORWARD, loc, self.nextloc, expand = False)
+        l = self.curr_loc.child
+        ctx = l.get_pango_context()
+        fd = ctx.get_font_description()
+        fd.set_absolute_size(25*pango.SCALE)
+        l.modify_font(fd)
+
+        # buttons to edit the current location or place
+        self.button(gtk.STOCK_DELETE, locedit, self.locdelete)
+        self.button(gtk.STOCK_GO_UP, locedit, self.loc_move_up)
+        self.button(gtk.STOCK_GO_DOWN, locedit, self.loc_move_down)
+        self.button(gtk.STOCK_ADD, locedit, self.add_item)
+        self.button(gtk.STOCK_EDIT, locedit, self.change_name)
+        
+        # Buttons to change current 'place'
+        self.button(gtk.STOCK_MEDIA_REWIND, placeb, self.prevplace, expand = False)
+        self.curr_place = self.button("HOME", placeb, self.curr_switch)
+        self.button(gtk.STOCK_MEDIA_FORWARD, placeb, self.nextplace, expand = False)
+        l = self.curr_place.child
+        ctx = l.get_pango_context()
+        fd = ctx.get_font_description()
+        fd.set_absolute_size(25*pango.SCALE)
+        l.modify_font(fd)
+
+        window.set_default_size(480, 640)
+        window.show()
+
+
+    def close_application(self, widget):
+        gtk.main_quit()
+        if table_timeout:
+            table_tick()
+        if list_timeout:
+            list_tick()
+
+    def item_selected(self, purch, pos):
+        if pos == 'heading':
+            if self.mode != 'loc':
+                return
+            newloc = None
+            i = None
+            for i in range(len(locations[place])):
+                if locations[place][i] == purch:
+                    newloc = i
+            if i == None:
+                return
+            extend_array(products[self.purch.prod].loc, place, -1)
+            products[self.purch.prod].loc[place] = newloc
+            table_changed()
+            self.lview.plist.sort(purch_cmp)
+            self.set_loc()
+            self.lview.select()
+            return
+
+        if self.entry_mode == "comment":
+            self.lview.select(purch.prod)
+            self.purch = purch
+            self.entry_ignore = True
+            self.entry.set_text(purch.comment)
+            self.entry_ignore = False
+            self.set_loc()
+            #if pos < 0:
+            #    self.tick(None)
+            #if pos > 0:
+            #    self.cross(None)
+        if self.entry_mode == "search":
+            if pos != 'auto':
+                if purch.state == 'X' or purch.state == 'R':
+                    purch.state = 'N'
+                elif purch.state == 'N':
+                    purch.state = 'X'
+            self.lview.select(purch.prod)
+            self.purch = purch
+            self.set_loc()
+            list_changed()
+
+
+    def entry_changed(self, widget):
+        if self.entry_ignore:
+            return
+        if self.entry_mode == "search":
+            self.lview.set_search(self.entry.get_text())
+        if self.entry_mode == "comment" and self.purch != None:
+            self.purch.comment = self.entry.get_text()
+            list_changed()
+            
+    def set_purch(self, purch):
+        self.lview.set_purch(purch)
+
+    def button(self, name, bar, func, expand = True, toggle = False):
+        if toggle:
+            btn = gtk.ToggleButton()
+        else:
+            btn = gtk.Button()
+        if type(name) == str and name[0:3] != "gtk":
+            if not expand:
+                name = " " + name + " "
+            btn.set_label(name)
+        else:
+            img = gtk.image_new_from_stock(name, self.isize)
+            if not expand:
+                img.set_size_request(80, -1)
+            img.show()
+            btn.add(img)
+        btn.show()
+        bar.pack_start(btn, expand = expand)
+        btn.connect("clicked", func)
+        btn.set_focus_on_click(False)
+        return btn
+
+    def record(self, widget):
+        # Record current purchase file in datestamped storage
+        save_list(time.strftime("purch-%Y%m%d-%H"))
+        
+    def reset(self, widget):
+        # Reset the shopping list.
+        # All regular to noneed
+        # if there were no regular, then
+        #   found -> noneed
+        #   cannot find -> need
+        found = False
+        for p in purch:
+            if p.state == 'R':
+                p.state = 'X'
+                found = True
+        if not found:
+            for p in purch:
+                if p.state == 'F':
+                    p.state = 'X'
+                if p.state == 'C':
+                    p.state = 'N'
+        list_changed()
+        self.lview.select()
+
+    def add_regulars(self, widget):
+        if self.add_reg_clicked:
+            return self.all_regulars(widget)
+        # Mark all regulars (not already selected) as regulars
+        for p in purch:
+            if products[p.prod].regular and p.state == 'X':
+                p.state = 'R'
+        list_changed()
+        self.lview.select()
+        self.add_reg_clicked = True
+
+    def all_regulars(self, widget):
+        # Mark all regulars and don'twant (not already selected) as regulars
+        for p in purch:
+            if p.state == 'X':
+                p.state = 'R'
+        list_changed()
+        self.lview.select()
+    def clear_regulars(self, widget):
+        self.add_reg_clicked = False
+
+    def save(self, widget):
+        table_tick()
+        list_tick()
+        self.curr_switch(widget)
+
+
+    def setecol(self):
+        if self.ecol_search != None:
+            return
+        c = gtk.gdk.color_parse("yellow")
+        self.ecol_search = c
+        
+        c = gtk.gdk.color_parse("white")
+        self.ecol_comment = c
+
+        c = gtk.gdk.color_parse("pink")
+        self.ecol_item = c
+
+        c = gtk.gdk.color_parse('grey90')
+        self.ecol_loc = c
+        
+    def toggle_search(self, widget):
+        self.setecol()
+        if self.entry_mode == "item":
+            self.search_toggle.set_active(False)
+            return
+        if self.entry_mode == "search":
+            self.entry_ignore = True
+            self.entry.set_text(self.purch.comment)
+            self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_comment)
+            self.entry_mode = "comment"
+            self.lview.set_search(None)
+            self.entry_ignore = False
+            return
+        self.entry_mode = "search"
+        self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_search)
+        self.entry_ignore = True
+        self.entry.set_text("")
+        self.lview.set_search("")
+        self.entry_ignore = False
+        self.search_toggle.set_active(True)
+
+    def choose_loc(self, widget):
+        # replace 'curr' buttons with 'loc' buttons
+        if self.purch == None:
+            return
+        self.curr.hide()
+        self.filemenu.hide()
+        self.loc.show()
+        self.locedit.show()
+        self.glob_control.hide()
+        self.mode = 'loc'
+        self.set_loc()
+        self.lview.show_headers(True)
+        
+    def set_loc(self):
+        loc = locname(self.purch)
+        self.current_loc = self.purch.loc()
+        self.curr_loc.child.set_text(places[place]+" / "+loc)
+
+    def curr_switch(self, widget):
+        # set current item to current location, and switch back
+        self.lview.show_headers(False)
+        self.loc.hide()
+        self.locedit.hide()
+        self.glob_control.show()
+        self.place.hide()
+        self.filemenu.hide()
+        self.curr.show()
+        self.mode = 'curr'
+
+    def show_controls(self, widget):
+        if self.mode == 'filemenu':
+            self.curr_switch(widget)
+        else:
+            self.lview.show_headers(False)
+            self.loc.hide()
+            self.place.hide()
+            self.curr.hide()
+            self.locedit.hide()
+            self.glob_control.show()
+            self.filemenu.show()
+            self.mode = 'filemenu'
+        
+
+    def nextloc(self, widget):
+        if self.entry_mode != 'comment':
+            self.enter_change(None)
+        if self.current_loc != -1 and self.current_loc != self.purch.loc():
+            newloc = self.current_loc
+            self.current_loc = -1
+        elif self.purch.loc() < 0:
+            newloc = locorder[place][0]
+        else:
+            i = locorder[place].index(self.purch.loc())
+            if i < len(locorder[place])-1:
+                newloc = locorder[place][i+1]
+            else:
+                return
+
+            
+        if newloc < -1 or newloc >= len(locations[place]):
+            return
+        extend_array(products[self.purch.prod].loc, place, -1)
+        products[self.purch.prod].loc[place] = newloc
+        table_changed()
+        self.lview.plist.sort(purch_cmp)
+        self.set_loc()
+        self.lview.select()
+
+    def prevloc(self, widget):
+        if self.entry_mode != 'comment':
+            self.enter_change(None)
+        if self.current_loc != -1 and self.current_loc != self.purch.loc():
+            newloc = self.current_loc
+            self.current_loc = -1
+        elif self.purch.loc() < 0:
+            return
+        else:
+            i = locorder[place].index(self.purch.loc())
+            if i > 0:
+                newloc = locorder[place][i-1]
+            else:
+                newloc = -1
+
+        if newloc < -1:
+            return
+        extend_array(products[self.purch.prod].loc, place, -1)
+        products[self.purch.prod].loc[place] = newloc
+        table_changed()
+        self.lview.plist.sort(purch_cmp)
+        self.set_loc()
+        self.lview.select()
+
+    def locdelete(self, widget):
+        # merge this location with the previous one
+        # So every product with this location needs to be changed,
+        # and the locorder updated.
+        if self.mode != "loc":
+            return
+        l = self.purch.loc()
+        if l < 0:
+            # cannot delete 'Unknown'
+            return
+        i = locorder[place].index(l)
+        if i == 0:
+            # nothing to merge with
+            return
+        self.backup_table()
+        newl = locorder[place][i-1]
+        for p in products:
+            if p != None:
+                if len(p.loc) > place:
+                    if p.loc[place] == l:
+                        p.loc[place] = newl
+        locorder[place][i:i+1] = []
+
+        table_changed()
+        self.lview.plist.sort(purch_cmp)
+        self.set_loc()
+        self.lview.select()
+
+    def loc_move_up(self, widget):
+        global place
+        if self.mode == "loc":
+            l = self.purch.loc()
+            if l < 0:
+                # Cannot move 'unknown'
+                return
+            i = locorder[place].index(l)
+            if i == 0:
+                # nowhere to move
+                pass
+            else:
+                o = locorder[place][i-1:i+1]
+                locorder[place][i-1:i+1] = [o[1],o[0]]
+                table_changed()
+                self.lview.plist.sort(purch_cmp)
+                self.set_loc()
+                self.lview.select()
+            return
+        if self.mode == 'place':
+            if place > 0:
+                swap_place(place-1, place)
+            self.prevplace(None)
+            
+    def loc_move_down(self, widget):
+        global place
+        if self.mode == "loc":
+            l = self.purch.loc()
+            if l < 0:
+                # Cannot move 'unknown'
+                return
+            i = locorder[place].index(l)
+            if i+1 >= len(locorder[place]):
+                # nowhere to move
+                pass
+            else:
+                o = locorder[place][i:i+2]
+                locorder[place][i:i+2] = [o[1],o[0]]
+                table_changed()
+                self.lview.plist.sort(purch_cmp)
+                self.set_loc()
+                self.lview.select()
+
+        if self.mode == 'place':
+            if place < len(places)-1:
+                swap_place(place, place+1)
+            self.nextplace(None)
+
+
+    def choose_place(self, widget):
+        if self.entry_mode != 'comment':
+            self.enter_change(None)
+        if self.mode == 'place':
+            self.curr_switch(widget)
+            return
+        self.pl_visible = True
+        self.lview.show_headers(False)
+        self.loc.hide()
+        self.locedit.show()
+        self.glob_control.hide()
+        self.curr.hide()
+        self.filemenu.hide()
+        self.mode = 'place'
+        self.place.show()
+        self.set_place()
+
+    def set_place(self):
+        global place
+        if place >= len(places):
+            place = len(places) - 1
+        if place < 0:
+            place = 0
+        if place >= len(places):
+            pl = "Unknown Place"
+        else:
+            pl = places[place]
+        if place == 0:
+            prev = ""
+        else:
+            prev = places[place-1]
+        if place+1 >= len(places):
+            next = ""
+        else:
+            next = places[place+1]
+        spaces="             "
+        self.curr_place.child.set_markup(
+            '<span size="10000">'+prev+'\n</span>'
+            +spaces+pl+'\n'
+            +spaces+spaces+'<span size="10000" rise="5000">'+next+'</span>')
+        self.current_loc = -1
+
+    def nextplace(self, widget):
+        global place
+        if place >= len(places)-1:
+            return
+        place += 1
+        if self.lview.plist:
+            self.lview.plist.sort(purch_cmp)
+        self.set_place()
+        if self.purch:
+            self.lview.select()
+        else:
+            self.lview.set_purch(None)
+
+    def prevplace(self, widget):
+        global place
+        if place <= 0:
+            return
+        place -= 1
+        if self.lview.plist:
+            self.lview.plist.sort(purch_cmp)
+        self.set_place()
+        if self.purch:
+            self.lview.select()
+        else:
+            self.lview.set_purch(None)
+
+
+    def zoomin(self, widget):
+        self.lview.set_zoom(self.lview.zoom+1)
+    def zoomout(self, widget):
+        self.lview.set_zoom(self.lview.zoom-1)
+
+    def tick(self, widget):
+        if self.purch != None:
+            if self.entry_mode == "search":
+                # set to regular
+                products[self.purch.prod].regular = True
+                #self.purch.state = 'R'
+                self.lview.select(gonext=self.purch.state)
+                list_changed(); table_changed()
+                return
+            oldstate = self.purch.state
+            if self.purch.state == 'N': self.purch.state = 'F'
+            elif self.purch.state == 'C': self.purch.state = 'F'
+            elif self.purch.state == 'R': self.purch.state = 'N'
+            elif self.purch.state == 'X': self.purch.state = 'N'
+            if self.purch.state == 'F':
+                self.current_loc = self.purch.loc()
+            self.lview.select(gonext = oldstate)
+            list_changed()
+    def cross(self, widget):
+        if self.purch != None:
+            oldstate = self.purch.state
+            if self.entry_mode == "search":
+                # set to regular
+                products[self.purch.prod].regular = False
+                if self.purch.state == 'R':
+                    self.purch.state = 'X'
+                self.lview.select(gonext=oldstate)
+                list_changed(); table_changed()
+                return
+                
+            if self.purch.state == 'N': self.purch.state = 'C'
+            elif self.purch.state == 'F': self.purch.state = 'N'
+            elif self.purch.state == 'C': self.purch.state = 'X'
+            elif self.purch.state == 'R': self.purch.state = 'X'
+            elif self.purch.state == 'X': self.purch.state = 'X'
+            self.lview.select(gonext = oldstate)
+            list_changed()
+
+    def add_item(self, widget):
+        global place
+        self.setecol()
+        if self.entry_mode == "search":
+            self.search_toggle.set_active(False)
+        if self.entry_mode != "comment":
+            return
+        if self.mode == 'curr':
+            self.entry_mode = "item"
+            self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_item)
+            self.entry_ignore = True
+            self.entry.set_text("")
+            self.purch = None
+            self.lview.select()
+            self.entry_ignore = False
+        elif self.mode == 'loc':
+            if self.purch == None:
+                return
+            if None in locations[place]:
+                lnum = locations[place].index(None)
+            else:
+                lnum = len(locations[place])
+                locations[place].append(None)
+            locations[place][lnum] = 'NewLocation'
+            if self.purch.loc() == -1:
+                so = 0
+            else:
+                so = locorder[place].index(self.purch.loc())+1
+            locorder[place][so:so] = [lnum]
+            self.nextloc(None)
+            self.entry_mode = 'location'
+            self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc)
+            self.entry_ignore = True
+            self.entry.set_text('NewLocation')
+            self.entry.select_region(0,-1)
+            self.entry_ignore = False
+        elif self.mode == 'place':
+            if None in places:
+                pnum = places.index(None)
+            else:
+                pnum = len(places)
+                places.append(None)
+            places[pnum] = 'NewPlace'
+            extend_array(locations, pnum)
+            locations[pnum] = []
+            extend_array(locorder, pnum)
+            locorder[pnum] = []
+            place = pnum
+            self.lview.plist.sort(purch_cmp)
+            self.set_place()
+            self.entry_mode = 'place'
+            self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc)
+            self.entry_ignore = True
+            self.entry.set_text('NewPlace')
+            self.entry_ingore = False
+    def change_name(self, widget):
+        self.setecol()
+        if self.entry_mode == "search":
+            self.search_toggle.set_active(False)
+        if self.entry_mode == "item":
+            if self.purch != None:
+                self.entry.set_text(products[self.purch.prod].name)
+            return
+        if self.entry_mode == "location":
+            if self.purch != None:
+                self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc)
+                self.entry.set_text(locname(self.purch))
+            return
+        if self.entry_mode == 'place':
+            self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc)
+            set.entry.set_text(places[place])
+            return
+        if self.entry_mode != "comment":
+            return
+        if self.mode == 'curr':
+            if self.purch == None:
+                return
+            self.entry_mode = "item"
+            self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_item)
+            self.entry_ignore = True
+            self.entry.set_text(products[self.purch.prod].name)
+            self.entry_ignore = False
+        elif self.mode == 'loc':
+            if self.purch == None:
+                return
+            if self.purch.loc() < 0:
+                return
+            self.entry_mode = "location"
+            self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc)
+            self.entry_ignore = True
+            self.entry.set_text(locname(self.purch))
+            self.entry_ignore = False
+        elif self.mode == 'place':
+            self.entry_mode = 'place'
+            self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc)
+            self.entry_ignore = True
+            self.entry.set_text(places[place])
+            self.entry_inode = False
+
+    #
+    # An item is being added or renamed.  Commit the change
+    # If the new name is empty, that implys a delete.
+    # We only allow the delete if the state is 'X' and not regular
+    def update_item(self, name):
+        if len(name) > 0:
+            if self.purch == None:
+                # check for duplicates FIXME
+                num = len(products)
+                prod = Prod(num, name)
+                products.append(prod)
+                p = Purch(prod)
+                purch.append(p)
+                p.state = "N";
+                self.purch = p
+                self.set_purch(purch)
+                self.lview.select(num)
+                self.lview.plist.sort(purch_cmp)
+            else:
+                products[self.purch.prod].name = name
+                self.lview.select()
+                self.lview.plist.sort(purch_cmp)
+            self.forget_backup()
+            table_changed()
+            list_changed()
+        elif self.purch:
+            # delete?
+            if self.purch.state == 'N':
+                # OK to delete
+                products[self.purch.prod] = None
+                try:
+                    i = purch.index(self.purch)
+                except:
+                    pass
+                else:
+                    if i == 0:
+                        new = -1
+                    else:
+                        new = purch[i-1].prod
+                    del purch[i]
+                    self.lview.plist.sort(purch_cmp)
+                    self.lview.select(new)
+                table_changed()
+                list_changed()
+                self.forget_backup()
+
+    def update_location(self, name):
+        if len(name) > 0:
+            locations[place][self.purch.loc()] = name
+            self.set_loc()
+            self.lview.select()
+            table_changed()
+            return
+        # See if we can delete this location
+        # need to check all products that they aren't 'here'
+        for p in products:
+            if p and p.num != self.purch.prod and place < len(p.loc):
+                if p.loc[place] == self.purch.loc():
+                    return
+        # nothing here except 'purch'
+        l = self.purch.loc()
+        self.prevloc(None)
+        locations[place][l] = None
+        locorder[place].remove(l)
+        self.lview.plist.sort(purch_cmp)
+        self.lview.select()
+        table_changed()
+        list_changed()
+
+    def update_place(self, name):
+        global place
+        if len(name) > 0:
+            places[place] = name
+            self.lview.select()
+            self.set_place()
+            table_changed()
+            return
+        if len(places) <= 1:
+            return
+
+        self.backup_table()
+        places[place:place+1] = []
+        locations[place:place+1] = []
+        locorder[place:place+1] = []
+        if place >= len(places):
+            place -= 1
+        table_changed()
+        self.set_place()
+        self.lview.select()
+
+    def backup_table(self):
+        save_table("Products.backup")
+        self.save_button.hide()
+        self.revert_button.show()
+
+    def forget_backup(self):
+        self.save_button.show()
+        self.revert_button.hide()
+
+    def revert_to_saved(self, widget):
+        try:
+            f = open("Products.backup")
+            products = []
+            locations = []
+            locorder = []
+            places = []
+            load_table(f)
+            f.close()
+        except:
+            pass
+
+        self.forget_backup()
+
+        
+    def enter_change(self, widget):
+        name = self.entry.get_text()
+        mode = self.entry_mode
+        # This is need to avoid recursion as update_* calls {next,prev}loc
+        self.entry_mode = 'comment'
+        if mode == 'item':
+            self.update_item(name)
+        elif mode == 'location':
+            self.update_location(name)
+        elif mode == 'place':
+            self.update_place(name)
+
+        self.entry_mode = "comment"
+        self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_comment)
+        self.entry_ignore = True
+        self.entry.set_text("")
+        self.entry_ignore = False
+        
+def main():
+    gtk.main()
+    return 0
+
+if __name__ == "__main__":
+
+    home = os.getenv("HOME")
+    #p = os.path.join(home, "shopping")
+    p = "/data/shopping"
+    try:
+        os.mkdir(p)
+    except:
+        pass
+    if os.path.exists(p):
+        os.chdir(p)
+    else:
+        os.chdir(home)
+
+    products = []
+    locations = []
+    locorder = []
+    places = []
+    try:
+        f = open("Products")
+        load_table(f)
+        f.close()
+    except:
+        places = ['Home']
+        locorder = [[]]
+        locations =[[]]
+
+    purch = []
+    try:
+        f = open("Purchases")
+    except:
+        pass
+    else:
+        load_list(f)
+        f.close()
+    merge_list(purch, products)
+
+
+    place = 0
+    purch.sort(purch_cmp)
+
+    sl = ShoppingList()
+    sl.set_purch(purch)
+    ss = gtk.settings_get_default()
+    ss.set_long_property("gtk-cursor-blink", 0, "shop")
+    main()
diff --git a/sms/exesms b/sms/exesms
new file mode 100644 (file)
index 0000000..7929411
--- /dev/null
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+
+import urllib, sys, os
+
+def make_url(sender,recipient,mesg):
+       if recipient[0] == '+':
+               recipient = recipient[1:]
+       elif recipient[0:2] != '04':
+               print "Invalid SMS address: " + recipient
+               sys.exit(1)
+               
+       return "https://www.exetel.com.au/sendsms/api_sms.php?username=0293169905&password=birtwhistle&mobilenumber=%s&message=%s&sender=%s&messagetype=Text" % (
+               recipient,
+               urllib.quote(mesg),
+               sender
+               )
+
+
+def send(sender, recipient, mesg):
+       try:
+               f = urllib.urlopen(make_url(sender,recipient, mesg))
+       except:
+               rv = 2
+               print "Cannot connect: " + sys.exc_value.strerror
+       else:
+               rv = 0
+               for l in f:
+                       l = l.strip()
+                       if not l:
+                               continue
+                       f = l.split('|')
+                       if len(f) == 5:
+                               if f[0] != '1':
+                                       rv = 1
+                               m = f[4]
+                               if m[-4:] == '<br>':
+                                       m = m[0:-4]
+                               print m,
+                       else:
+                               rv = 1
+                               print l,
+               print
+       return rv
+
+os.system("/bin/sh /root/pppon")
+ec = send(sys.argv[1], sys.argv[2], sys.argv[3])
+sys.exit(ec)
+
diff --git a/sms/sendsms.py b/sms/sendsms.py
new file mode 100755 (executable)
index 0000000..ad9de15
--- /dev/null
@@ -0,0 +1,1614 @@
+#!/usr/bin/env python
+
+# Create/edit/send/display/search SMS messages.
+# Two main displays:  Create and display
+# Create:
+#   Allow entry of recipient and text of SMS message and allow basic editting
+#    When entering recipient, text box can show address matches for selection
+#     Bottom buttons are "Select"..
+#    When entering text, if there is no text, buttom buttons are:
+#       "Browse", "Close"
+#       If these is some text, bottom buttons are:
+#        "Send", Save"
+#
+# Display:
+#   We usually display a list of messages which can be selected from
+#    There is a 'search' box to restrict message to those with a string
+#   Options for selected message are:
+#     Delete Reply View  Open(for draft)/Forward(for non-draft)
+#   In View mode, the whole text is displayed and the 'View' button becomes "Index"
+#     or "Show List"  or "ReadIt"
+#   General options are:
+#     New Config  List
+#    New goes to Edit
+#
+#   Delete becomes Undelete and can undelete a whole stack.
+#    Delete can become undelete without deleting be press-and-hold
+#
+#
+# Messages are sent using a separate program. e.g. sms-gsm
+# Different recipients can use different programs based on flag in address book.
+# Somehow senders can be configured.
+#   e.g. sms-exetel needs username, password, sender strings.
+#   press-and-hold on the send button allows a sender to be selected.
+#     
+#
+# Send an SMS message using some backend.
+
+# 
+#
+# TODO:
+#   'del' to return to 'list' view
+#   top buttons:  del, view/list, new/open/reply
+#           so can only reply when viewing whole message
+#   Bottom:
+#      all:   sent recv
+#      send:  all  draft
+#      recv:  all new
+#      draft:  all  sent
+#      new:   all recv
+#   DONE handle newline chars in summary
+#   DONE cope properly when the month changes.
+#   switch-to-'new' on 'expose'
+#   'draft' button becomes 'cancel' when all is empty
+#   DONE better display of name/number of destination
+#   jump to list mode when change 'list'
+#   'open' becomes 'reply' when current message was received.
+#   new message becomes non-new when replied to
+#   '<list>' button doesn't select, but just makes choice.
+#      'new' becomes 'select' when <list> has been pressed.
+#   DONE Start in 'read', preferrably 'new' 
+#   DONE always report status from send
+#   DONE draft/new/recv/sent/all  - 5 groups
+#   DONE allow scrolling through list
+#   DONE + prefix to work
+#   DONE compose as 'GSM' or 'EXE' send
+#   DONE somehow do addressbook lookup for compose
+#   DONE addressbook lookup for display
+#   On 'send' move to 'sent' (not draft) and display list
+#   When open 'draft', delete from drafts... or later..
+#   When 'reply' to new message, make it not 'new'
+#
+#   get 'cut' to work from phone number entry.
+#   how to configure sender...
+#   need to select 'number only' mode for entry
+#   need drop-down of common numbers
+#   DONE text wrapping
+#   punctuation
+#   faster text input!!!
+#   DONE status message of transmission
+#   DONE maybe go to 'past messages' on send - need to go somewhere
+#   cut from other sources??
+#   DONE scroll if message is too long!
+#
+#   DONE reread sms file when changing view
+#   Don't add drafts that have not been changed... or
+#   When opening a draft, delete it... or replace when re-add
+#   DONE when sending a message, store - as draft if send failed
+#   DONE show the 'send' status somewhere
+#   DONE add a 'new' button from 'list' to 'send'
+#   Need 'reply' button.. Make 'open' show 'reply' when 'to' me.
+#   Scroll when near top or bottom
+#   hide status line when not needed.
+#   searching mesg list
+#   'folder' view - by month or day
+#   highlight 'new' and 'draft' messages in different colour
+#   support 'sent' and 'received' distinction
+#   when return from viewing a 'new' message, clear the 'new' status
+#   enable starting in 'listing/New' mode
+
+import gtk, pango
+import sys, time, os, re
+import struct
+from subprocess import Popen, PIPE
+from storesms import SMSstore, SMSmesg
+import dnotify
+
+###########################################################
+# Writing recognistion code
+import math
+
+
+def LoadDict(dict):
+    # Upper case.
+    # Where they are like lowercase, we either double
+    # the last stroke (L, J, I) or draw backwards (S, Z, X)
+    # U V are a special case
+
+    dict.add('A', "R(4)6,8")
+    dict.add('B', "R(4)6,4.R(7)1,6")
+    dict.add('B', "R(4)6,4.L(4)2,8.R(7)1,6")
+    dict.add('B', "S(6)7,1.R(4)6,4.R(7)0,6")
+    dict.add('C', "R(4)8,2")
+    dict.add('D', "R(4)6,6")
+    dict.add('E', "L(1)2,8.L(7)2,8")
+    # double the stem for F
+    dict.add('F', "L(4)2,6.S(3)7,1")
+    dict.add('F', "S(1)5,3.S(3)1,7.S(3)7,1")
+
+    dict.add('G', "L(4)2,5.S(8)1,7")
+    dict.add('G', "L(4)2,5.R(8)6,8")
+    # FIXME I need better straight-curve alignment
+    dict.add('H', "S(3)1,7.R(7)6,8.S(5)7,1")
+    dict.add('H', "L(3)0,5.R(7)6,8.S(5)7,1")
+    # capital I is down/up
+    dict.add('I', "S(4)1,7.S(4)7,1")
+
+    # Capital J has a left/right tail
+    dict.add('J', "R(4)1,6.S(7)3,5")
+
+    dict.add('K', "L(4)0,2.R(4)6,6.L(4)2,8")
+
+    # Capital L, like J, doubles the foot
+    dict.add('L', "L(4)0,8.S(7)4,3")
+
+    dict.add('M', "R(3)6,5.R(5)3,8")
+    dict.add('M', "R(3)6,5.L(1)0,2.R(5)3,8")
+
+    dict.add('N', "R(3)6,8.L(5)0,2")
+
+    # Capital O is CW, but can be CCW in special dict
+    dict.add('O', "R(4)1,1", bot='0')
+
+    dict.add('P', "R(4)6,3")
+    dict.add('Q', "R(4)7,7.S(8)0,8")
+
+    dict.add('R', "R(4)6,4.S(8)0,8")
+
+    # S is drawn bottom to top.
+    dict.add('S', "L(7)6,1.R(1)7,2")
+
+    # Double the stem for capital T
+    dict.add('T', "R(4)0,8.S(5)7,1")
+
+    # U is L to R, V is R to L for now
+    dict.add('U', "L(4)0,2")
+    dict.add('V', "R(4)2,0")
+
+    dict.add('W', "R(5)2,3.L(7)8,6.R(3)5,0")
+    dict.add('W', "R(5)2,3.R(3)5,0")
+
+    dict.add('X', "R(4)6,0")
+
+    dict.add('Y',"L(1)0,2.R(5)4,6.S(5)6,2")
+    dict.add('Y',"L(1)0,2.S(5)2,7.S(5)7,2")
+
+    dict.add('Z', "R(4)8,2.L(4)6,0")
+
+    # Lower case
+    dict.add('a', "L(4)2,2.L(5)1,7")
+    dict.add('a', "L(4)2,2.L(5)0,8")
+    dict.add('a', "L(4)2,2.S(5)0,8")
+    dict.add('b', "S(3)1,7.R(7)6,3")
+    dict.add('c', "L(4)2,8", top='C')
+    dict.add('d', "L(4)5,2.S(5)1,7")
+    dict.add('d', "L(4)5,2.L(5)0,8")
+    dict.add('e', "S(4)3,5.L(4)5,8")
+    dict.add('e', "L(4)3,8")
+    dict.add('f', "L(4)2,6", top='F')
+    dict.add('f', "S(1)5,3.S(3)1,7", top='F')
+    dict.add('g', "L(1)2,2.R(4)1,6")
+    dict.add('h', "S(3)1,7.R(7)6,8")
+    dict.add('h', "L(3)0,5.R(7)6,8")
+    dict.add('i', "S(4)1,7", top='I', bot='1')
+    dict.add('j', "R(4)1,6", top='J')
+    dict.add('k', "L(3)0,5.L(7)2,8")
+    dict.add('k', "L(4)0,5.R(7)6,6.L(7)1,8")
+    dict.add('l', "L(4)0,8", top='L')
+    dict.add('l', "S(3)1,7.S(7)3,5", top='L')
+    dict.add('m', "S(3)1,7.R(3)6,8.R(5)6,8")
+    dict.add('m', "L(3)0,2.R(3)6,8.R(5)6,8")
+    dict.add('n', "S(3)1,7.R(4)6,8")
+    dict.add('o', "L(4)1,1", top='O', bot='0')
+    dict.add('p', "S(3)1,7.R(4)6,3")
+    dict.add('q', "L(1)2,2.L(5)1,5")
+    dict.add('q', "L(1)2,2.S(5)1,7.R(8)6,2")
+    dict.add('q', "L(1)2,2.S(5)1,7.S(5)1,7")
+    # FIXME this double 1,7 is due to a gentle where the
+    # second looks like a line because it is narrow.??
+    dict.add('r', "S(3)1,7.R(4)6,2")
+    dict.add('s', "L(1)2,7.R(7)1,6", top='S', bot='5')
+    dict.add('t', "R(4)0,8", top='T', bot='7')
+    dict.add('t', "S(1)3,5.S(5)1,7", top='T', bot='7')
+    dict.add('u', "L(4)0,2.S(5)1,7")
+    dict.add('v', "L(4)0,2.L(2)0,2")
+    dict.add('w', "L(3)0,2.L(5)0,2", top='W')
+    dict.add('w', "L(3)0,5.R(7)6,8.L(5)3,2", top='W')
+    dict.add('w', "L(3)0,5.L(5)3,2", top='W')
+    dict.add('x', "L(4)0,6", top='X')
+    dict.add('y', "L(1)0,2.R(5)4,6", top='Y') # if curved
+    dict.add('y', "L(1)0,2.S(5)2,7", top='Y')
+    dict.add('z', "R(4)0,6.L(4)2,8", top='Z', bot='2')
+
+    # Digits
+    dict.add('0', "L(4)7,7")
+    dict.add('0', "R(4)7,7")
+    dict.add('1', "S(4)7,1")
+    dict.add('2', "R(4)0,6.S(7)3,5")
+    dict.add('2', "R(4)3,6.L(4)2,8")
+    dict.add('3', "R(1)0,6.R(7)1,6")
+    dict.add('4', "L(4)7,5")
+    dict.add('5', "L(1)2,6.R(7)0,3")
+    dict.add('5', "L(1)2,6.L(4)0,8.R(7)0,3")
+    dict.add('6', "L(4)2,3")
+    dict.add('7', "S(1)3,5.R(4)1,6")
+    dict.add('7', "R(4)0,6")
+    dict.add('7', "R(4)0,7")
+    dict.add('8', "L(4)2,8.R(4)4,2.L(3)6,1")
+    dict.add('8', "L(1)2,8.R(7)2,0.L(1)6,1")
+    dict.add('8', "L(0)2,6.R(7)0,1.L(2)6,0")
+    dict.add('8', "R(4)2,6.L(4)4,2.R(5)8,1")
+    dict.add('9', "L(1)2,2.S(5)1,7")
+
+    dict.add(' ', "S(4)3,5")
+    dict.add('<BS>', "S(4)5,3")
+    dict.add('-', "S(4)3,5.S(4)5,3")
+    dict.add('_', "S(4)3,5.S(4)5,3.S(4)3,5")
+    dict.add("<left>", "S(4)5,3.S(3)3,5")
+    dict.add("<right>","S(4)3,5.S(5)5,3")
+    dict.add("<left>", "S(4)7,1.S(1)1,7") # "<up>"
+    dict.add("<right>","S(4)1,7.S(7)7,1") # "<down>"
+    dict.add("<newline>", "S(4)2,6")
+
+
+class DictSegment:
+    # Each segment has for elements:
+    #   direction: Right Straight Left (R=cw, L=ccw)
+    #   location: 0-8.
+    #   start: 0-8
+    #   finish: 0-8
+    # Segments match if there difference at each element
+    # is 0, 1, or 3 (RSL coded as 012)
+    # A difference of 1 required both to be same / 3
+    # On a match, return number of 0s
+    # On non-match, return -1
+    def __init__(self, str):
+        # D(L)S,R
+        # 0123456
+        self.e = [0,0,0,0]
+        if len(str) != 7:
+            raise ValueError
+        if str[1] != '(' or str[3] != ')' or str[5] != ',':
+            raise ValueError
+        if str[0] == 'R':
+            self.e[0] = 0
+        elif str[0] == 'L':
+            self.e[0] = 2
+        elif str[0] == 'S':
+            self.e[0] = 1
+        else:
+            raise ValueError
+
+        self.e[1] = int(str[2])
+        self.e[2] = int(str[4])
+        self.e[3] = int(str[6])
+
+    def match(self, other):
+        cnt = 0
+        for i in range(0,4):
+            diff = abs(self.e[i] - other.e[i])
+            if diff == 0:
+                cnt += 1
+            elif diff == 3:
+                pass
+            elif diff == 1 and (self.e[i]/3 == other.e[i]/3):
+                pass
+            else:
+                return -1
+        return cnt
+
+class DictPattern:
+    # A Dict Pattern is a list of segments.
+    # A parsed pattern matches a dict pattern if
+    # the are the same nubmer of segments and they
+    # all match.  The value of the match is the sum
+    # of the individual matches.
+    # A DictPattern is printers as segments joined by periods.
+    #
+    def __init__(self, str):
+        self.segs = map(DictSegment, str.split("."))
+    def match(self,other):
+        if len(self.segs) != len(other.segs):
+            return -1
+        cnt = 0
+        for i in range(0,len(self.segs)):
+            m = self.segs[i].match(other.segs[i])
+            if m < 0:
+                return m
+            cnt += m
+        return cnt
+
+
+class Dictionary:
+    # The dictionary hold all the pattern for symbols and
+    # performs lookup
+    # Each pattern in the directionary can be associated
+    # with  3 symbols.  One when drawing in middle of screen,
+    # one for top of screen, one for bottom.
+    # Often these will all be the same.
+    # This allows e.g. s and S to have the same pattern in different
+    # location on the touchscreen.
+    # A match requires a unique entry with a match that is better
+    # than any other entry.
+    #
+    def __init__(self):
+        self.dict = []
+    def add(self, sym, pat, top = None, bot = None):
+        if top == None: top = sym
+        if bot == None: bot = sym
+        self.dict.append((DictPattern(pat), sym, top, bot))
+
+    def _match(self, p):
+        max = -1
+        val = None
+        for (ptn, sym, top, bot) in self.dict:
+            cnt = ptn.match(p)
+            if cnt > max:
+                max = cnt
+                val = (sym, top, bot)
+            elif cnt == max:
+                val = None
+        return val
+
+    def match(self, str, pos = "mid"):
+        p = DictPattern(str)
+        m = self._match(p)
+        if m == None:
+            return m
+        (mid, top, bot) = self._match(p)
+        if pos == "top": return top
+        if pos == "bot": return bot
+        return mid
+
+
+class Point:
+    # This represents a point in the path and all the points leading
+    # up to it.  It allows us to find the direction and curvature from
+    # one point to another
+    # We store x,y, and sum/cnt of points so far
+    def __init__(self,x,y) :
+        self.xsum = x
+        self.ysum = y
+        self.x = x
+        self.y = y
+        self.cnt = 1
+
+    def copy(self):
+        n = Point(0,0)
+        n.xsum = self.xsum
+        n.ysum = self.ysum
+        n.x = self.x
+        n.y = self.y
+        n.cnt = self.cnt
+        return n
+
+    def add(self,x,y):
+        if self.x == x and self.y == y:
+            return
+        self.x = x
+        self.y = y
+        self.xsum += x
+        self.ysum += y
+        self.cnt += 1
+
+    def xlen(self,p):
+        return abs(self.x - p.x)
+    def ylen(self,p):
+        return abs(self.y - p.y)
+    def sqlen(self,p):
+        x = self.x - p.x
+        y = self.y - p.y
+        return x*x + y*y
+
+    def xdir(self,p):
+        if self.x > p.x:
+            return 1
+        if self.x < p.x:
+            return -1
+        return 0
+    def ydir(self,p):
+        if self.y > p.y:
+            return 1
+        if self.y < p.y:
+            return -1
+        return 0
+    def curve(self,p):
+        if self.cnt == p.cnt:
+            return 0
+        x1 = p.x ; y1 = p.y
+        (x2,y2) = self.meanpoint(p)
+        x3 = self.x; y3 = self.y
+
+        curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
+        curve = curve * 100 / ((y3-y1)*(y3-y1)
+                               + (x3-x1)*(x3-x1))
+        if curve > 6:
+            return 1
+        if curve < -6:
+            return -1
+        return 0
+
+    def Vcurve(self,p):
+        if self.cnt == p.cnt:
+            return 0
+        x1 = p.x ; y1 = p.y
+        (x2,y2) = self.meanpoint(p)
+        x3 = self.x; y3 = self.y
+
+        curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
+        curve = curve * 100 / ((y3-y1)*(y3-y1)
+                               + (x3-x1)*(x3-x1))
+        return curve
+
+    def meanpoint(self,p):
+        x = (self.xsum - p.xsum) / (self.cnt - p.cnt)
+        y = (self.ysum - p.ysum) / (self.cnt - p.cnt)
+        return (x,y)
+
+    def is_sharp(self,A,C):
+        # Measure the cosine at self between A and C
+        # as A and C could be curve, we take the mean point on
+        # self.A and self.C as the points to find cosine between
+        (ax,ay) = self.meanpoint(A)
+        (cx,cy) = self.meanpoint(C)
+        a = ax-self.x; b=ay-self.y
+        c = cx-self.x; d=cy-self.y
+        x = a*c + b*d
+        y = a*d - b*c
+        h = math.sqrt(x*x+y*y)
+        if h > 0:
+            cs = x*1000/h
+        else:
+            cs = 0
+        return (cs > 900)
+
+class BBox:
+    # a BBox records min/max x/y of some Points and
+    # can subsequently report row, column, pos of each point
+    # can also locate one bbox in another
+
+    def __init__(self, p):
+        self.minx = p.x
+        self.maxx = p.x
+        self.miny = p.y
+        self.maxy = p.y
+
+    def width(self):
+        return self.maxx - self.minx
+    def height(self):
+        return self.maxy - self.miny
+
+    def add(self, p):
+        if p.x > self.maxx:
+            self.maxx = p.x
+        if p.x < self.minx:
+            self.minx = p.x
+
+        if p.y > self.maxy:
+            self.maxy = p.y
+        if p.y < self.miny:
+            self.miny = p.y
+    def finish(self, div = 3):
+        # if aspect ratio is bad, we adjust max/min accordingly
+        # before setting [xy][12].  We don't change self.min/max
+        # as they are used to place stroke in bigger bbox.
+        # Normally divisions are at 1/3 and 2/3. They can be moved
+        # by setting div e.g. 2 = 1/2 and 1/2
+        (minx,miny,maxx,maxy) = (self.minx,self.miny,self.maxx,self.maxy)
+        if (maxx - minx) * 3 < (maxy - miny) * 2:
+            # too narrow
+            mid = int((maxx + minx)/2)
+            halfwidth = int ((maxy - miny)/3)
+            minx = mid - halfwidth
+            maxx = mid + halfwidth
+        if (maxy - miny) * 3 < (maxx - minx) * 2:
+            # too wide
+            mid = int((maxy + miny)/2)
+            halfheight = int ((maxx - minx)/3)
+            miny = mid - halfheight
+            maxy = mid + halfheight
+
+        div1 = div - 1
+        self.x1 = int((div1*minx + maxx)/div)
+        self.x2 = int((minx + div1*maxx)/div)
+        self.y1 = int((div1*miny + maxy)/div)
+        self.y2 = int((miny + div1*maxy)/div)
+
+    def row(self, p):
+        # 0, 1, 2 - top to bottom
+        if p.y <= self.y1:
+            return 0
+        if p.y < self.y2:
+            return 1
+        return 2
+    def col(self, p):
+        if p.x <= self.x1:
+            return 0
+        if p.x < self.x2:
+            return 1
+        return 2
+    def box(self, p):
+        # 0 to 9
+        return self.row(p) * 3 + self.col(p)
+
+    def relpos(self,b):
+        # b is a box within self.  find location 0-8
+        if b.maxx < self.x2 and b.minx < self.x1:
+            x = 0
+        elif b.minx > self.x1 and b.maxx > self.x2:
+            x = 2
+        else:
+            x = 1
+        if b.maxy < self.y2 and b.miny < self.y1:
+            y = 0
+        elif b.miny > self.y1 and b.maxy > self.y2:
+            y = 2
+        else:
+            y = 1
+        return y*3 + x
+
+
+def different(*args):
+    cur = 0
+    for i in args:
+        if cur != 0 and i != 0 and cur != i:
+            return True
+        if cur == 0:
+            cur = i
+    return False
+
+def maxcurve(*args):
+    for i in args:
+        if i != 0:
+            return i
+    return 0
+
+class PPath:
+    # a PPath refines a list of x,y points into a list of Points
+    # The Points mark out segments which end at significant Points
+    # such as inflections and reversals.
+
+    def __init__(self, x,y):
+
+        self.start = Point(x,y)
+        self.mid = Point(x,y)
+        self.curr = Point(x,y)
+        self.list = [ self.start ]
+
+    def add(self, x, y):
+        self.curr.add(x,y)
+
+        if ( (abs(self.mid.xdir(self.start) - self.curr.xdir(self.mid)) == 2) or
+             (abs(self.mid.ydir(self.start) - self.curr.ydir(self.mid)) == 2) or
+             (abs(self.curr.Vcurve(self.start))+2 < abs(self.mid.Vcurve(self.start)))):
+            pass
+        else:
+            self.mid = self.curr.copy()
+
+        if self.curr.xlen(self.mid) > 4 or self.curr.ylen(self.mid) > 4:
+            self.start = self.mid.copy()
+            self.list.append(self.start)
+            self.mid = self.curr.copy()
+
+    def close(self):
+        self.list.append(self.curr)
+
+    def get_sectlist(self):
+        if len(self.list) <= 2:
+            return [[0,self.list]]
+        l = []
+        A = self.list[0]
+        B = self.list[1]
+        s = [A,B]
+        curcurve = B.curve(A)
+        for C in self.list[2:]:
+            cabc = C.curve(A)
+            cab = B.curve(A)
+            cbc = C.curve(B)
+            if B.is_sharp(A,C) and not different(cabc, cab, cbc, curcurve):
+                # B is too pointy, must break here
+                l.append([curcurve, s])
+                s = [B, C]
+                curcurve = cbc
+            elif not different(cabc, cab, cbc, curcurve):
+                # all happy
+                s.append(C)
+                if curcurve == 0:
+                    curcurve = maxcurve(cab, cbc, cabc)
+            elif not different(cabc, cab, cbc)  :
+                # gentle inflection along AB
+                # was: AB goes in old and new section
+                # now: AB only in old section, but curcurve
+                #      preseved.
+                l.append([curcurve,s])
+                s = [A, B, C]
+                curcurve =maxcurve(cab, cbc, cabc)
+            else:
+                # Change of direction at B
+                l.append([curcurve,s])
+                s = [B, C]
+                curcurve = cbc
+
+            A = B
+            B = C
+        l.append([curcurve,s])
+
+        return l
+
+    def remove_shorts(self, bbox):
+        # in self.list, if a point is close to the previous point,
+        # remove it.
+        if len(self.list) <= 2:
+            return
+        w = bbox.width()/10
+        h = bbox.height()/10
+        n = [self.list[0]]
+        leng = w*h*2*2
+        for p in self.list[1:]:
+            l = p.sqlen(n[-1])
+            if l > leng:
+                n.append(p)
+        self.list = n
+
+    def text(self):
+        # OK, we have a list of points with curvature between.
+        # want to divide this into sections.
+        # for each 3 consectutive points ABC curve of ABC and AB and BC
+        # If all the same, they are all in a section.
+        # If not B starts a new section and the old ends on B or C...
+        BB = BBox(self.list[0])
+        for p in self.list:
+            BB.add(p)
+        BB.finish()
+        self.bbox = BB
+        self.remove_shorts(BB)
+        sectlist = self.get_sectlist()
+        t = ""
+        for c, s in sectlist:
+            if c > 0:
+                dr = "R"  # clockwise is to the Right
+            elif c < 0:
+                dr = "L"  # counterclockwise to the Left
+            else:
+                dr = "S"  # straight
+            bb = BBox(s[0])
+            for p in s:
+                bb.add(p)
+            bb.finish()
+            # If  all points are in some row or column, then
+            # line is S
+            rwdiff = False; cldiff = False
+            rw = bb.row(s[0]); cl=bb.col(s[0])
+            for p in s:
+                if bb.row(p) != rw: rwdiff = True
+                if bb.col(p) != cl: cldiff = True
+            if not rwdiff or not cldiff: dr = "S"
+
+            t1 = dr
+            t1 += "(%d)" % BB.relpos(bb)
+            t1 += "%d,%d" % (bb.box(s[0]), bb.box(s[-1]))
+            t += t1 + '.'
+        return t[:-1]
+
+
+class text_input:
+    def __init__(self, page, callout):
+
+        self.page = page
+        self.callout = callout
+        self.colour = None
+        self.line = None
+        self.dict = Dictionary()
+        self.active = True
+        LoadDict(self.dict)
+
+        page.connect("button_press_event", self.press)
+        page.connect("button_release_event", self.release)
+        page.connect("motion_notify_event", self.motion)
+        page.set_events(page.get_events()
+                        | gtk.gdk.BUTTON_PRESS_MASK
+                        | gtk.gdk.BUTTON_RELEASE_MASK
+                        |  gtk.gdk.POINTER_MOTION_MASK
+                        | gtk.gdk.POINTER_MOTION_HINT_MASK)
+
+    def set_colour(self, col):
+        self.colour = col
+    
+    def press(self, c, ev):
+        if not self.active:
+            return
+        # Start a new line
+        self.line = [ [int(ev.x), int(ev.y)] ]
+        if not ev.send_event:
+            self.page.stop_emission('button_press_event')
+        return
+    def release(self, c, ev):
+        if self.line == None:
+            return
+        if len(self.line) == 1:
+            self.callout('click', ev)
+            self.line = None
+            return
+
+        sym = self.getsym()
+        if sym:
+            self.callout('sym', sym)
+        self.callout('redraw', None)
+        self.line = None
+        return
+
+    def motion(self, c, ev):
+        if self.line:
+            if ev.is_hint:
+                x, y, state = ev.window.get_pointer()
+            else:
+                x = ev.x
+                y = ev.y
+            x = int(x)
+            y = int(y)
+            prev = self.line[-1]
+            if abs(prev[0] - x) < 10 and abs(prev[1] - y) < 10:
+                return
+            if self.colour:
+                c.window.draw_line(self.colour, prev[0],prev[1],x,y)
+            self.line.append([x,y])
+        return
+
+    def getsym(self):
+        alloc = self.page.get_allocation()
+        pagebb = BBox(Point(0,0))
+        pagebb.add(Point(alloc.width, alloc.height))
+        pagebb.finish(div = 2)
+
+        p = PPath(self.line[1][0], self.line[1][1])
+        for pp in self.line[1:]:
+            p.add(pp[0], pp[1])
+        p.close()
+        patn = p.text()
+        pos = pagebb.relpos(p.bbox)
+        tpos = "mid"
+        if pos < 3:
+            tpos = "top"
+        if pos >= 6:
+            tpos = "bot"
+        sym = self.dict.match(patn, tpos)
+        if sym == None:
+            print "Failed to match pattern:", patn
+        return sym
+
+
+
+
+
+########################################################################
+
+
+
+class FingerText(gtk.TextView):
+    def __init__(self):
+        gtk.TextView.__init__(self)
+        self.set_wrap_mode(gtk.WRAP_WORD_CHAR)
+        self.exphan = self.connect('expose-event', self.config)
+        self.input = text_input(self, self.stylus)
+
+    def config(self, *a):
+        self.disconnect(self.exphan)
+        c = gtk.gdk.color_parse('red')
+        gc = self.window.new_gc()
+        gc.set_foreground(self.get_colormap().alloc_color(c))
+        #gc.set_line_attributes(2, gtk.gdk.LINE_SOLID, gtk.gdk.CAP_ROUND, gtk.gdk.JOIN_ROUND)
+        gc.set_subwindow(gtk.gdk.INCLUDE_INFERIORS)
+        self.input.set_colour(gc)
+
+    def stylus(self, cmd, info):
+        if cmd == "sym":
+            tl = self.get_toplevel()
+            w = tl.get_focus()
+            if w == None:
+                w = self
+            ev = gtk.gdk.Event(gtk.gdk.KEY_PRESS)
+            ev.window = w.window
+            if info == '<BS>':
+                ev.keyval = 65288
+                ev.hardware_keycode = 22
+            else:
+                (ev.keyval,) = struct.unpack_from("b", info)
+            w.emit('key_press_event', ev)
+            #self.get_buffer().insert_at_cursor(info)
+        if cmd == 'click':
+            self.grab_focus()
+            if not info.send_event:
+                info.send_event = True
+                ev2 = gtk.gdk.Event(gtk.gdk.BUTTON_PRESS)
+                ev2.send_event = True
+                ev2.window = info.window
+                ev2.time = info.time
+                ev2.x = info.x
+                ev2.y = info.y
+                ev2.button = info.button
+                self.emit('button_press_event', ev2)
+                self.emit('button_release_event', info)
+        if cmd == 'redraw':
+            self.queue_draw()
+
+    def insert_at_cursor(self, text):
+        self.get_buffer().insert_at_cursor(text)
+        
+class FingerEntry(gtk.Entry):
+    def __init__(self):
+        gtk.Entry.__init__(self)
+
+    def insert_at_cursor(self, text):
+        c = self.get_property('cursor-position')
+        t = self.get_text()
+        t = t[0:c]+text+t[c:]
+        self.set_text(t)
+
+class SMSlist(gtk.DrawingArea):
+    def __init__(self, getlist):
+        gtk.DrawingArea.__init__(self)
+        self.pixbuf = None
+        self.width = self.height = 0
+        self.need_redraw = True
+        self.colours = None
+        self.collist = {}
+        self.get_list = getlist
+
+        self.connect("expose-event", self.redraw)
+        self.connect("configure-event", self.reconfig)
+        
+        self.connect("button_release_event", self.release)
+        self.connect("button_press_event", self.press)
+        self.set_events(gtk.gdk.EXPOSURE_MASK
+                        | gtk.gdk.BUTTON_PRESS_MASK
+                        | gtk.gdk.BUTTON_RELEASE_MASK
+                        | gtk.gdk.STRUCTURE_MASK)
+
+        # choose a font
+        fd = pango.FontDescription('sans 10')
+        fd.set_absolute_size(25 * pango.SCALE)
+        self.font = fd
+        met = self.get_pango_context().get_metrics(fd)
+        self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE
+        fd = pango.FontDescription('sans 5')
+        fd.set_absolute_size(15 * pango.SCALE)
+        self.smallfont = fd
+        self.selected = 0
+        self.top = 0
+        self.book = None
+
+        self.smslist = []
+
+        self.queue_draw()
+
+
+    def set_book(self, book):
+        self.book = book
+
+    def lines(self):
+        alloc = self.get_allocation()
+        lines = alloc.height / self.lineheight
+        return lines
+
+    def reset_list(self):
+        self.selected = 0
+        self.smslist = None
+        self.size_requested = 0
+        self.refresh()
+
+    def refresh(self):
+        self.need_redraw = True
+        self.queue_draw()
+
+    def assign_colour(self, purpose, name):
+        self.collist[purpose] = name
+
+    def reconfig(self, w, ev):
+        alloc = w.get_allocation()
+        if not self.pixbuf:
+            return
+        if alloc.width != self.width or alloc.height != self.height:
+            self.pixbuf = None
+            self.need_redraw = True
+
+    def add_col(self, sym, col):
+        c = gtk.gdk.color_parse(col)
+        gc = self.window.new_gc()
+        gc.set_foreground(self.get_colormap().alloc_color(c))
+        self.colours[sym] = gc
+
+    def redraw(self, w, ev):
+        if self.colours == None:
+            self.colours = {}
+            for p in self.collist:
+                self.add_col(p, self.collist[p])
+            self.bg = self.get_style().bg_gc[gtk.STATE_NORMAL]
+
+        if self.need_redraw:
+            self.draw_buf()
+
+        self.window.draw_drawable(self.bg, self.pixbuf, 0, 0, 0, 0,
+                                         self.width, self.height)
+
+    def draw_buf(self):
+        self.need_redraw = False
+        if self.pixbuf == None:
+            alloc = self.get_allocation()
+            self.pixbuf = gtk.gdk.Pixmap(self.window, alloc.width, alloc.height)
+            self.width = alloc.width
+            self.height = alloc.height
+        self.pixbuf.draw_rectangle(self.bg, True, 0, 0,
+                                   self.width, self.height)
+
+        if self.top > self.selected:
+            self.top = 0
+        max = self.lines()
+        if self.smslist == None or \
+               (self.top + max > len(self.smslist) and self.size_requested < self.top + max):
+            self.size_requested = self.top + max
+            self.smslist = self.get_list(self.top + max)
+        for i in range(len(self.smslist)):
+            if i < self.top:
+                continue
+            if i > self.top + max:
+                break
+            if i == self.selected:
+                col = self.colours['bg-selected']
+            else:
+                col = self.colours['bg-%d'%(i%2)]
+
+            self.pixbuf.draw_rectangle(col,
+                                       True, 0, (i-self.top)*self.lineheight,
+                                       self.width, self.lineheight)
+            self.draw_sms(self.smslist[i], (i - self.top) * self.lineheight)
+
+
+    def draw_sms(self, sms, yoff):
+        
+        self.modify_font(self.smallfont)
+        tm = time.strftime("%Y-%m-%d %H:%M:%S", sms.time[0:6]+(0,0,0))
+        then = time.mktime(sms.time[0:6]+(0,0,-1))
+        now = time.time()
+        if now > then:
+            diff = now - then
+            if diff < 99:
+                delta = "%02d sec ago" % diff
+            elif diff < 99*60:
+                delta = "%02d min ago" % (diff/60)
+            elif diff < 48*60*60:
+                delta = "%02dh%02dm ago" % ((diff/60/60), (diff/60)%60)
+            else:
+                delta = tm[0:10]
+            tm = delta + tm[10:]
+
+        l = self.create_pango_layout(tm)
+        self.pixbuf.draw_layout(self.colours['time'],
+                                0, yoff, l)
+        co = sms.correspondent
+        if self.book:
+            cor = book_name(self.book, co)
+            if cor:
+                co = cor[0]
+        if sms.source == 'LOCAL':
+            col = self.colours['recipient']
+            co = 'To ' + co
+        else:
+            col = self.colours['sender']
+            co = 'From '+co
+        l = self.create_pango_layout(co)
+        self.pixbuf.draw_layout(col,
+                                0, yoff + self.lineheight/2, l)
+        self.modify_font(self.font)
+        t = sms.text.replace("\n", " ")
+        t = t.replace("\n", " ")
+        l = self.create_pango_layout(t)
+        if sms.state in ['DRAFT', 'NEW']:
+            col = self.colours['mesg-new']
+        else:
+            col = self.colours['mesg']
+        self.pixbuf.draw_layout(col,
+                                180, yoff, l)
+
+    def press(self,w,ev):
+        row = int(ev.y / self.lineheight)
+        self.selected = self.top + row
+        if self.selected >= len(self.smslist):
+            self.selected = len(self.smslist) - 1
+        if self.selected < 0:
+            self.selected = 0
+
+        l = self.lines()
+        self.top += row - l / 2
+        if self.top >= len(self.smslist) - l:
+            self.top = len(self.smslist) - l + 1
+        if self.top < 0:
+            self.top = 0
+
+        self.refresh()
+        
+    def release(self,w,ev):
+        pass
+
+def load_book(file):
+    try:
+        f = open(file)
+    except:
+        f = open('/home/neilb/home/mobile-numbers-jan-08')
+    rv = []
+    for l in f:
+        x = l.split(';')
+        rv.append([x[0],x[1]])
+    rv.sort(lambda x,y: cmp(x[0],y[0]))
+    return rv
+
+def book_lookup(book, name, num):
+    st=[]; mid=[]
+    for l in book:
+        if name.lower() == l[0][0:len(name)].lower():
+            st.append(l)
+        elif l[0].lower().find(name.lower()) >= 0:
+            mid.append(l)
+    st += mid
+    if len(st) == 0:
+        return [None, None]
+    if num >= len(st):
+        num = -1
+    return st[num]
+
+def book_parse(book, name):
+    if not book:
+        return None
+    cnt = 0
+    while len(name) and name[-1] == '.':
+        cnt += 1
+        name = name[0:-1]
+    return book_lookup(book, name, cnt)
+    
+
+
+def book_name(book, num):
+    if len(num) < 8:
+        return None
+    for ad in book:
+        if len(ad[1]) >= 8 and num[-8:] == ad[1][-8:]:
+            return ad
+    return None
+
+def book_speed(book, sym):
+    i = book_lookup(book, sym, 0)
+    if i[0] == None or i[0] != sym:
+        return None
+    j = book_lookup(book, i[1], 0)
+    if j[0] == None:
+        return (i[1], i[0])
+    return (j[1], j[0])
+
+def name_lookup(book, str):
+    # We need to report
+    #  - a number - to dial
+    #  - optionally a name that is associated with that number
+    #  - optionally a new name to save the number as
+    # The name is normally alpha, but can be a single digit for
+    # speed-dial
+    # Dots following a name allow us to stop through multiple matches.
+    # So input can be:
+    # A single symbol.
+    #         This is a speed dial.  It maps to name, then number
+    # A string of >1 digits
+    #         This is a literal number, we look up name if we can
+    # A string of dots
+    #         This is a look up against recent incoming calls
+    #         We look up name in phone book
+    # A string starting with alpha, possibly ending with dots
+    #         This is a regular lookup in the phone book
+    # A number followed by a string
+    #         This provides the string as a new name for saving
+    # A string of dots followed by a string
+    #         This also provides the string as a newname
+    # An alpha string, with dots, followed by '+'then a single symbol
+    #         This saves the match as a speed dial
+    #
+    # We return a triple of (number,oldname,newname)
+    if re.match('^[A-Za-z0-9]$', str):
+        # Speed dial lookup
+        s = book_speed(book, str)
+        if s:
+            return (s[0], s[1], None)
+        return None
+    m = re.match('^(\+?\d+)([A-Za-z][A-Za-z0-9 ]*)?$', str)
+    if m:
+        # Number and possible newname
+        s = book_name(book, m.group(1))
+        if s:
+            return (m.group(1), s[0], m.group(2))
+        else:
+            return (m.group(1), None, m.group(2))
+    m = re.match('^([A-Za-z][A-Za-z0-9 ]*)(\.*)(\+[A-Za-z0-9])?$', str)
+    if m:
+        # name and dots
+        speed = None
+        if m.group(3):
+            speed = m.group(3)[1]
+        i = book_lookup(book, m.group(1), len(m.group(2)))
+        if i[0]:
+            return (i[1], i[0], speed)
+        return None
+
+class SendSMS(gtk.Window):
+    def __init__(self, store):
+        gtk.Window.__init__(self)
+        self.set_default_size(480,640)
+        self.set_title("SendSMS")
+        self.store = store
+        self.connect('destroy', self.close_win)
+        
+        self.selecting = False
+        self.viewing = False
+        self.book = None
+        self.create_ui()
+
+        self.show()
+        self.reload_book = True
+        self.number = None
+        self.cutbuffer = None
+
+        d = dnotify.dir(store.dirname)
+        self.watcher = d.watch('newmesg', lambda f : self.got_new())
+
+        self.watch_clip('sms-new')
+
+        self.connect('property-notify-event', self.newprop)
+        self.add_events(gtk.gdk.PROPERTY_CHANGE_MASK)
+    def newprop(self, w, ev):
+        if ev.atom == '_INPUT_TEXT':
+            str = self.window.property_get('_INPUT_TEXT')
+            self.numentry.set_text(str[2])
+
+    def close_win(self, *a):
+        # FIXME save draft
+        gtk.main_quit()
+
+    def create_ui(self):
+
+        fd = pango.FontDescription("sans 10")
+        fd.set_absolute_size(25*pango.SCALE)
+        self.button_font = fd
+        v = gtk.VBox() ;v.show() ; self.add(v)
+
+        self.sender = self.send_ui()
+        v.add(self.sender)
+        self.sender.hide()
+
+        self.listing = self.list_ui()
+        v.add(self.listing)
+        self.listing.show()
+
+        self.book = load_book("/data/address-book")
+        self.listview.set_book(self.book)
+
+        self.rotate_list(self, target='All')
+
+    def send_ui(self):
+        v = gtk.VBox()
+        main = v
+
+        h = gtk.HBox()
+        h.show()
+        v.pack_start(h, expand=False)
+        l = gtk.Label('To:')
+        l.modify_font(self.button_font)
+        l.show()
+        h.pack_start(l, expand=False)
+        
+        self.numentry = FingerEntry()
+        h.pack_start(self.numentry)
+        self.numentry.modify_font(self.button_font)
+        self.numentry.show()
+        self.numentry.connect('changed', self.update_to);
+
+        h = gtk.HBox()
+        l = gtk.Label('')
+        l.modify_font(self.button_font)
+        l.show()
+        h.pack_start(l)
+        h.show()
+        v.pack_start(h, expand=False)
+        h = gtk.HBox()
+        self.to_label = l
+        l = gtk.Label('0 chars')
+        l.modify_font(self.button_font)
+        l.show()
+        self.cnt_label = l
+        h.pack_end(l)
+        h.show()
+        v.pack_start(h, expand=False)
+
+        h = gtk.HBox()
+        h.set_size_request(-1,80)
+        h.set_homogeneous(True)
+        h.show()
+        v.pack_start(h, expand=False)
+
+        self.add_button(h, 'select', self.select)
+        self.add_button(h, 'clear', self.clear)
+        self.add_button(h, 'paste', self.paste)
+
+        self.message = FingerText()
+        self.message.show()
+        self.message.modify_font(self.button_font)
+        sw = gtk.ScrolledWindow() ; sw.show()
+        sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+        #v.add(self.message)
+        v.add(sw)
+        sw.add(self.message)
+        self.message.get_buffer().connect('changed', self.buff_changed)
+
+        h = gtk.HBox()
+        h.set_size_request(-1,80)
+        h.set_homogeneous(True)
+        h.show()
+        v.pack_end(h, expand=False)
+
+        self.add_button(h, 'Send GSM', self.send, 'GSM')
+        self.draft_button = self.add_button(h, 'Draft', self.draft)
+        self.add_button(h, 'Send EXE', self.send, 'EXE')
+
+        return main
+
+    def list_ui(self):
+        v = gtk.VBox() ; main = v
+
+        h = gtk.HBox() ; h.show()
+        h.set_size_request(-1,80)
+        h.set_homogeneous(True)
+        v.pack_start(h, expand = False)
+        self.add_button(h, 'Del', self.delete)
+        self.view_button = self.add_button(h, 'View', self.view)
+        self.reply = self.add_button(h, 'New', self.open)
+
+        h = gtk.HBox() ; h.show()
+        h.set_size_request(-1,80)
+        h.set_homogeneous(True)
+        v.pack_end(h, expand=False)
+        self.buttonA = self.add_button(h, 'Sent', self.rotate_list, 'A')
+        self.buttonB = self.add_button(h, 'Recv', self.rotate_list, 'B')
+
+
+        self.last_response = gtk.Label('')
+        v.pack_end(self.last_response, expand = False)
+
+        h = gtk.HBox() ; h.show()
+        v.pack_start(h, expand=False)
+        b = gtk.Button("clr") ; b.show()
+        b.connect('clicked', self.clear_search)
+        h.pack_end(b, expand=False)
+        l = gtk.Label('search:') ; l.show()
+        h.pack_start(l, expand=False)
+        
+        e = gtk.Entry() ; e.show()
+        self.search_entry = e
+        h.pack_start(e)
+
+        self.listview = SMSlist(self.load_list)
+        self.listview.show()
+        self.listview.assign_colour('time', 'blue')
+        self.listview.assign_colour('sender', 'red')
+        self.listview.assign_colour('recipient', 'black')
+        self.listview.assign_colour('mesg', 'black')
+        self.listview.assign_colour('mesg-new', 'red')
+        self.listview.assign_colour('bg-0', 'yellow')
+        self.listview.assign_colour('bg-1', 'pink')
+        self.listview.assign_colour('bg-selected', 'white')
+        
+        v.add(self.listview)
+
+        self.singleview = gtk.TextView()
+        self.singleview.modify_font(self.button_font)
+        self.singleview.show()
+        self.singleview.set_wrap_mode(gtk.WRAP_WORD_CHAR)
+        sw = gtk.ScrolledWindow()
+        sw.add(self.singleview)
+        sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+        sw.hide()
+        v.add(sw)
+        self.singlescroll = sw
+        
+
+        main.show()
+        return main
+
+    def add_button(self, parent, label, func, *args):
+        b = gtk.Button(label)
+        b.child.modify_font(self.button_font)
+        b.connect('clicked', func, *args)
+        b.set_property('can-focus', False)
+        parent.add(b)
+        b.show()
+        return b
+
+    def update_to(self, w):
+        n = w.get_text()
+        if n == '':
+            self.reload_book = True
+            self.to_label.set_text('')
+        else:
+            if self.reload_book:
+                self.reload_book = False
+                self.book = load_book("/data/address-book")
+                self.listview.set_book(self.book)
+            e = name_lookup(self.book, n)
+            if e and e[1]:
+                self.to_label.set_text(e[1] + 
+                                       ' '+
+                                       e[0])
+                self.number = e[0]
+            else:
+                self.to_label.set_text('??')
+                self.number = n
+        self.buff_changed(None)
+
+    def buff_changed(self, w):
+        if self.numentry.get_text() == '' and self.message.get_buffer().get_property('text') == '':
+            self.draft_button.child.set_text('Cancel')
+        else:
+            self.draft_button.child.set_text('SaveDraft')
+        l = len(self.message.get_buffer().get_property('text'))
+        if l <= 160:
+            m = 1
+        else:
+            m = (l+152)/153
+        self.cnt_label.set_text('%d chars / %d msgs' % (l, m))
+
+    def select(self, w, *a):
+        if not self.selecting:
+            self.message.input.active = False
+            w.child.set_text('Cut')
+            self.selecting = True
+        else:
+            self.message.input.active = True
+            w.child.set_text('Select')
+            self.selecting = False
+            b = self.message.get_buffer()
+            bound = b.get_selection_bounds()
+            if bound:
+                (s,e) = bound
+                t = b.get_text(s,e)
+                self.cutbuffer = t
+                b.delete_selection(True, True)
+
+    def clear(self, *a):
+        w = self.get_toplevel().get_focus()
+        if w == None:
+            w = self.message
+        if w == self.message:
+            self.cutbuffer = self.message.get_buffer().get_property('text')
+            b = self.message.get_buffer()
+            b.set_text('')
+        else:
+            self.cutbuffer = w.get_text()
+            w.set_text('')
+            
+    def paste(self, *a):
+        w = self.get_toplevel().get_focus()
+        if w == None:
+            w = self.message
+        if self.cutbuffer:
+            w.insert_at_cursor(self.cutbuffer)
+        pass
+
+    def watch_clip(self, board):
+        self.cb = gtk.Clipboard(selection=board)
+        self.targets = [ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ]
+        self.cb.set_with_data(self.targets, self.get, self.got_clip, None)
+
+    def got_clip(self, clipb, data):
+        a = clipb.wait_for_text()
+        print "sms got clip", a
+        self.numentry.set_text(a)
+        self.listing.hide()
+        self.sender.show()
+        self.cb.set_with_data(self.targets, self.get, self.got_clip, None)
+        self.present()
+
+    def get(self, sel, info, data):
+        sel.set_text("Number Please")
+
+    def send(self, w, style):
+        sender = '0403463349'
+        recipient = self.number
+        mesg = self.message.get_buffer().get_property('text')
+        if not mesg or not recipient:
+            return
+        try:
+            if style == 'EXE':
+                p = Popen(['exesms', sender, recipient, mesg], stdout = PIPE)
+            else:
+                p = Popen(['gsm-sms', sender, recipient, mesg], stdout = PIPE)
+        except:
+            rv = 1
+            line = 'Fork Failed'
+        else:
+            line = 'no response'
+            rv = p.wait()
+            for l in p.stdout:
+                if l:
+                    line = l
+        
+        s = SMSmesg(to = recipient, text = mesg)
+
+        if rv or line[0:2] != 'OK':
+            s.state = 'DRAFT'
+            target = 'Draft'
+        else:
+            target = 'All'
+        self.store.store(s)
+        self.last_response.set_text('Mess Send: '+ line.strip())
+        self.last_response.show()
+
+        self.sender.hide()
+        self.listing.show()
+        self.rotate_list(target=target)
+
+    def draft(self, *a):
+        sender = '0403463349'
+        recipient = self.numentry.get_text()
+        if recipient:
+            rl = [recipient]
+        else:
+            rl = []
+        mesg = self.message.get_buffer().get_property('text')
+        if mesg:
+            s = SMSmesg(to = recipient, text = mesg, state = 'DRAFT')
+            self.store.store(s)
+        self.sender.hide()
+        self.listing.show()
+        self.rotate_list(target='Draft')
+    def config(self, *a):
+        pass
+    def delete(self, *a):
+        if len(self.listview.smslist ) < 1:
+            return
+        s = self.listview.smslist[self.listview.selected]
+        self.store.delete(s)
+        sel = self.listview.selected
+        self.rotate_list(target=self.display_list)
+        self.listview.selected = sel
+        if self.viewing:
+            self.view(self.view_button)
+
+    def view(self, w, *a):
+        if self.viewing:
+            w.child.set_text('View')
+            self.viewing = False
+            self.singlescroll.hide()
+            self.listview.show()
+            if self.listview.smslist and  len(self.listview.smslist ) >= 1:
+                s = self.listview.smslist[self.listview.selected]
+                if s.state == 'NEW':
+                    self.store.setstate(s, None)
+                    if self.display_list == 'New':
+                        self.rotate_list(target='New')
+            self.reply.child.set_text('New')
+        else:
+            if not self.listview.smslist or len(self.listview.smslist ) < 1:
+                return
+            s = self.listview.smslist[self.listview.selected]
+            w.child.set_text('List')
+            self.viewing = True
+            self.last_response.hide()
+            self.listview.hide()
+            if self.book:
+                n = book_name(self.book, s.correspondent)
+                if n and n[0]:
+                    n = n[0] + ' ['+s.correspondent+']'
+                else:
+                    n = s.correspondent
+            else:
+                n = s.correspondent
+            if s.source == 'LOCAL':
+                t = 'To: ' + n + '\n'
+            else:
+                t = 'From: %s (%s)\n' % (n, s.source)
+            tm = time.strftime('%d%b%Y %H:%M:%S', s.time[0:6]+(0,0,0))
+            t += 'Time: ' + tm + '\n'
+            t += '\n'
+            t += s.text
+            self.singleview.get_buffer().set_text(t)
+            self.singlescroll.show()
+
+            if s.source == 'LOCAL':
+                self.reply.child.set_text('Open')
+            else:
+                self.reply.child.set_text('Reply')
+            
+    def open(self, *a):
+        if self.viewing:
+            if len(self.listview.smslist) < 1:
+                return
+            s = self.listview.smslist[self.listview.selected]
+            if s.state == 'NEW':
+                self.store.setstate(s, None)
+        
+            self.numentry.set_text(s.correspondent)
+            self.message.get_buffer().set_text(s.text)
+            self.draft_button.child.set_text('SaveDraft')
+        else:
+            self.numentry.set_text('')
+            self.message.get_buffer().set_text('')
+            self.draft_button.child.set_text('Cancel')
+        self.listing.hide()
+        self.sender.show()
+
+    def load_list(self, lines):
+        now = time.time()
+        l = []
+        target = self.display_list
+        patn = self.search_entry.get_text()
+        #print 'pattern is', patn
+        if target == 'New':
+            (now, l) = self.store.lookup(now, 'NEW')
+        elif target == 'Draft':
+            (now, l) = self.store.lookup(now, 'DRAFT')
+        else:
+            if lines == 0: lines = 20
+            while now and len(l) < lines:
+                (now, l2) = self.store.lookup(now)
+                for e in l2:
+                    if patn and patn not in e.correspondent:
+                        continue
+                    if target == 'All':
+                        l.append(e)
+                    elif target == 'Sent' and e.source == 'LOCAL':
+                        l.append(e)
+                    elif target == 'Recv' and e.source != 'LOCAL':
+                        l.append(e)
+        return l
+        
+    def rotate_list(self, w=None, ev=None, which = None, target=None):
+        # lists are:
+        #   All, Recv, New, Sent, Draft
+        # When one is current, two others can be selected
+
+        if target == None:
+            if w == None:
+                target = self.display_list
+            else:
+                target = w.child.get_text()
+
+        if target == 'All':
+            self.buttonA.child.set_text('Sent')
+            self.buttonB.child.set_text('Recv')
+        if target == 'Sent':
+            self.buttonA.child.set_text('All')
+            self.buttonB.child.set_text('Draft')
+        if target == 'Draft':
+            self.buttonA.child.set_text('All')
+            self.buttonB.child.set_text('Sent')
+        if target == 'Recv':
+            self.buttonA.child.set_text('All')
+            self.buttonB.child.set_text('New')
+        if target == 'New':
+            self.buttonA.child.set_text('All')
+            self.buttonB.child.set_text('Recv')
+                
+        self.display_list = target
+        self.listview.reset_list()
+
+    def clear_search(self, *a):
+        pass
+
+    def got_new(self):
+        self.rotate_list(self, target = 'New')
+
+def main(args):
+    for p in ['/data','/media/card','/var/tmp']:
+        if os.path.exists(p):
+            pth = p
+            break
+    w = SendSMS(SMSstore(pth+'/SMS'))
+    gtk.settings_get_default().set_long_property("gtk-cursor-blink", 0, "main")
+
+    gtk.main()
+
+if __name__ == '__main__':
+    main(sys.argv)
diff --git a/sms/storesms.py b/sms/storesms.py
new file mode 100644 (file)
index 0000000..f0d5f2e
--- /dev/null
@@ -0,0 +1,486 @@
+#
+# FIXME
+#  - trim newmesg and draft when possible.
+#  - remove old multipart files
+#
+# Store SMS messages is a bunch of files, one per month.
+# Each message is stored on one line with space separated .
+# URL encoding (%XX) is used to quote white space, unprintables etc
+# We store 5 fields:
+# - time stamp that we first saw the message.  This is in UTC.
+#   This is the primary key.  If a second message is seen in the same second,
+#   we quietly add 1 to the second.
+# - Source, one of 'LOCAL' for locally composed, 'GSM' for recieved via GSM
+#   or maybe 'EMAIL' if received via email??
+# - Time message was sent, Localtime with -TZ.  For GSM messages this comes with the
+#   message. For 'LOCAL' it might be '-', or will be the time we succeeded
+#   in sending.
+#   time is stored as a tupple (Y m d H M S Z) where Z is timezone in multiples
+#   of 15 minutes.
+# - The correspondent: sender if GSM, recipient if LOCAL, or '-' if not sent.
+#     This might be a comma-separated list of recipients.
+# - The text of the message
+#
+# Times are formatted %Y%m%d-%H%M%S and local time has a  GSM TZ suffix.
+# GSM TZ is from +48 to -48 in units of 15 minutes. (0 is +00)
+#
+# We never modify a message once it has been stored.
+# If we have a draft that we edit and send, we delete the draft and
+# create a new sent-message
+# If we forward a message, we will then have two copies.
+#
+# New messages are not distinguished by a flag (which would have to be cleared)
+# but by being in a separate list of new messages.
+# We havea list of 'new' messages and a list of 'draft' messages.
+#
+# Multi-part messages are accumulated as they are received.  The quoted message
+# contains <N>text for each part of the message.
+#  e.g. <1><2>nd%20so%20no.....<3>
+# if we only have part 2 of 3.
+# For each incomplete message there is a file (like 'draft' and 'newmesg') named
+# for the message which provides an index to each incomplete message.
+# It will be named e.g. 'multipart-1C' when 1C is the message id.
+#
+# This module defines 2 classes:
+# SMSmesg
+#   This holds a message and so has timestamp, source, time, correspondent
+#    and text fields.  These are decoded.
+#   SMSmesg also has 'state' which can be one of "NEW", "DRAFT" or None
+#   Finally it might have a 'ref' and a 'part' which is a tuple (this,max)
+#    This is only used when storing the message to link it up with
+#    a partner
+# 
+# SMSstore
+#  This represents a collection of messages in a directory (one file per month)
+#  and provides lists of 'NEW' and 'DRAFT' messages.
+#  Operations:
+#     store(SMSmesg,  NEW|DRAFT|) -> None
+#       stores the message and sets the timestamp
+#  lookup(latest-time, NEW|DRAFT|ALL) -> (earlytime, [SMSmesg])
+#     collects a list of messages in reverse time order with times no later
+#     than 'latest-time'.  Only consider NEW or DRAFT or ALL messages.
+#     The list may not be complete (typically one month at a time are returnned)
+#      If you want more, call again with 'earlytime' as 'latest-time').
+#  delete(SMSmesg)
+#     delete the given message (based on the timestamp only)
+#  setstate(SMSmesg, NEW|DRAFT|None)
+#     update the 'new' and 'draft' lists or container, or not container, this
+#     message.
+#  
+#
+
+import os, fcntl, re, time, urllib
+
+def umktime(tm):
+    # like time.mktime, but tm is UTC
+    # So we want a 't' where
+    #  time.gmtime(t)[0:6] == tm[0:6]
+    estimate = time.mktime(tm) - time.timezone
+    t2 = time.gmtime(estimate)
+    while t2[0:6] < tm[0:6]:
+        estimate += 15*60
+        t2 = time.gmtime(estimate)
+    while t2[0:6] > tm[0:6]:
+        estimate -= 15*60
+        t2 = time.gmtime(estimate)
+    return estimate
+
+def parse_time(strg):
+    return int(umktime(time.strptime(strg, "%Y%m%d-%H%M%S")))
+def parse_ltime(strg):
+    n=3
+    if strg[-2] == '+' or strg[-2] == '-':
+        n=2
+    z = strg[-n:]
+    return time.strptime(strg[:-n], "%Y%m%d-%H%M%S")[0:6] + (int(z),)
+def format_time(t):
+    return time.strftime("%Y%m%d-%H%M%S", time.gmtime(t))
+def format_ltime(tm):
+    return time.strftime("%Y%m%d-%H%M%S", tm[0:6]+(0,0,0)) + ("%+03d" % tm[6])
+
+
+class SMSmesg:
+    def __init__(self, **a):
+        if len(a) == 1 and 'line' in a:
+            # line read from a file, with 5 fields.
+            #  stamp, source, time, correspondent, text
+            line = a['line'].split()
+            self.stamp = parse_time(line[0])
+            self.source = line[1]
+            self.time = parse_ltime(line[2])
+            self.correspondents = []
+            for c in line[3].split(','):
+                if c != '-':
+                    self.correspondents.append(urllib.unquote(c))
+            self.set_corresponent()
+            self.state = None
+
+            self.parts = None
+            txt = line[4]
+            if txt[0] != '<':
+                self.text = urllib.unquote(txt)
+                return
+            # multipart:   <1>text...<2>text...<3><4>
+            m = re.findall('<(\d+)>([^<]*)', txt)
+            parts = []
+            for (pos, strg) in m:
+                p = int(pos)
+                while len(parts) < p:
+                    parts.append(None)
+                if strg:
+                    parts[p-1] = urllib.unquote(strg)
+            self.parts = parts
+            self.reduce_parts()
+        else:
+            self.stamp = int(time.time())
+            self.source = None
+            lt = time.localtime()
+            z = time.timezone/15/60
+            if lt[8] == 1:
+                z -= 4
+            self.time = time.localtime()[0:6] + (z,)
+            self.correspondents = []
+            self.text = ""
+            self.state = None
+            self.ref = None
+            self.parts = None
+            part = None
+            for k in a:
+                if k == 'stamp':
+                    self.stamp = a[k]
+                elif k == 'source':
+                    self.source = a[k]
+                elif k == 'time':
+                    # time can be a GSM string: 09/02/09,09:56:28+44 (ymd,HMS+z)
+                    # or a tuple (y,m,d,H,M,S,z)
+                    if type(a[k]) == str:
+                        t = a[k][:-3]
+                        z = a[k][-3:]
+                        tm = time.strptime(t, "%y/%m/%d,%H:%M:%S")
+                        self.time = tm[0:6] + (int(z),)
+                elif k == 'to' or k == 'sender':
+                    if self.source == None:
+                        if k == 'to':
+                            self.source = 'LOCAL'
+                        if k == 'sender':
+                            self.source = 'GSM'
+                    self.correspondents = [ a[k] ]
+                elif k == 'correspondents':
+                    self.correspondents = a[k]
+                elif k == 'text':
+                    self.text = a[k]
+                elif k == 'state':
+                    self.state = a[k]
+                elif k == 'ref':
+                    if a[k] != None:
+                        self.ref = a[k]
+                elif k == 'part':
+                    if a[k]:
+                        part = a[k]
+                else:
+                    raise ValueError
+            if self.source == None:
+                self.source = 'LOCAL'
+            if part:
+                print 'part', part[0], part[1]
+                self.parts = [None for x in range(part[1])]
+                self.parts[part[0]-1] = self.text
+                self.reduce_parts()
+            self.set_corresponent()
+
+        self.month_re = re.compile("^[0-9]{6}$")
+
+    def reduce_parts(self):
+        def reduce_pair(a,b):
+            if b == None:
+                b = "...part of message missing..."
+            if a == None:
+                return b
+            return a+b
+        self.text = reduce(reduce_pair, self.parts)
+
+
+    def set_corresponent(self):
+        if len(self.correspondents) == 1:
+            self.correspondent = self.correspondents[0]
+        elif len(self.correspondents) == 0:
+            self.correspondent = "Unsent"
+        else:
+            self.correspondent = "Multiple"
+
+    def format(self):
+        fmt =  "%s %s %s %s " % (format_time(self.stamp), self.source,
+                                   format_ltime(self.time),
+                                   self.format_correspondents())
+        if not self.parts:
+            return fmt + urllib.quote(self.text)
+
+        for i in range(len(self.parts)):
+            fmt += ("<%d>" % (i+1)) + urllib.quote(self.parts[i]) if self.parts[i] else ""
+        return fmt
+
+    def format_correspondents(self):
+        r = ""
+        for i in self.correspondents:
+            if i:
+                r += ',' + urllib.quote(i)
+        if r:
+            return r[1:]
+        else:
+            return '-'
+        
+
+class SMSstore:
+    def __init__(self, dir):
+        self.month_re = re.compile("^[0-9]{6}$")
+        self.cached_month = None
+        self.dirname = dir
+        # find message files
+        self.set_files()
+        self.drafts = self.load_list('draft')
+        self.newmesg = self.load_list('newmesg')
+
+    def load_list(self, name, update = None, *args):
+
+        l = []
+        try:
+            f = open(self.dirname + '/' + name, 'r+')
+        except IOError:
+            return l
+
+        if update:
+            fcntl.lockf(f, fcntl.LOCK_EX)
+        for ln in f:
+            l.append(parse_time(ln.strip()))
+        l.sort()
+        l.reverse()
+
+        if update and update(l, *args):
+            f2 = open(self.dirname + '/' + name + '.new', 'w')
+            for t in l:
+                f2.write(format_time(t)+"\n")
+            f2.close()
+            os.rename(self.dirname + '/' + name + '.new',
+                      self.dirname + '/' + name)
+        f.close()
+        return l
+
+    def load_month(self, f):
+        # load the messages from f, which is open for read
+        rv = {}
+        for l in f:
+            l.strip()
+            m = SMSmesg(line=l)
+            rv[m.stamp] = m
+            if m.stamp in self.drafts:
+                m.state = 'DRAFT'
+            elif m.stamp in self.newmesg:
+                m.state = 'NEW'
+        return rv
+
+    def store_month(self, l, m):
+        dm = self.dirname + '/' + m
+        f = open(dm+'.new', 'w')
+        for s in l:
+            f.write(l[s].format() + "\n")
+        f.close()
+        os.rename(dm+'.new', dm)
+        if not m in self.files:
+            self.files.append(m)
+            self.files.sort()
+            self.files.reverse()
+        if self.cached_month == m:
+            self.cache = l
+
+    def store(self, sms):
+        orig = None
+        if sms.ref != None:
+            # This is part of a multipart.
+            # If there already exists part of this
+            # merge them together
+            #
+            times = self.load_list('multipart-' + sms.ref)
+            if len(times) == 1:
+                orig = self.load(times[0])
+                if orig and orig.parts:
+                    for i in range(len(sms.parts)):
+                        if sms.parts[i] == None and i < len(orig.parts):
+                            sms.parts[i] = orig.parts[i]
+                else:
+                    orig = None
+                    
+        m = time.strftime("%Y%m", time.gmtime(sms.stamp))
+        try:
+            f = open(self.dirname + '/' + m, "r+")
+        except:
+            f = open(self.dirname + '/' + m, "w+")
+        complete = True
+        if sms.ref != None:
+            for i in sms.parts:
+                if i == None:
+                    complete = False
+            if complete:
+                sms.reduce_parts()
+                sms.parts = None
+                
+        fcntl.lockf(f, fcntl.LOCK_EX)
+        l = self.load_month(f)
+        while sms.stamp in l:
+            sms.stamp += 1
+        l[sms.stamp] = sms
+        self.store_month(l, m);
+        f.close()
+
+        if orig:
+            self.delete(orig)
+        if sms.ref != None:
+            if complete:
+                try:
+                    os.unlink(self.dirname + '/multipart-' + sms.ref)
+                except:
+                    pass
+            elif orig:
+                def replacewith(l, tm):
+                    while len(l):
+                        l.pop()
+                    l.append(tm)
+                    return True
+                self.load_list('multipart-' + sms.ref, replacewith, sms.stamp)
+            else:
+                f = open(self.dirname +'/multipart-' + sms.ref, 'w')
+                fcntl.lockf(f, fcntl.LOCK_EX)
+                f.write(format_time(sms.stamp) + '\n')
+                f.close()
+
+        if sms.state == 'NEW' or sms.state == 'DRAFT':
+            s = 'newmesg'
+            if sms.state == 'DRAFT':
+                s = 'draft'
+            f = open(self.dirname +'/' + s, 'a')
+            fcntl.lockf(f, fcntl.LOCK_EX)
+            f.write(format_time(sms.stamp) + '\n')
+            f.close()
+        elif sms.state != None:
+            raise ValueError
+
+    def set_files(self):
+        self.files = []
+        for f in os.listdir(self.dirname):
+            if self.month_re.match(f):
+                self.files.append(f)
+        self.files.sort()
+        self.files.reverse()
+        
+    def lookup(self, lasttime = None, state = None):
+        if lasttime == None:
+            lasttime = int(time.time())
+        if state == None:
+            return self.getmesgs(lasttime)
+        if state == 'DRAFT':
+            self.drafts = self.load_list('draft')
+            times = self.drafts
+        elif state == 'NEW':
+            self.newmesg = self.load_list('newmesg')
+            times = self.newmesg
+        else:
+            raise ValueError
+
+        self.set_files()
+        self.cached_month = None
+        self.cache = None
+        rv = []
+        for t in times:
+            if t > lasttime:
+                continue
+            s = self.load(t)
+            if s:
+                s.state = state
+                rv.append(s)
+        return(0, rv)
+
+    def getmesgs(self, last):
+        rv = []
+        for m in self.files:
+            t = parse_time(m + '01-000000')
+            if t > last:
+                continue
+            mon = self.load_month(open(self.dirname + '/' + m))
+            for mt in mon:
+                if mt <= last:
+                    rv.append(mon[mt])
+            if rv:
+                rv.sort(cmp = lambda x,y:cmp(y.stamp, x.stamp))
+                return (t-1, rv)
+        return (0, [])
+
+    def load(self, t):
+        m = time.strftime("%Y%m", time.gmtime(t))
+        if not m in self.files:
+            return None
+        if self.cached_month != m:
+            self.cached_month = m
+            self.cache = self.load_month(open(self.dirname + '/' + m))
+        if t in self.cache:
+            return self.cache[t]
+        return None
+
+    def delete(self, msg):
+        if isinstance(msg, SMSmesg):
+            tm = msg.stamp
+        else:
+            tm = msg
+        m = time.strftime("%Y%m", time.gmtime(tm))
+        try:
+            f = open(self.dirname + '/' + m, "r+")
+        except:
+            return
+
+        fcntl.lockf(f, fcntl.LOCK_EX)
+        l = self.load_month(f)
+        if tm in l:
+            del l[tm]
+        self.store_month(l, m);
+        f.close()
+
+        def del1(l, tm):
+            if tm in l:
+                l.remove(tm)
+                return True
+            return False
+
+        self.drafts = self.load_list('draft', del1, tm)
+        self.newmesg = self.load_list('newmesg', del1, tm)
+
+    def setstate(self, msg, state):
+        tm = msg.stamp
+
+        def del1(l, tm):
+            if tm in l:
+                l.remove(tm)
+                return True
+            return False
+
+        if tm in self.drafts and state != 'DRAFT':
+            self.drafts = self.load_list('draft', del1, tm)
+        if tm in self.newmesg and state != 'NEW':
+            self.newmesg = self.load_list('newmesg', del1, tm)
+
+        if tm not in self.drafts and state == 'DRAFT':
+            f = open(self.dirname +'/draft', 'a')
+            fcntl.lockf(f, fcntl.LOCK_EX)
+            f.write(format_time(sms.stamp) + '\n')
+            f.close()
+            self.drafts.append(tm)
+            self.drafts.sort()
+            self.drafts.reverse()
+
+        if tm not in self.newmesg and state == 'NEW':
+            f = open(self.dirname +'/newmesg', 'a')
+            fcntl.lockf(f, fcntl.LOCK_EX)
+            f.write(format_time(sms.stamp) + '\n')
+            f.close()
+            self.newmesg.append(tm)
+            self.newmesg.sort()
+            self.newmesg.reverse()
+
+
diff --git a/sound/list.h b/sound/list.h
new file mode 100644 (file)
index 0000000..8626630
--- /dev/null
@@ -0,0 +1,289 @@
+/*
+ * Copied from the Linux kernel source tree, version 2.6.0-test1.
+ *
+ * Licensed under the GPL v2 as per the whole kernel source tree.
+ *
+ */
+
+#ifndef _LIST_H
+#define _LIST_H
+
+/**
+ * container_of - cast a member of a structure out to the containing structure
+ *
+ * @ptr:       the pointer to the member.
+ * @type:      the type of the container struct this is embedded in.
+ * @member:    the name of the member within the struct.
+ *
+ */
+#define container_of(ptr, type, member) ({                     \
+       const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
+       (type *)( (char *)__mptr - offsetof(type,member) );})
+
+/*
+ * These are non-NULL pointers that will result in page faults
+ * under normal circumstances, used to verify that nobody uses
+ * non-initialized list entries.
+ */
+#define LIST_POISON1  ((void *) 0x00100100)
+#define LIST_POISON2  ((void *) 0x00200200)
+
+/*
+ * Simple doubly linked list implementation.
+ *
+ * Some of the internal functions ("__xxx") are useful when
+ * manipulating whole lists rather than single entries, as
+ * sometimes we already know the next/prev entries and we can
+ * generate better code by using them directly rather than
+ * using the generic single-entry routines.
+ */
+
+struct list_head {
+       struct list_head *next, *prev;
+};
+
+#define LIST_HEAD_INIT(name) { &(name), &(name) }
+
+#define LIST_HEAD(name) \
+       struct list_head name = LIST_HEAD_INIT(name)
+
+#define INIT_LIST_HEAD(ptr) do { \
+       (ptr)->next = (ptr); (ptr)->prev = (ptr); \
+} while (0)
+
+/*
+ * Insert a new entry between two known consecutive entries. 
+ *
+ * This is only for internal list manipulation where we know
+ * the prev/next entries already!
+ */
+static inline void __list_add(struct list_head *new,
+                             struct list_head *prev,
+                             struct list_head *next)
+{
+       next->prev = new;
+       new->next = next;
+       new->prev = prev;
+       prev->next = new;
+}
+
+/**
+ * list_add - add a new entry
+ * @new: new entry to be added
+ * @head: list head to add it after
+ *
+ * Insert a new entry after the specified head.
+ * This is good for implementing stacks.
+ */
+static inline void list_add(struct list_head *new, struct list_head *head)
+{
+       __list_add(new, head, head->next);
+}
+
+/**
+ * list_add_tail - add a new entry
+ * @new: new entry to be added
+ * @head: list head to add it before
+ *
+ * Insert a new entry before the specified head.
+ * This is useful for implementing queues.
+ */
+static inline void list_add_tail(struct list_head *new, struct list_head *head)
+{
+       __list_add(new, head->prev, head);
+}
+
+/*
+ * Delete a list entry by making the prev/next entries
+ * point to each other.
+ *
+ * This is only for internal list manipulation where we know
+ * the prev/next entries already!
+ */
+static inline void __list_del(struct list_head * prev, struct list_head * next)
+{
+       next->prev = prev;
+       prev->next = next;
+}
+
+/**
+ * list_del - deletes entry from list.
+ * @entry: the element to delete from the list.
+ * Note: list_empty on entry does not return true after this, the entry is
+ * in an undefined state.
+ */
+static inline void list_del(struct list_head *entry)
+{
+       __list_del(entry->prev, entry->next);
+       entry->next = LIST_POISON1;
+       entry->prev = LIST_POISON2;
+}
+
+/**
+ * list_del_init - deletes entry from list and reinitialize it.
+ * @entry: the element to delete from the list.
+ */
+static inline void list_del_init(struct list_head *entry)
+{
+       __list_del(entry->prev, entry->next);
+       INIT_LIST_HEAD(entry); 
+}
+
+/**
+ * list_move - delete from one list and add as another's head
+ * @list: the entry to move
+ * @head: the head that will precede our entry
+ */
+static inline void list_move(struct list_head *list, struct list_head *head)
+{
+       __list_del(list->prev, list->next);
+       list_add(list, head);
+}
+
+/**
+ * list_move_tail - delete from one list and add as another's tail
+ * @list: the entry to move
+ * @head: the head that will follow our entry
+ */
+static inline void list_move_tail(struct list_head *list,
+                                 struct list_head *head)
+{
+       __list_del(list->prev, list->next);
+       list_add_tail(list, head);
+}
+
+/**
+ * list_empty - tests whether a list is empty
+ * @head: the list to test.
+ */
+static inline int list_empty(struct list_head *head)
+{
+       return head->next == head;
+}
+
+static inline void __list_splice(struct list_head *list,
+                                struct list_head *head)
+{
+       struct list_head *first = list->next;
+       struct list_head *last = list->prev;
+       struct list_head *at = head->next;
+
+       first->prev = head;
+       head->next = first;
+
+       last->next = at;
+       at->prev = last;
+}
+
+/**
+ * list_splice - join two lists
+ * @list: the new list to add.
+ * @head: the place to add it in the first list.
+ */
+static inline void list_splice(struct list_head *list, struct list_head *head)
+{
+       if (!list_empty(list))
+               __list_splice(list, head);
+}
+
+/**
+ * list_splice_init - join two lists and reinitialise the emptied list.
+ * @list: the new list to add.
+ * @head: the place to add it in the first list.
+ *
+ * The list at @list is reinitialised
+ */
+static inline void list_splice_init(struct list_head *list,
+                                   struct list_head *head)
+{
+       if (!list_empty(list)) {
+               __list_splice(list, head);
+               INIT_LIST_HEAD(list);
+       }
+}
+
+/**
+ * list_entry - get the struct for this entry
+ * @ptr:       the &struct list_head pointer.
+ * @type:      the type of the struct this is embedded in.
+ * @member:    the name of the list_struct within the struct.
+ */
+#define list_entry(ptr, type, member) \
+       container_of(ptr, type, member)
+
+/**
+ * list_for_each       -       iterate over a list
+ * @pos:       the &struct list_head to use as a loop counter.
+ * @head:      the head for your list.
+ */
+#define list_for_each(pos, head) \
+       for (pos = (head)->next; pos != (head); \
+               pos = pos->next)
+
+/**
+ * __list_for_each     -       iterate over a list
+ * @pos:       the &struct list_head to use as a loop counter.
+ * @head:      the head for your list.
+ *
+ * This variant differs from list_for_each() in that it's the
+ * simplest possible list iteration code.
+ * Use this for code that knows the list to be very short (empty
+ * or 1 entry) most of the time.
+ */
+#define __list_for_each(pos, head) \
+       for (pos = (head)->next; pos != (head); pos = pos->next)
+
+/**
+ * list_for_each_prev  -       iterate over a list backwards
+ * @pos:       the &struct list_head to use as a loop counter.
+ * @head:      the head for your list.
+ */
+#define list_for_each_prev(pos, head) \
+       for (pos = (head)->prev; pos != (head); pos = pos->prev)
+
+/**
+ * list_for_each_safe  -       iterate over a list safe against removal of list entry
+ * @pos:       the &struct list_head to use as a loop counter.
+ * @n:         another &struct list_head to use as temporary storage
+ * @head:      the head for your list.
+ */
+#define list_for_each_safe(pos, n, head) \
+       for (pos = (head)->next, n = pos->next; pos != (head); \
+               pos = n, n = pos->next)
+
+/**
+ * list_for_each_entry -       iterate over list of given type
+ * @pos:       the type * to use as a loop counter.
+ * @head:      the head for your list.
+ * @member:    the name of the list_struct within the struct.
+ */
+#define list_for_each_entry(pos, head, member)                         \
+       for (pos = list_entry((head)->next, typeof(*pos), member);      \
+            &pos->member != (head);                                    \
+            pos = list_entry(pos->member.next, typeof(*pos), member))
+
+/**
+ * list_for_each_entry_reverse - iterate backwards over list of given type.
+ * @pos:       the type * to use as a loop counter.
+ * @head:      the head for your list.
+ * @member:    the name of the list_struct within the struct.
+ */
+#define list_for_each_entry_reverse(pos, head, member)                 \
+       for (pos = list_entry((head)->prev, typeof(*pos), member);      \
+            &pos->member != (head);                                    \
+            pos = list_entry(pos->member.prev, typeof(*pos), member))
+
+/**
+ * list_for_each_entry_safe - iterate over list of given type safe against removal of list entry
+ * @pos:       the type * to use as a loop counter.
+ * @n:         another type * to use as temporary storage
+ * @head:      the head for your list.
+ * @member:    the name of the list_struct within the struct.
+ */
+#define list_for_each_entry_safe(pos, n, head, member)                 \
+       for (pos = list_entry((head)->next, typeof(*pos), member),      \
+               n = list_entry(pos->member.next, typeof(*pos), member); \
+            &pos->member != (head);                                    \
+            pos = n, n = list_entry(n->member.next, typeof(*n), member))
+
+#endif /* _LIST_H */
diff --git a/sound/notes b/sound/notes
new file mode 100644 (file)
index 0000000..8f109ce
--- /dev/null
@@ -0,0 +1,102 @@
+
+       snd_config_t *config;
+       file = "/data/senarios/$CHOICE";
+       err = snd_config_top(&config);
+       err = snd_input_stdio_open(&in, file, "r");
+       err = snd_config_load(config, in);
+       snd_input_close(in);
+       cardno = snd_card_get_index(cardname);
+
+       char name[32];
+       snd_ctl_t *handle;
+       snd_ctl_card_info_t *info;
+       snd_ctl_card_info_alloca(&info);
+       sprintf(name, "hw:%d", cardno);
+       err = snd_ctl_open(&handle, name, 0);
+       err = snd_ctl_card_info(handle, info);
+       id = snd_ctl_card_info_get_id(info);
+       err = snd_config_searchv(config, &control, "state", id, "control", 0);
+       snd_config_for_each(i, next, control) {
+               snd_config_t *n = snd_config_iterator_entry(i);
+       snd_ctl_elem_value_alloca(&ctl);
+       snd_ctl_elem_info_alloca(&info);
+       err = snd_config_get_id(control, &id);
+
+
+               err = set_control(handle, n);
+               if (err < 0 && ! force_restore)
+                       goto _close;
+       }
+
+
+-------------
+
+Enhancements:
+ want
+   volume control
+   route to bluetooth instead
+   seek, and restart a second or two back
+   route between GSM and main/bluetooth
+   record input - set samples etc?
+   mix samples with routing
+
+separate directory.
+When file appears there, recording is started
+If a symlink appears that names an alsa device, mix the
+recording in to that device.  It should also be echo-cancelled
+with the output if enabled for the devices
+(we enable for main and bt)
+   
+Cards are: gta04 gta04voice gta04headset  (CARD=gta04).
+
+need 'plug:' to ensure sample rate conversion etc.
+need 'dmix:' to allow other sounds to play.
+
+
+We can have multiple devices open at once, which might use
+dmix to output to the one device.
+Each 'record' device can be paired with a 'play' device.
+A record and a play can be marked for echo cancelling.
+This mixes the 'play' stream into the 'record' stream so that
+the echo disappears from it.  For this to work, the 'record'
+stream must be nearly synced with the 'play' stream.
+There is no point having 'play' samples which haven't gone out yet
+as they cannot have echo.  Rather I want the samples that I just
+played to mix with the samples that I just recored.
+
+However gsm-voice-routing doesn't do that.  It mixes the 'just
+recorded' on each path.??
+
+I need to work in whole periods and I would like these to be
+closely synced... Can I do that?
+Not easily.  I just need to keep the periods low so the sync difference
+is small.
+
+gsm-voice-routing uses a period_size of 256 of 32msec.
+oslec say 16ms or 32ms tail is fine.  64ms would cope with
+out-of-sync buffers.
+
+So... 
+ all processing must be async
+ several sounds with same priority can play at once
+ a sound file which contains 'gsm' triggers copying between
+  gta04voice and current device.
+  We start recording and don't start playing until a sample
+   is available.
+
+So the active things can be:
+ 'sound' which has a plug:dmix: device it is being written to
+ 'mic' which is the souce on the main device
+ 'gsmout' which 'mic' is being copied to
+ 'gsmin' which is being read in
+ 'speaker' which is another plug:dmix:
+
+So we can happily keep the voice routing in a separate process
+and still allow mixing.
+
+
+
+Copying needs to wait for 'voice' to start delivering data.
+So:
+ - configure all stream.
+ - 
\ No newline at end of file
diff --git a/sound/sound.c b/sound/sound.c
new file mode 100644 (file)
index 0000000..f2f0b4d
--- /dev/null
@@ -0,0 +1,816 @@
+/* TOFIX
+ * Record where up to - and total length
+ * Don't reconfigure between identical format sounds.
+ * handle seek for ogg vorbis
+ * ??handle MP3 in WAV files
+ * long gap between sounds.??? maybe all those zeros?
+ */
+
+/*
+ * 
+ * This is a daemon that waits for sound files to appear in a particular
+ * directory, and when they do, it plays them.
+ * Files can be WAV or OGG VORBIS
+ * If there are multiple files, the lexically first is played
+ * If a file has a suffix of -NNNNNNN, then play starts that many
+ * milliseconds in to the file.
+ * When a file disappear, play stops.
+ * When the end of the sound is reached the file (typically a link) is removed.
+ * However an empty file is treated as containing infinite silence, so
+ * it is never removed.
+ * When a new file appears which is lexically earlier than the one being
+ * played, the played file is suspended until the earlier files are finished
+ * with.
+ * The current-play position (in milliseconds) is occasionally written to a file
+ * with the same name as the sound file, but with a leading period.
+ * This file starts with 'P' if the sound is playing, and 'S' if it has
+ * stopped.  When playing, the actual position can be calculated using
+ * the current time and mtime of the file.
+ *
+ * Expected use is that various alert tones are added to the directory with
+ * early names, and a music file can be added with a later name for general
+ * listening.
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+ *
+ */
+
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <malloc.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <alsa/asoundlib.h>
+#include <sys/signal.h>
+#include <sys/dir.h>
+#include <asm/byteorder.h>
+#include <vorbis/vorbisfile.h>
+#include <event.h>
+
+#include "list.h"
+#include "libsus.h"
+
+#define WAV_RIFF       COMPOSE_ID('R','I','F','F')
+#define WAV_WAVE       COMPOSE_ID('W','A','V','E')
+#define WAV_FMT                COMPOSE_ID('f','m','t',' ')
+#define WAV_DATA       COMPOSE_ID('d','a','t','a')
+#define WAV_PCM_CODE   1
+
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+#define COMPOSE_ID(a,b,c,d)    ((a) | ((b)<<8) | ((c)<<16) | ((d)<<24))
+#define LE_SHORT(v)    (v)
+#define LE_INT(v)      (v)
+#define BE_SHORT(v)    bswap_16(v)
+#define BE_INT(v)      bswap_32(v)
+#elif __BYTE_ORDER == __BIG_ENDIAN
+#define COMPOSE_ID(a,b,c,d)    ((d) | ((c)<<8) | ((b)<<16) | ((a)<<24))
+#define LE_SHORT(v)    bswap_16(v)
+#define LE_INT(v)      bswap_32(v)
+#define BE_SHORT(v)    (v)
+#define BE_INT(v)      (v)
+#else
+#error "Wrong endian"
+#endif
+
+typedef struct {
+       u_int magic;    /* 'RIFF' */
+       u_int length;   /* filelen */
+       u_int type;     /* 'WAVE' */
+} WaveHeader;
+
+typedef struct {
+       u_short format;         /* should be 1 for PCM-code */
+       u_short modus;          /* 1 Mono, 2 Stereo */
+       u_int sample_fq;        /* frequence of sample */
+       u_int byte_p_sec;
+       u_short byte_p_spl;     /* samplesize; 1 or 2 bytes */
+       u_short bit_p_spl;      /* 8, 12 or 16 bit */
+} WaveFmtBody;
+
+typedef struct {
+       u_int type;             /* 'data' or 'fmt ' */
+       u_int length;           /* samplecount */
+} WaveChunkHeader;
+
+
+#ifndef LLONG_MAX
+#define LLONG_MAX      9223372036854775807LL
+#endif
+
+#define DEFAULT_FORMAT         SND_PCM_FORMAT_U8
+#define DEFAULT_SPEED          8000
+
+#define FORMAT_DEFAULT         -1
+#define FORMAT_RAW             0
+#define FORMAT_VOC             1
+#define FORMAT_WAVE            2
+#define FORMAT_AU              3
+#define FORMAT_VORBIS          4
+#define FORMAT_UNKNOWN         9999
+/* global data */
+
+
+struct sound {
+       int fd;
+       int empty;
+       struct list_head list;
+       int seen;
+       char *name;
+       char scenario[20];
+       int ino;
+       long posn;
+       int format; /* FORMAT_WAVE or FORMAT_OGG */
+       char buf[8192];
+       int period_bytes;
+       int bytes, bytes_used, last_read;
+       int eof;
+
+       /* An audio file can contain multiple chunks.
+        * Here we record the remaining bytes expected
+        * before a header
+        */
+       int chunk_bytes;
+
+       OggVorbis_File vf;
+
+       int pcm_format;
+       int sample_bytes;
+       int channels;
+       int rate;
+};
+
+struct dev {
+       snd_pcm_t       *handle;
+       char            *period_buf;
+       char            scenario[20];
+       int             buf_size;
+       int             period_bytes;
+       int             sample_bytes;
+       int             present;
+       int             pcm_format, channels, rate;
+};
+
+int dir_changed = 1;
+
+void handle_change(int sig)
+{
+       dir_changed = 1;
+}
+
+static void *raw_read(struct sound *s, int bytes)
+{
+       /* Return pointer to next 'bytes' bytes in the buffer.
+        * If there aren't that many, copy down and read some
+        * more.  If we hit EOF, return NULL and set ->eof.
+        */
+       while (s->bytes - s->bytes_used < bytes && !s->eof) {
+               if (s->bytes_used &&
+                   s->bytes_used < s->bytes)
+                       memmove(s->buf, s->buf+s->bytes_used,
+                               s->bytes - s->bytes_used);
+               s->bytes -= s->bytes_used;
+               s->bytes_used = 0;
+               while (s->bytes < bytes && !s->eof) {
+                       int n = read(s->fd, s->buf+s->bytes, sizeof(s->buf) - s->bytes);
+                       if (n <= 0)
+                               s->eof = 1;
+                       else
+                               s->bytes += n;
+               }
+       }
+       if (s->bytes - s->bytes_used < bytes)
+               return NULL;
+       else {
+               void *rv = s->buf + s->bytes_used;
+               s->bytes_used += bytes;
+               s->last_read = bytes;
+               return rv;
+       }
+}
+
+static void raw_unread(struct sound *s)
+{
+       s->bytes_used -= s->last_read;
+       s->last_read = 0;
+}
+
+static void raw_skip(struct sound *s, int bytes)
+{
+       if (s->bytes - s->bytes_used >= bytes) {
+               s->bytes_used += bytes;
+               return;
+       }
+       bytes -= (s->bytes - s->bytes_used);
+       s->bytes = 0;
+       s->bytes_used = 0;
+       lseek(s->fd, bytes, SEEK_CUR);
+}
+
+
+int parse_wave(struct sound *s)
+{
+       WaveHeader *h;
+       WaveChunkHeader *c;
+       WaveFmtBody *f;
+       int n;
+       int len;
+
+       h = raw_read(s, sizeof(*h));
+       if (!h)
+               return 0;
+       if (h->magic != WAV_RIFF || h->type != WAV_WAVE) {
+               raw_unread(s);
+               return 0;
+       }
+       /* Ignore the length - wait for EOF */
+       while (1) {
+               c = raw_read(s, sizeof(*c));
+               if (!c)
+                       return 0;
+               len = LE_INT(c->length);
+               len += len % 2;
+               if (c->type == WAV_FMT)
+                       break;
+               raw_skip(s, len);
+       }
+       if (len < sizeof(WaveFmtBody))
+               return 0;
+       f = raw_read(s, len);
+       if (!f)
+               return 0;
+
+       if (LE_SHORT(f->format) == 1 &&
+           LE_SHORT(f->bit_p_spl) == 16) {
+               s->pcm_format = SND_PCM_FORMAT_S16_LE;
+               s->sample_bytes = 2;
+       } else if (LE_SHORT(f->format) == 1 &&
+                  LE_SHORT(f->bit_p_spl) == 8) {
+               s->pcm_format = SND_PCM_FORMAT_U8;
+               s->sample_bytes = 1;
+       } else
+               return 0;
+       s->channels = LE_SHORT(f->modus);
+       if (s->channels < 1 || s->channels > 2)
+               return 0;
+       s->sample_bytes *= s->channels;
+       s->rate = LE_INT(f->sample_fq);
+       s->eof = 0;
+       s->chunk_bytes = 0;
+       return 1;
+}
+
+static int read_wave(struct sound *s, char *buf, int bytes)
+{
+       WaveChunkHeader *h;
+       int len;
+       int b = 0;
+       char *rbuf;
+       int i;
+
+       while (b < bytes) {
+               while (s->chunk_bytes == 0) {
+                       h = raw_read(s, sizeof(*h));
+                       if (!h)
+                               break;
+                       len = LE_INT(h->length);
+                       len += len % 2;
+                       if (h->type != WAV_DATA) {
+                               raw_skip(s, len);
+                               continue;
+                       }
+                       s->chunk_bytes = len;
+               }
+               if (s->chunk_bytes == 0)
+                       break;
+               if (s->chunk_bytes >= bytes - b) {
+                       rbuf = raw_read(s, bytes - b);
+                       if (!rbuf)
+                               break;
+                       s->chunk_bytes -= bytes - b;
+                       memcpy(buf + b, rbuf, bytes - b);
+                       return bytes;
+               }
+               /* Remainder of chunk is less than we need */
+               rbuf = raw_read(s, s->chunk_bytes);
+               if (!rbuf)
+                       break;
+               memcpy(buf + b, rbuf, s->chunk_bytes);
+               b += s->chunk_bytes;
+               s->chunk_bytes = 0;
+       }
+
+       rbuf = buf;
+       for (i = b; i < bytes && s->chunk_bytes > 1; i++) {
+               char *bf = raw_read(s, 1);
+               if (bf)
+                       buf[i] = bf[0];
+               else
+                       break;
+               s->chunk_bytes--;
+       }
+       return i;
+}
+
+void seek_wave(struct sound *s, int msec)
+{
+       /* skip over 'msec' milliseconds of the wave */
+       WaveChunkHeader *h;
+       int bytes = ((long long)msec * s->rate / 1000) * s->sample_bytes;
+       int len;
+       
+       while (bytes) {
+               while (s->chunk_bytes == 0) {
+                       h = raw_read(s, sizeof(*h));
+                       if (!h)
+                               break;
+                       len = LE_INT(h->length);
+                       len += len % 2;
+                       if (h->type != WAV_DATA) {
+                               raw_skip(s, len);
+                               continue;
+                       }
+                       s->chunk_bytes = len;
+               }
+               if (s->chunk_bytes == 0)
+                       break;
+               if (s->chunk_bytes >= bytes) {
+                       raw_skip(s, bytes);
+                       s->chunk_bytes -= bytes;
+                       return;
+               }
+               raw_skip(s, s->chunk_bytes);
+               bytes -= s->chunk_bytes;
+               s->chunk_bytes = 0;
+       }
+}
+
+static int parse_vorbis(struct sound *s)
+{
+       FILE *f;
+       vorbis_info *vi;
+       int rv;
+
+       lseek(s->fd, 0, 0);
+       f = fdopen(s->fd, "r");
+       if ((rv = ov_open_callbacks(f, &s->vf, NULL, 0, OV_CALLBACKS_DEFAULT)) < 0) {
+               fclose(f);
+               return 0;
+       }
+
+       vi = ov_info(&s->vf,-1);
+
+       s->pcm_format = SND_PCM_FORMAT_S16_LE;
+       s->channels = vi->channels;
+       s->rate = vi->rate;
+       s->sample_bytes = 2 * s->channels;
+       s->eof = 0;
+       return 1;
+}
+
+static int read_vorbis(struct sound *s, char *buf, int len)
+{
+       int section;
+       int have = 0;
+       if (s->eof)
+               return 0;
+
+       while(!s->eof && have < len) {
+               long ret = ov_read(&s->vf,
+                                  buf + have,
+                                  len - have,
+                                  0,2,1,&section);
+               if (ret == 0) {
+                       /* EOF */
+                       s->eof=1;
+               } else if (ret < 0) {
+                       /* error in the stream.  Not a problem, just reporting it in
+                          case we (the app) cares.  In this case, we don't. */
+                       s->eof = 1;
+               } else {
+                       /* we don't bother dealing with sample rate changes, etc, but
+                          you'll have to*/
+                       have += ret;
+               }
+       }
+
+       if (s->eof)
+               ov_clear(&s->vf);
+       return have;
+}
+
+static void seek_vorbis(struct sound *s, int msec)
+{
+}
+
+
+snd_pcm_t *open_dev(void)
+{
+       snd_pcm_t *handle;
+       int rc;
+
+       rc = snd_pcm_open(&handle, "default", SND_PCM_STREAM_PLAYBACK, 0/*SND_PCM_NONBLOCK*/);
+       if (rc < 0)
+               return NULL;
+       else
+               return handle;
+}
+
+void dev_close(struct dev *dev);
+void set_scenario(struct dev *dev, char *scenario)
+{
+       char path[100];
+       if (scenario[0] == 0)
+               return;
+       if (strcmp(dev->scenario, scenario) == 0)
+               return;
+       dev_close(dev);
+       strcpy(dev->scenario, scenario);
+       snprintf(path, 100, "alsactl -f /data/scenarios/%s restore", scenario);
+       system(path);
+}
+
+void set_params(struct dev *dev, struct sound *sound)
+{
+       snd_pcm_hw_params_t *hwp;
+
+       if (sound->format == FORMAT_UNKNOWN)
+               return;
+
+       set_scenario(dev, sound->scenario);
+       if (dev->handle == NULL) {
+               dev->handle = open_dev();
+               dev->pcm_format = 0;
+       }
+
+       if (dev->pcm_format == sound->pcm_format &&
+           dev->channels == sound->channels &&
+           dev->rate == sound->rate)
+               return;
+
+       if (dev->pcm_format)
+               snd_pcm_drop(dev->handle);
+
+       snd_pcm_hw_params_alloca(&hwp);
+       snd_pcm_hw_params_any(dev->handle, hwp);
+       snd_pcm_hw_params_set_access(dev->handle, hwp, SND_PCM_ACCESS_RW_INTERLEAVED);
+       snd_pcm_hw_params_set_format(dev->handle, hwp, sound->pcm_format);
+       snd_pcm_hw_params_set_channels(dev->handle, hwp, sound->channels);
+       snd_pcm_hw_params_set_rate(dev->handle, hwp, sound->rate, 0);
+       snd_pcm_hw_params_set_period_size(dev->handle, hwp,
+                                         sound->period_bytes/sound->sample_bytes, 0);
+       snd_pcm_hw_params_set_buffer_size(dev->handle, hwp,
+                                         sound->period_bytes*4/sound->sample_bytes);
+       snd_pcm_hw_params(dev->handle, hwp);
+       dev->pcm_format = sound->pcm_format;
+       dev->channels = sound->channels;
+       dev->rate = sound->rate;
+       dev->sample_bytes = sound->sample_bytes;
+       dev->period_bytes = sound->period_bytes;
+       if (dev->buf_size < dev->period_bytes) {
+               free(dev->period_buf);
+               dev->period_buf = malloc(dev->period_bytes);
+               dev->buf_size = dev->period_bytes;
+       }
+}
+
+void load_some(struct dev *dev, struct sound *sound)
+{
+       int len;
+       if (!dev || !dev->handle || !sound)
+               return;
+
+       switch(sound->format) {
+       case FORMAT_WAVE:
+               len = read_wave(sound,
+                               dev->period_buf + dev->present,
+                               dev->period_bytes - dev->present);
+               break;
+       case FORMAT_VORBIS:
+               len = read_vorbis(sound,
+                                 dev->period_buf + dev->present,
+                                 dev->period_bytes - dev->present);
+               break;
+       default:
+               sound->eof = 1;
+               len = 0;
+       }
+       dev->present += len;
+}
+
+struct sound *open_sound(char *dir, char *name, int ino)
+{
+       char path[200];
+       int fd;
+       struct sound *s;
+       char *eos, *eos1;
+
+       strcpy(path, dir);
+       strcat(path, "/");
+       strcat(path, name);
+       fd = open(path, O_RDONLY);
+       if (fd < 0)
+               return NULL;
+       s = malloc(sizeof(*s));
+       if (!s)
+               return NULL;
+       memset(s, 0, sizeof(*s));
+       s->fd = fd;
+       s->empty = 0;
+       s->eof = 0;
+       s->seen = 0;
+       s->name = strdup(name);
+       s->ino = ino;
+       s->posn = 0;
+       s->bytes = s->bytes_used = 0;
+
+       /* check for millisecond suffix */
+       eos = name + strlen(name);
+       while (eos > name && isdigit(eos[-1]))
+               eos--;
+       if (eos > name && eos[-1] == '-' && eos[0]) {
+               s->posn = atol(eos);
+               eos--;
+       }
+       /* Now pick off scenario name */
+       eos1 = eos;
+       while (eos1 > name && isalpha(eos1[-1]))
+               eos1--;
+       if (eos1 > name && eos1 < eos && eos - eos1 < 20) {
+               strncpy(s->scenario, eos1, eos-eos1);
+               s->scenario[eos-eos1] = 0;
+       }
+
+       if (lseek(fd, 0L, 2) == 0) {
+               close(fd);
+               s->fd = -1;
+               s->empty = 1;
+               s->format = FORMAT_UNKNOWN;
+               return s;
+       }
+       lseek(fd, 0L, 0);
+       /* Read header and set parameters */
+
+       if (parse_wave(s))
+               s->format = FORMAT_WAVE;
+       else if (parse_vorbis(s))
+               s->format = FORMAT_VORBIS;
+       else
+               s->format = FORMAT_UNKNOWN;
+
+       if (s->rate <= 8000) {
+               /* 100 ms == 800 samples, 1600 bytes */
+               s->period_bytes = s->rate / 100 * s->sample_bytes;
+       } else {
+               /* 44100, 2 seconds, 4 bytes would be 160K !!
+                * Doesn't work yet.
+                */
+               s->period_bytes = 8192;
+       }
+
+       if (s->posn)
+               switch(s->format) {
+               case FORMAT_WAVE:
+                       seek_wave(s, s->posn);
+                       break;
+               case FORMAT_VORBIS:
+                       seek_vorbis(s, s->posn);
+                       break;
+               }
+
+       return s;
+
+ fail:
+       close(s->fd);
+       free(s->name);
+       free(s);
+       return NULL;
+}
+
+void close_sound(struct sound *sound)
+{
+       close(sound->fd);
+       free(sound->name);
+       free(sound);
+}
+
+
+struct sound *find_match(struct list_head *list,
+                        char *name, int ino,
+                        int *matched)
+{
+       /* If name/ino is found in list, return it and set
+        * matched.
+        * else return previous entry (or NULL) and clear matched.
+        */
+       struct sound *rv = NULL;
+       struct sound *s;
+
+       *matched = 0;
+       list_for_each_entry(s, list, list) {
+               int c = strcmp(s->name, name);
+               if (c > 0)
+                       /* we have gone beyond */
+                       break;
+               rv = s;
+               if (c == 0) {
+                       if (s->ino == ino)
+                               *matched = 1;
+                       break;
+               }
+       }
+       return rv;
+}
+
+void scan_dir(char *path, struct list_head *soundqueue)
+{
+       DIR *dir = opendir(path);
+       struct dirent *de;
+       struct sound *match;
+       struct sound *pos;
+
+       list_for_each_entry(match, soundqueue, list)
+               match->seen = 0;
+
+       while ((de = readdir(dir)) != NULL) {
+               struct sound *new;
+               int matched = 0;
+               if (de->d_ino == 0 ||
+                   de->d_name[0] == '.')
+                       continue;
+
+               match = find_match(soundqueue, de->d_name, de->d_ino, &matched);
+               if (matched) {
+                       match->seen = 1;
+                       continue;
+               }
+               new = open_sound(path, de->d_name, de->d_ino);
+               if (! new)
+                       continue;
+               new->seen = 1;
+               if (match)
+                       list_add(&new->list, &match->list);
+               else
+                       list_add(&new->list, soundqueue);
+       }
+       closedir(dir);
+
+       list_for_each_entry_safe(match, pos, soundqueue, list)
+               if (!match->seen) {
+                       list_del(&match->list);
+                       close_sound(match);
+               }
+}
+
+void play_buf(struct dev *dev)
+{
+       if (dev->present == dev->period_bytes) {
+               alarm(30);
+               snd_pcm_writei(dev->handle,
+                              dev->period_buf,
+                              dev->period_bytes / dev->sample_bytes);
+               alarm(0);
+               dev->present = 0;
+       }
+}
+
+void dev_close(struct dev *dev)
+{
+       if (!dev->handle)
+               return;
+       if (dev->present) {
+               memset(dev->period_buf + dev->present, 0,
+                      dev->period_bytes - dev->present);
+               snd_pcm_writei(dev->handle,
+                              dev->period_buf,
+                              dev->period_bytes / dev->sample_bytes);
+               dev->present = 0;
+       }
+       snd_pcm_drain(dev->handle);
+       snd_pcm_close(dev->handle);
+       dev->handle = NULL;
+}
+
+char *dir = "/var/run/sound";
+int dfd;
+struct dev dev;
+int suspend_handle;
+struct sound *last = NULL;
+struct event work_ev;
+
+static void do_scan(int fd, short ev, void *vp)
+{
+       struct list_head *soundqueue = vp;
+       struct sound *next;
+       struct timeval tv = {0, 0};
+
+       fcntl(dfd, F_NOTIFY, DN_CREATE|DN_DELETE|DN_RENAME);
+       scan_dir(dir, soundqueue);
+
+       if (list_empty(soundqueue)) {
+               dev_close(&dev);
+               set_scenario(&dev, "off");
+               suspend_allow(suspend_handle);
+               last = NULL;
+               return;
+       }
+       next = list_entry(soundqueue->next,
+                         struct sound, list);
+       if (next->empty) {
+               dev_close(&dev);
+               set_scenario(&dev, next->scenario);
+               suspend_allow(suspend_handle);
+               last = next;
+               return;
+       }
+       suspend_block(suspend_handle);
+       event_add(&work_ev, &tv);
+}
+
+static void do_work(int fd, short ev, void *vp)
+{
+       struct list_head *soundqueue = vp;
+       struct sound *next;
+
+       if (list_empty(soundqueue)) {
+               set_scenario(&dev, "off");
+               return;
+       }
+       next = list_entry(soundqueue->next,
+                         struct sound, list);
+       if (next->empty) {
+               set_scenario(&dev, next->scenario);
+               return;
+       }
+
+       if (next != last) {
+               set_params(&dev, next);
+               last = next;
+       }
+       load_some(&dev, next);
+       play_buf(&dev);
+       if (next->eof) {
+               char buf[1000];
+               sprintf(buf, "%s/%s", dir, next->name);
+               unlink(buf);
+               list_del(&next->list);
+               close_sound(next);
+               do_scan(fd, ev, vp);
+       } else {
+               struct timeval tv = {0, 0};
+               event_add(&work_ev, &tv);
+       }
+}
+
+static int suspend(void *vp)
+{
+       /* Don't need to do anything here, just as long as
+        * we cause suspend to block until check_alarms
+        * had a chance to run.
+        */
+       do_scan(-1, 0, vp);
+       return 1;
+}
+
+
+int main(int argc, char *argv[])
+{
+       struct list_head soundqueue;
+       struct event ev;
+
+       INIT_LIST_HEAD(&soundqueue);
+
+       suspend_handle = suspend_block(-1);
+       mkdir(dir, 0755);
+       dfd = open(dir, O_RDONLY|O_DIRECTORY);
+       if (dfd < 0) {
+               fprintf(stderr, "sound: Cannot open %s\n", dir);
+               exit(1);
+       }
+
+       event_init();
+
+       suspend_watch(suspend, NULL, &soundqueue);
+       signal_set(&ev, SIGIO, do_scan, &soundqueue);
+       signal_add(&ev, NULL);
+       fcntl(dfd, F_NOTIFY, DN_CREATE|DN_DELETE|DN_RENAME);
+
+       event_set(&work_ev, -1, 0, do_work, &soundqueue);
+
+       memset(&dev, 0, sizeof(dev));
+
+       suspend_allow(suspend_handle);
+       event_loop(0);
+       exit(0);
+}
diff --git a/utils/dialer.py b/utils/dialer.py
new file mode 100644 (file)
index 0000000..0f67243
--- /dev/null
@@ -0,0 +1,334 @@
+#!/usr/bin/env python
+
+# TODO
+#  - Show time
+#  - Make actual call
+#  - integrate with launcher
+
+# Dialer
+#
+# Listen for the 'voice-dial' selection.  When it is active we steal
+# it and raise the dialer.
+# Display is:
+# Number, or DTMF send
+# Name/number (time-of-call)
+# Keypad:
+# 
+#  1  2  3  BS
+#  4  5  6  BS
+#  7  8  9  Ca
+#  *  0  #  ll
+#
+# Interaction with Modem:
+# - request call-out
+#    write number to /var/run/gsm-state/call
+# - answer incoming
+#    write 'answer' to /var/run/gsm-state/call
+# - hang-up
+#    write empty string to /var/run/gsm-state/call
+# - determine status:  on-call, incoming, idle
+#    examine /var/run/gsm-state/status
+# - send request
+# - recv reply
+
+import gtk, pygtk, gobject
+import pango
+import os, sys, time
+import dnotify
+from subprocess import Popen
+from contactdb import contacts
+
+def record(key, value):
+    f = open('/var/run/gsm-state/.new.' + key, 'w')
+    f.write(value)
+    f.close()
+    os.rename('/var/run/gsm-state/.new.' + key,
+              '/var/run/gsm-state/' + key)
+
+def recall(key):
+    try:
+        fd = open("/var/run/gsm-state/" + key)
+        l = fd.read(1000)
+        fd.close()
+    except IOError:
+        l = ""
+    return l.strip()
+
+
+class Dialer(gtk.Window):
+    def __init__(self):
+        gtk.Window.__init__(self)
+        self.set_default_size(480,640)
+        self.set_title("Dialer")
+        self.connect('destroy', self.close_win)
+
+        self.number = ""
+        self.dtmf = False
+
+        self.oncall = False
+
+        self.book = contacts()
+        self.create_ui()
+        self.watch_clip('voice-dial')
+
+        d = dnotify.dir('/var/run/gsm-state')
+        self.status_watcher = d.watch('status', self.check_status)
+        self.incoming_watcher = d.watch('incoming', self.check_incoming)
+        self.show()
+
+    def close_win(self, *a):
+        gtk.main_quit()
+
+    def create_ui(self):
+
+        fd = pango.FontDescription('sans 10')
+        fd.set_absolute_size(70 * pango.SCALE)
+        self.bfont = fd
+        fd = pango.FontDescription('sans 10')
+        fd.set_absolute_size(80 * pango.SCALE)
+        self.nfont = fd
+        self.nfont_size = 80 * pango.SCALE
+        fd = pango.FontDescription('sans 10')
+        fd.set_absolute_size(80 * pango.SCALE)
+        self.cfont = fd
+        self.cfont_size = 80 * pango.SCALE
+        v = gtk.VBox(); v.show(); self.add(v)
+
+        # number or DTMF
+        n = gtk.Entry(); n.show()
+        n.modify_font(self.nfont)
+        n.set_alignment(0.5)
+        n.set_size_request(-1, 90)
+        v.pack_start(n, expand=False)
+        self.num = n
+
+        # name (or number) of other end.
+        n = gtk.Label(); n.show()
+        n.modify_font(self.nfont)
+        n.set_size_request(-1, 90)
+        v.pack_start(n, expand=False)
+        self.callee = n
+        self.check_callee_font()
+
+        k = self.create_keypad()
+        v.pack_start(k, expand=True)
+
+    def create_keypad(self):
+        h = gtk.HBox(); h.show()
+
+        h.pack_start(self.create_col('1','4','7','*'))
+        h.pack_start(self.create_col('2','5','8','0'))
+        h.pack_start(self.create_col('3','6','9','#'))
+        cl = self.create_col('BS','CA\nLL')
+        h.pack_start(cl)
+        ch = cl.get_children()
+        self.BS = ch[0]
+        self.CALL = ch[1]
+        h.set_homogeneous(True)
+        return h
+
+    def create_col(self, *r):
+        v = gtk.VBox(); v.show(); v.set_homogeneous(True)
+        for b in r:
+            bt = gtk.Button(b);
+            bt.child.modify_font(self.bfont)
+            bt.connect('button_press_event', self.press, b)
+            bt.set_property('can-focus', False)
+            bt.show()
+            v.pack_start(bt)
+        return v
+
+    def press(self, b, ev, key):
+        if len(key) == 1:
+            if self.oncall == 2:
+                # Incoming call needs to be answered
+                return
+            if self.oncall and not self.dtmf:
+                self.num.set_text("")
+                self.num.set_position(-1)
+                self.dtmf = True
+                self.num.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("blue"))
+            if not self.oncall and key == '#' and self.num.get_text() == "":
+                key = '+'
+            self.num.insert_text(key, self.num.get_position())
+            self.num.set_position(self.num.get_position()+1)
+            if self.oncall:
+                self.do_dtmf(key)
+            self.check_num_font()
+            n = self.num.get_text()
+            if len(n) <= 1:
+                self.book.load()
+            e = self.book.find_num(n)
+            if e:
+                self.callee.set_text('To:' + e.name)
+                self.check_callee_font()
+            else:
+                self.callee.set_text('')
+        elif key == 'BS':
+            if self.oncall:
+                self.endcall()
+            else:
+                p = self.num.get_position()
+                if p > 0:
+                    self.num.delete_text(p-1, p)
+                    self.num.set_position(p-1)
+            self.check_num_font()
+            n = self.num.get_text()
+            if len(n) <= 1:
+                self.book.load()
+            e = self.book.find_num(n)
+            if e:
+                self.callee.set_text('To:' + e.name)
+                self.check_callee_font()
+            else:
+                self.callee.set_text('')
+        else:
+            if self.oncall == 2:
+                self.takecall()
+            elif not self.oncall:
+                self.makecall()
+
+    def watch_clip(self, board):
+        self.cb = gtk.Clipboard(selection=board)
+        self.targets = [ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ]
+
+        self.cb.set_with_data(self.targets, self.get, self.got_clip, None)
+
+    def got_clip(self, clipb, data):
+        a = clipb.wait_for_text()
+        if not self.oncall:
+            if a[:9] == "Incoming:":
+                self.incoming(a[9:])
+            else:
+                self.num.set_text(a)
+                self.num.set_position(-1)
+                self.check_num_font()
+                self.makecall()
+        self.cb.set_with_data(self.targets, self.get, self.got_clip, None)
+        self.present()
+
+    def incoming(self, num):
+
+        self.BS.child.set_text("En")
+        self.CALL.child.set_text("OK")
+        self.oncall = 2
+        self.dtmf = False
+        self.book.load()
+        self.set_incoming(num)
+
+    def set_incoming(self, num):
+        if num == '' or num == '-':
+            num = "Private Number"
+        self.num.set_text(num)
+        self.num.set_position(-1)
+        self.check_num_font()
+        self.number = num
+        e = self.book.find_num(num)
+        if e:
+            num = e.name
+        self.callee.set_text('From:' + num)
+        self.check_callee_font()
+
+    def check_incoming(self, f):
+        if self.oncall != 2:
+            return
+        n = recall('incoming')
+        self.set_incoming(n)
+
+    def get(self, sel, info, data):
+        sel.set_text("Number Please")
+
+    def check_status(self, f):
+        l = recall('status')
+        if l == 'INCOMING':
+            l = recall('incoming')
+            self.incoming(l)
+            self.present()
+        elif l == 'BUSY':
+            self.endcall()
+            self.callee.set_text('BUSY')
+            self.check_callee_font()
+            self.present()
+        elif l == 'on-call':
+            pass
+        elif l == '':
+            self.endcall()
+
+    def check_num_font(self):
+        n = self.num.get_text()
+        l = len(n)
+        if l <= 9:
+            s = 80 * pango.SCALE
+        else:
+            if l > 16:
+                l = 16
+            s = 80 * pango.SCALE * 9 / l
+        if self.nfont_size != s:
+            self.nfont.set_absolute_size(s)
+            self.nfont_size = s
+            self.num.modify_font(self.nfont)
+
+
+    def check_callee_font(self):
+        n = self.callee.get_text()
+        l = len(n)
+        if l <= 9:
+            s = 80 * pango.SCALE
+        else:
+            if l > 16:
+                l = 16
+            s = 80 * pango.SCALE * 9 / l
+        if self.cfont_size != s:
+            self.cfont.set_absolute_size(s)
+            self.cfont_size = s
+            self.callee.modify_font(self.cfont)
+
+
+    def makecall(self):
+        n = self.num.get_text()
+        self.num.select_region(0,-1)
+        if not n:
+            return
+        self.BS.child.set_text("En")
+        self.oncall = True
+        #Popen(['alsactl', '-f', '/usr/share/openmoko/scenarios/gsmhandset.state',
+        #       'restore' ], shell=False, close_fds = True)
+        self.number = n
+        self.dtmf = False
+        self.book.load()
+        e = self.book.find_num(n)
+        if e:
+            self.callee.set_text('To:' + e.name)
+        else:
+            self.callee.set_text('To:' + n)
+        self.check_callee_font()
+        record('call',n)
+
+    def takecall(self):
+        self.oncall = True
+        #Popen(['alsactl', '-f', '/usr/share/openmoko/scenarios/gsmhandset.state',
+        #       'restore' ], shell=False, close_fds = True)
+        self.num.select_region(0,-1)
+        record('call','answer')
+
+    def endcall(self):
+        if self.oncall == False:
+            return
+        record('call','')
+        self.oncall = False
+        #Popen(['alsactl', '-f', '/usr/share/openmoko/scenarios/stereoout.state',
+        #       'restore' ], shell=False, close_fds = True)
+        self.BS.child.set_text("BS")
+        self.CALL.child.set_text("CA\nLL")
+        self.num.set_text("")
+        self.num.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("black"))
+        self.num.set_position(-1)
+        self.callee.set_text("")
+        self.check_callee_font()
+
+    def do_dtmf(self, ch):
+        record('dtmf',ch)
+
+
+o = Dialer()
+gtk.main()
diff --git a/utils/tapinput-dextr.py b/utils/tapinput-dextr.py
new file mode 100644 (file)
index 0000000..799be2a
--- /dev/null
@@ -0,0 +1,145 @@
+#!/usr/bin/env python
+
+# Copyright (C) 2011-2012 Neil Brown <neilb@suse.de>
+#
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License along
+#    with this program; if not, write to the Free Software Foundation, Inc.,
+#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+# Connect 'tapboard' with 'fakeinput' to make a working on-screen
+# keyboard.
+# This script uses a long press on the 'power' button to raise
+# the keyboard, and also a double-tap on the 'z' axis of the
+# accelerometer.  However that requires a kernel driver that I
+# never published, so it won't work at the moment.
+
+import time, gtk
+from fakeinput import fakeinput
+from tapboard_dextr import TapBoard
+from evdev import EvDev
+import gobject
+
+class KeyboardIcon(gtk.StatusIcon):
+    def __init__(self, activate = None):
+        gtk.StatusIcon.__init__(self)
+        self.set_from_file('/usr/local/pixmaps/tapinput-dextr.png')
+        self.set_activate(activate)
+
+    def set_activate(self, activate):
+        if activate:
+            self.connect('activate', activate)
+
+power_timer = None
+def power_pressed(cnt, type, code, value, win):
+    if type != 1:
+        # not a key press
+        return
+    if code != 116:
+        # not the power key
+        return
+    global power_timer
+    if value != 1:
+        # not a down press
+        if power_timer != None:
+            gobject.source_remove(power_timer)
+            power_timer = None
+        return
+    power_timer = gobject.timeout_add(300, power_held, win)
+
+def power_held(win):
+    global power_timer
+    power_timer = None
+    if win.visible:
+        win.hide()
+        win.visible = False
+    else:
+        win.hide()
+        win.show()
+        win.activate()
+        win.visible = True
+    return False
+
+last_tap = 0
+def tap_pressed(cnt, type, code, value, win):
+    global last_tap
+    if type != 1:
+        # not a key press
+        return
+    if code != 309:
+        # not BtnZ
+        return
+    if value != 1:
+        # only want dow, not up
+        return
+    now = time.time()
+    print now, last_tap
+    if now - last_tap < 0.2:
+        # two taps
+        if win.visible:
+            win.hide()
+            win.visible = False
+        else:
+            win.hide()
+            win.show()
+            win.activate()
+            win.visible = True
+        last_tap = 0
+    else:
+        last_tap = now
+
+ico = KeyboardIcon()
+w = gtk.Window(type=gtk.WINDOW_POPUP)
+w.visible = True
+w.connect("destroy", lambda w: gtk.main_quit())
+ti = TapBoard()
+w.add(ti)
+w.set_default_size(ti.width, ti.height)
+root = gtk.gdk.get_default_root_window()
+(x,y,width,height,depth) = root.get_geometry()
+x = int((width - ti.width)/2)
+y = int((height - ti.height)/2)
+w.move(x,y)
+def activate(ignore):
+    w.hide()
+    w.show()
+    w.visible = True
+    w.activate()
+    w.move(x,y)
+ico.set_activate(activate)
+fi = fakeinput()
+def dokey(ti, str):
+    fi.send_char(str)
+ti.connect('key', dokey)
+def domove(ti, x,y):
+    global startx, starty
+    if x == 0 and y == 0:
+        (startx, starty) = w.get_position()
+    x = 0
+    w.move(startx+x, starty+y)
+
+ti.connect('move', domove)
+def hideme(ti):
+    w.hide()
+    w.visible = False
+ti.connect('hideme', hideme)
+try:
+    pbtn = EvDev("/dev/input/power", power_pressed, w)
+    #tbtn = EvDev("/dev/input/accel", tap_pressed, w)
+except:
+    pass
+ti.show()
+w.show()
+fi.new_window()
+hideme(ti)
+gtk.main()
+
index d39a79a0db5d7bc897d21b3b28c2c4f4b0986949..139fd88a7e0f712a3be650bae07f47535b572b5a 100755 (executable)
@@ -134,11 +134,12 @@ def hideme(ti):
 ti.connect('hideme', hideme)
 try:
     pbtn = EvDev("/dev/input/power", power_pressed, w)
-    tbtn = EvDev("/dev/input/accel", tap_pressed, w)
+    #tbtn = EvDev("/dev/input/accel", tap_pressed, w)
 except:
     pass
 ti.show()
 w.show()
 fi.new_window()
+hideme(ti)
 gtk.main()