#!/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()