2013-07-08 01:38:01 +00:00
""" ==========================================================
2013-08-05 05:30:24 +00:00
File : WakaTime . py
2013-07-08 01:38:01 +00:00
Description : Automatic time tracking for Sublime Text 2 and 3.
2014-03-05 21:55:08 +00:00
Maintainer : WakaTime < support @wakatime.com >
2014-03-13 00:03:14 +00:00
License : BSD , see LICENSE for more details .
2014-03-14 20:36:20 +00:00
Website : https : / / wakatime . com /
2013-07-08 01:38:01 +00:00
== == == == == == == == == == == == == == == == == == == == == == == == == == == == == = """
2013-07-02 09:21:13 +00:00
2015-03-20 08:16:34 +00:00
__version__ = ' 3.0.9 '
2013-07-02 09:21:13 +00:00
2013-07-23 19:09:58 +00:00
import sublime
import sublime_plugin
2013-07-23 22:18:31 +00:00
import glob
2013-08-05 05:11:22 +00:00
import os
2013-07-23 19:09:58 +00:00
import platform
2013-08-08 09:17:47 +00:00
import sys
2013-07-02 09:21:13 +00:00
import time
2013-08-08 09:17:47 +00:00
import threading
2015-02-24 06:20:27 +00:00
import webbrowser
2015-03-20 08:14:53 +00:00
from datetime import datetime
2015-03-09 21:32:27 +00:00
from os . path import expanduser , dirname , basename , realpath , join
2013-07-02 09:21:13 +00:00
2013-07-08 01:38:01 +00:00
# globals
2013-11-28 11:16:43 +00:00
ACTION_FREQUENCY = 2
2013-08-08 09:17:47 +00:00
ST_VERSION = int ( sublime . version ( ) )
2013-07-02 09:21:13 +00:00
PLUGIN_DIR = dirname ( realpath ( __file__ ) )
2015-03-09 22:23:29 +00:00
API_CLIENT = ' %s /packages/wakatime/cli.py ' % PLUGIN_DIR
2013-08-08 09:17:47 +00:00
SETTINGS_FILE = ' WakaTime.sublime-settings '
SETTINGS = { }
2013-11-28 11:16:43 +00:00
LAST_ACTION = {
' time ' : 0 ,
' file ' : None ,
' is_write ' : False ,
}
2013-08-13 03:30:06 +00:00
HAS_SSL = False
2013-08-14 09:36:17 +00:00
LOCK = threading . RLock ( )
2015-03-09 21:32:27 +00:00
PYTHON_LOCATION = None
2013-07-08 01:38:01 +00:00
2014-12-24 18:01:56 +00:00
# add wakatime package to path
2015-03-09 22:23:29 +00:00
sys . path . insert ( 0 , join ( PLUGIN_DIR , ' packages ' ) )
2014-12-24 18:01:56 +00:00
2013-08-09 01:52:45 +00:00
# check if we have SSL support
2013-08-08 09:41:29 +00:00
try :
import ssl
2013-09-30 13:58:12 +00:00
import socket
2015-02-24 06:20:27 +00:00
assert ssl
assert socket . ssl
2013-08-08 09:41:29 +00:00
HAS_SSL = True
2013-09-30 13:58:12 +00:00
except ( ImportError , AttributeError ) :
2013-09-22 23:15:43 +00:00
from subprocess import Popen
2013-08-08 09:17:47 +00:00
2013-09-22 23:15:43 +00:00
if HAS_SSL :
2014-12-24 18:01:56 +00:00
# import wakatime package so we can use built-in python
2013-08-13 03:30:06 +00:00
import wakatime
2013-08-08 09:17:47 +00:00
2014-12-24 18:01:56 +00:00
def createConfigFile ( ) :
""" Creates the .wakatime.cfg INI file in $HOME directory, if it does
not already exist .
"""
configFile = os . path . join ( os . path . expanduser ( ' ~ ' ) , ' .wakatime.cfg ' )
try :
2014-12-27 00:18:52 +00:00
with open ( configFile ) as fh :
2014-12-24 18:01:56 +00:00
pass
except IOError :
try :
2014-12-27 00:18:52 +00:00
with open ( configFile , ' w ' ) as fh :
2014-12-24 18:01:56 +00:00
fh . write ( " [settings] \n " )
fh . write ( " debug = false \n " )
fh . write ( " hidefilenames = false \n " )
except IOError :
pass
2013-08-14 09:36:17 +00:00
def prompt_api_key ( ) :
2013-08-08 09:17:47 +00:00
global SETTINGS
2014-12-24 18:01:56 +00:00
createConfigFile ( )
default_key = ' '
2015-03-09 22:23:29 +00:00
try :
from wakatime . base import parseConfigFile
configs = parseConfigFile ( )
if configs is not None :
if configs . has_option ( ' settings ' , ' api_key ' ) :
default_key = configs . get ( ' settings ' , ' api_key ' )
except :
pass
2014-12-24 18:01:56 +00:00
2014-06-18 17:10:28 +00:00
if SETTINGS . get ( ' api_key ' ) :
return True
else :
2013-08-05 05:11:22 +00:00
def got_key ( text ) :
if text :
2013-08-14 09:36:17 +00:00
SETTINGS . set ( ' api_key ' , str ( text ) )
2013-08-08 09:17:47 +00:00
sublime . save_settings ( SETTINGS_FILE )
2013-08-07 00:21:32 +00:00
window = sublime . active_window ( )
2013-08-08 09:17:47 +00:00
if window :
2014-12-24 18:01:56 +00:00
window . show_input_panel ( ' [WakaTime] Enter your wakatime.com api key: ' , default_key , got_key , None , None )
2013-08-14 09:36:17 +00:00
return True
2013-07-23 22:10:19 +00:00
else :
2013-12-13 15:08:34 +00:00
print ( ' [WakaTime] Error: Could not prompt for api key because no window found. ' )
2013-08-14 09:36:17 +00:00
return False
2013-07-10 07:14:44 +00:00
2013-08-09 01:52:45 +00:00
2013-08-08 09:41:29 +00:00
def python_binary ( ) :
2015-03-09 21:32:27 +00:00
global PYTHON_LOCATION
if PYTHON_LOCATION is not None :
return PYTHON_LOCATION
paths = [
" pythonw " ,
" python " ,
" /usr/local/bin/python " ,
" /usr/bin/python " ,
]
for path in paths :
2013-08-08 09:41:29 +00:00
try :
2015-03-09 21:32:27 +00:00
Popen ( [ path , ' --version ' ] )
PYTHON_LOCATION = path
return path
2013-08-08 09:41:29 +00:00
except :
2015-03-09 21:32:27 +00:00
pass
for path in glob . iglob ( ' /python* ' ) :
path = realpath ( join ( path , ' pythonw ' ) )
try :
Popen ( [ path , ' --version ' ] )
PYTHON_LOCATION = path
return path
except :
pass
return None
2013-07-10 07:14:44 +00:00
2013-08-09 01:52:45 +00:00
2013-11-28 11:16:43 +00:00
def enough_time_passed ( now , last_time ) :
if now - last_time > ACTION_FREQUENCY * 60 :
2013-07-10 07:14:44 +00:00
return True
return False
2014-06-18 17:35:59 +00:00
def find_project_name_from_folders ( folders ) :
2015-02-06 01:40:45 +00:00
try :
for folder in folders :
for file_name in os . listdir ( folder ) :
if file_name . endswith ( ' .sublime-project ' ) :
return file_name . replace ( ' .sublime-project ' , ' ' , 1 )
except :
pass
2014-06-18 17:35:59 +00:00
return None
2013-10-28 04:30:10 +00:00
def handle_action ( view , is_write = False ) :
2015-03-20 08:14:53 +00:00
target_file = view . file_name ( )
project = view . window ( ) . project_file_name ( ) if hasattr ( view . window ( ) , ' project_file_name ' ) else None
thread = SendActionThread ( target_file , view , is_write = is_write , project = project , folders = view . window ( ) . folders ( ) )
thread . start ( )
2013-08-08 09:17:47 +00:00
class SendActionThread ( threading . Thread ) :
2015-03-20 08:14:53 +00:00
def __init__ ( self , target_file , view , is_write = False , project = None , folders = None , force = False ) :
2013-08-08 09:17:47 +00:00
threading . Thread . __init__ ( self )
2015-03-20 08:14:53 +00:00
self . lock = LOCK
2013-10-28 04:30:10 +00:00
self . target_file = target_file
self . is_write = is_write
2014-06-18 17:10:28 +00:00
self . project = project
2014-06-18 17:35:59 +00:00
self . folders = folders
2013-08-08 09:17:47 +00:00
self . force = force
2013-08-14 08:49:08 +00:00
self . debug = SETTINGS . get ( ' debug ' )
2013-08-14 09:36:17 +00:00
self . api_key = SETTINGS . get ( ' api_key ' , ' ' )
2013-10-28 04:30:10 +00:00
self . ignore = SETTINGS . get ( ' ignore ' , [ ] )
2015-03-20 08:14:53 +00:00
self . last_action = LAST_ACTION . copy ( )
self . view = view
2013-08-08 09:17:47 +00:00
def run ( self ) :
2015-03-20 08:14:53 +00:00
with self . lock :
if self . target_file :
self . timestamp = time . time ( )
if self . force or ( self . is_write and not self . last_action [ ' is_write ' ] ) or self . target_file != self . last_action [ ' file ' ] or enough_time_passed ( self . timestamp , self . last_action [ ' time ' ] ) :
self . send_heartbeat ( )
2013-08-08 09:17:47 +00:00
2015-03-20 08:14:53 +00:00
def send_heartbeat ( self ) :
2013-08-14 08:49:08 +00:00
if not self . api_key :
2013-12-13 15:08:34 +00:00
print ( ' [WakaTime] Error: missing api key. ' )
2013-08-08 09:17:47 +00:00
return
2013-09-05 14:39:33 +00:00
ua = ' sublime/ %d sublime-wakatime/ %s ' % ( ST_VERSION , __version__ )
2013-08-08 09:17:47 +00:00
cmd = [
API_CLIENT ,
2013-10-28 04:30:10 +00:00
' --file ' , self . target_file ,
2013-08-08 09:17:47 +00:00
' --time ' , str ( ' %f ' % self . timestamp ) ,
2013-09-05 14:39:33 +00:00
' --plugin ' , ua ,
2013-08-14 08:49:08 +00:00
' --key ' , str ( bytes . decode ( self . api_key . encode ( ' utf8 ' ) ) ) ,
2013-08-08 09:17:47 +00:00
]
2013-10-28 04:30:10 +00:00
if self . is_write :
2013-08-08 09:17:47 +00:00
cmd . append ( ' --write ' )
2015-03-20 08:14:53 +00:00
if self . project :
self . project = basename ( self . project ) . replace ( ' .sublime-project ' , ' ' , 1 )
2014-06-18 17:10:28 +00:00
if self . project :
cmd . extend ( [ ' --project ' , self . project ] )
2014-06-18 17:35:59 +00:00
elif self . folders :
project_name = find_project_name_from_folders ( self . folders )
if project_name :
cmd . extend ( [ ' --project ' , project_name ] )
2013-10-28 04:30:10 +00:00
for pattern in self . ignore :
cmd . extend ( [ ' --ignore ' , pattern ] )
2013-08-14 08:49:08 +00:00
if self . debug :
2013-08-09 01:52:45 +00:00
cmd . append ( ' --verbose ' )
2013-08-13 03:30:06 +00:00
if HAS_SSL :
2015-03-09 22:25:40 +00:00
if self . debug :
print ( ' [WakaTime] %s ' % ' ' . join ( cmd ) )
2013-09-30 14:04:24 +00:00
code = wakatime . main ( cmd )
if code != 0 :
2013-12-13 15:08:34 +00:00
print ( ' [WakaTime] Error: Response code %d from wakatime package. ' % code )
2015-03-20 08:14:53 +00:00
else :
self . sent ( )
2013-08-13 03:30:06 +00:00
else :
2013-10-13 04:52:40 +00:00
python = python_binary ( )
2013-10-13 15:26:31 +00:00
if python :
2013-10-13 04:52:40 +00:00
cmd . insert ( 0 , python )
2015-03-09 22:25:40 +00:00
if self . debug :
print ( ' [WakaTime] %s %s ' % ( python , ' ' . join ( cmd ) ) )
2013-10-13 04:52:40 +00:00
if platform . system ( ) == ' Windows ' :
Popen ( cmd , shell = False )
else :
with open ( join ( expanduser ( ' ~ ' ) , ' .wakatime.log ' ) , ' a ' ) as stderr :
Popen ( cmd , stderr = stderr )
2015-03-20 08:14:53 +00:00
self . sent ( )
2013-10-13 15:26:31 +00:00
else :
2013-12-13 15:08:34 +00:00
print ( ' [WakaTime] Error: Unable to find python binary. ' )
2013-08-08 09:17:47 +00:00
2015-03-20 08:14:53 +00:00
def sent ( self ) :
sublime . set_timeout ( lambda : self . view . set_status ( ' wakatime ' , ' WakaTime heartbeat sent {0} ' . format ( datetime . now ( ) . strftime ( ' % A % b %d % Y at % I: % M % p % Z ' ) ) ) , 0 )
sublime . set_timeout ( self . set_last_action , 0 )
def set_last_action ( self ) :
global LAST_ACTION
LAST_ACTION = {
' file ' : self . target_file ,
' time ' : self . timestamp ,
' is_write ' : self . is_write ,
}
2013-08-08 09:17:47 +00:00
def plugin_loaded ( ) :
2013-10-28 04:30:10 +00:00
global SETTINGS
2014-06-18 17:10:28 +00:00
print ( ' [WakaTime] Initializing WakaTime plugin v %s ' % __version__ )
2014-10-14 18:02:52 +00:00
if not HAS_SSL :
python = python_binary ( )
if not python :
sublime . error_message ( " Unable to find Python binary! \n WakaTime needs Python to work correctly. \n \n Go to https://www.python.org/downloads " )
return
2013-10-28 04:30:10 +00:00
SETTINGS = sublime . load_settings ( SETTINGS_FILE )
2013-08-14 09:36:17 +00:00
after_loaded ( )
def after_loaded ( ) :
if not prompt_api_key ( ) :
sublime . set_timeout ( after_loaded , 500 )
2013-08-08 09:17:47 +00:00
# need to call plugin_loaded because only ST3 will auto-call it
if ST_VERSION < 3000 :
plugin_loaded ( )
2013-07-02 09:21:13 +00:00
class WakatimeListener ( sublime_plugin . EventListener ) :
def on_post_save ( self , view ) :
2013-10-28 04:30:10 +00:00
handle_action ( view , is_write = True )
2013-07-02 09:21:13 +00:00
def on_activated ( self , view ) :
2013-10-28 04:30:10 +00:00
handle_action ( view )
2013-07-08 01:38:01 +00:00
2013-07-16 11:02:30 +00:00
def on_modified ( self , view ) :
2013-10-28 04:30:10 +00:00
handle_action ( view )
2015-02-23 22:59:10 +00:00
class WakatimeDashboardCommand ( sublime_plugin . ApplicationCommand ) :
2015-02-24 06:20:27 +00:00
2015-02-23 22:59:10 +00:00
def run ( self ) :
webbrowser . open_new_tab ( ' https://wakatime.com/dashboard ' )