In unit-tests, always use StringIO, not io
[zeroinstall.git] / zeroinstall / gtkui / cache.py
blob59ea24ba70daa1bf76e5beb21a2c76a88686628b
1 """Display the contents of the implementation cache."""
2 # Copyright (C) 2009, Thomas Leonard
3 # See the README file for details, or visit http://0install.net.
5 from __future__ import print_function
7 from zeroinstall import _
8 import os
9 import gtk
11 from zeroinstall.injector import namespaces, model
12 from zeroinstall.zerostore import BadDigest, manifest
13 from zeroinstall import support
14 from zeroinstall.support import basedir
15 from zeroinstall.gtkui import help_box, gtkutils
17 __all__ = ['CacheExplorer']
19 ROX_IFACE = 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer'
21 # Tree view columns
22 class Column(object):
23 columns = []
24 def __init__(self, name, column_type, resizable=False, props={}, hide=False, markup=False):
25 self.idx = len(self.columns)
26 self.columns.append(self)
27 self.name = name
28 self.column_type = column_type
29 self.props = props
30 self.resizable = resizable
31 self.hide = hide
32 self.markup = markup
34 @classmethod
35 def column_types(cls):
36 return [col.column_type for col in cls.columns]
38 @classmethod
39 def add_all(cls, tree_view):
40 [col.add(tree_view) for col in cls.columns]
42 def get_cell(self):
43 cell = gtk.CellRendererText()
44 self.set_props(cell, self.props)
45 return cell
47 def set_props(self, obj, props):
48 for k,v in props.items():
49 obj.set_property(k, v)
51 def get_column(self):
52 if self.markup:
53 kwargs = {'markup': self.idx}
54 else:
55 kwargs = {'text': self.idx}
56 column = gtk.TreeViewColumn(self.name, self.get_cell(), **kwargs)
57 if 'xalign' in self.props:
58 self.set_props(column, {'alignment': self.props['xalign']})
59 return column
61 def add(self, tree_view):
62 if self.hide:
63 return
64 column = self.get_column()
65 if self.resizable: column.set_resizable(True)
66 tree_view.append_column(column)
68 NAME = Column(_('Name'), str, hide=True)
69 URI = Column(_('URI'), str, hide=True)
70 TOOLTIP = Column(_('Description'), str, hide=True)
71 ITEM_VIEW = Column(_('Item'), str, props={'ypad': 6, 'yalign': 0}, resizable=True, markup=True)
72 SELF_SIZE = Column(_('Self Size'), int, hide=True)
73 TOTAL_SIZE = Column(_('Total Size'), int, hide=True)
74 PRETTY_SIZE = Column(_('Size'), str, props={'xalign':1.0})
75 ITEM_OBJECT = Column(_('Object'), object, hide=True)
77 ACTION_REMOVE = object() # just make a unique value
79 class Section(object):
80 may_delete = False
81 def __init__(self, name, tooltip):
82 self.name = name
83 self.tooltip = tooltip
85 def append_to(self, model):
86 return model.append(None, extract_columns(
87 name=self.name,
88 tooltip=self.tooltip,
89 object=self,
92 SECTION_INTERFACES = Section(
93 _("Feeds"),
94 _("Feeds in the cache"))
95 SECTION_UNOWNED_IMPLEMENTATIONS = Section(
96 _("Unowned implementations and temporary files"),
97 _("These probably aren't needed any longer. You can delete them."))
98 SECTION_INVALID_INTERFACES = Section(
99 _("Invalid feeds (unreadable)"),
100 _("These feeds exist in the cache but cannot be read. You should probably delete them."))
102 import cgi
103 def extract_columns(**d):
104 vals = list(map(lambda x:None, Column.columns))
105 def setcol(column, val):
106 vals[column.idx] = val
108 name = d.get('name', None)
109 desc = d.get('desc', None)
110 uri = d.get('uri', None)
112 setcol(NAME, name)
113 setcol(URI, uri)
114 if name and uri:
115 setcol(ITEM_VIEW, '<span font-size="larger" weight="bold">%s</span>\n'
116 '<span color="#666666">%s</span>' % tuple(map(cgi.escape, (name, uri))))
117 else:
118 setcol(ITEM_VIEW, cgi.escape(name or desc))
120 size = d.get('size', 0)
121 setcol(SELF_SIZE, size)
122 setcol(TOTAL_SIZE, 0) # must be set to prevent type error
123 setcol(TOOLTIP, d.get('tooltip', None))
124 setcol(ITEM_OBJECT, d.get('object', None))
125 return vals
128 def popup_menu(bev, obj, model, path, cache_explorer):
129 menu = gtk.Menu()
130 for i in obj.menu_items:
131 if i is None:
132 item = gtk.SeparatorMenuItem()
133 else:
134 name, cb = i
135 item = gtk.MenuItem(name)
136 def _cb(item, cb=cb):
137 action_required = cb(obj, cache_explorer)
138 if action_required is ACTION_REMOVE:
139 model.remove(model.get_iter(path))
140 item.connect('activate', _cb)
141 item.show()
142 menu.append(item)
143 menu.popup(None, None, None, bev.button, bev.time)
145 def warn(message, parent=None):
146 "Present a blocking warning message with OK/Cancel buttons, and return True if OK was pressed"
147 dialog = gtk.MessageDialog(parent=parent, buttons=gtk.BUTTONS_OK_CANCEL, type=gtk.MESSAGE_WARNING)
148 dialog.set_property('text', message)
149 response = []
150 def _response(dialog, resp):
151 if resp == gtk.RESPONSE_OK:
152 response.append(True)
153 dialog.connect('response', _response)
154 dialog.run()
155 dialog.destroy()
156 return bool(response)
158 def size_if_exists(path):
159 "Get the size for a file, or 0 if it doesn't exist."
160 if path and os.path.isfile(path):
161 return os.path.getsize(path)
162 return 0
164 def get_size(path):
165 "Get the size for a directory tree. Get the size from the .manifest if possible."
166 man = os.path.join(path, '.manifest')
167 if os.path.exists(man):
168 size = os.path.getsize(man)
169 for line in open(man, 'rb'):
170 if line[:1] in "XF":
171 size += int(line.split(' ', 4)[3])
172 else:
173 size = 0
174 for root, dirs, files in os.walk(path):
175 for name in files:
176 size += os.path.getsize(os.path.join(root, name))
177 return size
179 def summary(feed):
180 if feed.summary:
181 return feed.get_name() + ' - ' + feed.summary
182 return feed.get_name()
184 def get_selected_paths(tree_view):
185 "GTK 2.0 doesn't have this built-in"
186 selection = tree_view.get_selection()
187 paths = []
188 def add(model, path, iter):
189 paths.append(path)
190 selection.selected_foreach(add)
191 return paths
193 def all_children(model, iter):
194 "make a python generator out of the children of `iter`"
195 iter = model.iter_children(iter)
196 while iter:
197 yield iter
198 iter = model.iter_next(iter)
200 # Responses
201 DELETE = 0
202 SAFE_MODE = False # really delete things
203 #SAFE_MODE = True # print deletes, instead of performing them
205 class CachedFeed(object):
206 def __init__(self, uri, size):
207 self.uri = uri
208 self.size = size
210 def delete(self):
211 if not os.path.isabs(self.uri):
212 cached_iface = basedir.load_first_cache(namespaces.config_site,
213 'interfaces', model.escape(self.uri))
214 if cached_iface:
215 if SAFE_MODE:
216 print("Delete", cached_iface)
217 else:
218 os.unlink(cached_iface)
219 user_overrides = basedir.load_first_config(namespaces.config_site,
220 namespaces.config_prog,
221 'interfaces', model._pretty_escape(self.uri))
222 if user_overrides:
223 if SAFE_MODE:
224 print("Delete", user_overrides)
225 else:
226 os.unlink(user_overrides)
228 def __cmp__(self, other):
229 return self.uri.__cmp__(other.uri)
231 class ValidFeed(CachedFeed):
232 def __init__(self, feed, size):
233 CachedFeed.__init__(self, feed.url, size)
234 self.feed = feed
235 self.in_cache = []
237 def delete_children(self):
238 deletable = self.deletable_children()
239 undeletable = list(filter(lambda child: not child.may_delete, self.in_cache))
240 # the only undeletable items we expect to encounter are LocalImplementations
241 unexpected_undeletable = list(filter(lambda child: not isinstance(child, LocalImplementation), undeletable))
242 assert not unexpected_undeletable, "unexpected undeletable items!: %r" % (unexpected_undeletable,)
243 [child.delete() for child in deletable]
245 def delete(self):
246 self.delete_children()
247 super(ValidFeed, self).delete()
249 def append_to(self, model, iter):
250 iter2 = model.append(iter, extract_columns(
251 name=self.feed.get_name(),
252 uri=self.uri,
253 tooltip=self.feed.summary,
254 object=self))
255 for cached_impl in self.in_cache:
256 cached_impl.append_to(model, iter2)
258 def launch(self, explorer):
259 os.spawnlp(os.P_NOWAIT, '0launch', '0launch', '--gui', self.uri)
261 def copy_uri(self, explorer):
262 clipboard = gtk.clipboard_get()
263 clipboard.set_text(self.uri)
264 primary = gtk.clipboard_get('PRIMARY')
265 primary.set_text(self.uri)
267 def deletable_children(self):
268 return list(filter(lambda child: child.may_delete, self.in_cache))
270 def prompt_delete(self, cache_explorer):
271 description = "\"%s\"" % (self.feed.get_name(),)
272 num_children = len(self.deletable_children())
273 if self.in_cache:
274 description += _(" (and %s %s)") % (num_children, _("implementation") if num_children == 1 else _("implementations"))
275 if warn(_("Really delete %s?") % (description,), parent=cache_explorer.window):
276 self.delete()
277 return ACTION_REMOVE
279 menu_items = [(_('Launch with GUI'), launch),
280 (_('Copy URI'), copy_uri),
281 (_('Delete'), prompt_delete)]
283 class RemoteFeed(ValidFeed):
284 may_delete = True
286 class LocalFeed(ValidFeed):
287 may_delete = False
289 class InvalidFeed(CachedFeed):
290 may_delete = True
292 def __init__(self, uri, ex, size):
293 CachedFeed.__init__(self, uri, size)
294 self.ex = ex
296 def append_to(self, model, iter):
297 model.append(iter, extract_columns(
298 name=self.uri.rsplit('/', 1)[-1],
299 uri=self.uri,
300 size=self.size,
301 tooltip=self.ex,
302 object=self))
304 class LocalImplementation:
305 may_delete = False
307 def __init__(self, impl):
308 self.impl = impl
310 def append_to(self, model, iter):
311 model.append(iter, extract_columns(
312 name=self.impl.local_path,
313 tooltip=_('This is a local version, not held in the cache.'),
314 object=self))
317 class CachedImplementation:
318 may_delete = True
320 def __init__(self, cache_dir, digest):
321 self.impl_path = os.path.join(cache_dir, digest)
322 self.size = get_size(self.impl_path)
323 self.digest = digest
325 def delete(self):
326 if SAFE_MODE:
327 print("Delete", self.impl_path)
328 else:
329 support.ro_rmtree(self.impl_path)
331 def open_rox(self, explorer):
332 os.spawnlp(os.P_WAIT, '0launch', '0launch', ROX_IFACE, '-d', self.impl_path)
334 def verify(self, explorer):
335 try:
336 manifest.verify(self.impl_path)
337 except BadDigest as ex:
338 box = gtk.MessageDialog(None, 0,
339 gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, str(ex))
340 if ex.detail:
341 swin = gtk.ScrolledWindow()
342 buffer = gtk.TextBuffer()
343 mono = buffer.create_tag('mono', family = 'Monospace')
344 buffer.insert_with_tags(buffer.get_start_iter(), ex.detail, mono)
345 text = gtk.TextView(buffer)
346 text.set_editable(False)
347 text.set_cursor_visible(False)
348 swin.add(text)
349 swin.set_shadow_type(gtk.SHADOW_IN)
350 swin.set_border_width(4)
351 box.vbox.pack_start(swin)
352 swin.show_all()
353 box.set_resizable(True)
354 else:
355 box = gtk.MessageDialog(None, 0,
356 gtk.MESSAGE_INFO, gtk.BUTTONS_OK,
357 _('Contents match digest; nothing has been changed.'))
358 box.run()
359 box.destroy()
361 def prompt_delete(self, explorer):
362 if warn(_("Really delete implementation?"), parent=explorer.window):
363 self.delete()
364 return ACTION_REMOVE
366 menu_items = [(_('Open in ROX-Filer'), open_rox),
367 (_('Verify integrity'), verify),
368 (_('Delete'), prompt_delete)]
370 class UnusedImplementation(CachedImplementation):
371 def append_to(self, model, iter):
372 model.append(iter, extract_columns(
373 name=self.digest,
374 size=self.size,
375 tooltip=self.impl_path,
376 object=self))
378 class KnownImplementation(CachedImplementation):
379 def __init__(self, cached_iface, cache_dir, impl, impl_size, digest):
380 CachedImplementation.__init__(self, cache_dir, digest)
381 self.cached_iface = cached_iface
382 self.impl = impl
383 self.size = impl_size
385 def delete(self):
386 if SAFE_MODE:
387 print("Delete", self.impl)
388 else:
389 CachedImplementation.delete(self)
390 self.cached_iface.in_cache.remove(self)
392 def append_to(self, model, iter):
393 impl = self.impl
394 label = _('Version %(implementation_version)s (%(arch)s)') % {
395 'implementation_version': impl.get_version(),
396 'arch': impl.arch or 'any platform'}
398 model.append(iter, extract_columns(
399 name=label,
400 size=self.size,
401 tooltip=self.impl_path,
402 object=self))
404 def __cmp__(self, other):
405 if hasattr(other, 'impl'):
406 return self.impl.__cmp__(other.impl)
407 return -1
409 class CacheExplorer:
410 """A graphical interface for viewing the cache and deleting old items."""
412 def __init__(self, iface_cache):
413 widgets = gtkutils.Template(os.path.join(os.path.dirname(__file__), 'cache.ui'), 'cache')
414 self.window = window = widgets.get_widget('cache')
415 window.set_default_size(gtk.gdk.screen_width() / 2, gtk.gdk.screen_height() / 2)
416 self.iface_cache = iface_cache
418 # Model
419 self.raw_model = gtk.TreeStore(*Column.column_types())
420 self.view_model = self.raw_model.filter_new()
421 self.model.set_sort_column_id(URI.idx, gtk.SORT_ASCENDING)
422 self.tree_view = widgets.get_widget('treeview')
423 self.tree_view.set_model(self.view_model)
424 Column.add_all(self.tree_view)
426 # Sort / Filter options:
428 def init_combo(combobox, items, on_select):
429 liststore = gtk.ListStore(str)
430 combobox.set_model(liststore)
431 cell = gtk.CellRendererText()
432 combobox.pack_start(cell, True)
433 combobox.add_attribute(cell, 'text', 0)
434 for item in items:
435 combobox.append_text(item[0])
436 combobox.set_active(0)
437 def _on_select(*a):
438 selected_item = combobox.get_active()
439 on_select(selected_item)
440 combobox.connect('changed', lambda *a: on_select(items[combobox.get_active()]))
442 def set_sort_order(sort_order):
443 #print "SORT: %r" % (sort_order,)
444 name, column, order = sort_order
445 self.model.set_sort_column_id(column.idx, order)
446 self.sort_combo = widgets.get_widget('sort_combo')
447 init_combo(self.sort_combo, SORT_OPTIONS, set_sort_order)
449 def set_filter(f):
450 #print "FILTER: %r" % (f,)
451 description, filter_func = f
452 self.view_model = self.model.filter_new()
453 self.view_model.set_visible_func(filter_func)
454 self.tree_view.set_model(self.view_model)
455 self.set_initial_expansion()
456 self.filter_combo = widgets.get_widget('filter_combo')
457 init_combo(self.filter_combo, FILTER_OPTIONS, set_filter)
459 def button_press(tree_view, bev):
460 if bev.button != 3:
461 return False
462 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
463 if not pos:
464 return False
465 path, col, x, y = pos
466 obj = self.model[path][ITEM_OBJECT.idx]
467 if obj and hasattr(obj, 'menu_items'):
468 popup_menu(bev, obj, model=self.model, path=path, cache_explorer=self)
469 self.tree_view.connect('button-press-event', button_press)
471 # Responses
472 window.set_default_response(gtk.RESPONSE_CLOSE)
474 selection = self.tree_view.get_selection()
475 def selection_changed(selection):
476 any_selected = False
477 for x in get_selected_paths(self.tree_view):
478 obj = self.model[x][ITEM_OBJECT.idx]
479 if obj is None or not obj.may_delete:
480 window.set_response_sensitive(DELETE, False)
481 return
482 any_selected = True
483 window.set_response_sensitive(DELETE, any_selected)
484 selection.set_mode(gtk.SELECTION_MULTIPLE)
485 selection.connect('changed', selection_changed)
486 selection_changed(selection)
488 def response(dialog, resp):
489 if resp == gtk.RESPONSE_CLOSE:
490 window.destroy()
491 elif resp == gtk.RESPONSE_HELP:
492 cache_help.display()
493 elif resp == DELETE:
494 self._delete()
495 window.connect('response', response)
497 @property
498 def model(self):
499 return self.view_model.get_model()
501 def _delete(self):
502 errors = []
504 model = self.model
505 paths = get_selected_paths(self.tree_view)
506 paths.reverse()
507 for path in paths:
508 item = model[path][ITEM_OBJECT.idx]
509 assert item.delete
510 try:
511 item.delete()
512 except OSError as ex:
513 errors.append(str(ex))
514 else:
515 model.remove(model.get_iter(path))
516 self._update_sizes()
518 if errors:
519 gtkutils.show_message_box(self.window, _("Failed to delete:\n%s") % '\n'.join(errors))
521 def show(self):
522 """Display the window and scan the caches to populate it."""
523 self.window.show()
524 self.window.window.set_cursor(gtkutils.get_busy_pointer())
525 gtk.gdk.flush()
526 self._populate_model()
527 self.set_initial_expansion()
529 def set_initial_expansion(self):
530 model = self.model
531 try:
532 i = model.get_iter_root()
533 while i:
534 # expand only "Feeds"
535 if model[i][ITEM_OBJECT.idx] is SECTION_INTERFACES:
536 self.tree_view.expand_row(model.get_path(i), False)
537 i = model.iter_next(i)
538 finally:
539 self.window.window.set_cursor(None)
541 def _populate_model(self):
542 # Find cached implementations
544 unowned = {} # Impl ID -> Store
545 duplicates = [] # TODO
547 for s in self.iface_cache.stores.stores:
548 if os.path.isdir(s.dir):
549 for id in os.listdir(s.dir):
550 if id in unowned:
551 duplicates.append(id)
552 unowned[id] = s
554 ok_feeds = []
555 error_feeds = []
557 # Look through cached feeds for implementation owners
558 all_interfaces = self.iface_cache.list_all_interfaces()
559 all_feeds = {}
560 for uri in all_interfaces:
561 try:
562 iface = self.iface_cache.get_interface(uri)
563 except Exception as ex:
564 error_feeds.append((uri, str(ex), 0))
565 else:
566 all_feeds.update(self.iface_cache.get_feeds(iface))
568 for url, feed in all_feeds.items():
569 if not feed: continue
570 feed_size = 0
571 try:
572 if url != feed.url:
573 # (e.g. for .new feeds)
574 raise Exception('Incorrect URL for feed (%s vs %s)' % (url, feed.url))
576 if os.path.isabs(url):
577 cached_feed = url
578 feed_type = LocalFeed
579 else:
580 feed_type = RemoteFeed
581 cached_feed = basedir.load_first_cache(namespaces.config_site,
582 'interfaces', model.escape(url))
583 user_overrides = basedir.load_first_config(namespaces.config_site,
584 namespaces.config_prog,
585 'interfaces', model._pretty_escape(url))
587 feed_size = size_if_exists(cached_feed) + size_if_exists(user_overrides)
588 except Exception as ex:
589 error_feeds.append((url, str(ex), feed_size))
590 else:
591 cached_feed = feed_type(feed, feed_size)
592 for impl in feed.implementations.values():
593 if impl.local_path:
594 cached_feed.in_cache.append(LocalImplementation(impl))
595 for digest in impl.digests:
596 if digest in unowned:
597 cached_dir = unowned[digest].dir
598 impl_path = os.path.join(cached_dir, digest)
599 impl_size = get_size(impl_path)
600 cached_feed.in_cache.append(KnownImplementation(cached_feed, cached_dir, impl, impl_size, digest))
601 del unowned[digest]
602 cached_feed.in_cache.sort()
603 ok_feeds.append(cached_feed)
605 if error_feeds:
606 iter = SECTION_INVALID_INTERFACES.append_to(self.raw_model)
607 for uri, ex, size in error_feeds:
608 item = InvalidFeed(uri, ex, size)
609 item.append_to(self.raw_model, iter)
611 unowned_sizes = []
612 local_dir = os.path.join(basedir.xdg_cache_home, '0install.net', 'implementations')
613 for id in unowned:
614 if unowned[id].dir == local_dir:
615 impl = UnusedImplementation(local_dir, id)
616 unowned_sizes.append((impl.size, impl))
617 if unowned_sizes:
618 iter = SECTION_UNOWNED_IMPLEMENTATIONS.append_to(self.raw_model)
619 for size, item in unowned_sizes:
620 item.append_to(self.raw_model, iter)
622 if ok_feeds:
623 iter = SECTION_INTERFACES.append_to(self.raw_model)
624 for item in ok_feeds:
625 item.append_to(self.raw_model, iter)
626 self._update_sizes()
628 def _update_sizes(self):
629 """Set TOTAL_SIZE and PRETTY_SIZE to the total size, including all children."""
630 m = self.raw_model
631 def update(itr):
632 total = m[itr][SELF_SIZE.idx]
633 total += sum(map(update, all_children(m, itr)))
634 m[itr][PRETTY_SIZE.idx] = support.pretty_size(total) if total else '-'
635 m[itr][TOTAL_SIZE.idx] = total
636 return total
637 itr = m.get_iter_root()
638 while itr:
639 update(itr)
640 itr = m.iter_next(itr)
643 SORT_OPTIONS = [
644 ('URI', URI, gtk.SORT_ASCENDING),
645 ('Name', NAME, gtk.SORT_ASCENDING),
646 ('Size', TOTAL_SIZE, gtk.SORT_DESCENDING),
649 def init_filters():
650 def filter_only(filterable_types, filter_func):
651 def _filter(model, iter):
652 obj = model.get_value(iter, ITEM_OBJECT.idx)
653 if any((isinstance(obj, t) for t in filterable_types)):
654 result = filter_func(model, iter)
655 return result
656 return True
657 return _filter
659 def not_(func):
660 return lambda *a: not func(*a)
662 def is_local_feed(model, iter):
663 return isinstance(model[iter][ITEM_OBJECT.idx], LocalFeed)
665 def has_implementations(model, iter):
666 return model.iter_has_child(iter)
668 return [
669 ('All', lambda *a: True),
670 ('Feeds with implementations', filter_only([ValidFeed], has_implementations)),
671 ('Feeds without implementations', filter_only([ValidFeed], not_(has_implementations))),
672 ('Local Feeds', filter_only([ValidFeed], is_local_feed)),
673 ('Remote Feeds', filter_only([ValidFeed], not_(is_local_feed))),
675 FILTER_OPTIONS = init_filters()
678 cache_help = help_box.HelpBox(_("Cache Explorer Help"),
679 (_('Overview'), '\n' +
680 _("""When you run a program using Zero Install, it downloads the program's 'feed' file, \
681 which gives information about which versions of the program are available. This feed \
682 file is stored in the cache to save downloading it next time you run the program.
684 When you have chosen which version (implementation) of the program you want to \
685 run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \
686 you have many different versions of each program on your computer at once. This is useful, \
687 since it lets you use an old version if needed, and different programs may need to use \
688 different versions of libraries in some cases.
690 The cache viewer shows you all the feeds and implementations in your cache. \
691 This is useful to find versions you don't need anymore, so that you can delete them and \
692 free up some disk space.""")),
694 (_('Invalid feeds'), '\n' +
695 _("""The cache viewer gets a list of all feeds in your cache. However, some may not \
696 be valid; they are shown in the 'Invalid feeds' section. It should be fine to \
697 delete these. An invalid feed may be caused by a local feed that no longer \
698 exists or by a failed attempt to download a feed (the name ends in '.new').""")),
700 (_('Unowned implementations and temporary files'), '\n' +
701 _("""The cache viewer searches through all the feeds to find out which implementations \
702 they use. If no feed uses an implementation, it is shown in the 'Unowned implementations' \
703 section.
705 Unowned implementations can result from old versions of a program no longer being listed \
706 in the feed file. Temporary files are created when unpacking an implementation after \
707 downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \
708 you are currently unpacking new programs, it should be fine to delete everything in this \
709 section.""")),
711 (_('Feeds'), '\n' +
712 _("""All remaining feeds are listed in this section. You may wish to delete old versions of \
713 certain programs. Deleting a program which you may later want to run will require it to be downloaded \
714 again. Deleting a version of a program which is currently running may cause it to crash, so be careful!""")))