diff --git src/Makefile.am src/Makefile.am
index 3d12240..4da980c 100644
--- src/Makefile.am
+++ src/Makefile.am
@@ -7,6 +7,11 @@ CPPFLAGS = \
 hamster-applet: hamster-applet.py
 	$(AM_V_GEN)sed -e "s|\@PYTHONDIR\@|$(pyexecdir)|" $< > $@
 
+hamster-client: hamster-client.py
+	$(AM_V_GEN)sed -e "s|__DEBUG__ = True|__DEBUG__ = False|" \
+				   -e "s|\@PYTHONDIR\@|$(pyexecdir)|" \
+				   $< > $@
+
 hamsterlibdir = $(libdir)/hamster-applet
 hamsterlib_SCRIPTS = hamster-applet
 
@@ -14,10 +19,12 @@ hamsterlib_SCRIPTS = hamster-applet
 hamsterbindir = $(bindir)
 hamsterbin_SCRIPTS = \
 	hamster-standalone \
-	gnome-time-tracker
+	gnome-time-tracker \
+	hamster-client
 
 BUILT_SOURCES = \
-	hamster-applet
+	hamster-applet \
+	hamster-client
 
 CLEANFILES = \
 	$(BUILT_SOURCES)
@@ -28,4 +35,5 @@ DISTCLEANFILES = \
 EXTRA_DIST = \
 	hamster-applet.py \
 	hamster-standalone \
