Rework pseudo-transparency by changing the background grabbing mechanism
[adesklets.git] / utils / adesklets_installer
blob05d5c9da32eed578f6aa809969241fe58f4223fe
1 #!/usr/bin/env python
2 """
3 -------------------------------------------------------------------------------
4 Copyright (C) 2005, 2006 Sylvain Fourmanoit
6 Some ideas were taken from a initial coding effort by Cameron Daniel
7 <me@camdaniel.com>.
9 Released under the GPL, version 2.
11 Permission is hereby granted, free of charge, to any person obtaining a copy
12 of this software and associated documentation files (the "Software"), to
13 deal in the Software without restriction, including without limitation the
14 rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
15 sell copies of the Software, and to permit persons to whom the Software is
16 furnished to do so, subject to the following conditions:
18 The above copyright notice and this permission notice shall be included in
19 all copies of the Software and its documentation and acknowledgment shall be
20 given in the documentation and software packages that this Software was
21 used.
23 THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
26 THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
27 IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
28 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29 -------------------------------------------------------------------------------
30 This is the adesklet's installer script. Have a look at adesklets
31 documentation for details.
32 """
33 # -----------------------------------------------------------------------------
34 # Imports
36 import sys, traceback
38 try:
39 import threading
40 except ImportError:
41 print 'Threading support is REQUIRED by adesklets_installer'
42 sys.exist(1)
44 import os, os.path, re, tarfile, md5, StringIO, cStringIO, copy, time
45 from urllib import urlopen
46 from xml.dom.minidom import parseString
48 # -----------------------------------------------------------------------------
49 class Driver(threading.Thread):
50 """
51 Driver embed the installer core logic into a single, reusable
52 monothreaded component. It will be reused here by GUI-derivated classes.
54 I am not a big fan of multithreaded applications, but this is needed
55 for clean integration with most widgets toolkits these days.
56 """
57 class Desklets(dict):
58 def __init__(self, logfile):
59 self.log=logfile
60 self.path = os.path.join(os.getenv('HOME',''), '.desklets')
61 self.ready = threading.Event()
62 self.ready.clear()
64 def descriptions(self):
65 """
66 Returns a preformatted desklets description, suitable
67 for monospace printing., in desklets name alphabetic order.
68 """
69 desc = ['%15s %5s (%s)' % (k, v['version'], v['status'])
70 for k, v in self.iteritems()]
71 desc.sort(lambda x, y: cmp(x.strip().lower(), y.strip().lower()))
72 return desc
74 def run(self):
75 def extract_tag(entry):
76 def extract_text(tag):
77 if not tag.hasChildNodes():
78 tag = tag.getAttributeNode('href')
79 return tag.firstChild.wholeText.encode('ascii')
80 return [extract_text(entry.getElementsByTagName(tag)[0])
81 for tag in ('title', 'link', 'generator')]
82 def extract_url(link):
83 if 'prdownloads.sourceforge.net' in link:
84 return ('http://dl.sourceforge.net/sourceforge/' +
85 'adesklets/%s' %
86 re.findall('adesklets/(.+)\?download', link)[0])
87 else:
88 return link
89 def by_version(x, y):
90 def version(ver):
91 return sum([int(v)*100**(2-i)
92 for v, i in zip(ver.split('.'), xrange(3))])
93 return version(x[1])-version(y[1])
95 # Fetch and organize the data
97 print >> self.log, 'Retrieving data online...',
98 self.log.flush()
99 dict.__init__(self,
100 [(title.split()[0],
101 dict(([('version', title.split()[1])] +
102 [('url', extract_url(link))] +
103 [tuple(generator.split(':'))])))
104 for title, link, generator in
105 [extract_tag(entry)
106 for entry in parseString(
107 urlopen('http://adesklets.sourceforge.net/desklets.atom').read()
108 ).getElementsByTagName('entry')]])
109 print >> self.log, 'OK'
111 # Compare to what's installed, and set the status
113 print >> self.log, 'Checking locally installed desklets...',
114 self.log.flush()
115 if os.path.isdir(self.path):
116 inst = [item.split('-')
117 for item in os.listdir(self.path)
118 if os.path.isdir(os.path.join(self.path, item))]
119 inst.sort(by_version)
120 inst = dict(inst)
121 else:
122 inst = {}
124 for desklet in self:
125 if desklet in inst:
126 if inst[desklet] == self[desklet]['version']:
127 self[desklet]['status']='installed'
128 else:
129 self[desklet]['status']='out of date'
130 else:
131 self[desklet]['status']='uninstalled'
132 print >> self.log, 'OK'
133 self.ready.set()
135 def install(self, desklet):
137 Perform a desklet installation
139 print >> self.log, 'Downloading %s desklet...' % desklet,
140 self.log.flush()
141 f = cStringIO.StringIO(urlopen(self[desklet]['url']).read())
142 print >> self.log, 'OK'
143 print >> self.log, 'Verifying download integrity...',
144 self.log.flush()
145 if md5.new(f.getvalue()).hexdigest() != self[desklet]['md5']:
146 raise RuntimeError('bad download checksum')
147 print >> self.log, 'OK'
148 print >> self.log, 'Opening the downloaded archive...',
149 self.log.flush()
150 tar = tarfile.open(mode='r|bz2', fileobj=f, name='archive')
151 print >> self.log, 'OK'
152 print >> self.log, 'Extracting the archive...',
153 self.log.flush()
154 if not os.path.isdir(self.path): os.mkdir(self.path)
155 for tarinfo in tar: tar.extract(tarinfo, path=self.path)
156 self[desklet]['status'] = 'installed'
157 print >> self.log, """OK
158 %(sep)s
159 desklet %(name)s version %(version)s was installed in:
160 %(path)s
161 Change to this directory, see the README,
162 and follow the instructions.
163 %(sep)s""" % {'name': desklet,
164 'version': self[desklet]['version'],
165 'path': '%s/%s-%s' % (self.path, desklet, self[desklet]['version']),
166 'sep': '='*30}
168 def __init__(self, logfile=sys.stdout):
169 self.cond = threading.Condition()
170 self.op = None
171 self.kw = None
172 self.alive = True
173 self.ready_state = False
174 self.desklets = self.Desklets(logfile)
175 threading.Thread.__init__(self)
176 def refreshed(self):
178 Whenever it returns True, the GUI instance need to
179 refresh its desklets lists (it takes care of initial load too)
181 if not self.ready_state:
182 if self.desklets.ready.isSet():
183 self.ready_state = True
184 return True
185 return False
186 def ready(self, block=False):
188 Returns True if Driver is ready to process command. It can be used
189 in non-blocking (default) or blocking mode. In blocking mode, it
190 will eventually block and wait for Driver to be ready before
191 continuing.
193 if block:
194 self.desklets.ready.wait()
195 return True
196 return self.desklets.ready.isSet()
198 def track(self):
199 """Traceback tracker"""
200 print >> self.desklets.log, \
201 '\n!!! An error occured during the operation !!!'
202 traceback.print_exc(file=self.desklets.log)
203 self.desklets.ready.set()
205 def run(self):
206 """Main thread loop"""
207 try:
208 self.desklets.run()
209 except:
210 self.track()
212 while self.alive:
213 try:
214 self.cond.acquire()
215 if not self.op: self.cond.wait()
216 op = self.op
217 kw = self.kw
218 self.op = None
219 self.kw = None
220 self.cond.release()
221 if hasattr(self, '_'+op):
222 self.desklets.ready.clear()
223 getattr(self, '_'+op)(**kw)
224 self.desklets.ready.set()
225 except:
226 self.track()
228 def command(self, op, **kw):
230 Execute a given command. For instance:
232 Driver.command('install', desklet='mydesklet')
234 NOTE: *DO NOT* call the commands directly
236 self.cond.acquire()
237 self.op = op
238 self.kw = kw
239 self.cond.notify()
240 self.cond.release()
242 def _install(self, desklet):
243 self.ready_state=False # <= Used to signal a possible
244 self.desklets.install(desklet) # refresh of desklets states
245 def _quit(self): self.alive = False
247 # -----------------------------------------------------------------------------
248 class UI(object):
250 UI is a base class for the installer interfaces.
252 class Log(StringIO.StringIO):
254 Abstracted log file
256 def __init__(self):
257 self.lock = threading.Lock()
258 self.refresh = False
259 StringIO.StringIO.__init__(self)
260 def write(self, string):
261 self.lock.acquire()
262 StringIO.StringIO.write(self, string)
263 self.refresh = True
264 self.lock.release()
265 def pool(self):
267 pool() => data OR None
269 If the logged information from the driver changed since last call,
270 It will return the full log.
272 result = None
273 if self.lock.acquire(False):
274 if self.refresh:
275 result=copy.deepcopy(self.getvalue())
276 self.refresh = False
277 self.lock.release()
278 return result
280 def __init__(self):
281 # Connect and run components
283 self.log = self.Log()
284 self.driver = Driver(self.log)
285 self.driver.start()
286 try:
287 try:
288 self.run()
289 except KeyboardInterrupt: print '(Interrupted)'
290 finally:
291 self.driver.command('quit')
292 self.driver.join()
294 def run(self):
295 """Main GUI loop: need to be overriden in children"""
296 pass
298 # -----------------------------------------------------------------------------
299 class rawGUI(UI):
300 """Very bare interface for any terminal: should always work"""
301 def Log(self): return sys.stdout
303 def run(self):
304 # Just output a list of choice, wait for the user, then repeat
305 # if the choice was not to exit in the first place.
307 answer = 0
308 while answer != -2:
309 self.driver.ready(block=True)
310 desklets = [(i, desc)
311 for (i, desc)
312 in zip(xrange(len(self.driver.desklets)),
313 self.driver.desklets.descriptions())]
314 print '%(sep)s\nAvailable desklets\n%(sep)s' % { 'sep': '-'*30 }
315 print '\n'.join(['%3d) %s' % item for item in desklets])
316 print ' q) %15s' % 'Quit'
317 answer = self.input('Enter your selection > ')
318 if dict(desklets).has_key(answer):
319 self.driver.command('install',
320 desklet=dict(desklets)[answer].split()[0])
321 time.sleep(1)
322 self.driver.ready(block=True)
323 answer = self.input('<<<Press q, to quit>>> ')
325 def input(self, prompt):
326 try:
327 val = raw_input(prompt)
328 return int(val)
329 except EOFError:
330 print '(End of File)'
331 return -2
332 except ValueError:
333 if val.strip().lower() == 'q':
334 return -2;
335 return -1
337 # -----------------------------------------------------------------------------
338 try:
339 import Tkinter
341 class _TkGUI(UI):
342 """Tk-based interface"""
343 def run(self):
344 # Initialize the various widgets
346 self.win = Tkinter.Tk()
347 self.win.resizable(False, False)
348 self.win.title('adesklets installer')
349 self.desklet = ''
350 f = Tkinter.Frame(self.win)
351 f.pack()
352 s = Tkinter.Scrollbar(f)
353 s.pack(side=Tkinter.RIGHT, fill=Tkinter.Y)
354 self.listbox = Tkinter.Listbox(f, yscrollcommand=s.set,
355 font="fixed", width=40)
356 self.listbox.pack(fill=Tkinter.Y)
357 s.config(command=self.listbox.yview)
359 f = Tkinter.Frame(self.win)
360 f.pack()
361 self.binstall = Tkinter.Button(f, \
362 text='Waiting for online description',
363 state=Tkinter.DISABLED,
364 command=self.install)
365 self.binstall.pack(side=Tkinter.LEFT)
366 w = Tkinter.Button(f, text='Quit',command=self.win.quit)
367 w.pack(side=Tkinter.RIGHT)
369 f = Tkinter.Frame(self.win)
370 f.pack(fill=Tkinter.Y)
371 s = Tkinter.Scrollbar(f)
372 s.pack(side=Tkinter.RIGHT, fill=Tkinter.Y)
373 self.logtext = Tkinter.Text(f, yscrollcommand=s.set,
374 wrap=None,width=50, height=6)
375 self.logtext.pack(fill=Tkinter.BOTH)
376 s.config(command=self.logtext.yview)
378 # Then, register a pooling callback, and start the main loop
380 self.win.after(0, self.pool)
381 self.win.mainloop()
383 def refresh_list(self):
384 self.listbox.delete(0, Tkinter.END)
385 for desc in self.driver.desklets.descriptions():
386 self.listbox.insert(Tkinter.END, desc)
387 self.binstall.config(text='Select a desklet')
389 def pool(self):
391 Periodic pooling method that refreshes the GUI
393 # Refresh the desklet list
395 if self.driver.refreshed(): self.refresh_list()
397 # Refresh the installation button (state and text)
399 s = self.listbox.curselection()
400 if len(s)>0:
401 desklet = self.listbox.get(s[0])
402 if self.desklet != desklet:
403 self.desklet = desklet
404 self.binstall.config(
405 text ='Install ' + \
406 ' '.join(self.desklet.split()[:2]))
407 if self.driver.ready():
408 self.binstall.config(state=Tkinter.ACTIVE)
410 # Refresh the log output
412 text = self.log.pool()
413 if text is not None:
414 self.logtext.config(state=Tkinter.NORMAL)
415 self.logtext.delete(1.0, Tkinter.END)
416 self.logtext.insert(Tkinter.END, text)
417 self.logtext.config(state=Tkinter.DISABLED)
419 # Call again the routine in 1/10th of a second
421 self.win.after(100, self.pool)
423 def install(self):
424 """Button installation callback"""
425 self.binstall.config(state=Tkinter.DISABLED)
426 self.driver.command('install', desklet=self.desklet.split()[0])
428 def TkGUI():
429 """Just a fallback wrapper"""
430 try:
431 return _TkGUI()
432 except Tkinter.TclError:
433 print 'Could not instanciate the Tk interface'
435 except ImportError: pass
437 # -----------------------------------------------------------------------------
438 try:
439 import curses
441 class _cursesGUI(UI):
442 """Curses-based interface"""
444 class TextZone(object):
446 Curses is fairly low-level, so let's start by defining our
447 listbox-like widget.
449 def __init__(self, *args):
450 self.h , self.w, \
451 self.sy , self.sx, \
452 self.sh , self.sw = args
453 self.hl = 0
454 self.cur = 0
455 self.lines = ['']
457 self.win = curses.newwin(self.sh-self.sy, self.w,
458 self.sy, self.sx)
459 self.win.box()
460 self.win.refresh()
461 self.pad = curses.newpad(self.h-2, self.w-2)
462 self.pad.keypad(1)
463 self.pad.nodelay(1)
465 def refresh(self, data = None):
466 """Refresh the widget content, as needed"""
467 if data is not None:
468 self.pad.erase()
469 if type(data) is type(''):
470 self.lines = data.splitlines()
471 else:
472 self.lines = data
473 for i, line in zip(range(len(self.lines)), self.lines):
474 self.pad.addstr(i, 0, line)
475 self.scroll(0)
476 else:
477 self.pad.refresh(self.cur, 0,
478 self.sy+1, self.sx+1,
479 self.sh-2, self.sw-1)
480 def status(self, status):
481 """Set the widget status"""
482 self.win.box()
483 self.win.addstr(0, 2, '[ %s ]' % status)
484 self.win.refresh()
485 self.refresh()
487 def toggle(self):
488 """Toggle widget in active/inactive mode"""
489 self.hl = (0, curses.A_REVERSE)[self.hl == 0]
490 self.scroll(0)
492 def scroll(self, delta):
493 """Perform a scroll"""
494 new = self.cur + delta
495 if new < 0:
496 new = 0
497 elif new >= len(self.lines):
498 new = len(self.lines)-1
500 if len(self.lines)>1:
501 self.pad.addstr(self.cur, 0, self.lines[self.cur])
502 self.pad.addstr(new, 0, self.lines[new], self.hl)
503 self.cur = new
504 self.refresh()
506 def pool(self):
508 Pool the keyboard: scrolling actions are self-contained,
509 selection, widget circulation and quitting are not.
511 c = self.pad.getch()
512 if c!=-1:
513 if c == curses.KEY_UP or c == ord('8'):
514 self.scroll(-1)
515 elif c == curses.KEY_DOWN or c == ord('2'):
516 self.scroll(1)
517 elif c == curses.KEY_PPAGE or c == ord('9'):
518 self.scroll((self.sy-self.sh)/2)
519 elif c == curses.KEY_NPAGE or c == ord('3'):
520 self.scroll((self.sh-self.sy)/2)
521 elif c == ord('\t'):
522 return 'next'
523 elif c == curses.KEY_ENTER or c == ord('\n'):
524 return 'select'
525 elif c == ord('q') or c == 27:
526 return 'quit'
528 def run(self):
529 curses.wrapper(self._run)
531 def _run(self, scr):
532 # Check out terminal real-estate
534 if scr.getmaxyx()[0]<20 or scr.getmaxyx()[1]<50:
535 raise curses.error('Terminal size should be at least 50x20')
536 curses.curs_set(0)
538 # Instanciate our two widgets: a desklets pad and a log pad
540 dpad = self.TextZone(100, scr.getmaxyx()[1],
541 0, 0,
542 scr.getmaxyx()[0]-10, scr.getmaxyx()[1])
543 lpad = self.TextZone(200, scr.getmaxyx()[1],
544 scr.getmaxyx()[0]-10, 0,
545 scr.getmaxyx()[0], scr.getmaxyx()[1])
547 # Start with the desklets pad active
549 dpad.toggle()
550 dpad.status('Waiting for desklets description')
551 pad = dpad
553 # Now loop indefinitively
555 while True:
556 # Refresh the desklet list
558 if self.driver.refreshed():
559 dpad.status('Ready')
560 dpad.refresh(self.driver.desklets.descriptions())
562 # Refresh the log output
564 text = self.log.pool()
565 if text is not None: lpad.refresh(text)
567 # React to keyboard...
569 action = pad.pool()
570 if action is not None:
571 # ...By changing the active TextZone
572 if action == 'next':
573 for item in (dpad, lpad): item.toggle()
574 pad = (dpad, lpad)[pad is dpad]
575 # ... By performing the installation, as needed
576 elif action == 'select' and pad is dpad:
577 desklet = pad.lines[pad.cur].split()[0]
578 if (self.driver.desklets.has_key(desklet) and
579 self.driver.ready()):
580 pad.status('Installing...')
581 self.driver.command('install', desklet=desklet)
582 #... And of course by exiting as needed
583 elif action == 'quit':
584 break
585 time.sleep(.01)
587 def cursesGUI():
588 """Just a fallback wrapper"""
589 try:
590 return _cursesGUI()
591 except curses.error:
592 print 'Could not instanciate the curses interface'
594 except ImportError: pass
596 # -----------------------------------------------------------------------------
597 if __name__ == '__main__':
599 # Parse the command line
601 from optparse import OptionParser
602 def version(option, opt, value, parser):
603 print """%s %s
604 Copyright (C) 2005, 2006 Sylvain Fourmanoit <syfou@users.sourceforge.net>
605 This is free software; see the source for copying conditions. There is NO
606 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
607 """ % (parser.get_prog_name(), os.getenv('ADESKLETS_VERSION', ''))
608 parser.exit()
610 p = OptionParser()
611 p.add_option('-v', '--version', action='callback', callback=version,
612 help='print out the version info and exit')
613 p.add_option('-r', '--raw', action='store_true', dest='raw',
614 help='force the use of the raw terminal interface ' +
615 '(always available)')
616 p.add_option('-c', '--curses', action='store_true', dest='curses',
617 help='force the use of the curses interface, when available')
618 p.add_option('-t', '--tk', action='store_true', dest='Tk',
619 help='force the use of the Tk interface, when available')
620 p.set_defaults(raw=False, curses=False, Tk=False)
621 opts, args= p.parse_args()
623 # Just instanciate the right GUI to start up the application
624 # In order, we try to instanciate the Tk, curses and raw
625 # interface, falling to the next in case of unavailability or
626 # initialization errors
628 select = opts.raw + opts.curses + opts.Tk
629 for ui in ('Tk', 'curses', 'raw'):
630 if (globals().has_key('%sGUI' % ui) and
631 (getattr(opts, ui) or select==0 or ui=='raw')):
632 if globals()['%sGUI' % ui](): break
634 # -----------------------------------------------------------------------------