Permalink
Branch: master
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 396 lines (297 sloc) 12.8 KB
#!/usr/bin/env python3
# - coding: utf-8 -
# Copyright (C) 2010 Matías Ribecky <matias at mribecky.com.ar>
# Copyright (C) 2010-2012 Toms Bauģis <toms.baugis@gmail.com>
# Copyright (C) 2012 Ted Smith <tedks at cs.umd.edu>
# 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 sys, os
import optparse
import re
import datetime as dt
from hamster import client, reports
from hamster.lib import Fact, stuff
def word_wrap(line, max_len):
"""primitive word wrapper"""
lines = []
cur_line, cur_len = "", 0
for word in line.split():
if len("%s %s" % (cur_line, word)) < max_len:
cur_line = ("%s %s" % (cur_line, word)).strip()
else:
if cur_line:
lines.append(cur_line)
cur_line = word
if cur_line:
lines.append(cur_line)
return lines
def fact_dict(fact_data, with_date):
fact = {}
if with_date:
fmt = '%Y-%m-%d %H:%M'
else:
fmt = '%H:%M'
fact['start'] = fact_data.start_time.strftime(fmt)
if fact_data.end_time:
fact['end'] = fact_data.end_time.strftime(fmt)
else:
end_date = dt.datetime.now()
fact['end'] = ''
fact['duration'] = stuff.format_duration(fact_data.delta)
fact['activity'] = fact_data.activity
fact['category'] = fact_data.category
if fact_data.tags:
fact['tags'] = ' '.join('#%s' % tag for tag in fact_data.tags)
else:
fact['tags'] = ''
fact['description'] = fact_data.description
return fact
_DATETIME_PATTERN = ('^((?P<relative>-\d.+)?|('
'(?P<date1>\d{4}-\d{2}-\d{2})?'
'(?P<time1> ?\d{2}:\d{2})?'
'(?P<dash> ?-)?'
'(?P<date2> ?\d{4}-\d{2}-\d{2})?'
'(?P<time2> ?\d{2}:\d{2})?)?)'
'(?P<rest>\D.+)?$')
_DATETIME_REGEX = re.compile(_DATETIME_PATTERN)
def parse_datetime_range(arg):
'''Parse a date and time.'''
match = _DATETIME_REGEX.match(arg)
if not match:
return None, None
fragments = match.groupdict()
rest = (fragments['rest'] or '').strip()
# bail out early on relative minutes
if fragments['relative']:
delta_minutes = int(fragments['relative'])
return dt.datetime.now() - dt.timedelta(minutes=delta_minutes), rest
start_time, end_time = None, None
def to_time(timestr):
timestr = (timestr or "").strip()
try:
return dt.datetime.strptime(timestr, "%Y-%m-%d").date()
except ValueError:
try:
return dt.datetime.strptime(timestr, "%H:%M").time()
except ValueError:
pass
return None
if fragments['date1'] or fragments['time1']:
start_time = dt.datetime.combine(to_time(fragments['date1']) or dt.date.today(),
to_time(fragments['time1']) or dt.time())
if fragments['date2'] or fragments['time2']:
end_time = dt.datetime.combine(to_time(fragments['date2']) or start_time or dt.date.today(),
to_time(fragments['time2']) or dt.time())
return start_time, end_time
class HamsterClient(object):
'''The main application.'''
def __init__(self):
self.storage = client.Storage()
def _launch_window(self, window_name):
try:
from hamster import defs
running_installed = True
except:
running_installed = False
if running_installed:
import dbus
bus = dbus.SessionBus()
server = bus.get_object("org.gnome.Hamster.WindowServer",
"/org/gnome/Hamster/WindowServer")
getattr(server, window_name)()
else:
print("Running in devel mode")
from gi.repository import Gtk as gtk
from hamster.lib.configuration import dialogs
getattr(dialogs, window_name).show()
gtk.main()
def overview(self, *args):
self._launch_window("overview")
def statistics(self, *args):
self._launch_window("statistics")
def about(self, *args):
self._launch_window("about")
def prefs(self, *args):
self._launch_window("prefs")
def add(self, *args):
self._launch_window("edit")
def assist(self, *args):
assist_command = args[0] if args else ""
if assist_command == "start":
hamster_client._activities(sys.argv[-1])
elif assist_command == "export":
formats = "html tsv xml ical".split()
chosen = sys.argv[-1]
formats = [f for f in formats if not chosen or f.startswith(chosen)]
print("\n".join(formats))
def toggle(self):
self.storage.toggle()
def track(self, *args):
"""same as start"""
self.start(*args)
def start(self, *args):
'''Start a new activity.'''
if not args:
print("Error: please specify activity")
return
activity = args[0]
start_time, end_time = parse_datetime_range(" ".join(args[1:]))
start_time = start_time or dt.datetime.now()
self.storage.add_fact(Fact(activity,
start_time = start_time,
end_time = end_time))
def stop(self, *args):
'''Stop tracking the current activity.'''
self.storage.stop_tracking()
def export(self, *args):
args = args or []
export_format, start_time, end_time = "html", None, None
if args:
export_format = args[0]
start_time, end_time = parse_datetime_range(" ".join(args[1:]))
start_time = start_time or dt.datetime.combine(dt.date.today(), dt.time())
end_time = end_time or start_time.replace(hour=23, minute=59, second=59)
facts = self.storage.get_facts(start_time, end_time, ongoing_days=31)
writer = reports.simple(facts, start_time.date(), end_time.date(), export_format)
def _activities(self, search=""):
'''Print the names of all the activities.'''
if "@" in search:
activity, category = search.split("@")
for cat in self.storage.get_categories():
if not category or cat['name'].lower().startswith(category.lower()):
print("{}@{}".format(activity, cat['name']))
else:
for activity in self.storage.get_activities(search):
print(activity['name'])
if activity['category']:
print("{}@{}".format(activity['name'], activity['category']))
def activities(self, *args):
'''Print the names of all the activities.'''
search = args[0] if args else ""
for activity in self.storage.get_activities(search):
print("{}@{}".format(activity['name'], activity['category']))
def categories(self, *args):
'''Print the names of all the categories.'''
for category in self.storage.get_categories():
print(category['name'])
def list(self, *times):
"""list facts within a date range"""
start_time, end_time = parse_datetime_range(" ".join(times or []))
start_time = start_time or dt.datetime.combine(dt.date.today(), dt.time())
end_time = end_time or start_time.replace(hour=23, minute=59, second=59)
self._list(start_time, end_time)
def current(self, *args):
"""prints current activity. kinda minimal right now"""
facts = self.storage.get_todays_facts()
if facts and not facts[-1].end_time:
print("{} {}".format(str(facts[-1]).strip(),
stuff.format_duration(facts[-1].delta, human=False)))
else:
print((_("No activity")))
def search(self, *args):
"""search for activities by name and optionally within a date range"""
args = args or []
search = ""
if args:
search = args[0]
start_time, end_time = parse_datetime_range(" ".join(args[1:]))
start_time = start_time or dt.datetime.combine(dt.date.today(), dt.time())
end_time = end_time or start_time.replace(hour=23, minute=59, second=59)
self._list(start_time, end_time, search)
def _list(self, start_time, end_time, search=""):
"""Print a listing of activities"""
facts = self.storage.get_facts(start_time, end_time, search, ongoing_days=31)
headers = {'activity': _("Activity"),
'category': _("Category"),
'tags': _("Tags"),
'description': _("Description"),
'start': _("Start"),
'end': _("End"),
'duration': _("Duration")}
# print date if it is not the same day
print_with_date = start_time.date() != end_time.date()
cols = 'start', 'end', 'duration', 'activity', 'category'
widths = dict([(col, len(headers[col])) for col in cols])
for fact in facts:
fact = fact_dict(fact, print_with_date)
for col in cols:
widths[col] = max(widths[col], len(fact[col]))
cols = ["{{{col}: <{len}}}".format(col=col, len=widths[col]) for col in cols]
fact_line = " | ".join(cols)
row_width = sum(val + 3 for val in list(widths.values()))
print()
print(fact_line.format(**headers))
print("-" * min(row_width, 80))
by_cat = {}
for fact in facts:
cat = fact.category or _("Uncategorized")
by_cat.setdefault(cat, dt.timedelta(0))
by_cat[cat] += fact.delta
pretty_fact = fact_dict(fact, print_with_date)
print(fact_line.format(**pretty_fact))
if pretty_fact['description']:
for line in word_wrap(pretty_fact['description'], 76):
print(" {}".format(line))
if pretty_fact['tags']:
for line in word_wrap(pretty_fact['tags'], 76):
print(" {}".format(line))
print("-" * min(row_width, 80))
cats = []
total_duration = dt.timedelta()
for cat, duration in sorted(by_cat.items(), key=lambda x: x[1], reverse=True):
cats.append("{}: {}".format(cat, stuff.format_duration(duration)))
total_duration += duration
for line in word_wrap(", ".join(cats), 80):
print(line)
print("Total: ", stuff.format_duration(total_duration))
print()
if __name__ == '__main__':
from hamster.lib import i18n
i18n.setup_i18n()
usage = _(
"""
Actions:
* start / track <activity> [start-time] [end-time]: Track an activity
* stop: Stop tracking current activity.
* list [start-time] [end-time]: List activities
* search [terms] [start-time] [end-time]: List activities matching a search
term
* export [html|tsv|ical|xml] [start-time] [end-time]: Export activities with
the specified format
* current: Print current activity
* activities: List all the activities names, one per line.
* categories: List all the categories names, one per line.
* overview / statistics / about: launch specific window
Time formats:
* 'YYYY-MM-DD hh:mm:ss': If date is missing, it will default to today.
If time is missing, it will default to 00:00 for start time and 23:59 for
end time.
* '-minutes': Relative time in minutes from the current date and time.
Example usage:
hamster start bananas -20
start activity 'bananas' with start time 20 minutes ago
hamster search pancakes 2012-08-01 2012-08-30
look for an activity matching terms 'pancakes` between 1st and 30st
August 2012. Will check against activity, category, description and tags
""")
hamster_client = HamsterClient()
import signal
signal.signal(signal.SIGINT, signal.SIG_DFL) # gtk3 screws up ctrl+c
if len(sys.argv) < 2:
hamster_client.overview()
else:
command, args = sys.argv[1], sys.argv[2:]
if hasattr(hamster_client, command):
getattr(hamster_client, command)(*args)
else:
sys.exit(usage % {'prog': sys.argv[0]})