719
by Bryce Harrington
Initial import of dbus-based command line tool |
1 |
#!/usr/bin/env python
|
2 |
# -*- coding:utf-8 -*-
|
|
3 |
||
4 |
'''Command line user interface for manipulating tasks in gtg.
|
|
5 |
||
6 |
Copyright (C) 2010 Bryce W. Harrington
|
|
7 |
||
8 |
This program is free software; you can redistribute it and/or modify it
|
|
9 |
under the terms of the GNU General Public License as published by the
|
|
10 |
Free Software Foundation; either version 2 of the License, or (at your
|
|
11 |
option) any later version. See http://www.gnu.org/copyleft/gpl.html for
|
|
12 |
the full text of the license.
|
|
13 |
'''
|
|
14 |
||
15 |
import re |
|
16 |
import sys |
|
17 |
import os |
|
18 |
import dbus |
|
19 |
import cgi |
|
20 |
import getopt |
|
21 |
import textwrap |
|
759.1.5
by Bryce Harrington
Adjust logic to try to better filter workable items |
22 |
from datetime import datetime, date, timedelta |
722.1.10
by Bryce Harrington
Search by multiple tags |
23 |
from string import split |
719
by Bryce Harrington
Initial import of dbus-based command line tool |
24 |
|
25 |
def _(text): |
|
26 |
return text |
|
27 |
||
28 |
def usage(): |
|
29 |
f = " %-30s %s\n" |
|
30 |
progname = sys.argv[0] |
|
31 |
||
32 |
text = _("gtcli -- a command line interface to gtg\n") |
|
33 |
text += "\n" |
|
34 |
||
35 |
text += _("Options:\n") |
|
36 |
text += f%( "-h, --help", _("This help") ) |
|
37 |
text += "\n" |
|
38 |
||
39 |
text += _("Basic commands:\n") |
|
40 |
text += f%( "gtcli new", _("Create a new task") ) |
|
41 |
text += f%( "gtcli show <tid>", _("Display detailed information on given task id") ) |
|
42 |
text += f%( "gtcli edit <tid>", _("Opens the GUI editor for the given task id") ) |
|
43 |
text += f%( "gtcli delete <tid>", _("Removes task identified by tid") ) |
|
722.1.15
by Bryce Harrington
Update documentation for gtcli list |
44 |
text += f%( "gtcli list [all|today|<filter>|<tag>]...", _("List tasks") ) |
45 |
text += f%( "gtcli count [all|today|<filter>|<tag>]...", _("Number of tasks") ) |
|
759.1.3
by Bryce Harrington
Add 'gtcli summary' which prints out # tasks for 2 weeks |
46 |
text += f%( "gtcli summary [all|today|<filter>|<tag>]...", _("Report how many tasks starting/due each day") ) |
725
by Bryce Harrington
Fix modify_task() dbus call by converting date strings to Dates |
47 |
text += f%( "gtcli postpone <tid> <date>", _("Updates the start date of task") ) |
726
by Bryce Harrington
Redocument gtcli close, which works now thanks to previous change |
48 |
text += f%( "gtcli close <tid>", _("Sets state of task identified by tid to closed") ) |
739.1.1
by Bryce Harrington
Add ability to hide/show the task browser. |
49 |
text += f%( "gtcli browse [hide|show]", _("Hides or shows the task browser window")) |
719
by Bryce Harrington
Initial import of dbus-based command line tool |
50 |
|
51 |
text += "\n" |
|
52 |
text += "http://gtg.fritalk.com/\n" |
|
53 |
sys.stderr.write( text ) |
|
54 |
||
55 |
def die(code=1, err=None): |
|
56 |
if err: |
|
57 |
sys.stderr.write(str(err)) |
|
58 |
sys.exit(code) |
|
59 |
||
60 |
def connect_to_gtg(): |
|
61 |
try: |
|
62 |
bus = dbus.SessionBus() |
|
63 |
except dbus.exceptions.DBusException, e: |
|
64 |
if "X11 initialization failed" in e.get_dbus_message(): |
|
65 |
os.environ['DISPLAY'] = ":0" |
|
66 |
bus = dbus.SessionBus() |
|
67 |
else: |
|
68 |
print "dbus exception: '%s'" %(err) |
|
69 |
raise
|
|
70 |
||
71 |
liste = bus.list_names() |
|
862.1.1
by Bryce Harrington
Namespace gtg's D-BUS service name |
72 |
busname = "org.gnome.GTG" |
73 |
remote_object = bus.get_object(busname,"/org/gnome/GTG") |
|
74 |
return dbus.Interface(remote_object,dbus_interface="org.gnome.GTG") |
|
719
by Bryce Harrington
Initial import of dbus-based command line tool |
75 |
|
76 |
def new_task(title, body): |
|
77 |
""" Retrieve task via dbus """
|
|
78 |
timi = connect_to_gtg() |
|
960.1.5
by Lionel Dricot
switched the DBus API to CamelCase |
79 |
timi.NewTask("Active", title, '', '', '', [], body, []) |
719
by Bryce Harrington
Initial import of dbus-based command line tool |
80 |
|
81 |
def delete_task(tid): |
|
82 |
""" Remove a task via dbus """
|
|
83 |
timi = connect_to_gtg() |
|
960.1.5
by Lionel Dricot
switched the DBus API to CamelCase |
84 |
timi.DeleteTask(tid) |
719
by Bryce Harrington
Initial import of dbus-based command line tool |
85 |
|
86 |
def close_task(tid): |
|
87 |
""" Marks a task closed """
|
|
88 |
timi = connect_to_gtg() |
|
960.1.5
by Lionel Dricot
switched the DBus API to CamelCase |
89 |
task_data = timi.GetTask(tid) |
719
by Bryce Harrington
Initial import of dbus-based command line tool |
90 |
task_data['status'] = "Done" |
960.1.5
by Lionel Dricot
switched the DBus API to CamelCase |
91 |
timi.ModifyTask(tid, task_data) |
719
by Bryce Harrington
Initial import of dbus-based command line tool |
92 |
|
93 |
def show_task(tid): |
|
94 |
""" Displays a given task """
|
|
95 |
timi = connect_to_gtg() |
|
960.1.5
by Lionel Dricot
switched the DBus API to CamelCase |
96 |
task_data = timi.GetTask(tid) |
719
by Bryce Harrington
Initial import of dbus-based command line tool |
97 |
content_regex = re.compile(r"<content>(.+)</content>", re.DOTALL) |
98 |
||
99 |
content = task_data['text'] + "\n(unknown)" |
|
100 |
m = content_regex.match(task_data['text']) |
|
101 |
if m: |
|
102 |
content = m.group(1) |
|
103 |
||
104 |
print task_data['title'] |
|
105 |
if len(task_data['tags'])>0: |
|
106 |
print " %-12s %s" %('tags:', task_data['tags'][0]) |
|
107 |
for k in ['id', 'startdate', 'duedate', 'status']: |
|
108 |
print " %-12s %s" %(k+":", task_data[k]) |
|
737
by Luca Invernizzi
* gtcli now shows parent task in the "show" option. |
109 |
if len(task_data['parents'])>0: |
110 |
print " %-12s %s" %('parents:', task_data['parents'][0]) |
|
719
by Bryce Harrington
Initial import of dbus-based command line tool |
111 |
print
|
112 |
print content |
|
113 |
||
823.1.5
by Bryce Harrington
Add ability to postpone tasks by tag |
114 |
def postpone(identifier, startdate): |
719
by Bryce Harrington
Initial import of dbus-based command line tool |
115 |
""" Change the start date of a task """
|
116 |
timi = connect_to_gtg() |
|
823.1.5
by Bryce Harrington
Add ability to postpone tasks by tag |
117 |
|
118 |
tasks = [] |
|
119 |
if identifier[0] == '@': |
|
120 |
filters = _criteria_to_filters(identifier) |
|
121 |
filters.extend(['active','workview']) |
|
122 |
timi = connect_to_gtg() |
|
960.1.5
by Lionel Dricot
switched the DBus API to CamelCase |
123 |
tasks = timi.GetTasksFiltered(filters) |
823.1.5
by Bryce Harrington
Add ability to postpone tasks by tag |
124 |
else: |
960.1.5
by Lionel Dricot
switched the DBus API to CamelCase |
125 |
tasks = [ timi.GeTask(identifier) ] |
823.1.5
by Bryce Harrington
Add ability to postpone tasks by tag |
126 |
|
127 |
for task in tasks: |
|
128 |
task['startdate'] = startdate |
|
959
by Izidor Matušov
Merged Bryce's fix, solved LP:602108 and updated DBUS interface to use own view instead of active view |
129 |
print "%-12s %-20s %s" %(task['id'], ", ".join(task['tags']), task['title']) |
960.1.5
by Lionel Dricot
switched the DBus API to CamelCase |
130 |
timi.ModifyTask(task['id'], task) |
719
by Bryce Harrington
Initial import of dbus-based command line tool |
131 |
|
132 |
def open_task_editor(tid): |
|
133 |
""" Load task in the task editor gui """
|
|
134 |
timi = connect_to_gtg() |
|
960.1.5
by Lionel Dricot
switched the DBus API to CamelCase |
135 |
task_data = timi.OpenTaskEditor(tid) |
719
by Bryce Harrington
Initial import of dbus-based command line tool |
136 |
|
739.1.1
by Bryce Harrington
Add ability to hide/show the task browser. |
137 |
def toggle_browser_visibility(state): |
722.1.15
by Bryce Harrington
Update documentation for gtcli list |
138 |
""" Cause the task browser to be displayed """
|
739.1.1
by Bryce Harrington
Add ability to hide/show the task browser. |
139 |
timi = connect_to_gtg() |
140 |
if state == "hide": |
|
960.1.5
by Lionel Dricot
switched the DBus API to CamelCase |
141 |
timi.HideTaskBrowser() |
744
by Bryce Harrington
Add ability to make task browser iconified |
142 |
elif state == "minimize" or state == "iconify": |
960.1.5
by Lionel Dricot
switched the DBus API to CamelCase |
143 |
if not timi.IsTaskBrowserVisible(): |
144 |
timi.ShowTaskBrowser() |
|
145 |
timi.IconifyTaskBrowser() |
|
744
by Bryce Harrington
Add ability to make task browser iconified |
146 |
else: |
960.1.5
by Lionel Dricot
switched the DBus API to CamelCase |
147 |
timi.ShowTaskBrowser() |
739.1.1
by Bryce Harrington
Add ability to hide/show the task browser. |
148 |
|
759.1.2
by Bryce Harrington
Refactor out counting code from list code |
149 |
def _criteria_to_filters(criteria): |
150 |
if not criteria: |
|
151 |
filters = ['active'] |
|
152 |
else: |
|
153 |
filters = split(criteria, ' ') |
|
154 |
||
155 |
# Special case 'today' filter
|
|
156 |
if 'today' in filters: |
|
157 |
filters.extend(['active', 'workview']) |
|
158 |
filters.remove('today') |
|
159 |
return filters |
|
160 |
||
161 |
def count_tasks(criteria): |
|
162 |
""" Print a simple count of tasks matching criteria """
|
|
163 |
||
164 |
filters = _criteria_to_filters(criteria) |
|
165 |
timi = connect_to_gtg() |
|
960.1.5
by Lionel Dricot
switched the DBus API to CamelCase |
166 |
tasks = timi.GetTasksFiltered(filters) |
759.1.2
by Bryce Harrington
Refactor out counting code from list code |
167 |
|
168 |
total = 0 |
|
169 |
for task in tasks: |
|
170 |
if 'title' not in task: |
|
171 |
continue
|
|
172 |
total += 1 |
|
173 |
||
174 |
print total |
|
175 |
return total |
|
176 |
||
759.1.3
by Bryce Harrington
Add 'gtcli summary' which prints out # tasks for 2 weeks |
177 |
def summary_of_tasks(criteria): |
178 |
""" Print report showing number of tasks starting and due each day """
|
|
179 |
||
759.1.10
by Bryce Harrington
Default summary to active workable tasks |
180 |
if not criteria: |
791
by Bryce Harrington
I think by definition summary only shows counts for active tasks, so |
181 |
criteria = 'workable' |
759.1.5
by Bryce Harrington
Adjust logic to try to better filter workable items |
182 |
|
759.1.3
by Bryce Harrington
Add 'gtcli summary' which prints out # tasks for 2 weeks |
183 |
filters = _criteria_to_filters(criteria) |
791
by Bryce Harrington
I think by definition summary only shows counts for active tasks, so |
184 |
filters.append('active') |
759.1.3
by Bryce Harrington
Add 'gtcli summary' which prints out # tasks for 2 weeks |
185 |
timi = connect_to_gtg() |
960.1.5
by Lionel Dricot
switched the DBus API to CamelCase |
186 |
tasks = timi.GetTasksFiltered(filters) |
759.1.3
by Bryce Harrington
Add 'gtcli summary' which prints out # tasks for 2 weeks |
187 |
|
188 |
report = { } |
|
189 |
for t in tasks: |
|
759.1.5
by Bryce Harrington
Adjust logic to try to better filter workable items |
190 |
if not t['startdate']: |
759.1.8
by Bryce Harrington
s/someday/unscheduled/ |
191 |
startdate = 'unscheduled' |
759.1.5
by Bryce Harrington
Adjust logic to try to better filter workable items |
192 |
elif datetime.strptime(t['startdate'], "%Y-%m-%d") < datetime.today(): |
193 |
startdate = date.today().strftime("%Y-%m-%d") |
|
194 |
else: |
|
195 |
startdate = t['startdate'] |
|
196 |
||
759.1.3
by Bryce Harrington
Add 'gtcli summary' which prints out # tasks for 2 weeks |
197 |
if startdate not in report: |
198 |
report[startdate] = { 'starting':0, 'due':0 } |
|
199 |
report[startdate]['starting'] += 1 |
|
200 |
||
201 |
duedate = t['duedate'] or 'never' |
|
202 |
if duedate not in report: |
|
203 |
report[duedate] = { 'starting':0, 'due':0 } |
|
204 |
report[duedate]['due'] += 1 |
|
205 |
||
206 |
day = date.today() |
|
207 |
print "%-20s %5s %5s" %("", "Start", "Due") |
|
823.1.1
by Bryce Harrington
Don't print unscheduled tasks if there aren't any |
208 |
if report.has_key('unscheduled'): |
209 |
print "%-20s %5d %5d" %('unscheduled', |
|
210 |
report['unscheduled']['starting'], |
|
211 |
report['unscheduled']['due']) |
|
798
by Bryce Harrington
Fix off by one to show three complete weeks of data |
212 |
num_days = 22 |
866
by Bryce Harrington
Be more consistent with date formatting |
213 |
fmt = "%a %-m-%-d" |
759.1.7
by Bryce Harrington
Make 'status today' show only the tasks for the current work day |
214 |
if criteria and 'today' in criteria: |
215 |
num_days = 1 |
|
216 |
for i in range(0, num_days): |
|
759.1.3
by Bryce Harrington
Add 'gtcli summary' which prints out # tasks for 2 weeks |
217 |
d = day + timedelta(i) |
218 |
dstr = str(d) |
|
219 |
if dstr in report: |
|
220 |
print "%-20s %5d %5d" %(d.strftime(fmt), |
|
221 |
report[dstr]['starting'], |
|
222 |
report[dstr]['due']) |
|
223 |
else: |
|
866
by Bryce Harrington
Be more consistent with date formatting |
224 |
print "%-20s %5d %5d" %(d.strftime(fmt), 0, 0) |
759.1.7
by Bryce Harrington
Make 'status today' show only the tasks for the current work day |
225 |
|
759.1.3
by Bryce Harrington
Add 'gtcli summary' which prints out # tasks for 2 weeks |
226 |
|
719
by Bryce Harrington
Initial import of dbus-based command line tool |
227 |
def list_tasks(criteria, count_only=False): |
722.1.15
by Bryce Harrington
Update documentation for gtcli list |
228 |
""" Display a listing of tasks
|
229 |
|
|
230 |
Accepts any filter or combination of filters or tags to limit the
|
|
231 |
set of tasks shown. If multiple tags specified, it lists only tasks
|
|
232 |
that have all the tags. If no filters or tags are specified,
|
|
233 |
defaults to showing all active tasks.
|
|
234 |
"""
|
|
759.1.2
by Bryce Harrington
Refactor out counting code from list code |
235 |
|
236 |
filters = _criteria_to_filters(criteria) |
|
719
by Bryce Harrington
Initial import of dbus-based command line tool |
237 |
timi = connect_to_gtg() |
960.1.5
by Lionel Dricot
switched the DBus API to CamelCase |
238 |
tasks = timi.GetTasksFiltered(filters) |
719
by Bryce Harrington
Initial import of dbus-based command line tool |
239 |
|
240 |
tasks_tree = { } |
|
241 |
notag = '@__notag' |
|
242 |
for task in tasks: |
|
243 |
if 'title' not in task: |
|
724
by Bryce Harrington
Replace broken dbus calls with new ones using filters |
244 |
continue
|
719
by Bryce Harrington
Initial import of dbus-based command line tool |
245 |
if not task['tags'] or len(task['tags']) == 0: |
246 |
if notag not in tasks_tree: |
|
247 |
tasks_tree[notag] = [] |
|
248 |
tasks_tree[notag].append(task) |
|
249 |
else: |
|
250 |
tags = [] |
|
251 |
for tag in list(task['tags']): |
|
940
by Lionel Dricot
fixing #655514, thanks Thibault Févry for the fix |
252 |
tags.append(tag) |
719
by Bryce Harrington
Initial import of dbus-based command line tool |
253 |
if tag not in tasks_tree: |
254 |
tasks_tree[tag] = [] |
|
255 |
tasks_tree[tag].append(task) |
|
256 |
||
722.1.15
by Bryce Harrington
Update documentation for gtcli list |
257 |
# If any tags were specified, use only those as the categories
|
722.1.14
by Bryce Harrington
Make `gtcli list` take space-separated filters/tags as criteria |
258 |
keys = [t for t in filters if t[0]=='@'] |
259 |
if not keys: |
|
722.1.10
by Bryce Harrington
Search by multiple tags |
260 |
keys = tasks_tree.keys() |
261 |
keys.sort() |
|
722.1.14
by Bryce Harrington
Make `gtcli list` take space-separated filters/tags as criteria |
262 |
|
719
by Bryce Harrington
Initial import of dbus-based command line tool |
263 |
for key in keys: |
722.1.16
by Bryce Harrington
Ignore non-existant tags that were asked as filters |
264 |
if key not in tasks_tree: |
265 |
continue
|
|
719
by Bryce Harrington
Initial import of dbus-based command line tool |
266 |
if key != notag: |
267 |
print "%s:" %(key[1:]) |
|
268 |
for task in tasks_tree[key]: |
|
269 |
text = textwrap.fill(task['title'], |
|
823.1.5
by Bryce Harrington
Add ability to postpone tasks by tag |
270 |
initial_indent='', |
845
by Bryce Harrington
Increase column width for ids now that ids can be uuids |
271 |
subsequent_indent=' ') |
272 |
print " %-36s %s" %(task['id'], text) |
|
719
by Bryce Harrington
Initial import of dbus-based command line tool |
273 |
|
274 |
if __name__ == '__main__': |
|
275 |
try: |
|
276 |
opts, args = getopt.gnu_getopt(sys.argv[1:], "h", ["help"]) |
|
277 |
except getopt.GetoptError, err: |
|
278 |
sys.stderr.write("Error: " + str(err) + "\n\n") |
|
279 |
usage() |
|
280 |
sys.exit(2) |
|
281 |
for o, a in opts: |
|
282 |
if o in ("-h", "--help"): |
|
283 |
usage() |
|
284 |
sys.exit(0) |
|
285 |
else: |
|
286 |
assert False, "unhandled option" |
|
287 |
||
288 |
if len(args) < 1: |
|
289 |
usage() |
|
290 |
sys.exit(2) |
|
291 |
||
292 |
command = args[0] |
|
293 |
||
294 |
if command == "new" or command == "add": |
|
295 |
subject_regex = re.compile("^Subject: (.*)$", re.M | re.I) |
|
296 |
||
297 |
title = " ".join(args[1:]) |
|
298 |
body = sys.stdin.read() |
|
299 |
if subject_regex.search(body): |
|
300 |
subject = subject_regex.findall(body)[0] |
|
301 |
title = title + ": " + subject |
|
302 |
||
303 |
new_task(title, cgi.escape(body)) |
|
304 |
||
305 |
elif command == "list": |
|
306 |
criteria = None |
|
307 |
if len(args)>1: |
|
722.1.14
by Bryce Harrington
Make `gtcli list` take space-separated filters/tags as criteria |
308 |
criteria = ' '.join(args[1:]) |
719
by Bryce Harrington
Initial import of dbus-based command line tool |
309 |
list_tasks(criteria, False) |
310 |
||
311 |
elif command == "count": |
|
312 |
criteria = None |
|
313 |
if len(args)>1: |
|
722.1.14
by Bryce Harrington
Make `gtcli list` take space-separated filters/tags as criteria |
314 |
criteria = ' '.join(args[1:]) |
759.1.2
by Bryce Harrington
Refactor out counting code from list code |
315 |
count_tasks(criteria) |
719
by Bryce Harrington
Initial import of dbus-based command line tool |
316 |
|
759.1.3
by Bryce Harrington
Add 'gtcli summary' which prints out # tasks for 2 weeks |
317 |
elif command == "summary": |
318 |
criteria = None |
|
319 |
if len(args)>1: |
|
320 |
criteria = ' '.join(args[1:]) |
|
321 |
summary_of_tasks(criteria) |
|
322 |
||
719
by Bryce Harrington
Initial import of dbus-based command line tool |
323 |
elif command == "rm" or command == "delete": |
324 |
if len(args)<2: |
|
325 |
usage() |
|
326 |
sys.exit(1) |
|
327 |
for tid in args[1:]: |
|
328 |
delete_task(tid) |
|
329 |
||
330 |
elif command == "close": |
|
331 |
if len(args)<2: |
|
332 |
usage() |
|
333 |
sys.exit(1) |
|
334 |
for tid in args[1:]: |
|
335 |
close_task(tid) |
|
336 |
||
337 |
elif command == "postpone": |
|
338 |
if len(args)<3: |
|
339 |
usage() |
|
340 |
sys.exit(1) |
|
823.1.5
by Bryce Harrington
Add ability to postpone tasks by tag |
341 |
postpone(args[1], args[2]) |
719
by Bryce Harrington
Initial import of dbus-based command line tool |
342 |
|
343 |
elif command == "show": |
|
344 |
if len(args)<2: |
|
345 |
usage() |
|
346 |
sys.exit(1) |
|
347 |
for tid in args[1:]: |
|
348 |
show_task(tid) |
|
349 |
||
350 |
elif command == "edit": |
|
351 |
if len(args)<2: |
|
352 |
usage() |
|
353 |
sys.exit(1) |
|
354 |
for tid in args[1:]: |
|
355 |
open_task_editor(tid) |
|
356 |
||
739.1.1
by Bryce Harrington
Add ability to hide/show the task browser. |
357 |
elif command == "browse": |
744
by Bryce Harrington
Add ability to make task browser iconified |
358 |
state = None |
359 |
if len(args)>1: |
|
360 |
state = args[1] |
|
361 |
toggle_browser_visibility(state) |
|
739.1.1
by Bryce Harrington
Add ability to hide/show the task browser. |
362 |
|
719
by Bryce Harrington
Initial import of dbus-based command line tool |
363 |
else: |
364 |
die("Unknown command '%s'\n" %(command)) |
|
365 |
||
366 |