teichmann@0: #!/usr/bin/env python teichmann@0: # -*- coding: utf-8 -*- teichmann@0: # teichmann@0: # getan teichmann@0: # ----- teichmann@0: # (c) 2008 by Sascha L. Teichmann teichmann@0: # teichmann@0: # A python worklog-alike to log what you have 'getan' (done). teichmann@0: # teichmann@0: # This is Free Software licensed under the terms of GPLv3 or later. teichmann@0: # For details see LICENSE coming with the source of 'getan'. teichmann@0: # teichmann@0: import sys teichmann@0: import re teichmann@0: import curses teichmann@0: import curses.ascii teichmann@0: import traceback teichmann@0: import signal teichmann@0: teichmann@2: from datetime import datetime, timedelta, tzinfo teichmann@0: teichmann@0: from pysqlite2 import dbapi2 as db teichmann@0: teichmann@0: PAUSED = 0 teichmann@0: RUNNING = 1 teichmann@0: PRE_EXIT = 2 teichmann@0: PAUSED_ESC = 3 teichmann@0: RUNNING_ESC = 4 teichmann@0: teichmann@0: SPACE = re.compile("[\\t ]+") teichmann@0: teichmann@0: DEFAULT_DATABASE = "time.db" teichmann@0: teichmann@0: LOAD_ACTIVE_PROJECTS = ''' teichmann@0: SELECT id, key, description, total teichmann@0: FROM projects LEFT JOIN teichmann@0: (SELECT teichmann@0: project_id, teichmann@0: sum(strftime('%s', stop_time) - strftime('%s', start_time)) AS total teichmann@0: FROM entries teichmann@0: GROUP BY project_id) ON project_id = id teichmann@0: WHERE active teichmann@0: ''' teichmann@0: teichmann@0: WRITE_LOG = ''' teichmann@0: INSERT INTO entries (project_id, start_time, stop_time, description) teichmann@0: VALUES(:project_id, :start_time, :stop_time, :description) teichmann@0: ''' teichmann@0: teichmann@0: CREATE_PROJECT = ''' teichmann@0: INSERT INTO projects (key, description) VALUES (:key, :description) teichmann@0: ''' teichmann@0: teichmann@0: LAST_PROJECT_ID = ''' teichmann@0: SELECT last_insert_rowid() teichmann@0: ''' teichmann@0: teichmann@0: RENAME_PROJECT = ''' teichmann@0: UPDATE projects set key = :key, description = :description WHERE id = :id teichmann@0: ''' teichmann@0: teichmann@0: ASSIGN_LOGS = ''' teichmann@0: UPDATE entries SET project_id = :new_id WHERE project_id = :old_id teichmann@0: ''' teichmann@0: teichmann@0: DELETE_PROJECT = ''' teichmann@0: DELETE FROM projects WHERE id = :id teichmann@0: ''' teichmann@0: teichmann@2: # XXX: This is not very efficent! teichmann@2: LAST_ENTRY = ''' teichmann@2: SELECT id, strftime('%s', start_time), strftime('%s', stop_time) FROM entries teichmann@2: WHERE project_id = :project_id teichmann@2: ORDER by strftime('%s', stop_time) DESC LIMIT 1 teichmann@2: ''' teichmann@2: teichmann@2: DELETE_ENTRY = ''' teichmann@2: DELETE FROM entries WHERE id = :id teichmann@2: ''' teichmann@2: teichmann@2: UPDATE_STOP_TIME = ''' teichmann@2: UPDATE entries SET stop_time = :stop_time WHERE id = :id teichmann@2: ''' teichmann@2: teichmann@0: worklog = None teichmann@0: stdscr = None teichmann@0: teichmann@0: orig_vis = None teichmann@0: teichmann@0: def cursor_visible(flag): teichmann@0: global orig_vis teichmann@0: try: teichmann@0: old = curses.curs_set(flag) teichmann@0: if orig_vis is None: orig_vis = old teichmann@0: return old teichmann@0: except: teichmann@0: pass teichmann@0: return 1 teichmann@0: teichmann@0: def restore_cursor(): teichmann@0: global orig_vis teichmann@0: if not orig_vis is None: teichmann@0: curses.curs_set(orig_vis) teichmann@0: teichmann@0: def render_header(ofs=0): teichmann@0: global stdscr teichmann@0: stdscr.attron(curses.A_BOLD) teichmann@0: stdscr.addstr(ofs, 5, "getan v0.1") teichmann@0: stdscr.addstr(ofs+1, 3, "--------------") teichmann@0: stdscr.attroff(curses.A_BOLD) teichmann@0: return ofs + 2 teichmann@0: teichmann@0: def render_quit(ofs=0): teichmann@0: global stdscr teichmann@0: stdscr.addstr(ofs + 2, 3, "Press DEL once more to quit") teichmann@0: return ofs + 3 teichmann@0: teichmann@0: def tolerantClose(cur): teichmann@0: if cur: teichmann@0: try: cur.close() teichmann@0: except: pass teichmann@0: teichmann@0: def ifNull(v, d): teichmann@0: if v is None: return d teichmann@0: return v teichmann@0: teichmann@0: def human_time(delta): teichmann@0: seconds = delta.seconds teichmann@0: s = seconds % 60 teichmann@0: if delta.microseconds >= 500000: s += 1 teichmann@0: seconds /= 60 teichmann@0: m = seconds % 60 teichmann@0: seconds /= 60 teichmann@0: out = "%02d:%02d:%02d" % (seconds, m, s) teichmann@0: if delta.days: teichmann@0: out = "%dd %s" % (delta.days, out) teichmann@0: return out teichmann@0: teichmann@2: FACTORS = { teichmann@2: 's': 1, teichmann@2: 'm': 60, teichmann@2: 'h' : 60*60, teichmann@2: 'd': 24*60*60} teichmann@2: thomas@17: def human_seconds(timespec): thomas@17: """Translate human input to seconds, default factor is minutes""" teichmann@6: total = 0 thomas@17: for v in timespec.split(':'): teichmann@6: factor = FACTORS.get(v[-1]) teichmann@6: if factor: v = v[:-1] teichmann@6: else: factor = 60 teichmann@6: total += int(v) * factor teichmann@6: return total teichmann@2: teichmann@7: ESC_MAP = { teichmann@7: curses.KEY_F1 : ord('1'), teichmann@7: curses.KEY_F2 : ord('2'), teichmann@7: curses.KEY_F3 : ord('3'), teichmann@7: curses.KEY_F4 : ord('4'), teichmann@7: curses.KEY_F5 : ord('5'), teichmann@7: curses.KEY_F6 : ord('6'), teichmann@7: curses.KEY_F7 : ord('7'), teichmann@7: curses.KEY_F8 : ord('8'), teichmann@7: curses.KEY_F9 : ord('9'), teichmann@7: curses.KEY_F10: ord('0'), teichmann@7: } teichmann@7: teichmann@2: ZERO = timedelta(0) teichmann@2: teichmann@2: class UTC(tzinfo): teichmann@2: """UTC""" teichmann@2: teichmann@2: def utcoffset(self, dt): teichmann@2: return ZERO teichmann@2: teichmann@2: def tzname(self, dt): teichmann@2: return "UTC" teichmann@2: teichmann@2: def dst(self, dt): teichmann@2: return ZERO teichmann@2: teichmann@0: class Project: teichmann@0: teichmann@0: def __init__(self, id = None, key = None, desc = None, total = 0): teichmann@0: self.id = id teichmann@0: self.key = key teichmann@0: self.desc = desc teichmann@0: self.total = timedelta(seconds = ifNull(total, 0)) teichmann@0: self.start_time = None teichmann@0: teichmann@0: def checkExistence(self, cur): teichmann@0: if self.id is None: teichmann@0: cur.execute(CREATE_PROJECT, { teichmann@0: 'key' : self.key, teichmann@0: 'description': self.desc}) teichmann@0: cur.execute(LAST_PROJECT_ID) teichmann@0: row = cur.fetchone() teichmann@0: cur.connection.commit() teichmann@0: self.id = row[0] teichmann@0: teichmann@0: def writeLog(self, cur, description = None): teichmann@0: if self.start_time is None: return teichmann@0: self.checkExistence(cur) teichmann@0: now = datetime.now() teichmann@0: cur.execute(WRITE_LOG, { teichmann@0: 'project_id' : self.id, teichmann@0: 'start_time' : self.start_time, teichmann@0: 'stop_time' : now, teichmann@0: 'description': description}) teichmann@0: self.total += now-self.start_time teichmann@0: return now teichmann@0: teichmann@0: def getId(self, cur): teichmann@0: self.checkExistence(cur) teichmann@0: return self.id teichmann@0: teichmann@0: def rename(self, cur, key, desc): teichmann@0: self.key = key teichmann@0: self.desc = desc teichmann@0: self.checkExistence(cur) teichmann@0: cur.execute(RENAME_PROJECT, { teichmann@0: 'key' : key, teichmann@0: 'description': desc, teichmann@0: 'id' : self.id }) teichmann@0: cur.connection.commit() teichmann@0: teichmann@0: def assignLogs(self, cur, anon): teichmann@0: self.total += anon.total teichmann@0: anon.total = timedelta(seconds=0) teichmann@0: old_id = anon.getId(cur) teichmann@0: new_id = self.getId(cur) teichmann@0: cur.execute(ASSIGN_LOGS, { teichmann@0: 'new_id': new_id, teichmann@0: 'old_id': old_id}) teichmann@0: cur.connection.commit() teichmann@0: teichmann@0: def delete(self, cur): teichmann@2: pid = self.getId(cur) teichmann@2: cur.execute(DELETE_PROJECT, { 'id': pid }) teichmann@0: cur.connection.commit() teichmann@0: thomas@19: def subtractTime(self, cur, seconds): thomas@19: subtractTimeed, zero = timedelta(), timedelta() teichmann@2: pid = {'project_id': self.getId(cur)} teichmann@2: utc = UTC() teichmann@2: while seconds > zero: teichmann@2: cur.execute(LAST_ENTRY, pid) teichmann@2: row = cur.fetchone() teichmann@2: if row is None: break teichmann@2: # TODO: evaluate egenix-mx teichmann@2: start_time = datetime.fromtimestamp(float(row[1]), utc) teichmann@2: stop_time = datetime.fromtimestamp(float(row[2]), utc) teichmann@2: runtime = stop_time - start_time teichmann@2: if runtime <= seconds: teichmann@2: cur.execute(DELETE_ENTRY, { 'id': row[0] }) teichmann@2: cur.connection.commit() teichmann@2: seconds -= runtime thomas@19: subtractTimeed += runtime teichmann@2: else: teichmann@2: stop_time -= seconds teichmann@2: cur.execute(UPDATE_STOP_TIME, { teichmann@2: 'id': row[0], teichmann@2: 'stop_time': stop_time}) teichmann@2: cur.connection.commit() thomas@19: subtractTimeed += seconds teichmann@2: break teichmann@2: thomas@19: self.total -= subtractTimeed thomas@19: return subtractTimeed teichmann@2: teichmann@6: def addTime(self, cur, seconds, description): teichmann@2: now = datetime.now() teichmann@2: cur.execute(WRITE_LOG, { teichmann@2: 'project_id' : self.getId(cur), teichmann@2: 'start_time' : now - seconds, teichmann@2: 'stop_time' : now, teichmann@6: 'description': description teichmann@2: }) teichmann@2: cur.connection.commit() teichmann@2: self.total += seconds teichmann@12: teichmann@12: def build_tree(project, depth): teichmann@12: if len(project.key) == depth+1: teichmann@12: return ProjectNode(project, project.key[depth]) teichmann@12: node = ProjectNode(None, project.key[depth]) teichmann@12: node.children.append(build_tree(project, depth+1)) teichmann@12: return node teichmann@12: teichmann@12: class ProjectNode: teichmann@12: teichmann@12: def __init__(self, project = None, key = None): teichmann@12: self.children = [] teichmann@12: self.project = project teichmann@12: self.key = key teichmann@12: teichmann@12: def insertProject(self, project, depth = 0): teichmann@12: teichmann@12: if not project.key: # anonym -> end teichmann@12: node = ProjectNode(project) teichmann@12: self.children.append(node) teichmann@12: return teichmann@12: teichmann@12: for i, child in enumerate(self.children): teichmann@12: if not child.key: # before anonym projects teichmann@12: self.children.insert(i, build_tree(project, depth)) teichmann@12: return teichmann@12: if child.key == project.key[depth]: teichmann@12: child.insertProject(project, depth+1) teichmann@12: return teichmann@12: self.children.append(build_tree(project, depth)) teichmann@12: teichmann@14: def removeProject(self, project): teichmann@14: teichmann@14: if self.isLeaf(): return teichmann@14: stack = [self] teichmann@14: while stack: teichmann@14: parent = stack.pop() teichmann@14: for child in parent.children: teichmann@14: if not child.isLeaf(): teichmann@14: stack.append(child) teichmann@14: continue teichmann@14: if child.project == project: teichmann@14: parent.children.remove(child) teichmann@14: return teichmann@14: teichmann@12: def isLeaf(self): teichmann@12: return not self.project is None teichmann@12: teichmann@12: def findProject(self, key): teichmann@12: l, lower = key.lower(), None teichmann@12: for child in self.children: teichmann@12: if child.key == key: teichmann@12: return child teichmann@12: if child.key and child.key.lower() == l: teichmann@12: lower = child teichmann@12: return lower teichmann@12: teichmann@12: def dump(self, depth = 0): teichmann@12: out = [] teichmann@12: indent = " " * depth teichmann@12: out.append("%skey: %s" % (indent, self.key)) teichmann@12: if self.project: teichmann@12: out.append("%sdescription: %s" % (indent, self.project.desc)) teichmann@12: for child in self.children: teichmann@12: out.append(child.dump(depth+1)) teichmann@12: return "\n".join(out) teichmann@2: teichmann@0: class Worklog: teichmann@0: teichmann@0: def __init__(self, database): teichmann@0: self.initDB(database) teichmann@12: self.projects = [] teichmann@12: self.tree = ProjectNode() teichmann@12: self.state = PAUSED teichmann@12: self.current_project = None teichmann@12: self.selection = self.tree thomas@20: self.stack = [] teichmann@0: self.loadProjects() teichmann@0: teichmann@0: def initDB(self, database): teichmann@0: self.con = db.connect(database) teichmann@0: teichmann@0: def loadProjects(self): teichmann@0: cur = None teichmann@0: try: teichmann@0: cur = self.con.cursor() teichmann@0: cur.execute(LOAD_ACTIVE_PROJECTS) teichmann@12: while True: teichmann@12: row = cur.fetchone() teichmann@12: if not row: break teichmann@12: project = Project(*row) teichmann@12: self.projects.append(project) teichmann@12: self.tree.insertProject(project) teichmann@0: finally: teichmann@0: tolerantClose(cur) teichmann@0: teichmann@0: def shutdown(self): teichmann@0: self.con.close() teichmann@0: thomas@20: def fetchStack(self): thomas@20: cut = ''.join([chr(i) for i in self.stack]) thomas@20: self.stack = [] thomas@20: return cut thomas@20: teichmann@0: def findProject(self, key): teichmann@0: key_lower = key.lower() teichmann@0: lower = None teichmann@0: teichmann@0: for p in self.projects: teichmann@0: if p.key == key: teichmann@0: return p teichmann@0: if p.key and p.key.lower() == key_lower: teichmann@0: lower = p teichmann@0: teichmann@0: return lower teichmann@0: teichmann@0: def findAnonymProject(self, num): teichmann@0: count = 0 teichmann@0: for p in self.projects: teichmann@0: if p.key is None: teichmann@0: if count == num: teichmann@0: return p teichmann@0: count += 1 teichmann@0: return None teichmann@0: teichmann@0: def renameAnonymProject(self, num, key, description): teichmann@0: project = self.findAnonymProject(num) teichmann@0: if project: teichmann@0: cur = None teichmann@0: try: teichmann@0: cur = self.con.cursor() teichmann@0: project.rename(cur, key, description) teichmann@0: finally: teichmann@0: tolerantClose(cur) teichmann@14: self.tree.removeProject(project) teichmann@14: self.tree.insertProject(project) teichmann@0: teichmann@0: def assignLogs(self, num, key): teichmann@0: anon = self.findAnonymProject(num) teichmann@0: if anon is None: return teichmann@0: project = self.findProject(key) teichmann@0: if project is None: return teichmann@0: cur = None teichmann@0: try: teichmann@0: cur = self.con.cursor() teichmann@0: project.assignLogs(cur, anon) teichmann@0: self.projects.remove(anon) teichmann@0: anon.delete(cur) teichmann@0: finally: teichmann@0: tolerantClose(cur) teichmann@0: teichmann@6: def addTime(self, key, seconds, description = None): teichmann@2: project = self.findProject(key) teichmann@2: if project is None: return teichmann@2: cur = None teichmann@2: try: teichmann@2: cur = self.con.cursor() teichmann@6: project.addTime(cur, seconds, description) teichmann@2: finally: teichmann@2: tolerantClose(cur) teichmann@2: thomas@19: def subtractTime(self, key, seconds): teichmann@2: project = self.findProject(key) teichmann@2: if project is None: return teichmann@2: cur = None teichmann@2: try: teichmann@2: cur = self.con.cursor() thomas@19: project.subtractTime(cur, seconds) teichmann@2: finally: teichmann@2: tolerantClose(cur) teichmann@2: teichmann@0: def isRunning(self): teichmann@0: return self.state in (RUNNING, RUNNING_ESC) teichmann@0: teichmann@3: def totalTime(self): teichmann@3: sum = timedelta() teichmann@3: for p in self.projects: teichmann@3: sum += p.total teichmann@3: return sum teichmann@3: teichmann@0: def render(self, ofs=0): teichmann@0: ofs = render_header(ofs) teichmann@0: ml = max([len(p.desc and p.desc or "unknown") for p in self.projects]) teichmann@0: unknown = 0 teichmann@0: teichmann@0: if self.current_project and self.current_project.start_time: teichmann@3: current_delta = datetime.now() - self.current_project.start_time teichmann@0: current_time_str = "%s " % human_time(current_delta) teichmann@0: current_time_space = " " * len(current_time_str) teichmann@0: else: teichmann@3: current_delta = timedelta() teichmann@0: current_time_str = "" teichmann@0: current_time_space = "" teichmann@0: teichmann@0: for project in self.projects: teichmann@0: is_current = project == self.current_project teichmann@0: pref = is_current and " -> " or " " teichmann@0: if project.key is None: teichmann@0: key = "^%d" % unknown teichmann@0: unknown += 1 teichmann@0: else: teichmann@0: key = " %s" % project.key teichmann@0: desc = project.desc is None and "unknown" or project.desc teichmann@0: stdscr.attron(curses.A_BOLD) teichmann@0: stdscr.addstr(ofs, 0, "%s%s" % (pref, key)) teichmann@0: stdscr.attroff(curses.A_BOLD) teichmann@0: stdscr.addstr(" %s" % desc) teichmann@0: teichmann@0: diff = ml - len(desc) + 1 teichmann@0: stdscr.addstr(" " * diff) teichmann@0: if is_current: stdscr.attron(curses.A_UNDERLINE) teichmann@0: teichmann@0: if is_current: teichmann@0: stdscr.addstr("%s(%s)" % ( teichmann@0: current_time_str, teichmann@0: human_time(project.total + current_delta))) teichmann@0: else: teichmann@0: stdscr.addstr("%s(%s)" % ( teichmann@0: current_time_space, teichmann@0: human_time(project.total))) teichmann@0: teichmann@0: if is_current: stdscr.attroff(curses.A_UNDERLINE) teichmann@0: ofs += 1 teichmann@0: teichmann@3: total_str = "(%s)" % human_time(self.totalTime() + current_delta) teichmann@3: total_x_pos = ml + 8 + len(current_time_space) teichmann@3: teichmann@3: stdscr.addstr(ofs, total_x_pos, "=" * len(total_str)) teichmann@3: ofs += 1 teichmann@3: stdscr.addstr(ofs, total_x_pos, total_str) teichmann@3: ofs += 1 teichmann@3: teichmann@0: return ofs teichmann@0: teichmann@0: def writeLog(self, description = None): teichmann@0: if self.current_project is None: teichmann@0: return datetime.now() teichmann@0: cur = None teichmann@0: try: teichmann@0: cur = self.con.cursor() teichmann@0: now = self.current_project.writeLog(cur, description) teichmann@0: self.con.commit() teichmann@0: return now teichmann@0: finally: teichmann@0: tolerantClose(cur) teichmann@0: teichmann@2: def pausedState(self, c): teichmann@7: c2 = ESC_MAP.get(c) teichmann@7: if c2: teichmann@7: self.pausedEscapeState(c2) teichmann@7: return teichmann@7: teichmann@2: global stdscr teichmann@2: if c in (curses.KEY_DC, curses.KEY_BACKSPACE): teichmann@2: stdscr.erase() teichmann@2: ofs = render_quit(self.render()) teichmann@2: stdscr.refresh() teichmann@2: self.state = PRE_EXIT teichmann@2: teichmann@2: elif c == curses.ascii.ESC: teichmann@2: self.state = PAUSED_ESC teichmann@2: teichmann@2: elif curses.ascii.isascii(c): teichmann@6: if c == ord('-'): teichmann@12: self.selection = self.tree teichmann@2: stdscr.erase() teichmann@2: ofs = self.render() teichmann@2: old_cur = cursor_visible(1) teichmann@2: curses.echo() teichmann@6: stdscr.addstr(ofs + 1, 3, " : ") teichmann@2: key = stdscr.getstr() teichmann@2: curses.noecho() teichmann@2: cursor_visible(old_cur) teichmann@2: key = key.strip() teichmann@2: if key: teichmann@2: parts = SPACE.split(key, 1) teichmann@2: if len(parts) > 1: thomas@17: key, timespec = parts[0], parts[1] teichmann@2: try: thomas@17: seconds = human_seconds(timespec) teichmann@2: if seconds > 0: teichmann@2: seconds = timedelta(seconds=seconds) thomas@19: self.subtractTime(key, seconds) teichmann@6: except ValueError: teichmann@6: pass teichmann@6: stdscr.erase() teichmann@6: self.render() teichmann@6: stdscr.refresh() teichmann@6: teichmann@6: elif c == ord('+'): teichmann@12: self.selection = self.tree teichmann@6: stdscr.erase() teichmann@6: ofs = self.render() teichmann@6: old_cur = cursor_visible(1) teichmann@6: curses.echo() teichmann@6: stdscr.addstr(ofs + 1, 3, " []: ") teichmann@6: key = stdscr.getstr() teichmann@6: curses.noecho() teichmann@6: cursor_visible(old_cur) teichmann@6: key = key.strip() teichmann@6: if key: teichmann@6: parts = SPACE.split(key, 2) teichmann@6: if len(parts) > 1: thomas@17: key, timespec = parts[0], parts[1] teichmann@6: if len(parts) > 2: desc = parts[2] teichmann@6: else: desc = None teichmann@6: try: thomas@17: seconds = human_seconds(timespec) teichmann@6: if seconds > 0: teichmann@6: seconds = timedelta(seconds=seconds) teichmann@6: self.addTime(key, seconds, desc) teichmann@2: except ValueError: teichmann@2: pass teichmann@2: stdscr.erase() teichmann@2: self.render() teichmann@2: stdscr.refresh() teichmann@2: teichmann@2: else: teichmann@12: node = self.selection.findProject(chr(c)) teichmann@12: if not node: teichmann@12: self.selection = self.tree teichmann@12: return teichmann@12: if node.isLeaf(): teichmann@12: self.selection = self.tree teichmann@12: nproject = node.project teichmann@12: self.current_project = nproject teichmann@12: nproject.start_time = datetime.now() teichmann@12: stdscr.erase() teichmann@12: ofs = self.render() teichmann@12: stdscr.refresh() teichmann@12: self.state = RUNNING teichmann@12: signal.signal(signal.SIGALRM, alarm_handler) teichmann@12: signal.alarm(1) teichmann@12: else: teichmann@12: self.selection = node teichmann@2: teichmann@2: def runningState(self, c): teichmann@2: global stdscr teichmann@7: c2 = ESC_MAP.get(c) teichmann@7: if c2: teichmann@7: self.runningEscapeState(c2) teichmann@7: return teichmann@7: teichmann@2: if c == curses.ascii.ESC: teichmann@2: self.state = RUNNING_ESC teichmann@2: teichmann@2: elif c == curses.ascii.NL: teichmann@2: signal.signal(signal.SIGALRM, signal.SIG_IGN) teichmann@2: self.state = PAUSED teichmann@2: stdscr.erase() teichmann@2: ofs = self.render() teichmann@2: old_cur = cursor_visible(1) teichmann@2: curses.echo() teichmann@2: stdscr.addstr(ofs + 1, 3, "Description: ") teichmann@2: description = stdscr.getstr() teichmann@2: curses.noecho() teichmann@2: cursor_visible(old_cur) teichmann@2: self.writeLog(description) teichmann@2: self.current_project = None teichmann@2: stdscr.erase() teichmann@2: ofs = self.render() teichmann@2: stdscr.refresh() thomas@18: signal.signal(signal.SIGALRM, alarm_handler) thomas@18: signal.alarm(1) thomas@18: elif c == ord('+'): thomas@18: signal.signal(signal.SIGALRM, signal.SIG_IGN) thomas@18: stdscr.erase() thomas@18: ofs = self.render() thomas@20: if self.stack: thomas@20: timespec = self.fetchStack() thomas@20: else: thomas@20: old_cur = cursor_visible(1) thomas@20: curses.echo() thomas@20: stdscr.addstr(ofs + 1, 3, "Enter time to add: ") thomas@20: timespec = stdscr.getstr() thomas@20: curses.noecho() thomas@20: cursor_visible(old_cur) thomas@20: stdscr.erase() thomas@20: ofs = self.render() thomas@18: try: thomas@18: seconds = human_seconds(timespec) thomas@18: if seconds > 0: thomas@20: seconds = timedelta(seconds=seconds) thomas@20: self.current_project.start_time -= seconds thomas@20: stdscr.addstr(ofs + 1, 3, "added %s" % human_time(seconds)) thomas@21: except (ValueError, IndexError): thomas@18: pass thomas@18: stdscr.refresh() thomas@18: signal.signal(signal.SIGALRM, alarm_handler) thomas@18: signal.alarm(1) thomas@18: elif c == ord('-'): thomas@18: signal.signal(signal.SIGALRM, signal.SIG_IGN) thomas@18: stdscr.erase() thomas@18: ofs = self.render() thomas@20: if self.stack: thomas@20: timespec = self.fetchStack() thomas@20: else: thomas@20: old_cur = cursor_visible(1) thomas@20: curses.echo() thomas@20: stdscr.addstr(ofs + 1, 3, "Enter time to subtract: ") thomas@20: timespec = stdscr.getstr() thomas@20: curses.noecho() thomas@20: cursor_visible(old_cur) thomas@20: stdscr.erase() thomas@20: ofs = self.render() thomas@18: try: thomas@18: seconds = human_seconds(timespec) thomas@18: if seconds > 0: thomas@18: now = datetime.now() thomas@20: seconds = timedelta(seconds=seconds) thomas@20: self.current_project.start_time += seconds thomas@20: stdscr.addstr(ofs + 1, 3, "subtracted %s" % human_time(seconds)) thomas@18: if self.current_project.start_time > now: thomas@18: seconds = self.current_project.start_time - now thomas@18: self.current_project.start_time = now thomas@18: cur = None thomas@18: try: thomas@18: cur = self.con.cursor() thomas@19: self.current_project.subtractTime(cur, seconds) thomas@18: finally: thomas@18: tolerantClose(cur) thomas@21: except (ValueError, IndexError): thomas@18: pass thomas@18: stdscr.refresh() thomas@18: signal.signal(signal.SIGALRM, alarm_handler) thomas@18: signal.alarm(1) thomas@20: elif self.stack or curses.ascii.isdigit(c): thomas@20: self.stack.append(c) teichmann@2: elif curses.ascii.isascii(c): teichmann@12: project_node = self.selection.findProject(chr(c)) teichmann@12: if project_node is None: teichmann@12: self.selection = self.tree teichmann@2: return teichmann@12: teichmann@12: if project_node.isLeaf(): teichmann@12: self.selection = self.tree teichmann@12: nproject = project_node.project teichmann@12: if nproject == self.current_project: teichmann@12: return teichmann@12: nproject.start_time = self.writeLog() teichmann@12: self.current_project = nproject teichmann@12: stdscr.erase() teichmann@12: ofs = self.render() teichmann@12: stdscr.refresh() teichmann@12: else: teichmann@12: self.selection = project_node teichmann@2: teichmann@2: def pausedEscapeState(self, c): teichmann@2: global stdscr teichmann@2: if curses.ascii.isdigit(c): teichmann@2: pnum = c - ord('0') teichmann@2: nproject = self.findAnonymProject(pnum) teichmann@2: if nproject is None: teichmann@2: nproject = Project() teichmann@2: self.projects.append(nproject) teichmann@2: teichmann@2: nproject.start_time = self.writeLog() teichmann@2: self.current_project = nproject teichmann@2: self.state = RUNNING teichmann@2: stdscr.erase() teichmann@2: ofs = self.render() teichmann@2: stdscr.refresh() teichmann@2: signal.signal(signal.SIGALRM, alarm_handler) teichmann@2: signal.alarm(1) teichmann@2: elif curses.ascii.isalpha(c): teichmann@2: if c == ord('n'): teichmann@2: stdscr.erase() teichmann@2: ofs = self.render() teichmann@2: old_cur = cursor_visible(1) teichmann@2: curses.echo() teichmann@2: stdscr.addstr(ofs + 1, 3, " : ") teichmann@2: stdscr.refresh() teichmann@2: description = stdscr.getstr() teichmann@2: curses.noecho() teichmann@2: cursor_visible(old_cur) teichmann@2: teichmann@2: description = description.strip() teichmann@2: if description: teichmann@2: num, key, description = SPACE.split(description, 2) teichmann@2: try: teichmann@2: num = int(num) teichmann@2: self.renameAnonymProject(num, key, description) teichmann@2: except ValueError: teichmann@2: pass teichmann@2: teichmann@2: stdscr.erase() teichmann@2: ofs = self.render() teichmann@2: stdscr.refresh() teichmann@2: self.state = PAUSED teichmann@2: teichmann@2: elif c == ord('a'): teichmann@2: stdscr.erase() teichmann@2: ofs = self.render() teichmann@2: old_cur = cursor_visible(1) teichmann@2: curses.echo() teichmann@2: stdscr.addstr(ofs + 1, 3, " : ") teichmann@2: stdscr.refresh() teichmann@2: key = stdscr.getstr() teichmann@2: curses.noecho() teichmann@2: cursor_visible(old_cur) teichmann@2: teichmann@2: key = key.strip() teichmann@2: if key: teichmann@2: num, key = SPACE.split(key, 1) teichmann@2: try: teichmann@2: num = int(num) teichmann@2: self.assignLogs(num, key) teichmann@2: except ValueError: teichmann@2: pass teichmann@2: teichmann@2: stdscr.erase() teichmann@2: ofs = self.render() teichmann@2: stdscr.refresh() teichmann@2: self.state = PAUSED teichmann@2: else: teichmann@2: self.state = PAUSED teichmann@2: else: teichmann@2: self.state = PAUSED teichmann@2: teichmann@2: def runningEscapeState(self, c): teichmann@2: global stdscr teichmann@2: if curses.ascii.isdigit(c): teichmann@2: signal.signal(signal.SIGALRM, signal.SIG_IGN) teichmann@2: pnum = c - ord('0') teichmann@2: nproject = self.findAnonymProject(pnum) teichmann@2: if nproject is None: teichmann@2: nproject = Project() teichmann@2: self.projects.append(nproject) teichmann@2: teichmann@2: nproject.start_time = self.writeLog() teichmann@2: self.current_project = nproject teichmann@2: self.state = RUNNING teichmann@2: stdscr.erase() teichmann@2: self.render() teichmann@2: stdscr.refresh() teichmann@2: signal.signal(signal.SIGALRM, alarm_handler) teichmann@2: signal.alarm(1) teichmann@2: else: teichmann@2: self.state = RUNNING teichmann@2: teichmann@2: teichmann@0: def run(self): teichmann@0: global stdscr teichmann@0: teichmann@0: stdscr.erase() teichmann@2: self.render() teichmann@0: stdscr.refresh() teichmann@0: teichmann@0: while True: teichmann@0: c = stdscr.getch() teichmann@0: if c == -1: continue teichmann@0: teichmann@0: if self.state == PAUSED: teichmann@2: self.pausedState(c) teichmann@0: teichmann@0: elif self.state == RUNNING: teichmann@2: self.runningState(c) teichmann@0: teichmann@0: elif self.state == PAUSED_ESC: teichmann@2: self.pausedEscapeState(c) teichmann@0: teichmann@0: elif self.state == RUNNING_ESC: teichmann@2: self.runningEscapeState(c) teichmann@0: teichmann@0: elif self.state == PRE_EXIT: teichmann@1: if c in (curses.KEY_DC, curses.KEY_BACKSPACE): teichmann@0: break teichmann@0: else: teichmann@0: stdscr.erase() teichmann@2: self.render() teichmann@0: stdscr.refresh() teichmann@0: self.state = PAUSED teichmann@0: teichmann@0: def alarm_handler(flag, frame): teichmann@0: global worklog teichmann@0: global stdscr teichmann@0: teichmann@0: stdscr.erase() teichmann@0: worklog.render() teichmann@0: stdscr.refresh() teichmann@0: if worklog.isRunning(): teichmann@0: signal.alarm(1) teichmann@0: teichmann@0: def exit_handler(flag, frame): teichmann@0: exit_code = 0 teichmann@0: global worklog teichmann@0: try: teichmann@0: worklog.shutdown() teichmann@0: except: teichmann@0: traceback.print_exc(file=sys.stderr) teichmann@0: exit_code = 1 teichmann@0: teichmann@0: restore_cursor() teichmann@0: curses.nocbreak() teichmann@0: stdscr.keypad(0) teichmann@0: curses.echo() teichmann@0: curses.endwin() teichmann@0: sys.exit(exit_code) teichmann@0: teichmann@0: def main(): teichmann@0: teichmann@0: database = len(sys.argv) < 2 and DEFAULT_DATABASE or sys.argv[1] teichmann@0: # TODO: create database file if it does not exist. teichmann@0: teichmann@0: global worklog teichmann@0: try: teichmann@0: worklog = Worklog(database) teichmann@0: except: teichmann@0: traceback.print_exc(file=sys.stderr) teichmann@0: sys.exit(1) teichmann@0: teichmann@0: global stdscr teichmann@0: stdscr = curses.initscr() teichmann@0: curses.noecho() teichmann@0: curses.cbreak() teichmann@0: stdscr.keypad(1) teichmann@0: cursor_visible(0) teichmann@0: teichmann@0: signal.signal(signal.SIGHUP, exit_handler) teichmann@0: signal.signal(signal.SIGINT, exit_handler) teichmann@0: signal.signal(signal.SIGQUIT, exit_handler) teichmann@0: signal.signal(signal.SIGTERM, exit_handler) teichmann@0: teichmann@0: try: teichmann@0: try: teichmann@0: worklog.run() teichmann@0: except: teichmann@0: traceback.print_exc(file=sys.stderr) teichmann@0: finally: teichmann@0: exit_handler(0, None) teichmann@0: teichmann@0: if __name__ == '__main__': teichmann@0: main() teichmann@0: teichmann@0: # vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8: