--- /dev/null
+
+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'.
+
--- /dev/null
+
+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
+
--- /dev/null
+
+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
--- /dev/null
+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.
--- /dev/null
+#!/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()
--- /dev/null
+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
--- /dev/null
+12,N,
+3,N,
+36,N,bob
+25,N,twinings
--- /dev/null
+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
--- /dev/null
+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??
--- /dev/null
+#!/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
--- /dev/null
+#!/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
--- /dev/null
+#!/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()
+
--- /dev/null
+
+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
+
--- /dev/null
+Subproject commit b5979fa52ad017c1d5853cd533024364e967c4e2
--- /dev/null
+"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 ,
--- /dev/null
+#!/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()
--- /dev/null
+#!/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
--- /dev/null
+#!/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('&','&')
+ a = a.replace('<','<')
+ a = a.replace('>','>')
+ 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('&','&')
+ a = a.replace('<','<')
+ a = a.replace('>','>')
+ 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))
+
--- /dev/null
+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
+
+
--- /dev/null
+
+#
+# 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('&','&')
+ name = name.replace('<','<')
+ name = name.replace('>','>')
+ 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()
+
--- /dev/null
+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
--- /dev/null
+
+#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);
+}
+
--- /dev/null
+
+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])
--- /dev/null
+
+# 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()
--- /dev/null
+#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);
+}
+
--- /dev/null
+
+#
+# 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()
+
--- /dev/null
+#!/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()
--- /dev/null
+#!/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))
--- /dev/null
+
+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
--- /dev/null
+
+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()
--- /dev/null
+#!/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 )
--- /dev/null
+#!/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()
+
--- /dev/null
+
+#
+# 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
+
--- /dev/null
+#!/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)
+
--- /dev/null
+
+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
--- /dev/null
+#!/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)
--- /dev/null
+#
+# 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()
+
+
--- /dev/null
+
+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()
+
--- /dev/null
+#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 */
--- /dev/null
+/*
+ *
+ * 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(¶ms);
+ 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);
+}
--- /dev/null
+#!/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()
--- /dev/null
+#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);
+ }
+}
--- /dev/null
+
+
+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()
+
--- /dev/null
+
+#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);
+ }
+}
--- /dev/null
+#!/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()
--- /dev/null
+#!/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()
--- /dev/null
+
+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
--- /dev/null
+
+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()
--- /dev/null
+
+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()
--- /dev/null
+
+# 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()
+
--- /dev/null
+
+# 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()
+
--- /dev/null
+/*
+ * 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;
+}