diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c40365758..79cb4ad15 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -124,6 +124,7 @@ dependencies { testImplementation(libs.junitJupiterParams) implementation(libs.clikt) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") } tasks.test { @@ -419,7 +420,6 @@ tasks.register("renameWindres") { } tasks.register("includeProcessingResources"){ dependsOn( - "includeJdk", "includeCore", "includeJavaMode", "includeSharedAssets", @@ -428,6 +428,7 @@ tasks.register("includeProcessingResources"){ "includeJavaModeResources", "renameWindres" ) + mustRunAfter("includeJdk") finalizedBy("signResources") } @@ -534,6 +535,7 @@ afterEvaluate { dependsOn("includeProcessingResources") } tasks.named("createDistributable").configure { + dependsOn("includeJdk") finalizedBy("setExecutablePermissions") } } diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index b5aa599b9..4d57135c9 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -166,18 +166,6 @@ static public void main(final String[] args) { static private void createAndShowGUI(String[] args) { // these times are fairly negligible relative to Base. // long t1 = System.currentTimeMillis(); - var preferences = java.util.prefs.Preferences.userRoot().node("org/processing/app"); - var installLocations = new ArrayList<>(List.of(preferences.get("installLocations", "").split(","))); - var installLocation = System.getProperty("user.dir") + "^" + Base.getVersionName(); - - // Check if the installLocation is already in the list - if (!installLocations.contains(installLocation)) { - // Add the installLocation to the list - installLocations.add(installLocation); - - // Save the updated list back to preferences - preferences.put("installLocations", String.join(",", installLocations)); - } // TODO: Cleanup old locations if no longer installed // TODO: Cleanup old locations if current version is installed in the same location diff --git a/app/src/processing/app/Processing.kt b/app/src/processing/app/Processing.kt index 11555edf5..bb05a72f3 100644 --- a/app/src/processing/app/Processing.kt +++ b/app/src/processing/app/Processing.kt @@ -10,7 +10,12 @@ import com.github.ajalt.clikt.parameters.arguments.multiple import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.help import com.github.ajalt.clikt.parameters.options.option +import processing.app.api.Contributions +import processing.app.api.Sketchbook import processing.app.ui.Start +import java.io.File +import java.util.prefs.Preferences +import kotlin.concurrent.thread class Processing: SuspendingCliktCommand("processing"){ val version by option("-v","--version") @@ -29,6 +34,11 @@ class Processing: SuspendingCliktCommand("processing"){ return } + thread { + // Update the install locations in preferences + updateInstallLocations() + } + val subcommand = currentContext.invokedSubcommand if (subcommand == null) { Start.main(sketches.toTypedArray()) @@ -40,7 +50,9 @@ suspend fun main(args: Array){ Processing() .subcommands( LSP(), - LegacyCLI(args) + LegacyCLI(args), + Contributions(), + Sketchbook() ) .main(args) } @@ -49,10 +61,13 @@ class LSP: SuspendingCliktCommand("lsp"){ override fun help(context: Context) = "Start the Processing Language Server" override suspend fun run(){ try { + // run in headless mode + System.setProperty("java.awt.headless", "true") + // Indirect invocation since app does not depend on java mode Class.forName("processing.mode.java.lsp.PdeLanguageServer") .getMethod("main", Array::class.java) - .invoke(null, *arrayOf(emptyList())) + .invoke(null, arrayOf()) } catch (e: Exception) { throw InternalError("Failed to invoke main method", e) } @@ -76,9 +91,8 @@ class LegacyCLI(val args: Array): SuspendingCliktCommand( "cli"){ override suspend fun run(){ val cliArgs = args.filter { it != "cli" } try { - if(build){ - System.setProperty("java.awt.headless", "true") - } + System.setProperty("java.awt.headless", "true") + // Indirect invocation since app does not depend on java mode Class.forName("processing.mode.java.Commander") .getMethod("main", Array::class.java) @@ -87,4 +101,50 @@ class LegacyCLI(val args: Array): SuspendingCliktCommand( "cli"){ throw InternalError("Failed to invoke main method", e) } } +} + +fun updateInstallLocations(){ + val preferences = Preferences.userRoot().node("org/processing/app") + val installLocations = preferences.get("installLocations", "") + .split(",") + .dropLastWhile { it.isEmpty() } + .filter { install -> + try{ + val (path, version) = install.split("^") + val file = File(path) + if(!file.exists() || file.isDirectory){ + return@filter false + } + // call the path to check if it is a valid install location + val process = ProcessBuilder(path, "--version") + .redirectErrorStream(true) + .start() + val exitCode = process.waitFor() + if(exitCode != 0){ + return@filter false + } + val output = process.inputStream.bufferedReader().readText() + return@filter output.contains(version) + } catch (e: Exception){ + false + } + } + .toMutableList() + val command = ProcessHandle.current().info().command() + if(command.isEmpty) { + return + } + val installLocation = "${command.get()}^${Base.getVersionName()}" + + + // Check if the installLocation is already in the list + if (installLocations.contains(installLocation)) { + return + } + + // Add the installLocation to the list + installLocations.add(installLocation) + + // Save the updated list back to preferences + preferences.put("installLocations", java.lang.String.join(",", installLocations)) } \ No newline at end of file diff --git a/app/src/processing/app/api/Contributions.kt b/app/src/processing/app/api/Contributions.kt new file mode 100644 index 000000000..80bfa502f --- /dev/null +++ b/app/src/processing/app/api/Contributions.kt @@ -0,0 +1,63 @@ +package processing.app.api + +import com.github.ajalt.clikt.command.SuspendingCliktCommand +import com.github.ajalt.clikt.core.Context +import com.github.ajalt.clikt.core.subcommands +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import processing.app.Base +import processing.app.api.Sketch.Companion.getSketches +import java.io.File + +class Contributions: SuspendingCliktCommand(){ + override fun help(context: Context) = "Manage Processing contributions" + override suspend fun run() { + System.setProperty("java.awt.headless", "true") + } + init { + subcommands(Examples()) + } + + class Examples: SuspendingCliktCommand("examples") { + override fun help(context: Context) = "Manage Processing examples" + override suspend fun run() { + } + init { + subcommands(ExamplesList()) + } + } + + class ExamplesList: SuspendingCliktCommand("list") { + + + val serializer = Json { + prettyPrint = true + } + + override fun help(context: Context) = "List all examples" + override suspend fun run() { + + val examplesFolder = Base.getSketchbookExamplesFolder() + + // TODO: Decouple modes listing from `Base` class, defaulting to Java mode for now + val resourcesDir = System.getProperty("compose.application.resources.dir") + val javaMode = "$resourcesDir/modes/java" + val javaModeExamples = "$javaMode/examples" + + val javaExamples = getSketches(File(javaModeExamples)) + + val json = serializer.encodeToString(listOf(javaExamples)) + println(json) + + // Build-in examples for each mode + // Get examples for core libraries + + // Examples downloaded in the sketchbook + // Library contributions + // Mode examples + } + + + } +} \ No newline at end of file diff --git a/app/src/processing/app/api/Sketch.kt b/app/src/processing/app/api/Sketch.kt new file mode 100644 index 000000000..a0e740014 --- /dev/null +++ b/app/src/processing/app/api/Sketch.kt @@ -0,0 +1,48 @@ +package processing.app.api + +import kotlinx.serialization.Serializable +import java.io.File + +class Sketch { + companion object{ + @Serializable + data class Sketch( + val type: String = "sketch", + val name: String, + val path: String, + val mode: String = "java", + ) + + @Serializable + data class Folder( + val type: String = "folder", + val name: String, + val path: String, + val mode: String = "java", + val children: List = emptyList(), + val sketches: List = emptyList() + ) + + fun getSketches(file: File, filter: (File) -> Boolean = { true }): Folder { + val name = file.name + val (sketchesFolders, childrenFolders) = file.listFiles()?.partition { isSketchFolder(it) } ?: return Folder( + name = name, + path = file.absolutePath, + sketches = emptyList(), + children = emptyList() + ) + + val children = childrenFolders.filter(filter) .map { getSketches(it) } + val sketches = sketchesFolders.map { Sketch(name = it.name, path = it.absolutePath) } + return Folder( + name = name, + path = file.absolutePath, + children = children, + sketches = sketches + ) + } + fun isSketchFolder(file: File): Boolean { + return file.isDirectory && file.listFiles().any { it.isFile && it.name.endsWith(".pde") } + } + } +} diff --git a/app/src/processing/app/api/Sketchbook.kt b/app/src/processing/app/api/Sketchbook.kt new file mode 100644 index 000000000..43d1cc6ce --- /dev/null +++ b/app/src/processing/app/api/Sketchbook.kt @@ -0,0 +1,42 @@ +package processing.app.api + +import com.github.ajalt.clikt.command.SuspendingCliktCommand +import com.github.ajalt.clikt.core.Context +import com.github.ajalt.clikt.core.subcommands +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import processing.app.Platform +import processing.app.Preferences +import processing.app.api.Sketch.Companion.getSketches +import java.io.File + +class Sketchbook: SuspendingCliktCommand() { + + + override fun help(context: Context) = "Manage the sketchbook" + override suspend fun run() { + System.setProperty("java.awt.headless", "true") + } + init { + subcommands(SketchbookList()) + } + + + class SketchbookList: SuspendingCliktCommand("list") { + val serializer = Json { + prettyPrint = true + } + + override fun help(context: Context) = "List all sketches" + override suspend fun run() { + Platform.init() + // TODO: Allow the user to change the sketchbook location + // TODO: Currently blocked since `Base.getSketchbookFolder()` is not available in headless mode + val sketchbookFolder = Platform.getDefaultSketchbookFolder() + + val sketches = getSketches(sketchbookFolder.resolve("sketchbook")) + val json = serializer.encodeToString(listOf(sketches)) + println(json) + } + } +} \ No newline at end of file