""" ========================================================== File: WakaTime.py Description: Automatic time tracking for Sublime Text 2 and 3. Maintainer: WakaTime License: BSD, see LICENSE for more details. Website: https://wakatime.com/ ===========================================================""" __version__ = '3.0.13' import sublime import sublime_plugin import glob import os import platform import sys import time import threading import webbrowser from datetime import datetime from os.path import expanduser, dirname, basename, realpath, join # globals ACTION_FREQUENCY = 2 ST_VERSION = int(sublime.version()) PLUGIN_DIR = dirname(realpath(__file__)) API_CLIENT = '%s/packages/wakatime/cli.py' % PLUGIN_DIR SETTINGS_FILE = 'WakaTime.sublime-settings' SETTINGS = {} LAST_ACTION = { 'time': 0, 'file': None, 'is_write': False, } HAS_SSL = False LOCK = threading.RLock() PYTHON_LOCATION = None # add wakatime package to path sys.path.insert(0, join(PLUGIN_DIR, 'packages')) # check if we have SSL support try: import ssl import socket assert ssl assert socket.ssl assert ssl.OPENSSL_VERSION HAS_SSL = True except (ImportError, AttributeError): from subprocess import Popen if HAS_SSL: # import wakatime package so we can use built-in python import wakatime 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: with open(configFile) as fh: pass except IOError: try: with open(configFile, 'w') as fh: fh.write("[settings]\n") fh.write("debug = false\n") fh.write("hidefilenames = false\n") except IOError: pass def prompt_api_key(): global SETTINGS createConfigFile() default_key = '' 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 if SETTINGS.get('api_key'): return True else: def got_key(text): if text: SETTINGS.set('api_key', str(text)) sublime.save_settings(SETTINGS_FILE) window = sublime.active_window() if window: window.show_input_panel('[WakaTime] Enter your wakatime.com api key:', default_key, got_key, None, None) return True else: print('[WakaTime] Error: Could not prompt for api key because no window found.') return False def python_binary(): 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: try: Popen([path, '--version']) PYTHON_LOCATION = path return path except: 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 def enough_time_passed(now, last_time): if now - last_time > ACTION_FREQUENCY * 60: return True return False def find_project_name_from_folders(folders): 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 return None def handle_action(view, is_write=False): 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() class SendActionThread(threading.Thread): def __init__(self, target_file, view, is_write=False, project=None, folders=None, force=False): threading.Thread.__init__(self) self.lock = LOCK self.target_file = target_file self.is_write = is_write self.project = project self.folders = folders self.force = force self.debug = SETTINGS.get('debug') self.api_key = SETTINGS.get('api_key', '') self.ignore = SETTINGS.get('ignore', []) self.last_action = LAST_ACTION.copy() self.view = view def run(self): 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() def send_heartbeat(self): if not self.api_key: print('[WakaTime] Error: missing api key.') return ua = 'sublime/%d sublime-wakatime/%s' % (ST_VERSION, __version__) cmd = [ API_CLIENT, '--file', self.target_file, '--time', str('%f' % self.timestamp), '--plugin', ua, '--key', str(bytes.decode(self.api_key.encode('utf8'))), ] if self.is_write: cmd.append('--write') if self.project: self.project = basename(self.project).replace('.sublime-project', '', 1) if self.project: cmd.extend(['--project', self.project]) elif self.folders: project_name = find_project_name_from_folders(self.folders) if project_name: cmd.extend(['--project', project_name]) for pattern in self.ignore: cmd.extend(['--ignore', pattern]) if self.debug: cmd.append('--verbose') if HAS_SSL: if self.debug: print('[WakaTime] %s' % ' '.join(cmd)) code = wakatime.main(cmd) if code != 0: print('[WakaTime] Error: Response code %d from wakatime package.' % code) else: self.sent() else: python = python_binary() if python: cmd.insert(0, python) if self.debug: print('[WakaTime] %s %s' % (python, ' '.join(cmd))) if platform.system() == 'Windows': Popen(cmd, shell=False) else: with open(join(expanduser('~'), '.wakatime.log'), 'a') as stderr: Popen(cmd, stderr=stderr) self.sent() else: print('[WakaTime] Error: Unable to find python binary.') def sent(self): sublime.set_timeout(self.set_status_bar, 0) sublime.set_timeout(self.set_last_action, 0) def set_status_bar(self): if SETTINGS.get('status_bar_message'): self.view.set_status('wakatime', 'WakaTime active {0}'.format(datetime.now().strftime('%I:%M %p'))) def set_last_action(self): global LAST_ACTION LAST_ACTION = { 'file': self.target_file, 'time': self.timestamp, 'is_write': self.is_write, } def plugin_loaded(): global SETTINGS print('[WakaTime] Initializing WakaTime plugin v%s' % __version__) if not HAS_SSL: python = python_binary() if not python: sublime.error_message("Unable to find Python binary!\nWakaTime needs Python to work correctly.\n\nGo to https://www.python.org/downloads") return SETTINGS = sublime.load_settings(SETTINGS_FILE) after_loaded() def after_loaded(): if not prompt_api_key(): sublime.set_timeout(after_loaded, 500) # need to call plugin_loaded because only ST3 will auto-call it if ST_VERSION < 3000: plugin_loaded() class WakatimeListener(sublime_plugin.EventListener): def on_post_save(self, view): handle_action(view, is_write=True) def on_activated(self, view): handle_action(view) def on_modified(self, view): handle_action(view) class WakatimeDashboardCommand(sublime_plugin.ApplicationCommand): def run(self): webbrowser.open_new_tab('https://wakatime.com/dashboard')