kill version magic from setup.py, rename scripts to global names and move package data into the package
authorTero Marttila <terom@fixme.fi>
Sun, 13 Sep 2009 20:08:16 +0300
changeset 143 154d2d8ae9c0
parent 142 e163794ccf54
child 144 35c4c56f1376
kill version magic from setup.py, rename scripts to global names and move package data into the package
bin/index.cgi
bin/index.fcgi
bin/qmsk-irclogs-search-index
bin/qmsk-irclogs.cgi
bin/qmsk-irclogs.fcgi
bin/search-index
qmsk/irclogs/static/irclogs.css
qmsk/irclogs/templates/channel.tmpl
qmsk/irclogs/templates/channel_calendar.tmpl
qmsk/irclogs/templates/channel_date.tmpl
qmsk/irclogs/templates/channel_last.tmpl
qmsk/irclogs/templates/channel_search.tmpl
qmsk/irclogs/templates/inc_paginate.tmpl
qmsk/irclogs/templates/index.tmpl
qmsk/irclogs/templates/layout.tmpl
qmsk/irclogs/templates/lines.tmpl
qmsk/irclogs/templates/preferences.tmpl
setup.py
static/irclogs.css
templates/channel.tmpl
templates/channel_calendar.tmpl
templates/channel_date.tmpl
templates/channel_last.tmpl
templates/channel_search.tmpl
templates/inc_paginate.tmpl
templates/index.tmpl
templates/layout.tmpl
templates/lines.tmpl
templates/preferences.tmpl
--- a/bin/index.cgi	Sun Sep 13 18:57:48 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-#!/usr/bin/python2.5
-
-"""
-    CGI mode using qmsk.web.cgi
-"""
-
-def error () :
-    """
-        Dumps out a raw traceback of the current exception to stdout, call from except.
-
-        Used for low-level ImportError's
-    """
-    
-    import sys
-
-    # if this import fails, we're doomed
-    from qmsk.irclogs import error
-    
-    # format info
-    status, content_type, body = error.build_error()
-    
-    # HTTP headers+body
-    sys.stdout.write('Status: %s\r\n' % status)
-    sys.stdout.write('Content-type: %s\r\n' % content_type)
-    sys.stdout.write('\r\n')
-    sys.stdout.write(body)
-    
-def main () :
-    """
-        Build our wsgi.Application and run
-    """
-
-    try :
-        from qmsk.web import cgi_main
-        from qmsk.irclogs import wsgi
-
-        # create app
-        app = wsgi.Application()
-        
-        # run once
-        cgi_main.run(app)
-
-    except :
-        # display error on stdout
-        error()
-    
-if __name__ == '__main__' :
-    main()
-
--- a/bin/index.fcgi	Sun Sep 13 18:57:48 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-#!/usr/bin/python2.5
-# :set filetype=py
-
-"""
-    FastCGI mode using qmsk.web.fastcgi_main
-"""
-
-from qmsk.web import fastcgi_main
-
-# XXX: error handling for imports? Lighttp sucks hard at this
-from qmsk.irclogs import wsgi
-
-def main () :
-    """
-        Build our WSGIApplication and run
-    """
-
-    # create app
-    app = wsgi.Application()
-
-    # run once
-    fastcgi_main.run(app)
-
-if __name__ == '__main__' :
-    main()
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/qmsk-irclogs-search-index	Sun Sep 13 20:08:16 2009 +0300
@@ -0,0 +1,640 @@
+#!/usr/bin/env python2.5
+
+"""
+    Tool for accessing the search index
+"""
+
+# XXX: fix path
+import sys; sys.path.insert(0, '.'); sys.path.insert(0, '..')
+
+import os, os.path, fcntl
+import datetime, pytz
+import optparse
+
+# configuration and the LogSearchIndex module
+from qmsk.irclogs import config, utils, log_search, channels
+
+def _open_index (options, open_mode) :
+    """
+        Opens the LogSearchIndex
+    """
+
+    return log_search.LogSearchIndex(config.LOG_CHANNELS, options.index_path, open_mode)
+
+
+def _open_index_and_channel (options, channel_name, open_mode) :
+    """
+        Opens+returns a LogSearchIndex and a LogChannel
+    """
+    
+    # open the LogSearchIndex
+    index = _open_index(options, open_mode)
+
+    # open the channel
+    channel = config.LOG_CHANNELS.lookup(channel_name)
+    
+    # return
+    return index, channel
+
+def _iter_insert_stats (index, channel, lines) :
+    """
+        Insert the given lines into the index.
+
+        Assumes the lines will be in time-order, and yields a series of (date, count) tuples for every date that lines
+        are inserted for
+    """
+
+    # last date
+    date = None
+
+    # count
+    count = 0
+
+    # iter lines
+    for line in lines :
+        # next day?
+        if not date or line.timestamp.date() != date :
+            if date :
+                # yield stats
+                yield date, count
+
+            # reset count
+            count = 0
+
+            # timestamp's date
+            date = line.timestamp.date()
+
+        # insert
+        index.insert_line(channel, line)
+
+        # count
+        count += 1
+    
+    # final count?
+    if date and count :
+        yield date, count
+
+def _insert_lines (index, options, channel, lines) :
+    """
+        Insert the given lines into the index.
+
+        Assumes the lines will be in time-order, and prints out as status messages the date and count for the inserted lines
+    """
+    
+    # iterate insert stats
+    for date, count in _iter_insert_stats(index, channel, lines) :
+        # output date header?
+        if not options.quiet :
+            print "%s: %s" % (date.strftime('%Y-%m-%d'), count),
+
+def _load_channel_date (index, options, channel, date) :
+    """
+        Loads the logs for the given date from the channel's LogSource into the given LogSearchIndex
+    """
+
+    if not options.quiet :
+        print "Loading date for channel %s" % channel.id
+        
+    try :
+        # load lines for date
+        lines = channel.source.get_date(date)
+    
+    except Exception, e :
+        if not options.skip_missing :
+            raise
+            
+        if not options.quiet :
+            print "\tSkipped: %s" % (e, )
+    
+    else :
+        # insert
+        _insert_lines(index, options, channel, lines)
+
+def _parse_date (options, date_str, tz=None, fmt='%Y-%m-%d') :
+    """
+        Parse the given datetime, using the given timezone(defaults to options.tz) and format
+    """
+
+    # default tz
+    if not tz :
+        tz = options.timezone
+
+    try :
+        # parse
+        return datetime.datetime.strptime(date_str, fmt).replace(tzinfo=tz)
+
+    except Exception, e :
+        raise CommandError("[ERROR] Invalid date: %s: %s" % (date_str, e))
+
+def _output_lines (options, lines) :
+    """
+        Display the formatted LogLines
+    """
+
+    # display as plaintext
+    for line, txt_data in options.formatter.format_txt(lines, full_timestamps=True) :
+        print txt_data
+
+class CommandError (Exception) :
+    """
+        Error with command-line arguments
+    """
+
+    pass
+
+def cmd_create (options) :
+    """
+        Creates a new index
+    """
+
+    # open index
+    index = _open_index(options, 'ctrunc' if options.force else 'c')
+
+    # that's all
+    pass
+
+def cmd_load (options, channel_name, *dates) :
+    """
+        Loads the logs for a specific channel for the given dates (in terms of the channe logs' timezone) into the index
+    """
+
+    # open index/channel
+    index, channel = _open_index_and_channel(options, channel_name, 'c' if options.create else 'a')
+    
+    # handle each date
+    for date_str in dates :
+        # prase date
+        try :
+            date = _parse_date(options, date_str, channel.source.tz)
+        
+        # handle errors
+        except CommandError, e :
+            if options.skip_missing :
+                print "[ERROR] %s" % (date_name, e)
+
+            else :
+                raise
+        
+        # otherwise, load
+        else :        
+            _load_channel_date(index, options, channel, date)
+
+def cmd_load_month (options, channel_name, *months) :
+    """
+        Loads the logs for a specific channel for the given months (in terms of the channel's timezone) into the index
+    """
+
+    # open index/channel
+    index, channel = _open_index_and_channel(options, channel_name, 'c' if options.create else 'a')
+    
+    # handle each date
+    for month_str in months :
+        # prase date
+        try :
+            month = _parse_date(options, month_str, channel.source.tz, '%Y-%m')
+        
+        # handle errors
+        except CommandError, e :
+            # skip?
+            if options.skip_missing :
+                if not options.quiet :
+                    print "[ERROR] %s" % (date_name, e)
+                continue
+
+            else :
+                raise
+        
+        # get the set of days
+        days = list(channel.source.get_month_days(month))
+        
+        if not options.quiet :
+            print "Loading %d days of logs:" % (len(days))
+
+        # load each day
+        for date in days :
+            # convert to datetime
+            dt = datetime.datetime.combine(date, datetime.time(0)).replace(tzinfo=channel.source.tz)
+            
+            # load
+            _load_channel_date(index, options, channel, dt)
+
+def cmd_search (options, channel_name, query) :
+    """
+        Search the index for events on a specific channel with the given query
+    """
+    
+    # sanity-check
+    if options.create :
+        raise Exception("--create doesn't make sense for 'search'")
+    
+    # open index/channel
+    index, channel = _open_index_and_channel(options, channel_name, 'r')
+    
+    # search
+    lines = index.search_simple(channel, query)
+    
+    # display
+    _output_lines(options, lines)
+
+def cmd_list (options, channel_name, *dates) :
+    """
+        List the indexed events for a specific date
+    """
+
+    # sanity-check
+    if options.create :
+        raise Exception("--create doesn't make sense for 'search'")
+    
+    # open index/channel
+    index, channel = _open_index_and_channel(options, channel_name, 'r')
+
+    # ...for each date
+    for date_str in dates :
+        # parse date
+        date = _parse_date(options, date_str)
+
+        # list
+        lines = index.list(channel, date)
+        
+        # display
+        _output_lines(options, lines)
+
+def _autoload_reset (options, channels) :
+    """
+        Reset old autoload state
+    """
+    
+    # warn
+    if not options.quiet :
+        print "[WARN] Resetting autoload state for: %s" % ', '.join(channel.id for channel in channels)
+    
+    # iter
+    for channel in channels :
+        # statefile path
+        statefile_path = os.path.join(options.autoload_state_path, 'chan-%s' % channel.id)
+
+        # is it present?
+        if not os.path.exists(statefile_path) :
+            if not options.quiet :
+                print "[WARN] No statefile found at %s" % statefile_path
+        
+        else :
+            if not options.quiet :
+                print "\t%s: " % channel.id,
+
+            # remove the statefile
+            os.remove(statefile_path)
+            
+            if not options.quiet :
+                print "OK"
+
+def cmd_autoload (options, *channel_names) :
+    """
+        Automatically loads all channel logs that have not been indexed yet (by logfile mtime)
+    """
+    
+    # open index, nonblocking
+    index = _open_index(options, 'c?' if options.create else 'a?')
+
+    # default to all channels
+    if not channel_names :
+        channels = config.LOG_CHANNELS
+    
+    else :
+        channels = [config.LOG_CHANNELS.lookup(channel_name) for channel_name in channel_names]
+    
+    # reset autoload state?
+    if options.reset :
+        _autoload_reset(options, channels)
+        if not options.quiet :
+            print
+
+    # iterate channels
+    for channel in channels :
+        if not options.quiet :
+            print "Channel %s:" % channel.id
+
+        # no 'from' by default
+        after = None
+
+        # path to our state file
+        statefile_path = os.path.join(options.autoload_state_path, 'chan-%s' % channel.id)
+        statefile_tmppath = statefile_path + '.tmp'
+
+        # does it exist?
+        have_tmpfile = os.path.exists(statefile_tmppath)
+        
+        # do we have a tempfile from a previous crash?
+        if have_tmpfile and not options.ignore_resume :
+            # first, open it...
+            statefile_tmp = open(statefile_tmppath, 'r+')
+
+            # ... then lock it
+            fcntl.lockf(statefile_tmp, fcntl.LOCK_EX | fcntl.LOCK_NB)
+            
+            # read after timestamp
+            after_str = statefile_tmp.read().rstrip()
+
+            if after_str :
+                # parse timestamp
+                after = utils.from_utc_timestamp(int(after_str))
+
+                if not options.quiet :
+                    print "\tContinuing earlier progress from %s" % after
+
+            else :
+                # ignore
+                if not options.quiet :
+                    print "\t[WARN] Ignoring empty temporary statefile"
+
+        else :
+            # warn about old tmpfile that was ignored
+            if have_tmpfile and not options.quiet :
+                print "\t[WARN] Ignoring old tmpfile state"
+
+            # open new tempfile
+            statefile_tmp = open(statefile_tmppath, 'w')
+            
+            # lock
+            fcntl.lockf(statefile_tmp, fcntl.LOCK_EX | fcntl.LOCK_NB)
+
+        # override?
+        if options.reload :
+            # load all
+            mtime = None
+
+            if not options.quiet :
+                print "\tForcing reload!"
+
+        # stat for mtime
+        else :
+            # stat for mtime, None if unknown
+            mtime = utils.mtime(statefile_path, ignore_missing=True)
+
+            if mtime and not options.quiet :
+                print "\tLast load time was %s" % mtime
+
+            elif not options.quiet :
+                print "\t[WARN] No previous load state! Loading full logs"
+ 
+        # only after some specific date?
+        if options.after :
+            # use unless read from tempfile
+            if not after :
+                after = options.after
+               
+                if not options.quiet :
+                    print "\tOnly including dates from %s onwards" % after
+            
+            else :
+                if not options.quiet :
+                    print "\t[WARN] Ignoring --from because we found a tempfile"
+            
+        # only up to some specific date?
+        if options.until :
+            until = options.until
+
+            if not options.quiet :
+                print "\tOnly including dates up to (and including) %s" % until
+        else :
+            # default to now
+            until = None
+
+        # get lines
+        lines = channel.source.get_modified(mtime, after, until)
+        
+        # insert
+        if not options.quiet :
+            print "\tLoading and inserting..."
+            print
+     
+        # iterate insert() per day to display info and update progress
+        for date, count in _iter_insert_stats(index, channel, lines) :
+            # output date header?
+            if not options.quiet :
+                print "\t%10s: %d" % (date.strftime('%Y-%m-%d'), count)
+            
+            # write temp state
+            statefile_tmp.seek(0)
+            statefile_tmp.write(str(utils.to_utc_timestamp(datetime.datetime.combine(date, datetime.time(0)))))
+            statefile_tmp.flush()
+
+        # write autoload state
+        open(statefile_path, 'w').close()
+
+        # close+delete tempfile
+        statefile_tmp.close()
+        os.remove(statefile_tmppath)
+        
+        if not options.quiet :
+            print
+    
+    # done
+    return
+
+def cmd_help (options, *args) :
+    """
+        Help about commands
+    """
+
+    import inspect
+    
+    # general help stuff
+    options._parser.print_help()
+
+    # specific command?
+    if args :
+        # the command name
+        command, = args
+        
+        # XXX: display info about specific command
+        xxx
+    
+    # general
+    else :
+        print
+        print "Available commands:"
+
+        # build list of all cmd_* objects
+        cmd_objects = [(name, obj) for name, obj in globals().iteritems() if name.startswith('cmd_') and inspect.isfunction(obj)]
+
+        # sort alphabetically
+        cmd_objects.sort()
+        
+        # iterate through all cmd_* objects
+        for cmd_func_name, cmd_func in cmd_objects :
+            # remove cmd_ prefix
+            cmd_name = cmd_func_name[4:]
+
+            # inspect
+            cmd_args, cmd_varargs, cmd_varkw, cmd_default = inspect.getargspec(cmd_func)
+            cmd_doc = inspect.getdoc(cmd_func)
+
+            # remove the "options" arg
+            cmd_args = cmd_args[1:]
+
+            # display
+            print "\t%10s %-30s : %s" % (cmd_name, inspect.formatargspec(cmd_args, cmd_varargs, None, cmd_default), cmd_doc)
+
+class MyOption (optparse.Option) :
+    """
+        Our custom types for optparse
+    """
+
+    def check_date (option, opt, value) :
+        """
+            Parse a date
+        """
+
+        try :
+            # parse
+            return datetime.datetime.strptime(value, '%Y-%m-%d')
+        
+        # trap -> OptionValueError
+        except Exception, e :
+            raise optparse.OptionValueError("option %s: invalid date value: %r" % (opt, value))
+    
+    def check_timezone (option, opt, value) :
+        """
+            Parse a timezone
+        """
+
+        try :
+            # parse
+            return pytz.timezone(value)
+        
+        # trap -> OptionValueError
+        except Exception, e :
+            raise optparse.OptionValueError("option %s: invalid timezone: %r" % (opt, value))
+
+    def take_action (self, action, dest, opt, value, values, parser) :
+        """
+            Override take_action to handle date
+        """
+
+        if action == "parse_date" :
+            # get timezone
+            tz = values.timezone
+
+            # set timezone
+            value = value.replace(tzinfo=tz)
+
+            # store
+            return optparse.Option.take_action(self, 'store', dest, opt, value, values, parser)
+
+        else :
+            # default
+            return optparse.Option.take_action(self, action, dest, opt, value, values, parser)
+
+    TYPES = optparse.Option.TYPES + ('date', 'timezone')
+    TYPE_CHECKER = optparse.Option.TYPE_CHECKER.copy()
+    TYPE_CHECKER['date'] = check_date
+    TYPE_CHECKER['timezone'] = check_timezone
+    ACTIONS = optparse.Option.ACTIONS + ('parse_date', )
+    STORE_ACTIONS = optparse.Option.STORE_ACTIONS + ('parse_date', )
+    TYPED_ACTIONS = optparse.Option.TYPED_ACTIONS + ('parse_date', )
+    ACTIONS = optparse.Option.ACTIONS + ('parse_date', )
+
+def main (argv) :
+    """
+        Command-line main, with given argv
+    """
+
+    # define parser
+    parser = optparse.OptionParser(
+        usage           = "%prog [options] <command> [ ... ]",
+        add_help_option = False,
+        option_class    = MyOption,
+    )
+
+    # general options       #                   #                       #                                   #
+    general = optparse.OptionGroup(parser, "General Options")
+    general.add_option('-h', "--help",          dest="help",            help="Show this help message and exit",     
+                                                action="store_true"                                         )
+
+    general.add_option(     "--formatter",      dest="formatter_name",  help="LogFormatter to use",                 
+            metavar="FMT",  type="choice",                              default=config.PREF_FORMATTER_DEFAULT.name,
+            choices=[fmt_name for fmt_name in config.LOG_FORMATTERS.iterkeys()]                             )
+
+    general.add_option(     "--index",          dest="index_path",      help="Index database path",                 
+            metavar="PATH",                                             default=config.SEARCH_INDEX_PATH    )
+
+    general.add_option(     "--timezone",       dest="timezone",        help="Timezone for output",                 
+            metavar="TZ",   type="timezone",                            default=pytz.utc                    )
+
+    general.add_option(     "--force",          dest="force",           help="Force dangerous operation",           
+                                                action="store_true"                                         )
+
+    general.add_option(     "--quiet",          dest="quiet",           help="Supress status messages",             
+                                                action="store_true"                                         )
+    parser.add_option_group(general)
+    
+
+    # cmd_load options      #                   #                       #                                   #
+    load = optparse.OptionGroup(parser, "Load Options")
+    load.add_option(        "--skip-missing",   dest="skip_missing",    help="Skip missing logfiles",
+                                                action="store_true"                                         )
+
+    load.add_option(        "--create",         dest="create",          help="Create index database", 
+                                                action="store_true"                                         )
+    parser.add_option_group(load)
+    
+
+    # cmd_autoload options  #                   #                       #                                   #
+    autoload = optparse.OptionGroup(parser, "Autoload Options")
+    autoload.add_option(    "--autoload-state", dest="autoload_state_path", help="Path to autoload state dir",      
+            metavar="PATH",                                             default=config.SEARCH_AUTOINDEX_PATH)
+
+    autoload.add_option(    "--from",           dest="after",           help="Only autoload logfiles from the given date on", 
+            metavar="DATE", type="date",        action="parse_date",    default=None                        )
+
+    autoload.add_option(    "--until",          dest="until",           help="Only autoload logfiles up to (and including) the given date",  
+            metavar="DATE", type="date",        action="parse_date",    default=None                        )
+
+    autoload.add_option(    "--reload",         dest="reload",          help="Force reload lines",
+                                                action="store_true"                                         )
+
+    autoload.add_option(    "--reset",          dest="reset",           help="Reset old autload state",
+                                                action="store_true"                                         )
+
+    autoload.add_option(    "--ignore-resume",  dest="ignore_resume",   help="Do not try and resume interrupted autoload",  
+                                                action="store_true"                                         )
+    parser.add_option_group(autoload)
+
+    # parse
+    options, args = parser.parse_args(argv[1:])
+
+    # postprocess stuff
+    options._parser = parser
+    options.formatter = config.LOG_FORMATTERS[options.formatter_name](options.timezone, "%H:%M:%S", None, None)
+
+    # special-case --help
+    if options.help :
+        return cmd_help(options, *args)
+    
+    # must have at least the command argument
+    if not args :
+        raise CommandError("Missing command")
+    
+    # pop command
+    command = args.pop(0)
+    
+    # get func
+    func = globals().get('cmd_%s' % command)
+    
+    # unknown command?
+    if not func :
+        raise CommandError("Unknown command: %s" % command)
+    
+    # call
+    func(options, *args)
+
+if __name__ == '__main__' :
+    try :
+        main(sys.argv)
+        sys.exit(0)
+
+    except CommandError, e :
+        print e
+        sys.exit(1)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/qmsk-irclogs.cgi	Sun Sep 13 20:08:16 2009 +0300
@@ -0,0 +1,49 @@
+#!/usr/bin/python2.5
+
+"""
+    CGI mode using qmsk.web.cgi
+"""
+
+def error () :
+    """
+        Dumps out a raw traceback of the current exception to stdout, call from except.
+
+        Used for low-level ImportError's
+    """
+    
+    import sys
+
+    # if this import fails, we're doomed
+    from qmsk.irclogs import error
+    
+    # format info
+    status, content_type, body = error.build_error()
+    
+    # HTTP headers+body
+    sys.stdout.write('Status: %s\r\n' % status)
+    sys.stdout.write('Content-type: %s\r\n' % content_type)
+    sys.stdout.write('\r\n')
+    sys.stdout.write(body)
+    
+def main () :
+    """
+        Build our wsgi.Application and run
+    """
+
+    try :
+        from qmsk.web import cgi_main
+        from qmsk.irclogs import wsgi
+
+        # create app
+        app = wsgi.Application()
+        
+        # run once
+        cgi_main.run(app)
+
+    except :
+        # display error on stdout
+        error()
+    
+if __name__ == '__main__' :
+    main()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/qmsk-irclogs.fcgi	Sun Sep 13 20:08:16 2009 +0300
@@ -0,0 +1,26 @@
+#!/usr/bin/python2.5
+# :set filetype=py
+
+"""
+    FastCGI mode using qmsk.web.fastcgi_main
+"""
+
+from qmsk.web import fastcgi_main
+
+# XXX: error handling for imports? Lighttp sucks hard at this
+from qmsk.irclogs import wsgi
+
+def main () :
+    """
+        Build our WSGIApplication and run
+    """
+
+    # create app
+    app = wsgi.Application()
+
+    # run once
+    fastcgi_main.run(app)
+
+if __name__ == '__main__' :
+    main()
+
--- a/bin/search-index	Sun Sep 13 18:57:48 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,640 +0,0 @@
-#!/usr/bin/env python2.5
-
-"""
-    Tool for accessing the search index
-"""
-
-# XXX: fix path
-import sys; sys.path.insert(0, '.'); sys.path.insert(0, '..')
-
-import os, os.path, fcntl
-import datetime, pytz
-import optparse
-
-# configuration and the LogSearchIndex module
-from qmsk.irclogs import config, utils, log_search, channels
-
-def _open_index (options, open_mode) :
-    """
-        Opens the LogSearchIndex
-    """
-
-    return log_search.LogSearchIndex(config.LOG_CHANNELS, options.index_path, open_mode)
-
-
-def _open_index_and_channel (options, channel_name, open_mode) :
-    """
-        Opens+returns a LogSearchIndex and a LogChannel
-    """
-    
-    # open the LogSearchIndex
-    index = _open_index(options, open_mode)
-
-    # open the channel
-    channel = config.LOG_CHANNELS.lookup(channel_name)
-    
-    # return
-    return index, channel
-
-def _iter_insert_stats (index, channel, lines) :
-    """
-        Insert the given lines into the index.
-
-        Assumes the lines will be in time-order, and yields a series of (date, count) tuples for every date that lines
-        are inserted for
-    """
-
-    # last date
-    date = None
-
-    # count
-    count = 0
-
-    # iter lines
-    for line in lines :
-        # next day?
-        if not date or line.timestamp.date() != date :
-            if date :
-                # yield stats
-                yield date, count
-
-            # reset count
-            count = 0
-
-            # timestamp's date
-            date = line.timestamp.date()
-
-        # insert
-        index.insert_line(channel, line)
-
-        # count
-        count += 1
-    
-    # final count?
-    if date and count :
-        yield date, count
-
-def _insert_lines (index, options, channel, lines) :
-    """
-        Insert the given lines into the index.
-
-        Assumes the lines will be in time-order, and prints out as status messages the date and count for the inserted lines
-    """
-    
-    # iterate insert stats
-    for date, count in _iter_insert_stats(index, channel, lines) :
-        # output date header?
-        if not options.quiet :
-            print "%s: %s" % (date.strftime('%Y-%m-%d'), count),
-
-def _load_channel_date (index, options, channel, date) :
-    """
-        Loads the logs for the given date from the channel's LogSource into the given LogSearchIndex
-    """
-
-    if not options.quiet :
-        print "Loading date for channel %s" % channel.id
-        
-    try :
-        # load lines for date
-        lines = channel.source.get_date(date)
-    
-    except Exception, e :
-        if not options.skip_missing :
-            raise
-            
-        if not options.quiet :
-            print "\tSkipped: %s" % (e, )
-    
-    else :
-        # insert
-        _insert_lines(index, options, channel, lines)
-
-def _parse_date (options, date_str, tz=None, fmt='%Y-%m-%d') :
-    """
-        Parse the given datetime, using the given timezone(defaults to options.tz) and format
-    """
-
-    # default tz
-    if not tz :
-        tz = options.timezone
-
-    try :
-        # parse
-        return datetime.datetime.strptime(date_str, fmt).replace(tzinfo=tz)
-
-    except Exception, e :
-        raise CommandError("[ERROR] Invalid date: %s: %s" % (date_str, e))
-
-def _output_lines (options, lines) :
-    """
-        Display the formatted LogLines
-    """
-
-    # display as plaintext
-    for line, txt_data in options.formatter.format_txt(lines, full_timestamps=True) :
-        print txt_data
-
-class CommandError (Exception) :
-    """
-        Error with command-line arguments
-    """
-
-    pass
-
-def cmd_create (options) :
-    """
-        Creates a new index
-    """
-
-    # open index
-    index = _open_index(options, 'ctrunc' if options.force else 'c')
-
-    # that's all
-    pass
-
-def cmd_load (options, channel_name, *dates) :
-    """
-        Loads the logs for a specific channel for the given dates (in terms of the channe logs' timezone) into the index
-    """
-
-    # open index/channel
-    index, channel = _open_index_and_channel(options, channel_name, 'c' if options.create else 'a')
-    
-    # handle each date
-    for date_str in dates :
-        # prase date
-        try :
-            date = _parse_date(options, date_str, channel.source.tz)
-        
-        # handle errors
-        except CommandError, e :
-            if options.skip_missing :
-                print "[ERROR] %s" % (date_name, e)
-
-            else :
-                raise
-        
-        # otherwise, load
-        else :        
-            _load_channel_date(index, options, channel, date)
-
-def cmd_load_month (options, channel_name, *months) :
-    """
-        Loads the logs for a specific channel for the given months (in terms of the channel's timezone) into the index
-    """
-
-    # open index/channel
-    index, channel = _open_index_and_channel(options, channel_name, 'c' if options.create else 'a')
-    
-    # handle each date
-    for month_str in months :
-        # prase date
-        try :
-            month = _parse_date(options, month_str, channel.source.tz, '%Y-%m')
-        
-        # handle errors
-        except CommandError, e :
-            # skip?
-            if options.skip_missing :
-                if not options.quiet :
-                    print "[ERROR] %s" % (date_name, e)
-                continue
-
-            else :
-                raise
-        
-        # get the set of days
-        days = list(channel.source.get_month_days(month))
-        
-        if not options.quiet :
-            print "Loading %d days of logs:" % (len(days))
-
-        # load each day
-        for date in days :
-            # convert to datetime
-            dt = datetime.datetime.combine(date, datetime.time(0)).replace(tzinfo=channel.source.tz)
-            
-            # load
-            _load_channel_date(index, options, channel, dt)
-
-def cmd_search (options, channel_name, query) :
-    """
-        Search the index for events on a specific channel with the given query
-    """
-    
-    # sanity-check
-    if options.create :
-        raise Exception("--create doesn't make sense for 'search'")
-    
-    # open index/channel
-    index, channel = _open_index_and_channel(options, channel_name, 'r')
-    
-    # search
-    lines = index.search_simple(channel, query)
-    
-    # display
-    _output_lines(options, lines)
-
-def cmd_list (options, channel_name, *dates) :
-    """
-        List the indexed events for a specific date
-    """
-
-    # sanity-check
-    if options.create :
-        raise Exception("--create doesn't make sense for 'search'")
-    
-    # open index/channel
-    index, channel = _open_index_and_channel(options, channel_name, 'r')
-
-    # ...for each date
-    for date_str in dates :
-        # parse date
-        date = _parse_date(options, date_str)
-
-        # list
-        lines = index.list(channel, date)
-        
-        # display
-        _output_lines(options, lines)
-
-def _autoload_reset (options, channels) :
-    """
-        Reset old autoload state
-    """
-    
-    # warn
-    if not options.quiet :
-        print "[WARN] Resetting autoload state for: %s" % ', '.join(channel.id for channel in channels)
-    
-    # iter
-    for channel in channels :
-        # statefile path
-        statefile_path = os.path.join(options.autoload_state_path, 'chan-%s' % channel.id)
-
-        # is it present?
-        if not os.path.exists(statefile_path) :
-            if not options.quiet :
-                print "[WARN] No statefile found at %s" % statefile_path
-        
-        else :
-            if not options.quiet :
-                print "\t%s: " % channel.id,
-
-            # remove the statefile
-            os.remove(statefile_path)
-            
-            if not options.quiet :
-                print "OK"
-
-def cmd_autoload (options, *channel_names) :
-    """
-        Automatically loads all channel logs that have not been indexed yet (by logfile mtime)
-    """
-    
-    # open index, nonblocking
-    index = _open_index(options, 'c?' if options.create else 'a?')
-
-    # default to all channels
-    if not channel_names :
-        channels = config.LOG_CHANNELS
-    
-    else :
-        channels = [config.LOG_CHANNELS.lookup(channel_name) for channel_name in channel_names]
-    
-    # reset autoload state?
-    if options.reset :
-        _autoload_reset(options, channels)
-        if not options.quiet :
-            print
-
-    # iterate channels
-    for channel in channels :
-        if not options.quiet :
-            print "Channel %s:" % channel.id
-
-        # no 'from' by default
-        after = None
-
-        # path to our state file
-        statefile_path = os.path.join(options.autoload_state_path, 'chan-%s' % channel.id)
-        statefile_tmppath = statefile_path + '.tmp'
-
-        # does it exist?
-        have_tmpfile = os.path.exists(statefile_tmppath)
-        
-        # do we have a tempfile from a previous crash?
-        if have_tmpfile and not options.ignore_resume :
-            # first, open it...
-            statefile_tmp = open(statefile_tmppath, 'r+')
-
-            # ... then lock it
-            fcntl.lockf(statefile_tmp, fcntl.LOCK_EX | fcntl.LOCK_NB)
-            
-            # read after timestamp
-            after_str = statefile_tmp.read().rstrip()
-
-            if after_str :
-                # parse timestamp
-                after = utils.from_utc_timestamp(int(after_str))
-
-                if not options.quiet :
-                    print "\tContinuing earlier progress from %s" % after
-
-            else :
-                # ignore
-                if not options.quiet :
-                    print "\t[WARN] Ignoring empty temporary statefile"
-
-        else :
-            # warn about old tmpfile that was ignored
-            if have_tmpfile and not options.quiet :
-                print "\t[WARN] Ignoring old tmpfile state"
-
-            # open new tempfile
-            statefile_tmp = open(statefile_tmppath, 'w')
-            
-            # lock
-            fcntl.lockf(statefile_tmp, fcntl.LOCK_EX | fcntl.LOCK_NB)
-
-        # override?
-        if options.reload :
-            # load all
-            mtime = None
-
-            if not options.quiet :
-                print "\tForcing reload!"
-
-        # stat for mtime
-        else :
-            # stat for mtime, None if unknown
-            mtime = utils.mtime(statefile_path, ignore_missing=True)
-
-            if mtime and not options.quiet :
-                print "\tLast load time was %s" % mtime
-
-            elif not options.quiet :
-                print "\t[WARN] No previous load state! Loading full logs"
- 
-        # only after some specific date?
-        if options.after :
-            # use unless read from tempfile
-            if not after :
-                after = options.after
-               
-                if not options.quiet :
-                    print "\tOnly including dates from %s onwards" % after
-            
-            else :
-                if not options.quiet :
-                    print "\t[WARN] Ignoring --from because we found a tempfile"
-            
-        # only up to some specific date?
-        if options.until :
-            until = options.until
-
-            if not options.quiet :
-                print "\tOnly including dates up to (and including) %s" % until
-        else :
-            # default to now
-            until = None
-
-        # get lines
-        lines = channel.source.get_modified(mtime, after, until)
-        
-        # insert
-        if not options.quiet :
-            print "\tLoading and inserting..."
-            print
-     
-        # iterate insert() per day to display info and update progress
-        for date, count in _iter_insert_stats(index, channel, lines) :
-            # output date header?
-            if not options.quiet :
-                print "\t%10s: %d" % (date.strftime('%Y-%m-%d'), count)
-            
-            # write temp state
-            statefile_tmp.seek(0)
-            statefile_tmp.write(str(utils.to_utc_timestamp(datetime.datetime.combine(date, datetime.time(0)))))
-            statefile_tmp.flush()
-
-        # write autoload state
-        open(statefile_path, 'w').close()
-
-        # close+delete tempfile
-        statefile_tmp.close()
-        os.remove(statefile_tmppath)
-        
-        if not options.quiet :
-            print
-    
-    # done
-    return
-
-def cmd_help (options, *args) :
-    """
-        Help about commands
-    """
-
-    import inspect
-    
-    # general help stuff
-    options._parser.print_help()
-
-    # specific command?
-    if args :
-        # the command name
-        command, = args
-        
-        # XXX: display info about specific command
-        xxx
-    
-    # general
-    else :
-        print
-        print "Available commands:"
-
-        # build list of all cmd_* objects
-        cmd_objects = [(name, obj) for name, obj in globals().iteritems() if name.startswith('cmd_') and inspect.isfunction(obj)]
-
-        # sort alphabetically
-        cmd_objects.sort()
-        
-        # iterate through all cmd_* objects
-        for cmd_func_name, cmd_func in cmd_objects :
-            # remove cmd_ prefix
-            cmd_name = cmd_func_name[4:]
-
-            # inspect
-            cmd_args, cmd_varargs, cmd_varkw, cmd_default = inspect.getargspec(cmd_func)
-            cmd_doc = inspect.getdoc(cmd_func)
-
-            # remove the "options" arg
-            cmd_args = cmd_args[1:]
-
-            # display
-            print "\t%10s %-30s : %s" % (cmd_name, inspect.formatargspec(cmd_args, cmd_varargs, None, cmd_default), cmd_doc)
-
-class MyOption (optparse.Option) :
-    """
-        Our custom types for optparse
-    """
-
-    def check_date (option, opt, value) :
-        """
-            Parse a date
-        """
-
-        try :
-            # parse
-            return datetime.datetime.strptime(value, '%Y-%m-%d')
-        
-        # trap -> OptionValueError
-        except Exception, e :
-            raise optparse.OptionValueError("option %s: invalid date value: %r" % (opt, value))
-    
-    def check_timezone (option, opt, value) :
-        """
-            Parse a timezone
-        """
-
-        try :
-            # parse
-            return pytz.timezone(value)
-        
-        # trap -> OptionValueError
-        except Exception, e :
-            raise optparse.OptionValueError("option %s: invalid timezone: %r" % (opt, value))
-
-    def take_action (self, action, dest, opt, value, values, parser) :
-        """
-            Override take_action to handle date
-        """
-
-        if action == "parse_date" :
-            # get timezone
-            tz = values.timezone
-
-            # set timezone
-            value = value.replace(tzinfo=tz)
-
-            # store
-            return optparse.Option.take_action(self, 'store', dest, opt, value, values, parser)
-
-        else :
-            # default
-            return optparse.Option.take_action(self, action, dest, opt, value, values, parser)
-
-    TYPES = optparse.Option.TYPES + ('date', 'timezone')
-    TYPE_CHECKER = optparse.Option.TYPE_CHECKER.copy()
-    TYPE_CHECKER['date'] = check_date
-    TYPE_CHECKER['timezone'] = check_timezone
-    ACTIONS = optparse.Option.ACTIONS + ('parse_date', )
-    STORE_ACTIONS = optparse.Option.STORE_ACTIONS + ('parse_date', )
-    TYPED_ACTIONS = optparse.Option.TYPED_ACTIONS + ('parse_date', )
-    ACTIONS = optparse.Option.ACTIONS + ('parse_date', )
-
-def main (argv) :
-    """
-        Command-line main, with given argv
-    """
-
-    # define parser
-    parser = optparse.OptionParser(
-        usage           = "%prog [options] <command> [ ... ]",
-        add_help_option = False,
-        option_class    = MyOption,
-    )
-
-    # general options       #                   #                       #                                   #
-    general = optparse.OptionGroup(parser, "General Options")
-    general.add_option('-h', "--help",          dest="help",            help="Show this help message and exit",     
-                                                action="store_true"                                         )
-
-    general.add_option(     "--formatter",      dest="formatter_name",  help="LogFormatter to use",                 
-            metavar="FMT",  type="choice",                              default=config.PREF_FORMATTER_DEFAULT.name,
-            choices=[fmt_name for fmt_name in config.LOG_FORMATTERS.iterkeys()]                             )
-
-    general.add_option(     "--index",          dest="index_path",      help="Index database path",                 
-            metavar="PATH",                                             default=config.SEARCH_INDEX_PATH    )
-
-    general.add_option(     "--timezone",       dest="timezone",        help="Timezone for output",                 
-            metavar="TZ",   type="timezone",                            default=pytz.utc                    )
-
-    general.add_option(     "--force",          dest="force",           help="Force dangerous operation",           
-                                                action="store_true"                                         )
-
-    general.add_option(     "--quiet",          dest="quiet",           help="Supress status messages",             
-                                                action="store_true"                                         )
-    parser.add_option_group(general)
-    
-
-    # cmd_load options      #                   #                       #                                   #
-    load = optparse.OptionGroup(parser, "Load Options")
-    load.add_option(        "--skip-missing",   dest="skip_missing",    help="Skip missing logfiles",
-                                                action="store_true"                                         )
-
-    load.add_option(        "--create",         dest="create",          help="Create index database", 
-                                                action="store_true"                                         )
-    parser.add_option_group(load)
-    
-
-    # cmd_autoload options  #                   #                       #                                   #
-    autoload = optparse.OptionGroup(parser, "Autoload Options")
-    autoload.add_option(    "--autoload-state", dest="autoload_state_path", help="Path to autoload state dir",      
-            metavar="PATH",                                             default=config.SEARCH_AUTOINDEX_PATH)
-
-    autoload.add_option(    "--from",           dest="after",           help="Only autoload logfiles from the given date on", 
-            metavar="DATE", type="date",        action="parse_date",    default=None                        )
-
-    autoload.add_option(    "--until",          dest="until",           help="Only autoload logfiles up to (and including) the given date",  
-            metavar="DATE", type="date",        action="parse_date",    default=None                        )
-
-    autoload.add_option(    "--reload",         dest="reload",          help="Force reload lines",
-                                                action="store_true"                                         )
-
-    autoload.add_option(    "--reset",          dest="reset",           help="Reset old autload state",
-                                                action="store_true"                                         )
-
-    autoload.add_option(    "--ignore-resume",  dest="ignore_resume",   help="Do not try and resume interrupted autoload",  
-                                                action="store_true"                                         )
-    parser.add_option_group(autoload)
-
-    # parse
-    options, args = parser.parse_args(argv[1:])
-
-    # postprocess stuff
-    options._parser = parser
-    options.formatter = config.LOG_FORMATTERS[options.formatter_name](options.timezone, "%H:%M:%S", None, None)
-
-    # special-case --help
-    if options.help :
-        return cmd_help(options, *args)
-    
-    # must have at least the command argument
-    if not args :
-        raise CommandError("Missing command")
-    
-    # pop command
-    command = args.pop(0)
-    
-    # get func
-    func = globals().get('cmd_%s' % command)
-    
-    # unknown command?
-    if not func :
-        raise CommandError("Unknown command: %s" % command)
-    
-    # call
-    func(options, *args)
-
-if __name__ == '__main__' :
-    try :
-        main(sys.argv)
-        sys.exit(0)
-
-    except CommandError, e :
-        print e
-        sys.exit(1)
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk/irclogs/static/irclogs.css	Sun Sep 13 20:08:16 2009 +0300
@@ -0,0 +1,393 @@
+/*
+ * Global styles
+ */
+body {
+    padding: 0px;
+    margin: 0px;
+
+    background-color: #ffffff;
+    color: #000000;
+}
+
+a {
+    color: #454545;
+}
+
+/*
+ * Menu
+ */
+#menu {
+    padding: 0px;
+    margin: 0px;
+}
+
+#menu ul {
+    display: table;
+    list-style-type: none;
+
+    width: 100%;
+
+    padding: 0px;
+    margin: 0px;
+    
+    background-color: #f0f0f0;
+    border-bottom: 1px dashed #a5a5a5;
+
+    text-align: center;
+    white-space: nowrap;
+}
+
+#menu li {
+    display: table-cell;
+
+    height: 1.5em;
+
+    padding: 0px;
+    margin: 0px;
+
+    border-left: 1px solid #b0b0b0;
+
+    font-weight: bold;
+}
+
+#menu li.join-left {
+    border-left: none;
+}
+
+#menu li a,
+#menu li form {
+    height: 1.5em;
+}
+
+#menu li:first-child {
+    border-left: none;
+}
+
+#menu li a {
+    display: block;
+
+    padding: 6px;
+    margin: 0px;
+
+    height: 100%;
+
+    color: #494949;
+    text-decoration: none;
+}
+
+#menu li a:hover {
+    background-color: #d0d0d0;
+    text-decoration: none;
+}
+
+#menu form {
+    display: block;
+
+    padding: 0px;
+    margin: 0px;
+}
+
+#menu form input[type=submit] {
+    padding: 9px;
+    margin: 0px;
+
+    border: none;
+    background-color: inherit;
+
+    cursor: pointer;
+}
+
+#menu form input[type=submit]:hover {
+    background-color: #d0d0d0;
+}
+
+/*
+ * Content
+ */
+div#content {
+    padding: 25px;
+}
+
+/*
+ * Footer
+ */
+div#footer {
+    padding: 10px;
+    
+    border-top: 1px dashed #a5a5a5;
+
+    font-size: x-small;
+    font-style: italic;
+}
+
+div#footer-left {
+    float: left;
+}
+
+div#footer-right {
+    float: right;
+    text-align: right;
+}
+
+div#footer-center {
+    text-align: center;
+}
+
+/*
+ * General
+ */
+/* Channel view */
+div#title {
+    text-align: center;
+    font-size: x-large;
+    font-weight: bold;
+
+    margin-bottom: 20px;
+}
+
+/* Calendar */
+div.calendar-list {
+    text-align: center;
+}
+
+table.calendar {
+    display: inline-table;
+}
+
+table.calendar th {
+    background-color: #c8c8c8;
+}
+
+/* month header */
+table.calendar tr.month-header th {
+
+}
+
+table.calendar tr.month-header form {
+    padding: 5px;
+}
+
+table.calendar tr.month-header a,
+table.calendar span.prev-month,
+table.calendar span.next-month {
+    display: block;
+    
+    color: inherit;
+    text-decoration: none;
+    
+    padding: 3px;
+    padding-left: 8px;
+    padding-right: 8px;
+
+    font-size: x-large;
+}
+
+table.calendar th.this-month a.next-month,
+table.calendar span.prev-month,
+table.calendar span.next-month {
+    color: #d5d5d5;
+}
+ 
+table.calendar tr.month-header a:hover {
+    background-color: #b5b5b5;
+}
+
+table.calendar tr.month-header span {
+    margin-top: 5px;
+}
+
+table.calendar .prev-month {
+    float: left;
+}
+
+table.calendar .next-month {
+    float: right;
+}
+
+/* week header */
+table.calendar tr.week-header th {
+    width: 14%
+}
+
+/* cells */
+table.calendar td {
+    padding: 2px;
+    margin: 1px;
+    text-align: center;
+}
+
+table.calendar td a {
+    display: block;
+    padding: 2px;
+
+    background-color: #e0e0e0;
+    text-decoration: none;
+
+    color: inherit;
+}
+   
+table.calendar td.empty {
+    color: #d0d0d0;
+}
+
+table.calendar td a:hover {
+    background-color: #d0d0d0;
+}
+
+table.calendar td#today {
+    font-weight: bold;
+}
+
+/* Preferences form */
+fieldset {
+    background-color: #e8e8e8;
+
+    margin-bottom: 1em;
+}
+
+legend {
+    padding: 0.2em 0.5em;
+    
+    background-color: inherit;
+    border: inherit;
+}
+
+label {
+    display: block;
+    float: left;
+
+    width: 8em;
+    margin-right: 0.5em;
+
+    text-align: right;
+}
+
+fieldset input,
+fieldset select {
+    width: 15em;
+}
+
+fieldset input[type=submit] {
+    width: 8em;
+}
+
+fieldset input.wide {
+    width: 30em;
+}
+
+fieldset span.example {
+    font-size: x-small;
+
+    margin-left: 1em;
+}
+
+/* Search form */
+div#search-form {
+    padding: 25px;
+
+    text-align: center;
+}
+
+/*
+div#search input[type=text] {
+    display: block;
+
+    width: 40%;
+    margin: 0px auto 5px auto;
+}
+
+div#search div.indent {
+    margin-left: 8.5em;
+}
+*/
+
+div#search-help {
+
+}
+
+div#search-error {
+    margin: 50px;
+
+    text-align: center;
+    font-size: x-large;
+    font-style: italic;
+}
+
+/* Log lines */
+a.more-link {
+    color: black;
+    text-decoration: none;
+}
+
+a.more-link:hover {
+    text-decoration: underline;
+}
+
+div#lines a {
+    color: black;
+}
+
+div#lines a[id] {
+    color: #ffffff;
+    text-decoration: none;
+}
+
+div#lines a[id]:hover,
+div#lines a[id]:target {
+    color: #000000;
+}
+
+/* Pagination */
+div.paginate {
+    height: 50px;
+
+    text-align: center;
+}
+
+div.paginate ul {
+    margin: 0px;
+    padding: 0px;
+
+    line-height: 30px;
+
+    border: 1px solid #aaa;
+
+    white-space: nowrap;
+}
+
+div.paginate li {
+    list-style-type: none;
+    display: inline;
+}
+
+div.paginate li * {
+    padding: 7px 10px;
+}
+
+div.paginate li a {
+    color: black;
+    text-decoration: none;
+}
+
+div.paginate li span {
+    color: #d5d5d5;
+}
+
+div.paginate li a:hover {
+    background-color: #d0d0d0;
+}
+
+li.paginate-left {
+    float: left;
+}
+
+li.paginate-right {
+    float: right;
+}
+
+/* Lastlog */
+div#other-formats {
+    font-size: small;
+    text-align: center;
+    font-style: italic;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk/irclogs/templates/channel.tmpl	Sun Sep 13 20:08:16 2009 +0300
@@ -0,0 +1,39 @@
+<%inherit file="layout.tmpl" />
+
+<%def name="menu()">
+<ul>
+    <li><a href="${urls.index.build(req)}">Home</a></li>
+    <li><a href="${urls.preferences.build(req)}">Preferences</a></li>
+
+    <li>
+        <a href="${urls.channel.build(req, channel=channel)}">Channel:</a>
+    </li><li class="join-left">
+        <form action="${urls.channel_select.build(req)}" method="GET">
+            <select name="channel">
+            ${h.select_options(((ch.id, ch.title) for ch in channel_list), channel.id)}
+            </select><input type="submit" value="Go &raquo;" />
+        </form>
+    </li>
+
+    <li><a href="${urls.channel_calendar.build(req, channel=channel)}">Browse by Date</a></li>
+
+    <li>
+        <a href="${h.build_url(urls.channel_search, channel=channel)}">Search:</a>
+    </li><li class="join-left">
+        <form action="${urls.channel_search.build(req, channel=channel)}" method="GET">
+            <input type="hidden" name="count" value="100" />
+            <input type="text" name="q" value="${search_query if search_query else ''}" />
+            <input type="submit" value="Go &raquo;" />
+        </form>
+    </li>
+<!--
+    <li><a href="${h.build_url(urls.channel_last, channel=channel, count=count or None, type='rss')}">[RSS]</a></li>
+-->    
+</ul>
+</%def>
+
+${next.body()}
+
+<%def name="footer_right()">
+    All times are in <a href="${h.build_url(urls.preferences)}">${h.tz_name(prefs['timezone'])}</a>
+</%def>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk/irclogs/templates/channel_calendar.tmpl	Sun Sep 13 20:08:16 2009 +0300
@@ -0,0 +1,69 @@
+<%inherit file="channel.tmpl" />
+
+<%def name="month_table(month, is_center=True)">
+## the set of available days
+<% log_dates = h.set(channel.source.get_month_days(month)) %>
+## the calendar table
+<table class="calendar">
+## table header - month name
+    <tr class="month-header">
+        <th colspan="7"${' class="this-month"' if h.is_this_month(month) else ''}>
+        % if is_center :
+            <a href="${urls.channel_calendar.build(req, channel=channel, year=h.next_month(month).year, month=h.next_month(month).month)}" class="next-month" title="Next month">&raquo;</a>
+            <a href="${urls.channel_calendar.build(req, channel=channel, year=h.prev_month(month).year, month=h.prev_month(month).month)}" class="prev-month" title="Previous month">&laquo;</a>
+            
+            <form action="${urls.channel_calendar.build(req, channel=channel)}" method="GET">
+                <select name="month">
+                    ${h.select_options(((month_num, name) for month_num, name in h.months), month.month)}
+                </select>
+                <select name="year">
+                    ${h.select_options(((None, 2006), (None, 2007), (None, 2008), (None, 2009)), month.year)}
+                </select>
+                <input type="submit" value="Go &raquo;" />
+            </form>
+        % else :
+            <span id="month-name">${h.fmt_month(month)}</span>
+        % endif
+        </th>
+    </tr>
+## month header - weekday names    
+    <tr class="week-header">
+    % for weekday in h.calendar.iterweekdays() :
+        <th>${h.fmt_weekday(weekday)}</th>
+    % endfor
+    </tr>
+## iterate over the weeks
+% for week in h.calendar.monthdays2calendar(month.year, month.month) :
+    <tr>
+    ## iterate over the week's days
+    % for day, weekday in week :
+        ## is it an empty cell?
+        % if not day :
+        <td>&nbsp;</td>
+        % else :
+        ## build date
+        <% date = h.build_date(month, day) %>\
+        ## render cell
+        <td${' id="today"' if h.is_today(date) else ''}${' class="empty"' if date.date() not in log_dates else ''}>\
+        ## link to logs for this day?
+        % if date.date() in log_dates :
+<a href="${urls.channel_date.build(req, channel=channel, date=date)}">${day}</a>\
+        % else :
+${day}\
+        % endif
+</td>
+    % endif
+    % endfor
+    </tr>
+% endfor
+</table>
+</%def>
+
+<div id="title">${channel.title} :: Calendar for ${h.fmt_month(month)}</div>
+
+<div class="calendar-list">
+## three months
+${month_table(h.prev_month(month), is_center=False  )}
+${month_table(month,               is_center=True   )}
+${month_table(h.next_month(month), is_center=False  )}
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk/irclogs/templates/channel_date.tmpl	Sun Sep 13 20:08:16 2009 +0300
@@ -0,0 +1,29 @@
+<%inherit file="channel.tmpl" />
+<%namespace file="inc_paginate.tmpl" name="paginate" />
+
+## special pagination stuff
+<%def name="paginate_date()">
+    <%call expr="paginate.paginate(urls.channel_date, count, page, max, channel=channel, date=date)">
+        <%def name="left()">
+        % if date_prev :
+            <a href="${h.build_url(urls.channel_date, channel=channel, date=date_prev)}">&laquo; ${h.fmt_date(date_prev)}</a>
+        % endif
+        </%def>
+        <%def name="right()">
+        % if date_next :
+            <a href="${h.build_url(urls.channel_date, channel=channel, date=date_next)}">${h.fmt_date(date_next)} &raquo;</a>
+        % endif
+        </%def>
+    </%call>
+</%def>
+
+<div id="title">${channel.title} :: Logs for ${h.fmt_date(date)}</div>
+
+% if page :
+${paginate_date()}
+% endif
+<%include file="lines.tmpl" />
+% if page :
+${paginate_date()}
+% endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk/irclogs/templates/channel_last.tmpl	Sun Sep 13 20:08:16 2009 +0300
@@ -0,0 +1,18 @@
+<%inherit file="channel.tmpl" />
+
+<div id="title">${channel.title} :: Last ${count} lines</div>
+
+<form action="${h.url(urls.channel_last, channel=channel)}" method="GET">
+    View last <select name="count">
+        ${h.select_options(((None, cc) for cc in (10, 20, 50, 100, 200, 500, 1000)), count)}
+    </select> lines: <input type="submit" value="Go &raquo;" />
+</form>
+
+<%include file="lines.tmpl" />
+
+<div id="other-formats">
+    Other formats available:<br/>
+        <a href="${h.url(urls.channel_last, channel=channel, count=count, type='txt')}">Plaintext</a>
+    |   <a href="${h.url(urls.channel_last, channel=channel, count=count, type='png')}">PNG</a>
+    |   <a href="${h.url(urls.channel_last, channel=channel, count=count, type='rss')}">RSS</a>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk/irclogs/templates/channel_search.tmpl	Sun Sep 13 20:08:16 2009 +0300
@@ -0,0 +1,62 @@
+<%inherit file="channel.tmpl" />
+<%namespace file="inc_paginate.tmpl" import="paginate" />
+
+% if not show_results :
+<div id="title">${channel.title} :: Search</div>
+
+<div id="search">
+    <form action="${h.build_url(urls.channel_search, channel=channel)}" method="GET">
+        <fieldset>
+            <p>
+                <label for="q">Message:</label>
+                <input type="text" name="q" class="wide" />
+            </p>
+            
+            <p>
+                <label for="nick">By nickname:</label>
+                <input type="text" name="nick" />
+                <span>(optional)</span>
+            </p>
+
+            <p>
+                <label for="count">Results/page:</label>
+                <select name="count">
+                    ${h.select_options(((cc, cc_label) for cc, cc_label in config.SEARCH_LINE_COUNT_OPTIONS), count)}
+                </select>
+            </p>
+            
+            <input type="submit" value="Search" />
+        </fieldset>        
+    </form>
+    
+    <div id="search-help">
+        <p>Search powered by <a href="http://hyperestraier.sourceforge.net/">Hyper Estraier</a>:</p>
+
+        <ul>
+            <li>Group words together using quotes: <tt>"united nations"</tt></li>
+            <li>Searching for multiple words is AND: <tt>internet security</tt></li>
+            <li>To exclude terms, use <strong>!</strong> : <tt>hacker ! cracker</tt></li>
+            <li>Union (i.e. <q>or</q>) using <strong>|</strong> : <tt>proxy | firewall</tt></li>
+            <li>Search is case-insensitive</li>
+        </ul>
+    </div>
+</div>
+
+% else :
+<div id="title">${channel.title} :: Results\
+%   if search_query :
+ With '${search_query}'\
+%   endif
+%   if search_nick :
+ By ${search_nick}\
+%   endif
+</div>
+
+${paginate(urls.channel_search, count, page, max, channel=channel, q=search_query, t=search_targets, _more=True, _last=not(bool(lines)))}
+% if lines :
+<%include file="lines.tmpl" />
+% else :
+<div id="search-error">No results found</div>
+% endif
+${paginate(urls.channel_search, count, page, max, channel=channel, q=search_query, t=search_targets, _more=True, _last=not(bool(lines)))}
+% endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk/irclogs/templates/inc_paginate.tmpl	Sun Sep 13 20:08:16 2009 +0300
@@ -0,0 +1,60 @@
+## special overrides...
+<%def name="paginate_left()">
+
+</%def>
+
+<%def name="paginate(url, count, page_cur, page_max, _more=None, _last=False, **args)">
+    <%doc>
+        Pagination works using page numbers, with a specific number of maximum pages displayed. If _more is True,
+        then instead of a "Next" button, we have a "More" button, which goes to the max+1'th page, unless _last is
+        True, whereupon it's not displayed.
+
+        Can be called using <%call>, whereupon caller.left/caller.right can define additional data to display at the
+        left/right of the pagination ul.
+    </%doc>
+    <div class="paginate">
+        <ul>
+        % if caller and caller.right :
+            <li class="paginate-right">
+                ${caller.right()}
+            </li>
+        % endif
+        % if caller and caller.left :
+            <li class="paginate-left">
+                ${caller.left()}
+            </li>
+        % endif
+            <li>
+            % if page_cur > 1 :
+                <a href="${h.build_url(url, count=count, page=page_cur-1, max=max, **args)}">&laquo; Prev</a>
+            % else :
+                <span>&laquo; Prev</span>
+            %endif
+            </li>
+        % for page in xrange(1, page_max + 1) :
+            <li>
+            % if page == page_cur :
+                <strong>${page}</strong>
+            % else :
+                <a href="${h.build_url(url, count=count, page=page, max=page_max, **args)}">${page}</a>
+            % endif
+            </li>
+        % endfor
+        % if _more and not _last :
+            <li>&hellip;</li>
+        % endif
+            <li>
+            % if _more and _last :
+                <span>More &raquo;</span>
+            % elif _more : 
+                <a href="${h.build_url(url, count=count, page=page_max+1, **args)}">More &raquo;</a>
+            % elif page_cur == page_max : ## last page
+                <span>Next &raquo;</span>
+            % else : 
+                <a href="${h.build_url(url, count=count, page=page_cur+1, **args)}">Next &raquo;</a>
+            % endif
+            </li>
+        </ul>
+    </div>
+</%def>
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk/irclogs/templates/index.tmpl	Sun Sep 13 20:08:16 2009 +0300
@@ -0,0 +1,10 @@
+<%inherit file="layout.tmpl" />
+
+<div id="title">Available Channels</div>
+
+<ul>
+% for channel in channel_list :
+    <li><a href="${urls.channel.build(req, channel=channel)}">${channel.title}</a></li>
+% endfor
+</ul>
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk/irclogs/templates/layout.tmpl	Sun Sep 13 20:08:16 2009 +0300
@@ -0,0 +1,84 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+
+<%def name="menu()">
+<ul>
+    <li><a href="${urls.index.build(req)}">Home</a></li>
+    <li><a href="${urls.preferences.build(req)}">Preferences</a></li>
+    
+    <li>
+        <span>Channel:</span>
+    </li><li class="join-left">
+        <form action="${urls.channel_select.build(req)}" method="GET">
+            <select name="channel">
+            ${h.select_options(((ch.id, ch.title) for ch in channel_list), channel.id if channel else None)}
+            </select><input type="submit" value="Go &raquo;" />
+        </form>
+    </li>
+</ul>
+</%def>
+
+<%def name="footer_right()">
+
+</%def>
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+    <head>
+        <title>irclogs.qmsk.net${(' :: ' + channel.title) if channel else ''}</title>
+        <link rel="Stylesheet" type="text/css" href="${req.site_root}/static/irclogs.css" />
+    ## timezone-autodetect
+    % if prefs and prefs.is_default('timezone') :
+        <script language="Javascript" type="text/javascript">
+/*
+ * Set a preference cookie
+ */
+function set_pref (name, value) {
+    // XXX: expire?
+    document.cookie = (name + "=" + value + "; path=/");
+}
+
+/*
+ * Set the timezone_offset cookie to the current Date's timezone offset
+ */
+function autodetect_tz_offset () {
+    // current datetime
+    var now = new Date();
+
+    // timezone offset from UTC in minutes
+    var timezone_offset = -now.getTimezoneOffset()
+    
+    // store cookie with offset in minutes
+    set_pref('timezone_offset', timezone_offset);
+}
+
+/*
+ * Autodetect at load
+ */
+window.onload = autodetect_tz_offset;
+        </script>
+    % endif
+    </head>
+    <body>
+        <div id="menu">
+            ${next.menu()}
+        </div>
+
+        <div id="content">
+            ${next.body()}
+        </div>
+
+        <div id="footer">
+            <div id="footer-right">
+                ${next.footer_right()}
+            </div>
+
+            <div id="footer-left">
+                <a href="http://projects.qmsk.net/irclogs2">irclogs2</a> version ${h.version_link()}
+            </div>
+
+            <div id="footer-center">
+                ${h.validation_notice(req.site_host)}
+            </div>
+        </div>
+    </body>
+</html>
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk/irclogs/templates/lines.tmpl	Sun Sep 13 20:08:16 2009 +0300
@@ -0,0 +1,13 @@
+<% formatter = prefs['formatter'] %>
+
+<div id="lines">
+% if formatter.html_fixedwidth :
+<pre>
+% endif
+% for line, html in lines:
+<a href="${h.build_url(urls.channel_link, channel=channel, timestamp=line.timestamp)}#${h.utc_timestamp(line.timestamp)}" id="${h.utc_timestamp(line.timestamp)}">&raquo;&raquo; </a>${html}
+% endfor
+% if formatter.html_fixedwidth :
+</pre>
+% endif
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk/irclogs/templates/preferences.tmpl	Sun Sep 13 20:08:16 2009 +0300
@@ -0,0 +1,67 @@
+<%inherit file="layout.tmpl" />
+
+<div id="title">Edit Preferences</div>
+
+<form action="${urls.preferences.build(req)}" method="POST">
+    <fieldset>
+        <legend>Dates / Times</legend>
+        
+        <p>
+            <label for="timezone">Timezone:</label>
+            <select name="timezone">
+            ${h.select_options(prefs.pref('timezone').OPTIONS, prefs.build('timezone'))}
+            </select>
+            <span class="example">(${h.tz_name(prefs['timezone'])})</span>
+        </p>
+
+        <p>
+            <label for="date_format">Date format:</label>
+            <input type="text" name="date_format" value="${prefs['date_format']}" />
+            <span class="example">(${h.fmt_date()})</span>
+        </p>
+        
+        <p>
+            <label for="time_format">Time format:</label>
+            <input type="text" name="time_format" value="${prefs['time_format']}" />
+            <span class="example">(${h.fmt_time()})</span>
+        </p>
+
+    </fieldset>
+
+    <fieldset>
+        <legend>Log Output</legend>
+        
+        <p>
+            <label for="formatter">Formatter:</label>
+            <select name="formatter">
+                ${h.select_options(((fmt_name, fmt.title) for fmt_name, fmt in preferences.formatter.formatters.iteritems()), prefs['formatter'].name)}
+            </select>
+        </p>
+
+        <p>
+            <label for="count">Lines / Page:</label>
+            <input type="text" name="count" value="${prefs['count']}" />
+<!--            <span class="example">(Blank for infinite)</span> -->
+        </p>
+    </fieldset>
+    
+    <fieldset>
+        <legend>Image Options</legend>
+
+        <p>
+            <label for="image_font">Font:</label>
+            <select name="image_font">
+                ${h.select_options(((name, title) for name, (path, title) in config.FORMATTER_IMAGE_FONTS.iteritems()), prefs['image_font'][0])}
+            </select>
+        </p>
+
+        <p>
+            <label for="image_font_size">Font size:</label>
+            <input type="text" name="image_font_size" value="${prefs['image_font_size']}" />
+            <span class="example">pt</span>
+        </p>
+    </fieldset>
+
+    <input type="submit" value="Save" />
+</form>
+
--- a/setup.py	Sun Sep 13 18:57:48 2009 +0300
+++ b/setup.py	Sun Sep 13 20:08:16 2009 +0300
@@ -2,16 +2,16 @@
 
 from distutils.core import setup
 
