~gtg-contributors/gtg/tracks

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