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..f80a664
--- /dev/null
+++ src/hamster-client.py
@@ -0,0 +1,407 @@
+#!/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/>.
+
+'''A script to control the applet from the command line.'''
+
+import os
+import re
+import sys
+import time
+import locale
+import gettext
+import logging
+import optparse
+import datetime
+
+import dbus
+import dbus.mainloop.glib
+
+import hamster.defs
+import hamster.stuff
+
+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
+
+class HamsterClient(object):
+    '''The main application.'''
+
+    actions = ['start', 'stop', 'list', 'list-activities', 'list-categories']
+
+    def __init__(self, argv):
+        self.hamster_conn = connect()
+        self.opts = None
+        self.args = []
+        self.action = None
+
+        self._configure(argv)
+
+    def _configure(self, argv):
+        '''Parse the arguments to the program.'''
+        parser = self._create_argument_parser()
+
+        logging.debug("Command line arguments: %s", argv)
+        argv = list(_escape_relative_minutes(argv))
+        self.opts, self.args = parser.parse_args(argv[1:])
+        self.args = list(_unescape_relative_minutes(self.args))
+        logging.debug("Parsed options: %s", self.opts)
+        logging.debug("Parsed arguments: %s", self.args)
+
+        self.action = self.args.pop(0)
+        logging.debug("The action is '%s'", self.action)
+        if self.action not in self.actions:
+            raise ConfigurationError(_("Invalid or no action specified"))
+        self.action_method = self.action.replace('-', '_')
+
+        try:
+            action_config = getattr(self, self.action_method + '_configure')
+        except AttributeError:
+            pass
+        else:
+            action_config()
+
+    def _create_argument_parser(self):
+        '''Build an argument parser with all the options required.'''
+
+        usage = _(
+"""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.
+
+""")
+        parser = optparse.OptionParser(usage=__doc__)
+        return parser
+
+
+    def run(self):
+        '''Execute the action.'''
+
+        getattr(self, self.action_method)()
+
+    def start_configure(self):
+        '''Configure the 'start' action.'''
+
+        start_date, end_date, arg = parse_datetime_range(' '.join(self.args))
+        self.activity = arg
+
+        if start_date > now() or (end_date and end_date > now()):
+            raise ConfigurationError(_("cannot start/end an activity in "
+                                       "the future."))
+
+        self.start_stamp = timestamp_from_datetime(start_date)
+        if end_date:
+            self.end_stamp = timestamp_from_datetime(end_date)
+        else:
+            self.end_stamp = 0
+
+    def start(self):
+        '''Start a new activity.'''
+
+        logging.debug("Creating fact '%s' from %s to %s",
+                      self.activity,
+                      self.start_stamp,
+                      self.end_stamp)
+        if not __DEBUG__:
+            self.hamster_conn.AddFact(self.activity,
+                                 dbus.UInt32(self.start_stamp),
+                                 dbus.UInt32(self.end_stamp))
+        return 0
+
+    def stop(self):
+        '''Stop tracking the current activity.'''
+        logging.debug("Stoping tracking current activity")
+        if not __DEBUG__:
+            self.hamster_conn.StopTracking()
+        return 0
+
+    def list_configure(self):
+        '''Configure the 'list' action.'''
+        if self.args:
+            arg = ' '.join(self.args)
+            self.start_date, self.end_date, arg = parse_datetime_range(arg)
+        else:
+            today = datetime.datetime.today()
+            self.start_date = datetime.datetime.combine(today, datetime.time())
+            self.end_date = None
+
+        if not self.end_date:
+            self.end_date = datetime.datetime(year=self.start_date.year,
+                                              month=self.start_date.month,
+                                              day=self.start_date.day,
+                                              hour=23,
+                                              minute=59,
+                                              second=59)
+
+    def list(self):
+        '''Print a listing of activities.'''
+
+        headers = {'activity': _("Activity"),
+                   'category': _("Category"),
+                   'tags': _("Tags"),
+                   'start': _("Start"),
+                   'end': _("End"),
+                   'duration': _("Duration")}
+        line_fmt = ' %*s - %*s (%*s) | %s@%s %s'
+
+        logging.debug("Listing activities from '%s' to '%s'",
+                      self.start_date.isoformat(' '),
+                      self.end_date.isoformat(' '))
+
+        start_stamp = timestamp_from_datetime(self.start_date)
+        end_stamp = timestamp_from_datetime(self.end_date)
+
+        print_with_date = self.start_date.date() != self.end_date.date()
+        if print_with_date:
+            # len('xxxx-xx-xx xx:xx')
+            dates_align_width = 16
+        else:
+            # len('xx:xx')
+            dates_align_width = 5
+        column_width = {'start': max(len(headers['start']), dates_align_width),
+                        'end': max(len(headers['end']), dates_align_width),
+                        'duration': max(len(headers['duration']), 7)}
+
+        print line_fmt % (column_width['start'], headers['start'],
+                          column_width['end'], headers['end'],
+                          column_width['duration'], headers['duration'],
+                          headers['activity'],
+                          headers['category'],
+                          headers['tags'])
+        first_column_width = (8 + sum(column_width.values()))
+        second_column_width = 4 + len(headers['activity']) + \
+                              len(headers['category']) + \
+                              len(headers['tags'])
+        print '-' * first_column_width + '+' + '-' * second_column_width
+        for fact in self.hamster_conn.GetFacts(start_stamp, end_stamp):
+            if not (start_stamp < fact['start_time'] < end_stamp):
+                # Hamster returns activities for the whole day, not just the
+                # time range we sent
+                continue
+
+            fact_data = fact_dict(fact, print_with_date)
+            print line_fmt % (column_width['start'], fact_data['start'],
+                              column_width['end'], fact_data['end'],
+                              column_width['duration'], fact_data['duration'],
+                              fact_data['activity'],
+                              fact_data['category'],
+                              fact_data['tags'])
+
+
+        return 0
+
+    def list_activities(self):
+        '''Print the names of all the activities.'''
+        for activity, category in self.hamster_conn.GetActivities():
+            print '%s@%s' % (activity.encode('utf8'), category.encode('utf8'))
+        return 0
+
+    def list_categories(self):
+        '''Print the names of all the categories.'''
+        for category in self.hamster_conn.GetCategories():
+            print category.encode('utf8')
+        return 0
+
+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_conn = dbus.Interface(connection.get_object(HAMSTER_SERVICE,
+                                                            HAMSTER_PATH),
+                                      dbus_interface=HAMSTER_INTERFACE)
+        return hamster_conn
+    except dbus.DBusException:
+        logging.exception("Can't init dbus")
+        raise ConfigurationError(_("Could not connect to hamster-applet."))
+
+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 now():
+    '''Return the current datetime.'''
+    return datetime.datetime.now().replace(microsecond=0)
+
+def timestamp_from_datetime(date):
+    '''Convert a local time datetime into an utc timestamp.'''
+    tz_offset =  datetime.timedelta(seconds=time.timezone)
+    return time.mktime((date - tz_offset).timetuple())
+
+def datetime_from_timestamp(timestamp):
+    '''Convert an utc timestamp into a local time datetime.'''
+    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()
+        try:
+            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))
+        except ValueError, err:
+            if match.groupdict()['rest']:
+                date_str = arg[:-len(match.groupdict()['rest'])]
+            else:
+                date_str = arg
+            logging.exception("invalid date/time '%s', %s", date_str, err)
+            raise ConfigurationError(_("invalid date/time '%s'" % date_str))
+        rest = (match.groupdict()['rest'] or '').strip()
+        return datetime.datetime.combine(date, time), rest
+
+def fact_dict(fact_data, with_date):
+    fact = {}
+    if with_date:
+        fmt = '%Y-%m-%d %H:%M'
+    else:
+        fmt = '%H:%M'
+
+    start_date = datetime_from_timestamp(fact_data['start_time'])
+    fact['start'] = start_date.strftime(fmt)
+    if fact_data['end_time']:
+        end_date = datetime_from_timestamp(fact_data['end_time'])
+        fact['end'] = end_date.strftime(fmt)
+    else:
+        end_date = now()
+        fact['end'] = ''
+
+    fact['duration'] = hamster.stuff.format_duration(end_date - start_date)
+    fact['duration'] = fact['duration']
+
+    fact['activity'] = fact_data['name']
+    fact['category'] = fact_data['category']
+    if fact_data['tags']:
+        fact['tags'] = ' '.join('#%s' % tag for tag in fact_data['tags'])
+    else:
+        fact['tags'] = ''
+    return fact
+
+def main(argv):
+    '''Main application body.'''
+
+    # Setup i18n
+    gettext.install("hamster-applet", unicode=True)
+    locale_dir = os.path.abspath(os.path.join(hamster.defs.DATA_DIR, "locale"))
+    for module in (gettext, locale):
+        module.bindtextdomain('hamster-applet', locale_dir)
+        module.textdomain('hamster-applet')
+
+        if hasattr(module, 'bind_textdomain_codeset'):
+            module.bind_textdomain_codeset('hamster-applet','UTF-8')
+
+    if __DEBUG__:
+        logging.basicConfig(level=logging.DEBUG)
+    else:
+        logging.basicConfig(level=logging.FATAL)
+
+    try:
+        hamster_client = HamsterClient(argv)
+    except ConfigurationError, error:
+        if os.path.isabs(argv[0]):
+            prog = os.path.basename(argv[0])
+        else:
+            prog = argv[0]
+        print >>sys.stderr, _("%s: error: %s") % (prog, str(error))
+        return 1
+
+    return hamster_client.run()
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv))
diff --git tests/hamster_client_test.py tests/hamster_client_test.py
new file mode 100644
index 0000000..c713bed
--- /dev/null
+++ tests/hamster_client_test.py
@@ -0,0 +1,85 @@
+import sys, os.path, imp
+# Convoluted lines to import a file with a '-' in its name
+src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
+                                       os.pardir,
+                                       'src'))
+hamster_client_filename = os.path.join(src_dir, 'hamster-client.py')
+hamster_client = imp.load_module('hamster_client',
+                                 open(hamster_client_filename, 'r'),
+                                 hamster_client_filename,
+                                 ('.py', 'r', imp.PY_SOURCE))
+
+import datetime
+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 `now` function.'''
+        self.orig_now = hamster_client.now
+        hamster_client.now = self.now
+
+    def tearDown(self):
+        '''Restores the `now` function.'''
+        hamster_client.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 = hamster_client.parse_datetime(test_value)
+            real_result2, rest2 = hamster_client.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))
+
+if __name__ == '__main__':
+    unittest.main()
