]> git.neil.brown.name Git - freerunner.git/commitdiff
Lots of random updates
authorNeilBrown <neilb@suse.de>
Sun, 6 Feb 2011 09:44:35 +0000 (20:44 +1100)
committerNeilBrown <neilb@suse.de>
Sun, 6 Feb 2011 09:44:35 +0000 (20:44 +1100)
Just trying to sync-up with various previously-untracked things

Signed-off-by: NeilBrown <neilb@suse.de>
54 files changed:
NOTES/Control [new file with mode: 0644]
NOTES/Debian [new file with mode: 0644]
NOTES/Network [new file with mode: 0644]
NOTES/TODO [moved from TODO with 100% similarity]
NOTES/TODO2 [new file with mode: 0644]
Shop/shop.py [new file with mode: 0755]
Shop/t.table [new file with mode: 0644]
Shop/test.list [new file with mode: 0644]
Shop/test.table [new file with mode: 0644]
Shop/todo [new file with mode: 0644]
apmhacks/apmd_proxy [new file with mode: 0755]
auxlaunch/auxlaunch.py [new file with mode: 0755]
auxlaunch/auxlaunch2.py [new file with mode: 0644]
gsm/At.Commands.pdf [new file with mode: 0644]
gsm/notes [new file with mode: 0644]
guessture [new submodule]
launcher/.launchrc [new file with mode: 0644]
launcher/fingerscroll.py [new file with mode: 0644]
launcher/gpstz [new file with mode: 0644]
launcher/launch.py [new file with mode: 0644]
launcher/launch_settings.py [new file with mode: 0644]
launcher/wmctrl.py [new file with mode: 0644]
launcher/wpa [new file with mode: 0644]
lib/decode-long-sms.c [new file with mode: 0644]
lib/decode-long-sms.py [new file with mode: 0644]
lib/play.py [new file with mode: 0644]
lib/pyfakekey.c [new file with mode: 0644]
lib/tapinput.py [new file with mode: 0644]
mickeyterm/mickeyterm.py [new file with mode: 0755]
music/music.py [new file with mode: 0755]
music/notes [new file with mode: 0644]
music/properties.py [new file with mode: 0644]
netchoose/mdbus.py [new file with mode: 0644]
netchoose/netchoose.py [new file with mode: 0755]
sms/contact.py [new file with mode: 0644]
sms/exesms [new file with mode: 0644]
sms/notes [new file with mode: 0644]
sms/sendsms.py [new file with mode: 0644]
sms/storesms.py [new file with mode: 0644]
sms/test.py [new file with mode: 0644]
sounds/formats.h [new file with mode: 0644]
sounds/sound.c [new file with mode: 0644]
sounds/sounds.py [new file with mode: 0644]
test/autocon.c [new file with mode: 0644]
test/clock.py [new file with mode: 0644]
test/fake-test.c [new file with mode: 0644]
test/notify.py [new file with mode: 0644]
test/pykey.py [new file with mode: 0644]
test/reflash [new file with mode: 0644]
test/status.py [new file with mode: 0644]
test/test.py [new file with mode: 0644]
test/test1.py [new file with mode: 0644]
test/test2.py [new file with mode: 0644]
test/wkalrm.c [new file with mode: 0644]

