~nmu-sscheel/gtg/rework-task-editor

719 by Bryce Harrington
Initial import of dbus-based command line tool
1
#!/usr/bin/env python
2
# -*- coding:utf-8 -*-
1173 by Izidor Matušov
Pylint loves GTCli now :-)
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
""" Command line user interface for manipulating tasks in gtg. """
719 by Bryce Harrington
Initial import of dbus-based command line tool
16
17
import re
18
import sys
19
import os
20
import dbus
21
import cgi
22
import getopt
23
import textwrap
759.1.5 by Bryce Harrington
Adjust logic to try to better filter workable items
24
from datetime import datetime, date, timedelta
1173 by Izidor Matušov
Pylint loves GTCli now :-)
25
26
from GTG import _
719 by Bryce Harrington
Initial import of dbus-based command line tool
27
1152.2.1 by huxuan
Bug 965583
28
MSG_ERROR_TASK_ID_INVALID = '[Error] Task ID Invalid'
29
719 by Bryce Harrington
Initial import of dbus-based command line tool
30
31
def usage():
1173 by Izidor Matušov
Pylint loves GTCli now :-)
32
    """ Print usage info """
33
    spaces = "  %-30s %s\n"
719 by Bryce Harrington
Initial import of dbus-based command line tool
34
35
    text = _("gtcli -- a command line interface to gtg\n")
36
    text += "\n"
37
38
    text += _("Options:\n")
1173 by Izidor Matušov
Pylint loves GTCli now :-)
39
    text += spaces % ("-h, --help", _("This help"))
719 by Bryce Harrington
Initial import of dbus-based command line tool
40
    text += "\n"
41
42
    text += _("Basic commands:\n")
1173 by Izidor Matušov
Pylint loves GTCli now :-)
43
    text += spaces % ("gtcli new", _("Create a new task"))
44
    text += spaces % ("gtcli show <tid>",
45
        _("Display detailed information on given task id"))
46
    text += spaces % ("gtcli edit <tid>",
47
        _("Opens the GUI editor for the given task id"))
48
    text += spaces % ("gtcli delete <tid>",
49
        _("Removes task identified by tid"))
50
    text += spaces % ("gtcli list [all|today|<filter>|<tag>]...",
51
        _("List tasks"))
52
    text += spaces % ("gtcli search <expression>", _("Search tasks"))
53
    text += spaces % ("gtcli count [all|today|<filter>|<tag>]...",
54
        _("Number of tasks"))
55
    text += spaces % ("gtcli summary [all|today|<filter>|<tag>]...",
56
        _("Report how many tasks starting/due each day"))
57
    text += spaces % ("gtcli postpone <tid> <date>",
58
        _("Updates the start date of task"))
59
    text += spaces % ("gtcli close <tid>",
60
        _("Sets state of task identified by tid to closed"))
61
    text += spaces % ("gtcli browser [hide|show]",
62
        _("Hides or shows the task browser window"))
719 by Bryce Harrington
Initial import of dbus-based command line tool
63
64
    text += "\n"
65
    text += "http://gtg.fritalk.com/\n"
1173 by Izidor Matušov
Pylint loves GTCli now :-)
66
    sys.stderr.write(text)
719 by Bryce Harrington
Initial import of dbus-based command line tool
67
68
69
def connect_to_gtg():
1173 by Izidor Matušov
Pylint loves GTCli now :-)
70
    """ Connect and return GTG DBus interface.
71
72
    This function handles possible errors while connecting to GTG """
719 by Bryce Harrington
Initial import of dbus-based command line tool
73
    try:
74
        bus = dbus.SessionBus()
1173 by Izidor Matušov
Pylint loves GTCli now :-)
75
    except dbus.exceptions.DBusException, err:
76
        if "X11 initialization failed" in err.get_dbus_message():
719 by Bryce Harrington
Initial import of dbus-based command line tool
77
            os.environ['DISPLAY'] = ":0"
78
            bus = dbus.SessionBus()
79
        else:
1173 by Izidor Matušov
Pylint loves GTCli now :-)
80
            print "dbus exception: '%s'" % err
719 by Bryce Harrington
Initial import of dbus-based command line tool
81
            raise
82
1173 by Izidor Matušov
Pylint loves GTCli now :-)
83
    proxy = bus.get_object("org.gnome.GTG", "/org/gnome/GTG")
84
    return dbus.Interface(proxy, "org.gnome.GTG")
