]> git.neil.brown.name Git - freerunner.git/blob - launcher/launch.py
Lots of random updates
[freerunner.git] / launcher / launch.py
1 #!/usr/bin/env python2.5
2
3 # Neo Launcher
4 # inspired in part by Auxlaunch
5 #
6 # We have a list of folders each of which contains a list of
7 # tasks, each of which can have a small number of options.
8 # We present the folders in one column and the tasks in another,
9 # with the options in buttons below.
10 # Task types are:
11 #   Program:  Option is to run the program
12 #      If the program is currently running, there is also an option to
13 #      kill the program
14 #      If there is a window that is believed to be attached to the program
15 #      The kill option simply closes that window, and there is another option
16 #        to raise the window
17 #   Window: Options are to raise or to kill the window.
18 #
19 #
20 # TODO
21 #  LATER  Make list of windows-to-exclude (Panel 0) configurable
22 #  Sort things?
23 #  more space around main words
24 #
25 # Design thoughts 28Dec2010
26 #  Having separete 'internal' folders is bad
27 #  And having to explicitly list lots of speed-dials for a speed-dial
28 #  folder is bad.
29 #  So I want two classes of folder:
30 #  - one that has an explicit list of items, which can be programs or
31 #    any internal function.
32 #  - one that has an implicit list of items, which is generated by an
33 #    internal function or a plug-in
34 #  The implicit list could be an internal function which creates multiple
35 #  entries..
36 #  So:
37 #  [foldername]
38 #   tag,command line,window-name
39 #   tag,(internal)
40 #   tag,internal()
41 #   tag,module.internal(arguments)
42 #   *,internal(arguments)
43 #
44 # The list-creating function would need a call-back to ask for the list
45 # to be re-calculated
46 # possible function lists are:
47 # - windows
48 # - speed-dials
49 # - recent-calls
50 # - wifi networks scanned (add/add-auto, possibly with password)
51 # - wifi networks known (connect, delete)
52 # - nearby time zones
53 # - generic list that was asked for, thus effecting a three-level
54 #   list.  Maybe I want that anyway?
55 # Slight change - allow an internal function to provide a new list.  This
56 # switches to a virtual folder showing that list.  When the original folder
57 # is selected, we go back there...
58
59 import gtk, gobject
60 import pygtk
61 import sys, os, time
62 import pango, re
63 import struct
64 import dnotify
65 import fcntl
66 from fingerscroll import FingerScroll
67 from subprocess import Popen, PIPE
68 from wmctrl import winlist
69
70 import ctypes
71 libc = ctypes.cdll.LoadLibrary("libc.so.6")
72 libc.mlockall(3)
73
74 class EvDev:
75     def __init__(self, path, on_event):
76         self.f = os.open(path, os.O_RDWR|os.O_NONBLOCK);
77         self.ev = gobject.io_add_watch(self.f, gobject.IO_IN, self.read)
78         self.on_event = on_event
79         self.grabbed = False
80         self.down_count = 0
81     def read(self, x, y):
82         try:
83             str = os.read(self.f, 16)
84         except:
85             return True
86
87         if len(str) != 16:
88             return True
89         (sec,usec,typ,code,value) = struct.unpack_from("IIHHI", str)
90         if typ == 0x01:
91             # KEY event
92             if value == 0:
93                 self.down_count -= 1
94             else:
95                 self.down_count += 1
96             if self.down_count < 0:
97                 self.down_count = 0
98         self.on_event(self.down_count, typ, code, value, sec* 1000 + int(usec/1000))
99         return True
100     def grab(self):
101         if self.grabbed:
102             return
103         #print "grab"
104         fcntl.ioctl(self.f, EVIOC_GRAB, 1)
105         self.grabbed = True
106     def ungrab(self):
107         if not self.grabbed:
108             return
109         #print "release"
110         fcntl.ioctl(self.f, EVIOC_GRAB, 0)
111         self.grabbed = False
112
113
114 class WinList:
115     """
116     read in a window list - present each as a Task
117     Allow registering tasks so that when a window appears, we connect it.
118     """
119     def __init__(self):
120         self.windows = {}
121         self.tasks = {}
122         self.tasklist = []
123         self.pid = os.getpid()
124         self.old_windows = None
125         self.last_reload = 0
126         self.winlist = winlist()
127         gobject.io_add_watch(self.winlist.fd, gobject.IO_IN, self.winlist.events)
128         self.winlist.on_change(self.refresh)
129
130     def add(self, winid, desk, pid, host, name):
131         self.windows[winid] = [name, pid]
132         if self.old_windows and winid in self.old_windows:
133             self.windows[winid] = [name, pid] + self.old_windows[winid][2:]
134         p = 'pid:%d' % int(pid)
135         #print "Looking for ", p
136         if p in self.tasks:
137             self.tasks[p](winid)
138             self.windows[winid].append(self.tasks[p])
139             del self.tasks[p]
140         n = 'name:' + name
141         if n in self.tasks:
142             self.tasks[n](winid)
143             self.windows[winid].append(self.tasks[n])
144             del self.tasks[n]
145
146     def remove_old(self):
147         for winid in self.old_windows:
148             if not winid in self.windows:
149                 #print "removing",winid
150                 for c in self.old_windows[winid][2:]:
151                     c("")
152         self.old_windows = None
153
154     def register(self, pid, name, found):
155         if pid != None:
156             p = 'pid:%d' % int(pid)
157             self.tasks[p] = found
158         if name != None:
159             n = 'name:' + name
160             self.tasks[n] = found
161
162     def reload(self):
163         self.old_windows = self.windows
164         self.windows = {}
165         self.tasklist = []
166         for w in self.winlist.winfo:
167             win = self.winlist.winfo[w]
168             if win.pid == self.pid:
169                 continue
170             self.add(win, 0, win.pid, '', win.name)
171             self.tasklist.append(WinTask(win, 0, win.pid, '', win.name))
172         self.remove_old()
173
174         togo = []
175         for k in self.tasks:
176             if not self.tasks[k]():
177                 togo.append(k)
178         for k in togo:
179             del self.tasks[k]
180
181         return self.tasklist
182
183
184     def refresh(self):
185         self.reload()
186         global window
187         if window:
188             window.refresh()
189
190 ProcessList = []
191 class JobCtrl:
192     """
193     Manage processes.
194     Call to start a process, and get a call back when the process finishes.
195
196     """
197     global ProcessList
198     def __init__(self, cmd, finished = None):
199         self.finished = finished
200         if cmd == None:
201             self._child_created = False
202             return
203         self.Popen = Popen(cmd, shell=True, close_fds = True)
204         self.pid = self.Popen.pid
205         self.returncode = None
206         ProcessList.append(self)
207
208     def poll(self):
209         if self.Popen.poll() != None and self.finished:
210             self.returncode = self.Popen.returncode
211             self.finished(self.returncode)
212             self.finished = None
213             window.folder_select(window.folder_num)
214         return self.returncode
215             
216             
217     def poll_all(self):
218         l = range(len(ProcessList))
219         l.reverse()
220         for i in l:
221             p = ProcessList[i]
222             if p.poll() != None:
223                 del ProcessList[i]
224                 
225         
226
227 class Selector(gtk.DrawingArea):
228     def __init__(self, names = [], pos = 0, center=False):
229         gtk.DrawingArea.__init__(self)
230
231         self.on_select = None
232         self.do_center = center
233
234         self.pixbuf = None
235         self.width = self.height = 0
236         self.need_redraw = True
237         self.colours = None
238         self.collist = {}
239
240         self.connect("expose-event", self.redraw)
241         self.connect("configure-event", self.reconfig)
242         
243         self.connect("button_release_event", self.release)
244         self.connect("button_press_event", self.press)
245         self.set_events(gtk.gdk.EXPOSURE_MASK
246                         | gtk.gdk.BUTTON_PRESS_MASK
247                         | gtk.gdk.BUTTON_RELEASE_MASK
248                         | gtk.gdk.STRUCTURE_MASK)
249
250         # choose a font
251         fd = self.get_pango_context().get_font_description()
252         fd.set_absolute_size(25 * pango.SCALE)
253         self.fd = fd
254         self.modify_font(fd)
255         met = self.get_pango_context().get_metrics(fd)
256         self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE
257         self.lineheight *= 1.5
258         self.lineheight = int(self.lineheight)
259
260         self.offsets = []
261         self.names = names
262         self.pos = pos
263         self.top = 0
264         self.queue_draw()
265
266     def assign_colour(self, purpose, name):
267         self.collist[purpose] = name
268
269     def set_list(self, names, pos = 0):
270         self.names = names
271         self.pos = pos
272         self.refresh()
273         if self.on_select:
274             self.on_select(pos)
275
276     def reconfig(self, w, ev):
277         alloc = w.get_allocation()
278         if not self.pixbuf:
279             return
280         if alloc.width != self.width or alloc.height != self.height:
281             self.pixbuf = None
282             self.need_redraw = True
283
284     def add_col(self, sym, col):
285         c = gtk.gdk.color_parse(col)
286         gc = self.window.new_gc()
287         gc.set_foreground(self.get_colormap().alloc_color(c))
288         self.colours[sym] = gc
289
290     def redraw(self, w, ev):
291         if self.colours == None:
292             self.colours = {}
293             for p in self.collist:
294                 self.add_col(p, self.collist[p])
295             self.bg = self.get_style().bg_gc[gtk.STATE_NORMAL]
296
297         if self.need_redraw:
298             self.draw_buf()
299
300         self.window.draw_drawable(self.bg, self.pixbuf, 0, 0, 0, 0,
301                                          self.width, self.height)
302
303
304     def draw_buf(self):
305         self.need_redraw = False
306         if self.pixbuf == None:
307             alloc = self.get_allocation()
308             self.pixbuf = gtk.gdk.Pixmap(self.window, alloc.width, alloc.height)
309             self.width = alloc.width
310             self.height = alloc.height
311         self.pixbuf.draw_rectangle(self.bg, True, 0, 0,
312                                    self.width, self.height)
313
314         if not self.names:
315             return
316         
317         lines = int(self.height / self.lineheight)
318         entries = self.names()
319         # probably place current entry in the middle
320         top = self.pos - lines / 2
321         if top < 0:
322             top = 0
323         # but try not to leave blank space at the end
324         if top + lines > entries:
325             top = entries - lines
326         # but never have blank space at the top
327         if top < 0:
328             top = 0
329         self.top = top
330         offsets = [0]
331
332         for l in range(lines):
333             
334             (type, name, other) = self.names(top+l)
335             #print type, name, other
336             if type == "end":
337                 break
338
339             offset = offsets[-1]
340             layout = self.create_pango_layout("")
341             layout.set_markup(name)
342             (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
343             if ew > self.width:
344                 # never truncate the start
345                 ew = self.width
346
347             height = self.lineheight
348             #print eh, height
349             if eh > height:
350                 height = eh
351
352             if l == self.pos - top:
353                 self.pixbuf.draw_rectangle(self.colours['selected'], True,
354                                            0+2, offset,
355                                            self.width-4, height)
356             self.pixbuf.draw_layout(self.colours[type],
357                                     (self.width-ew)/2,
358                                     offset + (height-eh)/2,
359                                     layout)
360             offsets.append(offset + height)
361         self.offsets = offsets
362
363     def refresh(self):
364         #print "refresh"
365         self.need_redraw = True
366         self.queue_draw()
367         # must return False so that timers don't refire
368         return False
369
370     def press(self,w,ev):
371         if not self.offsets:
372             return
373         row = len(self.offsets)
374         for i in range(len(self.offsets)):
375             if ev.y < self.offsets[i]:
376                 row = i-1
377                 break
378
379         if self.pos != row + self.top:
380             self.pos = row + self.top
381         if self.on_select:
382             self.on_select(self.pos)
383                 
384         self.refresh()
385         
386     def release(self,w,ev):
387         pass
388
389
390 class Task:
391     """Identifies a particular task that is a member of a folder.
392        If the task is running, the PID is tracked here too.
393
394     """
395     def __init__(self, name):
396         self.name = name
397
398     def options(self):
399         #return ["Yes", "No"]
400         return ["Yes"]
401
402     def copyconfig(self, orig):
403         pass
404
405     def setgroup(self, group, pos):
406         self.group = group
407         self.pos = pos
408
409
410     def refresh(self, select):
411         global window
412         if not window:
413             return
414         if select:
415             print "REFRESH", window.folder_num, window.folder_pos[window.folder_num], self.group, self.pos
416             if window.folder_num != self.group:
417                 window.folder_select(self.group)
418                 window.task_select(self.pos)
419             elif window.folder_pos[window.folder_num] != self.pos:
420                 window.task_select(self.pos)
421             window.active = False
422             window.activate()
423
424         gobject.timeout_add(300, window.refresh)
425
426     def set_tasks(self, list, pos):
427         global window
428         window.set_tasks(list, pos)
429
430 class CmdTask(Task):
431     """
432     Task subtype for handling normal commands
433     """
434     def __init__(self, line):
435         fields = line.split(',')
436         name = fields[0]
437         Task.__init__(self, name)
438         self.job = None
439         self.win_id = None
440         self.cmd = None
441         self.winname = None
442
443         if len(fields) > 1:
444             self.cmd = fields[1]
445         if len(fields) > 2:
446             self.winname = fields[2].strip()
447
448         global windowlist
449         def gotit(winid = None):
450             if winid:
451                 self.win_id = winid
452             return True
453             
454         windowlist.register(None, self.winname, gotit)
455
456     def options(self):
457         if self.win_id:
458             return ["Raise", "Close"]
459         if self.job:
460             return ["Kill"]
461         if self.cmd:
462             return ["Run"]
463         return ["--"]
464
465     def event(self, num):
466         if self.win_id:
467             if num == 0:
468                 try:
469                     self.win_id.raise_win()
470                 except:
471                     self.win_id = None
472
473                 return True
474             if num == 1:
475                 self.win_id.close_win()
476
477         elif self.job:
478             if num == 0:
479                 os.kill(self.job.pid, 15)
480                 self.job.poll()
481                 gobject.timeout_add(400,
482                                     lambda *a :(JobCtrl(None).poll_all(),window.refresh()))
483         elif self.cmd:
484             global windowlist
485             self.job = JobCtrl(self.cmd, self.finished)
486             windowlist.register(self.job.pid, self.winname, self.winfound)
487             print "registered ", self.job.pid, " for ", self.name
488             return True
489         return False
490
491     def winfound(self, winid = None):
492         if winid != None:
493             #print "task",self.name,"now has winid", winid
494             self.win_id = winid
495         return self.job != None
496
497     def finished(self, retcode):
498         self.job = None
499
500     def info(self):
501         if self.job:
502             self.job.poll()
503         if self.win_id or self.job:
504             typ = 'active'
505         elif self.cmd:
506             typ = 'cmd'
507         else:
508             typ = 'void'
509         return (typ, self.name, self)
510
511     def copyconfig(self, orig):
512         self.cmd = orig.cmd
513         self.winname = orig.winname
514
515
516 class WmTask(Task):
517     """
518     This is a fake task that simply holds the name of a window
519     not to show -- i.e. the parsed info from the config file
520     """
521     def __init__(self, line):
522         self.name = line[1:]
523
524 class WinTask(Task):
525     """
526     Task subtype for handling Windows that have been found
527     """
528     def __init__(self, winid, desk, pid, host, name):
529         Task.__init__(self, name)
530         self.win_id = winid
531         self.pid = int(pid)
532
533     def options(self):
534         if self.pid > 0:
535             return ["Raise", "Close", "Kill"]
536         else:
537             return ["Raise", "Close"]
538
539     def event(self, num):
540         if num == 0:
541             self.win_id.raise_win()
542             return True
543         if num == 1:
544             self.win_id.close_win()
545         if num == 2:
546             os.kill(self.pid, 15)
547         return False
548
549     def info(self):
550         return ('window', self.name, self)
551
552
553 class InternTask(Task):
554     """
555     An InternTask runs an internal command to choose text to display
556     It may be inactive, so options returns an empty list.
557     If the name contains a dot, we import the module and just call the
558     named function.  If not, we run internal_$name.
559     The function takes two argument.  A string:
560        "_name" - return (type, name, self)
561        "_options" - return list of button strings
562        button-name - take appropriate action
563     and this InternTask object.  The 'state' array may be manipulated.
564     """
565
566     def __init__(self, line, tag = None):
567         self.state = {}
568         self.fn = None
569         f = line.split('(')
570         p = f[0].split('.')
571         if not p[0]:
572             return
573
574         self.tag = tag
575         self.name = p[-1]
576         if p[0] != f[0]:
577             #try:
578             exec "import " +  p[0]
579             #except:
580             #    self.fn = None
581             #    return
582             self.fn = eval(line)
583         else:
584             self.fn = eval("internal_" + line)
585
586     def info(self):
587         global current_input
588         self.current_input = current_input
589         if self.tag:
590             t,n = 'cmd', self.tag
591         else:
592             t,n = self.fn("_name", self)
593         return (t,n,self)
594
595     def options(self):
596         global current_input
597         self.current_input = current_input
598         self.optionlist = self.fn("_options", self)
599         return self.optionlist
600
601     def event(self, num):
602         global current_input
603         self.current_input = current_input
604         return self.fn(num, self)
605     
606 class Tasks:
607     """Holds a number of folders, each with a number of tasks
608
609        Tasks(filename)  loads from a file (or directory???)
610        reload()    re-reads the file and makes changes as needed
611        folders() - array of folder names
612        tasks(folder) - array of Task objects
613        
614     """
615     def __init__(self, path):
616         self.path = path
617         self.tasks = {}; self.gtype = {}
618         self.reload()
619
620     def reload(self):
621         self.orig_tasks = self.tasks
622         self.orig_types = self.gtype
623         self.folders = []
624         self.tasks = {}
625         self.gtype = {}
626         group = "UnSorted"
627         try:
628             f = open(self.path)
629         except:
630             f = ["[Built-in]", "Exit,(quit),"]
631         for line in f:
632             l = line.strip()
633             if not l:
634                 continue
635
636             if l[0] == '"' or l[0] == '[':
637                 l = l.strip('"[],')
638                 f = l.split('/', 1)
639                 group = f[0]
640                 if len(f) > 1:
641                     group_type = f[1]
642                 else:
643                     group_type = 'cmd'
644                 if not group:
645                     group = 'UnSorted'
646                 if group_type not in ['cmd','wm']:
647                     group_type = 'cmd'
648             else:
649                 if group_type == 'cmd':
650                     words = l.split(',',1)
651                     word1 = words[1].strip(' ')
652                     arg0 = word1.split(' ',1)[0]
653                     if arg0[0] == '(':
654                         t = InternTask(word1.strip('()'), words[0])
655                     elif '(' in arg0:
656                         t = InternTask(word1, words[0])
657                     else:
658                         t = CmdTask(l)
659                 elif group_type == 'wm':
660                     t = WmTask(l)
661                 if not t:
662                     continue
663                 if group not in self.tasks:
664                     self.folders.append(group)
665                     self.tasks[group] = []
666                     self.gtype[group] = group_type
667                 if group in self.orig_tasks and \
668                    self.orig_types[group] == self.gtype[group]:
669                     for ot in self.orig_tasks[group]:
670                         if t.name == ot.name:
671                             ot.copyconfig(t)
672                             t = ot
673                             break
674                 self.tasks[group].append(t)
675                 t.setgroup(len(self.folders)-1, len(self.tasks[group])-1)
676         self.orig_tasks = None
677         self.orig_types = None
678
679
680     def folder_list(self):
681         return self.get_folder
682     def get_folder(self, ind = -1):
683         if ind == -1:
684             return len(self.folders)
685         elif ind < len(self.folders):
686             return ("folder", self.folders[ind], None)
687         else:
688             return ("end", "end", None)
689
690     def task_list(self, num):
691         global windowlist
692         gtype = self.gtype[self.folders[num]]
693         if gtype == "wm":
694             return lambda ind = -1 : self.get_task(ind, windowlist.reload())
695
696         return lambda ind = -1 : self.get_task(ind, self.tasks[self.folders[num]])
697     def get_task(self, ind, tl):
698         if tl == None:
699             tl = []
700         if ind == -1:
701             return len(tl)
702         elif ind < len(tl):
703             return tl[ind].info()
704         else:
705             return ("end", None, None)
706
707 cliptargets = [ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ]
708 class LaunchWindow(gtk.Window):
709     """
710     A window containing a list of folders and a list of entries in the folder
711     Along the bottom are per-entry buttons.
712     When a folder is selected, the entires are updated.  The last-used entry
713         in the folder is selected.
714     When an entry is selected, the buttons are updated.
715     When a button is pressed, its action is effected.
716
717     One type of action can produce text output.  In this case a replacement
718     display pane is used that is finger-scrollable.  It comes with a button
719     to revert to main display
720     """
721
722     def __init__(self, tasks):
723         gtk.Window.__init__(self)
724         self.connect("destroy", self.close_application)
725         self.set_title("Launcher")
726         self.tasks = tasks
727         self.active = False
728
729         self.create_ui()
730
731         self.clip = gtk.Clipboard(selection='PRIMARY')
732         self.cliptext = ''
733                                                          
734
735         self.folder_list = tasks.folder_list()
736         self.folder_pos = self.folder_list() * [0]
737         self.col1.set_list(self.folder_list)
738         self.set_default_size(480, 640)
739         self.show()
740
741     def create_ui(self):
742         v1 = gtk.VBox()
743         self.add(v1)
744         v1.show()
745
746         v = gtk.VBox()
747         v1.add(v)
748         v.show()
749
750         v.set_property('can-focus', True)
751         v.grab_focus()
752         v.add_events(gtk.gdk.KEY_PRESS_MASK)
753         v.connect('key_press_event', self.keystroke)
754         self.v = v
755
756         e = gtk.Entry()
757         v.pack_start(e, expand=False);
758         e.set_alignment(0.5)
759         e.connect('changed', self.entry_changed)
760         e.connect('backspace', self.entry_changed)
761         e.show()
762         self.entry = e
763         self.entry.set_text("")
764
765         h = gtk.HBox()
766         v.pack_start(h, expand=True, fill=True)
767         h.show()
768
769         self.col1 = Selector()
770         self.col2 = Selector(center=True)
771         self.col1.show()
772         self.col2.show()
773         h.pack_start(self.col1)
774         h.pack_end(self.col2)
775
776         self.col1.on_select = self.folder_select
777         self.col2.on_select = self.task_select
778
779         self.col1.assign_colour('folder','darkblue')
780         self.col1.assign_colour('selected','white')
781
782         self.col2.assign_colour('active','blue')
783         self.col2.assign_colour('cmd','black')
784         self.col2.assign_colour('selected','white')
785         self.col2.assign_colour('window','blue')
786         
787
788         h = gtk.HBox()
789         v.pack_end(h, expand=False)
790         h.set_size_request(-1,80)
791         h.show()
792
793         ctx = self.get_pango_context()
794         fd = ctx.get_font_description()
795         fd.set_absolute_size(30*pango.SCALE)
796
797         self.buttons = []
798         self.button_names = []
799         for bn in range(3):
800             b = gtk.Button("?")
801             b.child.modify_font(fd)
802             b.set_property('can-focus', False)
803             h.add(b)
804             b.connect('clicked', self.button_pressed, bn)
805             self.buttons.append(b)
806
807         fd.set_absolute_size(40*pango.SCALE)
808         self.entry.modify_font(fd)
809
810         self.main_view = v
811
812         # Now create alternate view with FingerScroll and a button
813         v = gtk.VBox()
814         v1.add(v)
815         f = FingerScroll(); f.show()
816         v.add(f)
817         self.text_buffer = f.get_buffer()
818         b = gtk.Button("Done")
819         fd.set_absolute_size(30*pango.SCALE)
820         b.child.modify_font(fd)
821         b.set_property('can-focus', False)
822         b.connect('clicked', self.text_done)
823         v.pack_end(b, expand=False)
824
825         fd = pango.FontDescription('Monospace 10')
826         fd.set_absolute_size(15*pango.SCALE)
827         f.modify_font(fd)
828         self.text_view = v
829         b.show()
830
831
832     def text_done(self,widget):
833         self.text_view.hide()
834         self.main_view.show()
835
836     def close_application(self, widget):
837         gtk.main_quit()
838
839     def checkclip(self):
840         cl = self.clip.wait_for_text()
841         if cl == self.cliptext:
842             return False
843         self.cliptext = cl
844         if type(cl) != str:
845             return False
846
847         while len(cl) > 0 and  ord(cl[-1]) >= 127:
848             cl = cl[0:-1]
849         if re.match('^ *\+?[-0-9 ()\n]*$', cl):
850             # looks like a phone number. Remove rubbish.
851             cl = cl.replace('-', '')
852             cl = cl.replace('(', '')
853             cl = cl.replace(')', '')
854             cl = cl.replace(' ', '')
855             cl = cl.replace('\n', '')
856
857         if len(self.entry.get_text()) == 0:
858             self.entry.set_text(cl)
859         else:
860             self.entry.insert_text(cl, self.entry.get_position())
861         self.entry.set_position(self.entry.get_position() +
862                                 len(cl))
863         return False
864
865     def entry_changed(self, widget):
866         if not widget.get_text():
867             #widget.hide()
868             self.v.grab_focus()
869         else:
870             widget.show()
871             if not widget.is_focus():
872                 widget.grab_focus()
873         global current_input
874         current_input = widget.get_text()
875         self.col2.refresh()
876         self.task_select(self.col2.pos)
877
878     def keystroke(self, widget, ev):
879         if not widget.is_focus():
880             return
881         if not ev.string:
882             # some weird control key - or AUX
883             return
884         self.entry.show()
885         self.entry.grab_focus()
886         self.entry.event(ev)
887
888     def button_pressed(self, widget, num):
889         hide = self.task.event(num)
890         self.folder_select(self.folder_num)
891         if hide:
892             self.active = False
893
894     def set_tasks(self, lister, posn, folder_num = -1):
895         self.folder_num = folder_num
896         self.get_task = lister
897         self.col2.set_list(lister, posn)
898
899     def folder_select(self, folder_num):
900         if folder_num < 0:
901             self.col1.refresh()
902             self.col2.refresh()
903             return
904         if folder_num < 0 or  folder_num >= self.folder_list():
905             return
906         self.col1.pos = folder_num
907         self.col1.refresh()
908         self.set_tasks(self.tasks.task_list(folder_num),
909                        self.folder_pos[folder_num],
910                        folder_num)
911
912     def task_select(self, task_num):
913         if task_num >= self.get_task():
914             return
915         if self.folder_num >= 0:
916             self.folder_pos[self.folder_num] = task_num
917         (typ, name, self.task) = self.get_task(task_num)
918         if self.task == None:
919             print "folder %d task %d" %(self.folder_num, task_num)
920             # FIXME how does this happen? what do I do with buttons?
921             # This can happen if we remember and old task number
922             # which (For window list) no longer exists.
923             # Fixed now I think
924             return
925         options = self.task.options()
926         while len(options) < len(self.button_names):
927             self.button_names.pop()
928             self.buttons[len(self.button_names)].hide()
929         for i in range(len(self.button_names)):
930             if options[i] != self.button_names[i]:
931                 self.button_names[i] = options[i]
932                 self.buttons[i].child.set_text(self.button_names[i])
933         while len(options) > len(self.button_names):
934             p = len(self.button_names)
935             self.button_names.append(options[p])
936             self.buttons[p].child.set_text(self.button_names[p])
937             self.buttons[p].show()
938
939     def activate(self):
940         #self.maximize()
941         self.text_done(None)
942         self.refresh()
943         self.present()
944         gobject.idle_add(self.checkclip)
945         if self.active:
946             self.col1.set_list(self.folder_list, 0)
947         self.active = True
948
949     def refresh(self):
950         self.folder_select(self.folder_num)
951         return False
952
953 class LaunchIcon(gtk.StatusIcon):
954     def __init__(self):
955         gtk.StatusIcon.__init__(self)
956         self.set_from_stock(gtk.STOCK_EXECUTE)
957         self.connect('activate', activate)
958         
959 window = None
960 def activate(*a):
961     global window
962
963     JobCtrl(None).poll_all()
964     window.activate()
965
966 down_at = 0
967 def aux_activate(cnt, type, code, value, msec):
968     if type != 1:
969         # not a key press
970         return
971     if code != 169 and code != 116:
972         # not the AUX key and not the power key
973         return
974     global down_at
975     if value == 1:
976         # down press
977         down_at = msec
978         #print "down_at", down_at
979         return
980     if value == 0:
981         #print "up at", msec, down_at
982         if msec - down_at > 250:
983             # too long - someone else wants this press
984             return
985         activate()
986
987 last_tap = 0
988 def tap_check(cnt, type, code, value, msec):
989     global last_tap
990     if type != 1:
991         # not a key press
992         return
993     if code != 307:
994         # not BtnX
995         return
996     if value != 1:
997         # not a down press
998         return
999     # hack - only require one tap
1000     last_tap = msec - 1
1001     
1002     if msec - last_tap < 200:
1003         # two taps
1004         last_tap = msec - 400
1005         global window
1006         if window.active:
1007             window.entry.delete_text(0,-1)
1008         activate()
1009     else:
1010         last_tap = msec
1011
1012 def internal_quit(arg, obj):
1013     global window
1014     if arg == "_name":
1015         return ('cmd', 'Exit')
1016     if arg == "_options":
1017         return ['quit']
1018     if arg == 0:
1019         window.close_application(None)
1020
1021 def internal_time(arg, obj):
1022     global window
1023     if arg == "_name":
1024         if 'next' not in obj.state:
1025             obj.state['next'] = 0
1026         now = time.time()
1027         next_minute = int(now/60)+1
1028         if next_minute != obj.state['next']:
1029             gobject.timeout_add(int (((next_minute*60) - now) * 1000),
1030                                 lambda *a :(window.refresh()))
1031             obj.state['next'] = next_minute
1032         tm = time.strftime("%H:%M", time.localtime(now))
1033         return ('cmd', '<span size="15000">'+tm+'</span>')
1034     if arg == "_options":
1035         return ['Set Timezone', 'wifi']
1036     if arg == 0:
1037         window.set_tasks(tasklist_tz(), 0)
1038     if arg == 1:
1039         window.set_tasks(tasklist_wifi(), 0)
1040     return None
1041
1042 def internal_date(arg, obj):
1043     if len(obj.state) == 0:
1044         obj.state['cmd'] = CmdTask('cal,/usr/local/bin/cal,cal')
1045     if arg == "_name":
1046         # no need to schedule a timeout as the 1-minute tick will do it.
1047
1048         #tm = time.strftime('<span size="5000">%d-%b-%Y</span>', time.localtime(time.time()))
1049         tm = time.strftime('%d-%b-%Y', time.localtime(time.time()))
1050         return ('cmd', tm)
1051     if arg == '_options':
1052         return obj.state['cmd'].options()
1053     return obj.state['cmd'].event(arg)
1054
1055 def internal_tz(zone):
1056     return lambda arg, obj: _internal_tz(arg, obj, zone)
1057
1058 def _internal_tz(arg, obj, zone):
1059     if arg == '_name':
1060         if 'TZ' in os.environ:
1061             TZ = os.environ['TZ']
1062         else:
1063             TZ = None
1064         os.environ['TZ'] = zone
1065         time.tzset()
1066         now = time.time()
1067         tm = time.strftime("%d-%b-%Y %H:%M", time.localtime(now))
1068
1069         if TZ:
1070             os.environ['TZ'] = TZ
1071         else:
1072             del(os.environ['TZ'])
1073         return ('cmd', '<span size="10000">'+tm+"\n"+zone+'</span>')
1074     if arg == '_options':
1075         return []
1076     return None
1077
1078 def internal_echo(arg, obj):
1079     if arg == '_name':
1080         global current_input
1081         a = current_input
1082         a = a.replace('&','&amp;')
1083         a = a.replace('<','&lt;')
1084         a = a.replace('>','&gt;')
1085         return ('cmd', a)
1086     if arg == '_options':
1087         return []
1088     return None
1089
1090 def internal_calc(arg, obj):
1091     if arg == '_name':
1092         global current_input
1093         try:
1094             n = eval(current_input)
1095             a = '=' + str(n)
1096         except:
1097             if current_input:
1098                 a = '= ?'
1099             else:
1100                 a = ''
1101         a = a.replace('&','&amp;')
1102         a = a.replace('<','&lt;')
1103         a = a.replace('>','&gt;')
1104         return ('cmd', a)
1105     if arg == '_options':
1106         return []
1107     return None
1108
1109 def internal_rotate(arg, obj):
1110     if arg == '_name':
1111         return ('cmd', 'rotate')
1112     if arg == '_options':
1113         return ['normal','left']
1114     if arg == 0:
1115         Popen(['xrandr', '-o', 'normal'], shell=False, close_fds = True)
1116         return
1117     if arg == 1:
1118         Popen(['xrandr', '-o', 'left'], shell=False, close_fds = True)
1119         return
1120
1121 def internal_text(cmd):
1122     return lambda arg, obj : _internal_text(arg, cmd, obj)
1123
1124 def readsome(f, dir, p, b):
1125     l = f.read()
1126     b.insert(b.get_end_iter(), l)
1127     if l == "":
1128         return False
1129     return True
1130
1131 def child_done(pid, status, arg):
1132     (p, b, w) = arg
1133     fcntl.fcntl(p.stdout, fcntl.F_SETFL, 0)
1134     while readsome(p.stdout, None, p, b):
1135         pass
1136     gobject.source_remove(w)
1137     b.insert(b.get_end_iter(), "-----//-----")
1138     p.stdout.close()
1139
1140 def _internal_text(arg, cmd, obj):
1141     if arg == '_name':
1142         return ('cmd', cmd)
1143     if arg == '_options':
1144         return ['view']
1145     if arg == 0:
1146         global window
1147         b = window.text_buffer
1148         b.delete(b.get_start_iter(),b.get_end_iter())
1149         p = Popen(cmd, shell=True, close_fds = True, stdout=PIPE)
1150         flg = fcntl.fcntl(p.stdout, fcntl.F_GETFL, 0)
1151         fcntl.fcntl(p.stdout, fcntl.F_SETFL, flg | os.O_NONBLOCK)
1152         watch = gobject.io_add_watch(p.stdout, gobject.IO_IN, readsome, p, b)
1153         gobject.child_watch_add(p.pid, child_done, ( p, b, watch ))
1154         window.text_view.show()
1155         window.main_view.hide()
1156
1157 def internal_file(fname):
1158     # return a function to be used as an internal_* function
1159     # that reads the content of a file
1160     return lambda arg, obj :  _internal_file(arg, fname, obj)
1161
1162 def _internal_file(arg, fname, obj):
1163     if 'dndir' not in obj.state:
1164         try:
1165             d = dnotify.dir(os.path.dirname(fname))
1166             obj.state['dndir'] = d
1167             obj.state['pending'] = False
1168         except OSError:
1169             obj.state['pending'] = True
1170             obj.state['value'] = '--'
1171     if arg == '_name':
1172         if not obj.state['pending']:
1173             try:
1174                 obj.state['dndir'].watch(os.path.basename(fname),
1175                                          lambda f : _internal_file_notify(f, obj))
1176                 obj.state['pending'] = True
1177             
1178                 f = open(fname)
1179                 l = f.readline().strip()
1180                 f.close()
1181                 obj.state['value'] = l
1182             except OSError:
1183                 obj.state['value'] = '--'
1184                 l = '--'
1185         else:
1186             l = obj.state['value']
1187         return ('cmd', l)
1188     if arg == '_options':
1189         return []
1190     return None
1191
1192 def _internal_file_notify(f, obj):
1193     global window
1194     obj.state['pending'] = False
1195     f.cancel()
1196     # wait a while for changes to the file to stablise
1197     gobject.timeout_add(300, window.refresh)
1198
1199 def get_task(ind, tl):
1200     if tl == None:
1201         tl = []
1202     if ind == -1:
1203         return len(tl)
1204     elif ind < len(tl):
1205         return tl[ind].info()
1206     else:
1207         return ("end", None, None)
1208
1209 def internal_windows(arg, obj):
1210     if arg == '_name':
1211         return "Window List"
1212     if arg == '_options':
1213         return ['open']
1214     if arg == 0:
1215         global windowlist, window
1216         window.set_tasks(lambda ind = -1 : get_task(ind, windowlist.reload()), 0)
1217
1218 class tasklist:
1219     def __init__(self):
1220         self.last_refresh = 0
1221         self.list = []
1222         self.newlist = []
1223         self.refresh_time = 60
1224         self.callback = None
1225         self.refresh_task = 'refresh_list'
1226         self.name = 'Generic List'
1227         
1228     def __call__(self, ind = -1):
1229         if ind <= -1:
1230             if self.last_refresh + self.refresh_time < time.time():
1231                 self.last_refresh = time.time()
1232                 self.start_refresh()
1233             return len(self.list) + 1
1234         if ind == 0:
1235             # The first entry is a simple refresh task
1236             t = InternTask(self.refresh_task, self.name)
1237             t.state['list'] = self
1238             self.callback = t
1239             return t.info()
1240         if ind <= len(self.list):
1241             return tasklist_task(self, ind-1).info()
1242         return ("end", None, None)
1243
1244     def refresh_cmd(self, cmd):
1245         p = Popen(cmd, shell=True, close_fds=True, stdout=PIPE)
1246         fcntl.fcntl(p.stdout, fcntl.F_SETFL, os.O_NONBLOCK)
1247         watch = gobject.io_add_watch(p.stdout, gobject.IO_IN, self.readsome, p)
1248         gobject.child_watch_add(p.pid, self.child_done, (p, watch))
1249         
1250     def readsome(self, f, dir, p):
1251         l = f.readline()
1252         if l != "" :
1253             self.readline(l.strip())
1254             return True
1255         return False
1256
1257     def child_done(self, pid, status, arg):
1258         (p, watch) = arg
1259         fcntl.fcntl(p.stdout, fcntl.F_SETFL, 0)
1260         while self.readsome(p.stdout, None, p):
1261             pass
1262         gobject.source_remove(watch)
1263         p.stdout.close()
1264         self.readline(None)
1265         self.list = self.newlist
1266         self.newlist = []
1267         if self.callback:
1268             self.callback.refresh(False)
1269
1270 class tasklist_task(Task):
1271     """
1272     A tasklist_task calls into the tasklist to get info required.
1273     """
1274     def __init__(self, tasklist, entry):
1275         self.list = tasklist
1276         self.entry = entry
1277     def info(self):
1278         t,n = self.list.info(self.entry)
1279         return (t,n,self)
1280     def options(self):
1281         return self.list.options(self.entry)
1282     def event(self, num):
1283         return self.list.event(self.entry, num)
1284
1285 class tasklist_tz(tasklist):
1286     # Synthesise a list of tasks to represent selection a time zone
1287     # ind==-1 must return the length of the list, other values return tasks
1288     # We can call window.set_folder (or something) to get the list refreshed
1289     # First item is 'TimeZone' with a button to refresh the list
1290     # other items are best 10 timezones.
1291     # We refresh the list when the refresh button is pressed, or when
1292     # len is requested move than 10 minutes after the last refresh.
1293
1294     def __init__(self):
1295         tasklist.__init__(self)
1296         self.refresh_time = 10*60
1297         self.name = 'TimeZone'
1298
1299     def start_refresh(self):
1300         self.refresh_cmd("/root/gpstz --list")
1301         
1302     def readline(self, l):
1303         if l == None:
1304             return
1305         words = l.split()
1306         self.newlist.append(words[1])
1307
1308     def info(self, n):
1309         return 'cmd', self.list[n]
1310     def options(self, n):
1311         return ['Set Timezone']
1312     def event(self, n, ev):
1313         if ev == 0:
1314             Popen("/root/gpstz "+ self.list[n], shell=True, close_fds=True)
1315     
1316
1317
1318 def internal_refresh_list(arg, obj):
1319     if arg == '_name':
1320         return "Refresh List"
1321     if arg == '_options':
1322         return ['Refresh']
1323     if arg == 0:
1324         t = obj.state['list']
1325         t.start_refresh()
1326     return None
1327
1328
1329 class tasklist_wifi(tasklist):
1330     def __init__(self):
1331         tasklist.__init__(self)
1332         self.refresh_time = 60
1333         self.name = 'Wifi Networks'
1334
1335     def start_refresh(self):
1336         self.essid = None
1337         self.encrypt = None
1338         self.quality = None
1339         self.refresh_cmd("iwlist eth0 scanning")
1340
1341     def readline(self, l):
1342         if l == None:
1343             self.read_finished()
1344             return
1345         w = l.split()
1346         if len(w) == 0:
1347             return
1348         if w[0] == 'Cell':
1349             self.read_finished()
1350             return
1351         w0 = w[0]
1352         w = l.split(':')
1353         if w[0] == "ESSID":
1354             id = w[1]
1355             self.essid = id.strip('"')
1356             return
1357         if w[0] == 'Encryption key':
1358             self.encrypt = (w[1] == 'on')
1359             return
1360         w = w0.split('=')
1361         if w[0] == 'Quality':
1362             self.quality = w[1]
1363             return
1364
1365     def read_finished(self):
1366         if self.essid == None:
1367             self.encrypt = None
1368             self.quality = None
1369             return
1370         if self.quality == None:
1371             self.quality = "0"
1372         c = ''
1373         if self.encrypt:
1374             c = ' XX'
1375         self.newlist.append((self.essid, self.quality, c))
1376
1377     def info(self, n):
1378         essid, quality, c = self.list[n]
1379         return 'cmd', ('<span size="15000">%s</span>\n<span size="10000">%s%s</span>'
1380                        % (essid, quality, c))
1381     def options(self, n):
1382         return ['Configure Wifi']
1383     def event(self, n, ev):
1384         print "please configure %s"% self.list[n][0]
1385
1386 def main(args):
1387     global window, windowlist, tasks
1388     global current_input
1389     current_input = ''
1390     windowlist = WinList()
1391     tasks = Tasks(os.getenv('HOME') + "/.launchrc")
1392     i = LaunchIcon()
1393     window = LaunchWindow(tasks)
1394     try:
1395         aux = EvDev("/dev/input/event4", aux_activate)
1396         # may aux button broke so ... 
1397         EvDev("/dev/input/event0", aux_activate)
1398     except:
1399         aux = None
1400     try:
1401         EvDev("/dev/input/event3", tap_check)
1402     except:
1403         pass
1404
1405     gtk.settings_get_default().set_long_property("gtk-cursor-blink", 0, "main")
1406
1407     gtk.main()
1408
1409 if __name__ == '__main__':
1410     sys.exit(main(sys.argv))
1411