-from qmsk.irclogs.version import version_string
-
 # version magic...
-import os.path
-version = version_string(os.path.dirname(__file__))
+#from qmsk.irclogs.version import version_string
+#import os.path
+#version = version_string(os.path.dirname(__file__))
 
 
 setup(
     name        = "qmsk.irclogs",
-    version     = version,
+    #    version     = version,
+    version     = '0.2.0-rc1',
     description = "Irclogs2",
     author      = "Tero Marttila",
     author_email= "terom@fixme.fi",
@@ -22,9 +22,13 @@
     ],
 
     scripts     = [
-        'bin/index.cgi',
-        'bin/index.fcgi',
-        'bin/search-index'
+        'bin/qmsk-irclogs.cgi',
+        'bin/qmsk-irclogs.fcgi',
+        'bin/qmsk-irclogs-search-index'
     ],
+
+    package_data    = {
+        'qmsk.irclogs':     ['templates/*.tmpl', 'static/*'],
+    },
 )
 
--- a/static/irclogs.css	Sun Sep 13 18:57:48 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,393 +0,0 @@
-/*
- * Global styles
- */
-body {
-    padding: 0px;
-    margin: 0px;
-
-    background-color: #ffffff;
-    color: #000000;
-}
-
-a {
-    color: #454545;
-}
-
-/*
- * Menu
- */
-#menu {
-    padding: 0px;
-    margin: 0px;
-}
-
-#menu ul {
-    display: table;
-    list-style-type: none;
-
-    width: 100%;
-
-    padding: 0px;
-    margin: 0px;
-    
-    background-color: #f0f0f0;
-    border-bottom: 1px dashed #a5a5a5;
-
-    text-align: center;
-    white-space: nowrap;
-}
-
-#menu li {
-    display: table-cell;
-
-    height: 1.5em;
-
-    padding: 0px;
-    margin: 0px;
-
-    border-left: 1px solid #b0b0b0;
-
-    font-weight: bold;
-}
-
-#menu li.join-left {
-    border-left: none;
-}
-
-#menu li a,
-#menu li form {
-    height: 1.5em;
-}
-
-#menu li:first-child {
-    border-left: none;
-}
-
-#menu li a {
-    display: block;
-
-    padding: 6px;
-    margin: 0px;
-
-    height: 100%;
-
-    color: #494949;
-    text-decoration: none;
-}
-
-#menu li a:hover {
-    background-color: #d0d0d0;
-    text-decoration: none;
-}
-
-#menu form {
-    display: block;
-
-    padding: 0px;
-    margin: 0px;
-}
-
-#menu form input[type=submit] {
-    padding: 9px;
-    margin: 0px;
-
-    border: none;
-    background-color: inherit;
-
-    cursor: pointer;
-}
-
-#menu form input[type=submit]:hover {
-    background-color: #d0d0d0;
-}
-
-/*
- * Content
- */
-div#content {
-    padding: 25px;
-}
-
-/*
- * Footer
- */
-div#footer {
-    padding: 10px;
-    
-    border-top: 1px dashed #a5a5a5;
-
-    font-size: x-small;
-    font-style: italic;
-}
-
-div#footer-left {
-    float: left;
-}
-
-div#footer-right {
-    float: right;
-    text-align: right;
-}
-
-div#footer-center {
-    text-align: center;
-}
-
-/*
- * General
- */
-/* Channel view */
-div#title {
-    text-align: center;
-    font-size: x-large;
-    font-weight: bold;
-
-    margin-bottom: 20px;
-}
-
-/* Calendar */
-div.calendar-list {
-    text-align: center;
-}
-
-table.calendar {
-    display: inline-table;
-}
-
-table.calendar th {
-    background-color: #c8c8c8;
-}
-
-/* month header */
-table.calendar tr.month-header th {
-
-}
-
-table.calendar tr.month-header form {
-    padding: 5px;
-}
-
-table.calendar tr.month-header a,
-table.calendar span.prev-month,
-table.calendar span.next-month {
-    display: block;
-    
-    color: inherit;
-    text-decoration: none;
-    
-    padding: 3px;
-    padding-left: 8px;
-    padding-right: 8px;
-
-    font-size: x-large;
-}
-
-table.calendar th.this-month a.next-month,
-table.calendar span.prev-month,
-table.calendar span.next-month {
-    color: #d5d5d5;
-}
- 
-table.calendar tr.month-header a:hover {
-    background-color: #b5b5b5;
-}
-
-table.calendar tr.month-header span {
-    margin-top: 5px;
-}
-
-table.calendar .prev-month {
-    float: left;
-}
-
-table.calendar .next-month {
-    float: right;
-}
-
-/* week header */
-table.calendar tr.week-header th {
-    width: 14%
-}
-
-/* cells */
-table.calendar td {
-    padding: 2px;
-    margin: 1px;
-    text-align: center;
-}
-
-table.calendar td a {
-    display: block;
-    padding: 2px;
-
-    background-color: #e0e0e0;
-    text-decoration: none;
-
-    color: inherit;
-}
-   
-table.calendar td.empty {
-    color: #d0d0d0;
-}
-
-table.calendar td a:hover {
-    background-color: #d0d0d0;
-}
-
-table.calendar td#today {
-    font-weight: bold;
-}
-
-/* Preferences form */
-fieldset {
-    background-color: #e8e8e8;
-
-    margin-bottom: 1em;
-}
-
-legend {
-    padding: 0.2em 0.5em;
-    
-    background-color: inherit;
-    border: inherit;
-}
-
-label {
-    display: block;
-    float: left;
-
-    width: 8em;
-    margin-right: 0.5em;
-
-    text-align: right;
-}
-
-fieldset input,
-fieldset select {
-    width: 15em;
-}
-
-fieldset input[type=submit] {
-    width: 8em;
-}
-
-fieldset input.wide {
-    width: 30em;
-}
-
-fieldset span.example {
-    font-size: x-small;
-
-    margin-left: 1em;
-}
-
-/* Search form */
-div#search-form {
-    padding: 25px;
-
-    text-align: center;
-}
-
-/*
-div#search input[type=text] {
-    display: block;
-
-    width: 40%;
-    margin: 0px auto 5px auto;
-}
-
-div#search div.indent {
-    margin-left: 8.5em;
-}
-*/
-
-div#search-help {
-
-}
-
-div#search-error {
-    margin: 50px;
-
-    text-align: center;
-    font-size: x-large;
-    font-style: italic;
-}
-
-/* Log lines */
-a.more-link {
-    color: black;
-    text-decoration: none;
-}
-
-a.more-link:hover {
-    text-decoration: underline;
-}
-
-div#lines a {
-    color: black;
-}
-
-div#lines a[id] {
-    color: #ffffff;
-    text-decoration: none;
-}
-
-div#lines a[id]:hover,
-div#lines a[id]:target {
-    color: #000000;
-}
-
-/* Pagination */
-div.paginate {
-    height: 50px;
-
-    text-align: center;
-}
-
-div.paginate ul {
-    margin: 0px;
-    padding: 0px;
-
-    line-height: 30px;
-
-    border: 1px solid #aaa;
-
-    white-space: nowrap;
-}
-
-div.paginate li {
-    list-style-type: none;
-    display: inline;
-}
-
-div.paginate li * {
-    padding: 7px 10px;
-}
-
-div.paginate li a {
-    color: black;
-    text-decoration: none;
-}
-
-div.paginate li span {
-    color: #d5d5d5;
-}
-
-div.paginate li a:hover {
-    background-color: #d0d0d0;
-}
-
-li.paginate-left {
-    float: left;
-}
-
-li.paginate-right {
-    float: right;
-}
-
-/* Lastlog */
-div#other-formats {
-    font-size: small;
-    text-align: center;
-    font-style: italic;
-}
--- a/templates/channel.tmpl	Sun Sep 13 18:57:48 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,39 +0,0 @@
-<%inherit file="layout.tmpl" />
-
-<%def name="menu()">
-<ul>
-    <li><a href="${urls.index.build(req)}">Home</a></li>
-    <li><a href="${urls.preferences.build(req)}">Preferences</a></li>
-
-    <li>
-        <a href="${urls.channel.build(req, channel=channel)}">Channel:</a>
-    </li><li class="join-left">
-        <form action="${urls.channel_select.build(req)}" method="GET">
-            <select name="channel">
-            ${h.select_options(((ch.id, ch.title) for ch in channel_list), channel.id)}
-            </select><input type="submit" value="Go &raquo;" />
-        </form>
-    </li>
-
-    <li><a href="${urls.channel_calendar.build(req, channel=channel)}">Browse by Date</a></li>
-
-    <li>
-        <a href="${h.build_url(urls.channel_search, channel=channel)}">Search:</a>
-    </li><li class="join-left">
-        <form action="${urls.channel_search.build(req, channel=channel)}" method="GET">
-            <input type="hidden" name="count" value="100" />
-            <input type="text" name="q" value="${search_query if search_query else ''}" />
-            <input type="submit" value="Go &raquo;" />
-        </form>
-    </li>
-<!--
-    <li><a href="${h.build_url(urls.channel_last, channel=channel, count=count or None, type='rss')}">[RSS]</a></li>
--->    
-</ul>
-</%def>
-
-${next.body()}
-
-<%def name="footer_right()">
-    All times are in <a href="${h.build_url(urls.preferences)}">${h.tz_name(prefs['timezone'])}</a>
-</%def>
--- a/templates/channel_calendar.tmpl	Sun Sep 13 18:57:48 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,69 +0,0 @@
-<%inherit file="channel.tmpl" />
-
-<%def name="month_table(month, is_center=True)">
-## the set of available days
-<% log_dates = h.set(channel.source.get_month_days(month)) %>
-## the calendar table
-<table class="calendar">
-## table header - month name
-    <tr class="month-header">
-        <th colspan="7"${' class="this-month"' if h.is_this_month(month) else ''}>
-        % if is_center :
-            <a href="${urls.channel_calendar.build(req, channel=channel, year=h.next_month(month).year, month=h.next_month(month).month)}" class="next-month" title="Next month">&raquo;</a>
-            <a href="${urls.channel_calendar.build(req, channel=channel, year=h.prev_month(month).year, month=h.prev_month(month).month)}" class="prev-month" title="Previous month">&laquo;</a>
-            
-            <form action="${urls.channel_calendar.build(req, channel=channel)}" method="GET">
-                <select name="month">
-                    ${h.select_options(((month_num, name) for month_num, name in h.months), month.month)}
-                </select>
-                <select name="year">
-                    ${h.select_options(((None, 2006), (None, 2007), (None, 2008), (None, 2009)), month.year)}
-                </select>
-                <input type="submit" value="Go &raquo;" />
-            </form>
-        % else :
-            <span id="month-name">${h.fmt_month(month)}</span>
-        % endif
-        </th>
-    </tr>
-## month header - weekday names    
-    <tr class="week-header">
-    % for weekday in h.calendar.iterweekdays() :
-        <th>${h.fmt_weekday(weekday)}</th>
-    % endfor
-    </tr>
-## iterate over the weeks
-% for week in h.calendar.monthdays2calendar(month.year, month.month) :
-    <tr>
-    ## iterate over the week's days
-    % for day, weekday in week :
-        ## is it an empty cell?
-        % if not day :
-        <td>&nbsp;</td>
-        % else :
-        ## build date
-        <% date = h.build_date(month, day) %>\
-        ## render cell
-        <td${' id="today"' if h.is_today(date) else ''}${' class="empty"' if date.date() not in log_dates else ''}>\
-        ## link to logs for this day?
-        % if date.date() in log_dates :
-<a href="${urls.channel_date.build(req, channel=channel, date=date)}">${day}</a>\
-        % else :
-${day}\
-        % endif
-</td>
-    % endif
-    % endfor
-    </tr>
-% endfor
-</table>
-</%def>
-
-<div id="title">${channel.title} :: Calendar for ${h.fmt_month(month)}</div>
-
-<div class="calendar-list">
-## three months
-${month_table(h.prev_month(month), is_center=False  )}
-${month_table(month,               is_center=True   )}
-${month_table(h.next_month(month), is_center=False  )}
-</div>
--- a/templates/channel_date.tmpl	Sun Sep 13 18:57:48 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-<%inherit file="channel.tmpl" />
-<%namespace file="inc_paginate.tmpl" name="paginate" />
-
-## special pagination stuff
-<%def name="paginate_date()">
-    <%call expr="paginate.paginate(urls.channel_date, count, page, max, channel=channel, date=date)">
-        <%def name="left()">
-        % if date_prev :
-            <a href="${h.build_url(urls.channel_date, channel=channel, date=date_prev)}">&laquo; ${h.fmt_date(date_prev)}</a>
-        % endif
-        </%def>
-        <%def name="right()">
-        % if date_next :
-            <a href="${h.build_url(urls.channel_date, channel=channel, date=date_next)}">${h.fmt_date(date_next)} &raquo;</a>
-        % endif
-        </%def>
-    </%call>
-</%def>
-
-<div id="title">${channel.title} :: Logs for ${h.fmt_date(date)}</div>
-
-% if page :
-${paginate_date()}
-% endif
-<%include file="lines.tmpl" />
-% if page :
-${paginate_date()}
-% endif
-
--- a/templates/channel_last.tmpl	Sun Sep 13 18:57:48 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-<%inherit file="channel.tmpl" />
-
-<div id="title">${channel.title} :: Last ${count} lines</div>
-
-<form action="${h.url(urls.channel_last, channel=channel)}" method="GET">
-    View last <select name="count">
-        ${h.select_options(((None, cc) for cc in (10, 20, 50, 100, 200, 500, 1000)), count)}
-    </select> lines: <input type="submit" value="Go &raquo;" />
-</form>
-
-<%include file="lines.tmpl" />
-
-<div id="other-formats">
-    Other formats available:<br/>
-        <a href="${h.url(urls.channel_last, channel=channel, count=count, type='txt')}">Plaintext</a>
-    |   <a href="${h.url(urls.channel_last, channel=channel, count=count, type='png')}">PNG</a>
-    |   <a href="${h.url(urls.channel_last, channel=channel, count=count, type='rss')}">RSS</a>
-</div>
--- a/templates/channel_search.tmpl	Sun Sep 13 18:57:48 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-<%inherit file="channel.tmpl" />
-<%namespace file="inc_paginate.tmpl" import="paginate" />
-
-% if not show_results :
-<div id="title">${channel.title} :: Search</div>
-
-<div id="search">
-    <form action="${h.build_url(urls.channel_search, channel=channel)}" method="GET">
-        <fieldset>
-            <p>
-                <label for="q">Message:</label>
-                <input type="text" name="q" class="wide" />
-            </p>
-            
-            <p>
-                <label for="nick">By nickname:</label>
-                <input type="text" name="nick" />
-                <span>(optional)</span>
-            </p>
-
-            <p>
-                <label for="count">Results/page:</label>
-                <select name="count">
-                    ${h.select_options(((cc, cc_label) for cc, cc_label in config.SEARCH_LINE_COUNT_OPTIONS), count)}
-                </select>
-            </p>
-            
-            <input type="submit" value="Search" />
-        </fieldset>        
-    </form>
-    
-    <div id="search-help">
-        <p>Search powered by <a href="http://hyperestraier.sourceforge.net/">Hyper Estraier</a>:</p>
-
-        <ul>
-            <li>Group words together using quotes: <tt>"united nations"</tt></li>
-            <li>Searching for multiple words is AND: <tt>internet security</tt></li>
-            <li>To exclude terms, use <strong>!</strong> : <tt>hacker ! cracker</tt></li>
-            <li>Union (i.e. <q>or</q>) using <strong>|</strong> : <tt>proxy | firewall</tt></li>
-            <li>Search is case-insensitive</li>
-        </ul>
-    </div>
-</div>
-
-% else :
-<div id="title">${channel.title} :: Results\
-%   if search_query :
- With '${search_query}'\
-%   endif
-%   if search_nick :
- By ${search_nick}\
-%   endif
-</div>
-
-${paginate(urls.channel_search, count, page, max, channel=channel, q=search_query, t=search_targets, _more=True, _last=not(bool(lines)))}
-% if lines :
-<%include file="lines.tmpl" />
-% else :
-<div id="search-error">No results found</div>
-% endif
-${paginate(urls.channel_search, count, page, max, channel=channel, q=search_query, t=search_targets, _more=True, _last=not(bool(lines)))}
-% endif
--- a/templates/inc_paginate.tmpl	Sun Sep 13 18:57:48 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-## special overrides...
-<%def name="paginate_left()">
-
-</%def>
-
-<%def name="paginate(url, count, page_cur, page_max, _more=None, _last=False, **args)">
-    <%doc>
-        Pagination works using page numbers, with a specific number of maximum pages displayed. If _more is True,
-        then instead of a "Next" button, we have a "More" button, which goes to the max+1'th page, unless _last is
-        True, whereupon it's not displayed.
-
-        Can be called using <%call>, whereupon caller.left/caller.right can define additional data to display at the
-        left/right of the pagination ul.
-    </%doc>
-    <div class="paginate">
-        <ul>
-        % if caller and caller.right :
-            <li class="paginate-right">
-                ${caller.right()}
-            </li>
-        % endif
-        % if caller and caller.left :
-            <li class="paginate-left">
-                ${caller.left()}
-            </li>
-        % endif
-            <li>
-            % if page_cur > 1 :
-                <a href="${h.build_url(url, count=count, page=page_cur-1, max=max, **args)}">&laquo; Prev</a>
-            % else :
-                <span>&laquo; Prev</span>
-            %endif
-            </li>
-        % for page in xrange(1, page_max + 1) :
-            <li>
-            % if page == page_cur :
-                <strong>${page}</strong>
-            % else :
-                <a href="${h.build_url(url, count=count, page=page, max=page_max, **args)}">${page}</a>
-            % endif
-            </li>
-        % endfor
-        % if _more and not _last :
-            <li>&hellip;</li>
-        % endif
-            <li>
-            % if _more and _last :
-                <span>More &raquo;</span>
-            % elif _more : 
-                <a href="${h.build_url(url, count=count, page=page_max+1, **args)}">More &raquo;</a>
-            % elif page_cur == page_max : ## last page
-                <span>Next &raquo;</span>
-            % else : 
-                <a href="${h.build_url(url, count=count, page=page_cur+1, **args)}">Next &raquo;</a>
-            % endif
-            </li>
-        </ul>
-    </div>
-</%def>
-
--- a/templates/index.tmpl	Sun Sep 13 18:57:48 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-<%inherit file="layout.tmpl" />
-
-<div id="title">Available Channels</div>
-
-<ul>
-% for channel in channel_list :
-    <li><a href="${urls.channel.build(req, channel=channel)}">${channel.title}</a></li>
-% endfor
-</ul>
-
--- a/templates/layout.tmpl	Sun Sep 13 18:57:48 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-
-<%def name="menu()">
-<ul>
-    <li><a href="${urls.index.build(req)}">Home</a></li>
-    <li><a href="${urls.preferences.build(req)}">Preferences</a></li>
-    
-    <li>
-        <span>Channel:</span>
-    </li><li class="join-left">
-        <form action="${urls.channel_select.build(req)}" method="GET">
-            <select name="channel">
-            ${h.select_options(((ch.id, ch.title) for ch in channel_list), channel.id if channel else None)}
-            </select><input type="submit" value="Go &raquo;" />
-        </form>
-    </li>
-</ul>
-</%def>
-
-<%def name="footer_right()">
-
-</%def>
-
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
-    <head>
-        <title>irclogs.qmsk.net${(' :: ' + channel.title) if channel else ''}</title>
-        <link rel="Stylesheet" type="text/css" href="${req.site_root}/static/irclogs.css" />
-    ## timezone-autodetect
-    % if prefs and prefs.is_default('timezone') :
-        <script language="Javascript" type="text/javascript">
-/*
- * Set a preference cookie
- */
-function set_pref (name, value) {
-    // XXX: expire?
-    document.cookie = (name + "=" + value + "; path=/");
-}
-
-/*
- * Set the timezone_offset cookie to the current Date's timezone offset
- */
-function autodetect_tz_offset () {
-    // current datetime
-    var now = new Date();
-
-    // timezone offset from UTC in minutes
-    var timezone_offset = -now.getTimezoneOffset()
-    
-    // store cookie with offset in minutes
-    set_pref('timezone_offset', timezone_offset);
-}
-
-/*
- * Autodetect at load
- */
-window.onload = autodetect_tz_offset;
-        </script>
-    % endif
-    </head>
-    <body>
-        <div id="menu">
-            ${next.menu()}
-        </div>
-
-        <div id="content">
-            ${next.body()}
-        </div>
-
-        <div id="footer">
-            <div id="footer-right">
-                ${next.footer_right()}
-            </div>
-
-            <div id="footer-left">
-                <a href="http://projects.qmsk.net/irclogs2">irclogs2</a> version ${h.version_link()}
-            </div>
-
-            <div id="footer-center">
-                ${h.validation_notice(req.site_host)}
-            </div>
-        </div>
-    </body>
-</html>
-
--- a/templates/lines.tmpl	Sun Sep 13 18:57:48 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-<% formatter = prefs['formatter'] %>
-
-<div id="lines">
-% if formatter.html_fixedwidth :
-<pre>
-% endif
-% for line, html in lines:
-<a href="${h.build_url(urls.channel_link, channel=channel, timestamp=line.timestamp)}#${h.utc_timestamp(line.timestamp)}" id="${h.utc_timestamp(line.timestamp)}">&raquo;&raquo; </a>${html}
-% endfor
-% if formatter.html_fixedwidth :
-</pre>
-% endif
-</div>
--- a/templates/preferences.tmpl	Sun Sep 13 18:57:48 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,67 +0,0 @@
-<%inherit file="layout.tmpl" />
-
-<div id="title">Edit Preferences</div>
-
-<form action="${urls.preferences.build(req)}" method="POST">
-    <fieldset>
-        <legend>Dates / Times</legend>
-        
-        <p>
-            <label for="timezone">Timezone:</label>
-            <select name="timezone">
-            ${h.select_options(prefs.pref('timezone').OPTIONS, prefs.build('timezone'))}
-            </select>
-            <span class="example">(${h.tz_name(prefs['timezone'])})</span>
-        </p>
-
-        <p>
-            <label for="date_format">Date format:</label>
-            <input type="text" name="date_format" value="${prefs['date_format']}" />
-            <span class="example">(${h.fmt_date()})</span>
-        </p>
-        
-        <p>
-            <label for="time_format">Time format:</label>
-            <input type="text" name="time_format" value="${prefs['time_format']}" />
-            <span class="example">(${h.fmt_time()})</span>
-        </p>
-
-    </fieldset>
-
-    <fieldset>
-        <legend>Log Output</legend>
-        
-        <p>
-            <label for="formatter">Formatter:</label>
-            <select name="formatter">
-                ${h.select_options(((fmt_name, fmt.title) for fmt_name, fmt in preferences.formatter.formatters.iteritems()), prefs['formatter'].name)}
-            </select>
-        </p>
-
-        <p>
-            <label for="count">Lines / Page:</label>
-            <input type="text" name="count" value="${prefs['count']}" />
-<!--            <span class="example">(Blank for infinite)</span> -->
-        </p>
-    </fieldset>
-    
-    <fieldset>
-        <legend>Image Options</legend>
-
-        <p>
-            <label for="image_font">Font:</label>
-            <select name="image_font">
-                ${h.select_options(((name, title) for name, (path, title) in config.FORMATTER_IMAGE_FONTS.iteritems()), prefs['image_font'][0])}
-            </select>
-        </p>
-
-        <p>
-            <label for="image_font_size">Font size:</label>
-            <input type="text" name="image_font_size" value="${prefs['image_font_size']}" />
-            <span class="example">pt</span>
-        </p>
-    </fieldset>
-
-    <input type="submit" value="Save" />
-</form>
-