433 lines
12 KiB
GDScript3
433 lines
12 KiB
GDScript3
|
@tool
|
||
|
extends EditorPlugin
|
||
|
|
||
|
const HeartBeat = preload('res://addons/wakatime/heartbeat.gd')
|
||
|
|
||
|
const PLUGIN_PATH = 'res://addons/wakatime'
|
||
|
const WAKATIME_ZIP_FILEPATH = '%s/wakatime.zip' % PLUGIN_PATH
|
||
|
|
||
|
const WAKATIME_URL_FMT = 'https://github.com/wakatime/wakatime-cli/releases/download/v1.54.0/{wakatime_build}.zip'
|
||
|
const DECOMPRESSOR_URL_FMT = 'https://github.com/ouch-org/ouch/releases/download/0.3.1/{ouch_build}'
|
||
|
|
||
|
const API_KEY_MENU_ITEM_NAME = 'Wakatime API Key'
|
||
|
const CONFIG_MENU_ITEM_NAME = 'Wakatime Config File'
|
||
|
|
||
|
var last_heartbeat = HeartBeat.new()
|
||
|
|
||
|
var wakatime_dir = null
|
||
|
var wakatime_cli = null
|
||
|
var decompressor_cli = null
|
||
|
|
||
|
const ApiKeyPrompt = preload('res://addons/wakatime/api_key_prompt.tscn')
|
||
|
|
||
|
var is_windows = OS.has_feature('windows') or OS.has_feature('uwp')
|
||
|
var is_linux = OS.has_feature('linux')
|
||
|
var is_macos = OS.has_feature('macos')
|
||
|
var is_amd64 = OS.has_feature('x86_64')
|
||
|
var is_arm64 = OS.has_feature('arm64')
|
||
|
|
||
|
var debug = false
|
||
|
|
||
|
|
||
|
func get_wakatime_build():
|
||
|
# Default system = Linux
|
||
|
var platform = 'linux'
|
||
|
if is_windows:
|
||
|
platform = 'windows'
|
||
|
elif is_macos:
|
||
|
platform = 'darwin'
|
||
|
|
||
|
# Default architecture = x86_64 (amd64)
|
||
|
var architecture = 'amd64'
|
||
|
if is_amd64:
|
||
|
architecture = 'amd64'
|
||
|
elif is_arm64:
|
||
|
architecture = 'arm64'
|
||
|
|
||
|
return 'wakatime-cli-%s-%s' % [platform, architecture]
|
||
|
|
||
|
func get_ouch_build():
|
||
|
# Default system = Linux
|
||
|
var platform = 'linux-musl'
|
||
|
if is_windows:
|
||
|
platform = 'pc-windows-msvc'
|
||
|
elif is_macos:
|
||
|
platform = 'apple-darwin'
|
||
|
|
||
|
return 'ouch-%s-%s' % ['x86_64', platform]
|
||
|
|
||
|
|
||
|
func get_wakatime_directory() -> String:
|
||
|
if wakatime_dir == null:
|
||
|
wakatime_dir = '%s/.wakatime' % get_home_directory()
|
||
|
|
||
|
return wakatime_dir
|
||
|
|
||
|
|
||
|
func get_wakatime_cli() -> String:
|
||
|
if wakatime_cli == null:
|
||
|
var build = get_wakatime_build()
|
||
|
var ext = '.exe' if is_windows else ''
|
||
|
wakatime_cli = '%s/%s%s' % [get_wakatime_directory(), build, ext]
|
||
|
|
||
|
return wakatime_cli
|
||
|
|
||
|
|
||
|
func get_decompressor_cli() -> String:
|
||
|
if decompressor_cli == null:
|
||
|
var build = get_ouch_build()
|
||
|
var ext = '.exe' if is_windows else ''
|
||
|
decompressor_cli = '%s/%s%s' % [PLUGIN_PATH, build, ext]
|
||
|
|
||
|
return decompressor_cli
|
||
|
|
||
|
|
||
|
func get_home_directory():
|
||
|
var home = null
|
||
|
for env in ['WAKATIME_HOME', 'USERPROFILE', 'HOME']:
|
||
|
home = OS.get_environment(env)
|
||
|
if home:
|
||
|
if is_windows:
|
||
|
home = home.replace('\\', '/')
|
||
|
return home
|
||
|
|
||
|
return PLUGIN_PATH
|
||
|
|
||
|
|
||
|
func get_config_filepath() -> String:
|
||
|
return '%s/.wakatime.cfg' % get_home_directory()
|
||
|
|
||
|
|
||
|
func has_decompression_lib():
|
||
|
return FileAccess.file_exists(get_decompressor_cli())
|
||
|
|
||
|
|
||
|
func has_wakatime_cli():
|
||
|
return FileAccess.file_exists(get_wakatime_cli())
|
||
|
|
||
|
|
||
|
func has_wakatime_zip():
|
||
|
return FileAccess.file_exists(WAKATIME_ZIP_FILEPATH)
|
||
|
|
||
|
|
||
|
func download_decompressor():
|
||
|
pprint('Downloading Ouch! (decompression lib)...')
|
||
|
var url = DECOMPRESSOR_URL_FMT.format({'ouch_build': get_ouch_build()})
|
||
|
if is_windows:
|
||
|
url = '%s.exe' % url
|
||
|
|
||
|
var http = HTTPRequest.new()
|
||
|
http.download_file = get_decompressor_cli()
|
||
|
http.connect('request_completed', Callable(self, '_decompressor_download_completed'))
|
||
|
add_child(http)
|
||
|
|
||
|
var error = http.request(url)
|
||
|
if error != OK:
|
||
|
disable_plugin()
|
||
|
pprint_error('Failed to download decompression lib: %s' % error)
|
||
|
|
||
|
|
||
|
func _decompressor_download_completed(result, status_code, headers, body):
|
||
|
if result != HTTPRequest.RESULT_SUCCESS:
|
||
|
pprint_error('Failed to download decompression lib')
|
||
|
disable_plugin()
|
||
|
return
|
||
|
|
||
|
if !has_decompression_lib():
|
||
|
pprint_error('Failed to save decompression lib')
|
||
|
disable_plugin()
|
||
|
return
|
||
|
|
||
|
var decompressor = ProjectSettings.globalize_path(get_decompressor_cli())
|
||
|
if is_linux or is_macos:
|
||
|
OS.execute('chmod', ['+x', decompressor], [], true)
|
||
|
|
||
|
pprint('Ouch! download completed. Saved at %s' % get_decompressor_cli())
|
||
|
|
||
|
extract_files(WAKATIME_ZIP_FILEPATH, get_wakatime_directory())
|
||
|
|
||
|
|
||
|
func download_wakatime():
|
||
|
pprint('Downloading Wakatime CLI...')
|
||
|
var url = WAKATIME_URL_FMT.format({'wakatime_build': get_wakatime_build()})
|
||
|
|
||
|
var http = HTTPRequest.new()
|
||
|
http.download_file = WAKATIME_ZIP_FILEPATH
|
||
|
http.connect('request_completed', Callable(self, '_wakatime_download_completed'))
|
||
|
add_child(http)
|
||
|
|
||
|
var error = http.request(url)
|
||
|
if error != OK:
|
||
|
pprint_error('Failed to download Wakatime CLI: %s' % error)
|
||
|
disable_plugin()
|
||
|
|
||
|
|
||
|
func _wakatime_download_completed(result, status_code, headers, body):
|
||
|
if result != HTTPRequest.RESULT_SUCCESS:
|
||
|
pprint_error('Failed to download Wakatime')
|
||
|
disable_plugin()
|
||
|
return
|
||
|
|
||
|
pprint('Wakatime download completed. Saved at %s' % WAKATIME_ZIP_FILEPATH)
|
||
|
|
||
|
extract_files(WAKATIME_ZIP_FILEPATH, get_wakatime_directory())
|
||
|
|
||
|
|
||
|
func extract_files(source_file, output_dir):
|
||
|
# Both decompression lib and Wakatime zip downloads must be complete
|
||
|
if not (has_decompression_lib() and has_wakatime_zip()):
|
||
|
return
|
||
|
|
||
|
pprint('Extracting files from Wakatime zip')
|
||
|
var decompressor = ProjectSettings.globalize_path(get_decompressor_cli())
|
||
|
var source = ProjectSettings.globalize_path(source_file)
|
||
|
var destination = ProjectSettings.globalize_path(output_dir)
|
||
|
var errors = []
|
||
|
var args = ['--yes', 'decompress', source, '--dir', destination]
|
||
|
var error = OS.execute(decompressor, args, errors, true)
|
||
|
if error:
|
||
|
pprint_error(errors)
|
||
|
disable_plugin()
|
||
|
return
|
||
|
|
||
|
if has_wakatime_cli():
|
||
|
pprint('Wakatime CLI installed at %s' % get_wakatime_cli())
|
||
|
else:
|
||
|
disable_plugin()
|
||
|
pprint_error('Failed to install Wakatime CLI')
|
||
|
|
||
|
# Remove unnecessary files
|
||
|
pprint('Cleaning downloaded files')
|
||
|
clean_downloaded_files()
|
||
|
|
||
|
|
||
|
func clean_downloaded_files():
|
||
|
if has_wakatime_zip():
|
||
|
delete_file(WAKATIME_ZIP_FILEPATH)
|
||
|
|
||
|
if has_decompression_lib():
|
||
|
delete_file(get_decompressor_cli())
|
||
|
|
||
|
|
||
|
func delete_file(path):
|
||
|
var dir = DirAccess.open('res://')
|
||
|
var error = dir.remove(path)
|
||
|
if error != OK:
|
||
|
pprint_error('Failed to remove %s' % path)
|
||
|
else:
|
||
|
pprint('File %s removed' % path)
|
||
|
|
||
|
|
||
|
func check_dependencies():
|
||
|
if has_wakatime_cli():
|
||
|
return
|
||
|
|
||
|
download_wakatime()
|
||
|
|
||
|
if !has_decompression_lib():
|
||
|
download_decompressor()
|
||
|
|
||
|
func get_api_key():
|
||
|
var output = []
|
||
|
var err = OS.execute(get_wakatime_cli(), ['--config-read', 'api_key'], output)
|
||
|
if err == -1:
|
||
|
return null
|
||
|
|
||
|
var api_key = output[0].strip_edges()
|
||
|
if api_key.is_empty():
|
||
|
return null
|
||
|
|
||
|
return api_key
|
||
|
|
||
|
|
||
|
func request_api_key():
|
||
|
var prompt = ApiKeyPrompt.instantiate()
|
||
|
_set_api_key_on_prompt(prompt, get_api_key())
|
||
|
_register_api_key_signals(prompt)
|
||
|
add_child(prompt)
|
||
|
prompt.popup_centered()
|
||
|
await prompt.popup_hide
|
||
|
prompt.queue_free()
|
||
|
|
||
|
|
||
|
func open_config_file():
|
||
|
OS.shell_open(get_config_filepath())
|
||
|
|
||
|
|
||
|
func setup_plugin():
|
||
|
pprint('Initializing %s plugin.' % get_user_agent())
|
||
|
|
||
|
check_dependencies()
|
||
|
|
||
|
var api_key = get_api_key()
|
||
|
if api_key == null:
|
||
|
request_api_key()
|
||
|
|
||
|
await get_tree().process_frame
|
||
|
|
||
|
# Adds tool menu item command to open API key prompt
|
||
|
add_tool_menu_item(API_KEY_MENU_ITEM_NAME, request_api_key)
|
||
|
# Adds tool menu item command to open global WakaTime config file
|
||
|
add_tool_menu_item(CONFIG_MENU_ITEM_NAME, open_config_file)
|
||
|
# Register editor changed callback
|
||
|
var script_editor = get_editor_interface().get_script_editor()
|
||
|
script_editor.call_deferred('connect', 'editor_script_changed', Callable(self, '_on_script_changed'))
|
||
|
|
||
|
|
||
|
func _disable_plugin():
|
||
|
remove_tool_menu_item(API_KEY_MENU_ITEM_NAME)
|
||
|
remove_tool_menu_item(CONFIG_MENU_ITEM_NAME)
|
||
|
var script_editor = get_editor_interface().get_script_editor()
|
||
|
if script_editor.is_connected('editor_script_changed', Callable(self, '_on_script_changed')):
|
||
|
script_editor.disconnect('editor_script_changed', Callable(self, '_on_script_changed'))
|
||
|
|
||
|
|
||
|
func _ready():
|
||
|
setup_plugin()
|
||
|
|
||
|
|
||
|
func _exit_tree():
|
||
|
_disable_plugin()
|
||
|
|
||
|
|
||
|
func get_current_file():
|
||
|
return get_editor_interface().get_script_editor().get_current_script()
|
||
|
|
||
|
|
||
|
func handle_activity(file, is_write=false):
|
||
|
if not (file and has_wakatime_cli()):
|
||
|
return
|
||
|
|
||
|
var filepath = ProjectSettings.globalize_path(file.resource_path)
|
||
|
|
||
|
if is_write or filepath != last_heartbeat.filepath or enough_time_has_passed(last_heartbeat.timestamp):
|
||
|
send_heartbeat(filepath, is_write)
|
||
|
|
||
|
|
||
|
func _send_heartbeat(cmd_args):
|
||
|
if wakatime_cli == null:
|
||
|
wakatime_cli = get_wakatime_cli()
|
||
|
|
||
|
var output = []
|
||
|
var exit_code = OS.execute(wakatime_cli, cmd_args, output, true)
|
||
|
if debug:
|
||
|
if exit_code == -1:
|
||
|
pprint('Failed to send heartbeat: %s' % output)
|
||
|
else:
|
||
|
pprint('Heartbeat sent: %s' % output)
|
||
|
|
||
|
|
||
|
func send_heartbeat(filepath, is_write):
|
||
|
var wakatime_api_key = get_api_key()
|
||
|
if wakatime_api_key == null:
|
||
|
pprint_error('Failed to get API key')
|
||
|
return
|
||
|
|
||
|
var heartbeat = HeartBeat.new(filepath, Time.get_unix_time_from_system(), is_write)
|
||
|
var cmd = ['--entity', heartbeat.filepath,
|
||
|
'--key', wakatime_api_key,
|
||
|
'--plugin', get_user_agent()]
|
||
|
|
||
|
if is_write:
|
||
|
cmd.append('--write')
|
||
|
|
||
|
cmd.append('--project')
|
||
|
cmd.append(ProjectSettings.get('application/config/name'))
|
||
|
|
||
|
var cmd_callable = Callable(self, '_send_heartbeat').bind(cmd)
|
||
|
WorkerThreadPool.add_task(cmd_callable)
|
||
|
|
||
|
last_heartbeat = heartbeat
|
||
|
|
||
|
|
||
|
func enough_time_has_passed(last_sent_time):
|
||
|
return Time.get_unix_time_from_system() - last_heartbeat.timestamp >= HeartBeat.FILE_MODIFIED_DELAY
|
||
|
|
||
|
|
||
|
# file changed
|
||
|
func _on_script_changed(file):
|
||
|
handle_activity(file)
|
||
|
|
||
|
|
||
|
# file modfied
|
||
|
func _unhandled_key_input(ev):
|
||
|
var file = get_current_file()
|
||
|
handle_activity(file)
|
||
|
|
||
|
|
||
|
# file saved
|
||
|
func _save_external_data():
|
||
|
var file = get_current_file()
|
||
|
handle_activity(file, true)
|
||
|
|
||
|
|
||
|
func _set_api_key_on_prompt(instance: PopupPanel, api_key):
|
||
|
if api_key == null:
|
||
|
api_key = ''
|
||
|
var text_edit = instance.get_node('vbox_container/hbox_container_top/line_edit')
|
||
|
text_edit.text = api_key
|
||
|
|
||
|
|
||
|
func _register_api_key_signals(instance: PopupPanel):
|
||
|
var show_btn = instance.get_node('vbox_container/hbox_container_top/show_btn')
|
||
|
var save_btn = instance.get_node('vbox_container/hbox_container_bottom/save_btn')
|
||
|
var text_edit = instance.get_node('vbox_container/hbox_container_top/line_edit')
|
||
|
|
||
|
show_btn.connect('pressed', Callable(self, '_on_toggle_secret_text').bind(instance))
|
||
|
save_btn.connect('pressed', Callable(self, '_on_save_api_key').bind(instance))
|
||
|
instance.connect('popup_hide', Callable(self, '_on_popup_hide').bind(instance))
|
||
|
|
||
|
|
||
|
func disable_plugin():
|
||
|
pprint_error('Disabling wakatime-godot plugin due some setup error. ' \
|
||
|
+ 'Check your internet connection and reload the plugin')
|
||
|
get_editor_interface().call_deferred('set_plugin_enabled', 'wakatime', false)
|
||
|
|
||
|
|
||
|
func _on_popup_hide(prompt: PopupPanel):
|
||
|
prompt.queue_free()
|
||
|
|
||
|
|
||
|
func _on_toggle_secret_text(prompt: PopupPanel):
|
||
|
var text_edit = prompt.get_node('vbox_container/hbox_container_top/line_edit')
|
||
|
var show_btn = prompt.get_node('vbox_container/hbox_container_top/show_btn')
|
||
|
|
||
|
text_edit.secret = not text_edit.secret
|
||
|
show_btn.text = 'Show' if text_edit.secret else 'Hide'
|
||
|
|
||
|
|
||
|
func _on_save_api_key(prompt: PopupPanel):
|
||
|
var text_edit = prompt.get_node('vbox_container/hbox_container_top/line_edit')
|
||
|
var api_key = text_edit.text.strip_edges()
|
||
|
var err = OS.execute(get_wakatime_cli(), ['--config-write', 'api_key=%s' % api_key])
|
||
|
if err == -1:
|
||
|
pprint_error('Failed to save API key')
|
||
|
|
||
|
prompt.visible = false
|
||
|
|
||
|
|
||
|
func pprint(message):
|
||
|
print('[godot-wakatime] %s' % message)
|
||
|
|
||
|
|
||
|
func pprint_error(message):
|
||
|
push_error('[godot-wakatime] %s' % message)
|
||
|
|
||
|
|
||
|
func get_user_agent():
|
||
|
return 'godot/%s %s/%s' % [get_engine_version(), _get_plugin_name(), get_plugin_version()]
|
||
|
|
||
|
|
||
|
func _get_plugin_name():
|
||
|
return 'godot-wakatime'
|
||
|
|
||
|
|
||
|
func get_plugin_version():
|
||
|
return '1.5.0'
|
||
|
|
||
|
|
||
|
func get_engine_version():
|
||
|
return '%s.%s.%s' % [ Engine.get_version_info()['major'],
|
||
|
Engine.get_version_info()['minor'],
|
||
|
Engine.get_version_info()['patch']]
|