#!/usr/bin/python

# gnome-tasksel - GNOME interface to the Debian tasks.
#
# Copyright (c) 2002 Progeny Linux Systems.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# $Progeny: gnome-tasksel,v 1.46 2002/09/26 21:28:47 licquia Exp $
#
# This program allows a user to choose and install Debian tasks.  It
# is designed to be used in an installation environment; thus, it will
# not report on already installed tasks, and it assumes it has root
# privileges.

# Modules

import sys
import os
import string
import re
import xreadlines
import pygtk
pygtk.require('1.2')
import gtk
import gnome
import gnome.zvt
import libglade

# XXX: ZVT hack
#import _zvt_color

# This first section deals with reading and working with the tasks and
# packages.

# Global settings regarding the tasks and packages.

TRUE = 1
FALSE = 0
priority_list = [ "required", "important", "standard", "optional", "extra" ]
overrides_fn = "/etc/gnome-tasksel/overrides"
debconf_frontend = "gnome"

# The following option is dangerous to turn on.  Only do it in a local
# copy of gnome-tasksel.  Do NOT turn this on in the main distributed
# sources!

install_optional = FALSE

def compare_strings(s1, s2):
    "Compare two strings in a similar manner to strcmp() in C."

    if s1 == s2:
        return 0
    else:
        if s1 > s2:
            return 1
        else:
            return -1

class PackageInfo:
    def __init__(self):
        self.name = ""
        self.section = ""
        self.priority = ""
        self.shortdesc = ""
        self.longdesc = ""

    def get_name(self):
        return self.name[:]

    def set_name(self, name):
        self.name = name

    def get_shortdesc(self):
        return self.shortdesc[:]

    def set_shortdesc(self, shortdesc):
        self.shortdesc = shortdesc

    def get_longdesc(self):
        return self.longdesc

    def set_longdesc(self, longdesc):
        self.longdesc = longdesc

    def get_section(self):
        return self.section[:]

    def set_section(self, section):
        self.section = section

    def get_priority(self):
        return self.priority[:]

    def set_priority(self, priority):
        if priority in priority_list:
            self.priority = priority

    def get_depends(self):
        return self.depends[:]

    def set_depends(self, depends):
        self.depends = depends

    def get_recommends(self):
        return self.recommends[:]

    def set_recommends(self, recommends):
        self.recommends = recommends

    def get_suggests(self):
        return self.suggests[:]

    def set_suggests(self, suggests):
        self.suggests = suggests

def compare_packages(p1, p2):
    return compare_strings(p1.get_name(), p2.get_name())

class TaskInfo:
    def __init__(self):
        self.pkgs = []
        self.name = ""
        self.section = ""
        self.shortdesc = ""
        self.longdesc = ""
        self.essential = []

    def get_name(self):
        return self.name[:]

    def set_name(self, name):
        self.name = name

    def get_shortdesc(self):
        return self.shortdesc[:]

    def set_shortdesc(self, shortdesc):
        self.shortdesc = shortdesc

    def get_longdesc(self):
        return self.longdesc

    def set_longdesc(self, longdesc):
        self.longdesc = longdesc

    def get_section(self):
        return self.section[:]

    def set_section(self, section):
        self.section = section

    def get_essential(self):
        return self.essential[:]

    def set_essential(self, essential):
        self.essential = essential

    def resolve_essential(self):
        if len(self.pkgs) == 0:
            return FALSE

        # XXX: Let's not actually use essential package information yet.
##         if len(self.essential) > 0:
##             for essential_pkg in self.essential:
##                 if not essential_pkg in self.pkgs:
##                     return FALSE

        return TRUE

    def add_package(self, pkg):
        self.pkgs.append(pkg)

    def get_packages(self):
        return self.pkgs[:]

    def get_selected(self):
        return self.selected

    def set_selected(self, selected):
        self.selected = selected

def compare_tasks(t1, t2):
    return compare_strings(t1.get_name(), t2.get_name())

def convert_deps(depline):
    dep_list_raw = re.split(r',\s*', depline)
    dep_list = []
    for dep in dep_list_raw:
        match = re.match(r'(\w+)\s+.*', dep)
        if match is not None:
            dep_list.append(match.group(1))

    return dep_list

