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
)
167 if len(iface
.get_metadata(XMLNS_IFACE
, 'needs-terminal')):
168 desktop
.write('Terminal=true\n')
170 status
= os
.spawnlp(os
.P_WAIT
, 'xdg-desktop-menu', 'xdg-desktop-menu', 'install', desktop_name
)
172 shutil
.rmtree(tmpdir
)
174 box
= gtk
.MessageDialog(self
.window
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, gtk
.BUTTONS_OK
,
175 'Failed to run xdg-desktop-menu (error code %d)' % status
)
179 self
.window
.destroy()
181 def response(box
, resp
):
182 if resp
== RESPONSE_NEXT
:
183 iface
= uri
.get_active_text()
184 self
.window
.set_sensitive(False)
185 self
.set_keep_above(False)
186 child
= popen2
.Popen4(['0launch',
187 '--gui', '--download-only',
189 child
.tochild
.close()
191 def output_ready(src
, cond
):
192 got
= os
.read(src
.fileno(), 100)
196 status
= child
.wait()
197 self
.window
.set_sensitive(True)
198 self
.set_keep_above(True)
200 update_details_page()
202 dialog_next
.set_property('visible', False)
203 dialog_ok
.set_property('visible', True)
204 dialog_ok
.grab_focus()
206 box
= gtk
.MessageDialog(self
.window
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, gtk
.BUTTONS_OK
,
207 'Failed to run 0launch.\n' + errors
[0])
212 gobject
.io_add_watch(child
.fromchild
,
213 gobject
.IO_IN | gobject
.IO_HUP
,
215 elif resp
== gtk
.RESPONSE_OK
:
217 elif resp
== RESPONSE_PREV
:
218 dialog_next
.set_property('visible', True)
219 dialog_ok
.set_property('visible', False)
220 dialog_next
.grab_focus()
222 self
.window
.set_response_sensitive(RESPONSE_PREV
, False)
225 self
.window
.connect('response', response
)
227 if len(sys
.argv
) > 1:
228 self
.window
.response(RESPONSE_NEXT
)
230 def set_keep_above(self
, above
):
231 if hasattr(self
.window
, 'set_keep_above'):
232 # This isn't very nice, but GNOME defaults to
233 # click-to-raise and in that mode drag-and-drop
234 # is useless without this...
235 self
.window
.set_keep_above(above
)
238 main
.window
.connect('destroy', lambda box
: gtk
.main_quit())