85
86
87
def new_task(title):
88
    """ Create a new task with given title
89
90
    Body of the new task is read from stdin. If it contains Subject:,
91
    add it to the title.
92
    (It is handy, when forwarding e-mail to this script). """
93
94
    subject_regex = re.compile("^Subject: (.*)$", re.M | re.I)
95
    body = sys.stdin.read()
96
    if subject_regex.search(body):
97
        subject = subject_regex.findall(body)[0]
98
        title = title + ": " + subject
99
100
    gtg = connect_to_gtg()
101
    gtg.NewTask("Active", title, '', '', '', [], cgi.escape(body), [])
102
103
104
def delete_tasks(task_ids):
105
    """ Delete tasks from GTG """
106
    gtg = connect_to_gtg()
107
    for task_id in task_ids.split():
108
        gtg.DeleteTask(task_id)
109
110
111
def close_tasks(task_ids):
112
    """ Marks tasks as closed """
113
    gtg = connect_to_gtg()
114
    for task_id in task_ids.split():
115
        task = gtg.GetTask(task_id)
116
        if task:
117
            task['status'] = "Done"
118
            gtg.ModifyTask(task_id, task)
119
        else:
120
            print MSG_ERROR_TASK_ID_INVALID
121
            sys.exit(1)
122
123
124
def show_tasks(task_ids):
125
    """ Displays information about tasks """
126
    gtg = connect_to_gtg()
127
    for task_id in task_ids.split():
128
        task = gtg.GetTask(task_id)
129
        if task:
130
            content_regex = re.compile(r"<content>(.+)</content>", re.DOTALL)
131
132
            content = task['text'] + "\n(unknown)"
133
            decoration = content_regex.match(task['text'])
134
            if decoration:
135
                content = decoration.group(1)
136
137
            print task['title']
138
            if len(task['tags'])>0:
139
                print " %-12s %s" % ('tags:', task['tags'][0])
140
            for k in ['id', 'startdate', 'duedate', 'status']:
141
                print " %-12s %s" % (k + ":", task[k])
142
            if len(task['parents'])>0:
143
                print " %-12s %s" % ('parents:', task['parents'][0])
144
            print
145
            print content
146
            print
147
        else:
148
            print MSG_ERROR_TASK_ID_INVALID
149
            sys.exit(1)
150
151
152
def postpone(args):
719 by Bryce Harrington
Initial import of dbus-based command line tool
153
    """ Change the start date of a task """
1173 by Izidor Matušov
Pylint loves GTCli now :-)
154
    gtg = connect_to_gtg()
155
    identifier, startdate = args.split()[:2]
156
823.1.5 by Bryce Harrington
Add ability to postpone tasks by tag
157
    tasks = []
158
    if identifier[0] == '@':
159
        filters = _criteria_to_filters(identifier)
1173 by Izidor Matušov
Pylint loves GTCli now :-)
160
        filters.extend(['active', 'workview'])
161
        gtg = connect_to_gtg()
162
        tasks = gtg.GetTasksFiltered(filters)
823.1.5 by Bryce Harrington
Add ability to postpone tasks by tag
163
    else:
1173 by Izidor Matušov
Pylint loves GTCli now :-)
164
        tasks = [gtg.GetTask(identifier)]
823.1.5 by Bryce Harrington
Add ability to postpone tasks by tag
165
166
    for task in tasks:
1152.2.1 by huxuan
Bug 965583
167
        if task:
168
            task['startdate'] = startdate
1173 by Izidor Matušov
Pylint loves GTCli now :-)
169
            tags = ", ".join(task['tags'])
170
            print "%-12s %-20s %s" % (task['id'], tags, task['title'])
171
            gtg.ModifyTask(task['id'], task)
1152.2.1 by huxuan
Bug 965583
172
        else:
173
            print MSG_ERROR_TASK_ID_INVALID
1152.2.2 by huxuan
gtcli:
174
            sys.exit(1)
719 by Bryce Harrington
Initial import of dbus-based command line tool
175
1173 by Izidor Matušov
Pylint loves GTCli now :-)
176
177
def edit_tasks(task_ids):
178
    """ Open tasks in the task editor GUI """
179
    gtg = connect_to_gtg()
180
    for task in task_ids.split():
181
        gtg.OpenTaskEditor(task)
182
719 by Bryce Harrington
Initial import of dbus-based command line tool
183
739.1.1 by Bryce Harrington
Add ability to hide/show the task browser.
184
def toggle_browser_visibility(state):
722.1.15 by Bryce Harrington
Update documentation for gtcli list
185
    """ Cause the task browser to be displayed """
1173 by Izidor Matušov
Pylint loves GTCli now :-)
186
    gtg = connect_to_gtg()
739.1.1 by Bryce Harrington
Add ability to hide/show the task browser.
187
    if state == "hide":
1173 by Izidor Matušov
Pylint loves GTCli now :-)
188
        gtg.HideTaskBrowser()
189
    elif state in ["minimize", "iconify"]:
190
        if not gtg.IsTaskBrowserVisible():
191
            gtg.ShowTaskBrowser()
192
        gtg.IconifyTaskBrowser()
744 by Bryce Harrington
Add ability to make task browser iconified
193
    else:
1173 by Izidor Matušov
Pylint loves GTCli now :-)
194
        gtg.ShowTaskBrowser()
195
739.1.1 by Bryce Harrington
Add ability to hide/show the task browser.
196
759.1.2 by Bryce Harrington
Refactor out counting code from list code
197
def _criteria_to_filters(criteria):
1173 by Izidor Matušov
Pylint loves GTCli now :-)
198
    """ Convert user input for filtering into GTG filters """
199
    criteria = criteria
200
    if criteria in ['', 'all']:
759.1.2 by Bryce Harrington
Refactor out counting code from list code
201
        filters = ['active']
202
    else:
1173 by Izidor Matušov
Pylint loves GTCli now :-)
203
        filters = criteria.split()
204
759.1.2 by Bryce Harrington
Refactor out counting code from list code
205
    # Special case 'today' filter
206
    if 'today' in filters:
207
        filters.extend(['active', 'workview'])
208
        filters.remove('today')
1173 by Izidor Matušov
Pylint loves GTCli now :-)
209
759.1.2 by Bryce Harrington
Refactor out counting code from list code
210
    return filters
211
1173 by Izidor Matušov
Pylint loves GTCli now :-)
212
759.1.2 by Bryce Harrington
Refactor out counting code from list code
213
def count_tasks(criteria):
214
    """ Print a simple count of tasks matching criteria """
215
    filters = _criteria_to_filters(criteria)
1173 by Izidor Matušov
Pylint loves GTCli now :-)
216
    gtg = connect_to_gtg()
217
    tasks = gtg.GetTasksFiltered(filters)
759.1.2 by Bryce Harrington
Refactor out counting code from list code
218
219
    total = 0
220
    for task in tasks:
221
        if 'title' not in task:
222
            continue
223
        total += 1
224
225
    print total
226
    return total
227
1173 by Izidor Matušov
Pylint loves GTCli now :-)
228
759.1.3 by Bryce Harrington
Add 'gtcli summary' which prints out # tasks for 2 weeks
229
def summary_of_tasks(criteria):
230
    """ Print report showing number of tasks starting and due each day """
1173 by Izidor Matušov
Pylint loves GTCli now :-)
231
    if criteria in ['', 'all']:
791 by Bryce Harrington
I think by definition summary only shows counts for active tasks, so
232
        criteria = 'workable'
759.1.5 by Bryce Harrington
Adjust logic to try to better filter workable items
233
759.1.3 by Bryce Harrington
Add 'gtcli summary' which prints out # tasks for 2 weeks
234
    filters = _criteria_to_filters(criteria)
791 by Bryce Harrington
I think by definition summary only shows counts for active tasks, so
235
    filters.append('active')
1173 by Izidor Matušov
Pylint loves GTCli now :-)
236
    gtg = connect_to_gtg()
237
    tasks = gtg.GetTasksFiltered(filters)
759.1.3 by Bryce Harrington
Add 'gtcli summary' which prints out # tasks for 2 weeks
238
1173 by Izidor Matušov
Pylint loves GTCli now :-)
239
    report = {}
240
    for task in tasks:
241
        if not task['startdate']:
759.1.8 by Bryce Harrington
s/someday/unscheduled/
242
            startdate = 'unscheduled'
759.1.5 by Bryce Harrington
Adjust logic to try to better filter workable items
243
        else:
1173 by Izidor Matušov
Pylint loves GTCli now :-)
244
            startdate = task['startdate']
245
            if datetime.strptime(startdate, "%Y-%m-%d") < datetime.today():
246
                startdate = date.today().strftime("%Y-%m-%d")