def read_tasks(fn):
    global section_list

    longfields = ["LongDesc", "Essential"]

    file = open(fn)
    task_list = {}
    current_task = TaskInfo()
    field = ""
    value = ""

    for line in file.readlines():
        if len(string.strip(line)) == 0:
            if field == "LongDesc" and value:
                current_task.set_longdesc(value)
            elif field == "Essential" and value:
                current_task.set_essential(string.split(value))
            field = ""
            task_list[current_task.get_name()] = current_task
            current_task = TaskInfo()
        elif line[0] in string.whitespace and field in longfields:
            if line[1] == ".":
                line = "  " + line[2:]
            value = value + line[1:]
        elif string.find(line, ":"):
            if field == "LongDesc" and value:
                current_task.set_longdesc(value)
            elif field == "Essential" and value:
                current_task.set_essential(value)
            components = re.split(r':\s*', string.strip(line), 1)
            if len(components) > 1:
                (field, value) = components[:2]
            else:
                field = components[0]
                value = ""
            if field == "Task":
                current_task.set_name(value)
            elif field == "Section":
                current_task.set_section(value)
            elif field == "Description":
                current_task.set_shortdesc(value)
                value = ""
                field = "LongDesc"

    file.close()
    return task_list

def read_packages(fn, task_list, status_callback = None):
    # Priority modifier file
    global overrides_fn
    priority_overrides = {}
    if os.path.exists(overrides_fn):
        overrides_file = open(overrides_fn)
        for line in xreadlines.xreadlines(overrides_file):
            if re.match(r'^\s*#', line) or not re.match(r'\S', line):
                continue
            string.strip(line)
            (name, value) = string.split(line)[:2]
            priority_overrides[name] = value
        overrides_file.close()

    file = open(fn)
    pkg_list = []
    current_pkg = PackageInfo()
    field = ""
    value = ""
    for line in xreadlines.xreadlines(file):
        if len(string.strip(line)) == 0:
            if field == "LongDesc" and value:
                current_pkg.set_longdesc(value)
            field = ""
            pkg_list.append(current_pkg)
            current_pkg = PackageInfo()
            if status_callback is not None:
                status_callback(len(pkg_list))
        elif line[0] in string.whitespace and field == "LongDesc":
            if line[1] == ".":
                line = "  " + line[2:]
            value = value + line[1:]
        elif string.find(line, ":"):
            if field == "LongDesc" and value:
                current_pkg.set_longdesc(value)
            (field, value) = re.split(r':\s*', string.strip(line), 1)
            if field == "Task":
                pkg_tasks = re.split(r',\s*', value)
                for pkg_task in pkg_tasks:
                    if task_list.has_key(pkg_task):
                        task_list[pkg_task].add_package(current_pkg)
            elif field == "Package":
                current_pkg.set_name(value)
                if priority_overrides.has_key(value):
                    current_pkg.set_priority(priority_overrides[value])
            elif field == "Section":
                current_pkg.set_section(value)
            elif field == "Priority":
                if not priority_overrides.has_key(current_pkg.get_name()):
                    current_pkg.set_priority(value)
            elif field == "Depends":
                current_pkg.set_depends(convert_deps(value))
            elif field == "Recommends":
                current_pkg.set_recommends(convert_deps(value))
            elif field == "Suggests":
                current_pkg.set_suggests(convert_deps(value))
            elif field == "Description":
                current_pkg.set_shortdesc(value)
                value = ""
                field = "LongDesc"
    file.close()
    return pkg_list

def get_section_list(tasks):
    section_list = []
    for taskkey in tasks.keys():
        if tasks[taskkey].get_section() not in section_list:
            section_list.append(tasks[taskkey].get_section())

    section_list.sort()
    return section_list

def get_standard_list(pkgs):
    global install_optional

    std_list = []
    for pkg in pkgs:
        if pkg.get_priority() in ["required", "important", "standard"] or \
           (install_optional and pkg.get_priority() == "optional"):
            std_list.append(pkg.get_name())

    return std_list

# The following routines act as a "hybrid" of the data manipulation
# and interface sections.  Given a properly set up zvt widget, it will
# use it to run the apt session requested.

def update_pkgs(zvt_term):
    term_pid = zvt_term.forkpty(gtk.FALSE)
    if term_pid == 0:
        os.environ["TERM"] = "xterm"
        os.environ["COLORTERM"] = "zterm"
        os.environ["DEBIAN_FRONTEND"] = "readline"

        #os.execv("/usr/bin/dselect", ("dselect", "update"))
        #os.execv("/usr/bin/apt-get", ("apt-get", "update"))
        os.execv("/bin/sh", ("/bin/sh", "-e", "/usr/share/gnome-tasksel/update-helper"))

        sys.exit(1)

    return term_pid

def install_pkgs(task_list, pkg_list, zvt_term):
    args = ["apt-get", "install"]
    initial_args = len(args)
    args.extend(pkg_list)
    for task in task_list:
        for pkg in task.get_packages():
            args.append(pkg.get_name())

    term_pid = -1
    if len(args) > initial_args:
        term_pid = zvt_term.forkpty(gtk.FALSE)
        if term_pid == 0:
            os.environ["TERM"] = "xterm"
            os.environ["COLORTERM"] = "zterm"
            os.environ["DEBIAN_FRONTEND"] = debconf_frontend
            os.execv("/usr/bin/apt-get", args)

    return term_pid

# User interface section.

def format_longdesc(longdesc):
    # XXX: This version allows for automatic formatting.
    #single_newline = re.compile(r'([^\n])\n([^\n])', re.MULTILINE)
    #result = single_newline.sub(r'\1 \2', longdesc)

    # XXX: This version simply removes single periods in lines.
    no_period = re.compile(r'(\s+)\.(\s+)', re.MULTILINE)
    result = no_period.sub(r'\1 \2', longdesc)

    return result

def get_standard_list_cached():
    global pkgs
    global standard_list

    if not len(standard_list):
        standard_list = get_standard_list(pkgs)

    return standard_list

# StatusWindow.  For recording status on long operations.

class StatusWindow:
    def __init__(self):
        self.win = gtk.GtkWindow()

        self.win.set_position(gtk.WIN_POS_CENTER)
        #self.win.type = gtk.WINDOW_POPUP

        hbox = gtk.GtkHBox()
        hbox.set_border_width(5)
        self.win.add(hbox)

        self.label = gtk.GtkLabel()
        self.label.set_text("")
        hbox.pack_start(self.label)

        self.status = gtk.GtkLabel()
        self.status.set_text("")
        hbox.pack_start(self.status)

        self.destroyed = 0

        self.handle_events()

    def handle_events(self):
        if not self.destroyed:
            while gtk.events_pending():
                gtk.mainiteration()

    def show(self):
        if not self.destroyed:
            self.win.show_all()

        self.handle_events()

    def hide(self):
        if not self.destroyed:
            self.win.hide()

        self.handle_events()

    def set_label(self, label):
        if not self.destroyed:
            self.label.set_text(label)

        self.handle_events()

    def update(self, status):
        if not self.destroyed:
            self.status.set_text(status)

        self.handle_events()

    def close(self):
        self.win.destroy()
        self.handle_events()
        self.destroyed = 1

# Setup functions.  These set up various parts of the user interface.

def setup_zvt():
    global zvt_win
    global zvt_button
    global zvt_color_array
    global zvt_normal_font

    zvt_win = gtk.GtkWindow()
    vbox = gtk.GtkVBox()
    zvt_win.add(vbox)

    hbox = gtk.GtkHBox()
    vbox.pack_start(hbox)

    term = gnome.zvt.ZvtTerm(80, 25)
    term.connect("child-died", on_apt_terminated)
    term.set_font_name(zvt_normal_font)
    term.set_scrollback(2000)

    # XXX: ZVT color scheme hacking section.
    #
    # Here lies a sordid tale.  The Python bindings for setting the
    # color scheme for ZVT arrays has never been usable.  We implement
    # several hacks here to attempt to set the color scheme, as the
    # default is abysmal.

    # This first attempt is the proper way.  If this binding ever gets
    # fixed, this is the code that should be called.

    try:
        term.set_color_scheme(zvt_color_array)
    except:
        try:
            # Alternative ZVT hack.  The current bug can supposedly be
            # worked around by changing all unsigned integer values
            # greater than 32767 to negative values by subtracting
            # 0xffff from them.

            for color_index in range(0, len(zvt_color_array)):
                new_color_tuple = []
                for color in zvt_color_array[color_index]:
                    if color > 32767:
                        color = color - 0xffff
                    new_color_tuple.append(color)
                zvt_color_array[color_index] = tuple(new_color_tuple)

            term.set_color_scheme(zvt_color_array)

        except:
            # Give up in disgust.
            pass

    hbox.pack_start(term)

    scrollbar = gtk.GtkVScrollbar(term.adjustment)
    hbox.pack_start(scrollbar)

    buttonbox = gtk.GtkHButtonBox()
    zvt_button = gtk.GtkButton("Cancel")
    zvt_button.connect("clicked", on_zvt_button_clicked, term)
    buttonbox.pack_start(zvt_button)
    vbox.pack_start(buttonbox)

    zvt_win.show_all()

    return term

