Add "Terminal=true" in desktop files if the feed says <needs-terminal>.
[Zero2Desktop.git] / zero2desktop
blob51c680f4642abb473a8173beb630457dee9a25ed
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 if len(iface.get_metadata(XMLNS_IFACE, 'needs-terminal')):
168 desktop.write('Terminal=true\n')
169 desktop.close()
170 status = os.spawnlp(os.P_WAIT, 'xdg-desktop-menu', 'xdg-desktop-menu', 'install', desktop_name)
171 finally:
172 shutil.rmtree(tmpdir)
173 if status:
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)
176 box.run()
177 box.destroy()
178 else:
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',
188 '--', iface])
189 child.tochild.close()
190 errors = ['']
191 def output_ready(src, cond):
192 got = os.read(src.fileno(), 100)
193 if got:
194 errors[0] += got
195 else:
196 status = child.wait()
197 self.window.set_sensitive(True)
198 self.set_keep_above(True)
199 if status == 0:
200 update_details_page()
201 nb.next_page()
202 dialog_next.set_property('visible', False)
203 dialog_ok.set_property('visible', True)
204 dialog_ok.grab_focus()
205 else:
206 box = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
207 'Failed to run 0launch.\n' + errors[0])
208 box.run()
209 box.destroy()
210 return False
211 return True
212 gobject.io_add_watch(child.fromchild,
213 gobject.IO_IN | gobject.IO_HUP,
214 output_ready)
215 elif resp == gtk.RESPONSE_OK:
216 finish()
217 elif resp == RESPONSE_PREV:
218 dialog_next.set_property('visible', True)
219 dialog_ok.set_property('visible', False)
220 dialog_next.grab_focus()
221 nb.prev_page()
222 self.window.set_response_sensitive(RESPONSE_PREV, False)
223 else:
224 box.destroy()
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)
237 main = MainWindow()
238 main.window.connect('destroy', lambda box: gtk.main_quit())
239 gtk.main()