#!/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 '&lt;'
        elif c == '>': return '&gt;'
        elif c == '&': return '&amp;'
        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 = '<big>' + xml_esc(section) + '</big>'
        # 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 = '<big>' + model.get_value(iter, 1) + '</big>'
            else:                
                # if it hasn't already been done:
                # read and cache data
                if not data.desc:
                    data.read_description()
                model.set_value(iter, 2,
                                '<i>' + xml_esc(data.desc) + '</i>')
                name_markup = '<big>' + xml_esc(data.fullname) + '</big>'
                url = data.get_homepage()
                desc_markup = '\n<b>' + xml_esc(data.desc) + '</b>\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 = '<u><span foreground="blue">' \
                         + xml_esc(url) + "</span></u>"
            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 = "<b>" + spam + "</b>"
        model.set_value(entry_iter, 1, spam)
        model.set_value(entry_iter, 2,
                        "<i>" + xml_esc(data.desc) + "</i>")

    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,
                                '<b>' + xml_esc(cat) + '</b>')
                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()

