Cope with our own .desktop syntax.
[Zero2Desktop.git] / zero2desktop
blob502e1013bb8e7c0353357313b902a188f2bf0217
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
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
13 version = '0.2'
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()
18 if options.version:
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."
26 sys.exit(0)
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
38 Type=Application
39 Version=1.0
40 Name=%s
41 Comment=%s
42 Exec=0launch -- %s %%f
43 Categories=Application;%s
44 """
46 icon_template = """Icon=%s
47 """
49 URI_LIST = 0
50 UTF_16 = 1
52 RESPONSE_PREV = 0
53 RESPONSE_NEXT = 1
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)
60 try:
61 for line in file(full):
62 if line.startswith('Exec='):
63 bits = line.split(' -- ', 1)
64 if ' ' in bits[0]:
65 uri = bits[0].split(' ', 1)[1] # 0launch URI -- %u
66 else:
67 uri = bits[1].split(' ', 1)[0].strip() # 0launch -- URI %u
68 already_installed[uri] = desktop_file
69 break
70 else:
71 warn("Failed to find Exec line in %s", full)
72 except Exception, ex:
73 warn("Failed to load .desktop file %s: %s", full, ex)
75 class MainWindow:
76 def __init__(self):
77 tree = gtk.glade.XML(gladefile, 'main')
78 self.window = tree.get_widget('main')
80 self.set_keep_above(True)
82 def set_uri_ok(uri):
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')
94 if len(sys.argv) > 1:
95 uri.append_text(sys.argv[1])
97 uri.connect('changed', set_uri_ok)
98 set_uri_ok(uri)
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):
106 if info == UTF_16:
107 import codecs
108 data = codecs.getdecoder('utf16')(selection_data.data)[0]
109 data = data.split('\n', 1)[0].strip()
110 else:
111 data = selection_data.data.split('\n', 1)[0].strip()
112 uri.set_text(data)
113 drag_context.finish(True, False, timestamp)
114 self.window.response(RESPONSE_NEXT)
115 return True
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)],
119 gtk.gdk.ACTION_COPY)
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)
128 if icon_path:
129 try:
130 # Icon format must be PNG (to avoid attacks)
131 loader = gtk.gdk.PixbufLoader('png')
132 try:
133 loader.write(file(icon_path).read())
134 finally:
135 loader.close()
136 icon_pixbuf = loader.get_pixbuf()
137 except Exception, ex:
138 print >>sys.stderr, "Failed to load cached PNG icon: %s" % ex
139 else:
140 icon.set_from_pixbuf(icon_pixbuf)
142 feed_category = None
143 for meta in iface.get_metadata(XMLNS_IFACE, 'category'):
144 feed_category = meta.content
145 break
146 if feed_category:
147 i = 0
148 for row in category.get_model():
149 if row[0].lower() == feed_category.lower():
150 category.set_active(i)
151 break
152 i += 1
153 self.window.set_response_sensitive(RESPONSE_PREV, True)
155 def finish():
156 iface = iface_cache.get_interface(uri.get_active_text())
157 tmpdir = tempfile.mkdtemp(prefix = 'zero2desktop-')
158 try:
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,
162 iface.uri,
163 category.get_active_text()))
164 icon_path = iface_cache.get_icon_path(iface)
165 if icon_path:
166 desktop.write(icon_template % icon_path)
167 desktop.close()
168 status = os.spawnlp(os.P_WAIT, 'xdg-desktop-menu', 'xdg-desktop-menu', 'install', desktop_name)
169 finally:
170 shutil.rmtree(tmpdir)
171 if status:
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)
174 box.run()
175 box.destroy()
176 else:
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',
186 '--', iface])
187 child.tochild.close()
188 errors = ['']
189 def output_ready(src, cond):
190 got = os.read(src.fileno(), 100)
191 if got:
192 errors[0] += got
193 else:
194 status = child.wait()
195 self.window.set_sensitive(True)
196 self.set_keep_above(True)
197 if status == 0:
198 update_details_page()
199 nb.next_page()
200 dialog_next.set_property('visible', False)
201 dialog_ok.set_property('visible', True)
202 dialog_ok.grab_focus()
203 else:
204 box = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
205 'Failed to run 0launch.\n' + errors[0])
206 box.run()
207 box.destroy()
208 return False
209 return True
210 gobject.io_add_watch(child.fromchild,
211 gobject.IO_IN | gobject.IO_HUP,
212 output_ready)
213 elif resp == gtk.RESPONSE_OK:
214 finish()
215 elif resp == RESPONSE_PREV:
216 dialog_next.set_property('visible', True)
217 dialog_ok.set_property('visible', False)
218 dialog_next.grab_focus()
219 nb.prev_page()
220 self.window.set_response_sensitive(RESPONSE_PREV, False)
221 else:
222 box.destroy()
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)
235 main = MainWindow()
236 main.window.connect('destroy', lambda box: gtk.main_quit())
237 gtk.main()