# -*- coding: UTF-8 -*-
import sys
try:
    import wx
except:
    print('Could not load wx module.')
    sys.exit()
try:
    import wx.html
except:
    print('Could not load wx.html module.')
    sys.exit()
try:
    from wx.lib.stattext import GenStaticText
    from wx.lib.expando import ExpandoTextCtrl, EVT_ETC_LAYOUT_NEEDED
except:
    print('Could not import wx.lib.expando.')
    sys.exit()
try:
    import wx.lib.colourdb
except:
    print('Could not import wx.lib.colourdb.')
    sys.exit()

import time
from threading import Timer, Thread
from sched import scheduler
from etm.data import *
from etm.cal import *
from etm.v import version
from etm.sunmoon import *
from etm.weather import *
from etm.busyfree import *

# etmalerts = os.path.join(homedir, ".etm", "etmalerts")
# etmlock = os.path.join(homedir, ".etm", "etmlock")
# etmlock_ = os.path.join(homedir, ".etm", "etmlock_")


import locale
wx.SetDefaultPyEncoding(encoding)

startdate = today
stopdate = agendadate

context_group_keys = []
keyword_group_keys = []

entry_html = """
<br>
<b>
    <em>&lt;Esc&gt; cancels entry;  &lt;Return&gt; accepts entry.
    <br>
    Select: `d (date), `c (context) or `k (keyword).</em>
</b>
"""

date_html="""\
<font size="+1"><b>date, time and list details</b></font>
<pre>
%s
</pre>
""" % "\n".join(list_text)

project_html=["<title>project options</title>",
'<body text="%s" bgcolor="%s">' % (main_fgcolor, main_bgcolor), 
'<font size="+1"><b>The project line format</b></font>',
entry_html,
"""\
<pre>
%s
</pre>
</body>
""" % "\n".join(project_text)]

action_html= ["<title>action options</title>",
'<body text="%s" bgcolor="%s">' % (main_fgcolor, main_bgcolor), 
'<font size="+1"><b>The format for action lines</b></font>',
entry_html,
"""\
<pre>
%s
</pre>
</body>
""" % "\n".join(action_text)]

event_html=["<title>event options</title>",
'<body text="%s" bgcolor="%s">' % (main_fgcolor, main_bgcolor), 
'<font size="+1"><b>The format for event lines</b></font>',
entry_html,
"""
<pre>
%s
</pre>
""" % "\n".join(event_text)]

task_html=["<title>task options</title>",
'<body text="%s" bgcolor="%s">' % (main_fgcolor, main_bgcolor), 
'<font size="+1"><b>The format for task lines</b></font>',
entry_html,
"""\
<pre>
%s
</pre>
</body>
""" % "\n".join(task_text)]

help_text = """\
        <title>etm: event and task manager</title>
<center>
Copyright %s Daniel A Graham. All rights reserved.
<br>
%s</center>
<pre>
<b>General</b>
   Ctrl-Q   Quit etm.
   Ctrl-P   Print the current display.
   Ctrl-S   Save the current agenda, list, busy/free or reckoning view
            as formatted text to the system clipboard.
   Escape   Cancel the current action.

<b>Showing information:</b>
    F1      Toggle displaying this help message.
    F2      Toggle displaying a twelve month calendar starting
            with the previous month.
    F3      Force reloading of event, task and action data.
    q       Show the status of the background alert warnings queue.
    c       Prompt for a date expression of the form 'date (+-) string'
            where 'string' is either a date or an integer followed
            by the word 'days' and return the result. E.g., 'dec 1 +
            90 days' or 'nov 30 - sep 1'. Note: '+' cannot be used
            when 'string' is a date.
    n       Check for a later version of etm.
    w       Show current Yahoo weather data.
    d       Show current USNO sun and moon data.

<b>Creating new entries and changing existing ones</b>
    e:  Create a new event. Prompt for file number, display event 
        help and prompt for entry.
    t:  Create a new task. Prompt for file number, display task help
        and prompt for entry.
    i:  Start the interval timer for a new action first prompting for an
        initial entry for the action. Pressing 'i' again pauses a running
        timer or restarts a paused timer. Pressing 'I' stops the timer,
        prompts for a file number to record the entry, shows action help
        information and prompts for the completion of an entry that
        already contains the date and elapsed time.
    p:  Create a new project. Prompt for file name, display project 
        help and prompt for entry.
    f:  Mark a task finished. Prompt for the task number. If an argument
        is provided it will be (fuzzy) parsed for the completion date.
        Otherwise TODAY (today's date) will be used. 
    u:  Mark a task unfinished.
    m:  Modify an existing task, event or action. Prompt for the 
        number.
    P:  Open an existing project for editing. Display a numbered list 
        of existing projects and prompt for the file number to use.
    D:  Delete an existing task, event, action or action. Prompt for
        the item number. Warning. This cannot be undone!

    Note: After pressing f, u, m or D, you will need to enter the id of 
          the relevant item. This id is a string of from 1 to 3 lower
          case letters followed by ')' that will be prepended to the
          title of the item after pressing one of these keys.

<b>Viewing tasks, events and actions</b>
    a  A    (or space) Display the agenda view using default values (a,
            space) or prompting for options and showing detailed agenda
            usage information (A).
    b  B    Display the busy/free times view using default values (b) 
            or prompting for options and detailed showing busy/free 
            usage information (B).
    l  L    Display the list view of events, actions and tasks using 
            default values (l) or prompting for options and showing 
            detailed list usage information (L).
    r  R    Display the reckoning view of time spent in events and 
            actions using default values (r) or prompting for options
            and showing detailed reckoning usage information (R).
    v       Toggle verbose display in agenda and list views.
    .       (or Right-Arrow, &gt;) Shift the period displayed forward by
            the number of days currently being displayed. [Not available
            in agenda view.]
    ,       (or Left-Arrow, &lt;) Shift the period displayed backward by
            the number of days currently being displayed. [Not available
            in agenda view.]
    /       Reset the period displayed to that initially displayed. [Not
            available in agenda view.]

<b>Entry bar editing keys</b>
    &lt;Esc&gt;: cancel the entry dialog. 
    &lt;Return&gt;: accept the entry and end the dialog. 

    The following 2-key sequences are also available (backward quote 
    key first and then the letter key):
        `d: select date from a calendar dialog. 
        `c: select context from a list of previously used contexts.
        `k: select keyword from a list of previously used keywords.
        `h: select options from a list of previously used option 
            strings (only when entering options for agenda, list, busy
            and reckoning views).
    The selection will be inserted in the entry bar at the current 
    cursor location. 
</pre>
""" % (copyright, sysinfo())

