Mercurial > getan
comparison classic/getan.py @ 115:32dad62909c3
Convert classic getan into a module to be able to install it
author | Björn Ricks <bjoern.ricks@intevation.de> |
---|---|
date | Mon, 12 Dec 2011 09:36:21 +0100 |
parents | classic/getan@9c4e8ba3c4fa |
children |
comparison
equal
deleted
inserted
replaced
114:6df408534f3f | 115:32dad62909c3 |
---|---|
1 #!/usr/bin/env python | |
2 # -*- coding: utf-8 -*- | |
3 # | |
4 # getan | |
5 # ----- | |
6 # (c) 2008 by Sascha L. Teichmann <sascha.teichmann@intevation.de> | |
7 # | |
8 # A python worklog-alike to log what you have 'getan' (done). | |
9 # | |
10 # This is Free Software licensed under the terms of GPLv3 or later. | |
11 # For details see LICENSE coming with the source of 'getan'. | |
12 # | |
13 import sys | |
14 import re | |
15 import curses | |
16 import curses.ascii | |
17 import traceback | |
18 import signal | |
19 | |
20 from datetime import datetime, timedelta, tzinfo | |
21 | |
22 from pysqlite2 import dbapi2 as db | |
23 | |
24 PAUSED = 0 | |
25 RUNNING = 1 | |
26 PRE_EXIT = 2 | |
27 PAUSED_ESC = 3 | |
28 RUNNING_ESC = 4 | |
29 | |
30 SPACE = re.compile("[\\t ]+") | |
31 | |
32 DEFAULT_DATABASE = "time.db" | |
33 | |
34 LOAD_ACTIVE_PROJECTS = ''' | |
35 SELECT id, key, description, total | |
36 FROM projects LEFT JOIN | |
37 (SELECT | |
38 project_id, | |
39 sum(strftime('%s', stop_time) - strftime('%s', start_time)) AS total | |
40 FROM entries | |
41 GROUP BY project_id) ON project_id = id | |
42 WHERE active | |
43 ''' | |
44 | |
45 WRITE_LOG = ''' | |
46 INSERT INTO entries (project_id, start_time, stop_time, description) | |
47 VALUES(:project_id, :start_time, :stop_time, :description) | |
48 ''' | |
49 | |
50 CREATE_PROJECT = ''' | |
51 INSERT INTO projects (key, description) VALUES (:key, :description) | |
52 ''' | |
53 | |
54 LAST_PROJECT_ID = ''' | |
55 SELECT last_insert_rowid() | |
56 ''' | |
57 | |
58 RENAME_PROJECT = ''' | |
59 UPDATE projects set key = :key, description = :description WHERE id = :id | |
60 ''' | |
61 | |
62 ASSIGN_LOGS = ''' | |
63 UPDATE entries SET project_id = :new_id WHERE project_id = :old_id | |
64 ''' | |
65 | |
66 DELETE_PROJECT = ''' | |
67 DELETE FROM projects WHERE id = :id | |
68 ''' | |
69 | |
70 # XXX: This is not very efficent! | |
71 LAST_ENTRY = ''' | |
72 SELECT id, strftime('%s', start_time), strftime('%s', stop_time) FROM entries | |
73 WHERE project_id = :project_id | |
74 ORDER by strftime('%s', stop_time) DESC LIMIT 1 | |
75 ''' | |
76 | |
77 DELETE_ENTRY = ''' | |
78 DELETE FROM entries WHERE id = :id | |
79 ''' | |
80 | |
81 UPDATE_STOP_TIME = ''' | |
82 UPDATE entries SET stop_time = :stop_time WHERE id = :id | |
83 ''' | |
84 | |
85 worklog = None | |
86 stdscr = None | |
87 | |
88 orig_vis = None | |
89 | |
90 def cursor_visible(flag): | |
91 global orig_vis | |
92 try: | |
93 old = curses.curs_set(flag) | |
94 if orig_vis is None: orig_vis = old | |
95 return old | |
96 except: | |
97 pass | |
98 return 1 | |
99 | |
100 def restore_cursor(): | |
101 global orig_vis | |
102 if not orig_vis is None: | |
103 curses.curs_set(orig_vis) | |
104 | |
105 def render_header(ofs=0): | |
106 global stdscr | |
107 stdscr.attron(curses.A_BOLD) | |
108 stdscr.addstr(ofs, 5, "getan v0.1") | |
109 stdscr.addstr(ofs+1, 3, "--------------") | |
110 stdscr.attroff(curses.A_BOLD) | |
111 return ofs + 2 | |
112 | |
113 def render_quit(ofs=0): | |
114 global stdscr | |
115 stdscr.addstr(ofs + 2, 3, "Press DEL once more to quit") | |
116 return ofs + 3 | |
117 | |
118 def tolerantClose(cur): | |
119 if cur: | |
120 try: cur.close() | |
121 except: pass | |
122 | |
123 def ifNull(v, d): | |
124 if v is None: return d | |
125 return v | |
126 | |
127 def human_time(delta): | |
128 seconds = delta.seconds | |
129 s = seconds % 60 | |
130 if delta.microseconds >= 500000: s += 1 | |
131 seconds /= 60 | |
132 m = seconds % 60 | |
133 seconds /= 60 | |
134 out = "%02d:%02d:%02d" % (seconds, m, s) | |
135 if delta.days: | |
136 out = "%dd %s" % (delta.days, out) | |
137 return out | |
138 | |
139 FACTORS = { | |
140 's': 1, | |
141 'm': 60, | |
142 'h' : 60*60, | |
143 'd': 24*60*60} | |
144 | |
145 def human_seconds(timespec): | |
146 """Translate human input to seconds, default factor is minutes""" | |
147 total = 0 | |
148 for v in timespec.split(':'): | |
149 factor = FACTORS.get(v[-1]) | |
150 if factor: v = v[:-1] | |
151 else: factor = 60 | |
152 total += int(v) * factor | |
153 return total | |
154 | |
155 ESC_MAP = { | |
156 curses.KEY_F1 : ord('1'), | |
157 curses.KEY_F2 : ord('2'), | |
158 curses.KEY_F3 : ord('3'), | |
159 curses.KEY_F4 : ord('4'), | |
160 curses.KEY_F5 : ord('5'), | |
161 curses.KEY_F6 : ord('6'), | |
162 curses.KEY_F7 : ord('7'), | |
163 curses.KEY_F8 : ord('8'), | |
164 curses.KEY_F9 : ord('9'), | |
165 curses.KEY_F10: ord('0'), | |
166 } | |
167 | |
168 ZERO = timedelta(0) | |
169 | |
170 class UTC(tzinfo): | |
171 """UTC""" | |
172 | |
173 def utcoffset(self, dt): | |
174 return ZERO | |
175 | |
176 def tzname(self, dt): | |
177 return "UTC" | |
178 | |
179 def dst(self, dt): | |
180 return ZERO | |
181 | |
182 class Project: | |
183 | |
184 def __init__(self, id = None, key = None, desc = None, total = 0): | |
185 self.id = id | |
186 self.key = key | |
187 self.desc = desc | |
188 self.total = timedelta(seconds = ifNull(total, 0)) | |
189 self.start_time = None | |
190 | |
191 def checkExistence(self, cur): | |
192 if self.id is None: | |
193 cur.execute(CREATE_PROJECT, { | |
194 'key' : self.key, | |
195 'description': self.desc}) | |
196 cur.execute(LAST_PROJECT_ID) | |
197 row = cur.fetchone() | |
198 cur.connection.commit() | |
199 self.id = row[0] | |
200 | |
201 def writeLog(self, cur, description = None): | |
202 if self.start_time is None: return | |
203 self.checkExistence(cur) | |
204 now = datetime.now() | |
205 cur.execute(WRITE_LOG, { | |
206 'project_id' : self.id, | |
207 'start_time' : self.start_time, | |
208 'stop_time' : now, | |
209 'description': description}) | |
210 self.total += now-self.start_time | |
211 return now | |
212 | |
213 def getId(self, cur): | |
214 self.checkExistence(cur) | |
215 return self.id | |
216 | |
217 def rename(self, cur, key, desc): | |
218 self.key = key | |
219 self.desc = desc | |
220 self.checkExistence(cur) | |
221 cur.execute(RENAME_PROJECT, { | |
222 'key' : key, | |
223 'description': desc, | |
224 'id' : self.id }) | |
225 cur.connection.commit() | |
226 | |
227 def assignLogs(self, cur, anon): | |
228 self.total += anon.total | |
229 anon.total = timedelta(seconds=0) | |
230 old_id = anon.getId(cur) | |
231 new_id = self.getId(cur) | |
232 cur.execute(ASSIGN_LOGS, { | |
233 'new_id': new_id, | |
234 'old_id': old_id}) | |
235 cur.connection.commit() | |
236 | |
237 def delete(self, cur): | |
238 pid = self.getId(cur) | |
239 cur.execute(DELETE_PROJECT, { 'id': pid }) | |
240 cur.connection.commit() | |
241 | |
242 def subtractTime(self, cur, seconds): | |
243 subtractTimeed, zero = timedelta(), timedelta() | |
244 pid = {'project_id': self.getId(cur)} | |
245 utc = UTC() | |
246 while seconds > zero: | |
247 cur.execute(LAST_ENTRY, pid) | |
248 row = cur.fetchone() | |
249 if row is None: break | |
250 # TODO: evaluate egenix-mx | |
251 start_time = datetime.fromtimestamp(float(row[1]), utc) | |
252 stop_time = datetime.fromtimestamp(float(row[2]), utc) | |
253 runtime = stop_time - start_time | |
254 if runtime <= seconds: | |
255 cur.execute(DELETE_ENTRY, { 'id': row[0] }) | |
256 cur.connection.commit() | |
257 seconds -= runtime | |
258 subtractTimeed += runtime | |
259 else: | |
260 stop_time -= seconds | |
261 cur.execute(UPDATE_STOP_TIME, { | |
262 'id': row[0], | |
263 'stop_time': stop_time}) | |
264 cur.connection.commit() | |
265 subtractTimeed += seconds | |
266 break | |
267 | |
268 self.total -= subtractTimeed | |
269 return subtractTimeed | |
270 | |
271 def addTime(self, cur, seconds, description): | |
272 now = datetime.now() | |
273 cur.execute(WRITE_LOG, { | |
274 'project_id' : self.getId(cur), | |
275 'start_time' : now - seconds, | |
276 'stop_time' : now, | |
277 'description': description | |
278 }) | |
279 cur.connection.commit() | |
280 self.total += seconds | |
281 | |
282 def build_tree(project, depth): | |
283 if len(project.key) == depth+1: | |
284 return ProjectNode(project, project.key[depth]) | |
285 node = ProjectNode(None, project.key[depth]) | |
286 node.children.append(build_tree(project, depth+1)) | |
287 return node | |
288 | |
289 class ProjectNode: | |
290 | |
291 def __init__(self, project = None, key = None): | |
292 self.children = [] | |
293 self.project = project | |
294 self.key = key | |
295 | |
296 def insertProject(self, project, depth = 0): | |
297 | |
298 if not project.key: # anonym -> end | |
299 node = ProjectNode(project) | |
300 self.children.append(node) | |
301 return | |
302 | |
303 for i, child in enumerate(self.children): | |
304 if not child.key: # before anonym projects | |
305 self.children.insert(i, build_tree(project, depth)) | |
306 return | |
307 if child.key == project.key[depth]: | |
308 child.insertProject(project, depth+1) | |
309 return | |
310 self.children.append(build_tree(project, depth)) | |
311 | |
312 def removeProject(self, project): | |
313 | |
314 if self.isLeaf(): return | |
315 stack = [self] | |
316 while stack: | |
317 parent = stack.pop() | |
318 for child in parent.children: | |
319 if not child.isLeaf(): | |
320 stack.append(child) | |
321 continue | |
322 if child.project == project: | |
323 parent.children.remove(child) | |
324 return | |
325 | |
326 def isLeaf(self): | |
327 return not self.project is None | |
328 | |
329 def findProject(self, key): | |
330 l, lower = key.lower(), None | |
331 for child in self.children: | |
332 if child.key == key: | |
333 return child | |
334 if child.key and child.key.lower() == l: | |
335 lower = child | |
336 return lower | |
337 | |
338 def dump(self, depth = 0): | |
339 out = [] | |
340 indent = " " * depth | |
341 out.append("%skey: %s" % (indent, self.key)) | |
342 if self.project: | |
343 out.append("%sdescription: %s" % (indent, self.project.desc)) | |
344 for child in self.children: | |
345 out.append(child.dump(depth+1)) | |
346 return "\n".join(out) | |
347 | |
348 class Worklog: | |
349 | |
350 def __init__(self, database): | |
351 self.initDB(database) | |
352 self.projects = [] | |
353 self.tree = ProjectNode() | |
354 self.state = PAUSED | |
355 self.current_project = None | |
356 self.selection = self.tree | |
357 self.stack = [] | |
358 self.loadProjects() | |
359 | |
360 def initDB(self, database): | |
361 self.con = db.connect(database) | |
362 | |
363 def loadProjects(self): | |
364 cur = None | |
365 try: | |
366 cur = self.con.cursor() | |
367 cur.execute(LOAD_ACTIVE_PROJECTS) | |
368 while True: | |
369 row = cur.fetchone() | |
370 if not row: break | |
371 project = Project(*row) | |
372 self.projects.append(project) | |
373 self.tree.insertProject(project) | |
374 finally: | |
375 tolerantClose(cur) | |
376 | |
377 def shutdown(self): | |
378 self.con.close() | |
379 | |
380 def fetchStack(self): | |
381 cut = ''.join([chr(i) for i in self.stack]) | |
382 self.stack = [] | |
383 return cut | |
384 | |
385 def findProject(self, key): | |
386 key_lower = key.lower() | |
387 lower = None | |
388 | |
389 for p in self.projects: | |
390 if p.key == key: | |
391 return p | |
392 if p.key and p.key.lower() == key_lower: | |
393 lower = p | |
394 | |
395 return lower | |
396 | |
397 def findAnonymProject(self, num): | |
398 count = 0 | |
399 for p in self.projects: | |
400 if p.key is None: | |
401 if count == num: | |
402 return p | |
403 count += 1 | |
404 return None | |
405 | |
406 def renameAnonymProject(self, num, key, description): | |
407 project = self.findAnonymProject(num) | |
408 if project: | |
409 cur = None | |
410 try: | |
411 cur = self.con.cursor() | |
412 project.rename(cur, key, description) | |
413 finally: | |
414 tolerantClose(cur) | |
415 self.tree.removeProject(project) | |
416 self.tree.insertProject(project) | |
417 | |
418 def assignLogs(self, num, key): | |
419 anon = self.findAnonymProject(num) | |
420 if anon is None: return | |
421 project = self.findProject(key) | |
422 if project is None: return | |
423 cur = None | |
424 try: | |
425 cur = self.con.cursor() | |
426 project.assignLogs(cur, anon) | |
427 self.projects.remove(anon) | |
428 anon.delete(cur) | |
429 finally: | |
430 tolerantClose(cur) | |
431 | |
432 def addTime(self, key, seconds, description = None): | |
433 project = self.findProject(key) | |
434 if project is None: return | |
435 cur = None | |
436 try: | |
437 cur = self.con.cursor() | |
438 project.addTime(cur, seconds, description) | |
439 finally: | |
440 tolerantClose(cur) | |
441 | |
442 def subtractTime(self, key, seconds): | |
443 project = self.findProject(key) | |
444 if project is None: return | |
445 cur = None | |
446 try: | |
447 cur = self.con.cursor() | |
448 project.subtractTime(cur, seconds) | |
449 finally: | |
450 tolerantClose(cur) | |
451 | |
452 def isRunning(self): | |
453 return self.state in (RUNNING, RUNNING_ESC) | |
454 | |
455 def totalTime(self): | |
456 sum = timedelta() | |
457 for p in self.projects: | |
458 sum += p.total | |
459 return sum | |
460 | |
461 def render(self, ofs=0): | |
462 ofs = render_header(ofs) | |
463 ml = max([len(p.desc and p.desc or "unknown") for p in self.projects]) | |
464 unknown = 0 | |
465 | |
466 if self.current_project and self.current_project.start_time: | |
467 current_delta = datetime.now() - self.current_project.start_time | |
468 current_time_str = "%s " % human_time(current_delta) | |
469 current_time_space = " " * len(current_time_str) | |
470 else: | |
471 current_delta = timedelta() | |
472 current_time_str = "" | |
473 current_time_space = "" | |
474 | |
475 for project in self.projects: | |
476 is_current = project == self.current_project | |
477 pref = is_current and " -> " or " " | |
478 if project.key is None: | |
479 key = "^%d" % unknown | |
480 unknown += 1 | |
481 else: | |
482 key = " %s" % project.key | |
483 desc = project.desc is None and "unknown" or project.desc | |
484 stdscr.attron(curses.A_BOLD) | |
485 stdscr.addstr(ofs, 0, "%s%s" % (pref, key)) | |
486 stdscr.attroff(curses.A_BOLD) | |
487 stdscr.addstr(" %s" % desc) | |
488 | |
489 diff = ml - len(desc) + 1 | |
490 stdscr.addstr(" " * diff) | |
491 if is_current: stdscr.attron(curses.A_UNDERLINE) | |
492 | |
493 if is_current: | |
494 stdscr.addstr("%s(%s)" % ( | |
495 current_time_str, | |
496 human_time(project.total + current_delta))) | |
497 else: | |
498 stdscr.addstr("%s(%s)" % ( | |
499 current_time_space, | |
500 human_time(project.total))) | |
501 | |
502 if is_current: stdscr.attroff(curses.A_UNDERLINE) | |
503 ofs += 1 | |
504 | |
505 total_str = "(%s)" % human_time(self.totalTime() + current_delta) | |
506 total_x_pos = ml + 8 + len(current_time_space) | |
507 | |
508 stdscr.addstr(ofs, total_x_pos, "=" * len(total_str)) | |
509 ofs += 1 | |
510 stdscr.addstr(ofs, total_x_pos, total_str) | |
511 ofs += 1 | |
512 | |
513 return ofs | |
514 | |
515 def writeLog(self, description = None): | |
516 if self.current_project is None: | |
517 return datetime.now() | |
518 cur = None | |
519 try: | |
520 cur = self.con.cursor() | |
521 now = self.current_project.writeLog(cur, description) | |
522 self.con.commit() | |
523 return now | |
524 finally: | |
525 tolerantClose(cur) | |
526 | |
527 def pausedState(self, c): | |
528 c2 = ESC_MAP.get(c) | |
529 if c2: | |
530 self.pausedEscapeState(c2) | |
531 return | |
532 | |
533 global stdscr | |
534 if c in (curses.KEY_DC, curses.KEY_BACKSPACE): | |
535 stdscr.erase() | |
536 ofs = render_quit(self.render()) | |
537 stdscr.refresh() | |
538 self.state = PRE_EXIT | |
539 | |
540 elif c == curses.ascii.ESC: | |
541 self.state = PAUSED_ESC | |
542 | |
543 elif curses.ascii.isascii(c): | |
544 if c == ord('-'): | |
545 self.selection = self.tree | |
546 stdscr.erase() | |
547 ofs = self.render() | |
548 old_cur = cursor_visible(1) | |
549 curses.echo() | |
550 stdscr.addstr(ofs + 1, 3, "<key> <minutes>: ") | |
551 key = stdscr.getstr() | |
552 curses.noecho() | |
553 cursor_visible(old_cur) | |
554 key = key.strip() | |
555 if key: | |
556 parts = SPACE.split(key, 1) | |
557 if len(parts) > 1: | |
558 key, timespec = parts[0], parts[1] | |
559 try: | |
560 seconds = human_seconds(timespec) | |
561 if seconds > 0: | |
562 seconds = timedelta(seconds=seconds) | |
563 self.subtractTime(key, seconds) | |
564 except ValueError: | |
565 pass | |
566 stdscr.erase() | |
567 self.render() | |
568 stdscr.refresh() | |
569 | |
570 elif c == ord('+'): | |
571 self.selection = self.tree | |
572 stdscr.erase() | |
573 ofs = self.render() | |
574 old_cur = cursor_visible(1) | |
575 curses.echo() | |
576 stdscr.addstr(ofs + 1, 3, "<key> <minutes> [<description>]: ") | |
577 key = stdscr.getstr() | |
578 curses.noecho() | |
579 cursor_visible(old_cur) | |
580 key = key.strip() | |
581 if key: | |
582 parts = SPACE.split(key, 2) | |
583 if len(parts) > 1: | |
584 key, timespec = parts[0], parts[1] | |
585 if len(parts) > 2: desc = parts[2] | |
586 else: desc = None | |
587 try: | |
588 seconds = human_seconds(timespec) | |
589 if seconds > 0: | |
590 seconds = timedelta(seconds=seconds) | |
591 self.addTime(key, seconds, desc) | |
592 except ValueError: | |
593 pass | |
594 stdscr.erase() | |
595 self.render() | |
596 stdscr.refresh() | |
597 | |
598 else: | |
599 node = self.selection.findProject(chr(c)) | |
600 if not node: | |
601 self.selection = self.tree | |
602 return | |
603 if node.isLeaf(): | |
604 self.selection = self.tree | |
605 nproject = node.project | |
606 self.current_project = nproject | |
607 nproject.start_time = datetime.now() | |
608 stdscr.erase() | |
609 ofs = self.render() | |
610 stdscr.refresh() | |
611 self.state = RUNNING | |
612 signal.signal(signal.SIGALRM, alarm_handler) | |
613 signal.alarm(1) | |
614 else: | |
615 self.selection = node | |
616 | |
617 def runningState(self, c): | |
618 global stdscr | |
619 c2 = ESC_MAP.get(c) | |
620 if c2: | |
621 self.runningEscapeState(c2) | |
622 return | |
623 | |
624 if c == curses.ascii.ESC: | |
625 self.state = RUNNING_ESC | |
626 | |
627 elif c == curses.ascii.NL: | |
628 signal.signal(signal.SIGALRM, signal.SIG_IGN) | |
629 self.state = PAUSED | |
630 stdscr.erase() | |
631 ofs = self.render() | |
632 old_cur = cursor_visible(1) | |
633 curses.echo() | |
634 stdscr.addstr(ofs + 1, 3, "Description: ") | |
635 description = stdscr.getstr() | |
636 curses.noecho() | |
637 cursor_visible(old_cur) | |
638 self.writeLog(description) | |
639 self.current_project = None | |
640 stdscr.erase() | |
641 ofs = self.render() | |
642 stdscr.refresh() | |
643 signal.signal(signal.SIGALRM, alarm_handler) | |
644 signal.alarm(1) | |
645 elif c == ord('+'): | |
646 signal.signal(signal.SIGALRM, signal.SIG_IGN) | |
647 stdscr.erase() | |
648 ofs = self.render() | |
649 if self.stack: | |
650 timespec = self.fetchStack() | |
651 else: | |
652 old_cur = cursor_visible(1) | |
653 curses.echo() | |
654 stdscr.addstr(ofs + 1, 3, "Enter time to add: ") | |
655 timespec = stdscr.getstr() | |
656 curses.noecho() | |
657 cursor_visible(old_cur) | |
658 stdscr.erase() | |
659 ofs = self.render() | |
660 try: | |
661 seconds = human_seconds(timespec) | |
662 if seconds > 0: | |
663 seconds = timedelta(seconds=seconds) | |
664 self.current_project.start_time -= seconds | |
665 stdscr.addstr(ofs + 1, 3, "added %s" % human_time(seconds)) | |
666 except (ValueError, IndexError): | |
667 pass | |
668 stdscr.refresh() | |
669 signal.signal(signal.SIGALRM, alarm_handler) | |
670 signal.alarm(1) | |
671 elif c == ord('-'): | |
672 signal.signal(signal.SIGALRM, signal.SIG_IGN) | |
673 stdscr.erase() | |
674 ofs = self.render() | |
675 if self.stack: | |
676 timespec = self.fetchStack() | |
677 else: | |
678 old_cur = cursor_visible(1) | |
679 curses.echo() | |
680 stdscr.addstr(ofs + 1, 3, "Enter time to subtract: ") | |
681 timespec = stdscr.getstr() | |
682 curses.noecho() | |
683 cursor_visible(old_cur) | |
684 stdscr.erase() | |
685 ofs = self.render() | |
686 try: | |
687 seconds = human_seconds(timespec) | |
688 if seconds > 0: | |
689 now = datetime.now() | |
690 seconds = timedelta(seconds=seconds) | |
691 self.current_project.start_time += seconds | |
692 stdscr.addstr(ofs + 1, 3, "subtracted %s" % human_time(seconds)) | |
693 if self.current_project.start_time > now: | |
694 seconds = self.current_project.start_time - now | |
695 self.current_project.start_time = now | |
696 cur = None | |
697 try: | |
698 cur = self.con.cursor() | |
699 self.current_project.subtractTime(cur, seconds) | |
700 finally: | |
701 tolerantClose(cur) | |
702 except (ValueError, IndexError): | |
703 pass | |
704 stdscr.refresh() | |
705 signal.signal(signal.SIGALRM, alarm_handler) | |
706 signal.alarm(1) | |
707 elif self.stack or curses.ascii.isdigit(c): | |
708 self.stack.append(c) | |
709 elif curses.ascii.isascii(c): | |
710 project_node = self.selection.findProject(chr(c)) | |
711 if project_node is None: | |
712 self.selection = self.tree | |
713 return | |
714 | |
715 if project_node.isLeaf(): | |
716 self.selection = self.tree | |
717 nproject = project_node.project | |
718 if nproject == self.current_project: | |
719 return | |
720 nproject.start_time = self.writeLog() | |
721 self.current_project = nproject | |
722 stdscr.erase() | |
723 ofs = self.render() | |
724 stdscr.refresh() | |
725 else: | |
726 self.selection = project_node | |
727 | |
728 def pausedEscapeState(self, c): | |
729 global stdscr | |
730 if curses.ascii.isdigit(c): | |
731 pnum = c - ord('0') | |
732 nproject = self.findAnonymProject(pnum) | |
733 if nproject is None: | |
734 nproject = Project() | |
735 self.projects.append(nproject) | |
736 | |
737 nproject.start_time = self.writeLog() | |
738 self.current_project = nproject | |
739 self.state = RUNNING | |
740 stdscr.erase() | |
741 ofs = self.render() | |
742 stdscr.refresh() | |
743 signal.signal(signal.SIGALRM, alarm_handler) | |
744 signal.alarm(1) | |
745 elif curses.ascii.isalpha(c): | |
746 if c == ord('n'): | |
747 stdscr.erase() | |
748 ofs = self.render() | |
749 old_cur = cursor_visible(1) | |
750 curses.echo() | |
751 stdscr.addstr(ofs + 1, 3, "<num> <key> <description>: ") | |
752 stdscr.refresh() | |
753 description = stdscr.getstr() | |
754 curses.noecho() | |
755 cursor_visible(old_cur) | |
756 | |
757 description = description.strip() | |
758 if description: | |
759 num, key, description = SPACE.split(description, 2) | |
760 try: | |
761 num = int(num) | |
762 self.renameAnonymProject(num, key, description) | |
763 except ValueError: | |
764 pass | |
765 | |
766 stdscr.erase() | |
767 ofs = self.render() | |
768 stdscr.refresh() | |
769 self.state = PAUSED | |
770 | |
771 elif c == ord('a'): | |
772 stdscr.erase() | |
773 ofs = self.render() | |
774 old_cur = cursor_visible(1) | |
775 curses.echo() | |
776 stdscr.addstr(ofs + 1, 3, "<num> <key>: ") | |
777 stdscr.refresh() | |
778 key = stdscr.getstr() | |
779 curses.noecho() | |
780 cursor_visible(old_cur) | |
781 | |
782 key = key.strip() | |
783 if key: | |
784 num, key = SPACE.split(key, 1) | |
785 try: | |
786 num = int(num) | |
787 self.assignLogs(num, key) | |
788 except ValueError: | |
789 pass | |
790 | |
791 stdscr.erase() | |
792 ofs = self.render() | |
793 stdscr.refresh() | |
794 self.state = PAUSED | |
795 else: | |
796 self.state = PAUSED | |
797 else: | |
798 self.state = PAUSED | |
799 | |
800 def runningEscapeState(self, c): | |
801 global stdscr | |
802 if curses.ascii.isdigit(c): | |
803 signal.signal(signal.SIGALRM, signal.SIG_IGN) | |
804 pnum = c - ord('0') | |
805 nproject = self.findAnonymProject(pnum) | |
806 if nproject is None: | |
807 nproject = Project() | |
808 self.projects.append(nproject) | |
809 | |
810 nproject.start_time = self.writeLog() | |
811 self.current_project = nproject | |
812 self.state = RUNNING | |
813 stdscr.erase() | |
814 self.render() | |
815 stdscr.refresh() | |
816 signal.signal(signal.SIGALRM, alarm_handler) | |
817 signal.alarm(1) | |
818 else: | |
819 self.state = RUNNING | |
820 | |
821 | |
822 def run(self): | |
823 global stdscr | |
824 | |
825 stdscr.erase() | |
826 self.render() | |
827 stdscr.refresh() | |
828 | |
829 while True: | |
830 c = stdscr.getch() | |
831 if c == -1: continue | |
832 | |
833 if self.state == PAUSED: | |
834 self.pausedState(c) | |
835 | |
836 elif self.state == RUNNING: | |
837 self.runningState(c) | |
838 | |
839 elif self.state == PAUSED_ESC: | |
840 self.pausedEscapeState(c) | |
841 | |
842 elif self.state == RUNNING_ESC: | |
843 self.runningEscapeState(c) | |
844 | |
845 elif self.state == PRE_EXIT: | |
846 if c in (curses.KEY_DC, curses.KEY_BACKSPACE): | |
847 break | |
848 else: | |
849 stdscr.erase() | |
850 self.render() | |
851 stdscr.refresh() | |
852 self.state = PAUSED | |
853 | |
854 def alarm_handler(flag, frame): | |
855 global worklog | |
856 global stdscr | |
857 | |
858 stdscr.erase() | |
859 worklog.render() | |
860 stdscr.refresh() | |
861 if worklog.isRunning(): | |
862 signal.alarm(1) | |
863 | |
864 def exit_handler(flag, frame): | |
865 exit_code = 0 | |
866 global worklog | |
867 try: | |
868 worklog.shutdown() | |
869 except: | |
870 traceback.print_exc(file=sys.stderr) | |
871 exit_code = 1 | |
872 | |
873 restore_cursor() | |
874 curses.nocbreak() | |
875 stdscr.keypad(0) | |
876 curses.echo() | |
877 curses.endwin() | |
878 sys.exit(exit_code) | |
879 | |
880 def main(): | |
881 | |
882 database = len(sys.argv) < 2 and DEFAULT_DATABASE or sys.argv[1] | |
883 # TODO: create database file if it does not exist. | |
884 | |
885 global worklog | |
886 try: | |
887 worklog = Worklog(database) | |
888 except: | |
889 traceback.print_exc(file=sys.stderr) | |
890 sys.exit(1) | |
891 | |
892 global stdscr | |
893 stdscr = curses.initscr() | |
894 curses.noecho() | |
895 curses.cbreak() | |
896 stdscr.keypad(1) | |
897 cursor_visible(0) | |
898 | |
899 signal.signal(signal.SIGHUP, exit_handler) | |
900 signal.signal(signal.SIGINT, exit_handler) | |
901 signal.signal(signal.SIGQUIT, exit_handler) | |
902 signal.signal(signal.SIGTERM, exit_handler) | |
903 | |
904 try: | |
905 try: | |
906 worklog.run() | |
907 except: | |
908 traceback.print_exc(file=sys.stderr) | |
909 finally: | |
910 exit_handler(0, None) | |
911 | |
912 if __name__ == '__main__': | |
913 main() | |
914 | |
915 # vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8: |