New release.
[Zero2Desktop.git] / zero2desktop
bloba9fc31b3049d6701b8d2394a6542f9cd343d2d1e
1 #!/usr/bin/env python
2 import os, sys, popen2, tempfile, shutil, optparse
3 import pygtk; pygtk.require('2.0')
4 import gtk, gobject
5 import gtk.glade
7 from zeroinstall.injector.namespaces import XMLNS_IFACE
8 from zeroinstall.injector.policy import Policy
9 from zeroinstall.injector.iface_cache import iface_cache
11 version = '0.2'
12 parser = optparse.OptionParser(usage="usage: %prog [options] [interface]")
13 parser.add_option("-V", "--version", help="display version information", action='store_true')
14 (options, args) = parser.parse_args()
16 if options.version:
17 print "Zero2Desktop (zero-install) " + version
18 print "Copyright (C) 2007 Thomas Leonard"
19 print "This program comes with ABSOLUTELY NO WARRANTY,"
20 print "to the extent permitted by law."
21 print "You may redistribute copies of this program"
22 print "under the terms of the GNU General Public License."
23 print "For more information about these matters, see the file named COPYING."
24 sys.exit(0)
26 zero2desktop_uri = "http://0install.net/2007/interfaces/Zero2Desktop.xml"
28 gladefile = os.path.join(os.path.dirname(__file__), 'zero2desktop.glade')
30 # XDG_UTILS should go at the end, so that the local copy is used first
31 os.environ['PATH'] += ':' + os.environ['XDG_UTILS']
33 template = """[Desktop Entry]
34 # This file was generated by zero2desktop.
35 # See the Zero Install project for details: http://0install.net
36 Type=Application
37 Version=1.0
38 Name=%s
39 Comment=%s
40 Exec=0launch -- %s %%f
41 Categories=Application;%s
42 """
44 icon_template = """Icon=%s
45 """
47 URI_LIST = 0
48 UTF_16 = 1
50 RESPONSE_PREV = 0
51 RESPONSE_NEXT = 1
53 class MainWindow:
54 def __init__(self):
55 tree = gtk.glade.XML(gladefile, 'main')
56 self.window = tree.get_widget('main')
58 self.set_keep_above(True)
60 def set_uri_ok(uri):
61 text = uri.get_text()
62 self.window.set_response_sensitive(RESPONSE_NEXT, bool(text))
64 drop_uri = tree.get_widget('drop_uri')
65 uri = tree.get_widget('interface_uri')
66 about = tree.get_widget('about')
67 icon = tree.get_widget('icon')
68 category = tree.get_widget('category')
69 dialog_next = tree.get_widget('dialog_next')
70 dialog_ok = tree.get_widget('dialog_ok')
72 if len(sys.argv) > 1:
73 uri.set_text(sys.argv[1])
75 uri.connect('changed', set_uri_ok)
76 set_uri_ok(uri)
78 category.set_active(11)
80 def uri_dropped(eb, drag_context, x, y, selection_data, info, timestamp):
81 if info == UTF_16:
82 import codecs
83 data = codecs.getdecoder('utf16')(selection_data.data)[0]
84 data = data.split('\n', 1)[0].strip()
85 else:
86 data = selection_data.data.split('\n', 1)[0].strip()
87 uri.set_text(data)
88 drag_context.finish(True, False, timestamp)
89 self.window.response(RESPONSE_NEXT)
90 return True
91 drop_uri.drag_dest_set(gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_DROP | gtk.DEST_DEFAULT_HIGHLIGHT,
92 [('text/uri-list', 0, URI_LIST),
93 ('text/x-moz-url', 0, UTF_16)],
94 gtk.gdk.ACTION_COPY)
95 drop_uri.connect('drag-data-received', uri_dropped)
97 nb = tree.get_widget('notebook1')
99 def update_details_page():
100 iface = iface_cache.get_interface(uri.get_text())
101 about.set_text('%s - %s' % (iface.get_name(), iface.summary))
102 icon_path = iface_cache.get_icon_path(iface)
103 if icon_path:
104 try:
105 # Icon format must be PNG (to avoid attacks)
106 loader = gtk.gdk.PixbufLoader('png')
107 try:
108 loader.write(file(icon_path).read())
109 finally:
110 loader.close()
111 icon_pixbuf = loader.get_pixbuf()
112 except Exception, ex:
113 print >>sys.stderr, "Failed to load cached PNG icon: %s" % ex
114 else:
115 icon.set_from_pixbuf(icon_pixbuf)
117 feed_category = None
118 for meta in iface.get_metadata(XMLNS_IFACE, 'category'):
119 feed_category = meta.content
120 break
121 if feed_category:
122 i = 0
123 for row in category.get_model():
124 if row[0].lower() == feed_category.lower():
125 category.set_active(i)
126 break
127 i += 1
128 self.window.set_response_sensitive(RESPONSE_PREV, True)
130 def finish():
131 iface = iface_cache.get_interface(uri.get_text())
132 tmpdir = tempfile.mkdtemp(prefix = 'zero2desktop-')
133 try:
134 desktop_name = os.path.join(tmpdir, 'zeroinstall-%s.desktop' % iface.get_name().lower())
135 desktop = file(desktop_name, 'w')
136 desktop.write(template % (iface.get_name(), iface.summary,
137 iface.uri,
138 category.get_active_text()))
139 icon_path = iface_cache.get_icon_path(iface)
140 if icon_path:
141 desktop.write(icon_template % icon_path)
142 desktop.close()
143 status = os.spawnlp(os.P_WAIT, 'xdg-desktop-menu', 'xdg-desktop-menu', 'install', desktop_name)
144 finally:
145 shutil.rmtree(tmpdir)
146 if status:
147 box = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
148 'Failed to run xdg-desktop-menu (error code %d)' % status)
149 box.run()
150 box.destroy()
151 else:
152 self.window.destroy()
154 def response(box, resp):
155 if resp == RESPONSE_NEXT:
156 iface = uri.get_text()
157 self.window.set_sensitive(False)
158 self.set_keep_above(False)
159 child = popen2.Popen4(['0launch',
160 '--gui', '--download-only',
161 '--', iface])
162 child.tochild.close()
163 errors = ['']
164 def output_ready(src, cond):
165 got = os.read(src.fileno(), 100)
166 if got:
167 errors[0] += got
168 else:
169 status = child.wait()
170 self.window.set_sensitive(True)
171 self.set_keep_above(True)
172 if status == 0:
173 update_details_page()
174 nb.next_page()
175 dialog_next.set_property('visible', False)
176 dialog_ok.set_property('visible', True)
177 dialog_ok.grab_focus()
178 else:
179 box = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
180 'Failed to run 0launch.\n' + errors[0])
181 box.run()
182 box.destroy()
183 return False
184 return True
185 gobject.io_add_watch(child.fromchild,
186 gobject.IO_IN | gobject.IO_HUP,
187 output_ready)
188 elif resp == gtk.RESPONSE_OK:
189 finish()
190 elif resp == RESPONSE_PREV:
191 dialog_next.set_property('visible', True)
192 dialog_ok.set_property('visible', False)
193 dialog_next.grab_focus()
194 nb.prev_page()
195 self.window.set_response_sensitive(RESPONSE_PREV, False)
196 else:
197 box.destroy()
198 self.window.connect('response', response)
200 if len(sys.argv) > 1:
201 self.window.response(RESPONSE_NEXT)
203 def set_keep_above(self, above):
204 if hasattr(self.window, 'set_keep_above'):
205 # This isn't very nice, but GNOME defaults to
206 # click-to-raise and in that mode drag-and-drop
207 # is useless without this...
208 self.window.set_keep_above(above)
210 main = MainWindow()
211 main.window.connect('destroy', lambda box: gtk.main_quit())
212 gtk.main()