updated wakatime.py package. using new usage logic for better actions accuracy.
This commit is contained in:
parent
eb1e3f72db
commit
a9e0bdb3fe
9 changed files with 180 additions and 83 deletions
|
@ -1,4 +1,4 @@
|
||||||
sublime-wakatime 0.1.0
|
sublime-wakatime
|
||||||
================
|
================
|
||||||
|
|
||||||
automatic time tracking for Sublime Text 2 & 3 using https://wakati.me
|
automatic time tracking for Sublime Text 2 & 3 using https://wakati.me
|
||||||
|
|
|
@ -27,15 +27,13 @@ class CustomEncoder(json.JSONEncoder):
|
||||||
|
|
||||||
class JsonFormatter(logging.Formatter):
|
class JsonFormatter(logging.Formatter):
|
||||||
|
|
||||||
def __init__(self, timestamp, endtime, isWrite, targetFile, version,
|
def setup(self, timestamp, endtime, isWrite, targetFile, version, plugin):
|
||||||
plugin, datefmt=None):
|
|
||||||
self.timestamp = timestamp
|
self.timestamp = timestamp
|
||||||
self.endtime = endtime
|
self.endtime = endtime
|
||||||
self.isWrite = isWrite
|
self.isWrite = isWrite
|
||||||
self.targetFile = targetFile
|
self.targetFile = targetFile
|
||||||
self.version = version
|
self.version = version
|
||||||
self.plugin = plugin
|
self.plugin = plugin
|
||||||
super(JsonFormatter, self).__init__(datefmt=datefmt)
|
|
||||||
|
|
||||||
def format(self, record):
|
def format(self, record):
|
||||||
data = OrderedDict([
|
data = OrderedDict([
|
||||||
|
@ -66,14 +64,14 @@ def setup_logging(args, version):
|
||||||
if not logfile:
|
if not logfile:
|
||||||
logfile = '~/.wakatime.log'
|
logfile = '~/.wakatime.log'
|
||||||
handler = logging.FileHandler(os.path.expanduser(logfile))
|
handler = logging.FileHandler(os.path.expanduser(logfile))
|
||||||
formatter = JsonFormatter(
|
formatter = JsonFormatter(datefmt='%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
formatter.setup(
|
||||||
timestamp=args.timestamp,
|
timestamp=args.timestamp,
|
||||||
endtime=args.endtime,
|
endtime=args.endtime,
|
||||||
isWrite=args.isWrite,
|
isWrite=args.isWrite,
|
||||||
targetFile=args.targetFile,
|
targetFile=args.targetFile,
|
||||||
version=version,
|
version=version,
|
||||||
plugin=args.plugin,
|
plugin=args.plugin,
|
||||||
datefmt='%Y-%m-%dT%H:%M:%SZ',
|
|
||||||
)
|
)
|
||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
|
@ -30,6 +30,6 @@ PLUGINS = [
|
||||||
def find_project(path):
|
def find_project(path):
|
||||||
for plugin in PLUGINS:
|
for plugin in PLUGINS:
|
||||||
project = plugin(path)
|
project = plugin(path)
|
||||||
if project.config:
|
if project.process():
|
||||||
return project
|
return project
|
||||||
return BaseProject(path)
|
return BaseProject(path)
|
||||||
|
|
|
@ -16,32 +16,39 @@ import os
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class BaseProject():
|
class BaseProject(object):
|
||||||
|
""" Parent project class only
|
||||||
|
used when no valid project can
|
||||||
|
be found for the current path.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
self.path = path
|
self.path = path
|
||||||
self.config = self.findConfig(path)
|
|
||||||
|
|
||||||
def name(self):
|
|
||||||
base = self.base()
|
|
||||||
if base:
|
|
||||||
return os.path.basename(base)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def type(self):
|
def type(self):
|
||||||
|
""" Returns None if this is the base class.
|
||||||
|
Returns the type of project if this is a
|
||||||
|
valid project.
|
||||||
|
"""
|
||||||
type = self.__class__.__name__.lower()
|
type = self.__class__.__name__.lower()
|
||||||
if type == 'baseproject':
|
if type == 'baseproject':
|
||||||
type = None
|
type = None
|
||||||
return type
|
return type
|
||||||
|
|
||||||
def base(self):
|
def process(self):
|
||||||
if self.config:
|
""" Processes self.path into a project and
|
||||||
return os.path.dirname(self.config)
|
returns True if project is valid, otherwise
|
||||||
|
returns False.
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
""" Returns the project's name.
|
||||||
|
"""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def tags(self):
|
def tags(self):
|
||||||
tags = []
|
""" Returns an array of tag strings for the
|
||||||
return tags
|
path and/or project.
|
||||||
|
"""
|
||||||
def findConfig(self, path):
|
return []
|
||||||
return ''
|
|
||||||
|
|
|
@ -11,13 +11,10 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
|
|
||||||
from .base import BaseProject
|
from .base import BaseProject
|
||||||
|
from ..packages.ordereddict import OrderedDict
|
||||||
try:
|
|
||||||
from collections import OrderedDict
|
|
||||||
except ImportError:
|
|
||||||
from ..packages.ordereddict import OrderedDict
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -25,21 +22,54 @@ log = logging.getLogger(__name__)
|
||||||
|
|
||||||
class Git(BaseProject):
|
class Git(BaseProject):
|
||||||
|
|
||||||
def base(self):
|
def process(self):
|
||||||
|
self.config = self._find_config(self.path)
|
||||||
if self.config:
|
if self.config:
|
||||||
return os.path.dirname(os.path.dirname(self.config))
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
base = self._project_base()
|
||||||
|
if base:
|
||||||
|
return os.path.basename(base)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def tags(self):
|
def tags(self):
|
||||||
tags = []
|
tags = []
|
||||||
if self.config:
|
if self.config:
|
||||||
sections = self.parseConfig()
|
base = self._project_base()
|
||||||
|
if base:
|
||||||
|
tags.append(base)
|
||||||
|
sections = self._parse_config()
|
||||||
for section in sections:
|
for section in sections:
|
||||||
if section.split(' ', 1)[0] == 'remote' and 'url' in sections[section]:
|
if section.split(' ', 1)[0] == 'remote' and 'url' in sections[section]:
|
||||||
tags.append(sections[section]['url'])
|
tags.append(sections[section]['url'])
|
||||||
|
branch = self._current_branch()
|
||||||
|
if branch is not None:
|
||||||
|
tags.append(branch)
|
||||||
return tags
|
return tags
|
||||||
|
|
||||||
def findConfig(self, path):
|
def _project_base(self):
|
||||||
|
if self.config:
|
||||||
|
return os.path.dirname(os.path.dirname(self.config))
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _current_branch(self):
|
||||||
|
stdout = None
|
||||||
|
try:
|
||||||
|
stdout, stderr = Popen([
|
||||||
|
'git', 'branch', '--no-color', '--list'
|
||||||
|
], stdout=PIPE, cwd=self._project_base()).communicate()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
if stdout:
|
||||||
|
for line in stdout.splitlines():
|
||||||
|
line = line.split(' ', 1)
|
||||||
|
if line[0] == '*':
|
||||||
|
return line[1]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _find_config(self, path):
|
||||||
path = os.path.realpath(path)
|
path = os.path.realpath(path)
|
||||||
if os.path.isfile(path):
|
if os.path.isfile(path):
|
||||||
path = os.path.split(path)[0]
|
path = os.path.split(path)[0]
|
||||||
|
@ -48,9 +78,9 @@ class Git(BaseProject):
|
||||||
split_path = os.path.split(path)
|
split_path = os.path.split(path)
|
||||||
if split_path[1] == '':
|
if split_path[1] == '':
|
||||||
return None
|
return None
|
||||||
return self.findConfig(split_path[0])
|
return self._find_config(split_path[0])
|
||||||
|
|
||||||
def parseConfig(self):
|
def _parse_config(self):
|
||||||
sections = {}
|
sections = {}
|
||||||
try:
|
try:
|
||||||
f = open(self.config, 'r')
|
f = open(self.config, 'r')
|
||||||
|
|
|
@ -20,9 +20,11 @@ log = logging.getLogger(__name__)
|
||||||
|
|
||||||
class Mercurial(BaseProject):
|
class Mercurial(BaseProject):
|
||||||
|
|
||||||
def base(self):
|
def process(self):
|
||||||
return super(Mercurial, self).base()
|
return False
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
return None
|
||||||
|
|
||||||
def tags(self):
|
def tags(self):
|
||||||
tags = []
|
return []
|
||||||
return tags
|
|
||||||
|
|
|
@ -11,8 +11,10 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
|
|
||||||
from .base import BaseProject
|
from .base import BaseProject
|
||||||
|
from ..packages.ordereddict import OrderedDict
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -20,9 +22,42 @@ log = logging.getLogger(__name__)
|
||||||
|
|
||||||
class Subversion(BaseProject):
|
class Subversion(BaseProject):
|
||||||
|
|
||||||
def base(self):
|
def process(self):
|
||||||
return super(Subversion, self).base()
|
self.info = self._get_info()
|
||||||
|
if 'Repository Root' in self.info:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
return self.info['Repository Root'].split('/')[-1]
|
||||||
|
|
||||||
|
def _get_info(self):
|
||||||
|
info = OrderedDict()
|
||||||
|
stdout = None
|
||||||
|
try:
|
||||||
|
stdout, stderr = Popen([
|
||||||
|
'svn', 'info', os.path.realpath(self.path)
|
||||||
|
], stdout=PIPE).communicate()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if stdout:
|
||||||
|
interesting = [
|
||||||
|
'Repository Root',
|
||||||
|
'Repository UUID',
|
||||||
|
'URL',
|
||||||
|
]
|
||||||
|
for line in stdout.splitlines():
|
||||||
|
line = line.split(': ', 1)
|
||||||
|
if line[0] in interesting:
|
||||||
|
info[line[0]] = line[1]
|
||||||
|
return info
|
||||||
|
|
||||||
def tags(self):
|
def tags(self):
|
||||||
tags = []
|
tags = []
|
||||||
|
for key in self.info:
|
||||||
|
if key == 'Repository UUID':
|
||||||
|
tags.append(self.info[key])
|
||||||
|
if key == 'URL':
|
||||||
|
tags.append(os.path.dirname(self.info[key]))
|
||||||
return tags
|
return tags
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
__title__ = 'wakatime'
|
__title__ = 'wakatime'
|
||||||
__version__ = '0.1.1'
|
__version__ = '0.1.2'
|
||||||
__author__ = 'Alan Hamlett'
|
__author__ = 'Alan Hamlett'
|
||||||
__license__ = 'BSD'
|
__license__ = 'BSD'
|
||||||
__copyright__ = 'Copyright 2013 Alan Hamlett'
|
__copyright__ = 'Copyright 2013 Alan Hamlett'
|
||||||
|
@ -97,10 +97,6 @@ def parseArguments():
|
||||||
args.key = default_key
|
args.key = default_key
|
||||||
else:
|
else:
|
||||||
parser.error('Missing api key')
|
parser.error('Missing api key')
|
||||||
if args.endtime and args.endtime < args.timestamp:
|
|
||||||
tmp = args.timestamp
|
|
||||||
args.timestamp = args.endtime
|
|
||||||
args.endtime = tmp
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -16,15 +16,16 @@ import sublime
|
||||||
import sublime_plugin
|
import sublime_plugin
|
||||||
|
|
||||||
|
|
||||||
# Prompt user if no activity for this many minutes
|
|
||||||
AWAY_MINUTES = 5
|
|
||||||
|
|
||||||
# globals
|
# globals
|
||||||
|
AWAY_MINUTES = 10
|
||||||
|
ACTION_FREQUENCY = 5
|
||||||
PLUGIN_DIR = dirname(realpath(__file__))
|
PLUGIN_DIR = dirname(realpath(__file__))
|
||||||
API_CLIENT = '%s/packages/wakatime/wakatime.py' % PLUGIN_DIR
|
API_CLIENT = '%s/packages/wakatime/wakatime.py' % PLUGIN_DIR
|
||||||
LAST_ACTION = 0
|
LAST_ACTION = 0
|
||||||
|
LAST_USAGE = 0
|
||||||
LAST_FILE = None
|
LAST_FILE = None
|
||||||
|
|
||||||
|
|
||||||
# To be backwards compatible, rename config file
|
# To be backwards compatible, rename config file
|
||||||
if isfile(expanduser('~/.wakatime')):
|
if isfile(expanduser('~/.wakatime')):
|
||||||
call([
|
call([
|
||||||
|
@ -35,7 +36,7 @@ if isfile(expanduser('~/.wakatime')):
|
||||||
|
|
||||||
|
|
||||||
def api(targetFile, timestamp, isWrite=False, endtime=None):
|
def api(targetFile, timestamp, isWrite=False, endtime=None):
|
||||||
global LAST_ACTION, LAST_FILE
|
global LAST_ACTION, LAST_USAGE, LAST_FILE
|
||||||
if not targetFile:
|
if not targetFile:
|
||||||
targetFile = LAST_FILE
|
targetFile = LAST_FILE
|
||||||
if targetFile:
|
if targetFile:
|
||||||
|
@ -43,6 +44,7 @@ def api(targetFile, timestamp, isWrite=False, endtime=None):
|
||||||
'--file', targetFile,
|
'--file', targetFile,
|
||||||
'--time', str('%f' % timestamp),
|
'--time', str('%f' % timestamp),
|
||||||
'--plugin', 'sublime-wakatime/%s' % __version__,
|
'--plugin', 'sublime-wakatime/%s' % __version__,
|
||||||
|
#'--verbose',
|
||||||
]
|
]
|
||||||
if isWrite:
|
if isWrite:
|
||||||
cmd.append('--write')
|
cmd.append('--write')
|
||||||
|
@ -53,53 +55,80 @@ def api(targetFile, timestamp, isWrite=False, endtime=None):
|
||||||
if endtime and endtime > LAST_ACTION:
|
if endtime and endtime > LAST_ACTION:
|
||||||
LAST_ACTION = endtime
|
LAST_ACTION = endtime
|
||||||
LAST_FILE = targetFile
|
LAST_FILE = targetFile
|
||||||
|
LAST_USAGE = LAST_ACTION
|
||||||
|
|
||||||
|
|
||||||
def away(now):
|
def away(now):
|
||||||
if LAST_ACTION == 0:
|
duration = now - LAST_USAGE
|
||||||
return False
|
units = 'seconds'
|
||||||
duration = now - LAST_ACTION
|
if duration > 59:
|
||||||
if duration > AWAY_MINUTES * 60:
|
duration = int(duration / 60)
|
||||||
duration = int(duration)
|
units = 'minutes'
|
||||||
units = 'seconds'
|
if duration > 59:
|
||||||
if duration > 59:
|
duration = int(duration / 60)
|
||||||
duration = int(duration / 60.0)
|
units = 'hours'
|
||||||
units = 'minutes'
|
if duration > 24:
|
||||||
if duration > 59:
|
duration = int(duration / 24)
|
||||||
duration = int(duration / 60.0)
|
units = 'days'
|
||||||
units = 'hours'
|
return sublime\
|
||||||
if duration > 24:
|
.ok_cancel_dialog("You were away %d %s. Add time to current file?"\
|
||||||
duration = int(duration / 24.0)
|
% (duration, units), 'Yes, log this time')
|
||||||
units = 'days'
|
|
||||||
return sublime\
|
|
||||||
.ok_cancel_dialog("You were away %d %s. Add time to current file?"\
|
|
||||||
% (duration, units), 'Yes, log this time')
|
|
||||||
|
|
||||||
|
|
||||||
def enough_time_passed(now):
|
def enough_time_passed(now):
|
||||||
return (now - LAST_ACTION >= 299)
|
if now - LAST_ACTION > ACTION_FREQUENCY * 60:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def should_prompt_user(now):
|
||||||
|
if not LAST_USAGE:
|
||||||
|
return False
|
||||||
|
duration = now - LAST_USAGE
|
||||||
|
if duration > AWAY_MINUTES * 60:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def handle_write_action(view):
|
||||||
|
now = time.time()
|
||||||
|
targetFile = view.file_name()
|
||||||
|
if enough_time_passed(now) or targetFile != LAST_FILE:
|
||||||
|
if should_prompt_user(now):
|
||||||
|
if away(now):
|
||||||
|
api(targetFile, now, endtime=LAST_ACTION, isWrite=True)
|
||||||
|
else:
|
||||||
|
api(targetFile, now, isWrite=True)
|
||||||
|
else:
|
||||||
|
api(targetFile, now, endtime=LAST_ACTION, isWrite=True)
|
||||||
|
else:
|
||||||
|
api(targetFile, now, isWrite=True)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_normal_action(view):
|
||||||
|
global LAST_USAGE
|
||||||
|
now = time.time()
|
||||||
|
targetFile = view.file_name()
|
||||||
|
if enough_time_passed(now) or targetFile != LAST_FILE:
|
||||||
|
if should_prompt_user(now):
|
||||||
|
if away(now):
|
||||||
|
api(targetFile, now, endtime=LAST_ACTION)
|
||||||
|
else:
|
||||||
|
api(targetFile, now)
|
||||||
|
else:
|
||||||
|
api(targetFile, now, endtime=LAST_ACTION)
|
||||||
|
else:
|
||||||
|
LAST_USAGE = now
|
||||||
|
|
||||||
|
|
||||||
class WakatimeListener(sublime_plugin.EventListener):
|
class WakatimeListener(sublime_plugin.EventListener):
|
||||||
|
|
||||||
def on_post_save(self, view):
|
def on_post_save(self, view):
|
||||||
api(view.file_name(), time.time(), isWrite=True)
|
handle_write_action(view)
|
||||||
|
|
||||||
def on_activated(self, view):
|
def on_activated(self, view):
|
||||||
now = time.time()
|
handle_normal_action(view)
|
||||||
targetFile = view.file_name()
|
|
||||||
if enough_time_passed(now) or targetFile != LAST_FILE:
|
|
||||||
if away(now):
|
|
||||||
api(targetFile, LAST_ACTION, endtime=now)
|
|
||||||
else:
|
|
||||||
api(targetFile, now)
|
|
||||||
|
|
||||||
def on_selection_modified(self, view):
|
def on_selection_modified(self, view):
|
||||||
now = time.time()
|
handle_normal_action(view)
|
||||||
targetFile = view.file_name()
|
|
||||||
if enough_time_passed(now) or targetFile != LAST_FILE:
|
|
||||||
if away(now):
|
|
||||||
api(targetFile, LAST_ACTION, endtime=now)
|
|
||||||
else:
|
|
||||||
api(targetFile, now)
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue