#!/usr/bin/env python ## gportage version 0.2.1 (19 Nov 2003) ## A simple portage package browser for PyGTK 2 ## ## Copyright (c) 2003, Fredrik Arnerup (e97_far@e.kth.se) ## All rights reserved. ## ## Redistribution and use in source and binary forms, with or without ## modification, are permitted provided that the following conditions are met: ## ## * Redistributions of source code must retain the above copyright notice, ## this list of conditions and the following disclaimer. ## ## * Redistributions in binary form must reproduce the above copyright ## notice, this list of conditions and the following disclaimer in the ## documentation and/or other materials provided with the distribution. ## ## THIS SOFTWARE IS PROVIDED BY FREDRIK ARNERUP "AS IS" AND ANY ## EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED ## WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE ## DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE ## FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL ## DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR ## SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER ## CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT ## LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY ## OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH ## DAMAGE. news = """0.2.1: Fixed bug that caused an exception to be thrown when the portage tree contains binary packages. 0.2: Displays installed and available packages in the same way. Shows categories in two levels.""" import sys, pygtk pygtk.require('2.0') try: import portage except ImportError: sys.exit("Could not find portage module.\n" "Are you sure this is a Gentoo system?") import gtk, gobject, re, string, threading, time, os gtk.threads_init() # make sure gtk lets other threads run too debug = 0 browser = 'gnome-moz-remote' single_space = 6 double_space = 2 * single_space def get_version(ebuild): """Extract version number from ebuild name""" result = '' parts = portage.catpkgsplit(ebuild) if parts: result = parts[2] if parts[3] != 'r0': result += '-' + parts[3] return result class PackageData: """An entry in the package database""" def __init__(self, fullname): self.fullname = fullname self.desc = '' self.installed = portage.db["/"]["vartree"].dep_match(fullname) #self.read_description() # too slow, no dough def get_name(self): return self.fullname.split('/')[1] def get_section(self): return self.fullname.split('/')[0] def get_latest_ebuild(self, include_masked = 1): if include_masked: criterion = "match-all" else: criterion = "match-visible" return portage.best(portage.portdb.xmatch(criterion, self.fullname)) def get_homepage(self): try: return portage.portdb.aux_get(self.get_latest_ebuild(), ["HOMEPAGE"])[0] except: return '' def get_installed(self): return self.installed def read_description(self): try: latest = self.get_latest_ebuild() if not latest: raise Exception('No ebuild found.') desc = portage.portdb.aux_get(latest, ["DESCRIPTION"]) self.desc = desc[0].encode('UTF-8') except Exception, e: self.desc = ("An error occured when reading the description:\n" + str(e)) def sort(list): """sort in alphabetic instead of ASCIIbetic order""" spam = [(x[0].upper(), x) for x in list] spam.sort() return [x[1] for x in spam] class ReadDatabaseThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) self.done = 0 self.count = 0 self.error = "" def get_entry(self, fullname): data = PackageData(fullname) section = data.get_section() name = data.get_name() if section in self.db and name in self.db[section]: return self.db[section][name] else: return None def read_db(self): self.db = {} # section dictionary with sorted lists of packages self.list = [] # all packages in a list sorted by package name tree = portage.db['/']['porttree'] try: allnodes = tree.getallnodes() except OSError, e: # I once forgot to give read permissions # to an ebuild I created in the portage overlay. self.error = str(e) return for entry in allnodes: section, name = entry.split('/') if name == 'timestamp.x': # why does getallnodes() continue # return timestamps? if debug: print 'patang' self.count += 1 if not section in self.db: self.db[section] = [] data = PackageData(entry) self.db[section].append((name, data)); self.list.append((name, data)) for section in self.db.keys(): self.db[section] = sort(self.db[section]) self.list = sort(self.list) def run(self): self.read_db() self.done = 1 # tell main thread that this thread has finished def xml_esc(string): """Escape characters that have special meanings in XML""" def subst(c): if c == '<': return '<' elif c == '>': return '>' elif c == '&': return '&' else: return c return ''.join(map(subst, string)) class Win: def open_webpage(self, button): # perhaps we should use the webbrowser module instead? os.system(browser + ' ' + button.url) def on_search(self, entry): """This gets called when you press enter in the search entry.""" regexp = self.use_regexp.get_active() search_term = entry.get_text() if not (db_thread.done and search_term): return if regexp: # compile pattern for efficiency re_object = re.compile(search_term, re.I) else: # search is case-insensitive search_term = search_term.upper() model = self.search_view.plist_model model.clear() self.notebook.set_current_page(self.search_view.page_number) self.cafe_opera.push(0, 'Searching ...') count = 0 for entry in db_thread.list: if regexp: match = re_object.search(entry[0]) != None else: match = string.find(entry[0].upper(), search_term) != -1 if match: count += 1 # entry[1].read_description() # too slow self.add_entry(model, None, entry[1]) self.cafe_opera.pop(0) if count > 1: result = str(count) + " matches found" elif count == 1: result = "1 match found" else: result = "No matches" self.cafe_opera.push(0, result) self.search_view.message = result def on_section_selection_changed(self, selection): name_markup = '' model, iter = selection.get_selected() section = '' if iter: section = model.get_value(iter, 1) name_markup = '' + xml_esc(section) + '' # update properties display for child in self.propbox.get_children(): self.propbox.remove(child) label = gtk.Label(""); label.set_alignment(0, 0); label.set_markup(name_markup) self.propbox.pack_start(label, gtk.FALSE, gtk.FALSE) self.propbox.show_all() # update package list model = model.plist_model model.clear() if section: db = db_thread.db for name, data in db[section]: if (not model.installed) or data.installed: self.add_entry(model, None, data) def on_selection_changed(self, selection): name_markup = url = desc_markup = '' model, iter = selection.get_selected() if iter: data = model.get_value(iter, 0) self.current_data = data if not data: # a category name_markup = '' + model.get_value(iter, 1) + '' else: # if it hasn't already been done: # read and cache data if not data.desc: data.read_description() model.set_value(iter, 2, '' + xml_esc(data.desc) + '') name_markup = '' + xml_esc(data.fullname) + '' url = data.get_homepage() desc_markup = '\n' + xml_esc(data.desc) + '\n\n' if data.installed: desc_markup += ( "Latest version installed:\t\t" + get_version(portage.best(data.installed)) + "\n") else: desc_markup += "Not installed\n" latest = get_version( data.get_latest_ebuild(include_masked = 1)) latest_unmasked = get_version( data.get_latest_ebuild(include_masked = 0)) if latest_unmasked: desc_markup += "Latest unmasked version:\t" \ + latest_unmasked + "\n" if latest != latest_unmasked: desc_markup += "Latest masked version:\t\t" + latest + "\n" # Update properties display for child in self.propbox.get_children(): self.propbox.remove(child) label = gtk.Label(""); label.set_alignment(0, 0); label.set_markup(name_markup) self.propbox.pack_start(label, gtk.FALSE, gtk.FALSE) if url: button = gtk.Button(); button.set_relief(gtk.RELIEF_NONE) button.url = url # save url in button button.connect("activate", self.open_webpage) button.connect("clicked", self.open_webpage) label = gtk.Label(""); label.set_alignment(0, 0); url_markup = '' \ + xml_esc(url) + "" label.set_markup(url_markup) button.add(label) self.propbox.pack_start(button, gtk.FALSE, gtk.FALSE) if desc_markup: label = gtk.Label(""); label.set_alignment(0, 0); label.set_line_wrap(gtk.TRUE); label.set_markup(desc_markup) self.propbox.pack_start(label, gtk.FALSE, gtk.FALSE) self.propbox.show_all() def add_entry(self, model, parent, data): entry_iter = model.insert_before(parent, None) model.set_value(entry_iter, 0, data) spam = xml_esc(data.get_name()) if data.installed: spam = "" + spam + "" model.set_value(entry_iter, 1, spam) model.set_value(entry_iter, 2, "" + xml_esc(data.desc) + "") def build_uncategorized_lists(self): list = db_thread.list for name, data in list: self.add_entry(self.all_view.ulist_model, None, data) if data.installed: self.add_entry(self.installed_view.ulist_model, None, data) def build_section_lists(self): def build_list(cats, model): catkeys = cats.keys() catkeys.sort() for cat in catkeys: cat_iter = model.insert_before(None, None) model.set_value(cat_iter, 0, '' + xml_esc(cat) + '') model.set_value(cat_iter, 1, '') kittens = cats[cat] kittens.sort() # ? for kitten in kittens: kitten_iter = model.insert_before(cat_iter, None) model.set_value(kitten_iter, 0, xml_esc(kitten)) section = cat + '-' + kitten model.set_value(kitten_iter, 1, section) # Categories: db = db_thread.db sections = db.keys() sections.sort() cats = {} for section in sections: try: cat, kitten = section.split('-') except: continue if not cats.has_key(cat): cats[cat] = [kitten] else: cats[cat] += [kitten] build_list(cats, self.all_view.slist_model) self.views[0].message = (str(len(db_thread.list)) + ' packages in ' + str(len(db)) + ' categories') self.cafe_opera.push(0, self.views[0].message) # default tab # Installed: installed = sort(portage.db["/"]["vartree"].getallnodes()) cats = {} for entry in installed: try: cat, kitten = entry.split('/')[0].split('-') except: continue if not cats.has_key(cat): cats[cat] = [] if not kitten in cats[cat]: cats[cat] += [kitten] build_list(cats, self.installed_view.slist_model) self.views[1].message = str(len(installed)) + ' installed packages' def on_page_change(self, notebook, spam, num): self.cafe_opera.pop(0) self.cafe_opera.push(0, self.views[num].message) self.on_selection_changed(self.views[num].get_selection()) def __init__(self): self.current_data = None self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.window.set_title("Portage Browser") vbox = gtk.VBox() # search entry hbox = gtk.HBox(gtk.FALSE, double_space) hbox.set_border_width(double_space) label = gtk.Label("_Search:") label.set_use_underline(gtk.TRUE) hbox.pack_start(label, gtk.FALSE, gtk.FALSE) self.search_entry = gtk.Entry() self.search_entry.connect("activate", self.on_search) hbox.pack_start(self.search_entry) label.set_mnemonic_widget(self.search_entry) self.use_regexp = gtk.CheckButton("Use _regexp") hbox.pack_start(self.use_regexp, gtk.FALSE, gtk.FALSE) vbox.pack_start(hbox, gtk.FALSE, gtk.FALSE) # vertical split paned = gtk.VPaned() vbox.pack_start(paned) # tabbed notebook self.notebook = gtk.Notebook() self.notebook.set_size_request(500, 350) self.notebook.connect("switch-page", self.on_page_change) paned.pack1(self.notebook) # tabs def make_package_list(): renderer1 = gtk.CellRendererText() renderer2 = gtk.CellRendererText() column1 = gtk.TreeViewColumn("Name", renderer1, markup = 1) column2 = gtk.TreeViewColumn("Description", renderer2, markup = 2) model = gtk.TreeStore(gobject.TYPE_PYOBJECT, # database entry gobject.TYPE_STRING, # name gobject.TYPE_STRING) # description list = gtk.TreeView(model) list.set_enable_search(gtk.TRUE) list.append_column(column1) list.append_column(column2) list.set_headers_visible(gtk.FALSE) list.get_selection().connect("changed", self.on_selection_changed) return model, list def make_section_list(): renderer = gtk.CellRendererText() column = gtk.TreeViewColumn("Name", renderer, markup = 0) model = gtk.TreeStore(gobject.TYPE_STRING, # name (markup) gobject.TYPE_STRING) # full name list = gtk.TreeView(model) list.set_enable_search(gtk.TRUE) list.append_column(column) list.set_headers_visible(gtk.FALSE) list.get_selection().connect("changed", self.on_section_selection_changed) return model, list def scroll_wrap(widget): scroller = gtk.ScrolledWindow() scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) scroller.set_shadow_type(gtk.SHADOW_IN) scroller.add(widget) return scroller def make_label(text): label = gtk.Label(text) label.set_use_underline(gtk.TRUE) return label class PackageView: def __init__(self, page_number, installed = 0): self.plist_model, self.plist = make_package_list() self.message = '' self.plist_model.installed = installed self.page_number = page_number def get_selection(self): return self.plist.get_selection() class SectionView(gtk.VBox, PackageView): def __init__(self, parent, page_number, installed = 0): gtk.VBox.__init__(self) PackageView.__init__(self, page_number, installed) self.notebook = gtk.Notebook() self._parent = parent paned = gtk.HPaned() self.notebook.set_show_tabs(gtk.FALSE) self.slist_model, self.slist = make_section_list() # let the section list have a reference to the package list self.slist_model.plist_model = self.plist_model self.ulist_model, self.ulist = make_package_list() scroller = scroll_wrap(self.slist) scroller.set_size_request(200, 300) paned.pack1(scroller) paned.pack2(scroll_wrap(self.plist)) self.notebook.append_page(paned, gtk.Label('')) self.notebook.append_page(scroll_wrap(self.ulist), gtk.Label('')) checkbutton = gtk.CheckButton('By _category') checkbutton.set_active(gtk.TRUE) checkbutton.connect('clicked', self.toggle_view) self.pack_start(self.notebook) self.pack_start(checkbutton, gtk.FALSE, gtk.FALSE, single_space) def toggle_view(self, checkbutton): self.notebook.set_current_page( not checkbutton.get_active()) self._parent.on_selection_changed(self.get_selection()) def get_selection(self): if not self.notebook.get_current_page(): return self.plist.get_selection() else: return self.ulist.get_selection() # status bar self.cafe_opera = gtk.Statusbar() self.cafe_opera.push(0, "") # so the stack isn't empty vbox.pack_start(self.cafe_opera, gtk.FALSE, gtk.FALSE) # properties view self.propbox = gtk.VBox(); self.propbox.set_border_width(double_space) scroller = gtk.ScrolledWindow() scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) scroller.add_with_viewport(self.propbox) scroller.set_size_request(400, 200) paned.pack2(scroller) num = 0 self.views = [] # tab 1 self.all_view = SectionView(self, num); num += 1 self.views.append(self.all_view) self.notebook.append_page(self.all_view, make_label('_All packages')) # tab 2 self.installed_view = SectionView(self, num, 1); num += 1 self.views.append(self.installed_view) self.notebook.append_page(self.installed_view, make_label('_Installed packages')) # tab 3 self.search_view = PackageView(num); num += 1 self.views.append(self.search_view) self.notebook.append_page(scroll_wrap(self.search_view.plist), make_label('S_earch results')) self.window.connect("destroy", self.on_destroy) self.window.add(vbox) self.window.show_all() gtk.timeout_add(100, self.check_db_thread) def check_db_thread(self): if debug: print 'ekki' self.cafe_opera.pop(0) self.cafe_opera.push(0, 'Reading ... (' + str(db_thread.count) + ' packages)') if db_thread.done: db_thread.join() if db_thread.error: sys.exit(db_thread.error) # Todo: display error dialog self.build_section_lists(); self.build_uncategorized_lists() return gtk.FALSE # disable this callback else: return gtk.TRUE def on_destroy(self, widget, data = None): gtk.main_quit() w = Win() db_thread = ReadDatabaseThread(); db_thread.start() gtk.main()