-	gnome-time-tracker
+	gnome-time-tracker \
+	hamster-client.py
diff --git src/hamster-client.py src/hamster-client.py
new file mode 100755
index 0000000..04749f7
--- /dev/null
+++ src/hamster-client.py
@@ -0,0 +1,454 @@
+#!/usr/bin/env python
+# - coding: utf-8 -
+
+# Copyright (C) 2010 Matías Ribecky <matias at mribecky.com.ar>
+
+# This file is part of Project Hamster.
+
+# Project Hamster is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# Project Hamster is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
+
+"""usage: %prog start [START_TIME[-END_TIME]] ACTIVITY
+       %prog stop
+       %prog list [START_TIME[-END_TIME]]
+
+Client for controlling the hamster-applet.
+
+Actions:
+    * start (default): Start tracking an activity.
+    * stop: Stop tracking current activity.
+    * list: List activities.
+    * list-activities: List all the activities names, one per line.
+    * list-categories: List all the categories names, one per line.
+
+Times formats:
+    * 'YYYY-MM-DD hh:mm:ss': Absolute time. Defaulting to 0 for the time
+            values missing, and current day for date values.
+            E.g. (considering 2010-03-09 16:30:20 as current date, time):
+                2010-03 13:15:40 is 2010-03-09 13:15:40
+                2010-03-09 13:15 is 2010-03-09 13:15:00
+                2010-03-09 13    is 2010-03-09 00:13:00
+                2010-02 13:15:40 is 2010-02-09 13:15:40
+                13:20            is 2010-03-09 13:20:00
+                20               is 2010-03-09 00:20:00
+    * '-hh:mm:ss': Relative time. From the current date and time. Defaulting
+            to 0 for the time values missing, same as in absolute time.
+"""
+
+import re
+import sys
+import time
+import logging
+import optparse
+import datetime
+
+import dbus
+import dbus.mainloop.glib
+
+HAMSTER_SERVICE = 'org.gnome.Hamster'
+HAMSTER_PATH = '/org/gnome/Hamster'
+HAMSTER_INTERFACE = 'org.gnome.Hamster'
+
+__DEBUG__ = True
+
+class ConfigurationError(Exception):
+    '''An error of configuration.'''
+    pass
+
+def main(argv):
+    '''Main application body.'''
+    try:
+        hamster, action, arg = configure(argv)
+    except ConfigurationError:
+        return 1
+
+    return FUNCTION_FOR_ACTION[action](hamster, arg)
+
+def configure(argv):
+    '''Configure the application.'''
+
+    if __DEBUG__:
+        logging.basicConfig(level=logging.DEBUG)
+
+    sys.exceptionhook = log_exception
+
+    action, arg = parse_arguments(argv)
+
+    hamster = connect()
+
+    return hamster, action, arg
+
+def log_exception(exc_type, exc, traceback):
+    '''Log the exception and exit application.'''
+    logging.exception("Unexpected error")
+    sys.exit(1)
+
+def parse_arguments(argv):
+    '''Parse the arguments to the program.'''
+    parser = create_argument_parser()
+
+    logging.debug("Command line arguments: %s", argv)
+    argv = list(escape_relative_minutes(argv))
+    opts, args = parser.parse_args(argv[1:])
+    args = list(unescape_relative_minutes(args))
+    logging.debug("Parsed options: %s", opts)
+    logging.debug("Parsed arguments: %s", args)
+
+    if args[0] in FUNCTION_FOR_ACTION.keys():
+        action = args.pop(0)
+    else:
+        action = 'start'
+
+    arg = ' '.join(args)
+
+    return action, arg
+
+def create_argument_parser():
+    '''Build an argument parser with all the options required.'''
+    parser = optparse.OptionParser(usage=__doc__)
+    return parser
+
+def escape_relative_minutes(args):
+    '''Add a \\ in front of relative times arguments (e.g. -15)'''
+    yield args[0]
+    for arg in args[1:]:
+        if arg[1].isdigit() and arg.startswith('-'):
+            yield '\\' + arg
+        else:
+            yield arg
+
+def unescape_relative_minutes(args):
+    '''Remove the \\ in front of relative times arguments (e.g. \\-15)'''
+    for arg in args:
+        if arg.startswith('\\-') and arg[2].isdigit():
+            yield arg[1:]
+        else:
+            yield arg
+
+def connect():
+    '''Connect to hamster via DBus.'''
+    try:
+        logging.info("Conecting to DBus server")
+        connection = dbus.SessionBus()
+        logging.info("Getting hamster object from DBus server")
+        hamster = dbus.Interface(connection.get_object(HAMSTER_SERVICE,
+                                                       HAMSTER_PATH),
+                                 dbus_interface=HAMSTER_INTERFACE)
+        return hamster
+    except dbus.DBusException:
+        logging.exception("Can't init dbus")
+        raise ConfigurationError()
+
+def start_action(hamster, arg):
+    '''Start a new activity.'''
+    start_date, end_date, arg = parse_datetime_range(arg)
+    activity = arg
+
+    if start_date > now() or (end_date and end_date > now()):
+        print >>sys.stderr, \
+              "error: cannot start/end an activity in the future."
+        return 1
+
+    start_stamp = timestamp_from_datetime(start_date)
+    if end_date:
+        end_stamp = timestamp_from_datetime(end_date)
+    else:
+        end_stamp = 0
+
+    logging.debug("Creating fact '%s' from %s to %s",
+                  activity,
+                  start_stamp,
+                  end_stamp)
+    if not __DEBUG__:
+        hamster.AddFact(activity,
+                        dbus.UInt32(start_stamp),
+                        dbus.UInt32(end_stamp))
+    return 0
+
+def now():
+    '''Return the current datetime.'''
+    return datetime.datetime.now().replace(microsecond=0)
+
+def timestamp_from_datetime(date):
+    tz_offset =  datetime.timedelta(seconds=time.timezone)
+    return time.mktime((date - tz_offset).timetuple())
+
+def datetime_from_timestamp(timestamp):
+    return datetime.datetime.utcfromtimestamp(timestamp)
+
+def parse_datetime_range(arg):
+    '''Parse starting and ending datetime separated by a '-'.'''
+    start_date, arg = parse_datetime(arg)
+    if arg and arg[0] == '-':
+        arg = arg[1:]
+        end_date, arg = parse_datetime(arg)
+    else:
+        end_date = None
+    return start_date, end_date, arg
+
+_DATETIME_PATTERN = ('^((?P<relative>-)?|'
+                       '(?P<year>\d{4})'
+                       '(-(?P<month>\d{1,2})'
+                        '(-(?P<day>\d{1,2}))?)? )?'
+                     '((?P<hour>\d{1,2}):)?'
+                     '(?P<minute>\d{1,2})'
+                     '(:(?P<second>\d{1,2}))?'
+                     '(?P<rest>\D.+)?$')
+_DATETIME_REGEX = re.compile(_DATETIME_PATTERN)
+def parse_datetime(arg):
+    '''Parse a date and time.'''
+    match = _DATETIME_REGEX.match(arg)
+    if not match:
+        return now(), arg
+
+    if match.groupdict()['relative']:
+        hour = int(match.groupdict()['hour'] or 0)
+        minute = int(match.groupdict()['minute'])
+        second = int(match.groupdict()['second'] or 0)
+        time_ago = datetime.timedelta(hours=hour,
+                                      minutes=minute,
+                                      seconds=second)
+        rest = (match.groupdict()['rest'] or '').strip()
+        return now() - time_ago, rest
+    else:
+        date = now().date()
+        if match.groupdict()['year']:
+            date = date.replace(year=int(match.groupdict()['year']))
+            if match.groupdict()['month']:
+                date = date.replace(month=int(match.groupdict()['month']))
+                if match.groupdict()['day']:
+                    date = date.replace(day=int(match.groupdict()['day']))
+        time = datetime.time(hour=int(match.groupdict()['hour'] or 0),
+                             minute=int(match.groupdict()['minute']),
+                             second=int(match.groupdict()['second'] or 0))
+        rest = (match.groupdict()['rest'] or '').strip()
+        return datetime.datetime.combine(date, time), rest
+
+def stop_action(hamster, arg):
+    '''Stop tracking the current activity.'''
+    if arg:
+        # There is no clean way to implement this right now, so not using it.
+        end_date, arg = parse_datetime(arg)
+        end_stamp = timestamp_from_datetime(end_date)
+    else:
+        end_stamp = 0
+
+    logging.debug("Stoping tracking current activity")
+    if not __DEBUG__:
+        hamster.StopTracking()
+    return 0
+
+def list_action(hamster, arg):
+    '''Print a listing of activities.'''
+    if arg:
+        start_date, end_date, arg = parse_datetime_range(arg)
+    else:
+        start_date = datetime.datetime.combine(datetime.datetime.today(),
+                                               datetime.time())
+        end_date = None
+
+    if not end_date:
+        end_date = datetime.datetime(year=start_date.year,
+                                     month=start_date.month,
+                                     day=start_date.day,
+                                     hour=23,
+                                     minute=59,
+                                     second=59)
+
+    logging.debug("Listing activities from '%s' to '%s'",
+                  start_date.isoformat(' '),
+                  end_date.isoformat(' '))
+
+    start_stamp = timestamp_from_datetime(start_date)
+    end_stamp = timestamp_from_datetime(end_date)
+    logging.debug("Time stamps %s-%s", start_stamp, end_stamp)
+
+    print_listing_header(not same_day(start_date, end_date))
+
+    for fact in hamster.GetFacts(start_stamp, end_stamp):
+        fact_start_date = datetime_from_timestamp(fact['start_time'])
+        if fact['end_time']:
+            fact_end_date = datetime_from_timestamp(fact['end_time'])
+        else:
+            fact_end_date = None
+
+        if not (start_date < fact_start_date < end_date):
+            continue
+
+        start_string, end_string = format_datetimes(fact_start_date,
+                                                    fact_end_date,
+                                                    not same_day(start_date,
+                                                                 end_date))
+        spent_string = format_spent_time(fact_start_date, fact_end_date)
+
+        print ' %s - %s | %s %s' % (start_string,
+                                    end_string,
+                                    ('%s@%s' % (fact['name'],
+                                                fact['category'])).ljust(27),
+                                    spent_string)
+    return 0
+
+def same_day(lhs, rhs):
+    '''Wether two datetimes have the same day.'''
+    return lhs.date() == rhs.date()
+
+def print_listing_header(with_dates):
+    '''Print the header for the listing.'''
+    if with_dates:
+        start_label = "Start".rjust(16)
+        end_label = "End".ljust(16)
+        activity_label = "Activity".ljust(27)
+        elapsed_label = "Elapsed time"
+    else:
+        start_label = "Start".rjust(5)
+        end_label = "End".ljust(5)
+        activity_label = "Activity".ljust(27)
+        elapsed_label = "Elapsed time"
+
+    first_line= ' %s - %s | %s %s' % (start_label,
+                                      end_label,
+                                      activity_label,
+                                      elapsed_label)
+
+    # position of the '+'
+    joint_position = 5 + len(start_label) + len(end_label)
+
+    print first_line
+    print '-' * joint_position + '+' + '-' * (len(first_line) - joint_position)
+
+def format_datetimes(start_date, end_date, with_date):
+    '''Format the start and end dates of an activity.'''
+    if with_date:
+        fmt = '%Y-%m-%d %H:%M'
+    else:
+        fmt = '%H:%M'
+    start_string = start_date.strftime(fmt)
+    if end_date:
+        end_string = end_date.strftime(fmt)
+    else:
+        if with_date:
+            end_string = '                '
+        else:
+            end_string = '     '
+    return start_string, end_string
+
+def format_spent_time(start_date, end_date):
+    '''Format the spent time in an activity.'''
+    if end_date:
+        spent_time = (end_date - start_date)
+    else:
+        spent_time = (datetime.datetime.now() - start_date)
+    spent_string = '%smin' % ((spent_time.seconds % (60 * 60)) / 60)
+    if spent_time.seconds > (60 * 60):
+        spent_string = ('%sh ' + spent_string) % \
+                         (spent_time.seconds // (60 * 60))
+    return spent_string
+
+def list_activities_action(hamster, arg):
+    '''Print the names of all the activities.'''
+    for activity, category in hamster.GetActivities():
+        print '%s@%s' % (activity.encode('utf8'), category.encode('utf8'))
+    return 0
+
+def list_categories_action(hamster, arg):
+    '''Print the names of all the categories.'''
+    for category in hamster.GetCategories():
+        print category.encode('utf8')
+    return 0
+
+if __DEBUG__:
+    import unittest
+
+    class TestDateParsing(unittest.TestCase):
+        '''Tests for the function parse_datetime.'''
+
+        now_test_date = datetime.datetime(2010, 3, 14, 11, 32, 25)
+        test_data = {'2009-01-25 16:11:53': (2009, 1, 25, 16, 11, 53),
+                     '2009-01-25 16:11': (2009, 1, 25, 16, 11, 0),
+                     '2009-01-25 11': (2009, 1, 25, 0, 11, 0),
+# TODO: make a better parser without regex to inclued this two cases
+#                      '2009-01': (2009, 1, 14, 0, 0, 0),
+#                      '2009': (2009, 3, 14, 0, 0, 0),
+                     '11': (2010, 3, 14, 0, 11, 0),
+                     '16:11': (2010, 3, 14, 16, 11, 0),
+                     '16:11:53': (2010, 3, 14, 16, 11, 53),
+                     '2009-01 11': (2009, 1, 14, 0, 11, 0),
+                     '2009 11': (2009, 3, 14, 0, 11, 0),
+                     '-3:21:16': (2010, 3, 14, 8, 11, 9),
+                     '-3:21': (2010, 3, 14, 8, 11, 25),
+                     '-21': (2010, 3, 14, 11, 11, 25),
+                     '-3:21:30': (2010, 3, 14, 8, 10, 55),
+                     '-16:21:16': (2010, 3, 13, 19, 11, 9)}
+
+        def now(self):
+            '''Replacement for the global `now`.'''
+            return self.now_test_date
+
+        def setUp(self):
+            '''Replaces the global `now` function.'''
+            global now
+            self.orig_now = now
+            now = self.now
+
+        def tearDown(self):
+            '''Restores the global `now` function.'''
+            global now
+            now = self.orig_now
+
+        def test_parsing(self):
+            '''Run the test data.'''
+            test_rest = ' test rest'
+            for test_value, test_result in self.test_data.items():
+                real_result, rest = parse_datetime(test_value)
+                real_result2, rest2 = parse_datetime(test_value + test_rest)
+
+                self.assertEqual(real_result, real_result2,
+                                 'testing "%s", result1=%s result2=%s' %
+                                  (test_value, real_result.isoformat(' '),
+                                   real_result2.isoformat(' ')))
+                self.assertEqual(rest, '',
+                                 'testing "%s" rest=%s' % (test_value, rest))
+                self.assertEqual(rest2, test_rest.strip(),
+                                 'testing "%s" rest=%s' % (test_value, rest2))
+                self.assertEqual(real_result.year, test_result[0],
+                                 'testing "%s", year=%s' % (test_value,
+                                                            real_result.year))
+                self.assertEqual(real_result.month, test_result[1],
+                                 'testing "%s", month=%s' %
+                                  (test_value, real_result.month))
+                self.assertEqual(real_result.day, test_result[2],
+                                 'testing "%s", day=%s' % (test_value,
+                                                           real_result.day))
+                self.assertEqual(real_result.hour, test_result[3],
+                                 'testing "%s", hour=%s' % (test_value,
+                                                            real_result.hour))
+                self.assertEqual(real_result.minute, test_result[4],
+                                 'testing "%s", minute=%s' %
+                                  (test_value, real_result.minute))
+                self.assertEqual(real_result.second, test_result[5],
+                                 'testing "%s", second=%s' %
+                                  (test_value, real_result.second))
+    def test_action(hamster, arg):
+        '''Run the tests.'''
+        unittest.main(argv=[sys.argv[0]])
+
+FUNCTION_FOR_ACTION = {'start': start_action,
+                       'stop': stop_action,
+                       'list': list_action,
+                       'list-activities': list_activities_action,
+                       'list-categories': list_categories_action}
+if __DEBUG__:
+    FUNCTION_FOR_ACTION['test'] = test_action
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv))