# GTK signal functions.

def on_update_button_clicked(garbage):
    global apt_pid

    if apt_pid == -1:
        apt_pid = update_pkgs(setup_zvt())

    return gtk.TRUE

def on_install_button_clicked(garbage):
    global wtree
    global selection_state
    global tasks
    global pkgs
    global apt_pid
    global standard_installed

    if apt_pid == -1:
        tasks_to_install = []
        for taskname in selection_state.keys():
            if selection_state[taskname] and taskname != "standard":
                tasks_to_install.append(tasks[taskname])

        if not standard_installed:
            standard_pkgs = get_standard_list_cached()
        else:
            standard_pkgs = []

        if len(tasks_to_install) > 0 or len(standard_pkgs) > 0:
            apt_pid = install_pkgs(tasks_to_install, standard_pkgs,
                                   setup_zvt())

    return gtk.TRUE

def on_apt_terminated(term):
    global apt_pid
    global zvt_button

    apt_pid = -1
    term.feed("Package installation complete.  Click the Close button to continue.")
    if zvt_button is not None:
        zvt_button.children()[0].set_text("Close")

def on_zvt_button_clicked(button, term):
    global apt_pid
    global zvt_win
    global zvt_button
    global standard_installed

    if apt_pid >= 0:
        term.killchild(15)
    else:
        # XXX: For now, let's install the standard packages every time.
        #standard_installed = 1
        pass

    zvt_win.destroy()

    zvt_win = None
    zvt_button = None
    apt_pid = -1

def on_checkbox_toggled(checkbox, name):
    global selection_state
    global itemlist
    global wtree

    selection_state[name] = checkbox.get_active()
    wtree.get_widget("task_tree").select_child(itemlist[name])

def on_task_selected(item, name):
    global tasks
    global pkgs
    global previous_item
    global wtree

    package_list = wtree.get_widget("package_list")
    desc_label = wtree.get_widget("desc_label")

    # XXX: Turn off playing with the selections for now.
    if 0:
        if previous_item:
            previous_item.deselect()
        item.select()
        previous_item = item

    package_list.clear_items(0, -1)
    items = []
    if name == "none":
        desc_label.set_text("")
    elif name == "standard":
        for pkg in get_standard_list(pkgs):
            list_item = gtk.GtkListItem(pkg)
            list_item.show()
            items.append(list_item)
        package_list.append_items(items)
        desc_label.set_text(standard_desc)
    else:
        task = tasks[name]
        for pkg in task.get_packages():
            list_item = gtk.GtkListItem(pkg.get_name())
            list_item.show()
            items.append(list_item)
        package_list.append_items(items)
        desc_label.set_text(format_longdesc(task.get_longdesc()))

    return gtk.FALSE

# Status callback.

def statuswin_callback(status):
    global statuswin

    if not (status % 100):
        statuswin.update("%d" % (status,))

# Global settings for the interface portion of the file.

standard_desc = "All packages that are part of the standard Debian system."
pretty_section_desc = { "devel": "Development",
                        "l10n":  "Localization",
                        "misc":  "Miscellaneous",
                        "server": "Server",
                        "user":   "User" }

zvt_color_array = [ (0x0000, 0x0000, 0x0000), (0xaaaa, 0x0000, 0x0000),
                    (0x0000, 0xaaaa, 0x0000), (0xaaaa, 0x5555, 0x0000),
                    (0x0000, 0x0000, 0xaaaa), (0xaaaa, 0x0000, 0xaaaa),
                    (0x0000, 0xaaaa, 0xaaaa), (0xaaaa, 0xaaaa, 0xaaaa),
                    (0x5555, 0x5555, 0x5555), (0xffff, 0x5555, 0x5555),
                    (0x5555, 0xffff, 0x5555), (0xffff, 0xffff, 0x5555),
                    (0x5555, 0x5555, 0xffff), (0xffff, 0x5555, 0xffff),
                    (0x5555, 0xffff, 0xffff), (0xffff, 0xffff, 0xffff),
                    (0xaaaa, 0xaaaa, 0xaaaa), (0x0000, 0x0000, 0x0000) ]