agendaview_html = """
<title>etm</title>
<body text="%s" bgcolor="%s"> 
<font size="+1"><b>agenda view</b></font>
<br>
<em>&lt;Esc&gt; cancels entry; &lt;Return&gt; accepts entry.<br>
Select: `d (date), `c (context), `k (keyword) or `h (entire option string from history list).</em>
<pre>
<b>options:</b>
  -d DAYS  Positive integer. The number of days to display. Default:
           %s.
  -e END   Date. Display tasks/events beginning with BEGIN and ending 
           with this date (fuzzy parsed). Default: %s.
  -f FIND     Regular expression. Show items containing FIND (ignoring
              case) in the task title or note within the BEGIN ~ END
              interval.
  -c CONTEXT  Regular expression. Show items with contexts matching 
              CONTEXT (ignoring case) within the BEGIN ~ END interval.
  -p PROJECT  Regular expression. Show items with projects matching 
              PROJECT (ignoring case) within the BEGIN ~ END interval.
  -k KEYWORD  Regular expression. Show items with contexts matching 
              KEYWORD (ignoring case) within the BEGIN ~ END interval.
  -s SHOW     String. Show events, tasks and/or actions depending 
              upon whether SHOW contains 'e', 't'/'T' and/or 'a'. 
              Using 'T' rather than 't' limits the display of tasks to
              those which are unfinished. Default: eta.
</pre>
</body>
""" % (main_fgcolor, main_bgcolor, agenda, agendadate)

listview_html = """
<title>etm</title>
<body text="%s" bgcolor="%s">
<font size="+1">List view</font>
<br>
<em>&lt;Esc&gt; cancels entry; &lt;Return&gt; accepts entry.<br>
Select: `d (date), `c (context), `k (keyword) or `h (entire option 
string from history list).</em>
<pre>
<b>options:</b>
  -d DAYS     Positive integer. The number of days to display. 
              Default: %s.
  -b BEGIN    Date. Display tasks/events beginning with this date 
              (fuzzy parsed) and including DAYS days. Default:
              %s.
  -e END      Date. Display tasks/events beginning with BEGIN and 
              ending with this date (fuzzy parsed). Default: 
              %s.
  -g GROUPBY  An element from [d,p,c,k] where:
              d: group by date
              j: group by project
              c: group by context
              k: group by keyword
              Default: d.
  -f FIND     Regular expression. Show items containing FIND (ignoring
              case) in the task title or note within the BEGIN ~ END
              interval.
  -c CONTEXT  Regular expression. Show items with contexts matching 
              CONTEXT (ignoring case) within the BEGIN ~ END interval.
  -p PROJECT  Regular expression. Show items with projects matching 
              PROJECT (ignoring case) within the BEGIN ~ END interval.
  -k KEYWORD  Regular expression. Show items with contexts matching 
              KEYWORD (ignoring case) within the BEGIN ~ END interval.
  -s SHOW     String. Show events, tasks and/or actions depending 
              upon whether SHOW contains 'e', 't'/'T' and/or 'a'. 
              Using 'T' rather than 't' limits the display of tasks to
              those which are unfinished. Default: eta.
  -x EXPORT   Export list view items in iCal format to file EXPORT.ics
              in %s.
</pre>
</body>
""" % (main_fgcolor, main_bgcolor, next, pastdate, nextdate, etmical)

busyview_html = """
<body text="%s" bgcolor="%s">
<font size="+1">Busy/free view</font>
<br><em>&lt;Esc&gt; cancels entry; &lt;Return&gt; accepts entry.<br>
Select: `d (date), `c (context), `k (keyword) or `h (entire option string from history list).</em>
<pre>
<b>options:</b>
  -d DAYS     Positive integer. The number of days to display. Default:
              %s.
  -b BEGIN    Date. Display tasks/events beginning with this date 
              (fuzzy parsed) and continuing for the next DAYS days.
              Default: %s.
  -e END      Date. Display tasks/events beginning with BEGIN and 
              ending with this date (fuzzy parsed). Default:
              %s.
  -s SLACK    Positive integer. Skip SLACK minutes before and after
              busy periods when computing free periods. Default: %s.
  -m MINIMUM  Positive integer. The minimum length in minutes for an
              unscheduled period to be displayed. Default:
              %s.
  -o OPENING  Time. The opening or earliest time (fuzzy parsed) to be
              considered when displaying unscheduled periods. 
              Default: %s.
  -c CLOSING  Time. The closing or latest time (fuzzy parsed) to be
              considered when displaying unscheduled periods. Default:
              %s.
  -i INCLUDE  String containing one or more of the letters 'b' 
              (include busy time bars, 'B' (include busy times), 'f'
              (include free time bars) and/or 'F' (include free
              times). Default: %s.

</pre>
</body>
""" % (main_fgcolor, main_bgcolor, next, pastdate, nextdate, slack, minimum, opening, closing, include)

reckoningview_html = """
<title>etm</title>
<body text="%s" bgcolor="%s">
<font size="+1">Reckoning view</font>
<br>
<em>&lt;Esc&gt; cancels entry; &lt;Return&gt; accepts entry.<br>
Select: `d (date), `c (context), `k (keyword) or `h (entire option string from history list).</em>
<pre>
<b>options:</b>
  -d DAYS        Positive integer. The number of days to display. 
                 Default: %s.
  -b BEGIN       Date. Display tasks/events beginning with this date 
                 (fuzzy parsed) and continuing for the next DAYS days.
                 Default: %s.
  -e END         Date. Display tasks/events beginning with BEGIN and 
                 ending with this date (fuzzy parsed). Default:
                 %s.
  -C C_POSITION  Integer. Subtotal by category if C_POSITION is 
                 positive and in an order corresponding to C_POSITION.
                 Default 0.
  -D D_POSITION  Integer. Subtotal by date if D_POSITION is positive
                 and in an order corresponding to D_POSITION. 
                 Default 1.
  -K K_LEVEL     Integer. Subtotal by keywords to a depth corresponding
                 to the value of K_LEVEL if positive. E.g., if K_LEVEL
                 is 2, and there is an item with keyword 'a:b:c', then
                 subtotals corresponding to 'a' and 'a:b' would be
                 formed. Default 2.
  -I             True/False. Subtotal by title if True. Default: False.
</pre>
</body>
""" % (main_fgcolor, main_bgcolor, next, pastdate, nextdate)

help_html="""\
<title>etm</title>
<body text="%%s" bgcolor="%%s">
%s
<font size="+2">Project files</font>
<p />
Each file in
<pre>
    '%s'
</pre>
('etmdata' in ~/.etm/etmrc) that has the suffix '<tt>txt</tt>' is treated
as a separate project and must begin with a line having the following
format and be followed by task, event and action lines. All tasks, events
and actions used by etm are contained in these project files.
<p />
%s
%s
%s
%s
%s
</body>
""" % (help_text, etmdata, project_html[2]+project_html[4], task_html[2]+task_html[4], event_html[2]+event_html[4], action_html[2]+action_html[4], date_html)

