Started updating main GUI for Python 3 and GTK 3
[zeroinstall/solver.git] / zeroinstall / gtkui / cache.py
blob632a70f502a44931a8fe92be67bc3c9a2af51898
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()
136 item.set_label(name)
137 def _cb(item, cb=cb):
138 action_required = cb(obj, cache_explorer)
139 if action_required is ACTION_REMOVE:
140 model.remove(model.get_iter(path))
141 item.connect('activate', _cb)
142 item.show()
143 menu.append(item)
144 menu.popup(None, None, None, bev.button, bev.time)
146 def warn(message, parent=None):
147 "Present a blocking warning message with OK/Cancel buttons, and return True if OK was pressed"
148 dialog = gtk.MessageDialog(parent=parent, buttons=gtk.BUTTONS_OK_CANCEL, type=gtk.MESSAGE_WARNING)
149 dialog.set_property('text', message)
150 response = []
151 def _response(dialog, resp):
152 if resp == gtk.RESPONSE_OK:
153 response.append(True)
154 dialog.connect('response', _response)
155 dialog.run()
156 dialog.destroy()
157 return bool(response)
159 def size_if_exists(path):
160 "Get the size for a file, or 0 if it doesn't exist."
161 if path and os.path.isfile(path):
162 return os.path.getsize(path)
163 return 0
165 def get_size(path):
166 "Get the size for a directory tree. Get the size from the .manifest if possible."
167 man = os.path.join(path, '.manifest')
168 if os.path.exists(man):
169 size = os.path.getsize(man)
170 for line in open(man, 'rb'):
171 if line[:1] in "XF":
172 size += int(line.split(' ', 4)[3])
173 else:
174 size = 0
175 for root, dirs, files in os.walk(path):
176 for name in files:
177 size += os.path.getsize(os.path.join(root, name))
178 return size
180 def summary(feed):
181 if feed.summary:
182 return feed.get_name() + ' - ' + feed.summary
183 return feed.get_name()
185 def get_selected_paths(tree_view):
186 "GTK 2.0 doesn't have this built-in"
187 selection = tree_view.get_selection()
188 paths = []
189 def add(model, path, iter):
190 paths.append(path)
191 selection.selected_foreach(add)
192 return paths
194 def all_children(model, iter):
195 "make a python generator out of the children of `iter`"
196 iter = model.iter_children(iter)
197 while iter:
198 yield iter
199 iter = model.iter_next(iter)
201 # Responses
202 DELETE = 0
203 SAFE_MODE = False # really delete things
204 #SAFE_MODE = True # print deletes, instead of performing them
206 class CachedFeed(object):
207 def __init__(self, uri, size):
208 self.uri = uri
209 self.size = size
211 def delete(self):
212 if not os.path.isabs(self.uri):
213 cached_iface = basedir.load_first_cache(namespaces.config_site,
214 'interfaces', model.escape(self.uri))
215 if cached_iface:
216 if SAFE_MODE:
217 print("Delete", cached_iface)
218 else:
219 os.unlink(cached_iface)
220 user_overrides = basedir.load_first_config(namespaces.config_site,
221 namespaces.config_prog,
222 'interfaces', model._pretty_escape(self.uri))
223 if user_overrides:
224 if SAFE_MODE:
225 print("Delete", user_overrides)
226 else:
227 os.unlink(user_overrides)
229 def __cmp__(self, other):
230 return self.uri.__cmp__(other.uri)
232 class ValidFeed(CachedFeed):
233 def __init__(self, feed, size):
234 CachedFeed.__init__(self, feed.url, size)
235 self.feed = feed
236 self.in_cache = []
238 def delete_children(self):
239 deletable = self.deletable_children()
240 undeletable = list(filter(lambda child: not child.may_delete, self.in_cache))
241 # the only undeletable items we expect to encounter are LocalImplementations
242 unexpected_undeletable = list(filter(lambda child: not isinstance(child, LocalImplementation), undeletable))
243 assert not unexpected_undeletable, "unexpected undeletable items!: %r" % (unexpected_undeletable,)
244 [child.delete() for child in deletable]
246 def delete(self):
247 self.delete_children()
248 super(ValidFeed, self).delete()
250 def append_to(self, model, iter):
251 iter2 = model.append(iter, extract_columns(
252 name=self.feed.get_name(),
253 uri=self.uri,
254 tooltip=self.feed.summary,
255 object=self))
256 for cached_impl in self.in_cache:
257 cached_impl.append_to(model, iter2)
259 def launch(self, explorer):
260 os.spawnlp(os.P_NOWAIT, '0launch', '0launch', '--gui', self.uri)
262 def copy_uri(self, explorer):
263 clipboard = gtk.clipboard_get()
264 clipboard.set_text(self.uri)
265 primary = gtk.clipboard_get('PRIMARY')
266 primary.set_text(self.uri)
268 def deletable_children(self):
269 return list(filter(lambda child: child.may_delete, self.in_cache))
271 def prompt_delete(self, cache_explorer):
272 description = "\"%s\"" % (self.feed.get_name(),)
273 num_children = len(self.deletable_children())
274 if self.in_cache:
275 description += _(" (and %s %s)") % (num_children, _("implementation") if num_children == 1 else _("implementations"))
276 if warn(_("Really delete %s?") % (description,), parent=cache_explorer.window):
277 self.delete()
278 return ACTION_REMOVE
280 menu_items = [(_('Launch with GUI'), launch),
281 (_('Copy URI'), copy_uri),
282 (_('Delete'), prompt_delete)]
284 class RemoteFeed(ValidFeed):
285 may_delete = True
287 class LocalFeed(ValidFeed):
288 may_delete = False
290 class InvalidFeed(CachedFeed):
291 may_delete = True
293 def __init__(self, uri, ex, size):
294 CachedFeed.__init__(self, uri, size)
295 self.ex = ex
297 def append_to(self, model, iter):
298 model.append(iter, extract_columns(
299 name=self.uri.rsplit('/', 1)[-1],
300 uri=self.uri,
301 size=self.size,
302 tooltip=self.ex,
303 object=self))
305 class LocalImplementation:
306 may_delete = False
308 def __init__(self, impl):
309 self.impl = impl
311 def append_to(self, model, iter):
312 model.append(iter, extract_columns(
313 name=self.impl.local_path,
314 tooltip=_('This is a local version, not held in the cache.'),
315 object=self))
318 class CachedImplementation:
319 may_delete = True
321 def __init__(self, cache_dir, digest):
322 self.impl_path = os.path.join(cache_dir, digest)
323 self.size = get_size(self.impl_path)
324 self.digest = digest
326 def delete(self):
327 if SAFE_MODE:
328 print("Delete", self.impl_path)
329 else:
330 support.ro_rmtree(self.impl_path)
332 def open_rox(self, explorer):
333 os.spawnlp(os.P_WAIT, '0launch', '0launch', ROX_IFACE, '-d', self.impl_path)
335 def verify(self, explorer):
336 try:
337 manifest.verify(self.impl_path)
338 except BadDigest as ex:
339 box = gtk.MessageDialog(None, 0,
340 gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, str(ex))
341 if ex.detail:
342 swin = gtk.ScrolledWindow()
343 buffer = gtk.TextBuffer()
344 mono = buffer.create_tag('mono', family = 'Monospace')
345 buffer.insert_with_tags(buffer.get_start_iter(), ex.detail, mono)
346 text = gtk.TextView(buffer)
347 text.set_editable(False)
348 text.set_cursor_visible(False)
349 swin.add(text)
350 swin.set_shadow_type(gtk.SHADOW_IN)
351 swin.set_border_width(4)
352 box.vbox.pack_start(swin)
353 swin.show_all()
354 box.set_resizable(True)
355 else:
356 box = gtk.MessageDialog(None, 0,
357 gtk.MESSAGE_INFO, gtk.BUTTONS_OK,
358 _('Contents match digest; nothing has been changed.'))
359 box.run()
360 box.destroy()
362 def prompt_delete(self, explorer):
363 if warn(_("Really delete implementation?"), parent=explorer.window):
364 self.delete()
365 return ACTION_REMOVE
367 menu_items = [(_('Open in ROX-Filer'), open_rox),
368 (_('Verify integrity'), verify),
369 (_('Delete'), prompt_delete)]
371 class UnusedImplementation(CachedImplementation):
372 def append_to(self, model, iter):
373 model.append(iter, extract_columns(
374 name=self.digest,
375 size=self.size,
376 tooltip=self.impl_path,
377 object=self))
379 class KnownImplementation(CachedImplementation):
380 def __init__(self, cached_iface, cache_dir, impl, impl_size, digest):
381 CachedImplementation.__init__(self, cache_dir, digest)
382 self.cached_iface = cached_iface
383 self.impl = impl
384 self.size = impl_size
386 def delete(self):
387 if SAFE_MODE:
388 print("Delete", self.impl)
389 else:
390 CachedImplementation.delete(self)
391 self.cached_iface.in_cache.remove(self)
393 def append_to(self, model, iter):
394 impl = self.impl
395 label = _('Version %(implementation_version)s (%(arch)s)') % {
396 'implementation_version': impl.get_version(),
397 'arch': impl.arch or 'any platform'}
399 model.append(iter, extract_columns(
400 name=label,
401 size=self.size,
402 tooltip=self.impl_path,
403 object=self))
405 def __cmp__(self, other):
406 if hasattr(other, 'impl'):
407 return self.impl.__cmp__(other.impl)
408 return -1
410 class CacheExplorer:
411 """A graphical interface for viewing the cache and deleting old items."""
413 def __init__(self, iface_cache):
414 widgets = gtkutils.Template(os.path.join(os.path.dirname(__file__), 'cache.ui'), 'cache')
415 self.window = window = widgets.get_widget('cache')
416 window.set_default_size(gtk.gdk.screen_width() / 2, gtk.gdk.screen_height() / 2)
417 self.iface_cache = iface_cache
419 # Model
420 self.raw_model = gtk.TreeStore(*Column.column_types())
421 self.view_model = self.raw_model.filter_new()
422 self.model.set_sort_column_id(URI.idx, gtk.SORT_ASCENDING)
423 self.tree_view = widgets.get_widget('treeview')
424 self.tree_view.set_model(self.view_model)
425 Column.add_all(self.tree_view)
427 # Sort / Filter options:
429 def init_combo(combobox, items, on_select):
430 liststore = gtk.ListStore(str)
431 combobox.set_model(liststore)
432 cell = gtk.CellRendererText()
433 combobox.pack_start(cell, True)
434 combobox.add_attribute(cell, 'text', 0)
435 for item in items:
436 combobox.append_text(item[0])
437 combobox.set_active(0)
438 def _on_select(*a):
439 selected_item = combobox.get_active()
440 on_select(selected_item)
441 combobox.connect('changed', lambda *a: on_select(items[combobox.get_active()]))
443 def set_sort_order(sort_order):
444 #print "SORT: %r" % (sort_order,)
445 name, column, order = sort_order
446 self.model.set_sort_column_id(column.idx, order)
447 self.sort_combo = widgets.get_widget('sort_combo')
448 init_combo(self.sort_combo, SORT_OPTIONS, set_sort_order)
450 def set_filter(f):
451 #print "FILTER: %r" % (f,)
452 description, filter_func = f
453 self.view_model = self.model.filter_new()
454 self.view_model.set_visible_func(filter_func)
455 self.tree_view.set_model(self.view_model)
456 self.set_initial_expansion()
457 self.filter_combo = widgets.get_widget('filter_combo')
458 init_combo(self.filter_combo, FILTER_OPTIONS, set_filter)
460 def button_press(tree_view, bev):
461 if bev.button != 3:
462 return False
463 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
464 if not pos:
465 return False
466 path, col, x, y = pos
467 obj = self.model[path][ITEM_OBJECT.idx]
468 if obj and hasattr(obj, 'menu_items'):
469 popup_menu(bev, obj, model=self.model, path=path, cache_explorer=self)
470 self.tree_view.connect('button-press-event', button_press)
472 # Responses
473 window.set_default_response(gtk.RESPONSE_CLOSE)
475 selection = self.tree_view.get_selection()
476 def selection_changed(selection):
477 any_selected = False
478 for x in get_selected_paths(self.tree_view):
479 obj = self.model[x][ITEM_OBJECT.idx]
480 if obj is None or not obj.may_delete:
481 window.set_response_sensitive(DELETE, False)
482 return
483 any_selected = True
484 window.set_response_sensitive(DELETE, any_selected)
485 selection.set_mode(gtk.SELECTION_MULTIPLE)
486 selection.connect('changed', selection_changed)
487 selection_changed(selection)
489 def response(dialog, resp):
490 if resp == gtk.RESPONSE_CLOSE:
491 window.destroy()
492 elif resp == gtk.RESPONSE_HELP:
493 cache_help.display()
494 elif resp == DELETE:
495 self._delete()
496 window.connect('response', response)
498 @property
499 def model(self):
500 return self.view_model.get_model()
502 def _delete(self):
503 errors = []
505 model = self.model
506 paths = get_selected_paths(self.tree_view)
507 paths.reverse()
508 for path in paths:
509 item = model[path][ITEM_OBJECT.idx]
510 assert item.delete
511 try:
512 item.delete()
513 except OSError as ex:
514 errors.append(str(ex))
515 else:
516 model.remove(model.get_iter(path))
517 self._update_sizes()
519 if errors:
520 gtkutils.show_message_box(self.window, _("Failed to delete:\n%s") % '\n'.join(errors))
522 def show(self):
523 """Display the window and scan the caches to populate it."""
524 self.window.show()
525 self.window.window.set_cursor(gtkutils.get_busy_pointer())
526 gtk.gdk.flush()
527 self._populate_model()
528 self.set_initial_expansion()
530 def set_initial_expansion(self):
531 model = self.model
532 try:
533 i = model.get_iter_root()
534 while i:
535 # expand only "Feeds"
536 if model[i][ITEM_OBJECT.idx] is SECTION_INTERFACES:
537 self.tree_view.expand_row(model.get_path(i), False)
538 i = model.iter_next(i)
539 finally:
540 self.window.window.set_cursor(None)
542 def _populate_model(self):
543 # Find cached implementations
545 unowned = {} # Impl ID -> Store
546 duplicates = [] # TODO
548 for s in self.iface_cache.stores.stores:
549 if os.path.isdir(s.dir):
550 for id in os.listdir(s.dir):
551 if id in unowned:
552 duplicates.append(id)
553 unowned[id] = s
555 ok_feeds = []
556 error_feeds = []
558 # Look through cached feeds for implementation owners
559 all_interfaces = self.iface_cache.list_all_interfaces()
560 all_feeds = {}
561 for uri in all_interfaces:
562 try:
563 iface = self.iface_cache.get_interface(uri)
564 except Exception as ex:
565 error_feeds.append((uri, str(ex), 0))
566 else:
567 all_feeds.update(self.iface_cache.get_feeds(iface))
569 for url, feed in all_feeds.items():
570 if not feed: continue
571 feed_size = 0
572 try:
573 if url != feed.url:
574 # (e.g. for .new feeds)
575 raise Exception('Incorrect URL for feed (%s vs %s)' % (url, feed.url))
577 if os.path.isabs(url):
578 cached_feed = url
579 feed_type = LocalFeed
580 else:
581 feed_type = RemoteFeed
582 cached_feed = basedir.load_first_cache(namespaces.config_site,
583 'interfaces', model.escape(url))
584 user_overrides = basedir.load_first_config(namespaces.config_site,
585 namespaces.config_prog,
586 'interfaces', model._pretty_escape(url))
588 feed_size = size_if_exists(cached_feed) + size_if_exists(user_overrides)
589 except Exception as ex:
590 error_feeds.append((url, str(ex), feed_size))
591 else:
592 cached_feed = feed_type(feed, feed_size)
593 for impl in feed.implementations.values():
594 if impl.local_path:
595 cached_feed.in_cache.append(LocalImplementation(impl))
596 for digest in impl.digests:
597 if digest in unowned:
598 cached_dir = unowned[digest].dir
599 impl_path = os.path.join(cached_dir, digest)
600 impl_size = get_size(impl_path)
601 cached_feed.in_cache.append(KnownImplementation(cached_feed, cached_dir, impl, impl_size, digest))
602 del unowned[digest]
603 cached_feed.in_cache.sort()
604 ok_feeds.append(cached_feed)
606 if error_feeds:
607 iter = SECTION_INVALID_INTERFACES.append_to(self.raw_model)
608 for uri, ex, size in error_feeds:
609 item = InvalidFeed(uri, ex, size)
610 item.append_to(self.raw_model, iter)
612 unowned_sizes = []
613 local_dir = os.path.join(basedir.xdg_cache_home, '0install.net', 'implementations')
614 for id in unowned:
615 if unowned[id].dir == local_dir:
616 impl = UnusedImplementation(local_dir, id)
617 unowned_sizes.append((impl.size, impl))
618 if unowned_sizes:
619 iter = SECTION_UNOWNED_IMPLEMENTATIONS.append_to(self.raw_model)
620 for size, item in unowned_sizes:
621 item.append_to(self.raw_model, iter)
623 if ok_feeds:
624 iter = SECTION_INTERFACES.append_to(self.raw_model)
625 for item in ok_feeds:
626 item.append_to(self.raw_model, iter)
627 self._update_sizes()
629 def _update_sizes(self):
630 """Set TOTAL_SIZE and PRETTY_SIZE to the total size, including all children."""
631 m = self.raw_model
632 def update(itr):
633 total = m[itr][SELF_SIZE.idx]
634 total += sum(map(update, all_children(m, itr)))
635 m[itr][PRETTY_SIZE.idx] = support.pretty_size(total) if total else '-'
636 m[itr][TOTAL_SIZE.idx] = total
637 return total
638 itr = m.get_iter_root()
639 while itr:
640 update(itr)
641 itr = m.iter_next(itr)
644 SORT_OPTIONS = [
645 ('URI', URI, gtk.SORT_ASCENDING),
646 ('Name', NAME, gtk.SORT_ASCENDING),
647 ('Size', TOTAL_SIZE, gtk.SORT_DESCENDING),
650 def init_filters():
651 def filter_only(filterable_types, filter_func):
652 def _filter(model, iter):
653 obj = model.get_value(iter, ITEM_OBJECT.idx)
654 if any((isinstance(obj, t) for t in filterable_types)):
655 result = filter_func(model, iter)
656 return result
657 return True
658 return _filter
660 def not_(func):
661 return lambda *a: not func(*a)
663 def is_local_feed(model, iter):
664 return isinstance(model[iter][ITEM_OBJECT.idx], LocalFeed)
666 def has_implementations(model, iter):
667 return model.iter_has_child(iter)
669 return [
670 ('All', lambda *a: True),
671 ('Feeds with implementations', filter_only([ValidFeed], has_implementations)),
672 ('Feeds without implementations', filter_only([ValidFeed], not_(has_implementations))),
673 ('Local Feeds', filter_only([ValidFeed], is_local_feed)),
674 ('Remote Feeds', filter_only([ValidFeed], not_(is_local_feed))),
676 FILTER_OPTIONS = init_filters()
679 cache_help = help_box.HelpBox(_("Cache Explorer Help"),
680 (_('Overview'), '\n' +
681 _("""When you run a program using Zero Install, it downloads the program's 'feed' file, \
682 which gives information about which versions of the program are available. This feed \
683 file is stored in the cache to save downloading it next time you run the program.
685 When you have chosen which version (implementation) of the program you want to \
686 run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \
687 you have many different versions of each program on your computer at once. This is useful, \
688 since it lets you use an old version if needed, and different programs may need to use \
689 different versions of libraries in some cases.
691 The cache viewer shows you all the feeds and implementations in your cache. \
692 This is useful to find versions you don't need anymore, so that you can delete them and \
693 free up some disk space.""")),
695 (_('Invalid feeds'), '\n' +
696 _("""The cache viewer gets a list of all feeds in your cache. However, some may not \
697 be valid; they are shown in the 'Invalid feeds' section. It should be fine to \
698 delete these. An invalid feed may be caused by a local feed that no longer \
699 exists or by a failed attempt to download a feed (the name ends in '.new').""")),
701 (_('Unowned implementations and temporary files'), '\n' +
702 _("""The cache viewer searches through all the feeds to find out which implementations \
703 they use. If no feed uses an implementation, it is shown in the 'Unowned implementations' \
704 section.
706 Unowned implementations can result from old versions of a program no longer being listed \
707 in the feed file. Temporary files are created when unpacking an implementation after \
708 downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \
709 you are currently unpacking new programs, it should be fine to delete everything in this \
710 section.""")),
712 (_('Feeds'), '\n' +
713 _("""All remaining feeds are listed in this section. You may wish to delete old versions of \
714 certain programs. Deleting a program which you may later want to run will require it to be downloaded \
715 again. Deleting a version of a program which is currently running may cause it to crash, so be careful!""")))