From: NeilBrown Date: Sun, 6 Feb 2011 09:44:35 +0000 (+1100) Subject: Lots of random updates X-Git-Url: http://git.neil.brown.name/?a=commitdiff_plain;h=26c1d8db4284880bcab0db449e9d1634e157dc7b;p=freerunner.git Lots of random updates Just trying to sync-up with various previously-untracked things Signed-off-by: NeilBrown --- diff --git a/NOTES/Control b/NOTES/Control new file mode 100644 index 0000000..8c5abd1 --- /dev/null +++ b/NOTES/Control @@ -0,0 +1,73 @@ + +Overall control: + - blanking + - suspend + - screen-lock + - screen-capture + - screen-rotate? + - charging current ?? + - + +Use AUX button?? + + +Blanking: + Need to know if screen is being used. + hint: if GPS is on, then screen is watched. + if touchpad gets input, then screen is watched. + if power is on can wait longer. + After 10 seconds of no touchpad, check power and gps power + + /sys/devices/platform/s3c2440-i2c/i2c-adapter/i2c-0/0-0073/charger_type + starts "none" or "host" + /sys/bus/platform/devices/neo1973-pm-gps.0/pwron + 0 or 1 + + First stage blank goes to half brightness and disables keyboard + A tap goes full and enables. (so we lose the tap) + + Second stage goes off. Tap returns to first stage. + +Suspend: + Need to know when system is being used: + - network connections other than 127.0.0.1 + - active phone call + - active screen + +So: + + We have a current state: + on: screen is on and accepting input + half: screen is reduced brightness and blocked + off: screen is off, input is diverted + + We have a time of last touchpad input + + We measure: + gps power + charger type + network connections. + + We decide in new state and timeout + We change state (if needed) and sleep. + sleeping involves reading touchpad so we know time of last input. + + + switch state: + case 'on': + if last input < 10 seconds, set time for remaining seconds done + if charger and last input < 30 seconds, as above + set state to half + set time to 50 + + case 'half' + if < 10 seconds, set state to on, wait 10 seconds + if gps power wait for 10 minutes, else 1 minute + set state to off + + case 'off' + if < 10 seconds, set state to half, wait 10 seconds + if gps, wait 30 minutes, else 2 minutes + if network or charger, stay at off indefinately + if time is up, "apm -s", assume input, set to 'on'. + diff --git a/NOTES/Debian b/NOTES/Debian new file mode 100644 index 0000000..610e523 --- /dev/null +++ b/NOTES/Debian @@ -0,0 +1,70 @@ + +Install + +remove fso-frameworkd and zhone +but keep python-gst10.0 and python-gtk, apmd +discard dash, just use bash + +Install latest andy-tracking kernel plus modules + +add gcc, libc6-dev, and some other dev to get gsm0710muxd to compile +add nfs-utils +add less, lsof +add x11-utils +add gpsd tangogps +add xserver-xglamo +add mplayer +add ntpdate +add bc +gcc libc6-dev nfs-utils less lsof x11-utils gpsd tangogps xserver-xglamo mplayer ntpdate bc + +add libts-bin and calibrate the touchscreen with ... you cannot + just copy /etc/pointercal + 557 38667 -4654632 -51172 121 46965312 65536 + + +dpkg-reconfigure gpsd + tell it to use /dev/ttySAC1 +echo 1 > + /sys/class/i2c-adapter/i2c-0/0-0073/pcf50633-regltr.7/neo1973-pm-gps.0/power_on + /sys/class/i2c-adapter/i2c-0/0-0073/pcf50633-regltr.7/neo1973-pm-gps.0/keep_on_in_suspend + +add telnet + +DPI is different... what to do?? + +remove /var/lib/apt/lists and symlink to /media/card +remove /var/cache/apt and symlink to /media/card +remove /usr/share/man /usr/share/doc + should probably mount tmpfs on those to avoid getting new stuff + + +/dev/ttySAC don't get setup!! +rather we get +crw-rw---- 2 root dialout 204, 64 Feb 11 13:51 s3c2410_serial0 +crw-rw---- 2 root dialout 204, 65 Feb 11 12:03 s3c2410_serial1 +crw-rw---- 2 root dialout 204, 66 Feb 11 12:03 s3c2410_serial2 + +echo 'KERNEL=="s3c2410_serial[0-9]", NAME="ttySAC%n"' > /etc/udev/rules.d/56-ttySAC.rules + +or >> /etc/udev/rules.d/56-freerunner.rules + + + +add wmctrl for launch - should get rid of this +---- +copy gsm0710muxd, run it +copy Freerunner/gsm,lib/* to /usr/local/lib/python2.5/site-packages/ +chmod +X gsmd and link to /usr/local/bin +add #!env to top +mkdir /var/run/gsm-state +mkdir /var/lib/misc/flightmode + and > active +copy apmd/ + + +Need to: + start gsm0710muxd + gsmd + launch + diff --git a/NOTES/Network b/NOTES/Network new file mode 100644 index 0000000..688647b --- /dev/null +++ b/NOTES/Network @@ -0,0 +1,15 @@ + +Want to have easy config for network: + + Start/stop GPRS, and set AP name + Start/stop wireless, and set AP name + +So: + Big "GPRS" button toggles on/off + text entry for AP name + + Big "WLAN" button toggles on/off + If on and no ESSID chosen, try to fill in box. + + +AP name is stored in /media/card/gprs diff --git a/NOTES/TODO b/NOTES/TODO new file mode 100644 index 0000000..5a7b3b7 --- /dev/null +++ b/NOTES/TODO @@ -0,0 +1,59 @@ + +My freerunner stuff is a bit of a mess.... and there is lots to do: + +- Tidy up + + + put all code in single git tree (or maybe a few) + + remove code that is no longer used like speeddial and runit + + collect all random scripts and document how they work - just comments + in the scripts + + keep copy on phone/eli/notebook + + +- Networking + - wifi + + use wpa_cli to get list of networks and to connect to new + + button to switch to 'access point' mode + - usb + - bluetooth + +- audio + - bluetooth + - headset switching + - music playing + +- calls + - different tones for different callers + - handle 'busy' and 'no carrier' better + +- GPS + - ?? auto time sync + +- timezone + - allow list of interesting timezones to be created + +- GPRS + - fix flow control + - auto connect based on wifi status and GSM service provider + +- address book + - general editing app + +- status widgets: + - network status + - GSM status + - active network connections?? + +- GSM + - make carrier-search return a task list + +- launch + - make it easier(?) to cache task lists so we don't keep generating + them anew. + - give feed back while list is being refreshed + +- SMS + - allow search/filter to work better + +- glamo timing fixes + install 'omhack'?? diff --git a/NOTES/TODO2 b/NOTES/TODO2 new file mode 100644 index 0000000..5637046 --- /dev/null +++ b/NOTES/TODO2 @@ -0,0 +1,868 @@ +TODO: sep 2009, now that I am using this as my real phone: + - send touch tone commands + - keep record of incoming/outgoing calls + - reduce time from wake to ring +TODO: + DONE runit to scroll to the bottom + DONE bigger buttons in runit + text, not icons, for shop config stuff + DONE launch/status to update more often + DONE gsm info in launch/status + DONE gsmd: reading 'extra' needs to cope with multiple lines: + SMS: + - update git + - add 'xterm' so I can type... + DONE - add 'esc' to 'tapinput' + - look for launch errors in .xsession-errors: + File "/usr/local/bin/launch", line 324, in press + if not self.offsets: +AttributeError: 'Selector' object has no attribute 'offsets' + + - make sure launch updates when sms message arrives + - find out why everything is so slow + - make sure only one 'gsm-getsms' running at a time. Maybe kill + old one? + - trace?? to find out why we don't seem to pick up a message on + resume. The newsms file gets changed but maybe no signal gets sent. + - "sendsms -n" should go straight to 'new'. + - clean up rubbish in 'new' and 'draft' files. + - lock should be in charge of buzzer. + It turns it off on 'wake' or after a timeout and turns it on when some file changes. + Maybe there is an 'alert' file which gets 'sms' or 'ring'. + In the case of 'ring' it is updates regularly. + 'lock' sets a buzz, turns on display, maybe beeps + - run gsm-getsms at appropriate times + -n when /var/run/gsm-state/new-sms changes + DONE -a at boot time, or when gsmd starts?? + - write some info to a 'new sms' file that I can watch for status + Probably just watch the 'newmesg' log and update shortly after that changes. + - improve atchan code - better abstractions + - 'delete' should return to list view if in message view + - Make sure sendsms notices when the files change + - Make 'sender' and time easier to read. + Probably make lines twice as high + time can be 'today' and 'yesterday' and ' 5 minutes ago' etc + mark day/month boundaries some how? + - figure out if a '+' should be at the front of the phone number + - look into timezone information on exesms messages + - implement search function + - Highlight 'new', 'draft' 'in/out' better. Icon? + - Don't duplicate a draft when we edit it. + - Find some way to remove cruft from new/draft + - buzz buzzer when message arrives. + - include transit time in full message view. + - Make messages disappear from 'new' when they are read. + - popup reading in 'new' mode when selected from launcher + - capture and decode catenated messages + - figure out why changing lists is so slow. + - implement undelete. + - implement scrolling of message list + - change 'open' to 'forward' or 'edit' + - add 'reply' button + ?? detect SIM card id and have separate mirror file?? + AT+CIMI + 505038191025166 + 505038240084403 + - move 'config' to 'listing' page? + - change between 'save draft' and 'cancel' when clear. + - press-and-hold send to select handler + - make 'paste' active only when there is something to paste + - send only active when fields are filled in. + + +receive AT response +CBM: 4576,50,1,1,1 +receive AT response South +receive AT response Sydney +receive AT response + +Sms-store: + DONE store direction (in/out) and only one address (Sender or recipient) + +Inter toy communication: + e.g. + play this tune + call this number + I want the speakers + don't suspend just now + call me on resume + everybody pause + + Use X clipboards for specific messages + use lock files for 'dont' + use leases for 'call me' and 'everybody' + That would require all running as same uid. + Maybe DNOTIFY, but cannot use that well from python + So write C helper and do inotify? + But check gobject doco first. + I think I can use dnotify OK, poll each file on each signal. + So use that for 'call me on resume' + Possibly for 'incoming call', 'received sms', + Maybe for 'Internet now available' + Used by weather, time, + Also 'GSM' and 'GPS' now available + + Root Properties - one-to-many messages. that are ui based. + e.g. 'pause' 'ABC/abc/123/$%&', 'scale size' + + So: suspend. + We have a directory /var/lock/suspend + containing: + auto: Taking a LOCK_SH on this prevents auto suspend. + If auto-suspender can get a LOCK_EX, it suspends, then + unlocks on resume + suspend: Taking a LOCK_SH on this prevents any suspend completing. + However any locker must also watch the file with e.g. + dnotify, and when the file is nonempty, the lock must be + released promptly. + next_suspend: Taking a lock on this while holding a lock on + suspend ensures that you will not miss a suspend/resume cycle. + On resume, the file is renamed to 'suspend', + So 'dnotify' can discover when resume happens and a subsequent + suspend will not complete until the lock is released. + + A client with: + take a lock on 'suspend' + check that link count is non-zero (if zero, close and loop) + loop: + set up for active system + wait + when there is a change in the file: + set up for suspend + possibly take lock on next_suspend + release lock + wait for activity on next_suspend (which will be renamed to 'suspend') + goto loop + + +Alert: + Alert is currently part of 'lock'. Is that a good idea? + The connection is that we only want to sound an alert if there is + an easy way to turn it off - and 'lock' is in the best position to + turn things off. It is closest to the user. If lock isn't + running, don't want to make any noise. + + Alert needs: + - different alerts: ring, sms, alarm, low-battery + - different sub-alerts: ring/family. sms/work, alarm/urgent + - different settings: silent, unobtrusive, normal + - different actions: tone, buzz, flash, repeat count, volume + change, LED flash, screen flash. + - some alerts stop when there is user input (tone). Some + continue (LED) + + - concurrent alerts?? alarm and SMS at same time? + As long as we get attention, and display status on screen.. + Maybe need alert priority and only play the most important. + + Configuration: too complex for just directories with symlinks to + .wav files. + Still want to make it easy to change config, either temp or perm. + + Config changes can be + - time based + - location based? + - orientation based + - user-requested + + Does the alert program do this itself, or does some other program + move files around? + + Maybe have a set of .ini files where settings cumulate. + Read base, then others + - calendar program creates a plan every so often which lists + time - profile e.g. 20090228-123456 silent + 20090301-012334 - + - The location service gives a name to where we are, as precise + as possible. We then look for setting "location-$NAME" + - ditto for orientation. + + +Disable 'xset' screen blan!k + +tapinput: + DONE differently OK should become 'go away' + DONE Need a 'mode' where all 12 are used. press/hold mode/del to recover. + DONE Move > half off screen, and disappear. + DONE statusicon to recover + DONE Add: Ret UP DOWN LEFT RIGHT + + +Weather: + Waits for 'internet now available' and if more than 8 hours since + last check, download weather details. + +Library: + Selector + Currently used in + launcher (folders/tasks), music, shop, sendsms + Features: + - goes multi-column if possible/needed + - auto-scroll when near extreme + - tap selects but doesn't activate + - small left/right tap areas for select-and-activate + - simple text with different colours, or app-specific rendering + - alternating background, background for highlight + - drags used for text entry + + AutoBox + Acts like vbox or hbox depending on available space + + Slider + pop-up window + + +Note pad with search. + This might just be an extension to scribble. + - simple text file editor + - multiple pages with a list that can be selected from + - search across all pages - show page name and line. + - mark pages 'read-only' + (ideas from tomboy) + - text size. features: highlight strikethrough bold italic + - 'link' a word to a page of the same name. + - group pages into notebooks??? + - scrollable pages? + - collapsable lists. + +Scribble: + - larger buttons + - ??save png?? + - allow grouping of texts with tap-draw-hold + - when we have a group of texts, allow + - align + - sort + - fold + - move a text with tap-hold-draw + - easier to select individual text + - allow text to be deleted - maybe drag to corner + - new line gets leading number or bullet or whatever + - I need: + 'text' entries to record opposite corner when drawn + some set of the last N text entries + these are drawn with a grey background?? + select text + +runit + Set window name to something appropriate + DONE run a command and display the output. No user interaction(yet) + DONE One button to close + Possible additions. + - rerun button + - different colour for stderr + - buffer to build text, then enter it + Combine with launcher??? or teach launcher to understand it better + Make it easier to run a little shell script + + +lock: + DONE Be more careful about counting up/down - count per button. + DONE be careful about press/release difference + DONE allow insta-lock with icon + LATER use lockfile to tell when someone wants no-blank + LATER Have lockfile for 'next suspend'. use inotify for clients + to find out when 'next' becomes 'now' + DONE use lockfile to tell when someone wants no-suspend + DONE suspend when more idle + DONE use external net connections to disable suspend + DONE maybe don't suspend while charging (of >50%) + DONE don't suspend while load-average is high + DONE no-suspend setting. + Only check TCP if keep-alive is working.... + Use alsa to play tones for alerts. Also buzz + Alerts are requested by creating file /var/run/alert/$name + where 'name' is something like 'sms' or 'ring' or 'alarm' + We look for /etc/alert/$name to find out what to play. + Not sure how to include + tone + volume + repeat/louder + vibrate + display brightness + LED flash + + We terminate the alert tone when a button is pressed + Vibrate continues if it is a repeating alert (ring) + + +Event Viewer + Events such as SMS, missed-call, alarm get appended to a file + The viewer shows recent events and allows the relevant window to be + selected. + It has a blinking statusicon when there are new events + + This is probably just included in launcher + +PictureViewer + one pane which just shows a photo, is full screen, taps left and right moves + one pane with browser showing date information + thumbnails? + +Address Book + store addresses with phone numbers + lookup and display + Format address label as postscript for printing + send phone number to dialler + + Store: + Names + Addresses - people share these + Phone number - people have several, and share them + Email address - again, can be multiple and shared. + + So we allow entries to include others by reference + + Name - given / family + Category (family, collegue, church, ...) + Address[desc] - l1 / l2 / postcode / country + Phone[desc] - number + Email[desc] - addr + group[desc] - entity + + What key to use for 'entity' ?? + Name? not unique and can change. + UUID I guess. assigned sequentially. Maybe use hostname to + uniqueify + + Store: + notabene.1234 = { + name = { given = Neil ; family = Brown } + Category = me; + Category = Family; + address = { type=Home; a= "13 Lang Ave"; + city="Pagewood"; postcode=2019; + country = Australia + } + Phone = { type=mobile; num=0403463349; } + Valid = date + } + + + That is horribly verbose and not needed. + Though if we store gzipped, it isn't much waste. + + We don't really want to store it in memory as that isn't paged out, + So we want an ondisk format which is very easy to parse. Hopefully + it will be cached most of the time. + So: ':' separated fields with ',' separated subfields and '=' assignments + id:family:given:cat,cat:ref,ref:PO:addr:addrext:city:postcode:country:mobile=number,work=number:Date + + Should probably use VCARD - it isn't that hard. + Just ignore the bits we don't understand yet. + When editting, only change fields we understand. + When there are interdependent fields like N vs FN and ADR vs LBL + that can be awkward. + + No. Don't like VCARD - no references (that I can find) as no ID for each entry ??? + + Anyway I'll use my own internal format and maybe export/import one day if I decide to care. + + We mmap the file and typicaly use 're' to search through it, while holding a read lock and having + checked the size. + + We access the address book by extracting addresses. + 'first', 'next' + These can have an 're' argument meaning 'first/next that matches re' + File contains an initial line with a version number, so that each line starts and ends + with '\n'. Thus each field starts and ends with [\n:,=] + + How to integrate this? + Any program that just needs read access goes direct to the file, mmaping and searching + Auto updates such as 'last used' can similarly be on on the mmapped file. + Updates need to be handled by a single GUI. + When someone - e.g. SMS or Call-Log - wants to store a number or edit an entry, + they put a resource on the root window. If the contact list is watching, it + picks it up. If it isn't, the button is greyed out (or we run the program) + + The contact editor: + Has an edit window where content of interest appears + +music: + doesn't always move forward at end-of-song + have better way to 'play chosen song' + If 'seek' dropdown didn't select a time, don't jump at all. + report song on root window property + Have fs browser to find music folder(s) + survive suspend somehow + Don't seek when there is nothing to seek (negative offset??) + record current location for restart + remove "/home/music/ogg" from displayed path + grab /var/run/suspend_disabled when playing + DONE add volume control + DONE Think about seek control + Maybe support HTTP: uris such as classic-fm + Support Random-Album + Support Random-Song + tap 'current song' to go there in browser + search by substring + +clock: + sync to real time properly + alarm clock: + periodic or one-off + wake to music or buzzer + pop up: + set timezone + show date/calendar + ntpdate + gpsdate + gps-timezone + +Shopping List + Need to add/edit locations and places. + When 'choose location' or 'choose place' is up, the + 'add' and 'edit' button affect the location name instead of the product + renaming a thing to 'empty' only works if it is not in use. + When we click 'config', product list is replaced by location list. + Click 'config' again to go back. Different background colour?? + If we change place, it changes set of locations + '+' adds a new location + 'edit' changes the name of current location + zoom still exist + tick and cross become up and down to move current location + + +Battery + try to get more timely alerts without polling - Need to use DBUS. + DONE Select between pop-up new window and restore old window + DONE Show time_to_{full,empty}_now in correct units + ?? use 'status' for 'Charging' ... + DONE timeout to make window disappear? or to refresh display + DONE pop up window with + suspend + shutdown + 500/1000 mAmps + +network + need to consider Wifi, USB, bluetooth, gprs, BT/ppp + Show status of each + device specific page for each: + Wifi: + list access points + connect with DHCP, enter WPA-PSK + suspend mode: power off or WOW + USB: + disable/DHCP-client/DHCP-server + if dchp-server: have list of address configs to choose between + if another network is active, choose based on that. + Bluetooth + list peers + connect + listen + DHCP client/server + disable/power down + ppp over rfcomm + GPRS + on-demand + enter AP + +launcher + content for status page: + - date, time, timezone?? + - GSM carrier, Cellid strenght? + - Network connectivity + - new messages + - missed calls + - last window + - GPS co-ords? + - next reminder + - top of todo list?? + - load average / uptime + Delay "wmctrl -l" a bit so that we start up faster ?? + + support displaying values from root attributes + run wmctrl at the right time better. + have a 'status' folder which shows 'today' + use pango.markup + Don't adjust sizes of entires + Allow different sizes items - keep list of offset for click + Don't allow item to be selected if no buttons + Center type. MAybe "type" can be e.g. "cmd:center" or "thing:right" + How to schedule redraw of e.g. time / date / etc + Expensive tasks would need to cache values incase they are called too often. + don't iconize- just stay behind + watch root properties to be notified of window changes. + + DONE direct button access + DONE list options + DONEkill window + DONE reload option + + DONE Two columns with button row at bottom. + DONE First column is cmd group and includes 'active windows' + DONE Second column is options in the group + DONE Buttons are "run" or "stop" + + I want a third column. When it appears, the first column disappears. + This can be used for: + - address-book access when making a call + - log of recent calls - in or out + - todo list? + Why not just put these in the second column with the trigger in the first. + So first column gets: + contact: shows address book. + text entry reduces list + selection provides 'call' and 'txt' and 'open' buttons + calls: shows similar list, but of recent calls. Can call back + or save numbers. + Selecting 'last call' or 'dialout' in status window can allows jump to + this different page. + + I think I need to rewrite launcher from scratch - it got messy. + Have same layout: + - text entry at the top: + any change is sent to any visible task + - buttons at the bottom. + these are chosen by active task + - left column of task groups. + apps comms calls contacts windows games status + - right column of tasks + + A 'tasklist' is given to each column and can be static or dynamic. + A 'task' has an appearance and an action. + For the right column, the action sets the left column. + For the left column, the action sets the buttons + The buttons also have appearance and action. The action + can do all sorts of things: + - run or kill program + - raise or close window + - make phone call / hang up + + The two columns can be replaced by a textarea that displays the output + of some command. + + The 'aux' button can be pressed or held. + - press raises the window, then selects 'status', then returns to previous window + - hold ... what can that do? Answer the phone? + Maybe if a task is signalling for attention, then 'press' goes to + that task, and 'hold' activated the first button for the task. + + I need to define the interface for pluggins. + A plugin needs to be able to: + - create a task + - create a group + - watch a file + - run a process + - open a window + - Add an embedded-window + + A task must be able to + - display a text + - present some buttons + - present an embedded window + - receive keyboard input + - request that a different task or group be selected + + Some examples of the embedded window: + - tap-board for text/number entry + - calendar - one month + - text, e.g. SMS message, email + + Config file: + This stores a list of groups. + Some groups need no further details (e.g. active windows) + Others need to explicitly list tasks + Tasks can be: + Internal (in a module) + External-window (program to run, window to find) + External-text + + When listing explicit tasks we can use: + !command + for external-text + (window) command + for external-window, where 'window' is the name of the window + module.command + for an internal module + + Each group is given in config file as: + [name/type] + if '/type' is missing, "list" is assumed. + Types and the required content are: + list + a series of task descriptions + windows + a series of window names to ignore (?) + contacts + file= name of address book + call-log + nothing - the call log has a standard location + + If 'type' is 'modules', then each line names a module + + Modules have an entry point "init" which returns: + a list of group types supported + a list of internal commands + each is paired with an object-creation command. + + A 'group' object must be able to parse config lines + and produce a task list etc. + +gpstrack + pygtk similar to tangogps + + Display maps, zoom follow GPS + Separate threads for file loading, GPS track, and UI + 3-D view of path?? + record path followed + Always compress and write to a file + record drawn path + download maps + - always all parent maps for a given download + - download maps along a path (to some depth/width) + This includes a drawn path. + If map not available, scale other resolution + record locations + display locations + report time-to-first-fix + three different modes interpretting drag + 1/ move map around + 2/ draw a path on the map + 3/ write text to tag location or path. + + +Bible viewer: + Store KJV + Download and cache NIV if Network available. + Windows: + +Sound recorder: + - support pause/restart + - play from start, play last 10 seconds, seek + - allow naming of music files + - adjustable audio level?? + - adjustable compression ?? speex/vorbis + +phone functions + ON-CALL + pops up when a call becomes active + presents number*# buttons for tones + Allows HOLD/CALL-WAITING/HANG_UP + Disappears when no call is active + Should handle VOIP too, including call-waiting + Logs times?? + disables suspend + un-pause after the call ?? + Warning tone at points in call time + + Answer-Call + Display CNI, look up in address book + Choose ring tone bassed on number and time of day?? + Check device position and be quiet if face down. + + Make Call + allow number to be entered somehow + choose VOIP or mobile, while number is being entered. + silence music, disable suspend + Make connections, log, trigger ON-CALL + + SendSMS + text + recipient + send via eXeTeL if network is up, else ?? + + Editor window: simple text, no new lines. auto scroll, + simple editing + spell check + auto-wrap + + Q: + How to select for cut/paste?? + 'cut' button is normally 'select'. + tap that, then draw range, then 'cut' + status bar with valid spellings? and common continuations + Buttons: + send: sends to one or more addresses + draft: save as a draft and go to list of drafts + config: go to config page. + + If sending fails, save as draft and go to draft page. + If it succeeds, save as 'send' and save to 'sent' page. + + 'message list' page shows one message per line. + messages can be selected. + the selected message can be: + deleted + reloaded for send + viewed? + The list can be: + all / draft / new / sent / received + restricted by search string + up/down a page + So: + Top: + delete/view/send buttons + Bottom: + One button rotates through all/draft/new/sent/received + two for up/down + one for search + + Config options: + backend to use for different class of number. + each backend has a config page. + exetel userid and password + + RecvSMS + Database of all messages + Highlight unread messages + Sort in selectable order - provide virtual folders + Date, Month, Sender "contains text" + + Handle VCARD: +0000000 006 005 004 # 364 \0 \0 B E G I N : V C A +0000020 R D \r \n V E R S I O N : 2 . 1 \r +0000040 \n N : F a n n y \r \n T E L ; P R +0000060 E F ; W O R K ; V O I C E : 9 2 +0000100 3 4 8 9 0 8 \r \n T E L ; C E L L +0000120 ; V O I C E : 0 4 3 3 2 3 1 0 6 +0000140 9 \r \n T E L ; H O M E ; V O I C +0000160 E : 0 2 9 6 6 1 8 8 8 8 \r \n E N +0000200 D : V C A R D \r \n +BEGIN:VCARD^M +VERSION:2.1^M +N:Fanny^M +TEL;PREF;WORK;VOICE:92348908^M +TEL;CELL;VOICE:0433231069^M +TEL;HOME;VOICE:0296618888^M +END:VCARD^M + + + SMS storage: + + ** Worry about ASCII / UNICODE + a/ could use one file per message, named by time + with links to a directory for 'draft' or 'new' + But that wastes space. + b/ could use a TDB database, keyed by date, with + a hierarchy of days and years for directories. + c/ plain text file, one per month + 'new' and 'draft' are separate files that list ids. + + Probably c. + + Each SMS entry is one line, space separated: + date-time %Y%m%d-%H%M%S.uuid + 'from' number + 'to' numbers, comma separated + 'text' of message, URL encoded on to one line with no spaces. + + Hmm.. + + + More on storage. + There are two dates that could be of interest. + - the time I first saw the message (my clock) + - the time the message was sent (with incoming messages). + We need to record both of these. + For outgoing, there could be "time message successfully sent" + + +---------------------------------------------------------------- +Thinking about buttons. + + - I want to easily/quickly get to 'main page' with time and status etc. + But sometimes I want to get 'back where I was' after a screen blank + + - I want the 'power' button to always do the same thing. It wakes up + from suspend, so it must wake up the same way from blank. + But what should it present: last screen, last launcher, or status? + It should be the thing I most likely want after a long abscence, which + would be the front status page. + It should also be the thing that I can use when there is an incoming call. + That would be something with an 'answer' button. So The status page + with 'incoming call' selected and 'answer/cancel' buttons at the bottom. + 'Answer' would alert the GSM-call window to pop up and answer the call. + + - So the aux button does: + - nothing during suspend + - wakeup display during blank + - launcher when active + + - But that means the lock program needs to grab AUX, so the earpiece + switch gets grabbed too. So either the lock program handles earpiece + switching, or it signals someone else. I guess we write to a file when + that changes. Though we could go direct to scenarios symlink. + How complex should this be? + /var/lib/audio/scenario -> handset or headset + handset -> scenario/handset ditto for headset + scenario points to gsm, voip, stereo, record, gsm-speaker + these are each directories which contain 2 .state files. + + - Maybe I want a button to bring up the tap-board as well?? + + Maybe: + power button always brings up control window, and selects status + aux button returns to + + + +And about lights. +We have two lights. +One can be red. +One can be blue, orange, or purple. +We can used the signal: + - charging state (powered, battery full) + - ringing state (could be different colour depending on if we know them) + - blank-but-not-off-yet + - wifi associated (might be a waste of battery...) + - + +PLAN: + + - arrange for power button to present status page + - Sort out tapinput usage + - install sendsms and make sure I can send via GSM + - sync SMS messages from SIM to flash + - display unread message info in status page + - tap on that goes to SMS application + + +QVGA: + qvga-normal & normal in "/sys/bus/spi/devices/spi2.0/state" . + +SMS/cellid + www.opencellid.org + + Keep database of locations and cells I have seen. + Don't add duplicates that are within 100m... say 3 seconds or arc. + + ?? Create a key by interleaving bits from lat and long + That gives discontinuities.. but we only need to search 2 places.. well, 4. + So if we want lat==a..b and long==c..d + We find the most sig bit in which a and b (resp c and d) differ + and need to check + +---------------------------------------------------- +What happens when I type text. I might want to: + + - make a phone call .. or SMS + - look up name in address book + - calculate result + - add to TODO list + - lookup in shopping list + +Maybe each 'task' can register a text handler. + They get called when text changes and can update their name?? + They could make themselves active, but hopefully only one would do that. + How could 'address book' show a list of possibles? + +I have outgoing calls sortof working. +Need: + - gsmd to know and track status in 'incoming'. + - wait for atchan to be set up so I don't have to press twice DONE + - clean text when call completes + - steal keyboard input for touchtones ?? + - call history + - address book lookup + + +---------------------------------------- +Main page currently has + two columns for selection/status + buttons at bottom for action + + Want text input line at the top for e.g. phone number, + contact name, calculation, note search + But when not typing anything it is visually unpleasant. + For buttons at bottom, they simple provide more space. + That doesn't work so well at the top.. unless we can do something with the + 'gravity' setting .. seems unlikely. + So I want something permanent there when there is no input. diff --git a/Shop/shop.py b/Shop/shop.py new file mode 100755 index 0000000..a7b7048 --- /dev/null +++ b/Shop/shop.py @@ -0,0 +1,2160 @@ +#!/usr/bin/env python + +# +# TO FIX +# - re-order places? +# - document +# - use separate hand-writing code +# - use separate list-select code + + +import sys, os, time +import pygtk, gtk, pango +import gobject + +########################################################### +# Writing recognistion code +import math + + +def LoadDict(dict): + # Upper case. + # Where they are like lowercase, we either double + # the last stroke (L, J, I) or draw backwards (S, Z, X) + # U V are a special case + + dict.add('A', "R(4)6,8") + dict.add('B', "R(4)6,4.R(7)1,6") + dict.add('B', "R(4)6,4.L(4)2,8.R(7)1,6") + dict.add('B', "S(6)7,1.R(4)6,4.R(7)0,6") + dict.add('C', "R(4)8,2") + dict.add('D', "R(4)6,6") + dict.add('E', "L(1)2,8.L(7)2,8") + # double the stem for F + dict.add('F', "L(4)2,6.S(3)7,1") + dict.add('F', "S(1)5,3.S(3)1,7.S(3)7,1") + + dict.add('G', "L(4)2,5.S(8)1,7") + dict.add('G', "L(4)2,5.R(8)6,8") + # FIXME I need better straight-curve alignment + dict.add('H', "S(3)1,7.R(7)6,8.S(5)7,1") + dict.add('H', "L(3)0,5.R(7)6,8.S(5)7,1") + # capital I is down/up + dict.add('I', "S(4)1,7.S(4)7,1") + + # Capital J has a left/right tail + dict.add('J', "R(4)1,6.S(7)3,5") + + dict.add('K', "L(4)0,2.R(4)6,6.L(4)2,8") + + # Capital L, like J, doubles the foot + dict.add('L', "L(4)0,8.S(7)4,3") + + dict.add('M', "R(3)6,5.R(5)3,8") + dict.add('M', "R(3)6,5.L(1)0,2.R(5)3,8") + + dict.add('N', "R(3)6,8.L(5)0,2") + + # Capital O is CW, but can be CCW in special dict + dict.add('O', "R(4)1,1", bot='0') + + dict.add('P', "R(4)6,3") + dict.add('Q', "R(4)7,7.S(8)0,8") + + dict.add('R', "R(4)6,4.S(8)0,8") + + # S is drawn bottom to top. + dict.add('S', "L(7)6,1.R(1)7,2") + + # Double the stem for capital T + dict.add('T', "R(4)0,8.S(5)7,1") + + # U is L to R, V is R to L for now + dict.add('U', "L(4)0,2") + dict.add('V', "R(4)2,0") + + dict.add('W', "R(5)2,3.L(7)8,6.R(3)5,0") + dict.add('W', "R(5)2,3.R(3)5,0") + + dict.add('X', "R(4)6,0") + + dict.add('Y',"L(1)0,2.R(5)4,6.S(5)6,2") + dict.add('Y',"L(1)0,2.S(5)2,7.S(5)7,2") + + dict.add('Z', "R(4)8,2.L(4)6,0") + + # Lower case + dict.add('a', "L(4)2,2.L(5)1,7") + dict.add('a', "L(4)2,2.L(5)0,8") + dict.add('a', "L(4)2,2.S(5)0,8") + dict.add('b', "S(3)1,7.R(7)6,3") + dict.add('c', "L(4)2,8", top='C') + dict.add('d', "L(4)5,2.S(5)1,7") + dict.add('d', "L(4)5,2.L(5)0,8") + dict.add('e', "S(4)3,5.L(4)5,8") + dict.add('e', "L(4)3,8") + dict.add('f', "L(4)2,6", top='F') + dict.add('f', "S(1)5,3.S(3)1,7", top='F') + dict.add('g', "L(1)2,2.R(4)1,6") + dict.add('h', "S(3)1,7.R(7)6,8") + dict.add('h', "L(3)0,5.R(7)6,8") + dict.add('i', "S(4)1,7", top='I', bot='1') + dict.add('j', "R(4)1,6", top='J') + dict.add('k', "L(3)0,5.L(7)2,8") + dict.add('k', "L(4)0,5.R(7)6,6.L(7)1,8") + dict.add('l', "L(4)0,8", top='L') + dict.add('l', "S(3)1,7.S(7)3,5", top='L') + dict.add('m', "S(3)1,7.R(3)6,8.R(5)6,8") + dict.add('m', "L(3)0,2.R(3)6,8.R(5)6,8") + dict.add('n', "S(3)1,7.R(4)6,8") + dict.add('o', "L(4)1,1", top='O', bot='0') + dict.add('p', "S(3)1,7.R(4)6,3") + dict.add('q', "L(1)2,2.L(5)1,5") + dict.add('q', "L(1)2,2.S(5)1,7.R(8)6,2") + dict.add('q', "L(1)2,2.S(5)1,7.S(5)1,7") + # FIXME this double 1,7 is due to a gentle where the + # second looks like a line because it is narrow.?? + dict.add('r', "S(3)1,7.R(4)6,2") + dict.add('s', "L(1)2,7.R(7)1,6", top='S', bot='5') + dict.add('t', "R(4)0,8", top='T', bot='7') + dict.add('t', "S(1)3,5.S(5)1,7", top='T', bot='7') + dict.add('u', "L(4)0,2.S(5)1,7") + dict.add('v', "L(4)0,2.L(2)0,2") + dict.add('w', "L(3)0,2.L(5)0,2", top='W') + dict.add('w', "L(3)0,5.R(7)6,8.L(5)3,2", top='W') + dict.add('w', "L(3)0,5.L(5)3,2", top='W') + dict.add('x', "L(4)0,6", top='X') + dict.add('y', "L(1)0,2.R(5)4,6", top='Y') # if curved + dict.add('y', "L(1)0,2.S(5)2,7", top='Y') + dict.add('z', "R(4)0,6.L(4)2,8", top='Z', bot='2') + + # Digits + dict.add('0', "L(4)7,7") + dict.add('0', "R(4)7,7") + dict.add('1', "S(4)7,1") + dict.add('2', "R(4)0,6.S(7)3,5") + dict.add('2', "R(4)3,6.L(4)2,8") + dict.add('3', "R(1)0,6.R(7)1,6") + dict.add('4', "L(4)7,5") + dict.add('5', "L(1)2,6.R(7)0,3") + dict.add('5', "L(1)2,6.L(4)0,8.R(7)0,3") + dict.add('6', "L(4)2,3") + dict.add('7', "S(1)3,5.R(4)1,6") + dict.add('7', "R(4)0,6") + dict.add('7', "R(4)0,7") + dict.add('8', "L(4)2,8.R(4)4,2.L(3)6,1") + dict.add('8', "L(1)2,8.R(7)2,0.L(1)6,1") + dict.add('8', "L(0)2,6.R(7)0,1.L(2)6,0") + dict.add('8', "R(4)2,6.L(4)4,2.R(5)8,1") + dict.add('9', "L(1)2,2.S(5)1,7") + + dict.add(' ', "S(4)3,5") + dict.add('', "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("", "S(4)5,3.S(3)3,5") + dict.add("","S(4)3,5.S(5)5,3") + dict.add("", "S(4)7,1.S(1)1,7") # "" + dict.add("","S(4)1,7.S(7)7,1") # "" + dict.add("", "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 == "": + self.entry.emit("backspace") + elif info == "": + self.entry.emit("activate") + else: + self.entry.emit("insert-at-cursor",info) + #print "Got Sym ", info + + def add_col(self, sym, col): + c = gtk.gdk.color_parse(col) + gc = self.widget.window.new_gc() + gc.set_foreground(self.widget.get_colormap().alloc_color(c)) + self.colours[sym] = gc + + def set_zoom(self, zoom): + if zoom > 50: + zoom = 50 + if zoom < 20: + zoom = 20 + if zoom == self.zoom: + return + self.need_redraw = True + self.zoom = zoom + s = pango.SCALE + for i in range(zoom): + s = s * 11 / 10 + self.fd.set_absolute_size(s) + self.widget.modify_font(self.fd) + met = self.widget.get_pango_context().get_metrics(self.fd) + + self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE + self.lineascent = met.get_ascent() / pango.SCALE + self.widget.queue_draw() + + def set_search(self, str): + self.search = str + self.need_redraw = True + self.widget.queue_draw() + + def reconfig(self, w, ev): + alloc = w.get_allocation() + if not self.pixbuf: + return + if alloc.width != self.width or alloc.height != self.height: + self.pixbuf = None + self.need_redraw = True + + def redraw(self, w, ev): + if self.colours == None: + self.colours = {} + self.add_col('N', "blue") # need + self.add_col('F', "darkgreen") # found + self.add_col('C', "red") # Cannot find + self.add_col('R', "orange")# Regular + self.add_col('X', "black") # No Need + self.add_col(' ', "white") # selected background + self.add_col('_', "black") # location separator + self.add_col('title', "cyan") + self.bg = self.widget.get_style().bg_gc[gtk.STATE_NORMAL] + self.writing.set_colour(self.colours['_']) + + + if self.need_redraw: + self.draw_buf() + + self.widget.window.draw_drawable(self.bg, self.pixbuf, 0, 0, 0, 0, + self.width, self.height) + + def draw_buf(self): + self.need_redraw = False + p = self.widget + if self.pixbuf == None: + alloc = p.get_allocation() + self.pixbuf = gtk.gdk.Pixmap(p.window, alloc.width, alloc.height) + self.width = alloc.width + self.height = alloc.height + self.pixbuf.draw_rectangle(self.bg, True, 0, 0, + self.width, self.height) + + if self.plist == None: + # Empty list, say so. + layout = self.widget.create_pango_layout("List Is Empty") + (ink, log) = layout.get_pixel_extents() + (ex,ey,ew,eh) = log + self.pixbuf.draw_layout(self.colours['X'], (self.width-ew)/2, + (self.height-eh)/2, + layout) + return + + # find max width and height + maxw = 1; maxh = 1; longest = "nothing" + curr = None; top = 0 + visible = [] + curloc = None + for p in self.plist: + + if self.search == None and p.state == 'X': + # Don't normally show "noneed" entries + if p.prod == self.current and self.gonext: + curr = len(visible) + continue + if self.search != None and products[p.prod].name.lower().find(self.search.lower()) < 0: + # doesn't contain search string + continue + while p.loc() != curloc: + if not self.all_headers: + curloc = p.loc() + elif curloc == None: + curloc = -1 + else: + if curloc < 0: + i = -1 + else: + i = locorder[place].index(curloc) + + if i < len(locorder[place]) - 1: + curloc = locorder[place][i+1] + else: + break + if curloc < 0: + locstr = "Unknown" + elif curloc < len(locations[place]): + locstr = locations[place][curloc] + else: + locstr = None + + if locstr == None: + break + + if self.top == locstr: + top = len(visible) + visible.append(locstr) + + layout = self.widget.create_pango_layout(locstr) + (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents() + if ew > maxw: maxw = ew; longest = products[p.prod].name + if eh > maxh: maxh = eh + + if p.prod == self.top: + top = len(visible) + if curr != None and self.gonext and self.gonext == p.state: + self.gonext = False + self.current = p.prod + self.callout(p, 'auto') + if p.prod == self.current: + curr = len(visible) + visible.append(p) + layout = self.widget.create_pango_layout(products[p.prod].name) + (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents() + if ew > maxw: maxw = ew; longest = products[p.prod].name + if eh > maxh: maxh = eh + # print "mw=%d mh=%d lh=%d la=%d" % (maxw, maxh, self.lineheight, self.lineascent) + + if self.all_headers: + # any following headers with no items visible + while True: + if curloc == None: + curloc = -1 + else: + if curloc < 0: + i = -1 + else: + i = locorder[place].index(curloc) + + if i < len(locorder[place]) - 1: + curloc = locorder[place][i+1] + else: + break + if curloc < 0: + locstr = "Unknown" + elif curloc < len(locations[place]): + locstr = locations[place][curloc] + else: + locstr = None + + if locstr == None: + break + + if self.top == locstr: + top = len(visible) + visible.append(locstr) + + layout = self.widget.create_pango_layout(locstr) + (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents() + if ew > maxw: maxw = ew; longest = products[p.prod].name + if eh > maxh: maxh = eh + + self.gonext = False + truemaxw = maxw + maxw = int(maxw * 11/10) + if maxh > self.lineheight: + self.lineheight = maxh + # Find max rows/columns + rows = int(self.height / self.lineheight) + cols = int(self.width / maxw) + if rows < 1 or cols < 1: + if self.zoom > 10: + self.set_zoom(self.zoom - 1) + self.need_redraw = True + self.widget.queue_draw() + return + #print "rows=%d cols=%s" % (rows,cols) + colw = int(self.width / cols) + offset = (colw - truemaxw)/2 + self.offset = offset + + # check 'curr' is in appropriate range and + # possibly adjust 'top'. Then trim visible to top. + # Allow one blank line at the end. + cells = rows * cols + if cells >= len(visible): + # display everything + top = 0 + elif curr != None: + # make sure curr is in good range + if curr - top < rows/3: + top = curr - (cells - rows/3) + if top < 0: + top = 0 + if (cells - (curr - top)) < rows/3: + top = curr - rows / 3 + if len(visible) - top < cells-1: + top = len(visible) - (cells-1) + if top < 0: + top = 0 + else: + if len(visible) - top < cells-1: + top = len(visible) - (cells-1) + if top < 0: + top = 0 + + visible = visible[top:] + self.top = None + + self.visible = visible + self.rows = rows + self.cols = cols + + for r in range(rows): + for c in range(cols): + pos = c*rows+r + uline = False + if pos < len(visible): + if type(visible[pos]) == str: + strng = visible[pos] + state = 'title' + comment = False + if self.top == None: + self.top = visible[pos] + else: + strng = products[visible[pos].prod].name + uline = products[visible[pos].prod].regular + state = visible[pos].state + if self.top == None: + self.top = visible[pos].prod + comment = (not not visible[pos].comment) + else: + break + layout = self.widget.create_pango_layout(strng) + if uline: + a = pango.AttrList() + a.insert(pango.AttrUnderline(pango.UNDERLINE_SINGLE,0,len(strng))) + layout.set_attributes(a) + if curr != None and c*rows+r == curr - top: + self.pixbuf.draw_rectangle(self.colours[' '], True, + c*colw, r*self.lineheight, + colw, self.lineheight) + if state == 'title': + self.pixbuf.draw_rectangle(self.colours['_'], True, + c*colw+2, r*self.lineheight, + colw-4, self.lineheight) + self.pixbuf.draw_rectangle(self.colours['_'], False, + c*colw+2, r*self.lineheight, + colw-4-1, self.lineheight-1) + + if comment: + if offset > self.lineheight * 0.8: + w = int (self.lineheight * 0.8 * 0.9) + o = (offset - w) / 2 + else: + w = int(offset*0.9) + o = int((offset-w)/2) + vo = int((self.lineheight-w)/2) + self.pixbuf.draw_rectangle(self.colours[state], True, + c * colw + o, r * self.lineheight + vo, + w, w) + + + self.pixbuf.draw_layout(self.colours[state], + c * colw + offset, + r * self.lineheight, + layout) + + + def click(self, w, ev): + cw = self.width / self.cols + rh = self.lineheight + col = int(ev.x/cw) + row = int(ev.y/rh) + cell = col * self.rows + row + cellpos = ev.x - col * cw + pos = 0 + if cellpos < self.lineheight or cellpos < self.offset: + pos = -1 + elif cellpos > cw - self.lineheight: + pos = 1 + + if cell < len(self.visible) and type(self.visible[cell]) != str: + if pos == 0: + prod = self.visible[cell].prod + layout = self.widget.create_pango_layout(products[prod].name) + (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents() + if cellpos > self.offset + ew: + pos = 1 + self.callout(self.visible[cell], pos) + if cell < len(self.visible) and type(self.visible[cell]) == str: + self.callout(self.visible[cell], 'heading') + + + def set_purch(self, purch): + if purch: + self.plist = purch + self.need_redraw = True + self.widget.queue_draw() + + def select(self, item = None, gonext = False): + if gonext: + self.gonext = gonext + elif item != None: + self.current = item + self.need_redraw = True + self.widget.queue_draw() + + def show_headers(self, all): + self.all_headers = all + self.need_redraw = True + self.widget.queue_draw() + +class ShoppingList: + + def __init__(self): + window = gtk.Window(gtk.WINDOW_TOPLEVEL) + window.connect("destroy", self.close_application) + window.set_title("Shopping List") + + self.purch = None + self.isize = gtk.icon_size_register("mine", 40, 40) + + # try to guess where user is so when he tried to change the + # location of something, we try 'here' first. + # Update this whenever we find something, or set a location. + # We clear to -1 when we change place + self.current_loc = -1 + + vb = gtk.VBox() + window.add(vb) + vb.show() + + top = gtk.HBox() + top.set_size_request(-1,40) + vb.pack_start(top, expand=False) + top.show() + + glob = gtk.HBox() + glob.set_size_request(-1,80) + glob.set_homogeneous(True) + vb.pack_end(glob, expand=False) + glob.show() + self.glob_control = glob + + locedit = gtk.HBox() + locedit.set_size_request(-1, 80) + vb.pack_end(locedit, expand=False) + locedit.hide() + self.locedit = locedit + + loc = gtk.HBox() + loc.set_size_request(-1,80) + vb.pack_end(loc, expand=False) + loc.hide() + self.loc = loc + + placeb = gtk.HBox() + placeb.set_size_request(-1,80) + vb.pack_end(placeb, expand=False) + placeb.hide() + self.place = placeb + + filemenu = gtk.HBox() + filemenu.set_homogeneous(True) + filemenu.set_size_request(-1,80) + vb.pack_end(filemenu, expand=False) + filemenu.hide() + self.filemenu = filemenu + + curr = gtk.HBox() + curr.set_size_request(-1,80) + curr.set_homogeneous(True) + vb.pack_end(curr, expand=False) + curr.show() + self.curr = curr + self.mode = 'curr' + + e = gtk.Entry() + + l = PurchView(34, self.item_selected, e) + vb.add(l.widget) + self.lview = l + + # multi use text-entry body + # used for search-string, comment-entry, item-entry/rename + # + ctx = e.get_pango_context() + fd = ctx.get_font_description() + fd.set_absolute_size(25*pango.SCALE) + e.modify_font(fd) + e.show() + top.add(e) + self.button(gtk.STOCK_OK, top, self.enter_change, expand = False) + self.entry = e + global XX + XX = e + self.entry_ignore = True + self.entry_mode = "comment" + e.connect("changed", self.entry_changed) + self.ecol_search = None + self.ecol_comment = None + self.ecol_item = None + self.ecol_loc = None + + # global control buttons + self.button(gtk.STOCK_REFRESH, glob, self.choose_place) + self.button(gtk.STOCK_PREFERENCES, glob, self.show_controls) + self.search_toggle = self.button(gtk.STOCK_FIND, glob, self.toggle_search, toggle=True) + self.button(gtk.STOCK_ADD, glob, self.add_item) + self.button(gtk.STOCK_EDIT, glob, self.change_name) + + # buttons to control current entry + self.button(gtk.STOCK_APPLY, curr, self.tick) + self.button(gtk.STOCK_CANCEL, curr, self.cross) + self.button(gtk.STOCK_JUMP_TO, curr, self.choose_loc) + self.button(gtk.STOCK_ZOOM_IN, curr, self.zoomin) + self.button(gtk.STOCK_ZOOM_OUT, curr, self.zoomout) + + # buttons for whole-list operations + self.button(gtk.STOCK_SAVE_AS, filemenu, self.record) + self.button(gtk.STOCK_HOME, filemenu, self.reset) + a = self.button(gtk.STOCK_CONVERT, filemenu, self.add_regulars) + self.add_reg_clicked = False + a.connect('leave',self.clear_regulars) + self.save_button = self.button(gtk.STOCK_SAVE, filemenu, self.save) + self.revert_button = self.button(gtk.STOCK_REVERT_TO_SAVED, filemenu, self.revert_to_saved) + self.revert_button.hide() + self.button(gtk.STOCK_QUIT, filemenu, self.close_application) + + # Buttons to change location of current entry + self.button(gtk.STOCK_GO_BACK, loc, self.prevloc, expand = False) + self.curr_loc = self.button("here", loc, self.curr_switch) + self.button(gtk.STOCK_GO_FORWARD, loc, self.nextloc, expand = False) + l = self.curr_loc.child + ctx = l.get_pango_context() + fd = ctx.get_font_description() + fd.set_absolute_size(25*pango.SCALE) + l.modify_font(fd) + + # buttons to edit the current location + self.button(gtk.STOCK_DELETE, locedit, self.locdelete) + self.button(gtk.STOCK_GO_UP, locedit, self.loc_move_up) + self.button(gtk.STOCK_GO_DOWN, locedit, self.loc_move_down) + self.button(gtk.STOCK_ADD, locedit, self.add_item) + self.button(gtk.STOCK_EDIT, locedit, self.change_name) + + + # Buttons to change current 'place' + self.button(gtk.STOCK_MEDIA_REWIND, placeb, self.prevplace, expand = False) + self.curr_place = self.button("HOME", placeb, self.curr_switch) + self.button(gtk.STOCK_MEDIA_FORWARD, placeb, self.nextplace, expand = False) + l = self.curr_place.child + ctx = l.get_pango_context() + fd = ctx.get_font_description() + fd.set_absolute_size(25*pango.SCALE) + l.modify_font(fd) + + + window.set_default_size(480, 640) + window.show() + + + def close_application(self, widget): + gtk.main_quit() + if table_timeout: + table_tick() + if list_timeout: + list_tick() + + def item_selected(self, purch, pos): + if pos == 'heading': + if self.mode != 'loc': + return + newloc = None + for i in range(len(locations[place])): + if locations[place][i] == purch: + newloc = i + if i == None: + return + extend_array(products[self.purch.prod].loc, place, -1) + products[self.purch.prod].loc[place] = newloc + table_changed() + self.lview.plist.sort(purch_cmp) + self.set_loc() + self.lview.select() + return + + if self.entry_mode == "comment": + self.lview.select(purch.prod) + self.purch = purch + self.entry_ignore = True + self.entry.set_text(purch.comment) + self.entry_ignore = False + self.set_loc() + #if pos < 0: + # self.tick(None) + #if pos > 0: + # self.cross(None) + if self.entry_mode == "search": + if pos != 'auto': + if purch.state == 'X' or purch.state == 'R': + purch.state = 'N' + elif purch.state == 'N': + purch.state = 'X' + self.lview.select(purch.prod) + self.purch = purch + self.set_loc() + list_changed() + + + def entry_changed(self, widget): + if self.entry_ignore: + return + if self.entry_mode == "search": + self.lview.set_search(self.entry.get_text()) + if self.entry_mode == "comment" and self.purch != None: + self.purch.comment = self.entry.get_text() + list_changed() + + def set_purch(self, purch): + self.lview.set_purch(purch) + + def button(self, name, bar, func, expand = True, toggle = False): + if toggle: + btn = gtk.ToggleButton() + else: + btn = gtk.Button() + if type(name) == str and name[0:3] != "gtk": + if not expand: + name = " " + name + " " + btn.set_label(name) + else: + img = gtk.image_new_from_stock(name, self.isize) + if not expand: + img.set_size_request(80, -1) + img.show() + btn.add(img) + btn.show() + bar.pack_start(btn, expand = expand) + btn.connect("clicked", func) + btn.set_focus_on_click(False) + return btn + + def record(self, widget): + # Record current purchase file in datestamped storage + save_list(time.strftime("purch-%Y%m%d-%H")) + + def reset(self, widget): + # Reset the shopping list. + # All regular to noneed + # if there were no regular, then + # found -> noneed + # cannot find -> need + found = False + for p in purch: + if p.state == 'R': + p.state = 'X' + found = True + if not found: + for p in purch: + if p.state == 'F': + p.state = 'X' + if p.state == 'C': + p.state = 'N' + list_changed() + self.lview.select() + + def add_regulars(self, widget): + if self.add_reg_clicked: + return self.all_regulars(widget) + # Mark all regulars (not already selected) as regulars + for p in purch: + if products[p.prod].regular and p.state == 'X': + p.state = 'R' + list_changed() + self.lview.select() + self.add_reg_clicked = True + + def all_regulars(self, widget): + # Mark all regulars and don'twant (not already selected) as regulars + for p in purch: + if p.state == 'X': + p.state = 'R' + list_changed() + self.lview.select() + def clear_regulars(self, widget): + self.add_reg_clicked = False + + def save(self, widget): + table_tick() + list_tick() + self.curr_switch(widget) + + + def setecol(self): + if self.ecol_search != None: + return + c = gtk.gdk.color_parse("yellow") + self.ecol_search = c + + c = gtk.gdk.color_parse("white") + self.ecol_comment = c + + c = gtk.gdk.color_parse("pink") + self.ecol_item = c + + c = gtk.gdk.color_parse('grey90') + self.ecol_loc = c + + def toggle_search(self, widget): + self.setecol() + if self.entry_mode == "item": + self.search_toggle.set_active(False) + return + if self.entry_mode == "search": + self.entry_ignore = True + self.entry.set_text(self.purch.comment) + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_comment) + self.entry_mode = "comment" + self.lview.set_search(None) + self.entry_ignore = False + return + self.entry_mode = "search" + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_search) + self.entry_ignore = True + self.entry.set_text("") + self.lview.set_search("") + self.entry_ignore = False + self.search_toggle.set_active(True) + + def choose_loc(self, widget): + # replace 'curr' buttons with 'loc' buttons + if self.purch == None: + return + self.curr.hide() + self.filemenu.hide() + self.loc.show() + self.locedit.show() + self.glob_control.hide() + self.mode = 'loc' + self.set_loc() + self.lview.show_headers(True) + + def set_loc(self): + loc = locname(self.purch) + self.current_loc = self.purch.loc() + self.curr_loc.child.set_text(places[place]+" / "+loc) + + def curr_switch(self, widget): + # set current item to current location, and switch back + self.lview.show_headers(False) + self.loc.hide() + self.locedit.hide() + self.glob_control.show() + self.place.hide() + self.filemenu.hide() + self.curr.show() + self.mode = 'curr' + + def show_controls(self, widget): + if self.mode == 'filemenu': + self.curr_switch(widget) + else: + self.lview.show_headers(False) + self.loc.hide() + self.place.hide() + self.curr.hide() + self.locedit.hide() + self.glob_control.show() + self.filemenu.show() + self.mode = 'filemenu' + + + def nextloc(self, widget): + if self.entry_mode != 'comment': + self.enter_change(None) + if self.current_loc != -1 and self.current_loc != self.purch.loc(): + newloc = self.current_loc + self.current_loc = -1 + elif self.purch.loc() < 0: + newloc = locorder[place][0] + else: + i = locorder[place].index(self.purch.loc()) + if i < len(locorder[place])-1: + newloc = locorder[place][i+1] + else: + return + + + if newloc < -1 or newloc >= len(locations[place]): + return + extend_array(products[self.purch.prod].loc, place, -1) + products[self.purch.prod].loc[place] = newloc + table_changed() + self.lview.plist.sort(purch_cmp) + self.set_loc() + self.lview.select() + + def prevloc(self, widget): + if self.entry_mode != 'comment': + self.enter_change(None) + if self.current_loc != -1 and self.current_loc != self.purch.loc(): + newloc = self.current_loc + self.current_loc = -1 + elif self.purch.loc() < 0: + return + else: + i = locorder[place].index(self.purch.loc()) + if i > 0: + newloc = locorder[place][i-1] + else: + newloc = -1 + + if newloc < -1: + return + extend_array(products[self.purch.prod].loc, place, -1) + products[self.purch.prod].loc[place] = newloc + table_changed() + self.lview.plist.sort(purch_cmp) + self.set_loc() + self.lview.select() + + def locdelete(self, widget): + # merge this location with the previous one + # So every product with this location needs to be changed, + # and the locorder updated. + l = self.purch.loc() + if l < 0: + # cannot delete 'Unknown' + return + i = locorder[place].index(l) + if i == 0: + # nothing to merge with + return + safe.backup_table() + newl = locorder[place][i-1] + for p in products: + if p != None: + if len(p.loc) > place: + if p.loc[place] == l: + p.loc[place] = newl + locorder[place][i:i+1] = [] + + table_changed() + self.lview.plist.sort(purch_cmp) + self.set_loc() + self.lview.select() + + def loc_move_up(self, widget): + l = self.purch.loc() + if l < 0: + # Cannot move 'unknown' + return + i = locorder[place].index(l) + if i == 0: + # nowhere to move + pass + else: + o = locorder[place][i-1:i+1] + locorder[place][i-1:i+1] = [o[1],o[0]] + table_changed() + self.lview.plist.sort(purch_cmp) + self.set_loc() + self.lview.select() + + def loc_move_down(self, widget): + l = self.purch.loc() + if l < 0: + # Cannot move 'unknown' + return + i = locorder[place].index(l) + if i+1 >= len(locorder[place]): + # nowhere to move + pass + else: + o = locorder[place][i:i+2] + locorder[place][i:i+2] = [o[1],o[0]] + table_changed() + self.lview.plist.sort(purch_cmp) + self.set_loc() + self.lview.select() + + + def choose_place(self, widget): + if self.entry_mode != 'comment': + self.enter_change(None) + if self.mode == 'place': + self.curr_switch(widget) + return + self.pl_visible = True + self.lview.show_headers(False) + self.loc.hide() + self.locedit.hide() + self.glob_control.show() + self.curr.hide() + self.filemenu.hide() + self.mode = 'place' + self.place.show() + self.set_place() + + def set_place(self): + global place + if place >= len(places): + place = len(places) - 1 + if place < 0: + place = 0 + if place >= len(places): + pl = "Unknown Place" + else: + pl = places[place] + self.curr_place.child.set_text(pl) + self.current_loc = -1 + + def nextplace(self, widget): + global place + if place >= len(places)-1: + return + place += 1 + self.lview.plist.sort(purch_cmp) + self.set_place() + if self.purch: + self.lview.select() + else: + self.lview.set_purch(None) + + def prevplace(self, widget): + global place + if place <= 0: + return + place -= 1 + self.lview.plist.sort(purch_cmp) + self.set_place() + if self.purch: + self.lview.select() + else: + self.lview.set_purch(None) + + + def zoomin(self, widget): + self.lview.set_zoom(self.lview.zoom+1) + def zoomout(self, widget): + self.lview.set_zoom(self.lview.zoom-1) + + def tick(self, widget): + if self.purch != None: + if self.entry_mode == "search": + # set to regular + products[self.purch.prod].regular = True + #self.purch.state = 'R' + self.lview.select(gonext=self.purch.state) + list_changed(); table_changed() + return + oldstate = self.purch.state + if self.purch.state == 'N': self.purch.state = 'F' + elif self.purch.state == 'C': self.purch.state = 'F' + elif self.purch.state == 'R': self.purch.state = 'N' + elif self.purch.state == 'X': self.purch.state = 'N' + if self.purch.state == 'F': + self.current_loc = self.purch.loc() + self.lview.select(gonext = oldstate) + list_changed() + def cross(self, widget): + if self.purch != None: + oldstate = self.purch.state + if self.entry_mode == "search": + # set to regular + products[self.purch.prod].regular = False + if self.purch.state == 'R': + self.purch.state = 'X' + self.lview.select(gonext=oldstate) + list_changed(); table_changed() + return + + if self.purch.state == 'N': self.purch.state = 'C' + elif self.purch.state == 'F': self.purch.state = 'N' + elif self.purch.state == 'C': self.purch.state = 'X' + elif self.purch.state == 'R': self.purch.state = 'X' + elif self.purch.state == 'X': self.purch.state = 'X' + self.lview.select(gonext = oldstate) + list_changed() + + def add_item(self, widget): + global place + self.setecol() + if self.entry_mode == "search": + self.search_toggle.set_active(False) + if self.entry_mode != "comment": + return + if self.mode == 'curr': + self.entry_mode = "item" + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_item) + self.entry_ignore = True + self.entry.set_text("") + self.purch = None + self.lview.select() + self.entry_ignore = False + elif self.mode == 'loc': + if self.purch == None: + return + if None in locations[place]: + lnum = locations[place].index(None) + else: + lnum = len(locations[place]) + locations[place].append(None) + locations[place][lnum] = 'NewLocation' + if self.purch.loc() == -1: + so = 0 + else: + so = locorder[place].index(self.purch.loc())+1 + locorder[place][so:so] = [lnum] + self.nextloc(None) + self.entry_mode = 'location' + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc) + self.entry_ignore = True + self.entry.set_text('NewLocation') + self.entry_ignore = False + elif self.mode == 'place': + if None in places: + pnum = places.index(None) + else: + pnum = len(places) + places.append(None) + places[pnum] = 'NewPlace' + extend_array(locations, pnum) + locations[pnum] = [] + extend_array(locorder, pnum) + locorder[pnum] = [] + place = pnum + self.lview.plist.sort(purch_cmp) + self.set_place() + self.entry_mode = 'place' + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc) + self.entry_ignore = True + self.entry.set_text('NewPlace') + self.entry_ingore = False + def change_name(self, widget): + self.setecol() + if self.entry_mode == "search": + self.search_toggle.set_active(False) + if self.entry_mode == "item": + if self.purch != None: + self.entry.set_text(products[self.purch.prod].name) + return + if self.entry_mode == "location": + if self.purch != None: + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc) + self.entry.set_text(locname(self.purch)) + return + if self.entry_mode == 'place': + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc) + set.entry.set_text(places[place]) + return + if self.entry_mode != "comment": + return + if self.mode == 'curr': + if self.purch == None: + return + self.entry_mode = "item" + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_item) + self.entry_ignore = True + self.entry.set_text(products[self.purch.prod].name) + self.entry_ignore = False + elif self.mode == 'loc': + if self.purch == None: + return + if self.purch.loc() < 0: + return + self.entry_mode = "location" + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc) + self.entry_ignore = True + self.entry.set_text(locname(self.purch)) + self.entry_ignore = False + elif self.mode == 'place': + self.entry_mode = 'place' + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc) + self.entry_ignore = True + self.entry.set_text(places[place]) + self.entry_inode = False + + # + # An item is being added or renamed. Commit the change + # If the new name is empty, that implys a delete. + # We only allow the delete if the state is 'X' and not regular + def update_item(self, name): + if len(name) > 0: + if self.purch == None: + # check for duplicates FIXME + num = len(products) + prod = Prod(num, name) + products.append(prod) + p = Purch(prod) + purch.append(p) + p.state = "N"; + self.purch = p + self.set_purch(purch) + self.lview.select(num) + self.lview.plist.sort(purch_cmp) + else: + products[self.purch.prod].name = name + self.lview.select() + self.lview.plist.sort(purch_cmp) + self.forget_backup() + table_changed() + list_changed() + elif self.purch: + # delete? + if self.purch.state == 'N': + # OK to delete + products[self.purch.prod] = None + try: + i = purch.index(self.purch) + except: + pass + else: + if i == 0: + new = -1 + else: + new = purch[i-1].prod + del purch[i] + self.lview.plist.sort(purch_cmp) + self.lview.select(new) + table_changed() + list_changed() + self.forget_backup() + + def update_location(self, name): + if len(name) > 0: + locations[place][self.purch.loc()] = name + self.set_loc() + self.lview.select() + table_changed() + return + # See if we can delete this location + # need to check all products that they aren't 'here' + for p in products: + if p and p.num != self.purch.prod and place < len(p.loc): + if p.loc[place] == self.purch.loc(): + return + # nothing here except 'purch' + l = self.purch.loc() + self.prevloc(None) + locations[place][l] = None + locorder[place].remove(l) + self.lview.plist.sort(purch_cmp) + self.lview.select() + table_changed() + list_changed() + + def update_place(self, name): + global place + if len(name) > 0: + places[place] = name + self.lview.select() + self.set_place() + table_changed() + return + if len(places) <= 1: + return + + self.backup_table() + places[place:place+1] = [] + locations[place:place+1] = [] + locorder[place:place+1] = [] + if place >= len(places): + place -= 1 + table_changed() + self.set_place() + self.lview.select() + + def backup_table(self): + save_table("Products.backup") + self.save_button.hide() + self.revert_button.show() + + def forget_backup(self): + self.save_button.show() + self.revert_button.hide() + + def revert_to_saved(self, widget): + try: + f = open("Products.backup") + products = [] + locations = [] + locorder = [] + places = [] + load_table(f) + f.close() + except: + pass + + self.forget_backup() + + + def enter_change(self, widget): + name = self.entry.get_text() + mode = self.entry_mode + # This is need to avoid recursion as update_* calls {next,prev}loc + self.entry_mode = 'comment' + if mode == 'item': + self.update_item(name) + elif mode == 'location': + self.update_location(name) + elif mode == 'place': + self.update_place(name) + + self.entry_mode = "comment" + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_comment) + self.entry_ignore = True + self.entry.set_text("") + self.entry_ignore = False + +def main(): + gtk.main() + return 0 + +if __name__ == "__main__": + + home = os.getenv("HOME") + p = os.path.join(home, "shopping") + if os.path.exists(p): + os.chdir(p) + else: + os.chdir(home) + + products = [] + locations = [] + locorder = [] + places = [] + try: + f = open("Products") + load_table(f) + f.close() + except: + places = ['Home'] + locorder = [[]] + locations =[[]] + + purch = [] + try: + f = open("Purchases") + except: + pass + else: + load_list(f) + f.close() + merge_list(purch, products) + + + place = 0 + purch.sort(purch_cmp) + + sl = ShoppingList() + sl.set_purch(purch) + ss = gtk.settings_get_default() + ss.set_long_property("gtk-cursor-blink", 0, "shop") + main() diff --git a/Shop/t.table b/Shop/t.table new file mode 100644 index 0000000..c51a30c --- /dev/null +++ b/Shop/t.table @@ -0,0 +1,4 @@ +P,0:Home,1:Coles-EG,2:Woolies-HD +L0,0:Pantry,1:Freezer,2:Fridge,3:Laundry,4:Bathroom,5:Highcupboard +L1,0:F&V,1:Deli,2:Bakery,3:Meat,4:A1,5:A2,6:A3,7:None,8:None,9:Fridge +L2,0:F&V,1:Bakery,2:None,3:Deli,4:Fridge,5:Bread,6:Choc diff --git a/Shop/test.list b/Shop/test.list new file mode 100644 index 0000000..8baf98d --- /dev/null +++ b/Shop/test.list @@ -0,0 +1,4 @@ +12,N, +3,N, +36,N,bob +25,N,twinings diff --git a/Shop/test.table b/Shop/test.table new file mode 100644 index 0000000..7662f28 --- /dev/null +++ b/Shop/test.table @@ -0,0 +1,39 @@ +P,0:Home,1:Coles-EG,2:Woolies-HD +L0,0:Pantry,1:Freezer,2:Fridge,3:Laundry,4:Bathroom,5:Highcupboard +L1,8:Cereal,12:Roast,0:F&V,1:Deli,2:Bakery,3:Meat,4:A1,9:FrontDoor,11:Cheeses,10:M Aisle,5:zzzz,6:JuiceAisle +L2,0:F&V,1:Bakery,2:No thing2,3:Deli,4:Fridge,5:Bread,6:Choc +I0,Cheese sliced,R,L0:2,L1:11,L2:4 +I1,Milk,R,L0:2,L1:10,L2:4 +I2,Bread,R,L0:5 +I3,Weetbix,R,L0:0,L1:6 +I4,Allbran,I,L0:0,L1:5 +I5,Tin Fruit,I,L0:0 +I6,Yoghurt,I,L0:2 +I8,Juice,I,L1:6 +I9,Sugar,I,L0:5 +I10,Flour,I +I11,Sugar Icing,I,L0:0,L1:9 +I12,Sugar Brown,I,L0:0,L1:5 +I13,Fruit,I +I14,Veges,R,L0:2,L1:12 +I15,RoastingVeges,R,L0:2,L1:12 +I17,Tomato Sauce,R,L0:0 +I18,Worchester Sc,R,L0:0,L1:5 +I19,Soy Sauce,R,L0:1,L1:1,L2:3 +I20,Spagetti,R,L0:2,L1:2,L2:2 +I21,Pasta,R,L0:1,L1:4 +I22,Noodles,R,L0:0 +I23,EasyMac,R,L0:1,L1:5 +I24,Oat Bran,R,L0:0,L1:8 +I25,Tea Bags,I,L0:4 +I26,Milo,R,L0:1,L1:10 +I27,Saltanas,R,L0:1,L1:4 +I28,Margarine,R,L0:2 +I29,Bacon,R,L0:2 +I30,Mince,R,L0:1,L1:3 +I31,Sugar Raw,I,L0:0,L1:0 +I32,Sustain,I,L0:0,L1:8 +I33,Cheese block,I,L0:2,L1:11 +I34,bob,I,L0:0 +I35,fredd,I +I36,SometingNew,I,L0:3 diff --git a/Shop/todo b/Shop/todo new file mode 100644 index 0000000..95870fe --- /dev/null +++ b/Shop/todo @@ -0,0 +1,67 @@ +Design: + + enter new 'location' + enter new 'place' + Delete items?? + prime new list from old list + go to top + scroll up/down?? + +DONE How to 'scroll' in search mode? + Maybe select location separator +DONE list load/save +DONE report 'place' on display +DONE Change product to/from 'regular' + tick or cross in search mode + + + FL menu: + - reset: + All 'found', 'regular', 'noneed' return to + 'regular' or 'noneed' + and lose comment + All 'need' and 'cannot' become 'need' + - exit ?? + - Edit place/location + - Delete. need to re-select item after entering menu, then delete + +Implement: + +DONE Make 'search' a toggle button +DONE Sort by location +DONE separator for location change +DONE change place +DONE change name of item +DONE change location of item +DONE save table +DONE save list +DONE load old list +DONE left/right click to tick/cross instantly +DONE guessture text input +DONE Don't jump instantly from 'search' back to 'list' +DONE Highlight current in 'search' mode + + ?highlight entries with comments? +Don't allow things to disappear instantly +New items need to be sorted in +disable button that won't work. + +DONE - Bigger left-right buttons when selecting place +DONE - Don't lose notes on newly added items. +DONE - newly added items should get sorted in to 'unknown' +DONE - When setting 'location', jump to 'here' being the last place +NO - just discard this functionality. - goto-head, goto-tail should update notes. +- button for 'fresh list' +- button for 'hide/show all regular' +DONE - Don't got to 'regular' if it isn't regular. +DONE - darker green colour +DONE - labels for each location +DONE - tag items with comments +- find files in more sensible way +- add places +- edit places +- re-order places +DONE - add locations +DONE - edit locations +- re-order locations +DONE - delete a product?? diff --git a/TODO b/TODO deleted file mode 100644 index 5a7b3b7..0000000 --- a/TODO +++ /dev/null @@ -1,59 +0,0 @@ - -My freerunner stuff is a bit of a mess.... and there is lots to do: - -- Tidy up - - + put all code in single git tree (or maybe a few) - + remove code that is no longer used like speeddial and runit - + collect all random scripts and document how they work - just comments - in the scripts - + keep copy on phone/eli/notebook - - -- Networking - - wifi - + use wpa_cli to get list of networks and to connect to new - + button to switch to 'access point' mode - - usb - - bluetooth - -- audio - - bluetooth - - headset switching - - music playing - -- calls - - different tones for different callers - - handle 'busy' and 'no carrier' better - -- GPS - - ?? auto time sync - -- timezone - - allow list of interesting timezones to be created - -- GPRS - - fix flow control - - auto connect based on wifi status and GSM service provider - -- address book - - general editing app - -- status widgets: - - network status - - GSM status - - active network connections?? - -- GSM - - make carrier-search return a task list - -- launch - - make it easier(?) to cache task lists so we don't keep generating - them anew. - - give feed back while list is being refreshed - -- SMS - - allow search/filter to work better - -- glamo timing fixes - install 'omhack'?? diff --git a/apmhacks/apmd_proxy b/apmhacks/apmd_proxy new file mode 100755 index 0000000..3a8cb0d --- /dev/null +++ b/apmhacks/apmd_proxy @@ -0,0 +1,98 @@ +#!/bin/sh +# +# apmd_proxy - program dispatcher for APM daemon +# +# Written by Craig Markwardt (craigm@lheamail.gsfc.nasa.gov) 21 May 1999 +# Modified for Debian by Avery Pennarun +# +# This shell script is called by the APM daemon (apmd) when a power +# management event occurs. Its first and second arguments describe the +# event. For example, apmd will call "apmd_proxy suspend system" just +# before the system is suspended. +# +# Here are the possible arguments: +# +# start - APM daemon has started +# stop - APM daemon is shutting down +# suspend critical - APM system indicates critical suspend (++) +# suspend system - APM system has requested suspend mode +# suspend user - User has requested suspend mode +# standby system - APM system has requested standby mode +# standby user - User has requested standby mode +# resume suspend - System has resumed from suspend mode +# resume standby - System has resumed from standby mode +# resume critical - System has resumed from critical suspend +# change battery - APM system reported low battery +# change power - APM system reported AC/battery change +# change time - APM system reported time change (*) +# change capability - APM system reported config. change (+) +# +# (*) - APM daemon may be configured to not call these sequences +# (+) - Available if APM kernel supports it. +# (++) - "suspend critical" is never passed to apmd from the kernel, +# so we will never see it here. Scripts that process "resume +# critical" events need to take this into account. +# +# It is the proxy script's responsibility to examine the APM status +# (via /proc/apm) or other status and to take appropriate actions. +# For example, the script might unmount network drives before the +# machine is suspended. +# +# In Debian, the usual way of adding functionality to the proxy is to +# add a script to /etc/apm/event.d. This script will be called by +# apmd_proxy (via run-parts) with the same arguments. +# +# If it is important that a certain set of script be run in a certain +# order on suspend and in a different order on resume, then put all +# the scripts in /etc/apm/scripts.d instead of /etc/apm/event.d and +# symlink to these from /etc/apm/suspend.d, /etc/apm/resume.d and +# /etc/apm/other.d using names whose lexicographical order is the same +# as the desired order of execution. +# +# If the kernel's APM driver supports it, apmd_proxy can return a non-zero +# exit status on suspend and standby events, indicating that the suspend +# or standby event should be rejected. +# +# ******************************************************************* + +set -e + +# The following doesn't yet work, because current kernels (up to at least +# 2.4.20) do not support rejection of APM events. Supporting this would +# require substantial modifications to the APM driver. We will re-enable +# this feature if the driver is ever modified. -- cph@debian.org +# +#SUSPEND_ON_AC=false +#[ -r /etc/apm/apmd_proxy.conf ] && . /etc/apm/apmd_proxy.conf +# +#if [ "${SUSPEND_ON_AC}" = "false" -a "${2}" = "system" ] \ +# && on_ac_power >/dev/null; then +# # Reject system suspends and standbys if we are on AC power +# exit 1 # Reject (NOTE kernel support must be enabled) +#fi + +echo $1 $2 $FORCE_APM `date` >> /tmp/apm.trace +if [ " $1" = " suspend" -a " $2" = " user" -a " $FORCE_APM" != " yes" ] +then exit 0 +fi +if [ " $1" = " resume" -a " $FORCE_APM" != " yes" ] +then exit 0 +fi +if [ "${1}" = "suspend" -o "${1}" = "standby" ]; then + run-parts --arg="${1}" --arg="${2}" /etc/apm/event.d + if [ -d /etc/apm/suspend.d ]; then + run-parts --arg="${1}" --arg="${2}" /etc/apm/suspend.d + fi +elif [ "${1}" = "resume" ]; then + if [ -d /etc/apm/resume.d ]; then + run-parts --arg="${1}" --arg="${2}" /etc/apm/resume.d + fi + run-parts --arg="${1}" --arg="${2}" /etc/apm/event.d +else + run-parts --arg="${1}" --arg="${2}" /etc/apm/event.d + if [ -d /etc/apm/other.d ]; then + run-parts --arg="${1}" --arg="${2}" /etc/apm/other.d + fi +fi + +exit 0 diff --git a/auxlaunch/auxlaunch.py b/auxlaunch/auxlaunch.py new file mode 100755 index 0000000..a2aede8 --- /dev/null +++ b/auxlaunch/auxlaunch.py @@ -0,0 +1,457 @@ +#!/usr/bin/env python + +# Auxlaunch, October 2008, aliasid + + +import pygtk +import gtk +import sys +import os +import dbus +import dbus.glib +import posix +posix.chdir("/home/root") + +class ViewManager: + '''Manage the user interface''' + def __init__(self): + # Create map for change buttons. Indicate which modes + # are invoked by which button positions. This supports options + # like righ/left handed, and window-swithcing or not. + lns = {0:Controller.GRP,1:Controller.APP,2:Controller.ERR} + lsw = {0:Controller.GRP,1:Controller.APP,2:Controller.WIN} + rns = {0:Controller.APP,1:Controller.GRP,2:Controller.ERR} + rsw = {0:Controller.WIN,1:Controller.APP,2:Controller.GRP} + self.keymap = {'left': {'no_switching':lns,'switching':lsw}, + 'right':{'no_switching':rns,'switching':rsw}} + + self.hidden = False # Track if Auxluanch's window is showing or not + + def start_ui(self): + global ctrl + + # Set up GTK window and table + self.win = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.win.connect("delete_event", self.delete_event) + self.win.connect("focus_in_event", self.focus_event) + self.win.set_border_width(10) + if ctrl.simple_ui(): + rows = 2 + else: + rows = 3 + if ctrl.window_mgr(): + columns = 2 + else: + columns = 1 + self.tbl = gtk.Table(rows, columns, True) + self.win.add(self.tbl) + + # Create a "go" button + self.btnGo = self.create_button('gtk-dialog-error','go',0,3,0,1) + self.btnGo.set_label('initial') + + # Create multiple sets of arrow or "change" buttons + # Buttons' "data" indicates position (column) and + # "adjustment" (+1/-1 = up or down) + if ctrl.simple_ui(): + self.btn1dn = self.create_button('gtk-directory', '0,-1',0,1,1,3) + self.btn2dn = self.create_button('gtk-index', '1,-1',1,2,1,3) + if ctrl.window_mgr(): + self.btn3dn = self.create_button('gtk-refresh','2,-1',2,3,1,3) + else: + self.btn1up = self.create_button('gtk-go-up' ,'0,+1',0,1,1,2) + self.btn1dn = self.create_button('gtk-go-down','0,-1',0,1,2,3) + self.btn2up = self.create_button('gtk-go-up' ,'1,+1',1,2,1,2) + self.btn2dn = self.create_button('gtk-go-down','1,-1',1,2,2,3) + if ctrl.window_mgr(): + self.btn3up = self.create_button('gtk-go-up' ,'2,+1',2,3,1,2) + self.btn3dn = self.create_button('gtk-go-down','2,-1',2,3,2,3) + + # Prepare UI + self.tbl.show() + self.win.show() + self.hide() + + # Hook up to AUX button + bus = dbus.SystemBus() + #bus.add_signal_receiver(self.signal_handler, + # dbus_interface="org.freesmartphone.odeviced", + # signal_name=None) + bus.add_signal_receiver(self.signal_handler, + dbus_interface="org.freesmartphone.Device.Input", + signal_name="Event") + #bus.add_signal_receiver(self.signal_handler,None, None, + # "org.freesmartphone.odeviced","/org/freesmartphone/Device/Input") + self.x = True + + gtk.main() # Allow GTK's event loop to run and call us back + + def signal_handler(self, name, action, xx): + '''React to AUX button press''' + print "name = %s and action = %s xx=%s" % (name,action,xx) + if self.x: + self.x = False + return + self.x = True + if (name == "AUX" or name == "ButtonPressed") and (action == "pressed" or action == "phone"): + if ctrl.hide() : + if self.hidden: + self.show() + else: + self.hide() + else: + self.show() + + + def button_pressed(self, widget, data): + global ctrl + if data == 'go': + ctrl.go() + else: + # Figure out what user meant based on button button position and + # command line options. Asumme defaults 1st. + # TODO: clean up constants below + right_left = 'left' + switching = 'no_switching' + if ctrl.right_hand(): right_left = 'right' + if ctrl.window_mgr(): switching = 'switching' + data = data.split(',') + column = self.keymap[right_left][switching][int(data[0])] + ctrl.adjust(column, int(data[1])) + + def set_go(self, icon, text): + '''Update Go button's image and label''' + self.btnGo.set_image(icon) + self.btnGo.set_label(text) + label = self.btnGo.child.child.get_children()[1] + label.set_markup(''+text+'') + + def delete_event(self, widget, event, data=None): + '''Answers qeuestion: should exit event be deleted?''' + if ctrl.done(): + gtk.main_quit() + return False + else: + return True + + def focus_event(self, widget, event, data=None): + ''' Process event: app window gained or lost focus''' + ctrl.refresh() + + def create_button(self, stock_id, data, left,right,top,botton): + '''Utility function to create buttons''' + button = gtk.Button() + image = gtk.Image() + image.set_from_stock(stock_id, gtk.ICON_SIZE_DIALOG) + button.set_image(image) + button.connect("clicked", self.button_pressed, data) + self.tbl.attach(button, left, right, top, botton) + button.show() + return button + + def hide(self): + self.win.iconify() + self.hidden = True + + def show(self): + self.win.maximize() + self.win.present() + self.hidden = False + + def stop_ui(self): + gtk.main_quit() + +class Controller: + '''Track status of auxlaunch app, manages model and view''' + + # These constants are passed between the view and the controller. + # They represent the "mode" (what the go button will do). Mode is + # determined by most recently pressed adjust button's position on-screen. + APP = 'app' + WIN = 'win' + GRP = 'grp' + ERR = ' ' + + def __init__(self): + # The application's "state" is the mode of the go button + # plus the current index positions in the lists of groups, + # apps, and windows + self.goMode = self.APP + self.curGrp = 0 + self.curApp = 0 + self.curWin = 0 + + def run(self): + if self.help_needed(): + pass + else: + global model + global view + model.load_from_rc() + if self.dms(): + model.load_from_dms() + view.start_ui() + + # Command line options: + def help_needed(self): + if '-help' in sys.argv: + print '-dms = Include items from Debian Menu System.' + print '-nowm = No window manager. (Do not show window switching buttons).' + print '-right = Swap app-launchning and window-switching buttons (right for left).' + print '-hide = Cause AUX button to hide Auxlaunch (when it is displayed).' + print '-simple = Use "change" buttons for grp/app/win selecting (instead of up/down buttons).' + return True + else: + return False + def window_mgr(self): + if '-nowm' in sys.argv: return False + else: return True + def right_hand(self): + if '-right' in sys.argv: return True + else: return False + def dms(self): + if '-dms' in sys.argv: return True + else: return False + def hide(self): + if '-hide' in sys.argv: return True + else: return False + def simple_ui(self): + if '-simple' in sys.argv: return True + else: return False + + def done(self): + '''Check if ok to exit application''' + return True # For now, always exit - nothing needs to be saved + + def go(self): + '''React to "Go" button press''' + if self.goMode == self.APP: + command = model.get_app(self.curGrp,self.curApp).get_command() + if command == '(cancel)': + view.hide() + elif command == '(quit)': + view.stop_ui() + else: + print 'auxlaunch: launching "'+command+'"' # debug + view.hide() + os.system(command) + elif self.goMode == self.WIN: + window_title = model.get_window(self.curWin).get_title() + window_ID = model.get_window(self.curWin).get_win_id() + print 'auxlaunch: swtiching "'+window_title+'": '+window_ID # debug + view.hide() + os.system('wmctrl -i -a '+window_ID) + elif self.goMode == self.GRP: + self.curApp = 0 + app = model.get_app(self.curGrp,self.curApp) + view.set_go(app.get_icon(), app.get_name()) + self.goMode = self.APP + + def adjust(self, new_mode, change): + '''React to change-button press, update state and "Go" button''' + + # Adjust state while enforcing bounds checking of list indexes + if new_mode == Controller.GRP: + self.curGrp = (self.curGrp + change) % model.num_groups() + view.set_go(model.group_image(), model.get_group(self.curGrp)) + elif new_mode == Controller.APP: + self.curApp = (self.curApp + change) % model.num_apps_in_group(self.curGrp) + app = model.get_app(self.curGrp,self.curApp) + view.set_go(app.get_icon(), app.get_name()) + elif new_mode == Controller.WIN: + self.curWin = (self.curWin + change) % model.num_win() + window = model.get_window(self.curWin) + view.set_go(model.switch_image, window.get_title()) + + # Adjust Go button's mode + self.goMode = new_mode + + def refresh(self): + '''Load new snapshot of other apps' window titles''' + if self.window_mgr(): + model.reload_windows() + self.curWin = 0 # TODO: guess window based on history + window = model.get_window(self.curWin) + view.set_go(model.switch_image, window.get_title()) + +class ModelManager: + '''Provide data - groups, apps, windows, images''' + def __init__(self): + # Create a group-to-app-list dictionary where keys will + # be group names and items will be lists of "Apps"s + self.grpApps = {} + + # Keep list objects that represent current X application windows + self.winList = [] + + # Store generic "group' and "switch' images - since other images are in model + self.switch_image = gtk.Image() + self.switch_image.set_from_stock('gtk-refresh', gtk.ICON_SIZE_DIALOG) + self.grp_image = gtk.Image() + self.grp_image.set_from_stock('gtk-directory', gtk.ICON_SIZE_DIALOG) + + # Images + def switch_image(self): + return self.switch_image + def group_image(self): + return self.grp_image + + # Windows + def num_win(self): + return len(self.winList) + + def get_window(self, number): + return self.winList[number] + + def reload_windows(self): + '''Build list of other apps' window IDs and titles''' + self.winList = [] + output = os.popen('wmctrl -l','r') + while True: + line = output.readline() + if not line: break + field = line.split(None,3) + title = field[3].rstrip() + title = title.replace('<','-') + title = title.replace('>','-') + if title == 'auxlaunch': + continue + self.winList.append(WinItem(field[0], title)) + + # Groups and apps + def num_groups(self): + return len(self.grpApps.keys()) + + def num_apps_in_group(self, groupNum): + group = self.grpApps.keys()[groupNum] + return len(self.grpApps[group]) + + def get_group(self, number): + return self.grpApps.keys()[number] + + def get_app(self, groupNum, appNum): + '''Provide desired app item object''' + group = self.grpApps.keys()[groupNum] + return self.grpApps[group][appNum] + + def load_from_rc(self): + '''Read Auxlaunch's config file''' + # Create temporary, holding variables + INITGROUP = '(My Default Group)' + curGroup = INITGROUP + curApps = [] + + rcfile = open('.auxlaunchrc') + for line in rcfile: + field = line.split(',') + if field[0][0].upper() == '"': # Menu record + if not (curGroup == INITGROUP and len(curApps) == 0): + self.grpApps[curGroup] = curApps # Flush holding vars + curGroup = field[0].strip('"').strip("'") + curApps = [] + else: # Item record + image = gtk.Image() + if field[2].rstrip() == '': + image.set_from_stock('gtk-execute', gtk.ICON_SIZE_DIALOG) + elif field[2][:4] == 'gtk-': + image.set_from_stock(field[2], gtk.ICON_SIZE_DIALOG) + else: + image.set_from_file(field[2]) + app = AppItem(image,field[0],field[1]) + curApps.append(app) + # Flush last holding value + if not (curGroup == INITGROUP and len(curApps) == 0): + self.grpApps[curGroup] = curApps + + def load_from_dms(self): + '''Read entries out of Debian Menu System files''' + for filename in os.listdir('/usr/share/menu'): + file_object = open('/usr/share/menu/'+filename) + buf = file_object.read(1000000) + buf = buf.decode('string_escape') # Remove escaped newlines + entries = buf.splitlines() + for entry in entries: + entry = entry.split() # Remove leading, trailing, + entry = ' '.join(entry) # and extra inner spaces + entry = entry.strip() + if entry == '': continue # Skip empties and non-entries + if entry[0] <> '?': continue + entry = entry.partition(':')[2] # Remove prior to ':' + if entry == '': continue + quoted = False # Chop at unquoted spaces + space_at = [] + for i in range(len(entry)): + if entry[i] == '"': + quoted = not quoted + if entry[i] == ' ' and not quoted: + space_at.append(i) + fields = [] + start = 0 + for end in space_at: + fields.append(entry[start:end]) + start = end+1 + fields.append(entry[start:]) + + # Extract field names and values + item = {'needs':'', 'title':'', 'command':'', 'icon':'', 'section':'Default'} + for field in fields: + if field == '': continue + if not '=' in field: continue + pair = field.split('=') # TODO Why needed to avoid error? + item[pair[0]] = pair[1].strip('"') + + # TODO: Don't yet know how to launch cmd line ("text") apps, so skip 'em + if not item['needs'].upper() == 'X11': continue + + # Derive label, image, command, and group + label = '' + if item['title'] <> '': label = ' ' + item['title'] + elif item['command'] <> '': label = ' ' + item['command'] + else: continue + command = item['command'] + ' &' # TODO: Best place to add '&'? + image = gtk.Image() + if item['icon'] == '': image.set_from_stock('gtk-execute', gtk.ICON_SIZE_DIALOG) + else: image.set_from_file(item['icon']) + + # Insert app, and possibly group, into dictionary + appList = [] + if self.grpApps.has_key(item['section']): + appList = self.grpApps[item['section']] + appList.append(AppItem(image, label, command)) + self.grpApps[item['section']] = appList + + +class AppItem: + '''Store attributes of an application''' + def __init__(self, icon, name, command): + self.icon = icon + self.name = name + self.command = command + + def get_icon(self): + return self.icon + + def get_name(self): + return self.name + + def get_command(self): + return self.command + +class WinItem: + '''Store attributes of a window''' + def __init__(self, window_id, title): + self.win_id = window_id + self.title = title + + def get_win_id(self): + return self.win_id + + def get_title(self): + return self.title + +# Auxluanch execution starts here +model = ModelManager() +ctrl = Controller() +view = ViewManager() +ctrl.run() + diff --git a/auxlaunch/auxlaunch2.py b/auxlaunch/auxlaunch2.py new file mode 100644 index 0000000..6edc037 --- /dev/null +++ b/auxlaunch/auxlaunch2.py @@ -0,0 +1,457 @@ +#!/usr/bin/env python + +# Auxlaunch, October 2008, aliasid + + +import pygtk +import gtk +import sys +import os +import dbus +import dbus.glib +import posix +posix.chdir("/home/root") + +class ViewManager: + '''Manage the user interface''' + def __init__(self): + # Create map for change buttons. Indicate which modes + # are invoked by which button positions. This supports options + # like righ/left handed, and window-swithcing or not. + lns = {0:Controller.GRP,1:Controller.APP,2:Controller.ERR} + lsw = {0:Controller.GRP,1:Controller.APP,2:Controller.WIN} + rns = {0:Controller.APP,1:Controller.GRP,2:Controller.ERR} + rsw = {0:Controller.WIN,1:Controller.APP,2:Controller.GRP} + self.keymap = {'left': {'no_switching':lns,'switching':lsw}, + 'right':{'no_switching':rns,'switching':rsw}} + + self.hidden = False # Track if Auxluanch's window is showing or not + + def start_ui(self): + global ctrl + + # Set up GTK window and table + self.win = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.win.connect("delete_event", self.delete_event) + self.win.connect("focus_in_event", self.focus_event) + self.win.set_border_width(10) + if ctrl.simple_ui(): + rows = 2 + else: + rows = 3 + if ctrl.window_mgr(): + columns = 2 + else: + columns = 1 + self.tbl = gtk.Table(rows, columns, True) + self.win.add(self.tbl) + + # Create a "go" button + self.btnGo = self.create_button('gtk-dialog-error','go',0,3,0,1) + self.btnGo.set_label('initial') + + # Create multiple sets of arrow or "change" buttons + # Buttons' "data" indicates position (column) and + # "adjustment" (+1/-1 = up or down) + if ctrl.simple_ui(): + self.btn1dn = self.create_button('gtk-directory', '0,-1',0,1,1,3) + self.btn2dn = self.create_button('gtk-index', '1,-1',1,2,1,3) + if ctrl.window_mgr(): + self.btn3dn = self.create_button('gtk-refresh','2,-1',2,3,1,3) + else: + self.btn1up = self.create_button('gtk-go-up' ,'0,+1',0,1,1,2) + self.btn1dn = self.create_button('gtk-go-down','0,-1',0,1,2,3) + self.btn2up = self.create_button('gtk-go-up' ,'1,+1',1,2,1,2) + self.btn2dn = self.create_button('gtk-go-down','1,-1',1,2,2,3) + if ctrl.window_mgr(): + self.btn3up = self.create_button('gtk-go-up' ,'2,+1',2,3,1,2) + self.btn3dn = self.create_button('gtk-go-down','2,-1',2,3,2,3) + + # Prepare UI + self.tbl.show() + self.win.show() + self.hide() + + # Hook up to AUX button + bus = dbus.SystemBus() + #bus.add_signal_receiver(self.signal_handler, + # dbus_interface="org.freesmartphone.odeviced", + # signal_name=None) + bus.add_signal_receiver(self.signal_handler, + dbus_interface="org.freesmartphone.Device.Input", + signal_name="Event") + #bus.add_signal_receiver(self.signal_handler,None, None, + # "org.freesmartphone.odeviced","/org/freesmartphone/Device/Input") + self.x = True + + gtk.main() # Allow GTK's event loop to run and call us back + + def signal_handler(self, name, action, xx): + '''React to AUX button press''' + print "name = %s and action = %s xx=%s" % (name,action,xx) + if self.x: + self.x = False + return + self.x = True + if (name == "AUX" or name == "ButtonPressed") and (action == "pressed" or action == "phone"): + if ctrl.hide() : + if self.hidden: + self.show() + else: + self.hide() + else: + self.show() + + + def button_pressed(self, widget, data): + global ctrl + if data == 'go': + ctrl.go() + else: + # Figure out what user meant based on button button position and + # command line options. Asumme defaults 1st. + # TODO: clean up constants below + right_left = 'left' + switching = 'no_switching' + if ctrl.right_hand(): right_left = 'right' + if ctrl.window_mgr(): switching = 'switching' + data = data.split(',') + column = self.keymap[right_left][switching][int(data[0])] + ctrl.adjust(column, int(data[1])) + + def set_go(self, icon, text): + '''Update Go button's image and label''' + self.btnGo.set_image(icon) + self.btnGo.set_label(text) + label = self.btnGo.child.child.get_children()[1] + label.set_markup(''+text+'') + + def delete_event(self, widget, event, data=None): + '''Answers qeuestion: should exit event be deleted?''' + if ctrl.done(): + gtk.main_quit() + return False + else: + return True + + def focus_event(self, widget, event, data=None): + ''' Process event: app window gained or lost focus''' + ctrl.refresh() + + def create_button(self, stock_id, data, left,right,top,botton): + '''Utility function to create buttons''' + button = gtk.Button() + image = gtk.Image() + image.set_from_stock(stock_id, gtk.ICON_SIZE_DIALOG) + button.set_image(image) + button.connect("clicked", self.button_pressed, data) + self.tbl.attach(button, left, right, top, botton) + button.show() + return button + + def hide(self): + self.win.iconify() + self.hidden = True + + def show(self): + self.win.maximize() + self.win.present() + self.hidden = False + + def stop_ui(self): + gtk.main_quit() + +class Controller: + '''Track status of auxlaunch app, manages model and view''' + + # These constants are passed between the view and the controller. + # They represent the "mode" (what the go button will do). Mode is + # determined by most recently pressed adjust button's position on-screen. + APP = 'app' + WIN = 'win' + GRP = 'grp' + ERR = ' ' + + def __init__(self): + # The application's "state" is the mode of the go button + # plus the current index positions in the lists of groups, + # apps, and windows + self.goMode = self.APP + self.curGrp = 0 + self.curApp = 0 + self.curWin = 0 + + def run(self): + if self.help_needed(): + pass + else: + global model + global view + model.load_from_rc() + if self.dms(): + model.load_from_dms() + view.start_ui() + + # Command line options: + def help_needed(self): + if '-help' in sys.argv: + print '-dms = Include items from Debian Menu System.' + print '-nowm = No window manager. (Do not show window switching buttons).' + print '-right = Swap app-launchning and window-switching buttons (right for left).' + print '-hide = Cause AUX button to hide Auxlaunch (when it is displayed).' + print '-simple = Use "change" buttons for grp/app/win selecting (instead of up/down buttons).' + return True + else: + return False + def window_mgr(self): + if '-nowm' in sys.argv: return False + else: return True + def right_hand(self): + if '-right' in sys.argv: return True + else: return False + def dms(self): + if '-dms' in sys.argv: return True + else: return False + def hide(self): + if '-hide' in sys.argv: return True + else: return False + def simple_ui(self): + if '-simple' in sys.argv: return True + else: return False + + def done(self): + '''Check if ok to exit application''' + return True # For now, always exit - nothing needs to be saved + + def go(self): + '''React to "Go" button press''' + if self.goMode == self.APP: + command = model.get_app(self.curGrp,self.curApp).get_command() + if command == '(cancel)': + view.hide() + elif command == '(quit)': + view.stop_ui() + else: + print 'auxlaunch: launching "'+command+'"' # debug + view.hide() + os.system(command) + elif self.goMode == self.WIN: + window_title = model.get_window(self.curWin).get_title() + window_ID = model.get_window(self.curWin).get_win_id() + print 'auxlaunch: swtiching "'+window_title+'": '+window_ID # debug + view.hide() + os.system('wmctrl -i -a '+window_ID) + elif self.goMode == self.GRP: + self.curApp = 0 + app = model.get_app(self.curGrp,self.curApp) + view.set_go(app.get_icon(), app.get_name()) + self.goMode = self.APP + + def adjust(self, new_mode, change): + '''React to change-button press, update state and "Go" button''' + + # Adjust state while enforcing bounds checking of list indexes + if new_mode == Controller.GRP: + self.curGrp = (self.curGrp + change) % model.num_groups() + view.set_go(model.group_image(), model.get_group(self.curGrp)) + elif new_mode == Controller.APP: + self.curApp = (self.curApp + change) % model.num_apps_in_group(self.curGrp) + app = model.get_app(self.curGrp,self.curApp) + view.set_go(app.get_icon(), app.get_name()) + elif new_mode == Controller.WIN: + self.curWin = (self.curWin + change) % model.num_win() + window = model.get_window(self.curWin) + view.set_go(model.switch_image, window.get_title()) + + # Adjust Go button's mode + self.goMode = new_mode + + def refresh(self): + '''Load new snapshot of other apps' window titles''' + if self.window_mgr(): + model.reload_windows() + self.curWin = 0 # TODO: guess window based on history + window = model.get_window(self.curWin) + view.set_go(model.switch_image, window.get_title()) + +class ModelManager: + '''Provide data - groups, apps, windows, images''' + def __init__(self): + # Create a group-to-app-list dictionary where keys will + # be group names and items will be lists of "Apps"s + self.grpApps = {} + + # Keep list objects that represent current X application windows + self.winList = [] + + # Store generic "group' and "switch' images - since other images are in model + self.switch_image = gtk.Image() + self.switch_image.set_from_stock('gtk-refresh', gtk.ICON_SIZE_DIALOG) + self.grp_image = gtk.Image() + self.grp_image.set_from_stock('gtk-directory', gtk.ICON_SIZE_DIALOG) + + # Images + def switch_image(self): + return self.switch_image + def group_image(self): + return self.grp_image + + # Windows + def num_win(self): + return len(self.winList) + + def get_window(self, number): + return self.winList[number] + + def reload_windows(self): + '''Build list of other apps' window IDs and titles''' + self.winList = [] + output = os.popen('wmctrl -l','r') + while True: + line = output.readline() + if not line: break + field = line.split(None,3) + title = field[3].rstrip() + title = title.replace('<','-') + title = title.replace('>','-') + if title == 'auxlaunch': + continue + self.winList.append(WinItem(field[0], title)) + + # Groups and apps + def num_groups(self): + return len(self.grpApps.keys()) + + def num_apps_in_group(self, groupNum): + group = self.grpApps.keys()[groupNum] + return len(self.grpApps[group]) + + def get_group(self, number): + return self.grpApps.keys()[number] + + def get_app(self, groupNum, appNum): + '''Provide desired app item object''' + group = self.grpApps.keys()[groupNum] + return self.grpApps[group][appNum] + + def load_from_rc(self): + '''Read Auxlaunch's config file''' + # Create temporary, holding variables + INITGROUP = '(My Default Group)' + curGroup = INITGROUP + curApps = [] + + rcfile = open('.auxlaunchrc') + for line in rcfile: + field = line.split(',') + if field[0][0].upper() == '"': # Menu record + if not (curGroup == INITGROUP and len(curApps) == 0): + self.grpApps[curGroup] = curApps # Flush holding vars + curGroup = field[0].strip('"').strip("'") + curApps = [] + else: # Item record + image = gtk.Image() + if field[2].rstrip() == '': + image.set_from_stock('gtk-execute', gtk.ICON_SIZE_DIALOG) + elif field[2][:4] == 'gtk-': + image.set_from_stock(field[2], gtk.ICON_SIZE_DIALOG) + else: + image.set_from_file(field[2]) + app = AppItem(image,field[0],field[1]) + curApps.append(app) + # Flush last holding value + if not (curGroup == INITGROUP and len(curApps) == 0): + self.grpApps[curGroup] = curApps + + def load_from_dms(self): + '''Read entries out of Debian Menu System files''' + for filename in os.listdir('/usr/share/menu'): + file_object = open('/usr/share/menu/'+filename) + buf = file_object.read(1000000) + buf = buf.decode('string_escape') # Remove escaped newlines + entries = buf.splitlines() + for entry in entries: + entry = entry.split() # Remove leading, trailing, + entry = ' '.join(entry) # and extra inner spaces + entry = entry.strip() + if entry == '': continue # Skip empties and non-entries + if entry[0] <> '?': continue + entry = entry.partition(':')[2] # Remove prior to ':' + if entry == '': continue + quoted = False # Chop at unquoted spaces + space_at = [] + for i in range(len(entry)): + if entry[i] == '"': + quoted = not quoted + if entry[i] == ' ' and not quoted: + space_at.append(i) + fields = [] + start = 0 + for end in space_at: + fields.append(entry[start:end]) + start = end+1 + fields.append(entry[start:]) + + # Extract field names and values + item = {'needs':'', 'title':'', 'command':'', 'icon':'', 'section':'Default'} + for field in fields: + if field == '': continue + if not '=' in field: continue + pair = field.split('=') # TODO Why needed to avoid error? + item[pair[0]] = pair[1].strip('"') + + # TODO: Don't yet know how to launch cmd line ("text") apps, so skip 'em + if not item['needs'].upper() == 'X11': continue + + # Derive label, image, command, and group + label = '' + if item['title'] <> '': label = ' ' + item['title'] + elif item['command'] <> '': label = ' ' + item['command'] + else: continue + command = item['command'] + ' &' # TODO: Best place to add '&'? + image = gtk.Image() + if item['icon'] == '': image.set_from_stock('gtk-execute', gtk.ICON_SIZE_DIALOG) + else: image.set_from_file(item['icon']) + + # Insert app, and possibly group, into dictionary + appList = [] + if self.grpApps.has_key(item['section']): + appList = self.grpApps[item['section']] + appList.append(AppItem(image, label, command)) + self.grpApps[item['section']] = appList + + +class AppItem: + '''Store attributes of an application''' + def __init__(self, icon, name, command): + self.icon = icon + self.name = name + self.command = command + + def get_icon(self): + return self.icon + + def get_name(self): + return self.name + + def get_command(self): + return self.command + +class WinItem: + '''Store attributes of a window''' + def __init__(self, window_id, title): + self.win_id = window_id + self.title = title + + def get_win_id(self): + return self.win_id + + def get_title(self): + return self.title + +# Auxluanch execution starts here +model = ModelManager() +ctrl = Controller() +view = ViewManager() +ctrl.run() + diff --git a/gsm/At.Commands.pdf b/gsm/At.Commands.pdf new file mode 100644 index 0000000..4ddb3d5 Binary files /dev/null and b/gsm/At.Commands.pdf differ diff --git a/gsm/notes b/gsm/notes new file mode 100644 index 0000000..15c18ff --- /dev/null +++ b/gsm/notes @@ -0,0 +1,105 @@ + +gsmd. + +1/ Need to make sure device is working. + + run muxer + get a connection + run CFUN? and CFUN=1 + try COPS + + Check if Pin needed. Watch /var/run/gsm-state/pin to get number. + + If we cannot get anything useful, + kill muxer + power down, power up + try again + rate limit this severely. + +2/ get suspend notification and request updates + + On every update and at least every 30 seconds, get signal strength + Also check CFUN if 0, try COPS + validate COPS every 5 minutes + If COPS? is 0, get COPS=? every 10 minutes. or when + Store state info in + /var/run/gsm-state/ + cell carrier strength activity newsms incoming signal_strength + +3/ When a suspend is signalled + disable notification of cellid and status - leave SMS + allow suspend + on wake, request updates + + +------------------------- +When sending a command, need to "\r" to get attention +If last reply was more than 5 seconds ago, send 'AT\r' every second + until get a response, or reset. +Wait for OK, then send command. +Always expect async notifications and handle them directly. + +So: + we need an 'init' string. + ATV1;E0;+CMEE=2;+CRC=1; + + Then some settings that we can check and set. + + +CFUN? +CFUN: (\d+) 0 +CFUN=1 + +COPS? +COPS: (.*) 0 +COPS + +CMFG? +CMFG: \d 0 +CMGF=1 + +CPIN? +CPIN: READY + + Some things we just periodically get + +CSQ +CSQ: \d+,\d+ +CLIP +CNMI +CSCB + + +So each of these settings has: + - string to send to check + - string to send to set + - regexp to see if check or not + - last send time + - last recv time + - count of attempts to set + - current setting + - poll timeout + - handler to call on change + + +I need to know: + - phone is on + - provider + - cell id + - signal strength + - call state + - incoming caller number + - incoming TXT message + +I need to adjust to requested state: + - active + - suspend + - flight mode + +Someone else worries about whether to answer calls etc. + +Flight mode must survive power-cycle. So it doesn't live in /var/volatile. +Probably /var/state or /var/lib/misc + +Others are in /var/run places. +There is /var/run/suspend/whatever +and /var/run/gsm/{provider,cell,signal,state,CNI,lasttxt + + +So states are: + flight + suspend + idle + incoming + on-call + +For each of these, there is a collection of setting that we want +to impose and monitor + diff --git a/guessture b/guessture new file mode 160000 index 0000000..b5979fa --- /dev/null +++ b/guessture @@ -0,0 +1 @@ +Subproject commit b5979fa52ad017c1d5853cd533024364e967c4e2 diff --git a/launcher/.launchrc b/launcher/.launchrc new file mode 100644 index 0000000..90fb0a3 --- /dev/null +++ b/launcher/.launchrc @@ -0,0 +1,25 @@ +"Admin", +Suspend,/media/card/suspend,gtk-media-pause, +Cancel,(cancel),gtk-cancel, +Quit,(quit),gtk-quit, +Reload,(reload), +PanelPlug,sh /home/root/gopanel, +HTop,xterm -e htop , +"Network", +wifi, ifconfig usb0 down ; iwconfig eth0 essid JesusIsHere;ifconfig eth0 up;udhcpc eth0 , +UsbNet, ifdown eth0; ifdown bnep0; ifup usb0, +gprs,/media/card/neilb/gprs, +nogprs,/media/card/neilb/nogprs, +BlueNet, ifdown usb0; ifdown eth0; ifup bnep0,1 +"Music", +Music, mplayer /media/card/Music/*/* , gtk-media-play, +Mokoko, mokoko , gtk-media-play, +MyMusic, exec music.py , gtk-media-play, +ABC, mplayer http://mp3media1.abc.net.au:8060/classicfm.mp3 ,gtk-media-play, +Stop, fuser -k /dev/dsp;fuser -k /dev/snd/timer,gtk-media-stop, +Movie, xrandr -o 1 ; mplayer /media/card/orig.mov ; xrandr -o 0, +"Apps", +TangoGPS,exec tangogps , +SuDoKu, exec sudoku_main.py, +Scribble,exec scribble.py ,ScribblePad +Shop, exec shop.py , diff --git a/launcher/fingerscroll.py b/launcher/fingerscroll.py new file mode 100644 index 0000000..662484f --- /dev/null +++ b/launcher/fingerscroll.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python + +# FingerScroll is a simple widget to wrap around TextView +# so that the TextBuffer can be scrolled with finger-wipes. + +import gtk + +class FingerScroll(gtk.TextView): + def __init__(self): + gtk.TextView.__init__(self) + self.hadj = gtk.Adjustment() + self.vadj = gtk.Adjustment() + self.set_size_request(1,1) + self.set_scroll_adjustments(self.hadj, self.vadj) + + self.add_events(gtk.gdk.POINTER_MOTION_MASK + | gtk.gdk.POINTER_MOTION_HINT_MASK + | gtk.gdk.BUTTON_PRESS_MASK + | gtk.gdk.BUTTON_RELEASE_MASK) + self.connect("button_press_event", self.press) + self.connect("button_release_event", self.release) + self.connect("motion_notify_event", self.motion) + self.drag_start = None + + def press(self, w, ev): + w.stop_emission("button_press_event") + + self.drag_start = int(ev.x), int(ev.y) + self.xstart = self.hadj.value + self.ystart = self.vadj.value + + def release(self, w, ev): + self.drag_start = None + w.stop_emission("button_release_event") + + def motion(self, w, ev): + if self.drag_start == None: + return + + if ev.is_hint: + x, y, state = ev.window.get_pointer() + else: + x = ev.x + y = ev.y + x = int(x) + y = int(y) + dx = x - self.drag_start[0] + dy = y - self.drag_start[1] + newx, newy = self.xstart, self.ystart + if abs(dx) > abs(dy): + newx = newx - dx + else: + newy = newy - dy + + if newx >= self.hadj.upper - self.hadj.page_size: + newx = self.hadj.upper - self.hadj.page_size + if newx <= self.hadj.lower: newx = self.hadj.lower + if newy >= self.vadj.upper - self.vadj.page_size: + newy = self.vadj.upper - self.vadj.page_size + if newy <= self.vadj.lower: newy = self.vadj.lower + self.hadj.value = newx + self.vadj.value = newy + w.stop_emission("motion_notify_event") + +if __name__ == "__main__": + # test app for FingerScroll + import sys + w = gtk.Window(gtk.WINDOW_TOPLEVEL) + w.connect("destroy", lambda w: gtk.main_quit()) + w.set_title("FingerScroll test") + w.show() + w.set_default_size(200,200) + + sw = FingerScroll(); sw.show() + w.add(sw) + + b = sw.get_buffer() + + f = open(sys.argv[-1], "r") + l = f.readline() + while len(l): + b.insert(b.get_end_iter(), l) + l = f.readline() + + gtk.main() diff --git a/launcher/gpstz b/launcher/gpstz new file mode 100644 index 0000000..be7a405 --- /dev/null +++ b/launcher/gpstz @@ -0,0 +1,62 @@ +#!/bin/bash + +case $1 in + */* ) + if cmp -s /etc/localtime /usr/share/zoneinfo/$1 + then : localtime is OK + else : echo Copying to localtime + cp /usr/share/zoneinfo/$1 /etc/localtime + fi + if [ `cat /etc/timezone` != $1 ] + then : echo Setting /etc/timezone + echo $1 > /etc/timezone + fi + exit 0 + ;; + --list ) ;; + * ) echo >&2 Usage: gpstz [--list] zone/name + exit 1 +esac + +gpspipe -r -n 20 | grep GPGGA | while IFS=, read a tm lat NS long EW etc + do + long=${long%.*} lat=${lat%.*} + case $NS in + N) lat=+$lat;; + S) lat=-$lat;; + esac + case $EW in + E) long=+$long ;; + W) long=-$long ;; + esac + # echo $lat $long + mind=9999999999 + while read country loc tz desc + do + case $country in + \#* ) continue;; + esac + case $loc in + [-+][0-9][0-9][0-9][0-9][-+][0-9][0-9][0-9][0-9][0-9] ) + tlat=${loc%??????} + tlat=${tlat#?} + tlat=${tlat#0} + tlat=${tlat#0} + tlat=${tlat#0} + tlat=${loc%??????????}$tlat + tlong=${loc#?????} + slong=${tlong%?????} + tlong=${tlong#?} + tlong=${tlong#0} + tlong=${tlong#0} + tlong=$slong${tlong#0} + ;; + * ) continue + esac + # echo $tz at $tlat $tlong + x=$[long-tlong] y=$[lat-tlat] + d=$[x*x+y*y] + echo $d $tz + done < /usr/share/zoneinfo/zone.tab + break + done | sort -n | sed 10q diff --git a/launcher/launch.py b/launcher/launch.py new file mode 100644 index 0000000..64d012e --- /dev/null +++ b/launcher/launch.py @@ -0,0 +1,1411 @@ +#!/usr/bin/env python2.5 + +# Neo Launcher +# inspired in part by Auxlaunch +# +# We have a list of folders each of which contains a list of +# tasks, each of which can have a small number of options. +# We present the folders in one column and the tasks in another, +# with the options in buttons below. +# Task types are: +# Program: Option is to run the program +# If the program is currently running, there is also an option to +# kill the program +# If there is a window that is believed to be attached to the program +# The kill option simply closes that window, and there is another option +# to raise the window +# Window: Options are to raise or to kill the window. +# +# +# TODO +# LATER Make list of windows-to-exclude (Panel 0) configurable +# Sort things? +# more space around main words +# +# Design thoughts 28Dec2010 +# Having separete 'internal' folders is bad +# And having to explicitly list lots of speed-dials for a speed-dial +# folder is bad. +# So I want two classes of folder: +# - one that has an explicit list of items, which can be programs or +# any internal function. +# - one that has an implicit list of items, which is generated by an +# internal function or a plug-in +# The implicit list could be an internal function which creates multiple +# entries.. +# So: +# [foldername] +# tag,command line,window-name +# tag,(internal) +# tag,internal() +# tag,module.internal(arguments) +# *,internal(arguments) +# +# The list-creating function would need a call-back to ask for the list +# to be re-calculated +# possible function lists are: +# - windows +# - speed-dials +# - recent-calls +# - wifi networks scanned (add/add-auto, possibly with password) +# - wifi networks known (connect, delete) +# - nearby time zones +# - generic list that was asked for, thus effecting a three-level +# list. Maybe I want that anyway? +# Slight change - allow an internal function to provide a new list. This +# switches to a virtual folder showing that list. When the original folder +# is selected, we go back there... + +import gtk, gobject +import pygtk +import sys, os, time +import pango, re +import struct +import dnotify +import fcntl +from fingerscroll import FingerScroll +from subprocess import Popen, PIPE +from wmctrl import winlist + +import ctypes +libc = ctypes.cdll.LoadLibrary("libc.so.6") +libc.mlockall(3) + +class EvDev: + def __init__(self, path, on_event): + self.f = os.open(path, os.O_RDWR|os.O_NONBLOCK); + self.ev = gobject.io_add_watch(self.f, gobject.IO_IN, self.read) + self.on_event = on_event + self.grabbed = False + self.down_count = 0 + def read(self, x, y): + try: + str = os.read(self.f, 16) + except: + return True + + if len(str) != 16: + return True + (sec,usec,typ,code,value) = struct.unpack_from("IIHHI", str) + if typ == 0x01: + # KEY event + if value == 0: + self.down_count -= 1 + else: + self.down_count += 1 + if self.down_count < 0: + self.down_count = 0 + self.on_event(self.down_count, typ, code, value, sec* 1000 + int(usec/1000)) + return True + def grab(self): + if self.grabbed: + return + #print "grab" + fcntl.ioctl(self.f, EVIOC_GRAB, 1) + self.grabbed = True + def ungrab(self): + if not self.grabbed: + return + #print "release" + fcntl.ioctl(self.f, EVIOC_GRAB, 0) + self.grabbed = False + + +class WinList: + """ + read in a window list - present each as a Task + Allow registering tasks so that when a window appears, we connect it. + """ + def __init__(self): + self.windows = {} + self.tasks = {} + self.tasklist = [] + self.pid = os.getpid() + self.old_windows = None + self.last_reload = 0 + self.winlist = winlist() + gobject.io_add_watch(self.winlist.fd, gobject.IO_IN, self.winlist.events) + self.winlist.on_change(self.refresh) + + def add(self, winid, desk, pid, host, name): + self.windows[winid] = [name, pid] + if self.old_windows and winid in self.old_windows: + self.windows[winid] = [name, pid] + self.old_windows[winid][2:] + p = 'pid:%d' % int(pid) + #print "Looking for ", p + if p in self.tasks: + self.tasks[p](winid) + self.windows[winid].append(self.tasks[p]) + del self.tasks[p] + n = 'name:' + name + if n in self.tasks: + self.tasks[n](winid) + self.windows[winid].append(self.tasks[n]) + del self.tasks[n] + + def remove_old(self): + for winid in self.old_windows: + if not winid in self.windows: + #print "removing",winid + for c in self.old_windows[winid][2:]: + c("") + self.old_windows = None + + def register(self, pid, name, found): + if pid != None: + p = 'pid:%d' % int(pid) + self.tasks[p] = found + if name != None: + n = 'name:' + name + self.tasks[n] = found + + def reload(self): + self.old_windows = self.windows + self.windows = {} + self.tasklist = [] + for w in self.winlist.winfo: + win = self.winlist.winfo[w] + if win.pid == self.pid: + continue + self.add(win, 0, win.pid, '', win.name) + self.tasklist.append(WinTask(win, 0, win.pid, '', win.name)) + self.remove_old() + + togo = [] + for k in self.tasks: + if not self.tasks[k](): + togo.append(k) + for k in togo: + del self.tasks[k] + + return self.tasklist + + + def refresh(self): + self.reload() + global window + if window: + window.refresh() + +ProcessList = [] +class JobCtrl: + """ + Manage processes. + Call to start a process, and get a call back when the process finishes. + + """ + global ProcessList + def __init__(self, cmd, finished = None): + self.finished = finished + if cmd == None: + self._child_created = False + return + self.Popen = Popen(cmd, shell=True, close_fds = True) + self.pid = self.Popen.pid + self.returncode = None + ProcessList.append(self) + + def poll(self): + if self.Popen.poll() != None and self.finished: + self.returncode = self.Popen.returncode + self.finished(self.returncode) + self.finished = None + window.folder_select(window.folder_num) + return self.returncode + + + def poll_all(self): + l = range(len(ProcessList)) + l.reverse() + for i in l: + p = ProcessList[i] + if p.poll() != None: + del ProcessList[i] + + + +class Selector(gtk.DrawingArea): + def __init__(self, names = [], pos = 0, center=False): + gtk.DrawingArea.__init__(self) + + self.on_select = None + self.do_center = center + + self.pixbuf = None + self.width = self.height = 0 + self.need_redraw = True + self.colours = None + self.collist = {} + + self.connect("expose-event", self.redraw) + self.connect("configure-event", self.reconfig) + + self.connect("button_release_event", self.release) + self.connect("button_press_event", self.press) + self.set_events(gtk.gdk.EXPOSURE_MASK + | gtk.gdk.BUTTON_PRESS_MASK + | gtk.gdk.BUTTON_RELEASE_MASK + | gtk.gdk.STRUCTURE_MASK) + + # choose a font + fd = self.get_pango_context().get_font_description() + fd.set_absolute_size(25 * pango.SCALE) + self.fd = fd + self.modify_font(fd) + met = self.get_pango_context().get_metrics(fd) + self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE + self.lineheight *= 1.5 + self.lineheight = int(self.lineheight) + + self.offsets = [] + self.names = names + self.pos = pos + self.top = 0 + self.queue_draw() + + def assign_colour(self, purpose, name): + self.collist[purpose] = name + + def set_list(self, names, pos = 0): + self.names = names + self.pos = pos + self.refresh() + if self.on_select: + self.on_select(pos) + + def reconfig(self, w, ev): + alloc = w.get_allocation() + if not self.pixbuf: + return + if alloc.width != self.width or alloc.height != self.height: + self.pixbuf = None + self.need_redraw = True + + def add_col(self, sym, col): + c = gtk.gdk.color_parse(col) + gc = self.window.new_gc() + gc.set_foreground(self.get_colormap().alloc_color(c)) + self.colours[sym] = gc + + def redraw(self, w, ev): + if self.colours == None: + self.colours = {} + for p in self.collist: + self.add_col(p, self.collist[p]) + self.bg = self.get_style().bg_gc[gtk.STATE_NORMAL] + + if self.need_redraw: + self.draw_buf() + + self.window.draw_drawable(self.bg, self.pixbuf, 0, 0, 0, 0, + self.width, self.height) + + + def draw_buf(self): + self.need_redraw = False + if self.pixbuf == None: + alloc = self.get_allocation() + self.pixbuf = gtk.gdk.Pixmap(self.window, alloc.width, alloc.height) + self.width = alloc.width + self.height = alloc.height + self.pixbuf.draw_rectangle(self.bg, True, 0, 0, + self.width, self.height) + + if not self.names: + return + + lines = int(self.height / self.lineheight) + entries = self.names() + # probably place current entry in the middle + top = self.pos - lines / 2 + if top < 0: + top = 0 + # but try not to leave blank space at the end + if top + lines > entries: + top = entries - lines + # but never have blank space at the top + if top < 0: + top = 0 + self.top = top + offsets = [0] + + for l in range(lines): + + (type, name, other) = self.names(top+l) + #print type, name, other + if type == "end": + break + + offset = offsets[-1] + layout = self.create_pango_layout("") + layout.set_markup(name) + (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents() + if ew > self.width: + # never truncate the start + ew = self.width + + height = self.lineheight + #print eh, height + if eh > height: + height = eh + + if l == self.pos - top: + self.pixbuf.draw_rectangle(self.colours['selected'], True, + 0+2, offset, + self.width-4, height) + self.pixbuf.draw_layout(self.colours[type], + (self.width-ew)/2, + offset + (height-eh)/2, + layout) + offsets.append(offset + height) + self.offsets = offsets + + def refresh(self): + #print "refresh" + self.need_redraw = True + self.queue_draw() + # must return False so that timers don't refire + return False + + def press(self,w,ev): + if not self.offsets: + return + row = len(self.offsets) + for i in range(len(self.offsets)): + if ev.y < self.offsets[i]: + row = i-1 + break + + if self.pos != row + self.top: + self.pos = row + self.top + if self.on_select: + self.on_select(self.pos) + + self.refresh() + + def release(self,w,ev): + pass + + +class Task: + """Identifies a particular task that is a member of a folder. + If the task is running, the PID is tracked here too. + + """ + def __init__(self, name): + self.name = name + + def options(self): + #return ["Yes", "No"] + return ["Yes"] + + def copyconfig(self, orig): + pass + + def setgroup(self, group, pos): + self.group = group + self.pos = pos + + + def refresh(self, select): + global window + if not window: + return + if select: + print "REFRESH", window.folder_num, window.folder_pos[window.folder_num], self.group, self.pos + if window.folder_num != self.group: + window.folder_select(self.group) + window.task_select(self.pos) + elif window.folder_pos[window.folder_num] != self.pos: + window.task_select(self.pos) + window.active = False + window.activate() + + gobject.timeout_add(300, window.refresh) + + def set_tasks(self, list, pos): + global window + window.set_tasks(list, pos) + +class CmdTask(Task): + """ + Task subtype for handling normal commands + """ + def __init__(self, line): + fields = line.split(',') + name = fields[0] + Task.__init__(self, name) + self.job = None + self.win_id = None + self.cmd = None + self.winname = None + + if len(fields) > 1: + self.cmd = fields[1] + if len(fields) > 2: + self.winname = fields[2].strip() + + global windowlist + def gotit(winid = None): + if winid: + self.win_id = winid + return True + + windowlist.register(None, self.winname, gotit) + + def options(self): + if self.win_id: + return ["Raise", "Close"] + if self.job: + return ["Kill"] + if self.cmd: + return ["Run"] + return ["--"] + + def event(self, num): + if self.win_id: + if num == 0: + try: + self.win_id.raise_win() + except: + self.win_id = None + + return True + if num == 1: + self.win_id.close_win() + + elif self.job: + if num == 0: + os.kill(self.job.pid, 15) + self.job.poll() + gobject.timeout_add(400, + lambda *a :(JobCtrl(None).poll_all(),window.refresh())) + elif self.cmd: + global windowlist + self.job = JobCtrl(self.cmd, self.finished) + windowlist.register(self.job.pid, self.winname, self.winfound) + print "registered ", self.job.pid, " for ", self.name + return True + return False + + def winfound(self, winid = None): + if winid != None: + #print "task",self.name,"now has winid", winid + self.win_id = winid + return self.job != None + + def finished(self, retcode): + self.job = None + + def info(self): + if self.job: + self.job.poll() + if self.win_id or self.job: + typ = 'active' + elif self.cmd: + typ = 'cmd' + else: + typ = 'void' + return (typ, self.name, self) + + def copyconfig(self, orig): + self.cmd = orig.cmd + self.winname = orig.winname + + +class WmTask(Task): + """ + This is a fake task that simply holds the name of a window + not to show -- i.e. the parsed info from the config file + """ + def __init__(self, line): + self.name = line[1:] + +class WinTask(Task): + """ + Task subtype for handling Windows that have been found + """ + def __init__(self, winid, desk, pid, host, name): + Task.__init__(self, name) + self.win_id = winid + self.pid = int(pid) + + def options(self): + if self.pid > 0: + return ["Raise", "Close", "Kill"] + else: + return ["Raise", "Close"] + + def event(self, num): + if num == 0: + self.win_id.raise_win() + return True + if num == 1: + self.win_id.close_win() + if num == 2: + os.kill(self.pid, 15) + return False + + def info(self): + return ('window', self.name, self) + + +class InternTask(Task): + """ + An InternTask runs an internal command to choose text to display + It may be inactive, so options returns an empty list. + If the name contains a dot, we import the module and just call the + named function. If not, we run internal_$name. + The function takes two argument. A string: + "_name" - return (type, name, self) + "_options" - return list of button strings + button-name - take appropriate action + and this InternTask object. The 'state' array may be manipulated. + """ + + def __init__(self, line, tag = None): + self.state = {} + self.fn = None + f = line.split('(') + p = f[0].split('.') + if not p[0]: + return + + self.tag = tag + self.name = p[-1] + if p[0] != f[0]: + #try: + exec "import " + p[0] + #except: + # self.fn = None + # return + self.fn = eval(line) + else: + self.fn = eval("internal_" + line) + + def info(self): + global current_input + self.current_input = current_input + if self.tag: + t,n = 'cmd', self.tag + else: + t,n = self.fn("_name", self) + return (t,n,self) + + def options(self): + global current_input + self.current_input = current_input + self.optionlist = self.fn("_options", self) + return self.optionlist + + def event(self, num): + global current_input + self.current_input = current_input + return self.fn(num, self) + +class Tasks: + """Holds a number of folders, each with a number of tasks + + Tasks(filename) loads from a file (or directory???) + reload() re-reads the file and makes changes as needed + folders() - array of folder names + tasks(folder) - array of Task objects + + """ + def __init__(self, path): + self.path = path + self.tasks = {}; self.gtype = {} + self.reload() + + def reload(self): + self.orig_tasks = self.tasks + self.orig_types = self.gtype + self.folders = [] + self.tasks = {} + self.gtype = {} + group = "UnSorted" + try: + f = open(self.path) + except: + f = ["[Built-in]", "Exit,(quit),"] + for line in f: + l = line.strip() + if not l: + continue + + if l[0] == '"' or l[0] == '[': + l = l.strip('"[],') + f = l.split('/', 1) + group = f[0] + if len(f) > 1: + group_type = f[1] + else: + group_type = 'cmd' + if not group: + group = 'UnSorted' + if group_type not in ['cmd','wm']: + group_type = 'cmd' + else: + if group_type == 'cmd': + words = l.split(',',1) + word1 = words[1].strip(' ') + arg0 = word1.split(' ',1)[0] + if arg0[0] == '(': + t = InternTask(word1.strip('()'), words[0]) + elif '(' in arg0: + t = InternTask(word1, words[0]) + else: + t = CmdTask(l) + elif group_type == 'wm': + t = WmTask(l) + if not t: + continue + if group not in self.tasks: + self.folders.append(group) + self.tasks[group] = [] + self.gtype[group] = group_type + if group in self.orig_tasks and \ + self.orig_types[group] == self.gtype[group]: + for ot in self.orig_tasks[group]: + if t.name == ot.name: + ot.copyconfig(t) + t = ot + break + self.tasks[group].append(t) + t.setgroup(len(self.folders)-1, len(self.tasks[group])-1) + self.orig_tasks = None + self.orig_types = None + + + def folder_list(self): + return self.get_folder + def get_folder(self, ind = -1): + if ind == -1: + return len(self.folders) + elif ind < len(self.folders): + return ("folder", self.folders[ind], None) + else: + return ("end", "end", None) + + def task_list(self, num): + global windowlist + gtype = self.gtype[self.folders[num]] + if gtype == "wm": + return lambda ind = -1 : self.get_task(ind, windowlist.reload()) + + return lambda ind = -1 : self.get_task(ind, self.tasks[self.folders[num]]) + def get_task(self, ind, tl): + if tl == None: + tl = [] + if ind == -1: + return len(tl) + elif ind < len(tl): + return tl[ind].info() + else: + return ("end", None, None) + +cliptargets = [ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ] +class LaunchWindow(gtk.Window): + """ + A window containing a list of folders and a list of entries in the folder + Along the bottom are per-entry buttons. + When a folder is selected, the entires are updated. The last-used entry + in the folder is selected. + When an entry is selected, the buttons are updated. + When a button is pressed, its action is effected. + + One type of action can produce text output. In this case a replacement + display pane is used that is finger-scrollable. It comes with a button + to revert to main display + """ + + def __init__(self, tasks): + gtk.Window.__init__(self) + self.connect("destroy", self.close_application) + self.set_title("Launcher") + self.tasks = tasks + self.active = False + + self.create_ui() + + self.clip = gtk.Clipboard(selection='PRIMARY') + self.cliptext = '' + + + self.folder_list = tasks.folder_list() + self.folder_pos = self.folder_list() * [0] + self.col1.set_list(self.folder_list) + self.set_default_size(480, 640) + self.show() + + def create_ui(self): + v1 = gtk.VBox() + self.add(v1) + v1.show() + + v = gtk.VBox() + v1.add(v) + v.show() + + v.set_property('can-focus', True) + v.grab_focus() + v.add_events(gtk.gdk.KEY_PRESS_MASK) + v.connect('key_press_event', self.keystroke) + self.v = v + + e = gtk.Entry() + v.pack_start(e, expand=False); + e.set_alignment(0.5) + e.connect('changed', self.entry_changed) + e.connect('backspace', self.entry_changed) + e.show() + self.entry = e + self.entry.set_text("") + + h = gtk.HBox() + v.pack_start(h, expand=True, fill=True) + h.show() + + self.col1 = Selector() + self.col2 = Selector(center=True) + self.col1.show() + self.col2.show() + h.pack_start(self.col1) + h.pack_end(self.col2) + + self.col1.on_select = self.folder_select + self.col2.on_select = self.task_select + + self.col1.assign_colour('folder','darkblue') + self.col1.assign_colour('selected','white') + + self.col2.assign_colour('active','blue') + self.col2.assign_colour('cmd','black') + self.col2.assign_colour('selected','white') + self.col2.assign_colour('window','blue') + + + h = gtk.HBox() + v.pack_end(h, expand=False) + h.set_size_request(-1,80) + h.show() + + ctx = self.get_pango_context() + fd = ctx.get_font_description() + fd.set_absolute_size(30*pango.SCALE) + + self.buttons = [] + self.button_names = [] + for bn in range(3): + b = gtk.Button("?") + b.child.modify_font(fd) + b.set_property('can-focus', False) + h.add(b) + b.connect('clicked', self.button_pressed, bn) + self.buttons.append(b) + + fd.set_absolute_size(40*pango.SCALE) + self.entry.modify_font(fd) + + self.main_view = v + + # Now create alternate view with FingerScroll and a button + v = gtk.VBox() + v1.add(v) + f = FingerScroll(); f.show() + v.add(f) + self.text_buffer = f.get_buffer() + b = gtk.Button("Done") + fd.set_absolute_size(30*pango.SCALE) + b.child.modify_font(fd) + b.set_property('can-focus', False) + b.connect('clicked', self.text_done) + v.pack_end(b, expand=False) + + fd = pango.FontDescription('Monospace 10') + fd.set_absolute_size(15*pango.SCALE) + f.modify_font(fd) + self.text_view = v + b.show() + + + def text_done(self,widget): + self.text_view.hide() + self.main_view.show() + + def close_application(self, widget): + gtk.main_quit() + + def checkclip(self): + cl = self.clip.wait_for_text() + if cl == self.cliptext: + return False + self.cliptext = cl + if type(cl) != str: + return False + + while len(cl) > 0 and ord(cl[-1]) >= 127: + cl = cl[0:-1] + if re.match('^ *\+?[-0-9 ()\n]*$', cl): + # looks like a phone number. Remove rubbish. + cl = cl.replace('-', '') + cl = cl.replace('(', '') + cl = cl.replace(')', '') + cl = cl.replace(' ', '') + cl = cl.replace('\n', '') + + if len(self.entry.get_text()) == 0: + self.entry.set_text(cl) + else: + self.entry.insert_text(cl, self.entry.get_position()) + self.entry.set_position(self.entry.get_position() + + len(cl)) + return False + + def entry_changed(self, widget): + if not widget.get_text(): + #widget.hide() + self.v.grab_focus() + else: + widget.show() + if not widget.is_focus(): + widget.grab_focus() + global current_input + current_input = widget.get_text() + self.col2.refresh() + self.task_select(self.col2.pos) + + def keystroke(self, widget, ev): + if not widget.is_focus(): + return + if not ev.string: + # some weird control key - or AUX + return + self.entry.show() + self.entry.grab_focus() + self.entry.event(ev) + + def button_pressed(self, widget, num): + hide = self.task.event(num) + self.folder_select(self.folder_num) + if hide: + self.active = False + + def set_tasks(self, lister, posn, folder_num = -1): + self.folder_num = folder_num + self.get_task = lister + self.col2.set_list(lister, posn) + + def folder_select(self, folder_num): + if folder_num < 0: + self.col1.refresh() + self.col2.refresh() + return + if folder_num < 0 or folder_num >= self.folder_list(): + return + self.col1.pos = folder_num + self.col1.refresh() + self.set_tasks(self.tasks.task_list(folder_num), + self.folder_pos[folder_num], + folder_num) + + def task_select(self, task_num): + if task_num >= self.get_task(): + return + if self.folder_num >= 0: + self.folder_pos[self.folder_num] = task_num + (typ, name, self.task) = self.get_task(task_num) + if self.task == None: + print "folder %d task %d" %(self.folder_num, task_num) + # FIXME how does this happen? what do I do with buttons? + # This can happen if we remember and old task number + # which (For window list) no longer exists. + # Fixed now I think + return + options = self.task.options() + while len(options) < len(self.button_names): + self.button_names.pop() + self.buttons[len(self.button_names)].hide() + for i in range(len(self.button_names)): + if options[i] != self.button_names[i]: + self.button_names[i] = options[i] + self.buttons[i].child.set_text(self.button_names[i]) + while len(options) > len(self.button_names): + p = len(self.button_names) + self.button_names.append(options[p]) + self.buttons[p].child.set_text(self.button_names[p]) + self.buttons[p].show() + + def activate(self): + #self.maximize() + self.text_done(None) + self.refresh() + self.present() + gobject.idle_add(self.checkclip) + if self.active: + self.col1.set_list(self.folder_list, 0) + self.active = True + + def refresh(self): + self.folder_select(self.folder_num) + return False + +class LaunchIcon(gtk.StatusIcon): + def __init__(self): + gtk.StatusIcon.__init__(self) + self.set_from_stock(gtk.STOCK_EXECUTE) + self.connect('activate', activate) + +window = None +def activate(*a): + global window + + JobCtrl(None).poll_all() + window.activate() + +down_at = 0 +def aux_activate(cnt, type, code, value, msec): + if type != 1: + # not a key press + return + if code != 169 and code != 116: + # not the AUX key and not the power key + return + global down_at + if value == 1: + # down press + down_at = msec + #print "down_at", down_at + return + if value == 0: + #print "up at", msec, down_at + if msec - down_at > 250: + # too long - someone else wants this press + return + activate() + +last_tap = 0 +def tap_check(cnt, type, code, value, msec): + global last_tap + if type != 1: + # not a key press + return + if code != 307: + # not BtnX + return + if value != 1: + # not a down press + return + # hack - only require one tap + last_tap = msec - 1 + + if msec - last_tap < 200: + # two taps + last_tap = msec - 400 + global window + if window.active: + window.entry.delete_text(0,-1) + activate() + else: + last_tap = msec + +def internal_quit(arg, obj): + global window + if arg == "_name": + return ('cmd', 'Exit') + if arg == "_options": + return ['quit'] + if arg == 0: + window.close_application(None) + +def internal_time(arg, obj): + global window + if arg == "_name": + if 'next' not in obj.state: + obj.state['next'] = 0 + now = time.time() + next_minute = int(now/60)+1 + if next_minute != obj.state['next']: + gobject.timeout_add(int (((next_minute*60) - now) * 1000), + lambda *a :(window.refresh())) + obj.state['next'] = next_minute + tm = time.strftime("%H:%M", time.localtime(now)) + return ('cmd', ''+tm+'') + 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('%d-%b-%Y', 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', ''+tm+"\n"+zone+'') + 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', ('%s\n%s%s' + % (essid, quality, c)) + def options(self, n): + return ['Configure Wifi'] + def event(self, n, ev): + print "please configure %s"% self.list[n][0] + +def main(args): + global window, windowlist, tasks + global current_input + current_input = '' + windowlist = WinList() + tasks = Tasks(os.getenv('HOME') + "/.launchrc") + i = LaunchIcon() + window = LaunchWindow(tasks) + try: + aux = EvDev("/dev/input/event4", aux_activate) + # may aux button broke so ... + EvDev("/dev/input/event0", aux_activate) + except: + aux = None + try: + EvDev("/dev/input/event3", tap_check) + except: + pass + + gtk.settings_get_default().set_long_property("gtk-cursor-blink", 0, "main") + + gtk.main() + +if __name__ == '__main__': + sys.exit(main(sys.argv)) + diff --git a/launcher/launch_settings.py b/launcher/launch_settings.py new file mode 100644 index 0000000..051a540 --- /dev/null +++ b/launcher/launch_settings.py @@ -0,0 +1,26 @@ +import os, stat + +def alert(cmd, obj): + if len(obj.state) == 0: + try: + obj.state['curr'] = os.readlink("/etc/alert/normal") + except: + obj.state['curr'] = '??' + o = [] + for i in os.listdir("/etc/alert"): + if stat.S_ISDIR(os.lstat("/etc/alert/"+i)[0]): + o.append(i) + obj.state['options'] = o + + + if cmd == '_name': + return ('cmd', 'mode: ' + obj.state['curr']) + if cmd == '_options': + return obj.state['options'] + if cmd >= 0 and cmd < len(obj.state['options']): + o = obj.state['options'][cmd] + os.unlink("/etc/alert/normal") + os.symlink(o, "/etc/alert/normal") + obj.state['curr'] = o + + diff --git a/launcher/wmctrl.py b/launcher/wmctrl.py new file mode 100644 index 0000000..724343b --- /dev/null +++ b/launcher/wmctrl.py @@ -0,0 +1,153 @@ + +# +# manage a list of current windows and allow a selected +# window to be raised. +# I'm using Xlib for this, which doesn't have a built-in event +# mechanism like gtk does in gobject. +# So if you want to make sure property change notify events +# get handled, you need to arrange that read events on +# winlist.fd are passed to winlist.events. +# e.g. gobject.io_add_watch(winlist.fd, gobject.IO_IN, winlist.events) +# + +import Xlib.X +import Xlib.display +import Xlib.protocol.event + +class mywindow: + def __init__(self, win, name, pid, list): + self.win = win + self.name = name + self.pid = pid + self.list = list + + def raise_win(self): + msg = Xlib.protocol.event.ClientMessage(window = self.win, + client_type = self.list.ACTIVE_WINDOW, + data = (32, [0,0,0,0,0]) + ) + msg.send_event = 1 + mask = (Xlib.X.SubstructureRedirectMask | + Xlib.X.SubstructureNotifyMask) + self.list.root.send_event(msg, event_mask = mask) + self.win.map() + self.win.raise_window() + #p = w.query_tree().parent + #if p: + # p.map() + # p.raise_window() + self.list.display.flush() + + def close_win(self): + msg = Xlib.protocol.event.ClientMessage(window = self.win, + client_type = self.list.CLOSE_WINDOW, + data = (32, [0,0,0,0,0]) + ) + msg.send_event = 1 + mask = (Xlib.X.SubstructureRedirectMask | + Xlib.X.SubstructureNotifyMask) + self.list.root.send_event(msg, event_mask = mask) + self.list.display.flush() + +class winlist: + def __init__(self): + self.display = Xlib.display.Display() + self.root = self.display.screen().root + self.winfo = {} + self.windows = () + self.WM_STRUT = self.display.intern_atom('_NET_WM_STRUT') + self.CARDINAL = self.display.intern_atom('CARDINAL') + self.ACTIVE_WINDOW = self.display.intern_atom('_NET_ACTIVE_WINDOW') + self.CLOSE_WINDOW = self.display.intern_atom('_NET_CLOSE_WINDOW') + self.NAME = self.display.intern_atom('WM_NAME') + self.STRING = self.display.intern_atom('STRING') + self.PID = self.display.intern_atom('_NET_WM_PID') + self.LIST = self.display.intern_atom('_NET_CLIENT_LIST_STACKING') + self.WINDOW = self.display.intern_atom('WINDOW') + + self.fd = self.display.fileno() + self.change_handle = None + + self.root.change_attributes(event_mask = Xlib.X.PropertyChangeMask ) + self.get_list() + + + def add_win(self, id): + if id in self.winfo: + return self.winfo[id] + w = self.display.create_resource_object('window', id) + p = w.get_property(self.WM_STRUT, self.CARDINAL, 0, 100) + self.winfo[id] = None + if p: + return None + p = w.get_property(self.NAME, self.STRING, 0, 100) + if p and p.format == 8: + name = p.value + name = name.replace('&','&') + 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() + diff --git a/launcher/wpa b/launcher/wpa new file mode 100644 index 0000000..d5017fa --- /dev/null +++ b/launcher/wpa @@ -0,0 +1,160 @@ +ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=root + +network={ + ssid="JesusIsHere" + scan_ssid=1 + key_mgmt=NONE +} +network={ + ssid="TorchNet" + scan_ssid=1 + key_mgmt=WPA-PSK + psk="a1b2c3d4e5" +} + +network={ + ssid="Sarah Smith's Network" + scan_ssid=1 + key_mgmt=WPA-PSK + psk="hellomuffin" +} + +network={ + ssid="LINUX" + scan_ssid=1 + key_mgmt=NONE +} + + +2010-11-21 12:36:57 0415836820 +2010-11-25 10:00:01 -call- +2010-11-25 10:00:01 0298924876 +2010-11-26 12:37:56 -call- +2010-11-26 12:37:56 0406022084 +2010-11-27 14:09:57 -call- +2010-11-27 14:09:58 0415836820 +2010-11-27 20:35:13 -call- +2010-11-27 20:35:13 0415836820 +2010-11-28 11:27:11 -call- +2010-11-28 11:27:12 0423939119 +2010-11-28 11:48:38 -call- +2010-11-28 11:48:38 0406022084 +2010-11-28 12:49:14 -call- +2010-11-28 12:49:14 0415836820 +2010-11-28 12:57:50 -call- +2010-11-28 12:57:50 0406022084 +2010-11-28 14:01:15 -call- +2010-11-28 14:01:15 0415836820 +2010-11-28 18:20:18 -call- +2010-11-28 18:20:18 0296624397 +2010-11-30 21:46:11 -call- +2010-12-03 13:13:17 -call- +2010-12-03 13:13:18 0415836820 +2010-12-03 13:18:05 -call- +2010-12-03 13:18:05 0415836820 +2010-12-03 14:33:38 -call- +2010-12-03 14:33:38 0415836820 +2010-12-03 17:01:57 -call- +2010-12-03 17:01:57 0296074529 +2010-12-04 16:52:59 -call- +2010-12-04 16:52:59 0415836820 +2010-12-05 16:15:15 -call- +2010-12-05 16:15:15 0415836820 +2010-12-05 16:22:19 -call- +2010-12-05 16:22:19 0415836820 +2010-12-05 16:25:05 -call- +2010-12-05 16:25:05 0415836820 +2010-12-05 16:28:06 -call- +2010-12-05 16:28:06 0415836820 +2010-12-05 21:29:29 -call- +2010-12-08 16:02:32 -call- +2010-12-09 18:13:19 -call- +2010-12-09 18:13:20 0415836820 +2010-12-10 12:46:22 -call- +2010-12-10 12:46:22 0406022084 +2010-12-10 13:40:01 -call- +2010-12-10 13:40:01 0406022084 +2010-12-10 13:42:52 -call- +2010-12-10 13:42:52 0406022084 +2010-12-10 13:55:35 -call- +2010-12-10 13:55:35 0415836820 +2010-12-10 13:59:09 -call- +2010-12-10 13:59:09 0415836820 +2010-12-10 14:33:20 -call- +2010-12-10 14:33:20 0415836820 +2010-12-10 14:36:05 -call- +2010-12-10 14:36:05 0415836820 +2010-12-10 14:46:15 -call- +2010-12-10 14:46:15 0415836820 +2010-12-10 15:12:16 -call- +2010-12-10 15:12:16 0411084748 +2010-12-10 15:14:04 -call- +2010-12-10 15:14:04 0411084748 +2010-12-10 15:55:42 -call- +2010-12-10 15:55:42 0415836820 +2010-12-10 16:04:01 -call- +2010-12-10 16:04:01 0411084748 +2010-12-10 16:13:52 -call- +2010-12-10 16:13:52 0415836820 +2010-12-11 11:57:01 -call- +2010-12-11 11:57:01 0415836820 +2010-12-14 07:01:42 -call- +2010-12-14 07:01:43 0406387449 +2010-12-14 09:47:47 -call- +2010-12-14 09:47:47 0403204499 +2010-12-14 22:45:06 -call- +2010-12-14 22:45:06 0406387449 +2010-12-15 20:39:51 -call- +2010-12-15 20:39:51 0406387449 +2010-12-17 16:24:17 -call- +2010-12-17 16:24:17 0415836820 +2010-12-17 17:33:08 -call- +2010-12-17 17:33:09 0415836820 +2010-12-17 17:47:17 -call- +2010-12-17 17:47:18 0415836820 +2010-12-17 21:15:20 -call- +2010-12-17 21:15:20 0406387449 +2010-12-18 13:17:59 -call- +2010-12-18 13:17:59 0415836820 +2010-12-18 14:23:59 -call- +2010-12-18 14:24:00 0415836820 +2010-12-19 14:47:29 -call- +2010-12-19 14:47:29 0407628926 +2010-12-19 20:26:35 -call- +2010-12-20 13:57:00 -call- +2010-12-20 14:35:15 -call- +2010-12-20 14:35:15 0415836820 +2010-12-20 15:34:23 -call- +2010-12-20 15:34:23 0415836820 +2010-12-20 15:44:50 -call- +2010-12-20 15:44:51 0415836820 +2010-12-20 19:14:27 -call- +2010-12-20 19:14:27 0415836820 +2010-12-21 11:50:19 -call- +2010-12-21 11:50:20 0415836820 +2010-12-21 13:03:03 -call- +2010-12-21 13:03:03 0406022084 +2010-12-24 17:55:45 -call- +2010-12-24 17:55:46 0415836820 +2010-12-24 18:48:32 -call- +2010-12-24 18:48:32 0265815991 +2010-12-24 18:49:18 -call- +2010-12-24 18:49:18 0265815991 +2010-12-25 18:42:04 -call- +2010-12-25 18:42:04 0398855778 +2010-12-25 19:58:33 -call- +2010-12-26 20:44:24 -call- +2010-12-26 20:44:24 0415836820 +2010-12-27 09:52:03 -call- +2010-12-27 09:52:03 0415836820 +2010-12-27 09:54:30 -call- +2010-12-27 09:54:30 0415836820 +2010-12-27 09:58:30 -call- +2010-12-27 09:58:30 0415836820 +2010-12-27 11:46:37 -call- +2010-12-27 11:46:37 0424041640 +2010-12-29 10:04:46 -call- +2010-12-29 12:18:36 -call- +2010-12-29 12:18:36 0415836820 +2010-12-29 12:19:31 -call- +2010-12-29 12:19:31 0415836820 diff --git a/lib/decode-long-sms.c b/lib/decode-long-sms.c new file mode 100644 index 0000000..43f450a --- /dev/null +++ b/lib/decode-long-sms.c @@ -0,0 +1,45 @@ + +#include +main(int argc, char *argv[]) +{ + + int pos = 0; + char *c; + int carry = 0; + + for (c = argv[1]; *c; c+= 2) { + int b; + char c1, c2; + c1 = c[0]; c2 = c[1]; + if (c1 > '9') + c1 = 10 + (c1-'A'); + else + c1 = c1 - '0'; + + if (c2 > '9') + c2 = 10 + (c2-'A'); + else + c2 = c2 - '0'; + + b = c1*16 + c2; + + if (pos == 0) { + if (carry) { + printf("%c", carry + ((b&1) << 6)); + carry = 0; + } + b = b >> 1; + } else { + b = (b << (pos-1)) | carry; + carry = (b & 0xff80) >> 7; + b &= 0x7f; + } + printf("%c", b); + pos++; + if (pos == 7) + pos = 0; + } + printf("\n"); + exit(0); +} + diff --git a/lib/decode-long-sms.py b/lib/decode-long-sms.py new file mode 100644 index 0000000..a998744 --- /dev/null +++ b/lib/decode-long-sms.py @@ -0,0 +1,25 @@ + +def sms_decode(msg): + pos = 0 + carry = 0 + str = '' + while msg: + c = msg[0:2] + msg = msg[2:] + b = int(c, 16) + + if pos == 0: + if carry: + str += chr(carry + (b&1)*64) + carry = 0 + b /= 2 + else: + b = (b << (pos-1)) | carry + carry = (b & 0xff80) >> 7 + b &= 0x7f + str += chr(b&0x7f) + pos = (pos+1) % 7 + return str + +import sys +print sms_decode(sys.argv[1]) diff --git a/lib/play.py b/lib/play.py new file mode 100644 index 0000000..2c564dd --- /dev/null +++ b/lib/play.py @@ -0,0 +1,133 @@ + +# Python library to play sounds using ALSA from inside a +# glib event loop +# +# We use the 'non-blocking' output routines, write until +# we can write no more, or we hit the stop-latency limit. +# Then set a timer to try again when we estimate the buffer +# will be 3/4 full. +# +# playing can be interrupted at any time - we allow the buffers +# to flush. +# +# Currently only wav files + +import gobject, alsaaudio, time, struct, sys + +class Play(): + def __init__(self, file, latency_ms = 1000, done = None): + # Arrange to play 'file' - which is the name of a .wav file + self.pcm = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, alsaaudio.PCM_NONBLOCK) + self.latency_ms = latency_ms + self.finished = False + self.done = done + self.setfile(file) + + def setfile(self, file): + # A wav file starts: + # 0-3 "RIFF" + # 4-7 Bytes in rest of file. + # 8-11 "WAVE" + # 12-15 "fmt " + # 16-19 bytes of format + # 20-21 ==1 Microsoft PCM + # 22-23 channels + # 24-27 freq + # 28-31 byte rate + # 32-33 bytes per frame + # 34-35 bits per sample + # 36-39 "data" + # 40-43 number of bytes of data + # 44... actual samples + self.f = open(file) + header = self.f.read(44) + if len(header) != 44: + raise IOError + riff, b1, wave, fmt, b2, format, chan, rate, br, bf, bs, data, b3 = \ + struct.unpack("4si4s 4sihhiihh 4si", header) + + if riff != "RIFF" or wave != "WAVE" or fmt != "fmt " or data != "data": + raise ValueError + if format != 1 or bs != 16: + raise ValueError + else: + self.pcm.setformat(alsaaudio.PCM_FORMAT_S16_LE) + + if chan < 1 or chan > 4: + raise ValueError + else: + self.pcm.setchannels(chan) + + self.pcm.setrate(rate) + self.bytes_per_second = rate * 2 * chan + + # choose the period to be 1/8 of the latency, + # probably need to set an upper bound + frames_per_latency = rate * self.latency_ms / 1000 + self.bytes_per_latency = frames_per_latency * chan * 2; + #self.bytes_per_period = (frames_per_latency / 8) * chan * 2 + self.bytes_per_period = 320 + + self.data = None + + self.pcm.setperiodsize(self.bytes_per_period / chan / 2) + #print "bytes_per_period", self.bytes_per_period + #print "period size", self.bytes_per_period / chan / 2 + + self.start = time.time() + self.loaded = 0 + self.finished = False + self.playsome() + + def playsome(self): + if self.finished: + return + now = time.time() + + self.now = now + pos = int( (time.time() - self.start) * self.bytes_per_second) + buffered = self.loaded - pos + cnt = 0 + data = self.data + while buffered < self.bytes_per_latency + self.bytes_per_period: + if not data: + data = self.f.read(self.bytes_per_period) + if not data: + self.finished = True + self.data = None + if self.done: + self.done() + return + if not self.pcm.write(data): + break + data = None + + cnt += 1 + buffered += self.bytes_per_period + + self.data = data + self.loaded = buffered + pos + + pos = int( (time.time() - self.start) * self.bytes_per_second) + buffered = self.loaded - pos + delay = int(buffered /4 * 1000 / self.bytes_per_second) + print "wrote", cnt, "delay" ,delay + if delay < 20: + self.start += float( 20 - delay) / 1000 + delay = 10 + gobject.timeout_add(delay, self.playsome, priority = gobject.PRIORITY_HIGH) + + +if __name__ == "__main__": + # test code. + # play given wav file in a loop for 20 seconds, then stop + p = None + def done(): + p.setfile(sys.argv[1]) + p = Play(sys.argv[1], 400, done) + c = gobject.main_context_default() + def abort(): + p.finished = True + gobject.timeout_add(20000, abort) + while not p.finished: + c.iteration() diff --git a/lib/pyfakekey.c b/lib/pyfakekey.c new file mode 100644 index 0000000..db662da --- /dev/null +++ b/lib/pyfakekey.c @@ -0,0 +1,20 @@ +#include +#include + + + +static PyMethodDef FakekeyMethods[] = { + ... + {"fakekey", fakekey_class, METH_VARARGS, + "Send synthesised key events to an X client"}, + ... + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + + +PyMODINIT_FUNC +initfakekey(void) +{ + (void) Py_InitModule("fakekey", FakekeyMethods); +} + diff --git a/lib/tapinput.py b/lib/tapinput.py new file mode 100644 index 0000000..2b756a3 --- /dev/null +++ b/lib/tapinput.py @@ -0,0 +1,347 @@ + +# +# experiment with tap input. +# Have a 3x4 array of buttons. +# Enter any symbol by tapping two buttons from the top 3x3 +# Bottom buttons are: mode cancel/delete enter +# mode cycles : upper lower symbol +# cancel is effective after a single tap, delete when no pending tap +# +# The 3x3 keys normally show a 3x3 matrix of what they enable +# When one is tapped, all keys change to show a single image. + +import gtk, pango, gobject + +keymap = {} + +# 4 in each corner, 6 on the sides plus 9 in the middle is 49. +# 26 + 10 leaves 13 for symbols +# 9 most common in middle leaves 15 in the corners (with .) +# digits with + - on two sides, symbols on other two +# e t a o i n s r h l d c u m f p g w y b v k x j q z +# from http://www.deafandblind.com/word_frequency.htm +keymap['lower'] = [ + '01 23 ?@#', + 'bcdfgh ', + '<45>67 {}', + 'jk~lm`np ', + 'aeio urst', + '=;:\\\'"|()', + '[] 89 +-_', + ' qvwxyz', + '!$%^*/&,.' + ] +keymap['UPPER'] = [ + '01 23 ?@#', + 'BCDFGH ', + '<45>67 {}', + 'JK~LM`NP ', + 'AEIO URST', + '=;:\\\'"|()', + '[] 89 +-_', + ' QVWXYZ', + '!$%^*/&,.' + ] +keymap['number'] = [ + '1 ', + ' 2 ', + ' 3 ', + ' 4 ', + ' 5 *0#', + ' 6 ', + ' 7 ', + ' 8 ', + ' 9' + ] + + +class X(gtk.Window): + def __init__(self): + gtk.Window.__init__(self, type=gtk.WINDOW_POPUP) + self.set_default_size(320, 420) + root = gtk.gdk.get_default_root_window() + (x,y,width,height,depth) = root.get_geometry() + x = int((width-320)/2) + y = int((height-420)/2) + self.move(x,y) + + self.dragx = None + self.dragy = None + self.moved = False + + self.button_timeout = None + + self.buttons = [] + v1 = gtk.VBox() + v1.show() + self.add(v1) + + self.entry = gtk.Entry() + self.entry.show() + v1.pack_start(self.entry, expand=False) + + + v = gtk.VBox() + v.show() + v1.add(v) + v.set_homogeneous(True) + + for row in range(3): + h = gtk.HBox() + h.show() + h.set_homogeneous(True) + v.add(h) + bl = [] + for col in range(3): + #b = gtk.Button("%d/%d" %(row, col)) + b = gtk.Button() + b.show() + b.connect('button_press_event', self.press) + b.connect('button_release_event', self.release, row, col) + b.connect('motion_notify_event', self.motion) + b.add_events(gtk.gdk.POINTER_MOTION_MASK| + gtk.gdk.POINTER_MOTION_HINT_MASK) + + h.add(b) + bl.append(b) + self.buttons.append(bl) + + + h = gtk.HBox() + h.show() + h.set_homogeneous(True) + v.add(h) + + b = gtk.Button('mode') + fd = pango.FontDescription('sans 10') + fd.set_absolute_size(30 * pango.SCALE) + b.child.modify_font(fd) + b.show() + b.connect('clicked', self.nextmode) + h.add(b) + self.modebutton = b + + b = gtk.Button(stock=gtk.STOCK_UNDO) + b.show() + b.connect('clicked', self.delete) + h.add(b) + + b = gtk.Button(stock=gtk.STOCK_OK) + b.show() + b.connect('clicked', self.enter) + h.add(b) + + self.show() + self.mode = 'lower' + self.single = False + self.prefix = None + self.size = 0 + self.update_buttons() + self.connect("configure-event", self.update_buttons) + + def update_buttons(self, *a): + alloc = self.buttons[0][0].get_allocation() + w = alloc.width; h = alloc.height + if w > h: + size = h + else: + size = w + size -= 12 + if size <= 10 or size == self.size: + return + self.size = size + + # For each button in 3x3 we need 10 images, + # one for initial state, and one for each of the new states + # So there are two fonts we want. + # First we make the initial images + fd = pango.FontDescription('sans 10') + fd.set_absolute_size(size / 4.5 * pango.SCALE) + self.modify_font(fd) + + bg = self.get_style().bg_gc[gtk.STATE_NORMAL] + fg = self.get_style().fg_gc[gtk.STATE_NORMAL] + red = self.window.new_gc() + red.set_foreground(self.get_colormap().alloc_color(gtk.gdk.color_parse('red'))) + base_images = {} + for mode in keymap.keys(): + base_images[mode] = 9*[None] + for row in range(3): + for col in range(3): + syms = keymap[mode][row*3+col] + pm = gtk.gdk.Pixmap(self.window, size, size) + pm.draw_rectangle(bg, True, 0, 0, size, size) + for r in range(3): + for c in range(3): + sym = syms[r*3+c] + if sym == ' ': + continue + xpos = ((c-col+1)*2+1) + ypos = ((r-row+1)*2+1) + colour = fg + if xpos != xpos%6: + xpos = xpos%6 + colour = red + if ypos != ypos%6: + ypos = ypos%6 + colour = red + layout = self.create_pango_layout(sym) + (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents() + pm.draw_layout(colour, + int(xpos*size/6 - ew/2), + int(ypos*size/6 - eh/2), + layout) + im = gtk.Image() + im.set_from_pixmap(pm, None) + base_images[mode][row*3+col] = im + self.base_images = base_images + fd.set_absolute_size(size / 1.5 * pango.SCALE) + self.modify_font(fd) + sup_images = {} + for mode in keymap.keys(): + sup_images[mode] = 9*[None] + for row in range(3): + for col in range(3): + ilist = 9 * [None] + for r in range(3): + for c in range(3): + sym = keymap[mode][r*3+c][row*3+col] + if sym == ' ': + continue + pm = gtk.gdk.Pixmap(self.window, size, size) + pm.draw_rectangle(bg, True, 0, 0, size, size) + layout = self.create_pango_layout(sym) + (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents() + pm.draw_layout(fg, + int((size - ew)/2), int((size - eh)/2), + layout) + im = gtk.Image() + im.set_from_pixmap(pm, None) + ilist[r*3+c] = im + sup_images[mode][row*3+col] = ilist + self.sup_images = sup_images + self.set_button_images() + + + def set_button_images(self): + for row in range(3): + for col in range(3): + b = self.buttons[row][col] + if self.prefix == None: + im = self.base_images[self.mode][row*3+col] + else: + im = self.sup_images[self.mode][row*3+col][self.prefix] + if im: + b.set_image(im) + + + def tap(self, widget, ev, row, col): + if row == 3: + self.update_buttons() + self.set_button_images() + return + + if self.prefix == None: + self.prefix = row*3 + col + self.button_timeout = gobject.timeout_add(500, self.do_buttons) + else: + sym = keymap[self.mode][self.prefix][row*3+col] + self.entry.emit("insert-at-cursor", sym) + self.noprefix() + + def press(self, widget, ev): + self.dragx = int(ev.x_root) + self.dragy = int(ev.y_root) + self.startx, self.starty = self.get_position() + + def release(self, widget, ev, row, col): + self.dragx = None + self.dragy = None + if self.moved: + self.moved = False + else: + self.tap(widget, ev, row, col) + def motion(self, widget, ev): + if self.dragx == None: + return + x = int(ev.x_root) + y = int(ev.y_root) + + if abs(x-self.dragx)+abs(y-self.dragy) > 40 or self.moved: + self.move(self.startx+x-self.dragx, + self.starty+y-self.dragy); + self.moved = True + if ev.is_hint: + gtk.gdk.flush() + ev.window.get_pointer() + + + def do_buttons(self): + self.set_button_images() + self.button_timeout = None + return False + + + def nextmode(self, w): + if self.prefix: + return self.noprefix() + if self.mode == 'lower': + self.mode = 'UPPER' + self.single = True + w.child.set_text('Mode') + elif self.mode == 'UPPER' and self.single: + self.single = False + w.child.set_text('MODE') + elif self.mode == 'UPPER' and not self.single: + self.mode = 'number' + w.child.set_text('123') + else: + self.mode = 'lower' + w.child.set_text('mode') + self.set_button_images() + + def delete(self, w): + if self.prefix == None: + self.entry.emit("backspace") + else: + self.noprefix() + + def noprefix(self): + self.prefix = None + + if self.button_timeout: + gobject.source_remove(self.button_timeout) + self.button_timeout = None + else: + self.set_button_images() + + if self.single: + self.mode = 'lower' + self.single = False + self.modebutton.child.set_text('mode') + self.set_button_images() + + def enter(self, w): + if self.prefix == None: + text = self.entry.get_text() + print "Answer is", text + self.entry.set_text('') + root = gtk.gdk.get_default_root_window() + app = root.property_get('_MB_CURRENT_APP_WINDOW') + if app and app[0] == 'WINDOW': + try: + appw = gtk.gdk.window_foreign_new(app[2][0]) + appw.property_change('_INPUT_TEXT', 'STRING', 8, + gtk.gdk.PROP_MODE_REPLACE, text) + except: + pass + gtk.main_quit() + else: + self.noprefix() + + + +x = X() + +gtk.main() + diff --git a/mickeyterm/mickeyterm.py b/mickeyterm/mickeyterm.py new file mode 100755 index 0000000..2ece6d7 --- /dev/null +++ b/mickeyterm/mickeyterm.py @@ -0,0 +1,532 @@ +#!/usr/bin/env python +""" +Mickey's own serial terminal. Based on miniterm.py. + +Additional Features: + * readline support with command completion and history + * org.freesmartphone.GSM.MUX support + * log to file + +(C) 2002-2006 Chris Liechti +(C) 2008 Michael 'Mickey' Lauer + +GPLv2 or later +""" + +__version__ = "2.9.1" + +import sys, os, serial, threading, termios + +def completer( text, state ): + """Return a possible readline completion""" + if state == 0: + line ="" + #line = readline.get_line_buffer() + if " " in line: + allmatches = [ "(No Matches Available for commands.)" ] + else: + if not hasattr( completer, "commands" ): + allmatches = [ "(No matches available yet. Did AT+CLAC yet?)" ] + else: + allmatches = completer.commands + + completer.matches = [ x for x in allmatches if x[:len(text)] == text ] + if len( completer.matches ) > state: + return completer.matches[state] + else: + return None + +commands = """ +AT+CACM +AT+CAMM +AT+CAOC +AT+CBC +AT+CBST +AT+CCFC +AT+CCUG +AT+CCWA +AT+CCWE +AT+CEER +AT+CFUN +AT+CGACT +AT+CGANS +AT+CGATT +AT+CGAUTO +AT+CGCLASS +AT+CGDATA +AT+CGDCONT +AT+CGEREP +AT+CGMI +AT+CGMM +AT+CGMR +AT+CGPADDR +AT+CGQMIN +AT+CGQREQ +AT+CGREG +AT+CGSMS +AT+CGSN +AT+CHLD +AT+CHUP +AT+CIMI +AT+CLAC +AT+CLAE +AT+CLAN +AT+CLCC +AT+CLCK +AT+CLIP +AT+CDIP +AT+CLIR +AT+CLVL +AT+CMEE +AT+CMGC +AT+CMGD +AT+CMGF +AT+CMGL +AT+CMGR +AT+CMGS +AT+CMGW +AT+CMOD +AT+CMSS +AT+CMMS +AT+CMUT +AT+CMUX +AT+CNMA +AT+CNMI +AT+CNUM +AT+COLP +AT+COPN +AT+COPS +AT+CPAS +AT+CPBF +AT+CPBR +AT+CPBS +AT+CPBW +AT+CPIN +AT+CPMS +AT+CPOL +AT+CPUC +AT+CPWD +AT+CR +AT+CRC +AT+CREG +AT+CRES +AT+CRLP +AT+CRSL +AT+CRSM +AT+CSAS +AT+CSCA +AT+CSCB +AT+CSCS +AT+CSDH +AT+CSIM +AT+CSMP +AT+CSMS +AT+CSNS +AT+CSQ +AT%CSQ +AT+CSSN +AT+CSTA +AT+CSVM +AT+CTFR +AT+CUSD +AT+DR +AT+FAP +AT+FBO +AT+FBS +AT+FBU +AT+FCC +AT+FCLASS +AT+FCQ +AT+FCR +AT+FCS +AT+FCT +AT+FDR +AT+FDT +AT+FEA +AT+FFC +AT+FHS +AT+FIE +AT+FIP +AT+FIS +AT+FIT +AT+FKS +AT+FLI +AT+FLO +AT+FLP +AT+FMI +AT+FMM +AT+FMR +AT+FMS +AT+FND +AT+FNR +AT+FNS +AT+FPA +AT+FPI +AT+FPS +AT+FPW +AT+FRQ +AT+FSA +AT+FSP +AT+GCAP +AT+GCI +AT+GMI +AT+GMM +AT+GMR +AT+GSN +AT+ICF +AT+IFC +AT+ILRR +AT+IPR +AT+VTS +AT+WS46 +AT%ALS +AT%ATR +AT%BAND +AT%CACM +AT%CAOC +AT%CCBS +AT%STDR +AT%CGAATT +AT%CGMM +AT%CGREG +AT%CNAP +AT%CPI +AT%COLR +AT%CPRIM +AT%CTV +AT%CUNS +AT%NRG +AT%SATC +AT%SATE +AT%SATR +AT%SATT +AT%SNCNT +AT%VER +AT%CGCLASS +AT%CGPCO +AT%CGPPP +AT%EM +AT%EMET +AT%EMETS +AT%CBHZ +AT%CPHS +AT%CPNUMS +AT%CPALS +AT%CPVWI +AT%CPOPN +AT%CPCFU +AT%CPINF +AT%CPMB +AT%CPRI +AT%DATA +AT%DINF +AT%CLCC +AT%DBGINFO +AT%VTS +AT%CHPL +AT%CREG +AT+CTZR +AT+CTZU +AT%CTZV +AT%CNIV +AT%PVRF +AT%CWUP +AT%DAR +AT+CIND +AT+CMER +AT%CSCN +AT%RDL +AT%RDLB +AT%CSTAT +AT%CPRSM +AT%CHLD +AT%SIMIND +AT%SECP +AT%SECS +AT%CSSN +AT+CCLK +AT%CSSD +AT%COPS +AT%CPMBW +AT%CUST +AT%SATCC +AT%COPN +AT%CGEREP +AT%CUSCFG +AT%CUSDR +AT%CPBS +AT%PBCF +AT%SIMEF +AT%EFRSLT +AT%CMGMDU +AT%CMGL +AT%CMGR +AT@ST +AT@AUL +AT@POFF +AT@RST +AT@SC +AT@BAND +ATA +ATB +AT&C +ATD +AT&D +ATE +ATF +AT&F +ATH +ATI +AT&K +ATL +ATM +ATO +ATP +ATQ +ATS +ATT +ATV +ATW +AT&W +ATX +ATZ +""".strip() +completer.commands = commands.split() + commands.lower().split() + +class Terminal( object ): + def __init__( self, port, baudrate, rtscts, xonxoff, lineending, inputmode=True ): + self.inputmode = inputmode + self.r = None + self.convert = lineending + self.EXITCHARACTER = '\x04' # ctrl+D + self.fd = None + self.serial = serial.Serial( port, baudrate, rtscts=rtscts, xonxoff=xonxoff ) + + def setQuietMode( self, quiet ): + self.quiet = quiet + + def setLogging( self, logging ): + self.logging = logging + if self.logging is not None: + self.ilog = open( "%s/mickeyterm.%d.input" % ( logging, os.getpid() ), "w" ) + self.olog = open( "%s/mickeyterm.%d.output" % ( logging, os.getpid() ), "w" ) + self.alog = open( "%s/mickeyterm.%d.all" % ( logging, os.getpid() ), "w" ) + + def run( self ): + self.prepare() + self.serial.open() + assert self.serial.isOpen(), "can't open serial port" + self.banner( True ) + self.r = threading.Thread( target = self.reader ) + self.r.setDaemon( True ) + self.r.start() + # optional + self.serial.write( "AT+CMEE=2;+CRC=1\r\n" ) + self.writer() + self.banner( False ) + self.serial.close() + self.restore() + + def banner( self, startup ): + if self.quiet: + return + if startup: + print "<----------- Mickey's Term V%s @ %s ----------->" % ( __version__, self.serial.port ) + else: + print "Good Bye." + + def prepare( self ): + if self.inputmode: + import readline + readline.set_completer( completer ) + readline.set_completer_delims( " " ) + readline.parse_and_bind("tab: complete") + self.historyfilename = os.path.expanduser( "~/.mickeyterm_history" ) + try: + readline.read_history_file( self.historyfilename ) + print "read history from", self.historyfilename + except IOError: + readline.clear_history() + + else: + self.fd = sys.stdin.fileno() + self.old = termios.tcgetattr( self.fd ) + new = termios.tcgetattr( self.fd ) + new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG + new[6][termios.VMIN] = 1 + new[6][termios.VTIME] = 0 + termios.tcsetattr( self.fd, termios.TCSANOW, new ) + + def restore( self ): + if self.inputmode: + import readline + try: + readline.write_history_file( self.historyfilename ) + except IOError, e: + print "Could not save history.", repr(e) + else: + termios.tcsetattr( self.fd, termios.TCSAFLUSH, self.old ) + + def writer( self ): + if self.inputmode: + # + # new style + # + while True: + try: + cmdline = raw_input( "" ) + except KeyboardInterrupt: + print "CTRL-C" + continue + except EOFError: + print "CTRL-D" + break + else: + if self.convert == "CRLF": + cmdline += "\r\n" + elif self.convert == "CR": + cmdline += "\r" + elif self.convert == "LF": + cmdline += "\n" + self.serial.write( cmdline ) + if self.logging: + self.ilog.write( cmdline ) + self.alog.write( cmdline ) + else: + # + # old style + # + while True: + c = os.read( self.fd, 1 ) + if c == self.EXITCHARACTER: + break + elif c == '\n': + if self.convert == "CRLF": + self.serial.write('\r\n') + elif self.convert == "CR": + self.serial.write('\r') + elif self.convert == "LF": + self.serial.write('\n') + else: + self.serial.write(c) + if self.logging: + self.ilog.write( c ) + self.alog.write( c ) + + def reader( self ): + while True: + data = self.serial.read() + sys.stdout.write(data) + sys.stdout.flush() + if self.logging: + self.olog.write( data ) + self.alog.write( data ) + + +if __name__ == "__main__": + import optparse + + parser = optparse.OptionParser(usage="""\ +%prog [options] [port [baudrate]] + +Mickey's Terminal Program.""") + + parser.add_option("-p", "--port", dest="port", + help="the port, device path, a portnumber, device name (deprecated option), or MUX (default)", + default="MUX") + + parser.add_option("-b", "--baud", dest="baudrate", action="store", type='int', + help="set baudrate, default 115200", default=115200) + + parser.add_option("", "--parity", dest="parity", action="store", + help="set parity, one of [N, E, O], default=N", default='N') + + if False: + parser.add_option("-e", "--echo", dest="echo", action="store_true", + help="enable local echo (default off)", default=False) + + parser.add_option("", "--rtscts", dest="rtscts", action="store_true", + help="enable RTS/CTS flow control (default off)", default=False) + + parser.add_option("", "--xonxoff", dest="xonxoff", action="store_true", + help="enable software flow control (default off)", default=False) + + parser.add_option("", "--cr", dest="cr", action="store_true", + help="do not send CR+LF, send CR only", default=False) + + parser.add_option("", "--lf", dest="lf", action="store_true", + help="do not send CR+LF, send LF only", default=False) + + if False: + parser.add_option("-D", "--debug", dest="repr_mode", action="count", + help="""debug received data (escape non-printable chars) + --debug can be given multiple times: + 0: just print what is received + 1: escape non-printable characters, do newlines as ususal + 2: escape non-printable characters, newlines too + 3: hex dump everything""", default=0) + + parser.add_option("", "--rts", dest="rts_state", action="store", type='int', + help="set initial RTS line state (possible values: 0, 1)", default=None) + + parser.add_option("", "--dtr", dest="dtr_state", action="store", type='int', + help="set initial DTR line state (possible values: 0, 1)", default=None) + + # behaviour + + parser.add_option("-c", "--char-by-char", dest="charbychar", action="store_true", + help="use character-by-character (traditional mode) instead of line-by-line (default)", + default=False) + + parser.add_option("-l", "--logdir", dest="log", + help="enable logging to files, specifies directory" ) + + parser.add_option("-q", "--quiet", dest="quiet", action="store_true", + help="suppress non error messages", default=False) + + options, args = parser.parse_args() + + if options.cr and options.lf: + parser.error("only one of --cr or --lf can be specified") + else: + if options.cr: + lineending = "CR" + elif options.lf: + lineending = "LF" + else: + lineending = "CRLF" + + port = options.port + baudrate = options.baudrate + if args: + if options.port is not None: + parser.error("no arguments are allowed, options only when --port is given") + port = args.pop(0) + if args: + try: + baudrate = int(args[0]) + except ValueError: + parser.error("baudrate must be a number, not %r" % args[0]) + args.pop(0) + if args: + parser.error("too many arguments") + else: + if port is "MUX": + # try to get portname from MUXer + import dbus + bus = dbus.SystemBus() + oMuxer = bus.get_object( "org.pyneo.muxer", "/org/pyneo/Muxer" ) + iMuxer = dbus.Interface( oMuxer, "org.freesmartphone.GSM.MUX" ) + port = iMuxer.AllocChannel( "mickeyterm.%d" % os.getpid() ) + assert port, "could not get path from muxer. need to supply explicit portname" + + if options.log is not None: + if not os.path.isdir( options.log ): + parser.error("%s not a directory") + + inputmode = not options.charbychar + + t = Terminal( str(port), baudrate, options.rtscts, options.xonxoff, lineending, inputmode ) + t.setQuietMode( options.quiet ) + t.setLogging( options.log ) + t.run() diff --git a/music/music.py b/music/music.py new file mode 100755 index 0000000..0882b2c --- /dev/null +++ b/music/music.py @@ -0,0 +1,852 @@ +#!/usr/bin/env python +# -*- Mode: Python -*- +# vi:si:et:sw=4:sts=4:ts=4 + +import pygtk +pygtk.require('2.0') + +import sys + +import gobject + +import suspend + +import pygst +pygst.require('0.10') +import gst +import gst.interfaces +import gtk + +import urllib +import os +import random +import pango + +class MusicList: + # Allows selecting songs and moving through the list. + # movement can be + # sequential (alpha order) + # random-album (Seq through album, then random next album) + # random (random walk through all) + # + # We store two states. + # 1/ The current song to play. This is in 'album' and 'song' and mode etc. + # 2/ The browse location, in 'dir' and 'pos' + def __init__(self, path): + self.albums = {} + self.dirs = {} + self.names = {} + self.add_path(path) + self.on_change = [] + for a in self.albums: + self.albums[a].sort() + for p in self.dirs: + self.dirs[p].sort() + + self.albumlist = self.albums.keys() + self.albumlist.sort() + + self.path = path + self.dir = "/" + self.pos = 0 + + self.album = None + self.song = None + self.mode = 'seq' + + self.set_dir("") + + def set_dir(self, dir): + self.dir = dir + p = self.path + self.dir + print "p is", p + self.folders = [] + if p in self.dirs: + self.folders = self.dirs[p] + self.songs = [] + if p in self.albums: + self.songs = self.albums[p] + self.pos = 0 + + def set_song(self, song): + # this song is in the current dir, + # update folder and song, and play + print "dir is", self.dir + print self.albumlist + print "xx" + self.album = self.albumlist.index(self.path+self.dir) + self.song = self.albums[self.path+self.dir].index(song) + print "set song", self.album, self.song + self.changed() + + def namecnt(self): + cnt = 0 + if self.dir != "": + cnt = 1 + cnt += len(self.folders) + cnt += len(self.songs) + return cnt + + def getname(self, num): + if self.dir != "": + if num == 0: + return ("parent", "") + 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(""); self.dirlabel.show() + vbox.pack_start(self.dirlabel, expand=False) + fd = self.dirlabel.get_pango_context().get_font_description() + fd.set_absolute_size(25 * pango.SCALE) + self.dirlabel.modify_font(fd) + + self.dirlabel.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse('magenta')) + self.dirlabel.set_property('ellipsize', pango.ELLIPSIZE_START) + + self.songlabel = gtk.Label("No Song Playing"); self.songlabel.show() + vbox.pack_end(self.songlabel, expand=False) + self.db.changer(lambda : self.songlabel.set_text(self.db.title())) + + self.songlabel.modify_font(fd) + self.songlabel.set_property('ellipsize', pango.ELLIPSIZE_MIDDLE) + self.songlabel.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse('magenta')) + + h = gtk.HBox(); h.show() + self.volumer = FingerScale(self.control_volume) + h.add(self.volumer) + self.tw = TitleWindow(self.db); self.tw.show() + h.add(self.tw) + self.seeker = FingerScale(self.control_seek) + h.add(self.seeker) + + vbox.pack_end(h, padding=5) + + def play_toggled(self): + self.play_button.remove(self.play_button.child) + if self.player.is_playing(): + self.player.pause() + self.play_button.add(self.play_image) + else: + self.player.play() + #if self.update_id == -1: + # self.update_id = gobject.timeout_add(self.UPDATE_INTERVAL, + # self.update_scale_cb) + self.play_button.add(self.pause_image) + + def soon_play(self, d,b): + # play d/b in 40msec if nothing else is suggested + self.player.stop() + if self.soonid != None: + gobject.source_remove(self.soonid) + self.soonfile = "file://" + urllib.quote(os.path.join(d,b)) + self.soonid = gobject.timeout_add(40, self.soon) + def soon(self): + self.soonid = None + self.load_file(self.soonfile) + self.player.stop() + self.play_toggled() + + def next_song(self): + if self.player.is_playing(): + self.player.stop() + if not self.db.next(): + self.db.next() + + def prev_song(self): + if self.player.is_playing(): + pos,dur = self.player.query_position() + self.player.stop() + if pos > 2*1000*1000*1000: + # 2 billion nanoseconds + self.player.seek(0) + self.player.play() + return + if not self.db.prev(): + self.db.prev() + + + def new_song(self): + (d,b,n) = self.db.curr_song() + self.tw.refresh() + self.dirlabel.set_text(urllib.unquote_plus(d)) + self.soon_play(d,b) + + def scale_format_value_cb(self, scale, value): + if self.p_duration == -1: + real = 0 + else: + real = value * self.p_duration / 100 + + seconds = real / gst.SECOND + + return "%02d:%02d" % (seconds / 60, seconds % 60) + + def scale_button_press_cb(self, widget, event): + # see seek.c:start_seek + gst.debug('starting seek') + + self.play_button.set_sensitive(False) + self.was_playing = self.player.is_playing() + if self.was_playing: + self.player.pause() + + # don't timeout-update position during seek + if self.update_id != -1: + gobject.source_remove(self.update_id) + self.update_id = -1 + + # make sure we get changed notifies + if self.changed_id == -1: + self.changed_id = self.hscale.connect('value-changed', + self.scale_value_changed_cb) + + def scale_value_changed_cb(self, scale): + # see seek.c:seek_cb + real = long(scale.get_value() * self.p_duration / 100) # in ns + gst.debug('value changed, perform seek to %r' % real) + self.player.seek(real) + # allow for a preroll + self.player.get_state(timeout=50*gst.MSECOND) # 50 ms + + def scale_button_release_cb(self, widget, event): + # see seek.cstop_seek + widget.disconnect(self.changed_id) + self.changed_id = -1 + + self.play_button.set_sensitive(True) + if self.seek_timeout_id != -1: + gobject.source_remove(self.seek_timeout_id) + self.seek_timeout_id = -1 + else: + gst.debug('released slider, setting back to playing') + if self.was_playing: + self.player.play() + + if self.update_id != -1: + self.error('Had a previous update timeout id') + else: + self.update_id = gobject.timeout_add(self.UPDATE_INTERVAL, + self.update_scale_cb) + + def update_scale_cb(self): + self.p_position, self.p_duration = self.player.query_position() + if self.p_position != gst.CLOCK_TIME_NONE: + value = self.p_position * 100.0 / self.p_duration + self.adjustment.set_value(value) + + return True + + def grab_seek(self, w, *a): + self.p_position, self.p_duration = self.player.query_position() + percent = self.p_position * 100 / self.p_duration + self.seeker.start(percent, w) + def release_seek(self, *a): + self.seeker.grab_remove() + self.seeker.hide() + def control_seek(self, percent, commit = False): + # return "string" + pos = percent * self.p_duration / 100 + seconds = pos / gst.SECOND + str = "%02d:%02d" % (seconds / 60, seconds % 60) + if commit: + self.player.seek(pos) + return str + + def grab_volume(self, w, *a): + a = w.get_allocation() + self.volumer.start(self.volume, w) + def release_volume(self, *a): + self.volumer.grab_remove() + self.volumer.hide() + def control_volume(self, percent, commit = False): + self.volume = percent + self.player.set_volume(percent) + str = "%d%%" % percent + return str + +def main(args): + + # Need to register our derived widget types for implicit event + # handlers to get called. + + gobject.type_register(PlayerWindow) + + db = MusicList("/home/music/ogg") + w = PlayerWindow(db) + w.show() + + w.on_eos() + gtk.main() + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/music/notes b/music/notes new file mode 100644 index 0000000..2c6598f --- /dev/null +++ b/music/notes @@ -0,0 +1,28 @@ + +Simple music player, using python,gtk,gst. + +Find music files in a directory tree. +Any directory that contains at least one music file is an 'album'. +Other directories are just collections. + +So we scan the directory tree looking for music files. +When found, we add to table: + album[dirpath] += song +if that is first song, add elements of dirpath to + tree[dir] += member + +Then sort them all + +-------------------- +Issue: What to display in song list? + + We sometimes want to display whatever we are browsing - directories or songs. + We sometimes want to display whatever we are playing - songs. + When shuffling, we want to show what is really the next song, so not + just the current album. + + or not... + + The main display could just be for browsing. + There is one line below to show 'current song' + click on 'current song' and it find it in the browser. \ No newline at end of file diff --git a/music/properties.py b/music/properties.py new file mode 100644 index 0000000..56d4c32 --- /dev/null +++ b/music/properties.py @@ -0,0 +1,40 @@ + +import gtk + +class RootProp(): + def __init__(self): + self.root = gtk.gdk.get_default_root_window() + + def setstr(self, name, val): + self.root.property_change(name, "STRING", 8, + gtk.gdk.PROP_MODE_REPLACE, val) + + def getstr(self, name): + (type, format, value) = self.root.property_get(name) + if type != "STRING" or format != 8: + return None + return value + + def watchstr(self, name, fn): + m = self.root.get_events() + self.root.set_events(m | gtk.gdk.PROPERTY_CHANGE_MASK) + self.root.add_filter(self.gotev, True) + + def gotev(self, ev, tr): + print ev, dir(ev) + print ev.type, ev.get_state() + if ev.type == gtk.gdk.PROPERTY_NOTIFY: + print ev.atom + else: + print ev.type + + ev2 = gtk.gdk.event_get() + print "and", ev2.type + return gtk.gdk.FILTER_CONTINUE + +def ping(*a): + print 'ping' + +a= RootProp() +a.watchstr('song', ping) +gtk.main() diff --git a/netchoose/mdbus.py b/netchoose/mdbus.py new file mode 100644 index 0000000..ac7c6ed --- /dev/null +++ b/netchoose/mdbus.py @@ -0,0 +1,431 @@ +#!/usr/bin/env python +""" +Mickey's own dbus introspection utility. + +(C) 2008 Michael 'Mickey' Lauer + +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. +# Copyright (C) 2003 David Zeuthen +# Copyright (C) 2004 Rob Taylor +# Copyright (C) 2005, 2006 Collabora Ltd. +# Copyright (C) 2007 John (J5) Palmieri +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + __slots__ = ('map', + 'in_iface', + 'in_method', + 'in_signal', + 'in_property', + 'property_access', + 'in_sig', + 'out_sig', + 'node_level', + 'in_signal') + def __init__(self): + self.map = {'child_nodes':[],'interfaces':{}} + self.in_iface = '' + self.in_method = '' + self.in_signal = '' + self.in_property = '' + self.property_access = '' + self.in_sig = [] + self.out_sig = [] + self.node_level = 0 + + def parse(self, data): + parser = ParserCreate('UTF-8', ' ') + parser.buffer_text = True + parser.StartElementHandler = self.StartElementHandler + parser.EndElementHandler = self.EndElementHandler + parser.Parse(data) + return self.map + + def StartElementHandler(self, name, attributes): + if name == 'node': + self.node_level += 1 + if self.node_level == 2: + self.map['child_nodes'].append(attributes['name']) + elif not self.in_iface: + if (not self.in_method and name == 'interface'): + self.in_iface = attributes['name'] + else: + if (not self.in_method and name == 'method'): + self.in_method = attributes['name'] + elif (self.in_method and name == 'arg'): + arg_type = attributes['type'] + arg_name = attributes.get('name', None) + if attributes.get('direction', 'in') == 'in': + self.in_sig.append({'name': arg_name, 'type': arg_type}) + if attributes.get('direction', 'out') == 'out': + self.out_sig.append({'name': arg_name, 'type': arg_type}) + elif (not self.in_signal and name == 'signal'): + self.in_signal = attributes['name'] + elif (self.in_signal and name == 'arg'): + arg_type = attributes['type'] + arg_name = attributes.get('name', None) + + if attributes.get('direction', 'in') == 'in': + self.in_sig.append({'name': arg_name, 'type': arg_type}) + elif (not self.in_property and name == 'property'): + prop_type = attributes['type'] + prop_name = attributes['name'] + + self.in_property = prop_name + self.in_sig.append({'name': prop_name, 'type': prop_type}) + self.property_access = attributes['access'] + + + def EndElementHandler(self, name): + if name == 'node': + self.node_level -= 1 + elif self.in_iface: + if (not self.in_method and name == 'interface'): + self.in_iface = '' + elif (self.in_method and name == 'method'): + if not self.map['interfaces'].has_key(self.in_iface): + self.map['interfaces'][self.in_iface]={'methods':{}, 'signals':{}, 'properties':{}} + + if self.map['interfaces'][self.in_iface]['methods'].has_key(self.in_method): + print "ERROR: Some clever service is trying to be cute and has the same method name in the same interface" + else: + self.map['interfaces'][self.in_iface]['methods'][self.in_method] = (self.in_sig, self.out_sig) + + self.in_method = '' + self.in_sig = [] + self.out_sig = [] + elif (self.in_signal and name == 'signal'): + if not self.map['interfaces'].has_key(self.in_iface): + self.map['interfaces'][self.in_iface]={'methods':{}, 'signals':{}, 'properties':{}} + + if self.map['interfaces'][self.in_iface]['signals'].has_key(self.in_signal): + print "ERROR: Some clever service is trying to be cute and has the same signal name in the same interface" + else: + self.map['interfaces'][self.in_iface]['signals'][self.in_signal] = (self.in_sig,) + + self.in_signal = '' + self.in_sig = [] + self.out_sig = [] + elif (self.in_property and name == 'property'): + if not self.map['interfaces'].has_key(self.in_iface): + self.map['interfaces'][self.in_iface]={'methods':{}, 'signals':{}, 'properties':{}} + + if self.map['interfaces'][self.in_iface]['properties'].has_key(self.in_property): + print "ERROR: Some clever service is trying to be cute and has the same property name in the same interface" + else: + self.map['interfaces'][self.in_iface]['properties'][self.in_property] = (self.in_sig, self.property_access) + + self.in_property = '' + self.in_sig = [] + self.out_sig = [] + self.property_access = '' + +#----------------------------------------------------------------------------# +def process_introspection_data(data): +#----------------------------------------------------------------------------# + """Return a structure mapping all of the elements from the introspect data + to python types TODO: document this structure + + :Parameters: + `data` : str + The introspection XML. Must be an 8-bit string of UTF-8. + """ + try: + return _Parser().parse(data) + except Exception, e: + raise IntrospectionParserException('%s: %s' % (e.__class__, e)) + +#----------------------------------------------------------------------------# +class Commands( object ): +#----------------------------------------------------------------------------# + """ + Implementing the dbus introspection / interaction. + """ + def __init__( self, bus ): + if mode == "listen": + self._setupMainloop() + self.bus = bus() + self.busname = None + self.objpath = None + self.rinterface = None + + def listBusNames( self ): + names = self.bus.list_names()[:] + names.sort() + for n in names: + print n + + def listObjects( self, busname ): + self._listChildren( busname, '/' ) + + def listMethods( self, busname, objname ): + obj = self._tryObject( busname, objname ) + if obj is not None: + data = process_introspection_data( obj.Introspect() ) + for name, interface in data["interfaces"].iteritems(): + self._listInterface( name, interface["signals"], interface["methods"], interface["properties"] ) + + def callMethod( self, busname, objname, methodname, parameters=[] ): + obj = self._tryObject( busname, objname ) + if obj is not None: + + if '.' in methodname: + # if we have a fully qualified methodname, use an Interface + ifacename = '.'.join( methodname.split( '.' )[:-1] ) + methodname = methodname.split( '.' )[-1] + iface = dbus.Interface( obj, ifacename ) + method = getattr( iface, methodname ) + else: + method = getattr( obj, methodname.split( '.' )[-1] ) + + try: + result = method( *parameters ) + except dbus.DBusException, e: + print "%s: %s failed: %s" % ( objname, methodname, e.get_dbus_name() ) + except TypeError, e: + pass # python will emit its own error here + else: + print "%s: %s -> " % ( objname, methodname ), + if result is not None: + self._prettyPrint( result ) + else: + print + + def monitorBus( self ): + self._runMainloop() + + def monitorService( self, busname ): + self.busname = busname + self._runMainloop() + + def monitorObject( self, busname, objname ): + self.busname = busname + self.objpath = objname + self._runMainloop() + + # + # command mode + # + + def _listChildren( self, busname, objname ): + fail = objname is '/' + obj = self._tryObject( busname, objname, fail ) + print objname + if obj is not None: + data = process_introspection_data( obj.Introspect() ) + for o in data["child_nodes"]: + newname = "%s/%s" % ( objname, o ) + newname = newname.replace( "//", "/" ) + self._listChildren( busname, newname ) + + def _tryObject( self, busname, objname, fail=True ): + try: + obj = self.bus.get_object( busname, objname ) + except ( dbus.DBusException, ValueError ): + if fail: + if busname in self.bus.list_names(): + print "Object name not found" + else: + print "Service name not found" + sys.exit( -1 ) + else: + return None + else: + return obj + + def _parameter( self, type_, name ): + return "%s:%s" % ( type_, name ) + + def _signature( self, parameters ): + string = "( " + for p in parameters[0]: + string += self._parameter( p["type"], p["name"] ) + string += ", " + if len( string ) == 2: + return "()" + else: + return string[:-2] + " )" + + def _listInterface( self, name, signals, methods, properties ): + methodnames = methods.keys() + methodnames.sort() + for mname in methodnames: + signature = self._signature( methods[mname] ) + print "[METHOD] %s.%s%s" % ( name, mname, signature ) + + signalnames = signals.keys() + signalnames.sort() + for mname in signalnames: + signature = self._signature( signals[mname] ) + print "[SIGNAL] %s.%s%s" % ( name, mname, signature ) + + propertynames = properties.keys() + propertynames.sort() + for mname in propertynames: + signature = self._signature( properties[mname] ) + print "[PROPERTY] %s.%s%s" % ( name, mname, signature ) + + def _prettyPrint( self, result ): + # FIXME pretty printing... + print result + + # + # listening mode + # + + def _setupMainloop( self ): + import gobject + import dbus.mainloop.glib + dbus.mainloop.glib.DBusGMainLoop( set_as_default=True ) + self.mainloop = gobject.MainLoop() + gobject.idle_add( self._setupListener ) + + def _runMainloop( self ): + try: + b = self.bus.__class__.__name__ + bname = self.busname or "all" + oname = self.objpath or "all" + print "listening for signals on %s from service '%s', object '%s'..." % ( b, bname, oname ) + self.mainloop.run() + except KeyboardInterrupt: + self.mainloop.quit() + sys.exit( 0 ) + + def _setupListener( self ): + self.bus.add_signal_receiver( + self._signalHandler, + None, + None, + self.busname, + self.objpath, + sender_keyword = "sender", + destination_keyword = "destination", + interface_keyword = "interface", + member_keyword = "member", + path_keyword = "path" ) + return False # don't call me again + + def _signalHandler( self, *args, **kwargs ): + timestamp = time.strftime("%Y%m%d.%H%M.%S") if timestamps else "" + print "%s [SIGNAL] %s.%s from %s %s" % ( timestamp, kwargs["interface"], kwargs["member"], kwargs["sender"], kwargs["path"] ) + self._prettyPrint( args ) + +#----------------------------------------------------------------------------# +if __name__ == "__main__": +#----------------------------------------------------------------------------# + import gobject + import dbus + import sys + import time + + argv = sys.argv[::-1] + execname = argv.pop() + + if ( "-h" in argv ) or ( "--help" in argv ): + print "Usage: %s [-s] [-l] [ busname [ objectpath [ methodname [ parameters... ] ] ] ]" % ( sys.argv[0] ) + sys.exit( 0 ) + + bus = dbus.SessionBus + mode = "command" + timestamps = False + escape = False + + # run through all arguments and check whether we got '-s' somewhere + if "-s" in argv: + bus = dbus.SystemBus + argv.remove( "-s" ) + + # run through all arguments and check whether we got '-l' somewhere + if "-l" in argv: + mode = "listen" + argv.remove( "-l" ) + + # run through all arguments and check whether we got '-t' somewhere + if "-t" in argv: + timestamps = True + argv.remove( "-t" ) + + # run through all arguments and check whether we got '-e' somewhere + if "-e" in argv: + escape = True + argv.remove( "-e" ) + + c = Commands( bus ) + + if len( argv ) == 0: + if mode == "command": + c.listBusNames() + else: + c.monitorBus() + + elif len( argv ) == 1: + busname = argv.pop() + if mode == "command": + c.listObjects( busname ) + else: + c.monitorService( busname ) + + elif len( argv ) == 2: + busname = argv.pop() + objname = argv.pop() + if mode == "command": + c.listMethods( busname, objname ) + else: + c.monitorObject( busname, objname ) + + elif len( argv ) == 3: + busname = argv.pop() + objname = argv.pop() + methodname = argv.pop() + c.callMethod( busname, objname, methodname ) + + else: + busname = argv.pop() + objname = argv.pop() + methodname = argv.pop() + parameters = [] + + while argv: + try: + string = argv.pop() + parameter = eval( string ) + except NameError: # treat as string + parameter = eval( '"""%s"""' % string ) + if escape: + parameter = parameter.replace( '.r', '\r' ) + parameter = parameter.replace( '.n', '\n' ) + parameters.append( parameter ) + except ( SyntaxError, ValueError, AttributeError ): + print "Error while evaluating '%s'" % string + sys.exit( -1 ) + else: + parameters.append( parameter ) + + c.callMethod( busname, objname, methodname, parameters ) diff --git a/netchoose/netchoose.py b/netchoose/netchoose.py new file mode 100755 index 0000000..8feb760 --- /dev/null +++ b/netchoose/netchoose.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +import pygtk +import gtk +import os + +import gobject +import dbus +import sys +import time +import mdbus + +class NetChooser: + def __init__(self): + window = gtk.Window(gtk.WINDOW_TOPLEVEL) + window.connect("destroy", self.close_application) + window.set_title("NetChooser") + + # vertical stack of stuff. + vb = gtk.VBox() + window.add(vb) + vb.show() + + ## GPRS: row of bits + hb = gtk.HBox() + vb.add(hb) + hb.show() + + ### GPRS toggle button + tb = gtk.ToggleButton("GPRS") + tb.connect("toggled", self.gprs_set) + tb.show() + ### AP name entry + ap = gtk.Entry(16) + ap.show() + + hb.add(tb) + hb.add(ap) + + ## WLAN: row of stuff + hb = gtk.HBox() + vb.add(hb) + hb.show() + + ### WLAN toggle button + tb = gtk.ToggleButton("WLAN") + tb.connect("toggled", self.wlan_set) + tb.show() + ### Acces point dropdown + ap = gtk.combo_box_entry_new_text() + ap.child.connect("changed", self.wlan_ap) + ap.show() + + hb.add(tb) + hb.add(ap) + + window.show() + + def close_application(self, widget): + gtk.main_quit() + + + def gprs_set(self, widget): + if widget.get_active(): + # start GPRS + c = Commands(dbus.SystemBus) + c.callMethod("org.freesmartphone.frameworkd", + "/org/freesmartphone/GSM/Device", + "org.freesmartphone.GSM.PDP.ActivateContext", + [ "vfinternet.au", "x", "x"]) + else: + # stop GPRS + c = Commands(dbus.SystemBus) + c.callMethod("org.freesmartphone.frameworkd", + "/org/freesmartphone/GSM/Device", + "org.freesmartphone.GSM.PDP.DeactivateContext") + return + def wlan_set(self, widget): + return + def wlan_ap(self, widget): + return + +def main(): + gtk.main() + return 0 +if __name__ == "__main__": + NetChooser() + main() + diff --git a/sms/contact.py b/sms/contact.py new file mode 100644 index 0000000..0dea474 --- /dev/null +++ b/sms/contact.py @@ -0,0 +1,21 @@ + +# +# Contacts are stored in a file, one per line, with ':' separated +# fields. If a field can have a list, it is comma separated. +# entries in a list can have a 'tag=' prefix. +# We can have references to other entries, so each entry has an ID +# Fields are: +# id +# Family Name +# Given Name +# groups - list +# references - list +# PO Box +# Address +# Address extension +# Suburb/town +# postcode +# County +# phone numbers - list +# modify date + diff --git a/sms/exesms b/sms/exesms new file mode 100644 index 0000000..7929411 --- /dev/null +++ b/sms/exesms @@ -0,0 +1,48 @@ +#!/usr/bin/env python + +import urllib, sys, os + +def make_url(sender,recipient,mesg): + if recipient[0] == '+': + recipient = recipient[1:] + elif recipient[0:2] != '04': + print "Invalid SMS address: " + recipient + sys.exit(1) + + return "https://www.exetel.com.au/sendsms/api_sms.php?username=0293169905&password=birtwhistle&mobilenumber=%s&message=%s&sender=%s&messagetype=Text" % ( + recipient, + urllib.quote(mesg), + sender + ) + + +def send(sender, recipient, mesg): + try: + f = urllib.urlopen(make_url(sender,recipient, mesg)) + except: + rv = 2 + print "Cannot connect: " + sys.exc_value.strerror + else: + rv = 0 + for l in f: + l = l.strip() + if not l: + continue + f = l.split('|') + if len(f) == 5: + if f[0] != '1': + rv = 1 + m = f[4] + if m[-4:] == '
': + m = m[0:-4] + print m, + else: + rv = 1 + print l, + print + return rv + +os.system("/bin/sh /root/pppon") +ec = send(sys.argv[1], sys.argv[2], sys.argv[3]) +sys.exit(ec) + diff --git a/sms/notes b/sms/notes new file mode 100644 index 0000000..23446a1 --- /dev/null +++ b/sms/notes @@ -0,0 +1,290 @@ + +AT+CFUN=1 # turn on. =0 to turn off?? +AT+COPS # connect to GSM network +AT+COPS? # get status and carrier +AT+COPS=? # get list of providers + +COPS: (2,"vodafone AU","voda AU","50503"),(3,"YES OPTUS","Optus","50502"),(3,"Telstra Mobile","Telstra","50501") + p=re.compile('^\+COPS: (\((\d+),"([^"]*)","([^"]*)","([^"]*)"\),)*$') + +AT%SLEEP=2 # disable deep sleep to avoid some bug. + +AT+CMGF=1 # enable text mode for SMS + + +# PIN +AT+CPIN? +AT+CPIN="XXXX" + +AT+CPWD=fac,old,new fac=PS SC AB P2 ??? + + AB = 1234 + +ATE0 - no echo + +AT+CSQ # signal quality +AT+CREG? # are we registered?? (0,1)==at home, (0,5) == roaming +AT+CREG=2 # get regular updates of location : LAC and CELLID in hex + +AT+CIMI # get imi number + +AT+CPAS # activity status?? + 0 == nothing + 3 == incoming call + 4 == on call + 5 == ??? no connected?? + + +ATA - answer call - +CRING: VOICE is received + Get 'OK' is there was nothing to answer any more + NO CARRIER when other end hangs up + +AT+CLIP=1 enables calling number + +CLIP: "0403463349",129,,,,0 + +ATDnumber; makes a voice call. +get NO CARRIER on hangup. +Can tell if answered with CPAS (==4) + + +ATH - hangup or AT+CHUPA + +AT+CUSD=1,"number" # sends special request, reply is asyn + +CUSD: 2 ..... + e.g. *61# - divert on no answer. + + +AT+CMEE=2 verbose errors +# SMS + +AT+CMGS="phonenumber" +> text +> text +> ctrl-Z + # send a text message + +AT+CMGL="ALL" or "REC UNREAD" etc to view all SMS messages + last number is byte count + +AT+CMGR=N read message N +AT+CMGD=N delete message N +# GPRS +AT+CGDCONT=1,"IP","AU internet" # or whatever +ATD*99# + + +# new messages: + +AT+CNMI: (0-2),(0-3),(0,2),(0,1),(0,1) + (0-2) 1 to send is possible + (0-3) sms incoming: 1 == just index, 2 == message + (0,2) ditto for cell broadcast + (0,1) sms status report + (0,1) flush or clear any buffered messages +AT+CNMI=1,2,2,0,1 + +AT+CSCB=? + request cell broadcast be collected + +Cell broadcast looks like ++CBM: 2000,50,1,1,1 +Eastlakes + +or in packet mode ++CBM: 88 +062000320111C2373DECCE37148D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D100 +^^ 6 letters + ^^ 20 == ?? + ^^ 00 + ^^32 port '50' is 'Cell Name' + ^^01 == ?? + +Remainder is encoded as 7 data,, 'Botany\r\n\r\r\r....\n' +But there is a leading \b ?? + ++CBM: 88 +07D000320111C5F09CCE0EAFCBF386A2D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D168341A8D46A3D100 +^^ 7 letters + D000 + 32 + 01 + \bEastlakes\r\n\r\r\r....\n + + +AT%EM=2,1 - check Serving Cell INfo +AT%EM=2,3 - check neighbours + +AT%EM=2,1 + +%EM: 102,39,39,39,49,28232,15,0,1,0,0,0,0,0,0,2215,0,0,2,255 + ^^ ^^ cell ^^ location + strength +OK +AT%EM=2,3 + +%EM: 6 +96,88,93,572,622,612 +17,30,32,1,1,-3 +17,30,32,-3,-3,-7 +19,32,34,22,22,18 strength?? +17,35,50,9,32,38 +28237,22793,22791,28237,22796,22798 <--- cell +2215,2215,2215,2215,2215,2215 <--- location +695457,2517437,2517437,2461758,644848,644848 +3888,944,948,2424,3380,3380 +0,0,0,0,0,0 +0,0,0,0,0,0 +2,2,2,2,2,2 +7,7,7,7,7,7 +0,0,0,2,2,2 +0,0,0,0,0,0 +0,0,0,23,23,23 + +AT%EM=2,4 + +%EM: 4,20,505,003,-1687800052 + + 505 is country + 003 is carrier + +OK +AT+CIMI + +505038240084403 + + + + + +AT%N0187 # maybe cancel echo ?? + + +AT+VTS=01234 - tone generation + +---------------------------------------------- + +Seem to have 4 channels. + +1 must be reserved for pppd +1 for management +1 for SMS sending? +1 for monitor + +Management: + Start muxer and connect + turn on device + attempt to register. On failure, get list of available networks. + Continue to check every 10 minutes while not suspended. + If not registerred. leave + + + +How do I get these??? ++CMS ERROR: 322 + ++CMS ERROR: memory full + +I did + at+CMGR=? + +then they spontaneously appeared. + + +OK + +current date time +> at+cclk? +> +> +CCLK: "0/1/1,0:0:9" +> +> OK +> at+cclk="09/04/01,14:30:00+00" +> +> OK +> at+cclk? +> +> +CCLK: "9/4/1,14:30:3" +> +This is not set automatically :-( + + +call volume level + +at+clvl=230 + +Command: AT+COPS?, +Response: +COPS: (,[,[,]]),¡Ä, (,[,[,]]) + +Command: AT+COPS=? +Response: +COPS: , long , short , numeric , + +Response: +CME ERROR: +Command: AT+COPS=,[,[,]] +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. + + + + 0. Automatic network selection ( ignored) + 1. Manual network selection, must be present, is optional. + 2. Deregister from network. + 3. Set + + 0. Long alphanumeric string + 1. Short alphanumeric string + 2. Numeric ID + + +String (based on ) that identifies the operator. + + + + 0. Unknown + 1. Available + 2. Current + 3. Forbidden + + Network access type + + 0. GSM + 1. Compact GSM + 2. UTRAN + 3. GSM with EGPRS + 4. UTRAN with HSDPA + 5. UTRAN with HSUPA + 6. UTRAN with HSDPA and HSU + +----------------------------------- +ATH +AT+CUSD=0 +ATH +AT+CUSD=0,"*100#",15 + +reply: ++CUSD: 1,"Try Again +1.Your Balance +2.Voucher Top-Up",15 +AT+CUSD=1,"1" + +OK ++CUSD: 0,"Your bal is $18.18 &expires on 11/01/2011. Your Magic Top Up 22c rate applies until 10/02/2010. You've got 100 FREE texts with 100 to use before 10/02/2010.",15 + +OK + + +So when sending: + 0 - start new interchange + 1 - follow up message +When receiving + 0 - no reply possible + 1 - waiting for reply + + +So: If number matches + [*#].*# +Then set button to "SEND" rather than "CALL" and us CUSD +Display reply in message area with "cancel" button. +If reply is wanted, also have "reply" button diff --git a/sms/sendsms.py b/sms/sendsms.py new file mode 100644 index 0000000..7f6d528 --- /dev/null +++ b/sms/sendsms.py @@ -0,0 +1,1595 @@ +#!/usr/bin/env python + +# Create/edit/send/display/search SMS messages. +# Two main displays: Create and display +# Create: +# Allow entry of recipient and text of SMS message and allow basic editting +# When entering recipient, text box can show address matches for selection +# Bottom buttons are "Select".. +# When entering text, if there is no text, buttom buttons are: +# "Browse", "Close" +# If these is some text, bottom buttons are: +# "Send", Save" +# +# Display: +# We usually display a list of messages which can be selected from +# There is a 'search' box to restrict message to those with a string +# Options for selected message are: +# Delete Reply View Open(for draft)/Forward(for non-draft) +# In View mode, the whole text is displayed and the 'View' button becomes "Index" +# or "Show List" or "ReadIt" +# General options are: +# New Config List +# New goes to Edit +# +# Delete becomes Undelete and can undelete a whole stack. +# Delete can become undelete without deleting be press-and-hold +# +# +# Messages are sent using a separate program. e.g. sms-gsm +# Different recipients can use different programs based on flag in address book. +# Somehow senders can be configured. +# e.g. sms-exetel needs username, password, sender strings. +# press-and-hold on the send button allows a sender to be selected. +# +# +# Send an SMS message using some backend. + +# +# +# TODO: +# 'del' to return to 'list' view +# top buttons: del, view/list, new/open/reply +# so can only reply when viewing whole message +# Bottom: +# all: sent recv +# send: all draft +# recv: all new +# draft: all sent +# new: all recv +# DONE handle newline chars in summary +# DONE cope properly when the month changes. +# switch-to-'new' on 'expose' +# 'draft' button becomes 'cancel' when all is empty +# DONE better display of name/number of destination +# jump to list mode when change 'list' +# 'open' becomes 'reply' when current message was received. +# new message becomes non-new when replied to +# '' button doesn't select, but just makes choice. +# 'new' becomes 'select' when 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('', "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("", "S(4)5,3.S(3)3,5") + dict.add("","S(4)3,5.S(5)5,3") + dict.add("", "S(4)7,1.S(1)1,7") # "" + dict.add("","S(4)1,7.S(7)7,1") # "" + dict.add("", "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 == '': + ev.keyval = 65288 + ev.hardware_keycode = 22 + else: + (ev.keyval,) = struct.unpack_from("b", info) + w.emit('key_press_event', ev) + #self.get_buffer().insert_at_cursor(info) + if cmd == 'click': + self.grab_focus() + if not info.send_event: + info.send_event = True + ev2 = gtk.gdk.Event(gtk.gdk.BUTTON_PRESS) + ev2.send_event = True + ev2.window = info.window + ev2.time = info.time + ev2.x = info.x + ev2.y = info.y + ev2.button = info.button + self.emit('button_press_event', ev2) + self.emit('button_release_event', info) + if cmd == 'redraw': + self.queue_draw() + + def insert_at_cursor(self, text): + self.get_buffer().insert_at_cursor(text) + +class FingerEntry(gtk.Entry): + def __init__(self): + gtk.Entry.__init__(self) + + def insert_at_cursor(self, text): + c = self.get_property('cursor-position') + t = self.get_text() + t = t[0:c]+text+t[c:] + self.set_text(t) + +class SMSlist(gtk.DrawingArea): + def __init__(self, getlist): + gtk.DrawingArea.__init__(self) + self.pixbuf = None + self.width = self.height = 0 + self.need_redraw = True + self.colours = None + self.collist = {} + self.get_list = getlist + + self.connect("expose-event", self.redraw) + self.connect("configure-event", self.reconfig) + + self.connect("button_release_event", self.release) + self.connect("button_press_event", self.press) + self.set_events(gtk.gdk.EXPOSURE_MASK + | gtk.gdk.BUTTON_PRESS_MASK + | gtk.gdk.BUTTON_RELEASE_MASK + | gtk.gdk.STRUCTURE_MASK) + + # choose a font + fd = pango.FontDescription('sans 10') + fd.set_absolute_size(25 * pango.SCALE) + self.font = fd + met = self.get_pango_context().get_metrics(fd) + self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE + fd = pango.FontDescription('sans 5') + fd.set_absolute_size(15 * pango.SCALE) + self.smallfont = fd + self.selected = 0 + self.top = 0 + self.book = None + + self.smslist = [] + + self.queue_draw() + + + def set_book(self, book): + self.book = book + + def lines(self): + alloc = self.get_allocation() + lines = alloc.height / self.lineheight + return lines + + def reset_list(self): + self.selected = 0 + self.smslist = None + self.size_requested = 0 + self.refresh() + + def refresh(self): + self.need_redraw = True + self.queue_draw() + + def assign_colour(self, purpose, name): + self.collist[purpose] = name + + def reconfig(self, w, ev): + alloc = w.get_allocation() + if not self.pixbuf: + return + if alloc.width != self.width or alloc.height != self.height: + self.pixbuf = None + self.need_redraw = True + + def add_col(self, sym, col): + c = gtk.gdk.color_parse(col) + gc = self.window.new_gc() + gc.set_foreground(self.get_colormap().alloc_color(c)) + self.colours[sym] = gc + + def redraw(self, w, ev): + if self.colours == None: + self.colours = {} + for p in self.collist: + self.add_col(p, self.collist[p]) + self.bg = self.get_style().bg_gc[gtk.STATE_NORMAL] + + if self.need_redraw: + self.draw_buf() + + self.window.draw_drawable(self.bg, self.pixbuf, 0, 0, 0, 0, + self.width, self.height) + + def draw_buf(self): + self.need_redraw = False + if self.pixbuf == None: + alloc = self.get_allocation() + self.pixbuf = gtk.gdk.Pixmap(self.window, alloc.width, alloc.height) + self.width = alloc.width + self.height = alloc.height + self.pixbuf.draw_rectangle(self.bg, True, 0, 0, + self.width, self.height) + + if self.top > self.selected: + self.top = 0 + max = self.lines() + if self.smslist == None or \ + (self.top + max > len(self.smslist) and self.size_requested < self.top + max): + self.size_requested = self.top + max + self.smslist = self.get_list(self.top + max) + for i in range(len(self.smslist)): + if i < self.top: + continue + if i > self.top + max: + break + if i == self.selected: + col = self.colours['bg-selected'] + else: + col = self.colours['bg-%d'%(i%2)] + + self.pixbuf.draw_rectangle(col, + True, 0, (i-self.top)*self.lineheight, + self.width, self.lineheight) + self.draw_sms(self.smslist[i], (i - self.top) * self.lineheight) + + + def draw_sms(self, sms, yoff): + + self.modify_font(self.smallfont) + tm = time.strftime("%Y-%m-%d %H:%M:%S", sms.time[0:6]+(0,0,0)) + then = time.mktime(sms.time[0:6]+(0,0,-1)) + now = time.time() + if now > then: + diff = now - then + if diff < 99: + delta = "%02d sec ago" % diff + elif diff < 99*60: + delta = "%02d min ago" % (diff/60) + elif diff < 48*60*60: + delta = "%02dh%02dm ago" % ((diff/60/60), (diff/60)%60) + else: + delta = tm[0:10] + tm = delta + tm[10:] + + l = self.create_pango_layout(tm) + self.pixbuf.draw_layout(self.colours['time'], + 0, yoff, l) + co = sms.correspondent + if self.book: + cor = book_name(self.book, co) + if cor: + co = cor[0] + if sms.source == 'LOCAL': + col = self.colours['recipient'] + co = 'To ' + co + else: + col = self.colours['sender'] + co = 'From '+co + l = self.create_pango_layout(co) + self.pixbuf.draw_layout(col, + 0, yoff + self.lineheight/2, l) + self.modify_font(self.font) + t = sms.text.replace("\n", " ") + t = t.replace("\n", " ") + l = self.create_pango_layout(t) + if sms.state in ['DRAFT', 'NEW']: + col = self.colours['mesg-new'] + else: + col = self.colours['mesg'] + self.pixbuf.draw_layout(col, + 180, yoff, l) + + def press(self,w,ev): + row = int(ev.y / self.lineheight) + self.selected = self.top + row + if self.selected >= len(self.smslist): + self.selected = len(self.smslist) - 1 + if self.selected < 0: + self.selected = 0 + + l = self.lines() + self.top += row - l / 2 + if self.top >= len(self.smslist) - l: + self.top = len(self.smslist) - l + 1 + if self.top < 0: + self.top = 0 + + self.refresh() + + def release(self,w,ev): + pass + +def load_book(file): + try: + f = open(file) + except: + f = open('/home/neilb/home/mobile-numbers-jan-08') + rv = [] + for l in f: + x = l.split(';') + rv.append([x[0],x[1]]) + rv.sort(lambda x,y: cmp(x[0],y[0])) + return rv + +def book_lookup(book, name, num): + st=[]; mid=[] + for l in book: + if name.lower() == l[0][0:len(name)].lower(): + st.append(l) + elif l[0].lower().find(name.lower()) >= 0: + mid.append(l) + st += mid + if len(st) == 0: + return [None, None] + if num >= len(st): + num = -1 + return st[num] + +def book_parse(book, name): + if not book: + return None + cnt = 0 + while len(name) and name[-1] == '.': + cnt += 1 + name = name[0:-1] + return book_lookup(book, name, cnt) + + + +def book_name(book, num): + if len(num) < 8: + return None + for ad in book: + if len(ad[1]) >= 8 and num[-8:] == ad[1][-8:]: + return ad + return None + +def book_speed(book, sym): + i = book_lookup(book, sym, 0) + if i[0] == None or i[0] != sym: + return None + j = book_lookup(book, i[1], 0) + if j[0] == None: + return (i[1], i[0]) + return (j[1], j[0]) + +def name_lookup(book, str): + # We need to report + # - a number - to dial + # - optionally a name that is associated with that number + # - optionally a new name to save the number as + # The name is normally alpha, but can be a single digit for + # speed-dial + # Dots following a name allow us to stop through multiple matches. + # So input can be: + # A single symbol. + # This is a speed dial. It maps to name, then number + # A string of >1 digits + # This is a literal number, we look up name if we can + # A string of dots + # This is a look up against recent incoming calls + # We look up name in phone book + # A string starting with alpha, possibly ending with dots + # This is a regular lookup in the phone book + # A number followed by a string + # This provides the string as a new name for saving + # A string of dots followed by a string + # This also provides the string as a newname + # An alpha string, with dots, followed by '+'then a single symbol + # This saves the match as a speed dial + # + # We return a triple of (number,oldname,newname) + if re.match('^[A-Za-z0-9]$', str): + # Speed dial lookup + s = book_speed(book, str) + if s: + return (s[0], s[1], None) + return None + m = re.match('^(\+?\d+)([A-Za-z][A-Za-z0-9 ]*)?$', str) + if m: + # Number and possible newname + s = book_name(book, m.group(1)) + if s: + return (m.group(1), s[0], m.group(2)) + else: + return (m.group(1), None, m.group(2)) + m = re.match('^([A-Za-z][A-Za-z0-9 ]*)(\.*)(\+[A-Za-z0-9])?$', str) + if m: + # name and dots + speed = None + if m.group(3): + speed = m.group(3)[1] + i = book_lookup(book, m.group(1), len(m.group(2))) + if i[0]: + return (i[1], i[0], speed) + return None + +class SendSMS(gtk.Window): + def __init__(self, store): + gtk.Window.__init__(self) + self.set_default_size(480,640) + self.set_title("SendSMS") + self.store = store + self.connect('destroy', self.close_win) + + self.selecting = False + self.viewing = False + self.book = None + self.create_ui() + + self.show() + self.reload_book = True + self.number = None + self.cutbuffer = None + + d = dnotify.dir(store.dirname) + self.watcher = d.watch('newmesg', lambda f : self.got_new()) + + self.connect('property-notify-event', self.newprop) + self.add_events(gtk.gdk.PROPERTY_CHANGE_MASK) + def newprop(self, w, ev): + if ev.atom == '_INPUT_TEXT': + str = self.window.property_get('_INPUT_TEXT') + self.numentry.set_text(str[2]) + + def close_win(self, *a): + # FIXME save draft + gtk.main_quit() + + def create_ui(self): + + fd = pango.FontDescription("sans 10") + fd.set_absolute_size(25*pango.SCALE) + self.button_font = fd + v = gtk.VBox() ;v.show() ; self.add(v) + + self.sender = self.send_ui() + v.add(self.sender) + self.sender.hide() + + self.listing = self.list_ui() + v.add(self.listing) + self.listing.show() + + self.book = load_book("/media/card/address-book") + self.listview.set_book(self.book) + + self.rotate_list(self, target='All') + + def send_ui(self): + v = gtk.VBox() + main = v + + h = gtk.HBox() + h.show() + v.pack_start(h, expand=False) + l = gtk.Label('To:') + l.modify_font(self.button_font) + l.show() + h.pack_start(l, expand=False) + + self.numentry = FingerEntry() + h.pack_start(self.numentry) + self.numentry.modify_font(self.button_font) + self.numentry.show() + self.numentry.connect('changed', self.update_to); + + h = gtk.HBox() + l = gtk.Label('') + l.modify_font(self.button_font) + l.show() + h.pack_start(l) + h.show() + v.pack_start(h, expand=False) + h = gtk.HBox() + self.to_label = l + l = gtk.Label('0 chars') + l.modify_font(self.button_font) + l.show() + self.cnt_label = l + h.pack_end(l) + h.show() + v.pack_start(h, expand=False) + + h = gtk.HBox() + h.set_size_request(-1,80) + h.set_homogeneous(True) + h.show() + v.pack_start(h, expand=False) + + self.add_button(h, 'select', self.select) + self.add_button(h, 'clear', self.clear) + self.add_button(h, 'paste', self.paste) + + self.message = FingerText() + self.message.show() + self.message.modify_font(self.button_font) + sw = gtk.ScrolledWindow() ; sw.show() + sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + #v.add(self.message) + v.add(sw) + sw.add(self.message) + self.message.get_buffer().connect('changed', self.buff_changed) + + h = gtk.HBox() + h.set_size_request(-1,80) + h.set_homogeneous(True) + h.show() + v.pack_end(h, expand=False) + + self.add_button(h, 'Send GSM', self.send, 'GSM') + self.draft_button = self.add_button(h, 'Draft', self.draft) + self.add_button(h, 'Send EXE', self.send, 'EXE') + + return main + + def list_ui(self): + v = gtk.VBox() ; main = v + + h = gtk.HBox() ; h.show() + h.set_size_request(-1,80) + h.set_homogeneous(True) + v.pack_start(h, expand = False) + self.add_button(h, 'Del', self.delete) + self.view_button = self.add_button(h, 'View', self.view) + self.reply = self.add_button(h, 'New', self.open) + + h = gtk.HBox() ; h.show() + h.set_size_request(-1,80) + h.set_homogeneous(True) + v.pack_end(h, expand=False) + self.buttonA = self.add_button(h, 'Sent', self.rotate_list, 'A') + self.buttonB = self.add_button(h, 'Recv', self.rotate_list, 'B') + + + self.last_response = gtk.Label('') + v.pack_end(self.last_response, expand = False) + + h = gtk.HBox() ; h.show() + v.pack_start(h, expand=False) + b = gtk.Button("clr") ; b.show() + b.connect('clicked', self.clear_search) + h.pack_end(b, expand=False) + l = gtk.Label('search:') ; l.show() + h.pack_start(l, expand=False) + + e = gtk.Entry() ; e.show() + self.search_entry = e + h.pack_start(e) + + self.listview = SMSlist(self.load_list) + self.listview.show() + self.listview.assign_colour('time', 'blue') + self.listview.assign_colour('sender', 'red') + self.listview.assign_colour('recipient', 'black') + self.listview.assign_colour('mesg', 'black') + self.listview.assign_colour('mesg-new', 'red') + self.listview.assign_colour('bg-0', 'yellow') + self.listview.assign_colour('bg-1', 'pink') + self.listview.assign_colour('bg-selected', 'white') + + v.add(self.listview) + + self.singleview = gtk.TextView() + self.singleview.modify_font(self.button_font) + self.singleview.show() + self.singleview.set_wrap_mode(gtk.WRAP_WORD_CHAR) + sw = gtk.ScrolledWindow() + sw.add(self.singleview) + sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + sw.hide() + v.add(sw) + self.singlescroll = sw + + + main.show() + return main + + def add_button(self, parent, label, func, *args): + b = gtk.Button(label) + b.child.modify_font(self.button_font) + b.connect('clicked', func, *args) + b.set_property('can-focus', False) + parent.add(b) + b.show() + return b + + def update_to(self, w): + n = w.get_text() + if n == '': + self.reload_book = True + self.to_label.set_text('') + else: + if self.reload_book: + self.reload_book = False + self.book = load_book("/media/card/address-book") + self.listview.set_book(self.book) + e = name_lookup(self.book, n) + if e and e[1]: + self.to_label.set_text(e[1] + + ' '+ + e[0]) + self.number = e[0] + else: + self.to_label.set_text('??') + self.number = n + self.buff_changed(None) + + def buff_changed(self, w): + if self.numentry.get_text() == '' and self.message.get_buffer().get_property('text') == '': + self.draft_button.child.set_text('Cancel') + else: + self.draft_button.child.set_text('SaveDraft') + l = len(self.message.get_buffer().get_property('text')) + if l <= 160: + m = 1 + else: + m = (l+152)/153 + self.cnt_label.set_text('%d chars / %d msgs' % (l, m)) + + def select(self, w, *a): + if not self.selecting: + self.message.input.active = False + w.child.set_text('Cut') + self.selecting = True + else: + self.message.input.active = True + w.child.set_text('Select') + self.selecting = False + b = self.message.get_buffer() + bound = b.get_selection_bounds() + if bound: + (s,e) = bound + t = b.get_text(s,e) + self.cutbuffer = t + b.delete_selection(True, True) + + def clear(self, *a): + w = self.get_toplevel().get_focus() + if w == None: + w = self.message + if w == self.message: + self.cutbuffer = self.message.get_buffer().get_property('text') + b = self.message.get_buffer() + b.set_text('') + else: + self.cutbuffer = w.get_text() + w.set_text('') + + + def paste(self, *a): + w = self.get_toplevel().get_focus() + if w == None: + w = self.message + if self.cutbuffer: + w.insert_at_cursor(self.cutbuffer) + pass + def send(self, w, style): + sender = '0403463349' + recipient = self.number + mesg = self.message.get_buffer().get_property('text') + if not mesg or not recipient: + return + try: + if style == 'EXE': + p = Popen(['exesms', sender, recipient, mesg], stdout = PIPE) + else: + p = Popen(['gsm-sms', sender, recipient, mesg], stdout = PIPE) + except: + rv = 1 + line = 'Fork Failed' + else: + line = 'no response' + rv = p.wait() + for l in p.stdout: + if l: + line = l + + s = SMSmesg(to = recipient, text = mesg) + + if rv or line[0:2] != 'OK': + s.state = 'DRAFT' + target = 'Draft' + else: + target = 'All' + self.store.store(s) + self.last_response.set_text('Mess Send: '+ line.strip()) + self.last_response.show() + + self.sender.hide() + self.listing.show() + self.rotate_list(target=target) + + def draft(self, *a): + sender = '0403463349' + recipient = self.numentry.get_text() + if recipient: + rl = [recipient] + else: + rl = [] + mesg = self.message.get_buffer().get_property('text') + if mesg: + s = SMSmesg(to = recipient, text = mesg, state = 'DRAFT') + self.store.store(s) + self.sender.hide() + self.listing.show() + self.rotate_list(target='Draft') + def config(self, *a): + pass + def delete(self, *a): + if len(self.listview.smslist ) < 1: + return + s = self.listview.smslist[self.listview.selected] + self.store.delete(s) + sel = self.listview.selected + self.rotate_list(target=self.display_list) + self.listview.selected = sel + if self.viewing: + self.view(self.view_button) + + def view(self, w, *a): + if self.viewing: + w.child.set_text('View') + self.viewing = False + self.singlescroll.hide() + self.listview.show() + if self.listview.smslist and len(self.listview.smslist ) >= 1: + s = self.listview.smslist[self.listview.selected] + if s.state == 'NEW': + self.store.setstate(s, None) + if self.display_list == 'New': + self.rotate_list(target='New') + self.reply.child.set_text('New') + else: + if not self.listview.smslist or len(self.listview.smslist ) < 1: + return + s = self.listview.smslist[self.listview.selected] + w.child.set_text('List') + self.viewing = True + self.last_response.hide() + self.listview.hide() + if self.book: + n = book_name(self.book, s.correspondent) + if n and n[0]: + n = n[0] + ' ['+s.correspondent+']' + else: + n = s.correspondent + else: + n = s.correspondent + if s.source == 'LOCAL': + t = 'To: ' + n + '\n' + else: + t = 'From: %s (%s)\n' % (n, s.source) + tm = time.strftime('%d%b%Y %H:%M:%S', s.time[0:6]+(0,0,0)) + t += 'Time: ' + tm + '\n' + t += '\n' + t += s.text + self.singleview.get_buffer().set_text(t) + self.singlescroll.show() + + if s.source == 'LOCAL': + self.reply.child.set_text('Open') + else: + self.reply.child.set_text('Reply') + + def open(self, *a): + if self.viewing: + if len(self.listview.smslist) < 1: + return + s = self.listview.smslist[self.listview.selected] + if s.state == 'NEW': + self.store.setstate(s, None) + + self.numentry.set_text(s.correspondent) + self.message.get_buffer().set_text(s.text) + self.draft_button.child.set_text('SaveDraft') + else: + self.numentry.set_text('') + self.message.get_buffer().set_text('') + self.draft_button.child.set_text('Cancel') + self.listing.hide() + self.sender.show() + + def load_list(self, lines): + now = time.time() + l = [] + target = self.display_list + patn = self.search_entry.get_text() + #print 'pattern is', patn + if target == 'New': + (now, l) = self.store.lookup(now, 'NEW') + elif target == 'Draft': + (now, l) = self.store.lookup(now, 'DRAFT') + else: + if lines == 0: lines = 20 + while now and len(l) < lines: + (now, l2) = self.store.lookup(now) + for e in l2: + if patn and patn not in e.correspondent: + continue + if target == 'All': + l.append(e) + elif target == 'Sent' and e.source == 'LOCAL': + l.append(e) + elif target == 'Recv' and e.source != 'LOCAL': + l.append(e) + return l + + def rotate_list(self, w=None, ev=None, which = None, target=None): + # lists are: + # All, Recv, New, Sent, Draft + # When one is current, two others can be selected + + if target == None: + if w == None: + target = self.display_list + else: + target = w.child.get_text() + + if target == 'All': + self.buttonA.child.set_text('Sent') + self.buttonB.child.set_text('Recv') + if target == 'Sent': + self.buttonA.child.set_text('All') + self.buttonB.child.set_text('Draft') + if target == 'Draft': + self.buttonA.child.set_text('All') + self.buttonB.child.set_text('Sent') + if target == 'Recv': + self.buttonA.child.set_text('All') + self.buttonB.child.set_text('New') + if target == 'New': + self.buttonA.child.set_text('All') + self.buttonB.child.set_text('Recv') + + self.display_list = target + self.listview.reset_list() + + def clear_search(self, *a): + pass + + def got_new(self): + self.rotate_list(self, target = 'New') + +def main(args): + for p in ['/media/card','/media/disk','/var/tmp']: + if os.path.exists(p): + pth = p + break + w = SendSMS(SMSstore(pth+'/SMS')) + gtk.settings_get_default().set_long_property("gtk-cursor-blink", 0, "main") + + gtk.main() + +if __name__ == '__main__': + main(sys.argv) diff --git a/sms/storesms.py b/sms/storesms.py new file mode 100644 index 0000000..3d3e3b1 --- /dev/null +++ b/sms/storesms.py @@ -0,0 +1,483 @@ +# +# FIXME +# - trim newmesg and draft when possible. +# - remove old multipart files +# +# Store SMS messages is a bunch of files, one per month. +# Each message is stored on one line with space separated . +# URL encoding (%XX) is used to quote white space, unprintables etc +# We store 5 fields: +# - time stamp that we first saw the message. This is in UTC. +# This is the primary key. If a second message is seen in the same second, +# we quietly add 1 to the second. +# - Source, one of 'LOCAL' for locally composed, 'GSM' for recieved via GSM +# or maybe 'EMAIL' if received via email?? +# - Time message was sent, Localtime with -TZ. For GSM messages this comes with the +# message. For 'LOCAL' it might be '-', or will be the time we succeeded +# in sending. +# time is stored as a tupple (Y m d H M S Z) where Z is timezone in multiples +# of 15 minutes. +# - The correspondent: sender if GSM, recipient if LOCAL, or '-' if not sent. +# This might be a comma-separated list of recipients. +# - The text of the message +# +# Times are formatted %Y%m%d-%H%M%S and local time has a GSM TZ suffix. +# GSM TZ is from +48 to -48 in units of 15 minutes. (0 is +00) +# +# We never modify a message once it has been stored. +# If we have a draft that we edit and send, we delete the draft and +# create a new sent-message +# If we forward a message, we will then have two copies. +# +# New messages are not distinguished by a flag (which would have to be cleared) +# but by being in a separate list of new messages. +# We havea list of 'new' messages and a list of 'draft' messages. +# +# Multi-part messages are accumulated as they are received. The quoted message +# contains text for each part of the message. +# e.g. <1><2>nd%20so%20no.....<3> +# if we only have part 2 of 3. +# For each incomplete message there is a file (like 'draft' and 'newmesg') named +# for the message which provides an index to each incomplete message. +# It will be named e.g. 'multipart-1C' when 1C is the message id. +# +# This module defines 2 classes: +# SMSmesg +# This holds a message and so has timestamp, source, time, correspondent +# and text fields. These are decoded. +# SMSmesg also has 'state' which can be one of "NEW", "DRAFT" or None +# Finally it might have a 'ref' and a 'part' which is a tuple (this,max) +# This is only used when storing the message to link it up with +# a partner +# +# SMSstore +# This represents a collection of messages in a directory (one file per month) +# and provides lists of 'NEW' and 'DRAFT' messages. +# Operations: +# store(SMSmesg, NEW|DRAFT|) -> None +# stores the message and sets the timestamp +# lookup(latest-time, NEW|DRAFT|ALL) -> (earlytime, [SMSmesg]) +# collects a list of messages in reverse time order with times no later +# than 'latest-time'. Only consider NEW or DRAFT or ALL messages. +# The list may not be complete (typically one month at a time are returnned) +# If you want more, call again with 'earlytime' as 'latest-time'). +# delete(SMSmesg) +# delete the given message (based on the timestamp only) +# setstate(SMSmesg, NEW|DRAFT|None) +# update the 'new' and 'draft' lists or container, or not container, this +# message. +# +# + +import os, fcntl, re, time, urllib + +def umktime(tm): + # like time.mktime, but tm is UTC + # So we want a 't' where + # time.gmtime(t)[0:6] == tm[0:6] + estimate = time.mktime(tm) - time.timezone + t2 = time.gmtime(estimate) + while t2[0:6] < tm[0:6]: + estimate += 15*60 + t2 = time.gmtime(estimate) + while t2[0:6] > tm[0:6]: + estimate -= 15*60 + t2 = time.gmtime(estimate) + return estimate + +def parse_time(strg): + return int(umktime(time.strptime(strg, "%Y%m%d-%H%M%S"))) +def parse_ltime(strg): + z = strg[-3:] + return time.strptime(strg[:-3], "%Y%m%d-%H%M%S")[0:6] + (int(z),) +def format_time(t): + return time.strftime("%Y%m%d-%H%M%S", time.gmtime(t)) +def format_ltime(tm): + return time.strftime("%Y%m%d-%H%M%S", tm[0:6]+(0,0,0)) + ("%+03d" % tm[6]) + + +class SMSmesg: + def __init__(self, **a): + if len(a) == 1 and 'line' in a: + # line read from a file, with 5 fields. + # stamp, source, time, correspondent, text + line = a['line'].split() + self.stamp = parse_time(line[0]) + self.source = line[1] + self.time = parse_ltime(line[2]) + self.correspondents = [] + for c in line[3].split(','): + if c != '-': + self.correspondents.append(urllib.unquote(c)) + self.set_corresponent() + self.state = None + + self.parts = None + txt = line[4] + if txt[0] != '<': + self.text = urllib.unquote(txt) + return + # multipart: <1>text...<2>text...<3><4> + m = re.findall('<(\d+)>([^<]*)', txt) + parts = [] + for (pos, strg) in m: + p = int(pos) + while len(parts) < p: + parts.append(None) + if strg: + parts[p-1] = urllib.unquote(strg) + self.parts = parts + self.reduce_parts() + else: + self.stamp = int(time.time()) + self.source = None + lt = time.localtime() + z = time.timezone/15/60 + if lt[8] == 1: + z -= 4 + self.time = time.localtime()[0:6] + (z,) + self.correspondents = [] + self.text = "" + self.state = None + self.ref = None + self.parts = None + part = None + for k in a: + if k == 'stamp': + self.stamp = a[k] + elif k == 'source': + self.source = a[k] + elif k == 'time': + # time can be a GSM string: 09/02/09,09:56:28+44 (ymd,HMS+z) + # or a tuple (y,m,d,H,M,S,z) + if type(a[k]) == str: + t = a[k][:-3] + z = a[k][-3:] + tm = time.strptime(t, "%y/%m/%d,%H:%M:%S") + self.time = tm[0:6] + (int(z),) + elif k == 'to' or k == 'sender': + if self.source == None: + if k == 'to': + self.source = 'LOCAL' + if k == 'sender': + self.source = 'GSM' + self.correspondents = [ a[k] ] + elif k == 'correspondents': + self.correspondents = a[k] + elif k == 'text': + self.text = a[k] + elif k == 'state': + self.state = a[k] + elif k == 'ref': + if a[k] != None: + self.ref = a[k] + elif k == 'part': + if a[k]: + part = a[k] + else: + raise ValueError + if self.source == None: + self.source = 'LOCAL' + if part: + print 'part', part[0], part[1] + self.parts = [None for x in range(part[1])] + self.parts[part[0]-1] = self.text + self.reduce_parts() + self.set_corresponent() + + self.month_re = re.compile("^[0-9]{6}$") + + def reduce_parts(self): + def reduce_pair(a,b): + if b == None: + b = "...part of message missing..." + if a == None: + return b + return a+b + self.text = reduce(reduce_pair, self.parts) + + + def set_corresponent(self): + if len(self.correspondents) == 1: + self.correspondent = self.correspondents[0] + elif len(self.correspondents) == 0: + self.correspondent = "Unsent" + else: + self.correspondent = "Multiple" + + def format(self): + fmt = "%s %s %s %s " % (format_time(self.stamp), self.source, + format_ltime(self.time), + self.format_correspondents()) + if not self.parts: + return fmt + urllib.quote(self.text) + + for i in range(len(self.parts)): + fmt += ("<%d>" % (i+1)) + urllib.quote(self.parts[i]) if self.parts[i] else "" + return fmt + + def format_correspondents(self): + r = "" + for i in self.correspondents: + if i: + r += ',' + urllib.quote(i) + if r: + return r[1:] + else: + return '-' + + +class SMSstore: + def __init__(self, dir): + self.month_re = re.compile("^[0-9]{6}$") + self.cached_month = None + self.dirname = dir + # find message files + self.set_files() + self.drafts = self.load_list('draft') + self.newmesg = self.load_list('newmesg') + + def load_list(self, name, update = None, *args): + + l = [] + try: + f = open(self.dirname + '/' + name, 'r+') + except IOError: + return l + + if update: + fcntl.lockf(f, fcntl.LOCK_EX) + for ln in f: + l.append(parse_time(ln.strip())) + l.sort() + l.reverse() + + if update and update(l, *args): + f2 = open(self.dirname + '/' + name + '.new', 'w') + for t in l: + f2.write(format_time(t)+"\n") + f2.close() + os.rename(self.dirname + '/' + name + '.new', + self.dirname + '/' + name) + f.close() + return l + + def load_month(self, f): + # load the messages from f, which is open for read + rv = {} + for l in f: + l.strip() + m = SMSmesg(line=l) + rv[m.stamp] = m + if m.stamp in self.drafts: + m.state = 'DRAFT' + elif m.stamp in self.newmesg: + m.state = 'NEW' + return rv + + def store_month(self, l, m): + dm = self.dirname + '/' + m + f = open(dm+'.new', 'w') + for s in l: + f.write(l[s].format() + "\n") + f.close() + os.rename(dm+'.new', dm) + if not m in self.files: + self.files.append(m) + self.files.sort() + self.files.reverse() + if self.cached_month == m: + self.cache = l + + def store(self, sms): + orig = None + if sms.ref != None: + # This is part of a multipart. + # If there already exists part of this + # merge them together + # + times = self.load_list('multipart-' + sms.ref) + if len(times) == 1: + orig = self.load(times[0]) + if orig and orig.parts: + for i in range(len(sms.parts)): + if sms.parts[i] == None and i < len(orig.parts): + sms.parts[i] = orig.parts[i] + else: + orig = None + + m = time.strftime("%Y%m", time.gmtime(sms.stamp)) + try: + f = open(self.dirname + '/' + m, "r+") + except: + f = open(self.dirname + '/' + m, "w+") + complete = True + if sms.ref != None: + for i in sms.parts: + if i == None: + complete = False + if complete: + sms.reduce_parts() + sms.parts = None + + fcntl.lockf(f, fcntl.LOCK_EX) + l = self.load_month(f) + while sms.stamp in l: + sms.stamp += 1 + l[sms.stamp] = sms + self.store_month(l, m); + f.close() + + if orig: + self.delete(orig) + if sms.ref != None: + if complete: + try: + os.unlink(self.dirname + '/multipart-' + sms.ref) + except: + pass + elif orig: + def replacewith(l, tm): + while len(l): + l.pop() + l.append(tm) + return True + self.load_list('multipart-' + sms.ref, replacewith, sms.stamp) + else: + f = open(self.dirname +'/multipart-' + sms.ref, 'w') + fcntl.lockf(f, fcntl.LOCK_EX) + f.write(format_time(sms.stamp) + '\n') + f.close() + + if sms.state == 'NEW' or sms.state == 'DRAFT': + s = 'newmesg' + if sms.state == 'DRAFT': + s = 'draft' + f = open(self.dirname +'/' + s, 'a') + fcntl.lockf(f, fcntl.LOCK_EX) + f.write(format_time(sms.stamp) + '\n') + f.close() + elif sms.state != None: + raise ValueError + + def set_files(self): + self.files = [] + for f in os.listdir(self.dirname): + if self.month_re.match(f): + self.files.append(f) + self.files.sort() + self.files.reverse() + + def lookup(self, lasttime = None, state = None): + if lasttime == None: + lasttime = int(time.time()) + if state == None: + return self.getmesgs(lasttime) + if state == 'DRAFT': + self.drafts = self.load_list('draft') + times = self.drafts + elif state == 'NEW': + self.newmesg = self.load_list('newmesg') + times = self.newmesg + else: + raise ValueError + + self.set_files() + self.cached_month = None + self.cache = None + rv = [] + for t in times: + if t > lasttime: + continue + s = self.load(t) + if s: + s.state = state + rv.append(s) + return(0, rv) + + def getmesgs(self, last): + rv = [] + for m in self.files: + t = parse_time(m + '01-000000') + if t > last: + continue + mon = self.load_month(open(self.dirname + '/' + m)) + for mt in mon: + if mt <= last: + rv.append(mon[mt]) + if rv: + rv.sort(cmp = lambda x,y:cmp(y.stamp, x.stamp)) + return (t-1, rv) + return (0, []) + + def load(self, t): + m = time.strftime("%Y%m", time.gmtime(t)) + if not m in self.files: + return None + if self.cached_month != m: + self.cached_month = m + self.cache = self.load_month(open(self.dirname + '/' + m)) + if t in self.cache: + return self.cache[t] + return None + + def delete(self, msg): + if isinstance(msg, SMSmesg): + tm = msg.stamp + else: + tm = msg + m = time.strftime("%Y%m", time.gmtime(tm)) + try: + f = open(self.dirname + '/' + m, "r+") + except: + return + + fcntl.lockf(f, fcntl.LOCK_EX) + l = self.load_month(f) + if tm in l: + del l[tm] + self.store_month(l, m); + f.close() + + def del1(l, tm): + if tm in l: + l.remove(tm) + return True + return False + + self.drafts = self.load_list('draft', del1, tm) + self.newmesg = self.load_list('newmesg', del1, tm) + + def setstate(self, msg, state): + tm = msg.stamp + + def del1(l, tm): + if tm in l: + l.remove(tm) + return True + return False + + if tm in self.drafts and state != 'DRAFT': + self.drafts = self.load_list('draft', del1, tm) + if tm in self.newmesg and state != 'NEW': + self.newmesg = self.load_list('newmesg', del1, tm) + + if tm not in self.drafts and state == 'DRAFT': + f = open(self.dirname +'/draft', 'a') + fcntl.lockf(f, fcntl.LOCK_EX) + f.write(format_time(sms.stamp) + '\n') + f.close() + self.drafts.append(tm) + self.drafts.sort() + self.drafts.reverse() + + if tm not in self.newmesg and state == 'NEW': + f = open(self.dirname +'/newmesg', 'a') + fcntl.lockf(f, fcntl.LOCK_EX) + f.write(format_time(sms.stamp) + '\n') + f.close() + self.newmesg.append(tm) + self.newmesg.sort() + self.newmesg.reverse() + + diff --git a/sms/test.py b/sms/test.py new file mode 100644 index 0000000..bacfd5e --- /dev/null +++ b/sms/test.py @@ -0,0 +1,33 @@ + +import os, time, sys +from storesms import SMSstore, SMSmesg + + +try: + os.mkdir("/tmp/sms") +except: + pass +st = SMSstore("/tmp/sms") + +if len(sys.argv) == 2 and sys.argv[1][0] == '-': + (next, l) = st.lookup(time.time(), sys.argv[1][1:]) + print next + for m in l: + print m.format() + +elif len(sys.argv) > 1: + m = SMSmesg(time.time(), + "0403463349", + ["0415836820"], + sys.argv[1] + ) + if len(sys.argv) > 2: + st.store(m, sys.argv[2]) + else: + st.store(m) +else: + (next, l) = st.lookup(time.time()) + print next + for m in l: + print m.format() + diff --git a/sounds/formats.h b/sounds/formats.h new file mode 100644 index 0000000..b5314f9 --- /dev/null +++ b/sounds/formats.h @@ -0,0 +1,127 @@ +#ifndef FORMATS_H +#define FORMATS_H 1 + +#include +#include + +/* Definitions for .VOC files */ + +#define VOC_MAGIC_STRING "Creative Voice File\x1A" +#define VOC_ACTUAL_VERSION 0x010A +#define VOC_SAMPLESIZE 8 + +#define VOC_MODE_MONO 0 +#define VOC_MODE_STEREO 1 + +#define VOC_DATALEN(bp) ((u_long)(bp->datalen) | \ + ((u_long)(bp->datalen_m) << 8) | \ + ((u_long)(bp->datalen_h) << 16) ) + +typedef struct voc_header { + u_char magic[20]; /* must be MAGIC_STRING */ + u_short headerlen; /* Headerlength, should be 0x1A */ + u_short version; /* VOC-file version */ + u_short coded_ver; /* 0x1233-version */ +} VocHeader; + +typedef struct voc_blocktype { + u_char type; + u_char datalen; /* low-byte */ + u_char datalen_m; /* medium-byte */ + u_char datalen_h; /* high-byte */ +} VocBlockType; + +typedef struct voc_voice_data { + u_char tc; + u_char pack; +} VocVoiceData; + +typedef struct voc_ext_block { + u_short tc; + u_char pack; + u_char mode; +} VocExtBlock; + +/* Definitions for Microsoft WAVE format */ + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#define COMPOSE_ID(a,b,c,d) ((a) | ((b)<<8) | ((c)<<16) | ((d)<<24)) +#define LE_SHORT(v) (v) +#define LE_INT(v) (v) +#define BE_SHORT(v) bswap_16(v) +#define BE_INT(v) bswap_32(v) +#elif __BYTE_ORDER == __BIG_ENDIAN +#define COMPOSE_ID(a,b,c,d) ((d) | ((c)<<8) | ((b)<<16) | ((a)<<24)) +#define LE_SHORT(v) bswap_16(v) +#define LE_INT(v) bswap_32(v) +#define BE_SHORT(v) (v) +#define BE_INT(v) (v) +#else +#error "Wrong endian" +#endif + +#define WAV_RIFF COMPOSE_ID('R','I','F','F') +#define WAV_WAVE COMPOSE_ID('W','A','V','E') +#define WAV_FMT COMPOSE_ID('f','m','t',' ') +#define WAV_DATA COMPOSE_ID('d','a','t','a') + +/* WAVE fmt block constants from Microsoft mmreg.h header */ +#define WAV_FMT_PCM 0x0001 +#define WAV_FMT_IEEE_FLOAT 0x0003 +#define WAV_FMT_DOLBY_AC3_SPDIF 0x0092 +#define WAV_FMT_EXTENSIBLE 0xfffe + +/* Used with WAV_FMT_EXTENSIBLE format */ +#define WAV_GUID_TAG "\x00\x00\x00\x00\x10\x00\x80\x00\x00\xAA\x00\x38\x9B\x71" + +/* it's in chunks like .voc and AMIGA iff, but my source say there + are in only in this combination, so I combined them in one header; + it works on all WAVE-file I have + */ +typedef struct { + u_int magic; /* 'RIFF' */ + u_int length; /* filelen */ + u_int type; /* 'WAVE' */ +} WaveHeader; + +typedef struct { + u_short format; /* see WAV_FMT_* */ + u_short channels; + u_int sample_fq; /* frequence of sample */ + u_int byte_p_sec; + u_short byte_p_spl; /* samplesize; 1 or 2 bytes */ + u_short bit_p_spl; /* 8, 12 or 16 bit */ +} WaveFmtBody; + +typedef struct { + WaveFmtBody format; + u_short ext_size; + u_short bit_p_spl; + u_int channel_mask; + u_short guid_format; /* WAV_FMT_* */ + u_char guid_tag[14]; /* WAV_GUID_TAG */ +} WaveFmtExtensibleBody; + +typedef struct { + u_int type; /* 'data' */ + u_int length; /* samplecount */ +} WaveChunkHeader; + +/* Definitions for Sparc .au header */ + +#define AU_MAGIC COMPOSE_ID('.','s','n','d') + +#define AU_FMT_ULAW 1 +#define AU_FMT_LIN8 2 +#define AU_FMT_LIN16 3 + +typedef struct au_header { + u_int magic; /* '.snd' */ + u_int hdr_size; /* size of header (min 24) */ + u_int data_size; /* size of data */ + u_int encoding; /* see to AU_FMT_XXXX */ + u_int sample_rate; /* sample rate */ + u_int channels; /* number of channels (voices) */ +} AuHeader; + +#endif /* FORMATS */ diff --git a/sounds/sound.c b/sounds/sound.c new file mode 100644 index 0000000..01d5122 --- /dev/null +++ b/sounds/sound.c @@ -0,0 +1,1440 @@ +/* + * + * This is a daemon that waits for sound files to appear in a particular + * directory, and when they do, it plays them. + * Files can be WAV or OGG VORBIS + * If there are multiple files, the lexically first is played + * If a file has a suffix of -NNNNNNN, then play starts that many + * milliseconds in to the file. + * When a file disappear, play stops. + * When the end of the sound is reached the file (typically a link) is removed. + * However an empty file is treated as containing infinite silence, so + * it is never removed. + * When a new file appears which is lexically earlier than the one being + * played, the played file is suspended until the earlier files are finished + * with. + * The current-play position (in milliseconds) is written to a file + * with the same name as the sound file, but with a leading period. + * + * Expected use is that various alert tones are added to the directory with + * early names, and a music file can be added with a later name for general + * listening. + * + * Contains code from: aplay.c - plays and records + * Copyright (c) by Jaroslav Kysela + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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 \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); +} diff --git a/sounds/sounds.py b/sounds/sounds.py new file mode 100644 index 0000000..d1f0013 --- /dev/null +++ b/sounds/sounds.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python +# +# This is a sound playing daemon for the freerunner. +# It watches the directory "/var/run/sound" and when a file appears +# there-in, it gets played. +# Currently the file must be a WAV file with 16 bit little-endian PCM encoding. +# +# Files can (and should) have priorities being leading digits. +# If there are multiple files, the one with the lowest number is played. +# +# Files normally appear via the creation of symlinks. +# When the file is removed, the playing stops. When a new file of higher +# priority appears, the current file is suspended. When the higher +# priority file is removed, the lower priority one resumes. +# +# When a file finishes playing it can do one of several thing: +# - the file can be removed (R) +# - the file can be replayed (P) +# - the player can stop and wait (W) +# If the file has a timestamp (on symlink) that has changed since +# play started, it is treated as a new file by 'W'. +# An empty file produces silence. +# +# File names should start with 1 or more leading digits (if there are +# no digits the effective priority is infinite). These form a number +# which is the reverse of priority, so a small number is played first. +# This a numerical sequence will be played in order. After these +# digits should be an R, P, or W. If none of these are present, R is +# assumed. +# +# The player can be stopped by creating a symlink from '0P-silence' to +# '/dev/null'. +# +# When a file is suspending, the position in the file, in microseconds +# is written to a new file with name formed by putting a '.' at the +# start of the file name. Maybe this is continuously updated... +# + + +import alsaaudio, time, struct, sys, os, signal, fcntl + +class PlayFile(): + def __init__(self, file, pcm): + # Arrange to play file through pcm + # Every time .play is called, we play some of the file + # If something else gets played, .resume must be called + # before .play is called again + self.pcm = pcm + self.filename = file + self.posfile = os.path.join(os.path.dirname(file), + '.'+os.path.basename(file)) + self.loadfile(file) + self.update() + + def loadfile(self, file): + # A wav file starts: + # 0-3 "RIFF" + # 4-7 Bytes in rest of file. + # 8-11 "WAVE" + # 12-15 "fmt " + # 16-19 bytes of format + # 20-21 ==1 Microsoft PCM + # 22-23 channels + # 24-27 freq + # 28-31 byte rate + # 32-33 bytes per frame + # 34-35 bits per sample + # 36-39 "data" + # 40-43 number of bytes of data + # 44... actual samples + self.pos = 0 + self.rate = 8000 + self.channels = 1 + self.bytes = 2 + self.format = alsaaudio.PCM_FORMAT_S16_LE + try: + self.f = open(file) + except IOError: + self.f = None + return + header = self.f.read(44) + if len(header) == 0: + # silence + return + if len(header) != 44: + raise IOError + riff, b1, wave, fmt, b2, format, chan, rate, br, bf, bs, data, b3 = \ + struct.unpack("4si4s 4sihhiihh 4si", header) + + if riff != "RIFF" or wave != "WAVE" or fmt != "fmt " or data != "data": + raise ValueError + if format == 1 and bs == 16: + self.format = alsaaudio.PCM_FORMAT_S16_LE + self.bytes = 2 + elif format == 1 and bs == 8: + self.format = alsaaudio.PCM_FORMAT_U8 + self.bytes = 1 + else: + raise ValueError + + if chan < 1 or chan > 4: + raise ValueError + else: + self.channels = chan + + self.rate = rate + self.finished = False + self.pos = 0; + self.resume() + + def resume(self): + try: + self.pcm.setformat(self.format) + self.pcm.setchannels(self.channels) + self.pcm.setrate(self.rate) + self.pcm.setperiodsize(640 / self.channels / self.bytes) + except: + pass + + def update(self): + f = open(self.posfile, 'w') + f.write("%d\n" % int(self.pos*1000000 / self.rate)) + + def play(self): + # play for at least 100ms + start = time.time() + if not self.f: + return False + while time.time() < start + 0.1: + data = self.f.read(640) + if not data: + self.finished = True + try: + os.unlink(self.posfile) + except OSError: + pass + return False + if len(data) % (self.channels * self.bytes) == 0: + self.pcm.write(data) + if len(data) != 640: + self.pcm.write(chr(0) * (640 - len(data))) + self.pos += len(data) / self.channels / self.bytes + self.update() + return True + + +class DirWatch: + def __init__(self, dirname): + self.mtime = 0 + self.dirname = dirname + self.name = '' + self.disp = '' + + def ping(self, *a): + signalled = True + + def choose(self, wait=False): + mtime = os.stat(self.dirname).st_mtime + if self.mtime == mtime: + if not wait: + return self.name, self.disp + # wait until it might have changed, using dnotify + f = os.open(self.dirname, 0) + signalled = False + signal.signal(signal.SIGIO, self.ping) + fcntl.fcntl(f, fcntl.F_NOTIFY, (fcntl.DN_MODIFY|fcntl.DN_RENAME| + fcntl.DN_CREATE|fcntl.DN_DELETE)) + mtime = os.stat(self.dirname).st_mtime + while not signalled and mtime == self.mtime: + signal.pause() + mtime = os.stat(self.dirname).st_mtime + os.close(f) + + # Better check again + self.mtime = mtime + min = None + disp = None + name = None + for n in os.listdir(self.dirname): + if n[0] == '.': + continue + (num,d) = self.parse(n) + if name == None: + name, disp, min = n, d, num + elif num == min: + if n > name: + name, disp = n, d + elif num == None: + pass + elif min == None or num < min: + name, disp, min = n, d, num + if name == None: + return name, None + self.name = os.path.join(self.dirname, name) + if disp != 'R' and disp != 'P': + disp = 'W' + self.disp = disp + return self.name, disp + + def parse(self, name): + n = '' + while name[0].isdigit(): + n += name[0] + name = name[1:] + disp = name[0] + if name[0] not in 'PRW': + disp = 'W' + + if n: + num = int(n) + else: + num = None + return num, disp + +def main(): + os.nice(-20) + dn = '/var/run/sound' + if not os.path.exists(dn): + os.mkdir(dn) + d = DirWatch(dn) + stack = [] + + current = None + disp = None + waiting = False + + while True: + newname, newdisp = d.choose(current == None or waiting) + if current and current.filename == newname: + if current.play(): + continue + if disp == 'R': + os.unlink(current.filename) + current = None + continue + if disp == 'P': + time.sleep(0.1) + current.loadfile(current.filename) + continue + waiting = True + continue + waiting = False + # need new... + if current and not os.path.exists(current.filename): + current = None + + if current == None and len(stack) > 0: + current, disp = stack.pop() + current.resume() + continue + + if current: + stack.append((current,disp)) + + if newname == None: + continue + + pcm = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK) + current = PlayFile(newname, pcm) + del pcm + disp = newdisp + +main() diff --git a/test/autocon.c b/test/autocon.c new file mode 100644 index 0000000..e1722cd --- /dev/null +++ b/test/autocon.c @@ -0,0 +1,60 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +main(int argc, char *argv[]) +{ + int fd; + struct ifreq ifr; + int err; + char *dev; + char cmdbuf[1000]; + char pbuf[1500]; + int n; + + fd = open("/dev/net/tun", O_RDWR); + if (fd < 0) { + perror("tun"); + exit(1); + } + + memset(&ifr, 0, sizeof(ifr)); + + ifr.ifr_flags = IFF_TUN; + err = ioctl(fd, TUNSETIFF, &ifr); + if (err < 0) { + perror("TUNSETIFF"); + exit(1); + } + + dev = ifr.ifr_name; + + printf("dev = %s\n", dev); + + sprintf(cmdbuf, "ifconfig %s 10.255.255.254 pointopoint 10.255.255.253", dev); + system(cmdbuf); + + sprintf(cmdbuf, "route add -net 0.0.0.0/1 gw 10.255.255.254 dev %s", dev); + system(cmdbuf); + sprintf(cmdbuf, "route add -net 128.0.0.0/1 gw 10.255.255.254 dev %s", dev); + system(cmdbuf); + + n = read(fd, pbuf, sizeof(pbuf)); + printf("read got %d\n", n); + + system(cmdbuf); + + system("ifconfig wlan0 192.168.1.70"); + system(" iptables -A POSTROUTING -t nat -j MASQUERADE -s 10.255.255.254"); + write(fd, pbuf, n); + while (1) { + n = read(fd, pbuf, sizeof(pbuf)); + printf("read got %d\n", n); + } +} diff --git a/test/clock.py b/test/clock.py new file mode 100644 index 0000000..d57d6f8 --- /dev/null +++ b/test/clock.py @@ -0,0 +1,36 @@ + + +import gtk, pango + +w = gtk.Window(gtk.WINDOW) +w.set_size_request(16,16) +w.realize() +w.window.property_change('_XEMBED_INFO', '_XEMBED_INFO', 32, gtk.gdk.PROP_MODE_REPLACE, [1,1]) +fd = pango.FontDescription('sans 10') +fd.set_absolute_size(25*pango.SCALE) +w.modify_font(fd) +layout = w.create_pango_layout("88:88") +(ink, (ex,ey,ew,eh)) = layout.get_pixel_extents() + +pm = gtk.gdk.Pixmap(w.window, ew,eh) +pm.draw_rectangle(w.get_style().bg_gc[gtk.STATE_NORMAL], + True, 0, 0, ew, eh) +pm.draw_layout(w.get_style().fg_gc[gtk.STATE_NORMAL], + 0,0,layout) +w.set_size_request(ew,eh) + + +w.show() + +def redraw(a,b): + print "event", b + w.window.draw_rectangle(w.get_style().bg_gc[gtk.STATE_NORMAL], + True, 0, 0, ew, eh) + w.window.draw_layout(w.get_style().fg_gc[gtk.STATE_NORMAL], + 0,0,layout) + + +w.connect('expose-event', redraw) +w.connect('configure-event', redraw) +gtk.main() + diff --git a/test/fake-test.c b/test/fake-test.c new file mode 100644 index 0000000..4527d44 --- /dev/null +++ b/test/fake-test.c @@ -0,0 +1,31 @@ + +#include + +#include + +main(int argc, char *argv[]) +{ + Display *d; + FakeKey *f; + int a; + char *c; + + d = XOpenDisplay(NULL); + + f = fakekey_init(d); + + for (a=1; a' : "greater", + '?' : "question", + '@' : "at", + '[' : "bracketleft", + ']' : "bracketright", + '\\' : "backslash", + '^' : "asciicircum", + '_' : "underscore", + '`' : "grave", + '{' : "braceleft", + '|' : "bar", + '}' : "braceright", + '~' : "asciitilde" + } + + +def get_keysym(ch) : + keysym = Xlib.XK.string_to_keysym(ch) + if keysym == 0 : + # Unfortunately, although this works to get the correct keysym + # i.e. keysym for '#' is returned as "numbersign" + # the subsequent display.keysym_to_keycode("numbersign") is 0. + keysym = Xlib.XK.string_to_keysym(special_X_keysyms[ch]) + return keysym + +def is_shifted(ch) : + if ch.isupper() : + return True + if "~!@#$%^&*()_+{}|:\"<>?".find(ch) >= 0 : + return True + return False + +def char_to_keycode(ch) : + keysym = get_keysym(ch) + keycode = display.keysym_to_keycode(keysym) + if keycode == 0 : + print "Sorry, can't map", ch + + if (is_shifted(ch)) : + shift_mask = Xlib.X.ShiftMask + else : + shift_mask = 0 + + return keycode, shift_mask + +def send_string(str) : + for ch in str : + #print "sending", ch, "=", display.keysym_to_keycode(Xlib.XK.string_to_keysym(ch)) + keycode, shift_mask = char_to_keycode(ch) + if (UseXTest) : + #print "Trying fake_input of", ch, ", shift_mask is", shift_mask + if shift_mask != 0 : + Xlib.ext.xtest.fake_input(display, Xlib.X.KeyPress, 50) + Xlib.ext.xtest.fake_input(display, Xlib.X.KeyPress, keycode) + Xlib.ext.xtest.fake_input(display, Xlib.X.KeyRelease, keycode) + if shift_mask != 0 : + Xlib.ext.xtest.fake_input(display, Xlib.X.KeyRelease, 50) + else : + event = Xlib.protocol.event.KeyPress( + time = int(time.time()), + root = display.screen().root, + window = window, + same_screen = 0, child = Xlib.X.NONE, + root_x = 0, root_y = 0, event_x = 0, event_y = 0, + state = shift_mask, + detail = keycode + ) + window.send_event(event, propagate = True) + event = Xlib.protocol.event.KeyRelease( + time = int(time.time()), + root = display.screen().root, + window = window, + same_screen = 0, child = Xlib.X.NONE, + root_x = 0, root_y = 0, event_x = 0, event_y = 0, + state = shift_mask, + detail = keycode + ) + window.send_event(event, propagate = True) + +for argp in range(1, len(sys.argv)) : + send_string(sys.argv[argp]) + display.sync() diff --git a/test/reflash b/test/reflash new file mode 100644 index 0000000..7902438 --- /dev/null +++ b/test/reflash @@ -0,0 +1,5 @@ + +cd /home/git/dfu-util +./src/dfu-util -a rootfs --device 1d50:5119 -D /home/neilb/Desktop/FSO/*jffs* +./src/dfu-util -a kernel --device 1d50:5119 -D /home/neilb/Desktop/FSO/uImag* +#./src/dfu-util --device 1d50:5119 --reset diff --git a/test/status.py b/test/status.py new file mode 100644 index 0000000..0e39c4c --- /dev/null +++ b/test/status.py @@ -0,0 +1,51 @@ + +import sys +import pygtk +import gtk +import os +import gobject + +capfile = "/sys/class/power_supply/battery/capacity" +curlimfile = "/sys/class/i2c-adapter/i2c-0/0-0073/pcf50633-mbc/usb_curlim" +chgfile = "/sys/class/i2c-adapter/i2c-0/0-0073/pcf50633-mbc/chgmode" +currfile = "/sys/class/power_supply/battery/current_now" +filename = "/media/card/panel-plugin/pixmaps/battery_%03d.png" +filename_charging = "/media/card/panel-plugin/pixmaps/battery_%03d_charging_%d.png" + +def file_text(name): + try: + f = open(name) + except: + return "" + t = f.read() + return t.strip() +def file_num(name): + try: + i = int(file_text(name)) + except: + i = 0 + return i + +def setfile(icon): + cap = file_num(capfile) + capr = int((cap+5)/10)*10 + curr = file_num(currfile) + lim = file_num(curlimfile) + if curr >= 0 or lim == 0: + f = filename % capr + else: + f = filename_charging % (capr, lim) + print f + i.set_from_file(f) + +def update(): + global i + setfile(i) + to = gobject.timeout_add(30*1000, update) + +i = gtk.StatusIcon() +setfile(i) +i.set_visible(True) +to = gobject.timeout_add(30*1000, update) + +gtk.main() diff --git a/test/test.py b/test/test.py new file mode 100644 index 0000000..06477d3 --- /dev/null +++ b/test/test.py @@ -0,0 +1,49 @@ + +import sys +import pygtk +import gtk +import os +import gobject + +capfile = "/sys/class/power_supply/battery/capacity" +curlimfile = "/sys/class/i2c-adapter/i2c-0/0-0073/pcf50633-mbc/usb_curlim" +chgfile = "/sys/class/i2c-adapter/i2c-0/0-0073/pcf50633-mbc/chgmode" +currfile = "/sys/class/power_supply/battery/current_now" +filename = "/tmp/pixmaps/battery_%03d.png" +filename_charging = "/media/card/panel-plugin/pixmaps/battery_%03d_charging_%d.png" + +def file_text(name): + try: + f = open(name) + except: + return "" + t = f.read() + return t.strip() +def file_num(name): + try: + i = int(file_text(name)) + except: + i = 0 + return i + +def setfile(icon, capr): + curr = 1 + lim = 0 + if curr >= 0 or lim == 0: + f = filename % capr + else: + f = filename_charging % (capr, lim) + print f + i.set_from_file(f) + +def update(): + global i + setfile(i, 0) + to = gobject.timeout_add(30*1000, update) + +i = gtk.StatusIcon() +setfile(i,100) +i.set_visible(True) +to = gobject.timeout_add(10*1000, update) + +gtk.main() diff --git a/test/test1.py b/test/test1.py new file mode 100644 index 0000000..0f2f17a --- /dev/null +++ b/test/test1.py @@ -0,0 +1,35 @@ + +# experiment with clip board +# We define a clip board "test" +# We set it to 'waiting' and whenever anyone else sets it, +# we collect the value and reset to 'waiting' + +import gtk +import pygtk +import gobject +targets = [ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ] + +def getdata(clipb, sel, info, data): + print "sending" + sel.set_text("waiting") + +def cleardatadelay(clipb, data): + print 'cleardel' + gobject.timeout_add(2000, lambda : cleardata(clipb, data)) + +def cleardata(clipb, data): + a = clipb.wait_for_text() + print "Got ", a + clipb.set_with_data(targets, getdata, cleardatadelay, None) + +cb = gtk.Clipboard(selection='PRIMARY') + +def set(): + global cb + print "set" + cb.set_with_data(targets, getdata, cleardatadelay, None) + +gobject.idle_add(set) + +gtk.main() + diff --git a/test/test2.py b/test/test2.py new file mode 100644 index 0000000..015be97 --- /dev/null +++ b/test/test2.py @@ -0,0 +1,29 @@ + +# get the value from clipboard "test", then set a new value + +import gtk +import pygtk +import sys +import gobject + +targets = [ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ] + +def getdata(clipb, sel, info, data): + a = sys.argv[1] + print "sending", a + sel.set_text(a) + +def cleardata(clipb, data): + print "clear" + gtk.main_quit() + +cb = gtk.Clipboard(selection='PRIMARY') + +def set(): + global cb + print "set" + cb.set_with_data(targets, getdata, cleardata, None) + +gobject.idle_add(set) +gtk.main() + diff --git a/test/wkalrm.c b/test/wkalrm.c new file mode 100644 index 0000000..89add78 --- /dev/null +++ b/test/wkalrm.c @@ -0,0 +1,244 @@ +/* + * wkalrm.c - Use the RTC alarm to wake us up + * + * Copyright (C) 2008 by OpenMoko, Inc. + * Written by Werner Almesberger + * 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 +#include +#include +#include +#include +#include +#include +#include + + +#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; +}