zvt_normal_font = "-misc-fixed-medium-r-normal-*-*-120-*-*-c-*-*"

task_path = "/usr/share/tasksel"
packages_file = "/var/lib/dpkg/available"
glade_file_paths = [ "./gnome-tasksel.glade",
                     "/usr/share/gnome-tasksel/gnome-tasksel.glade" ]
previous_item = None
selection_state = {}
standard_list = []
standard_installed = 0
apt_pid = -1
zvt_win = None
zvt_button = None

# The main program.

def main():
    global wtree
    global statuswin
    global package_list
    global desc_label
    global tasks
    global pkgs
    global itemlist

    # Read the task and package meta-information.

    statuswin = StatusWindow()
    statuswin.show()

    print "Reading task lists..."
    statuswin.set_label("Reading task lists...")
    tasks = {}
    for fn in os.listdir(task_path):
        if (len(fn) <= 5) or (fn[-5:] != ".desc"):
            continue
        tasks.update(read_tasks(task_path + "/" + fn))

    print "Reading package list..."
    statuswin.set_label("Reading package list: ")
    pkgs = read_packages(packages_file, tasks, statuswin_callback)
    print "Extracting section list..."
    statuswin.set_label("Reading section list...")
    section_list = get_section_list(tasks)

    statuswin.hide()
    statuswin.close()

    # Load the glade interface.

    wtree = None
    for glade_file in glade_file_paths:
        if os.path.exists(glade_file):
            wtree = libglade.GladeXML(glade_file)
            break
    if wtree is None:
        sys.stderr.write("cannot find glade file\n")
        sys.exit(1)

    mainwin = wtree.get_widget("mainwin")
    task_tree = wtree.get_widget("task_tree")

    # Set the font for the description label.
    gtk.rc_parse_string('style "description" { font = "fixed" }')
    gtk.rc_parse_string('widget "mainwin.*.*.desc_label" style "description"')


    # Set up the task checkboxes.

    itemlist = {}
    for section in section_list:
        section_subtree = gtk.GtkTree()
        found_task = 0
        for taskkey in tasks.keys():
            task = tasks[taskkey]
            if task.get_section() == section and task.resolve_essential():
                found_task = 1

                task_item = gtk.GtkTreeItem()

                task_checkbox = gtk.GtkCheckButton()
                task_checkbox.connect("toggled", on_checkbox_toggled,
                                      task.get_name())
                task_item.connect("select",
                                  on_task_selected, task.get_name())

                task_label = gtk.GtkLabel(task.get_shortdesc())
                task_label.set_justify(gtk.JUSTIFY_LEFT)
                gtk.GtkWidget.set(task_label, {"xalign": 0.0})

                task_hbox = gtk.GtkHBox()
                task_hbox.pack_start(task_checkbox)
                task_hbox.set_child_packing(task_checkbox, gtk.FALSE,
                                            gtk.FALSE, 0, gtk.PACK_START)
                task_hbox.pack_start(task_label)
                task_hbox.set_child_packing(task_label, gtk.TRUE, gtk.TRUE,
                                            0, gtk.PACK_START)

                task_hbox.show_all()
                task_item.add(task_hbox)
                task_item.show()

                section_subtree.append(task_item)

                itemlist[task.get_name()] = task_item

        # Only show task section if it contains a task with packages
        # in it.

        if found_task:
            if pretty_section_desc.has_key(section):
                section_item = gtk.GtkTreeItem(pretty_section_desc[section])
            else:
                section_item = gtk.GtkTreeItem(section)
            section_item.connect("select", on_task_selected, "none")

            task_tree.append(section_item)
            section_item.set_subtree(section_subtree)

    # Connect signals.

    wtree.signal_autoconnect({ "gtk_main_quit": gtk.mainquit,
                               "on_update_button_clicked": on_update_button_clicked,
                               "on_install_button_clicked": on_install_button_clicked})

    # Get the ball rolling.

    task_tree.show_all()
    mainwin.show()
    gtk.mainloop()

# Run the main program.

if __name__ == "__main__":
    main()

# vim:ai:et:sts=4:sw=4:tw=0:
