2 import os
, sys
, popen2
, tempfile
, shutil
, optparse
3 import pygtk
; pygtk
.require('2.0')
6 from logging
import warn
8 from zeroinstall
.injector
.namespaces
import XMLNS_IFACE
9 from zeroinstall
.injector
.policy
import Policy
10 from zeroinstall
.injector
.iface_cache
import iface_cache
11 from zeroinstall
.support
import basedir
14 parser
= optparse
.OptionParser(usage
="usage: %prog [options] [interface]")
15 parser
.add_option("-V", "--version", help="display version information", action
='store_true')
16 (options
, args
) = parser
.parse_args()
19 print "Zero2Desktop (zero-install) " + version
20 print "Copyright (C) 2007 Thomas Leonard"
21 print "This program comes with ABSOLUTELY NO WARRANTY,"
22 print "to the extent permitted by law."
23 print "You may redistribute copies of this program"
24 print "under the terms of the GNU General Public License."
25 print "For more information about these matters, see the file named COPYING."
28 zero2desktop_uri
= "http://0install.net/2007/interfaces/Zero2Desktop.xml"
30 gladefile
= os
.path
.join(os
.path
.dirname(__file__
), 'zero2desktop.glade')
32 # XDG_UTILS should go at the end, so that the local copy is used first
33 os
.environ
['PATH'] += ':' + os
.environ
['XDG_UTILS']
35 template
= """[Desktop Entry]
36 # This file was generated by zero2desktop.
37 # See the Zero Install project for details: http://0install.net
42 Exec=0launch -- %s %%f
43 Categories=Application;%s
46 icon_template
= """Icon=%s
55 already_installed
= {}
56 for d
in basedir
.load_data_paths('applications'):
57 for desktop_file
in os
.listdir(d
):
58 if desktop_file
.startswith('zeroinstall-') and desktop_file
.endswith('.desktop'):
59 full
= os
.path
.join(d
, desktop_file
)
61 for line
in file(full
):
62 if line
.startswith('Exec='):
63 bits
= line
.split(' -- ', 1)
65 uri
= bits
[0].split(' ', 1)[1] # 0launch URI -- %u
67 uri
= bits
[1].split(' ', 1)[0].strip() # 0launch -- URI %u
68 already_installed
[uri
] = desktop_file
71 warn("Failed to find Exec line in %s", full
)
73 warn("Failed to load .desktop file %s: %s", full
, ex
)
77 tree
= gtk
.glade
.XML(gladefile
, 'main')
78 self
.window
= tree
.get_widget('main')
80 self
.set_keep_above(True)
83 text
= uri
.get_active_text()
84 self
.window
.set_response_sensitive(RESPONSE_NEXT
, bool(text
))
86 drop_uri
= tree
.get_widget('drop_uri')
87 uri
= tree
.get_widget('interface_uri')
88 about
= tree
.get_widget('about')
89 icon
= tree
.get_widget('icon')
90 category
= tree
.get_widget('category')
91 dialog_next
= tree
.get_widget('dialog_next')
92 dialog_ok
= tree
.get_widget('dialog_ok')
95 uri
.append_text(sys
.argv
[1])
97 uri
.connect('changed', set_uri_ok
)
100 category
.set_active(11)
102 for item
in sorted(already_installed
):
103 uri
.append_text(item
)
105 def uri_dropped(eb
, drag_context
, x
, y
, selection_data
, info
, timestamp
):
108 data
= codecs
.getdecoder('utf16')(selection_data
.data
)[0]
109 data
= data
.split('\n', 1)[0].strip()
111 data
= selection_data
.data
.split('\n', 1)[0].strip()
113 drag_context
.finish(True, False, timestamp
)
114 self
.window
.response(RESPONSE_NEXT
)
116 drop_uri
.drag_dest_set(gtk
.DEST_DEFAULT_MOTION | gtk
.DEST_DEFAULT_DROP | gtk
.DEST_DEFAULT_HIGHLIGHT
,
117 [('text/uri-list', 0, URI_LIST
),
118 ('text/x-moz-url', 0, UTF_16
)],
120 drop_uri
.connect('drag-data-received', uri_dropped
)
122 nb
= tree
.get_widget('notebook1')
124 def update_details_page():
125 iface
= iface_cache
.get_interface(uri
.get_active_text())
126 about
.set_text('%s - %s' % (iface
.get_name(), iface
.summary
))
127 icon_path
= iface_cache
.get_icon_path(iface
)
130 # Icon format must be PNG (to avoid attacks)
131 loader
= gtk
.gdk
.PixbufLoader('png')
133 loader
.write(file(icon_path
).read())
136 icon_pixbuf
= loader
.get_pixbuf()
137 except Exception, ex
:
138 print >>sys
.stderr
, "Failed to load cached PNG icon: %s" % ex
140 icon
.set_from_pixbuf(icon_pixbuf
)
143 for meta
in iface
.get_metadata(XMLNS_IFACE
, 'category'):
144 feed_category
= meta
.content
148 for row
in category
.get_model():
149 if row
[0].lower() == feed_category
.lower():
150 category
.set_active(i
)
153 self
.window
.set_response_sensitive(RESPONSE_PREV
, True)
156 iface
= iface_cache
.get_interface(uri
.get_active_text())
157 tmpdir
= tempfile
.mkdtemp(prefix
= 'zero2desktop-')
159 desktop_name
= os
.path
.join(tmpdir
, 'zeroinstall-%s.desktop' % iface
.get_name().lower())
160 desktop
= file(desktop_name
, 'w')
161 desktop
.write(template
% (iface
.get_name(), iface
.summary
,
163 category
.get_active_text()))
164 icon_path
= iface_cache
.get_icon_path(iface
)
166 desktop
.write(icon_template
% icon_path
)
168 status
= os
.spawnlp(os
.P_WAIT
, 'xdg-desktop-menu', 'xdg-desktop-menu', 'install', desktop_name
)
170 shutil
.rmtree(tmpdir
)
172 box
= gtk
.MessageDialog(self
.window
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, gtk
.BUTTONS_OK
,
173 'Failed to run xdg-desktop-menu (error code %d)' % status
)
177 self
.window
.destroy()
179 def response(box
, resp
):
180 if resp
== RESPONSE_NEXT
:
181 iface
= uri
.get_active_text()
182 self
.window
.set_sensitive(False)
183 self
.set_keep_above(False)
184 child
= popen2
.Popen4(['0launch',
185 '--gui', '--download-only',
187 child
.tochild
.close()
189 def output_ready(src
, cond
):
190 got
= os
.read(src
.fileno(), 100)
194 status
= child
.wait()
195 self
.window
.set_sensitive(True)
196 self
.set_keep_above(True)
198 update_details_page()
200 dialog_next
.set_property('visible', False)
201 dialog_ok
.set_property('visible', True)
202 dialog_ok
.grab_focus()
204 box
= gtk
.MessageDialog(self
.window
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, gtk
.BUTTONS_OK
,
205 'Failed to run 0launch.\n' + errors
[0])
210 gobject
.io_add_watch(child
.fromchild
,
211 gobject
.IO_IN | gobject
.IO_HUP
,
213 elif resp
== gtk
.RESPONSE_OK
:
215 elif resp
== RESPONSE_PREV
:
216 dialog_next
.set_property('visible', True)
217 dialog_ok
.set_property('visible', False)
218 dialog_next
.grab_focus()
220 self
.window
.set_response_sensitive(RESPONSE_PREV
, False)
223 self
.window
.connect('response', response
)
225 if len(sys
.argv
) > 1:
226 self
.window
.response(RESPONSE_NEXT
)
228 def set_keep_above(self
, above
):
229 if hasattr(self
.window
, 'set_keep_above'):
230 # This isn't very nice, but GNOME defaults to
231 # click-to-raise and in that mode drag-and-drop
232 # is useless without this...
233 self
.window
.set_keep_above(above
)
236 main
.window
.connect('destroy', lambda box
: gtk
.main_quit())