diff --git a/NOTES/Control b/NOTES/Control
new file mode 100644 (file)
index 0000000..8c5abd1
--- /dev/null
@@ -0,0 +1,73 @@
+
+Overall control:
+  - blanking
+  - suspend
+  - screen-lock
+  - screen-capture
+  - screen-rotate?
+  - charging current ??
+  - 
+
+Use AUX button??
+
+
+Blanking:
+  Need to know if screen is being used.
+    hint:  if GPS is on, then screen is watched.
+    if touchpad gets input, then screen is watched.
+    if power is on can wait longer.
+  After 10 seconds of no touchpad, check power and gps power
+
+  /sys/devices/platform/s3c2440-i2c/i2c-adapter/i2c-0/0-0073/charger_type
+    starts "none" or "host"
+  /sys/bus/platform/devices/neo1973-pm-gps.0/pwron
+    0 or 1
+
+  First stage blank goes to half brightness and disables keyboard
+   A tap goes full and enables. (so we lose the tap)
+
+  Second stage goes off.  Tap returns to first stage.
+
+Suspend:
+  Need to know when system is being used:
+   - network connections other than 127.0.0.1
+   - active phone call
+   - active screen
+
+So:
+
+  We have a current state:
+      on:  screen is on and accepting input
+      half:  screen is reduced brightness and blocked
+      off:  screen is off, input is diverted
+
+  We have a time of last touchpad input
+
+  We measure:
+      gps power
+      charger type
+      network connections.
+
+  We decide in new state and timeout
+  We change state (if needed) and sleep.
+    sleeping involves reading touchpad so we know time of last input.
+
+
+  switch state:
+   case 'on':
+       if last input < 10 seconds, set time for remaining seconds done
+       if charger and last input < 30 seconds, as above
+       set state to half
+       set time to 50
+
+   case 'half'
+       if < 10 seconds, set state to on, wait 10 seconds
+       if gps power wait for 10 minutes, else 1 minute
+       set state to off
+
+   case 'off'
+       if < 10 seconds, set state to half, wait 10 seconds
+       if gps, wait 30 minutes, else 2 minutes
+       if network or charger, stay at off indefinately
+       if time is up, "apm -s", assume input, set to 'on'.
+
diff --git a/NOTES/Debian b/NOTES/Debian
new file mode 100644 (file)
index 0000000..610e523
--- /dev/null
@@ -0,0 +1,70 @@
+
+Install
+
+remove fso-frameworkd and zhone
+but keep python-gst10.0 and python-gtk, apmd
+discard dash, just use bash
+
+Install latest andy-tracking kernel plus modules
+
+add gcc, libc6-dev, and some other dev to get gsm0710muxd to compile
+add nfs-utils
+add less, lsof
+add x11-utils
+add gpsd tangogps
+add xserver-xglamo
+add mplayer 
+add ntpdate
+add bc
+gcc libc6-dev nfs-utils less lsof x11-utils gpsd tangogps xserver-xglamo mplayer ntpdate bc
+
+add libts-bin and calibrate the touchscreen with ... you cannot
+  just copy /etc/pointercal
+    557 38667 -4654632 -51172 121 46965312 65536
+
+
+dpkg-reconfigure gpsd
+  tell it to use /dev/ttySAC1
+echo 1 > 
+  /sys/class/i2c-adapter/i2c-0/0-0073/pcf50633-regltr.7/neo1973-pm-gps.0/power_on
+  /sys/class/i2c-adapter/i2c-0/0-0073/pcf50633-regltr.7/neo1973-pm-gps.0/keep_on_in_suspend
+
+add telnet
+
+DPI is different... what to do??
+
+remove /var/lib/apt/lists and symlink to /media/card
+remove /var/cache/apt and symlink to /media/card
+remove /usr/share/man /usr/share/doc
+   should probably mount tmpfs on those to avoid getting new stuff
+
+
+/dev/ttySAC don't get setup!!
+rather we get 
+crw-rw---- 2 root dialout 204,  64 Feb 11 13:51 s3c2410_serial0
+crw-rw---- 2 root dialout 204,  65 Feb 11 12:03 s3c2410_serial1
+crw-rw---- 2 root dialout 204,  66 Feb 11 12:03 s3c2410_serial2
+
+echo 'KERNEL=="s3c2410_serial[0-9]",   NAME="ttySAC%n"' > /etc/udev/rules.d/56-ttySAC.rules
+
+or >> /etc/udev/rules.d/56-freerunner.rules 
+
+
+
+add wmctrl for launch - should get rid of this
+----
+copy gsm0710muxd, run it
+copy Freerunner/gsm,lib/* to /usr/local/lib/python2.5/site-packages/
+chmod +X gsmd and link to /usr/local/bin
+add #!env  to top
+mkdir /var/run/gsm-state
+mkdir /var/lib/misc/flightmode
+  and > active
+copy apmd/
+
+
+Need to:
+  start gsm0710muxd
+        gsmd
+        launch
+
diff --git a/NOTES/Network b/NOTES/Network
new file mode 100644 (file)
index 0000000..688647b
--- /dev/null
@@ -0,0 +1,15 @@
+
+Want to have easy config for network:
+
+ Start/stop GPRS, and set AP name
+ Start/stop wireless, and set AP name
+
+So:
+ Big "GPRS" button toggles on/off
+  text entry for AP name
+
+ Big "WLAN" button toggles on/off
+   If on and no ESSID chosen, try to fill in box.
+
+
+AP name is stored in /media/card/gprs
diff --git a/TODO b/NOTES/TODO
similarity index 100%
rename from TODO
rename to NOTES/TODO
diff --git a/NOTES/TODO2 b/NOTES/TODO2
new file mode 100644 (file)
index 0000000..5637046
--- /dev/null
@@ -0,0 +1,868 @@
+TODO: sep 2009, now that I am using this as my real phone:
+ - send touch tone commands
+ - keep record of incoming/outgoing calls
+ - reduce time from wake to ring
+TODO:
+  DONE runit to scroll to the bottom
+  DONE bigger buttons in runit
+  text, not icons, for shop config stuff
+  DONE launch/status to update more often
+  DONE gsm info in launch/status
+  DONE gsmd: reading 'extra' needs to cope with multiple lines:
+  SMS:
+    - update git
+    - add 'xterm' so I can type...
+    DONE - add 'esc' to 'tapinput'
+    - look for launch errors in .xsession-errors:
+  File "/usr/local/bin/launch", line 324, in press
+    if not self.offsets:
+AttributeError: 'Selector' object has no attribute 'offsets'
+
+    - make sure launch updates when sms message arrives
+    - find out why everything is so slow
+    - make sure only one 'gsm-getsms' running at a time.  Maybe kill
+      old one?
+    - trace?? to find out why we don't seem to pick up a message on
+       resume.  The newsms file gets changed but maybe no signal gets sent.
+    - "sendsms -n" should go straight to 'new'.
+    - clean up rubbish in 'new' and 'draft' files.
+    - lock should be in charge of buzzer.
+       It turns it off on 'wake' or after a timeout and turns it on when some file changes.
+      Maybe there is an 'alert' file which gets 'sms' or 'ring'.
+      In the case of 'ring' it is updates regularly.
+      'lock' sets a buzz, turns on display, maybe beeps
+    - run gsm-getsms at appropriate times
+        -n when /var/run/gsm-state/new-sms changes
+      DONE  -a at boot time, or when gsmd starts??
+    - write some info to a 'new sms' file that I can watch for status
+        Probably just watch the 'newmesg' log and update shortly after that changes.
+    - improve atchan code - better abstractions
+    - 'delete' should return to list view if in message view
+    - Make sure sendsms notices when the files change
+    - Make 'sender' and time easier to read.
+       Probably make lines twice as high
+       time can be 'today' and 'yesterday' and ' 5 minutes ago' etc
+      mark day/month boundaries some how?
+    - figure out if a '+' should be at the front of the phone number
+    - look into timezone information on exesms messages
+    - implement search function
+    - Highlight 'new', 'draft' 'in/out' better.  Icon?
+    - Don't duplicate a draft when we edit it.
+    - Find some way to remove cruft from new/draft
+    - buzz buzzer when message arrives.
+    - include transit time in full message view.
+    - Make messages disappear from 'new' when they are read.
+    - popup reading in 'new' mode when selected from launcher
+    - capture and decode catenated messages
+    - figure out why changing lists is so slow.
+    - implement undelete.
+    - implement scrolling of message list
+    - change 'open' to 'forward' or 'edit'
+    - add 'reply' button
+    ?? detect SIM card id and have separate mirror file??
+            AT+CIMI
+                 505038191025166
+                 505038240084403
+    - move 'config' to 'listing' page?
+    - change between 'save draft' and 'cancel' when clear.
+    - press-and-hold send to select handler
+    - make 'paste' active only when there is something to paste
+    - send only active when fields are filled in.
+
+    
+receive AT response +CBM: 4576,50,1,1,1
+receive AT response South
+receive AT response Sydney
+receive AT response 
+
+Sms-store:
+  DONE store direction (in/out) and only one address (Sender or recipient)
+
+Inter toy communication:
+ e.g.
+   play this tune
+   call this number
+   I want the speakers
+   don't suspend just now
+   call me on resume
+   everybody pause
+
+ Use X clipboards for specific messages
+ use lock files for 'dont'
+ use leases for 'call me' and 'everybody'
+   That would require all running as same uid.
+   Maybe DNOTIFY, but cannot use that well from python
+   So write C helper and do inotify?
+     But check gobject doco first.
+   I think I can use dnotify OK, poll each file on each signal.
+   So use that for 'call me on resume'
+   Possibly for 'incoming call', 'received sms', 
+   Maybe for 'Internet now available'
+       Used by weather, time,
+   Also 'GSM' and 'GPS' now available
+
+ Root Properties - one-to-many messages. that are ui based.
+   e.g. 'pause' 'ABC/abc/123/$%&', 'scale size'
+
+ So: suspend.
+   We have a directory /var/lock/suspend
+   containing:
+       auto:  Taking a LOCK_SH on this prevents auto suspend.
+              If auto-suspender can get a LOCK_EX, it suspends, then
+                  unlocks on resume
+       suspend: Taking a LOCK_SH on this prevents any suspend completing.
+               However any locker must also watch the file with e.g.
+               dnotify, and when the file is nonempty, the lock must be
+               released promptly.
+       next_suspend:  Taking a lock on this while holding a lock on
+               suspend ensures that you will not miss a suspend/resume cycle.
+               On resume, the file is renamed to 'suspend',
+               So 'dnotify' can discover when resume happens and a subsequent
+               suspend will not complete until the lock is released.
+
+    A client with:
+      take a lock on 'suspend'
+      check that link count is non-zero (if zero, close and loop)
+     loop:
+      set up for active system
+      wait
+      when there is a change in the file:
+        set up for suspend
+        possibly take lock on next_suspend
+        release lock
+        wait for activity on next_suspend (which will be renamed to 'suspend')
+        goto loop
+               
+
+Alert:
+   Alert is currently part of 'lock'.  Is that a good idea?
+   The connection is that we only want to sound an alert if there is
+   an easy way to turn it off - and 'lock' is in the best position to
+   turn things off.  It is closest to the user.  If lock isn't
+   running,  don't want to make any noise.
+
+   Alert needs:
+     - different alerts:  ring, sms, alarm, low-battery
+     - different sub-alerts:  ring/family. sms/work, alarm/urgent
+     - different settings: silent, unobtrusive, normal
+     - different actions: tone, buzz, flash, repeat count, volume
+                  change, LED flash, screen flash.
+     - some alerts stop when there is user input (tone).  Some
+       continue (LED)
+   
+     - concurrent alerts??  alarm and SMS at same time?
+         As long as we get attention, and display status on screen..
+        Maybe need alert priority and only play the most important.
+    
+   Configuration: too complex for just directories with symlinks to
+     .wav files.
+     Still want to make it easy to change config, either temp or perm.
+
+     Config changes can be
+      -  time based
+      -  location based?
+      -  orientation based
+      -  user-requested
+
+     Does the alert program do this itself, or does some other program
+     move files around?
+
+     Maybe have a set of .ini files where settings cumulate.
+     Read base, then others
+      - calendar program creates a plan every so often which lists
+        time - profile  e.g. 20090228-123456  silent
+                            20090301-012334  -
+      - The location service gives a name to where we are, as precise
+        as possible.  We then look for setting "location-$NAME"
+      - ditto for orientation.
+
+
+Disable 'xset' screen blan!k
+
+tapinput:
+  DONE differently OK should become 'go away'
+  DONE Need a 'mode' where all 12 are used.  press/hold mode/del to recover.
+  DONE Move > half off screen, and disappear.
+  DONE statusicon to recover
+  DONE Add:  Ret UP DOWN LEFT RIGHT 
+
+
+Weather:
+  Waits for 'internet now available' and if more than 8 hours since
+   last check, download weather details.
+
+Library:
+  Selector
+   Currently used in
+           launcher (folders/tasks), music, shop, sendsms
+   Features:
+       - goes multi-column if possible/needed
+       - auto-scroll when near extreme
+       - tap selects but doesn't activate
+       - small left/right tap areas for select-and-activate
+       - simple text with different colours, or app-specific rendering
+       - alternating background, background for highlight
+       - drags used for text entry
+
+  AutoBox
+    Acts like vbox or hbox depending on available space
+
+  Slider
+    pop-up window
+
+
+Note pad with search.
+  This might just be an extension to scribble.
+   - simple text file editor
+   - multiple pages with a list that can be selected from
+   - search across all pages - show page name and line.
+   - mark pages 'read-only'
+   (ideas from tomboy)
+   - text size.  features:  highlight strikethrough bold italic
+   - 'link' a word to a page of the same name.
+   - group pages into notebooks???
+   - scrollable pages?
+   - collapsable lists.
+
+Scribble:
+  - larger buttons
+  - ??save png??
+  - allow grouping of texts with tap-draw-hold
+  - when we have a group of texts, allow
+       - align
+       - sort
+       - fold
+  - move a text with tap-hold-draw
+  - easier to select individual text
+  - allow text to be deleted - maybe drag to corner
+  - new line gets leading number or bullet or whatever
+  - I need:
+       'text' entries to record opposite corner when drawn
+       some set of the last N text entries
+         these are drawn with a grey background??
+       select text
+
+runit
+  Set window name to something appropriate
+  DONE run a command and display the output.  No user interaction(yet)
+  DONE One button to close
+  Possible additions.
+   - rerun button
+   - different colour for stderr
+   - buffer to build text, then enter it
+  Combine with launcher??? or teach launcher to understand it better
+  Make it easier to run a little shell script
+
+
+lock:
+ DONE Be more careful about counting up/down - count per button.
+ DONE  be careful about press/release difference
+ DONE  allow insta-lock with icon
+ LATER use lockfile to tell when someone wants no-blank
+ LATER Have lockfile for 'next suspend'. use inotify for clients
+    to find out when 'next' becomes 'now'
+ DONE use lockfile to tell when someone wants no-suspend
+ DONE suspend when more idle
+ DONE use external net connections to disable suspend
+ DONE maybe don't suspend while charging (of >50%)
+ DONE don't suspend while load-average is high
+ DONE no-suspend setting.
+ Only check TCP if keep-alive is working....
+ Use alsa to play tones for alerts.  Also buzz
+   Alerts are requested by creating file /var/run/alert/$name
+   where 'name' is something like 'sms' or 'ring' or 'alarm'
+   We look for /etc/alert/$name to find out what to play.
+    Not sure how to include
+        tone
+        volume
+        repeat/louder
+        vibrate
+        display brightness
+        LED flash
+
+ We terminate the alert tone when a button is pressed
+     Vibrate continues if it is a repeating alert (ring)
+
+
+Event Viewer
+  Events such as SMS, missed-call, alarm get appended to a file
+  The viewer shows recent events and allows the relevant window to be
+  selected.
+  It has a blinking statusicon when there are new events
+
+  This is probably just included in launcher
+
+PictureViewer
+  one pane which just shows a photo, is full screen, taps left and right moves
+  one pane with browser showing date information 
+  thumbnails?
+
+Address Book
+   store addresses with phone numbers
+   lookup and display
+   Format address label as postscript for printing
+   send phone number to dialler
+
+   Store:
+     Names
+     Addresses - people share these
+     Phone number - people have several, and share them
+     Email address - again, can be multiple and shared.
+
+   So we allow entries to include others by reference
+
+   Name - given / family
+   Category  (family, collegue, church, ...)
+   Address[desc] - l1 / l2 / postcode / country
+   Phone[desc] - number
+   Email[desc] - addr
+   group[desc] - entity
+
+   What key to use for 'entity' ??
+      Name?  not unique and can change.
+      UUID I guess.  assigned sequentially.  Maybe use hostname to
+         uniqueify
+
+   Store:
+     notabene.1234 = {
+         name = { given = Neil ; family = Brown }
+         Category = me;
+         Category = Family;
+         address = { type=Home; a= "13 Lang Ave";
+                        city="Pagewood"; postcode=2019;
+                         country = Australia
+         }
+         Phone = { type=mobile; num=0403463349; }
+         Valid = date
+     }
+         
+  That is horribly verbose and not needed.
+  Though if we store gzipped, it isn't much waste.
+
+   We don't really want to store it in memory as that isn't paged out,
+   So we want an ondisk format which is very easy to parse.  Hopefully
+   it will be cached most of the time.
+   So: ':' separated fields with ',' separated subfields and '=' assignments
+     id:family:given:cat,cat:ref,ref:PO:addr:addrext:city:postcode:country:mobile=number,work=number:Date
+
+   Should probably use VCARD - it isn't that hard.
+   Just ignore the bits we don't understand yet.
+   When editting, only change fields we understand.
+   When there are interdependent fields like N vs FN and ADR vs LBL
+    that can be awkward.
+
+   No.  Don't like VCARD - no references (that I can find) as no ID for each entry ???
+
+   Anyway I'll use my own internal format and maybe export/import one day if I decide to care.
+
+   We mmap the file and typicaly use 're' to search through it, while holding a read lock and having
+   checked the size.
+
+   We access the address book by extracting addresses.
+   'first', 'next'
+   These can have an 're' argument meaning 'first/next that matches re'
+   File contains an initial line with a version number, so that each line starts and ends
+   with '\n'.  Thus each field starts and ends with [\n:,=]
+
+   How to integrate this?
+     Any program that just needs read access goes direct to the file, mmaping and searching
+     Auto updates such as 'last used' can similarly be on on the mmapped file.
+     Updates need to be handled by a single GUI.
+     When someone - e.g. SMS or Call-Log - wants to store a number or edit an entry,
+      they put a resource on the root window.  If the contact list is watching, it
+       picks it up.  If it isn't, the button is greyed out (or we run the program)
+
+     The contact editor:
+        Has an edit window where content of interest appears
+
+music:
+  doesn't always move forward at end-of-song
+  have better way to 'play chosen song'
+  If 'seek' dropdown didn't select a time, don't jump at all.
+  report song on root window property
+  Have fs browser to find music folder(s)
+  survive suspend somehow
+  Don't seek when there is nothing to seek (negative offset??)
+  record current location for restart
+  remove "/home/music/ogg" from displayed path
+  grab /var/run/suspend_disabled when playing
+  DONE add volume control
+  DONE Think about seek control
+  Maybe support HTTP: uris such as classic-fm
+  Support Random-Album
+  Support Random-Song
+  tap 'current song' to go there in browser
+  search by substring
+
+clock:
+  sync to real time properly
+  alarm clock:
+    periodic or one-off
+    wake to music or buzzer
+  pop up:
+    set timezone
+    show date/calendar
+    ntpdate
+    gpsdate
+    gps-timezone
+
+Shopping List
+   Need to add/edit locations and places.
+   When 'choose location' or 'choose place' is up, the
+   'add' and 'edit' button affect the location name instead of the product
+   renaming a thing to 'empty' only works if it is not in use.
+   When we click 'config', product list is replaced by location list.
+   Click 'config' again to go back.  Different background colour??
+   If we change place, it changes set of locations
+   '+' adds a new location
+   'edit' changes the name of current location
+   zoom still exist
+   tick and cross become up and down to move current location
+
+    
+Battery
+  try to get more timely alerts without polling  - Need to use DBUS.
+  DONE  Select between pop-up new window and restore old window
+  DONE Show time_to_{full,empty}_now in correct units
+  ?? use 'status' for 'Charging' ...
+  DONE timeout to make window disappear?  or to refresh display
+  DONE  pop up window with
+    suspend
+    shutdown
+    500/1000 mAmps
+    
+network
+  need to consider Wifi, USB, bluetooth, gprs, BT/ppp
+   Show status of each
+   device specific page for each:
+    Wifi:
+       list access points
+       connect with DHCP, enter WPA-PSK
+       suspend mode: power off or WOW
+    USB:
+       disable/DHCP-client/DHCP-server
+       if dchp-server: have list of address configs to choose between
+        if another network is active, choose based on that.
+    Bluetooth
+       list peers
+       connect
+       listen
+       DHCP client/server
+       disable/power down
+       ppp over rfcomm
+    GPRS
+       on-demand
+       enter AP
+
+launcher
+  content for status page:
+    - date, time,  timezone??
+    - GSM carrier, Cellid  strenght?
+    - Network connectivity
+    - new messages
+    - missed calls
+    - last window
+    - GPS co-ords?
+    - next reminder
+    - top of todo list??
+    - load average / uptime
+  Delay "wmctrl -l" a bit so that we start up faster ??
+
+  support displaying values from root attributes
+  run wmctrl at the right time better.
+  have a 'status' folder which shows 'today'
+  use pango.markup
+  Don't adjust sizes of entires
+  Allow different sizes items - keep list of offset for click
+  Don't allow item to be selected if no buttons
+  Center type. MAybe "type" can be e.g. "cmd:center" or "thing:right"
+  How to schedule redraw of e.g. time / date / etc
+     Expensive tasks would need to cache values incase they are called too often.
+  don't iconize- just stay behind
+  watch root properties to be notified of window changes.
+
+  DONE direct button access
+  DONE list options
+  DONEkill window
+  DONE reload option
+
+  DONE Two columns with button row at bottom.
+  DONE First column is cmd group and includes 'active windows'
+  DONE Second column is options in the group
+  DONE Buttons are "run" or "stop"
+
+  I want a third column.  When it appears, the first column disappears.
+  This can be used for:
+    - address-book access when making a call
+    - log of recent calls - in or out
+    - todo list?
+  Why not just put these in the second column with the trigger in the first.
+  So first column gets:
+     contact:  shows address book.
+               text entry reduces list
+               selection provides 'call' and 'txt' and 'open' buttons
+     calls:  shows similar list, but of recent calls.  Can call back
+             or save numbers.
+  Selecting 'last call' or 'dialout' in status window can allows jump to
+     this different page.
+
+  I think I need to rewrite launcher from scratch - it got messy.
+  Have same layout:
+    - text entry at the top:
+          any change is sent to any visible task
+    - buttons at the bottom.
+          these are chosen by active task
+    - left column of task groups.
+        apps comms calls contacts windows games status
+    - right column of tasks
+
+  A 'tasklist' is given to each column and can be static or dynamic.
+  A 'task' has an appearance and an action.
+  For the right column, the action sets the left column.
+  For the left column, the action sets the buttons
+  The buttons also have appearance and action.  The action
+   can do all sorts of things:
+    - run or kill program
+    - raise or close window
+    - make phone call / hang up
+
+  The two columns can be replaced by a textarea that displays the output
+  of some command.
+
+  The 'aux' button can be pressed or held.
+   - press raises the window, then selects 'status', then returns to previous window
+   - hold ... what can that do?  Answer the phone?
+  Maybe if a task is signalling for attention, then 'press' goes to
+   that task, and 'hold' activated the first button for the task.
+
+  I need to define the interface for pluggins.
+   A plugin needs to be able to:
+      - create a task
+      - create a group
+      - watch a file
+      - run a process
+      - open a window
+      - Add an embedded-window
+   
+   A task must be able to
+      - display a text
+      - present some buttons
+      - present an embedded window
+      - receive keyboard input
+      - request that a different task or group be selected
+
+   Some examples of the embedded window:
+     - tap-board for text/number entry
+     - calendar - one month
+     - text, e.g. SMS message, email
+
+  Config file:
+   This stores a list of groups.
+   Some groups need no further details (e.g. active windows)
+   Others need to explicitly list tasks
+    Tasks can be:
+      Internal (in a module)
+      External-window (program to run, window to find)
+      External-text
+
+    When listing explicit tasks we can use:
+      !command
+          for external-text
+      (window) command
+          for external-window, where 'window' is the name of the window
+      module.command
+          for an internal module
+
+   Each group is given in config file as:
+     [name/type]
+   if '/type' is missing, "list" is assumed.
+   Types and the required content are:
+     list
+        a series of task descriptions
+     windows
+        a series of window names to ignore (?)
+     contacts
+        file= name of address book
+     call-log
+        nothing - the call log has a standard location
+     
+   If 'type' is 'modules', then each line names a module
+   
+   Modules have an entry point "init" which returns:
+     a list of group types supported
+     a list of internal commands
+   each is paired with an object-creation command.
+
+   A 'group' object must be able to parse config lines
+    and produce a task list etc.
+
+gpstrack
+  pygtk similar to tangogps
+
+   Display maps, zoom follow GPS
+     Separate threads for file loading, GPS track, and UI
+     3-D view of path??
+   record path followed
+     Always compress and write to a file
+   record drawn path
+   download maps
+      - always all parent maps for a given download
+      - download maps along a path (to some depth/width)
+         This includes a drawn path.
+   If map not available, scale other resolution
+   record locations
+   display locations
+   report time-to-first-fix
+   three different modes interpretting drag
+    1/ move map around
+    2/ draw a path on the map
+    3/ write text to tag location or path.
+
+
+Bible viewer:
+  Store KJV
+  Download and cache NIV if Network available.
+  Windows:
+
+Sound recorder:
+ - support pause/restart
+ - play from start, play last 10 seconds, seek
+ - allow naming of music files
+ - adjustable audio level??
+ - adjustable compression ??  speex/vorbis
+
+phone functions
+ ON-CALL
+   pops up when a call becomes active
+   presents number*# buttons for tones
+   Allows HOLD/CALL-WAITING/HANG_UP
+   Disappears when no call is active
+   Should handle VOIP too, including call-waiting
+   Logs times??
+   disables suspend
+   un-pause after the call ??
+   Warning tone at points in call time
+
+ Answer-Call
+   Display CNI, look up in address book
+   Choose ring tone bassed on number and time of day??
+   Check device position and be quiet if face down.
+   
+ Make Call
+   allow number to be entered somehow
+   choose VOIP or mobile, while number is being entered.
+   silence music, disable suspend
+   Make connections, log, trigger ON-CALL
+
+ SendSMS
+   text + recipient 
+    send via eXeTeL if network is up, else ??
+
+   Editor window:  simple text, no new lines. auto scroll,
+     simple editing
+     spell check
+     auto-wrap
+
+    Q:
+     How to select for cut/paste?? 
+       'cut' button is normally 'select'.
+       tap that, then draw range, then 'cut'
+     status bar with valid spellings?  and common continuations
+   Buttons:
+     send:   sends to one or more addresses
+     draft:  save as a draft and go to list of drafts
+     config: go to config page.
+
+   If sending fails, save as draft and go to draft page.
+   If it succeeds, save as 'send' and save to 'sent' page.
+
+   'message list' page shows one message per line.
+   messages can be selected.
+   the selected message can be:
+       deleted
+       reloaded for send
+       viewed?
+   The list can be:
+       all / draft / new / sent / received
+       restricted by search string
+       up/down a page
+    So:
+     Top:
+      delete/view/send buttons
+     Bottom:
+      One button rotates through all/draft/new/sent/received
+      two for up/down
+      one for search
+
+   Config options:
+      backend to use for different class of number.
+      each backend has a config page.
+         exetel userid and password
+
+ RecvSMS
+   Database of all messages
+   Highlight unread messages
+   Sort in selectable order - provide virtual folders
+        Date, Month, Sender "contains text"
+
+   Handle VCARD:
+0000000 006 005 004   # 364  \0  \0   B   E   G   I   N   :   V   C   A
+0000020   R   D  \r  \n   V   E   R   S   I   O   N   :   2   .   1  \r
+0000040  \n   N   :   F   a   n   n   y  \r  \n   T   E   L   ;   P   R
+0000060   E   F   ;   W   O   R   K   ;   V   O   I   C   E   :   9   2
+0000100   3   4   8   9   0   8  \r  \n   T   E   L   ;   C   E   L   L
+0000120   ;   V   O   I   C   E   :   0   4   3   3   2   3   1   0   6
+0000140   9  \r  \n   T   E   L   ;   H   O   M   E   ;   V   O   I   C
+0000160   E   :   0   2   9   6   6   1   8   8   8   8  \r  \n   E   N
+0000200   D   :   V   C   A   R   D  \r  \n
+BEGIN:VCARD^M
+VERSION:2.1^M
+N:Fanny^M
+TEL;PREF;WORK;VOICE:92348908^M
+TEL;CELL;VOICE:0433231069^M
+TEL;HOME;VOICE:0296618888^M
+END:VCARD^M
+
+
+ SMS storage:
+
+  ** Worry about ASCII / UNICODE
+  a/ could use one file per message, named by time
+     with links to a directory for 'draft' or 'new'
+     But that wastes space.
+  b/ could use a TDB database, keyed by date, with
+     a hierarchy of days and years for directories.
+  c/ plain text file, one per month
+     'new' and 'draft' are separate files that list ids.
+
+  Probably c.
+
+  Each SMS entry is one line, space separated:
+    date-time  %Y%m%d-%H%M%S.uuid
+    'from' number
+    'to' numbers, comma separated
+    'text' of message, URL encoded on to one line with no spaces.
+
+  Hmm.. 
+
+
+  More on storage.
+    There are two dates that could be of interest.
+      - the time I first saw the message (my clock)
+      - the time the message was sent (with incoming messages).
+     We need to record both of these.
+     For outgoing, there could be "time message successfully sent"
+
+
+----------------------------------------------------------------
+Thinking about buttons.
+
+ - I want to easily/quickly get to 'main page' with time and status etc.
+   But sometimes I want to get 'back where I was' after a screen blank
+
+ - I want the 'power' button to always do the same thing.  It wakes up
+   from suspend, so it must wake up the same way from blank.
+   But what should it present:  last screen, last launcher, or status?
+   It should be the thing I most likely want after a long abscence, which
+   would be the front status page.
+   It should also be the thing that I can use when there is an incoming call.
+   That would be something with an 'answer' button.  So The status page
+   with 'incoming call' selected and 'answer/cancel' buttons at the bottom.
+   'Answer' would alert the GSM-call window to pop up and answer the call.
+
+ - So the aux button does:
+    - nothing during suspend
+    - wakeup display during blank
+    - launcher when active
+
+ - But that means the lock program needs to grab AUX, so the earpiece
+   switch gets grabbed too.  So either the lock program handles earpiece
+   switching, or it signals someone else.  I guess we write to a file when
+   that changes.  Though we could go direct to scenarios symlink.
+   How complex should this be?
+     /var/lib/audio/scenario -> handset or headset
+     handset -> scenario/handset  ditto for headset
+     scenario points to gsm, voip, stereo, record, gsm-speaker
+     these are each directories which contain 2 .state files.
+
+ - Maybe I want a button to bring up the tap-board as well??
+
+ Maybe:
+   power button always brings up control window, and selects status
+   aux button returns to
+
+
+
+And about lights.
+We have two lights.
+One can be red.
+One can be blue, orange, or purple.
+We can used the signal:
+  - charging state (powered, battery full)
+  - ringing state (could be different colour depending on if we know them)
+  - blank-but-not-off-yet
+  - wifi associated (might be a waste of battery...)
+  - 
+
+PLAN:
+
+ - arrange for power button to present status page
+ - Sort out tapinput usage
+ - install sendsms and make sure I can send via GSM
+ - sync SMS messages from SIM to flash
+ - display unread message info in status page
+ - tap on that goes to SMS application
+
+
+QVGA:
+ qvga-normal & normal in "/sys/bus/spi/devices/spi2.0/state" .
+
+SMS/cellid
+  www.opencellid.org
+
+  Keep database of locations and cells I have seen.
+  Don't add duplicates that are within 100m... say 3 seconds or arc.
+
+  ?? Create a key by interleaving bits from lat and long
+   That gives discontinuities.. but we only need to search 2 places.. well, 4.
+    So if we want lat==a..b and long==c..d
+    We find the most sig bit in which a and b (resp c and d) differ
+    and need to check
+
+----------------------------------------------------
+What happens when I type text.  I might want to:
+
+   - make a phone call .. or SMS
+   - look up name in address book
+   - calculate result
+   - add to TODO list
+   - lookup in shopping list
+
+Maybe each 'task' can register a text handler.
+ They get called when text changes and can update their name??
+ They could make themselves active, but hopefully only one would do that.
+ How could 'address book' show a list of possibles?
+
+I have outgoing calls sortof working.
+Need:
+  - gsmd to know and track status in 'incoming'.
+  - wait for atchan to be set up so I don't have to press twice DONE
+  - clean text when call completes
+  - steal keyboard input for touchtones ??
+  - call history
+  - address book lookup
+
+
+----------------------------------------
+Main page currently has
+  two columns for selection/status
+  buttons at bottom for action
+
+  Want text input line at the top for e.g. phone number,
+  contact name, calculation, note search
+  But when not typing anything it is visually unpleasant.
+  For buttons at bottom, they simple provide more space.
+  That doesn't work so well at the top.. unless we can do something with the
+  'gravity' setting .. seems unlikely.
+  So I want something permanent there when there is no input.
diff --git a/Shop/shop.py b/Shop/shop.py
new file mode 100755 (executable)
index 0000000..a7b7048
--- /dev/null
@@ -0,0 +1,2160 @@
+#!/usr/bin/env python
+
+#
+# TO FIX
+# - re-order places?
+# - 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
+
+
+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")
+            (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()
+
+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
+        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
+            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.
+        l = self.purch.loc()
+        if l < 0:
+            # cannot delete 'Unknown'
+            return
+        i = locorder[place].index(l)
+        if i == 0:
+            # nothing to merge with
+            return
+        safe.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):
+        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()
+            
+    def loc_move_down(self, widget):
+        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()
+
+
+    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.hide()
+        self.glob_control.show()
+        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]
+        self.curr_place.child.set_text(pl)
+        self.current_loc = -1
+
+    def nextplace(self, widget):
+        global place
+        if place >= len(places)-1:
+            return
+        place += 1
+        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
+        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_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")
+    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/Shop/t.table b/Shop/t.table
new file mode 100644 (file)
index 0000000..c51a30c
--- /dev/null
@@ -0,0 +1,4 @@
+P,0:Home,1:Coles-EG,2:Woolies-HD
+L0,0:Pantry,1:Freezer,2:Fridge,3:Laundry,4:Bathroom,5:Highcupboard
+L1,0:F&V,1:Deli,2:Bakery,3:Meat,4:A1,5:A2,6:A3,7:None,8:None,9:Fridge
+L2,0:F&V,1:Bakery,2:None,3:Deli,4:Fridge,5:Bread,6:Choc
diff --git a/Shop/test.list b/Shop/test.list
new file mode 100644 (file)
index 0000000..8baf98d
--- /dev/null
@@ -0,0 +1,4 @@
+12,N,
+3,N,
+36,N,bob
+25,N,twinings
diff --git a/Shop/test.table b/Shop/test.table
new file mode 100644 (file)
index 0000000..7662f28
--- /dev/null
@@ -0,0 +1,39 @@
+P,0:Home,1:Coles-EG,2:Woolies-HD
+L0,0:Pantry,1:Freezer,2:Fridge,3:Laundry,4:Bathroom,5:Highcupboard
+L1,8:Cereal,12:Roast,0:F&V,1:Deli,2:Bakery,3:Meat,4:A1,9:FrontDoor,11:Cheeses,10:M Aisle,5:zzzz,6:JuiceAisle
+L2,0:F&V,1:Bakery,2:No thing2,3:Deli,4:Fridge,5:Bread,6:Choc
+I0,Cheese sliced,R,L0:2,L1:11,L2:4
+I1,Milk,R,L0:2,L1:10,L2:4
+I2,Bread,R,L0:5
+I3,Weetbix,R,L0:0,L1:6
+I4,Allbran,I,L0:0,L1:5
+I5,Tin Fruit,I,L0:0
+I6,Yoghurt,I,L0:2
+I8,Juice,I,L1:6
+I9,Sugar,I,L0:5
+I10,Flour,I
+I11,Sugar Icing,I,L0:0,L1:9
+I12,Sugar Brown,I,L0:0,L1:5
+I13,Fruit,I
+I14,Veges,R,L0:2,L1:12
+I15,RoastingVeges,R,L0:2,L1:12
+I17,Tomato Sauce,R,L0:0
+I18,Worchester Sc,R,L0:0,L1:5
+I19,Soy Sauce,R,L0:1,L1:1,L2:3
+I20,Spagetti,R,L0:2,L1:2,L2:2
+I21,Pasta,R,L0:1,L1:4
+I22,Noodles,R,L0:0
+I23,EasyMac,R,L0:1,L1:5
+I24,Oat Bran,R,L0:0,L1:8
+I25,Tea Bags,I,L0:4
+I26,Milo,R,L0:1,L1:10
+I27,Saltanas,R,L0:1,L1:4
+I28,Margarine,R,L0:2
+I29,Bacon,R,L0:2
+I30,Mince,R,L0:1,L1:3
+I31,Sugar Raw,I,L0:0,L1:0
+I32,Sustain,I,L0:0,L1:8
+I33,Cheese block,I,L0:2,L1:11
+I34,bob,I,L0:0
+I35,fredd,I
+I36,SometingNew,I,L0:3
diff --git a/Shop/todo b/Shop/todo
new file mode 100644 (file)
index 0000000..95870fe
--- /dev/null
+++ b/Shop/todo
@@ -0,0 +1,67 @@
+Design:
+
+  enter new 'location'
+  enter new 'place'
+  Delete items??  
+  prime new list from old list
+  go to top
+  scroll up/down??
+
+DONE  How to 'scroll' in search mode?
+    Maybe select location separator
+DONE  list load/save
+DONE  report 'place' on display
+DONE  Change product to/from 'regular'
+         tick or cross in search mode
+
+
+  FL menu:
+   - reset:
+        All 'found', 'regular', 'noneed' return to
+                 'regular' or 'noneed'
+                 and lose comment
+        All 'need' and 'cannot' become 'need'
+   - exit ??
+   - Edit place/location
+   - Delete.  need to re-select item after entering menu, then delete
+
+Implement:
+
+DONE  Make 'search' a toggle button
+DONE  Sort by location
+DONE  separator for location change
+DONE  change place
+DONE  change name of item
+DONE  change location of item
+DONE  save table
+DONE  save list
+DONE  load old list
+DONE  left/right click to tick/cross instantly
+DONE  guessture text input
+DONE  Don't jump instantly from 'search' back to 'list'
+DONE  Highlight current in 'search' mode
+
+  ?highlight entries with comments?  
+Don't allow things to disappear instantly
+New items need to be sorted in
+disable button that won't work.
+
+DONE - Bigger left-right buttons when selecting place
+DONE - Don't lose notes on newly added items.
+DONE - newly added items should get sorted in to 'unknown'
+DONE - When setting 'location', jump to 'here' being the last place
+NO - just discard this functionality. - goto-head, goto-tail should update notes.
+- button for 'fresh list'
+- button for 'hide/show all regular'
+DONE - Don't got to 'regular' if it isn't regular.
+DONE - darker green colour
+DONE - labels for each location
+DONE - tag items with comments
+- find files in more sensible way
+- add places
+- edit places
+- re-order places
+DONE - add locations
+DONE - edit locations
+- re-order locations
+DONE - delete a product??
diff --git a/apmhacks/apmd_proxy b/apmhacks/apmd_proxy
new file mode 100755 (executable)
index 0000000..3a8cb0d
--- /dev/null
@@ -0,0 +1,98 @@
+#!/bin/sh
+# 
+# apmd_proxy - program dispatcher for APM daemon
+#
+# Written by Craig Markwardt (craigm@lheamail.gsfc.nasa.gov) 21 May 1999
+# Modified for Debian by Avery Pennarun
+#
+# This shell script is called by the APM daemon (apmd) when a power
+# management event occurs.  Its first and second arguments describe the
+# event.  For example, apmd will call "apmd_proxy suspend system" just
+# before the system is suspended.
+#
+# Here are the possible arguments:
+#
+# start              - APM daemon has started
+# stop               - APM daemon is shutting down
+# suspend critical   - APM system indicates critical suspend (++)
+# suspend system     - APM system has requested suspend mode
+# suspend user       - User has requested suspend mode
+# standby system     - APM system has requested standby mode 
+# standby user       - User has requested standby mode
+# resume suspend     - System has resumed from suspend mode
+# resume standby     - System has resumed from standby mode
+# resume critical    - System has resumed from critical suspend
+# change battery     - APM system reported low battery
+# change power       - APM system reported AC/battery change
+# change time        - APM system reported time change (*)
+# change capability  - APM system reported config. change (+)
+#
+# (*) - APM daemon may be configured to not call these sequences
+# (+) - Available if APM kernel supports it.
+# (++) - "suspend critical" is never passed to apmd from the kernel,
+#   so we will never see it here.  Scripts that process "resume
+#   critical" events need to take this into account.
+#
+# It is the proxy script's responsibility to examine the APM status
+# (via /proc/apm) or other status and to take appropriate actions.
+# For example, the script might unmount network drives before the
+# machine is suspended.
+#
+# In Debian, the usual way of adding functionality to the proxy is to
+# add a script to /etc/apm/event.d.  This script will be called by
+# apmd_proxy (via run-parts) with the same arguments.
+#
+# If it is important that a certain set of script be run in a certain
+# order on suspend and in a different order on resume, then put all
+# the scripts in /etc/apm/scripts.d instead of /etc/apm/event.d and
+# symlink to these from /etc/apm/suspend.d, /etc/apm/resume.d and
+# /etc/apm/other.d using names whose lexicographical order is the same
+# as the desired order of execution.
+#
+# If the kernel's APM driver supports it, apmd_proxy can return a non-zero
+# exit status on suspend and standby events, indicating that the suspend
+# or standby event should be rejected.
+#
+# *******************************************************************
+
+set -e
+
+# The following doesn't yet work, because current kernels (up to at least
+# 2.4.20) do not support rejection of APM events.  Supporting this would
+# require substantial modifications to the APM driver.  We will re-enable
+# this feature if the driver is ever modified.       -- cph@debian.org
+#
+#SUSPEND_ON_AC=false
+#[ -r /etc/apm/apmd_proxy.conf ] && . /etc/apm/apmd_proxy.conf
+#
+#if [ "${SUSPEND_ON_AC}" = "false" -a "${2}" = "system" ] \
+#      && on_ac_power >/dev/null; then
+#    # Reject system suspends and standbys if we are on AC power
+#    exit 1  # Reject (NOTE kernel support must be enabled)
+#fi
+
+echo $1 $2 $FORCE_APM `date` >> /tmp/apm.trace
+if [ " $1" = " suspend" -a " $2" = " user" -a " $FORCE_APM" != " yes" ]
+then exit 0
+fi
+if [ " $1" = " resume" -a " $FORCE_APM" != " yes" ]
+then exit 0
+fi
+if [ "${1}" = "suspend" -o "${1}" = "standby" ]; then
+    run-parts --arg="${1}" --arg="${2}" /etc/apm/event.d
+    if [ -d /etc/apm/suspend.d ]; then
+        run-parts --arg="${1}" --arg="${2}" /etc/apm/suspend.d
+    fi
+elif [ "${1}" = "resume" ]; then
+    if [ -d /etc/apm/resume.d ]; then
+        run-parts --arg="${1}" --arg="${2}" /etc/apm/resume.d
+    fi
+    run-parts --arg="${1}" --arg="${2}" /etc/apm/event.d
+else
+    run-parts --arg="${1}" --arg="${2}" /etc/apm/event.d
+    if [ -d /etc/apm/other.d ]; then
+        run-parts --arg="${1}" --arg="${2}" /etc/apm/other.d
+    fi
+fi
+
+exit 0
diff --git a/auxlaunch/auxlaunch.py b/auxlaunch/auxlaunch.py
new file mode 100755 (executable)
index 0000000..a2aede8
--- /dev/null
@@ -0,0 +1,457 @@
+#!/usr/bin/env python
+\r
+# Auxlaunch, October 2008, aliasid\r
+
+\r
+import pygtk\r
+import gtk\r
+import sys\r
+import os\r
+import dbus\r
+import dbus.glib\r
+import posix
+posix.chdir("/home/root")
+\r
+class ViewManager:\r
+       '''Manage the user interface'''\r
+       def __init__(self):\r
+               # Create map for change buttons. Indicate which modes\r
+               #   are invoked by which button positions. This supports options
+               #   like righ/left handed, and window-swithcing or not.\r
+               lns = {0:Controller.GRP,1:Controller.APP,2:Controller.ERR}\r
+               lsw = {0:Controller.GRP,1:Controller.APP,2:Controller.WIN}\r
+               rns = {0:Controller.APP,1:Controller.GRP,2:Controller.ERR}\r
+               rsw = {0:Controller.WIN,1:Controller.APP,2:Controller.GRP}\r
+               self.keymap = {'left': {'no_switching':lns,'switching':lsw},\r
+                              'right':{'no_switching':rns,'switching':rsw}}
+               
+               self.hidden = False  # Track if Auxluanch's window is showing or not\r
+\r
+       def start_ui(self):\r
+               global ctrl\r
+\r
+               # Set up GTK window and table\r
+               self.win = gtk.Window(gtk.WINDOW_TOPLEVEL)\r
+               self.win.connect("delete_event", self.delete_event)\r
+               self.win.connect("focus_in_event", self.focus_event)\r
+               self.win.set_border_width(10)\r
+               if ctrl.simple_ui():
+                       rows = 2
+               else:
+                       rows = 3
+               if ctrl.window_mgr():\r
+                       columns = 2\r
+               else:
+                       columns = 1
+               self.tbl = gtk.Table(rows, columns, True)\r
+               self.win.add(self.tbl)\r
+\r
+               # Create a "go" button\r
+               self.btnGo = self.create_button('gtk-dialog-error','go',0,3,0,1)\r
+               self.btnGo.set_label('initial')\r
+\r
+               # Create multiple sets of arrow or "change" buttons\r
+               #   Buttons' "data" indicates position (column) and\r
+               #   "adjustment" (+1/-1 = up or down)\r
+               if ctrl.simple_ui():
+                       self.btn1dn = self.create_button('gtk-directory',  '0,-1',0,1,1,3)\r
+                       self.btn2dn = self.create_button('gtk-index',      '1,-1',1,2,1,3)\r
+                       if ctrl.window_mgr():\r
+                               self.btn3dn = self.create_button('gtk-refresh','2,-1',2,3,1,3)\r
+               else:
+                       self.btn1up = self.create_button('gtk-go-up'  ,'0,+1',0,1,1,2)\r
+                       self.btn1dn = self.create_button('gtk-go-down','0,-1',0,1,2,3)\r
+                       self.btn2up = self.create_button('gtk-go-up'  ,'1,+1',1,2,1,2)\r
+                       self.btn2dn = self.create_button('gtk-go-down','1,-1',1,2,2,3)\r
+                       if ctrl.window_mgr():\r
+                               self.btn3up = self.create_button('gtk-go-up'  ,'2,+1',2,3,1,2)\r
+                               self.btn3dn = self.create_button('gtk-go-down','2,-1',2,3,2,3)\r
+\r
+               # Prepare UI\r
+               self.tbl.show()\r
+               self.win.show()\r
+               self.hide()
+\r
+               # Hook up to AUX button\r
+               bus = dbus.SystemBus()\r
+               #bus.add_signal_receiver(self.signal_handler,\r
+               #       dbus_interface="org.freesmartphone.odeviced",
+               #       signal_name=None)\r
+               bus.add_signal_receiver(self.signal_handler,\r
+                       dbus_interface="org.freesmartphone.Device.Input",\r
+                       signal_name="Event")\r
+               #bus.add_signal_receiver(self.signal_handler,None, None,
+               #       "org.freesmartphone.odeviced","/org/freesmartphone/Device/Input")
+               self.x = True
+\r
+               gtk.main() # Allow GTK's event loop to run and call us back\r
+\r
+       def signal_handler(self, name, action, xx):\r
+               '''React to AUX button press'''\r
+               print "name = %s and action = %s xx=%s" % (name,action,xx)
+               if self.x:
+                       self.x = False
+                       return
+               self.x = True
+               if (name == "AUX" or name == "ButtonPressed") and (action == "pressed" or action == "phone"):
+                       if ctrl.hide() :
+                               if self.hidden:         
+                                       self.show()
+                               else:                           
+                                       self.hide()
+                       else:                                   
+                               self.show()\r
+
+\r
+       def button_pressed(self, widget, data):\r
+               global ctrl\r
+               if data == 'go':\r
+                       ctrl.go()\r
+               else:\r
+                       # Figure out what user meant based on button button position and\r
+                       #   command line options. Asumme defaults 1st.\r
+                       # TODO: clean up constants below\r
+                       right_left = 'left'\r
+                       switching  = 'no_switching'\r
+                       if ctrl.right_hand(): right_left = 'right'\r
+                       if ctrl.window_mgr(): switching  = 'switching'\r
+                       data = data.split(',')\r
+                       column = self.keymap[right_left][switching][int(data[0])]\r
+                       ctrl.adjust(column, int(data[1]))\r
+\r
+       def set_go(self, icon, text):\r
+               '''Update Go button's image and label'''\r
+               self.btnGo.set_image(icon)\r
+               self.btnGo.set_label(text)\r
+               label = self.btnGo.child.child.get_children()[1]\r
+               label.set_markup('<big><big><big>'+text+'</big></big></big>')\r
+\r
+       def delete_event(self, widget, event, data=None):\r
+               '''Answers qeuestion: should exit event be deleted?'''\r
+               if ctrl.done():\r
+                       gtk.main_quit()\r
+                       return False\r
+               else:\r
+                       return True\r
+\r
+       def focus_event(self, widget, event, data=None):\r
+               ''' Process event: app window gained or lost focus'''\r
+               ctrl.refresh()\r
+\r
+       def create_button(self, stock_id, data, left,right,top,botton):\r
+               '''Utility function to create buttons'''\r
+               button = gtk.Button()\r
+               image = gtk.Image()\r
+               image.set_from_stock(stock_id, gtk.ICON_SIZE_DIALOG)\r
+               button.set_image(image)\r
+               button.connect("clicked", self.button_pressed, data)\r
+               self.tbl.attach(button, left, right, top, botton)\r
+               button.show()\r
+               return button\r
+\r
+       def hide(self):\r
+               self.win.iconify()\r
+               self.hidden = True
+
+       def show(self):
+               self.win.maximize()
+               self.win.present()
+               self.hidden = False                             
+\r
+       def stop_ui(self):\r
+               gtk.main_quit()\r
+\r
+class Controller:\r
+       '''Track status of auxlaunch app, manages model and view'''\r
+\r
+       # These constants are passed between the view and the controller.\r
+       #   They represent the "mode" (what the go button will do). Mode is\r
+       #   determined by most recently pressed adjust button's position on-screen.\r
+       APP = 'app'\r
+       WIN = 'win'\r
+       GRP = 'grp'\r
+       ERR = '   '\r
+\r
+       def __init__(self):\r
+               # The application's "state" is the mode of the go button\r
+               #   plus the current index positions in the lists of groups,\r
+               #   apps, and windows\r
+               self.goMode = self.APP\r
+               self.curGrp = 0\r
+               self.curApp = 0\r
+               self.curWin = 0\r
+\r
+       def run(self):
+               if self.help_needed():
+                       pass
+               else:
+                       global model\r
+                       global view\r
+                       model.load_from_rc()\r
+                       if self.dms():\r
+                               model.load_from_dms()\r
+                       view.start_ui()\r
+\r
+       # Command line options:
+       def help_needed(self):\r
+               if '-help' in sys.argv: \r
+                       print '-dms    = Include items from Debian Menu System.'\r
+                       print '-nowm   = No window manager. (Do not show window switching buttons).'\r
+                       print '-right  = Swap app-launchning and window-switching buttons (right for left).'\r
+                       print '-hide   = Cause AUX button to hide Auxlaunch (when it is displayed).'\r
+                       print '-simple = Use "change" buttons for grp/app/win selecting (instead of up/down buttons).'
+                       return True
+               else:
+                       return False
+       def window_mgr(self):\r
+               if '-nowm' in sys.argv: return False\r
+               else:                                           return True\r
+       def right_hand(self):\r
+               if '-right' in sys.argv:        return True\r
+               else:                                           return False\r
+       def dms(self):\r
+               if '-dms' in sys.argv:          return True\r
+               else:                                           return False
+       def hide(self):
+               if '-hide' in sys.argv:         return True\r
+               else:                                           return False
+       def simple_ui(self):
+               if '-simple' in sys.argv:       return True
+               else:                                           return False
+\r
+       def done(self):\r
+               '''Check if ok to exit application'''\r
+               return True # For now, always exit - nothing needs to be saved\r
+\r
+       def go(self):\r
+               '''React to "Go" button press'''\r
+               if self.goMode == self.APP:\r
+                       command = model.get_app(self.curGrp,self.curApp).get_command()\r
+                       if command == '(cancel)':\r
+                               view.hide()\r
+                       elif command == '(quit)':\r
+                               view.stop_ui()\r
+                       else:\r
+                               print 'auxlaunch: launching "'+command+'"' # debug\r
+                               view.hide()
+                               os.system(command)\r
+               elif self.goMode == self.WIN:\r
+                       window_title = model.get_window(self.curWin).get_title()\r
+                       window_ID    = model.get_window(self.curWin).get_win_id()\r
+                       print 'auxlaunch: swtiching "'+window_title+'": '+window_ID  # debug\r
+                       view.hide()
+                       os.system('wmctrl -i -a '+window_ID)\r
+               elif self.goMode == self.GRP:\r
+                       self.curApp = 0\r
+                       app = model.get_app(self.curGrp,self.curApp)\r
+                       view.set_go(app.get_icon(), app.get_name())
+                       self.goMode = self.APP\r
+\r
+       def adjust(self, new_mode, change):\r
+               '''React to change-button press, update state and "Go" button'''\r
+\r
+               # Adjust state while enforcing bounds checking of list indexes\r
+               if new_mode == Controller.GRP:\r
+                       self.curGrp = (self.curGrp + change) % model.num_groups()\r
+                       view.set_go(model.group_image(), model.get_group(self.curGrp))\r
+               elif new_mode == Controller.APP:\r
+                       self.curApp = (self.curApp + change) % model.num_apps_in_group(self.curGrp)\r
+                       app = model.get_app(self.curGrp,self.curApp)\r
+                       view.set_go(app.get_icon(), app.get_name())\r
+               elif new_mode == Controller.WIN:\r
+                       self.curWin = (self.curWin + change) % model.num_win()
+                       window = model.get_window(self.curWin)\r
+                       view.set_go(model.switch_image, window.get_title())\r
+\r
+               # Adjust Go button's mode\r
+               self.goMode = new_mode\r
+\r
+       def refresh(self):\r
+               '''Load new snapshot of other apps' window titles'''\r
+               if self.window_mgr():\r
+                       model.reload_windows()\r
+                       self.curWin = 0   # TODO: guess window based on history
+                       window = model.get_window(self.curWin)\r
+                       view.set_go(model.switch_image, window.get_title())\r
+\r
+class ModelManager:\r
+       '''Provide data - groups, apps, windows, images'''\r
+       def __init__(self):\r
+               # Create a group-to-app-list dictionary where keys will\r
+               #    be group names and items will be lists of "Apps"s\r
+               self.grpApps = {}\r
+\r
+               # Keep list objects that represent current X application windows\r
+               self.winList = []\r
+\r
+               # Store generic "group' and "switch' images - since other images are in model\r
+               self.switch_image = gtk.Image()\r
+               self.switch_image.set_from_stock('gtk-refresh', gtk.ICON_SIZE_DIALOG)\r
+               self.grp_image = gtk.Image()\r
+               self.grp_image.set_from_stock('gtk-directory', gtk.ICON_SIZE_DIALOG)\r
+\r
+       # Images\r
+       def switch_image(self):\r
+               return self.switch_image\r
+       def group_image(self):\r
+               return self.grp_image\r
+\r
+       # Windows\r
+       def num_win(self):\r
+               return len(self.winList)\r
+\r
+       def get_window(self, number):\r
+               return self.winList[number]\r
+\r
+       def reload_windows(self):\r
+               '''Build list of other apps' window IDs and titles'''\r
+               self.winList = []\r
+               output = os.popen('wmctrl -l','r')\r
+               while True:\r
+                       line = output.readline()\r
+                       if not line: break\r
+                       field = line.split(None,3)
+                       title = field[3].rstrip()       
+                       title = title.replace('<','-')
+                       title = title.replace('>','-')
+                       if title == 'auxlaunch':
+                               continue
+                       self.winList.append(WinItem(field[0], title))\r
+\r
+       # Groups and apps\r
+       def num_groups(self):\r
+               return len(self.grpApps.keys())\r
+\r
+       def num_apps_in_group(self, groupNum):
+               group = self.grpApps.keys()[groupNum]\r
+               return len(self.grpApps[group])\r
+\r
+       def get_group(self, number):\r
+               return self.grpApps.keys()[number]\r
+\r
+       def get_app(self, groupNum, appNum):\r
+               '''Provide desired app item object'''\r
+               group = self.grpApps.keys()[groupNum]\r
+               return self.grpApps[group][appNum]\r
+\r
+       def load_from_rc(self):\r
+               '''Read Auxlaunch's config file'''\r
+               # Create temporary, holding variables           
+               INITGROUP  = '(My Default Group)' 
+               curGroup   = INITGROUP  \r
+               curApps    = []\r
+\r
+               rcfile = open('.auxlaunchrc')\r
+               for line in rcfile:\r
+                       field = line.split(',')\r
+                       if field[0][0].upper() == '"':  # Menu record\r
+                               if not (curGroup == INITGROUP and len(curApps) == 0):\r
+                                       self.grpApps[curGroup] = curApps   # Flush holding vars\r
+                               curGroup = field[0].strip('"').strip("'")\r
+                               curApps = []\r
+                       else:                                                   # Item record\r
+                               image = gtk.Image()\r
+                               if field[2].rstrip() == '':\r
+                                       image.set_from_stock('gtk-execute', gtk.ICON_SIZE_DIALOG)\r
+                               elif field[2][:4] == 'gtk-':\r
+                                       image.set_from_stock(field[2], gtk.ICON_SIZE_DIALOG)\r
+                               else:\r
+                                       image.set_from_file(field[2])\r
+                               app = AppItem(image,field[0],field[1])\r
+                               curApps.append(app)\r
+\r              # Flush last holding value              
+               if not (curGroup == INITGROUP and len(curApps) == 0):\r
+                       self.grpApps[curGroup] = curApps   \r
+\r
+       def load_from_dms(self):\r
+               '''Read entries out of Debian Menu System files'''\r
+               for filename in os.listdir('/usr/share/menu'):\r
+                       file_object = open('/usr/share/menu/'+filename)\r
+                       buf = file_object.read(1000000)\r
+                       buf = buf.decode('string_escape')   # Remove escaped newlines\r
+                       entries = buf.splitlines()\r
+                       for entry in entries:\r
+                               entry = entry.split()           # Remove leading, trailing,\r
+                               entry = ' '.join(entry)         #   and extra inner spaces\r
+                               entry = entry.strip()\r
+                               if entry ==  '':        continue    # Skip empties and non-entries\r
+                               if entry[0] <> '?':     continue\r
+                               entry = entry.partition(':')[2] # Remove prior to ':'\r
+                               if entry == '':         continue\r
+                               quoted = False                              # Chop at unquoted spaces\r
+                               space_at = []\r
+                               for i in range(len(entry)):\r
+                                       if entry[i] == '"':\r
+                                               quoted = not quoted\r
+                                       if entry[i] == ' ' and not quoted:\r
+                                               space_at.append(i)\r
+                               fields = []\r
+                               start = 0\r
+                               for end in space_at:\r
+                                       fields.append(entry[start:end])\r
+                                       start = end+1\r
+                               fields.append(entry[start:])\r
+\r
+                               # Extract field names and values\r
+                               item = {'needs':'', 'title':'', 'command':'', 'icon':'', 'section':'Default'}\r
+                               for field in fields:\r
+                                       if field == '':      continue\r
+                                       if not '=' in field: continue\r
+                                       pair = field.split('=')                 # TODO Why needed to avoid error?\r
+                                       item[pair[0]] = pair[1].strip('"')\r
+\r
+                               # TODO: Don't yet know how to launch cmd line ("text") apps, so skip 'em\r
+                               if not item['needs'].upper() == 'X11': continue\r
+\r
+                               # Derive label, image, command, and group\r
+                               label = ''\r
+                               if item['title'] <> '':         label = ' ' + item['title']\r
+                               elif item['command'] <> '':     label = ' ' + item['command']\r
+                               else:                      continue\r
+                               command = item['command'] + ' &'  # TODO: Best place to add '&'?\r
+                               image = gtk.Image()\r
+                               if item['icon'] == '': image.set_from_stock('gtk-execute', gtk.ICON_SIZE_DIALOG)\r
+                               else:                  image.set_from_file(item['icon'])\r
+\r
+                               # Insert app, and possibly group, into dictionary\r
+                               appList = []\r
+                               if self.grpApps.has_key(item['section']):\r
+                                  appList = self.grpApps[item['section']]\r
+                               appList.append(AppItem(image, label, command))\r
+                               self.grpApps[item['section']] = appList\r
+\r
+\r
+class AppItem:\r
+   '''Store attributes of an application'''\r
+   def __init__(self, icon, name, command):\r
+      self.icon = icon\r
+      self.name = name\r
+      self.command = command\r
+\r
+   def get_icon(self):\r
+      return self.icon\r
+\r
+   def get_name(self):\r
+      return self.name\r
+\r
+   def get_command(self):\r
+      return self.command\r
+\r
+class WinItem:\r
+   '''Store attributes of a window'''\r
+   def __init__(self, window_id, title):\r
+               self.win_id = window_id\r
+               self.title  = title\r
+\r
+   def get_win_id(self):\r
+      return self.win_id\r
+\r
+   def get_title(self):\r
+      return self.title\r
+\r
+# Auxluanch execution starts here\r
+model = ModelManager()\r
+ctrl  = Controller()\r
+view  = ViewManager()\r
+ctrl.run()\r
+\r
diff --git a/auxlaunch/auxlaunch2.py b/auxlaunch/auxlaunch2.py
new file mode 100644 (file)
index 0000000..6edc037
--- /dev/null
@@ -0,0 +1,457 @@
+#!/usr/bin/env python
+
+# Auxlaunch, October 2008, aliasid
+
+
+import pygtk
+import gtk
+import sys
+import os
+import dbus
+import dbus.glib
+import posix
+posix.chdir("/home/root")
+
+class ViewManager:
+       '''Manage the user interface'''
+       def __init__(self):
+               # Create map for change buttons. Indicate which modes
+               #   are invoked by which button positions. This supports options
+               #   like righ/left handed, and window-swithcing or not.
+               lns = {0:Controller.GRP,1:Controller.APP,2:Controller.ERR}
+               lsw = {0:Controller.GRP,1:Controller.APP,2:Controller.WIN}
+               rns = {0:Controller.APP,1:Controller.GRP,2:Controller.ERR}
+               rsw = {0:Controller.WIN,1:Controller.APP,2:Controller.GRP}
+               self.keymap = {'left': {'no_switching':lns,'switching':lsw},
+                              'right':{'no_switching':rns,'switching':rsw}}
+               
+               self.hidden = False  # Track if Auxluanch's window is showing or not
+
+       def start_ui(self):
+               global ctrl
+
+               # Set up GTK window and table
+               self.win = gtk.Window(gtk.WINDOW_TOPLEVEL)
+               self.win.connect("delete_event", self.delete_event)
+               self.win.connect("focus_in_event", self.focus_event)
+               self.win.set_border_width(10)
+               if ctrl.simple_ui():
+                       rows = 2
+               else:
+                       rows = 3
+               if ctrl.window_mgr():
+                       columns = 2
+               else:
+                       columns = 1
+               self.tbl = gtk.Table(rows, columns, True)
+               self.win.add(self.tbl)
+
+               # Create a "go" button
+               self.btnGo = self.create_button('gtk-dialog-error','go',0,3,0,1)
+               self.btnGo.set_label('initial')
+
+               # Create multiple sets of arrow or "change" buttons
+               #   Buttons' "data" indicates position (column) and
+               #   "adjustment" (+1/-1 = up or down)
+               if ctrl.simple_ui():
+                       self.btn1dn = self.create_button('gtk-directory',  '0,-1',0,1,1,3)
+                       self.btn2dn = self.create_button('gtk-index',      '1,-1',1,2,1,3)
+                       if ctrl.window_mgr():
+                               self.btn3dn = self.create_button('gtk-refresh','2,-1',2,3,1,3)
+               else:
+                       self.btn1up = self.create_button('gtk-go-up'  ,'0,+1',0,1,1,2)
+                       self.btn1dn = self.create_button('gtk-go-down','0,-1',0,1,2,3)
+                       self.btn2up = self.create_button('gtk-go-up'  ,'1,+1',1,2,1,2)
+                       self.btn2dn = self.create_button('gtk-go-down','1,-1',1,2,2,3)
+                       if ctrl.window_mgr():
+                               self.btn3up = self.create_button('gtk-go-up'  ,'2,+1',2,3,1,2)
+                               self.btn3dn = self.create_button('gtk-go-down','2,-1',2,3,2,3)
+
+               # Prepare UI
+               self.tbl.show()
+               self.win.show()
+               self.hide()
+
+               # Hook up to AUX button
+               bus = dbus.SystemBus()
+               #bus.add_signal_receiver(self.signal_handler,
+               #       dbus_interface="org.freesmartphone.odeviced",
+               #       signal_name=None)
+               bus.add_signal_receiver(self.signal_handler,
+                       dbus_interface="org.freesmartphone.Device.Input",
+                       signal_name="Event")
+               #bus.add_signal_receiver(self.signal_handler,None, None,
+               #       "org.freesmartphone.odeviced","/org/freesmartphone/Device/Input")
+               self.x = True
+
+               gtk.main() # Allow GTK's event loop to run and call us back
+
+       def signal_handler(self, name, action, xx):
+               '''React to AUX button press'''
+               print "name = %s and action = %s xx=%s" % (name,action,xx)
+               if self.x:
+                       self.x = False
+                       return
+               self.x = True
+               if (name == "AUX" or name == "ButtonPressed") and (action == "pressed" or action == "phone"):
+                       if ctrl.hide() :
+                               if self.hidden:         
+                                       self.show()
+                               else:                           
+                                       self.hide()
+                       else:                                   
+                               self.show()
+
+
+       def button_pressed(self, widget, data):
+               global ctrl
+               if data == 'go':
+                       ctrl.go()
+               else:
+                       # Figure out what user meant based on button button position and
+                       #   command line options. Asumme defaults 1st.
+                       # TODO: clean up constants below
+                       right_left = 'left'
+                       switching  = 'no_switching'
+                       if ctrl.right_hand(): right_left = 'right'
+                       if ctrl.window_mgr(): switching  = 'switching'
+                       data = data.split(',')
+                       column = self.keymap[right_left][switching][int(data[0])]
+                       ctrl.adjust(column, int(data[1]))
+
+       def set_go(self, icon, text):
+               '''Update Go button's image and label'''
+               self.btnGo.set_image(icon)
+               self.btnGo.set_label(text)
+               label = self.btnGo.child.child.get_children()[1]
+               label.set_markup('<big><big><big>'+text+'</big></big></big>')
+
+       def delete_event(self, widget, event, data=None):
+               '''Answers qeuestion: should exit event be deleted?'''
+               if ctrl.done():
+                       gtk.main_quit()
+                       return False
+               else:
+                       return True
+
+       def focus_event(self, widget, event, data=None):
+               ''' Process event: app window gained or lost focus'''
+               ctrl.refresh()
+
+       def create_button(self, stock_id, data, left,right,top,botton):
+               '''Utility function to create buttons'''
+               button = gtk.Button()
+               image = gtk.Image()
+               image.set_from_stock(stock_id, gtk.ICON_SIZE_DIALOG)
+               button.set_image(image)
+               button.connect("clicked", self.button_pressed, data)
+               self.tbl.attach(button, left, right, top, botton)
+               button.show()
+               return button
+
+       def hide(self):
+               self.win.iconify()
+               self.hidden = True
+
+       def show(self):
+               self.win.maximize()
+               self.win.present()
+               self.hidden = False                             
+
+       def stop_ui(self):
+               gtk.main_quit()
+
+class Controller:
+       '''Track status of auxlaunch app, manages model and view'''
+
+       # These constants are passed between the view and the controller.
+       #   They represent the "mode" (what the go button will do). Mode is
+       #   determined by most recently pressed adjust button's position on-screen.
+       APP = 'app'
+       WIN = 'win'
+       GRP = 'grp'
+       ERR = '   '
+
+       def __init__(self):
+               # The application's "state" is the mode of the go button
+               #   plus the current index positions in the lists of groups,
+               #   apps, and windows
+               self.goMode = self.APP
+               self.curGrp = 0
+               self.curApp = 0
+               self.curWin = 0
+
+       def run(self):
+               if self.help_needed():
+                       pass
+               else:
+                       global model
+                       global view
+                       model.load_from_rc()
+                       if self.dms():
+                               model.load_from_dms()
+                       view.start_ui()
+
+       # Command line options:
+       def help_needed(self):
+               if '-help' in sys.argv: 
+                       print '-dms    = Include items from Debian Menu System.'
+                       print '-nowm   = No window manager. (Do not show window switching buttons).'
+                       print '-right  = Swap app-launchning and window-switching buttons (right for left).'
+                       print '-hide   = Cause AUX button to hide Auxlaunch (when it is displayed).'
+                       print '-simple = Use "change" buttons for grp/app/win selecting (instead of up/down buttons).'
+                       return True
+               else:
+                       return False
+       def window_mgr(self):
+               if '-nowm' in sys.argv: return False
+               else:                                           return True
+       def right_hand(self):
+               if '-right' in sys.argv:        return True
+               else:                                           return False
+       def dms(self):
+               if '-dms' in sys.argv:          return True
+               else:                                           return False
+       def hide(self):
+               if '-hide' in sys.argv:         return True
+               else:                                           return False
+       def simple_ui(self):
+               if '-simple' in sys.argv:       return True
+               else:                                           return False
+
+       def done(self):
+               '''Check if ok to exit application'''
+               return True # For now, always exit - nothing needs to be saved
+
+       def go(self):
+               '''React to "Go" button press'''
+               if self.goMode == self.APP:
+                       command = model.get_app(self.curGrp,self.curApp).get_command()
+                       if command == '(cancel)':
+                               view.hide()
+                       elif command == '(quit)':
+                               view.stop_ui()
+                       else:
+                               print 'auxlaunch: launching "'+command+'"' # debug
+                               view.hide()
+                               os.system(command)
+               elif self.goMode == self.WIN:
+                       window_title = model.get_window(self.curWin).get_title()
+                       window_ID    = model.get_window(self.curWin).get_win_id()
+                       print 'auxlaunch: swtiching "'+window_title+'": '+window_ID  # debug
+                       view.hide()
+                       os.system('wmctrl -i -a '+window_ID)
+               elif self.goMode == self.GRP:
+                       self.curApp = 0
+                       app = model.get_app(self.curGrp,self.curApp)
+                       view.set_go(app.get_icon(), app.get_name())
+                       self.goMode = self.APP
+
+       def adjust(self, new_mode, change):
+               '''React to change-button press, update state and "Go" button'''
+
+               # Adjust state while enforcing bounds checking of list indexes
+               if new_mode == Controller.GRP:
+                       self.curGrp = (self.curGrp + change) % model.num_groups()
+                       view.set_go(model.group_image(), model.get_group(self.curGrp))
+               elif new_mode == Controller.APP:
+                       self.curApp = (self.curApp + change) % model.num_apps_in_group(self.curGrp)
+                       app = model.get_app(self.curGrp,self.curApp)
+                       view.set_go(app.get_icon(), app.get_name())
+               elif new_mode == Controller.WIN:
+                       self.curWin = (self.curWin + change) % model.num_win()
+                       window = model.get_window(self.curWin)
+                       view.set_go(model.switch_image, window.get_title())
+
+               # Adjust Go button's mode
+               self.goMode = new_mode
+
+       def refresh(self):
+               '''Load new snapshot of other apps' window titles'''
+               if self.window_mgr():
+                       model.reload_windows()
+                       self.curWin = 0   # TODO: guess window based on history
+                       window = model.get_window(self.curWin)
+                       view.set_go(model.switch_image, window.get_title())
+
+class ModelManager:
+       '''Provide data - groups, apps, windows, images'''
+       def __init__(self):
+               # Create a group-to-app-list dictionary where keys will
+               #    be group names and items will be lists of "Apps"s
+               self.grpApps = {}
+
+               # Keep list objects that represent current X application windows
+               self.winList = []
+
+               # Store generic "group' and "switch' images - since other images are in model
+               self.switch_image = gtk.Image()
+               self.switch_image.set_from_stock('gtk-refresh', gtk.ICON_SIZE_DIALOG)
+               self.grp_image = gtk.Image()
+               self.grp_image.set_from_stock('gtk-directory', gtk.ICON_SIZE_DIALOG)
+
+       # Images
+       def switch_image(self):
+               return self.switch_image
+       def group_image(self):
+               return self.grp_image
+
+       # Windows
+       def num_win(self):
+               return len(self.winList)
+
+       def get_window(self, number):
+               return self.winList[number]
+
+       def reload_windows(self):
+               '''Build list of other apps' window IDs and titles'''
+               self.winList = []
+               output = os.popen('wmctrl -l','r')
+               while True:
+                       line = output.readline()
+                       if not line: break
+                       field = line.split(None,3)
+                       title = field[3].rstrip()       
+                       title = title.replace('<','-')
+                       title = title.replace('>','-')
+                       if title == 'auxlaunch':
+                               continue
+                       self.winList.append(WinItem(field[0], title))
+
+       # Groups and apps
+       def num_groups(self):
+               return len(self.grpApps.keys())
+
+       def num_apps_in_group(self, groupNum):
+               group = self.grpApps.keys()[groupNum]
+               return len(self.grpApps[group])
+
+       def get_group(self, number):
+               return self.grpApps.keys()[number]
+
+       def get_app(self, groupNum, appNum):
+               '''Provide desired app item object'''
+               group = self.grpApps.keys()[groupNum]
+               return self.grpApps[group][appNum]
+
+       def load_from_rc(self):
+               '''Read Auxlaunch's config file'''
+               # Create temporary, holding variables           
+               INITGROUP  = '(My Default Group)' 
+               curGroup   = INITGROUP  
+               curApps    = []
+
+               rcfile = open('.auxlaunchrc')
+               for line in rcfile:
+                       field = line.split(',')
+                       if field[0][0].upper() == '"':  # Menu record
+                               if not (curGroup == INITGROUP and len(curApps) == 0):
+                                       self.grpApps[curGroup] = curApps   # Flush holding vars
+                               curGroup = field[0].strip('"').strip("'")
+                               curApps = []
+                       else:                                                   # Item record
+                               image = gtk.Image()
+                               if field[2].rstrip() == '':
+                                       image.set_from_stock('gtk-execute', gtk.ICON_SIZE_DIALOG)
+                               elif field[2][:4] == 'gtk-':
+                                       image.set_from_stock(field[2], gtk.ICON_SIZE_DIALOG)
+                               else:
+                                       image.set_from_file(field[2])
+                               app = AppItem(image,field[0],field[1])
+                               curApps.append(app)
+               # Flush last holding value              
+               if not (curGroup == INITGROUP and len(curApps) == 0):
+                       self.grpApps[curGroup] = curApps   
+
+       def load_from_dms(self):
+               '''Read entries out of Debian Menu System files'''
+               for filename in os.listdir('/usr/share/menu'):
+                       file_object = open('/usr/share/menu/'+filename)
+                       buf = file_object.read(1000000)
+                       buf = buf.decode('string_escape')   # Remove escaped newlines
+                       entries = buf.splitlines()
+                       for entry in entries:
+                               entry = entry.split()           # Remove leading, trailing,
+                               entry = ' '.join(entry)         #   and extra inner spaces
+                               entry = entry.strip()
+                               if entry ==  '':        continue    # Skip empties and non-entries
+                               if entry[0] <> '?':     continue
+                               entry = entry.partition(':')[2] # Remove prior to ':'
+                               if entry == '':         continue
+                               quoted = False                              # Chop at unquoted spaces
+                               space_at = []
+                               for i in range(len(entry)):
+                                       if entry[i] == '"':
+                                               quoted = not quoted
+                                       if entry[i] == ' ' and not quoted:
+                                               space_at.append(i)
+                               fields = []
+                               start = 0
+                               for end in space_at:
+                                       fields.append(entry[start:end])
+                                       start = end+1
+                               fields.append(entry[start:])
+
+                               # Extract field names and values
+                               item = {'needs':'', 'title':'', 'command':'', 'icon':'', 'section':'Default'}
+                               for field in fields:
+                                       if field == '':      continue
+                                       if not '=' in field: continue
+                                       pair = field.split('=')                 # TODO Why needed to avoid error?
+                                       item[pair[0]] = pair[1].strip('"')
+
+                               # TODO: Don't yet know how to launch cmd line ("text") apps, so skip 'em
+                               if not item['needs'].upper() == 'X11': continue
+
+                               # Derive label, image, command, and group
+                               label = ''
+                               if item['title'] <> '':         label = ' ' + item['title']
+                               elif item['command'] <> '':     label = ' ' + item['command']
+                               else:                      continue
+                               command = item['command'] + ' &'  # TODO: Best place to add '&'?
+                               image = gtk.Image()
+                               if item['icon'] == '': image.set_from_stock('gtk-execute', gtk.ICON_SIZE_DIALOG)
+                               else:                  image.set_from_file(item['icon'])
+
+                               # Insert app, and possibly group, into dictionary
+                               appList = []
+                               if self.grpApps.has_key(item['section']):
+                                  appList = self.grpApps[item['section']]
+                               appList.append(AppItem(image, label, command))
+                               self.grpApps[item['section']] = appList
+
+
+class AppItem:
+   '''Store attributes of an application'''
+   def __init__(self, icon, name, command):
+      self.icon = icon
+      self.name = name
+      self.command = command
+
+   def get_icon(self):
+      return self.icon
+
+   def get_name(self):
+      return self.name
+
+   def get_command(self):
+      return self.command
+
+class WinItem:
+   '''Store attributes of a window'''
+   def __init__(self, window_id, title):
+               self.win_id = window_id
+               self.title  = title
+
+   def get_win_id(self):
+      return self.win_id
+
+   def get_title(self):
+      return self.title
+
+# Auxluanch execution starts here
+model = ModelManager()
+ctrl  = Controller()
+view  = ViewManager()
+ctrl.run()
+
diff --git a/gsm/At.Commands.pdf b/gsm/At.Commands.pdf
new file mode 100644 (file)
index 0000000..4ddb3d5
Binary files /dev/null and b/gsm/At.Commands.pdf differ
diff --git a/gsm/notes b/gsm/notes
new file mode 100644 (file)
index 0000000..15c18ff
--- /dev/null
+++ b/gsm/notes
@@ -0,0 +1,105 @@
+
+gsmd.
+
+1/ Need to make sure device is working.
+
+   run muxer
+   get a connection
+   run CFUN? and CFUN=1
+   try COPS
+
+   Check if Pin needed.  Watch /var/run/gsm-state/pin to get number.
+
+   If we cannot get anything useful,
+        kill muxer
+        power down, power up
+        try again
+        rate limit this severely.
+
+2/ get suspend notification and request updates
+
+   On every update and at least every 30 seconds, get signal strength
+      Also check CFUN if 0, try COPS
+      validate COPS every 5 minutes
+   If COPS? is 0, get COPS=? every 10 minutes. or when
+   Store state info in
+      /var/run/gsm-state/
+           cell  carrier  strength  activity  newsms  incoming signal_strength
+
+3/ When a suspend is signalled
+      disable notification of cellid and status - leave SMS
+      allow suspend
+      on wake, request updates
+
+
+-------------------------
+When sending a command, need to "\r" to get attention
+If last reply was more than 5 seconds ago, send 'AT\r' every second
+  until get a response, or reset.
+Wait for OK, then send command.
+Always expect async notifications and handle them directly.
+
+So:
+ we need an 'init' string.
+  ATV1;E0;+CMEE=2;+CRC=1;
+
+ Then some settings that we can check and set.
+  
+  +CFUN?   +CFUN: (\d+)   0    +CFUN=1
+  +COPS?   +COPS: (.*)    0    +COPS
+  +CMFG?   +CMFG: \d      0    +CMGF=1
+  +CPIN?   +CPIN: READY
+ Some things we just periodically get
+  +CSQ     +CSQ: \d+,\d+
+CLIP
+CNMI
+CSCB
+
+
+So each of these settings has:
+  - string to send to check
+  - string to send to set
+  - regexp to see if check or not
+  - last send time
+  - last recv time
+  - count of attempts to set
+  - current setting
+  - poll timeout
+  - handler to call on change
+
+
+I need to know:
+  - phone is on
+  - provider
+  - cell id
+  - signal strength
+  - call state
+  - incoming caller number
+  - incoming TXT message
+
+I need to adjust to requested state:
+  - active
+  - suspend
+  - flight mode
+
+Someone else worries about whether to answer calls etc.
+
+Flight mode must survive power-cycle.  So it doesn't live in /var/volatile.
+Probably /var/state or /var/lib/misc
+
+Others are in /var/run places.
+There is /var/run/suspend/whatever
+and /var/run/gsm/{provider,cell,signal,state,CNI,lasttxt
+
+
+So states are:
+  flight
+  suspend
+  idle
+  incoming
+  on-call
+
+For each of these, there is a collection of setting that we want
+to impose and monitor
+
diff --git a/guessture b/guessture
new file mode 160000 (submodule)
index 0000000..b5979fa
--- /dev/null
+++ b/guessture
@@ -0,0 +1 @@
+Subproject commit b5979fa52ad017c1d5853cd533024364e967c4e2
diff --git a/launcher/.launchrc b/launcher/.launchrc
new file mode 100644 (file)
index 0000000..90fb0a3
--- /dev/null
@@ -0,0 +1,25 @@
+"Admin",
+Suspend,/media/card/suspend,gtk-media-pause,
+Cancel,(cancel),gtk-cancel,
+Quit,(quit),gtk-quit,
+Reload,(reload),
+PanelPlug,sh /home/root/gopanel,
+HTop,xterm -e htop ,
+"Network",
+wifi, ifconfig usb0 down ; iwconfig eth0 essid JesusIsHere;ifconfig eth0 up;udhcpc eth0  ,
+UsbNet, ifdown eth0; ifdown bnep0; ifup usb0,
+gprs,/media/card/neilb/gprs,
+nogprs,/media/card/neilb/nogprs,
+BlueNet, ifdown usb0; ifdown eth0; ifup bnep0,1
+"Music",
+Music, mplayer /media/card/Music/*/*  , gtk-media-play,
+Mokoko, mokoko , gtk-media-play,
+MyMusic, exec music.py , gtk-media-play,
+ABC, mplayer http://mp3media1.abc.net.au:8060/classicfm.mp3   ,gtk-media-play,
+Stop, fuser -k /dev/dsp;fuser -k /dev/snd/timer,gtk-media-stop,
+Movie, xrandr -o 1 ; mplayer /media/card/orig.mov ; xrandr -o 0,
+"Apps",
+TangoGPS,exec tangogps ,
+SuDoKu, exec sudoku_main.py,
+Scribble,exec scribble.py  ,ScribblePad
+Shop, exec shop.py ,
diff --git a/launcher/fingerscroll.py b/launcher/fingerscroll.py
new file mode 100644 (file)
index 0000000..662484f
--- /dev/null
@@ -0,0 +1,85 @@
+#!/usr/bin/env python
+
+# FingerScroll is a simple widget to wrap around TextView
+# so that the TextBuffer can be scrolled with finger-wipes.
+
+import gtk
+
+class FingerScroll(gtk.TextView):
+    def __init__(self):
+        gtk.TextView.__init__(self)
+        self.hadj = gtk.Adjustment()
+        self.vadj = gtk.Adjustment()
+        self.set_size_request(1,1)
+        self.set_scroll_adjustments(self.hadj, self.vadj)
+
+        self.add_events(gtk.gdk.POINTER_MOTION_MASK
+                        | gtk.gdk.POINTER_MOTION_HINT_MASK
+                        | gtk.gdk.BUTTON_PRESS_MASK
+                        | gtk.gdk.BUTTON_RELEASE_MASK)
+        self.connect("button_press_event", self.press)
+        self.connect("button_release_event", self.release)
+        self.connect("motion_notify_event", self.motion)
+        self.drag_start = None
+
+    def press(self, w, ev):
+        w.stop_emission("button_press_event")
+
+        self.drag_start = int(ev.x), int(ev.y)
+        self.xstart = self.hadj.value
+        self.ystart = self.vadj.value
+
+    def release(self, w, ev):
+        self.drag_start = None
+        w.stop_emission("button_release_event")
+
+    def motion(self, w, ev):
+        if self.drag_start == None:
+            return
+
+        if ev.is_hint:
+            x, y, state = ev.window.get_pointer()
+        else:
+            x = ev.x
+            y = ev.y
+        x = int(x)
+        y = int(y)
+        dx = x - self.drag_start[0]
+        dy = y - self.drag_start[1]
+        newx, newy = self.xstart, self.ystart
+        if abs(dx) > abs(dy):
+            newx = newx - dx
+        else:
+            newy = newy - dy
+
+        if newx >= self.hadj.upper - self.hadj.page_size:
+            newx = self.hadj.upper - self.hadj.page_size
+        if newx <= self.hadj.lower: newx = self.hadj.lower
+        if newy >= self.vadj.upper - self.vadj.page_size:
+            newy = self.vadj.upper - self.vadj.page_size
+        if newy <= self.vadj.lower: newy = self.vadj.lower
+        self.hadj.value = newx
+        self.vadj.value = newy
+        w.stop_emission("motion_notify_event")
+
+if __name__ == "__main__":
+    # test app for FingerScroll
+    import sys
+    w = gtk.Window(gtk.WINDOW_TOPLEVEL)
+    w.connect("destroy", lambda w: gtk.main_quit())
+    w.set_title("FingerScroll test")
+    w.show()
+    w.set_default_size(200,200)
+    
+    sw = FingerScroll(); sw.show()
+    w.add(sw)
+
+    b = sw.get_buffer()
+
+    f = open(sys.argv[-1], "r")
+    l = f.readline()
+    while len(l):
+        b.insert(b.get_end_iter(), l)
+        l = f.readline()
+
+    gtk.main()
diff --git a/launcher/gpstz b/launcher/gpstz
new file mode 100644 (file)
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/launcher/launch.py b/launcher/launch.py
new file mode 100644 (file)
index 0000000..64d012e
--- /dev/null
@@ -0,0 +1,1411 @@
+#!/usr/bin/env python2.5
+
+# Neo Launcher
+# inspired in part by Auxlaunch
+#
+# We have a list of folders each of which contains a list of
+# tasks, each of which can have a small number of options.
+# We present the folders in one column and the tasks in another,
+# with the options in buttons below.
+# Task types are:
+#   Program:  Option is to run the program
+#      If the program is currently running, there is also an option to
+#      kill the program
+#      If there is a window that is believed to be attached to the program
+#      The kill option simply closes that window, and there is another option
+#        to raise the window
+#   Window: Options are to raise or to kill the window.
+#
+#
+# TODO
+#  LATER  Make list of windows-to-exclude (Panel 0) configurable
+#  Sort things?
+#  more space around main words
+#
+# Design thoughts 28Dec2010
+#  Having separete 'internal' folders is bad
+#  And having to explicitly list lots of speed-dials for a speed-dial
+#  folder is bad.
+#  So I want two classes of folder:
+#  - one that has an explicit list of items, which can be programs or
+#    any internal function.
+#  - one that has an implicit list of items, which is generated by an
+#    internal function or a plug-in
+#  The implicit list could be an internal function which creates multiple
+#  entries..
+#  So:
+#  [foldername]
+#   tag,command line,window-name
+#   tag,(internal)
+#   tag,internal()
+#   tag,module.internal(arguments)
+#   *,internal(arguments)
+#
+# The list-creating function would need a call-back to ask for the list
+# to be re-calculated
+# possible function lists are:
+# - windows
+# - speed-dials
+# - recent-calls
+# - wifi networks scanned (add/add-auto, possibly with password)
+# - wifi networks known (connect, delete)
+# - nearby time zones
+# - generic list that was asked for, thus effecting a three-level
+#   list.  Maybe I want that anyway?
+# Slight change - allow an internal function to provide a new list.  This
+# switches to a virtual folder showing that list.  When the original folder
+# is selected, we go back there...
+
+import gtk, gobject
+import pygtk
+import sys, os, time
+import pango, re
+import struct
+import dnotify
+import fcntl
+from fingerscroll import FingerScroll
+from subprocess import Popen, PIPE
+from wmctrl import winlist
+
+import ctypes
+libc = ctypes.cdll.LoadLibrary("libc.so.6")
+libc.mlockall(3)
+
+class EvDev:
+    def __init__(self, path, on_event):
+        self.f = os.open(path, os.O_RDWR|os.O_NONBLOCK);
+        self.ev = gobject.io_add_watch(self.f, gobject.IO_IN, self.read)
+        self.on_event = on_event
+        self.grabbed = False
+        self.down_count = 0
+    def read(self, x, y):
+        try:
+            str = os.read(self.f, 16)
+        except:
+            return True
+
+        if len(str) != 16:
+            return True
+        (sec,usec,typ,code,value) = struct.unpack_from("IIHHI", str)
+        if typ == 0x01:
+            # KEY event
+            if value == 0:
+                self.down_count -= 1
+            else:
+                self.down_count += 1
+            if self.down_count < 0:
+                self.down_count = 0
+        self.on_event(self.down_count, typ, code, value, sec* 1000 + int(usec/1000))
+        return True
+    def grab(self):
+        if self.grabbed:
+            return
+        #print "grab"
+        fcntl.ioctl(self.f, EVIOC_GRAB, 1)
+        self.grabbed = True
+    def ungrab(self):
+        if not self.grabbed:
+            return
+        #print "release"
+        fcntl.ioctl(self.f, EVIOC_GRAB, 0)
+        self.grabbed = False
+
+
+class WinList:
+    """
+    read in a window list - present each as a Task
+    Allow registering tasks so that when a window appears, we connect it.
+    """
+    def __init__(self):
+        self.windows = {}
+        self.tasks = {}
+        self.tasklist = []
+        self.pid = os.getpid()
+        self.old_windows = None
+        self.last_reload = 0
+        self.winlist = winlist()
+        gobject.io_add_watch(self.winlist.fd, gobject.IO_IN, self.winlist.events)
+        self.winlist.on_change(self.refresh)
+
+    def add(self, winid, desk, pid, host, name):
+        self.windows[winid] = [name, pid]
+        if self.old_windows and winid in self.old_windows:
+            self.windows[winid] = [name, pid] + self.old_windows[winid][2:]
+        p = 'pid:%d' % int(pid)
+        #print "Looking for ", p
+        if p in self.tasks:
+            self.tasks[p](winid)
+            self.windows[winid].append(self.tasks[p])
+            del self.tasks[p]
+        n = 'name:' + name
+        if n in self.tasks:
+            self.tasks[n](winid)
+            self.windows[winid].append(self.tasks[n])
+            del self.tasks[n]
+
+    def remove_old(self):
+        for winid in self.old_windows:
+            if not winid in self.windows:
+                #print "removing",winid
+                for c in self.old_windows[winid][2:]:
+                    c("")
+        self.old_windows = None
+
+    def register(self, pid, name, found):
+        if pid != None:
+            p = 'pid:%d' % int(pid)
+            self.tasks[p] = found
+        if name != None:
+            n = 'name:' + name
+            self.tasks[n] = found
+
+    def reload(self):
+        self.old_windows = self.windows
+        self.windows = {}
+        self.tasklist = []
+        for w in self.winlist.winfo:
+            win = self.winlist.winfo[w]
+            if win.pid == self.pid:
+                continue
+            self.add(win, 0, win.pid, '', win.name)
+            self.tasklist.append(WinTask(win, 0, win.pid, '', win.name))
+        self.remove_old()
+
+        togo = []
+        for k in self.tasks:
+            if not self.tasks[k]():
+                togo.append(k)
+        for k in togo:
+            del self.tasks[k]
+
+        return self.tasklist
+
+
+    def refresh(self):
+        self.reload()
+        global window
+        if window:
+            window.refresh()
+
+ProcessList = []
+class JobCtrl:
+    """
+    Manage processes.
+    Call to start a process, and get a call back when the process finishes.
+
+    """
+    global ProcessList
+    def __init__(self, cmd, finished = None):
+        self.finished = finished
+        if cmd == None:
+            self._child_created = False
+            return
+        self.Popen = Popen(cmd, shell=True, close_fds = True)
+        self.pid = self.Popen.pid
+        self.returncode = None
+        ProcessList.append(self)
+
+    def poll(self):
+        if self.Popen.poll() != None and self.finished:
+            self.returncode = self.Popen.returncode
+            self.finished(self.returncode)
+            self.finished = None
+            window.folder_select(window.folder_num)
+        return self.returncode
+            
+            
+    def poll_all(self):
+        l = range(len(ProcessList))
+        l.reverse()
+        for i in l:
+            p = ProcessList[i]
+            if p.poll() != None:
+                del ProcessList[i]
+                
+        
+
+class Selector(gtk.DrawingArea):
+    def __init__(self, names = [], pos = 0, center=False):
+        gtk.DrawingArea.__init__(self)
+
+        self.on_select = None
+        self.do_center = center
+
+        self.pixbuf = None
+        self.width = self.height = 0
+        self.need_redraw = True
+        self.colours = None
+        self.collist = {}
+
+        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 = self.get_pango_context().get_font_description()
+        fd.set_absolute_size(25 * pango.SCALE)
+        self.fd = fd
+        self.modify_font(fd)
+        met = self.get_pango_context().get_metrics(fd)
+        self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE
+        self.lineheight *= 1.5
+        self.lineheight = int(self.lineheight)
+
+        self.offsets = []
+        self.names = names
+        self.pos = pos
+        self.top = 0
+        self.queue_draw()
+
+    def assign_colour(self, purpose, name):
+        self.collist[purpose] = name
+
+    def set_list(self, names, pos = 0):
+        self.names = names
+        self.pos = pos
+        self.refresh()
+        if self.on_select:
+            self.on_select(pos)
+
+    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 not self.names:
+            return
+        
+        lines = int(self.height / self.lineheight)
+        entries = self.names()
+        # probably place current entry in the middle
+        top = self.pos - lines / 2
+        if top < 0:
+            top = 0
+        # but try not to leave blank space at the end
+        if top + lines > entries:
+            top = entries - lines
+        # but never have blank space at the top
+        if top < 0:
+            top = 0
+        self.top = top
+        offsets = [0]
+
+        for l in range(lines):
+            
+            (type, name, other) = self.names(top+l)
+            #print type, name, other
+            if type == "end":
+                break
+
+            offset = offsets[-1]
+            layout = self.create_pango_layout("")
+            layout.set_markup(name)
+            (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+            if ew > self.width:
+                # never truncate the start
+                ew = self.width
+
+            height = self.lineheight
+            #print eh, height
+            if eh > height:
+                height = eh
+
+            if l == self.pos - top:
+                self.pixbuf.draw_rectangle(self.colours['selected'], True,
+                                           0+2, offset,
+                                           self.width-4, height)
+            self.pixbuf.draw_layout(self.colours[type],
+                                    (self.width-ew)/2,
+                                    offset + (height-eh)/2,
+                                    layout)
+            offsets.append(offset + height)
+        self.offsets = offsets
+
+    def refresh(self):
+        #print "refresh"
+        self.need_redraw = True
+        self.queue_draw()
+        # must return False so that timers don't refire
+        return False
+
+    def press(self,w,ev):
+        if not self.offsets:
+            return
+        row = len(self.offsets)
+        for i in range(len(self.offsets)):
+            if ev.y < self.offsets[i]:
+                row = i-1
+                break
+
+        if self.pos != row + self.top:
+            self.pos = row + self.top
+        if self.on_select:
+            self.on_select(self.pos)
+                
+        self.refresh()
+        
+    def release(self,w,ev):
+        pass
+
+
+class Task:
+    """Identifies a particular task that is a member of a folder.
+       If the task is running, the PID is tracked here too.
+
+    """
+    def __init__(self, name):
+        self.name = name
+
+    def options(self):
+        #return ["Yes", "No"]
+        return ["Yes"]
+
+    def copyconfig(self, orig):
+        pass
+
+    def setgroup(self, group, pos):
+        self.group = group
+        self.pos = pos
+
+
+    def refresh(self, select):
+        global window
+        if not window:
+            return
+        if select:
+            print "REFRESH", window.folder_num, window.folder_pos[window.folder_num], self.group, self.pos
+            if window.folder_num != self.group:
+                window.folder_select(self.group)
+                window.task_select(self.pos)
+            elif window.folder_pos[window.folder_num] != self.pos:
+                window.task_select(self.pos)
+            window.active = False
+            window.activate()
+
+        gobject.timeout_add(300, window.refresh)
+
+    def set_tasks(self, list, pos):
+        global window
+        window.set_tasks(list, pos)
+
+class CmdTask(Task):
+    """
+    Task subtype for handling normal commands
+    """
+    def __init__(self, line):
+        fields = line.split(',')
+        name = fields[0]
+        Task.__init__(self, name)
+        self.job = None
+        self.win_id = None
+        self.cmd = None
+        self.winname = None
+
+        if len(fields) > 1:
+            self.cmd = fields[1]
+        if len(fields) > 2:
+            self.winname = fields[2].strip()
+
+        global windowlist
+        def gotit(winid = None):
+            if winid:
+                self.win_id = winid
+            return True
+            
+        windowlist.register(None, self.winname, gotit)
+
+    def options(self):
+        if self.win_id:
+            return ["Raise", "Close"]
+        if self.job:
+            return ["Kill"]
+        if self.cmd:
+            return ["Run"]
+        return ["--"]
+
+    def event(self, num):
+        if self.win_id:
+            if num == 0:
+                try:
+                    self.win_id.raise_win()
+                except:
+                    self.win_id = None
+
+                return True
+            if num == 1:
+                self.win_id.close_win()
+
+        elif self.job:
+            if num == 0:
+                os.kill(self.job.pid, 15)
+                self.job.poll()
+                gobject.timeout_add(400,
+                                    lambda *a :(JobCtrl(None).poll_all(),window.refresh()))
+        elif self.cmd:
+            global windowlist
+            self.job = JobCtrl(self.cmd, self.finished)
+            windowlist.register(self.job.pid, self.winname, self.winfound)
+            print "registered ", self.job.pid, " for ", self.name
+            return True
+        return False
+
+    def winfound(self, winid = None):
+        if winid != None:
+            #print "task",self.name,"now has winid", winid
+            self.win_id = winid
+        return self.job != None
+
+    def finished(self, retcode):
+        self.job = None
+
+    def info(self):
+        if self.job:
+            self.job.poll()
+        if self.win_id or self.job:
+            typ = 'active'
+        elif self.cmd:
+            typ = 'cmd'
+        else:
+            typ = 'void'
+        return (typ, self.name, self)
+
+    def copyconfig(self, orig):
+        self.cmd = orig.cmd
+        self.winname = orig.winname
+
+
+class WmTask(Task):
+    """
+    This is a fake task that simply holds the name of a window
+    not to show -- i.e. the parsed info from the config file
+    """
+    def __init__(self, line):
+        self.name = line[1:]
+
+class WinTask(Task):
+    """
+    Task subtype for handling Windows that have been found
+    """
+    def __init__(self, winid, desk, pid, host, name):
+        Task.__init__(self, name)
+        self.win_id = winid
+        self.pid = int(pid)
+
+    def options(self):
+        if self.pid > 0:
+            return ["Raise", "Close", "Kill"]
+        else:
+            return ["Raise", "Close"]
+
+    def event(self, num):
+        if num == 0:
+            self.win_id.raise_win()
+            return True
+        if num == 1:
+            self.win_id.close_win()
+        if num == 2:
+            os.kill(self.pid, 15)
+        return False
+
+    def info(self):
+        return ('window', self.name, self)
+
+
+class InternTask(Task):
+    """
+    An InternTask runs an internal command to choose text to display
+    It may be inactive, so options returns an empty list.
+    If the name contains a dot, we import the module and just call the
+    named function.  If not, we run internal_$name.
+    The function takes two argument.  A string:
+       "_name" - return (type, name, self)
+       "_options" - return list of button strings
+       button-name - take appropriate action
+    and this InternTask object.  The 'state' array may be manipulated.
+    """
+
+    def __init__(self, line, tag = None):
+        self.state = {}
+        self.fn = None
+        f = line.split('(')
+        p = f[0].split('.')
+        if not p[0]:
+            return
+
+        self.tag = tag
+        self.name = p[-1]
+        if p[0] != f[0]:
+            #try:
+            exec "import " +  p[0]
+            #except:
+            #    self.fn = None
+            #    return
+            self.fn = eval(line)
+        else:
+            self.fn = eval("internal_" + line)
+
+    def info(self):
+        global current_input
+        self.current_input = current_input
+        if self.tag:
+            t,n = 'cmd', self.tag
+        else:
+            t,n = self.fn("_name", self)
+        return (t,n,self)
+
+    def options(self):
+        global current_input
+        self.current_input = current_input
+        self.optionlist = self.fn("_options", self)
+        return self.optionlist
+
+    def event(self, num):
+        global current_input
+        self.current_input = current_input
+        return self.fn(num, self)
+    
+class Tasks:
+    """Holds a number of folders, each with a number of tasks
+
+       Tasks(filename)  loads from a file (or directory???)
+       reload()    re-reads the file and makes changes as needed
+       folders() - array of folder names
+       tasks(folder) - array of Task objects
+       
+    """
+    def __init__(self, path):
+        self.path = path
+        self.tasks = {}; self.gtype = {}
+        self.reload()
+
+    def reload(self):
+        self.orig_tasks = self.tasks
+        self.orig_types = self.gtype
+        self.folders = []
+        self.tasks = {}
+        self.gtype = {}
+        group = "UnSorted"
+        try:
+            f = open(self.path)
+        except:
+            f = ["[Built-in]", "Exit,(quit),"]
+        for line in f:
+            l = line.strip()
+            if not l:
+                continue
+
+            if l[0] == '"' or l[0] == '[':
+                l = l.strip('"[],')
+                f = l.split('/', 1)
+                group = f[0]
+                if len(f) > 1:
+                    group_type = f[1]
+                else:
+                    group_type = 'cmd'
+                if not group:
+                    group = 'UnSorted'
+                if group_type not in ['cmd','wm']:
+                    group_type = 'cmd'
+            else:
+                if group_type == 'cmd':
+                    words = l.split(',',1)
+                    word1 = words[1].strip(' ')
+                    arg0 = word1.split(' ',1)[0]
+                    if arg0[0] == '(':
+                        t = InternTask(word1.strip('()'), words[0])
+                    elif '(' in arg0:
+                        t = InternTask(word1, words[0])
+                    else:
+                        t = CmdTask(l)
+                elif group_type == 'wm':
+                    t = WmTask(l)
+                if not t:
+                    continue
+                if group not in self.tasks:
+                    self.folders.append(group)
+                    self.tasks[group] = []
+                    self.gtype[group] = group_type
+                if group in self.orig_tasks and \
+                   self.orig_types[group] == self.gtype[group]:
+                    for ot in self.orig_tasks[group]:
+                        if t.name == ot.name:
+                            ot.copyconfig(t)
+                            t = ot
+                            break
+                self.tasks[group].append(t)
+                t.setgroup(len(self.folders)-1, len(self.tasks[group])-1)
+        self.orig_tasks = None
+        self.orig_types = None
+
+
+    def folder_list(self):
+        return self.get_folder
+    def get_folder(self, ind = -1):
+        if ind == -1:
+            return len(self.folders)
+        elif ind < len(self.folders):
+            return ("folder", self.folders[ind], None)
+        else:
+            return ("end", "end", None)
+
+    def task_list(self, num):
+        global windowlist
+        gtype = self.gtype[self.folders[num]]
+        if gtype == "wm":
+            return lambda ind = -1 : self.get_task(ind, windowlist.reload())
+
+        return lambda ind = -1 : self.get_task(ind, self.tasks[self.folders[num]])
+    def get_task(self, ind, tl):
+        if tl == None:
+            tl = []
+        if ind == -1:
+            return len(tl)
+        elif ind < len(tl):
+            return tl[ind].info()
+        else:
+            return ("end", None, None)
+
+cliptargets = [ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ]
+class LaunchWindow(gtk.Window):
+    """
+    A window containing a list of folders and a list of entries in the folder
+    Along the bottom are per-entry buttons.
+    When a folder is selected, the entires are updated.  The last-used entry
+        in the folder is selected.
+    When an entry is selected, the buttons are updated.
+    When a button is pressed, its action is effected.
+
+    One type of action can produce text output.  In this case a replacement
+    display pane is used that is finger-scrollable.  It comes with a button
+    to revert to main display
+    """
+
+    def __init__(self, tasks):
+        gtk.Window.__init__(self)
+        self.connect("destroy", self.close_application)
+        self.set_title("Launcher")
+        self.tasks = tasks
+        self.active = False
+
+        self.create_ui()
+
+        self.clip = gtk.Clipboard(selection='PRIMARY')
+        self.cliptext = ''
+                                                         
+
+        self.folder_list = tasks.folder_list()
+        self.folder_pos = self.folder_list() * [0]
+        self.col1.set_list(self.folder_list)
+        self.set_default_size(480, 640)
+        self.show()
+
+    def create_ui(self):
+        v1 = gtk.VBox()
+        self.add(v1)
+        v1.show()
+
+        v = gtk.VBox()
+        v1.add(v)
+        v.show()
+
+        v.set_property('can-focus', True)
+        v.grab_focus()
+        v.add_events(gtk.gdk.KEY_PRESS_MASK)
+        v.connect('key_press_event', self.keystroke)
+        self.v = v
+
+        e = gtk.Entry()
+        v.pack_start(e, expand=False);
+        e.set_alignment(0.5)
+        e.connect('changed', self.entry_changed)
+        e.connect('backspace', self.entry_changed)
+        e.show()
+        self.entry = e
+        self.entry.set_text("")
+
+        h = gtk.HBox()
+        v.pack_start(h, expand=True, fill=True)
+        h.show()
+
+        self.col1 = Selector()
+        self.col2 = Selector(center=True)
+        self.col1.show()
+        self.col2.show()
+        h.pack_start(self.col1)
+        h.pack_end(self.col2)
+
+        self.col1.on_select = self.folder_select
+        self.col2.on_select = self.task_select
+
+        self.col1.assign_colour('folder','darkblue')
+        self.col1.assign_colour('selected','white')
+
+        self.col2.assign_colour('active','blue')
+        self.col2.assign_colour('cmd','black')
+        self.col2.assign_colour('selected','white')
+        self.col2.assign_colour('window','blue')
+        
+
+        h = gtk.HBox()
+        v.pack_end(h, expand=False)
+        h.set_size_request(-1,80)
+        h.show()
+
+        ctx = self.get_pango_context()
+        fd = ctx.get_font_description()
+        fd.set_absolute_size(30*pango.SCALE)
+
+        self.buttons = []
+        self.button_names = []
+        for bn in range(3):
+            b = gtk.Button("?")
+            b.child.modify_font(fd)
+            b.set_property('can-focus', False)
+            h.add(b)
+            b.connect('clicked', self.button_pressed, bn)
+            self.buttons.append(b)
+
+        fd.set_absolute_size(40*pango.SCALE)
+        self.entry.modify_font(fd)
+
+        self.main_view = v
+
+        # Now create alternate view with FingerScroll and a button
+        v = gtk.VBox()
+        v1.add(v)
+        f = FingerScroll(); f.show()
+        v.add(f)
+        self.text_buffer = f.get_buffer()
+        b = gtk.Button("Done")
+        fd.set_absolute_size(30*pango.SCALE)
+        b.child.modify_font(fd)
+        b.set_property('can-focus', False)
+        b.connect('clicked', self.text_done)
+        v.pack_end(b, expand=False)
+
+        fd = pango.FontDescription('Monospace 10')
+        fd.set_absolute_size(15*pango.SCALE)
+        f.modify_font(fd)
+        self.text_view = v
+        b.show()
+
+
+    def text_done(self,widget):
+        self.text_view.hide()
+        self.main_view.show()
+
+    def close_application(self, widget):
+        gtk.main_quit()
+
+    def checkclip(self):
+        cl = self.clip.wait_for_text()
+        if cl == self.cliptext:
+            return False
+        self.cliptext = cl
+        if type(cl) != str:
+            return False
+
+        while len(cl) > 0 and  ord(cl[-1]) >= 127:
+            cl = cl[0:-1]
+        if re.match('^ *\+?[-0-9 ()\n]*$', cl):
+            # looks like a phone number. Remove rubbish.
+            cl = cl.replace('-', '')
+            cl = cl.replace('(', '')
+            cl = cl.replace(')', '')
+            cl = cl.replace(' ', '')
+            cl = cl.replace('\n', '')
+
+        if len(self.entry.get_text()) == 0:
+            self.entry.set_text(cl)
+        else:
+            self.entry.insert_text(cl, self.entry.get_position())
+        self.entry.set_position(self.entry.get_position() +
+                                len(cl))
+        return False
+
+    def entry_changed(self, widget):
+        if not widget.get_text():
+            #widget.hide()
+            self.v.grab_focus()
+        else:
+            widget.show()
+            if not widget.is_focus():
+                widget.grab_focus()
+        global current_input
+        current_input = widget.get_text()
+        self.col2.refresh()
+        self.task_select(self.col2.pos)
+
+    def keystroke(self, widget, ev):
+        if not widget.is_focus():
+            return
+        if not ev.string:
+            # some weird control key - or AUX
+            return
+        self.entry.show()
+        self.entry.grab_focus()
+        self.entry.event(ev)
+
+    def button_pressed(self, widget, num):
+        hide = self.task.event(num)
+        self.folder_select(self.folder_num)
+        if hide:
+            self.active = False
+
+    def set_tasks(self, lister, posn, folder_num = -1):
+        self.folder_num = folder_num
+        self.get_task = lister
+        self.col2.set_list(lister, posn)
+
+    def folder_select(self, folder_num):
+        if folder_num < 0:
+            self.col1.refresh()
+            self.col2.refresh()
+            return
+        if folder_num < 0 or  folder_num >= self.folder_list():
+            return
+        self.col1.pos = folder_num
+        self.col1.refresh()
+        self.set_tasks(self.tasks.task_list(folder_num),
+                       self.folder_pos[folder_num],
+                       folder_num)
+
+    def task_select(self, task_num):
+        if task_num >= self.get_task():
+            return
+        if self.folder_num >= 0:
+            self.folder_pos[self.folder_num] = task_num
+        (typ, name, self.task) = self.get_task(task_num)
+        if self.task == None:
+            print "folder %d task %d" %(self.folder_num, task_num)
+            # FIXME how does this happen? what do I do with buttons?
+            # This can happen if we remember and old task number
+            # which (For window list) no longer exists.
+            # Fixed now I think
+            return
+        options = self.task.options()
+        while len(options) < len(self.button_names):
+            self.button_names.pop()
+            self.buttons[len(self.button_names)].hide()
+        for i in range(len(self.button_names)):
+            if options[i] != self.button_names[i]:
+                self.button_names[i] = options[i]
+                self.buttons[i].child.set_text(self.button_names[i])
+        while len(options) > len(self.button_names):
+            p = len(self.button_names)
+            self.button_names.append(options[p])
+            self.buttons[p].child.set_text(self.button_names[p])
+            self.buttons[p].show()
+
+    def activate(self):
+        #self.maximize()
+        self.text_done(None)
+        self.refresh()
+        self.present()
+        gobject.idle_add(self.checkclip)
+        if self.active:
+            self.col1.set_list(self.folder_list, 0)
+        self.active = True
+
+    def refresh(self):
+        self.folder_select(self.folder_num)
+        return False
+
+class LaunchIcon(gtk.StatusIcon):
+    def __init__(self):
+        gtk.StatusIcon.__init__(self)
+        self.set_from_stock(gtk.STOCK_EXECUTE)
+        self.connect('activate', activate)
+        
+window = None
+def activate(*a):
+    global window
+
+    JobCtrl(None).poll_all()
+    window.activate()
+
+down_at = 0
+def aux_activate(cnt, type, code, value, msec):
+    if type != 1:
+        # not a key press
+        return
+    if code != 169 and code != 116:
+        # not the AUX key and not the power key
+        return
+    global down_at
+    if value == 1:
+        # down press
+        down_at = msec
+        #print "down_at", down_at
+        return
+    if value == 0:
+        #print "up at", msec, down_at
+        if msec - down_at > 250:
+            # too long - someone else wants this press
+            return
+        activate()
+
+last_tap = 0
+def tap_check(cnt, type, code, value, msec):
+    global last_tap
+    if type != 1:
+        # not a key press
+        return
+    if code != 307:
+        # not BtnX
+        return
+    if value != 1:
+        # not a down press
+        return
+    # hack - only require one tap
+    last_tap = msec - 1
+    
+    if msec - last_tap < 200:
+        # two taps
+        last_tap = msec - 400
+        global window
+        if window.active:
+            window.entry.delete_text(0,-1)
+        activate()
+    else:
+        last_tap = msec
+
+def internal_quit(arg, obj):
+    global window
+    if arg == "_name":
+        return ('cmd', 'Exit')
+    if arg == "_options":
+        return ['quit']
+    if arg == 0:
+        window.close_application(None)
+
+def internal_time(arg, obj):
+    global window
+    if arg == "_name":
+        if 'next' not in obj.state:
+            obj.state['next'] = 0
+        now = time.time()
+        next_minute = int(now/60)+1
+        if next_minute != obj.state['next']:
+            gobject.timeout_add(int (((next_minute*60) - now) * 1000),
+                                lambda *a :(window.refresh()))
+            obj.state['next'] = next_minute
+        tm = time.strftime("%H:%M", time.localtime(now))
+        return ('cmd', '<span size="15000">'+tm+'</span>')
+    if arg == "_options":
+        return ['Set Timezone', 'wifi']
+    if arg == 0:
+        window.set_tasks(tasklist_tz(), 0)
+    if arg == 1:
+        window.set_tasks(tasklist_wifi(), 0)
+    return None
+
+def internal_date(arg, obj):
+    if len(obj.state) == 0:
+        obj.state['cmd'] = CmdTask('cal,/usr/local/bin/cal,cal')
+    if arg == "_name":
+        # no need to schedule a timeout as the 1-minute tick will do it.
+
+        #tm = time.strftime('<span size="5000">%d-%b-%Y</span>', time.localtime(time.time()))
+        tm = time.strftime('%d-%b-%Y', time.localtime(time.time()))
+        return ('cmd', tm)
+    if arg == '_options':
+        return obj.state['cmd'].options()
+    return obj.state['cmd'].event(arg)
+
+def internal_tz(zone):
+    return lambda arg, obj: _internal_tz(arg, obj, zone)
+
+def _internal_tz(arg, obj, zone):
+    if arg == '_name':
+        if 'TZ' in os.environ:
+            TZ = os.environ['TZ']
+        else:
+            TZ = None
+        os.environ['TZ'] = 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'])
+        return ('cmd', '<span size="10000">'+tm+"\n"+zone+'</span>')
+    if arg == '_options':
+        return []
+    return None
+
+def internal_echo(arg, obj):
+    if arg == '_name':
+        global current_input
+        a = current_input
+        a = a.replace('&','&amp;')
+        a = a.replace('<','&lt;')
+        a = a.replace('>','&gt;')
+        return ('cmd', a)
+    if arg == '_options':
+        return []
+    return None
+
+def internal_calc(arg, obj):
+    if arg == '_name':
+        global current_input
+        try:
+            n = eval(current_input)
+            a = '=' + str(n)
+        except:
+            if current_input:
+                a = '= ?'
+            else:
+                a = ''
+        a = a.replace('&','&amp;')
+        a = a.replace('<','&lt;')
+        a = a.replace('>','&gt;')
+        return ('cmd', a)
+    if arg == '_options':
+        return []
+    return None
+
+def internal_rotate(arg, obj):
+    if arg == '_name':
+        return ('cmd', 'rotate')
+    if arg == '_options':
+        return ['normal','left']
+    if arg == 0:
+        Popen(['xrandr', '-o', 'normal'], shell=False, close_fds = True)
+        return
+    if arg == 1:
+        Popen(['xrandr', '-o', 'left'], shell=False, close_fds = True)
+        return
+
+def internal_text(cmd):
+    return lambda arg, obj : _internal_text(arg, cmd, obj)
+
+def readsome(f, dir, p, b):
+    l = f.read()
+    b.insert(b.get_end_iter(), l)
+    if l == "":
+        return False
+    return True
+
+def child_done(pid, status, arg):
+    (p, b, w) = arg
+    fcntl.fcntl(p.stdout, fcntl.F_SETFL, 0)
+    while readsome(p.stdout, None, p, b):
+        pass
+    gobject.source_remove(w)
+    b.insert(b.get_end_iter(), "-----//-----")
+    p.stdout.close()
+
+def _internal_text(arg, cmd, obj):
+    if arg == '_name':
+        return ('cmd', cmd)
+    if arg == '_options':
+        return ['view']
+    if arg == 0:
+        global window
+        b = window.text_buffer
+        b.delete(b.get_start_iter(),b.get_end_iter())
+        p = Popen(cmd, shell=True, close_fds = True, stdout=PIPE)
+        flg = fcntl.fcntl(p.stdout, fcntl.F_GETFL, 0)
+        fcntl.fcntl(p.stdout, fcntl.F_SETFL, flg | os.O_NONBLOCK)
+        watch = gobject.io_add_watch(p.stdout, gobject.IO_IN, readsome, p, b)
+        gobject.child_watch_add(p.pid, child_done, ( p, b, watch ))
+        window.text_view.show()
+        window.main_view.hide()
+
+def internal_file(fname):
+    # return a function to be used as an internal_* function
+    # that reads the content of a file
+    return lambda arg, obj :  _internal_file(arg, fname, obj)
+
+def _internal_file(arg, fname, obj):
+    if 'dndir' not in obj.state:
+        try:
+            d = dnotify.dir(os.path.dirname(fname))
+            obj.state['dndir'] = d
+            obj.state['pending'] = False
+        except OSError:
+            obj.state['pending'] = True
+            obj.state['value'] = '--'
+    if arg == '_name':
+        if not obj.state['pending']:
+            try:
+                obj.state['dndir'].watch(os.path.basename(fname),
+                                         lambda f : _internal_file_notify(f, obj))
+                obj.state['pending'] = True
+            
+                f = open(fname)
+                l = f.readline().strip()
+                f.close()
+                obj.state['value'] = l
+            except OSError:
+                obj.state['value'] = '--'
+                l = '--'
+        else:
+            l = obj.state['value']
+        return ('cmd', l)
+    if arg == '_options':
+        return []
+    return None
+
+def _internal_file_notify(f, obj):
+    global window
+    obj.state['pending'] = False
+    f.cancel()
+    # wait a while for changes to the file to stablise
+    gobject.timeout_add(300, window.refresh)
+
+def get_task(ind, tl):
+    if tl == None:
+        tl = []
+    if ind == -1:
+        return len(tl)
+    elif ind < len(tl):
+        return tl[ind].info()
+    else:
+        return ("end", None, None)
+
+def internal_windows(arg, obj):
+    if arg == '_name':
+        return "Window List"
+    if arg == '_options':
+        return ['open']
+    if arg == 0:
+        global windowlist, window
+        window.set_tasks(lambda ind = -1 : get_task(ind, windowlist.reload()), 0)
+
+class tasklist:
+    def __init__(self):
+        self.last_refresh = 0
+        self.list = []
+        self.newlist = []
+        self.refresh_time = 60
+        self.callback = None
+        self.refresh_task = 'refresh_list'
+        self.name = 'Generic List'
+        
+    def __call__(self, ind = -1):
+        if ind <= -1:
+            if self.last_refresh + self.refresh_time < time.time():
+                self.last_refresh = time.time()
+                self.start_refresh()
+            return len(self.list) + 1
+        if ind == 0:
+            # The first entry is a simple refresh task
+            t = InternTask(self.refresh_task, self.name)
+            t.state['list'] = self
+            self.callback = t
+            return t.info()
+        if ind <= len(self.list):
+            return tasklist_task(self, ind-1).info()
+        return ("end", None, None)
+
+    def refresh_cmd(self, cmd):
+        p = Popen(cmd, shell=True, close_fds=True, stdout=PIPE)
+        fcntl.fcntl(p.stdout, fcntl.F_SETFL, os.O_NONBLOCK)
+        watch = gobject.io_add_watch(p.stdout, gobject.IO_IN, self.readsome, p)
+        gobject.child_watch_add(p.pid, self.child_done, (p, watch))
+        
+    def readsome(self, f, dir, p):
+        l = f.readline()
+        if l != "" :
+            self.readline(l.strip())
+            return True
+        return False
+
+    def child_done(self, pid, status, arg):
+        (p, watch) = arg
+        fcntl.fcntl(p.stdout, fcntl.F_SETFL, 0)
+        while self.readsome(p.stdout, None, p):
+            pass
+        gobject.source_remove(watch)
+        p.stdout.close()
+        self.readline(None)
+        self.list = self.newlist
+        self.newlist = []
+        if self.callback:
+            self.callback.refresh(False)
+
+class tasklist_task(Task):
+    """
+    A tasklist_task calls into the tasklist to get info required.
+    """
+    def __init__(self, tasklist, entry):
+        self.list = tasklist
+        self.entry = entry
+    def info(self):
+        t,n = self.list.info(self.entry)
+        return (t,n,self)
+    def options(self):
+        return self.list.options(self.entry)
+    def event(self, num):
+        return self.list.event(self.entry, num)
+
+class tasklist_tz(tasklist):
+    # Synthesise a list of tasks to represent selection a time zone
+    # ind==-1 must return the length of the list, other values return tasks
+    # We can call window.set_folder (or something) to get the list refreshed
+    # First item is 'TimeZone' with a button to refresh the list
+    # other items are best 10 timezones.
+    # We refresh the list when the refresh button is pressed, or when
+    # len is requested move than 10 minutes after the last refresh.
+
+    def __init__(self):
+        tasklist.__init__(self)
+        self.refresh_time = 10*60
+        self.name = 'TimeZone'
+
+    def start_refresh(self):
+        self.refresh_cmd("/root/gpstz --list")
+        
+    def readline(self, l):
+        if l == None:
+            return
+        words = l.split()
+        self.newlist.append(words[1])
+
+    def info(self, n):
+        return 'cmd', self.list[n]
+    def options(self, n):
+        return ['Set Timezone']
+    def event(self, n, ev):
+        if ev == 0:
+            Popen("/root/gpstz "+ self.list[n], shell=True, close_fds=True)
+    
+
+
+def internal_refresh_list(arg, obj):
+    if arg == '_name':
+        return "Refresh List"
+    if arg == '_options':
+        return ['Refresh']
+    if arg == 0:
+        t = obj.state['list']
+        t.start_refresh()
+    return None
+
+
+class tasklist_wifi(tasklist):
+    def __init__(self):
+        tasklist.__init__(self)
+        self.refresh_time = 60
+        self.name = 'Wifi Networks'
+
+    def start_refresh(self):
+        self.essid = None
+        self.encrypt = None
+        self.quality = None
+        self.refresh_cmd("iwlist eth0 scanning")
+
+    def readline(self, l):
+        if l == None:
+            self.read_finished()
+            return
+        w = l.split()
+        if len(w) == 0:
+            return
+        if w[0] == 'Cell':
+            self.read_finished()
+            return
+        w0 = w[0]
+        w = l.split(':')
+        if w[0] == "ESSID":
+            id = w[1]
+            self.essid = id.strip('"')
+            return
+        if w[0] == 'Encryption key':
+            self.encrypt = (w[1] == 'on')
+            return
+        w = w0.split('=')
+        if w[0] == 'Quality':
+            self.quality = w[1]
+            return
+
+    def read_finished(self):
+        if self.essid == None:
+            self.encrypt = None
+            self.quality = None
+            return
+        if self.quality == None:
+            self.quality = "0"
+        c = ''
+        if self.encrypt:
+            c = ' XX'
+        self.newlist.append((self.essid, self.quality, c))
+
+    def info(self, n):
+        essid, quality, c = self.list[n]
+        return 'cmd', ('<span size="15000">%s</span>\n<span size="10000">%s%s</span>'
+                       % (essid, quality, c))
+    def options(self, n):
+        return ['Configure Wifi']
+    def event(self, n, ev):
+        print "please configure %s"% self.list[n][0]
+
+def main(args):
+    global window, windowlist, tasks
+    global current_input
+    current_input = ''
+    windowlist = WinList()
+    tasks = Tasks(os.getenv('HOME') + "/.launchrc")
+    i = LaunchIcon()
+    window = LaunchWindow(tasks)
+    try:
+        aux = EvDev("/dev/input/event4", aux_activate)
+        # may aux button broke so ... 
+        EvDev("/dev/input/event0", aux_activate)
+    except:
+        aux = None
+    try:
+        EvDev("/dev/input/event3", tap_check)
+    except:
+        pass
+
+    gtk.settings_get_default().set_long_property("gtk-cursor-blink", 0, "main")
+
+    gtk.main()
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv))
+    
diff --git a/launcher/launch_settings.py b/launcher/launch_settings.py
new file mode 100644 (file)
index 0000000..051a540
--- /dev/null
@@ -0,0 +1,26 @@
+import os, stat
+
+def alert(cmd, obj):
+    if len(obj.state) == 0:
+        try:
+            obj.state['curr'] = os.readlink("/etc/alert/normal")
+        except:
+            obj.state['curr'] = '??'
+        o = []
+        for i in os.listdir("/etc/alert"):
+            if stat.S_ISDIR(os.lstat("/etc/alert/"+i)[0]):
+                o.append(i)
+        obj.state['options'] = o
+
+
+    if cmd == '_name':
+        return ('cmd', 'mode: ' + obj.state['curr'])
+    if cmd == '_options':
+        return obj.state['options']
+    if cmd >= 0 and cmd < len(obj.state['options']):
+        o = obj.state['options'][cmd]
+        os.unlink("/etc/alert/normal")
+        os.symlink(o, "/etc/alert/normal")
+        obj.state['curr'] = o
+
+        
diff --git a/launcher/wmctrl.py b/launcher/wmctrl.py
new file mode 100644 (file)
index 0000000..724343b
--- /dev/null
@@ -0,0 +1,153 @@
+
+#
+# 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, list):
+        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):
+        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.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, self)
+        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]
+
+    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):
+        self.change_handle = func
+        
+
+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/launcher/wpa b/launcher/wpa
new file mode 100644 (file)
index 0000000..d5017fa
--- /dev/null
@@ -0,0 +1,160 @@
+ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=root
+
+network={
+       ssid="JesusIsHere"
+       scan_ssid=1
+       key_mgmt=NONE
+}
+network={
+       ssid="TorchNet"
+       scan_ssid=1
+       key_mgmt=WPA-PSK
+       psk="a1b2c3d4e5"
+}
+
+network={
+       ssid="Sarah Smith's Network"
+       scan_ssid=1
+       key_mgmt=WPA-PSK
+       psk="hellomuffin"
+}
+
+network={
+       ssid="LINUX"
+       scan_ssid=1
+       key_mgmt=NONE
+}
+
+
+2010-11-21 12:36:57 0415836820
+2010-11-25 10:00:01 -call-
+2010-11-25 10:00:01 0298924876
+2010-11-26 12:37:56 -call-
+2010-11-26 12:37:56 0406022084
+2010-11-27 14:09:57 -call-
+2010-11-27 14:09:58 0415836820
+2010-11-27 20:35:13 -call-
+2010-11-27 20:35:13 0415836820
+2010-11-28 11:27:11 -call-
+2010-11-28 11:27:12 0423939119
+2010-11-28 11:48:38 -call-
+2010-11-28 11:48:38 0406022084
+2010-11-28 12:49:14 -call-
+2010-11-28 12:49:14 0415836820
+2010-11-28 12:57:50 -call-
+2010-11-28 12:57:50 0406022084
+2010-11-28 14:01:15 -call-
+2010-11-28 14:01:15 0415836820
+2010-11-28 18:20:18 -call-
+2010-11-28 18:20:18 0296624397
+2010-11-30 21:46:11 -call-
+2010-12-03 13:13:17 -call-
+2010-12-03 13:13:18 0415836820
+2010-12-03 13:18:05 -call-
+2010-12-03 13:18:05 0415836820
+2010-12-03 14:33:38 -call-
+2010-12-03 14:33:38 0415836820
+2010-12-03 17:01:57 -call-
+2010-12-03 17:01:57 0296074529
+2010-12-04 16:52:59 -call-
+2010-12-04 16:52:59 0415836820
+2010-12-05 16:15:15 -call-
+2010-12-05 16:15:15 0415836820
+2010-12-05 16:22:19 -call-
+2010-12-05 16:22:19 0415836820
+2010-12-05 16:25:05 -call-
+2010-12-05 16:25:05 0415836820
+2010-12-05 16:28:06 -call-
+2010-12-05 16:28:06 0415836820
+2010-12-05 21:29:29 -call-
+2010-12-08 16:02:32 -call-
+2010-12-09 18:13:19 -call-
+2010-12-09 18:13:20 0415836820
+2010-12-10 12:46:22 -call-
+2010-12-10 12:46:22 0406022084
+2010-12-10 13:40:01 -call-
+2010-12-10 13:40:01 0406022084
+2010-12-10 13:42:52 -call-
+2010-12-10 13:42:52 0406022084
+2010-12-10 13:55:35 -call-
+2010-12-10 13:55:35 0415836820
+2010-12-10 13:59:09 -call-
+2010-12-10 13:59:09 0415836820
+2010-12-10 14:33:20 -call-
+2010-12-10 14:33:20 0415836820
+2010-12-10 14:36:05 -call-
+2010-12-10 14:36:05 0415836820
+2010-12-10 14:46:15 -call-
+2010-12-10 14:46:15 0415836820
+2010-12-10 15:12:16 -call-
+2010-12-10 15:12:16 0411084748
+2010-12-10 15:14:04 -call-
+2010-12-10 15:14:04 0411084748
+2010-12-10 15:55:42 -call-
+2010-12-10 15:55:42 0415836820
+2010-12-10 16:04:01 -call-
+2010-12-10 16:04:01 0411084748
+2010-12-10 16:13:52 -call-
+2010-12-10 16:13:52 0415836820
+2010-12-11 11:57:01 -call-
+2010-12-11 11:57:01 0415836820
+2010-12-14 07:01:42 -call-
+2010-12-14 07:01:43 0406387449
+2010-12-14 09:47:47 -call-
+2010-12-14 09:47:47 0403204499
+2010-12-14 22:45:06 -call-
+2010-12-14 22:45:06 0406387449
+2010-12-15 20:39:51 -call-
+2010-12-15 20:39:51 0406387449
+2010-12-17 16:24:17 -call-
+2010-12-17 16:24:17 0415836820
+2010-12-17 17:33:08 -call-
+2010-12-17 17:33:09 0415836820
+2010-12-17 17:47:17 -call-
+2010-12-17 17:47:18 0415836820
+2010-12-17 21:15:20 -call-
+2010-12-17 21:15:20 0406387449
+2010-12-18 13:17:59 -call-
+2010-12-18 13:17:59 0415836820
+2010-12-18 14:23:59 -call-
+2010-12-18 14:24:00 0415836820
+2010-12-19 14:47:29 -call-
+2010-12-19 14:47:29 0407628926
+2010-12-19 20:26:35 -call-
+2010-12-20 13:57:00 -call-
+2010-12-20 14:35:15 -call-
+2010-12-20 14:35:15 0415836820
+2010-12-20 15:34:23 -call-
+2010-12-20 15:34:23 0415836820
+2010-12-20 15:44:50 -call-
+2010-12-20 15:44:51 0415836820
+2010-12-20 19:14:27 -call-
+2010-12-20 19:14:27 0415836820
+2010-12-21 11:50:19 -call-
+2010-12-21 11:50:20 0415836820
+2010-12-21 13:03:03 -call-
+2010-12-21 13:03:03 0406022084
+2010-12-24 17:55:45 -call-
+2010-12-24 17:55:46 0415836820
+2010-12-24 18:48:32 -call-
+2010-12-24 18:48:32 0265815991
+2010-12-24 18:49:18 -call-
+2010-12-24 18:49:18 0265815991
+2010-12-25 18:42:04 -call-
+2010-12-25 18:42:04 0398855778
+2010-12-25 19:58:33 -call-
+2010-12-26 20:44:24 -call-
+2010-12-26 20:44:24 0415836820
+2010-12-27 09:52:03 -call-
+2010-12-27 09:52:03 0415836820
+2010-12-27 09:54:30 -call-
+2010-12-27 09:54:30 0415836820
+2010-12-27 09:58:30 -call-
+2010-12-27 09:58:30 0415836820
+2010-12-27 11:46:37 -call-
+2010-12-27 11:46:37 0424041640
+2010-12-29 10:04:46 -call-
+2010-12-29 12:18:36 -call-
+2010-12-29 12:18:36 0415836820
+2010-12-29 12:19:31 -call-
+2010-12-29 12:19:31 0415836820
diff --git a/lib/decode-long-sms.c b/lib/decode-long-sms.c
new file mode 100644 (file)
index 0000000..43f450a
--- /dev/null
@@ -0,0 +1,45 @@
+
+#include <stdio.h>
+main(int argc, char *argv[])
+{
+
+       int pos = 0;
+       char *c;
+       int carry = 0;
+
+       for (c = argv[1]; *c; c+= 2) {
+               int b;
+               char c1, c2;
+               c1 = c[0]; c2 = c[1];
+               if (c1 > '9')
+                       c1 = 10 + (c1-'A');
+               else
+                       c1 = c1 - '0';
+
+               if (c2 > '9')
+                       c2 = 10 + (c2-'A');
+               else
+                       c2 = c2 - '0';
+
+               b = c1*16 + c2;
+
+               if (pos == 0) {
+                       if (carry) {
+                               printf("%c", carry + ((b&1) << 6));
+                               carry = 0;
+                       }
+                       b = b >> 1;
+               } else {
+                       b = (b << (pos-1)) | carry;
+                       carry = (b & 0xff80) >> 7;
+                       b &=  0x7f;
+               }
+               printf("%c", b);
+               pos++;
+               if (pos == 7)
+                       pos = 0;
+       }
+       printf("\n");
+       exit(0);
+}
+
diff --git a/lib/decode-long-sms.py b/lib/decode-long-sms.py
new file mode 100644 (file)
index 0000000..a998744
--- /dev/null
@@ -0,0 +1,25 @@
+
+def sms_decode(msg):
+    pos = 0
+    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
+        str += chr(b&0x7f)
+        pos = (pos+1) % 7
+    return str
+
+import sys
+print sms_decode(sys.argv[1])
diff --git a/lib/play.py b/lib/play.py
new file mode 100644 (file)
index 0000000..2c564dd
--- /dev/null
@@ -0,0 +1,133 @@
+
+# Python library to play sounds using ALSA from inside a
+# glib event loop
+#
+# We use the 'non-blocking' output routines, write until
+# we can write no more, or we hit the stop-latency limit.
+# Then set a timer to try again when we estimate the buffer
+# will be 3/4 full.
+#
+# playing can be interrupted at any time - we allow the buffers
+# to flush.
+#
+# Currently only wav files
+
+import gobject, alsaaudio, time, struct, sys
+
+class Play():
+    def __init__(self, file, latency_ms = 1000, done = None):
+        # Arrange to play 'file' - which is the name of a .wav file
+        self.pcm = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, alsaaudio.PCM_NONBLOCK)
+        self.latency_ms = latency_ms
+        self.finished = False
+        self.done = done
+        self.setfile(file)
+
+    def setfile(self, file):
+        # A wav file starts:
+        #   0-3  "RIFF"
+        #   4-7  Bytes in rest of file.
+        #   8-11 "WAVE"
+        #  12-15 "fmt "
+        #  16-19 bytes of format
+        #  20-21 ==1  Microsoft PCM
+        #  22-23      channels
+        #  24-27  freq
+        #  28-31  byte rate
+        #  32-33  bytes per frame
+        #  34-35  bits per sample
+        #  36-39 "data"
+        #  40-43 number of bytes of data
+        #  44... actual samples
+        self.f = open(file)
+        header = self.f.read(44)
+        if len(header) != 44:
+            raise IOError
+        riff, b1, wave, fmt, b2, format, chan, rate, br, bf, bs, data, b3 = \
+              struct.unpack("4si4s 4sihhiihh 4si", header)
+
+        if riff != "RIFF" or wave != "WAVE" or fmt != "fmt " or data != "data":
+            raise ValueError
+        if format != 1 or bs != 16:
+            raise ValueError
+        else:
+            self.pcm.setformat(alsaaudio.PCM_FORMAT_S16_LE)
+
+        if chan < 1 or chan > 4:
+            raise ValueError
+        else:
+            self.pcm.setchannels(chan)
+
+        self.pcm.setrate(rate)
+        self.bytes_per_second = rate * 2 * chan
+
+        # choose the period to be 1/8 of the latency,
+        # probably need to set an upper bound
+        frames_per_latency = rate * self.latency_ms / 1000
+        self.bytes_per_latency = frames_per_latency * chan * 2;
+        #self.bytes_per_period = (frames_per_latency / 8) * chan * 2
+        self.bytes_per_period = 320
+
+        self.data = None
+        
+        self.pcm.setperiodsize(self.bytes_per_period / chan / 2)
+        #print "bytes_per_period", self.bytes_per_period
+        #print "period size", self.bytes_per_period / chan / 2
+
+        self.start = time.time()
+        self.loaded = 0
+        self.finished = False
+        self.playsome()
+
+    def playsome(self):
+        if self.finished:
+            return
+        now = time.time()
+
+        self.now = now
+        pos = int( (time.time() - self.start) * self.bytes_per_second)
+        buffered = self.loaded - pos
+        cnt = 0
+        data = self.data
+        while buffered < self.bytes_per_latency + self.bytes_per_period:
+            if not data:
+                data = self.f.read(self.bytes_per_period)
+            if not data:
+                self.finished = True
+                self.data = None
+                if self.done:
+                    self.done()
+                return
+            if not self.pcm.write(data):
+                break
+            data = None
+
+            cnt += 1
+            buffered += self.bytes_per_period
+
+        self.data = data
+        self.loaded = buffered + pos
+
+        pos = int( (time.time() - self.start) * self.bytes_per_second)
+        buffered = self.loaded - pos
+        delay = int(buffered /4 * 1000 / self.bytes_per_second)
+        print "wrote", cnt, "delay" ,delay
+        if delay < 20:
+            self.start += float( 20 - delay) / 1000
+            delay = 10
+        gobject.timeout_add(delay, self.playsome, priority = gobject.PRIORITY_HIGH)
+
+
+if __name__ == "__main__":
+    # test code.
+    # play given wav file in a loop for 20 seconds, then stop
+    p = None
+    def done():
+        p.setfile(sys.argv[1])
+    p = Play(sys.argv[1], 400, done)
+    c = gobject.main_context_default()
+    def abort():
+        p.finished = True
+    gobject.timeout_add(20000, abort)
+    while not p.finished:
+        c.iteration()
diff --git a/lib/pyfakekey.c b/lib/pyfakekey.c
new file mode 100644 (file)
index 0000000..db662da
--- /dev/null
@@ -0,0 +1,20 @@
+#include <Python.h>
+#include <fakekey/fakekey.h>
+
+
+
+static PyMethodDef FakekeyMethods[] = {
+    ...
+    {"fakekey", fakekey_class, METH_VARARGS,
+     "Send synthesised key events to an X client"},
+    ...
+    {NULL, NULL, 0, NULL}        /* Sentinel */
+};
+
+
+PyMODINIT_FUNC
+initfakekey(void)
+{
+    (void) Py_InitModule("fakekey", FakekeyMethods);
+}
+
diff --git a/lib/tapinput.py b/lib/tapinput.py
new file mode 100644 (file)
index 0000000..2b756a3
--- /dev/null
@@ -0,0 +1,347 @@
+
+#
+# experiment with tap input.
+# Have a 3x4 array of buttons.
+# Enter any symbol by tapping two buttons from the top 3x3
+# Bottom buttons are:  mode  cancel/delete  enter
+# mode cycles : upper lower symbol
+# cancel is effective after a single tap, delete when no pending tap
+#
+# The 3x3 keys normally show a 3x3 matrix of what they enable
+# When one is tapped, all keys change to show a single image.
+
+import gtk, pango, gobject
+
+keymap = {}
+
+# 4 in each corner, 6 on the sides plus 9 in the middle is 49.
+# 26 + 10 leaves 13  for symbols
+# 9 most common in middle leaves 15 in the corners (with .)
+# digits with + - on two sides, symbols on other two
+# e t a o i n s r h l d c u m f p g w y b v k x j q z
+# from http://www.deafandblind.com/word_frequency.htm
+keymap['lower'] = [
+    '01 23 ?@#',
+    'bcdfgh   ',
+    '<45>67 {}',
+    'jk~lm`np ',
+    'aeio urst',
+    '=;:\\\'"|()',
+    '[] 89 +-_',
+    '   qvwxyz',
+    '!$%^*/&,.'
+    ]
+keymap['UPPER'] = [
+    '01 23 ?@#',
+    'BCDFGH   ',
+    '<45>67 {}',
+    'JK~LM`NP ',
+    'AEIO URST',
+    '=;:\\\'"|()',
+    '[] 89 +-_',
+    '   QVWXYZ',
+    '!$%^*/&,.'
+    ]
+keymap['number'] = [
+    '1        ',
+    ' 2       ',
+    '  3      ',
+    '   4     ',
+    '    5 *0#',
+    '     6   ',
+    '      7  ',
+    '       8 ',
+    '        9'
+    ]
+
+
+class X(gtk.Window):
+    def __init__(self):
+        gtk.Window.__init__(self, type=gtk.WINDOW_POPUP)
+        self.set_default_size(320, 420)
+        root = gtk.gdk.get_default_root_window()
+        (x,y,width,height,depth) = root.get_geometry()
+        x = int((width-320)/2)
+        y = int((height-420)/2)
+        self.move(x,y)
+
+        self.dragx = None
+        self.dragy = None
+        self.moved = False
+
+        self.button_timeout = None
+
+        self.buttons = []
+        v1 = gtk.VBox()
+        v1.show()
+        self.add(v1)
+
+        self.entry = gtk.Entry()
+        self.entry.show()
+        v1.pack_start(self.entry, expand=False)
+
+
+        v = gtk.VBox()
+        v.show()
+        v1.add(v)
+        v.set_homogeneous(True)
+
+        for row in range(3):
+            h = gtk.HBox()
+            h.show()
+            h.set_homogeneous(True)
+            v.add(h)
+            bl = []
+            for col in range(3):
+                #b = gtk.Button("%d/%d" %(row, col))
+                b = gtk.Button()
+                b.show()
+                b.connect('button_press_event', self.press)
+                b.connect('button_release_event', self.release, row, col)
+                b.connect('motion_notify_event', self.motion)
+                b.add_events(gtk.gdk.POINTER_MOTION_MASK|
+                             gtk.gdk.POINTER_MOTION_HINT_MASK)
+                
+                h.add(b)
+                bl.append(b)
+            self.buttons.append(bl)
+
+
+        h = gtk.HBox()
+        h.show()
+        h.set_homogeneous(True)
+        v.add(h)
+
+        b = gtk.Button('mode')
+        fd = pango.FontDescription('sans 10')
+        fd.set_absolute_size(30 * pango.SCALE)
+        b.child.modify_font(fd)
+        b.show()
+        b.connect('clicked', self.nextmode)
+        h.add(b)
+        self.modebutton = b
+
+        b = gtk.Button(stock=gtk.STOCK_UNDO)
+        b.show()
+        b.connect('clicked', self.delete)
+        h.add(b)
+
+        b = gtk.Button(stock=gtk.STOCK_OK)
+        b.show()
+        b.connect('clicked', self.enter)
+        h.add(b)
+
+        self.show()
+        self.mode = 'lower'
+        self.single = False
+        self.prefix = None
+        self.size = 0
+        self.update_buttons()
+        self.connect("configure-event", self.update_buttons)
+
+    def update_buttons(self, *a):
+        alloc = self.buttons[0][0].get_allocation()
+        w = alloc.width; h = alloc.height
+        if w > h:
+            size = h
+        else:
+            size = w
+        size -= 12
+        if size <= 10 or size == self.size:
+            return
+        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
+        fd = pango.FontDescription('sans 10')
+        fd.set_absolute_size(size / 4.5 * pango.SCALE)
+        self.modify_font(fd)
+
+        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')))
+        base_images = {}
+        for mode in keymap.keys():
+            base_images[mode] = 9*[None]
+            for row in range(3):
+                for col in range(3):
+                    syms = keymap[mode][row*3+col]
+                    pm = gtk.gdk.Pixmap(self.window, size, size)
+                    pm.draw_rectangle(bg, True, 0, 0, size, size)
+                    for r in range(3):
+                        for c in range(3):
+                            sym = syms[r*3+c]
+                            if sym == ' ':
+                                continue
+                            xpos = ((c-col+1)*2+1)
+                            ypos = ((r-row+1)*2+1)
+                            colour = fg
+                            if xpos != xpos%6:
+                                xpos = xpos%6
+                                colour = red
+                            if ypos != ypos%6:
+                                ypos = ypos%6
+                                colour = red
+                            layout = self.create_pango_layout(sym)
+                            (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+                            pm.draw_layout(colour,
+                                           int(xpos*size/6 - ew/2),
+                                           int(ypos*size/6 - eh/2),
+                                           layout)
+                    im = gtk.Image()
+                    im.set_from_pixmap(pm, None)
+                    base_images[mode][row*3+col] = im
+        self.base_images = base_images
+        fd.set_absolute_size(size / 1.5 * pango.SCALE)
+        self.modify_font(fd)
+        sup_images = {}
+        for mode in keymap.keys():
+            sup_images[mode] = 9*[None]
+            for row in range(3):
+                for col in range(3):
+                    ilist = 9 * [None]
+                    for r in range(3):
+                        for c in range(3):
+                            sym = keymap[mode][r*3+c][row*3+col]
+                            if sym == ' ':
+                                continue
+                            pm = gtk.gdk.Pixmap(self.window, size, size)
+                            pm.draw_rectangle(bg, True, 0, 0, size, size)
+                            layout = self.create_pango_layout(sym)
+                            (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+                            pm.draw_layout(fg,
+                                           int((size - ew)/2), int((size - eh)/2),
+                                           layout)
+                            im = gtk.Image()
+                            im.set_from_pixmap(pm, None)
+                            ilist[r*3+c] = im
+                    sup_images[mode][row*3+col] = ilist
+        self.sup_images = sup_images
+        self.set_button_images()
+
+    
+    def set_button_images(self):
+        for row in range(3):
+            for col in range(3):
+                b = self.buttons[row][col]
+                if self.prefix == None:
+                    im = self.base_images[self.mode][row*3+col]
+                else:
+                    im = self.sup_images[self.mode][row*3+col][self.prefix]
+                if im:
+                    b.set_image(im)
+        
+
+    def tap(self, widget, ev, row, col):
+        if row == 3:
+            self.update_buttons()
+            self.set_button_images()
+            return
+            
+        if self.prefix == None:
+            self.prefix = row*3 + col
+            self.button_timeout = gobject.timeout_add(500, self.do_buttons)
+        else:
+            sym = keymap[self.mode][self.prefix][row*3+col]
+            self.entry.emit("insert-at-cursor", sym)
+            self.noprefix()
+
+    def press(self, widget, ev):
+        self.dragx = int(ev.x_root)
+        self.dragy = int(ev.y_root)
+        self.startx, self.starty  = self.get_position()
+
+    def release(self, widget, ev, row, col):
+        self.dragx = None
+        self.dragy = None
+        if self.moved:
+            self.moved = False
+        else:
+            self.tap(widget, ev, row, col)
+    def motion(self, widget, ev):
+        if self.dragx == None:
+            return
+        x = int(ev.x_root)
+        y = int(ev.y_root)
+
+        if abs(x-self.dragx)+abs(y-self.dragy) > 40 or self.moved:
+            self.move(self.startx+x-self.dragx,
+                      self.starty+y-self.dragy);
+            self.moved = True
+        if ev.is_hint:
+            gtk.gdk.flush()
+            ev.window.get_pointer()
+
+
+    def do_buttons(self):
+        self.set_button_images()
+        self.button_timeout = None
+        return False
+
+
+    def nextmode(self, w):
+        if self.prefix:
+            return self.noprefix()
+        if self.mode == 'lower':
+            self.mode = 'UPPER'
+            self.single = True
+            w.child.set_text('Mode')
+        elif self.mode == 'UPPER' and self.single:
+            self.single = False
+            w.child.set_text('MODE')
+        elif self.mode == 'UPPER' and not self.single:
+            self.mode = 'number'
+            w.child.set_text('123')
+        else:
+            self.mode = 'lower'
+            w.child.set_text('mode')
+        self.set_button_images()
+
+    def delete(self, w):
+        if self.prefix == None:
+            self.entry.emit("backspace")
+        else:
+            self.noprefix()
+
+    def noprefix(self):
+        self.prefix = None
+        
+        if self.button_timeout:
+            gobject.source_remove(self.button_timeout)
+            self.button_timeout = None
+        else:
+            self.set_button_images()
+
+        if self.single:
+            self.mode = 'lower'
+            self.single = False
+            self.modebutton.child.set_text('mode')
+            self.set_button_images()
+
+    def enter(self, w):
+        if self.prefix == None:
+            text = self.entry.get_text()
+            print "Answer is", text
+            self.entry.set_text('')
+            root = gtk.gdk.get_default_root_window()
+            app = root.property_get('_MB_CURRENT_APP_WINDOW')
+            if app and app[0] == 'WINDOW':
+                try:
+                    appw = gtk.gdk.window_foreign_new(app[2][0])
+                    appw.property_change('_INPUT_TEXT', 'STRING', 8,
+                                         gtk.gdk.PROP_MODE_REPLACE, text)
+                except:
+                    pass
+            gtk.main_quit()
+        else:
+            self.noprefix()
+
+    
+
+x = X()
+
+gtk.main()
+
diff --git a/mickeyterm/mickeyterm.py b/mickeyterm/mickeyterm.py
new file mode 100755 (executable)
index 0000000..2ece6d7
--- /dev/null
@@ -0,0 +1,532 @@
+#!/usr/bin/env python
+"""
+Mickey's own serial terminal. Based on miniterm.py.
+
+Additional Features:
+    * readline support with command completion and history
+    * org.freesmartphone.GSM.MUX support
+    * log to file
+
+(C) 2002-2006 Chris Liechti <cliecht@gmx.net>
+(C) 2008 Michael 'Mickey' Lauer <mlauer@vanille-media.de>
+
+GPLv2 or later
+"""
+
+__version__ = "2.9.1"
+
+import sys, os, serial, threading, termios
+
+def completer( text, state ):
+    """Return a possible readline completion"""
+    if state == 0:
+        line =""
+        #line = readline.get_line_buffer()
+        if " " in line:
+            allmatches = [ "(No Matches Available for commands.)" ]
+        else:
+            if not hasattr( completer, "commands" ):
+                allmatches = [ "(No matches available yet. Did AT+CLAC yet?)" ]
+            else:
+                allmatches = completer.commands
+
+        completer.matches = [ x for x in allmatches if x[:len(text)] == text ]
+    if len( completer.matches ) > state:
+        return completer.matches[state]
+    else:
+        return None
+
+commands = """
+AT+CACM
+AT+CAMM
+AT+CAOC
+AT+CBC
+AT+CBST
+AT+CCFC
+AT+CCUG
+AT+CCWA
+AT+CCWE
+AT+CEER
+AT+CFUN
+AT+CGACT
+AT+CGANS
+AT+CGATT
+AT+CGAUTO
+AT+CGCLASS
+AT+CGDATA
+AT+CGDCONT
+AT+CGEREP
+AT+CGMI
+AT+CGMM
+AT+CGMR
+AT+CGPADDR
+AT+CGQMIN
+AT+CGQREQ
+AT+CGREG
+AT+CGSMS
+AT+CGSN
+AT+CHLD
+AT+CHUP
+AT+CIMI
+AT+CLAC
+AT+CLAE
+AT+CLAN
+AT+CLCC
+AT+CLCK
+AT+CLIP
+AT+CDIP
+AT+CLIR
+AT+CLVL
+AT+CMEE
+AT+CMGC
+AT+CMGD
+AT+CMGF
+AT+CMGL
+AT+CMGR
+AT+CMGS
+AT+CMGW
+AT+CMOD
+AT+CMSS
+AT+CMMS
+AT+CMUT
+AT+CMUX
+AT+CNMA
+AT+CNMI
+AT+CNUM
+AT+COLP
+AT+COPN
+AT+COPS
+AT+CPAS
+AT+CPBF
+AT+CPBR
+AT+CPBS
+AT+CPBW
+AT+CPIN
+AT+CPMS
+AT+CPOL
+AT+CPUC
+AT+CPWD
+AT+CR
+AT+CRC
+AT+CREG
+AT+CRES
+AT+CRLP
+AT+CRSL
+AT+CRSM
+AT+CSAS
+AT+CSCA
+AT+CSCB
+AT+CSCS
+AT+CSDH
+AT+CSIM
+AT+CSMP
+AT+CSMS
+AT+CSNS
+AT+CSQ
+AT%CSQ
+AT+CSSN
+AT+CSTA
+AT+CSVM
+AT+CTFR
+AT+CUSD
+AT+DR
+AT+FAP
+AT+FBO
+AT+FBS
+AT+FBU
+AT+FCC
+AT+FCLASS
+AT+FCQ
+AT+FCR
+AT+FCS
+AT+FCT
+AT+FDR
+AT+FDT
+AT+FEA
+AT+FFC
+AT+FHS
+AT+FIE
+AT+FIP
+AT+FIS
+AT+FIT
+AT+FKS
+AT+FLI
+AT+FLO
+AT+FLP
+AT+FMI
+AT+FMM
+AT+FMR
+AT+FMS
+AT+FND
+AT+FNR
+AT+FNS
+AT+FPA
+AT+FPI
+AT+FPS
+AT+FPW
+AT+FRQ
+AT+FSA
+AT+FSP
+AT+GCAP
+AT+GCI
+AT+GMI
+AT+GMM
+AT+GMR
+AT+GSN
+AT+ICF
+AT+IFC
+AT+ILRR
+AT+IPR
+AT+VTS
+AT+WS46
+AT%ALS
+AT%ATR
+AT%BAND
+AT%CACM
+AT%CAOC
+AT%CCBS
+AT%STDR
+AT%CGAATT
+AT%CGMM
+AT%CGREG
+AT%CNAP
+AT%CPI
+AT%COLR
+AT%CPRIM
+AT%CTV
+AT%CUNS
+AT%NRG
+AT%SATC
+AT%SATE
+AT%SATR
+AT%SATT
+AT%SNCNT
+AT%VER
+AT%CGCLASS
+AT%CGPCO
+AT%CGPPP
+AT%EM
+AT%EMET
+AT%EMETS
+AT%CBHZ
+AT%CPHS
+AT%CPNUMS
+AT%CPALS
+AT%CPVWI
+AT%CPOPN
+AT%CPCFU
+AT%CPINF
+AT%CPMB
+AT%CPRI
+AT%DATA
+AT%DINF
+AT%CLCC
+AT%DBGINFO
+AT%VTS
+AT%CHPL
+AT%CREG
+AT+CTZR
+AT+CTZU
+AT%CTZV
+AT%CNIV
+AT%PVRF
+AT%CWUP
+AT%DAR
+AT+CIND
+AT+CMER
+AT%CSCN
+AT%RDL
+AT%RDLB
+AT%CSTAT
+AT%CPRSM
+AT%CHLD
+AT%SIMIND
+AT%SECP
+AT%SECS
+AT%CSSN
+AT+CCLK
+AT%CSSD
+AT%COPS
+AT%CPMBW
+AT%CUST
+AT%SATCC
+AT%COPN
+AT%CGEREP
+AT%CUSCFG
+AT%CUSDR
+AT%CPBS
+AT%PBCF
+AT%SIMEF
+AT%EFRSLT
+AT%CMGMDU
+AT%CMGL
+AT%CMGR
+AT@ST
+AT@AUL
+AT@POFF
+AT@RST
+AT@SC
+AT@BAND
+ATA
+ATB
+AT&C
+ATD
+AT&D
+ATE
+ATF
+AT&F
+ATH
+ATI
+AT&K
+ATL
+ATM
+ATO
+ATP
+ATQ
+ATS
+ATT
+ATV
+ATW
+AT&W
+ATX
+ATZ
+""".strip()
+completer.commands = commands.split() + commands.lower().split()
+
+class Terminal( object ):
+    def __init__( self, port, baudrate, rtscts, xonxoff, lineending, inputmode=True ):
+        self.inputmode = inputmode
+        self.r = None
+        self.convert = lineending
+        self.EXITCHARACTER = '\x04' # ctrl+D
+        self.fd = None
+        self.serial = serial.Serial( port, baudrate, rtscts=rtscts, xonxoff=xonxoff )
+
+    def setQuietMode( self, quiet ):
+        self.quiet = quiet
+
+    def setLogging( self, logging ):
+        self.logging = logging
+        if self.logging is not None:
+            self.ilog = open( "%s/mickeyterm.%d.input" % ( logging, os.getpid() ), "w" )
+            self.olog = open( "%s/mickeyterm.%d.output" % ( logging, os.getpid() ), "w" )
+            self.alog = open( "%s/mickeyterm.%d.all" % ( logging, os.getpid() ), "w" )
+
+    def run( self ):
+        self.prepare()
+        self.serial.open()
+        assert self.serial.isOpen(), "can't open serial port"
+        self.banner( True )
+        self.r = threading.Thread( target = self.reader )
+        self.r.setDaemon( True )
+        self.r.start()
+        # optional
+        self.serial.write( "AT+CMEE=2;+CRC=1\r\n" )
+        self.writer()
+        self.banner( False )
+        self.serial.close()
+        self.restore()
+
+    def banner( self, startup ):
+        if self.quiet:
+            return
+        if startup:
+            print "<----------- Mickey's Term V%s @ %s ----------->" % ( __version__, self.serial.port )
+        else:
+            print "Good Bye."
+
+    def prepare( self ):
+        if self.inputmode:
+            import readline
+            readline.set_completer( completer )
+            readline.set_completer_delims( " " )
+            readline.parse_and_bind("tab: complete")
+            self.historyfilename = os.path.expanduser( "~/.mickeyterm_history" )
+            try:
+                readline.read_history_file( self.historyfilename )
+                print "read history from", self.historyfilename
+            except IOError:
+                readline.clear_history()
+
+        else:
+            self.fd = sys.stdin.fileno()
+            self.old = termios.tcgetattr( self.fd )
+            new = termios.tcgetattr( self.fd )
+            new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG
+            new[6][termios.VMIN] = 1
+            new[6][termios.VTIME] = 0
+            termios.tcsetattr( self.fd, termios.TCSANOW, new )
+
+    def restore( self ):
+        if self.inputmode:
+            import readline
+            try:
+                readline.write_history_file( self.historyfilename )
+            except IOError, e:
+                print "Could not save history.", repr(e)
+        else:
+            termios.tcsetattr( self.fd, termios.TCSAFLUSH, self.old )
+
+    def writer( self ):
+        if self.inputmode:
+            #
+            # new style
+            #
+            while True:
+                try:
+                    cmdline = raw_input( "" )
+                except KeyboardInterrupt:
+                    print "CTRL-C"
+                    continue
+                except EOFError:
+                    print "CTRL-D"
+                    break
+                else:
+                    if self.convert == "CRLF":
+                        cmdline += "\r\n"
+                    elif self.convert == "CR":
+                        cmdline += "\r"
+                    elif self.convert == "LF":
+                        cmdline += "\n"
+                    self.serial.write( cmdline )
+                    if self.logging:
+                        self.ilog.write( cmdline )
+                        self.alog.write( cmdline )
+        else:
+            #
+            # old style
+            #
+            while True:
+                c = os.read( self.fd, 1 )
+                if c == self.EXITCHARACTER:
+                    break
+                elif c == '\n':
+                    if self.convert == "CRLF":
+                        self.serial.write('\r\n')
+                    elif self.convert == "CR":
+                        self.serial.write('\r')
+                    elif self.convert == "LF":
+                        self.serial.write('\n')
+                else:
+                    self.serial.write(c)
+                    if self.logging:
+                        self.ilog.write( c )
+                        self.alog.write( c )
+
+    def reader( self ):
+        while True:
+            data = self.serial.read()
+            sys.stdout.write(data)
+            sys.stdout.flush()
+            if self.logging:
+                self.olog.write( data )
+                self.alog.write( data )
+
+
+if __name__ == "__main__":
+    import optparse
+
+    parser = optparse.OptionParser(usage="""\
+%prog [options] [port [baudrate]]
+
+Mickey's Terminal Program.""")
+
+    parser.add_option("-p", "--port", dest="port",
+        help="the port, device path, a portnumber, device name (deprecated option), or MUX (default)",
+        default="MUX")
+
+    parser.add_option("-b", "--baud", dest="baudrate", action="store", type='int',
+        help="set baudrate, default 115200", default=115200)
+
+    parser.add_option("", "--parity", dest="parity", action="store",
+        help="set parity, one of [N, E, O], default=N", default='N')
+
+    if False:
+        parser.add_option("-e", "--echo", dest="echo", action="store_true",
+            help="enable local echo (default off)", default=False)
+
+    parser.add_option("", "--rtscts", dest="rtscts", action="store_true",
+        help="enable RTS/CTS flow control (default off)", default=False)
+
+    parser.add_option("", "--xonxoff", dest="xonxoff", action="store_true",
+        help="enable software flow control (default off)", default=False)
+
+    parser.add_option("", "--cr", dest="cr", action="store_true",
+        help="do not send CR+LF, send CR only", default=False)
+
+    parser.add_option("", "--lf", dest="lf", action="store_true",
+        help="do not send CR+LF, send LF only", default=False)
+
+    if False:
+        parser.add_option("-D", "--debug", dest="repr_mode", action="count",
+            help="""debug received data (escape non-printable chars)
+    --debug can be given multiple times:
+    0: just print what is received
+    1: escape non-printable characters, do newlines as ususal
+    2: escape non-printable characters, newlines too
+    3: hex dump everything""", default=0)
+
+    parser.add_option("", "--rts", dest="rts_state", action="store", type='int',
+        help="set initial RTS line state (possible values: 0, 1)", default=None)
+
+    parser.add_option("", "--dtr", dest="dtr_state", action="store", type='int',
+        help="set initial DTR line state (possible values: 0, 1)", default=None)
+
+    # behaviour
+
+    parser.add_option("-c", "--char-by-char", dest="charbychar", action="store_true",
+        help="use character-by-character (traditional mode) instead of line-by-line (default)",
+        default=False)
+
+    parser.add_option("-l", "--logdir", dest="log",
+        help="enable logging to files, specifies directory" )
+
+    parser.add_option("-q", "--quiet", dest="quiet", action="store_true",
+        help="suppress non error messages", default=False)
+
+    options, args = parser.parse_args()
+
+    if options.cr and options.lf:
+        parser.error("only one of --cr or --lf can be specified")
+    else:
+        if options.cr:
+            lineending = "CR"
+        elif options.lf:
+            lineending = "LF"
+        else:
+            lineending = "CRLF"
+
+    port = options.port
+    baudrate = options.baudrate
+    if args:
+        if options.port is not None:
+            parser.error("no arguments are allowed, options only when --port is given")
+        port = args.pop(0)
+        if args:
+            try:
+                baudrate = int(args[0])
+            except ValueError:
+                parser.error("baudrate must be a number, not %r" % args[0])
+            args.pop(0)
+        if args:
+            parser.error("too many arguments")
+    else:
+        if port is "MUX":
+            # try to get portname from MUXer
+            import dbus
+            bus = dbus.SystemBus()
+            oMuxer = bus.get_object( "org.pyneo.muxer", "/org/pyneo/Muxer" )
+            iMuxer = dbus.Interface( oMuxer, "org.freesmartphone.GSM.MUX" )
+            port = iMuxer.AllocChannel( "mickeyterm.%d" % os.getpid() )
+            assert port, "could not get path from muxer. need to supply explicit portname"
+
+    if options.log is not None:
+        if not os.path.isdir( options.log ):
+            parser.error("%s not a directory")
+
+    inputmode = not options.charbychar
+
+    t = Terminal( str(port), baudrate, options.rtscts, options.xonxoff, lineending, inputmode )
+    t.setQuietMode( options.quiet )
+    t.setLogging( options.log )
+    t.run()
diff --git a/music/music.py b/music/music.py
new file mode 100755 (executable)
index 0000000..0882b2c
--- /dev/null
@@ -0,0 +1,852 @@
+#!/usr/bin/env python
+# -*- Mode: Python -*-
+# vi:si:et:sw=4:sts=4:ts=4
+
+import pygtk
+pygtk.require('2.0')
+
+import sys
+
+import gobject
+
+import suspend
+
+import pygst
+pygst.require('0.10')
+import gst
+import gst.interfaces
+import gtk
+
+import urllib
+import os
+import random
+import pango
+
+class MusicList:
+    # Allows selecting songs and moving through the list.
+    # movement can be
+    #  sequential (alpha order)
+    #  random-album (Seq through album, then random next album)
+    #  random  (random walk through all)
+    #
+    # We store two states.
+    # 1/ The current song to play.  This is in 'album' and 'song' and mode etc.
+    # 2/ The browse location, in 'dir' and 'pos'
+    def __init__(self, path):
+        self.albums = {}
+        self.dirs = {}
+        self.names = {}
+        self.add_path(path)
+        self.on_change = []
+        for a in self.albums:
+            self.albums[a].sort()
+        for p in self.dirs:
+            self.dirs[p].sort()
+
+        self.albumlist = self.albums.keys()
+        self.albumlist.sort()
+
+        self.path = path
+        self.dir = "/"
+        self.pos = 0
+        
+        self.album = None
+        self.song = None
+        self.mode = 'seq'
+
+        self.set_dir("")
+
+    def set_dir(self, dir):
+        self.dir = dir
+        p = self.path + self.dir
+        print "p is", p
+        self.folders = []
+        if p in self.dirs:
+            self.folders = self.dirs[p]
+        self.songs = []
+        if p in self.albums:
+            self.songs = self.albums[p]
+        self.pos = 0
+
+    def set_song(self, song):
+        # this song is in the current dir,
+        # update folder and song, and play
+        print "dir is", self.dir
+        print self.albumlist
+        print "xx"
+        self.album = self.albumlist.index(self.path+self.dir)
+        self.song = self.albums[self.path+self.dir].index(song)
+        print "set song", self.album, self.song
+        self.changed()
+
+    def namecnt(self):
+        cnt = 0
+        if self.dir != "":
+            cnt = 1
+        cnt += len(self.folders)
+        cnt += len(self.songs)
+        return cnt
+
+    def getname(self, num):
+        if self.dir != "":
+            if num == 0:
+                return ("parent", "<parent>")
+            num -= 1
+        if num < len(self.folders):
+            return ("folder", self.folders[num])
+        num -= len(self.folders)
+        if num < len(self.songs):
+            return ("song", self.songs[num])
+        return ("end", "unknown")
+
+    def changer(self, func):
+        self.on_change.append(func)
+
+    def changed(self):
+        for func in self.on_change:
+            func()
+
+    def next(self):
+        # move to the next song
+        if self.mode == 'seq':
+            # just return the sequentially next song
+            while True:
+                if self.album == None:
+                    self.album = 0
+                    self.song = 0
+                elif self.song == None:
+                    self.song = 0
+                else:
+                    self.song += 1
+                if len(self.albumlist) == 0:
+                    break
+                if self.song < len(self.albums[self.albumlist[self.album]]):
+                    break
+                self.album += 1
+                self.song = None
+                if self.album >= len(self.albumlist):
+                    self.album = None
+                    return False
+
+            self.changed()
+            return True
+
+    def curr_song(self):
+        a = self.albumlist[self.album]
+        s = self.albums[a][self.song]
+        t = self.names[s]
+        return (a,s,t)
+
+    def prev(self):
+        # return the 'previous' song as (path,album,name)
+        if self.mode == 'seq':
+            # just return the sequentially previous song
+            while True:
+                if self.album == None:
+                    self.album = len(self.albumlist)-1
+                    self.song = len(self.albums[self.albumlist[self.album]])-1
+                elif self.song == None:
+                    self.song = len(self.albums[self.albumlist[self.album]])-1
+                else:
+                    self.song -= 1
+                if self.song >= 0:
+                    break
+                self.album -= 1
+                self.song = None
+                if self.album < 0:
+                    self.album = None
+                    return False
+            self.changed()
+            return True
+
+
+    def add_path(self, path):
+        try:
+            n = os.listdir(path)
+        except:
+            return
+        else:
+            pass
+        for f in n:
+            p = os.path.join(path,f)
+            if os.path.isdir(p):
+                self.add_path(p)
+            if os.path.isfile(p) and p[-4:] == ".ogg":
+                self.addsong(path, f)
+
+    def addsong(self, path, name):
+        if not path in self.albums:
+            self.albums[path] = []
+            self.add_dir(path)
+        p = path
+        n = name
+        while p and p != "/":
+            (p, b) = os.path.split(p)
+            n = n.replace(("- %s -"%b), "-")
+        if n[-4:] == ".ogg":
+            n = n[:-4]
+        self.albums[path].append(name)
+        self.names[name] = urllib.unquote_plus(n)
+
+    def add_dir(self, path):
+        (h,t) = os.path.split(path)
+        if not h in self.dirs:
+            self.dirs[h] = []
+            self.add_dir(h)
+        self.dirs[h].append(t)
+
+    def title(self, song = None):
+        if song != None:
+            return self.names[song]
+        if self.album == None or self.song == None:
+            return "Nothing Playing"
+        return self.names[self.albums[self.albumlist[self.album]][self.song]]
+
+class GstPlayer:
+    def __init__(self):
+        self.playing = False
+        self.player = gst.element_factory_make("playbin", "player")
+        self.on_eos = False
+
+        bus = self.player.get_bus()
+        bus.enable_sync_message_emission()
+        bus.add_signal_watch()
+        bus.connect('message', self.on_message)
+
+    def on_message(self, bus, message):
+        t = message.type
+        if t == gst.MESSAGE_ERROR:
+            err, debug = message.parse_error()
+            print "Error: %s" % err, debug
+            self.playing = False
+            if self.on_eos:
+                self.on_eos()
+        elif t == gst.MESSAGE_EOS:
+            self.playing = False
+            if self.on_eos:
+                self.on_eos()
+
+    def set_location(self, location):
+        self.player.set_property('uri', location)
+
+    def set_volume(self, volume):
+        self.player.set_property('volume', volume / 100.0)
+
+    def query_position(self):
+        "Returns a (position, duration) tuple"
+        try:
+            position, format = self.player.query_position(gst.FORMAT_TIME)
+        except:
+            position = gst.CLOCK_TIME_NONE
+
+        try:
+            duration, format = self.player.query_duration(gst.FORMAT_TIME)
+        except:
+            duration = gst.CLOCK_TIME_NONE
+
+        return (position, duration)
+
+    def seek(self, location):
+        """
+        @param location: time to seek to, in nanoseconds
+        """
+        gst.debug("seeking to %r" % location)
+        event = gst.event_new_seek(1.0, gst.FORMAT_TIME,
+            gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_ACCURATE,
+            gst.SEEK_TYPE_SET, location,
+            gst.SEEK_TYPE_NONE, 0)
+
+        res = self.player.send_event(event)
+        if res:
+            gst.info("setting new stream time to 0")
+            self.player.set_new_stream_time(0L)
+        else:
+            gst.error("seek to %r failed" % location)
+
+    def pause(self):
+        gst.info("pausing player")
+        self.player.set_state(gst.STATE_PAUSED)
+        self.playing = False
+
+    def play(self):
+        gst.info("playing player")
+        self.player.set_state(gst.STATE_PLAYING)
+        self.playing = True
+        
+    def stop(self):
+        self.player.set_state(gst.STATE_NULL)
+        self.playing = False
+        gst.info("stopped player")
+
+    def get_state(self, timeout=1):
+        return self.player.get_state(timeout=timeout)
+
+    def is_playing(self):
+        return self.playing
+
+class TitleWindow(gtk.DrawingArea):
+    def __init__(self, db):
+        gtk.DrawingArea.__init__(self)
+
+        self.pixbuf = None
+        self.width = self.height = 0
+        self.need_redraw = True
+        self.colours = None
+        self.db = db
+
+        self.pos_stack = []
+
+        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 = self.get_pango_context().get_font_description()
+        fd.set_absolute_size(25 * pango.SCALE)
+        self.fd = fd
+        self.modify_font(fd)
+        met = self.get_pango_context().get_metrics(fd)
+        self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE
+
+
+        self.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 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 = {}
+            self.add_col('song', "blue")
+            self.add_col('bg', "yellow")
+            self.add_col('C', "red")
+            self.add_col('parent', "orange")
+            self.add_col('folder', "black")
+            self.add_col('end', "white")
+            self.add_col('_', "black")
+            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)
+
+        lines = int((self.height) / self.lineheight) - 1
+        entries = self.db.namecnt()
+        # probably place current song in the middle
+        top = self.db.pos - lines / 2
+        # but try not to leave blank space at the end
+        if entries - self.db.pos < lines/2:
+            top = entries - lines
+        # but never have blank space at the top
+        if top < 0:
+            top = 0
+        self.top = top
+        offset = 0
+        for l in range(lines):
+            (type, name) = self.db.getname(top + l)
+            if type == "end":
+                break
+            if l == self.db.pos - top:
+                self.fd.set_absolute_size(40 * pango.SCALE)
+                self.modify_font(self.fd)
+            if type == "song":
+                layout = self.create_pango_layout(self.db.title(name))
+            elif type == "folder":
+                layout = self.create_pango_layout(urllib.unquote_plus(name))
+            else:
+                layout = self.create_pango_layout(name)
+            #(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)
+            if l == self.db.pos - top:
+                self.pixbuf.draw_rectangle(self.colours['end'], True,
+                                           0, l*self.lineheight,
+                                           self.width, self.lineheight*2)
+                self.pixbuf.draw_layout(self.colours[type],
+                                        0, l * self.lineheight,
+                                        layout)
+                offset = self.lineheight
+                self.fd.set_absolute_size(25 * pango.SCALE)
+                self.modify_font(self.fd)
+            else:
+                self.pixbuf.draw_layout(self.colours[type],
+                                        0, l * self.lineheight + offset,
+                                        layout)
+
+    def refresh(self):
+        self.need_redraw = True
+        self.queue_draw()
+        
+    def press(self,w,ev):
+        row = int(ev.y / self.lineheight)
+        if row > self.db.pos - self.top:
+            row -= 1
+        if self.db.pos != row + self.top:
+            self.db.pos = row + self.top
+        else:
+            (type,name) = self.db.getname(row + self.top)
+            if type == "parent":
+                sl = self.db.dir.rindex('/')
+                self.db.set_dir(self.db.dir[0:sl])
+                (t,p) = self.pos_stack.pop()
+                self.top = t
+                self.db.pos = p
+            elif type  == "folder":
+                self.pos_stack.append((self.top, self.db.pos))
+                self.db.set_dir(self.db.dir + "/" + name)
+            elif type == "song":
+                print "play", self.db.dir+"/"+name
+                self.db.set_song(name)
+                
+        self.refresh()
+        
+    def release(self,w,ev):
+        pass
+
+class FingerScale(gtk.DrawingArea):
+    def __init__(self, control):
+        gtk.DrawingArea.__init__(self)
+        self.control = control
+
+        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)
+
+        ctx = self.get_pango_context()
+        fd = ctx.get_font_description()
+        fd.set_absolute_size(25 * pango.SCALE)
+        self.modify_font(fd)
+        met = ctx.get_metrics(fd)
+        self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE
+
+    def start(self, percent, widget):
+        a = widget.get_allocation()
+        alloc = (a.x,a.y,a.width,a.height)
+        self.homewidget = alloc
+        self.grab_add()
+        self.tracking = False
+        self.show()
+        self.set(percent)
+
+    def end(self, percent = None):
+        self.hide()
+        self.grab_remove()
+        if percent == None:
+            percent = self.percent
+        if self.tracking:
+            self.control(percent, True)
+
+    def set(self, percent):
+        self.percent = percent
+        self.str = self.control(percent)
+        self.layout = self.create_pango_layout(self.str)
+        (ink, (ex,ey,ew,eh)) = self.layout.get_pixel_extents()
+        self.sw = ew
+        
+        self.queue_draw()
+
+    def reconfig(self, w, ev):
+        self.alloc = self.get_allocation()
+        self.theight = self.alloc.height - self.lineheight
+        
+    def redraw(self, area, ev):
+        self.window.draw_rectangle(self.get_style().bg_gc[gtk.STATE_NORMAL],
+                                   True, 0, 0,
+                                   self.alloc.width, self.alloc.height)
+        self.window.draw_rectangle(self.get_style().fg_gc[gtk.STATE_NORMAL],
+                                   False, 0, 0,
+                                   self.alloc.width-2, self.alloc.height-2)
+        self.window.draw_layout(self.get_style().fg_gc[gtk.STATE_NORMAL],
+                                int((self.alloc.width - self.sw)/2),
+                                int((self.percent * self.theight / 100)),
+                                self.layout)
+        
+    def release(self, c, ev):
+        if not self.tracking:
+            self.tracking = True
+            return
+        (gx,gy,gw,gh, gd) = ev.window.get_geometry()
+        (ox,oy) = c.window.get_origin()
+        y = ev.y_root - oy
+
+        percent = (y - self.lineheight/2) * 100 / self.theight
+        if percent < 0:
+            percent = 0
+        if percent > 100:
+            percent = 100
+        
+        if (gx,gy,gw,gh) == self.homewidget:
+            self.end()
+        else:
+            self.set(percent)
+
+    def press(self, c, ev):
+        pass
+    def motion(self, c, ev):
+        if ev.is_hint:
+            x, y, state = ev.window.get_pointer()
+        else:
+            x = ev.x
+            y = ev.y
+        a = c.get_allocation()
+        y -= a.y - self.offset
+        x = int(x); y = int(y)
+        if not self.tracking:
+            if y > ((self.percent * self.theight / 100)
+                    + self.lineheight/2):
+                self.tracking = True
+            else:
+                return
+        percent = (y - self.lineheight/2) * 100 / self.theight
+        if percent < 0:
+            percent = 0
+        if percent > 100:
+            percent = 100
+        self.set(percent)
+    
+class PlayerWindow(gtk.Window):
+    UPDATE_INTERVAL = 500
+    def __init__(self, db):
+        gtk.Window.__init__(self)
+        self.set_default_size(480, 640)
+        self.set_title("Music Player")
+
+        self.db = db
+
+        self.volume = 100
+        
+        self.update_id = -1
+        self.changed_id = -1
+        self.seek_timeout_id = -1
+
+        self.p_position = gst.CLOCK_TIME_NONE
+        self.p_duration = gst.CLOCK_TIME_NONE
+
+        self.create_ui()
+        self.soonid = None
+
+        self.player = GstPlayer()
+
+        self.player.on_eos = self.on_eos
+
+        db.changer(self.new_song)
+
+        suspend.monitor(self.on_suspend, self.on_resume)
+
+        def on_delete_event():
+            self.player.stop()
+            gtk.main_quit()
+        self.connect('delete-event', lambda *x: on_delete_event())
+
+    def on_eos(self, *a):
+        self.player.stop()
+        if not self.db.next() and not self.db.next():
+            return
+        (d,b,n) = self.db.curr_song()
+        self.load_file("file://" + urllib.quote(os.path.join(d,b)))
+        self.player.play()
+        self.tw.refresh()
+        #self.play_toggled()
+
+    def on_suspend(self):
+        self.player.stop()
+        self.play_button.remove(self.play_button.child)
+        self.play_button.add(self.play_image)
+        return True
+
+    def on_resume(self):
+        pass
+
+    def load_file(self, location):
+        self.player.set_location(location)
+
+    def create_ui(self):
+
+        isize = gtk.icon_size_register("big",120,120)
+        vbox = gtk.VBox(); vbox.show()
+        self.add(vbox)
+
+        hbox = gtk.HBox(); hbox.show()
+        vbox.pack_start(hbox, expand=False)
+
+        fd = pango.FontDescription("sans 10")
+        fd.set_absolute_size(25 * pango.SCALE)
+        b = gtk.Button("Seek");  b.show()
+        b.add_events(gtk.gdk.POINTER_MOTION_MASK|gtk.gdk.POINTER_MOTION_HINT_MASK)
+        b.child.modify_font(fd)
+        b.connect('button_press_event', self.grab_seek)
+        b.connect('button_release_event', self.release_seek)
+        hbox.add(b)
+
+        b = gtk.Button("Volume");  b.show()
+        b.add_events(gtk.gdk.POINTER_MOTION_MASK|gtk.gdk.POINTER_MOTION_HINT_MASK)
+        b.child.modify_font(fd)
+        b.connect('button_press_event', self.grab_volume)
+        b.connect('button_release_event', self.release_volume)
+        hbox.add(b)
+        hbox.set_homogeneous(True); hbox.set_size_request(-1,70)
+
+        hbox = gtk.HBox(); hbox.show()
+        vbox.pack_end(hbox, fill=False, expand=False)
+        # three button, prev, play/pause, next
+
+        image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_REWIND,
+                                         isize)
+        image.show()
+        button = gtk.Button()
+        button.add(image)
+        button.show()
+        hbox.pack_start(button)
+        button.set_focus_on_click(False)
+        button.connect('clicked', lambda *args: self.prev_song())
+        
+
+        image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_FORWARD,
+                                         isize)
+        image.show()
+        button = gtk.Button()
+        button.add(image)
+        button.show()
+        hbox.pack_end(button)
+        button.connect('clicked', lambda *args: self.next_song())
+
+        image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE,
+                                         isize)
+        image.show()
+        button = gtk.Button()
+        button.show()
+        hbox.pack_end(button)
+        self.pause_image = image
+        image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY,
+                                         isize)
+        image.show()
+        self.play_image = image
+        self.play_button = button
+        button.add(image)
+        button.connect('clicked', lambda *args: self.play_toggled())
+
+
+        self.dirlabel = gtk.Label("<Album>"); self.dirlabel.show()
+        vbox.pack_start(self.dirlabel, expand=False)
+        fd = self.dirlabel.get_pango_context().get_font_description()
+        fd.set_absolute_size(25 * pango.SCALE)
+        self.dirlabel.modify_font(fd)
+
+        self.dirlabel.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse('magenta'))
+        self.dirlabel.set_property('ellipsize', pango.ELLIPSIZE_START)
+
+        self.songlabel = gtk.Label("No Song Playing"); self.songlabel.show()
+        vbox.pack_end(self.songlabel, expand=False)
+        self.db.changer(lambda : self.songlabel.set_text(self.db.title()))
+
+        self.songlabel.modify_font(fd)
+        self.songlabel.set_property('ellipsize', pango.ELLIPSIZE_MIDDLE)
+        self.songlabel.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse('magenta'))
+
+        h = gtk.HBox(); h.show()
+        self.volumer = FingerScale(self.control_volume)
+        h.add(self.volumer)
+        self.tw = TitleWindow(self.db); self.tw.show()
+        h.add(self.tw)
+        self.seeker = FingerScale(self.control_seek)
+        h.add(self.seeker)
+
+        vbox.pack_end(h, padding=5)
+
+    def play_toggled(self):
+        self.play_button.remove(self.play_button.child)
+        if self.player.is_playing():
+            self.player.pause()
+            self.play_button.add(self.play_image)
+        else:
+            self.player.play()
+            #if self.update_id == -1:
+            #    self.update_id = gobject.timeout_add(self.UPDATE_INTERVAL,
+            #                                         self.update_scale_cb)
+            self.play_button.add(self.pause_image)
+
+    def soon_play(self, d,b):
+        # play d/b in 40msec if nothing else is suggested
+        self.player.stop()
+        if self.soonid != None:
+            gobject.source_remove(self.soonid)
+        self.soonfile = "file://" + urllib.quote(os.path.join(d,b))
+        self.soonid = gobject.timeout_add(40, self.soon)
+    def soon(self):
+        self.soonid = None
+        self.load_file(self.soonfile)
+        self.player.stop()
+        self.play_toggled()
+
+    def next_song(self):
+        if self.player.is_playing():
+            self.player.stop()
+        if not self.db.next():
+            self.db.next()
+
+    def prev_song(self):
+        if self.player.is_playing():
+            pos,dur = self.player.query_position()
+            self.player.stop()
+            if pos > 2*1000*1000*1000:
+                # 2 billion nanoseconds
+                self.player.seek(0)
+                self.player.play()
+                return
+        if not self.db.prev():
+            self.db.prev()
+
+
+    def new_song(self):
+        (d,b,n) = self.db.curr_song()
+        self.tw.refresh()
+        self.dirlabel.set_text(urllib.unquote_plus(d))
+        self.soon_play(d,b)
+
+    def scale_format_value_cb(self, scale, value):
+        if self.p_duration == -1:
+            real = 0
+        else:
+            real = value * self.p_duration / 100
+        
+        seconds = real / gst.SECOND
+
+        return "%02d:%02d" % (seconds / 60, seconds % 60)
+
+    def scale_button_press_cb(self, widget, event):
+        # see seek.c:start_seek
+        gst.debug('starting seek')
+        
+        self.play_button.set_sensitive(False)
+        self.was_playing = self.player.is_playing()
+        if self.was_playing:
+            self.player.pause()
+
+        # don't timeout-update position during seek
+        if self.update_id != -1:
+            gobject.source_remove(self.update_id)
+            self.update_id = -1
+
+        # make sure we get changed notifies
+        if self.changed_id == -1:
+            self.changed_id = self.hscale.connect('value-changed',
+                self.scale_value_changed_cb)
+            
+    def scale_value_changed_cb(self, scale):
+        # see seek.c:seek_cb
+        real = long(scale.get_value() * self.p_duration / 100) # in ns
+        gst.debug('value changed, perform seek to %r' % real)
+        self.player.seek(real)
+        # allow for a preroll
+        self.player.get_state(timeout=50*gst.MSECOND) # 50 ms
+
+    def scale_button_release_cb(self, widget, event):
+        # see seek.cstop_seek
+        widget.disconnect(self.changed_id)
+        self.changed_id = -1
+
+        self.play_button.set_sensitive(True)
+        if self.seek_timeout_id != -1:
+            gobject.source_remove(self.seek_timeout_id)
+            self.seek_timeout_id = -1
+        else:
+            gst.debug('released slider, setting back to playing')
+            if self.was_playing:
+                self.player.play()
+
+        if self.update_id != -1:
+            self.error('Had a previous update timeout id')
+        else:
+            self.update_id = gobject.timeout_add(self.UPDATE_INTERVAL,
+                self.update_scale_cb)
+
+    def update_scale_cb(self):
+        self.p_position, self.p_duration = self.player.query_position()
+        if self.p_position != gst.CLOCK_TIME_NONE:
+            value = self.p_position * 100.0 / self.p_duration
+            self.adjustment.set_value(value)
+
+        return True
+
+    def grab_seek(self, w, *a):
+        self.p_position, self.p_duration = self.player.query_position()
+        percent = self.p_position * 100 / self.p_duration
+        self.seeker.start(percent, w)
+    def release_seek(self, *a):
+        self.seeker.grab_remove()
+        self.seeker.hide()
+    def control_seek(self, percent, commit = False):
+        # return  "string"
+        pos = percent * self.p_duration / 100
+        seconds = pos / gst.SECOND
+        str = "%02d:%02d" % (seconds / 60, seconds % 60)
+        if commit:
+            self.player.seek(pos)
+        return str
+
+    def grab_volume(self, w, *a):
+        a = w.get_allocation()
+        self.volumer.start(self.volume, w)
+    def release_volume(self, *a):
+        self.volumer.grab_remove()
+        self.volumer.hide()
+    def control_volume(self, percent, commit = False):
+        self.volume = percent
+        self.player.set_volume(percent)
+        str = "%d%%" % percent
+        return str
+
+def main(args):
+
+    # Need to register our derived widget types for implicit event
+    # handlers to get called.
+
+    gobject.type_register(PlayerWindow)
+
+    db = MusicList("/home/music/ogg")
+    w = PlayerWindow(db)
+    w.show()
+
+    w.on_eos()
+    gtk.main()
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv))
diff --git a/music/notes b/music/notes
new file mode 100644 (file)
index 0000000..2c6598f
--- /dev/null
@@ -0,0 +1,28 @@
+
+Simple music player, using python,gtk,gst.
+
+Find music files in a directory tree.
+Any directory that contains at least one music file is an 'album'.
+Other directories are just collections.
+
+So we scan the directory tree looking for music files.
+When found, we add to table:
+   album[dirpath] += song
+if that is first song, add elements of dirpath to
+   tree[dir] += member
+
+Then sort them all
+
+--------------------
+Issue:  What to display in song list?
+
+ We sometimes want to display whatever we are browsing - directories or songs.
+ We sometimes want to display whatever we are playing - songs.
+    When shuffling, we want to show what is really the next song, so not
+      just the current album.
+
+ or not...
+
+ The main display could just be for browsing.
+ There is one line below to show 'current song'
+ click on 'current song' and it find it in the browser.
\ No newline at end of file
diff --git a/music/properties.py b/music/properties.py
new file mode 100644 (file)
index 0000000..56d4c32
--- /dev/null
@@ -0,0 +1,40 @@
+
+import gtk
+
+class RootProp():
+    def __init__(self):
+        self.root = gtk.gdk.get_default_root_window()
+
+    def setstr(self, name, val):
+        self.root.property_change(name, "STRING", 8,
+                                  gtk.gdk.PROP_MODE_REPLACE, val)
+
+    def getstr(self, name):
+        (type, format, value) = self.root.property_get(name)
+        if type != "STRING" or format != 8:
+            return None
+        return value
+
+    def watchstr(self, name, fn):
+        m = self.root.get_events()
+        self.root.set_events(m | gtk.gdk.PROPERTY_CHANGE_MASK)
+        self.root.add_filter(self.gotev, True)
+
+    def gotev(self, ev, tr):
+        print ev, dir(ev)
+        print ev.type, ev.get_state()
+        if ev.type == gtk.gdk.PROPERTY_NOTIFY:
+            print ev.atom
+        else:
+            print ev.type
+
+        ev2 = gtk.gdk.event_get()
+        print "and", ev2.type
+        return gtk.gdk.FILTER_CONTINUE
+
+def ping(*a):
+    print 'ping'
+
+a= RootProp()
+a.watchstr('song', ping)
+gtk.main()
diff --git a/netchoose/mdbus.py b/netchoose/mdbus.py
new file mode 100644 (file)
index 0000000..ac7c6ed
--- /dev/null
@@ -0,0 +1,431 @@
+#!/usr/bin/env python
+"""
+Mickey's own dbus introspection utility.
+
+(C) 2008 Michael 'Mickey' Lauer <mlauer@vanille-media.de>
+
+GPLv2 or later
+"""
+
+__version__ = "0.9.9"
+
+from xml.parsers.expat import ExpatError, ParserCreate
+from dbus.exceptions import IntrospectionParserException
+
+#----------------------------------------------------------------------------#
+class _Parser(object):
+#----------------------------------------------------------------------------#
+# Copyright (C) 2003, 2004, 2005, 2006 Red Hat Inc. <http://www.redhat.com/>
+# Copyright (C) 2003 David Zeuthen
+# Copyright (C) 2004 Rob Taylor
+# Copyright (C) 2005, 2006 Collabora Ltd. <http://www.collabora.co.uk/>
+# Copyright (C) 2007 John (J5) Palmieri
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use, copy,
+# modify, merge, publish, distribute, sublicense, and/or sell copies
+# of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+    __slots__ = ('map',
+                 'in_iface',
+                 'in_method',
+                 'in_signal',
+                 'in_property',
+                 'property_access',
+                 'in_sig',
+                 'out_sig',
+                 'node_level',
+                 'in_signal')
+    def __init__(self):
+        self.map = {'child_nodes':[],'interfaces':{}}
+        self.in_iface = ''
+        self.in_method = ''
+        self.in_signal = ''
+        self.in_property = ''
+        self.property_access = ''
+        self.in_sig = []
+        self.out_sig = []
+        self.node_level = 0
+
+    def parse(self, data):
+        parser = ParserCreate('UTF-8', ' ')
+        parser.buffer_text = True
+        parser.StartElementHandler = self.StartElementHandler
+        parser.EndElementHandler = self.EndElementHandler
+        parser.Parse(data)
+        return self.map
+
+    def StartElementHandler(self, name, attributes):
+        if name == 'node':
+            self.node_level += 1
+            if self.node_level == 2:
+                self.map['child_nodes'].append(attributes['name'])
+        elif not self.in_iface:
+            if (not self.in_method and name == 'interface'):
+                self.in_iface = attributes['name']
+        else:
+            if (not self.in_method and name == 'method'):
+                self.in_method = attributes['name']
+            elif (self.in_method and name == 'arg'):
+                arg_type = attributes['type']
+                arg_name = attributes.get('name', None)
+                if attributes.get('direction', 'in') == 'in':
+                    self.in_sig.append({'name': arg_name, 'type': arg_type})
+                if attributes.get('direction', 'out') == 'out':
+                    self.out_sig.append({'name': arg_name, 'type': arg_type})
+            elif (not self.in_signal and name == 'signal'):
+                self.in_signal = attributes['name']
+            elif (self.in_signal and name == 'arg'):
+                arg_type = attributes['type']
+                arg_name = attributes.get('name', None)
+
+                if attributes.get('direction', 'in') == 'in':
+                    self.in_sig.append({'name': arg_name, 'type': arg_type})
+            elif (not self.in_property and name == 'property'):
+                prop_type = attributes['type']
+                prop_name = attributes['name']
+
+                self.in_property = prop_name
+                self.in_sig.append({'name': prop_name, 'type': prop_type})
+                self.property_access = attributes['access']
+
+
+    def EndElementHandler(self, name):
+        if name == 'node':
+            self.node_level -= 1
+        elif self.in_iface:
+            if (not self.in_method and name == 'interface'):
+                self.in_iface = ''
+            elif (self.in_method and name == 'method'):
+                if not self.map['interfaces'].has_key(self.in_iface):
+                    self.map['interfaces'][self.in_iface]={'methods':{}, 'signals':{}, 'properties':{}}
+
+                if self.map['interfaces'][self.in_iface]['methods'].has_key(self.in_method):
+                    print "ERROR: Some clever service is trying to be cute and has the same method name in the same interface"
+                else:
+                    self.map['interfaces'][self.in_iface]['methods'][self.in_method] = (self.in_sig, self.out_sig)
+
+                self.in_method = ''
+                self.in_sig = []
+                self.out_sig = []
+            elif (self.in_signal and name == 'signal'):
+                if not self.map['interfaces'].has_key(self.in_iface):
+                    self.map['interfaces'][self.in_iface]={'methods':{}, 'signals':{}, 'properties':{}}
+
+                if self.map['interfaces'][self.in_iface]['signals'].has_key(self.in_signal):
+                    print "ERROR: Some clever service is trying to be cute and has the same signal name in the same interface"
+                else:
+                    self.map['interfaces'][self.in_iface]['signals'][self.in_signal] = (self.in_sig,)
+
+                self.in_signal = ''
+                self.in_sig = []
+                self.out_sig = []
+            elif (self.in_property and name == 'property'):
+                if not self.map['interfaces'].has_key(self.in_iface):
+                    self.map['interfaces'][self.in_iface]={'methods':{}, 'signals':{}, 'properties':{}}
+
+                if self.map['interfaces'][self.in_iface]['properties'].has_key(self.in_property):
+                    print "ERROR: Some clever service is trying to be cute and has the same property name in the same interface"
+                else:
+                    self.map['interfaces'][self.in_iface]['properties'][self.in_property] = (self.in_sig, self.property_access)
+
+                self.in_property = ''
+                self.in_sig = []
+                self.out_sig = []
+                self.property_access = ''
+
+#----------------------------------------------------------------------------#
+def process_introspection_data(data):
+#----------------------------------------------------------------------------#
+    """Return a structure mapping all of the elements from the introspect data
+       to python types TODO: document this structure
+
+    :Parameters:
+        `data` : str
+            The introspection XML. Must be an 8-bit string of UTF-8.
+    """
+    try:
+        return _Parser().parse(data)
+    except Exception, e:
+        raise IntrospectionParserException('%s: %s' % (e.__class__, e))
+
+#----------------------------------------------------------------------------#
+class Commands( object ):
+#----------------------------------------------------------------------------#
+    """
+    Implementing the dbus introspection / interaction.
+    """
+    def __init__( self, bus ):
+        if mode == "listen":
+            self._setupMainloop()
+        self.bus = bus()
+        self.busname = None
+        self.objpath = None
+        self.rinterface = None
+
+    def listBusNames( self ):
+        names = self.bus.list_names()[:]
+        names.sort()
+        for n in names:
+            print n
+
+    def listObjects( self, busname ):
+        self._listChildren( busname, '/' )
+
+    def listMethods( self, busname, objname ):
+        obj = self._tryObject( busname, objname )
+        if obj is not None:
+            data = process_introspection_data( obj.Introspect() )
+            for name, interface in data["interfaces"].iteritems():
+                self._listInterface( name, interface["signals"], interface["methods"], interface["properties"] )
+
+    def callMethod( self, busname, objname, methodname, parameters=[] ):
+        obj = self._tryObject( busname, objname )
+        if obj is not None:
+
+            if '.' in methodname:
+                # if we have a fully qualified methodname, use an Interface
+                ifacename = '.'.join( methodname.split( '.' )[:-1] )
+                methodname = methodname.split( '.' )[-1]
+                iface = dbus.Interface( obj, ifacename )
+                method = getattr( iface, methodname )
+            else:
+                method = getattr( obj, methodname.split( '.' )[-1] )
+
+            try:
+                result = method( *parameters )
+            except dbus.DBusException, e:
+                print "%s: %s failed: %s" % ( objname, methodname, e.get_dbus_name() )
+            except TypeError, e:
+                pass # python will emit its own error here
+            else:
+                print "%s: %s -> " % ( objname, methodname ),
+                if result is not None:
+                    self._prettyPrint( result )
+                else:
+                    print
+
+    def monitorBus( self ):
+        self._runMainloop()
+
+    def monitorService( self, busname ):
+        self.busname = busname
+        self._runMainloop()
+
+    def monitorObject( self, busname, objname ):
+        self.busname = busname
+        self.objpath = objname
+        self._runMainloop()
+
+    #
+    # command mode
+    #
+
+    def _listChildren( self, busname, objname ):
+        fail = objname is '/'
+        obj = self._tryObject( busname, objname, fail )
+        print objname
+        if obj is not None:
+            data = process_introspection_data( obj.Introspect() )
+            for o in data["child_nodes"]:
+                newname = "%s/%s" % ( objname, o )
+                newname = newname.replace( "//", "/" )
+                self._listChildren( busname, newname )
+
+    def _tryObject( self, busname, objname, fail=True ):
+        try:
+            obj = self.bus.get_object( busname, objname )
+        except ( dbus.DBusException, ValueError ):
+            if fail:
+                if busname in self.bus.list_names():
+                    print "Object name not found"
+                else:
+                    print "Service name not found"
+                sys.exit( -1 )
+            else:
+                return None
+        else:
+            return obj
+
+    def _parameter( self, type_, name ):
+        return "%s:%s" % ( type_, name )
+
+    def _signature( self, parameters ):
+        string = "( "
+        for p in parameters[0]:
+            string += self._parameter( p["type"], p["name"] )
+            string += ", "
+        if len( string ) == 2:
+            return "()"
+        else:
+            return string[:-2] + " )"
+
+    def _listInterface( self, name, signals, methods, properties ):
+        methodnames = methods.keys()
+        methodnames.sort()
+        for mname in methodnames:
+            signature = self._signature( methods[mname] )
+            print "[METHOD]    %s.%s%s" % ( name, mname, signature )
+
+        signalnames = signals.keys()
+        signalnames.sort()
+        for mname in signalnames:
+            signature = self._signature( signals[mname] )
+            print "[SIGNAL]    %s.%s%s" % ( name, mname, signature )
+
+        propertynames = properties.keys()
+        propertynames.sort()
+        for mname in propertynames:
+            signature = self._signature( properties[mname] )
+            print "[PROPERTY]  %s.%s%s" % ( name, mname, signature )
+
+    def _prettyPrint( self, result ):
+        # FIXME pretty printing...
+        print result
+
+    #
+    # listening mode
+    #
+
+    def _setupMainloop( self ):
+        import gobject
+        import dbus.mainloop.glib
+        dbus.mainloop.glib.DBusGMainLoop( set_as_default=True )
+        self.mainloop = gobject.MainLoop()
+        gobject.idle_add( self._setupListener )
+
+    def _runMainloop( self ):
+        try:
+            b = self.bus.__class__.__name__
+            bname = self.busname or "all"
+            oname = self.objpath or "all"
+            print "listening for signals on %s from service '%s', object '%s'..." % ( b, bname, oname )
+            self.mainloop.run()
+        except KeyboardInterrupt:
+            self.mainloop.quit()
+            sys.exit( 0 )
+
+    def _setupListener( self ):
+        self.bus.add_signal_receiver(
+            self._signalHandler,
+            None,
+            None,
+            self.busname,
+            self.objpath,
+            sender_keyword = "sender",
+            destination_keyword = "destination",
+            interface_keyword = "interface",
+            member_keyword = "member",
+            path_keyword = "path" )
+        return False # don't call me again
+
+    def _signalHandler( self, *args, **kwargs ):
+        timestamp = time.strftime("%Y%m%d.%H%M.%S") if timestamps else ""
+        print "%s [SIGNAL]    %s.%s    from %s %s" % ( timestamp, kwargs["interface"], kwargs["member"], kwargs["sender"], kwargs["path"] )
+        self._prettyPrint( args )
+
+#----------------------------------------------------------------------------#
+if __name__ == "__main__":
+#----------------------------------------------------------------------------#
+    import gobject
+    import dbus
+    import sys
+    import time
+
+    argv = sys.argv[::-1]
+    execname = argv.pop()
+
+    if ( "-h" in argv ) or ( "--help" in argv ):
+        print "Usage: %s [-s] [-l] [ busname [ objectpath [ methodname [ parameters... ] ] ] ]" % ( sys.argv[0] )
+        sys.exit( 0 )
+
+    bus = dbus.SessionBus
+    mode = "command"
+    timestamps = False
+    escape = False
+
+    # run through all arguments and check whether we got '-s' somewhere
+    if "-s" in argv:
+        bus = dbus.SystemBus
+        argv.remove( "-s" )
+
+    # run through all arguments and check whether we got '-l' somewhere
+    if "-l" in argv:
+        mode = "listen"
+        argv.remove( "-l" )
+
+    # run through all arguments and check whether we got '-t' somewhere
+    if "-t" in argv:
+        timestamps = True
+        argv.remove( "-t" )
+
+    # run through all arguments and check whether we got '-e' somewhere
+    if "-e" in argv:
+        escape = True
+        argv.remove( "-e" )
+
+    c = Commands( bus )
+
+    if len( argv ) == 0:
+        if mode == "command":
+            c.listBusNames()
+        else:
+            c.monitorBus()
+
+    elif len( argv ) == 1:
+        busname = argv.pop()
+        if mode == "command":
+            c.listObjects( busname )
+        else:
+            c.monitorService( busname )
+
+    elif len( argv ) == 2:
+        busname = argv.pop()
+        objname = argv.pop()
+        if mode == "command":
+            c.listMethods( busname, objname )
+        else:
+            c.monitorObject( busname, objname )
+
+    elif len( argv ) == 3:
+        busname = argv.pop()
+        objname = argv.pop()
+        methodname = argv.pop()
+        c.callMethod( busname, objname, methodname )
+
+    else:
+        busname = argv.pop()
+        objname = argv.pop()
+        methodname = argv.pop()
+        parameters = []
+
+        while argv:
+            try:
+                string = argv.pop()
+                parameter = eval( string )
+            except NameError: # treat as string
+                parameter = eval( '"""%s"""' % string )
+                if escape:
+                    parameter = parameter.replace( '.r', '\r' )
+                    parameter = parameter.replace( '.n', '\n' )
+                parameters.append( parameter )
+            except ( SyntaxError, ValueError, AttributeError ):
+                print "Error while evaluating '%s'" % string
+                sys.exit( -1 )
+            else:
+                parameters.append( parameter )
+
+        c.callMethod( busname, objname, methodname, parameters )
diff --git a/netchoose/netchoose.py b/netchoose/netchoose.py
new file mode 100755 (executable)
index 0000000..8feb760
--- /dev/null
@@ -0,0 +1,89 @@
+#!/usr/bin/env python
+
+import pygtk
+import gtk
+import os
+
+import gobject
+import dbus
+import sys
+import time
+import mdbus
+
+class NetChooser:
+    def __init__(self):
+        window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+        window.connect("destroy", self.close_application)
+        window.set_title("NetChooser")
+
+        # vertical stack of stuff.
+        vb = gtk.VBox()
+        window.add(vb)
+        vb.show()
+
+        ## GPRS: row of bits
+        hb = gtk.HBox()
+        vb.add(hb)
+        hb.show()
+
+        ### GPRS toggle button
+        tb = gtk.ToggleButton("GPRS")
+        tb.connect("toggled", self.gprs_set)
+        tb.show()
+        ### AP name entry
+        ap = gtk.Entry(16)
+        ap.show()
+
+        hb.add(tb)
+        hb.add(ap)
+
+        ## WLAN: row of stuff
+        hb = gtk.HBox()
+        vb.add(hb)
+        hb.show()
+
+        ### WLAN toggle button
+        tb = gtk.ToggleButton("WLAN")
+        tb.connect("toggled", self.wlan_set)
+        tb.show()
+        ### Acces point dropdown
+        ap = gtk.combo_box_entry_new_text()
+        ap.child.connect("changed", self.wlan_ap)
+        ap.show()
+
+        hb.add(tb)
+        hb.add(ap)
+
+        window.show()
+
+    def close_application(self, widget):
+        gtk.main_quit()
+        
+
+    def gprs_set(self, widget):
+        if widget.get_active():
+            # start GPRS
+            c = Commands(dbus.SystemBus)
+            c.callMethod("org.freesmartphone.frameworkd",
+                         "/org/freesmartphone/GSM/Device",
+                         "org.freesmartphone.GSM.PDP.ActivateContext",
+                         [ "vfinternet.au", "x", "x"])
+        else:
+            # stop GPRS
+            c = Commands(dbus.SystemBus)
+            c.callMethod("org.freesmartphone.frameworkd",
+                         "/org/freesmartphone/GSM/Device",
+                         "org.freesmartphone.GSM.PDP.DeactivateContext")
+        return
+    def wlan_set(self, widget):
+        return
+    def wlan_ap(self, widget):
+        return
+        
+def main():
+    gtk.main()
+    return 0
+if __name__ == "__main__":
+    NetChooser()
+    main()
+    
diff --git a/sms/contact.py b/sms/contact.py
new file mode 100644 (file)
index 0000000..0dea474
--- /dev/null
@@ -0,0 +1,21 @@
+
+#
+# Contacts are stored in a file, one per line, with ':' separated
+# fields.  If a field can have a list, it is comma separated.
+# entries in a list can have a 'tag=' prefix.
+# We can have references to other entries, so each entry has an ID
+# Fields are:
+#   id
+#   Family Name
+#   Given Name
+#   groups     - list
+#   references - list
+#   PO Box
+#   Address
+#   Address extension
+#   Suburb/town
+#   postcode
+#   County
+#   phone numbers - list
+#   modify date
+
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/notes b/sms/notes
new file mode 100644 (file)
index 0000000..23446a1
--- /dev/null
+++ b/sms/notes
@@ -0,0 +1,290 @@
+
+AT+CFUN=1    #  turn on.  =0 to turn off??
+AT+COPS      #  connect to GSM network
+AT+COPS?     #  get status and carrier
+AT+COPS=?    #  get list of providers
+  +COPS: (2,"vodafone AU","voda AU","50503"),(3,"YES OPTUS","Optus","50502"),(3,"Telstra Mobile","Telstra","50501")
+ p=re.compile('^\+COPS: (\((\d+),"([^"]*)","([^"]*)","([^"]*)"\),)*$')
+
+AT%SLEEP=2   # disable deep sleep to avoid some bug.
+
+AT+CMGF=1    # enable text mode for SMS
+
+
+# PIN
+AT+CPIN? 
+AT+CPIN="XXXX"
+
+AT+CPWD=fac,old,new  fac=PS SC AB P2 ???
+
+ AB = 1234
+
+ATE0  - no echo
+
+AT+CSQ   # signal quality
+AT+CREG? # are we registered?? (0,1)==at home, (0,5) == roaming
+AT+CREG=2 # get regular updates of location : LAC and CELLID in hex
+
+AT+CIMI  # get imi number
+
+AT+CPAS  # activity status??
+   0 == nothing
+   3 == incoming call
+   4 == on call
+   5 == ??? no connected??
+
+
+ATA - answer call - +CRING: VOICE is received
+   Get 'OK' is there was nothing to answer any more
+   NO CARRIER when other end hangs up
+
+AT+CLIP=1 enables calling number
+  +CLIP: "0403463349",129,,,,0
+
+ATDnumber;  makes a voice call.
+get NO CARRIER on hangup.
+Can tell if answered with CPAS (==4)
+
+
+ATH - hangup  or AT+CHUPA
+
+AT+CUSD=1,"number"   # sends special request, reply is asyn
+ +CUSD: 2 .....
+ e.g. *61#  - divert on no answer.
+
+
+AT+CMEE=2   verbose errors
+# SMS
+
+AT+CMGS="phonenumber"
+> text
+> text
+> ctrl-Z
+             # send a text message
+
+AT+CMGL="ALL" or "REC UNREAD"  etc to view all SMS messages
+             last number is byte count
+
+AT+CMGR=N   read message N
+AT+CMGD=N   delete message N
+# GPRS
+AT+CGDCONT=1,"IP","AU internet"   # or whatever
+ATD*99#
+
+
+# new messages:
+
+AT+CNMI: (0-2),(0-3),(0,2),(0,1),(0,1)
+  (0-2)  1 to send is possible
+  (0-3)  sms incoming: 1 == just index, 2 == message
+  (0,2)  ditto for cell broadcast
+  (0,1)  sms status report
+  (0,1)  flush or clear any buffered messages
+AT+CNMI=1,2,2,0,1
+
+AT+CSCB=?
+    request cell broadcast be collected
+
+Cell broadcast looks like
++CBM: 2000,50,1,1,1
+Eastlakes
+
+or in packet mode
++CBM: 88
+062000320111C2373DECCE37148D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D100
+^^ 6 letters
+  ^^ 20 == ??
+    ^^ 00 
+      ^^32  port '50' is 'Cell Name'
+        ^^01 == ??
+
+Remainder is encoded as 7 data,, 'Botany\r\n\r\r\r....\n'
+But there is a leading \b ??
+
++CBM: 88
+07D000320111C5F09CCE0EAFCBF386A2D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D100
+^^ 7 letters
+  D000
+      32
+        01
+          \bEastlakes\r\n\r\r\r....\n
+
+
+AT%EM=2,1 - check Serving Cell INfo
+AT%EM=2,3 - check neighbours
+
+AT%EM=2,1
+
+%EM: 102,39,39,39,49,28232,15,0,1,0,0,0,0,0,0,2215,0,0,2,255
+               ^^    ^^ cell                  ^^ location
+          strength
+OK
+AT%EM=2,3
+
+%EM: 6
+96,88,93,572,622,612
+17,30,32,1,1,-3
+17,30,32,-3,-3,-7
+19,32,34,22,22,18   strength??
+17,35,50,9,32,38
+28237,22793,22791,28237,22796,22798       <--- cell
+2215,2215,2215,2215,2215,2215             <--- location
+695457,2517437,2517437,2461758,644848,644848
+3888,944,948,2424,3380,3380
+0,0,0,0,0,0
+0,0,0,0,0,0
+2,2,2,2,2,2
+7,7,7,7,7,7
+0,0,0,2,2,2
+0,0,0,0,0,0
+0,0,0,23,23,23
+
+AT%EM=2,4
+
+%EM: 4,20,505,003,-1687800052
+
+ 505 is country
+ 003 is carrier
+
+OK
+AT+CIMI
+
+505038240084403
+
+
+
+
+
+AT%N0187  # maybe cancel echo ??
+
+
+AT+VTS=01234  - tone generation
+
+----------------------------------------------
+
+Seem to have 4 channels.
+
+1 must be reserved for pppd
+1 for management
+1 for SMS sending?
+1 for monitor
+
+Management:
+  Start muxer and connect
+  turn on device
+  attempt to register.  On failure, get list of available networks.
+    Continue to check every 10 minutes while not suspended.
+    If not registerred. leave 
+
+
+
+How do I get these???
++CMS ERROR: 322
+
++CMS ERROR: memory full
+
+I did
+  at+CMGR=?
+
+then they spontaneously appeared.
+
+
+OK
+
+current date time
+> at+cclk?
+> 
+> +CCLK: "0/1/1,0:0:9"
+> 
+> OK
+> at+cclk="09/04/01,14:30:00+00" 
+> 
+> OK
+> at+cclk? 
+> 
+> +CCLK: "9/4/1,14:30:3"
+> 
+This is not set automatically :-(
+
+
+call volume level
+
+at+clvl=230
+
+Command: AT+COPS?,
+Response: +COPS: (<mode>,[<format>,<oper>[,<AcT>]]),¡Ä, (<modeN>,[<formatN>,<operN>[,<AcTN>]])
+
+Command: AT+COPS=?
+Response: +COPS: <stat>, long <oper>, short <oper>, numeric <oper>, <AcT>
+
+Response: +CME ERROR:
+Command: AT+COPS=<mode>,[<format>,<oper>[,<AcT>]]
+Response: OK | +CME ERROR
+
+Description: Get/set current GSM/UMTS network operator, list available operators. This can be used to change for example access type and switch network.
+
+<mode>
+
+   0. Automatic network selection (<oper> ignored)
+   1. Manual network selection, <oper> must be present, <AcT> is optional.
+   2. Deregister from network.
+   3. Set <format only, no registration/deregistration.
+   4. Manual selection with automatic fall back (enters automatic mode if manual selection fails).
+
+<format>
+
+   0. Long alphanumeric string
+   1. Short alphanumeric string
+   2. Numeric ID
+
+<oper>
+String (based on <format>) that identifies the operator.
+
+<stat>
+
+   0. Unknown
+   1. Available
+   2. Current
+   3. Forbidden
+
+<AcT> Network access type
+
+   0. GSM
+   1. Compact GSM
+   2. UTRAN
+   3. GSM with EGPRS
+   4. UTRAN with HSDPA
+   5. UTRAN with HSUPA
+   6. UTRAN with HSDPA and HSU
+
+-----------------------------------
+ATH
+AT+CUSD=0
+ATH
+AT+CUSD=0,"*100#",15
+
+reply:
++CUSD: 1,"Try Again
+1.Your Balance
+2.Voucher Top-Up",15
+AT+CUSD=1,"1"
+
+OK
++CUSD: 0,"Your bal is $18.18 &expires on 11/01/2011. Your Magic Top Up 22c rate applies until 10/02/2010. You've got 100 FREE texts with 100 to use before 10/02/2010.",15
+
+OK
+
+
+So when sending:
+ 0 - start new interchange
+ 1 - follow up message
+When receiving
+ 0 - no reply possible
+ 1 - waiting for reply
+
+
+So: If number matches
+  [*#].*#
+Then set button to "SEND" rather than "CALL" and us CUSD
+Display reply in message area with "cancel" button.
+If reply is wanted, also have "reply" button
diff --git a/sms/sendsms.py b/sms/sendsms.py
new file mode 100644 (file)
index 0000000..7f6d528
--- /dev/null
@@ -0,0 +1,1595 @@
+#!/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.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("/media/card/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("/media/card/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 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 ['/media/card','/media/disk','/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..3d3e3b1
--- /dev/null
@@ -0,0 +1,483 @@
+#
+# 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):
+    z = strg[-3:]
+    return time.strptime(strg[:-3], "%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/sms/test.py b/sms/test.py
new file mode 100644 (file)
index 0000000..bacfd5e
--- /dev/null
@@ -0,0 +1,33 @@
+
+import os, time, sys
+from storesms import SMSstore, SMSmesg
+
+
+try:
+    os.mkdir("/tmp/sms")
+except:
+    pass
+st = SMSstore("/tmp/sms")
+
+if len(sys.argv) == 2 and sys.argv[1][0] == '-':
+    (next, l) = st.lookup(time.time(), sys.argv[1][1:])
+    print next
+    for m in l:
+        print m.format()
+
+elif len(sys.argv) > 1:
+    m = SMSmesg(time.time(),
+                "0403463349",
+                ["0415836820"],
+                sys.argv[1]
+                )
+    if len(sys.argv) > 2:
+        st.store(m, sys.argv[2])
+    else:
+        st.store(m)
+else:
+    (next, l) = st.lookup(time.time())
+    print next
+    for m in l:
+        print m.format()
+
diff --git a/sounds/formats.h b/sounds/formats.h
new file mode 100644 (file)
index 0000000..b5314f9
--- /dev/null
@@ -0,0 +1,127 @@
+#ifndef FORMATS_H
+#define FORMATS_H              1
+
+#include <endian.h>
+#include <byteswap.h>
+
+/* Definitions for .VOC files */
+
+#define VOC_MAGIC_STRING       "Creative Voice File\x1A"
+#define VOC_ACTUAL_VERSION     0x010A
+#define VOC_SAMPLESIZE         8
+
+#define VOC_MODE_MONO          0
+#define VOC_MODE_STEREO                1
+
+#define VOC_DATALEN(bp)                ((u_long)(bp->datalen) | \
+                               ((u_long)(bp->datalen_m) << 8) | \
+                               ((u_long)(bp->datalen_h) << 16) )
+
+typedef struct voc_header {
+       u_char magic[20];       /* must be MAGIC_STRING */
+       u_short headerlen;      /* Headerlength, should be 0x1A */
+       u_short version;        /* VOC-file version */
+       u_short coded_ver;      /* 0x1233-version */
+} VocHeader;
+
+typedef struct voc_blocktype {
+       u_char type;
+       u_char datalen;         /* low-byte    */
+       u_char datalen_m;       /* medium-byte */
+       u_char datalen_h;       /* high-byte   */
+} VocBlockType;
+
+typedef struct voc_voice_data {
+       u_char tc;
+       u_char pack;
+} VocVoiceData;
+
+typedef struct voc_ext_block {
+       u_short tc;
+       u_char pack;
+       u_char mode;
+} VocExtBlock;
+
+/* Definitions for Microsoft WAVE format */
+
+#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
+
+#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')
+
+/* WAVE fmt block constants from Microsoft mmreg.h header */
+#define WAV_FMT_PCM             0x0001
+#define WAV_FMT_IEEE_FLOAT      0x0003
+#define WAV_FMT_DOLBY_AC3_SPDIF 0x0092
+#define WAV_FMT_EXTENSIBLE      0xfffe
+
+/* Used with WAV_FMT_EXTENSIBLE format */
+#define WAV_GUID_TAG           "\x00\x00\x00\x00\x10\x00\x80\x00\x00\xAA\x00\x38\x9B\x71"
+
+/* it's in chunks like .voc and AMIGA iff, but my source say there
+   are in only in this combination, so I combined them in one header;
+   it works on all WAVE-file I have
+ */
+typedef struct {
+       u_int magic;            /* 'RIFF' */
+       u_int length;           /* filelen */
+       u_int type;             /* 'WAVE' */
+} WaveHeader;
+
+typedef struct {
+       u_short format;         /* see WAV_FMT_* */
+       u_short channels;
+       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 {
+       WaveFmtBody format;
+       u_short ext_size;
+       u_short bit_p_spl;
+       u_int channel_mask;
+       u_short guid_format;    /* WAV_FMT_* */
+       u_char guid_tag[14];    /* WAV_GUID_TAG */
+} WaveFmtExtensibleBody;
+
+typedef struct {
+       u_int type;             /* 'data' */
+       u_int length;           /* samplecount */
+} WaveChunkHeader;
+
+/* Definitions for Sparc .au header */
+
+#define AU_MAGIC               COMPOSE_ID('.','s','n','d')
+
+#define AU_FMT_ULAW            1
+#define AU_FMT_LIN8            2
+#define AU_FMT_LIN16           3
+
+typedef struct au_header {
+       u_int magic;            /* '.snd' */
+       u_int hdr_size;         /* size of header (min 24) */
+       u_int data_size;        /* size of data */
+       u_int encoding;         /* see to AU_FMT_XXXX */
+       u_int sample_rate;      /* sample rate */
+       u_int channels;         /* number of channels (voices) */
+} AuHeader;
+
+#endif                         /* FORMATS */
diff --git a/sounds/sound.c b/sounds/sound.c
new file mode 100644 (file)
index 0000000..01d5122
--- /dev/null
@@ -0,0 +1,1440 @@
+/*
+ * 
+ * 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 written to a file
+ * with the same name as the sound file, but with a leading period.
+ *
+ * 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.
+ *
+ *  Contains code from: aplay.c - plays and records
+ *  Copyright (c) by Jaroslav Kysela <perex@perex.cz>
+ *  Based on vplay program by Michael Beck
+ *
+ *
+ *   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 <string.h>
+#include <getopt.h>
+#include <fcntl.h>
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <time.h>
+#include <locale.h>
+#include <alsa/asoundlib.h>
+#include <assert.h>
+#include <sys/poll.h>
+#include <sys/uio.h>
+#include <sys/time.h>
+#include <sys/signal.h>
+#include <asm/byteorder.h>
+#include "aconfig.h"
+#include "gettext.h"
+#include "formats.h"
+#include "version.h"
+
+#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
+
+/* global data */
+
+static snd_pcm_sframes_t (*readi_func)(snd_pcm_t *handle, void *buffer, snd_pcm_uframes_t size);
+static snd_pcm_sframes_t (*writei_func)(snd_pcm_t *handle, const void *buffer, snd_pcm_uframes_t size);
+static snd_pcm_sframes_t (*readn_func)(snd_pcm_t *handle, void **bufs, snd_pcm_uframes_t size);
+static snd_pcm_sframes_t (*writen_func)(snd_pcm_t *handle, void **bufs, snd_pcm_uframes_t size);
+
+static char *command;
+static snd_pcm_t *handle;
+static struct {
+       snd_pcm_format_t format;
+       unsigned int channels;
+       unsigned int rate;
+} hwparams, rhwparams;
+static int quiet_mode = 0;
+static int file_type = FORMAT_DEFAULT;
+static int open_mode = 0;
+static snd_pcm_stream_t stream = SND_PCM_STREAM_PLAYBACK;
+static int mmap_flag = 0;
+static int interleaved = 1;
+static int nonblock = 0;
+static u_char *audiobuf = NULL;
+static snd_pcm_uframes_t chunk_size = 0;
+static unsigned period_time = 0;
+static unsigned buffer_time = 0;
+static snd_pcm_uframes_t period_frames = 0;
+static snd_pcm_uframes_t buffer_frames = 0;
+static int avail_min = -1;
+static int start_delay = 0;
+static int stop_delay = 0;
+static int monotonic = 0;
+static int verbose = 0;
+static int buffer_pos = 0;
+static size_t bits_per_sample, bits_per_frame;
+static size_t chunk_bytes;
+static int test_position = 0;
+static int test_coef = 8;
+static int test_nowait = 0;
+static snd_output_t *log;
+
+static int fd = -1;
+static off64_t pbrec_count = LLONG_MAX, fdcount;
+static int vocmajor, vocminor;
+
+/* needed prototypes */
+
+static void playback(char *filename);
+
+#if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 95)
+#define error(...) do {\
+       fprintf(stderr, "%s: %s:%d: ", command, __FUNCTION__, __LINE__); \
+       fprintf(stderr, __VA_ARGS__); \
+       putc('\n', stderr); \
+} while (0)
+#else
+#define error(args...) do {\
+       fprintf(stderr, "%s: %s:%d: ", command, __FUNCTION__, __LINE__); \
+       fprintf(stderr, ##args); \
+       putc('\n', stderr); \
+} while (0)
+#endif 
+
+static void device_list(void)
+{
+       snd_ctl_t *handle;
+       int card, err, dev, idx;
+       snd_ctl_card_info_t *info;
+       snd_pcm_info_t *pcminfo;
+       snd_ctl_card_info_alloca(&info);
+       snd_pcm_info_alloca(&pcminfo);
+
+       card = -1;
+       if (snd_card_next(&card) < 0 || card < 0) {
+               error(_("no soundcards found..."));
+               return;
+       }
+       printf(_("**** List of %s Hardware Devices ****\n"),
+              snd_pcm_stream_name(stream));
+       while (card >= 0) {
+               char name[32];
+               sprintf(name, "hw:%d", card);
+               if ((err = snd_ctl_open(&handle, name, 0)) < 0) {
+                       error("control open (%i): %s", card, snd_strerror(err));
+                       goto next_card;
+               }
+               if ((err = snd_ctl_card_info(handle, info)) < 0) {
+                       error("control hardware info (%i): %s", card, snd_strerror(err));
+                       snd_ctl_close(handle);
+                       goto next_card;
+               }
+               dev = -1;
+               while (1) {
+                       unsigned int count;
+                       if (snd_ctl_pcm_next_device(handle, &dev)<0)
+                               error("snd_ctl_pcm_next_device");
+                       if (dev < 0)
+                               break;
+                       snd_pcm_info_set_device(pcminfo, dev);
+                       snd_pcm_info_set_subdevice(pcminfo, 0);
+                       snd_pcm_info_set_stream(pcminfo, stream);
+                       if ((err = snd_ctl_pcm_info(handle, pcminfo)) < 0) {
+                               if (err != -ENOENT)
+                                       error("control digital audio info (%i): %s", card, snd_strerror(err));
+                               continue;
+                       }
+                       printf(_("card %i: %s [%s], device %i: %s [%s]\n"),
+                               card, snd_ctl_card_info_get_id(info), snd_ctl_card_info_get_name(info),
+                               dev,
+                               snd_pcm_info_get_id(pcminfo),
+                               snd_pcm_info_get_name(pcminfo));
+                       count = snd_pcm_info_get_subdevices_count(pcminfo);
+                       printf( _("  Subdevices: %i/%i\n"),
+                               snd_pcm_info_get_subdevices_avail(pcminfo), count);
+                       for (idx = 0; idx < (int)count; idx++) {
+                               snd_pcm_info_set_subdevice(pcminfo, idx);
+                               if ((err = snd_ctl_pcm_info(handle, pcminfo)) < 0) {
+                                       error("control digital audio playback info (%i): %s", card, snd_strerror(err));
+                               } else {
+                                       printf(_("  Subdevice #%i: %s\n"),
+                                               idx, snd_pcm_info_get_subdevice_name(pcminfo));
+                               }
+                       }
+               }
+               snd_ctl_close(handle);
+       next_card:
+               if (snd_card_next(&card) < 0) {
+                       error("snd_card_next");
+                       break;
+               }
+       }
+}
+
+static void pcm_list(void)
+{
+       void **hints, **n;
+       char *name, *descr, *descr1, *io;
+       const char *filter;
+
+       if (snd_device_name_hint(-1, "pcm", &hints) < 0)
+               return;
+       n = hints;
+       filter = "Output";
+       while (*n != NULL) {
+               name = snd_device_name_get_hint(*n, "NAME");
+               descr = snd_device_name_get_hint(*n, "DESC");
+               io = snd_device_name_get_hint(*n, "IOID");
+               if (io != NULL && strcmp(io, filter) != 0)
+                       goto __end;
+               printf("%s\n", name);
+               if ((descr1 = descr) != NULL) {
+                       printf("    ");
+                       while (*descr1) {
+                               if (*descr1 == '\n')
+                                       printf("\n    ");
+                               else
+                                       putchar(*descr1);
+                               descr1++;
+                       }
+                       putchar('\n');
+               }
+             __end:
+               if (name != NULL)
+                       free(name);
+               if (descr != NULL)
+                       free(descr);
+               if (io != NULL)
+                       free(io);
+               n++;
+       }
+       snd_device_name_free_hint(hints);
+}
+
+static void version(void)
+{
+       printf("%s: version " SND_UTIL_VERSION_STR " by Jaroslav Kysela <perex@perex.cz>\n", command);
+}
+
+static void signal_handler(int sig)
+{
+       if (verbose==2)
+               putchar('\n');
+       if (!quiet_mode)
+               fprintf(stderr, _("Aborted by signal %s...\n"), strsignal(sig));
+       if (fd > 1) {
+               close(fd);
+               fd = -1;
+       }
+       if (handle && sig != SIGABRT) {
+               snd_pcm_close(handle);
+               handle = NULL;
+       }
+       exit(EXIT_FAILURE);
+}
+
+enum {
+       OPT_VERSION = 1,
+       OPT_PERIOD_SIZE,
+       OPT_BUFFER_SIZE,
+       OPT_DISABLE_RESAMPLE,
+       OPT_DISABLE_CHANNELS,
+       OPT_DISABLE_FORMAT,
+       OPT_DISABLE_SOFTVOL,
+       OPT_TEST_POSITION,
+       OPT_TEST_COEF,
+       OPT_TEST_NOWAIT
+};
+
+int main(int argc, char *argv[])
+{
+       int option_index;
+       static const char short_options[] = "";
+       static const struct option long_options[] = {
+               {0, 0, 0, 0}
+       };
+       char *pcm_name = "default";
+       int tmp, err, c;
+       int do_device_list = 0, do_pcm_list = 0;
+       snd_pcm_info_t *info;
+
+       snd_pcm_info_alloca(&info);
+
+       err = snd_output_stdio_attach(&log, stderr, 0);
+       assert(err >= 0);
+
+       command = argv[0];
+       file_type = FORMAT_DEFAULT;
+
+       stream = SND_PCM_STREAM_PLAYBACK;
+       command = "aplay";
+
+       chunk_size = -1;
+       rhwparams.format = DEFAULT_FORMAT;
+       rhwparams.rate = DEFAULT_SPEED;
+       rhwparams.channels = 1;
+
+       while ((c = getopt_long(argc, argv, short_options, long_options, &option_index)) != -1) {
+               switch (c) {
+               case 'h':
+                       usage(command);
+                       return 0;
+               case OPT_VERSION:
+                       version();
+                       return 0;
+               case 'l':
+                       do_device_list = 1;
+                       break;
+               case 'L':
+                       do_pcm_list = 1;
+                       break;
+               case 'D':
+                       pcm_name = optarg;
+                       break;
+               case 'q':
+                       quiet_mode = 1;
+                       break;
+               case 't':
+                       if (strcasecmp(optarg, "raw") == 0)
+                               file_type = FORMAT_RAW;
+                       else if (strcasecmp(optarg, "voc") == 0)
+                               file_type = FORMAT_VOC;
+                       else if (strcasecmp(optarg, "wav") == 0)
+                               file_type = FORMAT_WAVE;
+                       else if (strcasecmp(optarg, "au") == 0 || strcasecmp(optarg, "sparc") == 0)
+                               file_type = FORMAT_AU;
+                       else {
+                               error(_("unrecognized file format %s"), optarg);
+                               return 1;
+                       }
+                       break;
+               case 'c':
+                       rhwparams.channels = strtol(optarg, NULL, 0);
+                       if (rhwparams.channels < 1 || rhwparams.channels > 32) {
+                               error(_("value %i for channels is invalid"), rhwparams.channels);
+                               return 1;
+                       }
+                       break;
+               case 'f':
+                       if (strcasecmp(optarg, "cd") == 0 || strcasecmp(optarg, "cdr") == 0) {
+                               if (strcasecmp(optarg, "cdr") == 0)
+                                       rhwparams.format = SND_PCM_FORMAT_S16_BE;
+                               else
+                                       rhwparams.format = file_type == FORMAT_AU ? SND_PCM_FORMAT_S16_BE : SND_PCM_FORMAT_S16_LE;
+                               rhwparams.rate = 44100;
+                               rhwparams.channels = 2;
+                       } else if (strcasecmp(optarg, "dat") == 0) {
+                               rhwparams.format = file_type == FORMAT_AU ? SND_PCM_FORMAT_S16_BE : SND_PCM_FORMAT_S16_LE;
+                               rhwparams.rate = 48000;
+                               rhwparams.channels = 2;
+                       } else {
+                               rhwparams.format = snd_pcm_format_value(optarg);
+                               if (rhwparams.format == SND_PCM_FORMAT_UNKNOWN) {
+                                       error(_("wrong extended format '%s'"), optarg);
+                                       exit(EXIT_FAILURE);
+                               }
+                       }
+                       break;
+               case 'r':
+                       tmp = strtol(optarg, NULL, 0);
+                       if (tmp < 300)
+                               tmp *= 1000;
+                       rhwparams.rate = tmp;
+                       if (tmp < 2000 || tmp > 192000) {
+                               error(_("bad speed value %i"), tmp);
+                               return 1;
+                       }
+                       break;
+               case 'N':
+                       nonblock = 1;
+                       open_mode |= SND_PCM_NONBLOCK;
+                       break;
+               case 'F':
+                       period_time = strtol(optarg, NULL, 0);
+                       break;
+               case 'B':
+                       buffer_time = strtol(optarg, NULL, 0);
+                       break;
+               case OPT_PERIOD_SIZE:
+                       period_frames = strtol(optarg, NULL, 0);
+                       break;
+               case OPT_BUFFER_SIZE:
+                       buffer_frames = strtol(optarg, NULL, 0);
+                       break;
+               case 'A':
+                       avail_min = strtol(optarg, NULL, 0);
+                       break;
+               case 'R':
+                       start_delay = strtol(optarg, NULL, 0);
+                       break;
+               case 'T':
+                       stop_delay = strtol(optarg, NULL, 0);
+                       break;
+               case 'M':
+                       mmap_flag = 1;
+                       break;
+               case 'I':
+                       interleaved = 0;
+                       break;
+               case 'P':
+                       stream = SND_PCM_STREAM_PLAYBACK;
+                       command = "aplay";
+                       break;
+               case OPT_DISABLE_RESAMPLE:
+                       open_mode |= SND_PCM_NO_AUTO_RESAMPLE;
+                       break;
+               case OPT_DISABLE_CHANNELS:
+                       open_mode |= SND_PCM_NO_AUTO_CHANNELS;
+                       break;
+               case OPT_DISABLE_FORMAT:
+                       open_mode |= SND_PCM_NO_AUTO_FORMAT;
+                       break;
+               case OPT_DISABLE_SOFTVOL:
+                       open_mode |= SND_PCM_NO_SOFTVOL;
+                       break;
+               case OPT_TEST_POSITION:
+                       test_position = 1;
+                       break;
+               case OPT_TEST_COEF:
+                       test_coef = strtol(optarg, NULL, 0);
+                       if (test_coef < 1)
+                               test_coef = 1;
+                       break;
+               case OPT_TEST_NOWAIT:
+                       test_nowait = 1;
+                       break;
+               default:
+                       fprintf(stderr, _("Try `%s --help' for more information.\n"), command);
+                       return 1;
+               }
+       }
+
+       if (do_device_list) {
+               if (do_pcm_list) pcm_list();
+               device_list();
+               goto __end;
+       } else if (do_pcm_list) {
+               pcm_list();
+               goto __end;
+       }
+
+       err = snd_pcm_open(&handle, pcm_name, stream, open_mode);
+       if (err < 0) {
+               error(_("audio open error: %s"), snd_strerror(err));
+               return 1;
+       }
+
+       if ((err = snd_pcm_info(handle, info)) < 0) {
+               error(_("info error: %s"), snd_strerror(err));
+               return 1;
+       }
+
+       if (nonblock) {
+               err = snd_pcm_nonblock(handle, 1);
+               if (err < 0) {
+                       error(_("nonblock setting error: %s"), snd_strerror(err));
+                       return 1;
+               }
+       }
+
+       chunk_size = 1024;
+       hwparams = rhwparams;
+
+       audiobuf = (u_char *)malloc(1024);
+       if (audiobuf == NULL) {
+               error(_("not enough memory"));
+               return 1;
+       }
+
+       if (mmap_flag) {
+               writei_func = snd_pcm_mmap_writei;
+               readi_func = snd_pcm_mmap_readi;
+               writen_func = snd_pcm_mmap_writen;
+               readn_func = snd_pcm_mmap_readn;
+       } else {
+               writei_func = snd_pcm_writei;
+               readi_func = snd_pcm_readi;
+               writen_func = snd_pcm_writen;
+               readn_func = snd_pcm_readn;
+       }
+
+
+       signal(SIGINT, signal_handler);
+       signal(SIGTERM, signal_handler);
+       signal(SIGABRT, signal_handler);
+       playback(argv[optind++]);
+       snd_pcm_close(handle);
+       free(audiobuf);
+      __end:
+       snd_output_close(log);
+       snd_config_update_free_global();
+       return EXIT_SUCCESS;
+}
+
+/*
+ * Safe read (for pipes)
+ */
+static ssize_t safe_read(int fd, void *buf, size_t count)
+{
+       ssize_t result = 0, res;
+
+       while (count > 0) {
+               if ((res = read(fd, buf, count)) == 0)
+                       break;
+               if (res < 0)
+                       return result > 0 ? result : res;
+               count -= res;
+               result += res;
+               buf = (char *)buf + res;
+       }
+       return result;
+}
+
+
+/*
+ * helper for test_wavefile
+ */
+
+static size_t test_wavefile_read(int fd, u_char *buffer, size_t *size, size_t reqsize, int line)
+{
+       if (*size >= reqsize)
+               return *size;
+       if ((size_t)safe_read(fd, buffer + *size, reqsize - *size) != reqsize - *size) {
+               error(_("read error (called from line %i)"), line);
+               exit(EXIT_FAILURE);
+       }
+       return *size = reqsize;
+}
+
+#define check_wavefile_space(buffer, len, blimit) \
+       if (len > blimit) { \
+               blimit = len; \
+               if ((buffer = realloc(buffer, blimit)) == NULL) { \
+                       error(_("not enough memory"));            \
+                       exit(EXIT_FAILURE); \
+               } \
+       }
+
+/*
+ * test, if it's a .WAV file, > 0 if ok (and set the speed, stereo etc.)
+ *                            == 0 if not
+ * Value returned is bytes to be discarded.
+ */
+static ssize_t test_wavefile(int fd, u_char *_buffer, size_t size)
+{
+       WaveHeader *h = (WaveHeader *)_buffer;
+       u_char *buffer = NULL;
+       size_t blimit = 0;
+       WaveFmtBody *f;
+       WaveChunkHeader *c;
+       u_int type, len;
+
+       if (size < sizeof(WaveHeader))
+               return -1;
+       if (h->magic != WAV_RIFF || h->type != WAV_WAVE)
+               return -1;
+       if (size > sizeof(WaveHeader)) {
+               check_wavefile_space(buffer, size - sizeof(WaveHeader), blimit);
+               memcpy(buffer, _buffer + sizeof(WaveHeader), size - sizeof(WaveHeader));
+       }
+       size -= sizeof(WaveHeader);
+       while (1) {
+               check_wavefile_space(buffer, sizeof(WaveChunkHeader), blimit);
+               test_wavefile_read(fd, buffer, &size, sizeof(WaveChunkHeader), __LINE__);
+               c = (WaveChunkHeader*)buffer;
+               type = c->type;
+               len = LE_INT(c->length);
+               len += len % 2;
+               if (size > sizeof(WaveChunkHeader))
+                       memmove(buffer, buffer + sizeof(WaveChunkHeader), size - sizeof(WaveChunkHeader));
+               size -= sizeof(WaveChunkHeader);
+               if (type == WAV_FMT)
+                       break;
+               check_wavefile_space(buffer, len, blimit);
+               test_wavefile_read(fd, buffer, &size, len, __LINE__);
+               if (size > len)
+                       memmove(buffer, buffer + len, size - len);
+               size -= len;
+       }
+
+       if (len < sizeof(WaveFmtBody)) {
+               error(_("unknown length of 'fmt ' chunk (read %u, should be %u at least)"),
+                     len, (u_int)sizeof(WaveFmtBody));
+               exit(EXIT_FAILURE);
+       }
+       check_wavefile_space(buffer, len, blimit);
+       test_wavefile_read(fd, buffer, &size, len, __LINE__);
+       f = (WaveFmtBody*) buffer;
+       if (LE_SHORT(f->format) == WAV_FMT_EXTENSIBLE) {
+               WaveFmtExtensibleBody *fe = (WaveFmtExtensibleBody*)buffer;
+               if (len < sizeof(WaveFmtExtensibleBody)) {
+                       error(_("unknown length of extensible 'fmt ' chunk (read %u, should be %u at least)"),
+                                       len, (u_int)sizeof(WaveFmtExtensibleBody));
+                       exit(EXIT_FAILURE);
+               }
+               if (memcmp(fe->guid_tag, WAV_GUID_TAG, 14) != 0) {
+                       error(_("wrong format tag in extensible 'fmt ' chunk"));
+                       exit(EXIT_FAILURE);
+               }
+               f->format = fe->guid_format;
+       }
+        if (LE_SHORT(f->format) != WAV_FMT_PCM &&
+            LE_SHORT(f->format) != WAV_FMT_IEEE_FLOAT) {
+                error(_("can't play WAVE-file format 0x%04x which is not PCM or FLOAT encoded"), LE_SHORT(f->format));
+               exit(EXIT_FAILURE);
+       }
+       if (LE_SHORT(f->channels) < 1) {
+               error(_("can't play WAVE-files with %d tracks"), LE_SHORT(f->channels));
+               exit(EXIT_FAILURE);
+       }
+       hwparams.channels = LE_SHORT(f->channels);
+       switch (LE_SHORT(f->bit_p_spl)) {
+       case 8:
+               if (hwparams.format != DEFAULT_FORMAT &&
+                   hwparams.format != SND_PCM_FORMAT_U8)
+                       fprintf(stderr, _("Warning: format is changed to U8\n"));
+               hwparams.format = SND_PCM_FORMAT_U8;
+               break;
+       case 16:
+               if (hwparams.format != DEFAULT_FORMAT &&
+                   hwparams.format != SND_PCM_FORMAT_S16_LE)
+                       fprintf(stderr, _("Warning: format is changed to S16_LE\n"));
+               hwparams.format = SND_PCM_FORMAT_S16_LE;
+               break;
+       case 24:
+               switch (LE_SHORT(f->byte_p_spl) / hwparams.channels) {
+               case 3:
+                       if (hwparams.format != DEFAULT_FORMAT &&
+                           hwparams.format != SND_PCM_FORMAT_S24_3LE)
+                               fprintf(stderr, _("Warning: format is changed to S24_3LE\n"));
+                       hwparams.format = SND_PCM_FORMAT_S24_3LE;
+                       break;
+               case 4:
+                       if (hwparams.format != DEFAULT_FORMAT &&
+                           hwparams.format != SND_PCM_FORMAT_S24_LE)
+                               fprintf(stderr, _("Warning: format is changed to S24_LE\n"));
+                       hwparams.format = SND_PCM_FORMAT_S24_LE;
+                       break;
+               default:
+                       error(_(" can't play WAVE-files with sample %d bits in %d bytes wide (%d channels)"),
+                             LE_SHORT(f->bit_p_spl), LE_SHORT(f->byte_p_spl), hwparams.channels);
+                       exit(EXIT_FAILURE);
+               }
+               break;
+       case 32:
+                if (LE_SHORT(f->format) == WAV_FMT_PCM)
+                        hwparams.format = SND_PCM_FORMAT_S32_LE;
+                else if (LE_SHORT(f->format) == WAV_FMT_IEEE_FLOAT)
+                        hwparams.format = SND_PCM_FORMAT_FLOAT_LE;
+               break;
+       default:
+               error(_(" can't play WAVE-files with sample %d bits wide"),
+                     LE_SHORT(f->bit_p_spl));
+               exit(EXIT_FAILURE);
+       }
+       hwparams.rate = LE_INT(f->sample_fq);
+       
+       if (size > len)
+               memmove(buffer, buffer + len, size - len);
+       size -= len;
+       
+       while (1) {
+               u_int type, len;
+
+               check_wavefile_space(buffer, sizeof(WaveChunkHeader), blimit);
+               test_wavefile_read(fd, buffer, &size, sizeof(WaveChunkHeader), __LINE__);
+               c = (WaveChunkHeader*)buffer;
+               type = c->type;
+               len = LE_INT(c->length);
+               if (size > sizeof(WaveChunkHeader))
+                       memmove(buffer, buffer + sizeof(WaveChunkHeader), size - sizeof(WaveChunkHeader));
+               size -= sizeof(WaveChunkHeader);
+               if (type == WAV_DATA) {
+                       if (len < pbrec_count && len < 0x7ffffffe)
+                               pbrec_count = len;
+                       if (size > 0)
+                               memcpy(_buffer, buffer, size);
+                       free(buffer);
+                       return size;
+               }
+               len += len % 2;
+               check_wavefile_space(buffer, len, blimit);
+               test_wavefile_read(fd, buffer, &size, len, __LINE__);
+               if (size > len)
+                       memmove(buffer, buffer + len, size - len);
+               size -= len;
+       }
+
+       /* shouldn't be reached */
+       return -1;
+}
+
+
+static void set_params(void)
+{
+       snd_pcm_hw_params_t *params;
+       snd_pcm_sw_params_t *swparams;
+       snd_pcm_uframes_t buffer_size;
+       int err;
+       size_t n;
+       unsigned int rate;
+       snd_pcm_uframes_t start_threshold, stop_threshold;
+       snd_pcm_hw_params_alloca(&params);
+       snd_pcm_sw_params_alloca(&swparams);
+       err = snd_pcm_hw_params_any(handle, params);
+       if (err < 0) {
+               error(_("Broken configuration for this PCM: no configurations available"));
+               exit(EXIT_FAILURE);
+       }
+       if (mmap_flag) {
+               snd_pcm_access_mask_t *mask = alloca(snd_pcm_access_mask_sizeof());
+               snd_pcm_access_mask_none(mask);
+               snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_MMAP_INTERLEAVED);
+               snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_MMAP_NONINTERLEAVED);
+               snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_MMAP_COMPLEX);
+               err = snd_pcm_hw_params_set_access_mask(handle, params, mask);
+       } else if (interleaved)
+               err = snd_pcm_hw_params_set_access(handle, params,
+                                                  SND_PCM_ACCESS_RW_INTERLEAVED);
+       else
+               err = snd_pcm_hw_params_set_access(handle, params,
+                                                  SND_PCM_ACCESS_RW_NONINTERLEAVED);
+       if (err < 0) {
+               error(_("Access type not available"));
+               exit(EXIT_FAILURE);
+       }
+       err = snd_pcm_hw_params_set_format(handle, params, hwparams.format);
+       if (err < 0) {
+               error(_("Sample format non available"));
+               exit(EXIT_FAILURE);
+       }
+       err = snd_pcm_hw_params_set_channels(handle, params, hwparams.channels);
+       if (err < 0) {
+               error(_("Channels count non available"));
+               exit(EXIT_FAILURE);
+       }
+
+#if 0
+       err = snd_pcm_hw_params_set_periods_min(handle, params, 2);
+       assert(err >= 0);
+#endif
+       rate = hwparams.rate;
+       err = snd_pcm_hw_params_set_rate_near(handle, params, &hwparams.rate, 0);
+       assert(err >= 0);
+       if ((float)rate * 1.05 < hwparams.rate || (float)rate * 0.95 > hwparams.rate) {
+               if (!quiet_mode) {
+                       char plugex[64];
+                       const char *pcmname = snd_pcm_name(handle);
+                       fprintf(stderr, _("Warning: rate is not accurate (requested = %iHz, got = %iHz)\n"), rate, hwparams.rate);
+                       if (! pcmname || strchr(snd_pcm_name(handle), ':'))
+                               *plugex = 0;
+                       else
+                               snprintf(plugex, sizeof(plugex), "(-Dplug:%s)",
+                                        snd_pcm_name(handle));
+                       fprintf(stderr, _("         please, try the plug plugin %s\n"),
+                               plugex);
+               }
+       }
+       rate = hwparams.rate;
+       if (buffer_time == 0 && buffer_frames == 0) {
+               err = snd_pcm_hw_params_get_buffer_time_max(params,
+                                                           &buffer_time, 0);
+               assert(err >= 0);
+               if (buffer_time > 500000)
+                       buffer_time = 500000;
+       }
+       if (period_time == 0 && period_frames == 0) {
+               if (buffer_time > 0)
+                       period_time = buffer_time / 4;
+               else
+                       period_frames = buffer_frames / 4;
+       }
+       if (period_time > 0)
+               err = snd_pcm_hw_params_set_period_time_near(handle, params,
+                                                            &period_time, 0);
+       else
+               err = snd_pcm_hw_params_set_period_size_near(handle, params,
+                                                            &period_frames, 0);
+       assert(err >= 0);
+       if (buffer_time > 0) {
+               err = snd_pcm_hw_params_set_buffer_time_near(handle, params,
+                                                            &buffer_time, 0);
+       } else {
+               err = snd_pcm_hw_params_set_buffer_size_near(handle, params,
+                                                            &buffer_frames);
+       }
+       assert(err >= 0);
+       monotonic = snd_pcm_hw_params_is_monotonic(params);
+       err = snd_pcm_hw_params(handle, params);
+       if (err < 0) {
+               error(_("Unable to install hw params:"));
+               snd_pcm_hw_params_dump(params, log);
+               exit(EXIT_FAILURE);
+       }
+       snd_pcm_hw_params_get_period_size(params, &chunk_size, 0);
+       snd_pcm_hw_params_get_buffer_size(params, &buffer_size);
+       if (chunk_size == buffer_size) {
+               error(_("Can't use period equal to buffer size (%lu == %lu)"),
+                     chunk_size, buffer_size);
+               exit(EXIT_FAILURE);
+       }
+       snd_pcm_sw_params_current(handle, swparams);
+       if (avail_min < 0)
+               n = chunk_size;
+       else
+               n = (double) rate * avail_min / 1000000;
+       err = snd_pcm_sw_params_set_avail_min(handle, swparams, n);
+
+       /* round up to closest transfer boundary */
+       n = buffer_size;
+       if (start_delay <= 0) {
+               start_threshold = n + (double) rate * start_delay / 1000000;
+       } else
+               start_threshold = (double) rate * start_delay / 1000000;
+       if (start_threshold < 1)
+               start_threshold = 1;
+       if (start_threshold > n)
+               start_threshold = n;
+       err = snd_pcm_sw_params_set_start_threshold(handle, swparams, start_threshold);
+       assert(err >= 0);
+       if (stop_delay <= 0) 
+               stop_threshold = buffer_size + (double) rate * stop_delay / 1000000;
+       else
+               stop_threshold = (double) rate * stop_delay / 1000000;
+       err = snd_pcm_sw_params_set_stop_threshold(handle, swparams, stop_threshold);
+       assert(err >= 0);
+
+       if (snd_pcm_sw_params(handle, swparams) < 0) {
+               error(_("unable to install sw params:"));
+               snd_pcm_sw_params_dump(swparams, log);
+               exit(EXIT_FAILURE);
+       }
+
+       if (verbose)
+               snd_pcm_dump(handle, log);
+
+       bits_per_sample = snd_pcm_format_physical_width(hwparams.format);
+       bits_per_frame = bits_per_sample * hwparams.channels;
+       chunk_bytes = chunk_size * bits_per_frame / 8;
+       audiobuf = realloc(audiobuf, chunk_bytes);
+       if (audiobuf == NULL) {
+               error(_("not enough memory"));
+               exit(EXIT_FAILURE);
+       }
+       // fprintf(stderr, "real chunk_size = %i, frags = %i, total = %i\n", chunk_size, setup.buf.block.frags, setup.buf.block.frags * chunk_size);
+
+
+       /* show mmap buffer arragment */
+       if (mmap_flag && verbose) {
+               const snd_pcm_channel_area_t *areas;
+               snd_pcm_uframes_t offset;
+               int i;
+               err = snd_pcm_mmap_begin(handle, &areas, &offset, &chunk_size);
+               if (err < 0) {
+                       error("snd_pcm_mmap_begin problem: %s", snd_strerror(err));
+                       exit(EXIT_FAILURE);
+               }
+               for (i = 0; i < hwparams.channels; i++)
+                       fprintf(stderr, "mmap_area[%i] = %p,%u,%u (%u)\n", i, areas[i].addr, areas[i].first, areas[i].step, snd_pcm_format_physical_width(hwparams.format));
+               /* not required, but for sure */
+               snd_pcm_mmap_commit(handle, offset, 0);
+       }
+
+       buffer_frames = buffer_size;    /* for position test */
+}
+
+#ifndef timersub
+#define        timersub(a, b, result) \
+do { \
+       (result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \
+       (result)->tv_usec = (a)->tv_usec - (b)->tv_usec; \
+       if ((result)->tv_usec < 0) { \
+               --(result)->tv_sec; \
+               (result)->tv_usec += 1000000; \
+       } \
+} while (0)
+#endif
+
+#ifndef timermsub
+#define        timermsub(a, b, result) \
+do { \
+       (result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \
+       (result)->tv_nsec = (a)->tv_nsec - (b)->tv_nsec; \
+       if ((result)->tv_nsec < 0) { \
+               --(result)->tv_sec; \
+               (result)->tv_nsec += 1000000000L; \
+       } \
+} while (0)
+#endif
+
+/* I/O error handler */
+static void xrun(void)
+{
+       snd_pcm_status_t *status;
+       int res;
+       
+       snd_pcm_status_alloca(&status);
+       if ((res = snd_pcm_status(handle, status))<0) {
+               error(_("status error: %s"), snd_strerror(res));
+               exit(EXIT_FAILURE);
+       }
+       if (snd_pcm_status_get_state(status) == SND_PCM_STATE_XRUN) {
+               if (monotonic) {
+#ifdef HAVE_CLOCK_GETTIME
+                       struct timespec now, diff, tstamp;
+                       clock_gettime(CLOCK_MONOTONIC, &now);
+                       snd_pcm_status_get_trigger_htstamp(status, &tstamp);
+                       timermsub(&now, &tstamp, &diff);
+                       fprintf(stderr, _("%s!!! (at least %.3f ms long)\n"),
+                               stream == SND_PCM_STREAM_PLAYBACK ? _("underrun") : _("overrun"),
+                               diff.tv_sec * 1000 + diff.tv_nsec / 10000000.0);
+#else
+                       fprintf(stderr, "%s !!!\n", _("underrun"));
+#endif
+               } else {
+                       struct timeval now, diff, tstamp;
+                       gettimeofday(&now, 0);
+                       snd_pcm_status_get_trigger_tstamp(status, &tstamp);
+                       timersub(&now, &tstamp, &diff);
+                       fprintf(stderr, _("%s!!! (at least %.3f ms long)\n"),
+                               stream == SND_PCM_STREAM_PLAYBACK ? _("underrun") : _("overrun"),
+                               diff.tv_sec * 1000 + diff.tv_usec / 1000.0);
+               }
+               if (verbose) {
+                       fprintf(stderr, _("Status:\n"));
+                       snd_pcm_status_dump(status, log);
+               }
+               if ((res = snd_pcm_prepare(handle))<0) {
+                       error(_("xrun: prepare error: %s"), snd_strerror(res));
+                       exit(EXIT_FAILURE);
+               }
+               return;         /* ok, data should be accepted again */
+       } if (snd_pcm_status_get_state(status) == SND_PCM_STATE_DRAINING) {
+               if (verbose) {
+                       fprintf(stderr, _("Status(DRAINING):\n"));
+                       snd_pcm_status_dump(status, log);
+               }
+       }
+       if (verbose) {
+               fprintf(stderr, _("Status(R/W):\n"));
+               snd_pcm_status_dump(status, log);
+       }
+       error(_("read/write error, state = %s"), snd_pcm_state_name(snd_pcm_status_get_state(status)));
+       exit(EXIT_FAILURE);
+}
+
+/* I/O suspend handler */
+static void suspend(void)
+{
+       int res;
+
+       if (!quiet_mode)
+               fprintf(stderr, _("Suspended. Trying resume. ")); fflush(stderr);
+       while ((res = snd_pcm_resume(handle)) == -EAGAIN)
+               sleep(1);       /* wait until suspend flag is released */
+       if (res < 0) {
+               if (!quiet_mode)
+                       fprintf(stderr, _("Failed. Restarting stream. ")); fflush(stderr);
+               if ((res = snd_pcm_prepare(handle)) < 0) {
+                       error(_("suspend: prepare error: %s"), snd_strerror(res));
+                       exit(EXIT_FAILURE);
+               }
+       }
+       if (!quiet_mode)
+               fprintf(stderr, _("Done.\n"));
+}
+
+
+
+
+static void do_test_position(void)
+{
+       static long counter = 0;
+       static time_t tmr = -1;
+       time_t now;
+       static float availsum, delaysum, samples;
+       static snd_pcm_sframes_t maxavail, maxdelay;
+       static snd_pcm_sframes_t minavail, mindelay;
+       static snd_pcm_sframes_t badavail = 0, baddelay = 0;
+       snd_pcm_sframes_t outofrange;
+       snd_pcm_sframes_t avail, delay;
+       int err;
+
+       err = snd_pcm_avail_delay(handle, &avail, &delay);
+       if (err < 0)
+               return;
+       outofrange = (test_coef * (snd_pcm_sframes_t)buffer_frames) / 2;
+       if (avail > outofrange || avail < -outofrange ||
+           delay > outofrange || delay < -outofrange) {
+         badavail = avail; baddelay = delay;
+         availsum = delaysum = samples = 0;
+         maxavail = maxdelay = 0;
+         minavail = mindelay = buffer_frames * 16;
+         fprintf(stderr, _("Suspicious buffer position (%li total): "
+               "avail = %li, delay = %li, buffer = %li\n"),
+               ++counter, (long)avail, (long)delay, (long)buffer_frames);
+       } else if (verbose) {
+               time(&now);
+               if (tmr == (time_t) -1) {
+                       tmr = now;
+                       availsum = delaysum = samples = 0;
+                       maxavail = maxdelay = 0;
+                       minavail = mindelay = buffer_frames * 16;
+               }
+               if (avail > maxavail)
+                       maxavail = avail;
+               if (delay > maxdelay)
+                       maxdelay = delay;
+               if (avail < minavail)
+                       minavail = avail;
+               if (delay < mindelay)
+                       mindelay = delay;
+               availsum += avail;
+               delaysum += delay;
+               samples++;
+               if (avail != 0 && now != tmr) {
+                       fprintf(stderr, "BUFPOS: avg%li/%li "
+                               "min%li/%li max%li/%li (%li) (%li:%li/%li)\n",
+                               (long)(availsum / samples),
+                               (long)(delaysum / samples),
+                               (long)minavail, (long)mindelay,
+                               (long)maxavail, (long)maxdelay,
+                               (long)buffer_frames,
+                               counter, badavail, baddelay);
+                       tmr = now;
+               }
+       }
+}
+
+/*
+ *  write function
+ */
+
+static ssize_t pcm_write(u_char *data, size_t count)
+{
+       ssize_t r;
+       ssize_t result = 0;
+
+       if (count < chunk_size) {
+               snd_pcm_format_set_silence(hwparams.format, data + count * bits_per_frame / 8, (chunk_size - count) * hwparams.channels);
+               count = chunk_size;
+       }
+       while (count > 0) {
+               if (test_position)
+                       do_test_position();
+               r = writei_func(handle, data, count);
+               if (test_position)
+                       do_test_position();
+               if (r == -EAGAIN || (r >= 0 && (size_t)r < count)) {
+                       if (!test_nowait)
+                               snd_pcm_wait(handle, 1000);
+               } else if (r == -EPIPE) {
+                       xrun();
+               } else if (r == -ESTRPIPE) {
+                       suspend();
+               } else if (r < 0) {
+                       error(_("write error: %s"), snd_strerror(r));
+                       exit(EXIT_FAILURE);
+               }
+               if (r > 0) {
+                       result += r;
+                       count -= r;
+                       data += r * bits_per_frame / 8;
+               }
+       }
+       return result;
+}
+
+
+/* playing raw data */
+
+static void playback_go(int fd, size_t loaded, off64_t count, int rtype, char *name)
+{
+       int l, r;
+       off64_t written = 0;
+       off64_t c;
+
+       set_params();
+
+       while (loaded > chunk_bytes && written < count) {
+               if (pcm_write(audiobuf + written, chunk_size) <= 0)
+                       return;
+               written += chunk_bytes;
+               loaded -= chunk_bytes;
+       }
+       if (written > 0 && loaded > 0)
+               memmove(audiobuf, audiobuf + written, loaded);
+
+       l = loaded;
+       while (written < count) {
+               do {
+                       c = count - written;
+                       if (c > chunk_bytes)
+                               c = chunk_bytes;
+                       c -= l;
+
+                       if (c == 0)
+                               break;
+                       r = safe_read(fd, audiobuf + l, c);
+                       if (r < 0) {
+                               perror(name);
+                               exit(EXIT_FAILURE);
+                       }
+                       fdcount += r;
+                       if (r == 0)
+                               break;
+                       l += r;
+               } while ((size_t)l < chunk_bytes);
+               l = l * 8 / bits_per_frame;
+               r = pcm_write(audiobuf, l);
+               if (r != l)
+                       break;
+               r = r * bits_per_frame / 8;
+               written += r;
+               l = 0;
+       }
+       snd_pcm_nonblock(handle, 0);
+       snd_pcm_drain(handle);
+       snd_pcm_nonblock(handle, nonblock);
+}
+
+
+/*
+ *  let's play it
+ */
+
+static void playback(char *name)
+{
+       int ofs;
+       size_t dta;
+       ssize_t dtawave;
+
+       pbrec_count = LLONG_MAX;
+       fdcount = 0;
+       if ((fd = open64(name, O_RDONLY, 0)) == -1) {
+               perror(name);
+               exit(EXIT_FAILURE);
+       }
+       /* read bytes for WAVE-header */
+       if ((dtawave = test_wavefile(fd, audiobuf, dta)) >= 0) {
+               playback_go(fd, dtawave, pbrec_count, FORMAT_WAVE, name);
+       }
+       close(fd);
+}
+
+struct sound {
+       int fd;
+       int empty;
+       struct list_head list;
+       int seen;
+       char *name;
+       int ino;
+       long posn;
+       int format; /* FORMAT_WAVE or FORMAT_OGG */
+       char buf[1024];
+       int bytes, bytes_used;
+       int eof;
+
+       int chunk_size;
+       int chunk_bytes;
+
+};
+
+int dir_changed = 1;
+
+int handle_change(int sig)
+{
+       dir_changed = 1;
+       return 0;
+}
+
+static void raw_read(struct sound *s)
+{
+       /* if there are bytes in the buffer but not at the start,
+        * copy them down.
+        * then try to fill the buffer.
+        * Set ->eof as appropriate
+        */
+       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 = 0;
+       while (s->bytes < sizeof(s->buf) && !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;
+       }
+}
+
+int parse_wave(struct sound *s)
+{
+       WaveHeader *h = (WaveHeader *)s->buf;
+       WaveChunkHeader *c;
+       WaveFmtBody *f;
+       int n;
+
+       if (s->bytes < sizeof(WaveHeader))
+               return 0;
+       if (h->magic != WAV_RIFF || h->type != WAV_WAVE)
+               return 0;
+       s->bytes_used = sizeof(WaveHeader);
+       raw_read(s);
+       while (1) {
+               n = 0;
+               c = (WaveChunkHeader*) s->buf;
+               len = LE_INT(c->length);
+               len += len % 2;
+               n += sizeof(WaveChunkHeader);
+               if (c->type == WAV_FMT)
+                       break;
+               n += len;
+               s->bytes_used = n;
+               raw_read(s);
+       }
+       if (len < sizeof(WaveFmtBody))
+               return 0;
+       f = (WaveFmtBody*)s->buf;
+       
+}
+
+void play_some(snd_pcm_t *handle, struct sound *sound)
+{
+       if (!handle || !sound)
+               return;
+
+       switch(sound->format) {
+       case FORMAT_WAVE:
+               read_wave(sound);
+               break;
+       default:
+               sound->eof = 1;
+       }
+       if (sound->bytes > sound->chunk_bytes || sound->eof) {
+               r = pcm_write(sound->buf, 
+                             sound->bytes > sound->chunk_bytes
+                             ? sound->chunk_bytes:
+                             : sound->bytes);
+               sound->bytes_used = r;
+       }
+}
+
+
+struct sound *open_sound(char *name, int ino)
+{
+       char path[200];
+       int fd;
+       struct sound *s;
+       char *eos;
+       strcpy(path, "/var/run/sound");
+       strcat(path, name);
+       fd = open(path, O_RDONLY);
+       if (fd < 0)
+               return NULL;
+       s = malloc(sizeof(*s));
+       if (!s)
+               return NULL;
+       s->fd = fd;
+       s->empty = 0;
+       s->seen = 0;
+       s->name = strdup(name);
+       s->ino = ino;
+       s->posn = 0;
+       s->bytes = s->bytes_used = 0;
+
+       if (lseek(fd, 0L, 2) == 0) {
+               close(fd);
+               s->fd = -1;
+               s->empty = 1;
+               return s;
+       }
+       /* check for millisecond suffix */
+       eos = name + strlen(name);
+       while (eos > name && is_digit(eos[-1]))
+               eos--;
+       if (eos > name && eos[-1] == '-' && eos[0])
+               s->posn = atol(eos);
+       /* Read header and set parameters */
+
+       raw_read(s);
+       if (parse_wave(s))
+               s->format = FORMAT_WAVE;
+       else
+               s->format = FORMAT_UNKNOWN;
+
+       if (s->posn)
+               switch(s->format) {
+               case FORMAT_WAVE:
+                       seek_wave(s, s->posn);
+               }
+
+       return s;
+
+ fail:
+       close(s->fd);
+       free(s->name);
+       free(s);
+       return NULL;
+}
+
+
+struct list_head *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 (Which might be head)
+        * and clear matched.
+        */
+       struct list_head *rv = list;
+       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->list;
+               if (c == 0) {
+                       if (s->ino == ino)
+                               *matched = 1;
+                       break;
+               }
+       }
+       return rv;
+}
+
+void scan_dir(int fd, struct list_head *soundqueue)
+{
+       DIR *dir;
+       struct dirent *de;
+       struct sound *match;
+
+       list_for_each_entry(match, soundqueue, list)
+               match->seen = 0;
+
+       lseek(fd, 0, 0);
+       dir = fdopendir(dup(fd));
+       while ((de = readdir(dir)) != NULL) {
+               struct list_head *match;
+               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(de->d_name, de->d_ino);
+               if (! new)
+                       continue;
+               list_add(&new->list, match);
+       }
+       closedir(dir);
+       
+       list_for_each_entry_safe(match, pos, soundqueue, list)
+               if (!match->seen) {
+                       list_del(&match->list);
+                       close_sound(match);
+               }
+}
+
+int main(int argc, char *argv[])
+{
+       int dfd;
+       struct sound *last = NULL;
+       struct list_head *soundqueue;
+       snd_pcm_t *handle = NULL;
+
+       INIT_LIST_HEAD(&soundqueue);
+
+       mkdir("/var/run/sound");
+       dfd = open("/var/run/sound", O_RDONLY|O_DIRECTORY);
+       if (dfd < 0) {
+               fprintf(stderr, "sound: Cannot open /var/run/sound\n");
+               exit(1);
+       }
+       signal(SIGIO, handle_change);
+
+       while (1) {
+               sigblock(IOmask);
+               if (dir_changed) {
+                       fcntl(dfd, F_NOTIFY, DN_CREATE|DN_DELETE|DN_RENAME);
+                       dir_changed = 0;
+                       scan_dir(dfd, &soundqueue);
+               }
+
+               if (list_empty(&soundqueue))
+                       sigsuspend(empty_mask);
+               else {
+                       struct sound *next = list_entry(soundqueue.next,
+                                                       struct sound, list);
+                       if (next != last) {
+                               if (handle == NULL)
+                                       open_dev(&handle);
+                               else {
+                                       snd_pcm_nonblock(handle, 0);
+                                       snd_pcm_drain(handle);
+                                       snd_pcm_nonblock(handle, nonblock);
+                               }
+                               set_params(handle, next);
+                               last = next;
+                       }
+                       if (next->empty) {
+                               sigsuspend(empty_mask);
+                               continue;
+                       }
+                       playsome(handle, next);
+               }
+               sigunblock(IOmask);
+       }
+       exit(0);
+}
diff --git a/sounds/sounds.py b/sounds/sounds.py
new file mode 100644 (file)
index 0000000..d1f0013
--- /dev/null
@@ -0,0 +1,263 @@
+#!/usr/bin/env python
+#
+# This is a sound playing daemon for the freerunner.
+# It watches the directory "/var/run/sound" and when a file appears
+# there-in, it gets played.
+# Currently the file must be a WAV file with 16 bit little-endian PCM encoding.
+#
+# Files can (and should) have priorities being leading digits.
+# If there are multiple files, the one with the lowest number is played.
+#
+# Files normally appear via the creation of symlinks.
+# When the file is removed, the playing stops.  When a new file of higher
+# priority appears, the current file is suspended.  When the higher
+# priority file is removed, the lower priority one resumes.
+#
+# When a file finishes playing it can do one of several thing:
+#  - the file can be removed (R)
+#  - the file can be replayed (P)
+#  - the player can stop and wait (W)
+# If the file has a timestamp (on symlink) that has changed since
+# play started, it is treated as a new file by 'W'.
+# An empty file produces silence.
+#
+# File names should start with 1 or more leading digits (if there are
+# no digits the effective priority is infinite).  These form a number
+# which is the reverse of priority, so a small number is played first.
+# This a numerical sequence will be played in order.  After these
+# digits should be an R, P, or W.  If none of these are present, R is
+# assumed.
+#
+# The player can be stopped by creating a symlink from '0P-silence' to
+# '/dev/null'.
+#
+# When a file is suspending, the position in the file, in microseconds
+# is written to a new file with name formed by putting a '.' at the
+# start of the file name.  Maybe this is continuously updated...
+#
+
+
+import alsaaudio, time, struct, sys, os, signal, fcntl
+
+class PlayFile():
+    def __init__(self, file, pcm):
+        # Arrange to play file through pcm
+        # Every time .play is called, we play some of the file
+        # If something else gets played, .resume must be called
+        # before .play is called again
+        self.pcm = pcm
+        self.filename = file
+        self.posfile = os.path.join(os.path.dirname(file),
+                                    '.'+os.path.basename(file))
+        self.loadfile(file)
+        self.update()
+
+    def loadfile(self, file):
+        # A wav file starts:
+        #   0-3  "RIFF"
+        #   4-7  Bytes in rest of file.
+        #   8-11 "WAVE"
+        #  12-15 "fmt "
+        #  16-19 bytes of format
+        #  20-21 ==1  Microsoft PCM
+        #  22-23      channels
+        #  24-27  freq
+        #  28-31  byte rate
+        #  32-33  bytes per frame
+        #  34-35  bits per sample
+        #  36-39 "data"
+        #  40-43 number of bytes of data
+        #  44... actual samples
+        self.pos = 0
+        self.rate = 8000
+        self.channels = 1
+        self.bytes = 2
+        self.format = alsaaudio.PCM_FORMAT_S16_LE
+        try:
+            self.f = open(file)
+        except IOError:
+            self.f = None
+            return
+        header = self.f.read(44)
+        if len(header) == 0:
+            # silence
+            return
+        if len(header) != 44:
+            raise IOError
+        riff, b1, wave, fmt, b2, format, chan, rate, br, bf, bs, data, b3 = \
+              struct.unpack("4si4s 4sihhiihh 4si", header)
+
+        if riff != "RIFF" or wave != "WAVE" or fmt != "fmt " or data != "data":
+            raise ValueError
+        if format == 1 and bs == 16:
+            self.format = alsaaudio.PCM_FORMAT_S16_LE
+            self.bytes = 2
+        elif format == 1 and bs == 8:
+            self.format = alsaaudio.PCM_FORMAT_U8
+            self.bytes = 1
+        else:
+            raise ValueError
+
+        if chan < 1 or chan > 4:
+            raise ValueError
+        else:
+            self.channels = chan
+
+        self.rate = rate
+        self.finished = False
+        self.pos = 0;
+        self.resume()
+
+    def resume(self):
+        try:
+            self.pcm.setformat(self.format)
+            self.pcm.setchannels(self.channels)
+            self.pcm.setrate(self.rate)
+            self.pcm.setperiodsize(640 / self.channels / self.bytes)
+        except:
+            pass
+
+    def update(self):
+        f = open(self.posfile, 'w')
+        f.write("%d\n" % int(self.pos*1000000 / self.rate))
+
+    def play(self):
+        # play for at least 100ms
+        start = time.time()
+        if not self.f:
+            return False
+        while time.time() < start + 0.1:
+            data = self.f.read(640)
+            if not data:
+                self.finished = True
+                try:
+                    os.unlink(self.posfile)
+                except OSError:
+                    pass
+                return False
+            if len(data) % (self.channels * self.bytes) == 0:
+                self.pcm.write(data)
+                if len(data) != 640:
+                    self.pcm.write(chr(0) * (640 - len(data)))
+            self.pos += len(data) / self.channels / self.bytes
+        self.update()
+        return True
+
+
+class DirWatch:
+    def __init__(self, dirname):
+        self.mtime = 0
+        self.dirname = dirname
+        self.name = ''
+        self.disp = ''
+
+    def ping(self, *a):
+        signalled = True
+        
+    def choose(self, wait=False):
+        mtime = os.stat(self.dirname).st_mtime
+        if self.mtime == mtime:
+            if not wait:
+                return self.name, self.disp
+            # wait until it might have changed, using dnotify
+            f = os.open(self.dirname, 0)
+            signalled = False
+            signal.signal(signal.SIGIO, self.ping)
+            fcntl.fcntl(f, fcntl.F_NOTIFY, (fcntl.DN_MODIFY|fcntl.DN_RENAME|
+                                            fcntl.DN_CREATE|fcntl.DN_DELETE))
+            mtime = os.stat(self.dirname).st_mtime
+            while not signalled and mtime == self.mtime:
+                signal.pause()
+                mtime = os.stat(self.dirname).st_mtime
+            os.close(f)
+
+        # Better check again
+        self.mtime = mtime
+        min = None
+        disp = None
+        name = None
+        for n in os.listdir(self.dirname):
+            if n[0] == '.':
+                continue
+            (num,d) = self.parse(n)
+            if name == None:
+                name, disp, min = n, d, num
+            elif num == min:
+                if n > name:
+                    name, disp = n, d
+            elif num == None:
+                pass
+            elif min == None or num < min:
+                name, disp, min = n, d, num
+        if name == None:
+            return name, None
+        self.name = os.path.join(self.dirname, name)
+        if disp != 'R' and disp != 'P':
+            disp = 'W'
+        self.disp = disp
+        return self.name, disp
+
+    def parse(self, name):
+        n = ''
+        while name[0].isdigit():
+            n += name[0]
+            name = name[1:]
+        disp = name[0]
+        if name[0] not in 'PRW':
+            disp = 'W'
+
+        if n:
+            num = int(n)
+        else:
+            num = None
+        return num, disp
+
+def main():
+    os.nice(-20)
+    dn = '/var/run/sound'
+    if not os.path.exists(dn):
+        os.mkdir(dn)
+    d = DirWatch(dn)
+    stack = []
+
+    current = None
+    disp = None
+    waiting = False
+
+    while True:
+        newname, newdisp = d.choose(current == None or waiting)
+        if current and current.filename == newname:
+            if current.play():
+                continue
+            if disp == 'R':
+                os.unlink(current.filename)
+                current = None
+                continue
+            if disp == 'P':
+                time.sleep(0.1)
+                current.loadfile(current.filename)
+                continue
+            waiting = True
+            continue
+        waiting = False
+        # need new...
+        if current and not os.path.exists(current.filename):
+            current = None
+
+        if current == None and len(stack) > 0:
+            current, disp = stack.pop()
+            current.resume()
+            continue
+
+        if current:
+            stack.append((current,disp))
+
+        if newname == None:
+            continue
+
+        pcm = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK)
+        current = PlayFile(newname, pcm)
+        del pcm
+        disp = newdisp
+
+main()
diff --git a/test/autocon.c b/test/autocon.c
new file mode 100644 (file)
index 0000000..e1722cd
--- /dev/null
@@ -0,0 +1,60 @@
+#include <unistd.h>
+#include <stdlib.h>
+#include <sys/fcntl.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <memory.h>
+#include <linux/if.h>
+#include <linux/if_tun.h>
+#include <stdio.h>
+
+main(int argc, char *argv[])
+{
+       int fd;
+       struct ifreq ifr;
+       int err;
+       char *dev;
+       char cmdbuf[1000];
+       char pbuf[1500];
+       int n;
+
+       fd = open("/dev/net/tun", O_RDWR);
+       if (fd < 0) {
+               perror("tun");
+               exit(1);
+       }
+       memset(&ifr, 0, sizeof(ifr));
+
+       ifr.ifr_flags = IFF_TUN;
+       err = ioctl(fd, TUNSETIFF, &ifr);
+       if (err < 0) {
+               perror("TUNSETIFF");
+               exit(1);
+       }
+
+       dev = ifr.ifr_name;
+
+       printf("dev = %s\n", dev);
+
+       sprintf(cmdbuf, "ifconfig %s 10.255.255.254 pointopoint 10.255.255.253", dev);
+       system(cmdbuf);
+
+       sprintf(cmdbuf, "route add -net 0.0.0.0/1 gw 10.255.255.254 dev %s", dev);
+       system(cmdbuf);
+       sprintf(cmdbuf, "route add -net 128.0.0.0/1 gw 10.255.255.254 dev %s", dev);
+       system(cmdbuf);
+
+       n = read(fd, pbuf, sizeof(pbuf));
+       printf("read got %d\n", n);
+
+       system(cmdbuf);
+
+       system("ifconfig wlan0 192.168.1.70");
+       system(" iptables -A POSTROUTING -t nat -j MASQUERADE -s 10.255.255.254");
+       write(fd, pbuf, n);
+       while (1) {
+       n = read(fd, pbuf, sizeof(pbuf));
+       printf("read got %d\n", n);
+       }
+}
diff --git a/test/clock.py b/test/clock.py
new file mode 100644 (file)
index 0000000..d57d6f8
--- /dev/null
@@ -0,0 +1,36 @@
+
+
+import gtk, pango
+
+w = gtk.Window(gtk.WINDOW)
+w.set_size_request(16,16)
+w.realize()
+w.window.property_change('_XEMBED_INFO', '_XEMBED_INFO', 32, gtk.gdk.PROP_MODE_REPLACE, [1,1])
+fd = pango.FontDescription('sans 10')
+fd.set_absolute_size(25*pango.SCALE)
+w.modify_font(fd)
+layout = w.create_pango_layout("88:88")
+(ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+
+pm = gtk.gdk.Pixmap(w.window, ew,eh)
+pm.draw_rectangle(w.get_style().bg_gc[gtk.STATE_NORMAL],
+                  True, 0, 0, ew, eh)
+pm.draw_layout(w.get_style().fg_gc[gtk.STATE_NORMAL],
+               0,0,layout)
+w.set_size_request(ew,eh)
+
+
+w.show()
+
+def redraw(a,b):
+    print "event", b
+    w.window.draw_rectangle(w.get_style().bg_gc[gtk.STATE_NORMAL],
+                            True, 0, 0, ew, eh)
+    w.window.draw_layout(w.get_style().fg_gc[gtk.STATE_NORMAL],
+                         0,0,layout)
+
+
+w.connect('expose-event', redraw)
+w.connect('configure-event', redraw)
+gtk.main()
+
diff --git a/test/fake-test.c b/test/fake-test.c
new file mode 100644 (file)
index 0000000..4527d44
--- /dev/null
@@ -0,0 +1,31 @@
+
+#include <fakekey/fakekey.h>
+
+#include <X11/Xlib.h>
+
+main(int argc, char *argv[])
+{
+       Display *d;
+       FakeKey *f;
+       int a;
+       char *c;
+
+       d = XOpenDisplay(NULL);
+
+       f = fakekey_init(d);
+
+       for (a=1; a<argc; a++) {
+               for (c=argv[a]; *c; c++) {
+                       if (*c == '#')
+                               fakekey_press_keysym(f, XK_BackSpace, 0);
+                       else
+                               fakekey_press(f, c, 1, 0);
+                       fakekey_release(f);
+               }
+               if (a+1 < argc)
+                       fakekey_press(f, " ", 1, 0);
+               else
+                       fakekey_press_keysym(f, XK_Return, 0);
+               fakekey_release(f);
+       }
+}
diff --git a/test/notify.py b/test/notify.py
new file mode 100644 (file)
index 0000000..4ad1eac
--- /dev/null
@@ -0,0 +1,12 @@
+#!/usr/bin/python
+import gtk
+import gio
+
+def file_changed (monitor, file, unknown, event):
+  if event == gio.FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
+    print "file finished changing"
+
+file = gio.File('/tmp/monitor')
+monitor = file.monitor_file ()
+monitor.connect ("changed", file_changed)
+gtk.main()
diff --git a/test/pykey.py b/test/pykey.py
new file mode 100644 (file)
index 0000000..39825ce
--- /dev/null
@@ -0,0 +1,130 @@
+#!/usr/bin/env python
+
+import Xlib.display
+import Xlib.X
+import Xlib.XK
+import Xlib.protocol.event
+
+UseXTest = True
+
+try :
+    import Xlib.ext.xtest
+except ImportError:
+    UseXTest = False
+    print "no XTest extension; using XSendEvent"
+
+import sys, time
+
+display = Xlib.display.Display()
+window = display.get_input_focus()._data["focus"];
+
+if UseXTest and not display.query_extension("XTEST") :
+    UseXTest = False
+
+special_X_keysyms = {
+    ' ' : "space",
+    '\t' : "Tab",
+    '\n' : "Return",  # for some reason this needs to be cr, not lf
+    '\r' : "Return",
+    '\e' : "Escape",
+    '!' : "exclam",
+    '#' : "numbersign",
+    '%' : "percent",
+    '$' : "dollar",
+    '&' : "ampersand",
+    '"' : "quotedbl",
+    '\'' : "apostrophe",
+    '(' : "parenleft",
+    ')' : "parenright",
+    '*' : "asterisk",
+    '=' : "equal",
+    '+' : "plus",
+    ',' : "comma",
+    '-' : "minus",
+    '.' : "period",
+    '/' : "slash",
+    ':' : "colon",
+    ';' : "semicolon",
+    '<' : "less",
+    '>' : "greater",
+    '?' : "question",
+    '@' : "at",
+    '[' : "bracketleft",
+    ']' : "bracketright",
+    '\\' : "backslash",
+    '^' : "asciicircum",
+    '_' : "underscore",
+    '`' : "grave",
+    '{' : "braceleft",
+    '|' : "bar",
+    '}' : "braceright",
+    '~' : "asciitilde"
+    }
+
+
+def get_keysym(ch) :
+    keysym = Xlib.XK.string_to_keysym(ch)
+    if keysym == 0 :
+        # Unfortunately, although this works to get the correct keysym
+        # i.e. keysym for '#' is returned as "numbersign"
+        # the subsequent display.keysym_to_keycode("numbersign") is 0.
+        keysym = Xlib.XK.string_to_keysym(special_X_keysyms[ch])
+    return keysym
+
+def is_shifted(ch) :
+    if ch.isupper() :
+        return True
+    if "~!@#$%^&*()_+{}|:\"<>?".find(ch) >= 0 :
+        return True
+    return False
+
+def char_to_keycode(ch) :
+    keysym = get_keysym(ch)
+    keycode = display.keysym_to_keycode(keysym)
+    if keycode == 0 :
+        print "Sorry, can't map", ch
+
+    if (is_shifted(ch)) :
+        shift_mask = Xlib.X.ShiftMask
+    else :
+        shift_mask = 0
+
+    return keycode, shift_mask
+
+def send_string(str) :
+    for ch in str :
+        #print "sending", ch, "=", display.keysym_to_keycode(Xlib.XK.string_to_keysym(ch))
+        keycode, shift_mask = char_to_keycode(ch)
+        if (UseXTest) :
+            #print "Trying fake_input of", ch, ", shift_mask is", shift_mask
+            if shift_mask != 0 :
+                Xlib.ext.xtest.fake_input(display, Xlib.X.KeyPress, 50)
+            Xlib.ext.xtest.fake_input(display, Xlib.X.KeyPress, keycode)
+            Xlib.ext.xtest.fake_input(display, Xlib.X.KeyRelease, keycode)
+            if shift_mask != 0 :
+                Xlib.ext.xtest.fake_input(display, Xlib.X.KeyRelease, 50)
+        else :
+            event = Xlib.protocol.event.KeyPress(
+                time = int(time.time()),
+                root = display.screen().root,
+                window = window,
+                same_screen = 0, child = Xlib.X.NONE,
+                root_x = 0, root_y = 0, event_x = 0, event_y = 0,
+                state = shift_mask,
+                detail = keycode
+                )
+            window.send_event(event, propagate = True)
+            event = Xlib.protocol.event.KeyRelease(
+                time = int(time.time()),
+                root = display.screen().root,
+                window = window,
+                same_screen = 0, child = Xlib.X.NONE,
+                root_x = 0, root_y = 0, event_x = 0, event_y = 0,
+                state = shift_mask,
+                detail = keycode
+                )
+            window.send_event(event, propagate = True)
+
+for argp in range(1, len(sys.argv)) :
+    send_string(sys.argv[argp])
+    display.sync()
diff --git a/test/reflash b/test/reflash
new file mode 100644 (file)
index 0000000..7902438
--- /dev/null
@@ -0,0 +1,5 @@
+
+cd /home/git/dfu-util
+./src/dfu-util  -a rootfs --device  1d50:5119 -D /home/neilb/Desktop/FSO/*jffs*
+./src/dfu-util -a kernel --device 1d50:5119 -D /home/neilb/Desktop/FSO/uImag*
+#./src/dfu-util --device 1d50:5119 --reset
diff --git a/test/status.py b/test/status.py
new file mode 100644 (file)
index 0000000..0e39c4c
--- /dev/null
@@ -0,0 +1,51 @@
+
+import sys
+import pygtk
+import gtk
+import os
+import gobject
+
+capfile = "/sys/class/power_supply/battery/capacity"
+curlimfile = "/sys/class/i2c-adapter/i2c-0/0-0073/pcf50633-mbc/usb_curlim"
+chgfile = "/sys/class/i2c-adapter/i2c-0/0-0073/pcf50633-mbc/chgmode"
+currfile = "/sys/class/power_supply/battery/current_now"
+filename = "/media/card/panel-plugin/pixmaps/battery_%03d.png"
+filename_charging = "/media/card/panel-plugin/pixmaps/battery_%03d_charging_%d.png"
+
+def file_text(name):
+    try:
+        f = open(name)
+    except:
+        return ""
+    t = f.read()
+    return t.strip()
+def file_num(name):
+    try:
+        i = int(file_text(name))
+    except:
+        i = 0
+    return i
+
+def setfile(icon):
+    cap = file_num(capfile)
+    capr = int((cap+5)/10)*10
+    curr = file_num(currfile)
+    lim = file_num(curlimfile)
+    if curr >= 0 or lim == 0:
+        f = filename % capr
+    else:
+        f = filename_charging % (capr, lim)
+    print f
+    i.set_from_file(f)
+    
+def update():
+    global i
+    setfile(i)
+    to = gobject.timeout_add(30*1000, update)
+
+i = gtk.StatusIcon()
+setfile(i)
+i.set_visible(True)
+to = gobject.timeout_add(30*1000, update)
+
+gtk.main()
diff --git a/test/test.py b/test/test.py
new file mode 100644 (file)
index 0000000..06477d3
--- /dev/null
@@ -0,0 +1,49 @@
+
+import sys
+import pygtk
+import gtk
+import os
+import gobject
+
+capfile = "/sys/class/power_supply/battery/capacity"
+curlimfile = "/sys/class/i2c-adapter/i2c-0/0-0073/pcf50633-mbc/usb_curlim"
+chgfile = "/sys/class/i2c-adapter/i2c-0/0-0073/pcf50633-mbc/chgmode"
+currfile = "/sys/class/power_supply/battery/current_now"
+filename = "/tmp/pixmaps/battery_%03d.png"
+filename_charging = "/media/card/panel-plugin/pixmaps/battery_%03d_charging_%d.png"
+
+def file_text(name):
+    try:
+        f = open(name)
+    except:
+        return ""
+    t = f.read()
+    return t.strip()
+def file_num(name):
+    try:
+        i = int(file_text(name))
+    except:
+        i = 0
+    return i
+
+def setfile(icon, capr):
+    curr = 1
+    lim = 0
+    if curr >= 0 or lim == 0:
+        f = filename % capr
+    else:
+        f = filename_charging % (capr, lim)
+    print f
+    i.set_from_file(f)
+    
+def update():
+    global i
+    setfile(i, 0)
+    to = gobject.timeout_add(30*1000, update)
+
+i = gtk.StatusIcon()
+setfile(i,100)
+i.set_visible(True)
+to = gobject.timeout_add(10*1000, update)
+
+gtk.main()
diff --git a/test/test1.py b/test/test1.py
new file mode 100644 (file)
index 0000000..0f2f17a
--- /dev/null
@@ -0,0 +1,35 @@
+
+# experiment with clip board
+# We define a clip board "test"
+# We set it to 'waiting' and whenever anyone else sets it,
+# we collect the value and reset to 'waiting'
+
+import gtk
+import pygtk
+import gobject
+targets = [ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ]
+
+def getdata(clipb, sel, info, data):
+    print "sending"
+    sel.set_text("waiting")
+
+def cleardatadelay(clipb, data):
+    print 'cleardel'
+    gobject.timeout_add(2000, lambda : cleardata(clipb, data))
+    
+def cleardata(clipb, data):
+    a = clipb.wait_for_text()
+    print "Got ", a
+    clipb.set_with_data(targets, getdata, cleardatadelay, None)
+    
+cb = gtk.Clipboard(selection='PRIMARY')
+
+def set():
+    global cb
+    print "set"
+    cb.set_with_data(targets, getdata, cleardatadelay, None)
+
+gobject.idle_add(set)
+
+gtk.main()
+
diff --git a/test/test2.py b/test/test2.py
new file mode 100644 (file)
index 0000000..015be97
--- /dev/null
@@ -0,0 +1,29 @@
+
+# get the value from clipboard "test", then set a new value
+
+import gtk
+import pygtk
+import sys
+import gobject
+
+targets = [ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ]
+
+def getdata(clipb, sel, info, data):
+    a = sys.argv[1]
+    print "sending", a
+    sel.set_text(a)
+
+def cleardata(clipb, data):
+    print "clear"
+    gtk.main_quit()
+    
+cb = gtk.Clipboard(selection='PRIMARY')
+
+def set():
+    global cb
+    print "set"
+    cb.set_with_data(targets, getdata, cleardata, None)
+
+gobject.idle_add(set)
+gtk.main()
+
diff --git a/test/wkalrm.c b/test/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;
+}