comparison getan @ 0:7eb7886ed8fd

Initial import.
author Sascha L. Teichmann <teichmann@intevation.de>
date Mon, 28 Jul 2008 22:33:36 +0200
parents
children a3fe8e4e9184
comparison
equal deleted inserted replaced
-1:000000000000 0:7eb7886ed8fd
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
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 worklog = None
71 stdscr = None
72
73 orig_vis = None
74
75 def cursor_visible(flag):
76 global orig_vis
77 try:
78 old = curses.curs_set(flag)
79 if orig_vis is None: orig_vis = old
80 return old
81 except:
82 pass
83 return 1
84
85 def restore_cursor():
86 global orig_vis
87 if not orig_vis is None:
88 curses.curs_set(orig_vis)
89
90 def render_header(ofs=0):
91 global stdscr
92 stdscr.attron(curses.A_BOLD)
93 stdscr.addstr(ofs, 5, "getan v0.1")
94 stdscr.addstr(ofs+1, 3, "--------------")
95 stdscr.attroff(curses.A_BOLD)
96 return ofs + 2
97
98 def render_quit(ofs=0):
99 global stdscr
100 stdscr.addstr(ofs + 2, 3, "Press DEL once more to quit")
101 return ofs + 3
102
103 def tolerantClose(cur):
104 if cur:
105 try: cur.close()
106 except: pass
107
108 def ifNull(v, d):
109 if v is None: return d
110 return v
111
112 def human_time(delta):
113 seconds = delta.seconds
114 s = seconds % 60
115 if delta.microseconds >= 500000: s += 1
116 seconds /= 60
117 m = seconds % 60
118 seconds /= 60
119 out = "%02d:%02d:%02d" % (seconds, m, s)
120 if delta.days:
121 out = "%dd %s" % (delta.days, out)
122 return out
123
124 class Project:
125
126 def __init__(self, id = None, key = None, desc = None, total = 0):
127 self.id = id
128 self.key = key
129 self.desc = desc
130 self.total = timedelta(seconds = ifNull(total, 0))
131 self.start_time = None
132
133 def checkExistence(self, cur):
134 if self.id is None:
135 cur.execute(CREATE_PROJECT, {
136 'key' : self.key,
137 'description': self.desc})
138 cur.execute(LAST_PROJECT_ID)
139 row = cur.fetchone()
140 cur.connection.commit()
141 self.id = row[0]
142
143 def writeLog(self, cur, description = None):
144 if self.start_time is None: return
145 self.checkExistence(cur)
146 now = datetime.now()
147 cur.execute(WRITE_LOG, {
148 'project_id' : self.id,
149 'start_time' : self.start_time,
150 'stop_time' : now,
151 'description': description})
152 self.total += now-self.start_time
153 return now
154
155 def getId(self, cur):
156 self.checkExistence(cur)
157 return self.id
158
159 def rename(self, cur, key, desc):
160 self.key = key
161 self.desc = desc
162 self.checkExistence(cur)
163 cur.execute(RENAME_PROJECT, {
164 'key' : key,
165 'description': desc,
166 'id' : self.id })
167 cur.connection.commit()
168
169 def assignLogs(self, cur, anon):
170 self.total += anon.total
171 anon.total = timedelta(seconds=0)
172 old_id = anon.getId(cur)
173 new_id = self.getId(cur)
174 cur.execute(ASSIGN_LOGS, {
175 'new_id': new_id,
176 'old_id': old_id})
177 cur.connection.commit()
178
179 def delete(self, cur):
180 id = self.getId(cur)
181 cur.execute(DELETE_PROJECT, { 'id': id })
182 cur.connection.commit()
183
184 class Worklog:
185
186 def __init__(self, database):
187 self.initDB(database)
188 self.loadProjects()
189 self.state = PAUSED
190 self.current_project = None
191
192 def initDB(self, database):
193 self.con = db.connect(database)
194
195 def loadProjects(self):
196 cur = None
197 try:
198 cur = self.con.cursor()
199 cur.execute(LOAD_ACTIVE_PROJECTS)
200 self.projects = [Project(*row) for row in cur.fetchall()]
201 finally:
202 tolerantClose(cur)
203
204 def shutdown(self):
205 self.con.close()
206
207 def findProject(self, key):
208 key_lower = key.lower()
209 lower = None
210
211 for p in self.projects:
212 if p.key == key:
213 return p
214 if p.key and p.key.lower() == key_lower:
215 lower = p
216
217 return lower
218
219 def findAnonymProject(self, num):
220 count = 0
221 for p in self.projects:
222 if p.key is None:
223 if count == num:
224 return p
225 count += 1
226 return None
227
228 def renameAnonymProject(self, num, key, description):
229 project = self.findAnonymProject(num)
230 if project:
231 cur = None
232 try:
233 cur = self.con.cursor()
234 project.rename(cur, key, description)
235 finally:
236 tolerantClose(cur)
237
238 def assignLogs(self, num, key):
239 anon = self.findAnonymProject(num)
240 if anon is None: return
241 project = self.findProject(key)
242 if project is None: return
243 cur = None
244 try:
245 cur = self.con.cursor()
246 project.assignLogs(cur, anon)
247 self.projects.remove(anon)
248 anon.delete(cur)
249 finally:
250 tolerantClose(cur)
251
252 def isRunning(self):
253 return self.state in (RUNNING, RUNNING_ESC)
254
255 def render(self, ofs=0):
256 ofs = render_header(ofs)
257 ml = max([len(p.desc and p.desc or "unknown") for p in self.projects])
258 unknown = 0
259
260 if self.current_project and self.current_project.start_time:
261 current_delta = datetime.now() - self.current_project.start_time
262 current_time_str = "%s " % human_time(current_delta)
263 current_time_space = " " * len(current_time_str)
264 else:
265 current_time_str = ""
266 current_time_space = ""
267
268 for project in self.projects:
269 is_current = project == self.current_project
270 pref = is_current and " -> " or " "
271 if project.key is None:
272 key = "^%d" % unknown
273 unknown += 1
274 else:
275 key = " %s" % project.key
276 desc = project.desc is None and "unknown" or project.desc
277 stdscr.attron(curses.A_BOLD)
278 stdscr.addstr(ofs, 0, "%s%s" % (pref, key))
279 stdscr.attroff(curses.A_BOLD)
280 stdscr.addstr(" %s" % desc)
281
282 diff = ml - len(desc) + 1
283 stdscr.addstr(" " * diff)
284 if is_current: stdscr.attron(curses.A_UNDERLINE)
285
286 if is_current:
287 stdscr.addstr("%s(%s)" % (
288 current_time_str,
289 human_time(project.total + current_delta)))
290 else:
291 stdscr.addstr("%s(%s)" % (
292 current_time_space,
293 human_time(project.total)))
294
295 if is_current: stdscr.attroff(curses.A_UNDERLINE)
296 ofs += 1
297
298 return ofs
299
300 def writeLog(self, description = None):
301 if self.current_project is None:
302 return datetime.now()
303 cur = None
304 try:
305 cur = self.con.cursor()
306 now = self.current_project.writeLog(cur, description)
307 self.con.commit()
308 return now
309 finally:
310 tolerantClose(cur)
311
312 def run(self):
313 global stdscr
314
315 stdscr.erase()
316 ofs = self.render()
317 stdscr.refresh()
318
319 while True:
320 c = stdscr.getch()
321 if c == -1: continue
322
323 if self.state == PAUSED:
324
325 if c == curses.KEY_DC:
326 stdscr.erase()
327 ofs = render_quit(self.render())
328 stdscr.refresh()
329 self.state = PRE_EXIT
330
331 elif c == curses.ascii.ESC:
332 self.state = PAUSED_ESC
333
334 elif curses.ascii.isascii(c):
335 nproject = self.findProject(chr(c))
336 if nproject is None: continue
337 self.current_project = nproject
338 nproject.start_time = datetime.now()
339 stdscr.erase()
340 ofs = self.render()
341 stdscr.refresh()
342 self.state = RUNNING
343 signal.signal(signal.SIGALRM, alarm_handler)
344 signal.alarm(1)
345
346 elif self.state == RUNNING:
347 if c == curses.ascii.ESC:
348 self.state = RUNNING_ESC
349
350 elif c == curses.ascii.NL:
351 signal.signal(signal.SIGALRM, signal.SIG_IGN)
352 self.state = PAUSED
353 stdscr.erase()
354 ofs = self.render()
355 old_cur = cursor_visible(1)
356 curses.echo()
357 stdscr.addstr(ofs + 1, 3, "Description: ")
358 description = stdscr.getstr()
359 curses.noecho()
360 cursor_visible(old_cur)
361 self.writeLog(description)
362 self.current_project = None
363 stdscr.erase()
364 ofs = self.render()
365 stdscr.refresh()
366
367 elif curses.ascii.isascii(c):
368 nproject = self.findProject(chr(c))
369 if nproject is None or nproject == self.current_project:
370 continue
371 nproject.start_time = self.writeLog()
372 self.current_project = nproject
373 stdscr.erase()
374 ofs = self.render()
375 stdscr.refresh()
376
377 elif self.state == PAUSED_ESC:
378 if curses.ascii.isdigit(c):
379 pnum = c - ord('0')
380 nproject = self.findAnonymProject(pnum)
381 if nproject is None:
382 nproject = Project()
383 self.projects.append(nproject)
384
385 nproject.start_time = self.writeLog()
386 self.current_project = nproject
387 self.state = RUNNING
388 stdscr.erase()
389 ofs = self.render()
390 stdscr.refresh()
391 signal.signal(signal.SIGALRM, alarm_handler)
392 signal.alarm(1)
393 elif curses.ascii.isalpha(c):
394 if c == ord('n'):
395 stdscr.erase()
396 ofs = self.render()
397 old_cur = cursor_visible(1)
398 curses.echo()
399 stdscr.addstr(ofs + 1, 3, "<num> <key> <description>: ")
400 stdscr.refresh()
401 description = stdscr.getstr()
402 curses.noecho()
403 cursor_visible(old_cur)
404
405 description = description.strip()
406 if description:
407 num, key, description = SPACE.split(description, 2)
408 try:
409 num = int(num)
410 self.renameAnonymProject(num, key, description)
411 except ValueError:
412 pass
413
414 stdscr.erase()
415 ofs = self.render()
416 stdscr.refresh()
417 self.state = PAUSED
418
419 elif c == ord('a'):
420 stdscr.erase()
421 ofs = self.render()
422 old_cur = cursor_visible(1)
423 curses.echo()
424 stdscr.addstr(ofs + 1, 3, "<num> <key>: ")
425 stdscr.refresh()
426 key = stdscr.getstr()
427 curses.noecho()
428 cursor_visible(old_cur)
429
430 key = key.strip()
431 if key:
432 num, key = SPACE.split(key, 1)
433 try:
434 num = int(num)
435 self.assignLogs(num, key)
436 except ValueError:
437 pass
438
439 stdscr.erase()
440 ofs = self.render()
441 stdscr.refresh()
442 self.state = PAUSED
443
444 else:
445 self.state = PAUSED
446 pass
447 else:
448 self.state = PAUSED
449
450 elif self.state == RUNNING_ESC:
451 if curses.ascii.isdigit(c):
452 signal.signal(signal.SIGALRM, signal.SIG_IGN)
453 pnum = c - ord('0')
454 nproject = self.findAnonymProject(pnum)
455 if nproject is None:
456 nproject = Project()
457 self.projects.append(nproject)
458
459 nproject.start_time = self.writeLog()
460 self.current_project = nproject
461 self.state = RUNNING
462 stdscr.erase()
463 ofs = self.render()
464 stdscr.refresh()
465 signal.signal(signal.SIGALRM, alarm_handler)
466 signal.alarm(1)
467 else:
468 self.state = RUNNING
469
470 elif self.state == PRE_EXIT:
471 if c == curses.KEY_DC:
472 break
473 else:
474 stdscr.erase()
475 ofs = self.render()
476 stdscr.refresh()
477 self.state = PAUSED
478
479
480
481 def alarm_handler(flag, frame):
482 global worklog
483 global stdscr
484
485 stdscr.erase()
486 worklog.render()
487 stdscr.refresh()
488 if worklog.isRunning():
489 signal.alarm(1)
490
491 def exit_handler(flag, frame):
492 exit_code = 0
493 global worklog
494 try:
495 worklog.shutdown()
496 except:
497 traceback.print_exc(file=sys.stderr)
498 exit_code = 1
499
500 restore_cursor()
501 curses.nocbreak()
502 stdscr.keypad(0)
503 curses.echo()
504 curses.endwin()
505 sys.exit(exit_code)
506
507 def main():
508
509 database = len(sys.argv) < 2 and DEFAULT_DATABASE or sys.argv[1]
510 # TODO: create database file if it does not exist.
511
512 global worklog
513 try:
514 worklog = Worklog(database)
515 except:
516 traceback.print_exc(file=sys.stderr)
517 sys.exit(1)
518
519 global stdscr
520 stdscr = curses.initscr()
521 curses.noecho()
522 curses.cbreak()
523 stdscr.keypad(1)
524 cursor_visible(0)
525
526 signal.signal(signal.SIGHUP, exit_handler)
527 signal.signal(signal.SIGINT, exit_handler)
528 signal.signal(signal.SIGQUIT, exit_handler)
529 signal.signal(signal.SIGTERM, exit_handler)
530
531 try:
532 try:
533 worklog.run()
534 except:
535 traceback.print_exc(file=sys.stderr)
536 finally:
537 exit_handler(0, None)
538
539 if __name__ == '__main__':
540 main()
541
542 # vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8:
This site is hosted by Intevation GmbH (Datenschutzerklärung und Impressum | Privacy Policy and Imprint)