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