]> Repositories - rushbound.git/commitdiff
Install Godot Super Wakatime
authorAyo Reis <hey@ayoreis.com>
Mon, 8 Sep 2025 18:39:18 +0000 (19:39 +0100)
committerAyo Reis <hey@ayoreis.com>
Tue, 9 Sep 2025 11:05:03 +0000 (12:05 +0100)
14 files changed:
addons/godot_super-wakatime/LICENSE [new file with mode: 0644]
addons/godot_super-wakatime/README.md [new file with mode: 0644]
addons/godot_super-wakatime/api_key_prompt.tscn [new file with mode: 0644]
addons/godot_super-wakatime/counter.tscn [new file with mode: 0644]
addons/godot_super-wakatime/decompressor.gd [new file with mode: 0644]
addons/godot_super-wakatime/decompressor.gd.uid [new file with mode: 0644]
addons/godot_super-wakatime/godot_super-wakatime.gd [new file with mode: 0644]
addons/godot_super-wakatime/godot_super-wakatime.gd.uid [new file with mode: 0644]
addons/godot_super-wakatime/heartbeat.gd [new file with mode: 0644]
addons/godot_super-wakatime/heartbeat.gd.uid [new file with mode: 0644]
addons/godot_super-wakatime/plugin.cfg [new file with mode: 0644]
addons/godot_super-wakatime/utils.gd [new file with mode: 0644]
addons/godot_super-wakatime/utils.gd.uid [new file with mode: 0644]
project.godot

