diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3e32aec..5b1602a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -49,7 +49,7 @@ jobs:
# Setup Gradle
- name: Setup Gradle
- uses: gradle/gradle-build-action@v2
+ uses: gradle/gradle-build-action@v3
with:
gradle-home-cache-cleanup: true
@@ -113,7 +113,7 @@ jobs:
# Setup Gradle
- name: Setup Gradle
- uses: gradle/gradle-build-action@v2
+ uses: gradle/gradle-build-action@v3
with:
gradle-home-cache-cleanup: true
@@ -131,7 +131,7 @@ jobs:
# Upload the Kover report to CodeCov
- name: Upload Code Coverage Report
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@v4
with:
files: ${{ github.workspace }}/build/reports/kover/report.xml
@@ -166,7 +166,7 @@ jobs:
# Run Qodana inspections
- name: Qodana - Code Inspection
- uses: JetBrains/qodana-action@v2023.3.0
+ uses: JetBrains/qodana-action@v2023.3.1
with:
cache-default-branch-only: true
@@ -197,13 +197,13 @@ jobs:
# Setup Gradle
- name: Setup Gradle
- uses: gradle/gradle-build-action@v2
+ uses: gradle/gradle-build-action@v3
with:
gradle-home-cache-cleanup: true
# Cache Plugin Verifier IDEs
- name: Setup Plugin Verifier IDEs Cache
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ${{ needs.build.outputs.pluginVerifierHomeDir }}/ides
key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0d48e56..ef10f57 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -33,7 +33,7 @@ jobs:
# Setup Gradle
- name: Setup Gradle
- uses: gradle/gradle-build-action@v2
+ uses: gradle/gradle-build-action@v3
with:
gradle-home-cache-cleanup: true
diff --git a/.github/workflows/run-ui-tests.yml b/.github/workflows/run-ui-tests.yml
index 05e483b..aedb91d 100644
--- a/.github/workflows/run-ui-tests.yml
+++ b/.github/workflows/run-ui-tests.yml
@@ -44,7 +44,7 @@ jobs:
# Setup Gradle
- name: Setup Gradle
- uses: gradle/gradle-build-action@v2
+ uses: gradle/gradle-build-action@v3
with:
gradle-home-cache-cleanup: true
@@ -54,7 +54,7 @@ jobs:
# Wait for IDEA to be started
- name: Health Check
- uses: jtalk/url-health-check-action@v3
+ uses: jtalk/url-health-check-action@v4
with:
url: http://127.0.0.1:8082
max-attempts: 15
diff --git a/.gitignore b/.gitignore
index e2e5d94..080816f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,6 @@
.idea
.qodana
build
+
+.notes
+.run
\ No newline at end of file
diff --git a/.run/Run Plugin.run.xml b/.run/Run Plugin.run.xml
index d15ff68..6df3995 100644
--- a/.run/Run Plugin.run.xml
+++ b/.run/Run Plugin.run.xml
@@ -19,6 +19,7 @@
true
true
false
+ false
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5ac3bb2..4c1734f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,4 +4,7 @@
## [Unreleased]
### Added
-- Initial scaffold created from [IntelliJ Platform Plugin Template](https://github.com/JetBrains/intellij-platform-plugin-template)
+- Embedded Wokwi simulator
+- Wokwi console to control simulator
+- wokwi.toml analysis support
+- Wokwi simulation debugging support
diff --git a/README.md b/README.md
index b6c816d..53958cd 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,119 @@
-# IntelliJ Wokwi Simulator Plugin
-This plugin integrates the [Wokwi](https://wokwi.com/) simulator for ESP32 devices into JetBrains' IntelliJ-based IDEs.
-The project is currently in its early stages and has not yet been published on JetBrains' Marketplace.
+
-## Project Status
+
+[![Contributors][contributors-shield]][contributors-url]
+[![MIT License][license-shield]][license-url]
+[![Issues][issues-shield]][issues-url]
+[![Forks][forks-shield]][forks-url]
+[![Stargazers][stars-shield]][stars-url]
-The version in the `main` branch is very unstable and uses a non-maintaned version of the Wokwi simulator. Therefore, the plugin is rewritten and uses the same Wokwi simulator as the official VS-Code extension. In addition, the plugin configuration is aligned with the VS-Code configuration, i.e. the file `wokwi.toml` defines all relevant settings. This makes switching between IDEs effortless.
+
+
+
+
+
+
+
+
-The progress of the new plugin version is tracked in the pull request [#14](https://github.com/Jozott00/wokwi-intellij/pull/14).
+
Wokwi Intellij Plugin
+
+ Integrate Wokwi in Intellij-based Jetbrains IDEs.
+
+ Explore the docs »
+
+
+ Report Bug
+ ·
+ Request Feature
+
+
+
+## About The Plugin
-## Main branch version
+
-At present, it is possible to specify and run a binary on the Wokwi simulator within the IDE. By enabling binary watch,
-the simulation automatically restarts after each new binary build.
+
+The Wokwi Intellij plugin, an open-source tool, integrates the [Wokwi](https://wokwi.com) simulator with Jetbrains IDEs like CLion and RustRover.
+It adopts the Wokwi VS Code extension's configuration approach for seamless IDE transitions, supporting the same platforms.
+
+This plugin is a community plugin and not maintained by the [Wokwi](https://wokwi.com) team.
+
+
+### Features
+- Run simulation in IDE window
+- Automatically restart the simulation on rebuild
+- Intelligent configuration checking
+- Intellij idiomatic debugging (CLion only)
+
+
+## Documentation
+
+Please visit the [Wokwi Intellij documentation](https://jozott00.github.io/wokwi-intellij/starter-topic.html).
+
+### Installation
+
+To follow the installation instructions, users typically navigate to the [installation section](https://jozott00.github.io/wokwi-intellij/starter-topic.html#installation).
+
+For building and installing the plugin from source:
+1. Clone or download the repository.
+2. Execute `./gradlew buildPlugin`.
This action saves the plugin build as `build/distributions/wokwi-intellij-x.x.x.zip`.
+3. Follow steps to [install the plugin from disk](https://www.jetbrains.com/help/idea/managing-plugins.html#install_plugin_from_disk).
+
+
+
+
+## Roadmap
+
+- [ ] Make Console writable
+- [ ] Add Serial Port forwarding
+- [ ] Add IoT gateway
+- [ ] Support custom chips
+- [ ] Add diagram.json editor
+
+See the [open issues](https://github.com/Jozott00/wokwi-intellij/issues) for a full list of proposed features (and known issues).
+
+
+
+## Contributing
+
+To make this plugin even better, contributions are very welcome!
+
+If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag `enhancement`.
+1. Fork the Project
+2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
+3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
+4. Push to the Branch (`git push origin feature/AmazingFeature`)
+5. Open a Pull Request
+
+
+
+
+## License
+
+Distributed under the MIT License. See `LICENSE.txt` for more information.
+
+
+
+
+
+
+[contributors-shield]: https://img.shields.io/github/contributors/jozott00/wokwi-intellij.svg
+[contributors-url]: https://github.com/Jozott00/wokwi-intellij/graphs/contributors
+[forks-shield]: https://img.shields.io/github/forks/jozott00/wokwi-intellij.svg
+[forks-url]: https://github.com/Jozott00/wokwi-intellij/network/members
+[stars-shield]: https://img.shields.io/github/stars/jozott00/wokwi-intellij.svg
+[stars-url]: https://github.com/Jozott00/wokwi-intellij/stargazers
+[issues-shield]: https://img.shields.io/github/issues/jozott00/wokwi-intellij.svg
+[issues-url]: https://github.com/Jozott00/wokwi-intellij/issues
+[license-shield]: https://img.shields.io/github/license/Jozott00/wokwi-intellij.svg
+[license-url]: https://github.com/Jozott00/wokwi-intellij/blob/master/LICENSE.txt
-
-
diff --git a/blob/imgs/logoColorful.svg b/blob/imgs/logoColorful.svg
new file mode 100644
index 0000000..9f76a06
--- /dev/null
+++ b/blob/imgs/logoColorful.svg
@@ -0,0 +1,16 @@
+
diff --git a/blob/imgs/pluginIcon.svg b/blob/imgs/pluginIcon.svg
new file mode 100644
index 0000000..7f3b453
--- /dev/null
+++ b/blob/imgs/pluginIcon.svg
@@ -0,0 +1,45 @@
+
+
+
diff --git a/blob/imgs/pluginIcon_dark.svg b/blob/imgs/pluginIcon_dark.svg
new file mode 100644
index 0000000..f1ca467
--- /dev/null
+++ b/blob/imgs/pluginIcon_dark.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/blob/imgs/sim_cfg.png b/blob/imgs/sim_cfg.png
new file mode 100644
index 0000000..73e4666
Binary files /dev/null and b/blob/imgs/sim_cfg.png differ
diff --git a/blob/imgs/sim_dbg.png b/blob/imgs/sim_dbg.png
new file mode 100644
index 0000000..6a4c04f
Binary files /dev/null and b/blob/imgs/sim_dbg.png differ
diff --git a/blob/imgs/sim_running.png b/blob/imgs/sim_running.png
new file mode 100644
index 0000000..3588d55
Binary files /dev/null and b/blob/imgs/sim_running.png differ
diff --git a/build.gradle.kts b/build.gradle.kts
index 481bad5..aaaca0d 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -27,7 +27,11 @@ repositories {
dependencies {
implementation(files("libs/espimg-0.1.0.jar"))
implementation("org.java-websocket:Java-WebSocket:1.5.4")
- implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
+ implementation("com.beust:klaxon:5.6")
+ implementation("com.akuleshov7:ktoml-core:0.5.1")
+ implementation("com.akuleshov7:ktoml-file:0.5.1")
+ implementation("io.arrow-kt:arrow-core:1.2.1")
}
// Set the JVM language level used to build the project. Use Java 11 for 2020.3+, and Java 17 for 2022.2+.
@@ -148,7 +152,6 @@ tasks {
// The pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3
// Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more:
// https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel
- channels =
- properties("pluginVersion").map { listOf(it.split('-').getOrElse(1) { "default" }.split('.').first()) }
+ channels = properties("pluginVersion").map { listOf(it.split('-').getOrElse(1) { "default" }.split('.').first()) }
}
}
diff --git a/docs/devnotes/c.list b/docs/devnotes/c.list
new file mode 100644
index 0000000..c4c77a2
--- /dev/null
+++ b/docs/devnotes/c.list
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/devnotes/redirection-rules.xml b/docs/devnotes/redirection-rules.xml
new file mode 100644
index 0000000..abcd171
--- /dev/null
+++ b/docs/devnotes/redirection-rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Created after removal of "Debug Architecture" from wokwi-intellij-notes
+ Debug-Architecture.html
+
+
\ No newline at end of file
diff --git a/docs/devnotes/topics/Communication-Architecture.md b/docs/devnotes/topics/Communication-Architecture.md
new file mode 100644
index 0000000..12420ed
--- /dev/null
+++ b/docs/devnotes/topics/Communication-Architecture.md
@@ -0,0 +1,24 @@
+# Communication Architecture
+
+The plugin uses a wrapper around the simulator webview (iframe), to establish communication with the plugin.
+It essentially serves as communcation proxy between the plugin and webview, as the communication
+requirements for both sides are different.
+
+> **Wcode** is the VSCode version of the Wokwi Simulator
+
+```mermaid
+sequenceDiagram
+ Intellij Plugin -->> Wcode Wrapper: Injects MessageRouter to listen on window.jcef()
+ Wcode -->> Wcode Wrapper: Sends message using postMessage(..., port)
+
+ Wcode Wrapper --> Wcode: Communicates over exchanged port
+ Intellij Plugin --> Wcode Wrapper: Communicates over injected message router
+
+ Intellij Plugin --> Wcode: Communicates using Wcode Wrapper in between
+```
+
+## Wcode
+
+The url of the Wcode simulator is `https://wokwi.com/vscode/wcode?v=`
+
+E.g. `https://wokwi.com/vscode/wcode?v=2.4.0&g=10277ff&u=385442252248670209`
diff --git a/docs/devnotes/topics/Debugger.md b/docs/devnotes/topics/Debugger.md
new file mode 100644
index 0000000..8c3b64d
--- /dev/null
+++ b/docs/devnotes/topics/Debugger.md
@@ -0,0 +1,11 @@
+# Debugger
+
+> This is not yet a good note
+{style="warning"}
+
+
+ Research on other debuggers
+ Create custom runner
+
+
+See [Custom Intellij Debugger](Intellij-Debugger.md) for research results.
diff --git a/docs/devnotes/topics/Intellij-Debugger.md b/docs/devnotes/topics/Intellij-Debugger.md
new file mode 100644
index 0000000..bc8fea7
--- /dev/null
+++ b/docs/devnotes/topics/Intellij-Debugger.md
@@ -0,0 +1,17 @@
+# Intellij Debugger
+
+Research results regarding the Intellij platform debuggers.
+
+**Intellij Community Classes**
+- [XDebugProcess Abstract Class](https://github.com/JetBrains/intellij-community/blob/master/platform/xdebugger-api/src/com/intellij/xdebugger/XDebugProcess.java#L37)
+ provides debugging capabilities for a custom language/framework
+
+
+## Python Intellij Debugger
+[Source Directory](https://github.com/JetBrains/intellij-community/tree/master/python/src/com/jetbrains/python/debugger)
+
+The [PyRemoteDebugProcess](https://github.com/JetBrains/intellij-community/blob/master/python/src/com/jetbrains/python/debugger/PyRemoteDebugProcess.java)
+and its [PyDebugProcess](https://github.com/JetBrains/intellij-community/blob/master/python/src/com/jetbrains/python/debugger/PyDebugProcess.java) might be
+a good starting point to implement a debug process to attach to Wokwi's GDB stub.
+
+For the Runner take a look at the [Execution Documentation](https://plugins.jetbrains.com/docs/intellij/execution.html)
\ No newline at end of file
diff --git a/docs/devnotes/topics/Wokwi-Code-Notes.md b/docs/devnotes/topics/Wokwi-Code-Notes.md
new file mode 100644
index 0000000..cb83608
--- /dev/null
+++ b/docs/devnotes/topics/Wokwi-Code-Notes.md
@@ -0,0 +1,127 @@
+# Wokwi Code Notes
+
+## Firmware Packaging
+
+Reads all required flash files and combines them in a `[[{offset: _, data: _}}]]` array at the right offset.
+This is only used for **ESP-IDF** projects (C++ only), with a `build/flasher_args.json`.
+
+```Javascript
+a.packageEspIdfFirmware = async function (flasherArgsJson, firmwareBasePath) {
+ const flasherArgs = JSON.parse(flasherArgsJson.toString());
+
+ if (!flasherArgs.flash_files) {
+ throw new Error("flash_files key is missing in flasher_args.json");
+ }
+
+ let fileContents = [];
+ let filePaths = [];
+ let maxFirmwareSize = 0;
+
+ for (const [offsetHex, filePath] of Object.entries(flasherArgs.flash_files)) {
+ const offset = parseInt(offsetHex, 16);
+ if (isNaN(offset)) {
+ throw new Error(`Invalid offset in flasher_args.json: ${offsetHex}`);
+ }
+
+ const resolvedFilePath = path.resolve(firmwareBasePath, filePath);
+ filePaths.push(resolvedFilePath);
+
+ const fileData = await readFileOrNull(resolvedFilePath);
+ if (!fileData) {
+ throw new Error(`Could not read file: ${filePath}`);
+ }
+
+ fileContents.push({offset, data: fileData});
+ maxFirmwareSize = Math.max(maxFirmwareSize, offset + fileData.byteLength);
+ }
+
+ if (maxFirmwareSize > 16777216) {
+ throw new Error(`Firmware size (${maxFirmwareSize} bytes) exceeds maximum supported size (16777216 bytes)`);
+ }
+
+ const firmwareData = new Uint8Array(maxFirmwareSize);
+ for (const {offset, data} of fileContents) {
+ firmwareData.set(new Uint8Array(data), offset);
+ }
+
+ return {
+ firmware: firmwareData,
+ watchPaths: filePaths
+ };
+};
+
+```
+
+{collapsible="true"}
+
+```javascript
+// Listener for the 'start' event with an asynchronous callback function
+t.listen("start", async settings => {
+ // Destructuring the settings object for easier access to its properties
+ let {
+ diagram: diagramJSON,
+ firmware: firmwareData,
+ chips: chipSettings,
+ useGateway: shouldUseGateway,
+ pause: shouldPause,
+ firmwareB64: isFirmwareBase64,
+ disableSerialMonitor: shouldDisableSerialMonitor
+ } = settings;
+
+ try {
+ // Attempt to process the license and initialize some component (represented by L())
+ x(await k(settings.license, L()))
+ } catch (error) {
+ // Handle any errors during the process
+ j(true)
+ }
+
+ // Check if the firmware data is not in the expected format
+ if (typeof firmwareData === "object" && !(firmwareData instanceof ArrayBuffer)) {
+ // Send a command to switch to Base64 if the condition is met
+ t.write({
+ command: "switchToBase64"
+ });
+ return;
+ }
+
+ // Processing chip settings if available
+ for (let chip of chipSettings || []) {
+ e.addFile(`${chip.name}.chip.json`, JSON.stringify(chip.json), true);
+ }
+
+ // Adding diagram and configuring firmware data
+ e.addFile("diagram.json", diagramJSON, true),
+ r.overrideHex = isFirmwareBase64 ? (0, A.Xs)(firmwareData).buffer : firmwareData,
+ r.wifiGateway = shouldUseGateway ? i : undefined,
+ e.setChips(chipSettings),
+
+ // Handling chip output
+ r.onChipOutput = (chipName, message) => {
+ t.write({
+ command: "chipOutput",
+ chipName: chipName,
+ message: message
+ })
+ },
+
+ // Setting up the current state
+ q.current = {
+ diagram: e.diagram,
+ files: [],
+ chips: chipSettings,
+ autoPause: shouldPause,
+ hideSerialMonitor: shouldDisableSerialMonitor
+ },
+
+ // Starting the process and handling completion
+ r.start(q.current).then(() => {
+ let wifiStatus;
+ g(false),
+ wifiStatus = r.wifi?.status;
+ wifiStatus?.setGatewayType("vscode")
+ })
+});
+```
+
+{collapsible="true"}
\ No newline at end of file
diff --git a/docs/devnotes/topics/Wokwi-Config.md b/docs/devnotes/topics/Wokwi-Config.md
new file mode 100644
index 0000000..3bc4a4b
--- /dev/null
+++ b/docs/devnotes/topics/Wokwi-Config.md
@@ -0,0 +1,7 @@
+# Wokwi Config
+
+We use the TOML intellij plugin as foundation layer of Wokwi configs.
+
+> Take a look at the [Rust-Intellij extension of TOML](https://github.com/intellij-rust/intellij-rust/pull/1982/files)
+> And
+> the [current implementation](https://github.com/intellij-rust/intellij-rust/tree/master/src/main/kotlin/org/rust/toml)
\ No newline at end of file
diff --git a/docs/devnotes/topics/starter-topic.md b/docs/devnotes/topics/starter-topic.md
new file mode 100644
index 0000000..5427892
--- /dev/null
+++ b/docs/devnotes/topics/starter-topic.md
@@ -0,0 +1,4 @@
+# Overview
+
+This is the developer notes documentation to archive the Wokwi infrastructure, code examples, related documentation and more.
+
diff --git a/docs/devnotes/v.list b/docs/devnotes/v.list
new file mode 100644
index 0000000..2d12cb3
--- /dev/null
+++ b/docs/devnotes/v.list
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/docs/devnotes/wokwi-intellij.tree b/docs/devnotes/wokwi-intellij.tree
new file mode 100644
index 0000000..24c6f12
--- /dev/null
+++ b/docs/devnotes/wokwi-intellij.tree
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/devnotes/writerside.cfg b/docs/devnotes/writerside.cfg
new file mode 100644
index 0000000..2d778c3
--- /dev/null
+++ b/docs/devnotes/writerside.cfg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 49a8a62..7a17c63 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,34 +1,25 @@
# IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html
-
-pluginGroup = com.github.jozott00.wokwiintellij
-pluginName = wokwi-intellij
-pluginRepositoryUrl = https://github.com/Jozott00/wokwi-intellij
+pluginGroup=com.github.jozott00.wokwiintellij
+pluginName=wokwi-intellij
+pluginRepositoryUrl=https://github.com/Jozott00/wokwi-intellij
# SemVer format -> https://semver.org
-pluginVersion = 0.0.1
-
+pluginVersion=0.9.0
# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
-pluginSinceBuild = 223
-pluginUntilBuild = 233.*
-
+pluginSinceBuild=232
+pluginUntilBuild=233.*
# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
-platformType = IC
-platformVersion = 2023.3.2
-
+platformType=IC
+platformVersion=2023.3
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
-platformPlugins =
-
+platformPlugins=org.toml.lang
# Gradle Releases -> https://github.com/gradle/gradle/releases
-gradleVersion = 8.5
-
+gradleVersion=8.5
# Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib
-kotlin.stdlib.default.dependency = false
-
+kotlin.stdlib.default.dependency=false
# Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html
-org.gradle.configuration-cache = true
-
+org.gradle.configuration-cache=true
# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html
-org.gradle.caching = true
-
+org.gradle.caching=true
# Enable Gradle Kotlin DSL Lazy Property Assignment -> https://docs.gradle.org/current/userguide/kotlin_dsl.html#kotdsl:assignment
-systemProp.org.gradle.unsafe.kotlin.assignment = true
+systemProp.org.gradle.unsafe.kotlin.assignment=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f6b4ec3..773bb14 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -5,7 +5,7 @@ annotations = "24.1.0"
# plugins
kotlin = "1.9.22"
changelog = "2.2.0"
-gradleIntelliJPlugin = "1.16.1"
+gradleIntelliJPlugin = "1.17.0"
qodana = "0.1.13"
kover = "0.7.5"
diff --git a/settings.gradle.kts b/settings.gradle.kts
index d597ebc..e9cf604 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,6 +1,6 @@
plugins {
- id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0"
+ id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
rootProject.name = "wokwi-intellij"
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/MyBundle.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/WokwiBundle.kt
similarity index 83%
rename from src/main/kotlin/com/github/jozott00/wokwiintellij/MyBundle.kt
rename to src/main/kotlin/com/github/jozott00/wokwiintellij/WokwiBundle.kt
index 273c49d..7277ed4 100644
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/MyBundle.kt
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/WokwiBundle.kt
@@ -5,9 +5,9 @@ import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.PropertyKey
@NonNls
-private const val BUNDLE = "messages.MyBundle"
+private const val BUNDLE = "messages.WokwiBundle"
-object MyBundle : DynamicBundle(BUNDLE) {
+object WokwiBundle : DynamicBundle(BUNDLE) {
@JvmStatic
fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) =
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/WokwiConstants.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/WokwiConstants.kt
new file mode 100644
index 0000000..a14ae06
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/WokwiConstants.kt
@@ -0,0 +1,12 @@
+package com.github.jozott00.wokwiintellij
+
+object WokwiConstants {
+ const val WOWKI_PLUGIN_SERVICE_NAME = "WokwiIntellij"
+ const val TOOL_WINDOW_SIM_ID = "Wokwi Simulator"
+ const val WOKWI_CONFIG_FILE = "wokwi.toml"
+ const val WOKWI_DIAGRAM_FILE = "diagram.json"
+ const val WOKWI_DEFAULT_CONFIG_VERSION = "1"
+ const val WOKWI_LICENCE_STORE_KEY = "WokwiLicense"
+ const val WOKWI_WCODE_VERSION = "1.0"
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiRestartAction.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiRestartAction.kt
index be49a63..1349448 100644
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiRestartAction.kt
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiRestartAction.kt
@@ -3,12 +3,11 @@ package com.github.jozott00.wokwiintellij.actions
import com.github.jozott00.wokwiintellij.services.WokwiProjectService
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
-import com.intellij.openapi.actionSystem.ToggleAction
import com.intellij.openapi.components.service
class WokwiRestartAction : AnAction() {
override fun actionPerformed(p0: AnActionEvent) {
- p0.project?.service()?.restartSimulation()
+ p0.project?.service()?.startSimulator()
}
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiStartAction.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiStartAction.kt
index 4e33042..e39abee 100644
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiStartAction.kt
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiStartAction.kt
@@ -1,15 +1,20 @@
package com.github.jozott00.wokwiintellij.actions
-import com.github.jozott00.wokwiintellij.services.WokwiComponentService
import com.github.jozott00.wokwiintellij.services.WokwiProjectService
-import com.github.jozott00.wokwiintellij.services.WokwiSimulationService
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.components.service
class WokwiStartAction : AnAction() {
+
+
+ // TODO: Consider run configuration instead of custom run handling
override fun actionPerformed(event: AnActionEvent) {
+// event.project?.let {
+// val config = RunManager.getInstance(it).createConfiguration("Wokwi Runner", WokwiConfigurationFactory(WokwiRunConfigType()))
+// ProgramRunnerUtil.executeConfiguration(config, DefaultRunExecutor.getRunExecutorInstance())
+// }
val s = event.project?.service()
s?.startSimulator()
}
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiStopAction.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiStopAction.kt
index 4cf7d7f..9ddbcfc 100644
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiStopAction.kt
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiStopAction.kt
@@ -1,15 +1,26 @@
package com.github.jozott00.wokwiintellij.actions
-import com.github.jozott00.wokwiintellij.services.WokwiComponentService
import com.github.jozott00.wokwiintellij.services.WokwiProjectService
-import com.github.jozott00.wokwiintellij.services.WokwiSimulationService
+import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.components.service
-import com.intellij.openapi.ui.Messages
class WokwiStopAction : AnAction() {
+
+ override fun getActionUpdateThread(): ActionUpdateThread {
+ return ActionUpdateThread.EDT
+ }
+
+ override fun update(e: AnActionEvent) {
+ val s = e.project?.service() ?: return
+ val p = e.presentation
+
+ p.isEnabled = s.isSimulatorRunning()
+
+ }
+
override fun actionPerformed(event: AnActionEvent) {
val s = event.project?.service()
s?.stopSimulator()
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiWatchAction.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiWatchAction.kt
index 46fca37..a3c4a1a 100644
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiWatchAction.kt
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiWatchAction.kt
@@ -1,24 +1,23 @@
package com.github.jozott00.wokwiintellij.actions
-import com.github.jozott00.wokwiintellij.services.WokwiProjectService
-import com.github.jozott00.wokwiintellij.states.WokwiConfigState
+import com.github.jozott00.wokwiintellij.states.WokwiSettingsState
+import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.ToggleAction
import com.intellij.openapi.components.service
class WokwiWatchAction : ToggleAction() {
+ override fun getActionUpdateThread(): ActionUpdateThread {
+ return ActionUpdateThread.EDT
+ }
+
override fun isSelected(p0: AnActionEvent): Boolean {
- return p0.project?.service()?.state?.watchElf ?: false
+ return p0.project?.service()?.state?.watchFirmware ?: false
}
override fun setSelected(even: AnActionEvent, watchEnabled: Boolean) {
- even.project?.service()?.state?.watchElf = watchEnabled
- if (watchEnabled) {
- even.project?.service()?.watchStart()
- } else {
- even.project?.service()?.watchStop()
- }
+ even.project?.service()?.state?.watchFirmware = watchEnabled
}
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/exceptions/results.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/exceptions/results.kt
new file mode 100644
index 0000000..8c28e3d
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/exceptions/results.kt
@@ -0,0 +1,4 @@
+package com.github.jozott00.wokwiintellij.exceptions
+
+open class WokwiError
+data class GenericError(val title: String, val message: String): WokwiError()
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/exceptions/utils.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/exceptions/utils.kt
new file mode 100644
index 0000000..797e766
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/exceptions/utils.kt
@@ -0,0 +1,10 @@
+package com.github.jozott00.wokwiintellij.exceptions
+
+
+inline fun catchIllArg(block: () -> R): Result {
+ return try {
+ Result.success(block())
+ } catch (e: IllegalArgumentException) {
+ Result.failure(e)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/extensions/DisposableExt.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/extensions/DisposableExt.kt
new file mode 100644
index 0000000..c897a44
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/extensions/DisposableExt.kt
@@ -0,0 +1,11 @@
+package com.github.jozott00.wokwiintellij.extensions
+
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.application.invokeLater
+import com.intellij.openapi.util.Disposer
+
+fun Disposable.disposeByDisposer() {
+ invokeLater {
+ Disposer.dispose(this)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/extensions/projectExt.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/extensions/projectExt.kt
new file mode 100644
index 0000000..2d27543
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/extensions/projectExt.kt
@@ -0,0 +1,62 @@
+package com.github.jozott00.wokwiintellij.extensions
+
+import com.github.jozott00.wokwiintellij.services.WokwiPluginDisposable
+import com.github.jozott00.wokwiintellij.services.WokwiProjectService
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.roots.ProjectRootManager
+import com.intellij.openapi.vfs.VirtualFile
+import kotlinx.coroutines.CoroutineScope
+import java.net.URI
+import java.nio.file.Path
+import kotlin.io.path.exists
+
+
+/**
+ * Represents a disposable object for the Wokwi plugin in a project.
+ *
+ * This property returns an instance of the `WokwiPluginDisposable` class, which is a service
+ * intended to be used as a parent disposable instead of the project itself.
+ */
+val Project.wokwiDisposable get() = service() as Disposable
+
+/**
+ * Creates a new CoroutineScope for the given childName in scope of the WokwiProjectService.
+ *
+ * @param childName the name of the child scope.
+ * @return the created CoroutineScope for the specified childName.
+ */
+@Suppress("unused")
+fun Project.wokwiCoroutineChildScope(childName: String): CoroutineScope {
+ return service().childScope(childName)
+}
+
+/**
+ * Finds the relative paths of files or directories within the project.
+ *
+ * @param path the path to resolve against project's content roots
+ * @return a list of resolved relative paths
+ */
+@Suppress("unused")
+fun Project.findRelativePaths(path: String): List {
+ val rootUrls = ProjectRootManager.getInstance(this).contentRootUrls
+ return rootUrls.map {
+ Path.of(URI(it)).resolve(path)
+ }.filter {
+ it.exists()
+ }
+}
+
+/**
+ * Finds the relative files based on the given path.
+ *
+ * @param path The relative path of the files to be found.
+ * @return A list of virtual files matching the given relative path. If no files are found, returns an empty list.
+ */
+fun Project.findRelativeFiles(path: String): List {
+ val rootUrls = ProjectRootManager.getInstance(this).contentRoots
+ return rootUrls.mapNotNull {
+ it.findFileByRelativePath(path)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/extensions/stringExt.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/extensions/stringExt.kt
new file mode 100644
index 0000000..df5d418
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/extensions/stringExt.kt
@@ -0,0 +1,7 @@
+package com.github.jozott00.wokwiintellij.extensions
+
+fun String.hexStringToByteArray(): ByteArray? = this.removePrefix("0x")
+ .chunked(2)
+ .map { it.toIntOrNull(16) ?: return null }
+ .map { it.toByte() }
+ .toByteArray()
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/ide/WokwiFileType.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/ide/WokwiFileType.kt
new file mode 100644
index 0000000..ad27a24
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/ide/WokwiFileType.kt
@@ -0,0 +1,23 @@
+package com.github.jozott00.wokwiintellij.ide
+
+import com.github.jozott00.wokwiintellij.ui.WokwiIcons
+import com.intellij.openapi.fileTypes.LanguageFileType
+import com.intellij.openapi.fileTypes.ex.FileTypeIdentifiableByVirtualFile
+import com.intellij.openapi.vfs.VirtualFile
+import org.toml.lang.TomlLanguage
+import org.toml.lang.psi.TomlFileType
+
+object WokwiFileType : LanguageFileType(TomlLanguage), FileTypeIdentifiableByVirtualFile {
+
+ override fun getName() = "WOKWI_TOML"
+
+ override fun getDescription() = "Wokwi configuration"
+
+ override fun getDefaultExtension() = "toml"
+
+ override fun getIcon() = WokwiIcons.ConfigFile
+
+ override fun isMyFileType(file: VirtualFile): Boolean {
+ return file.nameWithoutExtension == "wokwi" && file.extension == TomlFileType.defaultExtension
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/ide/inspections/ConfigVersionInspection.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/ide/inspections/ConfigVersionInspection.kt
new file mode 100644
index 0000000..b676c8f
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/ide/inspections/ConfigVersionInspection.kt
@@ -0,0 +1,49 @@
+package com.github.jozott00.wokwiintellij.ide.inspections
+
+import com.github.jozott00.wokwiintellij.WokwiBundle
+import com.github.jozott00.wokwiintellij.WokwiConstants
+import com.intellij.codeInspection.LocalQuickFix
+import com.intellij.codeInspection.ProblemDescriptor
+import com.intellij.codeInspection.ProblemsHolder
+import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiElementVisitor
+import org.toml.lang.psi.TomlKeyValue
+import org.toml.lang.psi.TomlPsiFactory
+
+class ConfigVersionInspection : WokwiConfigInspectionBase() {
+
+ override fun buildVisitorInternal(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
+ return object : WokwiConfigVisitor() {
+ override fun visitVersionValue(value: TomlKeyValue) {
+ super.visitVersionValue(value)
+
+ if (value.value?.text != "1") {
+ holder.registerProblem(
+ value,
+ WokwiBundle.message("config.inspection.version.invalid.problem.descriptor"),
+ SetValidVersionQuickFix
+ )
+ }
+ }
+ }
+ }
+
+
+ object SetValidVersionQuickFix : LocalQuickFix {
+ override fun getFamilyName(): String {
+ return WokwiBundle.message(
+ "config.inspection.version.invalid.quickfix.change",
+ WokwiConstants.WOKWI_DEFAULT_CONFIG_VERSION
+ )
+ }
+
+ override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
+ val elem = descriptor.psiElement as TomlKeyValue
+
+ val newElem = TomlPsiFactory(project, true)
+ .createKeyValue("version", WokwiConstants.WOKWI_DEFAULT_CONFIG_VERSION)
+ elem.replace(newElem)
+ }
+
+ }
+}
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/ide/inspections/ElfFirmwareInspection.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/ide/inspections/ElfFirmwareInspection.kt
new file mode 100644
index 0000000..f73068e
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/ide/inspections/ElfFirmwareInspection.kt
@@ -0,0 +1,49 @@
+package com.github.jozott00.wokwiintellij.ide.inspections
+
+import com.github.jozott00.wokwiintellij.WokwiBundle
+import com.github.jozott00.wokwiintellij.toml.stringValue
+import com.intellij.codeInspection.ProblemsHolder
+import com.intellij.openapi.vfs.LocalFileSystem
+import com.intellij.psi.PsiElementVisitor
+import org.toml.lang.psi.TomlKeyValue
+
+class ElfFirmwareInspection : WokwiConfigInspectionBase() {
+
+ override fun buildVisitorInternal(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
+ return object : WokwiConfigVisitor() {
+ override fun visitElfValue(value: TomlKeyValue) {
+ super.visitElfValue(value)
+ inspectBinaryPath(value)
+ }
+
+ override fun visitFirmwareValue(value: TomlKeyValue) {
+ super.visitFirmwareValue(value)
+ inspectBinaryPath(value)
+ }
+
+ private fun inspectBinaryPath(value: TomlKeyValue) {
+ val path = value.value?.stringValue ?: run {
+ holder.registerProblem(
+ value,
+ WokwiBundle.message("config.inspection.binary.invalid.string.descriptor")
+ )
+ return
+ }
+
+ val configRootDir = holder.file.virtualFile.parent
+ val filePath = configRootDir.toNioPath().resolve(path)
+ val file = LocalFileSystem.getInstance().findFileByNioFile(filePath)
+ if (file == null || file.isDirectory) {
+ holder.registerProblem(
+ value,
+ WokwiBundle.message("config.inspection.binary.invalid.path.descriptor", file?.path.toString())
+ )
+ return
+ }
+ }
+
+ }
+ }
+
+
+}
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/ide/inspections/MissingConfigurationInspection.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/ide/inspections/MissingConfigurationInspection.kt
new file mode 100644
index 0000000..11a1867
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/ide/inspections/MissingConfigurationInspection.kt
@@ -0,0 +1,179 @@
+package com.github.jozott00.wokwiintellij.ide.inspections
+
+import com.github.jozott00.wokwiintellij.WokwiBundle
+import com.github.jozott00.wokwiintellij.WokwiConstants
+import com.github.jozott00.wokwiintellij.toml.findTable
+import com.github.jozott00.wokwiintellij.toml.findValue
+import com.intellij.codeInsight.template.TemplateManager
+import com.intellij.codeInsight.template.impl.TextExpression
+import com.intellij.codeInspection.*
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.diagnostic.thisLogger
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.fileEditor.FileEditorManager
+import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiDocumentManager
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiFile
+import org.toml.lang.psi.TomlFile
+import org.toml.lang.psi.TomlPsiFactory
+import org.toml.lang.psi.TomlTable
+import org.toml.lang.psi.TomlTableHeader
+
+private fun InspectionManager.createErrorDescription(
+ elem: PsiElement,
+ descriptor: String,
+ quickFix: LocalQuickFix?,
+ type: ProblemHighlightType = ProblemHighlightType.ERROR
+): ProblemDescriptor {
+ return createProblemDescriptor(elem, descriptor, quickFix, type, false)
+}
+
+class MissingConfigurationInspection : WokwiConfigInspectionBase() {
+
+ override fun checkFileInternal(file: PsiFile, manager: InspectionManager, isOnTheFly: Boolean): Array {
+ val tomlFile = file as TomlFile
+
+ val wokwiTable = tomlFile.findTable("wokwi") ?: run {
+ return arrayOf(
+ manager.createErrorDescription(
+ tomlFile,
+ WokwiBundle.message("config.inspection.missing.wokwi.problem.descriptor"),
+ AddWokwiConfiguration
+ )
+ )
+ }
+
+ val problems = mutableListOf()
+
+ if (wokwiTable.findValue("version") == null) {
+ problems.add(
+ manager.createErrorDescription(
+ wokwiTable.header,
+ WokwiBundle.message("config.inspection.missing.version.problem.descriptor"),
+ AddWokwiAttribute(
+ "version",
+ WokwiConstants.WOKWI_DEFAULT_CONFIG_VERSION,
+ false,
+ WokwiBundle.message("config.inspection.missing.version.quickfix")
+ )
+ )
+ )
+ }
+
+ if (wokwiTable.findValue("elf") == null) {
+ problems.add(
+ manager.createErrorDescription(
+ wokwiTable.header,
+ WokwiBundle.message("config.inspection.missing.elf.problem.descriptor"),
+ AddWokwiAttribute(
+ "elf",
+ "path/to/your/elf",
+ true,
+ WokwiBundle.message("config.inspection.missing.elf.quickfix")
+ )
+ )
+ )
+ }
+
+ if (wokwiTable.findValue("firmware") == null) {
+ problems.add(
+ manager.createErrorDescription(
+ wokwiTable.header,
+ WokwiBundle.message("config.inspection.missing.firmware.problem.descriptor"),
+ AddWokwiAttribute(
+ "firmware",
+ "path/to/your/firmware",
+ true,
+ WokwiBundle.message("config.inspection.missing.firmware.quickfix"),
+ )
+ )
+ )
+ }
+
+
+
+ return problems.toTypedArray()
+ }
+
+ object AddWokwiConfiguration : LocalQuickFix {
+ override fun getFamilyName(): String {
+ return WokwiBundle.message("config.inspection.missing.wokwi.quickfix")
+ }
+
+ override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
+ val file = descriptor.psiElement as TomlFile
+ val factory = TomlPsiFactory(project, true)
+ val table = factory.createTable("wokwi")
+ table.add(factory.createNewline())
+
+ val version = factory.createKeyValue("version", WokwiConstants.WOKWI_DEFAULT_CONFIG_VERSION)
+ table.add(version)
+
+ file.add(table)
+
+ }
+
+ }
+
+ class AddWokwiAttribute(
+ private val attribute: String,
+ private val defaultValue: String,
+ private val runTemplate: Boolean,
+ private val familyName: String,
+ ) : LocalQuickFix {
+
+ override fun getFamilyName(): String {
+ return familyName
+ }
+
+ override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
+ if (!ApplicationManager.getApplication().isDispatchThread)
+ return
+
+ val factory = TomlPsiFactory(project, true)
+
+ val wokwiTable = when (val elem = descriptor.psiElement) {
+ is TomlTable -> elem
+ is TomlTableHeader -> elem.parent as TomlTable
+ else -> {
+ thisLogger().error("Invalid PSI element for AddWokwiAttribute quickfix: $elem")
+ return
+ }
+ }
+
+ wokwiTable.add(factory.createNewline())
+ if (!runTemplate) {
+ // if we do not want a template we just add the attribute
+ val elem = factory.createKeyValue(attribute, defaultValue)
+ wokwiTable.add(elem)
+ return
+ }
+
+
+ val editor =
+ FileEditorManager.getInstance(project).selectedTextEditor ?: return
+ PsiDocumentManager.getInstance(project)
+ .doPostponedOperationsAndUnblockDocument(editor.document)
+ insertTemplate(wokwiTable, project, editor)
+ }
+
+ private fun insertTemplate(wokwiTable: TomlTable, project: Project, editor: Editor) {
+ val templateManager = TemplateManager.getInstance(project)
+ val template = templateManager.createTemplate("", "", "$attribute = \"\$PATH$\"").apply {
+ addVariable("PATH", TextExpression(defaultValue), true)
+ }
+
+ // Insert the template at end of wokwi table
+ val caretModel = editor.caretModel
+ val offset = wokwiTable.textRange.endOffset
+ caretModel.moveToOffset(offset)
+
+ templateManager.startTemplate(editor, template)
+ }
+
+
+ }
+
+
+}
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/ide/inspections/WokwiConfigInspectionBase.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/ide/inspections/WokwiConfigInspectionBase.kt
new file mode 100644
index 0000000..74c74b2
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/ide/inspections/WokwiConfigInspectionBase.kt
@@ -0,0 +1,31 @@
+package com.github.jozott00.wokwiintellij.ide.inspections
+
+import com.github.jozott00.wokwiintellij.toml.isWokwiToml
+import com.intellij.codeInspection.InspectionManager
+import com.intellij.codeInspection.LocalInspectionTool
+import com.intellij.codeInspection.ProblemDescriptor
+import com.intellij.codeInspection.ProblemsHolder
+import com.intellij.psi.PsiElementVisitor
+import com.intellij.psi.PsiFile
+
+abstract class WokwiConfigInspectionBase: LocalInspectionTool() {
+
+ final override fun checkFile(
+ file: PsiFile,
+ manager: InspectionManager,
+ isOnTheFly: Boolean
+ ): Array? {
+ if (!file.isWokwiToml) return super.checkFile(file, manager, isOnTheFly)
+ return checkFileInternal(file, manager, isOnTheFly)
+ }
+
+ protected open fun checkFileInternal(file: PsiFile, manager: InspectionManager, isOnTheFly: Boolean): Array? = super.checkFile(file, manager, isOnTheFly)
+
+ final override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
+ if (!holder.file.isWokwiToml) return super.buildVisitor(holder, isOnTheFly)
+ return buildVisitorInternal(holder, isOnTheFly)
+ }
+
+ protected open fun buildVisitorInternal(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor = super.buildVisitor(holder, isOnTheFly)
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/ide/inspections/WokwiConfigVisitor.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/ide/inspections/WokwiConfigVisitor.kt
new file mode 100644
index 0000000..e7aac1e
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/ide/inspections/WokwiConfigVisitor.kt
@@ -0,0 +1,56 @@
+package com.github.jozott00.wokwiintellij.ide.inspections
+
+import com.github.jozott00.wokwiintellij.toml.stringValue
+import org.toml.lang.psi.TomlKeyValue
+import org.toml.lang.psi.TomlTable
+import org.toml.lang.psi.TomlVisitor
+
+@Suppress("EmptyMethod", "EmptyMethod")
+open class WokwiConfigVisitor : TomlVisitor() {
+
+ open fun visitWokwiTable(value: TomlTable) {
+ for (e in value.entries) {
+ visitWokwiKeyValue(e)
+ }
+ }
+
+ open fun visitVersionValue(value: TomlKeyValue) {
+
+ }
+
+ open fun visitElfValue(value: TomlKeyValue) {
+
+ }
+
+ open fun visitFirmwareValue(value: TomlKeyValue) {
+
+ }
+
+ open fun visitUnknownTable(ignoredValue: TomlTable) {
+
+ }
+
+ open fun visitUnknownWokwiValue() {
+
+ }
+
+ private fun visitWokwiKeyValue(element: TomlKeyValue) {
+ when (element.key.stringValue) {
+ "version" -> visitVersionValue(element)
+ "elf" -> visitElfValue(element)
+ "firmware" -> visitFirmwareValue(element)
+ else -> visitUnknownWokwiValue()
+ }
+ }
+
+ override fun visitTable(element: TomlTable) {
+ super.visitTable(element)
+
+ if (element.header.key?.stringValue == "wokwi") {
+ return visitWokwiTable(element)
+ }
+ visitUnknownTable(element)
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/jcef/BrowserPipe.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/jcef/BrowserPipe.kt
new file mode 100644
index 0000000..9a52b86
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/jcef/BrowserPipe.kt
@@ -0,0 +1,32 @@
+package com.github.jozott00.wokwiintellij.jcef
+
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.util.Disposer
+
+/**
+ * The `BrowserPipe` interface represents a pipe for communication between a browser and its subscribers.
+ * It provides methods for sending messages, subscribing to receive messages, and removing subscribers.
+ *
+ * This interface extends the `Disposable` interface, which means it can be disposed to release any resources
+ * it may be holding.
+ */
+interface BrowserPipe : Disposable {
+
+ fun send(type: String, data: String)
+
+
+ fun subscribe(type: String, subscriber: Subscriber)
+
+ fun subscribe(type: String, subscriber: Subscriber, parent: Disposable) {
+ Disposer.register(parent) { removeSubscriber(type, subscriber) }
+ subscribe(type, subscriber)
+ }
+
+
+ fun removeSubscriber(type: String, subscriber: Subscriber)
+
+ interface Subscriber {
+ fun messageReceived(data: String): Boolean
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/jcef/JcefUtil.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/jcef/JcefUtil.kt
new file mode 100644
index 0000000..a616502
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/jcef/JcefUtil.kt
@@ -0,0 +1,37 @@
+package com.github.jozott00.wokwiintellij.jcef
+
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.util.Disposer
+import com.intellij.ui.jcef.JBCefBrowser
+import com.intellij.ui.jcef.JBCefClient
+import org.cef.browser.CefBrowser
+import org.cef.handler.CefLoadHandler
+import org.cef.handler.CefRequestHandler
+import org.intellij.lang.annotations.Language
+
+internal fun JBCefBrowser.executeJavaScript(@Language("JavaScript") code: String) {
+ cefBrowser.executeJavaScript(code, null, 0)
+}
+
+@Suppress("unused")
+internal fun JBCefClient.addRequestHandler(
+ handler: CefRequestHandler,
+ browser: CefBrowser,
+ parentDisposable: Disposable
+) {
+ Disposer.register(parentDisposable) { removeRequestHandler(handler, browser) }
+ addRequestHandler(handler, browser)
+}
+
+internal fun JBCefClient.addLoadHandler(
+ handler: CefLoadHandler,
+ browser: CefBrowser,
+ parentDisposable: Disposable
+) {
+ Disposer.register(parentDisposable) { removeLoadHandler(handler, browser) }
+ addLoadHandler(handler, browser)
+}
+
+internal fun JBCefBrowser.addLoadHandler(handler: CefLoadHandler, parentDisposable: Disposable) {
+ jbCefClient.addLoadHandler(handler, cefBrowser, parentDisposable)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/jcef/impl/JcefBrowserPipe.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/jcef/impl/JcefBrowserPipe.kt
new file mode 100644
index 0000000..12b801f
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/jcef/impl/JcefBrowserPipe.kt
@@ -0,0 +1,128 @@
+package com.github.jozott00.wokwiintellij.jcef.impl
+
+import com.github.jozott00.wokwiintellij.jcef.BrowserPipe
+import com.github.jozott00.wokwiintellij.jcef.addLoadHandler
+import com.github.jozott00.wokwiintellij.jcef.executeJavaScript
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.util.Disposer
+import com.intellij.ui.jcef.JBCefBrowser
+import com.intellij.ui.jcef.JBCefBrowserBase
+import com.intellij.ui.jcef.JBCefJSQuery
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Serializer
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonElement
+import org.cef.browser.CefBrowser
+import org.cef.browser.CefFrame
+import org.cef.handler.CefLoadHandlerAdapter
+import org.intellij.lang.annotations.Language
+
+class JcefBrowserPipe(private val browser: JBCefBrowser, parentDisposable: Disposable) : BrowserPipe, CefLoadHandlerAdapter() {
+
+ // subscribers to specific types
+ private val subscribers = hashMapOf>()
+
+ private val injectQuery = JBCefJSQuery.create(browser as JBCefBrowserBase)
+
+ init {
+ Disposer.register(parentDisposable, this)
+ Disposer.register(this, injectQuery)
+ injectQuery.addHandler(::onReceive)
+ browser.addLoadHandler(this, this)
+ }
+
+ override fun send(type: String, data: String) {
+ val funCall = """
+ window.$namspaceInBrowser.$receiveMessageFromIntelliFunc("$type", $data);
+ """.trimIndent()
+
+ browser.executeJavaScript(funCall)
+ }
+
+ override fun subscribe(type: String, subscriber: BrowserPipe.Subscriber) {
+ subscribers.merge(type, mutableListOf(subscriber)) { current, _ ->
+ current.also { it.add(subscriber) }
+ }
+ }
+
+ override fun removeSubscriber(type: String, subscriber: BrowserPipe.Subscriber) {
+ subscribers[type]?.remove(subscriber)
+ if (subscribers[type]?.isEmpty() == true) {
+ subscribers.remove(type)
+ }
+ }
+
+ override fun dispose() {
+ subscribers.clear()
+
+ }
+
+ // inject code to browser
+ override fun onLoadEnd(browser: CefBrowser?, frame: CefFrame?, httpStatusCode: Int) {
+ @Language("JavaScript")
+ val code = """
+ window.$namspaceInBrowser.$postMessageToIntellijFunc = data => ${injectQuery.inject("data")};
+ """.trimIndent()
+
+ browser?.executeJavaScript(code, null, 0)
+ browser?.executeJavaScript("window.dispatchEvent(new Event('IdeReady'));", null, 0)
+ }
+
+ @Suppress("SameReturnValue")
+ private fun onReceive(msg: String): JBCefJSQuery.Response? {
+ val (type, data) = msg.let(::parseObj) ?: return null
+ informSubscribers(type, data)
+ return null
+ }
+
+ private fun informSubscribers(type: String, data: String) {
+ when (val subs = subscribers[type]) {
+ null -> logger.warn("No subscribers for $type!\nAttached data: $data")
+ else -> subs.takeWhile { it.messageReceived(data) }
+ }
+ }
+
+ private fun parseObj(json: String): MessageObj? {
+ try {
+ return Json.decodeFromString(json)
+ } catch (e: Exception) {
+ logger.error(e)
+ return null
+ }
+ }
+
+ companion object {
+ val logger = logger()
+
+ const val namspaceInBrowser = "__WokwiIntellij"
+ const val postMessageToIntellijFunc = "__postMessageToPipe"
+ const val receiveMessageFromIntelliFunc = "__receiveMessageFromPipe"
+
+
+ }
+
+
+ @Serializable
+ private data class MessageObj(
+ val type: String,
+ @Serializable(with = RawJsonSerializer::class) val data: String
+ )
+
+ @OptIn(ExperimentalSerializationApi::class)
+ @Serializer(forClass = String::class)
+ private object RawJsonSerializer : KSerializer {
+ override fun serialize(encoder: Encoder, value: String) {
+ encoder.encodeString(value)
+ }
+
+ override fun deserialize(decoder: Decoder): String {
+ return decoder.decodeSerializableValue(JsonElement.serializer()).toString()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiActivationListener.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiActivationListener.kt
deleted file mode 100644
index 77ff3b5..0000000
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiActivationListener.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.github.jozott00.wokwiintellij.listeners
-
-import com.intellij.openapi.application.ApplicationActivationListener
-import com.intellij.openapi.diagnostic.thisLogger
-import com.intellij.openapi.wm.IdeFrame
-
-internal class WokwiActivationListener : ApplicationActivationListener {
-
- override fun applicationActivated(ideFrame: IdeFrame) {
- thisLogger().warn("Don't forget to remove all non-needed sample code files with their corresponding registration entries in `plugin.xml`.")
- }
-}
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiElfFileListener.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiFirmwareWatcher.kt
similarity index 51%
rename from src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiElfFileListener.kt
rename to src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiFirmwareWatcher.kt
index 9fdbd0b..f1a403a 100644
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiElfFileListener.kt
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiFirmwareWatcher.kt
@@ -1,38 +1,42 @@
package com.github.jozott00.wokwiintellij.listeners
import com.github.jozott00.wokwiintellij.services.WokwiProjectService
-import com.github.jozott00.wokwiintellij.states.WokwiConfigState
-import com.intellij.openapi.components.Service
+import com.github.jozott00.wokwiintellij.states.WokwiSettingsState
import com.intellij.openapi.components.service
+import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
-import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.BulkFileListener
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
-class WokwiElfFileListener(val project: Project) : BulkFileListener {
-
+class WokwiFirmwareWatcher(val project: Project) : BulkFileListener {
override fun after(events: MutableList) {
- val configState = project.service()
+ val configState = project.service()
+ val projectService = project.service()
- if (!configState.watchElf) return
+ if (!configState.watchFirmware) return
+ val watchPaths = projectService.getWatchPaths() ?: return
- val watchPath = configState.elfPath
val result = events.find {
if (it.file?.isInLocalFileSystem != true)
return@find false
- if (it.file?.path == watchPath)
+ if (watchPaths.contains(it.file?.path))
return@find true
false
}
- val projectService = project.service()
if (result != null) {
- projectService.elfFileUpdate()
+ LOG.info("Triggered with: ${events.map { it.path }}")
+ LOG.info("Watch against: $watchPaths")
+ projectService.firmwareUpdated()
}
}
+ companion object {
+ private val LOG = logger()
+ }
+
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiPostStartupActivity.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiPostStartupActivity.kt
deleted file mode 100644
index 4f0cc6c..0000000
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiPostStartupActivity.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.github.jozott00.wokwiintellij.listeners
-
-import com.github.jozott00.wokwiintellij.services.WokwiProjectService
-import com.intellij.openapi.components.service
-import com.intellij.openapi.project.Project
-import com.intellij.openapi.startup.ProjectActivity
-import com.intellij.openapi.startup.StartupActivity
-
-class WokwiPostStartupActivity : ProjectActivity {
- override suspend fun execute(project: Project) {
- val projectService = project.service()
- projectService.startup()
- }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiProjectManagerListener.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiProjectManagerListener.kt
deleted file mode 100644
index e49020a..0000000
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiProjectManagerListener.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.github.jozott00.wokwiintellij.listeners
-
-import com.intellij.openapi.components.Service
-import com.intellij.openapi.diagnostic.thisLogger
-import com.intellij.openapi.project.Project
-import com.github.jozott00.wokwiintellij.MyBundle
-import com.intellij.openapi.project.ProjectManagerListener
-import com.intellij.openapi.project.impl.ProjectLifecycleListener
-
-
-
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/WokwiConfigurationFactory.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/WokwiConfigurationFactory.kt
new file mode 100644
index 0000000..2462cce
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/WokwiConfigurationFactory.kt
@@ -0,0 +1,27 @@
+package com.github.jozott00.wokwiintellij.runner
+
+import com.github.jozott00.wokwiintellij.runner.configs.*
+import com.intellij.execution.configurations.ConfigurationFactory
+import com.intellij.execution.configurations.ConfigurationType
+import com.intellij.openapi.project.Project
+
+class WokwiConfigurationFactory(type: ConfigurationType) : ConfigurationFactory(type) {
+ override fun getId(): String {
+ return type.id
+ }
+
+ override fun createTemplateConfiguration(
+ project: Project
+ ) = when (type) {
+ is WokwiStartDebugConfigType -> WokwiStartDebugConfig(project, this, type.displayName)
+ is WokwiRunConfigType -> WokwiRunConfig(project, this, type.displayName)
+ else -> error("Invalid configuration type")
+ }
+
+
+ override fun getOptionsClass() = when (type) {
+ is WokwiStartDebugConfigType -> WokwiStartDebugConfigOptions::class.java
+ is WokwiRunConfigType -> WokwiRunConfigOptions::class.java
+ else -> error("Invalid configuration type")
+ }
+}
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/WokwiProcessHandler.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/WokwiProcessHandler.kt
new file mode 100644
index 0000000..1f3311a
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/WokwiProcessHandler.kt
@@ -0,0 +1,7 @@
+package com.github.jozott00.wokwiintellij.runner
+
+import com.github.jozott00.wokwiintellij.simulator.WokwiSimulatorListener
+import com.intellij.execution.process.ProcessHandler
+
+abstract class WokwiProcessHandler : ProcessHandler(), WokwiSimulatorListener
+
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/configs/wokwiRunConfig.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/configs/wokwiRunConfig.kt
new file mode 100644
index 0000000..21c0764
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/configs/wokwiRunConfig.kt
@@ -0,0 +1,65 @@
+package com.github.jozott00.wokwiintellij.runner.configs
+
+import com.github.jozott00.wokwiintellij.runner.WokwiConfigurationFactory
+import com.github.jozott00.wokwiintellij.runner.profileStates.WokwiSimulatorRunnerState
+import com.github.jozott00.wokwiintellij.ui.WokwiIcons
+import com.intellij.execution.Executor
+import com.intellij.execution.configurations.ConfigurationFactory
+import com.intellij.execution.configurations.ConfigurationTypeBase
+import com.intellij.execution.configurations.RunConfigurationBase
+import com.intellij.execution.configurations.RunConfigurationOptions
+import com.intellij.execution.runners.ExecutionEnvironment
+import com.intellij.openapi.options.SettingsEditor
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.NotNullLazyValue
+import javax.swing.JComponent
+import javax.swing.JPanel
+
+class WokwiRunConfig(
+ project: Project,
+ factory: ConfigurationFactory, name: String
+) : RunConfigurationBase(project, factory, name) {
+
+ override fun getOptions(): WokwiRunConfigOptions {
+ return super.getOptions() as WokwiRunConfigOptions
+ }
+
+ override fun getState(executor: Executor, environment: ExecutionEnvironment) =
+ WokwiSimulatorRunnerState(environment)
+
+ override fun getConfigurationEditor() = WokwiRunEditor()
+
+}
+
+class WokwiRunConfigType : ConfigurationTypeBase(
+ ID, "Wokwi Run", "Run the Wokwi simulator and let it wait for a GDB debugger.",
+ NotNullLazyValue.createValue { WokwiIcons.Debug }
+) {
+ init {
+ addFactory(WokwiConfigurationFactory(this))
+ }
+
+ companion object {
+ const val ID: String = "WowkiRunConfig"
+ }
+}
+
+
+class WokwiRunConfigOptions : RunConfigurationOptions()
+
+
+class WokwiRunEditor : SettingsEditor() {
+ override fun resetEditorFrom(s: WokwiRunConfig) {
+ // currently no settings
+ }
+
+ override fun applyEditorTo(s: WokwiRunConfig) {
+ // currently no settings
+ }
+
+ override fun createEditor(): JComponent {
+ // currently no settings
+ return JPanel()
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/configs/wokwiStartDebugConfig.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/configs/wokwiStartDebugConfig.kt
new file mode 100644
index 0000000..17b4da0
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/configs/wokwiStartDebugConfig.kt
@@ -0,0 +1,67 @@
+package com.github.jozott00.wokwiintellij.runner.configs
+
+import com.github.jozott00.wokwiintellij.runner.WokwiConfigurationFactory
+import com.github.jozott00.wokwiintellij.runner.profileStates.WokwiSimulatorStartState
+import com.github.jozott00.wokwiintellij.ui.WokwiIcons
+import com.intellij.execution.Executor
+import com.intellij.execution.configurations.ConfigurationFactory
+import com.intellij.execution.configurations.ConfigurationTypeBase
+import com.intellij.execution.configurations.RunConfigurationBase
+import com.intellij.execution.configurations.RunConfigurationOptions
+import com.intellij.execution.runners.ExecutionEnvironment
+import com.intellij.openapi.options.SettingsEditor
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.NotNullLazyValue
+import javax.swing.JComponent
+import javax.swing.JPanel
+
+class WokwiStartDebugConfig(
+ project: Project,
+ factory: ConfigurationFactory, name: String
+) : RunConfigurationBase(project, factory, name) {
+
+ override fun getOptions(): WokwiStartDebugConfigOptions {
+ return super.getOptions() as WokwiStartDebugConfigOptions
+ }
+
+ override fun getState(executor: Executor, environment: ExecutionEnvironment) =
+ WokwiSimulatorStartState(project, true)
+// override fun getState(executor: Executor, environment: ExecutionEnvironment) =
+// WokwiSimulatorRunnerState(environment)
+
+ override fun getConfigurationEditor() = WokwiStartDebugEditor()
+
+}
+
+class WokwiStartDebugConfigType : ConfigurationTypeBase(
+ ID, "Wokwi Start Debug", "Start the Wokwi simulator and let it wait for a GDB debugger.",
+ NotNullLazyValue.createValue { WokwiIcons.Debug }
+) {
+ init {
+ addFactory(WokwiConfigurationFactory(this))
+ }
+
+ companion object {
+ const val ID: String = "WowkiStartDebugConfig"
+ }
+}
+
+
+class WokwiStartDebugConfigOptions : RunConfigurationOptions()
+
+
+class WokwiStartDebugEditor : SettingsEditor() {
+ override fun resetEditorFrom(s: WokwiStartDebugConfig) {
+ // currently no settings
+ }
+
+ override fun applyEditorTo(s: WokwiStartDebugConfig) {
+ // currently no settings
+ }
+
+ override fun createEditor(): JComponent {
+ // currently no settings
+ return JPanel()
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/macros/ElfPathMacro.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/macros/ElfPathMacro.kt
new file mode 100644
index 0000000..0dfadd6
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/macros/ElfPathMacro.kt
@@ -0,0 +1,20 @@
+package com.github.jozott00.wokwiintellij.runner.macros
+
+import com.github.jozott00.wokwiintellij.toml.WokwiConfigProcessor
+import com.intellij.ide.macro.Macro
+import com.intellij.openapi.actionSystem.CommonDataKeys
+import com.intellij.openapi.actionSystem.DataContext
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+
+class ElfPathMacro : Macro() {
+ override fun getName() = "WokwiElfPath"
+
+ override fun getDescription() = "Resolves to the ELF file path specified in the Wokwi configuration."
+
+ override fun expand(dataContext: DataContext): String? {
+ val project = CommonDataKeys.PROJECT.getData(dataContext) ?: return null
+ val config = runBlocking(Dispatchers.IO) { WokwiConfigProcessor.findElfFile(project) } ?: return null
+ return config.path
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/macros/GdbServerMacro.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/macros/GdbServerMacro.kt
new file mode 100644
index 0000000..e5ef516
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/macros/GdbServerMacro.kt
@@ -0,0 +1,22 @@
+package com.github.jozott00.wokwiintellij.runner.macros
+
+import com.github.jozott00.wokwiintellij.toml.WokwiConfigProcessor
+import com.intellij.ide.macro.Macro
+import com.intellij.openapi.actionSystem.CommonDataKeys
+import com.intellij.openapi.actionSystem.DataContext
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+
+class GdbServerMacro : Macro() {
+ override fun getName() = "WokwiGdbServer"
+
+ override fun getDescription() = "Resolves to the Wokwi's GDB Server address"
+
+ override fun expand(dataContext: DataContext): String? {
+ val project = CommonDataKeys.PROJECT.getData(dataContext) ?: return null
+ val config = runBlocking(Dispatchers.IO) { WokwiConfigProcessor.readConfig(project) } ?: return null
+
+ val port = config.gdbServerPort ?: return null
+ return "localhost:$port"
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/profileStates/WokwiSimulatorRunnerState.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/profileStates/WokwiSimulatorRunnerState.kt
new file mode 100644
index 0000000..3c6cc91
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/profileStates/WokwiSimulatorRunnerState.kt
@@ -0,0 +1,73 @@
+package com.github.jozott00.wokwiintellij.runner.profileStates
+
+import com.github.jozott00.wokwiintellij.runner.WokwiProcessHandler
+import com.github.jozott00.wokwiintellij.services.WokwiProjectService
+import com.intellij.execution.configurations.CommandLineState
+import com.intellij.execution.runners.ExecutionEnvironment
+import com.intellij.openapi.components.service
+import com.intellij.openapi.diagnostic.thisLogger
+import com.intellij.openapi.progress.ProgressIndicator
+import com.intellij.openapi.progress.ProgressManager
+import com.intellij.openapi.progress.Task
+import com.intellij.openapi.progress.util.ProgressIndicatorBase
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.Key
+import java.io.OutputStream
+
+//@Deprecated("The WokwiRunner is currently not used, and might be removed in the future.")
+class WokwiSimulatorRunnerState(private val myEnvironment: ExecutionEnvironment) : CommandLineState(myEnvironment) {
+ override fun startProcess() = WokwiRunnerProcessHandler(myEnvironment.project)
+}
+
+//@Deprecated("The WokwiRunner is currently not used, and might be removed in the future.")
+class WokwiRunnerProcessHandler(val project: Project) : WokwiProcessHandler() {
+
+ val wokwiService = project.service()
+
+ override fun startNotify() {
+ super.startNotify()
+
+ ProgressManager.getInstance().runProcessWithProgressAsynchronously(
+ object : Task.Backgroundable(project, "Wokwi execution", false) {
+ override fun run(indicator: ProgressIndicator) {
+ wokwiService.startSimulator(this@WokwiRunnerProcessHandler, false)
+ }
+ },
+ ProgressIndicatorBase()
+ )
+
+ }
+
+ override fun onShutdown() {
+ destroyProcess()
+ }
+
+ override fun onTextAvailable(text: String, outputType: Key<*>) {
+ notifyTextAvailable(text, outputType)
+ }
+
+ override fun destroyProcessImpl() {
+ thisLogger().info("Destroy Process")
+ wokwiService.stopSimulator()
+ notifyProcessTerminated(0)
+ }
+
+ override fun detachProcessImpl() {
+ thisLogger().info("Detach Process")
+ notifyProcessDetached()
+ }
+
+ override fun detachIsDefault() = false
+
+ override fun getProcessInput(): OutputStream {
+ thisLogger().info("Ouput stream")
+ val stream = object : OutputStream() {
+ override fun write(b: Int) {
+ thisLogger().info("Got new input $b")
+ }
+ }
+ return stream
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/profileStates/WokwiSimulatorStartState.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/profileStates/WokwiSimulatorStartState.kt
new file mode 100644
index 0000000..cd4c228
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/profileStates/WokwiSimulatorStartState.kt
@@ -0,0 +1,52 @@
+package com.github.jozott00.wokwiintellij.runner.profileStates
+
+import com.github.jozott00.wokwiintellij.runner.WokwiProcessHandler
+import com.github.jozott00.wokwiintellij.services.WokwiProjectService
+import com.github.jozott00.wokwiintellij.simulator.args.WokwiArgs
+import com.intellij.execution.ExecutionResult
+import com.intellij.execution.Executor
+import com.intellij.execution.configurations.RunProfileState
+import com.intellij.execution.runners.ProgramRunner
+import com.intellij.openapi.actionSystem.AnAction
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+
+class WokwiSimulatorStartState(private val project: Project, private val waitForDebugger: Boolean) : RunProfileState {
+ override fun execute(executor: Executor?, runner: ProgramRunner<*>) = object : ExecutionResult {
+ override fun getExecutionConsole() = null
+
+ override fun getActions(): Array {
+ return emptyArray()
+ }
+
+ override fun getProcessHandler() = WokwiStartProcessHandler(project, waitForDebugger)
+// override fun getProcessHandler() = WokwiRunnerProcessHandler(project)
+ }
+}
+
+
+class WokwiStartProcessHandler(project: Project, waitForDebugger: Boolean) :
+ WokwiProcessHandler() {
+
+ private val wokwiService = project.service()
+
+ init {
+ wokwiService.startSimulator(this, waitForDebugger)
+ }
+
+ override fun destroyProcessImpl() {
+ notifyProcessTerminated(0)
+ }
+
+ override fun detachProcessImpl() {
+ notifyProcessDetached()
+ }
+
+ override fun detachIsDefault() = false
+
+ override fun getProcessInput() = null
+
+ override fun onStarted(runArgs: WokwiArgs) {
+ this.destroyProcess()
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/runBefore/WokwiStartDebugBeforeRunTask.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/runBefore/WokwiStartDebugBeforeRunTask.kt
new file mode 100644
index 0000000..933978d
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/runner/runBefore/WokwiStartDebugBeforeRunTask.kt
@@ -0,0 +1,65 @@
+package com.github.jozott00.wokwiintellij.runner.runBefore
+
+import com.github.jozott00.wokwiintellij.services.WokwiProjectService
+import com.github.jozott00.wokwiintellij.simulator.WokwiSimulatorListener
+import com.github.jozott00.wokwiintellij.ui.WokwiIcons
+import com.intellij.execution.BeforeRunTask
+import com.intellij.execution.BeforeRunTaskProvider
+import com.intellij.execution.configurations.RunConfiguration
+import com.intellij.execution.runners.ExecutionEnvironment
+import com.intellij.openapi.actionSystem.DataContext
+import com.intellij.openapi.components.service
+import com.intellij.openapi.util.Key
+import kotlinx.coroutines.*
+import javax.swing.Icon
+
+class WokwiStartDebugBeforeRunTaskProvider : BeforeRunTaskProvider() {
+
+ override fun getId(): Key = ID
+
+ override fun getName() = "Start Wokwi Debug"
+
+ override fun getIcon(): Icon = WokwiIcons.Debug
+
+ override fun createTask(runConfiguration: RunConfiguration): WokwiStartDebugBeforeRunTask =
+ WokwiStartDebugBeforeRunTask()
+
+ override fun executeTask(
+ context: DataContext,
+ configuration: RunConfiguration,
+ environment: ExecutionEnvironment,
+ task: WokwiStartDebugBeforeRunTask
+ ): Boolean {
+ val projectService = environment.project.service()
+ return runBlocking(Dispatchers.IO) {
+ // start child scope to make cancellation on dispose possible.
+ val job = projectService.childScope("WokwiStartBeforeRunTask").async {
+ val result = projectService.startSimulatorSuspended(task, true)
+ task.waitForSimulatorToBeRunning()
+ result
+ }
+ try {
+ job.await()
+ } catch (e: CancellationException) {
+ false
+ }
+ }
+ }
+
+}
+
+class WokwiStartDebugBeforeRunTask :
+ BeforeRunTask(ID), WokwiSimulatorListener {
+
+ private val simulatorRunning = CompletableDeferred()
+
+ suspend fun waitForSimulatorToBeRunning() {
+ simulatorRunning.await()
+ }
+
+ override fun onRunning() {
+ simulatorRunning.complete(Unit)
+ }
+}
+
+val ID: Key = Key.create("WokwiStartDebug.Before.Run")
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiArgsLoader.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiArgsLoader.kt
new file mode 100644
index 0000000..220f8ba
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiArgsLoader.kt
@@ -0,0 +1,98 @@
+package com.github.jozott00.wokwiintellij.services
+
+import arrow.core.Either
+import com.github.jozott00.wokwiintellij.simulator.WokwiConfig
+import com.github.jozott00.wokwiintellij.simulator.args.WokwiArgs
+import com.github.jozott00.wokwiintellij.simulator.args.WokwiArgsFirmware
+import com.github.jozott00.wokwiintellij.simulator.args.WokwiProjectType
+import com.github.jozott00.wokwiintellij.utils.WokwiNotifier.notifyBalloonAsync
+import com.github.jozott00.wokwiintellij.utils.simulation.FirmwareUtils
+import com.intellij.notification.NotificationType
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.EDT
+import com.intellij.openapi.application.readAction
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.openapi.vfs.readBytes
+import com.intellij.openapi.vfs.readText
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+@Service(Service.Level.PROJECT)
+class WokwiArgsLoader(val project: Project) {
+
+ private var licensingService = ApplicationManager.getApplication().service()
+
+ suspend fun load(config: WokwiConfig): WokwiArgs? {
+ val license = loadLicense() ?: return null
+ val diagram = readAction { config.diagram.readText() }
+ val firmware = loadFirmware(config.firmware) ?: return null
+
+ val projectType = detectProject()
+ // TODO: Check for esp image
+
+ val args = WokwiArgs(license, diagram, firmware)
+ return args
+
+ }
+
+ suspend fun loadFirmware(firmwareFile: VirtualFile): WokwiArgsFirmware? = withContext(Dispatchers.IO) {
+ if (!readAction { firmwareFile.exists() }) {
+ withContext(Dispatchers.EDT) {
+ notifyBalloonAsync(
+ title = "Failed to load firmware",
+ message = "Firmware `${firmwareFile.path}` does not exist and therefore cannot be loaded for simulation.",
+ NotificationType.ERROR
+ )
+ }
+ return@withContext null
+ }
+
+ val isFlasherArgsFile = firmwareFile.name == "flasher_args.json"
+ val binaryPaths = mutableListOf(firmwareFile.path)
+
+ val buffer = if (isFlasherArgsFile) {
+ val packedResult=
+ when (val result = FirmwareUtils.packEspIdfFirmware(firmwareFile)) {
+ is Either.Left -> {
+ notifyBalloonAsync(result.value)
+ return@withContext null
+ }
+ is Either.Right -> result.value
+ }
+
+ binaryPaths.addAll(packedResult.binaryPaths)
+ packedResult.img
+ } else {
+ readAction { firmwareFile.readBytes() }
+ }
+
+ val format = FirmwareUtils.determineFirmwareFormat(firmwareFile, buffer)
+
+ WokwiArgsFirmware(
+ buffer = buffer,
+ format = format,
+ rootFile = firmwareFile,
+ isFlasherFile = isFlasherArgsFile,
+ size = buffer.size.toUInt(),
+ binaryPaths = binaryPaths
+ )
+ }
+
+ @Suppress("SameReturnValue")
+ private fun detectProject(): WokwiProjectType {
+ return WokwiProjectType.RUST
+ }
+
+ private suspend fun loadLicense() = licensingService.loadAndCheckLicense()
+ .onLeft {
+ notifyBalloonAsync(
+ title = it.title,
+ message = it.message,
+ type = NotificationType.ERROR
+ )
+ }.getOrNull()
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiComponentService.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiComponentService.kt
index da8fc50..65566af 100644
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiComponentService.kt
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiComponentService.kt
@@ -1,27 +1,27 @@
package com.github.jozott00.wokwiintellij.services
-import com.github.jozott00.wokwiintellij.states.WokwiConfigState
-import com.github.jozott00.wokwiintellij.toolWindow.SimulatorPanel
-import com.github.jozott00.wokwiintellij.toolWindow.WokwiToolWindow
-import com.github.jozott00.wokwiintellij.toolWindow.wokwiConfigPanel
+import com.github.jozott00.wokwiintellij.states.WokwiSettingsState
+import com.github.jozott00.wokwiintellij.toolWindow.WokwiConsoleToolWindow
+import com.github.jozott00.wokwiintellij.toolWindow.WokwiSimulationToolWindow
+import com.github.jozott00.wokwiintellij.ui.config.wokwiConfigPanel
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
-import javax.swing.JPanel
@Service(Service.Level.PROJECT)
class WokwiComponentService(val project: Project) {
- val wokwiConfigState = project.service()
+ private val wokwiConfigState = project.service()
- val simulatorPanel = SimulatorPanel()
- val configPanel = wokwiConfigPanel(wokwiConfigState.state) {
+ private val configPanel = wokwiConfigPanel(project, wokwiConfigState.state) {
onChangeAction = {
- println("Changes in model: ${wokwiConfigState.state}")
+ // do nothing
}
}
- val toolWindow = WokwiToolWindow(configPanel, simulatorPanel)
+ val simulatorToolWindowComponent = WokwiSimulationToolWindow(configPanel)
+ val consoleToolWindowComponent = WokwiConsoleToolWindow(project)
+
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiDataService.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiDataService.kt
deleted file mode 100644
index 419b5bc..0000000
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiDataService.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-package com.github.jozott00.wokwiintellij.services
-
-import com.github.jozott00.wokwiintellij.states.WokwiConfigState
-import com.github.jozott00.wokwiintellij.utils.WokwiNotifier
-import com.intellij.notification.NotificationType
-import com.intellij.openapi.components.Service
-import com.intellij.openapi.components.service
-import com.intellij.openapi.diagnostic.thisLogger
-import com.intellij.openapi.project.Project
-import com.intellij.openapi.vfs.VfsUtil
-import espimg.EspImg
-import espimg.ImageResult
-import espimg.exceptions.EspImgException
-import java.io.File
-import java.io.IOException
-import java.io.InputStream
-import java.nio.file.Files
-import java.nio.file.Paths
-import java.nio.file.attribute.BasicFileAttributes
-
-@Service(Service.Level.PROJECT)
-class WokwiDataService(val project: Project) {
-
- private val configState = project.service()
-
- private var lastFile: String? = null
- private var lastModifiedTime: Long? = null
- private var image: ImageResult? = null
-
- fun retrieveImage(): ImageResult? {
- if (checkForReload())
- loadImage()
-
- return image
- }
-
- private fun checkForReload(): Boolean {
- if (configState.elfPath != lastFile) {
- return true
- }
-
- val path = configState.elfPath
- try {
- val currentTimeStamp = this.readFileModification(path)
-
- if (configState.elfPath != lastFile || currentTimeStamp != lastModifiedTime) {
- println("RELOAD REQUIRED: ${configState.elfPath} vs $lastFile ... $currentTimeStamp vs $lastModifiedTime")
- return true
- }
- } catch (e: IOException) {
- println("RELOAD REQUIRED: EXCETPION $e")
- return true
- }
-
-
- return false
- }
-
- private fun loadImage(): Boolean {
- val path = configState.elfPath
- println("LOADING IMAGE $path")
- val file = File(path)
-
- if (!file.exists()) {
- thisLogger().warn("File $file does not exist!")
- }
-
- val vfile = VfsUtil.findFileByIoFile(file, true)
-
- if (vfile == null) {
- WokwiNotifier.notifyBalloon("ELF file `$path` not found", project, NotificationType.ERROR)
- return false;
- }
-
- val inputStream: InputStream = vfile.inputStream
-
- try {
- this.image = EspImg.getFlashImage(inputStream.readAllBytes(), null, null)
- this.lastModifiedTime = this.readFileModification(path)
- } catch (e: EspImgException) {
- WokwiNotifier.notifyBalloon("${e.message}", project, NotificationType.ERROR)
- return false;
- }
-
- lastFile = file.path
- return true
- }
-
- private fun readFileModification(path: String): Long {
- val fileAttributes = Files.readAttributes(Paths.get(path), BasicFileAttributes::class.java)
- return fileAttributes.lastModifiedTime().toMillis()
- }
-
-
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiLicensingService.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiLicensingService.kt
new file mode 100644
index 0000000..3f7a656
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiLicensingService.kt
@@ -0,0 +1,147 @@
+package com.github.jozott00.wokwiintellij.services
+
+import ai.grazie.utils.mpp.Base64
+import arrow.core.Either
+import arrow.core.left
+import arrow.core.right
+import com.github.jozott00.wokwiintellij.WokwiConstants
+import com.github.jozott00.wokwiintellij.exceptions.GenericError
+import com.github.jozott00.wokwiintellij.utils.WokwiNotifier
+import com.intellij.credentialStore.CredentialAttributes
+import com.intellij.ide.passwordSafe.PasswordSafe
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.diagnostic.logger
+import io.ktor.http.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.nio.charset.StandardCharsets
+import java.util.*
+
+
+@Service(Service.Level.APP)
+class WokwiLicensingService(private val cs: CoroutineScope) {
+
+ private val licenseAttributes =
+ CredentialAttributes(WokwiConstants.WOWKI_PLUGIN_SERVICE_NAME, WokwiConstants.WOKWI_LICENCE_STORE_KEY)
+
+ private var licenseCache: String? = null
+
+ suspend fun getLicense() = licenseCache ?: withContext(Dispatchers.IO) {
+ PasswordSafe.instance.let {
+ licenseCache = it.getPassword(licenseAttributes)
+ licenseCache
+ }
+ }
+
+ fun updateLicense(license: String) = cs.launch(Dispatchers.IO) {
+ LOG.info("Update Wokwi license")
+ licenseCache = license
+ PasswordSafe.instance.setPassword(licenseAttributes, license)
+ WokwiNotifier.notifyBalloonAsync("New Wokwi license activated", "You are ready to go!")
+ }
+
+ @Suppress("unused")
+ fun removeLicense() = cs.launch(Dispatchers.IO) {
+ licenseCache = null
+ PasswordSafe.instance.setPassword(licenseAttributes, null)
+ WokwiNotifier.notifyBalloonAsync("Wokwi license removed", "Your license has been removed.")
+ }
+
+ suspend fun loadAndCheckLicense(): Either {
+ val license = getLicense() ?:
+ return GenericError(
+ "No Wokwi license found",
+ "Set your Wokwi license in the Wokwi window.",
+ ).left()
+
+ val licenseObj = parseLicense(license) ?:
+ return GenericError(
+ "Invalid Wokwi license",
+ "The Wokwi license could not be parsed.",
+ ).left()
+
+ if (licenseObj.expiration < Date())
+ return GenericError(
+ "Expired Wokwi license",
+ "The Wokwi license is expired, please refresh it.",
+ ).left()
+
+ return license.right()
+ }
+
+
+ fun parseLicense(license: String): WokwiLicense? {
+ lateinit var decoded: ByteArray
+ try {
+ // Decoding the base64 input
+ val decodedString = base64DblClickDecode(license)
+ decoded = Base64.decode(decodedString)
+ } catch (e: Exception) {
+ return null
+ }
+
+ // Finding the first null byte
+ val zeroIndex = decoded.indexOf(0)
+ if (zeroIndex < 0) {
+ return null
+ }
+
+ // Parsing the URL parameters
+ val licenseText = String(decoded.sliceArray(0 until zeroIndex), StandardCharsets.UTF_8)
+ val params = licenseText.parseUrlEncodedParameters()
+
+ val userId = params["u"]
+ val name = params["n"]
+ val email = params["e"]
+ val expirationStr = params["x"]
+ val plan = params["p"]
+
+ if (userId == null || name == null || email == null || expirationStr == null) {
+ return null
+ }
+
+ if (!Regex("^[0-9]{8}$").matches(expirationStr)) {
+ return null
+ }
+
+ val year = expirationStr.substring(0, 4).toInt()
+ val month = expirationStr.substring(4, 6).toInt() - 1
+ val day = expirationStr.substring(6, 8).toInt()
+
+ val expiration = Calendar.getInstance().run {
+ set(year, month, day)
+ time
+ }
+
+ return WokwiLicense(userId, name, email, expiration, plan)
+ }
+
+ private fun base64DblClickDecode(value: String): String {
+ var result = value.replace("_P", "+")
+ .replace("_S", "/")
+ .replace("=", "")
+ while (result.length % 4 > 0) {
+ result += "="
+ }
+ return result
+ }
+
+ companion object {
+ val LOG = logger()
+ }
+
+
+ data class WokwiLicense(
+ val userId: String,
+ val name: String,
+ val email: String,
+ val expiration: Date,
+ val plan: String?
+ ) {
+ fun isValid(): Boolean {
+ return expiration.after(Date())
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiPluginDisposable.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiPluginDisposable.kt
new file mode 100644
index 0000000..b46d181
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiPluginDisposable.kt
@@ -0,0 +1,18 @@
+package com.github.jozott00.wokwiintellij.services
+
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+
+/**
+ * The service is intended to be used instead of a project as a parent disposable.
+ */
+@Service(Service.Level.PROJECT)
+class WokwiPluginDisposable: Disposable {
+ companion object {
+ fun getInstance(project: Project) = project.service()
+ }
+
+ override fun dispose() { }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiProjectService.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiProjectService.kt
index 859113a..ff51a54 100644
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiProjectService.kt
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiProjectService.kt
@@ -1,78 +1,215 @@
package com.github.jozott00.wokwiintellij.services
-import com.github.jozott00.wokwiintellij.listeners.WokwiElfFileListener
-import com.github.jozott00.wokwiintellij.states.WokwiConfigState
+import com.github.jozott00.wokwiintellij.extensions.disposeByDisposer
+import com.github.jozott00.wokwiintellij.simulator.WokwiSimulator
+import com.github.jozott00.wokwiintellij.simulator.WokwiSimulatorListener
+import com.github.jozott00.wokwiintellij.simulator.gdb.WokwiGDBServer
+import com.github.jozott00.wokwiintellij.states.WokwiSettingsState
+import com.github.jozott00.wokwiintellij.toml.WokwiConfigProcessor
+import com.github.jozott00.wokwiintellij.toolWindow.ConsoleWindowFactory
+import com.github.jozott00.wokwiintellij.ui.WokwiIcons
+import com.github.jozott00.wokwiintellij.ui.console.SimulationConsole
+import com.github.jozott00.wokwiintellij.utils.ToolWindowUtils
import com.github.jozott00.wokwiintellij.utils.WokwiNotifier
-import com.github.jozott00.wokwiintellij.wokwiServer.WokwiServer
+import com.intellij.notification.NotificationType
import com.intellij.openapi.Disposable
+import com.intellij.openapi.application.EDT
+import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
-import com.intellij.openapi.diagnostic.thisLogger
+import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
-import com.intellij.openapi.vfs.VirtualFileManager
-import com.intellij.util.messages.MessageBusConnection
+import com.intellij.openapi.util.Disposer
+import com.intellij.openapi.wm.ToolWindow
+import com.intellij.openapi.wm.ToolWindowManager
+import com.intellij.ui.jcef.JBCefApp
+import com.intellij.util.namedChildScope
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
@Service(Service.Level.PROJECT)
-class WokwiProjectService(val project: Project) : Disposable {
- private var server: WokwiServer? = null
+class WokwiProjectService(val project: Project, private val cs: CoroutineScope) : Disposable {
+
+ private var simulator: WokwiSimulator? = null
+ private var console: SimulationConsole? = null
private val componentService = project.service()
- private val configState = project.service()
- private val dataService = project.service()
- private val simulationService = project.service()
-
- private var msgBusConnection: MessageBusConnection? = null
- private var simulationRunning = false;
- fun startSimulator() {
- if (dataService.retrieveImage() == null) {
- return
+ private val settingsState = project.service()
+ private val argsLoader = project.service()
+ private var consoleToolWindow: ToolWindow? = null
+ private var gdbServer: WokwiGDBServer? = null
+
+ @Suppress("UnstableApiUsage")
+ fun childScope(name: String) = cs.namedChildScope(name)
+
+ fun startSimulator(withListener: WokwiSimulatorListener? = null, byDebugger: Boolean = false) {
+ cs.launch {
+ startSimulatorSuspended(withListener, byDebugger)
+ }
+ }
+
+ suspend fun startSimulatorSuspended(
+ withListener: WokwiSimulatorListener? = null,
+ byDebugger: Boolean = false
+ ): Boolean {
+ LOG.info("Start simulator...")
+
+ if (simulator == null || byDebugger) {
+ createNewSimulator(byDebugger)
+ } else {
+ updateFirmware()
+ }.also { if (!it) return false }
+
+
+ withListener?.let { simulator?.addSimulatorListener(it) }
+ simulator?.start()
+
+ invokeLater {
+ ToolWindowUtils.setSimulatorIcon(project, true)
+ activateConsoleToolWindow()
+ }
+
+ return true
+ }
+
+ private suspend fun createNewSimulator(waitForDebugger: Boolean = false): Boolean {
+
+ val config =
+ WokwiConfigProcessor.loadConfig(
+ project,
+ settingsState.wokwiConfigPath,
+ settingsState.wokwiDiagramPath
+ )
+ ?: return false
+ val args = argsLoader.load(config) ?: return false
+ args.waitForDebugger = waitForDebugger
+
+ simulator?.disposeByDisposer()
+
+ if (!JBCefApp.isSupported()) {
+ WokwiNotifier.notifyBalloonAsync(
+ "Could not create Wokwi simulator",
+ "JCEF browser is not supported. Please report this issue on the wokwi-intellij Github repository.",
+ NotificationType.ERROR
+ )
+ return false
+ }
+ configGDBServer(
+ waitForDebugger,
+ config.gdbServerPort ?: 3333
+ ) // configures gdbServer for new simulator instance
+
+ simulator = WokwiSimulator(args, this).also {
+ gdbServer?.let { server -> cs.launch { it.connectToGDBServer(server) } } // connect to server
+ }
+
+ withContext(Dispatchers.EDT) {
+ val console = getConsole()
+ simulator?.addSimulatorListener(console)
+
+ simulator?.let { componentService.simulatorToolWindowComponent.showSimulation(it.component) }
+ componentService.consoleToolWindowComponent.setConsole(console)
}
- componentService.toolWindow.showSimulation()
- simulationRunning = true
- watchStart()
+ return true
+ }
+
+ private fun configGDBServer(shouldDebug: Boolean, port: Int) {
+ if (!shouldDebug) {
+ gdbServer?.disposeByDisposer()
+ gdbServer = null
+ } else {
+ gdbServer?.let {
+ if (!it.isRunning()) {
+ it.disposeByDisposer()
+ return@let
+ }
+
+ it.resetEventChannel()
+ return
+ }
+ gdbServer = WokwiGDBServer(this.childScope("WokwiGDBServer"), this).also {
+ it.listen(port)
+ }
+ }
}
- fun stopSimulator() {
- componentService.toolWindow.showConfig()
- simulationRunning = false
- watchStop()
+ private suspend fun updateFirmware(): Boolean {
+ simulator?.let {
+ val firmware = it.getFirmware().rootFile
+ val newFirmware = argsLoader.loadFirmware(firmware) ?: return false
+ it.setFirmware(newFirmware)
+ }
+
+ return true
}
- fun startup() {
- val port = 9012 // Specify your port here
- server = WokwiServer(port, project).apply {
- start()
- println("WokwiServer started on port: $port")
+ fun stopSimulator() = cs.launch {
+ simulator?.disposeByDisposer()
+ simulator = null
+
+ gdbServer?.disposeByDisposer()
+ gdbServer = null
+
+ withContext(Dispatchers.EDT) {
+ ToolWindowUtils.setSimulatorIcon(project, false)
+ componentService.simulatorToolWindowComponent.showConfig()
}
}
+
override fun dispose() {
- server?.stop()
}
- fun restartSimulation() {
- simulationService.restartAll()
+ fun firmwareUpdated() = cs.launch {
+ WokwiNotifier.notifyBalloonAsync(title = "New firmware detected", "Restarting Wokwi simulator...")
+ startSimulatorSuspended()
}
- fun elfFileUpdate() {
- println("FILE UPDATED ... restart")
- WokwiNotifier.notifyBalloon("New build available, restarting simulation...", project)
- simulationService.restartAll()
+ fun getWatchPaths(): List? {
+ return simulator?.getFirmware()?.binaryPaths
}
- fun watchStart() {
- if (!configState.watchElf || !simulationRunning) return
- println("START WATCHING")
- msgBusConnection = project.messageBus.connect()
- msgBusConnection?.subscribe(VirtualFileManager.VFS_CHANGES, WokwiElfFileListener(project))
+ fun isSimulatorRunning(): Boolean {
+ return simulator != null
}
- fun watchStop() {
- println("STOP WATCHING")
- msgBusConnection?.disconnect()
- msgBusConnection = null
+ private suspend fun getConsole(): SimulationConsole {
+ return withContext(Dispatchers.EDT) {
+ val console = this@WokwiProjectService.console ?: run {
+ val c = SimulationConsole(project)
+ Disposer.register(this@WokwiProjectService, c)
+ c
+ }
+ console
+ }
+ }
+
+ private fun activateConsoleToolWindow() = cs.launch(Dispatchers.EDT) {
+ consoleToolWindow?.let {
+ it.show()
+ return@launch
+ }
+
+ val consoleToolWindowId = "Wokwi Run"
+
+ val tm = ToolWindowManager.getInstance(project)
+ if (tm.toolWindowIds.contains(consoleToolWindowId))
+ return@launch
+
+ consoleToolWindow = tm.registerToolWindow(consoleToolWindowId) {
+ val factory = ConsoleWindowFactory()
+ contentFactory = factory
+ icon = WokwiIcons.ConsoleToolWindowIcon
+ canCloseContent = false
+ }
}
+ companion object {
+ private val LOG = logger()
+ }
+
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiSimulationService.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiSimulationService.kt
deleted file mode 100644
index 31517bb..0000000
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiSimulationService.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-package com.github.jozott00.wokwiintellij.services
-
-import com.github.jozott00.wokwiintellij.utils.WokwiNotifier
-import com.github.jozott00.wokwiintellij.wokwiServer.WokwiCommand
-import com.intellij.notification.NotificationType
-import com.intellij.openapi.components.Service
-import com.intellij.openapi.components.service
-import com.intellij.openapi.project.Project
-import com.jetbrains.rd.generator.nova.PredefinedType
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.json.JsonElement
-import org.java_websocket.WebSocket
-
-@Service(Service.Level.PROJECT)
-class WokwiSimulationService(val project: Project) {
- var connection: WebSocket? = null
-
- val dataService = project.service()
-
-
- fun messageReceived(msg: Map, conn: WebSocket) {
-
- }
-
- fun connect(webSocket: WebSocket): Boolean {
- if (this.connection != null) {
- return false
- }
-
- this.connection = webSocket
- sendStart(webSocket)
-
- return true
- }
-
- fun restartAll() {
- if (connection != null) {
- sendStart(connection!!)
- }
- }
-
- private fun sendStart(webSocket: WebSocket) {
- val image = dataService.retrieveImage()
- if (image == null) {
- WokwiNotifier.notifyBalloon("Failed to retrieve image", project, NotificationType.ERROR)
- return
- }
-
- val cmd = WokwiCommand.start(image.elf, image.romSegments.toList())
-
- val json = Json.encodeToString(WokwiCommand.serializer(), cmd)
- webSocket.send(json)
- }
-
- fun disconnect(webSocket: WebSocket) {
- this.connection = null
- }
-
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/Command.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/Command.kt
new file mode 100644
index 0000000..f14a6b1
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/Command.kt
@@ -0,0 +1,62 @@
+package com.github.jozott00.wokwiintellij.simulator
+
+import com.beust.klaxon.json
+import com.github.jozott00.wokwiintellij.simulator.args.FirmwareFormat
+
+@Suppress("unused")
+object Command {
+
+ fun start(diagram: String, firmware: String, firmwareFormat: FirmwareFormat, license: String, waitForDebugger: Boolean): String {
+ return json {
+ obj(
+ "command" to "start",
+ "diagram" to diagram,
+ "license" to license,
+ "firmware" to firmware,
+ "firmwareFormat" to firmwareFormat.toString(),
+ "firmwareB64" to true,
+ "pause" to waitForDebugger,
+ "useGateway" to false, // private gateways not yet supported
+ "disableSerialMonitor" to true,
+ )
+ }.toJsonString()
+ }
+
+ fun editor(diagram: String, license: String) = json {
+ obj(
+ "command" to "editor",
+ "diagram" to diagram,
+ "license" to license,
+ "chips" to array(),
+ "readonly" to false,
+ )
+ }.toJsonString()
+
+
+ fun resourceData(buffer: String): String {
+ return json {
+ obj(
+ "command" to "resourceData",
+ "buffer" to buffer,
+ )
+ }.toJsonString()
+ }
+
+ fun gdbMessage(message: String): String {
+ return json {
+ obj(
+ "command" to "gdbMessage",
+ "message" to message,
+ )
+ }.toJsonString()
+ }
+
+ fun gdbBreak(): String {
+ return json {
+ obj(
+ "command" to "gdbBreak",
+ )
+ }.toJsonString()
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/Simulator.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/Simulator.kt
new file mode 100644
index 0000000..c14ce19
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/Simulator.kt
@@ -0,0 +1,16 @@
+package com.github.jozott00.wokwiintellij.simulator
+
+import com.github.jozott00.wokwiintellij.simulator.args.WokwiArgsFirmware
+import com.github.jozott00.wokwiintellij.simulator.gdb.GDBServerCommunicator
+
+interface Simulator {
+
+ fun start()
+
+ fun setFirmware(firmware: WokwiArgsFirmware)
+
+ fun getFirmware(): WokwiArgsFirmware
+
+ suspend fun connectToGDBServer(server: GDBServerCommunicator)
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/WokwiConfig.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/WokwiConfig.kt
new file mode 100644
index 0000000..df7c691
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/WokwiConfig.kt
@@ -0,0 +1,13 @@
+@file:Suppress("unused")
+
+package com.github.jozott00.wokwiintellij.simulator
+
+import com.intellij.openapi.vfs.VirtualFile
+
+class WokwiConfig(
+ val version: String,
+ val elf: VirtualFile,
+ val firmware: VirtualFile,
+ val diagram: VirtualFile,
+ val gdbServerPort: Int?
+)
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/WokwiSimulator.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/WokwiSimulator.kt
new file mode 100644
index 0000000..91e0dae
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/WokwiSimulator.kt
@@ -0,0 +1,226 @@
+package com.github.jozott00.wokwiintellij.simulator
+
+
+import com.github.jozott00.wokwiintellij.jcef.BrowserPipe
+import com.github.jozott00.wokwiintellij.simulator.args.WokwiArgs
+import com.github.jozott00.wokwiintellij.simulator.args.WokwiArgsFirmware
+import com.github.jozott00.wokwiintellij.simulator.gdb.GDBServerCommunicator
+import com.github.jozott00.wokwiintellij.simulator.gdb.GDBServerEvent
+import com.github.jozott00.wokwiintellij.ui.jcef.SimulatorJCEFHtmlPanel
+import com.intellij.execution.process.AnsiEscapeDecoder
+import com.intellij.execution.process.ProcessOutputTypes
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.ui.ComponentContainer
+import com.intellij.openapi.util.Disposer
+import com.intellij.openapi.util.Key
+import com.intellij.util.containers.ContainerUtil
+import io.ktor.util.*
+import kotlinx.serialization.json.*
+import java.net.URL
+import kotlin.io.encoding.Base64
+import kotlin.io.encoding.ExperimentalEncodingApi
+
+private val LOG = logger()
+
+class WokwiSimulator(
+ private val runArgs: WokwiArgs,
+ parentDisposable: Disposable,
+) : Disposable,
+ Simulator,
+ ComponentContainer,
+ BrowserPipe.Subscriber {
+
+ private var browserReady = false
+ private var startInvoked = false
+
+ private val browser = SimulatorJCEFHtmlPanel(this)
+ private val browserPipe = browser.browserPipe
+
+ private val myEventMulticaster = createEventMulticaster()
+ private val myListeners: MutableList = ContainerUtil.createLockFreeCopyOnWriteList()
+
+ private val ansiEscapeDecoder = AnsiEscapeDecoder()
+ private var gdbServer: GDBServerCommunicator? = null
+
+ private var simulationRunning = false
+
+ init {
+ Disposer.register(parentDisposable, this)
+ browserPipe.subscribe(PIPE_TOPIC, this, this)
+ }
+
+ override fun start() {
+ startInvoked = true
+ startInternal()
+ }
+
+ private fun startInternal() {
+ simulationRunning = false
+
+ // if browser not yet ready just return
+ if (!browserReady) return
+ if (!startInvoked) return
+
+ LOG.info("(Re)starting simulation...")
+
+ @OptIn(ExperimentalEncodingApi::class)
+ val firmwareString = Base64.encode(runArgs.firmware.buffer)
+
+ val cmd = Command.start(
+ diagram = runArgs.diagram,
+ firmware = firmwareString,
+ firmwareFormat = runArgs.firmware.format,
+ license = runArgs.license,
+ waitForDebugger = runArgs.waitForDebugger
+ )
+ browserPipe.send(PIPE_TOPIC, cmd)
+ myEventMulticaster.onStarted(runArgs)
+ }
+
+ override fun setFirmware(firmware: WokwiArgsFirmware) {
+ runArgs.firmware = firmware
+ }
+
+ override fun getFirmware(): WokwiArgsFirmware {
+ return runArgs.firmware
+ }
+
+ override suspend fun connectToGDBServer(server: GDBServerCommunicator) {
+ gdbServer = server
+ server.getMessageFlow().collect { event ->
+ when (event) {
+ is GDBServerEvent.Connected -> {}
+ is GDBServerEvent.Error -> LOG.error("Error: ${event.error}")
+ is GDBServerEvent.Message -> {
+ browserPipe.send(PIPE_TOPIC, Command.gdbMessage(event.message))
+ }
+ is GDBServerEvent.Break -> browserPipe.send(PIPE_TOPIC, Command.gdbBreak())
+ }
+ }
+ }
+
+ private fun startRecv() {
+ browserReady = true
+ startInternal()
+ }
+
+ private fun uartDataRecv(data: JsonObject) {
+ val bytes = data["bytes"]
+ ?.jsonArray
+ ?.map { it.jsonPrimitive.int.toByte() }
+ ?.toByteArray() ?: run {
+ LOG.error("Malformed data received: No bytes: $data")
+ return
+ }
+
+ if (bytes.isEmpty()) return
+
+ val str = String(bytes, Charsets.UTF_8)
+
+ ansiEscapeDecoder.escapeText(str, ProcessOutputTypes.STDOUT) { t, contentType ->
+ myEventMulticaster.onTextAvailable(t, contentType)
+ }
+ }
+
+ private fun loadResourceRecv(req: JsonObject) {
+ // TODO: Make this offline
+ val urlString = req["url"]?.jsonPrimitive?.content ?: run {
+ LOG.error("Malformed data received: No url: $req")
+ return
+ }
+ val url = URL(urlString)
+ val resource = url.readBytes().encodeBase64()
+ val cmd = Command.resourceData(resource)
+ browserPipe.send(PIPE_TOPIC, cmd)
+
+ checkSimulationStartedRunning()
+ }
+
+ private fun gdbResponseRecv(req: JsonObject) {
+ val response = req["response"]?.jsonPrimitive?.content ?: run {
+ LOG.error("Malformed data received: No response field: $req")
+ return
+ }
+ gdbServer?.sendResponse(response)
+ }
+
+ override fun messageReceived(data: String): Boolean {
+ val json = Json.parseToJsonElement(data).jsonObject
+
+ val type: String = json["command"]?.jsonPrimitive?.content ?: run {
+ LOG.error("Malformed data received: $data")
+ return false
+ }
+
+ when (type) {
+ "start" -> startRecv()
+ "loadResource" -> loadResourceRecv(json)
+ "uartData" -> uartDataRecv(json) // do nothing right now
+ "wifiFrame", "wifiConnect" -> {
+ TODO("Not yet implemented")
+ } // do nothing right now
+ "gdbResponse" -> gdbResponseRecv(json)
+ else -> {
+ LOG.warn("Unknown command: $type")
+ LOG.debug("Unknown command data: $data")
+ return false
+ }
+ }
+
+ return true
+ }
+
+ private fun checkSimulationStartedRunning() {
+ if (!simulationRunning) {
+ simulationRunning = true
+ myEventMulticaster.onRunning()
+ }
+ }
+
+ companion object {
+ private const val PIPE_TOPIC = "wokwi"
+ }
+
+ fun addSimulatorListener(listener: WokwiSimulatorListener) {
+ myListeners.add(listener)
+ }
+
+ override fun dispose() {
+ createEventMulticaster().onShutdown()
+ myListeners.clear()
+ }
+
+ override fun getComponent() = browser.component
+
+ override fun getPreferredFocusableComponent() = component
+
+
+ private fun createEventMulticaster(): WokwiSimulatorListener {
+ return object : WokwiSimulatorListener {
+ override fun onStarted(runArgs: WokwiArgs) {
+ notifyAll { it.onStarted(runArgs) }
+ }
+
+ override fun onShutdown() {
+ notifyAll { it.onShutdown() }
+ }
+
+ override fun onTextAvailable(text: String, outputType: Key<*>) {
+ notifyAll { it.onTextAvailable(text, outputType) }
+ }
+
+ override fun onRunning() {
+ notifyAll { it.onRunning() }
+ }
+
+ private fun notifyAll(m: (WokwiSimulatorListener) -> Unit) {
+ for (l in myListeners) {
+ m(l)
+ }
+ }
+
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/WokwiSimulatorListener.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/WokwiSimulatorListener.kt
new file mode 100644
index 0000000..ef2e0f6
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/WokwiSimulatorListener.kt
@@ -0,0 +1,11 @@
+package com.github.jozott00.wokwiintellij.simulator
+
+import com.github.jozott00.wokwiintellij.simulator.args.WokwiArgs
+import com.intellij.openapi.util.Key
+
+interface WokwiSimulatorListener {
+ fun onStarted(runArgs: WokwiArgs) {}
+ fun onShutdown() {}
+ fun onTextAvailable(text: String, outputType: Key<*>) {}
+ fun onRunning() {}
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/args/WokwiArgs.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/args/WokwiArgs.kt
new file mode 100644
index 0000000..ccd46bd
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/args/WokwiArgs.kt
@@ -0,0 +1,42 @@
+@file:Suppress("unused")
+
+package com.github.jozott00.wokwiintellij.simulator.args
+
+import com.intellij.openapi.vfs.VirtualFile
+
+
+class WokwiArgs(
+ val license: String,
+ val diagram: String,
+ var firmware: WokwiArgsFirmware,
+ var waitForDebugger: Boolean = false,
+)
+
+@Suppress("unused")
+class WokwiArgsFirmware(
+ val buffer: ByteArray,
+ val format: FirmwareFormat,
+ val rootFile: VirtualFile,
+ val isFlasherFile: Boolean,
+ val size: UInt,
+ val binaryPaths: List
+)
+
+enum class FirmwareFormat {
+ HEX,
+ UF2,
+ BIN;
+
+ override fun toString() = name.lowercase()
+}
+
+enum class WokwiProjectType {
+ RUST,
+ ZEPHYR,
+ PLATFORMIO,
+ ESP_IDF,
+ PICO_SDK,
+ ARDUINO,
+ SMING,
+ UNKNOWN
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/args/WokwiArgsUtils.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/args/WokwiArgsUtils.kt
new file mode 100644
index 0000000..10f1be1
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/args/WokwiArgsUtils.kt
@@ -0,0 +1,2 @@
+package com.github.jozott00.wokwiintellij.simulator.args
+
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/gdb/wokwiGDBServer.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/gdb/wokwiGDBServer.kt
new file mode 100644
index 0000000..287d67f
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/simulator/gdb/wokwiGDBServer.kt
@@ -0,0 +1,208 @@
+package com.github.jozott00.wokwiintellij.simulator.gdb
+
+import com.github.jozott00.wokwiintellij.utils.WokwiNotifier
+import com.github.jozott00.wokwiintellij.utils.runCloseable
+import com.intellij.notification.NotificationType
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.util.Disposer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.BufferedReader
+import java.io.Closeable
+import java.io.InputStreamReader
+import java.io.PrintWriter
+import java.net.ServerSocket
+import java.net.Socket
+import java.net.SocketException
+
+
+sealed class GDBServerEvent {
+ data object Connected : GDBServerEvent()
+ data class Error(val error: Throwable) : GDBServerEvent()
+ data class Message(val message: String) : GDBServerEvent()
+ data object Break : GDBServerEvent()
+}
+
+interface GDBServerCommunicator {
+ fun getMessageFlow(): Flow
+ fun sendResponse(response: String)
+}
+
+
+class WokwiGDBServer(private val cs: CoroutineScope, parentDisposable: Disposable) : GDBServerCommunicator, Disposable {
+
+ init {
+ Disposer.register(parentDisposable, this)
+ }
+
+ private var serverSocket: ServerSocket? = null
+ private var currentMessageProcessor: MessageProcessor? = null
+ private var eventChannel = Channel { Channel.BUFFERED }
+
+ fun listen(port: Int) = cs.launch(Dispatchers.IO) {
+ try {
+ ServerSocket(port).use { socket ->
+ serverSocket = socket
+
+ LOG.info("GDB Server listening on port $port")
+
+ while (true) {
+ val clientSocket = try {
+ socket.runCloseable { it.accept() }
+ } catch (e: SocketException) {
+ break
+ }
+ currentMessageProcessor?.close()
+ currentMessageProcessor = null
+ handleConnection(clientSocket)
+ }
+ }
+ } catch (e: Exception) {
+ LOG.warn(e)
+ WokwiNotifier.notifyBalloonAsync(
+ "Couldn't start GDB server",
+ "Failed to create server socket: ${e.message}",
+ NotificationType.ERROR
+ )
+ }
+ }
+
+ fun isRunning() = serverSocket?.isClosed?.not() ?: false
+
+ private suspend fun handleConnection(socket: Socket) {
+ currentMessageProcessor = MessageProcessor(socket, eventChannel)
+ currentMessageProcessor?.process()
+ }
+
+ override fun sendResponse(response: String) = cs.launch(Dispatchers.IO) {
+ currentMessageProcessor?.writeResponse(response)
+ }.let { }
+
+ override fun dispose() {
+ currentMessageProcessor?.close()
+ currentMessageProcessor = null
+
+ serverSocket?.close()
+ serverSocket = null
+ }
+
+
+ override fun getMessageFlow(): Flow {
+ return eventChannel.receiveAsFlow()
+ }
+
+ fun resetEventChannel() {
+ eventChannel.close()
+ eventChannel = Channel { Channel.BUFFERED }
+ }
+
+ companion object {
+ val LOG = logger()
+ }
+}
+
+private class MessageProcessor(private val socket: Socket, private val eventChannel: Channel) :
+ Closeable {
+
+ private val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
+ private val writer = PrintWriter(socket.getOutputStream(), true)
+
+ suspend fun process() = socket.use {
+ writer.println("+")
+
+ dispatchEvent(GDBServerEvent.Connected)
+
+ var buf = ""
+ while (true) {
+ val data = try {
+ reader.read()
+ } catch (e: Exception) {
+ return@use
+ }
+ if (data == -1)
+ break
+ if (data == 3) {
+ LOG.debug("Received break")
+ dispatchEvent(GDBServerEvent.Break)
+ continue
+ }
+ buf += data.toChar()
+ while (shouldContinueProcessingMessage(buf)) {
+ val message = extractMessage(buf)
+ val receivedChecksum = extractChecksum(buf)
+ buf = trimProcessedParts(buf)
+
+ if (calculateChecksum(message) != receivedChecksum) {
+ writer.println('-') // Negative acknowledgment
+ LOG.warn("Warning: GDB checksum error in message: $message")
+ } else {
+ writer.println('+') // Positive acknowledgment
+
+ if (checkDetach(message))
+ return@use
+
+ dispatchEvent(GDBServerEvent.Message(message))
+ }
+ }
+ }
+ }
+
+ fun writeResponse(response: String) {
+ writer.println(response)
+ }
+
+ private suspend fun dispatchEvent(event: GDBServerEvent) = withContext(Dispatchers.IO) {
+ eventChannel.send(event)
+ }
+
+
+ private fun shouldContinueProcessingMessage(buf: String): Boolean {
+ val dollar = buf.indexOf('$')
+ val hash = buf.indexOf('#')
+ return dollar > -1 && hash > -1 && hash > dollar && hash + 3 <= buf.length
+ }
+
+ private fun extractMessage(buf: String): String {
+ val dollar = buf.indexOf('$')
+ val hash = buf.indexOf('#')
+ return buf.substring(dollar + 1, hash)
+ }
+
+ private fun extractChecksum(buf: String): String {
+ val hash = buf.indexOf('#')
+ return buf.substring(hash + 1, hash + 3)
+ }
+
+ private fun trimProcessedParts(buf: String): String {
+ val hash = buf.indexOf('#')
+ return buf.substring(hash + 3)
+ }
+
+ private fun calculateChecksum(message: String): String {
+ val checksum = message.sumOf { it.code } and 0xff
+ return "${(checksum ushr 4).toString(16)}${(checksum and 0xf).toString(16)}"
+ }
+
+ private fun checkDetach(message: String): Boolean {
+ if (message == "D") {
+ writer.println("+\$#00")
+ return true
+ }
+ return false
+ }
+
+ companion object {
+ val LOG = logger()
+ }
+
+ override fun close() {
+ if (!socket.isClosed)
+ socket.close()
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/states/WokwiConfigState.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/states/WokwiConfigState.kt
deleted file mode 100644
index 477b816..0000000
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/states/WokwiConfigState.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.github.jozott00.wokwiintellij.states
-
-import com.intellij.ide.util.PropertiesComponent
-import com.intellij.openapi.components.PersistentStateComponent
-import com.intellij.openapi.components.Service
-import com.intellij.openapi.components.State
-import com.intellij.util.xmlb.XmlSerializerUtil
-
-@Service(Service.Level.PROJECT)
-@State(name = "WokwiConfigModel")
-data class WokwiConfigState(
- var espDevice: ESPDevice = ESPDevice.ESP32,
- var flashSize: FlashSize = FlashSize._4MB,
- var elfPath: String = "",
- var watchElf: Boolean = true,
-) : PersistentStateComponent {
-
- override fun getState(): WokwiConfigState {
- return this
- }
-
- override fun loadState(state: WokwiConfigState) {
- XmlSerializerUtil.copyBean(state, this)
- }
-}
-
-enum class ESPDevice {
- ESP32,
- ESP32s2,
- ESP32s3,
- ESP32c3,
- ESP32c6;
-}
-
-enum class FlashSize {
- _2MB,
- _4MB,
- _8MB,
- _16MB,
- _32MB;
-
- override fun toString(): String {
- return name.removePrefix("_").removeSuffix("MB") + " MB"
- }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/states/WokwiSettingsState.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/states/WokwiSettingsState.kt
new file mode 100644
index 0000000..9c870d6
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/states/WokwiSettingsState.kt
@@ -0,0 +1,24 @@
+package com.github.jozott00.wokwiintellij.states
+
+import com.github.jozott00.wokwiintellij.WokwiConstants
+import com.intellij.openapi.components.PersistentStateComponent
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.State
+import com.intellij.util.xmlb.XmlSerializerUtil
+
+@Service(Service.Level.PROJECT)
+@State(name = "WokwiProjectSettings")
+data class WokwiSettingsState(
+ var wokwiConfigPath: String = WokwiConstants.WOKWI_CONFIG_FILE,
+ var wokwiDiagramPath: String = WokwiConstants.WOKWI_DIAGRAM_FILE,
+ var watchFirmware: Boolean = true,
+) : PersistentStateComponent {
+
+ override fun getState(): WokwiSettingsState {
+ return this
+ }
+
+ override fun loadState(state: WokwiSettingsState) {
+ XmlSerializerUtil.copyBean(state, this)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toml/Util.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toml/Util.kt
new file mode 100644
index 0000000..87dbdc6
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/toml/Util.kt
@@ -0,0 +1,34 @@
+package com.github.jozott00.wokwiintellij.toml
+
+import com.github.jozott00.wokwiintellij.WokwiConstants
+import com.intellij.psi.PsiFile
+import com.intellij.psi.util.childrenOfType
+import org.toml.lang.psi.*
+import org.toml.lang.psi.ext.TomlLiteralKind
+import org.toml.lang.psi.ext.kind
+
+val PsiFile.isWokwiToml: Boolean get() = name == WokwiConstants.WOKWI_CONFIG_FILE
+
+val TomlKey.stringValue: String
+ get() {
+ return segments.map { it.name }.joinToString(".")
+ }
+
+val TomlValue.stringValue: String?
+ get() {
+ val kind = (this as? TomlLiteral)?.kind
+ return (kind as? TomlLiteralKind.String)?.value
+ }
+
+val TomlFile.tableList: List get() = childrenOfType()
+
+
+fun TomlFile.findTable(key: String): TomlTable? {
+ return tableList.find {
+ it.header.key?.stringValue == key
+ }
+}
+
+fun TomlKeyValueOwner.findValue(key: String): TomlValue? {
+ return entries.find { it.key.stringValue == key }?.value
+}
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toml/WokwiConfigProcessor.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toml/WokwiConfigProcessor.kt
new file mode 100644
index 0000000..f66c4f2
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/toml/WokwiConfigProcessor.kt
@@ -0,0 +1,185 @@
+package com.github.jozott00.wokwiintellij.toml
+
+import com.akuleshov7.ktoml.TomlInputConfig
+import com.akuleshov7.ktoml.exceptions.TomlDecodingException
+import com.akuleshov7.ktoml.file.TomlFileReader
+import com.github.jozott00.wokwiintellij.WokwiConstants
+import com.github.jozott00.wokwiintellij.extensions.findRelativeFiles
+import com.github.jozott00.wokwiintellij.simulator.WokwiConfig
+import com.github.jozott00.wokwiintellij.states.WokwiSettingsState
+import com.github.jozott00.wokwiintellij.utils.NotifyAction
+import com.github.jozott00.wokwiintellij.utils.WokwiNotifier
+import com.github.jozott00.wokwiintellij.utils.WokwiTemplates
+import com.intellij.notification.NotificationType
+import com.intellij.openapi.application.EDT
+import com.intellij.openapi.application.readAction
+import com.intellij.openapi.command.WriteCommandAction
+import com.intellij.openapi.components.service
+import com.intellij.openapi.fileEditor.FileEditorManager
+import com.intellij.openapi.fileEditor.OpenFileDescriptor
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.guessProjectDir
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.psi.PsiManager
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.serializer
+
+object WokwiConfigProcessor {
+
+ suspend fun loadConfig(project: Project, wokwiConfigPath: String, diagramPath: String): WokwiConfig? {
+ val absoluteWokwiPath = findWokwiConfigPath(wokwiConfigPath, project) ?: return null
+ val diagramFilePath = findWokwiDiagramPath(diagramPath, project) ?: return null
+ val tomlConfig = withContext(Dispatchers.IO) {
+ readConfig(absoluteWokwiPath, project)
+ } ?: return null
+ return withContext(Dispatchers.IO) {
+ loadConfig(project, tomlConfig, absoluteWokwiPath, diagramFilePath)
+ }
+ }
+
+ suspend fun readConfig(project: Project): WokwiTomlTable? {
+ val projectSettings = project.service()
+ val configFile = findWokwiConfigPath(projectSettings.wokwiConfigPath, project) ?: return null
+ return readConfig(configFile, project)
+ }
+
+ suspend fun findElfFile(project: Project): VirtualFile? {
+ val projectSettings = project.service()
+ val configFile = findWokwiConfigPath(projectSettings.wokwiConfigPath, project) ?: return null
+ val tomlConfig = readConfig(project) ?: return null
+ return configFile.parent.findFileByRelativePath(tomlConfig.elf)
+ }
+
+ private suspend fun readConfig(configFile: VirtualFile, project: Project): WokwiTomlTable? {
+
+ if (!configFile.exists()) {
+ notifyError("Configuration file `${configFile.path}` not found.")
+ return null
+ }
+
+ if (configFile.name != "wokwi.toml") {
+ notifyError("Wokwi configuration file must be called `wokwi.toml` but is actually `${configFile.name}`")
+ return null
+ }
+
+ val fileReader = TomlFileReader(
+ inputConfig = TomlInputConfig(
+ ignoreUnknownNames = true,
+ allowNullValues = true
+ )
+ )
+
+ lateinit var model: WokwiTomlConfig
+ try {
+ model = fileReader.decodeFromFile(serializer(), configFile.path)
+ } catch (e: TomlDecodingException) {
+ notifyError(
+ "Check your wokwi.toml file and try again",
+ getNotifyJumpToAction("Jump to config", project, configFile)
+ )
+ return null
+ }
+
+ return model.wokwi
+ }
+
+ private suspend fun loadConfig(
+ project: Project,
+ tomlConfig: WokwiTomlTable,
+ configFile: VirtualFile,
+ diagramFile: VirtualFile
+ ): WokwiConfig? {
+ val configDir = readAction { configFile.parent }
+
+ val elfFile = readAction { configDir.findFileByRelativePath(tomlConfig.elf) } ?: run {
+ notifyError(
+ "Invalid ELF path. Is the project already built?",
+ getNotifyJumpToAction("Jump to config", project, configFile)
+ )
+ return null
+ }
+
+ val firmwareFile = readAction { configDir.findFileByRelativePath(tomlConfig.firmware) } ?: run {
+ notifyError(
+ "Invalid firmware path. Is the project already built?",
+ getNotifyJumpToAction("Jump to config", project, configFile)
+ )
+ return null
+ }
+
+
+ return WokwiConfig(
+ version = tomlConfig.version.toString(),
+ elf = elfFile,
+ firmware = firmwareFile,
+ diagram = diagramFile,
+ gdbServerPort = tomlConfig.gdbServerPort
+ )
+ }
+
+
+ private suspend fun notifyError(error: String, action: NotifyAction? = null) {
+ withContext(Dispatchers.EDT) {
+ WokwiNotifier.notifyBalloonAsync(
+ "Couldn't load Wokwi configuration",
+ error,
+ NotificationType.ERROR,
+ action
+ )
+ }
+ }
+
+ @Suppress("SameParameterValue")
+ private fun getNotifyJumpToAction(text: String, project: Project, file: VirtualFile) = NotifyAction(text) { _, _ ->
+ val descriptor = OpenFileDescriptor(project, file)
+ FileEditorManager.getInstance(project).openTextEditor(descriptor, true)
+ }
+
+ private suspend fun findWokwiConfigPath(wokwiConfigPath: String, project: Project): VirtualFile? = withContext(Dispatchers.IO) { readAction { project
+ .findRelativeFiles(wokwiConfigPath)}.run {
+ if (isEmpty()) {
+ WokwiNotifier.notifyBalloon(
+ "Configuration file `$wokwiConfigPath` not found in project."
+ )
+ return@run null
+ }
+ if (size > 1) {
+ notifyError("Found multiple configuration files: \n${joinToString("\n")}. \nSpecify the concrete one in the Settings.")
+ return@run null
+ }
+
+ return@run first()
+ }}
+
+ private suspend fun findWokwiDiagramPath(wokwiDiagramPath: String, project: Project): VirtualFile? = withContext(Dispatchers.IO) {readAction { project
+ .findRelativeFiles(wokwiDiagramPath) }.run {
+ if (isEmpty()) {
+ notifyError(
+ "Diagram file `$wokwiDiagramPath` not found in project.",
+ NotifyAction("Create diagram.json") { _, _ ->
+ val psiManager = PsiManager.getInstance(project)
+ val virtualFile = project.guessProjectDir() ?: return@NotifyAction
+ val psiDir = psiManager.findDirectory(virtualFile)
+ WriteCommandAction.runWriteCommandAction(project) {
+ val diagramFile =
+ psiDir?.createFile(WokwiConstants.WOKWI_DIAGRAM_FILE) ?: return@runWriteCommandAction
+ val document = diagramFile.viewProvider.document
+ document.setText(WokwiTemplates.defaultDiagramJson())
+ val descriptor =
+ OpenFileDescriptor(project, diagramFile.virtualFile)
+ FileEditorManager.getInstance(project).openTextEditor(descriptor, true)
+ }
+ }
+ )
+ return@run null
+ }
+ if (size > 1) {
+ notifyError("Found multiple diagram files: \n${joinToString("\n")}. \nSpecify the concrete one in the Settings.")
+ return@run null
+ }
+ return@run first()
+ }}
+
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toml/WokwiTomlConfig.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toml/WokwiTomlConfig.kt
new file mode 100644
index 0000000..93f0473
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/toml/WokwiTomlConfig.kt
@@ -0,0 +1,17 @@
+package com.github.jozott00.wokwiintellij.toml
+
+import kotlinx.serialization.Serializable
+
+
+@Serializable
+data class WokwiTomlConfig(
+ val wokwi: WokwiTomlTable
+)
+
+@Serializable
+data class WokwiTomlTable(
+ val version: Int,
+ val elf: String,
+ val firmware: String,
+ val gdbServerPort: Int? = null
+)
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/ConsoleWindowFactory.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/ConsoleWindowFactory.kt
new file mode 100644
index 0000000..6480e00
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/ConsoleWindowFactory.kt
@@ -0,0 +1,24 @@
+package com.github.jozott00.wokwiintellij.toolWindow
+
+import com.github.jozott00.wokwiintellij.services.WokwiComponentService
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.wm.ToolWindow
+import com.intellij.openapi.wm.ToolWindowFactory
+import com.intellij.ui.content.ContentFactory
+
+class ConsoleWindowFactory : ToolWindowFactory {
+ override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
+ val componentService = toolWindow.project.service()
+
+ val consoleToolWindow = componentService.consoleToolWindowComponent
+ val content = ContentFactory.getInstance()
+ .createContent(consoleToolWindow, null, false)
+
+ toolWindow.contentManager.addContent(content)
+ }
+
+ @Deprecated("Use isApplicableAsync", ReplaceWith("isApplicableAsync"))
+ override fun isApplicable(project: Project): Boolean = false
+
+}
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/JCEFContent: JPanel().kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/JCEFContent: JPanel().kt
deleted file mode 100644
index 1a716a0..0000000
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/JCEFContent: JPanel().kt
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.github.jozott00.wokwiintellij.toolWindow
-
-import com.intellij.openapi.Disposable
-import com.intellij.ui.jcef.JBCefBrowser
-import com.intellij.ui.jcef.JBCefBrowserBase
-import com.intellij.ui.jcef.JBCefClient.Properties.JS_QUERY_POOL_SIZE
-import com.intellij.ui.jcef.JBCefJSQuery
-import com.intellij.ui.jcef.JCEFHtmlPanel
-import org.cef.browser.CefBrowser
-import org.cef.browser.CefFrame
-import org.cef.handler.CefLoadHandlerAdapter
-import java.awt.BorderLayout
-import javax.swing.JPanel
-
-
-class JCEFContent(onLoaded: (JCEFContent) -> Unit) : JPanel(), Disposable {
-
- val browser: JBCefBrowser =
- JCEFHtmlPanel("https://wokwi.com/_alpha/wembed/345144250522927698?partner=espressif&port=9012&data=demo")
-
- init {
- browser.let {
- layout = BorderLayout()
- add(it.component, BorderLayout.CENTER)
- }
- }
-
- init {
- browser.jbCefClient.setProperty(JS_QUERY_POOL_SIZE, 5)
- browser.jbCefClient.addLoadHandler(object : CefLoadHandlerAdapter() {
- override fun onLoadEnd(cefBrowser: CefBrowser?, frame: CefFrame?, httpStatusCode: Int) {
- println("-------------- LOAD END")
-
- val loadedCallback = JBCefJSQuery.create(browser as JBCefBrowserBase)
- loadedCallback.addHandler { _ ->
- onLoaded(this@JCEFContent)
- null
- }
-
- cefBrowser!!.executeJavaScript(
- """
- document.getElementsByTagName("header")[0].style.display = "none";
- document.body.style.overflow = "hidden";
-
- var parentDiv = document.querySelector('.simulation_simulationControls__Jqtsp');
- var childDivs = parentDiv.children;
-
- // Sleep for a little to delay simulator show up
- setTimeout(function(){
- ${loadedCallback.inject(null)}
- }, 300);
-
- """.trimIndent(), null, 0
- )
-
- }
- }, browser.cefBrowser)
- }
-
- override fun dispose() {
- browser.dispose()
- }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/SimulatorPanel.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/SimulatorPanel.kt
deleted file mode 100644
index 0cbb88c..0000000
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/SimulatorPanel.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-package com.github.jozott00.wokwiintellij.toolWindow
-
-
-import com.intellij.ide.wizard.withVisualPadding
-import com.intellij.openapi.actionSystem.ActionManager
-import com.intellij.openapi.actionSystem.DefaultActionGroup
-import com.intellij.ui.components.Label
-import com.intellij.ui.dsl.builder.Align
-import com.intellij.ui.dsl.builder.LabelPosition
-import com.intellij.ui.dsl.builder.panel
-import com.intellij.ui.util.preferredWidth
-import java.awt.BorderLayout
-import java.awt.CardLayout
-import java.awt.FlowLayout
-import javax.swing.JPanel
-import javax.swing.JProgressBar
-
-class SimulatorPanel : JPanel() {
-
- private val cardLayout = CardLayout()
- private var browser: JCEFContent? = null
-
- private val loadingPanel = panel {
- panel {
- row {
- cell(JProgressBar().apply {
- isIndeterminate = true
- preferredWidth = 300
- })
- .label("Starting simulator...", LabelPosition.TOP)
-
- }
- }.align(Align.CENTER)
-
- }.withVisualPadding()
-
-
- init {
- layout = cardLayout
- add(loadingPanel)
- add("LOADING", loadingPanel)
- cardLayout.show(this, "LOADING")
- }
-
-
- fun loadSimulator() {
- browser = JCEFContent { browser ->
- cardLayout.show(this, "SIMULATOR")
- revalidate()
- repaint()
- }
-
- val simulator = simulator(toolbar(), browser!!)
- add("SIMULATOR", simulator)
- revalidate()
- repaint()
- }
-
- fun stopSimulator() {
- remove(browser)
- browser?.dispose()
-
- cardLayout.show(this, "LOADING")
-
- revalidate()
- repaint()
- }
-
- private fun simulator(toolbar: JPanel, browser: JPanel): JPanel {
- val simulator = JPanel(BorderLayout())
- simulator.add(toolbar, BorderLayout.NORTH)
- simulator.add(browser!!, BorderLayout.CENTER)
-
- return simulator
- }
-
- private fun toolbar(): JPanel {
-
- val panel = JPanel(BorderLayout())
-
- val am = ActionManager.getInstance()
- val group = DefaultActionGroup(
- am.getAction("com.github.jozott00.wokwiintellij.actions.WokwiStopAction"),
- am.getAction("com.github.jozott00.wokwiintellij.actions.WokwiRestartAction"),
- am.getAction("com.github.jozott00.wokwiintellij.actions.WokwiWatchAction"),
- )
- val toolbar = am.createActionToolbar("com.github.jozott00.wokwiintellij.actions.WokwiToolbar", group, false)
- // horizonal orientation
- toolbar.orientation = 0
- toolbar.targetComponent = panel
- panel.add(toolbar.component, BorderLayout.NORTH)
-
-
- return panel
- }
-
- private fun updateLayout() {
- layout = CardLayout()
- }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/SimulatorWindowFactory.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/SimulatorWindowFactory.kt
index f614881..ee78372 100644
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/SimulatorWindowFactory.kt
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/SimulatorWindowFactory.kt
@@ -1,30 +1,22 @@
package com.github.jozott00.wokwiintellij.toolWindow
-import com.github.jozott00.wokwiintellij.actions.WokwiRestartAction
import com.github.jozott00.wokwiintellij.services.WokwiComponentService
-import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.components.service
-import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory
import com.intellij.ui.content.ContentFactory
-import com.intellij.ui.dsl.builder.panel
-import org.jdesktop.swingx.action.ActionManager
-import javax.swing.JPanel
class SimulatorWindowFactory : ToolWindowFactory {
- init {
- thisLogger().warn("Don't forget to remove all non-needed sample code files with their corresponding registration entries in `plugin.xml`.")
- }
-
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
val componentService = toolWindow.project.service()
- val toolWindowContent = componentService.toolWindow.getContent()
+ val toolWindowContent = componentService.simulatorToolWindowComponent
+
val content = ContentFactory.getInstance()
.createContent(toolWindowContent, null, false)
+
toolWindow.contentManager.addContent(content)
}
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiConfigPanel.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiConfigPanel.kt
deleted file mode 100644
index 894de0a..0000000
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiConfigPanel.kt
+++ /dev/null
@@ -1,84 +0,0 @@
-package com.github.jozott00.wokwiintellij.toolWindow
-
-import com.github.jozott00.wokwiintellij.states.ESPDevice
-import com.github.jozott00.wokwiintellij.states.FlashSize
-import com.github.jozott00.wokwiintellij.states.WokwiConfigState
-import com.intellij.icons.AllIcons
-import com.intellij.ide.wizard.withVisualPadding
-import com.intellij.openapi.actionSystem.ActionManager
-import com.intellij.openapi.application.invokeLater
-import com.intellij.openapi.ui.DialogPanel
-import com.intellij.openapi.ui.TextFieldWithBrowseButton
-import com.intellij.ui.dsl.builder.*
-import com.intellij.ui.dsl.gridLayout.HorizontalAlign
-import com.intellij.ui.util.preferredWidth
-import java.awt.Font
-import javax.swing.*
-
-
-class WokwiConfigPanelBuilder(val model: WokwiConfigState) {
-
- var onChangeAction: (() -> Unit)? = null
-
- fun build(): DialogPanel {
- var panel: DialogPanel? = null
- val action = ActionManager.getInstance().getAction("com.github.jozott00.wokwiintellij.actions.WokwiStartAction")
-
- fun onChange() {
- invokeLater {
- if (panel == null)
- return@invokeLater
-
- panel!!.apply()
- onChangeAction?.invoke()
- }
- }
-
- panel = panel {
- lateinit var textField: Cell
-
- row {
- button("Start Simulator", action)
- .align(Align.CENTER)
- .apply {
- this.component.icon = AllIcons.Debugger.ThreadRunning
- }
- }
-
- group("Simulation Settings") {
- row("ESP target device:") {
- comboBox(ESPDevice.entries)
- .onChanged { _ -> onChange() }
- .bindItem(model::espDevice.toNullableProperty())
- }.rowComment("Wokwi simulator diagram is chosen based on target device.")
-
- row("Executable: ") {
- textField = textFieldWithBrowseButton().apply {
- component.preferredWidth = 300
- }
- .onChanged { _ -> onChange() }
- .bindText(model::elfPath)
- }.rowComment("Path to ELF binary to run in simulator")
-
- row("Flash Size: ") {
- comboBox(FlashSize.entries)
- .onChanged { _ -> onChange() }
- .bindItem(model::flashSize.toNullableProperty())
- }.rowComment("Must be compatible with device and partition table")
- }
- }
- .withVisualPadding()
-
-
- return panel
- }
-
-}
-
-fun wokwiConfigPanel(model: WokwiConfigState, build: WokwiConfigPanelBuilder.() -> Unit): DialogPanel {
- return WokwiConfigPanelBuilder(model).apply(build).build()
-}
-
-private fun JComponent.bold(isBold: Boolean) {
- font = font.deriveFont(if (isBold) Font.BOLD else Font.PLAIN)
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiConsoleToolWindow.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiConsoleToolWindow.kt
new file mode 100644
index 0000000..75cd6e9
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiConsoleToolWindow.kt
@@ -0,0 +1,74 @@
+package com.github.jozott00.wokwiintellij.toolWindow
+
+import com.github.jozott00.wokwiintellij.extensions.wokwiDisposable
+import com.github.jozott00.wokwiintellij.ui.console.SimulationConsole
+import com.intellij.execution.ui.layout.impl.JBRunnerTabs
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.actionSystem.ActionGroup
+import com.intellij.openapi.actionSystem.ActionManager
+import com.intellij.openapi.actionSystem.DefaultActionGroup
+import com.intellij.openapi.project.Project
+import com.intellij.ui.tabs.TabInfo
+import java.awt.BorderLayout
+import javax.swing.JComponent
+import javax.swing.JPanel
+
+
+@Suppress("SameParameterValue")
+class WokwiConsoleToolWindow(project: Project) :
+ JPanel() {
+ private val tabs: WokwiConsoleTabs = WokwiConsoleTabs(project, project.wokwiDisposable)
+ private val controlActionGroup = createControlActionGroup()
+
+ private val actionPlace = "WokwiConsole.topMiddleToolbar"
+ private val wrapper = ConsoleWrapper()
+
+ init {
+ tabs.apply {
+ addTab(createTabInfo("Console", wrapper))
+ }
+
+ layout = BorderLayout()
+ add(tabs, BorderLayout.CENTER)
+ }
+
+ fun setConsole(console: SimulationConsole) {
+ wrapper.setConsole(console)
+ }
+
+ private fun createTabInfo(title: String, content: JComponent): TabInfo {
+ return TabInfo(content).apply {
+ text = title
+ setActions(controlActionGroup, actionPlace)
+ }
+ }
+
+ private fun createControlActionGroup(): ActionGroup {
+ val am = ActionManager.getInstance()
+ return DefaultActionGroup().apply {
+ add(am.getAction("com.github.jozott00.wokwiintellij.actions.WokwiRestartAction"))
+ add(am.getAction("com.github.jozott00.wokwiintellij.actions.WokwiStopAction"))
+ addSeparator()
+ add(am.getAction("com.github.jozott00.wokwiintellij.actions.WokwiWatchAction"))
+ }
+
+ }
+
+
+ class ConsoleWrapper : JPanel() {
+ init {
+ layout = BorderLayout()
+ }
+
+ fun setConsole(console: SimulationConsole) {
+ removeAll()
+ add(console)
+ repaint()
+ }
+
+
+ }
+
+ class WokwiConsoleTabs(project: Project, parentDisposable: Disposable) :
+ JBRunnerTabs(project, parentDisposable)
+}
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiSimulationToolWindow.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiSimulationToolWindow.kt
new file mode 100644
index 0000000..72de128
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiSimulationToolWindow.kt
@@ -0,0 +1,28 @@
+package com.github.jozott00.wokwiintellij.toolWindow
+
+import com.intellij.openapi.ui.DialogPanel
+import java.awt.BorderLayout
+import javax.swing.JComponent
+import javax.swing.JPanel
+
+class WokwiSimulationToolWindow(private val configPanel: DialogPanel) :
+ JPanel() {
+
+ init {
+ this.layout = BorderLayout()
+ this.add(configPanel)
+ }
+
+ fun showSimulation(simulator: JComponent) {
+ this.removeAll()
+ this.add(simulator)
+ this.repaint()
+ }
+
+ fun showConfig() {
+ this.removeAll()
+ this.add(configPanel)
+ this.repaint()
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiToolWindow.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiToolWindow.kt
deleted file mode 100644
index 45b066c..0000000
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiToolWindow.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.github.jozott00.wokwiintellij.toolWindow
-
-import com.intellij.openapi.ui.DialogPanel
-import java.awt.BorderLayout
-import javax.swing.JPanel
-
-class WokwiToolWindow(private val configPanel: DialogPanel, private val simulationPanel: SimulatorPanel) {
-
- private val panel = JPanel(BorderLayout()).apply {
- this.add(configPanel)
-
- }
-
- fun getContent() = panel
-
- fun showSimulation() {
- if (panel.components.contains(simulationPanel)) return
- panel.removeAll()
- panel.add(simulationPanel)
- simulationPanel.loadSimulator()
- panel.revalidate()
- panel.repaint()
- }
-
- fun showConfig() {
- if (panel.components.contains(configPanel)) return
- panel.removeAll()
- panel.add(configPanel)
- simulationPanel.stopSimulator()
- panel.revalidate()
- panel.repaint()
- }
-
-
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/WokwiIcons.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/WokwiIcons.kt
new file mode 100644
index 0000000..1df21fc
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/WokwiIcons.kt
@@ -0,0 +1,32 @@
+package com.github.jozott00.wokwiintellij.ui
+
+import com.intellij.icons.AllIcons
+import com.intellij.openapi.util.IconLoader
+import com.intellij.ui.LayeredIcon
+import com.intellij.util.IconUtil
+import javax.swing.Icon
+import javax.swing.SwingConstants
+
+object WokwiIcons {
+
+ private val Default = IconLoader.getIcon("icons/pluginIcon.svg", WokwiIcons.javaClass)
+
+ val SimulatorToolWindowIcon = IconLoader.getIcon("icons/pluginIcon@13x13.svg", WokwiIcons.javaClass)
+
+ val ConsoleToolWindowIcon = IconLoader.getIcon("icons/logIcon@13x13.svg", WokwiIcons.javaClass)
+
+ val ConfigFile = IconLoader.getIcon("icons/pluginIcon@16x16.svg", WokwiIcons.javaClass)
+
+ val Debug = LayeredIcon(2).also {
+ it.setIcon(Default, 0)
+ it.setIcon(Overlays.Debug, 1, SwingConstants.SOUTH_EAST)
+ }
+
+ object Overlays {
+ val Debug: Icon = IconUtil.scale(AllIcons.Actions.StartDebugger, null, 0.8f)
+ }
+
+}
+
+
+
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/config/LicensingDialog.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/config/LicensingDialog.kt
new file mode 100644
index 0000000..154c906
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/config/LicensingDialog.kt
@@ -0,0 +1,78 @@
+@file:Suppress("DialogTitleCapitalization")
+
+package com.github.jozott00.wokwiintellij.ui.config
+
+import com.github.jozott00.wokwiintellij.WokwiConstants
+import com.github.jozott00.wokwiintellij.services.WokwiLicensingService
+import com.intellij.ide.BrowserUtil
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.components.service
+import com.intellij.openapi.ui.DialogWrapper
+import com.intellij.openapi.ui.ValidationInfo
+import com.intellij.ui.components.JBTextArea
+import com.intellij.ui.dsl.builder.Align
+import com.intellij.ui.dsl.builder.panel
+import com.intellij.ui.dsl.builder.rows
+import java.util.*
+
+class LicensingDialog : DialogWrapper(true) {
+
+ private var licenseKeyArea: JBTextArea? = null
+ private val licensingService = ApplicationManager.getApplication().service()
+
+ init {
+ title = "Activate Wokwi License"
+
+ init()
+ }
+
+ override fun createCenterPanel() = panel {
+ row {
+ label("Get your license from wokwi.com and paste it below to use the Wokwi simulator.")
+ }
+ row {
+ button("Open wokwi.com") {
+ BrowserUtil.browse("https://wokwi.com/license?v=${WokwiConstants.WOKWI_WCODE_VERSION}")
+ }
+ }
+ separator()
+ row {
+ label("Enter license key:")
+ }
+ row {
+ licenseKeyArea = textArea()
+ .apply {
+ component.lineWrap = true
+ component.wrapStyleWord = true
+ }
+ .onChanged {
+ initValidation()
+ }
+ .rows(5)
+ .align(Align.FILL)
+ .component
+ }
+ }
+
+
+ override fun doValidate(): ValidationInfo? {
+ val license = licenseKeyArea?.text
+ if (license.isNullOrEmpty()) {
+ return ValidationInfo("License key cannot be empty")
+ }
+ val wokwiLicense = licensingService.parseLicense(license)
+ ?: return ValidationInfo("Invalid license key")
+
+
+ if (wokwiLicense.expiration < Date()) {
+ return ValidationInfo("License has expired")
+ }
+
+ return null
+ }
+
+ override fun doOKAction() {
+ super.doOKAction()
+ licenseKeyArea?.text?.let { licensingService.updateLicense(it) }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/config/LicensingPanel.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/config/LicensingPanel.kt
new file mode 100644
index 0000000..1a01f63
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/config/LicensingPanel.kt
@@ -0,0 +1,113 @@
+@file:Suppress("SameParameterValue")
+
+package com.github.jozott00.wokwiintellij.ui.config
+
+import com.github.jozott00.wokwiintellij.services.WokwiLicensingService
+import com.github.jozott00.wokwiintellij.utils.runInBackground
+import com.intellij.icons.AllIcons
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.invokeLater
+import com.intellij.openapi.components.service
+import com.intellij.openapi.ui.ComponentContainer
+import com.intellij.ui.dsl.builder.RightGap
+import com.intellij.ui.dsl.builder.panel
+import com.intellij.util.ui.AsyncProcessIcon
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import java.awt.CardLayout
+import javax.swing.JComponent
+import javax.swing.JLabel
+import javax.swing.JPanel
+
+
+class LicensingPanel : ComponentContainer {
+
+ private val licensingService = ApplicationManager.getApplication().service()
+
+ private val licenseNotSetPanel: JPanel = buildStatus(false, "No license activated!")
+ private val licenseInvalidPanel: JPanel = buildStatus(false, "License invalid!")
+ private val licenseExpiredPanel: JPanel = buildStatus(false, "License expired!")
+ private val licensePlanPanel = JLabel()
+ private val licenseSetPanel: JPanel = buildStatus(true, "License set", licensePlanPanel)
+
+ private val statusCardLayout = CardLayout()
+ private val statusCard = JPanel(statusCardLayout).also {
+ it.add("LOADING", AsyncProcessIcon("Loading"))
+ it.add("LICENSE_MISSING", licenseNotSetPanel)
+ it.add("LICENSE_INVALID", licenseInvalidPanel)
+ it.add("LICENSE_EXPIRED", licenseExpiredPanel)
+ it.add("LICENSE_SET", licenseSetPanel)
+ }
+
+ override fun getComponent() = panel {
+ row {
+ button("Activate License") {
+ LicensingDialog().show()
+ checkLicenseAvailability(true)
+ }
+
+// button("Remove License") {
+// licensingService.removeLicense()
+// checkLicenseAvailability(true)
+// }
+ cell(statusCard)
+
+ }
+ row {
+ comment("Wokwi requires a license to run the simulator. All features supported by the plugin are community license features and therefore free.")
+ }
+ }.also {
+ checkLicenseAvailability()
+ }
+
+ @Suppress("SameParameterValue")
+ private fun checkLicenseAvailability(recentlyChanged: Boolean = false) = runInBackground{ runBlocking(Dispatchers.IO) {
+ if (recentlyChanged) {
+ invokeLater { statusCardLayout.show(statusCard, "LOADING") }
+ delay(500)
+ }
+
+ val raw = licensingService.getLicense()
+ ?: return@runBlocking invokeLater {
+ statusCardLayout.show(statusCard, "LICENSE_MISSING")
+ }
+
+ invokeLater {
+ val parsed = licensingService.parseLicense(raw)
+ val statusPanel = when {
+ parsed == null -> "LICENSE_INVALID"
+ !parsed.isValid() -> "LICENSE_EXPIRED"
+ else -> {
+ licensePlanPanel.text = "(${parsed.plan ?: "Community"})"
+ "LICENSE_SET"
+ }
+ }
+ statusCardLayout.show(statusCard, statusPanel)
+ }
+ } }
+
+ private fun buildStatus(valid: Boolean, message: String, plan: JLabel? = null) = panel {
+ row {
+ icon(if (valid) AllIcons.RunConfigurations.TestPassed else AllIcons.RunConfigurations.TestFailed)
+ .gap(RightGap.SMALL)
+ label(message)
+ .gap(RightGap.SMALL)
+
+ plan?.let {
+ cell(plan)
+ }
+ }
+ }
+
+ override fun dispose() {
+
+ }
+
+ override fun getPreferredFocusableComponent(): JComponent {
+ return component
+ }
+
+}
+
+
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/config/WokwiConfigPanel.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/config/WokwiConfigPanel.kt
new file mode 100644
index 0000000..98f6021
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/config/WokwiConfigPanel.kt
@@ -0,0 +1,115 @@
+package com.github.jozott00.wokwiintellij.ui.config
+
+import com.github.jozott00.wokwiintellij.states.WokwiSettingsState
+import com.intellij.icons.AllIcons
+import com.intellij.ide.wizard.withVisualPadding
+import com.intellij.openapi.actionSystem.ActionManager
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.guessProjectDir
+import com.intellij.openapi.ui.DialogPanel
+import com.intellij.openapi.ui.ValidationInfo
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.ui.dsl.builder.Align
+import com.intellij.ui.dsl.builder.BottomGap
+import com.intellij.ui.dsl.builder.bindText
+import com.intellij.ui.dsl.builder.panel
+import com.intellij.ui.util.preferredWidth
+import kotlin.io.path.pathString
+import kotlin.io.path.relativeTo
+
+
+class WokwiConfigPanelBuilder(val project: Project, private val model: WokwiSettingsState) {
+
+ var onChangeAction: (() -> Unit)? = null
+
+ fun build(): DialogPanel {
+ var panel: DialogPanel? = null
+ val action = ActionManager.getInstance().getAction("com.github.jozott00.wokwiintellij.actions.WokwiStartAction")
+
+ fun onChange() {
+ if (panel == null)
+ return
+
+ panel!!.apply()
+ onChangeAction?.invoke()
+ }
+
+ panel = panel {
+ row {
+ button("Start Simulator", action)
+ .align(Align.CENTER)
+ .apply {
+ this.component.icon = AllIcons.Debugger.ThreadRunning
+ }
+ }
+
+ group("License") {
+ row {
+ cell(LicensingPanel().component)
+ }
+
+ }
+
+ group("Settings") {
+ row("wokwi.toml path: ") {
+ textFieldWithBrowseButton { getRootRelativePathOf(it) }.apply {
+ component.preferredWidth = 400
+ }
+ .validationOnInput {
+ ValidationInfo("Hello world", it)
+ }
+ .validationOnApply {
+ this.error("Test error")
+ }
+ .onChanged {
+ onChange()
+ }
+ .bindText(model::wokwiConfigPath)
+
+ }
+
+ row {
+ comment("The wokwi.toml holds all information the plugin needs to know. Visit the wokwi.toml docs for more information.")
+ }
+ .bottomGap(BottomGap.SMALL)
+
+
+ row("diagram.json path: ") {
+ textFieldWithBrowseButton { getRootRelativePathOf(it) }.apply {
+ component.preferredWidth = 400
+ }
+ .onChanged { _ -> onChange() }
+ .bindText(model::wokwiDiagramPath)
+ }
+
+ row {
+ comment("The diagram.json specifies the simulation runtime environment. Visit the diagram.json docs for more information.")
+ }
+ }
+ }.apply {
+ autoscrolls = true
+ }
+ .withVisualPadding()
+
+
+ return panel
+ }
+
+ private fun getRootRelativePathOf(file: VirtualFile): String {
+ val projectPath = project.guessProjectDir()?.toNioPath() ?: return file.path
+ val resolved = projectPath.resolve(file.path)
+ val relative = resolved.relativeTo(projectPath)
+ return relative.pathString
+ }
+
+
+}
+
+fun wokwiConfigPanel(
+ project: Project,
+ model: WokwiSettingsState,
+ build: WokwiConfigPanelBuilder.() -> Unit
+): DialogPanel {
+ return WokwiConfigPanelBuilder(project, model).apply(build).build()
+}
+
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/console/SimulationConsole.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/console/SimulationConsole.kt
new file mode 100644
index 0000000..2df5ba9
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/console/SimulationConsole.kt
@@ -0,0 +1,49 @@
+package com.github.jozott00.wokwiintellij.ui.console
+
+import com.github.jozott00.wokwiintellij.simulator.WokwiSimulatorListener
+import com.github.jozott00.wokwiintellij.simulator.args.WokwiArgs
+import com.intellij.execution.filters.TextConsoleBuilderFactory
+import com.intellij.execution.ui.ConsoleView
+import com.intellij.execution.ui.ConsoleViewContentType
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.Disposer
+import com.intellij.openapi.util.Key
+import java.awt.BorderLayout
+import javax.swing.JPanel
+
+
+class SimulationConsole(project: Project) : JPanel(), Disposable, WokwiSimulatorListener {
+
+ private var executionRunning = false
+
+ private val consoleView: ConsoleView = TextConsoleBuilderFactory.getInstance().createBuilder(project).console
+// private val consoleView = LanguageConsoleBuilder()
+// .executionEnabled { this@SimulationConsole.executionRunning }
+// .oneLineInput(true)
+// .build(project, PlainTextLanguage.INSTANCE)
+
+ init {
+ Disposer.register(this, consoleView)
+
+ this.layout = BorderLayout()
+ add(consoleView.component)
+ }
+
+ override fun onTextAvailable(text: String, outputType: Key<*>) {
+ consoleView.print(text, ConsoleViewContentType.getConsoleViewType(outputType))
+ }
+
+ override fun onStarted(runArgs: WokwiArgs) {
+ executionRunning = true
+ consoleView.clear()
+ }
+
+ override fun onShutdown() {
+ executionRunning = false
+ }
+
+ override fun dispose() {
+ // nothing to do
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/jcef/ResourceLoader.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/jcef/ResourceLoader.kt
new file mode 100644
index 0000000..bda0b5b
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/jcef/ResourceLoader.kt
@@ -0,0 +1,14 @@
+package com.github.jozott00.wokwiintellij.ui.jcef
+
+object ResourceLoader {
+ class Resource(
+ val content: ByteArray,
+ val type: String? = null
+ )
+
+ fun loadInternalResource(cls: Class, path: String, contentType: String?): Resource? {
+ return cls.getResourceAsStream(path)?.use {
+ Resource(it.readBytes(), contentType)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/jcef/SimulatorJCEFHtmlPanel.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/jcef/SimulatorJCEFHtmlPanel.kt
new file mode 100644
index 0000000..4f3c62c
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/ui/jcef/SimulatorJCEFHtmlPanel.kt
@@ -0,0 +1,20 @@
+package com.github.jozott00.wokwiintellij.ui.jcef
+
+import com.github.jozott00.wokwiintellij.jcef.impl.JcefBrowserPipe
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.util.Disposer
+import com.intellij.ui.jcef.JCEFHtmlPanel
+
+class SimulatorJCEFHtmlPanel(parentDisposable: Disposable) :
+ JCEFHtmlPanel(true, null, null) {
+
+ val browserPipe = JcefBrowserPipe(this, this)
+
+ init {
+ Disposer.register(parentDisposable, this)
+ val resource = ResourceLoader.loadInternalResource(this.javaClass, "/jcef/simulator/index.html", "text/html")
+ super.loadHTML(resource?.content?.toString(Charsets.UTF_8) ?: "Not Found
")
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/ToolWindowUtils.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/ToolWindowUtils.kt
new file mode 100644
index 0000000..c5332a7
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/ToolWindowUtils.kt
@@ -0,0 +1,19 @@
+package com.github.jozott00.wokwiintellij.utils
+
+import com.github.jozott00.wokwiintellij.WokwiConstants
+import com.github.jozott00.wokwiintellij.ui.WokwiIcons
+import com.intellij.execution.runners.ExecutionUtil
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.wm.ToolWindowManager
+
+object ToolWindowUtils {
+
+ fun setSimulatorIcon(project: Project, live: Boolean) {
+ val toolWindow = ToolWindowManager.getInstance(project).getToolWindow(WokwiConstants.TOOL_WINDOW_SIM_ID)
+ var icon = WokwiIcons.SimulatorToolWindowIcon
+ if (live)
+ icon = ExecutionUtil.getLiveIndicator(icon)
+ toolWindow?.setIcon(icon)
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/WokwiNotifier.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/WokwiNotifier.kt
index 49fa244..47ec987 100644
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/WokwiNotifier.kt
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/WokwiNotifier.kt
@@ -1,19 +1,55 @@
package com.github.jozott00.wokwiintellij.utils
-import com.intellij.notification.NotificationGroup
-import com.intellij.notification.NotificationGroupManager
-import com.intellij.notification.NotificationType
-import com.intellij.openapi.project.Project
+import com.github.jozott00.wokwiintellij.exceptions.GenericError
+import com.intellij.notification.*
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.application.EDT
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
object WokwiNotifier {
- private val NOTIFICATION_GROUP = "Wokwi Simulator"
+ private const val NOTIFICATION_GROUP = "Wokwi Simulator"
- fun notifyBalloon(message: String, project: Project, type: NotificationType = NotificationType.INFORMATION) {
- NotificationGroupManager.getInstance()
- .getNotificationGroup(NOTIFICATION_GROUP)
- .createNotification(message, type)
- .notify(project)
+
+ fun notifyBalloon(
+ title: String,
+ message: String = "",
+ type: NotificationType = NotificationType.INFORMATION,
+ action: NotifyAction? = null
+ ) {
+ val notification = pluginNotifications().createNotification(title, message, type)
+ action?.let { notification.addAction(it) }
+ Notifications.Bus.notify(notification)
+ }
+
+ suspend fun notifyBalloonAsync(error: GenericError, action: NotifyAction? = null) {
+ notifyBalloonAsync(error.title, error.message, NotificationType.ERROR, action)
+ }
+
+ suspend fun notifyBalloonAsync(
+ title: String,
+ message: String = "",
+ type: NotificationType = NotificationType.INFORMATION,
+ action: NotifyAction? = null
+ ) {
+ withContext(Dispatchers.EDT) {
+ val notification = pluginNotifications().createNotification(title, message, type)
+ action?.let { notification.addAction(it) }
+ Notifications.Bus.notify(notification)
+ }
+ }
+
+ private fun pluginNotifications(): NotificationGroup {
+ return NotificationGroupManager.getInstance().getNotificationGroup(NOTIFICATION_GROUP)
+ }
+
+}
+
+
+class NotifyAction(text: String, val action: (AnActionEvent, Notification) -> Unit) : NotificationAction(text) {
+ override fun actionPerformed(e: AnActionEvent, notification: Notification) {
+ action(e, notification)
}
-}
\ No newline at end of file
+}
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/WokwiTemplates.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/WokwiTemplates.kt
new file mode 100644
index 0000000..b23e19e
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/WokwiTemplates.kt
@@ -0,0 +1,29 @@
+package com.github.jozott00.wokwiintellij.utils
+
+import org.intellij.lang.annotations.Language
+
+object WokwiTemplates {
+
+ fun defaultDiagramJson(): String {
+ @Language("JSON")
+ val diagram = """
+ {
+ "version": 1,
+ "editor": "wokwi",
+ "author": "Cool Dude",
+ "parts": [{
+ "type": "board-esp32-s3-devkitc-1",
+ "id": "esp",
+ "top": 0.59,
+ "left": 0.67,
+ "attrs": {
+ "flashSize": "16"
+ }
+ }],
+ "connections": [ [ "esp:TX", "${'$'}serialMonitor:RX", "", [] ], [ "esp:RX", "${'$'}serialMonitor:TX", "", [] ] ]
+ }
+ """.trimIndent()
+
+ return diagram
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/applicationUtils.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/applicationUtils.kt
new file mode 100644
index 0000000..97b8074
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/applicationUtils.kt
@@ -0,0 +1,7 @@
+package com.github.jozott00.wokwiintellij.utils
+
+import com.intellij.openapi.application.ApplicationManager
+
+fun runInBackground(task: () -> Unit) {
+ ApplicationManager.getApplication().executeOnPooledThread(task)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/coroutineUtils.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/coroutineUtils.kt
new file mode 100644
index 0000000..4078c19
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/coroutineUtils.kt
@@ -0,0 +1,20 @@
+package com.github.jozott00.wokwiintellij.utils
+
+import kotlinx.coroutines.suspendCancellableCoroutine
+import java.io.Closeable
+import kotlin.coroutines.resume
+
+@Suppress("unused")
+suspend inline fun T.useCancellably(
+ crossinline block: (T) -> R
+): R = suspendCancellableCoroutine { cont ->
+ cont.invokeOnCancellation { this?.close() }
+ cont.resume(use(block))
+}
+
+suspend inline fun T.runCloseable(
+ crossinline block: (T) -> R
+): R = suspendCancellableCoroutine { cont ->
+ cont.invokeOnCancellation { this?.close() }
+ cont.resume(block(this))
+}
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/simulation/FirmwareUtils.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/simulation/FirmwareUtils.kt
new file mode 100644
index 0000000..0a718a0
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/simulation/FirmwareUtils.kt
@@ -0,0 +1,112 @@
+package com.github.jozott00.wokwiintellij.utils.simulation
+
+import arrow.core.Either
+import arrow.core.left
+import arrow.core.right
+import com.github.jozott00.wokwiintellij.exceptions.GenericError
+import com.github.jozott00.wokwiintellij.exceptions.catchIllArg
+import com.github.jozott00.wokwiintellij.extensions.hexStringToByteArray
+import com.github.jozott00.wokwiintellij.simulator.args.FirmwareFormat
+import com.intellij.openapi.application.readAction
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.diagnostic.thisLogger
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.openapi.vfs.readBytes
+import com.intellij.openapi.vfs.readText
+import io.ktor.util.collections.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+
+object FirmwareUtils {
+
+ private const val MAX_FIRMWARE_SIZE = 16 * 1024 * 1024
+ private val LOG = logger()
+
+ private val jsonParser = Json { ignoreUnknownKeys = true }
+
+ suspend fun packEspIdfFirmware(flasherArgs: VirtualFile): Either = withContext(Dispatchers.IO) {
+ LOG.info("Packing ESP-IDF image from flasher-args '${flasherArgs.path}'")
+ fun buildErrorResult(message: String) = GenericError("Failed to build image from flasher_args.json", message).left()
+
+ val flasherArgsString = readAction { flasherArgs.readText() }
+ val flasherJson = catchIllArg { jsonParser.decodeFromString(flasherArgsString) }
+ .getOrElse {
+ thisLogger().warn(it)
+ return@withContext buildErrorResult("Unable to parse content of flasher_args.json") }
+
+
+ val partPaths = ConcurrentSet()
+
+ // list of (offset, data)
+ val firmwareParts = flasherJson.flashFiles.entries.map {e ->
+ val offset = e.key.removePrefix("0x").toIntOrNull(16)
+ ?: return@withContext buildErrorResult("Offset '${e.key}' is invalid")
+
+ val partFile = flasherArgs.parent.findFileByRelativePath(e.value)
+ ?: return@withContext buildErrorResult("Firmware part '${e.value}' could not be found.")
+
+ val data = readAction { partFile.readBytes() }
+ partPaths.add(partFile.path)
+ Pair(offset, data)
+ }
+
+ val firmwareSize = firmwareParts.maxOf { it.first + it.second.size }
+ if (firmwareSize > MAX_FIRMWARE_SIZE)
+ return@withContext buildErrorResult("Firmware size ($firmwareSize bytes) exceeds the maximum supported size ($MAX_FIRMWARE_SIZE bytes)")
+
+ val firmwareData = ByteArray(firmwareSize)
+ firmwareParts.forEach { (offset, data) ->
+ data.copyInto(firmwareData, offset)
+ }
+
+ EspIdfPackResult(firmwareData, partPaths.toList()).right()
+ }
+
+ fun determineFirmwareFormat(file: VirtualFile, content: ByteArray): FirmwareFormat {
+ val isUf2 = content.size >= 512 && isUf2Block(content.sliceArray(0 until 512))
+ if (isUf2)
+ return FirmwareFormat.UF2
+
+ val fileExtension = file.extension?.lowercase()
+ val format = when (fileExtension) {
+ "hex" -> FirmwareFormat.HEX
+ else -> FirmwareFormat.BIN
+ }
+
+ return format
+ }
+
+ private fun isUf2Block(block: ByteArray): Boolean {
+ if (block.size != 512)
+ return false
+
+ val firstMagicNumber = block.sliceArray(0 until 4)
+ val expectedFirstMagicNumber = "0x0A324655".hexStringToByteArray()!!
+ if (!firstMagicNumber.contentEquals(expectedFirstMagicNumber))
+ return false
+
+ val secondMagicNumber = block.sliceArray(4 until 8)
+ val expectedSecondMagicNumber = "0x9E5D5157".hexStringToByteArray()!!
+ if (!secondMagicNumber.contentEquals(expectedSecondMagicNumber))
+ return false
+
+ val finalMagicNumber = block.sliceArray(block.size - 4 until block.size)
+ val expectedFinalMagicNumber = "0x0AB16F30".hexStringToByteArray()!!
+ return finalMagicNumber.contentEquals(expectedFinalMagicNumber)
+ }
+
+
+ class EspIdfPackResult(
+ val img: ByteArray,
+ val binaryPaths: List
+ )
+}
+
+@Serializable
+private data class FlasherJson(
+ @SerialName("flash_files")
+ val flashFiles: Map
+)
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/wokwiServer/WokwiCommand.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/wokwiServer/WokwiCommand.kt
deleted file mode 100644
index b015ca9..0000000
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/wokwiServer/WokwiCommand.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.github.jozott00.wokwiintellij.wokwiServer
-
-import espimg.RomSegment
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.JsonElement
-import kotlinx.serialization.json.JsonPrimitive
-import java.util.*
-
-
-@Serializable
-data class WokwiCommand(
- val type: String,
- val elf: String,
- val espBin: List>
-) {
- companion object {
- fun start(bytes: ByteArray, segments: List): WokwiCommand {
- val bootloader = segments[0]
- val partitionTable = segments[1]
- val app = segments[2]
-
- return WokwiCommand(
- type = "start",
- elf = bytes.toBase64(),
- espBin = listOf(
- romSegToVec(bootloader),
- romSegToVec(partitionTable),
- romSegToVec(app),
- )
- )
- }
-
- private fun romSegToVec(romSeg: RomSegment): List {
- return listOf(
- JsonPrimitive(romSeg.addr),
- JsonPrimitive(romSeg.data.toBase64()),
- )
- }
- }
-}
-
-private fun ByteArray.toBase64(): String =
- String(Base64.getEncoder().encode(this))
-
-
-
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/wokwiServer/WokwiServer.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/wokwiServer/WokwiServer.kt
deleted file mode 100644
index 674b8f7..0000000
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/wokwiServer/WokwiServer.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.github.jozott00.wokwiintellij.wokwiServer
-
-import com.github.jozott00.wokwiintellij.services.WokwiProjectService
-import com.github.jozott00.wokwiintellij.services.WokwiSimulationService
-import com.intellij.openapi.components.service
-import com.intellij.openapi.components.services
-import com.intellij.openapi.diagnostic.thisLogger
-import com.intellij.openapi.project.Project
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.json.JsonObject
-import org.java_websocket.WebSocket
-import org.java_websocket.handshake.ClientHandshake
-import org.java_websocket.server.WebSocketServer
-import java.net.InetSocketAddress
-
-
-// WokwiServer class
-class WokwiServer(port: Int, project: Project) : WebSocketServer(InetSocketAddress(port)) {
-
- val service = project.service()
-
- override fun onOpen(conn: WebSocket, handshake: ClientHandshake) {
- service.connect(conn)
- }
-
- override fun onClose(conn: WebSocket, code: Int, reason: String, remote: Boolean) {
- service.disconnect(conn)
- }
-
- override fun onMessage(conn: WebSocket, message: String) {
- val data = Json.parseToJsonElement(message)
- require(data is JsonObject) { "Failed to parse received message $message" }
- this.service.messageReceived(data, conn)
- }
-
- override fun onStart() {
-
- }
-
- override fun onError(conn: WebSocket?, ex: Exception) {
- ex.printStackTrace()
- }
-}
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index e93bcfe..e9cc48c 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -5,39 +5,52 @@
jozott00
com.intellij.modules.platform
+ org.toml.lang
- messages.MyBundle
+ messages.WokwiBundle
+
+
-
-
+
+
+
+
+
+
+
-
-
-
+
+
+
+ description="Restart Wokwi simulation"
+ text="Restart Wokwi Simulation"
+ icon="AllIcons.Actions.Rerun"
+ />`
@@ -62,6 +75,8 @@
+
+
diff --git a/src/main/resources/META-INF/pluginIconColorful.svg b/src/main/resources/META-INF/pluginIconColorful.svg
new file mode 100644
index 0000000..9f76a06
--- /dev/null
+++ b/src/main/resources/META-INF/pluginIconColorful.svg
@@ -0,0 +1,16 @@
+
diff --git a/src/main/resources/META-INF/toml-only.xml b/src/main/resources/META-INF/toml-only.xml
new file mode 100644
index 0000000..e4103c2
--- /dev/null
+++ b/src/main/resources/META-INF/toml-only.xml
@@ -0,0 +1,37 @@
+
+ messages.WokwiBundle
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/icons/logIcon@13x13.svg b/src/main/resources/icons/logIcon@13x13.svg
new file mode 100644
index 0000000..a0daaee
--- /dev/null
+++ b/src/main/resources/icons/logIcon@13x13.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/main/resources/icons/logIcon@13x13_dark.svg b/src/main/resources/icons/logIcon@13x13_dark.svg
new file mode 100644
index 0000000..88d55b2
--- /dev/null
+++ b/src/main/resources/icons/logIcon@13x13_dark.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/main/resources/icons/logIcon@16x16.svg b/src/main/resources/icons/logIcon@16x16.svg
new file mode 100644
index 0000000..462665d
--- /dev/null
+++ b/src/main/resources/icons/logIcon@16x16.svg
@@ -0,0 +1,12 @@
+
diff --git a/src/main/resources/icons/logIcon@16x16_dark.svg b/src/main/resources/icons/logIcon@16x16_dark.svg
new file mode 100644
index 0000000..58d7e20
--- /dev/null
+++ b/src/main/resources/icons/logIcon@16x16_dark.svg
@@ -0,0 +1,12 @@
+
diff --git a/src/main/resources/icons/logIcon@20x20.svg b/src/main/resources/icons/logIcon@20x20.svg
new file mode 100644
index 0000000..20e2970
--- /dev/null
+++ b/src/main/resources/icons/logIcon@20x20.svg
@@ -0,0 +1,12 @@
+
diff --git a/src/main/resources/icons/logIcon@20x20_dark.svg b/src/main/resources/icons/logIcon@20x20_dark.svg
new file mode 100644
index 0000000..bdbc051
--- /dev/null
+++ b/src/main/resources/icons/logIcon@20x20_dark.svg
@@ -0,0 +1,12 @@
+
diff --git a/src/main/resources/icons/pluginIcon.svg b/src/main/resources/icons/pluginIcon.svg
index 7f3b453..023e313 100644
--- a/src/main/resources/icons/pluginIcon.svg
+++ b/src/main/resources/icons/pluginIcon.svg
@@ -1,45 +1,10 @@
-
-
-