diff --git a/.idea/gradle.xml b/.idea/gradle.xml
deleted file mode 100644
index ce1c62c..0000000
--- a/.idea/gradle.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index bf0c939..481bad5 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -11,6 +11,7 @@ plugins {
alias(libs.plugins.changelog) // Gradle Changelog Plugin
alias(libs.plugins.qodana) // Gradle Qodana Plugin
alias(libs.plugins.kover) // Gradle Kover Plugin
+ kotlin("plugin.serialization") version "1.9.22"
}
group = properties("pluginGroup").get()
@@ -18,12 +19,15 @@ version = properties("pluginVersion").get()
// Configure project's dependencies
repositories {
+ mavenLocal()
mavenCentral()
}
// Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog
dependencies {
-// implementation(libs.annotations)
+ 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")
}
// Set the JVM language level used to build the project. Use Java 11 for 2020.3+, and Java 17 for 2022.2+.
@@ -73,6 +77,24 @@ tasks {
gradleVersion = properties("gradleVersion").get()
}
+ runIde {
+ autoReloadPlugins.set(true)
+ }
+
+ // to allow hot reload of sandbox using buildPlugin
+ buildSearchableOptions {
+ enabled = false
+ }
+
+ // Set the JVM compatibility versions
+ withType {
+ sourceCompatibility = "17"
+ targetCompatibility = "17"
+ }
+ withType {
+ kotlinOptions.jvmTarget = "17"
+ }
+
patchPluginXml {
version = properties("pluginVersion")
sinceBuild = properties("pluginSinceBuild")
@@ -83,7 +105,7 @@ tasks {
val start = ""
val end = ""
- with (it.lines()) {
+ with(it.lines()) {
if (!containsAll(listOf(start, end))) {
throw GradleException("Plugin description section not found in README.md:\n$start ... $end")
}
@@ -126,6 +148,7 @@ 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/gradle.properties b/gradle.properties
index 90ba70c..49a8a62 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -12,7 +12,7 @@ pluginUntilBuild = 233.*
# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
platformType = IC
-platformVersion = 2022.3.3
+platformVersion = 2023.3.2
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
diff --git a/libs/espimg-0.1.0.jar b/libs/espimg-0.1.0.jar
new file mode 100644
index 0000000..f9eb525
Binary files /dev/null and b/libs/espimg-0.1.0.jar differ
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 29763db..d597ebc 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,3 +1,4 @@
+
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0"
}
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiRestartAction.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiRestartAction.kt
new file mode 100644
index 0000000..be49a63
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiRestartAction.kt
@@ -0,0 +1,14 @@
+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()
+ }
+
+}
\ 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
new file mode 100644
index 0000000..4e33042
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiStartAction.kt
@@ -0,0 +1,16 @@
+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() {
+ override fun actionPerformed(event: AnActionEvent) {
+ val s = event.project?.service()
+ s?.startSimulator()
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiStopAction.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiStopAction.kt
new file mode 100644
index 0000000..4cf7d7f
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiStopAction.kt
@@ -0,0 +1,18 @@
+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
+import com.intellij.openapi.ui.Messages
+
+class WokwiStopAction : AnAction() {
+ override fun actionPerformed(event: AnActionEvent) {
+ val s = event.project?.service()
+ s?.stopSimulator()
+
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiWatchAction.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiWatchAction.kt
new file mode 100644
index 0000000..46fca37
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiWatchAction.kt
@@ -0,0 +1,24 @@
+package com.github.jozott00.wokwiintellij.actions
+
+import com.github.jozott00.wokwiintellij.services.WokwiProjectService
+import com.github.jozott00.wokwiintellij.states.WokwiConfigState
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.actionSystem.ToggleAction
+import com.intellij.openapi.components.service
+
+class WokwiWatchAction : ToggleAction() {
+
+ override fun isSelected(p0: AnActionEvent): Boolean {
+ return p0.project?.service()?.state?.watchElf ?: 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()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/MyApplicationActivationListener.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiActivationListener.kt
similarity index 84%
rename from src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/MyApplicationActivationListener.kt
rename to src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiActivationListener.kt
index 666a6cc..77ff3b5 100644
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/MyApplicationActivationListener.kt
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiActivationListener.kt
@@ -4,7 +4,7 @@ import com.intellij.openapi.application.ApplicationActivationListener
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.wm.IdeFrame
-internal class MyApplicationActivationListener : ApplicationActivationListener {
+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/WokwiElfFileListener.kt
new file mode 100644
index 0000000..9fdbd0b
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiElfFileListener.kt
@@ -0,0 +1,38 @@
+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.intellij.openapi.components.service
+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 {
+
+
+ override fun after(events: MutableList) {
+ val configState = project.service()
+
+ if (!configState.watchElf) return
+
+ val watchPath = configState.elfPath
+
+ val result = events.find {
+ if (it.file?.isInLocalFileSystem != true)
+ return@find false
+
+ if (it.file?.path == watchPath)
+ return@find true
+
+ false
+ }
+
+ val projectService = project.service()
+ if (result != null) {
+ projectService.elfFileUpdate()
+ }
+ }
+
+}
\ 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
new file mode 100644
index 0000000..4f0cc6c
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiPostStartupActivity.kt
@@ -0,0 +1,14 @@
+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
new file mode 100644
index 0000000..e49020a
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiProjectManagerListener.kt
@@ -0,0 +1,11 @@
+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/services/MyProjectService.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/MyProjectService.kt
deleted file mode 100644
index 279c8c2..0000000
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/MyProjectService.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.github.jozott00.wokwiintellij.services
-
-import com.intellij.openapi.components.Service
-import com.intellij.openapi.diagnostic.thisLogger
-import com.intellij.openapi.project.Project
-import com.github.jozott00.wokwiintellij.MyBundle
-
-@Service(Service.Level.PROJECT)
-class MyProjectService(project: Project) {
-
- init {
- thisLogger().info(MyBundle.message("projectService", project.name))
- thisLogger().warn("Don't forget to remove all non-needed sample code files with their corresponding registration entries in `plugin.xml`.")
- }
-
- fun getRandomNumber() = (1..100).random()
-}
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiComponentService.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiComponentService.kt
new file mode 100644
index 0000000..da8fc50
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiComponentService.kt
@@ -0,0 +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.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()
+
+ val simulatorPanel = SimulatorPanel()
+ val configPanel = wokwiConfigPanel(wokwiConfigState.state) {
+ onChangeAction = {
+ println("Changes in model: ${wokwiConfigState.state}")
+ }
+ }
+
+ val toolWindow = WokwiToolWindow(configPanel, simulatorPanel)
+
+}
\ 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
new file mode 100644
index 0000000..419b5bc
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiDataService.kt
@@ -0,0 +1,95 @@
+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/WokwiProjectService.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiProjectService.kt
new file mode 100644
index 0000000..2fc3145
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiProjectService.kt
@@ -0,0 +1,76 @@
+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.wokwiServer.WokwiServer
+import com.intellij.openapi.Disposable
+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.VirtualFileManager
+import com.intellij.util.messages.MessageBusConnection
+
+@Service(Service.Level.PROJECT)
+class WokwiProjectService(val project: Project) : Disposable {
+ private var server: WokwiServer? = 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
+ }
+
+ componentService.toolWindow.showSimulation()
+ simulationRunning = true
+ watchStart()
+ }
+
+ fun stopSimulator() {
+ componentService.toolWindow.showConfig()
+ simulationRunning = false
+ watchStop()
+ }
+
+ fun startup() {
+ val port = 9012 // Specify your port here
+ server = WokwiServer(port, project).apply {
+ start()
+ println("WokwiServer started on port: $port")
+ }
+ }
+
+ override fun dispose() {
+ server?.stop()
+ }
+
+ fun restartSimulation() {
+ simulationService.restartAll()
+ }
+
+ fun elfFileUpdate() {
+ println("FILE UPDATED ... restart")
+ simulationService.restartAll()
+ }
+
+ fun watchStart() {
+ if (!configState.watchElf || !simulationRunning) return
+ println("START WATCHING")
+ msgBusConnection = project.messageBus.connect()
+ msgBusConnection?.subscribe(VirtualFileManager.VFS_CHANGES, WokwiElfFileListener(project))
+ }
+
+ fun watchStop() {
+ println("STOP WATCHING")
+ msgBusConnection?.disconnect()
+ msgBusConnection = null
+ }
+
+
+}
\ 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
new file mode 100644
index 0000000..31517bb
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiSimulationService.kt
@@ -0,0 +1,59 @@
+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/states/WokwiConfigState.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/states/WokwiConfigState.kt
new file mode 100644
index 0000000..477b816
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/states/WokwiConfigState.kt
@@ -0,0 +1,45 @@
+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/toolWindow/JCEFContent: JPanel().kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/JCEFContent: JPanel().kt
new file mode 100644
index 0000000..1a716a0
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/JCEFContent: JPanel().kt
@@ -0,0 +1,63 @@
+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/MyToolWindowFactory.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/MyToolWindowFactory.kt
deleted file mode 100644
index 718c024..0000000
--- a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/MyToolWindowFactory.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.github.jozott00.wokwiintellij.toolWindow
-
-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.components.JBLabel
-import com.intellij.ui.components.JBPanel
-import com.intellij.ui.content.ContentFactory
-import com.github.jozott00.wokwiintellij.MyBundle
-import com.github.jozott00.wokwiintellij.services.MyProjectService
-import javax.swing.JButton
-
-
-class MyToolWindowFactory : 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 myToolWindow = MyToolWindow(toolWindow)
- val content = ContentFactory.getInstance().createContent(myToolWindow.getContent(), null, false)
- toolWindow.contentManager.addContent(content)
- }
-
- override fun shouldBeAvailable(project: Project) = true
-
- class MyToolWindow(toolWindow: ToolWindow) {
-
- private val service = toolWindow.project.service()
-
- fun getContent() = JBPanel>().apply {
- val label = JBLabel(MyBundle.message("randomLabel", "?"))
-
- add(label)
- add(JButton(MyBundle.message("shuffle")).apply {
- addActionListener {
- label.text = MyBundle.message("randomLabel", service.getRandomNumber())
- }
- })
- }
- }
-}
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/SimulatorPanel.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/SimulatorPanel.kt
new file mode 100644
index 0000000..0cbb88c
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/SimulatorPanel.kt
@@ -0,0 +1,100 @@
+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
new file mode 100644
index 0000000..f614881
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/SimulatorWindowFactory.kt
@@ -0,0 +1,34 @@
+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 content = ContentFactory.getInstance()
+ .createContent(toolWindowContent, null, false)
+ toolWindow.contentManager.addContent(content)
+
+ }
+
+ override fun shouldBeAvailable(project: Project) = true
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiConfigPanel.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiConfigPanel.kt
new file mode 100644
index 0000000..894de0a
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiConfigPanel.kt
@@ -0,0 +1,84 @@
+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/WokwiToolWindow.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiToolWindow.kt
new file mode 100644
index 0000000..fcf83a3
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiToolWindow.kt
@@ -0,0 +1,33 @@
+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()
+ }
+
+ fun showConfig() {
+ if (panel.components.contains(configPanel)) return
+ panel.removeAll()
+ panel.add(configPanel)
+ simulationPanel.stopSimulator()
+ panel.revalidate()
+ }
+
+
+}
\ 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
new file mode 100644
index 0000000..49fa244
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/WokwiNotifier.kt
@@ -0,0 +1,19 @@
+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
+
+object WokwiNotifier {
+
+ private 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)
+ }
+
+}
\ 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
new file mode 100644
index 0000000..b015ca9
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/wokwiServer/WokwiCommand.kt
@@ -0,0 +1,46 @@
+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
new file mode 100644
index 0000000..674b8f7
--- /dev/null
+++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/wokwiServer/WokwiServer.kt
@@ -0,0 +1,43 @@
+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 d65904d..e7ac195 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -9,10 +9,60 @@
messages.MyBundle
-
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/kotlin/com/github/jozott00/wokwiintellij/MyPluginTest.kt b/src/test/kotlin/com/github/jozott00/wokwiintellij/MyPluginTest.kt
index 086c2c4..93b4109 100644
--- a/src/test/kotlin/com/github/jozott00/wokwiintellij/MyPluginTest.kt
+++ b/src/test/kotlin/com/github/jozott00/wokwiintellij/MyPluginTest.kt
@@ -6,7 +6,6 @@ import com.intellij.psi.xml.XmlFile
import com.intellij.testFramework.TestDataPath
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.util.PsiErrorElementUtil
-import com.github.jozott00.wokwiintellij.services.MyProjectService
@TestDataPath("\$CONTENT_ROOT/src/test/testData")
class MyPluginTest : BasePlatformTestCase() {
@@ -29,11 +28,6 @@ class MyPluginTest : BasePlatformTestCase() {
myFixture.testRename("foo.xml", "foo_after.xml", "a2")
}
- fun testProjectService() {
- val projectService = project.service()
-
- assertNotSame(projectService.getRandomNumber(), projectService.getRandomNumber())
- }
override fun getTestDataPath() = "src/test/testData/rename"
}