247
759.1.5 by Bryce Harrington
Adjust logic to try to better filter workable items
248
759.1.3 by Bryce Harrington
Add 'gtcli summary' which prints out # tasks for 2 weeks
249
        if startdate not in report:
1173 by Izidor Matušov
Pylint loves GTCli now :-)
250
            report[startdate] = {'starting': 0, 'due': 0}
759.1.3 by Bryce Harrington
Add 'gtcli summary' which prints out # tasks for 2 weeks
251
        report[startdate]['starting'] += 1
252
1173 by Izidor Matušov
Pylint loves GTCli now :-)
253
        duedate = task['duedate'] or 'never'
759.1.3 by Bryce Harrington
Add 'gtcli summary' which prints out # tasks for 2 weeks
254
        if duedate not in report:
1173 by Izidor Matušov
Pylint loves GTCli now :-)
255
            report[duedate] = {'starting': 0, 'due': 0}
759.1.3 by Bryce Harrington
Add 'gtcli summary' which prints out # tasks for 2 weeks
256
        report[duedate]['due'] += 1
257
1173 by Izidor Matušov
Pylint loves GTCli now :-)
258
    print "%-20s %5s %5s" % ("", "Start", "Due")
259
    if 'unscheduled' in report:
260
        print "%-20s %5d %5d" % ('unscheduled',
823.1.1 by Bryce Harrington
Don't print unscheduled tasks if there aren't any
261
                                report['unscheduled']['starting'],
262
                                report['unscheduled']['due'])
798 by Bryce Harrington
Fix off by one to show three complete weeks of data
263
    num_days = 22
866 by Bryce Harrington
Be more consistent with date formatting
264
    fmt = "%a  %-m-%-d"
1173 by Izidor Matušov
Pylint loves GTCli now :-)
265
    if 'today' in criteria:
759.1.7 by Bryce Harrington
Make 'status today' show only the tasks for the current work day
266
        num_days = 1
267
    for i in range(0, num_days):
1173 by Izidor Matušov
Pylint loves GTCli now :-)
268
        day = date.today() + timedelta(i)
269
        sday = str(day)
270
        if sday in report:
271
            print "%-20s %5d %5d" % (day.strftime(fmt),
272
                                    report[sday]['starting'],
273
                                    report[sday]['due'])
759.1.3 by Bryce Harrington
Add 'gtcli summary' which prints out # tasks for 2 weeks
274
        else:
1173 by Izidor Matušov
Pylint loves GTCli now :-)
275
            print "%-20s %5d %5d" % (day.strftime(fmt), 0, 0)
276
277
278
def list_tasks(criteria):
279
    """ Display a listing of tasks
280
722.1.15 by Bryce Harrington
Update documentation for gtcli list
281
    Accepts any filter or combination of filters or tags to limit the
282
    set of tasks shown.  If multiple tags specified, it lists only tasks
283
    that have all the tags.  If no filters or tags are specified,
284
    defaults to showing all active tasks.
285
    """
759.1.2 by Bryce Harrington
Refactor out counting code from list code
286
287
    filters = _criteria_to_filters(criteria)
1173 by Izidor Matušov
Pylint loves GTCli now :-)
288
    gtg = connect_to_gtg()
289
    tasks = gtg.GetTasksFiltered(filters)
719 by Bryce Harrington
Initial import of dbus-based command line tool
290
1173 by Izidor Matušov
Pylint loves GTCli now :-)
291
    tasks_tree = {}
719 by Bryce Harrington
Initial import of dbus-based command line tool
292
    notag = '@__notag'
293
    for task in tasks:
294
        if 'title' not in task:
724 by Bryce Harrington
Replace broken dbus calls with new ones using filters
295
            continue
719 by Bryce Harrington
Initial import of dbus-based command line tool
296
        if not task['tags'] or len(task['tags']) == 0:
297
            if notag not in tasks_tree:
298
                tasks_tree[notag] = []
299
            tasks_tree[notag].append(task)
300
        else:
301
            tags = []
302
            for tag in list(task['tags']):
940 by Lionel Dricot
fixing #655514, thanks Thibault Févry for the fix
303
                tags.append(tag)
719 by Bryce Harrington
Initial import of dbus-based command line tool
304
                if tag not in tasks_tree:
305
                    tasks_tree[tag] = []
306
                tasks_tree[tag].append(task)
