~ubuntu-branches/ubuntu/oneiric/hamster-applet/oneiric

« back to all changes in this revision

Viewing changes to src/hamster-cli

  • Committer: Bazaar Package Importer
  • Author(s): Krzysztof Klimonda
  • Date: 2010-09-19 08:51:59 UTC
  • mfrom: (1.5.2 upstream)
  • Revision ID: james.westby@ubuntu.com-20100919085159-kac5xse5ccn11ysw
Tags: 2.31.92-0ubuntu1
* New upstream release. (LP: #629495) Fixes bugs:
  - Hamster is always using 1% of the CPU (LP: #620113)
* removed 01_startup-fix.patch and 02_fix_imports.patch
* debian/rules:
  - upstream has switched to waf, modified debian/rules accordingly.
* debian/control.in:
  - bump debhelper dependency to 5.0.51~ for dh_icons
  - bump Standards-Version to 3.9.1, no changes we required
  - add Build-Dependency on gnome-control-center-dev
  - add Dependency on python-wnck
  - changed Homepage
* debian/copyright:
  - Removed licenses for files that were removed from archive
  - Refreshed Copyright Holders list

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
# - coding: utf-8 -
 
3
 
 
4
# Copyright (C) 2010 Matías Ribecky <matias at mribecky.com.ar>
 
5
# Copyright (C) 2010 Toms Bauģis <toms.baugis@gmail.com>
 
6
 
 
7
# This file is part of Project Hamster.
 
8
 
 
9
# Project Hamster is free software: you can redistribute it and/or modify
 
10
# it under the terms of the GNU General Public License as published by
 
11
# the Free Software Foundation, either version 3 of the License, or
 
12
# (at your option) any later version.
 
13
 
 
14
# Project Hamster is distributed in the hope that it will be useful,
 
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
17
# GNU General Public License for more details.
 
18
 
 
19
# You should have received a copy of the GNU General Public License
 
20
# along with Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
 
21
 
 
22
'''A script to control the applet from the command line.'''
 
23
 
 
24
import sys, os
 
25
import optparse
 
26
import re
 
27
import datetime as dt
 
28
 
 
29
from hamster import client, stuff
 
30
 
 
31
class ConfigurationError(Exception):
 
32
    '''An error of configuration.'''
 
33
    pass
 
34
 
 
35
class HamsterClient(object):
 
36
    '''The main application.'''
 
37
 
 
38
    def __init__(self):
 
39
        self.storage = client.Storage()
 
40
 
 
41
    def toggle(self):
 
42
        self.storage.toggle()
 
43
 
 
44
 
 
45
    def start_tracking(self, activity, start_time = None, end_time = None):
 
46
        '''Start a new activity.'''
 
47
        self.storage.add_fact(activity, start_time = start_time, end_time = end_time)
 
48
 
 
49
 
 
50
    def stop_tracking(self):
 
51
        '''Stop tracking the current activity.'''
 
52
        self.storage.stop_tracking()
 
53
 
 
54
 
 
55
 
 
56
    def list(self, start_time = None, end_time = None):
 
57
        '''Print a listing of activities.'''
 
58
 
 
59
        start_time = start_time or dt.datetime.combine(dt.date.today(), dt.time())
 
60
        end_time = end_time or start_time.replace(hour=23, minute=59, second=59)
 
61
 
 
62
 
 
63
        headers = {'activity': _("Activity"),
 
64
                   'category': _("Category"),
 
65
                   'tags': _("Tags"),
 
66
                   'start': _("Start"),
 
67
                   'end': _("End"),
 
68
                   'duration': _("Duration")}
 
69
        line_fmt = ' %*s - %*s (%*s) | %s@%s %s'
 
70
 
 
71
 
 
72
        print_with_date = start_time.date() != start_time.date()
 
73
        if print_with_date:
 
74
            dates_align_width = len('xxxx-xx-xx xx:xx')
 
75
        else:
 
76
            dates_align_width = len('xx:xx')
 
77
 
 
78
        column_width = {'start': max(len(headers['start']), dates_align_width),
 
79
                        'end':   max(len(headers['end']), dates_align_width),
 
80
                        'duration': max(len(headers['duration']), 7)}
 
81
 
 
82
        print line_fmt % (column_width['start'], headers['start'],
 
83
                          column_width['end'], headers['end'],
 
84
                          column_width['duration'], headers['duration'],
 
85
                          headers['activity'],
 
86
                          headers['category'],
 
87
                          headers['tags'])
 
88
        first_column_width = (8 + sum(column_width.values()))
 
89
        second_column_width = 4 + len(headers['activity']) + \
 
90
                              len(headers['category']) + \
 
91
                              len(headers['tags'])
 
92
 
 
93
        print "%s+%s" % ('-' * first_column_width, '-' * second_column_width)
 
94
        for fact in self.storage.get_facts(start_time, end_time, ""):
 
95
            if fact['start_time'] < start_time or fact['start_time'] > end_time:
 
96
                # Hamster returns activities for the whole day, not just the
 
97
                # time range we sent
 
98
                # TODO - why should that be a problem? /toms/
 
99
                continue
 
100
 
 
101
            fact_data = fact_dict(fact, print_with_date)
 
102
            print line_fmt % (column_width['start'], fact_data['start'],
 
103
                              column_width['end'], fact_data['end'],
 
104
                              column_width['duration'], fact_data['duration'],
 
105
                              fact_data['activity'],
 
106
                              fact_data['category'],
 
107
                              fact_data['tags'])
 
108
 
 
109
    def list_activities(self):
 
110
        '''Print the names of all the activities.'''
 
111
        for activity in self.storage.get_activities():
 
112
            print '%s@%s' % (activity['name'].encode('utf8'), activity['category'].encode('utf8'))
 
113
 
 
114
    def list_categories(self):
 
115
        '''Print the names of all the categories.'''
 
116
        for category in self.storage.get_categories():
 
117
            print category['name'].encode('utf8')
 
118
 
 
119
 
 
120
def timestamp_from_datetime(date):
 
121
    '''Convert a local time datetime into an utc timestamp.'''
 
122
    if date:
 
123
        return timegm(date.timetuple())
 
124
    else:
 
125
        return 0
 
126
 
 
127
def datetime_from_timestamp(timestamp):
 
128
    '''Convert an utc timestamp into a local time datetime.'''
 
129
    return dt.datetime.utcfromtimestamp(timestamp)
 
130
 
 
131
def parse_datetime_range(time):
 
132
    '''Parse starting and ending datetime separated by a '-'.'''
 
133
    start_time, remainder = parse_datetime(time)
 
134
 
 
135
    end_time = None
 
136
    if remainder and remainder.startswith("-"):
 
137
        end_time, remainder = parse_datetime(remainder[1:])
 
138
 
 
139
    return start_time, end_time
 
140
 
 
141
_DATETIME_PATTERN = ('^((?P<relative>-)?|'
 
142
                       '(?P<year>\d{4})'
 
143
                       '(-(?P<month>\d{1,2})'
 
144
                        '(-(?P<day>\d{1,2}))?)? )?'
 
145
                     '((?P<hour>\d{1,2}):)?'
 
146
                     '(?P<minute>\d{1,2})'
 
147
                     '(:(?P<second>\d{1,2}))?'
 
148
                     '(?P<rest>\D.+)?$')
 
149
_DATETIME_REGEX = re.compile(_DATETIME_PATTERN)
 
150
 
 
151
def parse_datetime(arg):
 
152
    '''Parse a date and time.'''
 
153
    match = _DATETIME_REGEX.match(arg)
 
154
    if not match:
 
155
        return dt.datetime.now(), arg
 
156
 
 
157
    if match.groupdict()['relative']:
 
158
        hour = int(match.groupdict()['hour'] or 0)
 
159
        minute = int(match.groupdict()['minute'])
 
160
        second = int(match.groupdict()['second'] or 0)
 
161
        time_ago = dt.timedelta(hours=hour,
 
162
                                      minutes=minute,
 
163
                                      seconds=second)
 
164
        rest = (match.groupdict()['rest'] or '').strip()
 
165
        return dt.datetime.now() - time_ago, rest
 
166
    else:
 
167
        date = dt.datetime.now().date()
 
168
        try:
 
169
            if match.groupdict()['year']:
 
170
                date = date.replace(year=int(match.groupdict()['year']))
 
171
                if match.groupdict()['month']:
 
172
                    date = date.replace(month=int(match.groupdict()['month']))
 
173
                    if match.groupdict()['day']:
 
174
                        date = date.replace(day=int(match.groupdict()['day']))
 
175
            time = dt.time(hour=int(match.groupdict()['hour'] or 0),
 
176
                                 minute=int(match.groupdict()['minute']),
 
177
                                 second=int(match.groupdict()['second'] or 0))
 
178
        except ValueError, err:
 
179
            if match.groupdict()['rest']:
 
180
                date_str = arg[:-len(match.groupdict()['rest'])]
 
181
            else:
 
182
                date_str = arg
 
183
 
 
184
            raise ConfigurationError(_("invalid date/time '%s'" % date_str))
 
185
        rest = (match.groupdict()['rest'] or '').strip()
 
186
        return dt.datetime.combine(date, time), rest
 
187
 
 
188
def fact_dict(fact_data, with_date):
 
189
    fact = {}
 
190
    if with_date:
 
191
        fmt = '%Y-%m-%d %H:%M'
 
192
    else:
 
193
        fmt = '%H:%M'
 
194
 
 
195
    fact['start'] = fact_data['start_time'].strftime(fmt)
 
196
    if fact_data['end_time']:
 
197
        fact['end'] = fact_data['end_time'].strftime(fmt)
 
198
    else:
 
199
        end_date = dt.datetime.now()
 
200
        fact['end'] = ''
 
201
 
 
202
    fact['duration'] = stuff.format_duration(fact_data['delta'])
 
203
 
 
204
    fact['activity'] = fact_data['name']
 
205
    fact['category'] = fact_data['category']
 
206
    if fact_data['tags']:
 
207
        fact['tags'] = ' '.join('#%s' % tag for tag in fact_data['tags'])
 
208
    else:
 
209
        fact['tags'] = ''
 
210
    return fact
 
211
 
 
212
 
 
213
if __name__ == '__main__':
 
214
    from hamster import i18n
 
215
    i18n.setup_i18n()
 
216
 
 
217
    usage = _(
 
218
"""Client for controlling the hamster-applet. Usage:
 
219
  %(prog)s start ACTIVITY [START_TIME[-END_TIME]]
 
220
  %(prog)s stop
 
221
  %(prog)s list [START_TIME[-END_TIME]]
 
222
 
 
223
Actions:
 
224
    * start (default): Start tracking an activity.
 
225
    * stop: Stop tracking current activity.
 
226
    * list: List activities.
 
227
    * list-activities: List all the activities names, one per line.
 
228
    * list-categories: List all the categories names, one per line.
 
229
 
 
230
Time formats:
 
231
    * 'YYYY-MM-DD hh:mm:ss': Absolute time. Defaulting to 0 for the time
 
232
            values missing, and current day for date values.
 
233
            E.g. (considering 2010-03-09 16:30:20 as current date, time):
 
234
                2010-03 13:15:40 is 2010-03-09 13:15:40
 
235
                2010-03-09 13:15 is 2010-03-09 13:15:00
 
236
                2010-03-09 13    is 2010-03-09 00:13:00
 
237
                2010-02 13:15:40 is 2010-02-09 13:15:40
 
238
                13:20            is 2010-03-09 13:20:00
 
239
                20               is 2010-03-09 00:20:00
 
240
    * '-hh:mm:ss': Relative time. From the current date and time. Defaulting
 
241
            to 0 for the time values missing, same as in absolute time.
 
242
 
 
243
""")
 
244
 
 
245
 
 
246
    # CLI Structure: ./hamster-cli.py <start|stop|list|...> <conditional_args>
 
247
 
 
248
    if len(sys.argv) < 2:
 
249
        sys.exit(usage % {'prog': sys.argv[0]})
 
250
 
 
251
    command, args = sys.argv[1], sys.argv[2:]
 
252
 
 
253
    if command in ("toggle", "start", "stop", "list", "list-activities", "list-categories"):
 
254
        hamster_client = HamsterClient()
 
255
 
 
256
        if command == 'toggle':
 
257
            hamster_client.toggle()
 
258
 
 
259
        elif command == 'start':
 
260
            if not args: # mandatory is only the activity name
 
261
                sys.exit(usage % {'prog': sys.argv[0]})
 
262
 
 
263
            activity = args[0]
 
264
 
 
265
            start_time, end_time = None, None
 
266
            if len(args) > 1:
 
267
                start_time, end_time = parse_datetime_range(args[1])
 
268
 
 
269
                if start_time > dt.datetime.now() or (end_time and end_time > dt.datetime.now()):
 
270
                    sys.exit("Activity must start and finish before current time")
 
271
 
 
272
            hamster_client.start_tracking(activity, start_time, end_time)
 
273
 
 
274
        elif command == 'stop':
 
275
            hamster_client.stop_tracking()
 
276
 
 
277
        elif command == 'list':
 
278
            start_time, end_time = None, None
 
279
            if args:
 
280
                start_time, end_time = parse_datetime_range(args[0])
 
281
 
 
282
            hamster_client.list(start_time, end_time)
 
283
 
 
284
        elif command == 'list-activities':
 
285
            hamster_client.list_activities()
 
286
 
 
287
        elif command == 'list-categories':
 
288
            hamster_client.list_categories()
 
289
 
 
290
    else:
 
291
        # unknown command - print usage, go home
 
292
        sys.exit(usage % {'prog': sys.argv[0]})