diff --git a/README.md b/README.md index 6382ac1..a637dab 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,30 @@ sublime-wakatime ================ automatic time tracking for Sublime Text 2 + +Installation +------------ + +1) Get an api key from: + +https://wakati.me + +2) Run this shell command replacing KEY with your api key: + + echo "api_key=KEY" >> ~/.wakatime + +3) Install the plugin in Sublime's Packages directory. + +4) Use Sublime and your time will automatically be tracked for you. + +Visit https://wakati.me to view your time spent in each file. + +Screen Shots +------------ + +![Project Overview](https://www.wakati.me/static/img/ScreenShots/Screenshot%20from%202013-06-26%2001:12:59.png) + +![Files in a Project](https://www.wakati.me/static/img/ScreenShots/Screenshot%20from%202013-06-26%2001:13:13.png) + +![Changing Date Range](https://www.wakati.me/static/img/ScreenShots/Screenshot%20from%202013-06-26%2001:13:53.png) + diff --git a/libs/wakatime.py b/libs/wakatime.py new file mode 100644 index 0000000..30eb4aa --- /dev/null +++ b/libs/wakatime.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python + +import os +import sys +import argparse +import platform +import urllib2 +import json +import base64 +import uuid +import time +import re +from collections import OrderedDict +from httplib import BadStatusLine, IncompleteRead +from urllib2 import HTTPError, URLError +import logging as log + + +# Config +version = '0.1.0' +user_agent = 'sublime-wakatime/%s (%s)' % (version, platform.platform()) + + +def project_from_path(path): + project = git_project(path) + if project: + return project + return None + + +def tags_from_path(path): + tags = [] + if os.path.exists(path): + tags.extend(git_tags(path)) + tags.extend(mercurial_tags(path)) + return list(set(tags)) + + +def git_project(path): + config_file = find_git_config(path) + if config_file: + folder = os.path.split(os.path.split(os.path.split(config_file)[0])[0])[1] + if folder: + return folder + return None + + +def find_git_config(path): + path = os.path.realpath(path) + if os.path.isfile(path): + path = os.path.split(path)[0] + if os.path.isfile(os.path.join(path, '.git', 'config')): + return os.path.join(path, '.git', 'config') + split_path = os.path.split(path) + if split_path[1] == '': + return None + return find_git_config(split_path[0]) + + +def parse_git_config(config): + sections = OrderedDict() + try: + f = open(config, 'r') + except IOError as e: + log.exception("Exception:") + else: + with f: + section = None + for line in f.readlines(): + line = line.lstrip() + if len(line) > 0 and line[0] == '[': + section = line[1:].split(']', 1)[0] + temp = section.split(' ', 1) + section = temp[0].lower() + if len(temp) > 1: + section = ' '.join([section, temp[1]]) + sections[section] = OrderedDict() + else: + try: + (setting, value) = line.split('=', 1) + except ValueError: + setting = line.split('#', 1)[0].split(';', 1)[0] + value = 'true' + setting = setting.strip().lower() + value = value.split('#', 1)[0].split(';', 1)[0].strip() + sections[section][setting] = value + f.close() + return sections + + +def git_tags(path): + tags = [] + config_file = find_git_config(path) + if config_file: + sections = parse_git_config(config_file) + for section in sections: + if section.split(' ', 1)[0] == 'remote' and 'url' in sections[section]: + tags.append(sections[section]['url']) + return tags + + +def mercurial_tags(path): + tags = [] + return tags + + +def svn_tags(path): + tags = [] + return tags + + +def log_action(**kwargs): + kwargs['User-Agent'] = user_agent + log.info(json.dumps(kwargs)) + + +def send_action(key, instance, action, task, timestamp, project, tags): + url = 'https://www.wakati.me/api/v1/actions' + data = { + 'type': action, + 'task': os.path.realpath(task), + 'time': timestamp, + 'instance_id': instance, + 'project': project, + 'tags': tags, + } + request = urllib2.Request(url=url, data=json.dumps(data)) + request.add_header('User-Agent', user_agent) + request.add_header('Content-Type', 'application/json') + request.add_header('Authorization', 'Basic %s' % base64.b64encode(key)) + log_action(**data) + response = None + try: + response = urllib2.urlopen(request) + except HTTPError as ex: + log.error("%s:\ndata=%s\nresponse=%s" % (ex.getcode(), json.dumps(data), ex.read())) + if log.getLogger().isEnabledFor(log.DEBUG): + log.exception("Exception for %s:\n%s" % (data['time'], json.dumps(data))) + except (URLError, IncompleteRead, BadStatusLine) as ex: + log.error("%s:\ndata=%s\nmessage=%s" % (ex.__class__.__name__, json.dumps(data), ex)) + if log.getLogger().isEnabledFor(log.DEBUG): + log.exception("Exception for %s:\n%s" % (data['time'], json.dumps(data))) + if response: + log.debug('response_code=%s response_content=%s' % (response.getcode(), response.read())) + if response and (response.getcode() == 200 or response.getcode() == 201): + return True + return False + + +def parse_args(argv): + parser = argparse.ArgumentParser(description='Log time to the wakati.me api') + parser.add_argument('--key', dest='key', required=True, + help='your wakati.me api key') + parser.add_argument('--action', dest='action', required=True, + choices=['open_file', 'ping', 'close_file', 'write_file', 'open_editor', 'quit_editor', 'minimize_editor', 'maximize_editor', 'start', 'stop']) + parser.add_argument('--task', dest='task', required=True, + help='path to file or named task') + parser.add_argument('--instance', dest='instance', required=True, + help='the UUID4 representing the current editor') + parser.add_argument('--time', dest='timestamp', metavar='time', type=float, + help='optional floating-point timestamp in seconds') + parser.add_argument('--verbose', dest='verbose', action='store_true', + help='turns on debug messages in logfile') + parser.add_argument('--version', action='version', version=version) + return parser.parse_args(argv) + + +def main(argv): + args = parse_args(argv) + level = log.INFO + if args.verbose: + level = log.DEBUG + del args.verbose + if not args.timestamp: + args.timestamp = time.time() + log.basicConfig(filename=os.path.expanduser('~/.wakatime.log'), format='%(asctime)s vim-wakatime/'+version+' %(levelname)s %(message)s', datefmt='%Y-%m-%dT%H:%M:%SZ', level=level) + if os.path.isfile(os.path.realpath(args.task)): + tags = tags_from_path(args.task) + project = project_from_path(args.task) + send_action(project=project, tags=tags, **vars(args)) + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/sublime-wakatime.py b/sublime-wakatime.py new file mode 100644 index 0000000..76665e3 --- /dev/null +++ b/sublime-wakatime.py @@ -0,0 +1,66 @@ +""" ====================================================== +File: sublime-wakatime.py +Description: Automatic time tracking for Sublime Text 2. +Maintainer: Wakati.Me +Version: 0.1.0 +=======================================================""" + + +import time +import uuid +from os.path import expanduser, dirname, realpath +from subprocess import call, Popen + +import sublime +import sublime_plugin + + +# Create logfile if does not exist +call(['touch', '~/.wakatime.log']) + +PLUGIN_DIR = dirname(realpath(__file__)) +API_CLIENT = '%s/libs/wakatime.py' % PLUGIN_DIR +INSTANCE_ID = str(uuid.uuid4()) + + +def get_api_key(): + api_key = None + try: + cf = open(expanduser('~/.wakatime')) + for line in cf: + line = line.split('=', 1) + if line[0] == 'api_key': + api_key = line[1] + cf.close() + except IOError: + pass + return api_key + + +def api(action, task, timestamp): + if task: + api_key = get_api_key() + if api_key: + cmd = ['python', API_CLIENT, + '--key', api_key, + '--instance', INSTANCE_ID, + '--action', action, + '--task', task, + '--time', str('%f' % timestamp)] + Popen(cmd) + + +class WakatimeListener(sublime_plugin.EventListener): + + def on_post_save(self, view): + api('write_file', view.file_name(), time.time()) + + def on_activated(self, view): + api('open_file', view.file_name(), time.time()) + + def on_deactivated(self, view): + api('close_file', view.file_name(), time.time()) + + +if get_api_key() is None: + sublime.error_message('Missing your Wakati.Me api key')