307
722.1.15 by Bryce Harrington
Update documentation for gtcli list
308
    # If any tags were specified, use only those as the categories
1173 by Izidor Matušov
Pylint loves GTCli now :-)
309
    keys = [fname for fname in filters if fname.startswith('@')]
722.1.14 by Bryce Harrington
Make `gtcli list` take space-separated filters/tags as criteria
310
    if not keys:
722.1.10 by Bryce Harrington
Search by multiple tags
311
        keys = tasks_tree.keys()
312
        keys.sort()
722.1.14 by Bryce Harrington
Make `gtcli list` take space-separated filters/tags as criteria
313
719 by Bryce Harrington
Initial import of dbus-based command line tool
314
    for key in keys:
722.1.16 by Bryce Harrington
Ignore non-existant tags that were asked as filters
315
        if key not in tasks_tree:
316
            continue
719 by Bryce Harrington
Initial import of dbus-based command line tool
317
        if key != notag:
1173 by Izidor Matušov
Pylint loves GTCli now :-)
318
            print "%s:" % (key[1:])
719 by Bryce Harrington
Initial import of dbus-based command line tool
319
        for task in tasks_tree[key]:
1173 by Izidor Matušov
Pylint loves GTCli now :-)
320
            text = textwrap.fill(task['title'],
823.1.5 by Bryce Harrington
Add ability to postpone tasks by tag
321
                                 initial_indent='',
1038.1.3 by Izidor Matušov
Replace the long white-space string by a python expression
322
                                 subsequent_indent=' ' * 40)
1173 by Izidor Matušov
Pylint loves GTCli now :-)
323
            print "  %-36s  %s" % (task['id'], text)
324
325
326
def search_tasks(expression):
327
    """ Search Tasks according to expression"""
328
    gtg = connect_to_gtg()
329
    tasks = gtg.SearchTasks(expression)
1152.2.3 by huxuan
gtcli:
330
    for task in tasks:
1173 by Izidor Matušov
Pylint loves GTCli now :-)
331
        text = textwrap.fill(task['title'],
1152.2.5 by huxuan
dbuswrapper & gtcli:
332
                             initial_indent='',
333
                             subsequent_indent=' ' * 40)
1173 by Izidor Matušov
Pylint loves GTCli now :-)
334
        print "  %-36s  %s" % (task['id'], text)
335
336
337
def run_command(args):
338
    """ Run command and check for its minimal required arguments """
339
340
    def minimal_args(count):
341
        """ Check the minimal required arguments """
342
        if len(args) < count + 1:
343
            usage()
344
            sys.exit(1)
345
346
    minimal_args(0)
347
    commands = [
348
      (("new", "add"), 0, new_task),
349
      (("list"), 0, list_tasks),
350
      (("count"), 0, count_tasks),
351
      (("summary"), 0, summary_of_tasks),
352
      (("rm", "delete"), 1, delete_tasks),
353
      (("close"), 1, close_tasks),
354
      (("postpone"), 2, postpone),
355
      (("show"), 1, show_tasks),
356
      (("edit"), 1, edit_tasks),
357
      (("browser"), 0, toggle_browser_visibility),
358
      (("search"), 1, search_tasks),
359
    ]
360
361
    for aliases, min_args, command in commands:
362
        if args[0] in aliases:
363
            minimal_args(min_args)
364
            criteria = " ".join(args[1:]).strip()
365
            return command(criteria)
366
367
    sys.stderr.write("Unknown command '%s'\n" % args[0])
368
    usage()
369
    sys.exit(1)
370
371
372
def main():
373
    """ Parse arguments and launch command """
719 by Bryce Harrington
Initial import of dbus-based command line tool
374
    try:
375
        opts, args = getopt.gnu_getopt(sys.argv[1:], "h", ["help"])
376
    except getopt.GetoptError, err:
377
        sys.stderr.write("Error: " + str(err) + "\n\n")
378
        usage()
379
        sys.exit(2)
1173 by Izidor Matušov
Pylint loves GTCli now :-)
380
    for opt, arg in opts:
381
        if opt in ("-h", "--help"):
719 by Bryce Harrington
Initial import of dbus-based command line tool
382
            usage()
383
            sys.exit(0)
384
        else:
1173 by Izidor Matušov
Pylint loves GTCli now :-)
385
            assert False, "unhandled option %s=%s" % (opt, arg)
386
387
    run_command(args)
388
389
390
if __name__ == '__main__':
391
    main()