Package zeroinstall :: Package gtkui :: Module cache
[frames] | no frames]

Source Code for Module zeroinstall.gtkui.cache

  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. 
  4   
  5  from __future__ import print_function 
  6   
  7  from zeroinstall import _ 
  8  import os, sys 
  9  import gtk 
 10   
 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, tasks 
 15  from zeroinstall.gtkui import help_box, gtkutils 
 16   
 17  __all__ = ['CacheExplorer'] 
 18   
 19  ROX_IFACE = 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer' 
20 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
33 34 @classmethod
35 - def column_types(cls):
36 return [col.column_type for col in cls.columns]
37 38 @classmethod
39 - def add_all(cls, tree_view):
40 [col.add(tree_view) for col in cls.columns]
41
42 - def get_cell(self):
43 cell = gtk.CellRendererText() 44 self.set_props(cell, self.props) 45 return cell
46
47 - def set_props(self, obj, props):
48 for k,v in props.items(): 49 obj.set_property(k, v)
50
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
60
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)
67 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) 76 77 ACTION_REMOVE = object() # just make a unique value
78 79 -class Section(object):
80 may_delete = False
81 - def __init__(self, name, tooltip):
82 self.name = name 83 self.tooltip = tooltip
84
85 - def append_to(self, model):
86 return model.append(None, extract_columns( 87 name=self.name, 88 tooltip=self.tooltip, 89 object=self, 90 ))
91 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.")) 101 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
107 108 name = d.get('name', None) 109 desc = d.get('desc', None) 110 uri = d.get('uri', None) 111 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)) 119 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 126 127 menu = None 142 item.connect('activate', _cb) 143 item.show() 144 menu.append(item) 145 if gtk.pygtk_version >= (2, 90): 146 menu.popup(None, None, None, None, bev.button, bev.time) 147 else: 148 menu.popup(None, None, None, bev.button, bev.time) 149
150 -def warn(message, parent=None):
151 "Present a blocking warning message with OK/Cancel buttons, and return True if OK was pressed" 152 dialog = gtk.MessageDialog(parent=parent, buttons=gtk.BUTTONS_OK_CANCEL, type=gtk.MESSAGE_WARNING) 153 dialog.set_property('text', message) 154 response = [] 155 def _response(dialog, resp): 156 if resp == gtk.RESPONSE_OK: 157 response.append(True)
158 dialog.connect('response', _response) 159 dialog.run() 160 dialog.destroy() 161 return bool(response) 162
163 -def size_if_exists(path):
164 "Get the size for a file, or 0 if it doesn't exist." 165 if path and os.path.isfile(path): 166 return os.path.getsize(path) 167 return 0
168
169 -def get_size(path):
170 "Get the size for a directory tree. Get the size from the .manifest if possible." 171 man = os.path.join(path, '.manifest') 172 if os.path.exists(man): 173 size = os.path.getsize(man) 174 with open(man, 'rt') as stream: 175 for line in stream: 176 if line[:1] in "XF": 177 size += int(line.split(' ', 4)[3]) 178 else: 179 size = 0 180 for root, dirs, files in os.walk(path): 181 for name in files: 182 size += os.path.getsize(os.path.join(root, name)) 183 return size
184
185 -def summary(feed):
186 if feed.summary: 187 return feed.get_name() + ' - ' + feed.summary 188 return feed.get_name()
189
190 -def get_selected_paths(tree_view):
191 model, paths = tree_view.get_selection().get_selected_rows() 192 return paths
193
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)
200 201 # Responses 202 DELETE = 0 203 SAFE_MODE = False # really delete things
204 #SAFE_MODE = True # print deletes, instead of performing them 205 206 -class CachedFeed(object):
207 - def __init__(self, uri, size):
208 self.uri = uri 209 self.size = size
210
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)
228
229 - def __cmp__(self, other):
230 return self.uri.__cmp__(other.uri)
231
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 = []
237
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]
245
246 - def delete(self):
247 self.delete_children() 248 super(ValidFeed, self).delete()
249
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)
258
259 - def launch(self, explorer):
260 os.spawnlp(os.P_NOWAIT, '0launch', '0launch', '--gui', self.uri)
261
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)
267
268 - def deletable_children(self):
269 return list(filter(lambda child: child.may_delete, self.in_cache))
270
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
279 280 menu_items = [(_('Launch with GUI'), launch), 281 (_('Copy URI'), copy_uri), 282 (_('Delete'), prompt_delete)]
283
284 -class RemoteFeed(ValidFeed):
285 may_delete = True
286
287 -class LocalFeed(ValidFeed):
288 may_delete = False
289
290 -class InvalidFeed(CachedFeed):
291 may_delete = True 292
293 - def __init__(self, uri, ex, size):
294 CachedFeed.__init__(self, uri, size) 295 self.ex = ex
296
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))
304
305 -class LocalImplementation(object):
306 may_delete = False 307
308 - def __init__(self, impl):
309 self.impl = impl
310
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))
316
317 318 -class CachedImplementation(object):
319 may_delete = True 320
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
325
326 - def delete(self):
327 if SAFE_MODE: 328 print("Delete", self.impl_path) 329 else: 330 support.ro_rmtree(self.impl_path)
331
332 - def open_rox(self, explorer):
333 os.spawnlp(os.P_WAIT, '0launch', '0launch', ROX_IFACE, '-d', self.impl_path)
334
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()
361
362 - def prompt_delete(self, explorer):
363 if warn(_("Really delete implementation?"), parent=explorer.window): 364 self.delete() 365 return ACTION_REMOVE
366 367 if sys.version_info[0] > 2:
368 - def __lt__(self, other):
369 return self.digest < other.digest
370
371 - def __eq__(self, other):
372 return self.digest == other.digest
373 374 menu_items = [(_('Open in ROX-Filer'), open_rox), 375 (_('Verify integrity'), verify), 376 (_('Delete'), prompt_delete)]
377
378 -class UnusedImplementation(CachedImplementation):
379 - def append_to(self, model, iter):
380 model.append(iter, extract_columns( 381 name=self.digest, 382 size=self.size, 383 tooltip=self.impl_path, 384 object=self))
385
386 -class KnownImplementation(CachedImplementation):
387 - def __init__(self, cached_iface, cache_dir, impl, impl_size, digest):
388 CachedImplementation.__init__(self, cache_dir, digest) 389 self.cached_iface = cached_iface 390 self.impl = impl 391 self.size = impl_size
392
393 - def delete(self):
394 if SAFE_MODE: 395 print("Delete", self.impl) 396 else: 397 CachedImplementation.delete(self) 398 self.cached_iface.in_cache.remove(self)
399
400 - def append_to(self, model, iter):
401 impl = self.impl 402 label = _('Version %(implementation_version)s (%(arch)s)') % { 403 'implementation_version': impl.get_version(), 404 'arch': impl.arch or 'any platform'} 405 406 model.append(iter, extract_columns( 407 name=label, 408 size=self.size, 409 tooltip=self.impl_path, 410 object=self))
411
412 - def __cmp__(self, other):
413 if hasattr(other, 'impl'): 414 return self.impl.__cmp__(other.impl) 415 return -1
416 417 if sys.version_info[0] > 2:
418 - def __lt__(self, other):
419 return self.impl.__lt__(other.impl)
420
421 - def __eq__(self, other):
422 return self.impl.__eq__(other.impl)
423
424 -class CacheExplorer(object):
425 """A graphical interface for viewing the cache and deleting old items.""" 426
427 - def __init__(self, iface_cache):
428 widgets = gtkutils.Template(os.path.join(os.path.dirname(__file__), 'cache.ui'), 'cache') 429 self.window = window = widgets.get_widget('cache') 430 window.set_default_size(gtk.gdk.screen_width() / 2, gtk.gdk.screen_height() / 2) 431 self.iface_cache = iface_cache 432 433 # Model 434 self.raw_model = gtk.TreeStore(*Column.column_types()) 435 self.view_model = self.raw_model.filter_new() 436 self.model.set_sort_column_id(URI.idx, gtk.SORT_ASCENDING) 437 self.tree_view = widgets.get_widget('treeview') 438 Column.add_all(self.tree_view) 439 440 # Sort / Filter options: 441 442 def init_combo(combobox, items, on_select): 443 liststore = gtk.ListStore(str) 444 combobox.set_model(liststore) 445 cell = gtk.CellRendererText() 446 combobox.pack_start(cell, True) 447 combobox.add_attribute(cell, 'text', 0) 448 for item in items: 449 combobox.append_text(item[0]) 450 combobox.set_active(0) 451 def _on_select(*a): 452 selected_item = combobox.get_active() 453 on_select(selected_item)
454 combobox.connect('changed', lambda *a: on_select(items[combobox.get_active()]))
455 456 def set_sort_order(sort_order): 457 #print "SORT: %r" % (sort_order,) 458 name, column, order = sort_order 459 self.model.set_sort_column_id(column.idx, order) 460 self.sort_combo = widgets.get_widget('sort_combo') 461 init_combo(self.sort_combo, SORT_OPTIONS, set_sort_order) 462 463 def set_filter(f): 464 #print "FILTER: %r" % (f,) 465 description, filter_func = f 466 self.view_model = self.model.filter_new() 467 self.view_model.set_visible_func(filter_func) 468 self.tree_view.set_model(self.view_model) 469 self.set_initial_expansion() 470 self.filter_combo = widgets.get_widget('filter_combo') 471 init_combo(self.filter_combo, FILTER_OPTIONS, set_filter) 472 473 def button_press(tree_view, bev): 474 if bev.button != 3: 475 return False 476 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y)) 477 if not pos: 478 return False 479 path, col, x, y = pos 480 obj = self.model[path][ITEM_OBJECT.idx] 481 if obj and hasattr(obj, 'menu_items'): 482 popup_menu(bev, obj, model=self.model, path=path, cache_explorer=self) 483 self.tree_view.connect('button-press-event', button_press) 484 485 # Responses 486 window.set_default_response(gtk.RESPONSE_CLOSE) 487 488 selection = self.tree_view.get_selection() 489 def selection_changed(selection): 490 any_selected = False 491 for x in get_selected_paths(self.tree_view): 492 obj = self.model[x][ITEM_OBJECT.idx] 493 if obj is None or not obj.may_delete: 494 window.set_response_sensitive(DELETE, False) 495 return 496 any_selected = True 497 window.set_response_sensitive(DELETE, any_selected) 498 selection.set_mode(gtk.SELECTION_MULTIPLE) 499 selection.connect('changed', selection_changed) 500 selection_changed(selection) 501 502 def response(dialog, resp): 503 if resp == gtk.RESPONSE_CLOSE: 504 window.destroy() 505 elif resp == gtk.RESPONSE_HELP: 506 cache_help.display() 507 elif resp == DELETE: 508 self._delete() 509 window.connect('response', response) 510 511 @property
512 - def model(self):
513 return self.view_model.get_model()
514
515 - def _delete(self):
516 errors = [] 517 518 model = self.model 519 paths = get_selected_paths(self.tree_view) 520 paths.reverse() 521 for path in paths: 522 item = model[path][ITEM_OBJECT.idx] 523 assert item.delete 524 try: 525 item.delete() 526 except OSError as ex: 527 errors.append(str(ex)) 528 else: 529 model.remove(model.get_iter(path)) 530 self._update_sizes() 531 532 if errors: 533 gtkutils.show_message_box(self.window, _("Failed to delete:\n%s") % '\n'.join(errors))
534
535 - def show(self):
536 """Display the window and scan the caches to populate it.""" 537 self.window.show() 538 self.window.get_window().set_cursor(gtkutils.get_busy_pointer()) 539 gtk.gdk.flush() 540 # (async so that the busy pointer works on GTK 3) 541 @tasks.async 542 def populate(): 543 populate = self._populate_model() 544 yield populate 545 try: 546 tasks.check(populate) 547 except: 548 import logging 549 logging.warn("fail", exc_info = True) 550 raise 551 # (we delay until here because inserting with the view set is very slow) 552 self.tree_view.set_model(self.view_model) 553 self.set_initial_expansion()
554 return populate() 555
556 - def set_initial_expansion(self):
557 model = self.model 558 try: 559 i = model.get_iter_root() 560 while i: 561 # expand only "Feeds" 562 if model[i][ITEM_OBJECT.idx] is SECTION_INTERFACES: 563 self.tree_view.expand_row(model.get_path(i), False) 564 i = model.iter_next(i) 565 finally: 566 self.window.get_window().set_cursor(None)
567 568 @tasks.async
569 - def _populate_model(self):
570 # Find cached implementations 571 572 unowned = {} # Impl ID -> Store 573 duplicates = [] # TODO 574 575 for s in self.iface_cache.stores.stores: 576 if os.path.isdir(s.dir): 577 for id in os.listdir(s.dir): 578 if id in unowned: 579 duplicates.append(id) 580 unowned[id] = s 581 582 ok_feeds = [] 583 error_feeds = [] 584 585 # Look through cached feeds for implementation owners 586 all_interfaces = self.iface_cache.list_all_interfaces() 587 all_feeds = {} 588 for uri in all_interfaces: 589 try: 590 iface = self.iface_cache.get_interface(uri) 591 except Exception as ex: 592 error_feeds.append((uri, str(ex), 0)) 593 else: 594 all_feeds.update(self.iface_cache.get_feeds(iface)) 595 596 for url, feed in all_feeds.items(): 597 if not feed: continue 598 yield 599 feed_size = 0 600 try: 601 if url != feed.url: 602 # (e.g. for .new feeds) 603 raise Exception('Incorrect URL for feed (%s vs %s)' % (url, feed.url)) 604 605 if os.path.isabs(url): 606 cached_feed = url 607 feed_type = LocalFeed 608 else: 609 feed_type = RemoteFeed 610 cached_feed = basedir.load_first_cache(namespaces.config_site, 611 'interfaces', model.escape(url)) 612 user_overrides = basedir.load_first_config(namespaces.config_site, 613 namespaces.config_prog, 614 'interfaces', model._pretty_escape(url)) 615 616 feed_size = size_if_exists(cached_feed) + size_if_exists(user_overrides) 617 except Exception as ex: 618 error_feeds.append((url, str(ex), feed_size)) 619 else: 620 cached_feed = feed_type(feed, feed_size) 621 for impl in feed.implementations.values(): 622 if impl.local_path: 623 cached_feed.in_cache.append(LocalImplementation(impl)) 624 for digest in impl.digests: 625 if digest in unowned: 626 cached_dir = unowned[digest].dir 627 impl_path = os.path.join(cached_dir, digest) 628 impl_size = get_size(impl_path) 629 cached_feed.in_cache.append(KnownImplementation(cached_feed, cached_dir, impl, impl_size, digest)) 630 del unowned[digest] 631 cached_feed.in_cache.sort() 632 ok_feeds.append(cached_feed) 633 634 if error_feeds: 635 iter = SECTION_INVALID_INTERFACES.append_to(self.raw_model) 636 for uri, ex, size in error_feeds: 637 item = InvalidFeed(uri, ex, size) 638 item.append_to(self.raw_model, iter) 639 640 unowned_sizes = [] 641 local_dir = os.path.join(basedir.xdg_cache_home, '0install.net', 'implementations') 642 for id in unowned: 643 if unowned[id].dir == local_dir: 644 impl = UnusedImplementation(local_dir, id) 645 unowned_sizes.append((impl.size, impl)) 646 if unowned_sizes: 647 iter = SECTION_UNOWNED_IMPLEMENTATIONS.append_to(self.raw_model) 648 for size, item in unowned_sizes: 649 item.append_to(self.raw_model, iter) 650 651 if ok_feeds: 652 iter = SECTION_INTERFACES.append_to(self.raw_model) 653 for item in ok_feeds: 654 yield 655 item.append_to(self.raw_model, iter) 656 self._update_sizes()
657
658 - def _update_sizes(self):
659 """Set TOTAL_SIZE and PRETTY_SIZE to the total size, including all children.""" 660 m = self.raw_model 661 def update(itr): 662 total = m[itr][SELF_SIZE.idx] 663 total += sum(map(update, all_children(m, itr))) 664 m[itr][PRETTY_SIZE.idx] = support.pretty_size(total) if total else '-' 665 m[itr][TOTAL_SIZE.idx] = total 666 return total
667 itr = m.get_iter_root() 668 while itr: 669 update(itr) 670 itr = m.iter_next(itr) 671 672 673 SORT_OPTIONS = [ 674 ('URI', URI, gtk.SORT_ASCENDING), 675 ('Name', NAME, gtk.SORT_ASCENDING), 676 ('Size', TOTAL_SIZE, gtk.SORT_DESCENDING), 677 ]
678 679 -def init_filters():
680 def filter_only(filterable_types, filter_func): 681 def _filter(model, iter): 682 obj = model.get_value(iter, ITEM_OBJECT.idx) 683 if any((isinstance(obj, t) for t in filterable_types)): 684 result = filter_func(model, iter) 685 return result 686 return True
687 return _filter 688 689 def not_(func): 690 return lambda *a: not func(*a) 691 692 def is_local_feed(model, iter): 693 return isinstance(model[iter][ITEM_OBJECT.idx], LocalFeed) 694 695 def has_implementations(model, iter): 696 return model.iter_has_child(iter) 697 698 return [ 699 ('All', lambda *a: True), 700 ('Feeds with implementations', filter_only([ValidFeed], has_implementations)), 701 ('Feeds without implementations', filter_only([ValidFeed], not_(has_implementations))), 702 ('Local Feeds', filter_only([ValidFeed], is_local_feed)), 703 ('Remote Feeds', filter_only([ValidFeed], not_(is_local_feed))), 704 ] 705 FILTER_OPTIONS = init_filters() 706 707 708 cache_help = help_box.HelpBox(_("Cache Explorer Help"), 709 (_('Overview'), '\n' + 710 _("""When you run a program using Zero Install, it downloads the program's 'feed' file, \ 711 which gives information about which versions of the program are available. This feed \ 712 file is stored in the cache to save downloading it next time you run the program. 713 714 When you have chosen which version (implementation) of the program you want to \ 715 run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \ 716 you have many different versions of each program on your computer at once. This is useful, \ 717 since it lets you use an old version if needed, and different programs may need to use \ 718 different versions of libraries in some cases. 719 720 The cache viewer shows you all the feeds and implementations in your cache. \ 721 This is useful to find versions you don't need anymore, so that you can delete them and \ 722 free up some disk space.""")), 723 724 (_('Invalid feeds'), '\n' + 725 _("""The cache viewer gets a list of all feeds in your cache. However, some may not \ 726 be valid; they are shown in the 'Invalid feeds' section. It should be fine to \ 727 delete these. An invalid feed may be caused by a local feed that no longer \ 728 exists or by a failed attempt to download a feed (the name ends in '.new').""")), 729 730 (_('Unowned implementations and temporary files'), '\n' + 731 _("""The cache viewer searches through all the feeds to find out which implementations \ 732 they use. If no feed uses an implementation, it is shown in the 'Unowned implementations' \ 733 section. 734 735 Unowned implementations can result from old versions of a program no longer being listed \ 736 in the feed file. Temporary files are created when unpacking an implementation after \ 737 downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \ 738 you are currently unpacking new programs, it should be fine to delete everything in this \ 739 section.""")), 740 741 (_('Feeds'), '\n' + 742 _("""All remaining feeds are listed in this section. You may wish to delete old versions of \ 743 certain programs. Deleting a program which you may later want to run will require it to be downloaded \ 744 again. Deleting a version of a program which is currently running may cause it to crash, so be careful!"""))) 745