Cache explorer UI changes
[zeroinstall/zeroinstall-limyreth.git] / zeroinstall / gtkui / cache.py
blob4ad17da39e97c4c9178ab249c323c62b485a830a
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 _("Interfaces"),
92 _("Interfaces 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 interfaces (unreadable)"),
98 _("These interfaces 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, 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)
155 if response:
156 return True
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 file(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(iface):
180 if iface.summary:
181 return iface.get_name() + ' - ' + iface.summary
182 return iface.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 CachedInterface(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 'user_overrides', model.escape(self.uri))
222 if user_overrides:
223 if SAFE_MODE:
224 print "Delete", cached_iface
225 else:
226 os.unlink(user_overrides)
228 def __cmp__(self, other):
229 return self.uri.__cmp__(other.uri)
231 class ValidInterface(CachedInterface):
232 def __init__(self, iface, size):
233 CachedInterface.__init__(self, iface.uri, size)
234 self.iface = iface
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(ValidInterface, self).delete()
249 def append_to(self, model, iter):
250 iter2 = model.append(iter, extract_columns(
251 name=self.iface.get_name(),
252 uri=self.uri,
253 tooltip=self.iface.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)
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.iface.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 to clipboard'), copy_uri),
279 (_('Delete'), prompt_delete)]
281 class RemoteInterface(ValidInterface):
282 may_delete = True
284 class LocalInterface(ValidInterface):
285 may_delete = False
287 class InvalidInterface(CachedInterface):
288 may_delete = True
290 def __init__(self, uri, ex, size):
291 CachedInterface.__init__(self, uri, size)
292 self.ex = ex
294 def append_to(self, model, iter):
295 model.append(iter, [self.uri, self.size, None, self.ex, self])
297 class LocalImplementation:
298 may_delete = False
300 def __init__(self, impl):
301 self.impl = impl
303 def append_to(self, model, iter):
304 model.append(iter, extract_columns(
305 name=self.impl.local_path,
306 tooltip=_('This is a local version, not held in the cache.'),
307 object=self))
310 class CachedImplementation:
311 may_delete = True
313 def __init__(self, cache_dir, digest):
314 self.impl_path = os.path.join(cache_dir, digest)
315 self.size = get_size(self.impl_path)
316 self.digest = digest
318 def delete(self):
319 if SAFE_MODE:
320 print "Delete", self.impl_path
321 else:
322 support.ro_rmtree(self.impl_path)
324 def open_rox(self, explorer):
325 os.spawnlp(os.P_WAIT, '0launch', '0launch', ROX_IFACE, '-d', self.impl_path)
327 def verify(self, explorer):
328 try:
329 manifest.verify(self.impl_path)
330 except BadDigest, ex:
331 box = gtk.MessageDialog(None, 0,
332 gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, str(ex))
333 if ex.detail:
334 swin = gtk.ScrolledWindow()
335 buffer = gtk.TextBuffer()
336 mono = buffer.create_tag('mono', family = 'Monospace')
337 buffer.insert_with_tags(buffer.get_start_iter(), ex.detail, mono)
338 text = gtk.TextView(buffer)
339 text.set_editable(False)
340 text.set_cursor_visible(False)
341 swin.add(text)
342 swin.set_shadow_type(gtk.SHADOW_IN)
343 swin.set_border_width(4)
344 box.vbox.pack_start(swin)
345 swin.show_all()
346 box.set_resizable(True)
347 else:
348 box = gtk.MessageDialog(None, 0,
349 gtk.MESSAGE_INFO, gtk.BUTTONS_OK,
350 _('Contents match digest; nothing has been changed.'))
351 box.run()
352 box.destroy()
354 def prompt_delete(self, explorer):
355 if warn(_("Really delete implementation?"), parent=explorer.window):
356 self.delete()
357 return ACTION_REMOVE
359 menu_items = [(_('Open in ROX-Filer'), open_rox),
360 (_('Verify integrity'), verify),
361 (_('Delete'), prompt_delete)]
363 class UnusedImplementation(CachedImplementation):
364 def append_to(self, model, iter):
365 model.append(iter, extract_columns(
366 name=self.digest,
367 size=self.size,
368 tooltip=self.impl_path,
369 object=self))
371 class KnownImplementation(CachedImplementation):
372 def __init__(self, cached_iface, cache_dir, impl, impl_size, digest):
373 CachedImplementation.__init__(self, cache_dir, digest)
374 self.cached_iface = cached_iface
375 self.impl = impl
376 self.size = impl_size
378 def delete(self):
379 if SAFE_MODE:
380 print "Delete", self.impl
381 else:
382 CachedImplementation.delete(self)
383 self.cached_iface.in_cache.remove(self)
385 def append_to(self, model, iter):
386 model.append(iter, extract_columns(
387 name=_('Version %(implementation_version)s : %(implementation_id)s') % {'implementation_version': self.impl.get_version(), 'implementation_id': self.impl.id},
388 size=self.size,
389 tooltip=self.impl_path,
390 object=self))
392 def __cmp__(self, other):
393 if hasattr(other, 'impl'):
394 return self.impl.__cmp__(other.impl)
395 return -1
397 class CacheExplorer:
398 """A graphical interface for viewing the cache and deleting old items."""
400 def __init__(self, iface_cache):
401 widgets = gtkutils.Template(os.path.join(os.path.dirname(__file__), 'cache.ui'), 'cache')
402 self.window = window = widgets.get_widget('cache')
403 window.set_default_size(gtk.gdk.screen_width() / 2, gtk.gdk.screen_height() / 2)
404 self.iface_cache = iface_cache
406 # Model
407 self.raw_model = gtk.TreeStore(*Column.column_types())
408 self.view_model = self.raw_model.filter_new()
409 self.model.set_sort_column_id(URI.idx, gtk.SORT_ASCENDING)
410 self.tree_view = widgets.get_widget('treeview')
411 self.tree_view.set_model(self.view_model)
412 Column.add_all(self.tree_view)
414 # Sort / Filter options:
416 def init_combo(combobox, items, on_select):
417 liststore = gtk.ListStore(str)
418 combobox.set_model(liststore)
419 cell = gtk.CellRendererText()
420 combobox.pack_start(cell, True)
421 combobox.add_attribute(cell, 'text', 0)
422 for item in items:
423 combobox.append_text(item[0])
424 combobox.set_active(0)
425 def _on_select(*a):
426 selected_item = combobox.get_active()
427 on_select(selected_item)
428 combobox.connect('changed', lambda *a: on_select(items[combobox.get_active()]))
430 def set_sort_order(sort_order):
431 print "SORT: %r" % (sort_order,)
432 name, column, order = sort_order
433 self.model.set_sort_column_id(column.idx, order)
434 self.sort_combo = widgets.get_widget('sort_combo')
435 init_combo(self.sort_combo, SORT_OPTIONS, set_sort_order)
437 def set_filter(f):
438 print "FILTER: %r" % (f,)
439 description, filter_func = f
440 self.view_model = self.model.filter_new()
441 self.view_model.set_visible_func(filter_func)
442 self.tree_view.set_model(self.view_model)
443 self.set_initial_expansion()
444 self.filter_combo = widgets.get_widget('filter_combo')
445 init_combo(self.filter_combo, FILTER_OPTIONS, set_filter)
447 def button_press(tree_view, bev):
448 if bev.button != 3:
449 return False
450 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
451 if not pos:
452 return False
453 path, col, x, y = pos
454 obj = self.model[path][ITEM_OBJECT.idx]
455 if obj and hasattr(obj, 'menu_items'):
456 popup_menu(bev, obj, model=self.model, path=path, cache_explorer=self)
457 self.tree_view.connect('button-press-event', button_press)
459 # Responses
460 window.set_default_response(gtk.RESPONSE_CLOSE)
462 selection = self.tree_view.get_selection()
463 def selection_changed(selection):
464 any_selected = False
465 for x in get_selected_paths(self.tree_view):
466 obj = self.model[x][ITEM_OBJECT.idx]
467 if obj is None or not obj.may_delete:
468 window.set_response_sensitive(DELETE, False)
469 return
470 any_selected = True
471 window.set_response_sensitive(DELETE, any_selected)
472 selection.set_mode(gtk.SELECTION_MULTIPLE)
473 selection.connect('changed', selection_changed)
474 selection_changed(selection)
476 def response(dialog, resp):
477 if resp == gtk.RESPONSE_CLOSE:
478 window.destroy()
479 elif resp == gtk.RESPONSE_HELP:
480 cache_help.display()
481 elif resp == DELETE:
482 self._delete()
483 window.connect('response', response)
485 @property
486 def model(self):
487 return self.view_model.get_model()
489 def _delete(self):
490 errors = []
492 model = self.model
493 paths = get_selected_paths(self.tree_view)
494 paths.reverse()
495 for path in paths:
496 item = model[path][ITEM_OBJECT.idx]
497 assert item.delete
498 try:
499 item.delete()
500 except OSError, ex:
501 errors.append(str(ex))
502 else:
503 model.remove(model.get_iter(path))
504 self._update_sizes()
506 if errors:
507 gtkutils.show_message_box(self.window, _("Failed to delete:\n%s") % '\n'.join(errors))
509 def show(self):
510 """Display the window and scan the caches to populate it."""
511 self.window.show()
512 self.window.window.set_cursor(gtkutils.get_busy_pointer())
513 gtk.gdk.flush()
514 self._populate_model()
515 self.set_initial_expansion()
517 def set_initial_expansion(self):
518 model = self.model
519 try:
520 i = model.get_iter_root()
521 while i:
522 # expand only "Interfaces"
523 if model[i][ITEM_OBJECT.idx] is SECTION_INTERFACES:
524 self.tree_view.expand_row(model.get_path(i), False)
525 i = model.iter_next(i)
526 finally:
527 self.window.window.set_cursor(None)
529 def _populate_model(self):
530 # Find cached implementations
532 unowned = {} # Impl ID -> Store
533 duplicates = [] # TODO
535 for s in self.iface_cache.stores.stores:
536 if os.path.isdir(s.dir):
537 for id in os.listdir(s.dir):
538 if id in unowned:
539 duplicates.append(id)
540 unowned[id] = s
542 ok_interfaces = []
543 error_interfaces = []
545 # Look through cached interfaces for implementation owners
546 all = self.iface_cache.list_all_interfaces()
547 all.sort()
548 for uri in all:
549 iface_size = 0
550 try:
551 if os.path.isabs(uri):
552 cached_iface = uri
553 interface_type = LocalInterface
554 else:
555 interface_type = RemoteInterface
556 cached_iface = basedir.load_first_cache(namespaces.config_site,
557 'interfaces', model.escape(uri))
558 user_overrides = basedir.load_first_config(namespaces.config_site,
559 namespaces.config_prog,
560 'user_overrides', model.escape(uri))
562 iface_size = size_if_exists(cached_iface) + size_if_exists(user_overrides)
563 iface = self.iface_cache.get_interface(uri)
564 except Exception, ex:
565 error_interfaces.append((uri, str(ex), iface_size))
566 else:
567 cached_iface = interface_type(iface, iface_size)
568 for impl in iface.implementations.values():
569 if impl.local_path:
570 cached_iface.in_cache.append(LocalImplementation(impl))
571 for digest in impl.digests:
572 if digest in unowned:
573 cached_dir = unowned[digest].dir
574 impl_path = os.path.join(cached_dir, digest)
575 impl_size = get_size(impl_path)
576 cached_iface.in_cache.append(KnownImplementation(cached_iface, cached_dir, impl, impl_size, digest))
577 del unowned[digest]
578 cached_iface.in_cache.sort()
579 ok_interfaces.append(cached_iface)
581 if error_interfaces:
582 iter = SECTION_INVALID_INTERFACES.append_to(self.raw_model)
583 for uri, ex, size in error_interfaces:
584 item = InvalidInterface(uri, ex, size)
585 item.append_to(self.raw_model, iter)
587 unowned_sizes = []
588 local_dir = os.path.join(basedir.xdg_cache_home, '0install.net', 'implementations')
589 for id in unowned:
590 if unowned[id].dir == local_dir:
591 impl = UnusedImplementation(local_dir, id)
592 unowned_sizes.append((impl.size, impl))
593 if unowned_sizes:
594 iter = SECTION_UNOWNED_IMPLEMENTATIONS.append_to(self.raw_model)
595 for size, item in unowned_sizes:
596 item.append_to(self.raw_model, iter)
598 if ok_interfaces:
599 iter = SECTION_INTERFACES.append_to(self.raw_model)
600 for item in ok_interfaces:
601 item.append_to(self.raw_model, iter)
602 self._update_sizes()
604 def _update_sizes(self):
605 """Set TOTAL_SIZE and PRETTY_SIZE to the total size, including all children."""
606 m = self.raw_model
607 def update(itr):
608 total = m[itr][SELF_SIZE.idx]
609 total += sum(map(update, all_children(m, itr)))
610 m[itr][PRETTY_SIZE.idx] = support.pretty_size(total) if total else '-'
611 m[itr][TOTAL_SIZE.idx] = total
612 return total
613 itr = m.get_iter_root()
614 while itr:
615 update(itr)
616 itr = m.iter_next(itr)
619 SORT_OPTIONS = [
620 ('URI', URI, gtk.SORT_ASCENDING),
621 ('Name', NAME, gtk.SORT_ASCENDING),
622 ('Size', TOTAL_SIZE, gtk.SORT_DESCENDING),
625 def init_filters():
626 def filter_only(filterable_types, filter_func):
627 def _filter(model, iter):
628 obj = model.get_value(iter, ITEM_OBJECT.idx)
629 if any((isinstance(obj, t) for t in filterable_types)):
630 result = filter_func(model, iter)
631 return result
632 return True
633 return _filter
635 def not_(func):
636 return lambda *a: not func(*a)
638 def is_local_feed(model, iter):
639 return isinstance(model[iter][ITEM_OBJECT.idx], LocalInterface)
641 def has_implementations(model, iter):
642 return model.iter_has_child(iter)
644 return [
645 ('All', lambda *a: True),
646 ('Feeds with implementations', filter_only([ValidInterface], has_implementations)),
647 ('Feeds without implementations', filter_only([ValidInterface], not_(has_implementations))),
648 ('Local Feeds', filter_only([ValidInterface], is_local_feed)),
649 ('Remote Feeds', filter_only([ValidInterface], not_(is_local_feed))),
651 FILTER_OPTIONS = init_filters()
654 cache_help = help_box.HelpBox(_("Cache Explorer Help"),
655 (_('Overview'), '\n' +
656 _("""When you run a program using Zero Install, it downloads the program's 'interface' file, \
657 which gives information about which versions of the program are available. This interface \
658 file is stored in the cache to save downloading it next time you run the program.
660 When you have chosen which version (implementation) of the program you want to \
661 run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \
662 you have many different versions of each program on your computer at once. This is useful, \
663 since it lets you use an old version if needed, and different programs may need to use \
664 different versions of libraries in some cases.
666 The cache viewer shows you all the interfaces and implementations in your cache. \
667 This is useful to find versions you don't need anymore, so that you can delete them and \
668 free up some disk space.""")),
670 (_('Invalid interfaces'), '\n' +
671 _("""The cache viewer gets a list of all interfaces in your cache. However, some may not \
672 be valid; they are shown in the 'Invalid interfaces' section. It should be fine to \
673 delete these. An invalid interface may be caused by a local interface that no longer \
674 exists, by a failed attempt to download an interface (the name ends in '.new'), or \
675 by the interface file format changing since the interface was downloaded.""")),
677 (_('Unowned implementations and temporary files'), '\n' +
678 _("""The cache viewer searches through all the interfaces to find out which implementations \
679 they use. If no interface uses an implementation, it is shown in the 'Unowned implementations' \
680 section.
682 Unowned implementations can result from old versions of a program no longer being listed \
683 in the interface file. Temporary files are created when unpacking an implementation after \
684 downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \
685 you are currently unpacking new programs, it should be fine to delete everything in this \
686 section.""")),
688 (_('Interfaces'), '\n' +
689 _("""All remaining interfaces are listed in this section. You may wish to delete old versions of \
690 certain programs. Deleting a program which you may later want to run will require it to be downloaded \
691 again. Deleting a version of a program which is currently running may cause it to crash, so be careful!""")))