version 0.2.1. updated to new action api scheme.

This commit is contained in:
Alan Hamlett 2013-07-07 21:25:06 -07:00
parent 61370df1ba
commit 0b07cb21bd
16 changed files with 3125 additions and 308 deletions

35
.gitignore vendored Normal file
View file

@ -0,0 +1,35 @@
*.py[cod]
# C extensions
*.so
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject

View file

@ -1,8 +1,9 @@
vim-wakatime 0.1.2 vim-wakatime 0.2.1
=========== ==================
Automatic time tracking for Vim. Automatic time tracking for Vim.
Installation Installation
------------ ------------
@ -12,7 +13,7 @@ https://wakati.me
2) Run this shell command replacing KEY with your api key: 2) Run this shell command replacing KEY with your api key:
echo "api_key=KEY" >> ~/.wakatime echo "api_key=KEY" >> ~/.wakatime.conf
3) Using [Vundle](https://github.com/gmarik/vundle), the Vim plugin manager: 3) Using [Vundle](https://github.com/gmarik/vundle), the Vim plugin manager:
@ -26,6 +27,7 @@ Then inside Vim, type `:BundleInstall` or run this shell command:
Visit https://wakati.me to view your time spent in each file. Visit https://wakati.me to view your time spent in each file.
Screen Shots Screen Shots
------------ ------------

View file

View file

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
"""
wakatime.log
~~~~~~~~~~~~
Provides the configured logger for writing JSON to the log file.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import json
import logging
import os
try:
from collections import OrderedDict
except ImportError:
from .packages.ordereddict import OrderedDict
class CustomEncoder(json.JSONEncoder):
def default(self, obj):
return super(CustomEncoder, self).default(obj)
class JsonFormatter(logging.Formatter):
def __init__(self, timestamp, endtime, isWrite, targetFile, version,
plugin, datefmt=None):
self.timestamp = timestamp
self.endtime = endtime
self.isWrite = isWrite
self.targetFile = targetFile
self.version = version
self.plugin = plugin
super(JsonFormatter, self).__init__(datefmt=datefmt)
def format(self, record):
data = OrderedDict([
('now', self.formatTime(record, self.datefmt)),
('version', self.version),
('plugin', self.plugin),
('time', self.timestamp),
('endtime', self.endtime),
('isWrite', self.isWrite),
('file', self.targetFile),
('level', record.levelname),
('message', record.msg),
])
if not self.endtime:
del data['endtime']
if not self.plugin:
del data['plugin']
if not self.isWrite:
del data['isWrite']
return CustomEncoder().encode(data)
def formatException(self, exc_info):
return exec_info[2].format_exc()
def setup_logging(args, version):
logfile = args.logfile
if not logfile:
logfile = '~/.wakatime.log'
handler = logging.FileHandler(os.path.expanduser(logfile))
formatter = JsonFormatter(
timestamp=args.timestamp,
endtime=args.endtime,
isWrite=args.isWrite,
targetFile=args.targetFile,
version=version,
plugin=args.plugin,
datefmt='%Y-%m-%dT%H:%M:%SZ',
)
handler.setFormatter(formatter)
logger = logging.getLogger()
logger.addHandler(handler)
level = logging.INFO
if args.verbose:
level = logging.DEBUG
logger.setLevel(level)
return logger

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,127 @@
# Copyright (c) 2009 Raymond Hettinger
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
from UserDict import DictMixin
class OrderedDict(dict, DictMixin):
def __init__(self, *args, **kwds):
if len(args) > 1:
raise TypeError('expected at most 1 arguments, got %d' % len(args))
try:
self.__end
except AttributeError:
self.clear()
self.update(*args, **kwds)
def clear(self):
self.__end = end = []
end += [None, end, end] # sentinel node for doubly linked list
self.__map = {} # key --> [key, prev, next]
dict.clear(self)
def __setitem__(self, key, value):
if key not in self:
end = self.__end
curr = end[1]
curr[2] = end[1] = self.__map[key] = [key, curr, end]
dict.__setitem__(self, key, value)
def __delitem__(self, key):
dict.__delitem__(self, key)
key, prev, next = self.__map.pop(key)
prev[2] = next
next[1] = prev
def __iter__(self):
end = self.__end
curr = end[2]
while curr is not end:
yield curr[0]
curr = curr[2]
def __reversed__(self):
end = self.__end
curr = end[1]
while curr is not end:
yield curr[0]
curr = curr[1]
def popitem(self, last=True):
if not self:
raise KeyError('dictionary is empty')
if last:
key = reversed(self).next()
else:
key = iter(self).next()
value = self.pop(key)
return key, value
def __reduce__(self):
items = [[k, self[k]] for k in self]
tmp = self.__map, self.__end
del self.__map, self.__end
inst_dict = vars(self).copy()
self.__map, self.__end = tmp
if inst_dict:
return (self.__class__, (items,), inst_dict)
return self.__class__, (items,)
def keys(self):
return list(self)
setdefault = DictMixin.setdefault
update = DictMixin.update
pop = DictMixin.pop
values = DictMixin.values
items = DictMixin.items
iterkeys = DictMixin.iterkeys
itervalues = DictMixin.itervalues
iteritems = DictMixin.iteritems
def __repr__(self):
if not self:
return '%s()' % (self.__class__.__name__,)
return '%s(%r)' % (self.__class__.__name__, self.items())
def copy(self):
return self.__class__(self)
@classmethod
def fromkeys(cls, iterable, value=None):
d = cls()
for key in iterable:
d[key] = value
return d
def __eq__(self, other):
if isinstance(other, OrderedDict):
if len(self) != len(other):
return False
for p, q in zip(self.items(), other.items()):
if p != q:
return False
return True
return dict.__eq__(self, other)
def __ne__(self, other):
return not self == other

View file

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
"""
wakatime.project
~~~~~~~~~~~~~~~~
Returns a project for the given file.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import logging
import os
from .projects.base import BaseProject
from .projects.git import Git
from .projects.mercurial import Mercurial
from .projects.subversion import Subversion
log = logging.getLogger(__name__)
PLUGINS = [
Git,
Mercurial,
Subversion,
]
def find_project(path):
for plugin in PLUGINS:
project = plugin(path)
if project.config:
return project
return BaseProject(path)

View file

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
"""
wakatime.projects.base
~~~~~~~~~~~~~~~~~~~~~~
Base project for use when no other project can be found.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import logging
import os
log = logging.getLogger(__name__)
class BaseProject():
def __init__(self, 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):
type = self.__class__.__name__.lower()
if type == 'baseproject':
type = None
return type
def base(self):
if self.config:
return os.path.dirname(self.config)
return None
def tags(self):
tags = []
return tags
def findConfig(self, path):
return ''

View file

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
"""
wakatime.projects.git
~~~~~~~~~~~~~~~~~~~~~
Information about the git project for a given file.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import logging
import os
from .base import BaseProject
try:
from collections import OrderedDict
except ImportError:
from ..packages.ordereddict import OrderedDict
log = logging.getLogger(__name__)
class Git(BaseProject):
def base(self):
if self.config:
return os.path.dirname(os.path.dirname(self.config))
return None
def tags(self):
tags = []
if self.config:
sections = self.parseConfig()
for section in sections:
if section.split(' ', 1)[0] == 'remote' and 'url' in sections[section]:
tags.append(sections[section]['url'])
return tags
def findConfig(self, 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 self.findConfig(split_path[0])
def parseConfig(self):
sections = {}
try:
f = open(self.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] = {}
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

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
"""
wakatime.projects.mercurial
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Information about the mercurial project for a given file.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import logging
import os
from .base import BaseProject
log = logging.getLogger(__name__)
class Mercurial(BaseProject):
def base(self):
return super(Mercurial, self).base()
def tags(self):
tags = []
return tags

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
"""
wakatime.projects.subversion
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Information about the svn project for a given file.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import logging
import os
from .base import BaseProject
log = logging.getLogger(__name__)
class Subversion(BaseProject):
def base(self):
return super(Subversion, self).base()
def tags(self):
tags = []
return tags

View file

@ -0,0 +1,200 @@
# -*- coding: utf-8 -*-
"""
wakatime
~~~~~~~~
Action event appender for Wakati.Me, a time tracking api for text editors.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
from __future__ import print_function
__title__ = 'wakatime'
__version__ = '0.1.1'
__author__ = 'Alan Hamlett'
__license__ = 'BSD'
__copyright__ = 'Copyright 2013 Alan Hamlett'
# allow running script directly
if __name__ == '__main__' and __package__ is None:
import os, sys
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parent_dir)
import wakatime
__package__ = 'wakatime'
del os, sys
import base64
import json
import logging
import os
import platform
import re
import sys
import time
import traceback
import urllib2
from .log import setup_logging
from .project import find_project
try:
import argparse
except ImportError:
from .packages import argparse
log = logging.getLogger(__name__)
class FileAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
values = os.path.realpath(values)
setattr(namespace, self.dest, values)
def parseArguments():
parser = argparse.ArgumentParser(
description='Wakati.Me event api appender')
parser.add_argument('--file', dest='targetFile', metavar='file',
action=FileAction, required=True,
help='absolute path to file for current action')
parser.add_argument('--time', dest='timestamp', metavar='time',
type=float,
help='optional floating-point unix epoch timestamp; '+
'uses current time by default')
parser.add_argument('--endtime', dest='endtime',
help='optional end timestamp turning this action into '+
'a duration; if a non-duration action occurs within a '+
'duration, the duration is ignored')
parser.add_argument('--write', dest='isWrite',
action='store_true',
help='note action was triggered from writing to a file')
parser.add_argument('--plugin', dest='plugin',
help='optional text editor plugin name and version '+
'for User-Agent header')
parser.add_argument('--key', dest='key',
help='your wakati.me api key; uses api_key from '+
'~/.wakatime.conf by default')
parser.add_argument('--logfile', dest='logfile',
help='defaults to ~/.wakatime.log')
parser.add_argument('--config', dest='config',
help='defaults to ~/.wakatime.conf')
parser.add_argument('--verbose', dest='verbose', action='store_true',
help='turns on debug messages in log file')
parser.add_argument('--version', action='version', version=__version__)
args = parser.parse_args()
if not args.timestamp:
args.timestamp = time.time()
if not args.key:
default_key = get_api_key(args.config)
if default_key:
args.key = default_key
else:
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
def get_api_key(configFile):
if not configFile:
configFile = '~/.wakatime.conf'
api_key = None
try:
cf = open(os.path.expanduser(configFile))
for line in cf:
line = line.split('=', 1)
if line[0] == 'api_key':
api_key = line[1].strip()
cf.close()
except IOError:
print('Error: Could not read from config file.')
return api_key
def get_user_agent(plugin):
user_agent = 'wakatime/%s (%s)' % (__version__, platform.platform())
if plugin:
user_agent = user_agent+' '+plugin
return user_agent
def send_action(project=None, tags=None, key=None, targetFile=None,
timestamp=None, endtime=None, isWrite=None, plugin=None, **kwargs):
url = 'https://www.wakati.me/api/v1/actions'
log.debug('Sending action to api at %s' % url)
data = {
'time': timestamp,
'file': targetFile,
}
if endtime:
data['endtime'] = endtime
if isWrite:
data['isWrite'] = isWrite
if project:
data['project'] = project
if tags:
data['tags'] = tags
log.debug(data)
request = urllib2.Request(url=url, data=json.dumps(data))
user_agent = get_user_agent(plugin)
request.add_header('User-Agent', user_agent)
request.add_header('Content-Type', 'application/json')
request.add_header('Authorization', 'Basic %s' % base64.b64encode(key))
response = None
try:
response = urllib2.urlopen(request)
except urllib2.HTTPError as exc:
data = {
'response_code': exc.getcode(),
'response_content': exc.read(),
sys.exc_info()[0].__name__: str(sys.exc_info()[1]),
}
if log.isEnabledFor(logging.DEBUG):
data['traceback'] = traceback.format_exc()
log.error(data)
except:
data = {
sys.exc_info()[0].__name__: str(sys.exc_info()[1]),
}
if log.isEnabledFor(logging.DEBUG):
data['traceback'] = traceback.format_exc()
log.error(data)
else:
if response.getcode() >= 200 and response.getcode() < 300:
log.debug({
'response_code': response.getcode(),
'response_content': response.read(),
})
return True
log.error({
'response_code': response.getcode(),
'response_content': response.read(),
})
return False
def main():
args = parseArguments()
setup_logging(args, __version__)
if os.path.isfile(args.targetFile):
project = find_project(args.targetFile)
tags = project.tags()
if send_action(project=project.name(), tags=tags, **vars(args)):
return 0
return 102
else:
log.debug('File does not exist; ignoring this action.')
return 101
if __name__ == '__main__':
sys.exit(main())

View file

@ -1,185 +0,0 @@
#!/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.3'
user_agent = 'vim-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:]))

View file

@ -2,7 +2,7 @@
" File: wakatime.vim " File: wakatime.vim
" Description: Automatic time tracking for Vim. " Description: Automatic time tracking for Vim.
" Maintainer: Wakati.Me <support@wakatime.com> " Maintainer: Wakati.Me <support@wakatime.com>
" Version: 0.1.3 " Version: 0.2.1
" ============================================================================ " ============================================================================
@ -20,21 +20,6 @@
finish finish
endif endif
" Check for required user-defined settings
if !exists("g:wakatime_api_key")
if filereadable(expand("$HOME/.wakatime"))
for s:line in readfile(expand("$HOME/.wakatime"))
let s:setting = split(s:line, "=")
if s:setting[0] == "api_key"
let g:wakatime_api_key = s:setting[1]
endif
endfor
endif
if !exists("g:wakatime_api_key")
finish
endif
endif
" Only load plugin once " Only load plugin once
if exists("g:loaded_wakatime") if exists("g:loaded_wakatime")
finish finish
@ -45,38 +30,24 @@
let s:old_cpo = &cpo let s:old_cpo = &cpo
set cpo&vim set cpo&vim
let s:plugin_directory = expand("<sfile>:p:h") " Set default away minutes
if !exists("g:wakatime_AwayMinutes")
" Set default updatetime let g:wakatime_AwayMinutes = 10
if !exists("g:wakatime_updatetime")
let g:wakatime_updatetime = 15 " 15 minutes
endif endif
" We are not away until getting a CursorHold event " Globals
let s:away_start = 0 let s:plugin_directory = expand("<sfile>:p:h") . '/'
let s:last_action = 0
" Create logfile if does not exist
exec "silent !touch ~/.wakatime.log"
" Import things python needs
python import time python import time
python import time
python import uuid
python import vim python import vim
python instance_id = str(uuid.uuid4())
python vim.command('let s:instance_id = "%s"' % instance_id)
" }}} " }}}
" Function Definitions {{{ " Function Definitions {{{
function! s:setUpdateTime()
if &updatetime < 60 * 1000 * 2
let &updatetime = g:wakatime_updatetime * 60 * 1000
endif
endfunction
call s:setUpdateTime()
function! s:GetCurrentFile() function! s:GetCurrentFile()
return expand("%:p") return expand("%:p")
endfunction endfunction
@ -86,10 +57,41 @@
return current_time return current_time
endfunction endfunction
function! s:api(type, task, time) function! s:api(targetFile, time, endtime, is_write, last)
if a:task != '' let targetFile = a:targetFile
exec "silent !python " . s:plugin_directory . "/wakatime.py --key" g:wakatime_api_key "--instance" s:instance_id "--action" a:type "--task" shellescape(a:task) "--time" a:time . " &" if targetFile == ''
let targetFile = a:last[1]
endif endif
if targetFile != ''
let extras = ''
if a:is_write
let extras = extras . '--write'
endif
if a:endtime
let extras = extras . '--endtime ' . a:endtime
endif
exec "silent !python" s:plugin_directory . "packages/wakatime/wakatime.py --verbose --file" shellescape(targetFile) "--time" a:time extras . " &"
let time = a:time
if a:endtime && time < a:endtime
let time = a:endtime
endif
call s:SetLastAction(time, targetFile)
endif
endfunction
function! s:GetLastAction()
if !filereadable(expand("$HOME/.wakatime.data"))
return [0, '']
endif
let last = readfile(expand("$HOME/.wakatime.data"), '', 2)
if len(last) != 2
return [0, '']
endif
return [str2float(last[0]), last[1]]
endfunction
function! s:SetLastAction(time, targetFile)
call writefile([a:time, a:targetFile], expand("$HOME/.wakatime.data"))
endfunction endfunction
function! s:getchar() function! s:getchar()
@ -100,22 +102,39 @@
return c return c
endfunction endfunction
function! Wakatime_isAway() function! s:enoughTimePassed(now, prev)
return s:away_start if a:now - a:prev >= 299
return 1
endif
return 0
endfunction endfunction
function! s:allServersAway() function! s:away(now, last)
if has('clientserver') if a:last[0] < 1
let servers = split(serverlist())
for server in servers
if server != v:servername
if !remote_expr(server,'Wakatime_isAway()')
return 0 return 0
endif endif
let duration = a:now - a:last[0]
if duration > g:wakatime_AwayMinutes * 60
let units = 'seconds'
if duration > 59
let duration = round(duration / 60)
let units = 'minutes'
endif endif
endfor if duration > 59
let duration = round(duration / 60)
let units = 'hours'
endif endif
if duration > 24
let duration = round(duration / 24)
let units = 'days'
endif
let answer = input(printf("You were away %.f %s. Add time to current file? (y/n)", duration, units))
if answer != "y"
return 1 return 1
endif
return 0
endif
endfunction endfunction
" }}} " }}}
@ -123,66 +142,21 @@
" Event Handlers {{{ " Event Handlers {{{
function! s:bufenter() function! s:normalAction()
call s:api("open_file", s:GetCurrentFile(), s:GetCurrentTime()) let targetFile = s:GetCurrentFile()
endfunction let now = s:GetCurrentTime()
let last = s:GetLastAction()
function! s:bufleave() if s:enoughTimePassed(now, last[0]) || targetFile != last[1]
call s:api("close_file", s:GetCurrentFile(), s:GetCurrentTime()) if s:away(now, last)
endfunction call s:api(targetFile, last[0], now, 0, last)
function! s:vimenter()
call s:api("open_editor", s:GetCurrentFile(), s:GetCurrentTime())
endfunction
function! s:vimleave()
call s:api("quit_editor", s:GetCurrentFile(), s:GetCurrentTime())
endfunction
function! s:bufwrite()
call s:api("write_file", s:GetCurrentFile(), s:GetCurrentTime())
endfunction
function! s:cursorhold()
let s:away_task = s:GetCurrentFile()
python vim.command("let s:away_start=%f" % (time.time() - (float(vim.eval("&updatetime")) / 1000.0)))
autocmd Wakatime CursorMoved,CursorMovedI * call s:cursormoved()
endfunction
function! s:cursormoved()
autocmd! Wakatime CursorMoved,CursorMovedI *
" Don't do anything unless all other Vim instances are also away
if !s:allServersAway()
let s:away_start = 0
call s:api("ping", s:away_task, s:GetCurrentTime())
return
endif
python vim.command("let away_end=%f" % time.time())
let away_unit = "minutes"
let away_duration = (away_end - s:away_start) / 60
if away_duration < 2
call s:setUpdateTime()
return
endif
if away_duration > 59
let away_duration = away_duration / 60
let away_unit = "hours"
endif
if away_duration > 59
let away_duration = away_duration / 60
let away_unit = "days"
endif
let answer = input(printf("You were away %.f %s. Add time to current file? (y/n)", away_duration, away_unit))
if answer != "y"
call s:api("minimize_editor", s:away_task, printf("%f", s:away_start))
call s:api("maximize_editor", s:away_task, printf("%f", away_end))
else else
call s:api("ping", s:away_task, s:GetCurrentTime()) call s:api(targetFile, now, 0, 0, last)
endif endif
let s:away_start = 0 endif
"redraw! endfunction
function! s:writeAction()
call s:api(s:GetCurrentFile(), s:GetCurrentTime(), 0, 1, s:GetLastAction())
endfunction endfunction
" }}} " }}}
@ -192,12 +166,10 @@
augroup Wakatime augroup Wakatime
autocmd! autocmd!
autocmd BufEnter * call s:bufenter() autocmd BufEnter * call s:normalAction()
autocmd BufLeave * call s:bufleave() autocmd VimEnter * call s:normalAction()
autocmd VimEnter * call s:vimenter() autocmd BufWritePost * call s:writeAction()
autocmd VimLeave * call s:vimleave() autocmd CursorMoved,CursorMovedI * call s:normalAction()
autocmd BufWritePost * call s:bufwrite()
autocmd CursorHold,CursorHoldI * call s:cursorhold()
augroup END augroup END
" }}} " }}}