# TODO: add discussion of automatically created files.

class MyHtmlWindow(wx.html.HtmlWindow):
    # Keep it from taking the focus
    def AcceptsFocus(*args, **kwargs):
        return False

class MyStaticText(GenStaticText):
    # Keep it from taking the focus
    def AcceptsFocus(*args, **kwargs):
        return False

class MyFrame(wx.Frame):
    def __init__(self, width, height):
        wx.Frame.__init__(self, None, -1, 'etm')
        self.frame_id = self.GetId()
        self.leader = False
        self.alert_count = 0
        self.alert_ids = []
        self.alert_msg = []
        self.alert_update = None
        self.prompt = "etm %s   F1: toggle help    (%s)" % (version, self.alert_count)
        self.entrybartext = ""
        self.verbose_toggle = True
        self.title = ""
        self.html = ""
        self.cur_detail = ""
        self.header = None
        self.clip = None
        self.footer = None
        self.minattr = None
        self.advance = 0
        self.shiftopts = {}
        self.today = datetime.date.today()
        self.lastyear = str(int(self.today.strftime("%Y"))-1)
        self.thismonth = self.today.strftime("%m")
        self.startdate = None
        self.stopdate = None
        self.in_getresponse = False
        self.show_calendar = False
        self.show_help = False
        self.history = {}
        self.filehash = {}
        self.linehash = {}
        self.options = {}
        self.function = ''
        self.orig_entry = ''
        self.currentpage = ''
        self.file = ''
        self.cmd = 'a'
        self.last_cmd = 'a'
        self.entry_args = []
        self.args = []
        self.key = None
        self.prnt= ""
        self.view_memo = {}
        self.prnt_memo = {}
        self.clip_memo = {}
        self.begend_memo = {}
        self.timer_running = False
        self.timer_paused = False
        self.history['agenda'] = agenda_history
        self.history['list'] = list_history
        self.history['busy'] = busy_history
        self.history['reckoning'] = reckoning_history
        self.display = 'agenda'
        self.show_hist = False
        self.timer_active = False
        tfont = wx.Font(basefontsize, wx.DEFAULT,
                wx.ITALIC, wx.BOLD)
        efont = wx.Font(basefontsize, wx.DEFAULT,
                wx.NORMAL, wx.NORMAL)
        dfont = wx.Font(basefontsize, wx.DEFAULT,
                wx.ITALIC, wx.NORMAL)

        # the details bar
        self.detail_bar = MyStaticText(self, -1, " ", size = (-1,-1), 
                style = wx.BORDER_NONE | wx.ST_NO_AUTORESIZE)
        self.detail_bar.SetFont(dfont)
        self.detail_bar.SetForegroundColour(detail_fgcolor)
        self.detail_bar.SetBackgroundColour(detail_bgcolor)
        self.detail_bar_id = self.detail_bar.GetId()
        # the entry bar and key binding
        self.entry_bar = ExpandoTextCtrl(self, size=(-1,-1),
            value="This control will expand as you type", 
            style = wx.BORDER_NONE)
        self.entry_bar.SetBackgroundColour('%s' % main_bgcolor)
        self.entry_bar.SetForegroundColour('%s' % main_fgcolor)
        self.entry_bar.SetFont(efont)

        self.Bind(EVT_ETC_LAYOUT_NEEDED, self.OnRefit, self.entry_bar)
        self.entry_bar.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) 

        self.entry_bar.Bind(wx.EVT_CHAR, self.OnEntryBarChar)

        # the html window
        self.htmlwin = MyHtmlWindow(self, wx.ID_ANY, 
                size = (width, height), style=wx.BORDER_NONE)
        self.htmlwin.Bind(wx.EVT_CHAR, self.OnChar)
        self.htmlwin.SetFonts('', '', [i for i in range(htmlfont,
            htmlfont+13,2)])
        self.htmlwin.SetBorders(0)
        self.htmlwin.SetRelatedFrame(self, "%s")
        self.htmlwin.SetBorders(5)
        self.printer = wx.html.HtmlEasyPrinting()
        self.printer.SetFonts('', '', [i for i in range(htmlprintfont,
            htmlprintfont+13, 2)])
        self.printdata = self.printer.GetPrintData()
        self.printdata.SetColour(False)

        vbox = wx.BoxSizer(wx.VERTICAL)
        vbox.Add(self.htmlwin, 1, wx.EXPAND)
        vbox.Add(self.detail_bar, 0, wx.EXPAND)
        vbox.Add(self.entry_bar, 0, wx.EXPAND)
        self.SetSizerAndFit(vbox)
        self.setprompt(True)
        self.Bind(wx.EVT_CLOSE, self.OnQuit)
        self.data = ETMData()
        self.Refresh()
        msg, info = self.data.loadData()
        if msg:
            self.show_errors(msg)
        elif info:
            self.setmessage(info)
        self.show_view(self.cmd, self.args)
        self.start_alerts()

    def OnKillFocus(self, event): 
        self.entry_bar.SetFocus()

    def getPos(self):
        self.x_pos, self.y_pos = self.htmlwin.GetViewStart()

    def restorePos(self):
        self.htmlwin.Scroll(self.x_pos, self.y_pos)

    def show_view(self, cmd, args):
        """Display the view for cmd using args and the value of
        self.verbose_toggle, caching the result. If called again with the
        same cmd, args and verbose_ toggle, the cache value will be
        displayed."""
        self.cmd = cmd
        self.args = args
        if self.data.changed or self.data.today != datetime.date.today():
            # reload data and clear the cache
            self.data.loadData()
            if self.data.load_errors:
                self.show_errors(self.data.load_errors)
            self.view_memo.clear()
            self.begend_memo.clear()
        key = (self.cmd, tuple(self.args), self.data.today, 
                self.verbose_toggle, self.data.show_idnums, self.advance)
        self.key = key
        if key not in self.view_memo:
            (options, err) = parse_args(self.cmd, self.args)
            if self.cmd != 'a' and self.advance:
                options['begin'] = options['begin'] + \
                        self.advance*(options['days'])*oneday
                options['end'] = options['end'] + \
                        self.advance*(options['days'])*oneday
            minattr = -2 
            if self.verbose_toggle:
                minattr = -2
            else:
                minattr = -1
            h, l, f = self.data.prepare_view(options)
            self.header = h
            self.clip_memo[key] = l
            self.footer = f
            self.minattr = minattr
            lst = [ "<title>%s</title>" % h.str() ]
            prnt = [ "<title>%s</title>" % h.str() ]
            prnt.append('<body text="%s">' % (print_fgcolor))
            lst.append('<body text="%s" bgcolor="%s">' % (main_fgcolor,
                    main_bgcolor))
            if self.cmd in ['a', 'l']:
                # table output
                lst.append('<table width="100%" border="0" cellpadding="0"'
                        ' cellspacing="2" columns="4">')
                prnt.append('<table width="100%" border="0" cellpadding="0"'
                        ' cellspacing="2" columns="4">')
                for i in l:
                    lst.append(i.htm(minattr))
                    prnt.append(i.htm(minattr,True))
                lst.append('</table>') 
                prnt.append('</table>') 
            else:
                # preformated output
                lst.append('<pre>')
                prnt.append('<pre>')
                for i in l:
                    lst.append(i.pre(minattr))
                    prnt.append(i.pre(minattr, True))
                lst.append('</pre>')
                prnt.append('</pre>')
            lst.append('</body>')
            prnt.append('</body>')
            self.view_memo[key] = "\n".join(lst)
            self.prnt_memo[key] = "\n".join(prnt)
            self.begend_memo[key] = [options['begin'], options['end']]
        if self.cmd in ['a', 'l']:
            if self.verbose_toggle:
                self.prompt = "etm %s    F1: toggle help   v: hide details" % (version)
            else:
                self.prompt = "etm %s    F1: toggle help   v: show details" % (version)
        else:
            self.prompt = "etm %s    F1: toggle help " % (version)
        self.html = self.view_memo[key]
        self.prnt = self.prnt_memo[key]
        self.clip = self.clip_memo[key]
        self.currentpage = self.html
        self.htmlwin.SetPage(self.html)
        self.Refresh()
        self.setprompt(True)

    def view2Clipboard(self):
        lst = [ self.header.str() ]
        for i in self.clip:
            lst.append(i.str(self.minattr))
        text = "\n".join(lst)
        self.do = wx.TextDataObject()
        self.do.SetText(text)
        if wx.TheClipboard.Open():
            wx.TheClipboard.SetData(self.do)
            wx.TheClipboard.Close()
            self.setmessage('Saved current view to system clipboard.', True)
        else:
            self.show_errors("Unable to open the clipboard")

    def OnRefit(self, evt):
        # For the Expando control
        self.Fit()

    def setpage(self, html):
        # save title and html
        self.html = html
        self.currentpage = html
        self.showoutput()

    def setdates(self, startd, stopd):
        self.startdate = startd
        self.stopdate = stopd

    def showtable(self, h,l):
        l.insert(0, "<title>%s</title>" % h)
        l.insert(1, '<table width="100%" border="0" cellpadding="0" cellspacing="2" columns="4">')
        l.append("</table>")
        html = "\n".join(l)
        self.currentpage = html
        self.setpage(html)

    def showoutput(self, html):
        self.setmessage('<Esc> returns to %s display ' % self.display)
        self.currentpage = html
        self.htmlwin.SetPage(html)
        self.Refresh()

    def setprompt(self, force=False):
        if force or self.timer_active:
            self.cur_detail = "  %s" % self.prompt
            self.detail_bar.SetLabel("%s    (%s)" % 
                (self.cur_detail, self.alert_count))
            self.entry_bar.SetValue('')
            self.entry_bar.Enable(False)
            self.entry_bar.SetBackgroundColour('%s' % main_bgcolor)
            self.entry_bar.SetForegroundColour('%s' % main_fgcolor)
            self.htmlwin.SetFocus()
            self.Refresh()
            self.timer_active = False
            
    def refreshprompt(self):
        # subtract 1 since the queue will include the next update
        self.detail_bar.SetLabel("%s    (%s)" % 
            (self.cur_detail, self.alert_count))
        self.Refresh()

    def setmessage(self, message, pause=False):
        fmtmsg = "  %s" % message
        self.detail_bar.SetLabel("%s    (%s)" % (fmtmsg, self.alert_count))
        self.entry_bar.SetValue('')
        self.entry_bar.Enable(False)
        self.entry_bar.SetBackgroundColour('%s' % main_bgcolor)
        self.entry_bar.SetForegroundColour('%s' % main_fgcolor)
        self.Refresh()
        self.htmlwin.SetFocus()
        if pause:
            self.timer_active = True
            timer = Timer(delay, self.setprompt)
            timer.start()
        else:
            self.cur_detail = fmtmsg

    def start_alerts(self):
        self.alert_sched = scheduler(time.time, time.sleep)
        self.alert_thread = Thread(target=self.alert_sched.run)
        self.alert_sched.enter(0, 0, self.update_alerts, ())
        self.alert_thread.start()

    def stop_alerts(self):
        if not self.alert_sched.empty():
            for a in self.alert_ids:
                try:
                    self.alert_sched.cancel(a)
                except:
                    pass
        return(True)

    def alert_status(self):
        curtime = time.time()
        ret = ['<title>alert status</title>',
            '<body text="%s" bgcolor="%s">', 
                '<pre>']
        lastmtime = self.alert_update
        nextsec = int(lastmtime + 60)
        nexttime = datetime.datetime.fromtimestamp(nextsec).strftime("%H:%M:%S")
        difftime = int(lastmtime + inc - curtime)
        if difftime >= 0:
            lines = self.alert_msg
            if len(lines) > 0:
                ret.extend(["The following alerts are currently in the queue:", ''])
                for line in lines:
                    ret.append('    <font color="magenta">%s</font>' % line)
            else:
                ret.append("The alert queue is currently empty.")
            ret.extend(['', 
                "The next update will be %s seconds from now at %s." %
                (difftime, nexttime)])
        ret.append("</pre></body>")
        html = "\n".join(ret)
        return(html)
    
    def update_alerts(self):
        self.alert_msg = []
        try:
            # schedule next update
            self.alert_sched.enter(60, 0, self.update_alerts, ())
            self.clear_alerts()
            self.load_alerts()
            self.alert_update = time.time()
            queue = self.alert_sched.queue
            self.alert_count = len(self.alert_sched.queue) - 1  
            for e in queue:
                m = " ".join(e[3]).strip()
                dt = datetime.datetime.fromtimestamp(e[0])
                tfmt = dt.strftime(timefmt)
                if m:
                    self.alert_msg.append("%s: %s" % (tfmt, m))
            self.refreshprompt()
        except:
            pass
                    
    def clear_alerts(self):
        if not self.alert_sched.empty():
            for a in self.alert_ids:
                try:
                    self.alert_sched.cancel(a)
                except:
                    pass

    def load_alerts(self):
        self.alert_ids = []
        # self.data.alerts is a list of tuples: (title, starttime, minutes) 
        # where minutes is a list of integer minutes before
        # starttime at which alerts should be given 
        for event in self.data.alerts:
            (title, starttime, alerts) = event
            n = parse(starttime)
            now = datetime.datetime.now()
            for alert in alerts:
                t = n - alert * oneminute
                if t < now:
                    continue
                if alert > 1:
                    msg = "%s %s %s" % (title, alert, minutes)
                elif alert == 1:
                    msg = "%s 1 %s" % (title, minute)
                else:
                    msg = "%s %s" % (title, rightnow)
                at = time.mktime(t.timetuple())
                id = self.alert_sched.enterabs(at, 1, self.onAlert, (msg,))
                self.alert_ids.append(id)
                
    def onAlert(self, msg):
        T = time.strftime(timefmt, time.localtime(time.time()))
        if use_ampm:
            T = leadingzero.sub('', T)
        t = "%s %s" % (thetimeis, T)
        rephash = {'t' : t, 'T' : T, 'm' : msg}
        cmd = alertcmd % rephash
        os.system(cmd)
        self.refreshprompt()

    def getresponse(self, prompt, default="", title="", html=""):
        self.in_getresponse = True
        self.entrybartext = ""
        if html != "":
            self.showoutput(html)
        self.timer_active = False
        self.detail_bar.SetLabel("  %s" % prompt)
        # reverse colors when active
        self.entry_bar.SetBackgroundColour('%s' % entry_bgcolor)
        self.entry_bar.SetForegroundColour('%s' % entry_fgcolor)
        self.entry_bar.Enable(True)
        self.entry_bar.SetFocus()
        self.entry_bar.WriteText(default)
        self.Refresh()

    def OnPrint(self, event):
        self.printer.SetHeader(
          '<center><font size="+1">%s</font></center>' % 
          self.htmlwin.GetOpenedPageTitle())
        self.printer.SetFooter(
                '<center>Page @PAGENUM@ of @PAGESCNT@</center>')
        self.printer.PrintText(self.prnt)

    def OnQuit(self):
        self.stop_alerts()
        self.Destroy()

    def cancel(self):
        self.in_getresponse = False
        self.cmd = self.last_cmd
        self.getPos()
        self.show_view(self.cmd, self.args)
        self.restorePos()
        self.setmessage("cancelled ", True)

    def EntryBarAppend(self, str):
        cur_pos = self.entry_bar.GetInsertionPoint()
        self.entry_bar.AppendText(str)
        self.entry_bar.SetInsertionPoint(cur_pos)

    def EntryBarInsert(self, str):
        self.entry_bar.WriteText(str)

    def EntryBarEnter(self, event):
        self.in_getresponse = False
        self.entrybartext = self.entry_bar.GetValue().strip()
        if self.entrybartext:
            return self.functionHash[self.function](self)
        else:
            self.cancel()

    def getDate(self):
        dialog = ETMCal()
        result = dialog.ShowModal()
        if result == wx.ID_OK and dialog.selecteddate:
            retval = dialog.selecteddate
        else:
            retval = ''
        dialog.Destroy()
        return retval

    def getSelection(self, prompt, choices, dflt=None):
        dlg = wx.SingleChoiceDialog(self, prompt, 'etm', 
                choices, wx.CHOICEDLG_STYLE)
        if dflt:
            dlg.SetSelection(dflt)
        if dlg.ShowModal() == wx.ID_OK:
            return dlg.GetStringSelection()
        else:
            return ""

    def getFile(self, type, mode='append'):
        file = None
        if mode == 'create':
            filedlg = wx.FileDialog(
                self, message = "New project file", 
                defaultDir = etmdata, 
                defaultFile="", 
                wildcard="etm project file (*.txt)|*.txt",
                style=wx.SAVE
            )
            if filedlg.ShowModal() == wx.ID_OK:
                file = filedlg.GetPath()
                name, ext = os.path.splitext(file)
                fname = "%s.txt" % name
                if os.path.isfile(fname):
    	            self.show_errors(
                            ["Error: file '%s' already exists" % filename])
                    return()
                return(fname)
            else:
                return()
        elif mode == 'open':
            filedlg = wx.FileDialog(
                self, message = "Open project file in external editor", 
                defaultDir = etmdata, 
                defaultFile="", 
                wildcard="etm project file (*.txt)|*.txt",
                style=wx.OPEN
            )
            if filedlg.ShowModal() == wx.ID_OK:
                file = filedlg.GetPath()
                return(file)
            else:
                return()
        else:
            if type == 'event':
                cur = self.data.current_hash[event]
            elif type == 'task':
                cur = self.data.current_hash[task]
            elif type == 'action':
                cur = self.data.current_hash[action]
            flst = self.data.projects.keys()
            dflt = flst.index(cur)
            if mode == 'append':
                file = self.getSelection("The project file for the new %s" %
                        type, flst, dflt)
            elif mode == 'open':
                file = self.getSelection("The project file to open with %s" %
                        editor, flst, dflt)
        if file:
            return(os.path.join(etmdata, file))
        else:
            return()


    def show_errors(self, msg):
        s = "\n".join(msg)
        dlg = wx.MessageDialog(self, s,
            'etm error',
            wx.OK 
            )
        res = dlg.ShowModal()
        dlg.Destroy()
        return(res)


    def confirm(self, msg, prompt="etm confirmation"):
        s = "\n".join(msg)
        dlg = wx.MessageDialog(self, s,
            prompt,
            wx.YES_NO | wx.YES_DEFAULT 
            )
        res = dlg.ShowModal()
        dlg.Destroy()
        return(res)


    def show_pre(self, h, l, setpage=False):
        html = """
        <title>%s</title>
        <pre>%s</pre>
        """ % (h, '\n'.join(l))
        if setpage:
            self.setpage(html,html)
        else:
            self.showoutput(html)

    def show_agenda(self):
        self.display = 'agenda'
        if self.cmd == 'A':
            if self.entrybartext not in self.history['agenda']:
                self.history['agenda'].append(self.entrybartext)
            arglst = str(self.entrybartext).split()
        else:
            arglst = []
        self.show_view('a', arglst)

    def show_list(self):
        self.display = 'list'
        self.advance = 0
        if self.cmd == 'L' and self.entrybartext:
            if self.entrybartext not in self.history['list']:
                self.history['list'].append(self.entrybartext)
            arglst = str(self.entrybartext).split()
        else:
            arglst = []
        self.cmd = 'l'
        self.show_view('l', arglst)

    def show_day(self):
        args = ("-b %s -d 1" % self.targetdate).split(' ')
        self.show_view('l', args)


    def show_busy(self):
        self.display = 'busy'
        if self.cmd == 'B' and self.entrybartext:
            if self.entrybartext not in self.history['busy']:
                self.history['busy'].append(self.entrybartext)
            arglst = str(self.entrybartext).split()
        else:
            arglst = []
        self.cmd = 'b'
        self.show_view('b', arglst)

    def show_reckoning(self):
        self.display = 'reckoning'
        if self.cmd == 'R':
            if self.entrybartext not in self.history['reckoning']:
                self.history['reckoning'].append(self.entrybartext)
            arglst = str(self.entrybartext).split()
        else:
            arglst = []
        self.cmd = 'r'
        self.show_view('r', arglst)

    def edit_entry(self):
        num = str(self.entrybartext)
        msg = []
        chrcode = ''
        f, n = self.data.idnum_hash[num]
        line = self.data.getlinefromfile(n, f)
        self.orig_entry = line
        m = task_regex.match(line)
        if m:
            chrcode = m.group(1)
            if chrcode == '+':
                entry_type = 't'
            elif chrcode == '-':
                entry_type = 't'
            else: # *, ~
                if chrcode == '*':
                    entry_type = 'e'
                elif chrcode == '~':
                    entry_type = 'a'
        else: 
            msg.append("    %s does not begin with a character in [+-*~]" % line)
        self.function = 'process_entry'
        self.entry_args = (entry_type, f, n, 'r')
        self.getresponse('options for modified %s: ' % self.typeHash[entry_type][0],
                line, 
                "modify %s" % self.typeHash[entry_type][0],
                "\n".join(self.typeHash[entry_type][1]))


    def new_entry(self):
        global timer_data
        entry_type = self.entry_type
        f = self.file
        self.function = 'process_entry'
        self.entry_args = (entry_type, f, 0, 'a')
        self.process_entry()

    def new_project(self):
        f = self.file
        self.function = 'process_entry'
        self.entry_args = ('j', f, 0, 'c')
        self.process_entry()

    def open_project(self):
        command = editold % {'e': editor, 'n': 1, 'f': self.file}
        os.system(command)
        self.data.changed = True
        self.show_view(self.cmd, self.args)
        self.setmessage('finished editing %s' % self.file, True)

    def mark_finished(self):
        res = str(self.entrybartext).split()
        num = res[0]
        if len(res) > 1:
            date = " ".join(res[1:])
            (ok, msg) = self.data.mark_complete(num, date)
        else:
            (ok, msg) = self.data.mark_complete(num)
        self.show_view(self.cmd, self.args)
        if ok:
            self.setmessage(msg, True)
        else:
            self.show_errors([msg])

    def mark_unfinished(self):
        res = str(self.entrybartext).split()
        num = res[0]
        (ok, msg) = self.data.mark_incomplete(num)
        self.show_view(self.cmd, self.args)
        if ok:
            self.setmessage(msg, True)
        else:
            self.show_errors([msg])

    def delete_item(self):
        idnum = str(self.entrybartext)
        f, n = self.data.idnum_hash[idnum]
        line = self.data.getlinefromfile(n, f)
        msg = [
        '  Do you really want to permanently delete the entry:',
        '',
        '    %s' % line
        ]
        ok = self.confirm(msg)
        if ok == wx.ID_YES:
            self.data.deletelineinfile(n, f)
            self.show_view(self.cmd, self.args)
            self.setmessage('created backup and deleted entry', True)
        else:
            self.cancel()

    def start_timer(self):
        #  global msg, timer_data, timer_running
        self.timer_running = True
        self.timer_paused = False
        self.timer_entry = self.entrybartext
        sdt = datetime.datetime.now()
        d = sdt.strftime(date_fmt)
        t = sdt.strftime(timefmt)
        if use_ampm:
            t = t.lower()[:-1]
        sec = 0
        self.timer_data = [sdt, sec]
        self.setmessage("Timer for '%s' started at %s. 'i' interrupts; 'I' stops." 
                % (self.timer_entry, t), True)

    def interrupt_timer(self):
        #  global msg, timer_data, timer_running
        self.timer_paused = True
        sdt, sec = self.timer_data
        edt = datetime.datetime.now()
        sec += (edt - sdt).seconds
        minutes = sec/60
        seconds = sec%60
        t = edt.strftime(timefmt)
        if use_ampm:
            t = t.lower()[:-1]
        self.timer_data = [edt, sec]
        self.setmessage("Timer for '%s' paused at %s; %sm %ss. 'i' restarts; 'I' stops." % (self.timer_entry, t, minutes, seconds), True)

    def restart_timer(self):
        #  global msg, timer_data, timer_running
        self.timer_paused = False
        edt, sec = self.timer_data
        sdt = datetime.datetime.now()
        minutes = sec/60
        seconds = sec%60
        d = sdt.strftime(date_fmt)
        t = sdt.strftime(timefmt)
        if use_ampm:
            t = t.lower()[:-1]
        self.timer_data = [sdt, sec]
        self.setmessage("Timer for %s restarted at %s; %sm %ss. 'i' interrupts; 'I' stops." 
                % (self.timer_entry, t, minutes, seconds), True)

    def stop_timer(self):
        #  global msg, timer_data, timer_running
        self.timer_running = False
        self.timer_paused = False
        sdt, sec = self.timer_data
        edt = datetime.datetime.now()
        d = edt.strftime(date_fmt)
        sec += (edt - sdt).seconds
        minutes = sec/60
        seconds = sec%60
        p = minutes
        if seconds > 0:
            p += 1
        t = edt.strftime(timefmt)
        if use_ampm:
            t = t.lower()[:-1]
        self.setmessage("Timer for '%s' stopped at %s; %sm %ss." % 
                (self.timer_entry, t, minutes, seconds), True)
        return(d, p)

    def process_entry(self):
        entry_type, file, linenum, mode = self.entry_args
        #  s = str(self.entrybartext)
        s = self.entrybartext
        s_orig = self.orig_entry
        if s == s_orig:
            self.cancel()
            return()
        # mode = a)ppend, r)eplace
        prefix = None
        m0 = []
        s_orig = s
        item = True                               
        msg = []
        writeentry = False
        s = endline_regex.sub('', s)
        title = ''
        sl = []
        m1, hsh = self.data.line2hash({},s, item)
        m2, tsk = self.data.check_hash(hsh, item)
        m3 = []
        m4 = []
        # get the possibly changed chrcode
        if 'chrcode' in tsk:
            if tsk['chrcode'] == '*':
                new_type = 'e'
            elif tsk['chrcode'] in ['+', '-']:
                new_type = 't'
            elif tsk['chrcode'] == '~':
                new_type = 'a'
            elif tsk['chrcode'] == '':
                new_type = 'j'
        else:
            new_type = 'unknown'
        if new_type != entry_type:
            # chrcode has changed
            if new_type in ['e', 't', 'a'] and entry_type in ['e', 't', 'a']:
                # neither is a project, ok to change
                m4.append("Warning: changing entry type from '%s' to '%s'." 
                    % (entry_type, new_type))
                entry_type = new_type
            else:
                # at least one must be 'j'
                m3.append("Error: it is not possible to change the entry type from '%s' to '%s'." % (entry_type, new_type))
        if entry_type == 'e':
            type = 'event'
            cur = self.data.current_hash[event]
            keys = event_keys
            prefix = '* '
        elif entry_type == 't':
            type = 'task'
            keys = task_keys
            cur = self.data.current_hash[task]
            prefix = '+ '
        elif entry_type == 'a':
            type = 'action'
            keys = action_keys
            cur = self.data.current_hash[action]
            prefix = '~ '
        elif entry_type == 'j':
            item = False
            type = 'project'
            keys = common_keys + ['b']
            prefix = ''
        else:
            m0.append("unrecognized entry_type: %s" % entry_type)


        if len(m1+m2) == 0:
            if item:
                if 'r' in tsk and tsk['r'] != None:
                    m3.extend(self.data.get_rrule(tsk))
                if 'chrcode' in tsk and 't' in tsk:
                    sl = ["%s %s" % (tsk['chrcode'], tsk['t'])]
                else:
                    m3.append(
                "	   Entry must begin with '+', '-', '*' or '~'")
            else:
                sl = ['%s' % tsk['j']]
        for key in keys:
            if has(tsk, key):
                sl.append("@%s %s" % (key, tsk[key]))
        msg.extend(m0+m1+m2+m3)

        if len(msg) > 0:
            if self.show_errors(msg) == wx.ID_CANCEL:
                self.cancel()
                return()
            else:
                return()

    	# no errors
    	s = " ".join(sl)
    	if mode == 'a':
    		prep = 'to'
    		slst =	m4 + ['', 'Append', '']
        elif mode == 'r':
    		prep = 'in'
    		slst =	m4 + ['', 'Replace', '']
    		slst.append("    %s" % self.orig_entry)
    		slst.extend(['',"with", ''])
        elif mode == 'c':
    		prep = 'in'
    		slst =	['Create project containing']
    		slst.append("    %s" % self.orig_entry)
    	slst.append("    %s" % s)
    	slst.extend(['', "%s '%s' ?"  % (prep, file)])
        ok = self.confirm(slst)
        if ok == wx.ID_YES:
            if mode == 'a':
                self.data.addline2file(s, file)
            elif mode == 'r':
                self.data.replacelineinfile(linenum, s, file)
            elif mode == 'c':
                self.data.makefilewithline(linenum, s, file)
            self.show_view(self.cmd, self.args)
        elif ok == wx.ID_CANCEL:
            self.cancel()


    def toggle_htmlcalendar(self):
        if self.show_calendar:
            self.show_calendar = False
            self.htmlwin.SetPage(self.currentpage)
        else:
            self.show_calendar = True
            self.show_help = False
            html = """\
<title>calendar</title>
<body text="%s" bgcolor="%s"> 
<pre>
%s
</pre>
""" % (main_fgcolor, main_bgcolor,  "\n".join(cal()))
            self.html = html
            self.prnt = html
            self.htmlwin.SetPage(self.html)

    def toggle_help(self):
        if self.show_help:
            self.show_help = False
            self.htmlwin.SetPage(self.currentpage)
        else:
            self.show_help = True
            self.show_calendar = False
            self.html = help_html % (main_fgcolor, main_bgcolor)
            self.prnt = help_html % (print_fgcolor, 'white')
            self.htmlwin.SetPage(self.html)

    def OnEntryBarChar(self, event):
        curr_id = self.FindFocus().GetId()
        keycode = event.GetKeyCode()
        if keycode == 17:         # quit
            self.OnQuit()
        if self.leader:
            self.leader = False
            if keycode == ord('c'):
                contexts = [x for x in list(self.data.contexts) if x] 
                contexts.sort()
                self.EntryBarInsert(self.getSelection('contexts', 
                    contexts))
            elif keycode == ord('d'): # date
                self.EntryBarInsert(self.getDate())
            elif keycode == ord('h') and self.show_hist:
                histlst = self.history[self.function]
                self.EntryBarInsert(self.getSelection(
                    'previous %s option strings' % self.function, 
                    histlst))
            elif keycode == ord('k'): # keyword
                keywords = [x for x in list(self.data.keywords)]
                keywords.sort()
                self.EntryBarInsert(self.getSelection('keywords', keywords))
            else:
                event.Skip()
        elif keycode == ord('`'): # ` leader
            self.leader = True
        elif keycode == wx.WXK_RETURN: # Esc return 
            self.EntryBarEnter(event), 
        elif keycode == wx.WXK_F1:
            self.toggle_help()
        elif keycode == wx.WXK_F2:
            self.html = self.toggle_htmlcalendar()
        elif keycode == 27: # Esc cancel 
            self.cancel()
        else:
            event.Skip()

    def OnChar(self, event):
        curr_id = self.FindFocus().GetId()
        keycode = event.GetKeyCode()
        self.show_hist = False
        if keycode == 17:         # Ctrl-Q quit
            self.OnQuit()
        elif keycode == 16:         # Ctrl-P print
            self.OnPrint(event)
        elif keycode == 19:         # Ctrl-S save selection to clipboard
            self.view2Clipboard()
        elif keycode == wx.WXK_F1:
            self.toggle_help()
        elif keycode == wx.WXK_F2:
            self.toggle_htmlcalendar()
        elif keycode == wx.WXK_F3:
            self.data.changed = True
            self.show_view(self.cmd, self.args)
        elif keycode == 27: # Esc
            if self.in_getresponse:
                self.cancel()
            else:
                self.show_view(self.cmd, self.args)
                self.setprompt(True)
        elif keycode in [ord(','), ord('<'), wx.WXK_LEFT] and self.display != 'agenda':
            self.advance -= 1
            self.show_view(self.cmd, self.args)
        elif keycode in [ord('.'), ord('>'), wx.WXK_RIGHT] and self.display != 'agenda':
            self.advance += 1
            self.show_view(self.cmd, self.args)
        elif keycode == ord('/') and self.display != 'agenda':
            self.advance = 0
            self.show_view(self.cmd, self.args)
        elif keycode == ord('A'):
            self.show_hist = True
            self.last_cmd = self.cmd
            self.cmd = 'A' 
            self.function = 'agenda'
            self.getresponse("agenda options:",
                    "", "", agendaview_html)
        elif keycode in [ord('a'), wx.WXK_SPACE]:
            self.cmd = 'a'
            self.show_agenda()
        elif keycode == ord('B'):
            self.show_hist = True
            self.last_cmd = self.cmd
            self.cmd = 'B' 
            self.function = 'busy'
            self.getresponse("busy/free options:",
                    "", "", busyview_html)
        elif keycode == ord('b'):
            self.cmd = 'b' 
            self.show_busy()
        elif keycode == ord('c'):
            dlg = wx.TextEntryDialog(
                self, 
                "Enter an expression of the form 'date (+|-) string'\nwhere string is either a date or an integer followed by 'days',\ne.g., 'dec 1 + 30 days' or 'nov 30 - sep 1'.",
                    'etm date calculator', '')
            if dlg.ShowModal() == wx.ID_OK:
                s = dlg.GetValue()
                if s:
                    msg = date_calculator(str(s))
                    dlg = wx.MessageDialog(self, msg, 'etm date calculator',
                                        wx.OK | wx.ICON_INFORMATION)
                    dlg.ShowModal()
                    dlg.Destroy()
        elif keycode == ord('d'):
            self.html, self.prnt = sunmoon_html()
            self.showoutput(self.html)
        elif keycode == ord('D'):
            if self.cmd in ['a', 'l']:
                self.last_cmd = self.cmd
                self.data.show_idnums = True
                self.getPos()
                self.show_view(self.cmd, self.args)
                self.restorePos()
                self.data.show_idnums = False
                self.function = 'delete_item'
                self.getresponse("id (prepended to title) of item to delete:", '', '')
        elif keycode == ord('e'):
            self.entry_type = 'e'
            self.file = self.getFile('event')
            if self.file:
                self.function = 'new_entry'
                self.getresponse("options for new event:",
                        "* ", 'event', 
                        "\n".join(event_html))
            else:
                self.cancel()
        elif keycode == ord('f'):
            if self.cmd in ['a', 'l']:
                self.last_cmd = self.cmd
                self.data.show_idnums = True
                self.getPos()
                self.show_view(self.cmd, self.args)
                self.restorePos()
                self.data.show_idnums = False
                self.function = 'mark_finished'
                self.getresponse("id (prepended to title) of task to mark finished:", '', "")
        elif keycode == ord('i'):
            if self.timer_paused:
                self.restart_timer()
            elif self.timer_running:
                self.interrupt_timer()
            else:
                self.function = 'start_timer'
                self.getresponse("initial entry for new action:", "~ ", 'action')
        elif keycode == ord('I'):
            if self.timer_running:
                (date, minutes) = self.stop_timer()
                self.entry_type = 'a'
                self.file = self.getFile('action')
                if self.file:
                    self.function = 'new_entry'
                    entry = "%s @d %s @p %s" % (self.timer_entry,
                            date, minutes)
                    self.getresponse("options for new action:",
                            "%s" % entry, 'action', 
                            "\n".join(action_html))
                else:
                    self.cancel()
        elif keycode == ord('L'):
            self.show_hist = True
            self.last_cmd = self.cmd
            self.cmd = 'L' 
            self.function = 'list'
            self.getresponse("list options:", "", "", listview_html)
        elif keycode == ord('l'):
            self.last_cmd = self.cmd
            self.cmd = 'l' 
            self.show_list()
        elif keycode == ord('m'):
            if self.cmd in ['a', 'l']:
                self.last_cmd = self.cmd
                self.data.show_idnums = True
                self.getPos()
                self.show_view(self.cmd, self.args)
                self.restorePos()
                self.data.show_idnums = False
                self.function = 'edit_entry'
                self.getresponse("id (prepended to title) of item to modify:", "", "", "")
        elif keycode == ord('n'):
            self.setmessage(newer(), True)
        elif keycode == ord('p'):
            self.entry_type = 'p'
            self.file = self.getFile('project', 'create')
            if self.file:
                self.function = 'new_project'
                self.getresponse("options for new project:",
                        "", 'project', 
                        "\n".join(project_html))
            else:
                self.cancel()
        elif keycode == ord('P'):
            self.entry_type = 'p'
            self.file = self.getFile('project', 'open')
            if self.file:
                self.open_project()
            else:
                self.cancel()
        elif keycode == ord('q'):
            html = self.alert_status()
            self.html = html % (main_fgcolor, main_bgcolor)
            self.prnt = html % (print_fgcolor, 'white')
            self.showoutput(self.html)
        elif keycode == ord('R'):
            self.show_hist = True
            self.last_cmd = self.cmd
            self.cmd = 'R' 
            self.function = 'reckoning'
            self.getresponse("reckoning options:",
                    "", "", reckoningview_html)
        elif keycode == ord('r'):
            self.last_cmd = self.cmd
            self.cmd = 'r' 
            self.show_reckoning()
        elif keycode == ord('t'):
            self.entry_type = 't'
            self.file = self.getFile('task')
            if self.file:
                self.function = 'new_entry'
                self.getresponse("options for new task:",
                        "+ ", 'task', 
                        "\n".join(task_html))
            else:
                self.cancel()
        elif keycode == ord('u'):
            if self.cmd in ['a', 'l']:
                self.last_cmd = self.cmd
                self.data.show_idnums = True
                self.getPos()
                self.show_view(self.cmd, self.args)
                self.restorePos()
                self.data.show_idnums = False
                self.function = 'mark_unfinished'
                self.getresponse("id (prepended to title) of task to mark unfinished:", '', "")
        elif keycode == ord('v'):
            self.verbose_toggle = not self.verbose_toggle
            self.show_view(self.cmd, self.args)
        elif keycode == ord('w'):
            html = weather_html()
            self.html = html % (main_fgcolor, main_bgcolor)
            self.prnt = html % (print_fgcolor, 'white')
            self.showoutput(self.html)
        else:
            event.Skip()

    functionHash = {
            'agenda' : show_agenda,
            'list' : show_list, 
            'busy' : show_busy, 
            'reckoning' : show_reckoning, 
            'edit_entry' : edit_entry,
            'process_entry' : process_entry,
            'new_entry' : new_entry,
            'start_timer' : start_timer,
            'mark_finished' : mark_finished,
            'mark_unfinished' : mark_unfinished,
            'delete_item' : delete_item,
            'new_project' : new_project,
            'open_project' : open_project,
            }


    typeHash = {
            'e': ['event', event_html, event_keys, '*'],
            't': ['task', task_html, task_keys, '+'],
            'a': ['action', action_html, action_keys, '~'],
            }

def weather_html():
    lst = getweather()
    ret = ['<title>yahoo weather</title>',
        '<body text="%s" bgcolor="%s">', 
            '<pre>']
    for line in lst:
        ret.append(line)
    ret.append("</pre></body>")
    html = "\n".join(ret)
    return(html)


def sunmoon_html():
    lst = getsunmoon()
    po = ['<title>USNO sun and moon data</title>',
        '<body text="%s">' % (print_fgcolor), 
            '<pre>']
    ret = ['<title>USNO sun and moon data</title>',
        '<body text="%s" bgcolor="%s">' % (main_fgcolor, main_bgcolor), 
            '<pre>']
    for line in lst:
        line = line.rstrip()
        if line:
            ret.append(line)
            po.append(line)
    ret.append("</pre></body>")
    po.append("</pre></body>")
    html = "\n".join(ret)
    prnt = "\n".join(po)
    return(html, prnt)

class App(wx.App):
    def OnInit(self):
        wx.lib.colourdb.updateColourDB()

        self.frame = MyFrame(wxwidth, wxheight)
        self.frame.Show()
        self.SetTopWindow(self.frame)
        return True

def main():
#     res = force_start()
#     print("\n".join(res))
    app = App()
    app.MainLoop()

if __name__ == "__main__":
#     res = force_start()
#     print("\n".join(res))
    app = App(redirect = False)
    app.MainLoop()