shoDater/addons/wakatime/wakatime.gd

433 lines
12 KiB
GDScript3
Raw Normal View History

2024-07-19 01:26:40 +01:00
@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']]