4
# Copyright (C) 2010 Matías Ribecky <matias at mribecky.com.ar>
5
# Copyright (C) 2010 Toms Bauģis <toms.baugis@gmail.com>
7
# This file is part of Project Hamster.
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.
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.
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/>.
22
'''A script to control the applet from the command line.'''
29
from hamster import client, stuff
31
class ConfigurationError(Exception):
32
'''An error of configuration.'''
35
class HamsterClient(object):
36
'''The main application.'''
39
self.storage = client.Storage()
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)
50
def stop_tracking(self):
51
'''Stop tracking the current activity.'''
52
self.storage.stop_tracking()
56
def list(self, start_time = None, end_time = None):
57
'''Print a listing of activities.'''
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)
63
headers = {'activity': _("Activity"),
64
'category': _("Category"),
68
'duration': _("Duration")}
69
line_fmt = ' %*s - %*s (%*s) | %s@%s %s'
72
print_with_date = start_time.date() != start_time.date()
74
dates_align_width = len('xxxx-xx-xx xx:xx')
76
dates_align_width = len('xx:xx')
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)}
82
print line_fmt % (column_width['start'], headers['start'],
83
column_width['end'], headers['end'],
84
column_width['duration'], headers['duration'],
88
first_column_width = (8 + sum(column_width.values()))
89
second_column_width = 4 + len(headers['activity']) + \
90
len(headers['category']) + \
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
98
# TODO - why should that be a problem? /toms/
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'],
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'))
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')
120
def timestamp_from_datetime(date):
121
'''Convert a local time datetime into an utc timestamp.'''
123
return timegm(date.timetuple())
127
def datetime_from_timestamp(timestamp):
128
'''Convert an utc timestamp into a local time datetime.'''
129
return dt.datetime.utcfromtimestamp(timestamp)
131
def parse_datetime_range(time):
132
'''Parse starting and ending datetime separated by a '-'.'''
133
start_time, remainder = parse_datetime(time)
136
if remainder and remainder.startswith("-"):
137
end_time, remainder = parse_datetime(remainder[1:])
139
return start_time, end_time
141
_DATETIME_PATTERN = ('^((?P<relative>-)?|'
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}))?'
149
_DATETIME_REGEX = re.compile(_DATETIME_PATTERN)
151
def parse_datetime(arg):
152
'''Parse a date and time.'''
153
match = _DATETIME_REGEX.match(arg)
155
return dt.datetime.now(), arg
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,
164
rest = (match.groupdict()['rest'] or '').strip()
165
return dt.datetime.now() - time_ago, rest
167
date = dt.datetime.now().date()
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'])]
184
raise ConfigurationError(_("invalid date/time '%s'" % date_str))
185
rest = (match.groupdict()['rest'] or '').strip()
186
return dt.datetime.combine(date, time), rest
188
def fact_dict(fact_data, with_date):
191
fmt = '%Y-%m-%d %H:%M'
195
fact['start'] = fact_data['start_time'].strftime(fmt)
196
if fact_data['end_time']:
197
fact['end'] = fact_data['end_time'].strftime(fmt)
199
end_date = dt.datetime.now()
202
fact['duration'] = stuff.format_duration(fact_data['delta'])
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'])
213
if __name__ == '__main__':
214
from hamster import i18n
218
"""Client for controlling the hamster-applet. Usage:
219
%(prog)s start ACTIVITY [START_TIME[-END_TIME]]
221
%(prog)s list [START_TIME[-END_TIME]]
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.
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.
246
# CLI Structure: ./hamster-cli.py <start|stop|list|...> <conditional_args>
248
if len(sys.argv) < 2:
249
sys.exit(usage % {'prog': sys.argv[0]})
251
command, args = sys.argv[1], sys.argv[2:]
253
if command in ("toggle", "start", "stop", "list", "list-activities", "list-categories"):
254
hamster_client = HamsterClient()
256
if command == 'toggle':
257
hamster_client.toggle()
259
elif command == 'start':
260
if not args: # mandatory is only the activity name
261
sys.exit(usage % {'prog': sys.argv[0]})
265
start_time, end_time = None, None
267
start_time, end_time = parse_datetime_range(args[1])
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")
272
hamster_client.start_tracking(activity, start_time, end_time)
274
elif command == 'stop':
275
hamster_client.stop_tracking()
277
elif command == 'list':
278
start_time, end_time = None, None
280
start_time, end_time = parse_datetime_range(args[0])
282
hamster_client.list(start_time, end_time)
284
elif command == 'list-activities':
285
hamster_client.list_activities()
287
elif command == 'list-categories':
288
hamster_client.list_categories()
291
# unknown command - print usage, go home
292
sys.exit(usage % {'prog': sys.argv[0]})