@@ -23,7 +23,6 @@ import kotlinx.html.dom.createHTMLDocument
2323import kotlinx.html.dom.serialize
2424import kotlinx.serialization.json.*
2525import processing.app.*
26- import processing.app.Messages.Companion.log
2726import processing.app.syntax.JEditTextArea
2827import processing.app.syntax.PdeTextArea
2928import processing.app.syntax.PdeTextAreaDefaults
@@ -40,11 +39,11 @@ import javax.swing.JMenu
4039import javax.swing.JMenuItem
4140
4241
43- class p5jsEditor (base : Base , path : String? , state : EditorState ? , mode : Mode ? ): Editor(base, path, state, mode) {
42+ class p5jsEditor (base : Base , path : String? , state : EditorState ? , mode : Mode ? ) : Editor(base, path, state, mode) {
4443
4544 val scope = CoroutineScope (Dispatchers .Default )
4645 val isWindows = System .getProperty(" os.name" ).lowercase().contains(" windows" )
47- var sketchProcess : Process ? = null
46+ var processes : MutableList < Process > = mutableListOf ()
4847
4948 init {
5049 scope.launch {
@@ -57,8 +56,7 @@ class p5jsEditor(base: Base, path: String?, state: EditorState?, mode: Mode?): E
5756 try {
5857 javascriptFolder?.resolve(" package.json" )?.copyTo(sketch.folder.resolve(" package.json" ))
5958 javascriptFolder?.resolve(" pnpm-lock.yaml" )?.copyTo(sketch.folder.resolve(" pnpm-lock.yaml" ))
60- }
61- catch (e: FileAlreadyExistsException ) {
59+ } catch (e: FileAlreadyExistsException ) {
6260 Messages .log(" File already exists: ${e.message} " )
6361 // TODO: How to differentiate example with own `package.json` and saved sketch?
6462 }
@@ -69,21 +67,17 @@ class p5jsEditor(base: Base, path: String?, state: EditorState?, mode: Mode?): E
6967 statusNotice(" Looking for pnpm…" )
7068 try {
7169 runCommand(" pnpm -v" )
72- }
73- catch (e: Exception ) {
70+ } catch (e: Exception ) {
7471 statusNotice(" pnpm not found. Installing pnpm…" )
7572 if (isWindows) {
7673 runCommand(" powershell -command \" Invoke-WebRequest https://get.pnpm.io/install.ps1 -UseBasicParsing | Invoke-Expression\" " )
77- }
78- else {
74+ } else {
7975 runCommand(" chmod u+x ${mode?.folder} /install.sh" )
8076 runCommand(" ${mode?.folder} /install.sh" )
8177 }
8278
8379 statusNotice(" Installing Node via pnpm…" )
84- runCommand(" pnpm env use --global lts" ) {
85- statusNotice(" Installing Node dependencies…" )
86- }
80+ runCommand(" pnpm env use --global lts" )
8781 }
8882
8983 statusNotice(" All done! Enjoy p5.js mode." )
@@ -126,22 +120,13 @@ class p5jsEditor(base: Base, path: String?, state: EditorState?, mode: Mode?): E
126120 runCommand(" pnpm install --dangerously-allow-all-builds --force" )
127121 }
128122
129- runCommand(" pnpm app:pack" ) {
123+ scope.launch {
124+ runCommand(" pnpm app:pack" )
130125 Platform .openFolder(sketch.folder)
131126 statusNotice(Language .text(" export.notice.exporting.done" ))
132127 }
133128 }
134129
135- // override fun handleSaveAs(): Boolean {
136- // val saved = super.handleSaveAs()
137- // statusNotice("Rebuilding Node dependencies…")
138- // TODO: Saving is async and might not be finished once the function returns
139- // runCommand("pnpm install --force", onFinished = {
140- // statusNotice("Rebuilding Node dependencies… Done.")
141- // })
142- // return saved
143- // }
144-
145130 override fun buildSketchMenu (): JMenu {
146131 val runItem = Toolkit .newJMenuItem(Language .text(" menu.sketch.run" ), ' R' .code)
147132 runItem.addActionListener { e: ActionEvent ? -> toolbar.handleRun(0 ) }
@@ -189,12 +174,11 @@ class p5jsEditor(base: Base, path: String?, state: EditorState?, mode: Mode?): E
189174 }
190175
191176 override fun internalCloseRunner () {
192- sketchProcess?.destroy()
193177 }
194178
195179 override fun deactivateRun () {
196- sketchProcess?. destroy()
197- toolbar.deactivateRun ()
180+ processes.forEach { it. destroy() }
181+ updateToolbar ()
198182 }
199183
200184 override fun createFooter (): EditorFooter {
@@ -230,7 +214,8 @@ class p5jsEditor(base: Base, path: String?, state: EditorState?, mode: Mode?): E
230214 val npmResponseRaw = npmConn.getInputStream().readAllBytes().decodeToString()
231215 val npmResponse: JsonObject = Json .decodeFromString(npmResponseRaw)
232216 val npmPackages = npmResponse[" objects" ]!! .jsonArray
233- packagesSearched = npmPackages.map { it.jsonObject[" package" ]!! .jsonObject[" name" ]!! .jsonPrimitive.content }
217+ packagesSearched =
218+ npmPackages.map { it.jsonObject[" package" ]!! .jsonObject[" name" ]!! .jsonPrimitive.content }
234219 packageToInstall = packagesSearched[0 ]
235220 }
236221 })
@@ -249,7 +234,11 @@ class p5jsEditor(base: Base, path: String?, state: EditorState?, mode: Mode?): E
249234 Spacer (Modifier .width(16 .dp))
250235 LazyColumn {
251236 itemsIndexed(packagesSearched) { index, pkg ->
252- Text (text = pkg, fontWeight = if (index == 0 ) FontWeight .Bold else FontWeight .Normal , modifier = Modifier .fillMaxWidth().padding(4 .dp))
237+ Text (
238+ text = pkg,
239+ fontWeight = if (index == 0 ) FontWeight .Bold else FontWeight .Normal ,
240+ modifier = Modifier .fillMaxWidth().padding(4 .dp)
241+ )
253242 Divider ()
254243 }
255244 }
@@ -286,59 +275,96 @@ class p5jsEditor(base: Base, path: String?, state: EditorState?, mode: Mode?): E
286275 sketch.folder.resolve(" electron/index.html" ).writeText(htmlCode)
287276 }
288277
289- fun runCommand (action : String , directory : File = sketch.folder, onFinished : () -> Unit = {}) {
290- try {
291- // TODO: Get rid of magic strings. Better way to distinguish “endless” processes and processes we wait for?
292- if (action == " pnpm sketch:start" ) {
293- sketchProcess?.destroy()
294- }
295-
296- val processBuilder = ProcessBuilder ()
297-
298- // Set the command based on the operating system
299- val shell = System .getenv(" SHELL" )
300- val command = if (isWindows) {
301- listOf (" cmd" , " /c" , action)
302- } else {
303- listOf (shell, " -ci" , action)
304- }
278+ fun updateToolbar () {
279+ if (processes.any { it.isAlive }) {
280+ toolbar.activateRun()
281+ } else {
282+ toolbar.deactivateRun()
283+ }
284+ }
305285
306- processBuilder.command(command)
307- processBuilder.directory(directory)
286+ fun runSketch (present : Boolean ) {
287+ createIndexHtml()
288+ deactivateRun()
289+ statusNotice(" Starting up sketch…" )
308290
309- val process = processBuilder.start()
310291
311- if (action == " pnpm sketch:start" ) {
312- sketchProcess = process;
313- }
292+ scope.launch {
293+ try {
294+ val packageJson = sketch.folder.resolve(" package.json" )
295+ val hashFile = sketch.folder.resolve(" electron/.package_json_hash" )
296+ val newHash = packageJson.readText().hashCode()
297+ val oldHash = hashFile.let { if (it.exists()) it.readText().toInt() else null }
298+ if (newHash != oldHash) {
299+ statusNotice(" Installing Node dependencies…" )
300+ runCommand(" pnpm install --dangerously-allow-all-builds" )
301+ hashFile.writeText(newHash.toString())
302+ }
303+ statusNotice(" Running sketch…" )
304+ val builder = builder(" PRESENT=$present pnpm sketch:start" , sketch.folder)
305+ val process = builder.start()
306+ processes.add(process)
307+ updateToolbar()
308+
309+ // Handle output stream
310+ val reader = BufferedReader (InputStreamReader (process.inputStream))
311+ var line: String?
312+
313+ while (reader.readLine().also { line = it } != null ) {
314+ // TODO: so much refactoring!
315+ // Only check for errors when running the sketch
316+ if (line?.startsWith(" error" ) == true ) {
317+ // TODO: more robust data exchange, double-check with @Stef
318+ // TODO: `statusError` does not do anything with column of a SketchException
319+ val (msgType, msgText, msgFile, msgLine, msgCol) = line.split(" |" )
320+ statusError(processing.utils.SketchException (msgText, 0 , msgLine.toInt() - 1 , msgCol.toInt()))
321+ continue
322+ }
314323
315- // Handle output stream
316- val reader = BufferedReader (InputStreamReader (process.inputStream))
317- var line: String?
318-
319- while (reader.readLine().also { line = it } != null ) {
320- // TODO: so much refactoring!
321- // Only check for errors when running the sketch
322- if (action == " pnpm sketch:start" && line?.startsWith(" error" ) == true ) {
323- // TODO: more robust data exchange, double-check with @Stef
324- // TODO: `statusError` does not do anything with column of a SketchException
325- val ( msgType, msgText, msgFile, msgLine, msgCol ) = line.split(" |" )
326- statusError(processing.utils.SketchException (msgText, 0 , msgLine.toInt()- 1 , msgCol.toInt()))
327- continue
324+ println (line)
328325 }
326+ process.waitFor()
327+ if (processes.lastOrNull() == process) {
328+ statusNotice(" Sketch stopped." )
329+ }
330+ processes.remove(process)
331+ updateToolbar()
329332
330- println (line)
333+ } catch (e: Exception ) {
334+ statusError(" Failed to run sketch: ${e.message} " )
331335 }
336+ }
337+ }
338+
332339
340+ fun runCommand (action : String , directory : File = sketch.folder) {
341+ try {
342+ val processBuilder = builder(action, directory)
343+ val process = processBuilder.start()
344+ // suspend fun to wait for process to finish
333345 val exitCode = process.waitFor()
334346
335347 if (exitCode != 0 ) {
336348 throw RuntimeException (" Command failed with non-zero exit code $exitCode ." )
337349 }
338-
339- onFinished()
340350 } catch (e: Exception ) {
341351 statusError(" Failed to run `$action `: ${e.message} " )
342352 }
343353 }
354+
355+ private fun builder (action : String , directory : File ): ProcessBuilder {
356+ val processBuilder = ProcessBuilder ()
357+
358+ // Set the command based on the operating system
359+ val shell = System .getenv(" SHELL" )
360+ val command = if (isWindows) {
361+ listOf (" cmd" , " /c" , action)
362+ } else {
363+ listOf (shell, " -ci" , action)
364+ }
365+
366+ processBuilder.command(command)
367+ processBuilder.directory(directory)
368+ return processBuilder
369+ }
344370}
0 commit comments