diff --git a/addons/godot_super-wakatime/LICENSE b/addons/godot_super-wakatime/LICENSE
new file mode 100644 (file)
index 0000000..6ce453c
--- /dev/null
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Bartosz Budnik (BudzioT)
+
+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.
diff --git a/addons/godot_super-wakatime/README.md b/addons/godot_super-wakatime/README.md
new file mode 100644 (file)
index 0000000..809df21
--- /dev/null
@@ -0,0 +1,153 @@
+
+<a id="readme-top"></a>
+
+<!-- SHIELDS -->
+[![Contributors][contributors-shield]][contributors-url]
+[![Forks][forks-shield]][forks-url]
+[![Stargazers][stars-shield]][stars-url]
+[![Issues][issues-shield]][issues-url]
+[![MIT License][license-shield]][license-url]
+
+<!-- HEADER -->
+<br />
+<div align="center">
+    <a href="https://github.com/BudzioT/Godot_Super-Wakatime">
+        <img src="https://cloud-bo1ln2br1-hack-club-bot.vercel.app/0godotwaka22.png"  alt="Godot Wakatime"/>
+    </a>
+    <h3 align="center"> Godot Super Wakatime </h3>
+    <p align="center">
+        Tool to measure time spent in loved by many people game engine - Godot
+        <br />
+        Officially approved to use in High Seas event created by Hack Club
+        <br />
+        <br />
+        <a href="https://godotengine.org/asset-library/asset/3484">Get from Asset Lib</a>
+        ·
+        <a href="https://youtu.be/rqAc-YdVXyM">View Demo</a>
+        ·
+        <a href="https://github.com/BudzioT/Godot_Super-Wakatime/issues/new">Report Bug / Request Feature</a>
+    </p>
+</div>
+
+<!-- CONTENTS -->
+<details>
+    <summary>Table of Contents</summary>
+    <ol>
+        <li>
+            <a href="#about">About The Project</a>
+            <ul>
+                <li><a href="#built-with">Built Using</a></li>
+            </ul>
+        </li>
+        <li>
+            <a href="#getting-started">Getting Started</a>
+            <ul>
+                <li><a href="#installation">Installation</a></li>
+            </ul>
+        </li>
+        <li><a href="#usage">Usage</a></li>
+        <li><a href="#license">License</a></li>
+    </ol>
+</details>
+
+
+<!-- ABOUT -->
+## About The Project
+<br />
+
+[![Product Screenshot][product-screenshot]](https://waka.hackclub.com)
+
+This tool can successfully measure time spent building your games or apps in Godot.
+<br />
+Here's why:
+* It differentiates between switching a scene and script
+* It counts key presses as coding and mouse clicks as building scene
+* Changing scene structure results in a heartbeat sent
+* It correctly detects OS, machine name, language, editor, files
+* It can detect your cursor line and position
+* Time is split between: Building, Coding, Testing
+* In the future it will also detect testing your projects
+
+It works on both Linux and Windows, it wasn't tested on macOS yet
+<br />
+You can also see your time spent in the editor itself:
+[![Time in editor][time-screenshot]]
+
+<p align="right">(<a href="#readme-top">top</a>)</p>
+
+
+### Built Using
+I used the Ouch! CLI tool for decompression of files <br />
+This project was built using one simple, yet powerful language.<br />
+It required a lot of workarounds, but it was a pleasure to use it
+* [![GDScript][Godot]][Godot-url]
+* [![Ouch!][Ouch-shield]][Ouch-url]
+
+<p align="right">(<a href="#readme-top">top</a>)</p>
+
+<!-- GETTING STARTED -->
+## Getting Started
+How to install and use this software? It's easy!
+
+### Installation
+You can either download it from asset lib (will provide a link, once it gets accepted)
+<br />Or you can manually install it, here's how to do it!
+1. Clone the repository
+    ```sh
+    git clone https://github.com/BudzioT/Godot_Super-Wakatime.git
+    ```
+2. Go into your project
+3. Insert the entire `./addons` folder into your project `res://` directory
+
+<p align="right">(<a href="#readme-top">top</a>)</p>
+
+<!-- USAGE -->
+## Usage
+Don't know how to use this plugin? Here are the steps:
+1. Turn on the plugin in your plugins. In your `Project -> Project Settings -> Plugins -> `Click the `Enable` checkbox near this plugin
+2. If prompted for API key, provide it from Wakatime website
+3. if there is an issue with it, please manually create `~/.wakatime.cfg` file with these contents:
+    ```sh
+    [settings]
+    api_key=xxxx
+    ```
+    Where xxxx is your api key
+<br /><br />
+If you are coming from High Seas used this:
+    ```sh
+    [settings]
+    api_url = https://waka.hackclub.com/api
+    api_key=xxxx
+    ```
+4. Wakatime CLI should have been installed automatically along with Ouch! Decompression library
+5. Work on your project! You should see your results on either Wakatime or Hackatime!
+6. You can also see your time at the bottom panel
+
+<p align="right">(<a href="#readme-top">top</a>)</p>
+
+<!-- LICENSE -->
+## License
+
+Distributed under the MIT License. See `LICENSE` for more information.
+
+<p align="right">(<a href="#readme-top">back to top</a>)</p>
+
+
+<!-- URLS -->
+[contributors-shield]: https://img.shields.io/github/contributors/budziot/Godot_Super-Wakatime?style=for-the-badge
+[contributors-url]: https://github.com/BudzioT/Godot_Super-Wakatime/graphs/contributors
+[forks-shield]: https://img.shields.io/github/forks/budziot/Godot_Super-Wakatime?style=for-the-badge
+[forks-url]: https://github.com/BudzioT/Godot_Super-Wakatime/forks
+[stars-shield]: https://img.shields.io/github/stars/budziot/Godot_Super-Wakatime?style=for-the-badge
+[stars-url]: https://github.com/BudzioT/Godot_Super-Wakatime/stargazers
+[issues-shield]: https://img.shields.io/github/issues/budziot/Godot_Super-Wakatime?style=for-the-badge
+[issues-url]: https://github.com/BudzioT/Godot_Super-Wakatime/issues
+[license-shield]: https://img.shields.io/github/license/budziot/Godot_Super-Wakatime?style=for-the-badge
+[license-url]: https://github.com/BudzioT/Godot_Super-Wakatime/blob/master/addons/godot_super-wakatime/LICENSE
+[product-screenshot]: https://cloud-j4wibbzz7-hack-club-bot.vercel.app/0image.png
+[product-logo]: https://cloud-j4wibbzz7-hack-club-bot.vercel.app/2godotwaka2.png
+[Godot]: https://img.shields.io/badge/Godot%20Engine-478CBF?logo=godotengine&logoColor=fff&style=flat
+[Godot-url]: https://godotengine.org/
+[Ouch-shield]: https://img.shields.io/badge/Ouch!-tool-blue?label=Ouch!
+[Ouch-url]: https://github.com/ouch-org/ouch
+[time-screenshot]: https://cloud-l88kldf50-hack-club-bot.vercel.app/0image.png
diff --git a/addons/godot_super-wakatime/api_key_prompt.tscn b/addons/godot_super-wakatime/api_key_prompt.tscn
new file mode 100644 (file)
index 0000000..0cc0457
--- /dev/null
@@ -0,0 +1,55 @@
+[gd_scene load_steps=3 format=3 uid="uid://bv3rxl15k7bj6"]
+
+[sub_resource type="Theme" id="Theme_qedjm"]
+
+[sub_resource type="Theme" id="Theme_1xn0x"]
+
+[node name="ApiKeyPrompt" type="PopupPanel"]
+title = "API key configuration"
+initial_position = 1
+size = Vector2i(350, 100)
+visible = true
+content_scale_aspect = 4
+content_scale_stretch = 1
+theme = SubResource("Theme_qedjm")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 4.0
+offset_top = 4.0
+offset_right = -4.0
+offset_bottom = -4.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 4
+size_flags_vertical = 4
+theme = SubResource("Theme_1xn0x")
+
+[node name="HBoxContainerTop" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+
+[node name="LineEdit" type="LineEdit" parent="VBoxContainer/HBoxContainerTop"]
+custom_minimum_size = Vector2(200, 25)
+layout_mode = 2
+size_flags_horizontal = 6
+size_flags_vertical = 0
+placeholder_text = "Provide Your Wakatime API Key"
+expand_to_text_length = true
+secret = true
+
+[node name="ShowButton" type="Button" parent="VBoxContainer/HBoxContainerTop"]
+layout_mode = 2
+size_flags_horizontal = 6
+size_flags_vertical = 4
+text = "Show"
+
+[node name="HBoxContainerBottom" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+
+[node name="SubmitButton" type="Button" parent="VBoxContainer/HBoxContainerBottom"]
+layout_mode = 2
+size_flags_horizontal = 6
+size_flags_vertical = 0
+text = "Submit"
diff --git a/addons/godot_super-wakatime/counter.tscn b/addons/godot_super-wakatime/counter.tscn
new file mode 100644 (file)
index 0000000..405164e
--- /dev/null
@@ -0,0 +1,29 @@
+[gd_scene format=3 uid="uid://c4t47uj61wtu5"]
+
+[node name="Counter" type="Panel"]
+anchors_preset = 3
+anchor_left = 1.0
+anchor_top = 1.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = -228.0
+offset_top = -36.0
+grow_horizontal = 0
+grow_vertical = 0
+size_flags_vertical = 8
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="Label" type="Label" parent="HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 1
+text = "00 hrs 00 mins"
+horizontal_alignment = 1
+vertical_alignment = 1
diff --git a/addons/godot_super-wakatime/decompressor.gd b/addons/godot_super-wakatime/decompressor.gd
new file mode 100644 (file)
index 0000000..ee3e07d
--- /dev/null
@@ -0,0 +1,21 @@
+var Utils = preload("res://addons/godot_super-wakatime/utils.gd").new()
+
+func decompressor_cli(current_decompressor, platform: String, plugin_path: String) -> String:
+       """Get path to the decompressor cli"""
+       var build = Utils.get_ouch_build(platform)
+       var ext: String = ".exe" if platform == "windows" else ""
+               
+       if current_decompressor == null:
+               if platform == "windows":
+                       current_decompressor = "%s/%s%s" % [plugin_path, build, ext]
+               else:
+                       current_decompressor = "%s%s" % [build, ext]
+       else:
+               current_decompressor = "%s%s" % [build, ext]
+               
+       return current_decompressor
+       
+func lib_exists(current_decompressor, platform: String, plugin_path: String) -> bool:
+       """Return if ouch already exists"""
+       return FileAccess.file_exists(decompressor_cli(current_decompressor, platform, plugin_path));
+       
diff --git a/addons/godot_super-wakatime/decompressor.gd.uid b/addons/godot_super-wakatime/decompressor.gd.uid
new file mode 100644 (file)
index 0000000..94ad68f
--- /dev/null
@@ -0,0 +1 @@
+uid://hk0ile7qa8oi
diff --git a/addons/godot_super-wakatime/godot_super-wakatime.gd b/addons/godot_super-wakatime/godot_super-wakatime.gd
new file mode 100644 (file)
index 0000000..42c1799
--- /dev/null
@@ -0,0 +1,627 @@
+@tool
+extends EditorPlugin
+
+
+#------------------------------- SETUP -------------------------------
+# Utilities
+var Utils = preload("res://addons/godot_super-wakatime/utils.gd").new()
+var DecompressorUtils = preload("res://addons/godot_super-wakatime/decompressor.gd").new()
+
+# Hearbeat class
+const HeartBeat = preload("res://addons/godot_super-wakatime/heartbeat.gd")
+var last_heartbeat = HeartBeat.new()
+
+# Paths, Urls
+const PLUGIN_PATH: String = "res://addons/godot_super-wakatime"
+const ZIP_PATH: String = "%s/wakatime.zip" % PLUGIN_PATH
+
+const WAKATIME_URL_FMT: String = \
+       "https://github.com/wakatime/wakatime-cli/releases/download/v1.54.0/{wakatime_build}.zip"
+const DECOMPERSSOR_URL_FMT: String = \
+       "https://github.com/ouch-org/ouch/releases/download/0.3.1/{ouch_build}"
+
+# Names for menu
+const API_MENU_ITEM: String = "Wakatime API key"
+const CONFIG_MENU_ITEM: String = "Wakatime Config File"
+
+# Directories to grab wakatime from
+var wakatime_dir = null
+var wakatime_cli = null
+var decompressor_cli = null
+
+var ApiKeyPrompt: PackedScene = preload("res://addons/godot_super-wakatime/api_key_prompt.tscn")
+var Counter: PackedScene = preload("res://addons/godot_super-wakatime/counter.tscn")
+
+# Set platform
+var system_platform: String = Utils.set_platform()[0]
+var system_architecture: String = Utils.set_platform()[1]
+
+var debug: bool = false
+var last_scene_path: String = ''
+var last_file_path: String = ''
+var last_time: int = 0
+var previous_state: String = ''
+const LOG_INTERVAL: int = 60000
+var scene_mode: bool = false
+
+var key_get_tries: int = 0
+var counter_instance: Node
+var current_time: String = "0 hrs, 0mins"
+
+
+# #------------------------------- DIRECT PLUGIN FUNCTIONS -------------------------------
+func _ready() -> void:
+       setup_plugin()
+       set_process(true)
+       
+func _exit_tree() -> void:
+       _disable_plugin()
+       set_process(false)
+       
+       
+func _physics_process(delta: float) -> void:
+       """Process plugin changes over time"""
+       # Every 1000 frames check for updates
+       if Engine.get_physics_frames() % 1000 == 0:
+               # Check for scene change
+               var scene_root = get_editor_interface().get_edited_scene_root()
+               if scene_root:
+                       var current_scene_path = _get_current_scene()
+                       
+                       # If currently used scene is different thatn 1000 frames ago, log activity
+                       if current_scene_path != last_scene_path:
+                               last_scene_path = current_scene_path
+                               handle_activity_scene(current_scene_path, false, true)
+                               
+                       else:
+                               # Check for scene updates
+                               var current_scene = EditorInterface.get_edited_scene_root()
+                               if current_scene:
+                                       var state = generate_scene_state(current_scene)
+                                       # If current state is different than the previous one, handle activity
+                                       if state != previous_state:
+                                               previous_state = state
+                                               handle_activity_scene(current_scene_path, true)
+                                       # Otherwise just keep scene the same
+                                       else:
+                                               previous_state = state
+               else:
+                       last_scene_path = '' 
+                                       
+func generate_scene_state(node: Node) -> String:
+       """Generate a scene state identifier"""
+       var state = str(node.get_instance_id())
+       for child in node.get_children():
+               state += str(child.get_instance_id())
+       return str(state)
+       
+func _input(event: InputEvent) -> void:
+       """Handle all input events"""
+       # Key events
+       if event is InputEventKey:
+               var file = get_current_file()
+               if file:
+                       handle_activity(ProjectSettings.globalize_path(file.resource_path))
+       # Mouse button events
+       elif event is InputEventMouse and event.is_pressed():
+               var file = _get_current_scene()
+               if file != '' and file:
+                       handle_activity_scene(file)
+
+func setup_plugin() -> void:
+       """Setup Wakatime plugin, download dependencies if needed, initialize menu"""
+       Utils.plugin_print("Setting up %s" % get_user_agent())
+       check_dependencies()
+       
+       # Grab API key if needed
+       var api_key = get_api_key()
+       if api_key == null:
+               request_api_key()
+       await get_tree().process_frame
+       
+       # Add menu buttons
+       add_tool_menu_item(API_MENU_ITEM, request_api_key)
+       add_tool_menu_item(CONFIG_MENU_ITEM, open_config)
+       
+       counter_instance = Counter.instantiate()
+       add_control_to_bottom_panel(counter_instance, current_time)
+       
+       # Connect code editor signals
+       var script_editor: ScriptEditor = get_editor_interface().get_script_editor()
+       script_editor.call_deferred("connect", "editor_script_changed", Callable(self, 
+               "_on_script_changed"))
+
+func _disable_plugin() -> void:
+       """Cleanup after disabling plugin"""
+       # Remove items from menu
+       remove_tool_menu_item(API_MENU_ITEM)
+       remove_tool_menu_item(CONFIG_MENU_ITEM)
+       
+       remove_control_from_bottom_panel(counter_instance)
+       
+       # Disconnect script editor tracking
+       var script_editor: ScriptEditor = 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 _on_script_changed(file) -> void:
+       """Handle changing scripts"""
+       if file:
+               last_file_path = ProjectSettings.globalize_path(file.resource_path)
+       handle_activity(last_file_path)
+       
+#func _unhandled_key_input(event: InputEvent) -> void:
+#      """Handle key inputs"""
+#      var file = get_current_file()
+#      handle_activity(file)
+       
+func _save_external_data() -> void:
+       """Handle saving files"""
+       var file = get_current_file()
+       if file:
+               handle_activity(ProjectSettings.globalize_path(file.resource_path), true)
+       
+func _get_current_scene():
+       """Get currently used scene"""
+       if EditorInterface.get_edited_scene_root():
+               var file = EditorInterface.get_edited_scene_root()
+               if file:
+                       return ProjectSettings.globalize_path(file.scene_file_path)
+       else:
+               var file = get_current_file()
+               if file:
+                       return ProjectSettings.globalize_path(file.resource_path)
+                       
+       return null
+       
+func _on_scene_modified():
+       """Send heartbeat when scene is modified"""
+       var current_scene = get_tree().current_scene
+       if current_scene:
+               handle_activity_scene(_get_current_scene())
+       
+func get_current_file():
+       """Get currently used script file"""
+       var file = get_editor_interface().get_script_editor().get_current_script()
+       if file:
+               last_file_path = ProjectSettings.globalize_path(file.resource_path)
+       
+       return get_editor_interface().get_script_editor().get_current_script()
+               
+func handle_activity(file, is_write: bool = false) -> void:
+       """Handle user's activity"""
+       # If file that has activity in or wakatime cli doesn't exist, return
+       if not file or not Utils.wakatime_cli_exists(get_waka_cli()):
+               return
+       
+       # If user is saving file or has changed path, or enough time has passed for a heartbeat - send it
+       #var filepath = ProjectSettings.globalize_path(file.resource_path)
+       if is_write or file != last_heartbeat.file_path or enough_time_passed():
+               send_heartbeat(file, is_write)
+               
+func handle_activity_scene(file, is_write: bool = false, changed_file: bool = false) -> void:
+       """Handle activity in scenes"""
+       if not file or not Utils.wakatime_cli_exists(get_waka_cli()):
+               return
+               
+       if is_write or changed_file or enough_time_passed():
+               scene_mode = true
+               send_heartbeat(file, is_write)
+               
+func send_heartbeat(filepath: String, is_write: bool) -> void:
+       """Send Wakatimde heartbeat for the specified file"""
+       # Check Wakatime API key
+       var api_key = get_api_key()
+       if api_key == null:
+               Utils.plugin_print("Failed to get Wakatime API key. Are you sure it's correct?")
+               if (key_get_tries < 3):
+                       request_api_key()
+                       key_get_tries += 1
+               else:
+                       Utils.plugin_print("If this keep occuring, please create a file: ~/.wakatime.cfg\n
+                               initialize it with:\n[settings]\napi_key={your_key}")
+               return
+               
+       # Make sure not to trigger additional heartbeats cause of events from scenes
+       var file = filepath
+       if scene_mode:
+               file = last_file_path
+               #print("\n-------SCENE MODE--------\n")
+               
+       # Create heartbeat
+       var heartbeat = HeartBeat.new(file, Time.get_unix_time_from_system(), is_write)
+       
+       # Current text editor
+       var text_editor = _find_active_script_editor()
+       var cursor_pos = _get_cursor_pos(text_editor)
+       
+       # Append all informations as Wakatime CLI arguments
+       var cmd: Array[Variant] = ["--entity", filepath, "--key", api_key]
+       if is_write:
+               cmd.append("--write")
+       cmd.append_array(["--alternate-project", ProjectSettings.get("application/config/name")])
+       cmd.append_array(["--time", str(heartbeat.time)])
+       cmd.append_array(["--lineno", str(cursor_pos.line)])
+       cmd.append_array(["--cursorpos", str(cursor_pos.column)])
+       cmd.append_array(["--plugin", get_user_agent()])
+       
+       cmd.append_array(["--alternate-language", "Scene"])
+       if scene_mode:
+               cmd.append_array(["--category", "building"])
+       else:
+               cmd.append(["--category", "coding"])    
+       
+       # Send heartbeat using Wakatime CLI
+       var cmd_callable = Callable(self, "_handle_heartbeat").bind(cmd)
+       
+       scene_mode = false
+       
+       WorkerThreadPool.add_task(cmd_callable)
+       last_heartbeat = heartbeat
+       
+func _find_active_script_editor():
+       """Return currently used script editor"""
+       # Get script editor
+       var script_editor = get_editor_interface().get_script_editor()
+       var current_editor = script_editor.get_current_editor()
+       
+       # Try to find code editor from it
+       if current_editor:
+               return _find_code_edit_recursive(script_editor.get_current_editor())
+       return null
+
+func _find_code_edit_recursive(node: Node) -> CodeEdit:
+       """Find recursively code editor in a node"""
+       # If node is already a code editor, return it
+       if node is CodeEdit:
+               return node
+
+       # Try to find it in every child of a given node         
+       for child in node.get_children():
+               var editor = _find_code_edit_recursive(child)
+               if editor:
+                       return editor
+       return null
+       
+func _get_cursor_pos(text_editor) -> Dictionary:
+       """Get cursor editor from the given text editor"""
+       if text_editor:
+               
+               return {
+                       "line": text_editor.get_caret_line() + 1,
+                       "column": text_editor.get_caret_column() + 1
+               }
+               
+       return {
+               "line": 0,
+               "column": 0
+       }
+       
+func _handle_heartbeat(cmd_arguments) -> void:
+       """Handle sending the heartbeat"""
+       # Get Wakatime CLI and try to send a heartbeat
+       if wakatime_cli == null:
+               wakatime_cli = Utils.get_waka_cli()
+               
+       var output: Array[Variant] = []
+       var exit_code: int = OS.execute(wakatime_cli, cmd_arguments, output, true)
+       
+       update_today_time(wakatime_cli)
+               
+       # Inform about success or errors if user is in debug
+       if debug:
+               if exit_code == -1:
+                       Utils.plugin_print("Failed to send heartbeat: %s" % output)
+               else:
+                       Utils.plugin_print("Heartbeat sent: %s" % output)
+                       
+func enough_time_passed():
+       """Check if enough time has passed for another heartbeat"""
+       return Time.get_unix_time_from_system() - last_heartbeat.time >= HeartBeat.FILE_MODIFIED_DELAY
+       
+func update_today_time(wakatime_cli) -> void:
+       """Update today's time in menu"""
+       var output: Array[Variant] = []
+       # Get today's time from Wakatime CLI
+       var exit_code: int = OS.execute(wakatime_cli, ["--today"], output, true)
+       
+       # Convert it and combine different categories into
+       if exit_code == 0:
+               current_time = convert_time(output[0])
+       else:
+               current_time = "Wakatime"
+       #print(current_time)
+       call_deferred("_update_panel_label", current_time, output[0])
+       
+func _update_panel_label(label: String, content: String):
+       """Update bottom panel name that shows time"""
+       # If counter exists and it has a label, update both the label and panel's name
+       if counter_instance and counter_instance.get_node("HBoxContainer/Label"):
+               counter_instance.get_node("HBoxContainer/Label").text = content
+               # Workaround to rename panel
+               remove_control_from_bottom_panel(counter_instance)
+               add_control_to_bottom_panel(counter_instance, label)
+               
+func convert_time(complex_time: String):
+       """Convert time from complex format into basic one, combine times"""
+       return complex_time
+       
+       """
+       # Split times into categories
+       var time_categories = complex_time.split(', ')
+       for category in time_categories:
+               # Split time into parts, get first and third part (hours and minutes)
+               var time_parts = category.split(' ')
+               if time_parts.size() >= 3:
+                       hours += int(time_parts[0])
+                       minutes += int(time_parts[2])
+       
+       # Wrap minutes into hours if needed
+       while minutes >= 60:
+               minutes -= 60
+               hours += 1
+
+       return str(hours) + " hrs, " + str(minutes) + " mins"
+       """
+
+#------------------------------- FILE FUNCTIONS -------------------------------
+func open_config() -> void:
+       """Open wakatime config file"""
+       OS.shell_open(Utils.config_filepath(system_platform, PLUGIN_PATH))
+       
+func get_waka_dir() -> String:
+       """Search for and return wakatime directory"""
+       if wakatime_dir == null:
+               wakatime_dir = "%s/.wakatime" % Utils.home_directory(system_platform, PLUGIN_PATH)
+       return wakatime_dir
+       
+func get_waka_cli() -> String:
+       """Get wakatime_cli file"""
+       if wakatime_cli == null:
+               var build = Utils.get_waka_build(system_platform, system_architecture)
+               var ext: String = ".exe" if system_platform == "windows" else ''
+               wakatime_cli = "%s/%s%s" % [get_waka_dir(), build, ext]
+       return wakatime_cli
+       
+func check_dependencies() -> void:
+       """Make sure all dependencies exist"""
+       if !Utils.wakatime_cli_exists(get_waka_cli()):
+               download_wakatime()
+               if !DecompressorUtils.lib_exists(decompressor_cli, system_platform, PLUGIN_PATH):
+                       download_decompressor()
+       
+func download_wakatime() -> void:
+       """Download wakatime cli"""
+       Utils.plugin_print("Downloading Wakatime CLI...")
+       var url: String = WAKATIME_URL_FMT.format({"wakatime_build": 
+                       Utils.get_waka_build(system_platform, system_architecture)})
+       
+       # Try downloading wakatime
+       var http = HTTPRequest.new()
+       http.download_file = ZIP_PATH
+       http.connect("request_completed", Callable(self, "_wakatime_download_completed"))
+       add_child(http)
+       
+       # Handle errors
+       var status = http.request(url)
+       if status != OK:
+               Utils.plugin_print_err("Failed to start downloading Wakatime [Error: %s]" % status)
+               _disable_plugin()
+       
+func download_decompressor() -> void:
+       """Download ouch decompressor"""
+       Utils.plugin_print("Downloading Ouch! decompression library...")
+       var url: String = DECOMPERSSOR_URL_FMT.format({"ouch_build": 
+                       Utils.get_ouch_build(system_platform)})
+       if system_platform == "windows":
+               url += ".exe"
+               
+       # Try to download ouch
+       var http = HTTPRequest.new()
+       http.download_file = DecompressorUtils.decompressor_cli(decompressor_cli, system_platform, PLUGIN_PATH)
+       http.connect("request_completed", Callable(self, "_decompressor_download_finished"))
+       add_child(http)
+       
+       # Handle errors
+       var status = http.request(url)
+       if status != OK:
+               _disable_plugin()
+               Utils.plugin_print_err("Failed to start downloading Ouch! library [Error: %s]" % status)
+               
+func _wakatime_download_completed(result, status, headers, body) -> void:
+       """Finish downloading wakatime, handle errors"""
+       if result != HTTPRequest.RESULT_SUCCESS:
+               Utils.plugin_print_err("Error while downloading Wakatime CLI")
+               _disable_plugin()
+               return
+       
+       Utils.plugin_print("Wakatime CLI has been installed succesfully! Located at %s" % ZIP_PATH)
+       extract_files(ZIP_PATH, get_waka_dir())
+       
+func _decompressor_download_finished(result, status, headers, body) -> void:
+       """Handle errors and finishi decompressor download"""
+       # Error while downloading
+       if result != HTTPRequest.RESULT_SUCCESS:
+               Utils.plugin_print_err("Error while downloading Ouch! library")
+               _disable_plugin()
+               return
+       
+       # Error while saving
+       if !DecompressorUtils.lib_exists(decompressor_cli, system_platform, PLUGIN_PATH):
+               Utils.plugin_print_err("Failed to save Ouch! library")
+               _disable_plugin()
+               return
+
+       # Save decompressor path, give write permissions to it
+       var decompressor: String = \
+               ProjectSettings.globalize_path(DecompressorUtils.decompressor_cli(decompressor_cli, 
+                       system_platform, PLUGIN_PATH))
+                       
+       if system_platform == "linux" or system_platform == "darwin":
+               OS.execute("chmod", ["+x", decompressor], [], true)
+               
+       # Extract files, allowing usage of Ouch!
+       Utils.plugin_print("Ouch! has been installed succesfully! Located at %s" % \
+               DecompressorUtils.decompressor_cli(decompressor_cli, system_platform, PLUGIN_PATH))
+       extract_files(ZIP_PATH, get_waka_dir())
+       
+func extract_files(source: String, destination: String) -> void:
+       """Extract downloaded Wakatime zip"""
+       # If decompression library and wakatime zip folder don't exist, return
+       if not DecompressorUtils.lib_exists(decompressor_cli, system_platform, 
+                       PLUGIN_PATH) and not Utils.wakatime_zip_exists(ZIP_PATH):
+               return
+               
+       # Get paths as global
+       Utils.plugin_print("Extracting Wakatime...")
+       var decompressor: String
+       if system_platform == "windows":
+               decompressor = ProjectSettings.globalize_path( 
+                       DecompressorUtils.decompressor_cli(decompressor_cli, system_platform, PLUGIN_PATH))
+       else:
+               decompressor = ProjectSettings.globalize_path("res://" +
+                       DecompressorUtils.decompressor_cli(decompressor_cli, system_platform, PLUGIN_PATH))
+               
+       var src: String = ProjectSettings.globalize_path(source)
+       var dest: String = ProjectSettings.globalize_path(destination)
+       
+       # Execute Ouch! decompression command, catch errors
+       var errors: Array[Variant] = []
+       var args: Array[String] = ["--yes", "decompress", src, "--dir", dest]
+       
+       var error: int = OS.execute(decompressor, args, errors, true)
+       if error:
+               Utils.plugin_print(errors)
+               _disable_plugin()
+               return
+               
+       # Results
+       if Utils.wakatime_cli_exists(get_waka_cli()):
+               Utils.plugin_print("Wakatime CLI installed (path: %s)" % get_waka_cli())
+       else:
+               Utils.plugin_print_err("Installation of Wakatime failed")
+               _disable_plugin()
+               
+       # Remove leftover files
+       clean_files()
+       
+func clean_files():
+       """Delete files that aren't needed anymore"""
+       if Utils.wakatime_zip_exists(ZIP_PATH):
+               delete_file(ZIP_PATH)
+       if DecompressorUtils.lib_exists(decompressor_cli, system_platform, PLUGIN_PATH):
+               delete_file(ZIP_PATH)
+               
+func delete_file(path: String) -> void:
+       """Delete file at specified path"""
+       var dir: DirAccess = DirAccess.open("res://")
+       var status: int = dir.remove(path)
+       if status != OK:
+               Utils.plugin_print_err("Failed to clean unnecesary file at %s" % path)
+       else:
+               Utils.plugin_print("Clean unncecesary file at %s" % path)
+
+#------------------------------- API KEY FUNCTIONS -------------------------------
+
+func get_api_key():
+       """Get wakatime api key"""
+       var result = []
+       # Handle errors while getting the key
+       var err = OS.execute(get_waka_cli(), ["--config-read", "api_key"], result)
+       if err == -1:
+               return null
+               
+       # Trim API key from whitespaces and return it
+       var key = result[0].strip_edges()
+       if key.is_empty():
+               return null
+       return key
+       
+func request_api_key() -> void:
+       """Request Wakatime API key from the user"""
+       # Prepare prompt
+       var prompt = ApiKeyPrompt.instantiate()
+       _set_api_key(prompt, get_api_key())
+       _register_api_key_signals(prompt)
+       
+       # Show prompt and hide it on request
+       add_child(prompt)
+       prompt.popup_centered()
+       await prompt.popup_hide
+       prompt.queue_free()
+       
+func _set_api_key(prompt: PopupPanel, api_key) -> void:
+       """Set API key from prompt"""
+       # Safeguard against empty key
+       if api_key == null:
+               api_key = ''
+               
+       # Set correct text, to show API key
+       var edit_field: Node = prompt.get_node("VBoxContainer/HBoxContainerTop/LineEdit")
+       edit_field.text = api_key
+       
+func _register_api_key_signals(prompt: PopupPanel) -> void:
+       """Connect all signals related to API key popup"""
+       # Get all Nodes
+       var show_button: Node = prompt.get_node("VBoxContainer/HBoxContainerTop/ShowButton")
+       var save_button: Node = prompt.get_node("VBoxContainer/HBoxContainerBottom/SubmitButton")
+       
+       # Connect them to press events
+       show_button.connect("pressed", Callable(self, "_on_toggle_key_text").bind(prompt))
+       save_button.connect("pressed", Callable(self, "_on_save_key").bind(prompt))
+       prompt.connect("popup_hide", Callable(self, "_on_popup_hide").bind(prompt))
+       
+func _on_popup_hide(prompt: PopupPanel):
+       """Close the popup window when user wants to hide it"""
+       prompt.queue_free()
+       
+func _on_toggle_key_text(prompt: PopupPanel) -> void:
+       """Handle hiding and showing API key"""
+       # Get nodes
+       var show_button: Node = prompt.get_node("VBoxContainer/HBoxContainerTop/ShowButton")
+       var edit_field: Node = prompt.get_node("VBoxContainer/HBoxContainerTop/LineEdit")
+       
+       # Set the correct text and hide field if needed
+       edit_field.secret = not edit_field.secret
+       show_button.text = "Show" if edit_field.secret else "Hide"
+       
+func _on_save_key(prompt: PopupPanel) -> void:
+       """Handle entering API key"""
+       # Get text field node and api key that's entered
+       var edit_field: Node = prompt.get_node("VBoxContainer/HBoxContainerTop/LineEdit")
+       var api_key  = edit_field.text.strip_edges()
+       
+       # Try to set api key for wakatime and handle errors
+       var err: int = OS.execute(get_waka_cli(), ["--config-write", "api-key=%s" % api_key])
+       if err == -1:
+               Utils.plugin_print("Failed to save API key")
+       prompt.visible = false
+       
+#------------------------------- PLUGIN INFORMATIONS -------------------------------
+       
+func get_user_agent() -> String:
+       """Get user agent identifier"""
+       var os_name = OS.get_name().to_lower()
+       return "godot/%s godot-wakatime/%s" % [
+               get_engine_version(), 
+               _get_plugin_version()
+       ]
+       
+func _get_plugin_name() -> String:
+       """Get name of the plugin"""
+       return "Godot_Super-Wakatime"
+       
+func _get_plugin_version() -> String:
+       """Get plugin version"""
+       return "1.0.0"
+       
+func _get_editor_name() -> String:
+       """Get name of the editor"""
+       return "Godot%s" % get_engine_version()
+       
+func get_engine_version() -> String:
+       """Get verison of currently used engine"""
+       return "%s.%s.%s" % [Engine.get_version_info()["major"], Engine.get_version_info()["minor"], 
+               Engine.get_version_info()["patch"]]
diff --git a/addons/godot_super-wakatime/godot_super-wakatime.gd.uid b/addons/godot_super-wakatime/godot_super-wakatime.gd.uid
new file mode 100644 (file)
index 0000000..04253e3
--- /dev/null
@@ -0,0 +1 @@
+uid://cvox0hdbntt3o
diff --git a/addons/godot_super-wakatime/heartbeat.gd b/addons/godot_super-wakatime/heartbeat.gd
new file mode 100644 (file)
index 0000000..bd232ab
--- /dev/null
@@ -0,0 +1,22 @@
+const FILE_MODIFIED_DELAY = 120
+
+var file_path
+var scene_path
+var entity
+var type
+var category
+var time
+var is_write
+var cursorpos
+var lines
+var line_no
+var line_additions
+var line_deletions
+var language
+var project
+
+func _init(file_path: String = '', time: int = 0, is_write: bool = false):
+       self.file_path = file_path
+       self.time = time
+       self.is_write = is_write
+       self.scene_path = scene_path
diff --git a/addons/godot_super-wakatime/heartbeat.gd.uid b/addons/godot_super-wakatime/heartbeat.gd.uid
new file mode 100644 (file)
index 0000000..8a0f96b
--- /dev/null
@@ -0,0 +1 @@
+uid://bexs758bw2yb0
diff --git a/addons/godot_super-wakatime/plugin.cfg b/addons/godot_super-wakatime/plugin.cfg
new file mode 100644 (file)
index 0000000..ec1f701
--- /dev/null
@@ -0,0 +1,7 @@
+[plugin]
+
+name="Godot Super-Wakatime"
+description="Wakatime plugin for Godot, which provides many information to track your time in the best way possible. Made for Hack Club, to allow teenagers to spend time building games and earn rewards"
+author="Bartosz Budnik"
+version=""
+script="godot_super-wakatime.gd"
diff --git a/addons/godot_super-wakatime/utils.gd b/addons/godot_super-wakatime/utils.gd
new file mode 100644 (file)
index 0000000..4366986
--- /dev/null
@@ -0,0 +1,71 @@
+func plugin_print(msg) -> void:
+       """Print message from plugin"""
+       print("[Godot_Super-Wakatime]: %s" % msg)
+       
+func plugin_print_err(err) -> void:
+       """Inform about error from plugin"""
+       push_error("[Godot_Super-Wakatime]: %s" % err)
+
+func set_platform():
+       """Set currently used platform"""
+       var platform: String = "linux"
+       var architecture: String = "arm64"
+
+       if OS.has_feature("windows") or OS.has_feature("uwp"):
+               platform = "windows"
+       elif OS.has_feature("linux"):
+               platform = "linux"
+       elif OS.has_feature("macos"):
+               platform = "darwin"
+       elif OS.has_feature("android"):
+               platform = "android"
+       elif OS.has_feature("ios"):
+               platform = "ios"
+       
+       if OS.has_feature("x86_64"):
+               architecture = "amd64"
+       elif OS.has_feature("x86_32"):
+               architecture = "amd32"
+       elif OS.has_feature("arm64"):
+               architecture = "arm64"
+       elif OS.has_feature("arm32"):
+               architecture = "arm32"
+
+       return [platform, architecture]
+
+func get_waka_build(platform: String, architecture: String) -> String:
+       """Return wakatime build for current OS"""
+       return "wakatime-cli-%s-%s" % [platform, architecture]
+
+func get_ouch_build(system_platform: String) -> String:
+       """Get build for ouch (compression and decompression tool)"""
+       var platform: String = "linux-musl"
+       if system_platform == "windows":
+               platform = "pc-windows-msvc"
+       elif system_platform == "darwin":
+               platform = "apple-darwin"
+
+       return "ouch-%s-%s" % ["x86_64", platform]
+       
+func home_directory(platform: String, plugin_path: String) -> String:
+       """Get home directory from """
+       var home = null
+       for env in ["WAKATIME_HOME", "USERPROFILE", "HOME"]:
+               home = OS.get_environment(env)
+               if home:
+                       if platform == "windows":
+                               home = home.replace("\\", '/')
+                       return home
+       return plugin_path
+       
+func config_filepath(platform: String, plugin_path: String) -> String:
+       """Get path to wakatime configuration file"""
+       return "%s/.wakatime.cfg" % home_directory(platform, plugin_path)
+       
+func wakatime_cli_exists(wakatime_cli) -> bool:
+       """Return if wakatime cli tool exists already"""
+       return FileAccess.file_exists(wakatime_cli)
+       
+func wakatime_zip_exists(wakatime_zip: String) -> bool:
+       "Check if wakatime zip file exists"
+       return FileAccess.file_exists(wakatime_zip)
diff --git a/addons/godot_super-wakatime/utils.gd.uid b/addons/godot_super-wakatime/utils.gd.uid
new file mode 100644 (file)
index 0000000..f2fd2be
--- /dev/null
@@ -0,0 +1 @@
+uid://cg2vp0pl0l1bs
index 2ef5c8e5a2c9d470df6077384ff56377ed4c918c..c1a0fa14c877375e0633eeb7c66139a2f9f9f377 100644 (file)
@@ -13,6 +13,10 @@ config_version=5
 config/name="Rushbound"
 config/features=PackedStringArray("4.4", "GL Compatibility")
 
+[editor_plugins]
+
+enabled=PackedStringArray("res://addons/godot_super-wakatime/plugin.cfg")
+
 [rendering]
 
 renderer/rendering_method="gl_compatibility"